Skip to content

Commit 7c3d919

Browse files
committed
update
1 parent 1b05449 commit 7c3d919

16 files changed

Lines changed: 1733 additions & 250 deletions

File tree

internal/modules/dalfox/dalfox.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func RunDalfox(domain string, threads int) (*Result, error) {
3535
resultsDir := utils.GetResultsDir()
3636
domainDir := filepath.Join(resultsDir, domain)
3737
urlsFile := filepath.Join(domainDir, "urls", "all-urls.txt")
38-
inFile := filepath.Join(domainDir, "vulnerabilities", "xss", "gf-results.txt")
38+
inFile := filepath.Join(domainDir, "vulnerabilities", "xss", gf.ResultFileForPattern("xss"))
3939
outFile := filepath.Join(domainDir, "dalfox-results.txt")
4040

4141
if err := utils.EnsureDir(filepath.Dir(outFile)); err != nil {

internal/modules/db/db.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,16 @@ func DeleteScan(scanID string) error {
482482
return dbInstance.DeleteScan(scanID)
483483
}
484484

485+
// CountScansWithTargetExcluding counts scans sharing a target except the given scan_id.
486+
func CountScansWithTargetExcluding(excludeScanID, target string) (int, error) {
487+
if dbInstance == nil {
488+
if err := Init(); err != nil {
489+
return 0, err
490+
}
491+
}
492+
return dbInstance.CountScansWithTargetExcluding(excludeScanID, target)
493+
}
494+
485495
// Close closes the database connection pool
486496
func Close() {
487497
if dbInstance != nil {

internal/modules/db/postgres.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1543,6 +1543,19 @@ func (p *PostgresDB) DeleteScan(scanID string) error {
15431543
return nil
15441544
}
15451545

1546+
// CountScansWithTargetExcluding returns how many scans share this target besides excludeScanID.
1547+
func (p *PostgresDB) CountScansWithTargetExcluding(excludeScanID, target string) (int, error) {
1548+
var n int
1549+
err := p.pool.QueryRow(p.ctx,
1550+
`SELECT COUNT(*)::int FROM scans WHERE target = $1 AND scan_id != $2`,
1551+
target, excludeScanID,
1552+
).Scan(&n)
1553+
if err != nil {
1554+
return 0, fmt.Errorf("count scans by target: %v", err)
1555+
}
1556+
return n, nil
1557+
}
1558+
15461559
// ListVulnerableDNSProviders returns all vulnerable DNS providers from the database
15471560
func (p *PostgresDB) ListVulnerableDNSProviders() (map[string]string, error) {
15481561
rows, err := p.pool.Query(p.ctx, `

internal/modules/db/sqlite.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,6 +1486,19 @@ func (s *SQLiteDB) DeleteScan(scanID string) error {
14861486
return nil
14871487
}
14881488

1489+
// CountScansWithTargetExcluding returns how many scans share this target besides excludeScanID.
1490+
func (s *SQLiteDB) CountScansWithTargetExcluding(excludeScanID, target string) (int, error) {
1491+
var n int
1492+
err := s.db.QueryRow(
1493+
`SELECT COUNT(*) FROM scans WHERE target = ? AND scan_id != ?;`,
1494+
target, excludeScanID,
1495+
).Scan(&n)
1496+
if err != nil {
1497+
return 0, fmt.Errorf("count scans by target: %v", err)
1498+
}
1499+
return n, nil
1500+
}
1501+
14891502
// ListVulnerableDNSProviders returns all vulnerable DNS providers from the database for SQLite
14901503
func (s *SQLiteDB) ListVulnerableDNSProviders() (map[string]string, error) {
14911504
rows, err := s.db.Query(`

internal/modules/db/types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ type DB interface {
109109
// FailStaleActiveScans marks running/starting/paused/cancelling scans as failed (e.g. after API restart).
110110
FailStaleActiveScans() (int64, error)
111111
DeleteScan(scanID string) error
112+
// CountScansWithTargetExcluding counts rows with the same target excluding one scan_id.
113+
CountScansWithTargetExcluding(excludeScanID, target string) (int, error)
112114
// ListAllScanIDs returns every scan_id in the scans table (newest first).
113115
ListAllScanIDs() ([]string, error)
114116
// ListScanIDsForDomainRoot returns scan IDs whose target is the root domain or a host under it (sub.example.com for example.com).

internal/modules/gf/gf.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ type Result struct {
1919
ResultFiles []string
2020
}
2121

22+
// ResultFileForPattern returns the GF output filename for a pattern directory
23+
// (e.g. img-traversal → gf-img-traversal-results.txt).
24+
func ResultFileForPattern(pattern string) string {
25+
return fmt.Sprintf("gf-%s-results.txt", pattern)
26+
}
27+
2228
// Options for GF scan
2329
type Options struct {
2430
Domain string // Domain name (for directory structure)
@@ -92,7 +98,7 @@ func ScanGFWithOptions(opts Options) (*Result, error) {
9298
continue
9399
}
94100

95-
outFile := filepath.Join(outDir, "gf-results.txt")
101+
outFile := filepath.Join(outDir, ResultFileForPattern(pattern))
96102
if err := runGFPattern(urlsFile, pattern, outFile); err != nil {
97103
log.Printf("[WARN] GF pattern %s failed: %v", pattern, err)
98104
continue

internal/modules/gobot/api.go

Lines changed: 124 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,6 @@ type ScanStatusResponse struct {
124124
Error *string `json:"error,omitempty"`
125125
}
126126

127-
type ScanListResponse struct {
128-
ActiveScans []ScanInfo `json:"active_scans"`
129-
CompletedScans []ScanInfo `json:"completed_scans"`
130-
}
131-
132127
// reconcileStaleScansOnStartup marks DB scans that were still "running" as failed. In-memory
133128
// workers are gone after restart; leaving them active confuses the dashboard.
134129
func reconcileStaleScansOnStartup() {
@@ -170,6 +165,9 @@ func setupAPI() *gin.Engine {
170165
// Both /ui and /ui/* use the same handler — no redirect loops.
171166
r.GET("/ui", serveDashboardUI)
172167
r.GET("/ui/*filepath", serveDashboardUI)
168+
// Deep link: /scans/:scanId (same SPA; client router reads pathname)
169+
r.GET("/scans", serveDashboardUI)
170+
r.GET("/scans/*filepath", serveDashboardUI)
173171

174172
// Public: SPA reads this before login (no JWT).
175173
r.GET("/api/config", apiConfigHandler)
@@ -183,7 +181,11 @@ func setupAPI() *gin.Engine {
183181
apiGroup.DELETE("/domains/:domain", apiDeleteDomain)
184182
apiGroup.GET("/domains/:domain/subdomains", apiListSubdomains)
185183
apiGroup.GET("/scans", apiListScans)
184+
apiGroup.GET("/scans/:id/results/summary", apiScanResultsSummary)
185+
apiGroup.GET("/scans/:id/results/files", apiScanResultFiles)
186+
apiGroup.GET("/scans/:id/results/file", apiScanResultFileContent)
186187
apiGroup.GET("/scans/:id/artifacts", apiListScanArtifacts)
188+
apiGroup.GET("/scans/:id", apiGetScan)
187189
apiGroup.POST("/scans/bulk-delete", apiBulkDeleteScans)
188190
apiGroup.POST("/scans/clear-all", apiClearAllScans)
189191
apiGroup.DELETE("/scans/:id", apiDeleteScan)
@@ -204,6 +206,7 @@ func setupAPI() *gin.Engine {
204206
apiGroup.POST("/monitor/subdomain-targets/:id/pause", apiPauseMonitorSubdomainTarget)
205207
apiGroup.POST("/monitor/subdomain-targets/:id/resume", apiResumeMonitorSubdomainTarget)
206208
apiGroup.GET("/r2/files", apiR2Files)
209+
apiGroup.POST("/r2/delete", apiR2Delete)
207210
}
208211

209212
// Scan endpoints
@@ -255,9 +258,6 @@ func setupAPI() *gin.Engine {
255258
internal.POST("/send-message", sendMessageToDiscord)
256259
}
257260

258-
// List all scans (legacy path)
259-
r.GET("/scans", auth, listScans)
260-
261261
// Utility endpoints
262262
r.POST("/cleanup", auth, cleanupHandler)
263263

@@ -1643,41 +1643,6 @@ func downloadScanResults(c *gin.Context) {
16431643
c.File(tmpFile.Name())
16441644
}
16451645

1646-
func listScans(c *gin.Context) {
1647-
// #9: Pull active from memory, historical from DB (newest first, deterministic).
1648-
scansMutex.RLock()
1649-
active := make([]ScanInfo, 0, len(activeScans))
1650-
for _, scan := range activeScans {
1651-
active = append(active, *scan)
1652-
}
1653-
scansMutex.RUnlock()
1654-
1655-
// Fetch up to 50 recent scans from DB for the completed list.
1656-
completed := make([]ScanInfo, 0, 50)
1657-
if dbScans, err := db.ListRecentScans(50); err == nil {
1658-
for _, s := range dbScans {
1659-
if s.Status == "running" {
1660-
continue // already in active list
1661-
}
1662-
completed = append(completed, ScanInfo{
1663-
ScanID: s.ScanID,
1664-
Type: s.ScanType,
1665-
ScanType: s.ScanType,
1666-
Target: s.Target,
1667-
Status: s.Status,
1668-
StartTime: s.StartedAt,
1669-
StartedAt: s.StartedAt,
1670-
CompletedAt: s.CompletedAt,
1671-
})
1672-
}
1673-
}
1674-
1675-
c.JSON(http.StatusOK, ScanListResponse{
1676-
ActiveScans: active,
1677-
CompletedScans: completed,
1678-
})
1679-
}
1680-
16811646
// Helper functions
16821647

16831648
// generateScanID returns a cryptographically random UUID v4 (#1).
@@ -1835,6 +1800,8 @@ func executeScan(scanID string, command []string, scanType string) {
18351800
}
18361801
// Index any final tool-generated artifacts (nuclei/ffuf/gf/tech/etc) that bypass wrappers.
18371802
indexScanArtifacts(scanID, scanType, target)
1803+
// domain_run / subdomain_run delete local results after upload — backfill from R2 for the UI table.
1804+
indexWorkflowArtifactsFromR2(scanID, scanType, target)
18381805

18391806
scansMutex.Lock()
18401807
delete(activeScans, scanID)
@@ -1934,6 +1901,117 @@ func indexScanArtifacts(scanID, scanType, target string) {
19341901
}
19351902
}
19361903

1904+
// targetHostForR2Prefixes mirrors the UI r2PrefixesForScan hostname normalization (app.js).
1905+
func targetHostForR2Prefixes(target string) string {
1906+
t := strings.TrimSpace(target)
1907+
t = strings.TrimPrefix(strings.TrimPrefix(t, "http://"), "https://")
1908+
if i := strings.Index(t, "/"); i >= 0 {
1909+
t = t[:i]
1910+
}
1911+
t = strings.TrimPrefix(strings.ToLower(t), "www.")
1912+
return t
1913+
}
1914+
1915+
// workflowScanR2Prefixes returns R2 key prefixes used for domain_run / subdomain_run (matches app.js default branch).
1916+
func workflowScanR2Prefixes(target string) []string {
1917+
h := targetHostForR2Prefixes(target)
1918+
if h == "" {
1919+
return nil
1920+
}
1921+
seen := map[string]struct{}{}
1922+
var out []string
1923+
add := func(p string) {
1924+
if p == "" {
1925+
return
1926+
}
1927+
if _, ok := seen[p]; ok {
1928+
return
1929+
}
1930+
seen[p] = struct{}{}
1931+
out = append(out, p)
1932+
}
1933+
add("new-results/" + h + "/")
1934+
add("results/" + h + "/")
1935+
add("lite/" + h + "/")
1936+
add("new-results/misconfig/" + h + "/")
1937+
add("misconfig/" + h + "/")
1938+
return out
1939+
}
1940+
1941+
// isR2KeyIndexableArtifact matches shouldSkipArtifact extension rules for workflow backfill.
1942+
func isR2KeyIndexableArtifact(key string) bool {
1943+
name := strings.ToLower(filepath.Base(key))
1944+
if strings.HasPrefix(name, ".lite-uploads-") {
1945+
return false
1946+
}
1947+
if strings.HasPrefix(name, "temp-") || strings.Contains(name, "dangling-ip-temp") {
1948+
return false
1949+
}
1950+
if strings.EqualFold(name, "temp-url.txt") {
1951+
return false
1952+
}
1953+
ext := strings.ToLower(filepath.Ext(name))
1954+
switch ext {
1955+
case ".txt", ".json", ".log", ".csv", ".html", ".md", ".bin":
1956+
return true
1957+
default:
1958+
return false
1959+
}
1960+
}
1961+
1962+
// indexWorkflowArtifactsFromR2 populates scan_artifacts from R2 listings for domain_run / subdomain_run.
1963+
// Full-domain and single-subdomain workflows delete local result dirs on completion, so post-scan
1964+
// filesystem indexing finds nothing; uploads still land in R2. Backfilling here makes the scan modal
1965+
// use the same indexed-artifact table as other scans instead of the raw R2 fallback.
1966+
func indexWorkflowArtifactsFromR2(scanID, scanType, target string) {
1967+
st := strings.ToLower(strings.TrimSpace(scanType))
1968+
if st != "domain_run" && st != "subdomain_run" {
1969+
return
1970+
}
1971+
if strings.TrimSpace(scanID) == "" || !r2storage.IsEnabled() {
1972+
return
1973+
}
1974+
prefixes := workflowScanR2Prefixes(target)
1975+
if len(prefixes) == 0 {
1976+
return
1977+
}
1978+
seenKey := map[string]struct{}{}
1979+
for _, prefix := range prefixes {
1980+
objs, err := r2storage.ListObjectsRecursive(prefix)
1981+
if err != nil {
1982+
log.Printf("[indexWorkflowArtifactsFromR2] list prefix %q: %v", prefix, err)
1983+
continue
1984+
}
1985+
for _, o := range objs {
1986+
if o.Size == 0 {
1987+
continue
1988+
}
1989+
if !isR2KeyIndexableArtifact(o.Key) {
1990+
continue
1991+
}
1992+
if _, dup := seenKey[o.Key]; dup {
1993+
continue
1994+
}
1995+
seenKey[o.Key] = struct{}{}
1996+
pub := r2storage.PublicURLForKey(o.Key)
1997+
if pub == "" {
1998+
continue
1999+
}
2000+
art := &db.ScanArtifact{
2001+
ScanID: scanID,
2002+
FileName: filepath.Base(o.Key),
2003+
R2Key: o.Key,
2004+
PublicURL: pub,
2005+
SizeBytes: o.Size,
2006+
CreatedAt: o.LastModified,
2007+
}
2008+
if err := db.AppendScanArtifact(art); err != nil {
2009+
log.Printf("[indexWorkflowArtifactsFromR2] append %s: %v", o.Key, err)
2010+
}
2011+
}
2012+
}
2013+
}
2014+
19372015
func shouldSkipArtifact(path string) bool {
19382016
name := strings.ToLower(filepath.Base(path))
19392017
if strings.HasPrefix(name, ".lite-uploads-") {
@@ -1942,6 +2020,9 @@ func shouldSkipArtifact(path string) bool {
19422020
if strings.HasPrefix(name, "temp-") || strings.Contains(name, "dangling-ip-temp") {
19432021
return true
19442022
}
2023+
if strings.EqualFold(name, "temp-url.txt") {
2024+
return true
2025+
}
19452026
ext := strings.ToLower(filepath.Ext(name))
19462027
switch ext {
19472028
case ".txt", ".json", ".log", ".csv", ".html", ".md", ".bin":

internal/modules/gobot/commands.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,11 @@ func runScanBackground(scanID, scanType, target string, command []string, s *dis
363363
log.Printf("[INFO] Updated database scan status for %s to %s", scanID, status)
364364
}
365365

366+
// Match API executeScan: index R2 artifacts for workflow scans (local dirs are cleaned up before indexing).
367+
if err == nil && status == "completed" && (scanType == "domain_run" || scanType == "subdomain_run") {
368+
indexWorkflowArtifactsFromR2(scanID, scanType, target)
369+
}
370+
366371
// Update Discord message
367372
embed := createScanEmbed(scanType, target, status)
368373
if isCancelled {

0 commit comments

Comments
 (0)