---
name: CTI Expert
slug: cti-expert
category: AI Engineering
description: CTI Expert runs structured cyber threat intelligence and OSINT investigations, with sourced and trust-scored findings. Use it for domain, identity, exposure, breach, and infrastructure recon, plus reporting and case summaries.
github: "https://github.com/7onez/cti-expert"
language: Python
stars: 562
forks: 81
install: "npx degit https://github.com/7onez/cti-expert ~/.claude/skills/cti-expert"
installs_to: ~/.claude/skills/cti-expert
source_path: SKILL.md
collection_size: 1
category_size: 2451
added: 2026-08-25T05:14:40.395Z
last_synced: 2026-08-25T05:14:40.395Z
canonical_url: "https://dirskills.com/skills/cti-expert"
---

# CTI Expert

CTI Expert runs structured cyber threat intelligence and OSINT investigations, with sourced and trust-scored findings. Use it for domain, identity, exposure, breach, and infrastructure recon, plus reporting and case summaries.

**Install:**

```bash
npx degit https://github.com/7onez/cti-expert ~/.claude/skills/cti-expert
```

## README

# CTI Expert

Cyber threat intelligence and open-source intelligence skill. Turns Claude into a trained CTI/OSINT analyst. Generates precision search queries, interprets public data, builds case timelines, and delivers structured intelligence products — no API keys, no paid subscriptions.

> **Runs anywhere.** Works in **Claude Code** (Desktop & CLI) and in **OpenAI Codex / ChatGPT** and other `AGENTS.md`-aware agents — see [`AGENTS.md`](AGENTS.md) for the cross-agent runtime contract. Throughout this file, **`$SKILL_DIR`** = the directory containing this `SKILL.md` (Claude Code: `~/.claude/skills/cti-expert`; Codex/manual clone: the repo you are working in). Resolve it by locating `SKILL.md` — never hard-assume `~/.claude`. Detect the OS once (Windows/macOS/Linux) and prefer **uv** for all Python — see §13 Tool Auto-Install Policy.

Collection method: `agent-browser` when available (JavaScript-heavy sites, infinite-scroll, screenshot evidence), with automatic fallback to web search / web fetch / direct URL fetch. Tool limitations are logged as collection gaps — never as case blockers.

---

## 1. Quick Start

```bash
# Full autonomous case — runs every applicable technique
/case target.com

# Guided flow for first-time investigators
/flow person

# Summary of what's been found so far
/brief
```

Append `--yolo` to any command to skip all interactive prompts and confirmations. The analyst makes every decision autonomously.

---

## 2. AEAD Case Lifecycle

Every investigation follows four phases:

| Phase | What Happens |
|-------|-------------|
| **Acquire** | Collect raw data — `/sweep`, `/query`, `/username`, `/phone`, `/email-deep`, `/subdomain`, `/webpivot` + `/icp` (domain/URL targets), `/cn-corp` · `/iban` · `/hash-id` on discovery |
| **Enrich** | **Recursive pivot loop** — the [pivot orchestration engine](engine/pivot-orchestration.md) treats every discovered identifier as a new seed and expands the graph hop-by-hop (`/branch`, `/crossref`, `/link-subjects`, `/signatures`) **automatically until the frontier is exhausted**, no approval prompts (`autonomy=auto`). Acquire↔Enrich iterate, not run once. |
| **Assess** | Score and verify — `/exposure`, `/threat-model`, `/validate`, `/coverage`, `/verify-finding`. Judgments carry **likelihood terms**, coverage gets the **5W1H pass**, attributions get an **ACH matrix** ([`handbook/analytic-standards.md`](handbook/analytic-standards.md)) |
| **Deliver** | Package output — `/report`, `/brief`, `/render`, `/workspace save` — **auto-saves .md + .html + .json + .csv + IOC bundle** |

Run `/progress` at any point to see which phase you're in and what's pending.

