From 5221f1c70e72bcba24dbecaa6a842669511a6d93 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:11:57 +0000 Subject: [PATCH 1/7] Harden governance evidence, secret scanning and frontend dependencies Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> Signed-off-by: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> --- .github/dependabot.yml | 19 +++ .github/workflows/ci.yml | 20 ++- .github/workflows/governance.yml | 37 ++++- .github/workflows/secret-scanner.yml | 9 +- 0-AI-MANIFEST.a2ml | 32 ++++ MetaManifold-WebUI_chora.deed | 2 +- SECURITY.md | 51 +++++-- .../2026-10-23-governance-security.md | 97 ++++++++++++ frontend/bun.lock | 140 +++++++++--------- frontend/package.json | 4 +- scripts/check-governance-contract.sh | 26 ++++ scripts/tests/test-ci-logging.sh | 35 +++++ scripts/tests/test-governance-contract.sh | 31 ++++ 13 files changed, 407 insertions(+), 96 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 0-AI-MANIFEST.a2ml create mode 100644 docs/release-notes/2026-10-23-governance-security.md create mode 100644 scripts/tests/test-ci-logging.sh create mode 100644 scripts/tests/test-governance-contract.sh diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..b8dc3611 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: MPL-2.0 +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + # Dependabot's npm ecosystem supports bun.lock. Julia and R pins remain + # maintainer-reviewed in tool_versions.yml, Manifest.toml and renv.lock. + - package-ecosystem: npm + directory: /frontend + schedule: + interval: weekly + open-pull-requests-limit: 5 + groups: + development-tooling: + dependency-type: development + update-types: [minor, patch] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3800093..c8855907 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -172,6 +172,8 @@ jobs: # silently. If a matrix is ever genuinely wanted here, the required context must move # to a matrix-free aggregator job first. name: Julia tests + # R/Julia setup and integration tests are expensive, but may not hang for 6h. + timeout-minutes: 90 needs: gate if: needs.gate.outputs.relevant == 'true' # Explicit, not ubuntu-latest: the R pin below names an apt package revision built @@ -383,6 +385,12 @@ jobs: bun install --frozen-lockfile --ignore-scripts bun run build + - name: Audit frontend dependencies + working-directory: frontend + # Moderate advisories remain visible in `bun audit`; upgrading Router + # across a major version needs a separate routing/SSR review. + run: bun audit --audit-level high + - name: Frontend unit tests working-directory: frontend run: bun run test @@ -413,7 +421,11 @@ jobs: run: | set +e julia --project=. -t2 --compiled-modules=no test/runtests.jl numeric_policy exact_summaries high_precision derived_analyses validated_statistics 2>&1 | tee /tmp/validated-statistics-tests.log - status="${PIPESTATUS[0]}" + # Preserve both exits immediately: a successful Julia run must not + # hide a failed log write (e.g. a full runner filesystem). + statuses=("${PIPESTATUS[@]}") + status="${statuses[0]}" + if [ "$status" -eq 0 ]; then status="${statuses[1]}"; fi if [ "$status" -ne 0 ]; then enc() { head -c 3000 | sed -e 's/%/%25/g' -e 's/\r/%0D/g' | awk '{printf "%s%%0A", $0}'; } # One annotation cannot hold a failing run, and a tail hides every @@ -450,7 +462,11 @@ jobs: command -v vsearch # The full suite must run the occupancy experiment, not skip it. set +e julia --project=. -t 2 --code-coverage=user --compiled-modules=no test/runtests.jl --integration --server 2>&1 | tee /tmp/julia-tests.log - status="${PIPESTATUS[0]}" + # Preserve both exits immediately: a successful Julia run must not + # hide a failed log write (e.g. a full runner filesystem). + statuses=("${PIPESTATUS[@]}") + status="${statuses[0]}" + if [ "$status" -eq 0 ]; then status="${statuses[1]}"; fi if [ "$status" -ne 0 ]; then enc() { head -c 3500 | sed -e 's/%/%25/g' -e 's/\r/%0D/g' | awk '{printf "%s%%0A", $0}'; } awk ' diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index 167c44c2..19862556 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -1,7 +1,7 @@ # SPDX-License-Identifier: MPL-2.0 # SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) # -# Fork governance lane. DEED syntax comes from hyperpolymath/standards; the +# Fork governance lane. DEED and K9 conformance come from hyperpolymath/standards; the # Hypatia verdict grid uses a fork-local copy of the standards reusable so its # advisory PR scan cannot become a blocking code-scanning-results check. Both # sources are pinned to commits. The grid covers Hypatia, panic-attack, pons @@ -38,7 +38,10 @@ jobs: with: persist-credentials: false - name: Check the advisory SARIF boundary - run: bash scripts/check-governance-contract.sh + run: | + bash scripts/check-governance-contract.sh + bash scripts/tests/test-governance-contract.sh + bash scripts/tests/test-ci-logging.sh deed: name: DEED syntax @@ -48,6 +51,18 @@ jobs: with: require-files: true + k9: + name: K9 conformance + uses: hyperpolymath/standards/.github/workflows/k9-conformance.yml@c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d + permissions: + contents: read + pull-requests: read + with: + # No K9 contracts are committed yet: report grey, never pretend that + # DEED syntax validation establishes K9 semantic conformance. + require-files: false + fail-on-indeterminate: true + hypatia: name: Hypatia verdict grid needs: [workflow-contract] @@ -64,7 +79,7 @@ jobs: verdict: name: Post the verdict grid - needs: [deed, hypatia] + needs: [deed, k9, hypatia] if: ${{ always() && github.event_name == 'pull_request' }} runs-on: ubuntu-latest timeout-minutes: 5 @@ -80,11 +95,20 @@ jobs: name: verdict-grid path: verdict-grid + - name: Download the K9 conformance grid + continue-on-error: true + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: k9-grid + path: k9-grid + - name: Update the compact PR verdict env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} DEED_RESULT: ${{ needs.deed.result }} + K9_RESULT: ${{ needs.k9.result }} + K9_VERDICT: ${{ needs.k9.outputs.verdict }} HYPATIA_RESULT: ${{ needs.hypatia.result }} HYPATIA_VERDICT: ${{ needs.hypatia.outputs.verdict }} run: | @@ -104,6 +128,7 @@ jobs: echo "| Lane | Status | Run |" echo "|---|---|---|" echo "| DEED syntax (repo deed) | $deed | [run $GITHUB_RUN_ID]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID) |" + echo "| K9 conformance (L0โ€“L2; L3 not verified) | ${K9_VERDICT:-orange} (job: $K9_RESULT) | same run |" echo "| Hypatia verdict grid | ${HYPATIA_VERDICT:-orange} (job: $HYPATIA_RESULT) | same run |" echo if [ -s verdict-grid/report.md ]; then @@ -111,6 +136,12 @@ jobs: else echo '๐ŸŸ  indeterminate ยท no verdict-grid artifact was produced by the Hypatia lane; see the run.' fi + echo + if [ -s k9-grid/report.md ]; then + cat k9-grid/report.md + else + echo '๐ŸŸ  indeterminate ยท no K9 grid artifact was produced; see the run.' + fi } > "$RUNNER_TEMP/body.md" # One comment per PR, updated in place; newest run wins. comment_id=$(gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \ diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index 5bf9eb3a..fa8a07f6 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -1,11 +1,12 @@ # SPDX-License-Identifier: MPL-2.0 -# Full-history and working-tree secret scanning from the pinned estate reusable. name: Secret Scanner on: push: branches: [main] pull_request: + schedule: + - cron: '31 5 * * 1' workflow_dispatch: concurrency: @@ -18,7 +19,9 @@ permissions: jobs: scan: name: Secret history and working tree + # Full-history + working-tree gitleaks and shell-secret checks. The + # reusable installs a checksum-verified binary; no action licence or + # repository secrets are passed to code from a pull request. + uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d permissions: contents: read - uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 - # Deliberately no secrets: the pinned reusable declares and needs none. diff --git a/0-AI-MANIFEST.a2ml b/0-AI-MANIFEST.a2ml new file mode 100644 index 00000000..10bfb6f9 --- /dev/null +++ b/0-AI-MANIFEST.a2ml @@ -0,0 +1,32 @@ +; SPDX-License-Identifier: CC-BY-SA-4.0 +; Media-Type: application/a2ml +(manifest + (identity + (name "MetaManifold-WebUI") + (repo "https://github.com/hyperpolymath/MetaManifold-WebUI") + (fork-of "https://github.com/JoshuaJewell/MetaManifold-WebUI")) + (authority + (contribution "CONTRIBUTING.md") + (licensing "LICENSE") + (security "SECURITY.md") + (deed "MetaManifold-WebUI_chora.deed")) + (architecture + (server "Julia with Oxygen.jl; src/server/") + (analysis "Julia and R through RCall; src/analysis/") + (storage "DuckDB; src/core/duckdb_store.jl") + (frontend "React 18 and TypeScript, managed with bun; frontend/") + (proofs "Agda; proofs/")) + (invariants + (rule "Preserve existing licence headers, LICENSE and CITATION.cff.") + (rule "Keep settled toolchains: estate language-ban heuristics do not authorise a rewrite.") + (rule "Keep independent Python reference oracles; never replace scientific results with placeholders.") + (rule "Use config/defaults/tool_versions.yml, Manifest.toml and renv.lock for pinned tools.") + (rule "Do not put secrets or private analysis data in Git or frontend code.") + (rule "Run tests; report inaccessible services and missing evidence honestly.")) + (evidence + (application-ci ".github/workflows/ci.yml") + (governance ".github/workflows/governance.yml") + (history-scan ".github/workflows/secret-scanner.yml") + (security-practices ".github/workflows/scorecard.yml") + (fleet-enrolment ".gitbot-fleet/enrolment.json") + (scope "DEED syntax is not K9 semantic conformance. No K9 contracts are committed; the K9 corpus is grey, not a pass."))) diff --git a/MetaManifold-WebUI_chora.deed b/MetaManifold-WebUI_chora.deed index c2ce91f1..2f6653eb 100644 --- a/MetaManifold-WebUI_chora.deed +++ b/MetaManifold-WebUI_chora.deed @@ -20,7 +20,7 @@ :enforcement ".github/workflows/") (status :evidence-source ".github/workflows/governance.yml" - :evidence-status "Fork governance lane only: DEED syntax and the Hypatia verdict grid with gitbot-fleet report-backs. Upstream owns the code and its CI; this deed describes the fork.") + :evidence-status "Fork governance lane only: DEED syntax, the K9 conformance grid (no contracts yet; grey corpus), and the Hypatia verdict grid with gitbot-fleet report-backs. Upstream owns the code and its CI; this deed describes the fork.") (policies (licensing :initial-assignment "New files the fork authors carry MPL-2.0 with an SPDX header, as upstream CONTRIBUTING.md permits." diff --git a/SECURITY.md b/SECURITY.md index f4f9e18d..22ba5bd2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,24 +1,43 @@ # Security policy -## Supported version +## Reporting a vulnerability -Security fixes are applied to the current `main` branch. Update to the latest -revision before reporting a problem that may already have been fixed. +For vulnerabilities in this fork, use [GitHub private vulnerability +reporting](https://github.com/hyperpolymath/MetaManifold-WebUI/security/advisories/new). +If private reporting is unavailable, contact the fork maintainer at +**j.d.a.jewell@open.ac.uk**. Do not open a public issue containing exploit +instructions, credentials, private sequence data, or patient identifiers. -## Report a vulnerability +Include the affected commit, deployment configuration, reproduction steps, +expected and observed behaviour, and likely impact. Use synthetic or +appropriately de-identified examples. Never send production credentials. -Use [GitHub private vulnerability reporting](https://github.com/hyperpolymath/MetaManifold-WebUI/security/advisories/new) -for suspected vulnerabilities in this fork. If private reporting is not -available, contact the fork maintainer at `j.d.a.jewell@open.ac.uk` (the -contact already published in `MetaManifold-WebUI_chora.deed`). Do not disclose -an unpatched vulnerability in a public issue or pull request. +For defects shared with the upstream project, coordinate disclosure with +[JoshuaJewell/MetaManifold-WebUI](https://github.com/JoshuaJewell/MetaManifold-WebUI) +as well; this fork cannot make support commitments on its behalf. -Include the affected revision, a minimal reproduction, the security impact, and -any suggested mitigation. Remove real study data, credentials, access tokens, -and other secrets from the report. The maintainers will assess the report and -coordinate a fix and disclosure with the reporter. +## Supported versions and deployment boundary -For vulnerabilities confined to upstream code that are not specific to this -fork, also consult the upstream project's reporting process rather than -assuming the fork can publish an upstream fix. +Security fixes target the current `main` branch of this fork. Older commits +and downstream deployments must be updated; no backport or response-time +commitment is implied. + +Treat the Julia server as a trusted local analysis service, not a public, +multi-tenant hosting boundary. Do not expose it directly to the internet. +Remote deployments need an authenticated, TLS-terminating reverse proxy and +appropriate access controls for the server's files, tools and analysis data. +Keep DOI publication disabled unless deliberately configured; Zenodo tokens +belong in server-side environment variables, never frontend code or Git. + +## Automated checks and their limits + +CI runs application tests and proof checks. The fork governance workflow +reports DEED syntax, K9 conformance coverage, and advisory Hypatia findings. +The Secret Scanner workflow checks the working tree and Git history; the +Scorecard workflow assesses repository security practices. Dependency update +PRs still require review and passing tests. + +A green workflow is not a claim that all security findings are resolved. +Missing tools, inaccessible GitHub alert APIs, absent fleet report-backs and +an empty K9 corpus must remain visible as missing or inapplicable evidence. diff --git a/docs/release-notes/2026-10-23-governance-security.md b/docs/release-notes/2026-10-23-governance-security.md new file mode 100644 index 00000000..5858d560 --- /dev/null +++ b/docs/release-notes/2026-10-23-governance-security.md @@ -0,0 +1,97 @@ + +# Governance and security follow-up to PR #227 + +## Regression review + +PR #227 is superseded by merged #234, not a candidate for a conflict merge. +Its four Codex findings were addressed in #229/#234: byte-buffer Newick +slicing, graft `edge_number` access, invalid Faith PD counts and the raw-count +UniFrac overload. #238 corrects PD rooting; #239/#240 preserve placement mass +and fix the independent library-size-normalised weighted UniFrac expectation. +The four old review threads have been answered and resolved, and #227 closed. +No other contributor's branch was rewritten. + +## Changes + +- Full-history and working-tree secret scanning via the standards reusable at + `c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d`, with only `contents: read` and + no inherited secrets. Findings fail the job; no local suppressions added. +- Pinned Scorecard publication on main and weekly/manual runs, not PR code. +- Dependabot for Actions and the bun-managed frontend; a private disclosure + policy and deployment boundary in `SECURITY.md`. +- A 90-minute limit on the expensive Julia/R test job. Both Julia pipelines + now preserve failures from the producer **and** the log writer; diagnostic + collection cannot turn a failed test into success. +- A separate pinned K9 conformance family and artifact in the PR verdict. + There are currently **zero K9 contracts**: its corpus is grey/inapplicable, + not a semantic pass inferred from the DEED syntax row. Indeterminate K9 + validation fails closed; L3 signature verification is not claimed. +- Editor settings and a repository-specific AI manifest that defers to + `CONTRIBUTING.md`, rather than importing incompatible estate toolchains. +- Frontend lockfile refreshed with bun 1.3.10 within existing major-version + ranges, with minimum Vite 6.4.5 and React Router DOM 6.30.6. This lockfile + change is intentional: it removes known vulnerable transitive versions, + including DOMPurify 3.4.16, PostCSS, nanoid, picomatch and Browserslist. + CI audits high/critical dependencies; no advisories are suppressed. + +## Local verification + +With bun **1.3.10**: + +- `bun install --frozen-lockfile --ignore-scripts`: pass. +- `bun run typecheck`, `bun test src`: pass, **72 tests**, 200 assertions. +- `bun run build`: pass (existing large Plotly bundle warning remains). +- `bun audit --audit-level high`: pass. Full JSON audit fell from 31 advisories + (1 critical, 13 high, 14 moderate, 3 low) to **2 moderate** React Router + advisories. This is not a zero-vulnerability claim. +- `bash scripts/check-governance-contract.sh`: 31 assertions. +- `bash scripts/tests/test-governance-contract.sh`: 8 weakening mutations + rejected (SARIF, K9, scanner privileges/failure policy, Scorecard PR trigger). +- `bash scripts/tests/test-ci-logging.sh`: success and each combination of + producer/logger failures return the correct status for both CI pipelines. +- Canonical DEED linter from the existing pinned standards commit: pass. +- YAML parses with unique keys; Sonar reliability lint and proof axiom audit + pass (7 modules, no new axioms). Axiom audit is not Agda typechecking. + +Julia, R, Agda and Nickel are not installed in the authoring sandbox; their +runtime/semantic checks are delegated to hosted CI, not reported as local +passes. GitHub log/artifact downloads are restricted by the sandbox's host +allowlist; job/check APIs and the published Hypatia grid remain readable. + +## Remaining findings / decisions + +The source report is Hypatia grid run **39982506244**, commit `d69d4f9`. +It is historical evidence, not a freshly executed scan of this change. + +- **React Router:** GHSA-wrjc-x8rr-h8h6 and GHSA-337j-9hxr-rhxg still affect + the v6 dependency according to the registry. The published fixed range is + v7.18+. Plan a major-version routing/hydration review rather than silently + migrating or suppressing them. The current UI uses a client-side router; + that does not justify declaring all advisory paths unreachable. +- **Estate applicability:** the fixed React/TypeScript architecture and the + independent Python scientific reference oracles conflict with estate + language-ban heuristics. Do not rewrite them to make a badge green. +- **Scanner discovery:** Hypatia is called by `governance.yml`, not the + heuristic's expected `hypatia-scan.yml`. CodeQL is already supplied by the + GitHub App (Actions, JS/TS and Python checks on #227); do not run a second + scan just to satisfy filename detection. No mirror target is configured; + adding a no-op `mirror.yml` would manufacture coverage. +- **HTTP identifiers:** W3C SVG/XLink namespace URIs in `figure/export.ts` + and `TreeView.tsx` are identifiers, not insecure network requests. Replacing + their `http` scheme breaks namespace identity. +- **CI diagnostics:** the pre-existing `|| true` statements collect failure + digests *after* capturing the test exit. The final `exit "$status"` still + fails the job; the logging regression tests guard this boundary. +- **Licensing:** four upstream source files lack SPDX identifiers. Existing + headers are protected by `CONTRIBUTING.md`; no licence text was changed. +- **Further review:** outbound-egress monitoring on secret-bearing jobs, + panic-attack's dynamic R evaluation/FFI/input-boundary heuristics and pons's + loop/unreachable heuristics still need scoped review, not blanket dismissal. +- **Fleet:** seven enrolled bots had no report-back. They remain `not_run`; + no synthetic reports or success records have been created. +- **Alert permissions:** the connected GitHub integration returns HTTP 403 for + Dependabot and code-scanning alert listing. No security alerts were dismissed + or marked fixed through that API. Maintainer-side access is needed to verify + reconciliation after the next default-branch scan. +- **CodeRabbit:** review capacity and Autofix billing notices require an account + decision; this change neither purchases credits nor activates billing. diff --git a/frontend/bun.lock b/frontend/bun.lock index 742f42a5..50824e5b 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -10,7 +10,7 @@ "plotly.js-dist-min": "^2.35.2", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.28.0", + "react-router-dom": "^6.30.6", "svg2pdf.js": "^2.8.1", }, "devDependencies": { @@ -19,7 +19,7 @@ "@types/react-dom": "^18.3.5", "@vitejs/plugin-react": "^4.3.4", "typescript": "^5.7.2", - "vite": "^6.1.0", + "vite": "^6.4.5", }, }, }, @@ -28,45 +28,45 @@ "@types/react-dom": "^18.3.5", }, "packages": { - "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], - "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], - "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + "@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], - "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], - "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], + "@babel/helpers": ["@babel/helpers@7.29.10", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.8" } }, "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg=="], - "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + "@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], - "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="], - "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="], - "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + "@babel/runtime": ["@babel/runtime@7.29.10", "", {}, "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA=="], - "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + "@babel/traverse": ["@babel/traverse@7.29.10", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.9", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ=="], - "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], @@ -126,63 +126,65 @@ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@remix-run/router": ["@remix-run/router@1.23.2", "", {}, "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w=="], + "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], + + "@remix-run/router": ["@remix-run/router@1.23.4", "", {}, "sha512-q7j5geK7xs3UJSdm9/iytUNclBnLmYx1EnSeCFXHPeutdqgIMeFeHtUZgS3EhlKxdBEAu8OwtJCwmLrEzpSs7Q=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.65.0", "", { "os": "android", "cpu": "arm" }, "sha512-t2AnVba25baFdq7gczzS13JyVcdubLVbFX1X04mxrggDrTQPUXNv3XM0k9H1hr5tqvg7AVvtQ720QHAj0iYqhg=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.65.0", "", { "os": "android", "cpu": "arm64" }, "sha512-dNONUaEwPGhl/nuO2KV8VABWlcjYyU6KtZGN8dQv5OS5JU7JSCxPijVNKXbRJd0iuTzTiJ4TjPt/4bbvDRNiMQ=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.65.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zWQqUH+9zavr2Bxuzfpuv2SIF3X31cr58TvSQC9o1GhpOvkqRwr60GkZdQcudyT/BEgdpdSafjgd2I13MbTL7g=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.65.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-RQYreBFWtO4//hIuOaZkYUnF/lPU4lz35dveDimDshWCJvVisVtJ/shmT9bkXK8wFJY+FJF89X6F3Cq1qI3LCw=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.65.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-A0NfMj6Cuh/9BE5k3awJKv6HoxFVXIcLAb6SZqAMoxUHPA1ZNNK4RsGuUPD/vL7nEAm9LG+ZzXWWTrnC0XtkpA=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.65.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BI6/H7q3aP0I7S7GOrjyxTjgEJlSnovXE38+bdxBpuKN6z+d//uF+T+VQ3Q7ltJL/71p17ufartARX9Ja5gOGQ=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.65.0", "", { "os": "linux", "cpu": "arm" }, "sha512-W1vUprf09RWaVWnsFP1vlUkEWJzTZlpObOEAYZXt9Fq0UVAOYEkeGr7wBX7McKqqIQskXdBnpiOMgwPujordgA=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.65.0", "", { "os": "linux", "cpu": "arm" }, "sha512-s8QTcZWgrl9haFzEGIqyE5BWyo89pFDGLMBFZ752WYxk8DJwNU/O1X6pZgPw829d9/sm6CCHyrMP3aurt76p1Q=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.65.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ke1f6dLRzkqveyjjF2fkEwQYZvxHtNNwjegY8nJrwcO0Bdqr8CzFO/pKE0Bns13cx0qz2Ba2Y0Hs3dSwMdnHxA=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.65.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-926SMeUcy4y18Rz/w2aJiUKOnyAnm6czg72bXkQEaeemsll+Wr0hnsjb2BGRpHvWgwAMYy5ACJCotbvfLCfj0A=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.65.0", "", { "os": "linux", "cpu": "none" }, "sha512-6T7JNRP1hc4hqF2dUsEoVLOt9K8tmODoCLldtQRYnrddGGQY8Gqe2pM/qMMUo7EMowpP7oa2N95k0/8sYK/ipw=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.65.0", "", { "os": "linux", "cpu": "none" }, "sha512-SImGFl1Je5kpyAgOCPONVT7h6ASV8mAUwGRjY7eZvVjPg//PfHOXgI+P241RF/+HqsA4PP3qD527ysWFKNQyhQ=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.65.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-XeDTbmPF1Yu5asfXGPrMkWoxTDhgryQbmw5Q1+R582O51VNCk23J6f2pDrsAp5YkneBVBwtExx6ZGOb9D2aQ6A=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.65.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-VS55JUI/1nsljmV/jfqNkLZXFw8uMO8nNlfoHmSevNf6fO3neuhyDvx9dv3c/Ytt8r/431SL8Qax1dpYvOMZTg=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.65.0", "", { "os": "linux", "cpu": "none" }, "sha512-Vblw84TbX7RS4la1Y4+yQ7aore/oSyaNtFbppuBr5Vs/4LfBeXKq9dIfoExw6zOvRERqd2rRAjnZRaMhH/0x5w=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.65.0", "", { "os": "linux", "cpu": "none" }, "sha512-7+gHjtONvKgwR9InTKFaLq53f6YzBJkgeMRBENdWTsQr+spahGqQm8HF5aXpq8i4JaRrOqYyv6Zn1WltDTxaZQ=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.65.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-KvNDC0J6s4vVHhtJ6KrDXU0Er0au/hmRIBq/+1maCV8TseGupeKizq0bWcrbp2HE1T8xoknvN0Q/KZ/YF0qX3w=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.65.0", "", { "os": "linux", "cpu": "x64" }, "sha512-GExdD15WPvu/x/drELjVam7vX8EsPgvTMOa/iJL/p/4m/oOqzbb7JXZg8fXR4hlXtg/sF2zyTZ3bR0AEtEMlhQ=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.65.0", "", { "os": "linux", "cpu": "x64" }, "sha512-8sT9PemIZfIMED4Vd4KGoOXVDj5CMSWNcGdbheRUx9TYS03yOWdT2Q0lXPyEyNBB9YTilwewIQKNIBk6Nxi0Kg=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.65.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-pO6Ehi0jo5yE0R6PltKhJhv9mlIiyzBJk5N5NUF2OF6xtVKI+1bdXxj/rb0jGXy3hck1ELVT0QPY3tzkAm360w=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.65.0", "", { "os": "none", "cpu": "arm64" }, "sha512-vo7N3/+cjjPDYr8vtxC+qSNgWa9sDvJ7g8N8R5SWqFY4WiNH1ULcAU70SKVyHeiIZOYYwNP9pTipmhN5sl1NPw=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.65.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ArwvgFAbjKcDM7qqotZ4OVYf+UMT9xH2YzkCzK4+AuZSDihMlz1Ekoe4INwBVXzn2mnJzuIYOcSx07iTBijGRQ=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.65.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-QGMmK4/cxKE/36FoLMSHQBCyymv/eFTnsCvvsBF0rgmOvVbvX78VWZRerL3dHa77f4pM6Xcc2iGDhJ10w/svKQ=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.65.0", "", { "os": "win32", "cpu": "x64" }, "sha512-XfKYFWHIPs4HPODIMsGF1IKlS3H1e3WokUZIgpmn4+uNnZBlnRDDHEylNyazbQXwhMKZre68OjtRYbKqb5+j8w=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.65.0", "", { "os": "win32", "cpu": "x64" }, "sha512-wPWi++K4Bs1VR4XihOhfGi4IGX6Cub7FlerTf8X2v+2BtDHddgSqWCXwGD+h2yAe4/niIhNDwAltioOC+wEogQ=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -192,19 +194,19 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/lz-string": ["@types/lz-string@1.5.0", "", { "dependencies": { "lz-string": "*" } }, "sha512-s84fKOrzqqNCAPljhVyC5TjAo6BH4jKHw9NRNFNiRUY5QSgZCmVm5XILlWbisiKl+0OcS7eWihmKGS5akc2iQw=="], "@types/pako": ["@types/pako@2.0.4", "", {}, "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw=="], - "@types/plotly.js": ["@types/plotly.js@2.35.14", "", {}, "sha512-CcD/32JcK19+xWH4FFpmYez/5X9kOjUcBr8Hxh7gQ/3Z32gIoLLy/L9xvC7DG5YikPvJjq6QN05B9+MCRu/Ncw=="], + "@types/plotly.js": ["@types/plotly.js@2.35.18", "", {}, "sha512-aLYnDpGSuWJ7/H/7NwiuG4AysrmgERWBLy2q4/5EhAKbEN//9vQRT7Nz3WjuJAzZhR28C6CQJYgBPM2HsNiWDA=="], "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], "@types/raf": ["@types/raf@3.4.3", "", {}, "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw=="], - "@types/react": ["@types/react@18.3.28", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw=="], + "@types/react": ["@types/react@18.3.31", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw=="], "@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="], @@ -218,17 +220,17 @@ "base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.31", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-b0k7wXlWjpuL/Uxb/pYlpjY0ej5BjAmurtPUk3eRGuloI4yjK32m8TiKUQ81l1ptcLjKS/KH2+uqIolZti1x4Q=="], - "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + "browserslist": ["browserslist@4.29.4", "", { "dependencies": { "baseline-browser-mapping": "^2.11.29", "caniuse-lite": "^1.0.30001816", "electron-to-chromium": "^1.5.459", "node-releases": "^2.0.59", "update-browserslist-db": "^1.3.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA=="], - "caniuse-lite": ["caniuse-lite@1.0.30001778", "", {}, "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg=="], + "caniuse-lite": ["caniuse-lite@1.0.30001819", "", {}, "sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg=="], "canvg": ["canvg@3.0.11", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@types/raf": "^3.4.0", "core-js": "^3.8.3", "raf": "^3.4.1", "regenerator-runtime": "^0.13.7", "rgbcolor": "^1.0.1", "stackblur-canvas": "^2.0.0", "svg-pathdata": "^6.0.3" } }, "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "core-js": ["core-js@3.49.0", "", {}, "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg=="], + "core-js": ["core-js@3.51.0", "", {}, "sha512-7bDKx/5jm5MxzsXxc93g6Z2+4ZsuT/MddrfRmTDJNAkWsYWHU8EVNEsBiIEWnMAMCHMmRfUEjkuo3jeNBOIHRw=="], "css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="], @@ -238,9 +240,9 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "dompurify": ["dompurify@3.4.16", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ=="], + "dompurify": ["dompurify@3.4.17", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w=="], - "electron-to-chromium": ["electron-to-chromium@1.5.313", "", {}, "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.463", "", {}, "sha512-nVWNU6ltFjopvOdD44J4zoBkgO0M+WZiFKKFV7EjebOH9z0KNphk6bCEjplXtJofL21ARIuuwWOCsFkBVklm6w=="], "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], @@ -278,9 +280,9 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "nanoid": ["nanoid@3.3.20", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ=="], - "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], + "node-releases": ["node-releases@2.0.60", "", {}, "sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ=="], "pako": ["pako@2.2.0", "", {}, "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w=="], @@ -288,11 +290,11 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], "plotly.js-dist-min": ["plotly.js-dist-min@2.35.3", "", {}, "sha512-sz2HLP8gkysLx/BanM2PtJTtZ1PLPwdHwMWNri2YxLBy3IOeuDsVQtlmWa4hoK3j/fi4naaD3uZJqH5ozM3zGg=="], - "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "postcss": ["postcss@8.5.31", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg=="], "raf": ["raf@3.4.1", "", { "dependencies": { "performance-now": "^2.1.0" } }, "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA=="], @@ -302,21 +304,21 @@ "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], - "react-router": ["react-router@6.30.3", "", { "dependencies": { "@remix-run/router": "1.23.2" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw=="], + "react-router": ["react-router@6.30.6", "", { "dependencies": { "@remix-run/router": "1.23.4" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-5HfK7k5im7LTOB0EqCQmfvy4C13G92Ssj1VTmouTK3AJvyjKTnFuCV0vcMAD/JS+JC4DvDIBRrlAeJIFjh5VWg=="], - "react-router-dom": ["react-router-dom@6.30.3", "", { "dependencies": { "@remix-run/router": "1.23.2", "react-router": "6.30.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag=="], + "react-router-dom": ["react-router-dom@6.30.6", "", { "dependencies": { "@remix-run/router": "1.23.4", "react-router": "6.30.6" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-0RHKZz7wwffvkU+2MFVT2NnjK44ssLEV+m0CAJaS2Ksmorrwj7WxH00jO0SOCW26/tINUnJHToXblDs33I38YQ=="], "regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="], "rgbcolor": ["rgbcolor@1.0.1", "", {}, "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw=="], - "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + "rollup": ["rollup@4.65.0", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.65.0", "@rollup/rollup-android-arm64": "4.65.0", "@rollup/rollup-darwin-arm64": "4.65.0", "@rollup/rollup-darwin-x64": "4.65.0", "@rollup/rollup-freebsd-arm64": "4.65.0", "@rollup/rollup-freebsd-x64": "4.65.0", "@rollup/rollup-linux-arm-gnueabihf": "4.65.0", "@rollup/rollup-linux-arm-musleabihf": "4.65.0", "@rollup/rollup-linux-arm64-gnu": "4.65.0", "@rollup/rollup-linux-arm64-musl": "4.65.0", "@rollup/rollup-linux-loong64-gnu": "4.65.0", "@rollup/rollup-linux-loong64-musl": "4.65.0", "@rollup/rollup-linux-ppc64-gnu": "4.65.0", "@rollup/rollup-linux-ppc64-musl": "4.65.0", "@rollup/rollup-linux-riscv64-gnu": "4.65.0", "@rollup/rollup-linux-riscv64-musl": "4.65.0", "@rollup/rollup-linux-s390x-gnu": "4.65.0", "@rollup/rollup-linux-x64-gnu": "4.65.0", "@rollup/rollup-linux-x64-musl": "4.65.0", "@rollup/rollup-openbsd-x64": "4.65.0", "@rollup/rollup-openharmony-arm64": "4.65.0", "@rollup/rollup-win32-arm64-msvc": "4.65.0", "@rollup/rollup-win32-ia32-msvc": "4.65.0", "@rollup/rollup-win32-x64-gnu": "4.65.0", "@rollup/rollup-win32-x64-msvc": "4.65.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-QVCvQUTGDN6JRwi3jVGCY3H4aTHvi9rX0LyoBMkGUvdh/Z6O51CzcLz85BWZaTCDGhl6ySGqYZs+xI1jmPqGcw=="], "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "source-map-js": ["source-map-js@1.2.2", "", {}, "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw=="], "specificity": ["specificity@0.4.1", "", { "bin": { "specificity": "./bin/specificity" } }, "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg=="], @@ -330,15 +332,15 @@ "text-segmentation": ["text-segmentation@1.0.3", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw=="], - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "update-browserslist-db": ["update-browserslist-db@1.3.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A=="], "utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="], - "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], + "vite": ["vite@6.4.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4enwjFfi4IwtEh0w+jPzpLVOFQiPHmmzPxcHXQOyEur47o6u0jE573DmGpjA+7irXoMLYRZD5ZaFUEneS3abPg=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], } diff --git a/frontend/package.json b/frontend/package.json index 8793ef05..5f0a3f24 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,7 @@ "plotly.js-dist-min": "^2.35.2", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.28.0", + "react-router-dom": "^6.30.6", "svg2pdf.js": "^2.8.1" }, "devDependencies": { @@ -25,7 +25,7 @@ "@types/react-dom": "^18.3.5", "@vitejs/plugin-react": "^4.3.4", "typescript": "^5.7.2", - "vite": "^6.1.0" + "vite": "^6.4.5" }, "overrides": { "@types/react": "$@types/react", diff --git a/scripts/check-governance-contract.sh b/scripts/check-governance-contract.sh index b481f095..f12442de 100755 --- a/scripts/check-governance-contract.sh +++ b/scripts/check-governance-contract.sh @@ -141,6 +141,32 @@ else fail "expected one SARIF upload site, found $upload_count" fi +# These are independent conformance families. An empty K9 corpus must be +# shown explicitly; missing semantic evidence must not become a green DEED row. +contains "$caller" 'uses: hyperpolymath/standards/.github/workflows/k9-conformance.yml@c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d' \ + 'K9 validation uses an immutable standards source' +contains "$caller" 'fail-on-indeterminate: true' \ + 'missing K9 semantic evidence fails closed' +contains "$caller" 'needs: [deed, k9, hypatia]' \ + 'the PR verdict waits for all three independent lanes' +contains "$caller" 'name: k9-grid' 'the K9 artifact remains visible in the PR verdict' +contains "$caller" '${K9_VERDICT:-orange}' 'an absent K9 verdict is indeterminate' + +secret_workflow="$(cat .github/workflows/secret-scanner.yml)" +contains "$secret_workflow" 'uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d' \ + 'full-history secret scanning uses the audited immutable reusable' +not_contains "$secret_workflow" 'secrets:' 'the secret scanner receives no caller secrets' +not_contains "$secret_workflow" 'continue-on-error:' 'secret scanner failures cannot be advisory' +not_contains "$secret_workflow" 'pull_request_target:' 'secret scanning does not use a privileged PR trigger' +scorecard_workflow="$(cat .github/workflows/scorecard.yml)" +contains "$scorecard_workflow" 'uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d' \ + 'Scorecard uses the audited immutable reusable' +if grep -Eq '^ pull_request(_target)?:' <<<"$scorecard_workflow"; then + fail 'Scorecard publication must not run against PR code' +else + pass 'Scorecard OIDC publication is not enabled for PRs' +fi + if [ "$failures" -gt 0 ]; then printf 'governance contract: %d failure(s)\n' "$failures" >&2 exit 1 diff --git a/scripts/tests/test-ci-logging.sh b/scripts/tests/test-ci-logging.sh new file mode 100644 index 00000000..e7b19a10 --- /dev/null +++ b/scripts/tests/test-ci-logging.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Test the actual workflow exit-selection code without needing Julia or R. +set -euo pipefail +ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" +workflow="$ROOT/.github/workflows/ci.yml" +count=$(grep -Fc 'statuses=("${PIPESTATUS[@]}")' "$workflow") +[ "$count" -eq 2 ] || { echo 'expected both Julia pipelines to preserve all exits' >&2; exit 1; } +selection=$(awk ' + /statuses=\("\$\{PIPESTATUS\[@\]\}"\)/ { capture = 1 } + capture { sub(/^ /, ""); print } + capture && /then status=/ { exit } +' "$workflow") +# Both pipelines must use the same selection logic. +second=$(awk ' + /statuses=\("\$\{PIPESTATUS\[@\]\}"\)/ { n++; capture = (n == 2) } + capture { sub(/^ /, ""); print } + capture && /then status=/ { exit } +' "$workflow") +[ "$selection" = "$second" ] || { echo 'Julia exit-selection blocks differ' >&2; exit 1; } +for spec in '0 0 0' '42 0 42' '0 23 23' '42 23 42'; do + read -r producer logger expected <<< "$spec" + rc=0 + bash -c ' + set +e + (exit "$1") | (cat >/dev/null; exit "$2") + '"$selection"' + exit "$status" + ' test-ci-logging "$producer" "$logger" || rc=$? + [ "$rc" -eq "$expected" ] || { + echo "producer=$producer logger=$logger returned $rc, expected $expected" >&2 + exit 1 + } + echo "ok - producer=$producer logger=$logger returns $expected" +done diff --git a/scripts/tests/test-governance-contract.sh b/scripts/tests/test-governance-contract.sh new file mode 100644 index 00000000..894fb949 --- /dev/null +++ b/scripts/tests/test-governance-contract.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Mutation tests: the contract must reject weakened security boundaries. +set -euo pipefail +ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" +fixture=$(mktemp -d) +trap 'rm -rf "$fixture"' EXIT +mkdir -p "$fixture/scripts" "$fixture/.github/workflows" +cp "$ROOT/scripts/check-governance-contract.sh" "$fixture/scripts/" +reset_fixture() { cp "$ROOT"/.github/workflows/*.yml "$fixture/.github/workflows/"; } +reset_fixture +bash "$fixture/scripts/check-governance-contract.sh" > "$fixture/log" 2>&1 +for mutation in sarif k9-pin k9-indeterminate k9-verdict scanner-advisory scanner-secrets scanner-trigger scorecard-trigger; do + reset_fixture + case "$mutation" in + sarif) sed -i "/if: github.event_name != 'pull_request'/d" "$fixture/.github/workflows/hypatia-scan-reusable.yml" ;; + k9-pin) sed -i '/uses: .*k9-conformance.yml@/s/@.*/@main/' "$fixture/.github/workflows/governance.yml" ;; + k9-indeterminate) sed -i 's/fail-on-indeterminate: true/fail-on-indeterminate: false/' "$fixture/.github/workflows/governance.yml" ;; + k9-verdict) sed -i 's/${K9_VERDICT:-orange}/${K9_VERDICT:-green}/' "$fixture/.github/workflows/governance.yml" ;; + scanner-advisory) echo ' continue-on-error: true' >> "$fixture/.github/workflows/secret-scanner.yml" ;; + scanner-secrets) echo ' secrets: inherit' >> "$fixture/.github/workflows/secret-scanner.yml" ;; + scanner-trigger) sed -i 's/^ pull_request:/ pull_request_target:/' "$fixture/.github/workflows/secret-scanner.yml" ;; + scorecard-trigger) sed -i '/^on:/a\ pull_request:' "$fixture/.github/workflows/scorecard.yml" ;; + esac + if bash "$fixture/scripts/check-governance-contract.sh" > "$fixture/log" 2>&1; then + echo "ERROR: governance contract accepted mutation $mutation" >&2 + cat "$fixture/log" >&2 + exit 1 + fi + echo "ok - rejected $mutation" +done From 3b1a5fa30607c38b5281a0f25581fb2eee2ea00a Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:19:12 +0000 Subject: [PATCH 2/7] Fix empty-corpus K9 reporting and upgrade legacy secret detector Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> Signed-off-by: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> --- .github/workflows/governance.yml | 11 +- .github/workflows/k9-conformance-reusable.yml | 352 ++++++++ .github/workflows/secret-scanner-reusable.yml | 828 ++++++++++++++++++ .github/workflows/secret-scanner.yml | 2 +- .../2026-10-23-governance-security.md | 39 +- scripts/check-governance-contract.sh | 24 +- scripts/tests/test-governance-contract.sh | 2 +- scripts/tests/test-k9-log-copy.sh | 26 + scripts/tests/test-secret-scanner.sh | 21 + 9 files changed, 1295 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/k9-conformance-reusable.yml create mode 100644 .github/workflows/secret-scanner-reusable.yml create mode 100644 scripts/tests/test-k9-log-copy.sh create mode 100644 scripts/tests/test-secret-scanner.sh diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index 19862556..f7415d31 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -42,6 +42,7 @@ jobs: bash scripts/check-governance-contract.sh bash scripts/tests/test-governance-contract.sh bash scripts/tests/test-ci-logging.sh + bash scripts/tests/test-k9-log-copy.sh deed: name: DEED syntax @@ -53,7 +54,7 @@ jobs: k9: name: K9 conformance - uses: hyperpolymath/standards/.github/workflows/k9-conformance.yml@c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d + uses: ./.github/workflows/k9-conformance-reusable.yml permissions: contents: read pull-requests: read @@ -121,6 +122,12 @@ jobs: failure) deed='๐Ÿ”ด failing' ;; *) deed="๐ŸŸ  indeterminate ($DEED_RESULT)" ;; esac + # A rendered grid is not proof that its job completed successfully. + case "$K9_RESULT" in + success) k9="${K9_VERDICT:-orange}" ;; + failure) k9=red ;; + *) k9=orange ;; + esac { echo '' echo '# Fork governance' @@ -128,7 +135,7 @@ jobs: echo "| Lane | Status | Run |" echo "|---|---|---|" echo "| DEED syntax (repo deed) | $deed | [run $GITHUB_RUN_ID]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID) |" - echo "| K9 conformance (L0โ€“L2; L3 not verified) | ${K9_VERDICT:-orange} (job: $K9_RESULT) | same run |" + echo "| K9 conformance (L0โ€“L2; L3 not verified) | $k9 (job: $K9_RESULT) | same run |" echo "| Hypatia verdict grid | ${HYPATIA_VERDICT:-orange} (job: $HYPATIA_RESULT) | same run |" echo if [ -s verdict-grid/report.md ]; then diff --git a/.github/workflows/k9-conformance-reusable.yml b/.github/workflows/k9-conformance-reusable.yml new file mode 100644 index 00000000..12e3806c --- /dev/null +++ b/.github/workflows/k9-conformance-reusable.yml @@ -0,0 +1,352 @@ +# Fork-local copy of standards k9-conformance.yml at +# c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d. Deltas: workflow_call only; +# standards checkout/evidence use that immutable source rather than this +# caller's workflow SHA; optional corpus-log copying does not fail an empty +# corpus. Validator, renderer, semantic gate and ratchet are unchanged. +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +name: k9-conformance + +# Conformance lane for K9 contracts (standards#1058, ruling D173). The +# canonical validator is 1-formats/k9/tools/k9-validate.sh; the grid it +# reports through is scripts/k9-grid.sh (docs/VERDICT-GRID-SPEC.adoc, +# conformance family). docs/CONTRACTILE-DESCRIPTILE-VALIDATION.adoc says +# where this lane sits beside the DEED lane and the Hypatia lane. +# +# In this repo (push / pull_request) it checks the validator's self-test, +# the contract's own typecheck, and every committed K9 file. +# +# As a reusable (workflow_call) it validates the CALLER's committed K9 files +# with the validator, contract and renderer at THIS workflow's own commit +# (job.workflow_sha), so a caller pinned to a standards SHA gets exactly +# that validator. Layers L0โ€“L2 run; L3 (signature) is outside this lane and +# the grid says so. The caller's shrink-only debt ledger +# (.machine_readable/k9-contract-debt.txt) is honoured as +# 1-formats/k9/tools/CONFORMANCE-REPORTING.adoc states it, and compared with +# the PR base: an entry the PR adds exempts nothing unless a commit message or +# the PR body carries `Ratchet-exception: .machine_readable/k9-contract-debt.txt +# โ€” ` (the governance exemption ratchet's form; it watches this ledger +# too). The checkout is full so the changed-file list is the merge-base diff +# and the commit messages are readable; on a push the range is the push's own +# (`before`..`sha`), so a direct push to a ledgered contract, or a direct +# ledger addition, is ratcheted exactly as a pull request is. +# +# Outputs: the `k9-grid` artifact (report.json and badge.json as I-JSON/JCS, +# report.md, the validator logs) and the `verdict` output. The caller posts +# report.md to its pull request and publishes badge.json itself: a reusable +# cannot ask for more permissions than it is given, and this one asks for +# `contents: read` plus `pull-requests: read` (to read the grid comment it +# posted last time, for the "since previous revision" column). +# +# Caller: +# jobs: +# k9: +# uses: hyperpolymath/standards/.github/workflows/k9-conformance.yml@ +# permissions: +# contents: read +# pull-requests: read +# with: +# require-files: true # fail when the caller has no K9 file at all +# fail-on-indeterminate: true # orange fails the job too (default: only red fails) +# grid-author: github-actions[bot] # the account whose grid comment is the delta baseline + +on: + workflow_call: + inputs: + require-files: + description: "Fail when the caller has no committed K9 file (a zero-file run is otherwise grey, not red)." + type: boolean + required: false + default: false + fail-on-indeterminate: + description: "Fail the job on an orange grid as well as a red one." + type: boolean + required: false + default: false + grid-author: + description: "The account that posts the grid comment in the caller; only its comment is read as the previous revision's baseline, so a contributor's comment cannot pose as one." + type: string + required: false + default: github-actions[bot] + outputs: + verdict: + description: "Grid state: red, orange, yellow, green or grey." + value: ${{ jobs.validate.outputs.verdict }} + +permissions: {} + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + pull-requests: read + outputs: + verdict: ${{ steps.grid.outputs.state }} + env: + # In this repo the PR's own validator is in the first checkout; a caller + # gets validator, contract and renderer from the canon checkout at + # job.workflow_sha. + CANON: ${{ github.repository == 'hyperpolymath/standards' && '.' || '.k9-canon' }} + NICKEL_VERSION: '1.18.0' + NICKEL_SHA256: '9cba4dd65ae9915ec61f73033aafcff307a377665a83fd8f530df086763318cb' + IJSON_JCS_REV: '0fdeea716fd558e9807362acf6f8202d56d8d88f' + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + # Full history: the changed-file list is the merge-base diff and the + # ledger ratchet reads the commit messages, on a pull request (base + # ..head) and on a push (the push's before..sha) alike. + fetch-depth: 0 + persist-credentials: false + + - name: checkout the K9 validator, contract and renderer at this workflow's commit + if: github.repository != 'hyperpolymath/standards' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: hyperpolymath/standards + ref: c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d + path: .k9-canon + persist-credentials: false + sparse-checkout: "1-formats/k9\nscripts/k9-grid.sh\nscripts/lib/k9-grid.jq\n" + sparse-checkout-cone-mode: false + + - name: Record which standards commit supplies validator, contract and renderer + env: + # The job context is not available to job-level env, so the canon + # ref is recorded here. A caller gets this workflow's own commit. + WORKFLOW_SHA: c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + if [ "$GITHUB_REPOSITORY" = "hyperpolymath/standards" ]; then + echo "CANON_REF=$HEAD_SHA" >> "$GITHUB_ENV" + else + echo "CANON_REF=$WORKFLOW_SHA" >> "$GITHUB_ENV" + fi + + - name: Install Nickel (pinned, sha256-verified; one version estate-wide) + run: | + set -euo pipefail + URL="https://github.com/tweag/nickel/releases/download/${NICKEL_VERSION}/nickel-x86_64-linux" + curl -sSfL --proto '=https' --proto-redir '=https' --tlsv1.2 --retry 3 -o "$RUNNER_TEMP/nickel" "$URL" + echo "${NICKEL_SHA256} ${RUNNER_TEMP}/nickel" | sha256sum -c - + chmod +x "$RUNNER_TEMP/nickel" + echo "$RUNNER_TEMP" >> "$GITHUB_PATH" + + - name: Cache the ijson-jcs canonicaliser + id: jcs-cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cargo/bin/ijson-jcs + key: ijson-jcs-${{ env.IJSON_JCS_REV }}-${{ runner.os }} + + - name: Install ijson-jcs (3-practice/JSON-POLICY.adoc, D306) + if: steps.jcs-cache.outputs.cache-hit != 'true' + run: | + set -euo pipefail + # Best effort: the renderer falls back to jq's sorted compact form, + # byte-identical for its ASCII integer-only documents, and the grid + # records which serialiser ran. + cargo install --quiet --git https://github.com/hyperpolymath/ijson-jcs --rev "$IJSON_JCS_REV" --locked ijson-jcs \ + || echo "::warning::ijson-jcs did not install; report.json falls back to jq -S -c" + + - name: Renderer regression tests + if: github.repository == 'hyperpolymath/standards' + run: bash scripts/tests/k9-grid-test.sh + + - name: Validator self-test + run: | + set +e + bash "$CANON/1-formats/k9/tools/k9-validate.sh" --self-test > "$RUNNER_TEMP/selftest.log" 2>&1 + echo "SELFTEST_CODE=$?" >> "$GITHUB_ENV" + tail -n 5 "$RUNNER_TEMP/selftest.log" + + - name: Contract typecheck + run: | + set +e + nickel typecheck "$CANON/1-formats/k9/spec/contract/k9_contract.ncl" > "$RUNNER_TEMP/typecheck.log" 2>&1 + echo "TYPECHECK_CODE=$?" >> "$GITHUB_ENV" + cat "$RUNNER_TEMP/typecheck.log" + + - name: Enumerate the committed K9 files, the changed files and ledger growth + env: + # A pull request compares with its base; a push with the commit the + # push replaced (all zeros when the ref is new: nothing to compare). + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + LEDGER: .machine_readable/k9-contract-debt.txt + run: | + set -euo pipefail + # Tracked files only: an untracked file is not a committed contract. + # The two extensions K9-CONTRACT-SPEC.adoc reserves, nothing else. + # A caller with no K9 file at all is a legitimate (grey) corpus, so + # grep's no-match exit is not an error here. + git ls-files -z -- '*.k9.ncl' '*.k9' \ + | tr '\0' '\n' \ + | { grep -v -E '^(\.k9-canon/|9-archive/|1-formats/k9/tools/fixtures/)' || [ $? -eq 1 ]; } \ + | sort > "$RUNNER_TEMP/expected" + echo "$(wc -l < "$RUNNER_TEMP/expected") committed K9 file(s)" + : > "$RUNNER_TEMP/changed" + : > "$RUNNER_TEMP/ledger-growth" + echo "BASE_UNRESOLVED=" >> "$GITHUB_ENV" + case "$BASE_SHA" in *[!0]*) ;; *) BASE_SHA="" ;; esac + [ -n "$BASE_SHA" ] || exit 0 + + # The PR's own changes are those since its merge base, not since the + # base branch tip: a file the base branch changed after the PR + # branched is not this PR's edit. A push compares with `before` + # itself, never a merge base: after a force push the fork point is + # not the state the push replaced, and a ledger entry the old tip + # had removed would come back unseen. The full checkout already + # holds the base commit; the fetch + # is a fallback only, and runs without credentials + # (persist-credentials is off), so it serves public repositories. + if { git cat-file -e "$BASE_SHA^{commit}" 2>/dev/null || git fetch --quiet origin "$BASE_SHA" 2>/dev/null; } \ + && merge_base=$(if [ "$GITHUB_EVENT_NAME" = pull_request ]; then git merge-base "$BASE_SHA" "$HEAD_SHA"; else git rev-parse --verify "$BASE_SHA^{commit}"; fi 2>/dev/null) \ + && git diff --name-only "$merge_base" "$HEAD_SHA" > "$RUNNER_TEMP/changed" 2>/dev/null; then + echo "$(wc -l < "$RUNNER_TEMP/changed") file(s) changed since the merge base $merge_base" + else + echo "::warning::could not resolve the PR base/head range; debt exemptions are reported as indeterminate" + echo "BASE_UNRESOLVED=--base-unresolved" >> "$GITHUB_ENV" + exit 0 + fi + + # The ledger is shrink-only. Entries present at HEAD and absent at + # the merge base are growth; growth exempts nothing unless a commit + # message in the PR or the PR body declares it in the exemption + # ratchet's form, naming this ledger: + # Ratchet-exception: .machine_readable/k9-contract-debt.txt โ€” + # Ratchet-exception(.machine_readable/k9-contract-debt.txt): + # Read ledger paths from stdin and emit sorted unique entries, + # excluding comments and blank lines; an empty ledger succeeds. + entries() { { grep -vE '^[[:space:]]*(#|$)' || [ $? -eq 1 ]; } | sort -u; } + head_entries=$RUNNER_TEMP/ledger-head + base_entries=$RUNNER_TEMP/ledger-base + if [ -f "$LEDGER" ]; then entries < "$LEDGER" > "$head_entries"; else : > "$head_entries"; fi + { git show "$merge_base:$LEDGER" 2>/dev/null || true; } | entries > "$base_entries" + comm -13 "$base_entries" "$head_entries" > "$RUNNER_TEMP/ledger-growth" + [ -s "$RUNNER_TEMP/ledger-growth" ] || exit 0 + declared=no + if git log --format=%B "$merge_base..$HEAD_SHA" | grep -iE '^Ratchet-exception' | grep -qF "$LEDGER"; then + declared=yes + elif [ -f "${GITHUB_EVENT_PATH:-}" ] \ + && jq -r '.pull_request.body // ""' "$GITHUB_EVENT_PATH" | grep -iE '^Ratchet-exception' | grep -qF "$LEDGER"; then + declared=yes + fi + n=$(wc -l < "$RUNNER_TEMP/ledger-growth") + if [ "$declared" = yes ]; then + echo "::notice::$LEDGER grew by $n entr(y/ies) under a declared Ratchet-exception" + : > "$RUNNER_TEMP/ledger-growth" + else + echo "::error::$LEDGER grew by $n entr(y/ies) with no Ratchet-exception naming it; the new entries exempt nothing" + sed 's/^/ + /' "$RUNNER_TEMP/ledger-growth" + fi + + - name: Validate every committed K9 file (L0โ€“L2, strict) + run: | + set +e + : > "$RUNNER_TEMP/results.jsonl" + worst=0 + while IFS= read -r f; do + [ -n "$f" ] || continue + # `--` ends the options: a tracked file whose name starts with `-` + # is a path, not a flag. + bash "$CANON/1-formats/k9/tools/k9-validate.sh" --strict --layer L2 --json --quiet -- "$f" >> "$RUNNER_TEMP/results.jsonl" 2>> "$RUNNER_TEMP/corpus.log" + rc=$? + [ "$rc" -gt "$worst" ] && worst=$rc + done < "$RUNNER_TEMP/expected" + echo "CORPUS_CODE=$worst" >> "$GITHUB_ENV" + echo "validator worst exit status: $worst" + + - name: Read the grid comment from the previous revision (delta column) + if: github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ github.token }} + COMMENTS_URL: ${{ github.event.pull_request.comments_url }} + MARKER: '' + GRID_AUTHOR: ${{ inputs.grid-author || 'github-actions[bot]' }} + run: | + set -euo pipefail + # Best effort: the caller's posting account writes the grid under + # this marker and regenerates it in place, so its last comment + # carrying the marker is the previous revision's grid and baseline. + # Only that account's comments count: anyone who can comment could + # otherwise plant a marker and a forged baseline. Any failure here + # only costs the "since previous revision" column. + : > "$RUNNER_TEMP/previous.md" + page=1 + while [ "$page" -le 10 ]; do + if ! curl -sSf --retry 2 -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \ + "$COMMENTS_URL?per_page=100&page=$page" > "$RUNNER_TEMP/comments.json"; then + echo "::notice::could not read the pull request comments; the delta column reports no comparable baseline" + break + fi + jq -r --arg m "$MARKER" --arg a "$GRID_AUTHOR" \ + '[.[] | select((.user.login // "") == $a and ((.body // "") | contains($m)))] | last | .body // empty' \ + "$RUNNER_TEMP/comments.json" > "$RUNNER_TEMP/previous-page.md" + [ -s "$RUNNER_TEMP/previous-page.md" ] && mv "$RUNNER_TEMP/previous-page.md" "$RUNNER_TEMP/previous.md" + [ "$(jq length "$RUNNER_TEMP/comments.json")" -eq 100 ] || break + page=$((page + 1)) + done + [ -s "$RUNNER_TEMP/previous.md" ] && echo "previous grid found" || echo "no previous grid" + + - name: Render the verdict grid + id: grid + env: + REQUIRE_FILES: ${{ inputs.require-files || false }} + PR_NUMBER: ${{ github.event.pull_request.number || 'local' }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + set -euo pipefail + ledger=.machine_readable/k9-contract-debt.txt + args=(--out "$RUNNER_TEMP/k9-grid" --results "$RUNNER_TEMP/results.jsonl" --expected "$RUNNER_TEMP/expected" + --changed "$RUNNER_TEMP/changed" --selftest "$RUNNER_TEMP/selftest.log" --selftest-code "$SELFTEST_CODE" + --typecheck "$RUNNER_TEMP/typecheck.log" --typecheck-code "$TYPECHECK_CODE" --corpus-code "$CORPUS_CODE" + --sha "$HEAD_SHA" --run "$GITHUB_RUN_ID" --url "$RUN_URL" --pr "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" + --canon-ref "$CANON_REF") + [ -f "$ledger" ] && args+=(--ledger "$ledger") + [ -s "$RUNNER_TEMP/ledger-growth" ] && args+=(--ledger-growth "$RUNNER_TEMP/ledger-growth") + [ -s "$RUNNER_TEMP/previous.md" ] && args+=(--previous "$RUNNER_TEMP/previous.md") + [ -n "${BASE_UNRESOLVED:-}" ] && args+=("$BASE_UNRESOLVED") + # With require-files the empty corpus is a failing row in the grid + # itself, so report.md and badge.json carry the red the job reports. + [ "$REQUIRE_FILES" = "true" ] && args+=(--require-files) + bash "$CANON/scripts/k9-grid.sh" render "${args[@]}" + cp "$RUNNER_TEMP/selftest.log" "$RUNNER_TEMP/typecheck.log" "$RUNNER_TEMP/results.jsonl" "$RUNNER_TEMP/k9-grid/" + # The corpus log is optional for an empty corpus. A terminal + # `[ -f ] && cp` returns 1 when absent, failing a valid grey grid. + if [ -f "$RUNNER_TEMP/corpus.log" ]; then + cp "$RUNNER_TEMP/corpus.log" "$RUNNER_TEMP/k9-grid/" + fi + + - name: Archive the grid and its evidence + if: ${{ always() && steps.grid.outcome != 'skipped' }} + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: k9-grid + path: ${{ runner.temp }}/k9-grid + if-no-files-found: error + retention-days: 90 + + - name: Gate + env: + STATE: ${{ steps.grid.outputs.state }} + FAIL_ON_INDETERMINATE: ${{ inputs.fail-on-indeterminate || false }} + run: | + set -euo pipefail + echo "K9 verdict grid: $STATE" + case "$STATE" in + red) echo "::error::K9 conformance failed; see the k9-grid artifact and the step summary"; exit 1 ;; + orange) + if [ "$FAIL_ON_INDETERMINATE" = "true" ]; then + echo "::error::K9 conformance is indeterminate and fail-on-indeterminate is set"; exit 1 + fi + echo "::warning::K9 conformance is indeterminate: evidence is missing, not clean" ;; + yellow) echo "::notice::K9 conformance passes with ledgered debt or warnings" ;; + green|grey) ;; + *) echo "::error::no verdict was produced"; exit 1 ;; + esac diff --git a/.github/workflows/secret-scanner-reusable.yml b/.github/workflows/secret-scanner-reusable.yml new file mode 100644 index 00000000..9191f10f --- /dev/null +++ b/.github/workflows/secret-scanner-reusable.yml @@ -0,0 +1,828 @@ +# Fork-local copy of standards secret-scanner-reusable.yml at +# c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d. Deltas: Gitleaks 8.30.1 +# (upstream release-asset SHA256) instead of 8.18.4, and standards config +# checkouts pinned explicitly rather than using this caller's workflow SHA. +# A synthetic-credential sensitivity check is added. No local rule +# suppressions, history exclusions or advisory failure overrides are added. +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. +# secret-scanner-reusable.yml โ€” Reusable secret-scanner bundle. +# +# Consolidates the per-repo `secret-scanner.yml` workflow (estate-wide: +# 281 deployments, 54 unique blob SHAs, 19% structural drift โ€” top 4 +# SHAs cover 69% of repos, top 6 cover 79%). Sampled top + long-tail +# variants all carry the same 3-job structure (gitleaks + rust-secrets + +# shell-secrets) at 75-81 lines; drift is action-SHA / whitespace churn. +# +# Rationale for gitleaks over trufflehog: +# - Gitleaks has better CVE coverage for common secret patterns +# - Trufflehog was removed as redundant; gitleaks catches what we need +# - Cost reduction: eliminates duplicate scanning +# +# Critical security debt this PR addresses: +# The `shell-secrets` job (added to the canonical 2026-05-21 in +# response to the Cloudflare token leak via `2-protocols/avow/ +# deploy-repos.sh`, which both `trufflehog --only-verified` and +# default gitleaks missed) is currently present in 0 of 16 sampled +# estate repos. The post-incident guardrail intended to catch the +# *next* leak has propagated to nothing. Consolidating behind this +# reusable force-promotes shell-secrets to all 281 repos on the next +# wrapper sweep. +# +# Design: +# - No per-call inputs other than `runs-on`. Each job self-conditions +# internally (rust-secrets exits early on no Cargo.toml; shell-secrets +# no-ops on no .sh/.bash files), so there is nothing for the caller +# to toggle. +# - Caller passes NO `secrets:` line. This reusable declares no +# `workflow_call.secrets` and references no `secrets.*` context: +# gitleaks runs as a pinned local binary (see the PERMISSIONS note), +# and the `GITHUB_TOKEN` the jobs act under is governed by +# `permissions:`, not by caller secret passing. `secrets: inherit` +# would forward every repository and organisation secret the caller +# can see (CWE-250) and buy nothing. An earlier version of this +# header claimed inherit was REQUIRED so `gitleaks/gitleaks-action` +# could see `secrets.GITHUB_TOKEN`; that claim died with #500 but +# kept propagating into generated callers estate-wide. Do not +# re-add it. (standards#1108 ยง6b) +# - Caller keeps its own `on:` triggers and `concurrency:` group, so +# the read-only cancel-superseded behaviour stays in the wrapper. +# - Trufflehog removed: gitleaks provides sufficient coverage at lower cost. +# +# PERMISSIONS โ€” `contents: read` is the whole requirement. +# +# Every job here runs `actions/checkout` and then a self-contained binary or +# shell scan. There are no artifacts, no cache, no `gh api` calls and no +# workflow-run queries, so nothing needs `actions: read`, and since #500 +# replaced `gitleaks/gitleaks-action` with a pinned checksum-verified binary, +# nothing needs `pull-requests: write` either. +# +# This block previously instructed callers to grant `pull-requests: write` + +# `actions: read`. That guidance was correct for the pre-#500 action-based +# gitleaks job and became wrong when the binary replaced it โ€” but it was left +# in place, and the workflow-level block below kept an unused `actions: read`. +# Because a called workflow cannot exceed its caller's *job-level* grant, a +# correctly least-privileged wrapper granting only `contents: read` was +# rejected at startup โ€” which is exactly what happened to this repo's own +# `secret-scanner.yml`, silently disabling its secret scanning. +# +# Two rules that make that failure mode non-obvious, worth stating plainly: +# 1. A job-level `permissions:` block REPLACES the workflow-level one for +# that job โ€” it does not merge with it. A caller with `actions: read` at +# the top and `contents: read` on the job grants `contents: read` only. +# 2. A rejected run produces NO check run at all โ€” `gh pr checks` shows +# nothing and the annotations endpoint 404s. Diagnose with +# `gh run list --json conclusion`, never by the absence of a red tick. +# +# Every job below therefore declares `permissions: contents: read` explicitly +# rather than inheriting. Inheritance is what let an unused workflow-level +# grant leak into two jobs unnoticed; explicit per-job blocks mean a future +# widening of the workflow-level block cannot silently re-break callers. +# +# Caller example (wrapper) โ€” this is the complete, minimal, correct shape: +# # SPDX-License-Identifier: MPL-2.0 +# name: Secret Scanner +# on: +# pull_request: +# push: +# branches: [main] +# concurrency: +# group: ${{ github.workflow }}-${{ github.ref }} +# cancel-in-progress: true +# permissions: +# contents: read +# jobs: +# scan: +# permissions: +# contents: read +# uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@ +# # Deliberately NO `secrets:` line โ€” the callee declares none and +# # needs none (see the Design note above). + +name: Secret Scanner (reusable) + +on: + workflow_call: + inputs: + runs-on: + description: Runner label for all secret-scanner jobs + type: string + required: false + default: ubuntu-latest + +# `actions: read` was removed here: no job in this file reads workflow runs, +# artifacts or caches, so it was an unused grant โ€” and because a called +# workflow cannot exceed its caller's job-level grant, it broke every wrapper +# that correctly granted `contents: read` alone. See the PERMISSIONS note above. +permissions: + contents: read + +jobs: + gitleaks: + timeout-minutes: 20 + runs-on: ${{ inputs.runs-on }} + # Declared explicitly rather than inherited โ€” see the PERMISSIONS note. + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Full history. One of the two gating passes below scans git history, + # and the default depth-1 clone would silently reduce it to a single + # commit. That pass asserts non-shallowness for itself rather than + # trusting this line to have taken effect. + fetch-depth: 0 + + # Gitleaks runs as a pinned, checksum-verified binary rather than via + # gitleaks/gitleaks-action. The action injects gitleaks into the runner + # tool-cache and prepends it to PATH, which is unreliable on self-hosted + # runners โ€” that unreliability was the reason this step previously carried + # `continue-on-error: true`, i.e. the estate's primary secret scanner + # could not fail a PR. A pinned direct install is deterministic on every + # runner, so the scan gates for real. + - name: Install gitleaks (pinned + checksum-verified) + run: | + set -euo pipefail + GITLEAKS_VERSION=8.30.1 + SHA256=551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb + cd "$RUNNER_TEMP" + curl -fsSL -o gitleaks.tar.gz \ + "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + echo "${SHA256} gitleaks.tar.gz" | sha256sum -c - + tar -xzf gitleaks.tar.gz gitleaks + chmod +x gitleaks + ./gitleaks version + + # Stage the ESTATE BASELINE allowlist into the workspace root. + # + # WHY THIS EXISTS. The scan below used to run with no `--config` at all, + # so gitleaks fell back to looking for `.gitleaks.toml` in the CALLER's + # tree. Measured 2026-08-06: none of the eleven repositories then failing + # this gate had one, so all eleven ran on bare default rules with no + # allowlist โ€” and produced 154 findings of which every single one was a + # false positive (ML training corpora, vendored Yarn bundles, published + # conformance vectors, `.example` configs, and fixtures belonging to the + # estate's own secret DETECTORS). + # + # standards/.gitleaks.toml already anticipated precisely this: "turn the + # gate on with no allowlist, get a wall of red that is entirely noise, + # someone reverts, and the estate is back to a scanner that cannot fail." + # The file was written and then never wired to the scan. This wires it. + # + # It is staged at the WORKSPACE ROOT, not $RUNNER_TEMP, because gitleaks + # resolves `[extend] path` relative to the PROCESS CWD โ€” verified, and the + # opposite of the intuitive "relative to the config file". Putting it here + # lets a repo-local `.gitleaks.toml` portably declare + # [extend] + # path = ".gitleaks-estate.toml" + # and inherit the whole baseline while adding only its own exemptions, so + # a repo-specific blind spot stays local to the repo that justified it + # instead of widening the allowlist for all 400+. + # + # THIS STEP RUNS FOR EVERY CONSUMER WITHOUT A `.gitleaks.toml`. It is the + # only thing that creates `.gitleaks-estate.toml` for that consumer โ€” the + # large majority โ€” and that is exactly the consumer whose CONFIG resolves + # to that file in the scan step below. Do not delete it as a duplicate of + # the gated "Estate baseline โ€” is it referenced?" block further down; + # that block fires only for repos that DO have a local config, so + # removing this one leaves CONFIG naming a file nothing creates, across + # most of the estate. + # + # It is skipped when the repo HAS a `.gitleaks.toml`. That repo scans + # with its own config, and the gated block below stages the baseline + # only if that config `[extend]`s it. Staged anyway, the baseline sat + # inside the scanned tree under a config that does not allowlist the + # fixture values its own comments quote, and the gate reported the + # estate file as the caller's leak (echidnabot#185: three + # generic-api-key findings in .gitleaks-estate.toml). + # + # `ref` was `main` until 2026-09-04, and that was worse than a moving + # pin: the file this step fetches has been RENAMED under it. Before + # 86bad549 the shared allowlist WAS `.gitleaks.toml` (148 lines, 16 + # rules) and gen-1 sparse-checked-out that path; after the rename, + # `.gitleaks.toml` is standards' own 18-line dogfooding stub whose + # `[extend] path` points at `config/gitleaks/estate-baseline.toml` โ€” a + # path the consumer does NOT have, because this step stages exactly one + # file. So every consumer pinned before the rename silently began + # staging a stub with a dangling `[extend]`. `job.workflow_sha` is the + # commit of THIS reusable, so a consumer now receives the baseline as it + # stood at the SHA it pins, and the path each SHA references exists at + # that SHA (verified for all six gen-1 SHAs, 2026-09-04). + - name: Fetch estate gitleaks baseline + if: hashFiles('.gitleaks.toml') == '' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: hyperpolymath/standards + ref: c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d + path: .standards-gitleaks + sparse-checkout: | + config/gitleaks/estate-baseline.toml + sparse-checkout-cone-mode: false + persist-credentials: false + + - name: Stage estate gitleaks baseline + if: hashFiles('.gitleaks.toml') == '' + run: | + set -euo pipefail + # Stage THE estate baseline, not standards' own repo config. + # standards/.gitleaks.toml is a CONSUMER of the baseline -- it carries + # `[extend] path = "config/gitleaks/estate-baseline.toml"`, and + # `[extend] path` resolves against the PROCESS working directory. Copied + # into a consumer, that path does not exist and gitleaks dies with + # FTL failed to load extended config ... no such file + # scanning nothing and failing the gate. The baseline file's own header + # documents this staging as the intended behaviour. + cp .standards-gitleaks/config/gitleaks/estate-baseline.toml .gitleaks-estate.toml + # Delete the tooling checkout BEFORE scanning: otherwise the scan + # walks standards' own tree and reports ITS files as the caller's. + # (The Hypatia baseline job above does the same thing for the same + # reason.) + rm -rf .standards-gitleaks + test -s .gitleaks-estate.toml + + # TWO gating passes run below and they are not redundant. Each sees a + # population the other cannot: + # * working tree (--no-git) -- sees untracked and generated files; + # cannot see a blob deleted in an earlier commit + # * git history -- sees committed-then-deleted secrets; + # cannot see a file that was never committed + # Both gate: a finding in either exits non-zero and fails the check. + # Full history costs a slower scan on every PR. That is the accepted + # price of a deleted secret no longer being permanently invisible. + # โ”€โ”€ Estate baseline sharing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + # A consumer repo can inherit the estate-wide allowlist instead of + # copying it, by writing in its own .gitleaks.toml: + # + # [extend] + # path = ".gitleaks-estate.toml" + # + # The point of the split is blast radius. An entry in the estate baseline + # blinds all 400+ repositories; an entry in a repo's own config blinds + # one, and sits beside the code it describes. Before this existed the + # only way to share the baseline was to copy it, so the two drifted and + # repo-local suppressions had nowhere to live except the shared file. + # + # Staged ONLY when actually referenced. A repo that does not extend it + # must not gain an unreferenced file inside its own scanned tree. + # + # `[extend] path` resolves against the PROCESS working directory, not the + # config file's directory (verified empirically: the same relative path + # fails when gitleaks is invoked from a subdirectory). The scan below runs + # from the workspace root, so the workspace root is where this lands. + - name: Estate baseline โ€” is it referenced? + id: estate_baseline + run: | + set -euo pipefail + if [ -f .gitleaks.toml ] && \ + grep -qE "^[[:space:]]*path[[:space:]]*=[[:space:]]*[\"']\.gitleaks-estate\.toml[\"']" .gitleaks.toml; then + echo "referenced=true" >> "$GITHUB_OUTPUT" + echo "This repo extends the estate baseline; staging it." + else + echo "referenced=false" >> "$GITHUB_OUTPUT" + echo "No reference to .gitleaks-estate.toml; nothing staged." + fi + + # Pinned by construction: job.workflow_sha is the commit of THIS reusable + # workflow, so the baseline always matches the workflow consuming it. A + # caller pinned to an old SHA gets that SHA's baseline, not a moving one. + - name: Estate baseline โ€” fetch + if: steps.estate_baseline.outputs.referenced == 'true' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: hyperpolymath/standards + ref: c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d + path: .estate-baseline-checkout + sparse-checkout: config/gitleaks + sparse-checkout-cone-mode: false + persist-credentials: false + + - name: Estate baseline โ€” install + if: steps.estate_baseline.outputs.referenced == 'true' + run: | + set -euo pipefail + src=.estate-baseline-checkout/config/gitleaks/estate-baseline.toml + if [ ! -f "$src" ]; then + echo "::error::Estate baseline missing at $src. The repo's .gitleaks.toml extends .gitleaks-estate.toml, but the baseline could not be fetched โ€” failing rather than scanning with a silently reduced config." + exit 1 + fi + cp "$src" .gitleaks-estate.toml + # Remove the checkout before scanning: otherwise standards' own tree + # is scanned as if it were the caller's, which is how a shared config + # turns into a wall of someone else's findings. + rm -rf .estate-baseline-checkout + echo "Staged .gitleaks-estate.toml ($(wc -l < .gitleaks-estate.toml) lines)." + + # + # --verbose is REQUIRED, not cosmetic. Without it gitleaks prints only + # "leaks found: N" and nothing else โ€” no rule, no file, no line โ€” and the + # run uploads no artifact, so the findings exist solely inside a discarded + # container. That is a gate which blocks a merge while telling the author + # nothing actionable, and it is why 154 false positives sat untriaged + # across eleven repositories. --redact is already set, so values stay + # masked: the log gains the rule, path and line, never the secret. + - name: Verify scanner sensitivity with a synthetic credential + run: | + config=.gitleaks-estate.toml + if [ -f .gitleaks.toml ]; then config=.gitleaks.toml; fi + bash scripts/tests/test-secret-scanner.sh "$RUNNER_TEMP/gitleaks" "$config" + + - name: Gitleaks secret scan (gating) + run: | + set -euo pipefail + + # A repo-local baseline wins outright โ€” it is expected to `[extend]` + # the estate one, so "wins" still means "inherits". This mirrors what + # the AsciiDoc pass below already did, which was inconsistent with + # this step until now. + CONFIG=".gitleaks-estate.toml" + if [ -f .gitleaks.toml ]; then + CONFIG=".gitleaks.toml" + echo "Using repository .gitleaks.toml (extending the estate baseline)." + else + echo "Using estate baseline allowlist." + fi + + "$RUNNER_TEMP/gitleaks" detect \ + --source . \ + --no-git \ + --redact \ + --no-banner \ + --verbose \ + --config "$CONFIG" \ + --exit-code 1 + + # Full-history pass โ€” ADDITIVE, not a replacement. Measured on a fixture + # with this exact pinned binary, the same secret in two placements: + # + # committed then deleted working tree: MISSED history: FOUND + # untracked file working tree: FOUND history: MISSED + # + # git mode scans commit diffs, so it cannot see a file that was never + # committed; --no-git walks the checked-out tree, so it cannot see a blob + # that is no longer in it. Dropping either pass REMOVES coverage. Keeping + # both is why this was added as a new step rather than by editing the one + # above, whose name and log lines existing triage keys on. + # + # KNOWN GAP, stated rather than implied: the AsciiDoc mirror pass below + # scans a filesystem copy with --no-git, so fetch-depth: 0 cannot reach + # it. An AsciiDoc secret that was committed and later deleted stays + # invisible. "The gate is full-history" holds for every file class but + # that one. + - name: Gitleaks secret scan โ€” git history (gating) + run: | + set -euo pipefail + + # fetch-depth: 0 on the checkout is load-bearing HERE. If it ever + # regresses to the default depth-1 clone, detect would walk a single + # commit, find nothing and report a pass โ€” a gate that cannot fail. + # Assert completeness from git itself: gitleaks' own "scanned N + # commits" line under-reports and is not proof of depth. + if [ "$(git rev-parse --is-shallow-repository)" != "false" ]; then + echo "::error::checkout is shallow -- a history scan here would be vacuous; refusing to report a pass" + exit 1 + fi + echo "history available to this scan: $(git rev-list --count HEAD) commits" + + # Same config resolution as the working-tree pass, duplicated rather + # than factored out so that step stays byte-identical. + CONFIG=".gitleaks-estate.toml" + if [ -f .gitleaks.toml ]; then + CONFIG=".gitleaks.toml" + fi + # A staging step that failed silently would leave gitleaks running a + # reduced config over almost nothing and calling the result green. + test -s "$CONFIG" + + "$RUNNER_TEMP/gitleaks" detect \ + --source . \ + --redact \ + --no-banner \ + --verbose \ + --config "$CONFIG" \ + --exit-code 1 + + # AsciiDoc blind-spot pass โ€” MEASURED, not theoretical. + # + # gitleaks' DEFAULT global allowlist excludes paths matching + # `(jpg|gif|doc|docx|zip|xls|pdf|bin|svg|socket)$`. That `doc` alternative + # is an unanchored *substring* match on the filename, so it swallows + # `.adoc` and `.asciidoc` as collateral โ€” AsciiDoc is not a binary format + # and was never the intended target. + # + # This estate writes documentation in AsciiDoc by default, so the blind + # spot covers ~40,000 files estate-wide (1,620 in standards alone). It is + # not hypothetical: a live write-scoped `glpat-` token sat undetected in + # `rhodium-pipeline/docs/QUICKSTART.adoc` while this scanner reported + # "no leaks found". Proof โ€” identical content, four extensions: + # .txt -> DETECTED .md -> DETECTED + # .adoc -> MISSED .asciidoc -> MISSED + # + # The exclusion cannot be switched off from a config that sets + # `[extend] useDefault = true` (verified: neither `[allowlist] paths = []` + # nor adding an explicit custom rule re-enables scanning โ€” the file is + # dropped at path level, before any rule runs). Turning `useDefault` off + # would forfeit all ~150 default rules, so instead this step scans a + # throwaway mirror in which each AsciiDoc file is copied to a `.txt` + # suffix. Same binary, same rules, same repo config โ€” only the filename + # differs, which is the sole thing the allowlist keys on. + # + # Findings are re-mapped back to their real paths before reporting, so + # output never mentions the mirror. Runs as a separate step (not folded + # into the scan above) so the log distinguishes "leak in code" from + # "leak in docs". + - name: Gitleaks secret scan โ€” AsciiDoc mirror (gating) + run: | + set -euo pipefail + + MIRROR="$RUNNER_TEMP/adoc-mirror" + REPORT="$RUNNER_TEMP/adoc-report.json" + rm -rf "$MIRROR"; mkdir -p "$MIRROR" + + # -print0/read -d '' so paths with spaces or newlines survive; the + # estate has directories with spaces in them. + found=0 + while IFS= read -r -d '' f; do + dest="$MIRROR/$(dirname "$f")" + mkdir -p "$dest" + cp "$f" "$dest/$(basename "$f").txt" + found=$((found + 1)) + done < <(find . -path ./.git -prune -o \ + \( -name '*.adoc' -o -name '*.asciidoc' \) -type f -print0) + + if [ "$found" -eq 0 ]; then + echo "No AsciiDoc files present โ€” nothing to scan." + exit 0 + fi + echo "Scanning $found AsciiDoc file(s) via mirror." + + # Honour the caller's own baseline when it has one, so repo-specific + # allowlists still apply to docs exactly as they do to code โ€” and + # otherwise fall back to the estate baseline, so docs and code are + # judged by the SAME rules. Previously this step honoured a repo + # config while the code scan above honoured none, which meant an + # allowlist entry could suppress a finding in a `.adoc` file and not + # in the `.md` file beside it. + # + # Absolute paths: this scan's --source is the MIRROR directory, so a + # relative config path would resolve against the mirror rather than + # the repository. `[extend] path = ".gitleaks-estate.toml"` inside a + # repo config resolves against the process CWD (still the repo root), + # which is why the estate baseline is staged there. + config_args=(--config "$PWD/.gitleaks-estate.toml") + if [ -f .gitleaks.toml ]; then + config_args=(--config "$PWD/.gitleaks.toml") + echo "Using repository .gitleaks.toml." + else + echo "Using estate baseline allowlist." + fi + + # Report first, decide after: --exit-code would abort before the paths + # can be mapped back, leaving an unreadable mirror path in the log. + set +e + "$RUNNER_TEMP/gitleaks" detect \ + --source "$MIRROR" \ + --no-git \ + --redact \ + --no-banner \ + --report-format json \ + --report-path "$REPORT" \ + "${config_args[@]}" + set -e + + # `grep` exits 1 when it matches nothing, which under `set -e` + + # `pipefail` aborts the step with status 1 โ€” i.e. a repo whose docs are + # CLEAN would fail the gate. Caught by red-teaming the clean case; + # pipefail is disabled around the count for exactly this reason. + count=0 + if [ -s "$REPORT" ]; then + set +o pipefail + count=$(grep -o '"RuleID"' "$REPORT" 2>/dev/null | wc -l | tr -d ' ') + set -o pipefail + [ -n "$count" ] || count=0 + fi + + if [ "$count" -eq 0 ]; then + echo "No leaks found in AsciiDoc files." + exit 0 + fi + + # Findings list, always printed, values redacted. Paths are recovered + # from the mirror so the output names the real files. + echo "Findings (values redacted; paths are the REAL files, not the mirror):" + sed -n 's/.*"File": *"\([^"]*\)".*/\1/p' "$REPORT" \ + | sed -e "s|^${MIRROR}/||" -e 's|\.txt$||' \ + | sort -u \ + | sed 's/^/ - /' + echo + echo "These files are invisible to a default gitleaks run because the" + echo "default allowlist drops any path ending in 'doc'. Treat every hit" + echo "above as a real, previously unscanned leak." + echo + + # GRACE WINDOW โ€” same cutoff as the docs and package-policy gates + # (standards#513), for the same reason: this step makes ~40,000 + # previously unscanned estate files visible at once, and every hit is + # pre-existing debt rather than a regression introduced by the PR in + # front of it. Turning it straight to blocking would red a large number + # of repos simultaneously for faults none of their authors introduced. + # + # It never prints a green tick while findings exist, and it never + # suppresses the list above โ€” the warning is loud and the date is real. + # An unparseable date refuses to run rather than silently defaulting to + # the permissive branch, which is how a warn-first gate degrades into a + # fake one. + ENFORCE_ADOC_SCAN_FROM="${ENFORCE_ADOC_SCAN_FROM:-2026-08-21}" + TODAY="${ADOC_TODAY:-$(date -u +%Y-%m-%d)}" + for pair in "ENFORCE_ADOC_SCAN_FROM=$ENFORCE_ADOC_SCAN_FROM" "ADOC_TODAY=$TODAY"; do + case "${pair#*=}" in + [0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]) ;; + *) echo "::error::${pair%%=*} is not YYYY-MM-DD; refusing to run."; exit 1 ;; + esac + done + + if [[ "$TODAY" < "$ENFORCE_ADOC_SCAN_FROM" ]]; then + echo "::warning::${count} secret(s) found in AsciiDoc files. This becomes a" \ + "BLOCKING failure on ${ENFORCE_ADOC_SCAN_FROM} (today is ${TODAY})." + echo "NOT YET ENFORCED: AsciiDoc findings present but inside the grace window." + exit 0 + fi + + echo "::error::${count} secret(s) detected in AsciiDoc files." + exit 1 + + # Rust-specific: hardcoded crypto values. Self-skips when no Cargo.toml + # is present, so this job is safe to run on non-Rust repos. + rust-secrets: + timeout-minutes: 10 + runs-on: ${{ inputs.runs-on }} + # Declared explicitly rather than inherited โ€” see the PERMISSIONS note. + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Check for hardcoded secrets in Rust + env: + # Findings outside ./src (newly visible โ€” see the scan-scope note + # below) warn until this date, then block. Same warn-then-enforce + # idiom and same cutoff as check-package-policy.sh / + # check-docs-presence.sh. + ENFORCE_RUST_WIDE_SCAN_FROM: '2026-08-21' + run: | + TODAY="${RUST_TODAY:-$(date -u +%Y-%m-%d)}" + + # An unparseable cutoff would pick the warn branch forever, silently + # disarming the widened scan. Refuse to run instead. + require_date() { + case "$2" in + [0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]) : ;; + *) echo "::error::rust-secrets: $1='$2' is not YYYY-MM-DD." + echo "Refusing to run: an unparseable cutoff would disarm this gate." + exit 1 ;; + esac + } + require_date ENFORCE_RUST_WIDE_SCAN_FROM "$ENFORCE_RUST_WIDE_SCAN_FROM" + require_date RUST_TODAY "$TODAY" + + if ! find . -name Cargo.toml -not -path './target/*' -print -quit | grep -q .; then + echo 'No Cargo.toml found โ€” skipping Rust secrets check' + exit 0 + fi + + # Scan every .rs file, not just ./src. A Cargo workspace keeps its + # code in crates/*/src, and the previous `grep โ€ฆ src/` matched + # nothing there: grep exits 2 on a missing directory, which the `if` + # read as "clean". Measured 2026-07-21 across the 64 Rust repos in + # the estate re-pin sweep: 8 (12.5%) have no root src/ and were + # therefore never scanned at all โ€” a planted secret in + # crates/core/src/lib.rs passed this job silently. + mapfile -t FILES < <(find . -name '*.rs' \ + -not -path './target/*' -not -path '*/target/*' \ + -not -path './vendor/*' -not -path '*/vendor/*' \ + -not -path './.git/*' -print) + if [ ${#FILES[@]} -eq 0 ]; then + echo 'Cargo.toml present but no .rs files โ€” nothing to scan' + exit 0 + fi + echo "Scanning ${#FILES[@]} Rust file(s)." + + # Each pattern requires an assignment DIRECTLY to a string literal of + # meaningful length. The previous forms matched `= ... "` โ€” a quote + # anywhere after an equals โ€” which flagged code that hardcodes + # nothing at all. Measured across the 64 Rust repos in the estate + # sweep, every one of these was a false positive: + # let api_key = request["api_key"].as_str().unwrap_or("") (lookup) + # let api_key_file = temp_dir.path().join("config.js") (path) + # Query::new("Here's my api_key=sk-12345") (a demo) + # A credential shorter than 8 characters is not a credential either. + # `static` is accepted alongside `const`: the old `const.*KEY` form + # missed `static AUTH_KEY: &str = "โ€ฆ"`, a genuine hardcoded secret + # (caught by canary, not by inspection). + PATTERNS=( + '(const|static)[[:space:]]+(mut[[:space:]]+)?[A-Za-z0-9_]*SECRET[A-Za-z0-9_]*[[:space:]]*(:[^=]*)?=[[:space:]]*b?"[^"]{8,}"' + '(const|static)[[:space:]]+(mut[[:space:]]+)?[A-Za-z0-9_]*KEY[A-Za-z0-9_]*[[:space:]]*(:[^=]*)?=[[:space:]]*b?"[a-zA-Z0-9]{16,}"' + '(const|static)[[:space:]]+(mut[[:space:]]+)?[A-Za-z0-9_]*TOKEN[A-Za-z0-9_]*[[:space:]]*(:[^=]*)?=[[:space:]]*b?"[^"]{8,}"' + '(let|static)[[:space:]]+(mut[[:space:]]+)?[a-z0-9_]*api_key[a-z0-9_]*[[:space:]]*(:[^=]*)?=[[:space:]]*b?"[^"]{8,}"' + 'HMAC.*"[a-fA-F0-9]{32,}"' + '[a-z0-9_]*password[a-z0-9_]*[[:space:]]*(:[^=]*)?=[[:space:]]*b?"[^"]{8,}"' + ) + + # Exemptions, mirroring the four layers already documented on + # shell-secrets. All match on the VALUE or on an explicit pragma, + # never on a file path: a path-based exemption would blind the job + # to a genuine secret in the same file (the failure mode the + # .gitleaks.toml allowlist was rewritten to avoid, standards#512). + # + # 1. env-lookup RHS โ€” `env::var("NAME")` IS the practice this job + # exists to enforce, so flagging it fails the correct code. This + # was the sole rust-secrets failure in the 201-repo sweep: + # cloudguard-server src/main.rs:59, + # `let api_key = env::var("CLOUDGUARD_API_KEY").ok();`. Anchored + # to the assignment and stopped at `;`, so a literal secret with + # an unrelated env::var mention later on the line still trips. + # 2. URL values โ€” an OAuth endpoint is public and not a credential. + # `const MS_TOKEN_URL = "https://login.microsoftonline.com/โ€ฆ"` + # tripped `const.*TOKEN.*=.*"` in four places in one repo. + # 3. comment lines โ€” documentation of a pattern is not a live secret. + # 4. inline pragma `// scanner-allow: rust-secrets`. + COMMENT='^[^:]*:[0-9][0-9]*:[[:space:]]*(//|/\*|\*)' + ENV_RHS='=[^;]*(env::var|env::var_os|std::env|env!\(|option_env!\(|from_env|dotenv)' + URL_RHS='=[[:space:]]*b?"(https?|wss?|ftp)://' + PRAGMA='scanner-allow:[[:space:]]*rust-secrets' + + blocking=0 + advisory=0 + for pattern in "${PATTERNS[@]}"; do + hits="$(grep -nHE "$pattern" "${FILES[@]}" 2>/dev/null \ + | grep -vE "$COMMENT" \ + | grep -vE "$ENV_RHS" \ + | grep -vE "$URL_RHS" \ + | grep -vE "$PRAGMA" || [ $? -eq 1 ])" + [ -n "$hits" ] || continue + + # ./src was the only path the previous version scanned. Hits there + # keep blocking exactly as before โ€” no regression. Hits outside it + # are newly visible, so they warn until the cutoff rather than + # reddening repos that were never actually being scanned. + old_scope="$(printf '%s\n' "$hits" | grep -E '^\./src/' || [ $? -eq 1 ])" + new_scope="$(printf '%s\n' "$hits" | grep -vE '^\./src/' || [ $? -eq 1 ])" + + if [ -n "$old_scope" ]; then + printf '%s\n' "$old_scope" + echo "WARNING: Potential hardcoded secret found matching: $pattern" + blocking=1 + fi + if [ -n "$new_scope" ]; then + printf '%s\n' "$new_scope" + if [[ "$TODAY" < "$ENFORCE_RUST_WIDE_SCAN_FROM" ]]; then + echo "::warning::Potential hardcoded secret outside ./src matching:" \ + "$pattern โ€” becomes BLOCKING on $ENFORCE_RUST_WIDE_SCAN_FROM (today is $TODAY)." + advisory=1 + else + echo "WARNING: Potential hardcoded secret found matching: $pattern" + blocking=1 + fi + fi + done + + if [ "$blocking" -eq 1 ]; then + echo "::error::Potential hardcoded secrets detected. Use environment variables instead." + exit 1 + fi + if [ "$advisory" -eq 1 ]; then + # Never claim a clean pass while findings are outstanding. + echo "NOT YET ENFORCED: findings outside ./src are advisory until $ENFORCE_RUST_WIDE_SCAN_FROM." + exit 0 + fi + echo 'No hardcoded secrets detected in Rust sources.' + + # Shell-specific: catch hardcoded credentials in shell scripts. + # Added to canonical 2026-05-21 after default gitleaks missed a real + # Cloudflare API token leaked via 2-protocols/avow/deploy-repos.sh. + # As of this PR, 0 of 16 sampled estate repos carry this guardrail โ€” + # the reusable closes that gap. + # + # Exemption mechanisms (four layers, applied before reporting a hit): + # 1. Inline pragma (same line or the line immediately above): + # # scanner-allow: shell-secrets + # # hypatia: allow security_errors/secret_detected + # Either form suppresses the hit for that line. + # 2. .shell-secrets-ignore in repo root: gitignore-style line globs; + # a match against the file path suppresses every hit in that file. + # 3. Param-expansion RHS: assignments whose value is ${VAR}, ${VAR:-โ€ฆ}, + # ${VAR:?โ€ฆ}, or $VAR (no literal value) are skipped automatically. + # 4. Comment lines (leading non-whitespace is '#'): documentation of + # patterns cannot be real secrets. + shell-secrets: + timeout-minutes: 10 + runs-on: ${{ inputs.runs-on }} + # Declared explicitly rather than inherited โ€” see the PERMISSIONS note. + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Check for hardcoded secrets in shell scripts + run: | + # Patterns: an `export FOO=` or `FOO=` with a quoted literal of meaningful length. + # Restricted to *_TOKEN / *_KEY / *_SECRET / PASSWORD to keep false-positives low. + PATTERNS=( + '(export[[:space:]]+)?[A-Z_]*TOKEN[A-Z_]*=["'"'"'][A-Za-z0-9_./+=-]{20,}["'"'"']' + '(export[[:space:]]+)?[A-Z_]*API_KEY[A-Z_]*=["'"'"'][A-Za-z0-9_./+=-]{20,}["'"'"']' + '(export[[:space:]]+)?[A-Z_]*SECRET[A-Z_]*=["'"'"'][A-Za-z0-9_./+=-]{16,}["'"'"']' + '(export[[:space:]]+)?PASSWORD=["'"'"'][^"'"'"']{6,}["'"'"']' + ) + + # Inline pragma patterns โ€” suppress a hit when found on the same or + # immediately preceding line. + PRAGMA_RE='(scanner-allow:[[:space:]]*shell-secrets|hypatia:[[:space:]]*allow[[:space:]]+security_errors/secret_detected)' + + # Param-expansion RHS pattern โ€” assignments whose value is a variable + # reference rather than a literal are never real secrets. + # Matches: ="$VAR" ="${VAR}" ="${VAR:-โ€ฆ}" ="${VAR:?โ€ฆ}" ='${VAR}' =$VAR + PARAM_EXPANSION_RE='=['"'"'"'"'"']?\$\{?[A-Za-z_][A-Za-z0-9_]*(:[?-][^}]*)?\}?['"'"'"'"'"']?[[:space:]]*(#.*)?$' + + # Load per-repo ignore globs from .shell-secrets-ignore if present. + IGNORE_GLOBS=() + if [[ -f .shell-secrets-ignore ]]; then + while IFS= read -r line || [[ -n "$line" ]]; do + # Skip blank lines and comments + [[ -z "$line" || "$line" == \#* ]] && continue + IGNORE_GLOBS+=("$line") + done < .shell-secrets-ignore + fi + + # is_ignored โ€” returns 0 (true) if path matches any ignore glob. + is_ignored() { + local path="$1" + for glob in "${IGNORE_GLOBS[@]}"; do + # Use bash pattern matching; strip leading ./ for consistency. + local stripped="${path#./}" + # shellcheck disable=SC2254 + if [[ "$stripped" == $glob ]]; then + return 0 + fi + done + return 1 + } + + found=0 + for pattern in "${PATTERNS[@]}"; do + # Collect raw matches: filepath:lineno:content + while IFS= read -r raw_match; do + [[ -z "$raw_match" ]] && continue + + # Parse filepath and line number from grep -n output (file:line:content). + filepath="${raw_match%%:*}" + rest="${raw_match#*:}" + lineno="${rest%%:*}" + content="${rest#*:}" + + # Exemption 1: .shell-secrets-ignore glob match on the file path. + if is_ignored "$filepath"; then + echo " [skip] $filepath:$lineno โ€” matched .shell-secrets-ignore glob" + continue + fi + + # Exemption 2: param-expansion RHS โ€” value is a variable reference, not a literal. + if echo "$content" | grep -qE "$PARAM_EXPANSION_RE"; then + echo " [skip] $filepath:$lineno โ€” param-expansion (not a literal value)" + continue + fi + + # Exemption 3: comment line โ€” leading non-whitespace char is '#'. + # Comment lines cannot hold real secrets; they only document patterns. + if echo "$content" | grep -qE '^[[:space:]]*#'; then + echo " [skip] $filepath:$lineno โ€” comment line (not executable)" + continue + fi + + # Exemption 5: inline pragma on same line. + if echo "$content" | grep -qE "$PRAGMA_RE"; then + echo " [skip] $filepath:$lineno โ€” inline pragma on same line" + continue + fi + + # Exemption 6: inline pragma on the immediately preceding line. + if [[ "$lineno" -gt 1 ]]; then + prev_line=$(sed -n "$((lineno - 1))p" "$filepath" 2>/dev/null) + if echo "$prev_line" | grep -qE "$PRAGMA_RE"; then + echo " [skip] $filepath:$lineno โ€” pragma on preceding line" + continue + fi + fi + + echo "::warning::$filepath:$lineno: $content" + echo " Potential hardcoded secret matching pattern: $pattern" + found=1 + + done < <(grep -rnE --include='*.sh' --include='*.bash' \ + --exclude-dir='.git' --exclude-dir='node_modules' --exclude-dir='target' \ + "$pattern" . 2>/dev/null || [ $? -eq 1 ]) + done + + if [ $found -eq 1 ]; then + echo "::error::Hardcoded secret detected in a shell script. Read the value from the environment (e.g. \"\${MY_TOKEN:?}\") and supply it via a repo or org secret." + exit 1 + fi diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index fa8a07f6..ce0c5aa7 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -22,6 +22,6 @@ jobs: # Full-history + working-tree gitleaks and shell-secret checks. The # reusable installs a checksum-verified binary; no action licence or # repository secrets are passed to code from a pull request. - uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d + uses: ./.github/workflows/secret-scanner-reusable.yml permissions: contents: read diff --git a/docs/release-notes/2026-10-23-governance-security.md b/docs/release-notes/2026-10-23-governance-security.md index 5858d560..f2f7a1e1 100644 --- a/docs/release-notes/2026-10-23-governance-security.md +++ b/docs/release-notes/2026-10-23-governance-security.md @@ -13,7 +13,7 @@ No other contributor's branch was rewritten. ## Changes -- Full-history and working-tree secret scanning via the standards reusable at +- Full-history and working-tree secret scanning via a fork-local copy of the standards reusable at `c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d`, with only `contents: read` and no inherited secrets. Findings fail the job; no local suppressions added. - Pinned Scorecard publication on main and weekly/manual runs, not PR code. @@ -44,7 +44,7 @@ With bun **1.3.10**: - `bun audit --audit-level high`: pass. Full JSON audit fell from 31 advisories (1 critical, 13 high, 14 moderate, 3 low) to **2 moderate** React Router advisories. This is not a zero-vulnerability claim. -- `bash scripts/check-governance-contract.sh`: 31 assertions. +- `bash scripts/check-governance-contract.sh`: 39 assertions. - `bash scripts/tests/test-governance-contract.sh`: 8 weakening mutations rejected (SARIF, K9, scanner privileges/failure policy, Scorecard PR trigger). - `bash scripts/tests/test-ci-logging.sh`: success and each combination of @@ -58,6 +58,41 @@ runtime/semantic checks are delegated to hosted CI, not reported as local passes. GitHub log/artifact downloads are restricted by the sandbox's host allowlist; job/check APIs and the published Hypatia grid remain readable. +## Hosted K9 correction + +The first hosted run exposed a bug in the pinned standards wrapper: its +terminal `[ -f corpus.log ] && cp ...` returned 1 for the legitimate empty +corpus, after rendering and archiving the grid. The fork-local reusable now +uses an explicit conditional. It still checks out the validator, contract +and renderer at the same immutable standards SHA; no semantic checks or +ratchet rules were weakened. Three regression cases exercise absent log, +present log and a genuine copy failure. The local reusable only has a +`workflow_call` trigger to prevent duplicate standalone conformance runs. + +## Hosted history-scan correction + +The first hosted full-history scan failed under the standards wrapper's old +Gitleaks **8.18.4**. Reproducing its legacy rule set with the 8.30.1 engine +produced 211,428 findings: 211,408 AWS-pattern matches in historical sequence +and FASTQ data, 19 generic-key matches in minified bundles/annotation field +names, and one Facebook-pattern match in FASTQ data. For example, the old +AWS rule accepts low-entropy `ACCA`-prefixed DNA and a broader character +alphabet without the current detector's entropy threshold; the +annotation matches are schema labels (`associated_organism`, etc.). This is +not a reason to exempt the data directory or Git history from scanning. + +The fork-local scanner reusable now uses **Gitleaks 8.30.1**, verified against +upstream's release-asset SHA256 +`551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb`. +The estate baseline still comes from the same pinned standards source. +No repository allowlist or historical fingerprint suppression was added; +working-tree, full-history, shell, Rust and AsciiDoc checks remain gating. +The current engine reports no leaks locally, and a synthetic AWS credential +must still trigger its AWS rule. That sensitivity check is also run in CI. +The local binary came from the npm platform package at 8.30.1-post.3; hosted +CI installs the checksum-verified upstream archive, so hosted verification +remains necessary. No actual credential values were published. + ## Remaining findings / decisions The source report is Hypatia grid run **39982506244**, commit `d69d4f9`. diff --git a/scripts/check-governance-contract.sh b/scripts/check-governance-contract.sh index f12442de..9d46d095 100755 --- a/scripts/check-governance-contract.sh +++ b/scripts/check-governance-contract.sh @@ -143,8 +143,15 @@ fi # These are independent conformance families. An empty K9 corpus must be # shown explicitly; missing semantic evidence must not become a green DEED row. -contains "$caller" 'uses: hyperpolymath/standards/.github/workflows/k9-conformance.yml@c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d' \ - 'K9 validation uses an immutable standards source' +contains "$caller" 'uses: ./.github/workflows/k9-conformance-reusable.yml' \ + 'K9 validation uses the guarded fork-local reusable' +k9_workflow="$(cat .github/workflows/k9-conformance-reusable.yml)" +contains "$k9_workflow" 'ref: c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d' \ + 'the K9 validator and renderer use the immutable standards source' +not_contains "$k9_workflow" 'ref: ${{ job.workflow_sha }}' \ + 'the K9 standards checkout cannot use the caller SHA' +contains "$k9_workflow" 'if [ -f "$RUNNER_TEMP/corpus.log" ]; then' \ + 'optional corpus-log copying cannot fail an empty K9 corpus' contains "$caller" 'fail-on-indeterminate: true' \ 'missing K9 semantic evidence fails closed' contains "$caller" 'needs: [deed, k9, hypatia]' \ @@ -153,8 +160,17 @@ contains "$caller" 'name: k9-grid' 'the K9 artifact remains visible in the PR ve contains "$caller" '${K9_VERDICT:-orange}' 'an absent K9 verdict is indeterminate' secret_workflow="$(cat .github/workflows/secret-scanner.yml)" -contains "$secret_workflow" 'uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d' \ - 'full-history secret scanning uses the audited immutable reusable' +contains "$secret_workflow" 'uses: ./.github/workflows/secret-scanner-reusable.yml' \ + 'full-history secret scanning uses the guarded fork-local reusable' +secret_reusable="$(cat .github/workflows/secret-scanner-reusable.yml)" +contains "$secret_reusable" 'GITLEAKS_VERSION=8.30.1' 'Gitleaks uses the tested current detector' +contains "$secret_reusable" 'SHA256=551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb' \ + 'the scanner binary matches the upstream release-asset digest' +contains "$secret_reusable" 'ref: c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d' \ + 'the estate baseline remains pinned to the audited standards source' +contains "$secret_reusable" 'fetch-depth: 0' 'secret scanning retains full Git history' +not_contains "$secret_reusable" 'ref: ${{ job.workflow_sha }}' \ + 'secret baseline checkout cannot use the caller SHA' not_contains "$secret_workflow" 'secrets:' 'the secret scanner receives no caller secrets' not_contains "$secret_workflow" 'continue-on-error:' 'secret scanner failures cannot be advisory' not_contains "$secret_workflow" 'pull_request_target:' 'secret scanning does not use a privileged PR trigger' diff --git a/scripts/tests/test-governance-contract.sh b/scripts/tests/test-governance-contract.sh index 894fb949..570e9e0a 100644 --- a/scripts/tests/test-governance-contract.sh +++ b/scripts/tests/test-governance-contract.sh @@ -14,7 +14,7 @@ for mutation in sarif k9-pin k9-indeterminate k9-verdict scanner-advisory scanne reset_fixture case "$mutation" in sarif) sed -i "/if: github.event_name != 'pull_request'/d" "$fixture/.github/workflows/hypatia-scan-reusable.yml" ;; - k9-pin) sed -i '/uses: .*k9-conformance.yml@/s/@.*/@main/' "$fixture/.github/workflows/governance.yml" ;; + k9-pin) sed -i 's/ref: c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d/ref: main/' "$fixture/.github/workflows/k9-conformance-reusable.yml" ;; k9-indeterminate) sed -i 's/fail-on-indeterminate: true/fail-on-indeterminate: false/' "$fixture/.github/workflows/governance.yml" ;; k9-verdict) sed -i 's/${K9_VERDICT:-orange}/${K9_VERDICT:-green}/' "$fixture/.github/workflows/governance.yml" ;; scanner-advisory) echo ' continue-on-error: true' >> "$fixture/.github/workflows/secret-scanner.yml" ;; diff --git a/scripts/tests/test-k9-log-copy.sh b/scripts/tests/test-k9-log-copy.sh new file mode 100644 index 00000000..ebe1cd0a --- /dev/null +++ b/scripts/tests/test-k9-log-copy.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Exercise the actual optional-log branch from the fork-local K9 workflow. +set -euo pipefail +ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" +fixture=$(mktemp -d) +trap 'rm -rf "$fixture"' EXIT +copy_log=$(awk ' + /^ if \[ -f "\$RUNNER_TEMP\/corpus.log" \]; then/ { capture = 1 } + capture { sub(/^ /, ""); print } + capture && /^fi$/ { exit } +' "$ROOT/.github/workflows/k9-conformance-reusable.yml") +[ -n "$copy_log" ] || { echo 'optional K9 copy branch is missing' >&2; exit 1; } +mkdir -p "$fixture/k9-grid" +RUNNER_TEMP="$fixture" bash -euo pipefail -c "$copy_log" +echo 'ok - empty K9 corpus needs no log' +printf '%s\n' 'fixture validator log' > "$fixture/corpus.log" +RUNNER_TEMP="$fixture" bash -euo pipefail -c "$copy_log" +cmp "$fixture/corpus.log" "$fixture/k9-grid/corpus.log" +echo 'ok - present K9 corpus log is retained' +rm -rf "$fixture/k9-grid" +if RUNNER_TEMP="$fixture" bash -euo pipefail -c "$copy_log" 2>/dev/null; then + echo 'ERROR: failed K9 log copy was hidden' >&2 + exit 1 +fi +echo 'ok - genuine K9 copy errors still fail' diff --git a/scripts/tests/test-secret-scanner.sh b/scripts/tests/test-secret-scanner.sh new file mode 100644 index 00000000..22b0f24e --- /dev/null +++ b/scripts/tests/test-secret-scanner.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Check detector sensitivity after upgrading the scanner, not by suppressing +# old findings. The constructed token is synthetic and never a credential. +set -euo pipefail +scanner=${1:?usage: test-secret-scanner.sh GITLEAKS_BIN CONFIG} +config=$(realpath "${2:?a pinned scanner config is required}") +fixture=$(mktemp -d) +trap 'rm -rf "$fixture"' EXIT +mkdir "$fixture/source" +printf '%s\n' 'ordinary configuration' > "$fixture/source/production.env" +"$scanner" detect --source "$fixture/source" --no-git --redact --no-banner \ + --config "$config" > "$fixture/log" 2>&1 +printf 'AWS_ACCESS_KEY_ID=%s%s\n' AKIA Q7W6E5R2T3Y4U5I6 >> "$fixture/source/production.env" +rc=0 +"$scanner" detect --source "$fixture/source" --no-git --redact --no-banner \ + --config "$config" --exit-code 1 --report-format json --report-path "$fixture/report.json" \ + > "$fixture/log" 2>&1 || rc=$? +[ "$rc" -eq 1 ] || { echo "ERROR: synthetic credential was not rejected (exit $rc)" >&2; exit 1; } +jq -e 'any(.[]; .RuleID == "aws-access-token")' "$fixture/report.json" > /dev/null +echo 'ok - clean input passes; synthetic AWS credential fails with the expected rule' From f99aac296f925f002215f1b6b8d0667ef9ead73b Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:34:37 +0000 Subject: [PATCH 3/7] Address scanner review: redact findings and cover deleted shell credentials Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> Signed-off-by: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> --- .github/workflows/governance.yml | 1 + .github/workflows/secret-scanner-reusable.yml | 140 +++--------------- .../2026-10-23-governance-security.md | 17 +++ scripts/ci/check-shell-secrets.sh | 81 ++++++++++ scripts/tests/test-scanner-boundaries.sh | 86 +++++++++++ 5 files changed, 208 insertions(+), 117 deletions(-) create mode 100644 scripts/ci/check-shell-secrets.sh create mode 100644 scripts/tests/test-scanner-boundaries.sh diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index f7415d31..ce1213c2 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -43,6 +43,7 @@ jobs: bash scripts/tests/test-governance-contract.sh bash scripts/tests/test-ci-logging.sh bash scripts/tests/test-k9-log-copy.sh + bash scripts/tests/test-scanner-boundaries.sh deed: name: DEED syntax diff --git a/.github/workflows/secret-scanner-reusable.yml b/.github/workflows/secret-scanner-reusable.yml index 9191f10f..0f3a0fc7 100644 --- a/.github/workflows/secret-scanner-reusable.yml +++ b/.github/workflows/secret-scanner-reusable.yml @@ -2,7 +2,8 @@ # c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d. Deltas: Gitleaks 8.30.1 # (upstream release-asset SHA256) instead of 8.18.4, and standards config # checkouts pinned explicitly rather than using this caller's workflow SHA. -# A synthetic-credential sensitivity check is added. No local rule +# Synthetic sensitivity checks, redacted custom-scanner output, fail-closed +# AsciiDoc status handling and custom shell history coverage are added. No local rule # suppressions, history exclusions or advisory failure overrides are added. # This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 @@ -488,21 +489,24 @@ jobs: --report-format json \ --report-path "$REPORT" \ "${config_args[@]}" + scan_status=$? set -e - - # `grep` exits 1 when it matches nothing, which under `set -e` + - # `pipefail` aborts the step with status 1 โ€” i.e. a repo whose docs are - # CLEAN would fail the gate. Caught by red-teaming the clean case; - # pipefail is disabled around the count for exactly this reason. - count=0 - if [ -s "$REPORT" ]; then - set +o pipefail - count=$(grep -o '"RuleID"' "$REPORT" 2>/dev/null | wc -l | tr -d ' ') - set -o pipefail - [ -n "$count" ] || count=0 + if [ "$scan_status" -gt 1 ]; then + echo "::error::AsciiDoc scanner failed (exit $scan_status); not a clean scan." + exit "$scan_status" + fi + if ! jq -e 'type == "array" and all(.[]; type == "object" and (.RuleID | type == "string"))' "$REPORT" >/dev/null 2>&1; then + echo '::error::AsciiDoc scanner report is absent or invalid; refusing a clean verdict.' + exit 1 fi + count=$(jq 'length' "$REPORT") + if [ "$count" -eq 0 ]; then + if [ "$scan_status" -ne 0 ]; then + echo '::error::AsciiDoc scanner failed without findings; refusing a clean verdict.' + exit "$scan_status" + fi echo "No leaks found in AsciiDoc files." exit 0 fi @@ -669,12 +673,12 @@ jobs: new_scope="$(printf '%s\n' "$hits" | grep -vE '^\./src/' || [ $? -eq 1 ])" if [ -n "$old_scope" ]; then - printf '%s\n' "$old_scope" + printf '%s\n' "$old_scope" | awk -F: '{print $1 ":" $2 ": [REDACTED]"}' echo "WARNING: Potential hardcoded secret found matching: $pattern" blocking=1 fi if [ -n "$new_scope" ]; then - printf '%s\n' "$new_scope" + printf '%s\n' "$new_scope" | awk -F: '{print $1 ":" $2 ": [REDACTED]"}' if [[ "$TODAY" < "$ENFORCE_RUST_WIDE_SCAN_FROM" ]]; then echo "::warning::Potential hardcoded secret outside ./src matching:" \ "$pattern โ€” becomes BLOCKING on $ENFORCE_RUST_WIDE_SCAN_FROM (today is $TODAY)." @@ -722,107 +726,9 @@ jobs: contents: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false - - name: Check for hardcoded secrets in shell scripts - run: | - # Patterns: an `export FOO=` or `FOO=` with a quoted literal of meaningful length. - # Restricted to *_TOKEN / *_KEY / *_SECRET / PASSWORD to keep false-positives low. - PATTERNS=( - '(export[[:space:]]+)?[A-Z_]*TOKEN[A-Z_]*=["'"'"'][A-Za-z0-9_./+=-]{20,}["'"'"']' - '(export[[:space:]]+)?[A-Z_]*API_KEY[A-Z_]*=["'"'"'][A-Za-z0-9_./+=-]{20,}["'"'"']' - '(export[[:space:]]+)?[A-Z_]*SECRET[A-Z_]*=["'"'"'][A-Za-z0-9_./+=-]{16,}["'"'"']' - '(export[[:space:]]+)?PASSWORD=["'"'"'][^"'"'"']{6,}["'"'"']' - ) - - # Inline pragma patterns โ€” suppress a hit when found on the same or - # immediately preceding line. - PRAGMA_RE='(scanner-allow:[[:space:]]*shell-secrets|hypatia:[[:space:]]*allow[[:space:]]+security_errors/secret_detected)' - - # Param-expansion RHS pattern โ€” assignments whose value is a variable - # reference rather than a literal are never real secrets. - # Matches: ="$VAR" ="${VAR}" ="${VAR:-โ€ฆ}" ="${VAR:?โ€ฆ}" ='${VAR}' =$VAR - PARAM_EXPANSION_RE='=['"'"'"'"'"']?\$\{?[A-Za-z_][A-Za-z0-9_]*(:[?-][^}]*)?\}?['"'"'"'"'"']?[[:space:]]*(#.*)?$' - - # Load per-repo ignore globs from .shell-secrets-ignore if present. - IGNORE_GLOBS=() - if [[ -f .shell-secrets-ignore ]]; then - while IFS= read -r line || [[ -n "$line" ]]; do - # Skip blank lines and comments - [[ -z "$line" || "$line" == \#* ]] && continue - IGNORE_GLOBS+=("$line") - done < .shell-secrets-ignore - fi - - # is_ignored โ€” returns 0 (true) if path matches any ignore glob. - is_ignored() { - local path="$1" - for glob in "${IGNORE_GLOBS[@]}"; do - # Use bash pattern matching; strip leading ./ for consistency. - local stripped="${path#./}" - # shellcheck disable=SC2254 - if [[ "$stripped" == $glob ]]; then - return 0 - fi - done - return 1 - } - - found=0 - for pattern in "${PATTERNS[@]}"; do - # Collect raw matches: filepath:lineno:content - while IFS= read -r raw_match; do - [[ -z "$raw_match" ]] && continue - - # Parse filepath and line number from grep -n output (file:line:content). - filepath="${raw_match%%:*}" - rest="${raw_match#*:}" - lineno="${rest%%:*}" - content="${rest#*:}" - - # Exemption 1: .shell-secrets-ignore glob match on the file path. - if is_ignored "$filepath"; then - echo " [skip] $filepath:$lineno โ€” matched .shell-secrets-ignore glob" - continue - fi - - # Exemption 2: param-expansion RHS โ€” value is a variable reference, not a literal. - if echo "$content" | grep -qE "$PARAM_EXPANSION_RE"; then - echo " [skip] $filepath:$lineno โ€” param-expansion (not a literal value)" - continue - fi - - # Exemption 3: comment line โ€” leading non-whitespace char is '#'. - # Comment lines cannot hold real secrets; they only document patterns. - if echo "$content" | grep -qE '^[[:space:]]*#'; then - echo " [skip] $filepath:$lineno โ€” comment line (not executable)" - continue - fi - - # Exemption 5: inline pragma on same line. - if echo "$content" | grep -qE "$PRAGMA_RE"; then - echo " [skip] $filepath:$lineno โ€” inline pragma on same line" - continue - fi - - # Exemption 6: inline pragma on the immediately preceding line. - if [[ "$lineno" -gt 1 ]]; then - prev_line=$(sed -n "$((lineno - 1))p" "$filepath" 2>/dev/null) - if echo "$prev_line" | grep -qE "$PRAGMA_RE"; then - echo " [skip] $filepath:$lineno โ€” pragma on preceding line" - continue - fi - fi - - echo "::warning::$filepath:$lineno: $content" - echo " Potential hardcoded secret matching pattern: $pattern" - found=1 - - done < <(grep -rnE --include='*.sh' --include='*.bash' \ - --exclude-dir='.git' --exclude-dir='node_modules' --exclude-dir='target' \ - "$pattern" . 2>/dev/null || [ $? -eq 1 ]) - done - - if [ $found -eq 1 ]; then - echo "::error::Hardcoded secret detected in a shell script. Read the value from the environment (e.g. \"\${MY_TOKEN:?}\") and supply it via a repo or org secret." - exit 1 - fi + - name: Check for hardcoded secrets in shell scripts and Git history + run: bash scripts/ci/check-shell-secrets.sh diff --git a/docs/release-notes/2026-10-23-governance-security.md b/docs/release-notes/2026-10-23-governance-security.md index f2f7a1e1..d08db0c2 100644 --- a/docs/release-notes/2026-10-23-governance-security.md +++ b/docs/release-notes/2026-10-23-governance-security.md @@ -93,6 +93,23 @@ The local binary came from the npm platform package at 8.30.1-post.3; hosted CI installs the checksum-verified upstream archive, so hosted verification remains necessary. No actual credential values were published. +## Scanner review findings + +Codex review on #243 identified four inherited scanner defects. Rust and +shell findings now report locations without source values; the regression +tests assert the synthetic credential is absent from logs. The AsciiDoc +mirror preserves the scanner exit status, validates the report, and fails +on runtime errors or missing/invalid evidence rather than reporting clean. +The custom shell rules now scan every unique historical shell blob as well +as the working tree, so a deleted credential remains detectable independently +of Gitleaks' built-in rules. Both current and historical findings are blocking. + +`bash scripts/tests/test-scanner-boundaries.sh` exercises clean input, a +working-tree shell credential, a committed-then-deleted shell credential, +Rust findings in both scopes, and six AsciiDoc result/error modes. Fixture +repositories are temporary and do not change application Git history. +The custom shell scan also passes on this repository's actual history. + ## Remaining findings / decisions The source report is Hypatia grid run **39982506244**, commit `d69d4f9`. diff --git a/scripts/ci/check-shell-secrets.sh b/scripts/ci/check-shell-secrets.sh new file mode 100644 index 00000000..a0ad9bf7 --- /dev/null +++ b/scripts/ci/check-shell-secrets.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Apply the same literal-assignment rules to the working tree and every unique +# historical shell blob. Never print matching source or credential values. +set -euo pipefail +scratch=$(mktemp -d) +trap 'rm -rf "$scratch"' EXIT +patterns=( + "(export[[:space:]]+)?[A-Z_]*TOKEN[A-Z_]*=[\"'][A-Za-z0-9_./+=-]{20,}[\"']" + "(export[[:space:]]+)?[A-Z_]*API_KEY[A-Z_]*=[\"'][A-Za-z0-9_./+=-]{20,}[\"']" + "(export[[:space:]]+)?[A-Z_]*SECRET[A-Z_]*=[\"'][A-Za-z0-9_./+=-]{16,}[\"']" + "(export[[:space:]]+)?PASSWORD=[\"'][^\"']{6,}[\"']" +) +pragma='(scanner-allow:[[:space:]]*shell-secrets|hypatia:[[:space:]]*allow[[:space:]]+security_errors/secret_detected)' +parameter_rhs=$(cat <<'REGEX' +=["']?\$\{?[A-Za-z_][A-Za-z0-9_]*(:[?-][^}]*)?\}?["']?[[:space:]]*(#.*)?$ +REGEX +) +ignore=() +if [ -f .shell-secrets-ignore ]; then + while IFS= read -r line || [ -n "$line" ]; do + [[ -z "$line" || "$line" == \#* ]] || ignore+=("$line") + done < .shell-secrets-ignore +fi +ignored() { + local path=${1#./} glob + for glob in "${ignore[@]}"; do [[ "$path" == $glob ]] && return 0; done + return 1 +} +found=0 +scan_file() { + local file=$1 path=$2 revision=$3 pattern rc line number content previous + ignored "$path" && return 0 + for pattern in "${patterns[@]}"; do + rc=0 + grep -nE "$pattern" "$file" > "$scratch/matches" || rc=$? + if [ "$rc" -gt 1 ]; then echo 'ERROR: shell-secret matcher failed' >&2; return "$rc"; fi + while IFS= read -r line; do + number=${line%%:*}; content=${line#*:} + # PASSWORD allows arbitrary characters, including variable-only RHSs. + [[ "$content" =~ $parameter_rhs ]] && continue + [[ "$content" =~ ^[[:space:]]*# ]] && continue + [[ "$content" =~ $pragma ]] && continue + if [ "$number" -gt 1 ]; then + previous=$(sed -n "$((number - 1))p" "$file") + [[ "$previous" =~ $pragma ]] && continue + fi + printf 'Potential shell credential at %q:%s (revision %s; content redacted)\n' "$path" "$number" "$revision" + found=1 + done < "$scratch/matches" + done +} +find . -type d \( -name .git -o -name node_modules -o -name target \) -prune -o \ + -type f \( -name '*.sh' -o -name '*.bash' \) -print0 > "$scratch/files" +while IFS= read -r -d '' path; do scan_file "$path" "$path" working-tree; done < "$scratch/files" +if [ "$(git rev-parse --is-shallow-repository)" != false ]; then + echo 'ERROR: refusing a shallow custom shell-history scan' >&2 + exit 1 +fi +git rev-list HEAD > "$scratch/commits" +declare -A seen=() +while IFS= read -r revision; do + git ls-tree -r -z "$revision" > "$scratch/tree" + while IFS= read -r -d '' entry; do + header=${entry%%$'\t'*}; path=${entry#*$'\t'} + read -r mode type oid <<< "$header" + [ "$type" = blob ] || continue + case "$path" in *.sh|*.bash) ;; *) continue ;; esac + case "/$path/" in */node_modules/*|*/target/*) continue ;; esac + key="$oid:$path" + [ -z "${seen[$key]+x}" ] || continue + seen[$key]=1 + git cat-file blob "$oid" > "$scratch/blob" + scan_file "$scratch/blob" "$path" "$revision" + done < "$scratch/tree" +done < "$scratch/commits" +if [ "$found" -ne 0 ]; then + echo 'ERROR: hardcoded shell credentials detected; use environment variables.' >&2 + exit 1 +fi +echo 'No hardcoded shell credentials found in the working tree or Git history.' diff --git a/scripts/tests/test-scanner-boundaries.sh b/scripts/tests/test-scanner-boundaries.sh new file mode 100644 index 00000000..688c27bf --- /dev/null +++ b/scripts/tests/test-scanner-boundaries.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Run the actual workflow blocks on synthetic inputs, capturing all output. +set -euo pipefail +ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" +fixture=$(mktemp -d) +trap 'rm -rf "$fixture"' EXIT +extract() { + awk -v name="$1" ' + $0 == " - name: " name { found=1; next } + found && (/^ - name: / || /^ [a-zA-Z_-]+:/) { exit } + found && /^ run: \|/ { script=1; next } + script && /^ / { sub(/^ /, ""); print } + ' "$ROOT/.github/workflows/secret-scanner-reusable.yml" +} +extract 'Check for hardcoded secrets in Rust' > "$fixture/rust.sh" +extract 'Gitleaks secret scan โ€” AsciiDoc mirror (gating)' > "$fixture/adoc.sh" +[ -s "$fixture/rust.sh" ] && [ -s "$fixture/adoc.sh" ] +mkdir -p "$fixture/repo/src" "$fixture/tmp" +# This is a disposable fixture, not a branch in the application repository. +git init -q -b arena/b1571410-metamanifold-webui "$fixture/repo" +git -C "$fixture/repo" config user.name 'Scanner test' +git -C "$fixture/repo" config user.email 'scanner-test@example.invalid' +printf '%s\n' 'PASSWORD="${LONG_PASSWORD:?}"' > "$fixture/repo/env.sh" +printf '%s\n' 'initial fixture' > "$fixture/repo/README" +git -C "$fixture/repo" add README env.sh +git -C "$fixture/repo" commit -qm 'clean fixture' +(cd "$fixture/repo" && bash "$ROOT/scripts/ci/check-shell-secrets.sh") > "$fixture/log" 2>&1 +# Construct a dummy value at runtime; never commit a credential-shaped literal +# to the application repository or publish the fixture's matching source. +dummy="synthetic-$(printf '%s' 'credential-fixture-123456789')" +printf 'SERVICE_TOKEN="%s"\n' "$dummy" > "$fixture/repo/removed.sh" +rc=0 +(cd "$fixture/repo" && bash "$ROOT/scripts/ci/check-shell-secrets.sh") > "$fixture/log" 2>&1 || rc=$? +[ "$rc" -eq 1 ] && ! grep -Fq "$dummy" "$fixture/log" +echo 'ok - working-tree shell credential blocks without value disclosure' +git -C "$fixture/repo" add removed.sh +git -C "$fixture/repo" commit -qm 'synthetic historical credential' +git -C "$fixture/repo" rm -q removed.sh +git -C "$fixture/repo" commit -qm 'remove fixture credential' +rc=0 +(cd "$fixture/repo" && bash "$ROOT/scripts/ci/check-shell-secrets.sh") > "$fixture/log" 2>&1 || rc=$? +[ "$rc" -eq 1 ] && ! grep -Fq "$dummy" "$fixture/log" +echo 'ok - deleted historical shell credential still blocks without disclosure' +printf '[package]\nname="fixture"\nversion="0.1.0"\n' > "$fixture/repo/Cargo.toml" +for path in src/lib.rs crates/example.rs; do + mkdir -p "$fixture/repo/$(dirname "$path")" + printf 'const AUTH_TOKEN: &str = "%s";\n' "$dummy" > "$fixture/repo/$path" +done +rc=0 +(cd "$fixture/repo" && ENFORCE_RUST_WIDE_SCAN_FROM=2026-08-21 bash -eo pipefail "$fixture/rust.sh") > "$fixture/log" 2>&1 || rc=$? +[ "$rc" -eq 1 ] && ! grep -Fq "$dummy" "$fixture/log" +grep -q 'src/lib.rs:1: \[REDACTED\]' "$fixture/log" +grep -q 'crates/example.rs:1: \[REDACTED\]' "$fixture/log" +echo 'ok - Rust findings inside and outside src block without value disclosure' +printf '%s\n' 'ordinary documentation' > "$fixture/repo/sample.adoc" +printf '%s\n' '[extend]' 'useDefault = true' > "$fixture/repo/.gitleaks-estate.toml" +cat > "$fixture/tmp/gitleaks" <<'STUB' +#!/usr/bin/env bash +set -eu +report='' +while [ "$#" -gt 0 ]; do + if [ "$1" = --report-path ]; then report=$2; shift; fi + shift +done +rm -f "$report" +case "$SCAN_MODE" in + clean) printf '[]' > "$report"; exit 0 ;; + error) printf '[]' > "$report"; exit 2 ;; + nonzero-empty) printf '[]' > "$report"; exit 1 ;; + missing) exit 0 ;; + invalid) printf 'invalid JSON' > "$report"; exit 0 ;; + finding) printf '[{"RuleID":"fixture-rule","File":"sample.adoc.txt"}]' > "$report"; exit 1 ;; +esac +STUB +chmod +x "$fixture/tmp/gitleaks" +for mode in clean error nonzero-empty missing invalid finding; do + rc=0 + (cd "$fixture/repo" && SCAN_MODE="$mode" RUNNER_TEMP="$fixture/tmp" ADOC_TODAY=2026-10-23 \ + bash -eo pipefail "$fixture/adoc.sh") > "$fixture/log" 2>&1 || rc=$? + case "$mode" in + clean) [ "$rc" -eq 0 ] ;; + *) [ "$rc" -ne 0 ] ;; + esac + echo "ok - AsciiDoc mode $mode returns $rc" +done From dfcad4f6b39be73e5fffbcb73b683b502220f2d9 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:35:57 +0000 Subject: [PATCH 4/7] Match variable-only shell exemptions per assignment, including historical test fixtures Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> Signed-off-by: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> --- scripts/ci/check-shell-secrets.sh | 11 +++++++---- scripts/tests/test-scanner-boundaries.sh | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/scripts/ci/check-shell-secrets.sh b/scripts/ci/check-shell-secrets.sh index a0ad9bf7..a1da7f65 100644 --- a/scripts/ci/check-shell-secrets.sh +++ b/scripts/ci/check-shell-secrets.sh @@ -29,18 +29,21 @@ ignored() { } found=0 scan_file() { - local file=$1 path=$2 revision=$3 pattern rc line number content previous + local file=$1 path=$2 revision=$3 pattern rc line number content source_line previous ignored "$path" && return 0 for pattern in "${patterns[@]}"; do rc=0 - grep -nE "$pattern" "$file" > "$scratch/matches" || rc=$? + grep -noE "$pattern" "$file" > "$scratch/matches" || rc=$? if [ "$rc" -gt 1 ]; then echo 'ERROR: shell-secret matcher failed' >&2; return "$rc"; fi while IFS= read -r line; do number=${line%%:*}; content=${line#*:} # PASSWORD allows arbitrary characters, including variable-only RHSs. [[ "$content" =~ $parameter_rhs ]] && continue - [[ "$content" =~ ^[[:space:]]*# ]] && continue - [[ "$content" =~ $pragma ]] && continue + # Check exemptions against each assignment separately; a variable on + # one side of `;` cannot hide a literal credential later on the line. + source_line=$(sed -n "${number}p" "$file") + [[ "$source_line" =~ ^[[:space:]]*# ]] && continue + [[ "$source_line" =~ $pragma ]] && continue if [ "$number" -gt 1 ]; then previous=$(sed -n "$((number - 1))p" "$file") [[ "$previous" =~ $pragma ]] && continue diff --git a/scripts/tests/test-scanner-boundaries.sh b/scripts/tests/test-scanner-boundaries.sh index 688c27bf..e6a7edc5 100644 --- a/scripts/tests/test-scanner-boundaries.sh +++ b/scripts/tests/test-scanner-boundaries.sh @@ -29,7 +29,7 @@ git -C "$fixture/repo" commit -qm 'clean fixture' # Construct a dummy value at runtime; never commit a credential-shaped literal # to the application repository or publish the fixture's matching source. dummy="synthetic-$(printf '%s' 'credential-fixture-123456789')" -printf 'SERVICE_TOKEN="%s"\n' "$dummy" > "$fixture/repo/removed.sh" +printf 'PASSWORD="${LONG_PASSWORD:?}"; SERVICE_TOKEN="%s"\n' "$dummy" > "$fixture/repo/removed.sh" rc=0 (cd "$fixture/repo" && bash "$ROOT/scripts/ci/check-shell-secrets.sh") > "$fixture/log" 2>&1 || rc=$? [ "$rc" -eq 1 ] && ! grep -Fq "$dummy" "$fixture/log" From 13397fef70f46dbb133f2211c0d56b467d20e09e Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:37:51 +0000 Subject: [PATCH 5/7] Address Sonar shell findings and restrict scanner downloads to HTTPS Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> Signed-off-by: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> --- .github/workflows/secret-scanner-reusable.yml | 2 +- scripts/ci/check-shell-secrets.sh | 18 ++++++++--------- scripts/tests/test-ci-logging.sh | 6 +++--- scripts/tests/test-governance-contract.sh | 1 + scripts/tests/test-k9-log-copy.sh | 2 +- scripts/tests/test-scanner-boundaries.sh | 20 ++++++++++--------- scripts/tests/test-secret-scanner.sh | 2 +- 7 files changed, 27 insertions(+), 24 deletions(-) diff --git a/.github/workflows/secret-scanner-reusable.yml b/.github/workflows/secret-scanner-reusable.yml index 0f3a0fc7..e9462860 100644 --- a/.github/workflows/secret-scanner-reusable.yml +++ b/.github/workflows/secret-scanner-reusable.yml @@ -148,7 +148,7 @@ jobs: GITLEAKS_VERSION=8.30.1 SHA256=551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb cd "$RUNNER_TEMP" - curl -fsSL -o gitleaks.tar.gz \ + curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 -o gitleaks.tar.gz \ "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" echo "${SHA256} gitleaks.tar.gz" | sha256sum -c - tar -xzf gitleaks.tar.gz gitleaks diff --git a/scripts/ci/check-shell-secrets.sh b/scripts/ci/check-shell-secrets.sh index a1da7f65..78cf9cbd 100644 --- a/scripts/ci/check-shell-secrets.sh +++ b/scripts/ci/check-shell-secrets.sh @@ -17,8 +17,8 @@ parameter_rhs=$(cat <<'REGEX' REGEX ) ignore=() -if [ -f .shell-secrets-ignore ]; then - while IFS= read -r line || [ -n "$line" ]; do +if [[ -f .shell-secrets-ignore ]]; then + while IFS= read -r line || [[ -n "$line" ]]; do [[ -z "$line" || "$line" == \#* ]] || ignore+=("$line") done < .shell-secrets-ignore fi @@ -34,7 +34,7 @@ scan_file() { for pattern in "${patterns[@]}"; do rc=0 grep -noE "$pattern" "$file" > "$scratch/matches" || rc=$? - if [ "$rc" -gt 1 ]; then echo 'ERROR: shell-secret matcher failed' >&2; return "$rc"; fi + if [[ "$rc" -gt 1 ]]; then echo 'ERROR: shell-secret matcher failed' >&2; return "$rc"; fi while IFS= read -r line; do number=${line%%:*}; content=${line#*:} # PASSWORD allows arbitrary characters, including variable-only RHSs. @@ -44,7 +44,7 @@ scan_file() { source_line=$(sed -n "${number}p" "$file") [[ "$source_line" =~ ^[[:space:]]*# ]] && continue [[ "$source_line" =~ $pragma ]] && continue - if [ "$number" -gt 1 ]; then + if [[ "$number" -gt 1 ]]; then previous=$(sed -n "$((number - 1))p" "$file") [[ "$previous" =~ $pragma ]] && continue fi @@ -56,7 +56,7 @@ scan_file() { find . -type d \( -name .git -o -name node_modules -o -name target \) -prune -o \ -type f \( -name '*.sh' -o -name '*.bash' \) -print0 > "$scratch/files" while IFS= read -r -d '' path; do scan_file "$path" "$path" working-tree; done < "$scratch/files" -if [ "$(git rev-parse --is-shallow-repository)" != false ]; then +if [[ "$(git rev-parse --is-shallow-repository)" != false ]]; then echo 'ERROR: refusing a shallow custom shell-history scan' >&2 exit 1 fi @@ -67,17 +67,17 @@ while IFS= read -r revision; do while IFS= read -r -d '' entry; do header=${entry%%$'\t'*}; path=${entry#*$'\t'} read -r mode type oid <<< "$header" - [ "$type" = blob ] || continue + [[ "$type" = blob ]] || continue case "$path" in *.sh|*.bash) ;; *) continue ;; esac - case "/$path/" in */node_modules/*|*/target/*) continue ;; esac + case "/$path/" in */node_modules/*|*/target/*) continue ;; *) ;; esac key="$oid:$path" - [ -z "${seen[$key]+x}" ] || continue + [[ -z "${seen[$key]+x}" ]] || continue seen[$key]=1 git cat-file blob "$oid" > "$scratch/blob" scan_file "$scratch/blob" "$path" "$revision" done < "$scratch/tree" done < "$scratch/commits" -if [ "$found" -ne 0 ]; then +if [[ "$found" -ne 0 ]]; then echo 'ERROR: hardcoded shell credentials detected; use environment variables.' >&2 exit 1 fi diff --git a/scripts/tests/test-ci-logging.sh b/scripts/tests/test-ci-logging.sh index e7b19a10..a6b636b5 100644 --- a/scripts/tests/test-ci-logging.sh +++ b/scripts/tests/test-ci-logging.sh @@ -5,7 +5,7 @@ set -euo pipefail ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" workflow="$ROOT/.github/workflows/ci.yml" count=$(grep -Fc 'statuses=("${PIPESTATUS[@]}")' "$workflow") -[ "$count" -eq 2 ] || { echo 'expected both Julia pipelines to preserve all exits' >&2; exit 1; } +[[ "$count" -eq 2 ]] || { echo 'expected both Julia pipelines to preserve all exits' >&2; exit 1; } selection=$(awk ' /statuses=\("\$\{PIPESTATUS\[@\]\}"\)/ { capture = 1 } capture { sub(/^ /, ""); print } @@ -17,7 +17,7 @@ second=$(awk ' capture { sub(/^ /, ""); print } capture && /then status=/ { exit } ' "$workflow") -[ "$selection" = "$second" ] || { echo 'Julia exit-selection blocks differ' >&2; exit 1; } +[[ "$selection" = "$second" ]] || { echo 'Julia exit-selection blocks differ' >&2; exit 1; } for spec in '0 0 0' '42 0 42' '0 23 23' '42 23 42'; do read -r producer logger expected <<< "$spec" rc=0 @@ -27,7 +27,7 @@ for spec in '0 0 0' '42 0 42' '0 23 23' '42 23 42'; do '"$selection"' exit "$status" ' test-ci-logging "$producer" "$logger" || rc=$? - [ "$rc" -eq "$expected" ] || { + [[ "$rc" -eq "$expected" ]] || { echo "producer=$producer logger=$logger returned $rc, expected $expected" >&2 exit 1 } diff --git a/scripts/tests/test-governance-contract.sh b/scripts/tests/test-governance-contract.sh index 570e9e0a..ab184e55 100644 --- a/scripts/tests/test-governance-contract.sh +++ b/scripts/tests/test-governance-contract.sh @@ -21,6 +21,7 @@ for mutation in sarif k9-pin k9-indeterminate k9-verdict scanner-advisory scanne scanner-secrets) echo ' secrets: inherit' >> "$fixture/.github/workflows/secret-scanner.yml" ;; scanner-trigger) sed -i 's/^ pull_request:/ pull_request_target:/' "$fixture/.github/workflows/secret-scanner.yml" ;; scorecard-trigger) sed -i '/^on:/a\ pull_request:' "$fixture/.github/workflows/scorecard.yml" ;; + *) echo "ERROR: unknown mutation $mutation" >&2; exit 1 ;; esac if bash "$fixture/scripts/check-governance-contract.sh" > "$fixture/log" 2>&1; then echo "ERROR: governance contract accepted mutation $mutation" >&2 diff --git a/scripts/tests/test-k9-log-copy.sh b/scripts/tests/test-k9-log-copy.sh index ebe1cd0a..a008a3ec 100644 --- a/scripts/tests/test-k9-log-copy.sh +++ b/scripts/tests/test-k9-log-copy.sh @@ -10,7 +10,7 @@ copy_log=$(awk ' capture { sub(/^ /, ""); print } capture && /^fi$/ { exit } ' "$ROOT/.github/workflows/k9-conformance-reusable.yml") -[ -n "$copy_log" ] || { echo 'optional K9 copy branch is missing' >&2; exit 1; } +[[ -n "$copy_log" ]] || { echo 'optional K9 copy branch is missing' >&2; exit 1; } mkdir -p "$fixture/k9-grid" RUNNER_TEMP="$fixture" bash -euo pipefail -c "$copy_log" echo 'ok - empty K9 corpus needs no log' diff --git a/scripts/tests/test-scanner-boundaries.sh b/scripts/tests/test-scanner-boundaries.sh index e6a7edc5..e1db046c 100644 --- a/scripts/tests/test-scanner-boundaries.sh +++ b/scripts/tests/test-scanner-boundaries.sh @@ -6,7 +6,8 @@ ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" fixture=$(mktemp -d) trap 'rm -rf "$fixture"' EXIT extract() { - awk -v name="$1" ' + local name=$1 + awk -v name="$name" ' $0 == " - name: " name { found=1; next } found && (/^ - name: / || /^ [a-zA-Z_-]+:/) { exit } found && /^ run: \|/ { script=1; next } @@ -15,7 +16,7 @@ extract() { } extract 'Check for hardcoded secrets in Rust' > "$fixture/rust.sh" extract 'Gitleaks secret scan โ€” AsciiDoc mirror (gating)' > "$fixture/adoc.sh" -[ -s "$fixture/rust.sh" ] && [ -s "$fixture/adoc.sh" ] +[[ -s "$fixture/rust.sh" ]] && [[ -s "$fixture/adoc.sh" ]] mkdir -p "$fixture/repo/src" "$fixture/tmp" # This is a disposable fixture, not a branch in the application repository. git init -q -b arena/b1571410-metamanifold-webui "$fixture/repo" @@ -32,7 +33,7 @@ dummy="synthetic-$(printf '%s' 'credential-fixture-123456789')" printf 'PASSWORD="${LONG_PASSWORD:?}"; SERVICE_TOKEN="%s"\n' "$dummy" > "$fixture/repo/removed.sh" rc=0 (cd "$fixture/repo" && bash "$ROOT/scripts/ci/check-shell-secrets.sh") > "$fixture/log" 2>&1 || rc=$? -[ "$rc" -eq 1 ] && ! grep -Fq "$dummy" "$fixture/log" +[[ "$rc" -eq 1 ]] && ! grep -Fq "$dummy" "$fixture/log" echo 'ok - working-tree shell credential blocks without value disclosure' git -C "$fixture/repo" add removed.sh git -C "$fixture/repo" commit -qm 'synthetic historical credential' @@ -40,7 +41,7 @@ git -C "$fixture/repo" rm -q removed.sh git -C "$fixture/repo" commit -qm 'remove fixture credential' rc=0 (cd "$fixture/repo" && bash "$ROOT/scripts/ci/check-shell-secrets.sh") > "$fixture/log" 2>&1 || rc=$? -[ "$rc" -eq 1 ] && ! grep -Fq "$dummy" "$fixture/log" +[[ "$rc" -eq 1 ]] && ! grep -Fq "$dummy" "$fixture/log" echo 'ok - deleted historical shell credential still blocks without disclosure' printf '[package]\nname="fixture"\nversion="0.1.0"\n' > "$fixture/repo/Cargo.toml" for path in src/lib.rs crates/example.rs; do @@ -49,7 +50,7 @@ for path in src/lib.rs crates/example.rs; do done rc=0 (cd "$fixture/repo" && ENFORCE_RUST_WIDE_SCAN_FROM=2026-08-21 bash -eo pipefail "$fixture/rust.sh") > "$fixture/log" 2>&1 || rc=$? -[ "$rc" -eq 1 ] && ! grep -Fq "$dummy" "$fixture/log" +[[ "$rc" -eq 1 ]] && ! grep -Fq "$dummy" "$fixture/log" grep -q 'src/lib.rs:1: \[REDACTED\]' "$fixture/log" grep -q 'crates/example.rs:1: \[REDACTED\]' "$fixture/log" echo 'ok - Rust findings inside and outside src block without value disclosure' @@ -59,8 +60,8 @@ cat > "$fixture/tmp/gitleaks" <<'STUB' #!/usr/bin/env bash set -eu report='' -while [ "$#" -gt 0 ]; do - if [ "$1" = --report-path ]; then report=$2; shift; fi +while [[ "$#" -gt 0 ]]; do + if [[ "$1" = --report-path ]]; then report=$2; shift; fi shift done rm -f "$report" @@ -71,6 +72,7 @@ case "$SCAN_MODE" in missing) exit 0 ;; invalid) printf 'invalid JSON' > "$report"; exit 0 ;; finding) printf '[{"RuleID":"fixture-rule","File":"sample.adoc.txt"}]' > "$report"; exit 1 ;; + *) exit 99 ;; esac STUB chmod +x "$fixture/tmp/gitleaks" @@ -79,8 +81,8 @@ for mode in clean error nonzero-empty missing invalid finding; do (cd "$fixture/repo" && SCAN_MODE="$mode" RUNNER_TEMP="$fixture/tmp" ADOC_TODAY=2026-10-23 \ bash -eo pipefail "$fixture/adoc.sh") > "$fixture/log" 2>&1 || rc=$? case "$mode" in - clean) [ "$rc" -eq 0 ] ;; - *) [ "$rc" -ne 0 ] ;; + clean) [[ "$rc" -eq 0 ]] ;; + *) [[ "$rc" -ne 0 ]] ;; esac echo "ok - AsciiDoc mode $mode returns $rc" done diff --git a/scripts/tests/test-secret-scanner.sh b/scripts/tests/test-secret-scanner.sh index 22b0f24e..63da2e8f 100644 --- a/scripts/tests/test-secret-scanner.sh +++ b/scripts/tests/test-secret-scanner.sh @@ -16,6 +16,6 @@ rc=0 "$scanner" detect --source "$fixture/source" --no-git --redact --no-banner \ --config "$config" --exit-code 1 --report-format json --report-path "$fixture/report.json" \ > "$fixture/log" 2>&1 || rc=$? -[ "$rc" -eq 1 ] || { echo "ERROR: synthetic credential was not rejected (exit $rc)" >&2; exit 1; } +[[ "$rc" -eq 1 ]] || { echo "ERROR: synthetic credential was not rejected (exit $rc)" >&2; exit 1; } jq -e 'any(.[]; .RuleID == "aws-access-token")' "$fixture/report.json" > /dev/null echo 'ok - clean input passes; synthetic AWS credential fails with the expected rule' From 3602fbc3d64e1214483592d7c199b4abd3ba9167 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:46:15 +0000 Subject: [PATCH 6/7] fix(security): validate data interpolated into R source Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> Signed-off-by: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> --- .../2026-10-23-governance-security.md | 40 +++++++++++++++++++ src/analysis/analysis.jl | 26 +++++++++++- src/core/provenance.jl | 9 ++++- test/unit/test_permanova.jl | 16 ++++++++ test/unit/test_provenance.jl | 9 +++++ 5 files changed, 98 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/2026-10-23-governance-security.md b/docs/release-notes/2026-10-23-governance-security.md index d08db0c2..cc126aa0 100644 --- a/docs/release-notes/2026-10-23-governance-security.md +++ b/docs/release-notes/2026-10-23-governance-security.md @@ -147,3 +147,43 @@ It is historical evidence, not a freshly executed scan of this change. reconciliation after the next default-branch scan. - **CodeRabbit:** review capacity and Autofix billing notices require an account decision; this change neither purchases credits nor activates billing. + + +## PR #228 supersession and R source boundaries + +The linked CodeRabbit Autofix response is a billing notice, not a code finding; +no credits or paid usage have been enabled. The recorded conflict-resolution +commit `77376ef18b06e01dd2537060064e78e090ceb68e` in #244 has the same tree as +main `d69d4f9910a143911499c596e0648932c834c42e` +(`8f91c388d973c6c4548c1d70d0d7af2e37c622c8`), independently verified with +`git diff --exit-code`. Re-merging #228 would not add its intended fixes. Keep +main's streamed bounded integer-count loading, strict I-JSON validation and +unplaced-read mass semantics rather than restoring the stale versions. + +The remaining leaf-PD inconsistency is real, but its implementations and +regressions are already in the **unmerged** #242 and #244. They are not in main +and are not duplicated here. Main's CI run `39982506136` passed. Replacement +#242's integration run `39986326202` subsequently passed all six jobs, including +Julia, Agda and both occupancy checks; its other reported checks were also green +at head `ed24c9c1708734fbc483f78f2bcca22ddb8a3074`. #244's Julia job was still +running at review time. Neither replacement has been merged. + +CodeRabbit correctly warned that using R rather than Julia does not by itself +make `RCall.reval` safe. Reviewing those call sites found two source boundaries +that now receive explicit validation: + +- PERMANOVA concatenates metadata column names into a formula. Binding the + resulting string through RCall does not stop `as.formula`/model-frame + evaluation from interpreting expressions. Formula terms are now restricted + to plain ASCII R identifiers; expressions, reserved words, special dot terms + and control characters throw `ArgumentError` before formula construction. + Non-identifier metadata columns must be renamed before this analysis. +- Provenance package-version queries interpolate a package name into an R + string literal. Only the R package-name alphabet is accepted before building + that query; quotes, escapes, path separators and controls are rejected. + +Pure-helper regressions do not require an R/vegan runtime, and a reference-suite +case checks the PERMANOVA entry point when R/vegan is available. The new Julia +regressions require hosted CI: Julia and R are not installed in the local +sandbox. These targeted fixes are not a blanket dismissal of the remaining +advisory findings or a complete R-call-site security audit. diff --git a/src/analysis/analysis.jl b/src/analysis/analysis.jl index f40bcc2e..3295eff1 100644 --- a/src/analysis/analysis.jl +++ b/src/analysis/analysis.jl @@ -1437,11 +1437,35 @@ function _varies_within_any_block(values::AbstractVector, blocks::AbstractVector false end +# Metadata column names are untrusted data. Binding formula_rhs through RCall +# does NOT make it inert: as.formula parses that string and model.frame can +# evaluate calls inside it. Only plain R identifiers may reach that parser. +const _R_FORMULA_RESERVED_NAMES = Set([ + "if", "else", "repeat", "while", "function", "for", "in", "next", "break", + "TRUE", "FALSE", "NULL", "Inf", "NaN", "NA", "NA_integer_", "NA_real_", + "NA_complex_", "NA_character_", ".", "...", +]) + +function _permanova_formula_rhs(covariates::AbstractVector{<:AbstractString}) + for name in covariates + plain = occursin(r"\A(?:[A-Za-z]|\.(?![0-9]))[A-Za-z0-9._]*\z", name) + if !plain || name in _R_FORMULA_RESERVED_NAMES || occursin(r"\A\.\.[0-9]+\z", name) + throw(ArgumentError( + "PERMANOVA covariate names must be plain R identifiers, not expressions or reserved words")) + end + end + join(covariates, " + ") +end + """ run_permanova(mat, metadata; seed=123, transform="none", blocks=nothing) -> Union{NamedTuple, Nothing} PERMANOVA via vegan::adonis2 with up to 999 permutations. `metadata` is a DataFrame with one row per sample and covariate columns. +Covariate names must be plain ASCII R identifiers (starting with a letter or +with a dot not followed by a digit, then letters, digits, dots or underscores). +Expressions, reserved words and control characters are rejected before formula +construction; rename such metadata columns before calling this function. Each covariate is tested as its own term (`by = "terms"`), sequentially in column order, so each row is conditioned on the terms before it. The top-level `term`, `r2`, `f_statistic` and `p_value` are those of the first term. @@ -1464,7 +1488,7 @@ function run_permanova(mat::Matrix{Float64}, metadata::DataFrame; seed::Integer= covariates = [c for c in names(metadata) if lowercase(c) != "sample" && length(unique(metadata[!, c])) >= 2] isempty(covariates) && return nothing - formula_rhs = join(covariates, " + ") + formula_rhs = _permanova_formula_rhs(covariates) mat = transform_counts(mat; method=transform) blocked = !isnothing(blocks) && length(blocks) == size(mat, 1) && !allunique(blocks) # Within-block permutation never exchanges the labels of a term that is diff --git a/src/core/provenance.jl b/src/core/provenance.jl index a3f8c048..637332f5 100644 --- a/src/core/provenance.jl +++ b/src/core/provenance.jl @@ -349,11 +349,18 @@ not even a well-formed R version. Two of five packages were misreported on this machine. Nothing here may reach for it, and nothing normalises what DESCRIPTION declares. """ -r_version_query(pkg::AbstractString) = """ +function r_version_query(pkg::AbstractString) + # This name becomes R source, not an RCall-bound value. Accept only the + # package-name alphabet before interpolation; quotes, escapes and controls + # must never be able to terminate its R string literal. + occursin(r"\A[A-Za-z][A-Za-z0-9.]*\z", pkg) || throw(ArgumentError( + "R package names must start with a letter and contain only letters, digits or dots")) + return """ local({ d <- tryCatch(utils::packageDescription("$pkg"), error = function(e) NULL) if (is.list(d) && !is.null(d[["Version"]])) as.character(d[["Version"]]) else NA_character_ })""" +end """ probe_r(; packages=R_PACKAGES, renv_lock, timeout=60) -> RRecord diff --git a/test/unit/test_permanova.jl b/test/unit/test_permanova.jl index 06ff287e..1b118273 100644 --- a/test/unit/test_permanova.jl +++ b/test/unit/test_permanova.jl @@ -44,6 +44,17 @@ function _adonis2_reference(mat, meta, perm_expr) end @testset "PERMANOVA" begin + @testset "formula names are data, never R expressions" begin + @test Analysis._permanova_formula_rhs(["group", "run_2", ".batch", "x.y"]) == + "group + run_2 + .batch + x.y" + for name in ("I(1 + 1)", "system('echo unsafe')", "group + run", "group:run", + "group; stop('unsafe')", "`group`", "x\"", "x\\", "x\n", "x\r", + "x\t", "x\0", "", "1group", ".1group", "_group", "has space", + "if", "TRUE", "NA_real_", ".", "...", "..1") + @test_throws ArgumentError Analysis._permanova_formula_rhs(["group", name]) + end + end + @testset "_varies_within_any_block" begin blocks = ["i1", "i1", "i2", "i2"] @test !Analysis._varies_within_any_block(["a", "a", "b", "b"], blocks) @@ -59,6 +70,11 @@ end group = [cld(k, 2) <= 3 ? "A" : "B" for k in 1:12], run = repeat(["r1", "r2"], 6)) + @testset "the public entry point rejects formula expressions" begin + expression_meta = rename(copy(meta), "run" => "I(seq_len(12))") + @test_throws ArgumentError Analysis.run_permanova(mat, expression_meta; seed=123) + end + @testset "each term is tested on its own, as vegan by = \"terms\"" begin res = Analysis.run_permanova(mat, meta; seed=123) ref = _adonis2_reference(mat, meta, "999") diff --git a/test/unit/test_provenance.jl b/test/unit/test_provenance.jl index 743c2fe2..cf59889b 100644 --- a/test/unit/test_provenance.jl +++ b/test/unit/test_provenance.jl @@ -116,6 +116,15 @@ canned(out::String, err::String = "", code::Int = 0) = @test !occursin("packageVersion", query) @test occursin("Version", query) + for name in ("dada2", "Biostrings", "glmGamPoi", "R.oo") + @test occursin("packageDescription(\"$name\")", PV.r_version_query(name)) + end + for name in ("", "1pkg", "../vegan", "vegan/other", "pkg-name", "pkg_name", + "vegan\"); stop('unsafe'); #", "vegan\\", "vegan\n", "vegan\r", + "vegan\t", "vegan\0") + @test_throws ArgumentError PV.r_version_query(name) + end + ## And the module as a whole must not smuggle it in elsewhere. source = read(joinpath(@__DIR__, "..", "..", "src", "core", "provenance.jl"), String) @test !occursin("packageVersion(", source) From 23bb21aa798dbdfa2d80dd0f8a71348417386744 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:56:36 +0000 Subject: [PATCH 7/7] fix: reconcile governance guards with current main Signed-off-by: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 2 -- .github/workflows/scorecard.yml | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8855907..df6d9759 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -172,8 +172,6 @@ jobs: # silently. If a matrix is ever genuinely wanted here, the required context must move # to a matrix-free aggregator job first. name: Julia tests - # R/Julia setup and integration tests are expensive, but may not hang for 6h. - timeout-minutes: 90 needs: gate if: needs.gate.outputs.relevant == 'true' # Explicit, not ubuntu-latest: the R pin below names an apt package revision built diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index c01b774d..6e1df4fa 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -29,4 +29,4 @@ jobs: contents: read security-events: write id-token: write - uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 + uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d