SheriffMark
Open-source brand-protection monitoring: watches for newly created domains resembling your brand (typosquats, lookalikes, combosquats) and alerts you, with evidence to support enforcement action. Self-host it on your own infrastructure — any cloud, or none at all.
This project is open source (AGPL-3.0) and cloud-agnostic by design: containers, SQLite by default (Postgres opt-in), SMTP, and configurable auth — built-in email/password by default, plus optional external OIDC (Supabase Auth, Keycloak, Authentik, Auth0, Zitadel, ...) and SAML 2.0 SSO, any combination enabled per deployment.
Screenshots
Quickstart
The fastest way to try it: package it once, then run one command.
git clone https://github.com/arunprasad/sheriffmark.git
cd sheriffmark
pip install -r requirements-dev.txt # gets `build`, for the step below
./scripts/build.sh # builds the frontend, bundles it into a wheel
pipx install dist/sheriffmark-*.whl # or: pip install dist/sheriffmark-*.whl
sheriffmark serve
That's one process on one port (http://127.0.0.1:8000 by default —
--host 0.0.0.0 to accept non-local connections) serving both the API
and the built UI, writing to a SQLite file in your current directory.
No Docker, no Postgres, no separate frontend dev server. sheriffmark --help lists the rest (migrate, worker, account create-account/
reset-password).
Once a release is published, this collapses further to pipx install sheriffmark with no clone or build step at all — see
CONTRIBUTING.md for the maintainer-side
publish step.
Prefer Docker, or need Postgres? See "Self-hosting" below.
Layout
core/ — pure detection logic (variant generation, RDAP/DNS/CT
checks, risk scoring). Zero cloud or tenant awareness.
adapters/ — storage/notifier implementations behind core/'s interfaces
shared/ — DB models, config, cross-cutting logic (e.g. plan limits)
used by both web/ and worker/
web/api/ — FastAPI app (JSON API + serves the built frontend)
web/frontend/ — React + shadcn/ui, Vite-built to static assets
worker/ — daily batch pipeline entrypoint
migrations/ — Alembic schema migrations
sheriffmark/ — the `sheriffmark` CLI (pip/pipx entry point) — thin
wrappers around the packages above, see Quickstart
scripts/ — build.sh: bundles the frontend + Python package into
one installable wheel
Self-hosting
Everything here runs as plain Docker containers — no dependency on any specific cloud provider. Two deployables:
worker/— a scheduled batch job (cron, Cloud Run Job, ECS Scheduled Task, a VPS crontab — anything that can run a container on a schedule). Does the actual detection work and sends email digests.web/— a long-running HTTP service (the dashboard + API). Auth works out of the box (built-in email/password,AUTH_ENABLE_LOCAL=trueby default) — no external provider required. See.env.exampleforAUTH_ENABLE_OIDC/AUTH_ENABLE_SAMLif you'd rather point at one.
Both need DATABASE_URL and, for worker/, SMTP credentials for sending
digests. See .env.example for the full list.
Database: SQLite by default, Postgres when you need it. Unset
DATABASE_URL and the app writes to a single SQLite file — nothing else
to run. This covers the common case (web/ and worker/ on the same
host, sharing a mounted volume) with zero extra services. Switch to
Postgres (DATABASE_URL=postgresql+psycopg://..., driver already baked
into both Dockerfiles) once you actually need it:
- Real concurrent write load (many brands, frequent scans).
web/andworker/run as separate containers that don't share a filesystem — e.g.web/on Cloud Run andworker/as an ECS Scheduled Task. This is the one case SQLite can't cover at all: it's a single file, so both processes need to see the same file, which only a shared volume/host mount guarantees.
Either way it's a connection-string change, not a code change.
No built-in "forgot password" flow yet — if you lose an account's password, or want to bootstrap the first account without going through the signup form, use the admin CLI directly against the database (no running server needed):
python -m web.api.manage create-account you@example.com
python -m web.api.manage reset-password you@example.com
Both prompt for the password interactively. This is also the account-
recovery path for a self-hosted deployment: if you can reach
DATABASE_URL, you can always recover an account this way.
Multi-tenancy is built in (useful if you're self-hosting this to watch brands for multiple clients, e.g. an agency) but there's no billing or plan-limit gating in this open-source build — usage is unlimited by default.
Local development
Nothing to run except Python — DATABASE_URL defaults to a SQLite file
in the repo root:
cp .env.example .env
pip install -r requirements-dev.txt
alembic upgrade head
python -m worker.seed # optional: one tenant + one brand for manual testing
pytest -q
To run the worker for real: python -m worker.main (needs SMTP env vars set
in .env to actually send a digest; without them, findings still get
written to the DB, only the send step will fail).
Prefer to develop against Postgres instead (matches production more
closely if that's what you'll deploy)? Uncomment DATABASE_URL in
.env, pip install -r requirements-postgres.txt, then:
docker compose up -d postgres
alembic upgrade head
pytest -q
Frontend:
cd web/frontend
npm install
cp .env.example .env.local # just VITE_API_BASE_URL for local dev — auth is all backend-side
npm run dev
See CONTRIBUTING.md for the fuller dev workflow and this project's design principles.
Running the worker in Docker
With the SQLite default, web/ and worker/ just need to share a
mounted directory:
mkdir -p data
alembic upgrade head # from the host — writes ./sheriffmark.db
cp sheriffmark.db data/
docker build -f worker/Dockerfile -t domain-watch-worker:latest .
docker run --rm \
-v "$(pwd)/data:/data" \
-e DATABASE_URL="sqlite:////data/sheriffmark.db" \
domain-watch-worker:latest
Run web/'s image the same way (-v "$(pwd)/data:/data" -e DATABASE_URL=sqlite:////data/sheriffmark.db)
so both containers see the same file.
Using Postgres instead:
docker compose up -d postgres
alembic upgrade head # from the host, against localhost:5432
python -m worker.seed
docker build -f worker/Dockerfile -t domain-watch-worker:latest .
docker run --rm \
--network <project-dir-name>_default \
-e DATABASE_URL="postgresql+psycopg://watch:watch@postgres:5432/watch" \
domain-watch-worker:latest
Note the container reaches Postgres via the compose service name
(postgres), not localhost — localhost inside a container means the
container itself. Find the actual network name with docker network ls
if <project-dir-name>_default doesn't match.
worker/main.py handles SIGTERM/SIGINT gracefully — docker stop
(and most schedulers' cancellation signal) lets it wind down at the next
safe point rather than force-killing it.
Both worker/ and web/ have also been deployed and verified live on
Google Cloud Run, purely as one example target — nothing in either
Dockerfile is GCP-specific.
License
AGPL-3.0. Contributions welcome — see CONTRIBUTING.md.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sheriffmark-0.1.0.tar.gz.
File metadata
- Download URL: sheriffmark-0.1.0.tar.gz
- Upload date:
- Size: 734.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
516560a77b277ecd26980f0f283956f2fd8b72e0eaa4ad8084d82906af35073d
|
|
| MD5 |
9bbc293992dbd9630be21805231945de
|
|
| BLAKE2b-256 |
f8142b5064dd32032b2a80809eba1294401d18bf3864eee85d6d836d99d9f6c8
|
Provenance
The following attestation bundles were made for sheriffmark-0.1.0.tar.gz:
Publisher:
release.yml on arunprasad/sheriffmark
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sheriffmark-0.1.0.tar.gz -
Subject digest:
516560a77b277ecd26980f0f283956f2fd8b72e0eaa4ad8084d82906af35073d - Sigstore transparency entry: 2678239946
- Sigstore integration time:
-
Permalink:
arunprasad/sheriffmark@a6e5c4e829c7f490990c10ddfa41b55e9c7dbb6e -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/arunprasad
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a6e5c4e829c7f490990c10ddfa41b55e9c7dbb6e -
Trigger Event:
release
-
Statement type:
File details
Details for the file sheriffmark-0.1.0-py3-none-any.whl.
File metadata
- Download URL: sheriffmark-0.1.0-py3-none-any.whl
- Upload date:
- Size: 357.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5e7fc1210ca4750613234882aa768b67ffaa6a2ceb7f59ed1f055286e512bca0
|
|
| MD5 |
1b149ffe8a8b5e066a28630d5cdfe2f1
|
|
| BLAKE2b-256 |
0ba56515229adf84e6c39584737c0d9a6c58832414642107fba674733565e262
|
Provenance
The following attestation bundles were made for sheriffmark-0.1.0-py3-none-any.whl:
Publisher:
release.yml on arunprasad/sheriffmark
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sheriffmark-0.1.0-py3-none-any.whl -
Subject digest:
5e7fc1210ca4750613234882aa768b67ffaa6a2ceb7f59ed1f055286e512bca0 - Sigstore transparency entry: 2678239963
- Sigstore integration time:
-
Permalink:
arunprasad/sheriffmark@a6e5c4e829c7f490990c10ddfa41b55e9c7dbb6e -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/arunprasad
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a6e5c4e829c7f490990c10ddfa41b55e9c7dbb6e -
Trigger Event:
release
-
Statement type: