From 12405aeacf79f67b4666e296b7a85fa86a78b37f Mon Sep 17 00:00:00 2001 From: Dobricean Ioan Dorian Date: Thu, 2 Jul 2026 19:38:39 +0300 Subject: [PATCH 1/4] Add betterleaks action --- .github/workflows/betterleaks.yml | 48 +++++++++++++++++ .gitleaks.toml | 89 +++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 .github/workflows/betterleaks.yml create mode 100644 .gitleaks.toml diff --git a/.github/workflows/betterleaks.yml b/.github/workflows/betterleaks.yml new file mode 100644 index 0000000..542f4a7 --- /dev/null +++ b/.github/workflows/betterleaks.yml @@ -0,0 +1,48 @@ +name: Betterleaks + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + scan: + name: Scan for secrets + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Betterleaks + id: betterleaks + continue-on-error: true + uses: dortort/betterleaks-action@v0.1.0 + 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: 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 diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..5e0ee0a --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,89 @@ +title = "volto-eea-website-theme 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=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''', +] From c6d98a6afc2b844a3479a33912a25da004b9abd9 Mon Sep 17 00:00:00 2001 From: Dobricean Ioan Dorian Date: Thu, 2 Jul 2026 19:54:01 +0300 Subject: [PATCH 2/4] Fix betterleaks findings --- .env | 4 +--- RELEASE.md | 2 +- cypress/support/commands.js | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.env b/.env index ef282fa..7afcd38 100644 --- a/.env +++ b/.env @@ -1,3 +1 @@ -# Jest configuration variables -# - possible values: ON, OFF -JEST_USE_SETUP=OFF \ No newline at end of file +JEST_USE_SETUP=OFF # Jest configuration variables: ON, OFF diff --git a/RELEASE.md b/RELEASE.md index 4e311a0..27baae1 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -33,7 +33,7 @@ Release-it is a tool that automates 4 important steps in the release process: To configure the authentification, you need to export GITHUB_TOKEN for [GitHub](https://github.com/settings/tokens) ``` - export GITHUB_TOKEN=XXX-XXXXXXXXXXXXXXXXXXXXXX + export GITHUB_TOKEN="${GITHUB_TOKEN}" ``` To configure npm, you can use the `npm login` command or use a configuration file with a TOKEN : diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 115811f..5ded953 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -5,14 +5,14 @@ Cypress.Commands.add('autologin', () => { let api_url, user, password; api_url = Cypress.env('API_PATH') || 'http://localhost:8080/Plone'; user = 'admin'; - password = 'admin'; + password = 'admin'; //betterleaks:allow return cy .request({ method: 'POST', url: `${api_url}/@login`, headers: { Accept: 'application/json' }, - body: { login: user, password: password }, + body: { login: user, password: password }, //betterleaks:allow }) .then((response) => cy.setCookie('auth_token', response.body.token)); }); From daca3cf66c5791c678825e5a3378d1b1a8526173 Mon Sep 17 00:00:00 2001 From: Dobricean Ioan Dorian Date: Thu, 2 Jul 2026 20:47:07 +0300 Subject: [PATCH 3/4] Update Betterleaks config documentation --- .gitleaks.toml | 2 +- README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/.gitleaks.toml b/.gitleaks.toml index 5e0ee0a..d26a674 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -1,4 +1,4 @@ -title = "volto-eea-website-theme Betterleaks config" +title = "Betterleaks config" [extend] useDefault = true diff --git a/README.md b/README.md index 85ac7eb..391edec 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,52 @@ See [RELEASE.md](https://github.com/eea/volto-slate-footnote/blob/master/RELEASE See [DEVELOP.md](https://github.com/eea/volto-slate-footnote/blob/master/DEVELOP.md). +## Secret Scanning + +This repository uses the Betterleaks GitHub Action to scan the current +repository content on every push and pull request. The scan uses the rules in +`.gitleaks.toml` and uploads a `betterleaks-report` artifact when a finding is +detected. + +There are three common outcomes: + +1. **Everything is OK.** The `Betterleaks / Scan for secrets` check is green and + no action is needed. Regular references to runtime values are OK, for example: + + ```js + const tokenFromCookie = req.universalCookies.get('auth_token'); + ``` + +2. **A real secret was found.** The check is red and the workflow log asks you to + download the `betterleaks-report` artifact. Open the artifact from the GitHub + Actions run and check the reported file, line and rule. Remove the committed + value, move it to the proper secret store, and rotate it if it was exposed. + A report entry looks like this: + + ```json + { + "RuleID": "secret-literal-assignment", + "File": "src/config.js", + "StartLine": 12, + "Secret": "[REDACTED]" + } + ``` + +3. **The finding is a false positive.** Keep the value only if it is clearly not + sensitive, such as a test fixture, placeholder, or public example. Add + `betterleaks:allow` on the same line and include a short explanation in the + pull request. + + ```js + const testPassword = 'admin'; //betterleaks:allow + ``` + + ```yaml + password: "admin" #betterleaks:allow + ``` + +Do not add `betterleaks:allow` to real credentials. + ## Copyright and license The Initial Owner of the Original Code is European Environment Agency (EEA). From 30732758e866d24643aceeacfdc0fa7b24ca4fa0 Mon Sep 17 00:00:00 2001 From: Dobricean Ioan Dorian Date: Fri, 3 Jul 2026 16:04:32 +0300 Subject: [PATCH 4/4] Update Betterleaks email notification --- .github/workflows/betterleaks.yml | 85 +++++++++++++++++++++++++++++++ README.md | 13 +++++ 2 files changed, 98 insertions(+) diff --git a/.github/workflows/betterleaks.yml b/.github/workflows/betterleaks.yml index 542f4a7..e3ee2df 100644 --- a/.github/workflows/betterleaks.yml +++ b/.github/workflows/betterleaks.yml @@ -11,11 +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 with: fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Run Betterleaks id: betterleaks @@ -41,6 +47,85 @@ jobs: 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<> "$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: | diff --git a/README.md b/README.md index 391edec..61fa686 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,19 @@ repository content on every push and pull request. The scan uses the rules in `.gitleaks.toml` and uploads a `betterleaks-report` artifact when a finding is detected. +If the optional SMTP secrets are configured, failed scans also send an email to +the last commit committer. The workflow expects these repository or +organization secrets: + +- `SMTP_URL` +- `SMTP_PORT` (optional, defaults to `25`) +- `SMTP_EMAIL` +- `SMTP_PASSWORD` (optional if the SMTP server does not require authentication) + +Port `465` is sent with direct TLS; other ports use the default SMTP handshake. +The email includes a short finding summary from the redacted Betterleaks report, +including the redacted matched line from each finding. + There are three common outcomes: 1. **Everything is OK.** The `Betterleaks / Scan for secrets` check is green and