mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
feat(pre-push-safety-net): preserve uncertainty in benchmark scoring
This commit is contained in:
@@ -216,6 +216,12 @@ question/context/depth, facts/sanitizers, query code and relevant analysis confi
|
|||||||
also includes ranking/admission versions and any model/prompt identity. Comparison results include
|
also includes ranking/admission versions and any model/prompt identity. Comparison results include
|
||||||
both base and head identities. Persist manifests with source byte counts and graph census.
|
both base and head identities. Persist manifests with source byte counts and graph census.
|
||||||
|
|
||||||
|
These identities deliberately separate graph construction from ranking experiments. A ranking-only
|
||||||
|
change can reuse a compatible graph and unchanged query evidence while invalidating affected ranked
|
||||||
|
results. A query/fact change invalidates its answers without automatically rebuilding a graph whose
|
||||||
|
input/frontend/overlay semantics are unchanged. This supports iteration on the core as well as
|
||||||
|
repeated push checks; it does not authorize reusing stale evidence after a source change.
|
||||||
|
|
||||||
Only validated, complete artifacts can satisfy complete-result lookups. Partial artifacts may be
|
Only validated, complete artifacts can satisfy complete-result lookups. Partial artifacts may be
|
||||||
retained for diagnostics but never promoted by cache presence. Publish atomically from private
|
retained for diagnostics but never promoted by cache presence. Publish atomically from private
|
||||||
temporary paths under a per-key lock; another push may wait only within its deadline. Apply a
|
temporary paths under a per-key lock; another push may wait only within its deadline. Apply a
|
||||||
|
|||||||
@@ -38,12 +38,13 @@ started with group 1 (tasks 1.1–1.4); each task remains pending until independ
|
|||||||
- _Boundary: Evaluation harness_
|
- _Boundary: Evaluation harness_
|
||||||
- _Requirements: 6.1, 6.3, 8.3, 8.4_
|
- _Requirements: 6.1, 6.3, 8.3, 8.4_
|
||||||
- _Verified 2026-09-12:_ independent review APPROVED; 902 tests passed, 9 skipped, then 34 focused tests passed after a diagnostic-only correction. Fresh offline validation opened 506,502 bytes and retained all 11 cases/9 snapshots: 7 cases input-ready, 4 with named missing prerequisites. See `benchmarks/push/inputs.json` and `input-validation.json`. Ghost checkout/reviewed labels and independent per-capability PHP/Python populations remain missing; every capability stays experimental. No detector run or admission claim.
|
- _Verified 2026-09-12:_ independent review APPROVED; 902 tests passed, 9 skipped, then 34 focused tests passed after a diagnostic-only correction. Fresh offline validation opened 506,502 bytes and retained all 11 cases/9 snapshots: 7 cases input-ready, 4 with named missing prerequisites. See `benchmarks/push/inputs.json` and `input-validation.json`. Ghost checkout/reviewed labels and independent per-capability PHP/Python populations remain missing; every capability stays experimental. No detector run or admission claim.
|
||||||
- [ ] 1.3 Make the baseline and push scorer fail honestly
|
- [x] 1.3 Make the baseline and push scorer fail honestly
|
||||||
- Retain unmatched targets and unanswered queries in the declared population; require a queried witness before labeling a target transitively detected.
|
- Retain unmatched targets and unanswered queries in the declared population; require a queried witness before labeling a target transitively detected.
|
||||||
- Emit actual vulnerable/fixed outcomes beside rank, selected mode, engine/facts/scope and elapsed stage costs. Freeze a versioned workload/profile before measuring.
|
- Emit actual vulnerable/fixed outcomes beside rank, selected mode, engine/facts/scope and elapsed stage costs. Freeze a versioned workload/profile before measuring.
|
||||||
- Done when a deliberately missing target and failed query cannot improve reported recall, while a verified transitive positive is counted with its witness.
|
- Done when a deliberately missing target and failed query cannot improve reported recall, while a verified transitive positive is counted with its witness.
|
||||||
- _Boundary: Evaluation harness, repository benchmark integration_
|
- _Boundary: Evaluation harness, repository benchmark integration_
|
||||||
- _Requirements: 6.2, 6.3, 6.4, 6.5, 7.3_
|
- _Requirements: 6.2, 6.3, 6.4, 6.5, 7.3_
|
||||||
|
- _Verified 2026-09-12:_ independent review APPROVED; 925 tests passed, 9 skipped. Parent reproduced the frozen profile and actual VAmPI static rerun (15,942 pinned bytes, all 3 targets retained, SQLI unmatched, no transitive/detection claim, exit 1). Empty-run scoring retains 11 unresolved cases and explicitly reports unmeasured/0 recorded cases. Historical baselines remain unchanged with a caveat; new scorer consumes associated execution/review evidence and does not implement replay or admission.
|
||||||
- [ ] 1.4 Establish typed push and engine integration contracts
|
- [ ] 1.4 Establish typed push and engine integration contracts
|
||||||
- Add importable push components and validated configuration for comparison base, shared deadline/cancellation allowance, local cache limit, advisory/blocking and separate coverage policy.
|
- Add importable push components and validated configuration for comparison base, shared deadline/cancellation allowance, local cache limit, advisory/blocking and separate coverage policy.
|
||||||
- Define ownership of update/comparison, change context, selected/deferred scope, graph identity/lease and separate finding/coverage/push statuses as described in design.
|
- Define ownership of update/comparison, change context, selected/deferred scope, graph identity/lease and separate finding/coverage/push statuses as described in design.
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
"counts": {
|
"counts": {
|
||||||
"load_bearing": 92,
|
"load_bearing": 92,
|
||||||
"standalone_capability": 8
|
"standalone_capability": 9
|
||||||
},
|
},
|
||||||
"modules": 100,
|
"modules": 101,
|
||||||
|
"note": "Inventory refreshed for pre-push-safety-net task 1.3 standalone scorer; original artifact filename retained for audit gate.",
|
||||||
"orphaned": [],
|
"orphaned": [],
|
||||||
"rows": [
|
"rows": [
|
||||||
{
|
{
|
||||||
@@ -610,6 +611,12 @@
|
|||||||
"module": "push_inputs",
|
"module": "push_inputs",
|
||||||
"reason": "pre-push-safety-net task 1.2: offline input provenance validator; python -m openultrasast.push_inputs"
|
"reason": "pre-push-safety-net task 1.2: offline input provenance validator; python -m openultrasast.push_inputs"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"classification": "standalone_capability",
|
||||||
|
"importers": [],
|
||||||
|
"module": "push_scoring",
|
||||||
|
"reason": "pre-push-safety-net task 1.3: frozen-profile diagnostic outcome scorer; python -m openultrasast.push_scoring"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"classification": "load_bearing",
|
"classification": "load_bearing",
|
||||||
"importers": [
|
"importers": [
|
||||||
@@ -1076,10 +1083,10 @@
|
|||||||
"hunter_harness": "harness wiring for the tool hunter; used by the hunter path and by mcp",
|
"hunter_harness": "harness wiring for the tool hunter; used by the hunter path and by mcp",
|
||||||
"mcp": "narrow MCP server over stdio; its own `ousast mcp` entry point and OpenCode integration",
|
"mcp": "narrow MCP server over stdio; its own `ousast mcp` entry point and OpenCode integration",
|
||||||
"push_inputs": "pre-push-safety-net task 1.2: offline input provenance validator; python -m openultrasast.push_inputs",
|
"push_inputs": "pre-push-safety-net task 1.2: offline input provenance validator; python -m openultrasast.push_inputs",
|
||||||
|
"push_scoring": "pre-push-safety-net task 1.3: frozen-profile diagnostic outcome scorer; python -m openultrasast.push_scoring",
|
||||||
"skills": "skill router; consumed by mcp and by the OpenCode integration, not by the scan path",
|
"skills": "skill router; consumed by mcp and by the OpenCode integration, not by the scan path",
|
||||||
"slot_contract": "the slot/contract protocol stage_processors implements",
|
"slot_contract": "the slot/contract protocol stage_processors implements",
|
||||||
"stage_processors": "HarnessX slot-contracted scan stages; pinned by the zero-dependency guard in test_gate"
|
"stage_processors": "HarnessX slot-contracted scan stages; pinned by the zero-dependency guard in test_gate"
|
||||||
},
|
},
|
||||||
"update_note": "Current tree inventory refreshed for pre-push-safety-net task 1.2 standalone input validator; filename retains the original audit date. No detection or ranking measurement.",
|
"updated_at": "2026-09-12"
|
||||||
"updated_date": "2026-09-12"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,3 +44,48 @@ Python populations are explicitly missing in `capability_prerequisites`. VAmPI r
|
|||||||
known targets (including SQLI), with `vulnerable=1`; these snapshot regressions are not paired pushes.
|
known targets (including SQLI), with `vulnerable=1`; these snapshot regressions are not paired pushes.
|
||||||
Libpng is an envelope/unsupported-arithmetic control, not a detection success or a supported miss.
|
Libpng is an envelope/unsupported-arithmetic control, not a detection success or a supported miss.
|
||||||
The validator makes no finding, precision, recall, silence, latency or admission claim.
|
The validator makes no finding, precision, recall, silence, latency or admission claim.
|
||||||
|
|
||||||
|
## Frozen diagnostic scoring (task 1.3)
|
||||||
|
|
||||||
|
`push_scoring` consumes recorded execution evidence; it does not run the scanner or
|
||||||
|
implement push delta/admission policy. Freeze the workload and expected configuration
|
||||||
|
**before** executing it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
.venv/bin/python -m openultrasast.push_scoring freeze benchmarks/push/inputs.json benchmarks/push/diagnostic-config-v1.json > /tmp/profile.json
|
||||||
|
.venv/bin/python -m openultrasast.push_scoring score /tmp/profile.json benchmarks/push/input-validation.json benchmarks/push/unmeasured-run-v1.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The committed v1 profile reserves the 11 declared cases and exact tip target locations.
|
||||||
|
It is a proposed 30-second evidence-mode experiment, not measured performance. The
|
||||||
|
`unmeasured-run-v1.json` deliberately contains no executions: scoring it exits **1**,
|
||||||
|
retains all 11 unresolved cases, and earns no recall, coverage or admission. An invalid
|
||||||
|
profile/runtime identity exits **2**. A fully resolved diagnostic score exits **0**;
|
||||||
|
that exit status is not capability admission. Freeze a new version when configuration,
|
||||||
|
source manifest, core, facts, query semantics or scope changes; do not edit a measured
|
||||||
|
profile to match later execution. Runtime `context` must record the actual values,
|
||||||
|
not merely copy an expected profile hash. Profiles are content identities, not signatures.
|
||||||
|
|
||||||
|
Each `records` entry supplies `case_id`, actual `ranking_mode`, `rank` (or null), exact
|
||||||
|
`selected`/`deferred` question IDs, `queries`, `alerts`, `delta`, `coverage`, `cache_state`
|
||||||
|
and nonnegative elapsed `timings` including `total`. Cache states distinguish `cold`,
|
||||||
|
`warm_changed` and `identical_tip`; stage costs remain beside each outcome. Queries
|
||||||
|
supply `id`, `status` and `outcome`. A positive needs a finding with the exact frozen
|
||||||
|
`site` and `family`, a supported `rung`, and a nonempty actual `witness`. A transitive
|
||||||
|
claim additionally needs a recorded `origin` and ordered `flow` from that region to
|
||||||
|
the frozen target's path/function. Missing rank never proves transitivity. Negative
|
||||||
|
answers require the exact `target` and a witnessed discharge; empty/failed queries
|
||||||
|
cannot earn fixed-side silence. These checks establish evidence association, not the
|
||||||
|
truth of an arbitrary supplied witness: use engine output and review the attribution.
|
||||||
|
|
||||||
|
Alerts are separate from raw findings. Only reviewed `actionable_true` alerts with
|
||||||
|
`review_evidence`, `question_id` and exact `site` association can contribute precision;
|
||||||
|
unreviewed/wrong-target alerts remain in its denominator. Benign comment changes retain
|
||||||
|
vulnerable backlog, so a raw positive is not automatically a new alert. The scorer
|
||||||
|
consumes a recorded delta; task 5.1 supplies replay, and the policy tasks establish it.
|
||||||
|
|
||||||
|
Reports retain unresolved cases, sample counts, Wilson intervals, separate regression
|
||||||
|
results, per-capability metrics, coverage and cold/warm latency. Missing benign runs
|
||||||
|
have unknown interruption rate. VAmPI's readable regression may be measured while
|
||||||
|
independent admission prerequisites remain missing. Unsupported/unreviewed populations
|
||||||
|
stay visible. All results remain experimental pending the later joint admission gate.
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
"version": "pre-push-diagnostic-v1-2026-09-12",
|
||||||
|
"ranking_mode": "evidence",
|
||||||
|
"engine": "joern-4.0.625",
|
||||||
|
"facts": "642d643d5b40a20fe482ace566cc77d132870930ecd2f2e881dcecd7dc9a0641",
|
||||||
|
"queries": "64c1534edb07e770ab8631b48bc156ef54b71ddb8d8ee4357fd5022eb11f0229",
|
||||||
|
"policy": "diagnostic-only-no-admission-v1",
|
||||||
|
"scope": "first-party-only; declared vendor exclusion in graph and targets; exact selected/deferred question IDs required per record",
|
||||||
|
"hardware": "Linux-6.8.0-138-generic-x86_64-with-glibc2.39",
|
||||||
|
"budget_seconds": 30,
|
||||||
|
"core": "7ac2bc93b35ce9d0cdcfc543266025519999ff45339657d4a5d283c67957d682",
|
||||||
|
"targets": {
|
||||||
|
"pmpro-introduce": {
|
||||||
|
"path": "classes/class.memberorder.php",
|
||||||
|
"function": "getMemberOrderByCode",
|
||||||
|
"family": "injection",
|
||||||
|
"lines": [
|
||||||
|
936
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pmpro-repair": {
|
||||||
|
"path": "classes/class.memberorder.php",
|
||||||
|
"function": "getMemberOrderByCode",
|
||||||
|
"family": "injection",
|
||||||
|
"lines": [
|
||||||
|
937
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pmpro-benign": {
|
||||||
|
"path": "classes/class.memberorder.php",
|
||||||
|
"function": "getMemberOrderByCode",
|
||||||
|
"family": "injection",
|
||||||
|
"lines": [
|
||||||
|
937
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nodegoat-introduce": {
|
||||||
|
"path": "app/routes/contributions.js",
|
||||||
|
"function": "handleContributionsUpdate",
|
||||||
|
"family": "injection",
|
||||||
|
"lines": [
|
||||||
|
32,
|
||||||
|
33,
|
||||||
|
34
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nodegoat-repair": {
|
||||||
|
"path": "app/routes/contributions.js",
|
||||||
|
"function": "handleContributionsUpdate",
|
||||||
|
"family": "injection",
|
||||||
|
"lines": [
|
||||||
|
32,
|
||||||
|
33,
|
||||||
|
34
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nodegoat-benign": {
|
||||||
|
"path": "app/routes/contributions.js",
|
||||||
|
"function": "handleContributionsUpdate",
|
||||||
|
"family": "injection",
|
||||||
|
"lines": [
|
||||||
|
32,
|
||||||
|
33,
|
||||||
|
34
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"VAmPI-SQLI": {
|
||||||
|
"path": "models/user_model.py",
|
||||||
|
"function": "get_user",
|
||||||
|
"family": "injection",
|
||||||
|
"lines": [
|
||||||
|
73
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"VAmPI-BOLA": {
|
||||||
|
"path": "api_views/users.py",
|
||||||
|
"function": "update_password",
|
||||||
|
"family": "access_control",
|
||||||
|
"lines": [
|
||||||
|
187
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"VAmPI-BOLA-BOOKS": {
|
||||||
|
"path": "api_views/books.py",
|
||||||
|
"function": "get_by_title",
|
||||||
|
"family": "access_control",
|
||||||
|
"lines": [
|
||||||
|
51
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"manifest_sha256": "eec13cb19fe5e2c2402e61ebf4fd17fa809482f321e7a8e615215a12e1bcf845",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"id": "pmpro-introduce",
|
||||||
|
"base": "pmpro-fixed",
|
||||||
|
"tip": "pmpro-vulnerable",
|
||||||
|
"label": "vulnerable",
|
||||||
|
"role": "development",
|
||||||
|
"capability": "php/wordpress/injection",
|
||||||
|
"provenance": "Authored replay direction reintroducing the reviewed operation; not a claim about the historical introducing commit.",
|
||||||
|
"prerequisites": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pmpro-repair",
|
||||||
|
"base": "pmpro-vulnerable",
|
||||||
|
"tip": "pmpro-fixed",
|
||||||
|
"label": "fixed",
|
||||||
|
"role": "development",
|
||||||
|
"capability": "php/wordpress/injection",
|
||||||
|
"provenance": "PMPro upstream2.9.7 to2.9.8 parameter binding fix for CVE-2023-23488.",
|
||||||
|
"prerequisites": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pmpro-benign",
|
||||||
|
"base": "pmpro-vulnerable",
|
||||||
|
"tip": "pmpro-benign",
|
||||||
|
"label": "benign",
|
||||||
|
"role": "development",
|
||||||
|
"capability": "php/wordpress/injection",
|
||||||
|
"provenance": "Authored comment-only change: no new defect; vulnerable backlog remains. This label does not mean repository-clean.",
|
||||||
|
"prerequisites": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nodegoat-introduce",
|
||||||
|
"base": "nodegoat-fixed",
|
||||||
|
"tip": "nodegoat-vulnerable",
|
||||||
|
"label": "vulnerable",
|
||||||
|
"role": "transfer",
|
||||||
|
"capability": "javascript/express/injection",
|
||||||
|
"provenance": "Authored replay direction reintroducing the reviewed operation; not a claim about the historical introducing commit.",
|
||||||
|
"prerequisites": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nodegoat-repair",
|
||||||
|
"base": "nodegoat-vulnerable",
|
||||||
|
"tip": "nodegoat-fixed",
|
||||||
|
"label": "fixed",
|
||||||
|
"role": "transfer",
|
||||||
|
"capability": "javascript/express/injection",
|
||||||
|
"provenance": "Authored replacement of three eval calls with Number conversion; source tutorial also documents alternate numeric parsing.",
|
||||||
|
"prerequisites": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nodegoat-benign",
|
||||||
|
"base": "nodegoat-vulnerable",
|
||||||
|
"tip": "nodegoat-benign",
|
||||||
|
"label": "benign",
|
||||||
|
"role": "transfer",
|
||||||
|
"capability": "javascript/express/injection",
|
||||||
|
"provenance": "Authored comment-only change: no new defect; vulnerable backlog remains. This label does not mean repository-clean.",
|
||||||
|
"prerequisites": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "VAmPI-SQLI",
|
||||||
|
"base": "vampi-regression",
|
||||||
|
"tip": "vampi-regression",
|
||||||
|
"label": "regression",
|
||||||
|
"role": "regression",
|
||||||
|
"capability": "python/flask/injection",
|
||||||
|
"provenance": "Existing recipe known target, full target population retained; environment vulnerable=1. Snapshot regression, not an introducing push.",
|
||||||
|
"prerequisites": [
|
||||||
|
"Reviewed independent vulnerable/fixed/benign push pair required before hook admission."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "VAmPI-BOLA",
|
||||||
|
"base": "vampi-regression",
|
||||||
|
"tip": "vampi-regression",
|
||||||
|
"label": "regression",
|
||||||
|
"role": "regression",
|
||||||
|
"capability": "python/flask/access_control",
|
||||||
|
"provenance": "Existing recipe known target, full target population retained; environment vulnerable=1. Snapshot regression, not an introducing push.",
|
||||||
|
"prerequisites": [
|
||||||
|
"Reviewed independent vulnerable/fixed/benign push pair required before hook admission."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "VAmPI-BOLA-BOOKS",
|
||||||
|
"base": "vampi-regression",
|
||||||
|
"tip": "vampi-regression",
|
||||||
|
"label": "regression",
|
||||||
|
"role": "regression",
|
||||||
|
"capability": "python/flask/access_control",
|
||||||
|
"provenance": "Existing recipe known target, full target population retained; environment vulnerable=1. Snapshot regression, not an introducing push.",
|
||||||
|
"prerequisites": [
|
||||||
|
"Reviewed independent vulnerable/fixed/benign push pair required before hook admission."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "libpng-CVE-2025-66293",
|
||||||
|
"base": "libpng-envelope",
|
||||||
|
"tip": "libpng-envelope",
|
||||||
|
"label": "unsupported",
|
||||||
|
"role": "envelope",
|
||||||
|
"capability": "c/libpng/memory_safety",
|
||||||
|
"provenance": "Arithmetic/bounds is outside the current abstract domain; measures graph/cost envelope only.",
|
||||||
|
"prerequisites": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ghost-reserved-population",
|
||||||
|
"base": "ghost-reserved",
|
||||||
|
"tip": "ghost-reserved",
|
||||||
|
"label": "unreviewed",
|
||||||
|
"role": "reserved_holdout",
|
||||||
|
"capability": "javascript/express/unreviewed",
|
||||||
|
"provenance": "Reserved from production API/module/async shape and public security-fix history before scanner results. Manifest6.64.0-rc.0, Express4.22.2. Not an ESM or Express5 coverage claim.",
|
||||||
|
"prerequisites": [
|
||||||
|
"Review and freeze supported positive/fixed/benign changes from independent history; select family before scanning.",
|
||||||
|
"Freeze core and existing facts after development; preserve reservation and record any contamination."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"config": {
|
||||||
|
"version": "pre-push-diagnostic-v1-2026-09-12",
|
||||||
|
"ranking_mode": "evidence",
|
||||||
|
"engine": "joern-4.0.625",
|
||||||
|
"facts": "642d643d5b40a20fe482ace566cc77d132870930ecd2f2e881dcecd7dc9a0641",
|
||||||
|
"queries": "64c1534edb07e770ab8631b48bc156ef54b71ddb8d8ee4357fd5022eb11f0229",
|
||||||
|
"policy": "diagnostic-only-no-admission-v1",
|
||||||
|
"scope": "first-party-only; declared vendor exclusion in graph and targets; exact selected/deferred question IDs required per record",
|
||||||
|
"hardware": "Linux-6.8.0-138-generic-x86_64-with-glibc2.39",
|
||||||
|
"budget_seconds": 30,
|
||||||
|
"core": "7ac2bc93b35ce9d0cdcfc543266025519999ff45339657d4a5d283c67957d682",
|
||||||
|
"targets": {
|
||||||
|
"pmpro-introduce": {
|
||||||
|
"path": "classes/class.memberorder.php",
|
||||||
|
"function": "getMemberOrderByCode",
|
||||||
|
"family": "injection",
|
||||||
|
"lines": [
|
||||||
|
936
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pmpro-repair": {
|
||||||
|
"path": "classes/class.memberorder.php",
|
||||||
|
"function": "getMemberOrderByCode",
|
||||||
|
"family": "injection",
|
||||||
|
"lines": [
|
||||||
|
937
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pmpro-benign": {
|
||||||
|
"path": "classes/class.memberorder.php",
|
||||||
|
"function": "getMemberOrderByCode",
|
||||||
|
"family": "injection",
|
||||||
|
"lines": [
|
||||||
|
937
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nodegoat-introduce": {
|
||||||
|
"path": "app/routes/contributions.js",
|
||||||
|
"function": "handleContributionsUpdate",
|
||||||
|
"family": "injection",
|
||||||
|
"lines": [
|
||||||
|
32,
|
||||||
|
33,
|
||||||
|
34
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nodegoat-repair": {
|
||||||
|
"path": "app/routes/contributions.js",
|
||||||
|
"function": "handleContributionsUpdate",
|
||||||
|
"family": "injection",
|
||||||
|
"lines": [
|
||||||
|
32,
|
||||||
|
33,
|
||||||
|
34
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nodegoat-benign": {
|
||||||
|
"path": "app/routes/contributions.js",
|
||||||
|
"function": "handleContributionsUpdate",
|
||||||
|
"family": "injection",
|
||||||
|
"lines": [
|
||||||
|
32,
|
||||||
|
33,
|
||||||
|
34
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"VAmPI-SQLI": {
|
||||||
|
"path": "models/user_model.py",
|
||||||
|
"function": "get_user",
|
||||||
|
"family": "injection",
|
||||||
|
"lines": [
|
||||||
|
73
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"VAmPI-BOLA": {
|
||||||
|
"path": "api_views/users.py",
|
||||||
|
"function": "update_password",
|
||||||
|
"family": "access_control",
|
||||||
|
"lines": [
|
||||||
|
187
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"VAmPI-BOLA-BOOKS": {
|
||||||
|
"path": "api_views/books.py",
|
||||||
|
"function": "get_by_title",
|
||||||
|
"family": "access_control",
|
||||||
|
"lines": [
|
||||||
|
51
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gates": {
|
||||||
|
"precision": 0.95,
|
||||||
|
"recall": 0.9,
|
||||||
|
"completion": 0.95,
|
||||||
|
"warm_p95_seconds": 30.0,
|
||||||
|
"fixed_false_alerts": 0,
|
||||||
|
"benign_false_alerts": 0
|
||||||
|
},
|
||||||
|
"profile_sha256": "95c26cf474573766fa24c44f4375cfcb506b196412cd421deadff35b4eb9ef3f"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"profile_sha256": "95c26cf474573766fa24c44f4375cfcb506b196412cd421deadff35b4eb9ef3f",
|
||||||
|
"context": {
|
||||||
|
"ranking_mode": "evidence",
|
||||||
|
"engine": "joern-4.0.625",
|
||||||
|
"facts": "642d643d5b40a20fe482ace566cc77d132870930ecd2f2e881dcecd7dc9a0641",
|
||||||
|
"queries": "64c1534edb07e770ab8631b48bc156ef54b71ddb8d8ee4357fd5022eb11f0229",
|
||||||
|
"policy": "diagnostic-only-no-admission-v1",
|
||||||
|
"scope": "first-party-only; declared vendor exclusion in graph and targets; exact selected/deferred question IDs required per record",
|
||||||
|
"hardware": "Linux-6.8.0-138-generic-x86_64-with-glibc2.39",
|
||||||
|
"budget_seconds": 30,
|
||||||
|
"core": "7ac2bc93b35ce9d0cdcfc543266025519999ff45339657d4a5d283c67957d682"
|
||||||
|
},
|
||||||
|
"records": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"schema_version": 2,
|
||||||
|
"measurement": "positional_coverage_not_detection",
|
||||||
|
"ranking_mode": "static",
|
||||||
|
"profile": {
|
||||||
|
"checkout": {
|
||||||
|
"commit": "f16052dce83f05847133ec98f01c5193a41de7d8",
|
||||||
|
"clean": true,
|
||||||
|
"status_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
},
|
||||||
|
"version": "position-v2",
|
||||||
|
"requested_ranking_mode": "static",
|
||||||
|
"known_targets": [
|
||||||
|
{
|
||||||
|
"id": "VAmPI-SQLI",
|
||||||
|
"family": "injection",
|
||||||
|
"file": "models/user_model.py",
|
||||||
|
"function": "get_user",
|
||||||
|
"line": 73,
|
||||||
|
"in_scope": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "VAmPI-BOLA",
|
||||||
|
"family": "access_control",
|
||||||
|
"file": "api_views/users.py",
|
||||||
|
"function": "update_password",
|
||||||
|
"line": 187,
|
||||||
|
"in_scope": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "VAmPI-BOLA-BOOKS",
|
||||||
|
"family": "access_control",
|
||||||
|
"file": "api_views/books.py",
|
||||||
|
"function": "get_by_title",
|
||||||
|
"line": 51,
|
||||||
|
"in_scope": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"input_witnesses": [
|
||||||
|
{
|
||||||
|
"path": "api_views/books.py",
|
||||||
|
"bytes_read": 3043,
|
||||||
|
"sha256": "78e2eb77d655573f03b7a4d0bca73dc27383b0c5ec7882a5aa8608caa52828e8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "api_views/users.py",
|
||||||
|
"bytes_read": 9257,
|
||||||
|
"sha256": "46006eb7207ebbae163f5320b13e3bf0ec464cef201a51fb2cd43b5cfdb4116c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "models/user_model.py",
|
||||||
|
"bytes_read": 3642,
|
||||||
|
"sha256": "4ce47b9850812e47140b2d2f2e20c08ae4b5338d168ce72f23e3127986ef6e1b"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"implementation": {
|
||||||
|
"benchmarks/ranking/position.py": "c362e58843e669b68bb032574beff2342bd9597e0b451b38c727c1af643450e2",
|
||||||
|
"src/openultrasast/model/regions.py": "0ea1a7b153fad403848aa03f54ae45ea0dcf9c3d8b0f710238362de05139c84b",
|
||||||
|
"src/openultrasast/model/scan.py": "e11423755be4b72cfb6b5fd86b3d2fef68c0cc33aab997210f51430570256e93",
|
||||||
|
"src/openultrasast/model/evidence.py": "cda3619170a77bb8a4b7bb7fecb261e990699fe671714ccc830505a81029788d",
|
||||||
|
"src/openultrasast/model/layout.py": "dc9dc338dff5f1d4389d59d84ba4ddd249949470f9c2303df5ee70ad5d257f2d"
|
||||||
|
},
|
||||||
|
"detection": "not_measured",
|
||||||
|
"sha256": "273dd99adcd94ec93df5e4c30018c145092141ba2c4e28a409d8bb5f8e6939a2"
|
||||||
|
},
|
||||||
|
"detection_recall": null,
|
||||||
|
"supported_population": 3,
|
||||||
|
"unmatched_supported": 1,
|
||||||
|
"input_witnesses": [
|
||||||
|
{
|
||||||
|
"path": "api_views/books.py",
|
||||||
|
"bytes_read": 3043,
|
||||||
|
"sha256": "78e2eb77d655573f03b7a4d0bca73dc27383b0c5ec7882a5aa8608caa52828e8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "api_views/users.py",
|
||||||
|
"bytes_read": 9257,
|
||||||
|
"sha256": "46006eb7207ebbae163f5320b13e3bf0ec464cef201a51fb2cd43b5cfdb4116c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "models/user_model.py",
|
||||||
|
"bytes_read": 3642,
|
||||||
|
"sha256": "4ce47b9850812e47140b2d2f2e20c08ae4b5338d168ce72f23e3127986ef6e1b"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"input_status": "readable",
|
||||||
|
"total_seconds": 0.793,
|
||||||
|
"repo": "vampi",
|
||||||
|
"regions": 25,
|
||||||
|
"distinct_ranks": 5,
|
||||||
|
"largest_tier_share": 0.36,
|
||||||
|
"largest_tier_rank": 1.0,
|
||||||
|
"budget_at_recall": null,
|
||||||
|
"budget_at_full_target_position_coverage": null,
|
||||||
|
"known": [
|
||||||
|
{
|
||||||
|
"id": "VAmPI-SQLI",
|
||||||
|
"family": "injection",
|
||||||
|
"site": "models/user_model.py:73:get_user",
|
||||||
|
"position": null,
|
||||||
|
"rank": null,
|
||||||
|
"in_scope": true,
|
||||||
|
"reached_transitively": false,
|
||||||
|
"detection": "not_measured",
|
||||||
|
"position_status": "unresolved_target"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "VAmPI-BOLA",
|
||||||
|
"family": "access_control",
|
||||||
|
"site": "api_views/users.py:187:update_password",
|
||||||
|
"position": 15,
|
||||||
|
"rank": 0.8,
|
||||||
|
"in_scope": true,
|
||||||
|
"reached_transitively": false,
|
||||||
|
"detection": "not_measured",
|
||||||
|
"position_status": "matched"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "VAmPI-BOLA-BOOKS",
|
||||||
|
"family": "access_control",
|
||||||
|
"site": "api_views/books.py:51:get_by_title",
|
||||||
|
"position": 10,
|
||||||
|
"rank": 0.8,
|
||||||
|
"in_scope": true,
|
||||||
|
"reached_transitively": false,
|
||||||
|
"detection": "not_measured",
|
||||||
|
"position_status": "matched"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Historical ranking measurements
|
||||||
|
|
||||||
|
The eight JSON artifacts dated 2026-09-10 and 2026-09-11 are preserved as recorded.
|
||||||
|
They predate the scoring correction in pre-push-safety-net task 1.3 and must not be
|
||||||
|
interpreted as end-to-end detection or pre-push acceptance results.
|
||||||
|
|
||||||
|
An audit on 2026-09-12 read all eight artifacts. Each records VAmPI-SQLI with
|
||||||
|
`position: null`, `reached_transitively: true` and a repository `budget_at_recall`
|
||||||
|
of 16. The old position instrument assigned that transitive label solely because
|
||||||
|
it could not locate a matching region, then omitted the target from the budget
|
||||||
|
calculation. These artifacts contain no queried witness establishing that label.
|
||||||
|
This does not negate separately demonstrated VAmPI detections; it means this
|
||||||
|
particular metric cannot establish them.
|
||||||
|
|
||||||
|
Other recorded positions, scores, and elapsed stage costs remain historical
|
||||||
|
observations under their recorded setup. Rank coverage is not detection recall,
|
||||||
|
and a stage duration is not total hook latency. Use the corrected instrument and
|
||||||
|
an explicit frozen profile for new comparisons. Retain unknown and unanswered
|
||||||
|
cases, and require a queried target witness before counting transitive detection.
|
||||||
|
Do not rewrite these files to make an earlier run appear to use the new semantics.
|
||||||
@@ -1,23 +1,19 @@
|
|||||||
"""Phase 0 of flow-aware-ranking: where does each pinned CVE sit in the order the scan actually examines?
|
"""Positional coverage baseline v2: ranks are not detection recall.
|
||||||
|
|
||||||
The ranker's success metric is not "is the CVE in the budget" -- on Paid Memberships Pro it already is, at
|
Every declared target remains visible, including unmatched sites. No transitive detection
|
||||||
position 390 of 4,463 -- but **how small the budget can be while every CVE stays in it**. That number is
|
is inferred here: that requires recorded answered queries and target witnesses in the
|
||||||
`budget_at_recall`, and until it is written down nothing later can claim to have improved it.
|
push scorer. Historical v1 artifacts remain unchanged.
|
||||||
|
|
||||||
This is the baseline. It runs no engine and asks no model: it is the region pipeline exactly as the scan
|
|
||||||
uses it, sorted exactly as the scan sorts it, with each known vulnerability's position read off. Offline,
|
|
||||||
against pinned checkouts, so two runs of one repository agree.
|
|
||||||
|
|
||||||
ousast-venv/bin/python benchmarks/ranking/position.py # every pinned recipe with a checkout
|
|
||||||
ousast-venv/bin/python benchmarks/ranking/position.py --json # the same, as an artifact
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -101,10 +97,55 @@ def evidence_ordered_regions(root: Path, regions: list[ScanRegion], language: st
|
|||||||
|
|
||||||
|
|
||||||
def measure(name: str, root: Path, known: list[dict[str, object]], *, by_evidence: bool = False, language: str = "") -> dict[str, object]:
|
def measure(name: str, root: Path, known: list[dict[str, object]], *, by_evidence: bool = False, language: str = "") -> dict[str, object]:
|
||||||
regions = ordered_regions(root)
|
started = time.monotonic()
|
||||||
|
inputs = []
|
||||||
|
for path in sorted({str(item["file"]) for item in known}):
|
||||||
|
try:
|
||||||
|
content = (root / path).read_bytes()
|
||||||
|
if not content:
|
||||||
|
raise ValueError("empty source")
|
||||||
|
inputs.append(dict(path=path, bytes_read=len(content), sha256=hashlib.sha256(content).hexdigest()))
|
||||||
|
except (OSError, ValueError) as exc:
|
||||||
|
inputs.append(dict(path=path, bytes_read=0, problem=str(exc)))
|
||||||
facts: dict[str, object] = {}
|
facts: dict[str, object] = {}
|
||||||
|
# Frozen identity recorded before the evidence pass; this is a rank-only experiment.
|
||||||
|
source_root = Path(__file__).resolve().parents[2]
|
||||||
|
version_paths = [
|
||||||
|
"benchmarks/ranking/position.py",
|
||||||
|
"src/openultrasast/model/regions.py",
|
||||||
|
"src/openultrasast/model/scan.py",
|
||||||
|
"src/openultrasast/model/evidence.py",
|
||||||
|
"src/openultrasast/model/layout.py",
|
||||||
|
]
|
||||||
|
checkout = {}
|
||||||
|
try:
|
||||||
|
commit = (
|
||||||
|
subprocess.check_output(["git", "-C", str(root), "rev-parse", "HEAD"], stderr=subprocess.DEVNULL, timeout=10).decode().strip()
|
||||||
|
)
|
||||||
|
status = subprocess.check_output(["git", "-C", str(root), "status", "--porcelain"], stderr=subprocess.DEVNULL, timeout=10)
|
||||||
|
checkout = dict(commit=commit, clean=not status, status_sha256=hashlib.sha256(status).hexdigest())
|
||||||
|
except (OSError, subprocess.SubprocessError):
|
||||||
|
checkout = dict(problem="checkout commit unavailable")
|
||||||
|
profile = dict(
|
||||||
|
checkout=checkout,
|
||||||
|
version="position-v2",
|
||||||
|
requested_ranking_mode="evidence" if by_evidence else "static",
|
||||||
|
known_targets=known,
|
||||||
|
input_witnesses=inputs,
|
||||||
|
implementation={path: hashlib.sha256((source_root / path).read_bytes()).hexdigest() for path in version_paths},
|
||||||
|
detection="not_measured",
|
||||||
|
)
|
||||||
|
profile["sha256"] = hashlib.sha256(json.dumps(profile, sort_keys=True).encode()).hexdigest()
|
||||||
|
regions = ordered_regions(root) if root.is_dir() else []
|
||||||
|
actual_mode = "static"
|
||||||
if by_evidence:
|
if by_evidence:
|
||||||
|
try:
|
||||||
regions, facts = evidence_ordered_regions(root, regions, language)
|
regions, facts = evidence_ordered_regions(root, regions, language)
|
||||||
|
actual_mode = "evidence"
|
||||||
|
except (OSError, RuntimeError, subprocess.SubprocessError, SystemExit) as exc:
|
||||||
|
regions = []
|
||||||
|
actual_mode = "unavailable"
|
||||||
|
facts = dict(evidence_failure=str(exc))
|
||||||
tiers = Counter(r.rank for r in regions)
|
tiers = Counter(r.rank for r in regions)
|
||||||
largest = max(tiers.values()) if tiers else 0
|
largest = max(tiers.values()) if tiers else 0
|
||||||
rows: list[dict[str, object]] = []
|
rows: list[dict[str, object]] = []
|
||||||
@@ -112,7 +153,11 @@ def measure(name: str, root: Path, known: list[dict[str, object]], *, by_evidenc
|
|||||||
# A known vulnerability names a file and a function. Match on both, path by suffix because a recipe
|
# A known vulnerability names a file and a function. Match on both, path by suffix because a recipe
|
||||||
# writes it relative to the repository and a region may carry it the same way or deeper.
|
# writes it relative to the repository and a region may carry it the same way or deeper.
|
||||||
hit = next(
|
hit = next(
|
||||||
(i for i, r in enumerate(regions) if r.path.endswith(str(item["file"])) and r.function == item["function"]),
|
(
|
||||||
|
i
|
||||||
|
for i, r in enumerate(regions)
|
||||||
|
if r.path in (str(item["file"]), str(root / str(item["file"]))) and r.function == item["function"]
|
||||||
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
rows.append(
|
rows.append(
|
||||||
@@ -120,26 +165,39 @@ def measure(name: str, root: Path, known: list[dict[str, object]], *, by_evidenc
|
|||||||
"id": item["id"],
|
"id": item["id"],
|
||||||
"family": item["family"],
|
"family": item["family"],
|
||||||
"site": f"{item['file']}:{item['line']}:{item['function']}",
|
"site": f"{item['file']}:{item['line']}:{item['function']}",
|
||||||
# Zero-based. None means no region is named after this function: the site is a SINK that an
|
# Zero-based position is only a region match, never a completed detection.
|
||||||
# entry-point region elsewhere reaches through `callDepth` -- VAmPI's SQL injection lives in
|
|
||||||
# `models/user_model.py:get_user` and is found from the handler that calls it. Its true
|
|
||||||
# position is that handler's, which only the flow can say, so it is reported and left out of
|
|
||||||
# `budget_at_recall` rather than counted as a miss it is not.
|
|
||||||
"position": hit,
|
"position": hit,
|
||||||
"rank": regions[hit].rank if hit is not None else None,
|
"rank": regions[hit].rank if hit is not None else None,
|
||||||
"in_scope": item["in_scope"],
|
"in_scope": item["in_scope"],
|
||||||
"reached_transitively": hit is None,
|
"reached_transitively": False,
|
||||||
|
"detection": "not_measured",
|
||||||
|
"position_status": "matched" if hit is not None else "unresolved_target",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
positioned = [r["position"] for r in rows if r["position"] is not None and r["in_scope"]]
|
positioned = [r["position"] for r in rows if r["position"] is not None and r["in_scope"]]
|
||||||
|
supported = [row for row in rows if row["in_scope"]]
|
||||||
|
all_located = bool(supported) and len(positioned) == len(supported)
|
||||||
return {
|
return {
|
||||||
|
"schema_version": 2,
|
||||||
|
"measurement": "positional_coverage_not_detection",
|
||||||
|
"ranking_mode": actual_mode,
|
||||||
|
"profile": profile,
|
||||||
|
"detection_recall": None,
|
||||||
|
"supported_population": len(supported),
|
||||||
|
"unmatched_supported": len(supported) - len(positioned),
|
||||||
|
"input_witnesses": inputs,
|
||||||
|
"input_status": "readable" if inputs and all(row["bytes_read"] for row in inputs) else "incomplete",
|
||||||
|
"total_seconds": round(time.monotonic() - started, 3),
|
||||||
"repo": name,
|
"repo": name,
|
||||||
"regions": len(regions),
|
"regions": len(regions),
|
||||||
"distinct_ranks": len(tiers),
|
"distinct_ranks": len(tiers),
|
||||||
"largest_tier_share": round(largest / len(regions), 3) if regions else 0.0,
|
"largest_tier_share": round(largest / len(regions), 3) if regions else 0.0,
|
||||||
"largest_tier_rank": max(tiers, key=lambda k: tiers[k]) if tiers else None,
|
"largest_tier_rank": max(tiers, key=lambda k: tiers[k]) if tiers else None,
|
||||||
# The smallest examined-region budget that still holds every in-scope known vulnerability.
|
# The smallest examined-region budget that still holds every in-scope known vulnerability.
|
||||||
"budget_at_recall": (max(positioned) + 1) if positioned else None,
|
"budget_at_recall": None, # Retired misleading name: rank does not measure recall.
|
||||||
|
"budget_at_full_target_position_coverage": (max(positioned) + 1)
|
||||||
|
if all_located and all(row["bytes_read"] for row in inputs)
|
||||||
|
else None,
|
||||||
"known": rows,
|
"known": rows,
|
||||||
**facts,
|
**facts,
|
||||||
}
|
}
|
||||||
@@ -160,8 +218,7 @@ def main() -> int:
|
|||||||
continue
|
continue
|
||||||
root = checkout_path(recipe)
|
root = checkout_path(recipe)
|
||||||
if not root.is_dir():
|
if not root.is_dir():
|
||||||
print(f"{recipe.name}: not checked out, skipping", file=sys.stderr)
|
print(f"{recipe.name}: not checked out; retaining unresolved targets", file=sys.stderr)
|
||||||
continue
|
|
||||||
known = [
|
known = [
|
||||||
{"id": k.id, "family": k.family, "file": k.file, "function": k.function, "line": k.line, "in_scope": k.in_scope}
|
{"id": k.id, "family": k.family, "file": k.file, "function": k.function, "line": k.line, "in_scope": k.in_scope}
|
||||||
for k in recipe.known
|
for k in recipe.known
|
||||||
@@ -173,20 +230,20 @@ def main() -> int:
|
|||||||
|
|
||||||
if args.json:
|
if args.json:
|
||||||
print(json.dumps(results, indent=2))
|
print(json.dumps(results, indent=2))
|
||||||
return 0
|
return 1 if any(r["unmatched_supported"] or r["input_status"] != "readable" or r.get("evidence_failure") for r in results) else 0
|
||||||
for r in results:
|
for r in results:
|
||||||
print(
|
print(
|
||||||
f"{r['repo']}: {r['regions']} regions, {r['distinct_ranks']} distinct ranks, "
|
f"{r['repo']}: {r['regions']} regions, {r['distinct_ranks']} distinct ranks, "
|
||||||
f"{100 * float(r['largest_tier_share']):.1f}% share rank {r['largest_tier_rank']}"
|
f"{100 * float(r['largest_tier_share']):.1f}% share rank {r['largest_tier_rank']}"
|
||||||
)
|
)
|
||||||
for k in r["known"]:
|
for k in r["known"]:
|
||||||
where = "reached via another region" if k["position"] is None else f"position {k['position']} (rank {k['rank']})"
|
where = "unresolved target position" if k["position"] is None else f"position {k['position']} (rank {k['rank']})"
|
||||||
scope = "" if k["in_scope"] else " [out of scope]"
|
scope = "" if k["in_scope"] else " [out of scope]"
|
||||||
print(f" {k['id']:16} {where:28} {k['site']}{scope}")
|
print(f" {k['id']:16} {where:28} {k['site']}{scope}")
|
||||||
print(f" budget_at_recall = {r['budget_at_recall']}")
|
print(f" budget_at_full_target_position_coverage = {r['budget_at_full_target_position_coverage']}; detection not measured")
|
||||||
if "tier_counts" in r:
|
if "tier_counts" in r:
|
||||||
print(f" build {r['build_seconds']}s, evidence {r['evidence_seconds']}s over {r['pairs']} pairs, tiers {r['tier_counts']}")
|
print(f" build {r['build_seconds']}s, evidence {r['evidence_seconds']}s over {r['pairs']} pairs, tiers {r['tier_counts']}")
|
||||||
return 0
|
return 1 if any(r["unmatched_supported"] or r["input_status"] != "readable" or r.get("evidence_failure") for r in results) else 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ SPEC_OWNED = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
STANDALONE = {
|
STANDALONE = {
|
||||||
|
"push_scoring": "pre-push-safety-net task 1.3: frozen-profile diagnostic outcome scorer; python -m openultrasast.push_scoring",
|
||||||
"push_inputs": "pre-push-safety-net task 1.2: offline input provenance validator; python -m openultrasast.push_inputs",
|
"push_inputs": "pre-push-safety-net task 1.2: offline input provenance validator; python -m openultrasast.push_inputs",
|
||||||
"mcp": "narrow MCP server over stdio; its own `ousast mcp` entry point and OpenCode integration",
|
"mcp": "narrow MCP server over stdio; its own `ousast mcp` entry point and OpenCode integration",
|
||||||
"skills": "skill router; consumed by mcp and by the OpenCode integration, not by the scan path",
|
"skills": "skill router; consumed by mcp and by the OpenCode integration, not by the scan path",
|
||||||
|
|||||||
@@ -0,0 +1,301 @@
|
|||||||
|
"""Versioned diagnostic scorer for recorded push experiments, independent of replay.
|
||||||
|
|
||||||
|
Answers and reviewed alerts are supplied by the experiment; rank is never an answer.
|
||||||
|
This instrument checks provenance and exact witness associations, not witness semantics.
|
||||||
|
Capability admission remains the later independent evaluation gate.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
_CONTEXT = ("ranking_mode", "engine", "facts", "queries", "policy", "scope", "hardware", "budget_seconds", "core")
|
||||||
|
|
||||||
|
|
||||||
|
def _digest(value: Any) -> str:
|
||||||
|
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _unique(rows: list[dict[str, Any]], key: str) -> dict[str, dict[str, Any]]:
|
||||||
|
indexed = {row[key]: row for row in rows}
|
||||||
|
if len(indexed) != len(rows):
|
||||||
|
raise ValueError(f"duplicate {key}")
|
||||||
|
return indexed
|
||||||
|
|
||||||
|
|
||||||
|
def freeze_profile(manifest: bytes, config: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Freeze BEFORE executing queries; changing any comparison identity needs a new profile."""
|
||||||
|
data = json.loads(manifest)
|
||||||
|
cases = data["cases"]
|
||||||
|
if data.get("schema_version") != 1 or not cases:
|
||||||
|
raise ValueError("unsupported or empty input manifest")
|
||||||
|
_unique(cases, "id")
|
||||||
|
for name in ("version", *_CONTEXT):
|
||||||
|
if name not in config or config[name] in (None, ""):
|
||||||
|
raise ValueError(f"profile requires {name}")
|
||||||
|
if config["ranking_mode"] not in ("static", "evidence"):
|
||||||
|
raise ValueError("unknown ranking mode")
|
||||||
|
if not math.isfinite(config["budget_seconds"]) or config["budget_seconds"] <= 0:
|
||||||
|
raise ValueError("budget must be finite and positive")
|
||||||
|
for case in cases:
|
||||||
|
if case["label"] not in ("unsupported", "unreviewed"):
|
||||||
|
target = config.get("targets", {}).get(case["id"], {})
|
||||||
|
if not target.get("path") or not target.get("family") or not target.get("function") or not target.get("lines"):
|
||||||
|
raise ValueError(f"declare target before measuring {case['id']}")
|
||||||
|
if any(type(line) is not int or line < 1 for line in target["lines"]):
|
||||||
|
raise ValueError("target lines must be positive integers")
|
||||||
|
profile: dict[str, Any] = dict(
|
||||||
|
schema_version=1,
|
||||||
|
manifest_sha256=hashlib.sha256(manifest).hexdigest(),
|
||||||
|
cases=cases,
|
||||||
|
config=config,
|
||||||
|
gates=dict(precision=0.95, recall=0.90, completion=0.95, warm_p95_seconds=30.0, fixed_false_alerts=0, benign_false_alerts=0),
|
||||||
|
)
|
||||||
|
# Own copies: mutating the caller's config cannot mutate a frozen profile.
|
||||||
|
profile = json.loads(json.dumps(profile, allow_nan=False))
|
||||||
|
profile["profile_sha256"] = _digest(profile)
|
||||||
|
return profile
|
||||||
|
|
||||||
|
|
||||||
|
def _rate(numerator: int, denominator: int) -> dict[str, Any]:
|
||||||
|
"""Observed rate and Wilson 95% interval; no samples is undefined, never perfect."""
|
||||||
|
result: dict[str, Any] = dict(numerator=numerator, denominator=denominator, value=None, wilson95=None)
|
||||||
|
if denominator:
|
||||||
|
p, z = numerator / denominator, 1.959963984540054
|
||||||
|
center = (p + z * z / (2 * denominator)) / (1 + z * z / denominator)
|
||||||
|
half = z * math.sqrt(p * (1 - p) / denominator + z * z / (4 * denominator**2)) / (1 + z * z / denominator)
|
||||||
|
result.update(value=p, wilson95=[max(0.0, center - half), min(1.0, center + half)])
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _witness(query: dict[str, Any], target: dict[str, Any]) -> dict[str, Any] | None:
|
||||||
|
finding = query.get("finding", {})
|
||||||
|
sites = {f"{target['path']}:{line}" for line in target["lines"]}
|
||||||
|
if (
|
||||||
|
query.get("status") == "answered"
|
||||||
|
and query.get("outcome") == "positive"
|
||||||
|
and finding.get("site") in sites
|
||||||
|
and finding.get("family") == target["family"]
|
||||||
|
and finding.get("rung") in ("model_entailed", "execution_confirmed")
|
||||||
|
and isinstance(finding.get("witness"), str)
|
||||||
|
and finding["witness"].strip()
|
||||||
|
):
|
||||||
|
return dict(question_id=query["id"], **finding)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _case(
|
||||||
|
case: dict[str, Any], record: dict[str, Any] | None, profile: dict[str, Any], snapshots: dict[str, dict[str, Any]]
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
row = dict(case, outcome="unresolved", reached_transitively=False, witnesses=[], complete=False, recorded=record)
|
||||||
|
if record is None:
|
||||||
|
row["problem"] = "no recorded execution"
|
||||||
|
return row
|
||||||
|
if case["label"] in ("unsupported", "unreviewed"):
|
||||||
|
row["problem"] = f"{case['label']} population; no detection claim"
|
||||||
|
return row
|
||||||
|
if any(
|
||||||
|
snapshots.get(case[key], {}).get("status") != "ready" or snapshots.get(case[key], {}).get("bytes_read", 0) <= 0
|
||||||
|
for key in ("base", "tip")
|
||||||
|
):
|
||||||
|
row["problem"] = "input bytes not verified"
|
||||||
|
return row
|
||||||
|
selected, deferred = record["selected"], record["deferred"]
|
||||||
|
if len(set(selected + deferred)) != len(selected + deferred):
|
||||||
|
raise ValueError("duplicate or overlapping selected/deferred questions")
|
||||||
|
queries = _unique(record["queries"], "id")
|
||||||
|
if not set(queries).issubset(selected):
|
||||||
|
row["problem"] = "query absent from selected scope"
|
||||||
|
return row
|
||||||
|
target = profile["config"]["targets"][case["id"]]
|
||||||
|
witnesses = [w for q in queries.values() if (w := _witness(q, target)) is not None]
|
||||||
|
complete = (
|
||||||
|
bool(selected)
|
||||||
|
and not deferred
|
||||||
|
and set(queries) == set(selected)
|
||||||
|
and all(q.get("status") == "answered" and q.get("outcome") in ("positive", "negative") for q in queries.values())
|
||||||
|
and record.get("coverage") == "complete"
|
||||||
|
)
|
||||||
|
negative = any(
|
||||||
|
q.get("status") == "answered" and q.get("outcome") == "negative" and q.get("target") == target and isinstance(q.get("witness"), str)
|
||||||
|
and bool(q["witness"].strip())
|
||||||
|
for q in queries.values()
|
||||||
|
)
|
||||||
|
target_region = {key: target[key] for key in ("path", "function")}
|
||||||
|
transitive = any(
|
||||||
|
q.get("flow")
|
||||||
|
and len(q["flow"]) > 1
|
||||||
|
and q["flow"][0] == q.get("origin")
|
||||||
|
and q["flow"][-1] == target_region
|
||||||
|
and q.get("origin") != target_region
|
||||||
|
and _witness(q, target)
|
||||||
|
for q in queries.values()
|
||||||
|
)
|
||||||
|
row.update(
|
||||||
|
witnesses=witnesses,
|
||||||
|
complete=complete and bool(witnesses or negative),
|
||||||
|
outcome="positive" if witnesses else "negative" if complete and negative else "unresolved",
|
||||||
|
reached_transitively=bool(transitive),
|
||||||
|
)
|
||||||
|
if row["outcome"] == "unresolved":
|
||||||
|
row["problem"] = "target unmatched, unanswered, or lacks exact witnessed answer"
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def _actionable(row: dict[str, Any], alert: dict[str, Any]) -> bool:
|
||||||
|
return (
|
||||||
|
alert.get("review") == "actionable_true"
|
||||||
|
and bool(alert.get("review_evidence"))
|
||||||
|
and any(alert.get("question_id") == w["question_id"] and alert.get("site") == w["site"] for w in row["witnesses"])
|
||||||
|
and row["label"] == "vulnerable"
|
||||||
|
and row["outcome"] == "positive"
|
||||||
|
and (row["recorded"] or {}).get("delta") in ("new", "worsened")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _metrics(rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||||||
|
positive = [r for r in rows if r["label"] == "vulnerable"]
|
||||||
|
fixed = [r for r in rows if r["label"] == "fixed"]
|
||||||
|
benign = [r for r in rows if r["label"] == "benign"]
|
||||||
|
supported = [r for r in rows if r["label"] not in ("unsupported", "unreviewed")]
|
||||||
|
alerts = [(r, a) for r in rows for a in (r["recorded"] or {}).get("alerts", [])]
|
||||||
|
# Unreviewed emitted alerts remain in the precision denominator. A high rung alone is not actionability.
|
||||||
|
actionable = sum(_actionable(r, a) for r, a in alerts)
|
||||||
|
result = dict(
|
||||||
|
recall=_rate(
|
||||||
|
sum(r["outcome"] == "positive" and (r["recorded"] or {}).get("delta") in ("new", "worsened") for r in positive), len(positive)
|
||||||
|
),
|
||||||
|
precision=_rate(actionable, len(alerts)),
|
||||||
|
fixed_side_silence=_rate(sum(r["outcome"] == "negative" and not r["recorded"].get("alerts") for r in fixed), len(fixed)),
|
||||||
|
benign_interruption=_rate(sum(bool((r["recorded"] or {}).get("alerts")) for r in benign), len(benign)),
|
||||||
|
completion=_rate(sum(r["complete"] for r in supported), len(supported)),
|
||||||
|
)
|
||||||
|
latency = {}
|
||||||
|
for state in ("cold", "warm_changed", "identical_tip"):
|
||||||
|
values = sorted(r["recorded"]["timings"]["total"] for r in rows if r["recorded"] and r["recorded"].get("cache_state") == state)
|
||||||
|
latency[state] = dict(
|
||||||
|
samples=len(values),
|
||||||
|
p50=values[math.ceil(len(values) * 0.5) - 1] if values else None,
|
||||||
|
p95=values[math.ceil(len(values) * 0.95) - 1] if values else None,
|
||||||
|
)
|
||||||
|
regression = [r for r in rows if r["label"] == "regression"]
|
||||||
|
result["regression_detection"] = _rate(sum(r["outcome"] == "positive" for r in regression), len(regression))
|
||||||
|
unknown_benign = sum(r["recorded"] is None for r in benign)
|
||||||
|
result["benign_interruption"]["unobserved"] = unknown_benign
|
||||||
|
if unknown_benign:
|
||||||
|
result["benign_interruption"]["value"] = None
|
||||||
|
result["benign_interruption"]["wilson95"] = None
|
||||||
|
result["actionable_recall"] = _rate(
|
||||||
|
sum(any(_actionable(r, a) for a in (r["recorded"] or {}).get("alerts", [])) for r in positive), len(positive)
|
||||||
|
)
|
||||||
|
declared_queries = sum(len(r["recorded"]["selected"]) + len(r["recorded"]["deferred"]) for r in supported if r["recorded"])
|
||||||
|
answered_queries = sum(
|
||||||
|
q.get("status") == "answered" and q.get("outcome") in ("positive", "negative") and q.get("id") in r["recorded"]["selected"]
|
||||||
|
for r in supported
|
||||||
|
if r["recorded"]
|
||||||
|
for q in r["recorded"]["queries"]
|
||||||
|
)
|
||||||
|
result["query_completion"] = _rate(answered_queries, declared_queries)
|
||||||
|
result["query_completion"]["unmeasured_cases"] = sum(r["recorded"] is None for r in supported)
|
||||||
|
if result["query_completion"]["unmeasured_cases"]:
|
||||||
|
result["query_completion"]["value"] = None
|
||||||
|
result["query_completion"]["wilson95"] = None
|
||||||
|
result["latency"] = latency
|
||||||
|
result["unresolved"] = sum(r["outcome"] == "unresolved" for r in rows)
|
||||||
|
result["timeouts"] = sum((r["recorded"] or {}).get("coverage") == "timeout" for r in rows)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def score(profile: dict[str, Any], validation: dict[str, Any], run: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Score a recorded experiment against its predeclared population, never a filtered result list."""
|
||||||
|
identity = profile["profile_sha256"]
|
||||||
|
if _digest({k: v for k, v in profile.items() if k != "profile_sha256"}) != identity:
|
||||||
|
raise ValueError("mutated frozen profile")
|
||||||
|
if run["profile_sha256"] != identity or validation["manifest_sha256"] != profile["manifest_sha256"]:
|
||||||
|
raise ValueError("profile or manifest mismatch")
|
||||||
|
for key in _CONTEXT:
|
||||||
|
if run.get("context", {}).get(key) != profile["config"][key]:
|
||||||
|
raise ValueError(f"actual run context mismatch: {key}")
|
||||||
|
cases = _unique(profile["cases"], "id")
|
||||||
|
validated = _unique(validation["cases"], "id")
|
||||||
|
if set(cases) != set(validated) or any(
|
||||||
|
any(validated[key].get(field) != case.get(field) for field in case) for key, case in cases.items()
|
||||||
|
):
|
||||||
|
raise ValueError("validation population or labels differ from frozen manifest")
|
||||||
|
records = _unique(run["records"], "case_id")
|
||||||
|
if not set(records).issubset(cases):
|
||||||
|
raise ValueError("undeclared result case")
|
||||||
|
for record in records.values():
|
||||||
|
for field in ("rank", "selected", "deferred", "queries", "alerts", "delta", "coverage"):
|
||||||
|
if field not in record:
|
||||||
|
raise ValueError(f"record requires observed {field}")
|
||||||
|
if record["delta"] not in ("new", "worsened", "removed", "unchanged", "unknown"):
|
||||||
|
raise ValueError("unknown delta disposition")
|
||||||
|
if record["coverage"] not in ("complete", "incomplete", "timeout", "error"):
|
||||||
|
raise ValueError("unknown coverage status")
|
||||||
|
if record["ranking_mode"] != profile["config"]["ranking_mode"]:
|
||||||
|
raise ValueError("actual ranking mode differs from frozen profile")
|
||||||
|
times = record["timings"]
|
||||||
|
if not {"total", "build", "query"}.issubset(times) or any(
|
||||||
|
type(v) not in (int, float) or not math.isfinite(v) or v < 0 for v in times.values()
|
||||||
|
):
|
||||||
|
raise ValueError("timings must contain finite nonnegative build, query and total elapsed seconds")
|
||||||
|
if record.get("cache_state") not in ("cold", "warm_changed", "identical_tip"):
|
||||||
|
raise ValueError("declare cold, warm_changed or identical_tip cache state")
|
||||||
|
snapshots = _unique(validation["snapshots"], "id")
|
||||||
|
rows = [_case(case, records.get(key), profile, snapshots) for key, case in cases.items()]
|
||||||
|
for row in rows:
|
||||||
|
row["input_status"] = validated[row["id"]]["status"]
|
||||||
|
row["input_problem"] = validated[row["id"]].get("problem")
|
||||||
|
if row["input_status"] == "invalid_input":
|
||||||
|
row.update(outcome="unresolved", complete=False, witnesses=[], reached_transitively=False, problem="invalid validated input")
|
||||||
|
return dict(
|
||||||
|
schema_version=1,
|
||||||
|
profile_sha256=identity,
|
||||||
|
profile=profile,
|
||||||
|
population=len(rows),
|
||||||
|
recorded_cases=len(records),
|
||||||
|
measurement_status="recorded" if records else "unmeasured",
|
||||||
|
cases=rows,
|
||||||
|
metrics=_metrics(rows),
|
||||||
|
by_capability={cap: _metrics([r for r in rows if r["capability"] == cap]) for cap in sorted({r["capability"] for r in rows})},
|
||||||
|
admission="experimental",
|
||||||
|
gates_passed=False,
|
||||||
|
note=(
|
||||||
|
"Diagnostic instrument only. No admission without independent capability evaluation. "
|
||||||
|
"Missing runs contribute only conservative full-population bounds, not observed detector performance. "
|
||||||
|
"Witness association is checked; semantic/actionability review is supplied evidence, not inferred from rank."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
freeze = sub.add_parser("freeze")
|
||||||
|
freeze.add_argument("manifest", type=Path)
|
||||||
|
freeze.add_argument("config", type=Path)
|
||||||
|
measure = sub.add_parser("score")
|
||||||
|
measure.add_argument("profile", type=Path)
|
||||||
|
measure.add_argument("validation", type=Path)
|
||||||
|
measure.add_argument("run", type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
if args.command == "freeze":
|
||||||
|
result = freeze_profile(args.manifest.read_bytes(), json.loads(args.config.read_text()))
|
||||||
|
else:
|
||||||
|
result = score(json.loads(args.profile.read_text()), json.loads(args.validation.read_text()), json.loads(args.run.read_text()))
|
||||||
|
except (ValueError, KeyError, TypeError, OSError) as exc:
|
||||||
|
print(json.dumps(dict(status="invalid_measurement", problem=str(exc))))
|
||||||
|
return 2
|
||||||
|
print(json.dumps(result, indent=2, allow_nan=False))
|
||||||
|
return 1 if args.command == "score" and result["metrics"]["unresolved"] else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
"""An absent answer must never improve a security score."""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import hashlib
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from openultrasast.push_scoring import freeze_profile, score
|
||||||
|
|
||||||
|
|
||||||
|
def inputs():
|
||||||
|
cases = [
|
||||||
|
dict(id=label, label=label, capability="javascript/express/injection", role="development", base="b", tip="h", prerequisites=[])
|
||||||
|
for label in ("vulnerable", "fixed", "benign", "regression", "unreviewed")
|
||||||
|
]
|
||||||
|
manifest = json.dumps(dict(schema_version=1, cases=cases)).encode()
|
||||||
|
config = dict(
|
||||||
|
version="test-v1",
|
||||||
|
core="core-test",
|
||||||
|
ranking_mode="static",
|
||||||
|
engine="joern-test",
|
||||||
|
facts="facts-test",
|
||||||
|
queries="queries-test",
|
||||||
|
policy="diagnostic-test",
|
||||||
|
scope="first-party-test",
|
||||||
|
hardware="test",
|
||||||
|
budget_seconds=30,
|
||||||
|
targets={case["id"]: dict(path="api.js", function="evalBody", lines=[4], family="injection") for case in cases},
|
||||||
|
)
|
||||||
|
profile = freeze_profile(manifest, config)
|
||||||
|
validation = dict(
|
||||||
|
manifest_sha256=hashlib.sha256(manifest).hexdigest(),
|
||||||
|
cases=[dict(case, status="ready") for case in cases],
|
||||||
|
snapshots=[dict(id="b", status="ready", bytes_read=42), dict(id="h", status="ready", bytes_read=42)],
|
||||||
|
)
|
||||||
|
return profile, validation
|
||||||
|
|
||||||
|
|
||||||
|
def record(case="vulnerable", **updates):
|
||||||
|
value = dict(
|
||||||
|
case_id=case,
|
||||||
|
ranking_mode="static",
|
||||||
|
rank=None,
|
||||||
|
selected=["q-handler"],
|
||||||
|
deferred=[],
|
||||||
|
queries=[
|
||||||
|
dict(
|
||||||
|
id="q-handler",
|
||||||
|
status="answered",
|
||||||
|
outcome="positive",
|
||||||
|
origin=dict(path="routes.js", function="handler"),
|
||||||
|
flow=[dict(path="routes.js", function="handler"), dict(path="api.js", function="evalBody")],
|
||||||
|
finding=dict(
|
||||||
|
site="api.js:4", family="injection", rung="model_entailed", witness="source req.body.value -> eval at api.js:4"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
alerts=[],
|
||||||
|
delta="new",
|
||||||
|
coverage="complete",
|
||||||
|
cache_state="cold",
|
||||||
|
timings=dict(build=1.0, query=2.0, total=3.0),
|
||||||
|
)
|
||||||
|
value.update(updates)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def run(records):
|
||||||
|
profile, validation = inputs()
|
||||||
|
return score(profile, validation, dict(profile_sha256=profile["profile_sha256"], context=profile["config"], records=records))
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_and_failed_keep_positive_denominator():
|
||||||
|
missing = run([])
|
||||||
|
failed = run([record(queries=[dict(id="q-handler", status="failed", outcome="unknown")])])
|
||||||
|
assert missing["metrics"]["recall"]["denominator"] == failed["metrics"]["recall"]["denominator"] == 1
|
||||||
|
assert failed["metrics"]["recall"]["numerator"] == 0
|
||||||
|
assert failed["cases"][0]["outcome"] == "unresolved"
|
||||||
|
assert len(missing["cases"]) == 5
|
||||||
|
assert missing["recorded_cases"] == 0
|
||||||
|
assert missing["measurement_status"] == "unmeasured"
|
||||||
|
|
||||||
|
|
||||||
|
def test_witnessed_positive_without_rank_is_counted():
|
||||||
|
result = run([record()])
|
||||||
|
assert result["metrics"]["recall"]["numerator"] == 1
|
||||||
|
assert result["cases"][0]["reached_transitively"] is True
|
||||||
|
assert result["cases"][0]["witnesses"][0]["question_id"] == "q-handler"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mutation", ["empty_witness", "wrong_site", "wrong_family", "unanswered", "unselected"])
|
||||||
|
def test_positive_requires_answered_exact_target_witness(mutation):
|
||||||
|
row = record()
|
||||||
|
q = row["queries"][0]
|
||||||
|
if mutation == "empty_witness":
|
||||||
|
q["finding"]["witness"] = ""
|
||||||
|
elif mutation == "wrong_site":
|
||||||
|
q["finding"]["site"] = "other-api.js:4"
|
||||||
|
elif mutation == "wrong_family":
|
||||||
|
q["finding"]["family"] = "path"
|
||||||
|
elif mutation == "unanswered":
|
||||||
|
q["status"] = "failed"
|
||||||
|
else:
|
||||||
|
row["selected"] = []
|
||||||
|
result = run([row])
|
||||||
|
assert result["metrics"]["recall"]["numerator"] == 0
|
||||||
|
assert result["cases"][0]["reached_transitively"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_fixed_negative_needs_completed_answer_benign_backlog_is_not_alert():
|
||||||
|
fixed = record(
|
||||||
|
"fixed",
|
||||||
|
queries=[
|
||||||
|
dict(
|
||||||
|
id="q-handler",
|
||||||
|
status="answered",
|
||||||
|
outcome="negative",
|
||||||
|
target=dict(path="api.js", function="evalBody", lines=[4], family="injection"),
|
||||||
|
witness="bound parameter discharges target",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
delta="removed",
|
||||||
|
)
|
||||||
|
benign = record("benign", delta="unchanged")
|
||||||
|
result = run([fixed, benign])
|
||||||
|
assert result["metrics"]["fixed_side_silence"]["numerator"] == 1
|
||||||
|
assert result["metrics"]["benign_interruption"]["numerator"] == 0
|
||||||
|
assert result["cases"][2]["outcome"] == "positive"
|
||||||
|
assert result["metrics"]["precision"]["denominator"] == 0
|
||||||
|
failed = run([record("fixed", queries=[], coverage="incomplete")])
|
||||||
|
assert failed["metrics"]["fixed_side_silence"]["numerator"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_profiles_cannot_be_swapped_or_mutated():
|
||||||
|
profile, validation = inputs()
|
||||||
|
for mutation in ("hash", "mode", "manifest", "profile"):
|
||||||
|
p, v = copy.deepcopy(profile), copy.deepcopy(validation)
|
||||||
|
data = dict(profile_sha256=p["profile_sha256"], context=profile["config"], records=[record()])
|
||||||
|
if mutation == "hash":
|
||||||
|
data["profile_sha256"] = "other"
|
||||||
|
elif mutation == "mode":
|
||||||
|
data["records"][0]["ranking_mode"] = "evidence"
|
||||||
|
elif mutation == "manifest":
|
||||||
|
v["manifest_sha256"] = "other"
|
||||||
|
else:
|
||||||
|
p["config"]["facts"] = "new"
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
score(p, v, data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_static_rank_never_claims_transitive_detection(tmp_path, monkeypatch):
|
||||||
|
path = Path(__file__).parents[1] / "benchmarks/ranking/position.py"
|
||||||
|
spec = importlib.util.spec_from_file_location("position", path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
monkeypatch.setattr(module, "ordered_regions", lambda root: [])
|
||||||
|
(tmp_path / "api.js").write_text("eval(req.body.value)")
|
||||||
|
result = module.measure(
|
||||||
|
"test", tmp_path, [dict(id="missing", file="api.js", function="handler", family="injection", line=4, in_scope=True)]
|
||||||
|
)
|
||||||
|
assert result["known"][0]["reached_transitively"] is False
|
||||||
|
assert result["supported_population"] == 1
|
||||||
|
assert result["budget_at_recall"] is None
|
||||||
|
assert result["detection_recall"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_rank_absence_without_path_is_not_transitivity():
|
||||||
|
row = record()
|
||||||
|
del row["queries"][0]["flow"]
|
||||||
|
result = run([row])
|
||||||
|
assert result["metrics"]["recall"]["numerator"] == 1
|
||||||
|
assert result["cases"][0]["reached_transitively"] is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("field", ["engine", "facts", "queries", "scope", "budget_seconds"])
|
||||||
|
def test_actual_execution_context_must_match(field):
|
||||||
|
profile, validation = inputs()
|
||||||
|
context = dict(profile["config"], **{field: "different"})
|
||||||
|
with pytest.raises(ValueError, match="context mismatch"):
|
||||||
|
score(profile, validation, dict(profile_sha256=profile["profile_sha256"], context=context, records=[]))
|
||||||
|
|
||||||
|
|
||||||
|
def test_unreviewed_wrong_target_alerts_remain_precision_denominator():
|
||||||
|
row = record(
|
||||||
|
alerts=[
|
||||||
|
dict(review="unreviewed"),
|
||||||
|
dict(review="actionable_true", review_evidence="review1", question_id="q-handler", site="other.js:4"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
result = run([row])
|
||||||
|
assert result["metrics"]["precision"]["numerator"] == 0
|
||||||
|
assert result["metrics"]["precision"]["denominator"] == 2
|
||||||
|
assert result["gates_passed"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_regression_readable_despite_missing_admission_prerequisite():
|
||||||
|
profile, validation = inputs()
|
||||||
|
validation["cases"][3]["status"] = "missing_prerequisite"
|
||||||
|
validation["cases"][3]["problem"] = "independent holdout missing"
|
||||||
|
result = score(
|
||||||
|
profile,
|
||||||
|
validation,
|
||||||
|
dict(profile_sha256=profile["profile_sha256"], context=profile["config"], records=[record("regression", delta="unchanged")]),
|
||||||
|
)
|
||||||
|
assert result["metrics"]["regression_detection"]["numerator"] == 1
|
||||||
|
assert result["metrics"]["recall"]["numerator"] == 0
|
||||||
|
assert result["admission"] == "experimental"
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_source_overrides_an_asserted_positive():
|
||||||
|
profile, validation = inputs()
|
||||||
|
validation["snapshots"][1]["bytes_read"] = 0
|
||||||
|
result = score(profile, validation, dict(profile_sha256=profile["profile_sha256"], context=profile["config"], records=[record()]))
|
||||||
|
assert result["metrics"]["recall"]["numerator"] == 0
|
||||||
|
assert result["cases"][0]["outcome"] == "unresolved"
|
||||||
|
|
||||||
|
|
||||||
|
def test_unmeasured_benign_is_not_perfect_interruption_rate():
|
||||||
|
assert run([])["metrics"]["benign_interruption"]["value"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_evidence_launch_failure_retains_unresolved_population(tmp_path, monkeypatch):
|
||||||
|
path = Path(__file__).parents[1] / "benchmarks/ranking/position.py"
|
||||||
|
spec = importlib.util.spec_from_file_location("position_failure", path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
monkeypatch.setattr(module, "ordered_regions", lambda root: [])
|
||||||
|
|
||||||
|
def fail(*args):
|
||||||
|
raise SystemExit("no graph: input unreadable")
|
||||||
|
|
||||||
|
monkeypatch.setattr(module, "evidence_ordered_regions", fail)
|
||||||
|
result = module.measure(
|
||||||
|
"test",
|
||||||
|
tmp_path,
|
||||||
|
[dict(id="missing", file="api.js", function="handler", family="injection", line=4, in_scope=True)],
|
||||||
|
by_evidence=True,
|
||||||
|
)
|
||||||
|
assert result["ranking_mode"] == "unavailable"
|
||||||
|
assert result["supported_population"] == 1
|
||||||
|
assert result["input_status"] == "incomplete"
|
||||||
|
assert "no graph" in result["evidence_failure"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_deferred_questions_and_unmeasured_cases_remain_coverage_limits():
|
||||||
|
result = run([record(deferred=["q-deferred"])])
|
||||||
|
assert result["metrics"]["completion"]["numerator"] == 0
|
||||||
|
assert result["metrics"]["query_completion"]["denominator"] == 2
|
||||||
|
assert result["metrics"]["query_completion"]["numerator"] == 1
|
||||||
|
assert result["metrics"]["query_completion"]["value"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_actionable_recall_requires_reviewed_alert_beside_detection():
|
||||||
|
result = run([record()])
|
||||||
|
assert result["metrics"]["recall"]["numerator"] == 1
|
||||||
|
assert result["metrics"]["actionable_recall"]["numerator"] == 0
|
||||||
|
result = run([record(alerts=[dict(review="actionable_true", review_evidence="review1", question_id="q-handler", site="api.js:4")])])
|
||||||
|
assert result["metrics"]["actionable_recall"]["numerator"] == 1
|
||||||
|
assert result["metrics"]["precision"]["numerator"] == 1
|
||||||
Reference in New Issue
Block a user