enroll/tests/test_secret_detection.py
Miguel Jacq 3781d602a1
Some checks failed
CI / test (push) Successful in 50s
CI / test (almalinux, docker.io/library/almalinux:9, python3.11) (push) Failing after 2m42s
CI / test (debian, docker.io/library/debian:13, python3) (push) Failing after 3m12s
Lint / test (push) Successful in 1m13s
Fix overenthusiastic match on pass
2026-06-30 11:34:37 +10:00

180 lines
6.9 KiB
Python

"""Regression test: safe-mode content sniff must catch URI-embedded credentials
and HTTP Authorization headers, not just assignment-style credential keys."""
from enroll.ignore import IgnorePolicy
def test_uri_embedded_credentials_denied():
pol = IgnorePolicy(dangerous=False)
leaking = [
b"DATABASE_URL=postgresql://admin:S3cr3tPass@db:5432/app\n",
b"REDIS_URL=redis://:mypassword@localhost:6379/0\n",
b"broker_url = amqp://guest:guest@rabbit:5672//\n",
b"uri: mongodb+srv://user:p%40ss@cluster0.mongodb.net\n",
]
for data in leaking:
assert (
pol._content_deny_reason("/etc/svc/app.conf", data) == "sensitive_content"
), data
def test_authorization_headers_denied():
pol = IgnorePolicy(dangerous=False)
for data in [
b"Authorization: Bearer eyJhbGciOiJIUzI1NiIs\n",
b"Authorization: Basic dXNlcjpwYXNzd29yZA==\n",
b"Proxy-Authorization: Bearer xyz\n",
]:
assert (
pol._content_deny_reason("/etc/svc/app.conf", data) == "sensitive_content"
), data
def test_benign_urls_not_false_positive():
pol = IgnorePolicy(dangerous=False)
for data in [
b"homepage = https://example.com/docs\n",
b"server = http://localhost:8080/api\n",
b"origin = git://github.com/u/repo.git\n",
b"bind = http://[::1]:9000/\n",
b"contact = mailto:admin@example.com\n",
b"log_level = info\nworkers = 4\n",
]:
assert pol._content_deny_reason("/etc/svc/app.conf", data) is None, data
def test_bare_word_password_denied():
"""Credential lines using the word password/passphrase/credentials without
an assignment delimiter (netrc, ppp, space/tab separated) must be denied."""
pol = IgnorePolicy(dangerous=False)
for data in [
b"machine api.example.com login bob password s3cret\n",
b"passphrase mysecretphrase\n",
b"credentials /run/creds/db\n",
b"pin 1234\n",
]:
assert (
pol._content_deny_reason("/etc/svc.conf", data) == "sensitive_content"
), data
def test_ppp_secrets_paths_denied():
pol = IgnorePolicy(dangerous=False)
for p in ("/etc/ppp/chap-secrets", "/etc/ppp/pap-secrets", "/etc/ppp/my-secrets"):
assert pol._path_deny_reason(p) == "denied_path", p
def test_short_credential_key_components_denied():
pol = IgnorePolicy(dangerous=False)
for data in [
b"pass = hunter2\n",
b"db_pass = hunter2\n",
b"pass_key = hunter2\n",
b"db-pass: hunter2\n",
b"pin = 1234\n",
b"db_pin = 1234\n",
b"pin-key: 1234\n",
]:
assert (
pol._content_deny_reason("/etc/app.conf", data) == "sensitive_content"
), data
def test_password_keyword_no_false_positives():
pol = IgnorePolicy(dangerous=False)
for data in [
b"passive_mode = true\n",
b"PassengerRoot /usr/lib\n",
b"pinboard = enabled\n",
b"pinot_region = noir\n",
b"description = reset the account\n",
b"compression = gzip\n",
]:
assert pol._content_deny_reason("/etc/app.conf", data) is None, data
def test_block_comment_open_cannot_mask_private_key():
"""Security regression: a line beginning with ``/*`` must not put the content
scanner into a runaway block-comment state that hides later real secrets.
Previously, any line starting with ``/*`` suppressed scanning of every
subsequent line until a ``*/`` appeared. An unterminated (or hostile) block
open therefore masked a PEM private key / password in an arbitrary config
file (one not covered by a path deny-glob)."""
pol = IgnorePolicy(dangerous=False)
leaking = [
# leading /* with no closing */ masking a PEM private key
b"/* app config\n"
b"tls_key = -----BEGIN PRIVATE KEY-----\n"
b"MIIBVw...\n"
b"-----END PRIVATE KEY-----\n",
# leading /* masking an assignment-style password
b"/* app config\npassword = realSecret123\n",
# unterminated block masking an age secret key
b"/* doc\nAGE-SECRET-KEY-1ABCDEF0123456789\n",
# inline open+close then a secret on the same line
b"/* note */ password = realSecret123\n",
]
for data in leaking:
assert (
pol._content_deny_reason("/etc/app/app.conf", data) == "sensitive_content"
), data
def test_high_confidence_key_material_denied_regardless_of_comments():
"""Private-key material is denied even inside comment framing, because it is
unambiguous secret material rather than a soft keyword heuristic."""
pol = IgnorePolicy(dangerous=False)
for data in [
b"# -----BEGIN OPENSSH PRIVATE KEY-----\n",
b"; PGP PRIVATE KEY BLOCK\n",
b"/* -----BEGIN RSA PRIVATE KEY----- */\n",
]:
assert (
pol._content_deny_reason("/etc/app/app.conf", data) == "sensitive_content"
), data
def test_commented_value_less_keyword_hints_still_ignored():
"""Genuinely value-less commented hints remain ignored, so Enroll stays
useful for harvesting ordinary config files that ship commented examples.
Only a bare credential *word* with no assigned value is tolerated in a
comment; a populated commented assignment is treated as a real (disabled)
secret -- see test_commented_out_credential_values_require_dangerous."""
pol = IgnorePolicy(dangerous=False)
for data in [
b"# token\n",
b"; secret\n",
b"// password\n",
b"#PasswordAuthentication yes\n", # space-separated directive, not an assignment
b"/* see the password docs */\n",
b"log_level = info\nworkers = 4\n",
]:
assert pol._content_deny_reason("/etc/app/app.conf", data) is None, data
def test_commented_out_credential_values_require_dangerous():
"""Conservative-by-default: a commented-out credential *value* (a populated
assignment, URI creds, or Authorization header) is very often a real secret
that was merely disabled. Such a file is refused in safe mode and requires
--dangerous, regardless of comment style or block-comment framing."""
safe = IgnorePolicy(dangerous=False)
dangerous = IgnorePolicy(dangerous=True)
commented_real_secrets = [
b"# password = realProductionPass\n",
b"; auth_token = abc123secret\n",
b"// client_secret: deadbeef\n",
b"/* password = changeme123 */\n",
b"/*\n api_key = AKIA0123\n*/\nlisten = 8080\n",
b"# DATABASE_URL=postgres://user:pass@host/db\n",
b"# -----BEGIN OPENSSH PRIVATE KEY-----\n",
b"/*\nAuthorization: Bearer eyJabc\n*/\n",
]
for data in commented_real_secrets:
# Refused in safe mode...
assert (
safe._content_deny_reason("/etc/app/app.conf", data) == "sensitive_content"
), data
# ...but --dangerous still collects it (operator's explicit choice).
assert dangerous._content_deny_reason("/etc/app/app.conf", data) is None, data