> **`/case` and web-infra pivoting.** For a **domain or URL** target, `/case` includes
> web-infrastructure pivoting (`/webpivot`) in the Acquire phase. It runs **keyless by default**
> (crt.sh + passive DNS + anonymous urlscan) and **upgrades automatically when premium keys are
> set** via `/apikeys` (Shodan/Censys/FOFA/DNSLytics/SecurityTrails/urlscan-PRO/WhoisXML). Because
> `/webpivot` can fetch the target directly, for hostile infrastructure it prefers passive capture
> (urlscan/Wayback) — see [`techniques/web-pivot.md`](techniques/web-pivot.md). It is **not** run for
> username/phone/person targets.
>
> **Archive IOC harvest runs by default too.** For domain/URL targets the Acquire phase also runs
> `wayback_harvest.py <domain> --indicators` (add `--urlscan` when `URLSCAN_API_KEY` is set),
> harvesting **emails, phones, crypto wallets, tracking/verification IDs, SaaS-operator IDs, and
> socials from the *entire* Wayback history** — not just the live page — with first-seen/last-seen
> per selector. It writes case-schema `indicators[]` to `<case>/raw/harvest.indicators.json`, which
> merge into the case and flow into the **auto-saved IOC bundle** at Deliver. This is the step that
> recovers selectors a network later scrubbed — across the whole snapshot corpus, not just the live page.
> Passive by construction — only web.archive.org (+ urlscan.io if keyed), never the target.
>
> **The five v2.6 commands are in the pipeline too — no flags.** `/icp` runs for every
> domain/URL/org target (and an IP's resolved hostname); `/cn-corp`, `/iban` and `/hash-id`
> fire the moment a company name/USCC, payment detail, or hash appears — and all three feed
> their yields **back into the recursive pivot loop** as new seeds, so an ICP licence serial or
> a reused bank account expands the graph like any other node. `/redact` is the exception: it
> is **opt-in** (`--redact`), because a redacted report is a weaker artifact and that should
> always be a deliberate choice. Full trigger table: §Technique Activation Matrix.
> Narrow with `--no-cn`.

> **Two layers, one skill: broad collector → deep pipeline.** cti-expert is the **broad
> collector** — the wide net of Acquire/Enrich commands (`/webpivot`, `/sweep`, `/subdomain`,
> `/icp`, `/username`, `/email-deep`, `/breach-deep`, …) that pull artifacts from anywhere. The
> **`intel_engine` engine is now vendored in-repo under `intel_engine/`** (`intel_engine/harness/`,
> `intel_engine/tools/`, `intel_engine/WebPivot/`, `intel_engine/IntelGraph|IntelReport|BinaryPivot|IntelAnalysis/`)
> and supplies the **pipeline chains + deeper pivoting logic**: a persistent knowledge base (`intel_engine/knowledge/`), versioned cases
> (`cases/`), cross-case correlation, calibrated assessment, and rendering.
>
> **The chain:** broad collection (cti-expert) → the pipeline (`/pipeline`, `/harness`) ingests it,
> then applies the deep logic — *"seen this operator before?"* (`/recall`), whole-KB clustering
> (`/kb --cluster`, `/cert-overlap`), false-positive control (`/reference`), risk scoring
> (`/risk`), hypothesis generation, confidence calibration, and a versioned `Assessment`. The
> pipeline drives cti-expert's own `scripts/webpivot/pivot_extract.py` collector, so the broad and
> deep layers share one artifact shape end-to-end.
>
> **Self-contained & self-resolving.** `/backend` resolves to **SELF** (in-repo) — no external
> setup. Deps: `uv venv && uv pip install -r requirements.txt` (harness SDK/MCP + IntelGraph
> renderers; the collector + KB + deterministic pipeline are stdlib and need none). An explicit
> `$INTEL_HOME` still overrides for a shared external KB. Full architecture, the op map, and the
> evidence-envelope schema: [`connectors/intel-backend.md`](connectors/intel-backend.md).

---

## 2.5. Pivot Priority & False-Positive Control (CRITICAL)

Two failure modes ruin a cluster: asserting a link that isn't there, and missing one that is.
This section governs both. Apply it in Enrich, before anything reaches a report.

### Pivot priority ladder

Work **down** this ladder. Never assert same-operator on a lower rung when a higher rung is
available or contradicts it. Tag every asserted link in the report with the rung it rests on.

| Rung | Indicator | Strength |
|---|---|---|
| 1 | Registrant email / phone / org — **including historic WHOIS** | decisive |
| 2 | One domain carrying **two identities across its own WHOIS history** | decisive — proves an alias |
| 3 | Site-verification token (Google Search Console, etc.) | decisive — proves account control |
| 4 | Shared TLS certificate / SAN cross-cover | strong |
| 5 | Nameserver delegation to a host the operator **runs themselves** | strong — proves zone control |
| 6 | APK signing certificate | strong |
| 7 | Distinctive favicon / analytics / tracker / backend tenant ID | moderate — verify below |
| 8 | Co-tenancy on a **dedicated** host (few tenants) | moderate |
| 9 | Site template / framework / kit | **weak — kit-level, never operator-level** |
| 10 | Co-tenancy on **shared/reseller** hosting; managed-provider nameservers | information, not a link |

**Reverse-WHOIS is the highest-yield pivot here.** Always `mode=preview` first — the count is
free. A term returning hundreds is shared boilerplate; do not purchase it.

### Mandatory false-positive control

Before any indicator becomes a cluster edge, run `/reference check <value>`. If it returns
UNKNOWN, **decide and record it** with `/reference add` so the next case inherits the judgement.

Six traps, all of which have produced real false clusters:

| Trap | Why it fools you | Test |
|---|---|---|
| **Commodity site kit** | A template sold to hundreds of unrelated fraud operators | Search the template path in urlscan/FOFA — a large population means kit-level |
| **Privacy-proxy contacts** | The registrar's boilerplate phone/email, shared by every customer of that service | Reverse-WHOIS it; a spread of unrelated domains means noise |
| **Shared/reseller hosting IP** | A 20+-tenant cPanel box links nothing | Count tenants before clustering |
| **Managed-provider nameservers** | Cloudflare/GoDaddy/Gandi/Wix NS are shared by millions | Self-hosted NS is rung 5; provider NS is rung 10 |
| **Org-name collision** | A registrant org string that also matches a real, unrelated company | Reverse-WHOIS the org; inspect what comes back before attributing |
| **Shared analytics / tag container** | Often one web developer reusing a container across unrelated clients | **Check domain creation dates** — a decade-old business sharing a tag with a new fraud domain is a third party |

> **Never put an unvalidated indicator into a report that recommends abuse reporting.** Naming an
> uninvolved business is the most damaging error this skill can produce. When a cluster rests on a
> single rung-7-or-below indicator, label it *candidate, single-indicator* — not a cluster member.

### Never submit the case's own sample to a public sandbox (CRITICAL)

`/anyrun` is **lookup-only**. It reads detonations that already happened; it has no submit path,
and the submission endpoint is deliberately absent from `BinaryPivot/references/anyrun.json`.
`tests/test_no_sample_submission.py` enforces that as a gate, so it cannot regress quietly.

**Do not work around it.** Uploading the case's own APK / installer / archive to ANY.RUN —
or VirusTotal, or any public sandbox — is an **outbound, irreversible** act:

- A public task is **world-readable**: the file, its hash, screenshots and full network log.
- **Operators watch for their own samples.** The standard response is to rotate the backend,
  revoke the signing key and re-skin the front — destroying the infrastructure the case is built
  on, often days before a takedown or referral can land.
- **It cannot be recalled.** Unlike a query from the wrong egress, there is no cleanup.

If detonation is genuinely necessary, **stop and put it to the analyst in plain terms** — what
becomes public, and that it is permanent — and let them do it themselves in the sandbox UI on a
**private** plan. Never as a side effect of a pivot, and never on standing permission inferred
from an earlier approval. The same reasoning governs `--submit` (urlscan/Wayback): a public
urlscan scan of a live scam funnel is visible to the operator too.

### A permuted email is a hypothesis, never a finding (CRITICAL)

When a case yields a **real person's name** or a **username**, and you already hold a domain that
matters to the case, run **`/email-permute`**. An operator's mailbox is almost never published, but
it is usually *derivable* — mail hosts use a small set of local-part conventions, and the operator's
own domain is the highest-yield thing to permute against.

That value comes with a matching hazard, so this rule is absolute:

- **Permute against the case's own domains.** Name × the operator's domain is a narrow, high-prior
  question. Name × `gmail.com` is volume with no prior behind it — `--free` exists, is capped, and
  should be a deliberate choice, not a reflex.
- **Never ingest a candidate into the KB, cite one in a report, or contact one.** A fabricated
  address that reaches `kb_ingest` becomes a shared indicator, and a shared indicator merges two
  operator clusters. A permutator wired straight into correlation does not enrich a case — it
  silently names an innocent party. This is the same failure RULE 5 exists to prevent.
- **Candidates are not seeds.** They never enter the spider-map frontier. Only an address in the
  tool's `promote` list — corroborated by *independent* evidence (Gravatar registration, breach
  corpus, a GitHub commit, a page/DOM hit, a dork) — may be treated as a real email seed, and that
  promotion is an analyst decision.
