mirror of
https://github.com/trailofbits/skills.git
synced 2026-09-14 14:28:48 +08:00
cfe5d7b161
* rust-review: add Rust security review plugin Add the rust-review plugin: a comprehensive Rust security review skill with clustered finders covering memory safety, concurrency/data races, panic-induced DoS, FFI/cross-language boundaries, error handling, resource handling, async runtime, and static hygiene. Includes worker, dedup-judge, fp-judge, and planner agents, SARIF generation with rule descriptions and regression tests, deterministic cluster chunking, and Codex skills mapping. Versioned at 1.0.0 and registered in the marketplace, CODEOWNERS, and root README. * c-review: backport rust-review protocol fixes and planner chunking Port the language-agnostic fixes made while building rust-review (which was ported from c-review) back into c-review: - worker/fp-judge: force findings, coverage gate, and REPORT.md to disk via Write instead of returning content in the reply (orchestrator context-bloat hardening); add a pre-complete file-existence check. - worker: move the cache-primer block below the normal self-check and pre-work budget so a non-primer worker does not start under a global "no tool calls" rule. - planner: add --max-passes-per-worker (default 4) with deterministic split_oversized_clusters chunking; skill passes the flag and documents the chunked-subset worker rule. - scripts: add test_split.py and test_generate_sarif.py regression tests. The SARIF test caught a missing RULE_DESCRIPTIONS entry for uninitialized-data, now added. Bump c-review to 1.2.0. * c-review/rust-review: validate artifacts, index-aware SARIF, protocol cleanups - Add validate_artifacts.py (+ tests) to both plugins to check worker shard, coverage, and finding files before accepting completions. - generate_sarif.py now reads the canonical findings-index.txt when present, falling back to findings/*.md only if the index is absent. - Merge the worker step-6 verification paragraphs and drop orchestrator -internal Phase 7 / plan.json jargon in favor of worker-facing stakes. - Tighten uninitialized-read-finder guidance: primitive integers still require initialization. * rust-review/c-review: per-cluster max_passes_per_worker override Lets output-heavy clusters declare a smaller manifest-level max_passes_per_worker so each expensive pass group gets its own worker, validated by a single shared cluster_max_passes_per_worker helper and honored by split_oversized_clusters via an explicit override (0 is rejected rather than silently falling back to the global cap). rust-review opts in concurrency-locking and recursion-dos; c-review ports the capability for parity. validate_artifacts now accepts grouped or repeated --claimed-count values. * rust-review: broaden bug-class coverage with capability-gated clusters Add layout-safety, input-os-safety, and info-disclosure clusters behind new has_packed_repr / has_fs_io capability gates so packed-repr, path, and pointer-exposure passes only run where they apply, and gate unsafe-only passes behind has_unsafe to cut noise on safe crates. Extend existing clusters with new bug classes: RefCell double-borrow panics, unflushed BufWriter, string-comparison bypasses, serialize_struct mismatches, nondeterminism, in-collection key mutation, and destructor-skip cleanup leaks. Fix detector regexes that missed or over-matched real Rust (packed-field borrows, RefCell try_borrow_mut, HashMap substrings, path push, packed inner attrs, fs/path probes) and add a regression test pinning them to snippets. * fix dedup * safety-net check for REPORT.md * on-disk data -> shards reconciliation * on-disk data -> shards reconciliation - v2 * ls -> glob * memory-safety gate * path validation * fix numbers/counting * rm PACKEDREF from FFI cluster prompt, it is in layout-safety * fix unsafe-boundary count * minor fixes for prompts * do not filter unknown-severity findings, just mark them as such * fix minor behavior changes in worker * Correctness: - generate_sarif: clamp startLine >=1 (`:0` produced schema-invalid SARIF) - generate_sarif: don't drop a judged survivor with blank severity - dedup-judge: Tier-2 carry-forward so a primary can't be demoted/orphaned - dedup-judge: crash-recovery unions shards with findings/*.md (empty-shard trap) Robustness: - generate_sarif: skip frontmatter-less files; add originalUriBaseIds Contracts: - SKILL: gate dedup-judge before fp-judge (prevent concurrent-spawn race) - worker: verbatim coverage cells; sub_prompt_paths omitted-not-empty; skip_subclasses reserved; Codebase comma format * improve prompts regexes, add missing deconflictions * prompt factual fixes * fix dozen of small prompt inconsistencies and add missing sections * more prompt fixes, fix retry guard in SKILL, small fixes in agents * dozen more small fixes * final regex fixes * fixes from rust to c-review * agents cannot use write tool for reports (strange cc limitation) - bypass via bash * spawnings agents is capped to 20 - explicit handling for that * fix glob -> read (glob is blocked for agents that has also bash) * fix regex patterns to work with grep * soften output requirements - they were violated anyway * consolidated clusters are no longer chunked — one worker owns the whole cluster, builds its shared Phase-A inventory once, and runs every phase * fix judge finding counting and low-severity guidance * fix metadata * small fix for skipped findings * Carry forward guard for `also_known_as` bucket * Gracefully handle parse_frontmatter error * Extend has_ffi coverage * Broader gate for has_concurrency * Update FFI-safe layout regex to support C, C+packed, and C+u32 in unsafe-boundary and dyn-trait-ffi-finder prompts * Small refine of regex patterns * Improve regex patterns for recursive type detection to include Mutex and RwLock * rm global .codex/rust-review * backport fixes to c-review * merge changes * Backport SARIF merge-survivor + malformed-frontmatter guards to c-review, mark missing locations, fix prompt-regex test extractor, and harden planner/validator scripts across both review plugins * fix pytest * fix global gitignore, adds / and ruff_cache * small fixes from pr-review * small fixes from pr-review - 2 * fix copilot finding --------- Co-authored-by: GrosQuildu <e2.8a.95@gmail.com>
301 lines
10 KiB
Python
301 lines
10 KiB
Python
"""Unit tests for the cluster-chunking helper in build_run_plan.py.
|
|
|
|
The helper partitions any **non-consolidated** cluster whose `passes` list exceeds
|
|
`max_passes_per_worker` into contiguous, order-preserving chunks. Each chunk
|
|
becomes its own pseudo-cluster entry, carrying the source cluster's prompt
|
|
path and `consolidated` flag, with a `-{i}` suffix appended to its
|
|
cluster_id (1-indexed). Clusters that fit within the threshold pass through
|
|
unchanged. **Consolidated clusters are never chunked** (one worker owns all
|
|
phases so the shared Phase-A inventory grounds every phase).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from build_run_plan import build_selection, split_oversized_clusters
|
|
|
|
|
|
def _mk_cluster(
|
|
cid: str,
|
|
n_passes: int,
|
|
*,
|
|
consolidated: bool = False,
|
|
max_passes_per_worker: int | None = None,
|
|
) -> dict:
|
|
"""Construct a synthetic cluster entry matching build_selection()'s output shape."""
|
|
cluster = {
|
|
"cluster_id": cid,
|
|
"consolidated": consolidated,
|
|
"cluster_prompt": f"/abs/prompts/clusters/{cid}.md",
|
|
"passes": [
|
|
{"bug_class": f"{cid}-bc-{i}", "prefix": f"{cid.upper()}{i}"} for i in range(n_passes)
|
|
],
|
|
}
|
|
if max_passes_per_worker is not None:
|
|
cluster["max_passes_per_worker"] = max_passes_per_worker
|
|
return cluster
|
|
|
|
|
|
# --- Pass-through (single-chunk) cases ---------------------------------------
|
|
|
|
|
|
def test_pass_through_k_equals_one():
|
|
"""K=1 < N=4 → 1 chunk, bare cluster_id, byte-identical to input."""
|
|
src = [_mk_cluster("small", 1)]
|
|
out = split_oversized_clusters(src, max_passes=4)
|
|
assert out == src
|
|
|
|
|
|
def test_pass_through_k_equals_n():
|
|
"""K=4 == N=4 → still 1 chunk, bare cluster_id, no suffix."""
|
|
src = [_mk_cluster("exact", 4)]
|
|
out = split_oversized_clusters(src, max_passes=4)
|
|
assert len(out) == 1
|
|
assert out[0]["cluster_id"] == "exact"
|
|
assert len(out[0]["passes"]) == 4
|
|
|
|
|
|
def test_pass_through_preserves_consolidated_flag():
|
|
"""The `consolidated` flag must round-trip through the splitter even on pass-through."""
|
|
src = [_mk_cluster("c", 3, consolidated=True)]
|
|
out = split_oversized_clusters(src, max_passes=4)
|
|
assert out[0]["consolidated"] is True
|
|
|
|
|
|
# --- Split cases -------------------------------------------------------------
|
|
|
|
|
|
def test_split_k_5_n_4_yields_4_plus_1():
|
|
src = [_mk_cluster("big", 5)]
|
|
out = split_oversized_clusters(src, max_passes=4)
|
|
assert [c["cluster_id"] for c in out] == ["big-1", "big-2"]
|
|
assert [len(c["passes"]) for c in out] == [4, 1]
|
|
|
|
|
|
def test_split_k_8_n_4_yields_4_plus_4():
|
|
src = [_mk_cluster("bigger", 8)]
|
|
out = split_oversized_clusters(src, max_passes=4)
|
|
assert [c["cluster_id"] for c in out] == ["bigger-1", "bigger-2"]
|
|
assert [len(c["passes"]) for c in out] == [4, 4]
|
|
|
|
|
|
def test_split_k_9_n_4_yields_4_4_1():
|
|
src = [_mk_cluster("huge", 9)]
|
|
out = split_oversized_clusters(src, max_passes=4)
|
|
assert [c["cluster_id"] for c in out] == ["huge-1", "huge-2", "huge-3"]
|
|
assert [len(c["passes"]) for c in out] == [4, 4, 1]
|
|
|
|
|
|
def test_split_preserves_pass_order():
|
|
"""Chunk i must contain passes[(i-1)*N : i*N] in original manifest order."""
|
|
src = [_mk_cluster("ordered", 9)]
|
|
out = split_oversized_clusters(src, max_passes=4)
|
|
expected_bcs = [f"ordered-bc-{i}" for i in range(9)]
|
|
flat = [p["bug_class"] for c in out for p in c["passes"]]
|
|
assert flat == expected_bcs
|
|
|
|
|
|
def test_consolidated_cluster_never_chunks():
|
|
"""Consolidated clusters are exempt from chunking — one worker owns all phases
|
|
so the shared Phase-A inventory grounds every phase (it would otherwise be
|
|
rebuilt per chunk, which workers skip in practice)."""
|
|
src = [_mk_cluster("share", 8, consolidated=True)]
|
|
out = split_oversized_clusters(src, max_passes=4)
|
|
assert len(out) == 1
|
|
assert out[0]["cluster_id"] == "share" # bare id, no -1/-2 suffix
|
|
assert out[0]["consolidated"] is True
|
|
assert out[0]["cluster_prompt"] == "/abs/prompts/clusters/share.md"
|
|
assert len(out[0]["passes"]) == 8
|
|
|
|
|
|
def test_consolidated_cluster_ignores_mppw_override():
|
|
"""An mppw override on a consolidated cluster is validated but does not chunk it."""
|
|
src = [_mk_cluster("share", 6, consolidated=True, max_passes_per_worker=1)]
|
|
out = split_oversized_clusters(src, max_passes=4)
|
|
assert len(out) == 1
|
|
assert out[0]["cluster_id"] == "share"
|
|
assert len(out[0]["passes"]) == 6
|
|
|
|
|
|
def test_cluster_override_splits_below_global_max():
|
|
src = [_mk_cluster("output-heavy", 3, max_passes_per_worker=1)]
|
|
out = split_oversized_clusters(src, max_passes=4)
|
|
assert [c["cluster_id"] for c in out] == [
|
|
"output-heavy-1",
|
|
"output-heavy-2",
|
|
"output-heavy-3",
|
|
]
|
|
assert [len(c["passes"]) for c in out] == [1, 1, 1]
|
|
|
|
|
|
def test_cluster_override_does_not_affect_other_clusters():
|
|
src = [
|
|
_mk_cluster("output-heavy", 3, max_passes_per_worker=1),
|
|
_mk_cluster("normal", 3),
|
|
]
|
|
out = split_oversized_clusters(src, max_passes=4)
|
|
assert [c["cluster_id"] for c in out] == [
|
|
"output-heavy-1",
|
|
"output-heavy-2",
|
|
"output-heavy-3",
|
|
"normal",
|
|
]
|
|
|
|
|
|
def test_consolidated_override_ignored_passes_still_filtered(tmp_path):
|
|
prompt_path = tmp_path / "prompts" / "clusters" / "heavy.md"
|
|
prompt_path.parent.mkdir(parents=True)
|
|
prompt_path.write_text("# heavy\n", encoding="utf-8")
|
|
manifest = {
|
|
"version": 1,
|
|
"clusters": [
|
|
{
|
|
"cluster_id": "heavy",
|
|
"prompt": "prompts/clusters/heavy.md",
|
|
"consolidated": True,
|
|
"max_passes_per_worker": 1,
|
|
"gate": "always",
|
|
"passes": [
|
|
{
|
|
"bug_class": "filtered",
|
|
"prefix": "FILTERED",
|
|
"requires": ["has_unsafe"],
|
|
},
|
|
{"bug_class": "kept-a", "prefix": "KEPTA"},
|
|
{"bug_class": "kept-b", "prefix": "KEPTB"},
|
|
],
|
|
}
|
|
],
|
|
}
|
|
|
|
selected = build_selection(
|
|
manifest,
|
|
plugin_root=tmp_path,
|
|
flags={
|
|
"has_unsafe": False,
|
|
"has_ffi": False,
|
|
"has_concurrency": False,
|
|
"has_async": False,
|
|
"has_packed_repr": False,
|
|
"has_fs_io": False,
|
|
},
|
|
threat_model="REMOTE",
|
|
)
|
|
out = split_oversized_clusters(selected, max_passes=4)
|
|
|
|
# Consolidated cluster: the mppw override is ignored (no chunking), but pass
|
|
# filtering still applies — the `requires: has_unsafe` pass is dropped.
|
|
assert [c["cluster_id"] for c in out] == ["heavy"]
|
|
assert [[p["prefix"] for p in c["passes"]] for c in out] == [["KEPTA", "KEPTB"]]
|
|
|
|
|
|
@pytest.mark.parametrize("override", [False, "1", 0, -1])
|
|
def test_build_selection_rejects_invalid_manifest_override(tmp_path, override):
|
|
prompt_path = tmp_path / "prompts" / "clusters" / "heavy.md"
|
|
prompt_path.parent.mkdir(parents=True)
|
|
prompt_path.write_text("# heavy\n", encoding="utf-8")
|
|
manifest = {
|
|
"version": 1,
|
|
"clusters": [
|
|
{
|
|
"cluster_id": "heavy",
|
|
"prompt": "prompts/clusters/heavy.md",
|
|
"consolidated": True,
|
|
"max_passes_per_worker": override,
|
|
"gate": "always",
|
|
"passes": [{"bug_class": "kept", "prefix": "KEPT"}],
|
|
}
|
|
],
|
|
}
|
|
|
|
with pytest.raises(SystemExit) as excinfo:
|
|
build_selection(
|
|
manifest,
|
|
plugin_root=tmp_path,
|
|
flags={
|
|
"has_unsafe": False,
|
|
"has_ffi": False,
|
|
"has_concurrency": False,
|
|
"has_async": False,
|
|
"has_packed_repr": False,
|
|
"has_fs_io": False,
|
|
},
|
|
threat_model="REMOTE",
|
|
)
|
|
|
|
assert excinfo.value.code == 2
|
|
|
|
|
|
@pytest.mark.parametrize("override", [False, "1", 0, -1])
|
|
def test_split_rejects_invalid_standalone_override(override):
|
|
src = [_mk_cluster("invalid", 3, max_passes_per_worker=override)]
|
|
|
|
with pytest.raises(ValueError, match="positive integer"):
|
|
split_oversized_clusters(src, max_passes=4)
|
|
|
|
|
|
# --- Identity (disable) case -------------------------------------------------
|
|
|
|
|
|
def test_max_passes_zero_is_identity_no_suffix():
|
|
"""N=0 is the explicit 'disable chunking' sentinel — pass through with bare ids."""
|
|
src = [_mk_cluster("a", 8), _mk_cluster("b", 3, max_passes_per_worker=1)]
|
|
out = split_oversized_clusters(src, max_passes=0)
|
|
assert out == src
|
|
|
|
|
|
# --- Multiple clusters in one call -------------------------------------------
|
|
|
|
|
|
def test_mixed_input_handles_each_cluster_independently():
|
|
src = [
|
|
_mk_cluster("small", 2), # pass-through
|
|
_mk_cluster("split-me", 6), # 4 + 2
|
|
_mk_cluster("exact", 4), # pass-through
|
|
_mk_cluster("huge", 9), # 4 + 4 + 1
|
|
]
|
|
out = split_oversized_clusters(src, max_passes=4)
|
|
cids = [c["cluster_id"] for c in out]
|
|
sizes = [len(c["passes"]) for c in out]
|
|
assert cids == ["small", "split-me-1", "split-me-2", "exact", "huge-1", "huge-2", "huge-3"]
|
|
assert sizes == [2, 4, 2, 4, 4, 4, 1]
|
|
|
|
|
|
def test_order_of_source_clusters_is_preserved():
|
|
"""The relative ordering of source clusters in the input list survives splitting."""
|
|
src = [
|
|
_mk_cluster("alpha", 8),
|
|
_mk_cluster("beta", 3),
|
|
_mk_cluster("gamma", 5),
|
|
]
|
|
out = split_oversized_clusters(src, max_passes=4)
|
|
cids = [c["cluster_id"] for c in out]
|
|
assert cids == ["alpha-1", "alpha-2", "beta", "gamma-1", "gamma-2"]
|
|
|
|
|
|
# --- Determinism -------------------------------------------------------------
|
|
|
|
|
|
def test_same_input_same_output_repeated_calls():
|
|
src = [_mk_cluster("d", 7)]
|
|
out1 = split_oversized_clusters(src, max_passes=4)
|
|
out2 = split_oversized_clusters(src, max_passes=4)
|
|
assert out1 == out2
|
|
|
|
|
|
# --- Negative input ----------------------------------------------------------
|
|
|
|
|
|
def test_negative_max_passes_raises():
|
|
"""Negative N is a CLI input error; helper rejects it explicitly."""
|
|
src = [_mk_cluster("x", 4)]
|
|
with pytest.raises(ValueError):
|
|
split_oversized_clusters(src, max_passes=-1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
raise SystemExit(pytest.main([__file__, *sys.argv[1:]]))
|