BanditRS
The Python security scanner bandit, rewritten in pure Rust.
Same tests, same options, same outputs, same exit codes — 17× to 86× faster,
with 57% less memory.
Install
BanditRS is on PyPI as prebuilt wheels for six platforms — no Rust toolchain needed, nothing to compile. Fastest way to try it, nothing installed permanently:
uvx banditrs -r .
To keep it around — bandit, banditrs, bandit-baseline and bandit-config-generator all
land on your PATH:
uv tool install banditrs
bandit -r .
pip and pipx work the same way:
pip install banditrs # or: pipx install banditrs
bandit -r .
Platform wheel names and building from source: §7.
Usage
Every bandit invocation works as-is — same options, same output, same exit codes:
bandit -r my_project/ # recursive scan
bandit -r my_project/ -f json -o report.json # JSON report
bandit -r my_project/ -ll -ii # severity ≥ MEDIUM, confidence ≥ MEDIUM
bandit -r my_project/ -s B101,B601 # skip tests
bandit -r my_project/ -t B602 # run a single test
bandit -r my_project/ -c bandit.yaml # configuration profile
bandit -r my_project/ -b baseline.json # show only new issues
bandit -r my_project/ --exit-zero # never fail (report mode)
bandit --help # all 27 options
Installing gives you four commands:
bandit drop-in replacement for the PyCQA command
banditrs identical alias (see below)
bandit-baseline scan against a git baseline
bandit-config-generator emit a configuration profile
bandit-config-generator --show-defaults > bandit.yaml # generate a full profile
bandit-baseline -r my_project/ # diff against the parent commit (requires git)
Coexisting with PyCQA bandit. The bandit command deliberately occupies the same file name as
the PyCQA package, so if both are installed in one environment the last one installed wins — silently.
The banditrs alias is byte-for-byte the same program under a name nothing else claims: use it to
compare the two side by side, or to migrate gradually, without uninstalling anything.
In pre-commit. Same invocation, same output, same exit code:
# before
- repo: https://github.com/PyCQA/bandit
rev: <tag>
hooks: [{ id: bandit, args: ["-r", "src/"] }]
# after
- repo: https://github.com/LePhilippeDucTai/BanditRS
rev: v0.2.1
hooks: [{ id: banditrs, args: ["-r", "src/"] }]
pre-commit builds the hook in its own isolated environment, so this route compiles from source and
needs a Rust toolchain (rustc ≥ 1.96) on the machine running the hook.
From Python. The banditrs module is a thin launcher around the same executables — the analysis
is never reimplemented in Python. python -m banditrs … is equivalent to calling bandit directly:
import banditrs
result = banditrs.run(["-r", "src/", "-f", "json"], capture_output=True, text=True)
result.returncode # non-zero when issues are found
Contents
- In one minute
- Is the output really identical?
- Benchmarks
- Why it's faster
- How parity is proven
- Remaining differences
- Installation details
- Architecture and development
- License and credits
1. In one minute
bandit walks the AST of a Python file and runs ~75 security tests (command injection, unsafe
deserialization, weak crypto, hardcoded passwords, vulnerable XML…). BanditRS does the same thing,
using the Python parser from ruff and per-file parallel analysis.
The goal is not "a linter inspired by bandit": it is a drop-in replacement. You can substitute the
binary in a pre-commit hook, a CI pipeline or a Makefile without changing a single option, and get
byte-for-byte the same output (modulo the exceptions documented in §6).
bandit (Python) |
BanditRS | |
|---|---|---|
| Language / runtime | Python ≥ 3.10 + dependencies (PyYAML, stevedore, rich, GitPython…) | Rust, one native binary, zero runtime |
| Test IDs | 75 (42 plugins + 33 blacklists) | the same 75 |
| Long command-line options | 27 | the same 27 |
| Output formats | 9 (csv, custom, html, json, sarif, screen, txt, xml, yaml) |
the same 9 |
| Executables | bandit, bandit-baseline, bandit-config-generator |
the same 3, plus the banditrs alias |
| Exit codes | 0 / 1 (issues) / 2 (usage error) |
identical |
# nosec, .bandit, pyproject.toml, profiles, baseline |
✅ | ✅ |
| Parallelism | no (one file after another) | yes (rayon, one file per task) |
| Scanning the CPython 3.11 stdlib (672 files, 307k lines) | 18.4 s — 75 MiB | 0.21 s — 33 MiB |
2. Is the output really identical?
The decisive test for a drop-in replacement is not "does it find the same bugs", it is
"is the diff empty". Here is the answer, on a deliberately faulty file:
# app.py
import subprocess
import yaml
def load(path):
return yaml.load(open(path).read())
def run(user_input):
subprocess.call("grep " + user_input, shell=True)
PASSWORD = "hunter2"
The diff -u of the two full outputs contains only ten changed lines — five pairs, four of which are
exactly the same pattern:
@@ timestamp (volatile on both sides) @@
-Run started:2026-09-09 09:53:52.239965+00:00
+Run started:2026-09-09 09:53:52.273576+00:00
@@ version in the documentation URL (deviation #12) @@
- More Info: …/en/0.0.1.dev49/blacklists/blacklist_imports.html#b404-import-subprocess
+ More Info: …/en/latest/blacklists/blacklist_imports.html#b404-import-subprocess
- More Info: …/en/0.0.1.dev49/plugins/b506_yaml_load.html
+ More Info: …/en/latest/plugins/b506_yaml_load.html
- More Info: …/en/0.0.1.dev49/plugins/b602_subprocess_popen_with_shell_equals_true.html
+ More Info: …/en/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html
- More Info: …/en/0.0.1.dev49/plugins/b105_hardcoded_password_string.html
+ More Info: …/en/latest/plugins/b105_hardcoded_password_string.html
Everything else — issue ordering, severities, confidences, CWEs, line:column positions, code snippets, tabs, separator widths, closing metrics, exit code — is byte-for-byte identical. The only two differences are the timestamp (volatile by nature) and the version in the documentation URL (deviation #12, a deliberate choice).
The same exercise over the full fixture corpus (examples/, 94 files), in every report format, after
neutralizing the volatile fields only (timestamp, tool version in the doc URL). screen is txt plus
ANSI escapes and is covered by a unit test instead:
| Format | Report lines | Diverging lines | Verdict |
|---|---|---|---|
json |
13,293 | 0 | identical |
txt |
6,073 | 0 | identical |
csv |
598 | 0 | identical |
xml |
1,792 | 0 | identical |
html |
13,915 | 0 | identical |
custom |
597 | 0 | identical |
sarif |
23,973 | 4 | the tool's own "version"/"semanticVersion" (0.0.1.dev49 vs 0.2.0) |
yaml |
11,589 | 164 | PyYAML anchors/aliases (&id001/*id001) — deviation #11; content identical when re-read |
Six formats out of eight are strictly identical. The other two diverge only in their representation,
never in the data: SARIF announces its own version (which is the correct behaviour for two different tools),
and YAML forgoes a PyYAML optimization based on Python object identity (id()), which Rust structurally does
not have.
On real code the answer is the same: scanning bandit's own repository (69 files, 8,737 lines) with both tools gives an identical JSON report — same single issue, same per-file metrics, same ordering.
3. Benchmarks
3.1 Protocol
No number in this section is an estimate. Everything is reproducible:
cargo build --release
scripts/bench_vs_python.sh -n 7 # corpus throughput
scripts/bench_vs_python.sh -n 15 examples/subprocess_shell.py examples/long_set.py
- Machine: 4 vCPU container (Intel Xeon @ 2.10 GHz), 15 GiB RAM, Linux 6.18.44-fc-v24.
- Reference:
bandit 0.0.1.dev49(PyCQA repository @1d3053d), CPython 3.11.15. - Candidate:
banditrs 0.2.0,releaseprofile (fat LTO,codegen-units = 1). - Measurement: median of N runs after one warm-up run, wall-clock time,
-f json -qoutput redirected to/dev/null(we measure the analysis, not the terminal). - Memory:
ru_maxrssfromRUSAGE_CHILDREN, one dedicated measuring process per reading.
The full protocol, the non-regression thresholds and the callgrind profile are in
docs/plan/benchmarks.md.
3.2 Throughput
| Target | Files | Lines | bandit (Python) |
BanditRS | Factor |
|---|---|---|---|---|---|
| 36 real libraries (§5.5) | 21,536 | 7,170,763 | 506.9 s | 8.3 s | 61.0× |
/usr/lib/python3.11 (CPython stdlib) |
672 | 307,504 | 18.40 s | 0.214 s | 86.0× |
examples/ (bandit fixtures) |
94 | 9,534 | 6.497 s | 0.341 s | 19.0× |
examples/long_set.py (the largest, 65 KiB) |
1 | 7,279 | 6.247 s | 0.359 s | 17.4× |
examples/subprocess_shell.py (a typical file) |
1 | 60 | 0.197 s | 0.006 s | 32.3× |
Cost per line on the stdlib: 60 µs/line on the Python side against 0.70 µs/line on the Rust side — i.e. 17 kloc/s against 1.44 Mloc/s.
Why is
examples/"only" 19× when the stdlib is 86×? Becauseexamples/is dominated by a single pathological file:long_set.pyaccounts for 6.25 s out of the corpus's 6.50 s on the Python side. Measuringexamples/therefore essentially means measuring one file — and parallelism does not help on a single file. The 19× is the pure sequential gain; the 86× on the stdlib is that same gain multiplied by parallelism. Both numbers are consistent, see §3.5.
3.3 Per-library breakdown
§3.2 aggregates 21,536 files into one number. To check the gain isn't an artifact of one favorable
package dominating the average, here it is broken down library by library, on the eight
real-world packages of the smoke tier of tests/corpus/manifest.tsv —
the same pinned sdists the differential test suite (§5.5)
runs both tools against, so a library that appears here is also proven byte-for-byte identical:
scripts/bench_corpus.py -n 5 --tier smoke # target/bench/corpus.md
| Library | Files | Lines | bandit (Python) |
BanditRS | Factor |
|---|---|---|---|---|---|
Werkzeug 3.1.8 |
138 | 35,090 | 2.389 s | 0.055 s | 43.4× |
urllib3 2.7.0 |
81 | 32,241 | 2.511 s | 0.070 s | 35.6× |
Flask 3.1.3 |
83 | 17,889 | 1.212 s | 0.028 s | 43.4× |
Jinja2 3.1.6 |
52 | 22,755 | 1.793 s | 0.044 s | 40.8× |
paramiko 5.0.0 |
65 | 26,972 | 1.837 s | 0.042 s | 44.0× |
Mako 1.4.1 |
71 | 20,199 | 1.232 s | 0.025 s | 48.6× |
PyYAML 6.0.3 |
47 | 9,371 | 0.861 s | 0.016 s | 53.7× |
requests 2.34.2 |
35 | 11,526 | 0.942 s | 0.031 s | 30.6× |
| 8 libraries, total | 572 | 176,043 | 12.78 s | 0.31 s | 41.1× |
No outlier: every one of the eight libraries lands between 30.6× and 53.7×, a tighter band than
the 19×–86× spread of §3.2 because these are same-sized, ordinary application code rather than the
extremes (one pathological file, or the finding-poor stdlib). Extending to the full 36-package corpus
of §5.5 keeps the same shape: 88.0×
on the best package (transformers) down to 37.6× on the worst (fabric) — the aggregate 61.0×
of §3.2 is a genuine middle, not an average dragged up by one library.
3.4 Latency and memory
| Metric | Python | BanditRS | Delta |
|---|---|---|---|
Latency on a 60-line file (the editor / pre-commit case) |
197 ms | 6 ms | −97% |
| Peak RSS, stdlib scan | 75.5 MiB | 32.7 MiB | −57% |
| Deployment size | Python venv + ~8 dependencies | one 5.1 MB binary | — |
The 6 ms single-file latency is the most concrete day-to-day argument: under 10 ms, the analysis becomes fast enough to run on every save in an editor, which the ~200 ms of Python interpreter startup rules out.
3.5 Breaking the gain down: engine × parallelism
The overall factor mixes two independent effects. They can be separated by throttling rayon:
RAYON_NUM_THREADS |
Time (stdlib) | Speedup vs Python | Parallel speedup | Efficiency |
|---|---|---|---|---|
| 1 | 0.591 s | 28.8× | 1.00× | — |
| 2 | 0.331 s | 51.4× | 1.79× | 89% |
| 4 | 0.176 s | 96.7× | 3.36× | 84% |
This series is a separate campaign from the one in §3.2 (median of 3 instead of 7), hence the 0.176 s here against 0.214 s there: run-to-run variance on this shared container is on the order of ±10%. What matters here is not the absolute value but the ratio between rows, all drawn from the same series.
The observed factor therefore factorizes cleanly:
$$ S_{\text{total}} ;=; \underbrace{28.8}{\text{sequential engine}} ;\times; \underbrace{3.36}{\text{4 cores}} ;\approx; 97 $$
Feeding the 4-core speedup into Amdahl's law recovers the parallelizable fraction of the work:
$$ S(p) = \frac{1}{(1-f) + \frac{f}{p}} \quad\Longrightarrow\quad f = \frac{1 - 1/S(4)}{1 - 1/4} = \frac{1 - 0.2976}{0.75} \approx \mathbf{0.94} $$
94% of the work is parallelizable; the remaining 6% is the filesystem walk, the serialization of the final report and the aggregation of metrics — all intrinsically sequential. The model therefore predicts a ceiling of $1/0.06 \approx 16.7\times$ for the parallel component alone: beyond a dozen cores, adding cores will bring almost nothing more, and the factor will stay governed by the sequential gain and by disk I/O. This is an extrapolation of a model from a single measurement point, to be confirmed on a machine with more cores.
4. Why it's faster
Three causes, in decreasing order of importance. None of them is "Rust is fast": they are architectural choices that the language merely makes possible.
1. The hot loop is no longer interpreted. bandit visits each AST node and, for each node, calls a list
of plugin functions looked up in a Python dictionary, building a Context object wrapping a dict every time.
The dominant cost is not the analysis, it is dispatch: hundreds of millions of interpreted dict lookups and
function calls. On the Rust side the walker is a match on an enum of nodes, plugins are fns in a static
table, and the Context is a typed struct borrowed from the tree — zero allocation per node.
Analogy. Python hands each node to a switchboard operator who looks up the right party in a directory, picks up the phone, and forwards the call. Rust wired the switchboard once and for all at compile time: each node lands directly on the right extension.
2. The parser. CPython's ast.parse is written in C and is fast, but it materializes one Python object
per node (with a header, a reference count, an attribute dict). The ruff_python_parser / ruff_python_ast
crates produce a compact tree of enums, with no indirection and no garbage collector.
3. Parallelism. Bandit's natural unit of work is the file, and files are independent. Rust has no GIL:
rayon simply hands out one file per task. That is what the factor 3.36 in §3.5 measures — a factor the
Python version cannot obtain without going multi-process (and therefore paying for a full interpreter per
core, which would also explain its RSS).
An honest counter-example. The callgrind profile (benchmarks.md §6) shows
that 51% of the current instruction budget goes into two functions that parity forces us to keep naive:
NosecLines::for_range (30.8%, a line-by-line sweep of # nosec comments, quadratic in nesting depth) and
the trojansource plugin (20.4%, ten search passes per line). Fixing them would probably buy another 25–30%
— but any optimization that would change issue ordering, a position or an output string is forbidden without
a validated entry in DEVIATIONS.md. Parity outranks speed.
5. How parity is proven
Relying on a single mode of proof means relying on its blind spots. BanditRS layers four of them, sharing no assumptions — one reads the specification, one queries the living oracle, one freezes the past, and one takes the tool out of the laboratory:
| Mode | What is compared | Scale | Result |
|---|---|---|---|
| Ported test suite | bandit's own tests, one Python test ⇒ one Rust test with the same name | 273 Python tests → 354 Rust tests | 263 ported, 10 not portable (#8), 0 ignored |
| Functional coverage | test IDs and CLI options, enumerated from both tools | 75 IDs, 27 options | 75/75, 27/27 |
| Differential | JSON report per file, both tools live, volatile fields normalized | examples/ + stdlib + bandit's source: 835 files, 91 AST walk traces |
0 unexpected divergences |
| Golden corpus | reports recorded from the reference, replayed without Python (cargo test --test golden) |
96 files, 3.7 MB | regression invariant: any moved byte fails CI |
| Real third-party code | 36 PyPI packages pinned by sha256, bandit -r on each |
21,536 files, 7.2 M lines, 130,730 issues | 36/36 identical, errors[] included |
| Whole option surface | 88 CLI invocations: stdout, stderr and exit code | 88 | 85/88, the 3 others are deviations #10, #18, #19 |
Between them these exercise all 75 test IDs. Two findings are worth naming: the CLI matrix caught
-ll/-ii filtering one level too low (bandit's suite never tests its own argument parser), and one
message in bandit's output interpolates a Python memory address, so the reference is not
reproducible between two runs — BanditRS renders 0x0 instead (#5).
The method, the per-mode numbers and the commands to reproduce them: docs/parity.md.
The package-by-package evidence, generated from the differential's own output and never hand-edited:
docs/drop-in-parity.md.
6. Remaining differences (deliberate)
The policy is explicit: strict parity on everything the test suite observes; bandit's known bugs are fixed
only when no test depends on them. The typos in messages, the bad_calls/bad_imports inversion in legacy
configuration conversion, the first-match ordering of blacklists, the "first hit" behaviour of nosec over a
linerange, the absent ≠ None distinction in check_call_arg_value: all of it is reproduced
identically, bugs included.
Here are the 16 deliberate deviations that remain. The DEVIATIONS.md file gives the full
rationale for each, backed by a test case; it is append-only, so its numbering is stable and three of its
nineteen entries (#1, #14, #15) are struck through — the CLI matrix of §5
showed they were divergences BanditRS could simply stop having, and they were resolved rather than justified.
| # | Deviation | Visible where | Nature |
|---|---|---|---|
| 2 | CSV formatter with no CWE (NOTSET): Python crashes (KeyError: 'link'), BanditRS writes an empty column |
-f csv |
🐞 bug fixed |
| 3 | .bandit (INI): level/confidence/number converted to integers; the configfile key actually honoured |
--ini |
🐞 bug fixed |
| 4 | discover_files no longer mutates the loaded configuration (Python accumulated exclusions across calls) |
repeated calls | 🐞 bug fixed |
| 5 | Memory addresses (<ast.List object at 0x…>) rendered as 0x0; no RecursionError (iterative walker) |
B202, deeply nested files |
🎯 determinism |
| 6 | No rich progress bar; --version shows the crate version; no running on Python x.y.z line |
console output | 🎨 cosmetic |
| 7 | Positions of f-string constants: CPython ≥ 3.12 semantics by default | -f json, columns |
⚙️ BANDITRS_PYTHON_COMPAT=3.11 |
| 8 | 10 Python introspection tests (deepgetattr, meta_ast…) with no Rust equivalent |
test suite | 📐 not portable |
| 9 | B703 / DeepAssignation: two edge cases where BanditRS is stricter (it does not reproduce two bugs in django_xss.py) |
ad-hoc files only | 🐞 bug fixed |
| 10 | YAML: double-quoted (non-ASCII) scalars are not wrapped at 80 columns | -f yaml, rare |
🎨 cosmetic |
| 11 | YAML: no &id001/*id001 anchors/aliases (PyYAML deduplicates by Python object id()) |
-f yaml |
🎨 cosmetic |
| 12 | more_info points at …/en/latest/… instead of the installed package version |
all formats | 🎯 deliberate choice |
| 13 | Context::statement() has a real implementation (in Python the key is never written, so the property is always None) |
no plugin reads it | 📐 testability |
| 16 | Parser target version: set by BANDITRS_PYTHON_COMPAT, not by a host interpreter — align it with the reference and there is no gap at all, 3.12-only packages included |
files using syntax the reference rejects | ⚙️ configuration |
| 17 | The ids in "profile include/exclude tests" come out in a stable order; Python joins a set, whose order changes between runs of the same command |
-v -p <profile> |
🎯 determinism |
| 18 | -d does not dump the Context dict node by node (Python prints <ast.Name object at 0x…> reprs — 17,514 lines against 38); the INFO/WARNING/ERROR log lines are identical |
-d |
🎨 cosmetic |
| 19 | An invalid YAML config reports saphyr-parser's wording instead of PyYAML's — same behaviour, same exit code 2, different parser message |
broken -c file |
🎨 cosmetic |
None of these deviations changes which issue is reported, at which line, with which severity — with the
assumed exception of #9 (two B703 edge cases that no upstream fixture exercises) and #16, which is not a
gap but a setting: with the parser target aligned to the reference interpreter, the reports match on real
code down to errors[] (§5).
7. Installation details
BanditRS ships as a Python package, so it drops into any environment that already
has pip, uv or pipx — no Rust knowledge required to use it. The quick
commands are at the top of this README; here is the full matrix.
| Method | Command | Needs a Rust toolchain? |
|---|---|---|
| uv, on the fly (recommended) | uvx banditrs -r . |
No |
| uv, as a tool | uv tool install banditrs |
No |
| pip | pip install banditrs |
No |
| pipx | pipx install banditrs |
No |
| A specific version or platform | pip install https://github.com/LePhilippeDucTai/BanditRS/releases/download/v0.2.1/<wheel> |
No |
| From git (unreleased/dev) | pip install git+https://github.com/LePhilippeDucTai/BanditRS |
Yes |
| From source | cargo build --release |
Yes |
Why
git+needs Rust.pip install git+…always builds from source: the tool clones the repository and builds a wheel from the checkout on the spot. No prebuilt artifact is involved, sorustc≥ 1.96 andcargomust be present, and the build takes a couple of minutes. That is inherent to thegit+syntax, not a limitation of this package — use it only to try an unreleased commit; every tagged release ships as a prebuilt wheel on PyPI, which is what the methods above install.
Why
uvx banditrsneeds no--from. The distribution is namedbanditrs, and so is one of the four commands it installs (an alias forbandit— see Usage), souvxfinds it directly. To run thebanditname itself without installing anything, name the package explicitly:uvx --from banditrs bandit -r .— plainuvx banditwould instead look for the unrelated PyCQAbanditpackage on PyPI.
Every release publishes one wheel per platform. Because the wheels contain native
executables that do not link against libpython, each one is tagged
py3-none-<platform> and works with every supported Python version:
| Platform | Architecture | Wheel tag |
|---|---|---|
| Linux (glibc ≥ 2.17) | x86-64 | manylinux2014_x86_64 |
| Linux (glibc ≥ 2.17) | ARM64 | manylinux2014_aarch64 |
| macOS | Intel | macosx_*_x86_64 |
| macOS | Apple Silicon | macosx_*_arm64 |
| Windows | x86-64 | win_amd64 |
| Windows | ARM64 | win_arm64 |
Building from source
rustup update stable # rustc ≥ 1.96 required
git clone https://github.com/LePhilippeDucTai/BanditRS && cd BanditRS
cargo build --release
# → target/release/{bandit, banditrs, bandit-baseline, bandit-config-generator}
8. Architecture and development
This is not a line-by-line transcription: it is an idiomatic redesign (no dynamic dictionaries, no mutable global state, per-file parallelism) under an identical-output constraint.
src/
├── ast/ 2,586 l. ruff AST walk: walker, linerange, qualname, positions,
│ literals, f-strings, debug trace
├── core/ 4,654 l. the engine: config, profiles, plugin registry, blacklists, tester,
│ context, nosec, file discovery, metrics, parallel scan
├── plugins/ 2,399 l. the 42 plugins (32 files), messages and thresholds verbatim
├── formatters/ 1,464 l. the 9 output formats
├── cli/ 1,579 l. compatible argparse, bandit-baseline, bandit-config-generator
├── pycompat/ 2,587 l. exact emulation of the Python standard library wherever the output
│ depends on it: PyYAML (80-column wrapping), configparser, format(),
│ repr() of literals, encodings
└── source/ 253 l. file reading/decoding, line index, snippets
Python (bandit) |
Rust (BanditRS) | |
|---|---|---|
| Source code | 11,234 lines | 15,961 lines |
| Tests | 5,053 lines | 5,725 lines |
The pycompat/ module (2,587 lines, 16% of the code) deserves a word: it implements no security analysis at
all. It exists only because bandit's output exposes implementation details of the Python standard library —
PyYAML's line-wrapping algorithm, str.format() syntax, the repr() of literals, configparser semantics.
This is the real price of byte-for-byte parity, and it is higher than that of the analysis engine itself.
Dependencies
ruff_python_{parser,ast} · ruff_{text_size,source_file} (parser) · rayon (parallelism) · regex ·
rustc-hash · indexmap · serde/serde_json · toml · saphyr-parser · encoding_rs · typed-arena.
Quality gate
scripts/check.sh # build + 354 tests + rustfmt + clippy -D warnings + benchmark compilation
scripts/check.sh fast # without the benchmarks (development loop)
scripts/check.sh reproduces the jobs of the GitHub Actions workflow in .github/workflows/ci.yml.
scripts/check.sh parity adds the differential against the reference Python bandit (the CLI matrix and
the smoke tier of the real-world corpus); it needs the network and the reference binary, which is why it
is not in the default run. Exit 0 = the equivalent of a green CI.
Non-regression guardrail
Gains are worthless if they erode silently. benches/e2e.rs (criterion) measures 7 hot spots, and
scripts/bench_regression.sh fails if any of them degrades by more than +10% relative to the
committed baseline:
cargo bench --bench e2e # HTML report in target/criterion/
scripts/bench_regression.sh # comparison against the baseline, +10% threshold
Checking parity yourself
Two of these need no Python at all — they replay output recorded from the reference:
cargo test --test golden # the golden corpus: examples/ in nine formats
cargo test --test cli_matrix # 88 CLI invocations: stdout, stderr AND exit code
The rest run the reference bandit alongside BanditRS and compare:
scripts/cli_matrix.py diff # the CLI matrix, live
scripts/corpus.py fetch --tier standard # 24 real libraries, pinned by sha256 (~200 MB)
scripts/corpus.py verify --parse # every file parses under the reference interpreter
scripts/diff_corpus.py --tier standard # differential over 11 624 files of real code
scripts/diff_against_python.sh examples # the original per-file differential
scripts/gen_golden.sh # regenerate the golden corpus
scripts/bench_corpus.py -n 5 # speed, memory, thread scaling, cold start
They expect a reference Python bandit; by default /home/user/.pyenv-bandit/bin/bandit, overridable
through the PY_BANDIT variable. scripts/check.sh parity runs the fast subset of all of this.
What that produces: docs/drop-in-parity.md — the evidence behind the
"drop-in replacement" claim, generated by scripts/parity_report.py, never edited by hand.
Debugging aids
bandit --dump-walk examples/nosec.py # AST walk trace (node order, contexts)
BANDITRS_PYTHON_COMPAT=3.11 bandit … # f-string positions the CPython 3.11 way
Internal documentation
| File | Contents |
|---|---|
PLAN.md |
progress status, decisions, milestones, architecture, pitfalls encountered |
DEVIATIONS.md |
the 16 remaining deliberate deviations, justified one by one (append-only: 19 entries, 3 struck through as resolved) |
docs/spec/core.md |
the exact semantics of the Python core (read before porting anything) |
docs/spec/plugins.md |
the 42 plugins + blacklists: messages, regexes, defaults verbatim |
docs/spec/cli_formatters_tests.md |
CLI, formatters, test suite as specification |
docs/plan/benchmarks.md |
measurement protocol, thresholds, results, callgrind profile |
docs/plan/test-inventory.md |
Python test ↔ Rust test mapping, line by line |
docs/drop-in-parity.md |
the drop-in evidence, package by package and invocation by invocation — generated, never hand-edited |
tests/corpus/README.md |
the real-world corpus: tiers, why sdists are pinned by sha256, why verify --parse is the precondition |
docs/plan/README.md |
the parallel development plan (21 work packages), kept for the record |
9. License and credits
Apache-2.0 — see LICENSE and NOTICE.
BanditRS is an independent reimplementation of bandit (PyCQA), from
which it takes the entirety of its semantics, its messages, the examples/ fixtures and the test
specifications. All credit for the design of the security rules goes to the original project.
The Python parser comes from ruff (Astral).
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file banditrs-0.2.1.tar.gz.
File metadata
- Download URL: banditrs-0.2.1.tar.gz
- Upload date:
- Size: 1.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1b357e35121c1983caf061d571833a4de9230a86d7479b8c544e9e1e8c2c4e81
|
|
| MD5 |
6d28407da3a9064ebe5bc55e951bf99f
|
|
| BLAKE2b-256 |
c80754d467fe43f334fd66d2c9d2e4fdc5965e4bcc0889e320d70857d68ddab6
|
Provenance
The following attestation bundles were made for banditrs-0.2.1.tar.gz:
Publisher:
release.yml on LePhilippeDucTai/BanditRS
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
banditrs-0.2.1.tar.gz -
Subject digest:
1b357e35121c1983caf061d571833a4de9230a86d7479b8c544e9e1e8c2c4e81 - Sigstore transparency entry: 2774162430
- Sigstore integration time:
-
Permalink:
LePhilippeDucTai/BanditRS@a9a218f37bb23711704bbc7fc87a8fc678ec2bff -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/LePhilippeDucTai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a9a218f37bb23711704bbc7fc87a8fc678ec2bff -
Trigger Event:
push
-
Statement type:
File details
Details for the file banditrs-0.2.1-py3-none-win_arm64.whl.
File metadata
- Download URL: banditrs-0.2.1-py3-none-win_arm64.whl
- Upload date:
- Size: 5.4 MB
- Tags: Python 3, Windows ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
29f5ec690502bdf354ec2dac66590a1f640a98d62b9cab9fb273fbb09a9e73c5
|
|
| MD5 |
9b3be602f8ddd160ea6219f13f88dda2
|
|
| BLAKE2b-256 |
97b2267b5cccafa69fa310752aaade85a378955f148ef27f236238e449dd808b
|
Provenance
The following attestation bundles were made for banditrs-0.2.1-py3-none-win_arm64.whl:
Publisher:
release.yml on LePhilippeDucTai/BanditRS
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
banditrs-0.2.1-py3-none-win_arm64.whl -
Subject digest:
29f5ec690502bdf354ec2dac66590a1f640a98d62b9cab9fb273fbb09a9e73c5 - Sigstore transparency entry: 2774162491
- Sigstore integration time:
-
Permalink:
LePhilippeDucTai/BanditRS@a9a218f37bb23711704bbc7fc87a8fc678ec2bff -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/LePhilippeDucTai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a9a218f37bb23711704bbc7fc87a8fc678ec2bff -
Trigger Event:
push
-
Statement type:
File details
Details for the file banditrs-0.2.1-py3-none-win_amd64.whl.
File metadata
- Download URL: banditrs-0.2.1-py3-none-win_amd64.whl
- Upload date:
- Size: 5.7 MB
- Tags: Python 3, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4e65a45473451845e0b68fa70ee0f0f40af73dd4f71114fb3c09f93e2c4e64ce
|
|
| MD5 |
e8e9fbc88d0b1dc02c1d8fdfc5c99611
|
|
| BLAKE2b-256 |
ac5b6c1bda7d82ff316c6723a1f81edfe04eac0ab1905a424a567657f882b5e4
|
Provenance
The following attestation bundles were made for banditrs-0.2.1-py3-none-win_amd64.whl:
Publisher:
release.yml on LePhilippeDucTai/BanditRS
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
banditrs-0.2.1-py3-none-win_amd64.whl -
Subject digest:
4e65a45473451845e0b68fa70ee0f0f40af73dd4f71114fb3c09f93e2c4e64ce - Sigstore transparency entry: 2774162682
- Sigstore integration time:
-
Permalink:
LePhilippeDucTai/BanditRS@a9a218f37bb23711704bbc7fc87a8fc678ec2bff -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/LePhilippeDucTai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a9a218f37bb23711704bbc7fc87a8fc678ec2bff -
Trigger Event:
push
-
Statement type:
File details
Details for the file banditrs-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: banditrs-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 5.9 MB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5e58e042e1364eae9075e506fc1cc40f66d577d7abf4faa76db3347c30d49fe5
|
|
| MD5 |
2eaac1bd8a7d2a764840d61076538dc5
|
|
| BLAKE2b-256 |
5a6da738fc5d60b15222dfb1eae66cbe8964c11244cdc15f38144ecb837890d7
|
Provenance
The following attestation bundles were made for banditrs-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
release.yml on LePhilippeDucTai/BanditRS
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
banditrs-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
5e58e042e1364eae9075e506fc1cc40f66d577d7abf4faa76db3347c30d49fe5 - Sigstore transparency entry: 2774162629
- Sigstore integration time:
-
Permalink:
LePhilippeDucTai/BanditRS@a9a218f37bb23711704bbc7fc87a8fc678ec2bff -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/LePhilippeDucTai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a9a218f37bb23711704bbc7fc87a8fc678ec2bff -
Trigger Event:
push
-
Statement type:
File details
Details for the file banditrs-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: banditrs-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 5.7 MB
- Tags: Python 3, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9de97366e241a932869394fec20c58d42c113edb1de8e3ccbd78b7feb5bfad71
|
|
| MD5 |
5aad11a08daacfd3aa03d1668f56e406
|
|
| BLAKE2b-256 |
0be7349ffd6a7ca35dfc6200a1b0b4c6c684755ba5ffe087ff57a0278e83d7cf
|
Provenance
The following attestation bundles were made for banditrs-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:
Publisher:
release.yml on LePhilippeDucTai/BanditRS
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
banditrs-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl -
Subject digest:
9de97366e241a932869394fec20c58d42c113edb1de8e3ccbd78b7feb5bfad71 - Sigstore transparency entry: 2774162831
- Sigstore integration time:
-
Permalink:
LePhilippeDucTai/BanditRS@a9a218f37bb23711704bbc7fc87a8fc678ec2bff -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/LePhilippeDucTai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a9a218f37bb23711704bbc7fc87a8fc678ec2bff -
Trigger Event:
push
-
Statement type:
File details
Details for the file banditrs-0.2.1-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: banditrs-0.2.1-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 5.4 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d5da54d4a259ea6914e778668d6172310f4c04f5dd4aa9697913836e1a80d2f9
|
|
| MD5 |
940e7d39eb098ca35ddfca064faeab0c
|
|
| BLAKE2b-256 |
dd8d08b0300270d6412e64d7e25585a6d9058e42dac6a3f0a87d25a7efb27bed
|
Provenance
The following attestation bundles were made for banditrs-0.2.1-py3-none-macosx_11_0_arm64.whl:
Publisher:
release.yml on LePhilippeDucTai/BanditRS
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
banditrs-0.2.1-py3-none-macosx_11_0_arm64.whl -
Subject digest:
d5da54d4a259ea6914e778668d6172310f4c04f5dd4aa9697913836e1a80d2f9 - Sigstore transparency entry: 2774162557
- Sigstore integration time:
-
Permalink:
LePhilippeDucTai/BanditRS@a9a218f37bb23711704bbc7fc87a8fc678ec2bff -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/LePhilippeDucTai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a9a218f37bb23711704bbc7fc87a8fc678ec2bff -
Trigger Event:
push
-
Statement type:
File details
Details for the file banditrs-0.2.1-py3-none-macosx_10_12_x86_64.whl.
File metadata
- Download URL: banditrs-0.2.1-py3-none-macosx_10_12_x86_64.whl
- Upload date:
- Size: 5.7 MB
- Tags: Python 3, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dfa122957f4ff85f14b841bc552632635adb612ca4bb5629800d9fed5b1f56cd
|
|
| MD5 |
0b03acfef1449614f94b67d5b7718274
|
|
| BLAKE2b-256 |
f42adb4207ec527bcd2cd6505bf69e440ae518c5265c64618a0b168bf3999586
|
Provenance
The following attestation bundles were made for banditrs-0.2.1-py3-none-macosx_10_12_x86_64.whl:
Publisher:
release.yml on LePhilippeDucTai/BanditRS
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
banditrs-0.2.1-py3-none-macosx_10_12_x86_64.whl -
Subject digest:
dfa122957f4ff85f14b841bc552632635adb612ca4bb5629800d9fed5b1f56cd - Sigstore transparency entry: 2774162744
- Sigstore integration time:
-
Permalink:
LePhilippeDucTai/BanditRS@a9a218f37bb23711704bbc7fc87a8fc678ec2bff -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/LePhilippeDucTai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a9a218f37bb23711704bbc7fc87a8fc678ec2bff -
Trigger Event:
push
-
Statement type: