There is a particular category of tool that does not exist until you build it yourself. Not because it cannot be built, but because the off-the-shelf alternatives all make one of three tradeoffs: they are SaaS with a per-query price tag, they are a loose collection of browser tabs, or they are a pipeline that requires four flags and a config file just to ask whether an IP is malicious.

scope-recon is a Rust binary that hits 12 threat intelligence sources in parallel and returns a verdict in a terminal UI. You provide an IP address. Results appear as they arrive. When all sources have responded, an LLM synthesizes them into a threat assessment. The tool completes in approximately three seconds for a fully-keyed configuration. For a detailed walkthrough of the async architecture, verdict logic, and cache design, see the architecture deep-dive.

What It Does

scope-recon 185.220.101.47

One argument. The tool determines whether to launch the TUI (single IP, interactive terminal) or the CLI path (bulk, JSON, file output), loads API keys from the environment, fires all queries concurrently, and renders.

It also accepts hostnames and CIDR ranges:

scope-recon dns.google          # resolves to 8.8.8.8 via async DNS lookup
scope-recon 192.168.1.0/30      # expands to host IPs, queries each, cap at 256
scope-recon --file targets.txt  # newline-delimited list, CIDR lines expand inline

The 12 Sources

All sources are queried concurrently, with one exception: AI analysis fires sequentially after the other eleven complete, because it requires the full dataset as context.

SourceKey RequiredWhat It Provides
ip-api.comNoGeolocation: country, region, city, ISP, org, ASN
Shodan / InternetDBOptionalOpen ports, service banners, hostnames, CVEs, tags
AbuseIPDBYesAbuse confidence score (0–100), report history, usage type
VirusTotalYesDetection counts across 80+ AV engines
AlienVault OTXYesThreat intelligence pulses and pulse names
GreyNoiseOptionalNoise/RIOT classification, last seen, actor name
ThreatFoxYes (free)C2 IOC matches with malware family and confidence
RIPE StatNoASN, prefix, RIR, PTR record
IPQualityScoreYesFraud score, proxy/VPN/TOR flags, bot status, abuse velocity
PulsediveYesRisk rating, threat feed memberships
IPinfoOptionalHostname, org, timezone, VPN/proxy/TOR/hosting flags
AI Analysis (Grok)OptionalSynthesized threat assessment, per-malware-family context

Key requirements are intentionally minimal. Three sources — ip-api, RIPE Stat, and Shodan InternetDB — require no key. Shodan, GreyNoise, and IPinfo degrade gracefully, using a free or limited path when a key is absent rather than failing. ThreatFox is free to register. The tool is functional without any keys configured; it simply has less data to work with.

The TUI

Running scope-recon <ip> in an interactive terminal produces the following interface:

┌─ scope-recon ──────────────────────────────────────────────────────┐
│  IP: 185.220.101.47                       VERDICT: ● MALICIOUS      │
├──────────────────┬─────────────────────────────────────────────────┤
│ SOURCES          │ THREATFOX  (abuse.ch)                           │
│                  │                                                  │
│ ▶ Geolocation ✓  │   C2 IOCs:    3                                  │
│   Shodan      ✓  │                                                  │
│   AbuseIPDB   ✓  │   IOC:        185.220.101.47:9001               │
│   VirusTotal  ✓  │   Threat Type: botnet_cc                        │
│   OTX         ✓  │   Malware:    Tor                               │
│   GreyNoise   ✓  │   Confidence: 75%                               │
│   ThreatFox   ✓  │   First Seen: 2024-08-12                        │
│   RIPE Stat   ✓  │                                                  │
│   IPQS        ✓  │   IOC:        185.220.101.47:443                │
│   Pulsedive   ✓  │   Threat Type: botnet_cc                        │
│   IPInfo      ✓  │   Malware:    Tor                               │
│   AI Analysis ⠸  │   Confidence: 75%                               │
├──────────────────┴─────────────────────────────────────────────────┤
│  q quit   ↑↓/jk navigate   PgUp/PgDn scroll   r refresh   s save  │
└────────────────────────────────────────────────────────────────────┘

Results populate in real time as each API call completes. An animated spinner cycles while a source is loading. Completed sources display a green . Errors display a red . Sources excluded via --only display a dim -.

