Skip to content

Commit 0130a54

Browse files
committed
feat: integrate Dalfox XSS scanner and add UI modules for Katana crawler and XSS detection.
1 parent 9d40743 commit 0130a54

5 files changed

Lines changed: 223 additions & 21 deletions

File tree

internal/api/scan_results_api.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,10 +316,15 @@ func inferModuleFromFileName(name string) string {
316316
// js-urls files are URL corpus lists (pipeline input), not JS analysis findings
317317
case strings.Contains(n, "js-url") || strings.Contains(n, "jsurl") || strings.Contains(n, "js-enum"):
318318
return "url-collection"
319+
// katana crawler results — separate from general URL collection
320+
case strings.Contains(n, "katana"):
321+
return "katana-crawler"
319322
// js-endpoints: API path extraction results from JS files
320323
case strings.Contains(n, "js-endpoint"):
321324
return "js-endpoints"
322-
// GitHub TruffleHog aggregate — must win over generic "secret" substring match.
325+
// js-secrets / js-exposure: actual secret/vuln findings from JS analysis
326+
case strings.Contains(n, "js-secret") || strings.Contains(n, "js-exposure"):
327+
return "js-analysis"
323328
case strings.Contains(n, "github-secret") || strings.Contains(n, "github-secrets") || (strings.Contains(n, "github") && strings.Contains(n, "secret")):
324329
return "github-scan"
325330
case strings.Contains(n, "js-secret") || strings.Contains(n, "js-exposure") || strings.Contains(n, "secret"):
@@ -350,7 +355,9 @@ func inferModuleFromFileName(name string) string {
350355
return "github-scan"
351356
case strings.Contains(n, "backup") || strings.Contains(n, "fuzzuli"):
352357
return "backup-detection"
353-
case strings.Contains(n, "reflection") || strings.Contains(n, "kxss") || strings.Contains(n, "dalfox") || strings.Contains(n, "xss"):
358+
case strings.Contains(n, "dalfox"):
359+
return "xss-detection"
360+
case strings.Contains(n, "reflection") || strings.Contains(n, "kxss") || strings.Contains(n, "xss"):
354361
return "reflection"
355362
case strings.Contains(n, "wp-confusion") || strings.Contains(n, "wp_confusion"):
356363
return "wordpress-confusion"

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

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,12 @@
6464
'nuclei': 'nuclei', 'mod:nuclei': 'nuclei',
6565
'ffuf': 'ffuf', 'ffuf-fuzzing': 'ffuf', 'mod:ffuf': 'ffuf',
6666
'gf-patterns': 'gf', 'mod:gf': 'gf',
67-
'js-analysis': 'js', 'mod:js': 'js', 'js': 'js',
68-
'js-endpoints': 'js-endpoints', 'mod:js-endpoints': 'js-endpoints',
67+
// JS modules — three distinct modules:
68+
'js-analysis': 'js', 'mod:js': 'js', 'js': 'js', // secrets/vuln findings
69+
'js-endpoints': 'js-endpoints', 'mod:js-endpoints': 'js-endpoints', // API paths extracted from JS
70+
'katana-crawler': 'katana', 'mod:katana': 'katana',
71+
'reflection': 'reflection', 'mod:reflection': 'reflection',
72+
'xss-detection': 'xss-detection', 'mod:xss-detection': 'xss-detection',
6973
'misconfig': 'misconfig', 'mod:misconfig': 'misconfig',
7074
'github-scan': 'github', 'mod:github': 'github',
7175
'apkx': 'apkx', 'mod:apkx': 'apkx',
@@ -243,10 +247,10 @@
243247
},
244248

245249

246-
/* ── JS Endpoints ───────────────────────────────────────────────────── */
250+
/* ── JS Endpoints (API paths extracted from JS files) ───────────────── */
247251
'js-endpoints': {
248252
columns: [
249-
{ id: 'endpoint', label: 'ENDPOINT', flex: '3', type: 'link' },
253+
{ id: 'endpoint', label: 'ENDPOINT', flex: '3', type: 'link' },
250254
{ id: 'source', label: 'SOURCE FILE', flex: '2', type: 'mono-muted' },
251255
],
252256
extract(r) {
@@ -261,14 +265,76 @@
261265
detail(r) {
262266
const raw = r.raw || {};
263267
return buildFields([
264-
['Endpoint', s(raw.endpoint || r.target || ''), { isLink: true }],
265-
['Source JS', s(r.file || '')],
266-
['Module', s(r.module || 'js-endpoints')],
268+
['Endpoint', s(raw.endpoint || r.target || ''), { isLink: true }],
269+
['Source JS', s(r.file || '')],
270+
['Module', 'JS Endpoints'],
267271
]);
268272
},
269273
},
270274

271-
/* ── JS Analysis ────────────────────────────────────────────────────── */
275+
/* ── Katana Crawler (JS-aware web crawler results) ───────────────────── */
276+
katana: {
277+
columns: [
278+
{ id: 'url', label: 'CRAWLED URL', flex: '4', type: 'link' },
279+
{ id: 'domain', label: 'DOMAIN', flex: '1', type: 'mono-muted' },
280+
],
281+
extract(r) {
282+
const raw = r.raw || {};
283+
const url = s(raw.url || r.target || r.finding || '-');
284+
let domain = '';
285+
try { domain = new URL(url).hostname; } catch (_) {}
286+
return {
287+
url: { href: toHref(url), label: url },
288+
domain,
289+
};
290+
},
291+
detail(r) {
292+
const raw = r.raw || {};
293+
const url = s(raw.url || r.target || r.finding || '');
294+
return buildFields([
295+
['URL', url, { isLink: true }],
296+
['Module', 'Katana Crawler'],
297+
]);
298+
},
299+
},
300+
301+
/* ── XSS Detection (Dalfox confirmed — from kxss {<}/{>} candidates) ───── */
302+
'xss-detection': {
303+
columns: [
304+
{ id: 'target', label: 'TARGET', flex: '3', type: 'link' },
305+
{ id: 'sev', label: 'SEV', w: '68px', type: 'sev-badge', align: 'center' },
306+
{ id: 'vulnType', label: 'TYPE', flex: '1', type: 'badge-pill' },
307+
{ id: 'parameter', label: 'PARAMETER', flex: '1', type: 'mono-muted' },
308+
{ id: 'payload', label: 'PAYLOAD', flex: '2', type: 'mono-trunc' },
309+
],
310+
extract(r) {
311+
const raw = r.raw || {};
312+
const target = s(raw['matched-at'] || r.target || r.host || '-');
313+
const vulnType = s(raw['template-id'] || r.finding || 'XSS');
314+
const param = s(raw.parameter || raw.param || r.parameter || '');
315+
const payload = s(raw.payload || r.payload || '');
316+
return {
317+
target: { href: toHref(target), label: target },
318+
sev: sevMeta(r.severity || raw.severity || 'high'),
319+
vulnType: { label: vulnType, color: '#f87171' },
320+
parameter: param,
321+
payload,
322+
};
323+
},
324+
detail(r) {
325+
const raw = r.raw || {};
326+
return buildFields([
327+
['Target', s(raw['matched-at'] || r.target || ''), { isLink: true }],
328+
['Type', s(raw['template-id'] || r.finding || '')],
329+
['Parameter', s(raw.parameter || raw.param || '')],
330+
['Payload', s(raw.payload || '')],
331+
['Severity', s(r.severity || raw.severity || 'high')],
332+
['Module', 'XSS Detection (Dalfox)'],
333+
]);
334+
},
335+
},
336+
337+
/* ── JS Analysis (secrets in JS files) ───────────────────────────────── */
272338
js: {
273339
columns: [
274340
{ id: 'file', label: 'JS FILE', flex: '2', type: 'link-amber' },

internal/scanner/reflection/reflection.go

Lines changed: 126 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"time"
1313

1414
kxsstool "github.com/h0tak88r/AutoAR/internal/tools/kxss"
15+
dalfoxtool "github.com/h0tak88r/AutoAR/internal/tools/dalfox"
1516
"github.com/h0tak88r/AutoAR/internal/scanner/urls"
1617
"github.com/h0tak88r/AutoAR/internal/utils"
1718
)
@@ -137,13 +138,6 @@ func ScanReflectionWithOptions(opts Options) (*Result, error) {
137138

138139
// Scan URLs with concurrency and timeout
139140
kxssResults, err := scanURLsWithConcurrency(ctx, validURLs, opts.Threads)
140-
type xssFinding struct {
141-
TemplateID string `json:"template-id"` // VULN TYPE column
142-
MatchedAt string `json:"matched-at"` // TARGET column
143-
Severity string `json:"severity"`
144-
Param string `json:"param"`
145-
Unfiltered []string `json:"unfiltered"`
146-
}
147141
findings := make([]xssFinding, 0, len(kxssResults))
148142
for _, r := range kxssResults {
149143
if r.URL == "" || r.Param == "" || len(r.Chars) == 0 {
@@ -194,7 +188,6 @@ func ScanReflectionWithOptions(opts Options) (*Result, error) {
194188
}
195189

196190
// Write structured JSON for the dashboard — one object per kxss finding.
197-
// Template: TARGET=url, VULN TYPE='xss @ param | Unfiltered: [chars]', SEV=medium.
198191
if scanID := utils.GetCurrentScanID(); scanID != "" {
199192
if len(findings) > 0 {
200193
if err := utils.WriteJSONToScanDir(scanID, "xss-reflection-vulnerabilities.json", findings); err != nil {
@@ -205,13 +198,138 @@ func ScanReflectionWithOptions(opts Options) (*Result, error) {
205198
}
206199
}
207200

201+
// ── Dalfox: confirm XSS on kxss findings that had {<} or {>} unfiltered ──
202+
// These characters indicate the site doesn't filter angle brackets —
203+
// the strongest signal for exploitable XSS. Feed only those URLs to dalfox.
204+
xssCandidateURLs := extractAngleBracketURLs(findings)
205+
if len(xssCandidateURLs) > 0 {
206+
logger.GetLogger().Infof("[INFO] Running dalfox on %d kxss angle-bracket candidates", len(xssCandidateURLs))
207+
runDalfoxOnURLs(xssCandidateURLs, opts.Domain, opts.Threads)
208+
} else {
209+
logger.GetLogger().Infof("[INFO] No angle-bracket candidates from kxss — skipping dalfox")
210+
if scanID := utils.GetCurrentScanID(); scanID != "" {
211+
_ = utils.WriteNoFindingsJSON(scanID, opts.Domain, "xss-detection", "dalfox-xss-results.json")
212+
}
213+
}
214+
208215
return &Result{
209216
Domain: opts.Domain,
210217
Reflections: reflectionCount,
211218
OutputFile: outFile,
212219
}, nil
213220
}
214221

222+
// xssFinding is one structured kxss result persisted to the dashboard.
223+
type xssFinding struct {
224+
TemplateID string `json:"template-id"`
225+
MatchedAt string `json:"matched-at"`
226+
Severity string `json:"severity"`
227+
Param string `json:"param"`
228+
Unfiltered []string `json:"unfiltered"`
229+
}
230+
231+
// extractAngleBracketURLs returns unique URLs from kxss findings where
232+
// either '{<}' or '{>}' (or both) was unfiltered — the strongest XSS signal.
233+
func extractAngleBracketURLs(findings []xssFinding) []string {
234+
seen := make(map[string]struct{})
235+
var out []string
236+
for _, f := range findings {
237+
for _, ch := range f.Unfiltered {
238+
if ch == "{<}" || ch == "{>}" {
239+
if _, ok := seen[f.MatchedAt]; !ok {
240+
seen[f.MatchedAt] = struct{}{}
241+
out = append(out, f.MatchedAt)
242+
}
243+
break
244+
}
245+
}
246+
}
247+
return out
248+
}
249+
250+
// runDalfoxOnURLs writes the candidate URLs to a temp file, runs dalfox (Go package)
251+
// and persists results as 'dalfox-xss-results.json' — a separate dashboard module.
252+
func runDalfoxOnURLs(candidateURLs []string, domain string, threads int) {
253+
if threads <= 0 {
254+
threads = 50
255+
}
256+
// Write URLs to a temp file for dalfoxtool.ScanFile
257+
tmpFile, err := os.CreateTemp("", "dalfox-targets-*.txt")
258+
if err != nil {
259+
logger.GetLogger().Infof("[WARN] dalfox: failed to create temp file: %v", err)
260+
return
261+
}
262+
defer os.Remove(tmpFile.Name())
263+
for _, u := range candidateURLs {
264+
_, _ = fmt.Fprintln(tmpFile, u)
265+
}
266+
tmpFile.Close()
267+
268+
results, err := dalfoxtool.ScanFile(tmpFile.Name(), dalfoxtool.Options{Threads: threads})
269+
if err != nil {
270+
logger.GetLogger().Infof("[WARN] dalfox scan failed: %v", err)
271+
return
272+
}
273+
274+
scanID := utils.GetCurrentScanID()
275+
if scanID == "" {
276+
return
277+
}
278+
279+
if len(results) == 0 {
280+
_ = utils.WriteNoFindingsJSON(scanID, domain, "xss-detection", "dalfox-xss-results.json")
281+
return
282+
}
283+
284+
type dalfoxFinding struct {
285+
TemplateID string `json:"template-id"`
286+
MatchedAt string `json:"matched-at"`
287+
Severity string `json:"severity"`
288+
Type string `json:"type,omitempty"`
289+
Parameter string `json:"parameter,omitempty"`
290+
Payload string `json:"payload,omitempty"`
291+
Module string `json:"module"`
292+
}
293+
294+
seen := make(map[string]struct{})
295+
var dfindings []dalfoxFinding
296+
for _, r := range results {
297+
fType := strings.TrimSpace(r.Type)
298+
if fType == "" {
299+
fType = "xss"
300+
}
301+
sev := strings.TrimSpace(strings.ToLower(r.Severity))
302+
if sev == "" {
303+
sev = "high"
304+
}
305+
label := fmt.Sprintf("XSS (%s)", strings.ToUpper(fType))
306+
key := label + "|" + r.Target + "|" + r.Parameter
307+
if _, ok := seen[key]; ok {
308+
continue
309+
}
310+
seen[key] = struct{}{}
311+
dfindings = append(dfindings, dalfoxFinding{
312+
TemplateID: label,
313+
MatchedAt: r.Target,
314+
Severity: sev,
315+
Type: fType,
316+
Parameter: r.Parameter,
317+
Payload: r.Payload,
318+
Module: "xss-detection",
319+
})
320+
}
321+
322+
if len(dfindings) > 0 {
323+
logger.GetLogger().Infof("[OK] Dalfox confirmed %d XSS finding(s)", len(dfindings))
324+
if err := utils.WriteJSONToScanDir(scanID, "dalfox-xss-results.json", dfindings); err != nil {
325+
logger.GetLogger().Infof("[WARN] Failed to write dalfox-xss JSON: %v", err)
326+
}
327+
} else {
328+
logger.GetLogger().Infof("[INFO] Dalfox found no confirmed XSS from kxss candidates")
329+
_ = utils.WriteNoFindingsJSON(scanID, domain, "xss-detection", "dalfox-xss-results.json")
330+
}
331+
}
332+
215333
// scanURLsWithConcurrency scans URLs with concurrency and context timeout
216334
func scanURLsWithConcurrency(ctx context.Context, urls []string, concurrency int) ([]kxsstool.Result, error) {
217335
if len(urls) == 0 {

internal/scanner/urls/urls.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ func CollectURLs(domain string, threads int, skipSubdomainEnum bool) (*Result, e
151151
existingURLs3, _ := readLines(allFile)
152152
merged := uniqueStrings(append(existingURLs3, kataURLs...))
153153
_ = utils.WriteLines(allFile, merged)
154+
// Persist Katana results as their own dashboard module
155+
if scanID := utils.GetCurrentScanID(); scanID != "" {
156+
_ = utils.WriteLinesAsJSON(scanID, dirDomain, "katana-crawler", "katana-urls.json", kataURLs)
157+
}
154158
}
155159
}
156160

internal/utils/json_output.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,17 @@ func WriteJSONToScanDir(scanID, fileName string, data interface{}) error {
5656

5757
module := ""
5858
lf := strings.ToLower(fileName)
59-
if strings.Contains(lf, "github-secret") || strings.Contains(lf, "trufflehog") {
59+
if strings.Contains(lf, "katana") {
60+
module = "katana-crawler"
61+
} else if strings.Contains(lf, "github-secret") || strings.Contains(lf, "trufflehog") {
6062
module = "github-scan"
61-
} else if strings.Contains(fileName, "url") {
63+
} else if strings.Contains(lf, "js-endpoint") {
64+
module = "js-endpoints"
65+
} else if strings.Contains(lf, "js-secret") || strings.Contains(lf, "js-exposure") {
66+
module = "js-analysis"
67+
} else if strings.Contains(lf, "url") {
6268
module = "url-collection"
63-
} else if strings.Contains(fileName, "subdomain") {
69+
} else if strings.Contains(lf, "subdomain") {
6470
module = "subdomain-enum"
6571
} else if idx := strings.Index(fileName, "-"); idx > 0 {
6672
module = fileName[:idx]
@@ -99,6 +105,7 @@ func WriteJSONToScanDir(scanID, fileName string, data interface{}) error {
99105
// Recon data files — these are pipeline inputs, not vulnerability findings.
100106
isReconFile := strings.Contains(fileName, "js-url") ||
101107
strings.Contains(fileName, "js-urls") ||
108+
strings.Contains(fileName, "katana-urls") ||
102109
strings.Contains(fileName, "all-url") ||
103110
strings.Contains(fileName, "all-subs") ||
104111
strings.Contains(fileName, "live-subs") ||

0 commit comments

Comments
 (0)