Posting Scan Results to Pental: the Ingestion API
Two public endpoints get scanner output into Pental without a browser: a webhook for anything that produces JSON, and the collector protocol for a scanner that Pental cannot reach. Tokens, payloads, field names and every error they return.
There are three ways results reach Pental. Somebody uploads an export in Vulnerability Scans, Pental pulls them from a scanner you have connected, or something you run posts them. This page documents the third: two HTTP endpoints, no browser and no user session, because a CI job has nobody to sign in as.
Both live under one route and are selected by a query parameter. Both authenticate with a token you generate in the portal, and both require your tenant id, which is on the same screen. Neither uses a cookie, and neither will tell an unauthenticated caller whether a tenant exists.
Webhook: posting results you already have
Use this when something in your own pipeline produces findings: Nuclei in CI, a Greenbone export script, a scanner Pental has no adapter for, or your own tooling. The results land as findings on an engagement exactly as a pulled scan would, deduplicated against anything already there.
Create the token in Settings, Vulnerability Scanning, Connected scanners. It is shown once and stored as a SHA-256 hash, so if you lose it you generate another rather than recovering it.
curl -X POST 'https://portal.example.com/api/utils?fn=scan-webhook&client=<client uuid>' \
-H 'x-tenant-id: <your tenant uuid>' \
-H 'x-pental-scan-token: <your ingestion token>' \
-H 'content-type: application/json' \
-d '[
{
"title": "TLS 1.0 supported",
"severity": "high",
"asset": "portal.example.com:443",
"description": "The server negotiates TLS 1.0.",
"remediation": "Disable TLS 1.0 and 1.1 at the load balancer.",
"cvss": 7.4,
"cwe": "CWE-327"
}
]'Say where the results belong with either ?client=<uuid> or ?schedule=<uuid>. A client id opens a new run for that customer; a schedule id attaches to the run that schedule is waiting on. Send neither and the request is refused, because guessing which customer a finding belongs to is not a thing software should do.
The body may be a bare JSON array, or an object with a results, findings or vulnerabilities key, whichever your tool already emits. Nuclei JSONL is accepted as-is: post the file, one JSON object per line, and it is read line by line. One push carries at most 2000 results.
What a result may contain
Only title is required, and even that falls back to "Unnamed finding". Everything else is optional, and each field accepts the names the common scanners already use, so in most cases you post what your tool produced without renaming anything.
| Field, and the names accepted for it | |
|---|---|
| title | title, name, vulnerability, issue, template-id, info.name |
| severity | severity, risk, risk_factor, info.severity. Anything unrecognised is derived from CVSS, and informational when there is none |
| cvss | cvss, cvss_score, cvss3, score, info.classification.cvss-score |
| cve / cwe | cve, cves / cwe, cwes, and the Nuclei classification equivalents |
| asset | asset, host, hostname, target, url, ip, matched-at, affects_url |
| description | description, desc, summary, info.description |
| impact | impact, synopsis, info.impact |
| remediation | remediation, solution, fix, recommendation, info.remediation |
| evidence | evidence, proof, output, plugin_output, extracted-results, response |
| refs | refs, references, see_also, info.reference |
| port, protocol, service | the same names |
| plugin_id | plugin_id, pluginID, qid, template-id, signature, id |
A field this list does not name is discarded before the results are written. If your tool emits something under a name that is not here, map it once on the scanner connection rather than reshaping every payload: the mapping is applied to pushed results and pulled ones alike.
Targets are optional and separate: send a targets array alongside the results and the run records what was in scope, which is what the vulnerability report prints.
What the webhook returns
{
"ok": true,
"run_id": "8f3c…",
"imported": 42,
"counts": {
"created": 12,
"updated": 30,
"closed": 3,
"below_threshold": 0,
"hosts": 4,
"critical": 1, "high": 4, "medium": 9, "low": 12, "info": 16
}
}imported is how many results were accepted. Inside counts, created and updated split those between new findings and ones already on the engagement; closed is how many previously reported issues the scanner no longer sees, which are marked remediated; below_threshold is how many were discarded by the minimum severity set for this customer; hosts is how many distinct assets appeared. The severity keys count the findings on the run as a whole.
| Failures, and what each one means | |
|---|---|
| 400 | The body was not JSON, was not an array or a recognised wrapper, or neither ?client= nor ?schedule= was given |
| 401 | The token is wrong, too short, or the tenant is not one this portal serves. Deliberately the same answer in every case |
| 405 | Anything other than POST |
| 409 | That scanner connection has been switched off in the portal |
| 413 | More than 2000 results in one push |
| 500 | The import failed after the results were accepted |
| 503 | The portal database has not had the update that adds scanning applied yet |
Collector: for a scanner Pental cannot reach
A scanner on a private network, or one your policy will not expose to the internet, is not going to accept a connection from a portal in a data centre. The collector solves it the other way round: a small Node program runs beside the scanner, connects outward, asks whether there is anything to do, and posts the results back. It is the same program a customer runs for scanning inside their own network, in which case the connection is owned by them rather than by you.
Download it from the same settings screen; it comes configured with its own token. It authenticates with x-pental-collector-token, and every action is a POST to one route.
POST /api/utils?fn=scan-agent&action=<action>
x-tenant-id: <your tenant uuid>
x-pental-collector-token: <the collector's own token>| Action, and what it does | |
|---|---|
| poll | Asks for work. Returns a queued run and what to scan, or nothing. Also records the collector as alive, with its host and version |
| results | Posts the findings for a run id it was given |
| failed | Reports that a run could not be completed, with a reason, so the run closes rather than hanging |
| test | Checks the credentials for one scanner connection and reports what the scanner said |
| adapters / catalogue | What this portal can talk to, and what the connected scanners offer |
| download | The collector program itself |
There is one collector protocol and one set of results endpoints. A customer-owned agent for internal scanning is the same program with the connection marked as theirs, so nothing about the integration changes between the two cases.
Where results appear
Whichever route you use, findings are created on an engagement and stay in its vulnerability scanning section rather than joining the report. A scanner gives a title, a severity and an asset; it does not give the impact, evidence and write-up a report finding needs, so scan results are kept apart until somebody promotes one deliberately. The vulnerability report is the document that renders them, and any PDFs you want the customer to have can be attached to the run itself.
A result that matches one already on the engagement updates it rather than creating a second copy, keeps the worse of the two severities, adds the host to the list of affected ones, and reopens the finding if it had been marked remediated. Anything a tester has written by hand is left alone.
Try This on Your Own Database
Pental runs on a Postgres project you own, under your own brand, with the AI on your own key. The trial is the whole platform.
Also Worth Reading
Creating Your Database and Installing the Schema
Four steps: create a Postgres project in your own account, run the setup SQL, register one auth hook, then connect the project to Pental. The hook is the step people miss.
SetupPutting Your Portal on Your Own Domain
One DNS record, then the portal verifies it and issues a certificate. Most failures are the same three causes, and the setup screen tells you which one you have hit.
ReportingMaking Your Word Template the One Pental Fills
Open the Document Builder, upload any document you already send, and let the AI take the last engagement out and place the fields; you check what it did and see the real pages before you save. Nothing asks you to know how a Word file is put together.