ci: set CI=true in test workflow #10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: vu1nz security scan | |
| on: | |
| pull_request: | |
| push: | |
| branches: [master] | |
| permissions: | |
| contents: read | |
| actions: read | |
| pull-requests: write | |
| jobs: | |
| scan: | |
| name: Scan CI/CD for vulnerabilities | |
| runs-on: ubuntu-latest | |
| 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: Scan workflows | |
| id: scan | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| vu1nz actions scan ${{ github.repository }} \ | |
| --token "$GITHUB_TOKEN" \ | |
| --json \ | |
| --output "$RUNNER_TEMP/vu1nz-report" \ | |
| | tee "$RUNNER_TEMP/vu1nz-scan-raw.txt" || true | |
| # vu1nz prints progress lines before the JSON and may include ANSI codes | |
| # Strip ANSI escape sequences, then extract the JSON object | |
| sed 's/\x1b\[[0-9;]*m//g' "$RUNNER_TEMP/vu1nz-scan-raw.txt" \ | |
| | sed -n '/^{/,/^}/p' > "$RUNNER_TEMP/vu1nz-scan.json" | |
| - name: Claude AI review | |
| if: env.HAS_CLAUDE_KEY == 'true' | |
| env: | |
| HAS_CLAUDE_KEY: ${{ secrets.ANTHROPIC_API_KEY != '' }} | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| run: | | |
| vu1nz actions scan ${{ github.repository }} \ | |
| --token "$GITHUB_TOKEN" \ | |
| --claude \ | |
| --output "$RUNNER_TEMP/vu1nz-report" | |
| - name: Evaluate findings and build comment | |
| id: eval | |
| run: | | |
| python3 << 'PYEOF' | |
| import json, os, sys | |
| scan_file = os.environ.get("RUNNER_TEMP", "") + "/vu1nz-scan.json" | |
| try: | |
| with open(scan_file) as f: | |
| data = json.load(f) | |
| except Exception as e: | |
| print(f"::warning::Could not parse scan results: {e}") | |
| # Write fallback so the comment step still works | |
| with open(os.environ.get("GITHUB_OUTPUT", ""), "a") as out: | |
| out.write("total=0\n") | |
| out.write("has_high_critical=false\n") | |
| comment_file = os.environ.get("RUNNER_TEMP", "") + "/vu1nz-comment.md" | |
| with open(comment_file, "w") as f: | |
| f.write("## vu1nz CI/CD Security Scan\n\nCould not parse scan results.\n") | |
| sys.exit(0) | |
| findings = data.get("findings", []) | |
| counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} | |
| for finding in findings: | |
| sev = finding.get("severity", "").lower() | |
| if sev in counts: | |
| counts[sev] += 1 | |
| total = len(findings) | |
| has_hc = counts["critical"] > 0 or counts["high"] > 0 | |
| # Build markdown comment body | |
| lines = ["## vu1nz CI/CD Security Scan", ""] | |
| lines.append(f"Scanned **{data.get('workflow_count', '?')}** workflows — **{total}** finding(s)") | |
| lines.append("") | |
| # Severity badges | |
| 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 detected — review before merging.**") | |
| lines.append("") | |
| # Findings table | |
| if findings: | |
| lines.append("### Findings") | |
| lines.append("") | |
| lines.append("| Severity | File | Finding | Recommendation |") | |
| lines.append("|----------|------|---------|----------------|") | |
| for finding in findings: | |
| sev = finding.get("severity", "?").upper() | |
| title = finding.get("title", "") | |
| desc = finding.get("description", "").replace("\n", " ")[:120] | |
| file = finding.get("file", "") | |
| line_num = finding.get("line", "") | |
| loc = f"`{file}:{line_num}`" if line_num else f"`{file}`" | |
| rec = finding.get("recommendation", "").replace("\n", " ")[:120] | |
| cwe = finding.get("cwe", "") | |
| sev_label = sev | |
| if cwe: | |
| sev_label = f"{sev} ({cwe})" | |
| lines.append(f"| {sev_label} | {loc} | **{title}** — {desc} | {rec} |") | |
| lines.append("") | |
| else: | |
| lines.append("No findings. All clear.") | |
| lines.append("") | |
| comment_body = "\n".join(lines) | |
| # Write comment to a file (avoids output escaping issues) | |
| comment_file = os.environ.get("RUNNER_TEMP", "") + "/vu1nz-comment.md" | |
| with open(comment_file, "w") as f: | |
| f.write(comment_body) | |
| # Write simple outputs | |
| 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 CI/CD vulnerabilities") | |
| sys.exit(1) | |
| print(f"::notice::vu1nz scan: {total} finding(s), no high/critical issues") | |
| PYEOF | |
| - name: Comment on PR | |
| if: github.event_name == 'pull_request' && always() | |
| 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 CI/CD Security Scan\n\nScan completed but could not read results.'; | |
| } | |
| 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 CI/CD Security Scan') | |
| ); | |
| 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, | |
| }); | |
| } |