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
53 changes: 52 additions & 1 deletion docs/src/content/docs/configuration/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ Name of a sandbox preset (defined in `definitions.sandbox`) to apply when this r

### `definitions`

Reusable definitions for paths, sandbox presets, wrappers, and commands.
Reusable definitions for paths, variables, sandbox presets, wrappers, and commands.

**Type:** `object`\
**Default:** `{}`\
Expand Down Expand Up @@ -311,6 +311,51 @@ definitions:
- mycustomtool
```

#### `definitions.vars`

Typed variable definitions referenced by `<var:name>` in rule patterns. Each variable has a `type` (controlling how values are matched) and a list of `values`.

**Type:** `map[str, VarDefinition]`\
**Default:** `{}`

```yaml title="runok.yml"
definitions:
vars:
instance-ids:
values:
- i-abc123
- i-def456
test-script:
type: path
values:
- ./tests/run
```

##### Variable Definition Fields

###### `type`

Controls how the variable's values are matched against command arguments.

**Type:** `"literal" | "path"`\
**Default:** `"literal"`

| Type | Matching behavior |
| --------- | ------------------------------------------------------------------------- |
| `literal` | Exact string match |
| `path` | Canonicalize both sides before comparison, fallback to path normalization |

###### `values`

List of allowed values for this variable.

**Type:** `list[str]`\
**Required:** Yes

:::note
Variable definitions must contain concrete values. `<var:name>` or `<path:name>` references inside `definitions.vars` values are not allowed.
:::

### `audit`

Audit log settings. Controls whether command evaluations are recorded and where log files are stored. Audit settings can only be configured in the **global** `runok.yml` — audit sections in project or local override configs are silently ignored.
Expand Down Expand Up @@ -370,6 +415,12 @@ definitions:
secrets:
- ~/.ssh
- ~/.gnupg
vars:
safe-scripts:
type: path
values:
- ./tests/run
- ./scripts/lint.sh
sandbox:
standard:
fs:
Expand Down
39 changes: 20 additions & 19 deletions docs/src/content/docs/pattern-syntax/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,31 @@ Patterns are parsed exactly as written, with no hidden rewriting or implicit tra

## Syntax Elements

| Syntax | Example | Description |
| ----------------------------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------ |
| Literal | `git status` | Exact token match |
| [Wildcard](/pattern-syntax/wildcards/) | `git *` | Zero or more tokens |
| [Glob](/pattern-syntax/wildcards/#glob-patterns) | `list-*`, `*.txt` | `*` inside a literal matches zero or more characters |
| [Alternation](/pattern-syntax/alternation/) | `-X\|--request`, `main\|master` | Pipe-separated alternatives |
| [Negation](/pattern-syntax/alternation/#negation) | `!GET`, `!describe\|get` | Matches anything except the specified value(s) |
| [Optional group](/pattern-syntax/optional-groups/) | `[-f]`, `[-X POST]` | Matches with or without the group |
| [Flag with value](/pattern-syntax/matching-behavior/#flag-schema-inference) | `-X\|--request POST` | A flag-value pair matched in any order |
| [Placeholder](/pattern-syntax/placeholders/) | `<cmd>`, `<opts>`, `<path:...>` | Special tokens in `<...>` with various behaviors (see below) |
| Backslash escape | `\;` | Literal match after removing the backslash |
| Quoted literal | `"WIP*"`, `'hello'` | Exact match without glob expansion |
| [Multi-word alternation](/pattern-syntax/alternation/#multi-word-alternation) | `"npx prettier"\|prettier` | Alternatives that include multi-word commands |
| Syntax | Example | Description |
| ----------------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------ |
| Literal | `git status` | Exact token match |
| [Wildcard](/pattern-syntax/wildcards/) | `git *` | Zero or more tokens |
| [Glob](/pattern-syntax/wildcards/#glob-patterns) | `list-*`, `*.txt` | `*` inside a literal matches zero or more characters |
| [Alternation](/pattern-syntax/alternation/) | `-X\|--request`, `main\|master` | Pipe-separated alternatives |
| [Negation](/pattern-syntax/alternation/#negation) | `!GET`, `!describe\|get` | Matches anything except the specified value(s) |
| [Optional group](/pattern-syntax/optional-groups/) | `[-f]`, `[-X POST]` | Matches with or without the group |
| [Flag with value](/pattern-syntax/matching-behavior/#flag-schema-inference) | `-X\|--request POST` | A flag-value pair matched in any order |
| [Placeholder](/pattern-syntax/placeholders/) | `<cmd>`, `<opts>`, `<path:...>`, `<var:...>` | Special tokens in `<...>` with various behaviors (see below) |
| Backslash escape | `\;` | Literal match after removing the backslash |
| Quoted literal | `"WIP*"`, `'hello'` | Exact match without glob expansion |
| [Multi-word alternation](/pattern-syntax/alternation/#multi-word-alternation) | `"npx prettier"\|prettier` | Alternatives that include multi-word commands |

### Placeholders

Tokens wrapped in `<...>` are **placeholders** — special tokens that match dynamically rather than by exact string comparison. Each placeholder type has different matching behavior:

| Placeholder | Example | Description | Details |
| ------------- | ---------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------- |
| `<cmd>` | `sudo <cmd>` | Captures the wrapped command for further rule evaluation | [Command](/pattern-syntax/placeholders/#command-cmd) |
| `<opts>` | `env <opts> <cmd>` | Absorbs zero or more flag-like tokens (starting with `-`) | [Options](/pattern-syntax/placeholders/#options-opts) |
| `<vars>` | `env <vars> <cmd>` | Absorbs zero or more `KEY=VALUE` tokens | [Variables](/pattern-syntax/placeholders/#variables-vars) |
| `<path:name>` | `cat <path:sensitive>` | Matches against a named list of paths from `definitions` | [Path References](/pattern-syntax/placeholders/#path-references-pathname) |
| Placeholder | Example | Description | Details |
| ------------- | ---------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `<cmd>` | `sudo <cmd>` | Captures the wrapped command for further rule evaluation | [Command](/pattern-syntax/placeholders/#command-cmd) |
| `<opts>` | `env <opts> <cmd>` | Absorbs zero or more flag-like tokens (starting with `-`) | [Options](/pattern-syntax/placeholders/#options-opts) |
| `<vars>` | `env <vars> <cmd>` | Absorbs zero or more `KEY=VALUE` tokens | [Variables](/pattern-syntax/placeholders/#variables-vars) |
| `<path:name>` | `cat <path:sensitive>` | Matches against a named list of paths from `definitions` | [Path References](/pattern-syntax/placeholders/#path-references-pathname) |
| `<var:name>` | `cmd <var:ids>` | Matches against a typed variable definition | [Variable References](/pattern-syntax/placeholders/#variable-references-varname) |

## Pattern Structure

Expand Down
85 changes: 78 additions & 7 deletions docs/src/content/docs/pattern-syntax/placeholders.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ sidebar:

Tokens wrapped in `<...>` are **placeholders** — special tokens that match dynamically rather than by exact string comparison.

| Placeholder | Description |
| ------------------------------------------ | -------------------------------------------------------- |
| [`<cmd>`](#command-cmd) | Captures the wrapped command for further rule evaluation |
| [`<opts>`](#options-opts) | Absorbs zero or more flag-like tokens |
| [`<vars>`](#variables-vars) | Absorbs zero or more `KEY=VALUE` tokens |
| [`<path:name>`](#path-references-pathname) | Matches against a named list of paths |
| Placeholder | Description |
| -------------------------------------------- | -------------------------------------------------------- |
| [`<cmd>`](#command-cmd) | Captures the wrapped command for further rule evaluation |
| [`<opts>`](#options-opts) | Absorbs zero or more flag-like tokens |
| [`<vars>`](#variables-vars) | Absorbs zero or more `KEY=VALUE` tokens |
| [`<path:name>`](#path-references-pathname) | Matches against a named list of paths |
| [`<var:name>`](#variable-references-varname) | Matches against a typed variable definition |

## Command (`<cmd>`)

Expand Down Expand Up @@ -146,6 +147,76 @@ If a pattern references a path name that is not defined in `definitions.paths`,
- deny: 'cat <path:sensitive>'
```

