Scanning
Eon Kartis runs discovery and scanning as a pipeline in three stages:
operator ──► kartis-discovery ──► kartis-scan
│ │
▼ ▼
kartis-recon services, findings,
(subdomain enumeration) TLS and PQC detailStage A — domain discovery
kartis-discovery takes a target domain and runs a set of discovery sources
in parallel:
| Source | What it does |
|---|---|
| WHOIS | Registrant / nameserver pivots |
| CT logs | Certificate Transparency subdomain harvest |
| DNS recon | Brute-force / passive DNS / reverse lookups |
| IP-ASN | ASN expansion, neighboring prefixes |
| Tech fingerprint | HTTP/TLS fingerprint → related hosts |
| AI correlation | Optional AI-assisted pass relating the results to the same organization |
Subdomain enumeration is handed to a dedicated worker, kartis-recon, which
also checks each candidate for liveness and captures its certificate and IP
address. If the worker is unavailable, discovery still completes.
Recursive organization discovery
Beyond a single domain’s subdomains, kartis-discovery run <domain> expands
into related same-organization domains (found via WHOIS/DNS-recon/reverse-IP
pivots) and enumerates each of those in turn, up to a configurable depth:
kartis-discovery run <domain> [flags]
--depth <int> related-expansion hops; 0 = unlimited (default 2)
--min-confidence <float> minimum confidence to expand a related domain
--max-related-per-job <int> per-job cap on related domains expanded
--max-jobs <int> per-tree total job cap
--timeout <duration> max time to wait for the tree to finish (default 30m)
--json emit JSON instead of a tree (default false)
--judge final AI review pass flagging related domains that may
not belong to the org — needs ai.api_key (default true)The command prints a job tree annotated with each related domain’s discovery
source and confidence, and — when --judge is enabled and an AI provider is
configured — a ⚠ review flag on relations that may not actually belong to
the seed organization.
Stage B — DNS filter and scan queue
Once discovery for a job finishes, every discovered domain is DNS-resolved and each resolvable host is queued for scanning.
There is no manual trigger for this stage in isolation; it always runs immediately after Stage A completes.
Stage C — per-host scan
kartis-scan daemon picks up queued runs. For each run it:
- Runs port discovery, backed by nmap or a bare TCP-connect probe, at a chosen pacing profile.
- Runs every matching analyzer for each discovered service.
- Checks the collected evidence for leaked internal IP addresses and records a finding per leak.
Depth and category model
Every scan chooses a max depth and a category filter. kartis
runs every module at or below the chosen depth, shallow-first:
Depth (max_level) | Meaning | Intrusiveness |
|---|---|---|
discover | Port/service discovery only | Benign |
passive | Reads what the target offers, no crafted input (TLS cert/handshake read, tech fingerprint, WAF header sniff) | Benign |
active | Speaks the protocol interactively but benignly (SSH/IKE/OpenVPN/STARTTLS negotiation) | Benign |
vuln | Safe vulnerability detection, without exploitation | Non-destructive |
There is deliberately no offensive/attack level: attack-shaped probing is
not reachable through the API or the scan chain. Requesting an unknown level
returns an error (HTTP 400 on the API).
| Category | Modules |
|---|---|
discovery | port scan |
crypto | tls, starttls, ssh, ike, openvpn, plaintext, certinfo — the PQC/crypto posture core of the product |
web | waf (passive only) |
infoleak | internal-IP-leak detection (ipleak) |
Omitting categories runs all four; passing a subset (e.g. ["crypto","web"])
scopes the scan.
CLI
kartis-scan scan — standalone, no DB, no daemon
Prints JSON to stdout. Best for fast iteration on a single host; runs nmap plus every analyzer at or below the chosen depth. Internal-IP-leak detection is only available for DB-backed runs.
kartis-scan scan <target> [flags]
--profile <name> scan profile / nmap pacing (default "polite")
--max-level <level> discover|passive|active|vuln (default "active")
--categories <list> discovery,crypto,web,infoleak (default: all)
--only <name> run a single chain module by name
--engine <name> port discovery engine: nmap|connect (default "nmap")
--json output JSON to stdout (default true)--engine connect uses a single TCP connect per port — the gentlest
liveness probe, with no nmap dependency.
kartis-scan scan example.com
kartis-scan scan example.com --profile normal --max-level passive --categories crypto--profile accepts polite, normal, or aggressive (see
scan profiles).
kartis-scan daemon — DB-backed dispatch
kartis-scan daemon -c /etc/kartis/kartis-scan.yamlPicks up pending runs and executes up to concurrent_runs (from the active
scan profile) at once. Runs left unfinished by a crash are marked failed.
kartis-scan serve — HTTP API
kartis-scan serve -c /etc/kartis/kartis-scan.yamlServes the bearer-token-authenticated /v1/... API (sessions, runs,
findings, services, discovery reads) that Eon Center, Eon Aethis, and Eon
Insights consume. See API below.
kartis-discovery / kartis-recon
kartis-discovery [daemon] # runs the discovery daemon (default with no subcommand)
kartis-discovery run <domain> # recursive organization discovery, see above
kartis-recon # runs the enumeration workerBoth take -c/--config <path> (defaults /etc/kartis/kartis-discovery.yaml
and /etc/kartis/kartis-recon.yaml) and -d/--debug.
API
kartis-scan serve exposes, under bearer-token auth:
POST /v1/sessions create a session (name + targets)
GET /v1/sessions list sessions
GET /v1/sessions/{uuid} get a session
POST /v1/sessions/{uuid}/runs start a run — this is where profile/max_level/categories go
GET /v1/sessions/{uuid}/runs list runs for a session
GET /v1/runs/{uuid} run status: pending|running|completed|failed
GET /v1/runs/{uuid}/findings findings for a run
GET /v1/runs/{uuid}/services discovered services for a run
GET /v1/services/{uuid}/tls TLS detail for a service
GET /v1/discovery/trees, /trees/{root}, /trees/{root}/hosts
POST /v1/discovery/runs, /runs/batch trigger discovery
GET /v1/discovery/certificates, /dns, /tech, /domains, /hosts, /aggregate
GET /v1/healthz, /v1/readyz unauthenticated health checksPOST /v1/sessions/{uuid}/runs accepts an optional body:
{ "profile": "normal", "max_level": "active", "categories": ["crypto", "web"] }An empty body uses the defaults (profile=polite, max_level=active, all
categories). Poll GET /v1/runs/{uuid} until it reports a terminal status
rather than assuming the run has completed when the POST returns.