Skip to content

Commit 082714a

Browse files
lomduhaituBhorVaishnavInertFluid
authored
chore: implement scanner rollout (#11)
* chore: implement scanner rollout * fix(dependabot): add npm block to cover package.json * ci: inline security scanners and add Format and Test Check Public repos can't call reusable workflows in the internal guard0-ai/.github repo (they fail at startup with zero jobs), so inline the dependency-review and nightly TruffleHog jobs directly instead of via reusable-workflow calls. Add format-and-test.yml emitting the org-required "Format and Test Check" context (npm ci, lint, type-check, vitest), and an ESLint config so next lint runs non-interactively. * ci: run Format and Test Check on Node 20 for vitest 4 compatibility --------- Co-authored-by: BhorVaishnav <115548641+BhorVaishnav@users.noreply.github.com> Co-authored-by: Joel Estibeiro <joel@guard0.ai>
1 parent 9e02479 commit 082714a

6 files changed

Lines changed: 349 additions & 0 deletions

File tree

.eslintrc.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "next/core-web-vitals"
3+
}

.github/dependabot.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
version: 2
2+
3+
updates:
4+
# Always — pin GitHub Actions versions
5+
- package-ecosystem: "github-actions"
6+
directory: "/"
7+
schedule:
8+
interval: "weekly"
9+
day: "monday"
10+
open-pull-requests-limit: 5
11+
labels:
12+
- "ci/cd"
13+
14+
- package-ecosystem: "npm"
15+
directory: "/"
16+
schedule:
17+
interval: "weekly"
18+
day: "monday"
19+
open-pull-requests-limit: 10
20+
labels:
21+
- "dependencies"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Dependency Review
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
7+
permissions:
8+
contents: read
9+
pull-requests: write
10+
11+
jobs:
12+
dependency-review:
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: read
16+
pull-requests: write # required for the PR comment
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
21+
- name: Dependency Review
22+
# Findings are detected and summarised, but the check is allowed to pass
23+
# during the org-wide rollout; the PR comment still posts on failure.
24+
continue-on-error: true
25+
uses: actions/dependency-review-action@v4
26+
with:
27+
fail-on-severity: high
28+
comment-summary-in-pr: on-failure
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Format and Test
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
push:
7+
branches: [main]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
format-and-test:
14+
name: Format and Test Check
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Checkout code
18+
uses: actions/checkout@v4
19+
20+
- name: Setup Node.js
21+
uses: actions/setup-node@v4
22+
with:
23+
# vitest 4 (rolldown) requires Node's node:util styleText export,
24+
# added in Node 20; the repo's older Node 18 CI pin can't run it.
25+
node-version: '20'
26+
cache: npm
27+
28+
- name: Install dependencies
29+
run: npm ci
30+
31+
- name: Lint
32+
run: npm run lint
33+
34+
- name: Type check
35+
run: npm run type-check
36+
37+
- name: Test
38+
run: npm test -- --run --passWithNoTests
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
name: Nightly Secret Scan
2+
3+
on:
4+
schedule:
5+
- cron: '0 2 * * *'
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
trufflehog:
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: read
16+
env:
17+
# SLACK_WEBHOOK_URL also lifted to env so we can gate steps via `if:`
18+
# (secrets.* is forbidden in `if:`). Still passed as `with:` to the
19+
# Slack action — env alone doesn't populate its inputs.
20+
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
21+
# Cap on per-finding rows in annotations / Step Summary / log table.
22+
MAX_FINDING_ROWS: '100'
23+
24+
steps:
25+
- name: Checkout full history
26+
uses: actions/checkout@v4
27+
with:
28+
fetch-depth: 0
29+
30+
# install.sh SHA-pinned (bootstrap reproducible); binary floats to
31+
# latest release (fresh detectors). install.sh sha256-verifies the
32+
# tarball before installing, so the float doesn't lose integrity.
33+
- name: Install TruffleHog binary (latest)
34+
env:
35+
INSTALL_SH_SHA: f37f2eff68c31620fd88bc37ff2b7d57ea0c700c
36+
run: |
37+
curl -sSfL "https://raw.githubusercontent.com/trufflesecurity/trufflehog/${INSTALL_SH_SHA}/scripts/install.sh" \
38+
| sh -s -- -b "$RUNNER_TEMP/bin"
39+
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
40+
41+
# Runs the binary once, parses JSON, emits all four triage surfaces.
42+
# Never reads .Raw / .RawV2 — commit+file:line is enough for triage.
43+
- name: Scan and emit triage surfaces
44+
id: findings
45+
run: |
46+
trufflehog git file://. --only-verified --no-update --json > findings.jsonl || true
47+
if [ ! -s findings.jsonl ]; then
48+
{
49+
echo "has_findings=false"
50+
echo "has_verified=false"
51+
echo "extraction_failed=false"
52+
} >> "$GITHUB_OUTPUT"
53+
echo "No findings."
54+
exit 0
55+
fi
56+
57+
python3 <<'PYEOF'
58+
import json, os, sys, re
59+
from collections import Counter
60+
from urllib.parse import quote
61+
62+
REPO = os.environ['GITHUB_REPOSITORY']
63+
RUN_ID = os.environ['GITHUB_RUN_ID']
64+
RUNNER_TEMP = os.environ['RUNNER_TEMP']
65+
MAX_ROWS = int(os.environ.get('MAX_FINDING_ROWS', '100'))
66+
SLACK_PAYLOAD = os.path.join(RUNNER_TEMP, 'slack-payload.json')
67+
68+
def parse(line):
69+
try:
70+
obj = json.loads(line)
71+
except json.JSONDecodeError:
72+
return None
73+
g = obj.get('SourceMetadata', {}).get('Data', {}).get('Git')
74+
if not g:
75+
return None
76+
# TruffleHog's git source uses 'email' for the "Name <email>" line.
77+
return {
78+
'detector': obj.get('DetectorName', 'unknown'),
79+
'verified': bool(obj.get('Verified', False)),
80+
'commit': g.get('commit', '') or '',
81+
'file': g.get('file', '?'),
82+
'line': g.get('line', 0) or 0,
83+
'timestamp': g.get('timestamp', '') or '',
84+
'committer': g.get('email', '') or '',
85+
}
86+
87+
findings = []
88+
with open('findings.jsonl') as fp:
89+
for line in fp:
90+
line = line.strip()
91+
if not line:
92+
continue
93+
f = parse(line)
94+
if f:
95+
findings.append(f)
96+
97+
total = len(findings)
98+
verified_count = sum(1 for f in findings if f['verified'])
99+
100+
def short_committer(s):
101+
return re.sub(r' <.*$', '', s).strip() or 'unknown'
102+
103+
def emit_outputs(has_findings, has_verified, extraction_failed, **kw):
104+
with open(os.environ['GITHUB_OUTPUT'], 'a') as fp:
105+
fp.write(f"has_findings={'true' if has_findings else 'false'}\n")
106+
fp.write(f"has_verified={'true' if has_verified else 'false'}\n")
107+
fp.write(f"extraction_failed={'true' if extraction_failed else 'false'}\n")
108+
for k, v in kw.items():
109+
fp.write(f"{k}={v}\n")
110+
111+
# Fallback: trufflehog wrote output but no parseable git-source rows.
112+
if total == 0:
113+
print("::warning::TruffleHog produced output but no parseable git-source findings.")
114+
with open(os.environ['GITHUB_STEP_SUMMARY'], 'a') as fp:
115+
fp.write("## ⚠ TruffleHog — output present, but no parseable git-source findings\n\n")
116+
fp.write("Re-run TruffleHog locally with `--json` for full triage data.\n")
117+
slack_payload = {
118+
"text": f"⚠ TruffleHog extraction failed in {REPO}",
119+
"blocks": [
120+
{"type": "header", "text": {"type": "plain_text", "text": "⚠ TruffleHog: extraction failed", "emoji": True}},
121+
{"type": "section", "text": {"type": "mrkdwn", "text": f"TruffleHog produced output in `{REPO}` but no parseable git-source rows. Investigate the workflow run."}},
122+
{"type": "actions", "elements": [{"type": "button", "text": {"type": "plain_text", "text": "View Workflow Run"}, "url": f"https://github.com/{REPO}/actions/runs/{RUN_ID}", "style": "danger"}]},
123+
],
124+
}
125+
with open(SLACK_PAYLOAD, 'w') as fp:
126+
json.dump(slack_payload, fp)
127+
emit_outputs(has_findings=False, has_verified=False, extraction_failed=True,
128+
total=0, unique_commits=0, verified_count=0)
129+
sys.exit(0)
130+
131+
# Verified rows first, then by timestamp. Keeps incidents on top in
132+
# ad-hoc audit mode where unverified pattern matches are also present.
133+
findings.sort(key=lambda x: (not x['verified'], x['timestamp']))
134+
unique_commits = len({f['commit'] for f in findings if f['commit']})
135+
detector_counts = Counter(f['detector'] for f in findings).most_common()
136+
137+
unverified_count = total - verified_count
138+
summary_line = f"{verified_count} verified" + (f" / {unverified_count} unverified" if unverified_count else "")
139+
140+
# Surface 1: structured table → run log (stdout)
141+
print(f"=== TruffleHog findings — {summary_line}, across {unique_commits} unique commits ===")
142+
print()
143+
print(f"{'#':<4} {'Ver':<4} {'Detector':<15} {'Commit':<10} {'File:line':<60} {'Committed-at':<18} {'Committer'}")
144+
print('-' * 134)
145+
for i, f in enumerate(findings[:MAX_ROWS], 1):
146+
loc = f"{f['file']}:{f['line']}"
147+
v = '✓' if f['verified'] else '✗'
148+
print(f"{i:<4} {v:<4} {f['detector']:<15} {f['commit'][:8]:<10} {loc[:60]:<60} {f['timestamp'][:16]:<18} {short_committer(f['committer'])}")
149+
if total > MAX_ROWS:
150+
print(f"... + {total - MAX_ROWS} more (capped at {MAX_ROWS}; re-run TruffleHog locally for the full list)")
151+
print()
152+
print("=== By detector ===")
153+
for det, cnt in detector_counts:
154+
print(f" {det}: {cnt}")
155+
print()
156+
print("=== Unique commits to inspect ===")
157+
seen = set()
158+
for f in findings:
159+
c = f['commit']
160+
if c and c not in seen:
161+
seen.add(c)
162+
print(f" https://github.com/{REPO}/commit/{c}")
163+
164+
# Surface 2: ::warning:: workflow commands → annotations panel.
165+
# Only verified findings get annotations (signal vs noise in the panel).
166+
# Ad-hoc auditors get unverified rows via the Step Summary / log table.
167+
def wc_esc(s):
168+
return (str(s)
169+
.replace('%', '%25')
170+
.replace('\r', '%0D')
171+
.replace('\n', '%0A')
172+
.replace(':', '%3A')
173+
.replace(',', '%2C'))
174+
verified_findings = [f for f in findings if f['verified']]
175+
for f in verified_findings[:MAX_ROWS]:
176+
title = f"{f['detector']} @ {f['commit'][:8]}"
177+
msg = f"Committed {f['timestamp'][:10] or 'unknown'} by {short_committer(f['committer'])}"
178+
print(f"::warning file={wc_esc(f['file'])},line={f['line']},title={wc_esc(title)}::{wc_esc(msg)}")
179+
180+
# Surface 3: Step Summary (markdown) → top of run page
181+
def cell(s):
182+
return str(s).replace('|', '\\|')
183+
with open(os.environ['GITHUB_STEP_SUMMARY'], 'a') as fp:
184+
if verified_count:
185+
fp.write(f"## 🚨 TruffleHog — {verified_count} verified live secret{'s' if verified_count != 1 else ''}\n\n")
186+
else:
187+
fp.write(f"## TruffleHog — {total} unverified pattern matches\n\n")
188+
fp.write(f"**{summary_line}** · **{unique_commits}** unique commits affected\n\n")
189+
fp.write("### By detector\n\n")
190+
for det, cnt in detector_counts:
191+
fp.write(f"- `{cell(det)}` × {cnt}\n")
192+
fp.write("\n### Findings\n\n")
193+
fp.write("| # | Verified | Detector | Commit | File:line | Committed-at | Committer |\n")
194+
fp.write("|---|---|---|---|---|---|---|\n")
195+
for i, f in enumerate(findings[:MAX_ROWS], 1):
196+
c = f['commit']
197+
file_path = f['file']
198+
loc = f"{file_path}:{f['line']}"
199+
commit_url = f"https://github.com/{REPO}/commit/{c}"
200+
file_url = f"https://github.com/{REPO}/blob/{c}/{quote(file_path, safe='/')}#L{f['line']}"
201+
v = '✓' if f['verified'] else '✗'
202+
fp.write(f"| {i} | {v} | `{cell(f['detector'])}` | [{c[:8]}]({commit_url}) | [{cell(loc)}]({file_url}) | {cell(f['timestamp'][:16])} | {cell(short_committer(f['committer']))} |\n")
203+
if total > MAX_ROWS:
204+
fp.write(f"\n_{total - MAX_ROWS} more findings truncated (cap {MAX_ROWS}). Re-run TruffleHog locally with `--json` for the full list._\n")
205+
206+
# Surface 4: Slack payload — only built when there's something to page
207+
# on (verified finding). Unverified-only audit runs surface data via
208+
# Step Summary + log table; no Slack noise.
209+
if verified_count:
210+
breakdown_lines = "\n".join(f"• `{det}` × {cnt}" for det, cnt in detector_counts)
211+
fields = [
212+
{"type": "mrkdwn", "text": f"*Repository:*\n{REPO}"},
213+
{"type": "mrkdwn", "text": f"*Verified:*\n{verified_count}"},
214+
{"type": "mrkdwn", "text": f"*Unique commits:*\n{unique_commits}"},
215+
]
216+
if unverified_count:
217+
fields.append({"type": "mrkdwn", "text": f"*Unverified (audit):*\n{unverified_count}"})
218+
slack_payload = {
219+
"text": f"🚨 {verified_count} verified live secrets in {REPO}",
220+
"blocks": [
221+
{"type": "header", "text": {"type": "plain_text", "text": "🚨 Verified live secrets in git history", "emoji": True}},
222+
{"type": "section", "fields": fields},
223+
{"type": "section", "text": {"type": "mrkdwn", "text": f"*By detector*\n{breakdown_lines}"}},
224+
{"type": "section", "text": {"type": "mrkdwn", "text": "*What this means:* TruffleHog authenticated each candidate against the provider's live API. Findings are real credentials. Full triage details on the workflow run page — Step Summary, annotations, *Scan* step log."}},
225+
{"type": "actions", "elements": [{"type": "button", "text": {"type": "plain_text", "text": "View Workflow Run"}, "url": f"https://github.com/{REPO}/actions/runs/{RUN_ID}", "style": "danger"}]},
226+
],
227+
}
228+
with open(SLACK_PAYLOAD, 'w') as fp:
229+
json.dump(slack_payload, fp)
230+
231+
emit_outputs(has_findings=True,
232+
has_verified=verified_count > 0,
233+
extraction_failed=False,
234+
total=total,
235+
unique_commits=unique_commits,
236+
verified_count=verified_count)
237+
PYEOF
238+
239+
# Page on either a real incident (verified finding) or a tool failure
240+
# (TruffleHog produced output we couldn't parse). Both warrant attention;
241+
# the Slack payload distinguishes them.
242+
- name: Notify Slack
243+
if: ${{ (steps.findings.outputs.has_verified == 'true' || steps.findings.outputs.extraction_failed == 'true') && env.SLACK_WEBHOOK_URL != '' }}
244+
uses: slackapi/slack-github-action@v3.0.1
245+
with:
246+
webhook: ${{ env.SLACK_WEBHOOK_URL }}
247+
webhook-type: incoming-webhook
248+
payload-file-path: ${{ runner.temp }}/slack-payload.json
249+
250+
- name: Fail job
251+
if: ${{ steps.findings.outputs.has_verified == 'true' || steps.findings.outputs.extraction_failed == 'true' }}
252+
run: |
253+
echo "::error::TruffleHog: verified findings or extraction failure. See Step Summary, annotations, and the 'Scan' step's log."
254+
exit 1

.pre-commit-config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
repos:
2+
- repo: https://github.com/gitleaks/gitleaks
3+
rev: v8.30.1
4+
hooks:
5+
- id: gitleaks

0 commit comments

Comments
 (0)