Skip to content

Commit b8dafcd

Browse files
committed
perf: scan trivy+grype concurrently in one container per image
1 parent d67ae7f commit b8dafcd

1 file changed

Lines changed: 52 additions & 35 deletions

File tree

stack_scanner/main.py

Lines changed: 52 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -247,28 +247,69 @@ def scan_stackablectl(secobserve_api_token: str) -> None:
247247

248248
def _build_base_env(secobserve_api_token: str, product_name: str, branch_name: str) -> dict:
249249
return {
250-
"SO_UPLOAD": "true",
251250
"SO_PRODUCT_NAME": product_name,
252251
"SO_API_BASE_URL": SECOBSERVE_API_BASE_URL,
253252
"SO_API_TOKEN": secobserve_api_token,
254253
"SO_BRANCH_NAME": branch_name,
254+
"SO_SUPPRESS_LICENSES": "true",
255255
"TMPDIR": "/tmp/trivy_tmp",
256256
"TRIVY_CACHE_DIR": "/tmp/trivy_cache",
257-
"REPORT_NAME": "trivy.json",
257+
"GRYPE_DB_CACHE_DIR": "/tmp/grype_db_cache",
258258
}
259259

260260

261-
def _build_scanner_cmd(entrypoint: str, env: dict) -> list[str]:
261+
# Report file names written inside the scanner container. Trivy and Grype scan
262+
# the same target independently into separate files, so they never collide.
263+
_TRIVY_REPORT = "trivy.json"
264+
_GRYPE_REPORT = "grype.json"
265+
266+
267+
def _combined_scan_script(mode: str) -> str:
268+
"""Return a shell script that scans with Trivy and Grype, then uploads results.
269+
270+
Trivy and Grype scan the same target independently, so they are launched
271+
concurrently to cut wall-clock time. Running both in a single container also
272+
halves the number of ``docker run`` startups per image.
273+
274+
The uploads are kept sequential on purpose. SecObserve creates the branch on
275+
first import via ``get_or_create`` and has no request-level transaction, so
276+
two concurrent uploads for a not-yet-seen tag could race on branch creation.
277+
Scanning dominates the runtime, so serialising the two uploads costs almost
278+
nothing.
279+
280+
The existing per-scanner entrypoints are reused (with ``SO_UPLOAD=false`` so
281+
they only scan) to avoid duplicating the scanner invocation flags here.
282+
"""
283+
return (
284+
"export TRIVY_NO_PROGRESS=true\n"
285+
f"SO_UPLOAD=false REPORT_NAME={_TRIVY_REPORT} /entrypoints/entrypoint_trivy_{mode}.sh &\n"
286+
"trivy_pid=$!\n"
287+
f"SO_UPLOAD=false REPORT_NAME={_GRYPE_REPORT} /entrypoints/entrypoint_grype_{mode}.sh &\n"
288+
"grype_pid=$!\n"
289+
'wait "$trivy_pid" || echo "WARNING: Trivy scan failed"\n'
290+
'wait "$grype_pid" || echo "WARNING: Grype scan failed"\n'
291+
f"if [ -f {_TRIVY_REPORT} ]; then SO_FILE_NAME={_TRIVY_REPORT} SO_PARSER_NAME=CycloneDX "
292+
'file_upload_observations.sh; else echo "WARNING: no Trivy report to upload"; fi\n'
293+
f"if [ -f {_GRYPE_REPORT} ]; then SO_FILE_NAME={_GRYPE_REPORT} SO_PARSER_NAME=CycloneDX "
294+
'file_upload_observations.sh; else echo "WARNING: no Grype report to upload"; fi\n'
295+
)
296+
297+
298+
def _run_combined_scan(env: dict, mode: str) -> None:
299+
"""Run Trivy and Grype in a single container for one target, then upload."""
262300
cmd = [
263301
"docker", "run",
264-
"--entrypoint", entrypoint,
302+
"--entrypoint", "/bin/sh",
265303
"-v", "/tmp/stackable:/tmp",
266304
"-v", "/var/run/docker.sock:/var/run/docker.sock",
267305
]
268306
for key, value in env.items():
269307
cmd.extend(["-e", f"{key}={value}"])
270308
cmd.append(SECOBSERVE_SCANNER_IMAGE)
271-
return cmd
309+
cmd.extend(["-c", _combined_scan_script(mode)])
310+
311+
print(f"docker run (combined trivy+grype, {mode} mode) TARGET={env.get('TARGET')}")
312+
subprocess.run(cmd)
272313

273314

274315
def scan_sbom(
@@ -282,21 +323,9 @@ def scan_sbom(
282323
The file must reside under /tmp/stackable/ so it is accessible inside the
283324
scanner container (which mounts that directory to /tmp).
284325
"""
285-
trivy_env = _build_base_env(secobserve_api_token, product_name, branch_name)
286-
trivy_env["TARGET"] = f"/tmp/{file_name}"
287-
288-
cmd = _build_scanner_cmd("/entrypoints/entrypoint_trivy_sbom.sh", trivy_env)
289-
print(" ".join(cmd))
290-
subprocess.run(cmd)
291-
292-
grype_env = {
293-
**trivy_env,
294-
"FURTHER_PARAMETERS": "--by-cve",
295-
"GRYPE_DB_CACHE_DIR": "/tmp/grype_db_cache",
296-
"REPORT_NAME": "grype.json",
297-
}
298-
cmd = _build_scanner_cmd("/entrypoints/entrypoint_grype_sbom.sh", grype_env)
299-
subprocess.run(cmd)
326+
env = _build_base_env(secobserve_api_token, product_name, branch_name)
327+
env["TARGET"] = f"/tmp/{file_name}"
328+
_run_combined_scan(env, "sbom")
300329

301330

302331
_ARCH_SUFFIXES = ("-amd64", "-arm64")
@@ -545,21 +574,9 @@ def scan_image(
545574
print("No SBOM found, falling back to image mode")
546575
mode = "image" # fallback to image mode if no SBOM is available
547576

548-
trivy_env = _build_base_env(secobserve_api_token, product_name, branch_name)
549-
trivy_env["TARGET"] = image if mode == "image" else "/tmp/bom.json"
550-
551-
cmd = _build_scanner_cmd(f"/entrypoints/entrypoint_trivy_{mode}.sh", trivy_env)
552-
print(" ".join(cmd))
553-
subprocess.run(cmd)
554-
555-
grype_env = {
556-
**trivy_env,
557-
"FURTHER_PARAMETERS": "--by-cve",
558-
"GRYPE_DB_CACHE_DIR": "/tmp/grype_db_cache",
559-
"REPORT_NAME": "grype.json",
560-
}
561-
cmd = _build_scanner_cmd(f"/entrypoints/entrypoint_grype_{mode}.sh", grype_env)
562-
subprocess.run(cmd)
577+
env = _build_base_env(secobserve_api_token, product_name, branch_name)
578+
env["TARGET"] = image if mode == "image" else "/tmp/bom.json"
579+
_run_combined_scan(env, mode)
563580

564581

565582
if __name__ == "__main__":

0 commit comments

Comments
 (0)