Skip to content

Commit ad7b2ce

Browse files
committed
refactor: improve security dashboard UI, refine artifact categorization logic, and fix domain input sanitization.
1 parent bf90a9a commit ad7b2ce

6 files changed

Lines changed: 96 additions & 27 deletions

File tree

internal/api/assets_api.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ func buildAssets(scanID string) []AssetEntry {
147147
// For enumeration-style scans, pull the full record from the subdomains table.
148148
subdomainScanTypes := map[string]bool{
149149
"domain_run": true, "subdomain_run": true, "recon": true,
150-
"subdomains": true, "livehosts": true, "dns_cf1016": true, "dns-cf1016": true,
150+
"subdomains": true, "livehosts": true,
151151
}
152152
if scanTarget != "" && subdomainScanTypes[scanType] {
153153
// The DB stores subdomains under the ROOT domain key, not a subdomain target.
@@ -226,6 +226,26 @@ func buildAssets(scanID string) []AssetEntry {
226226
}
227227
}
228228
}
229+
230+
// Also read cf1016-vulnerabilities.json — findings are implicitly live
231+
if raw, _, err := loadFileContent(scanID, "cf1016-vulnerabilities.json"); err == nil && len(raw) > 0 {
232+
var arr []map[string]interface{}
233+
if json.Unmarshal(raw, &arr) == nil {
234+
for _, obj := range arr {
235+
subdomain := strPick(obj, "subdomain", "target")
236+
host := normalizeHost(subdomain)
237+
if e := getOrCreate(host); e != nil {
238+
e.IsLive = true
239+
if e.StatusCode == 0 {
240+
e.StatusCode = intPick(obj, "http_status", "status_code")
241+
}
242+
if e.URL == "" {
243+
e.URL = "https://" + host
244+
}
245+
}
246+
}
247+
}
248+
}
229249
// (Database-first discovery above already handled ListLiveSubdomains info)
230250

231251
// ── 3. Tech-detect — status code, title, tech ────────────────────────────

