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).
|
# Optional: restrict which severities Trivy reports (empty = all).
|
||||||
TRIVY_SEVERITIES=
|
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.
|
# How many findings to list in the notification body.
|
||||||
MAX_FINDINGS_IN_BODY=15
|
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
|
1. `scan.py` determines which images to scan (see
|
||||||
[Which images are scanned](#which-images-are-scanned)) and runs
|
[Which images are scanned](#which-images-are-scanned)) and runs
|
||||||
`trivy image` for each of them, writing a JSON report to
|
`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:
|
2. It counts findings per severity and checks them against two thresholds:
|
||||||
- `NOTIFY_MIN_SEVERITY` (default `HIGH`) – when to send an Apprise alert.
|
- `NOTIFY_MIN_SEVERITY` (default `HIGH`) – when to send an Apprise alert.
|
||||||
- `FAIL_ON` (default `HIGH`) – when the script should exit with code `1`
|
- `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. |
|
| `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. |
|
| `IGNORE_UNFIXED` | `false` | Skip vulnerabilities without a fixed version. |
|
||||||
| `TRIVY_SEVERITIES` | *(all)* | Restrict severities Trivy reports, e.g. `HIGH,CRITICAL`. |
|
| `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). |
|
| `NOTIFY_ON_SCAN_ERROR` | `true` | Also alert when Trivy itself fails (e.g. image pull error). |
|
||||||
| `REPORT_DIR` | `/reports` | Where JSON reports are written. |
|
| `REPORT_DIR` | `/reports` | Where JSON reports are written. |
|
||||||
| `TRIVY_CACHE_DIR` | `/var/lib/trivy` | Trivy DB cache (mount a volume to avoid re-downloading). |
|
| `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
|
sources are combined and de-duplicated. If all of them are empty, the images in
|
||||||
use are scanned.
|
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
|
||||||
|
|
||||||
`APPRISE_URLS` accepts any [Apprise URL](https://github.com/caronc/apprise/wiki).
|
`APPRISE_URLS` accepts any [Apprise URL](https://github.com/caronc/apprise/wiki).
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ services:
|
|||||||
FAIL_ON: ${FAIL_ON:-HIGH}
|
FAIL_ON: ${FAIL_ON:-HIGH}
|
||||||
IGNORE_UNFIXED: ${IGNORE_UNFIXED:-false}
|
IGNORE_UNFIXED: ${IGNORE_UNFIXED:-false}
|
||||||
TRIVY_SEVERITIES: ${TRIVY_SEVERITIES:-}
|
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}
|
MAX_FINDINGS_IN_BODY: ${MAX_FINDINGS_IN_BODY:-15}
|
||||||
NOTIFY_ON_SCAN_ERROR: ${NOTIFY_ON_SCAN_ERROR:-true}
|
NOTIFY_ON_SCAN_ERROR: ${NOTIFY_ON_SCAN_ERROR:-true}
|
||||||
TRIVY_TIMEOUT: ${TRIVY_TIMEOUT:-15m}
|
TRIVY_TIMEOUT: ${TRIVY_TIMEOUT:-15m}
|
||||||
@@ -37,6 +39,8 @@ services:
|
|||||||
# Remove this line if you do not want the host's runtime dirs exposed.
|
# Remove this line if you do not want the host's runtime dirs exposed.
|
||||||
# - /run/user:/run/user:ro
|
# - /run/user:/run/user:ro
|
||||||
- ${KOMODO_CACHE_DIR:-.}/cves/reports:/reports
|
- ${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).
|
# Keep the Trivy DB between runs (much faster repeat scans).
|
||||||
- trivy-cache:/var/lib/trivy
|
- trivy-cache:/var/lib/trivy
|
||||||
restart: "no"
|
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`). |
|
| `NOTIFY_MIN_SEVERITY` | Lowest severity that sends an alert (default `HIGH`). |
|
||||||
| `FAIL_ON` | Lowest severity that makes the container exit `1` (default `HIGH`). |
|
| `FAIL_ON` | Lowest severity that makes the container exit `1` (default `HIGH`). |
|
||||||
| `IGNORE_UNFIXED` | `true` to ignore findings without a fix. |
|
| `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. |
|
| `MAX_FINDINGS_IN_BODY` | Findings listed per notification. |
|
||||||
|
|
||||||
Schedule fields are on the Procedure:
|
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
|
`TRIVY_INSECURE` to the stack environment. The Compose file already passes
|
||||||
them through to the container.
|
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
|
## Troubleshooting
|
||||||
|
|
||||||
| Symptom | Fix |
|
| Symptom | Fix |
|
||||||
|
|||||||
@@ -364,6 +364,7 @@ class Settings:
|
|||||||
fail_on: str = "HIGH"
|
fail_on: str = "HIGH"
|
||||||
ignore_unfixed: bool = False
|
ignore_unfixed: bool = False
|
||||||
trivy_severities: str = ""
|
trivy_severities: str = ""
|
||||||
|
trivy_vex: list[str] = field(default_factory=list)
|
||||||
notify_on_scan_error: bool = True
|
notify_on_scan_error: bool = True
|
||||||
report_dir: str = "/reports"
|
report_dir: str = "/reports"
|
||||||
trivy_cache_dir: str = "/var/lib/trivy"
|
trivy_cache_dir: str = "/var/lib/trivy"
|
||||||
@@ -393,6 +394,7 @@ class Settings:
|
|||||||
fail_on=os.environ.get("FAIL_ON", "HIGH"),
|
fail_on=os.environ.get("FAIL_ON", "HIGH"),
|
||||||
ignore_unfixed=env_bool("IGNORE_UNFIXED", False),
|
ignore_unfixed=env_bool("IGNORE_UNFIXED", False),
|
||||||
trivy_severities=os.environ.get("TRIVY_SEVERITIES", "").strip(),
|
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),
|
notify_on_scan_error=env_bool("NOTIFY_ON_SCAN_ERROR", True),
|
||||||
report_dir=os.environ.get("REPORT_DIR", "/reports"),
|
report_dir=os.environ.get("REPORT_DIR", "/reports"),
|
||||||
trivy_cache_dir=os.environ.get("TRIVY_CACHE_DIR", "/var/lib/trivy"),
|
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:
|
if settings.trivy_severities:
|
||||||
command += ["--severity", settings.trivy_severities]
|
command += ["--severity", settings.trivy_severities]
|
||||||
|
for vex_source in settings.trivy_vex:
|
||||||
|
command += ["--vex", vex_source]
|
||||||
if settings.ignore_unfixed:
|
if settings.ignore_unfixed:
|
||||||
command.append("--ignore-unfixed")
|
command.append("--ignore-unfixed")
|
||||||
command.append(image)
|
command.append(image)
|
||||||
@@ -732,6 +736,9 @@ Environment:
|
|||||||
(default: false).
|
(default: false).
|
||||||
TRIVY_SEVERITIES Comma separated severities Trivy should report
|
TRIVY_SEVERITIES Comma separated severities Trivy should report
|
||||||
(default: empty = all severities).
|
(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).
|
NOTIFY_ON_SCAN_ERROR true/false - alert when Trivy fails (default: true).
|
||||||
REPORT_DIR Directory for JSON reports (default: /reports).
|
REPORT_DIR Directory for JSON reports (default: /reports).
|
||||||
TRIVY_CACHE_DIR Trivy database/cache directory (default: /var/lib/trivy).
|
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()
|
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)
|
targets = collect_targets(settings)
|
||||||
if not targets:
|
if not targets:
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ class SettingsTests(unittest.TestCase):
|
|||||||
"FAIL_ON": "NONE",
|
"FAIL_ON": "NONE",
|
||||||
"IGNORE_UNFIXED": "true",
|
"IGNORE_UNFIXED": "true",
|
||||||
"MAX_FINDINGS_IN_BODY": "3",
|
"MAX_FINDINGS_IN_BODY": "3",
|
||||||
|
"TRIVY_VEX": "/vex/app.json repo",
|
||||||
}
|
}
|
||||||
with mock.patch.dict(os.environ, env, clear=False):
|
with mock.patch.dict(os.environ, env, clear=False):
|
||||||
settings = scan.Settings.from_env([])
|
settings = scan.Settings.from_env([])
|
||||||
@@ -298,6 +299,7 @@ class SettingsTests(unittest.TestCase):
|
|||||||
self.assertEqual(settings.fail_rank, 0)
|
self.assertEqual(settings.fail_rank, 0)
|
||||||
self.assertTrue(settings.ignore_unfixed)
|
self.assertTrue(settings.ignore_unfixed)
|
||||||
self.assertEqual(settings.max_findings, 3)
|
self.assertEqual(settings.max_findings, 3)
|
||||||
|
self.assertEqual(settings.trivy_vex, ["/vex/app.json", "repo"])
|
||||||
|
|
||||||
def test_settings_rejects_unknown_severity(self) -> None:
|
def test_settings_rejects_unknown_severity(self) -> None:
|
||||||
with mock.patch.dict(os.environ, {"NOTIFY_MIN_SEVERITY": "SEVERE"}, clear=False):
|
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)
|
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):
|
class MainFlowTests(unittest.TestCase):
|
||||||
"""End-to-end runs of main() with fake trivy/apprise binaries."""
|
"""End-to-end runs of main() with fake trivy/apprise binaries."""
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user