Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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),
    ],
)