Skip to content

Unified Build and Attestation #17

Unified Build and Attestation

Unified Build and Attestation #17

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