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/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/pr-security-scan.yml b/.github/workflows/pr-security-scan.yml index 74054453..89b80666 100644 --- a/.github/workflows/pr-security-scan.yml +++ b/.github/workflows/pr-security-scan.yml @@ -68,7 +68,9 @@ on: type: boolean default: true docker_build_args: - description: 'Newline-separated Docker build arguments (e.g., "APP_NAME=spi\nCOMPONENT_NAME=api"). For sensitive values (tokens, keys, passwords), use BuildKit secrets instead — build arguments are visible in image history.' + description: >- + Newline-separated Docker build arguments (e.g., "APP_NAME=spi\nCOMPONENT_NAME=api"). + For sensitive values, use BuildKit secrets — build arguments are visible in image history. type: string required: false default: '' @@ -76,8 +78,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 +128,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 +178,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 }} @@ -158,8 +186,9 @@ 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: + # yamllint disable-line rule:line-length 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 }} platforms: linux/amd64 @@ -175,7 +204,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,49 +214,138 @@ 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@feat/pr-security-scan-codeql-prerelease with: github-token: ${{ secrets.MANAGE_TOKEN || secrets.GITHUB_TOKEN }} app-name: ${{ env.APP_NAME }} enable-docker-scan: ${{ inputs.enable_docker_scan }} enable-health-score: ${{ inputs.enable_health_score && inputs.enable_docker_scan }} dockerfile-has-non-root-user: ${{ steps.dockerfile-checks.outputs.has-non-root-user || 'false' }} + prerelease-findings-file: ${{ steps.prerelease-check.outputs.artifact-file }} 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 }} + # yamllint disable-line rule:line-length + 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' || '' }} + # yamllint disable-line rule:line-length + 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/.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/self-pr-validation.yml b/.github/workflows/self-pr-validation.yml index f7d0759b..5fb3566a 100644 --- a/.github/workflows/self-pr-validation.yml +++ b/.github/workflows/self-pr-validation.yml @@ -47,7 +47,7 @@ jobs: all_files: ${{ steps.detect.outputs.all-files }} steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Detect changed files id: detect @@ -63,7 +63,7 @@ jobs: if: needs.changed-files.outputs.yaml_files != '' steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: YAML Lint uses: ./src/lint/yamllint @@ -78,7 +78,7 @@ jobs: if: needs.changed-files.outputs.workflow_files != '' steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Action Lint uses: ./src/lint/actionlint @@ -93,7 +93,7 @@ jobs: if: needs.changed-files.outputs.action_files != '' steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Pinned Actions Check uses: ./src/lint/pinned-actions @@ -108,7 +108,7 @@ jobs: if: needs.changed-files.outputs.markdown_files != '' steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Markdown Link Check uses: ./src/lint/markdown-link-check @@ -123,7 +123,7 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Spelling Check uses: ./src/lint/typos @@ -138,7 +138,7 @@ jobs: if: needs.changed-files.outputs.action_files != '' steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Shell Check uses: ./src/lint/shellcheck @@ -153,7 +153,7 @@ jobs: if: needs.changed-files.outputs.action_files != '' steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: README Check uses: ./src/lint/readme-check @@ -168,7 +168,7 @@ jobs: if: needs.changed-files.outputs.composite_files != '' steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Composite Schema Lint uses: ./src/lint/composite-schema @@ -188,7 +188,7 @@ jobs: actions: read steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Generate CodeQL config for changed files id: codeql-config @@ -208,6 +208,7 @@ jobs: uses: ./src/security/codeql-analyze with: category: '/language:actions' + upload: 'true' - name: Post CodeQL Results to PR if: always() && github.event_name == 'pull_request' && steps.codeql-config.outputs.skip != 'true' @@ -230,7 +231,7 @@ jobs: if: always() && github.event_name == 'pull_request' && needs.changed-files.result == 'success' steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Post Lint Report uses: ./src/notify/pr-lint-reporter 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 }} diff --git a/docs/pr-security-scan-workflow.md b/docs/pr-security-scan-workflow.md index 101c1109..193b6230 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 PR comment. To also upload SARIF to the GitHub Security tab, set `codeql_upload_sarif: true` (requires Code Security / GHAS enabled on the repo). -**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 unstable version pins (`-alpha`, `-beta`, `-rc`, `-dev`, etc.). On branches listed in `prerelease_block_branches` (default: `release-candidate,main`) the PR is blocked. On other branches (e.g., `develop`) findings are reported as warnings only. ## 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 unstable version pins + +**Pattern matched**: `X.Y.Z-` for Go/npm (any pre-release suffix starting with a letter). For Docker, only known pre-release prefixes: `-alpha`, `-beta`, `-rc`, `-dev`, `-preview`, `-canary`, `-snapshot`, `-nightly`. Stable Docker variants like `-slim`, `-alpine`, `-bookworm` are allowed. + +**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..4ef880bb 100644 --- a/src/security/codeql-analyze/action.yml +++ b/src/security/codeql-analyze/action.yml @@ -9,12 +9,17 @@ 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 steps: - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4 with: category: ${{ inputs.category }} output: ${{ inputs.output }} + upload: ${{ inputs.upload }} diff --git a/src/security/codeql-init/action.yml b/src/security/codeql-init/action.yml index a2442047..c605f319 100644 --- a/src/security/codeql-init/action.yml +++ b/src/security/codeql-init/action.yml @@ -18,7 +18,7 @@ runs: using: composite steps: - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4 with: languages: ${{ inputs.languages }} queries: ${{ inputs.queries }} diff --git a/src/security/pr-security-reporter/action.yml b/src/security/pr-security-reporter/action.yml index f9da4ec6..cad5e7c3 100644 --- a/src/security/pr-security-reporter/action.yml +++ b/src/security/pr-security-reporter/action.yml @@ -20,6 +20,10 @@ inputs: description: Whether the Dockerfile sets a non-root USER directive required: false default: "false" + prerelease-findings-file: + description: 'Path to JSON file with pre-release version findings from the prerelease-check composite. Leave empty to skip.' + required: false + default: '' fail-on-findings: description: 'Fail the step with exit code 1 when security findings are detected (true/false)' required: false @@ -44,6 +48,7 @@ runs: ENABLE_DOCKER_SCAN: ${{ inputs.enable-docker-scan }} ENABLE_HEALTH_SCORE: ${{ inputs.enable-health-score }} DOCKERFILE_HAS_NON_ROOT_USER: ${{ inputs.dockerfile-has-non-root-user }} + PRERELEASE_FINDINGS_FILE: ${{ inputs.prerelease-findings-file }} with: github-token: ${{ inputs.github-token }} result-encoding: string @@ -277,12 +282,54 @@ runs: return out; } + // ── Pre-release Version Scan ── + function buildPrereleaseScan() { + const file = process.env.PRERELEASE_FINDINGS_FILE; + if (!file) return ''; + + try { + if (!fs.existsSync(file)) { + hasScanErrors = true; + return `---\n\n## Pre-release Version Check\n\n\u26A0\uFE0F Findings artifact not found.\n\n`; + } + const findings = JSON.parse(fs.readFileSync(file, 'utf8')); + if (!Array.isArray(findings)) { + throw new Error('Expected a JSON array of findings'); + } + if (findings.length === 0) { + return `---\n\n## Pre-release Version Check\n\n\u2705 No unstable version pins found.\n\n`; + } + + // Do NOT set hasFindings here — prerelease findings are gated + // separately by the branch-aware "Gate - Fail on Pre-release Versions" + // step in the workflow. Setting hasFindings would make the reporter's + // own gate (fail-on-findings) exit 1 on ALL branches, bypassing the + // warn-on-develop / block-on-rc-main semantics. + const MAX_PRERELEASE = 50; + let out = `---\n\n## Pre-release Version Check\n\n`; + out += `\u{1F6AB} **Found ${findings.length} unstable version pin(s).** Only stable releases (\`x.y.z\`) and SHA-based pins are allowed.\n\n`; + out += `| File | Line | Content |\n`; + out += `|------|------|----------|\n`; + for (const f of findings.slice(0, MAX_PRERELEASE)) { + out += `| \`${md(f.file)}\` | ${f.line} | \`${md(truncate(String(f.content ?? ''), 120))}\` |\n`; + } + if (findings.length > MAX_PRERELEASE) out += `\n_... and ${findings.length - MAX_PRERELEASE} more findings._\n`; + out += `\n`; + out += `> Replace pre-release suffixes (\`-alpha\`, \`-beta\`, \`-rc\`, \`-dev\`, etc.) with stable releases.\n\n`; + return out; + } catch (e) { + hasScanErrors = true; + return `---\n\n## Pre-release Version Check\n\n\u26A0\uFE0F Could not parse findings: ${e.message}\n\n`; + } + } + // ── Build Report ── body += `## \u{1F512} Security Scan Results \u2014 \`${appName}\`\n\n`; body += `## Trivy\n\n`; body += buildTrivyFsScan(); body += buildTrivyDockerScan(); body += buildHealthScoreSection(); + body += buildPrereleaseScan(); // ── Useful Links ── const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; diff --git a/src/security/prerelease-check/README.md b/src/security/prerelease-check/README.md new file mode 100644 index 00000000..c8de39e6 --- /dev/null +++ b/src/security/prerelease-check/README.md @@ -0,0 +1,76 @@ + + + + + +
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 | +| `artifact-file` | Path to the JSON findings file for consumption by `pr-security-reporter` | + +## What it scans + +For `go.mod` and `package.json`: matches any semver with a pre-release suffix starting with a letter (`x.y.z-`). For `Dockerfile`: only matches known pre-release prefixes to avoid false positives on stable image variants. + +| File | Scanned patterns | Blocked (unstable) | Allowed (stable) | +|---|---|---|---| +| `go.mod` | `vX.Y.Z-` | `v1.2.3-beta.1`, `v1.2.3-rc.1`, `v1.2.3-alpha.1` | `v1.2.3`, `v0.0.0-20240101-abcdef012345` | +| `package.json` | `"[~^>=]*X.Y.Z-"` | `"^2.0.0-beta.1"`, `"~1.0.0-rc.3"` | `"2.0.0"` | +| `Dockerfile`, `*.dockerfile`, `Dockerfile.*` | `:X.Y.Z-(alpha\|beta\|rc\|dev\|...)` | `golang:1.21.0-beta1` | `golang:1.21.0`, `python:3.12-slim`, `node:20-alpine` | + +## 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..a6d720fb --- /dev/null +++ b/src/security/prerelease-check/action.yml @@ -0,0 +1,165 @@ +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 }} + artifact-file: + description: 'Path to the JSON findings file for consumption by pr-security-reporter' + value: ${{ steps.scan.outputs.artifact_file }} + +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]' + + # Docker-specific pattern: only match known pre-release prefixes to avoid + # false positives on stable image variants like -slim, -alpine, -bookworm. + DOCKER_PRERELEASE='[0-9]+\.[0-9]+\.[0-9]+-(alpha|beta|rc|dev|preview|canary|snapshot|nightly)' + FINDINGS=() + + # 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 + + 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 + + # ----------------- 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 ":${DOCKER_PRERELEASE}" "$df" || true) + done + done + + 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" + + 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." + + # 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 "" + 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=${FILE},line=${LINE}::Unstable version pin: $(echo "$CONTENT" | sed 's/^[[:space:]]*//')" + done + else + echo "has_findings=false" >> "$GITHUB_OUTPUT" + echo "[]" > "$ARTIFACT_FILE" + echo "No pre-release version pins found." + fi