FalconEye
Free, self-hosted OSINT investigator's toolkit. 19 investigative tabs in one interface: crypto wallet tracing, phishing kit fingerprinting, domain intelligence, Telegram OSINT, IP reputation, email header forensics with LLM-powered scam detection, Google dork generation, suspicious script deobfuscation, URL expansion and redirect chain analysis, QR code decoding, company dossiers, reverse image search, username enumeration across 965 platforms, Have I Been Pwned breach checks, global + PH/SEA ransomware victim tracking, attack surface management for domains you control, phishing domain hunting for brand impersonation, traceroute mapping (Route Map), and a fictional sock-puppet persona generator with dossier export. Ten of the tabs are also exposed as MCP tools for Claude Code and Claude Desktop. The home page carries a Philippines-focused threat pulse and a curated news strip. The IP Reputation and Email Header tabs also compose abuse reports to the responsible provider (RDAP contact lookup, with optional Mailgun send).
Current version: 3.39.0
Live instance: falconeye.osintph.info
License: AGPL-3.0
Use FalconEye from Claude Code or Claude Desktop
FalconEye ships an MCP server, so ten of the tabs are available as tools inside Claude Code and Claude Desktop. Point it at your own instance and ask for a lookup instead of opening a browser tab.
claude mcp add falconeye -- /opt/falconeye/mcp-venv/bin/python -m app.mcp_server
It runs against your own instance, your own .env and your own API keys.
There is no hosted endpoint and no account: each tool calls the same route the
browser tab calls, in-process, so your rate limits, the SSRF guard and the
prompt-safety wrapping all apply unchanged, and the two LLM-backed tools spend
your Anthropic budget under the existing daily cap.
stdio transport only: no listener, no authentication, no key table, because the only caller is the user who launched the process. That also means it can drive a remote instance over SSH without opening a port. The person-centric tabs (username, phone, Telegram, reverse image search, sock puppet, dork generator) are deliberately not exposed.
Setup, the Claude Desktop config block and the SSH pattern: docs/mcp.md.
What it does
FalconEye is the workbench an OSINT investigator opens when a new lead arrives. Each tab does one thing well and connects to the others via one-click pivots, from "I have a wallet address" to "here are the related domains, email infrastructure, Telegram channel, and the script the phishing kit runs" without switching tools.
Tabs
LLM-powered tabs
Four tabs use Anthropic's Claude Haiku 4.5: Email Header, Dork Generator, Script Decoder, and Sock Puppet Generator (the Legend back-story). The model is hardcoded in each router (not a configuration variable) and protected by four cost safeguards (the Sock Puppet Legend is capped per IP per 24 hours, default 25, configurable via SOCKPUPPET_LLM_PER_DAY, since bulk legend generation is its abuse case):
- Anthropic Console spend limits at the API account level
- Per-feature, per-IP daily rate limits (10 calls per 24-hour rolling window), keyed on
CF-Connecting-IPso limits track real users behind Cloudflare, not edge nodes - Environment variable kill switches (
LLM_ANALYSIS_ENABLED,LLM_DORKGEN_ENABLED,LLM_DECODER_ENABLED) - Prompt caching on system prompts (~90% input cost discount on repeated calls within 5 minutes)
LLM JSON output is schema-validated and clamped before use: numeric fields are type-coerced and range-clamped, string fields are truncated, enum fields (severity, intent, risk_level) are validated against known values, and finding lists are filtered to allowed keys only. LLM output is presented in the UI as model opinion, not as verified verdict.
Typical cost per LLM call: ~$0.003.
Abuse Reporting
The IP Reputation and Email Header tabs can turn an identified piece of hostile infrastructure into an abuse report to the responsible provider. FalconEye resolves the abuse contact via RDAP, prefills a category-appropriate report from what it already knows, and offers two actions:
- Copy to Clipboard: always available, no configuration, no authentication. Paste into your own mail client and send from there.
- Send via Mailgun: optional. Gated behind admin HTTP Basic Auth, rate-limited per IP / per recipient / globally, and restricted to recipients FalconEye itself resolved via RDAP. Every send is written to an append-only audit log.
Compose-and-copy works out of the box. Enabling send requires reporter-identity and Mailgun environment variables plus an admin bcrypt hash. See docs/abuse-reporting.md for the full setup guide, security posture, and current Mailgun free-tier state.
Security posture
FalconEye is a public, unauthenticated OSINT tool with no login. The following controls are in place as of v3.39.0:
SSRF prevention (Phishing Scanner + URL Expander). All user-supplied URLs pass through the shared safe_fetch primitives before any HTTP request is made. safe_fetch resolves and validates every hop in a redirect chain independently against a complete blocklist: private/loopback/link-local/reserved/multicast/unspecified ranges (via the Python ipaddress stdlib), CGNAT (100.64.0.0/10), NAT64 (64:ff9b::/96), IPv4-mapped IPv6 (::ffff:a.b.c.d unwrapped before check), and the "this" network (0.0.0.0/8). The URL Expander re-runs this check (resolve_and_check) at the start of every hop and before its per-hop TLS grab, and rejects embedded userinfo; it does not add a second SSRF implementation. TLS certificate verification is enforced on all outbound fetches (verify=True). Response bodies are streamed and size-capped (10 MB by default, 2 MB per hop in the URL Expander) so a target cannot choose how much memory a fetch costs. Fixed-host API calls (Shodan, RDAP, Telegram, etc.) are not routed through safe_fetch as they are not SSRF surfaces.
Rate limiting. All per-IP limits, including LLM cost controls and phishing scanner, are keyed on get_client_ip(), which honours the CF-Connecting-IP header only when the direct TCP peer is inside a published Cloudflare range (app/utils/cloudflare_ips.py) or an operator-configured TRUSTED_PROXY_CIDRS network. From any other peer the header is ignored and the limit keys on the real peer address, so a caller that reaches the origin directly cannot rotate a header to mint fresh counters. Header values that are not well-formed IP addresses are ignored rather than becoming their own bucket. This is deliberately independent of the nginx allowlist below: the limits hold even if the origin is exposed. The fallback for local development (no trusted peer, no header) is request.client.host.
XSS. Attacker-controlled strings from Telegram channel metadata, RDAP registration fields, RSS feeds, and threat intelligence APIs are escaped with escapeHtml() / escapeAttr() before any DOM insertion. The existing escape helpers are used consistently; no innerHTML is assigned with unescaped external data.
Reputation coverage is stated, not assumed. The IP Reputation verdict is
only CLEAN when all four keyed sources (AbuseIPDB, VirusTotal, AlienVault OTX and ThreatFox) actually answered.
Censys is enrichment only, behind CENSYS_ENABLED (off by default, credit-metered),
and never counts towards the verdict or the coverage note. A source with no
key, an error, a timeout or an exhausted quota is reported as unavailable and
the verdict becomes INCOMPLETE, naming which source was not consulted. Before
v3.33.2 such a source was indistinguishable from one that answered and found
nothing, so the card could assert "No source flagged this IP" when nothing had
been asked. Every verdict now carries an "N of M reputation sources responded"
note, and the tab warns before a lookup when the instance has no keys at all.
Optional third-party source: Hudson Rock. Off by default
(HUDSONROCK_ENABLED). When on, Domain Intel and the Email Header risk
assessment gain infostealer exposure data. Only stealer family names,
compromise dates and counts are ever rendered: the client reduces every upstream
response to an allowlist of safe fields, because Hudson Rock's schema also
carries plaintext credentials, session cookies and victims' search history.
Enabling it forwards your visitors' lookups to a third party, and the endpoints
publish no rate limit and no terms of use, so it is treated as best effort. See
"Hudson Rock" in docs/deploy-runbook.md.
MCP server (optional, local mode). The server described under "Use FalconEye from Claude Code or Claude Desktop" above is not part of the web service and must not be exposed on a socket or a tunnel: stdio means the only caller is the user who launched the process, which is also why it carries no authentication. Its tools call the same routes the browser tabs call, so the rate limits, the SSRF guard and the prompt-safety wrapping apply unchanged, and nothing in it can be reached from the public instance. See docs/mcp.md.
Security headers. The nginx server block sets: Content-Security-Policy, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer, and Strict-Transport-Security with a one-year max-age. The CSP retains script-src 'unsafe-inline' for now because the frontend uses inline event handlers; removing it requires a frontend refactor to addEventListener bindings. script-src also allows cdn.tailwindcss.com for Tailwind and cdnjs.cloudflare.com for D3. These live in nginx/snippets/security-headers.conf, which is included at server level and inside every location that sets an add_header of its own. nginx does not merge add_header across levels: a location defining any header of its own inherits none of the server-level ones, which silently stripped CSP, HSTS and nosniff from /static/ and the favicon routes until v3.32.2. tests/unit/test_nginx_config.py fails if a location reintroduces that.
Error isolation. Exception strings from httpx and upstream APIs are logged server-side with log.exception and never echoed to the client. Client responses get generic messages only ("Upstream service unavailable.").
Prompt injection. Both LLM endpoints analyse input that is hostile by definition: obfuscated malware samples, scam email bodies. Every attacker-controlled value (the script decoder's code and hint, the email analyser's sender_email and body) is wrapped in <untrusted_data> fences by app/utils/prompt_safety.py before it reaches Claude, with any fence tag stripped from the value first so a sample cannot close its own fence and continue as operator framing. Both system prompts carry a trust-boundary instruction stating that fenced content is data to analyse and never an instruction to follow, and that an attempt to influence the verdict is itself an indicator to report. Untrusted input is never interpolated raw into a prompt.
LLM output validation. Parsed LLM JSON is validated before any field is used in application logic. clamp_int, safe_str, and validate_findings_list in app/utils/llm_response.py prevent type errors from malformed model output reaching max(), for loops, or string operations. Free-text fields are additionally sanitised by sanitize_llm_text (control and ANSI sequences stripped, echoed fence markers removed, length clamped) before they are returned to the browser, so injected content cannot smuggle escapes or forged framing into the UI. severity and intent remain allowlist-validated, so an injected verdict cannot survive parsing.
Input validation. All SQL uses parameterized queries. The only subprocess is whois in list-arg form over a strictly validated domain. No eval, exec, pickle, yaml.load, or shell=True anywhere in the codebase.
Origin protection. nginx only accepts inbound connections from Cloudflare IP ranges (IPv4 and IPv6), via nginx/snippets/cloudflare-origin-allow.conf. It is a separate file so a deployment that is not behind Cloudflare can drop one include rather than edit the vhost; see docs/deploy-runbook.md. The gunicorn listener binds to 127.0.0.1:8000 only, and pins --forwarded-allow-ips 127.0.0.1 so uvicorn trusts X-Forwarded-For from the local nginx alone. With * it would take the caller-supplied left-most entry and the peer address that Rate limiting depends on would become attacker-chosen. The same range list is enforced a second time in the application (see Rate limiting), so the two must be kept in step: a unit test fails if nginx/snippets/cloudflare-origin-allow.conf and app/utils/cloudflare_ips.py drift apart, or if the vhost stops including the snippet.
Unauthenticated endpoints. GET /api/scanner/history is public and returns an explicit column allowlist from phishing_scans. telegram_bot_id (the live bot token extracted from a kit's exfiltration call) is never in that list; it is returned only to the caller who submitted the scan that found it. Any column added to the table later is withheld until it is listed on purpose.
Stack
- Backend: Python 3.10+ (3.11 recommended), FastAPI, Uvicorn, Gunicorn
- Database: SQLite with WAL mode (caching and rate limits; no user data); Redis for the Company and Image tab response caches and the Route Map job handoff
- Frontend: Tailwind CSS via CDN, vanilla JavaScript, D3.js for graph visualizations
- Web server: nginx with Cloudflare Origin Certificate; Cloudflare in front for TLS termination, DDoS protection, and edge caching
- LLM: Anthropic Claude Haiku 4.5 via the official Python SDK (
anthropicpackage, install separately, see requirements.txt note) - Security: CrowdSec community blocklist + firewall bouncer, fail2ban, ufw, SSH on non-standard port with keys-only auth
Memory footprint at idle, measured on the live instance on 2026-10-18: about 555 MB RSS across the four gunicorn processes two days after a restart (10:22 UTC, load average 0.15; master 30 MB, three workers of 170, 172 and 184 MB; systemd reported 528 MB for the whole service), and 441 MB six minutes after one (10:37 UTC; workers of 136 to 138 MB), so workers grow by about a quarter over two days. RSS counts shared pages once per process. Redis adds 13 MB and nginx 27 MB (master and two workers). Disk: 13 MB for the code tree (28 MB with .git) and 19 MB for data/falconeye.db.
Self-hosting
Prerequisites
- Ubuntu 22.04 or 24.04 VPS: 1 vCPU, 2 GB RAM minimum, 4 GB if the box also
runs host security agents (an EDR, CrowdSec, Wazuh). 1 GB is not enough: the
service, Redis, nginx and a stock Ubuntu server already need about 1 GB at idle
with no swap. The measured figures and the arithmetic are in "Sizing" in
docs/deploy-runbook.md;
provision.shandupgrade.shwarn on a box with less than 2 GB. - A domain name pointed at the VPS
- Python 3.10 or later (3.11+ recommended)
- System packages beyond Python.
pipdoes not install these, and two of them fail only at runtime:
This list now lives in one place,sudo apt-get install -y --no-install-recommends \ python3 python3-pip python3-venv git redis-server libzbar0 whoisFE_SYSTEM_PACKAGESinscripts/lib/common.sh, and bothprovision.shandupgrade.shinstall from it, so a package added for a new feature reaches existing boxes too.libzbar0is required:pyzbarloads it via ctypes for the QR Code tab, and because that router is imported at module level, without it the whole app fails to start withImportError: Unable to find zbar shared library. On Ubuntu 24.04 the package is reallylibzbar0t64, but it provideslibzbar0, so this name works on 22.04 and 24.04.whoisis used as a subprocess by the Domain Intel tab and fails silently when absent. See docs/deploy-runbook.md ("Prerequisites: system packages") for the full audit. - Optional: Cloudflare account for TLS termination and DDoS protection. The shipped nginx config assumes it. Deploying behind another proxy, or none, needs the steps in docs/deploy-runbook.md ("Deploying without Cloudflare"), which also covers AWS and Cloudflare Tunnel.
- Optional: Anthropic API key for LLM-powered tabs; the rest of the tool runs without it
- If you self-host publicly, set the operator identity. Out of the box the
page carries the upstream operator's name, inbox, blog and privacy policy.
Until v3.39.0 an unset
CONTACT_FORM_ACTIONalso posted your visitors' messages to their Formspree form; it now defaults to empty and the form is not rendered until you set your ownCONTACT_FORM_ACTION. SetOPERATOR_NAME,OPERATOR_URL,OPERATOR_CONTACT_EMAIL,OPERATOR_PRIVACY_EMAILandSITE_ORIGIN, or setCONTACT_ENABLED=falseto remove the Contact page entirely (nav entry gone, panel gone from the HTML,/contactreturns 404). See "Operator identity" in docs/deploy-runbook.md. The AGPL notice and the link to this repository are not affected by any of these: that is the licence.
Quick install (automated)
sudo bash scripts/provision.sh
The provision script:
- Creates
/opt/falconeye/{data,venv}with correct ownership - Clones the repository to
/opt/falconeye/app_src - Creates a virtualenv and installs dependencies from
requirements.txt - Initializes the SQLite database
- Installs and enables the systemd service
- Installs the nginx vhost config
After provisioning, copy and configure the environment file:
sudo cp /opt/falconeye/app_src/.env.example /opt/falconeye/.env
sudo vi /opt/falconeye/.env # fill in your API keys
sudo chmod 600 /opt/falconeye/.env
sudo systemctl restart falconeye
Manual install
# 0. System packages (pip does not install these; libzbar0 is required or the
# app will not import at all)
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
python3 python3-pip python3-venv git redis-server libzbar0 whois
# 1. Clone
sudo mkdir -p /opt/falconeye
sudo chown $USER:$USER /opt/falconeye
cd /opt/falconeye
git clone https://github.com/osintph/falconeye.git app_src
cd app_src
# 2. Create venv and install deps
python3 -m venv /opt/falconeye/venv
/opt/falconeye/venv/bin/pip install -r requirements.txt
# Note: anthropic and extract-msg are not in requirements.txt (see below).
# Install them separately if you want LLM tabs or .msg file upload:
# /opt/falconeye/venv/bin/pip install "anthropic>=0.25" "extract-msg>=0.28"
# 3. Configure environment
cp .env.example /opt/falconeye/.env
vi /opt/falconeye/.env # set ANTHROPIC_API_KEY, IMAGE_UPLOAD_SECRET, etc.
chmod 600 /opt/falconeye/.env
# 4. Initialize the database
FALCONEYE_DB=/opt/falconeye/data/falconeye.db \
/opt/falconeye/venv/bin/python scripts/db_init.py
# 5. Smoke test before enabling anything. A missing native library fails here,
# not in a gunicorn restart loop where the traceback scrolls past.
/opt/falconeye/venv/bin/python -c "import app.main" && echo "imports OK"
# 6. Install systemd unit.
# The unit declares LogsDirectory=falconeye, so systemd creates
# /var/log/falconeye owned by the service user before gunicorn starts. Create it
# by hand too if you intend to run gunicorn directly before installing the unit;
# without it gunicorn exits with PermissionError on the error log.
sudo install -d -o ubuntu -g ubuntu -m 0755 /var/log/falconeye
sudo cp falconeye.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now falconeye
# 7. Install nginx vhost.
# Copy the snippets FIRST: the vhost includes them, so in the other order
# `nginx -t` fails and the reload is refused.
sudo mkdir -p /etc/nginx/snippets
sudo cp nginx/snippets/*.conf /etc/nginx/snippets/
sudo cp nginx/falconeye.conf /etc/nginx/sites-available/falconeye
sudo ln -sf /etc/nginx/sites-available/falconeye /etc/nginx/sites-enabled/falconeye
sudo nginx -t && sudo systemctl reload nginx
# Not behind Cloudflare? The vhost denies every request until you remove or
# replace the origin allow snippet. See "Deploying without Cloudflare" in
# docs/deploy-runbook.md before going live.
Upgrading
One command, on the box:
sudo /opt/falconeye/app_src/scripts/upgrade.sh v3.34.4 # a specific tag
sudo /opt/falconeye/app_src/scripts/upgrade.sh # the newest tag
sudo /opt/falconeye/app_src/scripts/upgrade.sh --dry-run # show, change nothing
It backs up .env, the database and the installed systemd unit to
/opt/falconeye/backups/<timestamp>/, checks out the tag, reinstalls
dependencies only if requirements.txt changed, copies the systemd unit and
reloads systemd if the unit changed, updates the nginx snippets (and any
conf.d file you already had), generates ASM_SIGNING_KEY and FE_HASH_KEY
into .env if either is missing, applies whatever migrations or cache flushes the
release declares, installs the 15-minute retention sweep timer, the gunicorn
logrotate file and the journald retention drop-in (all from deploy/, the same
functions provision.sh uses), restarts the service and verifies /health
reports the new version. If it does not, it fails with the rollback command already filled in.
It never overwrites your vhost. server_name, your certificate paths and any
rate-limit zones live in /etc/nginx/sites-available/falconeye, so when the
shipped vhost changes the script prints the diff command instead of clobbering
yours. It never touches .env.
Rolling back is the same command with the previous tag. The steps it performs are written out in "Appendix: what upgrade.sh does" in docs/deploy-runbook.md, for the day you have to do one of them by hand.
State to persist
Everything in this table is state git reset --hard cannot put back. The list
lives in fe_state_paths() in scripts/lib/common.sh; this table is generated
from it and a test fails if the two disagree.
v3.37.0 (Attack Surface Management) adds nothing to this list. Its state is a set of
asm_*tables insidedata/falconeye.db, which is already the first essential entry below, plus the operator key hash and TOTP secret and the ASM signing key, which live in.env, also already listed. Theasm_*tables are:asm_domains,asm_verifications,asm_grants,asm_scans,asm_hosts,asm_findings,asm_snapshots,asm_operator_sessions,asm_operator_auditandasm_rate_limit.
Essential means losing it loses something this repository plus a working afternoon cannot recreate. Convenience means it can be rebuilt, at the cost of history or of an hour.
Two things that are not on the list, deliberately:
/opt/falconeye/venvand/opt/falconeye/mcp-venvrebuild fromrequirements.txt. Back up the pin file, not the tree./opt/falconeye/data/image_temp/and/opt/falconeye/data/prospect/are scratch space the app cleans up after itself.
Keep anything else of your own (private ops scripts, a second watchlist) in
FE_BACKUP_EXTRA, a space-separated path list the backup includes and neither
script ever writes to.
Back up and restore
sudo /opt/falconeye/app_src/scripts/backup.sh # -> /opt/falconeye/backups/
sudo /opt/falconeye/app_src/scripts/backup.sh --out /mnt/x # somewhere else
sudo /opt/falconeye/app_src/scripts/backup.sh --dry-run # list, write nothing
sudo /opt/falconeye/app_src/scripts/restore.sh /path/to/falconeye-state-<stamp>.tar.gz
The backup writes falconeye-state-<timestamp>.tar.gz at mode 0600 with a
.sha256 sidecar, and prints the checksum. The archive holds the API keys and
the Cloudflare Origin CA private key, so copy it the way you would copy .env.
The restore verifies that checksum, refuses any archive carrying a path outside
the list above (it runs as root, and tar writes what it is told), stops the
service before touching the database, keeps whatever it overwrites in
/opt/falconeye/backups/pre-restore-<timestamp>/, then starts the service and
checks /health. It does not touch the checkout: restoring state onto a newer
release is the normal case, and scripts/upgrade.sh is what moves the code.
Ephemeral root disks
On a platform where the root filesystem is rebuilt on every deploy (container hosts, image-based VMs, some managed platforms), none of the paths above survive unless you put them somewhere that does:
- Mount a persistent volume at
/opt/falconeye. That covers.env, both databases, the Telegram session and the watchlist in one mount, which is why they all live under one prefix. PointFALCONEYE_DBandTELEGRAM_SESSION_PATHat the mounted paths if your layout differs. /etc/ssl/falconeyeand the nginx vhost belong in the platform's config store, not on the disk: a secret store or config map for the certificate and key, the vhost rendered at boot from your own template. They are the two essential paths that are not under/opt/falconeye, and the two most easily forgotten, because nothing fails until the next rebuild.- If your vhost names
goaccess_cf, shipconf.d/goaccess-logformat.confwith it. nginx refuses to start on an undefinedlog_format, so the pair has to land together.
A restore onto a fresh box is therefore: provision, mount or restore the volume,
put the certificate and vhost back, then scripts/restore.sh for the rest.
Required and optional API keys
If a key is missing, the relevant feature degrades gracefully (returns an error card) without crashing the rest of the tool.
API endpoints
All write endpoints accept JSON. Rate limits are enforced per real client IP (CF-Connecting-IP behind Cloudflare, request.client.host otherwise).
The per-minute limits are counted in memory; the hourly and daily caps are stored in SQLite with the caller's IP.
OpenAPI docs are disabled in production (FALCONEYE_PUBLIC_DOCS=false by default).
Development
cd /path/to/falconeye
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pip install "anthropic>=0.25" "extract-msg>=0.28" # if you need LLM tabs locally
# Run the dev server with hot reload
uvicorn app.main:app --reload --host 127.0.0.1 --port 8000
Then open http://127.0.0.1:8000/. Hot reload picks up Python and static file changes.
Running tests
source .venv/bin/activate
pytest tests/ -v
Expected baseline on v3.39.0 (Python 3.12, full suite, nothing ignored): 1857 passed, 13 skipped. The 13 skips need material that is not in the repository: two JPEG EXIF fixtures (tests/image_search/test_exif.py), nine tests against real kit bundles (set FALCONEYE_KIT_BUNDLES), and two live smoke tests (set FALCONEYE_LIVE_SMOKE=1).
Project structure
falconeye/
├── app/
│ ├── main.py # FastAPI entry point, router registration
│ ├── sweep.py # The 15-minute retention sweep (falconeye-sweep.timer)
│ ├── mcp_server.py # MCP tools over stdio, local mode, see docs/mcp.md
│ ├── config.py # Environment variable loading
│ ├── database.py
│ ├── routers/ # One file per tab / feature (single-file tabs)
│ │ ├── crypto.py
│ │ ├── scanner.py # Phishing Scanner: uses safe_fetch for user URLs
│ │ ├── domain_intel.py
│ │ ├── ip_intel.py
│ │ ├── sandbox.py
│ │ ├── email_header.py
│ │ ├── dork_generator.py
│ │ ├── script_decoder.py
│ │ ├── url_expander.py
│ │ ├── qr_analyzer.py
│ │ ├── sockpuppet.py # Sock Puppet Generator (Cover + Claude Haiku Legend)
│ │ ├── asm.py # Attack Surface routes and pages
│ │ ├── hunt.py # Phishing Hunt routes
│ │ ├── news.py
│ │ └── threat_pulse.py
│ ├── scanner/ # Phishing Scanner analysis engine, see docs/kit-analysis.md
│ │ ├── kit_analyzer.py # Deobfuscates string-obfuscated bundles; decodes the
│ │ │ # table and resolves call sites BEFORE searching
│ │ ├── rabbithunt_sig.py # Per-kit signature records + transparent weighted scorer
│ │ ├── kit_acquire.py # Read-only acquisition, every request via safe_fetch
│ │ ├── kit_report.py # Consolidated case report builder
│ │ ├── cloudflare_detect.py
│ │ └── ph_bank_indicators.py
│ ├── asm/ # Attack Surface: verification, passive discovery, probe,
│ │ # port scan, findings, one-time PDF report, operator mode
│ ├── hunt/ # Phishing Hunt: candidate discovery (CT, permutations,
│ │ # urlscan, URLhaus, NRD store), scoring, clustering
│ ├── routemap/ # Route Map: trace parsing, Hoiho + site-code placement,
│ │ # RIPE Atlas probes, speed-of-light check
│ ├── ip_sources/ # IP Reputation upstreams; catalog.py is the source list
│ ├── telegram/ # Telegram Inspector (t.me scrape, Bot API, MTProto)
│ ├── username/ # Username enumeration (WhatsMyName + Sherlock)
│ ├── breach/ # Breach Check (Have I Been Pwned)
│ ├── abuse/ # Abuse report compose and optional Mailgun send
│ ├── prospect/ # Company tab (SearchAPI.io dossier)
│ ├── image_search/ # Image Search tab (Google Lens + Yandex)
│ ├── ransomware/ # Ransomware Watch tab: reads local SQLite only
│ ├── holehe/, hudsonrock/ # Optional third-party clients
│ ├── pdf/ # Server-side PDF rendering
│ ├── data/ # Vendored WhatsMyName and Sherlock site data
│ ├── collectors/
│ │ └── ransomware_collect.py # Scheduled collector (ransomware.live + RansomLook); run by an out-of-repo systemd timer, see docs/ransomware-watch-runbook.md
│ └── utils/
│ ├── client_ip.py # CF-Connecting-IP extraction
│ ├── cloudflare_ips.py # Published Cloudflare ranges, kept in step with nginx
│ ├── safe_fetch.py # SSRF-safe HTTP fetcher for user-supplied URLs
│ ├── llm_response.py # LLM JSON validation helpers
│ ├── prompt_safety.py # <untrusted_data> fencing for LLM input
│ ├── fehash.py # HMAC-SHA256 (FE_HASH_KEY) for kept run values
│ ├── domain.py
│ └── indicator.py
├── app/static/ # Single-page app shell (index.html, app.js, etc.)
├── scripts/
│ ├── provision.sh # Automated VPS provisioning
│ ├── upgrade.sh # Tag-based upgrade with backup, health check, rollback hint
│ ├── backup.sh # State backup (fe_state_paths) to a checksummed tarball
│ ├── restore.sh # Verified restore of a backup.sh archive
│ ├── lib/
│ │ └── common.sh # Shared by provision.sh and upgrade.sh (packages, units, nginx)
│ ├── release-actions/ # Per-release migrations and cache flushes
│ ├── migrate_3_39_0.py # Data migration run by release-actions/3.39.0.sh
│ └── db_init.py # SQLite schema initializer
├── deploy/ # System files, every one installed by both scripts
│ ├── falconeye-asm-monitor.service / .timer
│ ├── falconeye-nrd.service / .timer # Phishing Hunt NRD feed, daily
│ ├── falconeye-sweep.service / .timer # Retention sweep (app/sweep.py), every 15 minutes
│ ├── logrotate/
│ │ └── falconeye # gunicorn logs: daily, 14 kept
│ └── journald/
│ └── falconeye-retention.conf # Journal kept 14 days
├── tools/ # Standalone CLI analysis tools (not imported by the app)
│ ├── kitanalyze.py # Reference deobfuscator the tab's engine was ported from
│ ├── runkit.sh # Reference acquisition sequence (raw curl; the app uses safe_fetch)
│ ├── decode_kit.py # Minimal string-table decoder
│ └── kitdecrypt.py # Decrypts captured kit blobs; CLI only, deliberately
│ # never wired into an endpoint. Needs `cryptography`.
├── nginx/
│ ├── falconeye.conf # nginx vhost
│ ├── snippets/
│ │ ├── cloudflare-origin-allow.conf # Origin lock; drop it if not behind Cloudflare
│ │ └── security-headers.conf # CSP/HSTS/nosniff, included per location
│ └── conf.d/
│ └── goaccess-logformat.conf # Optional GoAccess log format
├── docs/ # Runbook, MCP, IP sources, kit analysis and more
├── falconeye.service # systemd unit
├── requirements.txt
├── requirements-mcp.txt # MCP venv overrides, see docs/mcp.md
├── .env.example
├── LICENSE
├── CONTRIBUTING.md
└── README.md
Adding a new tab
- Create
app/routers/your_tab.pywith a FastAPI router - Register it in
app/main.py - Add the tab button and content section in
app/static/index.html - Add the JS handler in
app/static/app.js - Add
'your_tab'to theVALID_TABSarray for hash routing - Update this README's tab table and the endpoint list above
If the tab fetches a user-supplied URL, route it through safe_fetch in app/utils/safe_fetch.py. Do not use httpx.AsyncClient directly on user input.
If the tab uses an LLM, follow the pattern in script_decoder.py: hardcoded model constant, per-feature rate-limit table keyed on CF-Connecting-IP, environment kill switch, prompt caching, and output validation via app/utils/llm_response.py.
Privacy posture
FalconEye does not maintain user accounts.
- Results are cached for between 30 minutes and 30 days depending on the tab (most for 6 to 24 hours). Email Header, Dork Generator, Script Decoder and Breach Check email lookups are keyed by a SHA-256 hash of the input; the other caches are keyed by the input itself. Expired rows stay until the same lookup overwrites them.
- The Company tab saves each dossier it builds, with the domain and the time and nothing about the caller, and the sweep deletes it after
PROSPECT_RETENTION_DAYS(default 30). - Attack Surface and Phishing Hunt results are deleted when the PDF download completes, or otherwise by the 15-minute sweep timer once an hour old (at most about 75 minutes after the run). Each run leaves a record and an audit row in which the domain, or the Hunt keyword and legitimate domain, is stored only as HMAC-SHA256 under
FE_HASH_KEY. Revoking an Attack Surface grant deletes every record held for the domain. - Uploaded .eml, .msg and QR files are not kept; a .msg file and any upload over 1 MB pass through a temporary file deleted after parsing. Image Search uploads are kept on disk for about 15 minutes so SearchAPI can fetch them.
- Source IP is stored for rate limiting, 48 hours for most features (30 days for Attack Surface and Phishing Hunt), pruned the next time that feature records a call.
- Request logs (nginx, kept 14 days by the distribution's logrotate on the live instance; gunicorn's own logs, rotated daily and kept 14 days by
deploy/logrotate/falconeye) contain the looked-up value for tabs that put it in the URL. Route Map coordinates never travel in a URL. - Functional cookies only:
fe_theme,fe_sidebar_collapsed, and for Attack Surfaceasm_grantand the operator'sasm_operator. Cloudflare sets its own security cookies. - No third-party advertising or analytics
The in-app privacy policy is the authoritative statement and lists every third party per tab.
When you use the LLM-powered tabs, your input is sent to Anthropic's API. See Anthropic's privacy policy. At time of writing, Anthropic does not train on API input by default.
Roadmap
- IoC enrichment pipeline (one URL in, full report from all tabs out)
- Hash-based artifact deduplication across investigation history
- Maltego transform export for the crypto graph
- Public API tokens for trusted integrations
Contributing
See CONTRIBUTING.md.
Bug reports and feature requests go to the Contact tab or as a GitHub issue.
License
AGPL-3.0. Strong copyleft: if you run a modified version as a network service, you must offer the modified source to users of that service. See LICENSE.
Acknowledgments
- abuse.ch (URLhaus, MalwareBazaar)
- GreyNoise (IP intelligence)
- Shodan InternetDB (free, no-key endpoint)
- crt.sh, Cert Spotter (certificate transparency)
- RDAP.org (RDAP queries)
- RIPEstat (ASN data)
- Blockstream, BlockCypher, TronGrid (blockchain APIs)
- Anthropic Claude Haiku 4.5 (LLM analysis)
- D3.js (graph visualization)
- Tailwind CSS (styling)
Built and maintained by OSINTPH.
- City list from GeoNames (
cities15000), used under CC BY 4.0. Bundled offline in the routemap-engine package for the Route Map origin picker. - Router hostname geolocation from CAIDA Hoiho, the published ruleset behind the Route Map tab's hostname-first placement.
- Carrier site codes derived from Arelion's public looking glass.
The table ships in the routemap package; see its
routemap/engine/data/README.mdto regenerate it or add a carrier. - Traceroute measurements from RIPE Atlas. Measurements created by this tool are public.