Skip to content
Merged
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
122 changes: 8 additions & 114 deletions .github/workflows/betterleaks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,123 +11,17 @@ jobs:
scan:
name: Scan for secrets
runs-on: ubuntu-latest
env:
SMTP_URL: ${{ secrets.SMTP_URL }}
SMTP_PORT: ${{ secrets.SMTP_PORT || '25' }}
SMTP_EMAIL: ${{ secrets.SMTP_EMAIL }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 1
ref: ${{ github.event.pull_request.head.sha || github.sha }}

- name: Run Betterleaks
id: betterleaks
continue-on-error: true
uses: dortort/betterleaks-action@v0.1.0
# Shared EEA secret scan: https://github.com/eea/github-actions/tree/main/betterleaks
- uses: eea/github-actions/betterleaks@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
scan-mode: dir
scan-path: .
config: .gitleaks.toml
report-format: json
report-path: betterleaks-report.json
redact: "true"
no-color: "true"
no-banner: "true"
fail-on-leak: "true"

- name: Upload Betterleaks report
if: always()
uses: actions/upload-artifact@v4
with:
name: betterleaks-report
path: betterleaks-report.json
if-no-files-found: ignore

- name: Build Betterleaks email summary
id: leak_summary
if: steps.betterleaks.outcome == 'failure'
shell: bash
run: |
if [[ -s betterleaks-report.json ]]; then
jq -r '
def one_line:
tostring
| gsub("[\r\n]+"; " ")
| if length > 240 then .[0:240] + "..." else . end;

.[:20][]
| "- " + (.RuleID // "unknown-rule")
+ " at " + (.File // "unknown-file")
+ ":" + ((.StartLine // 0) | tostring)
+ "\n match: " + ((.Match // .Secret // "REDACTED") | one_line)
' betterleaks-report.json > betterleaks-email-summary.txt

count="$(jq 'length' betterleaks-report.json)"
if (( count > 20 )); then
{
echo ""
echo "... and $((count - 20)) more finding(s). Download the artifact for full details."
} >> betterleaks-email-summary.txt
fi
else
echo "No JSON report was generated. Download the workflow logs for details." > betterleaks-email-summary.txt
fi

{
echo "text<<BETTERLEAKS_SUMMARY"
cat betterleaks-email-summary.txt
echo "BETTERLEAKS_SUMMARY"
} >> "$GITHUB_OUTPUT"

- name: Resolve committer email
id: committer
if: steps.betterleaks.outcome == 'failure'
shell: bash
run: |
committer_email="$(git log -1 --format='%ce')"
author_email="$(git log -1 --format='%ae')"
email="$committer_email"
if [[ -z "$email" || "$email" == *"noreply.github.com"* ]]; then
email="$author_email"
fi
if [[ "$email" =~ ^[^[:space:]@]+@[^[:space:]@]+\.[^[:space:]@]+$ && "$email" != *"noreply.github.com"* ]]; then
echo "email=$email" >> "$GITHUB_OUTPUT"
else
echo "No deliverable committer email found; skipping Betterleaks email notification."
echo "email=" >> "$GITHUB_OUTPUT"
fi

- name: Email committer on Betterleaks failure
if: steps.betterleaks.outcome == 'failure' && steps.committer.outputs.email != '' && env.SMTP_URL != '' && env.SMTP_EMAIL != ''
uses: dawidd6/action-send-mail@v18
with:
server_address: ${{ env.SMTP_URL }}
server_port: ${{ env.SMTP_PORT }}
secure: ${{ env.SMTP_PORT == '465' }}
username: ${{ env.SMTP_EMAIL }}
password: ${{ env.SMTP_PASSWORD }}
from: ${{ env.SMTP_EMAIL }}
to: ${{ steps.committer.outputs.email }}
subject: "[Betterleaks] Secret scan failed in ${{ github.repository }}"
body: |
Betterleaks detected one or more potential secrets.

Repository: ${{ github.repository }}
Branch: ${{ github.ref_name }}
Commit: ${{ github.sha }}
Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}

Findings:
${{ steps.leak_summary.outputs.text }}

Download the betterleaks-report artifact from the workflow run for details.

- name: Fail if Betterleaks found leaks
if: steps.betterleaks.outcome == 'failure'
run: |
echo "Betterleaks detected one or more secrets. Download the betterleaks-report artifact from this workflow run for details."
exit 1
smtp-url: ${{ secrets.SMTP_URL }}
smtp-port: ${{ secrets.SMTP_PORT || '25' }}
smtp-email: ${{ secrets.SMTP_EMAIL }}
smtp-password: ${{ secrets.SMTP_PASSWORD }}
88 changes: 3 additions & 85 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -1,89 +1,7 @@
# Repository-specific Betterleaks config. The shared EEA rules are applied by
# eea/github-actions/betterleaks (betterleaks/config/eea-base.toml).

title = "Betterleaks config"

[extend]
useDefault = true

[[rules]]
id = "secret-literal-assignment"
description = "Secret-like literal assignment in source, YAML, env and config files"
regex = '''(?i)([A-Za-z0-9_.-]*(?:password|passwd|pwd|api[_-]?key|apikey|access[_-]?key|secret[_-]?access[_-]?key|private[_-]?key|client[_-]?secret|consumer[_-]?key|consumer[_-]?secret|(?:auth|access|refresh|session|api|bearer|id|jwt|csrf|xsrf|oauth)[_-]?token|translate[_-]?auth|translation[_-]?auth|auth[_-]?password|auth[_-]?key|auth[_-]?credentials|database[_-]?url|connection[_-]?string)[A-Za-z0-9_.-]*)[ \t]*[:=][ \t]*["'']?([^"''[:space:]#{}$.][^"''\n#{}]{2,})["'']?'''
secretGroup = 2
keywords = [
"password",
"passwd",
"pwd",
"api_key",
"apikey",
"access_key",
"private_key",
"client_secret",
"consumer_key",
"consumer_secret",
"auth_token",
"access_token",
"refresh_token",
"session_token",
"api_token",
"bearer_token",
"id_token",
"jwt_token",
"csrf_token",
"xsrf_token",
"oauth_token",
"translate_auth",
"translation_auth",
"auth_password",
"auth_key",
"auth_credentials",
"database_url",
"connection_string"
]
tags = ["literal-secret"]

[[rules]]
id = "env-short-secret-assignment"
description = "Uppercase env-style PASS/TOKEN/SECRET assignment"
regex = '''\b((?:PASS|TOKEN|SECRET|[A-Z0-9_]*(?:_PASS|_TOKEN|_SECRET|PASS_|TOKEN_|SECRET_)[A-Z0-9_]*))[ \t]*[:=][ \t]*["'']?([^"''[:space:]#{}$.][^"''\n#{}]{2,})["'']?'''
secretGroup = 2
keywords = [
"PASS",
"TOKEN",
"SECRET"
]
tags = ["env", "literal-secret"]

[[rules]]
id = "standalone-sk-token"
description = "Standalone sk-* token not attached to a secret-like variable name"
regex = '''(?i)\b(sk-[A-Za-z0-9][A-Za-z0-9_-]{20,})\b'''
secretGroup = 1
entropy = 2.5
keywords = ["sk-"]
tags = ["standalone-token", "generic"]

[[rules]]
id = "dotenv-only-jest-setup"
description = ".env may only contain the committed Jest setup lines"
path = '''(?i)(^|/)\.env$'''
regex = '''(?m)^(.+)$'''
secretGroup = 1
tags = ["file", "dotenv"]
[[rules.allowlists]]
regexTarget = "match"
regexes = [
'''^JEST_USE_SETUP=(ON|OFF) # Jest configuration variables: ON, OFF\r?$''',
]

[[rules]]
id = "forbidden-secret-file"
description = "Forbidden secret-bearing file committed to repository"
path = '''(?i)(^|/)(\.env\..*|\.npmrc|\.pypirc|id_rsa|id_ed25519|.*\.(pem|key|p12|pfx|jks|kubeconfig))$'''
regex = '''(?s).{1,}'''
tags = ["file", "secret-file"]

[[allowlists]]
description = "Allow Jenkins SonarQube token environment variable reference"
regexTarget = "match"
regexes = [
'''SONAR_AUTH_TOKEN''',
]
Loading