Skip to content

fix(assail): make assail reports deterministic - #245

Merged
hyperpolymath merged 4 commits into
mainfrom
claude/gallant-bell-4bo4oy-c2-deterministic-assail
Oct 26, 2026
Merged

hyperpolymath merged 4 commits into
mainfrom
claude/gallant-bell-4bo4oy-c2-deterministic-assail

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Follow-up to #237 (JCS reports), as noted in that PR.

Problem

Running assail twice on the same input gave different reports. On main, six runs of examples/vulnerable_program.rs produced six distinct reports. The cause is that these fields were built from HashSet/HashMap iteration order, which Rust randomises per run:

  • frameworks
  • recommended_attacks
  • the taint-matrix rows
  • the dependency-graph edges, despite a code comment claiming "deterministic output"
  • the test-only-function scan

JCS sorts object keys but, by design, leaves array order alone. So since #237, the canonical JSON, and any hash over it such as the --attest seal, differed for identical input.

Fix

  • Those five sites now use BTreeSet/BTreeMap.
  • AttackAxis, Framework and WeakPointCategory now derive PartialOrd and Ord (declaration order). The derives are additive.

Evidence

  • After the fix, runs are byte-identical:
    • six runs on the example file;
    • three runs on examples/;
    • three runs on the whole src/ tree.
  • New test test_assail_report_is_deterministic. It builds a multi-directory fixture whose Cargo.toml triggers several frameworks and asserts that nine analyses give identical canonical JSON. Every HashMap in a process gets its own random seed, so this catches regressions within a single test run.
  • The test is not vacuous. With the old HashMap code restored, it fails with "assail report differs between runs".

Verification

  • cargo fmt --check: clean.
  • cargo build --locked: no warnings.
  • cargo clippy --locked --all-features --all-targets -- -D warnings: clean.
  • cargo test --locked --all-features --no-fail-fast: 693 passed, 0 failed.

Generated by Claude Code

Two runs of `assail` on the same input produced different reports:
frameworks, recommended_attacks, the taint-matrix rows and the
dependency-graph edges (and the test-only-function scan) were built
from HashSet/HashMap iteration order, which is randomised per run. JCS
sorts object keys but by design not arrays, so the canonical JSON, and
any hash over it such as the --attest seal, differed for identical
input. Measured on main: six runs of examples/vulnerable_program.rs
gave six distinct reports.

Use BTreeSet/BTreeMap at those five sites. AttackAxis, Framework and
WeakPointCategory derive PartialOrd and Ord (declaration order); the
derives are additive.

