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.

nmap installs natively on Windows, but the pipeline below assumes a POSIX shell.subfinder, httpx, nmap, plus dig, jq, curl. No specific versions are pinned here; check with --version after install.<verify in documentation>.Install the tooling on Debian/Ubuntu:
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:
subfinder -version
httpx -version
nmap --version | head -n 1
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:
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:
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:
{"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:
jq -r -s '.[] | [.input, .url, (.status_code|tostring), (.tech[0] // "unknown")] | @csv' \
raw/http.json > out/inventory.csv

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 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>):
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:
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.

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:
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 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 projectcompleted for all runsProblem. Subdomain enumeration returns an empty file.
subfinder: no results found and wc -l raw/subdomains.txt prints 0dig +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.
curl: (60) SSL certificate problem: unable to get local issuer certificatecurl does not trust.curl at it — do not disable verification: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.
HTTP/1.1 401 Unauthorized with a body such as {"error":"invalid_token"}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.
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.
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.