Skip to content

Commit d5e7447

Browse files
committed
feat(frameworks): enrich cve findings with reference urls
add References []string to CVEEntry, populated with a deterministic NVD /vuln/detail/<CVE> url per entry. getVulnerabilities and WithVulnerabilities now also return/carry references, surfaced on FrameworkResult.References (json, omitempty) and the builtin module's Extracted["references"]. report-only: finding.Finding and flattenFramework are untouched, so the normalized line sink is unaffected.
1 parent a38ba0a commit d5e7447

6 files changed

Lines changed: 67 additions & 14 deletions

File tree

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: 21 additions & 2 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
}
@@ -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+
}

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

internal/scan/frameworks/detect_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ func TestDetectFramework_NoMatch(t *testing.T) {
286286

287287
func TestFrameworkResult_Fields(t *testing.T) {
288288
result := frameworks.NewFrameworkResult("Laravel", "9.0.0", 0.85, 0.9)
289-
result.WithVulnerabilities([]string{"CVE-2021-3129"}, []string{"Update to latest version"})
289+
result.WithVulnerabilities([]string{"CVE-2021-3129"}, []string{"Update to latest version"}, []string{"https://nvd.nist.gov/vuln/detail/CVE-2021-3129"})
290290

291291
if result.Name != "Laravel" {
292292
t.Errorf("expected Name 'Laravel', got '%s'", result.Name)
@@ -306,6 +306,9 @@ func TestFrameworkResult_Fields(t *testing.T) {
306306
if len(result.Suggestions) != 1 {
307307
t.Errorf("expected 1 suggestion, got %d", len(result.Suggestions))
308308
}
309+
if len(result.References) != 1 {
310+
t.Errorf("expected 1 reference, got %d", len(result.References))
311+
}
309312
}
310313

311314
func TestExtractVersionWithConfidence(t *testing.T) {
@@ -353,7 +356,7 @@ func TestDetermineRiskLevel(t *testing.T) {
353356
t.Run(tt.name, func(t *testing.T) {
354357
// Test via WithVulnerabilities which uses determineRiskLevel internally
355358
result := frameworks.NewFrameworkResult("Test", "1.0", 0.5, 0.5)
356-
result.WithVulnerabilities(tt.cves, nil)
359+
result.WithVulnerabilities(tt.cves, nil, nil)
357360
if result.RiskLevel != tt.expected {
358361
t.Errorf("determineRiskLevel() = %q, want %q", result.RiskLevel, tt.expected)
359362
}

internal/scan/frameworks/result.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type FrameworkResult struct {
2828
CVEs []string `json:"cves,omitempty"`
2929
Suggestions []string `json:"suggestions,omitempty"`
3030
RiskLevel string `json:"risk_level,omitempty"`
31+
References []string `json:"references,omitempty"`
3132
}
3233

3334
// ResultType implements the ScanResult interface.
@@ -44,10 +45,11 @@ func NewFrameworkResult(name, version string, confidence, versionConfidence floa
4445
}
4546

4647
// WithVulnerabilities adds CVE information to the result.
47-
func (r *FrameworkResult) WithVulnerabilities(cves, suggestions []string) *FrameworkResult {
48+
func (r *FrameworkResult) WithVulnerabilities(cves, suggestions, references []string) *FrameworkResult {
4849
r.CVEs = cves
4950
r.Suggestions = suggestions
5051
r.RiskLevel = determineRiskLevel(cves)
52+
r.References = references
5153
return r
5254
}
5355

0 commit comments

Comments
 (0)