The verdict in the header is recomputed on every render frame. It uses the same compute_verdict() function as the CLI path, fed a partial report built from whatever sources have finished so far. If ThreatFox returns IOC matches before AbuseIPDB has responded, the header already reflects MALICIOUS.

  • ↑↓ / jk — move between sources; the detail pane updates immediately
  • PgUp / PgDn — scroll the detail pane for sources with long output
  • r — refresh: drains the channel, creates a new one, respawns all 12 queries with cache bypassed
  • s — save: writes current state as pretty-printed JSON to {ip}-{YYYYMMDD-HHMMSS}.json in the current working directory; the footer shows ✓ Saved: filename.json for approximately three seconds
  • q / Ctrl-C — quit

AI Analysis in the TUI

The AI Analysis panel uses SSE streaming. The detail pane updates token by token as the response arrives — the assessment renders progressively rather than appearing all at once. The spinner remains active until the first token appears. Once the first token reaches the channel, the source transitions to Done even though text continues to accumulate.

The CLI Path

When scope-recon determines the TUI is not appropriate — due to --json, --output, --file, or multiple targets — it uses tokio::join! to fire all 11 concurrent queries simultaneously, then runs AI analysis sequentially afterward, then renders a single time.

Basic output

IP: 185.220.101.47
══════════════════════════════════

SUMMARY
  Verdict:         MALICIOUS
  Findings:        3 C2 IOC(s) on ThreatFox · 4 malicious VT detections · AbuseIPDB score 87/100 · IPQS: TOR exit node

══════════════════════════════════

GEOLOCATION  (ip-api.com)
  Country:         Germany
  Region:          Hesse
  City:            Frankfurt am Main
  ISP:             Hetzner Online GmbH
  Org:             Tor network
  ASN:             AS24940 Hetzner Online GmbH

SHODAN
  Hostnames:       tor-exit.example.de
  Tags:            tor
  Services:
    22/tcp       OpenSSH 8.4p1
    443/tcp      -
    9001/tcp     -

THREATFOX  (abuse.ch)
  C2 IOCs:         3
    IOC:           185.220.101.47:9001
    Threat Type:   botnet_cc
    Malware:       Tor
    Confidence:    75%
    First Seen:    2024-08-12
    ...

AI ANALYSIS  (Grok via OpenRouter)

  185.220.101.47 is a known Tor exit node hosted on Hetzner infrastructure in
  Frankfurt, Germany. The IP has been consistently flagged as malicious across
  multiple threat intelligence platforms...

JSON output

scope-recon 185.220.101.47 --json

Every source is a typed field in a flat struct. Unavailable sources are null and do not break the output shape.

{
  "queried_at": "2026-03-09T14:22:08.413+00:00",
  "ip": "185.220.101.47",
  "ipapi": { "country": "Germany", "city": "Frankfurt am Main" },
  "shodan": { "open_ports": [22, 443, 9001], "tags": ["tor"] },
  "abuseipdb": { "abuse_confidence": 87, "total_reports": 412 },
  "threatfox": { "ioc_count": 3, "iocs": ["..."] },
  "ai_analysis": { "analysis": "185.220.101.47 is a known Tor exit node..." }
}

Filtering sources

--only excludes everything not named. Excluded sources produce no output and no error.

# Quick IOC check — skip slower sources
scope-recon 1.2.3.4 --only threatfox,virustotal
 
# BGP context only, no authenticated sources
scope-recon 1.2.3.4 --only ipapi,bgpview,shodan
 
# AI synthesis of cached data only
scope-recon 1.2.3.4 --only openrouter

Bulk mode

scope-recon --file targets.txt --json > results.json

A 500ms courtesy delay between IPs prevents hammering rate-limited APIs. CIDR lines in the file expand inline. Comment lines prefixed with # are skipped.

jq Workflows

JSON output pairs with jq to build composable analysis pipelines.

Triage dashboard — key fields on one line:

scope-recon 1.2.3.4 --json | jq '{
  ip,
  vt_malicious: .virustotal.malicious,
  abuse_score:  .abuseipdb.abuse_confidence,
  fraud_score:  .ipqs.fraud_score,
  ioc_count:    .threatfox.ioc_count,
  tor:          .ipqs.tor
}'

Extract IOC details:

scope-recon 1.2.3.4 --json | jq '.threatfox.iocs[] | {ioc, threat_type, malware, confidence_level}'

Bulk: filter to IPs with VirusTotal detections:

scope-recon --file targets.txt --json | jq '.[] | select(.virustotal.malicious > 0) | {ip, malicious: .virustotal.malicious}'

Bulk: identify TOR, VPN, and proxy nodes:

scope-recon --file targets.txt --json | jq '.[] | select(.ipqs.tor == true or .ipqs.vpn == true or .ipqs.proxy == true) | {ip, tor: .ipqs.tor, vpn: .ipqs.vpn, proxy: .ipqs.proxy}'

Bulk: ASN pattern analysis:

scope-recon --file targets.txt --json | jq -r '.[] | [.ip, (.bgpview.asn_description // "-"), (.ipapi.isp // "-")] | @tsv'

Watch Mode

scope-recon 1.2.3.4 --watch 60    # CLI: reprint every 60s
scope-recon 1.2.3.4 --watch 60    # TUI: auto-refresh every 60s

Watch mode always bypasses the cache — the purpose is fresh data. In the TUI, the interval is shown in dim in the footer: watch: 60s. The first tick fires after N seconds rather than immediately, since the TUI already ran queries at startup.

In CLI mode, reruns are separated by a ━━━ divider. This is useful for a monitoring terminal alongside an incident response session.

AI Analysis

The 12th source differs from the other eleven. It does not query a static intelligence feed — it synthesizes all other results into a narrative assessment.

The prompt is built from the full JSON-serialized ThreatReport and requests a 2–3 paragraph threat assessment. If ThreatFox has IOC matches, the prompt appends a request for per-malware-family context paragraphs. An IP with Cobalt Strike and Emotet matches, for example, will receive a dedicated paragraph explaining each family.

AI analysis fires sequentially rather than concurrently because it depends on all other sources. It is spawned in a tokio::spawn only after all_sources_terminal() returns true — every source has reached Done, Error, or Skipped.

In the TUI, the request uses stream: true and consumes the SSE response via bytes_stream(). Each token delta sends a SourceUpdate::AiAnalysis with the accumulated text. The channel is the same one used by all other sources — the event loop does not differentiate between a one-time update and a repeatedly accumulating one.

In the CLI path, the blocking completion endpoint is used. The full response arrives and renders after all other source output.

The model is x-ai/grok-4.1-fast via OpenRouter. Requires OPENROUTER_API_KEY. Optional — exclude it with --only or simply do not set the key.

The Cache

Queries are cached at ~/.cache/scope-recon/{ip}.json with a default one-hour TTL, configurable via --cache-ttl (set to 0 to disable).

Cache entries are the full serialized ThreatReport — the same struct that --json outputs. To inspect an entry:

cat ~/.cache/scope-recon/8.8.8.8.json

To clear an entry:

rm ~/.cache/scope-recon/8.8.8.8.json

Freshness is checked against the queried_at timestamp embedded in the report — no separate metadata table is used. A future timestamp caused by clock skew is treated as expired.

In the TUI, cache hits are applied before any tasks are spawned. Sources already populated from cache skip their tokio::spawn entirely. If all sources are cached, no network traffic occurs and the TUI opens fully populated. The r key forces TTL to zero on a cloned Cli before calling spawn_queries, bypassing the cache entirely.

Architecture

Shared HTTP client

One reqwest::Client is constructed at startup with a 15-second timeout. It is stored in ApiKeys and passed as &client to every fetch function. reqwest::Client is Arc-backed, so client.clone() within tokio::spawn closures is a cheap reference count increment — no socket duplication.

The 15-second timeout is the difference between tolerating a slow API response and hanging indefinitely. Every source either responds or times out; the tool always finishes.

SourceState<T> as a state machine

Every source in App is typed as SourceState<T>:

pub enum SourceState<T> {
    Loading,
    Done(T),
    Error(String),
    Skipped,
}

State transitions are one-way. The TUI event loop pattern-matches on state to determine what to render: spinner frames for Loading, a green for Done, a red for Error, and a dim - for Skipped. The detail pane extracts the inner value from Done(v) and dispatches on the selected source index.

The tokio::select! loop

The TUI event loop selects on four things simultaneously:

