Skip to content

Commit 7b4c32f

Browse files
authored
Merge pull request #363 from TBX3D/feat/fw-detectors-batch2
feat(frameworks): add detectors for 56 additional cms, frontend and backend platforms
2 parents 6187d11 + f23fd00 commit 7b4c32f

11 files changed

Lines changed: 2025 additions & 47 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
3+
: :
4+
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
5+
: ▄█ █ █▀ · BSD 3-Clause License :
6+
: :
7+
: (c) 2022-2026 vmfunc, xyzeva, :
8+
: lunchcat alumni & contributors :
9+
: :
10+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
11+
*/
12+
13+
package frameworks_test
14+
15+
import (
16+
"net/http"
17+
"net/http/httptest"
18+
"testing"
19+
20+
"github.com/vmfunc/sif/internal/scan/frameworks"
21+
_ "github.com/vmfunc/sif/internal/scan/frameworks/detectors"
22+
)
23+
24+
// DetectFramework reports a single argmax across one registry, so a detector
25+
// that clears its own bar on one ubiquitous marker outranks a real framework
26+
// that only landed its primary signal. these pin the two cases that bit.
27+
28+
// a real wordpress site behind cloudflare: the app framework must win, not the edge.
29+
func TestHostingDoesNotShadowRealFramework(t *testing.T) {
30+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
31+
w.Header().Set("CF-RAY", "8a1b2c3d4e5f6789-LAX")
32+
w.Header().Set("Server", "cloudflare")
33+
w.WriteHeader(200)
34+
_, _ = w.Write([]byte(`<!DOCTYPE html><html><head>
35+
<link rel="stylesheet" href="/wp-content/themes/twentytwentyfour/style.css">
36+
<script src="/wp-includes/js/jquery/jquery.min.js"></script>
37+
<link rel="https://api.w.org/" href="/wp-json/">
38+
</head><body>hello</body></html>`))
39+
}))
40+
defer srv.Close()
41+
42+
res, err := frameworks.DetectFramework(srv.URL, 5e9, "")
43+
if err != nil {
44+
t.Fatalf("detect: %v", err)
45+
}
46+
if res == nil {
47+
t.Fatal("no framework detected")
48+
}
49+
t.Logf("winner=%q confidence=%.4f", res.Name, res.Confidence)
50+
if res.Name != "WordPress" {
51+
t.Errorf("edge/cdn shadowed the real framework: got %q (%.4f), want WordPress", res.Name, res.Confidence)
52+
}
53+
}
54+
55+
// a django app that ships jquery, as a huge share of them do. jquery is a
56+
// library on most of the web, so it must not outrank the app framework.
57+
func TestJQueryDoesNotShadowRealFramework(t *testing.T) {
58+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
59+
w.Header().Set("Set-Cookie", "csrftoken=abc123; Path=/")
60+
w.WriteHeader(200)
61+
_, _ = w.Write([]byte(`<!DOCTYPE html><html><head>
62+
<script src="/static/js/jquery.min.js"></script>
63+
<script src="/static/js/jquery-3.6.0.js"></script>
64+
</head><body><form><input name="csrfmiddlewaretoken" value="x"></form></body></html>`))
65+
}))
66+
defer srv.Close()
67+
68+
res, err := frameworks.DetectFramework(srv.URL, 5e9, "")
69+
if err != nil {
70+
t.Fatalf("detect: %v", err)
71+
}
72+
if res == nil {
73+
t.Fatal("no framework detected")
74+
}
75+
t.Logf("winner=%q confidence=%.4f", res.Name, res.Confidence)
76+
if res.Name == "jQuery" {
77+
t.Errorf("jquery shadowed the real framework (%.4f)", res.Confidence)
78+
}
79+
}

internal/scan/frameworks/detect_test.go

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -772,8 +772,23 @@ func TestDetectorRegistry(t *testing.T) {
772772
t.Fatal("expected registered detectors, got none")
773773
}
774774

