From 4a4d201b3533c19e693a36085f184daf1bd3e1fd Mon Sep 17 00:00:00 2001 From: sasha-s Date: Mon, 19 Oct 2026 18:48:11 +0000 Subject: [PATCH 1/3] Scan each change for secrets with gitleaks The workflow calls the shared reusable workflow in Positronic-Robotics/secret-scanning. It scans only the commits that a pull request or a push adds, with the default gitleaks rules and four added rules (Nebius IAM tokens, Anthropic OAuth tokens, Tailscale keys, secrets in command-line flags). A finding shows file, line and rule id, never the value. .gitleaks.toml is a copy of the shared file; the check fails if the copy differs. The pre-commit hook runs the same rules on staged changes. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/gitleaks.yml | 20 ++++++++++++ .gitleaks.toml | 59 ++++++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 6 ++++ 3 files changed, 85 insertions(+) create mode 100644 .github/workflows/gitleaks.yml create mode 100644 .gitleaks.toml create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml new file mode 100644 index 0000000..9ac0a79 --- /dev/null +++ b/.github/workflows/gitleaks.yml @@ -0,0 +1,20 @@ +name: gitleaks + +on: + pull_request: + push: + branches: [main, main-positronic] # owner-free: allow + +permissions: + contents: read + +# A push run is keyed on its own sha: each push scans only its own commits, so none may be dropped. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name == 'push' && github.sha || 'pr' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + gitleaks: + # Commit of Positronic-Robotics/secret-scanning to run; .gitleaks.toml is a copy of its gitleaks.toml. Public, not a secret. + # To move it, follow "Upgrading the rules or gitleaks" in that repository's README. + uses: Positronic-Robotics/secret-scanning/.github/workflows/gitleaks.yml@02467588d0db3bbe7c890d2b52d8a10d108a058b # owner-free: allow diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..7d61282 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,59 @@ +# Shared gitleaks rules. Copy this file byte for byte to `.gitleaks.toml` at a repository root; +# the reusable workflow fails when the copy differs. Edit it here, then follow the README upgrade steps. +title = "shared gitleaks rules" + +[extend] +useDefault = true + +# Credential shapes the default rules miss. Each has a canary in canary_test.py. +# Deleting this section falls back to the default rules. + +# Matches a Nebius IAM token: ne1 followed by 100 or more letters, digits, dots, dashes or underscores. +# Shape: ne1<100+ letters, digits, . - _> +[[rules]] +id = "nebius-iam-token" +description = "Nebius IAM access token (ne1 prefix)" +regex = '''\b(ne1[A-Za-z0-9][A-Za-z0-9_\-.]{99,})''' +secretGroup = 1 +entropy = 4.0 +keywords = ["ne1"] + +# Matches an Anthropic OAuth token: sk-ant-oat or sk-ant-ort, two digits, a dash, then 80 or more +# letters, digits, dashes or underscores. Shape: sk-ant-oat01-<80+ letters, digits, - _> +[[rules]] +id = "anthropic-oauth-token" +description = "Anthropic OAuth access or refresh token" +regex = '''\b(sk-ant-o[a-z]t\d{2}-[A-Za-z0-9_\-]{80,})''' +secretGroup = 1 +keywords = ["sk-ant-oat", "sk-ant-ort"] + +# Matches a Tailscale key: tskey-, a lowercase kind such as auth or client, then two dash-separated +# runs of letters and digits. Shape: tskey--<6+ letters, digits>-<16+ letters, digits> +[[rules]] +id = "tailscale-key" +description = "Tailscale auth, API, OAuth client or webhook key" +regex = '''\b(tskey-[a-z]+-[A-Za-z0-9]{6,}-[A-Za-z0-9]{16,})''' +secretGroup = 1 +keywords = ["tskey-"] + +# Matches a literal value of 16 or more characters given to a flag whose name ends in token, api key, +# password, passwd, secret, access key or auth key. Shape: --api-key=<16+ characters> or --hf-token <16+ characters> +[[rules]] +id = "cli-flag-secret" +description = "Credential passed as a command-line flag value" +# The flag name must end in the credential word, so --token-file and --password-stdin never match. +# A value starting with $ < { / . ~ or a quote is a variable, placeholder or path, not a literal. +regex = '''(?i)--(?:[a-z0-9]+[-_])*(?:token|api[-_]?key|password|passwd|secret|secret[-_]key|access[-_]key|auth[-_]?key)(?:=|[ \t]+)["']?([A-Za-z0-9_\-+=!@#%^&*:,][A-Za-z0-9_\-+/=.!@#%^&*~:,]{15,})''' +secretGroup = 1 +entropy = 3.0 +keywords = ["token", "key", "password", "passwd", "secret"] + +# `lpass show --password ` names the field to read; the value after it is a vault item path. +[[rules.allowlists]] +regexTarget = "line" +regexes = ['''\blpass\s+show\b'''] + +# Values that say where a secret lives rather than hold it: `--auth-key=file:`, docker +# `--secret id=,src=`, and `--env-secret NAME=` references. +[[rules.allowlists]] +regexes = ['''^(?:file:|id=|env=)''', '''^[A-Z][A-Z0-9_]*=[a-z0-9]+(?:-[a-z0-9]+)*$'''] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..de864ae --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,6 @@ +repos: + # Secret scan of the staged changes, with the rules in .gitleaks.toml + - repo: https://github.com/gitleaks/gitleaks + rev: v8.30.1 + hooks: + - id: gitleaks From b3b1e41e2d672cb242ab5ef83a48ac18959fe759 Mon Sep 17 00:00:00 2001 From: sasha-s Date: Tue, 20 Oct 2026 14:20:43 +0000 Subject: [PATCH 2/3] Turn git color off for the gitleaks pre-commit hook gitleaks 8.30.1 reads no changes from git output it colors. With color.ui or color.diff set to always, the hook passed a staged secret. The hook now runs with both set to never through GIT_CONFIG_COUNT; the upstream fix (gitleaks/gitleaks#2284) is not in a release yet. Co-Authored-By: Claude Opus 5.5 (1M context) --- .pre-commit-config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index de864ae..fb0f861 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,3 +4,6 @@ repos: rev: v8.30.1 hooks: - id: gitleaks + # gitleaks 8.30.1 reads no changes from git output it colors (color.ui or color.diff set to + # always), and then passes. Turn git color off for its run. + entry: env GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=color.ui GIT_CONFIG_VALUE_0=never GIT_CONFIG_KEY_1=color.diff GIT_CONFIG_VALUE_1=never gitleaks git --pre-commit --redact --staged --verbose From 8075122eeebc4d08a7e8a1b64543017dc8a16dfa Mon Sep 17 00:00:00 2001 From: sasha-s Date: Wed, 21 Oct 2026 05:18:49 +0000 Subject: [PATCH 3/3] Pin secret-scanning to its merged main 17271b2 The rules copy now matches gitleaks.toml at that commit: the lpass allowlist covers only the lpass command itself, curl -u with a shell variable is allowed, and cli-flag-secret also catches quoted values that hold shell punctuation. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/gitleaks.yml | 2 +- .gitleaks.toml | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml index 9ac0a79..337d060 100644 --- a/.github/workflows/gitleaks.yml +++ b/.github/workflows/gitleaks.yml @@ -17,4 +17,4 @@ jobs: gitleaks: # Commit of Positronic-Robotics/secret-scanning to run; .gitleaks.toml is a copy of its gitleaks.toml. Public, not a secret. # To move it, follow "Upgrading the rules or gitleaks" in that repository's README. - uses: Positronic-Robotics/secret-scanning/.github/workflows/gitleaks.yml@02467588d0db3bbe7c890d2b52d8a10d108a058b # owner-free: allow + uses: Positronic-Robotics/secret-scanning/.github/workflows/gitleaks.yml@17271b2ddeb36438a2650e53a6e0f335832dc87e # owner-free: allow diff --git a/.gitleaks.toml b/.gitleaks.toml index 7d61282..e4a6e53 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -1,11 +1,12 @@ -# Shared gitleaks rules. Copy this file byte for byte to `.gitleaks.toml` at a repository root; -# the reusable workflow fails when the copy differs. Edit it here, then follow the README upgrade steps. +# Shared gitleaks rules from Positronic-Robotics/secret-scanning. A repository holds a byte-for-byte copy as +# `.gitleaks.toml`, and the reusable workflow fails when the copy differs. Edit the file in that repository, +# then follow its README upgrade steps; a copy edited elsewhere fails the check. title = "shared gitleaks rules" [extend] useDefault = true -# Credential shapes the default rules miss. Each has a canary in canary_test.py. +# Credential shapes the default rules miss. Each has a canary in that repository's canary_test.py. # Deleting this section falls back to the default rules. # Matches a Nebius IAM token: ne1 followed by 100 or more letters, digits, dots, dashes or underscores. @@ -43,17 +44,26 @@ id = "cli-flag-secret" description = "Credential passed as a command-line flag value" # The flag name must end in the credential word, so --token-file and --password-stdin never match. # A value starting with $ < { / . ~ or a quote is a variable, placeholder or path, not a literal. -regex = '''(?i)--(?:[a-z0-9]+[-_])*(?:token|api[-_]?key|password|passwd|secret|secret[-_]key|access[-_]key|auth[-_]?key)(?:=|[ \t]+)["']?([A-Za-z0-9_\-+=!@#%^&*:,][A-Za-z0-9_\-+/=.!@#%^&*~:,]{15,})''' -secretGroup = 1 +# Inside quotes the rest of the value may hold any punctuation up to a space or the closing quote, +# except $ inside double quotes, where the shell expands it. The secret is the first group that matched. +regex = '''(?i)--(?:[a-z0-9]+[-_])*(?:token|api[-_]?key|password|passwd|secret|secret[-_]key|access[-_]key|auth[-_]?key)(?:=|[ \t]+)(?:'([^\s'$<{/.~][^\s']{15,})|"([^\s"$<{/.~][^\s"$]{15,})|([A-Za-z0-9_\-+=!@#%^&*:,][A-Za-z0-9_\-+/=.!@#%^&*~:,]{15,}))''' entropy = 3.0 keywords = ["token", "key", "password", "passwd", "secret"] # `lpass show --password ` names the field to read; the value after it is a vault item path. +# The line may hold no other `--` outside that command, so a literal flag beside it is still caught. [[rules.allowlists]] regexTarget = "line" -regexes = ['''\blpass\s+show\b'''] +regexes = ['''^(?:[^-]|-[^-])*\blpass\s+show\s+(?:--?[a-z][a-z-]*(?:=\S*)?\s+)*--password\s+(?:"[^"]*"|'[^']*'|\S+)(?:[^-]|-[^-])*$'''] # Values that say where a secret lives rather than hold it: `--auth-key=file:`, docker # `--secret id=,src=`, and `--env-secret NAME=` references. [[rules.allowlists]] regexes = ['''^(?:file:|id=|env=)''', '''^[A-Z][A-Z0-9_]*=[a-z0-9]+(?:-[a-z0-9]+)*$'''] + +# The default curl-auth-user rule flags `curl -u "$USER:"`. A value made only of shell variables, such as +# `$USER:` or `${USER}:${PASS}`, names where the credential comes from; a literal password after a variable is +# still caught. +[[allowlists]] +targetRules = ["curl-auth-user"] +regexes = ['''^["']?\$\{?[A-Za-z_][A-Za-z0-9_]*\}?:(?:\$\{?[A-Za-z_][A-Za-z0-9_]*\}?)?["']?$''']