{"slug": "running-an-ai-first-external-attack-surface-assessment-with-cvengine", "title": "Running an AI-first external attack surface assessment with CVEngine", "summary": "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.", "body_md": "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.\n\nTwo 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.\n\nWhere 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.\n\n`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>`.\nOn Debian or Ubuntu:\n\n```\nsudo apt update\nsudo apt install -y dnsutils nmap jq curl\ngo install -v github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest\ngo install -v github.com/projectdiscovery/httpx/cmd/httpx@latest\nexport PATH=\"$PATH:$HOME/go/bin\"\n```\n\nThat 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:\n\n```\nsubfinder -version\nhttpx -version\nnmap --version | head -n 1\n```\n\nEvery 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.\n\n```\nmkdir -p ~/asm-run/{raw,out} && cd ~/asm-run\ncat > scope.yaml <<'EOF'\norganisation: Example Corp\nin_scope:\n  - example.com\n  - api.example.com\n  - 203.0.113.0/24\nout_of_scope:\n  - billing.example.com\nwindow: \"2024-01-15 02:00Z/2024-01-15 06:00Z\"\ncontact: security@example.com\nEOF\n```\n\nIn 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.\n\n```\ndig +short example.com A\nsubfinder -d example.com -all -o raw/subdomains.txt\nsort -u raw/subdomains.txt -o raw/subdomains.txt\nwc -l raw/subdomains.txt\n\nhttpx -l raw/subdomains.txt -ports 443,80,8443 -status-code -title \\\n      -tech-detect -json -o raw/http.json\n```\n\nYou get one JSON object per responding host:\n\n```\n{\"input\":\"api.example.com\",\"url\":\"https://api.example.com\",\"status_code\":401,\"title\":\"Unauthorized\",\"tech\":[\"nginx\"]}\n```\n\nSanity 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.\n\nThen flatten it into the CSV that CVEngine ingests:\n\n```\njq -r -s '.[] | [.input, .url, (.status_code|tostring), (.tech[0] // \"unknown\")] | @csv' \\\n   raw/http.json > out/inventory.csv\n```\n\nThe 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.\n\nIn the UI: **Projects → New project → Import targets → CSV upload**, then map the columns:\n\n| CSV column | CVEngine field | Notes | \n|---|---|---|\n| `host` | target / hostname | required, one per row | \n| `url` | endpoint | scheme + host + port | \n| `status` | metadata | informational, not a finding | \n| `tech` | technology tag | improves AI correlation | \n\nIf 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>`):\n\n```\nexport CVENGINE_URL=\"https://cvengine.example.com\"   # your instance\nexport CVENGINE_TOKEN=\"<token-from-your-admin>\"\n\ncurl -sS -X POST \"$CVENGINE_URL/api/v1/projects\" \\\n  -H \"Authorization: Bearer $CVENGINE_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"external-asm-example-corp\",\"targets_file\":\"out/inventory.csv\",\"profile\":\"external\"}'\n```\n\nMost 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:\n\n```\ncurl -sS \"$CVENGINE_URL/api/v1/projects\" -H \"Authorization: Bearer $CVENGINE_TOKEN\" \\\n  | jq -r '.[] | \"\\(.id) \\(.name) \\(.targets_count)\"'\n```\n\nYou 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.\n\nPoll until the run reaches a completed state, then pull the findings:\n\n```\ncurl -sS \"$CVENGINE_URL/api/v1/projects/42/assessments\" \\\n  -H \"Authorization: Bearer $CVENGINE_TOKEN\" \\\n  | jq -r '.[] | \"\\(.id) \\(.status) \\(.started_at)\"'\n\ncurl -sS \"$CVENGINE_URL/api/v1/assessments/<assessment-id>/findings\" \\\n  -H \"Authorization: Bearer $CVENGINE_TOKEN\" -o out/findings.json\n\njq -r '.[] | [.severity, .host, .title] | @tsv' out/findings.json | sort | head -n 20\n```\n\nThe top of that list should read like a prioritised queue — highest severity first, host and title legible:\n\n```\ncritical   api.example.com    Exposed administrative interface\nhigh       www.example.com    Outdated TLS configuration\n```\n\nIf 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.\n\nAI 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.\n\nThe 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.\n\n*This article was originally published on AI Release — [https://ai-release.org](https://ai-release.org) — and the channel at [https://t.me/ai_release1](https://t.me/ai_release1) has more walkthroughs like this one.*", "url": "https://wpnews.pro/news/running-an-ai-first-external-attack-surface-assessment-with-cvengine", "canonical_source": "https://dev.to/qunirice_verase_1ab0caeba/running-an-ai-first-external-attack-surface-assessment-with-cvengine-2i29", "published_at": "2026-11-05 10:17:34+00:00", "updated_at": "2026-11-05 10:19:38.736354+00:00", "lang": "en", "topics": ["ai-tools", "artificial-intelligence"], "entities": ["CVEngine", "subfinder", "httpx", "nmap", "ProjectDiscovery"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/running-an-ai-first-external-attack-surface-assessment-with-cvengine", "markdown": "https://wpnews.pro/news/running-an-ai-first-external-attack-surface-assessment-with-cvengine.md", "text": "https://wpnews.pro/news/running-an-ai-first-external-attack-surface-assessment-with-cvengine.txt", "jsonld": "https://wpnews.pro/news/running-an-ai-first-external-attack-surface-assessment-with-cvengine.jsonld"}}