Wire BLSA scan results into the pipeline, MR widget and vulnerability report
What does this MR do and why?
- The business logic security scan publishes its SAST report on a separate
duo_workflowworkload pipeline, on a synthetic ref. - The existing security surfaces never look there, so its findings stay invisible.
- This MR teaches them to find that pipeline by commit SHA, behind
agentic_analyzer_security_ingestion. - The agentic scan pipeline lookup matches the requested report type (no behaviour change while only SAST is agentic).
Part of the BLSA split of !246889. Stacked on !257092 (merged). Tracker: https://gitlab.com/gitlab-org/gitlab/-/work_items/630266
The core recognition (default_branch_scan?) came first in !245055 (merged). scan_pipeline_for and its helpers come from the business logic security work in !246889: the MR widget needs them to find its head pipeline, and nothing else provides that yet.
What changes for users
All rows apply only with agentic_analyzer_security_ingestion on. With it off, ingestion, the widget and policies don't change. One exception: business logic flows started by name now upload their gl-sast-report.json as a SAST artifact, as catalog starts already do.
| Surface | Before | After |
|---|---|---|
| MR security widget and Rapid Diffs | Business logic findings never shown | SAST section and inline markers show them when the head pipeline has no successful SAST scan (including child pipelines) |
MR widget scan mode (enabledSecurityScans, enabledPartialSecurityScans) |
A business logic security scan reported as sast: false, so no SAST row and no partial-scan handling |
The workload pipeline at the same SHA counts, so the widget picks full or partial mode and hides "fixed" for partial scans |
| Vulnerability report | Flows started by name (workflow_definition=) never collected their reports |
Those flows now declare report_artifacts, so default-branch scans get ingested |
| Analyzer status / Security Inventory | Scan not recognized (its job is named workload) |
Recognized by scanner id gitlab_bl_security_analyzer, reported as business_logic. On Ultimate projects the status is only written once !257525 (merged) lands (see below) |
| Approval policies | Business logic findings ignored | Findings at the same SHA are included when a policy is evaluated (see Not in this MR) |
Review focus
Ci::Pipeline.recent_ids_for_sha_and_source(used byAgenticAnalyzer#workload_pipeline_ids_at): a SHA lookup in the 3 recent CI partitions, capped at 50 rows and cached per request.EE::MergeRequest#security_comparison_head_pipelineand#comparison_base_pipeline: the SAST-only head swap and base fallback.Resolvers::Security::EnabledScansResolver#pipeline_ids: adds the workload pipeline ids, so the widget's scan mode matches the swapped head.EnabledPartialScansResolverinherits it.AnalyzersStatus::UpdateService#agentic_scanner_scan?: the report parse for zero-finding scans.DuoWorkflowHelpers#foundational_flow_report_artifacts: shared by every flow started by name.
Backward compatibility / impact on existing flows
| Shared code | Risk | Mitigation |
|---|---|---|
| MR head/base pipeline for security comparison | Wrong pipeline picked for other comparisons | Swap is SAST-only, and only when the head has no SAST findings. Other report types keep diff_head_pipeline. |
has_sast_reports? |
Extra queries on every MR | Core check runs first. The agentic check runs only when that fails and the flag is on. |
| Enabled-scans resolver | Extra queries on every pipeline view | The helper returns an empty relation when the flag is off. No query runs. |
RelatedPipelines (policies) |
Policy findings include business logic scans | Intended with the flag on. Ids are de-duplicated. |
Pipeline after_transition hook |
Status workers run for more pipelines | Only for duo_workflow workload pipelines at the default-branch HEAD, flag on. |
| Analyzer status update | Extra query for business logic scans | Flag-gated, and in-memory checks run first: only succeeded duo_workflow workload scans reach the query. Ordinary pipelines don't. |
| Name-started flow params | report_artifacts sent for more flows |
Added only when the foundational flow declares artifacts. Others get no new key. |
MergeRequestSecurityReportGenerationService |
Cache key changes with the swapped pipelines | Head and base are resolved the same way for compute and freshness checks. |
Database
One new query: Ci::Pipeline.recent_ids_for_sha_and_source, used by Security::AgenticAnalyzer#workload_pipeline_ids_at (only runs with agentic_analyzer_security_ingestion on). It is scoped to Ci::Partition.recent_ids and cached per request.
SELECT "p_ci_pipelines"."id" FROM "p_ci_pipelines"
WHERE "p_ci_pipelines"."project_id" = 278964
AND "p_ci_pipelines"."partition_id" IN (115, 114, 113) -- Ci::Partition.recent_ids
AND "p_ci_pipelines"."sha" = '<sha>'
AND "p_ci_pipelines"."source" = 17 -- duo_workflow
ORDER BY "p_ci_pipelines"."id" DESC
LIMIT 50Plan from Database Lab (gitlab-production-ci, gitlab-org/gitlab; partitions 113–115 stand in for the recent IDs):
- Only the 3 recent partitions are read, each with its
(project_id, sha)index. - 0.12 ms execution, 15 buffers (warm cache). Before partition scoping: 15 partitions, 63 buffers read, 146 ms cold.
Limit (actual time=0.078..0.079 rows=0 loops=1)
-> Sort (Sort Key: p_ci_pipelines.id DESC)
-> Append (actual time=0.051..0.052 rows=0 loops=1)
-> Index Scan using ci_pipelines_113_project_id_sha_idx on ci_pipelines_113
Index Cond: ((project_id = 278964) AND ((sha)::text = '<sha>'::text))
Filter: ((source = 17) AND (partition_id = ANY ('{115,114,113}'::bigint[])))
... (same shape for partitions 114 and 115)
Time: 6.874 ms (planning 6.750 ms, execution 0.124 ms)
Buffers: shared hit=15New scope: Security::Scan.without_partial_scan
Used by Security::AgenticAnalyzer#scan_pipeline_ids_for to pick the latest full run (and partial the latest partial run) among at most 50 workload pipelines.
SELECT "security_scans"."pipeline_id" FROM "security_scans"
LEFT OUTER JOIN "vulnerability_partial_scans" "partial_scan"
ON "partial_scan"."scan_id" = "security_scans"."id"
WHERE "security_scans"."project_id" = 278964
AND "security_scans"."scan_type" = 1 -- sast
AND "security_scans"."status" = 1 -- succeeded
AND "security_scans"."latest" = TRUE
AND "security_scans"."pipeline_id" IN (...) -- at most 50 ids
AND "partial_scan"."scan_id" IS NULL
ORDER BY "security_scans"."pipeline_id" DESC
LIMIT 1Plan from Database Lab (gitlab-production-sec):
- Backward index scan on
index_for_security_scans_scan_type(no sort), then an index-only anti-join onindex_vulnerability_partial_scans_on_scan_id(0 heap fetches). - 3.9 ms execution cold (14 buffers), 0.17 ms warm.
Limit (actual rows=1)
-> Nested Loop Anti Join
-> Index Scan Backward using index_for_security_scans_scan_type on security_scans
Index Cond: ((scan_type = 1) AND (project_id = 278964) AND (pipeline_id = ANY ('{...}')))
Filter: latest
-> Index Only Scan using index_vulnerability_partial_scans_on_scan_id on vulnerability_partial_scans partial_scan
Heap Fetches: 0The partial variant (INNER JOIN instead of the anti-join) uses the same two indexes: 1.5 ms cold.
Design decisions
- Capped scans count as full and auto-resolve. This matches GitLab Advanced SAST.
- The workload pipeline is found by commit SHA, in the 3 recent CI partitions, capped at 50. The workload runs on a synthetic ref, so the SHA is the only link. A scan pipeline in an older partition is not found; the result is "no business logic result shown", never wrong data.
- The MR base fallback uses the target branch's current HEAD. It only applies when the target branch has no CI security pipeline. !257599 (merged) reworks the base and will try the merge-base SHAs first.
- Interim: with the head swapped to the business logic scan, CI SAST findings on the target branch can show as "fixed". !257599 (merged) removes the swap and compares business logic scans with each other. The flag stays off until it lands.
- Any
duo_workflowpipeline with a SAST scan at the SHA counts. There is no lookup yet for which flow produced a scan. Follow-up: record the producing flow on the security scan. - A re-run at the same commit counts only the latest full run and the latest partial run. Older runs at that SHA are ignored, so their findings are not shown twice.
Not in this MR (later in the stack)
- A default-branch scan that finishes after a newer push is not ingested: !257359 (merged), stacked on this MR.
- Co-ingestion with CI SAST scans: !257525 (merged). It ingests both the CI and the business logic scan pipelines, writes scan status for Ultimate projects, and finds the scan from merged-results pipelines. Until it lands, those cases can miss business logic findings or scan status (the flag is off by default).
- Approval policies re-checked when the scan finishes: !257598 (merged). Today, policies are evaluated when the CI head pipeline finishes, usually before the scan, so business logic findings rarely block a merge.
- Business logic results merged with SAST in the MR widget and comparison base: !257599 (merged). Today, a widget whose head has its own successful SAST scan shows only those results.
- Pipeline Security tab: this MR doesn't show business logic results on a CI pipeline's Security tab (a pipeline field should describe its own pipeline). A pipeline-level design is a follow-up issue.
Security::AgenticAnalyzeris called directly from eight call sites. A generic "which flow produced this pipeline" lookup is a follow-up.- Two agentic analyzers scanning the same commit. The scan lookup keeps the newest pipeline per commit, not per flow. A second agentic analyzer's findings on the same commit would be hidden. Fixing it needs a way to tell which flow produced a pipeline; follow-up.
File inventory (24 files, +1096 / −14)
Code (12 files, +185 / −13): ee/app/services/security/agentic_analyzer.rb, ee/app/models/ee/merge_request.rb, ee/app/services/security/analyzers_status/update_service.rb, ee/app/models/ee/ci/pipeline.rb, ee/lib/api/helpers/duo_workflow_helpers.rb, ee/app/models/security/scan.rb, ee/app/graphql/resolvers/security/enabled_scans_resolver.rb, ee/app/services/concerns/security/scan_result_policies/related_pipelines.rb, ee/app/services/security/merge_request_security_report_generation_service.rb, ee/app/models/ai/catalog/foundational_flow/bl_security/definition.rb, app/models/merge_request.rb, app/models/merge_requests/versioned_merge_request.rb.
Specs (12 files, +911 / −1): new agentic_analyzer_merge_request_spec.rb, agentic_analyzer_{mr_widget_e2e,policy_visibility,recognition}_spec.rb, bl_analyzer_inventory_recognition_spec.rb; extended agentic_analyzer_spec.rb, analyzers_status/update_service_spec.rb, duo_workflow_helpers_spec.rb, related_pipelines_spec.rb, security/scan_spec.rb, enabled_security_scans_spec.rb, versioned_merge_request_spec.rb.
History
- Scheduled scan execution policy dispatch moved out to a separate stacked MR, !257271. It is parked until the Duo Agent Platform autonomous service account (!253659 (merged)) lands.
- An ingestion guard for partial scans was dropped, to match GitLab Advanced SAST.
ee/app/models/ee/ci/pipeline.rbwas hand-merged. Master's.preload(:target_project)inopened_merge_requests_with_head_shais kept.- This MR no longer touches
goal_templates/bl_security_spec.rborfoundational_flow_spec.rb. The fixes it carried were folded into !257092 (merged).
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.