Skip to content

Commit ba3e5c8

Browse files
committed
feat: improve GF-pattern identification logic and normalize finding metadata for the frontend
1 parent 2bf479f commit ba3e5c8

2 files changed

Lines changed: 68 additions & 25 deletions

File tree

internal/api/scan_results_api.go

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1419,16 +1419,27 @@ func parseFindingFromObject(v map[string]interface{}, fallback string) parsedFin
14191419
}
14201420
}
14211421

1422-
template := firstNonEmpty(
1423-
fmt.Sprint(v["finding"]),
1424-
fmt.Sprint(v["template-id"]),
1425-
fmt.Sprint(v["template_id"]),
1426-
fmt.Sprint(v["template"]),
1427-
fmt.Sprint(v["id"]),
1428-
fmt.Sprint(v["name"]),
1429-
fmt.Sprint(v["title"]),
1430-
fmt.Sprint(v["issue"]),
1431-
)
1422+
// For GF-pattern findings: use template-id (e.g. "gf-ssrf") as the label,
1423+
// not the generic "Pattern-matched URL candidate" string stored in "finding".
1424+
findingVal := strings.TrimSpace(fmt.Sprint(v["finding"]))
1425+
templateIDVal := firstNonEmpty(fmt.Sprint(v["template-id"]), fmt.Sprint(v["template_id"]))
1426+
patternVal := strings.TrimSpace(fmt.Sprint(v["pattern"]))
1427+
isGFPattern := strings.EqualFold(strings.TrimSpace(fmt.Sprint(v["module"])), "gf-patterns")
1428+
var template string
1429+
if isGFPattern {
1430+
// Prefer explicit pattern name → template-id → fallback to finding
1431+
template = firstNonEmpty(templateIDVal, patternVal, findingVal)
1432+
} else {
1433+
template = firstNonEmpty(
1434+
findingVal,
1435+
templateIDVal,
1436+
fmt.Sprint(v["template"]),
1437+
fmt.Sprint(v["id"]),
1438+
fmt.Sprint(v["name"]),
1439+
fmt.Sprint(v["title"]),
1440+
fmt.Sprint(v["issue"]),
1441+
)
1442+
}
14321443
target := firstNonEmpty(
14331444
fmt.Sprint(v["matched-at"]),
14341445
fmt.Sprint(v["matched_at"]),
@@ -1476,10 +1487,20 @@ func parseFindingFromObject(v map[string]interface{}, fallback string) parsedFin
14761487
}
14771488
// Normalise nuclei hyphenated keys to underscored equivalents so the
14781489
// frontend can access them consistently (e.g. template-id → template_id).
1479-
normRaw := make(map[string]interface{}, len(v))
1490+
normRaw := make(map[string]interface{}, len(v)+4)
14801491
for k, val := range v {
14811492
normRaw[strings.ReplaceAll(k, "-", "_")] = val
14821493
}
1494+
// For GF findings: ensure pattern and template_id are always reachable
1495+
// by the frontend registry (some older scan JSON may only have template-id).
1496+
if isGFPattern {
1497+
if patternVal != "" {
1498+
normRaw["pattern"] = patternVal
1499+
}
1500+
if templateIDVal != "" {
1501+
normRaw["template_id"] = templateIDVal
1502+
}
1503+
}
14831504
return parsedFinding{
14841505
Severity: sev,
14851506
Target: target,

internal/api/ui/pages/module-registry.js

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -193,33 +193,55 @@
193193
gf: {
194194
columns: [
195195
{ id: 'target', label: 'TARGET', flex: '2', type: 'link' },
196+
{ id: 'sev', label: 'SEV', w: '68px', type: 'sev-badge', align: 'center' },
196197
{ id: 'pattern', label: 'PATTERN', flex: '1', type: 'badge-pill' },
197-
{ id: 'value', label: 'VALUE', flex: '3', type: 'mono-trunc' },
198-
{ id: 'source', label: 'SOURCE', flex: '1', type: 'muted-trunc' },
198+
{ id: 'value', label: 'MATCHED URL', flex: '3', type: 'mono-trunc' },
199199
],
200200
extract(r) {
201+
const raw = r.raw || {};
201202
const target = s(r.host || r.target || '-');
202-
203-
let patternName = s(r.pattern || r.finding_type || '');
203+
204+
// Resolve pattern name: prefer explicit raw.pattern → raw.template_id → r.finding → filename
205+
let patternName = s(raw.pattern || raw.template_id || '');
204206
if (!patternName && r.file) {
205-
patternName = r.file.replace(/\.txt$/i, '').replace(/^gf-/i, '');
206-
}
207-
if (!patternName) patternName = s(r.module || '—');
208-
209-
let valueStr = s(r.value || r.finding || '-');
210-
if (valueStr.toLowerCase() === 'gf-patterns' || valueStr.toLowerCase() === s(r.module).toLowerCase()) {
211-
valueStr = '-';
207+
patternName = r.file.replace(/\.txt$/i, '').replace(/^gf-/i, '').replace(/-results$/i, '');
212208
}
209+
if (!patternName) patternName = s(r.finding || r.module || '—');
210+
// Strip leading "gf-" prefix for display (gf-ssrf → ssrf)
211+
const displayName = patternName.replace(/^gf-/i, '');
212+
213+
// Pattern → colour mapping
214+
const patternColors = {
215+
ssrf: '#f87171', rce: '#f87171', lfi: '#f87171', sqli: '#f87171', ssti: '#f87171',
216+
xss: '#fb923c', redirect: '#fbbf24', idor: '#fbbf24', iparams: '#fbbf24', debug_logic: '#fbbf24',
217+
iext: '#4ade80', 'img-traversal': '#4ade80', isubs: '#22d3ee', jsvar: '#22d3ee',
218+
};
219+
const color = patternColors[displayName.toLowerCase()] || '#a78bfa';
220+
221+
// VALUE column: the actual matched URL (stored in target/host after backend parsing)
222+
const value = s(r.target || r.host || '-');
213223

214224
return {
215225
target: { href: toHref(target), label: target },
216-
pattern: { label: patternName, color: '#a78bfa' },
217-
value: valueStr,
218-
source: s(r.file || r.source || '—'),
226+
sev: sevMeta(r.severity),
227+
pattern: { label: displayName || patternName, color },
228+
value,
219229
};
220230
},
231+
detail(r) {
232+
const raw = r.raw || {};
233+
const patternName = s(raw.pattern || raw.template_id || r.finding || '');
234+
return buildFields([
235+
['Pattern', patternName.replace(/^gf-/i, '')],
236+
['Full ID', patternName],
237+
['Matched URL', s(r.target || r.host || ''), { isLink: true }],
238+
['Severity', s(r.severity)],
239+
['Source File', s(r.file || raw.module || '')],
240+
]);
241+
},
221242
},
222243

244+
223245
/* ── JS Analysis ────────────────────────────────────────────────────── */
224246
js: {
225247
columns: [

0 commit comments

Comments
 (0)