Exposure Changes
Exposure changes answer the core EASM question: what changed in the external attack surface since previous scans?
The MVP stores immutable change events in PostgreSQL and exposes them through organization-scoped API endpoints. Events are generated best-effort from existing asset, vulnerability, and file persistence paths. A failure to write an exposure change must not fail a scan, vulnerability update, or file upload.
Table
exposure_changes is created by backend/migrations/008_exposure_changes.sql.
Important columns:
| Column | Purpose |
|---|---|
organization_id |
Organization boundary for RBAC and filtering |
scan_id, scan_job_id |
Optional scan attribution when the event was produced by scan output |
asset_id, vulnerability_id |
Optional affected entity references |
change_type |
Event category, such as asset_discovered |
entity_type |
Entity family, such as asset, service, vulnerability, or file |
title, description |
Human-readable timeline text |
severity, risk_score |
Vulnerability or high-signal event severity context |
old_value, new_value |
Structured before/after payloads |
metadata |
Source plugin and future integration context |
created_at |
Timeline order |
Indexes cover organization timeline queries, change type filters, asset/vulnerability history, scan filtering, and severity filters.
Event Types
Implemented in the MVP:
asset_discoveredservice_discoveredurl_discoveredpath_discoveredcertificate_discoveredvulnerability_discoveredvulnerability_status_changedfile_collected
Reserved for future implementation:
port_openedport_closedtechnology_changedasset_staleasset_reappeared
Event Sources
Assets:
assets.Service.Upsertrecords a discovery event only when PostgreSQL reports the asset row was newly inserted.- Service, URL, path, and certificate assets use specialized change types and titles.
- Dedup updates that only refresh
last_seen_at, confidence, or metadata do not create another discovery event.
Vulnerabilities:
vulnerabilities.Service.Createrecordsvulnerability_discoveredonly when the vulnerability upsert inserts a new row.vulnerabilities.Service.UpdateStatusrecordsvulnerability_status_changedafter the existing status transition logic succeeds.
Files:
files.Service.SaveFilerecordsfile_collectedafter object storage and metadata persistence succeed.
Scan attribution:
- The worker attaches scan context while processing normalized plugin output, so scan-created assets and vulnerabilities can reference
scan_id. - File artifacts already carry scan and scan job ids through
SaveFileInput.
API
GET /api/v1/organizations/{org_id}/changesGET /api/v1/organizations/{org_id}/changes/summary?days=7GET /api/v1/assets/{asset_id}/changes
All endpoints are read-only and use the existing organization/resource RBAC model. Clients may read assigned organization changes; admins may read all organizations.
Duplicate Prevention
The primary duplicate prevention is source-based:
- Assets emit only when an upsert inserts a new asset.
- Vulnerabilities emit only when an upsert inserts a new vulnerability.
The changes service also checks for an existing similar event by organization, change type, entity id, and scan id before inserting. This protects against repeated calls in the same scan without adding a state machine.
Current Limitations
port_closedis not implemented yet.asset_staleis not implemented yet.asset_reappearedis not implemented yet.- Technology diffing is not implemented yet.
- Change detection is based on newly inserted assets and vulnerabilities in this MVP.
- Scan job attribution is available for file artifacts; asset and vulnerability discovery events currently carry scan attribution but not per-plugin job attribution.
Future Roadmap
The exposure_changes table is intentionally self-contained so future systems can consume it without changing scanner execution semantics:
- notifications and webhook fan-out
- SIEM export pipelines
- AI agent context
- stale asset detection
- closed-port detection based on historical service observations
- technology change diffs