@@ -247,28 +247,69 @@ def scan_stackablectl(secobserve_api_token: str) -> None:
247247
248248def _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
274315def 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
565582if __name__ == "__main__" :
0 commit comments