Skip to content

Commit 2f7f242

Browse files
authored
ci: set CI=true in test workflow (#217)
* ci: explicitly set CI=true in test workflow * fix: render findings detail in PR comment, add Claude AI review support * fix: pipe only stdout (JSON) to scan file, let stderr display separately * fix: extract JSON from vu1nz output (progress lines precede JSON on stdout) * fix: strip ANSI escape codes from vu1nz JSON output before parsing * fix: disable color output (NO_COLOR/TERM=dumb), use python to extract JSON * fix: use json.loads(strict=False) to handle unescaped newlines in vu1nz output * fix: use raw_decode to extract JSON object, ignore trailing output * fix: use review-pr to scan PR code for vulns, not actions scan * feat: use review-pr to scan PR code for vulns with Claude AI * fix: extract ANTHROPIC_API_KEY from ENV_FILE secret * debug: list ENV_FILE key names to find ANTHROPIC_API_KEY * ci: retrigger vu1nz scan after ENV_FILE update
1 parent a6aa525 commit 2f7f242

2 files changed

Lines changed: 112 additions & 34 deletions

File tree

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,5 @@ jobs:
2323
- run: pnpm install --frozen-lockfile
2424

2525
- run: pnpm test
26+
env:
27+
CI: true

.github/workflows/vu1nz-scan.yml

Lines changed: 110 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,14 @@ name: vu1nz security scan
22

33
on:
44
pull_request:
5-
push:
6-
branches: [master]
75

86
permissions:
97
contents: read
10-
actions: read
118
pull-requests: write
129

1310
jobs:
14-
scan:
15-
name: Scan CI/CD for vulnerabilities
11+
review:
12+
name: Review PR for security vulnerabilities
1613
runs-on: ubuntu-latest
1714

1815
steps:
@@ -25,69 +22,148 @@ jobs:
2522
- name: Install vu1nz
2623
run: pip install --quiet git+https://github.com/profullstack/vu1nz-gh-actions.git
2724

28-
- name: Scan workflows
29-
id: scan
25+
- name: Load env file
26+
env:
27+
ENV_FILE: ${{ secrets.ENV_FILE }}
28+
run: |
29+
echo "$ENV_FILE" > "$RUNNER_TEMP/.env"
30+
# Debug: show available key names only (no values)
31+
echo "Keys in ENV_FILE:"
32+
grep -oP '^[A-Z_]+(?==)' "$RUNNER_TEMP/.env" || echo "(no keys found or different format)"
33+
# Export ANTHROPIC_API_KEY from the env file
34+
ANTHROPIC_API_KEY=$(grep -E '^ANTHROPIC_API_KEY=' "$RUNNER_TEMP/.env" | head -1 | sed 's/^ANTHROPIC_API_KEY=//')
35+
if [ -n "$ANTHROPIC_API_KEY" ]; then
36+
echo "::add-mask::$ANTHROPIC_API_KEY"
37+
echo "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" >> "$GITHUB_ENV"
38+
echo "ANTHROPIC_API_KEY found and exported"
39+
else
40+
echo "::warning::ANTHROPIC_API_KEY not found in ENV_FILE"
41+
fi
42+
43+
- name: Review PR
44+
id: review
3045
env:
3146
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47+
NO_COLOR: "1"
48+
TERM: dumb
3249
run: |
33-
vu1nz actions scan ${{ github.repository }} \
50+
vu1nz review-pr main \
51+
${{ github.repository }} \
52+
${{ github.event.pull_request.number }} \
3453
--token "$GITHUB_TOKEN" \
3554
--json \
36-
2>&1 | tee "$RUNNER_TEMP/vu1nz-scan.json"
37-
38-
- name: Evaluate findings
39-
id: eval
55+
| tee "$RUNNER_TEMP/vu1nz-review-raw.txt" || true
56+
57+
# Extract JSON from output (vu1nz prints progress lines before JSON)
58+
python3 -c "
59+
import json, re, sys
60+
raw = open('$RUNNER_TEMP/vu1nz-review-raw.txt').read()
61+
raw = re.sub(r'\x1b\[[0-9;]*m', '', raw)
62+
start = raw.find('{')
63+
if start >= 0:
64+
obj, _ = json.JSONDecoder(strict=False).raw_decode(raw, start)
65+
json.dump(obj, sys.stdout)
66+
else:
67+
print('{}')
68+
" > "$RUNNER_TEMP/vu1nz-review.json"
69+
70+
- name: Build PR comment
71+
id: comment
4072
run: |
4173
python3 << 'PYEOF'
4274
import json, os, sys
4375
44-
scan_file = os.environ.get("RUNNER_TEMP", "") + "/vu1nz-scan.json"
76+
review_file = os.environ.get("RUNNER_TEMP", "") + "/vu1nz-review.json"
77+
comment_file = os.environ.get("RUNNER_TEMP", "") + "/vu1nz-comment.md"
78+
4579
try:
46-
with open(scan_file) as f:
47-
data = json.load(f)
80+
with open(review_file) as f:
81+
data = json.loads(f.read(), strict=False)
4882
except Exception as e:
49-
print(f"::warning::Could not parse scan results: {e}")
83+
print(f"::warning::Could not parse review results: {e}")
84+
with open(comment_file, "w") as f:
85+
f.write("## vu1nz Security Review\n\nCould not parse review results.\n")
5086
sys.exit(0)
5187
5288
findings = data.get("findings", [])
53-
counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
54-
for f in findings:
55-
sev = f.get("severity", "").lower()
89+
analysis = data.get("analysis", "")
90+
pr = data.get("pr_number", "?")
91+
total = len(findings)
92+
93+
counts = {"critical": 0, "high": 0, "medium": 0, "low": 0}
94+
for finding in findings:
95+
sev = finding.get("severity", "").lower()
5696
if sev in counts:
5797
counts[sev] += 1
5898
59-
total = len(findings)
6099
has_hc = counts["critical"] > 0 or counts["high"] > 0
61100
62-
parts = [f"**{total} findings**"]
101+
lines = ["## vu1nz Security Review", ""]
102+
lines.append(f"**{total}** finding(s) in PR #{pr}")
103+
lines.append("")
104+
105+
badge_parts = []
63106
for sev in ("critical", "high", "medium", "low"):
64107
if counts[sev] > 0:
65-
parts.append(f"{sev}: {counts[sev]}")
66-
summary = " | ".join(parts)
108+
badge_parts.append(f"**{sev.upper()}**: {counts[sev]}")
109+
if badge_parts:
110+
lines.append(" | ".join(badge_parts))
111+
lines.append("")
112+
113+
if has_hc:
114+
lines.append("> **High or critical findings — review before merging.**")
115+
lines.append("")
116+
117+
if findings:
118+
lines.append("### Findings")
119+
lines.append("")
120+
lines.append("| Severity | File | Issue | Suggestion |")
121+
lines.append("|----------|------|-------|------------|")
122+
for f in findings:
123+
sev = f.get("severity", "?").upper()
124+
file = f.get("file", "N/A")
125+
issue = f.get("issue", "").replace("\n", " ")[:150]
126+
suggestion = f.get("suggestion", "").replace("\n", " ")[:150]
127+
lines.append(f"| {sev} | `{file}` | {issue} | {suggestion} |")
128+
lines.append("")
129+
else:
130+
lines.append("No security issues found.")
131+
lines.append("")
132+
133+
if analysis:
134+
lines.append("<details><summary>Full AI Analysis</summary>")
135+
lines.append("")
136+
lines.append(analysis)
137+
lines.append("")
138+
lines.append("</details>")
139+
140+
body = "\n".join(lines)
141+
with open(comment_file, "w") as f:
142+
f.write(body)
67143
68144
with open(os.environ.get("GITHUB_OUTPUT", ""), "a") as out:
69145
out.write(f"total={total}\n")
70146
out.write(f"has_high_critical={'true' if has_hc else 'false'}\n")
71-
out.write(f"summary={summary}\n")
72147
73148
if has_hc:
74-
print(f"::error::vu1nz found high/critical CI/CD vulnerabilities")
149+
print(f"::error::vu1nz found high/critical vulnerabilities in PR code")
75150
sys.exit(1)
76151
77-
print(f"::notice::{summary}")
152+
print(f"::notice::vu1nz review: {total} finding(s), no high/critical issues")
78153
PYEOF
79154
80155
- name: Comment on PR
81-
if: github.event_name == 'pull_request' && always()
156+
if: always()
82157
uses: actions/github-script@v7
83158
with:
84159
script: |
85-
const summary = `${{ steps.eval.outputs.summary || 'Scan completed.' }}`;
86-
const hasHC = '${{ steps.eval.outputs.has_high_critical }}' === 'true';
87-
88-
let body = `## vu1nz CI/CD Security Scan\n\n${summary}\n\n`;
89-
if (hasHC) {
90-
body += '**High or critical findings detected — review before merging.**\n\n';
160+
const fs = require('fs');
161+
const commentFile = `${process.env.RUNNER_TEMP}/vu1nz-comment.md`;
162+
let body;
163+
try {
164+
body = fs.readFileSync(commentFile, 'utf8');
165+
} catch {
166+
body = '## vu1nz Security Review\n\nScan completed but could not read results.';
91167
}
92168
93169
const { data: comments } = await github.rest.issues.listComments({
@@ -97,7 +173,7 @@ jobs:
97173
});
98174
99175
const existing = comments.find(c =>
100-
c.user.type === 'Bot' && c.body.includes('vu1nz CI/CD Security Scan')
176+
c.user.type === 'Bot' && c.body.includes('vu1nz Security Review')
101177
);
102178
103179
if (existing) {

0 commit comments

Comments
 (0)