doc/development/sec/continuous_vulnerability_scanning.md
Focus: how Continuous Vulnerability Scanning (CVS) synthesizes vulnerabilities by matching
already-stored SBOM data against Package Metadata DB (PMDB) advisories, without an analyzer
running in a pipeline. The active flow is a global scan triggered for each newly published
advisory, converging on the same vulnerability_* tables as the
CycloneDX to security findings flow. A second, per-project
trigger exists but is disabled by default, so it is described in text rather than the diagram.
All paths are under ee/. Sbom::* and PackageMetadata::* models live on the sec DB
(gitlab_sec). The vulnerabilities and related tables live on main/ci.
flowchart TD
accTitle: Continuous Vulnerability Scanning global advisory scan
accDescr: How a newly published advisory triggers a global scan that reads stored SBOM occurrences, matches them against PMDB advisories, and writes to the same vulnerability tables as pipeline ingestion.
A["PackageMetadata::AdvisoriesSyncWorker
cron every 5 min"] --> B["Advisory IngestionService
publish PackageMetadata::IngestedAdvisoryEvent"]
B --> C["PackageMetadata::GlobalAdvisoryScanWorker
→ AdvisoryScanService
→ VulnerabilityScanning::AdvisoryScanner
scan_projects_for(advisory) (ALL projects)"]
C --> R["Sbom::PossiblyAffectedOccurrencesFinder
read sbom_occurrences (all projects, batched)"]
DB[("pm_advisories / pm_affected_packages")] -.-> R
R --> M["AffectedVersionRangeMatcher
(semver-dialect aware)"]
M --> D2["build FindingMap
VulnerabilityScanning::FindingBuilder /
BuildFindingMapService"]
D2 --> E["AdvisoryUtils#create_vulnerabilities
→ VulnerabilityScanning::CreateVulnerabilityService"]
E --> F["Security::Ingestion::IngestCvsSliceService
(same Security::Ingestion::Tasks framework;
adds IngestCvsSecurityScanners +
MAIN_DB MarkCvsProjectsAsVulnerable)"]
F --> G[("vulnerabilities
vulnerability_occurrences
vulnerability_reads
(same tables as pipeline ingestion)")]
E --> H["publish Sbom::VulnerabilitiesCreatedEvent"]
H --> I["Sbom::CreateOccurrencesVulnerabilitiesService"]
I --> J[("sbom_occurrences_vulnerabilities (join)")]
AdvisoriesSyncWorker cron (every five minutes)
feeds the advisory IngestionService, which publishes PackageMetadata::IngestedAdvisoryEvent
per recently published advisory. GlobalAdvisoryScanWorker calls AdvisoryScanService and
Gitlab::VulnerabilityScanning::AdvisoryScanner.scan_projects_for(advisory), which scans every
project on the instance affected by that one advisory, something the pipeline model cannot do.Sbom::PossiblyAffectedOccurrencesFinder reads sbom_occurrences
across projects in batches, matching against PMDB advisories (pm_advisories,
pm_affected_packages) using AffectedVersionRangeMatcher, which is semver-dialect aware.VulnerabilityScanning::FindingBuilder and BuildFindingMapService, synthesized from stored
data rather than parsed from an analyzer report.AdvisoryUtils#create_vulnerabilities,
Security::VulnerabilityScanning::CreateVulnerabilityService, and
Security::Ingestion::IngestCvsSliceService. This reuses the same Security::Ingestion::Tasks
framework as pipeline ingestion, so it writes the same vulnerabilities,
vulnerability_occurrences, and vulnerability_reads tables. CVS adds
IngestCvsSecurityScanners and a main-DB task MarkCvsProjectsAsVulnerable, and uses a
synthetic sbom_scanner identity to distinguish CVS vulnerabilities from analyzer
vulnerabilities.CreateVulnerabilityService publishes
Sbom::VulnerabilitiesCreatedEvent, which drives Sbom::CreateOccurrencesVulnerabilitiesService
to fill the sbom_occurrences_vulnerabilities join, connecting the new vulnerabilities back to
their occurrences.A second trigger runs after each SBOM ingestion, but it is effectively disabled on a default
instance and so is not shown in the diagram. The Sbom::SbomIngestedEvent is handled by
Sbom::ProcessVulnerabilitiesWorker, which runs Sbom::CreateVulnerabilitiesService for that
pipeline. CreateVulnerabilitiesService#scan_sbom_reports always skips dependency_scanning
sources (DS findings now come through the main pipeline ingestion) and skips every other source
unless cvs_for_container_scanning is enabled. That flag is a beta flag disabled by default, so
the worker runs but produces no vulnerabilities until it is enabled. When enabled, this path
handles container-scanning occurrences and also runs
Security::Ingestion::MarkAsResolvedService (scanner sbom_scanner) to auto-resolve CVS
vulnerabilities no longer present. When it does write, it uses the same shared write path as the
global scan above.
Project security settings continuous_vulnerability_scans_enabled,
cvs_for_container_scanning_enabled, and cvs_for_dependency_scanning_enabled (nil is treated
as enabled) control CVS; the cvs_for_container_scanning feature flag gates the per-project
container path.
Differences from pipeline ingestion: event-driven and cron-driven instead of
CI-artifact-driven; findings are synthesized from stored SBOM data and PMDB advisories instead of
parsed from a scanner; the global flow spans all projects; the write slice is
IngestCvsSliceService, not IngestReportSliceService. Overlaps: it reads the same
sbom_occurrences, reuses the same Security::Ingestion::Tasks, and writes the same
vulnerability_* tables.
vulnerability_* tables from a CI artifact.sbom_occurrences
data that the global CVS scan reads.