## Variable References (`<var:name>`)

The `<var:name>` placeholder matches a command argument against a **typed variable definition** in the [`definitions.vars`](/configuration/schema/#definitionsvars) block.

### Defining Variables

Each variable has an optional `type` (default: `literal`) and a list of `values`:

```yaml
definitions:
vars:
instance-ids:
values:
- i-abc123
- i-def456
- i-ghi789
test-script:
type: path
values:
- ./tests/run
```

### Variable Types

| Type | Matching behavior |
| --------- | ------------------------------------------------------------------------- |
| `literal` | Exact string match (default) |
| `path` | Canonicalize both sides before comparison, fallback to path normalization |

### Using Variable References

```yaml
rules:
- allow: aws ec2 terminate-instances --instance-ids <var:instance-ids>
- allow: bash <var:test-script>
```

| Command | Rule | Result |
| ------------------------------------------------------ | ------------------------------------------------ | ---------------------------- |
| `aws ec2 terminate-instances --instance-ids i-abc123` | `allow: "... --instance-ids <var:instance-ids>"` | Allowed |
| `aws ec2 terminate-instances --instance-ids i-UNKNOWN` | `allow: "... --instance-ids <var:instance-ids>"` | No match |
| `bash ./tests/run` | `allow: "bash <var:test-script>"` | Allowed |
| `bash tests/run` | `allow: "bash <var:test-script>"` | Allowed (path normalization) |

### Path Type Normalization

When `type: path` is set, both the command argument and the defined values are **canonicalized** (resolved to absolute paths via the filesystem). If the path does not exist on disk, logical normalization is used as a fallback (`.` removal and `..` resolution).

This handles cases where the same file is referenced with different path forms:

```yaml
definitions:
vars:
test-script:
type: path
values:
- ./tests/run
```

| Command | Matches `<var:test-script>` |
| --------------------------- | --------------------------- |
| `bash tests/run` | Yes |
| `bash ./tests/run` | Yes |
| `bash ./tests/../tests/run` | Yes |
| `bash ./scripts/deploy` | No |

### Undefined Variable Names

If a pattern references a variable name that is not defined in `definitions.vars`, the pattern **never matches**.

## Combining Placeholders

Placeholders can be combined to handle complex wrapper patterns:
Expand Down Expand Up @@ -173,7 +244,7 @@ In the `find` wrapper example, `\\;` is a backslash-escaped semicolon in YAML. T
## Restrictions

- `<cmd>` captures one or more tokens; it tries all possible split points to find a valid wrapped command
- Optional groups and path references are not supported inside wrapper patterns
- Optional groups, path references, and variable references are not supported inside wrapper patterns

## Related

Expand Down
44 changes: 43 additions & 1 deletion 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

Four context variables are available inside `when` expressions:
Five context variables are available inside `when` expressions:

### `env` — Environment variables

Expand Down Expand Up @@ -94,6 +94,48 @@ 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.

### `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.

```yaml
definitions:
vars:
instance-ids:
values:
- i-abc123
- i-prod-001

rules:
# Deny terminating production instances, allow others
- deny: 'aws ec2 terminate-instances --instance-ids <var:instance-ids>'
when: "vars['instance-ids'] == 'i-prod-001'"
- allow: 'aws ec2 terminate-instances --instance-ids <var:instance-ids>'
```

In this example, when the command matches `<var:instance-ids>`, the actual token value (e.g., `i-prod-001`) is captured into `vars['instance-ids']`. The `when` clause can then inspect this value to make conditional decisions.

```yaml
definitions:
vars:
regions:
type: literal
values:
- us-east-1
- eu-west-1
- ap-southeast-1

rules:
# Deny AWS operations in US regions, allow others
- deny: 'aws --region <var:regions> *'
when: "has(vars.regions) && vars.regions.startsWith('us-')"
- allow: 'aws --region <var:regions> *'
```

:::note
The `vars` map only contains entries for `<var:name>` placeholders that were present in the matched pattern. If the pattern doesn't use `<var:name>`, the `vars` map is empty. Use `has(vars.name)` to safely check for a variable before accessing it.
:::

## Operators

CEL supports standard operators for building conditions:
Expand Down
45 changes: 45 additions & 0 deletions schema/runok.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,16 @@
"$ref": "#/$defs/SandboxPreset"
}
},
"vars": {
"description": "Typed variable definitions referenced by `<var:name>` in rule patterns.",
"type": [
"object",
"null"
],
"additionalProperties": {
"$ref": "#/$defs/VarDefinition"
}
},
"wrappers": {
"description": "Wrapper command patterns for recursive evaluation (e.g., `sudo <cmd>`).",
"type": [
Expand Down Expand Up @@ -374,6 +384,41 @@
]
}
}
},
"VarDefinition": {
"description": "A typed variable definition with a list of allowed values.",
"type": "object",
"properties": {
"type": {
"description": "The type of this variable (default: `literal`).",
"$ref": "#/$defs/VarType"
},
"values": {
"description": "Allowed values for this variable.",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"values"
]
},
"VarType": {
"description": "Type of a variable definition, controlling how values are matched.",
"oneOf": [
{
"description": "Exact string match (default).",
"type": "string",
"const": "literal"
},
{
"description": "Path match: canonicalize both sides before comparison,\nfalling back to `normalize_path` when the file does not exist.",
"type": "string",
"const": "path"
}
]
}
}
}
Loading
Loading