Skip to content

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.

Exposure Changes are separate from Asset History and Asset Scan Changes. Asset History is a dedicated per-Asset lifecycle/system timeline stored in asset_history; Asset Scan Changes are field-level scanner-observed technical changes stored in asset_scan_changes; Exposure Changes remain the global attack-surface/security feed.

Asset Detail uses asset_history as System Changes and asset_scan_changes as Scan Changes. The top chart intentionally shows only those two categories. The History tab switches between System and Scan modes; Recent Changes merges the latest rows from both domains. Exposure Changes are not consumed by this Asset Detail history UI.

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, threat_id Optional affected entity references. Legacy API responses may also include vulnerability_id for compatibility.
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_discovered
  • service_discovered
  • webapp_discovered
  • url_discovered
  • path_discovered
  • certificate_discovered
  • threat_status_changed
  • vulnerability_discovered
  • vulnerability_status_changed
  • vulnerability_exploit_status_changed
  • file_collected
  • asset_criticality_changed
  • asset_approval_changed
  • asset_disabled_changed
  • asset_stale_changed

Reserved for future implementation:

  • port_opened
  • port_closed
  • technology_changed
  • asset_reappeared

Event Sources

Assets:

  • assets.Service.Upsert records a discovery event only when PostgreSQL reports the asset row was newly inserted.
  • Service, WebApp, and certificate assets use specialized change types and titles.
  • url_discovered and path_discovered may remain in immutable historical records from earlier Asset models; new production WebApp discovery uses webapp_discovered.
  • Dedup updates that only refresh last_seen_at, confidence, or metadata do not create another discovery event.
  • Manual lifecycle updates create asset_approval_changed, asset_disabled_changed, or asset_stale_changed events.

Threats:

  • Plugin-discovered vulnerability Threats record vulnerability_discovered only when the vulnerability upsert inserts a new row.
  • Threat status transitions record threat_status_changed with entity_type=threat and metadata.threat_type.
  • Vulnerability-only exploit flag changes record vulnerability_exploit_status_changed after a manual exploit flag change succeeds.
  • Historical vulnerability_status_changed events remain readable for compatibility.

Files:

  • files.Service.SaveFile records file_collected after object storage and metadata persistence succeed.

Scan attribution:

  • The worker attaches scan context while processing normalized plugin output, so scan-created assets and vulnerability Threats can reference scan_id.
  • File artifacts already carry scan and scan job ids through SaveFileInput.

API

  • GET /api/v1/organizations/{org_id}/changes
  • GET /api/v1/organizations/{org_id}/changes/summary?days=7
  • GET /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.
  • Vulnerability Threats emit only when an upsert inserts a new vulnerability Threat.

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_closed is not implemented yet.
  • Automatic stale detection is not implemented yet. Manual stale flag changes are recorded as asset_stale_changed.
  • asset_reappeared is not implemented yet.
  • Technology diffing is not implemented yet.
  • Change detection is based on newly inserted assets and vulnerability Threats in this MVP.
  • Scan job attribution is available for file artifacts; asset and vulnerability Threat discovery events currently carry scan attribution but not per-plugin job attribution.

Relationship to Asset Scan Changes

Asset Scan Changes are the canonical fine-grained technical diff store for scanner-owned metadata.asset_info changes. They record fields such as service.version, http.server, paths, certificate SANs, and DNS record sets at Asset scope.

Exposure Changes should not duplicate every field-level scan diff. It remains a higher-level organization feed for discovery, Threat, lifecycle, file, and security-relevant activity.

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

Asset Criticality Changes

Manual criticality updates create a best-effort exposure change event with change_type=asset_criticality_changed, entity_type=asset, old_value.criticality, and new_value.criticality. Recording failure does not block the criticality update.

Asset Lifecycle Changes

Manual asset lifecycle updates create best-effort exposure change events:

  • asset_approval_changed
  • asset_disabled_changed
  • asset_stale_changed

Events use entity_type=asset and store the changed boolean field in old_value and new_value. Recording failure does not block the lifecycle update.