Skip to main content

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-01xx and the ACMA 5550 xxxx ranges. It is an OSINT tool, so this matters more than usual.
  • One httpx.AsyncClient, built in fetchers/http.py, so every request carries the same User-Agent and timeouts.
  • The demo renders through the real templates with demo=True. A demo_*.html file 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.json is vendored byte-for-byte under CC BY-SA. Never edit it; keep casefile-specific behaviour keyed off site names. See src/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…". Allow 127.0.0.1 in the blocker.
  • MalwareBazaar is the one source needing a key: ABUSECH_AUTH_KEY, from the environment or a .env in 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)

Source distribution for casefile 1.1.0
File Size Uploaded
casefile-1.1.0.tar.gz 198.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for casefile 1.1.0
File Interpreter ABI Platform
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 log

Release 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 log

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page