# mhunt

Unified reconnaissance CLI with optional Mondoo Platform upload.

`mhunt` runs reconnaissance tools (nmap, Shodan, cloud asset discovery, web
technology fingerprinting) and can upload findings to Mondoo Platform as
VEX/FEX documents.

By default, findings are uploaded to Mondoo Platform using a service account
(from `--config` or `MONDOO_CONFIG_PATH`). If no credentials are available, the
tool runs in incognito mode automatically. Use `--incognito` to explicitly skip
upload even when credentials are available.

## Install

```sh
go install go.mondoo.com/mhunt/cmd/mhunt@latest
```

Or build from source:

```sh
make build      # produces ./mhunt
make install    # installs into $GOBIN
```

## Scanning

`mhunt scan <domain>` runs one or many recon modules against a single domain and
reports the combined result. Each module is also available as a **flat shortcut**
(`mhunt aws <domain>`, `mhunt shodan <domain>`, …). You always pass a bare
domain — the scheme/URL handling per module is automatic.

```sh
mhunt scan example.com                  # passive modules (default)
mhunt scan example.com -m aws,shodan    # a chosen set
mhunt scan example.com -m all           # every module
mhunt scan a.com b.com -m all           # several targets
mhunt scan example.com --format json -o out.json

mhunt aws example.com                   # single-module shortcut
mhunt shodan --api-key KEY example.com
```

With no `--module`, the passive/public modules run (`aws`, `azure`, `gcp`,
`webbom`, `dangling`). `shodan` (needs an API key), `nmap`, and `nuclei` (active
scans needing the `nmap`/`nuclei` binary) are opt-in via `-m`. A shortcut and
`scan -m <module>` share the exact same flags.

### Job config

A whole run — targets, which modules are enabled, their options, output, and
upload settings — can be saved as a YAML job config and replayed. Configure a
run with flags once and capture it with `--write-job`; replay it with `--job`
(the file is the source of truth — other flags are ignored):

```sh
# configure with flags, save the effective config, and run:
mhunt scan example.com example.org -m aws,nuclei \
  --nuclei-severity high,critical --write-job job.yaml

# replay it later (multi-target JSON is one document keyed by target):
mhunt scan --job job.yaml
```

```yaml
# job.yaml
targets: [example.com, example.org]
modules:
  aws:      { enabled: true }
  azure:    { enabled: true }
  gcp:      { enabled: true }
  webbom:   { enabled: true, max_phase: 3 }
  dangling: { enabled: true }
  shodan:   { enabled: false, api_key: "" }
  nmap:     { enabled: false, top_ports: 1000 }
  nuclei:   { enabled: true, severity: "high,critical", rate_limit: 1000, timeout: "30m" }
concurrency: 50
timeout: "5s"
output: { format: json, file: results.json }
incognito: false
no_download: false
```

### Continuous monitoring

One-shot recon shows the surface *now*; the EASM value is what *changed* — "3 new
hosts, 1 newly-exposed port since yesterday". `scan --diff` compares a run against
the last saved snapshot for each target and reports new/removed assets and
findings (or "baseline created" on the first run), then saves a new snapshot:

```sh
# first run establishes the baseline; later runs show the delta:
mhunt scan example.com --diff
mhunt scan example.com --diff      # → "+2 asset(s), +1 finding(s) since …"

# save a snapshot without diffing:
mhunt scan example.com --save-snapshot

# diff the two latest snapshots without re-scanning (--json for machine output):
mhunt diff example.com
```

Snapshots are normalized, sorted JSON (the discovered assets + findings, not every
module field) under `~/.mhunt/snapshots/<target>/<timestamp>.json` (override the
base with `$MHUNT_HOME`). A normal `scan` with neither flag writes nothing and
behaves exactly as before. The diff prints to stderr, so a `--format json` / `-o`
result on stdout stays clean. See [ADR 0016](docs/adr/0016-continuous-monitoring-diff.md).

### Graphing the attack surface

For a fast overview of the identified infrastructure and its vulnerable
domains/IPs, render the discovered assets + findings as a graph: the target at
the center, subdomains/hosts/IPs/services/buckets branching off it, open ports on
their hosts, and any vulnerable nodes highlighted by severity. The graph is built
from the same normalized snapshot the diff uses, so every module contributes for
free. See [ADR 0034](docs/adr/0034-infrastructure-graph.md).

The CLI emits the graph as **data** — Graphviz DOT (default) or the JSON
node/edge model — and leaves rendering an image to external tooling:

```sh
# graph a target's latest snapshot (read-only; no re-scan), pipe to Graphviz:
mhunt graph example.com | dot -Tsvg -o infra.svg

# the node/edge model for a custom viewer (cytoscape/d3/…):
mhunt graph example.com --format json -o infra.json

# or produce the graph from a fresh run (one scan, both outputs):
mhunt scan example.com -m all --graph infra.dot
mhunt scan example.com -m all --graph infra.json --graph-format json
```

