Metadata-Version: 2.5
Name: casefile
Version: 1.1.0
Summary: One input box, every relevant OSINT pivot. Runs locally.
Project-URL: Homepage, https://casefile.cpwillis.dev
Project-URL: Source, https://github.com/cpwillis/casefile
Author: cpwillis
License-Expression: MIT
License-File: LICENSE
Keywords: intelligence,investigation,osint,recon
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Information Technology
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Requires-Python: >=3.11
Requires-Dist: httpx>=0.28
Requires-Dist: idna>=3.10
Requires-Dist: jinja2>=3.1
Requires-Dist: phonenumbers>=9.0
Requires-Dist: starlette>=0.41
Requires-Dist: uvicorn>=0.32
Description-Content-Type: text/markdown

# 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](https://pypi.org/project/casefile/). With [uv](https://docs.astral.sh/uv/), which fetches its own Python:

```bash
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:

```bash
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:

```bash
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.

```bash
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

```bash
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:

```bash
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.
