Skip to content

Commit 552e7fb

Browse files
committed
feat: use review-pr to scan PR code for vulns with Claude AI
1 parent 87a788c commit 552e7fb

1 file changed

Lines changed: 142 additions & 2 deletions

File tree

.github/workflows/vu1nz-scan.yml

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ jobs:
2323
run: pip install --quiet git+https://github.com/profullstack/vu1nz-gh-actions.git
2424

2525
- name: Review PR
26+
id: review
2627
env:
2728
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2829
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
@@ -32,5 +33,144 @@ jobs:
3233
vu1nz review-pr main \
3334
${{ github.repository }} \
3435
${{ github.event.pull_request.number }} \
35-
--ai auto \
36-
--token "$GITHUB_TOKEN"
36+
--token "$GITHUB_TOKEN" \
37+
--json \
38+
| tee "$RUNNER_TEMP/vu1nz-review-raw.txt" || true
39+
40+
# Extract JSON from output (vu1nz prints progress lines before JSON)
41+
python3 -c "
42+
import json, re, sys
43+
raw = open('$RUNNER_TEMP/vu1nz-review-raw.txt').read()
44+
raw = re.sub(r'\x1b\[[0-9;]*m', '', raw)
45+
start = raw.find('{')
46+
if start >= 0:
47+
obj, _ = json.JSONDecoder(strict=False).raw_decode(raw, start)
48+
json.dump(obj, sys.stdout)
49+
else:
50+
print('{}')
51+
" > "$RUNNER_TEMP/vu1nz-review.json"
52+
53+
- name: Build PR comment
54+
id: comment
55+
run: |
56+
python3 << 'PYEOF'
57+
import json, os, sys
58+
59+
review_file = os.environ.get("RUNNER_TEMP", "") + "/vu1nz-review.json"
60+
comment_file = os.environ.get("RUNNER_TEMP", "") + "/vu1nz-comment.md"
61+
62+
try:
63+
with open(review_file) as f:
64+
data = json.loads(f.read(), strict=False)
65+
except Exception as e:
66+
print(f"::warning::Could not parse review results: {e}")
67+
with open(comment_file, "w") as f:
68+
f.write("## vu1nz Security Review\n\nCould not parse review results.\n")
69+
sys.exit(0)
70+
71+
findings = data.get("findings", [])
72+
analysis = data.get("analysis", "")
73+
pr = data.get("pr_number", "?")
74+
total = len(findings)
75+
76+
counts = {"critical": 0, "high": 0, "medium": 0, "low": 0}
77+
for finding in findings:
78+
sev = finding.get("severity", "").lower()
79+
if sev in counts:
80+
counts[sev] += 1
81+
82+
has_hc = counts["critical"] > 0 or counts["high"] > 0
83+
84+
lines = ["## vu1nz Security Review", ""]
85+
lines.append(f"**{total}** finding(s) in PR #{pr}")
86+
lines.append("")
87+
88+
badge_parts = []
89+
for sev in ("critical", "high", "medium", "low"):
90+
if counts[sev] > 0:
91+
badge_parts.append(f"**{sev.upper()}**: {counts[sev]}")
92+
if badge_parts:
93+
lines.append(" | ".join(badge_parts))
94+
lines.append("")
95+
96+
if has_hc:
97+
lines.append("> **High or critical findings — review before merging.**")
98+
lines.append("")
99+
100+
if findings:
101+
lines.append("### Findings")
102+
lines.append("")
103+
lines.append("| Severity | File | Issue | Suggestion |")
104+
lines.append("|----------|------|-------|------------|")
105+
for f in findings:
106+
sev = f.get("severity", "?").upper()
107+
file = f.get("file", "N/A")
108+
issue = f.get("issue", "").replace("\n", " ")[:150]
109+
suggestion = f.get("suggestion", "").replace("\n", " ")[:150]
110+
lines.append(f"| {sev} | `{file}` | {issue} | {suggestion} |")
111+
lines.append("")
112+
else:
113+
lines.append("No security issues found.")
114+
lines.append("")
115+
116+
if analysis:
117+
lines.append("<details><summary>Full AI Analysis</summary>")
118+
lines.append("")
119+
lines.append(analysis)
120+
lines.append("")
121+
lines.append("</details>")
122+
123+
body = "\n".join(lines)
124+
with open(comment_file, "w") as f:
125+
f.write(body)
126+
127+
with open(os.environ.get("GITHUB_OUTPUT", ""), "a") as out:
128+
out.write(f"total={total}\n")
129+
out.write(f"has_high_critical={'true' if has_hc else 'false'}\n")
130+
131+
if has_hc:
132+
print(f"::error::vu1nz found high/critical vulnerabilities in PR code")
133+
sys.exit(1)
134+
135+
print(f"::notice::vu1nz review: {total} finding(s), no high/critical issues")
136+
PYEOF
137+
138+
- name: Comment on PR
139+
if: always()
140+
uses: actions/github-script@v7
141+
with:
142+
script: |
143+
const fs = require('fs');
144+
const commentFile = `${process.env.RUNNER_TEMP}/vu1nz-comment.md`;
145+
let body;
146+
try {
147+
body = fs.readFileSync(commentFile, 'utf8');
148+
} catch {
149+
body = '## vu1nz Security Review\n\nScan completed but could not read results.';
150+
}
151+
152+
const { data: comments } = await github.rest.issues.listComments({
153+
issue_number: context.issue.number,
154+
owner: context.repo.owner,
155+
repo: context.repo.repo,
156+
});
157+
158+
const existing = comments.find(c =>
159+
c.user.type === 'Bot' && c.body.includes('vu1nz Security Review')
160+
);
161+
162+
if (existing) {
163+
await github.rest.issues.updateComment({
164+
comment_id: existing.id,
165+
owner: context.repo.owner,
166+
repo: context.repo.repo,
167+
body: body,
168+
});
169+
} else {
170+
await github.rest.issues.createComment({
171+
issue_number: context.issue.number,
172+
owner: context.repo.owner,
173+
repo: context.repo.repo,
174+
body: body,
175+
});
176+
}

0 commit comments

Comments
 (0)