server/lib/src/scanner/secrets-rules.ron
// Holger's built-in SECRETS rules (lane HOLGER-G3, 2026-10-24).
//
// Rules are DATA. This file is compiled into the server as the default set; an
// operator replaces it with their own file (`steps.secrets: (rules: "/path.ron")`,
// lowered onto `promotion.intake.secrets.rules`), carried over the airgap like the
// YARA rules. The scan worker applies the rules inside the scan sandbox.
//
// One rule:
// id stable, `[a-z0-9-]`; the finding is `secret:<id>`
// title what was found, in words
// severity info | low | medium | high | critical. `secrets: block` refuses
// high and critical; medium and below always warn
// pattern a regex over the member's bytes (Rust `regex`, linear time). A
// named group `secret` marks the secret itself (for `entropy_min`
// and `allow`); without it, the whole match is the secret
// path a regex over the member's path; the rule only looks at members it
// matches. A rule with a path and no pattern fires on the file alone
// path_allow paths the rule never fires on
// keywords cheap prefilter: a member is matched only if it holds one of
// these (ASCII, case-insensitive)
// entropy_min the secret's Shannon entropy (bits per byte) must reach this
// allow regexes over the secret: a match is a placeholder, not a secret
//
// The evidence names the rule and the path (and the line), NEVER the secret.
(
schema: "holger.secret-rules/v1",
version: "2026.10.24",
// Placeholders, for every rule.
allow: [
"(?i)example",
"(?i)x{8,}",
"(?i)dummy|placeholder|changeme|redacted|your[_-]?(key|token|secret)",
],
skip_paths: [],
rules: [
// ── private keys ────────────────────────────────────────────────
(id: "private-key-pem", title: "a PEM private key", severity: critical,
keywords: ["PRIVATE KEY-----"],
pattern: "-----BEGIN (?:RSA |EC |DSA |ENCRYPTED )?PRIVATE KEY-----\\r?\\n(?:[A-Za-z-]+: [^\\r\\n]*\\r?\\n)*\\r?\\n?(?P<secret>[A-Za-z0-9+/]{32,})"),
(id: "private-key-openssh", title: "an OpenSSH private key", severity: critical,
keywords: ["BEGIN OPENSSH PRIVATE KEY"],
pattern: "-----BEGIN OPENSSH PRIVATE KEY-----\\r?\\n(?P<secret>[A-Za-z0-9+/]{32,})"),
(id: "private-key-pgp", title: "a PGP private key block", severity: critical,
keywords: ["BEGIN PGP PRIVATE KEY BLOCK"],
pattern: "-----BEGIN PGP PRIVATE KEY BLOCK-----\\r?\\n(?:[A-Za-z-]+: [^\\r\\n]*\\r?\\n)*\\r?\\n?(?P<secret>[A-Za-z0-9+/]{32,})"),
(id: "private-key-putty", title: "a PuTTY private key", severity: critical,
keywords: ["PuTTY-User-Key-File-"],
pattern: "PuTTY-User-Key-File-[0-9]: [a-z0-9-]+\\r?\\n(?s:.*?)Private-Lines: [0-9]+\\r?\\n(?P<secret>[A-Za-z0-9+/]{32,})"),
// ── cloud credentials ───────────────────────────────────────────
(id: "aws-access-key-id", title: "an AWS access key id", severity: high,
keywords: ["AKIA", "ASIA", "ABIA", "ACCA"],
pattern: "\\b(?P<secret>(?:AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16})\\b"),
(id: "aws-secret-access-key", title: "an AWS secret access key", severity: critical,
keywords: ["aws"],
pattern: "(?i)aws_?secret_?(?:access_?)?key['\"]?\\s*[:=]\\s*['\"]?(?P<secret>[A-Za-z0-9/+=]{40})(?:[^A-Za-z0-9/+=]|$)",
entropy_min: 3.5),
(id: "gcp-service-account-key", title: "a Google Cloud service account key", severity: critical,
keywords: ["service_account"],
pattern: "\"type\"\\s*:\\s*\"service_account\"(?s:.*?)\"private_key\"\\s*:\\s*\"-----BEGIN (?:RSA )?PRIVATE KEY-----(?:\\\\n)?(?P<secret>[A-Za-z0-9+/]{32,})"),
(id: "gcp-api-key", title: "a Google API key", severity: high,
keywords: ["AIza"],
pattern: "\\b(?P<secret>AIza[0-9A-Za-z_\\-]{35})(?:[^0-9A-Za-z_\\-]|$)"),
(id: "azure-storage-account-key", title: "an Azure storage account key", severity: critical,
keywords: ["AccountKey="],
pattern: "(?i)AccountKey=(?P<secret>[A-Za-z0-9+/]{86}==)"),
(id: "azure-ad-client-secret", title: "an Azure AD (Entra ID) client secret", severity: high,
keywords: ["Q~"],
pattern: "(?:^|[^A-Za-z0-9_~.\\-])(?P<secret>[A-Za-z0-9_~.]{3}[0-9]Q~[A-Za-z0-9_~.\\-]{31,34})(?:[^A-Za-z0-9_~.\\-]|$)",
entropy_min: 3.5),
// ── tokens ──────────────────────────────────────────────────────
(id: "github-token", title: "a GitHub token", severity: high,
keywords: ["ghp_", "gho_", "ghu_", "ghs_", "ghr_"],
pattern: "\\b(?P<secret>gh[pousr]_[A-Za-z0-9]{36})\\b"),
(id: "github-fine-grained-token", title: "a GitHub fine-grained token", severity: high,
keywords: ["github_pat_"],
pattern: "\\b(?P<secret>github_pat_[A-Za-z0-9_]{82})\\b"),
(id: "gitlab-token", title: "a GitLab token", severity: high,
keywords: ["glpat-", "glrt-", "gldt-", "glptt-"],
pattern: "\\b(?P<secret>gl(?:pat|rt|dt|ptt)-[A-Za-z0-9_\\-]{20,})"),
(id: "slack-token", title: "a Slack token", severity: high,
keywords: ["xox"],
pattern: "\\b(?P<secret>xox[baprse]-[0-9A-Za-z\\-]{10,})"),
(id: "slack-webhook", title: "a Slack incoming webhook", severity: high,
keywords: ["hooks.slack.com"],
pattern: "https://hooks\\.slack\\.com/services/(?P<secret>T[A-Z0-9]+/B[A-Z0-9]+/[A-Za-z0-9]{20,})"),
(id: "stripe-live-key", title: "a Stripe live secret key", severity: critical,
keywords: ["_live_"],
pattern: "\\b(?P<secret>(?:sk|rk)_live_[0-9A-Za-z]{24,})"),
(id: "stripe-test-key", title: "a Stripe test secret key", severity: medium,
keywords: ["_test_"],
pattern: "\\b(?P<secret>(?:sk|rk)_test_[0-9A-Za-z]{24,})"),
(id: "npm-token", title: "an npm access token", severity: high,
keywords: ["npm_"],
pattern: "\\b(?P<secret>npm_[A-Za-z0-9]{36})\\b"),
(id: "npmrc-auth-token", title: "an auth token in an .npmrc", severity: high,
keywords: ["_authtoken"],
pattern: "(?i)_authToken\\s*=\\s*['\"]?(?P<secret>[^\\s'\"$]{20,})"),
(id: "pypi-token", title: "a PyPI API token", severity: high,
keywords: ["pypi-AgE"],
pattern: "\\b(?P<secret>pypi-AgE(?:IcHlwaS5vcmc|NdGVzdC5weXBpLm9yZw)[A-Za-z0-9_\\-]{50,})"),
(id: "crates-io-token", title: "a crates.io API token", severity: high,
keywords: ["cio"],
pattern: "\\b(?P<secret>cio[A-Za-z0-9]{32})\\b",
entropy_min: 4.0),
// ── files that hold credentials ─────────────────────────────────
(id: "kubeconfig-credentials", title: "a kubeconfig with credentials", severity: high,
keywords: ["kind: config"],
pattern: "(?m)^kind:\\s*Config\\s*$(?s:.*?)^\\s*(?:client-key-data|token|password):\\s*['\"]?(?P<secret>[^\\s'\"]{8,})"),
(id: "dotenv-secret", title: "a secret in a .env file", severity: high,
path: "(?:^|[/!])\\.env(?:\\.[A-Za-z0-9_\\-]+)?$",
path_allow: ["\\.(?:example|sample|template|dist|defaults?)$"],
pattern: "(?m)^[ \\t]*(?:export[ \\t]+)?[A-Za-z_][A-Za-z0-9_]*(?:SECRET|TOKEN|PASSWORD|PASSWD|API_?KEY|PRIVATE_?KEY|ACCESS_?KEY)[A-Za-z0-9_]*[ \\t]*=[ \\t]*['\"]?(?P<secret>[^\\s'\"#]{8,})"),
(id: "dotenv-file", title: "a .env file", severity: medium,
path: "(?:^|[/!])\\.env(?:\\.[A-Za-z0-9_\\-]+)?$",
path_allow: ["\\.(?:example|sample|template|dist|defaults?)$"]),
// ── high-entropy strings with context ───────────────────────────
(id: "generic-high-entropy-secret", title: "a high-entropy string assigned to a secret-like name", severity: medium,
keywords: ["key", "secret", "token", "passw"],
pattern: "(?i)(?:api[_-]?key|secret[_-]?key|client[_-]?secret|access[_-]?token|auth[_-]?token|passw(?:or)?d|secret|token)['\"]?\\s*(?::|=>?)\\s*['\"](?P<secret>[A-Za-z0-9+/=_\\-.]{20,128})['\"]",
entropy_min: 4.2),
],
)