@vigolium/vigolium
package.
This page mirrors
CHANGELOG.md
in the main repository, which is the source of truth. Run vigolium version to see
the build you have installed, and vigolium update to upgrade.A focused REST API scan-path hardening release: a server database-init correctness fix plus a batch of tests that drive both native and agentic scans end-to-end through the REST API. The fix closes a gap where a half-initialized results database (connection opens but schema creation fails — a locked, read-only, disk-full, or schema-incompatible file) left the server running with a live DB handle paired with a nil repository, which repo-backed handlers dereferenced into nil-pointer panics (HTTP 500). No module changes (registry stays at 201 active + 116 passive).
Fixed
- Server no longer 500-panics on a half-initialized database — schema-creation failure now collapses into the same no-persistence mode as an open failure, so the server is consistently DB-less and repo-backed endpoints (
/api/scans,/api/projects, …) return a clean 503 instead of a nil-pointer panic. Enforced in two places: a newinitServerDatabasehelper in the CLIserverpath closes the connection and returns no repo on schema failure, andNewHandlers— the single constructor every server is built through — normalizes the invariant by dropping to no-DB mode if it ever sees a non-nil db with a nil repo. The reverse pairing (repo-backed agent endpoints with no traffic DB) is a legitimate, tested config and is left untouched.
Internal
- REST scan-trigger E2E tests (
make test-e2e-rest-scan,TestAPI_REST_*) — boot the API server in-process and drive a native scan and an agentic (autopilot/swarm) scan entirely through the REST API against a hermetic127.0.0.1target. No Docker or LLM provider required — the agentic legs assert the launch/track/cancel wiring. A regression guard against server mis-wiring that turns scan-triggering endpoints into 500s/panics. - REST coverage scorecard canary (
TestCoverageScorecard_RESTNativeScan_DVWA) — the same DVWA ground-truth coverage scan, driven through the REST API instead of the CLI, folded intomake test-e2e-scorecard. - REST autopilot smoke test (
make test-smoke-autopilot-rest,TRANSPORT=rest) — the real, paid access-lab autopilot smoke run driven through the server (POST /api/agent/run/autopilot+ poll/api/agent/status/:id) instead of the CLI, exercising the operator/workbench path end-to-end. - Added no-persistence handler tests (
handlers_norepo_test.go) and DB-init tests (server_db_init_test.go) covering the degraded paths above.
The headline is a redesigned
agent autopilot — an opt-in (agent.olium.autopilot_mode) durable, bounded-section operator that rotates context instead of accumulating unbounded conversation history, paired with a verify-before-promote path where a fresh-context skeptic verifier grades each candidate against per-class evidence gates before it becomes a finding. Around it: durable state + --resume, an operator --knowledge-base (read-on-demand app docs), --prior-context (mine the traffic and findings already in the project DB), --plan-file seeding, and --headed / --browser-auth debugging controls across autopilot and swarm, plus autopilot --session-dir / --transcript controls. Underneath the features is a broad reliability sweep across the scan pipeline plus a batch of code-review fixes for regressions the v0.3.0 refactors introduced — scan lifecycle correctness (recovered panics/cancellations no longer report as successful scans, single-owner finalization, project-scoping enforced on every scan-control endpoint), several fixed data races and resource leaks, and restored CSRF / passive-module scoping and gating that had regressed. No module changes (registry stays at 201 active + 116 passive).Added
- Redesigned
agent autopilot— durable, bounded-section operator with verify-before-promote (opt-in viaagent.olium.autopilot_mode) —legacy(default) is byte-for-byte unchanged;shadowandenforcedopt into the redesign. The operator now runs in bounded sections with context rotation: instead of accumulating unbounded conversation history until it hits a context ceiling, it resets the engine and reconstructs a compact brief (durable scratchpad + previous-section closing note + candidate ledger + recent actions), rotating on a turn cap / token soft-cap / stall — removing the long-run context ceiling. Inenforcedmode apropose_candidatetool replacesreport_finding, and a fresh-context skeptic verifier (its own engine and transcript file, detached from the operator’s--max-durationdeadline) grades each candidate against per-class evidence gates — IDOR owner/non-owner, XSS browser-execution, SQLi paired controls, SSRF/RCE OAST — promoting only the confirmed ones;shadowmirrors reported findings into candidates for FP-rate comparison without changing behavior. State is durable in newagent_sections/agent_finding_candidatestables (cascade-delete), and additivesection_start/section_end/section_interruptedevents are recorded to the transcript. --resume <agentic-scan-uuid>for autopilot — resume a durable-mode run from its persisted state: reuses the run’s session dir, project, target, and durable scratchpad / candidate ledger, and skips pre-scan and audit re-prep. Requiresagent.olium.autopilot_mode != legacy.--knowledge-base <file|dir>/--knowledge-base-rawfor autopilot — front-load operator-supplied reference docs describing the app (auth model, login flows, roles / privilege tiers, business logic) into the operator’s opening brief. The design is read-on-demand, mirroring--source: an LLM distills the docs into a compact briefing plus an authoritative document index that are inlined, while the full documents stay on disk for the agent toread_file/grepon demand — so a directory of many markdown files adds a few hundred tokens, not the whole tree. Works blackbox and whitebox (it matters most blackbox). The distilled brief is cached to<session-dir>/knowledge-base-brief.mdso a--resumereuses it instead of re-paying the LLM call, and distillation is best-effort — any failure degrades to the deterministic index, the run never fails over the KB.--knowledge-base-rawskips distillation and inlines the deterministic index only, for offline / reproducible runs.--prior-context(auto|summary|off) for autopilot — front-load a bounded summary of the traffic and findings already in the project DB (Burp imports, prior scans, prior findings) so the operator mines them instead of re-deriving from scratch.auto(default) renders a bounded table — totals, up to 20 endpoints, up to 10 findings, each capped with an “N more — use the tool” pointer — when prior data exists;summaryis a one-line pointer;offdisables it. The brief is built before this run’s pre-scan (so it reflects genuinely prior data) and its token cost is roughly fixed regardless of DB size.--burp-bridge-urlon autopilot — pull live Burp Proxy history into the project DB before the run (default from$VIGOLIUM_BURP_BRIDGE_URL), so the pre-scan and operator can mine it alongside prior traffic.--plan-fileon autopilot and swarm — a plan file mixing free-text guidance and raw HTTP request block(s); the file owns the instruction and every request block becomes a seed input. Cannot be combined with--inputor a prompt (--prompt/ positional).--prompton autopilot and swarm — an explicit flag for the free-text task guidance that was previously positional-only (same effect as the positional[prompt]; use--plan-filefor a whole plan with seed requests).--headedon autopilot and swarm — show the browser window during the run (native pre-scan /--discoverspidering, in-process probes, and agent-browser subprocesses); setsVIGOLIUM_BROWSER_HEADED=1for the run. A debugging aid.--browser-authon swarm — run the browser-based auth phase before discovery.--session-dir/--transcripton autopilot — pin the run’s debug artifacts (transcript.jsonl,runtime.log, scratchpad, tool-results) to an explicit directory, and/or copytranscript.jsonlout to a chosen path after the run (the in-session copy is always kept). Both are conveniences for debugging throwaway / stateless-DB runs; the run UUID stays a real UUID for DB linkage.
Changed
- Prompt-embedded credentials are honored on explicit-flag runs — auth / browser signals carried in a free-text prompt (e.g. “log in as admin/admin123, focus on /admin”) are now extracted and applied even when the target / source come from explicit
-t/--sourceflags, instead of being silently dropped. The explicit flags still own the target / source; only the auth / browser signals are lifted from the prompt. - Multi-app autopilot runs are scheduled sequentially — when a prompt yields multiple apps, autopilot now runs them one at a time (swarm keeps its parallel fan-out), because autopilot overrides shared package-level flags per app and concurrent runs would cross-contaminate targets / sources.
agent.olium.max_concurrentnegative value = unbounded — a negativemax_concurrentnow disables the provider-call concurrency cap entirely (0 / unset still resolves to the default of 4; 0 does not mean unbounded).
Fixed
- Scan lifecycle correctness — a recovered panic or cancellation in a native scan is no longer reported as a successful scan; scan finalization now has a single owner (detached-context
CompleteScan+ aFinalized()safety net) so the server can’t overwrite a terminal status back tocompleted. Rejected/dry-run requests are admitted before heavyweight allocation, leaving no leaked runner or pending scan row, and shutdown cancels and awaits in-flight scans instead of starting queued ones. - CSRF regressions from v0.3.0 —
csrf_verifynow models a real cross-site forgery (foreignOrigin/Refereron the probe) and keys dedup on request identity after the token-applicability check;csrf_detectrequires a likely-session cookie, resolvesSameSitefrom the request’s own session cookie, and bounds its token regex (\btoken\b) soaccessToken/siteTokenno longer suppress a real missing-token finding. - Host-scoped passive regressions from v0.3.0 — reverted
cors_misconfigurationandclickjacking_detectto per-route scope; restored the HTML/schemeCanProcessgates onsecurity_headers_missing,csp_weakness_audit,hsts_preload_audit,permissions_policy_detect, andcross_origin_isolation_audit(HSTS gated to HTTPS);cookie_security_detectnow parsesSet-Cookieattributes instead of substring-matching and grades material session-cookie issues (missingSecure/HttpOnly) as Low/Tentative candidates. - Data races & resource leaks —
responseObserver.sinkmoved toatomic.Pointer; process-globalos.Stderrcapture is now single-owner (no cross-scan descriptor corruption);Runner.Closeguarded bysync.Once; fixed thePauseControllerpause/resume race, an OAST request-resolver race, a spider writer leak, and aRecordWriter.Closethat could hang shutdown on a wedged DB. - HTTP/executor robustness — transmit an explicit
Hostoverride viareq.Host; floorResponseHeaderTimeoutatmax(timeout, 30s)so a 6s time-blind SQLi probe isn’t aborted at the transport layer; per-scan requester isolation (CloneForScan); the cluster key now includesDisableCompression; batch-flush no longer double-counts the per-module cap. - Database & input hardening — finding host filter rewritten as
EXISTS(no duplicate rows) with a risk-ranked severity sort;total_findingsresettable to 0; fixed a risk-keyset coverage hole (retry the page rather than silently skip); fixed nuclei/deparos JSON decoder infinite-loops on malformed lines and persisted disk-queue sealed-segment state across restarts. - Agent / audit / JS-extension resilience — a canceled audit finalizes on a detached context (no more stuck
running); JS extensions are interruptible via a watchdog with poisoned VMs discarded; process cleanup no longer kills live or PID-reused processes; the JSTangle pool self-heals on worker-replacement failure. - Scanner-module correctness — gRPC missing-auth confirmation compares response content (hash) not just length; padding-oracle reconfirms the byte that actually controls final CBC padding; boolean-blind SQLi dedups claims per insertion point and honors the per-module deadline between payloads; IPv6
ServiceBaseURLbracketing. - httpmsg / WAF pacing — lowercase absolute-form scheme before parsing (
HTTP://was dropped) and makeOriginRefererSchemeport-aware so a cross-portOrigin/Refererno longer flips the inferred scheme; a per-entrypreArmedflag distinct fromarmedfixes birth-armed adaptivePreArm, and a limiter evict-notifier clears the edge-pacing dedup so a re-created entry re-arms after eviction.
Internal
- Marked
modernc.org/sqliteas a direct dependency (go mod tidy). - Added
.github/secret_scanning.ymlpaths-ignoreso the synthetic secret-detection test corpus isn’t flagged by scanning / push protection. - Consolidated the spider DESTRUCTIVE-endpoint guard into a single Go-side
filterDestructiveURLs; extracted adecodeResponseBodynetwork/capture helper; surfaced--no-waf-pacingunder Speed Control in CLI usage. - Durable-autopilot plumbing — a durable-state repository (
agent_sections/agent_finding_candidatesCRUD, indexes, cascade-delete);SaveFindingReportedso the autopilotreport_findingcounter tallies only distinct, non-deduped findings; andCountFindingsByModule/CountFindingsByURLfor run-scoped coverage. RegisterReadOnlyBuiltinsconsolidates the read-only olium builtin set (read_file,ls,grep,glob,web_fetch,browser_probe) in one place; the/diagnosticsembedded-binary + piolium probes now run concurrently (wall time bounded by the slowest single probe rather than their sum).- Multi-target autopilot smoke runner (
test/smoke-test/smoke-autopilot.sh,PROFILE=access-lab|ginandjuice|crapi|juiceshop) with a hermeticaccess-labvulnerable app; agent-transcript test fixtures + generator (test/testdata/agent-transcripts/);sanity-check.shde-staled (archon→auditdriver rename). Refreshed the bundledvigolium-scannerskill and the agentic-scan docs for the new flags.
Two-way Burp Suite integration through a new loopback live bridge (paired with the
burp-vigolium extension v0.2.0) headlines this release, alongside two engine-level upgrades. First, the embedded JavaScript analyzer is rebuilt and renamed jsscan → jstangle — turning a js-beautify + regex sweep into a deobfuscating, AST-based JavaScript intelligence engine that resolves real request facts (endpoints, GraphQL operations, WebSocket/SSE, client routes, browser security flows) out of minified, obfuscated bundles. Second, secret detection drops the external kingfisher binary for a pure-Go, in-process scanner (pkg/secretscan) built on wasilibs/go-re2. They join a broad CLI agent-ergonomics overhaul, a batch of autopilot / olium agent-tooling safety, provenance, and attribution fixes, and per-origin scan-identity / strict input-format correctness contracts. The module registry grows to 201 active + 116 passive. See the Burp Suite guide for bridge setup and usage.Added
- Burp Suite live bridge (bidirectional traffic sync) — Vigolium can now read live traffic straight out of a running Burp Suite and write traffic back into Burp’s Target → Site map, over an opt-in, loopback-only listener that the extension exposes (default
http://127.0.0.1:9009, loopback-bind only, no credentials). Point Vigolium at it with the new--burp-bridge-urlflag (or theVIGOLIUM_BURP_BRIDGE_URLenv var). Live Burp rows are merged into the ordinary traffic views labelledsource: burp, and the usual filters, sorting, pagination, JSON output, and record-detail workflow keep working. Imports are idempotent — new requests are inserted, changed responses refresh the existing row, and unchanged traffic is skipped. Implemented inpkg/burpbridge(client),pkg/olium/vigtool/burp_bridge.go(agent tool), andpkg/server/handlers_burp_bridge.go(server-side merge). - New CLI flags for fetching Burp traffic directly — bring Burp’s Proxy history and Site map into Vigolium (and push traffic back) without leaving the CLI:
vigolium traffic --burp-bridge-url <url>— merge live Burp Proxy history with the local database in thetrafficview.vigolium traffic --save-to-vigolium-db [--all]— persist bridge-visible Burp history into Vigolium’s database (the current page, or every matching record with--all).vigolium traffic --save-to-burp— copy selected Vigolium database traffic into Burp’s Target Site map.vigolium replay --record-uuid <uuid> --save-to-burp— save a mutated replay and its fresh response back into Burp’s Site map.vigolium import --burp-bridge-url <url>— persist all bridge-visible Proxy history into the database in one shot.vigolium server --burp-bridge-url <url>— merge live Burp rows intoGET /api/http-recordsfor the UI and API.
- Companion extension —
burp-vigoliumv0.2.0 — ships the loopback live-bridge listener and on-demand / interval Target Site map snapshots (idempotent, incrementally chunked upload;Ctrl+Alt+S), a dedicated Bridge tab/panel, and direct context-menu dispatch from Proxy History, Site map, and Repeater. Site-map snapshots are ingested by Vigolium’sPOST /api/burp/sitemap/snapshotendpoint. - Positional targets and prompts across the CLI —
scan(andscan-url/scan-request/run) now accept target URLs as bare positional arguments, deduplicated against-t/--target;agent autopilot/agent swarmpreserve a positional prompt as extra instruction context even when flags are also set; anddb listtakes a positional table selector (findings/scans/scopes), conflict-checked against--table. db reset --forceandconfig ls --show-secrets— a dedicateddb reset --forcereplaces the ambiguous overloaded--force, andconfig ls --show-secretsis now the explicit, self-documenting way to reveal masked secret values (VACUUM stays automatic on reset).swarm --provider/--model— the swarm agent gains provider/model overrides via the sharedoliumOverrideshelper, matching autopilot;--list-agentsand every--providerhelp string are now driven by a singleolium.ProviderNamesregistry so they can’t drift.report_findinglinks its proof;replay_requestpersists — the autopilotreport_findingtool now acceptsrecord_uuidsand populatesHTTPRecordUUIDs, so agent findings link thehttp_recordsthat prove them;replay_requestpersists the sent request and received response as anolium-replayhttp_record, returned asreplay_record_uuid.--input-modeadvertises every supported parser —SupportedFormats()now derives from a single format registry and lists all accepted formats (addspostman,curl,burpraw,burpxml,har).- “Did you mean” suggestions for mistyped flags — a near-miss long flag (e.g.
--modulefor--modules) no longer dead-ends at a bareunknown flag; the error now points at the closest registered flag on the command that failed to parse (… Did you mean --modules? (run 'vigolium scan --help' for all flags)). The match is Levenshtein-based with a length-scaled threshold so short flags don’t over-suggest, and only long flags qualify — an unknown shorthand is left untouched. Installed once on the root command so every subcommand inherits it, mirroring cobra’s built-in near-miss subcommand suggestions. Implemented inpkg/cli/flag_suggest.go. - Proactive WAF/CDN pacing (
--no-waf-pacingto disable) — an aggressive active phase can burst a CDN/WAF edge into arming a rate-based block (e.g. AWS WAF starts403-ing every probe seconds in), which hides real findings behind the edge. The per-host limiter now pre-throttles a host before the active phase bursts: the first time any earlier phase’s traffic fingerprints a host behind a CDN/WAF edge — CloudFront, Cloudflare, Akamai, Imperva/Incapsula, Sucuri, or Azure Front Door, from headers present on ordinary200s — the host’s concurrency is dropped (toMaxPerHost/4) and ramps back up on healthy responses. A one-time-per-host[waf-pacing-armed]notice prints the drop (e.g.40→10) to stderr and the session log so the slower pace is visible. It complements — and does not replace — the existing reactive WAF-block back-off; pass--no-waf-pacingto keep full opening concurrency (the reactive back-off still applies after a confirmed block). The fingerprint hangs off the shared HTTP requester and the notice off the shared host limiter, so it fires exactly once per host across every phase and requester. Implemented inpkg/deparos/waf(edge fingerprint) andpkg/core/ratelimit(PreArm).
Changed
jsscan→jstangle: a rebuilt JavaScript intelligence engine, not just a rename — the embedded Bun/webcrack helper and its Go wrapper are renamed end-to-end (platform/jstangle,pkg/deparos/jstangle, the per-platforminternal/resources/deparos/embed_jstangle_*binaries, discoveryengine_jstangle.go, config keyjsscan:→jstangle:, plus build targets, Dockerfiles, and docs), and the engine itself is substantially upgraded rather than left behavior-identical. A plain string/regex sweep over bundled JS is nearly blind — it can’t follow how a URL is assembled or whether a match is live code. jstangle instead deobfuscates and normalizes the source (webcrack-backed, loaded lazily), walks the AST, and resolves request facts with real evidence. Output is a typed, provenance-tracked analysis envelope spanning record familieshttpRequest,domFlow,assetReference,graphqlOperation,websocket,eventSource,clientRoute, andbrowserSecurityFlow— each with per-record confidence and a source span — while large transformed/beautified documents spill to contained, session-scoped artifacts. Callers pick analysis profiles (endpoints,dom-security,beautify,discovery,discovery-lite,full,inspect) so each runs only the stages it consumes, and rewrite levels (strict|standard(default) |aggressive) trade unminification depth against cost. Replay is safety-gated: only high-confidence facts are eligible for exact replay (observed method/query/body/safe static headers), and auth, cookies, API keys, CSRF tokens, and browser-controlled headers are never copied into replay traffic. The Go scanner drives it over a persistent length-framed--workertransport behind a machine-readable--capabilitiescontract, and jstangle is now a standalone, MIT-licensed, publishable CLI (npm install -g @vigolium/jstangle).- Native, in-process secret detection — the external
kingfisherbinary is dropped — passive secret scanning no longer shells out to (or downloads) the MongoDB kingfisher binary; the entirepkg/toolexecsubprocess/download tree is removed and replaced by a new in-processpkg/secretscandetector. It runs acoregx/ahocorasickkeyword prefilter → regex → gate pipeline overwasilibs/go-re2— real Google RE2 compiled to WebAssembly via wazero, so it needs no cgo, works onarm64, runs ~10× faster than theregexpstdlib, and is concurrency-safe. The rule set is a single embedded catalog of 1,264 rules (pkg/secretscan/catalog.json) merged from kingfisher v1.106.0 (Apache-2.0 patterns normalized from Rust-regex to RE2, live-validation blocks stripped) and betterleaks v1.6.1 (MIT, the gitleaks successor), regenerated byte-reproducibly viamake update-secret-rules. kingfisher’s detection semantics are preserved faithfully (entropy gate, character-class requirements, capture-group selection, safelist), and — matching the previous-ninvocation — live credential verification stays off. All three consumers (the passivesecret_detectmodule, the known-issue-scan batch, and the discovery engine) now detect inline with no temp files. Config renamed to match:disable_kingfisher→disable_secret_scan(theDisableKingfisherfield andkingfisher_findingsstorage column follow suit — no backward-compatible alias); original rule IDs,kingfisherprovenance, and attribution are retained. - Repeatable opaque flags use a stable
StringArray—-H/--header,--auth,--auth-file,--target,--spec-header, and--cookienow use aStringArrayvalue type that preserves each entry verbatim (no comma-splitting), so header/cookie values containing commas survive intact. - Every active module declares an intensity tier — 19 previously untagged active modules were assigned explicit intensity tiers, and a registry contract test now requires every active module to declare one (
xss-stored/xss-dom-confirmare waived as deliberate always-on modules). - Safer autopilot default tooling — the
attack_kitRedis gopher PoC is now a harmlessPINGprobe instead of a destructiveFLUSHALL, so an autonomous agent can’t wipe a target’s data by default;web_fetchis reclassified as non-read-only whenever capture is enabled (it writes anhttp_recordon every fetch, so it must not join the engine’s read-only parallel fan-out).
Fixed
- Per-origin scan identity (
scheme://host:port) — per-host module claims and the tech / content-class read key are now keyed by canonical origin rather than a bare hostname, so aScanPerHostmodule is no longer suppressed on:8443/:8080after:443claims the(module, host)pair, and per-port tech-gating no longer leaks or drops non-default-port detections. - Strict
--input-mode— no silent fallback — an unknown--input-modevalue is now rejected up front instead of silently falling through to the Nuclei parser (a typo previously yielded zero or partial records with no error). cwe_idpopulated on the native path — findings’Metadata["cwe"]now fills the existingcwe_idcolumn (previously dropped for native-scan findings; no schema change).web_fetchtruncation off-by-one — a response body landing exactly on the size cap is no longer mislabeled as truncated.get_sessioncross-project guard — a cross-project UUID can no longer read another project’s session record.storagerequested-but-disabled returns a config error — asking for storage output when storage is disabled now exits nonzero with a clear config error instead of silently succeeding (exit 0).db cleanrefuses no-selector wipes — adb cleanwith no selector no longer wipes everything; an explicit selector (ordb reset --force) is required.- General reliability, correctness, and false-positive hardening across the server, config handling, WAF/CDN detection, and scanner internals.
Internal
- Child-scan attribution —
run_native_scan/run_module/run_extensionnow attribute the child scan and its findings back to the parent agentic scan (ScanContext.attributeChildScan). - Single-source provider registry —
olium.ProviderNamesbacks--list-agentsand all--providerhelp text; the swarm/autopilot override paths share oneoliumOverrideshelper. - Registry contract test — a new test asserts every active module declares an intensity tier.
- Secret-catalog codegen —
internal/cmd/secretgenmerges the pinned kingfisher + betterleaks rule sets intopkg/secretscan/catalog.json(regenerated bymake update-secret-rules,KINGFISHER_VERSION/BETTERLEAKS_VERSIONpins);go.modgainswasilibs/go-re2andcoregx/ahocorasickand loses the kingfisher-download deps;THIRD_PARTY_NOTICES.mdis updated to attribute go-re2, ahocorasick, kingfisher (rules), and betterleaks (rules). Coverage:pkg/secretscan/{engine,detector,bundle}_test.goandpkg/modules/passive/secret_detect/{bundle,jstangle_beautify}_test.go(real minified-bundle detection, RE2↔stdlib parity,-raceconcurrency, and beautify→scan compose). - Removed the dead SDK-mode guardrails config block; refreshed stale bundled skills, the autopilot prompt, and built-in examples; added
Argsvalidators onscan/server/ingest/strategy/doctor/version/scope view/db list/config ls.
Catch-all / echo-server false-positive hardening across the endpoint-exposure module families. No new modules (registry stays at 198 active + 115 passive) — 31 existing active modules gain a shared confirmation guard against hosts that answer every path with the same 200 body. The release also hardens browser-launch reliability (a crash-on-startup system Chromium now falls through to a working browser, and
doctor smoke-launches the browser instead of trusting its --version banner), makes hand-picked module runs skip the broad known-issue-scan pass by default, fixes a jstangle large-output truncation, and closes an npm-publish staleness bug that shipped v0.2.2 binaries as 0.2.3.Added
- WAF/CDN block warning — when scan traffic to a host first comes back as a WAF/CDN block (a captcha, bot-detection, or challenge page), the scanner now prints a one-time-per-host
[waf-block-detected]warning to stderr (and the session log) noting the edge is filtering traffic and results for that host may be incomplete. Detection runs on the shared HTTP requester, so it fires across every phase (discovery, spidering, dynamic-assessment); the dedup means at most one line per host regardless of how many requests are blocked. Suppressed under--silent. - Expanded WAF/CDN + bot-management fingerprints — the block detector (
pkg/deparos/waf) gains 12 vendors on top of the existing Cloudflare/Akamai/AWS/F5/Imperva/Sucuri/ModSecurity set: WAF/CDN — Azure (Application Gateway / Front Door), Fortinet FortiWeb, Barracuda, Citrix NetScaler, Wallarm, Radware, Reblaze, Wordfence; bot management / anti-automation — DataDome, PerimeterX (HUMAN), Kasada, Queue-it. Header matching now inspects everySet-Cookievalue (not just the first) so cookie-name signatures likedatadome=,rbzid=, and_pxhdare caught reliably. The status-code gate still applies, so a bot-manager tracking cookie on an ordinary 200 is never mistaken for a block. - Browser launch smoke-test in
doctor/ first-run init —vigolium doctor(and the first-scan dependency check) now actually launches the resolved Chromium headless instead of trusting its--versionbanner. A binary that prints a version but SIGTRAPs on real headless startup (seen with Debian’s Chromium on a KVM guest) or is missing a runtime shared library is downgraded from green to a warning carrying a specific remedy — runvigolium doctor --fix --only chrometo download a working Chrome for Testing, or pointspidering.browser_pathat a working browser — instead of being reported healthy and then failing every spider. The probe (newpkg/browserprobe) performs the same DevTools remote-debugging handshake the spider relies on, in an isolated self-cleaning user-data dir, bounded to 30s. Only the one-shot CLI paths (doctor, first-run init) enable it; the server/diagnosticsendpoint leaves it off to stay a cheap, poll-safe readout.
Changed
- Hand-picked module selection auto-skips
known-issue-scan— narrowing a scan to specific modules (--module-id, or-m/--modules) now auto-appends--skip known-issue-scan, since selecting a handful of modules signals a targeted, low-noise scan and the broad Nuclei/Kingfisher known-issue pass would flood it. A one-line console note explains the auto-skip; pass--only known-issue-scanto force the pass. It’s a no-op when--onlyis set, when no module narrowing is active, or when the phase is already off or explicitly skipped, and is silent under--silent. - WAF/CDN block warning now fires on the direct single-request path too — the
scan-url/scan-requestpath builds its own HTTP requester and previously missed the one-time-per-host[waf-block-detected]warning; it now installs the same notifier so an edge filtering scan traffic is surfaced regardless of scan entry point. The sharedFormatBlockNoticeLinekeeps the message identical to the Runner-backed path. findingtree leaves show the producing module type — each collapsed leaf now renders a(confidence, active|passive)tag so it’s clear both how sure a finding is and whether an active or passive module produced it; theactivelabel is now colored green (matching the passive/active convention intraffic/db ls).- HTML-report body-trim note points at the full-fidelity formats — the “trimmed N bodies to stay within browser limits” stderr note now directs the reader to the
sqlite,jsonl, andfsexport formats for the full raw data (previously it named only the JSONL export).
Fixed
- Universal-catch-all false positives — a reflecting/echo server, SPA fallback, or blanket rewrite that serves the same 200 body for
/<dir>/<anything>could survive a weak marker (pool:,pong, a brand string in a truncated tail) and be reported as a genuine exposure. Affected modules now issue catch-all decoy disproof rounds: a random same-directory/same-extension sibling is fetched through the same marker predicate, and any candidate whose decoy also matches is dropped. A genuinely exposed file/panel/endpoint has no such sibling (the decoy 404s), so this costs no true positives, and running the decoy through the same predicate makes it robust to the body-truncation quirk that a body-similarity compare would miss. - Plaintext-only probes gated by content-type — probes whose genuine hit is only ever served as plaintext (PHP-FPM status/
/ping, and similar) now reject atext/htmlresponse outright as a catch-all/echo shell, while probes whose real content is legitimately HTML (phpinfo, admin/login UIs) skip the content-type gate and lean on the decoy disproof instead. - Discovery no longer brute-forces SPA build directories — spidering a Next.js/Nuxt/Vite/webpack/CRA app harvested chunk names from the JS bundles and replayed them under the immutable, content-hashed build-output directory (e.g.
/_next/static/chunks/<name>/,/_next/static/chunks/<name>), producing hundreds of pointless requests that can never surface a real route. Discovery now detects content-hash fingerprinted bundles by filename (main-5cf96b0d57f7f579.js,938-50137dfb3187f5b2.js, CRAmain.073c9bfa.chunk.js— a-/./_-delimited token of ≥8 hex chars containing at least onea-fletter, so version numbers and timestamps don’t match) and skips recursive wordlist/observed brute-forcing into any directory that holds them, plus backup/numeric derivations off the files themselves. This is framework-agnostic — no hardcoded directory list — with a cheap path fast-path for the well-known roots (/_next/static/,/_nuxt/,/_app/immutable/). A generic/static/with hand-authored assets stays fully discoverable. The bundles the spider actually found are still fetched and scanned; genuine endpoints like/api/...are unaffected. - Browser launch falls through a broken system Chromium instead of aborting the scan — spidering resolved a single browser binary and failed the whole scan if it crashed on real launch. Launch now tries each candidate in priority order — configured
browser_path→ embedded engine binary → system Google Chrome → system Chromium → Chrome for Testing (cached, then downloaded) → rod auto-download — and falls through to the next whenever one fails the launch/DevTools handshake, so a crash-on-startup distro Chromium auto-recovers to a working browser (in practice Chrome for Testing) rather than aborting. All resolvable system binaries are enumerated (real Chrome preferred over Chromium), and only when every candidate fails does launch return an aggregated error (with ansudo apt-get install -y chromiumhint on linux/arm64, where neither rod nor Chrome for Testing publish a build to auto-download). - jstangle large-output truncation — jstangle’s
code/beautifiedJSONL records for a big bundle exceed 64KB, and the Bun/Node runtime could exit before the tail of a large write drained through the ~64KB kernel pipe buffer, silently truncating those records so the JSON failed to parse and the beautified document was lost. jstangle’s stdout is now captured to a temp file (always write-ready, no pipe-buffer cap) instead of abytes.Bufferpipe, so the full output survives regardless of line size. - Request-cluster cache no longer double-decodes bodies — a cached response snapshot kept the original
Content-Encoding/Content-Length/Transfer-Encodingheaders even though its body was already decoded at capture, so reconstructing the response (ToResponseChain) could re-decode a plaintext body and misreport its size as 0. The snapshot now strips those content-coding / framing headers at the single point where the decoded body and headers are paired, and setsContentLengthto the decoded length, keeping the cached entry self-consistent. - npm publish shipped stale binaries under a newer version — the
NPM_NEEDS_BUILDstaleness guard grepped the built binary for the target version string, which false-matched because a binary embeds newer version strings in its content (a v0.2.2 build literally contains “v0.2.3”), somake npm-*repackaged stale v0.2.2 binaries under the 0.2.3 npm version. Staleness is now judged by the goreleaser archive name (vigolium_<version>_<os>_<arch>.tar.gz, authoritative since--cleanwipesbuild/disteach run), andbuild/npm/build.mjsgained averifyReleaseVersionbackstop that refuses to pack whenbuild/distholds binaries built for a version other than the one being published.
Modules hardened
aem_content_discovery, aspnet_blazor_exposure, aspnet_identity_probe, aspnet_misconfig, aspnet_sensitive_files, aspnet_service_exposure, cloud_storage_listing, cms_installer_exposure, dashboard_exposure, django_browsable_api_exposure, django_debug_exposure, fastify_hono_probe, firebase_misconfig, flask_werkzeug_debugger, js_devserver_exposure, laravel_devtool_exposure, laravel_ignition_rce, laravel_misconfig, laravel_sensitive_files, nextjs_chunk_audit, oauth_misconfiguration, php_composer_exposure, php_debug_exposure, php_framework_debug, php_path_info_misconfig, php_source_disclosure, rails_admin_dashboard, rails_sensitive_files, unauth_service_exposure, wp_ajax_exposure, wp_xmlrpc.Internal
- 30 new/expanded
scan_detect_test.gosuites covering the catch-all decoy and content-type gates. - New tests for the reliability work:
browserprobe_test.go,diagnostics/chromium_probe_test.go,spitolas/.../candidates_test.go(candidate ordering / fall-through),http/block_notifier_test.go,http/clustering_test.go,cftbrowser_test.go, plus jstangle, WAF-detector, and immutable-asset-dir suites.
False-positive hardening for the injection modules plus search-flag ergonomics. No new modules (registry stays at 198 active + 115 passive).
Added
--exclude-search/--exclude-header/--exclude-bodyonfinding,traffic,db ls— inverse of the search flags; drop any row where the term appears.--exclude-searchis repeatable (AND-combined).
Changed
--search/--header/--bodynow span the full request/response corpus (URL, path, headers, body — and linked records forfinding), not just URL/path.- XPath boolean oracle hardened — three operand pairs (was two) plus symmetric OR-false/AND-true inert controls;
sqli-boolean-blindconfirmation raised to three rounds.
Fixed
- Time-based injection FPs — new
infra.ScaledDelayConfirmedrequires the delay to scale with the injected sleep over a no-sleep control, acrosssqli-time-blind,command-injection-timing, andnosqli-operator-injection. - CDN/challenge FPs —
infra.LooksOpaqueBody(entropy) andinfra.IsCDNInfraPath(/cdn-cgi/) make the differential and injection modules fail closed on opaque, non-deterministic edge responses;xpath-injectionalso skips standard request headers, andldap-injectionrejects block pages served with a 200.
A false-positive-hardening and read-command ergonomics release on top of v0.2.1 — no new modules (registry stays at 198 active + 115 passive). It fixes two shared root causes behind a class of differential/blind false positives — confirmation legs that “reproduced” against a cached response, and non-deterministic endpoints whose status flips were misread as injection — and tightens signature-stripping, OAST callback fan-out, and hygiene-finding noise.
Added
--severity/--sevfilter onfindinganddb list— comma-separated severity list accepting single-letter shorthands (h,c) and any unambiguous prefix (me,info,crit).- “Filtered by:” summary line on
findingandtraffic— text-mode listings echo the active filter conditions (severity, confidence, search, host, …) so it’s clear how the result set was narrowed; suppressed for JSON output. - Per-source attribution for
--glob-dbmerged reads —finding/traffictrees built from multiple merged databases show one db-path root per source file, attributing each finding and record back to where it came from. --limitcap notice — a stderr hint reports how many rows were shown of the total and how to raise the cap.
Changed
- Finding grouping widened for noisy per-URL / per-page findings —
sensitive-header-leak,sensitive-url-params,clickjacking-detect,reverse-tabnabbing-detect,nginx-path-escape,cache-auth-misconfiguration,crlf-injection, andapi-key-url-exposurenow collapse to one per-host finding (affected routes/params/values unioned onto the survivor) instead of one row per crawled URL. - OAST findings coalesce per emission key — a payload’s callback fan-out (DNS A/AAAA, recursive resolvers, then the HTTP-fetch leg) collapses to one finding at the strongest protocol rank, and low-signal Info interactions also coalesce across the many URLs that plant the same injection point on a host.
- SAML signature-stripping scoped to where the attack applies — the leg now requires a SAML
<Assertion>(so aSAMLRequest/AuthnRequestto an IdP SSO endpoint no longer false-positives), requires an accepted baseline status (2xx/3xx), and treats a 5xx control as a crash rather than a bypass.
Fixed
- Confirmation legs poisoned by the request-cluster cache — the 500 ms cache keyed on raw request bytes returned the first response verbatim, so an identical re-send always “reproduced” (and timing re-sends came back ~0 ms). Confirm/baseline/timing round-trips now pass
NoClustering: trueacrosscode-exec,default-credentials,http-request-smuggling,race-interference,reverse-proxy-path-confusion,sqli-boolean-blind,sqli-time-blind,ssti-blind,unauth-service-exposure, andssrf-detection. - Non-deterministic / status-flip differentials misread as injection — endpoints that flap between a 200 page and a 302 redirect independent of the payload could mimic a proxied fetch or boolean oracle. The differential legs now require a determinism re-fetch, agreeing controls, and 2xx status discipline (via shared
infra.Is2xx) acrossssrf-detection,xpath-injection,ldap-injection,nosqli-operator-injection, andidor-guid(which also requires a substantial body difference so per-request tokens don’t read as a different object). - Standard request headers wrongly treated as URL-fetch / injection sinks —
Referer,Origin,Upgrade-Insecure-Requests,Via, andFromare fixed browser/protocol headers, not application parameters. A sharedinfra.IsStandardRequestHeadernow excludes them across the SSRF/OAST family (ssrf-blind,oast-probe), andinput-behavior-probedrops them from its probe set. - GraphQL boolean-injection keyword differentials — the
graphql_scanboolean leg now confirms against an inert-ORcontrol, so an endpoint reacting to the mere presence of the token is no longer misread as injection. - Base64 findings collapse rotating-token JSON blobs —
base64-data-detectkeys findings on the decoded JSON shape (per host+source), collapsing a rotating token embedded on every page to one finding while opaque/non-JSON blobs keep their per-value identity.
Internal
- New shared helpers in
pkg/modules/infra/filter.go(Is2xx,IsStandardRequestHeader) pluspkg/cli/{filter_summary,severity_gate}.go. - New/expanded tests:
severity_gate_test.go,oast/service_test.go,signature_bypass_test.go,base64_data_detect/scanner_test.go, scanner tests for the new gates (ssrf_detection,xpath_injection,ldap_injection), andknown_issue_scan_test.go.
A detection-breadth and agent-provider release on top of the v0.2.0 stable line. Eleven new modules land (5 active + 6 passive), several existing detectors grow out-of-band and bypass legs, the olium agent runtime gains three new provider backends (the public OpenAI Responses API, a Claude Code Agent-SDK bridge, and a generic Anthropic Messages proxy), and a path-normalization/FUZZ baselining bug that was suppressing cache-key and content-discovery findings is fixed. A batch of read-command ergonomics also lands — a grouped
finding tree view, --pick position selection, repeatable AND-combined --search, compact-by-default (and syntax-highlighted) Markdown, and post-scan traffic printing — alongside a subcommand-help reorganization and two scoping/merge correctness fixes. The registry now stands at 198 active + 115 passive registrations.Added
- New active modules (5):
xpath-injection(High) — XPath injection detection.sqli-out-of-band(Critical) — out-of-band SQL injection confirmed via OAST callbacks (an unforgeable hit on a per-payload subdomain).unauth-service-exposure(Critical) — unauthenticated infrastructure-service exposure (a backend admin/data service reachable without credentials).tls-protocol-cipher-audit(Medium) — weak TLS protocol / cipher-suite configuration audit.session-fixation(Medium) — permissive session management (server accepts an attacker-fixed session identifier).
- New passive modules (6):
llm-endpoint-fingerprint(Info) — detects LLM / AI API endpoints, backed by a newinfra/llmsigsignature catalog.payment-integration-audit(Info) — flags payment-gateway integration risk surfaces (Stripe, PayPal, Braintree, Square, Adyen, Razorpay, Checkout.com).css-injection-detect(Info) — input reflected into a CSS context.dom-clobbering(Info) — DOM-clobbering gadget: a JavaScript sink fed from a named global.cross-origin-isolation-audit(Info) — missing cross-origin isolation headers (COOP / COEP / CORP).reverse-tabnabbing-detect(Low) —target=_blanklinks withoutrel=noopener.
vigolium skillscommand —list, inspect (get), andinstallthe bundled coding-agent skills into a coding agent’s skills directory. Frontmatter parsing is lenient, so bundles using a looser SKILL.md schema still surface their name/description.- olium provider:
openai-responses— the public OpenAI Responses API (POST https://api.openai.com/v1/responses) with API-key auth. Same key resolution asopenai-api-key(agent.olium.llm_api_key→$OPENAI_API_KEY), differing only in the wire format (Responses instead of Chat Completions). Default modelgpt-5.5. - olium provider:
anthropic-claude-sdk-bridge— drives Claude Code (or Codex) through the Agent SDK via a newvigolium-audit bridgesidecar, using your logged-in Claude Code subscription; no key required (an explicitllm_api_key/oauth_tokenis forwarded). The bridge binary is resolved fromagent.olium.bridge_binary/--bridge-bin, else the embedded vigolium-audit blob, elsevigolium-auditonPATH. - olium provider:
anthropic-compatible— any Anthropic Messages (/v1/messages)-compatible endpoint at a customagent.olium.custom_provider.base_url(a self-hosted Claude gateway or a Messages-format proxy);extra_headerscan switch the auth scheme. scan --print-traffic/--print-traffic-tree— after a scan, dump the run’s HTTP traffic to stdout:--print-trafficprints raw request/response pairs (liketraffic --raw),--print-traffic-treeprints a host/path hierarchy tree (liketraffic --tree). Mirrors the existing--print-finding, pairs with-S/--silentfor a quick throwaway single-target scan, and is available onscan/scan-url/scan-request/run.finding --tree(alsovigolium finding tree) — a host/path hierarchy view that collapses recurring findings by title (module name + short description) and severity into one node with a×Nrecurrence count, listing each affected#id (confidence) → urlbeneath it. The positional argument now routes liketraffic:treeactivates the tree,ls/listselect the default table, anything else is a fuzzy search term.finding --pick— narrow the result page to specific 1-based positions after filters and sort (--pick 2,--pick 1,3,--pick 2-4); composes with--raw/--burp/--markdown/--json/--tui. Out-of-range positions warn on stderr and are skipped; a spec that selects nothing from a non-empty list errors with the valid range.- Repeatable
--searchonfindingandtraffic—--searchis now repeatable and AND-combined, so each added term further narrows the match (--search sqli --search login).findingsearch also widened to cover the module name and short description (in addition to the description, module id, and matched-at). - olium
--bridge-binflag /agent.olium.bridge_binaryconfig — point theanthropic-claude-sdk-bridgeprovider at a specificvigolium-auditbinary hosting the SDK bridge (default: embedded blob, thenPATH).anthropic-claude-cliis accepted as an alias foranthropic-cli.
Changed
- JWT vulnerability scanner gains two forgery legs:
jku/x5uheader-claim injection pointing at unique OAST URLs (a callback proves the server fetched an attacker-controlled verification key), and akidpath-traversal token that forcesalg=HS256and signs with the empty contents of/dev/null(accepted only by a server that resolveskidto a file and uses it as the HMAC secret). - XXE scanner gains blind / out-of-band detection: per-payload external-entity and parameter-entity DTD templates that reference a unique OAST URL, so a callback on the unguessable subdomain is unforgeable proof the XML parser resolved the external reference.
- XML / SAML security adds signature-bypass detection — stripping the
<Signature>element from a SAML assertion/response and confirming the target still accepts the unsigned document. - Sensitive-file discovery adds high-confidence signatures for a fully walkable
.gittree (indexDIRCmagic, reflog, packed-refs → whole source reconstructable with git-dumper, not merely a leaked config), Terraform state (*.tfstate), and docker-compose files, each with HTML anti-markers so catch-all pages don’t trigger it. - CSP weakness audit now understands
nonce-/sha*-source expressions andstrict-dynamic, so it no longer flagsunsafe-inlinewhen a nonce/hash orstrict-dynamicmakes modern browsers ignore it. - OAST service classifies the new JWT (
jku/x5u) and XXE out-of-band callbacks so their in-band-silent legs correlate correctly. - Codex provider refactor — the shared OpenAI Responses request shaping and
response.*SSE state machine are extracted intopkg/olium/provider/responses_stream.goand reused by bothopenai-codex-oauthand the newopenai-responsesprovider. finding --markdown/traffic --markdowncompact response bodies by default — a response body now renders as a window around the finding’smatched_at/evidence (or a leading preview with a truncation note) instead of dumping a whole page, and this no longer depends on-S/--compact; pass--full-bodyto render bodies whole. The request is always shown whole (it carries the payload/injection point). The rendered Markdown is also syntax-highlighted on an interactive terminal (headings, rules,```fences, bold,inline code) while piped/redirected output stays plain, greppable Markdown.- Subcommand help reorganized — the inherited Global Flags block is regrouped (Output / Network / Extensions / Project / Info / Configuration) and collapses to a single-line pointer when stdout is piped or redirected, so an agent or
cmd | lessno longer sees the full block repeated on every subcommand (the full list stays reachable viavigolium --help); the native-scan (scan/run/scan-url/scan-request) local-flag help is regrouped into Target & Input / Scanning / Discovery / Spidering / Module Selection / … sections. - Traffic /
db … treerows — dropped the per-row[Nh]request-header-count tag and now render the path in white and the response content-type in orange for readability. - Provider override flags list the new backends — the
--providerhelp across the agent subcommands now listsopenai-responses,anthropic-compatible, andanthropic-claude-sdk-bridge;vigolium agent --list-agentsshows the bridge agent and theanthropic-claude-clialias, and the active-provider marker is matched against the canonical provider name.
Fixed
- Path-normalization / FUZZ baseline false negatives — the discovery fingerprinter no longer runs bypass paths through
path.Clean; a newSetWirePathpreserves bypass sequences (%23,..,//,..;/) byte-for-byte on the wire,engine_initkeys the baseline viaExtractCacheKey(instead of apath.Dirthat collapsed the bypass to/), andhttpmsgemits the rawRequestURIrather than a decoded relative path. Cache-key and content-discovery findings that depended on an un-normalized path are detected again. - Scope keyword over-matching — origin-scope keyword matching now compares the keyword against the host’s registrable-domain leading label (its eTLD+1 label) instead of a bare full-hostname substring. Same-org hosts on other TLDs / brand domains stay in scope (keyword
acmestill matchesacme.io,acmegroup.com) while an unrelated third party whose subdomain merely contains the keyword — e.g.acmegroup.cloudflareaccess.com, a Cloudflare SSO wall whose registrable label iscloudflareaccess— is now correctly excluded. --db-isolatemerge race on a fresh shared--db— the destination-database critical section (open → schema-create → default-seed → merge) now runs entirely inside one cross-process merge lock. Previously the WAL-mode switch andCREATE … IF NOT EXISTSDDL ran outside the lock and could surfaceSQLITE_BUSYunder a-Pfan-out of finishers targeting one brand-new shared database.
Internal
- New shared infra:
infra/llmsig(LLM-endpoint signatures) andinfra/cookiesig(cookie signal catalog) backing the new passives;provider/responses_stream.goshared by the Responses-format providers. - vigolium-audit bridge sidecar (
platform/vigolium-audit) — a headless Agent-SDK entrypoint (bridgeone-shot +bridge servedaemon) that resolves a task preset (triage / exploit / plan), assembles an ephemeral plugin that always loads thevigolium-scannerskill, runs one agent loop, and returns a normalized event stream. Backs theanthropic-claude-sdk-bridgeprovider. - New CLI display helpers:
pkg/cli/finding_tree.go(grouped finding tree), the--pickposition selector and repeatable---searchwiring infinding.go/traffic.go,markdown_highlight.go(interactive-only Markdown ANSI tinting), andscan_print_traffic.go(--print-traffic{,-tree}). The query layer gains an AND-combinedSearchTerms []stringonQueryFilters(pkg/database/query.go), andpkg/cli/usage.gowas refactored so global and native-scan flags render from declarative group tables (with a piped-output terse mode). - New tests:
resolve_provider_test.go,openai_responses_test.go,claudesdkbridge_test.go,anthropic_compatible_test.go,bypass_regression_test.go,fuzz_probe_resolve_test.go,url_request_target_test.go,signature_bypass_test.go,finding_pick_test.go,finding_tree_test.go,usage_test.go, expandedscope_matcher_test.gokeyword-scope cases, plus scanner tests for the JWT/XXE/CSP legs and for the newskills/usageCLI.
The first stable (non-beta) release, and a major expansion of detection coverage. Six new module families land — GraphQL security testing, Adobe Experience Manager (AEM), SaaS data-exposure (Salesforce / ServiceNow / Power Pages), Internet Information Services (IIS), an enlarged Model Context Protocol (MCP) suite, plus standalone dependency-confusion and JavaScript-beautification passives. Together they add 29 registered modules (22 active + 7 passive), bringing the registry to 193 active + 109 passive. Every new active family is tech-gated, fail-closed, and multi-round-confirmed to keep the false-positive floor low.
Added
- GraphQL security scanning (OWASP WSTG) — the
graphql_scanmodule gains a full phase suite backed by a newpkg/graphqlxlibrary: operation-name expansion, introspection/console exposure, query batching, field-level authorization (IDOR), reflected-XSS-in-errors, boolean-based SQL injection, and denial-of-service (deeply-nested / aliased / circular queries). Every phase runs only against an endpoint confirmed to speak GraphQL (a{ __typename }probe returning a spec-shaped envelope, not merely the word “graphql”) and confirms findings across multiple rounds. - Adobe Experience Manager (AEM) module family — 11 active + 1 passive
aem_*modules over a shared, fail-closedinfra/aemgate (ConfirmAEM): dispatcher-bypass (differential), sensitive-servlet and console exposure, QueryBuilder content discovery (surfacingrep:passwordas Critical), default credentials, CloudSettings injection, reflected XSS (headless-confirmed), SSRF, out-of-band injection, XXE (CVE-2019-8086 entity-expansion proof), and RCE, alongside a passiveaem-fingerprint. Detection-only; every active probe is gated to a genuine AEM instance. - SaaS data-exposure family (Salesforce / ServiceNow / Power Pages) — 7 active + 3 passive modules over a shared
infra/saasprobegate: Salesforce Aura object/record exposure (getConfigData/getItems), Salesforce Aura guest Apex execution (ApexActionController.executereachability, confirmed with a benign read-only managed-package method — the pivot for guest-driven SSRF/DML/exfiltration), Salesforce Lightning debug-mode exposure (a site served inPRODDEBUG/DEV/JSTESTDEBUGthat leaks backend stacktraces to unauthenticated clients), ServiceNow widget and KB-article data exposure (g_ck+ cookie, treating a401as not a finding), and Power Pages Dataverse/_apiexposure, each paired with a per-vendor passive fingerprint. Every active probe gates on a live Aura gateway (fail-closed presence check), pairs a catch-all negative control with the positive, and confirms across multiple independent rounds. - IIS module family —
iis-shortname-discoverygains true 8.3 short-name resolution (a Go port of the gen8dot3/checksum technique that feeds deparos content-discovery with embedded wordlists), joined by two new active modules:iis-cookieless-source-disclosureandiis-extension-confusion-bypass(::$DATA/ trailing-dot). All IIS/ASPX-gated and multi-round-confirmed. - MCP suite expansion — two new active modules (
mcp-tool-definition-drift,mcp-dos-amplification) and a new passivemcp-dangerous-tool-exposureextend the Model Context Protocol family audited against OWASP. dependency-confusionpassive module — extracts scoped npm import specifiers from first-party JS responses only and flags any package left unclaimed (404) onregistry.npmjs.org. Suspect/Tentative severity, batched registry lookup via a flusher, with over-query guards (public-scope skiplist + statement-boundary regex) and fail-closed handling of any indeterminate lookup.js-beautifypassive module — unminifies and unpacks minified or bundled first-party scripts (React/Next/Vue SPA output) into readable source, driven by the embedded jstangle tool overwebcrack(a static unpacker — noeval-based deobfuscation). jstangle runs as a boundedbunsubprocess (capped atGOMAXPROCS-2, max 4, so a wide passive fan-out over many JS records never spawns a process per record) and returns a beautified document plus, for webpack/browserify bundles, a per-module split with recovered source paths — the jstanglewebpackExtractorwas extended in this release to reconstruct those module paths. For JavaScript responses the stored record body is rewritten in place with the beautified document (droppingContent-Encoding) and taggedjs-beautified/js-format:<format>, so linkfinder, observed-word harvesting, and manual review all read clean source; inline<script>code in HTML pages is beautified into finding evidence only (js-inline-beautified), never rewriting the page body. A cheap Go-side pre-gate (min 500 bytes, webpack/Next/SystemJS markers or a minified average line length) avoids spawning a subprocess for tiny or already-readable scripts, results are deduped per host+path, and known third-party assets — CDN libraries and analytics/captcha/chat/payment/bot-management SDKs, matched byjsvendoron host, path, and content signature — are skipped unless the scan is actually targeting that vendor. Informational severity; the module cleanly no-ops when the embedded binary is unavailable.
Changed
- Version dropped its
-betaprerelease label and jumped to a minor release — v0.2.0 is the first stable release, reflecting the size of this coverage expansion. - MCP false-positive hardening across the existing family — method-enum negative control, batch singleton baseline, session-fixation gate, origin locality, server-probe
isError/skip handling, and resource-fuzz OAST/metadata checks — plus expandedmcp-description-injectioncoverage.
Internal
- New shared libraries:
pkg/graphqlx(GraphQL detection, introspection, schema, and operation building) and theinfra/aem/infra/saasprobegate packages backing the AEM and SaaS families. - Tech-fingerprint tags added for the new gating (
aem/adobe,salesforce/lightning/aura,servicenow,powerpages/dataverse) intech_tags.go, published by the respective*_fingerprintpassives. - The jstangle
webpackExtractor(platform/jstangle) was extended to recover per-module source paths forjs-beautify; supporting tweaks landed across the spider (pkg/spitolas), discovery (pkg/deparos), and the core executor/rate-limiter.
A discovery-depth and data-portability release. The spider now fills forms with values derived from the target and the page itself (so register→login flows carry consistent identities), and — at balanced/deep intensity — sprays a short list of common credentials against confirmed local login forms to crawl authenticated. Separately,
vigolium import learns to merge one vigolium .sqlite result database into another, agent audit -S can bundle its report with the raw scanner output, and vigolium server gains a passive-only scan-on-receive mode.Added
- Spider response-aware form filling — the crawler (
pkg/spitolas) now derives form values from the target and the response instead of always using fixed placeholders: it recalls a value entered earlier in the same crawl (so a registered identity is reused at login), falls back to target-derived values (vigolium-crawl@<domain>,<label>_crawl), then to an on-page example (a field’sdefaultValueor adatalistoption), and only then to the existing smart/fixed defaults. Passwords are never taken from the page (a strong fixed password is kept). Default-on and non-destructive — with no fill context the original behavior is preserved. - Common-credential login spray on confirmed login forms — at
balancedintensity the spider tries a minimal list (admin:admin,admin:123456) and atdeepthe full documented short list (never a wordlist) against a login form, but only after confirming the form is a genuine local login (exactly one visible password + submit + in-scope action, not an external IdP) and that a bogus control pair is rejected first. On success the session cookies persist and the crawl continues authenticated. Off atquick/lite(lockout/authorization risk). Reported viaLoginCredsTried/LoginCredsSucceededon the spider result. vigolium importmerges vigolium SQLite databases —importnow accepts another vigolium result database as its source (auto-detected by its SQLite magic header, so.sqlite/.sqlite3/.dbor a bare name all work) and folds it into the destination--db(or the configured default). A lossless, idempotent SQLite→SQLite merge of HTTP records, findings, scans, agentic scans, and OAST interactions, deduped on natural keys — re-importing the same database is a no-op, and each row keeps its originalproject_uuid. The natural companion toscan -S --format sqlite: fan out per-host.sqlitefiles, then merge them back into one queryable DB.-jprints a per-table merge summary.agent audit --output-dir <dir>(stateless-only) — bundles a-Srun’s HTML report (as<dir>/vigolium-audit-report.html) and a copy of each driver’s rawvigolium-results/tree into one folder — one driver lands flat, several are namespaced under<dir>/<driver>/. A relative-onests under the bundle; an absolute path orgs://URL escapes it;{ts}/{project-uuid}are expanded once so the report and raw copy share a directory.-Swithout--output-dirnow nudges the operator that raw output stays under<source>/vigolium-results/.vigolium server --passive-only— with-S/--scan-on-receive, restricts scanning to passive modules only (no active scan traffic; secret detection still runs) on every ingested request. Warns when combined with--full-native-scan-on-receive, which still crawls.
Changed
- Example blocks refreshed —
replay,update,agent triage, andextensions examplenow carry inline--helpexamples; theimportexamples document the new SQLite-merge source.
Internal
- New
form.FillContext(pkg/spitolas/internal/form/fillcontext.go, per-crawl identity memory) threads response-aware values throughgetValueForInput;DetectedInputgainsDefaultValue/DatalistOptionsharvested by the DOM-detection JS. The login spray lives incrawler/login_creds.go(single-flight per host, negative-control gated), driven by newSpiderConfig.LoginCredentialAttempts/LoginCredentialFullListfields that the runner sets vialoginCredsPolicy(intensity)for the discovery and re-spider phases. - SQLite file detection is centralized in the new
pkg/database/detect.go(IsSQLiteFile/HasSQLiteHeader, magic-header based);dbimport.ImportPathdispatches to a newImportSQLite(over the existingdatabase.MergeSQLiteFile) and surfaces aMergeStatson the importResult.runner_modules.godocuments the module-slice sentinel convention (nil/empty → zero modules,["all"]→ every module) that--passive-onlyand--no-passiverely on. - Test coverage added:
form/fillcontext_test.go,crawler/login_creds_test.go(+ anintegration-tagged end-to-end),database/detect_test.go,dbimport/importer_test.go,agent_audit_stateless_test.go, and a--passive-onlyserver-options guard.
A replay-ergonomics release.
vigolium replay gains a bulk mode that re-sends every matching stored record through the mutation/diff engine — so an operator (or a coding agent driving vigolium) can fuzz one parameter across a whole host, or funnel all stored traffic through Burp, in a single command with streaming JSONL output.Added
vigolium replaybulk mode — passing--all(or any of--host/--method/--status/--path/--source/--search/--body) replays every matching stored record instead of a single source, mirroring thetraffic --replayfilters but running each record through the full replay engine. Any--mutateis applied to each record that has that insertion point; without one, records are re-sent verbatim. Results stream as JSONL (one stablereplayOutputper record) so a bad record surfaces an inlineerrorwithout aborting the batch. Throttle with-c/--concurrency(default 10) and cap with-n/--limit(default 100;--alllifts it).replay -S/--stateless --db <file>— replay can now read baselines from a standalone.sqliteor.jsonlexport with project scoping off, matching the read semantics oftraffic/finding. Every DB touchpoint in the command (record/finding lookup,--auth-sessionmerge, bulk query) routes through the sharedopenReadDB/effectiveProjectUUIDhelpers.
Changed
replayno longer prints the banner — added to the banner-suppression list alongsidescan/run/traffic, keeping JSONL output clean for piping.traffic --replayexamples refreshed — document stateless-export replay, method/host filtering, and--all --with-browser.
Internal
- The single-source and bulk paths share one call shape via a new
replayRunstruct and itsone()method; the HTTP client construction (cookie jar +--proxyrouting), cookie-jar save, header-overlay parsing, and output-writer setup were extracted into reusable helpers (newReplayClient,saveReplayJar,parseReplayHeaderFlags,openReplayOutputWriter,sourceFromDBRecord). Bulk selection maps directly ontodatabase.QueryFiltersviabuildReplayBulkFilters. New coverage inreplay_bulk_test.go.
A false-positive-hardening and severity-calibration release. A broad sweep stops markup-reflection, error-signature, IDOR, and differential detectors from self-triggering on echoed input, framework build artifacts, and ambiguous numeric parameters; a batch of low-signal passive detectors drops from Medium to Low/Info; and secret findings now carry a short credential-family label.
Added
- Secret findings are labelled by credential family —
secret-detecttags each finding with a short classification (JWT,Google API key,reCAPTCHA site key,Google OAuth client ID, or the Kingfisher rule name verbatim), rendered as a leading[JWT]bracket on the console line and carried in the jsonl/DBmeta.patternfield. The console renderer picks up any module’sMetadata["pattern"], so it’s a generic opt-in.
Fixed
- Markup-reflection oracles ignore JSON/XML-echoed payloads — a new
modkit.IsNonHTMLReflectionContexttreats an HTML/template marker echoed inside a non-HTML response (the canonical FastAPI/Pydantic422 application/jsonvalidation-error echo) as a data value, not rendered markup. Applied to the reflection paths ofcsti-detection,pdf-generation-injection, andws-injection; evaluated-result oracles (SSTI/OGNL/SQL errors) are left alone. - Error-signature detectors bound their match span — a loosely-anchored
X.*?Ypattern could bridge two coincidental words across a large body (Oracle.*?Driverspanning 60 KB of a Salesforce Aura shell). The sharedmodkit.MatchWithinSpan/MaxErrorSignatureSpan(512 B) gate requires a compact span while still firing on a genuine short signature elsewhere. Applied tosqli-error-based,insecure-deserialization, and siblings. - IDOR detectors no longer flag bare sequential integers —
authzutil.ClassifyParamdrops the value signal for a plain sequential integer lacking both an identifier-shaped name and a resource-noun path context, so telemetry/pagination/status params (page_number=5,responseStatus=200) stop being misreported. Wired intoidor-guidandidor-params-detect. - Passive server-side detectors skip client build bundles —
jsframework.IsClientBuildArtifactskips immutable framework output (/_next/static/,/_nuxt/), where server-only constructs are compiled out, so a shared vendor chunk no longer yields one false positive per host. Applied across the Next.js/Nuxt/server-action detectors andcache-data-leak. - Differential detectors confirm against non-determinism —
http-method-tamperingandrace-interferenceadd determinism controls so a per-request token/nonce or SSR variance between legs isn’t mistaken for a method-override or race divergence. - Discovery extension-fuzz gating — deparos trusts a start URL’s
.aspx/.php/.jspsuffix as a stack signal only when the URL was actually served as that extension (2xx/401/403, not a 3xx/404/5xx recon seed), plus a per-extension catch-all guard (two random<nonce>.<ext>probes) so an SPA/CDN that 200s any path isn’t fingerprinted into a wrong-stack wordlist fuzz. - Lower-signal passive findings recalibrated —
oauth-facebook-detectandgraphql-introspection-detect→ Info;serialized-object-detect,jackson-deserialize-detect,sensitive-api-fields-detect,api-pagination-leak,auth-headers-detect→ Low;jwt-claims-detectnow grades per-issue (hygiene → Low,alg=none→ High);postmessage-handler-detect,javascript-uri-sink,openredirect-params,input-reflection-detect, andunsafe-html-sinkgained value-shape/context gates.
Internal
- New shared helpers
modkit.IsNonHTMLReflectionContext,modkit.MatchWithinSpan+modkit.MaxErrorSignatureSpan,jsframework.IsClientBuildArtifact, theauthzutil.ClassifyParamsequential-int gate, andsecret_detect.PatternLabel;format_screen.gorenders any finding’sMetadata["pattern"]as a leading bracket. Regression tests added alongside each hardened module.
A false-positive-hardening release. Path-probe exposure modules no longer confirm on their own last path segment reflected into a content route, several injection detectors no longer self-trigger on an endpoint that merely echoes the rejected payload back, and secret-detect treats copy-paste sample credentials on documentation pages as low-value.
Fixed
- Path-probe exposure modules ignore their own reflected slug — a marker that is just the probe’s last path segment (
filamentfor/filament,redocfor/<dir>/redoc,healthchecks-uifor/healthchecks-ui) self-matched when a content/SEO route echoes the requested slug into the page (a/topic/filamentroute rendering “Filament Lenses” in the title/JSON-LD/canonical link). A new slug-reflection control probes a nonexistent sibling carrying a distinctive canary and drops the finding only when the route reflects that canary into a 200 response. Applied tolaravel-admin-exposure(barefilament/Filamenttokens dropped in favor of structural anchors likefilament-panels,/filament/assets/,fi-sidebar),swagger-exposure, andaspnet-health-exposure. struts-ognl-injectionconfirms the addHeader variant via a real response header — thestruts2-ct-ognlpayload setsX-Struts-Testvia OGNL, but a gateway that rejects the request (415/400) commonly echoes the injectedContent-Typeverbatim into the error body and into other header values (e.g. gRPC’sGrpc-Message), forging bothX-Struts-Testand the baked marker with no OGNL ever running. Detection now keys onX-Struts-Testbeing a genuinely parsed response header, and confirmation re-injects a fresh random marker and requires it back as a real header each round.- Injection detectors strip the reflected payload before matching — a new
modkit.StripReflectedremoves the injected payload from the response before signature matching, so an endpoint that merely rejects and quotes the input back (a 400 “invalid input:<payload>”) can’t self-trigger. Fixesinsecure-deserialization(PHP’sO:8:"stdClass":0:{}wire form matched theO:\d+:"..."error pattern) andxxe-generic(the internal-entity probe carried its own success marker as the entity value);nosqli-error-basednow uses the shared helper, and thestruts-ognl-injectionarithmetic variant strips the payload as defense in depth. secret-detectdowngrades documentation-page demo credentials — a secret-shaped string served as rendered page content from a docs/reference/manual/CLI/tutorial route (Supabase’ssupabase-demoanon/service_role JWTs, local-devpostgresql://postgres:postgres@…URLs, placeholder connection strings) is almost always an illustrative sample, so it drops to Low/Tentative. Gated on both the route and an HTML/RSC (text/x-component) content type, so a JWT in a/docs/_next/static/...bundle or a live token in a docs-path JSON API still keeps full severity. A validated live secret always outranks this to Critical. Wired into both the passive flush and the known-issue-scan path.
Internal
- New shared path-decomposition and slug-reflection helpers in
modkit/pathprobe.go:splitProbePath,PathSegmentReflected,SlugReflectionFP, and a slug-reflection control folded intoMatchAndConfirmSibling(grouped callers pass only the anchor; flat-marker callers pass every matched marker).modkit.StripReflectedis the request-body analogue ofStripReflectedProbePath.secret-detectgainsIsDocDemoSecretContext(plus thedocRouteSegmentsset and content-type gate) and threads the responseContent-Typethrough its batch entries.
A scope-isolation fix release. The known-issue-scan phase, dynamic-assessment, and the scan-completion summary now restrict themselves to the exact origins (scheme + host + port) actually being scanned, so records left in a shared project by prior scans of other origins no longer leak into a run’s targets or counts.
Fixed
known-issue-scanno longer scans origins outside the current scope — the Nuclei and Kingfisher secret-scan legs previously pulled every record in the project DB, so targets/response bodies from earlier scans of unrelated origins (e.g. leftoverlocalhostrecords in the default project) were silently scanned alongside the current target. Both legs are now restricted to the same in-scope originsdynamic-assessmentuses; with no CLI targets/scope it falls back to a project-wide pass, matchingdynamic-assessmentsemantics.- Record scoping now matches scheme + host + port, not just hostname —
dynamic-assessment,known-issue-scan, spidering, discovery, and the targeted re-spider select DB records by the CLI target’s full origin, so a different port on the same host left by a prior scan (e.g.localhost:8080while targetinglocalhost:3000) is no longer pulled in. Default ports are normalized (https→443, http→80) sohttps://example.commatches its stored records. - In-scan discoveries are always scanned, regardless of port — a host found during the current scan (a subdomain on the target’s scheme/port, or a different-port service from the
--intensity deepport sweep or a followed cross-port link) is scanned even when its origin differs from the CLI target’s. Provenance is keyed on the scan’s start time (records carry noscan_uuid), so this scan’s discoveries are kept while prior scans’ leftovers stay out. - The “Native scan completed” summary counts only the scanned origins — record and finding totals were previously a raw count of the entire database (no project filter, no host filter), inflating the summary with leftovers from prior runs. The record count is now scoped to the current project and in-scope origins; the finding count is scoped to the project and in-scope hostnames (the findings table carries no scheme/port column).
Internal
- New
Repository.InScopeHostsresolves the in-scope origins from CLI targets + scope config + DB state (single source of truth for the runner phases and the summary): an origin is in scope when its hostname passes the scope matcher AND either its (scheme, port) matches a target OR it was discovered during the current scan (records created at/after the scan start). The DB input sources gainWithHostScopes, andGetDistinctPaths/GetRecordsWithResponseBody/GetReSpiderCandidates/CountRecordsAfterCursortake an optional variadicHostTargetorigin filter that is a no-op when empty.
A performance and false-positive-hardening release. The CLI gains an opt-in update check, the evaluated-math and reflected-value detectors now confirm a match tracks fresh inputs instead of a fixed substring, and a broad hot-path sweep cuts allocations and blocking work across deduplication, the database, discovery, and HTML analysis.
Added
- Startup update check (
vigolium update) — Vigolium now checks the npm registry at most once every 24 hours (cached in~/.vigolium/update-check.json, 1.5s timeout, fetched in the background) and prints a one-line upgrade notice after the run when a newer@vigolium/vigoliumis available. The notice is suppressed for--json, CI, non-TTY/piped output, and theversion/update/initcommands. SetVIGOLIUM_AUTO_UPDATE=1to silently install the new version and re-exec the original command; setVIGOLIUM_DISABLE_UPDATE_CHECK=1to turn the whole thing off.
Fixed
reflected-sstiandstruts-ognl-injectionconfirm the evaluated result tracks fresh operands — both detectors previously matched a single hard-coded product (1970×2024for SSTI; a fixed constant for OGNL) appearing anywhere in the body, which collides with incidental numbers (product ids, file sizes, timestamps). Each now re-injects the winning payload with two rounds of fresh random operands and requires the newly computed product to appear each time. This also repairsstruts-ognl-injection, whose hard-coded result string (1614244871) didn’t equal the operands it claimed (41273×39127 = 1614888671), so genuine detections were being silently dropped — the marker is now computed, not hardcoded.web-cache-poisoningconfirms generic header values actually reflect — short, common probe values such asX-Forwarded-Port: 1337or a scheme override matched on a single coincidental substring (an asset hash, a story id). The detector now re-injects two fresh, distinct values (a random high port, a canarized scheme token) and requires each to reflect the same way before flagging.
Performance
- Lower-contention deduplication —
DiskSetlookups take a read lock and escalate to a write lock only for genuinely new keys, and request hashing no longer materializes per-insertion-point body copies when body hashing is off (the default), unblocking the 60+ modules that share one dedup set. - Off-hot-path database writes — DNS resolution for saved records moved to a bounded background pool (no more ~5s blocking timeouts on the write path; IP is now best-effort metadata), record dedup collapsed from a per-record
SELECTon each worker to one batched lookup in the flush goroutine, and a new covering index makes the scan-status severity aggregation index-only. - Cheaper per-request and per-module work —
IsMediaPath/media-class results are memoized on the request,GetMethod/GetPathscan only the request line, the tech registry serves read queries via lock-freePeek, the discovery enabled-module set is memoized, and the anomaly HTML extractor shares one lazily-built DOM index across its ~18 attribute checksums instead of re-walking the tree each time. Hot-path regexes and string formatting were hoisted/precompiled throughout.
A finding-deduplication and false-positive-hardening release. Out-of-band (OAST) callbacks now collapse to one finding per planted payload, the secret detector ignores build-tool content-hash maps and folds repeats of a single rule, and the path-traversal / forbidden-bypass family confirms the target isn’t already reachable by a plain request.
Fixed
- OAST callbacks collapse to one finding per planted payload — a single out-of-band payload normally triggers a burst of callbacks (DNS
A+AAAA, several recursive resolvers hitting the authoritative server, then the HTTP-fetch leg), and each previously became its own finding sharing one callback host. Findings are now keyed by the callback nonce and coalesced by protocol strength: duplicate or weaker callbacks fold into the existing finding, while a strictly stronger callback (the HTTP fetch confirming an earlier DNS lead) upgrades it in place. The command-injection modules also mint a unique OAST host per breakout variant instead of sharing one host across all payloads, so a callback pinpoints the exact shell payload that fired. command-injection-oastand blind-SSRF downgrade proxy-reflected host headers — a unique OAST host placed inX-Forwarded-Host,X-Forwarded-Server,X-Host,X-Original-Host,X-Original-URL, orX-Rewrite-URLis routinely reflected by a reverse proxy into a redirectLocation/ upstream URL that the proxy (or a redirect-following client) then fetches — an outbound request with no shell or server-side SSRF involved. A sharedisProxyReflectedHostHeaderset now downgrades both the DNS and HTTP callbacks on these headers to informational, replacing the single hard-codedX-Forwarded-Hostcheck; genuine parameter-based SSRF and client-IP headers (X-Forwarded-For,Referer,Origin, …) stay high.secret-detectignores build-tool content-hash manifests — a new structuralIsChunkHashManifestMatchguard recognises when a fixed-width / high-entropy rule clipped its “secret” out of a webpack/Vite/rspack chunk-hash map (a minified bundle ships dozens to hundreds of identical-shape lowercase-hex hashes). A response that genuinely leaks one short hex credential carries at most a handful of unrelated hashes, never a map of them, so the match is dropped. Wired into both the passive flush and the known-issue-scan path via the sharedIsNonSecretMatchchain.secret-detectfolds repeats of one rule while keeping distinct secrets apart — a new by-rule grouping mode collapses findings on(module, rule_name, severity[, host]), so one Kingfisher rule (e.g. “Looker Client ID”) matching every hash in a bundle’s content-hash map becomes a single finding (all values unioned on), while a genuinely different secret — an AWS key, a Slack token — keeps its own row. Distinct from the by-module mode, which would wrongly merge unrelated secrets under one finding.- Path-traversal and forbidden-bypass detectors confirm the target isn’t reachable cleanly — a “clean-canonical control” now verifies that the file/route a bypass appears to reach isn’t already served by a plain, un-mangled request (a public file at the web root, an app catch-all shell, or a resource that simply became public since the crawl-time
401/403baseline). Added topath-normalizationstatic traversal,nginx-off-by-slash,php-path-info-misconfig,cdn-object-traversal-listing,forbidden-bypass, andnextjs-middleware-bypass.
A false-positive-hardening release. Several detectors that fired on a name or substring match now require structural proof, and the secret detector gains per-URL deduplication, two new reflection/source-code guards, and an inline matched value in the finding body.
Fixed
secret-detectno longer reports public OAuth client IDs as leaked secrets — a Google OAuth client ID (NNNN-xxxx.apps.googleusercontent.com) is the public half of an OAuth client, embedded in every sign-in button by design, so it now drops to Info/Tentative. The paired client secret is a separate match and keeps full severity.secret-detectdrops JavaScript source artifacts matched out of unicode escapes — a new structuralIsJSEscapeArtifactMatchguard recognises when a fixed-length / high-entropy token rule clipped its match out of a\uXXXX/\xXXescape in a minified bundle (e.g. Angular’sɵ-prefixed exports), which is source code, not a credential. The check is body-located and conservative, so a genuine secret in the same bundle is still reported.secret-detectdowngrades values reflected from the request — a matched value that appears verbatim in the request URL or raw bytes (the dominant case: a Cloudflare Access application id in a/cdn-cgi/access/verify-code/<app-id>SSO URL echoed into the login page) is client-supplied input the server merely echoed, not a newly leaked server-held secret, so it drops to Low/Tentative.secret-detectcollapses the same secret re-observed on one URL — the same(host, url, rule, snippet)leak is now emitted once across the discovery / spidering / re-spider / dynamic-assessment passes (and across records in the known-issue-scan batch), so near-identical request/response copies no longer pile up as redundant Additional Evidence. Distinct secrets on a single URL (e.g. aclient_id,client_secret, andaccess_tokenin one response) are no longer merged by the URL-keyed finding dedup — secret-detect is excluded from that pass and deduped by value instead. Every secret finding’s description now ends with the matched value inline.csrf-verifyonly fires on a cross-site-forgeable request — token enforcement is now checked only when the request is actually CSRF-able: a simple/form content type, no header-based auth (Authorization), and an ambientCookie. A*token*-named field in a JSON/XML body (a CORS non-simple request that can’t be auto-submitted cross-origin) is application data, not an anti-CSRF token — closing the false positive on a Cloudflare RUM beacon’s JSONsiteToken. The token-name pattern is also anchored (\btoken\b) so camelCase app fields likeaccessToken/deviceTokenno longer match.wp-user-enumrequires a per-author leak, not a generic redirect — adds a baseline control (an author id far beyond any real account), an edge-block guard (IsBlockedResponsefor WAF/SSO/maintenance pages), a uniformity guard (multiple?author=Nids collapsing to one slug is a catch-all, not enumeration), and rejection of author-id echoes (/author/1→/author/1.html), WordPress’ own routes, and error/status-shaped slugs.drupal-user-enumrejects self-canonicalised UID echoes — a redirect whose captured segment is just the requested id echoed back, bare or with an appended extension/selectors (e.g. AEM canonicalising/user/1→/user/1.html), is no longer mined as a leaked username.cors-headers-detectonly flags credentialed CORS on true cross-origin reflection — credentials enabled alongside a specific origin is now reported only when the response echoes the request’sOriginand that origin is cross-origin to the target host. A fixed allow-list entry or a site reflecting its own origin (e.g. a Cloudflare RUM telemetry beacon) is the normal, safe pattern and is no longer flagged.
Adds a no-database
fs export format and a live filesystem mirror for the ingestion server, so a coding agent can investigate a scan or watch ingested traffic with ls/grep/jq. Also tightens the spider’s max-duration so it can no longer run past its deadline.Added
--format fs— a flat, browsable filesystem export — writes<base>-traffic/+<base>-findings/trees (per-host raw.req/.resp.*files, finding.mdfiles cross-linked to their request, and a jq-friendlyindex.json) so a scan can be triaged with no DB. Available onexport,db export, and thescan/scan-url/scan-request/runfamily; honors--omit-response.vigolium server --mirror-fs <dir>(configserver.mirror_fs_path) — mirrors every saved HTTP record and finding to a live<dir>/traffic+<dir>/findingsfilesystem tree as they are persisted, in addition to the database, so an external agent can read ingested Burp/proxy traffic as files in real time. Wired through new optionalRepository.OnRecordSaved/OnFindingSavedcallbacks that never block the DB save path; per-host id numbering resumes across restarts.
Fixed
- The spider now honors
max-durationduring in-flight browser operations — the crawl deadline is bound onto rod’s per-operation CDP timeouts (navigation,WaitStable, clicks, element lookups, form fills) rather than only being polled between actions, so an individual blocking browser op can no longer push the crawl far past its budget. Browser-level teardown is also bounded so a wedged browser can’t hang shutdown. - Spidering gains an overall phase budget ceiling — each target still gets its full
max-duration, but the whole phase is capped atmax-duration × min(targets, 8), so a large merged target list (CLI targets plus many in-scope DB hosts) can no longer stretch spidering out tolen(targets) × max-duration. - Further false-positive hardening in the
path-normalizationtraversal detector and thecdn-object-traversal-listing,auth-headers-detect, andserver-action-authmodules.
A false-positive-hardening release for the host-injection, blind-OAST, file-read, and header-leak detectors, plus a scan-resume convenience. Detectors that fired on a substring match now require structural proof, and a DNS-only command-injection callback on a forwarding header is no longer reported as a confirmed shell.
Added
- Bare
vigolium scan --resume— run with no other flags to auto-discover the*.progress.jsonmanifest in the working directory, rebuild the original-S -T --split-by-host -Pcommand from it, and relaunch — no need to re-type the full flag set. Pass-o <prefix>to disambiguate when several manifests exist.
Fixed
- Host-injection detectors require authority position, not a substring — a new shared
modkit.HostReflectedAsAuthoritygate confirms the client-supplied host actually becomes the authority of a generated URL, rather than appearing as an incidental substring. This closes a CDN/OAuth false positive whereX-Forwarded-Host/Hostis reflected into aredirect_uri=/continue=query parameter of a 3xx whose authority stays the trusted IdP (nothing to poison), which a barestrings.Containsflagged High/Firm. Wired intoproxy-header-trust,host-header-injection,express-trust-proxy-misconfig,web-cache-poisoning, andoauth-misconfiguration. command-injection-oastgrades confidence by callback proof — a DNS-only callback proves a value was resolved, not that a shell ran. An HTTP-fetch callback (curl/wget) stays Critical/Certain; a DNS-only callback on a genuine parameter is High/Firm; a DNS-only callback on a client-IP / forwarding header (X-Forwarded-For,X-Real-IP,True-Client-IP) is downgraded to Low/Tentative, since edge infrastructure resolves those header values for geo-IP/logging with no shell running. Findings also reconstruct the real planting request (the actual;nslookup <host>payload) via a new OASTRecordPayloadhook.- LFI / file-read modules confirm by file shape, not bare words — a new shared
pkg/modules/shared/filesigpackage requires the leaked file’s real structural shape (a genuinepasswdline, a bracketedwin.inisection, an anchored nginx directive) with baseline subtraction, replacing bare-word markers (server,location,root:) that also appear in English, CSP directives, and JSON. Closes a Cloudflare 403 block-page reported as LFI. Wired intolfi-path-traversal(with a block/status gate),mcp-resource-fuzz, andmcp-tool-fuzz. sensitive-header-leakdowngrades redirect / auth-challenge artifacts — a token-shaped value in aLocation/Www-Authenticate/Refresh/Proxy-Authenticateheader, or any sensitive value on a 3xx response, is now Info/Tentative rather than Medium/Firm — these are login-flow navigation artifacts, not deliberate secret leaks. A hit in a genuine custom header (e.g.X-Api-Key) keeps full severity.
Adds resumable parallel scans: a stateless
-S -T --split-by-host -P fan-out stopped with Ctrl-C can now be picked up where it left off.Added
--resumefor the stateless parallel fan-out —vigolium scan -S -T <file> --split-by-host -P <n>now writes a tiny line-cursor progress manifest (<output>.progress.json) tracking how far the contiguous run of cleanly-completed targets reached. Re-running the same command with--resumeskips that prefix and scans only the remainder, while the final roll-up still reflects the whole batch (previously-completed hosts are folded in from the manifest). On Ctrl-C — or a failure — the summary prints a copy-pasteable resume command. The manifest stores only a cursor plus aggregate counts, never the target list, so it stays small for huge target files; out-of-order completions under-Pare buffered so at most a handful of in-flight targets are ever re-scanned. The progress file is also surfaced on the scan banner’sOutput:line. Scoped to the stateless fan-out — the--db-isolateparallel path and the single-target/sequential split path are unaffected (the latter warns that--resumeis fan-out-only).
A hotfix release for a schema-migration ordering bug that could break an upgraded database.
Fixed
CreateSchemano longer aborts on a pre-existing database missing a migration-added column — v0.1.35 addedhttp_records.response_norm_hashtogether with an index on it (idx_records_norm_hash), but on a database created by an older binary the index-creation loop ran before the column-add migration.CREATE INDEXthen referenced a column that did not exist yet, errored, and aborted schema init entirely — leaving the server’s repositoryniland breaking a swathe of endpoints. The index loop now runs after alladdColumnIfNotExistsmigrations, so an upgraded database heals itself (adding both the column and its index) instead of failing to open. Addedschema_migration_test.gocovering the legacy-DB heal path andCreateSchemaidempotency.
A performance and false-positive-hardening release. The native-scan hot path and ingest write path shed redundant allocations and DB work, and the discovery engine’s server-side extension-confirmation gate gains two catch-all guards so an SPA/catch-all gateway that bounces or echoes every guessed path can no longer confirm (and then wordlist-fuzz) a phantom stack. The workbench HTTP-records page gains a source filter and a curl export.
Added
- Workbench: source filter + curl export on the HTTP records page — a source dropdown filter and an always-visible clear-filters button let you scope the records list by ingestion source, and a new curl export helper copies any record as a ready-to-run
curlcommand. Detail-panel updates and regenerated static UI assets ship alongside.
Performance
- Fewer allocations and less DB work across the scan pipeline — the executor now uses struct-keyed per-host claim LRUs and reuses the baseline-request hash for finding↔record links, the request builder gains a single-allocation body +
Content-Lengthfast path,modkitreconfirm gets an allocation-free tokenizer, host rate-limit acquisition is context-aware, and many active scanners wrap the raw request directly instead of re-parsing it. On the database side, ingest writes are slimmed (metadata-only FTS index, a covering dedup index, projected scan-record column reads, batched remarks updates) and a newDB.Optimizerefreshes query-planner stats on checkpoint/close.
Fixed
- Extension confirmation no longer trusts a path-preserving redirect — a redirect that merely bounces a request back to the same path it asked for (
/x.php→/x.phpfor an HTTP→HTTPS upgrade, an auth/cookie round-trip, or host/trailing-slash normalization) fires at the gateway before any handler runs, so it is no proof the server executes that extension. Because each suchLocationis per-path-distinct it also slips past the wildcard soft-404 filter, which on a catch-all/SPA gateway would otherwise let every guessed stack extension confirm — and get wordlist-fuzzed — at once. The newredirectPreservesPathguard withholds the extension confirmation in that case; names and paths are still harvested. - Extension confirmation refuses a second, incompatible server stack — a single application serves exactly one server-side stack family (PHP xor classic/modern ASP.NET xor Java/JSP/Struts xor ColdFusion xor CGI). The confirmation gate (
reserveExtension+serverStackFamily) now checks the family under one lock: once one family is confirmed, a second incompatible one is a signal the host is answering for extensions it does not run — a catch-all/SPA gateway echoing every guessed path — so it is refused rather than fuzzed. The first family confirmed still fuzzes normally.
A false-positive-hardening release for the differential and behavioral injection detectors. A new shared surface gate keeps the boolean-oracle modules off cache/CDN-fronted and large dynamic-HTML pages, every boolean-blind SQLi probe is now WAF/CDN-block aware, and curated payloads are re-confirmed with randomized operands to defeat the classic WAF
1=1-tautology false positive. NoSQLi and default-credentials gain control/reproduction gates of their own. Also adds a repeatable -T/--target-file flag and a finding --confidence filter.Added
-T/--target-fileis now repeatable — pass it multiple times (-T hosts-a.txt -T hosts-b.txt) to merge several URL-list files into one run; lines from all files are read in order and concatenated. Plumbed throughscan,ingest, the ingestor client, the high-level runner, and the shared input-source builder, so an ingest from preloaded stdin/-istill folds in any-Tfiles instead of dropping them, and an-T-only run no longer blocks waiting on a TTY. (InternallyOptions.TargetsFilePath string→TargetsFilePaths []string.)finding --confidence <certain,firm,tentative>— filter findings by confidence label (comma-separated), mirroring--severity. TheCONFIDENCEcolumn now also shows by default in thefindingtable and is colorized (certain→purple, firm→yellow, tentative→gray) in line with the other summary lines.
Changed
client-prototype-pollutionrecalibrated to Medium/Tentative and gated on a reachable URL source — the module is a static-analysis heuristic (it only verifies the URL is reachable, never that a prototype was polluted at runtime), so its blanket High/Firm verdict was overstated. Severity now tracks the evidence: a bare source pattern is Medium, an actual high-impact gadget (innerHTML/eval/document.write reading attacker-controllable keys) raises realistic impact to High, and confidence stays Tentative throughout. Generic “sink-only” shapes (deep-merge / recursive bracket-assignment) that a safe object clone shares with a vulnerable URL-param parser are now reported only when an attacker-controllable URL source (location.search/hash/href,URLSearchParams, …) sits within proximity of the sink, dropping benign library code (analytics serializers, script loaders, framework helpers) that merely shares the shape. Patterns that embed their own URL source still fire on the pattern alone (FN-safe).
Fixed
- Differential-oracle modules now skip unreliable surfaces — a new shared
modkit.DifferentialSurfaceUnreliablegate (header/size only, sends no traffic) flags a response as an untrustworthy boolean-differential surface when a cache/CDN layer fronts the origin (anX-Cache/Agecache state — a HIT can answer one probe while an origin renders the next) or when the body is a large (≥100 KiB) renderedtext/htmlcontent page whose per-request dynamic content (ads, recommendations, rotating blocks) dwarfs any real true/false difference. The size gate istext/html-scoped, so JSON / auth endpoints — the real injection-oracle surface — are never affected, and a missing baseline fails open. Wired intosqli-boolean-blind,ldap-injection(the boolean pass only; the error-token pass still runs),ssti-detection,smart-behavior-detection(also a meaningful speedup — it is the slowest active module), andnosqli-operator-injection. sqli-boolean-blindno longer fires on WAF/CDN tautology signatures — two long-standing false-positive classes are closed. (1) Every probe is now WAF/CDN-block aware: a blocked TRUE or FALSE branch is the edge reacting to a payload signature (the literal1=1, the/**/-comment evasion form), not the application’s SQL logic, and it kills the pair (or the whole insertion point on a blocked baseline) — the single most common boolean-blind false positive. (2) Curated / bypass / WAF-evasion payload pairs are re-confirmed with randomized operands (confirmRandomized): the matched comparison is rebuilt with fresh random numbers while the surrounding boundary, token separator, comment terminator, URL-encoding, and evasion mutation are preserved, so a differential bound to boolean truth reproduces while one bound to the literal1=1token vanishes and is rejected. This defeats the textbookAccept-Language: '/**/OR/**/1=1--finding where a WAF’s standalone1=1-tautology signature blocks the TRUE branch and passes the FALSE one. Header insertion points are held to the higher bar (rejected outright when operands can’t be re-derived, never falling back to repeat-only confirmation), and content-negotiation request headers (Accept,Accept-Language,Accept-Encoding,Accept-Charset) — whose response legitimately varies and almost never reaches a SQL backend — are excluded from testing.nosqli-operator-injectionboolean path hardened against scheduling and inert-surface noise — three additions cut phantom differentials: (1) a benign, operator-free control value is sampled on the same interleaved cadence as the true/false probes, and if it isn’t self-consistent the endpoint’s per-request variance is correlated with the request schedule (round-robin upstreams, alternating cache HIT/MISS), so any true/false divergence is a scheduling artifact rather than a query boolean; (2) a surviving differential must reproduce with a second set of always-true/always-false constants (9==9/9==8,4==4/4==5) before it is reported, so a one-off transient (a cache miss, a momentary content variant) can’t satisfy it; and (3) URL-path-segment and header insertion points are inert-prechecked — a benign value that returns the same page as the baseline marks a cosmetic catch-all segment or a header the app never consumes, where a boolean differential is noise. Plus the shared surface gate above.default-credentialsconfirms a login before reporting it — the failed-login baseline is now drawn from two independent invalid-credential probes that must agree, proving the login surface is stable enough to trust a differential (a page with a rotating CSRF token or dynamic chrome no longer manufactures a phantom success, and a single anomalous baseline can’t poison every comparison). A candidate success must then reproduce on re-send and be materially distinct from a fresh random-invalid negative control, so plain login-page variance isn’t mistaken for authentication. WAF/CDN blocks are recognized and skipped (neither a success nor a lockout), and a success indicator that also appears in the failed-login body (e.g. “dashboard” ondashboard.example.com) is treated as page chrome rather than evidence of a login.importreads JSONL lines of any size — the importer switched frombufio.Scannerto a growingbufio.Reader, so an exportedhttp_recordcarrying a multi-megabyte request/response body on a single line no longer overflows the scanner’s fixed token cap (“token too long”) and aborts the import.- Parallel per-target stats render under
--format sqlite— the-S -T --split-by-hostparallel rollup recovered each child’s record/finding counts only from a JSONL export; it now falls back to the child’s standalone.sqliteexport (opened read-only, query-only, no WAL sidecar next to the operator’s artifact), so the per-target stats segment and the final Totals line still show when the operator requested--format sqlite,htmlwithoutjsonl.
A performance and memory-footprint release: unbounded caches across the long-lived server become bounded LRUs, SQLite WALs are kept compact, and the native-scan hot path sheds redundant allocations. Adds a standalone
--format sqlite export, stateless reads of .jsonl/.sqlite files, and Markdown rendering for findings and traffic.Added
--format sqlite— dumps the run’s standalone DB to<output>.sqliteviaVACUUM INTO; requires-S/--stateless+-o, combines with other formats, and names per-host files under--split-by-host. Aliases:sqlite3,db.finding/traffic -S/--stateless— read a--dbsource (a--format jsonlexport or standalone.sqlite) directly with project scoping off; nothing is written to your project DB.finding/traffic -m/--markdown— render matched findings/records as Markdown (request/response in fencedhttpblocks) to stdout; under-S,--compactwindows the response around the match.
Performance
- Unbounded maps → bounded LRUs across the long-lived server — the per-host run-once claims, storage-host set, OAST nonce tracker, per-host Tech/WAF/ContentClass registries, the ParameterFinding dedup registry, and the transcript-sequence counter were all process-lifetime leaks in a scan-on-receive server. Each is now a capped LRU; eviction only forgets a cached hint or run-once claim, so at worst a stale entry is re-derived/re-fired once.
- SQLite WAL kept compact — a background TRUNCATE checkpointer (every 5m) plus per-connection
wal_autocheckpoint/mmap_size/temp_storetuning and an on-open checkpoint stop the ingest DB’s WAL ballooning over a multi-day run; reader pool raised 4 → 8. - Disk-backed dedup set bounded —
DiskSetcaps its keyspace (default 1M keys, batched eviction) so a process-lifetime dedup manager can’t grow LevelDB without limit. FN-safe: an evicted fingerprint is re-processed once and re-deduped at the DB layer. - Native-scan hot path allocations cut — single-allocation insertion-point splice (was N reallocs/fuzz), baseline response composed into one pooled buffer, body hashed only when stubbed/truncated, and
--with-recordsbatch-loads linked records in one query (no N+1). - Bounded memory on big scans/audits — the re-spider phase pages candidates body-free instead of holding 5000 bodies at once; the coverage probe and code-audit query metadata-only (skip
raw_request/raw_responseBLOBs) when they need only method/URL. - Fewer doomed goroutines/writes — module dispatch fast-exits an already-cancelled context;
DBInputSourcecoalesces a run of acks into one cursor UPDATE; the disk-queue Ack deletes the record instead of rewriting it; the server sheds the heavyResult/SwarmResultblobs from the status map once persisted.
Fixed
rails-info-exposure/rails-sensitive-filesblank-body catch-all FPs — a blank200on/upor/tmp/local_secret.txt(a catch-all/CDN placeholder) slipped past the soft-404 fingerprint and was reported as a health check / leaked secret. The markerless paths now reject an empty body, and the local-secret case requires an anchored hex token.- Host rate-limiter no longer evicts an in-flight host — forced-cap eviction could pick a host with acquired-but-not-released slots, orphaning its semaphore accounting; it now evicts the least-recently-used idle entry (or briefly exceeds the soft cap if all are busy).
- Disk-queue segment data race —
tryDequeue/Ackranged overq.segments[:], aliasing the backing array mutated concurrently by segment create/cleanup; they now copy the slice under the lock. - Combined audit keeps running after client disconnect — a dropped SSE stream during a
driver=auto/bothrun left the multi-hour subprocess running against nobody; the SSE pump now cancels the run on first write error.
Changed
balancedintensity now runs theintrusivetier —balancedresolves to theintrusiveceiling instead ofheavy, so the default scan runs the full module battery (includinginternal-header-probe). The aggressiveness-tier gate now only trims modules on aquick/litepass;balancedanddeepshare the same tier ceiling (deepstill differs via its other deep-only behaviors).internal/runner/module_tiers.go.
A reachability-and-recon release: an alternate-port sweep that finds web services running off the standard ports, TLS-certificate recon that mines self-signed and private-CA certs for subdomains and internal names, and default-credential checks that try documented vendor defaults against confirmed third-party consoles. It also makes the native scan intensity-aware — modules now run up to an aggressiveness tier set by
--intensity, passive analyzers skip responses of the wrong body shape, and value-only-different GET URLs collapse to a few representative samples.Added
- Discovery mines the JS that earlier phases already collected — the discovery crawl previously only ran jstangle/linkfinder on JavaScript it fetched itself, so a framework bundle the browser spider captured (e.g. a Salesforce Aura/Lightning app bundle) sat in storage unparsed and its embedded routes were never requested. Discovery now, at init, walks the stored responses and feeds every captured JS body through the same jstangle + linkfinder extraction (reusing the already-stored body — nothing is re-fetched), so endpoints that appear only as strings inside a bundle become observed paths and extracted requests. This recovers routes that only ever exist in JS — notably a Salesforce captcha iframe (
/apex/APP_Login_NewCaptcha?source=…) whose iframe mounts only after the login form is interacted with, but whose route is a literalsrcstring in the Aura bundle the spider already grabbed; the query param is preserved so the endpoint is fetched with its reflected parameter. NewextractRoutesFromStoredJSinpkg/deparos/discovery. - Alternate-port sweep — a new
port-sweepnative phase (backed bypkg/portsweep) probes each original CLI target host for web services running off the standard ports. For every target host it TCP-connects a curated list of common alternate HTTP(S) ports (3000,5000,8000,8008,8080–8083,8088,8888,9000,8443,9443) concurrently, HTTP-confirms the open ones, and appends any confirmed web service to the target set before the ingestion/scan phases consume it, so a console on:8443or a dev server on:3000flows through the normal pipeline instead of being missed. A honeypot guard discards a host whose ports are nearly all open with near-identical responses (an all-ports-open tarpit; open/probed ratio ≥0.7). Bounded by a 512-host cap (excess hosts announced, never silently dropped) and per-host/outer-host concurrency limits. Off by default and auto-enabled at--intensity deepor under--follow-subdomains; the port list is overridable per-run with--port-sweep-portsand configurable underscanning_strategy.port_sweep(ports,concurrency,dial_timeout_ms,http_timeout_ms,honeypot_ratio). - TLS certificate recon (
tls-cert-recon) — a new active per-host module completes a single TLS handshake per host and reads the leaf certificate for recon value without sending any application traffic. Certificates from recognized public CAs and CDNs (Let’s Encrypt, DigiCert, Cloudflare, …) are ordinary and skipped; what remains — self-signed certificates and ones from a private or internal CA — is flagged (INFO/Certain) as a likely forgotten staging/admin/appliance host. From those certs it harvests Subject Alternative Names: sibling subdomains under the target’s registrable domain (widening the surface) and internal-only names such as RFC1918/loopback IPs and.local/.internal/.corphostnames (leaking internal naming and network layout). - Default-credential checks on confirmed dashboards — once
dashboard-exposureactively confirms a third-party console is present, it now submits that product’s vendor-documented default credentials against the confirmed context path and escalates a working pair to a Critical/Certain default-credentials finding. Only documented default pairs are tried (never a brute-force wordlist, so it can’t trigger account lockout), and every attempt is negative-control gated — a random credential pair must be rejected by the success matcher first, so a login form that “succeeds” on anything can’t produce a false positive. The check runs once per(host, product), draws from the shared per-host probe budget, and is driven by a new nuclei-template-deriveddashboardsig.LoginProbe(per-product login path, request body, credential pairs, and success matcher). Shipped for 8 products: Grafana, GitLab, SonarQube, RabbitMQ, MinIO, Open WebUI, Apache Superset, and Node-RED. The module gains adefault-logintag.
Changed
vigolium updateno longer prompts for confirmation — the interactive “type ‘yes’ to confirm” gate (and its-F/--forcebypass) was removed, soupdateproceeds directly to fetching the binary and nuclei templates.- Per-asset findings collapse to one finding per host — the default finding-grouping
by_moduleset (formerly justsourcemap-detect) now covers the source-analysis and per-response hygiene modules that fire once per JS bundle or page, so a single domain reports one finding apiece instead of one per asset. Added:unsafe-html-sink,dom-xss-taint,dom-xss-detect,javascript-uri-sink,insecure-token-storage,cookie-security-detect, and the framework/build/SSR audit family (build-misconfig-detect,client-auth-guard,cache-data-leak,nextjs-config-audit,nextjs-dynamic-param-audit,nuxt-config-audit,nextauth-config-audit,server-action-{auth,bind-audit,input-audit},server-only-boundary-audit,ssr-data-exposure,ssr-hydration-xss,remix-loader-exposure). Grouping stays per-host (a finding ona.example.comnever merges with one onb.example.com) and per-severity, and the merged survivor keeps every affected URL undermatched_at(capped bymax_urls, default 50). Secret-bearing modules such asenv-secret-exposureare deliberately excluded — each distinct leaked value is signal, so they remain value-grouped (identical secrets collapse, different ones stay separate). Applies to both the stored/reported findings and the live console rollup; override viaknown_issue_scan.group_by_value.by_module. --intensitynow gates modules by an aggressiveness tier — a module can declare a single tier tag in itsTags()advertising how expensive/aggressive it is (lightrecon/fingerprinting,moderatestandard active testing,heavyblind/timing probes,intrusiverare high-side-effect checks), and the resolved intensity sets a ceiling on what runs:quickruns up to moderate (drops the heavy blind/timing probes and the intrusive checks for a fast pass),balancedruns up to heavy (the default — the full active battery minus intrusive, matching legacy coverage), anddeepruns everything. A module with no tier tag (TierAlwaysOn) is ranked strictly below every tier and so is never excluded — core detectors like stored / DOM-confirm XSS run at every intensity. The gate is applied symmetrically to active and passive modules, and the deferred module IDs are logged (never silently dropped). Newpkg/modules/tier_tags.go(ModuleTierRank) plus the runner-side ceiling ininternal/runner/module_tiers.go.- Passive content-class gating — a passive module that structurally requires a particular response body shape is now skipped on a record whose response is a confirmed different structured class, so an analyzer that could produce nothing on that body isn’t run against it. Currently only the UI-redress modules opt in (
clickjacking/ui-redressrequire an HTML document), via a tiny conservative tag→class map plus an explicitContentClassAwareinterface. The gate fails open on unknown/textbodies, on content-agnostic modules, and when the tech filter is disabled (--no-tech-filter, ordeep); the per-host root-page classification from heuristics seeds the registry and is consulted only when a record’s ownContent-Typeis indeterminate. Passive-only by design — skipping a passive analyzer that can’t parse the shape it was given can’t hide a finding, whereas active modules often probe a host independently of the triggering record’s body and so are intentionally left ungated. Newpkg/modules/content_class_tags.go. - Param-shape GET coalescing in dynamic assessment — the risk-prioritized dynamic-assessment input source now collapses redundant fan-out over URLs that differ only in query values (e.g.
/search?q=1..N): GET records sharing a(host, path, query-param-name-set)shape are reduced to at most a few value-distinct representatives (default 3,dynamic_assessment.max_param_shape_samples). It walks the already-risk-prioritized snapshot so the highest-value records claim the per-shape sample slots first. Only query-param-bearing GETs coalesce — other methods, body-bearing requests, and param-less GETs are always kept, so a request whose full shape isn’t visible from the URL alone (e.g. a POST body) is never dropped. Records stay in the database; only this scan’s iteration list is pruned, and the scan cursor still advances past coalesced-away records so they aren’t re-scanned in a later feedback round.0/unset uses the built-in default of 3; a negative value disables coalescing. Newpkg/database/param_shape.go.
Fixed
- Spidering now drives the login flow it used to abandon on SPA portals — an unauthenticated visit to a heavy enterprise SPA (Salesforce Lightning, Angular portals, …) routinely bounces to a landing page whose “Log on” button kicks off an OAuth/SAML/SSO navigation chain (
… /oauth2/authorize → /idp/login → SAML → vendor login). The browser crawler captured a half-rendered shell and never entered the flow, so its entire URL set — the OAuth authorize hop, the SAML redirect, the vendor login page and its own data XHRs — was missed. Three additive priming steps fix it, mirroring the existing service-worker / iframe priming: (1) SPA bootstrap settle waits for the landing’s sequential bootstrap XHRs (config → region → i18n → feature-flags → content) to go network-idle before the state is snapshotted and clickables are extracted (bounded bySPASettleTimeout, default 12s), so the real UI — including the login CTA and the deep data calls — has actually rendered; (2) consent-overlay dismissal clicks cookie-consent “accept” controls (OneTrust et al., piercing shadow DOM) so the overlay neither blocks the render nor masks the CTA; (2b) auto-scroll drives the landing through its full height (window + the largest scrollable container, dispatching scroll events with a pause per step) so content and assets that load only as sections enter the viewport — IntersectionObserver-gated sections, infinite-scroll data fetches, deferred hero/bundled-media images — are actually requested and captured (AutoScroll, default on); (3) login-CTA priming finds the best login call-to-action and drives it once per crawl. Detection is shadow-DOM-piercing and tag-agnostic — it scores not just<a>/<button>but any genuinely clickable element (web-component apps render the real “Log on” as a<div>/custom element with a JS click handler), by visible label / auth-endpoint href / login-ish attributes, excluding logout controls and non-interactivearia-hidden/disabled loading-skeleton placeholders (a SPA shows one before the real button hydrates). It clicks the top candidates in turn — a real CDP click for true buttons, a synthetic (bubbling) click for web-component divs — until one actually changes the URL (it waits for the navigation rather than racing ahead and cancelling it), then lets the destination login page run its own XHRs (Aura/aura,/c/*.app, the captcha iframe) and returns to the landing so the normal crawl resumes. Bounded so a landing whose button never becomes interactive can’t drain the crawl budget. All steps are on by default (DismissConsent,LoginCTAPriming,SPASettleTimeout,AutoScroll). The runner surfaces adrove login CTA into the auth flowline only when a click actually enters the flow. Two further pieces make this hold up on real enterprise portals: (4) the spider now presents as a normal browser — it stripsHeadlessChromefrom the User-Agent (keeping the real Chrome version) and pinsAccept-Language: en-US(+--lang=en-US); many SPAs gate their locale routing and content loading on a real UA + language and otherwise render only a blank shell that never routes to its localized landing (e.g./us/en-us/about), so the content and login button never appear; and (5) the login-CTA drive runs before form-filling, because a cookie-consent preference form (OneTrust et al.) can carry dozens of controls that the form filler spends the per-element timeout on apiece, which would otherwise exhaust the spider budget before the login flow is ever driven. Detection also waits specifically while a login control is still hydrating (so a slow SSO-bounce landing is given time) but returns immediately on pages with no login affordance. With these, a start URL that bounces through an SSO portal to a localized content landing and back into a Salesforce-Lightning login now captures the whole chain end-to-end (portal data XHRs,oauth2/authorize,idp/login, the SAMLidp/endpoint/HttpRedirect,aura,c/*.app). (Remaining gap: a captcha iframe that a vendor login mounts only after the form is interacted with isn’t harvested by passive crawling.) - Redirect hops are now captured, not overwritten — the browser network capture reused a single in-flight entry per CDP request id, so each
3xxhop in a redirect chain was silently replaced by its target and lost. The intermediate URLs an SSO chain is made of — an OAuth/oauth2/authorize, a SAML/idp/endpoint/HttpRedirect?SAMLRequest=…that only ever appears as a redirect — therefore never reached the records table even when the browser requested them.onRequestWillBeSentnow finalizes and emits the previous hop (with its redirect-response status/headers) before recording the redirect target, so every step of a redirect/auth chain is captured. - NoSQLi false positives cut hard; both modules normalized to High/Tentative —
nosqli-error-basedflagged a minified pdf.js worker (servedapplication/javascript) as a Critical MongoDB injection: a generic operator/expression token baked into the bundle matched a Mongo pattern, and an empty captured baseline disabled the present-without-payload guard. Several confirmation layers were added: (1) static assets / code bundles are skipped outright — by both captured content-type and URL path (IsStaticAssetContentType/IsStaticAssetPath, the latter extended with.mjs/.cjs), re-checked on the live fuzzed response; (2) the generic English/JS-ish patterns ($expr,unrecognized expression,unknown operator,FailedToParse) are now “weak” and believed only when an independent database/error-context marker sits near the match (windowed corroboration), while strong driver/class signatures still fire on their own; (3) a response-shape gate requires the body to actually look like an emitted error — a 5xx status, a JSON error envelope, or a stack trace — so a driver name in a normal 200 page is dropped; and the injected payload is stripped from the body before matching so a reflected operator can’t self-satisfy a pattern. The siblingnosqli-operator-injectiongained the same static-asset gate. Confidence is dialed down across the board to reflect that NoSQLi detections are behavioral/heuristic inferences:nosqli-error-baseddrops from Critical/Certain to High/Tentative, and the operator-injection auth-bypass and boolean-diff paths drop from High/Firm to High/Tentative (its weaker size-change and time-based paths remain Suspect/Tentative). Every finding now reads as a high-impact lead to verify rather than a definitive injection. - By-module finding grouping query — the
GroupFindingsByValuededup pass built itsmodule_id IN (…)clause withbun.In(which double-wraps the placeholder list intoIN ((?, ?))) instead ofbun.List, so the by-module grouping mode (e.g.sourcemap-detectcollapse) could fail to match. Corrected tobun.List. - Laravel admin-exposure false positives on catch-all login shells —
laravel-admin-exposureflagged a Salesforce/Visualforce login page (served identically for every probed path, with a per-request ViewState token defeating the soft-404 hash/length fingerprint) as exposed Nova/Filament panels, because each probe matched on a single weak generic token (login,email,admin,data) inside an OR-list. The module now confirms structurally withmodkit.MatchAllGroups: every probe requires a framework/endpoint-specific anchor (e.g.laravel-nova,filament-panels,swagger-ui,"openapi"+"paths",__schema+queryType) plus corroboration, so a bare generic word can no longer fire. Bare-word/adminand/backofficedetection now requires a named admin package (Backpack/Voyager) rather than the word “admin”/“dashboard”. The sub-directory catch-all guard re-checks the same marker groups against a nonexistent sibling path. - Same hardening applied across the path-probe exposure family — auditing the sibling modules for the identical weak-single-OR-token pattern, the grouped-marker (
MatchAllGroups) + group-aware catch-all guard was applied to: fastapi-docs-exposure (/openapi.jsonnow requires the"openapi"/"swagger"version key, not a bare"info"/"paths"), php-composer-exposure and the Composer probes in joomla-misconfig (anchor on Composer-specific keys like"_readme"/"content-hash"/"require"instead of bare"name"/"version"), joomla-misconfigcom_ajax(requires the full{success,message,data}envelope, configuration.php backups anchor onJConfig/$secret, manifests onfiles_joomla), firebase-misconfig (every config-file probe anchors on a Firebase-specific token —.runtimeconfig.jsonno longer matches a bare{, service-account files requireprivate_key+ a second key field), java-appserver-console (drops the genericOracle/Management Console/Web Console/Administration Consolewords, anchoring on the product name), tomcat-manager-exposure (drops bareDeploy/JVM/HTTP, anchoring on the manager title + a corroborating action), and sensitive-file-discovery (.vscode/settings.jsonrequires a VS Code setting-key prefix, not{/}). Directory-listing markers (Index of/Parent Directory) and already-framework-specific modules (django-admin, php-debug, symfony profiler) were left unchanged —Index ofgenuinely indicates a real listing, and those markers don’t fire on catch-all shells. additional_evidencecapped at 5 — the per-finding evidence ceiling (request/response pairs folded in from duplicate findings, and pairs a module collects itself) dropped from 10 to 5, and the cap is now also enforced at the result-event→finding converter, so a noisy module that fires across many sibling paths of one host no longer balloons the stored payload (or thehttp_recordsrows it pulls in).
A finding-fidelity and coverage release: context-path-aware exposure probing, secret findings that carry their evidence, probe-finding collapse to keep the traffic table clean, recovery of JS bundles and service-worker-precached assets that no link points at, passive recon harvesting with opt-in subdomain follow, third-party-dashboard detection, and targeted false-positive fixes.
Added
- Context-path-aware exposure probing — new
modkitpath helpers (CandidateBasePaths/CandidateBasePathsIncludingStatic,UnclaimedBasePaths,BasePathClaimKey, plusDecoyFileBaseline/FetchPath/SiblingServesAnyMarker) walk the web root plus each observed path prefix, so a known endpoint mounted under a context path (/api/actuator/env,/myapp/...) is probed, not just the root. Adopted across ~26 exposure modules (Spring family, Swagger, FastAPI, GraphQL, Firebase, Tomcat, WordPress, Drupal/Joomla/Magento/Symfony, ASP.NET, Django/Laravel/Rails, …), replacing each module’s hand-rolled checksum/path-walk loop; fan-out is capped (MaxBasePathDepth) and per-(host, base)dedup keeps traffic bounded. - Secret findings carry their evidence — passive secret detection and the known-issue-scan Kingfisher pass now attach the originating request and the matched response (head + windowed body around the leak) to each finding, via shared
secret_detecthelpers, backed by a newHttpResponse.Head(). - Probe-finding collapse — new
modkit.CollapseFindingsfolds many findings that confirm one issue into a single finding (onehttp_recordsrow), the rest riding along as capped inlineAdditionalEvidence.input-behavior-probenow collapses its per-endpoint header/path/char/debug probes this way (ranking a403→200bypass above a tag-only change) instead of writing one row per probe. - SPA-gated JS-bundle name sweep — on monolith / server-rendered apps, discovery now probes a curated list of common, never-content-hashed base names (
main,app,config,settings,admin,api,auth, …), each tried as both.js(bundles) and.json(sibling config/data), across the web root, the start directory, and the app’s observed JS directories. These files frequently aren’t linked from the HTML, so they were never harvested; confirmed hits — validated against the soft-404 baseline with a per-directory wildcard/catch-all guard — flow into the JS-fetch pipeline (jstangle + link extraction, recorded ashttp_records) so their endpoints and secrets reach later phases. The sweep is skipped when the start page fingerprints as a JS-shell SPA (Next.js/React/Angular/Vue/Svelte): those bundles are content-hashed (main.a1b2c3.js) and so unguessable, and the real ones are already linked and harvested. Bounded by hard request caps and default-on, configurable underdiscovery.extensions.js_bundle_sweep/js_bundle_names. - SPA / PWA asset-manifest recovery — modern frameworks code-split the app into hashed JS chunks (
100.<hash>.js) whose names are assembled at runtime from a webpack chunk-id→hash map and appear nowhere as literal strings, so neither link extraction nor a short headless visit finds them — a real browser only fetches them because the framework, or a service worker pre-caching the build, reads a manifest the page never links. Both ends of the pipeline now close that gap. In discovery, the engine derives the well-known service-worker and manifest URLs (/ngsw.json,/ngsw-worker.js,/sw.js,/manifest.webmanifest, …), fetches them through the JS-fetch pipeline, and parses each framework’s asset manifest so every cached chunk is fetched, recorded, and scanned — Angularngsw.json, React CRAasset-manifest.json, Nuxt build manifests, and the generic Workbox precache list embedded in a PWA service worker (mirroring the existing Next.js route-manifest harvest, but for asset manifests). In the browser spider, a bounded in-page priming step does what the worker would — locates the registered worker and manifest, reads the full asset list, and fetches each same-origin asset so the browser’s network capture records them as spidering traffic. Both sides are same-origin-only and capped (the crawler atServiceWorkerMaxAssets, default 600); priming is on by default (ServiceWorkerPriming). - Passive recon harvesting (subdomains + third-party backends) — two new INFO-severity passive modules, backed by a shared
pkg/reconsigsignature package, read HTML/JS/JSON response bodies for recon value without sending traffic.subdomain-harvestcollects every hostname sharing the page’s registrable domain (eTLD+1) — sibling origins, API hosts, and environment URLs that SPAs and minified bundles routinely embed — and flags ones whose name suggests a non-production environment (dev,staging,test,qa).baas-endpoint-fingerprintmatches a 22-provider catalog of backend-as-a-service / identity / serverless / managed-data backends (Okta, Auth0, Cognito, Supabase, Convex, Lambda function URLs, Cloud Run, Hasura, Algolia, Contentful, Sentry, …), recording the provider, category, and tenant/instance identifier; Firebase and object-storage references are deliberately left to their dedicated modules. --follow-subdomainsscope expansion — opt-in flag that letssubdomain-harvestfeed the in-scope subdomains it discovers back into the running scan, so a sibling host referenced only in a JS bundle gets crawled and scanned instead of being noted and dropped. It adds exact discovered hosts to a new runtime allow-set (ScopeMatcher.AllowHost, surfaced to modules viamodkit.ScopeExpander) rather than wildcarding the apex, so the scope stays tight —app.example.comreferencingstaging.example.compulls in that one host, not all of*.example.com. Off by default and automatically enabled at--intensity deep.- Third-party dashboard / console detection — a new module pair, backed by a shared
infra/dashboardsigcatalog of ~60 off-the-shelf products across 10 categories (observability, data, CI/CD, orchestration, automation, messaging, collaboration, infra, analytics, AI — Grafana, Kibana, Elasticsearch, Airflow, GitLab, Jenkins, HashiCorp Vault, OpenAI-compatible LLM APIs, database consoles, …), inventories and probes self-hosted admin surfaces. Passivedashboard-fingerprint(INFO) recognises a product from a catalogued response signature — a unique header, body markers, or a distinctive cookie plus a name reference — recording which off-the-shelf software is reachable so product-specific checks and CVE matching can follow. Activedashboard-exposureconfirms the product against its own health/version/config endpoints and grades the result in tiers: a reachable console is reported as attack surface (Medium), while an endpoint that discloses internal data — version, full config, model list, cluster status — without authentication escalates to a High info leak (the leaked value, e.g. the version, captured as evidence). Probing is intensity-scaled: at normal intensity only each product’s single best (Primary) endpoint is probed, and--intensity deep(plus passive tech hints) unlocks the full per-product confirmer set.
Changed
- Quieter stateless headers —
-S/--statelessscans no longer print the Scan ID or Project UUID; both live in a throwaway database and can’t be queried later, so they were just noise. --skipaccepts phase aliases — e.g.--skip discovery,spidering,kis; usage examples updated to show it.--only discoverysyncs the discovery toggle — keepsDiscoverEnabledin step with the skip (mirroring the--onlypath) so the config panel and downstream gates don’t still report discovery as active.- Tighter extension-confirmation source gating — the “observed” confirmation that gates server-side extension fuzzing now counts only URLs the application genuinely references (start URL, links from HTML attributes / meta-refresh / HTTP headers / robots.txt, sitemap entries, files actually fetched and confirmed). Path-like strings scavenged from JS/HTML body text no longer count — they aren’t proof the server serves that extension — and on a JS-shell SPA the observed source is suppressed entirely, since the index shell answers every path. Cuts spurious extension sweeps triggered by body-text noise.
- Per-module finding grouping (sourcemap noise collapse) — finding grouping gained a
by_modulemode: for the modules it lists, findings collapse to a single finding per(module, severity[, host])regardless of the per-URL extracted value, folding every URL intoMatchedAt. This handles modules that fire once per asset where the value is noise rather than a shared identity —sourcemap-detect, which reports a distinct.mapfilename for every JS/CSS bundle, so a site with dozens of bundles previously produced dozens of near-identical findings. Default-on forsourcemap-detect(per-host); severity stays in the key so a Low “sourcemap advertised” never merges with a High “full source exposed”. Applies both to the stored findings (theGroupFindingsByValuededup pass) and the live console output, configurable underknown_issue_scan.group_by_value.by_module. - Proxy path-normalization bypass probing on exposure modules — a shared
modkitpath-bypass helper appends/..;/-family prefixes (the Servlet matrix-parameter..;/, encoded..%3b//..%2f/ double-encoded..%252f, chained up toMaxPathBypassClimbdeep) ahead of a blocked endpoint so a request a fronting proxy refuses by literal prefix is forwarded and re-reaches the endpoint once the Java/Servlet backend strips the path parameter and collapses the climb. Wired into several path-gated modules (spring-actuator-misconfig,tomcat-manager-exposure,forbidden-bypass,xss-light) and gated on a proxy-block status (401/403/405) so it only runs where there’s actually a block to bypass. - Header-less Rails recognition for the Active Storage / Action Mailbox probes — a new
infra/railssigbody-signal set (CSRF/Turbo/Action Cable meta tags, the default Rails 404/500/exception pages,ActionController::references) lets these probes confirm a Ruby on Rails app even when it stripsX-Runtime/Serverheaders, with the probe path echo-stripped first so a reflectedActiveStorage/ActionMailboxtarget can’t self-match. - Decoded preview on base64 findings —
base64-data-detectnow decodes each matched blob (handling both standard and URL-safe alphabets, so JWTs decode cleanly) and, when it decodes to displayable text, attaches a truncated(decoded)preview line to the finding evidence and description, so the leak is readable without a manual round-trip. - Seed-parent directory fuzzing — the standard discovery profile now fuzzes each ancestor directory of the seed URL (
/api/v1/auth/x→/,/api/,/api/v1/,/api/v1/auth/) with the short file+dir lists and observed words (discovery.expand_seed_parents), whileenrich_targets: falsekeeps scope to the seed’s own path structure plus live crawl breadcrumbs instead of re-seeding from paths earlier phases wrote to the database. - Rebuilt the static HTML report bundle.
Fixed
input-behavior-probeempty-body / redirect false positives — a tag-structure delta now counts only when both the baseline and fuzzed responses serve 2xx content and the fuzz body carries real HTML (≥3 tags), so a 404/302 empty-body or non-HTML response no longer registers a bogus maximal tag distance; the reproduce round applies the same guard.- Reflected/redirect secrets downgraded — a secret that appears only on a 3xx redirect or reflected into a response header value (e.g. an OAuth identifier echoed into a
LocationURL on the way to SSO) now drops to Low instead of High/Critical. sensitive-file-discoverydecoy baseline + unified confirmation — adds a per-candidate decoy-file baseline (same directory and extension) to catch extension-scoped catch-alls, probes sensitive files under every candidate base path including static/CDN directories where a misconfigured front serves an exposed.env/.git/backup, and routes all confirmation rounds through oneconfirmspredicate (.envgated on a realKEY=VALUEline so prose mentioning “SECRET” can’t forge a Critical).- IDOR/BOLA neighbor-id false positives on public content —
idor-detectionandidor-guidnow require the original request to carry a credential (Authorization, Cookie, a well-known API-token header, or a credential query parameter) before a “neighbor id returns different-but-valid content” differential is reported as a High/Firm authorization bypass, via a new sharedauthzutilhelper. With no credential there is no per-user boundary to cross, so the same differential on public, navigable content —GET /blog/3/vs/blog/4/, a docs site, a product catalog — is downgraded to a Medium/Tentative lead rather than dropped; a baseline page that itself links the neighbor id is treated as public and likewise discounted. nextjs-image-ssrfconfirmation hardening — the Next.js image-optimizer SSRF check now confirms a reached internal target through the sharedinfraSSRF internal-target set and the stricterFreshMetadataMarkers/ConfirmFreshMetadatadefenses, so a generic token the app itself echoes (e.g.compute) can no longer confirm on its own — only a self-evidencing metadata marker that’s present in the optimizer response and absent from the baseline does.
A coverage-expansion and finding-quality release: a targeted re-spider phase that browser-crawls the SPA routes discovery finds after spidering already ran, Next.js route-manifest harvesting (Pages Router, SSG, and App Router route recovery), a ground-up rewrite of every module’s finding description plus classification tags on native findings, and a consolidated
traffic --replay that can drive a real browser through Burp.Added
- Targeted re-spider of discovery-found SPA routes — a new
targeted-respidernative phase (after discovery dedup, before dynamic assessment) closes a structural gap in the pipeline: the one-shot browser spider runs before discovery, so a rich route that discovery surfaces later —/ui/,/console/, an admin panel found in a JS bundle — never got its client-rendered pages crawled, and its XHR/fetch API calls never entered the scan. The phase re-reads discovery’s already-stored response bodies straight from the database (zero extra HTTP for candidate selection), keeps only pages that look client-rendered/SPA or meaningfully interactive, screens out login/SSO walls so the browser budget isn’t burned on auth bounces, and dedups candidates per (host, app-shell) so one crawl covers an entire SPA router instead of re-crawling the same shell per route. Survivors get short, budgeted browser crawls whose records land inhttp_recordsunder a distincttargeted-respidersource tag and flow into dynamic assessment like any other traffic. Default on with deliberately tight caps, all configurable underdiscovery.respider.*: max 3 seeds per host / 10 total, 45s + 25 states + depth 3 per seed, and a 5-minute wall-clock cap on the whole step; the phase only rides along when spidering, ingestion, and an assessment phase are all enabled, so headless/stateless modes are unaffected. - Next.js route-manifest harvesting — discovery now recognizes Next.js pages, derives the
buildId(from a referenced manifest URL in the markup, falling back to the__NEXT_DATA__JSON blob), and fetches two manifests that enumerate the app’s routes but are typically absent from the rendered HTML:_buildManifest.js— the full Pages Router route table, including dynamic routes like/blog/[slug]— and_ssgManifest.js— the concrete pre-rendered paths, which the client router loads at runtime and which fill in real values for those dynamic segments. Every harvested route flows through link extraction into observed paths (with trusted-source priority) andhttp_records, so dynamic assessment scans pages no link on the site points at. App Router routes, which appear in neither manifest, are recovered separately by decoding page/route-handler chunk paths (app/<segments>/page-<hash>.js) back into their addressable routes. - Shared login/SSO URL signatures (
pkg/spitolas/loginsig) — the spider’s login-detection tables (auth-fronting host prefixes likesso./adfs., known IdP hosts like Okta/Auth0/login.microsoftonline.com, and OAuth/SAML path markers) moved out of the crawler internals into a reusable package, so the re-spider candidate screen and any future phase share one source of truth instead of growing divergent copies.
Changed
- Finding descriptions and tags overhauled across all 263 modules — every module’s description was rewritten from internal implementation notes (detection-logic walkthroughs, CLI flag requirements, reference-link lists) into a concise, operator-facing What it means / How it’s exploited / Fix block: what the finding actually establishes, how an attacker leverages it, and the concrete remediation. Net effect on the tree: ~4,100 lines of boilerplate replaced by ~2,300 lines of substance. The executor now composes the stored description instead of letting one source overwrite the other — the module’s per-finding context line (the specific header/param/endpoint it flagged) leads, followed by the static explanation block — so reports keep both the instance specifics and the explanation. Module classification tags (
xss,spring,injection,light, …) now also propagate onto every native finding, which previously carried no tags at all (only known-issue-scan findings did); tags are copied, not aliased, so editing one finding’s tags can’t corrupt its siblings. This is the new convention for all future modules. traffic replayis nowtraffic --replay— the standalonevigolium traffic replaysubcommand was removed and folded into thetrafficcommand as a--replaymode, so the full filter surface (--host,--method,--status, fuzzy search, header/body search, …) applies identically to listing and replaying. New knobs:-c/--concurrency(default 10) replays the matched records concurrently — and lets you throttle down to avoid overwhelming an intercepting proxy like Burp;--with-browserreplays each URL through a real browser routed via--proxy, so the proxy captures genuine browser traffic — real TLS fingerprint, JS execution, subresource and XHR loads — with the storedAuthorization/Cookie/User-Agentheaders forwarded so the navigation runs under the captured session, redirects/page titles/fired JS dialogs reported per record, and Chrome’s implicit proxy bypass for loopback targets removed so evenlocalhosttraffic reaches the proxy (non-GET records are noted as replayed as GET navigations, since that’s what a browser can do); and-a/--alllifts the-n/--limitcap (default 100) so--replay --allre-sends every stored record.--in-replaceand--timeoutcarry over, and concurrent replays buffer their output so each original-vs-replay comparison block prints atomically rather than interleaved.- Live finding lines match the console format — the stderr finding echo used when results are deferred to files (
--format jsonl,html) previously had its own ad-hoc layout; it now renders the exact[type] [module] [severity] METHOD URL [value]shape of the console result writer (including the type/phase de-duplication and the grouped extracted-value suffix), so a finding reads identically whether it streams to the terminal via stdout or the stderr echo.
Fixed
bfla-detectionno longer flags empty-200 endpoints — an endpoint whose “privileged” baseline is an empty (or whitespace-only) body carries no privileged content to compare, so every sub-test degenerated into matching nothing against nothing: fronting CDNs, XSRF/login bounces, and JSP.jspaaction handlers that answer any request with an empty 200 were reported as unauthenticated-access or method-switch bypasses. The real-world trigger was an Atlassian/secure/ConfigureReport.jspareturningContent-Length: 0for both GET and POST — which also defeated the existing random-path method baseline, since the random path 404s while the action handler empty-200s, making the responses look “different enough” to pass the guard. The module now bails up front when the baseline body is empty and skips switched-method candidates whose bodies are empty, on the principle that an empty body is no-signal for both baseline and candidate. Covered by new regression tests alongside the existing positive cases.
A detection-expansion and noise-reduction release: object-storage traversal and cloud-URL harvesting, value-based finding grouping that collapses one leaked secret seen on many URLs into a single finding, tech-fingerprint-gated extension fuzzing, a faster native-scan hot path, and continued module false-positive hardening.
Added
- Value-based finding grouping — a secret that leaks on N different URLs now collapses to a single finding (and a single console line) keyed on the extracted value, instead of one finding per URL. Applied both live (the console
findingGroupersuppresses repeats to file-only and prints a grouped summary with per-host rollups) and in the database dedup pass (GroupFindingsByValue, folded intodeduplicateFindings, covering known-issue-scan secret templates and passive secret detection). Controlled by a newknown_issue_scan.group_by_valueconfig (Enabled/PerHost/Tags/MaxURLs; per-host grouping on by default), with an identical non-empty value required as the guardrail. - Tech-fingerprint-gated extension fuzzing — discovery now wordlist-fuzzes server-side extensions (
.php,.aspx,.jsp,.action,.cgi,.jspx,.ashx,.asmx, …) only after confirming the app actually serves them — via an observed URL, a technology-fingerprint match, or an active soft-404 probe — instead of always sweeping every extension. Newdeparos/configtech-signature table plus anextension_confirmpipeline; the previously always-on custom-list sweep is now gated behind a default-on confirmation step (with a consoleext-fuzznotice when it engages). - Object-storage traversal & cloud-URL harvesting — a new active
cdn-object-traversal-listingmodule appends..;-family trailing payloads to object-storage fetches to turn a single-object read into a full bucket listing, and a new passivecloud_storage_url_harvestmodule collects S3/GCS/Azure Blob/OSS/TOS storage URLs from response bodies. Both are backed by a newstoragesigpackage that fingerprints object-storage backends and listing responses behaviorally (storage response headers,/obj/<bucket>/<object>path shape, listing-XML bodies). Object-storage assets are now carved out of the static-file filter across the executor, CLI/JS/server ingest, and proxy paths and recorded metadata-only via HEAD/ranged-GET (newWorkItem.StaticMeta) instead of being dropped, so these modules can see them. - Concurrent triage batches —
TriageLoopConfig.BatchConcurrencyruns disjoint triage batches concurrently within a round (default 3, mirroring master-agent batch concurrency), wired throughSwarmRunner.runTriageLoop.
Changed
- Module false-positive hardening — expanded FP defenses and test coverage across
nosqli-*,sqli-time-blind,path-normalization,xml-saml-security,bfla-detection,proxy-header-trust, and other active/passive modules. - Trusted identity-header spoofing in
forbidden-bypass— the 403-bypass module now also replays the request with spoofed trusted identity/routing headers, catching gateways that authorize on a forwarded identity header an external client can set. - Faster native-scan hot path — three independent optimizations to the deterministic pipeline:
ParameterInsertionPointcaches theContent-Lengthbyte offsets at construction so body/form/XML fuzzing patches the length in place instead of re-parsing every header per payload (~4.4× faster, 15→6 allocs/op); the per-host rate limiter’sAcquireis now lock-free in steady state (drops the per-Acquireshard write-lock +heap.Fix), which matters most when a scan hammers a single host; and module workers enqueue findings through a new batchedFindingWriter/SaveFindingsBatchinstead of blocking on a synchronous per-finding save (coalesced into one transaction, retried per-finding, flushed on full/close so nothing is dropped — the linked HTTP record is still persisted synchronously first). - Autopilot prep overlapped with the background audit — auth preparation and pre-flight discovery now run concurrently with the background vigolium-audit pass and are joined before the frozen context bundle is assembled, taking their latency off the critical path.
- Richer report output & audit-platform refactor — enriched HTML/console report output, plus an internal refactor of the embedded vigolium-audit harness (shared CLI-process adapter, retry/cost engine helpers, content loader).
Fixed
--external-harvestnow works on demand — onvigolium scanthe scanning-strategy baseline unconditionally overwrote the external-harvest phase toggle, so passing--external-harvestunder the defaultbalancedstrategy had no effect. The flag is now a true per-phase override: when explicitly set it wins over the strategy (e.g.--intensity balanced --external-harvestenables harvesting while keeping balanced depth), and when unset the strategy’s value still applies.
A detection-expansion and false-positive-reduction release: two new modules (clickjacking detection and internal-header fuzzing), out-of-band findings that carry the request that triggered them, a stateless audit mode with an auto HTML report, and catch-all/reflection hardening across several modules.
Added
- Clickjacking (UI-redress) passive module — a new
clickjacking-detectmodule that flags a page only when it is both framable and worth hijacking, instead of every missingX-Frame-Options. It computes the framing verdict like a browser (enforced CSPframe-ancestorsoverridesX-Frame-Options; report-only,ALLOW-FROM, invalid/conflicting headers, and wildcard sources are treated as ineffective), requires sensitive/interactive content (credential form, authenticated session, or state-changing form), and downgrades when aSameSite=Strict/Laxsession cookie would make the cross-site frame unauthenticated. - Internal header probe (active module) — a new
internal-header-probemodule that mines a CORS preflight’sAccess-Control-Allow-Headers/Access-Control-Expose-Headersfor gateway-injected custom headers (identity, routing, trust, feature-flag), re-sends the request with each set to a battery of probe values, and reports those whose response body reproducibly changes — reflection stripped, measured against a per-endpoint noise floor, with a per-host circuit breaker. Adds an OAST spray per header for blind SSRF. Severity Suspect/Tentative: a body change proves the backend reads the header, not that it is exploitable. - Out-of-band findings carry their originating request — an OAST callback finding now embeds the request/response that planted the payload, the callback URL, the raw collaborator callback as evidence, and trace anchors, so it answers “which request caused this callback?” on its own. A new
GetRecordByRequestHashlookup recovers the origin even for late callbacks, and the finding is saved even when the record can’t be resolved. - Stateless audit with auto HTML report —
-S/--statelessonvigolium agent auditruns the whole audit into a throwaway temp DB (main DB untouched, mirroringvigolium scan -S) and auto-renders a self-contained HTML report via thevigolium import --format htmlgenerator. Defaults tovigolium-result/vigolium-audit-report.html;-o/--outputoverrides it (supportsgs://and{ts}). vigolium audittop-level alias — a shortcut forvigolium agent auditwith the same flags (mirroringvigolium olium).
Changed
- Audit
--keep-rawnow on by default (CLI) — retains the<source>/vigolium-results/copy for review/re-import; new--clean-rawremoves it after the run. Mutually exclusive, audit-leg only. - Catch-all false-positive hardening —
bfla-detectionandforbidden-bypassnow drop findings when a random unprivileged path returns the same response, catching empty-200 / reflected-shell edge gateways the existing wildcard guards missed. - ASP.NET false-positive hardening —
crossdomain.xml/clientaccesspolicy.xmlare flagged only when actually wildcard-permissive (not merely present), and the OIDC discovery document (/.well-known/openid-configuration) is no longer double-reported; all downgraded Medium→Low (these are Flash/Silverlight/OIDC standards, not ASP.NET-specific). - Severity recalibration — PDF-generation-injection’s plain HTML-marker reflection drops High/Firm→Medium/Tentative (the JS/SSRF/file-read variants stay High/Firm), and
web-cache-poisoningdrops Firm→Tentative.
Fixed
- Console output no longer clipped to a file or pipe — width-based truncation now applies only to an interactive TTY (new
terminal.IsTerminal()). Redirected output — including the-P/--parallelper-target.console.log— previously clipped URLs and payloads mid-token; file/pipe consumers now get the full, greppable line.
A false-positive-reduction release closing the “404 catch-all / SPA shell” and “reflected-but-not-executed” classes across the error-based injection and reflection modules: a shared error-surface status gate, structural (not bare-token) signatures, and a headless-browser confirmation tier for discovered-parameter XSS.
Added
infra.IsErrorSurfaceStatus— a shared status gate, companion toIsBlockedResponse. A genuine server-side leak (DBMS/driver error, reflected file contents, a stack trace) rides a 5xx or a 2xx/4xx that echoes the payload; a404means the route never resolved (no handler ran) and a3xxcarries no handler output, so a signature substring in either body is page noise — a catch-all/SPA 404 shell or a redirect interstitial. Body-matching modules whose finding is not itself a status signal now reject a response failing either gate before matching.
Changed
- Error-based injection signatures hardened against catch-all 404 shells —
sqli-error-based,nosqli-error-based,xxe-generic, andws-injectionno longer match DBMS/driver/error tokens on a404/3xxbody (via the newIsErrorSurfaceStatusgate), and their bare tokens are tightened to structural forms: the CockroachDB name is word-boundary-anchored (was firing onuserHasCockroachDBEnabledin a Salesforce community 404 shell’s feature-flag list), the MongoDB/BSONpatterns now require genuine driver/error contexts instead of the bare 4–6-char token, XXE confirms/etc/passwdby the fullroot:…:0:0:line shape (not aroot:substring that a--dxp-g-root:CSS var carries), andws-injectionrequires a pattern to be absent from the baseline, match the body only (not headers), and — for the{{7*7}}/${7*7}template probes — proves evaluation by requiring the literal payload to be gone from the response. nosqli-error-basedre-confirmation — a matched DBMS error must now reproduce when the payload is re-sent (a per-request random token that coincidentally matched won’t recur) and be absent from a fresh control fetch of the original value, withNoClusteringforcing a real origin round-trip so the request-clustering cache can’t replay the captured hit. Fails open on transport errors.lfi-genericreflection guards and file-shape confirmation — thephp://filter/convert.base64-encoderead now discards base64 runs that are simply our owndata://payload being reflected back (Salesforce-Aura-class echo endpoints) or that decode to carry our injection marker, the/etc/passwdrule requires the fullroot:…:0:0:…line shape (not the former greedyroot:.*:0:0:),win.iniis confirmed by ≥2 distinct bracketed section headers (was the bare English wordsfonts/extensions), and.env/.htaccessare confirmed by ≥2 distinct file-shaped lines — sensitiveKEY=VALUEassignments or recognised Apache directives — which both strengthens the evidence and broadens detection beyond the former rigidDB_PASSWORD+APP_KEY+APP_SECRETtriple that real Laravel/Symfony files rarely carry.xss-lightdiscovered-parameter confirmation — a reflected character-transform hit is now re-sent as a real, context-shaped executable payload and graded in tiers: dropped when the breakout signature never survives unescaped in the body (the reflection-only false positive this gate exists to suppress), reported Low/Tentative when it survives but no dialog fires (CSP-locked or non-executing context), and only raised to High/Certain once a headless browser actually popsalert(marker). Browser probes are globally rate-limited and injectable so tests never spawn a real browser.
Fixed
- Spidering no longer crashes the whole scan on a cross-origin iframe — go-rod’s lazy
getJSCtxID()dereferences a nilContentDocumentfor cross-origin/detached frames, and the existingframeAccessible()pre-filter could not close the TOCTOU gap where a same-origin iframe navigates cross-origin (or detaches) between frame enumeration and the element query during extraction — common on SPAs. The nil-pointer panic propagated to the top-levelRunNativeScanrecover and aborted the entire scan (later phases skipped, zero findings). The browser wrappers that reachgetJSCtxID(Element/ElementX/Elements/ElementsX/EvalWithArgs/HTML/HasElement[X], plus the recursive frame-HTML builders) now convert such panics into ordinary errors so the bad frame is skipped, with a per-framerecover()backstop in the candidate-element extractor as a second line of defense.
A false-positive-reduction release hardening the endpoint- and file-exposure modules against generic markers and reflected error pages.
Changed
- Rails Active Storage / Action Mailbox probe — confirms OPTIONS endpoints only on a 2xx
Allow: POSTheader (no longer body-matching"Allow"/"POST", which forged findings from nginx405 Not Allowedpages), and rejects blanket-OPTIONS hosts, CORS preflights, and WAF/rate-limit pages. - Marker hardening —
.envfiles now require a realKEY=VALUEline (was bare"="), Magentodeployed_version.txta valid version token (was"."), and ASP.NET health checks an actual health-state word (was generic JSON keys).
A false-positive-reduction and severity-recalibration release: per-template severity overrides for known-issue scan, decode-confirmed LFI and marker-confirmed WordPress detection, right-sized passive DOM-XSS severities, and a fix for response bodies being dropped from stored known-issue findings.
Added
- Known-issue-scan severity overrides — a new
known_issue_scan.severity_overridesmap remaps a finding’s recorded severity by nuclei template ID (case-insensitive), applied after a match but before output/persistence so the stored finding, console output, and severity counts all agree. Lets you right-size noisy or context-dependent templates without forking the upstream template (which reverts onnuclei -update-templates). Ships a defaultconfig-json-exposure-fuzz: medium— an exposedconfig.jsonoften carries only public base URLs / feature flags rather than always-critical secrets; set an entry back to the template’s own severity to undo a remap.
Changed
- WordPress module false-positive hardening —
wp-ajax-exposurenow requires plugin/action-specific markers (AND-of-OR groups viamodkit.MatchAllGroups) in the response body and rejects generic CDN/WAF/SPA error pages, so an unrelated “load-failed … Refresh” page is no longer mislabelled as a critical export vulnerability.cms-installer-exposurenow requires the CMS-name anchor and installer-specific context to co-occur (instead of any single generic word like “language” or “database”) and adds a soft-404 / SPA-shell gate (ConfirmNotSoft404) to reject wildcard catch-all hosts. - LFI base64-read confirmation — the
php://filter/convert.base64-encoderead is now confirmed by actually decoding the returned base64 and requiring real PHP source (a PHP open tag, not already present in the baseline), replacing a bare^[A-Za-z0-9+/=]{50,}charset regex that fired on incidental base64 (data-URI images/fonts) embedded in ordinary CDN/static 404 pages. LFI matches are additionally gated to 2xx/3xx responses, so a 4xx/5xx error/404 body is never mistaken for leaked file content. - Passive DOM-XSS severity recalibration —
dom-xss-detect,dom-xss-taint, andunsafe-html-sink(and each of its per-sink patterns) are lowered from Medium to Low, reflecting that these are static source/sink indicators without runtime confirmation.
Fixed
- Known-issue-scan findings lost their response body —
formatJSONzeroedResponsein place on the shared*ResultEventbeforeSaveFinding/SaveRecordran, wiping the response body from the stored finding and its HTTP record. It now serializes a shallow copy withResponsecleared, leaving the persisted finding and record intact.
A combined detection and agentic-scan reliability release: routing-based SSRF detection from PortSwigger’s “Cracking the lens” research, OpenRouter provider routing for the olium agent, resilient agent streaming with run cancellation and graceful shutdown, plus continued Spring/false-positive hardening.
Added
- Routing-based SSRF detection (“Cracking the lens”) — two new active modules.
routing-ssrfwrites an attacker-chosen host on the request line (absolute-URIhttp://internal/,@-userinfo, protocol-relative//host/) while still connecting to the victim host, confirming via an out-of-band OAST callback or a self-evidencing internal/metadata marker that reproduces, is absent from a baseline, and is absent for a benign decoy.upgrade-routing-ssrfdetects internal/metadata endpoints reachable only when a WebSocket-upgrade handshake bypasses a proxy URL filter, confirmed by a with-vs-without differential. Backed by a newhttp.Options.RawRequestTargetrequest-line primitive (rawhttp, Host/target mismatch preserved) and sharedinfrapayload ladders. - “Collaborator Everywhere” OAST headers —
oast-probeexpands its blind-callback header fan-out (True-Client-IP,CF-Connecting-IP,Forwarded,X-WAP-Profile, …) with per-header value shaping and aCache-Control: no-transformhint, surfacing SSRF in routing/analytics headers that backends behind a reverse proxy commonly fetch. - OpenRouter provider routing — a typed
agent.olium.custom_provider.provider_routingknob (provider order, fallbacks, data-collection, quantization, …) plus a genericextra_bodyJSON passthrough merged into every openai-compatible request, for steering OpenRouter and other OpenAI-compatible backends. - Agent run cancellation —
POST /api/agent/scans/:uuid/cancelaborts an in-flight autopilot/swarm/query/audit run (recorded ascancelled), wired to a Stop control in the workbench UI. - Agent config hot-reload — the
agentconfig section now reloads at runtime, sovigolium config set agent.olium.*(provider/model/credentials) takes effect on the next run without a server restart; the CLI echoes a reload line. - Invalid-date mutation payloads — the mutation engine now emits explicit, labeled invalid-date boundary values (impossible day-of-month, out-of-range month 13) for date parameters to probe lenient date parsers/validators, deterministically and reproducibly.
Changed
- Spring module false-positive hardening —
spring-actuator-misconfignow confirms each endpoint by its actuator-specific JSON structure ("status":"UP"for/health, thepropertySourcesenvelope for/env, dotted Micrometer metric ids for/metrics, …) instead of a generic word match, and the seven sibling exposure modules (boot-admin, cloud-config, data-rest, debug, gateway, h2-console, jolokia) now require co-occurring marker groups rather than any single weak token. All eight probe a guaranteed-nonexistent sibling under the same directory to reject catch-all handlers (e.g. Keycloak i18n message bundles, SPA fallbacks) that 200 every child path. New sharedmodkitprimitives back this:MatchAllGroups(AND-of-OR groups) andSiblingPathCatchAll, folded into a singleMatchAndConfirmSiblinghelper. - Parallel-scan interrupts — Ctrl-C during a
-P/--parallelbatch now treats un-started and cut-short targets as “not scanned” rather than failures: a muted per-target line, noERRORlog spam, aParallel scan interrupted · N not scannedroll-up, and an exit status that distinguishes an operator stop from a genuine all-failed batch.
Fixed
- Streaming agent disconnects — a streaming (
stream: true) agent run no longer freezesruntime.logor hangs inrunningwhen the SSE client disconnects (e.g. a connection dropped during a long model “thinking” pause). Writes now drain the agent pipe to EOF so logging and DB finalization always complete, a vanished client cancels the run instead of burning its full budget, and a client disconnect or server shutdown is recorded ascancelledrather thanfailed. Concurrent SSE producers (swarm phase callbacks) are serialized through a single sink to stop interleaved/half-written events. - Graceful server shutdown — shutdown now cancels in-flight agent runs first so live SSE streams release their connections, honors the caller’s deadline (
ShutdownWithContext) instead of waiting indefinitely for connections to go idle, and a second Ctrl+C (or a hard deadline) force-quits a hung shutdown. The config watcher also watches the actual--configfile rather than only the default path.
An agentic-scan and traffic-capture release: attack-vector skills wired into autopilot and swarm with planner-driven selection, an HTTPS-intercepting ingest proxy, sturdier olium stream recovery, a static-root traversal file-read oracle, and cleaner parallel-scan output.
Added
- Attack-vector skills for agentic scan — confirmation/escalation playbooks the agent loads to confirm and escalate findings (new
xss-browser-confirm, upgradedidor-blast-radius). Built-in skills are materialized to~/.vigolium/skills(overrideVIGOLIUM_SKILLS_DIR) onvigolium initas editable copies, which the loader prefers over the embedded fallback. - Planner-driven skill selection — the swarm planner picks skills matching the target’s attack surface (
RECOMMENDED_SKILLS) and the triage phase loads just that subset; autopilot runs an equivalent best-effort pre-flight pick before its run.--skill,--skill-tag, and--no-skill-filter(onagent autopilot/agent swarm) override the selection per run, andagent.olium.always_on_skills(defaulttriage-finding,write-jsext) pins general-purpose playbooks that stay available regardless of filtering. - HTTPS-intercepting ingest proxy —
vigolium server --ingest-proxy-port N --proxy-mitmterminates TLS with a generated CA so HTTPS traffic is recorded (and scanned with-S); trust the CA printed at startup or write it out with--export-ca <path>, and use--proxy-insecureto skip upstream TLS verification. Plain HTTP and un-intercepted CONNECT tunnels still pass through untouched.
Changed
- Active module improvements — additional detection coverage and false-positive hardening across the active modules (including a new static-root file-read oracle in
path-normalization). - Sturdier olium stream recovery — transient upstream stream failures (HTTP/2
INTERNAL_ERROR/GOAWAY/idle reset and content-less codexerrorframes) now retry even mid-tool-call instead of tearing down the run, recovering exactly the in-flight-tool-call blips that previously killed autopilot/swarm runs. - Parallel scan output — each
-P/--parallelchild now writes a self-contained<output>.console.logthat captures the live finding stream (even with deferred JSONL and noconsoleformat) and drops the repetitive[status]progress ticker, so per-target logs read like a normal console scan.
A false-positive-reduction and observability release: broad confirmation-hardening across the active and passive modules, full secret disclosure in findings, and Pi-compatible JSONL transcripts for every olium agent mode.
Added
- Olium session transcripts — every olium run (
olium/olTUI,-pheadless,agent autopilot, andagent swarm/queryper phase) writes a Pi-compatible JSONL conversation transcript to its session dir for debugging. - Shared confirmation primitives — static-asset Content-Type gate, WAF/CDN challenge-page detection (catching 200/202 Cloudflare/Incapsula interstitials), a genuine WebSocket-handshake check, and body-similarity / raw-replay helpers, reused across the hardening below.
Changed
- Active false-positive hardening — ~20 High/Critical modules now drop findings that fail strict confirmation (soft-404 / wildcard / reflection / content-shape gates), and ones that confirmed on HTTP status alone now require a content/body gate, so catch-all and SPA 200s no longer trip findings.
- Error/marker matching (
sqli-error-based,graphql-scan,ssrf-detection,ldap-injection,xxe-generic,nosqli-*, …) skips WAF/CDN challenge, auth-gate, and rate-limit pages before matching, closing the SSO/Cloudflare-challenge false-positive class. - Passive false-positive hardening — ~12 detectors (env-secret, info-disclosure, error-message, MCP-endpoint, jackson/joomla, …) skip static-asset and binary bodies, so a token baked into a minified bundle is no longer flagged.
- Secrets shown in full — target-discovered secrets are now reported unredacted (
secret-detect,env-secret-exposure,jwt-weak-secret,jwt-claims-detect); only operator BYOK credentials in logs/config stay masked. - vigolium-audit harness — restructured output layout with pre-merge and artifact-redaction passes, so
agent auditruns land cleaner, deduplicated, secret-safe output.
A detection-expansion and false-positive-reduction release: OS command-injection detection, a
-P/--parallel multi-target fan-out, finding evidence in output, and a broad hardening pass across the active modules and shared diffscan engine.Added
- OS command-injection detection — three new active modules covering results-based (in-band), out-of-band (OAST callback), and time-based blind techniques, each confirming execution across multiple rounds against a baseline to avoid false positives.
-P/--parallelflag (scan, default1): scan up to N targets concurrently via isolated child processes. Pair with-S -T --split-by-hostfor per-host output files, or--db-isolate -Tto merge every target into one shared--dband export a single unified result. Single target /-P 1is unchanged.- Finding evidence — findings now carry the supporting request/response pairs (baselines, confirmation rounds, control fetches) behind each decision, surfaced in console output and stored in the database.
Changed
- Broad false-positive hardening across the active modules: findings now have to reproduce and show real content-level changes, so they survive WAF/rate-limit pages, dynamic-content jitter, transient flaps, and reflection artifacts instead of misreading them as vulnerabilities. Confirmation replays also bypass the response cache so each sample is genuinely fresh.
- diffscan engine — excludes reflection-prone and volatile attributes from comparisons, gates out responses where the payload couldn’t have been evaluated (redirects, empty bodies, 404s), and surfaces which attributes actually differed across confirmation rounds.
- Severity recalibration — server-side template injection lowered to Info (a human-confirmation lead) and HTTP method tampering to Suspect (frequently non-exploitable alone).
- Passive checks — missing-security-header detection now also flags weak
Referrer-Policyvalues and cacheable sensitive HTTPS responses; CSRF detection filters out requests that aren’t actually CSRF-reachable (JSON bodies, header-based auth, no session cookie). - OAST service — command-injection callbacks are now classified as confirmed OS command injection rather than generic SSRF/XXE.
- Olium autopilot — project custom skills now load from
.agents/skills/, with a startup summary of available skills by source.
Removed
- Folded the standalone cacheable-HTTPS and Referrer-Policy passive checks into the missing-security-header module.
Added
- Five new active modules:
ssrf-filter-bypass,ssrf-protocol-smuggling,open-redirect-confusion,reverse-proxy-path-confusion(High), andcache-poisoned-dos/ CPDoS (Medium). --db-isolateflag (scan,agent autopilot,agent swarm): scan into a private temp SQLite DB and merge into--dbat the end, so parallel scans can share one--dbwithout write contention.--soft-failglobal flag: always exit 0 even on error (error still printed to stderr) for CI/scripts.
Changed
response-header-injection: confirmed injections served over a keep-alive connection are now flagged as likely to let an attacker poison the shared connection’s response queue and deliver their responses to other users, and are raised to High.
A false-positive reduction release: high/critical active modules now re-confirm findings (replay payload vs. clean baseline) before reporting, plus discovery and crawler robustness fixes.
Re-confirmation safety net
- Executor-level net (
modkit.ConfirmBodyDifferential+ opt-inBodyDifferentialConfirmable) replays a finding’s payload vs. a clean baseline and drops it without a reproducible difference, fails open on anything inconclusive. Opted in byhost-header-injection,reflected-ssti,struts-ognl-injection,web-cache-poisoning. - Dropped findings counted and surfaced via
SuppressedFindings().
SQL & NoSQL injection
sqli-boolean-blind— single-shot comparison replaced by a multi-round, multi-factor logic battery (operator probing, alternating comparisons, per-branch stability, invalid-syntax probe) + WAF payload mutation.sqli-time-blind— multi-round, delay-scaling confirmation to separate injection from network jitter.nosqli-operator-injection,nosqli-error-based— size-change hits re-confirmed against per-request variance; now require a captured baseline.- New
pkg/modules/infraSQLi helpers:sqldbms.go,sqlvalue.go,sqlwaf.go.
Active module confirmation
crlf-injection,response-header-injection— replay with fresh canaries across rounds.open-redirect— require the redirect to track a fresh injected domain across rounds.ssrf-detection— verify matched markers are payload-introduced, not ambient.idor-detection,idor-guid— determinism gate vs. per-request variance (skips analytics/beacon endpoints).mass-assignment— canary field detects endpoints that echo arbitrary keys.http-method-tampering— catch-all guard drops endpoints that accept any method.
Discovery & spidering
- Built-in wordlists materialized to disk and used as defaults (
internal/resources/wordlists). dedup_cluster_cap(default 10) collapses near-identical responses so catch-all/SPA targets don’t flood the scan;auto_fuzz_low_yield(default on) enablesFUZZbrute-forcing on low-yield/SSO-walled spidering.- Initial navigation retries on transient transport errors; proxied scans force Chrome to HTTP/1.1 (fixes
net::ERR_HTTP2_PROTOCOL_ERRORthrough Burp/ZAP); off-host start redirects classified as SSO wall vs. relocated app (host adopted into scope).
Expand XSS detection with additive modules that sit alongside the existing scanners rather than changing them. The WAF-aware evasion and encoding-payload work takes inspiration from dalfox.
XSS
- Stored XSS (
xss-stored) — browser-confirmed persistent XSS: writes a unique canary, re-fetches the page with a clean request, and only reports when the canary both persists and executes, distinguishing stored from reflected. - DOM-XSS taint (
dom-xss-taint) — passive AST taint analysis that raises a finding only when a DOM-controlled source (location.hash,document.cookie, …) provably flows into a dangerous sink (innerHTML,eval, …), complementing the pattern-baseddom-xss-detect. - Pre-encoded injection (
xss-light-encoded) — targets filters that decode a parameter (base64 / double-URL) before reflecting it. - WAF-aware evasion — a per-host
WAFRegistrylets modules publish the detected WAF/CDN so later insertion points reuse it, and a package-levelwaf.ClassifyPartshelper classifies blocks from raw response primitives. Inspired by dalfox’s WAF handling. - Encoding payloads —
pkg/modules/infra/xssencodesupplies execution-preserving payload mutators and an encoding ladder for bypassing filters. Inspired by dalfox’s evasion payloads.
jstangle
- Add axios and custom-protocol request-pattern extraction.
- Surface DOM-XSS source→sink taint flows (
dom_flows) in scanner output. - Add
linux/arm64/darwin/amd64correlation testdata.
Audit
- vigolium-audit no longer forces a hardcoded per-platform model and reasoning effort; it now inherits the agent runtime’s own configured default unless
--modelorVIGOLIUM_AUDIT_MODELis set explicitly.
Fix cross-platform release packaging for embedded helper binaries: GoReleaser, snapshot, release, public-release, and Docker builds now stage the matching
vigolium-audit blob per target, run cross-builds sequentially where the shared go:embed path would otherwise race, and restore the host blob afterward so local builds do not inherit the last release target. Add runtime and npm packaging guards that detect wrong-platform embedded audit blobs before users hit opaque exec-format failures. Also add missing jstangle embeds for linux/arm64 and darwin/amd64, with coverage tests to ensure every shipped release target has a real scanner binary instead of the unsupported stub.Make
--format jsonl emit the same post-scan, project-scoped {"type":...,"data":...} envelope as vigolium export (instead of the live nuclei-style stream) across scan, scan-url phase mode, and stateless runs; default stateless multi-target scans (-S -T file) to a single unified output file with new --split-by-host to opt into per-host files; surface timed-out modules in the scan status line (X/Y (A active, P passive, T timed out)); make failed scans exit non-zero and skip the “completed” banner instead of logging at INFO; accept --session/--session-file as aliases for --auth/--auth-file; and fold phases, intensities, and agent modes into vigolium strategy (dropping the ls subcommand).Publish multi-arch Docker images:
make docker-publish now builds and pushes both linux/amd64 and linux/arm64 (override via DOCKER_PLATFORMS) as a single manifest using docker buildx.Make
--scanning-max-duration cap total scan wall-clock time (all phases combined), widen severities to all levels for single-phase known-issue-scan runs, and add cve/kis/known-issues phase aliases.Bound the known-issue-scan phase to its
max_duration and default it to critical+high severities.Initial release of Vigolium open source.
