|
| 1 | +# Managed by sh1pt Actions Fleet |
| 2 | +# pack: vu1nz-scan@1.0.0 |
| 3 | +# install: sh1pt-actions-store |
| 4 | +# hash: sha256:a5f27998f1a6ddd9e2ff263724a5d4eb5887a306210d9c00591d9a918a7136ad |
| 5 | +name: vu1nz security scan |
| 6 | + |
| 7 | +on: |
| 8 | + pull_request: |
| 9 | + |
| 10 | +permissions: |
| 11 | + contents: read |
| 12 | + pull-requests: write |
| 13 | + |
| 14 | +jobs: |
| 15 | + review: |
| 16 | + name: Review PR for security vulnerabilities |
| 17 | + runs-on: ubuntu-latest |
| 18 | + timeout-minutes: 15 |
| 19 | + |
| 20 | + steps: |
| 21 | + - uses: actions/checkout@v4 |
| 22 | + |
| 23 | + - uses: actions/setup-python@v5 |
| 24 | + with: |
| 25 | + python-version: "3.12" |
| 26 | + |
| 27 | + - name: Install vu1nz |
| 28 | + run: pip install --quiet git+https://github.com/profullstack/vu1nz-gh-actions.git |
| 29 | + |
| 30 | + - name: Load env file |
| 31 | + env: |
| 32 | + ENV_FILE: ${{ secrets.ENV_FILE }} |
| 33 | + run: | |
| 34 | + echo "$ENV_FILE" > "$RUNNER_TEMP/.env" |
| 35 | + echo "Keys in ENV_FILE:" |
| 36 | + grep -oP '^[A-Z_]+(?==)' "$RUNNER_TEMP/.env" || echo "(no keys found or different format)" |
| 37 | + ANTHROPIC_API_KEY=$(grep -E '^ANTHROPIC_API_KEY=' "$RUNNER_TEMP/.env" | head -1 | sed 's/^ANTHROPIC_API_KEY=//') |
| 38 | + if [ -n "$ANTHROPIC_API_KEY" ]; then |
| 39 | + echo "::add-mask::$ANTHROPIC_API_KEY" |
| 40 | + echo "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" >> "$GITHUB_ENV" |
| 41 | + echo "ANTHROPIC_API_KEY found and exported" |
| 42 | + else |
| 43 | + echo "::warning::ANTHROPIC_API_KEY not found in ENV_FILE" |
| 44 | + fi |
| 45 | +
|
| 46 | + - name: Review PR |
| 47 | + id: review |
| 48 | + env: |
| 49 | + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 50 | + NO_COLOR: "1" |
| 51 | + TERM: dumb |
| 52 | + run: | |
| 53 | + vu1nz review-pr main \ |
| 54 | + ${{ github.repository }} \ |
| 55 | + ${{ github.event.pull_request.number }} \ |
| 56 | + --token "$GITHUB_TOKEN" \ |
| 57 | + --json \ |
| 58 | + | tee "$RUNNER_TEMP/vu1nz-review-raw.txt" || true |
| 59 | +
|
| 60 | + python3 -c " |
| 61 | + import json, re, sys |
| 62 | + raw = open('$RUNNER_TEMP/vu1nz-review-raw.txt').read() |
| 63 | + raw = re.sub(r'\x1b\[[0-9;]*m', '', raw) |
| 64 | + start = raw.find('{') |
| 65 | + if start >= 0: |
| 66 | + obj, _ = json.JSONDecoder(strict=False).raw_decode(raw, start) |
| 67 | + json.dump(obj, sys.stdout) |
| 68 | + else: |
| 69 | + print('{}') |
| 70 | + " > "$RUNNER_TEMP/vu1nz-review.json" |
| 71 | +
|
| 72 | + - name: Build PR comment |
| 73 | + id: comment |
| 74 | + run: | |
| 75 | + python3 << 'PYEOF' |
| 76 | + import json, os, sys |
| 77 | +
|
| 78 | + review_file = os.environ.get("RUNNER_TEMP", "") + "/vu1nz-review.json" |
| 79 | + comment_file = os.environ.get("RUNNER_TEMP", "") + "/vu1nz-comment.md" |
| 80 | +
|
| 81 | + try: |
| 82 | + with open(review_file) as f: |
| 83 | + data = json.loads(f.read(), strict=False) |
| 84 | + except Exception as e: |
| 85 | + print(f"::warning::Could not parse review results: {e}") |
| 86 | + with open(comment_file, "w") as f: |
| 87 | + f.write("## vu1nz Security Review\n\nCould not parse review results.\n") |
| 88 | + sys.exit(0) |
| 89 | +
|
| 90 | + findings = data.get("findings", []) |
| 91 | + analysis = data.get("analysis", "") |
| 92 | + pr = data.get("pr_number", "?") |
| 93 | + total = len(findings) |
| 94 | +
|
| 95 | + counts = {"critical": 0, "high": 0, "medium": 0, "low": 0} |
| 96 | + for finding in findings: |
| 97 | + sev = finding.get("severity", "").lower() |
| 98 | + if sev in counts: |
| 99 | + counts[sev] += 1 |
| 100 | +
|
| 101 | + has_hc = counts["critical"] > 0 or counts["high"] > 0 |
| 102 | +
|
| 103 | + lines = ["## vu1nz Security Review", ""] |
| 104 | + lines.append(f"**{total}** finding(s) in PR #{pr}") |
| 105 | + lines.append("") |
| 106 | +
|
| 107 | + badge_parts = [] |
| 108 | + for sev in ("critical", "high", "medium", "low"): |
| 109 | + if counts[sev] > 0: |
| 110 | + badge_parts.append(f"**{sev.upper()}**: {counts[sev]}") |
| 111 | + if badge_parts: |
| 112 | + lines.append(" | ".join(badge_parts)) |
| 113 | + lines.append("") |
| 114 | +
|
| 115 | + if has_hc: |
| 116 | + lines.append("> **High or critical findings - review before merging.**") |
| 117 | + lines.append("") |
| 118 | +
|
| 119 | + if findings: |
| 120 | + lines.append("### Findings") |
| 121 | + lines.append("") |
| 122 | + lines.append("| Severity | File | Issue | Suggestion |") |
| 123 | + lines.append("|----------|------|-------|------------|") |
| 124 | + for f in findings: |
| 125 | + sev = f.get("severity", "?").upper() |
| 126 | + file = f.get("file", "N/A") |
| 127 | + issue = f.get("issue", "").replace("\n", " ")[:150] |
| 128 | + suggestion = f.get("suggestion", "").replace("\n", " ")[:150] |
| 129 | + lines.append(f"| {sev} | `{file}` | {issue} | {suggestion} |") |
| 130 | + lines.append("") |
| 131 | + else: |
| 132 | + lines.append("No security issues found.") |
| 133 | + lines.append("") |
| 134 | +
|
| 135 | + if analysis: |
| 136 | + lines.append("<details><summary>Full AI Analysis</summary>") |
| 137 | + lines.append("") |
| 138 | + lines.append(analysis) |
| 139 | + lines.append("") |
| 140 | + lines.append("</details>") |
| 141 | +
|
| 142 | + body = "\n".join(lines) |
| 143 | + with open(comment_file, "w") as f: |
| 144 | + f.write(body) |
| 145 | +
|
| 146 | + with open(os.environ.get("GITHUB_OUTPUT", ""), "a") as out: |
| 147 | + out.write(f"total={total}\n") |
| 148 | + out.write(f"has_high_critical={'true' if has_hc else 'false'}\n") |
| 149 | +
|
| 150 | + if has_hc: |
| 151 | + print(f"::error::vu1nz found high/critical vulnerabilities in PR code") |
| 152 | + sys.exit(1) |
| 153 | +
|
| 154 | + print(f"::notice::vu1nz review: {total} finding(s), no high/critical issues") |
| 155 | + PYEOF |
| 156 | +
|
| 157 | + - name: Write report to job summary |
| 158 | + if: always() |
| 159 | + run: | |
| 160 | + if [ -f "$RUNNER_TEMP/vu1nz-comment.md" ]; then |
| 161 | + cat "$RUNNER_TEMP/vu1nz-comment.md" >> "$GITHUB_STEP_SUMMARY" |
| 162 | + else |
| 163 | + echo "## vu1nz Security Review" >> "$GITHUB_STEP_SUMMARY" |
| 164 | + echo "" >> "$GITHUB_STEP_SUMMARY" |
| 165 | + echo "Scan completed but could not read results." >> "$GITHUB_STEP_SUMMARY" |
| 166 | + fi |
| 167 | +
|
| 168 | + - name: Comment on PR |
| 169 | + if: always() && github.event.pull_request.head.repo.full_name == github.repository |
| 170 | + uses: actions/github-script@v7 |
| 171 | + with: |
| 172 | + script: | |
| 173 | + const fs = require('fs'); |
| 174 | + const commentFile = `${process.env.RUNNER_TEMP}/vu1nz-comment.md`; |
| 175 | + let body; |
| 176 | + try { |
| 177 | + body = fs.readFileSync(commentFile, 'utf8'); |
| 178 | + } catch { |
| 179 | + body = '## vu1nz Security Review\n\nScan completed but could not read results.'; |
| 180 | + } |
| 181 | +
|
| 182 | + try { |
| 183 | + const { data: comments } = await github.rest.issues.listComments({ |
| 184 | + issue_number: context.issue.number, |
| 185 | + owner: context.repo.owner, |
| 186 | + repo: context.repo.repo, |
| 187 | + }); |
| 188 | +
|
| 189 | + const existing = comments.find(c => |
| 190 | + c.user.type === 'Bot' && c.body.includes('vu1nz Security Review') |
| 191 | + ); |
| 192 | +
|
| 193 | + if (existing) { |
| 194 | + await github.rest.issues.updateComment({ |
| 195 | + comment_id: existing.id, |
| 196 | + owner: context.repo.owner, |
| 197 | + repo: context.repo.repo, |
| 198 | + body: body, |
| 199 | + }); |
| 200 | + } else { |
| 201 | + await github.rest.issues.createComment({ |
| 202 | + issue_number: context.issue.number, |
| 203 | + owner: context.repo.owner, |
| 204 | + repo: context.repo.repo, |
| 205 | + body: body, |
| 206 | + }); |
| 207 | + } |
| 208 | + } catch (err) { |
| 209 | + if (err.status === 403) { |
| 210 | + core.warning(`Cannot post PR comment (read-only token): ${err.message}. Findings are in the job summary.`); |
| 211 | + } else { |
| 212 | + throw err; |
| 213 | + } |
| 214 | + } |
0 commit comments