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
17 changes: 14 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches: [main]
pull_request:
branches: ["**"]

# A branch push is covered by its pull_request run; only `main` needs a push
# run. And cancel superseded runs on the same ref so rapid pushes don't pile up.
Expand Down Expand Up @@ -56,9 +57,19 @@ jobs:
find xff -type f \( -name '*.cc' -o -name '*.h' \) -print0 \
| xargs -0 tools/clang_format.sh --dry-run -Werror

# trunk (https://trunk.io) runs the heavier linters: buildifier, markdownlint,
# prettier, yamllint, the security scanners, and clang-tidy (local only;
# CI-skipped without a compile DB, so CI relies on the bazel -Werror gate).
# Mirrors helly25/mbo.
trunk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Trunk Check
uses: trunk-io/trunk-action@v1

# pre-commit hooks (clang-format, shfmt, shellcheck, actionlint, and the
# repo-policy text checks incl. no-em-dashes). Mirrors helly25/mbo. clang-tidy
# and the heavier linters are trunk's job, added separately.
# repo-policy text checks incl. no-em-dashes). Mirrors helly25/mbo.
pre-commit:
runs-on: ubuntu-latest
steps:
Expand All @@ -73,7 +84,7 @@ jobs:
# workflow job is wired into `needs`, so a newly added job cannot silently
# escape the gate. Mirrors helly25/mbo.
done:
needs: [test, pre-commit]
needs: [test, pre-commit, trunk]
if: always()
runs-on: ubuntu-latest
steps:
Expand Down
9 changes: 9 additions & 0 deletions .trunk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
*out
*logs
*actions
*notifications
*tools
plugins
user_trunk.yaml
user.yaml
tmp
17 changes: 17 additions & 0 deletions .trunk/configs/.markdownlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Autoformatter friendly markdownlint config (all formatting rules disabled)
default: true
blank_lines: false
bullet: false
html: false
indentation: false
line_length: false
spaces: false
url: false
whitespace: false
# CHANGELOG.md uses one top-level heading per version.
single-h1: false
# xff design docs use bold lead-ins as inline labels (not section headings) and a
# few language-less fenced blocks (ASCII trees / console output). Disable the two
# stylistic rules that flag those rather than churn the authored prose.
MD036: false # no-emphasis-as-heading
MD040: false # fenced-code-language
11 changes: 11 additions & 0 deletions .trunk/configs/.yamllint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
rules:
# `quoted-strings: only-when-needed` collides with prettier (which prefers
# double quotes for many string values) and creates noise without any
# functional difference. Disable it; yamllint's other checks remain.
quoted-strings: disable
empty-values:
forbid-in-block-mappings: true
forbid-in-flow-mappings: true
key-duplicates: {}
octal-values:
forbid-implicit-octal: true
81 changes: 81 additions & 0 deletions .trunk/trunk.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# This file controls the behavior of Trunk: https://docs.trunk.io/cli
# To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml
version: 0.1
cli:
version: 1.25.0
plugins:
sources:
- id: trunk
ref: v1.10.2
uri: https://github.com/trunk-io/plugins
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
runtimes:
enabled:
- go@1.21.0
- node@22.16.0
- python@3.14.4
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
lint:
definitions:
- name: buildifier
files: [starlark, bazel-build, bazel-workspace]
tools: [buildifier]
commands:
- name: fix
run: buildifier --lint=fix "${target}"
output: rewrite
cache_results: true
formatter: true
in_place: true
batch: true
success_codes: [0]
- name: warn
run: buildifier --lint=warn --format=json --mode=check "${target}"
# Custom parser type defined in the trunk cli to handle buildifier's JSON output.
output: buildifier
cache_results: true
batch: true
success_codes: [0]
suggest_if: files_present
direct_configs:
- .buildifier.json
# Not a native buildifier construct, but useful for 'addTables', see test file
- .buildifier-tables.json
environment:
- name: PATH
list: ["${linter}"]
known_good_version: 7.1.0
version_command:
parse_regex: ${semver}
run: buildifier --version
# clang-tidy is fundamentally useless without a compile_commands.json. Local
# devs generate it via `bazel run @hedron_compile_commands//:refresh_all`;
# CI does not. Override the plugin's `run_when` to skip CI so trunk's CI
# job doesn't bomb on "<header> file not found".
- name: clang-tidy
commands:
- name: lint
output: llvm
run: clang-tidy --export-fixes=${tmpfile} ${target} -p ${compile_commands_dir}
success_codes: [0, 1]
cache_results: true
run_from: ${compile_command}
read_output_from: tmp_file
run_when: [cli, monitor]
max_concurrency: 4
enabled:
- buildifier@8.5.1
- checkov@3.3.1
- clang-tidy@16.0.3
- git-diff-check
- markdownlint@0.49.0
- prettier@3.8.4
- trivy@0.70.0
- trufflehog@3.95.3
- yamllint@1.38.0
actions:
enabled:
- trunk-announce
- trunk-check-pre-push
- trunk-fmt-pre-commit
- trunk-upgrade-available
6 changes: 3 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ shipped one way but not yet settled.

