Skip to content

Commit 0d4a523

Browse files
authored
Merge pull request #380 from TBX3D/lane/cve-references
feat(frameworks): enrich cve findings with reference urls
2 parents 5414082 + 0c1b989 commit 0d4a523

8 files changed

Lines changed: 190 additions & 16 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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 finding
14+
15+
import (
16+
"fmt"
17+
"strings"
18+
"testing"
19+
20+
"github.com/vmfunc/sif/internal/scan/frameworks"
21+
)
22+
23+
// a cve-enriched framework result must still collapse to the frozen
24+
// "[severity] target module title" line - version, cves and references stay
25+
// out of Line() even once cve enrichment is populated, so downstream
26+
// notify/grep/awk consumers never see the line shape shift under them.
27+
func TestFlattenFramework_LineFrozenWithCVEEnrichment(t *testing.T) {
28+
fw := &frameworks.FrameworkResult{
29+
Name: "Drupal",
30+
Version: "10",
31+
Confidence: 0.9,
32+
CVEs: []string{"CVE-2023-44487 (high)"},
33+
Suggestions: []string{"Update to Drupal 10.1.4 or later"},
34+
RiskLevel: "high",
35+
References: []string{"https://nvd.nist.gov/vuln/detail/CVE-2023-44487"},
36+
}
37+
38+
findings := Flatten(target, "framework", fw)
39+
if len(findings) != 1 {
40+
t.Fatalf("got %d findings, want 1", len(findings))
41+
}
42+
43+
f := findings[0]
44+
want := fmt.Sprintf("[%s] %s %s %s", f.Severity, target, "framework", "Drupal detected")
45+
if got := f.Line(); got != want {
46+
t.Errorf("Line() = %q, want %q", got, want)
47+
}
48+
if f.Severity != SeverityHigh {
49+
t.Errorf("severity = %v, want %v (from RiskLevel=high)", f.Severity, SeverityHigh)
50+
}
51+
for _, leak := range []string{"CVE-2023-44487", "nvd.nist.gov"} {
52+
if strings.Contains(f.Line(), leak) {
53+
t.Errorf("Line() leaked enrichment detail %q: %q", leak, f.Line())
54+
}
55+
}
56+
}

internal/scan/builtin/frameworks_module.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ func (m *FrameworksModule) Execute(ctx context.Context, target string, opts modu
8989
finding.Extracted["recommendations"] = strings.Join(frameworkResult.Suggestions, "; ")
9090
}
9191

92+
// Add reference URLs
93+
if len(frameworkResult.References) > 0 {
94+
finding.Extracted["references"] = strings.Join(frameworkResult.References, ", ")
95+
}
96+
9297
result.Findings = append(result.Findings, finding)
9398

9499
return result, nil

internal/scan/frameworks/cve.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type CVEEntry struct {
2020
Severity string // critical, high, medium, low
2121
Description string
2222
Recommendations []string
23+
References []string
2324
}
2425

