Skip to content

Commit e7b8d05

Browse files
feat: add error handling and improve performance in dashboard UI and testing phase
1 parent 74ac61e commit e7b8d05

2 files changed

Lines changed: 103 additions & 51 deletions

File tree

internal/api/dashboard.go

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,23 @@ const dashboardHTML = `<!DOCTYPE html>
491491
}
492492
}
493493
494+
async function refreshLiveEvents() {
495+
try {
496+
const scansRes = await fetch('/api/dashboard/scans');
497+
const scans = await scansRes.json();
498+
const runningScans = scans.filter(s => s.status === 'running');
499+
500+
if (runningScans.length === 0) {
501+
document.getElementById('liveEventsSection').style.display = 'none';
502+
return;
503+
}
504+
505+
await showLiveEvents(scans);
506+
} catch (error) {
507+
console.error('Error refreshing live events:', error);
508+
}
509+
}
510+
494511
async function showLiveEvents(scans) {
495512
const runningScans = scans.filter(s => s.status === 'running');
496513
if (runningScans.length === 0) {
@@ -568,15 +585,23 @@ const dashboardHTML = `<!DOCTYPE html>
568585
async function viewScan(scanId) {
569586
try {
570587
const res = await fetch('/api/dashboard/scans/' + scanId);
588+
if (!res.ok) {
589+
throw new Error('Failed to fetch scan: ' + res.status + ' ' + res.statusText);
590+
}
571591
const data = await res.json();
572592
593+
// Validate response structure
594+
if (!data || !data.scan) {
595+
throw new Error('Invalid scan data received from API');
596+
}
597+
573598
// Fetch scan events/logs
574599
const eventsRes = await fetch('/api/dashboard/scans/' + scanId + '/events');
575-
const events = await eventsRes.json();
600+
const events = eventsRes.ok ? await eventsRes.json() : [];
576601
577602
let html = '<h2>Scan Details</h2>' +
578-
'<p><strong>Target:</strong> ' + escapeHtml(data.scan.target) + '</p>' +
579-
'<p><strong>Status:</strong> <span class="status-badge status-' + data.scan.status + '">' + data.scan.status + '</span></p>' +
603+
'<p><strong>Target:</strong> ' + escapeHtml(data.scan.target || 'Unknown') + '</p>' +
604+
'<p><strong>Status:</strong> <span class="status-badge status-' + (data.scan.status || 'unknown') + '">' + (data.scan.status || 'unknown') + '</span></p>' +
580605
'<p><strong>Started:</strong> ' + formatDate(data.scan.created_at) + '</p>' +
581606
(data.scan.error_message ? '<p class="error"><strong>Error:</strong> ' + escapeHtml(data.scan.error_message) + '</p>' : '');
582607
@@ -597,12 +622,13 @@ const dashboardHTML = `<!DOCTYPE html>
597622
html += '</div>';
598623
}
599624
600-
html += '<h3 style="margin-top: 30px;">Findings (' + data.findings.length + ')</h3>';
625+
const findings = data.findings || [];
626+
html += '<h3 style="margin-top: 30px;">Findings (' + findings.length + ')</h3>';
601627
602-
if (data.findings.length === 0) {
628+
if (findings.length === 0) {
603629
html += '<p style="color: #6b7280;">No findings for this scan.</p>';
604630
} else {
605-
data.findings.forEach(f => {
631+
findings.forEach(f => {
606632
html += '<div class="finding-card ' + f.severity.toLowerCase() + '">' +
607633
'<div class="finding-title">' + escapeHtml(f.title) + '</div>' +
608634
'<div class="finding-meta">' +
@@ -649,9 +675,14 @@ const dashboardHTML = `<!DOCTYPE html>
649675
return div.innerHTML;
650676
}
651677
652-
// Auto-refresh every 5 seconds
678+
// Load initial data
653679
loadData();
654-
setInterval(loadData, 5000);
680+
681+
// Auto-refresh only live events every 5 seconds (not the whole table)
682+
setInterval(refreshLiveEvents, 5000);
683+
684+
// Refresh full table every 30 seconds (less disruptive)
685+
setInterval(loadData, 30000);
655686
656687
// Close modal on outside click
657688
window.onclick = function(event) {

internal/orchestrator/bounty_engine.go

Lines changed: 64 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,7 @@ func (e *BugBountyEngine) Execute(ctx context.Context, target string) (*BugBount
617617
}
618618

619619
tracker.StartPhase("testing")
620-
findings, phaseResults := e.executeTestingPhase(ctx, target, prioritized, tracker)
620+
findings, phaseResults := e.executeTestingPhase(ctx, target, prioritized, tracker, dbLogger)
621621

622622
testingDuration := time.Since(testingStart)
623623
dbLogger.Infow(" Testing phase completed",
@@ -1069,8 +1069,11 @@ func (e *BugBountyEngine) analyzeAssetFeatures(asset *discovery.Asset) AssetFeat
10691069
}
10701070

10711071
// executeTestingPhase runs all vulnerability tests in parallel
1072-
func (e *BugBountyEngine) executeTestingPhase(ctx context.Context, target string, assets []*AssetPriority, tracker *progress.Tracker) ([]types.Finding, map[string]PhaseResult) {
1073-
e.logger.Infow("Phase 3: Vulnerability Testing", "assets", len(assets))
1072+
func (e *BugBountyEngine) executeTestingPhase(ctx context.Context, target string, assets []*AssetPriority, tracker *progress.Tracker, dbLogger *logger.DBEventLogger) ([]types.Finding, map[string]PhaseResult) {
1073+
dbLogger.Infow(" Phase 3: Starting vulnerability testing",
1074+
"assets", len(assets),
1075+
"component", "orchestrator",
1076+
)
10741077

10751078
phaseResults := make(map[string]PhaseResult)
10761079
allFindings := []types.Finding{}
@@ -1117,7 +1120,7 @@ func (e *BugBountyEngine) executeTestingPhase(ctx context.Context, target string
11171120
wg.Add(1)
11181121
go func() {
11191122
defer wg.Done()
1120-
findings, result := e.runAuthenticationTests(ctx, target, assets)
1123+
findings, result := e.runAuthenticationTests(ctx, target, assets, dbLogger)
11211124
mu.Lock()
11221125
allFindings = append(allFindings, findings...)
11231126
phaseResults["auth"] = result
@@ -1131,7 +1134,7 @@ func (e *BugBountyEngine) executeTestingPhase(ctx context.Context, target string
11311134
wg.Add(1)
11321135
go func() {
11331136
defer wg.Done()
1134-
findings, result := e.runSCIMTests(ctx, assets)
1137+
findings, result := e.runSCIMTests(ctx, assets, dbLogger)
11351138
mu.Lock()
11361139
allFindings = append(allFindings, findings...)
11371140
phaseResults["scim"] = result
@@ -1145,7 +1148,7 @@ func (e *BugBountyEngine) executeTestingPhase(ctx context.Context, target string
11451148
wg.Add(1)
11461149
go func() {
11471150
defer wg.Done()
1148-
findings, result := e.runAPITests(ctx, assets)
1151+
findings, result := e.runAPITests(ctx, assets, dbLogger)
11491152
mu.Lock()
11501153
allFindings = append(allFindings, findings...)
11511154
phaseResults["api"] = result
@@ -1159,7 +1162,7 @@ func (e *BugBountyEngine) executeTestingPhase(ctx context.Context, target string
11591162
wg.Add(1)
11601163
go func() {
11611164
defer wg.Done()
1162-
findings, result := e.runNmapScans(ctx, assets)
1165+
findings, result := e.runNmapScans(ctx, assets, dbLogger)
11631166
mu.Lock()
11641167
allFindings = append(allFindings, findings...)
11651168
phaseResults["nmap"] = result
@@ -1173,7 +1176,7 @@ func (e *BugBountyEngine) executeTestingPhase(ctx context.Context, target string
11731176
wg.Add(1)
11741177
go func() {
11751178
defer wg.Done()
1176-
findings, result := e.runNucleiScans(ctx, assets)
1179+
findings, result := e.runNucleiScans(ctx, assets, dbLogger)
11771180
mu.Lock()
11781181
allFindings = append(allFindings, findings...)
11791182
phaseResults["nuclei"] = result
@@ -1187,7 +1190,7 @@ func (e *BugBountyEngine) executeTestingPhase(ctx context.Context, target string
11871190
wg.Add(1)
11881191
go func() {
11891192
defer wg.Done()
1190-
findings, result := e.runGraphQLTests(ctx, assets)
1193+
findings, result := e.runGraphQLTests(ctx, assets, dbLogger)
11911194
mu.Lock()
11921195
allFindings = append(allFindings, findings...)
11931196
phaseResults["graphql"] = result
@@ -1201,7 +1204,7 @@ func (e *BugBountyEngine) executeTestingPhase(ctx context.Context, target string
12011204
wg.Add(1)
12021205
go func() {
12031206
defer wg.Done()
1204-
findings, result := e.runIDORTests(ctx, assets)
1207+
findings, result := e.runIDORTests(ctx, assets, dbLogger)
12051208
mu.Lock()
12061209
allFindings = append(allFindings, findings...)
12071210
phaseResults["idor"] = result
@@ -1217,7 +1220,7 @@ func (e *BugBountyEngine) executeTestingPhase(ctx context.Context, target string
12171220
}
12181221

12191222
// runAuthenticationTests executes all authentication vulnerability tests
1220-
func (e *BugBountyEngine) runAuthenticationTests(ctx context.Context, target string, assets []*AssetPriority) ([]types.Finding, PhaseResult) {
1223+
func (e *BugBountyEngine) runAuthenticationTests(ctx context.Context, target string, assets []*AssetPriority, dbLogger *logger.DBEventLogger) ([]types.Finding, PhaseResult) {
12211224
phaseStart := time.Now()
12221225
phase := PhaseResult{
12231226
Phase: "authentication",
@@ -1447,7 +1450,7 @@ func (e *BugBountyEngine) runAuthenticationTests(ctx context.Context, target str
14471450
}
14481451

14491452
// runSCIMTests executes SCIM vulnerability tests in parallel
1450-
func (e *BugBountyEngine) runSCIMTests(ctx context.Context, assets []*AssetPriority) ([]types.Finding, PhaseResult) {
1453+
func (e *BugBountyEngine) runSCIMTests(ctx context.Context, assets []*AssetPriority, dbLogger *logger.DBEventLogger) ([]types.Finding, PhaseResult) {
14511454
phaseStart := time.Now()
14521455
phase := PhaseResult{
14531456
Phase: "scim",
@@ -1543,7 +1546,7 @@ func (e *BugBountyEngine) runSCIMTests(ctx context.Context, assets []*AssetPrior
15431546
}
15441547

15451548
// runAPITests executes API vulnerability tests
1546-
func (e *BugBountyEngine) runAPITests(ctx context.Context, assets []*AssetPriority) ([]types.Finding, PhaseResult) {
1549+
func (e *BugBountyEngine) runAPITests(ctx context.Context, assets []*AssetPriority, dbLogger *logger.DBEventLogger) ([]types.Finding, PhaseResult) {
15471550
phase := PhaseResult{
15481551
Phase: "api",
15491552
Status: "running",
@@ -1595,7 +1598,7 @@ func (e *BugBountyEngine) runAPITests(ctx context.Context, assets []*AssetPriori
15951598
}
15961599

15971600
// runNmapScans performs service fingerprinting and port scanning
1598-
func (e *BugBountyEngine) runNmapScans(ctx context.Context, assets []*AssetPriority) ([]types.Finding, PhaseResult) {
1601+
func (e *BugBountyEngine) runNmapScans(ctx context.Context, assets []*AssetPriority, dbLogger *logger.DBEventLogger) ([]types.Finding, PhaseResult) {
15991602
phaseStart := time.Now()
16001603
phase := PhaseResult{
16011604
Phase: "nmap",
@@ -1744,7 +1747,7 @@ func (e *BugBountyEngine) runNmapScans(ctx context.Context, assets []*AssetPrior
17441747
}
17451748

17461749
// runNucleiScans performs vulnerability scanning with Nuclei templates
1747-
func (e *BugBountyEngine) runNucleiScans(ctx context.Context, assets []*AssetPriority) ([]types.Finding, PhaseResult) {
1750+
func (e *BugBountyEngine) runNucleiScans(ctx context.Context, assets []*AssetPriority, dbLogger *logger.DBEventLogger) ([]types.Finding, PhaseResult) {
17481751
phaseStart := time.Now()
17491752
phase := PhaseResult{
17501753
Phase: "nuclei",
@@ -1874,39 +1877,56 @@ func (e *BugBountyEngine) runNucleiScans(ctx context.Context, assets []*AssetPri
18741877
}
18751878

18761879
// runGraphQLTests performs GraphQL security testing
1877-
func (e *BugBountyEngine) runGraphQLTests(ctx context.Context, assets []*AssetPriority) ([]types.Finding, PhaseResult) {
1880+
func (e *BugBountyEngine) runGraphQLTests(ctx context.Context, assets []*AssetPriority, dbLogger *logger.DBEventLogger) ([]types.Finding, PhaseResult) {
18781881
phase := PhaseResult{
18791882
Phase: "graphql",
18801883
Status: "running",
18811884
StartTime: time.Now(),
18821885
}
18831886

1884-
e.logger.Infow("Running GraphQL security tests")
1887+
dbLogger.Infow(" Running GraphQL security tests",
1888+
"component", "graphql_scanner",
1889+
)
18851890

18861891
var findings []types.Finding
18871892

1888-
// Find GraphQL endpoints
1889-
graphqlCount := 0
1893+
// Extract unique base URLs from assets for GraphQL testing
1894+
// The GraphQL scanner will test common paths like /graphql, /gql, /api/graphql, etc.
1895+
baseURLs := make(map[string]bool)
18901896
for _, asset := range assets {
1891-
url := asset.Asset.Value
1892-
// Check if URL likely contains GraphQL endpoint
1893-
if !strings.Contains(strings.ToLower(url), "graphql") &&
1894-
!strings.Contains(strings.ToLower(url), "/api") {
1895-
continue
1897+
// Parse URL to extract base (scheme + host)
1898+
if strings.HasPrefix(asset.Asset.Value, "http://") || strings.HasPrefix(asset.Asset.Value, "https://") {
1899+
// Find the base URL (up to the first path separator after the host)
1900+
parts := strings.SplitN(asset.Asset.Value, "/", 4) // ["https:", "", "example.com", "path..."]
1901+
if len(parts) >= 3 {
1902+
baseURL := parts[0] + "//" + parts[2] // "https://example.com"
1903+
baseURLs[baseURL] = true
1904+
}
18961905
}
1906+
}
1907+
1908+
dbLogger.Infow(" Extracted base URLs for GraphQL testing",
1909+
"total_assets", len(assets),
1910+
"unique_base_urls", len(baseURLs),
1911+
"component", "graphql_scanner",
1912+
)
18971913

1914+
// Test each base URL - GraphQL scanner will discover endpoints automatically
1915+
graphqlCount := 0
1916+
for baseURL := range baseURLs {
18981917
graphqlCount++
1899-
e.logger.Debugw("Testing GraphQL endpoint",
1900-
"url", url,
1918+
dbLogger.Infow(" Testing base URL for GraphQL endpoints",
1919+
"url", baseURL,
1920+
"testing_count", fmt.Sprintf("%d/%d", graphqlCount, len(baseURLs)),
19011921
"component", "graphql_scanner",
19021922
)
19031923

1904-
// Run Go GraphQL scanner
1905-
results, err := e.graphqlScanner.Scan(ctx, url, nil)
1924+
// Run Go GraphQL scanner - it will test multiple common GraphQL paths
1925+
results, err := e.graphqlScanner.Scan(ctx, baseURL, nil)
19061926
if err != nil {
1907-
e.logger.Errorw("GraphQL scan failed",
1927+
dbLogger.Errorw(" GraphQL scan failed",
19081928
"error", err,
1909-
"url", url,
1929+
"url", baseURL,
19101930
"component", "graphql_scanner",
19111931
)
19121932
continue
@@ -1915,33 +1935,33 @@ func (e *BugBountyEngine) runGraphQLTests(ctx context.Context, assets []*AssetPr
19151935
// Convert results to findings
19161936
findings = append(findings, results...)
19171937

1918-
e.logger.Infow("Go GraphQL scan completed",
1919-
"url", url,
1938+
dbLogger.Infow(" Go GraphQL scan completed",
1939+
"url", baseURL,
19201940
"findings", len(results),
19211941
"component", "graphql_scanner",
19221942
)
19231943

19241944
// Also run Python GraphCrawler if available
19251945
if e.pythonWorkers != nil {
1926-
e.logger.Infow("Running Python GraphCrawler",
1927-
"url", url,
1946+
dbLogger.Infow(" Running Python GraphCrawler",
1947+
"url", baseURL,
19281948
"component", "graphcrawler",
19291949
)
19301950

1931-
jobStatus, err := e.pythonWorkers.ScanGraphQLSync(ctx, url, nil)
1951+
jobStatus, err := e.pythonWorkers.ScanGraphQLSync(ctx, baseURL, nil)
19321952
if err != nil {
1933-
e.logger.Errorw("GraphCrawler scan failed",
1953+
dbLogger.Errorw(" GraphCrawler scan failed",
19341954
"error", err,
1935-
"url", url,
1955+
"url", baseURL,
19361956
"component", "graphcrawler",
19371957
)
19381958
} else if jobStatus.Status == "completed" && jobStatus.Result != nil {
19391959
// Convert Python worker results to findings
1940-
pythonFindings := convertPythonGraphQLToFindings(jobStatus.Result, url)
1960+
pythonFindings := convertPythonGraphQLToFindings(jobStatus.Result, baseURL)
19411961
findings = append(findings, pythonFindings...)
19421962

1943-
e.logger.Infow("GraphCrawler scan completed",
1944-
"url", url,
1963+
dbLogger.Infow(" GraphCrawler scan completed",
1964+
"url", baseURL,
19451965
"findings", len(pythonFindings),
19461966
"component", "graphcrawler",
19471967
)
@@ -1954,17 +1974,18 @@ func (e *BugBountyEngine) runGraphQLTests(ctx context.Context, assets []*AssetPr
19541974
phase.Status = "completed"
19551975
phase.Findings = len(findings)
19561976

1957-
e.logger.Infow("GraphQL testing completed",
1977+
dbLogger.Infow(" GraphQL testing completed",
19581978
"findings", len(findings),
1959-
"duration", phase.Duration,
1979+
"duration", phase.Duration.String(),
19601980
"endpoints_tested", graphqlCount,
1981+
"component", "graphql_scanner",
19611982
)
19621983

19631984
return findings, phase
19641985
}
19651986

19661987
// runIDORTests performs IDOR vulnerability testing using Python workers
1967-
func (e *BugBountyEngine) runIDORTests(ctx context.Context, assets []*AssetPriority) ([]types.Finding, PhaseResult) {
1988+
func (e *BugBountyEngine) runIDORTests(ctx context.Context, assets []*AssetPriority, dbLogger *logger.DBEventLogger) ([]types.Finding, PhaseResult) {
19681989
phase := PhaseResult{
19691990
Phase: "idor",
19701991
Status: "running",

0 commit comments

Comments
 (0)