Skip to content

Commit bfdeb21

Browse files
feat(trufflehog): add org-wide path exclusions via exclude-paths.txt
Replace the inline hardcoded exclusion list with a centralized `trufflehog/exclude-paths.txt` file containing Go regexes that are passed directly to `trufflehog --exclude-paths`. Adding a new exclusion is now: add a regex line, merge to main. Changes: - Add trufflehog/exclude-paths.txt with patterns for vendor/, lock files, dependency manifests, grafana.json, and dashboard content - Add fetch step that loads the exclude file from security-github-actions at runtime (GitHub API → raw fallback → workflow ref fallback) - Simplify PR scan loop: pre-filter changed files with grep against exclude patterns instead of per-file case statements - Fix jq CHANGELOG filter to use try/catch syntax - Clean up org-required-trufflehog.yml comments Made-with: Cursor
1 parent bd0a4d1 commit bfdeb21

File tree

3 files changed

+86
-44
lines changed

3 files changed

+86
-44
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ jobs:
2323
name: TruffleHog Secret Scan
2424
uses: grafana/security-github-actions/.github/workflows/reusable-trufflehog.yml@main
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: 52 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,42 @@ jobs:
5050
- name: Remove persisted credentials
5151
run: git config --unset-all http.https://github.com/.extraheader
5252