- **Never validate over SMTP.** `RCPT TO` probing connects to the *target's* mail server, which the
  egress posture exists to prevent on a hostile case; and a catch-all domain answers `250` for
  every address ever tried, so it manufactures confidence instead of measuring it. Use `--verify`,
  which gates on MX (RFC 7505 null MX included) and checks Gravatar — both keyless, neither
  touching the target.

State the status in the turn. *"12 candidates, 0 corroborated"* is an honest result; presenting
those 12 as discovered addresses is not.

### Dead seed? Do not stop

Zero pivots, a parked page, or NXDOMAIN is not an answer. Run **`/fallback <domain>`** — crt.sh,
the full Wayback timeline, archive.today, and the local KB. A parked apex frequently has live
subdomains: enumerate CT and the Wayback CDX host histogram before writing a seed off. Report an
empty result as empty; a collector that returned nothing is a finding, not something to omit.

---

## 3. Command Reference

### 3.0 Entry point & registered commands

**`/cti <target>` is the single entry to this skill.** It routes any target type — domain, IP,
email, username, phone, wallet, hash, APK — through recall → collect → cluster → assess. Plain
English works identically ("analyze example.com and pivot the infrastructure"); the command form
just removes ambiguity.

Eight commands are **registered with Claude Code** by `bash scripts/register.sh` and work from a
cold prompt in any project:

