vulnfind 3.0.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.
- vulnfind-3.0.0/PKG-INFO +182 -0
- vulnfind-3.0.0/README.md +166 -0
- vulnfind-3.0.0/pyproject.toml +33 -0
- vulnfind-3.0.0/setup.cfg +4 -0
- vulnfind-3.0.0/tests/test_automation.py +195 -0
- vulnfind-3.0.0/tests/test_codeflow.py +122 -0
- vulnfind-3.0.0/tests/test_core.py +69 -0
- vulnfind-3.0.0/tests/test_deep.py +194 -0
- vulnfind-3.0.0/tests/test_depintel.py +136 -0
- vulnfind-3.0.0/tests/test_deps_and_cli.py +169 -0
- vulnfind-3.0.0/tests/test_infra.py +266 -0
- vulnfind-3.0.0/tests/test_manifests.py +88 -0
- vulnfind-3.0.0/tests/test_platform.py +115 -0
- vulnfind-3.0.0/tests/test_scanners.py +184 -0
- vulnfind-3.0.0/tests/test_ux.py +206 -0
- vulnfind-3.0.0/tests/test_web.py +237 -0
- vulnfind-3.0.0/tests/test_web_extra.py +178 -0
- vulnfind-3.0.0/vulnfind/__init__.py +3 -0
- vulnfind-3.0.0/vulnfind/__main__.py +6 -0
- vulnfind-3.0.0/vulnfind/ai.py +124 -0
- vulnfind-3.0.0/vulnfind/automation.py +250 -0
- vulnfind-3.0.0/vulnfind/bench/__init__.py +86 -0
- vulnfind-3.0.0/vulnfind/bench/cases/Dockerfile +9 -0
- vulnfind-3.0.0/vulnfind/bench/cases/ci.yml +14 -0
- vulnfind-3.0.0/vulnfind/bench/cases/flask_app.py +42 -0
- vulnfind-3.0.0/vulnfind/bench/cases/handler.go +16 -0
- vulnfind-3.0.0/vulnfind/bench/cases/index.php +12 -0
- vulnfind-3.0.0/vulnfind/bench/cases/server.js +35 -0
- vulnfind-3.0.0/vulnfind/bench/cases/settings.py +9 -0
- vulnfind-3.0.0/vulnfind/cli.py +1338 -0
- vulnfind-3.0.0/vulnfind/cloud.py +471 -0
- vulnfind-3.0.0/vulnfind/customrules.py +87 -0
- vulnfind-3.0.0/vulnfind/cvss.py +96 -0
- vulnfind-3.0.0/vulnfind/depgraph.py +344 -0
- vulnfind-3.0.0/vulnfind/depintel.py +319 -0
- vulnfind-3.0.0/vulnfind/engine.py +292 -0
- vulnfind-3.0.0/vulnfind/enrich.py +152 -0
- vulnfind-3.0.0/vulnfind/fixer.py +166 -0
- vulnfind-3.0.0/vulnfind/gitutil.py +121 -0
- vulnfind-3.0.0/vulnfind/history.py +184 -0
- vulnfind-3.0.0/vulnfind/image.py +524 -0
- vulnfind-3.0.0/vulnfind/manifests.py +428 -0
- vulnfind-3.0.0/vulnfind/models.py +172 -0
- vulnfind-3.0.0/vulnfind/osv.py +277 -0
- vulnfind-3.0.0/vulnfind/plain.py +83 -0
- vulnfind-3.0.0/vulnfind/progress.py +157 -0
- vulnfind-3.0.0/vulnfind/reach.py +181 -0
- vulnfind-3.0.0/vulnfind/remote.py +78 -0
- vulnfind-3.0.0/vulnfind/reporters/__init__.py +26 -0
- vulnfind-3.0.0/vulnfind/reporters/formats.py +208 -0
- vulnfind-3.0.0/vulnfind/reporters/html.py +133 -0
- vulnfind-3.0.0/vulnfind/reporters/terminal.py +326 -0
- vulnfind-3.0.0/vulnfind/scanners/__init__.py +125 -0
- vulnfind-3.0.0/vulnfind/scanners/code.py +413 -0
- vulnfind-3.0.0/vulnfind/scanners/deps.py +144 -0
- vulnfind-3.0.0/vulnfind/scanners/files.py +126 -0
- vulnfind-3.0.0/vulnfind/scanners/history.py +120 -0
- vulnfind-3.0.0/vulnfind/scanners/iac.py +316 -0
- vulnfind-3.0.0/vulnfind/scanners/routes.py +277 -0
- vulnfind-3.0.0/vulnfind/scanners/secrets.py +247 -0
- vulnfind-3.0.0/vulnfind/scanners/taint_js.py +280 -0
- vulnfind-3.0.0/vulnfind/scanners/taint_lines.py +268 -0
- vulnfind-3.0.0/vulnfind/scanners/taint_python.py +768 -0
- vulnfind-3.0.0/vulnfind/selfupdate.py +59 -0
- vulnfind-3.0.0/vulnfind/triage.py +112 -0
- vulnfind-3.0.0/vulnfind/walker.py +299 -0
- vulnfind-3.0.0/vulnfind/web/__init__.py +61 -0
- vulnfind-3.0.0/vulnfind/web/active.py +131 -0
- vulnfind-3.0.0/vulnfind/web/api.py +130 -0
- vulnfind-3.0.0/vulnfind/web/browser.py +63 -0
- vulnfind-3.0.0/vulnfind/web/checks.py +314 -0
- vulnfind-3.0.0/vulnfind/web/client.py +180 -0
- vulnfind-3.0.0/vulnfind/web/crawl.py +150 -0
- vulnfind-3.0.0/vulnfind/web/dns.py +84 -0
- vulnfind-3.0.0/vulnfind/web/libs.py +208 -0
- vulnfind-3.0.0/vulnfind/web/login.py +109 -0
- vulnfind-3.0.0/vulnfind/web/paths.py +233 -0
- vulnfind-3.0.0/vulnfind/web/scanner.py +229 -0
- vulnfind-3.0.0/vulnfind/web/software.py +246 -0
- vulnfind-3.0.0/vulnfind/web/subdomains.py +149 -0
- vulnfind-3.0.0/vulnfind/web/tls.py +118 -0
- vulnfind-3.0.0/vulnfind.egg-info/PKG-INFO +182 -0
- vulnfind-3.0.0/vulnfind.egg-info/SOURCES.txt +85 -0
- vulnfind-3.0.0/vulnfind.egg-info/dependency_links.txt +1 -0
- vulnfind-3.0.0/vulnfind.egg-info/entry_points.txt +2 -0
- vulnfind-3.0.0/vulnfind.egg-info/requires.txt +3 -0
- vulnfind-3.0.0/vulnfind.egg-info/top_level.txt +1 -0
vulnfind-3.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vulnfind
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: Zero-dependency security scanner for code, websites, container images and cloud: CVEs with exploit intel and reachability, cross-file data-flow analysis, secrets, IaC and DAST.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/codingsushi79/vulnfind
|
|
7
|
+
Project-URL: Issues, https://github.com/codingsushi79/vulnfind/issues
|
|
8
|
+
Keywords: security,cve,sast,dast,secrets,sbom,osv,scanner,web
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Security
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Provides-Extra: ai
|
|
15
|
+
Requires-Dist: anthropic; extra == "ai"
|
|
16
|
+
|
|
17
|
+
# vulnfind
|
|
18
|
+
|
|
19
|
+
[github.com/codingsushi79/vulnfind](https://github.com/codingsushi79/vulnfind)
|
|
20
|
+
|
|
21
|
+
A zero-dependency security scanner for **code, websites, container images and cloud accounts**, with ranked results that tell you what to fix first.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
vulnfind ~/code/my-app # your code
|
|
25
|
+
vulnfind github.com/owner/repo # someone's repo, by URL (shallow clone, nothing executed)
|
|
26
|
+
vulnfind https://my-site.com # a website you own
|
|
27
|
+
vulnfind image my-app:latest # a container image
|
|
28
|
+
vulnfind k8s · vulnfind cloud aws # a live cluster / cloud account (read-only)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Python 3.8+. The core needs no third-party packages; the optional AI explanations use the official `anthropic` SDK.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv tool install git+https://github.com/codingsushi79/vulnfind # or: pipx install git+https://github.com/codingsushi79/vulnfind
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## What it finds
|
|
38
|
+
|
|
39
|
+
### In your code (`vulnfind <folder>`)
|
|
40
|
+
|
|
41
|
+
| Scanner | Depth |
|
|
42
|
+
|---|---|
|
|
43
|
+
| **deps** | Known CVEs, GHSA advisories and malicious packages from [OSV.dev](https://osv.dev) across 10 ecosystems, enriched with **CISA KEV** (exploited in the wild) and **EPSS** (exploit probability). For each package it shows the dependency chain (`express › body-parser › qs`), whether it's direct, whether your code imports it, and **whether your code calls the vulnerable function** (exact symbols for Go; names from the advisory text otherwise, labelled as a heuristic). Plus typosquats, suspicious npm install scripts, deprecated and abandoned packages, license conflicts (GPL/AGPL in a permissive or proprietary project), missing lockfiles and unpinned requirements. |
|
|
44
|
+
| **secrets** | 45+ credential formats plus generic passwords, filtered by entropy and placeholder detection, always masked in output. `--history` checks every commit, so deleted-but-still-in-git secrets are found along with the commit that added them. |
|
|
45
|
+
| **code** | **Data-flow analysis.** Python (AST, **across files**: views → services → db), JavaScript/TypeScript, Go and PHP: user input is traced through variables, string building and helper functions into SQL, shell, eval, deserialization, templates, the filesystem, outbound HTTP, redirects and HTML. Sanitizers and constants are understood, so their lines aren't flagged. **Route inventory and missing-auth detection** for Flask, FastAPI, Django REST, Express-style routers and Next.js (`vulnfind routes`); auth helpers are recognized one call deep. Each data flow says whether an anonymous user can reach it. ~100 more pattern rules across 10 languages. |
|
|
46
|
+
| **config** | Dockerfile, compose, Kubernetes, Terraform **and Terraform plan JSON**, GitHub Actions (pwn requests, script injection, unpinned actions), pip dependency confusion, end-of-life runtimes… |
|
|
47
|
+
| **files** | Key stores, `.env`, Terraform state, dumps, world-writable files, with git awareness (tracked, ignored, about to be committed). |
|
|
48
|
+
|
|
49
|
+
### On a website (`vulnfind <url>`, see [Website scanning](#website-scanning))
|
|
50
|
+
|
|
51
|
+
TLS and certificates, security headers, cookies, CORS, debug pages, ~50 exposed-file probes, **server software CVEs** from version banners (NVD), vulnerable front-end JS libraries, secrets in served JS, SPF/DMARC/CAA, **login-aware crawling**, **OpenAPI-driven API auth testing**, optional headless-browser rendering, and **subdomain takeover detection**. With `--active`, it also confirms reflected XSS, SQL errors and open redirects.
|
|
52
|
+
|
|
53
|
+
### In infrastructure
|
|
54
|
+
|
|
55
|
+
| Command | What it checks (read-only) |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `vulnfind image <ref>` | Reads a container image layer by layer without running it. Checks: OS packages (Debian/Ubuntu dpkg, Alpine apk, RPM), npm/Python/**Java JARs incl. fat JARs**, **Go binaries' embedded modules**, servers compiled from source (Apache, nginx, PHP, Node, Redis, Postgres → NVD), secrets left in the image, root user, secrets in `ENV`, end-of-life base OS. |
|
|
58
|
+
| `vulnfind k8s` | Workloads (privileged, host namespaces, sensitive hostPath, dangerous capabilities, root, missing limits, unpinned images, plain-text secrets in env), cluster-admin bindings (incl. anonymous), Ingress without TLS, namespaces without NetworkPolicy. Uses your `kubectl` context. |
|
|
59
|
+
| `vulnfind cloud aws` | Root MFA and keys, IAM users without MFA, old access keys, public S3 (policy, ACL, Block Public Access), security groups exposing admin/database ports, public or unencrypted RDS, IMDSv1, missing CloudTrail. Uses your `aws` CLI. |
|
|
60
|
+
| `vulnfind cloud gcp` | Firewall rules, public IAM bindings, primitive roles on service accounts, public buckets, Cloud SQL exposure and SSL. Uses your `gcloud` CLI. |
|
|
61
|
+
| `vulnfind tfplan plan.json` | The same Terraform checks on the planned changes (`terraform show -json plan.out`); folder scans pick plan files up automatically. |
|
|
62
|
+
|
|
63
|
+
Every finding gets a **priority score (0–100)** combining severity, exploitation evidence, reachability (imported? function called? reachable without login?), confidence and context. Every report opens with **"Fix these first"**.
|
|
64
|
+
|
|
65
|
+
## Everyday use
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
vulnfind # scan the current folder (live progress on stderr)
|
|
69
|
+
vulnfind -v # full descriptions, data flows, references, every CVE
|
|
70
|
+
vulnfind --plain # plain-language explanations for non-security readers
|
|
71
|
+
vulnfind --history # + secrets anywhere in git history
|
|
72
|
+
vulnfind ~/code/* # many projects -> one summary table
|
|
73
|
+
vulnfind routes # HTTP endpoints and whether each requires auth
|
|
74
|
+
vulnfind triage # walk findings: accept / false positive / ignore rule
|
|
75
|
+
vulnfind trend -o trend.html # how findings changed across past scans (chart)
|
|
76
|
+
vulnfind fix --apply # upgrade vulnerable deps (requirements, package.json + overrides, go.mod)
|
|
77
|
+
vulnfind ai --top 3 # Claude explains the top findings and drafts patches (optional)
|
|
78
|
+
vulnfind watch # re-scan on save
|
|
79
|
+
vulnfind explain CVE-2021-44228 # an advisory or any rule
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Triage once, share the decisions
|
|
83
|
+
|
|
84
|
+
`vulnfind triage` saves every decision, with a reason, who made it and when, in `.vulnfind-baseline.json`. Commit that file: later scans, CI and teammates load it automatically, so accepted risks and false positives stay hidden until the code changes. `rule:ID` entries go to `.vulnfindignore`.
|
|
85
|
+
|
|
86
|
+
### AI explanations (optional)
|
|
87
|
+
|
|
88
|
+
`vulnfind ai` sends the highest-priority findings to Claude (`claude-opus-5-5`) together with ±30 lines of the surrounding source, including every file a cross-file flow passes through. For each finding it returns a verdict (real issue vs. likely false positive), the concrete impact, a minimal unified-diff fix, and a way to verify it. Setup: `pip install "vulnfind[ai]"` plus `ANTHROPIC_API_KEY` or `ant auth login`. Requests enable the server-side refusal fallback (`fallbacks: "default"`), because security-analysis prompts can occasionally trip safety classifiers on benign code. **Nothing is sent unless you run this command.**
|
|
89
|
+
|
|
90
|
+
### VS Code
|
|
91
|
+
|
|
92
|
+
[`editors/vscode`](editors/vscode) is a small extension (needs vulnfind 3.0+ on PATH):
|
|
93
|
+
- **Inline findings** with severity colors.
|
|
94
|
+
- **Data-flow steps** shown as related locations.
|
|
95
|
+
- **A quick fix** that inserts `vulnfind:ignore RULE-ID`.
|
|
96
|
+
- **A status-bar counter**; clicking it runs a full scan including dependency CVEs.
|
|
97
|
+
|
|
98
|
+
## Website scanning
|
|
99
|
+
|
|
100
|
+
**Only scan sites you own or have written permission to test.**
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
vulnfind https://example.com # passive
|
|
104
|
+
vulnfind web https://staging.example.com --active # + confirm XSS / SQL errors / open redirects
|
|
105
|
+
VULNFIND_USER=qa@example.com VULNFIND_PASSWORD=… \
|
|
106
|
+
vulnfind web https://app.example.com --login-url https://app.example.com/login # crawl behind login
|
|
107
|
+
vulnfind web https://api.example.com --openapi https://api.example.com/openapi.json # API auth testing
|
|
108
|
+
vulnfind web https://spa.example.com --browser # render JS (needs: pip install playwright)
|
|
109
|
+
vulnfind subdomains example.com # CT-log discovery + takeover check
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
What it's careful about:
|
|
113
|
+
- **Passive by default.** GET/OPTIONS only, same-origin crawling, and it skips logout/delete-style links. It's rate-limited and capped (`--rate`, `--max-requests`).
|
|
114
|
+
- **No soft-404 false positives.** An exposed-file hit must match a content signature and differ from a random-path baseline.
|
|
115
|
+
- **Login safety.** Credentials come from environment variables only, so they never land in shell history. It refuses to send them over plain HTTP, to an untrusted certificate (unless `--insecure`), or to a form that posts to another origin. It also reports two login weaknesses: no CSRF token, and a session ID that isn't rotated at login.
|
|
116
|
+
- **OpenAPI testing is read-only.** It sends GET operations only, without credentials. An operation the spec says requires auth but that answers anyway is flagged HIGH.
|
|
117
|
+
- **Narrow `--active` mode.** It sends harmless markers to GET parameters the crawl already found, never submits POST forms, and stops at a budget.
|
|
118
|
+
- **Version-based server CVEs.** Matched from the advertised version, never by sending exploits. Distro builds that may carry backported fixes are marked medium confidence.
|
|
119
|
+
|
|
120
|
+
## Automation
|
|
121
|
+
|
|
122
|
+
| Need | Command |
|
|
123
|
+
|---|---|
|
|
124
|
+
| CI for any repo | `uses: codingsushi79/vulnfind@main` (this repo is a GitHub Action: SARIF upload, PR-diff mode, job summary, `grade` / `findings` outputs), or `vulnfind ci github --write` for a full workflow. |
|
|
125
|
+
| Block bad commits | `vulnfind hook install` scans **staged content** and keeps any hook you already had. Also available through the pre-commit framework (`.pre-commit-hooks.yaml`). |
|
|
126
|
+
| Monitor over time | `vulnfind monitor ~/code/app https://app.example.com image:app:latest --notify $SLACK` alerts only on **new** findings since the previous run. `--print-cron` gives a crontab line. |
|
|
127
|
+
| PR / branch scans | `vulnfind --diff origin/main` · `vulnfind --staged` |
|
|
128
|
+
| Gate on regressions | `vulnfind compare before.json after.json` exits 1 if anything new reaches `--fail-on`. |
|
|
129
|
+
| Alerts | `--notify URL` posts to Slack, Discord, Teams or any JSON webhook. `--quiet-ok` stays silent unless something fails. |
|
|
130
|
+
| Config as code | `vulnfind init` writes `.vulnfind.json` (defaults for every flag, plus **custom rules**) and `.vulnfindignore`. |
|
|
131
|
+
| Stay current | `vulnfind update` upgrades vulnfind using whatever installed it (uv, pipx or pip). |
|
|
132
|
+
|
|
133
|
+
### Custom rules
|
|
134
|
+
|
|
135
|
+
Add your own regex rules inline in `.vulnfind.json` (`"rules": [...]`), per project in `.vulnfind/rules/*.json`, or for every project in `~/.config/vulnfind/rules/*.json`:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{"rules": [{"id": "ACME-NO-INTERNAL-API", "title": "Internal admin API called from a handler", "severity": "high",
|
|
139
|
+
"pattern": "internal_admin_api\\(", "extensions": [".py"], "fix": "Go through the permission-checked service"}]}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Output formats
|
|
143
|
+
|
|
144
|
+
`-f` for stdout, or `-o FILE` (repeatable), with the format taken from the extension: table · json · sarif · github (inline PR annotations) · junit (`.xml`) · html · markdown · csv · CycloneDX SBOM (`.cdx.json`).
|
|
145
|
+
|
|
146
|
+
Exit codes: `0` clean · `1` findings at or above `--fail-on` (default `high`) · `2` usage/runtime error.
|
|
147
|
+
|
|
148
|
+
## Accuracy
|
|
149
|
+
|
|
150
|
+
`vulnfind bench` measures recall and precision on a labelled corpus. In each file, `expect: RULE` marks a line that must be flagged and `safe` marks one that must not. The corpus covers data flows in four languages, secrets, Dockerfile and GitHub Actions issues, plus deliberate traps: sanitized values, constants, parameterized queries, `RegExp#exec`, placeholder credentials, multi-line templates. Current result: **100% recall, 100% precision (39/39, 0 false positives on 21 safe lines)**. CI fails below 90%. The corpus is small and hand-written, so treat it as a regression guard, not a guarantee. Real-world findings deserve the same review you'd give any scanner.
|
|
151
|
+
|
|
152
|
+
Noise control is built in:
|
|
153
|
+
- **Comments and strings:** code rules ignore both.
|
|
154
|
+
- **Confirmed and cleared lines:** a confirmed data flow replaces the weaker pattern hit on the same line, and lines the flow engines prove clean are dropped.
|
|
155
|
+
- **Context:** findings in tests, examples and docs drop one level; bind-parameter SQL is downgraded; placeholder and localhost credentials are skipped.
|
|
156
|
+
- **Auth checks:** missing-auth findings need evidence that the project uses auth elsewhere.
|
|
157
|
+
|
|
158
|
+
## Privacy & network
|
|
159
|
+
|
|
160
|
+
- **Folder scans:** package names and versions go to OSV.dev and deps.dev, CVE IDs to FIRST (EPSS), and the CISA KEV feed is downloaded. Source code never leaves your machine.
|
|
161
|
+
- **Website scans:** requests go to the target. Library and server versions go to OSV and NVD (set `NVD_API_KEY` for faster lookups), DNS lookups to `cloudflare-dns.com`, and the domain to crt.sh / CertSpotter for `subdomains`.
|
|
162
|
+
- **Image, k8s and cloud:** these use your local Docker, kubectl, aws and gcloud tools, read-only. vulnfind never handles cloud credentials itself.
|
|
163
|
+
- **`vulnfind ai`:** sends findings and nearby source to the Claude API, only when you run it.
|
|
164
|
+
- **History:** a one-line summary per scan (counts and grade, no findings) in `~/.local/share/vulnfind` (`--no-record` or `VULNFIND_NO_HISTORY=1` turns it off).
|
|
165
|
+
- **Turning it off:** `--offline` disables third-party lookups.
|
|
166
|
+
|
|
167
|
+
## Limitations
|
|
168
|
+
|
|
169
|
+
- **Code:** data-flow analysis covers Python across files, and JS, Go and PHP within a file. The JS/Go/PHP engines read code line by line, so unusual code shapes can be missed.
|
|
170
|
+
- **Web:** a fast baseline, not a penetration test. It doesn't fuzz POST forms or find business-logic flaws, and server CVEs need a visible version banner.
|
|
171
|
+
- **Images:** Go binaries built with `(devel)` main modules can't be version-matched, and RHEL/UBI OS packages are inventoried but not CVE-checked (OSV has no feed).
|
|
172
|
+
- **Tested against recorded output:** the AWS/GCP/kubectl audits (no live accounts were available while building them), Playwright rendering (not installed here) and the AI command (exercised with a stub SDK).
|
|
173
|
+
|
|
174
|
+
## Development
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
python3 -m unittest discover -s tests # 152 tests, no dependencies (web tests use a local fixture site)
|
|
178
|
+
python3 -m vulnfind bench # precision / recall on the labelled corpus
|
|
179
|
+
python3 tests/web_fixture.py 8090 # a deliberately vulnerable local website to scan
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Rules are data: `vulnfind/scanners/{secrets,code,iac,files}.py`, taint sinks in `taint_python.py` / `taint_js.py` / `taint_lines.py`, web probes in `vulnfind/web/paths.py`. Add a labelled case to `vulnfind/bench/cases/` with every new rule.
|
vulnfind-3.0.0/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# vulnfind
|
|
2
|
+
|
|
3
|
+
[github.com/codingsushi79/vulnfind](https://github.com/codingsushi79/vulnfind)
|
|
4
|
+
|
|
5
|
+
A zero-dependency security scanner for **code, websites, container images and cloud accounts**, with ranked results that tell you what to fix first.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
vulnfind ~/code/my-app # your code
|
|
9
|
+
vulnfind github.com/owner/repo # someone's repo, by URL (shallow clone, nothing executed)
|
|
10
|
+
vulnfind https://my-site.com # a website you own
|
|
11
|
+
vulnfind image my-app:latest # a container image
|
|
12
|
+
vulnfind k8s · vulnfind cloud aws # a live cluster / cloud account (read-only)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Python 3.8+. The core needs no third-party packages; the optional AI explanations use the official `anthropic` SDK.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv tool install git+https://github.com/codingsushi79/vulnfind # or: pipx install git+https://github.com/codingsushi79/vulnfind
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What it finds
|
|
22
|
+
|
|
23
|
+
### In your code (`vulnfind <folder>`)
|
|
24
|
+
|
|
25
|
+
| Scanner | Depth |
|
|
26
|
+
|---|---|
|
|
27
|
+
| **deps** | Known CVEs, GHSA advisories and malicious packages from [OSV.dev](https://osv.dev) across 10 ecosystems, enriched with **CISA KEV** (exploited in the wild) and **EPSS** (exploit probability). For each package it shows the dependency chain (`express › body-parser › qs`), whether it's direct, whether your code imports it, and **whether your code calls the vulnerable function** (exact symbols for Go; names from the advisory text otherwise, labelled as a heuristic). Plus typosquats, suspicious npm install scripts, deprecated and abandoned packages, license conflicts (GPL/AGPL in a permissive or proprietary project), missing lockfiles and unpinned requirements. |
|
|
28
|
+
| **secrets** | 45+ credential formats plus generic passwords, filtered by entropy and placeholder detection, always masked in output. `--history` checks every commit, so deleted-but-still-in-git secrets are found along with the commit that added them. |
|
|
29
|
+
| **code** | **Data-flow analysis.** Python (AST, **across files**: views → services → db), JavaScript/TypeScript, Go and PHP: user input is traced through variables, string building and helper functions into SQL, shell, eval, deserialization, templates, the filesystem, outbound HTTP, redirects and HTML. Sanitizers and constants are understood, so their lines aren't flagged. **Route inventory and missing-auth detection** for Flask, FastAPI, Django REST, Express-style routers and Next.js (`vulnfind routes`); auth helpers are recognized one call deep. Each data flow says whether an anonymous user can reach it. ~100 more pattern rules across 10 languages. |
|
|
30
|
+
| **config** | Dockerfile, compose, Kubernetes, Terraform **and Terraform plan JSON**, GitHub Actions (pwn requests, script injection, unpinned actions), pip dependency confusion, end-of-life runtimes… |
|
|
31
|
+
| **files** | Key stores, `.env`, Terraform state, dumps, world-writable files, with git awareness (tracked, ignored, about to be committed). |
|
|
32
|
+
|
|
33
|
+
### On a website (`vulnfind <url>`, see [Website scanning](#website-scanning))
|
|
34
|
+
|
|
35
|
+
TLS and certificates, security headers, cookies, CORS, debug pages, ~50 exposed-file probes, **server software CVEs** from version banners (NVD), vulnerable front-end JS libraries, secrets in served JS, SPF/DMARC/CAA, **login-aware crawling**, **OpenAPI-driven API auth testing**, optional headless-browser rendering, and **subdomain takeover detection**. With `--active`, it also confirms reflected XSS, SQL errors and open redirects.
|
|
36
|
+
|
|
37
|
+
### In infrastructure
|
|
38
|
+
|
|
39
|
+
| Command | What it checks (read-only) |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `vulnfind image <ref>` | Reads a container image layer by layer without running it. Checks: OS packages (Debian/Ubuntu dpkg, Alpine apk, RPM), npm/Python/**Java JARs incl. fat JARs**, **Go binaries' embedded modules**, servers compiled from source (Apache, nginx, PHP, Node, Redis, Postgres → NVD), secrets left in the image, root user, secrets in `ENV`, end-of-life base OS. |
|
|
42
|
+
| `vulnfind k8s` | Workloads (privileged, host namespaces, sensitive hostPath, dangerous capabilities, root, missing limits, unpinned images, plain-text secrets in env), cluster-admin bindings (incl. anonymous), Ingress without TLS, namespaces without NetworkPolicy. Uses your `kubectl` context. |
|
|
43
|
+
| `vulnfind cloud aws` | Root MFA and keys, IAM users without MFA, old access keys, public S3 (policy, ACL, Block Public Access), security groups exposing admin/database ports, public or unencrypted RDS, IMDSv1, missing CloudTrail. Uses your `aws` CLI. |
|
|
44
|
+
| `vulnfind cloud gcp` | Firewall rules, public IAM bindings, primitive roles on service accounts, public buckets, Cloud SQL exposure and SSL. Uses your `gcloud` CLI. |
|
|
45
|
+
| `vulnfind tfplan plan.json` | The same Terraform checks on the planned changes (`terraform show -json plan.out`); folder scans pick plan files up automatically. |
|
|
46
|
+
|
|
47
|
+
Every finding gets a **priority score (0–100)** combining severity, exploitation evidence, reachability (imported? function called? reachable without login?), confidence and context. Every report opens with **"Fix these first"**.
|
|
48
|
+
|
|
49
|
+
## Everyday use
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
vulnfind # scan the current folder (live progress on stderr)
|
|
53
|
+
vulnfind -v # full descriptions, data flows, references, every CVE
|
|
54
|
+
vulnfind --plain # plain-language explanations for non-security readers
|
|
55
|
+
vulnfind --history # + secrets anywhere in git history
|
|
56
|
+
vulnfind ~/code/* # many projects -> one summary table
|
|
57
|
+
vulnfind routes # HTTP endpoints and whether each requires auth
|
|
58
|
+
vulnfind triage # walk findings: accept / false positive / ignore rule
|
|
59
|
+
vulnfind trend -o trend.html # how findings changed across past scans (chart)
|
|
60
|
+
vulnfind fix --apply # upgrade vulnerable deps (requirements, package.json + overrides, go.mod)
|
|
61
|
+
vulnfind ai --top 3 # Claude explains the top findings and drafts patches (optional)
|
|
62
|
+
vulnfind watch # re-scan on save
|
|
63
|
+
vulnfind explain CVE-2021-44228 # an advisory or any rule
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Triage once, share the decisions
|
|
67
|
+
|
|
68
|
+
`vulnfind triage` saves every decision, with a reason, who made it and when, in `.vulnfind-baseline.json`. Commit that file: later scans, CI and teammates load it automatically, so accepted risks and false positives stay hidden until the code changes. `rule:ID` entries go to `.vulnfindignore`.
|
|
69
|
+
|
|
70
|
+
### AI explanations (optional)
|
|
71
|
+
|
|
72
|
+
`vulnfind ai` sends the highest-priority findings to Claude (`claude-opus-5-5`) together with ±30 lines of the surrounding source, including every file a cross-file flow passes through. For each finding it returns a verdict (real issue vs. likely false positive), the concrete impact, a minimal unified-diff fix, and a way to verify it. Setup: `pip install "vulnfind[ai]"` plus `ANTHROPIC_API_KEY` or `ant auth login`. Requests enable the server-side refusal fallback (`fallbacks: "default"`), because security-analysis prompts can occasionally trip safety classifiers on benign code. **Nothing is sent unless you run this command.**
|
|
73
|
+
|
|
74
|
+
### VS Code
|
|
75
|
+
|
|
76
|
+
[`editors/vscode`](editors/vscode) is a small extension (needs vulnfind 3.0+ on PATH):
|
|
77
|
+
- **Inline findings** with severity colors.
|
|
78
|
+
- **Data-flow steps** shown as related locations.
|
|
79
|
+
- **A quick fix** that inserts `vulnfind:ignore RULE-ID`.
|
|
80
|
+
- **A status-bar counter**; clicking it runs a full scan including dependency CVEs.
|
|
81
|
+
|
|
82
|
+
## Website scanning
|
|
83
|
+
|
|
84
|
+
**Only scan sites you own or have written permission to test.**
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
vulnfind https://example.com # passive
|
|
88
|
+
vulnfind web https://staging.example.com --active # + confirm XSS / SQL errors / open redirects
|
|
89
|
+
VULNFIND_USER=qa@example.com VULNFIND_PASSWORD=… \
|
|
90
|
+
vulnfind web https://app.example.com --login-url https://app.example.com/login # crawl behind login
|
|
91
|
+
vulnfind web https://api.example.com --openapi https://api.example.com/openapi.json # API auth testing
|
|
92
|
+
vulnfind web https://spa.example.com --browser # render JS (needs: pip install playwright)
|
|
93
|
+
vulnfind subdomains example.com # CT-log discovery + takeover check
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
What it's careful about:
|
|
97
|
+
- **Passive by default.** GET/OPTIONS only, same-origin crawling, and it skips logout/delete-style links. It's rate-limited and capped (`--rate`, `--max-requests`).
|
|
98
|
+
- **No soft-404 false positives.** An exposed-file hit must match a content signature and differ from a random-path baseline.
|
|
99
|
+
- **Login safety.** Credentials come from environment variables only, so they never land in shell history. It refuses to send them over plain HTTP, to an untrusted certificate (unless `--insecure`), or to a form that posts to another origin. It also reports two login weaknesses: no CSRF token, and a session ID that isn't rotated at login.
|
|
100
|
+
- **OpenAPI testing is read-only.** It sends GET operations only, without credentials. An operation the spec says requires auth but that answers anyway is flagged HIGH.
|
|
101
|
+
- **Narrow `--active` mode.** It sends harmless markers to GET parameters the crawl already found, never submits POST forms, and stops at a budget.
|
|
102
|
+
- **Version-based server CVEs.** Matched from the advertised version, never by sending exploits. Distro builds that may carry backported fixes are marked medium confidence.
|
|
103
|
+
|
|
104
|
+
## Automation
|
|
105
|
+
|
|
106
|
+
| Need | Command |
|
|
107
|
+
|---|---|
|
|
108
|
+
| CI for any repo | `uses: codingsushi79/vulnfind@main` (this repo is a GitHub Action: SARIF upload, PR-diff mode, job summary, `grade` / `findings` outputs), or `vulnfind ci github --write` for a full workflow. |
|
|
109
|
+
| Block bad commits | `vulnfind hook install` scans **staged content** and keeps any hook you already had. Also available through the pre-commit framework (`.pre-commit-hooks.yaml`). |
|
|
110
|
+
| Monitor over time | `vulnfind monitor ~/code/app https://app.example.com image:app:latest --notify $SLACK` alerts only on **new** findings since the previous run. `--print-cron` gives a crontab line. |
|
|
111
|
+
| PR / branch scans | `vulnfind --diff origin/main` · `vulnfind --staged` |
|
|
112
|
+
| Gate on regressions | `vulnfind compare before.json after.json` exits 1 if anything new reaches `--fail-on`. |
|
|
113
|
+
| Alerts | `--notify URL` posts to Slack, Discord, Teams or any JSON webhook. `--quiet-ok` stays silent unless something fails. |
|
|
114
|
+
| Config as code | `vulnfind init` writes `.vulnfind.json` (defaults for every flag, plus **custom rules**) and `.vulnfindignore`. |
|
|
115
|
+
| Stay current | `vulnfind update` upgrades vulnfind using whatever installed it (uv, pipx or pip). |
|
|
116
|
+
|
|
117
|
+
### Custom rules
|
|
118
|
+
|
|
119
|
+
Add your own regex rules inline in `.vulnfind.json` (`"rules": [...]`), per project in `.vulnfind/rules/*.json`, or for every project in `~/.config/vulnfind/rules/*.json`:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{"rules": [{"id": "ACME-NO-INTERNAL-API", "title": "Internal admin API called from a handler", "severity": "high",
|
|
123
|
+
"pattern": "internal_admin_api\\(", "extensions": [".py"], "fix": "Go through the permission-checked service"}]}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Output formats
|
|
127
|
+
|
|
128
|
+
`-f` for stdout, or `-o FILE` (repeatable), with the format taken from the extension: table · json · sarif · github (inline PR annotations) · junit (`.xml`) · html · markdown · csv · CycloneDX SBOM (`.cdx.json`).
|
|
129
|
+
|
|
130
|
+
Exit codes: `0` clean · `1` findings at or above `--fail-on` (default `high`) · `2` usage/runtime error.
|
|
131
|
+
|
|
132
|
+
## Accuracy
|
|
133
|
+
|
|
134
|
+
`vulnfind bench` measures recall and precision on a labelled corpus. In each file, `expect: RULE` marks a line that must be flagged and `safe` marks one that must not. The corpus covers data flows in four languages, secrets, Dockerfile and GitHub Actions issues, plus deliberate traps: sanitized values, constants, parameterized queries, `RegExp#exec`, placeholder credentials, multi-line templates. Current result: **100% recall, 100% precision (39/39, 0 false positives on 21 safe lines)**. CI fails below 90%. The corpus is small and hand-written, so treat it as a regression guard, not a guarantee. Real-world findings deserve the same review you'd give any scanner.
|
|
135
|
+
|
|
136
|
+
Noise control is built in:
|
|
137
|
+
- **Comments and strings:** code rules ignore both.
|
|
138
|
+
- **Confirmed and cleared lines:** a confirmed data flow replaces the weaker pattern hit on the same line, and lines the flow engines prove clean are dropped.
|
|
139
|
+
- **Context:** findings in tests, examples and docs drop one level; bind-parameter SQL is downgraded; placeholder and localhost credentials are skipped.
|
|
140
|
+
- **Auth checks:** missing-auth findings need evidence that the project uses auth elsewhere.
|
|
141
|
+
|
|
142
|
+
## Privacy & network
|
|
143
|
+
|
|
144
|
+
- **Folder scans:** package names and versions go to OSV.dev and deps.dev, CVE IDs to FIRST (EPSS), and the CISA KEV feed is downloaded. Source code never leaves your machine.
|
|
145
|
+
- **Website scans:** requests go to the target. Library and server versions go to OSV and NVD (set `NVD_API_KEY` for faster lookups), DNS lookups to `cloudflare-dns.com`, and the domain to crt.sh / CertSpotter for `subdomains`.
|
|
146
|
+
- **Image, k8s and cloud:** these use your local Docker, kubectl, aws and gcloud tools, read-only. vulnfind never handles cloud credentials itself.
|
|
147
|
+
- **`vulnfind ai`:** sends findings and nearby source to the Claude API, only when you run it.
|
|
148
|
+
- **History:** a one-line summary per scan (counts and grade, no findings) in `~/.local/share/vulnfind` (`--no-record` or `VULNFIND_NO_HISTORY=1` turns it off).
|
|
149
|
+
- **Turning it off:** `--offline` disables third-party lookups.
|
|
150
|
+
|
|
151
|
+
## Limitations
|
|
152
|
+
|
|
153
|
+
- **Code:** data-flow analysis covers Python across files, and JS, Go and PHP within a file. The JS/Go/PHP engines read code line by line, so unusual code shapes can be missed.
|
|
154
|
+
- **Web:** a fast baseline, not a penetration test. It doesn't fuzz POST forms or find business-logic flaws, and server CVEs need a visible version banner.
|
|
155
|
+
- **Images:** Go binaries built with `(devel)` main modules can't be version-matched, and RHEL/UBI OS packages are inventoried but not CVE-checked (OSV has no feed).
|
|
156
|
+
- **Tested against recorded output:** the AWS/GCP/kubectl audits (no live accounts were available while building them), Playwright rendering (not installed here) and the AI command (exercised with a stub SDK).
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
python3 -m unittest discover -s tests # 152 tests, no dependencies (web tests use a local fixture site)
|
|
162
|
+
python3 -m vulnfind bench # precision / recall on the labelled corpus
|
|
163
|
+
python3 tests/web_fixture.py 8090 # a deliberately vulnerable local website to scan
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Rules are data: `vulnfind/scanners/{secrets,code,iac,files}.py`, taint sinks in `taint_python.py` / `taint_js.py` / `taint_lines.py`, web probes in `vulnfind/web/paths.py`. Add a labelled case to `vulnfind/bench/cases/` with every new rule.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vulnfind"
|
|
7
|
+
version = "3.0.0"
|
|
8
|
+
description = "Zero-dependency security scanner for code, websites, container images and cloud: CVEs with exploit intel and reachability, cross-file data-flow analysis, secrets, IaC and DAST."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["security", "cve", "sast", "dast", "secrets", "sbom", "osv", "scanner", "web"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Environment :: Console",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Topic :: Security",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
ai = ["anthropic"] # only for `vulnfind ai`; everything else is dependency-free
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/codingsushi79/vulnfind"
|
|
24
|
+
Issues = "https://github.com/codingsushi79/vulnfind/issues"
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
vulnfind = "vulnfind.cli:main"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
include = ["vulnfind*"]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.package-data]
|
|
33
|
+
"vulnfind.bench" = ["cases/*", "cases/.*"]
|
vulnfind-3.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Tests for automation: config files, git hooks, diff/staged modes, compare, fix, formats."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
import unittest
|
|
10
|
+
|
|
11
|
+
from vulnfind import automation, cli, fixer, gitutil
|
|
12
|
+
from vulnfind.models import Finding, ScanResult, Severity
|
|
13
|
+
from vulnfind.reporters import FORMATTERS, format_for_path
|
|
14
|
+
|
|
15
|
+
from helpers import TempProject
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def slurp(path):
|
|
19
|
+
with open(path) as fh:
|
|
20
|
+
return fh.read()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def dump(obj, path):
|
|
24
|
+
with open(path, "w") as fh:
|
|
25
|
+
json.dump(obj, fh)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_cli(*args):
|
|
29
|
+
out, err = io.StringIO(), io.StringIO()
|
|
30
|
+
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
|
|
31
|
+
code = cli.main(list(args))
|
|
32
|
+
return code, out.getvalue(), err.getvalue()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GitRepo:
|
|
36
|
+
def __enter__(self):
|
|
37
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
38
|
+
self.root = self.tmp.name
|
|
39
|
+
self.git("init", "-q", "-b", "main")
|
|
40
|
+
self.git("config", "user.email", "t@example.com")
|
|
41
|
+
self.git("config", "user.name", "Tester")
|
|
42
|
+
self.write("app.py", "x = 1\n")
|
|
43
|
+
self.git("add", ".")
|
|
44
|
+
self.git("commit", "-qm", "init")
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
def __exit__(self, *exc):
|
|
48
|
+
self.tmp.cleanup()
|
|
49
|
+
|
|
50
|
+
def git(self, *args):
|
|
51
|
+
return subprocess.run(["git", "-C", self.root, *args], capture_output=True, text=True)
|
|
52
|
+
|
|
53
|
+
def write(self, rel, text):
|
|
54
|
+
with open(os.path.join(self.root, rel), "w") as fh:
|
|
55
|
+
fh.write(text)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
SECRET = 'KEY = "ghp_aB3dE6gH9jK2mN5pQ8sT1vW4yZ7bC0eF3hJ6"\n'
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ConfigTest(unittest.TestCase):
|
|
62
|
+
def test_config_file_and_flag_precedence(self):
|
|
63
|
+
with TempProject({"app.py": "x = eval(y)\n", ".vulnfind.json": json.dumps({"scanners": "secrets", "fail-on": "none"})}) as root:
|
|
64
|
+
code, out, _ = run_cli(root, "--offline", "--no-progress", "-f", "json")
|
|
65
|
+
self.assertEqual((code, json.loads(out)["findings"]), (0, [])) # config: secrets only, never fail
|
|
66
|
+
code, out, _ = run_cli(root, "--offline", "--no-progress", "-f", "json", "--scanners", "code", "--fail-on", "high")
|
|
67
|
+
self.assertEqual(code, 1) # flags win
|
|
68
|
+
self.assertEqual(json.loads(out)["findings"][0]["rule_id"], "PY-EVAL")
|
|
69
|
+
|
|
70
|
+
def test_bad_config_is_a_clear_error(self):
|
|
71
|
+
with TempProject({".vulnfind.json": json.dumps({"failon": "high"})}) as root:
|
|
72
|
+
code, _, err = run_cli(root, "--offline", "--no-progress")
|
|
73
|
+
self.assertEqual(code, 2)
|
|
74
|
+
self.assertIn("unknown option 'failon'", err)
|
|
75
|
+
|
|
76
|
+
def test_init_creates_files(self):
|
|
77
|
+
with TempProject({}) as root:
|
|
78
|
+
run_cli("init", root)
|
|
79
|
+
self.assertTrue(os.path.isfile(os.path.join(root, ".vulnfind.json")))
|
|
80
|
+
self.assertTrue(os.path.isfile(os.path.join(root, ".vulnfindignore")))
|
|
81
|
+
automation.load_config(os.path.join(root, ".vulnfind.json")) # the generated file must load
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class GitModesTest(unittest.TestCase):
|
|
85
|
+
def test_staged_scans_index_content(self):
|
|
86
|
+
with GitRepo() as r:
|
|
87
|
+
r.write("cfg.py", SECRET)
|
|
88
|
+
r.git("add", "cfg.py")
|
|
89
|
+
r.write("cfg.py", "KEY = os.environ['KEY']\n") # working tree is clean, index is not
|
|
90
|
+
self.assertEqual(gitutil.staged_files(r.root), {"cfg.py"})
|
|
91
|
+
code, out, _ = run_cli(r.root, "--staged", "--offline", "--no-progress", "-f", "json")
|
|
92
|
+
self.assertEqual(code, 1)
|
|
93
|
+
self.assertEqual([f["rule_id"] for f in json.loads(out)["findings"]], ["SECRET-GITHUB-TOKEN"])
|
|
94
|
+
|
|
95
|
+
def test_diff_only_reports_changed_files(self):
|
|
96
|
+
with GitRepo() as r:
|
|
97
|
+
r.write("old.py", "x = eval(y)\n")
|
|
98
|
+
r.git("add", ".")
|
|
99
|
+
r.git("commit", "-qm", "old")
|
|
100
|
+
r.git("checkout", "-qb", "feature")
|
|
101
|
+
r.write("new.py", "z = eval(w)\n")
|
|
102
|
+
files = {f["file"] for f in json.loads(run_cli(r.root, "--diff", "main", "--offline", "--no-progress", "-f", "json")[1])["findings"]}
|
|
103
|
+
self.assertEqual(files, {"new.py"})
|
|
104
|
+
|
|
105
|
+
def test_hook_install_blocks_and_uninstall_restores(self):
|
|
106
|
+
with GitRepo() as r:
|
|
107
|
+
hooks = os.path.join(r.root, ".git", "hooks")
|
|
108
|
+
os.makedirs(hooks, exist_ok=True)
|
|
109
|
+
with open(os.path.join(hooks, "pre-commit"), "w") as fh:
|
|
110
|
+
fh.write("#!/bin/sh\necho existing-hook\n")
|
|
111
|
+
path = gitutil.install_hook(r.root)
|
|
112
|
+
gitutil.install_hook(r.root) # idempotent
|
|
113
|
+
text = slurp(path)
|
|
114
|
+
self.assertEqual(text.count(gitutil.HOOK_MARK), 1)
|
|
115
|
+
self.assertIn("existing-hook", text)
|
|
116
|
+
self.assertTrue(os.access(path, os.X_OK))
|
|
117
|
+
gitutil.uninstall_hook(r.root)
|
|
118
|
+
text = slurp(path)
|
|
119
|
+
self.assertNotIn(gitutil.HOOK_MARK, text)
|
|
120
|
+
self.assertIn("existing-hook", text)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class CompareTest(unittest.TestCase):
|
|
124
|
+
def test_new_and_fixed(self):
|
|
125
|
+
def report(*fps):
|
|
126
|
+
return {"findings": [{"fingerprint": fp, "severity": "HIGH", "title": fp, "file": "a.py", "line": 1} for fp in fps]}
|
|
127
|
+
diff = automation.compare(report("a", "b"), report("b", "c"))
|
|
128
|
+
self.assertEqual([f["fingerprint"] for f in diff["new"]], ["c"])
|
|
129
|
+
self.assertEqual([f["fingerprint"] for f in diff["fixed"]], ["a"])
|
|
130
|
+
with tempfile.TemporaryDirectory() as d:
|
|
131
|
+
old, new = os.path.join(d, "old.json"), os.path.join(d, "new.json")
|
|
132
|
+
dump(report("a"), old)
|
|
133
|
+
dump(report("a", "c"), new)
|
|
134
|
+
self.assertEqual(run_cli("compare", old, new)[0], 1)
|
|
135
|
+
self.assertEqual(run_cli("compare", new, old)[0], 0) # only fixes: success
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def dep(manifest, eco, name, version, fixed, aliases=None):
|
|
139
|
+
return Finding(aliases[0] if aliases else "CVE-1", "t", Severity.HIGH, "dependency", manifest,
|
|
140
|
+
extra={"package": name, "version": version, "ecosystem": eco, "fixed": fixed, "aliases": aliases or ["CVE-1"]})
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class FixerTest(unittest.TestCase):
|
|
144
|
+
def test_requirements_package_json_overrides_go(self):
|
|
145
|
+
files = {
|
|
146
|
+
"requirements.txt": "Django==3.2.0\nPyYAML[extra] == 5.3 # pinned\nflask>=2\n",
|
|
147
|
+
"package.json": '{\n "name": "x",\n "dependencies": {\n "lodash": "^4.17.20"\n }\n}\n',
|
|
148
|
+
"package-lock.json": "{}",
|
|
149
|
+
"go.mod": "module x\n\nrequire (\n\tgithub.com/gin-gonic/gin v1.6.3\n)\n",
|
|
150
|
+
}
|
|
151
|
+
with TempProject(files) as root:
|
|
152
|
+
res = ScanResult(root=root, findings=[
|
|
153
|
+
dep("requirements.txt", "PyPI", "django", "3.2.0", "3.2.14"),
|
|
154
|
+
dep("requirements.txt", "PyPI", "django", "3.2.0", "4.2.16"),
|
|
155
|
+
dep("requirements.txt", "PyPI", "pyyaml", "5.3", "5.4"),
|
|
156
|
+
dep("package-lock.json", "npm", "lodash", "4.17.20", "4.17.21"),
|
|
157
|
+
dep("package-lock.json", "npm", "qs", "6.7.0", "6.7.3"),
|
|
158
|
+
dep("go.mod", "Go", "github.com/gin-gonic/gin", "1.6.3", "1.9.1"),
|
|
159
|
+
dep("Cargo.lock", "crates.io", "smallvec", "1.6.0", "1.6.1"),
|
|
160
|
+
])
|
|
161
|
+
pl = fixer.plan(res)
|
|
162
|
+
fixer.apply(pl)
|
|
163
|
+
read = lambda p: slurp(os.path.join(root, p))
|
|
164
|
+
self.assertIn("Django==4.2.16", read("requirements.txt"))
|
|
165
|
+
self.assertIn("PyYAML[extra] == 5.4 # pinned", read("requirements.txt"))
|
|
166
|
+
pj = json.loads(read("package.json"))
|
|
167
|
+
self.assertEqual(pj["dependencies"]["lodash"], "^4.17.21")
|
|
168
|
+
self.assertEqual(pj["overrides"], {"qs": "6.7.3"})
|
|
169
|
+
self.assertIn("github.com/gin-gonic/gin v1.9.1", read("go.mod"))
|
|
170
|
+
self.assertTrue(any("cargo update -p smallvec" in c for c in pl.commands))
|
|
171
|
+
self.assertIn("npm install", pl.commands)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class FormatsTest(unittest.TestCase):
|
|
175
|
+
def setUp(self):
|
|
176
|
+
self.res = ScanResult(root="/x", findings=[
|
|
177
|
+
Finding("PY-EVAL", "Use of eval, really", Severity.HIGH, "code", "a,b.py", 3, description="d\nmore", fix="f")])
|
|
178
|
+
|
|
179
|
+
def test_github_annotations_escape(self):
|
|
180
|
+
out = FORMATTERS["github"](self.res)
|
|
181
|
+
self.assertTrue(out.startswith("::error file=a%2Cb.py,line=3,title=[HIGH] PY-EVAL%3A Use of eval%2C really::d%0Amore"))
|
|
182
|
+
|
|
183
|
+
def test_junit_is_valid_xml(self):
|
|
184
|
+
import xml.etree.ElementTree as ET
|
|
185
|
+
root = ET.fromstring(FORMATTERS["junit"](self.res))
|
|
186
|
+
self.assertEqual(root.get("failures"), "1")
|
|
187
|
+
|
|
188
|
+
def test_csv_and_path_inference(self):
|
|
189
|
+
self.assertIn("PY-EVAL", FORMATTERS["csv"](self.res).splitlines()[1])
|
|
190
|
+
self.assertEqual(format_for_path("out/sbom.cdx.json"), "sbom")
|
|
191
|
+
self.assertEqual(format_for_path("r.xml"), "junit")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
if __name__ == "__main__":
|
|
195
|
+
unittest.main()
|