From ee74728958598e6c73e4713cc1848e8deb1325e4 Mon Sep 17 00:00:00 2001 From: TBX3D <88289044+TBX3D@users.noreply.github.com> Date: Thu, 25 Jun 2026 20:26:39 -0700 Subject: [PATCH 01/11] fix(frameworks): scope header markers to stop bare-brand false positives several detectors matched a brand name as a substring across all header names and values, so they fired on any response that merely referenced the brand: a vendor cdn named in a link or csp value, or a cookie that shares the brand 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. - strapi: "Strapi" scoped to x-powered-by, was firing on a link naming strapi.io. - flask: "Werkzeug" scoped to the server header. - cherrypy: "CherryPy" scoped to the server header. - symfony: dropped the bare "symfony" word (symfony sets no such header and 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. each false positive is folded into the detector tests as a regression. --- internal/scan/frameworks/detectors/backend.go | 243 +++++++++++++++++- .../scan/frameworks/detectors/backend_test.go | 123 +++++++++ .../scan/frameworks/detectors/cms_test.go | 161 ++++++++++++ .../scan/frameworks/detectors/meta_test.go | 109 ++++++++ 4 files changed, 635 insertions(+), 1 deletion(-) create mode 100644 internal/scan/frameworks/detectors/backend_test.go create mode 100644 internal/scan/frameworks/detectors/cms_test.go create mode 100644 internal/scan/frameworks/detectors/meta_test.go diff --git a/internal/scan/frameworks/detectors/backend.go b/internal/scan/frameworks/detectors/backend.go index e789d81a..1ab5abb7 100644 --- a/internal/scan/frameworks/detectors/backend.go +++ b/internal/scan/frameworks/detectors/backend.go @@ -22,6 +22,7 @@ package detectors import ( "math" "net/http" + "strings" fw "github.com/vmfunc/sif/internal/scan/frameworks" ) @@ -43,6 +44,15 @@ func init() { fw.Register(&adonisDetector{}) fw.Register(&cakephpDetector{}) fw.Register(&codeigniterDetector{}) + fw.Register(&tornadoDetector{}) + fw.Register(&cherrypyDetector{}) + fw.Register(&playDetector{}) + fw.Register(&sailsDetector{}) + fw.Register(&beegoDetector{}) + fw.Register(&jsfDetector{}) + fw.Register(&gwtDetector{}) + fw.Register(&vaadinDetector{}) + fw.Register(&coldfusionDetector{}) } // sigmoidConfidence maps the matched-weight fraction to a 0-1 confidence, @@ -52,6 +62,22 @@ func sigmoidConfidence(score float32) float32 { return float32(1.0 / (1.0 + math.Exp(-(float64(score)-0.3)*10.0))) } +// serverVersion pulls the trailing version out of a "/" Server +// header, e.g. "TornadoServer/6.4.1" -> "6.4.1". +func serverVersion(headers http.Header, product string) string { + server := headers.Get("Server") + i := strings.Index(server, product+"/") + if i < 0 { + return "" + } + rest := server[i+len(product)+1:] + end := 0 + for end < len(rest) && (rest[end] == '.' || (rest[end] >= '0' && rest[end] <= '9')) { + end++ + } + return rest[:end] +} + // laravelDetector detects Laravel framework. type laravelDetector struct { fw.BaseDetector @@ -357,7 +383,7 @@ func (d *strapiDetector) Name() string { return "Strapi" } func (d *strapiDetector) Signatures() []fw.Signature { return []fw.Signature{ - {Pattern: "strapi", Weight: 0.4}, + {Pattern: "Strapi", Weight: 0.4, HeaderOnly: true, Header: "X-Powered-By"}, } } @@ -442,3 +468,218 @@ func (d *codeigniterDetector) Detect(body string, headers http.Header) (float32, } return confidence, version } +// tornadoDetector detects the Tornado Python web framework. +type tornadoDetector struct{} + +func (d *tornadoDetector) Name() string { return "Tornado" } + +func (d *tornadoDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "TornadoServer", Weight: 0.6, HeaderOnly: true}, + } +} + +func (d *tornadoDetector) 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 = serverVersion(headers, "TornadoServer") + } + return confidence, version +} + +// cherrypyDetector detects the CherryPy Python web framework. +type cherrypyDetector struct{} + +func (d *cherrypyDetector) Name() string { return "CherryPy" } + +func (d *cherrypyDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "CherryPy", Weight: 0.6, HeaderOnly: true, Header: "Server"}, + } +} + +func (d *cherrypyDetector) 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 = serverVersion(headers, "CherryPy") + } + return confidence, version +} + +// playDetector detects the Play Framework (Scala/Java). +type playDetector struct{} + +func (d *playDetector) Name() string { return "Play Framework" } + +func (d *playDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "PLAY_SESSION", Weight: 0.5, HeaderOnly: true}, + {Pattern: "PLAY_FLASH", Weight: 0.3, HeaderOnly: true}, + {Pattern: "PLAY_LANG", Weight: 0.3, HeaderOnly: true}, + } +} + +func (d *playDetector) 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 +} + +// sailsDetector detects the Sails.js Node framework. +type sailsDetector struct{} + +func (d *sailsDetector) Name() string { return "Sails.js" } + +func (d *sailsDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "sails.sid", Weight: 0.5, HeaderOnly: true}, + } +} + +func (d *sailsDetector) 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 +} + +// beegoDetector detects the Beego Go web framework. +type beegoDetector struct{} + +func (d *beegoDetector) Name() string { return "Beego" } + +func (d *beegoDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "beegosessionID", Weight: 0.5, HeaderOnly: true}, + } +} + +func (d *beegoDetector) 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 +} + +// jsfDetector detects JavaServer Faces / Jakarta Faces. +type jsfDetector struct{} + +func (d *jsfDetector) Name() string { return "JavaServer Faces" } + +func (d *jsfDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `javax.faces.ViewState"`, Weight: 0.5}, + {Pattern: `jakarta.faces.ViewState"`, Weight: 0.5}, + } +} + +func (d *jsfDetector) 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 +} + +// gwtDetector detects Google Web Toolkit. +type gwtDetector struct{} + +func (d *gwtDetector) Name() string { return "Google Web Toolkit" } + +func (d *gwtDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `.nocache.js"`, Weight: 0.4}, + {Pattern: "gwt:", Weight: 0.3}, + {Pattern: "__gwt_", Weight: 0.3}, + } +} + +func (d *gwtDetector) 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 +} + +// vaadinDetector detects the Vaadin Java framework. +type vaadinDetector struct{} + +func (d *vaadinDetector) Name() string { return "Vaadin" } + +func (d *vaadinDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `"Vaadin-Security-Key"`, Weight: 0.5}, + {Pattern: "window.Vaadin", Weight: 0.3}, + {Pattern: "/VAADIN/", Weight: 0.3}, + } +} + +func (d *vaadinDetector) 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 +} + +// coldfusionDetector detects Adobe ColdFusion / Lucee (CFML). +type coldfusionDetector struct{} + +func (d *coldfusionDetector) Name() string { return "ColdFusion" } + +func (d *coldfusionDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "CFTOKEN", Weight: 0.4, HeaderOnly: true}, + {Pattern: "CFID", Weight: 0.3, HeaderOnly: true}, + {Pattern: "CFAUTHORIZATION", Weight: 0.3, HeaderOnly: true}, + } +} + +func (d *coldfusionDetector) 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 +} diff --git a/internal/scan/frameworks/detectors/backend_test.go b/internal/scan/frameworks/detectors/backend_test.go new file mode 100644 index 00000000..39cba961 --- /dev/null +++ b/internal/scan/frameworks/detectors/backend_test.go @@ -0,0 +1,123 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +package detectors + +import ( + "net/http" + "testing" + + fw "github.com/vmfunc/sif/internal/scan/frameworks" +) + +func hdr(pairs ...string) http.Header { + h := http.Header{} + for i := 0; i+1 < len(pairs); i += 2 { + h.Add(pairs[i], pairs[i+1]) + } + return h +} + +func TestWebFrameworkDetectors_Positive(t *testing.T) { + tests := []struct { + name string + detector fw.Detector + body string + headers http.Header + }{ + {"Tornado", &tornadoDetector{}, "", hdr("Server", "TornadoServer/6.4.1")}, + {"CherryPy", &cherrypyDetector{}, "", hdr("Server", "CherryPy/18.8.0")}, + {"Play session", &playDetector{}, "", hdr("Set-Cookie", "PLAY_SESSION=eyJhbGci; Path=/; HTTPOnly")}, + {"Sails.js", &sailsDetector{}, "", hdr("Set-Cookie", "sails.sid=s%3Aabc.def; Path=/; HttpOnly")}, + {"Beego", &beegoDetector{}, "", hdr("Set-Cookie", "beegosessionID=8f2a1c; Path=/; HttpOnly")}, + {"JSF javax", &jsfDetector{}, ``, http.Header{}}, + {"JSF jakarta", &jsfDetector{}, ``, http.Header{}}, + {"GWT", &gwtDetector{}, ``, http.Header{}}, + {"GWT meta combo", &gwtDetector{}, ``, http.Header{}}, + {"Vaadin security key", &vaadinDetector{}, `{"appConfig":{"uidl":{"Vaadin-Security-Key":"f0ef03d7-0cf4-4f32-834d-47b88a1034b7"}}}`, http.Header{}}, + {"Vaadin Flow deferred", &vaadinDetector{}, ``, http.Header{}}, + {"ColdFusion pair", &coldfusionDetector{}, "", hdr("Set-Cookie", "CFID=2401; CFTOKEN=55be99e7c")}, + {"ColdFusion token only", &coldfusionDetector{}, "", hdr("Set-Cookie", "CFTOKEN=55be99e7c2f8b2a1")}, + {"Strapi powered-by", &strapiDetector{}, "", hdr("X-Powered-By", "Strapi ")}, + {"CakePHP cookie", &cakephpDetector{}, "", hdr("Set-Cookie", "CAKEPHP=8f2a1c4e; path=/; HttpOnly")}, + {"Express powered-by", &expressDetector{}, "", hdr("X-Powered-By", "Express")}, + {"Flask werkzeug server", &flaskDetector{}, "", hdr("Server", "Werkzeug/2.3.0 Python/3.11")}, + {"Symfony debug token", &symfonyDetector{}, "", hdr("X-Debug-Token", "a1b2c3")}, + {"Spring Boot whitelabel", &springBootDetector{}, `

Whitelabel Error Page

This application has no explicit mapping for /error

There was an unexpected error (type=Not Found, status=404).
`, http.Header{}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf, _ := tt.detector.Detect(tt.body, tt.headers) + if conf <= 0.5 { + t.Errorf("%s: confidence = %.3f, want > 0.5", tt.name, conf) + } + }) + } +} + +func TestWebFrameworkDetectors_Negative(t *testing.T) { + tests := []struct { + name string + detector fw.Detector + body string + headers http.Header + }{ + {"JSF prose", &jsfDetector{}, "

In JSF, the javax.faces.ViewState field stores view state.

", http.Header{}}, + {"GWT prose", &gwtDetector{}, "

GWT writes a .nocache.js bootstrap that is never cached.

", http.Header{}}, + {"Vaadin prose", &vaadinDetector{}, "

Vaadin is a Java web framework with a Vaadin-Security-Key concept.

", http.Header{}}, + {"Vaadin path only", &vaadinDetector{}, ``, http.Header{}}, + {"Vaadin global only", &vaadinDetector{}, ``, http.Header{}}, + {"ColdFusion id only", &coldfusionDetector{}, "", hdr("Set-Cookie", "CFID=2401; Path=/")}, + {"CakePHP prose", &cakephpDetector{}, `cakephp tag`, http.Header{}}, + {"Strapi prose", &strapiDetector{}, "

Strapi is an open source headless CMS built on Node.

", http.Header{}}, + {"Strapi domain link header", &strapiDetector{}, "", hdr("Link", "; rel=help")}, + {"Express checkout cookie", &expressDetector{}, "", hdr("Set-Cookie", "express_checkout=1; path=/")}, + {"Flask werkzeug docs link", &flaskDetector{}, "", hdr("Link", "; rel=help")}, + {"CherryPy domain link", &cherrypyDetector{}, "", hdr("Link", "; rel=help")}, + {"Symfony domain link", &symfonyDetector{}, "", hdr("Link", "; rel=help")}, + {"Spring Boot tutorial prose", &springBootDetector{}, `
To fix the Whitelabel Error Page in Spring Boot, add a controller.
`, http.Header{}}, + {"plain page Tornado", &tornadoDetector{}, "hello", hdr("Server", "nginx/1.25.3")}, + {"plain page Play", &playDetector{}, "", hdr("Set-Cookie", "sessionid=abc; Path=/")}, + {"plain page ColdFusion", &coldfusionDetector{}, "", hdr("Set-Cookie", "JSESSIONID=abc; Path=/")}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf, _ := tt.detector.Detect(tt.body, tt.headers) + if conf > 0.5 { + t.Errorf("%s: confidence = %.3f, want <= 0.5", tt.name, conf) + } + }) + } +} + +func TestWebFrameworkDetectors_Version(t *testing.T) { + tests := []struct { + name string + detector fw.Detector + headers http.Header + want string + }{ + {"Tornado", &tornadoDetector{}, hdr("Server", "TornadoServer/6.4.1"), "6.4.1"}, + {"CherryPy", &cherrypyDetector{}, hdr("Server", "CherryPy/18.8.0"), "18.8.0"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, version := tt.detector.Detect("", tt.headers) + if version != tt.want { + t.Errorf("%s: version = %q, want %q", tt.name, version, tt.want) + } + }) + } +} diff --git a/internal/scan/frameworks/detectors/cms_test.go b/internal/scan/frameworks/detectors/cms_test.go new file mode 100644 index 00000000..a8027a63 --- /dev/null +++ b/internal/scan/frameworks/detectors/cms_test.go @@ -0,0 +1,161 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +package detectors + +import ( + "net/http" + "testing" + + fw "github.com/vmfunc/sif/internal/scan/frameworks" +) + +func TestPlatformDetectors_Positive(t *testing.T) { + tests := []struct { + name string + detector fw.Detector + body string + headers http.Header + }{ + {"TYPO3", &typo3Detector{}, ``, http.Header{}}, + {"TYPO3 4.x versioned", &typo3Detector{}, ``, http.Header{}}, + {"Contao", &contaoDetector{}, ``, http.Header{}}, + {"Wix header", &wixDetector{}, "", hdr("X-Wix-Request-Id", "1782416012.749990137421329")}, + {"Wix generator", &wixDetector{}, ``, http.Header{}}, + {"Webflow reversed attrs", &webflowDetector{}, ``, http.Header{}}, + {"Webflow html attrs", &webflowDetector{}, ``, http.Header{}}, + {"HubSpot", &hubspotDetector{}, ``, http.Header{}}, + {"PrestaShop global", &prestashopDetector{}, ``, http.Header{}}, + {"Sitecore cookie", &sitecoreDetector{}, "", hdr("Set-Cookie", "SC_ANALYTICS_GLOBAL_COOKIE=8f2; path=/; HttpOnly")}, + {"OpenCart cookie", &opencartDetector{}, "", hdr("Set-Cookie", "OCSESSID=2a1c; path=/; HttpOnly")}, + {"DotNetNuke cookie", &dnnDetector{}, "", hdr("Set-Cookie", "DNNPersonalization=; expires=Mon; path=/")}, + {"DotNetNuke mobile cookie", &dnnDetector{}, "", hdr("Set-Cookie", "dnn_IsMobile=False; path=/; HttpOnly")}, + {"Liferay header", &liferayDetector{}, "", hdr("X-Liferay-Request-Guest-User", "true")}, + {"Liferay body behind CDN", &liferayDetector{}, ``, http.Header{}}, + {"Squarespace context", &squarespaceDetector{}, ``, http.Header{}}, + {"WooCommerce store", &woocommerceDetector{}, ``, http.Header{}}, + {"Shopify storefront header", &shopifyDetector{}, "", hdr("X-Shopify-Stage", "production")}, + {"Craft header", &craftDetector{}, "", hdr("X-Powered-By", "Craft CMS,SEOmatic")}, + {"Craft csrf cookie", &craftDetector{}, "", hdr("Set-Cookie", "CRAFT_CSRF_TOKEN=a1b2c3; path=/; HttpOnly")}, + {"Concrete CMS 9", &concreteDetector{}, ``, http.Header{}}, + {"concrete5 legacy", &concreteDetector{}, ``, http.Header{}}, + {"Bitrix assets", &bitrixDetector{}, ``, http.Header{}}, + {"Blogger generator", &bloggerDetector{}, ``, http.Header{}}, + {"MediaWiki generator", &mediawikiDetector{}, ``, http.Header{}}, + {"Discourse generator", &discourseDetector{}, ``, http.Header{}}, + {"XenForo attrs", &xenforoDetector{}, ``, http.Header{}}, + {"Moodle cookie", &moodleDetector{}, "", hdr("Set-Cookie", "MoodleSession=2a1c; path=/; HttpOnly")}, + {"Plone generator", &ploneDetector{}, ``, http.Header{}}, + {"Grav generator", &gravDetector{}, ``, http.Header{}}, + {"Textpattern generator", &textpatternDetector{}, ``, http.Header{}}, + {"October cookie", &octoberDetector{}, "", hdr("Set-Cookie", "october_session=eyJpdiI6Ijk; path=/; httponly")}, + {"Statamic session cookie", &statamicDetector{}, "", hdr("Set-Cookie", "statamic_session=eyJpdiI6Ijd; path=/; secure; httponly")}, + {"Statamic branded cookie", &statamicDetector{}, "", hdr("Set-Cookie", "delicious_statamic_cookies=eyJpdiI6Ijh; path=/; secure")}, + {"Flarum bootstrap", &flarumDetector{}, `
`, http.Header{}}, + {"NodeBB assets", &nodebbDetector{}, ``, http.Header{}}, + {"XWiki attrs", &xwikiDetector{}, `home`, http.Header{}}, + {"Bolt generator", &boltDetector{}, ``, http.Header{}}, + {"ExpressionEngine csrf field", &expressionengineDetector{}, ``, http.Header{}}, + {"ExpressionEngine cookie", &expressionengineDetector{}, "", hdr("Set-Cookie", "exp_last_visit=1782; path=/; httponly")}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf, _ := tt.detector.Detect(tt.body, tt.headers) + if conf <= 0.5 { + t.Errorf("%s: confidence = %.3f, want > 0.5", tt.name, conf) + } + }) + } +} + +func TestPlatformDetectors_Negative(t *testing.T) { + tests := []struct { + name string + detector fw.Detector + body string + headers http.Header + }{ + {"Contao description", &contaoDetector{}, ``, http.Header{}}, + {"PrestaShop keywords", &prestashopDetector{}, ``, http.Header{}}, + {"TYPO3 prose", &typo3Detector{}, `

TYPO3 CMS is a popular enterprise CMS.

`, http.Header{}}, + {"Webflow review", &webflowDetector{}, ``, http.Header{}}, + {"HubSpot pricing", &hubspotDetector{}, ``, http.Header{}}, + {"Webflow brand og", &webflowDetector{}, ``, http.Header{}}, + {"HubSpot brand og", &hubspotDetector{}, ``, http.Header{}}, + {"Wix mention", &wixDetector{}, `

I built my first site on Wix.com years ago.

`, http.Header{}}, + {"Sitecore plain cookie", &sitecoreDetector{}, "", hdr("Set-Cookie", "sessionid=abc; path=/")}, + {"OpenCart plain cookie", &opencartDetector{}, "", hdr("Set-Cookie", "PHPSESSID=abc; path=/")}, + {"DotNetNuke plain cookie", &dnnDetector{}, "", hdr("Set-Cookie", "ASP.NET_SessionId=abc; path=/")}, + {"Liferay plain header", &liferayDetector{}, "", hdr("Server", "nginx/1.25.3")}, + {"Liferay prose", &liferayDetector{}, "

Liferay is a Java portal platform.

", http.Header{}}, + {"Squarespace prose", &squarespaceDetector{}, `

Squarespace is a hosted website builder.

`, http.Header{}}, + {"Shopify cdn link header", &shopifyDetector{}, "", hdr("Link", "; rel=preload")}, + {"Shopify cdn body only", &shopifyDetector{}, ``, http.Header{}}, + {"WooCommerce plain WP", &woocommerceDetector{}, ``, http.Header{}}, + {"WooCommerce class only", &woocommerceDetector{}, ``, http.Header{}}, + {"Craft unrelated header", &craftDetector{}, "", hdr("Server", "nginx/1.25.3")}, + {"Concrete brand og", &concreteDetector{}, ``, http.Header{}}, + {"Bitrix prose", &bitrixDetector{}, `

We migrated from Bitrix to Shopify last year.

`, http.Header{}}, + {"Blogger comparison prose", &bloggerDetector{}, ``, http.Header{}}, + {"MediaWiki brand og", &mediawikiDetector{}, ``, http.Header{}}, + {"Discourse comparison og", &discourseDetector{}, ``, http.Header{}}, + {"XenForo prose", &xenforoDetector{}, `

XenForo is a commercial forum platform.

`, http.Header{}}, + {"Moodle unrelated cookie", &moodleDetector{}, "", hdr("Set-Cookie", "sessionid=abc; path=/")}, + {"Moodle minified collision", &moodleDetector{}, ``, http.Header{}}, + {"Plone brand og", &ploneDetector{}, ``, http.Header{}}, + {"Grav prose", &gravDetector{}, `

Grav is a flat-file CMS.

`, http.Header{}}, + {"Textpattern brand og", &textpatternDetector{}, ``, http.Header{}}, + {"Statamic unrelated header", &statamicDetector{}, "", hdr("Server", "nginx/1.25.3")}, + {"Statamic domain in link header", &statamicDetector{}, "", hdr("Link", "; rel=preload")}, + {"Flarum prose", &flarumDetector{}, `

Flarum is a delightfully simple forum platform.

`, http.Header{}}, + {"Flarum near miss id", &flarumDetector{}, `
`, http.Header{}}, + {"Flarum generic forum asset", &flarumDetector{}, ``, http.Header{}}, + {"XWiki prose", &xwikiDetector{}, `

XWiki is an enterprise wiki platform.

`, http.Header{}}, + {"Bolt brand og", &boltDetector{}, ``, http.Header{}}, + {"ExpressionEngine prose", &expressionengineDetector{}, `

ExpressionEngine is a flexible PHP CMS.

`, http.Header{}}, + {"ExpressionEngine exp prefix collision", &expressionengineDetector{}, "", hdr("Set-Cookie", "exp_date=2026; path=/")}, + {"NodeBB prose", &nodebbDetector{}, `

We run NodeBB for our community forum.

`, http.Header{}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf, _ := tt.detector.Detect(tt.body, tt.headers) + if conf > 0.5 { + t.Errorf("%s: confidence = %.3f, want <= 0.5", tt.name, conf) + } + }) + } +} + +func TestPlatformDetectors_Version(t *testing.T) { + tests := []struct { + name string + detector fw.Detector + body string + want string + }{ + {"TYPO3 4.x", &typo3Detector{}, ``, "4.5"}, + {"TYPO3 modern", &typo3Detector{}, ``, "unknown"}, + {"MediaWiki", &mediawikiDetector{}, ``, "1.47.0"}, + {"Discourse", &discourseDetector{}, ``, "3.2.0"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, version := tt.detector.Detect(tt.body, http.Header{}) + if version != tt.want { + t.Errorf("%s: version = %q, want %q", tt.name, version, tt.want) + } + }) + } +} diff --git a/internal/scan/frameworks/detectors/meta_test.go b/internal/scan/frameworks/detectors/meta_test.go new file mode 100644 index 00000000..62567661 --- /dev/null +++ b/internal/scan/frameworks/detectors/meta_test.go @@ -0,0 +1,109 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +package detectors + +import ( + "net/http" + "testing" + + fw "github.com/vmfunc/sif/internal/scan/frameworks" +) + +func TestSiteGeneratorDetectors_Positive(t *testing.T) { + tests := []struct { + name string + detector fw.Detector + body string + }{ + {"Hugo minified", &hugoDetector{}, `x`}, + {"Jekyll", &jekyllDetector{}, ``}, + {"Docusaurus minified", &docusaurusDetector{}, ``}, + {"MkDocs Material", &mkdocsDetector{}, ``}, + {"Eleventy default", &eleventyDetector{}, ``}, + {"Eleventy custom label", &eleventyDetector{}, ``}, + {"Hexo", &hexoDetector{}, ``}, + {"VuePress", &vuepressDetector{}, ``}, + {"Sphinx assets", &sphinxDetector{}, ``}, + {"Nikola", &nikolaDetector{}, ``}, + {"Publii", &publiiDetector{}, ``}, + {"Remix context", &remixDetector{}, ``}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf, _ := tt.detector.Detect(tt.body, http.Header{}) + if conf <= 0.5 { + t.Errorf("%s: confidence = %.3f, want > 0.5", tt.name, conf) + } + }) + } +} + +func TestSiteGeneratorDetectors_Negative(t *testing.T) { + tests := []struct { + name string + detector fw.Detector + body string + }{ + {"Hugo Boss", &hugoDetector{}, `

Hugo is a designer.

`}, + {"Jekyll novel", &jekyllDetector{}, ``}, + {"Docusaurus tutorial", &docusaurusDetector{}, ``}, + {"Docusaurus brand og", &docusaurusDetector{}, ``}, + {"MkDocs guide", &mkdocsDetector{}, `

MkDocs is great.

`}, + {"plain migration prose", &hugoDetector{}, `

We migrated from Jekyll to Hugo last year.

`}, + {"Eleventy brand og", &eleventyDetector{}, ``}, + {"Hexo brand og", &hexoDetector{}, ``}, + {"VuePress release prose", &vuepressDetector{}, ``}, + {"Sphinx link only", &sphinxDetector{}, `

Built with Sphinx.

`}, + {"Sphinx doctools only", &sphinxDetector{}, ``}, + {"Nikola Tesla og", &nikolaDetector{}, ``}, + {"Publii brand og", &publiiDetector{}, ``}, + {"Remix audio asset", &remixDetector{}, ``}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf, _ := tt.detector.Detect(tt.body, http.Header{}) + if conf > 0.5 { + t.Errorf("%s: confidence = %.3f, want <= 0.5", tt.name, conf) + } + }) + } +} + +func TestSiteGeneratorDetectors_Version(t *testing.T) { + tests := []struct { + name string + detector fw.Detector + body string + want string + }{ + {"Hugo", &hugoDetector{}, ``, "0.163.3"}, + {"Jekyll", &jekyllDetector{}, ``, "4.4.1"}, + {"Docusaurus", &docusaurusDetector{}, ``, "3.10.1"}, + {"MkDocs", &mkdocsDetector{}, ``, "1.6.1"}, + {"Eleventy", &eleventyDetector{}, ``, "3.0.0"}, + {"Eleventy custom label", &eleventyDetector{}, ``, "4.0.0"}, + {"Hexo", &hexoDetector{}, ``, "8.1.1"}, + {"VuePress", &vuepressDetector{}, ``, "2.0.0"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, version := tt.detector.Detect(tt.body, http.Header{}) + if version != tt.want { + t.Errorf("%s: version = %q, want %q", tt.name, version, tt.want) + } + }) + } +} From 129abe8c7843f2a6670b40ead19b9c4b7a151d06 Mon Sep 17 00:00:00 2001 From: TBX3D <88289044+TBX3D@users.noreply.github.com> Date: Thu, 25 Jun 2026 19:36:31 -0700 Subject: [PATCH 02/11] fix(frameworks): tighten alpine and flarum against substrings a full fire-alone marker audit over the registry surfaced two more held detectors whose single markers tripped on input they should not claim, each proven with an almost-passing trap now folded into the tests as a permanent regression. alpine: the x-data directive marker was a bare substring that also matches inside an unrelated hyphenated token (class="max-data", id="webflux-data"), so it is anchored with a leading space to match the attribute form only. flarum: the /assets/forum- bundle path is a generic forum asset path (any site serving /assets/forum-banner.png trips it) and could only ever fire on its own, since a real flarum page already carries the definitive id="flarum-" marker, so it is dropped. --- internal/scan/frameworks/detectors/cms.go | 738 ++++++++++++++++++ .../scan/frameworks/detectors/frontend.go | 211 +++++ .../frameworks/detectors/frontend_test.go | 106 +++ 3 files changed, 1055 insertions(+) create mode 100644 internal/scan/frameworks/detectors/frontend_test.go diff --git a/internal/scan/frameworks/detectors/cms.go b/internal/scan/frameworks/detectors/cms.go index fd030512..706f6b88 100644 --- a/internal/scan/frameworks/detectors/cms.go +++ b/internal/scan/frameworks/detectors/cms.go @@ -33,6 +33,36 @@ func init() { fw.Register(&magentoDetector{}) fw.Register(&shopifyDetector{}) fw.Register(&ghostDetector{}) + fw.Register(&bitrixDetector{}) + fw.Register(&bloggerDetector{}) + fw.Register(&boltDetector{}) + fw.Register(&concreteDetector{}) + fw.Register(&contaoDetector{}) + fw.Register(&craftDetector{}) + fw.Register(&discourseDetector{}) + fw.Register(&dnnDetector{}) + fw.Register(&expressionengineDetector{}) + fw.Register(&flarumDetector{}) + fw.Register(&gravDetector{}) + fw.Register(&hubspotDetector{}) + fw.Register(&liferayDetector{}) + fw.Register(&mediawikiDetector{}) + fw.Register(&moodleDetector{}) + fw.Register(&nodebbDetector{}) + fw.Register(&octoberDetector{}) + fw.Register(&opencartDetector{}) + fw.Register(&ploneDetector{}) + fw.Register(&prestashopDetector{}) + fw.Register(&sitecoreDetector{}) + fw.Register(&squarespaceDetector{}) + fw.Register(&statamicDetector{}) + fw.Register(&textpatternDetector{}) + fw.Register(&typo3Detector{}) + fw.Register(&webflowDetector{}) + fw.Register(&wixDetector{}) + fw.Register(&woocommerceDetector{}) + fw.Register(&xenforoDetector{}) + fw.Register(&xwikiDetector{}) } // wordpressDetector detects WordPress CMS. @@ -190,3 +220,711 @@ func (d *ghostDetector) Detect(body string, headers http.Header) (float32, strin } return confidence, version } + +// typo3Detector detects the TYPO3 CMS. +type typo3Detector struct{} + +func (d *typo3Detector) Name() string { return "TYPO3" } + +func (d *typo3Detector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `generator" content="TYPO3`, Weight: 0.6}, + } +} + +func (d *typo3Detector) 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 +} + +// contaoDetector detects the Contao CMS. +type contaoDetector struct{} + +func (d *contaoDetector) Name() string { return "Contao" } + +func (d *contaoDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `content="Contao Open Source CMS"`, Weight: 0.6}, + } +} + +func (d *contaoDetector) 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 +} + +// wixDetector detects sites built on the Wix website builder. +type wixDetector struct{} + +func (d *wixDetector) Name() string { return "Wix" } + +func (d *wixDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "X-Wix-Request-Id", Weight: 0.5, HeaderOnly: true}, + {Pattern: `content="Wix.com Website Builder"`, Weight: 0.5}, + } +} + +func (d *wixDetector) 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 +} + +// webflowDetector detects sites built on Webflow. +type webflowDetector struct{} + +func (d *webflowDetector) Name() string { return "Webflow" } + +func (d *webflowDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "data-wf-page", Weight: 0.6}, + {Pattern: `content="Webflow" name="generator"`, Weight: 0.6}, + } +} + +func (d *webflowDetector) 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 +} + +// hubspotDetector detects pages built on the HubSpot CMS. +type hubspotDetector struct{} + +func (d *hubspotDetector) Name() string { return "HubSpot" } + +func (d *hubspotDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `generator" content="HubSpot"`, Weight: 0.6}, + } +} + +func (d *hubspotDetector) 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 +} + +// prestashopDetector detects the PrestaShop e-commerce platform. +type prestashopDetector struct{} + +func (d *prestashopDetector) Name() string { return "PrestaShop" } + +func (d *prestashopDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "var prestashop = {", Weight: 0.6}, + } +} + +func (d *prestashopDetector) 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 +} + +// sitecoreDetector detects the Sitecore platform. +type sitecoreDetector struct{} + +func (d *sitecoreDetector) Name() string { return "Sitecore" } + +func (d *sitecoreDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "SC_ANALYTICS_GLOBAL_COOKIE", Weight: 0.6, HeaderOnly: true}, + } +} + +func (d *sitecoreDetector) 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 +} + +// opencartDetector detects the OpenCart e-commerce platform. +type opencartDetector struct{} + +func (d *opencartDetector) Name() string { return "OpenCart" } + +func (d *opencartDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "OCSESSID", Weight: 0.6, HeaderOnly: true}, + } +} + +func (d *opencartDetector) 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 +} + +// dnnDetector detects the DNN (DotNetNuke) platform. +type dnnDetector struct{} + +func (d *dnnDetector) Name() string { return "DotNetNuke" } + +func (d *dnnDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "DNNPersonalization", Weight: 0.6, HeaderOnly: true}, + {Pattern: "dnn_IsMobile", Weight: 0.6, HeaderOnly: true}, + } +} + +func (d *dnnDetector) 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 +} + +// liferayDetector detects the Liferay portal. +type liferayDetector struct{} + +func (d *liferayDetector) Name() string { return "Liferay" } + +func (d *liferayDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "X-Liferay", Weight: 0.6, HeaderOnly: true}, + {Pattern: "Liferay.ThemeDisplay", Weight: 0.5}, + } +} + +func (d *liferayDetector) 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 +} + +// squarespaceDetector detects sites built on Squarespace. +type squarespaceDetector struct{} + +func (d *squarespaceDetector) Name() string { return "Squarespace" } + +func (d *squarespaceDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "SQUARESPACE_CONTEXT", Weight: 0.6}, + } +} + +func (d *squarespaceDetector) 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 +} + +// woocommerceDetector detects the WooCommerce WordPress plugin. +type woocommerceDetector struct{} + +func (d *woocommerceDetector) Name() string { return "WooCommerce" } + +func (d *woocommerceDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "/plugins/woocommerce/", Weight: 0.6}, + {Pattern: "woocommerce_params", Weight: 0.5}, + {Pattern: "wc-ajax", Weight: 0.4}, + {Pattern: "woocommerce-page", Weight: 0.4}, + } +} + +func (d *woocommerceDetector) 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 +} + +// craftDetector detects Craft CMS. +type craftDetector struct{} + +func (d *craftDetector) Name() string { return "Craft CMS" } + +func (d *craftDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "Craft CMS", Weight: 0.6, HeaderOnly: true}, + {Pattern: "CRAFT_CSRF_TOKEN", Weight: 0.5, HeaderOnly: true}, + {Pattern: "CraftSessionId", Weight: 0.4, HeaderOnly: true}, + } +} + +func (d *craftDetector) 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 +} + +// concreteDetector detects Concrete CMS (formerly concrete5). +type concreteDetector struct{} + +func (d *concreteDetector) Name() string { return "Concrete CMS" } + +func (d *concreteDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `generator" content="Concrete CMS"`, Weight: 0.6}, + {Pattern: `generator" content="concrete5`, Weight: 0.6}, + } +} + +func (d *concreteDetector) 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 +} + +// bitrixDetector detects the 1C-Bitrix platform. +type bitrixDetector struct{} + +func (d *bitrixDetector) Name() string { return "Bitrix" } + +func (d *bitrixDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "/bitrix/js/", Weight: 0.6}, + {Pattern: "/bitrix/templates/", Weight: 0.5}, + {Pattern: "BITRIX_SM_", Weight: 0.4}, + {Pattern: "/bitrix/cache/", Weight: 0.3}, + } +} + +func (d *bitrixDetector) 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 +} + +// bloggerDetector detects Google's Blogger platform. +type bloggerDetector struct{} + +func (d *bloggerDetector) Name() string { return "Blogger" } + +func (d *bloggerDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "content='blogger' name='generator'", Weight: 0.6}, + } +} + +func (d *bloggerDetector) 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 +} + +// mediawikiDetector detects MediaWiki. +type mediawikiDetector struct{} + +func (d *mediawikiDetector) Name() string { return "MediaWiki" } + +func (d *mediawikiDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `generator" content="MediaWiki`, Weight: 0.6}, + } +} + +func (d *mediawikiDetector) 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 +} + +// discourseDetector detects the Discourse forum platform. +type discourseDetector struct{} + +func (d *discourseDetector) Name() string { return "Discourse" } + +func (d *discourseDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `generator" content="Discourse`, Weight: 0.6}, + } +} + +func (d *discourseDetector) 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 +} + +// xenforoDetector detects the XenForo forum platform. +type xenforoDetector struct{} + +func (d *xenforoDetector) Name() string { return "XenForo" } + +func (d *xenforoDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "data-xf-init", Weight: 0.5}, + {Pattern: "/js/xf/", Weight: 0.4}, + {Pattern: "data-xf-key", Weight: 0.3}, + } +} + +func (d *xenforoDetector) 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 +} + +// moodleDetector detects the Moodle learning platform. +type moodleDetector struct{} + +func (d *moodleDetector) Name() string { return "Moodle" } + +func (d *moodleDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "MoodleSession", Weight: 0.6, HeaderOnly: true}, + } +} + +func (d *moodleDetector) 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 +} + +// ploneDetector detects the Plone CMS. +type ploneDetector struct{} + +func (d *ploneDetector) Name() string { return "Plone" } + +func (d *ploneDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `generator" content="Plone`, Weight: 0.6}, + } +} + +func (d *ploneDetector) 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 +} + +// gravDetector detects the Grav flat-file CMS. +type gravDetector struct{} + +func (d *gravDetector) Name() string { return "Grav" } + +func (d *gravDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `content="GravCMS"`, Weight: 0.6}, + } +} + +func (d *gravDetector) 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 +} + +// textpatternDetector detects the Textpattern CMS. +type textpatternDetector struct{} + +func (d *textpatternDetector) Name() string { return "Textpattern" } + +func (d *textpatternDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `generator" content="Textpattern`, Weight: 0.6}, + } +} + +func (d *textpatternDetector) 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 +} + +// octoberDetector detects the October CMS (Laravel-based). +type octoberDetector struct{} + +func (d *octoberDetector) Name() string { return "October CMS" } + +func (d *octoberDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "october_session", Weight: 0.6, HeaderOnly: true}, + } +} + +func (d *octoberDetector) 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 +} + +// statamicDetector detects the Statamic CMS (Laravel-based). +type statamicDetector struct{} + +func (d *statamicDetector) Name() string { return "Statamic" } + +func (d *statamicDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "statamic_", Weight: 0.6, HeaderOnly: true}, + } +} + +func (d *statamicDetector) 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 +} + +// flarumDetector detects the Flarum forum platform. +type flarumDetector struct{} + +func (d *flarumDetector) Name() string { return "Flarum" } + +func (d *flarumDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `id="flarum-`, Weight: 0.6}, + } +} + +func (d *flarumDetector) 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 +} + +// nodebbDetector detects the NodeBB forum platform. +type nodebbDetector struct{} + +func (d *nodebbDetector) Name() string { return "NodeBB" } + +func (d *nodebbDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "assets/nodebb", Weight: 0.6}, + } +} + +func (d *nodebbDetector) 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 +} + +// xwikiDetector detects the XWiki platform. +type xwikiDetector struct{} + +func (d *xwikiDetector) Name() string { return "XWiki" } + +func (d *xwikiDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "data-xwiki-", Weight: 0.5}, + {Pattern: "/xwiki/bin/", Weight: 0.4}, + } +} + +func (d *xwikiDetector) 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 +} + +// boltDetector detects the Bolt CMS (Symfony-based). +type boltDetector struct{} + +func (d *boltDetector) Name() string { return "Bolt CMS" } + +func (d *boltDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `generator" content="Bolt"`, Weight: 0.6}, + } +} + +func (d *boltDetector) 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 +} + +// expressionengineDetector detects ExpressionEngine. +type expressionengineDetector struct{} + +func (d *expressionengineDetector) Name() string { return "ExpressionEngine" } + +func (d *expressionengineDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "exp_csrf", Weight: 0.5}, + {Pattern: "exp_sessionid", Weight: 0.4, HeaderOnly: true}, + {Pattern: "exp_last_visit", Weight: 0.4, HeaderOnly: true}, + } +} + +func (d *expressionengineDetector) 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 +} diff --git a/internal/scan/frameworks/detectors/frontend.go b/internal/scan/frameworks/detectors/frontend.go index ed761d5b..205209cb 100644 --- a/internal/scan/frameworks/detectors/frontend.go +++ b/internal/scan/frameworks/detectors/frontend.go @@ -35,6 +35,14 @@ func init() { fw.Register(&backboneDetector{}) fw.Register(&meteorDetector{}) fw.Register(&htmxDetector{}) + fw.Register(&alpineDetector{}) + fw.Register(&jqueryDetector{}) + fw.Register(&knockoutDetector{}) + fw.Register(&livewireDetector{}) + fw.Register(&qwikDetector{}) + fw.Register(&stimulusDetector{}) + fw.Register(&turboDetector{}) + fw.Register(&unpolyDetector{}) } // reactDetector detects React framework. @@ -234,6 +242,209 @@ func (d *htmxDetector) Detect(body string, headers http.Header) (float32, string return confidence, version } +// alpineDetector detects Alpine.js. +type alpineDetector struct{} + +func (d *alpineDetector) Name() string { return "Alpine.js" } + +func (d *alpineDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: " x-data", Weight: 0.6}, + {Pattern: "alpinejs", Weight: 0.5}, + {Pattern: "x-cloak", Weight: 0.4}, + {Pattern: "x-transition", Weight: 0.4}, + } +} + +func (d *alpineDetector) 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 +} + +// qwikDetector detects the Qwik framework. +type qwikDetector struct{} + +func (d *qwikDetector) Name() string { return "Qwik" } + +func (d *qwikDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "q:container", Weight: 0.5}, + {Pattern: "q:version", Weight: 0.4}, + {Pattern: "q:base", Weight: 0.3}, + {Pattern: "qwikloader", Weight: 0.3}, + } +} + +func (d *qwikDetector) 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 +} + +// jqueryDetector detects the jQuery library. +type jqueryDetector struct{} + +func (d *jqueryDetector) Name() string { return "jQuery" } + +func (d *jqueryDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "jquery.min.js", Weight: 0.5}, + {Pattern: "jquery-", Weight: 0.5}, + {Pattern: "jQuery.fn.jquery", Weight: 0.4}, + } +} + +func (d *jqueryDetector) 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 +} + +// livewireDetector detects Laravel Livewire. +type livewireDetector struct{} + +func (d *livewireDetector) Name() string { return "Livewire" } + +func (d *livewireDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "wire:id", Weight: 0.5}, + {Pattern: "wire:snapshot", Weight: 0.4}, + {Pattern: "wire:model", Weight: 0.4}, + {Pattern: "wire:click", Weight: 0.4}, + } +} + +func (d *livewireDetector) 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 +} + +// stimulusDetector detects the Stimulus controller framework (part of Hotwire, the Rails 7 default). +type stimulusDetector struct{} + +func (d *stimulusDetector) Name() string { return "Stimulus" } + +func (d *stimulusDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "data-controller=", Weight: 0.5}, + {Pattern: "data-action=", Weight: 0.3}, + {Pattern: "@hotwired/stimulus", Weight: 0.4}, + } +} + +func (d *stimulusDetector) 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 +} + +// turboDetector detects Turbo (part of Hotwire, the Rails 7 default). +type turboDetector struct{} + +func (d *turboDetector) Name() string { return "Turbo" } + +func (d *turboDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: " 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// knockoutDetector detects Knockout.js. +type knockoutDetector struct{} + +func (d *knockoutDetector) Name() string { return "Knockout.js" } + +func (d *knockoutDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "data-bind=", Weight: 0.5}, + {Pattern: "ko.applyBindings", Weight: 0.5}, + {Pattern: "knockout-", Weight: 0.4}, + } +} + +func (d *knockoutDetector) 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 +} + +// unpolyDetector detects the Unpoly library. +type unpolyDetector struct{} + +func (d *unpolyDetector) Name() string { return "Unpoly" } + +func (d *unpolyDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "unpoly.min.js", Weight: 0.5}, + {Pattern: "unpoly.js", Weight: 0.4}, + {Pattern: "unpoly@", Weight: 0.4}, + } +} + +func (d *unpolyDetector) 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 +} + // meteorDetector detects Meteor framework. type meteorDetector struct{} diff --git a/internal/scan/frameworks/detectors/frontend_test.go b/internal/scan/frameworks/detectors/frontend_test.go new file mode 100644 index 00000000..07317209 --- /dev/null +++ b/internal/scan/frameworks/detectors/frontend_test.go @@ -0,0 +1,106 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +package detectors + +import ( + "net/http" + "testing" + + fw "github.com/vmfunc/sif/internal/scan/frameworks" +) + +func TestFrontendLibDetectors_Positive(t *testing.T) { + tests := []struct { + name string + detector fw.Detector + body string + }{ + {"Alpine x-data only", &alpineDetector{}, `
`}, + {"Alpine cdn plus cloak", &alpineDetector{}, `
`}, + {"Qwik container only", &qwikDetector{}, ``}, + {"Qwik bootstrap", &qwikDetector{}, ``}, + {"jQuery cdn", &jqueryDetector{}, ``}, + {"jQuery googleapis", &jqueryDetector{}, ``}, + {"jQuery wp bundled", &jqueryDetector{}, ``}, + {"Livewire component", &livewireDetector{}, `
`}, + {"Stimulus controller", &stimulusDetector{}, `
`}, + {"Turbo frame", &turboDetector{}, `x`}, + {"Turbo track only", &turboDetector{}, ``}, + {"Knockout bindings", &knockoutDetector{}, ``}, + {"Unpoly script", &unpolyDetector{}, ``}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf, _ := tt.detector.Detect(tt.body, http.Header{}) + if conf <= 0.5 { + t.Errorf("%s: confidence = %.3f, want > 0.5", tt.name, conf) + } + }) + } +} + +func TestFrontendLibDetectors_Negative(t *testing.T) { + tests := []struct { + name string + detector fw.Detector + body string + }{ + {"Alpine prose", &alpineDetector{}, `

Alpine.js is a lightweight framework.

`}, + {"Vue at-click not Alpine", &alpineDetector{}, `
`}, + {"Alpine max-data substring", &alpineDetector{}, `x
`}, + {"Qwik prose", &qwikDetector{}, `

Qwik is a resumable framework, see qwik.dev for details.

`}, + {"jQuery prose", &jqueryDetector{}, `

migrating off jquery this year.

`}, + {"Livewire single directive", &livewireDetector{}, ``}, + {"Livewire prose", &livewireDetector{}, `

Livewire is a full-stack framework for Laravel.

`}, + {"Stimulus prose", &stimulusDetector{}, `

Stimulus is a modest JavaScript framework for the HTML you already have.

`}, + {"Stimulus generic data-action", &stimulusDetector{}, ``}, + {"Turbo prose og", &turboDetector{}, `

Turbo Drive is great.

`}, + {"Turbo vs legacy turbolinks", &turboDetector{}, ``}, + {"Knockout prose", &knockoutDetector{}, `

Knockout.js is an MVVM library for JavaScript.

`}, + {"Unpoly attr substring collision", &unpolyDetector{}, ``}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf, _ := tt.detector.Detect(tt.body, http.Header{}) + if conf > 0.5 { + t.Errorf("%s: confidence = %.3f, want <= 0.5", tt.name, conf) + } + }) + } +} + +func TestFrontendLibDetectors_Version(t *testing.T) { + tests := []struct { + name string + detector fw.Detector + body string + want string + }{ + {"jQuery filename", &jqueryDetector{}, ``, "3.6.0"}, + {"jQuery googleapis path", &jqueryDetector{}, ``, "3.7.1"}, + {"Alpine cdn", &alpineDetector{}, `
`, "3.13.0"}, + {"Qwik attr", &qwikDetector{}, ``, "1.5.0"}, + {"Knockout filename", &knockoutDetector{}, ``, "3.5.1"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, version := tt.detector.Detect(tt.body, http.Header{}) + if version != tt.want { + t.Errorf("%s: version = %q, want %q", tt.name, version, tt.want) + } + }) + } +} From 47e28fa57d204b7cb369f3f2788fa424013b3292 Mon Sep 17 00:00:00 2001 From: TBX3D <88289044+TBX3D@users.noreply.github.com> Date: Thu, 25 Jun 2026 19:14:31 -0700 Subject: [PATCH 03/11] feat(frameworks): add 8 hosting and edge platform detectors add a hosting.go detector group that identifies the hosting platform or edge/cdn provider in front of a target rather than its application framework. grouped in one file so the category can be reviewed or excluded as a unit. hosting (5): Vercel, Netlify, GitHub Pages, Fly.io, Amazon S3. edge and cdn (3): Cloudflare, Amazon CloudFront, Akamai. each keys on a response header the provider sets on every response and that no other provider emits (x-vercel-id, x-nf-request-id, x-github-request-id, cf-ray, x-amz-cf-id, Akamai-GRN, fly-request-id, AmazonS3), so they carry no body-side false-positive surface. provider names in header values are avoided: Akamai keys on its set headers not the bare word, which appears in akamaihd.net csp values, and GitHub on x-github-request-id not the GitHub.com server value, which can appear in a link header. Fastly was considered but dropped because its x-served-by cache markers are generic varnish, not fastly-exclusive. --- internal/scan/frameworks/detect_test.go | 21 +- internal/scan/frameworks/detectors/hosting.go | 179 ++++++++++++++++++ .../scan/frameworks/detectors/hosting_test.go | 73 +++++++ 3 files changed, 271 insertions(+), 2 deletions(-) create mode 100644 internal/scan/frameworks/detectors/hosting.go create mode 100644 internal/scan/frameworks/detectors/hosting_test.go diff --git a/internal/scan/frameworks/detect_test.go b/internal/scan/frameworks/detect_test.go index 96906d3a..f2c58486 100644 --- a/internal/scan/frameworks/detect_test.go +++ b/internal/scan/frameworks/detect_test.go @@ -709,8 +709,25 @@ func TestDetectorRegistry(t *testing.T) { t.Fatal("expected registered detectors, got none") } - // Check that some expected detectors are registered - expectedDetectors := []string{"Laravel", "Django", "React", "Vue.js", "Angular", "Next.js", "WordPress", "Astro"} + // Check that expected detectors are registered: a spot-check of the + // originals plus every detector added to the backend, cms, and meta sets. + expectedDetectors := []string{ + "Laravel", "Django", "React", "Vue.js", "Angular", "Next.js", "WordPress", "Astro", + "Tornado", "CherryPy", "Play Framework", "Sails.js", "Beego", + "JavaServer Faces", "Google Web Toolkit", "Vaadin", "ColdFusion", + "TYPO3", "Contao", "Wix", "Webflow", "HubSpot", "PrestaShop", + "Sitecore", "OpenCart", "DotNetNuke", "Liferay", + "Hugo", "Jekyll", "Docusaurus", "MkDocs", + "Alpine.js", "Qwik", "jQuery", + "Squarespace", "WooCommerce", "Craft CMS", "Concrete CMS", "Bitrix", "Blogger", + "Eleventy", "Hexo", "VuePress", "Sphinx", + "MediaWiki", "Discourse", "XenForo", "Moodle", "Plone", "Grav", + "Textpattern", "October CMS", "Statamic", "Livewire", + "Stimulus", "Turbo", "Knockout.js", "Unpoly", "Flarum", "NodeBB", + "XWiki", "Bolt CMS", "Nikola", "Publii", "ExpressionEngine", + "Vercel", "Netlify", "GitHub Pages", "Cloudflare", + "Amazon CloudFront", "Akamai", "Fly.io", "Amazon S3", + } for _, name := range expectedDetectors { if _, ok := frameworks.GetDetector(name); !ok { t.Errorf("expected detector %q to be registered", name) diff --git a/internal/scan/frameworks/detectors/hosting.go b/internal/scan/frameworks/detectors/hosting.go new file mode 100644 index 00000000..33b02c19 --- /dev/null +++ b/internal/scan/frameworks/detectors/hosting.go @@ -0,0 +1,179 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · 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" +) + +// Detectors in this file identify the hosting platform or edge/CDN provider in +// front of a target rather than its application framework. + +func init() { + fw.Register(&vercelDetector{}) + fw.Register(&netlifyDetector{}) + fw.Register(&githubPagesDetector{}) + fw.Register(&cloudflareDetector{}) + fw.Register(&cloudfrontDetector{}) + fw.Register(&akamaiDetector{}) + fw.Register(&flyDetector{}) + fw.Register(&amazonS3Detector{}) +} + +// vercelDetector detects the Vercel hosting platform. +type vercelDetector struct{} + +func (d *vercelDetector) Name() string { return "Vercel" } + +func (d *vercelDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "x-vercel", Weight: 0.6, HeaderOnly: true}, + } +} + +func (d *vercelDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + return sigmoidConfidence(score), "" +} + +// netlifyDetector detects the Netlify hosting platform. +type netlifyDetector struct{} + +func (d *netlifyDetector) Name() string { return "Netlify" } + +func (d *netlifyDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "x-nf-request-id", Weight: 0.6, HeaderOnly: true}, + } +} + +func (d *netlifyDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + return sigmoidConfidence(score), "" +} + +// githubPagesDetector detects GitHub Pages (and GitHub-hosted infrastructure). +type githubPagesDetector struct{} + +func (d *githubPagesDetector) Name() string { return "GitHub Pages" } + +func (d *githubPagesDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "x-github-request-id", Weight: 0.6, HeaderOnly: true}, + } +} + +func (d *githubPagesDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + return sigmoidConfidence(score), "" +} + +// cloudflareDetector detects the Cloudflare edge. +type cloudflareDetector struct{} + +func (d *cloudflareDetector) Name() string { return "Cloudflare" } + +func (d *cloudflareDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "cf-ray", Weight: 0.6, HeaderOnly: true}, + } +} + +func (d *cloudflareDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + return sigmoidConfidence(score), "" +} + +// cloudfrontDetector detects the Amazon CloudFront CDN. +type cloudfrontDetector struct{} + +func (d *cloudfrontDetector) Name() string { return "Amazon CloudFront" } + +func (d *cloudfrontDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "x-amz-cf-id", Weight: 0.6, HeaderOnly: true}, + } +} + +func (d *cloudfrontDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + return sigmoidConfidence(score), "" +} + +// akamaiDetector detects the Akamai edge. +type akamaiDetector struct{} + +func (d *akamaiDetector) Name() string { return "Akamai" } + +func (d *akamaiDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "Akamai-GRN", Weight: 0.5, HeaderOnly: true}, + {Pattern: "x-akamai", Weight: 0.4, HeaderOnly: true}, + {Pattern: "AkamaiGHost", Weight: 0.4, HeaderOnly: true}, + } +} + +func (d *akamaiDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + return sigmoidConfidence(score), "" +} + +// flyDetector detects the Fly.io platform. +type flyDetector struct{} + +func (d *flyDetector) Name() string { return "Fly.io" } + +func (d *flyDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "fly-request-id", Weight: 0.6, HeaderOnly: true}, + } +} + +func (d *flyDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + return sigmoidConfidence(score), "" +} + +// amazonS3Detector detects content served from an Amazon S3 bucket. +type amazonS3Detector struct{} + +func (d *amazonS3Detector) Name() string { return "Amazon S3" } + +func (d *amazonS3Detector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "AmazonS3", Weight: 0.6, HeaderOnly: true}, + {Pattern: "x-amz-bucket-region", Weight: 0.4, HeaderOnly: true}, + } +} + +func (d *amazonS3Detector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + return sigmoidConfidence(score), "" +} diff --git a/internal/scan/frameworks/detectors/hosting_test.go b/internal/scan/frameworks/detectors/hosting_test.go new file mode 100644 index 00000000..7221b475 --- /dev/null +++ b/internal/scan/frameworks/detectors/hosting_test.go @@ -0,0 +1,73 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +package detectors + +import ( + "net/http" + "testing" + + fw "github.com/vmfunc/sif/internal/scan/frameworks" +) + +func TestHostingDetectors_Positive(t *testing.T) { + tests := []struct { + name string + detector fw.Detector + headers http.Header + }{ + {"Vercel", &vercelDetector{}, hdr("x-vercel-id", "sfo1::pdx1::dmmpr-1782439760448-608ebdfc")}, + {"Netlify", &netlifyDetector{}, hdr("x-nf-request-id", "01KW0V0MYRFJKYYNDRP7KC5DAJ")}, + {"GitHub Pages", &githubPagesDetector{}, hdr("x-github-request-id", "8714:20C798:191901:19DD8B:6A3DDD67")}, + {"Cloudflare", &cloudflareDetector{}, hdr("cf-ray", "a118ab5d9e7867e8-SJC")}, + {"CloudFront", &cloudfrontDetector{}, hdr("x-amz-cf-id", "0MsbJpMBovZpIG2KNmafF4RVM4GXD_iKAnm9friazwXUpC")}, + {"Akamai GRN", &akamaiDetector{}, hdr("Akamai-GRN", "0.1ea7cb17.1782439763.4a41c389")}, + {"Akamai server", &akamaiDetector{}, hdr("Server", "AkamaiGHost")}, + {"Fly.io", &flyDetector{}, hdr("fly-request-id", "01KW0V0QBEWMQ51YPTNZKE3EYJ-sjc")}, + {"Amazon S3 server", &amazonS3Detector{}, hdr("Server", "AmazonS3")}, + {"Amazon S3 region", &amazonS3Detector{}, hdr("x-amz-bucket-region", "us-east-2")}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf, _ := tt.detector.Detect("", tt.headers) + if conf <= 0.5 { + t.Errorf("%s: confidence = %.3f, want > 0.5", tt.name, conf) + } + }) + } +} + +func TestHostingDetectors_Negative(t *testing.T) { + tests := []struct { + name string + detector fw.Detector + headers http.Header + }{ + {"Vercel plain", &vercelDetector{}, hdr("Server", "nginx/1.25.3")}, + {"Netlify plain", &netlifyDetector{}, hdr("Server", "nginx")}, + {"Cloudflare plain", &cloudflareDetector{}, hdr("Server", "nginx")}, + {"GitHub link header", &githubPagesDetector{}, hdr("Link", "; rel=canonical")}, + {"Akamai csp asset", &akamaiDetector{}, hdr("Content-Security-Policy", "img-src https://example.akamaihd.net")}, + {"S3 generic amz id", &amazonS3Detector{}, hdr("x-amz-request-id", "4Y0WES8AVK3ZQ98N")}, + {"Fly plain", &flyDetector{}, hdr("Server", "Cowboy")}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf, _ := tt.detector.Detect("", tt.headers) + if conf > 0.5 { + t.Errorf("%s: confidence = %.3f, want <= 0.5", tt.name, conf) + } + }) + } +} From b2b25e7ce23a76650d0e4077aeb2a03a6914601c Mon Sep 17 00:00:00 2001 From: TBX3D <88289044+TBX3D@users.noreply.github.com> Date: Thu, 25 Jun 2026 18:59:27 -0700 Subject: [PATCH 04/11] feat(frameworks): add 5 framework detectors add detectors anchored on structural markers (framework-specific attribute, namespaced token, or generator meta) rather than a bare brand substring, with prose and bare-brand-og traps in the tests. wiki and cms (3): XWiki, Bolt CMS, ExpressionEngine. static site generators (2): Nikola, Publii. every fingerprint was verified against the vendor's own live output. XWiki keys on its data-xwiki- attributes and /xwiki/bin/ route, ExpressionEngine on its exp_ namespaced csrf token and session cookies, and Bolt, Nikola, and Publii on the generator-attribute prefix so a share card whose title is the brand cannot trip them. registers the new names in the registry check. --- internal/scan/frameworks/detect_test.go | 3 + internal/scan/frameworks/detectors/meta.go | 248 +++++++++++++++++++++ 2 files changed, 251 insertions(+) diff --git a/internal/scan/frameworks/detect_test.go b/internal/scan/frameworks/detect_test.go index f2c58486..7d593b8a 100644 --- a/internal/scan/frameworks/detect_test.go +++ b/internal/scan/frameworks/detect_test.go @@ -725,8 +725,11 @@ func TestDetectorRegistry(t *testing.T) { "Textpattern", "October CMS", "Statamic", "Livewire", "Stimulus", "Turbo", "Knockout.js", "Unpoly", "Flarum", "NodeBB", "XWiki", "Bolt CMS", "Nikola", "Publii", "ExpressionEngine", +<<<<<<< HEAD "Vercel", "Netlify", "GitHub Pages", "Cloudflare", "Amazon CloudFront", "Akamai", "Fly.io", "Amazon S3", +======= +>>>>>>> 579e7158 (feat(frameworks): add 5 framework detectors) } for _, name := range expectedDetectors { if _, ok := frameworks.GetDetector(name); !ok { diff --git a/internal/scan/frameworks/detectors/meta.go b/internal/scan/frameworks/detectors/meta.go index 55054c68..fa4bde45 100644 --- a/internal/scan/frameworks/detectors/meta.go +++ b/internal/scan/frameworks/detectors/meta.go @@ -33,6 +33,16 @@ func init() { fw.Register(&gatsbyDetector{}) fw.Register(&remixDetector{}) fw.Register(&astroDetector{}) + fw.Register(&hugoDetector{}) + fw.Register(&jekyllDetector{}) + fw.Register(&docusaurusDetector{}) + fw.Register(&mkdocsDetector{}) + fw.Register(&eleventyDetector{}) + fw.Register(&hexoDetector{}) + fw.Register(&vuepressDetector{}) + fw.Register(&sphinxDetector{}) + fw.Register(&nikolaDetector{}) + fw.Register(&publiiDetector{}) } // nextjsDetector detects Next.js framework. @@ -191,3 +201,241 @@ func (d *astroDetector) Detect(body string, headers http.Header) (float32, strin } return confidence, version } + +// The generator detectors below anchor on the content=" value: real +// sites minify and reorder the meta, dropping the name="generator" prefix. + +// hugoDetector detects the Hugo static site generator. +type hugoDetector struct{} + +func (d *hugoDetector) Name() string { return "Hugo" } + +func (d *hugoDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `content="Hugo 0.`, Weight: 0.6}, + } +} + +func (d *hugoDetector) 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 +} + +// jekyllDetector detects the Jekyll static site generator. +type jekyllDetector struct{} + +func (d *jekyllDetector) Name() string { return "Jekyll" } + +func (d *jekyllDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `content="Jekyll v`, Weight: 0.6}, + } +} + +func (d *jekyllDetector) 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 +} + +// docusaurusDetector detects the Docusaurus documentation site generator. +type docusaurusDetector struct{} + +func (d *docusaurusDetector) Name() string { return "Docusaurus" } + +func (d *docusaurusDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `content="Docusaurus v`, Weight: 0.6}, + } +} + +func (d *docusaurusDetector) 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 +} + +// mkdocsDetector detects MkDocs (including the Material theme). +type mkdocsDetector struct{} + +func (d *mkdocsDetector) Name() string { return "MkDocs" } + +func (d *mkdocsDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `content="mkdocs-`, Weight: 0.6}, + } +} + +func (d *mkdocsDetector) 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 +} + +// The generator detectors below anchor on the generator-attribute prefix +// (generator" content=") rather than a bare brand value. + +// eleventyDetector detects the Eleventy (11ty) static site generator. +type eleventyDetector struct{} + +func (d *eleventyDetector) Name() string { return "Eleventy" } + +func (d *eleventyDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `generator" content="Eleventy`, Weight: 0.6}, + } +} + +func (d *eleventyDetector) 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 +} + +// hexoDetector detects the Hexo static site generator. +type hexoDetector struct{} + +func (d *hexoDetector) Name() string { return "Hexo" } + +func (d *hexoDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `generator" content="Hexo`, Weight: 0.6}, + } +} + +func (d *hexoDetector) 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 +} + +// vuepressDetector detects VuePress. +type vuepressDetector struct{} + +func (d *vuepressDetector) Name() string { return "VuePress" } + +func (d *vuepressDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `generator" content="VuePress`, Weight: 0.6}, + } +} + +func (d *vuepressDetector) 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 +} + +// sphinxDetector detects the Sphinx documentation generator. +type sphinxDetector struct{} + +func (d *sphinxDetector) Name() string { return "Sphinx" } + +func (d *sphinxDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "_static/documentation_options.js", Weight: 0.6}, + {Pattern: "sphinx-doc.org", Weight: 0.3}, + {Pattern: "_static/doctools.js", Weight: 0.3}, + } +} + +func (d *sphinxDetector) 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 +} + +// nikolaDetector detects the Nikola static site generator. +type nikolaDetector struct{} + +func (d *nikolaDetector) Name() string { return "Nikola" } + +func (d *nikolaDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `generator" content="Nikola`, Weight: 0.6}, + } +} + +func (d *nikolaDetector) 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 +} + +// publiiDetector detects the Publii static-site CMS. +type publiiDetector struct{} + +func (d *publiiDetector) Name() string { return "Publii" } + +func (d *publiiDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: `generator" content="Publii`, Weight: 0.6}, + } +} + +func (d *publiiDetector) 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 +} From de7740cfa07136dd9368f93856bb02b252cae7b7 Mon Sep 17 00:00:00 2001 From: TBX3D <88289044+TBX3D@users.noreply.github.com> Date: Thu, 25 Jun 2026 18:40:13 -0700 Subject: [PATCH 05/11] feat(frameworks): add 6 framework detectors add detectors anchored on structural markers (bootstrap element id, asset path, framework-specific attribute, or script reference) rather than a bare brand substring, with prose and substring-collision traps in the tests. forums (2): Flarum, NodeBB. frontend (4): Stimulus, Turbo, Knockout.js, Unpoly. every fingerprint was verified against the vendor's own live output. Stimulus keys on its data-controller directive and Turbo on the turbo-frame element and data-turbo attributes (both ship with Hotwire), Knockout on data-bind and ko.applyBindings, Flarum on its flarum- prefixed bootstrap id and /assets/forum- bundle path, and NodeBB on its assets/nodebb path. Unpoly is anchored on its unpoly.min.js script rather than its up- attributes, which are substrings of common words (popup, sign-up) and of Stimulus's data-name-target. adds a Knockout version pattern and registers the new names in the registry check. --- internal/scan/frameworks/detect_test.go | 3 -- internal/scan/frameworks/detectors/cms.go | 7 +++ .../scan/frameworks/detectors/frontend.go | 9 ++++ internal/scan/frameworks/version.go | 47 +++++++++++++++++++ 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/internal/scan/frameworks/detect_test.go b/internal/scan/frameworks/detect_test.go index 7d593b8a..f2c58486 100644 --- a/internal/scan/frameworks/detect_test.go +++ b/internal/scan/frameworks/detect_test.go @@ -725,11 +725,8 @@ func TestDetectorRegistry(t *testing.T) { "Textpattern", "October CMS", "Statamic", "Livewire", "Stimulus", "Turbo", "Knockout.js", "Unpoly", "Flarum", "NodeBB", "XWiki", "Bolt CMS", "Nikola", "Publii", "ExpressionEngine", -<<<<<<< HEAD "Vercel", "Netlify", "GitHub Pages", "Cloudflare", "Amazon CloudFront", "Akamai", "Fly.io", "Amazon S3", -======= ->>>>>>> 579e7158 (feat(frameworks): add 5 framework detectors) } for _, name := range expectedDetectors { if _, ok := frameworks.GetDetector(name); !ok { diff --git a/internal/scan/frameworks/detectors/cms.go b/internal/scan/frameworks/detectors/cms.go index 706f6b88..e35e5c4f 100644 --- a/internal/scan/frameworks/detectors/cms.go +++ b/internal/scan/frameworks/detectors/cms.go @@ -819,6 +819,10 @@ func (d *flarumDetector) Name() string { return "Flarum" } func (d *flarumDetector) Signatures() []fw.Signature { return []fw.Signature{ {Pattern: `id="flarum-`, Weight: 0.6}, +<<<<<<< HEAD +======= + {Pattern: "/assets/forum-", Weight: 0.4}, +>>>>>>> ba041df1 (feat(frameworks): add 6 framework detectors) } } @@ -856,6 +860,7 @@ func (d *nodebbDetector) Detect(body string, headers http.Header) (float32, stri } return confidence, version } +<<<<<<< HEAD // xwikiDetector detects the XWiki platform. type xwikiDetector struct{} @@ -928,3 +933,5 @@ func (d *expressionengineDetector) Detect(body string, headers http.Header) (flo } return confidence, version } +======= +>>>>>>> ba041df1 (feat(frameworks): add 6 framework detectors) diff --git a/internal/scan/frameworks/detectors/frontend.go b/internal/scan/frameworks/detectors/frontend.go index 205209cb..6d8f19dc 100644 --- a/internal/scan/frameworks/detectors/frontend.go +++ b/internal/scan/frameworks/detectors/frontend.go @@ -353,7 +353,11 @@ func (d *stimulusDetector) Name() string { return "Stimulus" } func (d *stimulusDetector) Signatures() []fw.Signature { return []fw.Signature{ {Pattern: "data-controller=", Weight: 0.5}, +<<<<<<< HEAD {Pattern: "data-action=", Weight: 0.3}, +======= + {Pattern: "data-action=", Weight: 0.4}, +>>>>>>> ba041df1 (feat(frameworks): add 6 framework detectors) {Pattern: "@hotwired/stimulus", Weight: 0.4}, } } @@ -378,8 +382,13 @@ func (d *turboDetector) Name() string { return "Turbo" } func (d *turboDetector) Signatures() []fw.Signature { return []fw.Signature{ {Pattern: ">>>>>> ba041df1 (feat(frameworks): add 6 framework detectors) } } diff --git a/internal/scan/frameworks/version.go b/internal/scan/frameworks/version.go index 5a8853d8..ac036a67 100644 --- a/internal/scan/frameworks/version.go +++ b/internal/scan/frameworks/version.go @@ -148,6 +148,51 @@ func init() { {`Astro[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"}, {`"astro":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"}, }, + "Hugo": { + {`content="Hugo (\d+\.\d+(?:\.\d+)?)`, 0.95, "generator meta"}, + }, + "Jekyll": { + {`content="Jekyll v(\d+\.\d+(?:\.\d+)?)`, 0.95, "generator meta"}, + }, + "Docusaurus": { + {`content="Docusaurus v(\d+\.\d+(?:\.\d+)?)`, 0.95, "generator meta"}, + }, + "MkDocs": { + {`content="mkdocs-(\d+\.\d+(?:\.\d+)?)`, 0.9, "generator meta"}, + }, + "TYPO3": { + {`content="TYPO3 (\d+\.\d+(?:\.\d+)?)`, 0.9, "generator meta"}, + }, + "Eleventy": { + {`content="Eleventy[^"]*?v(\d+\.\d+(?:\.\d+)?)`, 0.9, "generator meta"}, + }, + "Hexo": { + {`content="Hexo (\d+\.\d+(?:\.\d+)?)`, 0.9, "generator meta"}, + }, + "VuePress": { + {`content="VuePress (\d+\.\d+(?:\.\d+)?)`, 0.9, "generator meta"}, + }, + "jQuery": { + {`jquery-(\d+\.\d+(?:\.\d+)?)(?:\.min)?\.js`, 0.9, "script filename"}, + {`jquery@(\d+\.\d+(?:\.\d+)?)`, 0.85, "CDN reference"}, + {`/jquery/(\d+\.\d+(?:\.\d+)?)/`, 0.85, "CDN path"}, + {`jQuery v(\d+\.\d+(?:\.\d+)?)`, 0.9, "library banner"}, + }, + "Alpine.js": { + {`alpinejs@(\d+\.\d+(?:\.\d+)?)`, 0.85, "CDN reference"}, + }, + "Qwik": { + {`q:version="(\d+\.\d+(?:\.\d+)?)"`, 0.9, "container attribute"}, + }, + "MediaWiki": { + {`content="MediaWiki (\d+\.\d+(?:\.\d+)?)`, 0.9, "generator meta"}, + }, + "Discourse": { + {`content="Discourse (\d+\.\d+(?:\.\d+)?)`, 0.9, "generator meta"}, + }, + "Knockout.js": { + {`knockout-(\d+\.\d+(?:\.\d+)?)`, 0.9, "script filename"}, + }, } // Compile all patterns @@ -224,6 +269,8 @@ func extractVersion(text string, framework string) VersionMatch { return bestMatch } +// isValidVersionString checks if a version string is digits and dots only, with +// at most three dots. func isValidVersionString(v string) bool { if v == "" || len(v) > 20 { return false From 72ac3574f6308a746dcca62abb626f06afa9f4b6 Mon Sep 17 00:00:00 2001 From: TBX3D <88289044+TBX3D@users.noreply.github.com> Date: Thu, 25 Jun 2026 18:22:35 -0700 Subject: [PATCH 06/11] feat(frameworks): add 10 framework detectors add detectors across forum/wiki/lms, cms, and frontend categories, each anchored on a structural marker (generator meta, response header, cookie, or a framework-specific dom attribute) rather than a bare brand substring, with prose and bare-brand-og traps in the tests. forums, wiki, and lms (4): MediaWiki, Discourse, XenForo, Moodle. cms (5): Plone, Grav, Textpattern, October CMS, Statamic. frontend (1): Livewire. every fingerprint was verified against the vendor's own live output. the generator-meta detectors (MediaWiki, Discourse, Plone, Textpattern) anchor on the attribute prefix, Grav keys on its compound content="GravCMS" token, XenForo on the data-xf-init attribute and /js/xf/ script path, and Moodle and October on their session cookies. Moodle stays cookie-only because the M.cfg config global is short enough to collide with minified javascript. adds version patterns for MediaWiki and Discourse, and registers the new names in the registry check. --- internal/scan/frameworks/detectors/cms.go | 7 ------- internal/scan/frameworks/detectors/frontend.go | 9 --------- 2 files changed, 16 deletions(-) diff --git a/internal/scan/frameworks/detectors/cms.go b/internal/scan/frameworks/detectors/cms.go index e35e5c4f..706f6b88 100644 --- a/internal/scan/frameworks/detectors/cms.go +++ b/internal/scan/frameworks/detectors/cms.go @@ -819,10 +819,6 @@ func (d *flarumDetector) Name() string { return "Flarum" } func (d *flarumDetector) Signatures() []fw.Signature { return []fw.Signature{ {Pattern: `id="flarum-`, Weight: 0.6}, -<<<<<<< HEAD -======= - {Pattern: "/assets/forum-", Weight: 0.4}, ->>>>>>> ba041df1 (feat(frameworks): add 6 framework detectors) } } @@ -860,7 +856,6 @@ func (d *nodebbDetector) Detect(body string, headers http.Header) (float32, stri } return confidence, version } -<<<<<<< HEAD // xwikiDetector detects the XWiki platform. type xwikiDetector struct{} @@ -933,5 +928,3 @@ func (d *expressionengineDetector) Detect(body string, headers http.Header) (flo } return confidence, version } -======= ->>>>>>> ba041df1 (feat(frameworks): add 6 framework detectors) diff --git a/internal/scan/frameworks/detectors/frontend.go b/internal/scan/frameworks/detectors/frontend.go index 6d8f19dc..205209cb 100644 --- a/internal/scan/frameworks/detectors/frontend.go +++ b/internal/scan/frameworks/detectors/frontend.go @@ -353,11 +353,7 @@ func (d *stimulusDetector) Name() string { return "Stimulus" } func (d *stimulusDetector) Signatures() []fw.Signature { return []fw.Signature{ {Pattern: "data-controller=", Weight: 0.5}, -<<<<<<< HEAD {Pattern: "data-action=", Weight: 0.3}, -======= - {Pattern: "data-action=", Weight: 0.4}, ->>>>>>> ba041df1 (feat(frameworks): add 6 framework detectors) {Pattern: "@hotwired/stimulus", Weight: 0.4}, } } @@ -382,13 +378,8 @@ func (d *turboDetector) Name() string { return "Turbo" } func (d *turboDetector) Signatures() []fw.Signature { return []fw.Signature{ {Pattern: ">>>>>> ba041df1 (feat(frameworks): add 6 framework detectors) } } From f64fe010d6516fc7a866814f9f25215a0ac5833c Mon Sep 17 00:00:00 2001 From: TBX3D <88289044+TBX3D@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:51:59 -0700 Subject: [PATCH 07/11] feat(frameworks): add 23 framework detectors add fingerprints across three categories, each anchored on a structural marker (cookie, response header, generator meta, or a framework-specific DOM attribute) rather than a bare brand substring, with prose and reversed/minified-attribute traps in the tests. backend (9): Tornado, CherryPy, Play Framework, Sails.js, Beego, JavaServer Faces, Google Web Toolkit, Vaadin, ColdFusion. cms and site builders (10): TYPO3, Contao, Wix, Webflow, HubSpot, PrestaShop, Sitecore, OpenCart, DotNetNuke, Liferay. static site generators (4): Hugo, Jekyll, Docusaurus, MkDocs. the generator-meta detectors anchor on a structural slice of the real tag (content="Hugo 0., content="mkdocs-, generator" content="TYPO3) so a page that merely names the brand in an og or twitter title cannot trip them, while still surviving the attribute-name minification and reversed attribute order seen in the wild. Webflow keys on the data-wf-page attribute so it catches published sites that drop the generator meta. adds generator version patterns for Hugo, Jekyll, Docusaurus, MkDocs, and TYPO3, a Server-header version helper for Tornado and CherryPy, and registers the new names in the registry check. --- internal/scan/frameworks/detectors/backend.go | 3 ++- internal/scan/frameworks/detectors/backend_test.go | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/scan/frameworks/detectors/backend.go b/internal/scan/frameworks/detectors/backend.go index 1ab5abb7..56a83c7b 100644 --- a/internal/scan/frameworks/detectors/backend.go +++ b/internal/scan/frameworks/detectors/backend.go @@ -468,6 +468,7 @@ func (d *codeigniterDetector) Detect(body string, headers http.Header) (float32, } return confidence, version } + // tornadoDetector detects the Tornado Python web framework. type tornadoDetector struct{} @@ -475,7 +476,7 @@ func (d *tornadoDetector) Name() string { return "Tornado" } func (d *tornadoDetector) Signatures() []fw.Signature { return []fw.Signature{ - {Pattern: "TornadoServer", Weight: 0.6, HeaderOnly: true}, + {Pattern: "TornadoServer", Weight: 0.6, HeaderOnly: true, Header: "Server"}, } } diff --git a/internal/scan/frameworks/detectors/backend_test.go b/internal/scan/frameworks/detectors/backend_test.go index 39cba961..51f4a3f0 100644 --- a/internal/scan/frameworks/detectors/backend_test.go +++ b/internal/scan/frameworks/detectors/backend_test.go @@ -84,6 +84,7 @@ func TestWebFrameworkDetectors_Negative(t *testing.T) { {"Express checkout cookie", &expressDetector{}, "", hdr("Set-Cookie", "express_checkout=1; path=/")}, {"Flask werkzeug docs link", &flaskDetector{}, "", hdr("Link", "; rel=help")}, {"CherryPy domain link", &cherrypyDetector{}, "", hdr("Link", "; rel=help")}, + {"Tornado via header", &tornadoDetector{}, "", hdr("Via", "1.1 proxy-fronting-tornadoserver")}, {"Symfony domain link", &symfonyDetector{}, "", hdr("Link", "; rel=help")}, {"Spring Boot tutorial prose", &springBootDetector{}, `
To fix the Whitelabel Error Page in Spring Boot, add a controller.
`, http.Header{}}, {"plain page Tornado", &tornadoDetector{}, "hello", hdr("Server", "nginx/1.25.3")}, From cc1b307d2010bc3d2df3236d6213ee0d4be83c31 Mon Sep 17 00:00:00 2001 From: TBX3D <88289044+TBX3D@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:50:58 -0700 Subject: [PATCH 08/11] fix(frameworks): tighten cakephp and strapi against prose both detectors matched a bare lowercase body substring, so any page that named the framework in prose or a tag link scored as a positive: cakephp fired on the stackoverflow tag page, strapi on any article mentioning it. drop the body substrings and key on structural markers instead: - cakephp: the CAKEPHP session cookie - strapi: the X-Powered-By Strapi response header extends the bare-substring cleanup from #133. adds prose-trap negatives and corrects the integration fixtures that encoded the old behaviour. --- internal/scan/frameworks/detect_test.go | 16 ++++++++++------ internal/scan/frameworks/detectors/backend.go | 1 - internal/scan/frameworks/version.go | 1 - 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/internal/scan/frameworks/detect_test.go b/internal/scan/frameworks/detect_test.go index f2c58486..70036e8f 100644 --- a/internal/scan/frameworks/detect_test.go +++ b/internal/scan/frameworks/detect_test.go @@ -859,8 +859,9 @@ func TestDetectFramework_Backbone(t *testing.T) { func TestDetectFramework_CakePHPFalsePositive(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) - w.Write([]byte(`

our cupcake and cheesecake recipes, - plus the best pancake stack in town.

`)) + // a Q&A/listicle page that merely names the framework, as on the live + // stackoverflow homepage that the bare body substring used to misfire on + w.Write([]byte(`cakephp`)) })) defer server.Close() @@ -869,7 +870,7 @@ func TestDetectFramework_CakePHPFalsePositive(t *testing.T) { t.Fatalf("unexpected error: %v", err) } if result != nil && result.Name == "CakePHP" { - t.Errorf("false positive: detected CakePHP (confidence %.2f) on prose about cakes", result.Confidence) + t.Errorf("false positive: detected CakePHP (confidence %.2f) on prose naming cakephp", result.Confidence) } } @@ -910,7 +911,8 @@ func TestDetectFramework_SvelteFalsePositive(t *testing.T) { func TestDetectFramework_StrapiFalsePositive(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) - w.Write([]byte(``)) + // prose naming the CMS plus a plain /api/ path: neither is the powered-by header + w.Write([]byte(`

built with Strapi

`)) })) defer server.Close() @@ -919,14 +921,16 @@ func TestDetectFramework_StrapiFalsePositive(t *testing.T) { t.Fatalf("unexpected error: %v", err) } if result != nil && result.Name == "Strapi" { - t.Errorf("false positive: detected Strapi (confidence %.2f) on a plain /api/ path", result.Confidence) + t.Errorf("false positive: detected Strapi (confidence %.2f) on prose naming strapi", result.Confidence) } } func TestDetectFramework_Strapi(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // the default poweredBy middleware sets this header on every response + w.Header().Set("X-Powered-By", "Strapi ") w.WriteHeader(http.StatusOK) - w.Write([]byte(`
powered by strapi
`)) + w.Write([]byte(`
welcome
`)) })) defer server.Close() diff --git a/internal/scan/frameworks/detectors/backend.go b/internal/scan/frameworks/detectors/backend.go index 56a83c7b..bc0dae88 100644 --- a/internal/scan/frameworks/detectors/backend.go +++ b/internal/scan/frameworks/detectors/backend.go @@ -429,7 +429,6 @@ func (d *cakephpDetector) Name() string { return "CakePHP" } func (d *cakephpDetector) Signatures() []fw.Signature { return []fw.Signature{ - {Pattern: "cakephp", Weight: 0.4}, {Pattern: "CAKEPHP", Weight: 0.4, HeaderOnly: true}, } } diff --git a/internal/scan/frameworks/version.go b/internal/scan/frameworks/version.go index ac036a67..9180001e 100644 --- a/internal/scan/frameworks/version.go +++ b/internal/scan/frameworks/version.go @@ -34,7 +34,6 @@ type compiledVersionPattern struct { } // frameworkVersionPatterns maps framework names to their pre-compiled version patterns. -// Patterns are compiled once at package initialization for optimal performance. var frameworkVersionPatterns map[string][]compiledVersionPattern func init() { From 13a6ac042da0cc0ee1a917d5dec7d3d800c2316b Mon Sep 17 00:00:00 2001 From: TBX3D <88289044+TBX3D@users.noreply.github.com> Date: Thu, 9 Jul 2026 17:48:58 -0700 Subject: [PATCH 09/11] fix(frameworks): tighten stimulus and symfony against solo false positives stimulus: the data-controller= marker had enough weight to clear the detection threshold alone, so a generic hand-rolled data-controller attribute with no other Hotwire markers fired at 0.76 confidence. lower data-controller= and data-action= so neither fires solo; only their combination or the definitive @hotwired/stimulus import clears 0.5 now. symfony: the bare "sf_" cookie substring scored exactly 0.5 against an unrelated cookie like misfa_sf_token, one float away from a false positive. drop it and key on the "_sf2_" session-cookie prefix instead, which is specific to symfony's own naming. each fix is folded into the detector tests as a regression, alongside a positive case proving the real marker still fires. --- internal/scan/frameworks/detectors/backend.go | 4 +++- internal/scan/frameworks/detectors/backend_test.go | 2 ++ internal/scan/frameworks/detectors/frontend.go | 10 +++++++--- internal/scan/frameworks/detectors/frontend_test.go | 2 ++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/internal/scan/frameworks/detectors/backend.go b/internal/scan/frameworks/detectors/backend.go index bc0dae88..a32d880b 100644 --- a/internal/scan/frameworks/detectors/backend.go +++ b/internal/scan/frameworks/detectors/backend.go @@ -334,7 +334,9 @@ func (d *symfonyDetector) Name() string { return "Symfony" } func (d *symfonyDetector) Signatures() []fw.Signature { return []fw.Signature{ {Pattern: "X-Debug-Token", Weight: 0.4, HeaderOnly: true}, - {Pattern: "sf_", Weight: 0.3, HeaderOnly: true}, + // the bare "sf_" substring also matches unrelated cookie names such as + // "misfa_sf_token", so key on the "_sf2_" session-cookie prefix, which + // is specific to Symfony's own cookie naming instead. {Pattern: "_sf2_", Weight: 0.3, HeaderOnly: true}, } } diff --git a/internal/scan/frameworks/detectors/backend_test.go b/internal/scan/frameworks/detectors/backend_test.go index 51f4a3f0..e507bd31 100644 --- a/internal/scan/frameworks/detectors/backend_test.go +++ b/internal/scan/frameworks/detectors/backend_test.go @@ -52,6 +52,7 @@ func TestWebFrameworkDetectors_Positive(t *testing.T) { {"Express powered-by", &expressDetector{}, "", hdr("X-Powered-By", "Express")}, {"Flask werkzeug server", &flaskDetector{}, "", hdr("Server", "Werkzeug/2.3.0 Python/3.11")}, {"Symfony debug token", &symfonyDetector{}, "", hdr("X-Debug-Token", "a1b2c3")}, + {"Symfony sf2 session cookie", &symfonyDetector{}, "", hdr("Set-Cookie", "_sf2_ses=8f2a1c4e9d3b; path=/; HttpOnly")}, {"Spring Boot whitelabel", &springBootDetector{}, `

Whitelabel Error Page

This application has no explicit mapping for /error

There was an unexpected error (type=Not Found, status=404).
`, http.Header{}}, } @@ -86,6 +87,7 @@ func TestWebFrameworkDetectors_Negative(t *testing.T) { {"CherryPy domain link", &cherrypyDetector{}, "", hdr("Link", "; rel=help")}, {"Tornado via header", &tornadoDetector{}, "", hdr("Via", "1.1 proxy-fronting-tornadoserver")}, {"Symfony domain link", &symfonyDetector{}, "", hdr("Link", "; rel=help")}, + {"Symfony near-miss cookie", &symfonyDetector{}, "", hdr("Set-Cookie", "misfa_sf_token=abc123; path=/")}, {"Spring Boot tutorial prose", &springBootDetector{}, `
To fix the Whitelabel Error Page in Spring Boot, add a controller.
`, http.Header{}}, {"plain page Tornado", &tornadoDetector{}, "hello", hdr("Server", "nginx/1.25.3")}, {"plain page Play", &playDetector{}, "", hdr("Set-Cookie", "sessionid=abc; Path=/")}, diff --git a/internal/scan/frameworks/detectors/frontend.go b/internal/scan/frameworks/detectors/frontend.go index 205209cb..03ce1821 100644 --- a/internal/scan/frameworks/detectors/frontend.go +++ b/internal/scan/frameworks/detectors/frontend.go @@ -352,9 +352,13 @@ func (d *stimulusDetector) Name() string { return "Stimulus" } func (d *stimulusDetector) Signatures() []fw.Signature { return []fw.Signature{ - {Pattern: "data-controller=", Weight: 0.5}, - {Pattern: "data-action=", Weight: 0.3}, - {Pattern: "@hotwired/stimulus", Weight: 0.4}, + // data-controller= alone is a common enough attribute name in + // unrelated hand-rolled JS that it must not clear the threshold by + // itself; neither must data-action= alone. each needs the other (or + // the definitive @hotwired/stimulus import) alongside it to fire. + {Pattern: "data-controller=", Weight: 0.2}, + {Pattern: "data-action=", Weight: 0.2}, + {Pattern: "@hotwired/stimulus", Weight: 0.6}, } } diff --git a/internal/scan/frameworks/detectors/frontend_test.go b/internal/scan/frameworks/detectors/frontend_test.go index 07317209..f1ed86b9 100644 --- a/internal/scan/frameworks/detectors/frontend_test.go +++ b/internal/scan/frameworks/detectors/frontend_test.go @@ -34,6 +34,7 @@ func TestFrontendLibDetectors_Positive(t *testing.T) { {"jQuery wp bundled", &jqueryDetector{}, ``}, {"Livewire component", &livewireDetector{}, `
`}, {"Stimulus controller", &stimulusDetector{}, `
`}, + {"Stimulus script reference", &stimulusDetector{}, ``}, {"Turbo frame", &turboDetector{}, `x`}, {"Turbo track only", &turboDetector{}, ``}, {"Knockout bindings", &knockoutDetector{}, ``}, @@ -65,6 +66,7 @@ func TestFrontendLibDetectors_Negative(t *testing.T) { {"Livewire prose", &livewireDetector{}, `

