One scanner for the workflow: introducing Suri
Every VAPT engagement starts the same way. Point Nikto at the target for baseline coverage. Run ZAP for passive analysis. Fire nuclei for template checks. Manually curl anything specific to the stack. Reconcile four sets of output into one client report.
The reconciliation is the part that eats the time. Four tools, four output formats, four vocabularies for the same finding. Half a day of a real engagement goes into deduping and rewriting rather than looking at what actually matters.
So I started to build Suri. Not because the existing tools are bad. They are excellent at what they each do. But no single one covers the surface I want covered on a modern engagement, and stitching them together every time is friction I can automate away.
It is also a first attempt of generating such a scanner from scratch, quite an intersting challenge, and there have been a lot of things that I learned from this. That alone justifies the time.
What Suri does
Suri is a Go binary that runs a scope-enforced scan against a web application and produces a client-shaped HTML or JSON report. The check set covers what I actually need surfaced on an engagement:
- Scope-enforced HTTP client with DNS rebinding protection
- Crawler with HTML link, script, form, sitemap, robots, and JS artifact extraction
- Admin path discovery with content-verified interesting-paths, no more
/adminfalse positives from SPA shells - API discovery from Swagger, OpenAPI, and GraphQL specs
- Cloud storage probing for S3, Azure Blob, and GCS with per-provider authorization
- Injection testing across XSS, SQLi (error and time-based), SSTI, command injection, and open redirect
- Coverage of query strings, form parameters, OpenAPI path parameters, and JSON request bodies
- Security header audit including Set-Cookie flag checks
- Backup file detection with content verification (SHA-256 identical or Jaccard similarity against original)
- WAF block page detection for Cloudflare, Akamai, Imperva, and AWS WAF
- Anti-CSRF token detection on POST forms
- Application error disclosure for 5xx stack traces
- Missing Subresource Integrity on cross-origin scripts
- HTML and JSON reports with curl reproduction commands per finding
- Diff engine for engagement re-scans
The stack: Go 1.23, SQLite for finding storage, single static binary, cross-platform.
github.com/osintph/suri, AGPL-3
Install
macOS:
brew tap osintph/tap
brew trust osintph/tap
brew install suri
sudo xattr -d com.apple.quarantine $(which suri)
suri --versionLinux:
wget https://github.com/osintph/suri/releases/download/v0.1.4/suri_0.1.4_linux_amd64.tar.gz
tar xzf suri_0.1.4_linux_amd64.tar.gz
./suri --versionWindows:
Download the Windows zip from the releases page, extract it, and add the folder containing suri.exe to your PATH.
Invoke-WebRequest -Uri "https://github.com/osintph/suri/releases/download/v0.1.4/suri_0.1.4_windows_amd64.zip" -OutFile "suri.zip"
Expand-Archive -Path suri.zip -DestinationPath .
.\suri.exe --version
Or use scoop or winget once packaging lands in v0.2.
Or build from source with Go 1.23+:
go install github.com/osintph/suri/cmd/suri@latestA first scan
Create a scope file:
toml
engagement_name = "my-engagement"
hostnames = ["target.example.com"]
ports = [443]Then run:
suri scan --scope scope.toml https://target.example.comReport generation:
suri report --scan <scan-id> --format html --out report.htmlRe-scan comparison across engagement phases:
suri diff --baseline <scan-id-1> --current <scan-id-2> --format html --out diff.htmlSample From a test run:

Sample of the html formatted report:




The findings also come in json format:

