Skip to content
cpwillisPublic

About

One input box, every relevant OSINT pivot. Runs locally.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

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. casefile never contacts the target itself unless you click Visit the site or pass --visit; everything else asks a third party about it.

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 example.com --visit            # also visit the site itself: about 5 requests it sees
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.

--visit is separate, and --deep never includes it: the site visit (HTTP response and headers, TLS certificate, robots.txt, sitemap.xml, security.txt) goes to the target from your IP, so the site sees it. In the browser it is one Visit the site button per domain or URL. Every hop is resolved first and refused if any address is not public. It sends a browser User-Agent, CASEFILE_VISIT_USER_AGENT if set (environment or .env), else a current Chrome string. Cookie names are shown, never their values.

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).

Many entries were found through awesome-osint (CC BY-SA 4.0) and are marked provenance = "awesome-osint". The list was used for discovery only: each lookup URL was researched and verified separately, and the notes are casefile's own.

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, so does the target if you visit it, 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 and URLhaus need one key between them: ABUSECH_AUTH_KEY, from the environment or a .env in the working directory (see .env.example). Without it those panels read "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.

About

One input box, every relevant OSINT pivot. Runs locally.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages