Features/intermediate stage #1304
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: FixOps Security Pipeline | |
| on: | |
| pull_request: | |
| branches: [main, master] | |
| push: | |
| branches: [main, master] | |
| schedule: | |
| # Run nightly at 2 AM UTC | |
| - cron: '0 2 * * *' | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| security-events: write | |
| jobs: | |
| fixops-scan: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v3 | |
| - name: Set up Python | |
| uses: actions/setup-python@v4 | |
| with: | |
| python-version: '3.11' | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y jq | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -r requirements.txt || echo "No requirements.txt found" | |
| - name: Run security scanners | |
| id: scanners | |
| run: | | |
| # Example: Run Snyk (requires SNYK_TOKEN secret) | |
| if [ -n "${{ secrets.SNYK_TOKEN }}" ]; then | |
| snyk test --json > /tmp/snyk_results.json || true | |
| fi | |
| # Example: Run Trivy for container scanning | |
| if command -v trivy &> /dev/null; then | |
| trivy fs --format json --output /tmp/trivy_results.json . || true | |
| fi | |
| # Example: Run Semgrep for SAST | |
| if command -v semgrep &> /dev/null; then | |
| semgrep --config auto --json --output /tmp/semgrep_results.json . || true | |
| fi | |
| echo "Scanners completed" | |
| - name: Download KEV and EPSS feeds | |
| run: | | |
| python scripts/fetch_feeds.py || echo "Feed download failed, using cached" | |
| - name: Run FixOps Orchestrator | |
| id: fixops | |
| run: | | |
| # Create artifacts directory | |
| mkdir -p artifacts | |
| # Run the comprehensive 6-step orchestrator | |
| python scripts/demo_orchestrator.py \ | |
| --snyk /tmp/snyk_results.json \ | |
| --design tests/fixtures/real_world/design.csv \ | |
| --out artifacts/ci_run_manifest.json || true | |
| # Create fallback manifest if orchestrator failed | |
| if [ ! -f artifacts/ci_run_manifest.json ]; then | |
| echo '{"summary":{"risk_distribution":{"CRITICAL":0,"HIGH":0,"MEDIUM":0,"LOW":0},"kev_findings":0,"high_epss_findings":0,"mitre_techniques":0},"run_id":"fallback"}' > artifacts/ci_run_manifest.json | |
| fi | |
| # Extract risk summary (always succeeds now) | |
| CRITICAL=$(jq -r '.summary.risk_distribution.CRITICAL // 0' artifacts/ci_run_manifest.json) | |
| HIGH=$(jq -r '.summary.risk_distribution.HIGH // 0' artifacts/ci_run_manifest.json) | |
| MEDIUM=$(jq -r '.summary.risk_distribution.MEDIUM // 0' artifacts/ci_run_manifest.json) | |
| KEV=$(jq -r '.summary.kev_findings // 0' artifacts/ci_run_manifest.json) | |
| # Always set outputs | |
| echo "critical=$CRITICAL" >> $GITHUB_OUTPUT | |
| echo "high=$HIGH" >> $GITHUB_OUTPUT | |
| echo "medium=$MEDIUM" >> $GITHUB_OUTPUT | |
| echo "kev=$KEV" >> $GITHUB_OUTPUT | |
| # Add summary | |
| echo "### FixOps Security Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Risk Tier | Count |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-----------|-------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| 🔴 CRITICAL | $CRITICAL |" >> $GITHUB_STEP_SUMMARY | |
| echo "| 🟠 HIGH | $HIGH |" >> $GITHUB_STEP_SUMMARY | |
| echo "| 🟡 MEDIUM | $MEDIUM |" >> $GITHUB_STEP_SUMMARY | |
| echo "| 🔵 KEV-Listed | $KEV |" >> $GITHUB_STEP_SUMMARY | |
| - name: Check risk thresholds | |
| id: gate | |
| run: | | |
| # Default to 0 if outputs are empty | |
| CRITICAL="${{ steps.fixops.outputs.critical }}" | |
| CRITICAL="${CRITICAL:-0}" | |
| HIGH="${{ steps.fixops.outputs.high }}" | |
| HIGH="${HIGH:-0}" | |
| KEV="${{ steps.fixops.outputs.kev }}" | |
| KEV="${KEV:-0}" | |
| # Policy: Block if CRITICAL > 0 or KEV > 5 (only on main/master, warn on PRs) | |
| if [ "$CRITICAL" -gt 0 ]; then | |
| echo "❌ BLOCKED: $CRITICAL CRITICAL findings detected" | |
| echo "gate_status=blocked" >> $GITHUB_OUTPUT | |
| # Only exit 1 on main/master branches, not on PRs | |
| if [ "${{ github.event_name }}" != "pull_request" ]; then | |
| exit 1 | |
| fi | |
| elif [ "$KEV" -gt 5 ]; then | |
| echo "❌ BLOCKED: $KEV KEV-listed vulnerabilities detected (threshold: 5)" | |
| echo "gate_status=blocked" >> $GITHUB_OUTPUT | |
| # Only exit 1 on main/master branches, not on PRs | |
| if [ "${{ github.event_name }}" != "pull_request" ]; then | |
| exit 1 | |
| fi | |
| elif [ "$HIGH" -gt 10 ]; then | |
| echo "⚠️ WARNING: $HIGH HIGH findings detected (threshold: 10)" | |
| echo "gate_status=warning" >> $GITHUB_OUTPUT | |
| else | |
| echo "✅ PASSED: No blocking findings" | |
| echo "gate_status=passed" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v3 | |
| if: always() | |
| with: | |
| name: fixops-results | |
| path: | | |
| artifacts/*.json | |
| artifacts/*.md | |
| retention-days: 90 | |
| - name: Comment on PR | |
| if: github.event_name == 'pull_request' && always() | |
| uses: actions/github-script@v6 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| let comment = '## 🔒 FixOps Security Analysis\n\n'; | |
| let manifest = null; | |
| if (fs.existsSync('artifacts/ci_run_manifest.json')) { | |
| manifest = JSON.parse(fs.readFileSync('artifacts/ci_run_manifest.json', 'utf8')); | |
| const summary = manifest.summary; | |
| comment += `**Risk Distribution:**\n`; | |
| comment += `- 🔴 CRITICAL: ${summary.risk_distribution.CRITICAL || 0}\n`; | |
| comment += `- 🟠 HIGH: ${summary.risk_distribution.HIGH || 0}\n`; | |
| comment += `- 🟡 MEDIUM: ${summary.risk_distribution.MEDIUM || 0}\n`; | |
| comment += `- 🟢 LOW: ${summary.risk_distribution.LOW || 0}\n\n`; | |
| comment += `**Threat Intelligence:**\n`; | |
| comment += `- 🚨 KEV-Listed: ${summary.kev_findings}\n`; | |
| comment += `- 📊 High EPSS (>0.7): ${summary.high_epss_findings}\n`; | |
| comment += `- 🎯 MITRE Techniques: ${summary.mitre_techniques}\n\n`; | |
| const gateStatus = '${{ steps.gate.outputs.gate_status }}'; | |
| if (gateStatus === 'blocked') { | |
| comment += `### ❌ BLOCKED\n\nThis PR would be blocked on main/master due to CRITICAL findings or excessive KEV-listed vulnerabilities.\n\n`; | |
| } else if (gateStatus === 'warning') { | |
| comment += `### ⚠️ WARNING\n\nThis PR has HIGH findings that should be reviewed.\n\n`; | |
| } else { | |
| comment += `### ✅ PASSED\n\nNo blocking security findings detected.\n\n`; | |
| } | |
| // Add top findings if available | |
| if (fs.existsSync('artifacts/explanations.json')) { | |
| const explanations = JSON.parse(fs.readFileSync('artifacts/explanations.json', 'utf8')); | |
| if (explanations.length > 0) { | |
| comment += `<details>\n<summary>Top Findings (${explanations.length})</summary>\n\n`; | |
| explanations.slice(0, 3).forEach(exp => { | |
| comment += `#### ${exp.cve_id || exp.id}: ${exp.title}\n`; | |
| comment += `**Risk Tier:** ${exp.risk_tier}\n\n`; | |
| }); | |
| comment += `</details>\n`; | |
| } | |
| } | |
| } else { | |
| comment += '⚠️ FixOps analysis failed. Check workflow logs for details.\n'; | |
| } | |
| comment += `\n---\n*Run ID: ${manifest?.run_id || 'unknown'}* | [View Full Report](${context.payload.repository.html_url}/actions/runs/${context.runId})`; | |
| github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: comment | |
| }); | |
| - name: Create SARIF report | |
| if: always() | |
| run: | | |
| # Convert FixOps findings to SARIF format for GitHub Security tab | |
| python3 << 'PYEOF' | |
| import json | |
| from pathlib import Path | |
| if Path('artifacts/prioritized_findings.json').exists(): | |
| with open('artifacts/prioritized_findings.json') as f: | |
| findings = json.load(f) | |
| sarif = { | |
| 'version': '2.1.0', | |
| '$schema': 'https://json.schemastore.org/sarif-2.1.0.json', | |
| 'runs': [{ | |
| 'tool': { | |
| 'driver': { | |
| 'name': 'FixOps', | |
| 'version': '1.0.0', | |
| 'informationUri': 'https://fixops.io' | |
| } | |
| }, | |
| 'results': [] | |
| }] | |
| } | |
| for finding in findings[:100]: | |
| result = { | |
| 'ruleId': finding.get('cve_id') or finding.get('id'), | |
| 'level': 'error' if finding.get('risk_tier') in ['CRITICAL', 'HIGH'] else 'warning', | |
| 'message': {'text': finding.get('title', 'Security finding')}, | |
| 'locations': [{'physicalLocation': {'artifactLocation': {'uri': finding.get('file_path', 'unknown')}}}] | |
| } | |
| sarif['runs'][0]['results'].append(result) | |
| with open('fixops.sarif', 'w') as f: | |
| json.dump(sarif, f, indent=2) | |
| PYEOF | |
| - name: Upload SARIF to GitHub Security | |
| uses: github/codeql-action/upload-sarif@v2 | |
| if: always() | |
| with: | |
| sarif_file: fixops.sarif | |
| category: fixops-security |