Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 85 additions & 5 deletions docs/src/content/docs/rule-evaluation/when-clause.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ CEL expressions must evaluate to a **boolean** (`true` or `false`). If the expre

## Context variables

Five context variables are available inside `when` expressions:
The following context variables are available inside `when` expressions:

### `env` — Environment variables

Expand Down Expand Up @@ -94,6 +94,58 @@ rules:

The `paths` variable is most useful for checking properties of the defined path list itself (e.g., its size), since the `<path:sensitive>` pattern already handles matching individual files against the list.

### `redirects` — Redirect operators

A list of redirect operators attached to the command. Each element is an object with the following fields:

| Field | Type | Description | Example |
| ------------ | --------------- | ------------------------------------ | ----------------------------------------------------------------------- |
| `type` | `string` | `"input"`, `"output"`, or `"dup"` | `"output"` |
| `operator` | `string` | The redirect operator | `">"`, `">>"`, `"<"`, `"<<<"`, `">&"`, `"<&"`, `"&>"`, `"&>>"`, `">\|"` |
| `target` | `string` | The redirect destination | `"/tmp/log.txt"`, `"/dev/null"` |
| `descriptor` | `int` or `null` | File descriptor number, if specified | `2` (for `2>`) |

Type classification:

- `"output"`: `>`, `>>`, `>|`, `&>`, `&>>`
- `"input"`: `<`, `<<<`, `<<`, `<<-`
- `"dup"`: `>&`, `<&`

```yaml
# Require output redirect for renovate-dryrun
- deny: 'renovate-dryrun'
when: '!redirects.exists(r, r.type == "output")'
message: 'Please redirect output to a log file'
fix_suggestion: 'renovate-dryrun > /tmp/renovate-dryrun.log 2>&1'

# Only allow output redirect to /tmp/
- allow: 'renovate-dryrun'
when: 'redirects.exists(r, r.type == "output" && r.target.startsWith("/tmp/"))'
```

:::note
The `redirects` list is empty when the command has no redirects attached. Both single commands (e.g., `renovate-dryrun > /tmp/log.txt`) and compound commands (e.g., `cmd > file && cmd2`) populate redirect metadata correctly.
:::

### `pipe` — Pipeline position

An object indicating whether the command receives piped input or sends piped output:

| Field | Type | Description |
| -------- | ------ | ---------------------------------------------------------- |
| `stdin` | `bool` | `true` if the command receives input from a preceding pipe |
| `stdout` | `bool` | `true` if the command's output feeds into a following pipe |

Both fields are `false` when the command is not part of a pipeline.

```yaml
# Block piped execution of sh/bash (e.g., curl | sh)
- deny: 'sh'
when: 'pipe.stdin'
- deny: 'bash'
when: 'pipe.stdin'
```

### `vars` -- Captured variable values

A map of values captured by `<var:name>` placeholders in the matched pattern. When a pattern contains `<var:name>` and matches a command token, the matched token value is stored in `vars` under the variable name.
Expand Down Expand Up @@ -167,10 +219,12 @@ CEL supports standard operators for building conditions:

### Collection

| Expression | Description |
| --------------- | ------------------------------- |
| `value in list` | Check if value exists in a list |
| `size(list)` | Get the length of a list or map |
| Expression | Description |
| -------------------- | ------------------------------------------------ |
| `value in list` | Check if value exists in a list |
| `size(list)` | Get the length of a list or map |
| `x.exists(e, p)` | Check if any element satisfies predicate |
| `x.exists_one(e, p)` | Check if exactly one element satisfies predicate |

## Evaluation order

Expand Down Expand Up @@ -225,6 +279,32 @@ rules:
when: "flags.request == 'POST' && args[0].startsWith('https://prod.')"
```

### Redirect-based gating

```yaml
rules:
# Require output redirect for commands that produce large output
- deny: 'renovate-dryrun'
when: '!redirects.exists(r, r.type == "output")'
message: 'Please redirect output to a log file'
fix_suggestion: 'renovate-dryrun > /tmp/renovate-dryrun.log 2>&1'

# Allow with output redirect to /tmp/
- allow: 'renovate-dryrun'
when: 'redirects.exists(r, r.type == "output" && r.target.startsWith("/tmp/"))'
```

### Pipe safety

```yaml
rules:
# Block curl-pipe-sh attacks
- deny: 'sh'
when: 'pipe.stdin'
- deny: 'bash'
when: 'pipe.stdin'
```

## Related

- [Configuration Schema: `when`](/configuration/schema/#when) -- Configuration reference for the `when` field.
Expand Down
28 changes: 24 additions & 4 deletions src/adapter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ use crate::audit::{
SubEvaluation,
};
use crate::config::{Config, Defaults, MergedSandboxPolicy};
use crate::rules::command_parser::extract_commands;
use crate::rules::command_parser::{ExtractedCommand, PipeInfo, extract_commands_with_metadata};
use crate::rules::rule_engine::{
Action, EvalContext, RuleMatchInfo, default_action, evaluate_command, evaluate_compound,
Action, EvalContext, RuleMatchInfo, default_action, evaluate_command_with_metadata,
evaluate_compound,
};

/// Unified evaluation result for the adapter layer.
Expand Down Expand Up @@ -229,7 +230,17 @@ pub fn run_with_options(endpoint: &dyn Endpoint, config: &Config, options: &RunO
let context = EvalContext::from_env();

// Determine if the command is compound (contains pipes, &&, ||, ;)
let commands = extract_commands(&command).unwrap_or_else(|_| vec![command.clone()]);
let extracted_commands = extract_commands_with_metadata(&command).unwrap_or_else(|_| {
vec![ExtractedCommand {
command: command.clone(),
redirects: vec![],
pipe: PipeInfo::default(),
}]
});
let commands: Vec<String> = extracted_commands
.iter()
.map(|ec| ec.command.clone())
.collect();

if options.verbose && commands.len() > 1 {
eprintln!(
Expand Down Expand Up @@ -302,7 +313,16 @@ pub fn run_with_options(endpoint: &dyn Endpoint, config: &Config, options: &RunO
sub_evaluations: None,
}
} else {
match evaluate_command(config, effective_command, &context) {
// Pass redirect/pipe metadata from the first extracted command so that
// `when` clauses referencing `redirects` or `pipe` work correctly
// even for single commands with redirects (e.g., `cmd > /tmp/log.txt`).
let first_extracted = extracted_commands.first();
let empty_redirects = vec![];
let default_pipe = PipeInfo::default();
let (redirects, pipe) = first_extracted
.map(|ec| (ec.redirects.as_slice(), &ec.pipe))
.unwrap_or((empty_redirects.as_slice(), &default_pipe));
match evaluate_command_with_metadata(config, effective_command, &context, redirects, pipe) {
Ok(result) => {
if options.verbose {
log_matched_rules(&result.matched_rules);
Expand Down
Loading
Loading