addedOutput schema / $defs
Added value: +{
+ "AbuseIpdbInfo": {
+ "description": "AbuseIPDB reputation check (Pro tier only).",
+ "properties": {
+ "abuse_score": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "AbuseIPDB confidence-of-abuse score (0-100). Only present when status='ok'.",
+ "title": "Abuse Score"
+ },
+ "country": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "ISO 3166-1 alpha-2 country code from AbuseIPDB geolocation (may differ from RIPE).",
+ "title": "Country"
+ },
+ "is_tor": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "AbuseIPDB's Tor exit flag (cross-reference with top-level tor_exit field).",
+ "title": "Is Tor"
+ },
+ "isp": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "ISP name as reported by AbuseIPDB.",
+ "title": "Isp"
+ },
+ "reason": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Human-readable reason string. Present when status is skipped/rate_limited/error/pro_only.",
+ "title": "Reason"
+ },
+ "status": {
+ "description": "'ok' = data fetched; 'skipped' = API key not configured; 'rate_limited' = AbuseIPDB quota exceeded; 'error' = transient HTTP/network failure; 'pro_only' = returned on Free tier as upsell hint (see upgrade_url).",
+ "enum": [
+ "ok",
+ "skipped",
+ "rate_limited",
+ "error",
+ "pro_only"
+ ],
+ "title": "Status",
+ "type": "string"
+ },
+ "total_reports": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Number of reports submitted against this IP in the last 90 days.",
+ "title": "Total Reports"
+ },
+ "upgrade_url": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Upgrade link returned when status='pro_only'.",
+ "title": "Upgrade Url"
+ },
+ "usage_type": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "AbuseIPDB usage classification: 'Data Center/Web Hosting/Transit', 'ISP', 'Mobile ISP', etc.",
+ "title": "Usage Type"
+ }
+ },
+ "required": [
+ "status"
+ ],
+ "title": "AbuseIpdbInfo",
+ "type": "object"
+ },
+ "ErrorDetail": {
+ "description": "Structured failure body. Codes mirror app/exceptions.AppException\nsubclasses; agent retry / upgrade decisions key off `code`, not `message`.",
+ "properties": {
+ "code": {
+ "description": "Stable machine-readable failure category. Agents key retry/upgrade decisions off this.",
+ "enum": [
+ "invalid_argument",
+ "not_found",
+ "rate_limit_exceeded",
+ "auth_required",
+ "tier_limit",
+ "upstream_timeout",
+ "upstream_error",
+ "internal_error"
+ ],
+ "title": "Code",
+ "type": "string"
+ },
+ "docs_url": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Documentation pointer (e.g. tool input contract) when code='invalid_argument'.",
+ "title": "Docs Url"
+ },
+ "message": {
+ "description": "Human-readable detail. Free text — never parse. Capped at 500 chars to prevent oversized upstream errors from bloating responses.",
+ "maxLength": 500,
+ "title": "Message",
+ "type": "string"
+ },
+ "retry_after_seconds": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "When code='rate_limit_exceeded', the minimum seconds to wait before retrying.",
+ "title": "Retry After Seconds"
+ },
+ "upgrade_url": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Pricing/upgrade URL when code='tier_limit' or 'rate_limit_exceeded' on the Free tier.",
+ "title": "Upgrade Url"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "title": "ErrorDetail",
+ "type": "object"
+ },
+ "ErrorResponse": {
+ "description": "MCP error envelope. Tool return type is always\n`SpecificResponse | ErrorResponse` — Union flag tells the agent which arm\narrived without parsing the inner body.",
+ "properties": {
+ "error": {
+ "$ref": "#/$defs/ErrorDetail"
+ }
+ },
+ "required": [
+ "error"
+ ],
+ "title": "ErrorResponse",
+ "type": "object"
+ },
+ "IpEnrichmentInfo": {
+ "description": "Shodan InternetDB enrichment subset (free, no API key) embedded in /v1/threat_report.\n\nMirrors the {ports, hostnames, vulns, cpes, tags} block at the top of ip_lookup,\nplus an internetdb_status field that surfaces the upstream fetch outcome —\nextracted as a sub-model so MCP clients see a typed schema instead of an opaque dict slot.",
+ "properties": {
+ "cpes": {
+ "description": "CPE 2.3 strings for services detected on this IP per Shodan InternetDB.",
+ "items": {
+ "type": "string"
+ },
+ "title": "Cpes",
+ "type": "array"
+ },
+ "hostnames": {
+ "description": "Hostnames Shodan InternetDB has observed pointing to this IP.",
+ "items": {
+ "type": "string"
+ },
+ "title": "Hostnames",
+ "type": "array"
+ },
+ "internetdb_status": {
+ "anyOf": [
+ {
+ "enum": [
+ "ok",
+ "error"
+ ],
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Outcome of the InternetDB fetch. 'error' indicates upstream failure; absent on cached/legacy paths.",
+ "title": "Internetdb Status"
+ },
+ "ports": {
+ "description": "Open ports observed by Shodan InternetDB. Empty on upstream failure (treat as 'no data', not 'closed').",
+ "items": {
+ "type": "integer"
+ },
+ "title": "Ports",
+ "type": "array"
+ },
+ "tags": {
+ "description": "Shodan InternetDB classification tags (e.g. 'cdn', 'cloud', 'vpn', 'tor', 'self-signed').",
+ "items": {
+ "type": "string"
+ },
+ "title": "Tags",
+ "type": "array"
+ },
+ "vulns": {
+ "description": "CVEs Shodan InternetDB has associated with banners on this IP, enriched with severity + cvss_v3 from local cve.db (Phase 2 IP enrichment, v1.16.0 BREAKING). Pre-1.16 this was a flat list[str] of CVE IDs. Unknown CVEs emit severity='UNKNOWN'.",
+ "items": {
+ "$ref": "#/$defs/VulnInfo"
+ },
+ "title": "Vulns",
+ "type": "array"
+ }
+ },
+ "title": "IpEnrichmentInfo",
+ "type": "object"
+ },
+ "PivotHint": {
+ "additionalProperties": true,
+ "description": "A suggested follow-up MCP tool call. Surfaced inside response.next_calls so\nLLM agents can chain related lookups without manual prompting. Each hint names\nthe tool, the input value to pass, and a short reason explaining why this\npivot adds value in the current context.",
+ "properties": {
+ "input": {
+ "description": "Suggested input value to pass to the tool — typically a CVE ID, CWE ID, domain, or IP. Pre-populated from the current response so the agent can call the next tool without re-deriving the argument.",
+ "title": "Input",
+ "type": "string"
+ },
+ "params": {
+ "anyOf": [
+ {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Optional extra kwargs to pass alongside `input`. Used by pivot generators when the next call benefits from a secondary parameter, e.g. {'exclude_id': 'AML.T0051'} to skip the originating technique from a sibling-tactic search. Omitted when no extra args are needed.",
+ "title": "Params"
+ },
+ "reason": {
+ "description": "Short rationale (one sentence) for why this follow-up call adds value, e.g. 'Federal patch deadline + ransomware association', 'Public exploits / PoC availability'.",
+ "title": "Reason",
+ "type": "string"
+ },
+ "tool": {
+ "description": "Canonical MCP tool name to call next. Constrained to known operation_ids in tools/list — adding a new tool here requires expanding the Literal.",
+ "enum": [
+ "cve_lookup",
+ "cve_search",
+ "cve_leading",
+ "bulk_cve_lookup",
+ "exploit_lookup",
+ "kev_detail",
+ "cwe_lookup",
+ "subdomain_enum",
+ "ssl_check",
+ "tech_fingerprint",
+ "asn_lookup",
+ "ip_lookup",
+ "ioc_lookup",
+ "bulk_ioc_lookup",
+ "hash_lookup",
+ "threat_intel",
+ "threat_report",
+ "audit_domain",
+ "domain_report",
+ "dns_lookup",
+ "whois_lookup",
+ "wayback_lookup",
+ "scan_headers",
+ "check_headers",
+ "check_secrets",
+ "check_injection",
+ "check_dependencies",
+ "email_mx",
+ "email_disposable",
+ "phone_lookup",
+ "username_lookup",
+ "password_check",
+ "phishing_check",
+ "atlas_technique_lookup",
+ "atlas_technique_search",
+ "bulk_atlas_technique_lookup",
+ "atlas_case_study_lookup",
+ "atlas_case_study_search",
+ "d3fend_defense_lookup",
+ "d3fend_defense_search",
+ "d3fend_defense_for_attack",
+ "d3fend_attack_coverage"
+ ],
+ "title": "Tool",
+ "type": "string"
+ }
+ },
+ "required": [
+ "tool",
+ "input",
+ "reason"
+ ],
+ "title": "PivotHint",
+ "type": "object"
+ },
+ "ShodanRepInfo": {
+ "description": "Shodan full API enrichment (Pro tier only). Richer than InternetDB fields at top level.",
+ "properties": {
+ "asn": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "ASN string per Shodan (e.g. 'AS13335'); may differ from top-level asn int.",
+ "title": "Asn"
+ },
+ "city": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "City name per Shodan geolocation.",
+ "title": "City"
+ },
+ "country_name": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Country name per Shodan geolocation.",
+ "title": "Country Name"
+ },
+ "hostnames": {
+ "description": "Hostnames observed pointing to this IP per Shodan.",
+ "items": {
+ "type": "string"
+ },
+ "title": "Hostnames",
+ "type": "array"
+ },
+ "isp": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "ISP per Shodan (may differ from AbuseIPDB/RIPE).",
+ "title": "Isp"
+ },
+ "last_update": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "ISO 8601 timestamp of Shodan's most recent data point for this IP.",
+ "title": "Last Update"
+ },
+ "org": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Organization name owning the IP per Shodan.",
+ "title": "Org"
+ },
+ "os": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Shodan-detected operating system (fingerprint-based, best-effort).",
+ "title": "Os"
+ },
+ "ports": {
+ "description": "Open ports observed by Shodan full scan (superset of top-level InternetDB ports).",
+ "items": {
+ "type": "integer"
+ },
+ "title": "Ports",
+ "type": "array"
+ },
+ "reason": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Human-readable reason. Present when status is skipped/restricted/rate_limited/error/pro_only.",
+ "title": "Reason"
+ },
+ "status": {
+ "description": "'ok' = data fetched; 'skipped' = API key not configured; 'restricted' = 403 (IP not available on free Shodan tier); 'rate_limited' = 429 quota exceeded; 'error' = transient HTTP/network failure; 'pro_only' = returned on Free tier as upsell hint.",
+ "enum": [
+ "ok",
+ "skipped",
+ "restricted",
+ "rate_limited",
+ "error",
+ "pro_only"
+ ],
+ "title": "Status",
+ "type": "string"
+ },
+ "upgrade_url": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Upgrade link returned when status='pro_only'.",
+ "title": "Upgrade Url"
+ },
+ "vulns": {
+ "description": "CVE IDs Shodan has associated with banners on this IP.",
+ "items": {
+ "type": "string"
+ },
+ "title": "Vulns",
+ "type": "array"
+ }
+ },
+ "required": [
+ "status"
+ ],
+ "title": "ShodanRepInfo",
+ "type": "object"
+ },
+ "ThreatReportResponse": {
+ "properties": {
+ "abuseipdb": {
+ "$ref": "#/$defs/AbuseIpdbInfo",
+ "description": "AbuseIPDB abuse-confidence enrichment. Pro tier returns live data; Free tier returns a {status:'pro_only', reason, upgrade_url} upsell stub (NOT an error). Pro failure paths emit status='error' / 'rate_limited' / 'skipped'. See AbuseIpdbInfo."
+ },
+ "asn": {
+ "additionalProperties": true,
+ "description": "ASN ownership from RIPE Stat network-info: {asn: int, prefix: str}. Empty dict when RIPE has no allocation; {error:'lookup_failed'} on fetch failure.",
+ "title": "Asn",
+ "type": "object"
+ },
+ "asn_name": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "ASN holder name from RIPE Stat as-overview, or null.",
+ "title": "Asn Name"
+ },
+ "cloud_provider": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Cloud / hosting provider name when the IP sits in a known CIDR or maps to a tier-1 ASN.",
+ "title": "Cloud Provider"
+ },
+ "country": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Country code from RIPE Stat rir-stats-country, or null.",
+ "title": "Country"
+ },
+ "enrichment": {
+ "$ref": "#/$defs/IpEnrichmentInfo",
+ "description": "Shodan InternetDB free-tier enrichment (ports, hostnames, vulns, cpes, tags). Available on all tiers. See IpEnrichmentInfo for the exact field shape. Returned with all-empty lists on upstream failure — treat as 'no data', not 'clean'."
+ },
+ "firehol": {
+ "anyOf": [
+ {
+ "additionalProperties": true,
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "FireHOL Level1 listing status: {status, listed, lists_matched}. Available on all tiers.",
+ "title": "Firehol"
+ },
+ "ip": {
+ "description": "Queried IP address (IPv4 or IPv6, echoed back verbatim).",
+ "title": "Ip",
+ "type": "string"
+ },
+ "is_datacenter": {
+ "default": false,
+ "description": "True if IP is hosted on a known datacenter / cloud provider (parity with ip_lookup.is_datacenter). Same two-tier detection — cloud_provider hit OR tier-1 datacenter ASN. Always present — never null.",
+ "title": "Is Datacenter",
+ "type": "boolean"
+ },
+ "next_calls": {
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/$defs/PivotHint"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Suggested follow-up MCP tool calls. Ordered by relevance; agents should chain these without re-prompting the user.",
+ "title": "Next Calls"
+ },
+ "ptr": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Reverse DNS PTR for the IP, or null when unresolvable.",
+ "title": "Ptr"
+ },
+ "risk_score": {
+ "default": 0,
+ "description": "Composite 0-100 score (parity with ip_lookup.risk_score). v1.17.0 additive components: ports (10 * min(count, 5) = 0-50), tor_exit (+30), firehol.listed (+20), AbuseIPDB confidence (round(15 * score / 100) = 0-15), is_datacenter (+10), known vulns (5 * min(count, 4) = 0-20). Use severity_label for thresholding.",
+ "title": "Risk Score",
+ "type": "integer"
+ },
+ "severity_label": {
+ "default": "low",
+ "description": "Coarse risk band derived from risk_score (parity with ip_lookup.severity_label). Pre-1.17 the route emitted this field and advertised it in verdict.falsifiable_fields but the schema didn't declare it, so Pydantic silently dropped it from the wire. Same thresholds: >=75 critical, >=50 high, >=25 medium, else low.",
+ "enum": [
+ "low",
+ "medium",
+ "high",
+ "critical"
+ ],
+ "title": "Severity Label",
+ "type": "string"
+ },
+ "shodan": {
+ "$ref": "#/$defs/ShodanRepInfo",
+ "description": "Shodan full-API enrichment (richer than the InternetDB enrichment block). Pro tier returns live data; Free tier returns a {status:'pro_only', reason, upgrade_url} upsell stub. Pro failure paths emit status='error' / 'rate_limited' / 'restricted' / 'skipped'. See ShodanRepInfo."
+ },
+ "summary": {
+ "default": "",
+ "description": "One-line human summary combining threat_level, port count, vuln count, and abuse signal.",
+ "title": "Summary",
+ "type": "string"
+ },
+ "threat_level": {
+ "default": "none",
+ "description": "Heuristic threat tier. 'high' when any vulns present OR abuse_score>=50; 'medium' when abuse_score>=25; 'low' when open ports observed; 'none' otherwise. On Free tier threat_level is necessarily conservative — abuse_score is unknown.",
+ "enum": [
+ "none",
+ "low",
+ "medium",
+ "high"
+ ],
+ "title": "Threat Level",
+ "type": "string"
+ },
+ "tor_exit": {
+ "default": false,
+ "description": "True if IP appears in the Tor Project bulk exit list (verdict.sources_unavailable['tor'] when fetch failed).",
+ "title": "Tor Exit",
+ "type": "boolean"
+ },
+ "verdict": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/Verdict"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Falsifiability metadata: sources_queried, sources_unavailable, completeness, deterministic flag. Lets agents distinguish 'no data' from 'source failed' without re-running the call."
+ }
+ },
+ "required": [
+ "ip"
+ ],
+ "title": "ThreatReportResponse",
+ "type": "object"
+ },
+ "Verdict": {
+ "properties": {
+ "completeness": {
+ "default": "complete",
+ "description": "'complete' = every planned source returned data; 'partial' = at least one source in sources_unavailable failed or was skipped; 'minimal' = only the primary/required source returned, optional enrichment missing.",
+ "enum": [
+ "complete",
+ "partial",
+ "minimal"
+ ],
+ "title": "Completeness",
+ "type": "string"
+ },
+ "data_age_seconds": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Seconds elapsed since the oldest cached source was fetched, or null when every source was queried live for this request. Use to judge freshness.",
+ "title": "Data Age Seconds"
+ },
+ "deterministic": {
+ "description": "True when the response is fully reproducible from the listed sources for the same input at the same moment (no randomness, no model inference). False for endpoints that include probabilistic scoring or LLM output.",
+ "title": "Deterministic",
+ "type": "boolean"
+ },
+ "falsifiable_fields": {
+ "description": "Top-level response fields whose values a caller can independently re-derive from the named upstream sources (e.g. 'dns', 'ssl', 'whois'). Fields not in this list are derived/computed and cannot be directly re-verified.",
+ "items": {
+ "type": "string"
+ },
+ "title": "Falsifiable Fields",
+ "type": "array"
+ },
+ "sources_queried": {
+ "description": "Canonical source identifiers successfully consulted for this response (e.g. 'ripe_stat', 'shodan_internetdb', 'firehol'). Agent-readable list, order not significant.",
+ "items": {
+ "type": "string"
+ },
+ "title": "Sources Queried",
+ "type": "array"
+ },
+ "sources_unavailable": {
+ "description": "Sources that were expected but not returned — either intentionally skipped (lite mode, tier gating) or failed (quota, timeout, upstream down). Empty list means every planned source produced data.",
+ "items": {
+ "type": "string"
+ },
+ "title": "Sources Unavailable",
+ "type": "array"
+ }
+ },
+ "required": [
+ "deterministic"
+ ],
+ "title": "Verdict",
+ "type": "object"
+ },
+ "VulnInfo": {
+ "description": "Severity-enriched CVE entry attached to /v1/ip and /v1/threat_report.\n\nPhase 2 IP enrichment (v1.16.0 BREAKING): Shodan InternetDB returns a flat\nlist of CVE IDs with no severity context, forcing agents to fan out\ncve_lookup calls for triage. We resolve severity + cvss_v3 against the\nlocal cve.db in a single SQL batch so the agent can prioritise without\nextra round-trips. Unknown CVEs are emitted with severity='UNKNOWN' /\ncvss_v3=null so the ID is preserved (the agent must not infer 'benign'\nfrom the absence of a row).",
+ "properties": {
+ "cve_id": {
+ "description": "CVE identifier (e.g. 'CVE-2021-44228').",
+ "title": "Cve Id",
+ "type": "string"
+ },
+ "cvss_v3": {
+ "anyOf": [
+ {
+ "maximum": 10,
+ "minimum": 0,
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "CVSS v3 base score (0.0-10.0). Null when severity='UNKNOWN' or NVD has no v3 score.",
+ "title": "Cvss V3"
+ },
+ "severity": {
+ "description": "NVD CVSS v3 severity bucket from local cve.db. 'UNKNOWN' when the CVE is not in our database (NVD may not have classified it yet, or the ID is reserved). Treat UNKNOWN as 'do not assume benign — call cve_lookup for fresh upstream data.'",
+ "enum": [
+ "CRITICAL",
+ "HIGH",
+ "MEDIUM",
+ "LOW",
+ "UNKNOWN"
+ ],
+ "title": "Severity",
+ "type": "string"
+ }
+ },
+ "required": [
+ "cve_id",
+ "severity"
+ ],
+ "title": "VulnInfo",
+ "type": "object"
+ }
+}