Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions internal/scan/frameworks/argmax_shadowing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/

package frameworks_test

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/vmfunc/sif/internal/scan/frameworks"
_ "github.com/vmfunc/sif/internal/scan/frameworks/detectors"
)

// DetectFramework reports a single argmax across one registry, so a detector
// that clears its own bar on one ubiquitous marker outranks a real framework
// that only landed its primary signal. these pin the two cases that bit.

// a real wordpress site behind cloudflare: the app framework must win, not the edge.
func TestHostingDoesNotShadowRealFramework(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("CF-RAY", "8a1b2c3d4e5f6789-LAX")
w.Header().Set("Server", "cloudflare")
w.WriteHeader(200)
_, _ = w.Write([]byte(`<!DOCTYPE html><html><head>
<link rel="stylesheet" href="/wp-content/themes/twentytwentyfour/style.css">
<script src="/wp-includes/js/jquery/jquery.min.js"></script>
<link rel="https://api.w.org/" href="/wp-json/">
</head><body>hello</body></html>`))
}))
defer srv.Close()

res, err := frameworks.DetectFramework(srv.URL, 5e9, "")
if err != nil {
t.Fatalf("detect: %v", err)
}
if res == nil {
t.Fatal("no framework detected")
}
t.Logf("winner=%q confidence=%.4f", res.Name, res.Confidence)
if res.Name != "WordPress" {
t.Errorf("edge/cdn shadowed the real framework: got %q (%.4f), want WordPress", res.Name, res.Confidence)
}
}

// a django app that ships jquery, as a huge share of them do. jquery is a
// library on most of the web, so it must not outrank the app framework.
func TestJQueryDoesNotShadowRealFramework(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Set-Cookie", "csrftoken=abc123; Path=/")
w.WriteHeader(200)
_, _ = w.Write([]byte(`<!DOCTYPE html><html><head>
<script src="/static/js/jquery.min.js"></script>
<script src="/static/js/jquery-3.6.0.js"></script>
</head><body><form><input name="csrfmiddlewaretoken" value="x"></form></body></html>`))
}))
defer srv.Close()

res, err := frameworks.DetectFramework(srv.URL, 5e9, "")
if err != nil {
t.Fatalf("detect: %v", err)
}
if res == nil {
t.Fatal("no framework detected")
}
t.Logf("winner=%q confidence=%.4f", res.Name, res.Confidence)
if res.Name == "jQuery" {
t.Errorf("jquery shadowed the real framework (%.4f)", res.Confidence)
}
}
35 changes: 27 additions & 8 deletions internal/scan/frameworks/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -709,8 +709,23 @@ 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",
"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",
}
for _, name := range expectedDetectors {
if _, ok := frameworks.GetDetector(name); !ok {
t.Errorf("expected detector %q to be registered", name)
Expand Down Expand Up @@ -842,8 +857,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(`<!DOCTYPE html><html><body><p>our cupcake and cheesecake recipes,
plus the best pancake stack in town.</p></body></html>`))
// 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(`<!DOCTYPE html><html><body><a href="/questions/tagged/cakephp">cakephp</a></body></html>`))
}))
defer server.Close()

Expand All @@ -852,7 +868,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)
}
}

Expand Down Expand Up @@ -893,7 +909,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(`<!DOCTYPE html><html><body><script>fetch("/api/v1/users")</script></body></html>`))
// prose naming the CMS plus a plain /api/ path: neither is the powered-by header
w.Write([]byte(`<!DOCTYPE html><html><body><p>built with Strapi</p><script>fetch("/api/v1/users")</script></body></html>`))
}))
defer server.Close()

Expand All @@ -902,14 +919,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 <strapi.io>")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`<!DOCTYPE html><html><body><div>powered by strapi</div></body></html>`))
w.Write([]byte(`<!DOCTYPE html><html><body><div>welcome</div></body></html>`))
}))
defer server.Close()

Expand Down
Loading
Loading