add vex support
This commit is contained in:
1 parent
81961f268f
commit
f6e723a63e
6 files changed
+90
-1
No files matched your search
@@ -41,6 +41,12 @@ IGNORE_UNFIXED=false
|
||||
# Optional: restrict which severities Trivy reports (empty = all).
|
||||
TRIVY_SEVERITIES=
|
||||
|
||||
# Optional: VEX sources passed to `trivy image --vex`, space separated.
|
||||
# Use "repo", "oci", "sbom-ref", local file paths, or a combination.
|
||||
# Mount local files into the container first (e.g. ./vex:/vex:ro).
|
||||
# TRIVY_VEX=repo
|
||||
# TRIVY_VEX=/vex/app.json
|
||||
|
||||
# How many findings to list in the notification body.
|
||||
MAX_FINDINGS_IN_BODY=15
|
||||
|
||||
|
||||
@@ -16,7 +16,9 @@ host only needs Docker. JSON reports are kept on disk for auditing.
|
||||
1. `scan.py` determines which images to scan (see
|
||||
[Which images are scanned](#which-images-are-scanned)) and runs
|
||||
`trivy image` for each of them, writing a JSON report to
|
||||
`/reports/<image>.json`.
|
||||
`/reports/<image>.json`. Optional VEX sources are applied through Trivy's
|
||||
`--vex` flag, so findings marked as not affected are filtered out before
|
||||
counting and notifying (see [VEX](#vex)).
|
||||
2. It counts findings per severity and checks them against two thresholds:
|
||||
- `NOTIFY_MIN_SEVERITY` (default `HIGH`) – when to send an Apprise alert.
|
||||
- `FAIL_ON` (default `HIGH`) – when the script should exit with code `1`
|
||||
@@ -142,6 +144,7 @@ All configuration is done through environment variables.
|
||||
| `FAIL_ON` | `HIGH` | Exit `1` when a finding at/above this severity exists. `NONE` never fails. |
|
||||
| `IGNORE_UNFIXED` | `false` | Skip vulnerabilities without a fixed version. |
|
||||
| `TRIVY_SEVERITIES` | *(all)* | Restrict severities Trivy reports, e.g. `HIGH,CRITICAL`. |
|
||||
| `TRIVY_VEX` | – | VEX sources passed to Trivy: `repo`, `oci`, `sbom-ref`, or file paths. |
|
||||
| `NOTIFY_ON_SCAN_ERROR` | `true` | Also alert when Trivy itself fails (e.g. image pull error). |
|
||||
| `REPORT_DIR` | `/reports` | Where JSON reports are written. |
|
||||
| `TRIVY_CACHE_DIR` | `/var/lib/trivy` | Trivy DB cache (mount a volume to avoid re-downloading). |
|
||||
@@ -153,6 +156,39 @@ Images can be passed as arguments, via `IMAGES`, or via `IMAGES_FILE`; the
|
||||
sources are combined and de-duplicated. If all of them are empty, the images in
|
||||
use are scanned.
|
||||
|
||||
### VEX
|
||||
|
||||
Trivy can filter findings with [VEX](https://trivy.dev/docs/latest/supply-chain/vex/)
|
||||
(Vulnerability Exploitability eXchange) documents. Set `TRIVY_VEX` to one or
|
||||
more space-separated sources; each is passed to `trivy image --vex`:
|
||||
|
||||
```dotenv
|
||||
# VEX Hub / repository lookup
|
||||
TRIVY_VEX=repo
|
||||
|
||||
# VEX attestations attached to the image in an OCI registry
|
||||
TRIVY_VEX=oci
|
||||
|
||||
# Local OpenVEX or CycloneDX VEX documents (mount them into the container)
|
||||
TRIVY_VEX=/vex/app.json /vex/base.json
|
||||
|
||||
# Several methods; the first source has the highest priority
|
||||
TRIVY_VEX="/vex/app.json repo"
|
||||
```
|
||||
|
||||
For local files with Compose, mount the directory read-only:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- ./vex:/vex:ro
|
||||
environment:
|
||||
TRIVY_VEX: /vex/app.json
|
||||
```
|
||||
|
||||
Findings suppressed by VEX are removed from the Trivy JSON report and are
|
||||
therefore not counted, notified, or considered by `FAIL_ON`. The report still
|
||||
reflects the filtered result.
|
||||
|
||||
### Apprise URLs
|
||||
|
||||
`APPRISE_URLS` accepts any [Apprise URL](https://github.com/caronc/apprise/wiki).
|
||||
|
||||
@@ -12,6 +12,8 @@ services:
|
||||
FAIL_ON: ${FAIL_ON:-HIGH}
|
||||
IGNORE_UNFIXED: ${IGNORE_UNFIXED:-false}
|
||||
TRIVY_SEVERITIES: ${TRIVY_SEVERITIES:-}
|
||||
# Optional: "repo", "oci", "sbom-ref" or local VEX file paths.
|
||||
TRIVY_VEX: ${TRIVY_VEX:-}
|
||||
MAX_FINDINGS_IN_BODY: ${MAX_FINDINGS_IN_BODY:-15}
|
||||
NOTIFY_ON_SCAN_ERROR: ${NOTIFY_ON_SCAN_ERROR:-true}
|
||||
TRIVY_TIMEOUT: ${TRIVY_TIMEOUT:-15m}
|
||||
@@ -37,6 +39,8 @@ services:
|
||||
# Remove this line if you do not want the host's runtime dirs exposed.
|
||||
# - /run/user:/run/user:ro
|
||||
- ${KOMODO_CACHE_DIR:-.}/cves/reports:/reports
|
||||
# Optional: local VEX documents, referenced as /vex/<file> in TRIVY_VEX.
|
||||
# - ./vex:/vex:ro
|
||||
# Keep the Trivy DB between runs (much faster repeat scans).
|
||||
- trivy-cache:/var/lib/trivy
|
||||
restart: "no"
|
||||
|
||||
@@ -142,6 +142,7 @@ Edit the stack's `environment` (in the TOML or in Komodo) and re-apply the sync:
|
||||
| `NOTIFY_MIN_SEVERITY` | Lowest severity that sends an alert (default `HIGH`). |
|
||||
| `FAIL_ON` | Lowest severity that makes the container exit `1` (default `HIGH`). |
|
||||
| `IGNORE_UNFIXED` | `true` to ignore findings without a fix. |
|
||||
| `TRIVY_VEX` | VEX sources: `repo`, `oci`, `sbom-ref`, or local file paths (space separated). |
|
||||
| `MAX_FINDINGS_IN_BODY` | Findings listed per notification. |
|
||||
|
||||
Schedule fields are on the Procedure:
|
||||
@@ -168,6 +169,10 @@ Private registries: add `TRIVY_USERNAME`, `TRIVY_PASSWORD` and optionally
|
||||
`TRIVY_INSECURE` to the stack environment. The Compose file already passes
|
||||
them through to the container.
|
||||
|
||||
VEX: set `TRIVY_VEX = repo`, `oci` or a combination in the stack environment.
|
||||
For local files, add `- ./vex:/vex:ro` to the `vulnscan` service volumes in
|
||||
`docker-compose.yml` and use a path such as `TRIVY_VEX = /vex/app.json`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Fix |
|
||||
|
||||
@@ -364,6 +364,7 @@ class Settings:
|
||||
fail_on: str = "HIGH"
|
||||
ignore_unfixed: bool = False
|
||||
trivy_severities: str = ""
|
||||
trivy_vex: list[str] = field(default_factory=list)
|
||||
notify_on_scan_error: bool = True
|
||||
report_dir: str = "/reports"
|
||||
trivy_cache_dir: str = "/var/lib/trivy"
|
||||
@@ -393,6 +394,7 @@ class Settings:
|
||||
fail_on=os.environ.get("FAIL_ON", "HIGH"),
|
||||
ignore_unfixed=env_bool("IGNORE_UNFIXED", False),
|
||||
trivy_severities=os.environ.get("TRIVY_SEVERITIES", "").strip(),
|
||||
trivy_vex=split_words(os.environ.get("TRIVY_VEX", "")),
|
||||
notify_on_scan_error=env_bool("NOTIFY_ON_SCAN_ERROR", True),
|
||||
report_dir=os.environ.get("REPORT_DIR", "/reports"),
|
||||
trivy_cache_dir=os.environ.get("TRIVY_CACHE_DIR", "/var/lib/trivy"),
|
||||
@@ -549,6 +551,8 @@ def run_trivy(image: str, settings: Settings, context: str = "") -> tuple[int, s
|
||||
]
|
||||
if settings.trivy_severities:
|
||||
command += ["--severity", settings.trivy_severities]
|
||||
for vex_source in settings.trivy_vex:
|
||||
command += ["--vex", vex_source]
|
||||
if settings.ignore_unfixed:
|
||||
command.append("--ignore-unfixed")
|
||||
command.append(image)
|
||||
@@ -732,6 +736,9 @@ Environment:
|
||||
(default: false).
|
||||
TRIVY_SEVERITIES Comma separated severities Trivy should report
|
||||
(default: empty = all severities).
|
||||
TRIVY_VEX Space separated VEX sources passed to Trivy as
|
||||
--vex: "repo", "oci", "sbom-ref" or file paths.
|
||||
Earlier sources take priority (default: none).
|
||||
NOTIFY_ON_SCAN_ERROR true/false - alert when Trivy fails (default: true).
|
||||
REPORT_DIR Directory for JSON reports (default: /reports).
|
||||
TRIVY_CACHE_DIR Trivy database/cache directory (default: /var/lib/trivy).
|
||||
@@ -763,6 +770,9 @@ def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
settings.docker_socket = find_docker_socket()
|
||||
|
||||
if settings.trivy_vex:
|
||||
log("INFO", f"Applying VEX source(s): {', '.join(settings.trivy_vex)}")
|
||||
|
||||
targets = collect_targets(settings)
|
||||
if not targets:
|
||||
return 0
|
||||
|
||||
@@ -291,6 +291,7 @@ class SettingsTests(unittest.TestCase):
|
||||
"FAIL_ON": "NONE",
|
||||
"IGNORE_UNFIXED": "true",
|
||||
"MAX_FINDINGS_IN_BODY": "3",
|
||||
"TRIVY_VEX": "/vex/app.json repo",
|
||||
}
|
||||
with mock.patch.dict(os.environ, env, clear=False):
|
||||
settings = scan.Settings.from_env([])
|
||||
@@ -298,6 +299,7 @@ class SettingsTests(unittest.TestCase):
|
||||
self.assertEqual(settings.fail_rank, 0)
|
||||
self.assertTrue(settings.ignore_unfixed)
|
||||
self.assertEqual(settings.max_findings, 3)
|
||||
self.assertEqual(settings.trivy_vex, ["/vex/app.json", "repo"])
|
||||
|
||||
def test_settings_rejects_unknown_severity(self) -> None:
|
||||
with mock.patch.dict(os.environ, {"NOTIFY_MIN_SEVERITY": "SEVERE"}, clear=False):
|
||||
@@ -380,6 +382,32 @@ class ReportTests(unittest.TestCase):
|
||||
self.assertIn("no fix available", body)
|
||||
|
||||
|
||||
class TrivyCommandTests(unittest.TestCase):
|
||||
def test_run_trivy_passes_vex_sources_in_order(self) -> None:
|
||||
settings = scan.Settings(
|
||||
explicit_images=[], trivy_vex=["/vex/app.json", "repo"]
|
||||
)
|
||||
completed = subprocess.CompletedProcess([], 0, stdout="", stderr="")
|
||||
with mock.patch.object(scan.subprocess, "run", return_value=completed) as run:
|
||||
status, _ = scan.run_trivy("nginx:1.27", settings)
|
||||
|
||||
command = run.call_args.args[0]
|
||||
self.assertEqual(status, 0)
|
||||
positions = [index for index, arg in enumerate(command) if arg == "--vex"]
|
||||
self.assertEqual(
|
||||
[command[index + 1] for index in positions],
|
||||
["/vex/app.json", "repo"],
|
||||
)
|
||||
|
||||
def test_run_trivy_omits_vex_when_not_configured(self) -> None:
|
||||
settings = scan.Settings(explicit_images=[])
|
||||
completed = subprocess.CompletedProcess([], 0, stdout="", stderr="")
|
||||
with mock.patch.object(scan.subprocess, "run", return_value=completed) as run:
|
||||
scan.run_trivy("nginx:1.27", settings)
|
||||
|
||||
self.assertNotIn("--vex", run.call_args.args[0])
|
||||
|
||||
|
||||
class MainFlowTests(unittest.TestCase):
|
||||
"""End-to-end runs of main() with fake trivy/apprise binaries."""
|
||||
|
||||
|
||||
Reference in new issue
Block a user