Scanner Job Dispatch
Scanner job dispatch is the pull-based protocol that transfers scan assignments from the dispatcher to each scanner. Scanners poll for pending jobs, claim them, report progress, and submit completion — the ingest nodes coordinate this lifecycle. Understanding the job protocol is essential when debugging stalled scans, investigating scanner connectivity issues, or integrating a custom scanner implementation.
Fields & columns
| Name | Description |
|---|---|
jobId | Unique integer identifying a scan job. Stable across the job lifecycle. |
scanId | The parent scan this job belongs to. Multiple jobs may share the same scanId when a scan spans multiple scanners. |
targets | Array of IP ranges and port combinations assigned to this job. |
maxProbesPerSecond | Effective rate limit for this job, computed from the scanner, subnet, and port list rate limits using slowest-wins logic. |
progressCount | Number of probes (one IP + one port) completed so far across the whole job (sent in progress updates). The server divides it by the job's planned probe count for the percentage shown on the scan. |
resultCount | Number of results recorded so far (sent in progress updates and completion). |
status | Final job outcome: "completed" (scanner finished successfully), "failed" (scanner reported an error) or "cancelled" (the scanner acknowledges an operator cancel; accepted only while the parent scan is cancelling). |
errorMessage | Optional human-readable error description sent with a "failed" completion. |
How to
Understand the job lifecycle
- Dispatcher creates pending jobs in the jobs database for each scanner assigned to a scan.
- Scanner calls GET /scanner/jobs to claim and receive pending assignments. Uses FOR UPDATE SKIP LOCKED for concurrent-safe claiming across multiple ingest nodes.
- Scanner calls POST /scanner/jobs/:jobId/start to mark the job as running.
- Scanner calls POST /scanner/jobs/:jobId/progress periodically with progressCount and resultCount as it scans.
- Scanner calls POST /scanner/jobs/:jobId/complete with status "completed", "failed" or "cancelled" (only while the parent scan is cancelling) and a final resultCount.
- When all jobs for a scan finish, the ingest node marks the parent scan as completed or failed.
Understand the reverse-DNS job lifecycle (resolvers)
- When a scan that requests reverse DNS discovers hosts, the dispatcher bundles their PTR lookups into DNS jobs for the scan's resolver group (up to 64 lookups per job).
- A resolver calls GET /scanner/dns/jobs to claim up to 4 jobs. Claiming IS starting: each job comes back assigned to that resolver with a lease running, and the resolver's heartbeat (activeDnsJobIds) renews the lease.
- As lookups finish, the resolver calls POST /scanner/dns/jobs/:dnsJobId/observations with numbered batches of up to 64 observations. The server files every item as accepted, duplicate, stale, conflicting or rejected; a replayed batch is answered "duplicate" rather than stored twice.
- When the job is done the resolver calls POST /scanner/dns/jobs/:dnsJobId/receipt with a signed receipt. The signature is mandatory: the server recomputes every hash and count before storing it, and the receipt is what the Execution Proof page lists for the job.
- A job whose lease lapses (the resolver stopped heartbeating) is marked expired, and its unfinished lookups return to the queue to be bundled into a new job for any online member of the group; lookups still open when the scan's 15-minute window ends are expired and the scan's reverse DNS closes incomplete.
Debug a stalled scan
- Check the scan status via GET /api/scans/:id — if it shows "running" for an extended period, jobs may be stuck.
- Check scanner heartbeat status in Config > Scanners to confirm the scanner is online.
- Verify the scanner process is running and can reach the ingest endpoint (port 443).
- A scan marked stuck will trigger the scan-stuck alarm after all jobs are finished but the scan remains in "running" state.
Gotchas
- Job ownership is enforced: a scanner can only start, update, or complete jobs assigned to it. Attempting to modify another scanner's job returns 403.
- Scanners authenticate using the X-API-Key header (not session cookies). A missing or invalid key returns 401.
- IP binding is checked on job fetches (GET /scanner/jobs). A scanner connecting from an unexpected IP will receive 403 with code IP_BINDING_VIOLATION.
- The limit query parameter on GET /scanner/jobs defaults to 1 and caps at 10. Scanners should claim one job at a time unless they are designed to parallelize.
- If a scanner disappears mid-job (heartbeat timeout), the job remains in "running" state. The dispatcher may re-assign it after a configurable timeout.
- All job endpoints require a valid API key. Disabled scanners receive 403 on all job operations.
- Job routes are scoped to the identity kind. A resolver identity calling GET /scanner/jobs is refused with 403 WRONG_IDENTITY_KIND before any claim is made, and a port scanner is refused the same way on the /scanner/dns/jobs routes — the two kinds share one identity table and one authentication path, but never each other's work.
API calls (7)
| Method | Path | Description |
|---|---|---|
| GET | /scanner/jobs | Claim and receive pending jobs for this scanner. Uses X-API-Key header. Optional ?limit=1 parameter (max 10). Returns { jobs: [...] }. |
| POST | /scanner/jobs/:jobId/start | Transition a claimed job to running state. Called immediately after the scanner begins work. Returns { ok: true }. |
| POST | /scanner/jobs/:jobId/progress | Stream incremental progress. Body: { progressCount, resultCount }. Call periodically during scanning. Returns { ok: true }. |
| POST | /scanner/jobs/:jobId/complete | Finalize a job. Body: { status: "completed"|"failed"|"cancelled", resultCount?, errorMessage? }. "cancelled" is accepted only while the parent scan is cancelling. Triggers scan completion check when all jobs finish. |
| GET | /scanner/dns/jobs | Resolver identities only: claim reverse-DNS jobs for this resolver. Optional ?limit=1 (max 4). Claiming is starting — each returned job is assigned with a lease the heartbeat renews. Returns { jobs: [...] }. |
| POST | /scanner/dns/jobs/:dnsJobId/observations | Resolver identities only: report lookup results for one claimed job. Body: { batchSeq, observations: [...] } (max 64 per batch). Returns each observation index under accepted, duplicate, stale, conflicting or rejected; a 200 is final even when every item was refused. |
| POST | /scanner/dns/jobs/:dnsJobId/receipt | Resolver identities only: submit the signed execution receipt that closes the job. Body: { scanId, dnsJobId, taskSpecHash, receipt, signature } — the signature is mandatory and the server recomputes every hash and count before storing. Returns { ok, outcome, receiptId, jobStatus }. |
Related
- Scanner Heartbeat — Heartbeat and job protocol are the two core scanner-to-ingest communication channels
- Scanners — Scanner provisioning and configuration governs which jobs each scanner receives
- Recent Scans — Scan status reflects the aggregate outcome of all its component jobs
- Resolvers — The identity kind that claims the reverse-DNS job routes described here