Repository navigation
A security release for multi-user self-hosted installs, plus everything built since 2.0.3: a shared knowledge
tier and owned page index, new intelligence sources, safety parity between the CLI and the web app, an official
progress hook, a cloud-ready source deny-list, and many reliability fixes.
Upgrade: read docs/SECURITY-ADVISORY-2026-10.md, install 2.1.0, rebuild the web UI, and run
alembic upgrade head (migrations 0027 to 0036). Rotate JWT_SECRET if you want to end sessions issued before
the upgrade. Multi-user installs should upgrade promptly.
Privacy: nothing leaves your install. The new "shared" tier is shared between the users of one install, in your
own database, and only ever holds sanitized observations, never who searched for what. The cross-install ingest API
is off unless you set VOIDACCESS_INGEST_ENABLED=true on a corpus service you run yourself.
Security
- Fix self-hosted multi-user data exposure: shared ownership dependencies return
404 for foreign investigation, entity, graph, analysis, export, actor and monitor resources. - Add owned entity link context, investigation-scoped pages, private actor notes
and aliases, database monitors and alerts, and user-filtered Chroma search. - Add the
user/adminrole andvoidaccess admin promote --email; administrative
routes now require an administrator. Existing first users bootstrap as admin. - Make forwarded-IP trust opt-in and add authenticated-user request limits.
- Move web sessions to httpOnly cookies; server proxies forward authentication,
and logout revokes backend JWTs. Database revocation errors fail closed. - Add opt-in
VOIDACCESS_RSS_SNIPPET_ONLY=1to avoid fetching article bodies. - Add migration
0036_multiuser_security, idempotent YAML/vector import, two-user
API tests, migration tests, route enumeration and browser-session tests. - See
docs/SECURITY-ADVISORY-2026-10.mdfor upgrade and attribution limitations.
Breaking changes
- Database: run
alembic upgrade head(migrations0027to0036). The ownership migration has no destructive
downgrade; back up first (see the advisory). - Administrative routes require the
adminrole. voidaccess.llm.generate_summaryraisesSummaryUnavailableinstead of returning an error string.- CLI JSON
sources_usedstatuses renamed:fail->error,skipped_no_key->no_key; newdisabled,
skipped_by_allowlist,timed_out. voidaccess investigate --quietprints nothing except fatal errors.
New: shared tier and owned page index (local to your install)
- Shared corpus tier (
shared_corpus/): discovered onions, page snapshots, canonical IOCs and source reputation,
kept apart from private investigation data, so one investigation's observations speed up the next (cache hits,
seed expansion, pre-extracted entities) without revealing who searched for what. Migrations0027,0028,
0030to0032,0034,0035. - One sanitizer for every shared write (
sanitizer/): content-safety drop, credential redaction and personal
data redaction before anything reaches the shared tier; it fails closed (stores nothing) when it can't guarantee
redaction. Plus a page-snapshot object store (snapshot_store/) and IOC hygiene helpers (hygiene/). - Anti-poisoning trust gate (
trust_gate/): deterministic quarantine, corroboration, source reputation and
anomaly checks for the shared tier, with corpus-health views for the maintainer (observability/). - Cross-install ingest API with Sybil-resistant corroboration (
ingest/,api/routes/ingest.py, migration
0033). Off by default (VOIDACCESS_INGEST_ENABLED=false). - Owned, semantically ranked page index (
owned_index/), filled from investigations and seeds
(index_population/) and kept fresh by a background loop (freshness/) on a durable DB-backed job queue
(jobqueue/, migration0029), with an in-process rate limiter (ratelimit/). - New sources and search improvements: CVE intelligence, EPSS exploit-probability scores and Exploit-DB; a much
larger curated onion seed list; search result ranking and an engine cull (search/ENGINE_CULL.md); a reusable
blocklist-feed matcher (feeds/); presence-only HIBP Pwned Passwords checks via k-anonymity
(credential_presence/); a shared clearnet connection pool (netpool/). - Benchmark tooling (
benchmark/): engine probes with latency and unique-result share.
Changed defaults (self-hosters will notice these)
-
More distinct onion pages per run. Onion links are now picked one per site and
at most two per mirror family (sites whose addresses share their first seven
characters), with real search-result links ahead of addresses a results page
only mentions. The number fetched follows--depth: 15 (shallow), 25 (normal),
40 (deep), overridable withVOIDACCESS_ONION_TOP_N(5-60). When failed fetches
or mirrored copies leave slots empty, one extra batch backfills them from the
next-ranked links;normalanddeepalso follow up to 10 / 20 links found on
the fetched pages. Every extra fetch goes through the same URL and content
safety gates. -
Source status can read
partial: some of the source's requests worked and
some failed. In theenrich.sourcetrace event a partial source readserror
(the trace vocabulary has nopartial). -
Packaging:
pip install voidaccessnow ships 13 packages it previously
omitted (shared_corpus,trust_gate,netpool,jobqueue,hygiene,
sanitizer,ratelimit,observability,snapshot_store,
credential_presence,feeds,ingest,benchmark) plus the new
progresspackage. A wheel install used to crash on import.tests/test_packaging.py
builds a wheel and checks it from a clean venv. -
CLI page cap:
voidaccess investigatenow honoursMAX_LLM_PAGES_PER_INV
(default unchanged at 10); it was silently ignored before. -
Summary loss: the CLI summary phase now retries once with a reduced context
after a timeout, then falls back to a deterministic non-LLM summary. The first
attempt gets 67% ofVOIDACCESS_SUMMARY_TIMEOUT(60 of 90 s by default) so the
retry fits in the same budget. The report JSON gainssummary_kind
(llm|fallback|none). The CLI also passespreset="threat_intel"by
keyword; it was being passed in theentitiesposition. -
Search engines:
OSSandTorNetare quarantined (excluded from the
fan-out, retained for re-probe). Live Tor probe, 4 samples each: p90 latency
48.6 s / 76.1 s and unique-result share 0% / 7.2%
(benchmark/artifacts/E1B_engine_probe.json). Kaizer, Anima, Tornado and
Find Tor were already quarantined and probed at 0% success again. -
Tests:
tests/test_setup.shno longer contains a real-looking OpenRouter
key or admin password. -
generate_summaryraises (library API): it now raises
voidaccess.llm.SummaryUnavailable(with the cause chained) when the LLM fails
or there is no page content, instead of returning the string
"Summary unavailable — LLM error: …"or"… No extractable content found.".
A failed LLM could therefore be stored as, and reported as, a real summary.
Callers must catch it; the CLI and the API do. -
Summary retry and fallback on every failure, in both entry points: the CLI
retried only after a timeout; now any failure (SummaryUnavailable, another
exception, an empty answer) retries once with reduced context and then falls
back to the deterministic summary. The API now does the same through the shared
voidaccess.summary_fallback.summarize_with_fallback(it used to store the
error text).summary_kindisllmonly when the model produced text. No
user-facing text contains a raw exception message any more: the API's stored
summary no longer ends insummary generation failed: <exception>, the CLI
display line shows a generic message, andsources_used[...].erroris the
exception class name rather than its message. -
--quietis now silent: it used to print aVoidAccess — <query>header,
every step line, and then the full result panel. It now prints nothing at all
except fatal errors, and those never echo the query (a too-short query used to
be quoted back). Library warnings, the startup banner (on a terminal) and the
optional-keys configuration warning are suppressed too. Output files are still
written; their paths are in thecompletedevent of--events-jsonl(file
names embed the query, so they are not printed). JSONL and callback sinks are
unaffected. -
sources_usedis honest and richer (CLI JSON): statuses are nowok,
error,timed_out,skipped_by_allowlist,disabled,no_key(plus
skippedfor "not run in this mode", e.g.--no-tor). Renames:fail->
error,skipped_no_key->no_key. A source switched off by
VOIDACCESS_ENRICHMENT_SOURCESor by its own off switch
(PASTE_SCRAPING_ENABLED=false,GITHUB_SCRAPING_ENABLED=false, ...) no longer
readsokwith zero results; a source whose every HTTP call failed reads
error; a side source that raised readserror(it used to readok). Every
entry gainsrequests(HTTP calls actually made), and any source that made
calls but was not reported before now appears. The summary panel's "Sources"
count countsoksources only. The API's flat string statuses are unchanged
apart from the newdisabledandskipped_by_allowliststrings. -
SIGTERM cancels
voidaccess investigatecleanly: it used to kill the
process with no event and no cleanup. Now the current item finishes (up to
VOIDACCESS_CANCEL_GRACE, 3 s), the progress hook emitscancelledwith an
accuratestages_completed, the investigation row is markedcancelled, and the
process exits 143 without writing any output file. Ctrl-C (exit 130) also marks
the rowcancelled(it used to stayrunninguntil the stuck-run sweep failed
it). -
Output files are written atomically: the JSON and Markdown reports are
staged as dot-prefixed temp files and renamed into place after the last cancel
checkpoint, so a cancelled or killed-at-the-wrong-moment run can no longer leave
a truncated report. -
Cancel checkpoints inside the long stages (API and CLI): a cancel request is
now honoured between engines in the search fan-out, between pages in fetch and
scrape, and between sources in enrichment, not only at stage boundaries. With no
cancel requested nothing changes. -
Tests:
tests/test_progress.py::test_rich_display_output_unchangedcompares
the live display (notquiet=True, which is now silent).
New, opt-in (no change unless set)
- Official progress hook (
progress/): 8-stage events with numeric counts, Rich /
JSONL / callback sinks,voidaccess investigate --events-jsonl PATH. VOIDACCESS_ENRICHMENT_SOURCESallowlist for enrichment sources.VOIDACCESS_DISABLED_SOURCES: a cloud deny-list. The named sources make no
network call in any stage (Tor engines, paste sites, GitHub, GitLab, RSS,
Telegram, blocklist feeds, historical intel, Pwned Passwords, enrichment) and
reportdisabled. Unknown ids are a hard error. Recommended cloud value:
openphish,sslbl,phishtank,ransomwhere,hibp_pwned_passwords,fbi_doj_press,pastebin,paste_ee,dpaste,rentry,gitlab,telegram
(sources.source_control.CLOUD_DISABLED_SOURCES). Source ids are listed in
docs/architecture.md section 13.18.VOIDACCESS_CANCEL_GRACEandVOIDACCESS_CANCEL_POLL_INTERVALtune the cancel
helper (utils/cancel.py);_run_investigation(..., cancel_token=...)lets an
embedding runner cancel cooperatively.- Per-source HTTP request counts:
sources_used[...].requests, and
source_requests,source_requests_by_step,sources_statusand the output
paths in thecompletedprogress event'smetrics. VOIDACCESS_SEARCH_WORKERS,VOIDACCESS_SCRAPE_WORKERS,
VOIDACCESS_EXTRACT_CONCURRENCY,VOIDACCESS_LLM_CHUNK_CONCURRENCY,VOIDACCESS_LLM_CHUNK_TIMEOUT,VOIDACCESS_SEARCH_DEADLINE,
VOIDACCESS_CONCURRENT_COLLECT;VOIDACCESS_ENRICHMENT_TIMEOUTis now applied
on the CLI path when set. See docs/architecture.md section 13.17.benchmark probeartifacts reportlatency_p90_msandunique_result_share
per engine.- Trace events (progress schema 2):
voidaccess investigate --trace --events-jsonl PATH
orVOIDACCESS_TRACE_EVENTS=1adds atraceevent for each refined query,
search engine call, URL found/kept/dropped (with the reason), page fetch outcome,
page entity counts, entity found (masked), enrichment source, graph progress and
summary status, so a dashboard can animate a live run. Hosts are shown only in a
shortened or defanged form, with names that spell out abuse content masked; no
page text, paths, credentials or personal data,
and anything the content-safety filter dropped is reported as an id and a reason
only. Volume is capped per kind (VOIDACCESS_TRACE_CAPS, at most 3,000 per run),
and traces are dropped first when a sink falls behind. JSON Schema:
progress/schema/progress-v2.schema.json; docs/architecture.md section 13.19. - Every progress event now carries
schema: 2(an added field; nothing else in the
schema-1 events changed).
Safety
- Align CLI and API content-safety gates: Tor and side-source page text is
scanned via sharedgate_pages()before persistence, vector cache, extraction,
or summarization; URL and entity gates fail closed on errors. - Add
VOIDACCESS_SAFETY_BYPASS_PROMPT(ondefault,offfor cloud runners)
to omit the LLM authorization preamble from refine, filter, and summary prompts
without disabling mandatory content filters.
Fixed
- A failed fetch is no longer counted or stored as a page. The scraper used to
return the search result's title as the text of a page it could not fetch (or
that had no text), so those titles were stored, extracted and counted in
pages_fetched. Now only a fetch that returned content is a page; the failure
shows only as itspage.fetchedoutcome. - Source status is honest for fixed endpoints. Any non-2xx answer from a feed
or a fixed API (ransomware.live, CISA advisories, justice.gov, the MITRE
bundle, the blocklists) is a failure; a per-item lookup's 404 (Shodan
InternetDB, HIBP, VirusTotal, NVD and similar) still means "no record". Calls
made asawait session.get(...)are counted too, and a feed that failed is not
requested again in the same run. Sources that silently readokwhile every
call 404'd now readerror. - Dead endpoints replaced: ransomware.live groups and victim posts now come
from its data feed (data.ransomware.live/groups.jsonandvictims.json; the
API's/groups,/group/{name}and/v2victim endpoints are gone), CISA advisories fromcybersecurity-advisories/all.xml, and
justice.gov press releases from/news/rss. The last two are RSS and are now
parsed as such (the justice.gov feed had never parsed). - MITRE ATT&CK techniques are attributed only to a group the run found, by
exact name or alias. Matching was by substring, so "Tor", "royal" or "bert"
pulled in unrelated groups' techniques. Now a name must equal an intrusion
set's name or alias (after a built-in alias table that now holds only names
ATT&CK itself lists for the group; brand mappings such as REvil, GandCrab or
Conti to Wizard Spider and DarkSide to FIN7, and the tool "Cobalt Strike",
were removed); an alias that is also an ATT&CK software name is
ambiguous and matches nothing; revoked sets are ignored. The overlay counts
THREAT_ACTOR_HANDLEas an actor, runs only when the run has no
MITRE_TECHNIQUEyet (it used to check a misspelt CVE type), adds one page per
group, and tags every technique with its group. - MITRE output now needs a corroborated actor. An actor drives the ATT&CK
overlay and historical intel only if a pattern or the gazetteer extractor found
it, it was seen on at least two different hosts, or the query names it. One
entity-recognition hit on one page (a common word that is also an ATT&CK group
name) used to add that group's techniques to most reports. The actor entity
itself is still reported. - ransomware.live and RansomLook group names match whole words only. A group
name or listed alias must appear as a whole word in the query (at least three
characters, case-insensitive); versioned names such aslockbit3also match
their stem ("LockBit"). Substring matching let a one-letter group name match
unrelated queries and add its pages. ransomware.live is off in the hosted
profile, so this affects self-hosted runs. - Organisation names drop link-directory and markup noise. Values such as
"Name - description" titles, markdown residue, slash lists,.onionnames,
directory words, base64 runs and dark-market listing words are removed before
they are stored or sent to the NVD CPE lookup; truncated parentheses, link
tails and leak-listing status words are trimmed first. On 379 labelled values
from the regression runs: 97.8% precision, 75.9% recall. - Enrichment no longer throws away finished work at its 59 s deadline. The
CISA KEV / NVD / Shodan / VirusTotal / historical-intel (MITRE) / CVE-fusion
group ran its steps one after another behind NVD, so it usually missed the
deadline and all of its results were discarded (MITRE techniques, the CVE
view and many relationships were lost). Now historical intel starts as soon as
CISA, Shodan and VirusTotal are done, the CVE fusion as soon as NVD and KEV
are (bounded at 8 s, falling back to the plain NVD/KEV pages), NVD stops
starting new lookups after 35 s and keeps what it fetched, and the group
returns its finished parts 2 s before the phase deadline. A slow source loses
only its own results; the phase still ends by 59 s. The constants are in
sources/enrichment.py(ENRICHMENT_DEADLINE_S,NVD_BUDGET_S, ...). - A source a deadline cut off now reads
timed_out, one that failederror,
insources_usedand in theenrich.sourcetrace events, including the
sources inside that group (cisa_kev,nvd,cve_intel,epss,
exploitdb,shodan_internetdb,virustotal,mitre_attack) and the
other enrichment feeds. Before, a dropped source readokwith 0 results.