mirror of
https://github.com/lunchcat/sif.git
synced 2026-07-28 22:40:54 -07:00
a detector accuracy audit surfaced two classes of bug in the framework detectors. bare-brand header false positives: header-only signatures matched a brand name as a substring across every header name and value, so a detector fired on any response that merely referenced the brand (a vendor cdn named in a link or csp value, a cookie sharing the prefix). add an optional Header field to Signature that scopes a header-only match to one named header's value, and apply it (or a structural anchor) per detector: - express: "Express" scoped to x-powered-by, was firing on an express_checkout cookie. - flask: "Werkzeug" scoped to the server header. - symfony: dropped the bare "symfony" word (symfony sets no such header, it fired on symfony.com links); the x-debug-token header is the marker. - shopify: key on the x-shopify response headers instead of the bare "Shopify" word, which fired on a cdn.shopify.com link. - remix: dropped the bare "remix"/"_remix" substrings that fired on a track_remix.mp3 asset; window.__remixContext is the definitive marker. - spring boot: anchor the whitelabel title in its h1 tag context so a tutorial discussing the error does not fire. the gin and fastapi detectors are removed: gin keyed on the "gin-gonic" import-path string (appears in tutorials, never in a real gin response) and fastapi on bare words matching the projects' doc domains. neither framework advertises itself in a response header or a non-prose body marker, so there is no clean passive signal to anchor on. version mis-extraction: drop the low-confidence ".*?" version fallbacks (rails, django, laravel, spring), whose unbounded gap grabbed the first version-shaped number after the framework word and reported an unrelated asset's cache-buster when no real version was present. let isValidVersionString accept a single integer so a bare major such as drupal's "Drupal 10" is no longer rejected as "unknown". each false positive and version bug is covered by a regression test.
193 lines
5.9 KiB
Go
193 lines
5.9 KiB
Go
/*
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
: :
|
|
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
|
: ▄█ █ █▀ · BSD 3-Clause License :
|
|
: :
|
|
: (c) 2022-2026 vmfunc, xyzeva, :
|
|
: lunchcat alumni & contributors :
|
|
: :
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
*/
|
|
|
|
/*
|
|
|
|
BSD 3-Clause License
|
|
(c) 2022-2026 vmfunc, xyzeva & contributors
|
|
|
|
*/
|
|
|
|
package detectors
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
fw "github.com/vmfunc/sif/internal/scan/frameworks"
|
|
)
|
|
|
|
func init() {
|
|
// Register all CMS detectors
|
|
fw.Register(&wordpressDetector{})
|
|
fw.Register(&drupalDetector{})
|
|
fw.Register(&joomlaDetector{})
|
|
fw.Register(&magentoDetector{})
|
|
fw.Register(&shopifyDetector{})
|
|
fw.Register(&ghostDetector{})
|
|
}
|
|
|
|
// wordpressDetector detects WordPress CMS.
|
|
type wordpressDetector struct{}
|
|
|
|
func (d *wordpressDetector) Name() string { return "WordPress" }
|
|
|
|
func (d *wordpressDetector) Signatures() []fw.Signature {
|
|
return []fw.Signature{
|
|
{Pattern: "wp-content", Weight: 0.4},
|
|
{Pattern: "wp-includes", Weight: 0.4},
|
|
{Pattern: "wp-json", Weight: 0.3},
|
|
{Pattern: "wordpress", Weight: 0.3},
|
|
{Pattern: "wp-emoji", Weight: 0.2},
|
|
}
|
|
}
|
|
|
|
func (d *wordpressDetector) Detect(body string, headers http.Header) (float32, string) {
|
|
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
|
score := base.MatchSignatures(body, headers)
|
|
confidence := sigmoidConfidence(score)
|
|
|
|
var version string
|
|
if confidence > 0.5 {
|
|
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
|
}
|
|
return confidence, version
|
|
}
|
|
|
|
// drupalDetector detects Drupal CMS.
|
|
type drupalDetector struct{}
|
|
|
|
func (d *drupalDetector) Name() string { return "Drupal" }
|
|
|
|
func (d *drupalDetector) Signatures() []fw.Signature {
|
|
return []fw.Signature{
|
|
{Pattern: "Drupal", Weight: 0.4, HeaderOnly: true},
|
|
{Pattern: "drupal.js", Weight: 0.4},
|
|
{Pattern: "/sites/default/files", Weight: 0.3},
|
|
{Pattern: "Drupal.settings", Weight: 0.3},
|
|
}
|
|
}
|
|
|
|
func (d *drupalDetector) Detect(body string, headers http.Header) (float32, string) {
|
|
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
|
score := base.MatchSignatures(body, headers)
|
|
confidence := sigmoidConfidence(score)
|
|
|
|
var version string
|
|
if confidence > 0.5 {
|
|
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
|
}
|
|
return confidence, version
|
|
}
|
|
|
|
// joomlaDetector detects Joomla CMS.
|
|
type joomlaDetector struct{}
|
|
|
|
func (d *joomlaDetector) Name() string { return "Joomla" }
|
|
|
|
func (d *joomlaDetector) Signatures() []fw.Signature {
|
|
return []fw.Signature{
|
|
{Pattern: "Joomla", Weight: 0.4},
|
|
{Pattern: "/media/jui/", Weight: 0.4},
|
|
{Pattern: "/components/com_", Weight: 0.3},
|
|
{Pattern: "joomla.javascript", Weight: 0.3},
|
|
}
|
|
}
|
|
|
|
func (d *joomlaDetector) Detect(body string, headers http.Header) (float32, string) {
|
|
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
|
score := base.MatchSignatures(body, headers)
|
|
confidence := sigmoidConfidence(score)
|
|
|
|
var version string
|
|
if confidence > 0.5 {
|
|
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
|
}
|
|
return confidence, version
|
|
}
|
|
|
|
// magentoDetector detects Magento CMS.
|
|
type magentoDetector struct{}
|
|
|
|
func (d *magentoDetector) Name() string { return "Magento" }
|
|
|
|
func (d *magentoDetector) Signatures() []fw.Signature {
|
|
return []fw.Signature{
|
|
{Pattern: "Magento", Weight: 0.4},
|
|
{Pattern: "/static/frontend/", Weight: 0.4},
|
|
{Pattern: "mage/", Weight: 0.3},
|
|
{Pattern: "Mage.Cookies", Weight: 0.3},
|
|
}
|
|
}
|
|
|
|
func (d *magentoDetector) Detect(body string, headers http.Header) (float32, string) {
|
|
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
|
score := base.MatchSignatures(body, headers)
|
|
confidence := sigmoidConfidence(score)
|
|
|
|
var version string
|
|
if confidence > 0.5 {
|
|
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
|
}
|
|
return confidence, version
|
|
}
|
|
|
|
// shopifyDetector detects Shopify platform.
|
|
type shopifyDetector struct{}
|
|
|
|
func (d *shopifyDetector) Name() string { return "Shopify" }
|
|
|
|
func (d *shopifyDetector) Signatures() []fw.Signature {
|
|
return []fw.Signature{
|
|
{Pattern: "x-shopify", Weight: 0.5, HeaderOnly: true},
|
|
{Pattern: "cdn.shopify.com", Weight: 0.4},
|
|
{Pattern: "shopify-section", Weight: 0.4},
|
|
{Pattern: "myshopify.com", Weight: 0.3},
|
|
}
|
|
}
|
|
|
|
func (d *shopifyDetector) Detect(body string, headers http.Header) (float32, string) {
|
|
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
|
score := base.MatchSignatures(body, headers)
|
|
confidence := sigmoidConfidence(score)
|
|
|
|
var version string
|
|
if confidence > 0.5 {
|
|
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
|
}
|
|
return confidence, version
|
|
}
|
|
|
|
// ghostDetector detects Ghost CMS.
|
|
type ghostDetector struct{}
|
|
|
|
func (d *ghostDetector) Name() string { return "Ghost" }
|
|
|
|
func (d *ghostDetector) Signatures() []fw.Signature {
|
|
return []fw.Signature{
|
|
{Pattern: `<meta name="generator" content="Ghost`, Weight: 0.4},
|
|
{Pattern: "Ghost", Weight: 0.3, HeaderOnly: true},
|
|
{Pattern: "/ghost/api/", Weight: 0.4},
|
|
}
|
|
}
|
|
|
|
func (d *ghostDetector) Detect(body string, headers http.Header) (float32, string) {
|
|
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
|
score := base.MatchSignatures(body, headers)
|
|
confidence := sigmoidConfidence(score)
|
|
|
|
var version string
|
|
if confidence > 0.5 {
|
|
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
|
}
|
|
return confidence, version
|
|
}
|