@pbpyrojust/universal-security-audit 0.0.0-stage → 0.2.0

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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +412 -2
  3. package/auth-config.example.json +12 -0
  4. package/bin/universal-security-audit.mjs +129 -0
  5. package/branding.example.json +10 -0
  6. package/package.json +60 -4
  7. package/scripts/compare-security-runs.mjs +94 -0
  8. package/scripts/generate-baseline.mjs +66 -0
  9. package/scripts/generate-security-report.mjs +99 -0
  10. package/scripts/generate-security-sarif.mjs +93 -0
  11. package/scripts/generate-security-tickets.mjs +126 -0
  12. package/scripts/lib/baseline.mjs +37 -0
  13. package/scripts/lib/drupal-admin-audit.mjs +84 -0
  14. package/scripts/lib/exposed-paths.mjs +111 -0
  15. package/scripts/lib/fingerprint.mjs +154 -0
  16. package/scripts/lib/graphql-introspect.mjs +55 -0
  17. package/scripts/lib/infra-topology.mjs +71 -0
  18. package/scripts/lib/intensity.mjs +30 -0
  19. package/scripts/lib/misc-checks.mjs +78 -0
  20. package/scripts/lib/pentest-recon.mjs +215 -0
  21. package/scripts/lib/pii-payment.mjs +79 -0
  22. package/scripts/lib/port-scan.mjs +72 -0
  23. package/scripts/lib/proxy-support.mjs +14 -0
  24. package/scripts/lib/report-builder.mjs +113 -0
  25. package/scripts/lib/scoring.mjs +44 -0
  26. package/scripts/lib/script-inventory.mjs +131 -0
  27. package/scripts/lib/security-headers.mjs +115 -0
  28. package/scripts/lib/security-txt.mjs +69 -0
  29. package/scripts/lib/subdomain-enum.mjs +64 -0
  30. package/scripts/lib/tls-enum.mjs +49 -0
  31. package/scripts/lib/vuln-lookup.mjs +118 -0
  32. package/scripts/lib/whois-lookup.mjs +44 -0
  33. package/scripts/lib/wp-admin-audit.mjs +102 -0
  34. package/scripts/lib/wp-plugin-directory.mjs +58 -0
  35. package/scripts/lib/wp-rest-audit.mjs +59 -0
  36. package/scripts/run-batch-audit.mjs +111 -0
  37. package/scripts/run-security-audit.mjs +1146 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Justin Adams
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,413 @@
1
- # Temporary Holding Version
1
+ # Universal Security Audit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Technical security & attack-surface audit CLI. Companion tool to [universal-seo-audit](../universal-seo-audit) and [universal-accessibility-audit](../universal-accessibility-audit) — same crawler/report/ticket/compare conventions, different lens.
4
+
5
+ Runs a battery of unauthenticated, publicly-visible checks against a website and produces a risk-graded HTML/PDF dashboard, CSV data files, a ticket-ready backlog, and run-over-run comparisons.
6
+
7
+ ## What's new in 0.2.0
8
+
9
+ - **security.txt (RFC 9116) validation:** finds it at `/.well-known/security.txt` (or the legacy `/security.txt`) and checks the required Contact and Expires fields, an expiry date that hasn't passed (and isn't more than a year out), HTTPS, `text/plain`, Canonical and Policy; reports PGP signing. A missing or expired file is a finding.
10
+ - **WPScan free tier:** when the daily limit (25 requests) is hit, the remaining WordPress CVE lookups are skipped with one clear message instead of an error per plugin.
11
+ - Playwright 1.64, undici 8.12 and current dependencies.
12
+
13
+ ## Part of Vinny
14
+
15
+ This scanner is the free, open-source engine behind the security checks in [Vinny](https://vinny.justwhat.net) and the [WordPress plugins](https://wordpress.justwhat.net). Everything here stays free.
16
+
17
+ ## ⚠️ Authorized use only
18
+
19
+ This tool actively sends requests to well-known admin/login/config paths, DNS-probes common subdomains, TCP-connects to common service ports, and queries third-party vulnerability databases. **Only run it against sites/infrastructure you own or have explicit written permission to test.** Every probe is a single unauthenticated GET/OPTIONS/DNS/TCP-connect to a publicly documented path, record, or port — no auth bypass, brute force, fuzzing, or payload injection is performed, and destructive HTTP verbs (PUT/DELETE) are only *introspected* via OPTIONS, never sent. The one exception is the **authenticated admin audit** (see below), which requires you to supply valid login credentials yourself — it only ever reads screens the logged-in account already has access to. Unauthorized scanning of third-party infrastructure may still violate computer-fraud laws or terms of service even when every individual request is "harmless" on its own.
20
+
21
+ ## What it checks
22
+
23
+ **Security headers** — A+–F grade covering CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, COOP/CORP, `Server`/`X-Powered-By` version disclosure, and cookie `Secure`/`HttpOnly`/`SameSite` flags.
24
+
25
+ **Platform & infrastructure fingerprinting** — CMS/platform detection (WordPress, Drupal, Joomla, Shopify, Squarespace, Wix, Webflow, Magento, Ruby on Rails, Next.js/SPA); hosting/CDN detection (Cloudflare, Vercel, Netlify, Fastly, AWS S3/CloudFront, GitHub Pages, Pantheon, WP Engine, origin Apache/nginx); DNS records (A/AAAA/MX/NS/TXT); TLS certificate expiry and trust chain. Rails is detected via its CSRF meta tags (`csrf-param`/`csrf-token`), `authenticity_token` form fields, the `X-Runtime` header, or its default `_appname_session` cookie naming convention.
26
+
27
+ **Multi-platform / hybrid-architecture detection** — platform signals and backend headers (`Server`/`X-Powered-By`) are compared *across every scanned page*, not just the homepage. A different high-confidence platform (or a different backend header) showing up on a different page is reported as a likely hybrid architecture (e.g. a WordPress marketing site with a separately-built donation/fundraising app) — each stack has its own patching surface and CVE exposure. This only works as well as your page coverage: use `--crawl` with a reasonable `--max-pages` for sites you suspect might be hybrid, since a single-page scan can only ever see whichever platform served that one page.
28
+
29
+ **Multi-server / reverse-proxy detection** — three complementary signals, all built from data the tool is already collecting or a handful of extra plain GETs to the homepage:
30
+ - **Reverse proxy indicators**: a `Via` header, or seeing both a CDN/edge signal (Cloudflare, Fastly, etc.) and an origin-server signal (nginx/Apache) in the same response
31
+ - **DNS load-balancing hint**: more than one A record with no recognized CDN in front (CDNs also return multiple IPs, but those are edge nodes, not your own servers — already excluded)
32
+ - **Response-header consistency**: several repeated requests to the homepage compared for `Server`/`X-Powered-By`/`Via`/`X-Served-By` differences — inconsistent answers to the same URL over time is a solid signal of multiple backend instances behind a load balancer that may not be equally patched/configured
33
+
34
+ **TLS protocol enumeration** — actually handshakes with the server pinned to each of TLS 1.0/1.1/1.2/1.3 (rather than only reporting whatever one default connection negotiated) and flags legacy protocols that are still accepted. Note: forcing specific *weak cipher suites* (RC4/3DES/NULL/export-grade) isn't included — modern Node's bundled OpenSSL has those compiled out entirely, so we cannot reliably ask a server that question from this client; claiming to test for them would risk a false "not vulnerable" reading.
35
+
36
+ **Exposed path / admin / API discovery** — well-known admin & login pages across platforms (`wp-login.php`, Joomla `/administrator/`, Drupal `/user/login`, cPanel, phpMyAdmin, Adminer, Craft CMS, Umbraco, TYPO3); exposed config/backup files (`.env`, `.git/`, `wp-config.php.bak`, `.htpasswd`, SQL dumps, `phpinfo.php`, `server-status`); exposed APIs (`wp-json`, `wp-json/wp/v2/users`, `xmlrpc.php`, `/graphql`); directory-listing detection. When a `/graphql` endpoint is found, the tool sends a real (read-only, standard) introspection query — if introspection is enabled, the response hands over the entire schema, so exposed type/mutation names (especially sensitive-sounding ones like `deleteUser`) are reported directly.
37
+
38
+ **Third-party script/library inventory + CVE lookups** — detects common front-end libraries and versions (from filenames, query strings, and CDN path segments), WordPress core/plugin/theme versions (generator meta tag, `readme.html`, plugin `readme.txt` stable tags), Drupal core version (`CHANGELOG.txt`). Cross-references detected versions against:
39
+ - **[OSV.dev](https://osv.dev)** (no API key needed) for npm-ecosystem JS libraries (jQuery, Bootstrap, Lodash, Angular, React, Vue, etc.)
40
+ - **[WPScan Vulnerability API](https://wpscan.com/api)** (free API key required) for WordPress core, plugins, and themes — findings are filtered against the detected installed version where known
41
+
42
+ **WHOIS / domain registration** (RDAP) — registrar, creation/expiration/last-changed dates, nameservers, and domain status codes via [RDAP](https://rdap.org) (the modern JSON/HTTP successor to WHOIS — no shelling out to a system `whois` binary). Flags expired or soon-to-expire registrations, since a lapsed domain can be re-registered by an attacker.
43
+
44
+ **Subdomain enumeration** — passive discovery via [crt.sh](https://crt.sh) certificate-transparency logs (no active DNS brute force), with an optional liveness check (single HEAD request per host, capped by `--intensity`) to see which discovered subdomains currently respond.
45
+
46
+ **Port scan** — a lightweight Node-native TCP *connect* scan (`node:net`, no nmap dependency) against ~40 common service ports (FTP/SSH/Telnet/SMTP/databases — MySQL/Postgres/MongoDB/Redis/Elasticsearch/Memcached — RDP/VNC/message queues/etc). A connect scan just completes the ordinary TCP handshake and closes, identical to what any client does when it connects; an optional passive banner read (never sends bytes first) is captured when the service volunteers one. Flags any of ~18 "should not be internet-facing" ports (databases, remote admin protocols) found open.
47
+
48
+ **Recon-phase pentest checks** (all passive or single-request) —
49
+ - **Subdomain takeover**: DNS CNAME lookups on the target + common subdomains (`www`, `dev`, `staging`, `api`, `cdn`, etc.), fingerprinted against known dangling-resource response bodies for GitHub Pages, Heroku, AWS S3, Azure, Surge, Bitbucket Pages, Ghost, Pantheon, WP Engine, Fastly, Shopify, Zendesk, Webflow, Netlify
50
+ - **HTTP method exposure**: `OPTIONS` request inspects the `Allow` header for risky methods (PUT/DELETE/TRACE/CONNECT) — no risky method is ever actually sent
51
+ - **Open redirect**: canary-URL probing of common redirect parameters (`redirect`, `next`, `return`, `dest`, `continue`, etc.), `Location` header inspected but never followed
52
+ - **Missing Subresource Integrity (SRI)**: cross-origin `<script>`/`<link>` tags without an `integrity` attribute
53
+ - **Exposed source maps**: `.js.map` files that leak original (unminified) source
54
+ - **Email spoofing defense**: SPF presence + DMARC record/policy (`_dmarc.<domain>` TXT lookup)
55
+ - **WordPress user enumeration**: `?author=1..5` redirect-based username disclosure
56
+ - **Insecure login forms**: password fields on plain-HTTP pages or with an `http://` form action
57
+ - **Verbose error/debug disclosure**: a nonexistent-path probe checked against PHP/.NET/Java/Python stack-trace signatures
58
+ - **JWT cookie weaknesses**: cookies shaped like JWTs are decoded (never verified/forged) and flagged for `alg: none` or a missing `exp` claim
59
+
60
+ **PII exposure scan** — emails, phone numbers, SSN-like numbers, Luhn-valid credit-card-like numbers, and leaked secrets (AWS access keys, Stripe live secret/publishable keys, Google API keys, Slack tokens, GitHub tokens, PEM private key blocks).
61
+
62
+ **Payment/donation detection** — Stripe, PayPal, Square, Donorbox, Classy, Network for Good, GiveWP, WooCommerce, Shopify Checkout, Venmo, Braintree, Authorize.Net, Kindful, Bloomerang, Convio/Luminate, plus card-input-without-recognized-processor detection.
63
+
64
+ **CORS misconfiguration, mixed content, and AI/SEO-crawler exposure** — reflected-Origin + credentials CORS check; `http://` resources on HTTPS pages; what `robots.txt`/`llms.txt`/sitemap reveal publicly (including sensitive-looking `Disallow` paths, which are visible to attackers and AI scrapers even though they aren't blocked). Sitemap discovery reads any `Sitemap:` directive(s) in `robots.txt` first (taking priority over guessing `sitemap_index.xml`/`wp-sitemap.xml`/`sitemap.xml`), and cross-checks the two: it flags when a working sitemap exists but isn't declared in `robots.txt` (crawlers that only read `robots.txt` for sitemap discovery would miss it) and when `robots.txt` declares a sitemap URL that doesn't actually resolve.
65
+
66
+ ## Authenticated admin audit (opt-in, WordPress + Drupal)
67
+
68
+ If you supply credentials (see **Authentication** below) and the site is running WordPress or Drupal, the tool logs in and audits the admin dashboard itself — everything here is a normal screen the logged-in account already has access to; nothing is written or changed.
69
+
70
+ ### WordPress
71
+
72
+ Two ways to authenticate, checked in this order:
73
+
74
+ 1. **Application Passwords (preferred)** — `--wp-app-user`/`--wp-app-password`. Uses the official REST API (`/wp-json/wp/v2/*`) over HTTP Basic Auth — no Playwright login, no HTML scraping, stable across admin-theme/core UI changes. Create one under **Users → Profile → Application Passwords** in wp-admin. Plugin/theme "outdated" status is cross-referenced against the official [wordpress.org Plugin/Theme Directory API](https://api.wordpress.org) rather than scraped update-nag text.
75
+ 2. **Form login (fallback)** — `--login-url`/`--username`/`--password`. Logs into `wp-login.php` with Playwright and scrapes `wp-admin` screens directly (plugins, themes, Site Health, users). Used automatically when no Application Password is supplied.
76
+
77
+ Either way, it reports:
78
+ - **WordPress core, PHP, and MySQL/MariaDB versions** (form-login method only — Application Passwords don't expose these via REST) — flags end-of-life PHP versions
79
+ - **Every installed plugin/theme** — name, version, active/inactive status, and outdated status (including *inactive* ones, which public-facing scans can't see at all)
80
+ - **User accounts + roles** — flags more than 3 Administrator accounts, and a literal `admin` username (the first guess in any credential-stuffing attempt)
81
+ - **Security plugin presence** — checks active plugins against known security/WAF plugin slugs (Wordfence, Sucuri, iThemes/SolidWP Security, All In One WP Security, Shield Security, WP Cerber, MalCare, Defender, etc.) and flags if none are active
82
+
83
+ ### Drupal
84
+
85
+ Form login only (`--login-url`/`--username`/`--password` against `/user/login`), then:
86
+ - **Drupal core, PHP, and database versions** from `/admin/reports/status` (parsed from visible text rather than theme-specific CSS classes, since Drupal admin themes vary more than WordPress's) — flags end-of-life Drupal major versions and PHP versions, and counts error/warning-level status-report items
87
+ - **Installed modules** — name, version, enabled/disabled status, from `/admin/modules`
88
+ - **User accounts + roles** — from `/admin/people`, same Administrator-count and `admin`-username checks as WordPress
89
+
90
+ This is the one part of the tool that requires you to hand it real credentials — treat `--auth-config` files (and Application Passwords) as secrets (they're already gitignored) and prefer a dedicated low-privilege-but-sufficient account over your own login where possible.
91
+
92
+ ## Install
93
+
94
+ From npm (`@pbpyrojust/universal-security-audit`):
95
+
96
+ ```bash
97
+ npm install -g @pbpyrojust/universal-security-audit
98
+ npx playwright install chromium
99
+ universal-security-audit --site https://www.example.com
100
+ ```
101
+
102
+ Or without installing: `npm exec --package @pbpyrojust/universal-security-audit -- universal-security-audit --site https://www.example.com`.
103
+
104
+ From a clone of this repository:
105
+
106
+ ```bash
107
+ pnpm install
108
+ npx playwright install chromium
109
+ ```
110
+
111
+ ## Usage
112
+
113
+ ```bash
114
+ node scripts/run-security-audit.mjs --site https://www.example.com
115
+ # or
116
+ pnpm security-audit --site https://www.example.com
117
+ # or, via the CLI wrapper
118
+ node bin/universal-security-audit.mjs audit --site https://www.example.com
119
+ ```
120
+
121
+ ### Flags
122
+
123
+ | Flag | Description |
124
+ | --- | --- |
125
+ | `--site <url>` | **(required)** Target website URL |
126
+ | `--max-pages <n>` | Max pages to scan. Omit it and there's no limit — sitemap discovery pulls every URL it finds, and `--crawl` follows links until it exhausts the site; pass a number to cap either one on large sites |
127
+ | `--crawl` | Discover pages by following internal links instead of using the sitemap |
128
+ | `--max-per-url-pattern <n>` | Crawl-trap guard (default 3): caps how many pages matching the same URL "shape" get crawled — path segments that look like IDs/hashes/years (`/participants/mypage/1174017/2026`) are collapsed to a placeholder (`/participants/mypage/:id/:id`) for this comparison, so a site with thousands of per-record pages (fundraiser pages, product pages, forum threads) doesn't turn an uncapped `--crawl` into tens of thousands of near-duplicate requests |
129
+ | `--max-depth <n>` | How many link "hops" from the homepage `--crawl` follows: `0` = homepage only, `1` = homepage + whatever it links to, `2` = plus one more layer, etc. Omit for no limit. Combine with a small `--max-pages` for a quick "top-level pages only" pass |
130
+ | `--slow` | Conservative mode: longer waits between requests |
131
+ | `--respect-robots` | Skip URLs/paths disallowed by robots.txt |
132
+ | `--intensity <light\|normal\|aggressive>` | One dial for concurrency/delay/probe-list-size across path probing, port scanning, and subdomain liveness checks (default `normal`) |
133
+ | `--wordlist <path>` | Extra file/dir paths to probe (one per line, `#` comments allowed), merged with the built-in curated list |
134
+ | `--skip-exposed-paths` | Skip the admin/login/config/API path-probing phase |
135
+ | `--skip-cve` | Skip live OSV.dev/WPScan CVE lookups |
136
+ | `--skip-recon` | Skip the recon-phase checks (subdomain takeover, open redirect, SRI, source maps, SPF/DMARC, user enum, verbose errors) |
137
+ | `--skip-port-scan` | Skip the TCP port scan |
138
+ | `--skip-subdomain-enum` | Skip crt.sh subdomain enumeration |
139
+ | `--skip-whois` | Skip the WHOIS/RDAP domain-registration lookup |
140
+ | `--skip-admin-audit` | Skip the authenticated admin audit even if credentials are supplied |
141
+ | `--wpscan-key <key>` | Your own WPScan Vulnerability API token (or set `WPSCAN_API_KEY` env var) — free tier is 25 requests/day; see [WordPress CVE lookups](#wordpress-cve-lookups) |
142
+ | `--brand-config <path>` | JSON file to white-label the HTML/PDF report (see below) |
143
+ | `--proxy <url>` | Route all traffic (fetch + browser) through an upstream proxy, e.g. Burp Suite / OWASP ZAP, for manual inspection/replay |
144
+ | `--fail-on <severity>` | Exit code 1 if any *active* (non-suppressed) finding at or above this severity exists (`critical`/`high`/`medium`/`low`) — for CI gating |
145
+ | `--min-grade <letter>` | Exit code 1 if the overall risk grade is below this letter (e.g. `--min-grade B`) — for CI gating |
146
+ | `--baseline <path>` | JSON file of known-accepted findings (by fingerprint) to suppress from scoring/CI gating while keeping them visible in the report — see **Baseline / exceptions** below |
147
+ | `--json` | Print the full findings/report as JSON to stdout (all progress output moves to stderr, so stdout stays clean for piping to `jq` etc.) — `full-report.json` is always written to the run directory regardless of this flag |
148
+
149
+ ### Authentication
150
+
151
+ For the authenticated admin audit (and for scanning staging/password-protected sites in general):
152
+
153
+ | Flag | Description |
154
+ | --- | --- |
155
+ | `--auth-config <path>` | JSON file with any of the fields below (see `auth-config.example.json`) |
156
+ | `--http-username` / `--http-password` | HTTP Basic Auth credentials (or `USA_HTTP_USERNAME`/`USA_HTTP_PASSWORD` env vars) |
157
+ | `--wp-app-user` / `--wp-app-password` | WordPress Application Password (or `USA_WP_APP_USER`/`USA_WP_APP_PASSWORD` env vars) — **preferred** for the WP admin audit; no Playwright login needed |
158
+ | `--login-url <url>` | Form-based login page (e.g. `https://example.com/wp-login.php` or `/user/login` for Drupal) — supplying this + `--username` enables the authenticated admin audit when no Application Password is given |
159
+ | `--username` / `--password` | Form login credentials (or `USA_LOGIN_USERNAME`/`USA_LOGIN_PASSWORD` env vars) |
160
+ | `--username-selector` / `--password-selector` / `--submit-selector` | CSS selectors for the login form fields (default to WordPress's `wp-login.php` field names) |
161
+ | `--ready-selector` | A selector to wait for post-login as a "login succeeded" signal |
162
+ | `--post-login-wait-ms` | Fallback wait time after submitting the login form (default 2000ms) |
163
+
164
+ ```bash
165
+ # WordPress via Application Password (preferred):
166
+ node scripts/run-security-audit.mjs --site https://www.example.com \
167
+ --wp-app-user your-admin-user --wp-app-password "xxxx xxxx xxxx xxxx xxxx xxxx"
168
+
169
+ # WordPress or Drupal via form login (fallback):
170
+ node scripts/run-security-audit.mjs --site https://www.example.com \
171
+ --login-url https://www.example.com/wp-login.php \
172
+ --username your-admin-user --password your-admin-password
173
+ ```
174
+
175
+ ### WordPress CVE lookups
176
+
177
+ WordPress core/plugin/theme vulnerability matching uses the [WPScan Vulnerability Database API](https://wpscan.com/api), which requires an API key — there's no shared/bundled key in this repo, so each user registers their own for free at wpscan.com. Set it either way:
178
+
179
+ ```bash
180
+ export WPSCAN_API_KEY=your-key-here
181
+ node scripts/run-security-audit.mjs --site https://www.example.com
182
+ ```
183
+
184
+ or pass it per-run with `--wpscan-key your-key-here` (this takes precedence over the env var). Without a key, core/plugin/theme versions are still detected and reported — just without live CVE matching.
185
+
186
+ The free WPScan tier is capped at **25 requests/day**. If a scan hits that cap mid-run (checking WordPress core plus every detected plugin/theme can use up the quota quickly), the tool detects the `429` response, prints a one-time warning to the console telling you the daily limit was hit, and skips the remaining WPScan lookups for that run rather than continuing to burn failed requests — everything else in the scan (headers, exposed paths, PII/payment detection, non-WPScan CVE sources, etc.) still completes normally. The quota resets daily; see [wpscan.com/api](https://wpscan.com/api) for paid tiers with a higher limit.
187
+
188
+ ## Output
189
+
190
+ Each run writes to `reports/<site>-<timestamp>/`:
191
+
192
+ | File | Contents |
193
+ | --- | --- |
194
+ | `security-dashboard.html` / `.pdf` | Visual risk-graded report |
195
+ | `findings-summary.csv` | All findings, sorted by severity — includes `suppressed`/`suppressedReason` columns when `--baseline` is used |
196
+ | `exposed-paths.csv` | Every probed admin/config/API path and its result |
197
+ | `script-inventory.csv` | Detected front-end libraries + versions |
198
+ | `wordpress-components.csv` | Detected WP plugins/themes + versions, from public-facing detection (WordPress sites only) |
199
+ | `vulnerabilities.csv` | Matched CVEs/advisories from OSV.dev + WPScan |
200
+ | `port-scan.csv` | Every scanned port, its state (open/closed/filtered), and any banner captured |
201
+ | `subdomains.csv` | Subdomains discovered via certificate-transparency logs (when any are found) |
202
+ | `wp-admin-plugins.csv` / `wp-admin-themes.csv` / `wp-admin-users.csv` | Authenticated WordPress admin-audit data (only written when credentials were supplied and the audit succeeded) |
203
+ | `drupal-admin-modules.csv` / `drupal-admin-users.csv` | Authenticated Drupal admin-audit data |
204
+ | `summary.json` | Machine-readable summary: risk grade, platform, hosting, DNS, TLS, WHOIS, subdomains, open ports, admin-audit data |
205
+ | `full-report.json` | Everything in `summary.json` plus every finding, all matched CVEs, all exposed-path results, and the full library inventory — the same document `--json` prints to stdout |
206
+
207
+ ## Reports, tickets, and comparisons
208
+
209
+ Same workflow shape as `universal-seo-audit` / `universal-accessibility-audit`: the live audit produces raw data, and three standalone scripts turn it into deliverables — usable independently of a live scan, against any existing `reports/<run>` directory.
210
+
211
+ ### Regenerate / re-brand the dashboard
212
+
213
+ ```bash
214
+ node scripts/generate-security-report.mjs --run-dir reports/<run-folder> --brand-config ./branding.json
215
+ ```
216
+
217
+ Rebuilds `security-dashboard.html`/`.pdf` from a run's `summary.json` + CSVs without re-scanning the site. Useful for white-labeling a past run or after a template change.
218
+
219
+ `branding.json` (see `branding.example.json`):
220
+
221
+ ```json
222
+ {
223
+ "companyName": "JustWhat.net",
224
+ "logo": "./assets/logo.png",
225
+ "primaryColor": "#ef4444",
226
+ "secondaryColor": "#0f172a",
227
+ "accentColor": "#22c55e",
228
+ "reportTitle": "Security & Attack Surface Audit",
229
+ "author": "Justin Adams",
230
+ "footerText": "Confidential — Prepared by JustWhat.net"
231
+ }
232
+ ```
233
+
234
+ ### Ticket-ready backlog
235
+
236
+ ```bash
237
+ node scripts/generate-security-tickets.mjs --run-dir reports/<run-folder>
238
+ ```
239
+
240
+ Writes `security-ticket-backlog.csv`: one ticket per site-wide issue (headers, TLS, CVEs, exposed paths, recon findings — deduped across pages) plus one ticket per page-level finding (PII, mixed content, missing SRI, insecure login forms, JWT issues), each with a priority (`P0`–`P4` from severity), description, and label set — ready to import into GitHub Issues, Jira, Linear, etc.
241
+
242
+ ### Compare two runs over time
243
+
244
+ ```bash
245
+ node scripts/compare-security-runs.mjs --before reports/run-a --after reports/run-b
246
+ ```
247
+
248
+ Writes `security-compare-summary.csv` (severity counts before/after/delta), `security-compare-new-findings.csv`, `security-compare-resolved-findings.csv`, and a human-readable `security-compare-summary.md` — including the risk-grade delta. Add `--out-dir <path>` to write these somewhere other than the "after" run's own directory (the default). Use this after remediation work to confirm fixes landed and nothing regressed.
249
+
250
+ ### SARIF export (GitHub code scanning)
251
+
252
+ ```bash
253
+ node scripts/generate-security-sarif.mjs --run-dir reports/<run-folder> --out results.sarif
254
+ ```
255
+
256
+ Converts `findings-summary.csv` into a valid SARIF 2.1.0 document — severity maps to SARIF `level` (critical/high → `error`, medium → `warning`, low/info → `note`) plus a `security-severity` score GitHub uses for its Security tab display. Locations use each finding's URL as the artifact URI (the same convention OWASP ZAP's SARIF exporter uses for web findings that aren't tied to a repo file). Wire it into a workflow with `github/codeql-action/upload-sarif`.
257
+
258
+ ### Baseline / exceptions
259
+
260
+ Known-accepted findings can be suppressed from risk scoring and `--fail-on`/`--min-grade` CI gating on future runs, without hiding them from the report:
261
+
262
+ ```bash
263
+ # Snapshot everything from the current run as accepted (edit the output afterward to add real reasons/expiry):
264
+ node scripts/generate-baseline.mjs --run-dir reports/<run-folder> --out ./security-baseline.json --reason "Accepted 2026-Q3, ticket JIRA-123"
265
+
266
+ # Use it on future runs:
267
+ node scripts/run-security-audit.mjs --site https://www.example.com --baseline ./security-baseline.json
268
+ ```
269
+
270
+ Suppression is keyed on a stable fingerprint of `category + title + url`, so a finding stays suppressed as long as it's the same issue at the same location. Suppressed findings still appear in `findings-summary.csv` and the dashboard (dimmed, with the acceptance reason), they just don't count toward the risk grade or trip `--fail-on`/`--min-grade`. Add an `"expires"` date (ISO format) to any baseline entry to make the exception temporary — after that date it stops applying automatically. `generate-baseline.mjs` flags: `--out <path>` (default `./security-baseline.json`), `--reason <text>`, `--expires <ISO date>`, `--overwrite` (replace the file instead of merging new exceptions into it).
271
+
272
+ ### Multi-site batch scanning
273
+
274
+ ```bash
275
+ node scripts/run-batch-audit.mjs --sites-file ./sites.txt --intensity light --skip-cve --concurrency 2 --out-dir ./reports
276
+ ```
277
+
278
+ `sites.txt` is one URL per line (`#` comments allowed). Runs the full audit against each site (`--concurrency N`, default 1/sequential — keep this low, each run launches its own browser), forwarding every other flag through to each individual audit. Every site still gets its normal `reports/<site>-<timestamp>/` output, plus an aggregated `batch-summary-<id>.csv`/`.md` (`--out-dir` controls where, default `./reports`) with one row per site (grade, score, severity counts) sorted worst-first — useful for agencies auditing a portfolio of client sites in one pass.
279
+
280
+ ### CLI wrapper
281
+
282
+ All of the above are also reachable through the packaged CLI:
283
+
284
+ ```bash
285
+ node bin/universal-security-audit.mjs audit --site https://www.example.com
286
+ node bin/universal-security-audit.mjs report --run-dir reports/<run-folder> --brand-config ./branding.json
287
+ node bin/universal-security-audit.mjs tickets --run-dir reports/<run-folder>
288
+ node bin/universal-security-audit.mjs compare --before reports/run-a --after reports/run-b
289
+ node bin/universal-security-audit.mjs sarif --run-dir reports/<run-folder> --out results.sarif
290
+ node bin/universal-security-audit.mjs baseline --run-dir reports/<run-folder> --out ./security-baseline.json
291
+ node bin/universal-security-audit.mjs batch-audit --sites-file ./sites.txt
292
+ node bin/universal-security-audit.mjs help
293
+ ```
294
+
295
+ ## Example workflows by site type / use case
296
+
297
+ ### Small stakeholder-facing overview (skip deep/individual-record pages)
298
+
299
+ For sites with lots of per-record pages (fundraiser/participant pages, product listings, forum threads, etc.) where you just want a quick, presentable snapshot rather than exhaustive coverage:
300
+
301
+ ```bash
302
+ node bin/universal-security-audit.mjs audit --site https://www.example.com \
303
+ --crawl --max-depth 2 --max-pages 25 --intensity light --respect-robots
304
+ ```
305
+
306
+ `--max-depth 2` keeps the crawl to the homepage, its direct nav links, and one layer of their children — the pages that actually represent the site's structure — without falling into individual per-record pages several links deep. `--max-pages 25` is a backstop in case even that top layer is large. Good enough to hand stakeholders a representative grade and findings list without a multi-hour run.
307
+
308
+ ### Quick first-pass on a live WordPress site
309
+
310
+ Default posture — fast, unauthenticated, safe to run against a client's live production site without asking anyone first (still get permission):
311
+
312
+ ```bash
313
+ node bin/universal-security-audit.mjs audit --site https://www.example.com --intensity light
314
+ ```
315
+
316
+ ### Full authenticated WordPress security review
317
+
318
+ Agency-style deep audit: create an Application Password in the client's wp-admin first (Users → Profile → Application Passwords), get a free [WPScan API key](https://wpscan.com/api), then:
319
+
320
+ ```bash
321
+ node bin/universal-security-audit.mjs audit --site https://www.example.com \
322
+ --wp-app-user admin --wp-app-password "xxxx xxxx xxxx xxxx xxxx xxxx" \
323
+ --wpscan-key YOUR_WPSCAN_KEY --crawl --max-pages 50
324
+ node bin/universal-security-audit.mjs tickets --run-dir ./reports/<run-id>
325
+ ```
326
+
327
+ Gives you plugin/theme CVEs, outdated inactive plugins nobody remembers installing, missing security plugins, and a ticket-ready backlog in one pass.
328
+
329
+ ### Drupal site
330
+
331
+ Same shape, form-login only (no Application Passwords in Drupal core):
332
+
333
+ ```bash
334
+ node bin/universal-security-audit.mjs audit --site https://www.example.com \
335
+ --login-url https://www.example.com/user/login --username admin --password your-password
336
+ ```
337
+
338
+ ### E-commerce / donation site
339
+
340
+ Payment-processor and PCI-scope questions matter most here; a full run already covers this, but it's worth calling out what to look at in the output — the `payment` category in `findings-summary.csv` and the "Payment/donation" card on the dashboard:
341
+
342
+ ```bash
343
+ node bin/universal-security-audit.mjs audit --site https://www.example.com
344
+ ```
345
+
346
+ Look for: a card-input form without a recognized tokenizing processor (Stripe/Square/Braintree/Authorize.Net) flagged `high`, missing HSTS/CSP on checkout pages, and mixed-content resources on any page that touches payment.
347
+
348
+ ### Staging / password-protected site (HTTP Basic Auth)
349
+
350
+ ```bash
351
+ node bin/universal-security-audit.mjs audit --site https://staging.example.com \
352
+ --http-username staging-user --http-password staging-pass --respect-robots
353
+ ```
354
+
355
+ ### CI/CD gate on every deploy (GitHub Actions)
356
+
357
+ ```bash
358
+ node bin/universal-security-audit.mjs audit --site https://www.example.com \
359
+ --intensity light --skip-port-scan --skip-subdomain-enum \
360
+ --baseline ./security-baseline.json --fail-on high --json > report.json
361
+ node bin/universal-security-audit.mjs sarif --run-dir "$(ls -td reports/*/ | head -1)" --out results.sarif
362
+ ```
363
+
364
+ Then upload `results.sarif` with `github/codeql-action/upload-sarif` so findings show up in the repo's Security tab. `--fail-on high` fails the build on anything high/critical that isn't already in the baseline; port scan/subdomain enum are skipped here since they're slower and less relevant to "did this deploy regress anything."
365
+
366
+ ### Agency portfolio — many client sites on a schedule
367
+
368
+ ```bash
369
+ node bin/universal-security-audit.mjs batch-audit --sites-file ./client-sites.txt \
370
+ --intensity light --skip-cve --concurrency 2
371
+ ```
372
+
373
+ Pair with a cron/scheduled job and diff each site's `summary.json` week over week (or use `compare` against the prior run per site) to catch regressions before a client notices.
374
+
375
+ ### Custom app / SPA / non-CMS site (Next.js, headless, etc.)
376
+
377
+ Platform fingerprinting won't find a CMS to fingerprint here, so CVE/plugin-version checks won't have much to do — the value comes from headers, TLS, CORS, exposed paths, port scan, and PII/secret scanning instead:
378
+
379
+ ```bash
380
+ node bin/universal-security-audit.mjs audit --site https://app.example.com --skip-cve
381
+ ```
382
+
383
+ ### Confirm remediation actually worked
384
+
385
+ ```bash
386
+ node bin/universal-security-audit.mjs audit --site https://www.example.com --intensity light # after fixes
387
+ node bin/universal-security-audit.mjs compare --before ./reports/<before-run> --after ./reports/<after-run>
388
+ ```
389
+
390
+ Check `security-compare-summary.md` for the risk-grade delta and confirm the specific findings you fixed show up under "Resolved findings," not "New findings" (which would mean something regressed elsewhere).
391
+
392
+ ### One-time deep pentest-style engagement
393
+
394
+ Maximum coverage, routed through Burp Suite so a human can inspect/replay anything interesting:
395
+
396
+ ```bash
397
+ node bin/universal-security-audit.mjs audit --site https://www.example.com \
398
+ --intensity aggressive --crawl --max-pages 100 \
399
+ --wordlist ./my-wordlist.txt --proxy http://127.0.0.1:8080 \
400
+ --wp-app-user admin --wp-app-password "xxxx xxxx xxxx xxxx xxxx xxxx" --wpscan-key YOUR_KEY
401
+ ```
402
+
403
+ ## Risk grading
404
+
405
+ Every finding is tagged `critical` / `high` / `medium` / `low` / `info`. The overall grade (A+–F) is a weighted deduction from 100 based on finding severity (critical −25, high −12, medium −5, low −2, info −0) — the header-grading module uses the same deduction methodology on its own 0–100 scale, loosely modeled on securityheaders.com / Mozilla Observatory.
406
+
407
+ ## Roadmap / not built yet
408
+
409
+ - `--crawl` is single-pass BFS link-following; no per-domain crawl-delay tuning yet
410
+ - No Drupal-specific CVE feed (SA-CORE) — Drupal core version is detected and reported, but not cross-referenced against known CVEs yet
411
+ - No favicon-hash-based CMS fingerprint boost
412
+ - Port scan is TCP-connect only (no UDP, no service-version fingerprinting beyond a passive banner read); weak-cipher-suite forcing isn't included (see the TLS section above for why)
413
+ - No login rate-limit/lockout detection or password-reset account enumeration — both are real pentest signals, but risk locking out real accounts or sending real emails, so they'd need explicit opt-in and heavy guardrails if ever added
@@ -0,0 +1,12 @@
1
+ {
2
+ "loginUrl": "https://www.example.com/wp-login.php",
3
+ "username": "your-admin-user",
4
+ "password": "your-admin-password",
5
+ "usernameSelector": "input[name='log']",
6
+ "passwordSelector": "input[name='pwd']",
7
+ "submitSelector": "#wp-submit",
8
+ "readySelector": "#wpadminbar",
9
+ "postLoginWaitMs": 2000,
10
+ "httpUsername": "",
11
+ "httpPassword": ""
12
+ }
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const root = path.resolve(__dirname, '..');
9
+
10
+ function run(script, args) {
11
+ const scriptPath = path.join(root, 'scripts', script);
12
+ const result = spawnSync(process.execPath, [scriptPath, ...args], { stdio: 'inherit', env: process.env });
13
+ process.exit(result.status ?? 1);
14
+ }
15
+
16
+ function printHelp() {
17
+ console.log(`universal-security-audit
18
+
19
+ Usage:
20
+ universal-security-audit <command> [options]
21
+
22
+ Commands:
23
+ audit Full security & attack-surface audit (headers, TLS protocol enumeration,
24
+ fingerprinting, exposed paths, live GraphQL introspection, port scan, subdomain
25
+ enum, WHOIS, CVE lookups, PII/payment detection, recon checks, optional
26
+ authenticated WordPress/Drupal admin audit) + HTML/PDF dashboard
27
+ report Regenerate the HTML/PDF dashboard from an existing run (e.g. after rebranding)
28
+ tickets Generate a ticket-ready backlog CSV from a run's findings
29
+ compare Diff two runs (new/resolved findings, risk grade delta)
30
+ sarif Convert a run's findings into SARIF for GitHub code scanning
31
+ baseline Snapshot a run's current findings into a baseline/exceptions file
32
+ batch-audit Run the full audit across a list of sites, aggregating a summary table
33
+ help Show this help
34
+ version Show package version
35
+
36
+ Examples:
37
+ universal-security-audit audit --site https://www.example.com
38
+ universal-security-audit audit --site https://www.example.com --crawl --max-pages 25
39
+ universal-security-audit audit --site https://www.example.com --intensity aggressive
40
+ universal-security-audit audit --site https://www.example.com --wpscan-key YOUR_KEY
41
+ universal-security-audit audit --site https://www.example.com --skip-exposed-paths --skip-recon
42
+ universal-security-audit audit --site https://www.example.com --login-url https://www.example.com/wp-login.php --username U --password P
43
+ universal-security-audit audit --site https://www.example.com --wp-app-user U --wp-app-password 'xxxx xxxx xxxx xxxx'
44
+ universal-security-audit audit --site https://www.example.com --proxy http://127.0.0.1:8080
45
+ universal-security-audit audit --site https://www.example.com --baseline ./security-baseline.json
46
+ universal-security-audit audit --site https://www.example.com --fail-on high --json > report.json
47
+ universal-security-audit report --run-dir ./reports/<run-id> --brand-config ./branding.json
48
+ universal-security-audit tickets --run-dir ./reports/<run-id>
49
+ universal-security-audit compare --before ./reports/<run-a> --after ./reports/<run-b>
50
+ universal-security-audit sarif --run-dir ./reports/<run-id> --out results.sarif
51
+ universal-security-audit baseline --run-dir ./reports/<run-id> --out ./security-baseline.json
52
+ universal-security-audit batch-audit --sites-file ./sites.txt --intensity light
53
+
54
+ Key flags (see README for the full list):
55
+ --intensity light|normal|aggressive Concurrency/delay/probe-size dial
56
+ --max-pages <n> Cap pages scanned (default: no limit)
57
+ --max-depth <n> Link-hops from homepage to follow during --crawl
58
+ --max-per-url-pattern <n> Crawl-trap guard, default 3 (per-record pages, etc.)
59
+ --wordlist <path> Extra paths to probe, one per line
60
+ --skip-port-scan / --skip-subdomain-enum / --skip-whois / --skip-admin-audit
61
+ --auth-config <path> | --login-url/--username/--password Enables authenticated admin audit
62
+ --wp-app-user / --wp-app-password WordPress Application Password (preferred over form login)
63
+ --proxy <url> Route traffic through Burp/ZAP for manual inspection
64
+ --fail-on <severity> / --min-grade <letter> CI/CD exit-code gating
65
+ --baseline <path> Suppress known-accepted findings from scoring/CI gating
66
+ --json Clean JSON on stdout (progress moves to stderr)
67
+
68
+ Primary outputs (in reports/<site>-<timestamp>/):
69
+ findings-summary.csv All findings, sorted by severity (includes suppressed column)
70
+ exposed-paths.csv Every probed admin/config/API path + result
71
+ script-inventory.csv Detected front-end libraries + versions
72
+ wordpress-components.csv Detected WP plugins/themes (WordPress sites only)
73
+ vulnerabilities.csv Matched CVEs/advisories (OSV.dev + WPScan)
74
+ port-scan.csv Every scanned port + state + banner
75
+ subdomains.csv Subdomains found via certificate-transparency logs
76
+ wp-admin-*.csv / drupal-admin-*.csv Authenticated admin-audit data
77
+ summary.json / full-report.json Machine-readable run summary / full data dump
78
+ security-dashboard.html/.pdf Visual risk-graded report
79
+ security-ticket-backlog.csv Ticket-ready findings (via 'tickets' command)
80
+
81
+ ⚠️ Authorized-use only: this tool actively probes admin/config paths, ports, and
82
+ subdomains on the target. Only run it against sites/infrastructure you own or
83
+ have explicit written permission to test.
84
+
85
+ If no command is provided, the CLI defaults to 'audit'.
86
+ `);
87
+ }
88
+
89
+ const argv = process.argv.slice(2);
90
+ const command = argv[0] && !argv[0].startsWith('--') ? argv[0] : 'audit';
91
+ const rest = command === 'audit' ? (argv[0] === 'audit' ? argv.slice(1) : argv) : argv.slice(1);
92
+
93
+ switch (command) {
94
+ case 'audit':
95
+ run('run-security-audit.mjs', rest);
96
+ break;
97
+ case 'report':
98
+ run('generate-security-report.mjs', rest);
99
+ break;
100
+ case 'tickets':
101
+ run('generate-security-tickets.mjs', rest);
102
+ break;
103
+ case 'compare':
104
+ run('compare-security-runs.mjs', rest);
105
+ break;
106
+ case 'sarif':
107
+ run('generate-security-sarif.mjs', rest);
108
+ break;
109
+ case 'baseline':
110
+ run('generate-baseline.mjs', rest);
111
+ break;
112
+ case 'batch-audit':
113
+ run('run-batch-audit.mjs', rest);
114
+ break;
115
+ case 'help':
116
+ case '--help':
117
+ case '-h':
118
+ printHelp();
119
+ break;
120
+ case 'version':
121
+ case '--version':
122
+ case '-v':
123
+ console.log('0.1.0');
124
+ break;
125
+ default:
126
+ console.error(`Unknown command: ${command}`);
127
+ printHelp();
128
+ process.exit(1);
129
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "companyName": "JustWhat.net",
3
+ "logo": "./assets/logo.png",
4
+ "primaryColor": "#ef4444",
5
+ "secondaryColor": "#0f172a",
6
+ "accentColor": "#22c55e",
7
+ "reportTitle": "Security & Attack Surface Audit",
8
+ "author": "Justin Adams",
9
+ "footerText": "Confidential — Prepared by JustWhat.net"
10
+ }