OpenAPI 3.0 spec — machine-readable version at /api/openapi.
openapi: 3.0.3
info:
title: ExploitSense API
version: "2.0.0"
description: >
Multi-methodology vulnerability assessment platform. Every scan runs under
an AuthorizationRecord scoped to an Asset and engagement type
(black_box/grey_box/white_box), enforced server-side. Authentication is a
session cookie (exploitsense_session, httpOnly) for user-facing endpoints;
the Collector agent endpoints under /api/collector/* use a separate
Bearer-token trust boundary instead.
servers:
- url: /api
security:
- cookieAuth: []
components:
securitySchemes:
cookieAuth:
type: apiKey
in: cookie
name: exploitsense_session
collectorBearer:
type: http
scheme: bearer
description: Issued once by POST /collectors, never recoverable after creation.
schemas:
Error:
type: object
properties:
error: { type: string }
Scan:
type: object
properties:
id: { type: string }
scanName: { type: string }
targetUrl: { type: string }
targetType: { type: string, enum: [domain, ip] }
scanType: { type: string, enum: [quick, standard, headers_only, deep] }
engagementType: { type: string, enum: [black_box, grey_box, white_box] }
status: { type: string, enum: [pending, running, completed, failed] }
riskScore: { type: integer }
progress: { type: integer }
orgVisible: { type: boolean }
Finding:
type: object
properties:
id: { type: string }
source: { type: string }
title: { type: string }
severity: { type: string, enum: [critical, high, medium, low, info] }
status: { type: string, enum: [open, accepted, fixed, false_positive] }
cveId: { type: string, nullable: true }
cvssScore: { type: number, nullable: true }
epssScore: { type: number, nullable: true }
paths:
/auth/register:
post:
summary: Create an account
security: []
requestBody:
content:
application/json:
schema:
type: object
required: [name, email, password]
properties: { name: { type: string }, email: { type: string }, password: { type: string, minLength: 10 } }
responses:
"201": { description: Created, session cookie set }
"400": { description: Validation error, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
"409": { description: Email already registered }
"429": { description: Rate limited — 5 attempts/hour per email }
/auth/login:
post:
summary: Log in
security: []
requestBody:
content:
application/json:
schema:
type: object
required: [email, password]
properties: { email: { type: string }, password: { type: string } }
responses:
"200": { description: Session cookie set, or { requiresTotp, pendingToken } if 2FA is enabled }
"401": { description: Invalid credentials }
"429": { description: Rate limited — 10 attempts/5min per email }
/auth/login/totp:
post:
summary: Complete login with a TOTP code (second factor)
security: []
requestBody:
content:
application/json:
schema:
type: object
required: [pendingToken, code]
properties: { pendingToken: { type: string }, code: { type: string } }
responses:
"200": { description: Session cookie set }
"401": { description: Invalid code or expired pending token }
/auth/logout-all:
post:
summary: Revoke every session for this account (all devices)
responses:
"200": { description: All sessions invalidated }
/auth/forgot-password:
post:
summary: Request a password reset link (doc 14 §5)
security: []
requestBody:
content:
application/json:
schema: { type: object, required: [email], properties: { email: { type: string } } }
responses:
"200": { description: Always the same generic message, regardless of whether the email exists (anti-enumeration) }
/auth/reset-password:
post:
summary: Complete a password reset — invalidates every existing session
security: []
requestBody:
content:
application/json:
schema:
type: object
required: [token, password]
properties: { token: { type: string }, password: { type: string, minLength: 10 } }
responses:
"200": { description: Password updated, all sessions revoked }
"400": { description: Invalid/expired token, or password breached/validation error }
/auth/sessions:
get:
summary: List active sessions (devices/browsers) for this account
responses: { "200": { description: OK } }
/auth/sessions/{id}:
delete:
summary: Revoke one specific session, leaving all others untouched
parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
responses: { "200": { description: OK } }
/auth/webauthn/register/options:
post:
summary: Start passkey registration (authenticated) — returns WebAuthn creation options
responses: { "200": { description: OK } }
/auth/webauthn/register/verify:
post:
summary: Complete passkey registration — verifies the attestation and stores the credential
responses: { "200": { description: Registered } }
/auth/webauthn/login/options:
post:
summary: Start passkey login (second factor) — returns WebAuthn request options for this user's credentials
security: []
requestBody:
content:
application/json:
schema: { type: object, required: [pendingToken], properties: { pendingToken: { type: string } } }
responses: { "200": { description: OK } }
/auth/webauthn/login/verify:
post:
summary: Complete passkey login — verifies the assertion and sets the session cookie
security: []
responses:
"200": { description: Session cookie set }
"401": { description: Unrecognized passkey or expired pending token }
/auth/webauthn/credentials:
get:
summary: List this account's registered passkeys
responses: { "200": { description: OK } }
/auth/webauthn/credentials/{id}:
delete:
summary: Remove a registered passkey
parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
responses: { "200": { description: OK } }
/approvals:
get:
summary: List pending four-eyes approval requests (admin-only, doc 14 §7)
responses: { "200": { description: OK } }
/approvals/{id}/decide:
post:
summary: Approve or reject a pending request — the approver must differ from the requester
parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
requestBody:
content:
application/json:
schema: { type: object, required: [decision], properties: { decision: { type: string, enum: [approved, rejected] } } }
responses:
"200": { description: OK }
"400": { description: Self-approval rejected, or request not pending/expired }
/scans/{id}/stop:
post:
summary: Kill-switch — stops every running/queued job under this scan's authorization scope (doc 01 §5)
parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
responses: { "200": { description: OK, includes scansStopped count } }
/auth/totp/enroll:
post:
summary: Start 2FA enrollment (returns a QR code + manual-entry secret)
responses: { "200": { description: OK } }
/auth/totp/verify:
post:
summary: Confirm enrollment with a code from the authenticator app — flips totpEnabled on
responses: { "200": { description: Enabled } }
/auth/totp/disable:
post:
summary: Disable 2FA (requires current password)
responses: { "200": { description: Disabled } }
/scans:
get:
summary: List scans owned by the caller, plus org-shared scans if applicable
responses: { "200": { description: OK } }
post:
summary: Create and enqueue a scan
description: >
Requires analyst role or higher. white_box engagement requires admin.
Rate limited 20/5min. Rejects targets resolving to private/internal
IPs (SSRF guard) and rejects engagement types the requested checks
aren't allowlisted for.
requestBody:
content:
application/json:
schema:
oneOf:
- type: object
required: [targetType, scanName, targetUrl, scanType, authorized]
properties: { targetType: { const: domain }, scanName: { type: string }, targetUrl: { type: string }, scanType: { type: string }, engagementType: { type: string }, orgVisible: { type: boolean }, authorized: { const: true } }
- type: object
required: [targetType, scanName, targetIp, scanType, authorized]
properties: { targetType: { const: ip }, scanName: { type: string }, targetIp: { type: string }, scanType: { type: string }, engagementType: { type: string }, orgVisible: { type: boolean }, authorized: { const: true } }
responses:
"201": { description: Created, content: { application/json: { schema: { type: object, properties: { scan: { $ref: "#/components/schemas/Scan" } } } } } }
"400": { description: Validation or SSRF-guard rejection }
"403": { description: Role/engagement not permitted }
"429": { description: Rate limited }
/scans/{id}:
get: { summary: Get a scan (owner or org-shared only), responses: { "200": { description: OK }, "404": { description: Not found or not owned } } }
delete: { summary: Delete a scan (owner only — silent no-op if not owned), responses: { "200": { description: OK } } }
/scans/{id}/findings:
get: { summary: List findings for a scan, responses: { "200": { description: OK } } }
/scans/{id}/report:
get: { summary: Get report metadata, responses: { "200": { description: OK } } }
post: { summary: Generate a PDF report for a completed scan, responses: { "201": { description: Created } } }
/scans/{id}/report/download:
get: { summary: Download the generated PDF (owner only, streamed from private storage — not a public URL), responses: { "200": { description: application/pdf }, "404": { description: Not found } } }
/scans/{id}/screenshot:
get: { summary: Fetch the target screenshot captured at scan time (owner only), responses: { "200": { description: image/png }, "404": { description: Not captured or not found } } }
/findings/{id}:
get: { summary: Get a finding (via owning scan), responses: { "200": { description: OK }, "404": { description: Not found } } }
/findings/{id}/status:
patch:
summary: Update remediation status
requestBody: { content: { application/json: { schema: { type: object, properties: { status: { type: string, enum: [open, accepted, fixed, false_positive] } } } } } }
responses: { "200": { description: OK } }
/networks:
get: { summary: List registered internal networks (analyst+), responses: { "200": { description: OK } } }
post:
summary: Register a network (CIDR) for Collector-based scanning
description: Restricted to RFC1918 private ranges — public CIDRs are rejected (use the black-box IP scan flow instead, which requires ownership verification).
responses: { "201": { description: Created }, "400": { description: Invalid or non-private CIDR } }
/networks/{assetId}/discover:
post: { summary: Queue a discovery job for the network's Collector, responses: { "201": { description: Created }, "400": { description: No Collector registered } } }
/networks/{assetId}/hosts:
get: { summary: List discovered hosts, responses: { "200": { description: OK } } }
/networks/{assetId}/hosts/{hostId}:
patch: { summary: Confirm/unconfirm a host as in-scope, responses: { "200": { description: OK } } }
/networks/{assetId}/hosts/{hostId}/scan:
post: { summary: Dispatch a deep-scan job for a confirmed host, responses: { "201": { description: Created }, "400": { description: Host not confirmed } } }
/collectors:
get: { summary: List registered Collectors (analyst+), responses: { "200": { description: OK } } }
post:
summary: Register a Collector for a network (admin only)
description: Returns a one-time bearer token, never retrievable again. Rate limited 10/hour.
responses: { "201": { description: Created, token returned once }, "403": { description: Requires admin } }
/collectors/{id}:
delete: { summary: Revoke a Collector's token, responses: { "200": { description: OK } } }
/collector/jobs:
get:
summary: Poll for pending jobs (Collector agent only)
security: [{ collectorBearer: [] }]
responses: { "200": { description: OK }, "401": { description: Invalid/revoked/missing token } }
/collector/jobs/{id}/result:
post:
summary: Submit a job result (Collector agent only)
security: [{ collectorBearer: [] }]
description: Payload is schema-validated with size caps (max 500 items). Only accepted for jobs in "dispatched" status — rejects replays.
responses: { "200": { description: OK }, "400": { description: Invalid payload }, "409": { description: Job not awaiting a result } }
/credentials:
get: { summary: List credential metadata for an asset (admin only — never returns secret material), responses: { "200": { description: OK } } }
post: { summary: Create a credential, encrypted at rest with AES-256-GCM (admin only), responses: { "201": { description: Created }, "403": { description: Requires admin } } }
/credentials/{id}:
delete: { summary: Delete a credential, responses: { "200": { description: OK } } }
/asm:
get: { summary: List domain assets and their ASM-monitored status, responses: { "200": { description: OK } } }
/asm/{assetId}:
get: { summary: Get external-asset inventory + change history for a monitored domain, responses: { "200": { description: OK } } }
/asm/{assetId}/scan:
post: { summary: Run ASM discovery now (rate limited 10/hour), responses: { "200": { description: OK } } }
/schedules:
get: { summary: List recurring scan schedules, responses: { "200": { description: OK } } }
post: { summary: Create a recurring schedule (daily/weekly/monthly), responses: { "201": { description: Created } } }
/schedules/{id}:
delete: { summary: Deactivate a schedule, responses: { "200": { description: OK } } }
/notifications:
get: { summary: List notification channels, responses: { "200": { description: OK } } }
post:
summary: Create a notification channel (webhook or email)
description: Webhook targets are validated against SSRF at creation and again — with the resolved IP pinned for the actual connection — at delivery time (DNS-rebinding protection).
responses: { "201": { description: Created }, "400": { description: SSRF-blocked or invalid target } }
/notifications/{id}:
delete: { summary: Delete a notification channel, responses: { "200": { description: OK } } }
/organizations:
get: { summary: Get the caller's organization + member list, responses: { "200": { description: OK } } }
post: { summary: Create an organization (caller becomes the first member), responses: { "201": { description: Created } } }
/organizations/join:
post: { summary: Join an organization by ID, responses: { "200": { description: OK } } }
/organizations/leave:
post: { summary: Leave the current organization, responses: { "200": { description: OK } } }
/audit/verify:
get: { summary: Recompute and verify the audit-log hash chain (admin only), responses: { "200": { description: "{ valid, totalEntries, legacyEntries, brokenAt }" } } }