# Step-by-step guide: AI-first external attack surface assessment with CVEngine

> AI Release · @ai_release1 · https://ai-release.org/guides/poshagovoe-rukovodstvo-kak-provesti-otsenku-vneshney-po_en.html

## TL;DR
- Time to complete: 60–90 minutes (scope definition, inventory build, assessment run, triage)
- Difficulty level: Intermediate
- You will need: a Linux/macOS host or WSL2, outbound DNS and TCP, `dig`, `subfinder`, `httpx`, `nmap`, `jq`, `curl`, a written authorisation for every asset in scope, and an account on your CVEngine instance with permission to create projects and run assessments.

Terminology used below: **attack surface** — every host, subdomain, IP address and open port your organisation exposes to the internet; **ASM** (attack surface management) — the process of discovering those assets and keeping the list current; **triage** — deciding which findings are real and which matter first.

Where this guide cannot confirm a CVEngine-specific menu label, API path, scan-profile name or token format from the source material, it is explicitly marked `<verify in documentation>`. Do not guess those values — open the API reference of your own CVEngine version.

## Requirements and setup
![Illustration: Requirements and setup](https://ai-release.org/guides/img/poshagovoe-rukovodstvo-kak-provesti-otsenku-vneshney-po-en-1.jpg?v=1792962575)

- **OS:** Linux (any current LTS), macOS, or Windows 11 with WSL2. `nmap` installs natively on Windows, but the pipeline below assumes a POSIX shell.
- **Resources:** 2 vCPU, 4 GB RAM, 10 GB free disk. Raw scan output grows fast — the JSON from a 500-host run is routinely several hundred MB.
- **Authorization:** a signed scope document naming in-scope domains, IP ranges, excluded hosts, the testing window and an emergency contact. Scanning assets you do not own is illegal in most jurisdictions.
- **Recon tooling:** `subfinder`, `httpx`, `nmap`, plus `dig`, `jq`, `curl`. No specific versions are pinned here; check with `--version` after install.
- **CVEngine access:** base URL of your instance, an account, and an API token. The header name and token format are deployment-specific — `<verify in documentation>`.
- **Network:** outbound DNS (UDP/53) and TCP/443 must work from the host that runs the discovery tools. Rate limits on your DNS resolver will silently truncate results.

Install the tooling on Debian/Ubuntu:

```bash
sudo apt update
sudo apt install -y dnsutils nmap jq curl
go install -v github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest
go install -v github.com/projectdiscovery/httpx/cmd/httpx@latest
export PATH="$PATH:$HOME/go/bin"
```

**Why it works:** `go install` drops the binaries into `$HOME/go/bin`, which is not on `PATH` by default — that single `export` is the difference between working tools and `command not found`.

**How to check:** each command prints usage text, not an error:

```bash
subfinder -version
httpx -version
nmap --version | head -n 1
```

## Step 1. Define scope and build the asset inventory
**What to do:** create a working directory, write the scope file first, then enumerate subdomains, probe them for live HTTP services, and export a normalised inventory.

**Why:** every later step — the CVEngine project, the AI ranking, the report — inherits these assets. An inventory built before the scope is written is how teams end up scanning a subsidiary nobody asked about.

Create the workspace and the scope file:

```bash
mkdir -p ~/asm-run/{raw,out} && cd ~/asm-run
cat > scope.yaml <<'EOF'
organisation: Example Corp
in_scope:
  - example.com
  - api.example.com
  - 203.0.113.0/24
out_of_scope:
  - billing.example.com
window: "2024-01-15 02:00Z/2024-01-15 06:00Z"
contact: security@example.com
EOF
```

**Example:** the file above declares `example.com` and the documentation IP range `203.0.113.0/24` as in scope, and excludes `billing.example.com` because it is hosted by a payment provider that forbids third-party scanning. `203.0.113.x` is the reserved documentation range — replace it with your real, owned range.

Enumerate and probe:

```bash
dig +short example.com A
subfinder -d example.com -all -o raw/subdomains.txt
sort -u raw/subdomains.txt -o raw/subdomains.txt
wc -l raw/subdomains.txt

httpx -l raw/subdomains.txt -ports 443,80,8443 -status-code -title \
      -tech-detect -json -o raw/http.json
```

**Example output** you should see — one JSON object per live host:

```json
{"input":"api.example.com","url":"https://api.example.com","status_code":401,"title":"Unauthorized","tech":["nginx"]}
```

**How to check:** `wc -l raw/subdomains.txt` returns a non-zero count, and `jq length raw/http.json` — run as `jq -s length raw/http.json` — returns fewer entries than the subdomain list, which is expected: only hosts answering on those ports appear. If the count is zero, DNS resolution failed; see Troubleshooting.

Normalise into the CSV that CVEngine ingests:

```bash
jq -r -s '.[] | [.input, .url, (.status_code|tostring), (.tech[0] // "unknown")] | @csv' \
   raw/http.json > out/inventory.csv
```

## Step 2. Load the inventory into CVEngine and run the AI-assisted assessment
![Illustration: Step 2. Load the inventory into CVEngine and run the AI-assisted assessment](https://ai-release.org/guides/img/poshagovoe-rukovodstvo-kak-provesti-otsenku-vneshney-po-en-2.jpg?v=1792962582)

**What to do:** create a project in CVEngine, import `out/inventory.csv` as the target list, attach the scope file, select the external assessment profile, and start the run. The AI layer then correlates the discovered hosts, services and versions, and ranks them by exposure.

**Why:** the platform's value is correlation — a stale `nginx` banner on an internet-facing host is not a finding by itself, but the same host with an exposed admin path and an end-of-life component is. Doing this by hand across hundreds of hosts does not scale.

If CVEngine runs on infrastructure you control, size and harden that server before the first run — the deployment notes in [How to add AI to your home server: self-hosted services guide](https://ai-release.org/guides/kak-podklyuchit-ii-k-domashnemu-serveru-poshagovoe-ruko.html?utm_source=tg&utm_medium=channel&utm_campaign=guide_inline&utm_content=guide_to_guide) apply to a self-hosted instance as well.

Import through the UI: **Projects → New project → Import targets → CSV upload**, then map the columns:

| CSV column | CVEngine field | Notes |
|---|---|---|
| `host` | target / hostname | required, one per row |
| `url` | endpoint | scheme + host + port |
| `status` | metadata | informational, not a finding |
| `tech` | technology tag | improves AI correlation |

If you prefer the API, this is the pattern — **the endpoint path, payload keys and auth header below are placeholders; confirm each against your version's API reference (`<verify in documentation>`)**:

```bash
export CVENGINE_URL="https://cvengine.example.com"   # your instance
export CVENGINE_TOKEN="<token-from-your-admin>"

curl -sS -X POST "$CVENGINE_URL/api/v1/projects" \
  -H "Authorization: Bearer $CVENGINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"external-asm-example-corp","targets_file":"out/inventory.csv","profile":"external"}'
```

**Why it works:** the request is idempotent per project name in most deployments, so re-running after a scope change updates the target list instead of creating duplicates. Verify that behaviour rather than assume it.

**How to check:** the API returns the created project with an ID, and `jq` confirms it:

```bash
curl -sS "$CVENGINE_URL/api/v1/projects" -H "Authorization: Bearer $CVENGINE_TOKEN" \
  | jq -r '.[] | "\(.id) \(.name) \(.targets_count)"'
```

Expected on screen: a line like `42 external-asm-example-corp 318`, where 318 matches `wc -l out/inventory.csv` minus the header. A `401` here means the token is wrong, not that the project is missing.

## Step 3. Verify results and triage AI-ranked findings
![Illustration: Step 3. Verify results and triage AI-ranked findings](https://ai-release.org/guides/img/poshagovoe-rukovodstvo-kak-provesti-otsenku-vneshney-po-en-3.jpg?v=1792962586)

**What to do:** wait for the assessment to reach a completed state, export the findings, and confirm the top-ranked ones manually before anyone acts on them.

**Why:** AI ranking orders your queue, it does not replace confirmation. A finding that survives a five-second manual check is a finding; one that does not is noise that costs the team trust in the tool.

Poll the run status, then export:

```bash
curl -sS "$CVENGINE_URL/api/v1/projects/42/assessments" \
  -H "Authorization: Bearer $CVENGINE_TOKEN" \
  | jq -r '.[] | "\(.id) \(.status) \(.started_at)"'

curl -sS "$CVENGINE_URL/api/v1/assessments/<assessment-id>/findings" \
  -H "Authorization: Bearer $CVENGINE_TOKEN" -o out/findings.json
jq -r '.[] | [.severity, .host, .title] | @tsv' out/findings.json | sort | head -n 20
```

**Example:** the top of that list should look like a prioritised queue — highest severity first, host and title readable:

```
critical   api.example.com    Exposed administrative interface
high       www.example.com    Outdated TLS configuration
medium     203.0.113.10       Service banner discloses version
```

Confirm each of the top items with a direct, read-only check:

| Finding type | Verification command | What confirms it |
|---|---|---|
| Exposed admin path | `curl -sS -o /dev/null -w '%{http_code}\n' https://api.example.com/admin` | `200` or `401` — the path exists |
| Open port | `nmap -Pn -p 8443 203.0.113.10` | `8443/tcp open` |
| TLS weakness | `nmap --script ssl-enum-ciphers -p 443 www.example.com` | weak ciphers listed in output |

Mark confirmed items as validated and the rest as false positive in CVEngine, then re-run the assessment so the next report reflects the corrected baseline. Keep external monitoring running between assessments — the approach in [How to Set Up VPS Monitoring via a Telegram Bot with Mini App](https://ai-release.org/guides/kak-nastroit-monitoring-vps-cherez-telegram-bota-s-mini.html?utm_source=tg&utm_medium=channel&utm_campaign=guide_inline&utm_content=guide_to_guide) covers alerting on host-level changes, which is the layer underneath this surface view.

**How to check:** the run is finished when every row of the status query reads `completed` rather than `running` or `queued`, and the findings export contains at least the hosts you expected. If `out/findings.json` is `[]`, the profile ran against an empty target list — recheck Step 2.

Success criteria:

- [ ] `out/inventory.csv` row count equals the target count shown in the project
- [ ] Assessment status is `completed` for all runs
- [ ] Top 20 AI-ranked findings reviewed, each marked validated or false positive
- [ ] At least one finding confirmed manually with a command from the table above
- [ ] Findings exported and attached to the ticket or report

## Troubleshooting

**Problem. Subdomain enumeration returns an empty file.**
- Error: `subfinder: no results found` and `wc -l raw/subdomains.txt` prints `0`
- Cause: the host has no outbound DNS, or the domain resolves only through an internal resolver.
- Solution: test resolution directly, then pass a resolver explicitly:
```bash
dig +short example.com A
dig @1.1.1.1 +short example.com A
subfinder -d example.com -r 1.1.1.1 -o raw/subdomains.txt
```

**Problem. HTTPS probe of the CVEngine instance fails before any request is sent.**
- Error: `curl: (60) SSL certificate problem: unable to get local issuer certificate`
- Cause: the instance uses an internal CA or a self-signed certificate that `curl` does not trust.
- Solution: fetch the CA chain from your administrator and point `curl` at it — do not disable verification:
```bash
curl -sS --cacert /etc/ssl/certs/corp-root-ca.pem "$CVENGINE_URL/api/v1/projects" \
  -H "Authorization: Bearer $CVENGINE_TOKEN" | jq length
```

**Problem. API calls return authorisation failures after a successful login in the browser.**
- Error: `HTTP/1.1 401 Unauthorized` with a body such as `{"error":"invalid_token"}`
- Cause: the browser session cookie is not an API token, or the token expired or lacks project-creation scope.
- Solution: issue a fresh token, re-export it, and re-test with a status-code-only call:
```bash
export CVENGINE_TOKEN="<new-token>"
curl -sS -o /dev/null -w '%{http_code}\n' "$CVENGINE_URL/api/v1/projects" \
  -H "Authorization: Bearer $CVENGINE_TOKEN"
```
A `200` means the token works; a repeated `401` means the scope, not the value, is wrong — `<verify in documentation>` for the required permission.

## Summary and next steps
You now have a scope file, a reproducible inventory pipeline, an assessment run in CVEngine, and a manually confirmed top of the findings queue. Next, schedule the pipeline as a weekly job, and close the remediation loop — patching internet-facing hosts follows the same manual-update discipline described in [Step-by-Step: How to Install a Windows 11 Security Update Manually via Microsoft Update Catalog](https://ai-release.org/guides/poshagovoe-rukovodstvo-kak-ustanovit-obnovlenie-bezopas.html?utm_source=tg&utm_medium=channel&utm_campaign=guide_inline&utm_content=guide_to_guide).

## FAQ

**What is an external attack surface assessment?**
It is an inventory and risk review of everything your organisation exposes to the internet — domains, subdomains, IP addresses, open ports and services — followed by triage of what is reachable and what is vulnerable.

**Do I need permission to scan my own company's domains?**
Yes, in writing. Get a scope document naming the in-scope hosts, excluded hosts, the testing window and a contact, even for internal work — subdomains are often hosted by third parties whose terms forbid scanning.

**Why verify AI-ranked findings manually?**
Because ranking is a prioritisation aid, not proof. A quick read-only check (`curl` status code, single `nmap` port probe) confirms the asset exists and behaves as reported, and stops false positives from consuming remediation time.

**How often should the assessment be re-run?**
Whenever the asset inventory changes, and on a fixed cadence — weekly is a common baseline. Re-run after every triage session so validated and false-positive marks are reflected in the next report.
