Skip to content

Commit 00ab22f

Browse files
committed
add method to fix trivyignore
1 parent ef29baf commit 00ab22f

8 files changed

Lines changed: 380 additions & 1405 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ node_modules/
22
.venv/
33
src/base/.devcontainer/language_versions/
44
.trivyignore_combined.yaml
5+
.out/

Makefile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ scan-image: guard-CONTAINER_NAME
3737
--exit-code 1 \
3838
--format table "${CONTAINER_PREFIX}$${CONTAINER_NAME}"
3939

40+
scan-image-json: guard-CONTAINER_NAME
41+
@combined="src/$${CONTAINER_NAME}/.trivyignore_combined.yaml"; \
42+
common="src/common/.trivyignore.yaml"; \
43+
specific="src/$${CONTAINER_NAME}/.trivyignore.yaml"; \
44+
echo "vulnerabilities:" > "$$combined"; \
45+
if [ -f "$$common" ]; then sed -n '2,$$p' "$$common" >> "$$combined"; fi; \
46+
if [ -f "$$specific" ]; then sed -n '2,$$p' "$$specific" >> "$$combined"; fi
47+
mkdir -p .out
48+
trivy image \
49+
--severity HIGH,CRITICAL \
50+
--config src/${CONTAINER_NAME}/trivy.yaml \
51+
--scanners vuln \
52+
--exit-code 1 \
53+
--format json \
54+
--output .out/scan.out.json "${CONTAINER_PREFIX}$${CONTAINER_NAME}"
55+
4056
shell-image: guard-CONTAINER_NAME
4157
docker run -it \
4258
"${CONTAINER_PREFIX}$${CONTAINER_NAME}" \

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,15 @@ RUN if [ -n "${DOCKER_GID}" ]; then \
8585
8686
USER vscode
8787
```
88+
89+
# Generating a .trivyignore file
90+
You can generate a .trivyignore file for known vulnerabilities by either downloading the json scan output generated by the build, or by generating it locally using
91+
```
92+
CONTAINER_NAME=base BASE_VERSION=latest make scan-image-json
93+
```
94+
If generated locally, then the output goes into .out/scan.out.json
95+
96+
Once you have this, use the following to generate a .trivyignore
97+
```
98+
poetry run python scripts/trivy_to_trivyignore.py --input .out/scan.out.json --output src/common/.trivyignore.yaml
99+
```

scripts/trivy_to_trivyignore.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
#!/usr/bin/env python3
2+
"""Convert Trivy JSON output into a .trivyignore YAML file."""
3+
4+
import argparse
5+
import datetime as dt
6+
import json
7+
from pathlib import Path
8+
from typing import Any, Dict, Iterable, List, Optional
9+
10+
11+
def add_months(date_value: dt.date, months: int) -> dt.date:
12+
"""
13+
Add months to a date, clamping the day to the last day of the target month.
14+
"""
15+
if months == 0:
16+
return date_value
17+
18+
month_index = date_value.month - 1 + months
19+
year = date_value.year + month_index // 12
20+
month = month_index % 12 + 1
21+
22+
# Clamp day to the last day of the target month.
23+
next_year = year + (1 if month == 12 else 0)
24+
next_month = 1 if month == 12 else month + 1
25+
first_of_next = dt.date(next_year, next_month, 1)
26+
last_day = first_of_next - dt.timedelta(days=1)
27+
day = min(date_value.day, last_day.day)
28+
return dt.date(year, month, day)
29+
30+
31+
def extract_vulnerabilities(data: Dict[str, Any]) -> List[Dict[str, Any]]:
32+
"""Collect vulnerability entries from Trivy JSON output."""
33+
results = data.get("Results", [])
34+
if not isinstance(results, list):
35+
return []
36+
37+
vulnerabilities: List[Dict[str, Any]] = []
38+
for result in results:
39+
if not isinstance(result, dict):
40+
continue
41+
for vuln in result.get("Vulnerabilities", []) or []:
42+
if isinstance(vuln, dict):
43+
vulnerabilities.append(vuln)
44+
return vulnerabilities
45+
46+
47+
def normalize_purl(vuln: Dict[str, Any]) -> Optional[str]:
48+
identifier = vuln.get("PkgIdentifier")
49+
if isinstance(identifier, dict):
50+
purl = identifier.get("PURL")
51+
if isinstance(purl, str) and purl.strip():
52+
return purl.strip()
53+
return None
54+
55+
56+
def build_entries(
57+
vulnerabilities: Iterable[Dict[str, Any]],
58+
expires_on: dt.date
59+
) -> List[Dict[str, Any]]:
60+
"""Build YAML entries with de-duplication by CVE, merging PURLs."""
61+
entries: Dict[str, Dict[str, Any]] = {}
62+
63+
for vuln in vulnerabilities:
64+
vuln_id = vuln.get("VulnerabilityID")
65+
title = vuln.get("Title")
66+
purl = normalize_purl(vuln)
67+
68+
if not isinstance(vuln_id, str) or not vuln_id.strip():
69+
continue
70+
if not isinstance(title, str) or not title.strip():
71+
continue
72+
73+
key = vuln_id.strip()
74+
entry = entries.get(key)
75+
if entry is None:
76+
entry = {
77+
"id": key,
78+
"statement": title.strip(),
79+
"purls": set(),
80+
"expired_at": expires_on.isoformat(),
81+
}
82+
entries[key] = entry
83+
84+
if purl:
85+
entry["purls"].add(purl)
86+
87+
merged_entries: List[Dict[str, Any]] = []
88+
for entry in entries.values():
89+
purls = sorted(entry["purls"])
90+
if purls:
91+
entry["purls"] = purls
92+
else:
93+
entry.pop("purls", None)
94+
merged_entries.append(entry)
95+
96+
return merged_entries
97+
98+
99+
def write_yaml(entries: List[Dict[str, Any]], output_path: Path) -> None:
100+
"""Write entries to a YAML file without external dependencies."""
101+
lines: List[str] = ["vulnerabilities:"]
102+
for entry in entries:
103+
lines.append(f" - id: {entry['id']}")
104+
lines.append(f" statement: {json.dumps(entry['statement'])}")
105+
if "purls" in entry:
106+
lines.append(" purls:")
107+
for purl in entry["purls"]:
108+
lines.append(f" - {json.dumps(purl)}")
109+
lines.append(f" expired_at: {entry['expired_at']}")
110+
111+
output_path.parent.mkdir(parents=True, exist_ok=True)
112+
output_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
113+
114+
115+
def parse_args() -> argparse.Namespace:
116+
parser = argparse.ArgumentParser(
117+
description="Convert Trivy JSON output to .trivyignore YAML."
118+
)
119+
parser.add_argument(
120+
"--input",
121+
required=True,
122+
help="Path to the Trivy JSON output file.",
123+
)
124+
parser.add_argument(
125+
"--output",
126+
required=True,
127+
help="Path to write the .trivyignore YAML file.",
128+
)
129+
return parser.parse_args()
130+
131+
132+
def main() -> int:
133+
args = parse_args()
134+
input_path = Path(args.input)
135+
output_path = Path(args.output)
136+
137+
if not input_path.is_file():
138+
raise FileNotFoundError(f"Input file not found: {input_path}")
139+
140+
data = json.loads(input_path.read_text(encoding="utf-8"))
141+
vulnerabilities = extract_vulnerabilities(data)
142+
143+
expires_on = add_months(dt.date.today(), 6)
144+
entries = build_entries(vulnerabilities, expires_on)
145+
146+
write_yaml(entries, output_path)
147+
return 0
148+
149+
150+
if __name__ == "__main__":
151+
raise SystemExit(main())

0 commit comments

Comments
 (0)