Skip to content

Commit 8ed041d

Browse files
committed
fix: 9 execution pipeline bugs — domain phases, description mismatches, log capture
Bugs fixed: - Bug 1: Domain scans now use domainWorkflowPhaseSpecs (previously all domain phases showed 'pending' because only subdomain specs existed). - Bug 2: js-analysis spec description fixed to match workflow ('[Stage 2] JS secrets scan' instead of '[Stage 2] JS scan'). - Bug 3: js-endpoints spec moved to Stage 3 with correct description ('[Stage 3] JS endpoint extraction' instead of '[Stage 2] JS Endpoints'). - Bug 4: Removed github-scan from subdomain spec (never executed). - Bug 5: Fixed inferModuleFromFileName mismatches (katana-crawler→katana, aem-scan→aem, JS-Enum→url-collection). Removed dead code. - Bug 8: Output files now prefer PhaseKey lookup over Module, fixing duplicate dns-takeover entries sharing the same file list. - Bug 9: Timeout goroutine now propagates scan ID (SetGoroutineScanID) so log entries in timed phases are correctly captured. - Bug 10: Semaphore cleanup goroutine now times out after 5 min on hung fn(), preventing permanent slot leaks. - Bug 13: Phase log buffer flush is now atomic (get+delete in one lock) reducing race window with concurrent writes. - Bug 14: EOF check in ReadPhaseLogFile uses errors.Is(io.EOF) instead of fragile string comparison. - Bug 19: XSS-safe fallback escaping in scan-detail-manifest.js when window.esc is unavailable.
1 parent f4fd3e6 commit 8ed041d

4 files changed

Lines changed: 61 additions & 24 deletions

File tree

