Skip to main content

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). 57 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 --exclude tests/ examples/
uv run fenceline --config fenceline.toml
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
--config none TOML file with a packages table of name = "path" entries (see below); replaces cwd auto-discovery
--package none Add or override one package as NAME=PATH (repeatable); applied on top of --config
--packages all resolved packages Names to scan from the resolved registry
--exclude none Path substrings to exclude from scanning
--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)
--baseline PATH — Only report/fail on findings not already present in this baseline
--write-baseline PATH — Snapshot current findings to PATH and exit 0
--include-tests off Include CWE-798/617/918/770 findings inside test code (see "Test code" below)
--test-paths DIRNAME [DIRNAME ...] — Extra directory names to treat as non-production, alongside the built-in tests/test/conftest.py conventions (see "Test code" below)
--version — Print the installed fenceline version and exit

Test code

By default, findings for CWE-798 (hardcoded credentials), CWE-617 (reachable assert), CWE-918 (SSRF), and CWE-770 (unbounded resource allocation) are suppressed when they occur in a tests//test/ directory, a test_*.py/*_test.py file, or conftest.py — an ephemeral testcontainers password, a pytest assertion, a hardcoded localhost test URL, or a small committed fixture read isn't the same risk as the identical pattern in a production request handler, and scoring them identically drowns out real findings in the same category. Pass --include-tests to see them anyway. Production code paths are unaffected either way. This is a naming-convention heuristic only — a codebase-specific directory like an evaluation/ benchmark harness isn't recognized by name alone, since that name means different things in different codebases. Declare your own such directories explicitly with --test-paths, e.g. --test-paths evaluation benchmarks — they're unioned with the built-in conventions above.

What gets scanned by default

A bare fenceline invocation (no --config/--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.

To point it at other packages — in this workspace, another workspace, or any directory on disk — use --config and/or --package:

  1. No --config/--package → cwd auto-discovery, as above.
  2. --config given → its packages table is used as the full set, explicitly rather than auto-discovered.
  3. --package NAME=PATH entries are then overlaid on top of whichever set (1) or (2) produced — adding new names or overriding ones already defined, so a config file plus a quick ad-hoc addition both work together.

Use --packages NAME [NAME ...] afterwards to narrow down to a subset of whichever registry was resolved. Use --exclude SUBSTR [SUBSTR ...] to skip additional path substrings on top of the built-in noise-directory list.

--config: TOML file

# fenceline.toml
[packages]
my-lib = "my-lib/src/my_lib"
other = "../other/src/other"
uv run fenceline --config fenceline.toml

TOML rather than YAML deliberately: fenceline's own dependency list is intentionally empty, and the PyYAML shadow vulnerability (CVE-2026-24009, mentioned above) is itself one of the patterns fenceline scans for — adding a YAML dependency to a tool that flags YAML-library CVEs would be ironic. Python's stdlib tomllib covers this without adding one. Paths resolve relative to the config file, and every resolved path passes the same path-security sandbox check --package entries get.

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 — the Finding dataclass, 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, plus fenceline.checks entry-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 — # nosec inline-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.

Metadata

Release files for fenceline 1.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fenceline 1.5.0
File Size Uploaded Provenance Actions
fenceline-1.5.0.tar.gz 64.2 kB Without provenance Details for fenceline-1.5.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for fenceline 1.5.0
File Interpreter ABI Platform Actions
fenceline-1.5.0-py3-none-any.whl Python 3 none any Details for fenceline-1.5.0-py3-none-any.whl

Total release size: 120.0 kB

Release files / fenceline-1.5.0.tar.gz

Download URL fenceline-1.5.0.tar.gz
Size 64.2 kB
Tags Source
SHA-256 checksum
How to use checksums
efad53d81df9434036bd77d5b78de8eaa1fcfaf696d126478f2adcfc809ff049
BLAKE2b-256 checksum
How to use checksums
3ad3576b5786cebce9f78e5575656439600db1a4a87d5f271e0e6972e89dbe54
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release files / fenceline-1.5.0-py3-none-any.whl

Download URL fenceline-1.5.0-py3-none-any.whl
Size 55.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
10604608c2a5ef0a450277811c5f39fdbe61d434a9571ab0c27db21b92cec3f7
BLAKE2b-256 checksum
How to use checksums
a6980ec284d94ba0a538c0952e6bd2b5f57d6b74dfc6f20f515398ba3c53957d
Upload date
Uploaded using Trusted Publishing?
What is 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}

Security for fenceline 1.5.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page