Authorized organizational exposure-monitoring agent for the DeHashed Search API
A production-ready, CLI-first Python security tool that monitors credential-exposure records for assets your organization has explicitly verified. Integrates with the official DeHashed Search API and Data Wells API.
Features · Quick Start · CLI Reference · Exports · Security · Architecture · Deploy
- Overview
- Features
- Quick Start
- CLI Reference
- Export Formats
- Security Guarantees
- Architecture
- Secrets Backends
- Integrations
- Production Deployment
- Testing
- Documentation
- Contributing
- License
Exposure Sentinel is an authorized exposure-monitoring agent. It is not a public person-search tool. Every query is restricted to:
- Domains your organization has verified via DNS TXT challenge
- Emails explicitly added to your authorized investigation list (with a mandatory ticket #)
- An investigation reason required for every single lookup
The tool sits between your security team and the DeHashed API, enforcing scope, redacting sensitive fields, recording an immutable audit trail, and producing professional reports in 7 formats.
| Without Exposure Sentinel | With Exposure Sentinel |
|---|---|
| Analysts paste API keys into chat scripts | Keys live in a secrets manager, never exposed |
| No audit trail of who searched what | Every action recorded immutably |
| Results often leak plaintext passwords | Redaction layer masks all sensitive fields |
| No scope enforcement — any email searchable | Hard scope filter on verified domains + authorized emails |
| Reports ad-hoc, copy-paste, error-prone | 7 export formats: JSON, CSV, HTML, PDF, XLSX, MD, SARIF |
| No severity prioritization | Multi-factor severity scoring (credentials, MFA, recency, …) |
- Masked input via
getpassin CLI, password field in web UI - Real authenticated validation against
api.dehashed.com— never simulated - 8 pluggable backends: local encrypted, env, Docker secret, K8s secret, AWS Secrets Manager, Azure Key Vault, GCP Secret Manager, HashiCorp Vault
- Masked display only —
dh_****8F2A - Never logged, returned by API, persisted to PostgreSQL, stored in browser, committed to source, or sent to the LLM agent
- Sanitized error categories:
invalid_api_key,unauthorized,subscription_or_quota,rate_limit_exceeded,network_failure,dehashed_service_unavailable,bad_query,unknown - Fail-closed production mode — 503 if key/DB/secrets unavailable
- DNS TXT challenge ownership proof (32-byte random token)
- Verified-domain allowlist — DeHashed results are filtered against it
- Authorized investigation email list — each entry requires a ticket # / reason
- Query injection prevention — DeHashed DSL validated against field allow-list
- One-command scans against your verified scope
- Diff vs. previous scan —
is_newflag on every record - Severity score (0-100) combining:
- Plaintext password exposure (+40)
- Password hash exposure (+25)
- Privileged account status (+15)
- MFA disabled (+10)
- Breach recency (+3 to +15)
- Distinct data wells (+2 each, max 5)
- Active account status (+10 / -5)
- Severity buckets:
CRITICAL≥70,HIGH≥45,MEDIUM≥25,LOW≥10,INFO<10 - Auto-incident creation for CRITICAL/HIGH findings with remediation recommendations
- 7 formats: JSON, CSV, HTML (styled), PDF (printable), XLSX (multi-sheet), Markdown, SARIF 2.1.0
- All exports redacted — no plaintext passwords or hashes ever reach a file
- Use cases: programmatic consumption, Excel/Sheets import, browser view, printable report, SIEM ingestion, Jira/Confluence tickets
- Envelope encryption at rest (KEK + DEK via Fernet / AES-128-CBC + HMAC-SHA256)
- RBAC —
admin,analyst,viewerroles - Immutable audit log — every key lifecycle event recorded with masked + fingerprinted representation
- Redaction layer — passwords, hashes, SSNs, credit cards, phone numbers masked before any output
- Rate limiting — per-user and per-org (Redis-backed in server mode)
- Secret-scrubbing log filter — defense-in-depth against accidental key leakage
- Query allow-list — blocks SQL-injection-style payloads against DeHashed DSL
- Slack — webhook alerts with color-coded severity
- Microsoft Teams — MessageCard alerts
- Jira — auto-create issues
- ServiceNow — auto-create incidents
- Splunk — HEC ingestion
- Microsoft Sentinel — Log Analytics ingestion
- 15 MCP tools for LLM agents
- LLM never receives: API key, plaintext passwords, raw DeHashed responses, full PII
- Deterministic auth/policy/redaction layer between DeHashed and the agent
- Python 3.12+
- A DeHashed subscription and API key
- (Optional) PostgreSQL + Redis for server mode; SQLite works for standalone CLI
git clone https://github.com/rksharma-owg/Exposure_Sentinel.git
cd Exposure_Sentinel/backend
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"$ exposure-sentinel setup
╭────────────────────────────────────────────────────────╮
│ Exposure Sentinel │
│ Authorized organizational exposure monitoring · v1.0.0 │
╰────────────────────────────────────────────────────────╯
ℹ Checking for existing DeHashed API key…
ℹ No DeHashed API key is configured.
ℹ The key will be validated with a real authenticated request to DeHashed.
ℹ It will never be printed back, logged, or shared with the LLM agent.
Enter your DeHashed API key: ************
ℹ Validating DeHashed API credentials…
✓ Connection successful.
✓ API key stored securely.
✓ DeHashed monitoring is now enabled.
ℹ Configured key: dh_****cdef$ exposure-sentinel domains add example.com
✓ Domain example.com registered (pending verification).
ℹ Publish this DNS TXT record:
ℹ Name: exposure-sentinel-verify.example.com
ℹ Value: exposure-sentinel-verify=AbC123...XYZ789
ℹ Then run: exposure-sentinel domains verify 1
# After DNS propagates:
$ exposure-sentinel domains verify 1
✓ Domain example.com verified!$ exposure-sentinel scan --reason "Quarterly audit TICKET-42"
✓ Scan c547e008-6106-4b3e-9de4-7aceb5ec3294 completed.
ℹ Records found: 1
ℹ New records: 1
ℹ Status: succeeded
ℹ Run exposure-sentinel export c547e008-6106-4b3e-9de4-7aceb5ec3294 --format pdf --out report.pdf to export.# View in terminal (rich tables, color-coded severity)
exposure-sentinel history
exposure-sentinel records c547e008
# Export to any of 7 formats
exposure-sentinel export c547e008 --format pdf --out report.pdf
exposure-sentinel export c547e008 --format xlsx --out report.xlsx
exposure-sentinel export c547e008 --format sarif --out scan.sarif
exposure-sentinel export c547e008 --format markdown --out scan.md| Command | Purpose |
|---|---|
setup |
First-run secure API-key onboarding wizard (masked input via getpass) |
status |
Show masked configuration status |
test |
Run a real authenticated request to DeHashed |
search |
Search exposure records for a verified domain or authorized email |
scan |
Run an exposure scan against the verified scope |
history |
List recent scans |
records <scan-id> |
List exposure records from a scan |
incidents [scan-id] |
List incidents and remediation recommendations |
audit |
View immutable audit log |
export <scan-id> |
Export scan results to a file |
server |
(Optional) Run the FastAPI server + web UI |
version |
Show version |
keys |
Sub-app: API-key lifecycle management |
domains |
Sub-app: domain registration & verification |
emails |
Sub-app: authorized investigation emails |
config |
Sub-app: view / edit CLI configuration |
integrations |
Sub-app: integration adapters |
exposure-sentinel keys list # Show masked API key metadata
exposure-sentinel keys add # Add a new key (alias for setup)
exposure-sentinel keys replace # Replace the current key
exposure-sentinel keys remove # Remove key — disables all scans immediately
exposure-sentinel keys test # Alias for `test`exposure-sentinel domains list
exposure-sentinel domains add example.com
exposure-sentinel domains verify 1
exposure-sentinel domains remove 1exposure-sentinel emails list
exposure-sentinel emails add alice@example.com --reason "TICKET-42 investigation"
exposure-sentinel emails remove alice@example.com# Search by verified domain
exposure-sentinel search --domain example.com --reason "TICKET-42"
# Search by authorized email
exposure-sentinel search --email alice@example.com --reason "TICKET-42"
# Limit results and export immediately
exposure-sentinel search --domain example.com --reason "TICKET-42" \
--size 50 --export pdf --out search.pdf# Full scan of verified scope
exposure-sentinel scan --reason "Quarterly audit"
# Restrict to a single domain
exposure-sentinel scan --reason "TICKET-99" --domain example.comexposure-sentinel export <scan-id> --format <fmt> --out <path>exposure-sentinel config show
exposure-sentinel config set mode standalone # standalone | server | embedded
exposure-sentinel config set server_url https://es.internal.example.com
exposure-sentinel config set org_id my-orgexposure-sentinel integrations list # Show configured adapters
exposure-sentinel integrations test # Send a test alert| Format | Flag | Use Case | Sample Output |
|---|---|---|---|
| JSON | --format json |
Programmatic consumption, API integration | Structured JSON with scan, records, incidents |
| CSV | --format csv |
Excel/Sheets import, data analysis | Flat one-row-per-record CSV |
| HTML | --format html |
Browser-openable styled report | Self-contained HTML with severity color-coding |
--format pdf |
Printable report for sharing | Landscape PDF with tables and severity badges | |
| XLSX | --format xlsx |
Multi-sheet Excel workbook | Summary + Records + Incidents sheets |
| Markdown | --format markdown |
Jira / Confluence / GitHub tickets | Markdown with tables and badges |
| SARIF | --format sarif |
SIEM / code-scanning consumers | SARIF 2.1.0 compliant JSON |
SCAN_ID=c547e008-6106-4b3e-9de4-7aceb5ec3294
mkdir -p reports
for fmt in json csv html pdf xlsx markdown sarif; do
exposure-sentinel export $SCAN_ID --format $fmt --out reports/scan.$fmt
doneAll exports are redacted — passwords become [REDACTED:password], hashes become hash:abc123…, SSNs / credit cards / phone numbers are scrubbed. This is verified by automated tests.
| ❌ Never | ✓ Instead |
|---|---|
| Printed to terminal | Only dh_****8F2A masked form shown |
| Logged (any level) | Secret-scrubbing log filter as defense-in-depth |
| Returned by any API endpoint | Status endpoints return only masked metadata |
| Persisted to PostgreSQL | Only masked + fingerprinted reference row |
| Stored in browser local storage | Web UI receives only masked form |
| Committed to source code | .gitignore excludes .env, *.key, secrets.enc |
| Sent to the LLM agent | MCP tools return only masked metadata |
| Stored in plain text on disk | Envelope-encrypted (KEK + DEK via Fernet) |
- Local encrypted (default for CLI): Fernet envelope-encrypted file at
~/.exposure-sentinel/secrets.enc - Docker secret:
/run/secrets/dehashed_api_key - Kubernetes secret: mounted file at
/etc/secrets/dehashed_api_key - AWS Secrets Manager:
/exposure-sentinel/dehashed_api_key - Azure Key Vault: stored as a Key Vault secret
- GCP Secret Manager:
projects/$PROJECT/secrets/dehashed_api_key - HashiCorp Vault: KV v2 at
secret/exposure-sentinel/dehashed_api_key
| Role | Can do |
|---|---|
admin |
Configure / replace / remove API keys, register domains, run scans, view everything |
analyst |
Run scans, view results, create incidents, manage authorized emails |
viewer |
Read-only access to dashboards and historical scan results |
Every sensitive operation records an immutable audit event with:
- Timestamp, actor (user ID + email + roles)
- Event type (
api_key.created,api_key.removed,scan.started,rbac.denied, etc.) - Outcome (
success/failure/denied) - For key events: masked key + truncated SHA-256 fingerprint (never the key value)
- Request ID, IP address, user agent (when available)
┌─────────────────────────────────────────┐
│ Analyst / Admin │
└────────┬────────────────────────┬───────┘
│ CLI (Typer + Rich) │ HTTPS (optional)
│ PRIMARY INTERFACE │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ exposure-sentinel │ │ React + TS frontend │
│ (Typer + Rich CLI) │ │ (optional server) │
└──────────┬───────────┘ └──────────┬───────────┘
│ │
▼ ▼
┌────────────────────────────────────────────┐
│ FastAPI backend │
│ ┌─────────┐ ┌──────────┐ ┌───────────┐ │
│ │ API │ │ Agent │ │ MCP │ │
│ │ routes │ │ route │ │ server │ │
│ └────┬────┘ └────┬─────┘ └─────┬─────┘ │
│ └────────────┴──────────────┘ │
│ Service layer │
│ ApiKeyService · DomainVerification │
│ ExposureScanner · SeverityEngine │
│ DehashedClient (ONLY caller of DeHashed) │
│ Redaction + RBAC + rate-limit + audit │
└───────────┬─────────────────────────────────┘
│
┌─────────────┼─────────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ Secrets │ │ Postgres │ │ Redis │
│ store │ │ / SQLite │ │ (rate-limit, │
│ (KEK- │ │ (audit, │ │ Celery) │
│ wrapped) │ │ scans) │ │ │
└────┬─────┘ └──────────┘ └──────────────┘
│
│ plaintext key lives in memory only during
│ a single outbound request to DeHashed
│
┌────┴──────┐ pluggable: local_encrypted | env | docker | k8s
│ KMS/Vault │ | aws_sm | azure_kv | gcp_sm | vault
└───────────┘
┌─────────────────────────────────────────┐
│ Integrations (outbound) │
│ Slack · Teams · Jira · ServiceNow │
│ Splunk · Microsoft Sentinel │
└─────────────────────────────────────────┘
- User → Frontend: HTTPS. Password field for API key. Key held in browser memory only until submitted, then dropped.
- Frontend → Backend: HTTPS. JWT bearer auth. RBAC enforced on every write endpoint.
- Backend → DeHashed: HTTPS. Bearer key fetched per-request from secrets store, never cached in module-level state.
- Backend → Secrets store: in-process. Plaintext key returned only to the
DehashedClientinstance for one request. - Backend → LLM agent (MCP): MCP handlers NEVER receive the key. Tool inputs are non-sensitive identifiers only. Outputs redacted + scope-filtered.
See docs/ARCHITECTURE.md for full details.
Select via EXPOSURE_SENTINEL_SECRETS_BACKEND env var:
| Backend | Use Case | Example Config |
|---|---|---|
local_encrypted |
Single-host / CLI (encrypted file at rest) | deploy/secrets/.env.local_encrypted.example |
env |
Dev only — never use in production | deploy/secrets/.env.env.example |
docker_secret |
Docker Swarm / Compose secrets | deploy/secrets/.env.docker_secret.example |
k8s_secret |
Kubernetes mounted secret | deploy/secrets/.env.k8s_secret.example |
aws_sm |
AWS Secrets Manager | deploy/secrets/.env.aws_sm.example |
azure_kv |
Azure Key Vault | deploy/secrets/.env.azure_kv.example |
gcp_sm |
Google Secret Manager | deploy/secrets/.env.gcp_sm.example |
vault |
HashiCorp Vault KV v2 | deploy/secrets/.env.vault.example |
Each integration is independently toggleable via env vars / secrets manager references.
| Integration | Env Vars | Auth Method |
|---|---|---|
| Slack | EXPOSURE_SENTINEL_SLACK_WEBHOOK_URL |
Incoming Webhook |
| Microsoft Teams | EXPOSURE_SENTINEL_TEAMS_WEBHOOK_URL |
Incoming Webhook |
| Jira | EXPOSURE_SENTINEL_JIRA_BASE_URL + token ref |
Bearer token |
| ServiceNow | EXPOSURE_SENTINEL_SERVICENOW_BASE_URL + token ref |
Bearer token |
| Splunk | EXPOSURE_SENTINEL_SPLUNK_HEC_URL + token ref |
HEC token |
| Microsoft Sentinel | EXPOSURE_SENTINEL_SENTINEL_WORKSPACE_ID + key ref |
HMAC-SHA256 shared key |
Test your integrations:
exposure-sentinel integrations list # Show which are configured
exposure-sentinel integrations test # Send a test alert# Generate a strong KEK and JWT secret
export EXPOSURE_SENTINEL_MASTER_KEK=$(python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())")
export EXPOSURE_SENTINEL_JWT_SECRET=$(openssl rand -hex 32)
# Start everything
docker compose up -dThe compose file starts: PostgreSQL, Redis, backend, Celery worker, Celery beat, and the frontend. See docker-compose.yml.
See deploy/k8s/deployment.yaml for a Deployment that mounts the DeHashed key as a Kubernetes secret and includes readiness/liveness probes that fail closed.
| Variable | Purpose |
|---|---|
EXPOSURE_SENTINEL_MASTER_KEK |
32-byte url-safe base64 Fernet key (KEK) |
EXPOSURE_SENTINEL_JWT_SECRET |
Long random secret for JWT signing |
EXPOSURE_SENTINEL_SECRETS_BACKEND |
One of the backends above |
EXPOSURE_SENTINEL_DATABASE_URL |
PostgreSQL async URL |
EXPOSURE_SENTINEL_REDIS_URL |
Redis URL (for rate limiting + Celery) |
EXPOSURE_SENTINEL_FAIL_CLOSED |
true (enforced in production anyway) |
In production, the readiness probe (/readyz) returns 503 if the database, secrets backend, or API key is unavailable. The app fails closed rather than running unprotected.
cd backend
pytest -q33/33 tests passing — unit + integration tests covering:
- Secrets store & envelope encryption (5 tests)
- Redaction layer (3 tests)
- Severity scoring engine (3 tests)
- DeHashed client query validation & error mapping (8 tests, mocked via
respx) - Audit log integrity (2 tests)
- Full onboarding flow integration (3 tests)
- CLI end-to-end smoke test (9 tests)
All DeHashed API calls in the test suite are mocked via respx. No real network calls are made. Production mode never simulates a successful API connection.
- 📐 Architecture — component diagram, trust boundaries, data model, lifecycle flows
- 🛡️ Threat Model — STRIDE-style analysis with mitigations
- 📚 OpenAPI spec — served at
/api/openapi.jsonwhen running in server mode - 🔧 Secure config examples — in
deploy/secrets/
- Fork the repository
- Create a feature branch:
git checkout -b feat/my-feature - Run tests:
cd backend && pytest -q - Run linters:
ruff check app && mypy app - Commit with conventional commits:
feat: ...,fix: ...,docs: ... - Open a pull request
Do NOT open a public GitHub issue for security vulnerabilities. Instead, email the maintainers privately. Include:
- Description of the issue
- Steps to reproduce
- Affected versions
- Suggested fix (if any)
Apache License 2.0 — see LICENSE.
Exposure Sentinel is designed for authorized organizational monitoring only. You must:
- Only search domains you own or have written authorization to monitor
- Only search emails of employees / contractors who have consented to monitoring per your organization's policies and applicable law (e.g., GDPR, CCPA, employment contracts)
- Comply with DeHashed's Terms of Service
- Comply with all applicable laws in your jurisdiction
The maintainers are not responsible for misuse of this tool.
Built with security as the first priority.