Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/gitleaks.yml
Original file line number Diff line number Diff line change
@@ -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@17271b2ddeb36438a2650e53a6e0f335832dc87e # owner-free: allow
69 changes: 69 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# 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 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.
# 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-<kind>-<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.
# 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,}))'''

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Extract the credential before applying its allowlist

Gitleaks uses capture group 0 (the entire --flag=value match) when secretGroup is unset, and an allowlist without regexTarget tests that extracted secret. Consequently, the value-anchored allowlists below can never match: intended exemptions such as --auth-key=file:/tmp/abcdefghijklmnop and --secret id=my-build-secret-source exceed the entropy threshold and fail both pre-commit and CI. The entropy check likewise measures the flag plus value rather than the credential. Restructure the alternatives around one capture group and set secretGroup, or target the full match explicitly.

Useful? React with 馃憤聽/ 馃憥.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not reproduced with gitleaks 8.30.1. When secretGroup is unset and the regex has capture groups, gitleaks uses the first group that matched as the secret, not group 0. Each of the three value alternatives here is a capture group.

Checked with this branch's .gitleaks.toml: --auth-key=file:/run/secrets/service-token, --auth-key=file:/tmp/abcdefghijklmnop, --secret id=my-build-secret-source,src=/tmp/abcdefghijklmnop and --env-secret API_TOKEN=my-secret-name-for-build are not reported. With this allowlist removed, all four are reported, and the reported secret starts at file:, id= or API_TOKEN=, without the flag. A literal such as --password=<random 24 characters> is still reported with the allowlist in place.

entropy = 3.0
keywords = ["token", "key", "password", "passwd", "secret"]

# `lpass show --password <item>` 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\s+(?:--?[a-z][a-z-]*(?:=\S*)?\s+)*--password\s+(?:"[^"]*"|'[^']*'|\S+)(?:[^-]|-[^-])*$''']

# Values that say where a secret lives rather than hold it: `--auth-key=file:<path>`, docker
# `--secret id=<name>,src=<path>`, and `--env-secret NAME=<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_]*\}?)?["']?$''']
9 changes: 9 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,12 @@ repos:
language: system
stages: [post-commit]
pass_filenames: false

# 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
# 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
Loading