Enable dual-scan (Trivy cross-check)¶
Dual-scan runs Trivy as a second, independent vulnerability engine against
packages Grype already published, to surface what Grype's initial scan
missed. This guide covers enabling it and reading its results — for why it
exists as a separate periodic sweep rather than a second blocking scan at
upload time, see services/dual_scan.py's module docstring, summarized
below.
SaaS only
Dual-scan is available only in Repod SaaS. It requires the depot-trivy
sidecar container, which exists only in docker-compose.saas.yml —
there is no equivalent overlay for EE on-premise or CE, and the feature
has no effect if enabled anywhere else. services/dual_scan.py:is_enabled()
checks DEPLOYMENT_MODE=saas first and returns False unconditionally
otherwise, regardless of what settings.json says.
1. Prerequisites¶
- Deployment mode must be
saas(DEPLOYMENT_MODE=saas), with thedepot-trivyservice running (docker-compose.saas.ymlstarts it automatically — no separate setup script). adminrole, to changesettings.json["dual_scan"].- The
dual_scanlicense feature must be entitled on the tenant's plan (seePLANSinservices/tenant_manager.py).
2. What it does — and doesn't do¶
- Trivy never runs in the upload/import path. Grype alone still decides
accept /
pending_review/ block at upload time — unchanged. Running two full scans synchronously per upload would double upload latency for no benefit. - Trivy only runs in a periodic background sweep (
dual_scan_dailycron) over packages already instatus = "validated"— deb, rpm, apk, Maven, PyPI, and npm artifacts (OCI is excluded; its images are scanned in full by Grype already viaoci-dir:, so there's no equivalent Trivy-only gap for it). - Each CVE in a scanned package's
cve_resultsgains adetected_byfield:["grype"],["trivy"], or["grype", "trivy"]. Grype's own results are never discarded or overwritten. - Only a CVE with
detected_by == ["trivy"]— genuinely new information Grype missed — can change a package's fate. A CVE both engines already agree on never re-triggers a decision, even if it's Critical. - A
["trivy"]-only finding that breachescve_policyflips the package topending_reviewand routes to the same RSSI decision queue as any other CVE decision (POST /security/packages/{name}/{version}/decide) — no new review mechanism.blockis downgraded toreviewhere, never an outright re-rejection: a Trivy-only match is name/version-based and noisier than Grype's own findings, so hard-blocking an already-published package on it would be disproportionate.
3. Enable it¶
curl -s -X PATCH http://localhost:8000/api/v1/settings/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"dual_scan": {
"enabled": true,
"hour": 5,
"minute": 15,
"max_artifacts_per_run": 50,
"max_runtime_minutes": 30
}
}'
| Field | Default | Meaning |
|---|---|---|
enabled |
false |
Must be explicitly set true — opt-in, same convention as mirror/backup/drift. |
hour / minute |
5 / 15 |
UTC time the dual_scan_daily cron job runs. |
max_artifacts_per_run |
50 |
Caps how many validated packages are re-scanned per run, oldest-rescanned-first. |
max_runtime_minutes |
30 |
Sweep stops picking up new artifacts past this wall-clock budget, even if the artifact cap hasn't been reached. |
Confirm the schedule took effect:
4. Read detected_by in the Security page¶
Open a package's CVE detail view (Security page → package → CVE list). Each CVE row carries a small badge once it has been through a dual-scan pass:
- "Grype + Trivy" (grey, understated) — both engines independently found this CVE. This is confirmation, not new information — deliberately styled as the less attention-grabbing badge.
- "Trivy uniquement" (violet) — found only by the second-opinion scan,
absent from Grype's original result. This is the actionable signal: it's
what triggers a policy re-evaluation and, if severe enough under
cve_policy, apending_reviewstate.
A package never touched by a dual scan (not yet reached by the sweep, or the
feature was disabled at the time it was published) shows no detected_by
field at all and no badge — this is expected, not an error, for any package
published before dual-scan was enabled or still waiting its turn in the
sweep.
The raw Trivy output and last-scan timestamp are kept separately on the
manifest (trivy_scan.matches, trivy_scan.scanned_at) for audit, alongside
trivy_scan.divergent_cve_ids — the exact list of CVE IDs that were
Trivy-only at the last pass.
5. Worked example¶
- A
.debis uploaded and published normally — Grype finds nothing above the configuredcve_policythreshold, package landsvalidated. - The next day, the Trivy vulnerability database picks up a CVE for one of the package's bundled libraries that Grype's own database hasn't matched yet.
- At
05:15UTC,dual_scan_dailyreaches this package in its sweep (assuming it's withinmax_artifacts_per_runfor that run — older, longer-unscanned packages are prioritized). - Trivy reports the CVE; it's absent from the package's existing
cve_results, so it lands intrivy_only. - If the CVE's severity maps to
revieworblockincve_policy, the package flips topending_review, adual_scanvalidation step is appended to the manifest, and an audit log entry (DUAL_SCAN,PENDING_REVIEW) is written. - The package now shows up in the RSSI review queue exactly like any
upload-time
pending_reviewpackage, with a["trivy"]-only badge on the flagged CVE in the Security page.
Verify it worked¶
GET /settings/showsdual_scan.enabled: truewith the schedule you set.- Wait for (or manually trigger, via a maintenance window) the next
dual_scan_dailyrun, then check the backend logs: - Pick a package that was
validatedbefore the sweep ran and confirm its manifest snapshot now has atrivy_scansection: - If Trivy was unreachable during a run, the manifest gets
trivy_scan.error = "trivy indisponible"andcve_resultsis left untouched — this is the fail-soft path, not a bug: a second engine being temporarily down never blocks or corrupts the existing Grype-based result.