Repository: gitguardian/ggshield
Issue: ggshield scan docker misses some files

**GitGuardian Shield Version**

- 1.11.0

**Describe the bug**

1. Unpack this archive: [missed-secret.zip](https://github.com/GitGuardian/ggshield/files/8415919/missed-secret.zip)
2. Create a Docker image from it:
```
cd missed-secret
docker build -t missed-secret:1 .
```
3. Scan the image:
```
ggshield scan docker missed-secret:1
```

→ No secrets are found.

4. Change the `COPY` line in the Dockerfile to:
```
COPY index.py /index.py
```

5. Rebuild the Docker image:
```
docker build -t missed-secret:2 .
```
6. Scan the image:

```
ggshield scan docker missed-secret:2
```

→ Secret is found.

Changed files:
tests/cmd/scan/test_docker.py, ggshield/scan/docker.py

diff --git a/ggshield/scan/docker.py b/ggshield/scan/docker.py
index 6a58559..02128ff 100644
--- a/ggshield/scan/docker.py
+++ b/ggshield/scan/docker.py
@@ -18,24 +18,26 @@ from ggshield.scan import ScanCollection
 from ggshield.scan.scannable import File, Files
 
 
-DEFAULT_FS_BANLIST = {
-    "/usr/",
-    "/lib/",
-    "/share/",
-    "/bin/",
-    "/sbin/",
-    "/node_modules/",
-    "/include/",
-    "/vendor/",
-    "/texlive/",
-    "/var/",
-    "/fonts/",
-    "/npm/",
-    "/site-packages/",
-    "/gems/",
+FILEPATH_BANLIST = [
+    r"^/?usr/(?!share/nginx)",
+    r"^/?lib/",
+    r"^/?share/",
+    r"^/?bin/",
+    r"^/?sbin/",
+    r"^/?node_modules/",
+    r"^/?include/",
+    r"^/?vendor/",
+    r"^/?texlive/",
+    r"^/?var/",
+    r"^/?fonts/",
+    r"^/?npm/",
+    r"^/?site-packages/",
+]
+FILEPATH_BANLIST_PATTERNS = {
+    re.compile(banned_filepath) for banned_filepath in FILEPATH_BANLIST
 }
 
-DEFAULT_EXTENSION_BANLIST = {".md", ".html", ".css", ".lock", ".storyboard", ".xib"}
+EXTENSIONS_BANLIST = {".md", ".html", ".css", ".lock", ".storyboard", ".xib"}
 
 LAYER_TO_SCAN_PATTERN = re.compile(r"\\b(copy|add)\\b", re.IGNORECASE)
 
@@ -134,6 +136,19 @@ def _get_layers_files(
         yield from _get_layer_files(archive, layer_info)
 
 
+def _validate_filepath(
+    filepath: str,
+) -> bool:
+    if any(
+        banned_pattern.search(filepath) for banned_pattern in FILEPATH_BANLIST_PATTERNS
+    ):
+        return False
+
+    if any(filepath.endswith(extension) for extension in EXTENSIONS_BANLIST):
+        return False
+    return True
+
+
 def _get_layer_files(archive: tarfile.TarFile, layer_info: Dict) -> Iterable[File]:
     """
     Extracts File objects to be scanned for given layer.
@@ -151,10 +166,9 @@ def _get_layer_files(archive: tarfile.TarFile, layer_info: Dict) -> Iterable[Fil
         if file_info.size > MAX_FILE_SIZE * 0.95:
             continue
 
-        if any(dir in "/" + file_info.path for dir in DEFAULT_FS_BANLIST):
-            continue
-
-        if any(file_info.path.endswith(ext) for ext in DEFAULT_EXTENSION_BANLIST):
+        if not _validate_filepath(
+            filepath=file_info.path,
+        ):
             continue
 
         file = layer_archive.extractfile(file_info)
@@ -162,8 +176,6 @@ def _get_layer_files(archive: tarfile.TarFile, layer_info: Dict) -> Iterable[Fil
             continue
 
         file_content = file.read()
-        if len(file_content) > MAX_FILE_SIZE * 0.95:
-            continue
 
         # layer_filename is "<some_uuid>/layer.tar". We only keep "<some_uuid>"
         layer_name = os.path.dirname(layer_filename)
diff --git a/tests/cmd/scan/test_docker.py b/tests/cmd/scan/test_docker.py
index 62d615b..6f39f79 100644
--- a/tests/cmd/scan/test_docker.py
+++ b/tests/cmd/scan/test_docker.py
@@ -6,6 +6,7 @@ import click
 import pytest
 
 from ggshield.cmd.main import cli
+from ggshield.scan.docker import _validate_filepath
 from ggshield.scan.scannable import File, Files, ScanCollection
 from tests.conftest import (
     _SIMPLE_SECRET,
@@ -22,6 +23,32 @@ DOCKER__INCOMPLETE_MANIFEST_EXAMPLE_PATH = (
 )
 
 
+class TestDockerUtils:
+    @pytest.mark.parametrize(
+        "filepath, valid",
+        (
+            ["/usr/bin/secret.py", False],
+            ["usr/bin/secret.py", False],
+            ["/my/file/secret.py", True],
+            ["/my/file/usr/bin/secret.py", True],
+            ["/usr/share/nginx/secret.py", True],
+            ["/gems/secret.py", True],
+            ["/npm-bis/secret.py", True],
+            ["/banned/extension/secret.md", False],
+            ["/banned/extension/secret.html", False],
+            ["/banned/extension/secret.css", False],
+            ["/banned/extension/secret.other", True],
+        ),
+    )
+    def test_docker_filepath_validation(self, filepath, valid):
+        assert (
+            _validate_filepath(
+                filepath=filepath,
+            )
+            is valid
+        )
+
+
 class TestDockerCMD:
     @patch("ggshield.cmd.secret.scan.docker.docker_save_to_tmp")
     @patch("ggshield.cmd.secret.scan.docker.docker_scan_archive")