- **`--timezone` scope and spelling.**
Shipped (config phase D4a) as `--timezone=ZONE`: overrides the zone used both
to *interpret* time-string arguments (`-newerXt`) and to *format* time fields
to _interpret_ time-string arguments (`-newerXt`) and to _format_ time fields
(`{atime}`/`{mtime}`/`{ctime}`/`{btime}`). Accepts `local`/empty,
`utc`/`z`/`zulu`, and IANA names (`America/New_York`); an unknown zone is a
usage error. The companion `--time-format=NAME` selector shipped alongside it
Expand All @@ -30,8 +30,8 @@ shipped one way but not yet settled.
- **Project `.xffrc` per-entry subtree scoping (deferred).**
The cascade (config phase E2a) reads, for each search root, every `.xffrc` from
the filesystem root down to the root's directory (ancestors), applied run-level.
The design (design-config.md L41, L56-58) also wants gitignore-style *subtree*
scoping: a `.xffrc` in a directory *below* a root should apply only to that
The design (design-config.md L41, L56-58) also wants gitignore-style _subtree_
scoping: a `.xffrc` in a directory _below_ a root should apply only to that
subtree -- which means config resolution would vary per directory during the
walk, an architectural change (per-entry layering on the traversal hot path).
Deferred until a real need appears; the ancestor cascade already covers the
Expand Down
45 changes: 23 additions & 22 deletions docs/design-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,30 @@
A find-compatible tool that can run `-exec`/`-capture` is, by construction, a
code-execution engine. A config system for it must make the common case
convenient **without** ever letting an untrusted, repo-local file silently run
commands. The design goal, in the user's words: *"by default there is no
security issue."*
commands. The design goal, in the user's words: _"by default there is no
security issue."_

The system has **four layers** with a strict trust order, and a single
root-owned **policy** that decides how much each lower (less-trusted) layer may
do. The policy is the keystone: absence of an admin policy yields the *safe*
do. The policy is the keystone: absence of an admin policy yields the _safe_
default, never an unsafe one.

## The four layers

Lowest → highest precedence; each overrides values set by an earlier one, and
the **CLI always wins** (typing a flag is explicit consent).

| # | Layer | Owner / trust | Format |
|---|-------------|--------------------------|-------------------------|
| 1 | built-in | the binary | (none) |
| 2 | **system** | root - the trust anchor | INI (structured policy) |
| 3 | **user** | the user - trusted as-is | `.xffrc` grammar |
| 4 | **project** | repo - **untrusted** | `.xffrc` grammar |
| 5 | **CLI** | explicit user consent | flags |
| # | Layer | Owner / trust | Format |
| --- | ----------- | ------------------------ | ----------------------- |
| 1 | built-in | the binary | (none) |
| 2 | **system** | root - the trust anchor | INI (structured policy) |
| 3 | **user** | the user - trusted as-is | `.xffrc` grammar |
| 4 | **project** | repo - **untrusted** | `.xffrc` grammar |
| 5 | **CLI** | explicit user consent | flags |

(Locations are detailed under Discovery below.)

- **System** holds global defaults *and* the per-flag **policy** (below). Only
- **System** holds global defaults _and_ the per-flag **policy** (below). Only
this root-owned layer may grant capability; lower layers can never widen their
own permissions.
- **User** is trusted as the user themselves - data and armed `[exec]`/named
Expand All @@ -43,7 +43,7 @@ the **CLI always wins** (typing a flag is explicit consent).
subtree - per `design.md` §132) and is **capped by the policy**: sensitive
flags are inert unless the system policy grants the project layer.
- Provenance: every setting records `enum{unset, system, user, project, cli}`
(not `bool`), so resolution is last-non-unset-wins and *unset**off*
(not `bool`), so resolution is last-non-unset-wins and _unset__off_
(`design.md` §102).

