@@ -2,17 +2,14 @@ name: vu1nz security scan
22
33on :
44 pull_request :
5- push :
6- branches : [master]
75
86permissions :
97 contents : read
10- actions : read
118 pull-requests : write
129
1310jobs :
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({
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