DEV Community

AI Release
AI Release

Posted on Originally published at ai-release.org

Running an AI-first external attack surface assessment with CVEngine

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.

What you need before you start

  • Linux (any current LTS), macOS, or Windows 11 with WSL2. nmap installs natively on Windows, but the pipeline below assumes a POSIX shell.
  • 2 vCPU, 4 GB RAM, 10 GB free disk. Output grows fast: the JSON from a 500-host run is routinely several hundred MB.
  • A signed scope document listing in-scope domains, IP ranges, excluded hosts, the testing window and an emergency contact.
  • subfinder, httpx, nmap, plus dig, jq and curl. No version pins here — check with --version after installing.
  • The base URL of your CVEngine instance, an account, and an API token with permission to create projects and run assessments. Header name and token format are deployment-specific: <verify in documentation>.
  • Working outbound DNS (UDP/53) and TCP/443 from the host running discovery. Resolver rate limits will silently truncate your results.

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Step 1: Write the scope file before you run anything

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
Enter fullscreen mode Exit fullscreen mode

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.

Step 2: Enumerate subdomains, probe for live HTTP, normalise

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
Enter fullscreen mode Exit fullscreen mode

You get one JSON object per responding host:

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

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
Enter fullscreen mode Exit fullscreen mode

Step 3: Load the inventory into CVEngine

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"}'
Enter fullscreen mode Exit fullscreen mode

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)"'
Enter fullscreen mode Exit fullscreen mode

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.

Step 4: Run the assessment and export findings

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Step 5: Verify before you escalate

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.

Takeaway

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.

Top comments (0)