## Discovery & loading
Expand All @@ -57,12 +57,12 @@ the **CLI always wins** (typing a flag is explicit consent).
filesystem root down to the entry's directory contributes to that subtree
(cascade). Ownership-gated even for data-only settings (`design.md` §132).
- **Explicit:** `--xffrc=FILE` loads a specific file and **arms** its
sensitive/named blocks (ownership-gated). Naming the file *is* the
sensitive/named blocks (ownership-gated). Naming the file _is_ the
authorization - no trust DB, no hashes. This replaces the working
`--config <file>` spelling in `design.md` §130 (which now collides with
`--config=NAME` below).
- **`--no-config`:** skip the user + project layers (and system *defaults*);
pure CLI + built-ins. The system *policy* still bounds anything that would
- **`--no-config`:** skip the user + project layers (and system _defaults_);
pure CLI + built-ins. The system _policy_ still bounds anything that would
otherwise be loaded - moot when nothing is.

## Formats
Expand All @@ -86,7 +86,7 @@ user.allow = @sensitive # user may arm -exec/-capture f
```

- Class tokens `@safe` / `@sensitive` / `@destructive` expand to every flag with
that registry `safety` classification (`design.md` §134), so a *new*
that registry `safety` classification (`design.md` §134), so a _new_
sensitive flag is covered without editing the policy - **no gaps as the tool
grows.**
- `deny` beats `allow` on conflict. A layer can be tightened (deny a safe flag)
Expand All @@ -110,15 +110,15 @@ find: --warn # only under the find style
expressive than bazel's single `command:config` - a custom config can differ
per style.
- A line is inert (parsed to AST, never executed). Sensitive flags within it are
subject to the policy of the *layer the file belongs to*.
subject to the policy of the _layer the file belongs to_.

## Capability policy (per-flag, safe-by-default)

The user-chosen model is **per-flag allow/deny**, anchored to the registry's
built-in `safety` classification so the safe default needs no admin file:

| Class | Project | User | System |
|---------------|---------|-------|--------|
| ------------- | ------- | ----- | ------ |
| `safe` | allow | allow | allow |
| `sensitive` | deny | allow | allow |
| `destructive` | deny | allow | allow |
Expand All @@ -143,11 +143,12 @@ built-in `safety` classification so the safe default needs no admin file:
xff: ignoring `-exec` from ./.xffrc - denied for the project layer [security];
arm it explicitly with `--xffrc=./.xffrc`, or allow in /etc/xff.ini.
```

- `--explain` lists every config source consulted, what each contributed, and
every gate that fired with its *why*.
every gate that fired with its _why_.
- Interacts with `--safe` (`design.md` §136): `--safe` is an orthogonal,
user-facing hard-refuse of destructive/dangerous ops regardless of layer; the
policy governs *where settings may originate*. Both can refuse; both explain.
policy governs _where settings may originate_. Both can refuse; both explain.

## CLI selectors

Expand Down Expand Up @@ -203,15 +204,15 @@ ln -s xff find && ./find . -println # → error: -println unknown (find s
- `/etc/xff.d/` drop-in dir now, or single `/etc/xff.ini` first?
- Class tokens (`@sensitive`) in the `[policy]` lists from day one, or per-flag
names only until proven necessary?
- Should `--no-config` also drop system *defaults*, or only user + project?
- Should `--no-config` also drop system _defaults_, or only user + project?
- Windows: system path (`%PROGRAMDATA%\xff`) and user path mapping (deferred with
the rest of Windows support - `design.md` §Non-Goals).

## Prior art

- **ripgrep:** config only via an explicit `RIPGREP_CONFIG_PATH` env var - no
ambient discovery. (We add cascading project files, but gate them.)
- **fd:** ships *no* config file at all - the maximally safe stance.
- **fd:** ships _no_ config file at all - the maximally safe stance.
- **sudoers / ssh:** a root-owned system file defines policy that user files
cannot override - the model for our system-over-project trust anchor.
- **bazel:** `--config=NAME` named configs and rc layering - adopted, minus
Expand Down
Loading
Loading