From c1eca479049ca47ca317b9af8deafcc2581d12f5 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/10] 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 f9616041..c58cdf53 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 @@ -349,7 +375,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"}, } } @@ -434,3 +460,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 a3be050007d3a69c8512299a1685b5370c4579e0 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/10] 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 488cfa46..5ee0010b 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. @@ -230,6 +238,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 9091ceee6ff89d9869dddec624b555348ab0b7e1 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/10] 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 217de13b2d23ad492abeccb9592510f06690c0e0 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/10] 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 ad30466d..8315f5b3 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. @@ -187,3 +197,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 6b578274477728070e7a4d022c7937be400bcee1 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/10] 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 5ee0010b..4130d0d8 100644 --- a/internal/scan/frameworks/detectors/frontend.go +++ b/internal/scan/frameworks/detectors/frontend.go @@ -349,7 +349,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}, } } @@ -374,8 +378,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 ee7dc9ec..469c8f83 100644 --- a/internal/scan/frameworks/version.go +++ b/internal/scan/frameworks/version.go @@ -142,6 +142,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 @@ -188,6 +233,8 @@ func ExtractVersionOptimized(body 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 1a0ecd57cb7e9f1109b24d949596e8ca18cb1cc1 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/10] 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 4130d0d8..5ee0010b 100644 --- a/internal/scan/frameworks/detectors/frontend.go +++ b/internal/scan/frameworks/detectors/frontend.go @@ -349,11 +349,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}, } } @@ -378,13 +374,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 e9a6bed61de0e87889bf69b6438ba465bdb087f6 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/10] 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 c58cdf53..f484bd3b 100644 --- a/internal/scan/frameworks/detectors/backend.go +++ b/internal/scan/frameworks/detectors/backend.go @@ -460,6 +460,7 @@ func (d *codeigniterDetector) Detect(body string, headers http.Header) (float32, } return confidence, version } + // tornadoDetector detects the Tornado Python web framework. type tornadoDetector struct{} @@ -467,7 +468,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 6c0bc33817dda080d65c87e6428409c2439b2f48 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/10] 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 | 2 -- 3 files changed, 10 insertions(+), 9 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 f484bd3b..7a34fc87 100644 --- a/internal/scan/frameworks/detectors/backend.go +++ b/internal/scan/frameworks/detectors/backend.go @@ -421,7 +421,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 469c8f83..f28062dc 100644 --- a/internal/scan/frameworks/version.go +++ b/internal/scan/frameworks/version.go @@ -32,7 +32,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() { @@ -205,7 +204,6 @@ func init() { } // ExtractVersionOptimized extracts version using pre-compiled patterns. -// This is exported for use by individual detector implementations. func ExtractVersionOptimized(body string, framework string) VersionMatch { patterns, exists := frameworkVersionPatterns[framework] if !exists { From 82cc60b0ece6d1d751dbd4c247265ebc0a0dab2c 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/10] 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 7a34fc87..77f6d68b 100644 --- a/internal/scan/frameworks/detectors/backend.go +++ b/internal/scan/frameworks/detectors/backend.go @@ -326,7 +326,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 5ee0010b..2071f899 100644 --- a/internal/scan/frameworks/detectors/frontend.go +++ b/internal/scan/frameworks/detectors/frontend.go @@ -348,9 +348,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 2c432aba020c2f944157c5985c39352e54f8f346 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/10] 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 77f6d68b..80bda9d7 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" } @@ -132,7 +130,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" } @@ -160,7 +157,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" } @@ -184,7 +180,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" } @@ -216,7 +211,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" } @@ -242,7 +236,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" } @@ -268,7 +261,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" } @@ -293,7 +285,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" } @@ -318,7 +309,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" } @@ -345,7 +335,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" } @@ -370,7 +359,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" } @@ -393,7 +381,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" } @@ -416,7 +403,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" } @@ -439,7 +425,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" } @@ -462,7 +447,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" } @@ -485,7 +469,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" } @@ -533,7 +516,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" } @@ -556,7 +538,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" } @@ -628,7 +609,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 2071f899..46a9596c 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" } @@ -130,7 +127,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" } @@ -155,7 +151,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" } @@ -183,7 +178,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" } @@ -210,7 +204,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" } @@ -238,7 +231,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" } @@ -264,7 +256,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" } @@ -290,7 +281,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" } @@ -395,7 +385,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" } @@ -420,7 +409,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" } @@ -445,7 +433,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 8315f5b3..b4e0f5ae 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" } @@ -71,7 +70,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" } @@ -96,7 +94,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" } @@ -121,7 +118,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" } @@ -146,7 +142,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" } @@ -169,7 +164,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" } @@ -201,7 +195,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" } @@ -224,7 +217,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" } @@ -247,7 +239,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" } @@ -319,7 +310,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" } @@ -342,7 +332,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" } @@ -365,7 +354,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" } @@ -390,7 +378,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" } @@ -413,7 +400,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" }