2526
// knownCVEs contains known vulnerabilities for popular frameworks.
@@ -33,6 +34,7 @@ var knownCVEs = map[string][]CVEEntry{
3334
Severity: "critical",
3435
Description: "Ignition debug mode RCE vulnerability",
3536
Recommendations: []string{"Update to Laravel 8.4.2 or later", "Disable debug mode in production"},
37+
References: []string{"https://nvd.nist.gov/vuln/detail/CVE-2021-3129"},
3638
},
3739
{
3840
CVE: "CVE-2021-21263",
@@ -41,6 +43,7 @@ var knownCVEs = map[string][]CVEEntry{
4143
Severity: "high",
4244
Description: "SQL injection via request validation",
4345
Recommendations: []string{"Update to Laravel 8.5.0 or later", "Use parameterized queries"},
46+
References: []string{"https://nvd.nist.gov/vuln/detail/CVE-2021-21263"},
4447
},
4548
},
4649
"Django": {
@@ -51,6 +54,7 @@ var knownCVEs = map[string][]CVEEntry{
5154
Severity: "high",
5255
Description: "Potential ReDoS in EmailValidator and URLValidator",
5356
Recommendations: []string{"Update to Django 4.2.3 or later"},
57+
References: []string{"https://nvd.nist.gov/vuln/detail/CVE-2023-36053"},
5458
},
5559
{
5660
CVE: "CVE-2023-31047",
@@ -59,6 +63,7 @@ var knownCVEs = map[string][]CVEEntry{
5963
Severity: "medium",
6064
Description: "File upload validation bypass",
6165
Recommendations: []string{"Update to Django 4.1.9 or later", "Implement additional file validation"},
66+
References: []string{"https://nvd.nist.gov/vuln/detail/CVE-2023-31047"},
6267
},
6368
},
6469
"WordPress": {
@@ -69,6 +74,7 @@ var knownCVEs = map[string][]CVEEntry{
6974
Severity: "medium",
7075
Description: "Directory traversal vulnerability",
7176
Recommendations: []string{"Update to WordPress 6.2 or later"},
77+
References: []string{"https://nvd.nist.gov/vuln/detail/CVE-2023-2745"},
7278
},
7379
},
7480
"Drupal": {
@@ -79,6 +85,7 @@ var knownCVEs = map[string][]CVEEntry{
7985
Severity: "high",
8086
Description: "HTTP/2 rapid reset attack (DoS)",
8187
Recommendations: []string{"Update to Drupal 10.1.4 or later", "Configure HTTP/2 rate limiting"},
88+
References: []string{"https://nvd.nist.gov/vuln/detail/CVE-2023-44487"},
8289
},
8390
},
8491
"Next.js": {
@@ -89,6 +96,7 @@ var knownCVEs = map[string][]CVEEntry{
8996
Severity: "medium",
9097
Description: "Server-side request forgery vulnerability",
9198
Recommendations: []string{"Update to Next.js 13.5.0 or later"},
99+
References: []string{"https://nvd.nist.gov/vuln/detail/CVE-2023-46298"},
92100
},
93101
},
94102
"Angular": {
@@ -99,6 +107,7 @@ var knownCVEs = map[string][]CVEEntry{
99107
Severity: "medium",
100108
Description: "Regular expression denial of service",
101109
Recommendations: []string{"Update to Angular 15.2.0 or later"},
110+
References: []string{"https://nvd.nist.gov/vuln/detail/CVE-2023-26117"},
102111
},
103112
},
104113
"Vue.js": {
@@ -109,6 +118,7 @@ var knownCVEs = map[string][]CVEEntry{
109118
Severity: "medium",
110119
Description: "XSS vulnerability in certain configurations",
111120
Recommendations: []string{"Update to Vue.js 2.7.16 or 3.x"},
121+
References: []string{"https://nvd.nist.gov/vuln/detail/CVE-2024-5987"},
112122
},
113123
},
114124
"Express.js": {
@@ -119,6 +129,7 @@ var knownCVEs = map[string][]CVEEntry{
119129
Severity: "medium",
120130
Description: "Open redirect vulnerability",
121131
Recommendations: []string{"Update to Express.js 4.19.2 or later"},
132+
References: []string{"https://nvd.nist.gov/vuln/detail/CVE-2024-29041"},
122133
},
123134
},
124135
"Ruby on Rails": {
@@ -129,6 +140,7 @@ var knownCVEs = map[string][]CVEEntry{
129140
Severity: "high",
130141
Description: "ReDoS vulnerability in Action Dispatch",
131142
Recommendations: []string{"Update to Rails 7.0.4.1 or later"},
143+
References: []string{"https://nvd.nist.gov/vuln/detail/CVE-2023-22795"},
132144
},
133145
},
134146
"Spring": {
@@ -139,6 +151,7 @@ var knownCVEs = map[string][]CVEEntry{
139151
Severity: "critical",
140152
Description: "Spring4Shell RCE vulnerability",
141153
Recommendations: []string{"Update to Spring 5.3.18 or later", "Disable class binding on user input"},
154+
References: []string{"https://nvd.nist.gov/vuln/detail/CVE-2022-22965"},
142155
},
143156
},
144157
"Spring Boot": {
@@ -149,6 +162,7 @@ var knownCVEs = map[string][]CVEEntry{
149162
Severity: "critical",
150163
Description: "RCE via Spring Cloud Function",
151164
Recommendations: []string{"Update to Spring Boot 2.6.6 or later"},
165+
References: []string{"https://nvd.nist.gov/vuln/detail/CVE-2022-22963"},
152166
},
153167
},
154168
"ASP.NET": {
@@ -159,6 +173,7 @@ var knownCVEs = map[string][]CVEEntry{
159173
Severity: "high",
160174
Description: "Elevation of privilege vulnerability",
161175
Recommendations: []string{"Apply latest security patches", "Ensure proper request validation"},
176+
References: []string{"https://nvd.nist.gov/vuln/detail/CVE-2023-36899"},
162177
},
163178
},
164179
"Joomla": {
@@ -169,6 +184,7 @@ var knownCVEs = map[string][]CVEEntry{
169184
Severity: "critical",
170185
Description: "Improper access check allowing unauthorized access to webservice endpoints",
171186
Recommendations: []string{"Update to Joomla 4.2.8 or later"},
187+
References: []string{"https://nvd.nist.gov/vuln/detail/CVE-2023-23752"},
172188
},
173189
},
174190
"Magento": {
@@ -179,6 +195,7 @@ var knownCVEs = map[string][]CVEEntry{
179195
Severity: "critical",
180196
Description: "Improper input validation leading to arbitrary code execution",
181197
Recommendations: []string{"Update to Magento 2.4.3-p1 or later"},
198+
References: []string{"https://nvd.nist.gov/vuln/detail/CVE-2022-24086"},
182199
},
183200
},
184201
}

