Skip to content
Scanning

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 detail

Stage A — domain discovery

kartis-discovery takes a target domain and runs a set of discovery sources in parallel:

SourceWhat it does
WHOISRegistrant / nameserver pivots
CT logsCertificate Transparency subdomain harvest
DNS reconBrute-force / passive DNS / reverse lookups
IP-ASNASN expansion, neighboring prefixes
Tech fingerprintHTTP/TLS fingerprint → related hosts
AI correlationOptional 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:

  1. Runs port discovery, backed by nmap or a bare TCP-connect probe, at a chosen pacing profile.
  2. Runs every matching analyzer for each discovered service.
  3. 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)MeaningIntrusiveness
discoverPort/service discovery onlyBenign
passiveReads what the target offers, no crafted input (TLS cert/handshake read, tech fingerprint, WAF header sniff)Benign
activeSpeaks the protocol interactively but benignly (SSH/IKE/OpenVPN/STARTTLS negotiation)Benign
vulnSafe vulnerability detection, without exploitationNon-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).

CategoryModules
discoveryport scan
cryptotls, starttls, ssh, ike, openvpn, plaintext, certinfo — the PQC/crypto posture core of the product
webwaf (passive only)
infoleakinternal-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.yaml

Picks 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.yaml

Serves 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 worker

Both 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 checks

POST /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.