Six runs on the example file, three on examples/ and three on the
whole src/ tree are now byte-identical. A new integration test builds
a multi-directory fixture that detects several frameworks and
recommended attacks, and asserts nine analyses give identical canonical
JSON. With the old HashMap code restored the test fails ("assail report
differs between runs").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PFaqv6iBXrt3W5Akd1Fq5k
@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 7 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 6a53113c-e981-45a6-8b76-c159ee02c734
Commits

Reviewing files that changed from the base of the PR and between b8f9e57 and 6a6aca4.

Files selected for processing: 4
  • CHANGELOG.adoc
  • src/assail/analyzer.rs
  • src/types.rs
  • tests/analyzer_tests.rs

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: f109db83-a5fe-4c73-afe2-0bcec0ac2011
Commits

Reviewing files that changed from the base of the PR and between e2eba7d and b8f9e57.

Files selected for processing: 4
  • CHANGELOG.adoc
  • src/assail/analyzer.rs
  • src/types.rs
  • tests/analyzer_tests.rs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Recent review details
Context from checks skipped due to timeout: 21
  • GitHub Check: Dogfooding compliance summary
  • GitHub Check: chapel-multilocale
  • GitHub Check: chapel-parse-check
  • GitHub Check: chapel-cli-contract
  • GitHub Check: cargo doc (no broken intra-doc links)
  • GitHub Check: cargo test ()
  • GitHub Check: cargo test (http,sarif)
  • GitHub Check: cargo clippy -D warnings
  • GitHub Check: cargo fmt --check
  • GitHub Check: MSRV (rust-version in Cargo.toml)
  • GitHub Check: Coverage ledger is honest (--fail-on-incomplete)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: Readiness — Component Grade Verification
  • GitHub Check: E2E — VeriSimDB Integration
  • GitHub Check: Bench — Scan Performance
  • GitHub Check: Ledger — coverage derived from executed tests
  • GitHub Check: E2E — Full Scan Pipeline
  • GitHub Check: Cargo Audit
  • GitHub Check: CI integrity (pins, lock, allowlist, YAML)
  • GitHub Check: Line coverage (cargo-llvm-cov)
  • GitHub Check: analyze (actions, none)
Additional comments: 4
src/types.rs: 1

250-250: LGTM!

Also applies to: 285-285, 402-402

src/assail/analyzer.rs: 1

15-15: LGTM!

Also applies to: 4136-4136, 5305-5305, 5587-5587, 5617-5617, 5666-5666

tests/analyzer_tests.rs: 1

477-523: LGTM!

CHANGELOG.adoc: 1

5-14: LGTM!

📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Assail reports now produce consistent output for identical inputs, including canonical JSON, hashes and --attest seals.
  • Tests
    • Added a regression test to verify that repeated analyses produce identical reports.

The analyzer now uses ordered collections for selected report data. Ordering traits support the ordered keys. A regression test checks that repeated analyses of the same project produce identical canonical JSON.

Changes: Assail report determinism

Layer / File(s) Summary
Add ordering traits
src/types.rs
Framework, AttackAxis and WeakPointCategory now derive PartialOrd and Ord.
Order report data
src/assail/analyzer.rs, tests/analyzer_tests.rs, CHANGELOG.adoc
The analyzer uses ordered collections for framework detection, attack recommendations, dependency-directory groups and taint-matrix rows. A regression test compares canonical JSON across repeated analyses. The changelog records the determinism changes.

Priority: Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Suggested reviewers: metadatastician

Merge Risk: Minimal · up to b8f9e

No merge-blocking issue was identified; the determinism change is ready for normal merge checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 3 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: making assail reports deterministic.
Description check Passed The description directly explains the determinism problem, the implemented fix, the regression test, and the verification results.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 3 files. (1 skipped: 1 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
📝 Generate docstrings
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@linear-code
linear-code Bot marked this pull request as ready for review October 26, 2026 19:17
@coderabbitai

coderabbitai Bot commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Add Carrot credits or activate Agent usage billing to use Autofix

@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

🤖 Completed: Generate docstrings for PR #245 — View commit ab75aeb

@hyperpolymath
hyperpolymath enabled auto-merge (squash) October 26, 2026 19:32
hyperpolymath added a commit that referenced this pull request Oct 26, 2026
Stacked PR E of the estate-format series:

- A: `.kyaml` loaders
- B: example profiles
- C: JCS reports
- C2: deterministic reports (#245)
- D: JCS attestation (#247)

This PR is independent of #245 and #247 and branches from `main`.

## What changes

- **New module `src/kyaml.rs`.** A KYAML emitter that follows mikefarah
`yq` v4's KYAML encoder (`pkg/yqlib/encoder_kyaml.go`, v4.54.1): flow
style, two-space indent, a trailing comma on every entry, bare keys only
for `^[A-Za-z_][A-Za-z0-9_-]*$`, and every string double-quoted.
- **Same data as the JCS report.** Keys follow JCS order (UTF-16 code
units) and numbers are in JCS form, so the KYAML and JSON reports of a
scan differ only in syntax.
- **New format value.** `ReportOutputFormat::Kyaml` adds
`--output-format kyaml`, extension `.kyaml`, parsed from `"kyaml"`.
Loaders have read `.kyaml` since PR A.
- **README fix.** `README.adoc` listed `--output-format` values
`markdown` and `text`, which never existed. It now lists the real ones.

## Contract, checked against the real tools

| Check | Result |
|---|---|
| Fixed point of `yq -p yaml -o kyaml`, the test
`scripts/kyaml-format.sh --check` runs | ✅ on all reports below |
| standards `scripts/kyaml-format.sh --check`, run for real | ✅ `all 4
file(s) are KYAML` |
| Byte-identical to `yq -p json -o kyaml` of the JCS report | ✅ two
historical `reports/*.yaml` assault reports, plus assail of `src/` (52
KB) and `tests/` |
| Read back through `crate::yaml` (serde-saphyr) | ✅ same data as the
JCS report |

The test `real_report_is_byte_identical_to_yq` pins this in CI without
yq. `tests/fixtures/kyaml/assail-report.kyaml` is yq's output for the
checked-in JCS report next to it.

## Two documented differences from yq

1. **Non-printables.** YAML's non-printables (U+007F–U+009F, U+FFFE,
U+FFFF) are escaped as `\uXXXX`. yq writes them raw, which YAML 1.2 does
not allow. A raw **U+0085 (NEL)** reads back as a line break, so yq
turns `"x\u0085y"` into `"x y"`. When a file contains one,
`kyaml-format.sh` reports `REFUSED … the KYAML rendering does not parse
to the same data` (exit 2). The gate therefore already treats yq's
output there as untrustworthy. I chose data integrity over gate
cleanliness for this rare case.
- **Alternative:** write them raw like yq. The gate stays quiet, but NEL
is silently corrupted.
- **Root-cause fix (beyond this repo):** escape these in yq's
`escapeDoubleQuotedString` upstream.
2. **Floats read from JSON.** `yq -p json` reformats some floats (`1e-7`
becomes `1e-07`, Go style). The KYAML report keeps the JCS form
(`1e-7`), which `yq -p yaml` preserves, so the fixed point holds. Byte
identity with yq's JSON→KYAML path holds whenever yq doesn't reformat,
which covers every report checked.

## Validation

- `cargo fmt --check`: clean.
- `cargo clippy --all-targets -D warnings`, with and without
`--all-features`: clean.
- `cargo test --all-features`: all 20 suites green.
- 8 new unit tests in `kyaml::tests`:
  - yq layout (expected text from yq);
  - scalars and lists at the top level;
  - C0 escapes;
  - non-printable escapes, including NEL read back intact;
  - JCS key order with a supplementary-plane key;
  - JCS and I-JSON numbers;
  - the real-report golden;
  - a report round trip.
- `--output-format bogus` now lists `json, yaml, kyaml, nickel, sarif`.

## Follow-ups (outside this PR)

- `assail` ignores `--output-format` and always writes JCS JSON.
Honouring it means deciding which bytes the `--attest` seal hashes.
- **Upstream (mikefarah/yq):** escape C1, DEL and NEL in the KYAML
encoder.
- **standards:** `kyaml-format.sh` could explain its refusal for this
case.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01PFaqv6iBXrt3W5Akd1Fq5k)_

Co-authored-by: Claude <noreply@anthropic.com>
@hyperpolymath
hyperpolymath enabled auto-merge (squash) October 26, 2026 20:04
@hyperpolymath
hyperpolymath merged commit dd91585 into main Oct 26, 2026
40 checks passed
@hyperpolymath
hyperpolymath deleted the claude/gallant-bell-4bo4oy-c2-deterministic-assail branch October 26, 2026 21:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants