[pitboss/grind] deferred session-0003 (20260521T143544Z-f898)

This commit is contained in:
pitboss 2026-05-21 12:17:45 -05:00
parent b3766311fb
commit 6341afec59
16 changed files with 346 additions and 48 deletions

View file

@ -116,7 +116,7 @@ A focused release that adds seven new vulnerability classes, ships two SSA sidec
- **FastAPI cross-file `include_router` dependency tracking.** `auth_analysis/router_facts.rs` captures per-file router declarations (`<router> = X(deps=[…])`) and `<parent>.include_router(<child_module>.<child_var>)` edges in pass 1, persists them into `GlobalSummaries::router_facts_by_module`, and resolves them into the active file's `AuthorizationModel::cross_file_router_deps` at pass 2 entry. Transitive lifts (grandparent to parent to child) handled by iterative index walk. Module identity is the file basename without `.py`. Closes the airflow execution-API shape where a child router lives in `routes/task_instances.py` and its auth is declared on the parent in `routes/__init__.py`.
- **FastAPI router-level `dependencies=[...]` propagation.** Module-level `router = APIRouter(dependencies=[Security(...)])` is pre-walked once per file and merged onto every `@<router>.<verb>(...)` route attached in the same file. Closes airflow execution-API routes that re-use a single `ti_id_router` declared once at module scope.
- **FastAPI `Security(callable, scopes=[...])` recognised distinctly from `Depends(callable)`.** Scoped Security promotes the synthetic `AuthCheck` to `AuthCheckKind::Other` (route-level scope-checked authorization), not Login. New scope-tracking boolean threaded through `expand_decorator_calls` and `extract_fastapi_dependencies`.
- **Caller-scope IPA: same-file route-handler-to-helper auth lift.** `apply_caller_scope_propagation` walks every non-route helper unit; if its in-file callers are non-empty AND every caller is itself an authorized route handler (route-level non-Login auth check) or already authorized via this same propagation, the caller's checks lift onto the helper as synthetic `is_route_level=true` `AuthCheck`s. Iterated to a small fixpoint so transitive helper chains (route to mid_helper to leaf_helper) are covered. Refuses to authorize helpers with no in-file caller, helpers called from a mix of authorized and unauthorized callers, and helpers called only from un-lifted helpers. Cross-file equivalent deferred. Closes the dominant FastAPI / Django / Flask "route authenticates via decorator/dependency, then delegates to a private helper that performs the sink" FP shape on sentry / saleor / airflow.
- **Caller-scope IPA: same-file route-handler-to-helper auth lift.** `apply_caller_scope_propagation` walks every non-route helper unit; if its in-file callers are non-empty AND every caller is itself an authorized route handler (route-level non-Login auth check) or already authorized via this same propagation, the caller's checks lift onto the helper as synthetic `is_route_level=true` `AuthCheck`s. Iterated to a small fixpoint so transitive helper chains (route to mid_helper to leaf_helper) are covered. Refuses to authorize helpers with no in-file caller, helpers called from a mix of authorized and unauthorized callers, and helpers called only from un-lifted helpers. Cross-file lifting is not implemented. Closes the dominant FastAPI / Django / Flask "route authenticates via decorator/dependency, then delegates to a private helper that performs the sink" FP shape on sentry / saleor / airflow.
- **Go DAO-helper id-scalar precision pass.** For non-route Go units, a parameter whose declared type is a bounded primitive scalar (`int64`, `uint32`, `string`, `bool`, `byte`, `rune`, `float64`, …) and whose name is id-shaped (`id`, `*Id`, `*_id`, `*ids`) is dropped from `unit.params` before ownership-check evaluation. Real Go HTTP handlers always carry a framework-request-typed param (`*http.Request`, `*gin.Context`, `echo.Context`, `*fiber.Ctx`); per-framework route extractors set `include_id_like_typed=true` so id-shaped path params survive on real routes. Mirrors the existing Python `is_python_id_like_typed_param` filter. Closes ~957 `go.auth.missing_ownership_check` findings on gitea backend DAO helpers (`func GetRunByRepoAndID(ctx, repoID, runID int64)`, `func DeleteRunner(ctx, id int64)`, the entire `models/...` layer where the ownership check sits in the calling route handler) and equivalent shapes in minio / Go ORM codebases.
- **Bare-callee verb-name fallback gate.** `list(...)`, `filter(...)`, `update(...)`, `create_audit_entry(...)`, `update_coding_agent_state(...)` (no receiver dot at all) no longer classify as `DbMutation` / `DbCrossTenantRead` via the loose verb-name fallback. Real ORM/DB calls carry a receiver (`User.find(id)`, `Model.objects.filter`, `repo.save(x)`); a bare `list(events)` is the Python builtin and `filter(fn, xs)` is `Iterable.filter`. New helper `receiver_is_simple_chain(callee)` requires a non-chained receiver dot. The realtime / outbound / cache prefix dispatches still match by chain root.
@ -150,7 +150,7 @@ Per-language label rules expanded for the seven new caps.
### CVE corpus
- **C.** CVE-2017-1000117 (git argv injection via `ssh://-oProxyCommand=…`) vulnerable + patched fixtures under `tests/benchmark/cve_corpus/c/CVE-2017-1000117/`. Three-layer engine gap deferred (array-element taint propagation, `c.cmdi.exec*` AST patterns, dash-prefix-byte sanitizer recognition).
- **C.** CVE-2017-1000117 (git argv injection via `ssh://-oProxyCommand=…`) vulnerable + patched fixtures under `tests/benchmark/cve_corpus/c/CVE-2017-1000117/`. Known remaining gap: array-element taint propagation, `c.cmdi.exec*` AST patterns, and dash-prefix-byte sanitizer recognition.
- **Python.** CVE-2023-6568 (mlflow reflected XSS), CVE-2024-21513 (langchain SQL / Jinja), CVE-2024-23334 (aiohttp static-file path traversal) vulnerable + patched fixtures.
- **PHP.** CVE-2026-33486 (roadiz/documents SSRF) vulnerable + patched fixtures.
- **JavaScript.** CVE-2026-42353 (i18next-http-middleware path traversal) vulnerable + patched fixtures.
@ -388,7 +388,7 @@ The biggest release since launch. The taint engine was rebuilt on top of an SSA
- Replaced the legacy `app.js` with a React + Vite + TypeScript SPA.
- Interactive graph workspace for CFG and call-graph views (Graphology + ELK + Sigma) with neighborhood reduction and a full-page inspector.
- Triage UI with database-backed decisions (true positive, false positive, deferred, suppressed) and `.nyx/triage.json` round-trip.
- Triage UI with database-backed decisions (true positive, false positive, accepted risk, suppressed) and `.nyx/triage.json` round-trip.
- Scan history, rules management, and finding detail panels with evidence and flow visualization.
- Vitest browser-side test suite wired into CI.
- Bumped to React 19, Vite 8, TypeScript 6.0, ESLint 10, `@vitejs/plugin-react` 6, with aligned `@types/react*`.

View file

@ -267,11 +267,11 @@ while the pass stabilises.
| CLI flag | `--backwards-analysis` / `--no-backwards-analysis` |
| Env var (legacy) | `NYX_BACKWARDS=1` |
**Limitations (first cut).** Reverse call-graph expansion past a
`ReachedParam` is deferred; the walk terminates at function parameters
rather than crossing back into callers. Path-constraint pruning is
conservative: only the accumulated `PredicateSummary` bits are consulted,
not the full symbolic predicate stack. Depth-bounded at k=2 for
**Limitations.** Reverse call-graph expansion stops at `ReachedParam`; the walk
terminates at function parameters rather than crossing back into callers.
Path-constraint pruning is conservative: only the accumulated
`PredicateSummary` bits are consulted, not the full symbolic predicate stack.
Depth-bounded at k=2 for
cross-function body expansion. See `DEFAULT_BACKWARDS_DEPTH`,
`BACKWARDS_VALUE_BUDGET`, and `MAX_BACKWARDS_CALLEE_BLOCKS` in
`src/taint/backwards.rs` for the exact bounds.

View file

@ -53,7 +53,7 @@ When a private helper is called only from authorized route handlers in the same
- Iterated to a small fixpoint so transitive chains (route to mid_helper to leaf_helper) are covered.
- Refuses to authorize helpers with no in-file caller, helpers called from a mix of authorized and unauthorized callers, and helpers called only from un-lifted helpers.
- Cross-file equivalent is deferred.
- Cross-file caller-scope lifting is not implemented yet.
This closes the FastAPI / Django / Flask shape where a route authenticates via decorator or dependency, then delegates to a private helper that performs the sink.

