Scanners & coverage¶
Nephilex runs a set of scanners over one project view in a single pass. They share a
severity model and a report, so you get one prioritized list, not six. Run
nephilex scanners to see which are active in your install.
Each scanner below lists what it detects, what it deliberately does not, and its current limitations. Stating the limits is intentional: it is how you know where to still apply human review.
Secrets¶
Detects. Hardcoded credentials by signature (cloud keys, tokens, private
keys) and by entropy, with common false positives (placeholders, sample values)
filtered out. Optional live verification (--verify-secrets) checks whether a
found credential is still active, using read-only endpoints.
Does not. Scan git history by default (working tree only), and never stores or prints a full secret: values are redacted to a short prefix plus length everywhere.
Git history (opt-in). Pass --secrets-history to also scan every commit for
secrets that were committed and later deleted, the most dangerous case, since a
credential removed from the current files is still recoverable from history. Each
history finding says plainly: rotate it, deleting it from history is not enough.
Needs a local git repository with full history.
Limits. Entropy detection trades recall for low noise. The history scan reads a bounded amount of diff text (and says so if it truncates on a very large repo).
Dependencies (SCA)¶
Detects. Known-vulnerable dependencies using live OSV
data, including transitive packages resolved from lockfiles across PyPI, npm, Go,
Rust, Ruby, and PHP. With --reachability (experimental, off by default), a CVE
in a Python or npm package your code never imports is marked lower priority.
Prioritizes with exploit signals. Each CVE finding is enriched, best-effort,
with two free feeds: EPSS (FIRST.org, the probability the CVE is exploited in
the wild) and the CISA KEV catalog (confirmed exploited in the wild). KEV
membership raises a finding's likelihood so "reachable + KEV + high EPSS" sorts to
the top, without ever changing the severity your --fail-on gate depends on.
Disable with exploit_signals: false under the dependencies scanner options.
Machine-readable exploitability (VEX). Export an OpenVEX document with
nephilex scan ... -f vex. CVEs are affected with the fix. When the scan ran with
--reachability, a CVE in a package your code never imports is not_affected
with the standard vulnerable_code_not_in_execute_path justification; without it,
every CVE is affected. It pairs with an SBOM for the exposure story enterprises
increasingly request.
Does not. Prove exploitability. Reachability is an import-level heuristic, not a call-graph proof, and EPSS is a probability, not a guarantee.
Limits. Coverage is bounded by OSV. Network access is required to refresh advisories and exploit signals; results are cached on disk with a TTL and degrade cleanly offline (a missing feed simply means no enrichment, never a failed scan). Only package names, versions, and CVE ids ever leave your machine.
SAST (pattern rules)¶
Detects. Dangerous code patterns across many languages: shell=True,
os.system on dynamic input, unsafe yaml.load, weak hashing (including
hashlib.new("md5")), insecure deserialization, Markup()/mark_safe() on a
formatted string, |safe and {% autoescape false %} in templates, lxml
resolve_entities=True, and more, each with a CWE mapping and a concrete fix.
Graded by data flow (Python). When the ast extra is installed, a few rules
ask the data-flow engine what the payload is before reporting:
| Rule | Payload built only from literals | Unknown origin | Request data |
|---|---|---|---|
python-shell-true, python-os-system |
not reported | reported (medium confidence) | reported; the taint tier adds a high-confidence finding |
python-pickle-loads |
not reported | reported (low confidence) | reported; the taint tier adds a high-confidence finding |
python-yaml-load (unsafe loader) |
reported, low severity and confidence | reported as before | reported, high confidence |
"Built only from literals" includes values that passed a numeric conversion
(int()), an allowlist check, or a module-level constant bound once. The
finding's metadata.dataflow records the verdict. An unsafe YAML loader is
always reported because it is wrong even when today's input is constant.
Does not. Follow data across functions and files on its own (that is the taint tier below). Flag every dynamic call, on purpose, to stay low-noise.
Limits. Pattern rules are precise but shallow. Long lines are scanned in
bounded, overlapping windows so a regex never sees more than 2000 characters at
once. Findings carry 1-based start_column/end_column (end exclusive, SARIF
style) and the source line with its indentation as the snippet.
Taint / data-flow (AST)¶
Detects. Untrusted input flowing into a dangerous sink across function and file boundaries (injection, SSRF, path traversal, command execution), using tree-sitter for Python and JavaScript/TypeScript.
Does not. Cover every language at this depth. Requires the ast extra
(pip install "nephilex[ast]"); without it, Nephilex falls back to the
lightweight-taint code scanner and says so.
Python sinks and rule ids (ast-python.*, and ast-python-xfile.* when the
source or the sink is in another file):
| Rule | CWE | Sink |
|---|---|---|
ast-sql-injection |
89 | execute()/executemany() query argument |
ast-sql-string-building |
89 | query built with %, +, .format() or an f-string from a function parameter (low confidence, no traced source) |
ast-command-injection |
78 | os.system, os.popen, subprocess.*, os.exec*/os.spawn* |
ast-path-traversal |
22 | open, io.open, codecs.open, os/shutil file operations, send_file, the file name of send_from_directory, and file methods on a pathlib.Path built from the value (Path(x), base / x) |
ast-code-injection |
95 | eval, exec, compile |
ast-unsafe-deserialization |
502 | pickle/dill/marshal loads, yaml.load without a safe loader, yaml.unsafe_load |
ast-xss |
79 | value returned from a Flask/Bottle view (@app.route, or @app.get in a Flask file), Markup(), mark_safe(), Django HttpResponse |
ast-open-redirect |
601 | redirect(), HttpResponseRedirect, RedirectResponse, headers["Location"] = ... |
ast-xxe |
611 | XML parse of the value with a parser that has external entities enabled (setFeature(feature_external_ges, True), lxml resolve_entities=True) |
ast-ldap-injection |
90 | ldap3 search() base/filter, python-ldap search_s() (only in files importing ldap/ldap3) |
ast-xpath-injection |
643 | lxml xpath()/XPath(), elementpath.select(), ElementTree find*() in XML files |
ast-trust-boundary |
501 | request data written into session[...] (key or value) |
ast-weak-random |
330, 338 | a random module value (not SystemRandom) stored as a token, password, salt, nonce, session or cookie value, or returned from a function named like one |
How values are tracked. The engine is flow-sensitive within a function:
if/elif/else, match, loops and try are analyzed per branch and joined,
so if not p: p = "" keeps the taint of the other branch. Conditions that fold
to a constant (if 7 * 18 + num > 200, a match on a constant) prune the dead
branch. Literal dicts, lists (append, pop, indexing) and configparser
objects are tracked per key, so storing request data under one key does not
taint a read of another. A method returns request data when it reads the
request (return self.request.form.get(name)); calling it on an object built
from the request (Wrapper(request).get_form_parameter("q")) is a source, in
the same file (ast-python) or across files (ast-python-xfile).
Guards and sanitizers. CWE-specific sanitizers clear a value only for their
class: HTML escaping (html.escape, markupsafe.escape, any *escape*) for
XSS, escape_filter_chars for LDAP. Numeric conversion, secure_filename,
basename, shlex.quote and url_for clear every class. A block that returns
or raises when a check fails refines the value after it, for checks that are
complete mitigations: an allowlist membership test (x not in (...)) or
x.isdigit() clears everything, urlparse(u).netloc against an allowlist
clears open redirect, and a resolved path (resolve(), os.path.realpath)
checked with startswith/is_relative_to clears path traversal. Blocklist
checks ('../' in p, "contains no quote") are deliberately not trusted: an
absolute path or another quoting style gets past them. request.path is not a
source inside a view whose routes have no <converters>, because routing
already fixed it.
Limits. Strongest for Python and JS/TS. Interprocedural analysis is bounded (one call hop, plus request wrappers); extremely dynamic code can defeat static tracing. Guards are recognized only in the simple forms above. A file that needs more than a fixed amount of analysis work, or nests statements deeper than 60 levels, falls back to a conservative flat scan or is reported as skipped.
Infrastructure as Code¶
Detects. Misconfiguration in Dockerfiles, Terraform, and Kubernetes manifests: running as root, privileged containers, exposed services, weak defaults.
Does not. Assess a live cluster (that is the commercial nephilex-k8s add-on).
Limits. Static file analysis only; it evaluates the declared config, not the running state.
LLM / AI application security¶
Detects. The OWASP Top 10 for LLM Apps: insecure model-output handling, prompt injection, secrets in prompts, over-privileged agent tools, and unsafe model loading. See LLM / AI security.
Does not. Test a deployed model or run prompts against it.
Limits. A newer domain; rules are evolving. This is the area under the most active development.
Authentication and session (authz)¶
Detects. Sensitive routes without an auth decorator, object-id routes with
no visible ownership check, weak JWT validation, OAuth callbacks without
state/PKCE, tokens in URLs, and cookies set without security flags:
set_cookie(...) with secure=False or without secure=True is
authz.insecure-session-cookie (CWE-614), and without httponly=True is
authz.cookie-missing-httponly (CWE-1004). The call is read across lines; an
explicit False is high confidence, a missing flag medium.
Does not. Judge a flag set from configuration (secure=app.config[...]), or
know whether a cookie is a session cookie.
Agent config¶
Detects. Risky settings in MCP server and coding-agent configuration files
that a teammate or CI agent runs simply by opening the project (.mcp.json,
.claude/settings.json, .cursor/mcp.json, .vscode/mcp.json,
claude_desktop_config.json, Windsurf mcp_config.json, Cline, Roo Code and
Gemini CLI settings):
mcp-hardcoded-secret(OWASP LLM02): a literal credential in a server'senv,headersorargsinstead of a variable reference. Uses the secrets scanner's signatures; the value is redacted.mcp-unpinned-package(LLM03): a server started withnpx,bunx,pnpm dlx,uvx,pipx runordocker runon a package or image without a pinned version.mcp-plaintext-http(LLM03): a remote server reached over plainhttp://(loopback excepted).mcp-broad-filesystem-root(LLM06): a filesystem server oradditionalDirectoriesentry granted/, a drive root or a home directory.agent-auto-approve(LLM06): tools that run without confirmation (ClineautoApprove, Roo CodealwaysAllow, Gemini CLItrust: true, Claude CodeenableAllProjectMcpServers, a bareBashallow rule,bypassPermissions).
Does not. Start or contact the configured servers, or judge what a pinned package actually does.
Limits. Only the documented file names and keys are read; a client with a different layout is not covered until it is added.
Custom rules¶
Detects. Whatever you define. Drop YAML rules in .nephilex/rules/*.yml or point
Nephilex at a rules file; no code needed. See Writing plugins.
Limits. Custom rules loaded from a scanned repository are ignored for remote targets by default, so an untrusted repo cannot define rules that reconfigure the scanner.