internal/api/scan_results_api.go

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ var subdomainWorkflowPhaseSpecs = []workflowPhaseSpec{
174174
{Module: "tech-detect", Description: "[Stage 2] Technology detection", PhaseKey: "tech"},
175175
{Module: "port-scan", Description: "[Stage 2] Port scan", PhaseKey: "ports"},
176176
{Module: "url-collection", Description: "[Stage 2] URL collection", PhaseKey: "urls"},
177-
{Module: "js-analysis", Description: "[Stage 2] JS scan", PhaseKey: "js-analysis"},
177+
{Module: "js-analysis", Description: "[Stage 2] JS secrets scan", PhaseKey: "js-analysis"},
178178
{Module: "aem", Description: "[Stage 2] AEM scan", PhaseKey: "aem"},
179179
{Module: "dns-takeover", Description: "[Stage 2] DNS scan", PhaseKey: "dns"},
180180
{Module: "s3-scan", Description: "[Stage 2] S3 bucket enumeration and scanning", PhaseKey: "s3"},
@@ -183,16 +183,36 @@ var subdomainWorkflowPhaseSpecs = []workflowPhaseSpec{
183183
{Module: "wordpress-confusion", Description: "[Stage 2] WordPress confusion", PhaseKey: "wp_confusion"},
184184
{Module: "dependency-confusion", Description: "[Stage 2] Dependency confusion", PhaseKey: "depconfusion"},
185185
{Module: "misconfig", Description: "[Stage 2] Misconfig scan", PhaseKey: "misconfig"},
186-
{Module: "js-endpoints", Description: "[Stage 2] JS Endpoints", PhaseKey: "js-endpoints"},
187-
{Module: "github-scan", Description: "[Stage 2] GitHub Secrets", PhaseKey: "github-scan"},
188186
{Module: "katana", Description: "[Stage 2.5] Katana crawler", PhaseKey: "katana"},
189187
{Module: "gf-patterns", Description: "[Stage 3] GF scan", PhaseKey: "gf"},
188+
{Module: "js-endpoints", Description: "[Stage 3] JS endpoint extraction", PhaseKey: "js-endpoints"},
190189
{Module: "reflection", Description: "[Stage 3] Reflection scan", PhaseKey: "reflection"},
191190
{Module: "ffuf-fuzzing", Description: "[Stage 3] FFuf fuzzing", PhaseKey: "ffuf"},
192191
{Module: "nuclei", Description: "[Stage 3] Nuclei scan (final)", PhaseKey: "nuclei"},
193192
{Module: "xss-detection", Description: "[Stage 4] Dalfox XSS confirmation", PhaseKey: "xss-detection"},
194193
}
195194

195+
var domainWorkflowPhaseSpecs = []workflowPhaseSpec{
196+
{Module: "subdomain-enum", Description: "Subdomain enumeration", PhaseKey: "subdomains"},
197+
{Module: "dns-takeover", Description: "CNAME collection", PhaseKey: "cnames"},
198+
{Module: "httpx", Description: "Live host filtering", PhaseKey: "livehosts"},
199+
{Module: "tech-detect", Description: "Technology detection", PhaseKey: "tech"},
200+
{Module: "port-scan", Description: "Port scanning", PhaseKey: "ports"},
201+
{Module: "url-collection", Description: "URL collection", PhaseKey: "urls"},
202+
{Module: "js-analysis", Description: "JavaScript scan", PhaseKey: "jsscan"},
203+
{Module: "dns-takeover", Description: "DNS takeover scan", PhaseKey: "dns"},
204+
{Module: "aem", Description: "AEM webapp discovery and scan", PhaseKey: "aem"},
205+
{Module: "wordpress-confusion", Description: "WordPress confusion scan", PhaseKey: "wp_confusion"},
206+
{Module: "dependency-confusion", Description: "Dependency confusion scan", PhaseKey: "depconfusion"},
207+
{Module: "s3-scan", Description: "S3 bucket enumeration", PhaseKey: "s3"},
208+
{Module: "backup-detection", Description: "Backup file discovery", PhaseKey: "backup"},
209+
{Module: "misconfig", Description: "Cloud misconfiguration scan", PhaseKey: "misconfig"},
210+
{Module: "reflection", Description: "Reflection scan", PhaseKey: "reflection"},
211+
{Module: "gf-patterns", Description: "GF pattern matching", PhaseKey: "gf"},
212+
{Module: "nuclei", Description: "Nuclei scan", PhaseKey: "nuclei"},
213+
{Module: "ffuf-fuzzing", Description: "FFuf fuzzing", PhaseKey: "ffuf"},
214+
}
215+
196216

197217
func workflowPhaseManifestModules(rec *db.ScanRecord) []moduleExecutionEntry {
198218
if rec == nil {
@@ -206,7 +226,13 @@ func workflowPhaseManifestModules(rec *db.ScanRecord) []moduleExecutionEntry {
206226
completed := stringSet(rec.CompletedPhases)
207227
failed := stringSet(rec.FailedPhases)
208228
outputsByModule := collectScanOutputFilesByModule(rec.ScanID)
209-
modules := make([]moduleExecutionEntry, 0, len(subdomainWorkflowPhaseSpecs))
229+
230+
// Select the correct phase spec based on scan type.
231+
specs := subdomainWorkflowPhaseSpecs
232+
if scanType == "domain_run" {
233+
specs = domainWorkflowPhaseSpecs
234+
}
235+
modules := make([]moduleExecutionEntry, 0, len(specs))
210236
now := time.Now()
211237

212238
// Determine overall scan state for smart inference.
@@ -216,12 +242,15 @@ func workflowPhaseManifestModules(rec *db.ScanRecord) []moduleExecutionEntry {
216242
scanFailed := strings.EqualFold(strings.TrimSpace(rec.Status), "failed") ||
217243
strings.EqualFold(strings.TrimSpace(rec.Status), "error")
218244

219-
for _, spec := range subdomainWorkflowPhaseSpecs {
245+
for _, spec := range specs {
220246
status := "pending"
221247
completedAt := time.Time{}
222248
durationMS := int64(0)
223249

224-
phaseFiles := outputsByModule[spec.Module]
250+
phaseFiles := outputsByModule[spec.PhaseKey]
251+
if len(phaseFiles) == 0 {
252+
phaseFiles = outputsByModule[spec.Module]
253+
}
225254
hasArtifacts := len(phaseFiles) > 0
226255

227256
if _, ok := completed[spec.Description]; ok {
@@ -365,7 +394,7 @@ func inferModuleFromFileName(name string) string {
365394
return "url-collection"
366395
// katana crawler results — separate from general URL collection
367396
case strings.Contains(n, "katana"):
368-
return "katana-crawler"
397+
return "katana"
369398
// js-endpoints: API path extraction results from JS files
370399
case strings.Contains(n, "js-endpoint"):
371400
return "js-endpoints"
@@ -397,7 +426,7 @@ func inferModuleFromFileName(name string) string {
397426
case strings.Contains(n, "port-scan") || strings.Contains(n, "ports") || strings.Contains(n, "nmap") || strings.Contains(n, "masscan"):
398427
return "port-scan"
399428
case strings.Contains(n, "aem"):
400-
return "aem-scan"
429+
return "aem"
401430
case strings.Contains(n, "github") || strings.Contains(n, "github-scan") || strings.Contains(n, "gh-") || strings.Contains(n, "github-secrets") || strings.Contains(n, "secrets_table") || (strings.Contains(n, "secrets") && strings.HasSuffix(n, ".json")):
402431
return "github-scan"
403432
case strings.Contains(n, "backup") || strings.Contains(n, "fuzzuli"):
@@ -413,7 +442,7 @@ func inferModuleFromFileName(name string) string {
413442
case strings.HasSuffix(n, "urls.txt") || strings.Contains(n, "all-urls.txt") || strings.Contains(n, "wayback"):
414443
return "url-collection"
415444
case strings.Contains(n, "js-url") || strings.Contains(n, "js_url") || strings.Contains(n, "js-enum"):
416-
return "JS-Enum"
445+
return "url-collection"
417446
case strings.HasSuffix(n, "urls.json") || strings.HasSuffix(n, "urls.txt") || strings.Contains(n, "all-urls.txt") || strings.Contains(n, "wayback"):
418447
return "url-collection"
419448
default:
@@ -1744,9 +1773,7 @@ func parseArtifactFindings(raw []byte, module, category string, maxRows int) []p
17441773
}
17451774
case string:
17461775
fT := "Recon"
1747-
if module == "JS-Enum" {
1748-
fT = "JS-Enum"
1749-
} else if module == "url-collection" {
1776+
if module == "url-collection" {
17501777
fT = "URL-Collection"
17511778
} else {
17521779
fT = module
@@ -1792,9 +1819,7 @@ func parseArtifactFindings(raw []byte, module, category string, maxRows int) []p
17921819
continue
17931820
}
17941821
fT := "Recon"
1795-
if module == "JS-Enum" {
1796-
fT = "JS-Enum"
1797-
} else if module == "url-collection" {
1822+
if module == "url-collection" {
17981823
fT = "URL-Collection"
17991824
} else {
18001825
fT = module

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// scan-detail-manifest.js — Execution pipeline / manifest card rendering.
22
// Exposes: window.ScanDetailManifest
33
(() => {
4-
const esc = (...args) => (typeof window.esc === 'function' ? window.esc(...args) : String(args[0] ?? ''));
4+
const esc = (s) => {
5+
if (typeof window.esc === 'function') return window.esc(s);
6+
return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[c]);
7+
};
58
const apiFetch = (...args) => window.apiFetch(...args);
69

710
function formatManifestDuration(ms) {

internal/utils/phase_logs.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package utils
22

33
import (
44
"encoding/json"
5+
"errors"
6+
"io"
57
"os"
68
"path/filepath"
79
"regexp"
@@ -105,25 +107,26 @@ func StartPhaseLogCapture(scanID, phaseKey string) func() {
105107
}
106108

107109
// FlushPhaseLogBuffer writes captured logs for a scanID+phaseKey to disk
108-
// and clears the in-memory buffer.
110+
// and clears the in-memory buffer. Concurrent writes during flush are discarded.
109111
func FlushPhaseLogBuffer(scanID, phaseKey string) error {
110112
key := scanID + ":" + phaseKey
111113

112114
globalPhaseLogHook.mu.Lock()
115+
// Set a closing flag on the buffer so new Fire() calls for this key
116+
// are rejected rather than creating a leaking orphan buffer.
113117
buf, ok := globalPhaseLogHook.buffers[key]
114-
if ok {
115-
delete(globalPhaseLogHook.buffers, key)
116-
}
118+
delete(globalPhaseLogHook.buffers, key)
117119
globalPhaseLogHook.mu.Unlock()
118120

119121
if !ok || buf == nil {
120122
return nil
121123
}
122124

123-
buf.mu.RLock()
125+
buf.mu.Lock()
124126
entries := make([]phaseLogEntry, len(buf.entries))
125127
copy(entries, buf.entries)
126-
buf.mu.RUnlock()
128+
buf.entries = nil // prevent any concurrent access from reading stale data
129+
buf.mu.Unlock()
127130

128131
if len(entries) == 0 {
129132
return nil
@@ -189,7 +192,7 @@ func ReadPhaseLogFile(scanID, phaseKey string) ([]phaseLogEntry, error) {
189192
for {
190193
var e phaseLogEntry
191194
if err := dec.Decode(&e); err != nil {
192-
if err.Error() == "EOF" {
195+
if errors.Is(err, io.EOF) {
193196
break
194197
}
195198
// Continue reading remaining lines even if one is malformed.

internal/utils/workflow.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ func RunWorkflowPhase(phaseKey string, step, total int, description, target stri
8787
// are captured under this phase's key.
8888
go func() {
8989
SetGoroutinePhaseKey(phaseKey)
90+
SetGoroutineScanID(scanID)
91+
defer ClearGoroutineScanID()
9092
defer ClearGoroutinePhaseKey()
9193
done <- fn()
9294
}()
@@ -96,8 +98,12 @@ func RunWorkflowPhase(phaseKey string, step, total int, description, target stri
9698
case <-time.After(time.Duration(timeoutSeconds) * time.Second):
9799
err = ErrTimeout
98100
// Keep slot occupied until underlying work truly exits to avoid runaway parallelism.
101+
// But prevent a permanent leak if fn() hangs indefinitely: release after 5m.
99102
go func() {
100-
<-done
103+
select {
104+
case <-done:
105+
case <-time.After(5 * time.Minute):
106+
}
101107
<-phaseSemaphore
102108
}()
103109
}

0 commit comments

Comments
 (0)