Comparison against the incumbent tools
Numbers matter more than promises, so I ran Suri, Nikto, ZAP baseline, and nuclei against the same targets and captured the results.
Target one: the-internet.herokuapp.com, a public practice target.
| Tool | Runtime | Findings | Approach |
|---|---|---|---|
| Nikto | 102 min | 13 items | Legacy vuln pattern scan |
| ZAP baseline | 5 min | 21 items | Passive HTTP response analysis |
| Nuclei | 4 min | 21 items | Template-based fingerprint scan |
| Suri v0.1.4 | 17 min | 260 items | Active injection plus API surface |
Target two: falconeye.osintph.info, one of my own production services fronted by Cloudflare.
| Tool | Runtime | Findings | Notes |
|---|---|---|---|
| Nikto | 1 hr, killed | 3 items | Cloudflare defeated it, could not finish |
| Nuclei | 2 min | 28 items | Detected WAF, kept scanning |
| Suri v0.1.4 | 45 sec | 13 items | Detected WAF, emitted coverage warning |
What that table actually says
Numbers without context are misleading, so a few honest observations from running these back to back.
Each tool has a different scan philosophy. Nikto looks for legacy known-vulnerable paths and configurations. ZAP baseline analyzes the passive properties of responses from URLs the crawler visits. Nuclei runs over 10,000 detection templates covering everything from tech fingerprinting to TLS metadata. Suri actively probes for injection and enumerates API surface using OpenAPI specs.
The overlap between them is not the interesting part. The overlap is where all four flag the same missing security headers. The interesting part is where their coverage differs.
Nikto goes deep on legacy web patterns and produces detailed output about server behavior, but on modern targets fronted by a CDN it grinds for hours. My run against my own Cloudflare-fronted service never completed. I killed it after an hour. The requests were mostly getting challenged by the WAF, and Nikto had no way to detect that or bail.
ZAP baseline is fast and passive. It runs against the URLs the crawler visits and reports on response properties. It caught cookie flag misses, JS library versions, and missing CORS headers. It does not probe for API endpoints or test injection at all, because that is not what a baseline scan is for.
Nuclei is fast and template-driven. It caught tech fingerprints, TLS details, DNS records, and header configurations that neither ZAP nor Suri report. It detected Cloudflare on my target and kept scanning. It does not do injection testing in its default template set, and its findings tend to skew informational rather than actionable.
Suri actively probes injection endpoints, enumerates API surface from OpenAPI specs, and produces a report structured around what a client would actually receive. It caught my exposed /openapi.json on FalconEye, which none of the other three flagged. It handles WAF-fronted targets in about a minute rather than an hour by detecting the block page pattern and reporting scan coverage limitations rather than churning silently.
The point of this comparison is not that Suri wins. The point is that for the engagement workflow I actually run, Suri covers the specific slice I care about (injection, API surface, WAF-aware behavior, client-shaped output) faster than the alternatives, and it complements rather than replaces the passive coverage of ZAP and the fingerprinting of nuclei. On any real engagement I still run all four. Suri is what lets me not run four to get the injection and API story.
About that 260 count
The-internet scan produced a big number. That number needs context. 247 of those 260 findings are the same cookie missing flag issue reported once per URL that returns a Set-Cookie header. Rack sets rack.session on nearly every response, so with about 80 URLs and multiple flag checks, the count multiplies. Deduping cookie findings by (cookie name, flag) so one architectural miss is one finding, not 80, is on the v0.2 roadmap.
There was also a high-severity command injection finding on /upload. I verified it manually: three curl requests against the same endpoint with and without a sleep payload all returned in about 1.1 seconds. False positive, caused by Heroku's per-request latency variance rather than payload processing. Suri's timing baseline caught the endpoint at 918ms, then a later payload request happened to hit a slower moment at 6.1 seconds, and the 5-second delta crossed the threshold.
This is a real limitation of timing-based checks against public shared hosting. The fix is nontrivial: increasing thresholds reduces false positives but misses real 5-second sleep payloads. Multi-sample baseline analysis or automatic control-payload retesting is on the roadmap.
The lesson matters more than the miss. Numbers on a scanner output are only useful when you understand what they represent. High counts against a target do not mean the target is highly vulnerable. Reviewing findings by hand is still part of the job. If your scanner claims to have found something, verify it before it hits the client report.
What Suri does not do
I want to be honest about the gaps, because a client report should be honest about them too.
Suri does not execute JavaScript. If your target uses JS to construct URLs or set form values, Suri will miss them. During the-internet scan, the site has a /redirect challenge that sets its target URL via a click handler at runtime. Suri did not find the redirect vulnerability there. Neither did Nikto or ZAP baseline, because they also do not execute JS. Headless browser support is on the roadmap.
Suri v0.1.x does not support authentication. It cannot log in, cannot maintain sessions, cannot re-fetch CSRF tokens. Most modern web app vulnerabilities live behind login, and Suri only tests the unauthenticated surface. The --cookie flag is planned for v0.2.
Suri does not check TLS certificates, DNS records, or side-channel signals. Nuclei is better at that.
Suri does not scan JS files for CVE-tagged library versions. ZAP does, and this is a planned addition.
What Suri found on my own deployment
I ran Suri against falconeye.osintph.info, my own OSINT toolkit. Here is what it caught:
api.openapi.spec-exposed on /openapi.json. FastAPI serves the complete OpenAPI spec at /openapi.json by default, which discloses every endpoint, every parameter, every request body schema, every response type. It is the complete attack map served publicly.
Fixed with a two-line change in the FastAPI app constructor:
python
app = FastAPI(
openapi_url=None if is_prod else "/openapi.json",
docs_url=None if is_prod else "/docs",
redoc_url=None if is_prod else "/redoc",
)web.headers.hsts medium confirmed, plus several other missing security headers.
web.sri.missing on four cross-origin script tags (tailwindcss and d3.js loaded from CDNs without integrity attributes).
web.forms.missing-csrf on the Formspree contact form.
scan.waf.detected info. Suri correctly identified Cloudflare, suppressed the false positives that a naive scanner would produce against WAF challenge pages, and warned that coverage on protected endpoints would be limited.
The wp-config.php false positive lesson from an earlier scan is worth calling out. My first FalconEye scan reported 10 findings involving wp-config.php and its .bak, .old, .orig, .swp variants. FalconEye is a Python FastAPI app. It does not have a wp-config.php. Why did Suri fire on it?
Because Cloudflare returns HTTP 200 (or 403) with an HTML challenge page when it blocks a probe. Suri's backup file check saw a 200 response, computed content similarity against the original URL response (also a Cloudflare page), got a Jaccard similarity above threshold, and emitted the finding. Working as designed on the wrong signal.
Adding WAF signature detection into the content verification path fixed it. The detector inspects the response body for known WAF block templates (Cloudflare's Sorry, you have been blocked, Cloudflare Ray ID:, cf-error-details, plus signatures for Akamai, Imperva, and AWS WAF) and suppresses findings when the response is actually a block page. False positives dropped from 10 to 0 on the re-scan.
That fix shipped in v0.1.2. The lesson is that content verification alone is not enough. You have to know when the content you are verifying is not the target's response at all.
What is next
Path parameter and JSON body injection landed in v0.1.3. My own OpenAPI-documented API endpoints are the reason I built this out first. The injection engine now covers query strings, form parameters, OpenAPI path parameters, and OpenAPI JSON request bodies.
v0.1.4 shipped cookie flag audit, anti-CSRF token detection, application error disclosure, missing SRI, longer default scan timeout for public targets, and an ASCII banner because every real tool needs one.
The near-term roadmap:
Authenticated scanning via --cookie. Multi-sample timing baseline to reduce timing-based false positives on variable-latency infrastructure. Cookie finding dedup by (cookie name, flag) to collapse 247 findings into 3 or 4. JS library CVE detection. Headless browser support for JS-set parameters. Debian packaging for Kali submission.
Contributions welcome. Issues tracker is open.
Ownership and the mundane bit
Suri is AGPL-3.0. If you fold it into a commercial scanner service, you have to publish your changes. If you use it during an engagement to serve your client, you do not have to publish anything. This is the license I settled on to keep the tool practitioner-first without giving away the value to commercial rebranders.
The project is at github.com/osintph/suri. Install, scan a target, tell me what happens.
Contributing
Suri is early. The check catalogue will grow, the reports will get better, the false positives will get squashed. If you want to help shape where it goes, here is how.
Try it on a target you have permission to scan. Look at what it found. Look at what it missed. Open an issue at github.com/osintph/suri/issues with the check ID, the URL pattern, and what you expected to see. Real engagement data is what makes this tool better than a lab exercise.
If you have a check you want to add, open a pull request. The code follows a straightforward pattern: one Go file per check, one test file, one entry in the registry. Look at internal/checks/web/cookies.go for a simple example, internal/checks/web/sqli.go for a complex one. Anything with a payload catalogue lives in TOML files inside the same package, easy to extend without touching Go code.
If you find a bug, especially a false positive or a check that stops firing on a real target, open an issue with the scan DB attached if you can share it. The identity hash on each finding makes it easy to trace what payload triggered what response, and having the DB means I can reproduce the finding without needing access to your target.
If you use Suri on a real engagement, tell me how it went. Blog, Twitter, email, GitHub discussion, whatever works. I do not care about GitHub stars, but I care about what checks the tool needs next.
IMPROVEMENTS.md in the repo is the roadmap and the honesty log. Every real-world gap surfaces there before the fix ships. If a limitation you hit is not already listed, add it via PR or issue.
Reach out if you have questions or comments or what to collaborate
Session ID: 059db238ab37c3d92615c5cc24b694da29c598cc13e27886053722404118e14271
