diff --git a/.docker/docker-entrypoint.sh b/.docker/docker-entrypoint.sh index e660a6ee1..df71b4b99 100755 --- a/.docker/docker-entrypoint.sh +++ b/.docker/docker-entrypoint.sh @@ -72,8 +72,12 @@ mkdir -p /app/data/caddy 2>/dev/null || true mkdir -p /app/data/crowdsec 2>/dev/null || true mkdir -p /app/data/geoip 2>/dev/null || true -# Fix ownership for directories created as root +# Fix ownership for the data volume and required subdirectories when running as root. +# This handles rootless Docker environments where the host volume may be owned by the +# host user (mapped to container UID 0), making it inaccessible to the charon user. if is_root; then + chown charon:charon /app/data 2>/dev/null || true + chown charon:charon /config 2>/dev/null || true chown -R charon:charon /app/data/caddy 2>/dev/null || true chown -R charon:charon /app/data/crowdsec 2>/dev/null || true chown -R charon:charon /app/data/geoip 2>/dev/null || true @@ -303,6 +307,19 @@ ACQUIS_EOF # Also handle case where it might be without trailing slash sed -i 's|log_dir: /var/log$|log_dir: /var/log/crowdsec|g' "$CS_CONFIG_DIR/config.yaml" + # Redirect CrowdSec LAPI database to persistent volume + # Default path /var/lib/crowdsec/data/crowdsec.db is ephemeral (not volume-mounted), + # so it is destroyed on every container rebuild. The bouncer API key (stored on the + # persistent volume at /app/data/crowdsec/) survives rebuilds but the LAPI database + # that validates it does not — causing perpetual key rejection. + # Redirecting db_path to the volume-mounted CS_DATA_DIR fixes this. + sed -i "s|db_path: /var/lib/crowdsec/data/crowdsec.db|db_path: ${CS_DATA_DIR}/crowdsec.db|g" "$CS_CONFIG_DIR/config.yaml" + if grep -q "db_path:.*${CS_DATA_DIR}" "$CS_CONFIG_DIR/config.yaml"; then + echo "✓ CrowdSec LAPI database redirected to persistent volume: ${CS_DATA_DIR}/crowdsec.db" + else + echo "⚠️ WARNING: Could not verify LAPI db_path redirect — bouncer keys may not survive rebuilds" + fi + # Verify LAPI configuration was applied correctly if grep -q "listen_uri:.*:8085" "$CS_CONFIG_DIR/config.yaml"; then echo "✓ CrowdSec LAPI configured for port 8085" @@ -310,23 +327,32 @@ ACQUIS_EOF echo "✗ WARNING: LAPI port configuration may be incorrect" fi - # Always refresh hub index on startup (stale index causes hash mismatch errors on collection install) - echo "Updating CrowdSec hub index..." - if ! timeout 60s cscli hub update 2>&1; then - echo "⚠️ Hub index update failed (network issue?). Collections may fail to install." - echo " CrowdSec will still start with whatever index is cached." - fi - - # Ensure local machine is registered (auto-heal for volume/config mismatch) - # We force registration because we just restored configuration (and likely credentials) + # Machine registration is fast (local DB write) and required for LAPI auth. + # Always run regardless of environment. echo "Registering local machine..." cscli machines add -a --force 2>/dev/null || echo "Warning: Machine registration may have failed" - # Always ensure required collections are present (idempotent — already-installed items are skipped). - # Collections are just config files with zero runtime cost when CrowdSec is disabled. - echo "Ensuring CrowdSec hub items are installed..." - if [ -x /usr/local/bin/install_hub_items.sh ]; then - /usr/local/bin/install_hub_items.sh || echo "⚠️ Some hub items may not have installed. CrowdSec can still start." + # Hub index update and hub item downloads are internet-bound operations (30–120 s). + # Skip them when CHARON_SECURITY_TESTS_ENABLED=false (non-security CI shards) so the + # container starts within the health-check window. + # Production and security-test environments leave this variable unset or true, which + # preserves the existing behaviour. + if [ "${CHARON_SECURITY_TESTS_ENABLED}" = "false" ]; then + echo "⚡ Skipping CrowdSec hub initialization (CHARON_SECURITY_TESTS_ENABLED=false)" + else + # Always refresh hub index on startup (stale index causes hash mismatch errors on collection install) + echo "Updating CrowdSec hub index..." + if ! timeout 60s cscli hub update 2>&1; then + echo "⚠️ Hub index update failed (network issue?). Collections may fail to install." + echo " CrowdSec will still start with whatever index is cached." + fi + + # Always ensure required collections are present (idempotent — already-installed items are skipped). + # Collections are just config files with zero runtime cost when CrowdSec is disabled. + echo "Ensuring CrowdSec hub items are installed..." + if [ -x /usr/local/bin/install_hub_items.sh ]; then + /usr/local/bin/install_hub_items.sh || echo "⚠️ Some hub items may not have installed. CrowdSec can still start." + fi fi # Fix ownership AFTER cscli commands (they run as root and create root-owned files) diff --git a/.github/renovate.json b/.github/renovate.json index 6a4451cf5..92f8c882b 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -7,7 +7,6 @@ "helpers:pinGitHubActionDigests" ], "baseBranchPatterns": [ - "feature/beta-release", "development" ], "postUpdateOptions": ["npmDedupe"], @@ -24,6 +23,7 @@ ".docker/**" ], + "minimumReleaseAge": null, "rebaseWhen": "auto", "vulnerabilityAlerts": { @@ -64,6 +64,19 @@ "datasourceTemplate": "go", "versioningTemplate": "semver" }, + { + "customType": "regex", + "description": "Track Go major-version module patches in Dockerfile via github-tags (workaround: Renovate go datasource cannot resolve /vN paths from custom managers)", + "managerFilePatterns": [ + "/^Dockerfile$/" + ], + "matchStrings": [ + "#\\s*renovate:\\s*datasource=github-tags\\s+depName=(?[^\\s]+)\\s*\\n\\s*go get [^@]+@v(?[^\\s|]+)" + ], + "datasourceTemplate": "github-tags", + "versioningTemplate": "semver", + "extractVersionTemplate": "^v(?.+)$" + }, { "customType": "regex", "description": "Track Alpine base image digest in Dockerfile for security updates", @@ -242,6 +255,20 @@ "depNameTemplate": "golang/go", "datasourceTemplate": "golang-version", "versioningTemplate": "semver" + }, + { + "customType": "regex", + "description": "Track golangci-lint version in quality checks workflow", + "managerFilePatterns": [ + "/^\\.github/workflows/quality-checks\\.yml$/" + ], + "matchStrings": [ + "# renovate: datasource=github-releases depName=golangci/golangci-lint\\n\\s+version: v(?[^\\s]+)" + ], + "depNameTemplate": "golangci/golangci-lint", + "datasourceTemplate": "github-releases", + "versioningTemplate": "semver", + "extractVersionTemplate": "^v(?.*)" } ], @@ -264,19 +291,12 @@ "matchPackageNames": [ "*" ] - }, - { - "description": "Feature branches: Auto-merge non-major updates after proven stable", - "matchBaseBranches": ["feature/**"], - "matchUpdateTypes": ["minor", "patch", "pin", "digest"], - "automerge": false }, { "description": "Development branch: Auto-merge non-major updates after proven stable", "matchBaseBranches": ["development"], "matchUpdateTypes": ["minor", "patch", "pin", "digest"], - "automerge": false, - "minimumReleaseAge": "14 days" + "automerge": false }, { "description": "Preserve your custom Caddy patch labels but allow them to group into a single PR", @@ -296,22 +316,31 @@ "allowedVersions": "<3.0.0" }, { - "description": "Go: keep pgx within v4 (CrowdSec requires pgx/v4 module path)", + "description": "Go: keep pgx within v4 (CrowdSec requires pgx/v4 module path) - applies to go.mod lookups", "matchDatasources": ["go"], "matchPackageNames": ["github.com/jackc/pgx/v4"], - "allowedVersions": "<5.0.0" + "allowedVersions": "<5.0.0", + "sourceUrl": "https://github.com/jackc/pgx" + }, + { + "description": "jackc/pgx via github-tags: constrain to v4.x.x patch releases (Dockerfile CVE pin)", + "matchDatasources": ["github-tags"], + "matchPackageNames": ["jackc/pgx"], + "allowedVersions": ">=4.0.0 <5.0.0" }, { "description": "Go: keep go-jose/v3 within v3 (v4 is a different Go module path)", "matchDatasources": ["go"], "matchPackageNames": ["github.com/go-jose/go-jose/v3"], - "allowedVersions": "<4.0.0" + "allowedVersions": "<4.0.0", + "sourceUrl": "https://github.com/go-jose/go-jose" }, { "description": "Go: keep go-jose/v4 within v4 (v5 would be a different Go module path)", "matchDatasources": ["go"], "matchPackageNames": ["github.com/go-jose/go-jose/v4"], - "allowedVersions": "<5.0.0" + "allowedVersions": "<5.0.0", + "sourceUrl": "https://github.com/go-jose/go-jose" }, { "description": "Safety: Keep MAJOR updates separate and require manual review", @@ -324,6 +353,66 @@ "matchDatasources": ["go"], "matchPackageNames": ["github.com/oschwald/geoip2-golang/v2"], "sourceUrl": "https://github.com/oschwald/geoip2-golang" + }, + { + "description": "Fix Renovate lookup for google/uuid", + "matchDatasources": ["go"], + "matchPackageNames": ["github.com/google/uuid"], + "sourceUrl": "https://github.com/google/uuid" + }, + { + "description": "Fix Renovate lookup for golang-jwt/jwt v5 module path", + "matchDatasources": ["go"], + "matchPackageNames": ["github.com/golang-jwt/jwt/v5"], + "sourceUrl": "https://github.com/golang-jwt/jwt" + }, + { + "description": "Fix Renovate lookup for robfig/cron v3 module path", + "matchDatasources": ["go"], + "matchPackageNames": ["github.com/robfig/cron/v3"], + "sourceUrl": "https://github.com/robfig/cron" + }, + { + "description": "Fix Renovate lookup for oschwald/maxminddb-golang v2 module path", + "matchDatasources": ["go"], + "matchPackageNames": ["github.com/oschwald/maxminddb-golang/v2"], + "sourceUrl": "https://github.com/oschwald/maxminddb-golang" + }, + { + "description": "Fix Renovate lookup for cespare/xxhash v2 module path", + "matchDatasources": ["go"], + "matchPackageNames": ["github.com/cespare/xxhash/v2"], + "sourceUrl": "https://github.com/cespare/xxhash" + }, + { + "description": "Fix Renovate lookup for klauspost/cpuid v2 module path", + "matchDatasources": ["go"], + "matchPackageNames": ["github.com/klauspost/cpuid/v2"], + "sourceUrl": "https://github.com/klauspost/cpuid" + }, + { + "description": "Fix Renovate lookup for pelletier/go-toml v2 module path", + "matchDatasources": ["go"], + "matchPackageNames": ["github.com/pelletier/go-toml/v2"], + "sourceUrl": "https://github.com/pelletier/go-toml" + }, + { + "description": "Fix Renovate lookup for go-playground/validator v10 module path", + "matchDatasources": ["go"], + "matchPackageNames": ["github.com/go-playground/validator/v10"], + "sourceUrl": "https://github.com/go-playground/validator" + }, + { + "description": "Fix Renovate lookup for gorm.io/gorm (vanity domain maps to go-gorm/gorm)", + "matchDatasources": ["go"], + "matchPackageNames": ["gorm.io/gorm"], + "sourceUrl": "https://github.com/go-gorm/gorm" + }, + { + "description": "Fix Renovate lookup for gorm.io/driver/sqlite (vanity domain maps to go-gorm/sqlite)", + "matchDatasources": ["go"], + "matchPackageNames": ["gorm.io/driver/sqlite"], + "sourceUrl": "https://github.com/go-gorm/sqlite" } ] } diff --git a/.github/skills/examples/gorm-scanner-ci-workflow.yml b/.github/skills/examples/gorm-scanner-ci-workflow.yml index 3144c1cd4..d5045d725 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@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: - go-version: "1.26.2" + go-version: "1.26.3" - 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 baf62ac70..4ec8eae4a 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.111.0" + log_error "Installation: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.112.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.42.4" -set_default_env "GRYPE_VERSION" "v0.111.0" +set_default_env "SYFT_VERSION" "v1.44.0" +set_default_env "GRYPE_VERSION" "v0.112.0" set_default_env "IMAGE_TAG" "charon:local" set_default_env "FAIL_ON_SEVERITY" "Critical,High" diff --git a/.github/workflows/auto-add-to-project.yml b/.github/workflows/auto-add-to-project.yml index fa74c1464..e53d6be07 100644 --- a/.github/workflows/auto-add-to-project.yml +++ b/.github/workflows/auto-add-to-project.yml @@ -26,7 +26,7 @@ jobs: - name: Add issue or PR to project if: steps.project_check.outputs.has_project == 'true' - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 + uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2.0.0 continue-on-error: true with: project-url: ${{ secrets.PROJECT_URL }} diff --git a/.github/workflows/auto-changelog.yml b/.github/workflows/auto-changelog.yml index e9bf4756f..cd3fc914f 100644 --- a/.github/workflows/auto-changelog.yml +++ b/.github/workflows/auto-changelog.yml @@ -24,6 +24,6 @@ jobs: with: ref: ${{ github.event.workflow_run.head_sha || github.sha }} - name: Draft Release - uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7 + uses: release-drafter/release-drafter@c2e2804cc59f45f57076a99af580d0fedb697927 # v7 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index aaa131c0b..8619215b2 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.2' + GO_VERSION: '1.26.3' GOTOOLCHAIN: auto # Minimal permissions at workflow level; write permissions granted at job level for push only @@ -52,7 +52,7 @@ jobs: # This avoids gh-pages branch errors and permission issues on fork PRs if: github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' # Security: Pinned to full SHA for supply chain security - uses: benchmark-action/github-action-benchmark@4e0b38bc48375986542b13c0d8976b7b80c60c00 # v1 + uses: benchmark-action/github-action-benchmark@52576c92bccf6ac60c8223ec7eb2565637cae9ba # v1.22.1 with: name: Go Benchmark tool: 'go' diff --git a/.github/workflows/codecov-upload.yml b/.github/workflows/codecov-upload.yml index b7153222f..8fced573f 100644 --- a/.github/workflows/codecov-upload.yml +++ b/.github/workflows/codecov-upload.yml @@ -23,7 +23,7 @@ concurrency: cancel-in-progress: true env: - GO_VERSION: '1.26.2' + GO_VERSION: '1.26.3' NODE_VERSION: '24.12.0' GOTOOLCHAIN: auto @@ -166,7 +166,7 @@ jobs: ref: ${{ github.sha }} - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 57e446519..2f3ea9ab2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -4,7 +4,7 @@ on: pull_request: branches: [main, nightly, development] push: - branches: [main] + branches: [main, nightly, development] workflow_dispatch: schedule: - cron: '0 3 * * 1' # Mondays 03:00 UTC @@ -15,7 +15,7 @@ concurrency: env: GOTOOLCHAIN: auto - GO_VERSION: '1.26.2' + GO_VERSION: '1.26.3' permissions: contents: read @@ -52,7 +52,7 @@ jobs: run: bash scripts/ci/check-codeql-parity.sh - name: Initialize CodeQL - uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4 + uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4 with: languages: ${{ matrix.language }} queries: security-and-quality @@ -92,16 +92,17 @@ jobs: run: mkdir -p sarif-results - name: Autobuild - uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4 + uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4 + id: codeql_analyze + uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4 with: category: "/language:${{ matrix.language }}" output: sarif-results/${{ matrix.language }} - name: Check CodeQL Results - if: always() + if: always() && steps.codeql_analyze.conclusion != 'skipped' run: | set -euo pipefail SARIF_DIR="sarif-results/${{ matrix.language }}" @@ -194,7 +195,7 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" - name: Fail on High-Severity Findings - if: always() + if: always() && steps.codeql_analyze.conclusion != 'skipped' run: | set -euo pipefail SARIF_DIR="sarif-results/${{ matrix.language }}" diff --git a/.github/workflows/container-prune.yml b/.github/workflows/container-prune.yml index 8c2a9d0cf..ba48bb1ed 100644 --- a/.github/workflows/container-prune.yml +++ b/.github/workflows/container-prune.yml @@ -25,9 +25,13 @@ permissions: jobs: prune-ghcr: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + image: [charon, orthrus] env: OWNER: ${{ github.repository_owner }} - IMAGE_NAME: charon + IMAGE_NAME: ${{ matrix.image }} KEEP_DAYS: ${{ github.event.inputs.keep_days || '30' }} KEEP_LAST_N: ${{ github.event.inputs.keep_last_n || '30' }} DRY_RUN: ${{ github.event_name == 'pull_request' && 'true' || github.event.inputs.dry_run || 'false' }} @@ -47,14 +51,21 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | chmod +x scripts/prune-ghcr.sh - ./scripts/prune-ghcr.sh 2>&1 | tee prune-ghcr-${{ github.run_id }}.log + ./scripts/prune-ghcr.sh 2>&1 | tee prune-ghcr-${{ matrix.image }}-${{ github.run_id }}.log + + - name: Namespace summary file + if: always() + run: | + if [ -f prune-summary-ghcr.env ]; then + mv prune-summary-ghcr.env prune-summary-ghcr-${{ matrix.image }}.env + fi - name: Summarize GHCR results if: always() run: | set -euo pipefail - SUMMARY_FILE=prune-summary-ghcr.env - LOG_FILE=prune-ghcr-${{ github.run_id }}.log + SUMMARY_FILE=prune-summary-ghcr-${{ matrix.image }}.env + LOG_FILE=prune-ghcr-${{ matrix.image }}-${{ github.run_id }}.log human() { local bytes=${1:-0} @@ -72,7 +83,7 @@ jobs: TOTAL_DELETED_BYTES=$(grep -E '^TOTAL_DELETED_BYTES=' "$SUMMARY_FILE" | cut -d= -f2 || echo 0) { - echo "## GHCR prune summary" + echo "## GHCR prune summary — ${{ matrix.image }}" echo "- candidates: ${TOTAL_CANDIDATES} (≈ $(human "${TOTAL_CANDIDATES_BYTES}"))" echo "- deleted: ${TOTAL_DELETED} (≈ $(human "${TOTAL_DELETED_BYTES}"))" } >> "$GITHUB_STEP_SUMMARY" @@ -81,7 +92,7 @@ jobs: deleted_count=$(grep -cE 'deleting |DRY RUN: would delete' "$LOG_FILE" || true) { - echo "## GHCR prune summary" + echo "## GHCR prune summary — ${{ matrix.image }}" echo "- deleted (approx): ${deleted_count} (≈ $(human "${deleted_bytes}"))" } >> "$GITHUB_STEP_SUMMARY" fi @@ -90,16 +101,20 @@ jobs: if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: prune-ghcr-log-${{ github.run_id }} + name: prune-ghcr-${{ matrix.image }}-log-${{ github.run_id }} path: | - prune-ghcr-${{ github.run_id }}.log - prune-summary-ghcr.env + prune-ghcr-${{ matrix.image }}-${{ github.run_id }}.log + prune-summary-ghcr-${{ matrix.image }}.env prune-dockerhub: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + image: [charon, orthrus] env: OWNER: ${{ github.repository_owner }} - IMAGE_NAME: charon + IMAGE_NAME: ${{ matrix.image }} KEEP_DAYS: ${{ github.event.inputs.keep_days || '30' }} KEEP_LAST_N: ${{ github.event.inputs.keep_last_n || '30' }} DRY_RUN: ${{ github.event_name == 'pull_request' && 'true' || github.event.inputs.dry_run || 'false' }} @@ -118,14 +133,21 @@ jobs: DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} run: | chmod +x scripts/prune-dockerhub.sh - ./scripts/prune-dockerhub.sh 2>&1 | tee prune-dockerhub-${{ github.run_id }}.log + ./scripts/prune-dockerhub.sh 2>&1 | tee prune-dockerhub-${{ matrix.image }}-${{ github.run_id }}.log + + - name: Namespace summary file + if: always() + run: | + if [ -f prune-summary-dockerhub.env ]; then + mv prune-summary-dockerhub.env prune-summary-dockerhub-${{ matrix.image }}.env + fi - name: Summarize Docker Hub results if: always() run: | set -euo pipefail - SUMMARY_FILE=prune-summary-dockerhub.env - LOG_FILE=prune-dockerhub-${{ github.run_id }}.log + SUMMARY_FILE=prune-summary-dockerhub-${{ matrix.image }}.env + LOG_FILE=prune-dockerhub-${{ matrix.image }}-${{ github.run_id }}.log human() { local bytes=${1:-0} @@ -143,7 +165,7 @@ jobs: TOTAL_DELETED_BYTES=$(grep -E '^TOTAL_DELETED_BYTES=' "$SUMMARY_FILE" | cut -d= -f2 || echo 0) { - echo "## Docker Hub prune summary" + echo "## Docker Hub prune summary — ${{ matrix.image }}" echo "- candidates: ${TOTAL_CANDIDATES} (≈ $(human "${TOTAL_CANDIDATES_BYTES}"))" echo "- deleted: ${TOTAL_DELETED} (≈ $(human "${TOTAL_DELETED_BYTES}"))" } >> "$GITHUB_STEP_SUMMARY" @@ -152,7 +174,7 @@ jobs: deleted_count=$(grep -cE 'deleting |DRY RUN: would delete' "$LOG_FILE" || true) { - echo "## Docker Hub prune summary" + echo "## Docker Hub prune summary — ${{ matrix.image }}" echo "- deleted (approx): ${deleted_count} (≈ $(human "${deleted_bytes}"))" } >> "$GITHUB_STEP_SUMMARY" fi @@ -161,10 +183,10 @@ jobs: if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: prune-dockerhub-log-${{ github.run_id }} + name: prune-dockerhub-${{ matrix.image }}-log-${{ github.run_id }} path: | - prune-dockerhub-${{ github.run_id }}.log - prune-summary-dockerhub.env + prune-dockerhub-${{ matrix.image }}-${{ github.run_id }}.log + prune-summary-dockerhub-${{ matrix.image }}.env summarize: runs-on: ubuntu-latest @@ -190,35 +212,51 @@ jobs: awk -v b="$bytes" 'BEGIN { split("B KiB MiB GiB TiB",u," "); i=0; x=b; while(x>1024){x/=1024;i++} printf "%0.2f %s", x, u[i+1] }' } - GHCR_CANDIDATES=0 GHCR_CANDIDATES_BYTES=0 GHCR_DELETED=0 GHCR_DELETED_BYTES=0 - if [ -f prune-summary-ghcr.env ]; then - GHCR_CANDIDATES=$(grep -E '^TOTAL_CANDIDATES=' prune-summary-ghcr.env | cut -d= -f2 || echo 0) - GHCR_CANDIDATES_BYTES=$(grep -E '^TOTAL_CANDIDATES_BYTES=' prune-summary-ghcr.env | cut -d= -f2 || echo 0) - GHCR_DELETED=$(grep -E '^TOTAL_DELETED=' prune-summary-ghcr.env | cut -d= -f2 || echo 0) - GHCR_DELETED_BYTES=$(grep -E '^TOTAL_DELETED_BYTES=' prune-summary-ghcr.env | cut -d= -f2 || echo 0) - fi - - HUB_CANDIDATES=0 HUB_CANDIDATES_BYTES=0 HUB_DELETED=0 HUB_DELETED_BYTES=0 - if [ -f prune-summary-dockerhub.env ]; then - HUB_CANDIDATES=$(grep -E '^TOTAL_CANDIDATES=' prune-summary-dockerhub.env | cut -d= -f2 || echo 0) - HUB_CANDIDATES_BYTES=$(grep -E '^TOTAL_CANDIDATES_BYTES=' prune-summary-dockerhub.env | cut -d= -f2 || echo 0) - HUB_DELETED=$(grep -E '^TOTAL_DELETED=' prune-summary-dockerhub.env | cut -d= -f2 || echo 0) - HUB_DELETED_BYTES=$(grep -E '^TOTAL_DELETED_BYTES=' prune-summary-dockerhub.env | cut -d= -f2 || echo 0) - fi + read_field() { + local file="$1" field="$2" + if [ -f "$file" ]; then + grep -E "^${field}=" "$file" | cut -d= -f2 | head -n1 + else + echo 0 + fi + } - TOTAL_CANDIDATES=$((GHCR_CANDIDATES + HUB_CANDIDATES)) - TOTAL_CANDIDATES_BYTES=$((GHCR_CANDIDATES_BYTES + HUB_CANDIDATES_BYTES)) - TOTAL_DELETED=$((GHCR_DELETED + HUB_DELETED)) - TOTAL_DELETED_BYTES=$((GHCR_DELETED_BYTES + HUB_DELETED_BYTES)) + TOTAL_CANDIDATES=0 + TOTAL_CANDIDATES_BYTES=0 + TOTAL_DELETED=0 + TOTAL_DELETED_BYTES=0 { echo "## Combined container prune summary" echo "" - echo "| Registry | Candidates | Deleted | Space Reclaimed |" - echo "|----------|------------|---------|-----------------|" - echo "| GHCR | ${GHCR_CANDIDATES} | ${GHCR_DELETED} | $(human "${GHCR_DELETED_BYTES}") |" - echo "| Docker Hub | ${HUB_CANDIDATES} | ${HUB_DELETED} | $(human "${HUB_DELETED_BYTES}") |" - echo "| **Total** | **${TOTAL_CANDIDATES}** | **${TOTAL_DELETED}** | **$(human "${TOTAL_DELETED_BYTES}")** |" + echo "| Registry | Image | Candidates | Deleted | Space Reclaimed |" + echo "|----------|-------|------------|---------|-----------------|" + } >> "$GITHUB_STEP_SUMMARY" + + for registry in ghcr dockerhub; do + for image in charon orthrus; do + file="prune-summary-${registry}-${image}.env" + cands=$(read_field "$file" TOTAL_CANDIDATES) + cands_b=$(read_field "$file" TOTAL_CANDIDATES_BYTES) + dels=$(read_field "$file" TOTAL_DELETED) + dels_b=$(read_field "$file" TOTAL_DELETED_BYTES) + + cands=${cands:-0}; cands_b=${cands_b:-0}; dels=${dels:-0}; dels_b=${dels_b:-0} + + TOTAL_CANDIDATES=$((TOTAL_CANDIDATES + cands)) + TOTAL_CANDIDATES_BYTES=$((TOTAL_CANDIDATES_BYTES + cands_b)) + TOTAL_DELETED=$((TOTAL_DELETED + dels)) + TOTAL_DELETED_BYTES=$((TOTAL_DELETED_BYTES + dels_b)) + + registry_label="GHCR" + [ "$registry" = "dockerhub" ] && registry_label="Docker Hub" + + echo "| ${registry_label} | ${image} | ${cands} | ${dels} | $(human "${dels_b}") |" >> "$GITHUB_STEP_SUMMARY" + done + done + + { + echo "| **Total** | — | **${TOTAL_CANDIDATES}** | **${TOTAL_DELETED}** | **$(human "${TOTAL_DELETED_BYTES}")** |" } >> "$GITHUB_STEP_SUMMARY" printf 'PRUNE_SUMMARY: candidates=%s candidates_bytes=%s deleted=%s deleted_bytes=%s\n' \ diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 106830979..ac4a48f26 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -47,6 +47,7 @@ env: TRIGGER_HEAD_REF: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.head_ref }} TRIGGER_PR_NUMBER: ${{ github.event_name == 'workflow_run' && join(github.event.workflow_run.pull_requests.*.number, '') || format('{0}', github.event.pull_request.number) }} TRIGGER_ACTOR: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.actor.login || github.actor }} + TRIGGER_BASE_REF: ${{ github.event_name == 'workflow_run' && '' || github.base_ref }} jobs: build-and-push: @@ -215,6 +216,7 @@ jobs: type=raw,value=latest,enable=${{ env.TRIGGER_REF == 'refs/heads/main' }} type=raw,value=dev,enable=${{ env.TRIGGER_REF == 'refs/heads/development' }} type=raw,value=nightly,enable=${{ env.TRIGGER_REF == 'refs/heads/nightly' }} + type=raw,value=beta,enable=${{ env.TRIGGER_EVENT == 'pull_request' && env.TRIGGER_BASE_REF == 'development' }} type=raw,value=${{ steps.branch-tags.outputs.pr_feature_branch_sha_tag }},enable=${{ env.TRIGGER_EVENT == 'pull_request' && steps.branch-tags.outputs.pr_feature_branch_sha_tag != '' }} type=raw,value=${{ steps.branch-tags.outputs.feature_branch_tag }},enable=${{ env.TRIGGER_EVENT != 'pull_request' && startsWith(env.TRIGGER_REF, 'refs/heads/feature/') && steps.branch-tags.outputs.feature_branch_tag != '' }} type=raw,value=${{ steps.branch-tags.outputs.branch_sha_tag }},enable=${{ env.TRIGGER_EVENT != 'pull_request' && steps.branch-tags.outputs.branch_sha_tag != '' }} @@ -535,25 +537,25 @@ jobs: - name: Run Trivy scan (table output) if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true' - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }} format: 'table' severity: 'CRITICAL,HIGH' exit-code: '0' - version: 'v0.69.3' + version: 'v0.70.0' continue-on-error: true - name: Run Trivy vulnerability scanner (SARIF) if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true' id: trivy - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }} format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' - version: 'v0.69.3' + version: 'v0.70.0' continue-on-error: true - name: Check Trivy SARIF exists @@ -568,7 +570,7 @@ jobs: - name: Upload Trivy results if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.trivy-check.outputs.exists == 'true' - uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: sarif_file: 'trivy-results.sarif' category: '.github/workflows/docker-build.yml:build-and-push' @@ -583,6 +585,7 @@ jobs: image: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }} format: cyclonedx-json output-file: sbom.cyclonedx.json + syft-version: v1.44.0 # Create verifiable attestation for the SBOM - name: Attest SBOM @@ -597,7 +600,7 @@ jobs: # Install Cosign for keyless signing - name: Install Cosign if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true' - uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 # Sign GHCR image with keyless signing (Sigstore/Fulcio) - name: Sign GHCR Image @@ -695,24 +698,24 @@ jobs: echo "✅ Image freshness validated" - name: Run Trivy scan on PR image (table output) - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ steps.pr-image.outputs.image_ref }} format: 'table' severity: 'CRITICAL,HIGH' exit-code: '0' - version: 'v0.69.3' + version: 'v0.70.0' - name: Run Trivy scan on PR image (SARIF - blocking) id: trivy-scan - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ steps.pr-image.outputs.image_ref }} format: 'sarif' output: 'trivy-pr-results.sarif' severity: 'CRITICAL,HIGH' exit-code: '1' # Intended to block, but continued on error for now - version: 'v0.69.3' + version: 'v0.70.0' continue-on-error: true - name: Check Trivy PR SARIF exists @@ -727,14 +730,14 @@ jobs: - name: Upload Trivy scan results if: always() && steps.trivy-pr-check.outputs.exists == 'true' - uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: sarif_file: 'trivy-pr-results.sarif' category: 'docker-pr-image' - name: Upload Trivy compatibility results (docker-build category) if: always() && steps.trivy-pr-check.outputs.exists == 'true' - uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: sarif_file: 'trivy-pr-results.sarif' category: '.github/workflows/docker-build.yml:build-and-push' @@ -742,7 +745,7 @@ jobs: - name: Upload Trivy compatibility results (docker-publish alias) if: always() && steps.trivy-pr-check.outputs.exists == 'true' - uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: sarif_file: 'trivy-pr-results.sarif' category: '.github/workflows/docker-publish.yml:build-and-push' @@ -750,7 +753,7 @@ jobs: - name: Upload Trivy compatibility results (nightly alias) if: always() && steps.trivy-pr-check.outputs.exists == 'true' - uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: sarif_file: 'trivy-pr-results.sarif' category: 'trivy-nightly' diff --git a/.github/workflows/docs-to-issues.yml b/.github/workflows/docs-to-issues.yml index 04d636830..24416231d 100644 --- a/.github/workflows/docs-to-issues.yml +++ b/.github/workflows/docs-to-issues.yml @@ -44,7 +44,7 @@ jobs: ref: ${{ github.event.workflow_run.head_sha || github.sha }} - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: ${{ env.NODE_VERSION }} @@ -343,7 +343,8 @@ jobs: # Removed [skip ci] to allow CI checks to run on PRs # Infinite loop protection: path filter excludes docs/issues/created/** AND github.actor guard prevents bot loops git diff --staged --quiet || git commit -m "chore: move processed issue files to created/" - git push + BRANCH="${{ github.event.workflow_run.head_branch || github.ref_name }}" + git push origin HEAD:refs/heads/${BRANCH} - name: Summary if: always() diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 15a35f24d..93bae6351 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -38,7 +38,7 @@ jobs: # Step 2: Set up Node.js (for building any JS-based doc tools) - name: 🔧 Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: ${{ env.NODE_VERSION }} diff --git a/.github/workflows/e2e-tests-split.yml b/.github/workflows/e2e-tests-split.yml index ed81d8c60..278604a28 100644 --- a/.github/workflows/e2e-tests-split.yml +++ b/.github/workflows/e2e-tests-split.yml @@ -83,7 +83,7 @@ on: env: NODE_VERSION: '20' - GO_VERSION: '1.26.2' + GO_VERSION: '1.26.3' GOTOOLCHAIN: auto DOCKERHUB_REGISTRY: docker.io IMAGE_NAME: ${{ github.repository_owner }}/charon @@ -151,7 +151,7 @@ jobs: - name: Set up Node.js if: steps.resolve-image.outputs.image_source == 'build' - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' @@ -225,7 +225,7 @@ jobs: ref: ${{ github.sha }} - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' @@ -286,7 +286,7 @@ jobs: - name: Wait for service health run: | echo "⏳ Waiting for Charon to be healthy..." - MAX_ATTEMPTS=30 + MAX_ATTEMPTS=60 ATTEMPT=0 while [[ ${ATTEMPT} -lt ${MAX_ATTEMPTS} ]]; do ATTEMPT=$((ATTEMPT + 1)) @@ -427,7 +427,7 @@ jobs: ref: ${{ github.sha }} - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' @@ -488,7 +488,7 @@ jobs: - name: Wait for service health run: | echo "⏳ Waiting for Charon to be healthy..." - MAX_ATTEMPTS=30 + MAX_ATTEMPTS=60 ATTEMPT=0 while [[ ${ATTEMPT} -lt ${MAX_ATTEMPTS} ]]; do ATTEMPT=$((ATTEMPT + 1)) @@ -637,7 +637,7 @@ jobs: ref: ${{ github.sha }} - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' @@ -698,7 +698,7 @@ jobs: - name: Wait for service health run: | echo "⏳ Waiting for Charon to be healthy..." - MAX_ATTEMPTS=30 + MAX_ATTEMPTS=60 ATTEMPT=0 while [[ ${ATTEMPT} -lt ${MAX_ATTEMPTS} ]]; do ATTEMPT=$((ATTEMPT + 1)) @@ -859,7 +859,7 @@ jobs: ref: ${{ github.sha }} - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' @@ -935,7 +935,7 @@ jobs: - name: Wait for service health run: | echo "⏳ Waiting for Charon to be healthy..." - MAX_ATTEMPTS=30 + MAX_ATTEMPTS=60 ATTEMPT=0 while [[ ${ATTEMPT} -lt ${MAX_ATTEMPTS} ]]; do ATTEMPT=$((ATTEMPT + 1)) @@ -980,6 +980,7 @@ jobs: --project=chromium \ --shard=${{ matrix.shard }}/${{ matrix.total-shards }} \ --output=playwright-output/chromium-shard-${{ matrix.shard }} \ + tests/a11y \ tests/core \ tests/dns-provider-crud.spec.ts \ tests/dns-provider-types.spec.ts \ @@ -1096,7 +1097,7 @@ jobs: ref: ${{ github.sha }} - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' @@ -1172,7 +1173,7 @@ jobs: - name: Wait for service health run: | echo "⏳ Waiting for Charon to be healthy..." - MAX_ATTEMPTS=30 + MAX_ATTEMPTS=60 ATTEMPT=0 while [[ ${ATTEMPT} -lt ${MAX_ATTEMPTS} ]]; do ATTEMPT=$((ATTEMPT + 1)) @@ -1225,6 +1226,7 @@ jobs: --project=firefox \ --shard=${{ matrix.shard }}/${{ matrix.total-shards }} \ --output=playwright-output/firefox-shard-${{ matrix.shard }} \ + tests/a11y \ tests/core \ tests/dns-provider-crud.spec.ts \ tests/dns-provider-types.spec.ts \ @@ -1341,7 +1343,7 @@ jobs: ref: ${{ github.sha }} - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' @@ -1417,7 +1419,7 @@ jobs: - name: Wait for service health run: | echo "⏳ Waiting for Charon to be healthy..." - MAX_ATTEMPTS=30 + MAX_ATTEMPTS=60 ATTEMPT=0 while [[ ${ATTEMPT} -lt ${MAX_ATTEMPTS} ]]; do ATTEMPT=$((ATTEMPT + 1)) @@ -1470,6 +1472,7 @@ jobs: --project=webkit \ --shard=${{ matrix.shard }}/${{ matrix.total-shards }} \ --output=playwright-output/webkit-shard-${{ matrix.shard }} \ + tests/a11y \ tests/core \ tests/dns-provider-crud.spec.ts \ tests/dns-provider-types.spec.ts \ diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index b4dd3a1f3..cf5b124f4 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.2' + GO_VERSION: '1.26.3' NODE_VERSION: '24.12.0' GOTOOLCHAIN: auto GHCR_REGISTRY: ghcr.io @@ -271,7 +271,7 @@ jobs: image: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.resolve_digest.outputs.digest }} format: cyclonedx-json output-file: sbom-nightly.json - syft-version: v1.42.1 + syft-version: v1.44.0 - name: Generate SBOM fallback with pinned Syft if: always() @@ -285,7 +285,7 @@ jobs: echo "Primary SBOM generation failed or produced missing/invalid output; using deterministic Syft fallback" - SYFT_VERSION="v1.42.4" + SYFT_VERSION="v1.44.0" OS="$(uname -s | tr '[:upper:]' '[:lower:]')" ARCH="$(uname -m)" case "$ARCH" in @@ -336,7 +336,7 @@ jobs: # Install Cosign for keyless signing - name: Install Cosign - uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 # Sign GHCR image with keyless signing (Sigstore/Fulcio) - name: Sign GHCR Image @@ -459,16 +459,16 @@ jobs: severity-cutoff: high - name: Scan with Trivy - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ needs.build-and-push-nightly.outputs.digest }} format: 'sarif' output: 'trivy-nightly.sarif' - version: 'v0.69.3' + version: 'v0.70.0' trivyignores: '.trivyignore' - name: Upload Trivy results - uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: sarif_file: 'trivy-nightly.sarif' category: 'trivy-nightly' diff --git a/.github/workflows/orthrus-build.yml b/.github/workflows/orthrus-build.yml new file mode 100644 index 000000000..d6045785f --- /dev/null +++ b/.github/workflows/orthrus-build.yml @@ -0,0 +1,181 @@ +name: Orthrus Agent — Build & Publish + +# Mirrors the trigger and tagging strategy from docker-build.yml so that the +# Orthrus agent image always receives the same identifiable tags as the main +# Charon image (e.g. feature-hecate-abc1234), enabling coordinated manual +# testing on remote servers. + +"on": + pull_request: + paths: + - 'agent/**' + - '.github/workflows/orthrus-build.yml' + push: + branches: [main, development] + tags: ['v*'] + paths: + - 'agent/**' + - '.github/workflows/orthrus-build.yml' + workflow_dispatch: + inputs: + reason: + description: 'Why are you running this manually?' + required: false + default: 'manual trigger' + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }} + cancel-in-progress: true + +env: + GHCR_REGISTRY: ghcr.io + DOCKERHUB_REGISTRY: docker.io + IMAGE_NAME: wikid82/orthrus + GO_VERSION: '1.26.3' + +permissions: + contents: read + packages: write + +jobs: + build-and-push: + env: + HAS_DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN != '' }} + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Normalize image name + run: | + IMAGE_NAME=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]') + echo "IMAGE_NAME=${IMAGE_NAME}" >> "$GITHUB_ENV" + + # Reuse the exact same tag-computation logic as docker-build.yml so that + # feature branches (feature/hecate → feature-hecate-abc1234) and PRs + # (pr-983-abc1234) get matching, pull-able tags on both images. + - name: Compute branch tags + id: branch-tags + run: | + if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then + BRANCH_NAME="$GITHUB_HEAD_REF" + else + BRANCH_NAME="$GITHUB_REF_NAME" + fi + SHORT_SHA="$(echo "$GITHUB_SHA" | cut -c1-7)" + + sanitize_tag() { + local raw="$1" + local max_len="$2" + local sanitized + sanitized=$(echo "$raw" | tr '[:upper:]' '[:lower:]') + sanitized=${sanitized//[^a-z0-9-]/-} + while [[ "$sanitized" == *"--"* ]]; do + sanitized=${sanitized//--/-} + done + sanitized=${sanitized##[^a-z0-9]*} + sanitized=${sanitized%%[^a-z0-9-]*} + if [ -z "$sanitized" ]; then sanitized="branch"; fi + sanitized=$(echo "$sanitized" | cut -c1-"$max_len") + sanitized=${sanitized##[^a-z0-9]*} + if [ -z "$sanitized" ]; then sanitized="branch"; fi + echo "$sanitized" + } + + SANITIZED_BRANCH=$(sanitize_tag "${BRANCH_NAME}" 128) + BASE_BRANCH=$(sanitize_tag "${BRANCH_NAME}" 120) + BRANCH_SHA_TAG="${BASE_BRANCH}-${SHORT_SHA}" + + if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then + if [[ "$BRANCH_NAME" == feature/* ]]; then + echo "pr_feature_branch_sha_tag=${BRANCH_SHA_TAG}" >> "$GITHUB_OUTPUT" + fi + else + echo "branch_sha_tag=${BRANCH_SHA_TAG}" >> "$GITHUB_OUTPUT" + if [[ "$GITHUB_REF" == refs/heads/feature/* ]]; then + echo "feature_branch_tag=${SANITIZED_BRANCH}" >> "$GITHUB_OUTPUT" + echo "feature_branch_sha_tag=${BRANCH_SHA_TAG}" >> "$GITHUB_OUTPUT" + fi + fi + + - name: Set up QEMU + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + + - name: Log in to Docker Hub + if: env.HAS_DOCKERHUB_TOKEN == 'true' + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + with: + registry: docker.io + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Log in to GitHub Container Registry + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate Docker metadata + id: meta + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 + with: + images: | + ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }} + ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} + type=raw,value=dev,enable=${{ github.ref == 'refs/heads/development' }} + type=raw,value=beta,enable=${{ github.event_name == 'pull_request' && github.base_ref == 'development' }} + type=raw,value=${{ steps.branch-tags.outputs.pr_feature_branch_sha_tag }},enable=${{ github.event_name == 'pull_request' && steps.branch-tags.outputs.pr_feature_branch_sha_tag != '' }} + type=raw,value=${{ steps.branch-tags.outputs.feature_branch_tag }},enable=${{ github.event_name != 'pull_request' && startsWith(github.ref, 'refs/heads/feature/') && steps.branch-tags.outputs.feature_branch_tag != '' }} + type=raw,value=${{ steps.branch-tags.outputs.branch_sha_tag }},enable=${{ github.event_name != 'pull_request' && steps.branch-tags.outputs.branch_sha_tag != '' }} + type=raw,value=pr-${{ github.event.pull_request.number }}-{{sha}},enable=${{ github.event_name == 'pull_request' }},prefix=,suffix= + flavor: | + latest=false + labels: | + org.opencontainers.image.title=Orthrus Agent + org.opencontainers.image.description=Lightweight reverse-proxy agent for Charon remote server connectivity + org.opencontainers.image.vendor=Wikid82 + org.opencontainers.image.revision=${{ github.sha }} + io.charon.component=orthrus-agent + io.charon.pr.number=${{ github.event.pull_request.number }} + + - name: Build and push Orthrus agent image + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + with: + context: ./agent + file: ./agent/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VERSION=${{ steps.meta.outputs.version }} + GIT_COMMIT=${{ github.sha }} + BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Print pull instructions + run: | + echo "" + echo "✅ Orthrus agent image published. To test on your remote server:" + echo "" + echo " # Pull the image:" + FIRST_TAG=$(echo "${{ steps.meta.outputs.tags }}" | head -n1) + echo " docker pull ${FIRST_TAG}" + echo "" + echo " # Or use the short SHA tag:" + echo " docker pull ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.branch-tags.outputs.branch_sha_tag || steps.branch-tags.outputs.pr_feature_branch_sha_tag }}" + echo "" + echo "All published tags:" + echo "${{ steps.meta.outputs.tags }}" diff --git a/.github/workflows/propagate-changes.yml b/.github/workflows/propagate-changes.yml index 5b950e21a..4ba1aac21 100644 --- a/.github/workflows/propagate-changes.yml +++ b/.github/workflows/propagate-changes.yml @@ -28,7 +28,7 @@ jobs: (github.event.workflow_run.head_branch == 'main' || github.event.workflow_run.head_branch == 'development') steps: - name: Set up Node (for github-script) - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: ${{ env.NODE_VERSION }} @@ -37,6 +37,8 @@ jobs: env: CURRENT_BRANCH: ${{ github.event.workflow_run.head_branch || github.ref_name }} CURRENT_SHA: ${{ github.event.workflow_run.head_sha || github.sha }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CHARON_TOKEN: ${{ secrets.CHARON_TOKEN }} with: script: | const currentBranch = process.env.CURRENT_BRANCH || context.ref.replace('refs/heads/', ''); @@ -133,7 +135,9 @@ jobs: const sensitive = files.some(fn => configPaths.some(sp => fn.startsWith(sp) || fn.includes(sp))); if (sensitive) { - core.info(`${src} -> ${base} contains sensitive changes (${files.join(', ')}). Skipping automatic propagation.`); + const preview = files.slice(0, 25).join(', '); + const suffix = files.length > 25 ? ` …(+${files.length - 25} more)` : ''; + core.info(`${src} -> ${base} contains sensitive changes (${preview}${suffix}). Skipping automatic propagation.`); return; } } catch (error) { @@ -179,30 +183,5 @@ jobs: core.info('Push originated from development (excluded). Skipping propagation back to development.'); } } else if (currentBranch === 'development') { - // Development -> Feature/Hotfix branches (The Pittsburgh Model) - // We propagate changes from dev DOWN to features/hotfixes so they stay up to date. - - const branches = await github.paginate(github.rest.repos.listBranches, { - owner: context.repo.owner, - repo: context.repo.repo, - }); - - // Filter for feature/* and hotfix/* branches using regex - // AND exclude the branch that just got merged in (if any) - const targetBranches = branches - .map(b => b.name) - .filter(name => { - const isTargetType = /^feature\/|^hotfix\//.test(name); - const isExcluded = (name === excludedBranch); - return isTargetType && !isExcluded; - }); - - core.info(`Found ${targetBranches.length} target branches (excluding '${excludedBranch || 'none'}'): ${targetBranches.join(', ')}`); - - for (const targetBranch of targetBranches) { - await createPR('development', targetBranch); - } + core.info('Push to development detected. No downstream propagation configured.'); } - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CHARON_TOKEN: ${{ secrets.CHARON_TOKEN }} diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index 8033435f2..3093ac90a 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.2' + GO_VERSION: '1.26.3' NODE_VERSION: '24.12.0' GOTOOLCHAIN: auto @@ -193,7 +193,8 @@ jobs: - name: Run golangci-lint uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: - version: latest + # renovate: datasource=github-releases depName=golangci/golangci-lint + version: v2.12.2 working-directory: backend args: --timeout=5m continue-on-error: true @@ -262,7 +263,7 @@ jobs: bash "scripts/repo_health_check.sh" - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' diff --git a/.github/workflows/release-goreleaser.yml b/.github/workflows/release-goreleaser.yml index 0507698cb..f2017ef8f 100644 --- a/.github/workflows/release-goreleaser.yml +++ b/.github/workflows/release-goreleaser.yml @@ -10,7 +10,7 @@ concurrency: cancel-in-progress: false env: - GO_VERSION: '1.26.2' + GO_VERSION: '1.26.3' NODE_VERSION: '24.12.0' GOTOOLCHAIN: auto @@ -52,7 +52,7 @@ jobs: cache-dependency-path: backend/go.sum - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: ${{ env.NODE_VERSION }} @@ -67,7 +67,7 @@ jobs: - name: Install Cross-Compilation Tools (Zig) # Security: Pinned to full SHA for supply chain security - uses: goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406 # v2 + uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 with: version: 0.13.0 @@ -75,7 +75,7 @@ jobs: - name: Run GoReleaser - uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7 + uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7 with: distribution: goreleaser version: '~> v2.5' diff --git a/.github/workflows/renovate.yml b/.github/workflows/renovate.yml index e8b2e9d26..e9a721caf 100644 --- a/.github/workflows/renovate.yml +++ b/.github/workflows/renovate.yml @@ -15,7 +15,7 @@ permissions: issues: write env: - GO_VERSION: '1.26.2' + GO_VERSION: '1.26.3' jobs: renovate: @@ -33,7 +33,7 @@ jobs: go-version: ${{ env.GO_VERSION }} - name: Run Renovate - uses: renovatebot/github-action@eb932558ad942cccfd8211cf535f17ff183a9f74 # v46.1.9 + uses: renovatebot/github-action@693b9ef15eec82123529a37c782242f091365961 # v46.1.14 with: configurationFile: .github/renovate.json token: ${{ secrets.RENOVATE_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/security-pr.yml b/.github/workflows/security-pr.yml index 5f1491385..cb4888d7f 100644 --- a/.github/workflows/security-pr.yml +++ b/.github/workflows/security-pr.yml @@ -226,11 +226,12 @@ jobs: ARTIFACT_ID=$(printf '%s' "${ARTIFACTS_JSON}" | jq -r --arg name "${ARTIFACT_NAME}" '.artifacts[] | select(.name == $name) | .id' | head -n 1) if [[ -z "${ARTIFACT_ID}" ]]; then - echo "❌ reason_category=not_found" - echo "reason=Required artifact was not found" + echo "⚠️ reason_category=not_found" + echo "reason=Artifact not found — build was likely skipped (e.g., renovate/chore PR)" echo "upstream_run_id=${RUN_ID}" echo "artifact_name=${ARTIFACT_NAME}" - exit 1 + echo "artifact_exists=false" >> "$GITHUB_OUTPUT" + exit 0 fi { @@ -241,7 +242,7 @@ jobs: echo "✅ Found artifact: ${ARTIFACT_NAME} (ID: ${ARTIFACT_ID})" - name: Download PR image artifact - if: github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch' + if: (github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch') && steps.check-artifact.outputs.artifact_exists == 'true' # actions/download-artifact v4.1.8 uses: actions/download-artifact@484a0b528fb4d7bd804637ccb632e47a0e638317 with: @@ -250,7 +251,7 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} - name: Load Docker image - if: github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch' + if: (github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch') && steps.check-artifact.outputs.artifact_exists == 'true' id: load-image run: | echo "📦 Loading Docker image..." @@ -364,14 +365,17 @@ jobs: - name: Run Trivy filesystem scan (SARIF output) if: steps.check-artifact.outputs.artifact_exists == 'true' || github.event_name == 'push' || github.event_name == 'pull_request' - # aquasecurity/trivy-action 0.35.0 - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 + # aquasecurity/trivy-action 0.36.0 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 with: scan-type: 'fs' scan-ref: ${{ steps.extract.outputs.binary_path }} format: 'sarif' output: 'trivy-binary-results.sarif' severity: 'CRITICAL,HIGH,MEDIUM' + version: 'v0.70.0' + trivyignores: '.trivyignore' + config: 'trivy.yaml' continue-on-error: true - name: Check Trivy SARIF output exists @@ -396,14 +400,17 @@ jobs: - name: Run Trivy filesystem scan (fail on CRITICAL/HIGH) if: steps.check-artifact.outputs.artifact_exists == 'true' || github.event_name == 'push' || github.event_name == 'pull_request' - # aquasecurity/trivy-action 0.35.0 - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 + # aquasecurity/trivy-action 0.36.0 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 with: scan-type: 'fs' scan-ref: ${{ steps.extract.outputs.binary_path }} format: 'table' severity: 'CRITICAL,HIGH' exit-code: '1' + version: 'v0.70.0' + trivyignores: '.trivyignore' + config: 'trivy.yaml' - name: Upload scan artifacts if: always() && steps.trivy-sarif-check.outputs.exists == 'true' diff --git a/.github/workflows/security-weekly-rebuild.yml b/.github/workflows/security-weekly-rebuild.yml index 45d4cd95d..8e9abab74 100644 --- a/.github/workflows/security-weekly-rebuild.yml +++ b/.github/workflows/security-weekly-rebuild.yml @@ -96,38 +96,38 @@ jobs: BASE_IMAGE=${{ steps.base-image.outputs.digest }} - name: Run Trivy vulnerability scanner (CRITICAL+HIGH) - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} format: 'table' severity: 'CRITICAL,HIGH' exit-code: '1' # Fail workflow if vulnerabilities found - version: 'v0.69.3' + version: 'v0.70.0' continue-on-error: true - name: Run Trivy vulnerability scanner (SARIF) id: trivy-sarif - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} format: 'sarif' output: 'trivy-weekly-results.sarif' severity: 'CRITICAL,HIGH,MEDIUM' - version: 'v0.69.3' + version: 'v0.70.0' - name: Upload Trivy results to GitHub Security - uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: sarif_file: 'trivy-weekly-results.sarif' - name: Run Trivy vulnerability scanner (JSON for artifact) - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} format: 'json' output: 'trivy-weekly-results.json' severity: 'CRITICAL,HIGH,MEDIUM,LOW' - version: 'v0.69.3' + version: 'v0.70.0' - name: Upload Trivy JSON results uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 diff --git a/.github/workflows/supply-chain-pr.yml b/.github/workflows/supply-chain-pr.yml index cc7c12675..8abfaf49f 100644 --- a/.github/workflows/supply-chain-pr.yml +++ b/.github/workflows/supply-chain-pr.yml @@ -272,6 +272,7 @@ jobs: image: ${{ steps.set-target.outputs.image_name }} format: cyclonedx-json output-file: sbom.cyclonedx.json + syft-version: v1.44.0 - name: Count SBOM components if: steps.set-target.outputs.image_name != '' @@ -285,7 +286,7 @@ 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.111.0 + curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.112.0 - name: Scan for vulnerabilities if: steps.set-target.outputs.image_name != '' @@ -362,7 +363,7 @@ jobs: - name: Upload SARIF to GitHub Security if: steps.check-artifact.outputs.artifact_found == 'true' - uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4 + uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # 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 402953e10..7bceb0023 100644 --- a/.github/workflows/supply-chain-verify.yml +++ b/.github/workflows/supply-chain-verify.yml @@ -124,6 +124,7 @@ jobs: image: ghcr.io/${{ github.repository_owner }}/charon:${{ steps.tag.outputs.tag }} format: cyclonedx-json output-file: sbom-verify.cyclonedx.json + syft-version: v1.44.0 - name: Verify SBOM Completeness if: steps.image-check.outputs.exists == 'true' diff --git a/.github/workflows/waf-integration.yml b/.github/workflows/waf-integration.yml index 76bc74d36..465f22f30 100644 --- a/.github/workflows/waf-integration.yml +++ b/.github/workflows/waf-integration.yml @@ -40,8 +40,8 @@ jobs: - name: Run WAF integration tests id: waf-test run: | - chmod +x scripts/coraza_integration.sh - scripts/coraza_integration.sh 2>&1 | tee waf-test-output.txt + chmod +x scripts/waf_integration.sh + scripts/waf_integration.sh 2>&1 | tee waf-test-output.txt exit "${PIPESTATUS[0]}" - name: Dump Debug Info on Failure @@ -53,25 +53,25 @@ jobs: echo "### Container Status" echo '```' - docker ps -a --filter "name=charon" --filter "name=coraza" 2>&1 || true + docker ps -a --filter "name=charon" --filter "name=waf" 2>&1 || true echo '```' echo "" echo "### Caddy Admin Config" echo '```json' - curl -s http://localhost:2019/config 2>/dev/null | head -200 || echo "Could not retrieve Caddy config" + curl -s http://localhost:2119/config/ 2>/dev/null | head -200 || echo "Could not retrieve Caddy config" echo '```' echo "" echo "### Charon Container Logs (last 100 lines)" echo '```' - docker logs charon-debug 2>&1 | tail -100 || echo "No container logs available" + docker logs charon-waf-test 2>&1 | tail -100 || echo "No container logs available" echo '```' echo "" echo "### WAF Ruleset Files" echo '```' - docker exec charon-debug sh -c 'ls -la /app/data/caddy/coraza/rulesets/ 2>/dev/null && echo "---" && cat /app/data/caddy/coraza/rulesets/*.conf 2>/dev/null' || echo "No ruleset files found" + docker exec charon-waf-test sh -c 'ls -la /app/data/caddy/coraza/rulesets/ 2>/dev/null && echo "---" && cat /app/data/caddy/coraza/rulesets/*.conf 2>/dev/null' || echo "No ruleset files found" echo '```' } >> "$GITHUB_STEP_SUMMARY" @@ -100,6 +100,6 @@ jobs: - name: Cleanup if: always() run: | - docker rm -f charon-debug || true - docker rm -f coraza-backend || true + docker rm -f charon-waf-test || true + docker rm -f waf-backend || true docker network rm containers_default || true diff --git a/.gitignore b/.gitignore index fadfc82a8..5b3674d79 100644 --- a/.gitignore +++ b/.gitignore @@ -316,6 +316,13 @@ docs/reports/codecove_patch_report.md vuln-results.json test_output.txt coverage_results.txt -new-results.json -.gitignore final-results.json +new-results.json +scan_output.json +coverage_output.txt +frontend/lint_output.txt +lefthook_out.txt +backend/test_out.txt +backend/cf_coverage.txt +backend/***_coverage.txt +backend/***_cov.txt diff --git a/.grype.yaml b/.grype.yaml index 042c1a792..ec7b426bf 100644 --- a/.grype.yaml +++ b/.grype.yaml @@ -203,45 +203,47 @@ ignore: # GHSA-6g7g-w4f8-9c9x: buger/jsonparser Delete panic on malformed JSON (DoS) # Severity: HIGH (CVSS 7.5) # Package: github.com/buger/jsonparser v1.1.1 (embedded in /usr/local/bin/crowdsec and /usr/local/bin/cscli) - # Status: NO upstream fix available — OSV marks "Last affected: v1.1.1" with no Fixed event + # Status: UPSTREAM FIX EXISTS (v1.1.2 released 2026-03-20) — awaiting CrowdSec to update dependency + # NOTE: As of 2026-04-20, grype v0.111.0 with fresh DB no longer flags this finding in the image. + # This suppression is retained as a safety net in case future DB updates re-surface it. # # Vulnerability Details: # - The Delete function fails to validate offsets on malformed JSON input, producing a # negative slice index and a runtime panic — denial of service (CWE-125). # - CVSSv3: AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H # - # Root Cause (Third-Party Binary + No Upstream Fix): + # Root Cause (Third-Party Binary — Fix Exists Upstream, Not Yet in CrowdSec): # - Charon does not use buger/jsonparser directly. It is compiled into CrowdSec binaries. - # - The buger/jsonparser repository has no released fix as of 2026-03-19 (GitHub issue #275 - # and golang/vulndb #4514 are both open). - # - Fix path: once buger/jsonparser releases a patched version and CrowdSec updates their - # dependency, rebuild the Docker image and remove this suppression. + # - buger/jsonparser released v1.1.2 on 2026-03-20 fixing issue #275. + # - CrowdSec has not yet released a version built with buger/jsonparser v1.1.2. + # - Fix path: once CrowdSec updates their dependency and rebuilds, rebuild the Docker image + # and remove this suppression. # - # Risk Assessment: ACCEPTED (Limited exploitability + no upstream fix) + # Risk Assessment: ACCEPTED (Limited exploitability; fix exists upstream but not yet in CrowdSec) # - The DoS vector requires passing malformed JSON to the vulnerable Delete function within # CrowdSec's internal processing pipeline; this is not a direct attack surface in Charon. # - CrowdSec's exposed surface is its HTTP API (not raw JSON stream parsing via this path). # # Mitigation (active while suppression is in effect): - # - Monitor buger/jsonparser: https://github.com/buger/jsonparser/issues/275 - # - Monitor CrowdSec releases: https://github.com/crowdsecurity/crowdsec/releases + # - Monitor CrowdSec releases for a build using buger/jsonparser >= v1.1.2. + # - CrowdSec releases: https://github.com/crowdsecurity/crowdsec/releases # - Weekly CI security rebuild flags the moment a fixed image ships. # # Review: - # - Reviewed 2026-03-19 (initial suppression): no upstream fix exists. Set 30-day review. - # - Extended 2026-04-04: no upstream fix available. buger/jsonparser issue #275 still open. - # - Next review: 2026-05-19. Remove suppression once buger/jsonparser ships a fix and - # CrowdSec updates their dependency. + # - Reviewed 2026-03-19 (initial suppression): no upstream fix. Set 30-day review. + # - Extended 2026-04-04: no upstream fix. buger/jsonparser issue #275 still open. + # - Updated 2026-04-20: buger/jsonparser v1.1.2 released 2026-03-20. CrowdSec not yet updated. + # Grype v0.111.0 with fresh DB (2026-04-20) no longer flags this finding. Suppression retained + # as a safety net. Next review: 2026-05-19 — remove if CrowdSec ships with v1.1.2+. # # Removal Criteria: - # - buger/jsonparser releases a patched version (v1.1.2 or higher) - # - CrowdSec releases a version built with the patched jsonparser + # - CrowdSec releases a version built with buger/jsonparser >= v1.1.2 # - Rebuild Docker image, run security-scan-docker-image, confirm finding is resolved # - Remove this entry and the corresponding .trivyignore entry simultaneously # # References: # - GHSA-6g7g-w4f8-9c9x: https://github.com/advisories/GHSA-6g7g-w4f8-9c9x - # - Upstream issue: https://github.com/buger/jsonparser/issues/275 + # - Upstream fix: https://github.com/buger/jsonparser/releases/tag/v1.1.2 # - golang/vulndb: https://github.com/golang/vulndb/issues/4514 # - CrowdSec releases: https://github.com/crowdsecurity/crowdsec/releases - vulnerability: GHSA-6g7g-w4f8-9c9x @@ -251,21 +253,20 @@ ignore: type: go-module reason: | HIGH — DoS panic via malformed JSON in buger/jsonparser v1.1.1 embedded in CrowdSec binaries. - No upstream fix: buger/jsonparser has no released patch as of 2026-03-19 (issue #275 open). - Charon does not use this package directly; the vector requires reaching CrowdSec's internal - JSON processing pipeline. Risk accepted; no remediation path until upstream ships a fix. - Reviewed 2026-03-19: no patched release available. - expiry: "2026-05-19" # Extended 2026-04-04: no upstream fix. Next review 2026-05-19. + Upstream fix: buger/jsonparser v1.1.2 released 2026-03-20; CrowdSec has not yet updated their + dependency. Grype no longer flags this as of 2026-04-20 (fresh DB). Suppression retained as + safety net pending CrowdSec update. Charon does not use this package directly. + Updated 2026-04-20: fix v1.1.2 exists upstream; awaiting CrowdSec dependency update. + expiry: "2026-05-19" # Review 2026-05-19: remove if CrowdSec ships with buger/jsonparser >= v1.1.2. # Action items when this suppression expires: - # 1. Check buger/jsonparser releases: https://github.com/buger/jsonparser/releases - # and issue #275: https://github.com/buger/jsonparser/issues/275 - # 2. If a fix has shipped AND CrowdSec has updated their dependency: - # a. Rebuild Docker image and run local security-scan-docker-image - # b. Remove this suppression entry and the corresponding .trivyignore entry - # 3. If no fix yet: Extend expiry by 30 days and update the review comment above - # 4. If extended 3+ times with no progress: Consider opening an issue upstream or - # evaluating whether CrowdSec can replace buger/jsonparser with a safe alternative + # 1. Check if CrowdSec has released a version with buger/jsonparser >= v1.1.2: + # https://github.com/crowdsecurity/crowdsec/releases + # 2. If CrowdSec has updated: rebuild Docker image, run security-scan-docker-image, + # and remove this suppression entry and the corresponding .trivyignore entry + # 3. If grype still does not flag it with fresh DB: consider removing the suppression as + # it may no longer be necessary + # 4. If no CrowdSec update yet: Extend expiry by 30 days # GHSA-jqcq-xjh3-6g23: pgproto3/v2 DataRow.Decode panic on negative field length (DoS) # Severity: HIGH (CVSS 7.5) @@ -482,73 +483,6 @@ ignore: # 4. If not yet migrated: Extend expiry by 30 days and update the review comment above # 5. If extended 3+ times: Open an upstream issue on crowdsecurity/crowdsec requesting pgx/v5 migration - # GHSA-x744-4wpc-v9h2 / CVE-2026-34040: Docker AuthZ plugin bypass via oversized request body - # Severity: HIGH (CVSS 8.8) - # CVSS Vector: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H - # CWE: CWE-863 (Incorrect Authorization) - # Package: github.com/docker/docker v28.5.2+incompatible (go-module) - # Status: Fixed in moby/moby v29.3.1 — NO fix available for docker/docker import path - # - # Vulnerability Details: - # - Incomplete fix for Docker AuthZ plugin bypass (CVE-2024-41110). An attacker can send an - # oversized request body to the Docker daemon, causing it to forward the request to the AuthZ - # plugin without the body, allowing unauthorized approvals. - # - # Root Cause (No Fix Available for Import Path): - # - The fix exists in moby/moby v29.3.1, but not for the docker/docker import path that Charon uses. - # - Migration to moby/moby/v2 is not practical: currently beta with breaking changes. - # - Fix path: once docker/docker publishes a patched version or moby/moby/v2 stabilizes, - # update the dependency and remove this suppression. - # - # Risk Assessment: ACCEPTED (Not exploitable in Charon context) - # - Charon uses the Docker client SDK only (list containers). The vulnerability is server-side - # in the Docker daemon's AuthZ plugin handler. - # - Charon does not run a Docker daemon or use AuthZ plugins. - # - The attack vector requires local access to the Docker daemon socket with AuthZ plugins enabled. - # - # Mitigation (active while suppression is in effect): - # - Monitor docker/docker releases: https://github.com/moby/moby/releases - # - Monitor moby/moby/v2 stabilization: https://github.com/moby/moby - # - Weekly CI security rebuild flags the moment a fixed version ships. - # - # Review: - # - Reviewed 2026-03-30 (initial suppression): no fix for docker/docker import path. Set 30-day review. - # - Next review: 2026-04-30. Remove suppression once a fix is available for the docker/docker import path. - # - # Removal Criteria: - # - docker/docker publishes a patched version OR moby/moby/v2 stabilizes and migration is feasible - # - Update dependency, rebuild, run security-scan-docker-image, confirm finding is resolved - # - Remove this entry, the GHSA-pxq6-2prw-chj9 entry, and the corresponding .trivyignore entries simultaneously - # - # References: - # - GHSA-x744-4wpc-v9h2: https://github.com/advisories/GHSA-x744-4wpc-v9h2 - # - CVE-2026-34040: https://nvd.nist.gov/vuln/detail/CVE-2026-34040 - # - CVE-2024-41110 (original): https://nvd.nist.gov/vuln/detail/CVE-2024-41110 - # - moby/moby releases: https://github.com/moby/moby/releases - - vulnerability: GHSA-x744-4wpc-v9h2 - package: - name: github.com/docker/docker - version: "v28.5.2+incompatible" - type: go-module - reason: | - HIGH — Docker AuthZ plugin bypass via oversized request body in docker/docker v28.5.2+incompatible. - Incomplete fix for CVE-2024-41110. Fixed in moby/moby v29.3.1 but no fix for docker/docker import path. - Charon uses Docker client SDK only (list containers); the vulnerability is server-side in the Docker - daemon's AuthZ plugin handler. Charon does not run a Docker daemon or use AuthZ plugins. - Risk accepted; no remediation path until docker/docker publishes a fix or moby/moby/v2 stabilizes. - Reviewed 2026-03-30: no patched release available for docker/docker import path. - expiry: "2026-04-30" # 30-day review: no fix for docker/docker import path. Extend in 30-day increments with documented justification. - - # Action items when this suppression expires: - # 1. Check docker/docker and moby/moby releases: https://github.com/moby/moby/releases - # 2. Check if moby/moby/v2 has stabilized: https://github.com/moby/moby - # 3. If a fix has shipped for docker/docker import path OR moby/moby/v2 is stable: - # a. Update the dependency and rebuild Docker image - # b. Run local security-scan-docker-image and confirm finding is resolved - # c. Remove this entry, GHSA-pxq6-2prw-chj9 entry, and all corresponding .trivyignore entries - # 4. If no fix yet: Extend expiry by 30 days and update the review comment above - # 5. If extended 3+ times: Open an issue to track moby/moby/v2 migration feasibility - # GHSA-pxq6-2prw-chj9 / CVE-2026-33997: Moby off-by-one error in plugin privilege validation # Severity: MEDIUM (CVSS 6.8) # Package: github.com/docker/docker v28.5.2+incompatible (go-module) @@ -559,9 +493,9 @@ ignore: # via crafted plugin configurations. # # Root Cause (No Fix Available for Import Path): - # - Same import path issue as GHSA-x744-4wpc-v9h2. The fix exists in moby/moby v29.3.1 but not + # - Same import path issue as CVE-2026-34040. The fix exists in moby/moby v29.3.1 but not # for the docker/docker import path that Charon uses. - # - Fix path: same as GHSA-x744-4wpc-v9h2 — wait for docker/docker patch or moby/moby/v2 stabilization. + # - Fix path: same dependency migration pattern as CVE-2026-34040 (if needed) or upstream fix. # # Risk Assessment: ACCEPTED (Not exploitable in Charon context) # - Charon uses the Docker client SDK only (list containers). The vulnerability is in Docker's @@ -577,9 +511,9 @@ ignore: # - Next review: 2026-04-30. Remove suppression once a fix is available for the docker/docker import path. # # Removal Criteria: - # - Same as GHSA-x744-4wpc-v9h2: docker/docker publishes a patched version OR moby/moby/v2 stabilizes + # - docker/docker publishes a patched version OR moby/moby/v2 stabilizes # - Update dependency, rebuild, run security-scan-docker-image, confirm finding is resolved - # - Remove this entry, GHSA-x744-4wpc-v9h2 entry, and all corresponding .trivyignore entries simultaneously + # - Remove this entry and all corresponding .trivyignore entries simultaneously # # References: # - GHSA-pxq6-2prw-chj9: https://github.com/advisories/GHSA-pxq6-2prw-chj9 @@ -605,7 +539,7 @@ ignore: # 3. If a fix has shipped for docker/docker import path OR moby/moby/v2 is stable: # a. Update the dependency and rebuild Docker image # b. Run local security-scan-docker-image and confirm finding is resolved - # c. Remove this entry, GHSA-x744-4wpc-v9h2 entry, and all corresponding .trivyignore entries + # c. Remove this entry and all corresponding .trivyignore entries # 4. If no fix yet: Extend expiry by 30 days and update the review comment above # 5. If extended 3+ times: Open an issue to track moby/moby/v2 migration feasibility diff --git a/.trivyignore b/.trivyignore index d5d1d9bd9..fdd90a138 100644 --- a/.trivyignore +++ b/.trivyignore @@ -87,23 +87,6 @@ GHSA-x6gf-mpr2-68h6 # exp: 2026-07-09 CVE-2026-32286 -# CVE-2026-34040 / GHSA-x744-4wpc-v9h2: Docker AuthZ plugin bypass via oversized request body -# Severity: HIGH (CVSS 8.8) — Package: github.com/docker/docker v28.5.2+incompatible -# Incomplete fix for CVE-2024-41110. Fixed in moby/moby v29.3.1 but no fix for docker/docker import path. -# Charon uses Docker client SDK only (list containers); the vulnerability is server-side in the Docker daemon. -# Review by: 2026-04-30 -# See also: .grype.yaml for full justification -# exp: 2026-04-30 -CVE-2026-34040 - -# GHSA-x744-4wpc-v9h2: Docker AuthZ plugin bypass via oversized request body (GHSA alias) -# Severity: HIGH (CVSS 8.8) — Package: github.com/docker/docker v28.5.2+incompatible -# GHSA alias for CVE-2026-34040. See CVE-2026-34040 entry above for full details. -# Review by: 2026-04-30 -# See also: .grype.yaml for full justification -# exp: 2026-04-30 -GHSA-x744-4wpc-v9h2 - # CVE-2026-33997 / GHSA-pxq6-2prw-chj9: Moby off-by-one error in plugin privilege validation # Severity: MEDIUM (CVSS 6.8) — Package: github.com/docker/docker v28.5.2+incompatible # Fixed in moby/moby v29.3.1 but no fix for docker/docker import path. diff --git a/.version b/.version index 759e855fb..0a8bf80d6 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -v0.21.0 +v0.27.0 diff --git a/.vscode/settings.json b/.vscode/settings.json index ebbec94af..179ff03c1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { + "docker.host": "unix:///run/user/1001/docker.sock", "gopls": { "buildFlags": ["-tags=integration"] }, diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 184de1f5d..9c43fbcef 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,21 +4,36 @@ { "label": "Docker Compose Up", "type": "shell", - "command": "docker compose -f /root/docker/containers/charon/docker-compose.yml up -d && echo 'Charon running at http://localhost:8787'", + "command": "docker compose -f /home/jeremy/docker/containers/charon/docker-compose.yml up -d && echo 'Charon running at http://localhost:8787'", + "options": { + "env": { + "DOCKER_HOST": "unix:///run/user/1001/docker.sock" + } + }, "group": "build", "problemMatcher": [] }, { "label": "Build & Run: Local Docker Image", "type": "shell", - "command": "docker build -t charon:local . && docker compose -f /root/docker/containers/charon/docker-compose.yml up -d && echo 'Charon running at http://localhost:8787'", + "command": "docker build -t charon:local . && docker compose -f /home/jeremy/docker/containers/charon/docker-compose.yml up -d && echo 'Charon running at http://localhost:8787'", + "options": { + "env": { + "DOCKER_HOST": "unix:///run/user/1001/docker.sock" + } + }, "group": "build", "problemMatcher": [] }, { "label": "Build & Run: Local Docker Image No-Cache", "type": "shell", - "command": "docker build --no-cache -t charon:local . && docker compose -f /root/docker/containers/charon/docker-compose.yml up -d && echo 'Charon running at http://localhost:8787'", + "command": "docker build --no-cache -t charon:local . && docker compose -f /home/jeremy/docker/containers/charon/docker-compose.yml up -d && echo 'Charon running at http://localhost:8787'", + "options": { + "env": { + "DOCKER_HOST": "unix:///run/user/1001/docker.sock" + } + }, "group": "build", "problemMatcher": [] }, diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 55d2aa54a..c28ffdf12 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -577,6 +577,7 @@ graph LR - Global threat intelligence (crowd-sourced IP reputation) - Automatic IP banning with configurable duration - Decision management API (view, create, delete bans) +- IP whitelist management: operators add/remove IPs and CIDRs via the management UI; entries are persisted in SQLite and regenerated into a `crowdsecurity/whitelists` parser YAML on every mutating operation and at startup **Modes:** diff --git a/CHANGELOG.md b/CHANGELOG.md index 7474d9e77..e6dba72e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Hecate Tunnel & Pathway Manager**: Connect remote servers behind NAT/firewalls without opening inbound ports, with pluggable connection types managed from the Remote Servers page (Issue #368) + - Connection types: Direct (host/port), Orthrus Agent (self-hosted), Cloudflare Tunnel, Tailscale, ZeroTier, NetBird + - Tunnel lifecycle management: create, start, stop, delete, rotate credentials + - Real-time tunnel log streaming to browser via WebSocket + - Orthrus agent provisioning with one-time auth key display and multi-method install wizard (Docker Compose, systemd, Tarball, Homebrew, Kubernetes) + - `TunnelStatusBadge` component on the Remote Servers page showing live connection state + - Orthrus agent binary published as `ghcr.io/wikid82/charon-orthrus-agent` (~2.4 MB, scratch-based) + - CI workflow for automated Orthrus agent image publishing + - E2E Playwright test coverage for tunnel manager and agent install wizard + +- **Accessibility Testing**: Integrated `@axe-core/playwright` for automated WCAG 2.0/2.1/2.2 AA accessibility testing across all application pages (Issue #929) + - Added 12 accessibility test specs: login, dashboard, proxy hosts, certificates, DNS providers, settings, security, uptime, tasks, domains, notifications, and setup pages + - Tests target WCAG 2.0 Level A/AA and WCAG 2.2 Level AA rule sets via axe-core + - Known violations baselined in `tests/a11y/a11y-baseline.ts` with per-entry expiry dates for tracked remediation + - Authenticated tests share a logged-in auth fixture; login page runs unauthenticated to test the pre-auth state + - Canvas elements excluded from scanning to prevent false positives from chart libraries (CrowdSec dashboard, Uptime monitors) + - Accessibility tests run in CI across Chromium, Firefox, and WebKit browsers via the non-security test shards + - **CrowdSec Dashboard**: Visual analytics for CrowdSec security data within the Security section - Summary cards showing total bans, active bans, unique IPs, and top scenario - Interactive charts: ban timeline (area), top attacking IPs (bar), scenario breakdown (pie) @@ -49,6 +67,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security +- **CVE-2026-34040**: Remediated high-severity vulnerability by migrating from `github.com/docker/docker` to `github.com/moby/moby/client v0.4.1` + - Affected component: Docker client SDK used for container management features + - Resolution: Updated `go.mod` to reference the actively maintained `moby/moby` module + - **Supply Chain**: Enhanced PR verification workflow stability and accuracy - **Vulnerability Reporting**: Eliminated false negatives ("0 vulnerabilities") by enforcing strict failure conditions - **Tooling**: Switched to manual Grype installation ensuring usage of latest stable binary @@ -68,6 +90,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **CI: Package Deduplication**: Removed 3 duplicate `devDependency` keys in `package.json` for `@typescript-eslint/eslint-plugin`, `@typescript-eslint/parser`, and related packages — duplicate keys caused the last value to silently overwrite earlier entries +- **Frontend: Fast-Refresh Violation**: Extracted `isInUse` and `isDeletable` helper functions from `CertificateList` into a new `certificateUtils` utility module to satisfy React Fast Refresh constraints (non-component exports must not share a file with components) +- **Accessibility: Form Labels**: Replaced 5 invalid `