The graph is the only view that carries **edges** and **per-node severity**; the
scan JSON is raw per-module data and the snapshot is a flat asset/finding
inventory. Beyond containment edges (target→subdomain, host→port), it draws
**pivot edges** between assets that share an EASM signal — the same favicon hash,
ASN, or TLS issuer — clustering likely-related infrastructure the way the
platform's attribution graph does ([ADR 0032](docs/adr/0032-easm-emission-contract.md)).

### Deploying on a VM (on-prem network scanning)

mhunt can run as a scheduled job on a VM inside a network, sweeping CIDR ranges
with `nmap`/`naabu` and uploading both findings and the discovered hosts (as
inventory assets) to Mondoo. See **[docs/deployment/on-prem.md](docs/deployment/on-prem.md)**
for the full setup — an example job config, systemd timer / cron units, and the
privilege (`setcap`) and logging guidance.

```sh
mhunt scan --job /etc/mhunt/network-scan.yaml
```

### External tool binaries

`nuclei` is downloaded automatically (pinned version, SHA-256 verified) into
`~/.mhunt/bin` the first time it's needed, if it isn't already on your `PATH`.
`nmap` has no portable binary, so it must be installed
([nmap.org/download](https://nmap.org/download)). Use `--no-download` to
disable all downloads; a missing required tool then errors (non-zero exit) with
an install hint. Override the cache location with `$MHUNT_HOME`.

### Cloud IP ranges

The `aws`, `azure`, and `gcp` modules match a target's resolved IPs against each
provider's published IP ranges to identify the owning cloud/service. The ranges
are fetched on first use from the authoritative sources
([AWS](https://ip-ranges.amazonaws.com/ip-ranges.json),
[GCP](https://www.gstatic.com/ipranges/cloud.json), and the weekly Azure
[Service Tags](https://www.microsoft.com/en-us/download/details.aspx?id=56519)),
then cached under `~/.mhunt/ipranges` (override with `$MHUNT_HOME`) and refreshed
weekly — so a scan always uses fresh ranges without rebuilding mhunt. A small
embedded subset ships with the binary, so matching still works offline or under
`--no-download`.

### Modules

| Module     | Default | Description                                              |
| ---------- | :-----: | ------------------------------------------------------- |
| `aws`      |   ✓     | AWS public asset discovery                              |
| `azure`    |   ✓     | Azure / Entra ID public asset discovery                |
| `gcp`      |   ✓     | GCP public asset discovery                              |
| `webbom`   |   ✓     | Web technology fingerprinting                          |
| `dangling` |   ✓     | Dangling CNAME / subdomain-takeover check              |
| `httpx`    |   ✓     | HTTP probing (status/title/CDN/TLS) + favicon hash     |
| `discovery`|   ✓     | Subdomain / asset enumeration (wraps subfinder + dnsx) |
| `asn`      |   ✓     | ASN / owned IP-range discovery via Team Cymru DNS + dnsx PTR  |
| `breach`   |   ✓     | Known data breaches affecting the domain (HIBP)        |
| `whois`    |   ✓     | Domain registration lookup (RDAP)                      |
| `shodan`   |         | Shodan host/service & CVE lookup (needs API key)       |
| `nmap`     |         | nmap service/version port scan (needs nmap binary)     |
| `nuclei`   |         | nuclei template vulnerability scan (needs nuclei binary) |
| `crawl`    |         | web crawling / endpoint discovery (in-process katana)    |
| `cforigin` |         | Cloudflare origin-IP discovery — experimental (DNS-only; optional API keys) |

Every module converts its results to Mondoo Finding Exchange (FEX/VEX) documents
and uploads them automatically, unless `--incognito` is set or no credentials are
available (in which case results are still printed locally). Findings map to
severity by signal:

- **Dangling CNAMEs** → `HIGH` security (subdomain-takeover risk).
- **Publicly listable/readable buckets and Firebase DBs** → `MEDIUM` security when
  ownership is corroborated (name derived from the target, or a matching region),
  downgraded to `LOW` when the global-namespace name only loosely matches the
  target (likely a different org's resource). The `confidence` property records
  the tier.
- **Shodan CVEs** → VEX with the actual CVSS score; **nmap open ports** → `HIGH`.
- **nmap NSE vulnerability scripts** (`--nmap-profile vuln-detection`, or
  `--nmap-scripts vulners,vuln`) → `vulners` CVEs and CVE-referencing `vuln`-category
  probes become VEX (CVE id + CVSS); a CVE-less `vuln` probe becomes a severity-mapped FEX.
  Weak-TLS scripts (`ssl-enum-ciphers`, `ssl-cert`) → security FEX.
- **nuclei results** → CVE templates become VEX (CVE id + CVSS); other templates
  become severity-mapped FEX (info → informational, else security).
- **Discovered services/subdomains, web technologies, Shodan open services, and
  the per-provider cloud footprint** (detected managed services/regions/tenant) →
  informational asset/software inventory.

Every module's results map to FEX/VEX, so the findings uploaded to Mondoo match
what the result envelope shows.

## Output convention

Every recon command shares one convention:

- `-o/--output FILE` — write here; default stdout.
- `--format json|text` — default `text`. (`--json` is a hidden alias.)

A scan emits a combined JSON envelope keyed by module:

```json
{
  "domain": "example.com",
  "scanned_at": "2026-06-05T...",
  "modules": {
    "aws":    { "data": { ... } },
    "shodan": { "skipped": "no Shodan API key" }
  }
}
```

## Commands

| Command                   | Description                                                  |
| ------------------------- | ------------------------------------------------------------ |
| `scan <domain>`           | Run one or many recon modules against a domain               |
| `diff <target>`           | Diff the two latest snapshots of a target (no re-scan)       |
| `graph <target>`          | Graph a target's infrastructure (JSON/DOT) from its snapshot |
| `aws` / `azure` / `gcp`   | Per-module cloud asset-discovery shortcuts                   |
| `shodan` / `nmap` / `nuclei` | Per-module Shodan / nmap / nuclei shortcuts               |
| `dangling`                | Per-module dangling-CNAME shortcut (single domain, envelope) |
| `webbom-scan`             | Per-module web-fingerprint shortcut (domain, envelope)       |
| `dangling-cname <…>`      | Batch dangling-CNAME checker (file input, streaming NDJSON)  |
| `webbom`                  | WebBOM subtree (`build` / `serve` / `stats` / `db` / `scan`) |

The per-module shortcuts are generated from the module registry, so they are all
structurally identical and share the output convention above; each is equivalent
to `mhunt scan -m <module> <domain>`.

Two pairs are intentional — a uniform shortcut plus a specialized command:

- `dangling <domain>` (single domain, combined envelope) vs `dangling-cname`
  (batch file/list input, streaming NDJSON via `--json`, `--exit-code`).
- `webbom-scan <domain>` (uniform shortcut) vs `webbom scan <url>` (the subtree's
  URL-based scanner, with `--remote`/`--pretty` and the `build`/`serve`/`db`
  operations).

Run `mhunt <command> --help` for details, or `mhunt --version` for build info.

## Global flags

| Flag           | Description                                                       |
| -------------- | ---------------------------------------------------------------- |
| `--incognito`           | Skip upload to Mondoo Platform (local scan only)                |
| `--config`              | Path to Mondoo service account config (or `MONDOO_CONFIG_PATH`) |
| `--scope-mrn`           | Mondoo scope MRN — space or org (overrides config)             |
| `--no-download`         | Don't auto-download missing tool binaries; error if absent      |

## Development

Run `make help` to see all targets. Common ones:

```sh
make build       # build ./mhunt for the current platform (with version ldflags)
make test        # run all tests
make integration # live scans of scanme.nmap.org + contoso.com (needs network + nmap)
make lint        # run golangci-lint
make fmt         # gofmt (skips generated .pb.go)
make vet         # go vet
make tidy        # go mod tidy
make generate    # regenerate protobuf code (requires protoc + plugins)
```

The test strategy is layered (see
[ADR 0007](docs/adr/0007-testing-strategy.md)):

- **Unit tests** (fixtures) own conversion correctness — run on `make test`,
  gate every PR.
- **`make integration`** (`//go:build integration`) — live scans of
  `scanme.nmap.org` + `contoso.com`, asserting results and FEX/VEX findings.
  Excluded from `make test`; a required CI job.
- **Local vuln-target scans** (`//go:build vulnscan`, **not** in CI) verify that
  predictable vulnerabilities convert correctly:

  ```sh
  make scan-testphp     # nuclei + webbom @ testphp.vulnweb.com
  make scan-juiceshop   # webbom + nuclei @ juice-shop.herokuapp.com
  make integration-vuln # all of the above
  ```

  Some assertions are expected to fail today and surface gaps to fix (e.g.
  webbom is HTTPS-only, so it can't scan HTTP-only testphp).

### Cross-compilation

The binary is pure Go (`CGO_ENABLED=0`, `modernc.org/sqlite`), so cross-builds
need no C toolchain:

```sh
make xc OS=linux ARCH=arm64   # single target -> dist/mhunt_linux_arm64
make build/cross              # all linux/darwin/windows amd64+arm64 targets
```

Release artifacts are produced with GoReleaser via `.goreleaser.yml`.

### Protobuf

Protobuf definitions live in `internal/fex/fex.proto` and
`internal/recon/webbom/proto/webbom/v1/webbom.proto`. Regenerating requires
`protoc` with the Go and Ranger RPC plugins (see the `go:generate` directives in
those packages).

## Layout

```
cmd/mhunt             CLI entrypoint
internal/recon/cli     cobra command wiring (scan orchestrator + subcommands)
internal/recon/module  Module abstraction: Target, Registry, runner, output,
                       and one adapter per recon module
internal/recon/<tool>  the recon workers (aws, azure, gcp, shodan, dangling, webbom)
internal/fex           FEX document model, upload, and converters
```

Each recon module implements `module.Module`; the runner drives them uniformly
and the `module.FindingsReporter` interface centralizes FEX/VEX upload. See
`docs/adr/0001-unified-recon-scan-orchestrator.md` for the design. Implementation
packages live under `internal/` so they cannot be imported from outside this
module.