Livewire is a full-stack framework for Laravel.

`}, {"Stimulus prose", &stimulusDetector{}, `

Stimulus is a modest JavaScript framework for the HTML you already have.

`}, {"Stimulus generic data-action", &stimulusDetector{}, ``}, + {"Stimulus generic data-controller only", &stimulusDetector{}, `
2 items
`}, {"Turbo prose og", &turboDetector{}, `

Turbo Drive is great.

`}, {"Turbo vs legacy turbolinks", &turboDetector{}, ``}, {"Knockout prose", &knockoutDetector{}, `

Knockout.js is an MVVM library for JavaScript.

`}, From 0a0e08bad4e773ce871e8be294d006b0781c56fa Mon Sep 17 00:00:00 2001 From: TBX3D <88289044+TBX3D@users.noreply.github.com> Date: Thu, 9 Jul 2026 17:49:34 -0700 Subject: [PATCH 10/11] chore(frameworks): trim tautological detector doc comments roughly half the one-line comments over the batch-2 detector structs just restated the type name (e.g. "wordpressDetector detects WordPress CMS"), adding nothing a glance at the struct wouldn't already tell you. drop those and keep the ones that add real information: abbreviation expansions, vendor/former-name notes, and framework-relationship context. --- internal/scan/frameworks/detectors/backend.go | 20 -------------- internal/scan/frameworks/detectors/cms.go | 27 ------------------- .../scan/frameworks/detectors/frontend.go | 13 --------- internal/scan/frameworks/detectors/hosting.go | 5 ---- internal/scan/frameworks/detectors/meta.go | 14 ---------- 5 files changed, 79 deletions(-) diff --git a/internal/scan/frameworks/detectors/backend.go b/internal/scan/frameworks/detectors/backend.go index a32d880b..c924f1dd 100644 --- a/internal/scan/frameworks/detectors/backend.go +++ b/internal/scan/frameworks/detectors/backend.go @@ -78,7 +78,6 @@ func serverVersion(headers http.Header, product string) string { return rest[:end] } -// laravelDetector detects Laravel framework. type laravelDetector struct { fw.BaseDetector } @@ -105,7 +104,6 @@ func (d *laravelDetector) Detect(body string, headers http.Header) (float32, str return confidence, version } -// djangoDetector detects Django framework. type djangoDetector struct{} func (d *djangoDetector) Name() string { return "Django" } @@ -135,7 +133,6 @@ func (d *djangoDetector) Detect(body string, headers http.Header) (float32, stri return confidence, version } -// railsDetector detects Ruby on Rails framework. type railsDetector struct{} func (d *railsDetector) Name() string { return "Ruby on Rails" } @@ -163,7 +160,6 @@ func (d *railsDetector) Detect(body string, headers http.Header) (float32, strin return confidence, version } -// expressDetector detects Express.js framework. type expressDetector struct{} func (d *expressDetector) Name() string { return "Express.js" } @@ -187,7 +183,6 @@ func (d *expressDetector) Detect(body string, headers http.Header) (float32, str return confidence, version } -// aspnetDetector detects ASP.NET framework. type aspnetDetector struct{} func (d *aspnetDetector) Name() string { return "ASP.NET" } @@ -223,7 +218,6 @@ func (d *aspnetDetector) Detect(body string, headers http.Header) (float32, stri return confidence, version } -// aspnetCoreDetector detects ASP.NET Core framework. type aspnetCoreDetector struct{} func (d *aspnetCoreDetector) Name() string { return "ASP.NET Core" } @@ -249,7 +243,6 @@ func (d *aspnetCoreDetector) Detect(body string, headers http.Header) (float32, return confidence, version } -// springDetector detects Spring framework. type springDetector struct{} func (d *springDetector) Name() string { return "Spring" } @@ -275,7 +268,6 @@ func (d *springDetector) Detect(body string, headers http.Header) (float32, stri return confidence, version } -// springBootDetector detects Spring Boot framework. type springBootDetector struct{} func (d *springBootDetector) Name() string { return "Spring Boot" } @@ -300,7 +292,6 @@ func (d *springBootDetector) Detect(body string, headers http.Header) (float32, return confidence, version } -// flaskDetector detects Flask framework. type flaskDetector struct{} func (d *flaskDetector) Name() string { return "Flask" } @@ -326,7 +317,6 @@ func (d *flaskDetector) Detect(body string, headers http.Header) (float32, strin return confidence, version } -// symfonyDetector detects Symfony framework. type symfonyDetector struct{} func (d *symfonyDetector) Name() string { return "Symfony" } @@ -353,7 +343,6 @@ func (d *symfonyDetector) Detect(body string, headers http.Header) (float32, str return confidence, version } -// phoenixDetector detects Phoenix framework. type phoenixDetector struct{} func (d *phoenixDetector) Name() string { return "Phoenix" } @@ -378,7 +367,6 @@ func (d *phoenixDetector) Detect(body string, headers http.Header) (float32, str return confidence, version } -// strapiDetector detects Strapi framework. type strapiDetector struct{} func (d *strapiDetector) Name() string { return "Strapi" } @@ -401,7 +389,6 @@ func (d *strapiDetector) Detect(body string, headers http.Header) (float32, stri return confidence, version } -// adonisDetector detects AdonisJS framework. type adonisDetector struct{} func (d *adonisDetector) Name() string { return "AdonisJS" } @@ -424,7 +411,6 @@ func (d *adonisDetector) Detect(body string, headers http.Header) (float32, stri return confidence, version } -// cakephpDetector detects CakePHP framework. type cakephpDetector struct{} func (d *cakephpDetector) Name() string { return "CakePHP" } @@ -447,7 +433,6 @@ func (d *cakephpDetector) Detect(body string, headers http.Header) (float32, str return confidence, version } -// codeigniterDetector detects CodeIgniter framework. type codeigniterDetector struct{} func (d *codeigniterDetector) Name() string { return "CodeIgniter" } @@ -470,7 +455,6 @@ func (d *codeigniterDetector) Detect(body string, headers http.Header) (float32, return confidence, version } -// tornadoDetector detects the Tornado Python web framework. type tornadoDetector struct{} func (d *tornadoDetector) Name() string { return "Tornado" } @@ -493,7 +477,6 @@ func (d *tornadoDetector) Detect(body string, headers http.Header) (float32, str return confidence, version } -// cherrypyDetector detects the CherryPy Python web framework. type cherrypyDetector struct{} func (d *cherrypyDetector) Name() string { return "CherryPy" } @@ -541,7 +524,6 @@ func (d *playDetector) Detect(body string, headers http.Header) (float32, string return confidence, version } -// sailsDetector detects the Sails.js Node framework. type sailsDetector struct{} func (d *sailsDetector) Name() string { return "Sails.js" } @@ -564,7 +546,6 @@ func (d *sailsDetector) Detect(body string, headers http.Header) (float32, strin return confidence, version } -// beegoDetector detects the Beego Go web framework. type beegoDetector struct{} func (d *beegoDetector) Name() string { return "Beego" } @@ -636,7 +617,6 @@ func (d *gwtDetector) Detect(body string, headers http.Header) (float32, string) return confidence, version } -// vaadinDetector detects the Vaadin Java framework. type vaadinDetector struct{} func (d *vaadinDetector) Name() string { return "Vaadin" } diff --git a/internal/scan/frameworks/detectors/cms.go b/internal/scan/frameworks/detectors/cms.go index 706f6b88..7206da25 100644 --- a/internal/scan/frameworks/detectors/cms.go +++ b/internal/scan/frameworks/detectors/cms.go @@ -65,7 +65,6 @@ func init() { fw.Register(&xwikiDetector{}) } -// wordpressDetector detects WordPress CMS. type wordpressDetector struct{} func (d *wordpressDetector) Name() string { return "WordPress" } @@ -92,7 +91,6 @@ func (d *wordpressDetector) Detect(body string, headers http.Header) (float32, s return confidence, version } -// drupalDetector detects Drupal CMS. type drupalDetector struct{} func (d *drupalDetector) Name() string { return "Drupal" } @@ -118,7 +116,6 @@ func (d *drupalDetector) Detect(body string, headers http.Header) (float32, stri return confidence, version } -// joomlaDetector detects Joomla CMS. type joomlaDetector struct{} func (d *joomlaDetector) Name() string { return "Joomla" } @@ -144,7 +141,6 @@ func (d *joomlaDetector) Detect(body string, headers http.Header) (float32, stri return confidence, version } -// magentoDetector detects Magento CMS. type magentoDetector struct{} func (d *magentoDetector) Name() string { return "Magento" } @@ -170,7 +166,6 @@ func (d *magentoDetector) Detect(body string, headers http.Header) (float32, str return confidence, version } -// shopifyDetector detects Shopify platform. type shopifyDetector struct{} func (d *shopifyDetector) Name() string { return "Shopify" } @@ -196,7 +191,6 @@ func (d *shopifyDetector) Detect(body string, headers http.Header) (float32, str return confidence, version } -// ghostDetector detects Ghost CMS. type ghostDetector struct{} func (d *ghostDetector) Name() string { return "Ghost" } @@ -221,7 +215,6 @@ func (d *ghostDetector) Detect(body string, headers http.Header) (float32, strin return confidence, version } -// typo3Detector detects the TYPO3 CMS. type typo3Detector struct{} func (d *typo3Detector) Name() string { return "TYPO3" } @@ -244,7 +237,6 @@ func (d *typo3Detector) Detect(body string, headers http.Header) (float32, strin return confidence, version } -// contaoDetector detects the Contao CMS. type contaoDetector struct{} func (d *contaoDetector) Name() string { return "Contao" } @@ -267,7 +259,6 @@ func (d *contaoDetector) Detect(body string, headers http.Header) (float32, stri return confidence, version } -// wixDetector detects sites built on the Wix website builder. type wixDetector struct{} func (d *wixDetector) Name() string { return "Wix" } @@ -291,7 +282,6 @@ func (d *wixDetector) Detect(body string, headers http.Header) (float32, string) return confidence, version } -// webflowDetector detects sites built on Webflow. type webflowDetector struct{} func (d *webflowDetector) Name() string { return "Webflow" } @@ -315,7 +305,6 @@ func (d *webflowDetector) Detect(body string, headers http.Header) (float32, str return confidence, version } -// hubspotDetector detects pages built on the HubSpot CMS. type hubspotDetector struct{} func (d *hubspotDetector) Name() string { return "HubSpot" } @@ -338,7 +327,6 @@ func (d *hubspotDetector) Detect(body string, headers http.Header) (float32, str return confidence, version } -// prestashopDetector detects the PrestaShop e-commerce platform. type prestashopDetector struct{} func (d *prestashopDetector) Name() string { return "PrestaShop" } @@ -361,7 +349,6 @@ func (d *prestashopDetector) Detect(body string, headers http.Header) (float32, return confidence, version } -// sitecoreDetector detects the Sitecore platform. type sitecoreDetector struct{} func (d *sitecoreDetector) Name() string { return "Sitecore" } @@ -384,7 +371,6 @@ func (d *sitecoreDetector) Detect(body string, headers http.Header) (float32, st return confidence, version } -// opencartDetector detects the OpenCart e-commerce platform. type opencartDetector struct{} func (d *opencartDetector) Name() string { return "OpenCart" } @@ -431,7 +417,6 @@ func (d *dnnDetector) Detect(body string, headers http.Header) (float32, string) return confidence, version } -// liferayDetector detects the Liferay portal. type liferayDetector struct{} func (d *liferayDetector) Name() string { return "Liferay" } @@ -455,7 +440,6 @@ func (d *liferayDetector) Detect(body string, headers http.Header) (float32, str return confidence, version } -// squarespaceDetector detects sites built on Squarespace. type squarespaceDetector struct{} func (d *squarespaceDetector) Name() string { return "Squarespace" } @@ -504,7 +488,6 @@ func (d *woocommerceDetector) Detect(body string, headers http.Header) (float32, return confidence, version } -// craftDetector detects Craft CMS. type craftDetector struct{} func (d *craftDetector) Name() string { return "Craft CMS" } @@ -602,7 +585,6 @@ func (d *bloggerDetector) Detect(body string, headers http.Header) (float32, str return confidence, version } -// mediawikiDetector detects MediaWiki. type mediawikiDetector struct{} func (d *mediawikiDetector) Name() string { return "MediaWiki" } @@ -625,7 +607,6 @@ func (d *mediawikiDetector) Detect(body string, headers http.Header) (float32, s return confidence, version } -// discourseDetector detects the Discourse forum platform. type discourseDetector struct{} func (d *discourseDetector) Name() string { return "Discourse" } @@ -648,7 +629,6 @@ func (d *discourseDetector) Detect(body string, headers http.Header) (float32, s return confidence, version } -// xenforoDetector detects the XenForo forum platform. type xenforoDetector struct{} func (d *xenforoDetector) Name() string { return "XenForo" } @@ -673,7 +653,6 @@ func (d *xenforoDetector) Detect(body string, headers http.Header) (float32, str return confidence, version } -// moodleDetector detects the Moodle learning platform. type moodleDetector struct{} func (d *moodleDetector) Name() string { return "Moodle" } @@ -696,7 +675,6 @@ func (d *moodleDetector) Detect(body string, headers http.Header) (float32, stri return confidence, version } -// ploneDetector detects the Plone CMS. type ploneDetector struct{} func (d *ploneDetector) Name() string { return "Plone" } @@ -742,7 +720,6 @@ func (d *gravDetector) Detect(body string, headers http.Header) (float32, string return confidence, version } -// textpatternDetector detects the Textpattern CMS. type textpatternDetector struct{} func (d *textpatternDetector) Name() string { return "Textpattern" } @@ -811,7 +788,6 @@ func (d *statamicDetector) Detect(body string, headers http.Header) (float32, st return confidence, version } -// flarumDetector detects the Flarum forum platform. type flarumDetector struct{} func (d *flarumDetector) Name() string { return "Flarum" } @@ -834,7 +810,6 @@ func (d *flarumDetector) Detect(body string, headers http.Header) (float32, stri return confidence, version } -// nodebbDetector detects the NodeBB forum platform. type nodebbDetector struct{} func (d *nodebbDetector) Name() string { return "NodeBB" } @@ -857,7 +832,6 @@ func (d *nodebbDetector) Detect(body string, headers http.Header) (float32, stri return confidence, version } -// xwikiDetector detects the XWiki platform. type xwikiDetector struct{} func (d *xwikiDetector) Name() string { return "XWiki" } @@ -904,7 +878,6 @@ func (d *boltDetector) Detect(body string, headers http.Header) (float32, string return confidence, version } -// expressionengineDetector detects ExpressionEngine. type expressionengineDetector struct{} func (d *expressionengineDetector) Name() string { return "ExpressionEngine" } diff --git a/internal/scan/frameworks/detectors/frontend.go b/internal/scan/frameworks/detectors/frontend.go index 03ce1821..c91ec29c 100644 --- a/internal/scan/frameworks/detectors/frontend.go +++ b/internal/scan/frameworks/detectors/frontend.go @@ -45,7 +45,6 @@ func init() { fw.Register(&unpolyDetector{}) } -// reactDetector detects React framework. type reactDetector struct{} func (d *reactDetector) Name() string { return "React" } @@ -72,7 +71,6 @@ func (d *reactDetector) Detect(body string, headers http.Header) (float32, strin return confidence, version } -// vueDetector detects Vue.js framework. type vueDetector struct{} func (d *vueDetector) Name() string { return "Vue.js" } @@ -100,7 +98,6 @@ func (d *vueDetector) Detect(body string, headers http.Header) (float32, string) return confidence, version } -// angularDetector detects Angular framework. type angularDetector struct{} func (d *angularDetector) Name() string { return "Angular" } @@ -132,7 +129,6 @@ func (d *angularDetector) Detect(body string, headers http.Header) (float32, str return confidence, version } -// svelteDetector detects Svelte framework. type svelteDetector struct{} func (d *svelteDetector) Name() string { return "Svelte" } @@ -159,7 +155,6 @@ func (d *svelteDetector) Detect(body string, headers http.Header) (float32, stri return confidence, version } -// emberDetector detects Ember.js framework. type emberDetector struct{} func (d *emberDetector) Name() string { return "Ember.js" } @@ -187,7 +182,6 @@ func (d *emberDetector) Detect(body string, headers http.Header) (float32, strin return confidence, version } -// backboneDetector detects Backbone.js framework. type backboneDetector struct{} func (d *backboneDetector) Name() string { return "Backbone.js" } @@ -214,7 +208,6 @@ func (d *backboneDetector) Detect(body string, headers http.Header) (float32, st return confidence, version } -// htmxDetector detects the htmx library. type htmxDetector struct{} func (d *htmxDetector) Name() string { return "htmx" } @@ -242,7 +235,6 @@ func (d *htmxDetector) Detect(body string, headers http.Header) (float32, string return confidence, version } -// alpineDetector detects Alpine.js. type alpineDetector struct{} func (d *alpineDetector) Name() string { return "Alpine.js" } @@ -268,7 +260,6 @@ func (d *alpineDetector) Detect(body string, headers http.Header) (float32, stri return confidence, version } -// qwikDetector detects the Qwik framework. type qwikDetector struct{} func (d *qwikDetector) Name() string { return "Qwik" } @@ -294,7 +285,6 @@ func (d *qwikDetector) Detect(body string, headers http.Header) (float32, string return confidence, version } -// jqueryDetector detects the jQuery library. type jqueryDetector struct{} func (d *jqueryDetector) Name() string { return "jQuery" } @@ -399,7 +389,6 @@ func (d *turboDetector) Detect(body string, headers http.Header) (float32, strin return confidence, version } -// knockoutDetector detects Knockout.js. type knockoutDetector struct{} func (d *knockoutDetector) Name() string { return "Knockout.js" } @@ -424,7 +413,6 @@ func (d *knockoutDetector) Detect(body string, headers http.Header) (float32, st return confidence, version } -// unpolyDetector detects the Unpoly library. type unpolyDetector struct{} func (d *unpolyDetector) Name() string { return "Unpoly" } @@ -449,7 +437,6 @@ func (d *unpolyDetector) Detect(body string, headers http.Header) (float32, stri return confidence, version } -// meteorDetector detects Meteor framework. type meteorDetector struct{} func (d *meteorDetector) Name() string { return "Meteor" } diff --git a/internal/scan/frameworks/detectors/hosting.go b/internal/scan/frameworks/detectors/hosting.go index 33b02c19..2aae2c1a 100644 --- a/internal/scan/frameworks/detectors/hosting.go +++ b/internal/scan/frameworks/detectors/hosting.go @@ -39,7 +39,6 @@ func init() { fw.Register(&amazonS3Detector{}) } -// vercelDetector detects the Vercel hosting platform. type vercelDetector struct{} func (d *vercelDetector) Name() string { return "Vercel" } @@ -56,7 +55,6 @@ func (d *vercelDetector) Detect(body string, headers http.Header) (float32, stri return sigmoidConfidence(score), "" } -// netlifyDetector detects the Netlify hosting platform. type netlifyDetector struct{} func (d *netlifyDetector) Name() string { return "Netlify" } @@ -90,7 +88,6 @@ func (d *githubPagesDetector) Detect(body string, headers http.Header) (float32, return sigmoidConfidence(score), "" } -// cloudflareDetector detects the Cloudflare edge. type cloudflareDetector struct{} func (d *cloudflareDetector) Name() string { return "Cloudflare" } @@ -124,7 +121,6 @@ func (d *cloudfrontDetector) Detect(body string, headers http.Header) (float32, return sigmoidConfidence(score), "" } -// akamaiDetector detects the Akamai edge. type akamaiDetector struct{} func (d *akamaiDetector) Name() string { return "Akamai" } @@ -143,7 +139,6 @@ func (d *akamaiDetector) Detect(body string, headers http.Header) (float32, stri return sigmoidConfidence(score), "" } -// flyDetector detects the Fly.io platform. type flyDetector struct{} func (d *flyDetector) Name() string { return "Fly.io" } diff --git a/internal/scan/frameworks/detectors/meta.go b/internal/scan/frameworks/detectors/meta.go index fa4bde45..fbcb92e9 100644 --- a/internal/scan/frameworks/detectors/meta.go +++ b/internal/scan/frameworks/detectors/meta.go @@ -45,7 +45,6 @@ func init() { fw.Register(&publiiDetector{}) } -// nextjsDetector detects Next.js framework. type nextjsDetector struct{} func (d *nextjsDetector) Name() string { return "Next.js" } @@ -72,7 +71,6 @@ func (d *nextjsDetector) Detect(body string, headers http.Header) (float32, stri return confidence, version } -// nuxtDetector detects Nuxt.js framework. type nuxtDetector struct{} func (d *nuxtDetector) Name() string { return "Nuxt.js" } @@ -97,7 +95,6 @@ func (d *nuxtDetector) Detect(body string, headers http.Header) (float32, string return confidence, version } -// sveltekitDetector detects SvelteKit framework. type sveltekitDetector struct{} func (d *sveltekitDetector) Name() string { return "SvelteKit" } @@ -122,7 +119,6 @@ func (d *sveltekitDetector) Detect(body string, headers http.Header) (float32, s return confidence, version } -// gatsbyDetector detects Gatsby framework. type gatsbyDetector struct{} func (d *gatsbyDetector) Name() string { return "Gatsby" } @@ -149,7 +145,6 @@ func (d *gatsbyDetector) Detect(body string, headers http.Header) (float32, stri return confidence, version } -// remixDetector detects Remix framework. type remixDetector struct{} func (d *remixDetector) Name() string { return "Remix" } @@ -172,7 +167,6 @@ func (d *remixDetector) Detect(body string, headers http.Header) (float32, strin return confidence, version } -// astroDetector detects Astro framework. type astroDetector struct{} func (d *astroDetector) Name() string { return "Astro" } @@ -205,7 +199,6 @@ func (d *astroDetector) Detect(body string, headers http.Header) (float32, strin // The generator detectors below anchor on the content=" value: real // sites minify and reorder the meta, dropping the name="generator" prefix. -// hugoDetector detects the Hugo static site generator. type hugoDetector struct{} func (d *hugoDetector) Name() string { return "Hugo" } @@ -228,7 +221,6 @@ func (d *hugoDetector) Detect(body string, headers http.Header) (float32, string return confidence, version } -// jekyllDetector detects the Jekyll static site generator. type jekyllDetector struct{} func (d *jekyllDetector) Name() string { return "Jekyll" } @@ -251,7 +243,6 @@ func (d *jekyllDetector) Detect(body string, headers http.Header) (float32, stri return confidence, version } -// docusaurusDetector detects the Docusaurus documentation site generator. type docusaurusDetector struct{} func (d *docusaurusDetector) Name() string { return "Docusaurus" } @@ -323,7 +314,6 @@ func (d *eleventyDetector) Detect(body string, headers http.Header) (float32, st return confidence, version } -// hexoDetector detects the Hexo static site generator. type hexoDetector struct{} func (d *hexoDetector) Name() string { return "Hexo" } @@ -346,7 +336,6 @@ func (d *hexoDetector) Detect(body string, headers http.Header) (float32, string return confidence, version } -// vuepressDetector detects VuePress. type vuepressDetector struct{} func (d *vuepressDetector) Name() string { return "VuePress" } @@ -369,7 +358,6 @@ func (d *vuepressDetector) Detect(body string, headers http.Header) (float32, st return confidence, version } -// sphinxDetector detects the Sphinx documentation generator. type sphinxDetector struct{} func (d *sphinxDetector) Name() string { return "Sphinx" } @@ -394,7 +382,6 @@ func (d *sphinxDetector) Detect(body string, headers http.Header) (float32, stri return confidence, version } -// nikolaDetector detects the Nikola static site generator. type nikolaDetector struct{} func (d *nikolaDetector) Name() string { return "Nikola" } @@ -417,7 +404,6 @@ func (d *nikolaDetector) Detect(body string, headers http.Header) (float32, stri return confidence, version } -// publiiDetector detects the Publii static-site CMS. type publiiDetector struct{} func (d *publiiDetector) Name() string { return "Publii" } From f23fd00ddb8adc47c20d7b5b7e2994c60140fcd1 Mon Sep 17 00:00:00 2001 From: TBX3D <88289044+TBX3D@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:33:01 -0700 Subject: [PATCH 11/11] fix(frameworks): drop the hosting and jquery detectors from this batch every detector registers into one pool and DetectFramework reports a single argmax across it, so a single-signature edge marker scores weightedScore/totalWeight = 1.0 and sigmoids to ~0.999, the ceiling. a multi-signature framework that lands only its primary signal scores lower. reproduced: a wordpress page (wp-content, wp-includes, wp-json) served behind cloudflare resolved to "Cloudflare" at 0.9991 instead of WordPress. that is the common case for any real target behind an edge, not an edge case. the cdn/host detectors belong in the separate DetectCDN pool. jquery is the same category error on a wider surface: it is a library on most of the web, and one "jquery.min.js" reference alone scores 0.5/1.4 -> 0.639, over the 0.5 floor. a django app that ships jquery resolved to "jQuery" at 0.9844. weight tuning cannot fix it, MatchSignatures normalizes by total weight so only the ratios matter. both are held for the surface that can report a technology without it competing for the single framework slot. the cms, frontend, backend and meta detectors are untouched. also drop the duplicate hdr test helper: main grew an identical one in hardening_test.go, so the two collided after the rebase. --- .../scan/frameworks/argmax_shadowing_test.go | 79 ++++++++ internal/scan/frameworks/detect_test.go | 4 +- .../scan/frameworks/detectors/backend_test.go | 8 - .../scan/frameworks/detectors/frontend.go | 26 --- .../frameworks/detectors/frontend_test.go | 6 - internal/scan/frameworks/detectors/hosting.go | 174 ------------------ .../scan/frameworks/detectors/hosting_test.go | 73 -------- 7 files changed, 80 insertions(+), 290 deletions(-) create mode 100644 internal/scan/frameworks/argmax_shadowing_test.go delete mode 100644 internal/scan/frameworks/detectors/hosting.go delete mode 100644 internal/scan/frameworks/detectors/hosting_test.go diff --git a/internal/scan/frameworks/argmax_shadowing_test.go b/internal/scan/frameworks/argmax_shadowing_test.go new file mode 100644 index 00000000..121a7f9e --- /dev/null +++ b/internal/scan/frameworks/argmax_shadowing_test.go @@ -0,0 +1,79 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +package frameworks_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/vmfunc/sif/internal/scan/frameworks" + _ "github.com/vmfunc/sif/internal/scan/frameworks/detectors" +) + +// DetectFramework reports a single argmax across one registry, so a detector +// that clears its own bar on one ubiquitous marker outranks a real framework +// that only landed its primary signal. these pin the two cases that bit. + +// a real wordpress site behind cloudflare: the app framework must win, not the edge. +func TestHostingDoesNotShadowRealFramework(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("CF-RAY", "8a1b2c3d4e5f6789-LAX") + w.Header().Set("Server", "cloudflare") + w.WriteHeader(200) + _, _ = w.Write([]byte(` + + + +hello`)) + })) + defer srv.Close() + + res, err := frameworks.DetectFramework(srv.URL, 5e9, "") + if err != nil { + t.Fatalf("detect: %v", err) + } + if res == nil { + t.Fatal("no framework detected") + } + t.Logf("winner=%q confidence=%.4f", res.Name, res.Confidence) + if res.Name != "WordPress" { + t.Errorf("edge/cdn shadowed the real framework: got %q (%.4f), want WordPress", res.Name, res.Confidence) + } +} + +// a django app that ships jquery, as a huge share of them do. jquery is a +// library on most of the web, so it must not outrank the app framework. +func TestJQueryDoesNotShadowRealFramework(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Set-Cookie", "csrftoken=abc123; Path=/") + w.WriteHeader(200) + _, _ = w.Write([]byte(` + + +
`)) + })) + defer srv.Close() + + res, err := frameworks.DetectFramework(srv.URL, 5e9, "") + if err != nil { + t.Fatalf("detect: %v", err) + } + if res == nil { + t.Fatal("no framework detected") + } + t.Logf("winner=%q confidence=%.4f", res.Name, res.Confidence) + if res.Name == "jQuery" { + t.Errorf("jquery shadowed the real framework (%.4f)", res.Confidence) + } +} diff --git a/internal/scan/frameworks/detect_test.go b/internal/scan/frameworks/detect_test.go index 70036e8f..11833ca0 100644 --- a/internal/scan/frameworks/detect_test.go +++ b/internal/scan/frameworks/detect_test.go @@ -718,15 +718,13 @@ func TestDetectorRegistry(t *testing.T) { "TYPO3", "Contao", "Wix", "Webflow", "HubSpot", "PrestaShop", "Sitecore", "OpenCart", "DotNetNuke", "Liferay", "Hugo", "Jekyll", "Docusaurus", "MkDocs", - "Alpine.js", "Qwik", "jQuery", + "Alpine.js", "Qwik", "Squarespace", "WooCommerce", "Craft CMS", "Concrete CMS", "Bitrix", "Blogger", "Eleventy", "Hexo", "VuePress", "Sphinx", "MediaWiki", "Discourse", "XenForo", "Moodle", "Plone", "Grav", "Textpattern", "October CMS", "Statamic", "Livewire", "Stimulus", "Turbo", "Knockout.js", "Unpoly", "Flarum", "NodeBB", "XWiki", "Bolt CMS", "Nikola", "Publii", "ExpressionEngine", - "Vercel", "Netlify", "GitHub Pages", "Cloudflare", - "Amazon CloudFront", "Akamai", "Fly.io", "Amazon S3", } for _, name := range expectedDetectors { if _, ok := frameworks.GetDetector(name); !ok { diff --git a/internal/scan/frameworks/detectors/backend_test.go b/internal/scan/frameworks/detectors/backend_test.go index e507bd31..053f35ce 100644 --- a/internal/scan/frameworks/detectors/backend_test.go +++ b/internal/scan/frameworks/detectors/backend_test.go @@ -19,14 +19,6 @@ import ( fw "github.com/vmfunc/sif/internal/scan/frameworks" ) -func hdr(pairs ...string) http.Header { - h := http.Header{} - for i := 0; i+1 < len(pairs); i += 2 { - h.Add(pairs[i], pairs[i+1]) - } - return h -} - func TestWebFrameworkDetectors_Positive(t *testing.T) { tests := []struct { name string diff --git a/internal/scan/frameworks/detectors/frontend.go b/internal/scan/frameworks/detectors/frontend.go index c91ec29c..7c34c3db 100644 --- a/internal/scan/frameworks/detectors/frontend.go +++ b/internal/scan/frameworks/detectors/frontend.go @@ -36,7 +36,6 @@ func init() { fw.Register(&meteorDetector{}) fw.Register(&htmxDetector{}) fw.Register(&alpineDetector{}) - fw.Register(&jqueryDetector{}) fw.Register(&knockoutDetector{}) fw.Register(&livewireDetector{}) fw.Register(&qwikDetector{}) @@ -285,31 +284,6 @@ func (d *qwikDetector) Detect(body string, headers http.Header) (float32, string return confidence, version } -type jqueryDetector struct{} - -func (d *jqueryDetector) Name() string { return "jQuery" } - -func (d *jqueryDetector) Signatures() []fw.Signature { - return []fw.Signature{ - {Pattern: "jquery.min.js", Weight: 0.5}, - {Pattern: "jquery-", Weight: 0.5}, - {Pattern: "jQuery.fn.jquery", Weight: 0.4}, - } -} - -func (d *jqueryDetector) 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 -} - -// livewireDetector detects Laravel Livewire. type livewireDetector struct{} func (d *livewireDetector) Name() string { return "Livewire" } diff --git a/internal/scan/frameworks/detectors/frontend_test.go b/internal/scan/frameworks/detectors/frontend_test.go index f1ed86b9..482ef6cc 100644 --- a/internal/scan/frameworks/detectors/frontend_test.go +++ b/internal/scan/frameworks/detectors/frontend_test.go @@ -29,9 +29,6 @@ func TestFrontendLibDetectors_Positive(t *testing.T) { {"Alpine cdn plus cloak", &alpineDetector{}, `
`}, {"Qwik container only", &qwikDetector{}, ``}, {"Qwik bootstrap", &qwikDetector{}, ``}, - {"jQuery cdn", &jqueryDetector{}, ``}, - {"jQuery googleapis", &jqueryDetector{}, ``}, - {"jQuery wp bundled", &jqueryDetector{}, ``}, {"Livewire component", &livewireDetector{}, `
`}, {"Stimulus controller", &stimulusDetector{}, `
`}, {"Stimulus script reference", &stimulusDetector{}, ``}, @@ -61,7 +58,6 @@ func TestFrontendLibDetectors_Negative(t *testing.T) { {"Vue at-click not Alpine", &alpineDetector{}, `
`}, {"Alpine max-data substring", &alpineDetector{}, `x
`}, {"Qwik prose", &qwikDetector{}, `