775-
// Check that some expected detectors are registered
776-
expectedDetectors := []string{"Laravel", "Django", "React", "Vue.js", "Angular", "Next.js", "WordPress", "Astro"}
775+
// Check that expected detectors are registered: a spot-check of the
776+
// originals plus every detector added to the backend, cms, and meta sets.
777+
expectedDetectors := []string{
778+
"Laravel", "Django", "React", "Vue.js", "Angular", "Next.js", "WordPress", "Astro",
779+
"Tornado", "CherryPy", "Play Framework", "Sails.js", "Beego",
780+
"JavaServer Faces", "Google Web Toolkit", "Vaadin", "ColdFusion",
781+
"TYPO3", "Contao", "Wix", "Webflow", "HubSpot", "PrestaShop",
782+
"Sitecore", "OpenCart", "DotNetNuke", "Liferay",
783+
"Hugo", "Jekyll", "Docusaurus", "MkDocs",
784+
"Alpine.js", "Qwik",
785+
"Squarespace", "WooCommerce", "Craft CMS", "Concrete CMS", "Bitrix", "Blogger",
786+
"Eleventy", "Hexo", "VuePress", "Sphinx",
787+
"MediaWiki", "Discourse", "XenForo", "Moodle", "Plone", "Grav",
788+
"Textpattern", "October CMS", "Statamic", "Livewire",
789+
"Stimulus", "Turbo", "Knockout.js", "Unpoly", "Flarum", "NodeBB",
790+
"XWiki", "Bolt CMS", "Nikola", "Publii", "ExpressionEngine",
791+
}
777792
for _, name := range expectedDetectors {
778793
if _, ok := frameworks.GetDetector(name); !ok {
779794
t.Errorf("expected detector %q to be registered", name)
@@ -905,8 +920,9 @@ func TestDetectFramework_Backbone(t *testing.T) {
905920
func TestDetectFramework_CakePHPFalsePositive(t *testing.T) {
906921
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
907922
w.WriteHeader(http.StatusOK)
908-
w.Write([]byte(`<!DOCTYPE html><html><body><p>our cupcake and cheesecake recipes,
909-
plus the best pancake stack in town.</p></body></html>`))
923+
// a Q&A/listicle page that merely names the framework, as on the live
924+
// stackoverflow homepage that the bare body substring used to misfire on
925+
w.Write([]byte(`<!DOCTYPE html><html><body><a href="/questions/tagged/cakephp">cakephp</a></body></html>`))
910926
}))
911927
defer server.Close()
912928

@@ -915,7 +931,7 @@ func TestDetectFramework_CakePHPFalsePositive(t *testing.T) {
915931
t.Fatalf("unexpected error: %v", err)
916932
}
917933
if result != nil && result.Name == "CakePHP" {
918-
t.Errorf("false positive: detected CakePHP (confidence %.2f) on prose about cakes", result.Confidence)
934+
t.Errorf("false positive: detected CakePHP (confidence %.2f) on prose naming cakephp", result.Confidence)
919935
}
920936
}
921937

@@ -956,7 +972,8 @@ func TestDetectFramework_SvelteFalsePositive(t *testing.T) {
956972
func TestDetectFramework_StrapiFalsePositive(t *testing.T) {
957973
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
958974
w.WriteHeader(http.StatusOK)
959-
w.Write([]byte(`<!DOCTYPE html><html><body><script>fetch("/api/v1/users")</script></body></html>`))
975+
// prose naming the CMS plus a plain /api/ path: neither is the powered-by header
976+
w.Write([]byte(`<!DOCTYPE html><html><body><p>built with Strapi</p><script>fetch("/api/v1/users")</script></body></html>`))
960977
}))
961978
defer server.Close()
962979

@@ -965,14 +982,16 @@ func TestDetectFramework_StrapiFalsePositive(t *testing.T) {
965982
t.Fatalf("unexpected error: %v", err)
966983
}
967984
if result != nil && result.Name == "Strapi" {
968-
t.Errorf("false positive: detected Strapi (confidence %.2f) on a plain /api/ path", result.Confidence)
985+
t.Errorf("false positive: detected Strapi (confidence %.2f) on prose naming strapi", result.Confidence)
969986
}
970987
}
971988

972989
func TestDetectFramework_Strapi(t *testing.T) {
973990
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
991+
// the default poweredBy middleware sets this header on every response
992+
w.Header().Set("X-Powered-By", "Strapi <strapi.io>")
974993
w.WriteHeader(http.StatusOK)
975-
w.Write([]byte(`<!DOCTYPE html><html><body><div>powered by strapi</div></body></html>`))
994+
w.Write([]byte(`<!DOCTYPE html><html><body><div>welcome</div></body></html>`))
976995
}))
977996
defer server.Close()
978997

0 commit comments

Comments
 (0)