tokio::select! {
    _ = tick.tick() => {
        app.tick = app.tick.wrapping_add(1);
    }
    Some(Ok(ev)) = event_stream.next() => {
        app.handle_key(ev);
    }
    Some(update) = rx.recv() => {
        app.apply_update(update);
        maybe_trigger_ai(&mut ai_triggered, &app, &keys, &cli, tx.clone()).await;
    }
    _ = async {
        if let Some(ref mut w) = watch_interval {
            w.tick().await;
        } else {
            std::future::pending::<()>().await
        }
    } => {
        app.refresh_requested = true;
    }
}

The watch interval arm uses std::future::pending() when watch mode is off — a future that never resolves. The select never picks that arm. No polling, no allocation, no overhead.

After every branch, the UI re-renders. The verdict in the header is rebuilt from scratch on every frame.

Verdict logic

compute_verdict() is a pure function: it accepts a ThreatReport and returns (&'static str, Vec<String>). No state, no side effects. The TUI calls it on every render by constructing a partial report from current App state. The CLI calls it once at the end. The function and its thresholds are identical across both paths.

SourceMALICIOUSSUSPICIOUS
ThreatFoxAny IOC match
VirusTotalAny malicious detectionAny suspicious detection
AbuseIPDBScore ≥ 75Score 25–74
GreyNoiseclassification == “malicious”
OTXAny pulse
IPQSFraud score ≥ 75Score 30–74
PulsediveRisk = high or criticalRisk = medium

IPQS TOR, proxy, and VPN flags are always surfaced in findings regardless of fraud score, because they provide meaningful context even when the numeric score is low. GreyNoise RIOT membership always appends "known benign infrastructure (RIOT)" to findings even for CLEAN verdicts.

MITRE ATT&CK Alignment

scope-recon is a defensive tool, but the intelligence it aggregates maps directly to adversary techniques observed across threat intelligence feeds:

  • T1071 — Application Layer Protocol: Open port and service banner data from Shodan surfaces non-standard C2 channel configurations (e.g., Tor traffic on port 9001/tcp).
  • T1090 — Proxy: IPQS TOR, VPN, and proxy flags, and GreyNoise noise classification, identify IPs used as anonymization infrastructure consistent with this technique.
  • T1583 / T1584 — Acquire / Compromise Infrastructure: ASN and geolocation data from ip-api, RIPE Stat, and IPinfo contextualizes whether an IP belongs to known bulletproof hosting, consumer ISP, or compromised cloud infrastructure — directly relevant to infrastructure acquisition analysis.
  • T1596 — Search Open Technical Databases: scope-recon itself performs this technique programmatically, querying Shodan, RIPE Stat, and public DNS to enumerate infrastructure context.
  • T1102 — Web Service: ThreatFox and OTX pulse matches surface IPs associated with C2 communication over web services.

Setup

API Keys

All keys are read from environment variables. The recommended approach is to add them directly to ~/.zshrc rather than exporting them at the terminal — inline export commands go to shell history and may expose keys.

# In ~/.zshrc
export SHODAN_API_KEY=your_key_here
export VIRUSTOTAL_API_KEY=your_key_here
export ABUSEIPDB_API_KEY=your_key_here
export GREYNOISE_API_KEY=your_key_here
export THREATFOX_API_KEY=your_key_here
export IPQS_API_KEY=your_key_here
export PULSEDIVE_API_KEY=your_key_here
export OTX_API_KEY=your_key_here
export IPINFO_TOKEN=your_token_here
export OPENROUTER_API_KEY=your_key_here

After saving the file:

chmod 600 ~/.zshrc
source ~/.zshrc

The secure handling of credentials extends to API keys — treat them as secrets equivalent to passwords. Missing keys degrade gracefully: key-required sources show [source unavailable] in CLI output or in the TUI, and the verdict is still computed from whatever data arrived.

Shell completions

scope-recon --completions zsh  > ~/.zfunc/_scope-recon
scope-recon --completions bash > /etc/bash_completion.d/scope-recon
scope-recon --completions fish > ~/.config/fish/completions/scope-recon.fish

Completions are implemented via clap_complete. The --completions flag is hidden from --help.


The source code is available at https://github.com/nethoundsh/scope-recon. Issues and pull requests are open.