Unified Build and Attestation #17
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: "Unified Build and Attestation" | |
| on: | |
| push: | |
| branches: ["main", "develop"] | |
| pull_request: | |
| branches: ["main"] | |
| release: | |
| types: ["published"] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| security-events: write | |
| attestations: write | |
| id-token: write | |
| actions: read | |
| packages: read | |
| jobs: | |
| build-scan-attest: | |
| name: Build, Scan, and Attest | |
| runs-on: ubuntu-latest | |
| outputs: | |
| artifact-name: ${{ steps.build.outputs.artifact_name }} | |
| artifact-hash: ${{ steps.build.outputs.artifact_hash }} | |
| codeql-results: ${{ steps.codeql-results.outputs.security_summary }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Build artifact | |
| id: build | |
| run: | | |
| echo "🏗️ Building artifact..." | |
| # Create the artifact package | |
| tar --exclude='.git' \ | |
| --exclude='venv' \ | |
| --exclude='__pycache__' \ | |
| --exclude='*.log' \ | |
| --exclude='*.db' \ | |
| --exclude='.github' \ | |
| -czf vulnerable-app-${{ github.sha }}.tar.gz \ | |
| *.py requirements.txt README.md *.md *.json *.ini | |
| # Calculate hash | |
| SHA256=$(sha256sum vulnerable-app-${{ github.sha }}.tar.gz | cut -d' ' -f1) | |
| # Output for later steps | |
| echo "artifact_name=vulnerable-app-${{ github.sha }}.tar.gz" >> $GITHUB_OUTPUT | |
| echo "artifact_hash=$SHA256" >> $GITHUB_OUTPUT | |
| echo "✅ Built: vulnerable-app-${{ github.sha }}.tar.gz" | |
| echo "🔐 SHA256: $SHA256" | |
| - name: Initialize CodeQL | |
| uses: github/codeql-action/init@v3 | |
| with: | |
| languages: python | |
| queries: security-extended | |
| - name: Perform CodeQL Analysis | |
| uses: github/codeql-action/analyze@v3 | |
| with: | |
| category: "/language:python" | |
| upload: true | |
| - name: Wait for CodeQL results and extract security data | |
| id: codeql-results | |
| run: | | |
| echo "🔍 Extracting CodeQL security results..." | |
| # Get ref and commit info | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| REF_NAME="refs/pull/${{ github.event.pull_request.number }}/head" | |
| COMMIT_SHA="${{ github.event.pull_request.head.sha }}" | |
| PR_NUMBER="${{ github.event.pull_request.number }}" | |
| else | |
| REF_NAME="${{ github.ref }}" | |
| COMMIT_SHA="${{ github.sha }}" | |
| PR_NUMBER="null" | |
| fi | |
| echo "📊 Looking for CodeQL analysis: ref=$REF_NAME, commit=$COMMIT_SHA" | |
| # Wait and retry logic for CodeQL analysis to be registered | |
| ANALYSIS_DATA="{}" | |
| MAX_ATTEMPTS=6 | |
| ATTEMPT=1 | |
| while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do | |
| echo "🔍 Attempt $ATTEMPT/$MAX_ATTEMPTS: Checking for CodeQL analysis..." | |
| # Try to find analysis for this exact commit and ref | |
| ANALYSIS_DATA=$(gh api repos/${{ github.repository }}/code-scanning/analyses \ | |
| --jq --arg commit "$COMMIT_SHA" --arg ref "$REF_NAME" ' | |
| map(select(.commit_sha == $commit and .ref == $ref)) | | |
| sort_by(.created_at) | reverse | .[0] // {}' 2>/dev/null || echo "{}") | |
| # Debug: Show what we got | |
| echo "🔍 Analysis data: $(echo "$ANALYSIS_DATA" | jq -c '.')" | |
| # Check if we have a valid analysis (check for id field) | |
| HAS_ID=$(echo "$ANALYSIS_DATA" | jq 'has("id")' 2>/dev/null || echo "false") | |
| if [ "$HAS_ID" = "true" ]; then | |
| echo "✅ Found CodeQL analysis on attempt $ATTEMPT" | |
| break | |
| fi | |
| # If this is the last attempt, try fallback strategies | |
| if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then | |
| echo "🔍 Final attempt: Trying fallback strategies..." | |
| # Fallback 1: Try just by commit SHA (any ref) | |
| ANALYSIS_DATA=$(gh api repos/${{ github.repository }}/code-scanning/analyses \ | |
| --jq --arg commit "$COMMIT_SHA" ' | |
| map(select(.commit_sha == $commit)) | | |
| sort_by(.created_at) | reverse | .[0] // {}' 2>/dev/null || echo "{}") | |
| HAS_ID=$(echo "$ANALYSIS_DATA" | jq 'has("id")' 2>/dev/null || echo "false") | |
| if [ "$HAS_ID" = "true" ]; then | |
| echo "✅ Found analysis using commit SHA fallback" | |
| break | |
| fi | |
| # Fallback 2: Get latest analysis for the ref | |
| ANALYSIS_DATA=$(gh api repos/${{ github.repository }}/code-scanning/analyses \ | |
| --jq --arg ref "$REF_NAME" ' | |
| map(select(.ref == $ref)) | | |
| sort_by(.created_at) | reverse | .[0] // {}' 2>/dev/null || echo "{}") | |
| HAS_ID=$(echo "$ANALYSIS_DATA" | jq 'has("id")' 2>/dev/null || echo "false") | |
| if [ "$HAS_ID" = "true" ]; then | |
| echo "✅ Found analysis using ref fallback" | |
| break | |
| fi | |
| fi | |
| echo "⏳ Analysis not ready yet, waiting 10 seconds..." | |
| sleep 10 | |
| ATTEMPT=$((ATTEMPT + 1)) | |
| done | |
| # Get previous analysis count to detect new alerts | |
| echo "📈 Checking for new alerts..." | |
| PREVIOUS_COUNT=0 | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| # For PRs, compare with main branch | |
| PREVIOUS_ANALYSIS=$(gh api repos/${{ github.repository }}/code-scanning/analyses \ | |
| --jq 'map(select(.ref == "refs/heads/main")) | sort_by(.created_at) | reverse | .[0] // {}' 2>/dev/null) || echo "{}" | |
| PREVIOUS_COUNT=$(echo "$PREVIOUS_ANALYSIS" | jq '.results_count // 0') | |
| else | |
| # For pushes, compare with previous analysis on same ref | |
| PREVIOUS_ANALYSIS=$(gh api repos/${{ github.repository }}/code-scanning/analyses \ | |
| --jq --arg ref "$REF_NAME" --arg commit "$COMMIT_SHA" ' | |
| map(select(.ref == $ref and .commit_sha != $commit)) | | |
| sort_by(.created_at) | reverse | .[0] // {}' 2>/dev/null) || echo "{}" | |
| PREVIOUS_COUNT=$(echo "$PREVIOUS_ANALYSIS" | jq '.results_count // 0') | |
| fi | |
| # Process the analysis data | |
| HAS_ID=$(echo "$ANALYSIS_DATA" | jq 'has("id")' 2>/dev/null || echo "false") | |
| if [ "$HAS_ID" = "true" ]; then | |
| CURRENT_COUNT=$(echo "$ANALYSIS_DATA" | jq '.results_count // 0') | |
| SCAN_TIME=$(echo "$ANALYSIS_DATA" | jq -r '.created_at // now') | |
| NEW_ALERTS_DETECTED=$((CURRENT_COUNT - PREVIOUS_COUNT)) | |
| echo "✅ Found CodeQL analysis: $CURRENT_COUNT total alerts, $NEW_ALERTS_DETECTED new compared to baseline" | |
| ALERTS_DATA=$(echo "$ANALYSIS_DATA" | jq --arg ref "$REF_NAME" --arg commit "$COMMIT_SHA" --arg pr "$PR_NUMBER" --argjson new_count "$NEW_ALERTS_DETECTED" --argjson prev_count "$PREVIOUS_COUNT" '{ | |
| scan_timestamp: .created_at, | |
| commit_sha: $commit, | |
| ref: $ref, | |
| pr_number: (if $pr == "null" then null else ($pr | tonumber) end), | |
| total_alerts: (.results_count // 0), | |
| new_alerts: $new_count, | |
| baseline_alerts: $prev_count, | |
| analysis_id: .id, | |
| tools_used: ["codeql"], | |
| languages_scanned: ["python"], | |
| note: "Fast analysis summary using CodeQL analyses API", | |
| scan_context: { | |
| analysis_key: ".github/workflows/unified-build-attest.yml:build-scan-attest", | |
| category: "/language:python", | |
| analysis_url: .url | |
| } | |
| }') | |
| else | |
| echo "⚠️ No CodeQL analysis found, creating placeholder summary..." | |
| ALERTS_DATA=$(jq -n --arg ref "$REF_NAME" --arg commit "$COMMIT_SHA" --arg pr "$PR_NUMBER" '{ | |
| scan_timestamp: now | strftime("%Y-%m-%dT%H:%M:%SZ"), | |
| commit_sha: $commit, | |
| ref: $ref, | |
| pr_number: (if $pr == "null" then null else ($pr | tonumber) end), | |
| total_alerts: 0, | |
| new_alerts: 0, | |
| baseline_alerts: 0, | |
| tools_used: ["codeql"], | |
| languages_scanned: ["python"], | |
| note: "CodeQL analysis not found - may still be processing", | |
| scan_context: { | |
| analysis_key: ".github/workflows/unified-build-attest.yml:build-scan-attest", | |
| category: "/language:python" | |
| } | |
| }') | |
| fi | |
| echo "security_summary<<EOF" >> $GITHUB_OUTPUT | |
| echo "$ALERTS_DATA" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| echo "📋 Security Summary:" | |
| echo "$ALERTS_DATA" | jq '.' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: vulnerable-app-source | |
| path: ${{ steps.build.outputs.artifact_name }} | |
| retention-days: 30 | |
| - name: Create SLSA build provenance attestation | |
| uses: actions/attest-build-provenance@v1 | |
| with: | |
| subject-path: ${{ steps.build.outputs.artifact_name }} | |
| - name: Wait between attestations | |
| run: sleep 2 | |
| - name: Create security assessment attestation | |
| uses: actions/attest@v1 | |
| with: | |
| subject-path: ${{ steps.build.outputs.artifact_name }} | |
| predicate-type: "https://github.com/in-toto/attestation/tree/main/spec/predicates/security-scan" | |
| predicate: | | |
| { | |
| "scan_type": "static-analysis", | |
| "scanner": { | |
| "name": "CodeQL", | |
| "vendor": "GitHub", | |
| "version": "v3", | |
| "uri": "https://github.com/github/codeql-action" | |
| }, | |
| "scan_started_on": "${{ github.run_id }}", | |
| "scan_finished_on": "${{ github.run_id }}", | |
| "artifact": { | |
| "name": "${{ steps.build.outputs.artifact_name }}", | |
| "digest": { | |
| "sha256": "${{ steps.build.outputs.artifact_hash }}" | |
| } | |
| }, | |
| "scan_context": { | |
| "repository": "${{ github.repository }}", | |
| "ref": "${{ github.ref }}", | |
| "commit_sha": "${{ github.sha }}", | |
| "workflow_run_id": "${{ github.run_id }}", | |
| "event_name": "${{ github.event_name }}", | |
| "pr_number": ${{ github.event.pull_request.number || 'null' }} | |
| }, | |
| "results": ${{ steps.codeql-results.outputs.security_summary || '{"total_alerts": 0, "note": "No security data available"}' }}, | |
| "metadata": { | |
| "created_by": "unified-build-attest.yml", | |
| "attestation_version": "1.0", | |
| "purpose": "Supply chain security assessment for intentionally vulnerable application" | |
| } | |
| } | |
| - name: Create vulnerability disclosure attestation | |
| uses: actions/attest@v1 | |
| with: | |
| subject-path: ${{ steps.build.outputs.artifact_name }} | |
| predicate-type: "https://slsa.dev/spec/v1.1/provenance" | |
| predicate: | | |
| { | |
| "notice": "INTENTIONALLY VULNERABLE SOFTWARE - FOR EDUCATIONAL USE ONLY", | |
| "purpose": "Security testing, training, and tool validation", | |
| "warnings": [ | |
| "Contains multiple SQL injection vulnerabilities", | |
| "Contains cross-site scripting (XSS) vulnerabilities", | |
| "Contains command injection vulnerabilities", | |
| "Uses vulnerable dependencies with known CVEs", | |
| "Implements insecure authentication mechanisms", | |
| "DO NOT DEPLOY IN PRODUCTION ENVIRONMENTS" | |
| ], | |
| "intended_use": [ | |
| "Security training and education", | |
| "Penetration testing practice", | |
| "Security tool validation", | |
| "DevSecOps pipeline testing" | |
| ], | |
| "build_info": { | |
| "repository": "${{ github.repository }}", | |
| "commit": "${{ github.sha }}", | |
| "ref": "${{ github.ref }}", | |
| "workflow_run": "${{ github.run_id }}", | |
| "built_at": "${{ github.event.head_commit.timestamp || github.event.repository.updated_at }}", | |
| "artifact_hash": "${{ steps.build.outputs.artifact_hash }}" | |
| }, | |
| "vulnerability_summary": ${{ steps.codeql-results.outputs.security_summary || '{"total_alerts": 0, "note": "No vulnerability data available"}' }}, | |
| "compliance": { | |
| "slsa_build_level": 1, | |
| "supply_chain_security": "educational-only" | |
| } | |
| } | |
| - name: Wait between attestations | |
| run: sleep 2 | |
| - name: Verify attestations | |
| run: | | |
| echo "🔐 Verifying created attestations..." | |
| ARTIFACT_FILE="${{ steps.build.outputs.artifact_name }}" | |
| echo "📁 Verifying attestations for: $ARTIFACT_FILE" | |
| # Verify SLSA build provenance | |
| echo "🏗️ Verifying SLSA build provenance..." | |
| if gh attestation verify "$ARTIFACT_FILE" --repo ${{ github.repository }} 2>/dev/null; then | |
| echo "✅ SLSA build provenance: VERIFIED" | |
| else | |
| echo "⚠️ SLSA build provenance: Could not verify (may be processing)" | |
| fi | |
| # Verify security assessment | |
| echo "🔍 Verifying security assessment..." | |
| if gh attestation verify "$ARTIFACT_FILE" \ | |
| --repo ${{ github.repository }} \ | |
| --predicate-type "https://github.com/in-toto/attestation/tree/main/spec/predicates/security-scan" 2>/dev/null; then | |
| echo "✅ Security assessment: VERIFIED" | |
| else | |
| echo "⚠️ Security assessment: Could not verify (may be processing)" | |
| fi | |
| # Verify vulnerability disclosure | |
| echo "⚠️ Verifying vulnerability disclosure..." | |
| if gh attestation verify "$ARTIFACT_FILE" \ | |
| --repo ${{ github.repository }} \ | |
| --predicate-type "https://slsa.dev/spec/v1.1/provenance" 2>/dev/null; then | |
| echo "✅ Vulnerability disclosure: VERIFIED" | |
| else | |
| echo "⚠️ Vulnerability disclosure: Could not verify (may be processing)" | |
| fi | |
| echo "" | |
| echo "📊 Attestation Summary:" | |
| echo "- Artifact: $ARTIFACT_FILE" | |
| echo "- SHA256: ${{ steps.build.outputs.artifact_hash }}" | |
| echo "- Security alerts found: $(echo '${{ steps.codeql-results.outputs.security_summary }}' | jq -r '.total_alerts // "unknown"')" | |
| echo "- Workflow: unified-build-attest.yml" | |
| echo "- Run ID: ${{ github.run_id }}" | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| - name: Generate step summary | |
| run: | | |
| echo "## 🏗️ Build and Security Attestation Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### 📦 Artifact Information" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Name**: \`${{ steps.build.outputs.artifact_name }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- **SHA256**: \`${{ steps.build.outputs.artifact_hash }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Size**: $(ls -lh ${{ steps.build.outputs.artifact_name }} | awk '{print $5}')" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### 🔍 Security Scan Results" >> $GITHUB_STEP_SUMMARY | |
| TOTAL_ALERTS=$(echo '${{ steps.codeql-results.outputs.security_summary }}' | jq -r '.total_alerts // 0') | |
| echo "- **Total Alerts**: $TOTAL_ALERTS" >> $GITHUB_STEP_SUMMARY | |
| if [ "$TOTAL_ALERTS" -gt 0 ]; then | |
| echo "- **Severity Breakdown**:" >> $GITHUB_STEP_SUMMARY | |
| echo '${{ steps.codeql-results.outputs.security_summary }}' | jq -r '.severity_counts | " - Critical: \(.critical), High: \(.high), Medium: \(.medium), Low: \(.low)"' >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### 🔐 Attestations Created" >> $GITHUB_STEP_SUMMARY | |
| echo "1. **SLSA Build Provenance** - Standard build provenance per SLSA specification" >> $GITHUB_STEP_SUMMARY | |
| echo "2. **Security Assessment** - CodeQL scan results and security analysis" >> $GITHUB_STEP_SUMMARY | |
| echo "3. **Vulnerability Disclosure** - Educational purpose and vulnerability warnings" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### 🔍 Verification" >> $GITHUB_STEP_SUMMARY | |
| echo "To verify these attestations:" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY | |
| echo "gh attestation verify ${{ steps.build.outputs.artifact_name }} --repo ${{ github.repository }}" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`" >> $GITHUB_STEP_SUMMARY |