Qwik is a resumable framework, see qwik.dev for details.

`}, - {"jQuery prose", &jqueryDetector{}, `

migrating off jquery this year.

`}, {"Livewire single directive", &livewireDetector{}, ``}, {"Livewire prose", &livewireDetector{}, `

Livewire is a full-stack framework for Laravel.

`}, {"Stimulus prose", &stimulusDetector{}, `

Stimulus is a modest JavaScript framework for the HTML you already have.

`}, @@ -90,8 +86,6 @@ func TestFrontendLibDetectors_Version(t *testing.T) { body string want string }{ - {"jQuery filename", &jqueryDetector{}, ``, "3.6.0"}, - {"jQuery googleapis path", &jqueryDetector{}, ``, "3.7.1"}, {"Alpine cdn", &alpineDetector{}, `
`, "3.13.0"}, {"Qwik attr", &qwikDetector{}, ``, "1.5.0"}, {"Knockout filename", &knockoutDetector{}, ``, "3.5.1"}, diff --git a/internal/scan/frameworks/detectors/hosting.go b/internal/scan/frameworks/detectors/hosting.go deleted file mode 100644 index 2aae2c1a..00000000 --- a/internal/scan/frameworks/detectors/hosting.go +++ /dev/null @@ -1,174 +0,0 @@ -/* -·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· -: : -: █▀ █ █▀▀ · 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" -) - -// Detectors in this file identify the hosting platform or edge/CDN provider in -// front of a target rather than its application framework. - -func init() { - fw.Register(&vercelDetector{}) - fw.Register(&netlifyDetector{}) - fw.Register(&githubPagesDetector{}) - fw.Register(&cloudflareDetector{}) - fw.Register(&cloudfrontDetector{}) - fw.Register(&akamaiDetector{}) - fw.Register(&flyDetector{}) - fw.Register(&amazonS3Detector{}) -} - -type vercelDetector struct{} - -func (d *vercelDetector) Name() string { return "Vercel" } - -func (d *vercelDetector) Signatures() []fw.Signature { - return []fw.Signature{ - {Pattern: "x-vercel", Weight: 0.6, HeaderOnly: true}, - } -} - -func (d *vercelDetector) Detect(body string, headers http.Header) (float32, string) { - base := fw.NewBaseDetector(d.Name(), d.Signatures()) - score := base.MatchSignatures(body, headers) - return sigmoidConfidence(score), "" -} - -type netlifyDetector struct{} - -func (d *netlifyDetector) Name() string { return "Netlify" } - -func (d *netlifyDetector) Signatures() []fw.Signature { - return []fw.Signature{ - {Pattern: "x-nf-request-id", Weight: 0.6, HeaderOnly: true}, - } -} - -func (d *netlifyDetector) Detect(body string, headers http.Header) (float32, string) { - base := fw.NewBaseDetector(d.Name(), d.Signatures()) - score := base.MatchSignatures(body, headers) - return sigmoidConfidence(score), "" -} - -// githubPagesDetector detects GitHub Pages (and GitHub-hosted infrastructure). -type githubPagesDetector struct{} - -func (d *githubPagesDetector) Name() string { return "GitHub Pages" } - -func (d *githubPagesDetector) Signatures() []fw.Signature { - return []fw.Signature{ - {Pattern: "x-github-request-id", Weight: 0.6, HeaderOnly: true}, - } -} - -func (d *githubPagesDetector) Detect(body string, headers http.Header) (float32, string) { - base := fw.NewBaseDetector(d.Name(), d.Signatures()) - score := base.MatchSignatures(body, headers) - return sigmoidConfidence(score), "" -} - -type cloudflareDetector struct{} - -func (d *cloudflareDetector) Name() string { return "Cloudflare" } - -func (d *cloudflareDetector) Signatures() []fw.Signature { - return []fw.Signature{ - {Pattern: "cf-ray", Weight: 0.6, HeaderOnly: true}, - } -} - -func (d *cloudflareDetector) Detect(body string, headers http.Header) (float32, string) { - base := fw.NewBaseDetector(d.Name(), d.Signatures()) - score := base.MatchSignatures(body, headers) - return sigmoidConfidence(score), "" -} - -// cloudfrontDetector detects the Amazon CloudFront CDN. -type cloudfrontDetector struct{} - -func (d *cloudfrontDetector) Name() string { return "Amazon CloudFront" } - -func (d *cloudfrontDetector) Signatures() []fw.Signature { - return []fw.Signature{ - {Pattern: "x-amz-cf-id", Weight: 0.6, HeaderOnly: true}, - } -} - -func (d *cloudfrontDetector) Detect(body string, headers http.Header) (float32, string) { - base := fw.NewBaseDetector(d.Name(), d.Signatures()) - score := base.MatchSignatures(body, headers) - return sigmoidConfidence(score), "" -} - -type akamaiDetector struct{} - -func (d *akamaiDetector) Name() string { return "Akamai" } - -func (d *akamaiDetector) Signatures() []fw.Signature { - return []fw.Signature{ - {Pattern: "Akamai-GRN", Weight: 0.5, HeaderOnly: true}, - {Pattern: "x-akamai", Weight: 0.4, HeaderOnly: true}, - {Pattern: "AkamaiGHost", Weight: 0.4, HeaderOnly: true}, - } -} - -func (d *akamaiDetector) Detect(body string, headers http.Header) (float32, string) { - base := fw.NewBaseDetector(d.Name(), d.Signatures()) - score := base.MatchSignatures(body, headers) - return sigmoidConfidence(score), "" -} - -type flyDetector struct{} - -func (d *flyDetector) Name() string { return "Fly.io" } - -func (d *flyDetector) Signatures() []fw.Signature { - return []fw.Signature{ - {Pattern: "fly-request-id", Weight: 0.6, HeaderOnly: true}, - } -} - -func (d *flyDetector) Detect(body string, headers http.Header) (float32, string) { - base := fw.NewBaseDetector(d.Name(), d.Signatures()) - score := base.MatchSignatures(body, headers) - return sigmoidConfidence(score), "" -} - -// amazonS3Detector detects content served from an Amazon S3 bucket. -type amazonS3Detector struct{} - -func (d *amazonS3Detector) Name() string { return "Amazon S3" } - -func (d *amazonS3Detector) Signatures() []fw.Signature { - return []fw.Signature{ - {Pattern: "AmazonS3", Weight: 0.6, HeaderOnly: true}, - {Pattern: "x-amz-bucket-region", Weight: 0.4, HeaderOnly: true}, - } -} - -func (d *amazonS3Detector) Detect(body string, headers http.Header) (float32, string) { - base := fw.NewBaseDetector(d.Name(), d.Signatures()) - score := base.MatchSignatures(body, headers) - return sigmoidConfidence(score), "" -} diff --git a/internal/scan/frameworks/detectors/hosting_test.go b/internal/scan/frameworks/detectors/hosting_test.go deleted file mode 100644 index 7221b475..00000000 --- a/internal/scan/frameworks/detectors/hosting_test.go +++ /dev/null @@ -1,73 +0,0 @@ -/* -·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· -: : -: █▀ █ █▀▀ · Blazing-fast pentesting suite : -: ▄█ █ █▀ · BSD 3-Clause License : -: : -: (c) 2022-2026 vmfunc, xyzeva, : -: lunchcat alumni & contributors : -: : -·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· -*/ - -package detectors - -import ( - "net/http" - "testing" - - fw "github.com/vmfunc/sif/internal/scan/frameworks" -) - -func TestHostingDetectors_Positive(t *testing.T) { - tests := []struct { - name string - detector fw.Detector - headers http.Header - }{ - {"Vercel", &vercelDetector{}, hdr("x-vercel-id", "sfo1::pdx1::dmmpr-1782439760448-608ebdfc")}, - {"Netlify", &netlifyDetector{}, hdr("x-nf-request-id", "01KW0V0MYRFJKYYNDRP7KC5DAJ")}, - {"GitHub Pages", &githubPagesDetector{}, hdr("x-github-request-id", "8714:20C798:191901:19DD8B:6A3DDD67")}, - {"Cloudflare", &cloudflareDetector{}, hdr("cf-ray", "a118ab5d9e7867e8-SJC")}, - {"CloudFront", &cloudfrontDetector{}, hdr("x-amz-cf-id", "0MsbJpMBovZpIG2KNmafF4RVM4GXD_iKAnm9friazwXUpC")}, - {"Akamai GRN", &akamaiDetector{}, hdr("Akamai-GRN", "0.1ea7cb17.1782439763.4a41c389")}, - {"Akamai server", &akamaiDetector{}, hdr("Server", "AkamaiGHost")}, - {"Fly.io", &flyDetector{}, hdr("fly-request-id", "01KW0V0QBEWMQ51YPTNZKE3EYJ-sjc")}, - {"Amazon S3 server", &amazonS3Detector{}, hdr("Server", "AmazonS3")}, - {"Amazon S3 region", &amazonS3Detector{}, hdr("x-amz-bucket-region", "us-east-2")}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - conf, _ := tt.detector.Detect("", tt.headers) - if conf <= 0.5 { - t.Errorf("%s: confidence = %.3f, want > 0.5", tt.name, conf) - } - }) - } -} - -func TestHostingDetectors_Negative(t *testing.T) { - tests := []struct { - name string - detector fw.Detector - headers http.Header - }{ - {"Vercel plain", &vercelDetector{}, hdr("Server", "nginx/1.25.3")}, - {"Netlify plain", &netlifyDetector{}, hdr("Server", "nginx")}, - {"Cloudflare plain", &cloudflareDetector{}, hdr("Server", "nginx")}, - {"GitHub link header", &githubPagesDetector{}, hdr("Link", "; rel=canonical")}, - {"Akamai csp asset", &akamaiDetector{}, hdr("Content-Security-Policy", "img-src https://example.akamaihd.net")}, - {"S3 generic amz id", &amazonS3Detector{}, hdr("x-amz-request-id", "4Y0WES8AVK3ZQ98N")}, - {"Fly plain", &flyDetector{}, hdr("Server", "Cowboy")}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - conf, _ := tt.detector.Detect("", tt.headers) - if conf > 0.5 { - t.Errorf("%s: confidence = %.3f, want <= 0.5", tt.name, conf) - } - }) - } -}