From 5d5a5791824c56d97b0d01d2c64f26c5929b65b8 Mon Sep 17 00:00:00 2001 From: Brett Date: Thu, 8 Oct 2026 17:04:46 -0500 Subject: [PATCH 1/2] fix(audit): detect an output flag from definitions and probe it as spelled `p2-json-output` decided whether a tool has an output flag by looking for the characters `--output` or `--format` anywhere in its lowercased `--help`, at the top level and then in each subcommand. A longer flag (`--output-format`, `--output-dir`, `--format-version`), a usage line and a sentence all counted, and the probe that followed always passed `--output json` or `--format json`, whatever the help printed. A Go `flag` help that declares `-format` was not detected at all. Detection now asks the definition query, per help, and the safe probes pass each flag as the help spells it: `--help -format json` for a help that declares `-format` and no double-dash name. A longer flag that starts with `--output` or `--format` is a different name and triggers no probe. The evidence names what was found or searched: - skip: `` `--output` is declared in `export --help`, but no safe probe printed JSON (--help and --version override output flags in most CLIs) `` - opt_out: `no option definition in --help declares --output or --format; usage lines are not read. The tool is scored as shipping no structured output, ...`, with the count of subcommand helps read when there were any, and the dash-rule note when a help declares `-format` beside double-dash names. The unverified outcome is a variant of its own, so the declared `[p2] json_probe` takes over from it without comparing evidence strings. `--help` that times out or crashes still leaves the row a skip. --- README.md | 19 +- src/audits/behavioral/json_output.rs | 389 +++++++++++++++++++-------- 2 files changed, 291 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index ae31688..e9ac376 100644 --- a/README.md +++ b/README.md @@ -137,17 +137,18 @@ domain_verbs = ["post", "like", "repost", "timeline"] ### The settings -`json_probe`: when a tool's help shows an `--output` or `--format` flag, `p2-must-output-flag` checks it by passing -`json` to the flag beside `--help` and `--version`, the only calls `anc` makes unprompted. Most tools answer those in -text, and then the row is `skip`: `anc` could not check, so the row is not scored. A declared probe is the call `anc` -runs instead, exactly as written: no shell, with the timeout, closed stdin, and `NO_COLOR=1` of every other probe. The -row passes when the call exits 0 and its stdout parses as JSON, and fails otherwise, with evidence naming the call and -the file: `` `kubectl version --client -o json` printed JSON; probe declared via .anc.toml [p2].json_probe ``. Declare a +`json_probe`: when a tool's help declares `--output` or `--format`, `p2-must-output-flag` checks it by passing `json` to +the flag, spelled as the help prints it, beside `--help` and `--version`, the only calls `anc` makes unprompted. A +longer flag such as `--output-format` is a different name and does not count. Most tools answer those calls in text, and +then the row is `skip`: `anc` could not check, so the row is not scored. A declared probe is the call `anc` runs +instead, exactly as written: no shell, with the timeout, closed stdin, and `NO_COLOR=1` of every other probe. The row +passes when the call exits 0 and its stdout parses as JSON, and fails otherwise, with evidence naming the call and the +file: `` `kubectl version --client -o json` printed JSON; probe declared via .anc.toml [p2].json_probe ``. Declare a call that only reads and exits on its own; `kubectl version -o json` contacts a cluster and exits 1 without one, while `version --client` stays local. A probe that prints JSON also shows `p2-must-schema-print` that the tool emits -structured output when its help does not say so. The probe never stands in for the flag: with no `--output` or -`--format` in the help, the row stays `opt_out`. The nearest file that declares `json_probe` supplies it, and an empty -list declares nothing. +structured output when its help does not say so. The probe never stands in for the flag: when no option definition in +the help declares `--output` or `--format`, the row stays `opt_out`. The nearest file that declares `json_probe` +supplies it, and an empty list declares nothing. `schema_command`: `p2-must-schema-print` looks for a `schema` subcommand or a `--schema` flag, at the top level and one level down. A tool whose schema surface has another name, such as kubectl's `explain`, declares the subcommand path, one diff --git a/src/audits/behavioral/json_output.rs b/src/audits/behavioral/json_output.rs index 77d208e..b500225 100644 --- a/src/audits/behavioral/json_output.rs +++ b/src/audits/behavioral/json_output.rs @@ -2,16 +2,25 @@ use std::ffi::OsString; use crate::anc_toml::{JSON_PROBE_KEY, Sourced}; use crate::audit::Audit; -use crate::audits::behavioral::subcommand_help::should_skip; +use crate::audits::behavioral::subcommand_help::{dash_rule_notes, should_skip}; use crate::project::Project; use crate::runner::{BinaryRunner, HelpOutput, RunStatus}; use crate::types::{ AuditGroup, AuditLayer, AuditResult, AuditStatus, Confidence, Mitigation, Verdict, }; -/// The evidence when an output flag is detected but no safe probe printed -/// JSON. A `[p2] json_probe` declaration takes over from here. -const UNVERIFIABLE: &str = "--output/--format flag detected but could not validate JSON via safe probes (--help/--version override output flags in most CLIs)"; +/// The flags that let a caller choose the output format. +const OUTPUT_FLAGS: &[&str] = &["--output", "--format"]; + +/// What the help and the safe probes established. +enum Detected { + /// The row's status, with nothing left to check. + Settled(AuditStatus), + /// An output flag is declared and no safe probe printed JSON, with the + /// evidence that says so. A `[p2] json_probe` declaration takes over + /// from here. + Unverified(String), +} pub struct JsonOutputAudit; @@ -47,17 +56,16 @@ impl Audit for JsonOutputAudit { .config() .and_then(|cfg| cfg.p2.json_probe.as_ref()); let verdict = match (detect_json_output(runner, project), declared) { - (AuditStatus::Skip(reason), Some(probe)) if reason == UNVERIFIABLE => { - audit_declared_probe(runner, probe) - } - (status @ AuditStatus::OptOut(_), Some(probe)) => status + (Detected::Unverified(_), Some(probe)) => audit_declared_probe(runner, probe), + (Detected::Unverified(evidence), None) => AuditStatus::Skip(evidence).into(), + (Detected::Settled(status @ AuditStatus::OptOut(_)), Some(probe)) => status .with_note(&format!( - "The probe declared via {} runs only for a tool whose help shows an \ - --output or --format flag.", + "The probe declared via {} runs only for a tool whose help declares \ + --output or --format.", probe.cite(JSON_PROBE_KEY) )) .into(), - (status, _) => Verdict::from(status), + (Detected::Settled(status), _) => Verdict::from(status), }; let status = match project.anc_config.void_note() { Some(note) => verdict.status.with_note(¬e), @@ -80,28 +88,35 @@ impl Audit for JsonOutputAudit { /// Find an output flag in the help and validate JSON through the safe /// probes, without any declaration. -fn detect_json_output(runner: &BinaryRunner, project: &Project) -> AuditStatus { - let help_result = runner.run(&["--help"], &[]); - match help_result.status { - RunStatus::Ok => { - let output = format!("{}{}", help_result.stdout, help_result.stderr); - let lower = output.to_lowercase(); - let has_output_flag = lower.contains("--output"); - let has_format_flag = lower.contains("--format"); - - if has_output_flag || has_format_flag { - // Flag found in top-level help, validate directly - validate_json_output(runner, &[], has_output_flag, has_format_flag) - } else { - // Flag not in top-level help. Probe subcommands, since most CLIs - // (gh, kubectl, cargo) put --output on subcommands, not top-level. - probe_subcommands(runner, project.help_output()) - } +fn detect_json_output(runner: &BinaryRunner, project: &Project) -> Detected { + let help = match (runner.run(&["--help"], &[]).status, project.help_output()) { + (RunStatus::Ok, Some(help)) => help, + _ => { + return Detected::Settled(AuditStatus::Skip( + "could not run --help to detect output flags".into(), + )); } - _ => AuditStatus::Skip("could not run --help to detect output flags".into()), + }; + let flags = declared_output_flags(help); + if flags.is_empty() { + // Most CLIs (gh, kubectl, cargo) put --output on subcommands, not + // top-level. + probe_subcommands(runner, help) + } else { + validate_json_output(runner, &[], &flags) } } +/// Each of [`OUTPUT_FLAGS`] the help declares, spelled as the help prints +/// it: `-format` for a Go `flag` help that declares no double-dash name. +fn declared_output_flags(help: &HelpOutput) -> Vec { + OUTPUT_FLAGS + .iter() + .filter_map(|flag| help.find_flag(&[flag])) + .map(|found| found.spelling.to_string()) + .collect() +} + /// Run the declared probe: Pass when it exits 0 with JSON on stdout, naming /// the probe and the file that declared it; Fail otherwise, saying what it /// did instead. @@ -166,38 +181,47 @@ pub(crate) fn probe_invocation(runner: &BinaryRunner, args: &[String]) -> String /// Most CLI frameworks (clap, cobra, argparse) list subcommands under a "Commands:" /// or "Subcommands:" section, and hand-written help under `... commands:`; the /// shared parser reads all of them. -fn probe_subcommands(runner: &BinaryRunner, help: Option<&HelpOutput>) -> AuditStatus { - let subcommands = subcommands_to_probe(help); +fn probe_subcommands(runner: &BinaryRunner, help: &HelpOutput) -> Detected { + let subcommands = subcommands_to_probe(Some(help)); if subcommands.is_empty() { - return AuditStatus::OptOut( - "no --output/--format flag detected — tool does not ship structured output. \ - Schema-discovery requirements (p2-must-schema-print, p2-should-schema-file) \ - collapse to n/a via antecedent propagation." - .into(), - ); + return Detected::Settled(AuditStatus::OptOut(help.noting_dash_rule( + OUTPUT_FLAGS, + "no option definition in --help declares --output or --format; usage lines are not \ + read. The tool is scored as shipping no structured output, and the \ + schema-discovery requirements (p2-must-schema-print, p2-should-schema-file) \ + collapse to n/a via antecedent propagation.", + ))); } + let mut read: Vec<(String, HelpOutput)> = Vec::new(); for subcmd in &subcommands { let sub_help = runner.run(&[subcmd, "--help"], &[]); if sub_help.status != RunStatus::Ok { continue; } - - let sub_output = format!("{}{}", sub_help.stdout, sub_help.stderr); - let sub_lower = sub_output.to_lowercase(); - let has_output = sub_lower.contains("--output"); - let has_format = sub_lower.contains("--format"); - - if has_output || has_format { - return validate_json_output(runner, &[subcmd], has_output, has_format); + let sub_help = HelpOutput::from_raw(format!("{}{}", sub_help.stdout, sub_help.stderr)); + let flags = declared_output_flags(&sub_help); + if !flags.is_empty() { + return validate_json_output(runner, &[subcmd], &flags); } - } - - AuditStatus::OptOut( - "no --output/--format flag detected in any subcommand — tool does not ship \ - structured output." - .into(), - ) + read.push((subcmd.to_string(), sub_help)); + } + + let message = help.noting_dash_rule( + OUTPUT_FLAGS, + &format!( + "no option definition in --help, or in the --help of the {} subcommand{} read, \ + declares --output or --format; usage lines are not read. The tool is scored as \ + shipping no structured output.", + read.len(), + if read.len() == 1 { "" } else { "s" }, + ), + ); + let names: Vec<&str> = read.iter().map(|(name, _)| name.as_str()).collect(); + Detected::Settled(AuditStatus::OptOut(format!( + "{message}{}", + dash_rule_notes(&names, &read, OUTPUT_FLAGS) + ))) } /// Top-level subcommand names worth probing for an output flag: the shared @@ -213,64 +237,36 @@ fn subcommands_to_probe(help: Option<&HelpOutput>) -> Vec<&str> { .collect() } -/// Try safe subcommands with the detected flag to validate actual JSON output. +/// Try safe subcommands with the declared flags to validate actual JSON output. /// /// `prefix` contains any subcommand path (e.g., ["audit"]) to prepend to the -/// probe commands. For top-level flags, prefix is empty. +/// probe commands. For top-level flags, prefix is empty. Each of `flags` is +/// passed as the help spells it. /// /// Strategy: try `[prefix...] --help --flag json` first (safe, --help always exits /// without side effects). If that produces non-JSON (many CLIs ignore --output with /// --help), fall back to `[prefix...] --version --flag json`. Never run the binary /// bare with just `--flag json`, as that could execute destructive commands. -fn validate_json_output( - runner: &BinaryRunner, - prefix: &[&str], - has_output_flag: bool, - has_format_flag: bool, -) -> AuditStatus { - let flag_variants: Vec<&str> = { - let mut v = Vec::new(); - if has_output_flag { - v.push("--output"); - } - if has_format_flag { - v.push("--format"); - } - v - }; - +fn validate_json_output(runner: &BinaryRunner, prefix: &[&str], flags: &[String]) -> Detected { // Safe suffixes: always probe with --help or --version, never bare invocation. // Bare subcommand probing (`&[]`) was removed because it is unsafe in the // general case — subcommands may have side effects (kubectl apply, docker rm, // terraform plan), and for agentnative itself it caused fork bombs. - let safe_suffixes: Vec<&[&str]> = vec![&["--help"], &["--version"]]; - - // Try space-separated: [prefix...] [suffix...] flag json - for flag in &flag_variants { - for suffix in &safe_suffixes { - let mut args: Vec<&str> = prefix.to_vec(); - args.extend_from_slice(suffix); - args.push(flag); - args.push("json"); - + const SAFE_SUFFIXES: &[&str] = &["--help", "--version"]; + + // Space-separated first (`flag json`), then `flag=json`. + let spaced = flags.iter().map(|flag| vec![flag.clone(), "json".into()]); + let joined = flags.iter().map(|flag| vec![format!("{flag}=json")]); + for value in spaced.chain(joined) { + for suffix in SAFE_SUFFIXES { + let args: Vec<&str> = prefix + .iter() + .copied() + .chain([*suffix]) + .chain(value.iter().map(String::as_str)) + .collect(); if let Some(status) = try_json_probe(runner, &args) { - return status; - } - } - } - - // Try =json syntax: [prefix...] [suffix...] flag=json - for flag in &flag_variants { - let flag_eq = format!("{flag}=json"); - for suffix in &safe_suffixes { - let mut args: Vec<&str> = prefix.to_vec(); - args.extend_from_slice(suffix); - let args_with_eq: Vec = args.iter().map(|s| s.to_string()).collect(); - let mut final_args: Vec<&str> = args_with_eq.iter().map(|s| s.as_str()).collect(); - final_args.push(&flag_eq); - - if let Some(status) = try_json_probe(runner, &final_args) { - return status; + return Detected::Settled(status); } } } @@ -278,7 +274,17 @@ fn validate_json_output( // The safe probes reach only `--help` and `--version`, which most CLIs // answer in text whatever the output flag says, so a miss is anc's // limit, not the tool's: the row is not scored. - AuditStatus::Skip(UNVERIFIABLE.into()) + let names: Vec = flags.iter().map(|flag| format!("`{flag}`")).collect(); + let place = match prefix { + [] => "--help".to_string(), + path => format!("`{} --help`", path.join(" ")), + }; + Detected::Unverified(format!( + "{} {} declared in {place}, but no safe probe printed JSON (--help and --version \ + override output flags in most CLIs)", + names.join(" and "), + if names.len() == 1 { "is" } else { "are" }, + )) } /// Run a single JSON probe and return Some(status) if valid JSON found. @@ -395,7 +401,7 @@ esac AuditStatus::OptOut(msg) => assert!( msg.ends_with( "The probe declared via .anc.toml [p2].json_probe runs only for a tool whose \ - help shows an --output or --format flag." + help declares --output or --format." ), "{msg}" ), @@ -403,6 +409,170 @@ esac } } + /// A help, and each output flag it declares as the probe must spell it. + const DECLARED: &[(&str, &str, &[&str])] = &[ + ( + "a long name with a short alias", + "Options:\n -o, --output Output format: text or json\n -h, --help Show help\n", + &["--output"], + ), + ( + "cobra's `--format string`", + "Flags:\n --format string Output format (default \"text\")\n -h, --help help for tool\n", + &["--format"], + ), + ( + "both flags", + "Options:\n --output Write to FILE\n --format Output format\n", + &["--output", "--format"], + ), + ( + "Go flag: one dash, and no double-dash name in the help", + "Usage of tool:\n -format string\n \tOutput format: text or json (default \"text\")\n -version\n \tPrint version\n", + &["-format"], + ), + ( + "a longer flag that starts with the name", + "Options:\n --output-format Output format\n --output-dir Where to write\n --format-version Schema version\n -h, --help Show help\n", + &[], + ), + ( + "a usage line", + "Usage: tool [--output FORMAT] \n\nOptions:\n -h, --help Show help\n", + &[], + ), + ( + "a sentence", + "Pass --format json for machine-readable output.\n\nOptions:\n -h, --help Show help\n", + &[], + ), + ( + "one dash beside double-dash names", + "Options:\n -format Output format\n --help Show help\n", + &[], + ), + ]; + + #[test] + fn output_flags_are_the_ones_the_help_declares_spelled_as_printed() { + let misread: Vec = DECLARED + .iter() + .filter_map(|(what, help, expected)| { + let found = declared_output_flags(&HelpOutput::from_raw(*help)); + (found != *expected) + .then(|| format!("{what}: read {found:?}, declares {expected:?}")) + }) + .collect(); + assert!(misread.is_empty(), "{misread:#?}"); + } + + #[test] + fn a_single_dash_format_flag_is_probed_as_the_help_spells_it() { + // Prints JSON only for the spelling its help declares. + let script = r#" +case "$*" in + "--help -format json") echo '{"format":"json"}';; + *--format*) echo "flag provided but not defined: -format" >&2; exit 2;; + *--help*) + printf 'Usage of tool:\n -format string\n \tOutput format: text or json (default "text")\n -version\n \tPrint version\n';; + *) echo "hello";; +esac +"#; + let project = test_project_with_sh_script(script); + let result = JsonOutputAudit.run(&project).expect("audit should run"); + assert_eq!(result.status, AuditStatus::Pass, "got {:?}", result.status); + } + + #[test] + fn a_longer_flag_that_starts_with_output_does_not_trigger_the_probe() { + // Would print JSON for `--output json`, which no line of its help + // declares. + let script = r#" +case "$*" in + *--output\ json*|*--output=json*) echo '{"probed":true}';; + *--help*) + printf 'Usage: tool [OPTIONS]\n\nOptions:\n --output-format Output format\n -h, --help Show help\n';; + *) echo "hello";; +esac +"#; + let project = test_project_with_sh_script(script); + let result = JsonOutputAudit.run(&project).expect("audit should run"); + assert_eq!( + result.status, + AuditStatus::OptOut( + "no option definition in --help declares --output or --format; usage lines are \ + not read. The tool is scored as shipping no structured output, and the \ + schema-discovery requirements (p2-must-schema-print, p2-should-schema-file) \ + collapse to n/a via antecedent propagation." + .into() + ) + ); + } + + #[test] + fn an_unverified_flag_is_named_as_declared_with_the_help_that_declares_it() { + let top = JsonOutputAudit + .run(&test_project_with_sh_script(UNVERIFIABLE_OUTPUT_FLAG)) + .expect("audit should run"); + assert_eq!( + top.status, + AuditStatus::Skip( + "`--output` is declared in --help, but no safe probe printed JSON (--help and \ + --version override output flags in most CLIs)" + .into() + ) + ); + + let script = r#" +case "$*" in + *export*--help*) + printf 'Usage: tool export [OPTIONS]\n\nOptions:\n --output Write to FILE\n --format Output format\n';; + *--help*) + printf 'Usage: tool [COMMAND]\n\nCommands:\n list List items\n export Export items\n\nOptions:\n -h, --help Show help\n';; + *) echo "hello";; +esac +"#; + let sub = JsonOutputAudit + .run(&test_project_with_sh_script(script)) + .expect("audit should run"); + assert_eq!( + sub.status, + AuditStatus::Skip( + "`--output` and `--format` are declared in `export --help`, but no safe probe \ + printed JSON (--help and --version override output flags in most CLIs)" + .into() + ) + ); + } + + #[test] + fn an_opt_out_after_reading_subcommands_says_how_many_and_notes_a_near_miss() { + let script = r#" +case "$*" in + *list*--help*) + printf 'Usage: tool list [OPTIONS]\n\nOptions:\n -format Output format\n --help Show help\n';; + *sync*--help*) + printf 'Usage: tool sync [OPTIONS]\n\nOptions:\n --help Show help\n';; + *--help*) + printf 'Usage: tool [COMMAND]\n\nCommands:\n list List items\n sync Sync items\n\nOptions:\n -h, --help Show help\n';; + *) echo "hello";; +esac +"#; + let result = JsonOutputAudit + .run(&test_project_with_sh_script(script)) + .expect("audit should run"); + assert_eq!( + result.status, + AuditStatus::OptOut( + "no option definition in --help, or in the --help of the 2 subcommands read, \ + declares --output or --format; usage lines are not read. The tool is scored as \ + shipping no structured output. In `list`, `-format` is declared, but this help \ + also declares double-dash names, so it does not count as `--format`." + .into() + ) + ); + } + #[test] fn json_output_pass_with_valid_json() { let script = r#" @@ -410,7 +580,7 @@ case "$*" in *--help*--output*json*|*--output*json*--help*) echo '{"help":true,"format":"json"}';; *--help*) - echo "Usage: test [--output FORMAT]";; + printf 'Usage: test [OPTIONS]\n\nOptions:\n --output Output format\n';; *--output\ json*|*--output=json*) echo '{"version":"1.0"}';; *) @@ -429,7 +599,7 @@ case "$*" in *--help*--format*json*|*--format*json*--help*) echo '{"help":true}';; *--help*) - echo "Usage: test [--format FORMAT]";; + printf 'Usage: test [OPTIONS]\n\nOptions:\n --format Output format\n';; *) echo "hello";; esac @@ -457,7 +627,7 @@ esac let result = JsonOutputAudit.run(&project).expect("audit should run"); match &result.status { AuditStatus::Skip(msg) => { - assert!(msg.contains("could not validate JSON"), "got: {msg}") + assert!(msg.contains("no safe probe printed JSON"), "got: {msg}") } other => panic!("expected Skip, got {other:?}"), } @@ -500,7 +670,10 @@ esac let project = test_project_with_sh_script("echo 'just some help text'"); let result = JsonOutputAudit.run(&project).expect("audit should run"); match &result.status { - AuditStatus::OptOut(msg) => assert!(msg.contains("no --output")), + AuditStatus::OptOut(msg) => assert!( + msg.starts_with("no option definition in --help declares --output or --format"), + "{msg}" + ), other => panic!("expected OptOut, got {other:?}"), } } @@ -512,7 +685,7 @@ case "$*" in *--version*--output*json*|*--output*json*--version*|*--version*--output=json*|*--output=json*--version*) echo '{"version":"2.0"}';; *--help*) - echo "Usage: test [--output FORMAT]";; + printf 'Usage: test [OPTIONS]\n\nOptions:\n --output Output format\n';; *--version*) echo "test 2.0";; *) @@ -542,7 +715,7 @@ case "$*" in *audit*--output*json*|*audit*--output=json*) echo '{"audits":"passed"}';; *audit*--help*) - echo "Usage: test audit [--output FORMAT]";; + printf 'Usage: test audit [OPTIONS]\n\nOptions:\n --output Output format\n';; *--help*) echo "Usage: test [COMMAND] @@ -574,7 +747,7 @@ case "$*" in *count*--output*json*|*count*--output=json*) echo '{"count":3}';; *count*--help*) - echo "Usage: test count [--output FORMAT]";; + printf 'Usage: test count [OPTIONS]\n\nOptions:\n --output Output format\n';; *--help*) echo "Usage: test [options] @@ -600,7 +773,7 @@ case "$*" in *help*--output*json*|*help*--output=json*) echo '{"help":true}';; help*--help*) - echo "Usage: test help [--output FORMAT]";; + printf 'Usage: test help [OPTIONS]\n\nOptions:\n --output Output format\n';; *--help*) echo "Usage: test [COMMAND] From 4697a3c2487970368550dca15db029ed81d3e884 Mon Sep 17 00:00:00 2001 From: Brett Date: Thu, 8 Oct 2026 17:29:04 -0500 Subject: [PATCH 2/2] fix(audit): keep the output-flag opt-out evidence to what was searched The opt-out evidence closed with `The tool is scored as shipping no structured output`. A tool whose help declares `--output-format` reads that sentence beside its own JSON mode, and the sentence is then false on its face. The evidence now says which flags the requirement accepts and stops there: `no option definition in --help declares --output or --format, the flags this requirement accepts; usage lines are not read.` --- src/audits/behavioral/json_output.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/audits/behavioral/json_output.rs b/src/audits/behavioral/json_output.rs index b500225..bd4ac91 100644 --- a/src/audits/behavioral/json_output.rs +++ b/src/audits/behavioral/json_output.rs @@ -186,8 +186,8 @@ fn probe_subcommands(runner: &BinaryRunner, help: &HelpOutput) -> Detected { if subcommands.is_empty() { return Detected::Settled(AuditStatus::OptOut(help.noting_dash_rule( OUTPUT_FLAGS, - "no option definition in --help declares --output or --format; usage lines are not \ - read. The tool is scored as shipping no structured output, and the \ + "no option definition in --help declares --output or --format, the flags this \ + requirement accepts; usage lines are not read. The row is opt_out, and the \ schema-discovery requirements (p2-must-schema-print, p2-should-schema-file) \ collapse to n/a via antecedent propagation.", ))); @@ -211,8 +211,8 @@ fn probe_subcommands(runner: &BinaryRunner, help: &HelpOutput) -> Detected { OUTPUT_FLAGS, &format!( "no option definition in --help, or in the --help of the {} subcommand{} read, \ - declares --output or --format; usage lines are not read. The tool is scored as \ - shipping no structured output.", + declares --output or --format, the flags this requirement accepts; usage lines \ + are not read.", read.len(), if read.len() == 1 { "" } else { "s" }, ), @@ -500,8 +500,8 @@ esac assert_eq!( result.status, AuditStatus::OptOut( - "no option definition in --help declares --output or --format; usage lines are \ - not read. The tool is scored as shipping no structured output, and the \ + "no option definition in --help declares --output or --format, the flags this \ + requirement accepts; usage lines are not read. The row is opt_out, and the \ schema-discovery requirements (p2-must-schema-print, p2-should-schema-file) \ collapse to n/a via antecedent propagation." .into() @@ -565,9 +565,9 @@ esac result.status, AuditStatus::OptOut( "no option definition in --help, or in the --help of the 2 subcommands read, \ - declares --output or --format; usage lines are not read. The tool is scored as \ - shipping no structured output. In `list`, `-format` is declared, but this help \ - also declares double-dash names, so it does not count as `--format`." + declares --output or --format, the flags this requirement accepts; usage lines \ + are not read. In `list`, `-format` is declared, but this help also declares \ + double-dash names, so it does not count as `--format`." .into() ) );