| Command | Does | Equivalent T2 op | Equivalent T1 tool |
|---|---|---|---|
| **`/cti <target>`** | **entry point — routes by target type** | *(whole chain)* | *(whole chain)* |
| `/cti-recall <seed>` | seen before? **run first, always** | `recall` | `domain_verdict`, `which_cases` |
| `/cti-case <ID> <seeds>` | full deterministic pipeline | `pipeline open` | *(none — CLI only)* |
| `/cti-pivot <url\|ip>` | collect one target | `pivot-extract` | `pivot_extract` |
| `/cti-cluster <domain>` | correlate & expand | `kb`, `cert-overlap` | `kb_cluster`, `cert_overlap` |
| `/cti-check <indicator>` | false-positive control | `reference check` | `reference_check`, `reference_add` |
| `/cti-report <ID>` | render graph + PDF/DOCX | `graph`, `report` | `render_diagram`, `render_report` |
| `/cti-status` | backend / MCP / credits health | `backend.py status` | `api_usage` |

> **Every other `/command` in §3 is a convention read from this file, not a registered command.**
> Once the skill is loaded they are unambiguous instructions; typed at a cold prompt they do
> nothing. When in doubt use `/cti` and describe the goal.

**Three layers, one operation.** The same capability is reachable three ways and the names differ
by layer — T0 uses `kebab-case` after a slash, T2 uses `kebab-case` ops, T1 uses `snake_case`
tools. The table above is the canonical mapping; when you add a capability, add a row here in the
same commit or the layers drift apart again.

Capabilities that are *not* registered commands still carry their layer mapping inline in the §3
tables. The engine's WebPivot/BinaryPivot collectors add these: `/capabilities` (T2 `capabilities`,
T1 `capability_check`), `/impersonate` (T2 `impersonate`, T1 `impersonation_hunt`), `/search-pivot`
(T2 `search-pivot`, T1 `search_pivot`), `/censys` (T2 `censys`, T1 `censys`), `/intelx`
(T2 `intelx`, T1 `intelx_search`) and `/anyrun` (T2 `anyrun`, T1 `anyrun_lookup`).

---

Commands grouped by AEAD phase.

### Acquire

