Documentation
¶
Overview ¶
Package database provides the repository layer over SQLite (default) or PostgreSQL via the Bun ORM. It persists scans, HTTP records, findings, scopes, and OAST interactions — all scoped to a project_uuid for multi-tenancy. It supports batched HTTP-record ingestion (SaveRecordBatch, RecordWriter) and per-source deduplication (DeduplicateRecordsBySource).
Index ¶
- Constants
- Variables
- func AllTablesDeleteOrder() []string
- func AuthenticationHostnameToSession(sh *AuthenticationHostname) *authentication.Session
- func AuthenticationHostnamesToSessionConfig(rows []*AuthenticationHostname) *authentication.SessionConfig
- func CachedDNS(hostname string) (a, aaaa, cname []string)
- func CountFindingsByAgenticScan(ctx context.Context, db *DB, agenticScanUUID string) (map[string]int64, error)
- func CountFindingsByAgenticScans(ctx context.Context, db *DB, agenticScanUUIDs []string) (map[string]int64, error)
- func CountFindingsByConfidence(ctx context.Context, db *DB, projectUUID string) (map[string]int64, error)
- func CountFindingsByModule(ctx context.Context, db *DB, agenticScanUUID string) (map[string]int64, error)
- func CountFindingsBySeverity(ctx context.Context, db *DB, projectUUID string, hostnames ...string) (map[string]int64, error)
- func CountFindingsByURL(ctx context.Context, db *DB, agenticScanUUID string) (map[string]int64, error)
- func CountRecordsByColumn(ctx context.Context, db *DB, projectUUID, column string) (map[string]int64, error)
- func CountSaved(uuids []string) int
- func CurrentSchemaVersion() int
- func DeleteGenericRecord(ctx context.Context, db *DB, tableName, pkValue string) error
- func EscapeLikeLiteral(s string) string
- func ExpandPath(path string) string
- func ExtractPrimaryToken(headers map[string]string) string
- func FormatStats(stats *DatabaseStats) string
- func GetGenericRecord(ctx context.Context, db *DB, tableName, pkValue string) (map[string]interface{}, error)
- func GroupRecordsByField(field string) (string, error)
- func GroupableFields() []string
- func HasDynamicSegment(path string) bool
- func HasSQLiteHeader(b []byte) bool
- func HostnamesOf(hosts []HostTarget) []string
- func InferSourceType(sourcePath string) string
- func InsertGenericRecord(ctx context.Context, db *DB, tableName string, fields map[string]interface{}) error
- func IsBridgeRecordSource(source string) bool
- func IsRedirectStatus(code int) bool
- func IsSQLiteFile(path string) (bool, error)
- func IsValidFindingSeverity(s string) bool
- func IsValidFindingStatus(s string) bool
- func LikeContains(term string) string
- func LikeGlob(pattern string) string
- func ListTables(ctx context.Context, db *DB) ([]string, error)
- func ParameterTypeFromParamType(ptype httpmsg.ParamType) string
- func PathToTemplate(path string) string
- func QueryGenericTable(ctx context.Context, db *DB, tableName string, limit, offset int) ([]map[string]interface{}, []string, int64, error)
- func QueryGenericTableFiltered(ctx context.Context, db *DB, tableName string, opts GenericQueryOptions) ([]map[string]interface{}, []string, int64, error)
- func RecordToHttpRequestResponse(record *HTTPRecord) (*httpmsg.HttpRequestResponse, error)
- func ResolveHostnameNow(ctx context.Context, hostname string) (ipv4, ipv6, cname []string)
- func UpdateGenericRecord(ctx context.Context, db *DB, tableName, pkValue string, ...) error
- func ValidateTableName(ctx context.Context, db *DB, tableName string) error
- func WithFacts(src *StoredHostFacts, r *HTTPRecord) any
- func WithLikeEscape(pred string) string
- func WithMergeLock(destPath string, timeout time.Duration, fn func() error) error
- type AgentFindingCandidate
- type AgentSection
- type AgenticScan
- type AnalysisArtifact
- type AuthenticationHostname
- type ColumnInfo
- type DB
- func (db *DB) Close() error
- func (db *DB) CreateRecordIndexes(ctx context.Context) error
- func (db *DB) CreateSchema(ctx context.Context) error
- func (db *DB) DeferRecordIndexes()
- func (db *DB) Driver() string
- func (db *DB) EnsureSchemaCurrent(ctx context.Context) error
- func (db *DB) GetStats(ctx context.Context, filters QueryFilters) (*DatabaseStats, error)
- func (db *DB) GetTopHosts(ctx context.Context, filters QueryFilters, limit int) ([]HostStats, error)
- func (db *DB) HasFTS() bool
- func (db *DB) HasVigoliumSchema(ctx context.Context) bool
- func (db *DB) Optimize(ctx context.Context)
- func (db *DB) Path() string
- func (db *DB) SQLDB() *sql.DB
- func (db *DB) SeedDefaults(ctx context.Context) error
- func (db *DB) ServerVersion(ctx context.Context) (string, error)
- type DBInputSource
- func (s *DBInputSource) Close() error
- func (s *DBInputSource) IdleFor() time.Duration
- func (s *DBInputSource) Next(ctx context.Context) (*work.WorkItem, error)
- func (s *DBInputSource) WithHostScopes(hosts []HostTarget) *DBInputSource
- func (s *DBInputSource) WithIdleTimeout(timeout time.Duration) *DBInputSource
- func (s *DBInputSource) WithIncludeSources(sources []string) *DBInputSource
- func (s *DBInputSource) WithOnActivity(fn func(recordCount int, idleFor time.Duration, firstBatch bool)) *DBInputSource
- func (s *DBInputSource) WithPageSize(pageSize int) *DBInputSource
- type DatabaseInfo
- type DatabaseStats
- type DateRangeStats
- type DeleteBuilder
- func (db *DeleteBuilder) DeleteAllTables(ctx context.Context, dryRun bool) (map[string]int64, error)
- func (db *DeleteBuilder) DeleteFindings(ctx context.Context, dryRun bool) (int64, error)
- func (db *DeleteBuilder) DeleteOrphans(ctx context.Context, dryRun bool) (int64, error)
- func (db *DeleteBuilder) DeleteRecords(ctx context.Context, dryRun bool) (int64, error)
- func (db *DeleteBuilder) DeleteTable(ctx context.Context, tableName string, dryRun bool) (int64, error)
- type DeparosStatusPolicy
- type EmbeddedParam
- type Finding
- type FindingSaveResult
- type FindingWrite
- type FindingWriter
- type FindingWriterConfig
- type FindingWriterMetrics
- type FindingsQueryBuilder
- type FindingsStats
- type GenericFilter
- type GenericQueryOptions
- type GroupFindingOptions
- type HTTPRecord
- func (r *HTTPRecord) EnrichFromHttpRequestResponse(ctx *httpmsg.HttpRequestResponse) error
- func (r *HTTPRecord) FromHttpRequestResponse(ctx *httpmsg.HttpRequestResponse) error
- func (r *HTTPRecord) MarshalJSON() ([]byte, error)
- func (r *HTTPRecord) ParsedRequest() *httpmsg.HttpRequest
- func (r *HTTPRecord) ParsedResponse() *httpmsg.HttpResponse
- func (r *HTTPRecord) ParsedView() (reqHdrs, respHdrs map[string][]string, reqBody, respBody []byte)
- func (r *HTTPRecord) PrepareIdentity(ctx *httpmsg.HttpRequestResponse) error
- func (r *HTTPRecord) RedirectLocation() string
- func (r *HTTPRecord) RequestBodyBytes() []byte
- func (r *HTTPRecord) RequestHeadersMap() map[string][]string
- func (r *HTTPRecord) ResponseBodyBytes() []byte
- func (r *HTTPRecord) ResponseHeadersMap() map[string][]string
- type HostFacts
- type HostObservation
- type HostObservationInput
- type HostStats
- type HostTarget
- type MergeOptions
- type MergeStats
- type OASTInteraction
- type PathTarget
- type PerformanceMetrics
- type PrimaryKeyInfo
- type Project
- type ProjectStatsRow
- type QueryBuilder
- func (qb *QueryBuilder) BuildRecordsQuery() *bun.SelectQuery
- func (qb *QueryBuilder) Count(ctx context.Context) (int64, error)
- func (qb *QueryBuilder) Execute(ctx context.Context) ([]*HTTPRecord, error)
- func (qb *QueryBuilder) ExecuteWithCount(ctx context.Context) ([]*HTTPRecord, int64, error)
- func (qb *QueryBuilder) GroupRecordsBy(ctx context.Context, field string, limit int) (RecordGrouping, error)
- func (qb *QueryBuilder) OmitBodies() *QueryBuilder
- type QueryFilters
- type ReSpiderCandidate
- type RecordCounts
- type RecordGroup
- type RecordGrouping
- type RecordResponseUpdate
- type RecordWithHostFacts
- type RecordWriter
- func (w *RecordWriter) Close()
- func (w *RecordWriter) Metrics() RecordWriterMetrics
- func (w *RecordWriter) SaveRecord(ctx context.Context, rr *httpmsg.HttpRequestResponse, source string, ...) (string, error)
- func (w *RecordWriter) SaveRecordBatch(ctx context.Context, records []*httpmsg.HttpRequestResponse, source string, ...) ([]string, error)
- func (w *RecordWriter) Write(ctx context.Context, rr *httpmsg.HttpRequestResponse, source string, ...) (string, error)
- func (w *RecordWriter) WriteWithParent(ctx context.Context, rr *httpmsg.HttpRequestResponse, source string, ...) (string, error)
- type RecordWriterConfig
- type RecordWriterMetrics
- type Repository
- func (r *Repository) AdoptRecordParent(ctx context.Context, uuid, parentUUID string)
- func (r *Repository) AdvanceScanCursor(ctx context.Context, scanUUID string, recordCreatedAt time.Time, ...) error
- func (r *Repository) AdvanceScanCursorBy(ctx context.Context, scanUUID string, recordCreatedAt time.Time, ...) error
- func (r *Repository) AgenticScanStatusByUUID(ctx context.Context, uuid string) (string, error)
- func (r *Repository) AppendRemarks(ctx context.Context, annotations map[string][]string) error
- func (r *Repository) ApplyDeparosStatusPolicy(ctx context.Context, projectUUID string, policy DeparosStatusPolicy) (int64, map[int]int64, error)
- func (r *Repository) AttributeScanToAgenticScan(ctx context.Context, scanUUID, agenticScanUUID string) error
- func (r *Repository) BackfillRecordResponse(ctx context.Context, uuid string, rr *httpmsg.HttpRequestResponse) error
- func (r *Repository) CompleteScan(ctx context.Context, scanUUID string, errMsg string) error
- func (r *Repository) CountRecordsAfterCursor(ctx context.Context, cursorAt time.Time, cursorUUID string, ...) (int64, error)
- func (r *Repository) CountRecordsAfterCursorBySource(ctx context.Context, cursorAt time.Time, cursorUUID string, sources []string, ...) (int64, error)
- func (r *Repository) CountRecordsByStatusCode(ctx context.Context, hosts ...HostTarget) (map[int]int64, error)
- func (r *Repository) CountRecordsForScan(ctx context.Context, projectUUID, scanUUID string) (int64, error)
- func (r *Repository) CreateAgenticScan(ctx context.Context, run *AgenticScan) error
- func (r *Repository) CreateProject(ctx context.Context, project *Project) error
- func (r *Repository) CreateScan(ctx context.Context, scan *Scan) error
- func (r *Repository) CreateScanLog(ctx context.Context, log *ScanLog) error
- func (r *Repository) CreateScanLogBatch(ctx context.Context, logs []*ScanLog) error
- func (r *Repository) CreateScanWithCursor(ctx context.Context, scan *Scan) error
- func (r *Repository) CreateUser(ctx context.Context, user *User) error
- func (r *Repository) DB() *DB
- func (r *Repository) DeduplicateDeparosByNormHash(ctx context.Context, projectUUID string) (int64, map[int]int64, error)
- func (r *Repository) DeduplicateDeparosRecords(ctx context.Context, projectUUID string) (int64, error)
- func (r *Repository) DeduplicateFindings(ctx context.Context, projectUUID string, hostnames ...string) (deleted int64, grouped int64, err error)
- func (r *Repository) DeduplicateRecordsBySource(ctx context.Context, projectUUID, source string) (int64, error)
- func (r *Repository) DeduplicateSoftDeparosRecords(ctx context.Context, projectUUID string) (int64, map[int]int64, error)
- func (r *Repository) DeleteAuthenticationHostname(ctx context.Context, id int64) error
- func (r *Repository) DeleteAuthenticationHostnamesByHostname(ctx context.Context, projectUUID, hostname string) error
- func (r *Repository) DeleteFinding(ctx context.Context, id int64) error
- func (r *Repository) DeleteFindingByHash(ctx context.Context, projectUUID, findingHash string) error
- func (r *Repository) DeleteOASTInteraction(ctx context.Context, id int64) error
- func (r *Repository) DeleteOldAgenticScans(ctx context.Context, olderThan time.Duration) (int, error)
- func (r *Repository) DeleteProject(ctx context.Context, uuid string) error
- func (r *Repository) DeleteRecord(ctx context.Context, uuid string) error
- func (r *Repository) DeleteScan(ctx context.Context, uuid string) error
- func (r *Repository) ExistingRecordUUIDs(ctx context.Context, projectUUID string, uuids []string) ([]string, error)
- func (r *Repository) FindingProjectUUID(ctx context.Context, id int64) (string, error)
- func (r *Repository) GetAgenticScan(ctx context.Context, uuid string) (*AgenticScan, error)
- func (r *Repository) GetAllProjectsStats(ctx context.Context) (map[string]*ProjectStatsRow, error)
- func (r *Repository) GetAuthenticationHostnamesByHostname(ctx context.Context, projectUUID, hostname string) ([]*AuthenticationHostname, error)
- func (r *Repository) GetAuthenticationHostnamesByProject(ctx context.Context, projectUUID string) ([]*AuthenticationHostname, error)
- func (r *Repository) GetAuthenticationHostnamesByScan(ctx context.Context, projectUUID, scanUUID string) ([]*AuthenticationHostname, error)
- func (r *Repository) GetChildAgenticScans(ctx context.Context, parentUUID string) ([]*AgenticScan, error)
- func (r *Repository) GetDistinctHosts(ctx context.Context, projectUUID string, since ...time.Time) ([]HostTarget, error)
- func (r *Repository) GetDistinctPaths(ctx context.Context, projectUUID string, hosts ...HostTarget) ([]PathTarget, error)
- func (r *Repository) GetFindingByID(ctx context.Context, id int64) (*Finding, error)
- func (r *Repository) GetFindingsByRecordUUID(ctx context.Context, uuid string) ([]*Finding, error)
- func (r *Repository) GetFindingsBySeverity(ctx context.Context, projectUUID, sev string, limit int) ([]*Finding, error)
- func (r *Repository) GetOASTInteractionByID(ctx context.Context, id int64) (*OASTInteraction, error)
- func (r *Repository) GetOASTInteractionsByScan(ctx context.Context, scanUUID string) ([]*OASTInteraction, error)
- func (r *Repository) GetProjectByName(ctx context.Context, name string) (*Project, error)
- func (r *Repository) GetProjectByUUID(ctx context.Context, uuid string) (*Project, error)
- func (r *Repository) GetProjectStats(ctx context.Context, projectUUID string) (*ProjectStatsRow, error)
- func (r *Repository) GetReSpiderCandidates(ctx context.Context, projectUUID, afterUUID string, limit int, ...) ([]ReSpiderCandidate, error)
- func (r *Repository) GetRecordByRequestHash(ctx context.Context, projectUUID, requestHash string) (*HTTPRecord, error)
- func (r *Repository) GetRecordByUUID(ctx context.Context, uuid string) (*HTTPRecord, error)
- func (r *Repository) GetRecordMetadataByHostname(ctx context.Context, projectUUID, hostname string, limit int) ([]*HTTPRecord, error)
- func (r *Repository) GetRecordsByHostname(ctx context.Context, projectUUID, hostname string, limit int) ([]*HTTPRecord, error)
- func (r *Repository) GetRecordsByUUIDs(ctx context.Context, uuids []string) ([]*HTTPRecord, error)
- func (r *Repository) GetRecordsWithResponseBody(ctx context.Context, projectUUID, afterUUID string, limit int, ...) ([]*HTTPRecord, error)
- func (r *Repository) GetRelatedRecords(ctx context.Context, uuid string, limit int) ([]*HTTPRecord, error)
- func (r *Repository) GetScanByUUID(ctx context.Context, uuid string) (*Scan, error)
- func (r *Repository) GetScanRecordsByUUIDs(ctx context.Context, uuids []string) ([]*HTTPRecord, error)
- func (r *Repository) GetUnprobedRecordsBySource(ctx context.Context, projectUUID, source, hostname string, limit int) ([]*HTTPRecord, error)
- func (r *Repository) GetUserByUUID(ctx context.Context, uuid string) (*User, error)
- func (r *Repository) GroupFindingsByValue(ctx context.Context, projectUUID string, opts GroupFindingOptions) (deleted int64, grouped int64, err error)
- func (r *Repository) HasHostObservations(ctx context.Context) bool
- func (r *Repository) HydrateFindingRequests(ctx context.Context, findings []*Finding)
- func (r *Repository) InScopeHosts(ctx context.Context, scopeCfg config.ScopeConfig, targets []string, ...) []HostTarget
- func (r *Repository) IncrementProcessedCount(ctx context.Context, scanUUID string, delta int64) error
- func (r *Repository) ListAgentSections(ctx context.Context, agenticScanUUID string) ([]*AgentSection, error)
- func (r *Repository) ListAgenticScanSummaries(ctx context.Context, projectUUID string, mode string, limit, offset int) ([]*AgenticScan, int64, error)
- func (r *Repository) ListAgenticScans(ctx context.Context, projectUUID string, mode string, limit, offset int) ([]*AgenticScan, int64, error)
- func (r *Repository) ListCandidates(ctx context.Context, agenticScanUUID string, statuses ...string) ([]*AgentFindingCandidate, error)
- func (r *Repository) ListFindings(ctx context.Context, filters QueryFilters) ([]*Finding, int64, error)
- func (r *Repository) ListOASTInteractions(ctx context.Context, projectUUID, scanUUID, protocol, moduleID, search string, ...) ([]*OASTInteraction, int64, error)
- func (r *Repository) ListProjects(ctx context.Context, ownerUUID string) ([]*Project, error)
- func (r *Repository) ListScanLogs(ctx context.Context, scanUUID string, level, phase string, limit, offset int) ([]*ScanLog, int64, error)
- func (r *Repository) ListScans(ctx context.Context, projectUUID string, limit, offset int) ([]*Scan, int64, error)
- func (r *Repository) ListUsers(ctx context.Context) ([]*User, error)
- func (r *Repository) LoadEnabledScopes(ctx context.Context, projectUUID string) ([]*Scope, error)
- func (r *Repository) LookupHostObservation(ctx context.Context, projectUUID, scanUUID, hostname string, port int) (HostFacts, error)
- func (r *Repository) OverwriteRecordResponseBody(ctx context.Context, uuid string, rawResponse []byte) error
- func (r *Repository) PauseScan(ctx context.Context, scanUUID string) error
- func (r *Repository) PurgeProjectData(ctx context.Context, projectUUID string) error
- func (r *Repository) ReassignProjectData(ctx context.Context, sourceUUID, targetUUID string) error
- func (r *Repository) RecordProjectUUID(ctx context.Context, uuid string) (string, error)
- func (r *Repository) RefreshScanStats(ctx context.Context, scanUUID string) error
- func (r *Repository) ResetScanCursor(ctx context.Context, scanUUID string) error
- func (r *Repository) ResumeScan(ctx context.Context, scanUUID string) error
- func (r *Repository) SaveAgentSection(ctx context.Context, section *AgentSection) error
- func (r *Repository) SaveAnalysisArtifact(ctx context.Context, httpRecordUUID, kind, filename, mediaType, sha256 string, ...) error
- func (r *Repository) SaveAnalysisArtifactForRecord(ctx context.Context, artifact *AnalysisArtifact) error
- func (r *Repository) SaveAuthenticationHostname(ctx context.Context, sh *AuthenticationHostname) error
- func (r *Repository) SaveAuthenticationHostnames(ctx context.Context, rows []*AuthenticationHostname) error
- func (r *Repository) SaveCandidate(ctx context.Context, cand *AgentFindingCandidate) (bool, error)
- func (r *Repository) SaveFinding(ctx context.Context, event *output.ResultEvent, httpRecordUUIDs []string, ...) error
- func (r *Repository) SaveFindingDirect(ctx context.Context, finding *Finding) error
- func (r *Repository) SaveFindingReported(ctx context.Context, finding *Finding) (bool, error)
- func (r *Repository) SaveFindingsBatch(ctx context.Context, writes []FindingWrite) error
- func (r *Repository) SaveFindingsDirectBatch(ctx context.Context, findings []*Finding) ([]FindingSaveResult, error)
- func (r *Repository) SaveHostObservations(ctx context.Context, projectUUID, scanUUID string, obs []HostObservationInput) error
- func (r *Repository) SaveOASTInteraction(ctx context.Context, interaction *OASTInteraction) error
- func (r *Repository) SaveRecord(ctx context.Context, httpRR *httpmsg.HttpRequestResponse, source string, ...) (string, error)
- func (r *Repository) SaveRecordBatch(ctx context.Context, records []*httpmsg.HttpRequestResponse, source string, ...) ([]string, error)
- func (r *Repository) SaveRecordWithParent(ctx context.Context, httpRR *httpmsg.HttpRequestResponse, source string, ...) (string, error)
- func (r *Repository) SaveRecordsBatch(ctx context.Context, records []*HTTPRecord) ([]string, error)
- func (r *Repository) ScanSeverityTally(ctx context.Context, scanUUID string) map[string]int
- func (r *Repository) SetRecordTechnology(ctx context.Context, technology map[string][]string) error
- func (r *Repository) UpdateAgentSection(ctx context.Context, section *AgentSection) error
- func (r *Repository) UpdateAgenticScan(ctx context.Context, run *AgenticScan) error
- func (r *Repository) UpdateAgenticScanStorageURL(ctx context.Context, agenticScanUUID, storageURL string) error
- func (r *Repository) UpdateCandidateStatus(ctx context.Context, uuid, status, verdictReason string, ...) error
- func (r *Repository) UpdateFindingSeverity(ctx context.Context, id int64, severity string) error
- func (r *Repository) UpdateFindingStatus(ctx context.Context, id int64, status string) error
- func (r *Repository) UpdateFindingStatusByHash(ctx context.Context, agenticScanUUID, findingHash, status string) (int64, error)
- func (r *Repository) UpdateFindingTriage(ctx context.Context, id int64, severity, description string) error
- func (r *Repository) UpdateProject(ctx context.Context, project *Project) error
- func (r *Repository) UpdateRecordAnnotations(ctx context.Context, uuid string, riskScore *int, remarks []string) error
- func (r *Repository) UpdateRecordResponse(ctx context.Context, uuid string, update *RecordResponseUpdate) error
- func (r *Repository) UpdateRiskScores(ctx context.Context, scores map[string]int) error
- func (r *Repository) UpdateScan(ctx context.Context, scan *Scan) error
- func (r *Repository) UpdateScanPartial(ctx context.Context, scan *Scan) error
- func (r *Repository) UpdateScanStorageURL(ctx context.Context, scanUUID, storageURL string) error
- func (r *Repository) UpdateSurfaceScores(ctx context.Context, scores map[string]int) error
- func (r *Repository) UpsertSnapshotRecord(ctx context.Context, httpRR *httpmsg.HttpRequestResponse, source string, ...) (uuid, outcome string, err error)
- func (r *Repository) UpsertUser(ctx context.Context, user *User) error
- func (r *Repository) WalkJavaScriptRecords(ctx context.Context, projectUUID, hostname string, limit int, ...) error
- type RiskPrioritizedDBInputSource
- func (s *RiskPrioritizedDBInputSource) Close() error
- func (s *RiskPrioritizedDBInputSource) CoalescedDropped() int
- func (s *RiskPrioritizedDBInputSource) Next(ctx context.Context) (*work.WorkItem, error)
- func (s *RiskPrioritizedDBInputSource) WithHostScopes(hosts []HostTarget) *RiskPrioritizedDBInputSource
- func (s *RiskPrioritizedDBInputSource) WithParamShapeCoalescing(maxSamples int) *RiskPrioritizedDBInputSource
- type Scan
- type ScanLog
- type ScanLogger
- func (l *ScanLogger) Close()
- func (l *ScanLogger) Error(phase, message string)
- func (l *ScanLogger) Flush()
- func (l *ScanLogger) Info(phase, message string)
- func (l *ScanLogger) InfoWithMeta(phase, message string, meta map[string]interface{})
- func (l *ScanLogger) StartBatcher()
- func (l *ScanLogger) Trace(phase, message string)
- func (l *ScanLogger) Warn(phase, message string)
- type ScanSessionStats
- type ScanView
- type Scope
- type ScopeEvaluator
- type SeverityCount
- type StatusCodeBreakdown
- type StoredHostFacts
- type TableInfo
- type UUIDListDBInputSource
- type User
- type WriteResult
Constants ¶
const ( RecordSourceIngestServer = "ingest-server" // POST /api/ingest-http RecordSourceIngestProxy = "ingest-proxy" // transparent ingest proxy capture RecordSourceIngestCLI = "ingest-cli" // `vigolium ingest ...` command RecordSourceScanner = "scanner" // executor feedback re-injection RecordSourceProbe = "probe" // `vigolium run probe` host sweep (one request per target, no fuzzing) RecordSourceFinding = "finding" // request/response attached to a finding RecordSourceBurp = "burp" // Burp Suite, via the loopback bridge or a push to /api/ingest-http RecordSourceCaido = "caido" // Caido, via the loopback bridge or a push to /api/ingest-http )
Canonical values for http_records.source. Kept here (rather than inline string literals at call sites) so the scan-on-receive filter and the ingest writers agree on a single set of labels.
const ( ModuleTypeActive = "active" ModuleTypePassive = "passive" ModuleTypeNuclei = "nuclei" ModuleTypeAgent = "agent" ModuleTypeOAST = "oast" ModuleTypeExtension = "extension" ModuleTypeKnownIssueScan = "known-issue-scan" ModuleTypeWhitebox = "whitebox" )
ModuleType constants classify the kind of module that created a finding.
const ( FindingSourceDynamicAssessment = "dynamic-assessment" FindingSourceKnownIssueScan = "known-issue-scan" FindingSourceAgent = "agent" FindingSourceOAST = "oast" FindingSourceExtension = "extension" FindingSourceAudit = "audit" FindingSourceImport = "import" FindingSourceSpidering = "spidering" )
FindingSource constants identify which phase/component produced a finding.
const ( RecordKindFinding = "finding" RecordKindCandidate = "candidate" RecordKindObservation = "observation" )
Record-kind constants distinguish reportable vulnerabilities from retained reconnaissance and unconfirmed hypotheses stored in the same table.
const ( StatusDraft = "draft" StatusTriaged = "triaged" StatusFalsePositive = "false_positive" StatusAcceptedRisk = "accepted_risk" StatusFixed = "fixed" )
Status constants represent the lifecycle state of a Finding.
Lifecycle:
draft ──triage──▶ triaged ──fix──▶ fixed
╲──▶ false_positive
╲──▶ accepted_risk
Trust model: deterministic engines (native scan, SAST tools) produce findings as `triaged`. LLM/agent-driven and externally imported findings land as `draft` and require triage to be promoted.
const ( SeverityCritical = "critical" SeverityHigh = "high" SeverityMedium = "medium" SeverityLow = "low" SeverityInfo = "info" SeveritySuspect = "suspect" )
Severity constants represent the canonical severity strings stored on a Finding. They match the lowercase keys used by `vigolium finding --severity` and the severity counters on Scan.
const ( SourceTypeLocal = "local" SourceTypeGitURL = "git-url" SourceTypeGCS = "gcs" )
SourceType constants classify how source code was provided.
const ( CandidateStatusProposed = "proposed" CandidateStatusVerifying = "verifying" CandidateStatusConfirmed = "confirmed" CandidateStatusRejected = "rejected" CandidateStatusNeedsEvidence = "needs_evidence" )
Candidate lifecycle status values.
const ( SectionStatusRunning = "running" SectionStatusCompleted = "completed" SectionStatusInterrupted = "interrupted" )
Section lifecycle status values.
const DefaultMaxParamShapeSamples = 3
DefaultMaxParamShapeSamples is the per-shape representative cap used when param-shape coalescing is enabled without an explicit override.
const DefaultProjectUUID = "00000000-0000-0000-defa-c01001000001"
DefaultProjectUUID is the UUID for the default project created during init. Lowercased for consistency with uuid.New().String() output and byte-wise SQL comparisons.
const DefaultUserUUID = "00000000-0000-0000-0000-000000000001"
DefaultUserUUID is the well-known UUID for the default local user created during init.
const EvidenceSeparator = output.EvidenceSeparator
EvidenceSeparator is the delimiter between request and response inside an AdditionalEvidence entry. Re-exported from pkg/output (the canonical owner) so the storage-layer dedup/merge path splits evidence the same way modules emit it.
const ( // SQLChunkSize is how many ids or uuids to put in one IN() list. It keeps the // generated SQL to a few hundred KB while staying under maxSQLParams with // room for the handful of other values a query carries (project scoping, a // date range, a LIMIT) — so one constant is correct on both kinds of path. SQLChunkSize = 8000 )
Chunking policy for the id/uuid IN() lists this package builds.
The reason is NOT the bound-parameter ceiling, and assuming it was is how the wrong chunk size got picked before. Two facts, both measured against the dependencies in go.mod rather than inferred:
- Through database/sql and the modernc driver directly, a statement binds at most 32766 parameters; 32767 fails with "too many SQL variables". That is the ceiling maxSQLParams records, and it applies to the raw-SQL paths (merge's pinned connection, the scratch-database probes).
- Through bun — which is every query builder call in this package — `?` and bun.List do not produce bound parameters at all. The formatter interpolates the values as SQL literals, so a 100k-element IN() list executes happily.
So an unchunked list does not fail: it silently becomes a multi-megabyte SQL string, built and escaped element by element in Go and then parsed by SQLite, for a statement whose result is the same either way. Chunking bounds that string. It also bounds the raw-SQL paths below the hard ceiling, so a caller does not have to know which kind of path it is on.
If a future bun upgrade switches to real parameter binding, the ceiling starts applying to these calls too — TestBunInterpolatesRatherThanBinds is the test that notices.
Variables ¶
var ( // ErrTableNotFound indicates the requested table is unknown to the generic API. ErrTableNotFound = errors.New("table not found") // ErrInvalidColumn indicates a supplied column does not exist on the target table. ErrInvalidColumn = errors.New("invalid column") // ErrNoValidFields indicates no usable fields were supplied for an insert/update. ErrNoValidFields = errors.New("no valid fields provided") // ErrCompositePKUnsupported indicates a single-value PK operation was attempted // on a table whose primary key spans multiple columns. ErrCompositePKUnsupported = errors.New("single-value operation not supported for composite primary key table") // ErrImmutablePrimaryKey indicates an attempt to update a primary-key column. ErrImmutablePrimaryKey = errors.New("cannot update primary key column") )
Sentinel errors returned by the generic table API. Callers (notably the REST DB handlers) discriminate on these with errors.Is to map failures to HTTP status codes, rather than matching on error-message substrings. They classify client-fixable input problems (unknown table/column, empty field set, single-value ops on composite-PK tables) distinctly from internal failures.
var AllowedCleanTables = map[string]cleanableTable{ "http_records": {SQLName: "http_records", CascadeFirst: []string{"finding_records"}}, "findings": {SQLName: "findings", CascadeFirst: []string{"finding_records"}}, "finding_records": {SQLName: "finding_records"}, "scans": {SQLName: "scans"}, "agentic_scans": {SQLName: "agentic_scans", CascadeFirst: []string{"agent_sections", "agent_finding_candidates"}}, "agent_sections": {SQLName: "agent_sections"}, "agent_finding_candidates": {SQLName: "agent_finding_candidates"}, "oast_interactions": {SQLName: "oast_interactions"}, "scan_logs": {SQLName: "scan_logs"}, "authentication_hostnames": {SQLName: "authentication_hostnames"}, "scopes": {SQLName: "scopes"}, }
AllowedCleanTables maps user-facing table names to their SQL table name and any dependent tables that must be cleaned first.
var BridgeRecordSources = []string{ RecordSourceBurp, RecordSourceCaido, }
BridgeRecordSources lists the http_records.source values a loopback bridge listener can produce. Which one a given record gets is decided by the implementation identity the listener reports (see pkg/burpbridge), not by the flag spelling the user typed — --burp-bridge-url and --caido-bridge-url are one flag pointing at one protocol.
var ErrRecordWriterClosed = errors.New("record writer is closed")
ErrRecordWriterClosed is returned when writes are attempted after shutdown starts.
var ErrScanProjectMismatch = errors.New("scan UUID exists under a different project")
ErrScanProjectMismatch is returned by CreateScan / CreateAgenticScan when the caller pins a UUID that already exists under a different project. This guards against cross-project record corruption when remote nodes sync via --scan-uuid.
var ErrSchemaOutdated = errors.New("database schema is older than this vigolium and --read-only cannot migrate it")
ErrSchemaOutdated marks a database written by an older vigolium that this handle cannot migrate because it was opened read-only. It is a distinct error because it is FATAL to the command — every query that follows would fail on a missing column — whereas a migration failure on a writable handle is best-effort and worth only a warning.
var HostFactsFieldNames = []string{"a", "aaaa", "cname", "tls"}
HostFactsFieldNames are the JSON keys AsMap can produce, for the --fields allowlist. Derived from the struct so a new fact reaches the allowlist with its emitter.
var IngestRecordSources = []string{ RecordSourceIngestServer, RecordSourceIngestProxy, RecordSourceIngestCLI, RecordSourceBurp, RecordSourceCaido, }
IngestRecordSources lists the http_records.source values that represent user-ingested traffic. Scan-on-receive filters the DBInputSource by this list so it only processes traffic the user actually ingested — excluding scanner-produced artefacts that would otherwise cause fan-out.
The bridge vendors belong here for the same reason ingest-server does: a record that arrived from Burp or Caido is traffic the user deliberately handed over, whether it was pulled in by `traffic --save-to-vigolium-db` / `import -B` or pushed to /api/ingest-http by the plugin. Omitting them is silent — the rows land in the database and scan-on-receive simply never polls them, with no error to explain why nothing was scanned.
var TerminalAgenticScanStatuses = []string{
"completed",
"completed_with_errors",
"failed",
"cancelled",
"timeout",
"error",
}
TerminalAgenticScanStatuses is the authoritative set of agentic_scans.status values meaning the run has finished. Two subsystems must agree on it — the SSE log follower, which tails runtime.log until the run is terminal, and the retention sweep below, which only deletes finished runs — and a status present in one and absent from the other is either a stream that never ends or a row that is never cleaned. Both were true of "completed_with_errors", what a multi-driver audit writes when one leg fails: following its log spun until the two-hour cap and its row outlived every retention window. Add new terminal statuses here, not at either call site.
Functions ¶
func AllTablesDeleteOrder ¶
func AllTablesDeleteOrder() []string
AllTablesDeleteOrder returns the ordered list of cleanable table names.
func AuthenticationHostnameToSession ¶
func AuthenticationHostnameToSession(sh *AuthenticationHostname) *authentication.Session
AuthenticationHostnameToSession converts a DB AuthenticationHostname row to a native authentication.Session.
Note: the DB schema does not carry the type/token_path shorthand fields — only the expanded ExtractRules. SessionToAuthenticationHostname normalizes shorthand into explicit rules on write so they round-trip correctly.
func AuthenticationHostnamesToSessionConfig ¶
func AuthenticationHostnamesToSessionConfig(rows []*AuthenticationHostname) *authentication.SessionConfig
AuthenticationHostnamesToSessionConfig converts a slice of DB rows (typically for one hostname) into a authentication.SessionConfig with ordered sessions.
func CachedDNS ¶
CachedDNS returns the full DNS answer this PROCESS resolved for hostname, or nothing when it never looked it up (or the entry has aged out).
Deliberately not persisted. `ip` — one address per record — is the only DNS the schema stores. A host sweep writes many records per host (a redirect chain alone is three), and a corpus of thousands of records across a few hundred hosts would otherwise store the same A/AAAA/CNAME arrays thousands of times, inflating the database for data that is identical on every row and stale the moment the TTL expires. Resolving is cheap and repeatable; storing it is not.
So this is a read-time attachment: the run that did the resolving reports the full answer inline in its output, and reading the same database back later in a fresh process correctly shows only `ip`. Expiry is deliberately NOT checked — this reports what was resolved for the records being emitted, and a sweep long enough to cross the TTL should still describe the rows it wrote.
func CountFindingsByAgenticScan ¶
func CountFindingsByAgenticScan(ctx context.Context, db *DB, agenticScanUUID string) (map[string]int64, error)
CountFindingsByAgenticScan returns finding counts grouped by severity for one agentic-scan run. Severity strings are lowercased/trimmed so callers get a canonical key set regardless of how the finding was inserted. Empty agenticScanUUID returns an empty map without querying.
func CountFindingsByAgenticScans ¶
func CountFindingsByAgenticScans(ctx context.Context, db *DB, agenticScanUUIDs []string) (map[string]int64, error)
CountFindingsByAgenticScans is like CountFindingsByAgenticScan but counts across several agentic-scan UUIDs at once (a parent run plus its driver / sub-run children). Returns severity→count with lowercase, trimmed keys.
func CountFindingsByConfidence ¶
func CountFindingsByConfidence(ctx context.Context, db *DB, projectUUID string) (map[string]int64, error)
CountFindingsByConfidence returns finding counts grouped by confidence.
func CountFindingsByModule ¶
func CountFindingsByModule(ctx context.Context, db *DB, agenticScanUUID string) (map[string]int64, error)
CountFindingsByModule returns finding counts grouped by module_id, scoped to one agentic-scan run when agenticScanUUID is non-empty (empty = all rows).
func CountFindingsBySeverity ¶
func CountFindingsBySeverity(ctx context.Context, db *DB, projectUUID string, hostnames ...string) (map[string]int64, error)
CountFindingsBySeverity returns finding counts grouped by severity, filtered by project and, when hostnames is non-empty, restricted to findings on those in-scope hosts. Empty hostnames means no host filter. Findings are scoped by hostname only (the findings table carries no scheme/port column), unlike the origin-precise record scoping; this lets the scan-completion summary count only findings on the hosts actually scanned, not leftovers from prior scans in the same project.
func CountFindingsByURL ¶
func CountFindingsByURL(ctx context.Context, db *DB, agenticScanUUID string) (map[string]int64, error)
CountFindingsByURL returns finding counts grouped by URL, scoped to one agentic-scan run when agenticScanUUID is non-empty (empty = all rows). Findings with an empty URL are skipped — they aren't endpoint-attributable.
func CountRecordsByColumn ¶
func CountRecordsByColumn(ctx context.Context, db *DB, projectUUID, column string) (map[string]int64, error)
CountRecordsByColumn returns http_record counts grouped by a single column (one of method, status_code, response_content_type), filtered by project (empty = every row, e.g. a --glob-db merge). Keys are the column values as text. Powers the traffic listing's status/method/content-type summary.
func CountSaved ¶
CountSaved reports how many records in a SaveRecordBatch result were actually persisted. The result is index-aligned with the input and carries "" for a record that was skipped, so len() over-reports — this is the only correct way to turn that slice into a count.
func CurrentSchemaVersion ¶
func CurrentSchemaVersion() int
CurrentSchemaVersion exposes the schema revision this binary expects, so a consumer can fail loudly on drift at startup (`vigolium version --json`) rather than discovering it as a query error mid-run.
func DeleteGenericRecord ¶
DeleteGenericRecord deletes a record by primary key.
func EscapeLikeLiteral ¶
EscapeLikeLiteral neutralizes the LIKE metacharacters in s so the string matches itself. The escape character is escaped first; doing it last would double the backslashes this function just introduced.
func ExpandPath ¶
ExpandPath resolves ~ and environment variables in a filesystem path using the same rules as the SQLite DSN builder. Exported so callers (e.g. the merge lock) can derive sidecar paths next to the real database file.
func ExtractPrimaryToken ¶
extractPrimaryToken extracts the primary session token value from a headers map. It checks Authorization (Bearer/token) first, then Cookie, then falls back to the first header value. Returns empty string if no headers.
func FormatStats ¶
func FormatStats(stats *DatabaseStats) string
FormatStats formats statistics as a human-readable string
func GetGenericRecord ¶
func GetGenericRecord(ctx context.Context, db *DB, tableName, pkValue string) (map[string]interface{}, error)
GetGenericRecord fetches a single record by primary key.
func GroupRecordsByField ¶
GroupRecordsByField resolves a public field name to its physical column, or explains what the accepted names are.
Exported so the CLI can reject a bad --group-by before it opens a database: a name typo is knowable from the flags alone, and finding out after the open is how a read command comes to touch a file it had no business touching.
func GroupableFields ¶
func GroupableFields() []string
GroupableFields lists the accepted --group-by names, sorted, for help text and for the error a rejected name produces.
func HasDynamicSegment ¶
HasDynamicSegment returns true if the path contains at least one dynamic segment — a numeric ID, UUID, long hex string, or long alphanumeric token. Version-like segments (v1, v2, v10) are not considered dynamic.
func HasSQLiteHeader ¶
HasSQLiteHeader reports whether b begins with the SQLite 3 file magic. Callers that already hold the leading bytes of a file use this to avoid a second read.
func HostnamesOf ¶
func HostnamesOf(hosts []HostTarget) []string
HostnamesOf returns the distinct hostnames of the given origins, order-preserving. Used to derive a hostname-only filter (e.g. for findings, which carry no port column) from a set of in-scope origins.
func InferSourceType ¶
InferSourceType returns the source type for a given path or URL. Both "gs://" (canonical) and "gcs://" (alias) are recognized as cloud storage.
func InsertGenericRecord ¶
func InsertGenericRecord(ctx context.Context, db *DB, tableName string, fields map[string]interface{}) error
InsertGenericRecord inserts a record into the given table. fields is a map of column_name -> value. Column names are validated.
func IsBridgeRecordSource ¶
IsBridgeRecordSource reports whether a source label names a bridge vendor. Case-insensitive, since it answers both a user-typed --source and a header value written by a plugin.
This is the single answer to "is this label a bridge vendor" — the traffic and replay record selection ask it to decide whether a --source filter can be satisfied by live bridge traffic at all, and the ingest endpoint asks it to decide whether a client may claim the label. A second copy of the list is how a third vendor ends up working on one path and not the other.
func IsRedirectStatus ¶
IsRedirectStatus reports whether a status code is one that carries a Location.
304 is excluded: it is a cache validator, not a redirect, and has no destination to show. Shared so the write path (which decides what to store in response_location) and the read path (which decides what to render) can never disagree about what counts as a redirect — a disagreement whose symptom is a blank destination rather than an error.
func IsSQLiteFile ¶
IsSQLiteFile reports whether the file at path is a SQLite 3 database, detected by its magic header rather than its extension — so a .sqlite/.sqlite3/.db or a bare filename all resolve correctly. Files shorter than the header, or with any other leading bytes (e.g. a JSONL export), read as not-SQLite; a genuine open/read error is surfaced.
func IsValidFindingSeverity ¶
IsValidFindingSeverity reports whether s is a recognised Finding severity level.
func IsValidFindingStatus ¶
IsValidFindingStatus reports whether s is a recognised Finding lifecycle status.
func LikeContains ¶
LikeContains renders term as a pattern matching any row that CONTAINS it as a literal substring. This is the shape almost every search filter wants.
func LikeGlob ¶
LikeGlob renders a user-facing glob as a LIKE pattern, where `*` is the only wildcard. It backs the filters documented as accepting wildcards (--host, --path): `*` becomes `%`, and every other metacharacter — including a `%` or `_` the user typed — stays literal.
The caller decides whether a pattern without a `*` means "contains" or "equals"; this function only translates.
func ListTables ¶
ListTables returns the names of all user tables in the database.
func ParameterTypeFromParamType ¶
ParameterTypeFromParamType converts ParamType to database parameter type string
func PathToTemplate ¶
PathToTemplate normalizes a URL path for related-record lookup by replacing dynamic path segments (IDs, UUIDs, tokens) with a wildcard "*". Static segments and version segments (v1, v2, v10) are preserved. The resulting template uses "*" which callers can replace with "%" for SQL LIKE.
Segments are replaced if they match any of:
- Pure numeric: `^\d+$`
- UUID: `^[0-9a-f]{8}-...-[0-9a-f]{12}$` (case-insensitive)
- All-hex ≥ 8 chars: `^[0-9a-f]{8,}$` (case-insensitive)
- Long alphanumeric ≥ 20 chars (token-like): `^[A-Za-z0-9_-]{20,}$`
Version-like segments (v1, v2, v10) are always kept as-is.
func QueryGenericTable ¶
func QueryGenericTable(ctx context.Context, db *DB, tableName string, limit, offset int) ([]map[string]interface{}, []string, int64, error)
QueryGenericTable runs a paginated SELECT * on the given table. Returns rows as ordered maps, column names, and total row count. The tableName must be validated against ListTables before calling.
func QueryGenericTableFiltered ¶
func QueryGenericTableFiltered(ctx context.Context, db *DB, tableName string, opts GenericQueryOptions) ([]map[string]interface{}, []string, int64, error)
QueryGenericTableFiltered runs a filtered, sorted, paginated SELECT on the given table. The tableName and all column references are validated before use.
func RecordToHttpRequestResponse ¶
func RecordToHttpRequestResponse(record *HTTPRecord) (*httpmsg.HttpRequestResponse, error)
RecordToHttpRequestResponse converts an HTTPRecord back to HttpRequestResponse. Exported for use by the agent input normalizer and other packages.
func ResolveHostnameNow ¶
ResolveHostnameNow resolves hostname synchronously and caches the answer, returning the full answer. Returns the cached value when one is fresh.
This is the DNS-prefetch entry point for a host sweep, where DNS is part of the ANSWER rather than incidental metadata: the probe phase resolves its whole target list through here (bounded, before any HTTP) so every record it writes carries a complete A/AAAA/CNAME set. Every other path keeps using the background best-effort resolver, because on a normal scan's write path a blocking lookup would stall the record writer for a full DNS timeout per dead host.
ctx cancels the WAIT, not the lookup: a resolution in flight is shared with every other caller asking the same question, so one caller walking away must not take the answer with it. The lookup itself is bounded by dnsLookupTimeout.
func UpdateGenericRecord ¶
func UpdateGenericRecord(ctx context.Context, db *DB, tableName, pkValue string, fields map[string]interface{}) error
UpdateGenericRecord updates a record by primary key. fields is a map of column_name -> new_value. PK columns cannot be updated.
func ValidateTableName ¶
ValidateTableName checks that the given name is in the list of real tables.
func WithFacts ¶
func WithFacts(src *StoredHostFacts, r *HTTPRecord) any
WithFacts pairs a record with what src observed about its host. Returns the bare record when nothing was observed, so a process that probed nothing emits exactly the bytes it always did. A nil src answers from the process caches.
func WithLikeEscape ¶
WithLikeEscape appends the ESCAPE clause to every `LIKE ?` placeholder in a predicate.
It is a rewrite rather than 30 hand-edited SQL strings on purpose. The predicates in this package span multi-line raw strings with up to twelve placeholders each, and an ESCAPE clause missed on one branch is a filter that silently keeps the old over-matching behavior — the exact failure mode this whole file exists to remove. Rewriting makes the guarantee positional: if the text says LIKE ?, it carries an ESCAPE.
Call it once per predicate at package level where possible; the per-query cost is a single pass over a short constant string.
func WithMergeLock ¶
WithMergeLock runs fn while holding a best-effort cross-process advisory lock for destPath, serializing concurrent merges into the same destination database. The lock is an O_EXCL sentinel file (<destPath>.merge-lock), which is portable across platforms and needs no syscall-level file locking.
It is intentionally best-effort: if the lock cannot be acquired within timeout, fn runs anyway and relies on SQLite's own busy_timeout + the merge's retry-on-busy loop for correctness. The lock is an optimization that keeps staggered finishers from contending, not a correctness requirement, so a stale or contended lock degrades to DB-level serialization rather than failing the merge.
Types ¶
type AgentFindingCandidate ¶
type AgentFindingCandidate struct {
bun.BaseModel `bun:"table:agent_finding_candidates,alias:acand" json:"-"`
ID int64 `bun:"id,pk,autoincrement" json:"id"`
UUID string `bun:"uuid,notnull,unique" json:"uuid"`
AgenticScanUUID string `bun:"agentic_scan_uuid,nullzero" json:"agentic_scan_uuid,omitempty"`
ProjectUUID string `bun:"project_uuid,nullzero" json:"project_uuid,omitempty"`
SectionUUID string `bun:"section_uuid,nullzero" json:"section_uuid,omitempty"`
Title string `bun:"title,nullzero" json:"title,omitempty"`
Severity string `bun:"severity,nullzero" json:"severity,omitempty"`
Description string `bun:"description,nullzero" json:"description,omitempty"`
Remediation string `bun:"remediation,nullzero" json:"remediation,omitempty"`
CWEID string `bun:"cwe_id,nullzero" json:"cwe_id,omitempty"`
SourceFile string `bun:"source_file,nullzero" json:"source_file,omitempty"`
URL string `bun:"url,nullzero" json:"url,omitempty"`
Hostname string `bun:"hostname,nullzero" json:"hostname,omitempty"`
Confidence string `bun:"confidence,nullzero" json:"confidence,omitempty"`
Class string `bun:"class,nullzero" json:"class,omitempty"` // idor | xss | sqli | ssrf | rce | auth | other
Status string `bun:"status,notnull,default:'proposed'" json:"status"` // proposed | verifying | confirmed | rejected | needs_evidence
VerdictReason string `bun:"verdict_reason,nullzero" json:"verdict_reason,omitempty"`
EvidenceGrade string `bun:"evidence_grade,nullzero" json:"evidence_grade,omitempty"`
RecordUUIDs []string `bun:"record_uuids,type:jsonb,nullzero" json:"record_uuids,omitempty"`
OASTIDs []string `bun:"oast_ids,type:jsonb,nullzero" json:"oast_ids,omitempty"`
Request string `bun:"request,nullzero" json:"request,omitempty"`
Response string `bun:"response,nullzero" json:"response,omitempty"`
DedupHash string `bun:"dedup_hash,notnull,default:''" json:"dedup_hash"`
PromotedFindingID int64 `bun:"promoted_finding_id,notnull,default:0" json:"promoted_finding_id"`
Tags []string `bun:"tags,type:jsonb,nullzero" json:"tags,omitempty"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
VerifiedAt time.Time `bun:"verified_at,nullzero" json:"verified_at,omitempty"`
}
AgentFindingCandidate is a proposed finding awaiting fresh-context verification in the durable-autopilot verify-before-promote pipeline. The operator's propose_candidate tool writes rows here (status=proposed); a skeptic verifier grades them, and confirmed ones are promoted into a real Finding (promoted_finding_id set, status=confirmed). Rows are only written when agent.olium.autopilot_mode != legacy. Cascade-deleted with the parent agentic scan.
type AgentSection ¶
type AgentSection struct {
bun.BaseModel `bun:"table:agent_sections,alias:asec" json:"-"`
ID int64 `bun:"id,pk,autoincrement" json:"id"`
UUID string `bun:"uuid,notnull,unique" json:"uuid"`
AgenticScanUUID string `bun:"agentic_scan_uuid,nullzero" json:"agentic_scan_uuid,omitempty"`
ProjectUUID string `bun:"project_uuid,nullzero" json:"project_uuid,omitempty"`
Seq int `bun:"seq,notnull,default:0" json:"seq"`
Kind string `bun:"kind,nullzero" json:"kind,omitempty"`
Status string `bun:"status,notnull,default:'running'" json:"status"` // running | completed | interrupted
Task string `bun:"task,nullzero" json:"task,omitempty"`
ClosingSummary string `bun:"closing_summary,nullzero" json:"closing_summary,omitempty"`
RotationReason string `bun:"rotation_reason,nullzero" json:"rotation_reason,omitempty"`
TurnCount int `bun:"turn_count,notnull,default:0" json:"turn_count"`
InputTokens int64 `bun:"input_tokens,notnull,default:0" json:"input_tokens"`
OutputTokens int64 `bun:"output_tokens,notnull,default:0" json:"output_tokens"`
ErrorMessage string `bun:"error_message,nullzero" json:"error_message,omitempty"`
StartedAt time.Time `bun:"started_at,nullzero" json:"started_at,omitempty"`
EndedAt time.Time `bun:"ended_at,nullzero" json:"ended_at,omitempty"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
}
AgentSection represents one bounded operator section within a durable autopilot run (an engine Reset() + reconstructed-brief cycle). Rows are only written when agent.olium.autopilot_mode != legacy; legacy runs never touch this table. Cascade-deleted with the parent agentic scan.
type AgenticScan ¶
type AgenticScan struct {
bun.BaseModel `bun:"table:agentic_scans,alias:ar" json:"-"`
ID int64 `bun:"id,pk,autoincrement" json:"id"`
UUID string `bun:"uuid,notnull,unique" json:"uuid"`
ProjectUUID string `bun:"project_uuid,notnull" json:"project_uuid"`
ScanUUID string `bun:"scan_uuid,nullzero" json:"scan_uuid,omitempty"`
// Config
Mode string `bun:"mode,notnull" json:"mode"` // query, autopilot, pipeline, scan
AgentName string `bun:"agent_name,notnull" json:"agent_name"`
Protocol string `bun:"protocol,nullzero" json:"protocol,omitempty"` // engine identifier — currently always "olium-engine" (subprocess SDK backends removed)
Model string `bun:"model,nullzero" json:"model,omitempty"` // model name used for this run (for cost audit)
InputRaw string `bun:"input_raw,nullzero" json:"input_raw,omitempty"`
InputType string `bun:"input_type,nullzero" json:"input_type,omitempty"` // url, curl, burp, raw, record_uuid
TargetURL string `bun:"target_url,nullzero" json:"target_url,omitempty"`
VulnType string `bun:"vuln_type,nullzero" json:"vuln_type,omitempty"`
ModuleNames []string `bun:"module_names,type:jsonb,nullzero" json:"module_names,omitempty"`
TemplateID string `bun:"template_id,nullzero" json:"template_id,omitempty"`
// Execution
Status string `bun:"status,notnull,default:'pending'" json:"status"` // pending, running, completed, failed, cancelled
CurrentPhase string `bun:"current_phase,nullzero" json:"current_phase,omitempty"`
PhasesRun []string `bun:"phases_run,type:jsonb,nullzero" json:"phases_run,omitempty"`
// Results
FindingCount int `bun:"finding_count,default:0" json:"finding_count"`
RecordCount int `bun:"record_count,default:0" json:"record_count"`
SavedCount int `bun:"saved_count,default:0" json:"saved_count"`
// Agent context
SourcePath string `bun:"source_path,nullzero" json:"source_path,omitempty"` // source code path used
SourceType string `bun:"source_type,nullzero" json:"source_type,omitempty"` // local, git-url, gcs
TokenUsage map[string]interface{} `bun:"token_usage,type:jsonb,nullzero" json:"token_usage,omitempty"` // input/output token counts per phase
TotalInputTokens int64 `bun:"total_input_tokens,default:0" json:"total_input_tokens"`
TotalOutputTokens int64 `bun:"total_output_tokens,default:0" json:"total_output_tokens"`
EstimatedCostUSD float64 `bun:"estimated_cost_usd,default:0" json:"estimated_cost_usd"`
RetryCount int `bun:"retry_count,default:0" json:"retry_count"`
ParentAgenticScanUUID string `bun:"parent_run_uuid,nullzero" json:"parent_agentic_scan_uuid,omitempty"` // for swarm sub-runs
InputRecordCount int `bun:"input_record_count,default:0" json:"input_record_count"`
// Agent session ID (for resume)
SessionID string `bun:"session_id,nullzero" json:"session_id,omitempty"`
// Filesystem path to the session directory holding agent artifacts
// (output.md, audit-stream.jsonl, extensions/, etc.). Populated by the
// server handlers so API clients can locate run outputs.
SessionDir string `bun:"session_dir,nullzero" json:"session_dir,omitempty"`
// Debug (stored as JSON text blobs)
AttackPlan string `bun:"attack_plan,nullzero" json:"attack_plan,omitempty"`
TriageResult string `bun:"triage_result,nullzero" json:"triage_result,omitempty"`
PromptSent string `bun:"prompt_sent,nullzero" json:"prompt_sent,omitempty"`
AgentRawOutput string `bun:"agent_raw_output,nullzero" json:"agent_raw_output,omitempty"`
ErrorMessage string `bun:"error_message,nullzero" json:"error_message,omitempty"`
// Pipeline/scan result (JSON blob for full result objects)
ResultJSON string `bun:"result_json,nullzero" json:"result_json,omitempty"`
StorageURL string `bun:"storage_url,nullzero" json:"storage_url,omitempty"`
// Timing
StartedAt time.Time `bun:"started_at,nullzero" json:"started_at,omitempty"`
CompletedAt time.Time `bun:"completed_at,nullzero" json:"completed_at,omitempty"`
DurationMs int64 `bun:"duration_ms,default:0" json:"duration_ms"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
}
AgenticScan represents a single agent execution for debugging and status tracking. It replaces the in-memory agent run status map with persistent DB storage.
type AnalysisArtifact ¶
type AnalysisArtifact struct {
bun.BaseModel `bun:"table:analysis_artifacts,alias:aa" json:"-"`
ID int64 `bun:"id,pk,autoincrement" json:"id"`
ProjectUUID string `bun:"project_uuid,notnull" json:"project_uuid"`
ScanUUID string `bun:"scan_uuid,nullzero" json:"scan_uuid,omitempty"`
HTTPRecordUUID string `bun:"http_record_uuid,notnull" json:"http_record_uuid"`
Kind string `bun:"kind,notnull" json:"kind"`
Filename string `bun:"filename,nullzero" json:"filename,omitempty"`
MediaType string `bun:"media_type,nullzero" json:"media_type,omitempty"`
SHA256 string `bun:"sha256,notnull" json:"sha256"`
ByteLength int64 `bun:"byte_length,notnull" json:"byte_length"`
Content []byte `bun:"content,type:bytea,notnull" json:"content,omitempty"`
Metadata string `bun:"metadata,type:jsonb,nullzero" json:"metadata,omitempty"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
}
AnalysisArtifact stores immutable derived content linked to an HTTP record. It deliberately lives beside, rather than inside, HTTPRecord so analysis can never replace the raw evidence captured from the target.
type AuthenticationHostname ¶
type AuthenticationHostname struct {
bun.BaseModel `bun:"table:authentication_hostnames,alias:sh" json:"-"`
ID int64 `bun:"id,pk,autoincrement" json:"id"`
ProjectUUID string `bun:"project_uuid,notnull" json:"project_uuid"`
ScanUUID string `bun:"scan_uuid,nullzero" json:"scan_uuid,omitempty"`
Hostname string `bun:"hostname,notnull" json:"hostname"`
SessionName string `bun:"session_name,notnull" json:"session_name"`
SessionRole string `bun:"session_role,nullzero,default:''" json:"session_role,omitempty"`
Position int `bun:"position,default:0" json:"position"`
// Primary session token (JWT, cookie value, API key) for quick access.
SessionToken string `bun:"session_token,nullzero" json:"session_token,omitempty"`
// Static auth headers (JSON map)
Headers map[string]string `bun:"headers,type:jsonb,nullzero" json:"headers,omitempty"`
// Login flow fields (flat for queryability)
LoginURL string `bun:"login_url,nullzero" json:"login_url,omitempty"`
LoginMethod string `bun:"login_method,nullzero" json:"login_method,omitempty"`
LoginContentType string `bun:"login_content_type,nullzero" json:"login_content_type,omitempty"`
LoginBody string `bun:"login_body,nullzero" json:"login_body,omitempty"`
LoginRequest string `bun:"login_request,nullzero" json:"login_request,omitempty"`
LoginResponse string `bun:"login_response,nullzero" json:"login_response,omitempty"`
// Extract rules (JSON array)
ExtractRules string `bun:"extract_rules,type:jsonb,nullzero" json:"extract_rules,omitempty"`
Source string `bun:"source,nullzero,default:''" json:"source,omitempty"`
HydratedAt *time.Time `bun:"hydrated_at,nullzero" json:"hydrated_at,omitempty"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp" json:"updated_at"`
}
AuthenticationHostname persists per-hostname session auth configs in the DB.
func SessionToAuthenticationHostname ¶
func SessionToAuthenticationHostname(s *authentication.Session, position int) *AuthenticationHostname
SessionToAuthenticationHostname converts a native authentication.Session to a DB AuthenticationHostname row. The caller must set ProjectUUID, Hostname, and optionally ScanUUID on the returned row.
func SessionsToAuthenticationHostnames ¶
func SessionsToAuthenticationHostnames(sessions []*authentication.Session, projectUUID, hostname string) []*AuthenticationHostname
SessionsToAuthenticationHostnames converts a slice of authentication.Session objects to DB rows for a given hostname. Sets ProjectUUID and Hostname on each row.
type ColumnInfo ¶
type ColumnInfo struct {
Name string `json:"name"`
Type string `json:"type"`
Nullable string `json:"nullable"`
Default string `json:"default,omitempty"`
}
ColumnInfo describes a single column in a database table.
func ListColumns ¶
ListColumns returns column metadata for the given table.
type DB ¶
DB wraps bun.DB with additional metadata
func NewDB ¶
func NewDB(cfg *config.DatabaseConfig) (*DB, error)
NewDB creates database connection based on config
func NewDBFromBun ¶
NewDBFromBun wraps an existing bun.DB for use in tests or external tooling.
func (*DB) CreateRecordIndexes ¶
CreateRecordIndexes builds the http_records read indexes. Idempotent (every statement is IF NOT EXISTS), so calling it on a store that never deferred is harmless.
func (*DB) CreateSchema ¶
CreateSchema creates all database tables and indexes if they don't exist.
It is a no-op on a read-only handle. The read commands call it on open to self-heal a fresh project database; against a source opened --read-only that would be a schema write to someone else's evidence, and a source genuinely missing the tables surfaces as a query error rather than being silently migrated into the current shape.
func (*DB) DeferRecordIndexes ¶
func (db *DB) DeferRecordIndexes()
DeferRecordIndexes tells CreateSchema to leave the http_records read indexes uncreated, so a bulk load runs against the primary key alone. The caller MUST call CreateRecordIndexes once loading is done — a store left without them is schema-complete but plans every read as a full scan.
Only for disposable stores that are filled once and then read (the --glob-db scratch merge). A persistent store must not defer: it is written to continuously, so there is no "after the load" to build them in.
func (*DB) EnsureSchemaCurrent ¶
EnsureSchemaCurrent brings an existing database up to date, doing NOTHING when it already is.
The "do nothing" path is the whole point. CreateSchema issues DDL, and DDL takes SQLite's WRITE lock — so calling it unconditionally on every open makes every read command queue behind any concurrent writer for up to busy_timeout (60s by default). Detecting staleness first is a read-only question, so the common case takes no lock at all.
A read-only handle cannot be migrated (mode=ro refuses DDL); CreateSchema's own read-only branch turns that into ErrSchemaOutdated naming the missing columns, so it is reached through the same call rather than special-cased here.
Indexes are deliberately not part of the check: they are a performance concern, not a correctness one, and the write commands still call CreateSchema directly. Only a missing TABLE or COLUMN can make a query fail outright.
func (*DB) GetStats ¶
func (db *DB) GetStats(ctx context.Context, filters QueryFilters) (*DatabaseStats, error)
GetStats retrieves database statistics
func (*DB) GetTopHosts ¶
func (db *DB) GetTopHosts(ctx context.Context, filters QueryFilters, limit int) ([]HostStats, error)
GetTopHosts retrieves top hosts by request count with per-host finding counts. It uses a constant TWO queries — one for the top hosts, one grouped finding-count query — instead of the previous 1 + N (a per-host count query), so its cost no longer grows with the requested host limit.
func (*DB) HasFTS ¶
HasFTS reports whether full-text search is available on this handle (FTS5 for SQLite, tsvector for PostgreSQL).
It DISCOVERS the answer rather than remembering whether this process created the index. It used to be a plain field assigned only by SeedDefaults, which made the answer depend on how the handle was opened rather than on what the database contains: `vigolium server` seeds and reported true, every read command (traffic, finding, db ls) does not seed and reported false on the very same file. Two commands then took different query paths over one database.
Discovery is lazy and cached: a handle that never searches pays nothing, and one that does pays a single sqlite_master lookup.
func (*DB) HasVigoliumSchema ¶
HasVigoliumSchema reports whether this store has been initialized as a vigolium database, by the presence of the core table every read touches.
It is the question "is this the right file?", which is NOT the same as "does it have rows": a foreign SQLite file and a scanned target both open cleanly, and only this distinguishes them before vigolium's tables get created inside the former.
func (*DB) Optimize ¶
Optimize refreshes the query-planner statistics so subsequent dedup and finding queries plan against real row counts. Call it after a bulk ingest phase (e.g. discovery) and before the heavy dedup passes; it also runs on Close and on the WAL checkpointer cadence. Best-effort — a failure degrades plans, never correctness.
SQLite uses `PRAGMA optimize` (incremental: only re-analyzes tables that changed enough to matter). PostgreSQL analyzes just the hot ingest tables rather than the whole database, so a short-lived CLI run against a shared server doesn't trigger a blocking full-DB ANALYZE on every close.
func (*DB) Path ¶
Path returns the resolved SQLite file backing this handle, or "" for a driver that is not a single file (Postgres).
It exists because DatabaseStats.Database.Path and .Size were declared and never filled: `db stats -j` reported `"path": "", "size": 0` for a store it had open and had just counted 60 records in. A consumer reading that to confirm WHICH database it was looking at — the whole reason the -j envelope carries db_path — was told nothing, in a field that looked like an answer.
func (*DB) SQLDB ¶
SQLDB returns the underlying *sql.DB. Used by callers that need a pinned *sql.Conn for connection-scoped operations (e.g. ATTACH DATABASE during a SQLite-to-SQLite merge) that must run on a single physical connection.
func (*DB) SeedDefaults ¶
SeedDefaults creates the default user and project if they don't exist. This is called during initialization to ensure CLI has a working project context.
type DBInputSource ¶
type DBInputSource struct {
// contains filtered or unexported fields
}
DBInputSource polls the database for HTTP records after the scan cursor and provides them as WorkItems. It implements source.InputSource.
func NewDBInputSource ¶
func NewDBInputSource(db *DB, repo *Repository, scanUUID string, pollInterval time.Duration) *DBInputSource
NewDBInputSource creates a new DBInputSource that polls for records after the scan cursor at the given interval.
func NewOneShotDBInputSource ¶
func NewOneShotDBInputSource(db *DB, repo *Repository, scanUUID string) *DBInputSource
NewOneShotDBInputSource creates a DBInputSource that returns io.EOF when no records remain after the cursor, instead of polling indefinitely.
func (*DBInputSource) Close ¶
func (s *DBInputSource) Close() error
Close stops the source. After Close, Next will return io.EOF.
func (*DBInputSource) IdleFor ¶
func (s *DBInputSource) IdleFor() time.Duration
IdleFor returns how long it has been since the source last observed a new row (or since creation, if none have arrived yet). Useful for status reporting to tell the user "scan is idle — server is still listening for more records."
func (*DBInputSource) Next ¶
Next returns the next record after the scan cursor as a WorkItem. It blocks (polling) until a record is available, the context is cancelled, or the source is closed.
func (*DBInputSource) WithHostScopes ¶
func (s *DBInputSource) WithHostScopes(hosts []HostTarget) *DBInputSource
WithHostScopes sets an in-scope origin filter so only records matching these (scheme, hostname, port) origins are returned. This keeps HTTP records from unrelated origins — including the same host on a different port left over from a previous scan (e.g. localhost:8080 when targeting localhost:3000) — out of the scan.
func (*DBInputSource) WithIdleTimeout ¶
func (s *DBInputSource) WithIdleTimeout(timeout time.Duration) *DBInputSource
WithIdleTimeout configures the source to return io.EOF after this long without any new rows arriving from the database. Only honored in polling mode (not oneShot). Zero (the default) keeps the original behavior — poll forever. Typical use: one-shot scan-on-receive where the caller wants the scan to terminate once the ingestion pipeline has settled.
func (*DBInputSource) WithIncludeSources ¶
func (s *DBInputSource) WithIncludeSources(sources []string) *DBInputSource
WithIncludeSources restricts the source to records whose http_records.source value is in the given list. Used by scan-on-receive shallow mode to exclude scanner-produced artefacts (source="finding", source="scanner", etc.) so the scan only processes user-ingested traffic and doesn't fan out across records that were created as byproducts of scanning itself. An empty slice disables the filter (default).
func (*DBInputSource) WithOnActivity ¶
func (s *DBInputSource) WithOnActivity(fn func(recordCount int, idleFor time.Duration, firstBatch bool)) *DBInputSource
WithOnActivity registers a callback invoked once per non-empty fetch batch. firstBatch is true for the very first batch ever returned by this source. idleFor is the duration the source had been quiet *before* this batch landed. Callers typically use this to print a "scan started / resumed" line so the user gets immediate feedback rather than waiting for the next status tick.
func (*DBInputSource) WithPageSize ¶
func (s *DBInputSource) WithPageSize(pageSize int) *DBInputSource
WithPageSize sets the number of records fetched from the database per batch.
type DatabaseInfo ¶
type DatabaseInfo struct {
Path string `json:"path"`
Driver string `json:"driver"`
Version string `json:"version,omitempty"`
Size int64 `json:"size"`
}
DatabaseInfo holds database metadata
type DatabaseStats ¶
type DatabaseStats struct {
Database DatabaseInfo `json:"database"`
Records RecordCounts `json:"records"`
Findings FindingsStats `json:"findings"`
HTTPMethods map[string]int64 `json:"http_methods"`
StatusCodes StatusCodeBreakdown `json:"status_codes"`
Performance PerformanceMetrics `json:"performance"`
DateRange DateRangeStats `json:"date_range"`
ScanSessions []ScanSessionStats `json:"scan_sessions,omitempty"`
TopHosts []HostStats `json:"top_hosts,omitempty"`
}
DatabaseStats holds database statistics
type DateRangeStats ¶
type DateRangeStats struct {
FirstRequest time.Time `json:"first_request"`
LastRequest time.Time `json:"last_request"`
Duration time.Duration `json:"duration"`
}
DateRangeStats holds date range information
type DeleteBuilder ¶
type DeleteBuilder struct {
// contains filtered or unexported fields
}
DeleteBuilder builds delete queries with filters
func NewDeleteBuilder ¶
func NewDeleteBuilder(db *DB, filters QueryFilters) *DeleteBuilder
NewDeleteBuilder creates a new delete builder
func (*DeleteBuilder) DeleteAllTables ¶
func (db *DeleteBuilder) DeleteAllTables(ctx context.Context, dryRun bool) (map[string]int64, error)
DeleteAllTables deletes all data from every data table.
func (*DeleteBuilder) DeleteFindings ¶
DeleteFindings deletes findings matching the configured filters. If dryRun is true, returns the count without deleting.
func (*DeleteBuilder) DeleteOrphans ¶
DeleteOrphans deletes orphaned findings (findings where none of the http_record_uuids exist in http_records).
It honors filters.ProjectUUID. Orphan cleanup reads as store maintenance, but the rows it removes belong to projects: an unscoped sweep run from one engagement deleted another engagement's findings, which is the same cross-project leak the scoped delete paths exist to prevent. With no project filter set the behavior is unchanged and the whole store is swept.
func (*DeleteBuilder) DeleteRecords ¶
DeleteRecords deletes the HTTP records matching filters, along with the findings whose only evidence they were.
The order below is the policy, and it is not the obvious one. Deleting every finding that merely *touches* a deleted record destroys findings that also cite records the caller is keeping — one module reporting the same issue across five endpoints would lose the whole finding because one endpoint was pruned. So the junction is narrowed first, and a finding is removed only once nothing it links to survives:
- collect the findings linked to the doomed records (bounded by findings touched, not by records deleted);
- drop those records' junction rows, so the junction describes surviving evidence only;
- delete the records;
- delete the collected findings that now link to nothing.
Step 4 is scoped to the collected candidates rather than sweeping every evidence-less finding, because a finding with no HTTP record is normal — audit and source-scan findings never have one, and this is not the command that removes them (see DeleteOrphans).
Every IN() list is chunked (see SQLChunkSize). The filters can select an unbounded number of records, and bun renders a list as SQL literals rather than bound parameters — so an unchunked delete builds one statement holding every uuid as text, tens of megabytes of it on a large corpus.
func (*DeleteBuilder) DeleteTable ¶
func (db *DeleteBuilder) DeleteTable(ctx context.Context, tableName string, dryRun bool) (int64, error)
DeleteTable deletes all rows from a specific table.
type DeparosStatusPolicy ¶
type DeparosStatusPolicy struct {
// KeepOnePerHost statuses collapse to a single representative per
// (hostname, status_code) — the shortest path. Use where "this host has one"
// is enough signal: authentication/authorization walls (401/403) and pacing
// (429). Route identity across paths is intentionally NOT preserved here — an
// authz wall is a host-level fact, and keeping every 403 would reintroduce
// catch-all floods.
KeepOnePerHost []int
// KeepPerPath statuses preserve every DISTINCT path (only exact-duplicate
// paths collapse), capped at PerPathCap records per (hostname, status_code) to
// bound pathological floods. Use for "endpoint exists" evidence where the path
// itself is the attack surface: 405/415/422 mean the route is real but the
// method/payload was wrong, so dynamic assessment should retry it with the
// right method. Routers only emit these for matched routes, so fuzz-flood risk
// is low — the cap is a backstop, not the primary control.
KeepPerPath []int
// PerPathCap bounds the KeepPerPath tier per (hostname, status_code). <=0
// disables the cap (keep all distinct paths).
PerPathCap int
}
DeparosStatusPolicy controls how discovery client-error (4xx) records are retained. It replaces a blanket "drop all 4xx" with priority tiers so that distinct route identities carrying real attack-surface signal survive, while fuzz noise (nonexistent paths → 404/400) is still pruned. Statuses in neither tier are dropped entirely.
type EmbeddedParam ¶
type EmbeddedParam struct {
Name string `json:"name"`
Value string `json:"value,omitempty"`
Type string `json:"type"`
NameStart int `json:"name_start,omitempty"`
NameEnd int `json:"name_end,omitempty"`
ValueStart int `json:"value_start,omitempty"`
ValueEnd int `json:"value_end,omitempty"`
Metadata string `json:"metadata,omitempty"`
}
EmbeddedParam represents a parameter stored as JSON within HTTPRecord
type Finding ¶
type Finding struct {
bun.BaseModel `bun:"table:findings,alias:f" json:"-"`
ID int64 `bun:"id,pk,autoincrement" json:"id"`
ProjectUUID string `bun:"project_uuid,notnull" json:"project_uuid"`
// UUID references to HTTPRecords (not FK, allows flexibility)
HTTPRecordUUIDs []string `bun:"http_record_uuids,type:jsonb,notnull" json:"http_record_uuids"`
// Scan reference (soft, no FK)
ScanUUID string `bun:"scan_uuid,nullzero" json:"scan_uuid,omitempty"`
AgenticScanUUID string `bun:"agentic_scan_uuid,nullzero" json:"agentic_scan_uuid,omitempty"` // link to agentic_scan that produced this finding
// Denormalized target info (for fast display/filtering without joining)
URL string `bun:"url,nullzero" json:"url,omitempty"`
Hostname string `bun:"hostname,nullzero" json:"hostname,omitempty"`
// Module info
ModuleID string `bun:"module_id,notnull" json:"module_id"`
ModuleName string `bun:"module_name,notnull" json:"module_name"`
ModuleType string `bun:"module_type,nullzero" json:"module_type,omitempty"`
FindingSource string `bun:"finding_source,nullzero" json:"finding_source,omitempty"`
RecordKind string `bun:"record_kind,notnull,default:'finding'" json:"record_kind,omitempty"`
EvidenceGrade string `bun:"evidence_grade,nullzero" json:"evidence_grade,omitempty"`
ModuleShort string `bun:"module_short,nullzero" json:"module_short,omitempty"`
Description string `bun:"description,nullzero" json:"description,omitempty"`
Severity string `bun:"severity,notnull" json:"severity"`
Confidence string `bun:"confidence,notnull,default:'firm'" json:"confidence"`
Tags []string `bun:"tags,type:jsonb,nullzero" json:"tags,omitempty"`
// Finding lifecycle
Status string `bun:"status,nullzero,default:'triaged'" json:"status,omitempty"` // draft, triaged, false_positive, accepted_risk, fixed
Remediation string `bun:"remediation,nullzero" json:"remediation,omitempty"`
// Classification
CWEID string `bun:"cwe_id,nullzero" json:"cwe_id,omitempty"`
CVSSScore float64 `bun:"cvss_score,default:0" json:"cvss_score"`
// Source info (for audit findings)
SourceFile string `bun:"source_file,nullzero" json:"source_file,omitempty"`
RepoName string `bun:"repo_name,nullzero" json:"repo_name,omitempty"` // repository name or URL for audit findings
MatchedAt []string `bun:"matched_at,type:jsonb,nullzero" json:"matched_at,omitempty"`
ExtractedResults []string `bun:"extracted_results,type:jsonb,nullzero" json:"extracted_results,omitempty"`
AdditionalEvidence []string `bun:"additional_evidence,type:jsonb,nullzero" json:"additional_evidence,omitempty"`
Request string `bun:"request,nullzero" json:"request,omitempty"`
Response string `bun:"response,nullzero" json:"response,omitempty"`
FindingHash string `bun:"finding_hash,notnull" json:"finding_hash"`
FoundAt time.Time `bun:"found_at,notnull,default:current_timestamp" json:"found_at"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
}
Finding represents a vulnerability finding (no FK, soft UUID reference)
func (*Finding) FromResultEvent ¶
func (f *Finding) FromResultEvent(event *output.ResultEvent) error
FromResultEvent converts output.ResultEvent to Finding
type FindingSaveResult ¶
type FindingSaveResult struct {
Inserted bool // a new finding row was written (false on a dedup-append to an existing finding)
Err error // non-nil if this finding failed to save
}
FindingSaveResult reports the outcome of persisting one finding in a batched direct save, aligned by index with the input slice passed to SaveFindingsDirectBatch.
type FindingWrite ¶
type FindingWrite struct {
Event *output.ResultEvent
HTTPRecordUUIDs []string
ScanUUID string
ProjectUUID string
}
FindingWrite bundles the arguments needed to persist a single finding, allowing a batch of findings to be coalesced into one transaction. See Repository.SaveFindingsBatch.
type FindingWriter ¶
type FindingWriter struct {
// contains filtered or unexported fields
}
FindingWriter decouples finding persistence from scan workers. SaveFinding is a multi-statement, dedup-aware operation; calling it synchronously on every worker blocks the worker on a database round-trip. FindingWriter funnels findings through a single background goroutine that coalesces them into batch transactions (one fsync for many findings) via Repository.SaveFindingsBatch.
Findings are low-volume relative to HTTP records, so a single writer goroutine keeps up while also eliminating write contention between concurrent finding transactions. This mirrors RecordWriter's lifecycle (buffered channel, background flush, bounded shutdown drain) but is fire-and-forget: the result UUID is never needed by the caller, so Save does not block on persistence.
func NewFindingWriter ¶
func NewFindingWriter(repo *Repository, cfg FindingWriterConfig) *FindingWriter
NewFindingWriter creates and starts a FindingWriter. Call Close() to flush remaining findings and stop the background goroutine.
func (*FindingWriter) Close ¶
func (w *FindingWriter) Close()
Close stops accepting new findings, flushes the buffer, and returns. After Close, Save persists synchronously. The drain is bounded by FlushTimeout so a wedged database can't hang Close forever.
func (*FindingWriter) Metrics ¶
func (w *FindingWriter) Metrics() FindingWriterMetrics
Metrics returns a snapshot of the writer's counters.
func (*FindingWriter) Save ¶
func (w *FindingWriter) Save(ctx context.Context, event *output.ResultEvent, recordUUIDs []string, scanUUID, projectUUID string) error
Save enqueues a finding for batched persistence. It does not block on the database: if the buffer is full or the writer is shutting down, the finding is persisted synchronously so nothing is dropped. Safe for concurrent callers.
type FindingWriterConfig ¶
type FindingWriterConfig struct {
// BufferSize is the channel capacity. When full, Save persists inline rather
// than blocking the scan worker. Default: 1024.
BufferSize int
// BatchSize is the maximum number of findings coalesced into one transaction.
// Default: 64.
BatchSize int
// FlushInterval is the maximum time a finding waits in the buffer before
// being flushed, even if the batch isn't full. Default: 50ms.
FlushInterval time.Duration
// FlushTimeout bounds the shutdown drain so a wedged database can't block
// Close() forever. Steady-state flushes use an uncancellable context.
// Default: 2m.
FlushTimeout time.Duration
}
FindingWriterConfig configures the batching behavior of FindingWriter.
type FindingWriterMetrics ¶
type FindingWriterMetrics struct {
Enqueued int64 // findings handed to the background writer
Written int64 // findings persisted by the background writer
Inline int64 // findings persisted synchronously (buffer full / shutting down)
Errors int64 // findings in a flush that ultimately failed to persist
}
FindingWriterMetrics exposes counters for monitoring.
type FindingsQueryBuilder ¶
type FindingsQueryBuilder struct {
// contains filtered or unexported fields
}
FindingsQueryBuilder builds filtered queries for the findings table
func NewFindingsQueryBuilder ¶
func NewFindingsQueryBuilder(db *DB, filters QueryFilters) *FindingsQueryBuilder
NewFindingsQueryBuilder creates a new findings query builder
func (*FindingsQueryBuilder) Count ¶
func (fqb *FindingsQueryBuilder) Count(ctx context.Context) (int64, error)
Count returns total number of matching findings
func (*FindingsQueryBuilder) Execute ¶
func (fqb *FindingsQueryBuilder) Execute(ctx context.Context) ([]*Finding, error)
Execute executes the query and returns findings
func (*FindingsQueryBuilder) ExecuteWithCount ¶
ExecuteWithCount runs the filtered query and returns matching findings alongside the total unfiltered count, in a single round-trip via Bun's ScanAndCount. Use this when callers need both the page and the total (paginated views) — saves one DB roundtrip vs. Execute + Count.
type FindingsStats ¶
type FindingsStats struct {
BySeverity map[string]int64 `json:"by_severity"`
Total int64 `json:"total"`
}
FindingsStats holds finding statistics by severity
type GenericFilter ¶
type GenericFilter struct {
Column string
Operator string // "eq", "like", "gt", "lt", "gte", "lte", "in", "neq"
Value string
}
GenericFilter represents a single column filter.
type GenericQueryOptions ¶
type GenericQueryOptions struct {
Limit int
Offset int
SortBy string // Column to sort by
SortAsc bool
Columns []string // Column whitelist (empty = all)
SearchTerm string // Fuzzy search across text columns
Truncate int // Truncate []byte fields to this length (0 = no truncation)
Filters []GenericFilter
}
GenericQueryOptions holds options for filtered, sorted, paginated generic queries.
type GroupFindingOptions ¶
type GroupFindingOptions struct {
// PerHost groups within each hostname — a value seen on two hosts stays two
// findings. When false, grouping is project-wide regardless of host.
PerHost bool
// Tags, when non-empty, restricts grouping to findings carrying at least one
// matching tag (case-insensitive).
Tags []string
// ByModule lists module IDs whose findings collapse to a single finding per
// (module, severity[, host]) regardless of the per-URL extracted value — for
// modules that fire once per asset (e.g. sourcemap-detect, one .map filename
// per JS bundle). These bypass the value-identity requirement and the Tags
// gate; module + severity is the guardrail.
ByModule []string
// ByRule lists module IDs whose findings collapse per (module, rule_name,
// severity[, host]) regardless of value — like ByModule, but the rule
// (module_name) stays part of the key, so DISTINCT rules remain distinct
// findings while repeats of the SAME rule fold together. This is for
// secret-detect, whose one module_id carries many rule_names (secret-scan
// rules): a "Looker Client ID" rule matching every chunk hash in a minified
// bundle's content-hash map yields dozens of identical findings differing only
// by value, which collapse to one finding (all values unioned on), while a
// genuinely different secret — an AWS key, a Slack token — keeps its own row.
ByRule []string
// BundleSuspect lists module IDs whose GENERIC Suspect-severity findings
// collapse by (module, severity[, host]) — the rule dropped from the key — so
// the family-less low-signal noise on a host folds into ONE bundle. Only
// findings carrying output.SuspectBundleTag qualify (secret-detect sets it on
// its generic-namespace rules); a NAMED provider family at Suspect severity is
// left untagged and stays per-rule via ByRule, so distinct families are never
// merged into one rollup. The module's higher-severity findings also stay
// per-rule. The bundle survivor's module_name is relabeled to
// suspectBundleFindingName and its description to suspectBundleDescription so it
// isn't mislabeled with just the first rule's name/text. Default: secret-detect.
BundleSuspect []string
// MaxURLs caps the merged matched-URL list on the survivor (0 = unlimited).
MaxURLs int
// Hostnames, when non-empty, scopes the pass to findings on those hosts. The
// dynamic-assessment feedback loop sets it to the hosts being scanned so a
// growing findings table isn't loaded in full each round. Empty = project-wide.
Hostnames []string
}
GroupFindingOptions configures value-based finding grouping (see GroupFindingsByValue).
type HTTPRecord ¶
type HTTPRecord struct {
bun.BaseModel `bun:"table:http_records,alias:r" json:"-"`
UUID string `bun:"uuid,pk,notnull" json:"uuid"`
ProjectUUID string `bun:"project_uuid,notnull" json:"project_uuid"`
ScanUUID string `bun:"scan_uuid,nullzero" json:"scan_uuid,omitempty"` // link to scan that produced this record
// Host info (embedded, replaces hosts table)
Scheme string `bun:"scheme,notnull" json:"scheme"`
Hostname string `bun:"hostname,notnull" json:"hostname"`
Port int `bun:"port,notnull" json:"port"`
IP string `bun:"ip,nullzero" json:"ip,omitempty"` // resolved IP address the request went to (httpx: host_ip)
// Request fields
Method string `bun:"method,notnull" json:"method"`
Path string `bun:"path,notnull" json:"path"`
URL string `bun:"url,notnull" json:"url"`
HTTPVersion string `bun:"http_version,notnull" json:"http_version"`
RequestContentType string `bun:"request_content_type,nullzero" json:"request_content_type,omitempty"`
RequestContentLength int64 `bun:"request_content_length,default:0" json:"request_content_length"`
RawRequest []byte `bun:"raw_request,type:bytea,nullzero" json:"raw_request,omitempty"`
RequestHash string `bun:"request_hash,notnull" json:"request_hash"`
RequestAuthorization string `bun:"request_authorization,nullzero" json:"request_authorization,omitempty"`
// Response fields
StatusCode int `bun:"status_code,default:0" json:"status_code"`
StatusPhrase string `bun:"status_phrase,nullzero" json:"status_phrase,omitempty"`
ResponseHTTPVersion string `bun:"response_http_version,nullzero" json:"response_http_version,omitempty"`
ResponseContentType string `bun:"response_content_type,nullzero" json:"response_content_type,omitempty"`
ResponseContentLength int64 `bun:"response_content_length,default:0" json:"response_content_length"`
RawResponse []byte `bun:"raw_response,type:bytea,nullzero" json:"raw_response,omitempty"`
ResponseHash string `bun:"response_hash,nullzero" json:"response_hash,omitempty"`
ResponseNormHash string `bun:"response_norm_hash,nullzero" json:"response_norm_hash,omitempty"` // hash of the body with reflected URL/path + dynamic runs stripped, for reflected-URL-robust dedup
ResponseTimeMs int64 `bun:"response_time_ms,default:0" json:"response_time_ms"`
ResponseWords int64 `bun:"response_words,default:0" json:"response_words"`
HasResponse bool `bun:"has_response,notnull,default:false" json:"has_response"`
ResponseTitle string `bun:"response_title,nullzero" json:"response_title,omitempty"`
// ResponseLocation is the Location header of a 3xx response, stored verbatim
// — relative values included, because rewriting them to absolute would be
// this column asserting something the server did not say.
//
// It exists because the redirect tree needs exactly one header out of a
// response whose body it has no use for. Without a column the destination is
// only recoverable by reading raw_response back, which on a `--glob-db` read
// means reopening every source file the selected redirects came from and
// holding an 8 KiB prefix per row until rendering finishes. Parsing it once at
// ingestion and storing ~30 bytes replaces all of that.
//
// Empty means "no Location", which for a record written by this version is
// also the answer for a non-redirect. It does NOT distinguish "not backfilled"
// on a record written before this column existed — see
// redirectLocationsAreBackfilled for how the read path handles that.
ResponseLocation string `bun:"response_location,nullzero" json:"response_location,omitempty"`
// Parameters (JSON array, replaces http_parameters table)
Parameters []EmbeddedParam `bun:"parameters,type:jsonb,nullzero" json:"parameters,omitempty"`
// Timestamps
SentAt time.Time `bun:"sent_at,notnull,default:current_timestamp" json:"sent_at"`
ReceivedAt time.Time `bun:"received_at,nullzero" json:"received_at,omitempty"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
Source string `bun:"source,nullzero,default:''" json:"source,omitempty"`
// Analysis & enrichment
Technology []string `bun:"technology,type:jsonb,nullzero" json:"technology,omitempty"` // detected technologies/frameworks
ContentHash string `bun:"content_hash,nullzero" json:"content_hash,omitempty"` // hash of meaningful response content for change detection
IsAuthenticated bool `bun:"is_authenticated,notnull,default:false" json:"is_authenticated"` // whether request was sent with valid auth
ParentUUID string `bun:"parent_uuid,nullzero" json:"parent_uuid,omitempty"` // parent record UUID (crawl/spider parent)
// Risk labeling (populated by background analysis)
Remarks []string `bun:"remarks,type:jsonb,nullzero" json:"remarks,omitempty"`
RiskScore int `bun:"risk_score,default:0" json:"risk_score"`
// SurfaceScore is the deterministic attack-surface score (0-100) written by
// the surface_scoring passive module, which owns the signal set and the
// scale — see pkg/modules/passive/surface_scoring. It
// answers "is this record worth attacking" from properties of the record
// itself, and is reproducible across scans.
//
// Deliberately NOT merged into RiskScore: that column holds anomaly_ranking's
// rank-within-batch percentile, which measures statistical rarity against
// whichever records happened to be flushed alongside it. The two are different
// scales from different inputs; sharing one column would mean whichever module
// flushed last silently overwrote the other.
SurfaceScore int `bun:"surface_score,default:0" json:"surface_score"`
}
HTTPRecord represents a single HTTP request/response record (denormalized)
func (*HTTPRecord) EnrichFromHttpRequestResponse ¶
func (r *HTTPRecord) EnrichFromHttpRequestResponse(ctx *httpmsg.HttpRequestResponse) error
EnrichFromHttpRequestResponse fills everything PrepareIdentity left out: the owned raw bytes, all response-derived columns, request authorization, parameters, resolved IP, and timestamps. Run it only for a record that is actually going to be stored. Safe to call on a record whose identity half has already run; it does not recompute it.
func (*HTTPRecord) FromHttpRequestResponse ¶
func (r *HTTPRecord) FromHttpRequestResponse(ctx *httpmsg.HttpRequestResponse) error
FromHttpRequestResponse populates an HTTPRecord from httpmsg.HttpRequestResponse.
The record OWNS its RawRequest/RawResponse: both are copied out of the source message rather than aliased. This is a lifetime requirement, not a style choice. A converted record routinely outlives the call that produced it — RecordWriter.admit hands it to a shard channel and returns, the flush goroutine inserts it later, and Repository.emitRecordSaved passes it to the filesystem mirror, which writes it from yet another goroutine. Meanwhile the executor builds its responses over a POOLED buffer (see pkg/core, getResponseBuffer) that processItem returns to the pool as soon as it is done with the item. Aliasing therefore let a record be persisted or mirrored with another response's bytes whenever the write outlived the caller — on scan cancellation the caller stops waiting while the record is still queued, which is exactly when this happened.
The copy (bytes.Clone below) costs one memcpy per converted record, which is small next to the two SHA-256 passes below, and is only paid for records that get enriched at all.
Conversion is split in two halves, and a caller that deduplicates should use them separately rather than calling this: PrepareIdentity fills only the columns that decide WHICH row this is, and EnrichFromHttpRequestResponse does the expensive response-derived work. See PrepareIdentity.
func (*HTTPRecord) MarshalJSON ¶
func (r *HTTPRecord) MarshalJSON() ([]byte, error)
MarshalJSON preserves the external JSON contract by injecting four derived fields (request_headers, request_body, response_headers, response_body) alongside the stored struct fields.
func (*HTTPRecord) ParsedRequest ¶
func (r *HTTPRecord) ParsedRequest() *httpmsg.HttpRequest
ParsedRequest parses RawRequest into an httpmsg.HttpRequest. Returns nil when there is no raw request. Each call constructs a new parser; callers that need multiple fields should hoist the result into a local, or use parsedView for the standard four-field expansion.
func (*HTTPRecord) ParsedResponse ¶
func (r *HTTPRecord) ParsedResponse() *httpmsg.HttpResponse
ParsedResponse parses RawResponse into an httpmsg.HttpResponse, or nil when no raw response is present.
func (*HTTPRecord) ParsedView ¶
func (r *HTTPRecord) ParsedView() (reqHdrs, respHdrs map[string][]string, reqBody, respBody []byte)
ParsedView decodes RawRequest and RawResponse exactly once each and returns the four derived fields. Use this whenever a caller needs more than one derived field — calling individual accessors for each field re-parses.
When raw bytes are absent (e.g., list endpoints exclude raw_* columns), returned values are nil — that is what makes the JSON projection in HandleListRecords drop the four derived fields.
func (*HTTPRecord) PrepareIdentity ¶
func (r *HTTPRecord) PrepareIdentity(ctx *httpmsg.HttpRequestResponse) error
PrepareIdentity fills the columns that establish a record's identity — the ones recordDedupKey builds its key from and findDuplicateRecordUUIDs matches on — and nothing else. Cheap: a URL parse, a few header reads, and one SHA-256 over the raw request.
It exists so a duplicate can be recognized WITHOUT paying for enrichment. EnrichFromHttpRequestResponse runs two more passes over the response body (a normalized-body-hash regex, a word count), tokenizes HTML to pull out a title, and copies the raw bytes. Profiling a 64 KiB HTML response put ~57% of conversion CPU in the normalizing regex alone, and the whole conversion at ~2.6ms and ~786 KiB allocated. On duplicate-heavy ingestion — discovery and spidering re-encountering the same URLs, which is the case the dedup cache was added for — all of that was spent on records thrown away microseconds later.
Deliberately NOT here: hostname resolution. The IP is stored metadata, not identity, and scheduling it from this half would spawn a background DNS goroutine for every duplicate too.
Callers must set Source (and ProjectUUID) before building a dedup key from the result: recordDedupKey consults both.
func (*HTTPRecord) RedirectLocation ¶
func (r *HTTPRecord) RedirectLocation() string
RedirectLocation returns where this record's response points, or "" when it is not a redirect and when the destination is not recoverable.
The stored column answers first. Parsing raw_response is the fallback for records written before response_location existed, whose column is empty whether or not the response carried a Location — the two are indistinguishable, so a legacy redirect with no Location costs one parse of bytes already in hand.
It lives here, beside the write path that fills the column, so the question "where does this redirect go" has ONE answer. The read path used to carry its own copy of the parse, which is exactly the drift this file exists to prevent.
func (*HTTPRecord) RequestBodyBytes ¶
func (r *HTTPRecord) RequestBodyBytes() []byte
RequestBodyBytes returns the request body bytes parsed from RawRequest.
func (*HTTPRecord) RequestHeadersMap ¶
func (r *HTTPRecord) RequestHeadersMap() map[string][]string
RequestHeadersMap returns request headers as a map[name][]values.
func (*HTTPRecord) ResponseBodyBytes ¶
func (r *HTTPRecord) ResponseBodyBytes() []byte
ResponseBodyBytes returns the response body bytes parsed from RawResponse.
func (*HTTPRecord) ResponseHeadersMap ¶
func (r *HTTPRecord) ResponseHeadersMap() map[string][]string
ResponseHeadersMap returns response headers as a map[name][]values.
type HostFacts ¶
type HostFacts struct {
A []string `json:"a,omitempty"`
AAAA []string `json:"aaaa,omitempty"`
CNAME []string `json:"cname,omitempty"`
TLS *tlsprobe.Info `json:"tls,omitempty"`
}
HostFacts are the per-host observations a run collects alongside its records: the full DNS answer and, under --tls-probe, the TLS handshake and leaf certificate.
They are NOT columns, and not attached by HTTPRecord.MarshalJSON. Both decisions follow from the same fact — these describe the HOST, not the record:
- Storing them per row would put many identical copies of one answer in the database (a sweep writes several records per host; a redirect chain alone is three), and they are stale the moment their TTL passes. Only the single `ip` column is persisted.
- Attaching them inside MarshalJSON would decorate every record that every caller in the process serializes from a global cache. The ingest server marshals `[]*HTTPRecord` straight to the HTTP response, so one project's rows would carry a name resolved at an unknown time while ingesting another's.
So the emit sites that KNOW this process did the probing opt in, and everyone else keeps serializing a record as itself.
func LookupHostFacts ¶
LookupHostFacts returns what this PROCESS observed for a record's host, or zero facts when it observed nothing (a run without --tls-probe, a plaintext endpoint, or any process that did no probing — an `export` of an old database, for instance).
type HostObservation ¶
type HostObservation struct {
bun.BaseModel `bun:"table:host_observations,alias:ho" json:"-"`
ID int64 `bun:"id,pk,autoincrement" json:"id"`
ProjectUUID string `bun:"project_uuid,notnull" json:"project_uuid"`
ScanUUID string `bun:"scan_uuid,nullzero" json:"scan_uuid,omitempty"`
Hostname string `bun:"hostname,notnull" json:"hostname"`
Port int `bun:"port,notnull" json:"port"`
ObservedAt time.Time `bun:"observed_at,notnull,default:current_timestamp" json:"observed_at"`
// DNS answers as jsonb columns, the same way Finding.Tags and
// HTTPRecord.Technology store their slices. Stored whole rather than as three
// child tables: they are read as a whole or not at all, and never queried by
// element.
DNSA []string `bun:"dns_a,type:jsonb,nullzero" json:"dns_a,omitempty"`
DNSAAAA []string `bun:"dns_aaaa,type:jsonb,nullzero" json:"dns_aaaa,omitempty"`
DNSCNAME []string `bun:"dns_cname,type:jsonb,nullzero" json:"dns_cname,omitempty"`
// TLS is the handshake and leaf certificate, absent when the endpoint was not
// handshaked (no --tls-probe, or a plaintext endpoint).
TLS *tlsprobe.Info `bun:"tls,type:jsonb,nullzero" json:"tls,omitempty"`
// Complete reports that the lookup actually ran to an answer. False marks an
// observation the run never finished - a cancelled or budget-limited
// prefetch - which is a different fact from a host that genuinely resolves to
// nothing, and the two are indistinguishable without it.
Complete bool `bun:"complete,notnull" json:"complete"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
}
HostObservation is what one scan observed about one endpoint: the full DNS answer and, under --tls-probe, the handshake and leaf certificate.
It is the durable counterpart to the process-local caches HostFacts reads. Those caches are bounded (8192 entries each) and evict in insertion order, so on a sweep larger than that the hosts resolved FIRST had their answers dropped before the export that wanted them - the work was done, the answer was known, and the output silently omitted it. A row here survives eviction, the process, and the run.
One row per (scan, hostname, port). Re-probing a host adds an observation, it does not replace one: the addresses a host resolved to during an earlier scan are that scan's evidence, and an upsert keyed on the host alone would erase it.
func (*HostObservation) Facts ¶
func (o *HostObservation) Facts() HostFacts
Facts renders a stored observation back into the output shape. The columns are bun jsonb fields, so this is a field copy rather than a decode.
type HostObservationInput ¶
type HostObservationInput struct {
Hostname string
Port int
HostFacts
// Complete reports that the lookup ran to an answer. A cut-short prefetch
// records incomplete observations rather than none, so a later reader can
// tell "no AAAA record" from "we never got there".
Complete bool
}
HostObservationInput is one endpoint's observation, as the probe collects it, before it becomes a row.
HostFacts is embedded rather than restated: the answer being stored IS the answer that will later be emitted, and copying its fields here meant two places to edit for every new fact and two Empty() methods that could silently disagree. What this type adds is the two things HostFacts cannot carry - which endpoint the answer describes, and whether the lookup finished.
func (HostObservationInput) Empty ¶
func (o HostObservationInput) Empty() bool
Empty reports whether the observation carries nothing worth storing. A row with no answer AND no completeness claim states nothing that the absence of a row does not already state.
type HostStats ¶
type HostStats struct {
Scheme string `json:"scheme"`
Hostname string `json:"hostname"`
Port int `json:"port"`
RequestCount int64 `json:"request_count"`
FindingCount int64 `json:"finding_count"`
}
HostStats holds statistics for a host
type HostTarget ¶
type HostTarget struct {
Scheme string `bun:"scheme"`
Hostname string `bun:"hostname"`
Port int `bun:"port"`
}
HostTarget represents a distinct scheme+hostname+port combination from HTTP records.
type MergeOptions ¶
type MergeOptions struct {
// SkipHTTPRecords omits the http_records table from the copy. It is safe for
// readers that never resolve a finding's linked records: finding_records
// carries no foreign key to http_records (it is a bare
// PRIMARY KEY (finding_id, record_uuid) table), and findings embed their own
// request/response inline, so the junction and every finding still merge
// intact — a later record lookup simply resolves to nothing.
//
// This exists because http_records holds the raw request/response blobs and
// is ~98% of the bytes in a typical result database. A read that renders
// purely from findings (the finding tree/table/JSON views) would otherwise
// pay to copy every blob into the destination only to discard it.
SkipHTTPRecords bool
// SkipRecordBodies copies http_records without its raw_request/raw_response
// columns, which are nullable and hold ~96% of the table's bytes. Every row
// still lands, with its metadata intact, so row counts and any predicate over
// metadata (status/method/host/content-type) are unaffected — unlike dropping
// rows, this changes nothing observable for such a reader.
//
// It is only safe when nothing downstream reads the raw corpus: a filter that
// LIKEs over it (see QueryFilters.UsesRawCorpus) silently matches nothing, and
// a renderer that prints it silently prints empty. Ignored when
// SkipHTTPRecords is set, which already omits the table.
SkipRecordBodies bool
// SkipFindings omits findings, the finding_records junction, and
// oast_interactions from the copy. Safe only for a reader that shows no
// finding and filters no record by one.
//
// It is the only option here that bounds the MERGING process's memory rather
// than the destination's size. The three tables it drops are the ones that
// cannot be copied set-based — findings.id is AUTOINCREMENT and is referenced
// by both the junction and the OAST rows, so the id remapping needs every
// source row in Go before the write transaction opens. Each finding row
// carries its inline request, response, and additional_evidence, so a corpus
// with many findings is buffered in full, per source, whether or not anything
// will read one.
SkipFindings bool
}
MergeOptions tunes what a SQLite-to-SQLite merge copies.
type MergeStats ¶
type MergeStats struct {
ProjectsMerged int
ScansMerged int
AgenticScansMerged int
RecordsMerged int
FindingsMerged int
FindingsDeduped int
FindingRecordsMerged int
OASTMerged int
}
MergeStats reports what a SQLite-to-SQLite merge copied into the destination.
func MergeSQLiteFile ¶
MergeSQLiteFile merges every native-scan result row from the SQLite database at srcPath into dest (which must be an already-open, schema-current SQLite database). It is safe to call repeatedly with the same source: rows dedup on their natural keys (uuid for records/scans, (project_uuid, finding_hash) for findings), so a re-merge is idempotent. project_uuid values are copied verbatim, so the merged rows stay scoped to whatever project the scan ran under.
The whole operation runs on a single pinned connection inside one BEGIN IMMEDIATE transaction, and retries on SQLITE_BUSY/locked so it tolerates other processes briefly holding the destination's write lock.
func MergeSQLiteFileWithOptions ¶
func MergeSQLiteFileWithOptions(ctx context.Context, dest *DB, srcPath string, opts MergeOptions) (*MergeStats, error)
MergeSQLiteFileWithOptions is MergeSQLiteFile with control over what is copied; see MergeOptions.
func (*MergeStats) Add ¶
func (s *MergeStats) Add(o *MergeStats)
Add accumulates o's counters into s, for aggregating the stats of several merges (e.g. a multi-source import) into a single total.
type OASTInteraction ¶
type OASTInteraction struct {
bun.BaseModel `bun:"table:oast_interactions,alias:oi" json:"-"`
ID int64 `bun:"id,pk,autoincrement" json:"id"`
ProjectUUID string `bun:"project_uuid,notnull" json:"project_uuid"`
ScanUUID string `bun:"scan_uuid,nullzero" json:"scan_uuid,omitempty"`
UniqueID string `bun:"unique_id,notnull" json:"unique_id"`
FullID string `bun:"full_id,notnull" json:"full_id"`
Protocol string `bun:"protocol,notnull" json:"protocol"`
QType string `bun:"q_type,nullzero" json:"q_type,omitempty"`
RawRequest string `bun:"raw_request,nullzero" json:"raw_request,omitempty"`
RawResponse string `bun:"raw_response,nullzero" json:"raw_response,omitempty"`
RemoteAddress string `bun:"remote_address,nullzero" json:"remote_address,omitempty"`
InteractedAt time.Time `bun:"interacted_at,notnull" json:"interacted_at"`
// Correlated context from payload tracker
TargetURL string `bun:"target_url,nullzero" json:"target_url,omitempty"`
ParameterName string `bun:"parameter_name,nullzero" json:"parameter_name,omitempty"`
InjectionType string `bun:"injection_type,nullzero" json:"injection_type,omitempty"`
ModuleID string `bun:"module_id,nullzero" json:"module_id,omitempty"`
FindingID int64 `bun:"finding_id,nullzero" json:"finding_id,omitempty"` // link to finding this interaction proves
Payload string `bun:"payload,nullzero" json:"payload,omitempty"` // exact payload that triggered the interaction
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
}
OASTInteraction records an out-of-band interaction received from an interactsh server.
type PathTarget ¶
type PathTarget struct {
Scheme string `bun:"scheme"`
Hostname string `bun:"hostname"`
Port int `bun:"port"`
Path string `bun:"path"`
}
PathTarget represents a distinct scheme+hostname+port+path combination from HTTP records.
type PerformanceMetrics ¶
type PerformanceMetrics struct {
AvgResponseTime int64 `json:"avg_response_time"`
MinResponseTime int64 `json:"min_response_time"`
MaxResponseTime int64 `json:"max_response_time"`
P50ResponseTime int64 `json:"p50_response_time"`
P95ResponseTime int64 `json:"p95_response_time"`
P99ResponseTime int64 `json:"p99_response_time"`
}
PerformanceMetrics holds response time statistics
type PrimaryKeyInfo ¶
type PrimaryKeyInfo struct {
Columns []string `json:"columns"`
}
PrimaryKeyInfo describes the primary key column(s) of a table.
func DetectPrimaryKey ¶
DetectPrimaryKey returns the primary key column(s) for the given table.
type Project ¶
type Project struct {
bun.BaseModel `bun:"table:projects,alias:p" json:"-"`
UUID string `bun:"uuid,pk,notnull" json:"uuid"`
Name string `bun:"name,notnull" json:"name"`
Description string `bun:"description,nullzero" json:"description,omitempty"`
OwnerUUID string `bun:"owner_uuid,nullzero" json:"owner_uuid,omitempty"` // soft FK → users
ConfigPath string `bun:"config_path,nullzero" json:"config_path,omitempty"`
Tags []string `bun:"tags,type:jsonb,nullzero" json:"tags,omitempty"`
DefaultTarget string `bun:"default_target,nullzero" json:"default_target,omitempty"`
LastScanAt time.Time `bun:"last_scan_at,nullzero" json:"last_scan_at,omitempty"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp" json:"updated_at"`
}
Project represents a logical grouping for all scan data.
type ProjectStatsRow ¶
type ProjectStatsRow struct {
ProjectUUID string `bun:"project_uuid"`
HTTPRecords int64 `bun:"http_records"`
HTTP2xx int64 `bun:"http_2xx"`
HTTP3xx int64 `bun:"http_3xx"`
HTTP4xx int64 `bun:"http_4xx"`
HTTP5xx int64 `bun:"http_5xx"`
Findings int64 `bun:"findings"`
Critical int64 `bun:"critical"`
High int64 `bun:"high"`
Medium int64 `bun:"medium"`
Low int64 `bun:"low"`
Info int64 `bun:"info"`
Scans int64 `bun:"scans"`
AgenticScans int64 `bun:"agentic_scans"`
OASTInteractions int64 `bun:"oast_interactions"`
}
ProjectStatsRow holds per-project aggregated counts used by GetAllProjectsStats.
type QueryBuilder ¶
type QueryBuilder struct {
// contains filtered or unexported fields
}
QueryBuilder builds filtered database queries
func NewQueryBuilder ¶
func NewQueryBuilder(db *DB, filters QueryFilters) *QueryBuilder
NewQueryBuilder creates a new query builder
func (*QueryBuilder) BuildRecordsQuery ¶
func (qb *QueryBuilder) BuildRecordsQuery() *bun.SelectQuery
BuildRecordsQuery builds a query for http_records table
func (*QueryBuilder) Count ¶
func (qb *QueryBuilder) Count(ctx context.Context) (int64, error)
Count returns total number of matching records
func (*QueryBuilder) Execute ¶
func (qb *QueryBuilder) Execute(ctx context.Context) ([]*HTTPRecord, error)
Execute executes the query and returns results
func (*QueryBuilder) ExecuteWithCount ¶
func (qb *QueryBuilder) ExecuteWithCount(ctx context.Context) ([]*HTTPRecord, int64, error)
ExecuteWithCount runs the filtered query and returns the matching page of records alongside the total count for the same filters.
It is NOT one round trip, despite what this comment used to claim. Bun's ScanAndCount collapses to a single statement only when there is no limit and no offset; a paginated call — the only reason to use this method — takes the two-query path, and with no explicit conn it runs the scan and the count CONCURRENTLY, so it occupies two pool connections for its duration. On SQLite that is worth remembering when this runs alongside a busy RecordWriter.
Still preferable to Execute + Count, which serializes the same two queries. Callers that do not need an exact total on every refresh should use Execute alone rather than paying the count.
func (*QueryBuilder) GroupRecordsBy ¶
func (qb *QueryBuilder) GroupRecordsBy(ctx context.Context, field string, limit int) (RecordGrouping, error)
GroupRecordsBy counts the filtered records by one column, largest bucket first, capped at limit (<= 0 means no cap).
The totals come from a second query ONLY when the cap actually bit. Summing the returned buckets describes the whole result exactly when every bucket came back — each filtered row lands in exactly one bucket, since COALESCE removes the NULL/empty split — and describes only the visible part otherwise, which is how a bounded view comes to report a complete-looking number that is wrong. So the expensive case is paid for and the common one is not: the filters here are the listing's filters, which include `LIKE '%…%'` over the raw request/response corpus, and running those twice reads every matched record's blobs twice to produce a dozen integers.
func (*QueryBuilder) OmitBodies ¶
func (qb *QueryBuilder) OmitBodies() *QueryBuilder
OmitBodies drops raw_request/raw_response from the SELECT list, for callers that read only metadata off the returned records. Those two columns hold the whole request/response corpus and dwarf every other column, so hydrating them for a list view costs memory proportional to the traffic scanned rather than to the page size.
Filters are unaffected: they run in SQL against the stored columns, so this is safe even with --search/--header/--body active (contrast MergeOptions. SkipRecordBodies, which removes the columns from the destination and so cannot be used with those filters). The only requirement is that the caller not read HTTPRecord.RawRequest/RawResponse — they come back empty.
This mirrors FindingsQueryBuilder.ExecuteWithCount, which always excludes its equivalent blob columns; records can't default to that because --raw/--burp and the record views legitimately need them.
type QueryFilters ¶
type QueryFilters struct {
// Project scoping
ProjectUUID string // Required: filter all queries to this project
// Identity filtering. RecordUUIDs selects exact stored records by UUID and is
// applied as a SQL predicate, so it narrows BEFORE pagination — a record is
// never reported absent merely because it fell outside the current page.
// Without it the read surface had no identity selector at all: the only flags
// that took a record UUID (`replay -u`, `fuzz -u`) both send traffic, and a
// UUID passed as the fuzzy search term matched zero rows because that term
// searches URL/path/body, not identity.
RecordUUIDs []string
// URLsExact selects records whose stored url matches EXACTLY — an equality
// IN (), never a LIKE.
//
// It exists because `--url` was the name callers reached for and the one name
// the read surface did not have: --host takes a hostname, --path a fuzzy
// pattern, and a URL passed as the positional term is a substring search that
// also matches every URL containing it. So "show me this one endpoint" had no
// spelling, and the nearest miss was answered by the flag suggester with
// "did you mean --all?", which lifts the result cap.
//
// Repeated values are OR-ed within the field and AND-ed against every other
// filter, matching how --uuid and --method already behave. No normalization,
// no case folding, no trailing-slash equivalence: equality is against the
// bytes that were stored, because a comparison that quietly canonicalizes is
// one the caller cannot predict from the value they passed.
URLsExact []string
// Host filtering
HostPattern string // Hostname pattern (supports wildcards)
// Request filtering
Methods []string // HTTP methods (GET, POST, etc.)
PathPattern string // Path pattern (supports wildcards)
StatusCodes []int // Status codes (200, 404, etc.)
ScanUUID string // Scan session UUID (for findings filtering)
AgenticScanUUIDs []string // Agentic-scan UUID(s) (links agent-produced findings; pass the scan tree for nested audit/swarm runs)
Source string // Filter by record source (e.g. scanner, ingest-cli, ingest-server)
// Response filtering
ContentType string // Filter by response content type
// Risk filtering
MinRiskScore int // Minimum risk score filter (anomaly_ranking's batch percentile)
MinSurfaceScore int // Minimum attack-surface score filter (surface_scoring's 0-100 signal score)
Remark string // Filter by remark substring (single)
Remarks []string // Filter by multiple remarks (AND: record must have all)
// Finding filtering
FindingID int // Filter by finding ID
FindingIDAfter int64 // Filter findings with ID greater than this value
Severity []string // Finding severity (critical, high, medium, low, info)
Confidence []string // Finding confidence (certain, firm, tentative)
ModuleName string // Filter findings by module name
ModuleType string // Filter findings by module type (active, passive, nuclei, etc.)
FindingSource string // Filter findings by source (audit, spa, agent, etc.)
RecordKinds []string // finding (default), candidate, observation
RepoName string // Filter findings by repo name
Status []string // Filter findings by lifecycle status (draft, triaged, false_positive, accepted_risk, fixed)
// Date range filtering
DateFrom *time.Time
DateTo *time.Time
// Full-text search
SearchTerm string // Search across URLs, paths
SearchTerms []string // Findings: repeatable search terms, AND-combined (each further narrows the match)
FuzzyTerm string // Broad fuzzy search across multiple fields
HeaderSearch string // Search in headers
BodySearch string // Search in request/response body
// Negative / exclusion search — drop rows where the term appears in the same
// corpus the positive counterpart scans.
ExcludeTerms []string // Repeatable, AND-combined: a row is dropped if ANY term matches (inverse of SearchTerms)
ExcludeHeaderSearch string // Drop rows whose header/raw corpus contains the term (inverse of HeaderSearch)
ExcludeBodySearch string // Drop rows whose body/raw corpus contains the term (inverse of BodySearch)
// Pagination
Limit int
Offset int
// Sorting
SortBy string // Field to sort by
SortAsc bool // Sort ascending (default: descending)
}
QueryFilters holds filter criteria for database queries
func (QueryFilters) EffectiveExcludeTerms ¶
func (f QueryFilters) EffectiveExcludeTerms() []string
EffectiveExcludeTerms returns the exclusion search terms with blanks dropped, so callers can loop the result directly. Each term becomes an independent NOT (...) conjunct — a row is dropped if ANY term matches.
func (QueryFilters) EffectiveSearchTerms ¶
func (f QueryFilters) EffectiveSearchTerms() []string
EffectiveSearchTerms returns the search terms to AND-combine: the repeatable SearchTerms when set, otherwise the single SearchTerm. Blank terms are dropped, so callers can loop the result directly.
func (QueryFilters) UsesLinkedFindings ¶
func (f QueryFilters) UsesLinkedFindings() bool
UsesLinkedFindings reports whether any active filter reaches into findings when the query is over HTTP RECORDS. It is the mirror of UsesLinkedRecords: records have no finding columns of their own, so --severity resolves through a join over the finding_records junction (see applyFilters).
A caller that would otherwise omit the finding tables entirely (globDBSkipSet.Findings, MergeOptions.SkipFindings) MUST check this first. The failure is silent: with no findings to join, a --severity record listing returns nothing rather than erroring.
Keep in sync with applyFilters.
func (QueryFilters) UsesLinkedRecords ¶
func (f QueryFilters) UsesLinkedRecords() bool
UsesLinkedRecords reports whether any active filter reaches into http_records when the query is over FINDINGS. It is the findings-side counterpart to UsesRawCorpus and a strictly wider question: a findings query has no record columns of its own, so host/path/method/status/source all resolve through an EXISTS over the finding_records junction (see applyFindingFilters), and every raw-corpus filter does too.
The question does not arise for traffic, whose query is over http_records directly — there the rows are always present by construction.
A caller that would otherwise omit the table entirely (globDBSkipSet.Records, MergeOptions.SkipHTTPRecords) MUST check this first. The failure is silent: with no rows to satisfy them, every EXISTS predicate is false, so --host returns nothing and the NOT EXISTS exclude forms return everything.
Keep in sync with applyFindingFilters.
func (QueryFilters) UsesRawCorpus ¶
func (f QueryFilters) UsesRawCorpus() bool
UsesRawCorpus reports whether any active filter matches against the raw_request/raw_response blob columns — the FuzzyTerm branches, recordSearchPredicate (--search) and the region predicates behind --header/--body (see message_regions.go), plus all three exclude forms.
A caller that would otherwise avoid materializing those blobs (a projected SELECT, or a merge that omits the columns) MUST check this first and keep them when it returns true. The failure is silent, not an error: with the blobs absent every LIKE matches nothing, so --search returns an empty result and the --exclude-* forms — which are negated — return everything.
Keep in sync with applyFilters; this is the reason to add a field here rather than open-code the flag checks at each call site.
type ReSpiderCandidate ¶
type ReSpiderCandidate struct {
UUID string `bun:"uuid"`
URL string `bun:"url"`
Scheme string `bun:"scheme"`
Hostname string `bun:"hostname"`
Port int `bun:"port"`
Source string `bun:"source"`
ResponseContentType string `bun:"response_content_type"`
RawResponse []byte `bun:"raw_response"`
}
ReSpiderCandidate is a lightweight projection of an HTTP record used by the targeted re-spider phase. It carries just enough to evaluate rich/SPA content and dedup app shells (source + raw_response) without materializing every column.
type RecordCounts ¶
type RecordCounts struct {
HTTPRecords int64 `json:"http_records"`
WithResponse int64 `json:"with_response"`
ResponsePercent float64 `json:"response_percent"`
Findings int64 `json:"findings"`
}
RecordCounts holds counts of each record type
type RecordGroup ¶
RecordGroup is one bucket: a column value and how many records carry it.
type RecordGrouping ¶
type RecordGrouping struct {
// Field is the public name that was grouped on, echoed back.
Field string `json:"field"`
// Groups are the largest buckets first, at most the requested limit.
Groups []RecordGroup `json:"groups"`
// DistinctGroups is how many buckets exist in total, whatever Groups holds.
DistinctGroups int64 `json:"distinct_groups"`
// TotalRecords is how many records the filters matched, whatever was bucketed.
TotalRecords int64 `json:"total_records"`
// OtherGroups / OtherRecords describe the tail beyond the limit. Both zero
// means the grouping is complete.
OtherGroups int64 `json:"other_groups"`
OtherRecords int64 `json:"other_records"`
}
RecordGrouping is a bounded answer, and says so.
A grouping is not automatically small — one recorded case grouped 56 rows into 54 distinct buckets, which is a listing wearing a summary's name. Reporting only the top N without saying what was left out turns that into a wrong answer, so the tail is counted rather than dropped.
type RecordResponseUpdate ¶
type RecordResponseUpdate struct {
StatusCode int
StatusPhrase string
ResponseHTTPVersion string
ResponseContentType string
ResponseContentLength int64
RawResponse []byte
ResponseHash string
ResponseTimeMs int64
}
RecordResponseUpdate contains response fields to update on an existing HTTP record. Used by the traffic --replay --in-replace feature.
type RecordWithHostFacts ¶
type RecordWithHostFacts struct {
Record *HTTPRecord
Facts HostFacts
}
RecordWithHostFacts is an HTTPRecord serialized together with its host facts. Marshals as the record's own object with the fact keys merged in at the top level, which is where httpx puts them and therefore where a consumer written against that output looks.
func (RecordWithHostFacts) MarshalJSON ¶
func (rw RecordWithHostFacts) MarshalJSON() ([]byte, error)
MarshalJSON merges the fact keys into the record's own object rather than nesting them under a wrapper, so the emitted shape is the record a consumer already parses plus the extra keys.
type RecordWriter ¶
type RecordWriter struct {
// contains filtered or unexported fields
}
RecordWriter serializes database writes through sharded goroutines that coalesce individual SaveRecord calls into batch transactions. Records are routed to shards by hashing the host name, so writes for the same host are serialized within a shard. With Shards=1 (default), behavior is identical to a single-goroutine writer. This eliminates SQLite SQLITE_BUSY errors under concurrent ingestion.
func NewRecordWriter ¶
func NewRecordWriter(repo *Repository, cfg RecordWriterConfig) *RecordWriter
NewRecordWriter creates and starts a RecordWriter. Call Close() to flush remaining records and stop the background goroutines.
func (*RecordWriter) Close ¶
func (w *RecordWriter) Close()
Close stops accepting new writes, flushes remaining records, and returns.
Ordering matters: it marks the writer closed under the admission write lock (so no new write can admit past this point), waits for every already-admitted enqueue to finish landing in a shard channel, and only THEN cancels the flush context. That guarantees the shutdown drain observes every admitted record — none is orphaned in a channel after the flush loop exits, and every admitted write receives its real result instead of a spurious "closed" error.
func (*RecordWriter) Metrics ¶
func (w *RecordWriter) Metrics() RecordWriterMetrics
Metrics returns a snapshot of the writer's counters.
func (*RecordWriter) SaveRecord ¶
func (w *RecordWriter) SaveRecord(ctx context.Context, rr *httpmsg.HttpRequestResponse, source string, projectUUID string) (string, error)
SaveRecord implements the network.RecordSaver interface by delegating to Write.
func (*RecordWriter) SaveRecordBatch ¶
func (w *RecordWriter) SaveRecordBatch(ctx context.Context, records []*httpmsg.HttpRequestResponse, source string, projectUUID string) ([]string, error)
SaveRecordBatch enqueues records to their shards BEFORE awaiting any result, so a single bulk producer's records land in the same writer batch instead of paying one flush interval per record (the trap of a Write-in-a-loop: Write blocks until its record is flushed, so a lone producer never fills a batch). Results are returned positionally aligned with the input — uuids[i] corresponds to records[i] — including deduplicated, invalid, and failed entries, so callers can't misassociate a UUID. The first error encountered is returned alongside the (still complete) uuid slice.
Submissions are admitted in BOUNDED slices rather than all at once. Callers hand this whole collections: discovery submits an entire provenance group in one call, which on a large corpus is tens of thousands of records. Admitting them together made every one of their converted HTTPRecords — each owning a full raw request and response — plus a result channel each live simultaneously, so peak memory scaled with the corpus rather than with anything configured. Slices stay large enough to fill many writer batches, so the coalescing this method exists for is unaffected.
func (*RecordWriter) Write ¶
func (w *RecordWriter) Write(ctx context.Context, rr *httpmsg.HttpRequestResponse, source string, projectUUID string) (string, error)
Write enqueues a record for batched insertion. It blocks until the record is persisted (or the context is cancelled). This is safe to call from multiple goroutines concurrently.
func (*RecordWriter) WriteWithParent ¶
func (w *RecordWriter) WriteWithParent(ctx context.Context, rr *httpmsg.HttpRequestResponse, source string, projectUUID string, parentUUID string) (string, error)
WriteWithParent is Write with an explicit parent_uuid link, used to chain the hops of a followed redirect into one walkable sequence. See admit for why the link is applied to new rows only.
type RecordWriterConfig ¶
type RecordWriterConfig struct {
// BufferSize is the channel capacity. Senders block when the buffer is full,
// providing natural backpressure. Default: 4096.
BufferSize int
// BatchSize is the maximum number of records flushed in a single transaction.
// Default: 128.
BatchSize int
// Shards is the number of independent flush goroutines. Each shard has its
// own channel and flushLoop. Records are routed to shards by hashing the
// host name, so writes for the same host are serialized within a shard.
// Default (when <= 0): 1 for SQLite, 4 for PostgreSQL. SQLite is a
// single-writer database, so multiple flush goroutines would only contend
// on BEGIN IMMEDIATE / busy_timeout without gaining write parallelism;
// PostgreSQL supports genuinely concurrent writers, so it fans out.
Shards int
// FlushTimeout bounds the shutdown drain, not steady-state flushes. Normal
// flushes run on an uncancellable context.Background() so a slow insert never
// drops records; only the drain triggered by Close() is capped by this single
// budget, so a wedged database can't block Close() forever while a healthy one
// still drains in full. Default: 2m (far longer than any healthy drain).
FlushTimeout time.Duration
// DedupCacheSize bounds the in-memory dedup cache (LRU) that maps a record's
// dedup key to its UUID. A cache hit lets Write skip the per-record SELECT and
// the redundant insert for a key already seen by this writer (common in
// discovery/spidering, which re-encounter the same URLs). Zero means "unset"
// and selects the default of 50000; pass a NEGATIVE size to disable the cache.
DedupCacheSize int
// MaxSubmitRecords and MaxSubmitBytes bound ONE SaveRecordBatch submission:
// how many records (and how many raw request+response bytes) it admits before
// waiting for that slice to resolve. They cap the peak memory of a bulk caller
// that hands over a whole collection at once — without them, peak retention is
// set by the caller's corpus size and nothing else.
//
// Both defaults sit well above BatchSize, so a submission still fills many
// writer batches and the coalescing SaveRecordBatch exists for is preserved.
// Defaults: 1024 records, 128 MiB.
MaxSubmitRecords int
MaxSubmitBytes int64
}
RecordWriterConfig configures the batching behavior of RecordWriter.
type RecordWriterMetrics ¶
type RecordWriterMetrics struct {
Enqueued int64
Flushed int64
FlushFailed int64
FlushErrors int64
BatchCount int64
BufferDepth int64
}
RecordWriterMetrics exposes counters for monitoring.
Flushed and FlushFailed partition the batch entries that reached a terminal outcome: Flushed counts entries that ended up persisted or deduplicated onto an existing row, FlushFailed counts entries whose insert transaction failed. FlushErrors counts failing BATCHES, so it is not comparable with either — one failed batch fails every entry in it.
type Repository ¶
type Repository struct {
// OnRecordSaved, when non-nil, is invoked with each newly inserted HTTP
// record (deduplicated saves and dedup hits do NOT fire). OnFindingSaved is
// the equivalent for newly inserted findings (a dedup-append to an existing
// finding does not fire). Both run synchronously on the save goroutine, so
// implementations must be fast and non-blocking (e.g. enqueue and return).
// Used by the server's --mirror-fs filesystem mirror; left nil elsewhere, so
// CLI scans and other repo users are unaffected.
OnRecordSaved func(*HTTPRecord)
OnFindingSaved func(*Finding)
// contains filtered or unexported fields
}
Repository handles HTTP record and finding storage
func NewRepository ¶
func NewRepository(db *DB) *Repository
NewRepository creates a new repository instance
func (*Repository) AdoptRecordParent ¶
func (r *Repository) AdoptRecordParent(ctx context.Context, uuid, parentUUID string)
AdoptRecordParent links an existing record to a parent, but ONLY when it has none. A no-op for an empty uuid or parent.
Backfill-never-overwrite is the whole contract. Filling an empty parent adds information and cannot contradict anything. Overwriting a set one would let the most recent scan to traverse a shared redirect target silently re-parent it out of the chain it was first recorded in, rewriting history for every earlier chain that ran through the same URL.
Best-effort: a failure to link leaves a shorter chain, which reads correctly as less lineage rather than as wrong lineage, and must not fail the save that prompted it.
func (*Repository) AdvanceScanCursor ¶
func (r *Repository) AdvanceScanCursor(ctx context.Context, scanUUID string, recordCreatedAt time.Time, recordUUID string) error
AdvanceScanCursor updates the cursor position and increments ProcessedCount.
func (*Repository) AdvanceScanCursorBy ¶
func (r *Repository) AdvanceScanCursorBy(ctx context.Context, scanUUID string, recordCreatedAt time.Time, recordUUID string, delta int64) error
AdvanceScanCursorBy updates the cursor position and increments ProcessedCount by delta.
func (*Repository) AgenticScanStatusByUUID ¶
AgenticScanStatusByUUID returns just the status column for a run.
It exists for the SSE log follower, which asks "is this run finished yet?" twice a second for as long as a client watches: doing that through GetAgenticScan reads the run's full row — blobs included — so N followers on a finished-but-still-watched autopilot run pull 2N megabyte-scale reads per second out of the database to learn one short string.
func (*Repository) AppendRemarks ¶
AppendRemarks batch-appends remarks to HTTPRecords identified by UUID. Existing remarks are preserved and duplicates within each record are deduplicated.
The read and the merged write happen in ONE transaction. Remarks are a growing set written by several analysis passes against the same records, and reading the current value outside the transaction turns every append into a read- modify-write race: two passes both read ["a"], both write their own merge, and whichever commits second silently drops the other's remark. The symptom is a missing annotation, which looks like the analysis never fired.
SQLite's write lock serializes the transactions outright. On PostgreSQL the rows are locked FOR UPDATE, so a concurrent appender blocks on the read rather than proceeding from a stale copy.
func (*Repository) ApplyDeparosStatusPolicy ¶
func (r *Repository) ApplyDeparosStatusPolicy(ctx context.Context, projectUUID string, policy DeparosStatusPolicy) (int64, map[int]int64, error)
ApplyDeparosStatusPolicy enforces the discovery status-retention policy on stored deparos records. A fuzzed path the server answers with a 4xx is usually not a discovered resource — it's a rejection (malformed/ambiguous path → 400, not-found → 404). But some 4xx statuses carry real attack surface: 401/403 prove an authz boundary exists, 405/415/422 prove a route exists but was hit with the wrong method/payload, and 429 is a pacing signal. The policy retains those (see DeparosStatusPolicy) and drops the rest, rather than deleting every 4xx and losing the signal.
Returns the number of deleted records and a status-code breakdown of them.
func (*Repository) AttributeScanToAgenticScan ¶
func (r *Repository) AttributeScanToAgenticScan(ctx context.Context, scanUUID, agenticScanUUID string) error
AttributeScanToAgenticScan links a native scan (and every finding it produced) to the agentic scan that launched it. The run_scan tool calls this after a blocking LaunchScan returns, so the child scan's findings show up under `finding --agentic-scan <parent>` — which filters on finding.agentic_scan_uuid directly rather than joining through scans. Both the scan row and its findings are stamped; a no-op when either UUID is empty.
func (*Repository) BackfillRecordResponse ¶
func (r *Repository) BackfillRecordResponse(ctx context.Context, uuid string, rr *httpmsg.HttpRequestResponse) error
BackfillRecordResponse populates the response columns of an existing request-only http_record stub from a freshly fetched request/response, computing every response-derived field (content-type/length, hashes, normalized hash, word count, title) via the shared FromHttpRequestResponse converter so the backfilled row matches a normally-saved record.
Discovery stores endpoints synthesized from a parsed OpenAPI/Swagger/Postman spec as request-only stubs (status 0, empty body); the executor then fetches each baseline to scan the endpoint, but without this the stub stays at status 0 with an empty response in the traffic view. The update is guarded on has_response = false so it only ever fills a genuine stub — it never clobbers a record that already captured a response, making it idempotent across the discovery and dynamic-assessment phases. An rr with no response is a no-op.
func (*Repository) CompleteScan ¶
CompleteScan marks a scan as completed (or failed if errMsg is non-empty) and populates severity counts from the findings table.
func (*Repository) CountRecordsAfterCursor ¶
func (r *Repository) CountRecordsAfterCursor(ctx context.Context, cursorAt time.Time, cursorUUID string, hosts ...HostTarget) (int64, error)
CountRecordsAfterCursor counts records after the given cursor position. A zero cursorAt means count all records. When hosts is non-empty, only records matching those in-scope origins (scheme+hostname+port) are counted.
func (*Repository) CountRecordsAfterCursorBySource ¶
func (r *Repository) CountRecordsAfterCursorBySource(ctx context.Context, cursorAt time.Time, cursorUUID string, sources []string, hosts []HostTarget) (int64, error)
CountRecordsAfterCursorBySource is like CountRecordsAfterCursor but also filters on http_records.source. Used by scan-on-receive shallow mode to report only user-ingested traffic in the "new ingested records" status, excluding finding/scanner artefacts produced by the scan itself.
func (*Repository) CountRecordsByStatusCode ¶
func (r *Repository) CountRecordsByStatusCode(ctx context.Context, hosts ...HostTarget) (map[int]int64, error)
CountRecordsByStatusCode returns http_record counts grouped by HTTP status code, restricted to the same in-scope origins as CountRecordsAfterCursor (empty hosts = every record). Keys are the raw numeric status codes (0 for a missing/unset status); callers bucket them into 2xx/3xx/… classes. Powers the scan-completion summary's status-class line, so its host scope matches the record count printed alongside it.
func (*Repository) CountRecordsForScan ¶
func (r *Repository) CountRecordsForScan(ctx context.Context, projectUUID, scanUUID string) (int64, error)
CountRecordsForScan returns how many HTTP records this scan persisted.
func (*Repository) CreateAgenticScan ¶
func (r *Repository) CreateAgenticScan(ctx context.Context, run *AgenticScan) error
CreateAgenticScan stores a new agent run record. When a row with run.UUID already exists, the call is a no-op as long as the existing project_uuid matches — this is the get-or-create path used for cross-node sync via --scan-uuid. Returns ErrScanProjectMismatch when the existing row belongs to a different project.
func (*Repository) CreateProject ¶
func (r *Repository) CreateProject(ctx context.Context, project *Project) error
CreateProject inserts a new project.
func (*Repository) CreateScan ¶
func (r *Repository) CreateScan(ctx context.Context, scan *Scan) error
CreateScan inserts a new scan record. When a row with scan.UUID already exists, the call is a no-op as long as the existing project_uuid matches — this is the get-or-create path used for cross-node sync via --scan-uuid. Returns ErrScanProjectMismatch when the existing row belongs to a different project.
func (*Repository) CreateScanLog ¶
func (r *Repository) CreateScanLog(ctx context.Context, log *ScanLog) error
CreateScanLog inserts a scan log entry.
func (*Repository) CreateScanLogBatch ¶
func (r *Repository) CreateScanLogBatch(ctx context.Context, logs []*ScanLog) error
CreateScanLogBatch inserts multiple scan log entries in a single bulk insert.
func (*Repository) CreateScanWithCursor ¶
func (r *Repository) CreateScanWithCursor(ctx context.Context, scan *Scan) error
CreateScanWithCursor creates a Scan record. If mode is "incremental", copies cursor from the last completed scan with matching Modules. Otherwise starts at zero.
func (*Repository) CreateUser ¶
func (r *Repository) CreateUser(ctx context.Context, user *User) error
CreateUser inserts a new user.
func (*Repository) DeduplicateDeparosByNormHash ¶
func (r *Repository) DeduplicateDeparosByNormHash(ctx context.Context, projectUUID string) (int64, map[int]int64, error)
DeduplicateDeparosByNormHash collapses deparos records whose NORMALIZED response body is identical — records that differ only by a reflected request URL/path or per-request dynamic tokens (timestamps, ids). This is the case the exact (hostname, status, content_length, response_hash) dedup misses: an error or echo page that mirrors the requested URI gives every probed path a distinct body, length, and hash, so they all survive as separate records even though they are the same page. response_norm_hash (computed at storage time with the reflected URL/path stripped and dynamic runs collapsed) makes them identical.
Grouping is on (hostname, method, status_code, response_content_type, response_norm_hash); the shortest path per group survives. Records with no normalized hash (empty bodies) are left to exact-hash dedup. Returns the number of deleted records and a status-code breakdown.
func (*Repository) DeduplicateDeparosRecords ¶
func (r *Repository) DeduplicateDeparosRecords(ctx context.Context, projectUUID string) (int64, error)
DeduplicateDeparosRecords removes duplicate deparos HTTP records. Delegates to DeduplicateRecordsBySource with source "deparos".
func (*Repository) DeduplicateFindings ¶
func (r *Repository) DeduplicateFindings(ctx context.Context, projectUUID string, hostnames ...string) (deleted int64, grouped int64, err error)
DeduplicateFindings merges duplicate findings that share the same (module_id, severity, matched_at URL) within a project. This collapses findings where the same module fires many times on the same URL with different payloads (e.g., input-behavior-probe producing dozens of results per endpoint). Within each group, the earliest finding is kept and the request/response pairs from duplicates are collected into its AdditionalEvidence field.
When one or more hostnames are passed, the pass is scoped to findings on those hosts (an empty/omitted list dedupes the whole project). The dynamic-assessment feedback loop scopes each round to the hosts being scanned — coverage-equivalent there, since every finding produced during DA is on an in-scope host — so a growing findings table isn't full-scanned once per round. Returns the count of deleted findings and the number of groups that were merged.
func (*Repository) DeduplicateRecordsBySource ¶
func (r *Repository) DeduplicateRecordsBySource(ctx context.Context, projectUUID, source string) (int64, error)
DeduplicateRecordsBySource removes duplicate HTTP records for a given source that share identical (hostname, method, status_code, response_content_length, response_hash). Within each group, the record with the shortest path is kept. Returns the number of deleted records.
func (*Repository) DeduplicateSoftDeparosRecords ¶
func (r *Repository) DeduplicateSoftDeparosRecords(ctx context.Context, projectUUID string) (int64, map[int]int64, error)
DeduplicateSoftDeparosRecords removes deparos HTTP records that are "soft duplicates": same response characteristics (status, word count, content type) under the same 2-segment path prefix. This catches cases where the server echoes part of the URL in the response body, causing different response_hash values for functionally identical pages. Exact response_content_length is deliberately NOT part of the key: a reflected URL/path shifts the body length by a handful of bytes per probe, which would otherwise split an otherwise-identical family into singletons (the very case this pass exists to collapse) — word count + content type + status + path prefix is the stable shape signal. Only groups with 3+ members are collapsed. The shortest path per group is kept.
func (*Repository) DeleteAuthenticationHostname ¶
func (r *Repository) DeleteAuthenticationHostname(ctx context.Context, id int64) error
DeleteAuthenticationHostname deletes a single session hostname by ID.
func (*Repository) DeleteAuthenticationHostnamesByHostname ¶
func (r *Repository) DeleteAuthenticationHostnamesByHostname(ctx context.Context, projectUUID, hostname string) error
DeleteAuthenticationHostnamesByHostname deletes all session hostnames for a project+hostname.
func (*Repository) DeleteFinding ¶
func (r *Repository) DeleteFinding(ctx context.Context, id int64) error
DeleteFinding deletes a finding by its numeric ID, including any finding_records junction rows.
func (*Repository) DeleteFindingByHash ¶
func (r *Repository) DeleteFindingByHash(ctx context.Context, projectUUID, findingHash string) error
DeleteFindingByHash deletes the finding(s) with the given content hash within a project, including their finding_records junction rows. Used by the OAST collector to replace a weaker finding (e.g. a DNS-resolution lead) with a stronger one (the HTTP-fetch confirmation) for the same callback payload, so each payload yields exactly one finding reflecting the strongest evidence. No-op when findingHash is empty or no such finding exists.
func (*Repository) DeleteOASTInteraction ¶
func (r *Repository) DeleteOASTInteraction(ctx context.Context, id int64) error
DeleteOASTInteraction deletes an OAST interaction by its numeric ID.
func (*Repository) DeleteOldAgenticScans ¶
func (r *Repository) DeleteOldAgenticScans(ctx context.Context, olderThan time.Duration) (int, error)
DeleteOldAgenticScans removes finished agent runs older than the given duration. "Finished" is TerminalAgenticScanStatuses, not a literal list — see that variable.
func (*Repository) DeleteProject ¶
func (r *Repository) DeleteProject(ctx context.Context, uuid string) error
DeleteProject deletes a project by UUID.
func (*Repository) DeleteRecord ¶
func (r *Repository) DeleteRecord(ctx context.Context, uuid string) error
DeleteRecord deletes an HTTP record by UUID, including any finding_records junction rows.
func (*Repository) DeleteScan ¶
func (r *Repository) DeleteScan(ctx context.Context, uuid string) error
DeleteScan deletes a scan record by UUID.
func (*Repository) ExistingRecordUUIDs ¶
func (r *Repository) ExistingRecordUUIDs(ctx context.Context, projectUUID string, uuids []string) ([]string, error)
ExistingRecordUUIDs returns the subset of uuids that exist. It selects the uuid column straight into a []string, so validating a caller-supplied list costs neither the raw_request/raw_response blobs nor an HTTPRecord struct per row.
The answer is always scoped to a project, blank meaning the default one as everywhere else in this package. The scoping is not optional because enforcing ownership is this method's whole purpose: record UUIDs are globally unique, so an unscoped check answers "does this row exist anywhere" when the question is "does the caller own it", and a caller who learns one UUID from another engagement could pull its traffic into their own scan. An opt-out parameter would make the safe and unsafe calls look identical.
func (*Repository) FindingProjectUUID ¶
FindingProjectUUID returns just the project a finding belongs to, for the existence-plus-scope check the mutating handlers perform before acting on it. Returns sql.ErrNoRows when the finding does not exist, like GetFindingByID.
Projected on purpose: the callers read only this one field, and a finding carries its own inline request/response blobs.
func (*Repository) GetAgenticScan ¶
func (r *Repository) GetAgenticScan(ctx context.Context, uuid string) (*AgenticScan, error)
GetAgenticScan retrieves an agent run by UUID.
func (*Repository) GetAllProjectsStats ¶
func (r *Repository) GetAllProjectsStats(ctx context.Context) (map[string]*ProjectStatsRow, error)
GetAllProjectsStats returns aggregated stats for all projects in bulk. Uses GROUP BY to avoid N+1 queries when listing projects.
func (*Repository) GetAuthenticationHostnamesByHostname ¶
func (r *Repository) GetAuthenticationHostnamesByHostname(ctx context.Context, projectUUID, hostname string) ([]*AuthenticationHostname, error)
GetAuthenticationHostnamesByHostname returns session hostnames for a project+hostname, ordered by position.
func (*Repository) GetAuthenticationHostnamesByProject ¶
func (r *Repository) GetAuthenticationHostnamesByProject(ctx context.Context, projectUUID string) ([]*AuthenticationHostname, error)
GetAuthenticationHostnamesByProject returns all session hostnames for a project, ordered by hostname then position.
func (*Repository) GetAuthenticationHostnamesByScan ¶
func (r *Repository) GetAuthenticationHostnamesByScan(ctx context.Context, projectUUID, scanUUID string) ([]*AuthenticationHostname, error)
GetAuthenticationHostnamesByScan returns session hostnames for a project+scan, ordered by hostname then position.
func (*Repository) GetChildAgenticScans ¶
func (r *Repository) GetChildAgenticScans(ctx context.Context, parentUUID string) ([]*AgenticScan, error)
GetChildAgenticScans returns agent runs whose ParentAgenticScanUUID matches the given UUID.
func (*Repository) GetDistinctHosts ¶
func (r *Repository) GetDistinctHosts(ctx context.Context, projectUUID string, since ...time.Time) ([]HostTarget, error)
GetDistinctHosts returns distinct scheme+hostname+port combinations from HTTP records, filtered by project. When a non-zero `since` is supplied, only records created at/after that time are considered (used to find origins discovered during a specific scan).
func (*Repository) GetDistinctPaths ¶
func (r *Repository) GetDistinctPaths(ctx context.Context, projectUUID string, hosts ...HostTarget) ([]PathTarget, error)
GetDistinctPaths returns distinct scheme+hostname+port+path combinations from HTTP records, filtered by project and, when hosts is non-empty, restricted to those in-scope origins (matching the executor's WithHostScopes convention). Empty hosts means no origin filter.
func (*Repository) GetFindingByID ¶
GetFindingByID retrieves a single finding by numeric ID.
func (*Repository) GetFindingsByRecordUUID ¶
GetFindingsByRecordUUID retrieves findings that reference a specific HTTP record UUID. Since http_record_uuids is a JSONB array, we use json_each to search inside it.
func (*Repository) GetFindingsBySeverity ¶
func (r *Repository) GetFindingsBySeverity(ctx context.Context, projectUUID, sev string, limit int) ([]*Finding, error)
GetFindingsBySeverity retrieves findings filtered by severity within a project.
func (*Repository) GetOASTInteractionByID ¶
func (r *Repository) GetOASTInteractionByID(ctx context.Context, id int64) (*OASTInteraction, error)
GetOASTInteractionByID retrieves a single OAST interaction by its numeric ID.
func (*Repository) GetOASTInteractionsByScan ¶
func (r *Repository) GetOASTInteractionsByScan(ctx context.Context, scanUUID string) ([]*OASTInteraction, error)
GetOASTInteractionsByScan retrieves OAST interactions for a specific scan.
func (*Repository) GetProjectByName ¶
GetProjectByName retrieves a project by name. Returns an error if zero or multiple projects match (names are not guaranteed to be unique).
func (*Repository) GetProjectByUUID ¶
GetProjectByUUID retrieves a project by UUID.
func (*Repository) GetProjectStats ¶
func (r *Repository) GetProjectStats(ctx context.Context, projectUUID string) (*ProjectStatsRow, error)
GetProjectStats returns aggregated stats for a single project.
func (*Repository) GetReSpiderCandidates ¶
func (r *Repository) GetReSpiderCandidates(ctx context.Context, projectUUID, afterUUID string, limit int, hosts ...HostTarget) ([]ReSpiderCandidate, error)
GetReSpiderCandidates returns records that carry a response body, for the targeted re-spider phase, using UUID-based cursor pagination (afterUUID="" for the first page). It deliberately includes spidering-sourced records too, so the caller can pre-seed its shell-dedup set with pages the browser already crawled. Ordered by uuid so paging is stable and per-host capping deterministic. Each returned row carries a full raw_response body, so the caller pages with a modest limit and reduces each page before reading the next rather than materializing every body at once.
func (*Repository) GetRecordByRequestHash ¶
func (r *Repository) GetRecordByRequestHash(ctx context.Context, projectUUID, requestHash string) (*HTTPRecord, error)
GetRecordByRequestHash retrieves the most recent HTTP record whose raw request hashes to requestHash (the SHA-256 over the raw request bytes, matching httpmsg.HttpRequest.ID). It recovers the originating request for an out-of-band (OAST) finding when the in-memory hash→UUID resolver is no longer available — e.g. a callback that lands after the executor that planted the payload has been torn down. projectUUID may be empty to search across projects.
func (*Repository) GetRecordByUUID ¶
func (r *Repository) GetRecordByUUID(ctx context.Context, uuid string) (*HTTPRecord, error)
GetRecordByUUID retrieves a single HTTP record by UUID
func (*Repository) GetRecordMetadataByHostname ¶
func (r *Repository) GetRecordMetadataByHostname(ctx context.Context, projectUUID, hostname string, limit int) ([]*HTTPRecord, error)
GetRecordMetadataByHostname is GetRecordsByHostname without the raw_request / raw_response BLOB columns, for callers that only need request-line metadata (method, URL, path, status). It avoids pulling multi-KB bodies for every row when they're immediately discarded — notably the autopilot coverage probe, which scans up to 5000 rows just to build (method, URL) signatures. Use the full GetRecordsByHostname when the bodies are needed.
func (*Repository) GetRecordsByHostname ¶
func (r *Repository) GetRecordsByHostname(ctx context.Context, projectUUID, hostname string, limit int) ([]*HTTPRecord, error)
GetRecordsByHostname retrieves HTTP records for a hostname within a project.
func (*Repository) GetRecordsByUUIDs ¶
func (r *Repository) GetRecordsByUUIDs(ctx context.Context, uuids []string) ([]*HTTPRecord, error)
GetRecordsByUUIDs retrieves HTTP records matching the given UUIDs.
func (*Repository) GetRecordsWithResponseBody ¶
func (r *Repository) GetRecordsWithResponseBody(ctx context.Context, projectUUID, afterUUID string, limit int, hosts ...HostTarget) ([]*HTTPRecord, error)
GetRecordsWithResponseBody returns HTTP records that have a non-empty response body, using UUID-based cursor pagination. Only columns needed for batch secret scanning are selected.
func (*Repository) GetRelatedRecords ¶
func (r *Repository) GetRelatedRecords(ctx context.Context, uuid string, limit int) ([]*HTTPRecord, error)
GetRelatedRecords finds HTTP records with the same hostname and a path matching the path-template of the given UUID's record. Default limit 10; excludes the source record itself. Results are filtered to the same path depth as the source record.
func (*Repository) GetScanByUUID ¶
GetScanByUUID retrieves a scan by its UUID
func (*Repository) GetScanRecordsByUUIDs ¶
func (r *Repository) GetScanRecordsByUUIDs(ctx context.Context, uuids []string) ([]*HTTPRecord, error)
GetScanRecordsByUUIDs retrieves HTTP records matching the given UUIDs, projecting only scanRecordColumns — what the dynamic-assessment scan feed consumes. This avoids loading (and JSON-deserializing) the parameters/technology/remarks jsonb columns plus ~25 unused scalar columns per record. Modules receive the reconstructed HttpRequestResponse (via recordToHttpRequestResponse), never the HTTPRecord, so no downstream consumer reads the dropped fields.
func (*Repository) GetUnprobedRecordsBySource ¶
func (r *Repository) GetUnprobedRecordsBySource(ctx context.Context, projectUUID, source, hostname string, limit int) ([]*HTTPRecord, error)
GetUnprobedRecordsBySource returns records with has_response=false for the given source and hostname.
func (*Repository) GetUserByUUID ¶
GetUserByUUID retrieves a user by UUID.
func (*Repository) GroupFindingsByValue ¶
func (r *Repository) GroupFindingsByValue(ctx context.Context, projectUUID string, opts GroupFindingOptions) (deleted int64, grouped int64, err error)
GroupFindingsByValue collapses findings that share an identical extracted value (e.g. the same leaked secret reported once per URL) into a single finding. The survivor (earliest by created_at) absorbs every duplicate's matched URLs into MatchedAt and a sample of their request/response pairs into AdditionalEvidence; the duplicates are then deleted. Grouping is keyed on (module_id, severity[, hostname], normalized extracted_results) — the value must match byte-for-byte, so findings with distinct or empty extracted values are never merged.
Modules listed in opts.ByModule are an exception: their findings collapse on (module_id, severity[, hostname]) alone, regardless of (or absent) an extracted value, and skip the Tags gate. This is for modules that fire once per asset where the per-URL value is noise (e.g. sourcemap-detect, a distinct .map filename per JS bundle).
Modules listed in opts.ByRule collapse on (module_id, rule_name, severity[, hostname]) — the same as ByModule but with the rule (module_name) kept in the key, so a module whose single id fronts many rules (secret-detect) folds repeats of one rule while keeping different rules apart. Their distinct values are unioned onto the survivor just like ByModule.
Returns the count of deleted findings and the number of groups that were collapsed.
func (*Repository) HasHostObservations ¶
func (r *Repository) HasHostObservations(ctx context.Context) bool
HasHostObservations reports whether this database can answer endpoint lookups at all.
A database written before this table existed has nothing stored, which is a fact about it and not a failure. Asked once up front so a read of an older file degrades to the cache-only behaviour silently, instead of failing a query per endpoint about a table it was never going to have. A read-only handle cannot be migrated into having one either, and missingColumns only tracks ALTER-added columns, never whole tables.
func (*Repository) HydrateFindingRequests ¶
func (r *Repository) HydrateFindingRequests(ctx context.Context, findings []*Finding)
HydrateFindingRequests populates the Request field on findings that were loaded without it — the list query (FindingsQueryBuilder) omits the request/response blobs for speed. It fetches id→request for the given findings in as few round-trips as one statement's worth of ids allows; findings that already carry a Request, or that have no id, are skipped. Used by display paths that need a finding's own PoC request (e.g. the tree's attack-URL rendering) without pulling the full raw view.
func (*Repository) InScopeHosts ¶
func (r *Repository) InScopeHosts(ctx context.Context, scopeCfg config.ScopeConfig, targets []string, projectUUID, scanUUID string) []HostTarget
InScopeHosts returns the distinct (scheme, hostname, port) origins in the project that are in scope for the supplied CLI targets. An origin is in scope when its hostname passes the scope matcher AND EITHER its (scheme, port) matches one of the targets' origins (default ports normalized like stored records: https→443, http→80) OR it was discovered during the current scan (records created at/after the scan's start). Returns nil when there are no targets (no filter — a project-wide pass). This is the single source of truth for the origin scoping the executor applies via WithHostScopes, so DynamicAssessment, KnownIssueScan, spidering/discovery seeds, and the scan-completion record count all derive the same set.
The full-origin match keeps e.g. localhost:3000 separate from localhost:8080 left in the project by a prior scan, while the current-scan provenance carve-out still lets THIS scan's own discoveries on a different port (e.g. the --intensity deep port sweep, or a followed cross-port link) be scanned. Records carry no scan_uuid, so provenance is keyed on the scan's start time. If no target yields a determinable scheme/port (e.g. all bare hosts), the scheme/port constraint is dropped — a safe fallback that never over-excludes.
func (*Repository) IncrementProcessedCount ¶
func (r *Repository) IncrementProcessedCount(ctx context.Context, scanUUID string, delta int64) error
IncrementProcessedCount adds delta to the scan's processed_count. Use this for phases that don't advance the cursor (discovery, spidering, etc.).
func (*Repository) ListAgentSections ¶
func (r *Repository) ListAgentSections(ctx context.Context, agenticScanUUID string) ([]*AgentSection, error)
ListAgentSections returns all sections for an agentic scan ordered by seq (execution order). Empty agenticScanUUID returns an empty slice.
func (*Repository) ListAgenticScanSummaries ¶
func (r *Repository) ListAgenticScanSummaries(ctx context.Context, projectUUID string, mode string, limit, offset int) ([]*AgenticScan, int64, error)
ListAgenticScanSummaries is ListAgenticScans with agenticScanBlobColumns dropped — same rows, same order, same total. Use it for any view that renders a run rather than replaying it; the omitted fields come back empty, so a caller that serializes the *AgenticScan verbatim (the `--json` CLI paths) must keep using ListAgenticScans.
func (*Repository) ListAgenticScans ¶
func (r *Repository) ListAgenticScans(ctx context.Context, projectUUID string, mode string, limit, offset int) ([]*AgenticScan, int64, error)
ListAgenticScans returns paginated agent runs for a project, ordered by created_at DESC, with every column populated. Callers that only render the run should prefer ListAgenticScanSummaries.
func (*Repository) ListCandidates ¶
func (r *Repository) ListCandidates(ctx context.Context, agenticScanUUID string, statuses ...string) ([]*AgentFindingCandidate, error)
ListCandidates returns candidates for an agentic scan, optionally filtered to the given statuses (empty statuses = all). Ordered by id so the verifier processes them in proposal order.
func (*Repository) ListFindings ¶
func (r *Repository) ListFindings(ctx context.Context, filters QueryFilters) ([]*Finding, int64, error)
ListFindings runs a filtered findings query and returns the matching page plus the total unfiltered count, in a single round-trip. The canonical entry point for callers that want paginated results — keeps FindingsQueryBuilder behind the repository boundary so they don't need to reach for Repository.DB().
func (*Repository) ListOASTInteractions ¶
func (r *Repository) ListOASTInteractions(ctx context.Context, projectUUID, scanUUID, protocol, moduleID, search string, limit, offset int) ([]*OASTInteraction, int64, error)
ListOASTInteractions returns a paginated, filtered list of OAST interactions. Heavy columns (raw_request, raw_response) are excluded for list performance.
func (*Repository) ListProjects ¶
ListProjects returns projects, optionally filtered by owner.
func (*Repository) ListScanLogs ¶
func (r *Repository) ListScanLogs(ctx context.Context, scanUUID string, level, phase string, limit, offset int) ([]*ScanLog, int64, error)
ListScanLogs returns log entries for a scan, ordered by created_at ascending. Both level and phase are optional filters; pass "" to skip.
func (*Repository) ListScans ¶
func (r *Repository) ListScans(ctx context.Context, projectUUID string, limit, offset int) ([]*Scan, int64, error)
ListScans returns scans ordered by created_at descending with limit/offset, filtered by project.
func (*Repository) ListUsers ¶
func (r *Repository) ListUsers(ctx context.Context) ([]*User, error)
ListUsers returns all users.
func (*Repository) LoadEnabledScopes ¶
LoadEnabledScopes loads enabled scope rules for a project, ordered by priority. An empty projectUUID means the default project — never "all projects". Falls back to the default project's scopes when a non-default project has none, but if the default project itself has no enabled scopes it returns empty rather than an unscoped query that would leak every project's rules across engagements.
func (*Repository) LookupHostObservation ¶
func (r *Repository) LookupHostObservation(ctx context.Context, projectUUID, scanUUID, hostname string, port int) (HostFacts, error)
LookupHostObservation returns the observation for one endpoint, or zero facts when none is stored.
One endpoint per call, rather than loading a run's observations up front. Preloading looked right for a streaming emit path - fetch once, then answer every record from memory - and was wrong in both directions. This table accumulates a row per endpoint per RUN, so the project-wide preload returned endpoints × runs rows to build a map of endpoints: on 20k endpoints across 25 runs, exporting a two-record database peaked at 842 MB against a 120 MB baseline, with export memory growing in scan history rather than in the corpus being exported. Resolving "latest per endpoint" in SQL instead still cost a full scan and a temp b-tree. And a paginated reader (`traffic -j`, 100 rows) paid all of it to ask about a handful of hosts.
A point lookup is index-only against idx_host_obs_project_host_id, and the caller memoizes, so the cost is one query per DISTINCT endpoint actually emitted and the memory is one entry per the same.
scanUUID empty widens the read to the whole project and takes the most recently RECORDED observation - the right answer for `export` of a database whose runs this process did not perform.
func (*Repository) OverwriteRecordResponseBody ¶
func (r *Repository) OverwriteRecordResponseBody(ctx context.Context, uuid string, rawResponse []byte) error
OverwriteRecordResponseBody replaces the stored raw response of the record with the given UUID and recomputes its derived fields (response_hash, response_norm_hash, response_words, response_content_length), keeping them consistent with dedup/resume/fingerprinting that key off those columns.
rawResponse must be a complete HTTP response (status line + headers + body). Used by the passive js-beautify module to overwrite a minified JS body with its beautified form in place. The derivation mirrors Record.FromRequestResponse so a rewritten record hashes identically to one ingested with the new body. (Distinct from UpdateRecordResponse, which is the replay feature's full response-field swap.)
func (*Repository) PauseScan ¶
func (r *Repository) PauseScan(ctx context.Context, scanUUID string) error
PauseScan sets a scan's status to "paused".
func (*Repository) PurgeProjectData ¶
func (r *Repository) PurgeProjectData(ctx context.Context, projectUUID string) error
PurgeProjectData deletes every row tied to projectUUID across all per-project tables. finding_records has no project_uuid, so it's pruned via a subquery on findings.id before the findings rows themselves are removed. Runs in a single transaction so a partial failure leaves the project intact.
func (*Repository) ReassignProjectData ¶
func (r *Repository) ReassignProjectData(ctx context.Context, sourceUUID, targetUUID string) error
ReassignProjectData moves all data owned by sourceUUID to targetUUID. This should be called before deleting a project so its records are not orphaned.
func (*Repository) RecordProjectUUID ¶
RecordProjectUUID returns just the project a record belongs to, for the existence-plus-scope check the mutating handlers perform before acting on it. Returns sql.ErrNoRows when the record does not exist, like GetRecordByUUID.
Projected on purpose: the callers read only this one field, and a record's raw_response alone can be megabytes.
func (*Repository) RefreshScanStats ¶
func (r *Repository) RefreshScanStats(ctx context.Context, scanUUID string) error
RefreshScanStats updates running scan stats during long-running scans where CompleteScan hasn't been called yet.
func (*Repository) ResetScanCursor ¶
func (r *Repository) ResetScanCursor(ctx context.Context, scanUUID string) error
ResetScanCursor resets the scan cursor to the beginning so all records are re-read on the next iteration (e.g., between seed and audit phases).
func (*Repository) ResumeScan ¶
func (r *Repository) ResumeScan(ctx context.Context, scanUUID string) error
ResumeScan sets a scan's status back to "running".
func (*Repository) SaveAgentSection ¶
func (r *Repository) SaveAgentSection(ctx context.Context, section *AgentSection) error
SaveAgentSection inserts a new bounded-section row. The uuid carries the section identity; a duplicate uuid is a no-op (idempotent on resume).
func (*Repository) SaveAnalysisArtifact ¶
func (r *Repository) SaveAnalysisArtifact( ctx context.Context, httpRecordUUID, kind, filename, mediaType, sha256 string, content []byte, metadata string, ) error
SaveAnalysisArtifact is the primitive-argument adapter used by input sources that deliberately avoid importing the database package in their interfaces.
func (*Repository) SaveAnalysisArtifactForRecord ¶
func (r *Repository) SaveAnalysisArtifactForRecord(ctx context.Context, artifact *AnalysisArtifact) error
SaveAnalysisArtifactForRecord stores a content-addressed immutable artifact beside an existing HTTP record. Duplicate writes of the same record/kind/hash are idempotent.
func (*Repository) SaveAuthenticationHostname ¶
func (r *Repository) SaveAuthenticationHostname(ctx context.Context, sh *AuthenticationHostname) error
SaveAuthenticationHostname upserts a single session hostname record. Conflict key: (project_uuid, hostname, session_name).
func (*Repository) SaveAuthenticationHostnames ¶
func (r *Repository) SaveAuthenticationHostnames(ctx context.Context, rows []*AuthenticationHostname) error
SaveAuthenticationHostnames batch-upserts session hostname records in a transaction.
func (*Repository) SaveCandidate ¶
func (r *Repository) SaveCandidate(ctx context.Context, cand *AgentFindingCandidate) (bool, error)
SaveCandidate inserts a proposed finding candidate, deduping on (agentic_scan_uuid, dedup_hash) via ON CONFLICT DO NOTHING so a looping model that re-proposes the same bug produces exactly one row. Returns inserted=true only when a new row was written (false on a dedup hit), so callers can keep an accurate proposed-count.
func (*Repository) SaveFinding ¶
func (r *Repository) SaveFinding(ctx context.Context, event *output.ResultEvent, httpRecordUUIDs []string, scanUUID string, projectUUID string) error
SaveFinding stores a vulnerability finding linked to HTTP records by UUIDs. Uses INSERT ON CONFLICT for atomic dedup when finding_hash is non-empty.
func (*Repository) SaveFindingDirect ¶
func (r *Repository) SaveFindingDirect(ctx context.Context, finding *Finding) error
SaveFindingDirect inserts a pre-built Finding directly (without ResultEvent conversion). Uses INSERT ON CONFLICT for atomic dedup when finding_hash is non-empty.
func (*Repository) SaveFindingReported ¶
SaveFindingReported is SaveFindingDirect that additionally reports whether a new finding row was inserted (true) vs collapsed into an existing finding by dedup (false). The autopilot report_finding tool uses it so its run counter only tallies distinct findings.
func (*Repository) SaveFindingsBatch ¶
func (r *Repository) SaveFindingsBatch(ctx context.Context, writes []FindingWrite) error
SaveFindingsBatch persists a batch of findings in a single transaction, coalescing what would otherwise be one transaction (and fsync) per finding. Findings are converted up front so a conversion error skips just that finding rather than aborting the batch. If the transaction itself fails (e.g. a database-level error), every finding is retried individually so one bad finding can't drop the rest — preserving the error isolation of per-finding SaveFinding while keeping the fast path a single transaction.
func (*Repository) SaveFindingsDirectBatch ¶
func (r *Repository) SaveFindingsDirectBatch(ctx context.Context, findings []*Finding) ([]FindingSaveResult, error)
SaveFindingsDirectBatch persists a batch of pre-built findings in a single transaction — the batch analogue of SaveFindingDirect, used by bulk importers to coalesce what would otherwise be one transaction (and fsync) per finding. The returned slice is aligned with the input; the top-level error is the first per-finding error, if any.
func (*Repository) SaveHostObservations ¶
func (r *Repository) SaveHostObservations(ctx context.Context, projectUUID, scanUUID string, obs []HostObservationInput) error
SaveHostObservations persists a run's host observations in bounded batches.
ON CONFLICT (scan_uuid, hostname, port) updates rather than inserting: within ONE run an endpoint has one observation, so a second write for it is a refinement (the TLS handshake finishing after the DNS answer, say) and not a second event. ACROSS runs the conflict cannot fire, which is the point - re-probing a host next month adds that month's observation and leaves this one intact.
func (*Repository) SaveOASTInteraction ¶
func (r *Repository) SaveOASTInteraction(ctx context.Context, interaction *OASTInteraction) error
SaveOASTInteraction stores an OAST interaction record.
func (*Repository) SaveRecord ¶
func (r *Repository) SaveRecord(ctx context.Context, httpRR *httpmsg.HttpRequestResponse, source string, projectUUID string) (string, error)
SaveRecord stores a denormalized HTTP record (request + response + host + parameters). The source identifies the origin of the record (e.g. "scanner", "ingest-cli", "ingest-server", "ingest-proxy"). Returns the UUID of the saved record. If a matching record already exists (same method, hostname, path, URL, and request body), the existing UUID is returned without inserting.
func (*Repository) SaveRecordBatch ¶
func (r *Repository) SaveRecordBatch(ctx context.Context, records []*httpmsg.HttpRequestResponse, source string, projectUUID string) ([]string, error)
SaveRecordBatch converts httpmsg.HttpRequestResponse objects to HTTPRecord models and batch-inserts them. This is the high-level batch equivalent of SaveRecord.
func (*Repository) SaveRecordWithParent ¶
func (r *Repository) SaveRecordWithParent(ctx context.Context, httpRR *httpmsg.HttpRequestResponse, source string, projectUUID string, parentUUID string) (string, error)
SaveRecordWithParent is SaveRecord with an explicit parent_uuid link, used to chain the hops of a followed redirect into one walkable sequence.
On a duplicate hit the existing row's parent is BACKFILLED but never overwritten (see AdoptRecordParent). A row can lose the race to its own chain: a passive module reporting on the final response persists it as finding evidence before the item's own save runs, and that earlier row then wins the dedup — leaving the last link of the chain dangling unless the parent is applied after the fact.
func (*Repository) SaveRecordsBatch ¶
func (r *Repository) SaveRecordsBatch(ctx context.Context, records []*HTTPRecord) ([]string, error)
SaveRecordsBatch inserts multiple HTTP records in a single transaction. Returns the UUIDs of all successfully inserted records.
func (*Repository) ScanSeverityTally ¶
ScanSeverityTally returns a scan's persisted finding counts keyed by severity name, omitting severities with no findings. It reads the same rows the scan row's own counters are computed from, so a consumer reading the machine event stream's scan.finished and a consumer querying the database afterwards agree.
func (*Repository) SetRecordTechnology ¶
SetRecordTechnology batch-writes the detected technology stack onto HTTPRecords identified by UUID, replacing whatever the column held.
Replace rather than merge (the shape AppendRemarks uses) because technology is not accumulated analyst annotation: it is the fingerprint modules' verdict about the host, recomputed from scratch on every scan, and a merge would make a record carry a stack the target stopped running two scans ago.
Records are grouped by their technology list before writing, because the source is host-scoped: every record on one host shares one list, so a corpus of N records over H hosts costs H UPDATEs rather than N. The per-value UUID set is still chunked so a single busy host cannot build an IN() clause past the driver's parameter limit.
func (*Repository) UpdateAgentSection ¶
func (r *Repository) UpdateAgentSection(ctx context.Context, section *AgentSection) error
UpdateAgentSection updates a section row identified by uuid. OmitZero lets the caller pass a partial struct (e.g. only status + closing_summary + ended_at) without clobbering columns set at BeginSection time.
func (*Repository) UpdateAgenticScan ¶
func (r *Repository) UpdateAgenticScan(ctx context.Context, run *AgenticScan) error
UpdateAgenticScan updates an agent run record by UUID. Uses OmitZero so callers (SwarmRunner.Run and the API enrichAgenticScanRecord helpers) can pass partial structs without zeroing fields they don't intend to touch. To explicitly clear a field, use a column-list update via NewUpdate().
func (*Repository) UpdateAgenticScanStorageURL ¶
func (r *Repository) UpdateAgenticScanStorageURL(ctx context.Context, agenticScanUUID, storageURL string) error
UpdateAgenticScanStorageURL sets the storage_url field on an agentic scan record.
func (*Repository) UpdateCandidateStatus ¶
func (r *Repository) UpdateCandidateStatus(ctx context.Context, uuid, status, verdictReason string, promotedFindingID int64) error
UpdateCandidateStatus records a verifier verdict on a candidate by uuid, setting status, verdict_reason, promoted_finding_id (when > 0), and stamping verified_at. Used by the fresh-context verifier to mark confirmed / rejected / needs_evidence.
func (*Repository) UpdateFindingSeverity ¶
UpdateFindingSeverity sets the severity of a single finding by ID. Returns sql.ErrNoRows if no finding matches.
func (*Repository) UpdateFindingStatus ¶
UpdateFindingStatus sets the lifecycle status of a single finding by ID. Returns sql.ErrNoRows if no finding matches.
func (*Repository) UpdateFindingStatusByHash ¶
func (r *Repository) UpdateFindingStatusByHash(ctx context.Context, agenticScanUUID, findingHash, status string) (int64, error)
UpdateFindingStatusByHash sets the lifecycle status of findings matching a finding_hash within an agentic_scan_uuid scope. Used by the swarm triage writeback to promote draft findings to triaged / false_positive based on agent verdicts. Returns the number of rows updated.
func (*Repository) UpdateFindingTriage ¶
func (r *Repository) UpdateFindingTriage(ctx context.Context, id int64, severity, description string) error
UpdateFindingTriage sets severity and description on a single finding in one statement. Used by the agent triage flow so a false-positive verdict can't land a half-updated row (severity downgraded but reasoning lost) if the process is killed between two separate UPDATEs.
func (*Repository) UpdateProject ¶
func (r *Repository) UpdateProject(ctx context.Context, project *Project) error
UpdateProject updates an existing project.
func (*Repository) UpdateRecordAnnotations ¶
func (r *Repository) UpdateRecordAnnotations(ctx context.Context, uuid string, riskScore *int, remarks []string) error
UpdateRecordAnnotations updates the risk_score and/or remarks of an HTTP record. Only non-nil fields are updated. Returns an error if no record matches the UUID.
func (*Repository) UpdateRecordResponse ¶
func (r *Repository) UpdateRecordResponse(ctx context.Context, uuid string, update *RecordResponseUpdate) error
UpdateRecordResponse replaces the response fields of an existing HTTP record.
func (*Repository) UpdateRiskScores ¶
UpdateRiskScores batch-updates risk_score on HTTPRecords identified by UUID. Uses CASE/WHEN SQL to update up to 500 records per statement, minimizing roundtrips.
func (*Repository) UpdateScan ¶
func (r *Repository) UpdateScan(ctx context.Context, scan *Scan) error
UpdateScan updates an existing scan record
func (*Repository) UpdateScanPartial ¶
func (r *Repository) UpdateScanPartial(ctx context.Context, scan *Scan) error
UpdateScanPartial updates a scan row by UUID, skipping fields whose Go value is the zero value. Use this when the caller only wants to touch a subset of fields (e.g. an API PATCH that should leave omitted fields unchanged). To explicitly clear a field, use a column-level Set via NewUpdate().
func (*Repository) UpdateScanStorageURL ¶
func (r *Repository) UpdateScanStorageURL(ctx context.Context, scanUUID, storageURL string) error
UpdateScanStorageURL sets the storage_url field on a scan record.
func (*Repository) UpdateSurfaceScores ¶
UpdateSurfaceScores batch-updates surface_score on HTTPRecords identified by UUID. Written by the surface_scoring passive module; see HTTPRecord.SurfaceScore for why this is a separate column from risk_score rather than a second writer of the same one.
func (*Repository) UpsertSnapshotRecord ¶
func (r *Repository) UpsertSnapshotRecord(ctx context.Context, httpRR *httpmsg.HttpRequestResponse, source string, projectUUID string) (uuid, outcome string, err error)
UpsertSnapshotRecord stores a Burp snapshot record idempotently. When an existing request later gains a response, or its response changes, the response-derived columns are refreshed in place while the stable UUID and finding links are preserved. outcome is inserted, updated, or unchanged.
func (*Repository) UpsertUser ¶
func (r *Repository) UpsertUser(ctx context.Context, user *User) error
UpsertUser inserts a new user or updates name/email if the UUID already exists. Returns the user's UUID.
func (*Repository) WalkJavaScriptRecords ¶
func (r *Repository) WalkJavaScriptRecords(ctx context.Context, projectUUID, hostname string, limit int, fn func(recordURL, contentType string, body []byte) error) error
WalkJavaScriptRecords streams JavaScript responses already stored for a host by earlier phases (spidering, proxy/Burp ingestion) so the discovery engine can feed browser-collected bundles through JSTangle even though they live in the main DB rather than the ephemeral discovery sitemap. Each callback receives the record URL, its response content-type, and the decoded (gzip-inflated) response body. JavaScript is matched by content-type or a .js/.mjs URL suffix; the walk is bounded by limit and ordered by uuid for determinism. A callback error stops the walk and is returned. This satisfies the source.spideredJSProvider optional interface structurally (primitive-typed) so neither package imports the other.
type RiskPrioritizedDBInputSource ¶
type RiskPrioritizedDBInputSource struct {
// contains filtered or unexported fields
}
RiskPrioritizedDBInputSource processes high-risk records first, then falls back to normal cursor-based order. It implements source.InputSource.
func NewRiskPrioritizedDBInputSource ¶
func NewRiskPrioritizedDBInputSource(db *DB, repo *Repository, scanUUID string) *RiskPrioritizedDBInputSource
NewRiskPrioritizedDBInputSource creates a DBInputSource that processes records with risk_score > 0 first (highest risk first), then continues with the normal cursor-based order for remaining records.
func (*RiskPrioritizedDBInputSource) Close ¶
func (s *RiskPrioritizedDBInputSource) Close() error
Close stops the source.
func (*RiskPrioritizedDBInputSource) CoalescedDropped ¶
func (s *RiskPrioritizedDBInputSource) CoalescedDropped() int
CoalescedDropped returns how many records the param-shape coalescing pass removed from this scan's iteration list. Valid after the snapshot loads (i.e. after the first Next / after Execute). Zero when coalescing is disabled.
func (*RiskPrioritizedDBInputSource) Next ¶
Next streams records high-risk-first then chronologically, via bounded keyset pages, so memory scales with the page size (not the whole eligible table).
func (*RiskPrioritizedDBInputSource) WithHostScopes ¶
func (s *RiskPrioritizedDBInputSource) WithHostScopes(hosts []HostTarget) *RiskPrioritizedDBInputSource
WithHostScopes sets an in-scope origin filter so only records matching these (scheme, hostname, port) origins are returned.
func (*RiskPrioritizedDBInputSource) WithParamShapeCoalescing ¶
func (s *RiskPrioritizedDBInputSource) WithParamShapeCoalescing(maxSamples int) *RiskPrioritizedDBInputSource
WithParamShapeCoalescing enables param-shape coalescing of the snapshot: GET records that share a (host, path, query-param-name-set) are reduced to at most maxSamples value-distinct representatives, cutting redundant dynamic-assessment fan-out over value-only-different URLs (e.g. /search?q=1..N). Records stay in the database; only this scan's iteration list is pruned. The scan cursor still advances past every record, so coalesced-away records are not re-scanned in a later feedback round. maxSamples <= 0 (the default) disables it.
type Scan ¶
type Scan struct {
bun.BaseModel `bun:"table:scans,alias:sc" json:"-"`
UUID string `bun:"uuid,pk,notnull" json:"uuid"`
ProjectUUID string `bun:"project_uuid,notnull" json:"project_uuid"`
Name string `bun:"name,nullzero" json:"name"`
Description string `bun:"description,nullzero" json:"description"`
Status string `bun:"status,notnull,default:'running'" json:"status"` // running, paused, completed, failed, cancelled
// Target can be a URL, domain, IP, CIDR, or file path (for imported requests)
Target string `bun:"target,nullzero" json:"target"`
Modules string `bun:"modules,nullzero" json:"modules"`
Threads int `bun:"threads,default:0" json:"threads"`
// Scan context
Profile string `bun:"profile,nullzero" json:"profile"` // scanning profile used (light, full, api, etc.)
SourcePath string `bun:"source_path,nullzero" json:"source_path"` // source code path for audit/agent scans
SourceType string `bun:"source_type,nullzero" json:"source_type"` // local, git-url, gcs
Tags []string `bun:"tags,type:jsonb,nullzero" json:"tags"` // arbitrary tags for filtering/grouping
TriggeredBy string `bun:"triggered_by,nullzero" json:"triggered_by"` // user, schedule, webhook, agent
AgenticScanUUID string `bun:"agentic_scan_uuid,nullzero" json:"agentic_scan_uuid"` // link to agentic_scan that spawned this scan
// Soft reference to the HTTP record that triggered this scan (scan-on-receive)
HTTPRecordUUID string `bun:"http_record_uuid,nullzero" json:"http_record_uuid"`
// Cursor-based scan tracking
ScanSource string `bun:"scan_source,nullzero" json:"scan_source"` // "cli", "api", "scan-on-receive"
ScanMode string `bun:"scan_mode,nullzero" json:"scan_mode"` // "incremental" or "full"
StartCursorAt time.Time `bun:"start_cursor_at,nullzero" json:"start_cursor_at"` // cursor position at scan start
StartCursorUUID string `bun:"start_cursor_uuid,nullzero" json:"start_cursor_uuid"`
CursorAt time.Time `bun:"cursor_at,nullzero" json:"cursor_at"` // current cursor position
CursorUUID string `bun:"cursor_uuid,nullzero" json:"cursor_uuid"`
ProcessedCount int64 `bun:"processed_count,default:0" json:"processed_count"`
StartedAt time.Time `bun:"started_at,notnull,default:current_timestamp" json:"started_at"`
FinishedAt time.Time `bun:"finished_at,nullzero" json:"finished_at"`
DurationMs int64 `bun:"duration_ms,default:0" json:"duration_ms"`
// Summary fields (populated at end of scan)
TotalRequests int64 `bun:"total_requests,default:0" json:"total_requests"`
TotalFindings int64 `bun:"total_findings,default:0" json:"total_findings"`
// Risk level counts (populated at end of scan)
CriticalCount int64 `bun:"critical_count,default:0" json:"critical_count"`
HighCount int64 `bun:"high_count,default:0" json:"high_count"`
MediumCount int64 `bun:"medium_count,default:0" json:"medium_count"`
LowCount int64 `bun:"low_count,default:0" json:"low_count"`
InfoCount int64 `bun:"info_count,default:0" json:"info_count"`
SuspectCount int64 `bun:"suspect_count,default:0" json:"suspect_count"`
// Error count (populated at end of scan)
ErrorMessage string `bun:"error_message,nullzero" json:"error_message"`
StorageURL string `bun:"storage_url,nullzero" json:"storage_url"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp" json:"updated_at"`
}
Scan represents a scan session
type ScanLog ¶
type ScanLog struct {
bun.BaseModel `bun:"table:scan_logs,alias:sl" json:"-"`
ID int64 `bun:"id,pk,autoincrement" json:"id"`
ProjectUUID string `bun:"project_uuid,notnull" json:"project_uuid"`
ScanUUID string `bun:"scan_uuid,notnull" json:"scan_uuid"`
Level string `bun:"level,notnull" json:"level"` // trace, info, warn, error
Phase string `bun:"phase,nullzero" json:"phase,omitempty"` // discovery, spidering, dynamic-assessment, etc.
Message string `bun:"message,notnull" json:"message"`
Metadata string `bun:"metadata,nullzero" json:"metadata,omitempty"` // JSON blob for extra context
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
}
ScanLog represents a log entry for a scan session.
type ScanLogger ¶
type ScanLogger struct {
// contains filtered or unexported fields
}
ScanLogger provides a convenient interface for writing scan log entries. It is safe to use with a nil Repository (all methods become no-ops).
func NewScanLogger ¶
func NewScanLogger(repo *Repository, scanUUID string) *ScanLogger
NewScanLogger creates a ScanLogger for the given scan. If repo is nil, all logging methods are no-ops so callers don't need nil checks.
func (*ScanLogger) Close ¶
func (l *ScanLogger) Close()
Close stops the batcher goroutine and flushes any remaining entries.
func (*ScanLogger) Error ¶
func (l *ScanLogger) Error(phase, message string)
Error logs an error message.
func (*ScanLogger) Flush ¶
func (l *ScanLogger) Flush()
Flush writes all buffered trace entries to the database.
func (*ScanLogger) Info ¶
func (l *ScanLogger) Info(phase, message string)
Info logs an informational message.
func (*ScanLogger) InfoWithMeta ¶
func (l *ScanLogger) InfoWithMeta(phase, message string, meta map[string]interface{})
InfoWithMeta logs an informational message with structured metadata.
func (*ScanLogger) StartBatcher ¶
func (l *ScanLogger) StartBatcher()
StartBatcher launches a background goroutine that periodically flushes buffered trace entries to the database. Call Close() to stop and flush.
func (*ScanLogger) Trace ¶
func (l *ScanLogger) Trace(phase, message string)
Trace logs a trace-level message (raw console output). These entries are buffered and flushed in batches to avoid per-line DB inserts.
func (*ScanLogger) Warn ¶
func (l *ScanLogger) Warn(phase, message string)
Warn logs a warning message.
type ScanSessionStats ¶
type ScanSessionStats struct {
ScanUUID string `json:"scan_uuid"`
Name string `json:"name,omitempty"`
Status string `json:"status"`
ProcessedCount int64 `json:"processed_count"`
ScanMode string `json:"scan_mode,omitempty"`
}
ScanSessionStats holds statistics for a scan session
type ScanView ¶
type ScanView struct {
*Scan
Target string `json:"target"`
Modules string `json:"modules"`
TotalActiveModules int `json:"total_active_modules"`
TotalPassiveModules int `json:"total_passive_modules"`
}
ScanView is a display-oriented wrapper around Scan used by listing endpoints (CLI `db ls --table scans --json` and REST `GET /api/scans`). It overrides the `modules` JSON field so a fully-populated module list renders as "all" instead of a multi-kilobyte CSV, and surfaces active/passive module counts for quick tracking without forcing callers to split the CSV themselves.
type Scope ¶
type Scope struct {
bun.BaseModel `bun:"table:scopes,alias:s" json:"-"`
ID int64 `bun:"id,pk,autoincrement" json:"id"`
ProjectUUID string `bun:"project_uuid,notnull" json:"project_uuid"`
Name string `bun:"name,notnull" json:"name"`
Description string `bun:"description,nullzero" json:"description,omitempty"`
RuleType string `bun:"rule_type,notnull" json:"rule_type"` // "include" or "exclude"
HostPattern string `bun:"host_pattern,nullzero" json:"host_pattern,omitempty"`
PathPattern string `bun:"path_pattern,nullzero" json:"path_pattern,omitempty"`
ContentTypePattern string `bun:"content_type_pattern,nullzero" json:"content_type_pattern,omitempty"` // e.g., "image/*" to exclude
Methods []string `bun:"methods,type:jsonb,nullzero" json:"methods,omitempty"`
Ports []int `bun:"ports,type:jsonb,nullzero" json:"ports,omitempty"`
Schemes []string `bun:"schemes,type:jsonb,nullzero" json:"schemes,omitempty"`
Priority int `bun:"priority,notnull,default:100" json:"priority"`
Enabled bool `bun:"enabled,notnull,default:true" json:"enabled"`
HitCount int64 `bun:"hit_count,default:0" json:"hit_count"`
LastMatchedAt time.Time `bun:"last_matched_at,nullzero" json:"last_matched_at,omitempty"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp" json:"updated_at"`
}
Scope defines URL/request scope rules (firewall-style: first match wins)
type ScopeEvaluator ¶
type ScopeEvaluator struct {
// contains filtered or unexported fields
}
ScopeEvaluator evaluates URLs against scope rules (firewall-style: first match wins, default=exclude)
func NewScopeEvaluator ¶
func NewScopeEvaluator(rules []*Scope) *ScopeEvaluator
NewScopeEvaluator creates a new evaluator from pre-loaded scope rules
type SeverityCount ¶
type SeverityCount struct {
Severity string `bun:"severity" json:"severity"`
Count int64 `bun:"count" json:"count"`
}
SeverityCount holds a severity label and its count.
type StatusCodeBreakdown ¶
type StatusCodeBreakdown struct {
Success int64 `json:"success"` // 2xx
Redirect int64 `json:"redirect"` // 3xx
ClientErr int64 `json:"client_err"` // 4xx
ServerErr int64 `json:"server_err"` // 5xx
ByCode map[int]int64 `json:"by_code"`
}
StatusCodeBreakdown holds status code statistics
type StoredHostFacts ¶
type StoredHostFacts struct {
// contains filtered or unexported fields
}
StoredHostFacts serves a run's persisted observations, falling back to the process caches.
The fallback order is deliberate. The stored row is the durable answer and survives both cache eviction and the process; the caches can still be ahead of it for an endpoint observed after the observations were written. Preferring the stored row means a large sweep emits the same facts for its first host as for its last, which the caches alone could not do: they hold 8192 entries and evict in insertion order, so on a bigger sweep the hosts resolved EARLIEST had their answers dropped before the export that wanted them.
Resolution is LAZY and memoized: one query per distinct endpoint the caller actually asks about, not a preload of the run's observations. See Repository.LookupHostObservation for why preloading was the wrong shape.
Every method is nil-receiver safe and a nil *StoredHostFacts behaves as "process caches only", so callers never branch on it.
func NewStoredHostFacts ¶
func NewStoredHostFacts(ctx context.Context, repo *Repository, projectUUID, scanUUID string) *StoredHostFacts
NewStoredHostFacts prepares an endpoint-fact reader for an emit pass. A nil repo, or a database predating the observations table, yields a reader that answers from the process caches alone.
func (*StoredHostFacts) Facts ¶
func (s *StoredHostFacts) Facts(hostname string, port int) HostFacts
Facts returns what was observed about one endpoint.
func (*StoredHostFacts) FactsMap ¶
func (s *StoredHostFacts) FactsMap(hostname string, port int) map[string]any
FactsMap is Facts rendered as JSON keys for the map-shaped agent views, memoized alongside the facts so the render cost is per endpoint, not per record.
func (*StoredHostFacts) MissingCount ¶
func (s *StoredHostFacts) MissingCount() int
MissingCount is how many distinct endpoints this pass could describe from neither the stored observations nor the process caches.
func (*StoredHostFacts) Stored ¶
func (s *StoredHostFacts) Stored() int
Stored reports how many distinct endpoints this pass could describe.
type UUIDListDBInputSource ¶
type UUIDListDBInputSource struct {
// contains filtered or unexported fields
}
UUIDListDBInputSource iterates over a pre-defined list of HTTP record UUIDs, fetching each from the database and converting to WorkItems. It implements source.InputSource.
func NewUUIDListDBInputSource ¶
func NewUUIDListDBInputSource(repo *Repository, uuids []string) *UUIDListDBInputSource
NewUUIDListDBInputSource creates a new UUIDListDBInputSource for the given UUIDs.
func (*UUIDListDBInputSource) Close ¶
func (s *UUIDListDBInputSource) Close() error
Close stops the source. After Close, Next will return io.EOF.
type User ¶
type User struct {
bun.BaseModel `bun:"table:users,alias:u" json:"-"`
UUID string `bun:"uuid,pk,notnull" json:"uuid"`
Email string `bun:"email,nullzero" json:"email,omitempty"`
Name string `bun:"name,nullzero" json:"name,omitempty"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"created_at"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp" json:"updated_at"`
}
User represents a system user.
type WriteResult ¶
WriteResult is the outcome of a single record write.
Source Files
¶
- converters.go
- db.go
- dbsource.go
- detect.go
- doc.go
- finding_writer.go
- host_facts.go
- like.go
- merge.go
- merge_lock.go
- message_regions.go
- models.go
- param_shape.go
- path_template.go
- query.go
- query_group.go
- record_accessors.go
- record_writer.go
- redirect_location.go
- repository.go
- repository_agent_state.go
- repository_agentic_scans.go
- repository_artifacts.go
- repository_dedup.go
- repository_findings.go
- repository_host_observations.go
- repository_oast.go
- repository_projects.go
- repository_records.go
- repository_scans.go
- repository_sessions.go
- scan_logger.go
- scan_view.go
- schema.go
- scope.go
- session_hostname_convert.go
- sqlchunk.go
- stats.go
- update.go