Skip to content

Commit d2d989c

Browse files
feat(trufflehog): centralize path exclusions and support .trufflehogignore
Add a centralized `trufflehog/exclude-paths.txt` for org-wide default exclusions (vendor/, lock files, manifests, grafana.json, dashboards) and support repo-local `.trufflehogignore` files for per-repo overrides. Changes: - Add trufflehog/exclude-paths.txt fetched at runtime from this repo (GitHub API → raw fallback → workflow ref fallback) - Append repo-local .trufflehogignore to exclude patterns if present - Simplify PR/merge-group scan loop with grep pre-filter - Move ${{ }} expressions from run blocks into env blocks - Fix jq CHANGELOG filter to use try/catch syntax - Clean up org-required-trufflehog.yml comments
1 parent a7ed27a commit d2d989c

3 files changed

Lines changed: 122 additions & 62 deletions

File tree

.github/workflows/org-required-trufflehog.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ permissions:
2121
jobs:
2222
secret-scan:
2323
name: TruffleHog Secret Scan
24-
uses: grafana/security-github-actions/.github/workflows/reusable-trufflehog.yml@main
24+
uses: grafana/security-github-actions/.github/workflows/reusable-trufflehog.yml@feature/trufflehog-global-excludes
2525
with:
26-
# Fail on verified secrets - blocking mode
27-
fail-on-verified: "true" # Block on verified secrets
28-
fail-on-unverified: "false" # Don't block on unverified secrets
26+
# Non-blocking: job succeeds; PR still gets comments/artifacts when findings exist
27+
fail-on-verified: "false" # Set "true" to fail on verified secrets
28+
fail-on-unverified: "false" # Set "true" to fail on unverified secrets
2929
runs-on: ${{ !github.event.repository.private && 'ubuntu-latest' || 'ubuntu-arm64-small' }} # Use same runner pattern as zizmor
3030
secrets: inherit

.github/workflows/reusable-trufflehog.yml

Lines changed: 87 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,63 @@ jobs:
4848
env:
4949
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
5050
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
51-
run: git fetch --depth=1 origin "$PR_BASE_SHA" "$PR_HEAD_SHA"
51+
run: git fetch --depth=1 origin "${PR_BASE_SHA}" "${PR_HEAD_SHA}"
5252

5353
- name: Fetch base and head commits (merge_group)
5454
if: github.event_name == 'merge_group'
5555
env:
5656
MERGE_GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }}
5757
MERGE_GROUP_HEAD_SHA: ${{ github.event.merge_group.head_sha }}
58-
run: git fetch --depth=1 origin "$MERGE_GROUP_BASE_SHA" "$MERGE_GROUP_HEAD_SHA"
58+
run: git fetch --depth=1 origin "${MERGE_GROUP_BASE_SHA}" "${MERGE_GROUP_HEAD_SHA}"
5959

6060
- name: Remove persisted credentials
6161
run: git config --unset-all http.https://github.com/.extraheader
6262

63+
- name: Fetch org-wide TruffleHog exclude patterns
64+
env:
65+
GH_TOKEN: ${{ github.token }}
66+
WORKFLOW_REF: ${{ github.workflow_ref }}
67+
run: |
68+
DEST=/tmp/trufflehog-exclude.txt
69+
REPO=grafana/security-github-actions
70+
FILE_PATH=trufflehog/exclude-paths.txt
71+
72+
# Resolve the ref the calling workflow used (e.g. @main, @feature/branch, @v1).
73+
CALLER_REF="main"
74+
if [[ -n "${WORKFLOW_REF}" ]]; then
75+
CALLER_REF=$(echo "${WORKFLOW_REF}" | sed 's|.*@||; s|^refs/heads/||; s|^refs/tags/||')
76+
fi
77+
78+
LOADED=false
79+
for REF in main "${CALLER_REF}"; do
80+
[[ "$LOADED" == "true" ]] && break
81+
if gh api "repos/${REPO}/contents/${FILE_PATH}?ref=${REF}" \
82+
-H "Accept: application/vnd.github.v3.raw" -o "${DEST}" 2>/dev/null && [[ -s "${DEST}" ]]; then
83+
echo "Loaded exclude patterns from ${REPO}@${REF} (GitHub API)"
84+
LOADED=true
85+
elif curl -fsSL "https://raw.githubusercontent.com/${REPO}/${REF}/${FILE_PATH}" \
86+
-o "${DEST}" 2>/dev/null && [[ -s "${DEST}" ]]; then
87+
echo "Loaded exclude patterns from raw.githubusercontent.com@${REF}"
88+
LOADED=true
89+
fi
90+
done
91+
92+
if [[ "$LOADED" != "true" ]]; then
93+
echo "::warning::Could not fetch TruffleHog exclude patterns from ${REPO} (tried main and ${CALLER_REF}). Scanning without exclusions."
94+
touch "${DEST}"
95+
fi
96+
97+
# Append repo-local .trufflehogignore if present (same Go regex format).
98+
if [[ -f ".trufflehogignore" ]]; then
99+
echo "" >> "${DEST}"
100+
echo "# Repo-local .trufflehogignore" >> "${DEST}"
101+
cat .trufflehogignore >> "${DEST}"
102+
echo "Appended repo-local .trufflehogignore"
103+
fi
104+
105+
echo "--- effective exclude patterns ---"
106+
cat "${DEST}"
107+
63108
- name: Install TruffleHog
64109
run: |
65110
# Download binary directly from GitHub releases for supply chain security
@@ -77,33 +122,10 @@ jobs:
77122
sudo chmod +x /usr/local/bin/trufflehog
78123
trufflehog --version
79124
80-
- name: Create global exclusions
81-
run: |
82-
# Create centralized exclusion patterns for common false positives
83-
cat > /tmp/trufflehog-exclude.txt <<'EOF'
84-
# Lock files and checksums (contain hashes, not secrets)
85-
path:go\.sum$
86-
path:go\.mod$
87-
# Dependency manifests (contain URLs that trigger false positives)
88-
path:package\.json$
89-
path:package-lock\.json$
90-
path:pnpm-lock\.yaml$
91-
path:yarn\.lock$
92-
path:poetry\.lock$
93-
path:Pipfile\.lock$
94-
path:uv\.lock$
95-
path:Cargo\.lock$
96-
path:Gemfile\.lock$
97-
# Grafana plugin metadata
98-
path:grafana\.json$
99-
EOF
100-
101-
echo "Created global exclusion patterns:"
102-
cat /tmp/trufflehog-exclude.txt
103-
104125
- name: Scan for secrets
105126
id: scan
106127
env:
128+
EVENT_NAME: ${{ github.event_name }}
107129
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
108130
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
109131
MERGE_GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }}
@@ -112,28 +134,24 @@ jobs:
112134
set +e
113135
echo "[]" > results.json
114136
115-
if [[ "${{ github.event_name }}" == "pull_request" ]] || [[ "${{ github.event_name }}" == "merge_group" ]]; then
116-
# PR / merge queue: scan only paths that differ from base..head (not the entire checkout)
117-
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
137+
# Extract non-comment, non-blank patterns for the shell pre-filter.
138+
grep -vE '^\s*#|^\s*$' /tmp/trufflehog-exclude.txt > /tmp/exclude-regexes.txt 2>/dev/null || true
139+
140+
if [[ "${EVENT_NAME}" == "pull_request" ]] || [[ "${EVENT_NAME}" == "merge_group" ]]; then
141+
if [[ "${EVENT_NAME}" == "pull_request" ]]; then
118142
echo "Scanning changed files in PR..."
119-
git diff --name-only "$PR_BASE_SHA" "$PR_HEAD_SHA" > changed-files.txt
143+
git diff --name-only "${PR_BASE_SHA}" "${PR_HEAD_SHA}" > changed-files.txt
120144
else
121145
echo "Scanning changed files in merge group..."
122-
git diff --name-only "$MERGE_GROUP_BASE_SHA" "$MERGE_GROUP_HEAD_SHA" > changed-files.txt
146+
git diff --name-only "${MERGE_GROUP_BASE_SHA}" "${MERGE_GROUP_HEAD_SHA}" > changed-files.txt
123147
fi
124148
125149
if [[ -s changed-files.txt ]]; then
126150
while IFS= read -r file; do
127-
# Get just the filename
128-
filename=$(basename "$file")
129-
130-
# Skip excluded files (use case statement for cleaner matching)
131-
case "$filename" in
132-
go.sum|go.mod|package.json|package-lock.json|pnpm-lock.yaml|yarn.lock|poetry.lock|Pipfile.lock|uv.lock|Cargo.lock|Gemfile.lock|grafana.json)
133-
echo "Skipping: ${file} (excluded manifest/lock file)"
134-
continue
135-
;;
136-
esac
151+
if [[ -s /tmp/exclude-regexes.txt ]] && echo "$file" | grep -qEf /tmp/exclude-regexes.txt 2>/dev/null; then
152+
echo "Skipping: ${file} (matches exclude pattern)"
153+
continue
154+
fi
137155
138156
if [[ -f "${file}" ]]; then
139157
echo "Scanning: ${file}"
@@ -144,7 +162,6 @@ jobs:
144162
echo "No files changed"
145163
fi
146164
else
147-
# push to main (and any other events): full filesystem scan
148165
echo "Scanning current filesystem..."
149166
trufflehog filesystem . --exclude-paths /tmp/trufflehog-exclude.txt --concurrency 16 --json --no-update --results=verified,unverified > results.ndjson || true
150167
fi
@@ -161,8 +178,12 @@ jobs:
161178
# Filter out CHANGELOG git hashes if we have results
162179
if jq -e 'length > 0' results.json >/dev/null 2>&1; then
163180
jq '[.[] | select(
164-
((.SourceMetadata?.Data?.Filesystem?.file // .SourceMetadata?.Data?.Git?.file) // "") as $file |
165-
!(($file | test("CHANGELOG|HISTORY\\.md|NEWS\\.md"; "i")) and (.Raw | test("^[0-9a-f]{7,40}$"; "i")))
181+
(
182+
(try .SourceMetadata.Data.Filesystem.file catch null) //
183+
(try .SourceMetadata.Data.Git.file catch null) //
184+
""
185+
) as $file |
186+
((($file | test("CHANGELOG|HISTORY\\.md|NEWS\\.md"; "i")) and (.Raw | test("^[0-9a-f]{7,40}$"; "i"))) | not)
166187
)]' results.json > results.json.tmp && mv results.json.tmp results.json
167188
fi
168189
else
@@ -196,26 +217,29 @@ jobs:
196217
197218
- name: Delete resolved comment
198219
if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && steps.scan.outputs.total == '0' }}
220+
env:
221+
GH_TOKEN: ${{ github.token }}
222+
GH_REPOSITORY: ${{ github.repository }}
223+
PR_NUMBER: ${{ github.event.pull_request.number }}
199224
run: |
200225
# Find and delete TruffleHog comment if secrets have been resolved
201-
COMMENT_ID=$(gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments --jq '.[] | select(.body | contains("<!-- trufflehog-secret-scan-comment -->")) | .id' | head -1 || echo "")
226+
COMMENT_ID=$(gh api "repos/${GH_REPOSITORY}/issues/${PR_NUMBER}/comments" --jq '.[] | select(.body | contains("<!-- trufflehog-secret-scan-comment -->")) | .id' | head -1 || echo "")
202227
203228
if [[ -n "$COMMENT_ID" ]]; then
204229
echo "Secrets resolved - deleting previous warning comment (ID: ${COMMENT_ID})"
205-
gh api -X DELETE /repos/${{ github.repository }}/issues/comments/${COMMENT_ID}
230+
gh api -X DELETE "/repos/${GH_REPOSITORY}/issues/comments/${COMMENT_ID}"
206231
echo "Comment deleted successfully"
207232
else
208233
echo "No existing TruffleHog comment to delete"
209234
fi
210-
env:
211-
GH_TOKEN: ${{ github.token }}
212235
213236
- name: Generate PR comment
214237
if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && steps.scan.outputs.total > 0 }}
215238
id: comment-body
239+
env:
240+
VERIFIED: ${{ steps.scan.outputs.verified }}
241+
UNVERIFIED: ${{ steps.scan.outputs.unverified }}
216242
run: |
217-
VERIFIED=${{ steps.scan.outputs.verified }}
218-
UNVERIFIED=${{ steps.scan.outputs.unverified }}
219243
TOTAL=$((VERIFIED+UNVERIFIED))
220244
221245
if [[ $TOTAL -eq 0 ]]; then
@@ -232,9 +256,9 @@ jobs:
232256
"- " +
233257
(if .Verified then "**VERIFIED SECRET**" else "**Possible secret**" end) +
234258
" (" + .DetectorName + ") at `" +
235-
((.SourceMetadata?.Data?.Filesystem?.file // .SourceMetadata?.Data?.Git?.file) // "unknown") +
259+
(((try .SourceMetadata.Data.Filesystem.file catch null) // (try .SourceMetadata.Data.Git.file catch null)) // "unknown") +
236260
":" +
237-
((.SourceMetadata?.Data?.Filesystem?.line // .SourceMetadata?.Data?.Git?.line) | tostring) +
261+
(((try .SourceMetadata.Data.Filesystem.line catch null) // (try .SourceMetadata.Data.Git.line catch null)) | tostring) +
238262
"` → `" +
239263
(if (.Raw | length) > 8 then (.Raw[:4] + "***" + .Raw[-4:]) else "***" end) +
240264
"`"' results.json 2>/dev/null || echo "- Error processing results")
@@ -281,23 +305,28 @@ jobs:
281305
- name: Create scan report
282306
env:
283307
GITHUB_REF_NAME: ${{ github.ref_name }}
308+
GH_REPOSITORY: ${{ github.repository }}
309+
GH_SHA: ${{ github.sha }}
310+
TOTAL_SECRETS: ${{ steps.scan.outputs.total }}
311+
VERIFIED_SECRETS: ${{ steps.scan.outputs.verified }}
312+
UNVERIFIED_SECRETS: ${{ steps.scan.outputs.unverified }}
284313
run: |
285314
{
286315
echo "TruffleHog Scan Report"
287316
echo "====================="
288317
echo "Date: $(date)"
289-
echo "Repository: ${{ github.repository }}"
318+
echo "Repository: ${GH_REPOSITORY}"
290319
echo "Branch: ${GITHUB_REF_NAME}"
291-
echo "Commit: ${{ github.sha }}"
320+
echo "Commit: ${GH_SHA}"
292321
echo ""
293322
echo "Summary:"
294-
echo "- Total secrets: ${{ steps.scan.outputs.total }}"
295-
echo "- Verified: ${{ steps.scan.outputs.verified }}"
296-
echo "- Unverified: ${{ steps.scan.outputs.unverified }}"
323+
echo "- Total secrets: ${TOTAL_SECRETS}"
324+
echo "- Verified: ${VERIFIED_SECRETS}"
325+
echo "- Unverified: ${UNVERIFIED_SECRETS}"
297326
echo ""
298327
echo "Detailed Results:"
299328
if [[ -f "results.json" && -s "results.json" ]] && jq empty results.json 2>/dev/null; then
300-
jq -r '.[] | "- " + (if .Verified then "VERIFIED" else "Unverified" end) + " " + .DetectorName + " at " + ((.SourceMetadata?.Data?.Filesystem?.file // .SourceMetadata?.Data?.Git?.file) // "unknown") + ":" + ((.SourceMetadata?.Data?.Filesystem?.line // .SourceMetadata?.Data?.Git?.line) | tostring) + " → " + (if (.Raw | length) > 8 then (.Raw[:4] + "***" + .Raw[-4:]) else "***" end)' results.json 2>/dev/null || echo "Error processing results"
329+
jq -r '.[] | "- " + (if .Verified then "VERIFIED" else "Unverified" end) + " " + .DetectorName + " at " + (((try .SourceMetadata.Data.Filesystem.file catch null) // (try .SourceMetadata.Data.Git.file catch null)) // "unknown") + ":" + (((try .SourceMetadata.Data.Filesystem.line catch null) // (try .SourceMetadata.Data.Git.line catch null)) | tostring) + " → " + (if (.Raw | length) > 8 then (.Raw[:4] + "***" + .Raw[-4:]) else "***" end)' results.json 2>/dev/null || echo "Error processing results"
301330
else
302331
echo "No secrets detected"
303332
fi

trufflehog/exclude-paths.txt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# TruffleHog path exclusions — one Go regex per line.
2+
# Edit this file and merge to main. CI fetches it at runtime and passes
3+
# it directly to `trufflehog --exclude-paths`.
4+
#
5+
# Syntax: Go regexp (https://pkg.go.dev/regexp/syntax).
6+
# A file is excluded when ANY pattern matches its path.
7+
# Lines starting with # and blank lines are ignored.
8+
9+
# Go vendor directory (third-party code, not repo secrets)
10+
vendor/
11+
12+
# Lock files and checksums (contain hashes, not secrets)
13+
go\.sum$
14+
go\.mod$
15+
16+
# Dependency manifests (contain URLs / hashes that trigger false positives)
17+
package\.json$
18+
package-lock\.json$
19+
pnpm-lock\.yaml$
20+
yarn\.lock$
21+
poetry\.lock$
22+
Pipfile\.lock$
23+
uv\.lock$
24+
Cargo\.lock$
25+
Gemfile\.lock$
26+
27+
# Grafana plugin metadata
28+
grafana\.json$
29+
30+
# Grafana dashboards (user-supplied site content, full of base64/hashes)
31+
content/grafana/dashboards

0 commit comments

Comments
 (0)