Continuous Vulnerability Scanning
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<br/>cron every 5 min"] --> B["Advisory IngestionService<br/>publish PackageMetadata::IngestedAdvisoryEvent"]
B --> C["PackageMetadata::GlobalAdvisoryScanWorker<br/>→ AdvisoryScanService<br/>→ VulnerabilityScanning::AdvisoryScanner<br/>scan_projects_for(advisory) (ALL projects)"]
C --> R["Sbom::PossiblyAffectedOccurrencesFinder<br/>read sbom_occurrences (all projects, batched)"]
DB[("pm_advisories / pm_affected_packages")] -.-> R
R --> M["AffectedVersionRangeMatcher<br/>(semver-dialect aware)"]
M --> D2["build FindingMap<br/>VulnerabilityScanning::FindingBuilder /<br/>BuildFindingMapService"]
D2 --> E["AdvisoryUtils#create_vulnerabilities<br/>→ VulnerabilityScanning::CreateVulnerabilityService"]
E --> F["Security::Ingestion::IngestCvsSliceService<br/>(same Security::Ingestion::Tasks framework;<br/>adds IngestCvsSecurityScanners +<br/>MAIN_DB MarkCvsProjectsAsVulnerable)"]
F --> G[("vulnerabilities<br/>vulnerability_occurrences<br/>vulnerability_reads<br/>(same tables as pipeline ingestion)")]
E --> H["publish Sbom::VulnerabilitiesCreatedEvent"]
H --> I["Sbom::CreateOccurrencesVulnerabilitiesService"]
I --> J[("sbom_occurrences_vulnerabilities (join)")]
Steps
- Trigger, global, per new advisory. The
AdvisoriesSyncWorkercron (every five minutes) feeds the advisoryIngestionService, which publishesPackageMetadata::IngestedAdvisoryEventper recently published advisory.GlobalAdvisoryScanWorkercallsAdvisoryScanServiceandGitlab::VulnerabilityScanning::AdvisoryScanner.scan_projects_for(advisory), which scans every project on the instance affected by that one advisory, something the pipeline model cannot do. - Read stored SBOM data.
Sbom::PossiblyAffectedOccurrencesFinderreadssbom_occurrencesacross projects in batches, matching against PMDB advisories (pm_advisories,pm_affected_packages) usingAffectedVersionRangeMatcher, which is semver-dialect aware. - Build findings. Matches become finding maps through
VulnerabilityScanning::FindingBuilderandBuildFindingMapService, synthesized from stored data rather than parsed from an analyzer report. - Shared write path. The scan converges on
AdvisoryUtils#create_vulnerabilities,Security::VulnerabilityScanning::CreateVulnerabilityService, andSecurity::Ingestion::IngestCvsSliceService. This reuses the sameSecurity::Ingestion::Tasksframework as pipeline ingestion, so it writes the samevulnerabilities,vulnerability_occurrences, andvulnerability_readstables. CVS addsIngestCvsSecurityScannersand a main-DB taskMarkCvsProjectsAsVulnerable, and uses a syntheticsbom_scanneridentity to distinguish CVS vulnerabilities from analyzer vulnerabilities. - Link back to SBOM.
CreateVulnerabilityServicepublishesSbom::VulnerabilitiesCreatedEvent, which drivesSbom::CreateOccurrencesVulnerabilitiesServiceto fill thesbom_occurrences_vulnerabilitiesjoin, connecting the new vulnerabilities back to their occurrences.
Per-project trigger (disabled by default)
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.
Settings and flags
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.
Related
- CycloneDX to security findings is the pipeline flow that
writes the same
vulnerability_*tables from a CI artifact. - CycloneDX license ingestion populates the
sbom_occurrencesdata that the global CVS scan reads. - New dependency scanning, the SBOM scan API is the analyzer-facing API that produces the CycloneDX SBOM.