Skip to content

Commit 13c7ce1

Browse files
committed
feat(framework-detection): weighted bayesian detection algorithm
- weighted signature matching for more accurate framework detection - sigmoid normalization for confidence scores - version detection with semantic versioning support - header-only pattern
1 parent 1443721 commit 13c7ce1

File tree

1 file changed

+88
-42
lines changed

1 file changed

+88
-42
lines changed

pkg/scan/frameworks/detect.go

Lines changed: 88 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -22,42 +22,48 @@ type FrameworkResult struct {
2222
Suggestions []string `json:"suggestions,omitempty"`
2323
}
2424

25-
var frameworkSignatures = map[string][]string{
25+
type FrameworkSignature struct {
26+
Pattern string
27+
Weight float32
28+
HeaderOnly bool
29+
}
30+
31+
var frameworkSignatures = map[string][]FrameworkSignature{
2632
"Laravel": {
27-
`laravel_session`,
28-
`XSRF-TOKEN`,
29-
`<meta name="csrf-token"`,
33+
{Pattern: `laravel_session`, Weight: 0.4, HeaderOnly: true},
34+
{Pattern: `XSRF-TOKEN`, Weight: 0.3, HeaderOnly: true},
35+
{Pattern: `<meta name="csrf-token"`, Weight: 0.3},
3036
},
3137
"Django": {
32-
`csrfmiddlewaretoken`,
33-
`django.contrib`,
34-
`django.core`,
35-
`__admin_media_prefix__`,
38+
{Pattern: `csrfmiddlewaretoken`, Weight: 0.4, HeaderOnly: true},
39+
{Pattern: `django.contrib`, Weight: 0.3},
40+
{Pattern: `django.core`, Weight: 0.3},
41+
{Pattern: `__admin_media_prefix__`, Weight: 0.3},
3642
},
3743
"Ruby on Rails": {
38-
`csrf-param`,
39-
`csrf-token`,
40-
`ruby-on-rails`,
41-
`rails-env`,
44+
{Pattern: `csrf-param`, Weight: 0.4, HeaderOnly: true},
45+
{Pattern: `csrf-token`, Weight: 0.3, HeaderOnly: true},
46+
{Pattern: `ruby-on-rails`, Weight: 0.3},
47+
{Pattern: `rails-env`, Weight: 0.3},
4248
},
4349
"Express.js": {
44-
`express`,
45-
`connect.sid`,
50+
{Pattern: `express`, Weight: 0.4, HeaderOnly: true},
51+
{Pattern: `connect.sid`, Weight: 0.3, HeaderOnly: true},
4652
},
4753
"ASP.NET": {
48-
`ASP.NET`,
49-
`__VIEWSTATE`,
50-
`__EVENTVALIDATION`,
54+
{Pattern: `ASP.NET`, Weight: 0.4, HeaderOnly: true},
55+
{Pattern: `__VIEWSTATE`, Weight: 0.3},
56+
{Pattern: `__EVENTVALIDATION`, Weight: 0.3},
5157
},
5258
"Spring": {
53-
`org.springframework`,
54-
`spring-security`,
55-
`jsessionid`,
59+
{Pattern: `org.springframework`, Weight: 0.4, HeaderOnly: true},
60+
{Pattern: `spring-security`, Weight: 0.3, HeaderOnly: true},
61+
{Pattern: `jsessionid`, Weight: 0.3, HeaderOnly: true},
5662
},
5763
"Flask": {
58-
`flask`,
59-
`werkzeug`,
60-
`jinja2`,
64+
{Pattern: `flask`, Weight: 0.4, HeaderOnly: true},
65+
{Pattern: `werkzeug`, Weight: 0.3, HeaderOnly: true},
66+
{Pattern: `jinja2`, Weight: 0.3},
6167
},
6268
}
6369

@@ -88,14 +94,23 @@ func DetectFramework(url string, timeout time.Duration, logdir string) (*Framewo
8894
var highestConfidence float32
8995

9096
for framework, signatures := range frameworkSignatures {
91-
var matches int
97+
var weightedScore float32
98+
var totalWeight float32
99+
92100
for _, sig := range signatures {
93-
if strings.Contains(bodyStr, sig) || containsHeader(resp.Header, sig) {
94-
matches++
101+
totalWeight += sig.Weight
102+
103+
if sig.HeaderOnly {
104+
if containsHeader(resp.Header, sig.Pattern) {
105+
weightedScore += sig.Weight
106+
}
107+
} else if strings.Contains(bodyStr, sig.Pattern) {
108+
weightedScore += sig.Weight
95109
}
96110
}
97111

98-
confidence := float32(matches) / float32(len(signatures))
112+
confidence := float32(1.0 / (1.0 + exp(-float64(weightedScore/totalWeight)*6.0)))
113+
99114
if confidence > highestConfidence {
100115
highestConfidence = confidence
101116
bestMatch = framework
@@ -118,7 +133,6 @@ func DetectFramework(url string, timeout time.Duration, logdir string) (*Framewo
118133
frameworklog.Infof("Detected %s framework (version: %s) with %.2f confidence",
119134
styles.Highlight.Render(bestMatch), version, highestConfidence)
120135

121-
// Add CVEs and suggestions based on version
122136
if cves, suggestions := getVulnerabilities(bestMatch, version); len(cves) > 0 {
123137
result.CVEs = cves
124138
result.Suggestions = suggestions
@@ -146,23 +160,34 @@ func containsHeader(headers http.Header, signature string) bool {
146160
}
147161

148162
func detectVersion(body string, framework string) string {
149-
patterns := map[string]*regexp.Regexp{
150-
"Laravel": regexp.MustCompile(`Laravel[/\s+]?([\d.]+)`),
151-
"Django": regexp.MustCompile(`Django/([\d.]+)`),
152-
"Ruby on Rails": regexp.MustCompile(`Rails/([\d.]+)`),
153-
"Express.js": regexp.MustCompile(`express/([\d.]+)`),
154-
"ASP.NET": regexp.MustCompile(`ASP\.NET[/\s+]?([\d.]+)`),
155-
"Spring": regexp.MustCompile(`spring-(core|framework)/([\d.]+)`),
156-
"Flask": regexp.MustCompile(`Flask/([\d.]+)`),
163+
version := extractVersion(body, framework)
164+
if version == "Unknown" {
165+
return version
157166
}
158167

159-
if pattern, exists := patterns[framework]; exists {
160-
matches := pattern.FindStringSubmatch(body)
161-
if len(matches) > 1 {
162-
return matches[1]
163-
}
168+
parts := strings.Split(version, ".")
169+
var normalized string
170+
if len(parts) >= 3 {
171+
normalized = fmt.Sprintf("%05s.%05s.%05s", parts[0], parts[1], parts[2])
164172
}
165-
return "Unknown"
173+
return normalized
174+
}
175+
176+
func exp(x float64) float64 {
177+
if x > 88.0 {
178+
return 1e38
179+
}
180+
if x < -88.0 {
181+
return 0
182+
}
183+
184+
sum := 1.0
185+
term := 1.0
186+
for i := 1; i <= 20; i++ {
187+
term *= x / float64(i)
188+
sum += term
189+
}
190+
return sum
166191
}
167192

168193
func getVulnerabilities(framework, version string) ([]string, []string) {
@@ -177,3 +202,24 @@ func getVulnerabilities(framework, version string) ([]string, []string) {
177202
}
178203
return nil, nil
179204
}
205+
206+
func extractVersion(body string, framework string) string {
207+
versionPatterns := map[string]string{
208+
"Laravel": `Laravel\s+[Vv]?(\d+\.\d+\.\d+)`,
209+
"Django": `Django\s+[Vv]?(\d+\.\d+\.\d+)`,
210+
"Ruby on Rails": `Rails\s+[Vv]?(\d+\.\d+\.\d+)`,
211+
"Express.js": `Express\s+[Vv]?(\d+\.\d+\.\d+)`,
212+
"ASP.NET": `ASP\.NET\s+[Vv]?(\d+\.\d+\.\d+)`,
213+
"Spring": `Spring\s+[Vv]?(\d+\.\d+\.\d+)`,
214+
"Flask": `Flask\s+[Vv]?(\d+\.\d+\.\d+)`,
215+
}
216+
217+
if pattern, exists := versionPatterns[framework]; exists {
218+
re := regexp.MustCompile(pattern)
219+
matches := re.FindStringSubmatch(body)
220+
if len(matches) > 1 {
221+
return matches[1]
222+
}
223+
}
224+
return "Unknown"
225+
}

0 commit comments

Comments
 (0)