Skip to content

External enrichment (threat intel)

FreeDR can look up case indicators (file hashes, public IPs, public domains) with optional threat-intel providers. Every provider is off by default. FreeDR sends indicators only, never files. Results are stored as facts on the case. They are not verdicts: FreeDR does not turn a provider answer into malicious, suspicious or clean.

Enrichment is not AI. The AI investigator can request it and read its results, but enrichment works with AI turned off.

Enrichment kinds

Kind Where it runs What it does
file_hash Local Hashes the implicated file through Artifact acquisition (or uses the SHA-256 already retained on the event). Adds host-prevalence facts: new_to_host, host_first_seen_at, host_paths, host_process_starts.
file_signature Local Offline Authenticode check of the exact file version: authenticode_status (trusted_offline, invalid, unsigned, unknown), signer, signer novelty on the host.
hash_reputation External Looks up the file's SHA-256. If the event has no retained hash, the file is hashed locally first, then the lookup follows.
ip_reputation External Looks up one public IP address from the event.
domain_reputation External Looks up one public domain from the event.

The rest of this page is about the three external kinds.

When enrichment runs

Trigger What happens
Automatic For every alert-trigger event of an open or in-progress case, and every event attached to such a case, FreeDR requests all five kinds. The evidence decides which kinds have a target. No rule chooses the kinds. An event is tried once per kind and settings revision, so saving provider settings re-runs the external kinds for open cases. Informational and muted alerts never enter cases, so they get no enrichment.
Manual Case → Investigation → Targeted enrichment form, or POST /api/v1/cases/{case_id}/enrichment. You choose a kind, 1–16 event IDs linked to the case and a purpose. You cannot type an indicator: the target always comes from retained evidence.
AI The investigator's enrich tool sends the same request as a manual one (requester ai). See AI features.

Automatic, manual, AI and access-check lookups all use the same daily quota per provider.

Which indicators are eligible

Network indicators come only from Sysmon events:

Sysmon event ip_reputation uses domain_reputation uses
Event 3, network connect DestinationIp DestinationHostname
Event 22, DNS query the first public address in QueryResults QueryName
  • A field that appears more than once in the event gives no target.
  • Without Sysmon, or for any other event type, the network kinds end unknown with reason source_public_network_pivot_unavailable.
  • Hashes come from the event's retained sha256, or from a local hash of the file the event names (its own file, a file named by a persisted value, the acting process's image; see Alerts & Cases).

Every indicator passes one validating constructor before it can be sent. Anything that fails is never looked up.

Class Accepted Never sent
SHA-256 64 hex characters, lowercased anything else
Public IP Global IPv4; IPv6 only in 2000::/3 IPv4: private (10/8, 172.16/12, 192.168/16), loopback, link-local, 0/8, 100.64/10 (CGNAT), 192.0.0/24, 192.0.2/24, 198.18/15, 198.51.100/24, 203.0.113/24, 192.88.99/24, multicast and reserved (224/3 and up), broadcast. IPv6: loopback, link-local, ULA, IPv4-mapped, 2001::/23, 2001:db8::/32, 2002::/16 (6to4), 3fff::/20
Public domain ASCII (punycode), at most 253 characters, at least two labels, letters/digits/hyphens, alphabetic TLD of 2+ letters. Lowercased, trailing dot removed IP literals, ports, single labels, and the TLDs local, localhost, localdomain, internal, intranet, private, arpa, lan, home, corp, test, invalid, onion
Exact URL http/https, ASCII, host passes the domain rules, valid percent-encoding URLs with a query (?), fragment (#), user info (@), backslash, IP host, or encoded control characters/spaces

Internal names under a public TLD

The domain check is syntactic. An internal host name under a public suffix (for example finance.example.com) is sent, even if it resolved to a private address. Only the reserved TLDs above are filtered.

Exact URLs

The contract and URLhaus support exact-URL lookups, but in this version no evidence path produces a URL indicator. In practice no URL is looked up.

One provider per lookup

Each request goes to one provider: the first enabled provider in this order that supports the indicator. FreeDR never fans out to several providers.

Kind Order
hash_reputation MalwareBazaar → VirusTotal → CIRCL → URLhaus → AlienVault OTX
ip_reputation AbuseIPDB → GreyNoise (IPv4 only) → ThreatFox → AlienVault OTX
domain_reputation URLhaus → ThreatFox → AlienVault OTX

If no provider in the list is enabled, the request names the first one and ends disabled (provider_disabled_or_key_missing), with no network traffic.

Providers

All providers share these rules:

  • Fixed HTTPS host; HTTPS only; redirects refused; system proxy ignored.
  • 10 s connect and total timeout; response body at most 256 KiB.
  • Headers: Accept: application/json, User-Agent: FreeDR/0.1 targeted-enrichment and, for keyed providers, the key header (marked sensitive). The key is never put in the URL or body.
  • The response must name the indicator that was sent. Otherwise the result is error / provider_identity_mismatch.
  • Only allowlisted fields are kept, as name/value facts: at most 32 facts, 512 characters per value, 16 KiB in total, control characters removed. More is cut and marked provider_details_bounded.
  • HTTP 429 → rate_limited; 401/403 → error / provider_access_denied; 3xx → provider_redirect_refused; 5xx → provider_unavailable; timeout → provider_timeout.

Result statuses

Status Meaning
hit The provider returned a record for the indicator. A hit is not "malicious": an AbuseIPDB record with 0 reports, a VirusTotal file with 0 detections and a GreyNoise benign IP are all hits. Read the facts.
not_found The provider has no record. Not a clean verdict.
unknown No answer (cancelled, deadline, unsupported indicator, no target, query rejected).
error Transport, HTTP, parse or identity failure. See the reason code.
rate_limited Provider 429, or FreeDR's daily quota / spacing (shared_provider_quota).
disabled Provider off or key missing.
stale Shown by the console for a hit, not_found or unknown result past its valid until time. The API keeps the stored status; compare valid_until_us yourself.

CIRCL hashlookup

Key slot none
Indicators SHA-256
Request GET https://hashlookup.circl.lu/lookup/sha256/{sha256}
Auth header none
Not found HTTP 404
Facts known_file_corpus_match, interpretation ("Corpus membership is context, not a benign verdict"), corpus_source, known_file_name, known_file_size, provider_trust_score (hashlookup:trust)
Report link https://hashlookup.circl.lu/lookup/sha256/{sha256}
Limits default 100/day, max 10,000/day

MalwareBazaar (abuse.ch)

Key slot abuse_ch (shared with ThreatFox and URLhaus)
Indicators SHA-256
Request POST https://mb-api.abuse.ch/api/v1/, form query=get_info&hash={sha256}
Auth header Auth-Key
Not found query_status: hash_not_found
Facts malware_corpus_match, malware_family (signature), file_type, first_seen, last_seen
Report link https://bazaar.abuse.ch/sample/{sha256}/
Limits default 100/day, max 10,000/day

ThreatFox (abuse.ch)

Key slot abuse_ch
Indicators SHA-256, public IP, public domain
Request POST https://threatfox-api.abuse.ch/api/v1/, JSON. Hash: {"query":"search_hash","hash":…}. IP/domain: {"query":"search_ioc","search_term":…,"exact_match":true}
Auth header Auth-Key
Not found query_status: no_result
Matching Hash: associated IOCs of type domain, ip:port, url, sha256_hash, md5_hash. Domain: IOC of type domain, same name. IP: IOC of type ip:port with the same IP.
Facts coverage (expired IOCs are omitted by the API), relation, returned_records; for up to 3 matching IOCs ioc_N.indicator, .indicator_type, .threat_type, .malware_family, .provider_confidence, .first_seen, .last_seen; details_bounded if more were returned
Report link https://threatfox.abuse.ch/ioc/{id}/ (first matching IOC)
Limits default 100/day, max 10,000/day

URLhaus (abuse.ch)

Key slot abuse_ch
Indicators SHA-256, public domain, exact URL
Request POST https://urlhaus-api.abuse.ch/v1/payload/ (form sha256_hash), /v1/host/ (form host), /v1/url/ (form url)
Auth header Auth-Key
Not found query_status: no_results
Facts malware_distribution_context, interpretation (malware distribution, not phishing or host guilt), malware_family, file_type, observed_url_count, url_status, threat_type, first_seen, last_seen, date_added
Report link hash: https://urlhaus.abuse.ch/browse.php?search={sha256}; domain: https://urlhaus.abuse.ch/host/{domain}/; URL: https://urlhaus.abuse.ch/url/{id}/
Limits default 100/day, max 10,000/day

AbuseIPDB

Key slot abuse_ipdb
Indicators public IP (IPv4 and IPv6)
Request GET https://api.abuseipdb.com/api/v2/check?ipAddress={ip}&maxAgeInDays=90
Auth header Key
Not found none: every valid answer is a hit (the record must say isPublic: true)
Facts abuse_confidence_score (0–100), total_reports, report_window_days (90), interpretation ("zero reports is not clean"), distinct_reporters, last_reported_at, country_code, usage_type
Report link https://www.abuseipdb.com/check/{ip}
Limits default 100/day, max 10,000/day. FreeDR also reads X-RateLimit-Remaining/X-RateLimit-Reset: at 0 remaining the provider is blocked until the reset time.

VirusTotal

Key slot virus_total
Indicators SHA-256 only (no upload, no URL/IP/domain)
Request GET https://www.virustotal.com/api/v3/files/{sha256}
Auth header x-apikey
Not found HTTP 404
Facts engines_malicious, engines_suspicious, engines_undetected, engines_harmless, interpretation ("zero detections is not a clean verdict"), threat_label, known_file_name, file_type, community_reputation, times_submitted, first_submitted_at, last_analysis_at. No per-engine results.
Report link https://www.virustotal.com/gui/file/{sha256}
Limits default 100/day, max 500/day; one request per 15 s (free tier: 4/min)

AlienVault OTX

Key slot alien_vault_otx
Indicators SHA-256, public IP (IPv4 and IPv6), public domain
Request GET https://otx.alienvault.com/api/v1/indicators/{file,IPv4,IPv6,domain}/{value}/general
Auth header X-OTX-API-KEY
Not found HTTP 404, or pulse_count 0 with no OTX validation notes
Facts pulse_count, interpretation, provider_validation (OTX's own allowlist notes, such as "Whitelisted domain"); for up to 3 pulses pulse_N.name, .adversary, .created, .modified, .tags (5), .malware_families (5); details_bounded
Report link https://otx.alienvault.com/indicator/{file,ip,domain}/{value}
Limits default 100/day, max 10,000/day

GreyNoise Community

Key slot grey_noise
Indicators public IPv4 only. An IPv6 address skips GreyNoise for the next enabled IP provider.
Request GET https://api.greynoise.io/v3/community/{ip}
Auth header key
Not found HTTP 404
Facts internet_scanner (noise), common_business_service (riot), classification, service_name, last_seen, interpretation ("a benign class does not clear this host's connection")
Report link https://viz.greynoise.io/ip/{ip}
Limits default 100/day, max 10,000/day

Settings

Settings → Enrichment & AI, or GET/POST /api/v1/settings/enrichment.

Setting Default Rules
Enable this provider off A keyed provider cannot be enabled without its key: the save fails with credential required.
Daily request limit 100 1 to 10,000; VirusTotal 1 to 500
Keys (5 slots) none abuse_ch, abuse_ipdb, virus_total, alien_vault_otx, grey_noise. 1–4096 printable ASCII characters. Write-only: Keep, Enter or replace, Remove.
  • Keys are encrypted with user-scoped DPAPI under the service account (LocalSystem) and stored in the control database with the settings. The API never returns a key, only whether a slot is configured.
  • Saves carry the settings revision and a CSRF token. A stale revision returns 409; reload and save again. A new policy takes effect only after the database commit.
  • Keys can also come from the setup configuration file (Getting started).

Active provider status and access check

The Active provider status list (also in the case panel under Provider status and shared quotas) comes from GET /api/v1/settings/enrichment/status. Per provider: enabled, key configured, supported indicators, used_today / daily limit, Retry after time if blocked, and the last lookup status and reason.

Note

The last status and reason are kept in memory. They are empty after a service restart until the next lookup.

Check access with a harmless lookup (POST /api/v1/settings/enrichment/{provider}/validate) queues one lookup of a fixed public value, never case evidence:

Provider Value sent
CIRCL, MalwareBazaar, ThreatFox, URLhaus, VirusTotal, AlienVault OTX SHA-256 of the empty file, e3b0c442…b855
AbuseIPDB, GreyNoise 1.1.1.1

The check uses the saved settings, counts against the daily quota, and is refused (403 enrichment_disabled) if the provider is off or has no key. At most 5 checks can be queued. The result appears as the provider's last status: refresh the status to see it. A not_found result also proves that the key works.

Quotas, spacing and retries

Topic Behaviour
Daily quota Counted per provider per UTC day in the control database. Each request sent to the provider counts, including retries and access checks. Cached results do not count.
Quota exhausted The request ends rate_limited (shared_provider_quota). Nothing is queued for the next day. Automatic lookups for the same events are not retried later, unless provider settings are saved again.
VirusTotal spacing Each reserved VirusTotal request blocks the provider for 15 s. Waiting requests are held in turn, up to 5 minutes after they were created. A request whose turn would come later ends rate_limited.
Provider 429 / Retry-After The provider is blocked until Retry-After (60 s if absent). Requests wait for the block if it ends before their deadline.
Retries Timeouts, connection errors, interrupted bodies, 5xx and 429 are retried once (2 attempts in total), not before 1 s and not after the deadline. 401/403, other 4xx and parse errors are not retried.
Deadline 60 s after the request is created (VirusTotal spacing can extend it to 5 minutes). After that the request ends unknown (enrichment_deadline).
Concurrency One coordinator processes requests one at a time. The store also caps running external lookups at 2. One canonical lookup (provider + indicator) runs once; other requests for it wait and share the result.
Capacity At most 256 requests pending or running in total. Beyond that, new requests are refused (enrichment_busy, HTTP 503; the AI is told to list the check as a gap).
Restart Running requests return to pending at service start.

Caching

A result is stored once as an immutable observation and linked to every case request that asked for the same provider and indicator. A new request reuses a fresh observation from any case without contacting the provider.

Result Reused for
hit for a SHA-256 24 h
hit for an IP, domain or URL 1 h
not_found, unknown 15 min
error, rate_limited, disabled never reused

The provider's Cache-Control: max-age (minus Age) or Expires can shorten this; no-store / no-cache makes the result non-reusable. A manual request for the same case, event and kind returns the earlier request while its result is fresh or it is still running. Old observations stay on the case and are shown as stale.

Privacy

What leaves the host What never leaves the host
One canonical indicator per request: a SHA-256, a public IP or a public domain (exact URL in the contract only) Files or file samples (no upload to any provider)
The provider key, in its header File paths, command lines, user names, host name, case data
User-Agent: FreeDR/0.1 targeted-enrichment Private, reserved and documentation IPs; reserved internal TLDs; URLs with query, fragment or credentials
  • Nothing is sent for a provider you have not enabled. Saving a key alone sends nothing; enabling and saving does start automatic lookups for open cases.
  • Endpoints are fixed in code. No case, indicator or console setting can change the host, path, proxy or redirect target.
  • Provider text is stored as plain display data and rendered as text. Report links are shown only if they start with https://.
  • Remember that a lookup reveals to the provider that this indicator interests you. Host name lookups of internal names under a public TLD are sent (see Which indicators are eligible).

Where results appear

Place What is shown
Case → Investigation → Targeted enrichment One card per request: provider and status (stale if expired), target, kind, purpose, retrieved and valid-until times, all facts, confidence basis, reason code, Provider reference link, observation ID, evidence ID. Cancel request while pending or running. Request form (kind, linked event IDs, purpose).
Case Summary Enrichment line counting requests by kind and state, and up to 8 observations with their first 4 facts.
HTML report (GET …/report.html) Enrichment observations section, up to 200 observations.
AI The case summary's enrichment entry, the case_context section enrichment, and the brief used by Explain this alert. Tool results pass through the AI egress redaction (AI overview).

API

Method and path Body / query Response
GET /api/v1/cases/{case_id}/enrichment limit (1–100, default 50), cursor {schema_version, requests[], observations[], next_cursor}
POST /api/v1/cases/{case_id}/enrichment {schema_version:1, csrf_token, request:{schema_version:1, evidence_event_ids[1–16], kind, purpose}} 202 {schema_version:1, requests[]}
POST /api/v1/cases/{case_id}/enrichment/{request_id}/cancel {schema_version:1, csrf_token} {schema_version:1, cancelled: bool}
GET /api/v1/settings/enrichment/status — {schema_version:1, providers[]}
POST /api/v1/settings/enrichment/{provider}/validate {schema_version:1, csrf_token} 202 {schema_version:1, provider}
GET / POST /api/v1/settings/enrichment save: {schema_version, csrf_token, change:{schema_version, expected_revision, providers[8], ai, credentials[]}} settings snapshot (no keys)
  • The case must be open or in progress, and every event ID must be a case event or alert evidence of the case. Extra fields (such as an indicator) are rejected with 400.
  • A request has state pending, waiting_local, running, completed or cancelled, and links to an observation_id when done. An observation has provider, target, status, facts[], reference_url, reason_code, retrieved_at_us, valid_until_us, provider_last_seen_us.
  • Cancelling marks the request cancelled and stops its HTTP request if it is the one in flight. Requests from other cases for the same lookup continue on their own.

Citing enrichment in a response

A response step can cite an enrichment observation (kind: enrichment, observation_id). The observation must be linked to the case. Its targets are the case events that requested it, and the citation is bound to the observation's content digest.

Who Behaviour
AI investigator inspect_response accepts an enrichment citation, but any containment step that cites enrichment stays a draft for you. The AI never runs it, even with Let AI contain threats it finds on.
You, in the console The response panel offers only alert and event citations.
You, via the API Enrichment citations are accepted and run like other citations.

Troubleshooting

Symptom Cause Fix
Result disabled, provider_disabled_or_key_missing No enabled provider supports the kind Enable a provider for that indicator type in Settings → Enrichment & AI and save. Open cases are re-enriched.
Save fails: credential required Keyed provider enabled without its key Enter the key in the same save.
Status Key required Key slot empty Enter the key and save.
error, provider_access_denied Key wrong, revoked or blocked (HTTP 401/403, or abuse.ch no_api_key / invalid_auth_key / user_blacklisted) Replace the key, then run the access check.
rate_limited, shared_provider_quota Daily limit used up (UTC day), provider blocked, or VirusTotal spacing queue too long Raise the daily limit (within the maximum) or wait for 00:00 UTC; request again manually.
rate_limited, provider_rate_limited Provider returned 429 Wait for the Retry after time shown in provider status.
error, provider_timeout / provider_transport_error No route to the provider within 10 s. FreeDR does not use a proxy. Allow direct HTTPS egress to the provider host.
error, provider_unavailable Provider 5xx Retried once automatically; request again later.
error, provider_redirect_refused Provider (or an intercepting proxy) redirected Redirects are never followed; check for TLS interception.
error, provider_identity_mismatch Answer did not name the queried indicator (for ThreatFox: no returned IOC matched) Treat as no result.
unknown, source_public_network_pivot_unavailable Not a Sysmon Event 3/22, no Sysmon, private destination, reserved TLD or duplicate field Expected for private/internal addresses. Install Sysmon for network lookups.
unknown, unsupported_indicator Provider cannot answer this indicator (for example IPv6 for GreyNoise) Enable another provider for that type.
unknown, enrichment_deadline Not done within 60 s (5 min for VirusTotal spacing) Request again.
Request refused, enrichment_busy (503) 256 requests already pending Wait for the queue to drain.
not_found / hit with no detections Normal provider answer Neither means benign; judge with case evidence.
Result shows stale Past its valid-until time Request again for a fresh lookup.
Provider status empty after restart Last status is kept in memory only Run the access check.
Provider settings degraded (settings_reset_invalid) Stored settings invalid or older version, reset to defaults Re-enter providers and keys (Health).