| Command | What It Does | Example |
|---------|-------------|---------|
| `/case [target]` | Full pipeline — runs every applicable technique | `/case example.com` |
| `/sweep [target]` | Multi-vector recon on any target type | `/sweep @username` |
| `/query [subject]` | Builds 12–15 advanced search operator queries | `/query example.com` |
| `/username [handle]` | Enumerate handle across 3000+ platforms | `/username johndoe` |
| `/phone [number]` | Carrier, line type, reputation, public associations | `/phone +84901234567` |
| `/email-deep [email]` | Accounts, breach history, infrastructure | `/email-deep u@domain.com` |
| `/subdomain [domain]` | CT logs, brute-force, passive enumeration; flags admin/sensitive subdomains (`admin`,`adm`,`kef`,`ador`,`panel`…) per `handbook/admin-endpoint-indicators.md` | `/subdomain example.com` |
| `/breach-deep [email]` | Multi-source breach lookup with context | `/breach-deep u@domain.com` |
| `/traffic [domain]` | Traffic estimation, ranking, audience data | `/traffic example.com` |
| `/visitors [domain]` | Full visitor intelligence: tech, geo, sources, analytics | `/visitors example.com` |
| `/techstack [domain]` | Technology fingerprint (CMS, analytics, CDN, server) | `/techstack example.com` |
| `/competitors [domain]` | Competitor & related site discovery | `/competitors example.com` |
| `/secrets [target]` | Exposed credentials in repos and paste sites | `/secrets github.com/org` |
| `/github-osint [target]` | GitHub user/org/repo recon: profiles, repos, code search, commits, forks | `/github-osint github.com/org/repo` |
| `/threat-check [target]` | IP/domain/URL/hash threat intelligence | `/threat-check 185.1.1.1` |
| `/scam-check [domain]` | Phishing/scam/malicious domain check | `/scam-check susp-site.xyz` |
| `/webpivot [url]` | Web-infra pivoting — extract favicon mmh3 / GA-GTM-AdSense / wallet / SaaS-operator artifacts from a page's DOM → ranked pivot queries (Shodan/PublicWWW/urlscan/FOFA). Flags: `--render`, `--crawl`, `--history` (Wayback GA), `--fetch` (pull archived page content — WebFetch can't reach Wayback), `--harvest` (full-IOC harvest across whole archive history → emails/phones/wallets/IDs/socials), `--whois`, `--graph` (cluster), `--rank` (score same-operator relations), `--cert` (cert-fingerprint pivot), `--suggest`, `--wallets`, `--paths`. See `techniques/web-pivot.md` (reverse-lookup engines per artifact → `handbook/pivot-services.md`) | `/webpivot https://scam-site.top` |
| *(automatic — no flag)* | Four layers now run on **every** collection and need no command. **Asset layer:** fetches the page's own JS bundles and re-runs every extractor over the source — the fix for SPA/white-label kits where the shell HTML is empty; yields off-apex `api_endpoint`/`websocket_endpoint` (the backend survives a front-end re-skin), `build_env:<KEY>` tenant tokens, `js_bundle_sha256`, and via `sourceMappingURL` the operator's own `dev_username`/`dev_project`. **SPA route table:** reads the app's router literals — `spa_route:admin`, `spa_route:funnel`, and a `spa_route_signature` that survives a re-skin. Zero extra requests, routes are leads only and are never fetched. **Well-known/policy files:** a fixed standards list (never a wordlist, no path brute-forcing) → `adstxt_publisher`, `apple_team_id`, `security_contact`. **JARM:** TLS-stack fingerprint of the server. Suppress with `--no-assets` / `--no-well-known`; cap fetches with `--assets-max N` | *(runs inside `/cti-pivot`)* |
| `/capabilities` | **Run this first, and again before reporting any "nothing found".** Which optional API keys are configured, and for each absent one the *evidence class that went unqueried* plus the free path that substitutes. A keyless run extracts every artifact but cannot **reverse** most of them — so "no sibling domains" with no FOFA/urlscan key is a fact about the credentials, not about the operator. Every collection also records this in `meta.capability`; carry the limitation statement into the assessment and cap confidence accordingly. T2: `capabilities` · T1: `capability_check` | `/capabilities` |
| `/impersonate [domain]` | Hunt **
