Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8644104
chore: add data/ to gitignore, fix malformed line
0xmanhnv Apr 14, 2026
2de006b
fix: properly separate data/ entry in gitignore
0xmanhnv Apr 14, 2026
7067ada
chore: sync main with working directory
0xmanhnv Apr 14, 2026
fdd42e3
refactor(api): consolidate state-history, optimize facets, deprecate …
0xmanhnv Apr 14, 2026
d97c919
fix: suppress noisy 'no items' debug logs from controller reconciliation
0xmanhnv Apr 14, 2026
08e6f36
refactor(api): consolidate 3 standalone prefixes into parent groups
0xmanhnv Apr 14, 2026
4b0f847
refactor(api): remove deprecated route groups (quick-scan, scan-manag…
0xmanhnv Apr 14, 2026
aaee578
refactor(api): standardize endpoint naming conventions
0xmanhnv Apr 14, 2026
4823102
chore: remove tracked data/ attachments (gitignored)
0xmanhnv Apr 14, 2026
ca4211f
feat: remediation campaign PATCH, threat intel daily cron controller
0xmanhnv Apr 14, 2026
61a830c
feat: verification scan automation — POST /findings/{id}/request-veri…
0xmanhnv Apr 14, 2026
7d11300
fix: add ListAllNodes/ListAllEdges stubs to test mocks + verification…
0xmanhnv Apr 14, 2026
b062308
fix: auto-detect subdomain type when collector sends root_domain prop…
0xmanhnv Apr 14, 2026
7b7e540
fix: auto-detect subdomains from domain name structure + migration 133
0xmanhnv Apr 14, 2026
2d094d9
fix: update test mocks for ListAllNodes + NewAttackSurfaceService arity
0xmanhnv Apr 14, 2026
605be51
fix: domains page flat mode when filtered by type
0xmanhnv Apr 14, 2026
204c663
chore: remove data/ from tracking
0xmanhnv Apr 14, 2026
ac8736d
feat(migration): 000134 — extract DNS fields from nested JSONB to fla…
0xmanhnv Apr 14, 2026
ad557ce
feat: auto-extract DNS fields from nested properties on ingest
0xmanhnv Apr 14, 2026
ce557f5
feat: control testing create dialog + MITRE coverage table + ticketin…
0xmanhnv Apr 14, 2026
1f57391
feat: snake_case property normalization, Jira sync, security fixes
0xmanhnv Apr 14, 2026
e293b9e
feat: BU update endpoint, threat intel sync, risk reduction tracking
0xmanhnv Apr 14, 2026
09be9a4
feat: filter tags by asset type, fix ListDistinctTags signature
0xmanhnv Apr 14, 2026
7451ed7
feat: server-side metadata counts via count_by parameter
0xmanhnv Apr 14, 2026
f0c6c62
feat: relationship suggestions — auto-detect + review workflow
0xmanhnv Apr 14, 2026
3f6e6d4
fix: enrich suggestions with asset names/types via SQL JOIN
0xmanhnv Apr 14, 2026
7bbaefd
feat: suggestions search, batch approve, sort by confidence
0xmanhnv Apr 14, 2026
2bd6a51
fix: use member_of instead of cname_of for subdomain→root relationship
0xmanhnv Apr 14, 2026
7190e45
fix: re-scan deletes stale pending suggestions before regenerating
0xmanhnv Apr 14, 2026
97df5c2
fix: relationship suggestions audit — security, scalability, UX
0xmanhnv Apr 14, 2026
db0bcfb
fix(security): cap slice preallocation to prevent excessive memory al…
0xmanhnv Apr 14, 2026
aabf965
fix(security): use constant for slice preallocation instead of user-d…
0xmanhnv Apr 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cmd/server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,14 @@ func NewHandlers(deps *HandlerDeps) routes.Handlers {
// CTEM Discovery - Network Services, State History & Relationships
AssetService: handler.NewAssetServiceHandler(repos.AssetService, repos.Asset, v, log),
AssetStateHistory: handler.NewAssetStateHistoryHandler(repos.AssetStateHistory, repos.Asset, v, log),
AssetRelationship: handler.NewAssetRelationshipHandler(svc.AssetRelationship, v, log),
AssetRelationship: handler.NewAssetRelationshipHandler(svc.AssetRelationship, v, log),
RelationshipSuggestion: handler.NewRelationshipSuggestionHandler(svc.RelationshipSuggestion, log),

// Vulnerabilities & Exposures
Vulnerability: vulnHandler,
FindingActivity: handler.NewFindingActivityHandler(svc.FindingActivity, svc.Vulnerability, log),
FindingActions: handler.NewFindingActionsHandler(svc.FindingActions, log),
JiraWebhook: handler.NewJiraWebhookHandler(svc.JiraSync, log),
Exposure: handler.NewExposureHandler(svc.Exposure, svc.User, v, log),
ThreatIntel: handler.NewThreatIntelHandler(svc.ThreatIntel, v, log),
CredentialImport: handler.NewCredentialImportHandler(svc.CredentialImport, v, log),
Expand Down
6 changes: 4 additions & 2 deletions cmd/server/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ type Repositories struct {
ScopeSchedule *postgres.ScopeScheduleRepository
AssetService *postgres.AssetServiceRepository // CTEM: Network services on assets
AssetStateHistory *postgres.AssetStateHistoryRepository // CTEM: State change audit log
AssetRelationship *postgres.AssetRelationshipRepository // CTEM: Asset topology graph
AssetRelationship *postgres.AssetRelationshipRepository // CTEM: Asset topology graph
RelationshipSuggestion *postgres.RelationshipSuggestionRepository // CTEM: Relationship suggestions

// Vulnerabilities & Findings
Vulnerability *postgres.VulnerabilityRepository
Expand Down Expand Up @@ -168,7 +169,8 @@ func NewRepositories(db *postgres.DB) *Repositories {
ScopeSchedule: postgres.NewScopeScheduleRepository(db),
AssetService: postgres.NewAssetServiceRepository(db), // CTEM: Network services
AssetStateHistory: postgres.NewAssetStateHistoryRepository(db), // CTEM: State change audit
AssetRelationship: postgres.NewAssetRelationshipRepository(db), // CTEM: Asset topology graph
AssetRelationship: postgres.NewAssetRelationshipRepository(db), // CTEM: Asset topology graph
RelationshipSuggestion: postgres.NewRelationshipSuggestionRepository(db), // CTEM: Relationship suggestions

// Vulnerabilities & Findings
Vulnerability: postgres.NewVulnerabilityRepository(db),
Expand Down
16 changes: 13 additions & 3 deletions cmd/server/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ type Services struct {
Asset *app.AssetService
AssetGroup *app.AssetGroupService
AssetType *app.AssetTypeService
AssetRelationship *app.AssetRelationshipService
Scope *app.ScopeService
AssetRelationship *app.AssetRelationshipService
RelationshipSuggestion *app.RelationshipSuggestionService
Scope *app.ScopeService
AttackSurface *app.AttackSurfaceService

// Configuration (read-only system config)
Expand Down Expand Up @@ -159,6 +160,9 @@ type Services struct {
APIKey *app.APIKeyService
Webhook *app.WebhookService

// Jira Bidirectional Sync
JiraSync *app.JiraSyncService

// AI Triage
AITriage *app.AITriageService

Expand Down Expand Up @@ -224,8 +228,9 @@ func NewServices(deps *ServiceDeps) (*Services, error) {
s.AssetGroup = app.NewAssetGroupService(repos.AssetGroup, log)
s.AssetType = app.NewAssetTypeService(repos.AssetType, repos.AssetTypeCat, log)
s.Scope = app.NewScopeService(repos.ScopeTarget, repos.ScopeExcl, repos.ScopeSchedule, repos.Asset, log)
s.AttackSurface = app.NewAttackSurfaceService(repos.Asset, log)
s.AttackSurface = app.NewAttackSurfaceService(repos.Asset, repos.AssetRelationship, log)
s.AssetRelationship = app.NewAssetRelationshipService(repos.AssetRelationship, repos.Asset, log)
s.RelationshipSuggestion = app.NewRelationshipSuggestionService(repos.RelationshipSuggestion, repos.Asset, repos.AssetRelationship, log)

// Initialize finding source service (read-only system configuration)
s.FindingSource = app.NewFindingSourceService(repos.FindingSource, repos.FindingSourceCat, log)
Expand Down Expand Up @@ -353,6 +358,7 @@ func NewServices(deps *ServiceDeps) (*Services, error) {
// Initialize API Key & Webhook services
s.APIKey = app.NewAPIKeyService(repos.APIKey, log)
s.Webhook = app.NewWebhookService(repos.Webhook, s.Encryptor, log)
s.JiraSync = app.NewJiraSyncService(repos.Finding, log)

// Initialize integration & notification services
s.Integration = app.NewIntegrationService(repos.Integration, repos.IntegrationSCMExt, s.Encryptor, log)
Expand Down Expand Up @@ -462,6 +468,10 @@ func NewServices(deps *ServiceDeps) (*Services, error) {
scan.WithProfileRepo(repos.ScanProfile),
)

// Wire verification scan trigger: allows FindingActionsService to launch targeted scans
// when a finding transitions to fix_applied and the user requests scan-based verification.
s.FindingActions.SetVerificationScanTrigger(app.NewVerificationScanTriggerAdapter(s.Scan))

// Create adapters for pipeline sub-package
pipelineAuditAdapter := app.NewPipelineAuditServiceAdapter(s.Audit)
pipelineAgentSelectorAdapter := app.NewPipelineAgentSelectorAdapter(s.AgentSelector)
Expand Down
17 changes: 17 additions & 0 deletions cmd/server/workers.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,23 @@ func NewWorkers(deps *WorkerDeps) (*Workers, error) {
},
))

// Threat intel — daily EPSS + KEV refresh (fetches + persists to DB)
w.ControllerManager.Register(controller.NewThreatIntelRefreshController(
svc.ThreatIntel,
log.With("controller", "threat-intel-refresh"),
))

// Control test scheduler — daily sweep to mark stale detection coverage as overdue
w.ControllerManager.Register(controller.NewControlTestSchedulerController(
repos.ControlTest,
&controller.ControlTestSchedulerConfig{
Interval: 24 * time.Hour,
StaleDays: 30,
BatchSize: 500,
Logger: log.With("controller", "control-test-scheduler"),
},
))

return w, nil
}

Expand Down
27 changes: 15 additions & 12 deletions configs/relationship-types.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ types:
# Code hierarchy
- sources: [repository]
targets: [container_image]
# DNS hierarchy
- sources: [domain]
targets: [subdomain]

- id: exposes
category: attack_surface_mapping
Expand All @@ -161,27 +164,27 @@ types:
direct: Resolves To
inverse: Resolved By
description: >-
Literal DNS A/AAAA resolution — a domain resolves to an IP record
or a load balancer that owns that IP. STRICT semantic: target
MUST be the network endpoint, not the server that happens to own
the IP. For "this domain leads to this server / website" use
Exposes. For subdomain → parent domain or CNAME aliases use
Cname Of.
Literal DNS A/AAAA resolution — a domain or subdomain resolves to
an IP record or a load balancer that owns that IP. STRICT semantic:
target MUST be the network endpoint, not the server that happens
to own the IP. For "this domain leads to this server / website"
use Exposes. For subdomain → parent domain hierarchy use Contains.
constraints:
- sources: [domain]
- sources: [domain, subdomain]
targets: [ip_address, load_balancer]

- id: cname_of
category: attack_surface_mapping
direct: CNAME Of
inverse: Has CNAME
description: >-
DNS aliasing — this name is a CNAME for that name. Also used for
subdomain → parent domain logical relationships. Distinct from
Resolves To which captures the final IP/host record.
DNS CNAME aliasing — this name is a CNAME record pointing to
another name. Strictly for actual DNS CNAME records, NOT for
subdomain hierarchy (use Contains for that). Distinct from
Resolves To which captures the final A/AAAA IP record.
constraints:
- sources: [domain]
targets: [domain]
- sources: [domain, subdomain]
targets: [domain, subdomain]

# ---- Attack Path Analysis ----------------------------------------------

Expand Down
36 changes: 36 additions & 0 deletions internal/app/adapters.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,3 +320,39 @@ func convertToPipelineValidationResult(r *ValidationResult) *pipeline.Validation
Errors: errors,
}
}

// =============================================================================
// Verification Scan Trigger Adapter
// =============================================================================

// verificationScanTriggerAdapter adapts scan.Service to FindingActionsService.VerificationScanTrigger.
type verificationScanTriggerAdapter struct {
svc *scan.Service
}

// NewVerificationScanTriggerAdapter creates an adapter that wraps scan.Service
// for use as a VerificationScanTrigger.
func NewVerificationScanTriggerAdapter(svc *scan.Service) VerificationScanTrigger {
return &verificationScanTriggerAdapter{svc: svc}
}

// TriggerVerificationScan implements VerificationScanTrigger.
func (a *verificationScanTriggerAdapter) TriggerVerificationScan(
ctx context.Context, tenantID, createdBy, scannerName, workflowID string, targets []string,
) (pipelineRunID, scanID string, err error) {
result, err := a.svc.QuickScan(ctx, scan.QuickScanInput{
TenantID: tenantID,
Targets: targets,
ScannerName: scannerName,
WorkflowID: workflowID,
CreatedBy: createdBy,
Tags: []string{"verification-scan"},
Config: map[string]any{
"trigger": "verification_scan",
},
})
if err != nil {
return "", "", err
}
return result.PipelineRunID, result.ScanID, nil
}
Loading
Loading