Test: Harden compose secret fixtures
This commit is contained in:
@@ -33,15 +33,26 @@ if ! python3 "$scanner" "$fixtures_dir/clean" compose.yml .env.example README.md
|
|||||||
note 'the clean secret-scanning fixture was rejected'
|
note 'the clean secret-scanning fixture was rejected'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for fixture in compose.yml .env.example; do
|
expect_leak() {
|
||||||
if output="$(python3 "$scanner" "$fixtures_dir/leaked" "$fixture" 2>&1)"; then
|
local fixture="$1"
|
||||||
note "the leaked $fixture fixture was accepted"
|
local expected="$2"
|
||||||
elif [[ "$fixture" == compose.yml && "$output" != *'POSTGRES_PASSWORD'* ]]; then
|
local output
|
||||||
note 'the leaked compose fixture did not name POSTGRES_PASSWORD'
|
local status
|
||||||
elif [[ "$fixture" == .env.example && "$output" != *'API_TOKEN'* ]]; then
|
|
||||||
note 'the leaked env fixture did not name API_TOKEN'
|
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
|
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/*')
|
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
|
if ! output="$(python3 "$scanner" "$repo_dir" "${tracked_server_files[@]}" 2>&1)"; then
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ API_TOKEN=
|
|||||||
ALLOW_EMPTY_PASSWORD=yes
|
ALLOW_EMPTY_PASSWORD=yes
|
||||||
FEATURE_SECRET_ENABLED=false
|
FEATURE_SECRET_ENABLED=false
|
||||||
PRIVATE_KEY_PATH=/run/secrets/private_key
|
PRIVATE_KEY_PATH=/run/secrets/private_key
|
||||||
|
TOKENIZER_MODEL=gpt2
|
||||||
|
PASSWORDLESS_PROVIDER=webauthn
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
services:
|
||||||
|
application:
|
||||||
|
environment:
|
||||||
|
- API_TOKEN=fixture-should-be-rejected
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
services:
|
||||||
|
application:
|
||||||
|
environment:
|
||||||
|
- "API_TOKEN=fixture-should-be-rejected"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
services:
|
||||||
|
application:
|
||||||
|
environment:
|
||||||
|
"API_TOKEN": fixture-should-be-rejected
|
||||||
@@ -16,11 +16,14 @@ from typing import Iterable
|
|||||||
ENV_FILENAMES = {".env", ".env.example"}
|
ENV_FILENAMES = {".env", ".env.example"}
|
||||||
YAML_SUFFIXES = {".yaml", ".yml"}
|
YAML_SUFFIXES = {".yaml", ".yml"}
|
||||||
IGNORED_KEY_SUFFIXES = ("_FILE", "_PATH", "_NAME", "_KEY_NAME")
|
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"}
|
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*$")
|
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_MAPPING = re.compile(
|
||||||
YAML_ENV_ITEM = re.compile(r"^(\s*)-\s+([A-Za-z_][A-Za-z0-9_]*)=(.*?)\s*$")
|
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")
|
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]")
|
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()
|
normalized = key.upper()
|
||||||
if normalized.endswith(IGNORED_KEY_SUFFIXES):
|
if normalized.endswith(IGNORED_KEY_SUFFIXES):
|
||||||
return False
|
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:
|
def is_placeholder(value: str) -> bool:
|
||||||
@@ -93,13 +103,16 @@ def semantic_findings(path: str, content: str) -> Iterable[str]:
|
|||||||
environment_indents = [depth for depth in environment_indents if indent > depth]
|
environment_indents = [depth for depth in environment_indents if indent > depth]
|
||||||
|
|
||||||
if mapping:
|
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():
|
if key.lower() == "environment" and not value.strip():
|
||||||
environment_indents.append(indent)
|
environment_indents.append(indent)
|
||||||
if is_secret_key(key) and not is_placeholder(value):
|
if is_secret_key(key) and not is_placeholder(value):
|
||||||
yield f"{path}:{line_number}: {key}"
|
yield f"{path}:{line_number}: {key}"
|
||||||
elif item and environment_indents:
|
elif item and environment_indents:
|
||||||
key, value = item.group(2), item.group(3)
|
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):
|
if is_secret_key(key) and not is_placeholder(value):
|
||||||
yield f"{path}:{line_number}: {key}"
|
yield f"{path}:{line_number}: {key}"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user