diff --git a/.github/workflows/pr-security-scan.yml b/.github/workflows/pr-security-scan.yml index 74054453..c9288c95 100644 --- a/.github/workflows/pr-security-scan.yml +++ b/.github/workflows/pr-security-scan.yml @@ -76,8 +76,34 @@ on: description: 'Use the component working_dir as Docker build context instead of repo root. Useful for independent modules (e.g., tools with their own go.mod).' type: boolean default: false + enable_codeql: + description: 'Enable CodeQL static analysis. Requires codeql_languages to be set.' + type: boolean + default: false + codeql_languages: + description: 'Languages to analyze with CodeQL (comma-separated, e.g., "go", "javascript-typescript", "actions")' + type: string + required: false + default: '' + codeql_fail_on_findings: + description: 'Fail the workflow when CodeQL detects security issues' + type: boolean + default: true + codeql_upload_sarif: + description: 'Upload CodeQL SARIF results to the GitHub Security tab. Requires Code Security (GHAS) enabled on the repo.' + type: boolean + default: false + enable_prerelease_check: + description: 'Block dependencies pinned to pre-release versions (-beta, -rc)' + type: boolean + default: true + prerelease_block_branches: + description: 'Comma-separated list of PR target branches where pre-release versions cause a hard failure. On other branches, findings are reported as warnings only.' + type: string + default: 'release-candidate,main' permissions: + actions: read # Required for CodeQL status reporting id-token: write # Required for OIDC authentication contents: read # Required to checkout the repository pull-requests: write # Allows commenting on PRs @@ -100,7 +126,7 @@ jobs: # ----------------- Detect Changes & Build Matrix ----------------- - name: Get changed paths id: changed-paths - uses: LerianStudio/github-actions-shared-workflows/src/config/changed-paths@v1.18.0 + uses: LerianStudio/github-actions-shared-workflows/src/config/changed-paths@v1.23.1 with: filter-paths: ${{ inputs.filter_paths }} shared-paths: ${{ inputs.shared_paths }} @@ -150,7 +176,7 @@ jobs: - name: Trivy Filesystem Scan id: fs-scan if: always() - uses: LerianStudio/github-actions-shared-workflows/src/security/trivy-fs-scan@v1.18.0 + uses: LerianStudio/github-actions-shared-workflows/src/security/trivy-fs-scan@v1.23.1 with: scan-ref: ${{ matrix.working_dir }} app-name: ${{ env.APP_NAME }} @@ -175,7 +201,7 @@ jobs: - name: Trivy Image Scan id: image-scan if: always() && inputs.enable_docker_scan - uses: LerianStudio/github-actions-shared-workflows/src/security/trivy-image-scan@v1.18.0 + uses: LerianStudio/github-actions-shared-workflows/src/security/trivy-image-scan@v1.23.1 with: image-ref: '${{ env.DOCKERHUB_ORG }}/${{ env.APP_NAME }}:pr-scan-${{ github.sha }}' app-name: ${{ env.APP_NAME }} @@ -185,15 +211,24 @@ jobs: - name: Dockerfile Compliance Checks id: dockerfile-checks if: always() && inputs.enable_docker_scan && inputs.enable_health_score - uses: LerianStudio/github-actions-shared-workflows/src/security/dockerfile-checks@v1.18.0 + uses: LerianStudio/github-actions-shared-workflows/src/security/dockerfile-checks@v1.23.1 with: dockerfile-path: ${{ env.DOCKERFILE_PATH }} + # ----------------- Pre-release Version Gate ----------------- + - name: Pre-release Version Check + id: prerelease-check + if: always() && inputs.enable_prerelease_check + uses: LerianStudio/github-actions-shared-workflows/src/security/prerelease-check@feat/pr-security-scan-codeql-prerelease + with: + scan-ref: ${{ matrix.working_dir }} + app-name: ${{ env.APP_NAME }} + # ----------------- Results & Security Gate ----------------- - name: Post Security Scan Results to PR id: post-results if: always() && github.event_name == 'pull_request' - uses: LerianStudio/github-actions-shared-workflows/src/security/pr-security-reporter@v1.18.0 + uses: LerianStudio/github-actions-shared-workflows/src/security/pr-security-reporter@v1.23.1 with: github-token: ${{ secrets.MANAGE_TOKEN || secrets.GITHUB_TOKEN }} app-name: ${{ env.APP_NAME }} @@ -202,32 +237,102 @@ jobs: dockerfile-has-non-root-user: ${{ steps.dockerfile-checks.outputs.has-non-root-user || 'false' }} fail-on-findings: 'true' - ## To be fixed - # - name: Upload Secret Scan Results - Repository (SARIF) to GitHub Security Tab - # uses: github/codeql-action/upload-sarif@v3 - # if: always() - # continue-on-error: true - # with: - # sarif_file: 'trivy-secret-scan-repo-${{ env.APP_NAME }}.sarif' - - # - name: Upload Vulnerability Scan Results - Docker Image (SARIF) to GitHub Security Tab - # uses: github/codeql-action/upload-sarif@v3 - # if: always() - # continue-on-error: true - # with: - # sarif_file: 'trivy-vulnerability-scan-docker-${{ env.APP_NAME }}.sarif' + - name: Gate - Fail on Pre-release Versions + if: always() && inputs.enable_prerelease_check && steps.prerelease-check.outputs.has-findings == 'true' + env: + BLOCK_BRANCHES: ${{ inputs.prerelease_block_branches }} + TARGET_BRANCH: ${{ github.base_ref }} + FINDINGS_COUNT: ${{ steps.prerelease-check.outputs.findings-count }} + run: | + SHOULD_BLOCK=false + IFS=',' read -ra BRANCHES <<< "$BLOCK_BRANCHES" + for branch in "${BRANCHES[@]}"; do + branch=$(echo "$branch" | xargs) + if [ "$TARGET_BRANCH" = "$branch" ]; then + SHOULD_BLOCK=true + break + fi + done + + if [ "$SHOULD_BLOCK" = "true" ]; then + echo "::error::Pre-release version pins detected ($FINDINGS_COUNT finding(s)). Target branch '$TARGET_BRANCH' does not allow beta or release candidate dependencies." + exit 1 + else + echo "::warning::Pre-release version pins detected ($FINDINGS_COUNT finding(s)). Allowed on '$TARGET_BRANCH' — will be blocked on: $BLOCK_BRANCHES." + fi + + # ----------------- CodeQL Analysis ----------------- + codeql_scan: + needs: prepare_matrix + if: inputs.enable_codeql && inputs.codeql_languages != '' && needs.prepare_matrix.outputs.matrix != '[]' + runs-on: ${{ inputs.runner_type }} + steps: + # ----------------- Setup ----------------- + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Extract changed paths from matrix + id: extract-paths + env: + MATRIX: ${{ needs.prepare_matrix.outputs.matrix }} + run: | + PATHS=$(echo "$MATRIX" | jq -r '.[].working_dir' | paste -sd ',' -) + echo "paths=$PATHS" >> "$GITHUB_OUTPUT" + + # ----------------- CodeQL Config ----------------- + - name: Generate CodeQL Config + id: codeql-config + uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-config@feat/pr-security-scan-codeql-prerelease + with: + changed-paths: ${{ steps.extract-paths.outputs.paths }} + + # ----------------- CodeQL Analysis ----------------- + - name: Initialize CodeQL + if: steps.codeql-config.outputs.skip != 'true' + uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-init@feat/pr-security-scan-codeql-prerelease + with: + languages: ${{ inputs.codeql_languages }} + config-file: ${{ steps.codeql-config.outputs.config-file }} + + - name: Configure private Go modules access + if: steps.codeql-config.outputs.skip != 'true' + env: + TOKEN: ${{ secrets.MANAGE_TOKEN || secrets.GITHUB_TOKEN }} + run: | + git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/" + echo "GOPRIVATE=github.com/LerianStudio/*" >> "$GITHUB_ENV" + + - name: Autobuild + if: steps.codeql-config.outputs.skip != 'true' + uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4 + + - name: Perform CodeQL Analysis + if: steps.codeql-config.outputs.skip != 'true' + uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-analyze@feat/pr-security-scan-codeql-prerelease + with: + category: '/language:${{ inputs.codeql_languages }}' + upload: ${{ inputs.codeql_upload_sarif }} + + # ----------------- Results & Security Gate ----------------- + - name: Post CodeQL Results to PR + if: always() && github.event_name == 'pull_request' && steps.codeql-config.outputs.skip != 'true' + uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-reporter@feat/pr-security-scan-codeql-prerelease + with: + github-token: ${{ secrets.MANAGE_TOKEN || secrets.GITHUB_TOKEN }} + languages: ${{ inputs.codeql_languages }} + fail-on-findings: ${{ inputs.codeql_fail_on_findings }} # ----------------- Slack Notification ----------------- notify: name: Notify - needs: [prepare_matrix, security_scan] + needs: [prepare_matrix, security_scan, codeql_scan] if: always() && needs.prepare_matrix.outputs.matrix != '[]' runs-on: ${{ inputs.runner_type }} steps: - name: Slack Notification - uses: LerianStudio/github-actions-shared-workflows/src/notify/slack-notify@v1.18.0 + uses: LerianStudio/github-actions-shared-workflows/src/notify/slack-notify@v1.23.1 with: webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} - status: ${{ needs.security_scan.result }} + status: ${{ (needs.security_scan.result == 'failure' || needs.codeql_scan.result == 'failure') && 'failure' || needs.security_scan.result }} workflow-name: "PR Security Scan" - failed-jobs: ${{ needs.security_scan.result == 'failure' && 'Security Scan' || '' }} + failed-jobs: ${{ needs.security_scan.result == 'failure' && needs.codeql_scan.result == 'failure' && 'Security Scan, CodeQL Scan' || needs.security_scan.result == 'failure' && 'Security Scan' || needs.codeql_scan.result == 'failure' && 'CodeQL Scan' || '' }} diff --git a/docs/pr-security-scan-workflow.md b/docs/pr-security-scan-workflow.md index 101c1109..2b464c40 100644 --- a/docs/pr-security-scan-workflow.md +++ b/docs/pr-security-scan-workflow.md @@ -6,6 +6,8 @@ Reusable workflow for comprehensive security scanning on pull requests. Supports - **Secret scanning**: Trivy filesystem scan for exposed secrets (scans only changed component folder) - **Vulnerability scanning**: Docker image vulnerability detection (optional) +- **CodeQL static analysis**: GitHub CodeQL for semantic code analysis (opt-in via `enable_codeql`) +- **Pre-release version gate**: Blocks dependencies pinned to `-beta` or `-rc` versions (enabled by default) - **CLI/Non-Docker support**: Skip Docker scanning for projects without Dockerfile via `enable_docker_scan: false` - **Monorepo support**: Automatic detection of changed components - **Component-scoped scanning**: Only scans the specific component folder that changed, not entire repo @@ -138,9 +140,9 @@ This will: - ❌ Skip Docker vulnerability scanning - ❌ Skip Docker Scout analysis -### Docker Scout Analysis +### With CodeQL Analysis -Enable Docker Scout for additional vulnerability scoring and CVE analysis on your Docker images: +Enable CodeQL for semantic static analysis on top of the standard security scans: ```yaml name: PR Security Scan @@ -153,16 +155,29 @@ jobs: uses: LerianStudio/github-actions-shared-workflows/.github/workflows/pr-security-scan.yml@v1.0.0 with: runner_type: "blacksmith-4vcpu-ubuntu-2404" - enable_docker_scout: true + enable_codeql: true + codeql_languages: 'go' secrets: inherit ``` -This will run all standard scans plus Docker Scout quickview and CVE analysis. +This will run all standard scans plus CodeQL analysis scoped to changed paths. Results are posted as a separate PR comment and uploaded to the GitHub Security tab. -**Requirements:** -- Docker Hub account with Scout access (Free, Team, or Business) -- `DOCKER_USERNAME` and `DOCKER_PASSWORD` secrets configured -- `enable_docker_scan` must also be `true` (default) — Scout reuses the same image built for Trivy scanning +**Supported languages:** `go`, `javascript-typescript`, `actions`, `python`, `java-kotlin`, `csharp`, `ruby`, `swift`, `cpp` + +### With Pre-release Version Gate + +Pre-release checks are enabled by default. To disable: + +```yaml +jobs: + security-scan: + uses: LerianStudio/github-actions-shared-workflows/.github/workflows/pr-security-scan.yml@v1.0.0 + with: + enable_prerelease_check: false + secrets: inherit +``` + +When enabled, the workflow scans `go.mod`, `package.json`, and `Dockerfile` for version pins containing `-beta` or `-rc` suffixes and fails the PR if any are found. ## Inputs @@ -177,7 +192,13 @@ This will run all standard scans plus Docker Scout quickview and CVE analysis. | `docker_registry` | string | `docker.io` | Docker registry URL | | `dockerfile_name` | string | `Dockerfile` | Name of the Dockerfile | | `enable_docker_scan` | boolean | `true` | Enable Docker image build and vulnerability scanning. Set to `false` for projects without Dockerfile (e.g., CLI tools) | -| `enable_docker_scout` | boolean | `false` | Enable Docker Scout image analysis for vulnerability scoring. Requires Docker Hub with Scout access | +| `enable_health_score` | boolean | `true` | Enable Docker Hub Health Score compliance checks (non-root user, CVEs, licenses) | +| `enable_codeql` | boolean | `false` | Enable CodeQL static analysis. Requires `codeql_languages` to be set | +| `codeql_languages` | string | `''` | Languages to analyze with CodeQL (comma-separated, e.g., `go`, `javascript-typescript`, `actions`) | +| `codeql_fail_on_findings` | boolean | `true` | Fail the workflow when CodeQL detects security issues | +| `codeql_upload_sarif` | boolean | `false` | Upload CodeQL SARIF results to the GitHub Security tab. Requires Code Security (GHAS) enabled on the repo | +| `enable_prerelease_check` | boolean | `true` | Block dependencies pinned to pre-release versions (`-beta`, `-rc`) | +| `prerelease_block_branches` | string | `release-candidate,main` | Comma-separated PR target branches where pre-release versions cause a hard failure. On other branches, findings are reported as warnings only | ## Secrets @@ -219,14 +240,26 @@ For each component in the matrix: 1. **Docker Login**: Authenticate to registry (avoids rate limits) 2. **Checkout Repository**: Clone the code 3. **Setup Docker Buildx**: Enable multi-platform builds *(skipped if `enable_docker_scan: false`)* -4. **Trivy Secret Scan (Table)**: Scan filesystem for secrets - **fails on detection** -5. **Trivy Secret Scan (SARIF)**: Generate SARIF report -6. **Build Docker Image**: Build image for vulnerability scanning *(skipped if `enable_docker_scan: false`)* -7. **Trivy Vulnerability Scan (Table)**: Scan image for vulnerabilities *(skipped if `enable_docker_scan: false`)* -8. **Trivy Vulnerability Scan (SARIF)**: Generate SARIF report *(skipped if `enable_docker_scan: false`)* -9. **Docker Scout Analysis**: Quickview and CVE analysis *(skipped unless `enable_docker_scout: true` AND `enable_docker_scan: true`)* +4. **Trivy Filesystem Scan**: Scan filesystem for secrets and vulnerabilities +5. **Build Docker Image**: Build image for vulnerability scanning *(skipped if `enable_docker_scan: false`)* +6. **Trivy Image Scan**: Scan image for vulnerabilities and licenses *(skipped if `enable_docker_scan: false`)* +7. **Dockerfile Compliance Checks**: Non-root user and health score checks *(skipped unless `enable_health_score: true` AND `enable_docker_scan: true`)* +8. **Pre-release Version Check**: Scan for `-beta`/`-rc` version pins *(skipped if `enable_prerelease_check: false`)* +9. **Post Security Scan Results**: PR comment with consolidated findings + +> **Note**: When `enable_docker_scan: false`, only filesystem scanning and pre-release checks run. -> **Note**: When `enable_docker_scan: false`, only filesystem secret scanning runs. This is useful for CLI tools and projects without Dockerfiles. +### Job 3: codeql_scan *(optional)* + +Runs when `enable_codeql: true` and `codeql_languages` is set: + +1. **Checkout Repository**: Clone the code +2. **Extract Changed Paths**: Derive scoped paths from the component matrix +3. **Generate CodeQL Config**: Scope analysis to changed paths +4. **Initialize CodeQL**: Set up CodeQL with configured languages and query suite +5. **Autobuild**: Automatically build the project for compiled languages +6. **Perform CodeQL Analysis**: Run semantic analysis and upload SARIF +7. **Post CodeQL Results**: PR comment with findings table and security gate ## Security Scans @@ -259,6 +292,24 @@ For each component in the matrix: **Exit behavior**: `exit-code: 0` (informative only, doesn't fail workflow) +### CodeQL Analysis + +**What it does**: Runs GitHub CodeQL semantic analysis for security vulnerabilities and code quality issues + +**Scope**: Automatically scoped to changed paths in the PR (via `codeql-config` composite) + +**Query suite**: `security-extended` (default) — covers OWASP Top 10, CWE Top 25, and more + +**Exit behavior**: Configurable via `codeql_fail_on_findings` (default: fails on findings) + +### Pre-release Version Gate + +**What it does**: Scans `go.mod`, `package.json`, and `Dockerfile` for version pins containing `-beta` or `-rc` suffixes + +**Pattern matched**: `X.Y.Z-beta.*` and `X.Y.Z-rc.*` (any semver followed by a pre-release identifier) + +**Exit behavior**: `exit-code: 1` on branches listed in `prerelease_block_branches` (default: `release-candidate,main`). On other branches (e.g., `develop`), findings are reported as warnings only. + ## Monorepo Type 2 Behavior ### Backend Changes @@ -493,7 +544,7 @@ Generated for each scan type: - `trivy-secret-scan-repo-{app-name}.sarif` - `trivy-vulnerability-scan-docker-{app-name}.sarif` -Can be uploaded to GitHub Security tab (currently commented out in workflow). +Uploaded to GitHub Security tab via CodeQL when `enable_codeql` is enabled. ## Related Workflows diff --git a/src/security/codeql-analyze/action.yml b/src/security/codeql-analyze/action.yml index ee2e7904..ce7d445e 100644 --- a/src/security/codeql-analyze/action.yml +++ b/src/security/codeql-analyze/action.yml @@ -9,6 +9,10 @@ inputs: description: 'Output directory for SARIF files' required: false default: '../results' + upload: + description: 'Upload SARIF to GitHub Security tab (requires Code Security / GHAS enabled on the repo)' + required: false + default: 'false' runs: using: composite @@ -18,3 +22,4 @@ runs: with: category: ${{ inputs.category }} output: ${{ inputs.output }} + upload: ${{ inputs.upload }} diff --git a/src/security/prerelease-check/README.md b/src/security/prerelease-check/README.md new file mode 100644 index 00000000..4be0819f --- /dev/null +++ b/src/security/prerelease-check/README.md @@ -0,0 +1,75 @@ + + + + + +
Lerian