View file

@ -138,8 +138,7 @@ use tree-sitter and are stable; parsing is not a differentiator.
- **Framework context**: Rails helpers (`sanitize_sql`, `permit`, `require`).
- **Known gaps**: string interpolation inside shell and SQL strings is
recognized structurally but not modeled as a distinct operator.
`begin/rescue/ensure` exception-edge wiring is documented as deferred
(structurally incompatible with `build_try()`).
`begin/rescue/ensure` exception-edge wiring is not implemented.
#### Rust: 100% P / 100% R / 100% F1 *(70-case adversarial corpus)*

View file

@ -86,7 +86,7 @@ Modifiers in the ±5 range nudge the result for trend (only after the second sca
It's a Nyx-finding-pressure metric, not a security audit. Score 100 means Nyx didn't find anything under its current rules and language coverage; it doesn't certify the absence of vulnerabilities. The score doesn't see runtime config, IAM, secret stores, dependency CVEs, or anything outside the source tree being scanned. A repo of mostly Kotlin (where Nyx coverage is thin) will score artificially well because most of the code never gets evaluated.
Ceilings are calibrated for the current scanner false-positive rates. As symex coverage and rule precision improve, the ceilings tighten. Calibration data and the rationale behind each tunable lives in [health-score-audit.md](health-score-audit.md).
Ceilings are calibrated for the current scanner false-positive rates. As symex coverage and rule precision improve, the ceilings may tighten.
### Findings and Finding detail

View file

@ -21,6 +21,28 @@ fn callee_is_header_setter(name: &str) -> bool {
matches!(last, "Set" | "Add" | "Header" | "WriteHeader")
}
/// True when `receiver` looks like a Go HTTP response-writer or framework
/// context expression. Filters out `url.Values.Set` / `sync.Map.Store` /
/// `flag.FlagSet.Set` and similar map-like receivers whose `Set` / `Add`
/// names collide with `http.Header.Set` / `Add`.
///
/// Drilled forms (root_receiver_text reduces `w.Header().Set` to `w`):
/// * `w` / `rw` / `writer` — canonical `http.ResponseWriter` names
/// * `c` / `ctx` — gin / echo / fiber / chi context handles
/// * `resp` / `response` — common response-wrapper names
/// * `headers` — `Header` value handle
///
/// Non-drilled forms (raw text when drilling fails):
/// * Any expression containing `.Header()` or `.Headers()` —
/// canonical chain accessor returning `http.Header`.
fn receiver_is_go_response_writer(receiver: &str) -> bool {
matches!(
receiver,
"w" | "rw" | "writer" | "c" | "ctx" | "resp" | "response" | "headers" | "header"
) || receiver.contains(".Header()")
|| receiver.contains(".Headers()")
}
fn source_imports_go_http(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"\"net/http\"",
@ -69,7 +91,11 @@ impl FrameworkAdapter for HeaderGoAdapter {
if value_routed_through_encoder(file_bytes) {
return None;
}
let matches_call = super::any_callee_matches(summary, callee_is_header_setter);
let matches_call = super::any_callee_matches_with_receiver(
summary,
callee_is_header_setter,
receiver_is_go_response_writer,
);
let matches_source = source_imports_go_http(file_bytes);
if matches_call && matches_source {
Some(FrameworkBinding {
@ -125,6 +151,55 @@ mod tests {
.is_none());
}
#[test]
fn skips_url_values_set_collision() {
// `params.Set(k, v)` on a `url.Values` collides with `http.Header.Set`
// on the bare callee name. Real CFG-derived callees carry the
// receiver text `params`, which is not in the response-writer
// allowlist, so the adapter rejects. Net/url is intentionally
// imported here to ensure the source-import gate alone would fire.
let src: &[u8] = b"package x\nimport (\"net/http\"; \"net/url\")\n\
func Run(w http.ResponseWriter, v string) {\n\
params := url.Values{}\n\
params.Set(\"k\", v)\n\
_ = params\n\
}\n";
let tree = parse_go(src);
let summary = FuncSummary {
name: "Run".into(),
callees: vec![crate::summary::CalleeSite {
name: "Set".into(),
receiver: Some("params".into()),
..Default::default()
}],
..Default::default()
};
assert!(HeaderGoAdapter
.detect(&summary, tree.root_node(), src)
.is_none());
}
#[test]
fn fires_on_response_writer_receiver() {
// Receiver-text discriminator accepts `w` (canonical
// `http.ResponseWriter` shorthand).
let src: &[u8] = b"package x\nimport \"net/http\"\n\
func Run(w http.ResponseWriter, v string) { w.Header().Set(\"X\", v) }\n";
let tree = parse_go(src);
let summary = FuncSummary {
name: "Run".into(),
callees: vec![crate::summary::CalleeSite {
name: "Set".into(),
receiver: Some("w".into()),
..Default::default()
}],
..Default::default()
};
assert!(HeaderGoAdapter
.detect(&summary, tree.root_node(), src)
.is_some());
}
#[test]
fn skips_when_value_url_encoded() {
let src: &[u8] = b"package x\nimport (\"net/http\"; \"net/url\")\n\

View file

@ -22,6 +22,23 @@ fn callee_is_header_setter(name: &str) -> bool {
matches!(last, "set_header" | "[]=" | "store" | "add_header")
}
/// True when `receiver` looks like a Ruby response or headers handle.
/// Filters out `Hash#[]=` / generic `Hash#store` collisions where the
/// receiver is an unrelated local (`h`, `params`, `attrs`, etc.).
///
/// Drilled forms covered:
/// * `response` / `resp` / `res` — `Rack::Response` / Rails / Sinatra response
/// * `headers` — bare headers handle
/// * `@response` / `@headers` — instance-var equivalents
/// * Any expression containing `.headers` or `.response` (chain access).
fn receiver_is_ruby_response(receiver: &str) -> bool {
matches!(
receiver,
"response" | "resp" | "res" | "headers" | "@response" | "@headers"
) || receiver.contains(".headers")
|| receiver.contains(".response")
}
fn source_uses_ruby_web(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"Rack::Response",
@ -73,7 +90,11 @@ impl FrameworkAdapter for HeaderRubyAdapter {
if value_routed_through_encoder(file_bytes) {
return None;
}
let matches_call = super::any_callee_matches(summary, callee_is_header_setter);
let matches_call = super::any_callee_matches_with_receiver(
summary,
callee_is_header_setter,
receiver_is_ruby_response,
);
let matches_source = source_uses_ruby_web(file_bytes);
if matches_call && matches_source {
Some(FrameworkBinding {
@ -129,6 +150,49 @@ mod tests {
.is_none());
}
#[test]
fn skips_hash_subscript_assign_collision() {
// `h['Set-Cookie'] = value` on a plain `Hash` collides with
// `response['Set-Cookie'] = value` on the bare `[]=` callee
// name. Receiver text `h` is not in the response allowlist,
// so the adapter rejects.
let src: &[u8] = b"require 'rack'\n\
def run(value)\n h = {}\n h['Set-Cookie'] = value\n h\nend\n";
let tree = parse_ruby(src);
let summary = FuncSummary {
name: "run".into(),
callees: vec![crate::summary::CalleeSite {
name: "[]=".into(),
receiver: Some("h".into()),
..Default::default()
}],
..Default::default()
};
assert!(HeaderRubyAdapter
.detect(&summary, tree.root_node(), src)
.is_none());
}
#[test]
fn fires_on_response_receiver() {
// Receiver `response` is in the allowlist.
let src: &[u8] = b"require 'rack'\n\
def run(value)\n response = Rack::Response.new\n response['Set-Cookie'] = value\nend\n";
let tree = parse_ruby(src);
let summary = FuncSummary {
name: "run".into(),
callees: vec![crate::summary::CalleeSite {
name: "[]=".into(),
receiver: Some("response".into()),
..Default::default()
}],
..Default::default()
};
assert!(HeaderRubyAdapter
.detect(&summary, tree.root_node(), src)
.is_some());
}
#[test]
fn skips_when_value_url_encoded() {
let src: &[u8] = b"require 'rack'\nrequire 'uri'\n\

View file

@ -23,6 +23,25 @@ fn callee_is_header_setter(name: &str) -> bool {
matches!(last, "insert" | "append" | "insert_header" | "header")
}
/// True when `receiver` looks like a Rust `HeaderMap` / response handle.
/// Filters out `BTreeMap::insert` / `HashMap::insert` / `Vec::insert`
/// collisions where the receiver is an unrelated local (`map`, `cache`,
/// `entries`, etc.).
///
/// Drilled forms covered:
/// * `headers` / `headers_mut` — canonical `axum` / `hyper` handles
/// * `response` / `resp` / `res` — `actix_web::HttpResponse` / hyper builder
/// * `builder` — `axum::http::Response::builder()` chain root
/// * Any expression containing `.headers_mut()` or `.headers()` —
/// chain accessor returning `&mut HeaderMap` / `&HeaderMap`.
fn receiver_is_rust_header_map(receiver: &str) -> bool {
matches!(
receiver,
"headers" | "headers_mut" | "response" | "resp" | "res" | "builder"
) || receiver.contains(".headers_mut()")
|| receiver.contains(".headers()")
}
fn source_imports_rust_http(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"use http::HeaderMap",
@ -71,7 +90,11 @@ impl FrameworkAdapter for HeaderRustAdapter {
if value_routed_through_encoder(file_bytes) {
return None;
}
let matches_call = super::any_callee_matches(summary, callee_is_header_setter);
let matches_call = super::any_callee_matches_with_receiver(
summary,
callee_is_header_setter,
receiver_is_rust_header_map,
);
let matches_source = source_imports_rust_http(file_bytes);
if matches_call && matches_source {
Some(FrameworkBinding {
@ -127,6 +150,54 @@ mod tests {
.is_none());
}
#[test]
fn skips_btreemap_insert_collision() {
// `map.insert(k, v)` on a `BTreeMap` / `HashMap` collides with
// `headers.insert(k, v)` on `HeaderMap` at the bare callee name.
// Receiver text `map` is not in the HeaderMap allowlist, so the
// adapter rejects. `headers_mut()` substring is present in the
// file so source-import gate alone would fire.
let src: &[u8] = b"use std::collections::BTreeMap;\nuse axum::http::HeaderMap;\n\
fn run(headers: &mut HeaderMap, value: String) {\n\
let mut map: BTreeMap<String, String> = BTreeMap::new();\n\
map.insert(\"k\".into(), value);\n\
let _ = headers.headers_mut();\n\
}\n";
let tree = parse_rust(src);
let summary = FuncSummary {
name: "run".into(),
callees: vec![crate::summary::CalleeSite {
name: "insert".into(),
receiver: Some("map".into()),
..Default::default()
}],
..Default::default()
};
assert!(HeaderRustAdapter
.detect(&summary, tree.root_node(), src)
.is_none());
}
#[test]
fn fires_on_headers_receiver() {
// Receiver `headers` is in the HeaderMap allowlist.
let src: &[u8] = b"use axum::http::HeaderMap;\n\
fn run(headers: &mut HeaderMap, value: &str) { headers.insert(\"X\", value.parse().unwrap()); }\n";
let tree = parse_rust(src);
let summary = FuncSummary {
name: "run".into(),
callees: vec![crate::summary::CalleeSite {
name: "insert".into(),
receiver: Some("headers".into()),
..Default::default()
}],
..Default::default()
};
assert!(HeaderRustAdapter
.detect(&summary, tree.root_node(), src)
.is_some());
}
#[test]
fn skips_when_value_url_encoded() {
let src: &[u8] = b"use axum::http::HeaderMap;\n\

View file

@ -227,6 +227,36 @@ fn any_callee_matches(
.any(|c| predicate(c.name.as_str()))
}
/// True when any callee in `summary.callees` matches `name_pred` AND
/// (its receiver matches `receiver_pred` OR its receiver is `None`).
///
/// Used by adapters where the callee name is ambiguous (e.g. Go's bare
/// `Set` / `Add` collides with `url.Values.Set`, Rust's `insert` collides
/// with `BTreeMap::insert`) and the receiver text provides the only
/// non-type-aware discriminator.
///
/// Receivers of `None` fall through to acceptance to preserve backward
/// compatibility with synthetic unit-test summaries built via
/// `CalleeSite::bare(...)` and with adapters whose callees are free
/// functions (no receiver). Real CFG-derived callees populate
/// `CalleeSite.receiver` whenever the call is a method invocation, so
/// the gate engages on production scans.
fn any_callee_matches_with_receiver(
summary: &crate::summary::FuncSummary,
name_pred: impl Fn(&str) -> bool,
receiver_pred: impl Fn(&str) -> bool,
) -> bool {
summary.callees.iter().any(|c| {
if !name_pred(c.name.as_str()) {
return false;
}
match c.receiver.as_deref() {
Some(r) => receiver_pred(r),
None => true,
}
})
}
/// True when `arg_text` resolves to a function parameter whose 0-based
/// index participates in taint flow — either listed in
/// `summary.tainted_sink_params` (param reaches an internal sink) or

View file

@ -16,6 +16,19 @@ fn callee_is_lodash_merge(name: &str) -> bool {
matches!(last, "merge" | "mergeWith" | "defaultsDeep" | "set" | "setWith")
}
/// True when `receiver` looks like a lodash module handle (`_`, `lodash`,
/// or any expression where lodash sits to the left of the dot).
///
/// Filters out `state.set(k, v)` on `Map`, `cache.set(k, v)` on `LRU`,
/// `tokens.merge(...)` on a user class, and similar same-name collisions
/// outside lodash scope. Receivers of `None` (bare callees like
/// `set(state, key, value)` from `const { set } = require('lodash')`
/// or unit-test `CalleeSite::bare`) pass through to preserve the
/// standalone-import path.
fn receiver_is_lodash(receiver: &str) -> bool {
matches!(receiver, "_" | "lodash" | "lodashImport") || receiver.starts_with("_.")
}
fn source_imports_lodash(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"require('lodash')",
@ -68,7 +81,11 @@ impl FrameworkAdapter for PpLodashMergeJsAdapter {
if super::source_filters_proto_keys(file_bytes) {
return None;
}
let matches_call = super::any_callee_matches(summary, callee_is_lodash_merge);
let matches_call = super::any_callee_matches_with_receiver(
summary,
callee_is_lodash_merge,
receiver_is_lodash,
);
let matches_source = source_imports_lodash(file_bytes);
if matches_call && matches_source {
Some(build_binding(JS_ADAPTER_NAME))
@ -100,7 +117,11 @@ impl FrameworkAdapter for PpLodashMergeTsAdapter {
if super::source_filters_proto_keys(file_bytes) {
return None;
}
let matches_call = super::any_callee_matches(summary, callee_is_lodash_merge);
let matches_call = super::any_callee_matches_with_receiver(
summary,
callee_is_lodash_merge,
receiver_is_lodash,
);
let matches_source = source_imports_lodash(file_bytes);
if matches_call && matches_source {
Some(build_binding(TS_ADAPTER_NAME))
@ -149,6 +170,54 @@ mod tests {
.is_none());
}
#[test]
fn skips_map_set_collision() {
// `state.set(k, v)` on a Map collides with `_.set(state, k, v)`
// on the bare callee name. Receiver text `state` is not in the
// lodash allowlist, so the adapter rejects. The lodash import
// is intentionally present to ensure the source-import gate
// alone would have fired.
let src: &[u8] = b"const _ = require('lodash');\n\
function run(payload) {\n\
const state = new Map();\n\
state.set('key', payload);\n\
return state;\n\
}\n";
let tree = parse_js(src);
let summary = FuncSummary {
name: "run".into(),
callees: vec![crate::summary::CalleeSite {
name: "set".into(),
receiver: Some("state".into()),
..Default::default()
}],
..Default::default()
};
assert!(PpLodashMergeJsAdapter
.detect(&summary, tree.root_node(), src)
.is_none());
}
#[test]
fn fires_on_underscore_receiver() {
// Receiver `_` is the canonical lodash binding.
let src: &[u8] = b"const _ = require('lodash');\n\
function run(payload) { return _.merge({}, payload); }\n";
let tree = parse_js(src);
let summary = FuncSummary {
name: "run".into(),
callees: vec![crate::summary::CalleeSite {
name: "merge".into(),
receiver: Some("_".into()),
..Default::default()
}],
..Default::default()
};
assert!(PpLodashMergeJsAdapter
.detect(&summary, tree.root_node(), src)
.is_some());
}
#[test]
fn skips_when_proto_key_filter_present() {
let src: &[u8] = b"const _ = require('lodash');\n\

View file

@ -537,7 +537,7 @@ fn build_toolchain_lock(spec: &HarnessSpec, root: &Path) -> Result<serde_json::V
}))
}
/// Phase 28 — Track H.3. Outcome of [`replay_bundle`].
/// Outcome of [`replay_bundle`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReplayResult {
/// `reproduce.sh` exited 0 — replay matched the recorded outcome.
@ -548,14 +548,13 @@ pub enum ReplayResult {
DockerUnavailable,
/// `reproduce.sh` exited 3 — host toolchain mismatched in process mode.
ToolchainMismatch,
/// Any other non-zero exit code, treated as an unexpected error. The
/// Phase 28 m7 Gate 5 inversion treats this as instability.
/// Any other non-zero exit code, treated as an unexpected error.
UnexpectedError {
/// Exit code surfaced by the script.
exit_code: i32,
},
/// `reproduce.sh` could not be invoked at all (script missing,
/// permissions, etc.). Phase 28 Gate 5 treats this as instability.
/// permissions, etc.).
ScriptInvocationFailed {
/// Human-readable error.
message: String,

View file

@ -1,8 +1,6 @@
//! Health-score scoring engine, v3.5.
//!
//! Pure-function scoring over a `HealthInputs` struct. Documented in
//! `docs/health-score-audit.md` (calibration, rationale) and
//! `docs/health-score.md` (customer methodology).
//! Pure-function scoring over a `HealthInputs` struct.
//!
//! ## Conceptual model
//!
@ -37,8 +35,8 @@
//! low-confidence HIGHs that got `NotAttempted` from symex doesn't
//! pay the same ceiling cost as a repo with 5 `Confirmed` HIGHs.
//! * Tighter modifier ranges so they can't flip a band.
//! * No `parse_success_rate` (it's actually a cache-miss metric ,
//! see `project_parse_success_rate_misnomer.md`).
//! * No `parse_success_rate`. It is a cache-miss metric, not a parse
//! success metric.
use crate::commands::scan::Diag;
use crate::evidence::{Confidence, Verdict};
@ -47,10 +45,8 @@ use crate::server::models::{BacklogStats, FindingSummary, HealthComponent, Healt
// ── Tunables ─────────────────────────────────────────────────────────────────
//
// Calibrated for v0.5.0 scanner FP rate. As Nyx symex coverage and
// rule precision improve, the HIGH ceilings should tighten, see
// `docs/health-score-audit.md` "Calibration trajectory" for the
// roadmap.
// Calibrated for the current scanner false-positive rate. As Nyx symex
// coverage and rule precision improve, the HIGH ceilings may tighten.
/// Below this file count, we floor the size divisor at 1.0, tiny
/// repos can't claim infinite per-LOC dilution from one finding.

View file

@ -122,8 +122,7 @@ async fn overview(State(state): State<AppState>) -> Json<OverviewResponse> {
fixed_since_last,
reintroduced: reintroduced_count,
// Files-scanned proxy for repo size, used for size-aware
// severity dampening in `health::compute`. See
// `docs/health-score-audit.md` for calibration data.
// severity dampening in `health::compute`.
repo_files: scanner_quality
.as_ref()
.map(|q| q.files_scanned)
@ -1128,7 +1127,4 @@ fn plural(n: usize) -> &'static str {
if n == 1 { "" } else { "s" }
}
// `compute_health_score` moved to `crate::server::health::compute`
// after the v2 audit (2026-04-28). See `docs/health-score-audit.md`
// for calibration data and the rationale, and `docs/health-score.md`
// for the customer-facing methodology.
// `compute_health_score` moved to `crate::server::health::compute`.

View file

@ -14,10 +14,10 @@
//! | `src/main.rs` | binary entry point; wires --features dynamic|
//! | `src/lib.rs` | crate root; `#[cfg(feature="dynamic")]` mod|
//! | `src/commands/scan.rs` | enrichment loop lives here |
//! | `src/commands/mod.rs` | `verify-feedback` subcommand (§21.2) |
//! | `src/commands/mod.rs` | `verify-feedback` subcommand |
//! | `src/server/` (any file) | server start_scan verify wiring |
//! | `src/rank.rs` | M7 rank-delta telemetry hook (§21 / M7) |
//! | `src/chain/reverify.rs` | Phase 26 — composite chain re-verification |
//! | `src/rank.rs` | dynamic-verdict rank scoring |
//! | `src/chain/reverify.rs` | composite chain re-verification |
use std::fs;
use std::path::{Path, PathBuf};
@ -31,8 +31,8 @@ const ALLOWED: &[&str] = &[
"commands/mod.rs",
"server/",
"rank.rs",
// Phase 26 — Track G.3: composite chain re-verification is the
// public bridge between the chain composer and the dynamic verifier.
// Composite chain re-verification is the public bridge between the chain
// composer and the dynamic verifier.
"chain/reverify.rs",
// The dynamic module itself is obviously allowed.
"dynamic/",

View file

@ -24,12 +24,12 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# ── Defaults ──────────────────────────────────────────────────────────────────
# Defaults
OUTPUT_DIR=""
NYX_BIN="${NYX_BIN:-${REPO_ROOT}/target/release/nyx}"
CORPUS_CACHE="${NYX_EVAL_CORPUS_DIR:-${HOME}/.cache/nyx/eval_corpus}"
SETS="owasp,sard,inhouse"
# Phase 29 (Track I): per-cell budgets + monotonic-improvement diff.
# Optional per-cell budgets and monotonic-improvement diff.
BUDGET_FILE=""
DIFF_FILE=""

View file

@ -1,9 +1,8 @@
//! Health-score calibration regression net (v3.5).
//!
//! Pins synthetic reference scenarios catalogued in
//! `docs/health-score-audit.md` to expected score bands. When a
//! constant or weight in `src/server/health.rs` changes, this test
//! fails fast if the change silently re-grades the boundary cases.
//! Pins synthetic reference scenarios to expected score bands. When a constant
//! or weight in `src/server/health.rs` changes, this test fails fast if the
//! change silently re-grades the boundary cases.
//!
//! Bands are deliberately wide (±5 points around the calibration
//! number) so honest curve-shape adjustments don't trip the test ,
@ -142,7 +141,7 @@ fn sev(h: &HealthScore) -> u8 {
.score
}
// ── Calibration cases (synthetic, mirror docs/health-score-audit.md) ─────────
// Calibration cases
#[test]
fn calibration_clean_first_scan() {