SpiderFoot's core is an event-driven pipeline:
- User provides a target (domain, IP, email, etc.)
- The scan engine creates a root event (
ROOT) - Modules that watch for ROOT produce new events (e.g.,
DOMAIN_NAME) - Other modules consume those events and produce more (e.g.,
INTERNET_NAME,IP_ADDRESS) - This cascades until no module produces new events
Events are typed strings. Key event types:
INTERNET_NAME,DOMAIN_NAME,IP_ADDRESS— core discoveryTCP_PORT_OPEN,UDP_PORT_OPEN— port scanningEMAILADDR,EMAILADDR_COMPROMISED,USERNAME— identityVULNERABILITY_CVE_CRITICAL/HIGH/MEDIUM/LOW— CVEsMALICIOUS_IPADDR,BLACKLISTED_IPADDR— reputationLEAKSITE_CONTENT,DARKNET_MENTION_CONTENT— breach/darknetRAW_RIR_DATA— raw data for archival
- API modules (e.g.,
sfp_shodan.py): Query external REST APIs - Tool wrappers (e.g.,
sfp_tool_nmap.py): Shell out to CLI tools, parse output - Internal modules (e.g.,
sfp_dnsresolve.py): Local analysis, no external calls - Feed modules (e.g.,
sfp_c2tracker.py): Download and cross-reference threat feeds
SQLite via spiderfoot/db.py. Key tables:
tbl_scan_instance— scan metadata (id, target, status, timestamps)tbl_scan_results— all events (type, data, module, hash, parent_hash)tbl_scan_config— per-scan module configurationtbl_scan_correlation_results— correlation rule matchestbl_config— global settings (API keys, proxy, etc.)
app.py: Factory pattern, registers 4 blueprints (+ 1 compat re-registration)blueprints/api.py: REST API (scan lifecycle, data export, config, presets)blueprints/ui.py: HTML pages (dashboard, newscan, scaninfo, settings)blueprints/fragments.py: HTMX fragments (tab content, table rows, settings sections)blueprints/ai.py: AI summarization SSE endpoints (scan summary, correlation explain)
All API endpoints are dual-registered at /api/* and /* for backwards compatibility.
- Tailwind CSS: PostCSS build via
tailwind.config.js, definessf.*color tokens. Rebuild withnpx tailwindcss -i static/css/tailwind-input.css -o static/css/tailwind.css - HTMX: Fragment-driven updates (scan table polling, tab switching, search/filter)
- Alpine.js: Reactive components (scan form, theme toggle, expandable rows)
- Scan table polls every 5s:
hx-trigger="every 5s" - Tab switching:
hx-get="/frag/results-tab?id=X&tab=Y"→hx-target="#tab-content" - Search/filter:
hx-trigger="keyup changed delay:300ms"withhx-includefor multi-field - Pagination:
hx-get="/frag/events?id=X&page=N"→hx-target="#event-rows"
Defined in services/event_service.py as EVENT_CATEGORIES:
- Attack Surface: INTERNET_NAME, IP_ADDRESS, TCP_PORT_OPEN, DOMAIN_NAME
- Identities & Exposure: EMAILADDR, USERNAME, SOCIAL_MEDIA, LEAKSITE_CONTENT
- Infrastructure: WEBSERVER_TECHNOLOGY, SSL_CERTIFICATE, BGP_AS_MEMBER, GEOINFO
- Reputation: MALICIOUS_IPADDR, BLACKLISTED_*, flagged affiliates
- Vulnerabilities: VULNERABILITY_CVE_*, VULNERABILITY_GENERAL
class sfp_example(SpiderFootPlugin):
meta = {
"name": "Example Service",
"summary": "What it does in one line.",
"flags": ["apikey"], # or [] for no-auth
"useCases": ["Footprint", "Investigate", "Passive"],
"categories": ["Search Engines"],
"dataSource": {
"website": "https://example.com/",
"model": "FREE_AUTH_LIMITED", # or FREE_NOAUTH_UNLIMITED, COMMERCIAL_ONLY
"references": ["https://docs.example.com/"],
"apiKeyInstructions": ["Visit...", "Sign up...", "Copy key..."],
"description": "...",
},
}
opts = {"api_key": "", "request_delay": 1.0}
optdescs = {"api_key": "Example API key.", "request_delay": "Delay between requests."}
results = None
errorState = False
def setup(self, sfc, userOpts=dict()):
self.sf = sfc
self.results = self.tempStorage()
for opt in list(userOpts.keys()):
self.opts[opt] = userOpts[opt]
def watchedEvents(self):
return ["IP_ADDRESS", "DOMAIN_NAME"]
def producedEvents(self):
return ["INTERNET_NAME", "RAW_RIR_DATA"]
def handleEvent(self, event):
if self.errorState:
return
if event.data in self.results:
return
self.results[event.data] = True
# Query API using self.sf.fetchUrl()
res = self.sf.fetchUrl(url, headers=headers, timeout=self.opts["_fetchtimeout"])
# Parse response, emit events
e = SpiderFootEvent("INTERNET_NAME", hostname, self.__name__, event)
self.notifyListeners(e)| Method | Endpoint | Purpose |
|---|---|---|
| POST | /api/startscan |
Start scan (params: scanname, scantarget, modulelist) |
| POST | /api/stopscan?id=X |
Stop running scan |
| POST | /api/scandelete?id=X |
Delete scan and data |
| GET | /api/scanlist |
List all scans |
| GET | /api/scanstatus?id=X |
Scan status + risk matrix |
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /api/scaneventresults?id=X |
All events for a scan |
| GET | /api/scansummary?id=X&by=type |
Summary grouped by type or module |
| GET | /api/search?id=X&value=Y |
Search events (supports regex) |
| GET | /api/scanexportjsonmulti?ids=X,Y |
Export as JSON |
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /api/optsexport |
Export config as CFG file |
| POST | /api/savesettings |
Import config (JSON or CFG file) |
| GET | /api/modules |
List all available modules |
- FREE_NOAUTH_UNLIMITED: ~35 modules
- FREE_NOAUTH_LIMITED: ~11 modules
- FREE_AUTH_LIMITED: ~55 modules
- FREE_AUTH_UNLIMITED: ~15 modules
- COMMERCIAL_ONLY: ~10 modules
- Internal (no API): ~40 modules
- Tool wrappers: 17 modules
Shodan InternetDB, LeakCheck, Hudson Rock, Criminal IP, Netlas.io, Validin, OpenSanctions, WhoisXML API, BeVigil, Postman, ZoomEye, FOFA, Snusbase, BBOT (4 wrappers), User Scanner, MISP, RansomLook, C2-Tracker, Vulners
Ransomware.live — leak-site victim lookup via the free v2 API. Adds the RANSOMWARE_VICTIM event type. Watches DOMAIN_NAME / COMPANY_NAME by default (configurable allowlist), 24h response cache, 65s inter-call throttle to respect the free 1 req/min limit.
STARTING → RUNNING → FINISHED
→ ABORT-REQUESTED → ABORTED
→ ERROR-FAILED
POST /api/startscancreates a scan instance in SQLite, spawns aProcess(not thread)- The scan process loads selected modules, creates event queues
- Root event is injected, modules consume/produce events via
handleEvent() - Each module has its own
incomingEventQueueandoutgoingEventQueue - Modules call
self.checkForStop()to poll for abort requests - When all queues are empty, scan transitions to
FINISHED
_fetchtimeout(default 5s) only applies to individual HTTP requests viafetchUrl()- There is NO timeout on module execution itself — a module iterating a /16 netblock or running BBOT can run for hours
- Workaround: manually
POST /api/stopscan?id=Xto setABORT-REQUESTED
- If the scan process dies abnormally (Docker restart, OOM, crash) the DB row stays
RUNNING/STARTING/ABORT-REQUESTEDindefinitely start_web_servercallsSpiderFootDb.scanInstanceReconcileZombies()once beforecreate_app(), rewriting all such rows toABORTEDwith the current timestamp onended- The reconciler is intentionally NOT in
SpiderFootDb.__init__— scan workers spawn their own DB handle and would mark their own just-started scan as a zombie
GET /api/optsexport→ text/plain CFG file, format:modulename:optionname=valueper linePOST /api/savesettings→ accepts JSON (alloptsparam) or CFG file upload (configFileparam)- Requires CSRF token from
GET /api/optsraw(returned in response) configSerialize()andconfigUnserialize()insflib.pyhandle the conversion- Both methods iterate
__modules__dynamically — new modules are auto-included
Tables don't allow wrapper elements between <tr> rows. For expandable row pairs:
<!-- WRONG: x-data on <tr> doesn't scope to sibling <tr> -->
<tr x-data="{ expanded: false }">...</tr>
<tr x-show="expanded">...</tr> <!-- Can't see 'expanded'! -->
<!-- RIGHT: wrap both rows in <tbody> -->
<tbody x-data="{ expanded: false }">
<tr @click="expanded = !expanded">...</tr>
<tr x-show="expanded" x-cloak>...</tr>
</tbody>The data tab uses a <table id="event-rows"> as the HTMX swap target. The fragment response contains <tbody> elements (one per expandable row pair). HTMX's default innerHTML swap replaces the table's children correctly.
- Dict key
items→ collides withdict.items(). Useentriesinstead. {{ "{:,}".format(number) }}for comma-formatted numbers{{ [a, b] | min }}for inline min in templates
-- Scan list with event counts
SELECT s.guid, s.name, s.seed_target, s.created, s.started, s.ended,
s.status, COUNT(r.type) FROM tbl_scan_instance s
LEFT JOIN tbl_scan_results r ON s.guid = r.scan_instance_id
GROUP BY s.guid ORDER BY s.started DESC
-- Event summary by type
SELECT r.type, e.event_descr, MAX(r.generated) AS last_in,
COUNT(r.type) AS total, COUNT(DISTINCT r.data) AS utotal
FROM tbl_scan_results r, tbl_event_types e
WHERE r.scan_instance_id = ? AND r.type = e.event
GROUP BY r.type ORDER BY e.event_descr
-- Events for a scan (used by data tab)
SELECT r.generated, r.data, r.source_data, r.module, r.type,
r.confidence, r.visibility, r.risk, r.hash, r.source_event_hash,
e.event_descr, e.event_type, r.scan_instance_id, r.false_positive
FROM tbl_scan_results r, tbl_event_types e
WHERE r.scan_instance_id = ? AND r.type = e.event
ORDER BY r.generated DESCTwo images:
# Slim — Python deps only (Alpine 3.18, multi-stage build, non-root)
docker build -t spiderfoot-revival .
docker run -p 5001:5001 -v ~/.spiderfoot:/var/lib/spiderfoot spiderfoot-revival
# Full — Python + every local-tool CLI binary the modules wrap
docker build -f Dockerfile.full -t spiderfoot-full .
docker run -p 5001:5001 -v ~/.spiderfoot:/var/lib/spiderfoot spiderfoot-fullThe full image is python:3.11-based (lxml<5 has no wheels on 3.13) and ships nmap, nuclei, whatweb, dnstwist, cmseek, wafw00f, onesixtyone, nbtscan, retire, testssl.sh, snallygaster, trufflehog, and BBOT with all module deps pre-installed.
For BBOT active port scanning (masscan SYN scans) add --cap-add=NET_RAW; without it BBOT falls back to TCP-connect.
- bbot: Required for
sfp_tool_bbot_*modules. Pre-installed inDockerfile.full. For local dev:pip install bbot && bbot --install-all-deps -y. Modules pass--no-depsat runtime; if you skip the install step BBOT will hang on a sudo prompt and silently emit nothing. - user-scanner: Included in
requirements.txt. Used bysfp_userscannerfor email registration + username checking.
- Cloud buckets:
CLOUD_STORAGE_BUCKET(exists) /CLOUD_STORAGE_BUCKET_OPEN(open). NOTCLOUD_STORAGE_OPEN. - Code repos:
PUBLIC_CODE_REPO. NOTCODE_REPOSITORY. - Malicious emails:
MALICIOUS_EMAILADDR. NOTMALICIOUS_INTERNET_NAMEfor email indicators.
docker stop sf-test 2>/dev/null; docker rm sf-test 2>/dev/null
docker build -t spiderfoot-revival .
docker run -d --name sf-test -p 5001:5001 spiderfoot-revival
sleep 5
curl -s -o /dev/null -w "%{http_code}" http://localhost:5001/ # Dashboard
curl -s -o /dev/null -w "%{http_code}" http://localhost:5001/newscan # New scan
docker logs sf-test 2>&1 | grep -i error # Check errors