prerelease-check

+ +Composite action that scans dependency files for unstable version pins. Only stable semver (`x.y.z`) and SHA-based pins (including Go pseudo-versions) are allowed. Checks `go.mod`, `package.json`, and `Dockerfile` and reports findings via GitHub annotations and step summary. + +## Inputs + +| Input | Description | Required | Default | +|---|---|:---:|---| +| `scan-ref` | Directory to scan for pre-release versions | No | `.` | +| `app-name` | Application name for reporting context | No | — | + +## Outputs + +| Output | Description | +|---|---| +| `has-findings` | `true` if unstable versions were detected | +| `findings-count` | Number of unstable version findings | + +## What it scans + +Matches any semver with a pre-release suffix starting with a letter (`x.y.z-`). + +| File | Blocked (unstable) | Allowed (stable) | +|---|---|---| +| `go.mod` | `v1.2.3-beta.1`, `v1.2.3-rc.1`, `v1.2.3-alpha.1`, `v1.2.3-dev.1` | `v1.2.3`, `v0.0.0-20240101-abcdef012345` (pseudo-version) | +| `package.json` | `"2.0.0-beta.1"`, `"1.0.0-canary.3"` | `"2.0.0"` | +| `Dockerfile` | `golang:1.21.0-beta1`, `node:20.0.0-rc.1` | `golang:1.21.0`, `golang:1.21.0@sha256:...` | + +## Usage + +### As a composite step (within a security workflow job) + +```yaml +jobs: + security: + runs-on: blacksmith-4vcpu-ubuntu-2404 + steps: + - uses: actions/checkout@v6 + + - name: Pre-release Version Check + id: prerelease-check + uses: LerianStudio/github-actions-shared-workflows/src/security/prerelease-check@v1.x.x + with: + scan-ref: '.' + app-name: 'my-app' + + - name: Fail on pre-release versions + if: steps.prerelease-check.outputs.has-findings == 'true' + run: exit 1 +``` + +### Via the reusable workflow + +Pre-release checks are built into the `pr-security-scan` workflow and enabled by default: + +```yaml +jobs: + security-scan: + uses: LerianStudio/github-actions-shared-workflows/.github/workflows/pr-security-scan.yml@v1.x.x + with: + enable_prerelease_check: true # default + secrets: inherit +``` + +## Permissions required + +```yaml +permissions: + contents: read +``` diff --git a/src/security/prerelease-check/action.yml b/src/security/prerelease-check/action.yml new file mode 100644 index 00000000..18b84443 --- /dev/null +++ b/src/security/prerelease-check/action.yml @@ -0,0 +1,100 @@ +name: Pre-release Version Check +description: Scans dependency files for unstable version pins. Only stable semver (x.y.z) and Go pseudo-versions (SHA-based) are allowed. + +inputs: + scan-ref: + description: 'Directory to scan for pre-release versions' + required: false + default: '.' + app-name: + description: 'Application name for reporting context' + required: false + default: '' + +outputs: + has-findings: + description: 'true if pre-release versions were detected' + value: ${{ steps.scan.outputs.has_findings }} + findings-count: + description: 'Number of pre-release version findings' + value: ${{ steps.scan.outputs.findings_count }} + +runs: + using: composite + steps: + - name: Scan for pre-release versions + id: scan + shell: bash + env: + SCAN_DIR: ${{ inputs.scan-ref }} + APP_NAME: ${{ inputs.app-name }} + run: | + # Matches x.y.z- — any semver with a pre-release suffix starting + # with a letter (alpha, beta, rc, dev, preview, canary, snapshot, etc.). + # Excludes Go pseudo-versions (v0.0.0-YYYYMMDDHHMMSS-commitsha) because + # their suffix starts with digits, not letters. SHA pins are also allowed. + PRERELEASE_PATTERN='[0-9]+\.[0-9]+\.[0-9]+-[a-zA-Z]' + FINDINGS=() + + # ----------------- go.mod ----------------- + if [ -f "$SCAN_DIR/go.mod" ]; then + while IFS= read -r match; do + FINDINGS+=("go.mod|$match") + done < <(grep -nE "v${PRERELEASE_PATTERN}" "$SCAN_DIR/go.mod" || true) + fi + + # ----------------- package.json ----------------- + if [ -f "$SCAN_DIR/package.json" ]; then + while IFS= read -r match; do + FINDINGS+=("package.json|$match") + done < <(grep -nE "\"${PRERELEASE_PATTERN}" "$SCAN_DIR/package.json" || true) + fi + + # ----------------- Dockerfile ----------------- + for df in "$SCAN_DIR/Dockerfile" "$SCAN_DIR/"*.dockerfile "$SCAN_DIR/Dockerfile."*; do + [ -f "$df" ] || continue + fname=$(basename "$df") + while IFS= read -r match; do + FINDINGS+=("${fname}|$match") + done < <(grep -nE ":${PRERELEASE_PATTERN}" "$df" || true) + done + + COUNT=${#FINDINGS[@]} + echo "findings_count=$COUNT" >> "$GITHUB_OUTPUT" + + if [ "$COUNT" -gt 0 ]; then + echo "has_findings=true" >> "$GITHUB_OUTPUT" + + LABEL="" + [ -n "$APP_NAME" ] && LABEL=" ($APP_NAME)" + + echo "::error::Found $COUNT unstable version pin(s)${LABEL}. Only stable versions (x.y.z) and SHA-based pins are allowed." + + { + echo "### :warning: Pre-release Version Pins${LABEL}" + echo "" + echo "| File | Line | Content |" + echo "|------|------|---------|" + for f in "${FINDINGS[@]}"; do + FILE="${f%%|*}" + REST="${f#*|}" + LINE="${REST%%:*}" + CONTENT="${REST#*:}" + CONTENT=$(echo "$CONTENT" | sed 's/^[[:space:]]*//' | sed 's/|/\\|/g') + echo "| \`${FILE}\` | ${LINE} | \`${CONTENT}\` |" + done + echo "" + echo "> Only stable versions (\`x.y.z\`) and SHA-based pins are allowed. Replace pre-release suffixes (\`-alpha\`, \`-beta\`, \`-rc\`, \`-dev\`, etc.) with stable releases." + } >> "$GITHUB_STEP_SUMMARY" + + for f in "${FINDINGS[@]}"; do + FILE="${f%%|*}" + REST="${f#*|}" + LINE="${REST%%:*}" + CONTENT="${REST#*:}" + echo "::warning file=${SCAN_DIR}/${FILE},line=${LINE}::Unstable version pin: $(echo "$CONTENT" | sed 's/^[[:space:]]*//')" + done + else + echo "has_findings=false" >> "$GITHUB_OUTPUT" + echo "No pre-release version pins found." + fi