Draft: [PoC] Enforce approval policies on analyzer-reported scan failures via scan.status
What does this MR do and why?
This is a proof of concept, not a merge-ready change. It demonstrates a topology-independent way to make approval policies react when a security analyzer reports a failed or incomplete scan, closing a bypass discovered while reviewing !243540 (merged).
Problem
Security report schemas carry a top-level scan.status ("success" / "failure") that an analyzer sets independently of its CI job exit status. Today this field is parsed into report.scan.status but only used for an SLI metric — nothing acts on it. A report declaring scan.status: "failure" is still stored as a succeeded Security::Scan as long as the job exited 0 and the schema validated.
This lets approval policies be bypassed with Dependency Scanning v2:
- The
dependency-scanninganalyzer job declares thedependency_scanningreport and succeeds, uploading a degraded report. - A resolution helper job (for example
dependency-scanning:maven-resolution, defaultallow_failure: true) fails and declares no report. enforce_scan_completion!sees asucceededscan, and detection based on the report-declaring CI job (the approach in !243540 (merged)) sees a success — so neither blocks.
DS v1 is a single job that both resolves and declares the report, so a failure there is caught. Hence "works in v1, not v2".
Approach
Treat scan.status as the signal, since it comes from the component that actually knows the scan failed, and is topology-independent:
- New
Security::Scanstatusscan_failed(analyzer self-reported failure; distinct fromjob_failed, since the job did succeed). StoreScanService#initial_scan_statusmaps a report whosescan.statusis present and!= "success"toscan_failed, and short-circuits finding ingestion (otherwisestore_findingswould flip the scan back tosucceeded).SCAN_TO_ANALYZER_STATUSmapsscan_failed → :failed.
No new enforcement code is needed: enforce_scan_completion! already blocks any required scan type that is present but not succeeded, emitting the existing SCAN_NOT_SUCCEEDED violation. This covers both scanner versions and both user-authored include: template and policy-injected pipelines.
Template side (illustrative only)
The change to Dependency-Scanning.v2.gitlab-ci.yml (resolution job records its own failure; analyzer after_script flips scan.status to "failure") is a PoC of the analyzer contract. In production the analyzer binary owns scan.status — see the open question below. It is included here only to demonstrate the end-to-end flow.
Open decisions before this could ship
- Feature-flag gating.
enforce_scan_completion!is not behindapproval_policies_block_on_failed_scan_job, so the new blocking would be live regardless of the flag. Decide whether to gate thescan.statusmapping or treat it as a general ingestion-correctness fix. - Analyzer contract. Does Dependency Scanning v2 already set
scan.status: "failure"when a resolution job fails? If yes, only the ingestion change is needed and the template PoC is unnecessary; if no, that is the ask for the Dependency Scanning analyzer team. This is why the template change stays illustrative.
How to set up and validate locally
Validated two ways.
Automated specs
ee/spec/services/security/store_scan_service_spec.rb— a report withscan.status: "failure"produces ascan_failedscan and skips finding ingestion;"success"still yieldssucceeded.ee/spec/services/security/scan_result_policies/update_approvals_service_spec.rb— ascan_failedscan blocks a fail-closed rule with aSCAN_NOT_SUCCEEDEDviolation.
End-to-end in a local GDK
A runner-executed pipeline published a dependency_scanning report with scan.status: "failure". The real ingestion path produced:
scan_type=dependency_scanning status=scan_failed latest=true errors=[]confirming the mapping works in a running instance, not just in specs.
References
- Issue: #604648
- Discovered in: !243540 (merged)
- Alternative angle (allow_failure on injected jobs): !243969