From 6a7bdcb8350fb95930c4df838b843d6ba5031a42 Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Wed, 26 Aug 2026 22:38:00 -0400 Subject: [PATCH] Test: Harden compose secret fixtures --- tests/server/compose-secrets-contract | 27 ++++++++++++----- .../fixtures/secrets/clean/.env.example | 2 ++ .../fixtures/secrets/leaked/plain-list.yml | 4 +++ .../fixtures/secrets/leaked/quoted-list.yml | 4 +++ .../secrets/leaked/quoted-mapping.yml | 4 +++ tests/server/scan-tracked-secrets.py | 29 ++++++++++++++----- 6 files changed, 54 insertions(+), 16 deletions(-) create mode 100644 tests/server/fixtures/secrets/leaked/plain-list.yml create mode 100644 tests/server/fixtures/secrets/leaked/quoted-list.yml create mode 100644 tests/server/fixtures/secrets/leaked/quoted-mapping.yml diff --git a/tests/server/compose-secrets-contract b/tests/server/compose-secrets-contract index 92c0f46..6cdc36d 100755 --- a/tests/server/compose-secrets-contract +++ b/tests/server/compose-secrets-contract @@ -33,15 +33,26 @@ if ! python3 "$scanner" "$fixtures_dir/clean" compose.yml .env.example README.md note 'the clean secret-scanning fixture was rejected' fi -for fixture in compose.yml .env.example; do - if output="$(python3 "$scanner" "$fixtures_dir/leaked" "$fixture" 2>&1)"; then - note "the leaked $fixture fixture was accepted" - elif [[ "$fixture" == compose.yml && "$output" != *'POSTGRES_PASSWORD'* ]]; then - note 'the leaked compose fixture did not name POSTGRES_PASSWORD' - elif [[ "$fixture" == .env.example && "$output" != *'API_TOKEN'* ]]; then - note 'the leaked env fixture did not name API_TOKEN' +expect_leak() { + local fixture="$1" + local expected="$2" + local output + local status + + output="$(python3 "$scanner" "$fixtures_dir/leaked" "$fixture" 2>&1)" + status=$? + if (( status != 1 )); then + note "the leaked $fixture fixture exited $status instead of 1" + elif [[ "$output" != "$expected" ]]; then + note "the leaked $fixture fixture reported '$output' instead of '$expected'" fi -done +} + +expect_leak compose.yml 'compose.yml:4: POSTGRES_PASSWORD' +expect_leak .env.example '.env.example:1: API_TOKEN' +expect_leak plain-list.yml 'plain-list.yml:4: API_TOKEN' +expect_leak quoted-mapping.yml 'quoted-mapping.yml:4: API_TOKEN' +expect_leak quoted-list.yml 'quoted-list.yml:4: API_TOKEN' mapfile -t tracked_server_files < <(git -C "$repo_dir" ls-files 'server/**' 'server/*') if ! output="$(python3 "$scanner" "$repo_dir" "${tracked_server_files[@]}" 2>&1)"; then diff --git a/tests/server/fixtures/secrets/clean/.env.example b/tests/server/fixtures/secrets/clean/.env.example index cc6a71d..94624db 100644 --- a/tests/server/fixtures/secrets/clean/.env.example +++ b/tests/server/fixtures/secrets/clean/.env.example @@ -3,3 +3,5 @@ API_TOKEN= ALLOW_EMPTY_PASSWORD=yes FEATURE_SECRET_ENABLED=false PRIVATE_KEY_PATH=/run/secrets/private_key +TOKENIZER_MODEL=gpt2 +PASSWORDLESS_PROVIDER=webauthn diff --git a/tests/server/fixtures/secrets/leaked/plain-list.yml b/tests/server/fixtures/secrets/leaked/plain-list.yml new file mode 100644 index 0000000..965df51 --- /dev/null +++ b/tests/server/fixtures/secrets/leaked/plain-list.yml @@ -0,0 +1,4 @@ +services: + application: + environment: + - API_TOKEN=fixture-should-be-rejected diff --git a/tests/server/fixtures/secrets/leaked/quoted-list.yml b/tests/server/fixtures/secrets/leaked/quoted-list.yml new file mode 100644 index 0000000..300519b --- /dev/null +++ b/tests/server/fixtures/secrets/leaked/quoted-list.yml @@ -0,0 +1,4 @@ +services: + application: + environment: + - "API_TOKEN=fixture-should-be-rejected" diff --git a/tests/server/fixtures/secrets/leaked/quoted-mapping.yml b/tests/server/fixtures/secrets/leaked/quoted-mapping.yml new file mode 100644 index 0000000..8fa0689 --- /dev/null +++ b/tests/server/fixtures/secrets/leaked/quoted-mapping.yml @@ -0,0 +1,4 @@ +services: + application: + environment: + "API_TOKEN": fixture-should-be-rejected diff --git a/tests/server/scan-tracked-secrets.py b/tests/server/scan-tracked-secrets.py index 8d80177..36e3fc0 100644 --- a/tests/server/scan-tracked-secrets.py +++ b/tests/server/scan-tracked-secrets.py @@ -16,11 +16,14 @@ from typing import Iterable ENV_FILENAMES = {".env", ".env.example"} YAML_SUFFIXES = {".yaml", ".yml"} IGNORED_KEY_SUFFIXES = ("_FILE", "_PATH", "_NAME", "_KEY_NAME") -SECRET_KEY_PARTS = ("PASSWORD", "SECRET", "TOKEN", "API_KEY", "PRIVATE_KEY", "ACCESS_KEY") +CREDENTIAL_SEGMENTS = {"PASSWORD", "SECRET", "TOKEN"} +CREDENTIAL_KEY_SHAPES = (("API", "KEY"), ("PRIVATE", "KEY"), ("ACCESS", "KEY")) BOOLEAN_VALUES = {"true", "false", "yes", "no", "0", "1"} ENV_ASSIGNMENT = re.compile(r"^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$") -YAML_MAPPING = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_.-]*)\s*:\s*(.*?)\s*$") -YAML_ENV_ITEM = re.compile(r"^(\s*)-\s+([A-Za-z_][A-Za-z0-9_]*)=(.*?)\s*$") +YAML_MAPPING = re.compile( + r"^(\s*)(?:\"([A-Za-z_][A-Za-z0-9_.-]*)\"|'([A-Za-z_][A-Za-z0-9_.-]*)'|([A-Za-z_][A-Za-z0-9_.-]*))\s*:\s*(.*?)\s*$" +) +YAML_ENV_ITEM = re.compile(r"^(\s*)-\s+(.*?)\s*$") PRIVATE_KEY_HEADER = re.compile(r"BEGIN [A-Z ]*PRIVATE KEY") PROVIDER_TOKEN = re.compile(r"sk-ant-[A-Za-z0-9]|ghp_[A-Za-z0-9]{20}|xox[baprs]-[A-Za-z0-9]") @@ -58,7 +61,14 @@ def is_secret_key(key: str) -> bool: normalized = key.upper() if normalized.endswith(IGNORED_KEY_SUFFIXES): return False - return any(part in normalized for part in SECRET_KEY_PARTS) + segments = tuple(segment for segment in re.split(r"[_.-]+", normalized) if segment) + if any(segment in CREDENTIAL_SEGMENTS for segment in segments): + return True + return any( + segments[index : index + len(shape)] == shape + for shape in CREDENTIAL_KEY_SHAPES + for index in range(len(segments) - len(shape) + 1) + ) def is_placeholder(value: str) -> bool: @@ -93,15 +103,18 @@ def semantic_findings(path: str, content: str) -> Iterable[str]: environment_indents = [depth for depth in environment_indents if indent > depth] if mapping: - key, value = mapping.group(2), mapping.group(3) + key = mapping.group(2) or mapping.group(3) or mapping.group(4) + value = mapping.group(5) if key.lower() == "environment" and not value.strip(): environment_indents.append(indent) if is_secret_key(key) and not is_placeholder(value): yield f"{path}:{line_number}: {key}" elif item and environment_indents: - key, value = item.group(2), item.group(3) - if is_secret_key(key) and not is_placeholder(value): - yield f"{path}:{line_number}: {key}" + assignment = ENV_ASSIGNMENT.match(normalize_value(item.group(2))) + if assignment: + key, value = assignment.groups() + if is_secret_key(key) and not is_placeholder(value): + yield f"{path}:{line_number}: {key}" def signature_findings(path: str, content: str) -> Iterable[str]: