diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 2fbedf5..681da1c 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -4,72 +4,84 @@ version: 'v0.0.3' workflows: '.github/workflows/boj-build.yml': - - 'actions/checkout@v4.1.7' + - 'actions/checkout@v7.0.1' '.github/workflows/bridge-gate.yml': - - 'actions/checkout@v4.3.1' + - 'actions/checkout@v7.0.1' '.github/workflows/cargo-audit.yml': - - 'actions/checkout@v5.0.1' + - 'actions/checkout@v7.0.1' - 'dtolnay/rust-toolchain@v1' '.github/workflows/casket-pages.yml': - - 'actions/cache@v4.3.0' - - 'actions/checkout@v4.1.1' - - 'actions/configure-pages@v5.0.0' - - 'actions/deploy-pages@v4.0.5' - - 'actions/upload-pages-artifact@v3.0.1' - - 'haskell-actions/setup@v2.7.5' - '.github/workflows/chapel-ci.yml': - 'actions/cache@v6.1.0' - 'actions/checkout@v7.0.1' - - 'actions/download-artifact@v8.0.2' - - 'actions/upload-artifact@v7.0.2' - - 'dtolnay/rust-toolchain@v1' + - 'actions/configure-pages@v6.0.0' + - 'actions/deploy-pages@v5.0.1' + - 'actions/upload-pages-artifact@v5.0.0' + - 'haskell-actions/setup@v2.12.1' + '.github/workflows/chapel-ci.yml': + - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333' + - 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9' + - 'dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8' '.github/workflows/codeql.yml': - 'actions/checkout@v7.0.1' - 'github/codeql-action@v4.39.0' '.github/workflows/coverage.yml': - - 'actions/checkout@v5.0.1' + - 'actions/checkout@v7.0.1' - 'dtolnay/rust-toolchain@v1' '.github/workflows/dependency-review.yml': - - 'actions/checkout@v5.0.1' - - 'actions/dependency-review-action@v4.3.4' + - 'actions/checkout@v7.0.1' + - 'actions/dependency-review-action@v5.0.0' '.github/workflows/dogfood-gate.yml': - - 'actions/checkout@v4.3.1' + - 'actions/checkout@v7.0.1' '.github/workflows/e2e.yml': - 'actions/checkout@v7.0.1' - 'actions/upload-artifact@v7.0.2' - 'dtolnay/rust-toolchain@v1' - 'swatinem/rust-cache@v2.9.2' - '.github/workflows/governance.yml': [] - '.github/workflows/hypatia-scan.yml': [] + '.github/workflows/governance.yml': + - 'hyperpolymath/standards@333b06a01db1874ffedb2de1bfabb195640927c7' + '.github/workflows/hypatia-scan.yml': + - 'hyperpolymath/standards@333b06a01db1874ffedb2de1bfabb195640927c7' '.github/workflows/instant-sync.yml': - 'peter-evans/repository-dispatch@v4.0.1' '.github/workflows/label-triage.yml': [] '.github/workflows/labels.yml': [] - '.github/workflows/mirror.yml': [] + '.github/workflows/mirror.yml': + - 'hyperpolymath/standards@333b06a01db1874ffedb2de1bfabb195640927c7' '.github/workflows/pages.yml': - - 'actions/checkout@v4.4.0' - - 'actions/deploy-pages@v4.0.5' - - 'actions/upload-pages-artifact@v3.0.1' + - 'actions/checkout@v7.0.1' + - 'actions/deploy-pages@v5.0.1' + - 'actions/upload-pages-artifact@v5.0.0' '.github/workflows/push-email-notify.yml': - - 'hyperpolymath/smtp-notify-action@v0.2.0' + - 'hyperpolymath/smtp-notify-action@v0.5.0' '.github/workflows/release.yml': - 'actions/checkout@v7.0.1' - 'actions/download-artifact@v8.0.2' - 'actions/upload-artifact@v7.0.2' - 'dtolnay/rust-toolchain@v1' + - 'slsa-framework/slsa-github-generator@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a' - 'softprops/action-gh-release@v3.0.3' - 'swatinem/rust-cache@v2.9.2' - '.github/workflows/rust-ci.yml': [] + '.github/workflows/rust-ci.yml': + - 'hyperpolymath/standards@333b06a01db1874ffedb2de1bfabb195640927c7' '.github/workflows/scan-and-report.yml': + - 'Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6' - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - 'dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8' - - 'swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6' - '.github/workflows/scorecard.yml': [] - '.github/workflows/secret-scanner.yml': [] + '.github/workflows/scorecard.yml': + - 'hyperpolymath/standards@333b06a01db1874ffedb2de1bfabb195640927c7' + '.github/workflows/secret-scanner.yml': + - 'hyperpolymath/standards@333b06a01db1874ffedb2de1bfabb195640927c7' dependencies: - 'actions/cache@v4.3.0': - ref: 'v4.3.0' - commit: 'sha1-0057852bfaa89a56745cba8c7296529d2fc39830' + 'Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6': + ref: 'v2.9.2' + commit: 'sha1-6323deb102c322ba6fcbdcafc7e3dddab59af2b6' + owner_id: 580492 + repo_id: 298565987 + 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9': + ref: 'v6.1.0' + commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9' owner_id: 44036562 repo_id: 215566462 'actions/cache@v6.1.0': @@ -82,59 +94,49 @@ dependencies: commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' owner_id: 44036562 repo_id: 197814629 - 'actions/checkout@v4.1.1': - ref: 'v4.1.1' - commit: 'sha1-b4ffde65f46336ab88eb53be808477a3936bae11' - owner_id: 44036562 - repo_id: 197814629 - 'actions/checkout@v4.1.7': - ref: 'v4.1.7' - commit: 'sha1-692973e3d937129bcbf40652eb9f2f61becf3332' - owner_id: 44036562 - repo_id: 197814629 - 'actions/checkout@v4.3.1': - ref: 'v4.3.1' - commit: 'sha1-34e114876b0b11c390a56381ad16ebd13914f8d5' - owner_id: 44036562 - repo_id: 197814629 - 'actions/checkout@v4.4.0': - ref: 'v4.4.0' - commit: 'sha1-11d5960a326750d5838078e36cf38b85af677262' - owner_id: 44036562 - repo_id: 197814629 - 'actions/checkout@v5.0.1': - ref: 'v5.0.1' - commit: 'sha1-93cb6efe18208431cddfb8368fd83d5badbf9bfd' - owner_id: 44036562 - repo_id: 197814629 'actions/checkout@v7.0.1': ref: 'v7.0.1' commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' owner_id: 44036562 repo_id: 197814629 - 'actions/configure-pages@v5.0.0': - ref: 'v5.0.0' - commit: 'sha1-983d7736d9b0ae728b81ab479565c72886d7745b' + 'actions/configure-pages@v6.0.0': + ref: 'v6.0.0' + commit: 'sha1-45bfe0192ca1faeb007ade9deae92b16b8254a0d' owner_id: 44036562 repo_id: 513659658 - 'actions/dependency-review-action@v4.3.4': - ref: 'v4.3.4' - commit: 'sha1-5a2ce3f5b92ee19cbb1541a4984c76d921601d7c' + 'actions/dependency-review-action@v5.0.0': + ref: 'v5.0.0' + commit: 'sha1-a1d282b36b6f3519aa1f3fc636f609c47dddb294' owner_id: 44036562 repo_id: 476372928 - 'actions/deploy-pages@v4.0.5': - ref: 'v4.0.5' - commit: 'sha1-d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e' + 'actions/deploy-pages@v5.0.1': + ref: 'v5.0.1' + commit: 'sha1-368f82528645a54fb793d4d04e342629a3f51346' owner_id: 44036562 repo_id: 438112499 + 'actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333': + ref: 'v8.0.2' + commit: 'sha1-9000827ccba6bdab643e8b6fd33ac0654aef8333' + owner_id: 44036562 + repo_id: 192626254 'actions/download-artifact@v8.0.2': ref: 'v8.0.2' commit: 'sha1-9000827ccba6bdab643e8b6fd33ac0654aef8333' owner_id: 44036562 repo_id: 192626254 - 'actions/upload-artifact@v4': - ref: 'v4' - commit: 'sha1-ea165f8d65b6e75b540449e92b4886f43607fa02' + 'actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08': + ref: 'v4.6.0' + commit: 'sha1-65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08' + owner_id: 44036562 + repo_id: 192625955 + 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f': + ref: 'v7.0.0' + commit: 'sha1-bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' + owner_id: 44036562 + repo_id: 192625955 + 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9': + ref: 'v7.0.2' + commit: 'sha1-cf430e030ddbb5b0abf93d22962f4752f3646cd9' owner_id: 44036562 repo_id: 192625955 'actions/upload-artifact@v7.0.2': @@ -142,50 +144,128 @@ dependencies: commit: 'sha1-cf430e030ddbb5b0abf93d22962f4752f3646cd9' owner_id: 44036562 repo_id: 192625955 - 'actions/upload-pages-artifact@v3.0.1': - ref: 'v3.0.1' - commit: 'sha1-56afc609e74202658d3ffba0e8f6dda462b719fa' + 'actions/upload-pages-artifact@v5.0.0': + ref: 'v5.0.0' + commit: 'sha1-fc324d3547104276b827a68afc52ff2a11cc49c9' owner_id: 44036562 repo_id: 496012378 uses: - - 'actions/upload-artifact@v4' + - 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' + 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed': + ref: 'v2.0.5' + commit: 'sha1-22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' + owner_id: 42048915 + repo_id: 356423100 'dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8': ref: 'v1' commit: 'sha1-683b588c487a69d01f6c6ddf40f036ff682cc2c8' owner_id: 1940490 repo_id: 260749683 + 'dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f': + ref: 'v1' + commit: 'sha1-e2a55d2ffb04f378e9626c28d38b36d230d1e12f' + owner_id: 1940490 + repo_id: 260749683 'dtolnay/rust-toolchain@v1': ref: 'v1' - commit: 'sha1-02cb101ec7c40f2c49e1d9714d64511d8e1b74de' + commit: 'sha1-683b588c487a69d01f6c6ddf40f036ff682cc2c8' owner_id: 1940490 repo_id: 260749683 + 'editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393': + ref: 'v3.0.0' + commit: 'sha1-51f63319f592f97930c73d9c46184d20bd206393' + owner_id: 26415196 + repo_id: 297874902 + 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124': + ref: 'v1.24.1' + commit: 'sha1-54075bcc5e249e4758d363f27d099f55d843f124' + owner_id: 47606891 + repo_id: 331103973 + 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63': + ref: 'v4.38.0' + commit: 'sha1-b96794f015dfd88f77b49b1c93e0fa7110f94c63' + owner_id: 9919 + repo_id: 259445878 'github/codeql-action@v4.39.0': ref: 'v4.39.0' commit: 'sha1-6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee' owner_id: 9919 repo_id: 259445878 - 'haskell-actions/setup@v2.7.5': - ref: 'v2.7.5' - commit: 'sha1-ec49483bfc012387b227434aba94f59a6ecd0900' + 'goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406': + ref: 'v2.2.1' + commit: 'sha1-abea47f85e598557f500fa1fd2ab7464fcb39406' + owner_id: 1006268 + repo_id: 212984112 + 'haskell-actions/setup@v2.12.1': + ref: 'v2.12.1' + commit: 'sha1-0f8e8c99d88aeb3fbfd523f1ef2c6f762d10d64d' owner_id: 75048950 repo_id: 623796603 - 'hyperpolymath/smtp-notify-action@v0.2.0': - ref: 'v0.2.0' - commit: 'sha1-ede1191ef6ff3ac02c4f4d9efdf837ee517e11d7' + 'hyperpolymath/smtp-notify-action@v0.5.0': + ref: 'v0.5.0' + commit: 'sha1-c1c9fa07992a02c1fd3d67a0dc1b08cccb852aef' owner_id: 6759885 repo_id: 1352485172 + 'hyperpolymath/standards@333b06a01db1874ffedb2de1bfabb195640927c7': + ref: '333b06a01db1874ffedb2de1bfabb195640927c7' + commit: 'sha1-333b06a01db1874ffedb2de1bfabb195640927c7' + owner_id: 6759885 + repo_id: 1116521501 + uses: + - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9' + - 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' + - 'dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8' + - 'editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393' + - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124' + - 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63' + - 'goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406' + - 'ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e' + - 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6' + - 'swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6' + - 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555' + 'ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e': + ref: 'v2.5.0' + commit: 'sha1-fa7117db83c52d787ae5a89f350999701505d78e' + owner_id: 67707773 + repo_id: 421101922 + 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6': + ref: 'v2.2.0' + commit: 'sha1-0c5077e51419868618aeaa5fe8019c62421857d6' + owner_id: 108928776 + repo_id: 512644635 'peter-evans/repository-dispatch@v4.0.1': ref: 'v4.0.1' commit: 'sha1-28959ce8df70de7be546dd1250a005dd32156697' owner_id: 18365890 repo_id: 220359305 + 'slsa-framework/slsa-github-generator@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a': + ref: 'v2.1.0' + commit: 'sha1-f7dd8c54c2067bafc12ca7a55595d5ee9b75204a' + owner_id: 80431187 + repo_id: 475074978 + uses: + - 'actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08' + - 'slsa-framework/slsa-github-generator@v2.1.0' + - 'softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda' + 'slsa-framework/slsa-github-generator@v2.1.0': + ref: 'v2.1.0' + commit: 'sha1-f7dd8c54c2067bafc12ca7a55595d5ee9b75204a' + owner_id: 80431187 + repo_id: 475074978 + 'softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda': + ref: 'v2.2.1' + commit: 'sha1-c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda' + owner_id: 2242 + repo_id: 204253808 'softprops/action-gh-release@v3.0.3': ref: 'v3.0.3' commit: 'sha1-efb35369e0ad2afab669f228072c1b0d510eae64' owner_id: 2242 repo_id: 204253808 'swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6': - ref: 'v2' + ref: 'v2.9.2' commit: 'sha1-6323deb102c322ba6fcbdcafc7e3dddab59af2b6' owner_id: 580492 repo_id: 298565987 @@ -194,3 +274,8 @@ dependencies: commit: 'sha1-6323deb102c322ba6fcbdcafc7e3dddab59af2b6' owner_id: 580492 repo_id: 298565987 + 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555': + ref: 'v0.10.0' + commit: 'sha1-e83874834305fe9a4a2997156cb26c5de65a8555' + owner_id: 135788 + repo_id: 208510314 diff --git a/.github/workflows/cargo-audit.yml b/.github/workflows/cargo-audit.yml index 97bb168..f6eb2a4 100644 --- a/.github/workflows/cargo-audit.yml +++ b/.github/workflows/cargo-audit.yml @@ -26,8 +26,6 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 - with: - toolchain: v1 with: toolchain: stable diff --git a/.github/workflows/chapel-ci.yml b/.github/workflows/chapel-ci.yml index 2939940..3ae7720 100644 --- a/.github/workflows/chapel-ci.yml +++ b/.github/workflows/chapel-ci.yml @@ -72,7 +72,7 @@ jobs: outputs: relevant: ${{ steps.f.outputs.relevant }} steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 2 - id: f @@ -105,7 +105,7 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 45 steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Chapel ${{ env.CHAPEL_VERSION }} run: | set -euo pipefail @@ -128,7 +128,7 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 45 steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Chapel ${{ env.CHAPEL_VERSION }} run: | set -euo pipefail @@ -144,7 +144,7 @@ jobs: chpl src/MassPanic.chpl src/Protocol.chpl src/Imaging.chpl src/Temporal.chpl -o mass-panic chpl smoke/two_repo_smoke.chpl src/Protocol.chpl src/Imaging.chpl -o smoke/two_repo_smoke - name: Upload Chapel artefacts - uses: actions/upload-artifact@v7.0.2 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: chapel-binaries path: | @@ -159,7 +159,7 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 45 steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Chapel ${{ env.CHAPEL_VERSION }} run: | set -euo pipefail @@ -168,7 +168,7 @@ jobs: curl -fsSL --retry 3 -o /tmp/chapel.deb "${{ env.CHAPEL_DEB_URL }}" sudo apt-get install -y /tmp/chapel.deb - name: Download Chapel artefacts - uses: actions/download-artifact@v8.0.2 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: chapel-binaries path: chapel/ @@ -184,7 +184,7 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 45 steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Chapel ${{ env.CHAPEL_VERSION }} run: | set -euo pipefail @@ -193,7 +193,7 @@ jobs: curl -fsSL --retry 3 -o /tmp/chapel.deb "${{ env.CHAPEL_DEB_URL }}" sudo apt-get install -y /tmp/chapel.deb - name: Download Chapel artefacts - uses: actions/download-artifact@v8.0.2 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: chapel-binaries path: chapel/ @@ -221,11 +221,11 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 45 steps: - - uses: actions/checkout@v7.0.1 - - uses: dtolnay/rust-toolchain@v1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1 with: - toolchain: v1 - - uses: actions/cache@v6.1.0 + toolchain: stable + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cargo/registry @@ -244,9 +244,11 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 45 steps: - - uses: actions/checkout@v7.0.1 - - uses: dtolnay/rust-toolchain@v1 - - uses: actions/cache@v6.1.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1 + with: + toolchain: stable + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cargo/registry @@ -261,7 +263,7 @@ jobs: curl -fsSL --retry 3 -o /tmp/chapel.deb "${{ env.CHAPEL_DEB_URL }}" sudo apt-get install -y /tmp/chapel.deb - name: Download Chapel artefacts - uses: actions/download-artifact@v8.0.2 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: chapel-binaries path: chapel/ @@ -281,7 +283,7 @@ jobs: env: CHPL_HOME: /opt/chapel-multilocale steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Cache the entire built-from-source Chapel tree. Key is stable across # PRs as long as the version, conduit, launcher and cache-gen marker @@ -289,7 +291,7 @@ jobs: # restore is ~30s. - name: Cache multilocale Chapel ($CHPL_HOME) id: chapel-cache - uses: actions/cache@v6.1.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.CHAPEL_MULTILOCALE_HOME }} key: ${{ runner.os }}-chapel-multilocale-${{ env.CHAPEL_VERSION }}-gasnet-smp-${{ env.CHAPEL_MULTILOCALE_CACHE_GEN }} diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index aefc6f3..93c65c5 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -24,8 +24,6 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 - with: - toolchain: v1 with: toolchain: stable components: llvm-tools-preview diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 39e4481..90e15a1 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -45,7 +45,7 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 with: - toolchain: v1 + toolchain: stable - name: Rust cache uses: Swatinem/rust-cache@v2.9.2 @@ -67,6 +67,8 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 + with: + toolchain: stable - name: Rust cache uses: Swatinem/rust-cache@v2.9.2 @@ -131,6 +133,8 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 + with: + toolchain: stable - name: Rust cache uses: Swatinem/rust-cache@v2.9.2 @@ -150,6 +154,8 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 + with: + toolchain: stable - name: Rust cache uses: Swatinem/rust-cache@v2.9.2 diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index e7aa4e9..c19baf7 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -17,4 +17,4 @@ permissions: jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@fad242d35291de1898242d6737ba02b74a59a2f2 + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@333b06a01db1874ffedb2de1bfabb195640927c7 diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index a5a2131..9bf8587 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -20,4 +20,4 @@ permissions: jobs: hypatia: - uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@84355587cb2a1f86e6882de83514a32db2646e7a + uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@333b06a01db1874ffedb2de1bfabb195640927c7 diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index b95ea10..7c43488 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -15,5 +15,12 @@ permissions: jobs: mirror: - uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@84355587cb2a1f86e6882de83514a32db2646e7a - secrets: inherit + uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@333b06a01db1874ffedb2de1bfabb195640927c7 + secrets: + GITLAB_SSH_KEY: ${{ secrets.GITLAB_SSH_KEY }} + BITBUCKET_SSH_KEY: ${{ secrets.BITBUCKET_SSH_KEY }} + CODEBERG_SSH_KEY: ${{ secrets.CODEBERG_SSH_KEY }} + SOURCEHUT_SSH_KEY: ${{ secrets.SOURCEHUT_SSH_KEY }} + DISROOT_SSH_KEY: ${{ secrets.DISROOT_SSH_KEY }} + GITEA_SSH_KEY: ${{ secrets.GITEA_SSH_KEY }} + RADICLE_KEY: ${{ secrets.RADICLE_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2503775..6c249ab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,8 +31,6 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 - with: - toolchain: v1 with: toolchain: stable diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index bb07f89..73b1435 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -18,4 +18,4 @@ permissions: jobs: rust-ci: - uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@84355587cb2a1f86e6882de83514a32db2646e7a # re-pinned 2026-07-01: toolchain-required-input fix (standards#439) + uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@333b06a01db1874ffedb2de1bfabb195640927c7 # re-pinned 2026-07-01: toolchain-required-input fix (standards#439) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 5d212f3..d1ca432 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -15,8 +15,9 @@ permissions: jobs: scorecard: - uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@84355587cb2a1f86e6882de83514a32db2646e7a + uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@333b06a01db1874ffedb2de1bfabb195640927c7 permissions: + actions: read # required by scorecard-reusable.yml to read workflow metadata contents: read security-events: write id-token: write diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index fffde78..c839b81 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -21,5 +21,4 @@ jobs: scan: permissions: contents: read - uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@84355587cb2a1f86e6882de83514a32db2646e7a - secrets: inherit + uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@333b06a01db1874ffedb2de1bfabb195640927c7 diff --git a/Cargo.lock b/Cargo.lock index b918de3..5e0d690 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -886,7 +886,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -939,16 +939,6 @@ dependencies = [ "libc", ] -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -962,7 +952,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.4", + "core-foundation", "core-graphics-types", "foreign-types", "libc", @@ -975,7 +965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.4", + "core-foundation", "libc", ] @@ -1050,9 +1040,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ "crossbeam-utils", ] @@ -1189,7 +1179,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1492,7 +1482,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1512,11 +1502,10 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] @@ -2348,9 +2337,9 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" -version = "0.9.9" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -3227,12 +3216,11 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.38.4" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", - "serde", ] [[package]] @@ -3467,14 +3455,14 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.23.45" +version = "0.23.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ "log", "once_cell", @@ -3909,7 +3897,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4038,7 +4026,7 @@ dependencies = [ "indexmap", "toml_datetime", "toml_parser", - "winnow", + "winnow 0.7.14", ] [[package]] @@ -4047,7 +4035,7 @@ version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "247eaa3197818b831697600aadf81514e577e0cba5eab10f7e064e78ae154df1" dependencies = [ - "winnow", + "winnow 0.7.14", ] [[package]] @@ -4462,9 +4450,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.8" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" dependencies = [ "proc-macro2", "quick-xml", @@ -4505,15 +4493,15 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.2.0" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe985f41e291eecef5e5c0770a18d28390addb03331c043964d9e916453d6f16" +checksum = "62c35be770821a214dbc362fc26908c853e776c0004294d0b10b8a6bad582f94" dependencies = [ - "core-foundation 0.10.1", "jni", "log", "ndk-context", "objc2 0.6.4", + "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", "url", "web-sys", @@ -4739,7 +4727,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4964,7 +4952,7 @@ dependencies = [ "calloop 0.13.0", "cfg_aliases", "concurrent-queue", - "core-foundation 0.9.4", + "core-foundation", "core-graphics", "cursor-icon", "dpi", @@ -5010,6 +4998,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -5137,7 +5134,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow", + "winnow 0.7.14", "zbus_macros", "zbus_names", "zvariant", @@ -5179,32 +5176,41 @@ dependencies = [ "syn 2.0.117", "zbus_names", "zvariant", - "zvariant_utils", + "zvariant_utils 3.3.0", ] [[package]] name = "zbus_names" -version = "4.3.1" +version = "4.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +checksum = "812a12be4ead7a59e18ea8f047d08add0066b73bc62fc1467066a2472ae088dd" dependencies = [ "serde", - "winnow", + "winnow 1.0.4", "zvariant", ] [[package]] name = "zbus_xml" -version = "5.1.0" +version = "5.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "441a0064125265655bccc3a6af6bef56814d9277ac83fce48b1cd7e160b80eac" +checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a" dependencies = [ - "quick-xml", "serde", + "winnow 1.0.4", "zbus_names", "zvariant", ] +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + [[package]] name = "zerocopy" version = "0.8.39" @@ -5308,29 +5314,30 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.10.0" +version = "5.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +checksum = "80bf6d6cd6a4b601acb64cfb8284cdc643fcfa75ef26ae3c46ec8aa4e8a1fbb7" dependencies = [ "endi", "enumflags2", "serde", - "winnow", + "winnow 1.0.4", + "zcheapstr", "zvariant_derive", - "zvariant_utils", + "zvariant_utils 4.2.0", ] [[package]] name = "zvariant_derive" -version = "5.10.0" +version = "5.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +checksum = "ef2672456ba1cb54b5eaec136e63135b3c7495fb938e53b28a62e633496d486d" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", - "zvariant_utils", + "syn 3.0.3", + "zvariant_utils 4.2.0", ] [[package]] @@ -5343,5 +5350,18 @@ dependencies = [ "quote", "serde", "syn 2.0.117", - "winnow", + "winnow 0.7.14", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.3", + "winnow 1.0.4", ] diff --git a/README.adoc b/README.adoc index c7a3502..8509005 100644 --- a/README.adoc +++ b/README.adoc @@ -183,13 +183,13 @@ Consult `man/panic-attack.1` or `panic-attack help` for every command, and use ` === Automation readiness -`panic-attack diagnostics` validates AI manifests, VeriSimDB caches, Hypatia and gitbot-fleet watchers, and the docs PanLL expects (`docs/ambush-timeline.md`, `docs/panll-export.md`). `docs/patch-bridge-design.md` details how the patch-bridge subcommands plug into BoJ cartridges, CVE workflows, and the PanLL panel. +`panic-attack diagnostics` validates AI manifests, VeriSimDB caches, Hypatia and gitbot-fleet watchers, and the docs PanLL expects (`docs/ambush-timeline.adoc`, `docs/panll-export.adoc`). `docs/patch-bridge-design.adoc` details how the patch-bridge subcommands plug into BoJ cartridges, CVE workflows, and the PanLL panel. `just test-elixir` runs the Elixir harnesses that cover the CLI surface so documentation, help text, and automation flags stay aligned. == Integration & Groove -panic-attack can emit PanLL-friendly exports (event chains, system images, temporal diffs) per `docs/panll-export.md`, and it also advertises its static-analysis capability via the Gossamer groove protocol. Start the discovery endpoint with `panic-attack groove --port 7600` and check the JSON manifest by curling `http://localhost:7600/.well-known/groove` (the health endpoint sits at `/health`). +panic-attack can emit PanLL-friendly exports (event chains, system images, temporal diffs) per `docs/panll-export.adoc`, and it also advertises its static-analysis capability via the Gossamer groove protocol. Start the discovery endpoint with `panic-attack groove --port 7600` and check the JSON manifest by curling `http://localhost:7600/.well-known/groove` (the health endpoint sits at `/health`). The manifest follows the Idris-aligned semantics that live in `boj-server/src/interface/abi/Groove.idr` and the `gossamer` schema references. As a result, any groove-aware consumer—PanLL event-chain panels, the Burble voice platform, Hypatia, or other automation fabrics—sees the same capability vocabulary and can attach its own UI (Groove + Burble PanLL) confidently. diff --git a/generated/k9iser/cargo-manifest.k9 b/generated/k9iser/cargo-manifest.k9 index ad723ab..0e313e0 100644 --- a/generated/k9iser/cargo-manifest.k9 +++ b/generated/k9iser/cargo-manifest.k9 @@ -1,19 +1,53 @@ -# Auto-generated K9 contract for cargo-manifest -# Safety tier: hunt +K9! +# SPDX-License-Identifier: MPL-2.0 +# K9 contract for cargo-manifest; constraints from k9iser.toml. +# Hunt-tier signing is required before execution; this file contains no signature. -[must] -package.name : string { == 'panic-attack' } -package.edition : string { == '2021' } -profile.release.codegen-units : string { == 1 } -profile.release.strip : string { == 'symbols' } -profile.release.panic : string { == 'unwind' } +{ + pedigree = { + schema_version = "1.0.0", + metadata = { + name = "cargo-manifest", + version = "1.0.0", + }, + security = { + leash = 'Hunt, + signature_required = true, + }, + }, -[trust] -signed-by = "ci-pipeline" + config = { + source = "Cargo.toml", + format = "toml", + }, -[dust] -remove = ["unused optional features"] + # Declarative requirements for the source configuration. + must = [ + "package.name == 'panic-attack'", + "package.edition == '2021'", + "package.rust-version is set", + "package.license-file is set", + "package.repository is non-empty", + "profile.release.lto is set", + "profile.release.codegen-units == 1", + "profile.release.strip == 'symbols'", + "profile.release.panic == 'unwind'", + "dependencies.blake3 is set", + "dependencies.sha2 is set", + "no dependency on md5", + "no dependency on sha1", + ], -[intend] -production-ready = true -security-critical = true + trust = { + "signed-by" = "ci-pipeline", + }, + + dust = { + remove = ["unused optional features"], + }, + + intend = { + "production-ready" = true, + "security-critical" = true, + }, +} diff --git a/generated/k9iser/container-build.k9 b/generated/k9iser/container-build.k9 index 0f79c65..99eb4e6 100644 --- a/generated/k9iser/container-build.k9 +++ b/generated/k9iser/container-build.k9 @@ -1,28 +1,58 @@ -# Auto-generated K9 contract for container-build -# Safety tier: hunt +K9! +# SPDX-License-Identifier: MPL-2.0 +# K9 contract for container-build; constraints from k9iser.toml. +# Hunt-tier signing is required before execution; this file contains no signature. -[must] -metadata.license : string { == 'MPL-2.0' } -metadata.registry : string { == 'ghcr.io/hyperpolymath' } -build.runtime : string { == 'podman' } -layers.base.verify : bool { == true } -layers.runtime.user : string { == 'nonroot' } -security.non-root : bool { == true } -security.no-new-privileges : bool { == true } -security.signing.algorithm : string { == 'ML-DSA-87' } -security.signing.provider : string { == 'cerro-torre' } -security.sbom.format : string { == 'spdx-json' } -security.sbom.include-deps : bool { == true } -verify.vordr : bool { == true } -verify.svalinn : bool { == true } -verify.scan-on-build : bool { == true } +{ + pedigree = { + schema_version = "1.0.0", + metadata = { + name = "container-build", + version = "1.0.0", + }, + security = { + leash = 'Hunt, + signature_required = true, + }, + }, -[trust] -signed-by = "ci-pipeline" + config = { + source = "stapeln.toml", + format = "toml", + }, -[dust] -remove = ["deprecated layer definitions"] + # Declarative requirements for the source configuration. + must = [ + "metadata.license == 'MPL-2.0'", + "metadata.registry == 'ghcr.io/hyperpolymath'", + "build.runtime == 'podman'", + "layers.base.from starts-with 'cgr.dev/chainguard/'", + "layers.base.verify == true", + "layers.runtime.user == 'nonroot'", + "security.non-root == true", + "security.no-new-privileges == true", + "security.cap-drop contains 'ALL'", + "security.signing.algorithm == 'ML-DSA-87'", + "security.signing.provider == 'cerro-torre'", + "security.sbom.format == 'spdx-json'", + "security.sbom.include-deps == true", + "verify.vordr == true", + "verify.svalinn == true", + "verify.scan-on-build == true", + "verify.fail-on contains 'critical'", + "verify.fail-on contains 'high'", + ], -[intend] -production-ready = true -supply-chain-verified = true + trust = { + "signed-by" = "ci-pipeline", + }, + + dust = { + remove = ["deprecated layer definitions"], + }, + + intend = { + "production-ready" = true, + "supply-chain-verified" = true, + }, +} diff --git a/src/aggregate/mod.rs b/src/aggregate/mod.rs index cc26ecd..4ecf13d 100644 --- a/src/aggregate/mod.rs +++ b/src/aggregate/mod.rs @@ -51,6 +51,9 @@ use std::io::Read; use std::path::{Path, PathBuf}; /// Ingest a `proven-tests` JSON run report as an aggregate input kind. +// This public library API is also compiled into the binary's private aggregate +// module, whose CLI does not yet consume proven-tests reports. +#[allow(dead_code)] pub mod proven_tests; /// Upper bound on the text we scan for verdict / coverage classification. @@ -394,11 +397,15 @@ pub fn run(config: AggregateConfig) -> Result { }) } +/// Write the aggregate report as pretty-printed JSON, creating parent directories +/// and replacing any existing file at `path`. +/// +/// # Errors +/// Returns directory creation, JSON serialisation and file write errors. pub fn write_report(report: &AggregateReport, path: &Path) -> Result<()> { if let Some(parent) = path.parent() { if !parent.as_os_str().is_empty() { - fs::create_dir_all(parent) - .with_context(|| format!("creating {}", parent.display()))?; + fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?; } } let json = serde_json::to_string_pretty(report).context("serializing aggregate report")?; @@ -494,10 +501,11 @@ fn has_metavar_hole(text: &str) -> bool { false } +/// Look for case-sensitive proof or declaration substrings as a closure heuristic; +/// a match does not verify that the text contains a valid proof. fn has_proof_keyword(text: &str) -> bool { const KW: &[&str] = &[ - "theorem", "lemma", "Theorem", "Lemma", "Proof", "proof", "where", ":=", "data ", - "record ", + "theorem", "lemma", "Theorem", "Lemma", "Proof", "proof", "where", ":=", "data ", "record ", ]; KW.iter().any(|k| text.contains(k)) } @@ -579,10 +587,7 @@ fn scan_name(text: &str) -> Option { let end = stripped.find('"')?; Some(stripped[..end].to_string()) } else { - let token: String = rest - .chars() - .take_while(|c| !c.is_whitespace()) - .collect(); + let token: String = rest.chars().take_while(|c| !c.is_whitespace()).collect(); if token.is_empty() { None } else { @@ -611,6 +616,10 @@ fn scan_covers(text: &str) -> Vec { /// Parse `[claim:]kind:value`, e.g. `sound:category:UnsafeCode`, /// `safe:file:src/foo.rs`, `category:PanicPath`, `free:my-lemma`. +/// +/// A bare token other than `safe` or `sound` becomes free-form coverage with an +/// unspecified claim. Returns `None` for an empty first field, an unrecognised +/// kind or an unsupported field layout. Values are retained without trimming. pub fn parse_coverage_spec(spec: &str) -> Option { let mut parts: Vec<&str> = spec.splitn(3, ':').collect(); if parts.is_empty() || parts[0].is_empty() { @@ -634,11 +643,7 @@ pub fn parse_coverage_spec(spec: &str) -> Option { [single] => (CoverageKind::Free, (*single).to_string()), _ => return None, }; - Some(Coverage { - claim, - kind, - value, - }) + Some(Coverage { claim, kind, value }) } fn kind_of(s: &str) -> Option { @@ -701,11 +706,9 @@ fn summarise(findings: &[FindingLite]) -> FindingSummary { } } +/// Count findings selected by the coverage annotation; free-form coverage matches none. fn count_matching(findings: &[FindingLite], cov: &Coverage) -> usize { - findings - .iter() - .filter(|f| matches_coverage(f, cov)) - .count() + findings.iter().filter(|f| matches_coverage(f, cov)).count() } fn matches_coverage(f: &FindingLite, cov: &Coverage) -> bool { @@ -897,7 +900,11 @@ mod tests { Prover::Idris2, "-- we deliberately avoid any postulate here\ntheorem t : Nat\nt = 0\n", ); - assert!(holes.is_empty(), "comment word should not register: {:?}", holes); + assert!( + holes.is_empty(), + "comment word should not register: {:?}", + holes + ); assert_eq!(v, ProofVerdict::Closed); } diff --git a/src/aggregate/proven_tests.rs b/src/aggregate/proven_tests.rs index a748760..9799cad 100644 --- a/src/aggregate/proven_tests.rs +++ b/src/aggregate/proven_tests.rs @@ -27,6 +27,7 @@ use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; use std::path::Path; +use std::str::FromStr; /// A cited proof step (only Actually-Proven entries carry these). #[derive(Debug, Clone, Serialize, Deserialize)] @@ -115,9 +116,14 @@ impl ReportTier { } } -impl ProvenTestsReport { - /// Parse a run report from JSON text. - pub fn from_str(text: &str) -> Result { +impl FromStr for ProvenTestsReport { + type Err = anyhow::Error; + + /// Parse a run report from JSON text, requiring schema version 1. + /// + /// # Errors + /// Returns JSON deserialisation errors or an error if `schema_version` is not 1. + fn from_str(text: &str) -> Result { let report: ProvenTestsReport = serde_json::from_str(text).context("parsing proven-tests run report")?; if report.schema_version != 1 { @@ -128,11 +134,17 @@ impl ProvenTestsReport { } Ok(report) } +} - /// Parse a run report from a file path. +impl ProvenTestsReport { + /// Read a UTF-8 JSON run report from `path` and parse it as schema version 1. + /// + /// # Errors + /// Returns file read errors (including invalid UTF-8), JSON deserialisation + /// errors or an error if `schema_version` is not 1. pub fn from_path(path: &Path) -> Result { - let text = std::fs::read_to_string(path) - .with_context(|| format!("reading {}", path.display()))?; + let text = + std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?; Self::from_str(&text) } @@ -163,12 +175,21 @@ impl ProvenTestsReport { e.proof_ladder.len() ), Some(ReportTier::ProvisionallyProven) => { - format!("{}: Provisionally-Proven → informational (no proof artifact)", e.test_id) + format!( + "{}: Provisionally-Proven → informational (no proof artifact)", + e.test_id + ) } Some(ReportTier::Unproven) => { - format!("{}: Unproven → informational (no proof artifact)", e.test_id) + format!( + "{}: Unproven → informational (no proof artifact)", + e.test_id + ) } - None => format!("{}: unrecognised provenance tier {:?}", e.test_id, e.provenance), + None => format!( + "{}: unrecognised provenance tier {:?}", + e.test_id, e.provenance + ), }) .collect() } @@ -201,7 +222,7 @@ mod tests { #[test] fn parses_schema_v1() { - let r = ProvenTestsReport::from_str(SAMPLE).expect("parse"); + let r: ProvenTestsReport = SAMPLE.parse().expect("parse"); assert_eq!(r.schema_version, 1); assert_eq!(r.entries.len(), 3); assert_eq!(r.summary.actually_proven, 1); @@ -240,6 +261,9 @@ mod tests { let weird = SAMPLE.replace("\"Unproven\"", "\"Sorta-Proven\""); let r = ProvenTestsReport::from_str(&weird).unwrap(); // notes() surfaces the unrecognised tier rather than crashing - assert!(r.notes().iter().any(|n| n.contains("unrecognised provenance tier"))); + assert!(r + .notes() + .iter() + .any(|n| n.contains("unrecognised provenance tier"))); } } diff --git a/src/assail/analyzer.rs b/src/assail/analyzer.rs index c21c436..4b86ad1 100644 --- a/src/assail/analyzer.rs +++ b/src/assail/analyzer.rs @@ -263,18 +263,16 @@ static RE_HARDCODED_SECRET: OnceLock = OnceLock::new(); /// Does not handle OCaml `(* *)` or Forth `\` — edge cases for later. static RE_TODO_COMMENT: OnceLock = OnceLock::new(); -/// Heuristic: is the `malloc` on `lines[idx]` NULL-checked within a short -/// window of following lines? +/// Heuristically recognise a null check for the allocation on `lines[idx]`. /// -/// An "UncheckedAllocation" finding should only fire when the result is *not* -/// guarded. This recognises the common C idioms (`if (p == NULL)`, -/// `if (!p)`, `if (p == nullptr)`) on the lvalue assigned from `malloc`. +/// Searches the allocation line and up to six following lines for `NULL` or +/// `nullptr` alongside the assigned identifier, or an `if` line containing `!` +/// immediately followed by that identifier. If no identifier can be extracted, +/// any `NULL` or `nullptr` substring in the window suffices. Substring matches +/// do not establish that the allocation is guarded. /// -/// Deliberately conservative to avoid false negatives in a security scanner: -/// a site is treated as checked only on a clear NULL-guard signal that -/// references the allocated variable (or, when no lvalue can be parsed, any -/// NULL/nullptr token in the window). Genuinely-unchecked allocations — the -/// real finding — are still reported. +/// # Panics +/// Panics if the zero-based `idx` is outside `lines`. fn c_alloc_result_is_null_checked(lines: &[&str], idx: usize) -> bool { let line = lines[idx]; // Parse the lvalue assigned from malloc: the last identifier left of '='. @@ -285,8 +283,7 @@ fn c_alloc_result_is_null_checked(lines: &[&str], idx: usize) -> bool { .next() .and_then(|lhs| { lhs.split(|c: char| !c.is_alphanumeric() && c != '_') - .filter(|s| !s.is_empty()) - .last() + .rfind(|s| !s.is_empty()) }) .map(str::to_string) } else { @@ -942,6 +939,10 @@ impl Analyzer { // Original language analyzers // ============================================================ + /// Accumulate Rust statistics and append heuristic findings for `file_path`. + /// Dangerous-pattern checks exclude string contents, comments, `cfg`-selected + /// test bodies and `should_panic` bodies. Panic counts and findings also exclude + /// test paths and build scripts. fn analyze_rust( &self, content: &str, @@ -984,9 +985,8 @@ impl Analyzer { // crypto counts too (a `#[test] fn exercises_unsafe_wrapper()` // inside a production file would otherwise count toward that // file's unsafe-block total). - let code_only = Self::strip_should_panic_items_rs( - &Self::strip_cfg_test_modules_rs(&without_comments), - ); + let code_only = + Self::strip_should_panic_items_rs(&Self::strip_cfg_test_modules_rs(&without_comments)); stats.unsafe_blocks += code_only.matches("unsafe {").count(); stats.unsafe_blocks += code_only.matches("unsafe fn").count(); @@ -1769,6 +1769,9 @@ impl Analyzer { false } + /// Accumulate C/C++ statistics and append heuristic findings for `file_path`. + /// Unchecked malloc findings include one-based line numbers for suppression + /// and omit allocations with a recognised nearby null check. fn analyze_c_cpp( &self, content: &str, @@ -1903,7 +1906,7 @@ impl Analyzer { ), recommended_attack: vec![AttackAxis::Network], suppressed: false, - test_context: None, + test_context: None, }); break; } @@ -1926,7 +1929,7 @@ impl Analyzer { ), recommended_attack: vec![AttackAxis::Network], suppressed: false, - test_context: None, + test_context: None, }); break; } @@ -2014,6 +2017,8 @@ impl Analyzer { Ok(()) } + /// Accumulate Go statistics and append heuristic concurrency, unsafe pointer, + /// command execution and cryptography findings located at `file_path`. fn analyze_go( &self, content: &str, @@ -2089,7 +2094,7 @@ impl Analyzer { ), recommended_attack: vec![AttackAxis::Network], suppressed: false, - test_context: None, + test_context: None, }); break; } @@ -2111,7 +2116,7 @@ impl Analyzer { ), recommended_attack: vec![AttackAxis::Network], suppressed: false, - test_context: None, + test_context: None, }); break; } @@ -2143,6 +2148,8 @@ impl Analyzer { Ok(()) } + /// Accumulate Python I/O and threading counts and append heuristic loop, + /// code execution, deserialisation, command and cryptography findings for `file_path`. fn analyze_python( &self, content: &str, @@ -2256,7 +2263,7 @@ impl Analyzer { ), recommended_attack: vec![AttackAxis::Network], suppressed: false, - test_context: None, + test_context: None, }); break; } @@ -2282,7 +2289,7 @@ impl Analyzer { ), recommended_attack: vec![AttackAxis::Network], suppressed: false, - test_context: None, + test_context: None, }); break; } @@ -3170,6 +3177,9 @@ impl Analyzer { } } + /// Accumulate OCaml statistics and append heuristic findings for `file_path`. + /// Classify `Obj.magic` as proof drift when Coq extraction markers are present, + /// or as unsafe type coercion otherwise. fn analyze_ocaml( &self, content: &str, @@ -3204,7 +3214,7 @@ impl Analyzer { ), recommended_attack: vec![AttackAxis::Memory], suppressed: false, - test_context: None, + test_context: None, }); } else { weak_points.push(WeakPoint { @@ -3216,7 +3226,7 @@ impl Analyzer { description: format!("Obj.magic (unsafe type coercion) in {}", file_path), recommended_attack: vec![AttackAxis::Memory], suppressed: false, - test_context: None, + test_context: None, }); } } @@ -4322,6 +4332,8 @@ impl Analyzer { Ok(()) } + /// Accumulate Odin operation counts and append a Medium unsafe-code finding + /// at `file_path` when `rawptr` occurs in the content. fn analyze_odin( &self, content: &str, @@ -4360,7 +4372,7 @@ impl Analyzer { description: format!("{} rawptr usage in {}", rawptr_count, file_path), recommended_attack: vec![AttackAxis::Memory], suppressed: false, - test_context: None, + test_context: None, }); } } @@ -4500,6 +4512,9 @@ impl Analyzer { // Config languages (Nickel, Nix) // ============================================================ + /// Accumulate Nix/Nickel I/O counts and append Nix command and unpinned-flake findings. + /// Uses `file_path` for file-type checks, finding locations and the sibling + /// `flake.lock` lookup on disk. fn analyze_config( &self, content: &str, @@ -4561,7 +4576,7 @@ impl Analyzer { ), recommended_attack: vec![], suppressed: false, - test_context: None, + test_context: None, }); } } @@ -4577,7 +4592,7 @@ impl Analyzer { description: format!("builtins.exec (command execution) in {}", file_path), recommended_attack: vec![AttackAxis::Cpu, AttackAxis::Disk], suppressed: false, - test_context: None, + test_context: None, }); } @@ -4604,6 +4619,9 @@ impl Analyzer { // Shell scripting // ============================================================ + /// Accumulate shell I/O counts and append heuristic command, permission and + /// temporary-path findings for `file_path`. Unquoted expansion findings require + /// more than 20 `$VAR` matches after quoted string contents are removed. fn analyze_shell( &self, content: &str, @@ -4619,8 +4637,9 @@ impl Analyzer { // statement position (start of line, or after whitespace/`;`/`&`/`|`/`(`) // so that `--eval` / `-eval` CLI flags and identifiers like `myeval` // are not flagged. - let shell_eval = RE_SHELL_EVAL_BUILTIN - .get_or_init(|| Regex::new(r"(?m)(?:^|[\s;&|(])eval[ \t]").expect("static regex is valid")); + let shell_eval = RE_SHELL_EVAL_BUILTIN.get_or_init(|| { + Regex::new(r"(?m)(?:^|[\s;&|(])eval[ \t]").expect("static regex is valid") + }); if shell_eval.is_match(content) { weak_points.push(WeakPoint { file: None, @@ -5036,6 +5055,8 @@ impl Analyzer { // Cross-language security checks (run on ALL files) // ============================================================ + /// Append heuristic protocol, hardcoded secret and excessive comment-marker + /// findings located at `file_path`, independently of the detected language. fn analyze_cross_language( &self, content: &str, @@ -5105,15 +5126,18 @@ impl Analyzer { // "passwd\s*=" do not appear contiguously on any single source // line — the estate-wide secret-scanner grep otherwise self-flags // this file (it is the scanner's own pattern data). - let secret_re = RE_HARDCODED_SECRET.get_or_init(|| Regex::new( - concat!( + let secret_re = RE_HARDCODED_SECRET.get_or_init(|| { + Regex::new(concat!( r#"(?i)(api[_-]?key|api[_-]?secret|"#, - "p", "assword|", - "p", "asswd|", + "p", + "assword|", + "p", + "asswd|", r#"secret[_-]?key|access[_-]?token|private[_-]?key)"#, r#"\s*[=:]\s*["'][^"']{8,}"#, - ) - ).expect("static regex is valid")); + )) + .expect("static regex is valid") + }); if secret_re.is_match(content) { weak_points.push(WeakPoint { file: None, @@ -5196,7 +5220,7 @@ impl Analyzer { ), recommended_attack: vec![], suppressed: false, - test_context: None, + test_context: None, }); } @@ -5215,7 +5239,7 @@ impl Analyzer { .to_string(), recommended_attack: vec![], suppressed: false, - test_context: None, + test_context: None, }); } } @@ -5241,7 +5265,7 @@ impl Analyzer { .to_string(), recommended_attack: vec![], suppressed: false, - test_context: None, + test_context: None, }); } } @@ -5275,7 +5299,7 @@ impl Analyzer { ), recommended_attack: vec![], suppressed: false, - test_context: None, + test_context: None, }); } } @@ -5331,7 +5355,7 @@ impl Analyzer { .to_string(), recommended_attack: vec![], suppressed: false, - test_context: None, + test_context: None, }); } } @@ -6904,7 +6928,10 @@ fn one_unwrap() { .filter(|wp| wp.category == WeakPointCategory::PanicPath) .collect(); - assert!(!panic_points.is_empty(), "A production unwrap must be reported"); + assert!( + !panic_points.is_empty(), + "A production unwrap must be reported" + ); } // --------------------------------------------------------------- diff --git a/src/assail/mod.rs b/src/assail/mod.rs index eb4e53b..aaf369e 100644 --- a/src/assail/mod.rs +++ b/src/assail/mod.rs @@ -402,21 +402,23 @@ pub fn apply_suppression(report: &mut AssailReport) { report.suppressed_count = count; } -/// v2.5.5 context-aware FP suppression pass — applies four checks per +/// v2.5.5 context-aware FP suppression pass — applies three checks per /// finding: /// 1. Inline `panic-attack: accepted` marker on the same or -/// preceding line ([[comment_marker]]). +/// preceding line, when `wp.line` is known ([[comment_marker]]). /// 2. PanicPath finding in a TestOnly or Doc test_context /// ([[test_context]]). /// 3. UnsafeFFI finding where the file's [[ffi_kind]] is /// `BuildSystem` or `TestMock` (audit-accepted by default). -/// 4. (jit_context wire-up handled inline in analyzer.rs:1117..1129 -/// already; not duplicated here.) /// -/// Sets `wp.test_context` for every finding with a known file path so +/// JIT context handling remains in the transmute classification block of +/// `Analyzer::analyze_rust` in `analyzer.rs`. +/// +/// Fills missing `wp.test_context` for findings with a known file path so /// downstream audit consumers can render it. Returns the number of /// findings newly flipped from `suppressed=false` to `suppressed=true` /// (already-suppressed findings are NOT re-counted). +/// File read errors disable marker checks and use path-only context classification. pub fn apply_v255_context_suppression(report: &mut AssailReport) -> usize { use crate::comment_marker; use crate::ffi_kind::FfiKind; @@ -432,7 +434,9 @@ pub fn apply_v255_context_suppression(report: &mut AssailReport) -> usize { // fall back to parsing `location` ("path:line"). let file_path: Option = wp.file.clone().or_else(|| { wp.location.as_ref().and_then(|loc| { - loc.rsplit_once(':').map(|(p, _)| p.to_string()).or_else(|| Some(loc.clone())) + loc.rsplit_once(':') + .map(|(p, _)| p.to_string()) + .or_else(|| Some(loc.clone())) }) }); @@ -708,7 +712,7 @@ mod classifications_tests { description: "8 unsafe blocks".to_string(), recommended_attack: vec![AttackAxis::Memory], suppressed: false, - test_context: None, + test_context: None, }, WeakPoint { file: None, @@ -719,7 +723,7 @@ mod classifications_tests { description: "unsafe block".to_string(), recommended_attack: vec![AttackAxis::Memory], suppressed: false, - test_context: None, + test_context: None, }, ], statistics: ProgramStatistics { @@ -758,8 +762,8 @@ mod classifications_tests { mod v255_context_suppression_tests { use super::apply_v255_context_suppression; use crate::types::{ - AssailReport, AttackAxis, ProgramStatistics, Severity, TestContext, - WeakPoint, WeakPointCategory, + AssailReport, AttackAxis, ProgramStatistics, Severity, TestContext, WeakPoint, + WeakPointCategory, }; use std::fs; use tempfile::TempDir; @@ -798,10 +802,7 @@ mod v255_context_suppression_tests { #[test] fn panic_path_in_test_file_is_suppressed() { - let mut report = report_with(vec![wp( - WeakPointCategory::PanicPath, - "tests/foo.rs:42", - )]); + let mut report = report_with(vec![wp(WeakPointCategory::PanicPath, "tests/foo.rs:42")]); let n = apply_v255_context_suppression(&mut report); assert_eq!(n, 1); assert!(report.weak_points[0].suppressed); @@ -813,10 +814,7 @@ mod v255_context_suppression_tests { #[test] fn panic_path_in_prod_file_not_suppressed() { - let mut report = report_with(vec![wp( - WeakPointCategory::PanicPath, - "src/main.rs:42", - )]); + let mut report = report_with(vec![wp(WeakPointCategory::PanicPath, "src/main.rs:42")]); let n = apply_v255_context_suppression(&mut report); assert_eq!(n, 0); assert!(!report.weak_points[0].suppressed); @@ -828,8 +826,7 @@ mod v255_context_suppression_tests { #[test] fn unsafe_ffi_in_build_zig_is_suppressed() { - let mut report = - report_with(vec![wp(WeakPointCategory::UnsafeFFI, "build.zig:5")]); + let mut report = report_with(vec![wp(WeakPointCategory::UnsafeFFI, "build.zig:5")]); let n = apply_v255_context_suppression(&mut report); assert_eq!(n, 1); assert!(report.weak_points[0].suppressed); @@ -851,7 +848,11 @@ mod v255_context_suppression_tests { let tmp = TempDir::new().unwrap(); let path = tmp.path().join("foo.rs"); // Line 2 has an unwrap; line 1 has the marker. - fs::write(&path, "// panic-attack: accepted - test\nlet x = foo.unwrap();\n").unwrap(); + fs::write( + &path, + "// panic-attack: accepted - test\nlet x = foo.unwrap();\n", + ) + .unwrap(); let mut report = report_with(vec![wp( WeakPointCategory::UnsafeCode, diff --git a/src/assay/mod.rs b/src/assay/mod.rs index b1fb21c..4cd34f0 100644 --- a/src/assay/mod.rs +++ b/src/assay/mod.rs @@ -277,11 +277,15 @@ pub fn run(config: AssayConfig) -> Result { }) } +/// Write the assay report as pretty-printed JSON, creating parent directories +/// and replacing any existing file at `path`. +/// +/// # Errors +/// Returns directory creation, JSON serialisation and file write errors. pub fn write_report(report: &AssayReport, path: &Path) -> Result<()> { if let Some(parent) = path.parent() { if !parent.as_os_str().is_empty() { - fs::create_dir_all(parent) - .with_context(|| format!("creating {}", parent.display()))?; + fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?; } } let json = serde_json::to_string_pretty(report).context("serializing assay report")?; @@ -349,6 +353,20 @@ pub struct AssimilateConfig { pub dry_run: bool, } +/// Apply selected proven-module substitutions and return provenance records and notes. +/// +/// `apply_all` selects offered candidates; otherwise `candidate_id` is required. +/// Already-applied candidates are skipped. The `from` override requires exactly +/// one selected candidate. Call sites are returned for manual rewiring. +/// +/// Unless `dry_run` is set, copy replacement files, preserve existing `.orig` +/// backups and write provenance under the target's `.assimilated/` directory. +/// Dry runs still resolve and hash sources and existing destination files. +/// +/// # Errors +/// Returns survey errors, invalid candidate or source selection errors, and errors +/// from hashing, directory creation, file copying or provenance serialisation/writing. +/// Earlier file changes remain in place if a later operation fails. pub fn assimilate(config: AssimilateConfig) -> Result { let assay = run(AssayConfig { target: config.target.clone(), @@ -479,8 +497,9 @@ pub fn assimilate(config: AssimilateConfig) -> Result { if let Some(bak) = &backup { // Never clobber a pre-existing .orig backup. if !bak.exists() { - fs::copy(&dest, bak) - .with_context(|| format!("backing up {} -> {}", dest.display(), bak.display()))?; + fs::copy(&dest, bak).with_context(|| { + format!("backing up {} -> {}", dest.display(), bak.display()) + })?; } } fs::copy(&source, &dest) @@ -684,7 +703,9 @@ mod tests { // Destination now holds the proven content. let dest = target.path().join("src/safe_url.rs"); - assert!(fs::read_to_string(&dest).unwrap().contains("PROVEN SafeUrl")); + assert!(fs::read_to_string(&dest) + .unwrap() + .contains("PROVEN SafeUrl")); // Backup holds the old content. let bak = rec.backup.as_ref().unwrap(); assert!(fs::read_to_string(bak).unwrap().contains("OLD placeholder")); @@ -698,11 +719,7 @@ mod tests { let proven = TempDir::new().unwrap(); let tsrc = target.path().join("src"); fs::create_dir_all(&tsrc).unwrap(); - fs::write( - tsrc.join("storage.rs"), - "VERISIMDB_URL env access here\n", - ) - .unwrap(); + fs::write(tsrc.join("storage.rs"), "VERISIMDB_URL env access here\n").unwrap(); let psrc = proven.path().join("ports"); fs::create_dir_all(&psrc).unwrap(); fs::write(psrc.join("safe_url.rs"), "// PROVEN\n").unwrap(); diff --git a/src/bridge/mod.rs b/src/bridge/mod.rs index a39e3c9..66e5ee3 100644 --- a/src/bridge/mod.rs +++ b/src/bridge/mod.rs @@ -12,7 +12,7 @@ //! This module extends panic-attack with `bridge` subcommands. The core tool //! remains standalone — PanLL panel and BoJ cartridge are optional integrations. //! -//! Design document: docs/patch-bridge-design.md +//! Design document: docs/patch-bridge-design.adoc pub mod classify; pub mod intelligence; diff --git a/src/bridge/registry.rs b/src/bridge/registry.rs index 0e55eca..9749606 100644 --- a/src/bridge/registry.rs +++ b/src/bridge/registry.rs @@ -7,7 +7,7 @@ //! Phase 2 will add VeriSimDB hexad persistence and SCM format. //! //! Lifecycle: Applied → Active → Retiring → Retired -//! See docs/patch-bridge-design.md Section 8 for full lifecycle specification. +//! See docs/patch-bridge-design.adoc Section 8 for full lifecycle specification. use super::{AssessedCve, Classification}; use anyhow::{Context, Result}; diff --git a/src/comment_marker.rs b/src/comment_marker.rs index cb47620..eab824e 100644 --- a/src/comment_marker.rs +++ b/src/comment_marker.rs @@ -229,10 +229,7 @@ mod tests { let m = parse_accepted_marker_in_line( "// panic-attack: accepted (PA005 — manual audit 2026-06-02)", ); - assert_eq!( - m, - Some("(PA005 — manual audit 2026-06-02)".to_string()) - ); + assert_eq!(m, Some("(PA005 — manual audit 2026-06-02)".to_string())); } #[test] diff --git a/src/diagnostics.rs b/src/diagnostics.rs index 7ddb57b..5890014 100644 --- a/src/diagnostics.rs +++ b/src/diagnostics.rs @@ -11,6 +11,12 @@ const HYPATIA_ENV: &str = "HYPATIA_API_KEY"; const GITBOT_FLEET_ENV: &str = "GITBOT_FLEET_ENDPOINT"; const PANICBOT_DIRECTIVE_PATH: &str = ".machine_readable/bot_directives/panicbot.scm"; +/// Print readiness checks using `manifest`, paths relative to the current +/// working directory and the configured integration environment variables. +/// +/// # Errors +/// Returns an error after printing all results if any check has error severity. +/// Warnings alone do not cause failure. pub fn run_self_diagnostics(manifest: &Manifest) -> Result<()> { println!("panic-attack self-diagnostics"); @@ -41,11 +47,11 @@ pub fn run_self_diagnostics(manifest: &Manifest) -> Result<()> { checks.push(check_verisimdb(Path::new("verisimdb-data/verisimdb"))); checks.push(check_file( "ambush timeline spec", - Path::new("docs/ambush-timeline.md"), + Path::new("docs/ambush-timeline.adoc"), )); checks.push(check_file( "panll export guide", - Path::new("docs/panll-export.md"), + Path::new("docs/panll-export.adoc"), )); checks.push(check_watcher("Hypatia scanner", HYPATIA_ENV)); @@ -193,8 +199,11 @@ fn check_watcher(label: &'static str, env_key: &str) -> Diagnostic { /// Verifies: /// 1. Bot directives file exists at `.machine_readable/bot_directives/panicbot.scm` /// 2. JSON output contract: AssailReport serialises with the fields panicbot expects -/// (`program_path`, `weak_points`, `language`, `statistics`) +/// (`program_path`, `weak_points`, `language`) /// 3. WeakPointCategory and Severity serialise in PascalCase (no serde rename_all) +/// +/// Failed JSON checks, including serialisation errors, yield an error diagnostic; +/// missing directives alone yield a warning. fn check_panicbot_readiness() -> Diagnostic { let directive_path = Path::new(PANICBOT_DIRECTIVE_PATH); let has_directives = directive_path.is_file(); @@ -214,7 +223,7 @@ fn check_panicbot_readiness() -> Diagnostic { description: "test".to_string(), recommended_attack: vec![], suppressed: false, - test_context: None, + test_context: None, }], statistics: crate::types::ProgramStatistics::default(), file_statistics: vec![], diff --git a/src/ffi_kind.rs b/src/ffi_kind.rs index 66312d2..79874e4 100644 --- a/src/ffi_kind.rs +++ b/src/ffi_kind.rs @@ -87,13 +87,16 @@ fn is_build_system_path(p: &str) -> bool { || p.contains("build.cargo/") } +/// Recognise test mock paths by case-sensitive test-scope and mock/stub substrings. fn is_test_mock_path(p: &str) -> bool { // Test mocks live under tests/ AND have "mock" or "stub" in the // path. Plain test files are still RuntimeAbi if they call the // real FFI surface (they exercise it). let has_test_scope = p.contains("tests/") || p.contains("test/"); - let has_mock_indicator = - p.contains("/mocks/") || p.contains("_mock.") || p.contains("/stubs/") || p.contains("_stub."); + let has_mock_indicator = p.contains("/mocks/") + || p.contains("_mock.") + || p.contains("/stubs/") + || p.contains("_stub."); has_test_scope && has_mock_indicator } @@ -118,9 +121,11 @@ fn is_runtime_abi_path(p: &str) -> bool { /// project's pre-approved FFI boundary audit (typically /// `audits/audit-ffi-unsafe.md`). /// -/// Implementation note: this is a placeholder that parses the audit -/// file when present and returns false otherwise. Wire-in to the -/// analyzer happens in the follow-up PR. +/// Reads the supplied `audit_text` without accessing the filesystem. Section +/// headings `Approved boundaries`, `Approved FFI boundaries` and `Pre-approved +/// boundaries` are recognised case-insensitively at Markdown level two. Within +/// them, the first token after an optional list marker is compared exactly with +/// `file_path`, ignoring surrounding backticks. Returns false if no token matches. pub fn is_audited_boundary(audit_text: &str, file_path: &str) -> bool { // The audit file's convention is a `## Approved boundaries` section // with a markdown list of `- file/path/here.zig — rationale` lines. @@ -128,10 +133,9 @@ pub fn is_audited_boundary(audit_text: &str, file_path: &str) -> bool { for line in audit_text.lines() { let trimmed = line.trim(); if trimmed.starts_with("## ") { - in_approved_section = - trimmed.eq_ignore_ascii_case("## Approved boundaries") - || trimmed.eq_ignore_ascii_case("## Approved FFI boundaries") - || trimmed.eq_ignore_ascii_case("## Pre-approved boundaries"); + in_approved_section = trimmed.eq_ignore_ascii_case("## Approved boundaries") + || trimmed.eq_ignore_ascii_case("## Approved FFI boundaries") + || trimmed.eq_ignore_ascii_case("## Pre-approved boundaries"); continue; } if !in_approved_section { @@ -160,10 +164,7 @@ mod tests { #[test] fn build_zig_classified_build_system() { - assert_eq!( - FfiKind::classify_by_path("build.zig"), - FfiKind::BuildSystem - ); + assert_eq!(FfiKind::classify_by_path("build.zig"), FfiKind::BuildSystem); assert_eq!( FfiKind::classify_by_path("src/foo/build.zig"), FfiKind::BuildSystem @@ -209,10 +210,7 @@ mod tests { #[test] fn unrecognised_path_is_unknown() { - assert_eq!( - FfiKind::classify_by_path("src/main.rs"), - FfiKind::Unknown - ); + assert_eq!(FfiKind::classify_by_path("src/main.rs"), FfiKind::Unknown); assert_eq!( FfiKind::classify_by_path("examples/hello.zig"), FfiKind::Unknown diff --git a/src/jit_context.rs b/src/jit_context.rs index 874ec09..d165441 100644 --- a/src/jit_context.rs +++ b/src/jit_context.rs @@ -81,12 +81,12 @@ pub fn classify_rust(content: &str) -> JitContext { } } +/// Recognise Cranelift imports or JIT API usage by case-sensitive substring matches. fn is_cranelift(content: &str) -> bool { content.contains("cranelift_jit::JITModule") || content.contains("cranelift_module::Module") || content.contains("JITModule::new(") - || (content.contains("cranelift_jit") - && content.contains("get_finalized_function")) + || (content.contains("cranelift_jit") && content.contains("get_finalized_function")) || content.contains("use cranelift_jit") || content.contains("use cranelift_module") } @@ -114,9 +114,10 @@ fn is_javascript_embedding(content: &str) -> bool { || (content.contains("v8::") && content.contains("Function::call")) } -/// True if a `mem::transmute` (or `transmute_copy`) in this content -/// targets a function-pointer type. Pattern-matches the standard -/// turbofish forms and let-binding forms documented in +/// Look for `transmute` syntax suggesting a function-pointer target. +/// Recognises selected turbofish forms or a function-pointer annotation occurring +/// anywhere alongside a `transmute(` call, without verifying they belong to the +/// same expression. `transmute_copy` is not recognised. See the related forms in /// `src/assail/analyzer.rs:1110..1129`. pub fn transmute_targets_fn_ptr(content: &str) -> bool { // Turbofish forms — unambiguous. @@ -131,8 +132,7 @@ pub fn transmute_targets_fn_ptr(content: &str) -> bool { // `: unsafe fn(...)`) annotation co-occurs with a `transmute(` // call. The `=` may have intervening tokens (`unsafe { ... }`, // closure wrappers, etc.), so we don't require it adjacent. - let has_fn_annotation = - content.contains(": fn(") || content.contains(": unsafe fn("); + let has_fn_annotation = content.contains(": fn(") || content.contains(": unsafe fn("); let has_transmute_call = content.contains("transmute(") || content.contains("mem::transmute(") || content.contains("std::mem::transmute("); @@ -157,8 +157,7 @@ mod tests { #[test] fn llvm_inkwell_classified() { - let src = - "use inkwell::execution_engine::ExecutionEngine;\nfn run() {}\n"; + let src = "use inkwell::execution_engine::ExecutionEngine;\nfn run() {}\n"; assert_eq!(classify_rust(src), JitContext::Llvm); } @@ -203,15 +202,13 @@ mod tests { #[test] fn transmute_to_fn_ptr_turbofish_recognised() { - let src = - "let f: fn() = unsafe { std::mem::transmute::<_, fn()>(ptr) };"; + let src = "let f: fn() = unsafe { std::mem::transmute::<_, fn()>(ptr) };"; assert!(transmute_targets_fn_ptr(src)); } #[test] fn transmute_to_fn_ptr_let_binding_recognised() { - let src = - "let f: fn(i64) -> i64 = unsafe { mem::transmute(raw_ptr) };"; + let src = "let f: fn(i64) -> i64 = unsafe { mem::transmute(raw_ptr) };"; assert!(transmute_targets_fn_ptr(src)); } @@ -223,8 +220,7 @@ mod tests { #[test] fn transmute_to_non_fn_not_recognised() { - let src = - "let n: u64 = unsafe { std::mem::transmute::<_, u64>(value) };"; + let src = "let n: u64 = unsafe { std::mem::transmute::<_, u64>(value) };"; assert!(!transmute_targets_fn_ptr(src)); } diff --git a/src/kanren/core.rs b/src/kanren/core.rs index 1de1ee8..e0730c4 100644 --- a/src/kanren/core.rs +++ b/src/kanren/core.rs @@ -1340,7 +1340,7 @@ mod tests { description: description.to_string(), recommended_attack: vec![], suppressed: false, - test_context: None, + test_context: None, } } diff --git a/src/lib.rs b/src/lib.rs index 14e2985..cb28a3a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,6 @@ pub mod assemblyline; pub mod attack; pub mod attestation; pub mod axial; -pub mod scan; #[cfg(feature = "http")] pub mod bridge; pub mod campaign; @@ -42,6 +41,7 @@ pub mod panll; pub mod query; pub mod report; pub mod safe_path; +pub mod scan; pub mod signatures; pub mod storage; pub mod sweep_tracker; diff --git a/src/main.rs b/src/main.rs index 41ab06c..adede31 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,39 +7,34 @@ //! concurrency) and detecting bug signatures using logic programming techniques inspired by //! Mozart/Oz and Datalog. -mod a2ml; -mod abduct; -mod adjudicate; -mod aggregate; -mod ambush; -mod amuck; -mod assail; -mod assay; -mod assemblyline; -mod attack; -mod attestation; -mod axial; +use panic_attack::a2ml; +use panic_attack::abduct; +use panic_attack::adjudicate; +use panic_attack::aggregate; +use panic_attack::ambush; +use panic_attack::amuck; +use panic_attack::assail; +use panic_attack::assay; +use panic_attack::assemblyline; +use panic_attack::attack; +use panic_attack::attestation; +use panic_attack::axial; #[cfg(feature = "http")] -mod bridge; -mod campaign; -mod comment_marker; +use panic_attack::bridge; +use panic_attack::campaign; mod diagnostics; -mod ffi_kind; mod groove; -mod i18n; -mod jit_context; -mod kanren; +use panic_attack::i18n; mod kin; -mod mass_panic; -mod notify; -mod panll; -mod query; -mod report; -mod signatures; -mod storage; -mod sweep_tracker; -mod test_context; -mod types; +use panic_attack::mass_panic; +use panic_attack::notify; +use panic_attack::panll; +use panic_attack::query; +use panic_attack::report; +use panic_attack::signatures; +use panic_attack::storage; +use panic_attack::sweep_tracker; +use panic_attack::types; extern crate walkdir; @@ -1370,6 +1365,13 @@ fn main() -> Result<()> { result } +/// Parse process arguments and execute the selected command, including its output +/// and requested persistence. Default manifest settings are used if loading fails. +/// Argument parsing exits the process for help or invalid CLI syntax. +/// +/// # Errors +/// Returns command validation errors and propagates I/O, serialisation and +/// operation errors that are not handled by a command-specific fallback. fn run_main() -> Result<()> { let cli = Cli::parse(); let manifest = match Manifest::load_default() { @@ -2734,9 +2736,8 @@ fn run_main() -> Result<()> { PanicAttackHexad, }; - let json = std::fs::read_to_string(&hexad).with_context(|| { - format!("reading hexad file {}", hexad.display()) - })?; + let json = std::fs::read_to_string(&hexad) + .with_context(|| format!("reading hexad file {}", hexad.display()))?; let hexad_value: PanicAttackHexad = serde_json::from_str(&json) .with_context(|| format!("parsing hexad JSON at {}", hexad.display()))?; diff --git a/src/panll/mod.rs b/src/panll/mod.rs index a183635..4f26a25 100644 --- a/src/panll/mod.rs +++ b/src/panll/mod.rs @@ -689,7 +689,7 @@ mod tests { description: "unwrap on None".to_string(), recommended_attack: vec![], suppressed: false, - test_context: None, + test_context: None, }, WeakPoint { category: WeakPointCategory::UnsafeCode, @@ -700,7 +700,7 @@ mod tests { description: "unsafe block".to_string(), recommended_attack: vec![], suppressed: false, - test_context: None, + test_context: None, }, ], vec![], diff --git a/src/report/gui.rs b/src/report/gui.rs index 014c3fa..549560b 100644 --- a/src/report/gui.rs +++ b/src/report/gui.rs @@ -120,8 +120,11 @@ impl ReportGui { } impl App for ReportGui { + /// Render the file loader, navigation and selected report tab. + /// A load-button click or Enter after the path field loses focus attempts + /// to read the specified JSON file, updating the viewer or its status message. fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut Frame) { - egui::Panel::top("header").show_inside(ui, |ui| { + egui::Panel::top("header").show(ui, |ui| { ui.horizontal(|ui| { ui.heading("panic-attack report"); ui.separator(); @@ -138,7 +141,7 @@ impl App for ReportGui { }); }); - egui::Panel::left("nav").show_inside(ui, |ui| { + egui::Panel::left("nav").show(ui, |ui| { ui.selectable_value(&mut self.tab, ReportTab::Summary, "Summary"); ui.selectable_value(&mut self.tab, ReportTab::Assail, "Assail"); ui.selectable_value(&mut self.tab, ReportTab::Matrix, "Matrix"); @@ -159,7 +162,7 @@ impl App for ReportGui { ui.selectable_value(&mut self.tab, ReportTab::Temporal, temporal_label); }); - egui::CentralPanel::default().show_inside(ui, |ui| match self.tab { + egui::CentralPanel::default().show(ui, |ui| match self.tab { ReportTab::Summary => self.render_summary(ui), ReportTab::Assail => self.render_assail(ui), ReportTab::Matrix => self.render_matrix(ui), diff --git a/src/safe_path.rs b/src/safe_path.rs index 8c63a9e..ec06273 100644 --- a/src/safe_path.rs +++ b/src/safe_path.rs @@ -62,7 +62,7 @@ const SAFE_PUNCT: &[char] = &['-', '_', '.', '~']; /// assert!(!has_traversal("foo..bar")); // .. inside a name is not a segment /// ``` pub fn has_traversal(path: &str) -> bool { - for segment in path.split(|c| c == '/' || c == '\\') { + for segment in path.split(['/', '\\']) { if segment == ".." { return true; } @@ -100,11 +100,13 @@ pub fn sanitize_filename(filename: &str) -> String { } } +#[cfg(test)] /// Returns true if `c` is a character `sanitize_filename` would keep. fn is_safe_char(c: char) -> bool { c.is_ascii_alphanumeric() || SAFE_PUNCT.contains(&c) } +#[cfg(test)] /// Returns true if every character in `s` is safe. Used by the /// idempotence + non-empty proptest invariants. fn all_chars_safe(s: &str) -> bool { @@ -191,7 +193,10 @@ mod tests { #[test] fn sanitize_keeps_safe_punct() { - assert_eq!(sanitize_filename("my-file_2026.06.02"), "my-file_2026.06.02"); + assert_eq!( + sanitize_filename("my-file_2026.06.02"), + "my-file_2026.06.02" + ); assert_eq!(sanitize_filename("config~"), "config~"); } @@ -285,7 +290,7 @@ mod tests { #[test] fn prop_has_traversal_operational(s in ".*") { let split_says: bool = s - .split(|c| c == '/' || c == '\\') + .split(['/', '\\']) .any(|seg| seg == ".."); prop_assert_eq!( has_traversal(&s), diff --git a/src/scan/cargodeny.rs b/src/scan/cargodeny.rs index 684bce9..e002feb 100644 --- a/src/scan/cargodeny.rs +++ b/src/scan/cargodeny.rs @@ -8,7 +8,7 @@ // feedback_no_automated_licence_edits. use crate::types::{Severity, WeakPoint, WeakPointCategory}; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use std::process::Command; @@ -112,10 +112,18 @@ fn code_to_category(code: &str) -> WeakPointCategory { } } -/// Run cargo-deny check and parse output +/// Run `cargo deny check` in the configured workspace and return its diagnostic findings. +/// +/// Standard output must be UTF-8 JSON matching `DenyOutput`, regardless of +/// `config.format`. A non-zero exit status still yields findings if parsing succeeds. +/// Missing severity and code fields default to Medium and Advisory respectively. +/// `config.deny_toml` is currently ignored. +/// +/// # Errors +/// Returns process execution errors, invalid UTF-8 errors and JSON deserialisation errors. pub fn run_cargo_deny(config: &CargoDenyConfig) -> Result> { let workspace_root = &config.workspace_root; - + // Build cargo deny command let mut cmd = Command::new("cargo"); cmd.arg("deny") @@ -123,17 +131,17 @@ pub fn run_cargo_deny(config: &CargoDenyConfig) -> Result> { .arg("--format") .arg(&config.format) .current_dir(workspace_root); - + // Add optional flags if config.fail_on_warnings { cmd.arg("--fail-on-warnings"); } - + // Add extra args for arg in &config.extra_args { cmd.arg(arg); } - + // Execute command let output = cmd.output().with_context(|| { format!( @@ -141,7 +149,7 @@ pub fn run_cargo_deny(config: &CargoDenyConfig) -> Result> { workspace_root.display() ) })?; - + // Check exit status - cargo-deny returns non-zero on failures // but we still want to parse the output if !output.status.success() { @@ -152,35 +160,35 @@ pub fn run_cargo_deny(config: &CargoDenyConfig) -> Result> { workspace_root.display() ); } - + // Parse JSON output - let stdout = String::from_utf8(output.stdout) - .with_context(|| "cargo deny output is not valid UTF-8")?; - - let deny_output: DenyOutput = serde_json::from_str(&stdout) - .with_context(|| "failed to parse cargo deny JSON output")?; - + let stdout = + String::from_utf8(output.stdout).with_context(|| "cargo deny output is not valid UTF-8")?; + + let deny_output: DenyOutput = + serde_json::from_str(&stdout).with_context(|| "failed to parse cargo deny JSON output")?; + // Convert diagnostics to WeakPoints let mut weak_points = Vec::new(); for diagnostic in deny_output.diagnostics { - let severity = diagnostic.severity + let severity = diagnostic + .severity .map(|s| parse_deny_severity(&s).into()) .unwrap_or(Severity::Medium); - - let category = diagnostic.code + + let category = diagnostic + .code .map(|c| code_to_category(&c)) .unwrap_or(WeakPointCategory::Advisory); - - let location = diagnostic.location.map(|loc| { + + let location = diagnostic.location.and_then(|loc| { if let (Some(ref file), Some(line)) = (&loc.file, loc.line) { Some(format!("{}:{}", file, line)) - } else if let Some(ref file) = loc.file { - Some(file.clone()) } else { - None + loc.file.clone() } - }).flatten(); - + }); + // Parse file and line from location string let (file, line) = if let Some(ref loc) = location { if let Some((f, l)) = loc.rsplit_once(':') { @@ -195,7 +203,7 @@ pub fn run_cargo_deny(config: &CargoDenyConfig) -> Result> { } else { (None, None) }; - + let weak_point = WeakPoint { category, location: location.clone(), @@ -207,10 +215,10 @@ pub fn run_cargo_deny(config: &CargoDenyConfig) -> Result> { suppressed: false, test_context: None, }; - + weak_points.push(weak_point); } - + Ok(weak_points) } @@ -257,44 +265,60 @@ impl From for CargoDenyWeakPoint { } } -/// Parse cargo-deny SARIF output (alternative to JSON) +/// Convert cargo-deny SARIF results into findings, using only the first location +/// of each result. Missing levels default to Medium severity; missing or +/// unrecognised rule IDs default to Advisory. +/// +/// # Errors +/// Returns JSON parsing errors, or an error if `runs` or any run's `results` is +/// absent or is not an array. Empty `runs` and `results` arrays are valid. #[cfg(feature = "sarif")] pub fn parse_sarif_output(sarif_json: &str) -> Result> { use serde_json::Value; - + let v: Value = serde_json::from_str(sarif_json)?; - + // SARIF structure: { runs: [{ results: [{ ... }] }] } - let runs = v["runs"].as_array().ok_or_else(|| anyhow!("no runs in SARIF"))?; + let runs = v["runs"] + .as_array() + .ok_or_else(|| anyhow::anyhow!("no runs in SARIF"))?; let mut weak_points = Vec::new(); - + for run in runs { - let results = run["results"].as_array().ok_or_else(|| anyhow!("no results in SARIF run"))?; + let results = run["results"] + .as_array() + .ok_or_else(|| anyhow::anyhow!("no results in SARIF run"))?; for result in results { - let message = result["message"]["text"].as_str() + let message = result["message"]["text"] + .as_str() .or_else(|| result["message"].as_str()) - .unwrap_or("Unknown cargo-deny finding").to_string(); - + .unwrap_or("Unknown cargo-deny finding") + .to_string(); + let severity_str = result["level"].as_str().unwrap_or("warning"); let severity = parse_deny_severity(severity_str).into(); - + let rule_id = result["ruleId"].as_str().map(|s| s.to_string()); let category = rule_id .as_ref() .map(|c| code_to_category(c)) .unwrap_or(WeakPointCategory::Advisory); - + let locations = result["locations"].as_array(); let (location, file, line) = if let Some(locs) = locations { if !locs.is_empty() { let loc = &locs[0]; let physical_loc = &loc["physicalLocation"]; - let file_path = physical_loc["artifactLocation"]["uri"].as_str() + let file_path = physical_loc["artifactLocation"]["uri"] + .as_str() .or_else(|| physical_loc["artifactLocation"]["uriBaseId"].as_str()) .or_else(|| loc["physicalLocation"]["artifactLocation"]["uri"].as_str()); - let line_num = physical_loc["region"]["startLine"].as_u64() - .or_else(|| physical_loc["region"]["startLine"].as_i64().map(|n| n as u64)); - + let line_num = physical_loc["region"]["startLine"].as_u64().or_else(|| { + physical_loc["region"]["startLine"] + .as_i64() + .map(|n| n as u64) + }); + let location = file_path.map(|f| { if let Some(ln) = line_num { format!("{}:{}", f, ln) @@ -302,15 +326,19 @@ pub fn parse_sarif_output(sarif_json: &str) -> Result> { f.to_string() } }); - - (location, file_path.map(|s| s.to_string()), line_num.map(|n| n as u32)) + + ( + location, + file_path.map(|s| s.to_string()), + line_num.map(|n| n as u32), + ) } else { (None, None, None) } } else { (None, None, None) }; - + let wp = WeakPoint { category, location: location.clone(), @@ -322,19 +350,21 @@ pub fn parse_sarif_output(sarif_json: &str) -> Result> { suppressed: false, test_context: None, }; - + weak_points.push(wp); } } - + Ok(weak_points) } -/// High-level function to scan a workspace and emit cargo-deny weak points -pub fn scan_workspace( - workspace_root: &Path, - deny_toml: Option<&Path>, -) -> Result> { +/// Scan a workspace with cargo-deny's JSON defaults and return diagnostic findings. +/// The `deny_toml` argument currently has no effect. +/// +/// # Errors +/// Propagates execution, UTF-8 decoding and JSON deserialisation errors from +/// [`run_cargo_deny`]. A non-zero cargo-deny exit status alone is not an error. +pub fn scan_workspace(workspace_root: &Path, deny_toml: Option<&Path>) -> Result> { let config = CargoDenyConfig { workspace_root: workspace_root.to_path_buf(), deny_toml: deny_toml.map(|p| p.to_path_buf()), @@ -342,30 +372,42 @@ pub fn scan_workspace( fail_on_warnings: false, extra_args: Vec::new(), }; - + run_cargo_deny(&config) } #[cfg(test)] mod tests { use super::*; - + #[test] fn test_severity_mapping() { assert_eq!(Severity::from(DenySeverity::Error), Severity::High); assert_eq!(Severity::from(DenySeverity::Warning), Severity::Medium); assert_eq!(Severity::from(DenySeverity::Note), Severity::Low); } - + #[test] fn test_code_to_category() { - assert_eq!(code_to_category("RUSTSEC-2021-0001"), WeakPointCategory::Advisory); - assert_eq!(code_to_category("CVE-2021-12345"), WeakPointCategory::Advisory); + assert_eq!( + code_to_category("RUSTSEC-2021-0001"), + WeakPointCategory::Advisory + ); + assert_eq!( + code_to_category("CVE-2021-12345"), + WeakPointCategory::Advisory + ); assert_eq!(code_to_category("banned"), WeakPointCategory::BannedDep); - assert_eq!(code_to_category("license"), WeakPointCategory::LicensePolicy); - assert_eq!(code_to_category("unknown-source"), WeakPointCategory::UnknownSource); + assert_eq!( + code_to_category("license"), + WeakPointCategory::LicensePolicy + ); + assert_eq!( + code_to_category("unknown-source"), + WeakPointCategory::UnknownSource + ); } - + #[test] fn test_parse_severity() { assert_eq!(parse_deny_severity("error"), DenySeverity::Error); @@ -412,11 +454,14 @@ mod tests { }"#; let deny_output: DenyOutput = serde_json::from_str(json_output).unwrap(); - + // Verify the parsing logic works assert_eq!(deny_output.diagnostics.len(), 3); assert_eq!(deny_output.diagnostics[0].code, Some("license".to_string())); assert_eq!(deny_output.diagnostics[1].code, Some("banned".to_string())); - assert_eq!(deny_output.diagnostics[2].code, Some("RUSTSEC-2021-0001".to_string())); + assert_eq!( + deny_output.diagnostics[2].code, + Some("RUSTSEC-2021-0001".to_string()) + ); } } diff --git a/src/storage/mod.rs b/src/storage/mod.rs index c5dc518..3680f52 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -229,13 +229,16 @@ pub struct MigrationSemantic { pub snapshot_label: Option, } -/// Build a VerisimDB hexad from an assault report +/// Build a VerisimDB hexad from an assault report. +/// +/// # Errors +/// Propagates errors from serialising the report into its JSON document. fn build_hexad(report: &AssaultReport) -> Result { let now = Utc::now(); let id = format!( "pa-{}-{}", now.format("%Y%m%d%H%M%S"), - &uuid_from_timestamp(now.timestamp_millis()) + uuid_from_timestamp(now.timestamp_millis()) ); let critical_count = report @@ -375,6 +378,9 @@ pub fn persist_report( /// /// Unlike single-repo hexads which wrap an AssaultReport, assemblyline /// hexads capture the batch scan results across many repos. +/// +/// # Errors +/// Propagates errors from serialising the report into its JSON document. fn build_assemblyline_hexad( report: &crate::assemblyline::AssemblylineReport, ) -> Result { @@ -382,7 +388,7 @@ fn build_assemblyline_hexad( let id = format!( "pa-asmline-{}-{}", now.format("%Y%m%d%H%M%S"), - &uuid_from_timestamp(now.timestamp_millis()) + uuid_from_timestamp(now.timestamp_millis()) ); let document = serde_json::to_value(report)?; @@ -492,13 +498,13 @@ fn severity_label(severity: &crate::types::Severity) -> &'static str { } } -/// Build one hexad per `WeakPoint` across all repo results in an -/// assemblyline report (issue #33 S1). +/// Build one hexad per unsuppressed `WeakPoint` across repo results with an +/// AssailReport in an assemblyline report (issue #33 S1). /// /// Subject identity lives in `semantic.finding.finding_id`; each emitted -/// hexad's top-level `id` remains per-run-unique so two runs of the same -/// finding produce two distinct hexad files (the join key is the -/// `finding_id`, not the hexad id). +/// hexad's top-level `id` includes a millisecond timestamp and repo/finding +/// indices. IDs are distinct within a call but may repeat across calls in +/// the same millisecond; use `finding_id` as the join key. /// /// `run_id` is shared across every finding-hexad in this run and stamped /// into both `first_seen_run` and `last_seen_run` (S1 has no prior-run @@ -510,7 +516,7 @@ pub fn build_finding_hexads( let run_id = format!( "pa-asmline-{}-{}", now.format("%Y%m%d%H%M%S"), - &uuid_from_timestamp(now.timestamp_millis()) + uuid_from_timestamp(now.timestamp_millis()) ); let mut hexads = Vec::new(); @@ -540,7 +546,7 @@ pub fn build_finding_hexads( now.format("%Y%m%d%H%M%S"), repo_idx, wp_idx, - &uuid_from_timestamp(now.timestamp_millis()), + uuid_from_timestamp(now.timestamp_millis()), ); let document = serde_json::json!({ @@ -662,10 +668,10 @@ fn derive_crosslang_interactions_for_report( /// repo in an assemblyline report (issue #33 kanren-crosslang follow-up). /// /// Subject identity lives in `semantic.crosslang.interaction_id`; the -/// top-level hexad `id` is per-run-unique so two runs of the same -/// interaction produce two distinct hexad files (the join key is the -/// `interaction_id`, not the hexad id). Returns an empty `Vec` when the -/// report has no AssailReport-bearing results or no derivable interactions. +/// top-level hexad `id` includes a millisecond timestamp and repo/interaction +/// indices. IDs are distinct within a call but may repeat across calls in +/// the same millisecond; use `interaction_id` as the join key. Returns an empty +/// `Vec` when the report has no AssailReport-bearing results or no derivable interactions. pub fn build_crosslang_hexads( report: &crate::assemblyline::AssemblylineReport, ) -> Result> { @@ -691,7 +697,7 @@ pub fn build_crosslang_hexads( run_ts, repo_idx, int_idx, - &uuid_from_timestamp(now.timestamp_millis()), + uuid_from_timestamp(now.timestamp_millis()), ); let document = serde_json::json!({ @@ -772,15 +778,16 @@ const HEXAD_FILE_READ_LIMIT: u64 = 16 * 1024 * 1024; /// Build a campaign-state hexad for one lifecycle event (issue #33 S2). /// -/// Append-only: each call produces a fresh hexad with a unique id. The -/// `finding_id` is carried as the semantic subject so the newest hexad -/// per finding is the current state. +/// The id is derived from the current millisecond timestamp, so calls in the +/// same millisecond can produce the same id. This only constructs a hexad; +/// it does not write to storage. The `finding_id` is carried as the semantic +/// subject so stored events can be grouped by finding. pub fn build_campaign_hexad(semantic: CampaignSemantic) -> PanicAttackHexad { let now = Utc::now(); let hexad_id = format!( "pa-campaign-{}-{}", now.format("%Y%m%d%H%M%S"), - &uuid_from_timestamp(now.timestamp_millis()) + uuid_from_timestamp(now.timestamp_millis()) ); PanicAttackHexad { @@ -1393,7 +1400,7 @@ mod tests { description: format!("test finding at {}:{}", file, line), recommended_attack: Vec::new(), suppressed: false, - test_context: None, + test_context: None, } } @@ -1619,7 +1626,7 @@ mod tests { description: "ffi boundary".to_string(), recommended_attack: Vec::new(), suppressed: false, - test_context: None, + test_context: None, } } diff --git a/src/test_context.rs b/src/test_context.rs index 7a5a612..17c2635 100644 --- a/src/test_context.rs +++ b/src/test_context.rs @@ -229,10 +229,7 @@ mod tests { assert_eq!(classify_path("src/foo.ts"), TestContext::Production); assert_eq!(classify_path("src/foo.test.ts"), TestContext::TestOnly); assert_eq!(classify_path("src/foo.spec.js"), TestContext::TestOnly); - assert_eq!( - classify_path("__tests__/foo.tsx"), - TestContext::TestOnly - ); + assert_eq!(classify_path("__tests__/foo.tsx"), TestContext::TestOnly); } #[test] @@ -251,10 +248,7 @@ mod tests { #[test] fn doc_paths_win_over_test() { - assert_eq!( - classify_path("docs/examples/hello.rs"), - TestContext::Doc - ); + assert_eq!(classify_path("docs/examples/hello.rs"), TestContext::Doc); assert_eq!(classify_path("foo.example.py"), TestContext::Doc); } diff --git a/tests/analyzer_tests.rs b/tests/analyzer_tests.rs index a8239a5..67570eb 100644 --- a/tests/analyzer_tests.rs +++ b/tests/analyzer_tests.rs @@ -391,8 +391,8 @@ int* make(void) { } #[test] -fn test_c_analyzer_still_flags_genuinely_unchecked_malloc( -) -> Result<(), Box> { +fn test_c_analyzer_still_flags_genuinely_unchecked_malloc() -> Result<(), Box> +{ let dir = TempDir::new()?; let file = write_test_file( &dir, @@ -416,17 +416,23 @@ int main() { } #[test] -fn test_shell_eval_builtin_flagged_but_not_cli_flag( -) -> Result<(), Box> { +fn test_shell_eval_builtin_flagged_but_not_cli_flag() -> Result<(), Box> { let dir = TempDir::new()?; // `--eval` is a CLI flag, not the shell eval builtin. - let flag = write_test_file(&dir, "flag.sh", "#!/bin/sh\nmybinary --eval \"window.close()\"\n")?; + let flag = write_test_file( + &dir, + "flag.sh", + "#!/bin/sh\nmybinary --eval \"window.close()\"\n", + )?; let report = assail::analyze(&flag)?; let flagged = report .weak_points .iter() .any(|wp| matches!(wp.category, WeakPointCategory::CommandInjection)); - assert!(!flagged, "--eval CLI flag must NOT be flagged as shell eval"); + assert!( + !flagged, + "--eval CLI flag must NOT be flagged as shell eval" + ); // The real eval builtin must still be flagged. let builtin = write_test_file(&dir, "real.sh", "#!/bin/sh\neval \"$user_input\"\n")?; @@ -453,7 +459,10 @@ fn test_js_ffi_eval_symbol_not_flagged() -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box> { #[test] fn readiness_d_diagnostics_runs() { - let (ok, _stdout, stderr) = run(&["diagnostics"]); - assert!(ok, "diagnostics should succeed: {}", stderr); + let (ok, stdout, stderr) = run(&["diagnostics"]); + assert!(ok, "diagnostics should succeed: {stdout}\n{stderr}"); } #[test] @@ -284,7 +284,11 @@ fn cfg_test_panic() { .iter() .filter(|wp| wp.category == WeakPointCategory::PanicPath && !wp.suppressed) .collect(); - assert_eq!(active.len(), 1, "only the production control should remain active"); + assert_eq!( + active.len(), + 1, + "only the production control should remain active" + ); assert_eq!( active[0].location.as_deref(), Some("src/positive_control.rs") @@ -312,12 +316,10 @@ fn cfg_test_panic() { )?; let clean_report = assail::analyze(dir.path())?; assert!( - clean_report + !clean_report .weak_points .iter() - .filter(|wp| wp.category == WeakPointCategory::PanicPath && !wp.suppressed) - .next() - .is_none(), + .any(|wp| wp.category == WeakPointCategory::PanicPath && !wp.suppressed), "excluded panic contexts must produce zero active findings" ); Ok(()) @@ -507,7 +509,13 @@ fn readiness_c_panll_runs() -> Result<(), Box> { #[test] fn readiness_c_diagnostics_output() { let (ok, stdout, stderr) = run(&["diagnostics"]); - assert!(ok, "diagnostics should succeed: {}", stderr); + assert!(ok, "diagnostics should succeed: {stdout}\n{stderr}"); + for guide in ["docs/ambush-timeline.adoc", "docs/panll-export.adoc"] { + assert!( + stdout.contains(&format!("{guide} exists")), + "diagnostics should find the shipped guide {guide}: {stdout}" + ); + } assert!( stdout.contains("panicbot integration"), "diagnostics should check panicbot integration readiness, got: {}", diff --git a/tests/report_tests.rs b/tests/report_tests.rs index b1f8346..bdcb698 100644 --- a/tests/report_tests.rs +++ b/tests/report_tests.rs @@ -27,7 +27,7 @@ fn make_assail_report() -> AssailReport { description: "2 unsafe blocks in src/main.rs".to_string(), recommended_attack: vec![AttackAxis::Memory, AttackAxis::Concurrency], suppressed: false, - test_context: None, + test_context: None, }, WeakPoint { category: WeakPointCategory::PanicPath, @@ -38,7 +38,7 @@ fn make_assail_report() -> AssailReport { description: "5 unwrap/expect calls in src/lib.rs".to_string(), recommended_attack: vec![AttackAxis::Memory], suppressed: false, - test_context: None, + test_context: None, }, ], statistics: ProgramStatistics { @@ -157,7 +157,7 @@ fn test_robustness_score_clamped_to_zero() -> Result<(), Box AssailReport { line: None, recommended_attack: vec![AttackAxis::Memory], suppressed: false, - test_context: None, + test_context: None, }, WeakPoint { category: WeakPointCategory::PanicPath, @@ -38,7 +38,7 @@ fn make_test_report() -> AssailReport { line: None, recommended_attack: vec![], suppressed: false, - test_context: None, + test_context: None, }, ], statistics: ProgramStatistics::default(), diff --git a/tests/seam_contract_tests.rs b/tests/seam_contract_tests.rs index e1c577b..b66d5cb 100644 --- a/tests/seam_contract_tests.rs +++ b/tests/seam_contract_tests.rs @@ -46,7 +46,7 @@ fn minimal_assail_report() -> AssailReport { description: "unwrap calls".to_string(), recommended_attack: vec![AttackAxis::Memory], suppressed: false, - test_context: None, + test_context: None, }], statistics: ProgramStatistics { total_lines: 100,