Skip to content

Commit e5cc1e2

Browse files
committed
initial buildx imagetools inspect sbom, not tested, needs failover logic
1 parent 7f6893b commit e5cc1e2

1 file changed

Lines changed: 98 additions & 0 deletions

File tree

ci/ci.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from logging import Logger
1111
import mimetypes
1212
import json
13+
import subprocess
1314
from requests.adapters import HTTPAdapter
1415
from urllib3.util.retry import Retry
1516
from functools import wraps
@@ -320,10 +321,12 @@ def container_test(self, tag: str) -> None:
320321

321322
# Run these tests in parallel so the runtime data is more accurate.
322323
with ThreadPoolExecutor(max_workers=2,thread_name_prefix=thread_name) as executor:
324+
future_buildx_sbom: Future[str] = executor.submit(self.generate_sbom_buildx, tag)
323325
future_sbom: Future[str] = executor.submit(self.generate_sbom, tag)
324326
future_logs: Future[bool] = executor.submit(self.watch_container_logs, container, tag)
325327

326328
sbom: str = future_sbom.result(self.sbom_timeout + 5) # Set a thread timeout if the function for some reason hangs
329+
buildx_sbom: str = future_buildx_sbom.result(self.sbom_timeout + 5) # Set a thread timeout if the function for some reason hangs
327330
logsfound: bool = future_logs.result(self.logs_timeout + 5) # Set a thread timeout if the function for some reason hangs
328331
build_info: dict = self.get_build_info(container,tag) # Get the image build info
329332

@@ -521,6 +524,101 @@ def generate_sbom(self, tag:str) -> str:
521524
except Exception:
522525
self.logger.exception("Failed to remove the syft container, %s",tag)
523526
return "ERROR"
527+
528+
def get_sbom_buildx(self, tag: str) -> str:
529+
"""get the SBOM for the image tag using docker buildx imagetools inspect.
530+
531+
Args:
532+
tag (str): The tag we are testing
533+
534+
Returns:
535+
str: SBOM output if successful, otherwise "ERROR".
536+
"""
537+
image_ref = f"{self.image}:{tag}"
538+
platform: str = self.get_platform(tag)
539+
cmd = [
540+
"docker", "buildx", "imagetools", "inspect",
541+
image_ref,
542+
"--format", f'{{{{ json (index .SBOM "linux/{platform}").SPDX }}}}'
543+
]
544+
try:
545+
result: subprocess.CompletedProcess[str] = subprocess.run(cmd, capture_output=True, text=True, timeout=self.sbom_timeout, check=False)
546+
if result.returncode == 0 and result.stdout.strip():
547+
self.logger.info("SBOM generated for %s using buildx imagetools inspect.", image_ref)
548+
return result.stdout.strip()
549+
self.logger.error("Failed to generate SBOM for %s using buildx: %s", image_ref, result.stderr)
550+
return "ERROR"
551+
except Exception:
552+
self.logger.exception("Exception while running buildx imagetools inspect for %s", image_ref)
553+
return "ERROR"
554+
555+
def parse_buildx_sbom(self, sbom: str) -> list[dict[str, str]]:
556+
"""Parse the buildx imagetools inspect SBOM string and extract package information.
557+
558+
Args:
559+
sbom (str): The SBOM in JSON format.
560+
Returns:
561+
list[dict[str, str]]: A list of dictionaries containing package information.
562+
"""
563+
try:
564+
sbom_data: dict[str, Any] = json.loads(sbom)
565+
packages: list[dict[str, str]] = []
566+
package_list: list[dict[str, Any]] = sbom_data.get("packages", [])
567+
for item in package_list:
568+
package_info = {
569+
"name": item.get("name", ""),
570+
"version": item.get("versionInfo", ""),
571+
}
572+
packages.append(package_info)
573+
return packages
574+
except json.JSONDecodeError:
575+
self.logger.error("Failed to parse buildx imagetools inspectSBOM.")
576+
return []
577+
578+
def format_package_table(self, packages: list[dict[str, str]]) -> str:
579+
"""Format a list of package dicts into a padded string table.
580+
581+
Args:
582+
packages (list[dict[str, str]]): List of package dicts with 'name' and 'version' keys.
583+
584+
Returns:
585+
str: Padded string table of package names and versions.
586+
"""
587+
name_width = 75
588+
version_width = 25
589+
lines: list[str] = []
590+
header = f"{'NAME'.ljust(name_width)}{'VERSION'.ljust(version_width)}"
591+
lines.append(header)
592+
for pkg in packages:
593+
name = pkg.get('name', '')[:name_width]
594+
version = pkg.get('version', '')[:version_width]
595+
lines.append(f"{name.ljust(name_width)}{version.ljust(version_width)}")
596+
return "\n".join(lines)
597+
598+
def generate_sbom_buildx(self, tag: str) -> str:
599+
"""Generate the SBOM for the image tag using docker buildx imagetools inspect.
600+
601+
Args:
602+
tag (str): The tag we are testing
603+
Returns:
604+
str: Formatted package table if successful, otherwise "ERROR".
605+
"""
606+
start_time = time.time()
607+
self.logger.info("Generating SBOM for %s using buildx imagetools inspect", tag)
608+
test = "Create SBOM (buildx)"
609+
sbom_raw: str = self.get_sbom_buildx(tag)
610+
if sbom_raw != "ERROR":
611+
packages_list: list[dict[str, str]] = self.parse_buildx_sbom(sbom_raw)
612+
if packages_list:
613+
packages_formatted: str = self.format_package_table(packages_list)
614+
self._add_test_result(tag, test, "PASS", "-", start_time)
615+
self.logger.success("%s package list %s: PASSED after %.2f seconds", test, tag, time.time() - start_time)
616+
self.create_html_ansi_file(packages_formatted, tag, "buildx_sbom")
617+
return packages_formatted
618+
self.logger.error("Failed to generate SBOM output on tag %s using buildx.", tag)
619+
self.report_status = "FAIL"
620+
self._add_test_result(tag, test, "FAIL", "Failed to generate SBOM using buildx", start_time)
621+
return "ERROR"
524622

525623
@deprecated(reason="Use get_build_info instead")
526624
def get_build_version(self,container:Container,tag:str) -> str:

0 commit comments

Comments
 (0)