|
10 | 10 | from logging import Logger |
11 | 11 | import mimetypes |
12 | 12 | import json |
| 13 | +import subprocess |
13 | 14 | from requests.adapters import HTTPAdapter |
14 | 15 | from urllib3.util.retry import Retry |
15 | 16 | from functools import wraps |
@@ -320,10 +321,12 @@ def container_test(self, tag: str) -> None: |
320 | 321 |
|
321 | 322 | # Run these tests in parallel so the runtime data is more accurate. |
322 | 323 | 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) |
323 | 325 | future_sbom: Future[str] = executor.submit(self.generate_sbom, tag) |
324 | 326 | future_logs: Future[bool] = executor.submit(self.watch_container_logs, container, tag) |
325 | 327 |
|
326 | 328 | 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 |
327 | 330 | logsfound: bool = future_logs.result(self.logs_timeout + 5) # Set a thread timeout if the function for some reason hangs |
328 | 331 | build_info: dict = self.get_build_info(container,tag) # Get the image build info |
329 | 332 |
|
@@ -521,6 +524,101 @@ def generate_sbom(self, tag:str) -> str: |
521 | 524 | except Exception: |
522 | 525 | self.logger.exception("Failed to remove the syft container, %s",tag) |
523 | 526 | 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" |
524 | 622 |
|
525 | 623 | @deprecated(reason="Use get_build_info instead") |
526 | 624 | def get_build_version(self,container:Container,tag:str) -> str: |
|
0 commit comments