internal/scan/frameworks/cve_internal_test.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,13 @@ func TestResolveVersionFeedsCVELookup(t *testing.T) {
5353
}
5454

5555
// ...and looking "unknown" up finds nothing, proving the old behavior missed it.
56-
if cves, _ := getVulnerabilities("Laravel", "unknown"); len(cves) != 0 {
56+
if cves, _, _ := getVulnerabilities("Laravel", "unknown"); len(cves) != 0 {
5757
t.Fatalf("expected no CVEs for unknown version, got %v", cves)
5858
}
5959

6060
// the reconciled version feeds the lookup and the CVE shows up.
6161
version := resolveVersion("unknown", extracted)
62-
cves, _ := getVulnerabilities("Laravel", version)
62+
cves, _, _ := getVulnerabilities("Laravel", version)
6363
if len(cves) == 0 {
6464
t.Errorf("expected Laravel %s to surface a CVE, got none", version)
6565
}
@@ -69,10 +69,10 @@ func TestResolveVersionFeedsCVELookup(t *testing.T) {
6969
// is "10"/"9". the cve lookup must still surface the matching CVE instead of
7070
// missing it because the affected entries are dotted.
7171
func TestGetVulnerabilitiesBareMajor(t *testing.T) {
72-
if cves, _ := getVulnerabilities("Drupal", "10"); len(cves) == 0 {
72+
if cves, _, _ := getVulnerabilities("Drupal", "10"); len(cves) == 0 {
7373
t.Error("expected Drupal 10 to surface CVE-2023-44487, got none")
7474
}
75-
if cves, _ := getVulnerabilities("Drupal", "9"); len(cves) == 0 {
75+
if cves, _, _ := getVulnerabilities("Drupal", "9"); len(cves) == 0 {
7676
t.Error("expected Drupal 9 to surface CVE-2023-44487, got none")
7777
}
7878
}
@@ -104,3 +104,22 @@ func TestVersionAffected(t *testing.T) {
104104
}
105105
}
106106
}
107+
108+
// reference URLs are deterministic NVD detail links derived from the CVE ID,
109+
// carried alongside cves/recommendations for report-only enrichment.
110+
func TestGetVulnerabilitiesReferences(t *testing.T) {
111+
_, _, references := getVulnerabilities("Drupal", "10")
112+
if len(references) == 0 {
113+
t.Fatal("expected at least one reference, got none")
114+
}
115+
want := "https://nvd.nist.gov/vuln/detail/CVE-2023-44487"
116+
found := false
117+
for _, ref := range references {
118+
if ref == want {
119+
found = true
120+
}
121+
}
122+
if !found {
123+
t.Errorf("expected references to contain %q, got %v", want, references)
124+
}
125+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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
14+
15+
import "testing"
16+
17+
// bare-major boundary matrix: a coarse detected major ("7") must cover its own
18+
// dotted sub-versions but never a version that merely shares the same leading
19+
// digits ("70.0", "17.0", "7000.0"). this is the off-by-one guard for the
20+
// bare-major fix in versionAffected - it pins the exact boundary, not just one
21+
// example of it.
22+
func TestVersionAffected_BareMajorBoundary(t *testing.T) {
23+
tests := []struct {
24+
version string
25+
affected string
26+
want bool
27+
}{
28+
{"7", "7.0", true},
29+
{"7", "7.4", true},
30+
{"7", "7.9", true},
31+
{"7", "70.0", false},
32+
{"7", "17.0", false},
33+
{"7", "7000.0", false},
34+
{"7", "6.9", false},
35+
{"4.20", "4.2", false},
36+
{"4.1", "4.10", false},
37+
{"4.10", "4.1", false},
38+
}
39+
40+
for _, tt := range tests {
41+
if got := versionAffected(tt.version, tt.affected); got != tt.want {
42+
t.Errorf("versionAffected(%q, %q) = %v, want %v", tt.version, tt.affected, got, tt.want)
43+
}
44+
}
45+
}
46+
47+
// every CVE entry must carry at least one reference, and that reference must
48+
// be exactly the NVD detail link derived from the entry's own CVE ID - no
49+
// stale/typo'd/borrowed reference from a neighboring entry.
50+
func TestKnownCVEs_ReferencesMatchOwnCVEID(t *testing.T) {
51+
for framework, entries := range knownCVEs {
52+
for _, entry := range entries {
53+
if len(entry.References) == 0 {
54+
t.Errorf("%s %s: no references", framework, entry.CVE)
55+
continue
56+
}
57+
want := "https://nvd.nist.gov/vuln/detail/" + entry.CVE
58+
for _, ref := range entry.References {
59+
if ref != want {
60+
t.Errorf("%s %s: reference %q, want %q", framework, entry.CVE, ref, want)
61+
}
62+
}
63+
}
64+
}
65+
}

internal/scan/frameworks/detect.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,10 @@ func DetectFrameworks(url string, timeout time.Duration, logdir string) ([]*Fram
200200
func assembleResult(d detectionResult, bodyStr, url, logdir string, log *output.ModuleLogger) *FrameworkResult {
201201
versionMatch := ExtractVersionOptimized(bodyStr, d.name)
202202
version := resolveVersion(d.version, versionMatch.Version)
203-
cves, suggestions := getVulnerabilities(d.name, version)
203+
cves, suggestions, references := getVulnerabilities(d.name, version)
204204

205205
result := NewFrameworkResult(d.name, version, d.confidence, versionMatch.Confidence)
206-
result.WithVulnerabilities(cves, suggestions)
206+
result.WithVulnerabilities(cves, suggestions, references)
207207

208208
if logdir != "" {
209209
logEntry := fmt.Sprintf("Detected framework: %s (version: %s, confidence: %.2f, version_confidence: %.2f)\n",
@@ -255,18 +255,19 @@ func resolveVersion(detectorVersion, extractedVersion string) string {
255255
return unknownVersion
256256
}
257257

258-
// getVulnerabilities returns CVEs and recommendations for a framework version.
259-
func getVulnerabilities(framework, version string) ([]string, []string) {
258+
// getVulnerabilities returns CVEs, recommendations and reference URLs for a
259+
// framework version.
260+
func getVulnerabilities(framework, version string) (cves, recommendations, references []string) {
260261
entries, exists := knownCVEs[framework]
261262
if !exists {
262-
return nil, nil
263+
return nil, nil, nil
263264
}
264265

265-
var cves []string
266-
var recommendations []string
267266
seenRecs := make(map[string]bool)
267+
seenRefs := make(map[string]bool)
268268

269-
for _, entry := range entries {
269+
for i := range entries {
270+
entry := &entries[i]
270271
for _, affectedVer := range entry.AffectedVersions {
271272
if versionAffected(version, affectedVer) {
272273
cves = append(cves, fmt.Sprintf("%s (%s)", entry.CVE, entry.Severity))
@@ -276,12 +277,18 @@ func getVulnerabilities(framework, version string) ([]string, []string) {
276277
seenRecs[rec] = true
277278
}
278279
}
280+
for _, ref := range entry.References {
281+
if !seenRefs[ref] {
282+
references = append(references, ref)
283+
seenRefs[ref] = true
284+
}
285+
}
279286
break
280287
}
281288
}
282289
}
283290

284-
return cves, recommendations
291+
return cves, recommendations, references
285292
}
286293

287294
// versionAffected reports whether version falls under an affected-version

0 commit comments

Comments
 (0)