cd /news/ai-tools/running-an-ai-first-external-attack-… Β· home β€Ί topics β€Ί ai-tools β€Ί article
[ARTICLE Β· art-164456] src=dev.to β†— pub= topic=ai-tools verified=true sentiment=Β· neutral

Running an AI-first external attack surface assessment with CVEngine

A security practitioner documented an end-to-end AI-first external attack surface assessment workflow that combines subfinder, httpx, dig, jq and nmap for asset discovery with CVEngine's AI-assisted correlation for triage, taking 60 to 90 minutes for a first pass. The workflow emphasizes written authorization for all in-scope assets, a scope.yaml inventory built before scanning, and flattening httpx JSON output into a CSV that CVEngine ingests for column mapping. The author notes that CVEngine-specific menu labels, API paths and token formats are marked for verification against each deployment's documentation.

by read5 min views1 publishedNov 5, 2026

I spend a lot of time on the least glamorous part of security work: keeping an accurate list of everything we actually expose to the internet. This is the loop I now run end to end β€” scope, inventory, an AI-assisted assessment in CVEngine, then hands-on triage. A first pass takes 60 to 90 minutes and sits at intermediate level: you should be comfortable with a shell, DNS and basic HTTP.

Two things before any command. First, get written authorisation for every asset in scope β€” scanning hosts you do not own is illegal in most jurisdictions, and "we thought it was ours" is not a defence. Second, three terms I use throughout: the attack surface is every host, subdomain, IP address and open port you expose; ASM is the ongoing job of discovering those assets and keeping the list current; triage is deciding which findings are real and which come first.

Where I could not confirm a CVEngine-specific menu label, API path, scan-profile name or token format from the source material, I mark it <verify in documentation>. Those values vary between deployments β€” read the API reference of your own version rather than guessing.

nmap installs natively on Windows, but the pipeline below assumes a POSIX shell.subfinder, httpx, dig, jq and curl. No version pins here β€” check with --version after installing.<verify in documentation>. On Debian or 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"

That last line does more than it looks. go install drops binaries into $HOME/go/bin, which is not on PATH by default; skip the export and you get command not found, which reads like a broken install. Confirm each tool prints usage text rather than an error:

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

Every later step inherits these assets β€” the CVEngine project, the AI ranking, the report. Build an inventory before writing the scope down and you will eventually scan a subsidiary nobody asked about.

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

In this example 203.0.113.0/24 is the reserved documentation range β€” swap in a range you actually own. billing.example.com is excluded because a payment provider hosts it and forbids third-party scanning.

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

You get one JSON object per responding host:

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

Sanity check: wc -l raw/subdomains.txt should be non-zero, and jq -s length raw/http.json should return fewer entries than the subdomain list. That gap is expected β€” only hosts answering on those ports appear. If the count is zero, DNS resolution failed.

Then flatten it into the CSV that CVEngine ingests:

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

The platform's value is correlation. A stale nginx banner on an internet-facing box is not a finding by itself; the same box with an exposed admin path and an end-of-life component is. Doing that matching by hand across hundreds of hosts does not scale.

In 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 would rather automate it, the API pattern looks like this β€” endpoint path, payload keys and auth header are placeholders, so confirm each one 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"}'

Most deployments treat this request as idempotent per project name, so a re-run after a scope change updates the target list instead of creating duplicates β€” verify that on your own instance rather than assuming it. Check what landed:

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

You want a line like 42 external-asm-example-corp 318, where 318 equals wc -l out/inventory.csv minus the header row. A 401 here means the token is wrong, not that the project is missing.

Poll until the run reaches a completed state, then pull the findings:

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

The top of that list should read like a prioritised queue β€” highest severity first, host and title legible:

critical   api.example.com    Exposed administrative interface
high       www.example.com    Outdated TLS configuration

If CVEngine runs on hardware you control, size and harden that box before the first scan. The same deployment hygiene you would apply to any self-hosted service applies here too.

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, and noise costs you the team's trust in the tool. So open the top entries by hand β€” hit the endpoint, read the TLS configuration, look for that exposed admin path β€” and only then assign an owner.

The scanner is the cheap part. What keeps an external attack surface assessment honest is the order of operations: written scope first, inventory second, AI correlation third, human confirmation last. Get that sequence right and a 90-minute run produces a queue your team can actually act on; skip a step and you produce a large file nobody trusts.

This article was originally published on AI Release β€” https://ai-release.org β€” and the channel at https://t.me/ai_release1 has more walkthroughs like this one.

── more in #ai-tools 4 stories Β· sorted by recency
── more on @cvengine 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/running-an-ai-first-…] indexed:0 read:5min 2026-11-05 Β· β€”