fenceline
Zero-day security scanner for Python codebases. Generic, pip-install-anywhere
tool — pip install fenceline && fenceline scans whatever's under the
current directory, no project-specific setup required.
Installation
uv add fenceline
Maps every finding to a CWE from the 2025 CWE Top 25, OWASP Top 10:2025, and known Python zero-day exploit patterns (pickle bypass CVE-2026-56315, PyYAML shadow vulnerability CVE-2026-24009, LangChain SSTI CVE-2025-68664, dependency confusion CVE-2025-61774, and more — see the CWE reference printed at the end of every text-mode report). 56 checks total: most are AST-based (won't false-positive on a dangerous call mentioned in a docstring or comment); the rest are line-regex checks for surface-syntax patterns that don't need full parsing.
Usage
uv run fenceline
uv run fenceline --json > report.json
uv run fenceline -q
uv run fenceline --fail-on critical
uv run fenceline --package my-lib=my-lib/src/my_lib
uv run fenceline --packages my-lib other-lib
Exit codes: 0 (no findings at or above --fail-on), 1 (one or more).
Options
| Flag | Default | Description |
|---|---|---|
--json |
off | Machine-readable output |
--quiet / -q |
off | Suppress the banner |
--fail-on |
high |
Severity threshold (critical|high|medium|low|info) for the exit code |
--confidence-min |
low |
Drop findings below this confidence (high|medium|low) |
--package NAME=PATH |
— | Add a scan target package (repeatable); replaces cwd auto-discovery entirely when given |
--packages NAME [NAME ...] |
all | Scan only these names from the resolved registry |
--baseline PATH |
— | Only report/fail on findings not already present in this baseline |
--write-baseline PATH |
— | Snapshot current findings to PATH and exit 0 |
What gets scanned by default
A bare fenceline invocation (no --package) auto-discovers packages from
the current directory: every immediate subdirectory containing at least one
.py file anywhere in its subtree becomes its own named package, skipping
noise directories (.venv, .git, __pycache__, node_modules, build,
dist, *.egg-info, and similar caches — also excluded within a scanned
subtree, not just at the top level). Loose .py files sitting directly in
the current directory (outside any subdirectory) are scanned too, grouped
into a package named after the directory itself.
Pass --package NAME=PATH (repeatable) to scan an explicit set of
directories instead — this replaces auto-discovery entirely rather than
adding to it. Use --packages NAME [NAME ...] afterwards to narrow down to
a subset of whichever registry was resolved.
Severity vs. confidence
Every finding carries two independent ratings, the same distinction Bandit
makes: severity is how bad it would be if the finding is real; confidence
is how sure the check is that it is real. An AST-based check that matched a
real Call node is HIGH confidence; a line-regex check that can't fully
rule out a docstring or string-literal mention is MEDIUM; a handful of
inherently heuristic checks (timing-attack keyword proximity, ReDoS
backtracking-risk judgment, debug=True pattern matching) are LOW. Use
--confidence-min medium to cut noise from the fuzzier checks without
raising your severity bar.
Baselining an existing codebase
Adopting fenceline on a codebase with existing findings doesn't mean fixing all of them before CI can pass:
uv run fenceline --write-baseline fenceline-baseline.json # snapshot today's findings
uv run fenceline --baseline fenceline-baseline.json # only new findings fail CI
A finding is matched against the baseline by (cwe_id, file, code_snippet),
not line number, so it keeps matching across unrelated edits that shift line
numbers elsewhere in the file. Re-run --write-baseline periodically (or
whenever a baselined finding is deliberately fixed) to keep it current.
Suppressing a specific finding
# nosec (bare) suppresses every finding on that line; # nosec CWE-502
suppresses only that CWE, leaving any other finding on the same line intact
— the same convention Bandit uses, with CWE IDs instead of Bandit's own
B-numbers:
pickle.loads(data) # nosec CWE-502 -- trusted internal cache, not user input
Extending fenceline with your own checks
Built-in checks aren't dynamically discovered — they're a fixed list. Your
own checks are, via a fenceline.checks entry point in your own
pyproject.toml:
[project.entry-points."fenceline.checks"]
"My Custom Check (CWE-000)" = "my_package.checks:my_check_function"
The entry point's own name becomes the check's display name; the object it
points at must be a function with the same (path, lines, tree) -> list[Finding] signature every built-in check has. A plugin that fails to
import is skipped with a warning rather than crashing the whole scan.
Design
fenceline.models— theFindingdataclass, severity ordering, and confidence ordering.fenceline.config— workspace-root discovery and the default package registry.fenceline.ast_helpers— shared AST/text-matching helpers used across checks.fenceline.scanner— file discovery and reading.fenceline.checks— the built-in check registry, plusfenceline.checksentry-point discovery for third-party checks.fenceline.checks.ast_checks— checks that walk the parsed AST (won't match a dangerous call mentioned in a docstring or string literal).fenceline.checks.text_checks— checks that scan raw source lines for surface-syntax patterns.fenceline.checks.manifest_checks— checks over dependency manifests (pyproject.toml,requirements.txt, etc.), not Python source.fenceline.suppression—# nosecinline-suppression parsing.fenceline.baseline— baseline snapshot/diff for adopting fenceline on an existing codebase.fenceline.reporting— text/JSON report rendering.fenceline.cli— argument parsing and the scan loop.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
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 fenceline-1.1.1.tar.gz.
File metadata
- Download URL: fenceline-1.1.1.tar.gz
- Upload date:
- Size: 46.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7aa6a369f459dc8e4e7a8254ec842a6569df6d85714b836e16a9e6343d75b691
|
|
| MD5 |
3b3b80735ec83d52752b7a554eb6f3ad
|
|
| BLAKE2b-256 |
c08ea3bf727e16963b61be9a5b7ba6d842fbdf12261f94af298b88e17f1db77c
|
File details
Details for the file fenceline-1.1.1-py3-none-any.whl.
File metadata
- Download URL: fenceline-1.1.1-py3-none-any.whl
- Upload date:
- Size: 43.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4b24210929aa7b92ef236df2b2883684e64e9fc8c23611a34cebc7474521fa7d
|
|
| MD5 |
e494d97e2f6d40656462bb469941c87f
|
|
| BLAKE2b-256 |
2bbbd7a903ea62827b6ef0a21882bc1fa8bc0f1c5168dc225070017845f68ae3
|