diff --git a/.github/workflows/supply-chain-pr.yml b/.github/workflows/supply-chain-pr.yml index 22e377e8..8371a5ec 100644 --- a/.github/workflows/supply-chain-pr.yml +++ b/.github/workflows/supply-chain-pr.yml @@ -53,23 +53,26 @@ jobs: id: pr-number env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + EVENT_NAME: ${{ github.event_name }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha || github.event.pull_request.head.sha || github.sha }} + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} + WORKFLOW_RUN_EVENT: ${{ github.event.workflow_run.event }} + REPO_OWNER: ${{ github.repository_owner }} + REPO_NAME: ${{ github.repository }} run: | - if [[ -n "${{ inputs.pr_number }}" ]]; then - echo "pr_number=${{ inputs.pr_number }}" >> "$GITHUB_OUTPUT" - echo "📋 Using manually provided PR number: ${{ inputs.pr_number }}" + if [[ -n "${INPUT_PR_NUMBER}" ]]; then + echo "pr_number=${INPUT_PR_NUMBER}" >> "$GITHUB_OUTPUT" + echo "📋 Using manually provided PR number: ${INPUT_PR_NUMBER}" exit 0 fi - if [[ "${{ github.event_name }}" != "workflow_run" && "${{ github.event_name }}" != "push" && "${{ github.event_name }}" != "pull_request" ]]; then + if [[ "${EVENT_NAME}" != "workflow_run" && "${EVENT_NAME}" != "push" && "${EVENT_NAME}" != "pull_request" ]]; then echo "❌ No PR number provided and not triggered by workflow_run/push/pr" echo "pr_number=" >> "$GITHUB_OUTPUT" exit 0 fi - # Extract PR number from context - HEAD_SHA="${{ github.event.workflow_run.head_sha || github.event.pull_request.head.sha || github.sha }}" - HEAD_BRANCH="${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }}" - echo "🔍 Looking for PR with head SHA: ${HEAD_SHA}" echo "🔍 Head branch: ${HEAD_BRANCH}" @@ -77,7 +80,7 @@ jobs: PR_NUMBER=$(gh api \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ - "/repos/${{ github.repository }}/pulls?state=open&head=${{ github.repository_owner }}:${HEAD_BRANCH}" \ + "/repos/${REPO_NAME}/pulls?state=open&head=${REPO_OWNER}:${HEAD_BRANCH}" \ --jq '.[0].number // empty' 2>/dev/null || echo "") if [[ -z "${PR_NUMBER}" ]]; then @@ -85,7 +88,7 @@ jobs: PR_NUMBER=$(gh api \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ - "/repos/${{ github.repository }}/commits/${HEAD_SHA}/pulls" \ + "/repos/${REPO_NAME}/commits/${HEAD_SHA}/pulls" \ --jq '.[0].number // empty' 2>/dev/null || echo "") fi @@ -98,9 +101,8 @@ jobs: fi # Check if this is a push event (not a PR) - if [[ "${{ github.event.workflow_run.event }}" == "push" || "${{ github.event_name }}" == "push" ]]; then + if [[ "${WORKFLOW_RUN_EVENT}" == "push" || "${EVENT_NAME}" == "push" ]]; then echo "is_push=true" >> "$GITHUB_OUTPUT" - HEAD_BRANCH="${{ github.event.workflow_run.head_branch || github.ref_name }}" echo "✅ Detected push build from branch: ${HEAD_BRANCH}" else echo "is_push=false" >> "$GITHUB_OUTPUT" @@ -108,28 +110,32 @@ jobs: - name: Sanitize branch name id: sanitize + env: + BRANCH_NAME: ${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} run: | # Sanitize branch name for use in artifact names # Replace / with - to avoid invalid reference format errors - BRANCH="${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }}" - SANITIZED=$(echo "$BRANCH" | tr '/' '-') + SANITIZED=$(echo "$BRANCH_NAME" | tr '/' '-') echo "branch=${SANITIZED}" >> "$GITHUB_OUTPUT" - echo "📋 Sanitized branch name: ${BRANCH} -> ${SANITIZED}" + echo "📋 Sanitized branch name: ${BRANCH_NAME} -> ${SANITIZED}" - name: Check for PR image artifact id: check-artifact if: github.event_name == 'workflow_run' && (steps.pr-number.outputs.pr_number != '' || steps.pr-number.outputs.is_push == 'true') env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + IS_PUSH: ${{ steps.pr-number.outputs.is_push }} + PR_NUMBER: ${{ steps.pr-number.outputs.pr_number }} + RUN_ID: ${{ github.event.workflow_run.id }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha || github.event.pull_request.head.sha || github.sha }} + REPO_NAME: ${{ github.repository }} run: | # Determine artifact name based on event type - if [[ "${{ steps.pr-number.outputs.is_push }}" == "true" ]]; then + if [[ "${IS_PUSH}" == "true" ]]; then ARTIFACT_NAME="push-image" else - PR_NUMBER="${{ steps.pr-number.outputs.pr_number }}" ARTIFACT_NAME="pr-image-${PR_NUMBER}" fi - RUN_ID="${{ github.event.workflow_run.id }}" echo "🔍 Looking for artifact: ${ARTIFACT_NAME}" @@ -138,18 +144,17 @@ jobs: ARTIFACT_ID=$(gh api \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ - "/repos/${{ github.repository }}/actions/runs/${RUN_ID}/artifacts" \ + "/repos/${REPO_NAME}/actions/runs/${RUN_ID}/artifacts" \ --jq ".artifacts[] | select(.name == \"${ARTIFACT_NAME}\") | .id" 2>/dev/null || echo "") else # If RUN_ID is empty (push/pr trigger), try to find a recent successful run for this SHA - HEAD_SHA="${{ github.event.workflow_run.head_sha || github.event.pull_request.head.sha || github.sha }}" echo "🔍 Searching for workflow run for SHA: ${HEAD_SHA}" # Retry a few times as the run might be just starting or finishing for i in {1..3}; do RUN_ID=$(gh api \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ - "/repos/${{ github.repository }}/actions/workflows/docker-build.yml/runs?head_sha=${HEAD_SHA}&status=success&per_page=1" \ + "/repos/${REPO_NAME}/actions/workflows/docker-build.yml/runs?head_sha=${HEAD_SHA}&status=success&per_page=1" \ --jq '.workflow_runs[0].id // empty' 2>/dev/null || echo "") if [[ -n "${RUN_ID}" ]]; then echo "✅ Found Run ID: ${RUN_ID}" @@ -163,7 +168,7 @@ jobs: ARTIFACT_ID=$(gh api \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ - "/repos/${{ github.repository }}/actions/runs/${RUN_ID}/artifacts" \ + "/repos/${REPO_NAME}/actions/runs/${RUN_ID}/artifacts" \ --jq ".artifacts[] | select(.name == \"${ARTIFACT_NAME}\") | .id" 2>/dev/null || echo "") fi fi @@ -174,7 +179,7 @@ jobs: ARTIFACT_ID=$(gh api \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ - "/repos/${{ github.repository }}/actions/artifacts?name=${ARTIFACT_NAME}" \ + "/repos/${REPO_NAME}/actions/artifacts?name=${ARTIFACT_NAME}" \ --jq '.artifacts[0].id // empty' 2>/dev/null || echo "") fi @@ -197,26 +202,26 @@ jobs: exit 0 - name: Download PR image artifact - if: github.event_name == 'workflow_run' && steps.set-target.outputs.image_name != '' + if: github.event_name == 'workflow_run' && steps.check-artifact.outputs.artifact_found == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ARTIFACT_ID: ${{ steps.check-artifact.outputs.artifact_id }} + ARTIFACT_NAME: ${{ steps.check-artifact.outputs.artifact_name }} + REPO_NAME: ${{ github.repository }} run: | - ARTIFACT_ID="${{ steps.check-artifact.outputs.artifact_id }}" - ARTIFACT_NAME="${{ steps.check-artifact.outputs.artifact_name }}" - echo "📦 Downloading artifact: ${ARTIFACT_NAME}" gh api \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ - "/repos/${{ github.repository }}/actions/artifacts/${ARTIFACT_ID}/zip" \ + "/repos/${REPO_NAME}/actions/artifacts/${ARTIFACT_ID}/zip" \ > artifact.zip unzip -o artifact.zip echo "✅ Artifact downloaded and extracted" - name: Load Docker image (Artifact) - if: github.event_name == 'workflow_run' && steps.set-target.outputs.image_name != '' + if: github.event_name == 'workflow_run' && steps.check-artifact.outputs.artifact_found == 'true' id: load-image-artifact run: | if [[ ! -f "charon-pr-image.tar" ]]; then @@ -291,34 +296,46 @@ jobs: fail-build: false output-format: json + - name: Debug Output Files + if: steps.set-target.outputs.image_name != '' + run: | + echo "📂 Listing workspace files:" + ls -la + - name: Process vulnerability results if: steps.set-target.outputs.image_name != '' id: vuln-summary run: | # The scan-action outputs results.json and results.sarif - # Rename for consistency with downstream steps - if [[ -f results.json ]]; then - mv results.json grype-results.json - fi - if [[ -f results.sarif ]]; then - mv results.sarif grype-results.sarif + JSON_RESULT="results.json" + SARIF_RESULT="results.sarif" + + # Verify scan actually produced output + if [[ ! -f "$JSON_RESULT" ]]; then + echo "❌ Error: $JSON_RESULT not found!" + echo "Available files:" + ls -la + exit 1 fi - # Count vulnerabilities by severity - if [[ -f grype-results.json ]]; then - CRITICAL_COUNT=$(jq '[.matches[] | select(.vulnerability.severity == "Critical")] | length' grype-results.json 2>/dev/null || echo "0") - HIGH_COUNT=$(jq '[.matches[] | select(.vulnerability.severity == "High")] | length' grype-results.json 2>/dev/null || echo "0") - MEDIUM_COUNT=$(jq '[.matches[] | select(.vulnerability.severity == "Medium")] | length' grype-results.json 2>/dev/null || echo "0") - LOW_COUNT=$(jq '[.matches[] | select(.vulnerability.severity == "Low")] | length' grype-results.json 2>/dev/null || echo "0") - TOTAL_COUNT=$(jq '.matches | length' grype-results.json 2>/dev/null || echo "0") - else - CRITICAL_COUNT=0 - HIGH_COUNT=0 - MEDIUM_COUNT=0 - LOW_COUNT=0 - TOTAL_COUNT=0 + # Rename for consistency with downstream steps + mv "$JSON_RESULT" grype-results.json + + if [[ -f "$SARIF_RESULT" ]]; then + mv "$SARIF_RESULT" grype-results.sarif fi + # Debug content (head) + echo "📄 Grype JSON Preview:" + head -n 20 grype-results.json + + # Count vulnerabilities by severity - strict failing if file is missing (already checked above) + CRITICAL_COUNT=$(jq '[.matches[] | select(.vulnerability.severity == "Critical")] | length' grype-results.json 2>/dev/null || echo "0") + HIGH_COUNT=$(jq '[.matches[] | select(.vulnerability.severity == "High")] | length' grype-results.json 2>/dev/null || echo "0") + MEDIUM_COUNT=$(jq '[.matches[] | select(.vulnerability.severity == "Medium")] | length' grype-results.json 2>/dev/null || echo "0") + LOW_COUNT=$(jq '[.matches[] | select(.vulnerability.severity == "Low")] | length' grype-results.json 2>/dev/null || echo "0") + TOTAL_COUNT=$(jq '.matches | length' grype-results.json 2>/dev/null || echo "0") + echo "critical_count=${CRITICAL_COUNT}" >> "$GITHUB_OUTPUT" echo "high_count=${HIGH_COUNT}" >> "$GITHUB_OUTPUT" echo "medium_count=${MEDIUM_COUNT}" >> "$GITHUB_OUTPUT" @@ -431,7 +448,7 @@ jobs: - name: Fail on critical vulnerabilities if: steps.set-target.outputs.image_name != '' run: | - CRITICAL_COUNT="${{ steps.grype-scan.outputs.critical_count }}" + CRITICAL_COUNT="${{ steps.vuln-summary.outputs.critical_count }}" if [[ "${CRITICAL_COUNT}" -gt 0 ]]; then echo "🚨 Found ${CRITICAL_COUNT} CRITICAL vulnerabilities!" diff --git a/docs/plans/supply_chain_fix.md b/docs/plans/supply_chain_fix.md new file mode 100644 index 00000000..66dafd90 --- /dev/null +++ b/docs/plans/supply_chain_fix.md @@ -0,0 +1,110 @@ +# Plan: Fix Supply Chain Vulnerability Reporting + +## Objective +Fix the `supply-chain-pr.yml` workflow where PR comments report 0 vulnerabilities despite known CVEs, and ensure the workflow correctly fails on critical vulnerabilities. + +## Context +The current workflow uses `anchore/scan-action` to scan for vulnerabilities. However, there are potential issues with: +1. **Output File Handling:** The workflow assumes `results.json` is created, but `anchore/scan-action` with `output-format: json` might not produce this file by default without an explicit `output-file` parameter or capturing output. +2. **Parsing Logic:** If the file is missing, the `jq` parsing gracefully falls back to 0, masking the error. +3. **Failure Condition:** The failure step references `${{ steps.grype-scan.outputs.critical_count }}`, which likely does not exist on the `anchore/scan-action` step. It should reference the calculated output from the parsing step. + +## Research & Diagnosis Steps + +### 1. Debug Output paths +We need to verify if `results.json` is actually generated. +- **Action:** Add a step to list files in the workspace immediately after the scan. +- **Action:** Add a debug `cat` of the results file if it exists, or header of it. + +### 2. Verify `anchore/scan-action` behavior +The `anchore/scan-action` (v7.3.2) documentation suggests that `output-format` is used, but typically it defaults to `results.[format]`. However, explicit `output-file` prevents ambiguity. + +## Implementation Plan + +### Phase 1: Robust Path & Debugging +1. **Explicit Output File:** Modify the `anchore/scan-action` step to explicitly set `output-format: json` AND likely we should try to rely on the default behavior but *check* it. + *Actually, better practice:* The action supports `output-format` as a list. If we want a file, we usually just look for it. + *Correction:* We will explicitly check for the file and fail if missing, rather than defaulting to 0. +2. **List Files:** Add `ls -la` after scan to see exactly what files are created. + +### Phase 2: Fix Logic Errors +1. **Update "Fail on critical vulnerabilities" step**: + - Change `${{ steps.grype-scan.outputs.critical_count }}` to `${{ steps.vuln-summary.outputs.critical_count }}`. +2. **Robust `jq` parsing**: + - In `Process vulnerability results`, explicitly check for existence of `results.json` (or whatever the action outputs). + - If missing, **EXIT 1** instead of setting counts to 0. This forces us to fix the path issue rather than silently passing. + - Use `tee` or `cat` to print the first few lines of the JSON to stdout for debugging logs. + +### Phase 3: Validation +1. Run the workflow on a PR (or simulate via push). +2. Verify the PR comment shows actual numbers. +3. Verify the workflow fails if critical vulnerabilities are found (or we can lower the threshold to test). + +## Detailed Changes + +### `supply-chain-pr.yml` + +```yaml + # ... inside steps ... + + - name: Scan for vulnerabilities + if: steps.set-target.outputs.image_name != '' + uses: anchore/scan-action@7037fa011853d5a11690026fb85feee79f4c946c # v7.3.2 + id: grype-scan + with: + sbom: sbom.cyclonedx.json + fail-build: false + output-format: json + # We might need explicit output selection implies asking for 'json' creates 'results.json' + + - name: Debug Output Files + if: steps.set-target.outputs.image_name != '' + run: | + echo "📂 Listing workspace files:" + ls -la + + - name: Process vulnerability results + if: steps.set-target.outputs.image_name != '' + id: vuln-summary + run: | + # The scan-action output behavior verification + JSON_RESULT="results.json" + SARIF_RESULT="results.sarif" + + # [NEW] Check if scan actually produced output + if [[ ! -f "$JSON_RESULT" ]]; then + echo "❌ Error: $JSON_RESULT not found!" + echo "Available files:" + ls -la + exit 1 + fi + + mv "$JSON_RESULT" grype-results.json + + # Debug content (head) + echo "📄 Grype JSON Preview:" + head -n 20 grype-results.json + + # ... existing renaming for sarif ... + + # ... existing jq logic, but remove 'else' block for missing file since we exit above ... + + # ... + + - name: Fail on critical vulnerabilities + if: steps.set-target.outputs.image_name != '' + run: | + # [FIX] Use the output from the summary step, NOT the scan step + CRITICAL_COUNT="${{ steps.vuln-summary.outputs.critical_count }}" + + if [[ "${CRITICAL_COUNT}" -gt 0 ]]; then + echo "🚨 Found ${CRITICAL_COUNT} CRITICAL vulnerabilities!" + echo "Please review the vulnerability report and address critical issues before merging." + exit 1 + fi +``` + +### Acceptance Criteria +- [ ] Workflow "Fail on critical vulnerabilities" uses `steps.vuln-summary.outputs.critical_count`. +- [ ] `Process vulnerability results` step fails if the scan output file is missing. +- [ ] Debug logging (ls -la) is present to confirm file placement. diff --git a/docs/reports/qa_report.md b/docs/reports/qa_report.md index a6e9cf34..bb2a2b3c 100644 --- a/docs/reports/qa_report.md +++ b/docs/reports/qa_report.md @@ -1,3 +1,47 @@ +# QA Report - Supply Chain Workflow Audit + +**Date:** February 6, 2026 +**Target:** `.github/workflows/supply-chain-pr.yml` +**Trigger:** Manual Lint Request +**Auditor:** QA Security Engineer (Gemini 3 Pro) + +## 1. Executive Summary + +A manual audit and linting session was performed on the `supply-chain-pr.yml` workflow. Critical logic errors were identified that would have prevented the workflow from correctly downloading artifacts during a PR event. Security vulnerabilities related to script injection were also mitigated. + +**Status:** 🟡 **REMEDIATED** (Issues found and fixed) + +## 2. Findings & Remediation + +### A. Logic Error: Circular Dependency +* **Severity:** 🔴 **CRITICAL** +* **Issue:** The steps "Download PR image artifact" and "Load Docker image" conditionally depended on `steps.set-target.outputs.image_name`. However, the `set-target` step is defined **after** these steps in the workflow execution order. +* **Impact:** These steps would invariably evaluate to `false` or crash, causing the workflow to skip image verification for PRs. +* **Fix:** Updated the conditions to depend on `steps.check-artifact.outputs.artifact_found == 'true'`, which is correctly populated by the preceding step. + +### B. Security: Script Injection Risk +* **Severity:** 🟠 **HIGH** +* **Issue:** User-controlled inputs (`github.head_ref`, `inputs.pr_number`) were used directly in inline scripts (`run` blocks). +* **Impact:** A malicious branch name or PR number could potentially execute arbitrary commands in the runner environment. +* **Fix:** Mapped all user inputs to environment variables (`env` block) and referenced them via shell variables (e.g., `${BRANCH_NAME}`) instead of template injection. + +### C. Syntax & Linting +* **Tool:** `actionlint` +* **Result:** Identified the logic errors and security warnings mentioned above. +* **Status:** All reported errors logic/security errors addressed. Shellcheck style warnings (redirects) noted but lower priority. + +### D. Security Scan (Trivy) +* **Tool:** `trivy fs` +* **Command:** `trivy fs --scanners secret,misconfig .github/workflows/supply-chain-pr.yml` +* **Result:** ✅ **PASS** + * No secrets detected. + * No infrastructure misconfigurations detected by Trivy policies. + +## 3. Verification +The workflow file has been updated with the fixes. It is recommended to trigger a test run (via PR or workflow_dispatch) to verify the runtime behavior. + +--- + # QA Report - Phase 6 Audit (Playwright Config Update) **Date:** February 6, 2026 diff --git a/trivy-report.json b/trivy-report.json new file mode 100644 index 00000000..9edeca44 --- /dev/null +++ b/trivy-report.json @@ -0,0 +1,10 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "0.69.1" + }, + "ReportID": "019c31f7-70d6-7974-912c-81d08eba4356", + "CreatedAt": "2026-02-06T08:00:25.814622916Z", + "ArtifactName": ".github/workflows/supply-chain-pr.yml", + "ArtifactType": "filesystem" +}