openapi: 3.0.3
info:
  title: ShadowMap Customer API
  version: 1.0.0
  description: |
    Read-only access to your ShadowMap data for SIEM, SOAR, BI and reporting tools.

    **Authentication:** a bearer token (`Authorization: Bearer <token>`) over HTTPS, issued per
    integration and valid for at most 365 days.
    - A client certificate (mutual TLS) is required only if your token was issued with one. Such a
      token must present that certificate in the TLS handshake, or it is refused with
      `certificate_mismatch`.
    - IP allowlisting is optional, set by ShadowMap on request. A token with an allowlist is accepted
      only from the networks on it (`ip_not_allowed` otherwise); IPv4 and IPv6 are separate entries,
      so if your network can reach us over IPv6, list your IPv6 range too. A token without one is
      accepted from any address.

    **Limits per token:** 60 requests per minute and 2 requests in flight. Successful responses and
    a `rate_limited` 429 carry `X-RateLimit-Limit` and `X-RateLimit-Remaining`. Every 429 carries
    `Retry-After` (seconds).

    **Time limit:** a request that would start a database read more than 25 seconds after it reached
    the API is stopped and answered `503 api_unavailable`; a read already under way can run for up to
    30 seconds. Retry it with a backoff; if it keeps failing, narrow it (for example fewer `status`
    values).

    **Errors from the API** always use `{"error": {"code", "message", "request_id"}}`. Branch on
    `code`; quote `request_id` (also in the `X-Request-Id` header) when you contact support. A request
    refused at ShadowMap's network edge (a revoked client certificate, a path outside `/api/v1/`, or a
    method other than GET/HEAD) never reaches the API: it gets an HTML 403 instead.
servers:
  - url: https://{host}
    description: Your zone's API hostname, given to you at onboarding.
    variables:
      host:
        default: api.shadowmap.com
security:
  - bearerToken: []
tags:
  - name: Platform
  - name: Vendor Risk Management
  - name: Data Leaks
  - name: Inventory
  - name: Scans
  - name: Attack Surface
  - name: Threats
  - name: Dark Web
  - name: SLA
