From 49915c05a75e52146ee218addb4de7a55b537a2a Mon Sep 17 00:00:00 2001 From: Lucas Bedatty Date: Thu, 9 Apr 2026 14:38:54 -0300 Subject: [PATCH 01/21] feat(security): add CodeQL analysis and pre-release version gate to PR security scan --- .github/workflows/pr-security-scan.yml | 115 ++++++++++++++++++----- docs/pr-security-scan-workflow.md | 83 ++++++++++++---- src/security/prerelease-check/README.md | 73 ++++++++++++++ src/security/prerelease-check/action.yml | 96 +++++++++++++++++++ 4 files changed, 327 insertions(+), 40 deletions(-) create mode 100644 src/security/prerelease-check/README.md create mode 100644 src/security/prerelease-check/action.yml diff --git a/.github/workflows/pr-security-scan.yml b/.github/workflows/pr-security-scan.yml index 74054453..16de8496 100644 --- a/.github/workflows/pr-security-scan.yml +++ b/.github/workflows/pr-security-scan.yml @@ -76,6 +76,23 @@ 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 + enable_prerelease_check: + description: 'Block dependencies pinned to pre-release versions (-beta, -rc)' + type: boolean + default: true permissions: id-token: write # Required for OIDC authentication @@ -100,7 +117,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 +167,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 +192,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 +202,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 +228,75 @@ 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' + run: | + echo "::error::Pre-release version pins detected (${{ steps.prerelease-check.outputs.findings-count }} finding(s)). Production code must not depend on beta or release candidate versions." + exit 1 + + # ----------------- 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: 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 }}' + + # ----------------- 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..a31c20c6 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,11 @@ 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 | +| `enable_prerelease_check` | boolean | `true` | Block dependencies pinned to pre-release versions (`-beta`, `-rc`) | ## Secrets @@ -219,14 +238,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 +290,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` (fails workflow when pre-release versions are found) + ## Monorepo Type 2 Behavior ### Backend Changes @@ -493,7 +542,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/prerelease-check/README.md b/src/security/prerelease-check/README.md new file mode 100644 index 00000000..4c52d156 --- /dev/null +++ b/src/security/prerelease-check/README.md @@ -0,0 +1,73 @@ + + + + + +
Lerian

prerelease-check

+ +Composite action that scans dependency files for pre-release version pins (`-beta`, `-rc`) that should not reach production. Checks `go.mod`, `package.json`, and `Dockerfile` for unstable version references 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 pre-release versions were detected | +| `findings-count` | Number of pre-release version findings | + +## What it scans + +| File | Pattern | Example match | +|---|---|---| +| `go.mod` | `vX.Y.Z-beta.*` / `vX.Y.Z-rc.*` | `v1.2.3-beta.1` | +| `package.json` | `"X.Y.Z-beta.*"` / `"X.Y.Z-rc.*"` | `"2.0.0-rc.1"` | +| `Dockerfile` | `:X.Y.Z-beta.*` / `:X.Y.Z-rc.*` | `golang:1.21.0-beta1` | + +## 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..7f810a18 --- /dev/null +++ b/src/security/prerelease-check/action.yml @@ -0,0 +1,96 @@ +name: Pre-release Version Check +description: Scans dependency files for pre-release version pins (-beta, -rc) that should not reach production. + +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: | + PRERELEASE_PATTERN='[0-9]+\.[0-9]+\.[0-9]+-(beta|rc)[.0-9]*' + 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 pre-release version pin(s)${LABEL}. Production code must not depend on beta or release candidate versions." + + { + 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 "> Replace \`-beta.*\` and \`-rc.*\` versions 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}::Pre-release version pin: $(echo "$CONTENT" | sed 's/^[[:space:]]*//')" + done + else + echo "has_findings=false" >> "$GITHUB_OUTPUT" + echo "No pre-release version pins found." + fi From aad6ec3db0a1aff1b2a3a2e3dd6690093cb258b7 Mon Sep 17 00:00:00 2001 From: Lucas Bedatty Date: Thu, 9 Apr 2026 15:08:11 -0300 Subject: [PATCH 02/21] fix(security): configure private Go module access for CodeQL autobuild --- .github/workflows/pr-security-scan.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/pr-security-scan.yml b/.github/workflows/pr-security-scan.yml index 16de8496..3fa4eb86 100644 --- a/.github/workflows/pr-security-scan.yml +++ b/.github/workflows/pr-security-scan.yml @@ -267,6 +267,14 @@ jobs: 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 From 046ef6400018be7bb9f436d8232c2fe82c35855a Mon Sep 17 00:00:00 2001 From: Lucas Bedatty Date: Thu, 9 Apr 2026 15:21:56 -0300 Subject: [PATCH 03/21] fix(security): add actions:read permission for CodeQL status reporting --- .github/workflows/pr-security-scan.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr-security-scan.yml b/.github/workflows/pr-security-scan.yml index 3fa4eb86..d35002c5 100644 --- a/.github/workflows/pr-security-scan.yml +++ b/.github/workflows/pr-security-scan.yml @@ -95,6 +95,7 @@ on: default: true 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 From 537771db4fe664d1cc4d91e8031150f5956788c0 Mon Sep 17 00:00:00 2001 From: Lucas Bedatty Date: Thu, 9 Apr 2026 15:58:42 -0300 Subject: [PATCH 04/21] fix(security): disable SARIF upload by default in codeql-analyze composite --- src/security/codeql-analyze/action.yml | 5 +++++ 1 file changed, 5 insertions(+) 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 }} From f89fd13b2cb50bbc165d8787dea4ed8306623fb4 Mon Sep 17 00:00:00 2001 From: Lucas Bedatty Date: Thu, 9 Apr 2026 16:09:23 -0300 Subject: [PATCH 05/21] feat(security): add codeql_upload_sarif input for Security tab integration --- .github/workflows/pr-security-scan.yml | 5 +++++ docs/pr-security-scan-workflow.md | 1 + 2 files changed, 6 insertions(+) diff --git a/.github/workflows/pr-security-scan.yml b/.github/workflows/pr-security-scan.yml index d35002c5..a3035e37 100644 --- a/.github/workflows/pr-security-scan.yml +++ b/.github/workflows/pr-security-scan.yml @@ -89,6 +89,10 @@ on: 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 @@ -285,6 +289,7 @@ jobs: 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 diff --git a/docs/pr-security-scan-workflow.md b/docs/pr-security-scan-workflow.md index a31c20c6..372b6d5e 100644 --- a/docs/pr-security-scan-workflow.md +++ b/docs/pr-security-scan-workflow.md @@ -196,6 +196,7 @@ When enabled, the workflow scans `go.mod`, `package.json`, and `Dockerfile` for | `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`) | ## Secrets From 927fb8793eef3b336baad55ba67dcf2025caab48 Mon Sep 17 00:00:00 2001 From: Lucas Bedatty Date: Thu, 9 Apr 2026 17:09:40 -0300 Subject: [PATCH 06/21] feat(security): make prerelease gate branch-aware (block on rc/main, warn on develop) --- .github/workflows/pr-security-scan.yml | 26 ++++++++++++++++++++++++-- docs/pr-security-scan-workflow.md | 3 ++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-security-scan.yml b/.github/workflows/pr-security-scan.yml index a3035e37..c9288c95 100644 --- a/.github/workflows/pr-security-scan.yml +++ b/.github/workflows/pr-security-scan.yml @@ -97,6 +97,10 @@ on: 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 @@ -235,9 +239,27 @@ jobs: - 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: | - echo "::error::Pre-release version pins detected (${{ steps.prerelease-check.outputs.findings-count }} finding(s)). Production code must not depend on beta or release candidate versions." - exit 1 + 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: diff --git a/docs/pr-security-scan-workflow.md b/docs/pr-security-scan-workflow.md index 372b6d5e..2b464c40 100644 --- a/docs/pr-security-scan-workflow.md +++ b/docs/pr-security-scan-workflow.md @@ -198,6 +198,7 @@ When enabled, the workflow scans `go.mod`, `package.json`, and `Dockerfile` for | `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 @@ -307,7 +308,7 @@ Runs when `enable_codeql: true` and `codeql_languages` is set: **Pattern matched**: `X.Y.Z-beta.*` and `X.Y.Z-rc.*` (any semver followed by a pre-release identifier) -**Exit behavior**: `exit-code: 1` (fails workflow when pre-release versions are found) +**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 From 2edd134b2ed653a28fb1a722f2e5a60b5eb0913a Mon Sep 17 00:00:00 2001 From: Lucas Bedatty Date: Thu, 9 Apr 2026 17:15:59 -0300 Subject: [PATCH 07/21] feat(security): broaden prerelease check to block all unstable version suffixes --- src/security/prerelease-check/README.md | 16 +++++++++------- src/security/prerelease-check/action.yml | 16 ++++++++++------ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/security/prerelease-check/README.md b/src/security/prerelease-check/README.md index 4c52d156..4be0819f 100644 --- a/src/security/prerelease-check/README.md +++ b/src/security/prerelease-check/README.md @@ -5,7 +5,7 @@ -Composite action that scans dependency files for pre-release version pins (`-beta`, `-rc`) that should not reach production. Checks `go.mod`, `package.json`, and `Dockerfile` for unstable version references and reports findings via GitHub annotations and step summary. +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 @@ -18,16 +18,18 @@ Composite action that scans dependency files for pre-release version pins (`-bet | Output | Description | |---|---| -| `has-findings` | `true` if pre-release versions were detected | -| `findings-count` | Number of pre-release version findings | +| `has-findings` | `true` if unstable versions were detected | +| `findings-count` | Number of unstable version findings | ## What it scans -| File | Pattern | Example match | +Matches any semver with a pre-release suffix starting with a letter (`x.y.z-`). + +| File | Blocked (unstable) | Allowed (stable) | |---|---|---| -| `go.mod` | `vX.Y.Z-beta.*` / `vX.Y.Z-rc.*` | `v1.2.3-beta.1` | -| `package.json` | `"X.Y.Z-beta.*"` / `"X.Y.Z-rc.*"` | `"2.0.0-rc.1"` | -| `Dockerfile` | `:X.Y.Z-beta.*` / `:X.Y.Z-rc.*` | `golang:1.21.0-beta1` | +| `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 diff --git a/src/security/prerelease-check/action.yml b/src/security/prerelease-check/action.yml index 7f810a18..18b84443 100644 --- a/src/security/prerelease-check/action.yml +++ b/src/security/prerelease-check/action.yml @@ -1,5 +1,5 @@ name: Pre-release Version Check -description: Scans dependency files for pre-release version pins (-beta, -rc) that should not reach production. +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: @@ -29,7 +29,11 @@ runs: SCAN_DIR: ${{ inputs.scan-ref }} APP_NAME: ${{ inputs.app-name }} run: | - PRERELEASE_PATTERN='[0-9]+\.[0-9]+\.[0-9]+-(beta|rc)[.0-9]*' + # 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 ----------------- @@ -43,7 +47,7 @@ runs: 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) + done < <(grep -nE "\"${PRERELEASE_PATTERN}" "$SCAN_DIR/package.json" || true) fi # ----------------- Dockerfile ----------------- @@ -64,7 +68,7 @@ runs: LABEL="" [ -n "$APP_NAME" ] && LABEL=" ($APP_NAME)" - echo "::error::Found $COUNT pre-release version pin(s)${LABEL}. Production code must not depend on beta or release candidate versions." + 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}" @@ -80,7 +84,7 @@ runs: echo "| \`${FILE}\` | ${LINE} | \`${CONTENT}\` |" done echo "" - echo "> Replace \`-beta.*\` and \`-rc.*\` versions with stable releases." + 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 @@ -88,7 +92,7 @@ runs: REST="${f#*|}" LINE="${REST%%:*}" CONTENT="${REST#*:}" - echo "::warning file=${SCAN_DIR}/${FILE},line=${LINE}::Pre-release version pin: $(echo "$CONTENT" | sed 's/^[[:space:]]*//')" + echo "::warning file=${SCAN_DIR}/${FILE},line=${LINE}::Unstable version pin: $(echo "$CONTENT" | sed 's/^[[:space:]]*//')" done else echo "has_findings=false" >> "$GITHUB_OUTPUT" From e4c9a7d4901b5ccab7ba224643b1d4f1393da97a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:54:21 +0000 Subject: [PATCH 08/21] chore(deps): bump docker/build-push-action in the docker group Bumps the docker group with 1 update: [docker/build-push-action](https://github.com/docker/build-push-action). Updates `docker/build-push-action` from 7.0.0 to 7.1.0 - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/d08e5c354a6adb9ed34480a06d141179aa583294...bcafcacb16a39f128d818304e6c9c0c18556b85f) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: 7.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- .github/workflows/go-release.yml | 2 +- .github/workflows/pr-security-scan.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7459ff0a..922385c0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -296,7 +296,7 @@ jobs: - name: Build and push Docker image id: build-push - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7 with: context: ${{ inputs.build_context_from_working_dir == true && matrix.app.working_dir || inputs.build_context }} file: ${{ matrix.app.working_dir }}/${{ inputs.dockerfile_name }} diff --git a/.github/workflows/go-release.yml b/.github/workflows/go-release.yml index 23cbf9e7..b0de567f 100644 --- a/.github/workflows/go-release.yml +++ b/.github/workflows/go-release.yml @@ -173,7 +173,7 @@ jobs: - name: Build and push id: build-push - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7 with: context: . platforms: ${{ inputs.docker_platforms }} diff --git a/.github/workflows/pr-security-scan.yml b/.github/workflows/pr-security-scan.yml index 74054453..97f436eb 100644 --- a/.github/workflows/pr-security-scan.yml +++ b/.github/workflows/pr-security-scan.yml @@ -158,7 +158,7 @@ jobs: # ----------------- Docker Build ----------------- - name: Build Docker Image for Scanning if: always() && inputs.enable_docker_scan - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7 with: context: ${{ inputs.build_context_from_working_dir == true && matrix.working_dir || (inputs.monorepo_type == 'type2' && matrix.working_dir == inputs.frontend_folder && inputs.frontend_folder || '.') }} file: ${{ env.DOCKERFILE_PATH }} From fafd2f9c643fd064183949905ad4b4acab7241ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:56:51 +0000 Subject: [PATCH 09/21] chore(deps): bump actions/create-github-app-token in the release group Bumps the release group with 1 update: [actions/create-github-app-token](https://github.com/actions/create-github-app-token). Updates `actions/create-github-app-token` from 3.0.0 to 3.1.1 - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Commits](https://github.com/actions/create-github-app-token/compare/f8d387b68d61c58ab83c6c016672934102569859...1b10c78c7865c340bc4f6099eb2f838309f1e8c3) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-version: 3.1.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: release ... Signed-off-by: dependabot[bot] --- .github/workflows/gptchangelog.yml | 2 +- .github/workflows/helm-update-chart.yml | 2 +- .github/workflows/release-notification.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/typescript-release.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/gptchangelog.yml b/.github/workflows/gptchangelog.yml index 88003b24..6da450d2 100644 --- a/.github/workflows/gptchangelog.yml +++ b/.github/workflows/gptchangelog.yml @@ -252,7 +252,7 @@ jobs: steps: - name: Create GitHub App Token - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3 id: app-token with: app-id: ${{ secrets.LERIAN_STUDIO_MIDAZ_PUSH_BOT_APP_ID }} diff --git a/.github/workflows/helm-update-chart.yml b/.github/workflows/helm-update-chart.yml index 72f1f214..561b7df2 100644 --- a/.github/workflows/helm-update-chart.yml +++ b/.github/workflows/helm-update-chart.yml @@ -100,7 +100,7 @@ jobs: steps: - name: Generate GitHub App Token id: app-token - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/.github/workflows/release-notification.yml b/.github/workflows/release-notification.yml index a2444cef..a36cd343 100644 --- a/.github/workflows/release-notification.yml +++ b/.github/workflows/release-notification.yml @@ -114,7 +114,7 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} steps: - name: Create GitHub App token - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3 id: app-token with: app-id: ${{ secrets.APP_ID }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4b6e573a..b2de026d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -103,7 +103,7 @@ jobs: gpg_fingerprint: ${{ steps.import_gpg.outputs.fingerprint }} steps: - - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 id: app-token with: app-id: ${{ secrets.LERIAN_STUDIO_MIDAZ_PUSH_BOT_APP_ID }} diff --git a/.github/workflows/typescript-release.yml b/.github/workflows/typescript-release.yml index 4cea8eac..511efab6 100644 --- a/.github/workflows/typescript-release.yml +++ b/.github/workflows/typescript-release.yml @@ -114,7 +114,7 @@ jobs: gpg_fingerprint: ${{ steps.import_gpg.outputs.fingerprint }} steps: - - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 id: app-token with: app-id: ${{ secrets.LERIAN_STUDIO_MIDAZ_PUSH_BOT_APP_ID }} From b0ff96d617369b73e08d054e06dfdf9c6eaadf84 Mon Sep 17 00:00:00 2001 From: Lucas Bedatty Date: Mon, 13 Apr 2026 16:09:33 -0300 Subject: [PATCH 10/21] fix(prerelease-check): also scan repo root for monorepos with shared go.mod --- src/security/prerelease-check/action.yml | 68 +++++++++++++++++------- 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/src/security/prerelease-check/action.yml b/src/security/prerelease-check/action.yml index 18b84443..55e0eab9 100644 --- a/src/security/prerelease-check/action.yml +++ b/src/security/prerelease-check/action.yml @@ -36,27 +36,57 @@ runs: 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) + # Scan the component scan-ref AND the repo root. In Go monorepos a single + # root go.mod is shared across components; without scanning root we'd miss + # dependency pins for components whose scan-ref is a subdirectory. + SEEN_FILES=() + SCAN_PATHS=("$SCAN_DIR") + if [ "$SCAN_DIR" != "." ] && [ "$SCAN_DIR" != "./" ]; then + SCAN_PATHS+=(".") 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 + already_seen() { + local target="$1" + for s in "${SEEN_FILES[@]}"; do + [ "$s" = "$target" ] && return 0 + done + return 1 + } + + for base in "${SCAN_PATHS[@]}"; do + # ----------------- go.mod ----------------- + if [ -f "$base/go.mod" ]; then + real=$(realpath "$base/go.mod") + if ! already_seen "$real"; then + SEEN_FILES+=("$real") + while IFS= read -r match; do + FINDINGS+=("${base}/go.mod|$match") + done < <(grep -nE "v${PRERELEASE_PATTERN}" "$base/go.mod" || true) + fi + 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) + # ----------------- package.json ----------------- + if [ -f "$base/package.json" ]; then + real=$(realpath "$base/package.json") + if ! already_seen "$real"; then + SEEN_FILES+=("$real") + while IFS= read -r match; do + FINDINGS+=("${base}/package.json|$match") + done < <(grep -nE "\"${PRERELEASE_PATTERN}" "$base/package.json" || true) + fi + fi + + # ----------------- Dockerfile ----------------- + for df in "$base/Dockerfile" "$base/"*.dockerfile "$base/Dockerfile."*; do + [ -f "$df" ] || continue + real=$(realpath "$df") + already_seen "$real" && continue + SEEN_FILES+=("$real") + relpath="${df#./}" + while IFS= read -r match; do + FINDINGS+=("${relpath}|$match") + done < <(grep -nE ":${PRERELEASE_PATTERN}" "$df" || true) + done done COUNT=${#FINDINGS[@]} @@ -92,7 +122,7 @@ runs: REST="${f#*|}" LINE="${REST%%:*}" CONTENT="${REST#*:}" - echo "::warning file=${SCAN_DIR}/${FILE},line=${LINE}::Unstable version pin: $(echo "$CONTENT" | sed 's/^[[:space:]]*//')" + echo "::warning file=${FILE},line=${LINE}::Unstable version pin: $(echo "$CONTENT" | sed 's/^[[:space:]]*//')" done else echo "has_findings=false" >> "$GITHUB_OUTPUT" From 2e7960b8eb64215635beabea9a3e722db5853afd Mon Sep 17 00:00:00 2001 From: Lucas Bedatty Date: Mon, 13 Apr 2026 16:40:38 -0300 Subject: [PATCH 11/21] feat(prerelease-check): post PR comment with unstable version findings --- .github/workflows/pr-security-scan.yml | 1 + src/security/prerelease-check/action.yml | 103 +++++++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/.github/workflows/pr-security-scan.yml b/.github/workflows/pr-security-scan.yml index c9288c95..0394bfdd 100644 --- a/.github/workflows/pr-security-scan.yml +++ b/.github/workflows/pr-security-scan.yml @@ -223,6 +223,7 @@ jobs: with: scan-ref: ${{ matrix.working_dir }} app-name: ${{ env.APP_NAME }} + github-token: ${{ secrets.MANAGE_TOKEN || secrets.GITHUB_TOKEN }} # ----------------- Results & Security Gate ----------------- - name: Post Security Scan Results to PR diff --git a/src/security/prerelease-check/action.yml b/src/security/prerelease-check/action.yml index 55e0eab9..173ea883 100644 --- a/src/security/prerelease-check/action.yml +++ b/src/security/prerelease-check/action.yml @@ -10,6 +10,10 @@ inputs: description: 'Application name for reporting context' required: false default: '' + github-token: + description: 'GitHub token for posting PR comments. When empty, the PR comment step is skipped.' + required: false + default: '' outputs: has-findings: @@ -92,6 +96,11 @@ runs: COUNT=${#FINDINGS[@]} echo "findings_count=$COUNT" >> "$GITHUB_OUTPUT" + # Persist findings as JSON so the next step can post a PR comment. + ARTIFACT_NAME="${APP_NAME:-default}" + ARTIFACT_FILE="prerelease-findings-${ARTIFACT_NAME}.json" + echo "artifact_file=$ARTIFACT_FILE" >> "$GITHUB_OUTPUT" + if [ "$COUNT" -gt 0 ]; then echo "has_findings=true" >> "$GITHUB_OUTPUT" @@ -100,6 +109,28 @@ runs: echo "::error::Found $COUNT unstable version pin(s)${LABEL}. Only stable versions (x.y.z) and SHA-based pins are allowed." + # Build JSON array of findings for the reporter step. + { + echo "[" + FIRST=true + for f in "${FINDINGS[@]}"; do + FILE="${f%%|*}" + REST="${f#*|}" + LINE="${REST%%:*}" + CONTENT="${REST#*:}" + CONTENT_TRIMMED=$(echo "$CONTENT" | sed 's/^[[:space:]]*//') + CONTENT_ESCAPED=$(echo "$CONTENT_TRIMMED" | jq -Rs .) + if [ "$FIRST" = "true" ]; then + FIRST=false + else + echo "," + fi + printf ' {"file":"%s","line":%s,"content":%s}' "$FILE" "$LINE" "$CONTENT_ESCAPED" + done + echo "" + echo "]" + } > "$ARTIFACT_FILE" + { echo "### :warning: Pre-release Version Pins${LABEL}" echo "" @@ -126,5 +157,77 @@ runs: done else echo "has_findings=false" >> "$GITHUB_OUTPUT" + echo "[]" > "$ARTIFACT_FILE" echo "No pre-release version pins found." fi + + - name: Post PR comment with pre-release findings + if: github.event_name == 'pull_request' && inputs.github-token != '' && steps.scan.outputs.has_findings == 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + APP_NAME: ${{ inputs.app-name }} + ARTIFACT_FILE: ${{ steps.scan.outputs.artifact_file }} + FINDINGS_COUNT: ${{ steps.scan.outputs.findings_count }} + with: + github-token: ${{ inputs.github-token }} + script: | + const fs = require('fs'); + + const appName = process.env.APP_NAME || 'default'; + const artifactFile = process.env.ARTIFACT_FILE; + const findingsCount = process.env.FINDINGS_COUNT; + const label = process.env.APP_NAME ? ` — \`${appName}\`` : ''; + + let findings = []; + try { + findings = JSON.parse(fs.readFileSync(artifactFile, 'utf8')); + } catch (e) { + core.warning(`Could not read prerelease findings: ${e.message}`); + return; + } + + if (findings.length === 0) return; + + const md = (value) => + String(value ?? '').replace(/\|/g, '\\|').replace(/\r?\n/g, ' ').replace(/`/g, '\\`'); + + let body = ''; + body += `## \u{1F6AB} Pre-release Version Pins${label}\n\n`; + body += `**Found ${findingsCount} unstable version pin(s).** Production code must only depend on stable releases (\`x.y.z\`) or SHA-based pins.\n\n`; + body += `| File | Line | Content |\n`; + body += `|------|------|----------|\n`; + for (const f of findings) { + body += `| \`${md(f.file)}\` | ${f.line} | \`${md(f.content)}\` |\n`; + } + body += `\n`; + body += `> Replace pre-release suffixes (\`-alpha\`, \`-beta\`, \`-rc\`, \`-dev\`, etc.) with stable releases before merging.\n\n`; + + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + body += `---\n\n\u{1F50D} [View full scan logs](${runUrl})\n\n`; + + const marker = ``; + body = marker + '\n' + body; + + try { + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + per_page: 100, + }); + + const existing = comments.find(c => c.body?.includes(marker)); + const params = { + owner: context.repo.owner, + repo: context.repo.repo, + body, + }; + + if (existing) { + await github.rest.issues.updateComment({ ...params, comment_id: existing.id }); + } else { + await github.rest.issues.createComment({ ...params, issue_number: context.issue.number }); + } + } catch (e) { + core.warning(`Could not post PR pre-release comment: ${e.message}`); + } From 279a4bc2c1145c9310ae18ecd9782c99faa1a9df Mon Sep 17 00:00:00 2001 From: Lucas Bedatty Date: Mon, 13 Apr 2026 16:48:34 -0300 Subject: [PATCH 12/21] fix(prerelease-check): consolidate PR comments with shared marker + cleanup legacy --- src/security/prerelease-check/action.yml | 37 +++++++++++++++++++----- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/security/prerelease-check/action.yml b/src/security/prerelease-check/action.yml index 173ea883..9f0e70fd 100644 --- a/src/security/prerelease-check/action.yml +++ b/src/security/prerelease-check/action.yml @@ -173,10 +173,8 @@ runs: script: | const fs = require('fs'); - const appName = process.env.APP_NAME || 'default'; const artifactFile = process.env.ARTIFACT_FILE; const findingsCount = process.env.FINDINGS_COUNT; - const label = process.env.APP_NAME ? ` — \`${appName}\`` : ''; let findings = []; try { @@ -192,7 +190,7 @@ runs: String(value ?? '').replace(/\|/g, '\\|').replace(/\r?\n/g, ' ').replace(/`/g, '\\`'); let body = ''; - body += `## \u{1F6AB} Pre-release Version Pins${label}\n\n`; + body += `## \u{1F6AB} Pre-release Version Pins\n\n`; body += `**Found ${findingsCount} unstable version pin(s).** Production code must only depend on stable releases (\`x.y.z\`) or SHA-based pins.\n\n`; body += `| File | Line | Content |\n`; body += `|------|------|----------|\n`; @@ -205,7 +203,12 @@ runs: const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; body += `---\n\n\u{1F50D} [View full scan logs](${runUrl})\n\n`; - const marker = ``; + // Shared marker (no app-name suffix): in monorepos each matrix component + // runs this composite and would otherwise post duplicate comments for + // shared files like the root go.mod. A single marker lets each run + // update the same comment in place. Components run sequentially via + // max-parallel: 1, so no race condition. + const marker = ``; body = marker + '\n' + body; try { @@ -216,15 +219,35 @@ runs: per_page: 100, }); - const existing = comments.find(c => c.body?.includes(marker)); + // Match both the new shared marker and legacy per-app markers + // () so stale comments from + // previous runs are cleaned up when transitioning to the shared marker. + const legacyMarkerPrefix = '`; - body = marker + '\n' + body; - - try { - const comments = await github.paginate(github.rest.issues.listComments, { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - per_page: 100, - }); - - // Match both the new shared marker and legacy per-app markers - // () so stale comments from - // previous runs are cleaned up when transitioning to the shared marker. - const legacyMarkerPrefix = '