# Managed by sh1pt Actions Fleet # pack: vu1nz-scan@1.0.0 # install: sh1pt-actions-store # hash: sha256:a5f27998f1a6ddd9e2ff263724a5d4eb5887a306210d9c00591d9a918a7136ad name: vu1nz security scan on: pull_request: permissions: contents: read pull-requests: write jobs: review: name: Review PR for security vulnerabilities runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install vu1nz run: pip install --quiet git+https://github.com/profullstack/vu1nz-gh-actions.git - name: Load env file env: ENV_FILE: ${{ secrets.ENV_FILE }} run: | echo "$ENV_FILE" > "$RUNNER_TEMP/.env" echo "Keys in ENV_FILE:" grep -oP '^[A-Z_]+(?==)' "$RUNNER_TEMP/.env" || echo "(no keys found or different format)" ANTHROPIC_API_KEY=$(grep -E '^ANTHROPIC_API_KEY=' "$RUNNER_TEMP/.env" | head -1 | sed 's/^ANTHROPIC_API_KEY=//') if [ -n "$ANTHROPIC_API_KEY" ]; then echo "::add-mask::$ANTHROPIC_API_KEY" echo "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" >> "$GITHUB_ENV" echo "ANTHROPIC_API_KEY found and exported" else echo "::warning::ANTHROPIC_API_KEY not found in ENV_FILE" fi - name: Review PR id: review env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NO_COLOR: "1" TERM: dumb run: | vu1nz review-pr main \ ${{ github.repository }} \ ${{ github.event.pull_request.number }} \ --token "$GITHUB_TOKEN" \ --json \ | tee "$RUNNER_TEMP/vu1nz-review-raw.txt" || true python3 -c " import json, re, sys raw = open('$RUNNER_TEMP/vu1nz-review-raw.txt').read() raw = re.sub(r'\x1b\[[0-9;]*m', '', raw) start = raw.find('{') if start >= 0: obj, _ = json.JSONDecoder(strict=False).raw_decode(raw, start) json.dump(obj, sys.stdout) else: print('{}') " > "$RUNNER_TEMP/vu1nz-review.json" - name: Build PR comment id: comment run: | python3 << 'PYEOF' import json, os, sys review_file = os.environ.get("RUNNER_TEMP", "") + "/vu1nz-review.json" comment_file = os.environ.get("RUNNER_TEMP", "") + "/vu1nz-comment.md" try: with open(review_file) as f: data = json.loads(f.read(), strict=False) except Exception as e: print(f"::warning::Could not parse review results: {e}") with open(comment_file, "w") as f: f.write("## vu1nz Security Review\n\nCould not parse review results.\n") sys.exit(0) findings = data.get("findings", []) analysis = data.get("analysis", "") pr = data.get("pr_number", "?") total = len(findings) counts = {"critical": 0, "high": 0, "medium": 0, "low": 0} for finding in findings: sev = finding.get("severity", "").lower() if sev in counts: counts[sev] += 1 has_hc = counts["critical"] > 0 or counts["high"] > 0 lines = ["## vu1nz Security Review", ""] lines.append(f"**{total}** finding(s) in PR #{pr}") lines.append("") badge_parts = [] for sev in ("critical", "high", "medium", "low"): if counts[sev] > 0: badge_parts.append(f"**{sev.upper()}**: {counts[sev]}") if badge_parts: lines.append(" | ".join(badge_parts)) lines.append("") if has_hc: lines.append("> **High or critical findings - review before merging.**") lines.append("") if findings: lines.append("### Findings") lines.append("") lines.append("| Severity | File | Issue | Suggestion |") lines.append("|----------|------|-------|------------|") for f in findings: sev = f.get("severity", "?").upper() file = f.get("file", "N/A") issue = f.get("issue", "").replace("\n", " ")[:150] suggestion = f.get("suggestion", "").replace("\n", " ")[:150] lines.append(f"| {sev} | `{file}` | {issue} | {suggestion} |") lines.append("") else: lines.append("No security issues found.") lines.append("") if analysis: lines.append("
Full AI Analysis") lines.append("") lines.append(analysis) lines.append("") lines.append("
") body = "\n".join(lines) with open(comment_file, "w") as f: f.write(body) with open(os.environ.get("GITHUB_OUTPUT", ""), "a") as out: out.write(f"total={total}\n") out.write(f"has_high_critical={'true' if has_hc else 'false'}\n") if has_hc: print(f"::error::vu1nz found high/critical vulnerabilities in PR code") sys.exit(1) print(f"::notice::vu1nz review: {total} finding(s), no high/critical issues") PYEOF - name: Write report to job summary if: always() run: | if [ -f "$RUNNER_TEMP/vu1nz-comment.md" ]; then cat "$RUNNER_TEMP/vu1nz-comment.md" >> "$GITHUB_STEP_SUMMARY" else echo "## vu1nz Security Review" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "Scan completed but could not read results." >> "$GITHUB_STEP_SUMMARY" fi - name: Comment on PR if: always() && github.event.pull_request.head.repo.full_name == github.repository uses: actions/github-script@v7 with: script: | const fs = require('fs'); const commentFile = `${process.env.RUNNER_TEMP}/vu1nz-comment.md`; let body; try { body = fs.readFileSync(commentFile, 'utf8'); } catch { body = '## vu1nz Security Review\n\nScan completed but could not read results.'; } try { const { data: comments } = await github.rest.issues.listComments({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, }); const existing = comments.find(c => c.user.type === 'Bot' && c.body.includes('vu1nz Security Review') ); if (existing) { await github.rest.issues.updateComment({ comment_id: existing.id, owner: context.repo.owner, repo: context.repo.repo, body: body, }); } else { await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: body, }); } } catch (err) { if (err.status === 403) { core.warning(`Cannot post PR comment (read-only token): ${err.message}. Findings are in the job summary.`); } else { throw err; } }