// Ops.Docs

ExploitSense API Reference

OpenAPI 3.0 spec — machine-readable version at /api/openapi.

Back to app
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 }" } } }