-
Notifications
You must be signed in to change notification settings - Fork 0
fix(hook): carry run_in_background into the policy input, as CLOUD-834 said it would
#725
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| //! `input.call["run-in-background"]`, over the compiled binary (CLOUD-1094). | ||
| //! | ||
| //! **A `with input as` case cannot answer this**, which is why the tier is here | ||
| //! rather than in a module's own suite. CLOUD-845 measured a module fabricating | ||
| //! an input KEY the engine cannot produce, and CLOUD-857 the same thing with an | ||
| //! input SHAPE; both were green over a gate that decided nothing. The question | ||
| //! this file asks is precisely the one those cannot: does the ENGINE put the key | ||
| //! in the document it hands a module. | ||
| //! | ||
| //! The fixture module is deliberately trivial — it fires on the flag and on | ||
| //! nothing else — so a failure here is about the projection and never about a | ||
| //! predicate. Two arms, because one proves nothing: a module that denied every | ||
| //! call would satisfy the positive and be useless (CLOUD-418). | ||
|
|
||
| // Panicking on setup failure is the idiomatic way for a test to fail loudly. | ||
| #![allow(clippy::unwrap_used, clippy::expect_used)] | ||
|
|
||
| mod common; | ||
|
|
||
| use std::path::{Path, PathBuf}; | ||
|
|
||
| use common::{git_in, run_with_stdin, scratch, stderr, write}; | ||
|
|
||
| const CONFIG: &str = r#"version = 1 | ||
|
|
||
| [[rule]] | ||
| id = "probe" | ||
| kind = "policy" | ||
| scope = "mediated_call" | ||
| module = "probe.rego" | ||
| severity = "deny" | ||
|
|
||
| [[verdict]] | ||
| id = "V-PROBE-BACKGROUNDED" | ||
| gloss = "the probe saw a backgrounded call" | ||
| class = "A fixture class, raised only by this suite's probe module." | ||
|
|
||
| [[verdict.route]] | ||
| id = "R-PROBE" | ||
| kind = "document" | ||
| target = "probe.rego" | ||
| "#; | ||
|
|
||
| /// Fires on the projected flag and on nothing else. | ||
| /// | ||
| /// `== true` rather than a truthiness test, deliberately: the key is | ||
| /// three-valued, and a predicate that fired on `null` would be reading | ||
| /// "the host said nothing" as "the host said no". | ||
| const PROBE: &str = r#"package batten.probe | ||
|
|
||
| import rego.v1 | ||
|
|
||
| rules contains "probe" | ||
|
|
||
| violation contains { | ||
| "rule": "probe", | ||
| "verdict": "V-PROBE-BACKGROUNDED", | ||
| } if { | ||
| input.call["run-in-background"] == true | ||
| } | ||
|
|
||
| test_a_backgrounded_call_fires if { | ||
| some v in violation with input as {"call": {"run-in-background": true, "command": "cd /tmp && sleep 1"}} | ||
| v.rule == "probe" | ||
| } | ||
|
|
||
| test_a_foreground_call_does_not if { | ||
| count(violation) == 0 with input as {"call": {"run-in-background": false, "command": "cd /tmp && sleep 1"}} | ||
| } | ||
|
|
||
| test_an_absent_flag_is_not_a_false_one if { | ||
| count(violation) == 0 with input as {"call": {"run-in-background": null, "command": "cd /tmp && sleep 1"}} | ||
| } | ||
| "#; | ||
|
|
||
| /// A fixture per case: these run in parallel and `git init` races on a shared | ||
| /// directory, which is a fact about the harness rather than about the projection. | ||
| fn fixture(name: &str) -> PathBuf { | ||
| let dir = scratch(&format!("call-background-flag-{name}")); | ||
| write(&dir, "batten.toml", CONFIG); | ||
| write(&dir, "probe.rego", PROBE); | ||
| git_in(&dir, &["init", "-q", "-b", "main", "."]); | ||
| dir | ||
| } | ||
|
|
||
| /// The exit status the `exit-code` harness renders: `2` is the policy verdict. | ||
| fn verdict(dir: &Path, payload: &str) -> (Option<i32>, String) { | ||
| let outcome = run_with_stdin(dir, &["hook", "--harness", "exit-code"], payload); | ||
| (outcome.status.code(), stderr(&outcome)) | ||
| } | ||
|
|
||
| fn envelope(flag: Option<bool>) -> String { | ||
| let extra = match flag { | ||
| Some(true) => r#","run_in_background":true"#, | ||
| Some(false) => r#","run_in_background":false"#, | ||
| None => "", | ||
| }; | ||
| format!( | ||
| "{{\"hook_event_name\":\"PreToolUse\",\"tool_name\":\"Bash\",\ | ||
| \"tool_input\":{{\"command\":\"sleep 90\"{extra}}}}}" | ||
| ) | ||
| } | ||
|
|
||
| #[test] | ||
| fn a_backgrounded_call_reaches_the_module() { | ||
| // The positive. Before the projection this key was undefined, Rego read that | ||
| // as *does not hold*, and the probe was silent on every call — a dead gate | ||
| // and a clean tree being byte-identical on the decision surface. | ||
| let dir = fixture("backgrounded"); | ||
| let (code, cause) = verdict(&dir, &envelope(Some(true))); | ||
| assert_eq!(code, Some(2), "the flag must reach the module\n{cause}"); | ||
| assert!(cause.contains("V-PROBE-BACKGROUNDED"), "{cause}"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn a_foreground_call_does_not() { | ||
| // The discrimination. Without it the case above passes over a projection | ||
| // that emitted `true` unconditionally. | ||
| let dir = fixture("foreground"); | ||
| let (code, cause) = verdict(&dir, &envelope(Some(false))); | ||
| assert_eq!(code, Some(0), "an explicit false must not fire\n{cause}"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn a_host_that_said_nothing_is_not_a_false_one() { | ||
| // THREE-VALUED, and this is the case that holds it. Most hosts send no such | ||
| // key at all, so collapsing absent into `false` would be a claim about every | ||
| // one of them — and a predicate wanting "definitely foreground" would then | ||
| // fire on a host that never spoke. | ||
| let dir = fixture("absent"); | ||
| let (code, cause) = verdict(&dir, &envelope(None)); | ||
| assert_eq!(code, Some(0), "an absent flag must not fire\n{cause}"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn the_other_host_spelling_resolves_to_the_same_answer() { | ||
| // `Field::RunInBackground` reads `run_in_background` or `runInBackground` | ||
| // because the hosts disagree the same way they do over | ||
| // `tool_response`/`toolResponse`. The projection carries THAT answer rather | ||
| // than a raw key, so a module never has to know which host it is behind — | ||
| // and this is what pins that, since a projection reading the key directly | ||
| // would pass every case above and fail only here. | ||
| let dir = fixture("camel-case"); | ||
| let payload = "{\"hook_event_name\":\"PreToolUse\",\"tool_name\":\"Bash\",\ | ||
| \"tool_input\":{\"command\":\"sleep 90\",\"runInBackground\":true}}"; | ||
| let (code, cause) = verdict(&dir, payload); | ||
| assert_eq!( | ||
| code, | ||
| Some(2), | ||
| "the camelCase spelling must resolve\n{cause}" | ||
| ); | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Test
nullindependently fromfalse.This case only verifies that
nulldoes not equaltrue. A regression that projects an absent flag asfalsekeeps both Line 121 and Line 132 green. Add a probe that matches== nulland assert that it denies only for an absent flag, while explicitfalsestill allows.🤖 Prompt for AI Agents