casefile
Paste an identifier (domain, IP, ASN, URL, email, username, person, company, phone, hash, CVE, crypto address or tx, coordinates, MAC, VIN, MMSI, IMO, ICAO24, tail number) and casefile works out what it could be, fetches what keyless sources will give up, and lists the rest as links you open yourself.
Local only: binds 127.0.0.1, no account, no telemetry, nothing hosted.
Run
On PyPI. With uv, which fetches its own Python:
uvx casefile
uv tool install casefile keeps it on your path; pip install casefile works too on Python 3.11 or newer. To run from source instead:
git clone https://github.com/cpwillis/casefile
cd casefile
uv sync
uv run casefile
Either way it starts the local web app and opens a browser on it:
casefile is running at http://127.0.0.1:8765
press ctrl-c to stop
--port if 8765 is taken, --no-browser on a headless box. There is no flag to bind another address, on purpose.
Forward the port instead:
ssh -L 8765:127.0.0.1:8765 user@host
CLI
A positional identifier prints to the terminal instead of starting the server. casefile below is uv run casefile
from a source checkout. --json applies to a lookup; the case and cache commands print text.
casefile example.com # text
casefile example.com --json # machine-readable
casefile example.com --no-fetch # links only, no requests
casefile example.com --no-cache # bypass the response cache for this run
casefile example.com --check-links # probe each link, flag the ones definitely gone
casefile jdoe-example --deep # also run on-demand sources (bare, or a comma-separated list of ids)
casefile --cases # list saved cases with their ids
casefile --export <case-id> --format md|json|html
casefile --clear-cache
casefile --forget-cases
--deep and --check-links are opt-in because the egress is large: the WhatsMyName check queries several hundred
sites from your IP and takes 30-60s. The browser has the same two as buttons (Run this check, Check for dead
links) rather than panels that load themselves. Only 404 and 410 count as a dead link; blocked, redirected and
unreachable are reported as telling you nothing, because a checker that guessed would invent cleared leads.
Checks
make check # ruff check, ruff format --check, pytest. Exactly what CI runs.
make test # pytest only
make fmt # ruff format, ruff check --fix
make live # pytest -m live: hits real third-party services, deselected by make test
make demo # render the static demo into site/
Adding a source
A data edit, not code: a [[source]] block in src/casefile/catalog/<category>.toml with id, name, accepts
(entity types) and an https:// url containing {value}. make test validates every entry, and rejects a
duplicate id or a duplicate url for the same type (two rows going to one page is a difference that is not there).
A fetcher, ie a source casefile calls itself rather than links to, is a registration in
src/casefile/fetchers/sources.py.
Constraints the tests enforce
Reversing one of these fails a test in tests/test_constraints.py, not by accident.
- No real host or dialable phone number in fixtures or in this README. Reserved only:
example.com, RFC 5737, RFC 3849,555-01xxand the ACMA5550 xxxxranges. It is an OSINT tool, so this matters more than usual. - One
httpx.AsyncClient, built infetchers/http.py, so every request carries the same User-Agent and timeouts. - The demo renders through the real templates with
demo=True. Ademo_*.htmlfile is the fork coming back. - No startup hook on the app: it would open a browser for every importer, including CI and
--build-demo. src/casefile/vendor/wmn-data.jsonis vendored byte-for-byte under CC BY-SA. Never edit it; keep casefile-specific behaviour keyed off site names. Seesrc/casefile/vendor/README.md.
Design and plan docs are in docs/superpowers/. Internal, and excluded from the sdist.
What hits disk
Two SQLite stores, purged by different commands on purpose: a privacy purge must not destroy work you saved deliberately.
| Store | Path | Lifetime | Purge |
|---|---|---|---|
| Response cache | ${XDG_CACHE_HOME:-~/.cache}/casefile/cache.db |
24h, 5 min for failures | --clear-cache |
| Saved cases | ${XDG_DATA_HOME:-~/.local/share}/casefile/cases.db |
until deleted | --forget-cases |
The cache is keyed on the identifier searched, so it is in effect a local log of what you looked up, written whether
or not you save anything, and the home page lists the most recent of them as "Recent searches". On-demand checks and dead-link verdicts are cached the same way, so a reload or a return to
an earlier search shows them without re-running, and a check still running when the page reloads is joined, not
restarted, and survives a restart of the app itself. A jobs table in the same file records each run while it lasts,
with the per-site and per-link outcomes it has collected, so the web app resumes an interrupted run on start rather
than losing it or beginning again. Expired rows are swept on open, not on write. Both files are 0600 inside a 0700
directory, and a purge unlinks the file plus its -journal/-wal/-shm siblings rather than deleting rows, which
would leave search terms readable in freed pages.
Fetching goes out over your own connection: every source sees your IP, and there is no proxy in this version.
Cases
A case is an investigation, not one identifier: jdoe-example the username and example.com the domain are the same
subject to you and nothing alike to a detector.
- Save this identifier keeps a lead before anything on it is worth starring. Starring a finding also opens a case.
- An identifier lives in at most one case. Adding it to another moves it, findings included.
- A case is named after its first identifier and can be renamed on its page.
- Removing an identifier takes its findings with it, and removing the last one closes the case. Un-starring the last finding does not, because that is a change of mind about one row.
Gotchas
- Panels load over
fetch, so a content blocker that matches the request URL kills one. A blocked panel reads failed with an explanation rather than sitting on "loading…". Allow127.0.0.1in the blocker. - MalwareBazaar is the one source needing a key:
ABUSECH_AUTH_KEY, from the environment or a.envin the working directory (see.env.example). Without it that panel reads "needs a key" and nothing else changes.
Demo and release
make demo builds a static prerender of the real templates against fixture data into site/. It cannot look anything
up. The same templates render the live app; demo=True is the only difference, so the demo cannot drift from it.
Deploy is manual, because site/ is build output and is not in git:
make demo && npx wrangler@4 deploy
wrangler.jsonc declares casefile.cpwillis.dev as a custom_domain, so the first deploy creates the DNS record.
The demo emits its own robots.txt, sitemap.xml and 404.html, and links to the shared policies at
cpwillis.dev, which is what brings it under them.
.github/workflows/release.yml is manual dispatch only: it runs make check, builds, asserts the wheel carries the
catalogue, the vendored dataset, the templates and the static assets, then publishes to PyPI via Trusted Publishing.
Tagging deliberately does not publish, since a PyPI version can never be re-uploaded. ci.yml and live.yml are
manual too.
Metadata
Release files for casefile 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| casefile-1.1.0.tar.gz | 198.2 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| casefile-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 352.6 kB
Release files / casefile-1.1.0.tar.gz
| Download URL | casefile-1.1.0.tar.gz |
|---|---|
| Size | 198.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e7efb37185b41c5806af8755810dbfce25d9956dcb7dc1945341ae4c776cac99
|
|
BLAKE2b-256 checksum How to use checksums |
7700ef0912ca0e58feb80f911e866dd57a4a849d24d83b067c3b2887cef5de03
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.
Transparency logRelease files / casefile-1.1.0-py3-none-any.whl
| Download URL | casefile-1.1.0-py3-none-any.whl |
|---|---|
| Size | 154.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
01ba0d027675495b7da1436bd338064191e0bcdeaaeb3b1aaeb6b81f5147e97b
|
|
BLAKE2b-256 checksum How to use checksums |
1969fabf7cbf071a019b2e255403b515bb56520eae182fc885d3e92df6c4daf8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.
Transparency logSecurity for casefile 1.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for casefile-1.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.