tree-police 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tree_police-0.2.0/.dockerignore +3 -0
- tree_police-0.2.0/.envrc +1 -0
- tree_police-0.2.0/.github/workflows/cli-release.yml +112 -0
- tree_police-0.2.0/.github/workflows/pypi-release.yml +152 -0
- tree_police-0.2.0/.github/workflows/release.yml +81 -0
- tree_police-0.2.0/.gitignore +12 -0
- tree_police-0.2.0/.markdownlint-cli2.jsonc +16 -0
- tree_police-0.2.0/.pre-commit-config.yaml +57 -0
- tree_police-0.2.0/.pre-commit-hooks.yaml +24 -0
- tree_police-0.2.0/AGENTS.md +191 -0
- tree_police-0.2.0/Cargo.lock +1947 -0
- tree_police-0.2.0/Cargo.toml +131 -0
- tree_police-0.2.0/Dockerfile +4 -0
- tree_police-0.2.0/LICENSE-APACHE +202 -0
- tree_police-0.2.0/LICENSE-MIT +21 -0
- tree_police-0.2.0/PKG-INFO +365 -0
- tree_police-0.2.0/README.md +346 -0
- tree_police-0.2.0/THIRD-PARTY-LICENSES.md +2520 -0
- tree_police-0.2.0/about.hbs +28 -0
- tree_police-0.2.0/about.toml +14 -0
- tree_police-0.2.0/docs/logo.jpg +0 -0
- tree_police-0.2.0/docs/pre-commit-example.md +132 -0
- tree_police-0.2.0/docs/writing-queries.md +261 -0
- tree_police-0.2.0/flake.lock +61 -0
- tree_police-0.2.0/flake.nix +104 -0
- tree_police-0.2.0/justfile +130 -0
- tree_police-0.2.0/pyproject.toml +36 -0
- tree_police-0.2.0/python/LICENSE-APACHE +202 -0
- tree_police-0.2.0/python/README.md +346 -0
- tree_police-0.2.0/python/THIRD-PARTY-LICENSES.md +2520 -0
- tree_police-0.2.0/python/pyproject.toml +36 -0
- tree_police-0.2.0/queries/README.md +110 -0
- tree_police-0.2.0/queries/rules-py.scm +258 -0
- tree_police-0.2.0/queries/rules-tf.scm +240 -0
- tree_police-0.2.0/renovate.json +29 -0
- tree_police-0.2.0/src/config.rs +425 -0
- tree_police-0.2.0/src/lib.rs +22 -0
- tree_police-0.2.0/src/main.rs +301 -0
- tree_police-0.2.0/src/registry.rs +1007 -0
- tree_police-0.2.0/src/report.rs +383 -0
- tree_police-0.2.0/src/rules.rs +341 -0
- tree_police-0.2.0/src/scan.rs +432 -0
- tree_police-0.2.0/src/severity.rs +73 -0
- tree_police-0.2.0/src/suppress.rs +144 -0
- tree_police-0.2.0/src/tree_view.rs +64 -0
- tree_police-0.2.0/tests/cli.rs +438 -0
- tree_police-0.2.0/tests/fixtures/python/clean.py +5 -0
- tree_police-0.2.0/tests/fixtures/python/security_violations.py +1 -0
- tree_police-0.2.0/tests/fixtures/python/violations.py +2 -0
- tree_police-0.2.0/tests/fixtures/terraform/clean.tf +33 -0
- tree_police-0.2.0/tests/fixtures/terraform/convention_violations.tf +76 -0
- tree_police-0.2.0/tests/fixtures/terraform/naming_violations.tf +23 -0
- tree_police-0.2.0/tests/packaging.rs +18 -0
- tree_police-0.2.0/tests/query-cases/python/forbidden-patterns.py +49 -0
- tree_police-0.2.0/tests/query-cases/python/security-patterns.py +28 -0
- tree_police-0.2.0/tests/query-cases/python/temporal-patterns.py +26 -0
- tree_police-0.2.0/tests/query-cases/python/web-patterns.py +25 -0
- tree_police-0.2.0/tests/query_cases.rs +165 -0
tree_police-0.2.0/.envrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
use flake
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
name: CLI release
|
|
2
|
+
|
|
3
|
+
# Only on a version tag -- unlike the Docker image (release.yml), which
|
|
4
|
+
# republishes `:latest` on every push to main, a GitHub Release should be a
|
|
5
|
+
# deliberate, versioned act.
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build-linux:
|
|
15
|
+
# Cross-compiled via Nix (not Docker buildx/QEMU) on one amd64 runner --
|
|
16
|
+
# same musl-static binaries release.yml builds for the Docker image.
|
|
17
|
+
# Everything (all ~90 grammars, the embedded query files) is statically
|
|
18
|
+
# linked in; there's no separate grammars folder to ship.
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
strategy:
|
|
21
|
+
matrix:
|
|
22
|
+
include:
|
|
23
|
+
- nix_package: tree-police-static-x86_64
|
|
24
|
+
target: x86_64-unknown-linux-musl
|
|
25
|
+
- nix_package: tree-police-static-aarch64
|
|
26
|
+
target: aarch64-unknown-linux-musl
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- uses: DeterminateSystems/nix-installer-action@main
|
|
30
|
+
- uses: DeterminateSystems/magic-nix-cache-action@main
|
|
31
|
+
|
|
32
|
+
- name: Build (musl cross)
|
|
33
|
+
run: nix build ".#${{ matrix.nix_package }}" -o result
|
|
34
|
+
|
|
35
|
+
- name: Package
|
|
36
|
+
run: |
|
|
37
|
+
mkdir -p dist
|
|
38
|
+
install -m755 result/bin/tree-police tree-police
|
|
39
|
+
tar czf "dist/tree-police-${{ matrix.target }}.tar.gz" tree-police
|
|
40
|
+
|
|
41
|
+
- uses: actions/upload-artifact@v4
|
|
42
|
+
with:
|
|
43
|
+
name: tree-police-${{ matrix.target }}
|
|
44
|
+
path: dist/*.tar.gz
|
|
45
|
+
|
|
46
|
+
build-macos:
|
|
47
|
+
# Native builds, one job per arch -- GH's macos-15-intel runner is
|
|
48
|
+
# Intel, macos-14 is Apple Silicon (GH's "-latest"/bare "macos-NN"
|
|
49
|
+
# labels default to Apple Silicon now; Intel needs the explicit
|
|
50
|
+
# "-intel" suffix), so `cargo build --release` with no explicit
|
|
51
|
+
# --target already produces the right binary for each; no cross-compile
|
|
52
|
+
# or universal-binary step needed. Not Nix/musl: macOS has no musl
|
|
53
|
+
# equivalent, dynamic linking against system libs is normal here.
|
|
54
|
+
strategy:
|
|
55
|
+
matrix:
|
|
56
|
+
include:
|
|
57
|
+
- os: macos-15-intel
|
|
58
|
+
target: x86_64-apple-darwin
|
|
59
|
+
- os: macos-14
|
|
60
|
+
target: aarch64-apple-darwin
|
|
61
|
+
runs-on: ${{ matrix.os }}
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/checkout@v4
|
|
64
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
65
|
+
|
|
66
|
+
- name: Build
|
|
67
|
+
run: cargo build --release
|
|
68
|
+
|
|
69
|
+
- name: Package
|
|
70
|
+
run: |
|
|
71
|
+
mkdir -p dist
|
|
72
|
+
install -m755 target/release/tree-police tree-police
|
|
73
|
+
tar czf "dist/tree-police-${{ matrix.target }}.tar.gz" tree-police
|
|
74
|
+
|
|
75
|
+
- uses: actions/upload-artifact@v4
|
|
76
|
+
with:
|
|
77
|
+
name: tree-police-${{ matrix.target }}
|
|
78
|
+
path: dist/*.tar.gz
|
|
79
|
+
|
|
80
|
+
release:
|
|
81
|
+
needs: [build-linux, build-macos]
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
steps:
|
|
84
|
+
- uses: actions/checkout@v4
|
|
85
|
+
- uses: DeterminateSystems/nix-installer-action@main
|
|
86
|
+
- uses: DeterminateSystems/magic-nix-cache-action@main
|
|
87
|
+
|
|
88
|
+
# Same CycloneDX SBOM attached to the Docker image (release.yml) --
|
|
89
|
+
# describes the Cargo dependency graph, which is identical across all
|
|
90
|
+
# four binaries above, so one SBOM covers the whole release.
|
|
91
|
+
- name: Build SBOM (nix)
|
|
92
|
+
run: |
|
|
93
|
+
nix build .#sbom -o sbom-result
|
|
94
|
+
cp sbom-result/bom.json tree-police.cdx.json
|
|
95
|
+
|
|
96
|
+
- uses: actions/download-artifact@v4
|
|
97
|
+
with:
|
|
98
|
+
path: dist
|
|
99
|
+
pattern: tree-police-*
|
|
100
|
+
merge-multiple: true
|
|
101
|
+
|
|
102
|
+
- name: Checksums
|
|
103
|
+
run: (cd dist && sha256sum -- *.tar.gz > checksums.txt)
|
|
104
|
+
|
|
105
|
+
- name: Create release
|
|
106
|
+
run: |
|
|
107
|
+
gh release create "${{ github.ref_name }}" \
|
|
108
|
+
dist/*.tar.gz dist/checksums.txt tree-police.cdx.json \
|
|
109
|
+
--title "${{ github.ref_name }}" \
|
|
110
|
+
--generate-notes
|
|
111
|
+
env:
|
|
112
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
name: PyPI release
|
|
2
|
+
|
|
3
|
+
# Wheels that wrap the `tree-police` binary (maturin `bindings = "bin"`, see
|
|
4
|
+
# python/pyproject.toml), so `pip install tree-police` / `uv tool install
|
|
5
|
+
# tree-police` works without Docker or a Rust toolchain. Published only from
|
|
6
|
+
# a version tag (same trigger as cli-release.yml). Pull requests that touch
|
|
7
|
+
# the packaging, and manual runs, build and smoke-test every wheel without
|
|
8
|
+
# publishing.
|
|
9
|
+
on:
|
|
10
|
+
push:
|
|
11
|
+
tags: ["v*"]
|
|
12
|
+
pull_request:
|
|
13
|
+
paths:
|
|
14
|
+
- pyproject.toml
|
|
15
|
+
- python/**
|
|
16
|
+
- .pre-commit-hooks.yaml
|
|
17
|
+
- .github/workflows/pypi-release.yml
|
|
18
|
+
workflow_dispatch:
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
linux:
|
|
25
|
+
# manylinux wheels are for glibc distros, musllinux ones for Alpine & co.
|
|
26
|
+
# Not the Nix musl-static build cli-release.yml uses: pip only installs a
|
|
27
|
+
# wheel whose platform tag matches the host, so glibc hosts need a
|
|
28
|
+
# manylinux-tagged build from maturin's manylinux containers. aarch64
|
|
29
|
+
# builds natively on GitHub's arm runner rather than cross-compiling the
|
|
30
|
+
# C/C++ grammars.
|
|
31
|
+
strategy:
|
|
32
|
+
fail-fast: false
|
|
33
|
+
matrix:
|
|
34
|
+
include:
|
|
35
|
+
- { runner: ubuntu-latest, target: x86_64, manylinux: "2_28" }
|
|
36
|
+
- { runner: ubuntu-24.04-arm, target: aarch64, manylinux: "2_28" }
|
|
37
|
+
- { runner: ubuntu-latest, target: x86_64, manylinux: musllinux_1_2 }
|
|
38
|
+
- { runner: ubuntu-24.04-arm, target: aarch64, manylinux: musllinux_1_2 }
|
|
39
|
+
runs-on: ${{ matrix.runner }}
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v4
|
|
42
|
+
|
|
43
|
+
- name: Build wheel
|
|
44
|
+
uses: PyO3/maturin-action@v1
|
|
45
|
+
with:
|
|
46
|
+
target: ${{ matrix.target }}
|
|
47
|
+
manylinux: ${{ matrix.manylinux }}
|
|
48
|
+
working-directory: python
|
|
49
|
+
args: --release --out ../dist
|
|
50
|
+
sccache: "true"
|
|
51
|
+
|
|
52
|
+
# The runner is glibc, so only the manylinux wheel installs on it.
|
|
53
|
+
- name: Smoke test
|
|
54
|
+
if: matrix.manylinux != 'musllinux_1_2'
|
|
55
|
+
run: |
|
|
56
|
+
python3 -m venv .venv
|
|
57
|
+
.venv/bin/pip install --no-index --find-links dist tree-police
|
|
58
|
+
.venv/bin/tree-police --version
|
|
59
|
+
.venv/bin/tree-police --list-rules >/dev/null
|
|
60
|
+
|
|
61
|
+
# The `tree-police` hook (.pre-commit-hooks.yaml) installs the root
|
|
62
|
+
# pyproject.toml stub, which pins this version on PyPI. Point pip at the
|
|
63
|
+
# wheel just built instead, so the hook is tested before publishing.
|
|
64
|
+
- name: Pre-commit hook smoke test
|
|
65
|
+
if: matrix.target == 'x86_64' && matrix.manylinux == '2_28'
|
|
66
|
+
env:
|
|
67
|
+
PIP_FIND_LINKS: ${{ github.workspace }}/dist
|
|
68
|
+
run: |
|
|
69
|
+
.venv/bin/pip install pre-commit
|
|
70
|
+
.venv/bin/pre-commit try-repo . tree-police \
|
|
71
|
+
--files tests/fixtures/python/clean.py tests/fixtures/terraform/clean.tf
|
|
72
|
+
|
|
73
|
+
- uses: actions/upload-artifact@v4
|
|
74
|
+
with:
|
|
75
|
+
name: wheels-linux-${{ matrix.target }}-${{ matrix.manylinux }}
|
|
76
|
+
path: dist/*.whl
|
|
77
|
+
|
|
78
|
+
macos:
|
|
79
|
+
# Same native Intel / Apple Silicon split as cli-release.yml.
|
|
80
|
+
strategy:
|
|
81
|
+
fail-fast: false
|
|
82
|
+
matrix:
|
|
83
|
+
include:
|
|
84
|
+
- { runner: macos-15-intel, target: x86_64 }
|
|
85
|
+
- { runner: macos-14, target: aarch64 }
|
|
86
|
+
runs-on: ${{ matrix.runner }}
|
|
87
|
+
env:
|
|
88
|
+
# Otherwise the wheel is tagged for the build host's macOS (a local
|
|
89
|
+
# build on macOS 14 came out as macosx_14_0) and pip refuses it on
|
|
90
|
+
# anything older. 11.0 is the first Apple Silicon release.
|
|
91
|
+
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.target == 'x86_64' && '10.13' || '11.0' }}
|
|
92
|
+
steps:
|
|
93
|
+
- uses: actions/checkout@v4
|
|
94
|
+
|
|
95
|
+
- name: Build wheel
|
|
96
|
+
uses: PyO3/maturin-action@v1
|
|
97
|
+
with:
|
|
98
|
+
target: ${{ matrix.target }}
|
|
99
|
+
working-directory: python
|
|
100
|
+
args: --release --out ../dist
|
|
101
|
+
sccache: "true"
|
|
102
|
+
|
|
103
|
+
- name: Smoke test
|
|
104
|
+
run: |
|
|
105
|
+
python3 -m venv .venv
|
|
106
|
+
.venv/bin/pip install --no-index --find-links dist tree-police
|
|
107
|
+
.venv/bin/tree-police --version
|
|
108
|
+
.venv/bin/tree-police --list-rules >/dev/null
|
|
109
|
+
|
|
110
|
+
- uses: actions/upload-artifact@v4
|
|
111
|
+
with:
|
|
112
|
+
name: wheels-macos-${{ matrix.target }}
|
|
113
|
+
path: dist/*.whl
|
|
114
|
+
|
|
115
|
+
sdist:
|
|
116
|
+
# Fallback for platforms without a wheel (needs Rust + a C/C++ compiler
|
|
117
|
+
# on the installing machine).
|
|
118
|
+
runs-on: ubuntu-latest
|
|
119
|
+
steps:
|
|
120
|
+
- uses: actions/checkout@v4
|
|
121
|
+
|
|
122
|
+
- name: Build sdist
|
|
123
|
+
uses: PyO3/maturin-action@v1
|
|
124
|
+
with:
|
|
125
|
+
command: sdist
|
|
126
|
+
working-directory: python
|
|
127
|
+
args: --out ../dist
|
|
128
|
+
|
|
129
|
+
- uses: actions/upload-artifact@v4
|
|
130
|
+
with:
|
|
131
|
+
name: sdist
|
|
132
|
+
path: dist/*.tar.gz
|
|
133
|
+
|
|
134
|
+
publish:
|
|
135
|
+
# PyPI trusted publishing (OIDC) -- no API token stored in the repo. Needs
|
|
136
|
+
# a one-time trusted-publisher entry on PyPI for this repo, this workflow
|
|
137
|
+
# file and the `pypi` environment.
|
|
138
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
139
|
+
needs: [linux, macos, sdist]
|
|
140
|
+
runs-on: ubuntu-latest
|
|
141
|
+
environment:
|
|
142
|
+
name: pypi
|
|
143
|
+
url: https://pypi.org/project/tree-police/
|
|
144
|
+
permissions:
|
|
145
|
+
id-token: write
|
|
146
|
+
steps:
|
|
147
|
+
- uses: actions/download-artifact@v4
|
|
148
|
+
with:
|
|
149
|
+
path: dist
|
|
150
|
+
merge-multiple: true
|
|
151
|
+
|
|
152
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
name: Build and publish image
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
packages: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: DeterminateSystems/nix-installer-action@main
|
|
19
|
+
- uses: DeterminateSystems/magic-nix-cache-action@main
|
|
20
|
+
|
|
21
|
+
# Cross-compiled via Nix, not Docker buildx/QEMU: each is a genuine
|
|
22
|
+
# musl-static binary for its target, built once on this amd64 runner.
|
|
23
|
+
- name: Build static binaries (musl cross, both arches)
|
|
24
|
+
run: |
|
|
25
|
+
nix build .#tree-police-static-x86_64 -o result-amd64
|
|
26
|
+
nix build .#tree-police-static-aarch64 -o result-arm64
|
|
27
|
+
|
|
28
|
+
- name: Stage build context
|
|
29
|
+
run: |
|
|
30
|
+
mkdir -p dist/amd64 dist/arm64
|
|
31
|
+
install -m755 result-amd64/bin/tree-police dist/amd64/tree-police
|
|
32
|
+
install -m755 result-arm64/bin/tree-police dist/arm64/tree-police
|
|
33
|
+
|
|
34
|
+
# CycloneDX SBOM of the exact dependency set in Cargo.lock, built the
|
|
35
|
+
# same reproducible way as the binaries (see flake.nix's `sbom`
|
|
36
|
+
# package). One SBOM covers both architectures -- it describes the
|
|
37
|
+
# Cargo dependency graph, not per-arch binary contents.
|
|
38
|
+
- name: Build SBOM (nix)
|
|
39
|
+
run: nix build .#sbom -o sbom-result
|
|
40
|
+
|
|
41
|
+
- uses: docker/login-action@v3
|
|
42
|
+
with:
|
|
43
|
+
registry: ghcr.io
|
|
44
|
+
username: ${{ github.actor }}
|
|
45
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
46
|
+
|
|
47
|
+
- uses: docker/metadata-action@v5
|
|
48
|
+
id: meta
|
|
49
|
+
with:
|
|
50
|
+
images: ghcr.io/${{ github.repository }}
|
|
51
|
+
tags: |
|
|
52
|
+
type=raw,value=latest,enable={{is_default_branch}}
|
|
53
|
+
type=semver,pattern={{version}}
|
|
54
|
+
|
|
55
|
+
# Only sets up the container driver so buildx can assemble a
|
|
56
|
+
# multi-platform manifest list; no QEMU needed since the Dockerfile
|
|
57
|
+
# just COPYs a prebuilt static binary per `TARGETARCH` -- no RUN step
|
|
58
|
+
# ever executes in the target architecture.
|
|
59
|
+
- uses: docker/setup-buildx-action@v3
|
|
60
|
+
|
|
61
|
+
- uses: docker/build-push-action@v6
|
|
62
|
+
id: push
|
|
63
|
+
with:
|
|
64
|
+
context: .
|
|
65
|
+
platforms: linux/amd64,linux/arm64
|
|
66
|
+
push: true
|
|
67
|
+
tags: ${{ steps.meta.outputs.tags }}
|
|
68
|
+
labels: ${{ steps.meta.outputs.labels }}
|
|
69
|
+
|
|
70
|
+
- uses: sigstore/cosign-installer@v3
|
|
71
|
+
|
|
72
|
+
# Attaches to the exact digest just pushed (not a mutable tag), as an
|
|
73
|
+
# OCI referrer in the same registry -- `cosign download sbom
|
|
74
|
+
# ghcr.io/<repo>@<digest>` (or any OCI-referrers-aware scanner) finds
|
|
75
|
+
# it from the image alone, no separate URL to track.
|
|
76
|
+
- name: Attach SBOM to published image
|
|
77
|
+
run: |
|
|
78
|
+
cosign attach sbom \
|
|
79
|
+
--type cyclonedx \
|
|
80
|
+
--sbom sbom-result/bom.json \
|
|
81
|
+
"ghcr.io/${{ github.repository }}@${{ steps.push.outputs.digest }}"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
.direnv/
|
|
2
|
+
result
|
|
3
|
+
sbom-result
|
|
4
|
+
|
|
5
|
+
# Rust build artifacts (Cargo.lock is committed)
|
|
6
|
+
/target/
|
|
7
|
+
|
|
8
|
+
# docker-build staging dir (dereferenced copy of result/bin/tree-police)
|
|
9
|
+
/dist/
|
|
10
|
+
|
|
11
|
+
# generated by `just sbom` (see flake.nix's `sbom` package)
|
|
12
|
+
/sbom.cdx.json
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
// THIRD-PARTY-LICENSES.md is generated (`just third-party-licenses`) from
|
|
3
|
+
// upstream crates' own license texts -- not hand-authored prose, and not
|
|
4
|
+
// ours to reformat.
|
|
5
|
+
"ignores": ["THIRD-PARTY-LICENSES.md"],
|
|
6
|
+
"config": {
|
|
7
|
+
// Hard 80-column wrapping doesn't suit docs full of inline code, links,
|
|
8
|
+
// and tables; line content matters more than column count here.
|
|
9
|
+
"MD013": false,
|
|
10
|
+
// README.md's logo <img> comes before the H1 on purpose (float it
|
|
11
|
+
// right of the title); both rules exist to catch accidental HTML/
|
|
12
|
+
// missing titles, not this deliberate pattern.
|
|
13
|
+
"MD033": false,
|
|
14
|
+
"MD041": false
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# This repo's own dev-workflow checks (not to be confused with
|
|
2
|
+
# .pre-commit-hooks.yaml, which is what makes *this repo* usable as a
|
|
3
|
+
# pre-commit hook source for other repos' Python/Terraform/etc. scanning).
|
|
4
|
+
#
|
|
5
|
+
# All hooks use `language: system` and run straight off the Nix devShell's
|
|
6
|
+
# PATH (see flake.nix) -- no separate per-hook virtualenv/npm install, and
|
|
7
|
+
# no tool version drifts from what `direnv allow`/`nix develop` already
|
|
8
|
+
# pins. Run `direnv allow` (or `nix develop`) once, then `pre-commit install`.
|
|
9
|
+
repos:
|
|
10
|
+
- repo: local
|
|
11
|
+
hooks:
|
|
12
|
+
- id: cargo-fmt
|
|
13
|
+
name: cargo fmt
|
|
14
|
+
language: system
|
|
15
|
+
entry: cargo fmt -- --check
|
|
16
|
+
files: \.rs$
|
|
17
|
+
pass_filenames: false
|
|
18
|
+
|
|
19
|
+
- id: cargo-clippy
|
|
20
|
+
name: cargo clippy (deny warnings)
|
|
21
|
+
language: system
|
|
22
|
+
entry: cargo clippy --all-targets --release -- --deny warnings
|
|
23
|
+
files: \.rs$
|
|
24
|
+
pass_filenames: false
|
|
25
|
+
|
|
26
|
+
- id: nix-fmt
|
|
27
|
+
name: alejandra (nix fmt, check only)
|
|
28
|
+
language: system
|
|
29
|
+
entry: alejandra --check
|
|
30
|
+
files: \.nix$
|
|
31
|
+
|
|
32
|
+
- id: nix-statix
|
|
33
|
+
name: statix (nix lint)
|
|
34
|
+
language: system
|
|
35
|
+
entry: statix check
|
|
36
|
+
files: \.nix$
|
|
37
|
+
pass_filenames: false
|
|
38
|
+
|
|
39
|
+
- id: nix-deadnix
|
|
40
|
+
name: deadnix (nix dead code)
|
|
41
|
+
language: system
|
|
42
|
+
entry: deadnix --fail
|
|
43
|
+
files: \.nix$
|
|
44
|
+
pass_filenames: false
|
|
45
|
+
|
|
46
|
+
- id: markdownlint
|
|
47
|
+
name: markdownlint-cli2
|
|
48
|
+
language: system
|
|
49
|
+
entry: markdownlint-cli2
|
|
50
|
+
files: \.md$
|
|
51
|
+
|
|
52
|
+
- id: actionlint
|
|
53
|
+
name: actionlint (GitHub Actions workflows)
|
|
54
|
+
language: system
|
|
55
|
+
entry: actionlint
|
|
56
|
+
files: ^\.github/workflows/.*\.ya?ml$
|
|
57
|
+
pass_filenames: false
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Installs the prebuilt `tree-police` wheel from PyPI via the stub package in
|
|
2
|
+
# the root pyproject.toml -- needs only Python, no Docker, no Rust toolchain.
|
|
3
|
+
- id: tree-police
|
|
4
|
+
name: tree-police (AST pattern scan)
|
|
5
|
+
description: >-
|
|
6
|
+
Scan source files for forbidden AST patterns via tree-sitter queries.
|
|
7
|
+
Drop extra rules in a repo-local .tree-police/ directory to extend the
|
|
8
|
+
built-in ruleset with no image rebuild.
|
|
9
|
+
entry: tree-police --fail-on error
|
|
10
|
+
language: python
|
|
11
|
+
pass_filenames: true
|
|
12
|
+
# The binary already scans files in parallel; one process per batch of
|
|
13
|
+
# filenames is plenty.
|
|
14
|
+
require_serial: true
|
|
15
|
+
|
|
16
|
+
# Same scan via the ghcr.io image, for machines that prefer Docker over a
|
|
17
|
+
# Python environment.
|
|
18
|
+
- id: tree-police-docker
|
|
19
|
+
name: tree-police (AST pattern scan, Docker)
|
|
20
|
+
description: >-
|
|
21
|
+
Same as `tree-police`, run from the ghcr.io/imochoa/tree-police image.
|
|
22
|
+
entry: ghcr.io/imochoa/tree-police:latest --fail-on error
|
|
23
|
+
language: docker_image
|
|
24
|
+
pass_filenames: true
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# AGENTS.md (tree-police)
|
|
2
|
+
|
|
3
|
+
Guide for AI coding agents working on `tree-police`. Human docs live in
|
|
4
|
+
[`README.md`](README.md) and [`queries/README.md`](queries/README.md); this
|
|
5
|
+
file is the fast path.
|
|
6
|
+
|
|
7
|
+
## What this is
|
|
8
|
+
|
|
9
|
+
A declarative linter, embeddable as a pre-commit `docker_image` hook: `.scm`
|
|
10
|
+
query files in `queries/` are compiled and embedded into the `tree-police`
|
|
11
|
+
binary at build time (`include_dir!` in `src/rules.rs`). Parses each file
|
|
12
|
+
once, in parallel (`ignore` + `rayon`), with severity levels and
|
|
13
|
+
pretty/JSON/JSONL output.
|
|
14
|
+
|
|
15
|
+
Rules currently cover Python and Terraform/HCL. `src/registry.rs` also bakes
|
|
16
|
+
in 90 other grammars (92 languages total) with no rule files yet — every
|
|
17
|
+
tree-sitter grammar crate on crates.io compatible with this crate's pinned
|
|
18
|
+
`tree-sitter` core version (see the comment above `registry::LANGUAGES` for
|
|
19
|
+
what's excluded and why). A new rule file is all that's needed to start
|
|
20
|
+
covering one of them, no Cargo/registry change required, and a pre-commit
|
|
21
|
+
hook can pass every changed file without its own file-type allowlist
|
|
22
|
+
(unrecognized extensions are just skipped).
|
|
23
|
+
|
|
24
|
+
## Getting started
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
direnv allow # or: nix develop
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The dev shell provides the Rust toolchain (`rustc`, `cargo`, `clippy`,
|
|
31
|
+
`rustfmt`), a C/C++ compiler (the grammar crates compile their grammars via
|
|
32
|
+
their own `build.rs`), and `just`.
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
All commands are recipes in the [`justfile`](justfile). Exit codes: `0` =
|
|
37
|
+
no findings, `1` = findings detected (suitable for CI gates), `2` = internal error.
|
|
38
|
+
|
|
39
|
+
| Command | Purpose |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `just build` | `cargo build --release`. |
|
|
42
|
+
| `just scan-bin [args]` | Run `tree-police`. Args pass through, e.g. `just scan-bin --format json src/`. |
|
|
43
|
+
| `just query-test` | `cargo test --test query_cases` — per-rule annotated fixtures; see below. |
|
|
44
|
+
| `just test` | `cargo test` — unit tests + black-box CLI tests (`tests/cli.rs`) + query-case tests. |
|
|
45
|
+
| `just docker-build` | `nix build .#tree-police-static`, then `podman build` a `FROM scratch` image. |
|
|
46
|
+
| `just docker-run [args]` | Run the built image (`-t` for color; see the recipe comment). |
|
|
47
|
+
| `just wheel` | `maturin build --release`: the binary as a Python wheel in `target/wheels/` (see `python/pyproject.toml`). |
|
|
48
|
+
| `just third-party-licenses` | Regenerate `THIRD-PARTY-LICENSES.md` from `Cargo.lock`. |
|
|
49
|
+
| `just sbom` | Build a CycloneDX SBOM (`sbom.cdx.json`) via Nix. |
|
|
50
|
+
| `just release X.Y.Z` | Bump the version, run the test/lint gate, commit, tag `vX.Y.Z`, and push — triggers the Docker, GitHub Release and PyPI workflows. Requires a clean `main` with no existing tag of that name. |
|
|
51
|
+
|
|
52
|
+
## Layout
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
.
|
|
56
|
+
├── flake.nix # devShell: Rust toolchain; tree-police-static (musl) package
|
|
57
|
+
├── Cargo.toml / Cargo.lock # crate manifest (lockfile committed)
|
|
58
|
+
├── Dockerfile / .dockerignore
|
|
59
|
+
├── pyproject.toml # pre-commit hook stub: only pins the `tree-police` PyPI wheel
|
|
60
|
+
├── python/pyproject.toml # maturin `bindings = "bin"`: ships the binary as a PyPI wheel
|
|
61
|
+
├── .pre-commit-hooks.yaml # `tree-police` (python, PyPI wheel) + `tree-police-docker` hooks
|
|
62
|
+
├── .github/workflows/ # image (ghcr.io), CLI tarballs (GitHub Release), wheels (PyPI)
|
|
63
|
+
├── src/ # main, registry, rules, scan, report, severity, config, suppress
|
|
64
|
+
├── tests/cli.rs # black-box CLI tests
|
|
65
|
+
├── tests/query_cases.rs # per-rule annotated fixture tests (see below)
|
|
66
|
+
├── .envrc # direnv: `use flake`
|
|
67
|
+
├── justfile # build / scan-bin / test / docker-* commands
|
|
68
|
+
├── queries/ # flat: <label>-<code>.scm, see queries/README.md
|
|
69
|
+
│ ├── rules-py.scm # categories: forbidden, security, temporal, web
|
|
70
|
+
│ └── rules-tf.scm # categories: naming, conventions, security
|
|
71
|
+
├── tests/fixtures/ # whole-file good/bad samples for tests/cli.rs
|
|
72
|
+
│ ├── python/
|
|
73
|
+
│ └── terraform/
|
|
74
|
+
├── tests/query-cases/ # per-rule annotated fixtures for `just query-test`
|
|
75
|
+
│ └── python/
|
|
76
|
+
├── README.md
|
|
77
|
+
└── queries/README.md # rule conventions
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Adding a rule
|
|
81
|
+
|
|
82
|
+
1. Add a pattern to `queries/<label>-<code>.scm` (`<code>` from
|
|
83
|
+
`src/registry.rs`, e.g. `py`, `tf`); embedded on the next `cargo build`.
|
|
84
|
+
2. Give each pattern exactly **one** non-`_` `@capture` — it is the rule ID in output (e.g. `@s3_bucket_missing_sf_prefix`). Prefix every predicate-only helper capture with `_` (e.g. `@_block_type`) so `tree-police` doesn't report it. Declare severity with `(#set! severity "error|warning|log")` (defaults to `warning`); optionally add `(#set! category "…")` (groups `--list-rules` output) and `(#set! description "…")` (shown per-finding and in `--list-rules`).
|
|
85
|
+
3. Add fixtures under `tests/fixtures/<language>/`:
|
|
86
|
+
- `*_violations.<ext>` — code that must trigger captures.
|
|
87
|
+
- `clean.<ext>` — code that must produce zero captures.
|
|
88
|
+
4. Add an assertion to `tests/cli.rs` for the new rule.
|
|
89
|
+
5. Add a `tests/query-cases/<language>/<topic>.<ext>` fixture (see below) and run `just query-test`.
|
|
90
|
+
|
|
91
|
+
## Query-case tests
|
|
92
|
+
|
|
93
|
+
`tests/query-cases/<lang>/<topic>.<ext>` are annotated fixtures, grouped by
|
|
94
|
+
topic/category rather than by `.scm` file (there's one `.scm` file per
|
|
95
|
+
language), checked by `tests/query_cases.rs` (`just query-test`, wired into
|
|
96
|
+
`just test`). A comment names the capture expected on the line directly
|
|
97
|
+
above it:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
print("debug")
|
|
101
|
+
# <- @forbidden_print
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Unlike `tree-sitter query --test` (same comment syntax, but it mis-pairs
|
|
105
|
+
assertions once a query file defines more than one distinct capture name —
|
|
106
|
+
confirmed by testing, not a guess), this harness checks each assertion
|
|
107
|
+
against the actual capture spans directly, and — the other direction —
|
|
108
|
+
fails if any capture fires on a line with no matching assertion. That gives
|
|
109
|
+
both "should match" and "should not match" coverage from one file, which
|
|
110
|
+
whole-file `tests/fixtures/*` checks don't: a rule firing on the wrong line
|
|
111
|
+
of a big fixture still passes a file-level `assert_clean`/`assert_match`.
|
|
112
|
+
|
|
113
|
+
An unannotated line is an *implicit* "nothing should fire here" assertion —
|
|
114
|
+
there's no separate negative-assertion syntax, and no need for one. Put each
|
|
115
|
+
rule's positive example directly above its negative counterexample (not in
|
|
116
|
+
a separate "clean" section at the bottom); see any file under
|
|
117
|
+
`tests/query-cases/python/` for the pattern.
|
|
118
|
+
|
|
119
|
+
It runs the **compiled `tree-police` matcher** directly (via
|
|
120
|
+
`tree_police::rules::load()`), not a subprocess. It caught a real bug when
|
|
121
|
+
this engine's rule files were consolidated to one per language: because
|
|
122
|
+
every rule now applies to every fixture, a rule's negative example in one
|
|
123
|
+
topic's fixture started tripping an unrelated rule from another category —
|
|
124
|
+
see the cross-category caveat in `queries/README.md`.
|
|
125
|
+
|
|
126
|
+
## Adding a language
|
|
127
|
+
|
|
128
|
+
1. Add the `tree-sitter-<lang>` crate to `Cargo.toml` and one `LangSpec`
|
|
129
|
+
entry (`name`, `code`, `extensions`, `LANGUAGE.into()`) to
|
|
130
|
+
[`src/registry.rs`](src/registry.rs).
|
|
131
|
+
2. Add `queries/<label>-<code>.scm` rule file(s), matching the new `code`.
|
|
132
|
+
3. Add fixtures and `tests/cli.rs` assertions as above.
|
|
133
|
+
|
|
134
|
+
Adding just the grammar + registry entry with no rule file yet is fine and
|
|
135
|
+
already done for several languages (see "What this is") — it costs nothing
|
|
136
|
+
at scan time (files with no rules for their language are skipped) and means
|
|
137
|
+
a future rule file needs no Cargo/registry change.
|
|
138
|
+
|
|
139
|
+
## Path scoping and suppressions
|
|
140
|
+
|
|
141
|
+
`src/config.rs` loads `.tree-police/config.toml` (or `--config FILE`). In it,
|
|
142
|
+
`[[scope]]` entries restrict the selected rules to `paths` and/or keep them
|
|
143
|
+
out of `exclude`, and `[[override]]` entries change severity per path.
|
|
144
|
+
`src/suppress.rs` reads `tree-police: ignore` / `ignore-next-line` /
|
|
145
|
+
`ignore-file` directives from comment nodes. Both run per file inside
|
|
146
|
+
`scan::scan_file`, after matching. Selectors (`rule_id`,
|
|
147
|
+
`category:<name>`, `file:<query-file-name>`) are checked against
|
|
148
|
+
`config::Catalog`. An unknown selector is a hard error in the config but
|
|
149
|
+
only a warning in a comment. User docs are in README "Config file" and
|
|
150
|
+
"Suppressing findings".
|
|
151
|
+
|
|
152
|
+
Renaming a rule ID or category also breaks any config file or suppression
|
|
153
|
+
comment that refers to it, which is one more reason to keep them stable.
|
|
154
|
+
|
|
155
|
+
## Gotchas
|
|
156
|
+
|
|
157
|
+
- **The root `pyproject.toml` is not the real package.** pre-commit/prek install a `language: python` hook with `pip install .` on the repo root, so the root holds a stub that only depends on `tree-police==<version>` from PyPI. That way hook users get the prebuilt wheel instead of compiling 92 grammars. The maturin config lives in `python/pyproject.toml` (README and licenses are symlinked in). `just release` bumps the stub's pin, and `tests/packaging.rs` fails if it drifts from `Cargo.toml`. A new tag's hook can only be installed once `pypi-release.yml` has published that version.
|
|
158
|
+
|
|
159
|
+
- **No regex lookaheads.** Rust's `regex` crate rejects `(?!...)` / `(?=...)`. Split into separate `#not-match?` / `#match?` predicates on the same capture instead.
|
|
160
|
+
- **A predicate can't reference the capture on its own enclosing node.** A node's capture is only declared at its closing paren, so a predicate that's one of that node's *own children* comes before the capture exists, textually — `(node (child) (#pred? @x) ) @x` fails to compile ("Invalid capture name"). Wrap the captured node and the predicate together in one more pair of parens instead — `((node (child)) @x (#pred? @x ...))` — so they become siblings at the outer level, where ordering works. See `missing_route_validation` in `queries/rules-py.scm` (and `bare_except`/`todo`, which use the same wrapping).
|
|
161
|
+
- **Only literal strings are visible to queries.** Interpolated expressions (`"${var.env}-thing"`) aren't matched against string predicates — those need runtime validation elsewhere.
|
|
162
|
+
- **`#set!` severity + `_`-helper captures are load-bearing.** The one non-`_` capture per pattern is the reported rule ID; severity comes from `(#set! severity …)` (default `warning`).
|
|
163
|
+
- **Non-core predicates don't just go unenforced — they zero out the whole match.** `#not-has-ancestor?` and similar are silently treated as unsatisfied by the embedded `tree_sitter::QueryCursor` matcher, dropping the match entirely rather than ignoring the predicate. Confirmed by testing (`tests/query_cases.rs`), not documented upstream. Avoid them.
|
|
164
|
+
- **Rule IDs and `category` values are stable identifiers** — referenced by CI/config elsewhere, don't rename without a coordinated change.
|
|
165
|
+
- **A query filename's code must match a registered `LangSpec::code`, or the binary fails to build the ruleset** — both for embedded queries and for `--rules-dir` repo-local ones. This is deliberate (fail fast on a typo) rather than silently dropping the file.
|
|
166
|
+
- **`--list-rules`'s catalog-building text scan skips `;` comments and `"..."` string literals, nothing else.** `rules.rs::primary_capture_in` recovers a pattern's rule ID by scanning the raw source slice from one pattern's start byte to the *next* pattern's start byte for the last `@word`-shaped mention. Two confirmed bugs from this, both fixed with regression tests in `rules.rs`: a trailing comment reading "isn't @activity/@workflow decorated" got its `@workflow` mistaken for the *preceding* pattern's rule ID (misreported `os_popen_usage` as `workflow`); a `(#not-match? ... "@validate\\(")` regex argument got its `@validate` mistaken the same way (misreported `missing_route_validation` as `validate`). Both are skipped now — but nothing else is, so a future `@word`-shaped mention anywhere else non-obvious could reproduce this. If `--list-rules` ever shows a rule ID that doesn't match the capture you wrote, check here first.
|
|
167
|
+
|
|
168
|
+
## Conventions when editing queries
|
|
169
|
+
|
|
170
|
+
- Prefer declarative predicates (`#eq?`, `#match?`, `#not-match?`) over broad structural matches.
|
|
171
|
+
- Capture names are the rule ID surfaced to users — make them descriptive (`hardcoded_provider_credentials`, not `bad1`).
|
|
172
|
+
- Keep one logical rule per capture name; combine predicates rather than duplicating patterns.
|
|
173
|
+
- Terraform rules are illustrative examples of naming/convention/security checks (e.g. a placeholder `acme-` bucket prefix, `-role`/`-sg` suffixes) — swap them for your own org's conventions rather than assuming they apply as-is.
|
|
174
|
+
|
|
175
|
+
## Credits
|
|
176
|
+
|
|
177
|
+
This crate's Rust architecture (parallel `ignore`+`rayon` scanning,
|
|
178
|
+
git-ignore awareness, embedded-grammar-per-language design, the `_`-prefix
|
|
179
|
+
capture convention) is built on and adapted from
|
|
180
|
+
[tree-grepper](https://github.com/BrianHicks/tree-grepper) (archived) by
|
|
181
|
+
Brian Hicks — it is the source and foundation for most of `src/`'s shape.
|
|
182
|
+
Note tree-grepper is Hippocratic-licensed, not standard OSS; see the
|
|
183
|
+
"Credits"/"License" sections in [`README.md`](README.md).
|
|
184
|
+
|
|
185
|
+
## References
|
|
186
|
+
|
|
187
|
+
- [`README.md`](README.md) — setup, pre-commit quickstart, repo-local rules, and credits/license.
|
|
188
|
+
- [`queries/README.md`](queries/README.md) — rule conventions.
|
|
189
|
+
- [Tree-sitter query syntax](https://tree-sitter.github.io/tree-sitter/using-parsers/queries/)
|
|
190
|
+
- [tree-sitter-python grammar](https://github.com/tree-sitter/tree-sitter-python)
|
|
191
|
+
- [tree-sitter-hcl grammar](https://github.com/tree-sitter-grammars/tree-sitter-hcl)
|