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.