From d969b925e0de186ceacc089600e60ae76dc4c6dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9verine=20Bonnech=C3=A8re?= <severine.bonnechere@gitguardian.com> Date: Wed, 11 Jun 2025 09:08:29 +0200 Subject: [PATCH 1/5] fix: docker pull fallback on linux amd64 for apple silicon --- ggshield/verticals/secret/docker.py | 38 ++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/ggshield/verticals/secret/docker.py b/ggshield/verticals/secret/docker.py index fb2b2a3215..973749f7a6 100644 --- a/ggshield/verticals/secret/docker.py +++ b/ggshield/verticals/secret/docker.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import platform import re import subprocess import tarfile @@ -289,15 +290,46 @@ def docker_pull_image(image_name: str, timeout: int) -> None: Timeout after `timeout` seconds. """ - command = ["docker", "pull", image_name] + # Base command for docker pull + base_command = ["docker", "pull", image_name] + + # Try standard pull first + if _run_docker_command(base_command, timeout): + return + + # Apple Silicon: fall back to linux/amd64 if no success + if platform.system() == "Darwin" and platform.machine() == "arm64": + amd64_command = base_command + ["--platform=linux/amd64"] + if _run_docker_command(amd64_command, timeout): + return + + # Raise error if no success + raise UsageError(f'Image "{image_name}" not found') + + +def _run_docker_command(command: List[str], timeout: int) -> bool: + """ + Run a docker command with timeout and return success status + + Args: + command: Docker command to run as a list of strings + timeout: Timeout in seconds + + Returns: + True if command succeeded, False if CalledProcessError + + Raises: + UnexpectedError: If command times out + """ try: subprocess.run( command, check=True, timeout=timeout, ) + return True except subprocess.CalledProcessError: - raise UsageError(f'Image "{image_name}" not found') + return False except subprocess.TimeoutExpired: raise UnexpectedError('Command "{}" timed out'.format(" ".join(command))) @@ -325,7 +357,7 @@ def docker_save_to_tmp(image_name: str, destination_path: Path, timeout: int) -> except subprocess.CalledProcessError as exc: err_string = str(exc.stderr) if "No such image" in err_string or "reference does not exist" in err_string: - ui.display_info("need to download image first") + ui.display_info("need to download image first") # ici docker_pull_image(image_name, timeout) docker_save_to_tmp(image_name, destination_path, timeout) From b29e110271e11112b750d665a91f70d35756a933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9verine=20Bonnech=C3=A8re?= <severine.bonnechere@gitguardian.com> Date: Fri, 20 Jun 2025 11:08:21 +0200 Subject: [PATCH 2/5] chore: always fallback on linux/amd64, regardless of platform --- ggshield/verticals/secret/docker.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/ggshield/verticals/secret/docker.py b/ggshield/verticals/secret/docker.py index 973749f7a6..50cb1850c1 100644 --- a/ggshield/verticals/secret/docker.py +++ b/ggshield/verticals/secret/docker.py @@ -1,7 +1,6 @@ from __future__ import annotations import json -import platform import re import subprocess import tarfile @@ -297,11 +296,10 @@ def docker_pull_image(image_name: str, timeout: int) -> None: if _run_docker_command(base_command, timeout): return - # Apple Silicon: fall back to linux/amd64 if no success - if platform.system() == "Darwin" and platform.machine() == "arm64": - amd64_command = base_command + ["--platform=linux/amd64"] - if _run_docker_command(amd64_command, timeout): - return + # Fall back to linux/amd64 if no success + amd64_command = base_command + ["--platform=linux/amd64"] + if _run_docker_command(amd64_command, timeout): + return # Raise error if no success raise UsageError(f'Image "{image_name}" not found') @@ -357,7 +355,7 @@ def docker_save_to_tmp(image_name: str, destination_path: Path, timeout: int) -> except subprocess.CalledProcessError as exc: err_string = str(exc.stderr) if "No such image" in err_string or "reference does not exist" in err_string: - ui.display_info("need to download image first") # ici + ui.display_info("need to download image first") docker_pull_image(image_name, timeout) docker_save_to_tmp(image_name, destination_path, timeout) From 55bd18d91d1c86947cc8356eaaf5852dc30d7080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9verine=20Bonnech=C3=A8re?= <severine.bonnechere@gitguardian.com> Date: Fri, 20 Jun 2025 14:19:19 +0200 Subject: [PATCH 3/5] chore: test for fallback on linux image --- tests/unit/verticals/secret/test_scan_docker.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/unit/verticals/secret/test_scan_docker.py b/tests/unit/verticals/secret/test_scan_docker.py index 94299f8039..2b64b72f34 100644 --- a/tests/unit/verticals/secret/test_scan_docker.py +++ b/tests/unit/verticals/secret/test_scan_docker.py @@ -148,6 +148,20 @@ def test_docker_pull_image_timeout(self): ): docker_pull_image("ggshield-non-existant", DOCKER_TIMEOUT) + def test_docker_pull_image_platform_fallback(self): + with patch( + "subprocess.run", side_effect=subprocess.CalledProcessError(1, cmd=[]) + ) as call, pytest.raises( + click.UsageError, + match='Image "ggshield-non-existant" not found', + ): + docker_pull_image("ggshield-non-existant", DOCKER_TIMEOUT) + call.assert_called_once_with( + ["docker", "pull", "ggshield-non-existant", "--platform=linux/amd64"], + check=True, + timeout=DOCKER_TIMEOUT, + ) + class TestDockerSave: TMP_ARCHIVE = Path("/tmp/as/archive.tar") From 11dca33d568f567a76953a686cd735eaca4155c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9verine=20Bonnech=C3=A8re?= <severine.bonnechere@gitguardian.com> Date: Fri, 20 Jun 2025 14:52:18 +0200 Subject: [PATCH 4/5] chore: adding changelog fragment --- ...145029_severine.bonnechere_fix_pull_docker_apple_silicon.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog.d/20250620_145029_severine.bonnechere_fix_pull_docker_apple_silicon.md diff --git a/changelog.d/20250620_145029_severine.bonnechere_fix_pull_docker_apple_silicon.md b/changelog.d/20250620_145029_severine.bonnechere_fix_pull_docker_apple_silicon.md new file mode 100644 index 0000000000..73900f6b3a --- /dev/null +++ b/changelog.d/20250620_145029_severine.bonnechere_fix_pull_docker_apple_silicon.md @@ -0,0 +1,3 @@ +### Changed + +- When scanning a docker image, if no image is found matching the client platform, try to pull the `linux/amd64` image. From c8835ddfa62eb22debd8d402dd1d38671d58c6ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9verine=20Bonnech=C3=A8re?= <severine.bonnechere@gitguardian.com> Date: Mon, 23 Jun 2025 16:26:53 +0200 Subject: [PATCH 5/5] fix: grumpy pre-commit --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce0eb9762c..58a7e3f842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -551,6 +551,7 @@ Yanked: release process issue. #### IaC - `ggshield iac scan` now provides three new commands for use as Git hooks: + - `ggshield iac scan pre-commit` - `ggshield iac scan pre-push` - `ggshield iac scan pre-receive` @@ -635,6 +636,7 @@ Yanked: release process issue. - New command: `ggshield iac scan all`. This command replaces the now-deprecated `ggshield iac scan`. It scans a directory for IaC vulnerabilities. - New command: `ggshield iac scan diff`. This command scans a Git repository and inspects changes in IaC vulnerabilities between two points in the history. + - All options from `ggshield iac scan all` are supported: `--ignore-policy`, `--minimum-severity`, `--ignore-path` etc. Execute `ggshield iac scan diff -h` for more details. - Two new options allow to choose which state to select for the difference: `--ref <GIT-REFERENCE>` and `--staged`. - The command can be integrated in Git hooks using the `--pre-commit`, `--pre-push`, `--pre-receive` options.