From 7c14aa10860cad431767ff7015e626619792da50 Mon Sep 17 00:00:00 2001 From: Tauan BF <11513929+tauanbinato@users.noreply.github.com> Date: Sat, 26 Sep 2026 18:38:06 -0300 Subject: [PATCH 1/5] Ask an environment finding where its value would differ A hardcoded-value review or consider that rests only on a value changing between environments is asked, once its value or constant is named and with the code that uses it, where the value would differ: each installation or the author's own account keeps it; the same value in every copy on purpose, a fallback used only when configuration gives none, or code no deployment runs, at 0.80, make it a note. The Choice that names a file's constant lists the lines that use each one. Labeled such findings went from 17 right and 36 wrong to 15 right and 19 wrong. --- CHANGELOG.md | 1 + docs/classification-cascade.md | 8 +- site/src/how-it-works.md | 8 +- src/catalog.rs | 3 +- src/units/compose.rs | 89 +++++++++++++++----- src/units/hardcoded.rs | 112 ++++++++++++++++++++++--- src/units/questions/maintainability.rs | 33 +++++++- src/units/tests/hardcoded.rs | 92 +++++++++++++++----- 8 files changed, 292 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 098dc09..c4a0dbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Measured on 103 pinned projects (24 new open-source ones of kinds not tried befo - Tests: a test said to assert internal details is asked, with the bodies of the functions it calls, what its assertions read: results, state the program shows or acts on next, or effects a caller observes clear the consider; stored input or calls between the program's own functions keep it. Asked of the test and the signatures it calls, the check read a debug panel's recorded queries (`panel._queries`, which the panel renders), Devise's documented hooks and an app's state after an action as internals: 49 of 66 such considers labeled on the corpus were wrong. A test that reads members through reflection or a cast to `any` keeps its consider without being asked. Labeled tests/value considers went from 29% to 77% right (20 right and 6 wrong, against 22 and 55), and on held-out projects from 2 right and 13 wrong to 1 wrong. About $0.006 of follow-ups on the corpus. - Injection: a consider that rests on the function's parameters ("a caller passing outside input would make it exploitable") is asked, with the functions that call it, what the values it places can hold. It becomes a note when text the program fixes (literals its callers pass, numbers, names from a fixed list), values it creates or a local tool's own arguments lead; text another party can set, or callers that are not shown, keep it. Asked where the values come from, the recheck answered "the function's parameters" at 0.9 even for a Rust helper whose four callers pass literal SQL fragments. On the labeled projects, such considers went from 30 right and 51 wrong to 30 right and 32 wrong. Only these follow-ups are asked. - Hardcoded values: the Choice that names a finding's value lists, with each value, the other lines of its file that write it. A consider that rests only on a value's name, whose file writes that value again, is then asked what the value is: copies that must change together, or a value nothing near it explains, keep it; a value that the field or argument it fills or a comment beside it explains, an idiom such as a tolerance near zero or a unit conversion, or a hand-tuned number, clearly chosen, make it a note. On the labeled projects, such considers went from 53 right and 77 wrong (41%) to 32 right and 28 wrong (53%); the others are notes. Offered instead as the locate's "no value stands out", those kinds also took values with copies that must stay equal. Only these follow-ups are asked. +- Hardcoded values: a review or consider that rests only on a value changing between environments is asked, once its value or constant is named and with the function or the lines that use the constant, where that value would differ. A value each installation must set, or the author's own account or domain, keeps it; the same value in every copy on purpose (the program's own service or registered client id, a provider's fixed address, a path the platform fixes), a fallback used only when configuration gives none, or code no deployment runs, at 0.80, make it a note. The Choice that names a file's constant lists the lines that use each one. On the labeled projects, such findings went from 17 right and 36 wrong to 15 right and 19 wrong: a desktop app's own license server, a provider's registered redirect and fallbacks behind environment variables are notes; a frontend's API host edited in code three times and a template author's domain as a fallback are the right ones lowered. Only these follow-ups are asked. - Unsafe settings: outside C#, PHP and Django, the TLS check asks whether code accepts a server's certificate, host key or host name without verifying it, and names a connection made without TLS (gRPC's insecure credentials, an insecure OpenTelemetry exporter, `sslmode=disable`) and verification skipped only when a caller or the operator asks for it as not turning it off. Asked whether code turns verification off, Online Boutique's gRPC clients and telemetry exporters inside its cluster, and its database reached through the AlloyDB connector, were seven reviews whatever the examples said, and httpx's `verify=False` branch and vaultwarden's opt-in setting for invalid SMTP certificates were reviews too. The escape check names i18next's `escapeValue: false` in a React app, whose components escape what they render, as escaping kept. Labeled unsafe-settings reviews went from 54 right and 24 wrong to 54 right and 14 wrong; pgweb defaulting a bookmark's `sslmode` to `disable`, which turns off TLS rather than verification, is no longer one. Only unsafe-settings traces are asked again. - Tests: Deno tests are test cases, in each of their forms: `Deno.test("name", fn)`, `Deno.test({ name: "name", fn() {…} })` and `Deno.test(function name() {…})`, with `.only` and `.ignore`. oak writes its 266 tests in the object form, and none of them was judged: its test files got a file-purpose request each and the test rules found nothing to ask. Only Deno projects' requests change. diff --git a/docs/classification-cascade.md b/docs/classification-cascade.md index c3c9239..2d73381 100644 --- a/docs/classification-cascade.md +++ b/docs/classification-cascade.md @@ -197,7 +197,13 @@ signatures, or one candidate pair. or a value nothing near it explains, keep it; a value that the field or argument it fills or a comment beside it explains, an idiom such as a tolerance near zero or a unit conversion, or a hand-tuned number make it a - note. + note. A finding that rests only on a value changing between environments + is asked, with the value's function or the lines that use the constant, + where it would differ: each installation or the author's own account + keeps it; the same in every copy on purpose (the program's own service, a + provider's fixed address, a path the platform fixes), a fallback used only + when configuration gives none, or code no deployment runs, at 0.80, make it + a note. Special-case findings in different files that name the same identity become one finding at the strongest site; the others are notes pointing at it. Numbers and paths are not grouped: `1000` meant metres per kilometre in diff --git a/site/src/how-it-works.md b/site/src/how-it-works.md index 526421b..8fff789 100644 --- a/site/src/how-it-works.md +++ b/site/src/how-it-works.md @@ -215,7 +215,13 @@ signatures, or one candidate pair. or a value nothing near it explains, keep it; a value that the field or argument it fills or a comment beside it explains, an idiom such as a tolerance near zero or a unit conversion, or a hand-tuned number make it a - note. + note. A finding that rests only on a value changing between environments + is asked, with the value's function or the lines that use the constant, + where it would differ: each installation or the author's own account + keeps it; the same in every copy on purpose (the program's own service, a + provider's fixed address, a path the platform fixes), a fallback used only + when configuration gives none, or code no deployment runs, at 0.80, make it + a note. Special-case findings in different files that name the same identity become one finding at the strongest site; the others are notes pointing at it. Numbers and paths are not grouped: `1000` meant metres per kilometre in diff --git a/src/catalog.rs b/src/catalog.rs index 55d2adb..e86924c 100644 --- a/src/catalog.rs +++ b/src/catalog.rs @@ -279,7 +279,8 @@ pub fn rule_version(key: &str) -> &'static str { TEST_REDUNDANCY => "4", INJECTION => "9", SENSITIVE_DATA => "7", - HARDCODED_VALUES | UNSAFE_SETTINGS => "6", + HARDCODED_VALUES => "7", + UNSAFE_SETTINGS => "6", AGENT_CONTEXT => "3", COMMENTS => "3", LARGE_DOCS => "3", diff --git a/src/units/compose.rs b/src/units/compose.rs index 0c2209e..262033a 100644 --- a/src/units/compose.rs +++ b/src/units/compose.rs @@ -1091,6 +1091,7 @@ fn capped( } if single_use_value(unit, judgments) || readable_value(unit, judgments) + || same_everywhere(unit, judgments) || short_outline(unit) || small_section(unit) { @@ -1106,9 +1107,11 @@ fn capped( if lower { lowered(outcome) } else { outcome } } -/// The follow-up of each hardcoded-value consider that rests on a value's -/// name, whose value the locate named and its file writes again, not yet -/// asked what that value is. +/// The kind follow-up of each hardcoded-value finding not yet asked one: +/// what the value is, for a consider that rests on a value's name whose +/// file writes the value again; where the value or constant would differ, +/// for a finding that rests on the environment. Both need the value or +/// constant the locate named. pub fn unkinded_values( plan: &FilePlan, judgments: &[Judgment], @@ -1117,24 +1120,34 @@ pub fn unkinded_values( .iter() .filter(|u| u.presence == Presence::Judged) .filter_map(|u| { - let Detail::Values { - locate: Some(locate), - repeated, - .. - } = &u.detail - else { - return None; - }; - if answers(judgments, &u.id, Pass::Locate).contains_key("value_kind") - || !named_value_only(u, judgments) - { + let asked = answers(judgments, &u.id, Pass::Locate); + if asked.contains_key("value_kind") || asked.contains_key("environment_kind") { return None; } - let option = located_option(u, judgments, ("value", 'v'))?; - if repeated.get(option) != Some(&true) { - return None; + match &u.detail { + Detail::Values { + locate: Some(locate), + repeated, + .. + } => { + let option = located_option(u, judgments, ("value", 'v'))?; + if named_value_only(u, judgments) && repeated.get(option) == Some(&true) { + super::hardcoded::value_kind(locate, option, &u.id) + } else if environment_only(u, judgments) { + super::hardcoded::environment_kind(locate, option, &u.id) + } else { + None + } + } + Detail::Constants { + locate: Some(locate), + .. + } if environment_only(u, judgments) => { + let option = located_constant(u, judgments)?; + super::hardcoded::environment_kind(locate, option, &u.id) + } + _ => None, } - super::hardcoded::value_kind(locate, option, &u.id) }) .collect() } @@ -1159,6 +1172,25 @@ fn readable_value(unit: &UnitPlan, judgments: &[Judgment]) -> bool { }) } +/// A finding that rests on the environment whose value or constant, asked +/// where it would differ, needs no configuration at the review threshold: +/// the same in every copy of the program on purpose, a fallback used only +/// when configuration gives none, or code no deployment runs. Its finding +/// is a note. Labeled by hand, that took 17 of 36 wrong reviews and +/// considers and 2 of 17 right ones (a frontend's API host, edited in code +/// three times, and a template author's domain as a fallback); leaning at +/// 0.50 would have taken 25 wrong and 6 right. +fn same_everywhere(unit: &UnitPlan, judgments: &[Judgment]) -> bool { + environment_only(unit, judgments) + && choice_mass( + answers(judgments, &unit.id, Pass::Locate) + .get("environment_kind") + .copied(), + &super::questions::SAME_EVERYWHERE, + ) + .is_some_and(|p| crate::policy::probability_at_least(p, crate::policy::REVIEW_PROBABILITY)) +} + /// A function's hardcoded-value review or consider whose value was not /// named: the locate Choice picked none clearly, or there were too many /// values to offer. Its finding is a note, since a reader cannot tell what @@ -1178,7 +1210,22 @@ fn unnamed_value(unit: &UnitPlan, judgments: &[Judgment]) -> bool { /// wrong ones tuning in game, audio and animation code (a scheduler's /// 500 ms, a hash seed, a mix gain, a float epsilon). fn named_value_only(unit: &UnitPlan, judgments: &[Judgment]) -> bool { - if !matches!(unit.detail, Detail::Values { .. }) { + matches!(unit.detail, Detail::Values { .. }) && rests_only_on(unit, judgments, "magic") +} + +/// A hardcoded-value review or consider that rests only on whether a value +/// changes between environments: a file's constants always do. +fn environment_only(unit: &UnitPlan, judgments: &[Judgment]) -> bool { + rests_only_on(unit, judgments, "environment") +} + +/// A hardcoded-value review or consider whose raised questions are all +/// `question`. +fn rests_only_on(unit: &UnitPlan, judgments: &[Judgment], question: &str) -> bool { + if !matches!( + unit.detail, + Detail::Values { .. } | Detail::Constants { .. } + ) { return false; } let (outcome, answers) = resolved(unit, judgments); @@ -1190,7 +1237,7 @@ fn named_value_only(unit: &UnitPlan, judgments: &[Judgment]) -> bool { .unwrap_or_default() .iter() .filter(|(_, o, _)| matches!(o, Outcome::Review(_) | Outcome::Consider(_))) - .all(|(question, ..)| *question == "magic") + .all(|(raised, ..)| *raised == question) } /// Such a finding about a value its file writes once is a note: labeled by @@ -1291,6 +1338,8 @@ fn lowered_value(unit: &UnitPlan, judgments: &[Judgment]) -> Option<(Strength, & "It is written once in its file, so it is a note." } else if readable_value(unit, judgments) { "It reads for itself where it is used, so it is a note." + } else if same_everywhere(unit, judgments) { + "It likely stays the same wherever the program runs, or is only a fallback, so it is a note." } else if named_value_only(unit, judgments) && matches!(resolved(unit, judgments).0, Outcome::Review(_)) { diff --git a/src/units/hardcoded.rs b/src/units/hardcoded.rs index 9bf71c2..ae3dfc0 100644 --- a/src/units/hardcoded.rs +++ b/src/units/hardcoded.rs @@ -107,14 +107,35 @@ fn written_at<'a>(source: &'a str, literal: &'a str) -> impl Iterator Vec { + numbered_lines(source, written_at(source, literal), lines) +} + +/// The lines of `source` outside a constant's own `lines` that name it. +fn used_at(source: &str, name: &str, lines: (usize, usize)) -> Vec { + let word = |c: char| c.is_alphanumeric() || c == '_' || c == '$'; + let named = source.match_indices(name).map(|(at, _)| at).filter(|&at| { + !(source[..at].chars().next_back().is_some_and(word) + || source[at + name.len()..].chars().next().is_some_and(word)) + }); + numbered_lines(source, named, lines) +} + +/// The lines holding the ascending byte offsets `at`, outside `lines`, +/// numbered and at most `ELSEWHERE_LINES`. +fn numbered_lines( + source: &str, + at: impl Iterator, + lines: (usize, usize), +) -> Vec { let mut found: Vec<(usize, String)> = Vec::new(); let (mut line, mut counted) = (1, 0); - for at in written_at(source, literal) { + for at in at { line += source.as_bytes()[counted..at] .iter() .filter(|b| **b == b'\n') @@ -192,21 +213,81 @@ pub(super) fn value_kind(locate: &FollowUp, option: usize, id: &str) -> Option<( "value_kind", Pass::Locate, ); + Some(located_request(&located, state, questions)) +} + +/// Where an environment finding's value or constant would differ, asked +/// about the one its locate named: the function and the value with the +/// other lines that write it, or the constant with the lines that use it, +/// taken from the locate request. +pub(super) fn environment_kind( + locate: &FollowUp, + option: usize, + id: &str, +) -> Option<(Value, Asked)> { + let located = locate.request(); + let file = &located["state"]["file"]; + let (state, subject, note) = if let Some(constants) = located["state"]["constants"].as_array() { + let mut constant = constants.get(option)?.clone(); + let used = constant.as_object_mut()?.remove("used_at"); + constant.as_object_mut()?.remove("id"); + let mut state = json!({"file": file, "constant": constant}); + if let Some(lines) = used { + state["used_at"] = lines; + } + ( + state, + "constant", + "`used_at` lists lines of the file that use the constant.", + ) + } else { + let function = &located["state"]["function"]; + let entry = function["values"].get(option)?; + let mut state = json!({ + "file": file, + "function": {"name": function["name"], "source": function["source"]}, + "value": entry["value"], + }); + if let Some(lines) = entry.get("elsewhere") { + state["elsewhere"] = lines.clone(); + } + ( + state, + "value", + "`elsewhere` lists other lines of the file that write the same value.", + ) + }; + let mut questions = Questions::default(); + questions.ask( + "environment_kind".into(), + questions::hardcoded_environment_kind(subject, note), + id, + HARDCODED_VALUES, + "environment_kind", + Pass::Locate, + ); + Some(located_request(&located, state, questions)) +} + +/// A follow-up of the file a locate request was made for: its model, its +/// sources and `state`, asking `questions`. +fn located_request(located: &Value, state: Value, questions: Questions) -> (Value, Asked) { let language = located["state"]["file"]["language"] .as_str() .unwrap_or_default(); let sources: Vec<(&Path, &str)> = located["jevgate"]["sources"] - .as_array()? - .iter() + .as_array() + .into_iter() + .flatten() .filter_map(|s| Some((Path::new(s["path"].as_str()?), s["source_hash"].as_str()?))) .collect(); - Some(super::evidence::request( - located["model"].as_str()?, + super::evidence::request( + located["model"].as_str().unwrap_or_default(), "locate", &sources, state, questions.reworded(language), - )) + ) } /// Option ids `{prefix}0`, `{prefix}1`, … for a locate Choice over `count` entries. @@ -331,10 +412,19 @@ fn plan_constants( let with_ids: Vec = ids .iter() .zip(&listed) - .map(|(id, constant)| { - let mut constant = constant.clone(); - constant["id"] = json!(id); - constant + .zip(constants) + .map(|((id, listed), constant)| { + let mut listed = listed.clone(); + listed["id"] = json!(id); + let lines = used_at( + file.source, + &constant.name, + (constant.line, constant.end_line), + ); + if !lines.is_empty() { + listed["used_at"] = json!(lines); + } + listed }) .collect(); locate_request( diff --git a/src/units/questions/maintainability.rs b/src/units/questions/maintainability.rs index 5cc5294..69b3744 100644 --- a/src/units/questions/maintainability.rs +++ b/src/units/questions/maintainability.rs @@ -181,7 +181,9 @@ pub fn outline_kind(tests: bool) -> Value { pub fn hardcoded_constant(ids: &[String]) -> Value { choose_id( "Which constant in `constants` most needs to come from configuration because it would change in another environment?", - format!("Options are the `id` values in `constants`. {EVIDENCE}"), + format!( + "Options are the `id` values in `constants`; a constant's `used_at` lists lines of the file that use it. {EVIDENCE}" + ), ids, "No single constant stands out.", ) @@ -228,6 +230,35 @@ pub fn hardcoded_value_kind() -> Value { /// The options of `hardcoded_value_kind` under which a value reads for itself. pub const READABLE_VALUES: [&str; 3] = ["named", "idiom", "tuning"]; +/// Asked only about the value or constant an environment finding names, +/// with the code around it: where it would differ. Labeled by hand, 39 of +/// 57 such reviews and considers were wrong: a desktop app's own license +/// server and OIDC client id, a redirect registered with a provider, a +/// fallback used only when configuration gives none, a path the platform +/// fixes, and fixtures, generator templates and scripts no deployment runs; +/// right ones named a server, path or secret each installation must set, or +/// the author's own account or domain. `subject` names the state key. +pub fn hardcoded_environment_kind(subject: &str, note: &str) -> Value { + json!({ + "type": "choice", + "instructions": { + "question": format!("Where would `{subject}` differ between the places this program runs?"), + "note": format!("{note} {EVIDENCE}"), + }, + "criteria": { + "each": "Each installation, server, account or machine that runs the program needs its own value, and the code does not read it from configuration or the environment.", + "author": "It names the author's own account, domain, machine, path or credentials, which anyone else running the program would have to change.", + "fallback": "It is only the default used when configuration or the environment gives none.", + "same": "Every copy of the program uses it on purpose: the program's own public service or registered client id, a provider's fixed address, a path the platform fixes, or text shown to users.", + "not_run": "The code does not run where the program is deployed: a test fixture or mock, an example, a template a generator copies, or a script run by hand.", + }, + }) +} + +/// The options of `hardcoded_environment_kind` under which the value needs +/// no configuration. +pub const SAME_EVERYWHERE: [&str; 3] = ["fallback", "same", "not_run"]; + /// Whether a value fixed in code changes between environments. `values` names /// the list of candidate values; `code` describes the code that uses them. /// Criteria name what is not environment-specific (the program's own routes, diff --git a/src/units/tests/hardcoded.rs b/src/units/tests/hardcoded.rs index cfb0a32..c9d404a 100644 --- a/src/units/tests/hardcoded.rs +++ b/src/units/tests/hardcoded.rs @@ -1,6 +1,31 @@ //! Hardcoded values: value units, benign kinds and repeated literals. use super::*; +/// A run answering `overrides`, the finding on `symbol` and how many locate +/// requests it asked; later runs with `options` ask again. +fn judged( + project: &Project, + options: &mut CheckArgs, + overrides: Vec<(&'static str, Value)>, + symbol: &str, +) -> (crate::schema::Finding, u64) { + let mut eval = scripted(0); + eval.overrides = overrides; + let report = run(project, options, &mut eval); + options.refresh = true; + let asked = report + .stages + .get("locate") + .map_or(0, |stage| stage.successful_requests); + let finding = report.files[0] + .findings + .iter() + .find(|f| f.symbol.as_deref() == Some(symbol)) + .cloned() + .unwrap(); + (finding, asked) +} + #[test] fn functions_with_literals_and_module_constants_are_hardcoded_value_units() { let (project, options) = hardcoded_project(); @@ -46,6 +71,49 @@ fn a_finding_on_module_constants_points_at_the_constant_it_is_about() { ); } +#[test] +fn an_environment_finding_whose_constant_stays_the_same_everywhere_is_a_note() { + let source = "const API_URL: &str = \"https://api.prod.example.com\";\nconst RETRIES: u32 = 3;\n\nfn client() -> Client {\n Client::new(API_URL)\n}\n"; + let (project, mut options) = rule_project(source, catalog::HARDCODED_VALUES); + let (_, plan) = planned(&project, &options); + let locate = plan.files[&0] + .units + .iter() + .find_map(|u| match &u.detail { + Detail::Constants { + locate: Some(locate), + .. + } => Some(locate.request()), + _ => None, + }) + .expect("a locate for the constants"); + assert_eq!( + locate["state"]["constants"][0]["used_at"], + json!(["5: Client::new(API_URL)"]) + ); + assert!(locate["state"]["constants"][1].get("used_at").is_none()); + let kinds = ["author", "each", "fallback", "not_run", "same"]; + let overrides = |kind: &str| { + vec![ + ("environment", spread(0.0, 0.05, 0.95)), + ("constant", choice_of("c0", &["c0", "c1", "none"])), + ("environment_kind", choice_of(kind, &kinds)), + ] + }; + // A server each installation must set keeps the review. + let (kept, _) = judged(&project, &mut options, overrides("each"), "API_URL"); + assert_eq!(kept.strength, Strength::Review, "{}", kept.message); + let (same, _) = judged(&project, &mut options, overrides("same"), "API_URL"); + assert_eq!(same.strength, Strength::Note); + assert!( + same.message.ends_with( + "The constant is `API_URL`. It likely stays the same wherever the program runs, or is only a fallback, so it is a note." + ), + "{}", + same.message + ); +} + #[test] fn a_local_default_is_a_note_and_a_special_case_is_a_review() { let (project, mut options) = hardcoded_project(); @@ -169,33 +237,19 @@ fn a_named_value_that_reads_for_itself_is_a_note() { ); let (project, mut options) = rule_project(&source, catalog::HARDCODED_VALUES); let kinds = ["copies", "idiom", "named", "tuning", "unexplained"]; - let mut connect = |kind: &str| { - let mut eval = scripted(0); - eval.overrides = vec![ + let overrides = |kind: &str| { + vec![ ("magic", spread(0.1, 0.35, 0.55)), ("value", choice_of("v1", &["v0", "v1", "none"])), ("value_kind", choice_of(kind, &kinds)), - ]; - let report = run(&project, &options, &mut eval); - options.refresh = true; - let asked = report - .stages - .get("locate") - .map_or(0, |stage| stage.successful_requests); - let finding = report.files[0] - .findings - .iter() - .find(|f| f.symbol.as_deref() == Some("connect")) - .cloned() - .unwrap(); - (finding, asked) + ] }; // Copies that must change together keep the consider. - let (kept, asked) = connect("copies"); + let (kept, asked) = judged(&project, &mut options, overrides("copies"), "connect"); assert_eq!(kept.strength, Strength::Consider, "{}", kept.message); // Both functions' values are located, then both are asked their kind. assert_eq!(asked, 4); - let (named, _) = connect("named"); + let (named, _) = judged(&project, &mut options, overrides("named"), "connect"); assert_eq!(named.strength, Strength::Note); assert!( named From fb6836cb6b11d405f8fc342fb6716d7d5e6c7cf7 Mon Sep 17 00:00:00 2001 From: Tauan BF <11513929+tauanbinato@users.noreply.github.com> Date: Sat, 26 Sep 2026 18:48:44 -0300 Subject: [PATCH 2/5] Ask where an undecided path comes from, outside PHP A path check left undecided after its trace and recheck is asked, with the functions that call it, where the paths come from: the program's own directories or configuration, the command line or settings of the person running a local program, a whole path its caller gives, or another party's input. The program's own or the local user's clear it: 74 units on the corpus, for about $0.02. --- src/catalog.rs | 2 +- src/units/questions/settle.rs | 58 +++++++++++++++++++++++++++-------- src/units/security.rs | 12 +++++++- src/units/tests/security.rs | 27 ++++++++++++---- 4 files changed, 78 insertions(+), 21 deletions(-) diff --git a/src/catalog.rs b/src/catalog.rs index e86924c..e07c6cd 100644 --- a/src/catalog.rs +++ b/src/catalog.rs @@ -277,7 +277,7 @@ pub fn rule_version(key: &str) -> &'static str { SHARED_LOGIC => "20", TEST_VALUE => "6", TEST_REDUNDANCY => "4", - INJECTION => "9", + INJECTION => "10", SENSITIVE_DATA => "7", HARDCODED_VALUES => "7", UNSAFE_SETTINGS => "6", diff --git a/src/units/questions/settle.rs b/src/units/questions/settle.rs index 9e4db02..db0c6c9 100644 --- a/src/units/questions/settle.rs +++ b/src/units/questions/settle.rs @@ -6,6 +6,46 @@ use super::{EVIDENCE, security::CALLERS}; use serde_json::{Value, json}; +/// Where a source Choice looks for the variables it asks about, and its +/// note: with `callers`, also in what the functions that call it pass. +fn sources_shown(callers: bool) -> (&'static str, String) { + if callers { + ( + ", in the function or in what `callers` pass it", + format!("{CALLERS} {EVIDENCE}"), + ) + } else { + ("", EVIDENCE.to_string()) + } +} + +/// Options of the path-source Choice that rule a path concern out: the +/// program's own paths, the local user's, or no file. +pub const OWN_PATHS: [&str; 3] = ["own", "local", "none"]; + +/// Where the file paths a function opens, writes or deletes come from, +/// asked outside PHP when the path check stays undecided (PHP asks its own +/// `path_parts`). Undecided path checks were 348 units across the corpus, +/// mostly command-line tools writing where their user points them and +/// helpers joining a fixed directory with a name. +pub fn security_path_source(code: &str, callers: bool) -> Value { + let (shown, note) = sources_shown(callers); + json!({ + "type": "choice", + "instructions": { + "question": format!("Where do the file paths that `{code}` opens, writes or deletes come from?"), + "note": note, + }, + "criteria": { + "own": format!("Directories and file names written in the code, the program's own directories, or its configuration or environment, with only names, ids or numbers from variables{shown}."), + "local": "The command line, settings or files of the person running a local program or script, such as an output path they pass.", + "given": "A whole path handed to the function as a parameter or field, whose origin this code does not show.", + "outside": "Partly from outside the program: a network request, message, uploaded file or archive entry, or a record users can edit.", + "none": "It opens, writes or deletes no file.", + }, + }) +} + /// Options of the URL-parts Choice that rule a URL concern out: a host of the /// program's own, or no request. pub const OWN_PARTS: [&str; 2] = ["own", "none"]; @@ -14,20 +54,12 @@ pub const OWN_PARTS: [&str; 2] = ["own", "none"]; /// stays undecided: on clients of a fixed or configured service the check /// split on a variable path or query, while naming the host decided them. A /// host that is sent another URL to fetch is its own option, since internal -/// proxies fetched what users sent. The same question about paths cleared -/// real traversals, reading names stored in an index as the program's own, -/// so paths are not settled this way. +/// proxies fetched what users sent. The same question about paths once +/// cleared real traversals, reading names stored in an index as the +/// program's own; `security_path_source` names such records as another +/// party's input. pub fn security_url_parts(code: &str, callers: bool) -> Value { - let shown = if callers { - ", in the function or in what `callers` pass it" - } else { - "" - }; - let note = if callers { - format!("{CALLERS} {EVIDENCE}") - } else { - EVIDENCE.to_string() - }; + let (shown, note) = sources_shown(callers); json!({ "type": "choice", "instructions": { diff --git a/src/units/security.rs b/src/units/security.rs index 80c95b1..f135e4a 100644 --- a/src/units/security.rs +++ b/src/units/security.rs @@ -855,7 +855,7 @@ pub(in crate::units) enum SettleWhen { /// does with tokens and how it handles passwords settle those checks /// whenever they are not clear: front ends that send their own token and /// HMAC signing split on them or were reviews. -pub(in crate::units) const SETTLES: [SettleKind; 13] = [ +pub(in crate::units) const SETTLES: [SettleKind; 14] = [ SettleKind { rule: INJECTION, question: "url_parts", @@ -919,6 +919,15 @@ pub(in crate::units) const SETTLES: [SettleKind; 13] = [ when: SettleWhen::NotClear, files: SettleFiles::Only(questions::PHP), }, + SettleKind { + rule: INJECTION, + question: "path_source", + checks: &["path"], + clears: &questions::OWN_PATHS, + callers: true, + when: SettleWhen::Undecided, + files: SettleFiles::Except(questions::PHP), + }, SettleKind { rule: SENSITIVE_DATA, question: "destination", @@ -1006,6 +1015,7 @@ fn settle( let callers = kind.callers && !subject.callers.is_empty(); let body = match kind.question { "url_parts" => questions::security_url_parts(&code, callers), + "path_source" => questions::security_path_source(&code, callers), "runs_in" => questions::security_runs_in(&code), "redirect_target" => questions::security_redirect_target(&code, callers), "markup_output" => { diff --git a/src/units/tests/security.rs b/src/units/tests/security.rs index 791b03b..3e57ace 100644 --- a/src/units/tests/security.rs +++ b/src/units/tests/security.rs @@ -208,6 +208,8 @@ const FETCH_QUOTE: &str = "fn quote(client: &Client, base: &Url, symbol: &str) - const URL_PARTS: [&str; 5] = ["own", "forwards", "given", "outside", "none"]; +const PATH_SOURCE: [&str; 5] = ["own", "local", "given", "outside", "none"]; + const RUNS_IN: [&str; 3] = ["browser", "server", "either"]; /// Injection status and settle requests with the URL (or path) check at @@ -224,6 +226,7 @@ fn settled_injection( (check, noul_at(0.4)), ("origin", spread(0.0, 0.9, 0.1)), ("url_parts", choice_of(parts, &URL_PARTS)), + ("path_source", choice_of(parts, &PATH_SOURCE)), ("runs_in", choice_of("server", &RUNS_IN)), ]; let report = run(project, options, &mut eval); @@ -254,12 +257,24 @@ fn an_undecided_url_is_settled_only_by_a_host_of_the_programs_own() { "{parts}" ); } - options.refresh = true; - assert_eq!( - settled_injection(&project, &options, "path", "own"), - (Status::Uncertain, 0), - "an undecided path is not settled" - ); +} + +#[test] +fn an_undecided_path_is_settled_by_the_programs_own_or_its_local_users_paths() { + let (project, mut options) = security_project(FETCH_QUOTE); + for (parts, status) in [ + ("own", Status::Clear), + ("local", Status::Clear), + ("given", Status::Uncertain), + ("outside", Status::Uncertain), + ] { + assert_eq!( + settled_injection(&project, &options, "path", parts), + (status, 1), + "{parts}" + ); + options.refresh = true; + } } #[test] From b82a5b6cddd35ae9f0ccdd568432e20bb16a647d Mon Sep 17 00:00:00 2001 From: Tauan BF <11513929+tauanbinato@users.noreply.github.com> Date: Sat, 26 Sep 2026 18:54:41 -0300 Subject: [PATCH 3/5] Make an undecided path, URL or redirect from parameters a note Parameters in a path, URL or redirect were a note once a check found them, but stayed uncertain while the check was undecided. Once the Choice that settles the check was asked and does not lean toward another party's input, such a unit is a note naming the path, URL or redirect: 450 injection units had stayed uncertain that way. --- src/units/outcome/injection.rs | 38 ++++++++++++++++++++++++++++++---- src/units/outcome/mod.rs | 2 +- src/units/tests/nextjs.rs | 6 ++++-- src/units/tests/security.rs | 27 +++++++++++++++++++++--- src/units/wording/mod.rs | 6 +++--- src/units/wording/security.rs | 11 +++++++++- 6 files changed, 76 insertions(+), 14 deletions(-) diff --git a/src/units/outcome/injection.rs b/src/units/outcome/injection.rs index 0492a23..c459c86 100644 --- a/src/units/outcome/injection.rs +++ b/src/units/outcome/injection.rs @@ -26,7 +26,7 @@ pub(in crate::units) fn origin_outcome(answer: &Answer) -> Outcome { /// Kinds where a variable is a concern only when another party controls it: /// helpers that build a path, URL or redirect target from their parameters /// are everywhere. -const RESOURCE_CHECKS: [&str; 3] = ["path", "url", "redirect"]; +pub(in crate::units) const RESOURCE_CHECKS: [&str; 3] = ["path", "url", "redirect"]; /// Presence alone never raises an injection: it only decides whether the /// trace is asked. When every specific check clears the unit, it is clear; @@ -96,6 +96,24 @@ fn found_injections<'a>(get: &impl Fn(&str) -> Option<&'a Answer>) -> Vec<&'stat .collect() } +/// Whether the Choice that settles an undecided resource check was asked +/// (where its paths, URLs or redirect targets come from) without leaning +/// toward another party's input, which keeps the check open. +fn settle_asked<'a>(check: &str, get: &impl Fn(&str) -> Option<&'a Answer>) -> bool { + let question = match check { + "path" if get("path_parts").is_some() => "path_parts", + "path" => "path_source", + "url" => "url_parts", + "redirect" => "redirect_target", + _ => return false, + }; + choice_mass(get(question), &OUTSIDE_SOURCES) + .is_some_and(|p| !probability_at_least(p, LEADING_PROBABILITY)) +} + +/// Options of the settle Choices that name another party's input. +const OUTSIDE_SOURCES: [&str; 3] = ["outside", "request", "stored"]; + /// The origin's outcome given the checks that found something: with none, /// another party's values are a note and parameters a note only when a /// check leans toward a concern; parameters only in paths or URLs are lower. @@ -108,14 +126,26 @@ fn by_origin<'a>( match outcome { Outcome::Review(p) if found.is_empty() => Outcome::Note(p), Outcome::Consider(p) if found.is_empty() => { - let leaning = settled_checks(catalog::INJECTION, get) + let open: Vec<(&str, Outcome)> = settled_checks(catalog::INJECTION, get) .into_iter() .filter(|(_, o)| *o != Outcome::Clear) + .collect(); + let leaning = open + .iter() .filter_map(|(id, _)| get(id)) .map(lean) .fold(0.0, f64::max); - if probability_at_least(leaning, LEADING_PROBABILITY) { - Outcome::Note(leaning) + // Parameters in a path, URL or redirect are a note until a caller + // shows another party controls them, found or not: undecided, + // 320 such units on the corpus stayed uncertain while a found one + // was a note. Only once the Choice that settles each check was + // asked and did not clear it. + let resources_open = !open.is_empty() + && open + .iter() + .all(|(id, _)| RESOURCE_CHECKS.contains(id) && settle_asked(id, get)); + if probability_at_least(leaning, LEADING_PROBABILITY) || resources_open { + Outcome::Note(leaning.max(p)) } else { Outcome::Uncertain(p) } diff --git a/src/units/outcome/mod.rs b/src/units/outcome/mod.rs index a0dd07a..0d37662 100644 --- a/src/units/outcome/mod.rs +++ b/src/units/outcome/mod.rs @@ -27,7 +27,7 @@ pub(super) use comments::{comment_concern_kind, comment_outcome, comment_signals use documentation::stale_outcome; pub(super) use documentation::{document_outcome, document_split, section_signals}; pub(super) use exposure::{Messages, django_settings_outcome, exposure_outcome, messages}; -pub(super) use injection::{injection_outcome, origin_outcome}; +pub(super) use injection::{RESOURCE_CHECKS, injection_outcome, origin_outcome}; pub(super) use maintainability::{ benign_key, function_outcome, organization_outcome, several_kind, shared_outcome, value_signals, values_outcome, diff --git a/src/units/tests/nextjs.rs b/src/units/tests/nextjs.rs index 6f7e93e..f418ec5 100644 --- a/src/units/tests/nextjs.rs +++ b/src/units/tests/nextjs.rs @@ -215,11 +215,13 @@ fn an_undecided_redirect_is_settled_by_where_its_target_comes_from() { settled_redirect(&project, &options, "own"), (Status::Clear, 1) ); - for chosen in ["given", "outside"] { + // A target its caller gives is a note, as a found one would be; one a + // request carries stays open. + for (chosen, status) in [("given", Status::Note), ("outside", Status::Uncertain)] { options.refresh = true; assert_eq!( settled_redirect(&project, &options, chosen), - (Status::Uncertain, 1), + (status, 1), "{chosen}" ); } diff --git a/src/units/tests/security.rs b/src/units/tests/security.rs index 3e57ace..7d0f184 100644 --- a/src/units/tests/security.rs +++ b/src/units/tests/security.rs @@ -249,11 +249,17 @@ fn an_undecided_url_is_settled_only_by_a_host_of_the_programs_own() { (Status::Clear, 2), "where its URLs come from and where it runs" ); - for parts in ["forwards", "given", "outside"] { + // A URL its caller gives or forwards is a note, as a found one would be; + // one from another party stays open. + for (parts, status) in [ + ("forwards", Status::Note), + ("given", Status::Note), + ("outside", Status::Uncertain), + ] { options.refresh = true; assert_eq!( settled_injection(&project, &options, "url", parts).0, - Status::Uncertain, + status, "{parts}" ); } @@ -265,7 +271,7 @@ fn an_undecided_path_is_settled_by_the_programs_own_or_its_local_users_paths() { for (parts, status) in [ ("own", Status::Clear), ("local", Status::Clear), - ("given", Status::Uncertain), + ("given", Status::Note), ("outside", Status::Uncertain), ] { assert_eq!( @@ -275,6 +281,21 @@ fn an_undecided_path_is_settled_by_the_programs_own_or_its_local_users_paths() { ); options.refresh = true; } + // The note names the path it left undecided. + let mut eval = scripted(0); + eval.overrides = vec![ + ("resource", noul_at(0.95)), + ("path", noul_at(0.4)), + ("origin", spread(0.0, 0.9, 0.1)), + ("path_source", choice_of("given", &PATH_SOURCE)), + ]; + let report = run(&project, &options, &mut eval); + let note = &report.files[0].findings[0]; + assert!( + note.message.contains("places a parameter into a file path"), + "{}", + note.message + ); } #[test] diff --git a/src/units/wording/mod.rs b/src/units/wording/mod.rs index 3da40e2..329bf5d 100644 --- a/src/units/wording/mod.rs +++ b/src/units/wording/mod.rs @@ -3,9 +3,9 @@ use super::{ Block, Detail, GroupInfo, outcome::{ - Answers, Outcome, benefit, comment_concern_kind, comment_signals, disagreement, - document_split, noul, origin_outcome, repeated, section_signals, settled_checks, - value_signals, + Answers, Outcome, RESOURCE_CHECKS, benefit, comment_concern_kind, comment_signals, + disagreement, document_split, noul, origin_outcome, repeated, section_signals, + settled_checks, value_signals, }, }; use crate::catalog; diff --git a/src/units/wording/security.rs b/src/units/wording/security.rs index 0fe98fb..aedfb9f 100644 --- a/src/units/wording/security.rs +++ b/src/units/wording/security.rs @@ -245,7 +245,7 @@ fn found_checks(rule: &str, answers: &Answers<'_>) -> Vec<&'static str> { return found.into_iter().map(|(id, _)| id).collect(); } // A note from a leaning check names the kind it leaned toward. - crate::units::security::checks(rule) + let leaning: Vec<&'static str> = crate::units::security::checks(rule) .iter() .filter_map(|check| match answers.get(check.id) { Some(Answer::Noul { noul }) @@ -261,6 +261,15 @@ fn found_checks(rule: &str, answers: &Answers<'_>) -> Vec<&'static str> { .max_by(|a, b| a.1.total_cmp(&b.1)) .map(|(id, _)| id) .into_iter() + .collect(); + if !leaning.is_empty() { + return leaning; + } + // A note from undecided checks of paths, URLs or redirects names them. + settled_checks(rule, &get) + .into_iter() + .filter(|(id, outcome)| *outcome != Outcome::Clear && RESOURCE_CHECKS.contains(id)) + .map(|(id, _)| id) .collect() } From 4c6ecdcc3f356aff385c24438dc7905f205b2003 Mon Sep 17 00:00:00 2001 From: Tauan BF <11513929+tauanbinato@users.noreply.github.com> Date: Sat, 26 Sep 2026 19:11:37 -0300 Subject: [PATCH 4/5] Let a test still undecided after its recheck lean clear below half A test whose hollow checks stay undecided once its recheck is asked is clear when they lean below 0.50. Labeled from the code, 4 of 43 such tests below 0.50 were hollow, against 10 of 35 above; undecided tests on the corpus went from 792 to 156, with nothing asked again. --- src/catalog.rs | 2 +- src/units/compose.rs | 24 +++++++++++++++++++++++- src/units/tests/test_rules.rs | 23 +++++++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/catalog.rs b/src/catalog.rs index e07c6cd..f48a9b3 100644 --- a/src/catalog.rs +++ b/src/catalog.rs @@ -275,7 +275,7 @@ pub fn rule_version(key: &str) -> &'static str { FILE_ORGANIZATION => "19", FUNCTION_SIMPLIFICATION => "14", SHARED_LOGIC => "20", - TEST_VALUE => "6", + TEST_VALUE => "7", TEST_REDUNDANCY => "4", INJECTION => "10", SENSITIVE_DATA => "7", diff --git a/src/units/compose.rs b/src/units/compose.rs index 262033a..9687473 100644 --- a/src/units/compose.rs +++ b/src/units/compose.rs @@ -122,7 +122,9 @@ fn resolved<'a>(unit: &UnitPlan, judgments: &'a [Judgment]) -> (Outcome, Answers } else if unit.rule == catalog::COMMENTS { comment_answers(unit, judgments) } else if unit.rule == catalog::TEST_VALUE { - test_value_answers(unit, judgments) + let merged = test_value_answers(unit, judgments); + let outcome = leaning_test(unit, judgments, unit_outcome(unit, &merged)); + return (outcome, merged); } else if unit.rule == catalog::TEST_REDUNDANCY { // Whether each test checks something the other does not, asked of a // pair that reached a review, sits beside its answers. @@ -135,6 +137,26 @@ fn resolved<'a>(unit: &UnitPlan, judgments: &'a [Judgment]) -> (Outcome, Answers (unit_outcome(unit, &merged), merged) } +/// A test whose hollow checks stay undecided once its recheck is asked (or +/// when it has none) leans: below 0.50 it is clear. Labeled from the code, +/// 4 of 43 such tests below 0.50 checked only their mocks or recomputed +/// their expected value (5 counting a test whose one real check is weak), +/// against 10 of 35 at 0.50 or more; 636 of the 792 undecided tests on the +/// corpus lean below. +fn leaning_test(unit: &UnitPlan, judgments: &[Judgment], outcome: Outcome) -> Outcome { + let rechecked = + unit.recheck.is_none() || !answers(judgments, &unit.id, Pass::Recheck).is_empty(); + match outcome { + Outcome::Uncertain(p) + if rechecked + && !crate::policy::probability_at_least(p, crate::policy::LEADING_PROBABILITY) => + { + Outcome::Clear + } + other => other, + } +} + /// The pass of the follow-ups whose questions sit beside the first answers /// under their own ids: document section and pair checks, the kind of a /// large document, and benign-kind value checks. diff --git a/src/units/tests/test_rules.rs b/src/units/tests/test_rules.rs index 4bdbe2e..bfc671e 100644 --- a/src/units/tests/test_rules.rs +++ b/src/units/tests/test_rules.rs @@ -460,6 +460,29 @@ fn copies_inside_tests_a_redundancy_finding_names_are_reported_once() { assert_eq!(rules, [catalog::id(catalog::TEST_REDUNDANCY)], "{rules:?}"); } +#[test] +fn a_test_still_undecided_after_its_recheck_leans_clear_below_half() { + let (project, mut options) = tests_project(&[("lib.rs", TESTS)], catalog::TEST_VALUE); + let mut status = |mock_only: f64| { + let mut eval = scripted(0); + eval.overrides.push(("mock_only", noul_at(mock_only))); + eval.recheck_overrides + .push(("mock_only", noul_at(mock_only))); + let report = run(&project, &options, &mut eval); + options.refresh = true; + ( + report.files[0].dimensions["test_value"].status.clone(), + report + .stages + .get("recheck") + .map_or(0, |stage| stage.successful_requests), + ) + }; + // Asked again first: the lean follows the recheck. + assert_eq!(status(0.4), (Status::Clear, 3)); + assert_eq!(status(0.6), (Status::Uncertain, 3)); +} + #[test] fn an_internal_details_consider_is_confirmed_by_what_its_assertions_read() { let (project, mut options) = tests_project(&[("lib.rs", TESTS)], catalog::TEST_VALUE); From 0e29e273fce06a49bf203c045eb13114bc6a36d3 Mon Sep 17 00:00:00 2001 From: Tauan BF <11513929+tauanbinato@users.noreply.github.com> Date: Sat, 26 Sep 2026 19:14:18 -0300 Subject: [PATCH 5/5] Describe the undecided-unit changes in the changelog and docs Undecided units on the corpus went from 1.39% to 0.83% of judged units with these changes and the environment Choice before them; no review or consider changed. --- CHANGELOG.md | 2 ++ docs/classification-cascade.md | 13 +++++++++++-- site/src/how-it-works.md | 13 +++++++++++-- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4a0dbb..cf72852 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ Measured on 103 pinned projects (24 new open-source ones of kinds not tried befo - Injection: a consider that rests on the function's parameters ("a caller passing outside input would make it exploitable") is asked, with the functions that call it, what the values it places can hold. It becomes a note when text the program fixes (literals its callers pass, numbers, names from a fixed list), values it creates or a local tool's own arguments lead; text another party can set, or callers that are not shown, keep it. Asked where the values come from, the recheck answered "the function's parameters" at 0.9 even for a Rust helper whose four callers pass literal SQL fragments. On the labeled projects, such considers went from 30 right and 51 wrong to 30 right and 32 wrong. Only these follow-ups are asked. - Hardcoded values: the Choice that names a finding's value lists, with each value, the other lines of its file that write it. A consider that rests only on a value's name, whose file writes that value again, is then asked what the value is: copies that must change together, or a value nothing near it explains, keep it; a value that the field or argument it fills or a comment beside it explains, an idiom such as a tolerance near zero or a unit conversion, or a hand-tuned number, clearly chosen, make it a note. On the labeled projects, such considers went from 53 right and 77 wrong (41%) to 32 right and 28 wrong (53%); the others are notes. Offered instead as the locate's "no value stands out", those kinds also took values with copies that must stay equal. Only these follow-ups are asked. - Hardcoded values: a review or consider that rests only on a value changing between environments is asked, once its value or constant is named and with the function or the lines that use the constant, where that value would differ. A value each installation must set, or the author's own account or domain, keeps it; the same value in every copy on purpose (the program's own service or registered client id, a provider's fixed address, a path the platform fixes), a fallback used only when configuration gives none, or code no deployment runs, at 0.80, make it a note. The Choice that names a file's constant lists the lines that use each one. On the labeled projects, such findings went from 17 right and 36 wrong to 15 right and 19 wrong: a desktop app's own license server, a provider's registered redirect and fallbacks behind environment variables are notes; a frontend's API host edited in code three times and a template author's domain as a fallback are the right ones lowered. Only these follow-ups are asked. +- Injection: outside PHP, a path check left undecided after its trace and recheck is asked, with the functions that call it, where the paths come from: the program's own directories or configuration, the command line or settings of the person running a local program, a whole path its caller gives, or another party's input (a request, an uploaded file or archive entry, a record users can edit). The program's own or the local user's clear it. A path, URL or redirect check still undecided on the function's parameters, whose Choice does not lean toward another party's input, is a note naming the path, URL or redirect, as a found one already was. Undecided injection units on the corpus went from 813 to 289, and no review or consider changed; the new Choice costs about $0.02 on the corpus. +- Tests: a test whose "checks only its mocks" or "recomputes its expected value" answer stays undecided after its recheck leans: below 0.50 it is clear. Labeled from the code, 4 of 43 such tests below 0.50 were hollow, against 10 of 35 at 0.50 or more. Undecided tests on the corpus went from 792 to 156, and undecided units overall from 1.39% to 0.83% of judged units. Nothing is asked again. - Unsafe settings: outside C#, PHP and Django, the TLS check asks whether code accepts a server's certificate, host key or host name without verifying it, and names a connection made without TLS (gRPC's insecure credentials, an insecure OpenTelemetry exporter, `sslmode=disable`) and verification skipped only when a caller or the operator asks for it as not turning it off. Asked whether code turns verification off, Online Boutique's gRPC clients and telemetry exporters inside its cluster, and its database reached through the AlloyDB connector, were seven reviews whatever the examples said, and httpx's `verify=False` branch and vaultwarden's opt-in setting for invalid SMTP certificates were reviews too. The escape check names i18next's `escapeValue: false` in a React app, whose components escape what they render, as escaping kept. Labeled unsafe-settings reviews went from 54 right and 24 wrong to 54 right and 14 wrong; pgweb defaulting a bookmark's `sslmode` to `disable`, which turns off TLS rather than verification, is no longer one. Only unsafe-settings traces are asked again. - Tests: Deno tests are test cases, in each of their forms: `Deno.test("name", fn)`, `Deno.test({ name: "name", fn() {…} })` and `Deno.test(function name() {…})`, with `.only` and `.ignore`. oak writes its 266 tests in the object form, and none of them was judged: its test files got a file-purpose request each and the test rules found nothing to ask. Only Deno projects' requests change. diff --git a/docs/classification-cascade.md b/docs/classification-cascade.md index 2d73381..ca473bc 100644 --- a/docs/classification-cascade.md +++ b/docs/classification-cascade.md @@ -158,6 +158,8 @@ signatures, or one candidate pair. its mocks is asked again with the bodies of the functions it calls and its file's imports, mocks and setup hooks (a part too long is left out, never cut); each answer replaces the first unless only the first is decisive. + One still undecided then leans: below 0.50 it is clear. Labeled from the + code, 4 of 43 such tests below 0.50 were hollow, against 10 of 35 above. A Ruby test is sent with the groups it is declared in, since an RSpec example reads as a sentence continuing them and the outer group often names the class under test. Its recheck shows, instead of every hook of the @@ -372,8 +374,15 @@ signatures, or one candidate pair. numbers, or values handed to it) was tried for considers on parameters and dropped: it cleared a sort column taken from the request as readily as clauses with placeholders. The same question about paths cleared real - traversals, reading names stored in an index as the program's own, so path - checks stay undecided until callers show more. The SQL check counts + traversals, reading names stored in an index as the program's own. Outside + PHP, an undecided path check is now asked, with its callers, where the + paths come from, naming a record users can edit as another party's input + and offering the local user's command line or settings: the program's own + paths or the local user's at 0.80 clear it (74 units on the corpus, among + them httprouter serving a directory through `http.Dir`). A path, URL or + redirect check still undecided on the function's parameters, whose Choice + does not lean toward another party's input, is a note, as a found one + already was: 450 injection units had stayed uncertain that way. The SQL check counts identifiers quoted by doubling embedded quotes as handled (identifiers cannot be bound), and the URL check excludes requests a web page sends from the user's browser; on fresh repositories both had flagged such code, while diff --git a/site/src/how-it-works.md b/site/src/how-it-works.md index 8fff789..bbf95af 100644 --- a/site/src/how-it-works.md +++ b/site/src/how-it-works.md @@ -176,6 +176,8 @@ signatures, or one candidate pair. its mocks is asked again with the bodies of the functions it calls and its file's imports, mocks and setup hooks (a part too long is left out, never cut); each answer replaces the first unless only the first is decisive. + One still undecided then leans: below 0.50 it is clear. Labeled from the + code, 4 of 43 such tests below 0.50 were hollow, against 10 of 35 above. A Ruby test is sent with the groups it is declared in, since an RSpec example reads as a sentence continuing them and the outer group often names the class under test. Its recheck shows, instead of every hook of the @@ -421,8 +423,15 @@ signatures, or one candidate pair. numbers, or values handed to it) was tried for considers on parameters and dropped: it cleared a sort column taken from the request as readily as clauses with placeholders. The same question about paths cleared real - traversals, reading names stored in an index as the program's own, so path - checks stay undecided until callers show more. The SQL check counts + traversals, reading names stored in an index as the program's own. Outside + PHP, an undecided path check is now asked, with its callers, where the + paths come from, naming a record users can edit as another party's input + and offering the local user's command line or settings: the program's own + paths or the local user's at 0.80 clear it (74 units on the corpus, among + them httprouter serving a directory through `http.Dir`). A path, URL or + redirect check still undecided on the function's parameters, whose Choice + does not lean toward another party's input, is a note, as a found one + already was: 450 injection units had stayed uncertain that way. The SQL check counts identifiers quoted by doubling embedded quotes as handled (identifiers cannot be bound), and the URL check excludes requests a web page sends from the user's browser; on fresh repositories both had flagged such code, while