53+
- name: Fetch org-wide TruffleHog exclude patterns
54+
env:
55+
GH_TOKEN: ${{ github.token }}
56+
run: |
57+
DEST=/tmp/trufflehog-exclude.txt
58+
REPO=grafana/security-github-actions
59+
FILE_PATH=trufflehog/exclude-paths.txt
60+
61+
# Resolve the ref the calling workflow used (e.g. @main, @feature/branch, @v1).
62+
CALLER_REF="main"
63+
if [[ -n "${{ github.workflow_ref }}" ]]; then
64+
CALLER_REF=$(echo "${{ github.workflow_ref }}" | sed 's|.*@||; s|^refs/heads/||; s|^refs/tags/||')
65+
fi
66+
67+
LOADED=false
68+
for REF in main "${CALLER_REF}"; do
69+
[[ "$LOADED" == "true" ]] && break
70+
if gh api "repos/${REPO}/contents/${FILE_PATH}?ref=${REF}" \
71+
-H "Accept: application/vnd.github.v3.raw" -o "${DEST}" 2>/dev/null && [[ -s "${DEST}" ]]; then
72+
echo "Loaded exclude patterns from ${REPO}@${REF} (GitHub API)"
73+
LOADED=true
74+
elif curl -fsSL "https://raw.githubusercontent.com/${REPO}/${REF}/${FILE_PATH}" \
75+
-o "${DEST}" 2>/dev/null && [[ -s "${DEST}" ]]; then
76+
echo "Loaded exclude patterns from raw.githubusercontent.com@${REF}"
77+
LOADED=true
78+
fi
79+
done
80+
81+
if [[ "$LOADED" != "true" ]]; then
82+
echo "::warning::Could not fetch TruffleHog exclude patterns from ${REPO} (tried main and ${CALLER_REF}). Scanning without exclusions."
83+
touch "${DEST}"
84+
fi
85+
86+
echo "--- effective exclude patterns ---"
87+
cat "${DEST}"
88+
5389
- name: Install TruffleHog
5490
run: |
5591
# Download binary directly from GitHub releases for supply chain security
@@ -67,53 +103,25 @@ jobs:
67103
sudo chmod +x /usr/local/bin/trufflehog
68104
trufflehog --version
69105
70-
- name: Create global exclusions
71-
run: |
72-
# Create centralized exclusion patterns for common false positives
73-
cat > /tmp/trufflehog-exclude.txt <<'EOF'
74-
# Lock files and checksums (contain hashes, not secrets)
75-
path:go\.sum$
76-
path:go\.mod$
77-
# Dependency manifests (contain URLs that trigger false positives)
78-
path:package\.json$
79-
path:package-lock\.json$
80-
path:pnpm-lock\.yaml$
81-
path:yarn\.lock$
82-
path:poetry\.lock$
83-
path:Pipfile\.lock$
84-
path:uv\.lock$
85-
path:Cargo\.lock$
86-
path:Gemfile\.lock$
87-
# Grafana plugin metadata
88-
path:grafana\.json$
89-
EOF
90-
91-
echo "Created global exclusion patterns:"
92-
cat /tmp/trufflehog-exclude.txt
93-
94106
- name: Scan for secrets
95107
id: scan
96108
run: |
97109
set +e
98110
echo "[]" > results.json
99111
112+
# Extract non-comment, non-blank patterns for the shell pre-filter.
113+
grep -vE '^\s*#|^\s*$' /tmp/trufflehog-exclude.txt > /tmp/exclude-regexes.txt 2>/dev/null || true
114+
100115
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
101-
# PR: Scan only changed files (using two-dot diff with explicit base SHA)
102116
echo "Scanning changed files in PR..."
103117
git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} > changed-files.txt
104118
105119
if [[ -s changed-files.txt ]]; then
106120
while IFS= read -r file; do
107-
# Get just the filename
108-
filename=$(basename "$file")
109-
110-
# Skip excluded files (use case statement for cleaner matching)
111-
case "$filename" in
112-
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)
113-
echo "Skipping: ${file} (excluded manifest/lock file)"
114-
continue
115-
;;
116-
esac
121+
if [[ -s /tmp/exclude-regexes.txt ]] && echo "$file" | grep -qEf /tmp/exclude-regexes.txt 2>/dev/null; then
122+
echo "Skipping: ${file} (matches exclude pattern)"
123+
continue
124+
fi
117125
118126
if [[ -f "${file}" ]]; then
119127
echo "Scanning: ${file}"
@@ -124,7 +132,6 @@ jobs:
124132
echo "No files changed"
125133
fi
126134
else
127-
# Push to main: Scan current filesystem
128135
echo "Scanning current filesystem..."
129136
trufflehog filesystem . --exclude-paths /tmp/trufflehog-exclude.txt --concurrency 16 --json --no-update --results=verified,unverified > results.ndjson || true
130137
fi
@@ -141,8 +148,12 @@ jobs:
141148
# Filter out CHANGELOG git hashes if we have results
142149
if jq -e 'length > 0' results.json >/dev/null 2>&1; then
143150
jq '[.[] | select(
144-
((.SourceMetadata?.Data?.Filesystem?.file // .SourceMetadata?.Data?.Git?.file) // "") as $file |
145-
!(($file | test("CHANGELOG|HISTORY\\.md|NEWS\\.md"; "i")) and (.Raw | test("^[0-9a-f]{7,40}$"; "i")))
151+
(
152+
(try .SourceMetadata.Data.Filesystem.file catch null) //
153+
(try .SourceMetadata.Data.Git.file catch null) //
154+
""
155+
) as $file |
156+
((($file | test("CHANGELOG|HISTORY\\.md|NEWS\\.md"; "i")) and (.Raw | test("^[0-9a-f]{7,40}$"; "i"))) | not)
146157
)]' results.json > results.json.tmp && mv results.json.tmp results.json
147158
fi
148159
else
@@ -212,9 +223,9 @@ jobs:
212223
"- " +
213224
(if .Verified then "**VERIFIED SECRET**" else "**Possible secret**" end) +
214225
" (" + .DetectorName + ") at `" +
215-
((.SourceMetadata?.Data?.Filesystem?.file // .SourceMetadata?.Data?.Git?.file) // "unknown") +
226+
(((try .SourceMetadata.Data.Filesystem.file catch null) // (try .SourceMetadata.Data.Git.file catch null)) // "unknown") +
216227
":" +
217-
((.SourceMetadata?.Data?.Filesystem?.line // .SourceMetadata?.Data?.Git?.line) | tostring) +
228+
(((try .SourceMetadata.Data.Filesystem.line catch null) // (try .SourceMetadata.Data.Git.line catch null)) | tostring) +
218229
"` → `" +
219230
(if (.Raw | length) > 8 then (.Raw[:4] + "***" + .Raw[-4:]) else "***" end) +
220231
"`"' results.json 2>/dev/null || echo "- Error processing results")
@@ -277,7 +288,7 @@ jobs:
277288
echo ""
278289
echo "Detailed Results:"
279290
if [[ -f "results.json" && -s "results.json" ]] && jq empty results.json 2>/dev/null; then
280-
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"
291+
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"
281292
else
282293
echo "No secrets detected"
283294
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)