diff --git a/.claude/settings.json b/.claude/settings.json index 4e1c91b02..122fe626c 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -10,5 +10,9 @@ }, "worktree": { "bgIsolation": "none" + }, + "attribution": { + "commit": "", + "pr": "" } } \ No newline at end of file diff --git a/.github/renovate.json b/.github/renovate.json index 1a5a68d8d..115c14938 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -244,6 +244,19 @@ "datasourceTemplate": "golang-version", "versioningTemplate": "semver" }, + { + "customType": "regex", + "description": "Track pinned Semgrep container image digest in the Semgrep CI workflow", + "managerFilePatterns": [ + "/^\\.github/workflows/semgrep\\.yml$/" + ], + "matchStrings": [ + "#\\s*renovate:\\s*datasource=docker\\s+depName=semgrep/semgrep\\s*\\n\\s*image:\\s*semgrep/semgrep:(?[^@\\s]+)@(?sha256:[a-f0-9]+)" + ], + "depNameTemplate": "semgrep/semgrep", + "datasourceTemplate": "docker", + "versioningTemplate": "docker" + }, { "customType": "regex", "description": "Track NODE_VERSION in Actions workflows", @@ -275,11 +288,15 @@ "description": "Track Syft version in workflows and scripts", "managerFilePatterns": [ "/^\\.github/workflows/nightly-build\\.yml$/", + "/^\\.github/workflows/docker-build\\.yml$/", + "/^\\.github/workflows/supply-chain-pr\\.yml$/", + "/^\\.github/workflows/supply-chain-verify\\.yml$/", "/^\\.github/skills/security-scan-docker-image-scripts/run\\.sh$/" ], "matchStrings": [ "SYFT_VERSION=\\\"v(?[^\\\"\\s]+)\\\"", - "set_default_env \\\"SYFT_VERSION\\\" \\\"v(?[^\\\"]+)\\\"" + "set_default_env \\\"SYFT_VERSION\\\" \\\"v(?[^\\\"]+)\\\"", + "syft-version:\\s*v(?[^\\s]+)" ], "depNameTemplate": "anchore/syft", "datasourceTemplate": "github-releases", @@ -396,6 +413,19 @@ "datasourceTemplate": "go", "versioningTemplate": "semver" }, + { + "customType": "regex", + "description": "Track github.com/klauspost/compress version ARG in Dockerfile", + "managerFilePatterns": [ + "/^Dockerfile$/" + ], + "matchStrings": [ + "#\\s*renovate:\\s*datasource=go\\s+depName=github\\.com/klauspost/compress\\s*\\nARG KLAUSPOST_COMPRESS_VERSION=(?[^\\s]+)" + ], + "depNameTemplate": "github.com/klauspost/compress", + "datasourceTemplate": "go", + "versioningTemplate": "semver" + }, { "customType": "regex", "description": "Track golang-jwt/jwt v5 in go.mod via github-tags (workaround: Renovate go datasource no-result for /vN module paths)", diff --git a/.github/skills/examples/gorm-scanner-ci-workflow.yml b/.github/skills/examples/gorm-scanner-ci-workflow.yml index 0666e5b74..fb5383608 100644 --- a/.github/skills/examples/gorm-scanner-ci-workflow.yml +++ b/.github/skills/examples/gorm-scanner-ci-workflow.yml @@ -25,7 +25,7 @@ jobs: - name: Setup Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 with: - go-version: "1.26.5" + go-version: "1.26.6" - name: Run GORM Security Scanner id: gorm-scan diff --git a/.github/skills/security-scan-docker-image-scripts/run.sh b/.github/skills/security-scan-docker-image-scripts/run.sh index 73f6042e8..f5e614524 100755 --- a/.github/skills/security-scan-docker-image-scripts/run.sh +++ b/.github/skills/security-scan-docker-image-scripts/run.sh @@ -35,7 +35,7 @@ fi # Check Grype if ! command -v grype >/dev/null 2>&1; then log_error "Grype not found - install from: https://github.com/anchore/grype" - log_error "Installation: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.116.1" + log_error "Installation: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.117.0" error_exit "Grype is required for vulnerability scanning" 2 fi @@ -50,8 +50,8 @@ SYFT_INSTALLED_VERSION=$(syft version | grep -oP 'Version:\s*\Kv?[0-9]+\.[0-9]+\ GRYPE_INSTALLED_VERSION=$(grype version | grep -oP 'Version:\s*\Kv?[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown") # Set defaults matching CI workflow -set_default_env "SYFT_VERSION" "v1.50.0" -set_default_env "GRYPE_VERSION" "v0.116.1" +set_default_env "SYFT_VERSION" "v1.51.0" +set_default_env "GRYPE_VERSION" "v0.117.0" set_default_env "IMAGE_TAG" "charon:local" set_default_env "FAIL_ON_SEVERITY" "Critical,High" diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 5f605a4c1..631d34d1d 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -12,7 +12,7 @@ concurrency: cancel-in-progress: true env: - GO_VERSION: '1.26.5' + GO_VERSION: '1.26.6' GOTOOLCHAIN: local # Minimal permissions at workflow level; write permissions granted at job level for push only diff --git a/.github/workflows/codecov-upload.yml b/.github/workflows/codecov-upload.yml index 5e596211a..a868ee9a7 100644 --- a/.github/workflows/codecov-upload.yml +++ b/.github/workflows/codecov-upload.yml @@ -28,7 +28,7 @@ concurrency: cancel-in-progress: true env: - GO_VERSION: '1.26.5' + GO_VERSION: '1.26.6' NODE_VERSION: '24.19.0' GOTOOLCHAIN: local diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a28524369..39132accf 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -15,7 +15,7 @@ concurrency: env: GOTOOLCHAIN: local - GO_VERSION: '1.26.5' + GO_VERSION: '1.26.6' permissions: contents: read @@ -52,7 +52,7 @@ jobs: run: bash scripts/ci/check-codeql-parity.sh - name: Initialize CodeQL - uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 + uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4 with: languages: ${{ matrix.language }} queries: security-and-quality @@ -92,11 +92,11 @@ jobs: run: mkdir -p sarif-results - name: Autobuild - uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 + uses: github/codeql-action/autobuild@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4 - name: Perform CodeQL Analysis id: codeql_analyze - uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 + uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4 with: category: "/language:${{ matrix.language }}" output: sarif-results/${{ matrix.language }} diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 76d111d5d..598e3b48f 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -912,7 +912,7 @@ jobs: format: 'table' severity: 'CRITICAL,HIGH' exit-code: '0' - version: 'v0.73.0' + version: 'v0.74.0' trivyignores: '.trivyignore' continue-on-error: true @@ -925,7 +925,7 @@ jobs: format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' - version: 'v0.73.0' + version: 'v0.74.0' trivyignores: '.trivyignore' continue-on-error: true @@ -941,7 +941,7 @@ jobs: - name: Upload Trivy results if: env.TRIGGER_EVENT != 'pull_request' && steps.trivy-check.outputs.exists == 'true' - uses: github/codeql-action/upload-sarif@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: sarif_file: 'trivy-results.sarif' category: ${{ env.TRIVY_SARIF_CATEGORY }} @@ -964,14 +964,60 @@ jobs: # Generate SBOM (Software Bill of Materials) for supply chain security # Only for production builds (main/development) - feature branches use downstream supply-chain-pr.yml + # anchore/sbom-action resolves the pinned syft release tag against the GitHub + # API at job runtime; transient upstream 5xx/rate-limit errors there fail the + # step even though the pin itself is valid, so continue-on-error + a + # deterministic pinned-Syft fallback (below) makes this resilient. - name: Generate SBOM + id: sbom_primary + continue-on-error: true uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 if: env.TRIGGER_EVENT != 'pull_request' && needs.setup.outputs.is_feature_push != 'true' with: image: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.merge.outputs.digest }} format: cyclonedx-json output-file: sbom.cyclonedx.json - syft-version: v1.45.1 + syft-version: v1.51.0 + + - name: Generate SBOM fallback with pinned Syft + if: env.TRIGGER_EVENT != 'pull_request' && needs.setup.outputs.is_feature_push != 'true' && always() + run: | + set -euo pipefail + + if [[ "${{ steps.sbom_primary.outcome }}" == "success" ]] && [[ -s sbom.cyclonedx.json ]] && jq -e . sbom.cyclonedx.json >/dev/null 2>&1; then + echo "Primary SBOM generation succeeded with valid JSON; skipping fallback" + exit 0 + fi + + echo "Primary SBOM generation failed or produced missing/invalid output; using deterministic Syft fallback" + + SYFT_VERSION="v1.51.0" + OS="$(uname -s | tr '[:upper:]' '[:lower:]')" + ARCH="$(uname -m)" + case "$ARCH" in + x86_64) ARCH="amd64" ;; + aarch64|arm64) ARCH="arm64" ;; + *) echo "Unsupported architecture: $ARCH"; exit 1 ;; + esac + + TARBALL="syft_${SYFT_VERSION#v}_${OS}_${ARCH}.tar.gz" + BASE_URL="https://github.com/anchore/syft/releases/download/${SYFT_VERSION}" + + curl -fsSLo "$TARBALL" "${BASE_URL}/${TARBALL}" + curl -fsSLo checksums.txt "${BASE_URL}/syft_${SYFT_VERSION#v}_checksums.txt" + + grep " ${TARBALL}$" checksums.txt > checksum_line.txt + sha256sum -c checksum_line.txt + + tar -xzf "$TARBALL" syft + chmod +x syft + + DIGEST="${{ steps.merge.outputs.digest }}" + if [[ -z "$DIGEST" ]]; then + echo "::error::Digest from merge step is empty; the merge step did not complete successfully" + exit 1 + fi + ./syft "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${DIGEST}" -o cyclonedx-json=sbom.cyclonedx.json # Create verifiable attestation for the SBOM # (actions/attest-sbom is deprecated; actions/attest supports sbom-path natively) @@ -979,7 +1025,7 @@ jobs: # ("CI: no tokens available"), so retry once before failing the build - name: Attest SBOM id: attest-sbom - uses: actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d # v4.2.1 + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 if: env.TRIGGER_EVENT != 'pull_request' && needs.setup.outputs.is_feature_push != 'true' continue-on-error: true with: @@ -1152,7 +1198,7 @@ jobs: trivyignores: '.trivyignore' severity: 'CRITICAL,HIGH' exit-code: '0' - version: 'v0.73.0' + version: 'v0.74.0' - name: Run Trivy scan on PR image (SARIF - blocking) id: trivy-scan @@ -1167,7 +1213,7 @@ jobs: # Keep scanning strict for CRITICAL/HIGH; fail is enforced explicitly # at the end so SARIF upload and summaries still run. exit-code: '1' - version: 'v0.73.0' + version: 'v0.74.0' continue-on-error: true - name: Check Trivy PR SARIF exists @@ -1182,7 +1228,7 @@ jobs: - name: Upload Trivy scan results if: always() && steps.trivy-pr-check.outputs.exists == 'true' - uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: sarif_file: 'trivy-pr-results.sarif' category: ${{ env.TRIVY_SARIF_CATEGORY }} @@ -1285,15 +1331,15 @@ jobs: $result.ruleId // ($result.rule // {} | .id) // ( - if ($result.ruleIndex != null and (($run.tool.driver.rules? // null) | type) == \"array\") then - ($run.tool.driver.rules[$result.ruleIndex].id // \"unknown\") + if ($result.ruleIndex != null and (($run.tool.driver.rules? // null) | type) == "array") then + ($run.tool.driver.rules[$result.ruleIndex].id // "unknown") else - \"unknown\" + "unknown" end ) )) | package: \(( - ($result.message.text // \"\") - | (try capture(\"(?i)(?:Package|PkgName|Pkg|Library)\\\\s*[:=]\\\\s*`?(?[A-Za-z0-9._+:+-]+)`?\").pkg catch \"n/a\") + ($result.message.text // "") + | (try capture("(?i)(?:Package|PkgName|Pkg|Library)\\s*[:=]\\s*`?(?[A-Za-z0-9._+:/-]+)`?").pkg catch "n/a") ))" ' "${SARIF_PATH}"; then echo "- unable to render parsed findings" diff --git a/.github/workflows/e2e-tests-split.yml b/.github/workflows/e2e-tests-split.yml index 7e7acf6dc..3fb9611d6 100644 --- a/.github/workflows/e2e-tests-split.yml +++ b/.github/workflows/e2e-tests-split.yml @@ -83,7 +83,7 @@ on: env: NODE_VERSION: '24.19.0' - GO_VERSION: '1.26.5' + GO_VERSION: '1.26.6' GOTOOLCHAIN: local DOCKERHUB_REGISTRY: docker.io IMAGE_NAME: ${{ github.repository_owner }}/charon diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 72fd05254..790917ff4 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -15,7 +15,7 @@ on: default: "false" env: - GO_VERSION: '1.26.5' + GO_VERSION: '1.26.6' NODE_VERSION: '24.19.0' GOTOOLCHAIN: local GHCR_REGISTRY: ghcr.io @@ -285,9 +285,9 @@ jobs: uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 with: image: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.resolve_digest.outputs.digest }} - format: cyclonedx-json + format: spdx-json output-file: sbom-nightly.json - syft-version: v1.45.1 + syft-version: v1.51.0 - name: Generate SBOM fallback with pinned Syft if: always() @@ -301,7 +301,7 @@ jobs: echo "Primary SBOM generation failed or produced missing/invalid output; using deterministic Syft fallback" - SYFT_VERSION="v1.50.0" + SYFT_VERSION="v1.51.0" OS="$(uname -s | tr '[:upper:]' '[:lower:]')" ARCH="$(uname -m)" case "$ARCH" in @@ -327,7 +327,7 @@ jobs: echo "::error::Digest from resolve_digest step is empty; the digest-resolution step did not complete successfully" exit 1 fi - ./syft "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${DIGEST}" -o cyclonedx-json=sbom-nightly.json + ./syft "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${DIGEST}" -o spdx-json=sbom-nightly.json - name: Verify SBOM artifact if: always() @@ -336,11 +336,9 @@ jobs: test -s sbom-nightly.json jq -e . sbom-nightly.json >/dev/null jq -e ' - .bomFormat == "CycloneDX" - and (.specVersion | type == "string" and length > 0) - and has("version") - and has("metadata") - and (.components | type == "array") + (.spdxVersion | type == "string" and length > 0) + and has("SPDXID") + and (.packages | type == "array") ' sbom-nightly.json >/dev/null - name: Upload SBOM artifact @@ -614,11 +612,11 @@ jobs: image-ref: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ needs.build-and-push-nightly.outputs.digest }} format: 'sarif' output: 'trivy-nightly.sarif' - version: 'v0.73.0' + version: 'v0.74.0' trivyignores: '.trivyignore' - name: Upload Trivy results - uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: sarif_file: 'trivy-nightly.sarif' category: 'trivy-nightly' diff --git a/.github/workflows/orthrus-build.yml b/.github/workflows/orthrus-build.yml index 48aa2bcb7..23f0ada33 100644 --- a/.github/workflows/orthrus-build.yml +++ b/.github/workflows/orthrus-build.yml @@ -29,7 +29,7 @@ env: GHCR_REGISTRY: ghcr.io DOCKERHUB_REGISTRY: docker.io IMAGE_NAME: wikid82/orthrus - GO_VERSION: '1.26.5' + GO_VERSION: '1.26.6' permissions: contents: read diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index 6a679062a..f7948c6fb 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -16,7 +16,7 @@ permissions: checks: write env: - GO_VERSION: '1.26.5' + GO_VERSION: '1.26.6' NODE_VERSION: '24.19.0' GOTOOLCHAIN: local diff --git a/.github/workflows/renovate.yml b/.github/workflows/renovate.yml index bc1281087..6d63abc06 100644 --- a/.github/workflows/renovate.yml +++ b/.github/workflows/renovate.yml @@ -16,7 +16,7 @@ permissions: issues: write env: - GO_VERSION: '1.26.5' + GO_VERSION: '1.26.6' jobs: renovate: diff --git a/.github/workflows/security-pr.yml b/.github/workflows/security-pr.yml index 17b4dd2c6..6eaa6c5fc 100644 --- a/.github/workflows/security-pr.yml +++ b/.github/workflows/security-pr.yml @@ -373,7 +373,7 @@ jobs: format: 'sarif' output: 'trivy-binary-results.sarif' severity: 'CRITICAL,HIGH,MEDIUM' - version: 'v0.73.0' + version: 'v0.74.0' trivyignores: '.trivyignore' config: 'trivy.yaml' continue-on-error: true @@ -392,7 +392,7 @@ jobs: - name: Upload Trivy SARIF to GitHub Security if: always() && steps.trivy-sarif-check.outputs.exists == 'true' # github/codeql-action v4.36.2 - uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: sarif_file: 'trivy-binary-results.sarif' category: ${{ steps.pr-info.outputs.is_push == 'true' && format('security-scan-{0}', github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.ref_name) || format('security-scan-pr-{0}', steps.pr-info.outputs.pr_number) }} @@ -408,7 +408,7 @@ jobs: format: 'table' severity: 'CRITICAL,HIGH' exit-code: '1' - version: 'v0.73.0' + version: 'v0.74.0' trivyignores: '.trivyignore' config: 'trivy.yaml' diff --git a/.github/workflows/security-weekly-rebuild.yml b/.github/workflows/security-weekly-rebuild.yml index e582efc48..84aa89b1a 100644 --- a/.github/workflows/security-weekly-rebuild.yml +++ b/.github/workflows/security-weekly-rebuild.yml @@ -105,7 +105,7 @@ jobs: format: 'table' severity: 'CRITICAL,HIGH' exit-code: '1' # Fail workflow if vulnerabilities found - version: 'v0.73.0' + version: 'v0.74.0' continue-on-error: true - name: Run Trivy vulnerability scanner (SARIF) @@ -116,12 +116,12 @@ jobs: format: 'sarif' output: 'trivy-weekly-results.sarif' severity: 'CRITICAL,HIGH,MEDIUM' - version: 'v0.73.0' + version: 'v0.74.0' trivyignores: '.trivyignore' - name: Upload Trivy results to GitHub Security id: upload-trivy-weekly - uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: sarif_file: 'trivy-weekly-results.sarif' category: ${{ env.TRIVY_SARIF_CATEGORY }} @@ -156,7 +156,7 @@ jobs: format: 'json' output: 'trivy-weekly-results.json' severity: 'CRITICAL,HIGH,MEDIUM,LOW' - version: 'v0.73.0' + version: 'v0.74.0' - name: Upload Trivy JSON results uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 000000000..b64da6669 --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,101 @@ +name: Semgrep - SAST Scan + +on: + pull_request: + branches: [main, nightly, development] + push: + branches: [main, nightly, development] + workflow_dispatch: + schedule: + - cron: '0 4 * * 1' # Mondays 04:00 UTC — offset 1h after CodeQL's 03:00 to avoid runner contention + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + security-events: write + actions: read + pull-requests: read + +jobs: + semgrep-scan: + name: Semgrep SAST Scan + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + security-events: write + actions: read + pull-requests: read + container: + # renovate: datasource=docker depName=semgrep/semgrep + image: semgrep/semgrep:1.173.0@sha256:67319956da3dcb58baf5b322899c15458e3963e7018a86aeeb5cd224e69cb77a # semgrep/semgrep 1.173.0 + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.ref }} + + - name: Configure git safe.directory + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Verify Semgrep parity guard + run: bash scripts/ci/check-semgrep-parity.sh + + - name: Verify Semgrep version + run: semgrep --version + + - name: Run Semgrep (SARIF output) + id: semgrep_sarif + continue-on-error: true + env: + SEMGREP_SARIF_OUTPUT: semgrep-results.sarif + run: bash scripts/pre-commit-hooks/semgrep-scan.sh + + - name: Check Semgrep SARIF output exists + id: semgrep_sarif_check + if: always() + run: | + if [ -f semgrep-results.sarif ]; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "No Semgrep SARIF output found; skipping SARIF upload" + fi + + - name: Upload Semgrep SARIF to GitHub Security + if: always() && steps.semgrep_sarif_check.outputs.exists == 'true' + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 + with: + sarif_file: semgrep-results.sarif + category: semgrep + continue-on-error: true + + - name: Run Semgrep (hard-fail gate) + run: bash scripts/pre-commit-hooks/semgrep-scan.sh + + - name: Upload SARIF artifact + if: always() && steps.semgrep_sarif_check.outputs.exists == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: semgrep-sarif-${{ github.run_id }} + path: semgrep-results.sarif + retention-days: 14 + continue-on-error: true + + - name: Create job summary + if: always() + run: | + { + echo "## Semgrep SAST Scan Results" + echo "" + echo "**Rulesets**: p/golang, p/javascript, p/typescript, p/react, p/secrets, p/dockerfile" + echo "**Severity Gate**: ERROR, WARNING (--error)" + if [ "${{ job.status }}" == "success" ]; then + echo "PASSED: no blocking Semgrep findings" + else + echo "FAILED: Semgrep reported blocking findings — see step logs and the Security tab" + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/supply-chain-pr.yml b/.github/workflows/supply-chain-pr.yml index 8e0359b7f..c06d195e9 100644 --- a/.github/workflows/supply-chain-pr.yml +++ b/.github/workflows/supply-chain-pr.yml @@ -266,15 +266,60 @@ jobs: fi # Generate SBOM using official Anchore action (auto-updated by Renovate) + # anchore/sbom-action resolves the pinned syft release tag against the GitHub + # API at job runtime; transient upstream 5xx/rate-limit errors there fail the + # step even though the pin itself is valid, so continue-on-error + a + # deterministic pinned-Syft fallback (below) makes this resilient. - name: Generate SBOM if: steps.set-target.outputs.image_name != '' + id: sbom_primary + continue-on-error: true uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 - id: sbom with: image: ${{ steps.set-target.outputs.image_name }} format: cyclonedx-json output-file: sbom.cyclonedx.json - syft-version: v1.45.1 + syft-version: v1.51.0 + + - name: Generate SBOM fallback with pinned Syft + if: steps.set-target.outputs.image_name != '' && always() + run: | + set -euo pipefail + + if [[ "${{ steps.sbom_primary.outcome }}" == "success" ]] && [[ -s sbom.cyclonedx.json ]] && jq -e . sbom.cyclonedx.json >/dev/null 2>&1; then + echo "Primary SBOM generation succeeded with valid JSON; skipping fallback" + exit 0 + fi + + echo "Primary SBOM generation failed or produced missing/invalid output; using deterministic Syft fallback" + + SYFT_VERSION="v1.51.0" + OS="$(uname -s | tr '[:upper:]' '[:lower:]')" + ARCH="$(uname -m)" + case "$ARCH" in + x86_64) ARCH="amd64" ;; + aarch64|arm64) ARCH="arm64" ;; + *) echo "Unsupported architecture: $ARCH"; exit 1 ;; + esac + + TARBALL="syft_${SYFT_VERSION#v}_${OS}_${ARCH}.tar.gz" + BASE_URL="https://github.com/anchore/syft/releases/download/${SYFT_VERSION}" + + curl -fsSLo "$TARBALL" "${BASE_URL}/${TARBALL}" + curl -fsSLo checksums.txt "${BASE_URL}/syft_${SYFT_VERSION#v}_checksums.txt" + + grep " ${TARBALL}$" checksums.txt > checksum_line.txt + sha256sum -c checksum_line.txt + + tar -xzf "$TARBALL" syft + chmod +x syft + + IMAGE_NAME="${{ steps.set-target.outputs.image_name }}" + if [[ -z "$IMAGE_NAME" ]]; then + echo "::error::image_name from set-target step is empty; the target-selection step did not complete successfully" + exit 1 + fi + ./syft "${IMAGE_NAME}" -o cyclonedx-json=sbom.cyclonedx.json - name: Count SBOM components if: steps.set-target.outputs.image_name != '' @@ -288,7 +333,18 @@ jobs: - name: Install Grype if: steps.set-target.outputs.image_name != '' run: | - curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.116.1 + MAX_ATTEMPTS=3 + for attempt in $(seq 1 "$MAX_ATTEMPTS"); do + if curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.117.0; then + break + fi + if [[ "$attempt" -eq "$MAX_ATTEMPTS" ]]; then + echo "::error::Failed to install Grype after ${MAX_ATTEMPTS} attempts" + exit 1 + fi + echo "Grype install attempt ${attempt} failed; retrying after backoff..." + sleep $((attempt * 5)) + done - name: Scan for vulnerabilities if: steps.set-target.outputs.image_name != '' @@ -365,7 +421,7 @@ jobs: - name: Upload SARIF to GitHub Security if: steps.set-target.outputs.image_name != '' - uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4 continue-on-error: true with: sarif_file: grype-results.sarif diff --git a/.github/workflows/supply-chain-verify.yml b/.github/workflows/supply-chain-verify.yml index a5bb1eee3..8fb1e6978 100644 --- a/.github/workflows/supply-chain-verify.yml +++ b/.github/workflows/supply-chain-verify.yml @@ -118,14 +118,55 @@ jobs: fi # Generate SBOM using official Anchore action (auto-updated by Renovate) + # anchore/sbom-action resolves the pinned syft release tag against the GitHub + # API at job runtime; transient upstream 5xx/rate-limit errors there fail the + # step even though the pin itself is valid, so continue-on-error + a + # deterministic pinned-Syft fallback (below) makes this resilient. - name: Generate and Verify SBOM if: steps.image-check.outputs.exists == 'true' + id: sbom_primary + continue-on-error: true uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 with: image: ghcr.io/${{ github.repository_owner }}/charon:${{ steps.tag.outputs.tag }} format: cyclonedx-json output-file: sbom-verify.cyclonedx.json - syft-version: v1.45.1 + syft-version: v1.51.0 + + - name: Generate SBOM fallback with pinned Syft + if: steps.image-check.outputs.exists == 'true' && always() + run: | + set -euo pipefail + + if [[ "${{ steps.sbom_primary.outcome }}" == "success" ]] && [[ -s sbom-verify.cyclonedx.json ]] && jq -e . sbom-verify.cyclonedx.json >/dev/null 2>&1; then + echo "Primary SBOM generation succeeded with valid JSON; skipping fallback" + exit 0 + fi + + echo "Primary SBOM generation failed or produced missing/invalid output; using deterministic Syft fallback" + + SYFT_VERSION="v1.51.0" + OS="$(uname -s | tr '[:upper:]' '[:lower:]')" + ARCH="$(uname -m)" + case "$ARCH" in + x86_64) ARCH="amd64" ;; + aarch64|arm64) ARCH="arm64" ;; + *) echo "Unsupported architecture: $ARCH"; exit 1 ;; + esac + + TARBALL="syft_${SYFT_VERSION#v}_${OS}_${ARCH}.tar.gz" + BASE_URL="https://github.com/anchore/syft/releases/download/${SYFT_VERSION}" + + curl -fsSLo "$TARBALL" "${BASE_URL}/${TARBALL}" + curl -fsSLo checksums.txt "${BASE_URL}/syft_${SYFT_VERSION#v}_checksums.txt" + + grep " ${TARBALL}$" checksums.txt > checksum_line.txt + sha256sum -c checksum_line.txt + + tar -xzf "$TARBALL" syft + chmod +x syft + + ./syft "ghcr.io/${{ github.repository_owner }}/charon:${{ steps.tag.outputs.tag }}" -o cyclonedx-json=sbom-verify.cyclonedx.json - name: Verify SBOM Completeness if: steps.image-check.outputs.exists == 'true' diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 507bbd67f..e9b8bd408 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -163,7 +163,7 @@ graph TB | **Base Image** | Debian Trixie Slim | Latest | Security-hardened base | | **CI/CD** | GitHub Actions | N/A | Automated testing and deployment | | **Registry** | Docker Hub + GHCR | N/A | Image distribution | -| **Security Scanning** | Trivy + Grype | Latest | Vulnerability detection | +| **Security Scanning** | Trivy + Grype + Semgrep | Latest | Vulnerability detection | | **SBOM Generation** | Syft | Latest | Software Bill of Materials | | **Signature Verification** | Cosign | Latest | Supply chain integrity | @@ -1373,7 +1373,7 @@ go test ./integration/... 1. **Lint:** golangci-lint, ESLint, markdownlint, hadolint 2. **Test:** Go tests, Vitest, Playwright -3. **Security:** Trivy, CodeQL, Grype, Govulncheck +3. **Security:** Trivy, CodeQL, Grype, Govulncheck, Semgrep 4. **Build:** Docker image build 5. **Coverage:** Upload to Codecov (85% gate) — `backend`, `frontend`, and `agent` each upload under a distinct Codecov flag @@ -1524,6 +1524,7 @@ as long as release-please continues creating bare `v*` tags. - Trivy: Fast vulnerability scanning (filesystem) - Grype: Deep image scanning (layers, dependencies) - CodeQL: Static analysis (Go, JavaScript) + - Semgrep: Static analysis for security anti-patterns (Go, JS/TS, React, secrets, Dockerfile) 3. **Cryptographic Signing:** - Cosign signs Docker images with keyless signing (OIDC) diff --git a/Dockerfile b/Dockerfile index 14f618f9d..6fa1c87f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ ARG BUILD_DEBUG=0 # ---- Pinned Toolchain Versions ---- # renovate: datasource=docker depName=golang versioning=docker -ARG GO_VERSION=1.26.5 +ARG GO_VERSION=1.26.6 # renovate: datasource=docker depName=alpine versioning=docker ARG ALPINE_IMAGE=alpine:3.24.1@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b @@ -25,9 +25,14 @@ ARG CROWDSEC_RELEASE_SHA256=704e37121e7ac215991441cef0d8732e33fa3b1a2b2b88b53a0b # renovate: datasource=github-tags depName=expr-lang/expr extractVersion=^v(?.+)$ ARG EXPR_LANG_VERSION=1.17.8 # renovate: datasource=go depName=golang.org/x/net -ARG XNET_VERSION=0.57.0 +ARG XNET_VERSION=0.58.0 # renovate: datasource=go depName=golang.org/x/crypto -ARG XCRYPTO_VERSION=0.54.0 +ARG XCRYPTO_VERSION=0.55.0 +# klauspost/compress DoS/resource-exhaustion fix, matching how golang.org/x/crypto +# is patched above: pinned here so the CrowdSec/cscli and Caddy binaries (which +# pull it in transitively) are patched immediately, ahead of upstream releases. +# renovate: datasource=go depName=github.com/klauspost/compress +ARG KLAUSPOST_COMPRESS_VERSION=1.19.2 # renovate: datasource=npm depName=npm ARG NPM_VERSION=12.0.2 @@ -299,6 +304,7 @@ ARG XCADDY_VERSION=0.4.6 ARG EXPR_LANG_VERSION ARG XNET_VERSION ARG XCRYPTO_VERSION +ARG KLAUSPOST_COMPRESS_VERSION ARG CROWDSEC_VERSION # hadolint ignore=DL3018 @@ -382,6 +388,9 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ _retry go get github.com/hslatman/ipstore@v0.4.0; \ _retry go get golang.org/x/crypto@v${XCRYPTO_VERSION}; \ _retry go get golang.org/x/net@v${XNET_VERSION}; \ + # klauspost/compress DoS/resource-exhaustion fix. Affects /usr/bin/caddy + # (transitive dependency). Fix available at v1.18.7. + _retry go get github.com/klauspost/compress@v${KLAUSPOST_COMPRESS_VERSION}; \ # GHSA-hrxh-6v49-42gf: grpc-go xDS RBAC and HTTP/2 vulnerabilities # Patched in grpc-go v1.82.1. Pin here so the Caddy binary is patched immediately. # renovate: datasource=go depName=google.golang.org/grpc @@ -516,6 +525,7 @@ ARG CROWDSEC_VERSION ARG CROWDSEC_RELEASE_SHA256 ARG EXPR_LANG_VERSION ARG XNET_VERSION +ARG KLAUSPOST_COMPRESS_VERSION # hadolint ignore=DL3018 RUN apk add --no-cache git clang lld @@ -554,6 +564,9 @@ RUN set -e; \ # renovate: datasource=go depName=golang.org/x/crypto _retry go get golang.org/x/crypto@v0.52.0; \ _retry go get golang.org/x/net@v${XNET_VERSION}; \ + # klauspost/compress DoS/resource-exhaustion fix. Affects /usr/local/bin/crowdsec + # and /usr/local/bin/cscli (transitive dependency). Fix available at v1.18.7. + _retry go get github.com/klauspost/compress@v${KLAUSPOST_COMPRESS_VERSION}; \ # GHSA-hrxh-6v49-42gf: grpc-go xDS RBAC and HTTP/2 vulnerabilities # Patched in grpc-go v1.82.1. Pin here so the CrowdSec binary is patched immediately. # renovate: datasource=go depName=google.golang.org/grpc @@ -590,6 +603,12 @@ RUN set -e; \ # GHSA-r277-6w6q-xmqw: kin-openapi ValidationHandler.Load() Fail-Open Authentication Bypass via NoopAuthenticationFunc Default # renovate: datasource=go depName=github.com/getkin/kin-openapi _retry go get github.com/getkin/kin-openapi@v0.144.0; \ + # CVE-2026-56864 / CVE-2026-56865: golang.org/x/mod/sumdb GOSUMDB tile-verification bypass + # (a colluding GOPROXY+GOSUMDB pair could forge sumdb tiles / serve module content outside + # the transparency log). Affects /usr/local/bin/crowdsec and /usr/local/bin/cscli — go mod + # tidy's MVS resolution otherwise lands on v0.38.0. Fix available at v0.40.0. + # renovate: datasource=go depName=golang.org/x/mod + _retry go get golang.org/x/mod@v0.40.0; \ _retry go mod tidy # Fix compatibility issues with expr-lang v1.17.7 @@ -688,7 +707,7 @@ SHELL ["/bin/ash", "-o", "pipefail", "-c"] # Note: In production, users should provide their own MaxMind license key # This uses the publicly available GeoLite2 database # In CI, timeout quickly rather than retrying to save build time -ARG GEOLITE2_COUNTRY_SHA256=b4f624e1411c28701d724503b8d15ed4997de70cb6ea05d6f11bf572ea552240 +ARG GEOLITE2_COUNTRY_SHA256=8cc00bbcd9734df804acc36196c84abe65c2ef4beb4294c2bf4d25ac356db933 RUN mkdir -p /app/data/geoip && \ if [ "$CI" = "true" ] || [ "$CI" = "1" ]; then \ echo "⏱️ CI detected - quick download (10s timeout, no retries)"; \ diff --git a/SECURITY.md b/SECURITY.md index deae334b2..bf21754fd 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -997,6 +997,7 @@ for developer iteration. Tag-only overrides MUST NOT be used in CI contexts. |------|---------| | Trivy | Container image vulnerability scanning | | CodeQL | Static analysis for Go and JavaScript | +| Semgrep | Static analysis for security anti-patterns (Go, JS/TS, React, secrets, Dockerfile) | | govulncheck | Go module vulnerability scanning | | golangci-lint (gosec) | Go code linting | | npm audit | Frontend dependency scanning | @@ -1019,6 +1020,14 @@ artifacts for 90 days. **PR-Specific Scanning** — extracts and scans only the Charon application binary on each pull request. Fails the PR if CRITICAL or HIGH vulnerabilities are found in application code. +**Semgrep SAST Scan** (`.github/workflows/semgrep.yml`) — runs on every push and pull request to +`main`, `nightly`, and `development`, on manual dispatch, and weekly on Mondays at 04:00 UTC. Scans +the full repository inside a pinned `semgrep/semgrep` container using the `p/golang`, +`p/javascript`, `p/typescript`, `p/react`, `p/secrets`, and `p/dockerfile` rulesets — the same +rule configs, exclusions, and ERROR/WARNING severity gate developers already run locally via +`scripts/pre-commit-hooks/semgrep-scan.sh`. Uploads SARIF results to the GitHub Security tab and +fails the build on any blocking finding. + ### Manual Reviews - Security code reviews for all major features diff --git a/agent/go.mod b/agent/go.mod index 2235fadf1..9f3174863 100644 --- a/agent/go.mod +++ b/agent/go.mod @@ -1,11 +1,11 @@ module github.com/Wikid82/charon/agent -go 1.26.5 +go 1.26.6 require ( github.com/gorilla/websocket v1.5.3 github.com/hashicorp/yamux v0.1.2 - github.com/sirupsen/logrus v1.9.4 + github.com/sirupsen/logrus v1.10.0 github.com/stretchr/testify v1.11.1 ) diff --git a/agent/go.sum b/agent/go.sum index 0efd37fcc..00d20580c 100644 --- a/agent/go.sum +++ b/agent/go.sum @@ -18,8 +18,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sirupsen/logrus v1.10.0 h1:T8MxJJXVZkfcC5zSRMRAg2F8+lxjmUCGGWPzFxO+Msc= +github.com/sirupsen/logrus v1.10.0/go.mod h1:FXZFonkDAnFozmO+5hGAFvB0Yg9/j2SIhA/QuIkP180= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= diff --git a/backend/go.mod b/backend/go.mod index 84adc4cf6..19a9d28be 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,6 +1,6 @@ module github.com/Wikid82/charon/backend -go 1.26.5 +go 1.26.6 require ( filippo.io/age v1.3.1 @@ -17,14 +17,14 @@ require ( github.com/pkg/sftp v1.13.11 github.com/prometheus/client_golang v1.24.1 github.com/robfig/cron/v3 v3.0.1 - github.com/sirupsen/logrus v1.9.4 + github.com/sirupsen/logrus v1.10.0 github.com/stretchr/testify v1.11.1 github.com/studio-b12/gowebdav v0.13.0 - golang.org/x/crypto v0.54.0 - golang.org/x/mod v0.39.0 - golang.org/x/net v0.57.0 + golang.org/x/crypto v0.55.0 + golang.org/x/mod v0.40.0 + golang.org/x/net v0.58.0 golang.org/x/oauth2 v0.36.0 - golang.org/x/text v0.40.0 + golang.org/x/text v0.41.0 golang.org/x/time v0.15.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 diff --git a/backend/go.sum b/backend/go.sum index 06a0f1cf9..7cd563f9a 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -165,8 +165,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sirupsen/logrus v1.10.0 h1:T8MxJJXVZkfcC5zSRMRAg2F8+lxjmUCGGWPzFxO+Msc= +github.com/sirupsen/logrus v1.10.0/go.mod h1:FXZFonkDAnFozmO+5hGAFvB0Yg9/j2SIhA/QuIkP180= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -218,12 +218,12 @@ go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/arch v0.30.0 h1:sB9h+1gRGa2+LauFSV0tm8bK1J2yo1bx6/Uyi/P6DTU= golang.org/x/arch v0.30.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= -golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74= -golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY= -golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= -golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= +golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= @@ -232,12 +232,12 @@ golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= -golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/backend/internal/api/handlers/cerberus_logs_ws.go b/backend/internal/api/handlers/cerberus_logs_ws.go index 222fa78a1..c4aad94c7 100644 --- a/backend/internal/api/handlers/cerberus_logs_ws.go +++ b/backend/internal/api/handlers/cerberus_logs_ws.go @@ -41,8 +41,7 @@ func (h *CerberusLogsHandler) LiveLogs(c *gin.Context) { logger.Log().Info("Cerberus logs WebSocket connection attempt") // Upgrade HTTP connection to WebSocket - // CheckOrigin is enforced on the shared upgrader in logs_ws.go (same package). - conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) // nosemgrep: go.gorilla.security.audit.websocket-missing-origin-check.websocket-missing-origin-check + conn, err := upgradeWebSocket(c) if err != nil { logger.Log().WithError(err).Error("Failed to upgrade Cerberus logs WebSocket") return diff --git a/backend/internal/api/handlers/hecate_ws_handler.go b/backend/internal/api/handlers/hecate_ws_handler.go index b392f7940..541b5b0fe 100644 --- a/backend/internal/api/handlers/hecate_ws_handler.go +++ b/backend/internal/api/handlers/hecate_ws_handler.go @@ -37,7 +37,7 @@ func (h *HecateWSHandler) StreamLogs(c *gin.Context) { return } - conn, upgradeErr := upgrader.Upgrade(c.Writer, c.Request, nil) // nosemgrep: go.gorilla.security.audit.websocket-missing-origin-check.websocket-missing-origin-check + conn, upgradeErr := upgradeWebSocket(c) if upgradeErr != nil { logger.Log().WithError(upgradeErr).Error("hecate ws: upgrade failed") return diff --git a/backend/internal/api/handlers/logs_ws.go b/backend/internal/api/handlers/logs_ws.go index 2b846e7c5..cad751c4e 100644 --- a/backend/internal/api/handlers/logs_ws.go +++ b/backend/internal/api/handlers/logs_ws.go @@ -35,6 +35,15 @@ var upgrader = websocket.Upgrader{ }, } +// upgradeWebSocket upgrades an HTTP connection to WebSocket using the +// shared, origin-checked upgrader above. All WS handlers in this package +// MUST go through this helper rather than calling upgrader.Upgrade +// directly, so the origin check stays visible to single-file static +// analysis at every call site. +func upgradeWebSocket(c *gin.Context) (*websocket.Conn, error) { + return upgrader.Upgrade(c.Writer, c.Request, nil) +} + // LogEntry represents a structured log entry sent over WebSocket. type LogEntry struct { Level string `json:"level"` @@ -68,7 +77,7 @@ func (h *LogsWSHandler) HandleWebSocket(c *gin.Context) { logger.Log().Info("WebSocket connection attempt received") // Upgrade HTTP connection to WebSocket - conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) + conn, err := upgradeWebSocket(c) if err != nil { logger.Log().WithError(err).Error("Failed to upgrade WebSocket connection") return diff --git a/backend/internal/api/handlers/stats_handler.go b/backend/internal/api/handlers/stats_handler.go index 00276ff82..23c52c6f0 100644 --- a/backend/internal/api/handlers/stats_handler.go +++ b/backend/internal/api/handlers/stats_handler.go @@ -172,7 +172,7 @@ func (h *StatsHandler) GetStatsHealth(c *gin.Context) { // StatsWS upgrades the connection to WebSocket and streams stats push messages. // GET /api/stats/ws func (h *StatsHandler) StatsWS(c *gin.Context) { - conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) // nosemgrep: go.gorilla.security.audit.websocket-missing-origin-check.websocket-missing-origin-check + conn, err := upgradeWebSocket(c) if err != nil { return } diff --git a/docs/issues/semgrep-ci-manual-test-plan.md b/docs/issues/semgrep-ci-manual-test-plan.md new file mode 100644 index 000000000..bda5edb5a --- /dev/null +++ b/docs/issues/semgrep-ci-manual-test-plan.md @@ -0,0 +1,128 @@ +--- +title: "Manual Test Plan - Semgrep CI Security Scan" +status: Open +priority: Medium +labels: testing, ci, security +--- + +# Test Objective + +Confirm that the new `.github/workflows/semgrep.yml` CI workflow behaves correctly once it +actually runs against a live GitHub Actions PR — image pull, scan execution, SARIF upload, and +the hard-fail gate. This is the one part of the Semgrep CI Security Scan feature that could not +be verified locally: Supervisor code review and the qa-security audit both passed (see +`docs/plans/current_spec.md` and `docs/reports/qa_report.md`), but neither can observe a real +GitHub Actions runner pulling the pinned container image or timing a full-repo scan under actual +CI conditions. + +# What Was Built + +- `scripts/pre-commit-hooks/semgrep-scan.sh` gained an additive, backward-compatible + `SEMGREP_SARIF_OUTPUT` env var so CI can reuse the exact same scan invocation developers already + run locally, for both a SARIF-producing pass and a hard-fail gate pass. +- `.github/workflows/semgrep.yml` runs that script inside a pinned + `semgrep/semgrep:1.173.0@sha256:...` container on every push/PR to `main`, `nightly`, and + `development`, on manual dispatch, and weekly (Mondays 04:00 UTC). It uploads SARIF results to + the GitHub Security tab and hard-fails the job on any ERROR/WARNING-severity finding. +- `scripts/ci/check-semgrep-parity.sh` guards against the workflow and the local script silently + drifting apart in the future. +- `SECURITY.md` and `ARCHITECTURE.md` were updated to document the new coverage. + +Commits: `6bf066f8` (script hook + parity guard), `2fbecf07` (workflow), `7c6fb04f` (docs). + +# Prerequisites + +- A pull request open against `development` (or `main`/`nightly`) that includes these three + commits, so `semgrep.yml`'s `pull_request` trigger fires. +- Repo admin/write access to view the Actions run and the Security → Code scanning alerts tab. + +# Manual Scenarios + +## 1) Workflow triggers and appears as a PR check + +- [ ] Open the PR containing commits `6bf066f8`, `2fbecf07`, `7c6fb04f`. +- [ ] **Expected**: A check named **Semgrep SAST Scan** (job `semgrep-scan` in workflow + `Semgrep - SAST Scan`) appears in the PR's checks list shortly after the PR is opened or + updated. + +## 2) Pinned container image pulls successfully + +- [ ] Open the Actions run for the Semgrep workflow, expand the earliest steps. +- [ ] **Expected**: No container-pull error (e.g. `manifest unknown`, rate-limit, or timeout + pulling `semgrep/semgrep:1.173.0@sha256:...`). The job proceeds past the container-setup + phase into "Checkout repository." + +## 3) Job completes within the timeout; check actual timing + +- [ ] Note the total run duration for the `semgrep-scan` job once it finishes. +- [ ] **Expected**: Job completes well within the current `timeout-minutes: 15` cap. +- [ ] **If the run takes noticeably close to 15 minutes** (cold image pull + rule-registry fetch + was never observed live before this PR — flagged as an open risk by both Supervisor and + DevOps): file a follow-up to bump `timeout-minutes` to ~20-25 in `semgrep.yml`. This is not + a blocker for merging this PR, but should not be left unaddressed if observed. + +## 4) SARIF results appear in the Security tab + +- [ ] Navigate to the repo's **Security → Code scanning alerts** tab. +- [ ] Filter by tool **Semgrep**, category **semgrep**. +- [ ] **Expected**: A scan result is listed for the commit/PR, even if it shows 0 findings (a + SARIF upload with an empty `results` array is still a valid, visible scan entry — this + confirms the upload step itself worked, not just that the repo is clean). + +## 5) Hard-fail gate passes on a clean repo + +- [ ] Check the **Run Semgrep (hard-fail gate)** step's log output. +- [ ] **Expected**: Step exits 0. The repo is expected to be clean — 0 findings was reproduced + locally multiple times (both in DevOps validation and independently in QA's audit) — so + this step should pass without needing any fix commits. + +## 6) Job summary renders correctly + +- [ ] Open the Actions run's **Summary** tab (not the individual job log). +- [ ] **Expected**: A "Semgrep SAST Scan Results" section is present, listing the rulesets + scanned (`p/golang, p/javascript, p/typescript, p/react, p/secrets, p/dockerfile`), the + severity gate (`ERROR, WARNING`), and a clear PASSED/FAILED line matching the job's actual + outcome. + +# Expected Results + +| Scenario | Expected outcome | +|---|---| +| PR trigger | "Semgrep SAST Scan" check appears on the PR | +| Image pull | Pinned `semgrep/semgrep` image pulls with no error | +| Timing | Job finishes comfortably under 15 minutes | +| SARIF upload | Result visible under Security → Code scanning alerts, tool "Semgrep", category `semgrep` | +| Hard-fail gate | Passes (0 findings expected) | +| Job summary | Renders ruleset, severity gate, and pass/fail line in the run summary tab | + +# Pass / Fail Criteria + +**PASS** — All six scenarios behave as expected: the check appears, the image pulls, the job +finishes well under the timeout, SARIF results are visible in the Security tab under the correct +category, the gate step passes, and the job summary renders correctly. + +**FAIL** — Any of: the check never appears on the PR, the image fails to pull, the job times out +or runs suspiciously close to the 15-minute cap, no SARIF entry appears in the Security tab, the +gate step fails unexpectedly on a repo believed to be clean, or the job summary is missing/blank. + +A FAIL on the gate step specifically should be triaged on its merits (a real finding vs. a CI +environment issue) before assuming the feature itself is broken — see +`docs/plans/current_spec.md` §3.7 for documented edge cases. + +# Known Follow-Ups (not blockers) + +1. ~~**Renovate coverage for the pinned image is not yet configured.**~~ **Resolved** (commit + `9dc2be4e`). Added an explicit custom regex manager in `.github/renovate.json`, anchored on a + `# renovate: datasource=docker depName=semgrep/semgrep` comment above the `image:` line in + `semgrep.yml`, mirroring the existing Alpine-image digest tracker pattern used elsewhere in this + repo. Confirm on the next Semgrep image bump that Renovate actually opens a PR as expected. +2. **`timeout-minutes` may need adjustment after observing real timing.** Set to 15 based on local + estimates (~45-48s per full-repo scan pass locally); this was never observed against a cold + image pull + rule-registry fetch on an actual GitHub Actions runner. See Scenario 3 above — + bump to ~20-25 if the real run comes in close to the cap. + +# Related + +- `docs/plans/current_spec.md` — full implementation plan for the Semgrep CI Security Scan feature. +- `docs/reports/qa_report.md` — QA/security audit (PASS, no blocking issues). +- Commits `6bf066f8`, `2fbecf07`, `7c6fb04f` on `development`. diff --git a/docs/plans/archive/2026-08-14_semgrep-ci-security-scan-plan.md b/docs/plans/archive/2026-08-14_semgrep-ci-security-scan-plan.md new file mode 100644 index 000000000..7aa3ef37f --- /dev/null +++ b/docs/plans/archive/2026-08-14_semgrep-ci-security-scan-plan.md @@ -0,0 +1,505 @@ +# Semgrep CI Security Scan — Implementation Plan + +Status: Planning complete, revised per Supervisor review (round 2). +Owner for implementation: **devops** agent (CI/CD-only change; no backend-dev or frontend-dev involvement — no application code, no models, no UI). +Branch: current working branch (`development`) per `CLAUDE.md` — no worktree. +PR base branch: `development` (standard feature PR convention observed in this repo; `main` only receives weekly `nightly` promotion merges). + +--- + +## 1. Introduction + +### 1.1 Objective + +Add an independent Semgrep SAST scan to GitHub Actions CI that reproduces, byte-for-byte, the same scan behavior developers already run locally via `scripts/pre-commit-hooks/semgrep-scan.sh` (wired through `lefthook.yml`'s `pre-commit`/`pre-push`/`security-full` targets and `make security-local`). Today, Semgrep coverage exists **only** on the developer's machine — CI has zero Semgrep footprint (confirmed: no match in `.github/workflows/`, `.github/renovate.json`, or any Dockerfile/compose file). This means: + +- A developer who bypasses lefthook (`--no-verify`, an emergency hotfix, a machine without semgrep installed) ships code with no Semgrep signal at all. +- Nobody re-verifies the "clean" local Semgrep run against a controlled, versioned environment — the local binary's version, ruleset revision, and installed registry rules can silently drift per-developer with no CI backstop. + +This plan adds CI-side Semgrep coverage that is authoritative (independent of the developer's local environment) while staying faithful to the existing local invocation. + +### 1.2 Goals + +1. A new CI job runs the **exact same** rule configs, exclusions, and severity/error-gating behavior as `scripts/pre-commit-hooks/semgrep-scan.sh`'s default (no-override) path, scanning the full repo. +2. Semgrep's version is pinned in CI (image tag + digest) — today there is no version pin anywhere in the repo for Semgrep, local or CI. +3. Findings are visible in the GitHub Security tab (SARIF upload), consistent with how CodeQL and Trivy results are surfaced today. +4. A hard-fail gate blocks the PR/branch on ERROR/WARNING findings, mirroring the local script's `--error` behavior — CI is a gate, not just an informational report. +5. `scripts/pre-commit-hooks/semgrep-scan.sh`'s binary/version resolution logic (the `command -v semgrep` check, §2.1) is **not touched** — that stays developer-local tooling, per the original brief's explicit scope boundary. The script's rule-config/exclude/severity logic, by contrast, **is** extended with one small, additive, backward-compatible hook (§2.7/§3.0) so CI can reuse it directly instead of duplicating it — see §2.7 for why this is a different constraint than "freeze the whole file," and why the narrower reading is the right one. +6. Documentation (`SECURITY.md` and `ARCHITECTURE.md`) is updated to reflect the new CI coverage. + +### 1.3 Non-goals + +- No change to how the local pre-commit/pre-push semgrep **binary** is discovered, installed, or versioned (the `command -v semgrep` / exit-127 block in `scripts/pre-commit-hooks/semgrep-scan.sh` is untouched). +- No new GitHub Action marketplace dependency requiring npm/JS runtime — Semgrep ships as a self-contained CLI in an official container image, which is used directly. +- No change to `.gitignore`, `.dockerignore`, `.codecov.yml`, or any `Dockerfile` (see §2.9 — reviewed explicitly, no changes needed). +- No attempt to unify Trivy's/CodeQL's SARIF-upload plumbing into a shared reusable workflow — out of scope for this feature; each scanner's workflow remains independent, consistent with current repo structure (`codeql.yml`, `security-pr.yml`, `security-weekly-rebuild.yml` are all separate files today). + +--- + +## 2. Research Findings + +### 2.1 Local Semgrep invocation (`scripts/pre-commit-hooks/semgrep-scan.sh`) + +Full script behavior (verified by reading the file): + +- Requires `semgrep` on `PATH`; exits 127 if missing (this resolution logic is untouched by this plan — see §1.3). +- Default rule configs (used unless `SEMGREP_CONFIG` env override is set): + ``` + --config p/golang + --config p/javascript + --config p/typescript + --config p/react + --config p/secrets + --config p/dockerfile + ``` +- Targets: staged files if passed as args (lefthook `pre-commit`), else full-repo default `Dockerfile backend frontend/src scripts .github/workflows` (lefthook `security-full` / manual run). +- Exact scan flags (current, pre-change): + ``` + semgrep scan \ + "${SEMGREP_CONFIGS[@]}" \ + --severity ERROR \ + --severity WARNING \ + --error \ + --exclude "frontend/node_modules" \ + --exclude "frontend/coverage" \ + --exclude "frontend/dist" \ + --exclude-rule "go.secrets.gorm.gorm-empty-password.gorm-empty-password" \ + "${TARGETS[@]}" + ``` +- `--error` makes semgrep exit non-zero if any ERROR/WARNING-severity finding exists — this is the local "hard fail" behavior CI must reproduce. + +Wiring confirmed in `lefthook.yml`: +- `pre-commit.semgrep` (line ~113-116): glob-scoped, staged-files-only, blocking. +- `security-full.semgrep` (line ~137-140, manual stage, `lefthook run security-full`): full-repo, no args → this is the invocation CI should mirror most closely (full-repo, not staged-file-scoped). +- `Makefile:security-local` additionally runs `SEMGREP_CONFIG=p/golang` as a fast pre-push subset — this is a narrower override path, not the target for CI parity (CI should mirror the **full** default ruleset, matching `security-full`). + +### 2.2 Confirmed: zero Semgrep footprint in CI today + +`grep -rn "semgrep" .github/workflows/ .github/renovate.json` (and Dockerfiles/compose) returns no matches. Semgrep is 100% local-only today. (Note: the repo's Renovate config lives at `.github/renovate.json`, not a root-level `renovate.json` — corrected throughout this plan.) + +### 2.3 Existing CI patterns to mirror + +**`.github/workflows/codeql.yml`** (closest pattern for a source-level SAST tool): +- Triggers: `pull_request`/`push` on `[main, nightly, development]`, `workflow_dispatch`, weekly `schedule` cron (`0 3 * * 1`, Mondays 03:00 UTC). +- `concurrency` group keyed on workflow/event/ref, `cancel-in-progress: true`. +- `permissions:` declared at **both** the workflow (top) level and again, identically, at job level (`contents: read`, `security-events: write`, `actions: read`, `pull-requests: read`). +- All third-party actions pinned by commit SHA with a `# vX.Y.Z` trailing comment, e.g. `github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4`. +- Has a **parity guard** step ("Verify CodeQL parity guard" → `scripts/ci/check-codeql-parity.sh`) that runs *before* the scan, structurally checking that local pre-commit scripts, `.vscode/tasks.json`, and the CI workflow all agree on query-suite pinning and trigger branches — added specifically because CodeQL's local/CI ruleset previously drifted silently (see `check-codeql-parity.sh` comment referencing a real incident: a suppressed finding rode through PR #1216 unnoticed because local and CI independently duplicated blocking logic). +- Emits results to `$GITHUB_STEP_SUMMARY`, then a **separate, later step** does the actual hard-fail (`Fail on High-Severity Findings`) — reporting and gating are deliberately split into two steps so the summary always renders even on failure. + +**`.github/workflows/security-pr.yml`** (closest pattern for "pinned scanner → SARIF upload → hard-fail gate"): +- Runs Trivy via `aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25` (SHA-pinned, `# aquasecurity/trivy-action 0.36.0` comment), with an explicit `version: 'v0.73.0'` input additionally pinning the *scanner* version, not just the action wrapper. +- Runs the scan **twice**: once with `format: 'sarif'` (`continue-on-error: true`, purely for the Security tab), then again with `format: 'table'` + `exit-code: '1'` (no continue-on-error) as the actual blocking gate. It also has an explicit "Check Trivy SARIF output exists" gating step between the SARIF-producing run and the upload step. This two-pass "report, then gate" split, plus the existence check, is the direct template for Semgrep's SARIF-vs-hard-fail split (§3.3). +- SARIF uploaded via `github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7` (same SHA-pinned action already used elsewhere in this repo for SARIF ingestion — no new third-party dependency needed for the upload step). +- Trigger shape is materially more complex than needed here (`workflow_run` chaining off `docker-build.yml`, PR-number resolution, artifact download) because Trivy scans a **built container image**. Semgrep scans **source**, so it needs none of that — it can trigger directly on `push`/`pull_request` like CodeQL, with no dependency on a prior Docker build. + +### 2.4 Repo-wide pinning convention + +Every third-party action in this repo is pinned to an exact commit SHA with a trailing `# vX.Y.Z` comment — never a floating tag, never `@latest`. This is enforced by convention/review, not currently by a lint rule for actions specifically. Any new job must follow this exactly. + +### 2.5 Semgrep version/mechanism research + +Options considered: + +| Option | Assessment | +|---|---| +| `pip install semgrep==` on `ubuntu-latest` | Works, but reintroduces a Python toolchain dependency into a Go+TS repo purely for CI plumbing (`CLAUDE.md`: "No Python — do not introduce Python scripts or requirements"). While this is arguably a tooling install rather than an authored script, it still pulls in `pip`/Python resolution behavior (version solving, transitive dependency drift) that the repo's own conventions steer away from. Rejected. | +| `semgrep/semgrep-action` (formerly `returntocorp/semgrep-action`) marketplace GitHub Action | Semgrep's own current CI docs no longer lead with this as the primary GitHub Actions pattern; it's a thin wrapper around the same official Docker image. Using it would add an extra layer of indirection (an Action wrapping an image) for no behavioral benefit over using the image directly, and re-pinning *that* action's SHA doesn't pin Semgrep's own version any more precisely than pinning the image does. Rejected in favor of the image directly. | +| Official `semgrep/semgrep` Docker image, used as a job-level `container:`, pinned by exact tag **and** digest | Matches this repo's SHA-pinning strictness (a digest is the container-image equivalent of an action's commit SHA — both are content-addressed, immutable references). Gives the CLI directly, with the identical `semgrep scan ...` invocation used locally — maximizes behavioral parity with `semgrep-scan.sh`. **Selected.** | + +Confirmed via the Semgrep GitHub releases API (`api.github.com/repos/semgrep/semgrep/releases/latest`) and PyPI, current stable version at plan time is **`1.173.0`**. Resolved the corresponding Docker Hub manifest digest for `semgrep/semgrep:1.173.0`: + +``` +sha256:67319956da3dcb58baf5b322899c15458e3963e7018a86aeeb5cd224e69cb77a +``` + +Pinned reference to use in the workflow: + +``` +semgrep/semgrep:1.173.0@sha256:67319956da3dcb58baf5b322899c15458e3963e7018a86aeeb5cd224e69cb77a +``` + +**Note for the implementer (devops agent):** re-resolve this digest at implementation time (`docker buildx imagetools inspect semgrep/semgrep:1.173.0` or the registry API) rather than trusting the value transcribed into this plan verbatim, in case the tag's digest has moved between planning and implementation (Docker Hub does not guarantee a tag's digest is immutable the way a Git SHA is — pinning to the *tag+digest pair as observed at merge time* is the achievable guarantee here, and Renovate, already active in this repo via `.github/renovate.json`, will pick up future digest/tag bumps the same way it tracks other pinned SHAs if configured to watch this image — see §3.7 edge case). + +**Known gotcha (Semgrep's own docs, `semgrep.dev/docs/kb/semgrep-ci/using-nonroot-docker-image-with-gha`):** running `semgrep/semgrep` as a job-level `container:` against a `actions/checkout`-produced workspace can hit git's "dubious ownership" safety check because the container user doesn't match the checkout's file ownership. Mitigate with an explicit `git config --global --add safe.directory "$GITHUB_WORKSPACE"` step before invoking `semgrep-scan.sh` (§3.3). + +**Correction (Supervisor round 2, required change 1):** the container image reference **cannot** be centralized in a workflow-level `env:` var and referenced as `container.image: ${{ env.SEMGREP_IMAGE }}`. GitHub Actions' documented context-availability rules do not expose the `env` context to `jobs..container` — this is a known, currently-true limitation (not something that needs "verification at implementation time"; treating it as an open question in the prior draft was itself the error). The plan now specifies the pinned string **inlined directly** in `container.image` as the only correct form (§3.2) — no `env` indirection. + +### 2.6 Placement decision: new file vs. an existing workflow + +*(Per mid-task correction from Management: decide the best location and justify it, rather than defaulting to a new file. Approved as-is by Supervisor round 2 — no changes in this revision.)* + +Three placements were evaluated: + +| Placement | Verdict | +|---|---| +| **New job added to `codeql.yml`** | Rejected. `codeql.yml`'s entire structure is a `strategy.matrix` over CodeQL *languages* (`go`, `javascript-typescript`), with per-language conditional steps (`if: matrix.language == 'go'`) for Go toolchain setup/build and the CodeQL parity guard. Semgrep is not a CodeQL language variant — it's a different tool with a different container, different config format, and a different (single, non-matrixed) invocation. Bolting it in as a third matrix leg would force awkward `if: matrix.language == 'semgrep'` conditionals across steps that don't apply to it (Autobuild, `codeql-action/init`, Go build verification), degrading the readability of a file whose entire premise is "one job, matrixed by CodeQL language." Also couples Semgrep's schedule/trigger lifecycle to CodeQL's, when they are independent tools that should be able to fail, be disabled, or be re-scheduled independently. | +| **New job added to `security-pr.yml`** | Rejected. That workflow's trigger shape and majority of its steps exist *solely* to solve "how do I scan a Docker image that was already built by a separate upstream workflow" — PR-number resolution from `workflow_run` payloads, artifact download/load fallback logic, container extraction of the `charon` binary, a trust-boundary validation step for the `workflow_run` event. None of that applies to Semgrep, which scans source text directly on `push`/`pull_request` with no dependency on `docker-build.yml` having run first. Adding a source-scanning job to an image-scanning workflow would mean either (a) it inherits triggers/conditions built for image scanning that don't fit it (e.g. `workflow_dispatch` inputs are `pr_number`-shaped, meaningless for a source scan), or (b) it needs its own parallel `if:` conditions bolted onto an already condition-heavy file, adding complexity for no shared benefit — the two jobs would share a file but no actual logic. | +| **New file: `.github/workflows/semgrep.yml`** | **Selected.** Semgrep is source-level SAST, triggered directly on `push`/`pull_request`/`schedule`/`workflow_dispatch` — structurally identical in trigger shape to `codeql.yml`, but a distinct tool with its own container, config, and failure/gating semantics. This also matches the repo's existing convention of **one file per scanner**: `codeql.yml` (CodeQL), `security-pr.yml` (Trivy on PR images), `security-weekly-rebuild.yml` (Trivy weekly full scan) are already separate files rather than merged into one "security" workflow, even though they're conceptually related. A dedicated `semgrep.yml` continues that pattern: each scanner is independently triggerable, independently disable-able, and independently readable, at the cost of one more file — a cost the repo has already accepted three times over for its other scanners. | + +### 2.7 "Freeze the whole script" reconsidered — design revision (Supervisor round 2, required change 3) + +**The original brief's non-goal, re-read precisely:** *"Do NOT touch `scripts/pre-commit-hooks/semgrep-scan.sh`'s binary/version resolution logic itself — that's explicitly out of scope, reserved for the user's own local tooling."* This is a constraint about **binary/version discovery** (the `command -v semgrep` / exit-127 block, §2.1) — not a blanket freeze on every line of the file. The first draft of this plan over-read it into "never touch this file at all," which forced: + +- A second, hand-written `semgrep scan ...` invocation inline in the workflow YAML, duplicating all six `--config` flags, all three `--exclude` flags, and the `--exclude-rule` value. +- A `check-semgrep-parity.sh` script whose primary job was detecting drift between that duplicated invocation and the real script. +- Pressure to extract shared assertion helpers out of `check-codeql-parity.sh` mainly to support that parity script's config-matching checks. + +That is real, avoidable complexity, not an inherent requirement. **Revised design (adopted — option (a) from Supervisor's feedback):** add one small, additive, backward-compatible hook to `semgrep-scan.sh` itself, leaving the binary/version-resolution logic (the actual thing the non-goal protects) completely untouched: + +```bash +# Existing lines (SEMGREP_CONFIGS / TARGETS construction) unchanged above this point. + +if [ -n "${SEMGREP_SARIF_OUTPUT:-}" ]; then + OUTPUT_FLAGS=(--sarif --output "${SEMGREP_SARIF_OUTPUT}") +else + OUTPUT_FLAGS=(--error) +fi + +semgrep scan \ + "${SEMGREP_CONFIGS[@]}" \ + --severity ERROR \ + --severity WARNING \ + "${OUTPUT_FLAGS[@]}" \ + --exclude "frontend/node_modules" \ + --exclude "frontend/coverage" \ + --exclude "frontend/dist" \ + --exclude-rule "go.secrets.gorm.gorm-empty-password.gorm-empty-password" \ + "${TARGETS[@]}" +``` + +Behavior: +- **`SEMGREP_SARIF_OUTPUT` unset (every existing call site — `pre-commit`, `pre-push`/`security-full`, `make security-local`):** `OUTPUT_FLAGS=(--error)` — byte-identical to today's behavior. Zero change for any existing developer workflow. +- **`SEMGREP_SARIF_OUTPUT=` set (new — CI only):** swaps `--error` for `--sarif --output `, while every `--config`, `--exclude`, and `--exclude-rule` argument stays exactly as-is, sourced from exactly one place. + +This lets CI invoke the **same script** for both the SARIF-producing pass and the hard-fail gate pass (§3.3 steps 5 and 8), varying only an env var. Consequences: + +- The duplicated `--config`/`--exclude` list in the workflow YAML is **eliminated entirely** — there is now exactly one place (`semgrep-scan.sh`) that defines what gets scanned, for both local and CI, for both the reporting pass and the gating pass. +- `check-semgrep-parity.sh` shrinks correspondingly (§3.4) — it no longer needs to compare two independent config lists (nothing to compare; there's only one). It still has a real, narrower job: confirming the additive hook isn't silently removed, confirming the workflow actually delegates to the script for both passes (rather than a future edit reintroducing an inline duplicate), and confirming the image pin and trigger branches stay correct. This is a smaller, more clearly justified guard than the original draft's. +- The pressure to extract `scripts/ci/lib/workflow-yaml-asserts.sh` out of `check-codeql-parity.sh` is now a plain, optional DRY nicety (the branch-check helper is still needed by both scripts) rather than something load-bearing for the config-parity story — see §3.5. + +**Why not stop here and also drop the parity guard entirely?** Because two failure-independent invariants remain worth checking even with zero config duplication: (1) that the additive `SEMGREP_SARIF_OUTPUT` hook stays present in the script (a future refactor of `semgrep-scan.sh` could drop it without realizing CI depends on it), and (2) that the workflow keeps *delegating* to the script for both passes rather than a future edit reintroducing an inline `semgrep scan` call (e.g. someone "simplifying" the SARIF step by hand and accidentally dropping an `--exclude`). Both are cheap, structural, grep-level checks — proportionate, not over-engineering, and much smaller than the original draft's guard (§3.4). + +This section supersedes the original §2.7 ("Parity guard: warranted, and why") from the first draft. + +### 2.8 Documentation review + +- **`SECURITY.md`** (`## Security Audits & Scanning` → `### Automated Scanning` table, lines 992-1020): lists Trivy, CodeQL, govulncheck, golangci-lint (gosec), npm audit, and a `### Scanning Workflows` subsection describing each workflow file's purpose (`docker-build.yml`, `supply-chain-verify.yml`, `security-weekly-rebuild.yml`, PR-specific scanning). **This is the primary file to update** — add a `Semgrep` row to the table and a new `**Semgrep SAST Scan**` paragraph under `### Scanning Workflows` describing `.github/workflows/semgrep.yml`. +- **`docs/security.md`**: verified by full-text search (`codeql|trivy|scan|pipeline`, no matches) — this file is entirely about the Cerberus runtime security feature (CrowdSec/WAF/access lists), unrelated to the CI/SAST scanning pipeline. **No change needed here.** +- **`ARCHITECTURE.md` (Supervisor round 2, required change 2 — added to scope):** `CLAUDE.md` requires `ARCHITECTURE.md` updates for changes touching security architecture, and this file already documents the CI security-scanning stack in three places that must be kept current: + - Line 166, tech-stack table: `| **Security Scanning** | Trivy + Grype | Latest | Vulnerability detection |` — append Semgrep, e.g. `Trivy + Grype + Semgrep`. + - Line 1376, CI Jobs list: `3. **Security:** Trivy, CodeQL, Grype, Govulncheck` — append `, Semgrep`. + - Lines 1498-1501, "Container Scanning" components list (`Trivy: ...`, `Grype: ...`, `CodeQL: ...`) — add a fourth line, `Semgrep: Static analysis for security anti-patterns (Go, JS/TS, React, secrets, Dockerfile)`, consistent with the existing one-line-per-tool style. + - This is now part of Commit 3's scope (§6) and Acceptance Criteria (§5), alongside `SECURITY.md`. + +### 2.9 Ignore-file / build-file review (explicit confirmation per `CLAUDE.md`) + +- **`.gitignore`**: already contains a blanket `*.sarif` ignore (line 189) with a narrow `!scripts/security/testdata/*.sarif` carve-out (line 190). A new `semgrep-results.sarif` file in the repo root matches the existing wildcard — **no change needed**. +- **`.dockerignore`**: already excludes `*.sarif` (line 179) — irrelevant anyway, since this is a CI-only workflow change with no Docker image content change — **no change needed**. +- **`.codecov.yml`**: workflow-only YAML change, produces no coverage-relevant files — **no change needed**. +- **Any `Dockerfile`**: not touched; Semgrep runs in its own CI container, never inside the Charon application image — **no change needed**. + +(Approved as-is by Supervisor round 2 — no changes in this revision.) + +### 2.10 Commit scope: `feat:` vs `feat(security):` + +Per `CLAUDE.md`, `feat:`/`fix:`/`perf:` trigger Docker builds; `chore:` skips them, and `feat(security):`/`fix(security):` is reserved for "genuinely security-relevant... real vulnerability fixes, new protective mechanisms." **Decision:** the workflow-adding commit (Commit 2, §6) qualifies as a **new protective mechanism** — it is, definitionally, new automated vulnerability/anti-pattern detection gating merges — so it uses `feat(security):`, not plain `feat:`. Commit 1 (the additive `semgrep-scan.sh` hook + parity guard) also touches genuine security tooling directly and is scoped `feat(security):` for the same reason. Commit 3 (docs) stays `docs:`, matching repo convention for documentation-only changes regardless of what they document. Per `CLAUDE.md`'s vagueness requirement for `(security)` subjects, none of these commit subjects name a vulnerability class or attack vector — they describe the category ("add CI security scanning coverage") only, which is appropriate here since this isn't a vulnerability fix in the first place, just extra coverage. + +**Nuance retained from the original draft:** this change touches zero Docker-build-relevant paths (no `Dockerfile`, no backend/frontend source), so the triggered Docker build (a side effect of `feat`/`feat(security)` prefixes repo-wide) is harmless but expected — not a sign something is wrong with a "just workflow files + one shell script" PR. + +--- + +## 3. Technical Specifications + +### 3.0 Change to `scripts/pre-commit-hooks/semgrep-scan.sh` (additive, in scope per §2.7) + +**File:** `scripts/pre-commit-hooks/semgrep-scan.sh` +**Change:** insert the `OUTPUT_FLAGS` branch (§2.7) immediately before the existing `semgrep scan \` invocation, and replace the invocation's `--error` line with `"${OUTPUT_FLAGS[@]}"`. No other line in the file changes — the `command -v semgrep` check, the `SEMGREP_CONFIG` override branch, and the `TARGETS` construction are byte-identical to today. +**Backward compatibility:** every existing call site (`lefthook.yml`'s `pre-commit.semgrep`, `security-full.semgrep`, `Makefile`'s `security-local`) never sets `SEMGREP_SARIF_OUTPUT`, so `OUTPUT_FLAGS=(--error)` unconditionally for all of them — identical exit-code and output behavior to the pre-change script. +**New behavior (CI-only):** `SEMGREP_SARIF_OUTPUT= bash scripts/pre-commit-hooks/semgrep-scan.sh [targets...]` scans with the same configs/exclusions but emits SARIF to `` instead of hard-failing on findings. + +### 3.1 New file: `.github/workflows/semgrep.yml` + +No API/DB/frontend surface — this is CI/YAML only. Full structural spec below (devops agent should treat this as the authoritative shape; exact YAML syntax is implementer's to finalize, but every element listed must be present). + +**Workflow name:** `Semgrep - SAST Scan` + +**Triggers:** +```yaml +on: + pull_request: + branches: [main, nightly, development] + push: + branches: [main, nightly, development] + workflow_dispatch: + schedule: + - cron: '0 4 * * 1' # Mondays 04:00 UTC — offset 1h after CodeQL's 03:00 to avoid runner contention +``` + +**Concurrency:** +```yaml +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }} + cancel-in-progress: true +``` +(Identical pattern to `codeql.yml`.) + +**Permissions — declared at both workflow (top) level and job level, identically, matching `codeql.yml`'s style (Supervisor round 2, non-blocking fix):** +```yaml +permissions: + contents: read + security-events: write + actions: read + pull-requests: read +``` +This exact block appears twice: once at the workflow top level (sibling of `on:`/`concurrency:`), and again inside `jobs.semgrep-scan.permissions` (§3.2). + +### 3.2 Job: `semgrep-scan` + +**Correction (Supervisor round 2, required change 1):** the pinned image is inlined directly as a literal string in `container.image` — `jobs..container` does not have access to the `env` context per GitHub Actions' documented context-availability rules, so a workflow-level `env:` indirection (as drafted originally) would not resolve at all. Inlining is the only correct form, not a fallback. + +```yaml +jobs: + semgrep-scan: + name: Semgrep SAST Scan + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + security-events: write + actions: read + pull-requests: read + container: + image: semgrep/semgrep:1.173.0@sha256:67319956da3dcb58baf5b322899c15458e3963e7018a86aeeb5cd224e69cb77a # semgrep/semgrep 1.173.0 +``` + +### 3.3 Steps + +1. **Checkout repository** + ```yaml + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.ref }} + ``` + (Same SHA already pinned and in active use in `codeql.yml` — reuse, don't re-pin a different version.) + +2. **Fix git safe.directory for container user** (mitigates the "dubious ownership" issue documented in Semgrep's own GHA KB article, §2.5): + ```yaml + - name: Configure git safe.directory + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + ``` + +3. **Verify Semgrep parity guard** (§3.4): + ```yaml + - name: Verify Semgrep parity guard + run: bash scripts/ci/check-semgrep-parity.sh + ``` + +4. **Print Semgrep version** (cheap sanity check that the pinned image actually resolves to the expected CLI version — catches a bad digest pin immediately and legibly, rather than surfacing as a confusing downstream scan failure): + ```yaml + - name: Verify Semgrep version + run: semgrep --version + ``` + +5. **Run Semgrep (SARIF output, non-blocking)** — calls the real script (§2.7/§3.0) with the new opt-in var; no duplicated config list. + ```yaml + - name: Run Semgrep (SARIF output) + id: semgrep_sarif + continue-on-error: true + env: + SEMGREP_SARIF_OUTPUT: semgrep-results.sarif + run: bash scripts/pre-commit-hooks/semgrep-scan.sh + ``` + `continue-on-error: true` because this pass must not block the job even if semgrep itself errors — the SARIF file's presence is checked explicitly next (step 6), and the actual gate is step 8, not this step. + +6. **Check Semgrep SARIF output exists** (mirrors `security-pr.yml`'s `Check Trivy SARIF output exists` step — this was an orphaned reference in the first draft of this plan; it is now a real, numbered step): + ```yaml + - name: Check Semgrep SARIF output exists + id: semgrep_sarif_check + if: always() + run: | + if [ -f semgrep-results.sarif ]; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "No Semgrep SARIF output found; skipping SARIF upload" + fi + ``` + +7. **Upload Semgrep SARIF to GitHub Security** (gated on step 6's output rather than blindly attempting the upload): + ```yaml + - name: Upload Semgrep SARIF to GitHub Security + if: always() && steps.semgrep_sarif_check.outputs.exists == 'true' + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 + with: + sarif_file: semgrep-results.sarif + category: semgrep + continue-on-error: true + ``` + (Reuses the exact SHA already pinned for this purpose in `security-pr.yml` — no new pin to introduce or maintain.) + +8. **Run Semgrep (hard-fail gate)** — calls the same script, this time with the default (unset `SEMGREP_SARIF_OUTPUT`) path, i.e. its normal `--error` behavior: + ```yaml + - name: Run Semgrep (hard-fail gate) + run: bash scripts/pre-commit-hooks/semgrep-scan.sh + ``` + This is the literal `security-full` invocation (§2.1) — same script, same default full-repo targets, same `--error` flag, run a second time (this time without the SARIF env var) so failure here genuinely gates the job. If this step fails, the job fails, blocking the PR/branch — this is the CI-independent reproduction of the local "green" signal the feature exists to deliver. SARIF upload (step 7) has already completed by this point, so a gate failure here does not suppress the informational upload — order matters and is intentional. + +9. **Upload SARIF artifact** (retention, matches `security-pr.yml`'s `Upload scan artifacts` step): + ```yaml + - name: Upload SARIF artifact + if: always() && steps.semgrep_sarif_check.outputs.exists == 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v4.4.3 + with: + name: semgrep-sarif-${{ github.run_id }} + path: semgrep-results.sarif + retention-days: 14 + continue-on-error: true + ``` + +10. **Job summary** + ```yaml + - name: Create job summary + if: always() + run: | + { + echo "## Semgrep SAST Scan Results" + echo "" + echo "**Rulesets**: p/golang, p/javascript, p/typescript, p/react, p/secrets, p/dockerfile" + echo "**Severity Gate**: ERROR, WARNING (--error)" + if [ "${{ job.status }}" == "success" ]; then + echo "PASSED: no blocking Semgrep findings" + else + echo "FAILED: Semgrep reported blocking findings — see step logs and the Security tab" + fi + } >> "$GITHUB_STEP_SUMMARY" + ``` + +### 3.4 New file: `scripts/ci/check-semgrep-parity.sh` + +**Revised, smaller scope (per §2.7's design change)** — modeled on `scripts/ci/check-codeql-parity.sh`'s approach (grep/structural assertions, not full YAML parsing), but no longer needs to compare two independent config lists, because §3.0's design means there's only one config list in the whole repo (in `semgrep-scan.sh`) and the workflow only ever delegates to it. Exits non-zero with an `::error title=Semgrep parity drift::` annotation on any mismatch. + +**Checks performed:** + +1. Required files exist: `.github/workflows/semgrep.yml`, `scripts/pre-commit-hooks/semgrep-scan.sh`. +2. Assert `scripts/pre-commit-hooks/semgrep-scan.sh` still contains the string `SEMGREP_SARIF_OUTPUT` — the additive CI hook (§3.0) must not be silently removed by a future edit to the script that forgets CI depends on it. +3. Assert `.github/workflows/semgrep.yml` contains **two** distinct delegating calls to the real script, not a reimplemented/inlined `semgrep scan ...` invocation: + - a call with `SEMGREP_SARIF_OUTPUT` set (the reporting pass, step 5) — e.g. assert both the literal strings `SEMGREP_SARIF_OUTPUT` and `scripts/pre-commit-hooks/semgrep-scan.sh` appear within the same step block; + - a bare `bash scripts/pre-commit-hooks/semgrep-scan.sh` call with no env override (the gate pass, step 8). + - This is the direct analogue of `check-codeql-parity.sh`'s "shared blocking logic must live in exactly one place" check (that script's lines enforcing `SHARED_GATE_SCRIPT` usage) — applied here to prevent a future edit from "simplifying" either step by inlining `semgrep scan` directly, which would silently reintroduce the duplicated-config problem §2.7 eliminated. +4. Assert `.github/workflows/semgrep.yml`'s pinned image reference matches the pattern `semgrep/semgrep:[0-9]+\.[0-9]+\.[0-9]+@sha256:[0-9a-f]{64}` (tag + digest both present — catches an accidental un-pin, e.g. someone changing it to `semgrep/semgrep:latest` during a quick edit). +5. Assert `pull_request`/`push` trigger branches in `semgrep.yml` are `[main, nightly, development]`, reusing `check-codeql-parity.sh`'s existing `ensure_event_branches_semantic` helper pattern (§3.5). + +Note what this script **no longer does**, relative to the first draft: it does not enumerate or compare `--config`/`--exclude`/`--exclude-rule` values between two files, because after §3.0's change there is only one file that defines them. + +**Where it's invoked:** +- `.github/workflows/semgrep.yml` step 3 (§3.3), analogous to `codeql.yml`'s "Verify CodeQL parity guard" step. +- Not wired into `lefthook.yml` in this PR — consistent with existing precedent: `check-codeql-parity.sh` is also CI-only today, invoked directly from `codeql.yml` and not from any lefthook stage. Noted as a possible follow-up, not a gap introduced by this plan. + +### 3.5 Shared helper extraction (optional, DRY nicety — from §3.4 item 5) + +`check-semgrep-parity.sh` is now the second script needing the branch-list assertion logic (`ensure_event_branches` / `ensure_event_branches_with_yq` / `ensure_event_branches_semantic`) that currently lives only in `check-codeql-parity.sh`. Recommend extracting it into `scripts/ci/lib/workflow-yaml-asserts.sh`, sourced by both scripts via `source "$(dirname "${BASH_SOURCE[0]}")/lib/workflow-yaml-asserts.sh"`, per `CLAUDE.md`'s "consolidate after second occurrence" DRY guideline. This is a pure refactor of existing, already-tested logic — low risk. Unlike the first draft, this extraction is no longer load-bearing for anything (the config-parity story doesn't depend on it, since there's no config duplication left to compare) — it is a legitimate but strictly optional cleanup. If time-boxed out of this PR, note it as a follow-up rather than skipping silently. + +### 3.6 SARIF category naming + +`category: semgrep` for the `upload-sarif` step (§3.3 step 7) — single, flat category since (unlike CodeQL's per-language matrix) there is only one Semgrep job/run per commit, no need for a parameterized category string. + +### 3.7 Error handling / edge cases + +| Scenario | Behavior | +|---|---| +| Pinned image digest becomes invalid/removed from registry (rare, but Docker Hub retention policies exist) | Job fails at container-pull time with a clear GitHub Actions infra error, not a silent skip. Remediation: re-resolve digest, bump the pin — a normal dependency-bump PR, same as any other pinned SHA bump in this repo. | +| SARIF step (step 5) itself crashes (e.g. semgrep internal error, not a rule finding) | `continue-on-error: true` on step 5 means the job continues; step 6 explicitly checks for the SARIF file's existence and sets an output consumed by steps 7 and 9, so a missing file cleanly skips upload rather than `upload-sarif` failing opaquely on a missing path. | +| Hard-fail gate step (step 8) fails legitimately (real findings) | Job fails, PR shows a red check, `$GITHUB_STEP_SUMMARY` still renders (step 10 runs on `if: always()`), SARIF is still uploaded to the Security tab (step 7 already ran before step 8 — order is intentional: SARIF upload must happen *before* the blocking step so a gate failure doesn't skip the informational upload). | +| Renovate later proposes bumping the pinned `semgrep/semgrep` image tag/digest | Handled like any other Renovate-tracked pin, via `.github/renovate.json` — devops agent should confirm at implementation time whether Renovate's Docker-image datasource already picks up `container: image:` refs in workflow YAML by default, or needs an explicit entry added to `.github/renovate.json`; note as a follow-up if configuration is needed, not a blocker for this PR. | +| A future edit to `semgrep-scan.sh` removes the `SEMGREP_SARIF_OUTPUT` hook, or an edit to `semgrep.yml` reintroduces an inline `semgrep scan` call instead of delegating | `check-semgrep-parity.sh` fails CI on the very next PR that makes either change, per §3.4 items 2-3. | + +--- + +## 4. Implementation Plan + +This is a CI/DevOps-only change plus one small, additive shell-script change. There is no Playwright/E2E surface (no user-facing behavior changes), no backend implementation, no frontend implementation. The phase structure below is adapted accordingly — **the `devops` agent implements this directly; no handoff to backend-dev, frontend-dev, or playwright-dev is needed.** + +### Phase 1 — Foundation (script hook + parity guard + optional shared lib) +- Apply the additive `SEMGREP_SARIF_OUTPUT` change to `scripts/pre-commit-hooks/semgrep-scan.sh` (§3.0). +- Write `scripts/ci/check-semgrep-parity.sh` (§3.4). +- Optionally extract `scripts/ci/lib/workflow-yaml-asserts.sh` from `check-codeql-parity.sh` (§3.5); if done, refactor `check-codeql-parity.sh` to source it and verify it still passes unchanged. +- Validation gate: run the existing `semgrep` lefthook hooks locally (`lefthook run pre-commit` touching a Go/JS file, or `lefthook run security-full`) to confirm the script's default (`SEMGREP_SARIF_OUTPUT` unset) behavior is byte-identical to pre-change — this is the regression check for §3.0's edit. `shellcheck scripts/pre-commit-hooks/semgrep-scan.sh scripts/ci/check-semgrep-parity.sh` (+ `scripts/ci/lib/workflow-yaml-asserts.sh` if extracted). If the shared lib was extracted, `bash scripts/ci/check-codeql-parity.sh` still exits 0 (regression check on that refactor). + +### Phase 2 — Workflow file +- Write `.github/workflows/semgrep.yml` per §3.1-§3.3 (10 steps, including the SARIF-existence-check as a first-class step, not an orphaned reference). +- Validation gate: `actionlint .github/workflows/semgrep.yml` (tool already required per `lefthook.yml`'s `actionlint` hook, §2 header comment listing required tools). `bash scripts/ci/check-semgrep-parity.sh` now passes against the real files. YAML syntax sanity via `yq eval '.' .github/workflows/semgrep.yml >/dev/null` or equivalent. +- **Live GitHub Actions execution cannot be validated locally** — the actual scan run (image pull, semgrep execution against the real repo, SARIF upload, gate pass/fail) is confirmed only once the PR opens and the workflow triggers on `pull_request`. Note this explicitly in the PR description as a manual verification step, not a local DoD gate. + +### Phase 3 — Documentation +- Update `SECURITY.md` per §2.8: add Semgrep row to the `### Automated Scanning` table, add a `**Semgrep SAST Scan**` paragraph to `### Scanning Workflows` describing `.github/workflows/semgrep.yml`'s trigger shape and what it covers. +- Update `ARCHITECTURE.md` per §2.8: the three call-outs at lines 166, 1376, and 1498-1501. +- Validation gate: `markdownlint SECURITY.md ARCHITECTURE.md` (tool already required per `lefthook.yml` header comment). + +### Phase 4 — Integration validation +- `lefthook run pre-commit` (full local hook suite, including the existing `actionlint` and `semgrep` hooks, to confirm nothing in this change breaks existing local gates). +- `bash scripts/ci/check-codeql-parity.sh` (if refactored) and `bash scripts/ci/check-semgrep-parity.sh` both green. +- Manual review of the rendered `semgrep.yml` against `codeql.yml`/`security-pr.yml` for pinning-comment consistency (every third-party `uses:` has a SHA + version comment; the container image has tag + digest inlined, not via `env`). + +### Phase 5 — PR & CI confirmation +- Open PR; confirm `semgrep.yml` actually triggers on the PR event, pulls the pinned image successfully, produces a SARIF upload visible under the repo's Security → Code scanning alerts (filtered by tool "Semgrep"), and that the hard-fail gate step correctly reflects the current repo's Semgrep cleanliness (expected: green, since this is the same ruleset the repo already passes locally today). +- If CI surfaces findings the local run didn't (e.g. a stale local semgrep binary/ruleset that had drifted below the pinned CI version), that is itself the feature working as intended — resolve findings on their merits, not by weakening the pin. + +--- + +## 5. Acceptance Criteria + +1. `.github/workflows/semgrep.yml` exists, triggers on `pull_request`/`push` to `[main, nightly, development]`, `workflow_dispatch`, and a weekly `schedule`. +2. Semgrep runs inside a `container:` whose `image:` is the pinned string `semgrep/semgrep:@sha256:` inlined directly (no `env:` indirection) — no floating tag, no `@latest`. +3. `permissions:` is declared identically at both the workflow (top) level and the job level. +4. `scripts/pre-commit-hooks/semgrep-scan.sh` carries exactly the additive `SEMGREP_SARIF_OUTPUT` change described in §3.0 — its binary/version-resolution logic and its `--config`/`--exclude`/`--exclude-rule` values are otherwise unchanged, and every existing call site's behavior (`SEMGREP_SARIF_OUTPUT` unset) is byte-identical to pre-change. +5. `.github/workflows/semgrep.yml` invokes `scripts/pre-commit-hooks/semgrep-scan.sh` directly for **both** the SARIF-producing pass (with `SEMGREP_SARIF_OUTPUT` set) and the hard-fail gate pass (unset) — no independent/duplicated `semgrep scan ...` invocation exists anywhere in the workflow YAML. +6. `scripts/ci/check-semgrep-parity.sh` exists, passes against the merged state, and is invoked as a CI step in `semgrep.yml` before the scan runs. +7. SARIF results upload to the GitHub Security tab under category `semgrep`, using the same `github/codeql-action/upload-sarif` SHA pin already used in `security-pr.yml`, gated on an explicit SARIF-existence check step. +8. `SECURITY.md`'s `### Automated Scanning` table and `### Scanning Workflows` section mention Semgrep and `semgrep.yml`. +9. `ARCHITECTURE.md` mentions Semgrep at all three existing security-scanning call-out locations (tech-stack table, CI Jobs list, Container Scanning components list). +10. `.gitignore`, `.dockerignore`, `.codecov.yml`, and all Dockerfiles are confirmed unchanged (per §2.9 — no diff expected in this PR). +11. `actionlint`, `markdownlint`, `shellcheck`, and `lefthook run pre-commit` all pass locally on the final diff. +12. `docs/plans/current_spec.md` (this file) reflects the implemented state — no divergence between plan and shipped workflow at PR time (devops agent should update this file if implementation deviates from any spec section above, per standard plan-fidelity practice). + +--- + +## 6. Commit Slicing Strategy + +**Decision:** single PR, one feature ("Semgrep CI coverage"), ordered logical commits. No cross-PR splitting per `CLAUDE.md`'s "One Feature = One PR" rule — this is a small, cohesive, CI-only change (plus one additive shell-script hook); splitting it further would violate that rule for no benefit (there's no independently-shippable sub-feature here — a workflow with no parity guard, or a parity guard with no workflow, are both incomplete on their own). Approved as-is by Supervisor round 2 — shape unchanged, contents updated below for §2.7's design revision and the ARCHITECTURE.md addition. + +### Commit 1 — Script hook + parity guard foundation +- **Scope:** Additive-only, no behavior change for any existing call site. Adds the `SEMGREP_SARIF_OUTPUT` hook to `semgrep-scan.sh`, the new (as-yet-unused-by-CI) parity script, and optionally the shared helper extraction. +- **Files:** `scripts/pre-commit-hooks/semgrep-scan.sh` (modified — additive only, per §3.0), `scripts/ci/check-semgrep-parity.sh` (new — will fail if run now, since `semgrep.yml` doesn't exist yet; not wired into any workflow in this commit), `scripts/ci/lib/workflow-yaml-asserts.sh` (new, optional) and `scripts/ci/check-codeql-parity.sh` (refactored to source it, optional, no behavioral change) if the extraction from §3.5 is included. +- **Dependencies:** none. +- **Validation gate:** `shellcheck` on all touched/new scripts; local `lefthook run pre-commit` / `lefthook run security-full` on a sample file confirms `semgrep-scan.sh`'s default behavior is unchanged; `bash scripts/ci/check-codeql-parity.sh` passes unchanged if the refactor is included (regression check). +- **Commit message:** `feat(security): add opt-in SARIF output mode to local Semgrep script and add CI parity guard` + +### Commit 2 — Semgrep CI workflow +- **Scope:** Adds the new workflow file, delegating both its SARIF and gate passes to the script from Commit 1, and wires the parity guard from Commit 1 into it. +- **Files:** `.github/workflows/semgrep.yml` (new). +- **Dependencies:** Commit 1 (the `SEMGREP_SARIF_OUTPUT` hook and the parity guard must exist for this workflow to reference real, working behavior). +- **Validation gate:** `actionlint .github/workflows/semgrep.yml`; `bash scripts/ci/check-semgrep-parity.sh` now passes (workflow file exists, delegates correctly, image pin format valid); `lefthook run pre-commit` clean on the diff. +- **Commit message:** `feat(security): add pinned Semgrep SAST scan to CI, mirroring local pre-commit/pre-push scan` + +### Commit 3 — Documentation +- **Scope:** `SECURITY.md` and `ARCHITECTURE.md` updates only, per §4 Phase 3. +- **Files:** `SECURITY.md`, `ARCHITECTURE.md`. +- **Dependencies:** Commit 2 (documents the workflow file that now exists). +- **Validation gate:** `markdownlint SECURITY.md ARCHITECTURE.md`. +- **Commit message:** `docs: document Semgrep CI scan in SECURITY.md and ARCHITECTURE.md` + +### Commit 4 — Hardening / fixups (conditional) +- **Scope:** Only if Phase 5 (opening the PR and observing the first real workflow run) surfaces something unfixable purely by inspection — e.g. the digest needs re-resolution, `actionlint`/a GitHub Actions schema quirk requires a syntax adjustment not visible from local linting alone, or the container's default shell needs an explicit `shell: bash` on a step. +- **Files:** `.github/workflows/semgrep.yml` and/or `scripts/ci/check-semgrep-parity.sh`, as needed. +- **Dependencies:** Commits 1-3, plus one observed CI run on the PR. +- **Validation gate:** the actual GitHub Actions run on the PR going green. +- **Commit message:** `fix: address Semgrep CI workflow issues found in first live run` (only created if needed — do not pre-author an empty placeholder commit). + +### Rollback / contingency + +- **Rollback:** revert the PR's merge commit. The change is additive-only (new files + one additive, backward-compatible shell-script hook + documentation sections); reverting it removes Semgrep CI coverage cleanly with no residual state — no DB migration, no data written, no schema changed. `git revert -m 1 ` is sufficient. +- **Contingency — pinned image becomes unpullable mid-development-cycle (e.g. registry outage, Docker Hub rate limiting on `ubuntu-latest` runners):** the job fails visibly (container pull failure is unambiguous in the Actions log, distinct from a scan failure), does not block other workflows (independent job, independent file), and does not gate merges any more strictly than any other required-check outage would — same failure mode and same operational response as a transient CodeQL or Trivy Action outage today. +- **Contingency — CI Semgrep surfaces findings that don't reproduce locally:** expected and desired (§4 Phase 5) — indicates local environment drift, not a CI bug. Do not suppress via `--exclude-rule` additions without documenting rationale (matching the existing precedent set by the one documented `gorm-empty-password` exclusion already in the script). +- **Contingency — parity guard is judged too strict/noisy after landing** (e.g. flags legitimate divergence that's actually fine): tune the specific assertion in `check-semgrep-parity.sh`, don't delete the guard wholesale — same operating principle already established for `check-codeql-parity.sh`, which has been iterated on rather than removed. diff --git a/docs/reports/archive/2026-08-14_semgrep-ci-security-scan-qa_report.md b/docs/reports/archive/2026-08-14_semgrep-ci-security-scan-qa_report.md new file mode 100644 index 000000000..46f5606ec --- /dev/null +++ b/docs/reports/archive/2026-08-14_semgrep-ci-security-scan-qa_report.md @@ -0,0 +1,108 @@ +# QA Report — Semgrep CI Security Scan (Independent Verification) + +**Branch**: `development` +**Commits reviewed**: `6bf066f8`, `2fbecf07`, `7c6fb04f` +**Reviewed by**: qa-security agent +**Date**: 2026-08-14 +**Scope**: CI/CD-only feature — no application code, models, or frontend/UI surface touched. +**Prior review**: Supervisor code review — approved, no blocking issues. +**Purpose**: Independent functional/security verification per Phase 6 of the management pipeline, ahead of a final "done" determination. + +## Summary Verdict: **PASS** — no blocking defects found. + +The Semgrep CI gate is functionally real (confirmed to fail on findings and pass when clean, via a positive-control test), the parity guard genuinely detects drift (confirmed via four separate intentional-break tests), the container image is correctly digest-pinned, and all local DoD-relevant checks scoped to a CI/shell-script-only change are clean. Two pre-existing environment/documentation gaps were identified and are explicitly **not** attributed to this feature (see §4 and §6). + +--- + +## 1. Functional Correctness of the Scan (PASS) + +Installed Semgrep 1.173.0 into a throwaway venv (`/tmp/.../scratchpad/semgrep-venv`, exact version match to the pinned CI image) and ran the actual wrapper script `scripts/pre-commit-hooks/semgrep-scan.sh` exactly as CI invokes it. + +| Check | Result | +|---|---| +| `SEMGREP_SARIF_OUTPUT= bash scripts/pre-commit-hooks/semgrep-scan.sh` (full repo, no targets) | Exit 0. Produced a valid SARIF file (`version`, `runs`, `results`, `$schema` present; parsed cleanly as JSON). | +| `bash scripts/pre-commit-hooks/semgrep-scan.sh` (no env var, full repo) | Exit 0. `--error` semantics confirmed live (see §1.1). | +| Repo clean under full scan | Reproduced: 974 files tracked by git, 160 rules run, **0 findings** — matches both prior QA/DevOps reports exactly. Two suppressed (`nosemgrep`-annotated) `websocket-missing-origin-check` findings appear in the SARIF's `results` array with `suppressions: [{kind: inSource}]` — this is correct SARIF behavior (audit trail for suppressed findings) and does not affect the "0 findings / 0 blocking" scan summary or exit code. | +| Runtime | ~45–48s per full-repo pass locally (single-threaded venv on this sandbox; CI's dedicated `semgrep/semgrep` container should be comparable or faster). | + +### 1.1 Positive-control test: does the gate actually gate? (Most important check — PASS) + +Constructed a minimal Go file containing an unguarded `websocket.Upgrader{}.Upgrade()` call (the same rule ID, `go.gorilla.security.audit.websocket-missing-origin-check`, that appears — suppressed — in the real codebase), and ran it through the **actual, unmodified** wrapper script with a single-file target: + +``` +SEMGREP_SARIF_OUTPUT=out.sarif bash scripts/pre-commit-hooks/semgrep-scan.sh + → Findings: 1 (1 blocking) → exit 0 (SARIF mode does not hard-fail) + +bash scripts/pre-commit-hooks/semgrep-scan.sh + → Findings: 1 (1 blocking) → exit 1 (--error mode hard-fails) +``` + +This is the critical distinction the task flagged as the top risk: a gate that always exits 0 regardless of findings would be a silent no-op. **Confirmed not the case.** The `SEMGREP_SARIF_OUTPUT` toggle in `scripts/pre-commit-hooks/semgrep-scan.sh:42-46` genuinely swaps `--error` for `--sarif --output `, and only the `--error` invocation (the CI workflow's "hard-fail gate" step, `semgrep.yml:75-76`) enforces blocking. The SARIF-producing pass (`semgrep.yml:49-54`) is additionally wrapped in `continue-on-error: true` at the workflow level, which is defense-in-depth on top of the script's own non-blocking `--sarif` exit code — belt and suspenders, not a substitute for the real gate. + +--- + +## 2. Workflow YAML Structural Validity (PASS) + +- `actionlint .github/workflows/semgrep.yml` (installed via `go install github.com/rhysd/actionlint@latest` into a throwaway `GOBIN`): **0 findings, exit 0.** +- Container image resolution: `docker buildx imagetools inspect semgrep/semgrep:1.173.0@sha256:67319956da3dcb58baf5b322899c15458e3963e7018a86aeeb5cd224e69cb77a` (the exact digest read fresh from the committed file, `semgrep.yml:33`) resolved successfully against the registry, returning a multi-platform manifest list whose index digest matches the pinned digest exactly. The pin is real and correct, not a stale/copy-pasted digest. + +--- + +## 3. Parity Guards (PASS, and confirmed non-trivial) + +- `bash scripts/ci/check-semgrep-parity.sh` — exit 0. +- `bash scripts/ci/check-codeql-parity.sh` — exit 0 (unaffected by the refactor that extracted `scripts/ci/lib/workflow-yaml-asserts.sh`; this file was verified as a correct behavior-preserving extraction, not a modification of the CodeQL guard's assertions). + +**Adversarial drift tests** (performed against throwaway copies in `/tmp/.../scratchpad/parity-break-test/`, never against the real repo files; all discarded after each test, working tree confirmed clean of these edits afterward): + +| Simulated drift | Guard result | +|---|---| +| Inline `semgrep scan --config p/golang` reintroduced in place of `bash scripts/pre-commit-hooks/semgrep-scan.sh` delegation | **Caught.** `must delegate to scripts/pre-commit-hooks/semgrep-scan.sh instead of reimplementing the semgrep scan invocation inline` — exit 1. | +| `SEMGREP_SARIF_OUTPUT` hook deleted from the local script | **Caught.** `must retain the SEMGREP_SARIF_OUTPUT hook so CI can produce SARIF via the same script` — exit 1. | +| Image pin degraded from `semgrep/semgrep:1.173.0@sha256:...` to `semgrep/semgrep:latest` | **Caught.** `must pin the semgrep/semgrep image with both an exact tag and a sha256 digest` — exit 1. | +| `pull_request` branch list narrowed from `[main, nightly, development]` to `[main, nightly]` | **Caught.** `pull_request branches must be [main, nightly, development]` — exit 1. | + +All four drift classes are detected. The guard is a real structural check, not a no-op that always passes. + +--- + +## 4. Local DoD-Relevant Checks (PASS, with one noted pre-existing environment gap) + +Scoped per the task's guidance: no Playwright E2E (no user-facing behavior), no GORM scan (confirmed zero files under `backend/internal/models/**` or any `.go` files touched — `git diff --name-only c510085f 7c6fb04f` shows only workflow/doc/shell files), no frontend type-check/build (zero `frontend/` files touched). + +- **shellcheck** (installed a static v0.10.0 binary into a throwaway location, no sudo/apt available) on all touched/new shell scripts (`semgrep-scan.sh`, `check-semgrep-parity.sh`, `workflow-yaml-asserts.sh`, `check-codeql-parity.sh`), using the project's actual severity threshold from `lefthook.yml` (`shellcheck --severity=error`): **0 findings.** (Default-severity mode surfaces two SC1091 "info"-level "not following sourced file" notices caused by the scripts' dynamic `SCRIPT_DIR` resolution pattern — expected and filtered out by the repo's own configured threshold, not a defect.) +- **`lefthook run pre-commit`**, targeted against this feature's exact changed-file set (`--file <7 files>`, since nothing was staged in this session): all hooks pass — `trailing-whitespace`, `end-of-file-fixer`, `actionlint`, `check-lfs-large-files`, `block-codeql-db`, `block-data-backups`, `semgrep` (0 findings, `semgrep` installed into PATH via the throwaway venv for this run only). One hook, **`check-yaml`, failed** with `ModuleNotFoundError: No module named 'yaml'` — **confirmed pre-existing/environmental, not introduced by this feature**: the hook shells out to system `python3 -c "import yaml..."` (`lefthook.yml:54`), and this sandbox's system Python lacks PyYAML. Verified by installing PyYAML into the throwaway venv and re-running the identical parse command directly against `.github/workflows/semgrep.yml` — it parsed cleanly (exit 0), proving the YAML itself is valid and the failure is purely a missing sandbox dependency, structurally identical to the already-documented `gitleaks`-unavailable gap below. +- **`gitleaks`**: confirmed absent from PATH (`which gitleaks` → exit 1) in this sandbox, matching the DevOps report. This predates the Semgrep feature entirely (a secrets-scanning tool unrelated to Semgrep) and is not something this feature could plausibly mask — the feature adds zero new secret-bearing surface (see §5). + +--- + +## 5. Security-Specific Checks (PASS) + +- **`permissions:`** — both the workflow-level and job-level blocks are exactly `contents: read`, `security-events: write`, `actions: read`, `pull-requests: read` — least-privilege, no broader scope (no `write` on `contents`, no `id-token`, no `packages`, etc.). +- **Triggers** — `pull_request` (not `pull_request_target`) confirmed at `semgrep.yml:4`. No fork-PR privilege-escalation risk. +- **Secrets/tokens** — `grep -in "secrets\.\|token\|GITHUB_TOKEN"` across `semgrep.yml`, `semgrep-scan.sh`, `check-semgrep-parity.sh`, `workflow-yaml-asserts.sh` returns no matches (the only "token" hits were unrelated word fragments in comments, none present). The SARIF-upload step uses `github/codeql-action/upload-sarif`, which relies on the workflow's implicit default `GITHUB_TOKEN` scoped by the `permissions:` block above — no custom secret is declared or required anywhere in this feature. +- **Container image pin** — digest-pinned (`semgrep/semgrep:1.173.0@sha256:67319956...`), confirmed resolvable and matching the registry (§2). Not a floating tag. + +--- + +## 6. Documentation Lint (PASS — no regression introduced) + +Ran `markdownlint-cli2@0.23.2` (the exact tool/version pinned in `package.json`, not the unrelated `markdownlint-cli` package) against `SECURITY.md` and `ARCHITECTURE.md`. + +- **SECURITY.md**: 114 findings (MD036 emphasis-as-heading, MD060 table-column-style, one MD034 bare-URL) — **identical count before and after** commit `7c6fb04f` (verified via `git show 7c6fb04f~1:SECURITY.md` piped through the same linter: 114 findings on the pre-commit version too). All findings are pre-existing formatting debt scattered across unrelated CVE-entry sections (lines 407–813); zero findings land in the new Semgrep paragraph or table row this PR added (~line 1000–1032). +- **ARCHITECTURE.md**: 93 findings, likewise identical before/after `7c6fb04f`. + +This PR's doc changes introduce zero new lint findings. Pre-existing lint debt is a separate, out-of-scope cleanup item and not this feature's responsibility. + +--- + +## Blocking Issues + +**None.** + +## Non-Blocking Observations (informational only, no action required for this PR) + +1. `check-yaml` and `gitleaks` are unavailable in this local sandbox due to missing system dependencies (PyYAML, gitleaks binary). Both are pre-existing environment gaps unrelated to this feature; CI's environment has these tools installed and is authoritative. No masking risk identified — the underlying YAML was independently verified valid, and this feature introduces no new secret-bearing surface for `gitleaks` to have caught. +2. SECURITY.md/ARCHITECTURE.md carry substantial pre-existing markdownlint debt (207 combined findings) unrelated to this PR. Worth a future standalone cleanup pass, but explicitly out of scope here per this feature's CI/shell-script-only mandate. + +## Final Overall Verdict: **PASS — ready to be marked done.** diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7d11e0f6e..083f2e39c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -44,7 +44,7 @@ "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.3", + "@testing-library/user-event": "^14.6.4", "@types/eslint-plugin-jsx-a11y": "6.10.0", "@types/node": "^26.2.0", "@types/react": "^19.2.18", @@ -73,7 +73,7 @@ "eslint-plugin-unicorn": "^73.0.0", "eslint-plugin-unused-imports": "^4.4.1", "jsdom": "^30.0.1", - "knip": "^6.32.1", + "knip": "^6.32.2", "postcss": "^8.5.26", "tailwindcss": "^4.3.3", "typescript": "^6.0.3", @@ -1040,9 +1040,9 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", - "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", + "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", "dev": true, "license": "MIT", "optional": true, @@ -1057,14 +1057,14 @@ "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", - "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "node_modules/@oxc-parser/binding-android-arm-eabi": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.142.0.tgz", - "integrity": "sha512-ZiRGDutGsv1G6bL/ozy/koC0Sv39T1DqyoC4KD1DOy9ZoACm1O5UWhEK2c02Qdk+4lfLVkvFa/mQ0fm/4h1BtQ==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.143.0.tgz", + "integrity": "sha512-n9uozULWflPqBtdmI8lAabLqGKNgLVNN0ZH8HfgCwpKGNtzRzauB76jTiW/3YLkcA7N1zskpi9GdVnZuu1SAvg==", "cpu": [ "arm" ], @@ -1079,9 +1079,9 @@ } }, "node_modules/@oxc-parser/binding-android-arm64": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.142.0.tgz", - "integrity": "sha512-WZkvGRLNQTz8lR9zP5nLjUdlroRCopBu3g9zF1p/laE6DzT1UbQo8Rdz5MWhaJUPYg/6gp+jo7HUgsyKaN1FtQ==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.143.0.tgz", + "integrity": "sha512-9BbdjHETk6O3zH/DDid9IgBtF0GlpLabNKN231uraXpRDSfY+iiZxTP5bk1Z63GBownVdhdINFIeddmMz4MzpQ==", "cpu": [ "arm64" ], @@ -1096,9 +1096,9 @@ } }, "node_modules/@oxc-parser/binding-darwin-arm64": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.142.0.tgz", - "integrity": "sha512-l4khS8LQOOVYsGRVARo1gSaCT/aBSceUVXgtovWc2+drnxVuDr082WA3OCHVdVzIz5JIrP/y9CWsSKxBDNmYGg==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.143.0.tgz", + "integrity": "sha512-gh+6ecoHUy4/sUcolBl/1qPXKBbYNxFY0Pk0ujgQvINTMSftJY7o4yb8gOkDJPeZeB8+a+u7xTe6umoP8N5HFA==", "cpu": [ "arm64" ], @@ -1113,9 +1113,9 @@ } }, "node_modules/@oxc-parser/binding-darwin-x64": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.142.0.tgz", - "integrity": "sha512-QBsNF3nqlXmcH2B1YOPqQYmCJoy4HuIjUxGbBO/k5JAJUl68ghU2psRY2zPk+RyBaWqKP/qfL4oaFgEMCdwskA==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.143.0.tgz", + "integrity": "sha512-qd1hl2d+lXgHv/VQ/M9qm8TrMC5T4RqDBwtOnl+1D0QMjwcz+8AaB4JSg8STgeag0GP6a6L74XEGAsrTSJWNzQ==", "cpu": [ "x64" ], @@ -1130,9 +1130,9 @@ } }, "node_modules/@oxc-parser/binding-freebsd-x64": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.142.0.tgz", - "integrity": "sha512-b7Q7m4Cqc6XqNhri3R+QhU+GVy646Pn+bkdhrDdWym/Fdi0ZUa+d73H9dm5H91JtbtAQ/z1d8XKMW3oOV8a4tQ==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.143.0.tgz", + "integrity": "sha512-M5XXcNa7aOqLPKTR41msfghKu2yQ4xWvCm11/gwU0JzOzHNk5sgW//rVEjJ+LO48+VDAMzXTSzurUVxIDKwozw==", "cpu": [ "x64" ], @@ -1147,9 +1147,9 @@ } }, "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.142.0.tgz", - "integrity": "sha512-3riVS5IhdH3uCZj1Y9ftDQlR0dvLsIlw/edrRqk8JhgNd5K0XSs+UBtgh50N13CAlW9/TXj6sVGXaKNBocd0Yg==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.143.0.tgz", + "integrity": "sha512-T/GXusuOkPNQhCQCSBbcU/N8j0rAypuDBl1IyFK+lyYT594XsVz80clPC/OtbSSpBGyJxj8uYEfctxVuxVYoww==", "cpu": [ "arm" ], @@ -1164,9 +1164,9 @@ } }, "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.142.0.tgz", - "integrity": "sha512-NmXUOpgpTSkhl795TiXmWppTwmSJ92RC1qvD6e4XOF+slgmo3e6Ah+kEu+6AN8s7NAOEwqGmir58MgSQSWmBSA==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.143.0.tgz", + "integrity": "sha512-oKu4RcBlXSqo3OC62dp6YTnQaZIurNDpCX3BnAM3+bJxt7s8J2TJKMnC0UYer1qhlRaDCg6wkTaTw+2IlsZ12w==", "cpu": [ "arm" ], @@ -1181,9 +1181,9 @@ } }, "node_modules/@oxc-parser/binding-linux-arm64-gnu": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.142.0.tgz", - "integrity": "sha512-gc0EXsKtXgerujmU2Bql3u1L1HsSQ2774R83idq/FoNMPVV/RY/1ErFsvnit7KoiP/sLvzQixeUo4Ut0ic0wmw==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.143.0.tgz", + "integrity": "sha512-WJBbD186AZmMGaSIhlktC+rPl8L3peCTXAh88Ih9uEvK0en2mPojGyCGYiL6mHtV1RPV3JyfJW5t6n5hh0lXhA==", "cpu": [ "arm64" ], @@ -1201,9 +1201,9 @@ } }, "node_modules/@oxc-parser/binding-linux-arm64-musl": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.142.0.tgz", - "integrity": "sha512-F2XvmWSE0uWpie+jHKKIFgdVOe9ypGhkEZxKx5DuW215K6cbAC274yYaPkcM7EqY4Df3Weyhpcz3lsURyH2LVg==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.143.0.tgz", + "integrity": "sha512-t1AcYOwEzgceadT4v5e+vaCCb0AncCA3v5AyzfBAz/tMq11qzVccXKzNHtkWdjBsgvTKwRkaUF3QvT4kot8vcQ==", "cpu": [ "arm64" ], @@ -1221,9 +1221,9 @@ } }, "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.142.0.tgz", - "integrity": "sha512-wLMbT21U/QxknQsk+VvNF0b9D2/aGWhcaQQQ+VYlE8FwD5+GoWZIPPXNzyHmkYyhm0KB3itL+TBavjMatqNnYA==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.143.0.tgz", + "integrity": "sha512-RsnO/NoD8376LMJq8JS8TwI0ieNaFRTuNe2GVJntQg6gwZNMENZsEbknHdVwjpOmxdGLGodcwaGSbAeRr5Bgjw==", "cpu": [ "ppc64" ], @@ -1241,9 +1241,9 @@ } }, "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.142.0.tgz", - "integrity": "sha512-+G8F/4ckwT7FCJV4H2bt09xEzJbjNCfuL4Sp1AYNaFtFMVtgIGMuJlteT82U+K0UIZ/DzAR/LDlMFnEuajG7Kw==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.143.0.tgz", + "integrity": "sha512-48fSVfR9TZi5CASZFyv0VC6z6BCoeihFsX031mAD/oSH7d9PYsPgIqza7d9mjP7Z2KTEpTFyH6SIu0Ui6R1vdg==", "cpu": [ "riscv64" ], @@ -1261,9 +1261,9 @@ } }, "node_modules/@oxc-parser/binding-linux-riscv64-musl": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.142.0.tgz", - "integrity": "sha512-hTsHtTLxMAfCo+rpF5K3qZJKW2NpPN/CHd4mYB3y7XlSdspHkd2gehDIofP64AacA9nWQw2tY3O7wR6UY8IVOA==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.143.0.tgz", + "integrity": "sha512-T8CpdD+SfE01DnIOD4HpVxu0ZJOfMJ/VhCvikKfaXAxkZ+9veyLM/D2hpi7Y2hFUyPmVQO3FNZHmYzV/WlVR4g==", "cpu": [ "riscv64" ], @@ -1281,9 +1281,9 @@ } }, "node_modules/@oxc-parser/binding-linux-s390x-gnu": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.142.0.tgz", - "integrity": "sha512-6y7qYY3TCUDYjqswImdTGl92y+KA/80twALegQPN27kfY+bG7Ib1+L3jbmrCZQx6wrVnai9IPsEZp07I0hx7JQ==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.143.0.tgz", + "integrity": "sha512-QLdeMsCcacenPEFsfxnBUDF1y6opyz5+fmOz9bfD5Y7fiGCMupUCuB3KTPQhNwshIG1P9fPqar9MHxuBDd4bwQ==", "cpu": [ "s390x" ], @@ -1301,9 +1301,9 @@ } }, "node_modules/@oxc-parser/binding-linux-x64-gnu": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.142.0.tgz", - "integrity": "sha512-i69kAWU+2LgoH5bR+zWiiu+UzAw7Oxkwv7COeJTeY19pn4e70nKQcr9Pm6cL2Z0Z54d+gl9qADlK/0yyuCPiBA==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.143.0.tgz", + "integrity": "sha512-659ujfqLy6k7cuH3sbzhd8b+ztSq+i6E2E9pG78Q0BmHjAExfGIdgc8cGgMdwAozDXeZFHkJ+LXYJdWsaGdgyw==", "cpu": [ "x64" ], @@ -1321,9 +1321,9 @@ } }, "node_modules/@oxc-parser/binding-linux-x64-musl": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.142.0.tgz", - "integrity": "sha512-4SQs678MmjYVrmhAgCWD4o0vpaFszXw9xLX5p2Z9MMFcltxiLkA88wQjh80YHjPrXtpyZ2CWI5m+1yNKM0m2Pw==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.143.0.tgz", + "integrity": "sha512-/Mw/9j4TfZcnKphPrzOE6t4MMknXadcAAuVUlDRTF/ETWB5xOgQvOJV2Mh9We/bWxZdoxaGAdc+hy4GuYwQ2yQ==", "cpu": [ "x64" ], @@ -1341,9 +1341,9 @@ } }, "node_modules/@oxc-parser/binding-openharmony-arm64": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.142.0.tgz", - "integrity": "sha512-YHpx9N7Ln3a++Tc8rv+H7mrK1zyJQOAwCFg8LZ3lTs1T5afGWeZrLPhPT9HLnIwSjCyJqPWVMIrMxbjcmBr2oQ==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.143.0.tgz", + "integrity": "sha512-8rIKWR2BFuifbIK/1XB9wTaSdtuJ25dlE7ZQYDnEwj/2xH2vHsxnvIjHT3ZjSVuLLwGGlSslIG/fbOJ8TV8rTw==", "cpu": [ "arm64" ], @@ -1357,29 +1357,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxc-parser/binding-wasm32-wasi": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.142.0.tgz", - "integrity": "sha512-3pLDyY3+oogW73RM5uehNgAiR/Xfb7fvO2Q1Z1gIqZ2+50XDVQmBVlRkHXZTU4gKnQHpwETNsYQVsJ3joVB2iA==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.11.2", - "@emnapi/runtime": "1.11.2", - "@napi-rs/wasm-runtime": "^1.1.6" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@oxc-parser/binding-win32-arm64-msvc": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.142.0.tgz", - "integrity": "sha512-Had/VeVY28Oyb0K+Q4FV8KCzoBycIh93oDK6pCbya9lkzdq+ikMHMgBubsdqqlybjJmQRawCQRrnBRHyQwYvcQ==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.143.0.tgz", + "integrity": "sha512-5U9kQYMfRRI6Zq7KDxgbIP0RMnKrfn3gLepRMgJuRkPSUALTiRCk9d/uyhb4lGDjUdzwK7mBkKqhLgzBPCmLpQ==", "cpu": [ "arm64" ], @@ -1394,9 +1375,9 @@ } }, "node_modules/@oxc-parser/binding-win32-ia32-msvc": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.142.0.tgz", - "integrity": "sha512-GGi3+YphVHavvgs6gum2UXoNCqzHAmPt/nXkn8ZQZstV2Q1qZD1Mn8fz/nWrDkefHQtrG/+1/XrbMxsBTo6Svw==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.143.0.tgz", + "integrity": "sha512-25P7AaHk4R88Yv2XH4gToDVmh0cOu+bEURQU10CRrmvgabfRArSGAP5osmwUKeSUHj0VS50upbpbRWWW/m7mHA==", "cpu": [ "ia32" ], @@ -1411,9 +1392,9 @@ } }, "node_modules/@oxc-parser/binding-win32-x64-msvc": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.142.0.tgz", - "integrity": "sha512-Ny/Wv4Us1LGC/ljwNTp+Hx3r/pH15EFfeDF0p+n898gt+TtRd6C9SccHcuUhDiNTb8s5tt7jdeAMDRQZ4Vq6hg==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.143.0.tgz", + "integrity": "sha512-ORMh3JE1s6V7ySicdRK7vgaDQnn5o+UHg9ct989PlWHbel8O9ARrmWXM6kZjrBMtNucxNayQ8g69G0VfWzhANw==", "cpu": [ "x64" ], @@ -1428,9 +1409,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz", - "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", "dev": true, "license": "MIT", "funding": { @@ -2488,9 +2469,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", - "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz", + "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==", "cpu": [ "arm64" ], @@ -2505,9 +2486,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", - "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==", "cpu": [ "arm64" ], @@ -2522,9 +2503,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", - "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz", + "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==", "cpu": [ "x64" ], @@ -2539,9 +2520,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", - "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz", + "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==", "cpu": [ "x64" ], @@ -2556,9 +2537,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", - "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz", + "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==", "cpu": [ "arm" ], @@ -2573,9 +2554,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", - "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz", + "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==", "cpu": [ "arm64" ], @@ -2593,9 +2574,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", - "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz", + "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==", "cpu": [ "arm64" ], @@ -2613,9 +2594,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", - "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz", + "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==", "cpu": [ "ppc64" ], @@ -2633,9 +2614,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", - "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz", + "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==", "cpu": [ "s390x" ], @@ -2653,9 +2634,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", - "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz", + "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==", "cpu": [ "x64" ], @@ -2673,9 +2654,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", - "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz", + "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==", "cpu": [ "x64" ], @@ -2693,9 +2674,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", - "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz", + "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==", "cpu": [ "arm64" ], @@ -2710,9 +2691,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", - "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz", + "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==", "cpu": [ "arm64" ], @@ -2727,9 +2708,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", - "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz", + "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==", "cpu": [ "x64" ], @@ -3156,9 +3137,9 @@ } }, "node_modules/@testing-library/user-event": { - "version": "14.6.3", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.3.tgz", - "integrity": "sha512-6dBq67jT8lE+JTE8Exm02Kt6ze43hz1jdiSpSJwtTZiT1xQQ6b7nZYTTQ9njdArdU8XklOwaDp/AbT/eYSKF4g==", + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.4.tgz", + "integrity": "sha512-QCGwP6QrjypBLwyj5cuyfVamkaIEy/XGY+1VDehbtbQqOggYmTFpFOdWR5mPz14vX8vXLMVjDHlRNBcClyO9ew==", "dev": true, "license": "MIT", "engines": { @@ -4587,9 +4568,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.11.13", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz", - "integrity": "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==", + "version": "2.11.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.14.tgz", + "integrity": "sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5390,9 +5371,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.403", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.403.tgz", - "integrity": "sha512-MQsYmdaLzvaCX5j+ZZBr5Fm6uCCnPQcRtlvmvRlWqrXy+BH2O4ffXIAScF+JQznQWB9brWp4lSD9Z4yNmaf2BA==", + "version": "1.5.406", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.406.tgz", + "integrity": "sha512-hWH5ORBi3d0IipnMh7BN5GDTaAmrSSSWmznwt2zltdiRNEWoEQyTwF0FFSBxzHO7hLSRT6loQu3IQGV0wg/Tvg==", "dev": true, "license": "ISC" }, @@ -6657,9 +6638,9 @@ } }, "node_modules/globals": { - "version": "17.9.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.9.0.tgz", - "integrity": "sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==", + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz", + "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==", "dev": true, "license": "MIT", "engines": { @@ -7702,9 +7683,9 @@ } }, "node_modules/knip": { - "version": "6.32.1", - "resolved": "https://registry.npmjs.org/knip/-/knip-6.32.1.tgz", - "integrity": "sha512-mIiIHMTJVUgSlz0mxEgPt7wg8DmfbCp1Txqab3WpbMCJF7YHvHtC9jeAHHXfISMl72N8WzhyG71SQlaqCOGZtg==", + "version": "6.32.2", + "resolved": "https://registry.npmjs.org/knip/-/knip-6.32.2.tgz", + "integrity": "sha512-WXTXbmocrw7gqm1A1TQvFN0OgJ7hUSU6E1g6SPRIzzHFogUBhXByc7cYeOFVtJ2uODg7DP4VbESYBYnfbtBYsg==", "dev": true, "funding": [ { @@ -7722,13 +7703,13 @@ "formatly": "^0.3.0", "get-tsconfig": "4.14.1", "jiti": "^2.7.0", - "oxc-parser": "^0.142.0", + "oxc-parser": "^0.143.0", "oxc-resolver": "11.24.2", "picomatch": "^4.0.5", "smol-toml": "^1.7.1", "strip-json-comments": "5.0.3", "tinyglobby": "^0.2.17", - "unbash": "^4.0.4", + "unbash": "^4.0.9", "yaml": "^2.9.0", "zod": "^4.4.3" }, @@ -9342,13 +9323,13 @@ } }, "node_modules/oxc-parser": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.142.0.tgz", - "integrity": "sha512-kKR+jPiRJYJDexVoziIg/FVGvr1fT1FZSSJOk6tVoMKKSlsf1Cso+cgGCJkOEDWOP174vRntCPFKg+AS7InWvw==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.143.0.tgz", + "integrity": "sha512-ov0NzaDCOInknS7mP1cwKdJERt3utPW8ldjtdUXQ8Ty0GEFD08wk422vCUN0d7pST6kqtV7dxoI9w1Zi0l/9TA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "^0.142.0" + "@oxc-project/types": "^0.143.0" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -9357,26 +9338,25 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxc-parser/binding-android-arm-eabi": "0.142.0", - "@oxc-parser/binding-android-arm64": "0.142.0", - "@oxc-parser/binding-darwin-arm64": "0.142.0", - "@oxc-parser/binding-darwin-x64": "0.142.0", - "@oxc-parser/binding-freebsd-x64": "0.142.0", - "@oxc-parser/binding-linux-arm-gnueabihf": "0.142.0", - "@oxc-parser/binding-linux-arm-musleabihf": "0.142.0", - "@oxc-parser/binding-linux-arm64-gnu": "0.142.0", - "@oxc-parser/binding-linux-arm64-musl": "0.142.0", - "@oxc-parser/binding-linux-ppc64-gnu": "0.142.0", - "@oxc-parser/binding-linux-riscv64-gnu": "0.142.0", - "@oxc-parser/binding-linux-riscv64-musl": "0.142.0", - "@oxc-parser/binding-linux-s390x-gnu": "0.142.0", - "@oxc-parser/binding-linux-x64-gnu": "0.142.0", - "@oxc-parser/binding-linux-x64-musl": "0.142.0", - "@oxc-parser/binding-openharmony-arm64": "0.142.0", - "@oxc-parser/binding-wasm32-wasi": "0.142.0", - "@oxc-parser/binding-win32-arm64-msvc": "0.142.0", - "@oxc-parser/binding-win32-ia32-msvc": "0.142.0", - "@oxc-parser/binding-win32-x64-msvc": "0.142.0" + "@oxc-parser/binding-android-arm-eabi": "0.143.0", + "@oxc-parser/binding-android-arm64": "0.143.0", + "@oxc-parser/binding-darwin-arm64": "0.143.0", + "@oxc-parser/binding-darwin-x64": "0.143.0", + "@oxc-parser/binding-freebsd-x64": "0.143.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.143.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.143.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.143.0", + "@oxc-parser/binding-linux-arm64-musl": "0.143.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.143.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.143.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.143.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.143.0", + "@oxc-parser/binding-linux-x64-gnu": "0.143.0", + "@oxc-parser/binding-linux-x64-musl": "0.143.0", + "@oxc-parser/binding-openharmony-arm64": "0.143.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.143.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.143.0", + "@oxc-parser/binding-win32-x64-msvc": "0.143.0" } }, "node_modules/oxc-resolver": { @@ -10146,13 +10126,13 @@ } }, "node_modules/rolldown": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", - "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz", + "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.143.0", + "@oxc-project/types": "=0.144.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -10162,26 +10142,26 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.2.3", - "@rolldown/binding-darwin-arm64": "1.2.3", - "@rolldown/binding-darwin-x64": "1.2.3", - "@rolldown/binding-freebsd-x64": "1.2.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", - "@rolldown/binding-linux-arm64-gnu": "1.2.3", - "@rolldown/binding-linux-arm64-musl": "1.2.3", - "@rolldown/binding-linux-ppc64-gnu": "1.2.3", - "@rolldown/binding-linux-s390x-gnu": "1.2.3", - "@rolldown/binding-linux-x64-gnu": "1.2.3", - "@rolldown/binding-linux-x64-musl": "1.2.3", - "@rolldown/binding-openharmony-arm64": "1.2.3", - "@rolldown/binding-win32-arm64-msvc": "1.2.3", - "@rolldown/binding-win32-x64-msvc": "1.2.3" + "@rolldown/binding-android-arm64": "1.2.4", + "@rolldown/binding-darwin-arm64": "1.2.4", + "@rolldown/binding-darwin-x64": "1.2.4", + "@rolldown/binding-freebsd-x64": "1.2.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", + "@rolldown/binding-linux-arm64-gnu": "1.2.4", + "@rolldown/binding-linux-arm64-musl": "1.2.4", + "@rolldown/binding-linux-ppc64-gnu": "1.2.4", + "@rolldown/binding-linux-s390x-gnu": "1.2.4", + "@rolldown/binding-linux-x64-gnu": "1.2.4", + "@rolldown/binding-linux-x64-musl": "1.2.4", + "@rolldown/binding-openharmony-arm64": "1.2.4", + "@rolldown/binding-win32-arm64-msvc": "1.2.4", + "@rolldown/binding-win32-x64-msvc": "1.2.4" } }, "node_modules/rolldown/node_modules/@oxc-project/types": { - "version": "0.143.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", - "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", + "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==", "dev": true, "license": "MIT", "funding": { @@ -10492,9 +10472,9 @@ } }, "node_modules/smol-toml": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.1.tgz", - "integrity": "sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.8.0.tgz", + "integrity": "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==", "dev": true, "license": "BSD-3-Clause", "engines": { diff --git a/frontend/package.json b/frontend/package.json index 6c1896c42..d9a3144ae 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -64,7 +64,7 @@ "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.3", + "@testing-library/user-event": "^14.6.4", "@types/eslint-plugin-jsx-a11y": "6.10.0", "@types/node": "^26.2.0", "@types/react": "^19.2.18", @@ -93,7 +93,7 @@ "eslint-plugin-unicorn": "^73.0.0", "eslint-plugin-unused-imports": "^4.4.1", "jsdom": "^30.0.1", - "knip": "^6.32.1", + "knip": "^6.32.2", "postcss": "^8.5.26", "tailwindcss": "^4.3.3", "typescript": "^6.0.3", diff --git a/go.work b/go.work index fe3169ac3..f8865202d 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.26.5 +go 1.26.6 use ( ./agent diff --git a/go.work.sum b/go.work.sum index 6bca41c3e..a68e5aef5 100644 --- a/go.work.sum +++ b/go.work.sum @@ -289,6 +289,7 @@ golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwE golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= diff --git a/package-lock.json b/package-lock.json index 8934b12e7..6bd02cee7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "type-check": "^0.4.0" }, "devDependencies": { - "@axe-core/playwright": "^4.12.1", + "@axe-core/playwright": "^4.13.0", "@bgotink/playwright-coverage": "^0.3.2", "@playwright/test": "^1.62.1", "@types/eslint-plugin-jsx-a11y": "6.10.0", @@ -28,13 +28,13 @@ } }, "node_modules/@axe-core/playwright": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.12.1.tgz", - "integrity": "sha512-rMd7xriptqKpP+w5265i4Hdkv2X5kbu6uiBi/B2I7uf3hieRBM3qDCfaKPtxfiYb2mKXfF+yLODJwIx+Jv1GDw==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.13.0.tgz", + "integrity": "sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg==", "dev": true, "license": "MPL-2.0", "dependencies": { - "axe-core": "~4.12.1" + "axe-core": "~4.13.0" }, "peerDependencies": { "playwright-core": ">= 1.0.0" @@ -148,9 +148,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.143.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", - "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", + "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==", "dev": true, "license": "MIT", "funding": { @@ -174,9 +174,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", - "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz", + "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==", "cpu": [ "arm64" ], @@ -191,9 +191,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", - "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==", "cpu": [ "arm64" ], @@ -208,9 +208,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", - "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz", + "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==", "cpu": [ "x64" ], @@ -225,9 +225,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", - "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz", + "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==", "cpu": [ "x64" ], @@ -242,9 +242,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", - "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz", + "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==", "cpu": [ "arm" ], @@ -259,9 +259,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", - "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz", + "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==", "cpu": [ "arm64" ], @@ -279,9 +279,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", - "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz", + "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==", "cpu": [ "arm64" ], @@ -299,9 +299,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", - "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz", + "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==", "cpu": [ "ppc64" ], @@ -319,9 +319,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", - "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz", + "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==", "cpu": [ "s390x" ], @@ -339,9 +339,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", - "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz", + "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==", "cpu": [ "x64" ], @@ -359,9 +359,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", - "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz", + "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==", "cpu": [ "x64" ], @@ -379,9 +379,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", - "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz", + "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==", "cpu": [ "arm64" ], @@ -396,9 +396,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", - "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz", + "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==", "cpu": [ "arm64" ], @@ -413,9 +413,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", - "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz", + "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==", "cpu": [ "x64" ], @@ -692,9 +692,9 @@ } }, "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", "dev": true, "license": "MIT", "engines": { @@ -843,9 +843,9 @@ } }, "node_modules/axe-core": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", - "integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz", + "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", "dev": true, "license": "MPL-2.0", "engines": { @@ -3081,13 +3081,13 @@ } }, "node_modules/rolldown": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", - "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz", + "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.143.0", + "@oxc-project/types": "=0.144.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -3097,20 +3097,20 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.2.3", - "@rolldown/binding-darwin-arm64": "1.2.3", - "@rolldown/binding-darwin-x64": "1.2.3", - "@rolldown/binding-freebsd-x64": "1.2.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", - "@rolldown/binding-linux-arm64-gnu": "1.2.3", - "@rolldown/binding-linux-arm64-musl": "1.2.3", - "@rolldown/binding-linux-ppc64-gnu": "1.2.3", - "@rolldown/binding-linux-s390x-gnu": "1.2.3", - "@rolldown/binding-linux-x64-gnu": "1.2.3", - "@rolldown/binding-linux-x64-musl": "1.2.3", - "@rolldown/binding-openharmony-arm64": "1.2.3", - "@rolldown/binding-win32-arm64-msvc": "1.2.3", - "@rolldown/binding-win32-x64-msvc": "1.2.3" + "@rolldown/binding-android-arm64": "1.2.4", + "@rolldown/binding-darwin-arm64": "1.2.4", + "@rolldown/binding-darwin-x64": "1.2.4", + "@rolldown/binding-freebsd-x64": "1.2.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", + "@rolldown/binding-linux-arm64-gnu": "1.2.4", + "@rolldown/binding-linux-arm64-musl": "1.2.4", + "@rolldown/binding-linux-ppc64-gnu": "1.2.4", + "@rolldown/binding-linux-s390x-gnu": "1.2.4", + "@rolldown/binding-linux-x64-gnu": "1.2.4", + "@rolldown/binding-linux-x64-musl": "1.2.4", + "@rolldown/binding-openharmony-arm64": "1.2.4", + "@rolldown/binding-win32-arm64-msvc": "1.2.4", + "@rolldown/binding-win32-x64-msvc": "1.2.4" } }, "node_modules/run-parallel": { @@ -3214,9 +3214,9 @@ } }, "node_modules/smol-toml": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.1.tgz", - "integrity": "sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.8.0.tgz", + "integrity": "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==", "dev": true, "license": "BSD-3-Clause", "engines": { diff --git a/package.json b/package.json index 9989789d2..0c21bcd52 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,12 @@ "type-check": "^0.4.0" }, "overrides": { - "smol-toml": "^1.7.1", + "smol-toml": "^1.8.0", "js-yaml": "^4.3.0", "markdown-it": "^15.0.0" }, "devDependencies": { - "@axe-core/playwright": "^4.12.1", + "@axe-core/playwright": "^4.13.0", "@bgotink/playwright-coverage": "^0.3.2", "@playwright/test": "^1.62.1", "@types/eslint-plugin-jsx-a11y": "6.10.0", diff --git a/scripts/charon_dep_update.sh b/scripts/charon_dep_update.sh index 1c00e46f5..a03785d63 100644 --- a/scripts/charon_dep_update.sh +++ b/scripts/charon_dep_update.sh @@ -55,7 +55,7 @@ echo "========================================================================== echo "Current local versions (npm / npx):" npm -v && npx -v -echo -n "Latest available npm version on registry: " +echo "Latest available npm version on registry: " npm view npm version echo "Installing latest global npm..." @@ -100,7 +100,16 @@ for MODULE in "${NPM_MODULES[@]}"; do if [ "$MODULE" != "$REPO_ROOT/frontend" ]; then # Root package.json has only flat string overrides — safe to update all # except js-yaml, which has breaking changes in v6+; keep pinned to ^5. - npx --yes npm-check-updates -u --dep overrides --reject js-yaml + # + # Update one package at a time: ncu throws an unhandled "Overlapping + # edit" rejection (a known upstream bug) when --dep overrides is + # asked to rewrite more than one entry in the same pass. + OVERRIDE_PACKAGES="$(node -e "console.log(Object.keys(require('./package.json').overrides || {}).join('\n'))")" + while IFS= read -r PKG; do + [ -z "$PKG" ] && continue + [ "$PKG" = "js-yaml" ] && continue + npx --yes npm-check-updates -u --dep overrides --filter "$PKG" + done <<< "$OVERRIDE_PACKAGES" fi rm -rf node_modules package-lock.json diff --git a/scripts/ci/check-codeql-parity.sh b/scripts/ci/check-codeql-parity.sh index 13cd1058f..8c5126f1f 100755 --- a/scripts/ci/check-codeql-parity.sh +++ b/scripts/ci/check-codeql-parity.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/workflow-yaml-asserts.sh +source "${SCRIPT_DIR}/lib/workflow-yaml-asserts.sh" + CODEQL_WORKFLOW=".github/workflows/codeql.yml" TASKS_FILE=".vscode/tasks.json" GO_PRECOMMIT_SCRIPT="scripts/pre-commit-hooks/codeql-go-scan.sh" @@ -26,83 +30,6 @@ ensure_task_command() { "$tasks_file" >/dev/null } -ensure_event_branches() { - local workflow_file="$1" - local event_name="$2" - local expected_line="$3" - - awk -v event_name="$event_name" -v expected_line="$expected_line" ' - /^on:/ { - in_on = 1 - next - } - - in_on && $1 == event_name ":" { - in_event = 1 - next - } - - in_on && in_event && $1 == "branches:" { - line = $0 - gsub(/^ +/, "", line) - if (line == expected_line) { - found = 1 - } - in_event = 0 - next - } - - in_on && in_event && $1 ~ /^[a-z_]+:$/ { - in_event = 0 - } - - END { - exit found ? 0 : 1 - } - ' "$workflow_file" -} - -ensure_event_branches_with_yq() { - local workflow_file="$1" - local event_name="$2" - shift 2 - local expected_branches=("$@") - - local expected_json - local actual_json - - expected_json="$(printf '%s\n' "${expected_branches[@]}" | jq -R . | jq -s .)" - - if actual_json="$(yq eval -o=json ".on.${event_name}.branches // []" "$workflow_file" 2>/dev/null)"; then - : - elif actual_json="$(yq -o=json ".on.${event_name}.branches // []" "$workflow_file" 2>/dev/null)"; then - : - else - return 1 - fi - - jq -e \ - --argjson expected "$expected_json" \ - 'if type != "array" then false else ((map(tostring) | unique | sort) == ($expected | map(tostring) | unique | sort)) end' \ - <<<"$actual_json" >/dev/null -} - -ensure_event_branches_semantic() { - local workflow_file="$1" - local event_name="$2" - local fallback_line="$3" - shift 3 - local expected_branches=("$@") - - if command -v yq >/dev/null 2>&1; then - if ensure_event_branches_with_yq "$workflow_file" "$event_name" "${expected_branches[@]}"; then - return 0 - fi - fi - - ensure_event_branches "$workflow_file" "$event_name" "$fallback_line" -} - [[ -f "$CODEQL_WORKFLOW" ]] || fail "Missing workflow file: $CODEQL_WORKFLOW" [[ -f "$TASKS_FILE" ]] || fail "Missing tasks file: $TASKS_FILE" [[ -f "$GO_PRECOMMIT_SCRIPT" ]] || fail "Missing pre-commit script: $GO_PRECOMMIT_SCRIPT" diff --git a/scripts/ci/check-semgrep-parity.sh b/scripts/ci/check-semgrep-parity.sh new file mode 100755 index 000000000..dd7e4d0a2 --- /dev/null +++ b/scripts/ci/check-semgrep-parity.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Structural parity guard for the Semgrep CI workflow, modeled on +# check-codeql-parity.sh's approach (grep/structural assertions, not full +# YAML parsing). Unlike the CodeQL guard, this script does NOT compare two +# independent rule-config lists between local and CI — the Semgrep CI +# workflow delegates directly to scripts/pre-commit-hooks/semgrep-scan.sh +# for both its SARIF-producing pass and its hard-fail gate pass, so there is +# exactly one place in the repo that defines --config/--exclude/ +# --exclude-rule values. See docs/plans/current_spec.md §2.7/§3.4 for the +# full rationale. +# +# This guard instead checks the invariants that remain worth checking even +# with zero config duplication: +# 1. Required files exist. +# 2. The additive SEMGREP_SARIF_OUTPUT hook is still present in +# semgrep-scan.sh (a future refactor could drop it without realizing +# CI depends on it). +# 3. semgrep.yml still delegates to the real script for both passes, +# rather than a future edit reintroducing an inline `semgrep scan` +# call (which would silently reintroduce config duplication). +# 4. The pinned container image reference has both a tag and a digest +# (catches an accidental un-pin, e.g. a quick edit to `:latest`). +# 5. pull_request/push trigger branches match [main, nightly, development]. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/workflow-yaml-asserts.sh +source "${SCRIPT_DIR}/lib/workflow-yaml-asserts.sh" + +SEMGREP_WORKFLOW=".github/workflows/semgrep.yml" +SEMGREP_SCRIPT="scripts/pre-commit-hooks/semgrep-scan.sh" + +fail() { + local message="$1" + echo "::error title=Semgrep parity drift::${message}" + exit 1 +} + +# --- Check 1: required files exist --- +[[ -f "$SEMGREP_WORKFLOW" ]] || fail "Missing workflow file: $SEMGREP_WORKFLOW" +[[ -f "$SEMGREP_SCRIPT" ]] || fail "Missing pre-commit script: $SEMGREP_SCRIPT" + +command -v jq >/dev/null 2>&1 || fail "jq is required for semantic Semgrep parity checks" + +# --- Check 2: additive SARIF hook still present in the script --- +grep -Fq 'SEMGREP_SARIF_OUTPUT' "$SEMGREP_SCRIPT" || fail "$SEMGREP_SCRIPT must retain the SEMGREP_SARIF_OUTPUT hook so CI can produce SARIF via the same script CI/local both use" + +# --- Check 3: workflow delegates to the real script for both passes, --- +# --- rather than a reimplemented/inlined `semgrep scan ...` invocation --- +grep -Fq 'SEMGREP_SARIF_OUTPUT' "$SEMGREP_WORKFLOW" || fail "$SEMGREP_WORKFLOW must set SEMGREP_SARIF_OUTPUT for its SARIF-producing pass" +grep -Fq "$SEMGREP_SCRIPT" "$SEMGREP_WORKFLOW" || fail "$SEMGREP_WORKFLOW must delegate to $SEMGREP_SCRIPT instead of reimplementing the semgrep scan invocation inline" + +# Count distinct delegating calls to the real script; there must be at least +# two (the SARIF pass and the hard-fail gate pass). +DELEGATE_CALL_COUNT="$(grep -Fc "bash ${SEMGREP_SCRIPT}" "$SEMGREP_WORKFLOW" || true)" +if [[ "$DELEGATE_CALL_COUNT" -lt 2 ]]; then + fail "$SEMGREP_WORKFLOW must call 'bash $SEMGREP_SCRIPT' at least twice (SARIF pass + hard-fail gate pass); found $DELEGATE_CALL_COUNT" +fi + +! grep -Eq '^\s*semgrep scan\b' "$SEMGREP_WORKFLOW" || fail "$SEMGREP_WORKFLOW must not contain an inline 'semgrep scan' invocation — delegate to $SEMGREP_SCRIPT" + +# --- Check 4: pinned image reference has both tag and digest --- +grep -Eq 'semgrep/semgrep:[0-9]+\.[0-9]+\.[0-9]+@sha256:[0-9a-f]{64}' "$SEMGREP_WORKFLOW" || fail "$SEMGREP_WORKFLOW must pin the semgrep/semgrep image with both an exact tag and a sha256 digest" + +# --- Check 5: trigger branches --- +ensure_event_branches_semantic \ + "$SEMGREP_WORKFLOW" \ + "pull_request" \ + "branches: [main, nightly, development]" \ + "main" "nightly" "development" || fail "semgrep.yml pull_request branches must be [main, nightly, development]" +ensure_event_branches_semantic \ + "$SEMGREP_WORKFLOW" \ + "push" \ + "branches: [main, nightly, development]" \ + "main" "nightly" "development" || fail "semgrep.yml push branches must be [main, nightly, development]" + +echo "Semgrep parity check passed (script delegation + SARIF hook present + image pin format + trigger branches)" diff --git a/scripts/ci/lib/workflow-yaml-asserts.sh b/scripts/ci/lib/workflow-yaml-asserts.sh new file mode 100755 index 000000000..42b702d62 --- /dev/null +++ b/scripts/ci/lib/workflow-yaml-asserts.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# Shared assertion helpers for CI parity-guard scripts (e.g. +# check-codeql-parity.sh, check-semgrep-parity.sh) that need to verify a +# GitHub Actions workflow's `on..branches` list matches an expected +# set of branches. Extracted per CLAUDE.md's "consolidate after second +# occurrence" DRY guideline once a second parity script needed the same +# branch-list assertion logic. +# +# Intended usage: source this file, then call ensure_event_branches_semantic. +# This file only defines functions — it has no side effects when sourced and +# does not set -euo pipefail itself (the sourcing script controls that). + +# ensure_event_branches: AWK-based fallback branch check. Compares the +# literal `branches:` line under `on.:` against expected_line, +# e.g. "branches: [main, nightly, development]". Used when `yq` is +# unavailable or fails. +ensure_event_branches() { + local workflow_file="$1" + local event_name="$2" + local expected_line="$3" + + awk -v event_name="$event_name" -v expected_line="$expected_line" ' + /^on:/ { + in_on = 1 + next + } + + in_on && $1 == event_name ":" { + in_event = 1 + next + } + + in_on && in_event && $1 == "branches:" { + line = $0 + gsub(/^ +/, "", line) + if (line == expected_line) { + found = 1 + } + in_event = 0 + next + } + + in_on && in_event && $1 ~ /^[a-z_]+:$/ { + in_event = 0 + } + + END { + exit found ? 0 : 1 + } + ' "$workflow_file" +} + +# ensure_event_branches_with_yq: semantic (order-independent) branch check +# using yq + jq to parse the workflow YAML directly, rather than matching a +# literal formatted line. +ensure_event_branches_with_yq() { + local workflow_file="$1" + local event_name="$2" + shift 2 + local expected_branches=("$@") + + local expected_json + local actual_json + + expected_json="$(printf '%s\n' "${expected_branches[@]}" | jq -R . | jq -s .)" + + if actual_json="$(yq eval -o=json ".on.${event_name}.branches // []" "$workflow_file" 2>/dev/null)"; then + : + elif actual_json="$(yq -o=json ".on.${event_name}.branches // []" "$workflow_file" 2>/dev/null)"; then + : + else + return 1 + fi + + jq -e \ + --argjson expected "$expected_json" \ + 'if type != "array" then false else ((map(tostring) | unique | sort) == ($expected | map(tostring) | unique | sort)) end' \ + <<<"$actual_json" >/dev/null +} + +# ensure_event_branches_semantic: prefers the semantic yq-based check when +# `yq` is installed, falling back to the literal-line AWK check otherwise. +ensure_event_branches_semantic() { + local workflow_file="$1" + local event_name="$2" + local fallback_line="$3" + shift 3 + local expected_branches=("$@") + + if command -v yq >/dev/null 2>&1; then + if ensure_event_branches_with_yq "$workflow_file" "$event_name" "${expected_branches[@]}"; then + return 0 + fi + fi + + ensure_event_branches "$workflow_file" "$event_name" "$fallback_line" +} diff --git a/scripts/install-codeql.sh b/scripts/install-codeql.sh index 15dbd6caf..983917bd3 100755 --- a/scripts/install-codeql.sh +++ b/scripts/install-codeql.sh @@ -41,7 +41,7 @@ NC='\033[0m' # deliberately and re-validate `lefthook run codeql` locally before raising # this — an exact pin (not "latest") keeps local scans reproducible, per # CLAUDE.md's "use exact dependency versions" convention. -CODEQL_VERSION="${CODEQL_VERSION:-v2.26.2}" +CODEQL_VERSION="${CODEQL_VERSION:-v2.26.3}" # Where to install the `codeql` PATH shim. Defaults to the first writable, # user-owned directory already on PATH so no sudo is required. diff --git a/scripts/pre-commit-hooks/semgrep-scan.sh b/scripts/pre-commit-hooks/semgrep-scan.sh index 6eb5d6a7a..475a90e6a 100755 --- a/scripts/pre-commit-hooks/semgrep-scan.sh +++ b/scripts/pre-commit-hooks/semgrep-scan.sh @@ -39,11 +39,17 @@ else TARGETS=(Dockerfile backend frontend/src scripts .github/workflows) fi +if [ -n "${SEMGREP_SARIF_OUTPUT:-}" ]; then + OUTPUT_FLAGS=(--sarif --output "${SEMGREP_SARIF_OUTPUT}") +else + OUTPUT_FLAGS=(--error) +fi + semgrep scan \ "${SEMGREP_CONFIGS[@]}" \ --severity ERROR \ --severity WARNING \ - --error \ + "${OUTPUT_FLAGS[@]}" \ --exclude "frontend/node_modules" \ --exclude "frontend/coverage" \ --exclude "frontend/dist" \ diff --git a/scripts/security-scan.sh b/scripts/security-scan.sh index b207b5b84..b057b2a08 100755 --- a/scripts/security-scan.sh +++ b/scripts/security-scan.sh @@ -20,7 +20,7 @@ echo "🔒 Running local security scan..." if ! command -v govulncheck &> /dev/null; then echo -e "${YELLOW}Installing govulncheck...${NC}" # renovate: datasource=go depName=golang.org/x/vuln - go install golang.org/x/vuln/cmd/govulncheck@v1.6.0 + go install golang.org/x/vuln/cmd/govulncheck@v1.7.0 fi # Run govulncheck on backend Go code