paths:
  /api/v1/health:
    get:
      operationId: getHealth
      tags: [Platform]
      summary: Check your credentials and network path
      description: Succeeds only when the token is accepted, together with its client certificate and source IP when the token was issued with a certificate or an allowlist. Requires no scope.
      responses:
        "200":
          description: Credentials accepted.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/HealthResponse" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/capabilities:
    get:
      operationId: getCapabilities
      tags: [Platform]
      summary: What each module supports, and whether your token holds its scope
      responses:
        "200":
          description: The module list.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CapabilitiesResponse" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/vendor-risk-management/vendors:
    get:
      operationId: exportVendors
      tags: [Vendor Risk Management]
      summary: Export every vendor with its ratings, scores and finding counts
      description: |
        Requires scope `vrm:read`. Returns the whole export as one array (not paged). Rows are
        returned in no guaranteed order: use `type` (current or historical) and `scan_date` to order
        them. Served from a snapshot refreshed hourly. Filter client-side.
      parameters:
        - name: format
          in: query
          required: false
          schema: { type: string, enum: [json, xml], default: json }
      responses:
        "200":
          description: The export.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/VendorRow" }
            application/xml:
              schema: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/data-leaks/s3-buckets:
    get:
      operationId: listS3Buckets
      tags: [Data Leaks]
      summary: List your exposed storage buckets, one page at a time
      description: |
        Requires scope `data-leaks:read`. Exactly the buckets the summary's `all` counts (buckets with a zero
        confidence score and buckets removed from your scope are not listed), in ascending `id` order - walking
        every page returns each of them once - at most 500 per page. Records are typed JSON: integers, booleans
        and arrays as such, null when a value is unknown, and timestamps in ISO-8601 with their offset (+05:30).

        `status` is the dashboard tab the bucket is under, one of the summary's status labels; it is null for a
        bucket in no tab (one still carrying a retired workflow state). The tab depends on the workflow state and,
        for Needs Review, Investigating, To Be Closed and Offline, on whether the bucket is still online. `online`
        is that liveness itself: true or false, and null when it is unknown. `severity` is the summary's severity
        label for the stored `risk`: 5 Critical, 4 High, 3 Medium, 2 Low, 1 Info, and N/A for any other value.
        `provider` is the storage provider's code, passed through exactly as ShadowMap stores it: `aws` (Amazon S3),
        `gcp` (Google Cloud Storage), `azure` (Azure Blob Storage), `dos` (DigitalOcean Spaces) or `ali` (Alibaba
        Cloud Object Storage); a code added later is passed through the same way.
        `classification_tags` and `compliance_frameworks` list the bucket's tags in the order they were written.
        `created_at` is when ShadowMap first found the bucket - the module's discovery column. `updated_at` is the
        record's last-updated time as ShadowMap stores it (the export's Last seen column); not every change to a
        bucket moves it, so do not use it to detect changes. The number of days a bucket has been open is not a
        field: compute it from `created_at`. `web_url` opens the bucket on your dashboard, and
        `/api/v1/data-leaks/s3-buckets/{id}` returns the same record on its own.

        Filters combine: a bucket is listed only when it matches every filter given. The `severity` filter takes the
        labels the summary groups by, so the buckets listed under a label are exactly the ones the summary counts under
        it. The summary (`/api/v1/data-leaks/s3-buckets/summary`) takes the same filters, with the same labels and the
        same 422s, and counts exactly what this list returns under them.
      parameters:
        - $ref: "#/components/parameters/PageSize"
        - $ref: "#/components/parameters/Cursor"
        - name: sort
          in: query
          required: false
          description: |
            One key to order the records by, ascending; prefix it with `-` for descending: `severity` (the record's `severity`, by the risk behind it: `-severity` puts the most severe first), `first_seen` (`created_at`, the discovery column `discovered_from` and `discovered_to` filter) or `id`.
            The record id breaks ties, in the same direction. Sorting is for ranking and top-N reads: a record whose
            `severity` or `last_seen` changes during a walk can be skipped, or listed twice. For a complete pull use no
            `sort`, `sort=id` or `sort=first_seen`, and de-duplicate by `id`. One key only: a second (`sort=a,b`)
            or an unknown key is refused with 422 naming the keys this list takes - the `sort_keys` of /api/v1/capabilities.
            Without `sort` the order above, unchanged.
            A record with no value for `first_seen` sorts before every value ascending and after every value descending.
            A `next_cursor` continues only the sort it was returned under: send it back with the same `sort` (or none, for
            an unsorted list); with another key, the other direction or no sort it is refused with 422.
          schema:
            type: string
            enum: [severity, "-severity", first_seen, "-first_seen", id, "-id"]
        - name: status
          in: query
          required: false
          description: |
            Only buckets under these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Offline`. The labels are the summary's status labels. Several labels return the union of
            their tabs, each bucket once; a label given twice counts once. An unknown label or an empty entry is
            refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "Offline", "To Be Closed", "Closed", "Filtered by AI"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - name: severity
          in: query
          required: false
          description: |
            Only buckets of these severities: one label, or several separated by commas, for example `Critical,High`.
            The labels are the summary's `severity` groups: Critical, High, Medium, Low, Info and N/A. Several labels
            return the buckets of any of them, each once; a label given twice counts once. An unknown label or an
            empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items: { type: string, enum: [Critical, High, Medium, Low, Info, "N/A"] }
      responses:
        "200":
          description: One page of S3 buckets.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/S3BucketList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/data-leaks/s3-buckets/{id}:
    get:
      operationId: getS3Bucket
      tags: [Data Leaks]
      summary: Get one exposed storage bucket
      description: |
        Requires scope `data-leaks:read`. The record the list returns for this `id`, with every field. An id that is
        not one of your listed buckets - another company's, one that does not exist, or one the list leaves out (a
        zero confidence score, or removed from your scope) - answers 404 `not_found`, the same response whichever it
        is. The endpoint takes no query parameter.
      parameters:
        - $ref: "#/components/parameters/RecordId"
      responses:
        "200":
          description: The bucket.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/S3BucketDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/data-leaks/s3-buckets/summary:
    get:
      operationId: getS3BucketsSummary
      tags: [Data Leaks]
      summary: Count S3 bucket findings by status tab, optionally by severity
      description: |
        Requires scope `data-leaks:read`. Status labels: Needs Review, Investigating, Accepted Risk,
        Dismissed, Offline, To Be Closed, Closed, Filtered by AI. Severity labels: Critical, High, Medium,
        Low, Info, N/A. `all` counts every bucket; a bucket with an unrecognised legacy state is in
        `all` but in no tab.

        Filters: the list's filters (`/api/v1/data-leaks/s3-buckets`) - `status`, `discovered_from`, `discovered_to` and
        `severity` - apply here too, with the same labels and the same 422s, so the summary counts exactly the records
        the list returns under them. The discovery window and the `severity` filter narrow every count: each tab, `all`
        and `removed_from_scope`. A `status` filter limits the status groups to the tabs it names (in tab order, zeros
        included) and `all` to the findings in any of them, and `removed_from_scope` is then null: a descoped finding is
        in no tab. `group_by` groups the findings the filters leave.
      parameters:
        - $ref: "#/components/parameters/GroupBy"
        - name: status
          in: query
          required: false
          description: |
            Count only these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Investigating`. The status groups are then these tabs alone, in tab order, zeros included; `all`
            counts the findings in any of them, and `removed_from_scope` is null. The labels are the list's; an unknown
            label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "Offline", "To Be Closed", "Closed", "Filtered by AI"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - name: severity
          in: query
          required: false
          description: |
            Count only findings under these `severity` labels: one label, or several separated by commas, for example
            `Critical,High`. They are the labels `group_by=severity` reports - the list's `severity` filter; an unknown
            label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Critical", "High", "Medium", "Low", "Info", "N/A"]
      responses:
        "200": { $ref: "#/components/responses/Summary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/data-leaks/code-repositories:
    get:
      operationId: listCodeRepositories
      tags: [Data Leaks]
      summary: List your exposed code repositories, one page at a time
      description: |
        Requires scope `data-leaks:read`. Every repository the summary's `all` counts, each ONCE, in ascending `id` order -
        walking every page returns each of them once - at most 200 per page (the records are heavy). As on the dashboard,
        a repository removed from your scope, and one whose details or keyword index ShadowMap has not recorded yet, are
        not listed. Records are typed JSON: integers and booleans as such, null when a value is unknown or empty, and
        timestamps in ISO-8601 with their offset (+05:30).

        `full_name` (owner/name) and `html_url` identify the exposed repository; `owner_name` is its owner. `files` lists
        the files ShadowMap found in it - at most 50, the first 50 by `id` (the dashboard also shows 50 at a time) - and
        `files_total` how many there are; the rest are not in the record. `keywords` is the text ShadowMap matched the
        repository on, returned as stored. `custom_tags` holds your own tags, under the keys you defined for Code
        Repositories.

        `status` is the dashboard tab the repository is under, one of the summary's status labels: Dismissed for a
        repository marked a false positive (`is_bad_repository`), Filtered by AI, Takedown In Progress (we filed a takedown
        that has not succeeded yet), Taken Down (we filed one and the repository is gone), Offline (it is gone and we filed
        no takedown), and otherwise Needs Review, Investigating or Accepted Risk by its review code. `status` is null for a
        repository in no tab: a pinned one (`is_pinned`, the Pin Repository action) outside the takedown flow, or one with
        a review code no tab shows. The raw fields the tab is computed from are returned beside it, for transparency:
        `is_bad_repository`, `is_pinned`, `response_status` (0 needs review, 1 investigating, 3 accepted risk, 6 filtered by
        AI), `takedown_requested_on`, and `scanner_status` - the scanner's own view, `online` or `takendown`, where
        `takendown` means the scanner no longer finds the repository, NOT that ShadowMap took it down.
        `severity` is the summary's band of the risk score `risk`: High (500 and above), Medium (100 to 499), Low (1 to
        99), Informational (0 or below). `source` is the platform, the summary's source label: GitHub, GitHub Gist,
        GitLab or GitLab Snippet, and Unknown for a source ShadowMap has no name for - the label `group_by=source` counts
        the repository under.
        `first_seen_on` is when ShadowMap first found the repository - the module's discovery column. `updated_at` is the
        record's last-updated time as stored; not every change moves it, so do not use it to detect changes. `web_url`
        opens the repository on your dashboard, and `/api/v1/data-leaks/code-repositories/{id}` returns the same record on
        its own. The credentials found in repositories are listed by Leaked Credentials, which counts credentials, not
        repositories: the two cannot be added up.

        Filters combine: a repository is listed only when it matches every filter given. The `severity` filter takes the
        labels the summary groups by, so the repositories listed under a label are exactly the ones the summary counts
        under it. The summary (`/api/v1/data-leaks/code-repositories/summary`) takes the same filters, with the same
        labels and the same 422s, and counts the repositories this list returns under them: a repository with more than
        one details, index or extensions record counts once per record in every status tab but Takedown In Progress, as
        the dashboard's badges count it, while `all`, Takedown In Progress and `removed_from_scope` count it once and this
        list returns it once.
      parameters:
        - name: page_size
          in: query
          required: false
          description: Rows per page, 1 to 200 (this module's maximum, as in `/api/v1/capabilities`). Default 100.
          schema: { type: integer, minimum: 1, maximum: 200 }
        - $ref: "#/components/parameters/Cursor"
        - name: sort
          in: query
          required: false
          description: |
            One key to order the records by, ascending; prefix it with `-` for descending: `severity` (the record's `severity`, by the risk behind it: `-severity` puts the most severe first), `first_seen` (`first_seen_on`, the discovery column `discovered_from` and `discovered_to` filter), `last_seen` (`last_seen_on`) or `id`.
            The record id breaks ties, in the same direction. Sorting is for ranking and top-N reads: a record whose
            `severity` or `last_seen` changes during a walk can be skipped, or listed twice. For a complete pull use no
            `sort`, `sort=id` or `sort=first_seen`, and de-duplicate by `id`. One key only: a second (`sort=a,b`)
            or an unknown key is refused with 422 naming the keys this list takes - the `sort_keys` of /api/v1/capabilities.
            Without `sort` the order above, unchanged.
            A record with no value for `first_seen` or `last_seen` sorts before every value ascending and after every value descending.
            A `next_cursor` continues only the sort it was returned under: send it back with the same `sort` (or none, for
            an unsorted list); with another key, the other direction or no sort it is refused with 422.
          schema:
            type: string
            enum: [severity, "-severity", first_seen, "-first_seen", last_seen, "-last_seen", id, "-id"]
        - name: status
          in: query
          required: false
          description: |
            Only repositories under these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Takedown In Progress`. The labels are the summary's status labels. Several labels return the
            union of their tabs, each repository once; a label given twice counts once. An unknown label or an empty
            entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "Takedown In Progress", "Taken Down", "Offline", "Filtered by AI"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - name: severity
          in: query
          required: false
          description: |
            Only repositories of these severities: one label, or several separated by commas, for example `High,Medium`.
            The labels are the summary's `severity` labels, the bands of the risk score. Several labels return the
            repositories of any of them, each once; a label given twice counts once. An unknown label or an empty entry is
            refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items: { type: string, enum: [High, Medium, Low, Informational] }
      responses:
        "200":
          description: One page of code repositories.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CodeRepositoryList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/data-leaks/code-repositories/{id}:
    get:
      operationId: getCodeRepository
      tags: [Data Leaks]
      summary: Get one exposed code repository
      description: |
        Requires scope `data-leaks:read`. The record the list returns for this `id`, with every field. An id that is not
        one of your listed repositories - another company's, one that does not exist, or one the list leaves out (removed
        from your scope, or without recorded details or keyword index) - answers 404 `not_found`, the same response
        whichever it is; the dashboard still opens a repository removed from your scope, the API does not. The endpoint
        takes no query parameter.
      parameters:
        - $ref: "#/components/parameters/RecordId"
      responses:
        "200":
          description: The code repository.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CodeRepositoryDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/data-leaks/code-repositories/summary:
    get:
      operationId: getCodeRepositoriesSummary
      tags: [Data Leaks]
      summary: Count code repository findings by status tab, optionally by severity or source
      description: |
        Requires scope `data-leaks:read`. Status labels: Needs Review, Investigating, Accepted Risk,
        Dismissed, Takedown In Progress, Taken Down, Offline, Filtered by AI. Severity labels (risk
        score bands): High (500 and above), Medium (100 to 499), Low (1 to 99), Informational (0 or below).
        Source labels: GitHub, GitHub Gist, GitLab, GitLab Snippet. Any other source is labelled
        Unknown. `all` counts every repository. A repository that has been pinned
        (the Pin Repository action on its detail page) can be in `all` but in no status tab.

        Filters: the list's filters (`/api/v1/data-leaks/code-repositories`) - `status`, `discovered_from`,
        `discovered_to` and `severity` - apply here too, with the same labels and the same 422s, so the summary counts
        the records the list returns under them. A repository with more than one details, index or extensions record
        counts once per record in every status tab but Takedown In Progress, as the dashboard's badges count it, while
        `all`, Takedown In Progress and `removed_from_scope` count it once (the list returns it once), so a status
        count can exceed the repositories the list returns under it. The discovery window and the `severity` filter
        narrow every count: each tab, `all` and `removed_from_scope`. A `status` filter limits the status groups to the
        tabs it names (in tab order, zeros included) and `all` to the findings in any of them, and `removed_from_scope`
        is then null: a descoped finding is in no tab. `group_by` groups the findings the filters leave.
      parameters:
        - $ref: "#/components/parameters/GroupBy"
        - name: status
          in: query
          required: false
          description: |
            Count only these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Investigating`. The status groups are then these tabs alone, in tab order, zeros included; `all`
            counts the findings in any of them, and `removed_from_scope` is null. The labels are the list's; an unknown
            label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "Takedown In Progress", "Taken Down", "Offline", "Filtered by AI"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - name: severity
          in: query
          required: false
          description: |
            Count only findings under these `severity` labels: one label, or several separated by commas, for example
            `High,Medium`. They are the labels `group_by=severity` reports - the list's `severity` filter; an unknown label
            or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["High", "Medium", "Low", "Informational"]
      responses:
        "200": { $ref: "#/components/responses/Summary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/data-leaks/leaked-credentials:
    get:
      operationId: listLeakedCredentials
      tags: [Data Leaks]
      summary: List your leaked credentials, one page at a time, with their leaked values in full
      description: |
        Requires scope `data-leaks:read`. Every credential the summary's `all` counts - hidden (set-aside) credentials
        included - in ascending `id` order: walking every page returns each of them once, at most 500 per page. Records
        are typed JSON: integers as such, null when a value is unknown or empty, and timestamps in ISO-8601 with their
        offset (+05:30).

        `matching` is the leaked value, and `before` and `after` the text stored around it where it was found. They and
        `probe_metadata` (the evidence ShadowMap's probe recorded) are returned IN FULL - not truncated or masked; they
        hold live credentials, so store and log them as secrets. The API returns values the dashboard does not show: the
        dashboard never shows `before` or `after`, and shows `probe_metadata` only in a credential's probe detail.

        `status` is the tab the credential is under, one of the summary's status labels. A visible credential is under
        Needs Review, Investigating, Accepted Risk, Closed or Filtered by AI by its `response_status` (0, 1, 2, 3, 6);
        one with any other code has `status` null. A hidden credential (`hidden` is not null) is under Dismissed (a
        person dismissed it, or the repository or app it was found in), Third-party (nothing links its repository to
        you), Resolved (its source is gone or taken down) or Filtered by AI (a machine filter), by `hidden.reason`;
        `hidden.explanation` is the dashboard's sentence for it. `source` is the summary's source label (the module that
        found the credential) and `source_module_id` that module's stored id. `source_ref` names where the credential
        was found - a code repository (its `id` is the Code Repositories record id) and file, a mobile application and
        file, or the URL of a web page or an exposed database - and is null for every other module, and when the
        source is not one of your own records. `probe_status` is the probe's verdict (for example confirmed_live),
        not_applicable when the credential cannot be validated, or not_probed; `working_status` groups it as the
        dashboard does. `impact_level` is the summary's impact label - Unmeasured when no impact was measured - and any
        other stored level is returned as stored. `created_at` is when ShadowMap first stored the credential - the
        module's discovery column. `first_seen_at` and `last_seen_at` are when it was first and last seen where it
        leaked, null on credentials stored before sightings were recorded (the dashboard shows `created_at` and
        `updated_at` there instead). `updated_at` moves on any change to the row, so do not use it to detect changes.
        `web_url` opens the credential on your dashboard, and `/api/v1/data-leaks/leaked-credentials/{id}` returns the
        same record on its own. Credentials found in code repositories and leaked APIs also appear in those modules,
        which count repositories and APIs, not credentials: the counts cannot be added up.

        Filters combine: a credential is listed only when it matches every filter given. The `impact_level` filter takes
        the labels the summary groups by, so the credentials listed under a label are exactly the ones the summary
        counts under it. The summary (`/api/v1/data-leaks/leaked-credentials/summary`) takes the same filters, with the
        same labels and the same 422s, and counts exactly what this list returns under them.
      parameters:
        - $ref: "#/components/parameters/PageSize"
        - $ref: "#/components/parameters/Cursor"
        - name: sort
          in: query
          required: false
          description: |
            One key to order the records by, ascending; prefix it with `-` for descending: `first_seen` (`created_at`, the discovery column `discovered_from` and `discovered_to` filter), `last_seen` (`last_seen_at`) or `id`.
            The record id breaks ties, in the same direction. Sorting is for ranking and top-N reads: a record whose
            `severity` or `last_seen` changes during a walk can be skipped, or listed twice. For a complete pull use no
            `sort`, `sort=id` or `sort=first_seen`, and de-duplicate by `id`. One key only: a second (`sort=a,b`)
            or an unknown key is refused with 422 naming the keys this list takes - the `sort_keys` of /api/v1/capabilities.
            Without `sort` the order above, unchanged.
            A record with no value for `first_seen` or `last_seen` sorts before every value ascending and after every value descending.
            A `next_cursor` continues only the sort it was returned under: send it back with the same `sort` (or none, for
            an unsorted list); with another key, the other direction or no sort it is refused with 422.
          schema:
            type: string
            enum: [first_seen, "-first_seen", last_seen, "-last_seen", id, "-id"]
        - name: status
          in: query
          required: false
          description: |
            Only credentials under these tabs: one label, or several separated by commas, for example
            `Needs Review,Dismissed`. The labels are the summary's status labels. Several labels return the union of
            their tabs, each credential once; a label given twice counts once. An unknown label or an empty entry is
            refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Accepted Risk", "Closed", "Filtered by AI", "Dismissed", "Third-party", "Resolved"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - name: impact_level
          in: query
          required: false
          description: |
            Only credentials of these impact levels: one label, or several separated by commas, for example
            `Critical,High`. The labels are the summary's `impact_level` labels; Unmeasured holds the credentials with
            no impact measured. A stored level no label names can be read on the record but not filtered on. Several
            labels return the credentials of any of them, each once; a label given twice counts once. An unknown label
            or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items: { type: string, enum: [Critical, High, Medium, Low, None, Unmeasured] }
      responses:
        "200":
          description: One page of leaked credentials.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LeakedCredentialList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/data-leaks/leaked-credentials/{id}:
    get:
      operationId: getLeakedCredential
      tags: [Data Leaks]
      summary: Get one leaked credential, with its leaked values in full
      description: |
        Requires scope `data-leaks:read`. The record the list returns for this `id`, with every field - `matching`,
        `before`, `after` and `probe_metadata` in full. A hidden credential is served like any other. An id that is not
        one of your credentials - another company's, or one that does not exist - answers 404 `not_found`, the same
        response whichever it is. The endpoint takes no query parameter.
      parameters:
        - $ref: "#/components/parameters/RecordId"
      responses:
        "200":
          description: The leaked credential.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LeakedCredentialDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/data-leaks/leaked-credentials/summary:
    get:
      operationId: getLeakedCredentialsSummary
      tags: [Data Leaks]
      summary: Count leaked credentials by status tab, optionally by impact level or source module
      description: |
        Requires scope `data-leaks:read`. Status labels: Needs Review, Investigating, Accepted Risk,
        Closed, Filtered by AI, Dismissed, Third-party, Resolved. Dismissed, Third-party and Resolved
        list credentials set aside from the working queues: a person dismissed them, nothing links
        their repository to you, or their source is gone. Filtered by AI also lists the credentials
        a machine filter set aside. `all` counts every credential, set-aside ones included. No
        credential is in two tabs, and a visible credential with an unrecognised legacy status is in
        `all` but in no tab. Impact levels: Critical, High, Medium, Low, None, Unmeasured (impact never
        demonstrated); any other stored level is returned as stored. Source labels name the module that
        found the credential, for example Code
        Repositories, Mobile Application or Leaked APIs; a source the dashboard has no name for is
        labelled Unknown. Credentials found in code repositories or leaked APIs also appear in those
        modules, which count repositories and APIs, not credentials, so the counts cannot be added
        or compared. This module has no Removed From Scope view, so `removed_from_scope` is null.

        Filters: the list's filters (`/api/v1/data-leaks/leaked-credentials`) - `status`, `discovered_from`,
        `discovered_to` and `impact_level` - apply here too, with the same labels and the same 422s, so the summary
        counts exactly the records the list returns under them. The discovery window and the `impact_level` filter
        narrow every count: each tab and `all`. A `status` filter limits the status groups to the tabs it names (in tab
        order, zeros included) and `all` to the findings in any of them. `group_by` groups the findings the filters
        leave.
      parameters:
        - $ref: "#/components/parameters/GroupBy"
        - name: status
          in: query
          required: false
          description: |
            Count only these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Investigating`. The status groups are then these tabs alone, in tab order, zeros included; `all`
            counts the findings in any of them. The labels are the list's; an unknown label or an empty entry is refused
            with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Accepted Risk", "Closed", "Filtered by AI", "Dismissed", "Third-party", "Resolved"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - name: impact_level
          in: query
          required: false
          description: |
            Count only findings under these `impact_level` labels: one label, or several separated by commas, for example
            `Critical,High`. They are the labels `group_by=impact_level` reports - the list's `impact_level` filter; an
            unknown label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Critical", "High", "Medium", "Low", "None", "Unmeasured"]
      responses:
        "200": { $ref: "#/components/responses/Summary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/data-leaks/docker-containers:
    get:
      operationId: listDockerContainers
      tags: [Data Leaks]
      summary: List your exposed container images, one page at a time
      description: |
        Requires scope `data-leaks:read`. Every image the summary's `all` counts, each ONCE, in ascending `id` order -
        walking every page returns each of them once - at most 500 per page. As on the dashboard, an image with a zero
        confidence score, one removed from your scope, and one whose details ShadowMap has not recorded yet are not
        listed. Nor is an image that is not Dismissed and whose scanner status is anything but `online` or `takendown`:
        `all` does not count it either, though the summary's Requested Takedown, Taken Down and Filtered by AI tabs can
        (see the summary). An image can have more than one details record; the dashboard's badges, and so the summary,
        count such an image once per details record, while this list returns it once - so the list can hold fewer
        images than the summary's `all` (and a tab's count) reports. Its record shows its first details record. Records
        are typed JSON: integers and booleans as such, null when a value is unknown or empty, and timestamps in ISO-8601
        with their offset (+05:30).

        `status` is the dashboard tab the image is under, one of the summary's status labels: Dismissed for an image
        marked a false positive (`is_bad_image`), Filtered by AI, Requested Takedown (we filed a takedown that has not
        succeeded yet), Taken Down (we filed one and the image is gone), Offline (the image is gone and we filed no
        takedown), and otherwise Needs Review, Investigating or Accepted Risk by its review code. `status` is null for an
        image in no tab (a review code no tab shows). The raw fields the tab is computed from are returned beside it, for
        transparency: `is_bad_image`, `response_status` (0 needs review, 1 investigating, 3 accepted risk, 6 filtered by
        AI), `takedown_requested_on`, and `scanner_status` - the scanner's own view, `online` or `takendown`, where
        `takendown` means the scanner no longer finds the image in the registry, NOT that ShadowMap took it down.
        `severity` is the summary's severity label for the stored risk: High, Medium or Low in any letter case; any other
        stored value is returned as stored. `confidence` is ShadowMap's confidence label as stored (high, moderate or
        low). `image_link` is the image's Docker Hub page, as the dashboard links it; null when the image has no
        publisher or its source has no link. `pull_count` is returned as stored, as text. `first_seen_on` is when
        ShadowMap first found the image - the module's discovery column. `updated_at` is the record's last-updated time
        as stored; not every change moves it, so do not use it to detect changes. `web_url` opens the image on your
        dashboard, and `/api/v1/data-leaks/docker-containers/{id}` returns the same record on its own.

        Filters combine: an image is listed only when it matches every filter given. The `severity` filter takes the
        labels the summary groups by, so the images listed under a label are the ones the summary counts under it. The
        summary (`/api/v1/data-leaks/docker-containers/summary`) takes the same filters, with the same labels and the
        same 422s, and counts the images this list returns under them, with two differences: an image with more than one
        details record counts once per record, as above; and without a `status` filter its Requested Takedown, Taken Down
        and Filtered by AI groups also count, as the dashboard's badges do, the images this list leaves out because their
        scanner status is neither `online` nor `takendown`. Under a `status` filter every tab counts only images this
        list returns.
      parameters:
        - $ref: "#/components/parameters/PageSize"
        - $ref: "#/components/parameters/Cursor"
        - name: sort
          in: query
          required: false
          description: |
            One key to order the records by, ascending; prefix it with `-` for descending: `severity` (the record's `severity`, by the risk behind it: `-severity` puts the most severe first), `first_seen` (`first_seen_on`, the discovery column `discovered_from` and `discovered_to` filter), `last_seen` (`last_seen_on`) or `id`.
            The record id breaks ties, in the same direction. Sorting is for ranking and top-N reads: a record whose
            `severity` or `last_seen` changes during a walk can be skipped, or listed twice. For a complete pull use no
            `sort`, `sort=id` or `sort=first_seen`, and de-duplicate by `id`. One key only: a second (`sort=a,b`)
            or an unknown key is refused with 422 naming the keys this list takes - the `sort_keys` of /api/v1/capabilities.
            Without `sort` the order above, unchanged.
            A record with no value for `first_seen` or `last_seen` sorts before every value ascending and after every value descending.
            A `next_cursor` continues only the sort it was returned under: send it back with the same `sort` (or none, for
            an unsorted list); with another key, the other direction or no sort it is refused with 422.
          schema:
            type: string
            enum: [severity, "-severity", first_seen, "-first_seen", last_seen, "-last_seen", id, "-id"]
        - name: status
          in: query
          required: false
          description: |
            Only images under these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Offline`. The labels are the summary's status labels. Several labels return the union of their
            tabs, each image once; a label given twice counts once. An unknown label or an empty entry is refused with
            422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "Requested Takedown", "Taken Down", "Offline", "Filtered by AI"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - name: severity
          in: query
          required: false
          description: |
            Only images of these severities: one label, or several separated by commas, for example `High,Medium`. The
            labels are the summary's `severity` labels: High, Medium and Low. A stored risk no label names can be read
            on the record but not filtered on. Several labels return the images of any of them, each once; a label given
            twice counts once. An unknown label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items: { type: string, enum: [High, Medium, Low] }
      responses:
        "200":
          description: One page of container images.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DockerContainerList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/data-leaks/docker-containers/{id}:
    get:
      operationId: getDockerContainer
      tags: [Data Leaks]
      summary: Get one exposed container image
      description: |
        Requires scope `data-leaks:read`. The record the list returns for this `id`, with every field. An id that is not
        one of your listed images - another company's, one that does not exist, or one the list leaves out (a zero
        confidence score, removed from your scope, or without recorded details) - answers 404 `not_found`, the same
        response whichever it is. The endpoint takes no query parameter.
      parameters:
        - $ref: "#/components/parameters/RecordId"
      responses:
        "200":
          description: The container image.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DockerContainerDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/data-leaks/docker-containers/summary:
    get:
      operationId: getDockerContainersSummary
      tags: [Data Leaks]
      summary: Count container image findings by status tab, optionally by severity
      description: |
        Requires scope `data-leaks:read`. Status labels: Needs Review, Investigating, Accepted Risk,
        Dismissed, Requested Takedown (we asked the registry to remove the image), Taken Down
        (removed after our request), Offline (gone without our request), Filtered by AI. The
        dashboard shows Filtered by AI only where AI review is enabled; the API always returns it.
        Severity labels: High, Medium, Low; any other stored risk value is returned as stored.
        Images with a zero confidence score are not counted, as on the dashboard. `all` counts every
        image; an image in an unrecognised review state can be in `all` but in no tab. The other way
        round, the Requested Takedown, Taken Down and Filtered by AI tabs count, as their dashboard badges
        do, an image that `all` leaves out and the list never returns: one that is not Dismissed and whose
        scanner status is neither `online` nor `takendown`.

        Filters: the list's filters (`/api/v1/data-leaks/docker-containers`) - `status`, `discovered_from`,
        `discovered_to` and `severity` - apply here too, with the same labels and the same 422s, and the summary counts
        the images the list returns under them, with two differences. An image with more than one details record counts
        once per record, as on the dashboard's badges (the list returns it once). And without a `status` filter - no
        filter at all, or only the discovery window and `severity` - the Requested Takedown, Taken Down and Filtered by
        AI status groups also count the images `all` leaves out (above), as the badges do; under a `status` filter each
        tab counts only the images `all` holds, the ones the list returns. The discovery window and the `severity` filter
        narrow every count: each tab, `all` and `removed_from_scope`. A `status` filter limits the status groups to the
        tabs it names (in tab order, zeros included) and `all` to the findings in any of them, and `removed_from_scope`
        is then null: a descoped finding is in no tab. `group_by` groups the findings the filters leave.
      parameters:
        - $ref: "#/components/parameters/GroupBy"
        - name: status
          in: query
          required: false
          description: |
            Count only these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Investigating`. The status groups are then these tabs alone, in tab order, zeros included; `all`
            counts the findings in any of them, and `removed_from_scope` is null. The labels are the list's; an unknown
            label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "Requested Takedown", "Taken Down", "Offline", "Filtered by AI"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - name: severity
          in: query
          required: false
          description: |
            Count only findings under these `severity` labels: one label, or several separated by commas, for example
            `High,Medium`. They are the labels `group_by=severity` reports - the list's `severity` filter; an unknown label
            or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["High", "Medium", "Low"]
      responses:
        "200": { $ref: "#/components/responses/Summary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/data-leaks/leaked-files:
    get:
      operationId: listLeakedFiles
      tags: [Data Leaks]
      summary: List your leaked files, one page at a time
      description: |
        Requires scope `data-leaks:read`. Exactly the files the summary's `all` counts (files with a zero confidence
        score and files removed from your scope are not listed), in ascending `id` order - walking every page returns
        each of them once - at most 500 per page. Records are typed JSON: integers as such, null when a value is
        unknown or empty, and timestamps in ISO-8601 with their offset (+05:30).

        `status` is the dashboard tab the file is under, one of the summary's status labels. The dashboard calls its
        first tab All, but that tab lists the Needs Review queue, which is what the API calls it. A file we requested a
        takedown for is under Requested Takedown, or Taken Down once the takedown is complete, unless it is marked
        Investigating, Dismissed or Filtered by AI, which keep it. `status` is null for a file in no tab, such as one
        still on a retired status code. The raw codes the tab is computed from are returned beside it, for
        transparency: `triage_status` (0 needs review, 1 accepted; 2 and 3 are retired codes) and `response_status`
        (0 none, 1 investigating, 4 dismissed, 6 filtered by AI), with `takedown_requested_on`.
        `severity` is the summary's severity bucket for `threat_level`: High for the malicious, suspicious and
        ambiguous malware verdicts and the Critical and High sensitivity tiers, Medium for the Medium sensitivity tier,
        Low for every other level, including a blank or unrecognised one. `threat_level` is the level as stored and
        `threat_level_label` its display label (the export's Threat Level column). `source` is the summary's source
        label: Hybrid Analysis or Scribd, any other stored source as stored, null when none is stored.
        `risk_score` (0-100) is null for a file that has not been scored. `time_stamp` is when ShadowMap discovered the
        file - the module's discovery column (the export's Discovered). `updated_at` is the record's last-updated time
        as stored; not every change to a file moves it, so do not use it to detect changes. `web_url` opens the file on
        your dashboard, and `/api/v1/data-leaks/leaked-files/{id}` returns the same record on its own.

        Filters combine: a file is listed only when it matches every filter given. The `severity` filter takes the
        labels the summary groups by, so the files listed under a label are exactly the ones the summary counts under
        it. The summary (`/api/v1/data-leaks/leaked-files/summary`) takes the same filters, with the same labels and the
        same 422s, and counts exactly what this list returns under them.
      parameters:
        - $ref: "#/components/parameters/PageSize"
        - $ref: "#/components/parameters/Cursor"
        - name: sort
          in: query
          required: false
          description: |
            One key to order the records by, ascending; prefix it with `-` for descending: `severity` (the record's `severity`, by the risk behind it: `-severity` puts the most severe first), `first_seen` (`time_stamp`, the discovery column `discovered_from` and `discovered_to` filter) or `id`.
            The record id breaks ties, in the same direction. Sorting is for ranking and top-N reads: a record whose
            `severity` or `last_seen` changes during a walk can be skipped, or listed twice. For a complete pull use no
            `sort`, `sort=id` or `sort=first_seen`, and de-duplicate by `id`. One key only: a second (`sort=a,b`)
            or an unknown key is refused with 422 naming the keys this list takes - the `sort_keys` of /api/v1/capabilities.
            Without `sort` the order above, unchanged.
            A `next_cursor` continues only the sort it was returned under: send it back with the same `sort` (or none, for
            an unsorted list); with another key, the other direction or no sort it is refused with 422.
          schema:
            type: string
            enum: [severity, "-severity", first_seen, "-first_seen", id, "-id"]
        - name: status
          in: query
          required: false
          description: |
            Only files under these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Investigating`. The labels are the summary's status labels. Several labels return the union
            of their tabs, each file once; a label given twice counts once. An unknown label or an empty entry is
            refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "Requested Takedown", "Taken Down", "Filtered by AI"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - name: severity
          in: query
          required: false
          description: |
            Only files of these severities: one label, or several separated by commas, for example `High,Medium`. The
            labels are the summary's `severity` groups: High, Medium and Low. Several labels return the files of any of
            them, each once; a label given twice counts once. An unknown label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items: { type: string, enum: [High, Medium, Low] }
      responses:
        "200":
          description: One page of leaked files.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LeakedFileList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/data-leaks/leaked-files/{id}:
    get:
      operationId: getLeakedFile
      tags: [Data Leaks]
      summary: Get one leaked file
      description: |
        Requires scope `data-leaks:read`. The record the list returns for this `id`, with every field. An id that is not
        one of your listed files - another company's, one that does not exist, or one the list leaves out (a zero
        confidence score, or removed from your scope) - answers 404 `not_found`, the same response whichever it is. The
        endpoint takes no query parameter.
      parameters:
        - $ref: "#/components/parameters/RecordId"
      responses:
        "200":
          description: The file.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LeakedFileDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/data-leaks/leaked-files/summary:
    get:
      operationId: getLeakedFilesSummary
      tags: [Data Leaks]
      summary: Count leaked file findings by status tab, optionally by severity or source
      description: |
        Requires scope `data-leaks:read`. Status labels: Needs Review, Investigating, Accepted Risk,
        Dismissed, Requested Takedown (a takedown we requested is not yet complete), Taken Down (a
        takedown we requested is complete), Filtered by AI. A file we requested a takedown for is under
        Requested Takedown or Taken Down unless it is marked Investigating, Dismissed or Filtered by
        AI, which keep it. The dashboard calls its first tab All, but that tab lists the Needs Review
        queue, which is what the API calls it. `all` is not that tab's count: it counts every file the
        dashboard's total counts, whatever its tab. No file is in two tabs, and a file in a state the
        dashboard no longer uses (for example a retired status code) can be in `all` but in no tab.
        Severity labels: High, Medium, Low, from the file's threat level: High for the malicious,
        suspicious and ambiguous malware verdicts and the Critical and High sensitivity tiers, Medium
        for the Medium sensitivity tier, Low for every other level, including a blank or unrecognised
        one. Source labels: Hybrid Analysis, Scribd; any other stored source is returned as stored,
        after these, and a file with no source is grouped under `null`, listed last. Files with a zero
        confidence score are not counted, as on the dashboard.

        Filters: the list's filters (`/api/v1/data-leaks/leaked-files`) - `status`, `discovered_from`, `discovered_to`
        and `severity` - apply here too, with the same labels and the same 422s, so the summary counts exactly the
        records the list returns under them. The discovery window and the `severity` filter narrow every count: each
        tab, `all` and `removed_from_scope`. A `status` filter limits the status groups to the tabs it names (in tab
        order, zeros included) and `all` to the findings in any of them, and `removed_from_scope` is then null: a
        descoped finding is in no tab. `group_by` groups the findings the filters leave.
      parameters:
        - $ref: "#/components/parameters/GroupBy"
        - name: status
          in: query
          required: false
          description: |
            Count only these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Investigating`. The status groups are then these tabs alone, in tab order, zeros included; `all`
            counts the findings in any of them, and `removed_from_scope` is null. The labels are the list's; an unknown
            label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "Requested Takedown", "Taken Down", "Filtered by AI"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - name: severity
          in: query
          required: false
          description: |
            Count only findings under these `severity` labels: one label, or several separated by commas, for example
            `High,Medium`. They are the labels `group_by=severity` reports - the list's `severity` filter; an unknown label
            or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["High", "Medium", "Low"]
      responses:
        "200": { $ref: "#/components/responses/Summary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/data-leaks/leaked-apis:
    get:
      operationId: listLeakedApis
      tags: [Data Leaks]
      summary: List your leaked APIs, one page at a time, with their leaked values in full
      description: |
        Requires scope `data-leaks:read`. Exactly the APIs the summary's `all` counts, in ascending `id` order - walking
        every page returns each of them once - at most 200 per page (the records are heavy). As on the dashboard, an API
        with no API URL (null), with a zero confidence score, or removed from your scope is not listed. Records are
        typed JSON: integers and booleans as such, null when a value is unknown, and timestamps in ISO-8601 with their
        offset (+05:30).

        `auth`, `headers`, `params` and `body` are the leaked request's values, returned IN FULL exactly as stored -
        not decoded, truncated or masked; they often hold live credentials, so store and log them as secrets. Each can
        be JSON text or plain text, and is null when nothing was captured.

        `status` is the dashboard tab the API is under, one of the summary's status labels. A tab is decided by the
        workflow code and the liveness, which are returned beside it for transparency: `response_status` (null or 1
        new, 6 reopened: Needs Review; 10 Investigating; 4 Accepted Risk; 2 public and 9 false positive: Dismissed; 3 To
        Be Closed; 5 Closed; 11 Filtered by AI) and `online` (true online, false gone, null unknown). Closed wins over the
        liveness; any other code on a gone API is Offline; Needs Review, Investigating, Accepted Risk, Dismissed, To Be
        Closed and Filtered by AI hold online APIs only (a null liveness counts as online there). The tabs do not cover
        every API: one in a state no tab names (for example the Open code, 7) has `status` null.
        `severity` is the summary's severity label for the stored risk: High, Medium or Low in any letter case; any
        other stored value is returned as stored (an empty one included), and a null risk has a null severity.
        `leaked_on` is when the API was leaked - the module's discovery column. A few APIs have none (null): they are
        listed, but `discovered_from` and `discovered_to` never return them. `updated_at` is the record's last-updated
        time as stored; not every change moves it, so do not use it to detect changes. `web_url` opens the API on your
        dashboard, and `/api/v1/data-leaks/leaked-apis/{id}` returns the same record on its own. Leaked APIs also feed
        Leaked Credentials, which counts the credentials found in them: the two cannot be added up.

        Filters combine: an API is listed only when it matches every filter given. The `severity` filter takes the
        labels the summary groups by, so the APIs listed under a label are exactly the ones the summary counts under it.
        The summary (`/api/v1/data-leaks/leaked-apis/summary`) takes the same filters, with the same labels and the same
        422s, and counts exactly what this list returns under them.
      parameters:
        - name: page_size
          in: query
          required: false
          description: Rows per page, 1 to 200 (this module's maximum, as in `/api/v1/capabilities`). Default 100.
          schema: { type: integer, minimum: 1, maximum: 200 }
        - $ref: "#/components/parameters/Cursor"
        - name: sort
          in: query
          required: false
          description: |
            One key to order the records by, ascending; prefix it with `-` for descending: `severity` (the record's `severity`, by the risk behind it: `-severity` puts the most severe first), `first_seen` (`leaked_on`, the discovery column `discovered_from` and `discovered_to` filter) or `id`.
            The record id breaks ties, in the same direction. Sorting is for ranking and top-N reads: a record whose
            `severity` or `last_seen` changes during a walk can be skipped, or listed twice. For a complete pull use no
            `sort`, `sort=id` or `sort=first_seen`, and de-duplicate by `id`. One key only: a second (`sort=a,b`)
            or an unknown key is refused with 422 naming the keys this list takes - the `sort_keys` of /api/v1/capabilities.
            Without `sort` the order above, unchanged.
            A record with no value for `first_seen` sorts before every value ascending and after every value descending.
            A `next_cursor` continues only the sort it was returned under: send it back with the same `sort` (or none, for
            an unsorted list); with another key, the other direction or no sort it is refused with 422.
          schema:
            type: string
            enum: [severity, "-severity", first_seen, "-first_seen", id, "-id"]
        - name: status
          in: query
          required: false
          description: |
            Only APIs under these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Offline`. The labels are the summary's status labels. Several labels return the union of their
            tabs, each API once; a label given twice counts once. An unknown label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "Offline", "To Be Closed", "Closed", "Filtered by AI"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - name: severity
          in: query
          required: false
          description: |
            Only APIs of these severities: one label, or several separated by commas, for example `High,Medium`. The
            labels are the summary's `severity` labels: High, Medium and Low. A stored risk no label names can be read
            on the record but not filtered on. Several labels return the APIs of any of them, each once; a label given
            twice counts once. An unknown label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items: { type: string, enum: [High, Medium, Low] }
      responses:
        "200":
          description: One page of leaked APIs.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LeakedApiList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/data-leaks/leaked-apis/{id}:
    get:
      operationId: getLeakedApi
      tags: [Data Leaks]
      summary: Get one leaked API, with its leaked values in full
      description: |
        Requires scope `data-leaks:read`. The record the list returns for this `id`, with every field - `auth`,
        `headers`, `params` and `body` in full. An id that is not one of your listed APIs - another company's, one that
        does not exist, or one the list leaves out (no API URL, a zero confidence score, or removed from your scope) -
        answers 404 `not_found`, the same response whichever it is. The endpoint takes no query parameter.
      parameters:
        - $ref: "#/components/parameters/RecordId"
      responses:
        "200":
          description: The leaked API.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LeakedApiDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/data-leaks/leaked-apis/summary:
    get:
      operationId: getLeakedApisSummary
      tags: [Data Leaks]
      summary: Count leaked API findings by status tab, optionally by severity
      description: |
        Requires scope `data-leaks:read`. Status labels: Needs Review, Investigating, Accepted Risk,
        Dismissed, Offline, To Be Closed, Closed, Filtered by AI. The dashboard shows Filtered by AI
        only where AI review is enabled; the API always returns it. `all` is the dashboard's All tab.
        No record is in two tabs, and a record in a state no tab covers (for example the Open state)
        is in `all` but in no tab, so the tabs can add up to less than `all`
        (`meta.status_partition` is false). As on the dashboard, a record with no API URL (null) or
        with a zero confidence score is not counted. Severity labels: High, Medium, Low, matched in
        any letter case. A record whose risk is null has severity null, and any other stored risk
        value (an empty one included) is returned as stored, after these labels.

        Filters: the list's filters (`/api/v1/data-leaks/leaked-apis`) - `status`, `discovered_from`, `discovered_to`
        and `severity` - apply here too, with the same labels and the same 422s, so the summary counts exactly the
        records the list returns under them. The discovery window and the `severity` filter narrow every count: each
        tab, `all` and `removed_from_scope`. A `status` filter limits the status groups to the tabs it names (in tab
        order, zeros included) and `all` to the findings in any of them, and `removed_from_scope` is then null: a
        descoped finding is in no tab. `group_by` groups the findings the filters leave.
      parameters:
        - $ref: "#/components/parameters/GroupBy"
        - name: status
          in: query
          required: false
          description: |
            Count only these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Investigating`. The status groups are then these tabs alone, in tab order, zeros included; `all`
            counts the findings in any of them, and `removed_from_scope` is null. The labels are the list's; an unknown
            label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "Offline", "To Be Closed", "Closed", "Filtered by AI"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - name: severity
          in: query
          required: false
          description: |
            Count only findings under these `severity` labels: one label, or several separated by commas, for example
            `High,Medium`. They are the labels `group_by=severity` reports - the list's `severity` filter; an unknown label
            or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["High", "Medium", "Low"]
      responses:
        "200": { $ref: "#/components/responses/Summary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/attack-surface/web-applications:
    get:
      operationId: listWebApplications
      tags: [Attack Surface]
      summary: List your web application exposures, one page at a time
      description: |
        Requires scope `attack-surface:read`. Every web application the summary's `all` counts, each ONCE, in ascending
        `id` order - walking every page returns each of them once - at most 200 per page (the records are heavy). Records
        are typed JSON: integers as such, null when a value is unknown or empty, and timestamps in ISO-8601 with their
        offset (+05:30).

        One record is one logical web application: its protocol, host, port and path, the host and path compared
        case-insensitively - (proto, host, port, path) is what identifies it. ShadowMap can hold several copies of one
        application (a CDN copy, a copy found again at another IP address, a closed earlier copy) and represents it by
        one of them, the one the dashboard shows: a live copy before a closed one, then one not on a CDN, then the lowest
        id. A record's `id` is that representative's, so a web application's `id` can change when ShadowMap's
        representative for it changes - for example when a scan closes the representative while a copy is still live.
        The old `id` then answers 404, like any id the list does not return. Use (proto, host, port, path) to follow one
        application from one pull to the next.

        `status` is the dashboard tab the application is under, one of the summary's status labels: Closed for a closed
        application, and for a live one Needs Review until an analyst responds, then the tab of the response
        (`response_status`: 9 Investigating, 4 Accepted Risk, 2 Dismissed - which the export calls Reviewed - and 3 To Be
        Closed). `status` is null for an application in no tab: one with a legacy lifecycle code, or a response no tab
        shows. `lifecycle_status` is the stored lifecycle, the export's Status (New, Open, Reopened or Closed), null for
        a legacy code. ShadowMap closes an application itself when scans stop finding it; `close_reason` records why.
        `uri` is built as the export builds it, from `proto`, `host` (the IP when there is none), `port` - always written,
        even 80 or 443 - and `path`. `mapped_hosts` are the host names ShadowMap mapped to the application's public IP in
        the scan that found it. `tags` are the stored tags, every one (the dashboard's list hides the tags of closed tag
        rules), and `custom_tags` your own. `asn`, `provider` and `country` come from the IP's ASN and geolocation
        records, `netblock` from the application. `severity` is the summary's label of ShadowMap's risk;
        `suggested_risk` is the risk an analyst suggested in its place, in the same labels.
        `first_seen_on` is when ShadowMap first found the application - the module's discovery column. `updated_at` moves
        on scans and system closes as well as on analyst changes, so do not use it to detect changes. `web_url` opens the
        application on your dashboard, and `/api/v1/attack-surface/web-applications/{id}` returns the same record on its
        own.

        Filters combine: an application is listed only when it matches every filter given. The `severity` filter takes
        the labels the summary groups by, so the applications listed under a label are exactly the ones the summary
        counts under it. The summary (`/api/v1/attack-surface/web-applications/summary`) takes the same filters, with
        the same labels and the same 422s, and counts exactly what this list returns under them.
      parameters:
        - name: page_size
          in: query
          required: false
          description: Rows per page, 1 to 200 (this module's maximum, as in `/api/v1/capabilities`). Default 100.
          schema: { type: integer, minimum: 1, maximum: 200 }
        - $ref: "#/components/parameters/Cursor"
        - name: sort
          in: query
          required: false
          description: |
            One key to order the records by, ascending; prefix it with `-` for descending: `severity` (the record's `severity`, by the risk behind it: `-severity` puts the most severe first), `first_seen` (`first_seen_on`, the discovery column `discovered_from` and `discovered_to` filter), `last_seen` (`last_seen_on`) or `id`.
            The record id breaks ties, in the same direction. Sorting is for ranking and top-N reads: a record whose
            `severity` or `last_seen` changes during a walk can be skipped, or listed twice. For a complete pull use no
            `sort`, `sort=id` or `sort=first_seen`, and de-duplicate by `id`. One key only: a second (`sort=a,b`)
            or an unknown key is refused with 422 naming the keys this list takes - the `sort_keys` of /api/v1/capabilities.
            Without `sort` the order above, unchanged.
            A record with no value for `first_seen` or `last_seen` sorts before every value ascending and after every value descending.
            A `next_cursor` continues only the sort it was returned under: send it back with the same `sort` (or none, for
            an unsorted list); with another key, the other direction or no sort it is refused with 422.
          schema:
            type: string
            enum: [severity, "-severity", first_seen, "-first_seen", last_seen, "-last_seen", id, "-id"]
        - name: status
          in: query
          required: false
          description: |
            Only applications under these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Investigating`. The labels are the summary's status labels. Several labels return the union of
            their tabs, each application once; a label given twice counts once. An unknown label or an empty entry is
            refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "To Be Closed", "Closed"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - name: severity
          in: query
          required: false
          description: |
            Only applications of these severities: one label, or several separated by commas, for example `High,Medium`.
            The labels are the summary's `severity` labels. Several labels return the applications of any of them, each
            once; a label given twice counts once. An unknown label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items: { type: string, enum: [High, Medium, Low, Informational] }
      responses:
        "200":
          description: One page of web applications.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebApplicationList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/attack-surface/web-applications/{id}:
    get:
      operationId: getWebApplication
      tags: [Attack Surface]
      summary: Get one web application exposure
      description: |
        Requires scope `attack-surface:read`. The record the list returns for this `id`, with every field. An id that is
        not one of your listed applications - another company's, one that does not exist, or a copy that is not its
        logical application's representative (see the list) - answers 404 `not_found`, the same response whichever it is;
        the dashboard still opens such a copy, the API does not. The endpoint takes no query parameter.
      parameters:
        - $ref: "#/components/parameters/RecordId"
      responses:
        "200":
          description: The web application.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebApplicationDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/attack-surface/web-applications/summary:
    get:
      operationId: getWebApplicationsSummary
      tags: [Attack Surface]
      summary: Count web application exposures by status tab, optionally by severity
      description: |
        Requires scope `attack-surface:read`. One web application is one logical application
        (protocol, host, port and path, compared case-insensitively). ShadowMap may hold several
        records for it (a CDN copy, a copy found again at another IP address, a closed earlier copy);
        it is counted once, under the status of the record the dashboard shows. Status labels: Needs
        Review, Investigating, Accepted Risk, Dismissed, To Be Closed, Closed. Severity labels: High,
        Medium, Low, Informational. An application whose record has no computed severity groups under
        `null`, listed last. `all` counts every logical application; one with a legacy status is in
        `all` but in no tab.

        Filters: the list's filters (`/api/v1/attack-surface/web-applications`) - `status`, `discovered_from`,
        `discovered_to` and `severity` - apply here too, with the same labels and the same 422s, so the summary counts
        exactly the records the list returns under them. The discovery window and the `severity` filter narrow every
        count: each tab and `all`. A `status` filter limits the status groups to the tabs it names (in tab order, zeros
        included) and `all` to the findings in any of them. `group_by` groups the findings the filters leave.

        Hosts at risk: `group_by=host` ranks the hosts (the record's `host`) with the most applications under the filters,
        `top` of them (default 10, at most 100), by count descending then host ascending, each application counted once,
        with its `by_severity` counts. A host can be a domain or an IP address; hosts that differ only in letter case are
        one host. For example the ten hosts with the most open exposures:
        `/api/v1/attack-surface/web-applications/summary?group_by=host&top=10&status=Needs%20Review,Investigating,Accepted%20Risk`.
      parameters:
        - $ref: "#/components/parameters/GroupBy"
        - $ref: "#/components/parameters/Top"
        - name: status
          in: query
          required: false
          description: |
            Count only these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Investigating`. The status groups are then these tabs alone, in tab order, zeros included; `all`
            counts the findings in any of them. The labels are the list's; an unknown label or an empty entry is refused
            with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "To Be Closed", "Closed"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - name: severity
          in: query
          required: false
          description: |
            Count only findings under these `severity` labels: one label, or several separated by commas, for example
            `High,Medium`. They are the labels `group_by=severity` reports - the list's `severity` filter; an unknown label
            or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["High", "Medium", "Low", "Informational"]
      responses:
        "200": { $ref: "#/components/responses/Summary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/attack-surface/mobile-applications:
    get:
      operationId: listMobileApplications
      tags: [Attack Surface]
      summary: List the mobile apps found for you, one page at a time
      description: |
        Requires scope `attack-surface:read`. Every app the summary's `all` counts, each ONCE, in ascending `id` order -
        walking every page returns each of them once - at most 500 per page. As on the dashboard, an app removed from your
        scope is not listed. Records are typed JSON: integers and numbers as such, null when a value is unknown or empty,
        and timestamps in ISO-8601 with their offset (+05:30). The same apps are listed under Brand Protection's
        Fake Applications page: do not add the two together.

        `app_id` is the app's id on its store - the Play Store package name or the App Store bundle id - and `store` the
        store: Play Store or App Store (null for a store the dashboard has no name for). `store_url`, `developer`, `score`,
        `downloads` (Play Store only) and `size` are the store's own data as ShadowMap last read it, each null when the
        store gave none; `developer_url` is the developer's page on the store, built from the store's developer name and
        id as the dashboard's export builds it. `alert_count` and `secret_count` count the issues and the leaked secrets
        ShadowMap found in the app's package as the export counts them: every finding, so a secret found in two places
        counts twice (the dashboard's detail page counts each distinct secret once).

        `status` is the dashboard tab the app is under, one of the summary's status labels: Genuine or Dismissed by your
        verdict, Investigating or Filtered by AI by its review code, Requested Takedown (we filed a takedown that has not
        completed) or Taken Down (a takedown completed), and otherwise Needs Review. A takedown request that has since been
        deleted still places an app in a takedown tab, exactly as the dashboard's tab counts do. `status` is null for an app
        in no tab (a review code no tab shows).
        `created_at` is when ShadowMap first found the app - the module's discovery column - and `updated_at` when its row
        was last updated; not every change moves it, so do not use it to detect changes. `store_updated_at` is the date the
        store says the app was last updated. `web_url` opens the app on your dashboard, and
        `/api/v1/attack-surface/mobile-applications/{id}` returns the same record on its own.

        Filters combine: an app is listed only when it matches every filter given. The summary
        (`/api/v1/attack-surface/mobile-applications/summary`) takes the same filters, with the same labels and the same
        422s, and counts exactly what this list returns under them.
      parameters:
        - $ref: "#/components/parameters/PageSize"
        - $ref: "#/components/parameters/Cursor"
        - name: sort
          in: query
          required: false
          description: |
            One key to order the records by, ascending; prefix it with `-` for descending: `first_seen` (`created_at`, the discovery column `discovered_from` and `discovered_to` filter) or `id`.
            The record id breaks ties, in the same direction. Sorting is for ranking and top-N reads: a record whose
            `severity` or `last_seen` changes during a walk can be skipped, or listed twice. For a complete pull use no
            `sort`, `sort=id` or `sort=first_seen`, and de-duplicate by `id`. One key only: a second (`sort=a,b`)
            or an unknown key is refused with 422 naming the keys this list takes - the `sort_keys` of /api/v1/capabilities.
            Without `sort` the order above, unchanged.
            A `next_cursor` continues only the sort it was returned under: send it back with the same `sort` (or none, for
            an unsorted list); with another key, the other direction or no sort it is refused with 422.
          schema:
            type: string
            enum: [first_seen, "-first_seen", id, "-id"]
        - name: status
          in: query
          required: false
          description: |
            Only apps under these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Requested Takedown`. The labels are the summary's status labels. Several labels return the union
            of their tabs, each app once; a label given twice counts once. An unknown label or an empty entry is refused
            with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Genuine", "Dismissed", "Requested Takedown", "Taken Down", "Filtered by AI"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
      responses:
        "200":
          description: One page of mobile apps.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MobileApplicationList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/attack-surface/mobile-applications/{id}:
    get:
      operationId: getMobileApplication
      tags: [Attack Surface]
      summary: Get one mobile app
      description: |
        Requires scope `attack-surface:read`. The record the list returns for this `id`, with every field. An id that is
        not one of your listed apps - another company's, one that does not exist, or one removed from your scope - answers
        404 `not_found`, the same response whichever it is; the dashboard still opens an app removed from your scope, the
        API does not. The endpoint takes no query parameter.
      parameters:
        - $ref: "#/components/parameters/RecordId"
      responses:
        "200":
          description: The mobile app.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MobileApplicationDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/attack-surface/mobile-applications/summary:
    get:
      operationId: getMobileApplicationsSummary
      tags: [Attack Surface]
      summary: Count mobile application findings by status tab
      description: |
        Requires scope `attack-surface:read`. Status labels: Needs Review, Investigating, Genuine,
        Dismissed, Requested Takedown, Taken Down, Filtered by AI (the dashboard shows Filtered by AI
        only where AI review is enabled for you). `group_by` takes `status` only. `all` counts every app
        found for you; an app in a state no tab models (for example a retired takedown state that is also
        under investigation) is in `all` but in no tab. The same apps are listed under Brand
        Protection's Fake Applications page: do not add the two counts together.

        Filters: the list's filters (`/api/v1/attack-surface/mobile-applications`) - `status`, `discovered_from` and
        `discovered_to` - apply here too, with the same labels and the same 422s, so the summary counts exactly the
        records the list returns under them. The discovery window narrows every count: each tab, `all` and
        `removed_from_scope`. A `status` filter limits the status groups to the tabs it names (in tab order, zeros
        included) and `all` to the findings in any of them, and `removed_from_scope` is then null: a descoped finding is
        in no tab. `group_by` groups the findings the filters leave.
      parameters:
        - $ref: "#/components/parameters/GroupBy"
        - name: status
          in: query
          required: false
          description: |
            Count only these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Investigating`. The status groups are then these tabs alone, in tab order, zeros included; `all`
            counts the findings in any of them, and `removed_from_scope` is null. The labels are the list's; an unknown
            label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Genuine", "Dismissed", "Requested Takedown", "Taken Down", "Filtered by AI"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
      responses:
        "200": { $ref: "#/components/responses/Summary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/threats/alerts:
    get:
      operationId: listAlerts
      tags: [Threats]
      summary: List your alerts, one page at a time
      description: |
        Requires scope `alerts:read`. Exactly the alerts the summary's `all` counts (alerts removed from your
        scope and the alerts the dashboard's totals leave out are not listed), in ascending `added_on` order
        and then ascending `id` - or in the order `sort` asks for - walking every page returns each of them once - at
        most 500 per page. For example the five most severe open alerts:
        `/api/v1/threats/alerts?sort=-severity&page_size=5&status=Needs%20Review,Investigating,Accepted%20Risk`
        (`-severity` orders by `risk_score`, the analyst's risk where one is set, highest first).
        Records are typed JSON: integers, booleans and arrays as such, null when a value is unknown, and
        timestamps in ISO-8601 with their offset (+05:30).

        `status` is the dashboard tab the alert is under, one of the summary's status labels; it is null for
        an alert in no tab (one still carrying a retired workflow state). `scanner_status` is the scanner's own
        view (new, open, closed) and `closed_by` says who closed a Closed alert. `severity` and `finding_type`
        are the values the summary groups by. For a leaked-credential finding, which has no host, `host` and
        `full_uri` name where the credential leaked. `related_assets` lists at most 50 hosts of your open web
        applications at the alert's IP address or host, and `related_assets_truncated` is true when there are
        more. `custom_tags` holds your own tag values as stored; the summary's `custom_tag:<key>` grouping
        folds them to lower case. `asn`, `netblock` and `country` come from the alert's ASN and geolocation
        records. `scanner_close_reason` is recorded for closes from 31 March 2026 on. `web_url` opens the alert
        on your dashboard, and `/api/v1/threats/alerts/{id}` returns the same record on its own.

        Filters combine: an alert is listed only when it matches every filter given. The `severity`, `finding_type` and
        `closed_by` filters take the labels the summary groups by, so the alerts listed under a label are exactly the
        ones the summary counts under it. The summary (`/api/v1/threats/alerts/summary`) takes the same filters, with
        the same labels and the same 422s, and counts exactly what this list returns under them.
      parameters:
        - $ref: "#/components/parameters/PageSize"
        - $ref: "#/components/parameters/Cursor"
        - name: sort
          in: query
          required: false
          description: |
            One key to order the records by, ascending; prefix it with `-` for descending: `severity` (the record's `severity`, by the risk behind it: `-severity` puts the most severe first), `first_seen` (`added_on`, the discovery column `discovered_from` and `discovered_to` filter), `last_seen` (`last_seen_on`) or `id`.
            The record id breaks ties, in the same direction. Sorting is for ranking and top-N reads: a record whose
            `severity` or `last_seen` changes during a walk can be skipped, or listed twice. For a complete pull use no
            `sort`, `sort=id` or `sort=first_seen`, and de-duplicate by `id`. One key only: a second (`sort=a,b`)
            or an unknown key is refused with 422 naming the keys this list takes - the `sort_keys` of /api/v1/capabilities.
            Without `sort` the order above, unchanged.
            A record with no value for `last_seen` sorts before every value ascending and after every value descending.
            A `next_cursor` continues only the sort it was returned under: send it back with the same `sort` (or none, for
            an unsorted list); with another key, the other direction or no sort it is refused with 422.
          schema:
            type: string
            enum: [severity, "-severity", first_seen, "-first_seen", last_seen, "-last_seen", id, "-id"]
        - name: status
          in: query
          required: false
          description: |
            Only alerts under these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Investigating`. The labels are the summary's status labels. Several labels return the
            union of their tabs, each alert once; a label given twice counts once. An unknown label or an empty
            entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "To Be Closed", "Closed", "Filtered by AI"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - name: severity
          in: query
          required: false
          description: |
            Only alerts of these severities: one label, or several separated by commas, for example `High,Medium`.
            The labels are the summary's `severity` groups: High, Medium, Low and Informational. Several labels
            return the alerts of any of them, each once; a label given twice counts once. An unknown label or an
            empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items: { type: string, enum: [High, Medium, Low, Informational] }
        - name: finding_type
          in: query
          required: false
          description: |
            Only alerts of these finding types: one label, or several separated by commas. The labels are the
            summary's `finding_type` groups - the types of the dashboard's Finding Type filter, and Other for an
            alert of none of them. Several labels return the alerts of any of them, each once; a label given twice
            counts once. An unknown label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum:
                - "Web Application Vulnerability"
                - "Service Exposure"
                - "TLS Configuration"
                - "DNS Configuration"
                - "Cloud Storage Misconfiguration"
                - "Credential Exposure"
                - "Domain Lifecycle"
                - "Reputation"
                - "Honeypot Interaction"
                - "Cryptojacking"
                - "Website Compromise / Malware"
                - "Origin Server Exposure (WAF Bypass)"
                - "Internal Application Exposure"
                - "Known Exploited Vulnerability"
                - "Supply-Chain Compromise"
                - "Other"
        - name: closed_by
          in: query
          required: false
          description: |
            Only Closed alerts, by who closed them: `analyst`, `scanner` (the scanner no longer detects the
            finding), or both separated by a comma. An alert an analyst closed that the scanner also no longer
            detects is `analyst`, as in the summary's `closed_by` groups. An unknown label or an empty entry is
            refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items: { type: string, enum: [analyst, scanner] }
      responses:
        "200":
          description: One page of alerts.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AlertList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/threats/alerts/{id}:
    get:
      operationId: getAlert
      tags: [Threats]
      summary: Get one alert
      description: |
        Requires scope `alerts:read`. The record the list returns for this `id`, with every field. An id that is not
        one of your listed alerts - another company's, one that does not exist, or one the list leaves out (removed
        from your scope, or outside the dashboard's totals) - answers 404 `not_found`, the same response whichever it
        is. The endpoint takes no query parameter.
      parameters:
        - $ref: "#/components/parameters/RecordId"
      responses:
        "200":
          description: The alert.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AlertDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/threats/alerts/summary:
    get:
      operationId: getAlertsSummary
      tags: [Threats]
      summary: Count alerts by status tab, severity, finding type, who closed them, or a custom tag
      description: |
        Requires scope `alerts:read`. Status labels: Needs Review, Investigating, Accepted Risk,
        Dismissed, To Be Closed, Closed, Filtered by AI. Closed holds the alerts an analyst closed and
        the alerts the scanner no longer detects; Filtered by AI holds the alerts AI review set aside
        (the dashboard shows that tab only where AI review is enabled for you). Other dimensions:
        `severity` (High, Medium, Low, Informational), `finding_type` (the types of the dashboard's
        Finding Type filter, and Other), `closed_by` (`analyst` or `scanner` for a Closed alert, null
        otherwise) and `custom_tag:<key>` (your own tag values, folded to lower case: an alert tagged
        `Yes` and one tagged `yes` are one group labelled `yes`; an alert with two values for the key is
        counted under both, an untagged alert under null). `all` counts the alerts the dashboard's Alerts
        totals count, leaving out `removed_from_scope` alerts and the alerts those totals leave out. An
        alert still carrying a retired workflow state is in `all` but in no tab (one the scanner no longer
        sees is in Closed). `removed_from_scope` counts the alerts the dashboard's Analyst Response filter
        calls Removed From Scope - every descoped alert, whatever its status, so it counts alerts `all`
        leaves out too. `all` and `removed_from_scope` are therefore not a partition and must not be added
        together: `removed_from_scope` can exceed `all`, and it is still returned for a company the
        dashboard draws no Analyst Response facet for. The Reputation finding type includes the findings
        the IP Reputation summary counts: do not add the two counts together.

        Filters: the list's filters (`/api/v1/threats/alerts`) - `status`, `discovered_from`, `discovered_to`,
        `severity`, `finding_type` and `closed_by` - apply here too, with the same labels and the same 422s, so the
        summary counts exactly the records the list returns under them. The discovery window and the `severity`,
        `finding_type` and `closed_by` filters narrow every count: each tab, `all` and `removed_from_scope`. A `status`
        filter limits the status groups to the tabs it names (in tab order, zeros included) and `all` to the findings in
        any of them, and `removed_from_scope` is then null: a descoped finding is in no tab. `group_by` groups the
        findings the filters leave.

        Hosts at risk: `group_by=host` ranks the hosts (the host each alert stores) with the most alerts under the
        filters, `top` of them (default 10, at most 100), by count descending then host ascending, each with its
        `by_severity` counts (High, Medium, Low, Informational). A host can be a domain or an IP address; only findings with
        no stored host (e.g. leaked credentials) are not ranked, and hosts that differ only in letter case are one host.
        For example the ten hosts with the most open alerts:
        `/api/v1/threats/alerts/summary?group_by=host&top=10&status=Needs%20Review,Investigating,Accepted%20Risk`, and
        the five with the most open High alerts: add `&severity=High` and `top=5`.
      parameters:
        - $ref: "#/components/parameters/GroupBy"
        - $ref: "#/components/parameters/Top"
        - name: status
          in: query
          required: false
          description: |
            Count only these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Investigating`. The status groups are then these tabs alone, in tab order, zeros included; `all`
            counts the findings in any of them, and `removed_from_scope` is null. The labels are the list's; an unknown
            label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "To Be Closed", "Closed", "Filtered by AI"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - name: severity
          in: query
          required: false
          description: |
            Count only findings under these `severity` labels: one label, or several separated by commas, for example
            `High,Medium`. They are the labels `group_by=severity` reports - the list's `severity` filter; an unknown label
            or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["High", "Medium", "Low", "Informational"]
        - name: finding_type
          in: query
          required: false
          description: |
            Count only findings under these `finding_type` labels: one label, or several separated by commas, for example
            `Web Application Vulnerability,Service Exposure`. They are the labels `group_by=finding_type` reports - the
            list's `finding_type` filter; an unknown label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Web Application Vulnerability", "Service Exposure", "TLS Configuration", "DNS Configuration", "Cloud Storage Misconfiguration", "Credential Exposure", "Domain Lifecycle", "Reputation", "Honeypot Interaction", "Cryptojacking", "Website Compromise / Malware", "Origin Server Exposure (WAF Bypass)", "Internal Application Exposure", "Known Exploited Vulnerability", "Supply-Chain Compromise", "Other"]
        - name: closed_by
          in: query
          required: false
          description: |
            Count only findings under these `closed_by` labels: one label, or several separated by commas, for example
            `analyst,scanner`. They are the labels `group_by=closed_by` reports - the list's `closed_by` filter; an unknown
            label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["analyst", "scanner"]
      responses:
        "200": { $ref: "#/components/responses/Summary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/threats/ip-reputation:
    get:
      operationId: listIpReputation
      tags: [Threats]
      summary: List your IP reputation listings, one page at a time
      description: |
        Requires scope `alerts:read`. Exactly the listings the summary's `all` counts, in ascending `id` order -
        walking every page returns each of them once - at most 500 per page. Records are typed JSON: integers and
        arrays as such, null when a value is unknown, and timestamps in ISO-8601 with their offset (+05:30).

        A listing is one of your IP addresses on public blocklists for one set of flagging feeds: when the set of
        feeds flagging an IP changes, a new listing is created and the earlier one stays, so one IP can have more
        than one listing. `flagged_by` names this listing's feeds and `source_count` how many there are. `status` is
        the dashboard tab the listing is under, one of the summary's status labels; it is null for a listing in no
        tab. `severity` is the risk level the dashboard shows: how reliable the flagging feeds are, whether a feed
        reports malware or command-and-control activity, and whether the IP is dedicated to you or shared (CDN or
        cloud). `asn` is the IP's network owner as recorded, `country_code` its country as an ISO code, and
        `bot_name` the malware a feed associates with the IP. `first_seen` is when ShadowMap first found the
        listing - the module's discovery column - and `last_seen` when a scan last saw it. `updated_at` is set only
        by a change made on the dashboard (an assignment or a status change), and is null when there has been none.
        `web_url` opens the listing on your dashboard, and `/api/v1/threats/ip-reputation/{id}` returns the same
        record on its own.

        Filters combine: a listing is listed only when it matches every filter given. The `severity` filter takes the
        labels the summary groups by, so the listings listed under a label are exactly the ones the summary counts under
        it; it is refused with 422 for a company with more than 10,000 listings. The summary
        (`/api/v1/threats/ip-reputation/summary`) takes the same filters, with the same labels and the same 422s, and
        counts exactly what this list returns under them. The same findings also reach Alerts as the Reputation finding
        type.
      parameters:
        - $ref: "#/components/parameters/PageSize"
        - $ref: "#/components/parameters/Cursor"
        - name: sort
          in: query
          required: false
          description: |
            One key to order the records by, ascending; prefix it with `-` for descending: `first_seen` (`first_seen`, the discovery column `discovered_from` and `discovered_to` filter), `last_seen` (`last_seen`) or `id`.
            The record id breaks ties, in the same direction. Sorting is for ranking and top-N reads: a record whose
            `severity` or `last_seen` changes during a walk can be skipped, or listed twice. For a complete pull use no
            `sort`, `sort=id` or `sort=first_seen`, and de-duplicate by `id`. One key only: a second (`sort=a,b`)
            or an unknown key is refused with 422 naming the keys this list takes - the `sort_keys` of /api/v1/capabilities.
            Without `sort` the order above, unchanged.
            A `next_cursor` continues only the sort it was returned under: send it back with the same `sort` (or none, for
            an unsorted list); with another key, the other direction or no sort it is refused with 422.
          schema:
            type: string
            enum: [first_seen, "-first_seen", last_seen, "-last_seen", id, "-id"]
        - name: status
          in: query
          required: false
          description: |
            Only listings under these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Confirmed Threat`. The labels are the summary's status labels. Several labels return the
            union of their tabs, each listing once; a label given twice counts once. An unknown label or an empty
            entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Confirmed Threat", "Mitigated", "Accepted Risk", "False Positive"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - name: severity
          in: query
          required: false
          description: |
            Only listings of these severities: one label, or several separated by commas, for example `Critical,High`.
            The labels are the summary's `severity` groups: Critical, High, Medium and Low. Several labels return the
            listings of any of them, each once; a label given twice counts once. An unknown label or an empty entry is
            refused with 422, and so is this filter for a company with more than 10,000 listings.
          style: form
          explode: false
          schema:
            type: array
            items: { type: string, enum: [Critical, High, Medium, Low] }
      responses:
        "200":
          description: One page of IP reputation listings.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/IpReputationList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/threats/ip-reputation/{id}:
    get:
      operationId: getIpReputationListing
      tags: [Threats]
      summary: Get one IP reputation listing
      description: |
        Requires scope `alerts:read`. The record the list returns for this `id`, with every field. An id that is not
        one of your listings - another company's, or one that does not exist - answers 404 `not_found`, the same
        response whichever it is. The endpoint takes no query parameter.
      parameters:
        - $ref: "#/components/parameters/RecordId"
      responses:
        "200":
          description: The listing.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/IpReputationListingDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/threats/ip-reputation/summary:
    get:
      operationId: getIpReputationSummary
      tags: [Threats]
      summary: Count IP reputation listings by status tab, optionally by severity
      description: |
        Requires scope `alerts:read`. Status labels: Needs Review, Investigating, Confirmed Threat,
        Mitigated, Accepted Risk, False Positive. Severity labels: Critical, High, Medium, Low, the risk
        level each listing shows: how reliable the flagging feeds are, whether a feed reports malware or
        command-and-control activity, and whether the IP is dedicated to you or shared (CDN or cloud).
        A listing whose network owner is unknown (no ASN) counts as shared, so it is High at most. Four
        or more distinct aggregator feeds raise a Low listing to Medium.
        Counts are per listing, and one IP can have more than one listing. `removed_from_scope` is
        always null: this module has no such view. The same findings also reach Alerts as the
        Reputation finding type: do not add the two counts together.

        Filters: the list's filters (`/api/v1/threats/ip-reputation`) - `status`, `discovered_from`, `discovered_to` and
        `severity` - apply here too, with the same labels and the same 422s, so the summary counts exactly the records
        the list returns under them. The discovery window and the `severity` filter narrow every count: each tab and
        `all`. A `status` filter limits the status groups to the tabs it names (in tab order, zeros included) and `all`
        to the findings in any of them. `group_by` groups the findings the filters leave.
      parameters:
        - $ref: "#/components/parameters/GroupBy"
        - name: status
          in: query
          required: false
          description: |
            Count only these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Investigating`. The status groups are then these tabs alone, in tab order, zeros included; `all`
            counts the findings in any of them. The labels are the list's; an unknown label or an empty entry is refused
            with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Confirmed Threat", "Mitigated", "Accepted Risk", "False Positive"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - name: severity
          in: query
          required: false
          description: |
            Count only findings under these `severity` labels: one label, or several separated by commas, for example
            `Critical,High`. They are the labels `group_by=severity` reports - the list's `severity` filter; an unknown
            label or an empty entry is refused with 422, and so is this filter for a company with more than 10,000 records
            (the most it labels in one request).
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Critical", "High", "Medium", "Low"]
      responses:
        "200": { $ref: "#/components/responses/Summary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/inventory/domains:
    get:
      operationId: listInventoryDomains
      tags: [Inventory]
      summary: List your domains, one page at a time
      description: |
        Requires scope `inventory:read`. Every domain of your inventory, in and out of scope, in
        ascending `id` order, at most 500 per page. Each record carries the values of the dashboard's
        Domains export as the export writes them (dates like `01 May 2031`, `N/A` when unknown), with
        the counts as integers and `custom_tags` as an object of tag key to values, plus `id`,
        `added_at` (when the domain entered your inventory) and `last_seen_at` (when its record last
        changed, the dashboard's Last Seen). When ShadowMap holds several WHOIS records for a domain,
        the newest is used.
      parameters:
        - $ref: "#/components/parameters/PageSize"
        - $ref: "#/components/parameters/Cursor"
        - name: status
          in: query
          required: false
          description: Only domains with this status.
          schema: { type: string, enum: ["In Scope", "Out of Scope"] }
      responses:
        "200":
          description: One page of domains.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DomainList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/inventory/subdomains:
    get:
      operationId: listInventorySubdomains
      tags: [Inventory]
      summary: List your subdomains, one record per subdomain, one page at a time
      description: |
        Requires scope `inventory:read`. ShadowMap records a subdomain once per IP address; this list
        returns one record per subdomain with all its IP addresses in `ips`, in ascending order of the
        subdomain name, at most 500 per page. `status` follows the dashboard: In Scope when any of its
        IP addresses is in scope, otherwise Not Reachable when any is not reachable, otherwise Out of
        Scope. `parent_domain` is the most specific of your tracked domains the subdomain belongs to,
        ignoring a leading `www.`, or null when there is none. `last_seen_on` is returned as stored.
      parameters:
        - $ref: "#/components/parameters/PageSize"
        - $ref: "#/components/parameters/Cursor"
        - name: status
          in: query
          required: false
          description: Only subdomains with this status.
          schema: { type: string, enum: ["In Scope", "Out of Scope", "Not Reachable"] }
      responses:
        "200":
          description: One page of subdomains.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SubdomainList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/inventory/ips:
    get:
      operationId: listInventoryIps
      tags: [Inventory]
      summary: List your IP addresses, one page at a time
      description: |
        Requires scope `inventory:read`. Every IP address of your inventory, online and offline, in
        ascending `id` order, at most 500 per page. Each record carries the values of the dashboard's
        IP Addresses export (`last_seen_on` formatted like `01 Oct 2026`, empty when unknown) plus `id`.
      parameters:
        - $ref: "#/components/parameters/PageSize"
        - $ref: "#/components/parameters/Cursor"
        - name: status
          in: query
          required: false
          description: Only IP addresses with this status.
          schema: { type: string, enum: [Online, Offline] }
      responses:
        "200":
          description: One page of IP addresses.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/IpAddressList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/inventory/summary:
    get:
      operationId: getInventorySummary
      tags: [Inventory]
      summary: Count your domains, subdomains and IP addresses, in total and by status
      description: |
        Requires scope `inventory:read`. For each asset type, `all` is the total across every status
        (the figure the inventory pages show) and `by_status` splits it. The daily report's totals
        count only In Scope domains and subdomains and Online IP addresses: they are the "In Scope"
        and "Online" values of `by_status`. A subdomain is counted once however many IP addresses it has.
      responses:
        "200":
          description: The counts.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InventorySummaryResponse" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/scans:
    get:
      operationId: listScans
      tags: [Scans]
      summary: List your scan sessions, one page at a time
      description: |
        Requires scope `scans:read`. The scan sessions the dashboard's Reports page lists (those with
        at least one report), in ascending `id` order, at most 500 per page. Times are ISO-8601 with
        the +05:30 offset. `ended_at` and `duration_seconds` are null while a scan is running.
        `lifecycle_state`: `sealed` is a completed scan; `discarded` and `superseded` scans have an
        `ended_at` but did not complete; every other state is a scan that has not completed. It is
        null on scans that ended before lifecycle tracking began (18 July 2026): treat those with an
        `ended_at` as completed.

        Filter scans with `lifecycle_state`; its accepted values are the `status_labels` that /capabilities lists for scans (sending `status=` returns 422 with the accepted names).
      parameters:
        - $ref: "#/components/parameters/PageSize"
        - $ref: "#/components/parameters/Cursor"
        - name: started_from
          in: query
          required: false
          description: Only scans started on or after this day (UTC+05:30).
          schema: { type: string, format: date }
        - name: started_to
          in: query
          required: false
          description: Only scans started on or before this day (UTC+05:30).
          schema: { type: string, format: date }
        - name: lifecycle_state
          in: query
          required: false
          description: Only scans in this lifecycle state. Scans with no recorded state never match.
          schema:
            type: string
            enum: [running, paused, held_coverage, held_host_count, wedged, dead_pre_nmap, dead_analyze_incomplete, sealed, discarded, superseded]
      responses:
        "200":
          description: One page of scans.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ScanList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/darkweb/data-breaches:
    get:
      operationId: listDataBreaches
      tags: [Dark Web]
      summary: List the breached credentials found for you, one page at a time
      description: |
        Requires scope `dark-web:read`. Exactly the breaches the summary's `all` counts, in ascending `id` order - walking
        every page returns each of them once - at most 200 per page (the records are heavy). Records are typed JSON:
        booleans and objects as such, null when a value is unknown or empty, and timestamps in ISO-8601 with their offset
        (+05:30).

        One record is one leaked credential. Breach data is shared across ShadowMap: a breach is yours when it is on a
        domain of your last completed scan or linked to one of your tracked executives, and a credential stored more than
        once is listed once, as on the dashboard. Its `id` is the id of the copy kept for it - the keeper: the copy linked
        to one of your own tracked executives when there is one, else the one stored first (the lowest id). An `id` is
        stable only while that rule gives the same keeper - adding or removing an executive can move a credential to
        another keeper id - and the id of a copy that is not the keeper answers 404 on the detail.

        `password` is returned in full and exactly as stored (empty when the breach exposed none), as the dashboard's data
        and its export carry it (the dashboard's list masks it on screen): treat these records as secrets. `breach_value`
        is the leaked identifier - an e-mail address, a phone number or a username, as `breach_type` says - and `domain`
        the domain the breach was matched on. `breach_date` is the breach's date exactly as its source publishes it (month
        precision at best, sometimes a bare year): a string, never read as a time.

        `status` is the dashboard tab the breach is under, one of the summary's status labels; the stored status code is
        never returned, and `status` is null for a breach in no tab (an unrecognised legacy state). `actioned` is the
        legacy action flag, true when it is stored as Yes; on an untriaged breach it is what puts it under To Be Closed. `severity` and `source`
        are the summary's labels, the ones `group_by` counts the breach under. `is_executive` and `executive_name` name
        your own tracked executives only: a breach whose stored executive is not one of yours - another company's, or
        one no longer tracked - carries `is_executive` false and no name. `custom_tags` are your own tags, key to
        values.

        `created_at` is when ShadowMap stored the breach - the module's discovery column. It is null for a breach stored
        without one and for one holding the zero date (some older breaches do): either way the breach was found at an
        unknown time, so it is in no discovery window - not even one given only `discovered_to`, although the zero date
        sorts before every date - and the summary under a window does not count it. The clock the breach collector stamps
        `created_at`, `updated_at`, `first_seen_at` and `last_seen_at` with is not verified: the API reads them as
        +05:30, as the dashboard does, so a value stamped in UTC is 5 hours 30 minutes early, and a breach stored near
        midnight can fall in the neighbouring day's window.
        `updated_at` is the record's last-updated time as stored; not every change to a breach moves it, so do not use it
        to detect changes. `web_url` opens the breach on your dashboard, and `/api/v1/darkweb/data-breaches/{id}` returns
        the same record on its own.

        Filters combine: a breach is listed only when it matches every filter given. The `severity` filter takes the
        labels the summary groups by, so the breaches listed under a label are exactly the ones the summary counts under
        it. The summary (`/api/v1/darkweb/data-breaches/summary`) takes the same filters, with the same labels and the
        same 422s, and counts exactly what this list returns under them.
      parameters:
        - name: page_size
          in: query
          required: false
          description: Rows per page, 1 to 200 (this module's maximum, as in `/api/v1/capabilities`). Default 100.
          schema: { type: integer, minimum: 1, maximum: 200 }
        - $ref: "#/components/parameters/Cursor"
        - name: sort
          in: query
          required: false
          description: |
            One key to order the records by, ascending; prefix it with `-` for descending: `severity` (the record's `severity`, by the risk behind it: `-severity` puts the most severe first), `first_seen` (`created_at`, the discovery column `discovered_from` and `discovered_to` filter), `last_seen` (`last_seen_at`) or `id`.
            The record id breaks ties, in the same direction. Sorting is for ranking and top-N reads: a record whose
            `severity` or `last_seen` changes during a walk can be skipped, or listed twice. For a complete pull use no
            `sort`, `sort=id` or `sort=first_seen`, and de-duplicate by `id`. One key only: a second (`sort=a,b`)
            or an unknown key is refused with 422 naming the keys this list takes - the `sort_keys` of /api/v1/capabilities.
            Without `sort` the order above, unchanged.
            A record with no value for `first_seen` or `last_seen` sorts before every value ascending and after every value descending.
            A `next_cursor` continues only the sort it was returned under: send it back with the same `sort` (or none, for
            an unsorted list); with another key, the other direction or no sort it is refused with 422.
          schema:
            type: string
            enum: [severity, "-severity", first_seen, "-first_seen", last_seen, "-last_seen", id, "-id"]
        - name: status
          in: query
          required: false
          description: |
            Only breaches under these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Investigating`. The labels are the summary's status labels. Several labels return the union of
            their tabs, each breach once; a label given twice counts once. An unknown label or an empty entry is refused
            with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Dismissed", "To Be Closed", "Closed", "Filtered by AI"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - name: severity
          in: query
          required: false
          description: |
            Only breaches of these severities: one label, or several separated by commas, for example `Critical,High`.
            The labels are the summary's `severity` labels; Other is a severity ShadowMap has no name for. Several labels
            return the breaches of any of them, each once; a label given twice counts once. An unknown label or an empty
            entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items: { type: string, enum: [Critical, High, Medium, Low, Other] }
      responses:
        "200":
          description: One page of breached credentials.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DataBreachList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/darkweb/data-breaches/{id}:
    get:
      operationId: getDataBreach
      tags: [Dark Web]
      summary: Get one breached credential
      description: |
        Requires scope `dark-web:read`. The record the list returns for this `id`, with every field - the password in full.
        An id that is not one of your listed breaches - another company's, one that does not exist, or a copy of a
        credential that is not its keeper (see the list) - answers 404 `not_found`, the same response whichever it is; the
        dashboard's own detail still opens such a copy, the API does not. The endpoint takes no query parameter.
      parameters:
        - $ref: "#/components/parameters/RecordId"
      responses:
        "200":
          description: The breached credential.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DataBreachDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/darkweb/data-breaches/summary:
    get:
      operationId: getDataBreachesSummary
      tags: [Dark Web]
      summary: Count data breach findings by status tab, optionally by severity or source
      description: |
        Requires scope `dark-web:read`. Status labels: Needs Review, Investigating, Dismissed,
        To Be Closed, Closed, Filtered by AI. Severity labels: Critical, High, Medium, Low, Other.
        Source labels: Data Breach, Stealer Log, Other; a severity or source value the module does not
        name (a NULL or unlisted stored value) is returned as Other. Breach records are matched to you
        through the domains of your last completed scan and your tracked executives, and a credential
        stored more than once is counted once, as on the dashboard. `all` counts every finding; a
        finding with an unrecognised legacy state is in `all` but in no tab. `removed_from_scope` is
        always null: this module has no Removed From Scope view.

        Filters: the list's filters (`/api/v1/darkweb/data-breaches`) - `status`, `discovered_from`, `discovered_to` and
        `severity` - apply here too, with the same labels and the same 422s, so the summary counts exactly the records
        the list returns under them. The discovery window and the `severity` filter narrow every count: each tab and
        `all`. A `status` filter limits the status groups to the tabs it names (in tab order, zeros included) and `all`
        to the findings in any of them. `group_by` groups the findings the filters leave.
      parameters:
        - $ref: "#/components/parameters/GroupBy"
        - name: status
          in: query
          required: false
          description: |
            Count only these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Investigating`. The status groups are then these tabs alone, in tab order, zeros included; `all`
            counts the findings in any of them. The labels are the list's; an unknown label or an empty entry is refused
            with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Dismissed", "To Be Closed", "Closed", "Filtered by AI"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - name: severity
          in: query
          required: false
          description: |
            Count only findings under these `severity` labels: one label, or several separated by commas, for example
            `Critical,High`. They are the labels `group_by=severity` reports - the list's `severity` filter; an unknown
            label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Critical", "High", "Medium", "Low", "Other"]
      responses:
        "200": { $ref: "#/components/responses/Summary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/darkweb/discussions:
    get:
      operationId: listDiscussions
      tags: [Dark Web]
      summary: List the dark web discussions found for you, one page at a time
      description: |
        Requires scope `dark-web:read`. Exactly the discussions the summary's `all` counts (a discussion removed from your
        scope is not listed), in ascending `id` order - walking every page returns each of them once - at most 500 per
        page. Records are typed JSON: arrays as such, null when a value is unknown or empty, and timestamps in ISO-8601
        with their offset (+05:30). No saved-search restriction applies: the list holds every discussion of your company,
        as the summary counts it.

        `status` is the dashboard tab the discussion is under, one of the summary's status labels; the stored status code
        itself is never returned. A discussion marked taken down (`liveness` is `takendown`), or one with a takedown
        request on this module, is in no tab, so its `status` is null - as is a discussion on a code no tab names.
        `liveness` is the stored state the export calls "status": `online`, `takendown`, or a legacy value as stored.
        `source` is the stored source folded to lower case, exactly the label `group_by=source` counts the discussion under
        (for a DWI feed row it is `dwi_ransomware` or `dwi_threatfeed`; the dashboard's list shows the threat actor's name
        there instead). `title` and `description` are the scraped text as the dashboard's list shows it, with HTML entities
        decoded and double-encoded UTF-8 repaired. `url` is the discussion's identifier exactly as stored - often an
        `.onion` address, never rewritten through the `.onion.ly` gateway the dashboard links to; a post number or a
        relative path is kept as stored, and it is null only for the placeholders that mean "no URL" (blank, `None`,
        `http://None` and the like). `keywords` are the terms the discussion matched, as the dashboard's list shows them:
        for a forum post, lower-cased, each once; for a DWI feed row, the feed's own list.

        `published_on` is the module's discovery column. It is null for a few discussions, and no discovery window
        (`discovered_from` / `discovered_to`) returns a discussion without one: walk the list without a window to see
        them. `ingested_on` is when ShadowMap collected the discussion. `updated_at` is the record's last-updated time as
        stored; not every change to a discussion moves it, so do not use it to detect changes. `web_url` opens the
        discussion on your dashboard, and `/api/v1/darkweb/discussions/{id}` returns the same record on its own.

        Filters combine: a discussion is listed only when it matches every filter given. The summary
        (`/api/v1/darkweb/discussions/summary`) takes the same filters, with the same labels and the same 422s, and
        counts exactly what this list returns under them.
      parameters:
        - $ref: "#/components/parameters/PageSize"
        - $ref: "#/components/parameters/Cursor"
        - name: sort
          in: query
          required: false
          description: |
            One key to order the records by, ascending; prefix it with `-` for descending: `first_seen` (`published_on`, the discovery column `discovered_from` and `discovered_to` filter) or `id`.
            The record id breaks ties, in the same direction. Sorting is for ranking and top-N reads: a record whose
            `severity` or `last_seen` changes during a walk can be skipped, or listed twice. For a complete pull use no
            `sort`, `sort=id` or `sort=first_seen`, and de-duplicate by `id`. One key only: a second (`sort=a,b`)
            or an unknown key is refused with 422 naming the keys this list takes - the `sort_keys` of /api/v1/capabilities.
            Without `sort` the order above, unchanged.
            A record with no value for `first_seen` sorts before every value ascending and after every value descending.
            A `next_cursor` continues only the sort it was returned under: send it back with the same `sort` (or none, for
            an unsorted list); with another key, the other direction or no sort it is refused with 422.
          schema:
            type: string
            enum: [first_seen, "-first_seen", id, "-id"]
        - name: status
          in: query
          required: false
          description: |
            Only discussions under these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Investigating`. The labels are the summary's status labels. Several labels return the union of
            their tabs, each discussion once; a label given twice counts once. A discussion in no tab is never returned by
            a status filter. An unknown label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Legitimate Breach", "Dismissed", "To Be Closed", "Closed", "Filtered by AI"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
      responses:
        "200":
          description: One page of discussions.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DiscussionList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/darkweb/discussions/{id}:
    get:
      operationId: getDiscussion
      tags: [Dark Web]
      summary: Get one dark web discussion
      description: |
        Requires scope `dark-web:read`. The record the list returns for this `id`, with every field. An id that is not one
        of your listed discussions - another company's, one that does not exist, or one removed from your scope - answers
        404 `not_found`, the same response whichever it is; the dashboard still opens a discussion removed from your
        scope, the API does not. The endpoint takes no query parameter.
      parameters:
        - $ref: "#/components/parameters/RecordId"
      responses:
        "200":
          description: The discussion.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DiscussionDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/darkweb/discussions/summary:
    get:
      operationId: getDiscussionsSummary
      tags: [Dark Web]
      summary: Count dark web discussion findings by status tab, optionally by source
      description: |
        Requires scope `dark-web:read`. Status labels: Needs Review, Investigating, Legitimate Breach,
        Dismissed, To Be Closed, Closed, Filtered by AI. `source` groups by the discussion's stored
        source value (the forum or feed the scraper recorded, for example `xss` or `breached-forum`).
        It is an open dimension - every stored value is returned - but values are compared
        case-insensitively and returned in lower case, so a discussion stored `Xss` and one stored `xss`
        are one group labelled `xss`; a discussion with no source (`null`) is counted under `null`. `all` counts every discussion; one with a takedown
        request on this module, or one marked taken down, is in `all` but in no status tab, so
        `meta.status_partition` is false.

        Filters: the list's filters (`/api/v1/darkweb/discussions`) - `status`, `discovered_from` and `discovered_to` -
        apply here too, with the same labels and the same 422s, so the summary counts exactly the records the list
        returns under them. The discovery window narrows every count: each tab, `all` and `removed_from_scope`. A
        `status` filter limits the status groups to the tabs it names (in tab order, zeros included) and `all` to the
        findings in any of them, and `removed_from_scope` is then null: a descoped finding is in no tab. `group_by`
        groups the findings the filters leave.
      parameters:
        - $ref: "#/components/parameters/GroupBy"
        - name: status
          in: query
          required: false
          description: |
            Count only these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Investigating`. The status groups are then these tabs alone, in tab order, zeros included; `all`
            counts the findings in any of them, and `removed_from_scope` is null. The labels are the list's; an unknown
            label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Legitimate Breach", "Dismissed", "To Be Closed", "Closed", "Filtered by AI"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
      responses:
        "200": { $ref: "#/components/responses/Summary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/darkweb/compromised-users:
    get:
      operationId: listCompromisedUsers
      tags: [Dark Web]
      summary: List the compromised user credentials found for you, one page at a time
      description: |
        Requires scope `dark-web:read`. Exactly the credentials the summary's `all` counts (the dashboard's All tab; a
        credential removed from your scope is not listed), in ascending `id` order - walking every page returns each of
        them once - at most 500 per page. Records are typed JSON: booleans and objects as such, null when a value is
        unknown or empty, and timestamps in ISO-8601 with their offset (+05:30).

        One record is one credential found in a stealer log. `password` is returned in full and exactly as stored, as the
        dashboard's data and its export carry it (the dashboard's list masks it on screen): treat these records as secrets.
        `status` is the dashboard tab the credential is under, one of the summary's status labels; the stored status code
        itself is never returned, and `status` is null for a credential on a code no tab names. `compromise_type` is the
        account type as the dashboard labels it - Corporate Assets, Customers, Corporate Users, SaaS or Others (null for a
        code it has no label for) - and `is_executive` whether the credential is linked to one of your tracked executives
        (VIP). `domain`, `sub_domain` and `url` describe where the credential was used, as ShadowMap stores them; ShadowMap
        has no admin flag, so derive one from `url` or `sub_domain` if you need it. `custom_tags` are your own tags, key to
        values. `quality` and `is_valid_url` are the ingest's checks of the credential, null where it never checked.

        The record does not carry the computer's name: `hwid` is the hardware id of the computer the credential was stolen
        from - join it to `/api/v1/darkweb/compromised-computers` for the computer (null when no hardware id is stored).
        `timestamp` is a token the ingest stores as text; it is returned exactly as stored and the API never reads it as a
        time.

        `seen_on` is when the credential was seen in a stealer log - the module's discovery column. It is null for a few
        credentials, and no discovery window (`discovered_from` / `discovered_to`) returns a credential without one. The
        API reads `seen_on`, `created_at` and `updated_at` as +05:30, as the dashboard does. The stealer-log ingest stamps
        `created_at` and `updated_at` in UTC, so a value it wrote reads 5 hours 30 minutes early (ShadowMap's own changes
        are stamped at +05:30); the clock it stamps `seen_on` with is not verified, so a credential seen near midnight can
        fall in the neighbouring day's window. `web_url` opens the credential on your dashboard, and
        `/api/v1/darkweb/compromised-users/{id}` returns the same record on its own.

        Changes since your last pull: `changed_since` returns the credentials whose `updated_at` is at or after the moment
        you give, compared from 5 hours 30 minutes EARLIER - the allowance for the ingest's UTC clock - so, if you overlap
        your pulls as below, it never misses a change to a field the record carries, and it also returns ShadowMap's own
        changes from up to 5 hours 30 minutes before your moment. Every change to a field of this record moves
        `updated_at`: a status change or removal (an analyst's, a bulk action's, the automated mitigation's, ShadowMap's
        own credential checks and corrections), a dismissal, a custom-tag change, a
        corrected domain, and the ingest seeing the credential again - which moves it with no visible change, so a
        credential can come back unchanged. The record carries no field kept in another table whose changes would leave
        `updated_at` as it was: not the computer's name, which the computers ingest can add later without touching the
        credential (join to `/api/v1/darkweb/compromised-computers` by `hwid`). What `changed_since`
        cannot tell you: a credential that was deleted (ShadowMap deletes false positives outright) or that left your
        scope disappears from every list without a trace, and one restored to your scope comes back with its old
        `updated_at` - reconcile with a full pull, or by comparing the summary's `all`. Remove repeats by `id`, and start
        each pull from the moment your previous pull started, less a margin.

        Filters combine: a credential is listed only when it matches every filter given. The summary
        (`/api/v1/darkweb/compromised-users/summary`) takes the same filters, with the same labels and the same 422s,
        and counts exactly what this list returns under them.
      parameters:
        - $ref: "#/components/parameters/PageSize"
        - $ref: "#/components/parameters/Cursor"
        - name: sort
          in: query
          required: false
          description: |
            One key to order the records by, ascending; prefix it with `-` for descending: `first_seen` (`seen_on`, the discovery column `discovered_from` and `discovered_to` filter) or `id`.
            The record id breaks ties, in the same direction. Sorting is for ranking and top-N reads: a record whose
            `severity` or `last_seen` changes during a walk can be skipped, or listed twice. For a complete pull use no
            `sort`, `sort=id` or `sort=first_seen`, and de-duplicate by `id`. One key only: a second (`sort=a,b`)
            or an unknown key is refused with 422 naming the keys this list takes - the `sort_keys` of /api/v1/capabilities.
            Without `sort` the order above, unchanged.
            A record with no value for `first_seen` sorts before every value ascending and after every value descending.
            A `next_cursor` continues only the sort it was returned under: send it back with the same `sort` (or none, for
            an unsorted list); with another key, the other direction or no sort it is refused with 422.
          schema:
            type: string
            enum: [first_seen, "-first_seen", id, "-id"]
        - name: status
          in: query
          required: false
          description: |
            Only credentials under these dashboard tabs: one label, or several separated by commas, for example
            `Needs Action,Working Accounts`. The labels are the summary's status labels. Several labels return the union of
            their tabs, each credential once; a label given twice counts once. An unknown label or an empty entry is
            refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Action", "Working Accounts", "Valid Users", "Action Taken", "To Be Closed", "Dismissed", "Mitigated"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - $ref: "#/components/parameters/ChangedSince"
      responses:
        "200":
          description: One page of compromised user credentials.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CompromisedUserList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/darkweb/compromised-users/{id}:
    get:
      operationId: getCompromisedUser
      tags: [Dark Web]
      summary: Get one compromised user credential
      description: |
        Requires scope `dark-web:read`. The record the list returns for this `id`, with every field - the password in full.
        An id that is not one of your listed credentials - another company's, one that does not exist, or one removed from
        your scope - answers 404 `not_found`, the same response whichever it is; the dashboard still opens a credential
        removed from your scope, the API does not. The endpoint takes no query parameter.
      parameters:
        - $ref: "#/components/parameters/RecordId"
      responses:
        "200":
          description: The credential.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CompromisedUserDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/darkweb/compromised-users/summary:
    get:
      operationId: getCompromisedUsersSummary
      tags: [Dark Web]
      summary: Count compromised user credentials by status tab
      description: |
        Requires scope `dark-web:read`. Status labels: Needs Action, Working Accounts, Valid Users,
        Action Taken, To Be Closed, Dismissed, Mitigated. `status` is the only dimension. `all` counts
        every credential (the dashboard's All tab); a credential with an unrecognised state is in
        `all` but in no status tab. `response_status` is NOT NULL (default 0), so there is no unlisted
        NULL state. `removed_from_scope` counts the Removed From Scope tab.

        Filters: the list's filters (`/api/v1/darkweb/compromised-users`) - `status`, `discovered_from`, `discovered_to`
        and `changed_since` - apply here too, with the same labels and the same 422s, so the summary counts exactly the
        records the list returns under them. The discovery window and `changed_since` narrow every count: each tab, `all`
        and `removed_from_scope`. A `status` filter limits the status groups to the tabs it names (in tab order, zeros
        included) and `all` to the findings in any of them, and `removed_from_scope` is then null: a descoped finding is
        in no tab. `group_by` groups the findings the filters leave.
      parameters:
        - $ref: "#/components/parameters/GroupBy"
        - name: status
          in: query
          required: false
          description: |
            Count only these dashboard tabs: one label, or several separated by commas, for example
            `Needs Action,Working Accounts`. The status groups are then these tabs alone, in tab order, zeros included;
            `all` counts the findings in any of them, and `removed_from_scope` is null. The labels are the list's; an
            unknown label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Action", "Working Accounts", "Valid Users", "Action Taken", "To Be Closed", "Dismissed", "Mitigated"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - $ref: "#/components/parameters/ChangedSince"
      responses:
        "200": { $ref: "#/components/responses/Summary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/darkweb/compromised-computers:
    get:
      operationId: listCompromisedComputers
      tags: [Dark Web]
      summary: List the computers stealer malware took your credentials from, one page at a time
      description: |
        Requires scope `dark-web:read`. Exactly the computers the summary's `all` counts, in ascending `id` order - walking
        every page returns each of them once - at most 200 per page. As on the dashboard, a computer with no name, user,
        visited domains or IP address is left out, and so is one removed from your scope. Records are typed JSON: booleans,
        arrays and objects as such, null when a value is unknown or empty, and timestamps in ISO-8601 with their offset
        (+05:30).

        One record is one infected computer as a stealer log describes it. `status` is the dashboard tab the computer is
        under, one of the summary's status labels; the stored status code itself is never returned, and `status` is null
        for a computer on a code no tab names. `is_executive` is whether the computer is linked to one of your tracked
        executives (VIP). The stealer logs write placeholders for what they do not know, and the record returns null for
        each, never the placeholder: `-1` in any identification field (`hwid`, `computer`, `hostname`, `user`, `domain`,
        `os`, `malware_group`, `malware_path`), `zz` as the country and `0.0.0.0` as the IP address - and an empty value
        anywhere. `custom_tags` are your own tags, key to values.

        `subdomains` lists every sub-domain of any monitored organisation's credentials found on this machine — not only
        your own; your own credentials for the machine are in Compromised Users (join by hwid). The record carries neither
        the computer's credentials nor its password count: read your own credentials from
        `/api/v1/darkweb/compromised-users`, whose credentials name the computer by the same `hwid`. `timestamp` is a token
        the ingest stores as text; it is returned exactly as stored and the API never reads it as a time.

        `breach_date` is when the stealer log was captured - the module's discovery column, always set. The API reads
        `breach_date`, `created_at` and `updated_at` as +05:30, as the dashboard does. The stealer-log ingest stamps
        `created_at` and `updated_at` in UTC, so a value it wrote reads 5 hours 30 minutes early (ShadowMap's own changes
        are stamped at +05:30); the clock it stamps `breach_date` with is not verified, so a computer captured near
        midnight can fall in the neighbouring day's window. `web_url` opens the computer on your dashboard, and
        `/api/v1/darkweb/compromised-computers/{id}` returns the same record on its own.

        Changes since your last pull: `changed_since` returns the computers whose `updated_at` is at or after the moment
        you give, compared from 5 hours 30 minutes EARLIER - the allowance for the ingest's UTC clock - so, if you overlap
        your pulls as below, it never misses a change to a field the record carries, and it also returns ShadowMap's own
        changes from up to 5 hours 30 minutes before your moment. Every change to a field of this record moves
        `updated_at`: a status change or removal, a dismissal, any change to your custom tags (they are kept in their own
        table, and every tag change moves the computer's `updated_at`), and the ingest
        seeing the computer again - which refreshes its sub-domains and hardware details and moves it even when nothing
        visible changed, so a computer can come back unchanged. A comment is not a field of the record. That is why the
        record carries no credential count and no credentials: those are Compromised Users records, which change without
        touching the computer - follow them with that endpoint's own `changed_since`. What `changed_since` cannot tell you:
        a computer that left your scope (or was deleted) disappears from every list without a trace, and one restored to
        your scope comes back with its old `updated_at` - reconcile with a full pull, or by comparing the summary's `all`.
        Remove repeats by `id`, and start each pull from the moment your previous pull started, less a margin.

        Filters combine: a computer is listed only when it matches every filter given. The summary
        (`/api/v1/darkweb/compromised-computers/summary`) takes the same filters, with the same labels and the same
        422s, and counts exactly what this list returns under them.
      parameters:
        - name: page_size
          in: query
          required: false
          description: Rows per page, 1 to 200 (this module's maximum, as in `/api/v1/capabilities`). Default 100.
          schema: { type: integer, minimum: 1, maximum: 200 }
        - $ref: "#/components/parameters/Cursor"
        - name: sort
          in: query
          required: false
          description: |
            One key to order the records by, ascending; prefix it with `-` for descending: `first_seen` (`breach_date`, the discovery column `discovered_from` and `discovered_to` filter) or `id`.
            The record id breaks ties, in the same direction. Sorting is for ranking and top-N reads: a record whose
            `severity` or `last_seen` changes during a walk can be skipped, or listed twice. For a complete pull use no
            `sort`, `sort=id` or `sort=first_seen`, and de-duplicate by `id`. One key only: a second (`sort=a,b`)
            or an unknown key is refused with 422 naming the keys this list takes - the `sort_keys` of /api/v1/capabilities.
            Without `sort` the order above, unchanged.
            A `next_cursor` continues only the sort it was returned under: send it back with the same `sort` (or none, for
            an unsorted list); with another key, the other direction or no sort it is refused with 422.
          schema:
            type: string
            enum: [first_seen, "-first_seen", id, "-id"]
        - name: status
          in: query
          required: false
          description: |
            Only computers under these dashboard tabs: one label, or several separated by commas, for example
            `Needs Action,Investigating`. The labels are the summary's status labels. Several labels return the union of
            their tabs, each computer once; a label given twice counts once. An unknown label or an empty entry is refused
            with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Action", "Investigating", "Action Taken", "To Be Closed", "Dismissed"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - $ref: "#/components/parameters/ChangedSince"
      responses:
        "200":
          description: One page of compromised computers.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CompromisedComputerList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/darkweb/compromised-computers/{id}:
    get:
      operationId: getCompromisedComputer
      tags: [Dark Web]
      summary: Get one compromised computer
      description: |
        Requires scope `dark-web:read`. The record the list returns for this `id`, with every field. An id that is not one
        of your listed computers - another company's, one that does not exist, an empty one or one removed from your scope -
        answers 404 `not_found`, the same response whichever it is; the dashboard still opens a computer removed from your
        scope, the API does not. The record does not carry the computer's credentials: read them from
        `/api/v1/darkweb/compromised-users` by `hwid`. The endpoint takes no query parameter.
      parameters:
        - $ref: "#/components/parameters/RecordId"
      responses:
        "200":
          description: The compromised computer.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CompromisedComputerDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/darkweb/compromised-computers/summary:
    get:
      operationId: getCompromisedComputersSummary
      tags: [Dark Web]
      summary: Count compromised computers by status tab
      description: |
        Requires scope `dark-web:read`. Status labels: Needs Action, Investigating, Action Taken,
        To Be Closed, Dismissed. `status` is the only dimension. A computer with no name, user,
        visited domains or IP address is left out, as on the dashboard. A computer with an
        unrecognised state is in `all` but in no status tab: the five tabs are the module's designed
        status partition, so `meta.status_partition` is true — the counts are designed to cover `all`
        exactly, not guaranteed to, and a computer left on a code no tab names (a legacy or foreign
        code) is counted in `all` and in no tab. `response_status` is NOT NULL (default 0), so there
        is no unlisted NULL state. `removed_from_scope` counts the Removed From Scope tab.

        Filters: the list's filters (`/api/v1/darkweb/compromised-computers`) - `status`, `discovered_from`,
        `discovered_to` and `changed_since` - apply here too, with the same labels and the same 422s, so the summary counts
        exactly the records the list returns under them. The discovery window and `changed_since` narrow every count: each
        tab, `all` and `removed_from_scope`. A `status` filter limits the status groups to the tabs it names (in tab order, zeros
        included) and `all` to the findings in any of them, and `removed_from_scope` is then null: a descoped finding is
        in no tab. `group_by` groups the findings the filters leave.
      parameters:
        - $ref: "#/components/parameters/GroupBy"
        - name: status
          in: query
          required: false
          description: |
            Count only these dashboard tabs: one label, or several separated by commas, for example
            `Needs Action,Investigating`. The status groups are then these tabs alone, in tab order, zeros included; `all`
            counts the findings in any of them, and `removed_from_scope` is null. The labels are the list's; an unknown
            label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Action", "Investigating", "Action Taken", "To Be Closed", "Dismissed"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - $ref: "#/components/parameters/ChangedSince"
      responses:
        "200": { $ref: "#/components/responses/Summary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/darkweb/telegram:
    get:
      operationId: listTelegramMessages
      tags: [Dark Web]
      summary: List the Telegram messages found for you, one page at a time
      description: |
        Requires scope `dark-web:read`. Exactly the messages the summary's `all` counts (a message removed from your scope
        is not listed), in ascending `id` order - walking every page returns each of them once - at most 500 per page.
        Records are typed JSON: arrays as such, null when a value is unknown or empty, and timestamps in ISO-8601 with
        their offset (+05:30).

        `status` is the dashboard tab the message is under, one of the summary's status labels; the stored status code
        itself is never returned. `status` is null for a message on a code no tab names. `channel_name` is the channel's
        Telegram handle and `channel_url` its address on Telegram (null when no handle is stored). `keywords` are the
        terms the message matched, each once. `message` and `cleaned_message` are the message text as stored, and
        `content` the message as ShadowMap stored it from Telegram, all three returned whole: they can quote leaked
        material.

        `messaged_at` is when the message was posted - the module's discovery column. It is null for a few messages, and
        no discovery window (`discovered_from` / `discovered_to`) returns a message without one: walk the list without a
        window to see them. `ingested_on` is when ShadowMap collected the message. `updated_at` is the record's
        last-updated time as stored; not every change to a message moves it, so do not use it to detect changes.
        `web_url` opens the message on your dashboard, and `/api/v1/darkweb/telegram/{id}` returns the same record on its
        own.

        Filters combine: a message is listed only when it matches every filter given. The summary
        (`/api/v1/darkweb/telegram/summary`) takes the same filters, with the same labels and the same 422s, and counts
        exactly what this list returns under them.
      parameters:
        - $ref: "#/components/parameters/PageSize"
        - $ref: "#/components/parameters/Cursor"
        - name: sort
          in: query
          required: false
          description: |
            One key to order the records by, ascending; prefix it with `-` for descending: `first_seen` (`messaged_at`, the discovery column `discovered_from` and `discovered_to` filter) or `id`.
            The record id breaks ties, in the same direction. Sorting is for ranking and top-N reads: a record whose
            `severity` or `last_seen` changes during a walk can be skipped, or listed twice. For a complete pull use no
            `sort`, `sort=id` or `sort=first_seen`, and de-duplicate by `id`. One key only: a second (`sort=a,b`)
            or an unknown key is refused with 422 naming the keys this list takes - the `sort_keys` of /api/v1/capabilities.
            Without `sort` the order above, unchanged.
            A record with no value for `first_seen` sorts before every value ascending and after every value descending.
            A `next_cursor` continues only the sort it was returned under: send it back with the same `sort` (or none, for
            an unsorted list); with another key, the other direction or no sort it is refused with 422.
          schema:
            type: string
            enum: [first_seen, "-first_seen", id, "-id"]
        - name: status
          in: query
          required: false
          description: |
            Only messages under these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Investigating`. The labels are the summary's status labels. Several labels return the union of
            their tabs, each message once; a label given twice counts once. An unknown label or an empty entry is refused
            with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Legitimate Breach", "Dismissed", "To Be Closed", "Closed", "Filtered by AI"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
      responses:
        "200":
          description: One page of Telegram messages.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TelegramMessageList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/darkweb/telegram/{id}:
    get:
      operationId: getTelegramMessage
      tags: [Dark Web]
      summary: Get one Telegram message
      description: |
        Requires scope `dark-web:read`. The record the list returns for this `id`, with every field, read by its `id`
        within your company. An id that is not one of your listed messages - another company's, one that does not exist,
        or one removed from your scope - answers 404 `not_found`, the same response whichever it is; the dashboard still
        opens a message removed from your scope, the API does not. The endpoint takes no query parameter.
      parameters:
        - $ref: "#/components/parameters/RecordId"
      responses:
        "200":
          description: The Telegram message.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TelegramMessageDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/darkweb/telegram/summary:
    get:
      operationId: getTelegramSummary
      tags: [Dark Web]
      summary: Count Telegram message findings by status tab
      description: |
        Requires scope `dark-web:read`. Status labels: Needs Review, Investigating, Legitimate Breach,
        Dismissed, To Be Closed, Closed, Filtered by AI. `status` is the only dimension. A message
        with an unrecognised state is in `all` but in no status tab: the seven tabs are the module's
        designed status partition, so `meta.status_partition` is true — the counts are designed to
        cover `all` exactly, not guaranteed to, and a message left on a code no tab names (for example
        a legacy code 9) is counted in `all` and in no tab. `response_status` is NOT NULL (default 0),
        so there is no unlisted NULL state. `removed_from_scope` counts the Removed From Scope tab.

        Filters: the list's filters (`/api/v1/darkweb/telegram`) - `status`, `discovered_from` and `discovered_to` -
        apply here too, with the same labels and the same 422s, so the summary counts exactly the records the list
        returns under them. The discovery window narrows every count: each tab, `all` and `removed_from_scope`. A
        `status` filter limits the status groups to the tabs it names (in tab order, zeros included) and `all` to the
        findings in any of them, and `removed_from_scope` is then null: a descoped finding is in no tab. `group_by`
        groups the findings the filters leave.
      parameters:
        - $ref: "#/components/parameters/GroupBy"
        - name: status
          in: query
          required: false
          description: |
            Count only these dashboard tabs: one label, or several separated by commas, for example
            `Needs Review,Investigating`. The status groups are then these tabs alone, in tab order, zeros included; `all`
            counts the findings in any of them, and `removed_from_scope` is null. The labels are the list's; an unknown
            label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Needs Review", "Investigating", "Legitimate Breach", "Dismissed", "To Be Closed", "Closed", "Filtered by AI"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
      responses:
        "200": { $ref: "#/components/responses/Summary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/sla/policies:
    get:
      operationId: listSlaPolicies
      tags: [SLA]
      summary: List your SLA policies, one page at a time
      description: |
        Requires scope `sla:read`. Every SLA policy of your company, whoever created it, in ascending `id` order - walking
        every page returns each of them once - at most 500 per page. Records are typed JSON: booleans and arrays as such,
        null when a value is unknown or empty, and timestamps in ISO-8601 with their offset (+05:30).

        `status` is the policy's state as the SLA Policies page labels it, Active or Inactive - one of the summary's status
        labels; it is null for a policy on a stored status neither label names. ShadowMap's SLA sweep evaluates Active
        policies only: an Inactive policy raises no violation, and its open violations are neither escalated nor closed by
        the sweep. `escalation_enabled` is false for a policy that does not escalate: its `escalations` are then inert
        (they can still be listed), and every violation it raises is created already Closed.

        `module_types` lists each module the policy covers, once, in `type` order: the SLA module type number (the
        `module_type` of the policy's violations), its name, and `evaluated` - false for every type the sweep does not
        evaluate, for which the policy raises no violation (for example IP Reputation and SSL Certificates, which can be
        chosen in the dashboard, or a type ShadowMap has no name for, labelled Unknown). `escalations` are the policy's
        levels in `level` order (1 to 4; a level can be missing): a level is due `offset` days, or months (`unit`), after
        the start of the day the violation was raised (IST), and the sweep escalates an open violation on or after that
        day, one level at a time. A month offset is counted as the sweep counts it: a day the target month lacks rolls
        over into the next month, so one month after 31 January is 3 March (2 March in a leap year) - up to 3 days past
        the end of the shorter month. A violation record does not carry its due dates: compute them from its
        `created_at` and these levels.

        Not in the record: the policy's recipients and integrations, its creator, its matching criteria, and an
        updated time (editing a level does not move the policy's own). No field of the record tells you a policy was
        edited, so to follow your policies pull this list whole. `web_url` opens the policy on your dashboard, and
        `/api/v1/sla/policies/{id}` returns the same record on its own.

        Filters combine: a policy is listed only when it matches every filter given. The summary
        (`/api/v1/sla/policies/summary`) takes the same filters, with the same labels and the same 422s, and counts exactly
        what this list returns under them.
      parameters:
        - $ref: "#/components/parameters/PageSize"
        - $ref: "#/components/parameters/Cursor"
        - name: sort
          in: query
          required: false
          description: |
            One key to order the records by, ascending; prefix it with `-` for descending: `first_seen` (`created_at`, the discovery column `discovered_from` and `discovered_to` filter) or `id`.
            The record id breaks ties, in the same direction. Sorting is for ranking and top-N reads: a record whose
            `severity` or `last_seen` changes during a walk can be skipped, or listed twice. For a complete pull use no
            `sort`, `sort=id` or `sort=first_seen`, and de-duplicate by `id`. One key only: a second (`sort=a,b`)
            or an unknown key is refused with 422 naming the keys this list takes - the `sort_keys` of /api/v1/capabilities.
            Without `sort` the order above, unchanged.
            A record with no value for `first_seen` sorts before every value ascending and after every value descending.
            A `next_cursor` continues only the sort it was returned under: send it back with the same `sort` (or none, for
            an unsorted list); with another key, the other direction or no sort it is refused with 422.
          schema:
            type: string
            enum: [first_seen, "-first_seen", id, "-id"]
        - name: status
          in: query
          required: false
          description: |
            Only policies under these status labels: one label, or both separated by a comma, `Active,Inactive`. The labels
            are the summary's status labels. Several labels return the policies under any of them, each once; a label given
            twice counts once. An unknown label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Active", "Inactive"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
      responses:
        "200":
          description: One page of SLA policies.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SlaPolicyList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/sla/policies/{id}:
    get:
      operationId: getSlaPolicy
      tags: [SLA]
      summary: Get one SLA policy
      description: |
        Requires scope `sla:read`. The record the list returns for this `id`, with every field, read by its `id` within
        your company. An id that is not one of your policies - another company's, or one that does not exist - answers 404
        `not_found`, the same response whichever it is. The endpoint takes no query parameter.
      parameters:
        - $ref: "#/components/parameters/RecordId"
      responses:
        "200":
          description: The SLA policy.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SlaPolicyDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/sla/policies/summary:
    get:
      operationId: getSlaPoliciesSummary
      tags: [SLA]
      summary: Count SLA policies by status
      description: |
        Requires scope `sla:read`. Status labels: Active, Inactive. `status` is the only dimension. The two labels are the
        module's designed status partition, so `meta.status_partition` is true: a policy left on another stored status is
        counted in `all` and in no status group. `removed_from_scope` is always null - a policy is never removed from
        your scope.

        Filters: the list's filters (`/api/v1/sla/policies`) - `status`, `discovered_from` and `discovered_to` - apply
        here too, with the same labels and the same 422s, so the summary counts exactly the records the list returns under
        them. The discovery window (on `created_at`) narrows every count. A `status` filter limits the status groups to the
        labels it names (in label order, zeros included) and `all` to the policies under any of them. `group_by` groups the
        policies the filters leave.
      parameters:
        - $ref: "#/components/parameters/GroupBy"
        - name: status
          in: query
          required: false
          description: |
            Count only these status labels: one label, or both separated by a comma. The status groups are then these labels
            alone, in label order, zeros included, and `all` counts the policies under any of them. The labels are the
            list's; an unknown label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Active", "Inactive"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
      responses:
        "200": { $ref: "#/components/responses/Summary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/sla/violations:
    get:
      operationId: listSlaViolations
      tags: [SLA]
      summary: List your SLA violations, one page at a time
      description: |
        Requires scope `sla:read`. Every SLA violation of your company in ascending `id` order - walking every page returns
        each of them once - at most 500 per page. ShadowMap's SLA sweep raises a violation when it first finds a finding
        that matches one of your Active policies, and closes it when the finding is settled or an analyst closes it; a
        finding that opens again gets a new violation. Records are typed JSON: arrays as such, null when a value is
        unknown, and timestamps in ISO-8601 with their offset (+05:30).

        `status` is the dashboard tab the violation is under, one of the summary's status labels: Open (open and never
        escalated), Escalated (open and escalated at least once - ShadowMap stores no such status; it is the dashboard's
        tab) or Closed. A policy that does not escalate creates its violations already Closed, so Closed does not always
        mean resolved. `status` is null for a violation on a stored status no tab names.

        `module_type` and `module` name the module of the violated finding - its SLA type number and its name (Unknown for
        a type ShadowMap has no name for) - and `finding_id` is that finding's id in its module, so it joins to the
        module's records: Exposures (1) to `/api/v1/attack-surface/web-applications`, Alerts (2) to
        `/api/v1/threats/alerts`, Ransomware Group & Forum (5) to `/api/v1/darkweb/discussions`, Data Breaches (6) to
        `/api/v1/darkweb/data-breaches`, Docker Container (7) to `/api/v1/data-leaks/docker-containers`, Leaked APIs (12)
        to `/api/v1/data-leaks/leaked-apis`, Leaked File (13) to `/api/v1/data-leaks/leaked-files`, Code Repositories (14)
        to `/api/v1/data-leaks/code-repositories`, S3 Bucket (17) to `/api/v1/data-leaks/s3-buckets`, Telegram
        Conversations (20) to `/api/v1/darkweb/telegram`, Malware Compromised Users (24) to
        `/api/v1/darkweb/compromised-users`, Malware Compromised Computers (25) to `/api/v1/darkweb/compromised-computers`
        and Leaked Credentials (31) to `/api/v1/data-leaks/leaked-credentials`. Where a module lists one record for several
        stored copies (Web Applications' representative copy, Data Breaches' kept copy of a credential), the finding can be
        listed there under another id, and its own id then answers 404. Mobile Application (15) and IP Reputation (19) have
        record endpoints too, but the sweep does not evaluate them, so it raises no violation on them; the other types
        have no endpoint in this version.

        `policy_id` is the policy that raised the violation (`/api/v1/sla/policies`). `created_at` is the clock start: when
        the sweep first matched the finding, not when the finding was discovered - and the module's discovery column.
        `escalations` is the violation's escalation history: each level it reached, once, in level order, with the time it
        was first reached (`escalated_at`; null when no time was recorded); `escalation_level` is the highest level
        reached, null for a violation never escalated. `updated_at` is when the violation last changed - the column
        `changed_since` follows. It is set when the sweep raises the violation and moves when it is closed (by the sweep
        when the finding is settled, by an analyst, or by ShadowMap's clean-up of violations whose findings were settled; a
        policy that does not escalate raises its violations already closed), each time it is escalated to a level, and
        when it is re-pointed to another finding (`finding_id`: ShadowMap's one-time re-mapping of Leaked Credentials
        ids). Nothing else moves it - and a policy edit moves nothing on its violations, which is why the record carries
        no field read from the policy. An escalation recorded before this change feed was released did not move it, so
        begin with one full pull.

        Changes since your last pull: `changed_since` returns the violations whose `updated_at` is at or after the moment
        you give, compared from that moment itself - only ShadowMap writes these records, on one clock. It cannot tell you
        that a violation was deleted: ShadowMap deletes violations outright with their policy, with a module type removed
        from a policy, and with their finding when that finding is deleted (several scanners delete a finding they
        retire), among other clean-ups - reconcile with a full pull, or by comparing the summary's `all`. Remove repeats
        by `id`, and start each pull from the moment your previous pull started, less a margin.

        Not in the record: `policy_name` and the due date of each escalation level. Both come from the policy, and a policy
        edit changes them without moving the violation, so read them from `/api/v1/sla/policies` by `policy_id` (pull that
        list whole): a level is due `offset` days, or months (counted as the policies list describes), after the start of
        the day the violation was raised (IST). The number of days a violation has been open is not a field either:
        compute it from `created_at`.
        `web_url` opens the dashboard tab the violation is listed under (the dashboard has no page for one violation), and
        `/api/v1/sla/violations/{id}` returns the same record on its own.

        Filters combine: a violation is listed only when it matches every filter given. The `module` filter takes the
        labels the summary groups by, so the violations listed under a label are exactly the ones the summary counts under
        it. The summary (`/api/v1/sla/violations/summary`) takes the same filters, with the same labels and the same 422s,
        and counts exactly what this list returns under them.
      parameters:
        - $ref: "#/components/parameters/PageSize"
        - $ref: "#/components/parameters/Cursor"
        - name: sort
          in: query
          required: false
          description: |
            One key to order the records by, ascending; prefix it with `-` for descending: `first_seen` (`created_at`, the discovery column `discovered_from` and `discovered_to` filter) or `id`.
            The record id breaks ties, in the same direction. Sorting is for ranking and top-N reads: a record whose
            `severity` or `last_seen` changes during a walk can be skipped, or listed twice. For a complete pull use no
            `sort`, `sort=id` or `sort=first_seen`, and de-duplicate by `id`. One key only: a second (`sort=a,b`)
            or an unknown key is refused with 422 naming the keys this list takes - the `sort_keys` of /api/v1/capabilities.
            Without `sort` the order above, unchanged.
            A record with no value for `first_seen` sorts before every value ascending and after every value descending.
            A `next_cursor` continues only the sort it was returned under: send it back with the same `sort` (or none, for
            an unsorted list); with another key, the other direction or no sort it is refused with 422.
          schema:
            type: string
            enum: [first_seen, "-first_seen", id, "-id"]
        - name: status
          in: query
          required: false
          description: |
            Only violations under these dashboard tabs: one label, or several separated by commas, for example
            `Open,Escalated` (every open violation). The labels are the summary's status labels. Several labels return the
            union of their tabs, each violation once - Closed is kept whatever else is given; a label given twice counts
            once. An unknown label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Open", "Escalated", "Closed"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - $ref: "#/components/parameters/ChangedSince"
        - name: module
          in: query
          required: false
          description: |
            Only violations of findings in these modules: one label, or several separated by commas, for example
            `Alerts,Exposures`. The labels are the summary's `module` groups - the record's `module`, Unknown included.
            Several labels return the violations of any of them, each once; a label given twice counts once. An unknown
            label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Exposures", "Alerts", "Stealer Log", "Ransomware Group & Forum", "Docker Container", "Domain Squatting", "Phishing & Impersonations", "Leaked APIs", "Leaked File", "Code Repositories", "Mobile Application", "Social Media Monitoring", "S3 Bucket", "Open Ports", "IP Reputation", "Telegram Conversations", "Software As A Service", "Credit Card Leaks", "SSL Certificates", "Malware Compromised Users", "Malware Compromised Computers", "Data Breaches", "Elastic Search Instances", "Executive Leak", "AI Domains", "Vendor Risk Management", "Subdomains", "Leaked Credentials", "Exposed Database", "Executive Monitoring", "Tracker", "Internal Host", "IP Address", "Shortener URL", "Stack Overflow", "Campaign", "CVE", "IOC Search", "Malware", "Ransomware", "Threat Actor", "Threat Feed", "App Misconfiguration", "Defacement", "Network Service", "Technology Stack", "SSO Login Page", "Malware Compromised Cookies", "Malware Compromised Autofills", "Malware Compromised Cards", "Malware Compromised Wallets", "Malware Compromised Tokens", "Malware Compromised Browser History", "Unknown"]
      responses:
        "200":
          description: One page of SLA violations.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SlaViolationList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/sla/violations/{id}:
    get:
      operationId: getSlaViolation
      tags: [SLA]
      summary: Get one SLA violation
      description: |
        Requires scope `sla:read`. The record the list returns for this `id`, with every field, read by its `id` within
        your company. An id that is not one of your violations - another company's, or one that does not exist (a
        violation is deleted with its policy, its module type or its finding) - answers 404 `not_found`, the same response
        whichever it is. The endpoint takes no query parameter.
      parameters:
        - $ref: "#/components/parameters/RecordId"
      responses:
        "200":
          description: The SLA violation.
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SlaViolationDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /api/v1/sla/violations/summary:
    get:
      operationId: getSlaViolationsSummary
      tags: [SLA]
      summary: Count SLA violations by status tab, optionally by module
      description: |
        Requires scope `sla:read`. Status labels: Open, Escalated, Closed - the dashboard's tabs (its All tab is `all`).
        Escalated is an open violation escalated at least once, so Open and Escalated together are every open violation.
        The three tabs are the module's designed status partition, so `meta.status_partition` is true: a violation left
        on another stored status is counted in `all` and in no tab. `group_by=module` counts by the violated finding's
        module, the record's `module` (Unknown included). `removed_from_scope` is always null - a violation is never
        removed from your scope.

        Filters: the list's filters (`/api/v1/sla/violations`) - `status`, `discovered_from`, `discovered_to`,
        `changed_since` and `module` - apply here too, with the same labels and the same 422s, so the summary counts exactly
        the records the list returns under them. The discovery window (on `created_at`), `changed_since` (on `updated_at`)
        and the `module` filter narrow every count. A `status`
        filter limits the status groups to the tabs it names (in tab order, zeros included) and `all` to the violations in
        any of them. `group_by` groups the violations the filters leave.
      parameters:
        - $ref: "#/components/parameters/GroupBy"
        - name: status
          in: query
          required: false
          description: |
            Count only these dashboard tabs: one label, or several separated by commas, for example `Open,Escalated`. The
            status groups are then these tabs alone, in tab order, zeros included, and `all` counts the violations in any
            of them. The labels are the list's; an unknown label or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Open", "Escalated", "Closed"]
        - $ref: "#/components/parameters/DiscoveredFrom"
        - $ref: "#/components/parameters/DiscoveredTo"
        - $ref: "#/components/parameters/ChangedSince"
        - name: module
          in: query
          required: false
          description: |
            Count only violations under these `module` labels: one label, or several separated by commas, for example
            `Alerts,Exposures`. They are the labels `group_by=module` reports - the list's `module` filter; an unknown label
            or an empty entry is refused with 422.
          style: form
          explode: false
          schema:
            type: array
            items:
              type: string
              enum: ["Exposures", "Alerts", "Stealer Log", "Ransomware Group & Forum", "Docker Container", "Domain Squatting", "Phishing & Impersonations", "Leaked APIs", "Leaked File", "Code Repositories", "Mobile Application", "Social Media Monitoring", "S3 Bucket", "Open Ports", "IP Reputation", "Telegram Conversations", "Software As A Service", "Credit Card Leaks", "SSL Certificates", "Malware Compromised Users", "Malware Compromised Computers", "Data Breaches", "Elastic Search Instances", "Executive Leak", "AI Domains", "Vendor Risk Management", "Subdomains", "Leaked Credentials", "Exposed Database", "Executive Monitoring", "Tracker", "Internal Host", "IP Address", "Shortener URL", "Stack Overflow", "Campaign", "CVE", "IOC Search", "Malware", "Ransomware", "Threat Actor", "Threat Feed", "App Misconfiguration", "Defacement", "Network Service", "Technology Stack", "SSO Login Page", "Malware Compromised Cookies", "Malware Compromised Autofills", "Malware Compromised Cards", "Malware Compromised Wallets", "Malware Compromised Tokens", "Malware Compromised Browser History", "Unknown"]
      responses:
        "200": { $ref: "#/components/responses/Summary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/Unavailable" }
components:
  securitySchemes:
    bearerToken:
      type: http
      scheme: bearer
      description: A ShadowMap API token (`<id>|smk_…`), sent over HTTPS. A client certificate is needed only for a token issued with one, and an IP allowlist applies only to a token that has one.
  parameters:
    GroupBy:
      name: group_by
      in: query
      required: false
      description: |
        Up to three comma-separated dimensions from the module's `group_by` list in
        `/api/v1/capabilities`, for example `status,severity` or `custom_tag:false_positive`. The order
        you give is the order of the keys and of the sort. Omit it to get one count per status tab
        (every tab - or every tab a `status` filter names - zeros included). With other dimensions,
        only non-zero combinations are returned. A finding with several values for one custom tag is
        counted under each value. The module's filters narrow the findings first: the groups count
        only the findings they leave.

        `host` (Alerts and Web Applications only) is a ranking, given on its own: the `top` hosts with the most
        findings the filters leave, by count descending and then host ascending, each with its `count` and its
        `by_severity` counts. A host can be a domain or an IP address; only findings with no stored host (e.g. leaked
        credentials) are not ranked. For example
        `group_by=host&top=10&status=Needs%20Review,Investigating,Accepted%20Risk` is the ten hosts with the most open
        findings.
      schema: { type: string }
    Top:
      name: top
      in: query
      required: false
      description: |
        With `group_by=host`: how many hosts to return, 1 to 100. Default 10. Given with any other `group_by`, or
        without one, it is refused with 422.
      schema: { type: integer, minimum: 1, maximum: 100, default: 10 }
    PageSize:
      name: page_size
      in: query
      required: false
      description: Rows per page, 1 to the module's maximum (in `/api/v1/capabilities`). Default 100.
      schema: { type: integer, minimum: 1, maximum: 500 }
    Cursor:
      name: cursor
      in: query
      required: false
      description: |
        The `next_cursor` of the previous page, unchanged, sent with the same `sort` as that page (or none). Omit it for
        the first page. A cursor is refused with 422 under any other sort, the other direction, or no sort when it came
        from a sorted page; a cursor from an unsorted page is accepted only without `sort`.
      schema: { type: string }
    RecordId:
      name: id
      in: path
      required: true
      description: The record's `id`, as the list returns it - a positive integer without leading zeros.
      schema: { type: integer, minimum: 1 }
    DiscoveredFrom:
      name: discovered_from
      in: query
      required: false
      description: |
        Only records discovered at or after this moment (the module's `discovery_column`). A date
        (`2026-09-01`) means the start of that day at +05:30. A date-time (`2026-09-01T09:30:00Z`) is
        ISO-8601; without an offset it is read as +05:30. A record whose discovery time is unknown (none
        stored, or the zero date some older records hold) is in no window.
      schema: { type: string, example: "2026-09-01" }
    DiscoveredTo:
      name: discovered_to
      in: query
      required: false
      description: |
        Only records discovered at or before this moment. A date (`2026-09-30`) covers the WHOLE day at
        +05:30, up to 23:59:59. A date-time is ISO-8601; without an offset it is read as +05:30. A window that
        ends before it starts is refused with 422. A record whose discovery time is unknown (none stored, or
        the zero date some older records hold) is in no window, even one given only `discovered_to`.
      schema: { type: string, example: "2026-09-30" }
    ChangedSince:
      name: changed_since
      in: query
      required: false
      description: |
        Only records changed at or after this moment - the record's `updated_at` - less the module's allowance for a
        writer whose clock runs behind ShadowMap's: 5 hours 30 minutes on Compromised Users and Compromised Computers,
        none on SLA violations. A date (`2026-10-01`) means the start of that day at +05:30; a date-time
        (`2026-10-01T09:30:00Z`) is ISO-8601, and without an offset it is read as +05:30 (send `+` as `%2B`). Offered only
        where `/api/v1/capabilities` says `changed_since: true` - Compromised Users, Compromised Computers and SLA
        violations; every other endpoint refuses it with 422. It narrows the records, never their order, and it combines
        with every other filter. It over-reports, and if you overlap your pulls (below) it never misses a change to a field
        the record carries: a record changed shortly before the moment is returned too, and a record changed again is
        returned again - remove repeats by `id`. It never returns a deleted record and
        never tells you a record left your scope: reconcile those with a full pull or the summary's `all`. A record whose
        change time is unknown (no `updated_at` stored, or the zero date some older records hold; the record shows it as
        null) is in no pull until it changes. Overlap your pulls: start each from the moment your previous pull STARTED,
        less a margin (we suggest an hour), because a change is stamped when it is written, which can be before it is
        visible. Begin with one full pull: before this change feed was released, some changes did not move `updated_at`.
      schema: { type: string, example: "2026-10-01T09:30:00+05:30" }
  headers:
    RequestId:
      description: Unique id of this request (a ULID). Quote it to support.
      schema: { type: string, pattern: "^[0-9A-HJKMNP-TV-Z]{26}$" }
    RateLimitLimit:
      description: Requests allowed per minute for this token.
      schema: { type: string, pattern: "^[0-9]+$" }
    RateLimitRemaining:
      description: Requests left in the current minute.
      schema: { type: string, pattern: "^[0-9]+$" }
    RetryAfter:
      description: Seconds to wait before retrying.
      schema: { type: string, pattern: "^[0-9]+$" }
  responses:
    Unauthorized:
      description: "token_invalid, token_expired, token_revoked or certificate_mismatch."
      headers: { X-Request-Id: { $ref: "#/components/headers/RequestId" } }
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Forbidden:
      description: "origin_not_verified, token_not_permitted, account_disabled, ip_not_allowed or scope_missing."
      headers: { X-Request-Id: { $ref: "#/components/headers/RequestId" } }
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    NotFound:
      description: not_found.
      headers: { X-Request-Id: { $ref: "#/components/headers/RequestId" } }
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    TooManyRequests:
      description: "rate_limited or concurrency_limited."
      headers:
        X-Request-Id: { $ref: "#/components/headers/RequestId" }
        Retry-After: { $ref: "#/components/headers/RetryAfter" }
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    ServerError:
      description: server_error. Already reported to ShadowMap.
      headers: { X-Request-Id: { $ref: "#/components/headers/RequestId" } }
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Unavailable:
      description: "api_unavailable: the API is temporarily unavailable, or the request ran past its time limit (see the time limit in the description). Retry with a backoff."
      headers: { X-Request-Id: { $ref: "#/components/headers/RequestId" } }
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Summary:
      description: The counts.
      headers:
        X-Request-Id: { $ref: "#/components/headers/RequestId" }
        X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
        X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/SummaryResponse" }
    ValidationFailed:
      description: validation_failed. The message names the parameter and the reason.
      headers: { X-Request-Id: { $ref: "#/components/headers/RequestId" } }
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
  schemas:
    Error:
      type: object
      additionalProperties: false
      required: [error]
      properties:
        error:
          type: object
          additionalProperties: false
          required: [code, message, request_id]
          properties:
            code:
              type: string
              enum:
                - origin_not_verified
                - token_invalid
                - token_expired
                - token_revoked
                - token_not_permitted
                - account_disabled
                - certificate_mismatch
                - ip_not_allowed
                - scope_missing
                - rate_limited
                - concurrency_limited
                - not_found
                - method_not_allowed
                - validation_failed
                - api_unavailable
                - server_error
            message: { type: string }
            request_id: { type: string }
    HealthResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [status, time, company_id, token]
          properties:
            status: { type: string, enum: [ok] }
            time: { type: string, format: date-time }
            company_id: { type: integer }
            token:
              type: object
              required: [id, name, scopes, expires_at]
              properties:
                id: { type: integer }
                name: { type: string }
                scopes: { type: array, items: { type: string } }
                expires_at: { type: string, format: date-time, nullable: true }
    CapabilitiesResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [api_version, modules]
          properties:
            api_version: { type: string, enum: [v1] }
            modules:
              type: array
              items: { $ref: "#/components/schemas/ModuleCapability" }
    ModuleCapability:
      type: object
      required: [module, scope, endpoints, formats, filters, filter_labels, discovery_column, changed_since, group_by, pagination, sort_keys, status_labels, status_partition, granted]
      properties:
        module: { type: string }
        scope: { type: string }
        endpoints: { type: array, items: { type: string } }
        formats: { type: array, items: { type: string, enum: [json, xml] } }
        filters:
          type: array
          items: { type: string }
          description: |
            The filter parameters the module's list and summary endpoints both accept, with the same values and the
            same refusals: the list takes them besides page_size and cursor, the summary besides group_by.
        filter_labels:
          type: object
          additionalProperties:
            type: array
            items: { type: string }
          description: |
            For each of the module's own filters in `filters` - every one besides `status`, `discovered_from`,
            `discovered_to` and `changed_since`, for example `severity`, `impact_level`, `finding_type`, `closed_by` or
            `module` - the labels it accepts, in the order the summary's `group_by` reports them. A filter takes a
            comma-separated subset; any other value is refused with 422. Empty (`{}`) for an entry with no such filter;
            `status` takes `status_labels` (and so does the scans list's `lifecycle_state`).
        discovery_column: { type: string, nullable: true }
        changed_since:
          type: boolean
          description: |
            True when the module's list and summary take `changed_since` - its records changed since a moment - and
            `filters` lists it: Compromised Users, Compromised Computers and SLA violations, where every change to a
            record's fields moves its `updated_at`. False everywhere else, where a change can leave every timestamp as it
            was, so the parameter is refused with 422 rather than answered incompletely.
        group_by: { type: array, items: { type: string } }
        pagination:
          type: object
          nullable: true
          description: |
            How the module's list endpoint pages: `key` (the field records are ordered by), `tiebreak` (the
            field that orders records sharing a key; present only when the key is not the id),
            `default_page_size` and `max_page_size`. Null for a module without a list endpoint.
        sort_keys:
          type: array
          items: { type: string, enum: [severity, first_seen, last_seen, id] }
          description: |
            The keys the module's list takes in `sort`: `sort=<key>` ascending, `sort=-<key>` descending, the record id
            breaking ties in the same direction. `severity` orders by the risk behind the record's `severity`,
            `first_seen` by the discovery column, `last_seen` by the record's last-seen time, `id` by the id. Empty for
            an entry whose list takes no `sort` (or that has no list).
        refreshed: { type: string }
        status_labels: { type: array, items: { type: string } }
        status_partition: { type: boolean, nullable: true }
        granted: { type: boolean }
    SummaryResponse:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: object
          additionalProperties: false
          required: [all, removed_from_scope, groups]
          properties:
            all:
              type: integer
              minimum: 0
              description: Every finding the module holds for you, whatever its tab - under filters, every finding they leave (with a `status` filter, the findings in any of its tabs). Findings removed from your scope are not included.
            removed_from_scope:
              type: integer
              minimum: 0
              nullable: true
              description: Findings hidden because they left your scope (the dashboard's Removed From Scope tab), narrowed by the discovery window, changed_since and the dimension filters like every count; null when the module has no such view, and under a `status` filter (a finding removed from your scope is in no tab).
            groups:
              type: array
              items:
                type: object
                required: [count]
                properties:
                  count: { type: integer, minimum: 0 }
                  host:
                    type: string
                    description: With `group_by=host`, the ranked host; `count` is its findings under the request's filters.
                  by_severity:
                    type: object
                    description: |
                      With `group_by=host`, the host's findings per severity label - every label of the module's `severity`
                      dimension, zeros included. A finding whose severity is none of them counts in `count` only.
                    additionalProperties: { type: integer, minimum: 0 }
                additionalProperties:
                  type: string
                  nullable: true
        meta:
          type: object
          additionalProperties: false
          required: [module, group_by, status_partition, generated_at]
          properties:
            module: { type: string }
            group_by: { type: array, items: { type: string } }
            top:
              type: integer
              minimum: 1
              maximum: 100
              description: With `group_by=host`, how many hosts were asked for (10 when `top` was not given).
            status_partition:
              type: boolean
              description: True when the module's status tabs are designed to cover `all` exactly.
            generated_at: { type: string, format: date-time }
    ListMeta:
      type: object
      additionalProperties: false
      required: [page_size, next_cursor]
      properties:
        page_size: { type: integer, minimum: 1 }
        next_cursor:
          type: string
          nullable: true
          description: Pass it as `cursor` to get the next page; null on the last page.
    VendorRow:
      type: object
      required: [type, name]
      properties:
        type: { type: string, enum: [current, historical] }
        name: { type: string }
        initials: { type: string, nullable: true }
        scan_date: { type: string, nullable: true }
        score_date: { type: string, nullable: true }
        tags: { type: string, nullable: true }
        custom_tags: { type: string, nullable: true }
        overall_security_rating: { type: string, nullable: true, enum: [A, B, C, D, F, "", null] }
        overall_risk_score: { type: number, nullable: true }
        alerts_rating: { type: string, nullable: true }
        alerts_risk_score: { type: number, nullable: true }
        brand_protection_rating: { type: string, nullable: true }
        brand_protection_risk_score: { type: number, nullable: true }
        data_leaks_rating: { type: string, nullable: true }
        data_leaks_risk_score: { type: number, nullable: true }
        dark_web_rating: { type: string, nullable: true }
        dark_web_risk_score: { type: number, nullable: true }
        email_and_dns_rating: { type: string, nullable: true }
        email_and_dns_score: { type: number, nullable: true }
        encryption_and_certificates_rating: { type: string, nullable: true }
        encryption_and_certificates_score: { type: number, nullable: true }
        application_security_rating: { type: string, nullable: true }
        application_security_score: { type: number, nullable: true }
        vulnerability_management_rating: { type: string, nullable: true }
        vulnerability_management_score: { type: number, nullable: true }
        alerts_high: { type: integer, nullable: true }
        alerts_medium: { type: integer, nullable: true }
        alerts_low: { type: integer, nullable: true }
        brand_protection_high: { type: integer, nullable: true }
        brand_protection_medium: { type: integer, nullable: true }
        brand_protection_low: { type: integer, nullable: true }
        data_leaks_high: { type: integer, nullable: true }
        data_leaks_medium: { type: integer, nullable: true }
        data_leaks_low: { type: integer, nullable: true }
        dark_web_high: { type: integer, nullable: true }
        dark_web_medium: { type: integer, nullable: true }
        dark_web_low: { type: integer, nullable: true }
    DomainList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/Domain" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    Domain:
      type: object
      additionalProperties: false
      required: [id, domain, status, registrar, registered_date, expiry_date, domain_validity, mail_provider, dns_provider, email_security, dns_records, name_servers, country, asn, apps, alerts, subdomains, ssl_certificates, ip_addresses, custom_tags, organizations, last_seen_on, added_at, last_seen_at]
      properties:
        id: { type: integer }
        domain: { type: string }
        status: { type: string, enum: ["In Scope", "Out of Scope", "Unknown"] }
        registrar: { type: string }
        registered_date: { type: string }
        expiry_date: { type: string }
        domain_validity: { type: string, enum: [Valid, "Expiring Soon", Expired, Unverified, "No Expiry Date", Unknown] }
        mail_provider: { type: string }
        dns_provider: { type: string }
        email_security: { type: string }
        dns_records: { type: string }
        name_servers: { type: string }
        country: { type: string }
        asn: { type: string }
        apps: { type: integer, minimum: 0 }
        alerts: { type: integer, minimum: 0 }
        subdomains: { type: integer, minimum: 0 }
        ssl_certificates: { type: integer, minimum: 0 }
        ip_addresses: { type: integer, minimum: 0 }
        custom_tags:
          type: object
          additionalProperties:
            type: array
            items: { type: string }
        organizations: { type: string }
        last_seen_on: { type: string }
        added_at: { type: string, format: date-time, nullable: true }
        last_seen_at: { type: string, format: date-time, nullable: true }
    SubdomainList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/Subdomain" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    Subdomain:
      type: object
      additionalProperties: false
      required: [subdomain, parent_domain, status, ips, last_seen_on]
      properties:
        subdomain: { type: string }
        parent_domain: { type: string, nullable: true }
        status: { type: string, enum: ["In Scope", "Out of Scope", "Not Reachable"] }
        ips:
          type: array
          items: { type: string }
        last_seen_on:
          type: string
          nullable: true
          description: As stored by the scanner, as text.
    IpAddressList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/IpAddress" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    IpAddress:
      type: object
      additionalProperties: false
      required: [id, ip_address, status, reverse_dns, city, country, provider, last_seen_on]
      properties:
        id: { type: integer }
        ip_address: { type: string }
        status: { type: string, enum: [Online, Offline] }
        reverse_dns: { type: string, nullable: true }
        city: { type: string, nullable: true }
        country: { type: string, nullable: true }
        provider: { type: string, nullable: true }
        last_seen_on: { type: string }
    InventorySummaryResponse:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: object
          additionalProperties: false
          required: [domains, subdomains, ips]
          properties:
            domains:
              type: object
              additionalProperties: false
              required: [all, by_status]
              properties:
                all: { type: integer, minimum: 0 }
                by_status:
                  type: object
                  additionalProperties: false
                  required: ["In Scope", "Out of Scope"]
                  properties:
                    "In Scope": { type: integer, minimum: 0 }
                    "Out of Scope": { type: integer, minimum: 0 }
            subdomains:
              type: object
              additionalProperties: false
              required: [all, by_status]
              properties:
                all: { type: integer, minimum: 0 }
                by_status:
                  type: object
                  additionalProperties: false
                  required: ["In Scope", "Out of Scope", "Not Reachable"]
                  properties:
                    "In Scope": { type: integer, minimum: 0 }
                    "Out of Scope": { type: integer, minimum: 0 }
                    "Not Reachable": { type: integer, minimum: 0 }
            ips:
              type: object
              additionalProperties: false
              required: [all, by_status]
              properties:
                all: { type: integer, minimum: 0 }
                by_status:
                  type: object
                  additionalProperties: false
                  required: [Online, Offline]
                  properties:
                    Online: { type: integer, minimum: 0 }
                    Offline: { type: integer, minimum: 0 }
        meta:
          type: object
          additionalProperties: false
          required: [generated_at]
          properties:
            generated_at: { type: string, format: date-time }
    ScanList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/Scan" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    Scan:
      type: object
      additionalProperties: false
      required: [id, started_at, ended_at, duration_seconds, lifecycle_state, hold_reason, last_transition_at]
      properties:
        id: { type: integer }
        started_at: { type: string, format: date-time, nullable: true }
        ended_at:
          type: string
          format: date-time
          nullable: true
          description: Null while the scan is running.
        duration_seconds: { type: integer, minimum: 0, nullable: true }
        lifecycle_state:
          type: string
          nullable: true
          enum: [running, paused, held_coverage, held_host_count, wedged, dead_pre_nmap, dead_analyze_incomplete, sealed, discarded, superseded, null]
        hold_reason: { type: string, nullable: true }
        last_transition_at: { type: string, format: date-time, nullable: true }
    S3BucketList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/S3Bucket" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    S3BucketDetail:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/S3Bucket" }
    S3Bucket:
      type: object
      additionalProperties: false
      required: [id, web_url, bucket_url, provider, status, online, severity, risk, confidence_score, files_exposed, keyword, classification_tags, compliance_frameworks, assignee, created_at, updated_at, takedown_requested_on]
      properties:
        id: { type: integer }
        web_url:
          type: string
          description: The bucket's page on your dashboard.
        bucket_url: { type: string }
        provider:
          type: string
          nullable: true
          description: The storage provider's code, passed through exactly as stored - aws, gcp, azure, dos (DigitalOcean Spaces) or ali (Alibaba Cloud Object Storage).
        status:
          type: string
          nullable: true
          enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "Offline", "To Be Closed", "Closed", "Filtered by AI", null]
          description: The dashboard tab the bucket is under; null when it is in none (a retired workflow state).
        online:
          type: boolean
          nullable: true
          description: Whether the bucket is still online; null when that is unknown.
        severity: { type: string, enum: [Critical, High, Medium, Low, Info, "N/A"] }
        risk:
          type: integer
          description: The stored risk behind severity - 5 Critical, 4 High, 3 Medium, 2 Low, 1 Info; any other value is N/A.
        confidence_score: { type: integer }
        files_exposed: { type: integer, nullable: true }
        keyword: { type: string, nullable: true }
        classification_tags: { type: array, items: { type: string } }
        compliance_frameworks: { type: array, items: { type: string } }
        assignee:
          type: string
          nullable: true
          description: The user or team (`#Team name`) the bucket is assigned to.
        created_at:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap first found the bucket - the module's discovery column.
        updated_at:
          type: string
          format: date-time
          nullable: true
          description: The record's last-updated time as stored (the export's Last seen); not every change moves it.
        takedown_requested_on: { type: string, format: date-time, nullable: true }
    CodeRepositoryList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          maxItems: 200
          items: { $ref: "#/components/schemas/CodeRepository" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    CodeRepositoryDetail:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/CodeRepository" }
    CodeRepository:
      type: object
      additionalProperties: false
      required: [id, web_url, full_name, html_url, owner_name, source, severity, risk, status, scanner_status, response_status, is_bad_repository, is_pinned, keywords, files, files_total, author_name, author_email, author_email_domain, custom_tags, first_seen_on, last_seen_on, updated_at, takedown_requested_on]
      properties:
        id: { type: integer }
        web_url:
          type: string
          description: The repository's page on your dashboard.
        full_name:
          type: string
          description: The repository's full name, owner/name, as stored - with html_url, the identifier of the exposure.
        html_url:
          type: string
          description: The repository's URL on its platform, as stored.
        owner_name:
          type: string
          description: The account that owns the repository.
        source:
          type: string
          enum: [GitHub, GitHub Gist, GitLab, GitLab Snippet, Unknown]
          description: The platform - the summary's source label, the one group_by=source counts the repository under; Unknown for a source ShadowMap has no name for.
        severity:
          type: string
          enum: [High, Medium, Low, Informational]
          description: The summary's band of risk - High 500 and above, Medium 100 to 499, Low 1 to 99, Informational 0 or below.
        risk:
          type: integer
          description: ShadowMap's risk score for the repository, which severity bands.
        status:
          type: string
          nullable: true
          enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "Takedown In Progress", "Taken Down", "Offline", "Filtered by AI", null]
          description: The dashboard tab the repository is under; null when it is in none (pinned outside the takedown flow, or a review code no tab shows).
        scanner_status:
          type: string
          description: |
            The scanner's own view of the repository, the export's Status - online, or takendown when the scanner no
            longer finds it. takendown is NOT a ShadowMap takedown; the tab (status) says whether we requested one.
        response_status:
          type: integer
          description: The stored review code the tab is computed from - 0 needs review, 1 investigating, 3 accepted risk, 6 filtered by AI.
        is_bad_repository:
          type: boolean
          description: True when the repository was marked a false positive - the Dismissed tab.
        is_pinned:
          type: boolean
          description: True when the repository was pinned (the Pin Repository action); a pinned repository outside the takedown flow is in no tab.
        keywords:
          type: string
          nullable: true
          description: The text ShadowMap matched the repository on, as stored (one string, never split); null when empty.
        files:
          type: array
          maxItems: 50
          description: The files ShadowMap found in the repository - the first 50 by id; files_total says how many there are.
          items:
            type: object
            additionalProperties: false
            required: [name, path, url]
            properties:
              name: { type: string }
              path: { type: string }
              url:
                type: string
                description: The file's page on the repository's platform.
        files_total:
          type: integer
          minimum: 0
          description: How many files ShadowMap found in the repository; more than the files listed when it holds over 50.
        author_name: { type: string, nullable: true }
        author_email: { type: string, nullable: true }
        author_email_domain: { type: string, nullable: true }
        custom_tags:
          type: object
          description: Your own tags, tag key to values, under the keys you defined for Code Repositories. A repository without any has an empty object.
          additionalProperties:
            type: array
            items: { type: string }
        first_seen_on:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap first found the repository - the module's discovery column.
        last_seen_on:
          type: string
          format: date-time
          nullable: true
          description: When a scan last found the repository.
        updated_at:
          type: string
          format: date-time
          nullable: true
          description: The record's last-updated time as stored; not every change moves it. Always stored; null only if the stored value is not a valid time.
        takedown_requested_on:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap filed a takedown for the repository; null when none was filed.
    DockerContainerList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/DockerContainer" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    DockerContainerDetail:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/DockerContainer" }
    DockerContainer:
      type: object
      additionalProperties: false
      required: [id, web_url, publisher_name, image_name, image_link, is_bad_image, is_pinned, source, severity, confidence, status, scanner_status, response_status, pull_count, keyword, overview, first_seen_on, last_seen_on, updated_at, takedown_requested_on]
      properties:
        id: { type: integer }
        web_url:
          type: string
          description: The image's page on your dashboard.
        publisher_name:
          type: string
          nullable: true
          description: The registry account that publishes the image; null when none is recorded.
        image_name: { type: string }
        image_link:
          type: string
          nullable: true
          description: The image's page on its registry (Docker Hub), as the dashboard links it; null without a publisher or for a source without a link.
        is_bad_image:
          type: boolean
          description: True when the image was marked a false positive - the Dismissed tab.
        is_pinned: { type: boolean }
        source:
          type: string
          nullable: true
          description: The registry the image was found on (Docker Hub); null for a source the dashboard has no name for.
        severity:
          type: string
          description: The summary's severity label - High, Medium or Low; any other stored risk is returned as stored.
        confidence:
          type: string
          description: ShadowMap's confidence label as stored - high, moderate or low.
        status:
          type: string
          nullable: true
          enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "Requested Takedown", "Taken Down", "Offline", "Filtered by AI", null]
          description: The dashboard tab the image is under; null when it is in none (a review code no tab shows).
        scanner_status:
          type: string
          description: |
            The scanner's own view of the image, the export's Status - online, or takendown when the scanner no longer
            finds the image in the registry. takendown is NOT a ShadowMap takedown; the tab (status) says whether we
            requested one.
        response_status:
          type: integer
          description: The stored review code the tab is computed from - 0 needs review, 1 investigating, 3 accepted risk, 6 filtered by AI.
        pull_count:
          type: string
          nullable: true
          description: The registry's pull count, as stored (text).
        keyword: { type: string, nullable: true }
        overview:
          type: string
          nullable: true
          description: The image's description on its registry.
        first_seen_on:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap first found the image - the module's discovery column.
        last_seen_on:
          type: string
          format: date-time
          nullable: true
          description: When a scan last found the image.
        updated_at:
          type: string
          format: date-time
          nullable: true
          description: The record's last-updated time as stored; not every change moves it. Always stored; null only if the stored value is not a valid time.
        takedown_requested_on:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap filed a takedown for the image; null when none was filed.
    LeakedFileList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/LeakedFile" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    LeakedFileDetail:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/LeakedFile" }
    LeakedFile:
      type: object
      additionalProperties: false
      required: [id, web_url, file_name, hash, source, type, environment, threat_level, threat_level_label, severity, risk_score, sample_link, status, triage_status, response_status, time_stamp, created_at, updated_at, takedown_requested_on, confidence_score]
      properties:
        id: { type: integer }
        web_url:
          type: string
          description: The file's page on your dashboard.
        file_name: { type: string }
        hash: { type: string }
        source:
          type: string
          nullable: true
          description: Where the file was found - Hybrid Analysis or Scribd; any other stored source is returned as stored, and null means none is stored.
        type:
          type: string
          description: The file's type, as the source reports it.
        environment:
          type: string
          nullable: true
          description: The environment (operating system) the source reports for the file, as stored; null when none.
        threat_level:
          type: string
          nullable: true
          description: The threat level as stored - a malware verdict (malicious, suspicious, ...) or a Scribd sensitivity tier (sensitive_critical, sensitive_high, sensitive_medium); null when blank.
        threat_level_label:
          type: string
          nullable: true
          description: |
            The display label of threat_level (the export's Threat Level), for example "Sensitivity: Critical" for
            sensitive_critical or "Malicious" for malicious; a level without a label is returned as stored.
        severity: { type: string, enum: [High, Medium, Low] }
        risk_score:
          type: integer
          minimum: 0
          nullable: true
          description: The file's sensitivity score (0 to 100 as ShadowMap scores it); null when the file has not been scored.
        sample_link:
          type: string
          nullable: true
          description: The link to the file at its source.
        status:
          type: string
          nullable: true
          enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "Requested Takedown", "Taken Down", "Filtered by AI", null]
          description: The dashboard tab the file is under; null when it is in none (for example a retired status code).
        triage_status:
          type: integer
          description: The stored triage code the tab is computed from - 0 needs review, 1 accepted; 2 and 3 are retired codes.
        response_status:
          type: integer
          description: The stored response code the tab is computed from - 0 none, 1 investigating, 4 dismissed, 6 filtered by AI.
        time_stamp:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap discovered the file - the module's discovery column. Always stored; null only if the stored value is not a valid date.
        created_at: { type: string, format: date-time, nullable: true }
        updated_at:
          type: string
          format: date-time
          nullable: true
          description: The record's last-updated time as stored; not every change moves it.
        takedown_requested_on:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap filed a takedown for the file; null when none was filed.
        confidence_score:
          type: integer
          description: How confident ShadowMap is that the file is yours; files scored 0 are not listed.
    LeakedApiList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          maxItems: 200
          items: { $ref: "#/components/schemas/LeakedApi" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    LeakedApiDetail:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/LeakedApi" }
    LeakedApi:
      type: object
      additionalProperties: false
      required: [id, web_url, api_url, source_url, method, severity, status, response_status, online, keyword, confidence, sectional_risk, leaked_on, leak_updated_on, created_at, updated_at, auth, headers, params, body]
      properties:
        id: { type: integer }
        web_url:
          type: string
          description: The API's page on your dashboard.
        api_url:
          type: string
          nullable: true
          description: The leaked API's URL, as stored (it can carry credentials in its query string). An API with no URL is not listed; null means an empty one is stored.
        source_url:
          type: string
          nullable: true
          description: Where the API was found (for example a public Postman workspace).
        method: { type: string, nullable: true }
        severity:
          type: string
          nullable: true
          description: The summary's severity label - High, Medium or Low; any other stored risk is returned as stored, and a null risk is null.
        status:
          type: string
          nullable: true
          enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "Offline", "To Be Closed", "Closed", "Filtered by AI", null]
          description: The dashboard tab the API is under; null when it is in none (a state no tab names, such as the Open code).
        response_status:
          type: integer
          nullable: true
          description: The stored workflow code the tab is computed from - null, 1 or 6 needs review, 10 investigating, 4 accepted risk, 2 or 9 dismissed, 3 to be closed, 5 closed, 11 filtered by AI; any other code is in no tab.
        online:
          type: boolean
          nullable: true
          description: The stored liveness the tab is computed from - true online, false gone; null when unknown (the tabs count it as online).
        keyword: { type: string, nullable: true }
        confidence:
          type: integer
          nullable: true
          description: The ingest's confidence score; APIs at 0 or below are not listed.
        sectional_risk:
          type: object
          nullable: true
          additionalProperties: true
          description: The risk of each part of the request, for example {"params":"low","auth":"critical"}, as the ingest stores it; null when none is stored or the stored value is not a JSON object (an older form).
        leaked_on:
          type: string
          format: date-time
          nullable: true
          description: When the API was leaked - the module's discovery column; null for the few APIs without one, which no discovery window returns.
        leak_updated_on:
          type: string
          format: date-time
          nullable: true
          description: When the leak itself was last updated at its source.
        created_at: { type: string, format: date-time, nullable: true }
        updated_at:
          type: string
          format: date-time
          nullable: true
          description: The record's last-updated time as stored; not every change moves it.
        auth:
          type: string
          nullable: true
          description: The leaked request's authentication, in full and exactly as stored. A secret.
        headers:
          type: string
          nullable: true
          description: The leaked request's headers, in full and exactly as stored. Can hold secrets.
        params:
          type: string
          nullable: true
          description: The leaked request's parameters, in full and exactly as stored. Can hold secrets.
        body:
          type: string
          nullable: true
          description: The leaked request's body, in full and exactly as stored. Can hold secrets.
    LeakedCredentialList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/LeakedCredential" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    LeakedCredentialDetail:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/LeakedCredential" }
    LeakedCredential:
      type: object
      additionalProperties: false
      required: [id, web_url, source, source_module_id, source_ref, rule_name, matching, before, after, status, response_status, hidden, probe_status, working_status, probe_provider, probe_metadata, last_probed_at, impact_level, impact_score, assignee, occurrence_count, first_seen_at, last_seen_at, created_at, updated_at]
      properties:
        id: { type: integer }
        web_url:
          type: string
          description: Opens the credential on your dashboard (the Leaked Credentials list, with the credential open).
        source:
          type: string
          description: |
            The module that found the credential, as the summary's source labels name it - for example Code
            Repositories, Mobile Application, Leaked APIs or Leaked Credential (Web App); Unknown for a module the
            dashboard has no name for.
        source_module_id:
          type: integer
          description: The stored id of that module - 12 Leaked APIs, 13 Leaked Files, 14 Code Repositories, 15 Mobile Applications, 28 web applications, 29 Vendor Risk Management, 32 exposed databases.
        source_ref:
          type: object
          nullable: true
          additionalProperties: false
          required: [type, id, label, file]
          description: |
            Where the credential was found, for the four sources that name a place; null for every other source module,
            and when the stored reference does not lead to one of your own records.
          properties:
            type:
              type: string
              enum: [code_repository, mobile_application, web_application, exposed_database]
            id:
              type: integer
              nullable: true
              description: |
                The code repository's id (the Code Repositories record id; a repository removed from your scope is not
                served there) or the mobile application's id; null for a web page or a database, which have no record.
            label:
              type: string
              nullable: true
              description: The repository's full name (owner/name), the app's title, or the host of the web page's or database's URL; null when none is stored.
            file:
              type: string
              nullable: true
              description: The file in the repository or app the credential was found in, or the URL of the web page or database.
        rule_name:
          type: string
          description: The detection rule that matched, as stored (for example AWS API Key).
        matching:
          type: string
          description: The leaked value, in full and exactly as stored. A secret.
        before:
          type: string
          nullable: true
          description: The text stored before the leaked value where it was found, in full and exactly as stored; the dashboard does not show it. Can hold secrets.
        after:
          type: string
          nullable: true
          description: The text stored after the leaked value where it was found, in full and exactly as stored; the dashboard does not show it. Can hold secrets.
        status:
          type: string
          nullable: true
          enum: ["Needs Review", "Investigating", "Accepted Risk", "Closed", "Filtered by AI", "Dismissed", "Third-party", "Resolved", null]
          description: The tab the credential is under; null for a visible credential with a code no tab shows.
        response_status:
          type: integer
          description: |
            The stored review code a visible credential's tab is computed from - 0 needs review, 1 investigating,
            2 accepted risk, 3 closed, 6 filtered by AI. A hidden credential's tab is decided by hidden.reason instead.
        hidden:
          type: object
          nullable: true
          additionalProperties: false
          required: [reason, explanation]
          description: Why a hidden (set-aside) credential is hidden; null for a visible one.
          properties:
            reason:
              type: string
              nullable: true
              description: |
                The stored reason, as stored - analyst_fp, bad_repository or app_reviewed (Dismissed), unowned-repo
                (Third-party), orphan_no_repo, orphan_no_app or app_takedown (Resolved); any other reason, or none
                (null), is a machine filter (Filtered by AI).
            explanation:
              type: string
              description: The dashboard's sentence for the reason (the export's Hidden Reason).
        probe_status:
          type: string
          description: |
            As the dashboard derives it: the probe's verdict as stored (for example confirmed_live, confirmed_dead,
            jwt_active, inconclusive), not_applicable when the credential cannot be validated, or not_probed when it has
            not been probed yet.
        working_status:
          type: string
          enum: [Confirmed Working, Maybe Working, Not Working, Not Tested]
          description: probe_status grouped as on the dashboard; Not Tested covers every status without a definitive verdict.
        probe_provider:
          type: string
          nullable: true
          description: The provider the probe validated the credential against.
        probe_metadata:
          type: object
          nullable: true
          additionalProperties: true
          description: |
            The evidence the probe recorded, as a JSON object, in full; the dashboard shows it only in a credential's
            probe detail. Null when none is stored or the stored value is not a JSON object. Can hold secrets.
        last_probed_at: { type: string, format: date-time, nullable: true }
        impact_level:
          type: string
          description: The summary's impact label - Critical, High, Medium, Low, None, or Unmeasured when no impact was measured; any other stored level is returned as stored.
        impact_score:
          type: integer
          minimum: 0
          nullable: true
          description: The probe's impact score; null when no impact was measured.
        assignee:
          type: string
          nullable: true
          description: The user or team (`#Team name`) the credential is assigned to.
        occurrence_count:
          type: integer
          minimum: 1
          description: In how many places this exact value was found (at least 1).
        first_seen_at:
          type: string
          format: date-time
          nullable: true
          description: When the credential was first seen where it leaked; null on credentials stored before sightings were recorded.
        last_seen_at:
          type: string
          format: date-time
          nullable: true
          description: When the credential was last seen where it leaked; null on credentials stored before sightings were recorded.
        created_at:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap first stored the credential - the module's discovery column.
        updated_at:
          type: string
          format: date-time
          nullable: true
          description: The row's last-updated time as stored; it moves on any change to the row, so do not use it to detect changes.
    WebApplicationList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          maxItems: 200
          items: { $ref: "#/components/schemas/WebApplication" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    WebApplicationDetail:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/WebApplication" }
    WebApplication:
      type: object
      additionalProperties: false
      required: [id, web_url, title, status, lifecycle_status, response_status, severity, suggested_risk, proto, host, ip, port, path, uri, mapped_hosts, assignee, tags, custom_tags, netblock, asn, provider, country, comment_count, last_comment, last_comment_at, last_comment_author, close_reason, first_seen_on, last_seen_on, created_at, updated_at]
      properties:
        id:
          type: integer
          description: |
            The id of ShadowMap's representative row for this logical web application. It can change when the
            representative does; (proto, host, port, path) identifies the application.
        web_url:
          type: string
          description: The application's page on your dashboard.
        title:
          type: string
          nullable: true
          description: The page title the scanner read; null when it read none.
        status:
          type: string
          nullable: true
          enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "To Be Closed", "Closed", null]
          description: The dashboard tab the application is under; null when it is in none (a legacy lifecycle code, or a response no tab shows).
        lifecycle_status:
          type: string
          nullable: true
          enum: ["New", "Reviewed", "To Be Closed", "Accepted Risk", "Closed", "Reopened", "Open", "Investigating", null]
          description: The stored lifecycle - the export's Status; in practice New, Open, Reopened or Closed. Null for a legacy code.
        response_status:
          type: integer
          nullable: true
          description: The analyst's response code a live application's tab is computed from - 9 investigating, 4 accepted risk, 2 dismissed (the export's "Reviewed"), 3 to be closed; null when there is none yet.
        severity:
          type: string
          nullable: true
          enum: [High, Medium, Low, Informational, null]
          description: The summary's label of ShadowMap's risk for the application; null when it has none.
        suggested_risk:
          type: string
          nullable: true
          description: The risk an analyst suggested in place of severity, in the same labels (stored text no label names is returned as stored); null when none was suggested.
        proto:
          type: string
          nullable: true
        host:
          type: string
          nullable: true
        ip:
          type: string
          nullable: true
        port:
          type: integer
        path:
          type: string
          nullable: true
        uri:
          type: string
          description: The application's URI as the export writes it - proto://host:port followed by the path, the IP when there is no host, the port always written.
        mapped_hosts:
          type: array
          items: { type: string }
          description: The host names ShadowMap mapped to the application's public IP in the scan that found it, each once, sorted; none for a private IP.
        assignee:
          type: string
          nullable: true
          description: The user or team (`#Team name`) the application is assigned to.
        tags:
          type: array
          items: { type: string }
          description: The stored tags, every one, sorted (the dashboard's list hides the tags of closed tag rules).
        custom_tags:
          type: object
          description: Your own tags, tag key to values. An application without any has an empty object.
          additionalProperties:
            type: array
            items: { type: string }
        netblock:
          type: string
          nullable: true
          description: The network block recorded for the application.
        asn:
          type: integer
          nullable: true
          description: The number of the IP's ASN.
        provider:
          type: string
          nullable: true
          description: The provider of the IP's ASN - the export's ASN - Provider.
        country:
          type: string
          nullable: true
          description: The country of the IP's geolocation.
        comment_count:
          type: integer
          minimum: 0
        last_comment:
          type: string
          nullable: true
        last_comment_at:
          type: string
          format: date-time
          nullable: true
        last_comment_author:
          type: string
          nullable: true
          description: Who wrote the last comment (the export leaves this column blank).
        close_reason:
          type: string
          nullable: true
          description: Why ShadowMap closed the application, as recorded - set for closes since 16 June 2026 and cleared when it reopens.
        first_seen_on:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap first found the application - the module's discovery column.
        last_seen_on:
          type: string
          format: date-time
          nullable: true
          description: When a scan last found the application.
        created_at:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap stored the row.
        updated_at:
          type: string
          format: date-time
          nullable: true
          description: The row's last-updated time as stored; scans and system closes move it as well as analyst changes, so do not use it to detect changes.
    MobileApplicationList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/MobileApplication" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    MobileApplicationDetail:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/MobileApplication" }
    MobileApplication:
      type: object
      additionalProperties: false
      required: [id, web_url, title, app_id, store, status, version, country_code, store_url, developer, developer_url, score, downloads, size, alert_count, secret_count, created_at, updated_at, store_updated_at, takedown_requested_on]
      properties:
        id: { type: integer }
        web_url:
          type: string
          description: The app's page on your dashboard.
        title:
          type: string
          description: The app's name on its store.
        app_id:
          type: string
          description: The app's id on its store - the Play Store package name or the App Store bundle id.
        store:
          type: string
          nullable: true
          enum: ["Play Store", "App Store", null]
          description: The store the app was found on; null for a store the dashboard has no name for.
        status:
          type: string
          nullable: true
          enum: ["Needs Review", "Investigating", "Genuine", "Dismissed", "Requested Takedown", "Taken Down", "Filtered by AI", null]
          description: The dashboard tab the app is under; null when it is in none (a review code no tab shows).
        version:
          type: string
          description: The app version the store listed when ShadowMap last read it.
        country_code:
          type: string
          nullable: true
          description: The country of the store listing ShadowMap read, as an ISO code.
        store_url:
          type: string
          nullable: true
          description: The app's page on its store.
        developer:
          type: string
          nullable: true
          description: The developer's name on the store.
        developer_url:
          type: string
          nullable: true
          description: The developer's page on the store, built from the store's developer name and id as the dashboard's export builds it (App Store links use its /in/ storefront); null when the store gave no developer id or no developer name, when an App Store developer id is not a number, or when the app's store is one the dashboard does not know (`store` is null).
        score:
          type: number
          nullable: true
          description: The app's rating on its store; null when the store reports none (the dashboard's export prints 0 then).
        downloads:
          type: integer
          nullable: true
          description: The install count the Play Store reports for the app; null on the App Store, which reports none.
        size:
          type: string
          nullable: true
          description: The app's size as the store gave it - text with its unit on the Play Store; on the App Store a number as text, its unit not verified.
        alert_count:
          type: integer
          minimum: 0
          description: How many issues ShadowMap's analysis of the app raised, as the export counts them.
        secret_count:
          type: integer
          minimum: 0
          description: How many leaked secrets ShadowMap found in the app's package, as the export counts them - every finding, so a secret found in two places counts twice; the dashboard's detail page counts each distinct secret once.
        created_at:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap first found the app - the module's discovery column. Always stored; null only if the stored value is not a valid time.
        updated_at:
          type: string
          format: date-time
          nullable: true
          description: When the app's row was last updated, as stored; not every change moves it, so do not use it to detect changes. Always stored; null only if the stored value is not a valid time.
        store_updated_at:
          type: string
          format: date-time
          nullable: true
          description: When the store says the app was last updated - the export's Last Updated On. The App Store gives it in UTC and the Play Store in the scan's local time; both are returned as the instant they name. Null when the store gives none.
        takedown_requested_on:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap filed a takedown for the app; null when none was filed.
    IpReputationList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/IpReputationListing" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    IpReputationListingDetail:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/IpReputationListing" }
    IpReputationListing:
      type: object
      additionalProperties: false
      required: [id, web_url, ip_address, status, severity, source_count, flagged_by, asn, country_code, bot_name, assignee, first_seen, last_seen, updated_at]
      properties:
        id: { type: integer }
        web_url:
          type: string
          description: The listing's page on your dashboard.
        ip_address: { type: string }
        status:
          type: string
          nullable: true
          enum: ["Needs Review", "Investigating", "Confirmed Threat", "Mitigated", "Accepted Risk", "False Positive", null]
          description: The dashboard tab the listing is under; null when it is in none.
        severity: { type: string, enum: [Critical, High, Medium, Low] }
        source_count:
          type: integer
          minimum: 0
          description: How many distinct feeds flag the IP in this listing - the length of flagged_by.
        flagged_by:
          type: array
          items: { type: string }
          description: The blocklist feeds flagging the IP in this listing, each once.
        asn:
          type: string
          nullable: true
          description: The IP's network owner, as recorded.
        country_code:
          type: string
          nullable: true
          description: The IP's country, as an ISO code.
        bot_name:
          type: string
          nullable: true
          description: The malware a feed associates with the IP, when one does.
        assignee:
          type: string
          nullable: true
          description: The user or team (`#Team name`) the listing is assigned to.
        first_seen:
          type: string
          format: date-time
          description: When ShadowMap first found the listing - the module's discovery column.
        last_seen:
          type: string
          format: date-time
          description: When a scan last saw the listing.
        updated_at:
          type: string
          format: date-time
          nullable: true
          description: When the listing was last changed on the dashboard; null when it never was.
    AlertList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/Alert" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    AlertDetail:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/Alert" }
    Alert:
      type: object
      additionalProperties: false
      required: [id, web_url, title, status, scanner_status, closed_by, severity, risk_score, finding_type, host, ip, port, path, proto, full_uri, related_assets, related_assets_truncated, tags, custom_tags, cve_ids, cwe_ids, description, impact, recommendation, assignee, country, asn, netblock, provider, comment_count, last_comment, last_comment_at, last_comment_author, dispositioned_at, dispositioned_by, scanner_close_reason, added_on, last_seen_on]
      properties:
        id: { type: integer }
        web_url:
          type: string
          description: The alert's page on your dashboard.
        title: { type: string }
        status:
          type: string
          nullable: true
          enum: ["Needs Review", "Investigating", "Accepted Risk", "Dismissed", "To Be Closed", "Closed", "Filtered by AI", null]
          description: The dashboard tab the alert is under; null when it is in none (a retired workflow state).
        scanner_status:
          type: string
          nullable: true
          enum: [new, open, closed, null]
          description: The scanner's own view of the finding.
        closed_by:
          type: string
          nullable: true
          enum: [analyst, scanner, null]
          description: Who closed a Closed alert - an analyst, or the scanner no longer detecting it; null when not Closed.
        severity: { type: string, enum: [High, Medium, Low, Informational] }
        risk_score:
          type: integer
          nullable: true
          description: The risk from 0 to 10 that the severity is banded on - the alert's effective risk where ShadowMap records one (the analyst's risk when one re-banded the alert, otherwise the scanner's), and the scanner's risk where it does not.
        finding_type:
          type: string
          description: One of the types of the dashboard's Finding Type filter, or Other.
        host:
          type: string
          nullable: true
          description: The host (the IP address when there is none); for a leaked credential, where it leaked.
        ip: { type: string, nullable: true }
        port: { type: integer, nullable: true }
        path: { type: string, nullable: true }
        proto: { type: string, nullable: true }
        full_uri:
          type: string
          nullable: true
          description: The finding's URI; for a leaked credential, a link to where it leaked (null when there is none).
        related_assets:
          type: array
          maxItems: 50
          items: { type: string }
          description: Hosts of your open web applications at the alert's IP address or host, at most 50.
        related_assets_truncated:
          type: boolean
          description: True when there are more than the 50 related assets listed.
        tags: { type: array, items: { type: string } }
        custom_tags:
          type: object
          description: Your own tags, tag key to values, as stored. An alert without any has an empty object.
          additionalProperties:
            type: array
            items: { type: string }
        cve_ids: { type: array, items: { type: string } }
        cwe_ids: { type: array, items: { type: string } }
        description: { type: string, nullable: true }
        impact: { type: string, nullable: true }
        recommendation: { type: string, nullable: true }
        assignee:
          type: string
          nullable: true
          description: The user or team (`#Team name`) the alert is assigned to.
        country: { type: string, nullable: true }
        asn: { type: integer, nullable: true }
        netblock:
          type: string
          nullable: true
          description: The network block of the alert's ASN record.
        provider: { type: string, nullable: true }
        comment_count: { type: integer, minimum: 0 }
        last_comment: { type: string, nullable: true }
        last_comment_at: { type: string, format: date-time, nullable: true }
        last_comment_author: { type: string, nullable: true }
        dispositioned_at:
          type: string
          format: date-time
          nullable: true
          description: When the alert's workflow status last changed.
        dispositioned_by: { type: string, nullable: true }
        scanner_close_reason:
          type: string
          nullable: true
          description: What triggered the scanner's latest close of the finding (recorded from 31 March 2026).
        added_on:
          type: string
          format: date-time
          description: When ShadowMap first found the alert - the module's discovery column.
        last_seen_on: { type: string, format: date-time, nullable: true }
    DataBreachList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          maxItems: 200
          items: { $ref: "#/components/schemas/DataBreach" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    DataBreachDetail:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/DataBreach" }
    DataBreach:
      type: object
      additionalProperties: false
      required: [id, web_url, status, severity, source, title, breach_date, breach_type, breach_value, domain, password, actioned, is_executive, executive_name, custom_tags, created_at, updated_at, takedown_requested_on, first_seen_at, last_seen_at]
      properties:
        id:
          type: integer
          description: The id of the copy kept for the credential - the keeper; stable only while the keeper rule gives the same copy (see the list).
        web_url:
          type: string
          description: The breach's page on your dashboard.
        status:
          type: string
          nullable: true
          enum: ["Needs Review", "Investigating", "Dismissed", "To Be Closed", "Closed", "Filtered by AI", null]
          description: The dashboard tab the breach is under; null when it is in none (an unrecognised legacy state).
        severity:
          type: string
          enum: [Critical, High, Medium, Low, Other]
          description: The summary's severity label, the one group_by=severity counts the breach under; Other for a severity ShadowMap has no name for.
        source:
          type: string
          enum: [Data Breach, Stealer Log, Other]
          description: Where the credential was found - the summary's source label, the one group_by=source counts the breach under; Other for a source ShadowMap has no name for.
        title:
          type: string
          nullable: true
          description: The breach's name, as its source publishes it.
        breach_date:
          type: string
          nullable: true
          description: The breach's date exactly as its source publishes it - month precision at best (2019-01), sometimes a bare year - never read as a time; null when none is stored.
        breach_type:
          type: string
          nullable: true
          description: What breach_value is, as stored - for example email, username or phone_number.
        breach_value:
          type: string
          nullable: true
          description: The leaked identifier - an e-mail address, a phone number or a username.
        domain:
          type: string
          nullable: true
          description: The domain the breach was matched on - one of your monitored domains, or the domain of an executive's address (the export's Email Domain).
        password:
          type: string
          nullable: true
          description: The password in full, exactly as stored - empty when the breach exposed none. A secret.
        actioned:
          type: boolean
          description: The legacy action flag, true when it is stored as Yes; on an untriaged breach it is what puts it under To Be Closed.
        is_executive:
          type: boolean
          description: True only when the breach is linked to one of your own tracked executives (VIP).
        executive_name:
          type: string
          nullable: true
          description: The name of your own tracked executive the breach is linked to; null when it is linked to none of yours (another company's executive, or one no longer tracked).
        custom_tags:
          type: object
          description: Your own tags, tag key to values. A breach without any has an empty object.
          additionalProperties:
            type: array
            items: { type: string }
        created_at:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap stored the breach - the module's discovery column; null when none is stored or it holds the zero date, and a breach with a null `created_at` is in no discovery window (see the list). Read as +05:30; the collector's clock is not verified (a UTC stamp would be 5 hours 30 minutes early).
        updated_at:
          type: string
          format: date-time
          nullable: true
          description: The record's last-updated time as stored; not every change moves it.
        takedown_requested_on:
          type: string
          format: date-time
          nullable: true
          description: When a takedown was requested for the breach on the dashboard; null when none was.
        first_seen_at:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap's breach collection first recorded the credential; null on breaches stored before it recorded one.
        last_seen_at:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap's breach collection last found the credential again; null on breaches stored before it recorded one.
    DiscussionList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/Discussion" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    DiscussionDetail:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/Discussion" }
    Discussion:
      type: object
      additionalProperties: false
      required: [id, web_url, status, liveness, source, category, title, description, url, keywords, published_on, ingested_on, created_at, updated_at, takedown_requested_on]
      properties:
        id: { type: integer }
        web_url:
          type: string
          description: The discussion's page on your dashboard.
        status:
          type: string
          nullable: true
          enum: ["Needs Review", "Investigating", "Legitimate Breach", "Dismissed", "To Be Closed", "Closed", "Filtered by AI", null]
          description: The dashboard tab the discussion is under; null when it is in none - marked taken down, filed for takedown on this module, or a code no tab names.
        liveness:
          type: string
          nullable: true
          description: The stored liveness the export calls "status" - online, takendown, or a legacy value as stored.
        source:
          type: string
          nullable: true
          description: The stored source folded to lower case, exactly the label group_by=source counts the discussion under (dwi_ransomware or dwi_threatfeed for a DWI feed row).
        category: { type: string, nullable: true }
        title:
          type: string
          description: The scraped title as the dashboard shows it (HTML entities decoded, double-encoded UTF-8 repaired).
        description:
          type: string
          description: The scraped description as the dashboard shows it (HTML entities decoded, double-encoded UTF-8 repaired).
        url:
          type: string
          nullable: true
          description: The discussion's identifier exactly as stored (often an .onion address, never rewritten through a gateway; a post number or a relative path as stored); null for a placeholder that means no URL.
        keywords:
          type: array
          items: { type: string }
          description: The terms the discussion matched, as the dashboard's list shows them - lower-cased and each once for a forum post, the feed's own list for a DWI feed row; empty when none.
        published_on:
          type: string
          format: date-time
          nullable: true
          description: The module's discovery column; null for the few discussions without one, which no discovery window returns.
        ingested_on:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap collected the discussion.
        created_at: { type: string, format: date-time, nullable: true }
        updated_at:
          type: string
          format: date-time
          nullable: true
          description: The record's last-updated time as stored; not every change moves it.
        takedown_requested_on:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap filed a takedown for the discussion; null when none was filed.
    CompromisedUserList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/CompromisedUser" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    CompromisedUserDetail:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/CompromisedUser" }
    CompromisedUser:
      type: object
      additionalProperties: false
      required: [id, web_url, status, compromise_type, is_executive, domain, sub_domain, url, user, password, hwid, application, malware_group, quality, is_valid_url, custom_tags, timestamp, seen_on, created_at, updated_at]
      properties:
        id: { type: integer }
        web_url:
          type: string
          description: The credential's page on your dashboard.
        status:
          type: string
          nullable: true
          enum: ["Needs Action", "Working Accounts", "Valid Users", "Action Taken", "To Be Closed", "Dismissed", "Mitigated", null]
          description: The dashboard tab the credential is under; null when it is in none (a code no tab names).
        compromise_type:
          type: string
          nullable: true
          enum: ["Others", "Corporate Assets", "Customers", "Corporate Users", "SaaS", null]
          description: The account type as the dashboard labels it; null for a code it has no label for.
        is_executive:
          type: boolean
          description: True when the credential is linked to one of your tracked executives (VIP).
        domain: { type: string, nullable: true }
        sub_domain: { type: string, nullable: true }
        url: { type: string, nullable: true }
        user:
          type: string
          nullable: true
          description: The username or e-mail address the credential logs in with.
        password:
          type: string
          nullable: true
          description: The password in full, exactly as stored (an empty one included). A secret.
        hwid:
          type: string
          nullable: true
          description: The hardware id of the computer the credential was stolen from - join it to the compromised computers' hwid for the computer; null when none is stored (blank, or the -1 placeholder).
        application:
          type: string
          nullable: true
          description: The application the credential was saved in, as the stealer log names it.
        malware_group:
          type: string
          nullable: true
          description: The stealer malware family or group the log came from, as stored.
        quality:
          type: string
          nullable: true
          description: The ingest's quality check of the credential (for example valid or partial); null where it never checked.
        is_valid_url:
          type: boolean
          nullable: true
          description: Whether the ingest found url valid; null where it never checked.
        custom_tags:
          type: object
          description: Your own tags, tag key to values (tags from before keys existed are under legacy_tag). A credential without any has an empty object.
          additionalProperties:
            type: array
            items: { type: string }
        timestamp:
          type: string
          nullable: true
          description: A token the ingest stores as text, returned exactly as stored and never read as a time; null when empty.
        seen_on:
          type: string
          format: date-time
          nullable: true
          description: When the credential was seen in a stealer log - the module's discovery column; null for the few credentials without one, which no discovery window returns. Read as +05:30; the ingest's clock is not verified (a UTC stamp would be 5 hours 30 minutes early).
        created_at:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap stored the credential (the export's Added On). Read as +05:30; the ingest stamps it in UTC, so it reads 5 hours 30 minutes early.
        updated_at:
          type: string
          format: date-time
          nullable: true
          description: When the credential last changed - every change to a field of this record moves it, and so does the ingest seeing the credential again; the column changed_since filters. Read as +05:30; a value the ingest wrote is stamped in UTC and reads 5 hours 30 minutes early (changed_since allows for it). Null when none is stored - such a credential is in no changed_since pull until it changes.
    CompromisedComputerList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          maxItems: 200
          items: { $ref: "#/components/schemas/CompromisedComputer" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    CompromisedComputerDetail:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/CompromisedComputer" }
    CompromisedComputer:
      type: object
      additionalProperties: false
      required: [id, web_url, status, is_executive, hwid, computer, hostname, user, domain, subdomains, ip, country, os, malware_group, malware_path, custom_tags, timestamp, breach_date, created_at, updated_at]
      properties:
        id: { type: integer }
        web_url:
          type: string
          description: The computer's page on your dashboard.
        status:
          type: string
          nullable: true
          enum: ["Needs Action", "Investigating", "Action Taken", "To Be Closed", "Dismissed", null]
          description: The dashboard tab the computer is under; null when it is in none (a code no tab names).
        is_executive:
          type: boolean
          description: True when the computer is linked to one of your tracked executives (VIP).
        hwid:
          type: string
          nullable: true
          description: The computer's hardware id - the key its credentials in compromised-users carry; null when unknown (blank or -1).
        computer:
          type: string
          nullable: true
          description: The computer's name; null when unknown (blank or -1).
        hostname:
          type: string
          nullable: true
          description: The computer's host name; null when unknown (blank or -1).
        user:
          type: string
          nullable: true
          description: The computer's user account; null when unknown (blank or -1).
        domain:
          type: string
          nullable: true
          description: The computer's network domain as the stealer log reports it; null when unknown (blank or -1).
        subdomains:
          type: array
          items: { type: string }
          description: |
            Lists every sub-domain of any monitored organisation's credentials found on this machine — not only your own;
            your own credentials for the machine are in Compromised Users (join by hwid). In the stored order, each entry as
            stored with its surrounding spaces removed; empty when none.
        ip:
          type: string
          nullable: true
          description: The computer's IP address; null when unknown (blank, -1 or 0.0.0.0).
        country:
          type: string
          nullable: true
          description: The computer's country code; null when unknown (blank, -1 or zz in any case).
        os:
          type: string
          nullable: true
          description: The computer's operating system; null when unknown (blank or -1).
        malware_group:
          type: string
          nullable: true
          description: The stealer malware family or group the log came from; null when unknown (blank or -1).
        malware_path:
          type: string
          nullable: true
          description: Where the stealer ran on the computer; null when unknown (blank or -1).
        custom_tags:
          type: object
          description: Your own tags, tag key to values (active tags only, sorted). A computer without any has an empty object.
          additionalProperties:
            type: array
            items: { type: string }
        timestamp:
          type: string
          nullable: true
          description: A token the ingest stores as text, returned exactly as stored and never read as a time; null when empty.
        breach_date:
          type: string
          format: date-time
          nullable: true
          description: When the stealer log was captured - the module's discovery column, always stored; null only if the stored value is not a valid time. Read as +05:30; the ingest's clock is not verified (a UTC stamp would be 5 hours 30 minutes early).
        created_at:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap stored the computer. Read as +05:30; the ingest stamps it in UTC, so it reads 5 hours 30 minutes early.
        updated_at:
          type: string
          format: date-time
          nullable: true
          description: When the computer last changed - every change to a field of this record moves it, your custom tags included, and so does the ingest seeing the computer again; the column changed_since filters. Read as +05:30; a value the ingest wrote is stamped in UTC and reads 5 hours 30 minutes early (changed_since allows for it). Null when none is stored - such a computer is in no changed_since pull until it changes.
    TelegramMessageList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/TelegramMessage" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    TelegramMessageDetail:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/TelegramMessage" }
    TelegramMessage:
      type: object
      additionalProperties: false
      required: [id, web_url, status, channel_name, channel_url, channel_nice_name, channel_description, keywords, message, cleaned_message, content, messaged_at, ingested_on, created_at, updated_at, takedown_requested_on]
      properties:
        id: { type: integer }
        web_url:
          type: string
          description: The message's page on your dashboard.
        status:
          type: string
          nullable: true
          enum: ["Needs Review", "Investigating", "Legitimate Breach", "Dismissed", "To Be Closed", "Closed", "Filtered by AI", null]
          description: The dashboard tab the message is under; null when it is in none (a code no tab names).
        channel_name:
          type: string
          nullable: true
          description: The channel's Telegram handle; null when none is stored.
        channel_url:
          type: string
          nullable: true
          description: The channel's address on Telegram (https://t.me/ and the handle); null when no handle is stored.
        channel_nice_name:
          type: string
          nullable: true
          description: The channel's display name; null when none is stored.
        channel_description:
          type: string
          nullable: true
          description: The channel's own description; null when none is stored.
        keywords:
          type: array
          items: { type: string }
          description: The terms the message matched, each once, in the order ShadowMap stored them; empty when none.
        message:
          type: string
          description: The message text, whole and exactly as stored. It can quote leaked material.
        cleaned_message:
          type: string
          description: The cleaned message text as ShadowMap stores it (the export's Cleaned Message), whole.
        content:
          type: string
          description: The message as ShadowMap stored it from Telegram, whole and exactly as stored (not parsed). It can quote leaked material.
        messaged_at:
          type: string
          format: date-time
          nullable: true
          description: When the message was posted - the module's discovery column; null for the few messages without one, which no discovery window returns.
        ingested_on:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap collected the message.
        created_at: { type: string, format: date-time, nullable: true }
        updated_at:
          type: string
          format: date-time
          nullable: true
          description: The record's last-updated time as stored; not every change moves it.
        takedown_requested_on:
          type: string
          format: date-time
          nullable: true
          description: When ShadowMap filed a takedown for the message; null when none was filed.
    SlaPolicyList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/SlaPolicy" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    SlaPolicyDetail:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/SlaPolicy" }
    SlaPolicy:
      type: object
      additionalProperties: false
      required: [id, web_url, name, description, status, escalation_enabled, module_types, escalations, activated_at, created_at]
      properties:
        id: { type: integer }
        web_url:
          type: string
          description: The policy's page on your dashboard (its settings form).
        name:
          type: string
          description: The policy's name, as stored. Names are not guaranteed unique - key on id.
        description:
          type: string
          nullable: true
          description: The policy's description, as stored; null when it has none.
        status:
          type: string
          nullable: true
          enum: ["Active", "Inactive", null]
          description: Active or Inactive, as the SLA Policies page labels the policy; null for a stored status neither label names. The sweep evaluates Active policies only.
        escalation_enabled:
          type: boolean
          description: Whether the policy escalates. When false its escalations are inert and every violation it raises is created already Closed.
        module_types:
          type: array
          description: Each module the policy covers, once, in type order; empty when none.
          items:
            type: object
            additionalProperties: false
            required: [type, label, evaluated]
            properties:
              type:
                type: integer
                description: The SLA module type number - the module_type of the policy's violations - kept as stored.
              label:
                type: string
                description: The module's name, as the SLA violations' module; Unknown for a type ShadowMap has no name for.
              evaluated:
                type: boolean
                description: False for every type the SLA sweep does not evaluate (for example IP Reputation, SSL Certificates or an Unknown type) - the policy raises no violation for that module.
        escalations:
          type: array
          description: The policy's escalation levels, in level order; listed even while escalation_enabled is false, and a level can be missing.
          items:
            type: object
            additionalProperties: false
            required: [level, offset, unit]
            properties:
              level:
                type: integer
                description: The escalation level, 1 to 4 - the level a violation's escalations reach.
              offset:
                type: integer
                description: How many units after the start of the day a violation was raised (IST) this level is due.
              unit:
                type: string
                nullable: true
                enum: ["days", "months", null]
                description: The unit of offset - days, or months (counted as the policies list describes); null for a unit ShadowMap does not define.
        activated_at:
          type: string
          format: date-time
          nullable: true
          description: The policy's watermark - a finding ShadowMap discovered before it raises no violation for the policy. Stamped when the policy is switched on from an alert template (older policies were given their creation time); null when the policy has no such limit, as for one created in the dashboard.
        created_at:
          type: string
          format: date-time
          nullable: true
          description: When the policy was created - the module's discovery column; null for a policy without one, which no discovery window returns.
    SlaViolationList:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/SlaViolation" }
        meta: { $ref: "#/components/schemas/ListMeta" }
    SlaViolationDetail:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/SlaViolation" }
    SlaViolation:
      type: object
      additionalProperties: false
      required: [id, web_url, status, module_type, module, finding_id, policy_id, created_at, updated_at, escalation_level, escalations]
      properties:
        id: { type: integer }
        web_url:
          type: string
          description: The dashboard tab the violation is listed under (the dashboard has no page for one violation).
        status:
          type: string
          nullable: true
          enum: ["Open", "Escalated", "Closed", null]
          description: The dashboard tab the violation is under - Escalated is an open violation escalated at least once; null when it is in none (a stored status no tab names).
        module_type:
          type: integer
          description: The violated finding's module as its SLA type number, kept as stored - the type in the policy's module_types.
        module:
          type: string
          enum: ["Exposures", "Alerts", "Stealer Log", "Ransomware Group & Forum", "Docker Container", "Domain Squatting", "Phishing & Impersonations", "Leaked APIs", "Leaked File", "Code Repositories", "Mobile Application", "Social Media Monitoring", "S3 Bucket", "Open Ports", "IP Reputation", "Telegram Conversations", "Software As A Service", "Credit Card Leaks", "SSL Certificates", "Malware Compromised Users", "Malware Compromised Computers", "Data Breaches", "Elastic Search Instances", "Executive Leak", "AI Domains", "Vendor Risk Management", "Subdomains", "Leaked Credentials", "Exposed Database", "Executive Monitoring", "Tracker", "Internal Host", "IP Address", "Shortener URL", "Stack Overflow", "Campaign", "CVE", "IOC Search", "Malware", "Ransomware", "Threat Actor", "Threat Feed", "App Misconfiguration", "Defacement", "Network Service", "Technology Stack", "SSO Login Page", "Malware Compromised Cookies", "Malware Compromised Autofills", "Malware Compromised Cards", "Malware Compromised Wallets", "Malware Compromised Tokens", "Malware Compromised Browser History", "Unknown"]
          description: The module's name - the summary's module label; Unknown for a type ShadowMap has no name for.
        finding_id:
          type: integer
          description: The violated finding's id in its module; the list's description names the endpoint each module_type joins to.
        policy_id:
          type: integer
          description: The policy that raised the violation - its record, with its name and escalation levels, is in /api/v1/sla/policies.
        created_at:
          type: string
          format: date-time
          nullable: true
          description: The clock start - when ShadowMap's SLA sweep first matched the finding - and the module's discovery column.
        updated_at:
          type: string
          format: date-time
          nullable: true
          description: When the violation last changed - set when it is raised, moved when it is closed, escalated to a level or re-pointed to another finding (finding_id), never when its policy is edited; the column changed_since filters. An escalation recorded before this change feed was released did not move it, so begin with one full pull.
        escalation_level:
          type: integer
          nullable: true
          description: The highest escalation level the violation reached; null when it was never escalated.
        escalations:
          type: array
          description: The levels the violation reached, each once, in level order; empty when it was never escalated. A level the policy no longer defines is still listed.
          items:
            type: object
            additionalProperties: false
            required: [level, escalated_at]
            properties:
              level: { type: integer }
              escalated_at:
                type: string
                format: date-time
                nullable: true
                description: When the level was first reached; null when no time was recorded.