internal/api/scan_handlers.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -786,7 +786,8 @@ var execCommand = exec.Command
786786
// type is handled; returns "", false if the scan type is not supported.
787787
func runInProcessRescan(scanType, target string) (newScanID string, ok bool) {
788788
newScanID = generateScanID()
789-
switch scanType {
789+
st := strings.ToLower(strings.TrimSpace(scanType))
790+
switch st {
790791
case "domain_run":
791792
go RunScanInProcess(newScanID, scanType, target, func() error {
792793
_, err := domainmod.RunDomain(domainmod.ScanOptions{Domain: target})

internal/api/ui/pages/domains.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
</div>
6868
<div class="filter-bar" style="margin-bottom:16px">
6969
<input class="search-input" id="subdomain-search" placeholder="Filter subdomains…"
70-
oninput="window.renderSubdomainView('${window.esc(domain)}')" value="${q}">
70+
oninput="window.renderSubdomainView('${window.esc(domain)}')" value="${window.esc(q)}">
7171
</div>
7272
<div class="card">
7373
<div class="card-body">

internal/api/ui/pages/scan-common.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,13 @@
9090
if (existingModule) return existingModule;
9191
const n = String(fileName || '').toLowerCase();
9292
if (!n) return 'unknown';
93+
// JS secret artifacts (check this before generic github/secret checks)
94+
if (n.includes('js-secret') || n.includes('js-exposure') || n.includes('js-analysis') || n.startsWith('js-sec')) return 'js-analysis';
9395
if (n.includes('/apkx/') || n.includes('\\apkx\\')) return 'apkx';
9496
// GitHub must be BEFORE generic "secret" match
9597
if (n.includes('github-secret') || n.includes('github-secrets') || (n.includes('github') && n.includes('secret'))) return 'github-scan';
96-
if (n.includes('github') || n.includes('trufflehog') || n.includes('secrets') || (n.endsWith('.json') && n.includes('github'))) return 'github-scan';
98+
if (n.includes('github') || n.includes('trufflehog') || (n.endsWith('.json') && n.includes('github')) || n.includes('git-secret')) return 'github-scan';
9799
if (n.startsWith('nuclei-') || n.includes('nuclei')) return 'nuclei';
98-
if (n.includes('subdomain') || n.includes('subfinder') || n.includes('amass')) return 'subdomain-enum';
99-
if (n.includes('live-subs') || n.includes('httpx') || n.includes('livehosts')) return 'httpx';
100-
// js-urls are URL corpus (pipeline input), not JS analysis findings
101-
if (n.includes('js-urls') || n.includes('jsurl') || n.includes('js-enum')) return 'url-collection';
102-
// JS secret artifacts only (avoid matching trufflehog "secrets.json" via substring "secret")
103-
if (n.includes('js-secret') || n.includes('js-exposure')) return 'js-analysis';
104100
if (n.includes('apk') || n.includes('androidmanifest') || n.includes('jadx') || n.includes('dex')) return 'apkx';
105101
if (n.includes('kxss') || n.includes('dalfox') || n.includes('xss-reflection')) return 'xss-detection';
106102
if (n.includes('reflection')) return 'xss-detection';

internal/api/ui/pages/scan-detail.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@
1919
window._scanDetailRefreshTimer = null;
2020
window._scanDetailRefreshId = null;
2121
let _assetsCache = null;
22+
let _assetsLoading = false;
2223

2324
async function renderScanDetailView(scanId) {
25+
_assetsCache = null;
26+
_assetsLoading = false;
2427
const container = document.getElementById('scan-detail-container');
2528
const sub = document.getElementById('scan-detail-sub');
2629
const apiA = document.getElementById('scan-detail-api');
@@ -296,6 +299,7 @@
296299
scheduleScanDetailRefresh(scanId, 4500);
297300
} else {
298301
clearScanDetailRefreshTimer();
302+
_assetsCache = null; // Reset cache on auto-refresh to pick up new assets
299303
await renderScanDetailView(scanId);
300304
}
301305
} catch (e) {
@@ -389,9 +393,10 @@
389393
if (kind === 'js-urls') kind = 'js_urls';
390394
if (kind === 'unknown' || kind === 'unknowns') kind = 'other';
391395

392-
const looksLikeJSMatcher = (/^\s*\[[^\]]+\].*->/i.test(finding) || (file.includes('js-') && !file.includes('trufflehog') && !file.includes('github'))) && !file.includes('trufflehog') && !file.includes('github-secrets');
396+
const looksLikeJSMatcher = (/^\s*\[[^\]]+\].*->/i.test(finding) || (file.includes('js-') && !file.includes('trufflehog') && !file.includes('github'))) && !file.includes('trufflehog') && !file.includes('github-secrets');
393397
const looksLikeJSURL = file.includes('js-url') || /\.m?jsx?(\?|$)/i.test(target);
394-
const looksLikeGitHub = file.includes('github') || file.includes('trufflehog') || file.includes('secrets_table') || file.includes('github-secrets') || (file.includes('secrets') && file.endsWith('.json'));
398+
// Exclude JS analysis files from GitHub bucket
399+
const looksLikeGitHub = (file.includes('github') || file.includes('trufflehog') || file.includes('secrets_table') || file.includes('github-secrets') || (file.includes('secrets') && file.endsWith('.json'))) && !file.startsWith('js-');
395400

396401
if (looksLikeGitHub) kind = 'github-scan';
397402
else if (looksLikeJSMatcher) kind = 'js-analysis';
@@ -508,7 +513,7 @@ const looksLikeJSMatcher = (/^\s*\[[^\]]+\].*->/i.test(finding) || (file.include
508513
if (bi !== -1) return 1;
509514
return a.localeCompare(b);
510515
});
511-
const excludedModuleTabs = new Set(['autoar', 'unknown', 'tech-detect', 'ffuf-fuzzing', 'js-analysis']);
516+
const excludedModuleTabs = new Set(['autoar', 'unknown', 'tech-detect', 'ffuf-fuzzing', 'js-analysis', 'github-scan', 'nuclei', 'ffuf', 'reflection', 'js-analysis']);
512517
const hasUrlsDatasetTab = UNIQUE_TABS.some((t) => t[0] === 'urls');
513518
if (hasUrlsDatasetTab) excludedModuleTabs.add('url-collection');
514519
const hasApkxDatasetTab = UNIQUE_TABS.some((t) => t[0] === 'apkx');

internal/api/ui/securitylab/index.html

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,25 @@
2121
.wrap { max-width: 1200px; margin: 0 auto; padding: 20px; }
2222
.title { font-size: 24px; font-weight: 800; margin: 0 0 4px; }
2323
.sub { color: var(--muted); margin: 0 0 18px; }
24-
.tabs { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
25-
.tab { background: #151a33; color: var(--muted); border: 1px solid var(--border); padding: 8px 12px; border-radius: 10px; cursor: pointer; font-weight: 600; }
26-
.tab.active { color: var(--text); border-color: var(--accent); background: rgba(123,139,255,.12); }
24+
.tabs { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px; }
25+
.tab {
26+
background: rgba(21, 26, 51, 0.6);
27+
color: var(--muted);
28+
border: 1px solid var(--border);
29+
padding: 10px 16px;
30+
border-radius: 12px;
31+
cursor: pointer;
32+
font-weight: 600;
33+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
34+
backdrop-filter: blur(8px);
35+
}
36+
.tab:hover { background: rgba(123, 139, 255, 0.1); color: var(--text); border-color: rgba(123, 139, 255, 0.4); }
37+
.tab.active {
38+
color: #fff;
39+
border-color: var(--accent);
40+
background: linear-gradient(135deg, rgba(123, 139, 255, 0.25), rgba(123, 139, 255, 0.1));
41+
box-shadow: 0 4px 15px rgba(123, 139, 255, 0.15);
42+
}
2743
.panel { display: none; }
2844
.panel.active { display: block; }
2945
.card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 14px; margin-bottom: 12px; }
@@ -58,20 +74,50 @@
5874
.xss-row:hover { background:rgba(255,255,255,.03); }
5975
.xss-row:last-child { border-bottom:0; }
6076
.xss-meta { display:flex; gap:6px; flex-wrap:wrap; margin-bottom:6px; align-items:center; }
61-
.xss-event { font-size:11px; font-weight:700; color:var(--accent); background:rgba(123,139,255,.12); padding:2px 8px; border-radius:6px; }
62-
.xss-tag { font-size:11px; font-weight:600; color:var(--ok); background:rgba(52,211,153,.1); padding:2px 7px; border-radius:6px; }
77+
.xss-event { font-size: 11px; font-weight: 700; color: #fff; background: linear-gradient(135deg, #6366f1, #8b5cf6); padding: 3px 10px; border-radius: 8px; box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3); }
78+
.xss-tag { font-size: 11px; font-weight: 700; color: #fff; background: linear-gradient(135deg, #10b981, #059669); padding: 3px 10px; border-radius: 8px; box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3); }
6379
.xss-ui { font-size:11px; padding:2px 7px; border-radius:6px; font-weight:600; }
6480
.xss-ui.auto { background:rgba(52,211,153,.08); color:var(--ok); }
6581
.xss-ui.manual { background:rgba(245,158,11,.08); color:var(--warn); }
6682
.xss-code { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; color:#c5d0ff; word-break:break-all; line-height:1.5; flex:1; }
6783
.xss-desc { font-size:11px; color:var(--muted); margin-top:4px; }
6884
.xss-copy-btn { flex-shrink:0; background:#1a1f3b; border:1px solid var(--border); color:var(--muted); border-radius:7px; padding:4px 9px; font-size:11px; cursor:pointer; white-space:nowrap; }
6985
.xss-copy-btn:hover { color:var(--text); border-color:var(--accent); }
70-
.xss-group-header { padding:8px 10px; background:rgba(123,139,255,.06); border-bottom:1px solid var(--border); display:flex; justify-content:space-between; align-items:center; position:sticky; top:0; z-index:5; }
71-
.xss-group-name { font-size:12px; font-weight:800; text-transform:uppercase; letter-spacing:.6px; color:var(--accent); }
72-
.xss-count-badge { font-size:11px; color:var(--muted); }
73-
.xss-results-box { border:1px solid var(--border); border-radius:10px; overflow:auto; max-height:600px; background:#090c1a; }
74-
.copy-bulk-btn { background:linear-gradient(135deg,#6577ff,#7f8fff); color:white; border:none; border-radius:8px; padding:7px 14px; cursor:pointer; font-weight:700; font-size:12px; }
86+
.xss-group-header {
87+
padding: 12px 16px;
88+
background: rgba(123, 139, 255, 0.08);
89+
border-bottom: 1px solid var(--border);
90+
display: flex;
91+
justify-content: space-between;
92+
align-items: center;
93+
position: sticky;
94+
top: 0;
95+
z-index: 10;
96+
backdrop-filter: blur(12px);
97+
}
98+
.xss-group-name { font-size: 13px; font-weight: 800; text-transform: uppercase; letter-spacing: 1px; color: var(--accent); text-shadow: 0 0 10px rgba(123, 139, 255, 0.3); }
99+
.xss-count-badge { font-size: 11px; color: var(--muted); background: rgba(255,255,255,0.05); padding: 2px 8px; border-radius: 20px; }
100+
.xss-results-box {
101+
border: 1px solid var(--border);
102+
border-radius: 14px;
103+
overflow: auto;
104+
max-height: 650px;
105+
background: rgba(9, 12, 26, 0.7);
106+
box-shadow: inset 0 0 40px rgba(0,0,0,0.4);
107+
}
108+
.copy-bulk-btn {
109+
background: linear-gradient(135deg, #4f46e5, #7c3aed);
110+
color: white;
111+
border: none;
112+
border-radius: 10px;
113+
padding: 8px 18px;
114+
cursor: pointer;
115+
font-weight: 700;
116+
font-size: 13px;
117+
transition: transform 0.1s, box-shadow 0.2s;
118+
}
119+
.copy-bulk-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 15px rgba(124, 58, 237, 0.4); }
120+
.copy-bulk-btn:active { transform: translateY(0); }
75121
</style>
76122
<script src="ssrf-payloads.js"></script>
77123
<script src="xss-payloads.js"></script>
@@ -266,7 +312,7 @@ <h1 class="title">Security Lab</h1>
266312
$('jwtPayload').textContent = '{}';
267313
checks.push(['bad', 'Invalid JWT: ' + e.message]);
268314
}
269-
$('jwtChecks').innerHTML = checks.map(([cls, msg]) => `<div class="item"><span class="pill ${cls}">${cls.toUpperCase()}</span> <span style="margin-left:8px">${msg}</span></div>`).join('');
315+
$('jwtChecks').innerHTML = checks.map(([cls, msg]) => `<div class="item"><span class="pill ${cls}">${cls.toUpperCase()}</span> <span style="margin-left:8px">${escapeHTML(msg)}</span></div>`).join('');
270316
});
271317

272318
$('regexRun').addEventListener('click', () => {
@@ -290,10 +336,10 @@ <h1 class="title">Security Lab</h1>
290336
if (matches.length > 500) break;
291337
}
292338
$('regexResults').innerHTML = matches.length
293-
? matches.map(x => `<div class="item"><span class="pill ok">MATCH</span> <span class="muted" style="margin-left:8px">@${x.idx}</span><div style="margin-top:6px"><code>${x.val.replace(/</g, '&lt;')}</code></div></div>`).join('')
339+
? matches.map(x => `<div class="item"><span class="pill ok">MATCH</span> <span class="muted" style="margin-left:8px">@${x.idx}</span><div style="margin-top:6px"><code>${escapeHTML(x.val)}</code></div></div>`).join('')
294340
: '<div class="item muted">No matches.</div>';
295341
} catch (e) {
296-
$('regexResults').innerHTML = `<div class="item"><span class="pill bad">ERROR</span><span style="margin-left:8px">${e.message}</span></div>`;
342+
$('regexResults').innerHTML = `<div class="item"><span class="pill bad">ERROR</span><span style="margin-left:8px">${escapeHTML(e.message)}</span></div>`;
297343
}
298344
});
299345

@@ -746,7 +792,8 @@ <h1 class="title">Security Lab</h1>
746792
return true;
747793
});
748794

749-
$('xssStats').textContent = `${xssFilteredPayloads.length} payload(s) of ${window.XSS_PAYLOADS.length} total`;
795+
const statsText = search ? `${xssFilteredPayloads.length} payload(s) matching "${escapeHTML(search)}"` : `${xssFilteredPayloads.length} payload(s) of ${window.XSS_PAYLOADS.length} total`;
796+
$('xssStats').textContent = statsText;
750797

751798
const container = $('xssResults');
752799
// Clear safely — no innerHTML involved

0 commit comments

Comments
 (0)