{
  "openapi": "3.0.1",
  "info": {
    "title": "PortWall management API",
    "description": "The documented part of PortWall's management API. Send an API key in the X-Api-Key header. On Windows the API answers only on the server itself, at http://127.0.0.1:5000. Documented endpoints may gain fields and optional parameters in any release; removing or renaming anything documented is announced in the release notes at least 60 days ahead.",
    "version": "1"
  },
  "paths": {
    "/api/allowlist": {
      "get": {
        "tags": [
          "Allowlist"
        ],
        "summary": "List the allowlist",
        "description": "Needs a Viewer or Admin key.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AllowlistEntry"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Viewer"
      },
      "post": {
        "tags": [
          "Allowlist"
        ],
        "summary": "Allowlist an address or range",
        "description": "Needs an Admin key.\n\n`scope` is `Waf` (never blocked by the WAF or by blocks), `Feeds` (never blocked by threat-intelligence feeds) or `Both`.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AllowlistRequest"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AllowlistEntry"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/allowlist/{ipOrCidr}": {
      "delete": {
        "tags": [
          "Allowlist"
        ],
        "summary": "Remove an allowlist entry",
        "description": "Needs an Admin key.\n\nGive the address or range exactly as it was added; a range needs no escaping, for example `/api/allowlist/198.51.100.0/24`.",
        "parameters": [
          {
            "name": "ipOrCidr",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "No Content"
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/api-keys": {
      "get": {
        "tags": [
          "API keys"
        ],
        "summary": "List API keys",
        "description": "Needs an Admin key.\n\nEvery key made on the API Keys page or with this API, newest first. The key itself is never shown again; `prefix` identifies it.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ApiKeyView"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      },
      "post": {
        "tags": [
          "API keys"
        ],
        "summary": "Create an API key",
        "description": "Needs an Admin key.\n\n`role` is Admin, Viewer or Tenant; a Tenant key also needs `tenantId`. The reply's `key` is shown this once only.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/CreateApiKeyRequest"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyCreated"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/api-keys/{id}/revoke": {
      "post": {
        "tags": [
          "API keys"
        ],
        "summary": "Revoke an API key",
        "description": "Needs an Admin key.\n\nThe key stops working at once.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResult"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/events": {
      "get": {
        "tags": [
          "Attacks"
        ],
        "summary": "List attacks, a page at a time",
        "description": "Needs a Viewer or Admin key.\n\nNewest first. `take` is up to 200 a page and `skip` moves through them. `q` matches the address, the site, the kind of attack or the rule ID. `since` (an ISO 8601 time, for example 2026-10-27T00:00:00Z) returns only attacks at or after it, so a script can fetch what is new since its last run.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "skip",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "take",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResultOfSecurityEventSummary"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Viewer"
      }
    },
    "/api/audit": {
      "get": {
        "tags": [
          "Attacks"
        ],
        "summary": "Read the audit log",
        "description": "Needs an Admin key.\n\nWho changed what, newest first (the latest 200).",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AuditEntry"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/campaigns": {
      "get": {
        "tags": [
          "Attacks"
        ],
        "summary": "List attack campaigns",
        "description": "Needs a Viewer or Admin key.\n\nAttackers seen hitting many different sites on this installation, newest first (up to 200).",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Campaign"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Viewer"
      }
    },
    "/api/campaigns/{id}": {
      "get": {
        "tags": [
          "Attacks"
        ],
        "summary": "Get one attack campaign",
        "description": "Needs a Viewer or Admin key.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Campaign"
                }
              }
            }
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Viewer"
      }
    },
    "/api/config/export": {
      "get": {
        "tags": [
          "Backup"
        ],
        "summary": "Download the configuration",
        "description": "Needs an Admin key.\n\nThe whole configuration as one JSON file, the same as Download backup on the Backup & Restore page. Its content is for `POST /api/config/import` to read back; its layout is not part of the documented API and changes between releases.",
        "responses": {
          "200": {
            "description": "OK"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/config/import": {
      "post": {
        "tags": [
          "Backup"
        ],
        "summary": "Restore a configuration",
        "description": "Needs an Admin key.\n\nSend `{ \"dryRun\": true, \"skipHostSpecific\": false, \"bundle\": <the exported file> }`. Try `dryRun` first: it reports what would change and changes nothing. A licence key is not restored onto a different machine.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConfigImportBody"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConfigImportResult"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/blocks": {
      "get": {
        "tags": [
          "Blocking"
        ],
        "summary": "List blocked addresses",
        "description": "Needs a Viewer or Admin key.\n\nEvery address blocked now, by any part of PortWall: permanent blocks first, then the newest. `expiresAt` is empty for a permanent block.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/BlockDto"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Viewer"
      },
      "post": {
        "tags": [
          "Blocking"
        ],
        "summary": "Block an address",
        "description": "Needs an Admin key.\n\nBlocks an IPv4 or IPv6 address for `ttlMinutes` (or `ttlHours`), up to a year, or until removed when `permanent` is true. Takes effect within moments. An allowlisted address cannot be blocked (409).",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/CreateBlockRequest"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlockDto"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/blocks/{ip}": {
      "delete": {
        "tags": [
          "Blocking"
        ],
        "summary": "Unblock an address",
        "description": "Needs an Admin key.\n\nRemoves the block on this address, whatever created it. The firewall releases it within moments.",
        "parameters": [
          {
            "name": "ip",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "No Content"
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/blocks/flush-temporary": {
      "post": {
        "tags": [
          "Blocking"
        ],
        "summary": "Remove all temporary blocks",
        "description": "Needs an Admin key.\n\nRemoves every block that has an expiry, for example after fixing the cause of a wave of automatic blocks. Permanent blocks are kept.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlushTemporaryResult"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/bot-protection": {
      "get": {
        "tags": [
          "Bot protection"
        ],
        "summary": "Get the bot-protection settings",
        "description": "Needs a Viewer or Admin key.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BotProtectionView"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Viewer"
      },
      "put": {
        "tags": [
          "Bot protection"
        ],
        "summary": "Change the bot-protection settings",
        "description": "Needs an Admin key.\n\nFields left out keep their current value, except `underAttackSites` and `exemptPaths`, which are replaced (empty when left out).",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/BotProtectionDto"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettingsSaved"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/bruteforce/settings": {
      "get": {
        "tags": [
          "Brute-force protection"
        ],
        "summary": "Get the brute-force settings",
        "description": "Needs a Viewer or Admin key.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BruteForceSettings"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Viewer"
      },
      "put": {
        "tags": [
          "Brute-force protection"
        ],
        "summary": "Change the brute-force settings",
        "description": "Needs an Admin key.\n\nSend the whole settings object, as `GET` returns it, with your changes.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/BruteForceSettings"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BruteForceSettingsSaved"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/geo": {
      "get": {
        "tags": [
          "Geo-blocking"
        ],
        "summary": "List geo-blocking policies",
        "description": "Needs a Viewer or Admin key.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/GeoBlockPolicy"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Viewer"
      }
    },
    "/api/geo/settings": {
      "put": {
        "tags": [
          "Geo-blocking"
        ],
        "summary": "Turn geo-blocking on or off",
        "description": "Needs an Admin key.\n\nThe master switch. When it is off no policy is enforced.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/GeoSubsystemUpdate"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SwitchState"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/geo/search-engines": {
      "put": {
        "tags": [
          "Geo-blocking"
        ],
        "summary": "Let search engines through geo blocking",
        "description": "Needs an Admin key.\n\nOn by default. The crawlers of Google, Bing, Apple and DuckDuckGo are recognised by the address ranges those search engines publish, and are not blocked by any country policy, so the sites stay in search results. Turn it off to block them like any other visitor from a blocked country.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/GeoSearchEnginesUpdate"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchEnginesState"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/geo/{id}": {
      "get": {
        "tags": [
          "Geo-blocking"
        ],
        "summary": "Get one geo-blocking policy",
        "description": "Needs a Viewer or Admin key.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GeoBlockPolicy"
                }
              }
            }
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Viewer"
      },
      "put": {
        "tags": [
          "Geo-blocking"
        ],
        "summary": "Create or change a geo-blocking policy",
        "description": "Needs an Admin key.\n\n`countries` are two-letter ISO codes. `mode` Audit only records matching traffic; Enforce blocks it. Set `letCertificateChecksThrough` so certificate renewals keep working.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/GeoPolicyUpdate"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GeoBlockPolicy"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      },
      "delete": {
        "tags": [
          "Geo-blocking"
        ],
        "summary": "Delete a geo-blocking policy",
        "description": "Needs an Admin key.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "No Content"
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/notifications": {
      "get": {
        "tags": [
          "Notifications"
        ],
        "summary": "Get the notification settings",
        "description": "Needs a Viewer or Admin key.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotificationPreferences"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Viewer"
      },
      "put": {
        "tags": [
          "Notifications"
        ],
        "summary": "Change the notification settings",
        "description": "Needs an Admin key.\n\nSend the whole settings object, as `GET` returns it, with your changes.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/NotificationPreferences"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotificationPreferences"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/sites/{name}/egress": {
      "get": {
        "tags": [
          "Outbound control"
        ],
        "summary": "Get a site's outbound (egress) policy",
        "description": "Needs a Viewer or Admin key.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EgressPolicy"
                }
              }
            }
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Viewer"
      },
      "put": {
        "tags": [
          "Outbound control"
        ],
        "summary": "Set a site's outbound (egress) policy",
        "description": "Needs an Admin key.\n\nLimits what the site's application may connect out to: only `allowedRemotes` (addresses or ranges). Start with `mode` Audit, which only records, then Enforce.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/EgressPolicyUpdate"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EgressPolicy"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      },
      "delete": {
        "tags": [
          "Outbound control"
        ],
        "summary": "Remove a site's outbound (egress) policy",
        "description": "Needs an Admin key.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "No Content"
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/sites": {
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "List sites, a page at a time",
        "description": "Needs a Viewer or Admin key.\n\n`q` filters by name. Pages hold up to 200 (`take`); `skip` moves through them.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "skip",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "take",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResultOfSitePolicy"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Viewer"
      }
    },
    "/api/sites/all": {
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "List every site",
        "description": "Needs a Viewer or Admin key.\n\n`mode` is Off, DetectionOnly (attacks are logged, nothing is blocked) or On (blocking).",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SitePolicy"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Viewer"
      }
    },
    "/api/sites/mode": {
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Switch sites between watching and blocking",
        "description": "Needs an Admin key.\n\nPuts the sites named in `sites` (or every site, when `sites` is left out) in `mode`: Off, DetectionOnly or On.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/BulkSiteModeUpdate"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteModeResult"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/sites/{name}": {
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "Get one site's settings",
        "description": "Needs a Viewer or Admin key.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitePolicy"
                }
              }
            }
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Viewer"
      },
      "put": {
        "tags": [
          "Sites"
        ],
        "summary": "Change one site's settings",
        "description": "Needs an Admin key.\n\nSend the whole settings object, as `GET /api/sites/{name}` returns it, with your changes. Each save is kept as a revision you can roll back to.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SitePolicyUpdate"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitePolicySaved"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      },
      "delete": {
        "tags": [
          "Sites"
        ],
        "summary": "Remove a site from PortWall",
        "description": "Needs an Admin key.\n\nRemoves PortWall's settings for the site. It does not touch the site itself.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "No Content"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteDeletedWithWarning"
                }
              }
            }
          },
          "404": {
            "description": "Not Found"
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/sites/{name}/revisions": {
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "List a site's saved revisions",
        "description": "Needs a Viewer or Admin key.\n\nThe latest 50, newest first.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SitePolicyRevision"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Viewer"
      }
    },
    "/api/sites/{name}/rollback/{version}": {
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Roll a site back to a saved revision",
        "description": "Needs an Admin key.\n\nRestores that revision's settings and records the result as a new revision.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "version",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RollbackResult"
                }
              }
            }
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/health": {
      "get": {
        "tags": [
          "Status"
        ],
        "summary": "Check that the API is answering",
        "description": "No key needed.\n\nAnswers `ok` while the management service is running. It says nothing about protection: use `/api/protection-status` for that.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthView"
                }
              }
            }
          }
        },
        "x-portwall-role": "none"
      }
    },
    "/api/protection-status": {
      "get": {
        "tags": [
          "Status"
        ],
        "summary": "Check that protection is working",
        "description": "Needs a Viewer or Admin key.\n\nThe same checks as the Overview's Protection status cards. `allHealthy` is true when every protection that is turned on is enforcing; `problems` lists those that are on but not working, each with `fix`.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProtectionStatusView"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Viewer"
      }
    },
    "/api/license": {
      "get": {
        "tags": [
          "Status"
        ],
        "summary": "Check the licence",
        "description": "Needs a Viewer or Admin key.\n\n`state` is Licensed, Grace or Unlicensed. While Unlicensed, `enforcementActive` is false: attacks are still detected and recorded, but nothing is blocked until the licence is renewed.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LicenseView"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Viewer"
      }
    },
    "/api/license/key": {
      "post": {
        "tags": [
          "Status"
        ],
        "summary": "Enter or replace the licence key",
        "description": "Needs an Admin key.\n\nSaves the key and asks for a licence check at once, so the new state shows within moments on `GET /api/license`.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SetLicenseKeyRequest"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetLicenseKeyResult"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/config-apply/status": {
      "get": {
        "tags": [
          "Status"
        ],
        "summary": "Wait for a change to be in force",
        "description": "Needs a Viewer or Admin key.\n\nMany changes are saved at once and applied in the background; their reply carries `applySeq`. The change is in force when `completed` here reaches that number. `outcome` is applied, deferred (saved, taken up as soon as it can be) or failed, with `message` saying why.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConfigApplyStatus"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Viewer"
      }
    },
    "/api/my/reports/security": {
      "get": {
        "tags": [
          "Tenant API"
        ],
        "summary": "Get a security report for the tenant's sites",
        "description": "Needs a Tenant key; answers only for that tenant's sites.\n\nWhat was attacked and blocked on the tenant's own sites over the last `days`.",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "description": "How many days to cover, 1 to 90. Default 7.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "`text` for a plain-text summary instead of JSON.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SecurityReport"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Tenant"
      }
    },
    "/api/my/sites": {
      "get": {
        "tags": [
          "Tenant API"
        ],
        "summary": "List the tenant's sites",
        "description": "Needs a Tenant key; answers only for that tenant's sites.\n\nEvery site the tenant owns or has been given, with `role`: Viewer may only look, Manager may change settings.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SiteAccess"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Tenant"
      }
    },
    "/api/my/sites/{name}": {
      "get": {
        "tags": [
          "Tenant API"
        ],
        "summary": "Get one of the tenant's sites",
        "description": "Needs a Tenant key; answers only for that tenant's sites.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteAccess"
                }
              }
            }
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Tenant"
      },
      "put": {
        "tags": [
          "Tenant API"
        ],
        "summary": "Change one of the tenant's sites",
        "description": "Needs a Tenant key; answers only for that tenant's sites.\n\nSend the whole settings object, as `GET` returns it under `site`, with your changes. The host's tenant policy (`GET /api/my/policy`) sets what may change; a refused change answers 400 with the reasons.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SitePolicyUpdate"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteAccess"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TenantErrors"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Tenant"
      }
    },
    "/api/my/policy": {
      "get": {
        "tags": [
          "Tenant API"
        ],
        "summary": "Get what the host lets the tenant change",
        "description": "Needs a Tenant key; answers only for that tenant's sites.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TenantSelfServicePolicy"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Tenant"
      }
    },
    "/api/my/sites/{name}/scan": {
      "post": {
        "tags": [
          "Tenant API"
        ],
        "summary": "Scan a site's files for malware",
        "description": "Needs a Tenant key; answers only for that tenant's sites.\n\nStarts a scan of the site's files. The host's policy decides whether scans are allowed and how often; when it is too soon, the reply says when to try again.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScanStarted"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RetryLater"
                }
              }
            }
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Tenant"
      }
    },
    "/api/my/sites/{name}/scans": {
      "get": {
        "tags": [
          "Tenant API"
        ],
        "summary": "List a site's scans",
        "description": "Needs a Tenant key; answers only for that tenant's sites.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResultOfScanRun"
                }
              }
            }
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Tenant"
      }
    },
    "/api/my/quarantine": {
      "get": {
        "tags": [
          "Tenant API"
        ],
        "summary": "List the tenant's quarantined files",
        "description": "Needs a Tenant key; answers only for that tenant's sites.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResultOfQuarantineEntry"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Tenant"
      }
    },
    "/api/my/quarantine/{sha}/request": {
      "post": {
        "tags": [
          "Tenant API"
        ],
        "summary": "Restore or delete a quarantined file",
        "description": "Needs a Tenant key; answers only for that tenant's sites.\n\nDepending on the host's policy this is done at once (200) or sent to the host for approval (202).",
        "parameters": [
          {
            "name": "sha",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/QuarantineRequestDto"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuarantineDone"
                }
              }
            }
          },
          "202": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuarantineRequested"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Tenant"
      }
    },
    "/api/my/quarantine/{sha}/content": {
      "get": {
        "tags": [
          "Tenant API"
        ],
        "summary": "Look at a quarantined file's content",
        "description": "Needs a Tenant key; answers only for that tenant's sites.",
        "parameters": [
          {
            "name": "sha",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuarantineContent"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Tenant"
      }
    },
    "/api/my/events": {
      "get": {
        "tags": [
          "Tenant API"
        ],
        "summary": "List recent attacks on the tenant's sites",
        "description": "Needs a Tenant key; answers only for that tenant's sites.\n\nThe latest 200, newest first.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SecurityEventSummary"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Tenant"
      }
    },
    "/api/tenant-policy": {
      "get": {
        "tags": [
          "Tenants"
        ],
        "summary": "Get the tenant policy",
        "description": "Needs an Admin key.\n\nWhat every tenant may change on its own sites, unless overridden for that tenant.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TenantSelfServicePolicy"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      },
      "put": {
        "tags": [
          "Tenants"
        ],
        "summary": "Change the tenant policy",
        "description": "Needs an Admin key.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/TenantSelfServicePolicy"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TenantSelfServicePolicy"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/tenant-policy/overrides": {
      "get": {
        "tags": [
          "Tenants"
        ],
        "summary": "List per-tenant policy overrides",
        "description": "Needs an Admin key.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/TenantPolicyOverride"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/tenant-policy/overrides/{tenantId}": {
      "get": {
        "tags": [
          "Tenants"
        ],
        "summary": "Get one tenant's policy override",
        "description": "Needs an Admin key.",
        "parameters": [
          {
            "name": "tenantId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TenantPolicyOverride"
                }
              }
            }
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      },
      "put": {
        "tags": [
          "Tenants"
        ],
        "summary": "Set one tenant's policy override",
        "description": "Needs an Admin key.",
        "parameters": [
          {
            "name": "tenantId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/TenantPolicyOverride"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TenantPolicyOverride"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      },
      "delete": {
        "tags": [
          "Tenants"
        ],
        "summary": "Remove one tenant's policy override",
        "description": "Needs an Admin key.\n\nThe tenant falls back to the tenant policy.",
        "parameters": [
          {
            "name": "tenantId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "No Content"
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/sites/{name}/owner": {
      "put": {
        "tags": [
          "Tenants"
        ],
        "summary": "Give a site to a tenant",
        "description": "Needs an Admin key.\n\nSets which tenant owns the site, so that tenant's Tenant keys and panel users can see and manage it.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AssignOwnerRequest"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "No Content"
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/sites/{name}/grants": {
      "get": {
        "tags": [
          "Tenants"
        ],
        "summary": "List who else may use a site",
        "description": "Needs an Admin key.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/UserSiteGrant"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      },
      "put": {
        "tags": [
          "Tenants"
        ],
        "summary": "Let a panel user see or manage a site",
        "description": "Needs an Admin key.\n\n`role` Viewer may only look; Manager may also change the site's settings within the tenant policy.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/GrantRequest"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "No Content"
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/sites/{name}/grants/{userId}": {
      "delete": {
        "tags": [
          "Tenants"
        ],
        "summary": "Take away a panel user's access to a site",
        "description": "Needs an Admin key.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "No Content"
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/feeds": {
      "get": {
        "tags": [
          "Threat-intelligence feeds"
        ],
        "summary": "List the feeds and their state",
        "description": "Needs a Viewer or Admin key.\n\n`subsystemEnabled` is the master switch: when it is off, no feed is fetched whatever its own setting. `enforcedRanges` is how many address ranges the feeds are blocking now.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeedsView"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Viewer"
      }
    },
    "/api/feeds/settings": {
      "put": {
        "tags": [
          "Threat-intelligence feeds"
        ],
        "summary": "Turn the feeds on or off",
        "description": "Needs an Admin key.\n\nThe master switch. When it is off no feed is fetched or enforced, whatever each feed's own setting.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/FeedSubsystemUpdate"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SwitchState"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/feeds/{id}": {
      "put": {
        "tags": [
          "Threat-intelligence feeds"
        ],
        "summary": "Turn one feed on or off, or change its action",
        "description": "Needs an Admin key.\n\n`action` is Drop (blocked at the firewall), Challenge (sent to the browser check) or DetectionOnly (recorded only).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/FeedUpdate"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeedStatus"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/sites/{name}/patches": {
      "get": {
        "tags": [
          "Virtual patches"
        ],
        "summary": "List a site's own virtual patches",
        "description": "Needs a Viewer or Admin key.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/VirtualPatch"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Viewer"
      },
      "post": {
        "tags": [
          "Virtual patches"
        ],
        "summary": "Add a virtual patch to a site",
        "description": "Needs an Admin key.\n\nStart a new patch in `stage` DetectionOnly, check the attack feed for what it matches, then switch it to Blocking.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/VirtualPatchRequest"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VirtualPatch"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/sites/{name}/patches/{ruleId}/stage": {
      "put": {
        "tags": [
          "Virtual patches"
        ],
        "summary": "Switch a virtual patch between detecting and blocking",
        "description": "Needs an Admin key.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ruleId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/PatchStageRequest"
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "No Content"
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    },
    "/api/sites/{name}/patches/{ruleId}": {
      "delete": {
        "tags": [
          "Virtual patches"
        ],
        "summary": "Delete a virtual patch",
        "description": "Needs an Admin key.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ruleId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "No Content"
          },
          "404": {
            "description": "Not Found"
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-portwall-role": "Admin"
      }
    }
  },
  "components": {
    "schemas": {
      "AllowScope": {
        "enum": [
          "None",
          "Waf",
          "Feeds",
          "Both"
        ],
        "type": "string",
        "description": "What an allowlist entry exempts the address or range from.\n\n- `None`: Exempt from nothing. Not accepted when adding an entry; use Waf, Feeds or Both.\n- `Waf`: Never blocked by PortWall's own detection, automatic blocks, geo-blocking or manual blocks.\n- `Feeds`: Never blocked because a threat-intelligence feed lists it.\n- `Both`: Exempt from both the WAF side and the feeds.",
        "x-enum-descriptions": {
          "None": "Exempt from nothing. Not accepted when adding an entry; use Waf, Feeds or Both.",
          "Waf": "Never blocked by PortWall's own detection, automatic blocks, geo-blocking or manual blocks.",
          "Feeds": "Never blocked because a threat-intelligence feed lists it.",
          "Both": "Exempt from both the WAF side and the feeds."
        }
      },
      "AllowlistEntry": {
        "required": [
          "actor",
          "addedAt",
          "ipOrCidr",
          "reason",
          "scope"
        ],
        "type": "object",
        "properties": {
          "ipOrCidr": {
            "type": "string",
            "description": "The IP address, or range in CIDR form such as 198.51.100.0/24, that is exempt. Also the key used to remove the entry."
          },
          "scope": {
            "allOf": [
              {
                "$ref": "#/components/schemas/AllowScope"
              }
            ],
            "description": "What the address is exempt from: Waf, Feeds or Both."
          },
          "reason": {
            "type": "string",
            "description": "Why it was added, for whoever reviews the list later. Empty if none was given."
          },
          "actor": {
            "type": "string",
            "description": "Who added the entry."
          },
          "addedAt": {
            "type": "string",
            "description": "When the entry was added.",
            "format": "date-time"
          }
        },
        "description": "One allowlist entry: an address or range that PortWall will not block, what it is exempt from, and who added it."
      },
      "AllowlistRequest": {
        "required": [
          "ipOrCidr",
          "scope"
        ],
        "type": "object",
        "properties": {
          "ipOrCidr": {
            "type": "string",
            "description": "The IP address, or range in CIDR form such as 198.51.100.0/24, that is exempt. Also the key used to remove the entry."
          },
          "scope": {
            "allOf": [
              {
                "$ref": "#/components/schemas/AllowScope"
              }
            ],
            "description": "What the address is exempt from: Waf, Feeds or Both."
          },
          "reason": {
            "type": "string",
            "description": "Why it was added, for whoever reviews the list later. Empty if none was given.",
            "nullable": true
          }
        },
        "description": "The request body for adding an address or range to the allowlist."
      },
      "ApiError": {
        "required": [
          "error"
        ],
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "What went wrong and what to change, in words."
          }
        },
        "description": "An error the caller can fix, in words a person can act on."
      },
      "ApiKeyCreated": {
        "required": [
          "id",
          "key",
          "name",
          "prefix",
          "role"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The key's identifier, used to revoke it."
          },
          "name": {
            "type": "string",
            "description": "The name given to the key."
          },
          "role": {
            "type": "string",
            "description": "What the key may do: Admin, Viewer or Tenant."
          },
          "tenantId": {
            "type": "string",
            "description": "For a Tenant key, the tenant it is limited to. Null for other keys.",
            "nullable": true
          },
          "prefix": {
            "type": "string",
            "description": "The first characters of the key, which also show in the key list."
          },
          "key": {
            "type": "string",
            "description": "The full key. It is shown this once only and cannot be recovered; copy it now or create another."
          }
        },
        "description": "A newly made API key, including the key itself, which is shown this once only."
      },
      "ApiKeyView": {
        "required": [
          "createdAt",
          "id",
          "name",
          "prefix",
          "revoked",
          "role"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The key's identifier, used to revoke it."
          },
          "name": {
            "type": "string",
            "description": "The name given to the key when it was created."
          },
          "role": {
            "type": "string",
            "description": "What the key may do: Admin (read and change), Viewer (read only) or Tenant (one tenant's own sites)."
          },
          "tenantId": {
            "type": "string",
            "description": "For a Tenant key, the tenant it is limited to. Null for other keys.",
            "nullable": true
          },
          "prefix": {
            "type": "string",
            "description": "The first characters of the key, to tell keys apart. It cannot be used to sign in."
          },
          "createdAt": {
            "type": "string",
            "description": "When the key was created.",
            "format": "date-time"
          },
          "lastUsedAt": {
            "type": "string",
            "description": "When the key was last used. Null if it never has been.",
            "format": "date-time",
            "nullable": true
          },
          "revoked": {
            "type": "boolean",
            "description": "True when the key has been revoked and no longer works."
          }
        },
        "description": "An API key as listed. The key itself is never shown again; only its first characters are."
      },
      "AssignOwnerRequest": {
        "type": "object",
        "properties": {
          "tenantId": {
            "type": "string",
            "description": "The tenant that should own the site. Null or empty removes the site's owner.",
            "nullable": true
          }
        },
        "description": "Request body that sets which tenant owns a site."
      },
      "AuditEntry": {
        "required": [
          "action",
          "actor",
          "target",
          "timestamp"
        ],
        "type": "object",
        "properties": {
          "timestamp": {
            "type": "string",
            "description": "When the change was made.",
            "format": "date-time"
          },
          "actor": {
            "type": "string",
            "description": "Who made the change: the user or API key, with the signed-in person noted when the console made the call for them."
          },
          "action": {
            "type": "string",
            "description": "What was done, as a short code such as block.create or allowlist.add."
          },
          "target": {
            "type": "string",
            "description": "What the change was made to, such as an IP address, a site or a feed ID."
          },
          "details": {
            "type": "string",
            "description": "Extra detail about the change, such as the duration or the new setting. Null when there is none.",
            "nullable": true
          }
        },
        "description": "One entry in the audit log: a change someone made to PortWall's configuration."
      },
      "BlockDto": {
        "required": [
          "backend",
          "category",
          "createdAt",
          "ip",
          "reason"
        ],
        "type": "object",
        "properties": {
          "ip": {
            "type": "string",
            "description": "The blocked IP address, or the range when the block covers a range."
          },
          "reason": {
            "type": "string",
            "description": "Why the block exists: the text given when it was created by hand, or a short explanation for an automatic block."
          },
          "backend": {
            "type": "string",
            "description": "Where the block is enforced. WindowsFirewall (Windows) and Nftables (Linux appliance) drop the traffic at the network layer before it reaches the engine. SoftBlock refuses it in the engine, so the request still arrives there. IPSec is also possible where it has been set up."
          },
          "createdAt": {
            "type": "string",
            "description": "When the block was created.",
            "format": "date-time"
          },
          "expiresAt": {
            "type": "string",
            "description": "When the block ends and the address is released. Empty for a permanent block, which stands until someone removes it.",
            "format": "date-time",
            "nullable": true
          },
          "category": {
            "type": "string",
            "description": "What created the block: Manual (someone added it), CorrelatedRisk (the address's attack score crossed the limit), RateLimit, ThreatIntel, Honeypot or BruteForce (repeated failed logins)."
          },
          "country": {
            "type": "string",
            "description": "Two-letter ISO country code of the address. Null when the country database does not know it.",
            "nullable": true
          },
          "actor": {
            "type": "string",
            "description": "Who created the block, for a manual block. Null for blocks PortWall created automatically.",
            "nullable": true
          }
        },
        "description": "One address (or range) that PortWall is blocking now, with why, where it is enforced and when it ends."
      },
      "BotCrawlerSourceView": {
        "required": [
          "id",
          "label",
          "ranges"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "A short name for the search engine's list."
          },
          "label": {
            "type": "string",
            "description": "The search engine's name as shown in the console."
          },
          "fetchedAt": {
            "type": "string",
            "description": "When PortWall last downloaded this list. Null until the first download.",
            "format": "date-time",
            "nullable": true
          },
          "ranges": {
            "type": "integer",
            "description": "How many address ranges the list holds. 0 until it has been downloaded.",
            "format": "int32"
          },
          "error": {
            "type": "string",
            "description": "Why the last download failed, in words. Null when it worked or has not been tried; the previous list is kept after a failure.",
            "nullable": true
          }
        },
        "description": "One search engine's published list of crawler addresses and when PortWall last downloaded it."
      },
      "BotCrawlersView": {
        "required": [
          "sources",
          "total"
        ],
        "type": "object",
        "properties": {
          "sources": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BotCrawlerSourceView"
            },
            "description": "The search engines whose crawler addresses are never checked, each with its list status."
          },
          "total": {
            "type": "integer",
            "description": "The total number of crawler address ranges held across all lists.",
            "format": "int32"
          }
        },
        "description": "The lists of search-engine crawler addresses that the browser check never challenges."
      },
      "BotProtectionDto": {
        "required": [
          "ja4",
          "underAttackAll"
        ],
        "type": "object",
        "properties": {
          "underAttackAll": {
            "type": "boolean",
            "description": "True to show the browser check to every visitor of every site."
          },
          "underAttackSites": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Names of the sites whose visitors are all shown the browser check. Use names from sites. Not needed when underAttackAll is true.",
            "nullable": true
          },
          "exemptPaths": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Paths the browser check never applies to, on every site, for example /api/ or /webhooks/ (API clients and webhooks cannot run the check). Each starts with /, and has no spaces, quotes, braces, # or backslashes. /.well-known/ is always left out.",
            "nullable": true
          },
          "difficulty": {
            "type": "integer",
            "description": "How much work the browser check asks of a visitor's browser, from 16 (normal) to 22 (strongest). Default 18.",
            "format": "int32",
            "nullable": true
          },
          "clearanceMinutes": {
            "type": "integer",
            "description": "How long, in minutes, a visitor who passed the check is left alone, from 5 to 1440 (24 hours). Default 30.",
            "format": "int32",
            "nullable": true
          },
          "ja4": {
            "type": "boolean",
            "description": "True to record each visitor's TLS fingerprint, which lets PortWall recognise one bot that uses many addresses. Default false."
          },
          "botClusterAction": {
            "allOf": [
              {
                "$ref": "#/components/schemas/DetectionAction"
              }
            ],
            "description": "What happens to the addresses of one bot spread over many addresses once it is detected. Default Challenge.",
            "nullable": true
          },
          "credentialStuffingAction": {
            "allOf": [
              {
                "$ref": "#/components/schemas/DetectionAction"
              }
            ],
            "description": "What happens to the addresses behind a credential-stuffing attack on a login page once it is detected. Default Challenge.",
            "nullable": true
          },
          "flaggedMinutes": {
            "type": "integer",
            "description": "How long, in minutes, an address that a detection acted on stays challenged or blocked, from 5 to 10080 (7 days). Default 60.",
            "format": "int32",
            "nullable": true
          }
        },
        "description": "The bot-protection settings to save; fields you leave out keep their current value, except underAttackSites and exemptPaths, which are replaced (empty when left out)."
      },
      "BotProtectionView": {
        "required": [
          "botClusterAction",
          "clearanceMinutes",
          "crawlers",
          "credentialStuffingAction",
          "difficulty",
          "exemptPaths",
          "flagged",
          "flaggedMinutes",
          "ja4",
          "plan",
          "premium",
          "sites",
          "underAttackAll",
          "underAttackSites"
        ],
        "type": "object",
        "properties": {
          "underAttackAll": {
            "type": "boolean",
            "description": "True to show the browser check to every visitor of every site."
          },
          "underAttackSites": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Names of the sites whose visitors are all shown the browser check. Use names from sites. Not needed when underAttackAll is true."
          },
          "exemptPaths": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Paths the browser check never applies to, on every site, for example /api/ or /webhooks/ (API clients and webhooks cannot run the check). Each starts with /, and has no spaces, quotes, braces, # or backslashes. /.well-known/ is always left out."
          },
          "difficulty": {
            "type": "integer",
            "description": "How much work the browser check asks of a visitor's browser, from 16 (normal) to 22 (strongest). Default 18.",
            "format": "int32"
          },
          "clearanceMinutes": {
            "type": "integer",
            "description": "How long, in minutes, a visitor who passed the check is left alone, from 5 to 1440 (24 hours). Default 30.",
            "format": "int32"
          },
          "ja4": {
            "type": "boolean",
            "description": "True to record each visitor's TLS fingerprint, which lets PortWall recognise one bot that uses many addresses. Default false."
          },
          "botClusterAction": {
            "allOf": [
              {
                "$ref": "#/components/schemas/DetectionAction"
              }
            ],
            "description": "What happens to the addresses of one bot spread over many addresses once it is detected. Default Challenge."
          },
          "credentialStuffingAction": {
            "allOf": [
              {
                "$ref": "#/components/schemas/DetectionAction"
              }
            ],
            "description": "What happens to the addresses behind a credential-stuffing attack on a login page once it is detected. Default Challenge."
          },
          "flaggedMinutes": {
            "type": "integer",
            "description": "How long, in minutes, an address that a detection acted on stays challenged or blocked, from 5 to 10080 (7 days). Default 60.",
            "format": "int32"
          },
          "sites": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The names of the sites on this installation, which you can use in underAttackSites. Read-only; ignored if you send it."
          },
          "crawlers": {
            "allOf": [
              {
                "$ref": "#/components/schemas/BotCrawlersView"
              }
            ],
            "description": "The search-engine crawler addresses that are never checked. Read-only."
          },
          "feedChallengeRanges": {
            "type": "integer",
            "description": "How many address ranges from threat-intelligence feeds are set to Challenge, and so are shown the browser check. Null when it cannot be read. Read-only.",
            "format": "int32",
            "nullable": true
          },
          "flagged": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FlaggedVisitor"
            },
            "description": "The addresses that detections are challenging or blocking now, soonest to expire first (at most 200). Read-only."
          },
          "premium": {
            "type": "boolean",
            "description": "True when your licence includes bot protection. When false the settings are saved but take effect only once your edition includes it. Read-only."
          },
          "plan": {
            "type": "string",
            "description": "The name of your licence edition. Read-only."
          }
        },
        "description": "The bot-protection settings, with the sites you can choose from, the search-engine crawlers that are never checked, and the visitors currently sent to the browser check."
      },
      "BruteForceSettings": {
        "required": [
          "authFailurePoints",
          "baseBlockTtlMinutes",
          "escalationThreshold",
          "maxBlockTtlMinutes",
          "sources"
        ],
        "type": "object",
        "properties": {
          "sources": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BruteForceSource"
            },
            "description": "The logs to read for failed sign-ins. Send the whole list when you save (at most 50); a source you leave out is removed."
          },
          "authFailurePoints": {
            "type": "number",
            "description": "Risk points added for each failed sign-in, from above 0 up to 1000. Default 4.",
            "format": "double"
          },
          "escalationThreshold": {
            "type": "number",
            "description": "The risk score at which an address is blocked. It must be above 0; default 20. Lower it to block sooner, raise it to allow more failures.",
            "format": "double"
          },
          "baseBlockTtlMinutes": {
            "type": "integer",
            "description": "How long, in minutes, a block lasts when the address has just reached the threshold, from 1 to 10080 (7 days). Default 30. Blocks last longer as the score grows, up to maxBlockTtlMinutes.",
            "format": "int32"
          },
          "maxBlockTtlMinutes": {
            "type": "integer",
            "description": "The longest a block can last, in minutes. It must be at least baseBlockTtlMinutes and no more than 10080 (7 days). Default 240. Blocks always expire.",
            "format": "int32"
          }
        },
        "description": "The brute-force detection settings: which logs PortWall reads for failed sign-ins, and how many failures lead to a temporary block."
      },
      "BruteForceSettingsSaved": {
        "required": [
          "logAccess",
          "settings"
        ],
        "type": "object",
        "properties": {
          "settings": {
            "allOf": [
              {
                "$ref": "#/components/schemas/BruteForceSettings"
              }
            ],
            "description": "The brute-force settings as now saved."
          },
          "logAccess": {
            "type": "array",
            "items": { },
            "description": "One entry for each enabled log folder, saying whether PortWall could be given permission to read it. A failure here does not undo the save."
          }
        },
        "description": "The brute-force settings as saved, and what PortWall did to be able to read each enabled log."
      },
      "BruteForceSource": {
        "required": [
          "enabled",
          "id",
          "kind",
          "label",
          "site"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "A short name that identifies the source, for example windows-auth or mail. It must be unique in the list and must not be empty. Keep the same id for a source you are editing."
          },
          "kind": {
            "allOf": [
              {
                "$ref": "#/components/schemas/BruteForceSourceKind"
              }
            ],
            "description": "Which kind of log this source reads."
          },
          "label": {
            "type": "string",
            "description": "The name shown for the source in the console."
          },
          "enabled": {
            "type": "boolean",
            "description": "Whether PortWall reads this source. A disabled source is kept in the list but not read. Changes take effect within about 30 seconds."
          },
          "site": {
            "type": "string",
            "description": "The name that failures from this source appear under in the attack feed, for example windows-auth, ftp or mail. Required."
          },
          "path": {
            "type": "string",
            "description": "Where the log is. For a ServiceLog source it is the log file, or the folder holding the logs when glob is set; for an IisFtpLog source it is the FTP log folder. Required when the source is enabled; not used by WindowsSecurityLog.",
            "nullable": true
          },
          "pattern": {
            "type": "string",
            "description": "A regular expression that matches one failed-sign-in line in the log. It must capture the attacker's address in a named group (by default called ip) and may capture the user name in another. Required for an enabled ServiceLog source; not used by the other kinds.",
            "nullable": true
          },
          "ipGroup": {
            "type": "string",
            "description": "The name of the group in pattern that captures the client address. Empty means ip. ServiceLog sources only.",
            "nullable": true
          },
          "userGroup": {
            "type": "string",
            "description": "The name of the group in pattern that captures the user name, if the log has one. Empty means user. ServiceLog sources only.",
            "nullable": true
          },
          "glob": {
            "type": "string",
            "description": "A file name pattern, such as u_ex*.log or hmailserver_*.log, for a log that starts a new file each day. When set, path is the folder and PortWall follows the newest matching file. Leave empty for a service that keeps writing to one fixed file, such as SQL Server's ERRORLOG.",
            "nullable": true
          }
        },
        "description": "One log that PortWall reads to find failed sign-ins (Windows accounts, IIS FTP, SQL Server, a mail server and so on)."
      },
      "BruteForceSourceKind": {
        "enum": [
          "WindowsSecurityLog",
          "IisFtpLog",
          "ServiceLog"
        ],
        "type": "string",
        "description": "Which kind of log a brute-force source reads.\n\n- `WindowsSecurityLog`: Failed Windows sign-ins (RDP, SMB, network logon, and FTP or mail servers that use Windows accounts), read from the Windows Security log. One source covers them all and needs no path.\n- `IisFtpLog`: The IIS FTP log folder, for FTP users who are not Windows accounts (those never reach the Security log).\n- `ServiceLog`: Any other text log, such as SQL Server's or a mail server's, matched with a regular expression you give that captures the attacker's address.",
        "x-enum-descriptions": {
          "WindowsSecurityLog": "Failed Windows sign-ins (RDP, SMB, network logon, and FTP or mail servers that use Windows accounts), read from the Windows Security log. One source covers them all and needs no path.",
          "IisFtpLog": "The IIS FTP log folder, for FTP users who are not Windows accounts (those never reach the Security log).",
          "ServiceLog": "Any other text log, such as SQL Server's or a mail server's, matched with a regular expression you give that captures the attacker's address."
        }
      },
      "BulkSiteModeUpdate": {
        "required": [
          "mode"
        ],
        "type": "object",
        "properties": {
          "mode": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EngineMode"
              }
            ],
            "description": "The mode to put the sites in: Off, DetectionOnly or On. Off is meant for a named site that cannot work with the WAF."
          },
          "sites": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The names of the sites to change. Leave out or send null for every site.",
            "nullable": true
          }
        },
        "description": "A request to put many sites, or every site, into one WAF mode in a single call."
      },
      "Campaign": {
        "required": [
          "distinctSites",
          "distinctTenants",
          "enforced",
          "eventCount",
          "firstSeen",
          "id",
          "keyKind",
          "keyValue",
          "lastSeen",
          "maxSeverity",
          "status"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The campaign's ID, made from the key kind and value (for example Ip:203.0.113.5)."
          },
          "keyKind": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CampaignKeyKind"
              }
            ],
            "description": "What identifies the attacker: an IP address, a TLS fingerprint (Ja4) or a network operator (Asn)."
          },
          "keyValue": {
            "type": "string",
            "description": "The attacker's identifier: the IP address, the JA4 fingerprint, or the network number such as AS64500."
          },
          "firstSeen": {
            "type": "string",
            "description": "When PortWall first saw this campaign.",
            "format": "date-time"
          },
          "lastSeen": {
            "type": "string",
            "description": "When PortWall last saw it, which is also when its counts were last updated.",
            "format": "date-time"
          },
          "distinctTenants": {
            "type": "integer",
            "description": "How many different customers (tenants) the attacker hit. A site with no tenant counts as its own.",
            "format": "int32"
          },
          "distinctSites": {
            "type": "integer",
            "description": "How many different sites the attacker hit.",
            "format": "int32"
          },
          "eventCount": {
            "type": "integer",
            "description": "How many flagged requests from this attacker were counted in the detection window.",
            "format": "int32"
          },
          "maxSeverity": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RuleSeverity"
              }
            ],
            "description": "The most serious rule severity among those requests."
          },
          "status": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CampaignStatus"
              }
            ],
            "description": "Whether the campaign is still active."
          },
          "enforced": {
            "type": "boolean",
            "description": "True when PortWall acted on it automatically: it blocked the address or throttled the fingerprint. Once true it stays true."
          }
        },
        "description": "An attacker seen hitting many different customers (tenants) on this installation, which points to a coordinated sweep rather than one noisy site."
      },
      "CampaignKeyKind": {
        "enum": [
          "Ip",
          "Ja4",
          "Asn"
        ],
        "type": "string",
        "description": "What ties a campaign's attacks together.\n\n- `Ip`: The campaign is one IP address.\n- `Ja4`: The campaign is one TLS fingerprint, which finds an attacker who keeps changing IP address.\n- `Asn`: The campaign is one network operator (autonomous system), which finds a botnet spread over many addresses. It is reported only and never blocked automatically.",
        "x-enum-descriptions": {
          "Ip": "The campaign is one IP address.",
          "Ja4": "The campaign is one TLS fingerprint, which finds an attacker who keeps changing IP address.",
          "Asn": "The campaign is one network operator (autonomous system), which finds a botnet spread over many addresses. It is reported only and never blocked automatically."
        }
      },
      "CampaignStatus": {
        "enum": [
          "Active",
          "Retired"
        ],
        "type": "string",
        "description": "Whether a campaign is still going on.\n\n- `Active`: Seen recently, within the detection window.\n- `Retired`: No longer active; kept as a record.",
        "x-enum-descriptions": {
          "Active": "Seen recently, within the detection window.",
          "Retired": "No longer active; kept as a record."
        }
      },
      "CmsProfile": {
        "enum": [
          "None",
          "WordPress",
          "Forum"
        ],
        "type": "string",
        "description": "A ready-made set of protections for a common kind of website application.\n\n- `None`: No preset.\n- `WordPress`: Limits how often one visitor can use xmlrpc, login, comments and sign-up, and blocks pingback and multicall abuse and the listing of user names.\n- `Forum`: Limits how often one visitor can use a forum's login, posting and registration pages, as on phpBB.",
        "x-enum-descriptions": {
          "None": "No preset.",
          "WordPress": "Limits how often one visitor can use xmlrpc, login, comments and sign-up, and blocks pingback and multicall abuse and the listing of user names.",
          "Forum": "Limits how often one visitor can use a forum's login, posting and registration pages, as on phpBB."
        }
      },
      "ConfigApplyStatus": {
        "required": [
          "completed",
          "outcome",
          "requested",
          "state"
        ],
        "type": "object",
        "properties": {
          "state": {
            "type": "string",
            "description": "applying while changes are still being applied, idle when nothing is waiting."
          },
          "requested": {
            "type": "integer",
            "description": "A counter of the changes asked for since PortWall started. A save returns the number its change was given (applySeq).",
            "format": "int64"
          },
          "completed": {
            "type": "integer",
            "description": "A counter of the changes applied so far. A change is in force once this reaches the applySeq its save returned.",
            "format": "int64"
          },
          "outcome": {
            "type": "string",
            "description": "How the latest apply ended: applied, deferred (saved, and taken up as soon as it can be), failed (see message), or none before the first apply."
          },
          "message": {
            "type": "string",
            "description": "Why the latest apply was deferred or failed. Null when it was applied or nothing has been applied yet.",
            "nullable": true
          },
          "completedAt": {
            "type": "string",
            "description": "When the latest apply finished. Null until the first apply after PortWall started.",
            "format": "date-time",
            "nullable": true
          },
          "reason": {
            "type": "string",
            "description": "A short label for the change that was last asked for, such as the site it was for. Null before the first change.",
            "nullable": true
          }
        },
        "description": "Where PortWall stands in applying saved changes in the background; poll it until your change is in force."
      },
      "ConfigImportBody": {
        "required": [
          "bundle",
          "dryRun",
          "skipHostSpecific"
        ],
        "type": "object",
        "properties": {
          "dryRun": {
            "type": "boolean",
            "description": "True to report what would change without changing anything. Default false, which applies the import."
          },
          "skipHostSpecific": {
            "type": "boolean",
            "description": "True to leave out settings that belong to the original server, such as its network addresses, for example when moving to a new server. Default false."
          },
          "bundle": {
            "description": "The content of the file you downloaded from GET /api/config/export, sent as it was."
          }
        },
        "description": "A configuration file to restore, and how to restore it."
      },
      "ConfigImportResult": {
        "required": [
          "applied",
          "notes",
          "schemaVersion",
          "sections",
          "sourceServer",
          "sourceVersion"
        ],
        "type": "object",
        "properties": {
          "applied": {
            "type": "boolean",
            "description": "True when the import was applied; false for a dry run."
          },
          "sourceServer": {
            "type": "string",
            "description": "The name of the server the bundle was exported from, or unknown."
          },
          "sourceVersion": {
            "type": "string",
            "description": "The PortWall version that exported the bundle, or unknown."
          },
          "schemaVersion": {
            "type": "integer",
            "description": "The layout version of the bundle; 0 when it does not say.",
            "format": "int32"
          },
          "sections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ConfigSectionChange"
            },
            "description": "The parts of the configuration found in the bundle and what happened to each. Parts the bundle does not contain are left out."
          },
          "notes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Things to know after the import, such as settings that were skipped or secrets you must enter again."
          }
        },
        "description": "What a configuration import did, or, for a dry run, what it would do."
      },
      "ConfigSectionChange": {
        "required": [
          "added",
          "present",
          "section",
          "updated"
        ],
        "type": "object",
        "properties": {
          "section": {
            "type": "string",
            "description": "The name of the part of the configuration, for example feed-enablement."
          },
          "present": {
            "type": "boolean",
            "description": "True when the bundle contains this part."
          },
          "added": {
            "type": "integer",
            "description": "How many items were new on this server. 0 for a part that is a single set of settings.",
            "format": "int32"
          },
          "updated": {
            "type": "integer",
            "description": "How many items already existed and were replaced. A single set of settings reports 1.",
            "format": "int32"
          }
        },
        "description": "What an import did, or would do, to one part of the configuration."
      },
      "CreateApiKeyRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "A name to recognise the key by, 1 to 80 characters. Required.",
            "nullable": true
          },
          "role": {
            "type": "string",
            "description": "What the key may do: Admin (read and change), Viewer (read only) or Tenant (one tenant's own sites). Required.",
            "nullable": true
          },
          "tenantId": {
            "type": "string",
            "description": "The tenant a Tenant key is limited to. Required for role Tenant; ignored for other roles.",
            "nullable": true
          }
        },
        "description": "The details of an API key to create."
      },
      "CreateBlockRequest": {
        "required": [
          "ip",
          "permanent",
          "ttlMinutes"
        ],
        "type": "object",
        "properties": {
          "ip": {
            "type": "string",
            "description": "The IPv4 or IPv6 address to block. An allowlisted address is refused with 409."
          },
          "ttlMinutes": {
            "type": "integer",
            "description": "How long to block for, in minutes. Between 1 minute and 1 year (525600); values outside that are adjusted to the nearest limit. Ignored when ttlHours or permanent is set.",
            "format": "int32"
          },
          "reason": {
            "type": "string",
            "description": "Why you are blocking it, shown in the block list and the audit log. Empty uses manual block.",
            "nullable": true
          },
          "ttlHours": {
            "type": "number",
            "description": "How long to block for, in hours; fractions are allowed (0.5 is 30 minutes). When set it is used instead of ttlMinutes. Limits are the same as for ttlMinutes.",
            "format": "double",
            "nullable": true
          },
          "permanent": {
            "type": "boolean",
            "description": "True to block until you remove it by hand; the block then has no expiry and any duration sent is ignored. Default false."
          }
        },
        "description": "The request body for blocking an address by hand, saying how long the block lasts."
      },
      "DetectionAction": {
        "enum": [
          "Alert",
          "Challenge",
          "Block"
        ],
        "type": "string",
        "description": "What PortWall does to the addresses behind a bot cluster or credential stuffing.\n\n- `Alert`: Record the detection in the attack feed and send an alert; do nothing else to the addresses.\n- `Challenge`: Show the addresses the browser check for a while: a bot is stopped and a person passes in a moment.\n- `Block`: Block the addresses for a while.",
        "x-enum-descriptions": {
          "Alert": "Record the detection in the attack feed and send an alert; do nothing else to the addresses.",
          "Challenge": "Show the addresses the browser check for a while: a bot is stopped and a person passes in a moment.",
          "Block": "Block the addresses for a while."
        }
      },
      "EgressMode": {
        "enum": [
          "Audit",
          "Enforce"
        ],
        "type": "string",
        "description": "Whether a site's outbound policy only reports connections it would stop, or stops them.\n\n- `Audit`: Outbound connections are not stopped; the ones the policy would stop are reported. Use it to learn what the site connects to.\n- `Enforce`: Outbound connections are denied by default: only the allowed remotes (and PortWall's essential destinations) can be reached.",
        "x-enum-descriptions": {
          "Audit": "Outbound connections are not stopped; the ones the policy would stop are reported. Use it to learn what the site connects to.",
          "Enforce": "Outbound connections are denied by default: only the allowed remotes (and PortWall's essential destinations) can be reached."
        }
      },
      "EgressPolicy": {
        "required": [
          "allowedRemotes",
          "enabled",
          "mode",
          "programPath",
          "siteName",
          "updatedAt"
        ],
        "type": "object",
        "properties": {
          "siteName": {
            "type": "string",
            "description": "The site the policy belongs to."
          },
          "enabled": {
            "type": "boolean",
            "description": "True to apply the policy. False keeps it saved but applies no outbound rules."
          },
          "mode": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EgressMode"
              }
            ],
            "description": "Audit records the outbound connections the policy would stop without stopping them; Enforce stops every connection not on the allowlist."
          },
          "allowedRemotes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The remote addresses or ranges (for example 203.0.113.5 or 10.0.0.0/24) the site may connect out to, such as its database and payment gateway. PortWall's own essential destinations are always allowed in addition."
          },
          "programPath": {
            "type": "string",
            "description": "The program whose outbound connections are controlled. Defaults to the IIS web worker process when not given."
          },
          "updatedBy": {
            "type": "string",
            "description": "Who last set the policy: the signed-in user or API key name.",
            "nullable": true
          },
          "updatedAt": {
            "type": "string",
            "description": "When the policy was last set.",
            "format": "date-time"
          }
        },
        "description": "A site's outbound (egress) policy: which remote addresses the site's application may connect out to, and whether that is only recorded or enforced."
      },
      "EgressPolicyUpdate": {
        "required": [
          "enabled",
          "mode"
        ],
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "True to apply the policy. False keeps it saved but applies no outbound rules."
          },
          "mode": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EgressMode"
              }
            ],
            "description": "Audit records the outbound connections the policy would stop without stopping them; Enforce stops every connection not on the allowlist."
          },
          "allowedRemotes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The remote addresses or ranges (for example 203.0.113.5 or 10.0.0.0/24) the site may connect out to, such as its database and payment gateway. PortWall's own essential destinations are always allowed in addition.",
            "nullable": true
          },
          "programPath": {
            "type": "string",
            "description": "The program whose outbound connections are controlled. Defaults to the IIS web worker process when not given.",
            "nullable": true
          }
        },
        "description": "The outbound policy to set for a site; the site name comes from the URL."
      },
      "EngineMode": {
        "enum": [
          "Off",
          "DetectionOnly",
          "On"
        ],
        "type": "string",
        "description": "Whether the WAF engine inspects a site's traffic and whether it blocks what it flags.\n\n- `Off`: The WAF does not inspect this site's requests.\n- `DetectionOnly`: The WAF inspects requests and records attacks in the Attack Feed but blocks nothing. The default for a new site.\n- `On`: The WAF inspects requests and blocks those that reach the anomaly threshold.",
        "x-enum-descriptions": {
          "Off": "The WAF does not inspect this site's requests.",
          "DetectionOnly": "The WAF inspects requests and records attacks in the Attack Feed but blocks nothing. The default for a new site.",
          "On": "The WAF inspects requests and blocks those that reach the anomaly threshold."
        }
      },
      "FailMode": {
        "enum": [
          "Open",
          "Closed"
        ],
        "type": "string",
        "description": "What a site does with a request PortWall cannot fully inspect: a body larger than the WAF inspection limit, or an upload the scanner could not check.\n\n- `Open`: Let it through: the first part of an oversized body is inspected and the rest passed, and an upload the scanner could not check goes to the site.\n- `Closed`: Refuse it: a body larger than the inspection limit gets 413, and an upload the scanner could not check (scanner down, timed out, file too large to scan) gets 403.",
        "x-enum-descriptions": {
          "Open": "Let it through: the first part of an oversized body is inspected and the rest passed, and an upload the scanner could not check goes to the site.",
          "Closed": "Refuse it: a body larger than the inspection limit gets 413, and an upload the scanner could not check (scanner down, timed out, file too large to scan) gets 403."
        }
      },
      "FeedAction": {
        "enum": [
          "Drop",
          "DetectionOnly",
          "Challenge"
        ],
        "type": "string",
        "description": "What happens to addresses a threat-intelligence feed lists.\n\n- `Drop`: Addresses on the feed are blocked at the firewall.\n- `DetectionOnly`: Addresses on the feed are counted but not blocked, so you can try a feed before enforcing it.\n- `Challenge`: Addresses on the feed get the browser check instead of a hard block, which suits shared addresses; a real browser passes and a simple bot does not.",
        "x-enum-descriptions": {
          "Drop": "Addresses on the feed are blocked at the firewall.",
          "DetectionOnly": "Addresses on the feed are counted but not blocked, so you can try a feed before enforcing it.",
          "Challenge": "Addresses on the feed get the browser check instead of a hard block, which suits shared addresses; a real browser passes and a simple bot does not."
        }
      },
      "FeedFormat": {
        "enum": [
          "PlainList",
          "SpamhausDrop"
        ],
        "type": "string",
        "description": "The layout of a threat-intelligence feed's list.\n\n- `PlainList`: One address or range per line, with blank lines and comment lines ignored.\n- `SpamhausDrop`: The Spamhaus DROP layout, a range followed by a semicolon and a reference.",
        "x-enum-descriptions": {
          "PlainList": "One address or range per line, with blank lines and comment lines ignored.",
          "SpamhausDrop": "The Spamhaus DROP layout, a range followed by a semicolon and a reference."
        }
      },
      "FeedStatus": {
        "required": [
          "action",
          "enabled",
          "entryCount",
          "feedId",
          "lastRefreshOk"
        ],
        "type": "object",
        "properties": {
          "feedId": {
            "type": "string",
            "description": "The feed's ID."
          },
          "enabled": {
            "type": "boolean",
            "description": "Whether the feed is switched on."
          },
          "action": {
            "allOf": [
              {
                "$ref": "#/components/schemas/FeedAction"
              }
            ],
            "description": "What happens to addresses the feed lists: Drop, DetectionOnly or Challenge."
          },
          "entryCount": {
            "type": "integer",
            "description": "How many addresses or ranges the last refresh loaded. 0 before the first refresh.",
            "format": "int32"
          },
          "lastRefreshedAt": {
            "type": "string",
            "description": "When the feed was last downloaded. Null until the first refresh.",
            "format": "date-time",
            "nullable": true
          },
          "lastRefreshOk": {
            "type": "boolean",
            "description": "True when the last refresh worked."
          },
          "lastError": {
            "type": "string",
            "description": "Why the last refresh failed. Null when it worked or has not run yet.",
            "nullable": true
          }
        },
        "description": "A feed's switch, action and the outcome of its last refresh, as saved after you change the feed."
      },
      "FeedSubsystemUpdate": {
        "required": [
          "enabled"
        ],
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "True to turn threat-intelligence feeds on, false to turn them all off."
          }
        },
        "description": "The request body for the threat-intelligence master switch."
      },
      "FeedUpdate": {
        "required": [
          "action",
          "enabled"
        ],
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Whether this feed is switched on. A feed does nothing while the master switch is off, whatever this says."
          },
          "action": {
            "allOf": [
              {
                "$ref": "#/components/schemas/FeedAction"
              }
            ],
            "description": "What happens to addresses the feed lists: Drop, DetectionOnly or Challenge. Drop is the default for a feed that has not been set."
          }
        },
        "description": "The request body for turning one feed on or off and choosing what happens to addresses it lists."
      },
      "FeedView": {
        "required": [
          "action",
          "attribution",
          "commercialSafe",
          "enabled",
          "entryCount",
          "format",
          "id",
          "lastRefreshOk",
          "license",
          "name",
          "refreshIntervalMinutes"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The feed's ID, used in the URL to change it."
          },
          "name": {
            "type": "string",
            "description": "The feed's display name."
          },
          "format": {
            "allOf": [
              {
                "$ref": "#/components/schemas/FeedFormat"
              }
            ],
            "description": "How the feed's list is laid out, so PortWall can read it."
          },
          "license": {
            "type": "string",
            "description": "The terms under which the feed's publisher allows it to be used."
          },
          "attribution": {
            "type": "string",
            "description": "The publisher to credit when you use the feed."
          },
          "commercialSafe": {
            "type": "boolean",
            "description": "True when the publisher's terms allow commercial use."
          },
          "refreshIntervalMinutes": {
            "type": "integer",
            "description": "How often PortWall downloads the feed again, in minutes.",
            "format": "int32"
          },
          "enabled": {
            "type": "boolean",
            "description": "Whether this feed is switched on. A feed does nothing while the master switch is off, whatever this says."
          },
          "action": {
            "type": "string",
            "description": "What happens to addresses the feed lists: Drop, DetectionOnly or Challenge. Drop is the default for a feed that has not been set."
          },
          "entryCount": {
            "type": "integer",
            "description": "How many addresses or ranges the last refresh loaded. 0 before the first refresh.",
            "format": "int32"
          },
          "lastRefreshedAt": {
            "type": "string",
            "description": "When the feed was last downloaded. Null until the first refresh.",
            "format": "date-time",
            "nullable": true
          },
          "lastRefreshOk": {
            "type": "boolean",
            "description": "True when the last refresh worked. False before the first refresh or when it failed; see lastError."
          },
          "lastError": {
            "type": "string",
            "description": "Why the last refresh failed. Null when it worked or has not run yet.",
            "nullable": true
          }
        },
        "description": "One built-in threat-intelligence feed with its licence, settings and the outcome of its last refresh."
      },
      "FeedsView": {
        "required": [
          "enforcedRanges",
          "feeds",
          "subsystemEnabled"
        ],
        "type": "object",
        "properties": {
          "subsystemEnabled": {
            "type": "boolean",
            "description": "The threat-intelligence master switch. While it is off no feed is downloaded or enforced, whatever each feed's own setting."
          },
          "enforcedRanges": {
            "type": "integer",
            "description": "How many address ranges the feeds are blocking now, after your allowlist is taken out.",
            "format": "int32"
          },
          "feeds": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FeedView"
            },
            "description": "Every built-in feed and its state."
          }
        },
        "description": "The threat-intelligence feeds: the master switch, how many address ranges are being blocked, and each feed."
      },
      "FlaggedVisitor": {
        "required": [
          "address",
          "reason",
          "until"
        ],
        "type": "object",
        "properties": {
          "address": {
            "type": "string",
            "description": "The visitor's IP address."
          },
          "until": {
            "type": "string",
            "description": "When the challenge or block on this address ends.",
            "format": "date-time"
          },
          "reason": {
            "type": "string",
            "description": "Why the address was flagged, in words."
          }
        },
        "description": "One address that a detection has sent to the browser check or blocked, and until when."
      },
      "FlushTemporaryResult": {
        "required": [
          "removed"
        ],
        "type": "object",
        "properties": {
          "removed": {
            "type": "integer",
            "description": "How many temporary blocks were removed. Permanent blocks are not counted and not removed.",
            "format": "int32"
          }
        },
        "description": "The result of removing all temporary blocks."
      },
      "ForensicHeader": {
        "required": [
          "name",
          "value"
        ],
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "The header name."
          },
          "value": {
            "type": "string",
            "description": "The header value, with sensitive values masked."
          }
        },
        "description": "One request header captured in a forensic snapshot."
      },
      "ForensicSnapshot": {
        "required": [
          "bodySample",
          "bodyTruncated",
          "headers",
          "method",
          "protocol",
          "uri"
        ],
        "type": "object",
        "properties": {
          "method": {
            "type": "string",
            "description": "The HTTP method of the request, for example GET or POST."
          },
          "uri": {
            "type": "string",
            "description": "The path and query string that was requested."
          },
          "protocol": {
            "type": "string",
            "description": "The HTTP version used, for example HTTP/1.1 or HTTP/2.0."
          },
          "headers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ForensicHeader"
            },
            "description": "A selection of the request's headers. Sensitive values such as cookies and authorisation headers are masked."
          },
          "bodySample": {
            "type": "string",
            "description": "The start of the request body, cut to a fixed size. Empty unless request-body capture has been turned on, which is off by default because bodies often hold personal data."
          },
          "bodyTruncated": {
            "type": "boolean",
            "description": "True when the body was longer than the sample kept."
          },
          "ja4": {
            "type": "string",
            "description": "The JA4 fingerprint of the client's TLS connection, which identifies the software making the request. Null when none was captured.",
            "nullable": true
          }
        },
        "description": "A copy of the request that triggered a detection, kept so you can see what the attacker actually sent."
      },
      "GeoBlockPolicy": {
        "required": [
          "countries",
          "id",
          "layer",
          "letCertificateChecksThrough",
          "mode",
          "ports",
          "protocol",
          "response",
          "selector",
          "updatedAt"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The policy's ID: a single word with no spaces, underscores or quotes, chosen when the policy was created."
          },
          "countries": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The countries the policy covers, as two-letter ISO codes (stored in upper case). At least one."
          },
          "protocol": {
            "allOf": [
              {
                "$ref": "#/components/schemas/GeoProtocol"
              }
            ],
            "description": "Which traffic a firewall policy covers: Any, Tcp or Udp. Only used by firewall policies."
          },
          "ports": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int32"
            },
            "description": "The ports a firewall policy covers, from 1 to 65535. Empty means all ports, which needs protocol Any; a list needs Tcp or Udp. Not used by engine policies."
          },
          "mode": {
            "allOf": [
              {
                "$ref": "#/components/schemas/GeoMode"
              }
            ],
            "description": "Audit records what the policy would have done without blocking anything; Enforce blocks."
          },
          "layer": {
            "allOf": [
              {
                "$ref": "#/components/schemas/GeoLayer"
              }
            ],
            "description": "Where the policy is enforced: Firewall drops the traffic at the network layer on any port; DataPlane blocks web requests in the engine. Default Firewall."
          },
          "selector": {
            "allOf": [
              {
                "$ref": "#/components/schemas/GeoSelector"
              }
            ],
            "description": "Block refuses the listed countries; Allow admits only the listed countries and refuses everyone else. Allow needs layer DataPlane. Default Block."
          },
          "response": {
            "allOf": [
              {
                "$ref": "#/components/schemas/GeoResponse"
              }
            ],
            "description": "How an engine policy answers: Deny refuses with 403 Forbidden; Challenge gives visitors from the listed countries the browser check (on an Allow policy, requests from elsewhere are refused with 429). Not used by firewall policies. Default Deny."
          },
          "updatedBy": {
            "type": "string",
            "description": "Who last saved the policy. Null if not recorded.",
            "nullable": true
          },
          "updatedAt": {
            "type": "string",
            "description": "When the policy was last saved.",
            "format": "date-time"
          },
          "letCertificateChecksThrough": {
            "type": "boolean",
            "description": "For a firewall Block policy: leave ports 80 and 443 to the engine, which blocks the countries there but lets certificate checks through, so certificate issuing and renewal keep working. Default false."
          }
        },
        "description": "A geo-blocking policy: which countries it covers, where and how it is enforced, and whether it only records or actually blocks."
      },
      "GeoLayer": {
        "enum": [
          "Firewall",
          "DataPlane"
        ],
        "type": "string",
        "description": "Where a geo-blocking policy is enforced.\n\n- `Firewall`: Blocks at the network layer, before the engine, on the ports and protocol you choose. Supports Block only.\n- `DataPlane`: Blocks web requests in the engine. Ports and protocol are ignored. The only layer that supports Allow.",
        "x-enum-descriptions": {
          "Firewall": "Blocks at the network layer, before the engine, on the ports and protocol you choose. Supports Block only.",
          "DataPlane": "Blocks web requests in the engine. Ports and protocol are ignored. The only layer that supports Allow."
        }
      },
      "GeoMode": {
        "enum": [
          "Audit",
          "Enforce"
        ],
        "type": "string",
        "description": "Whether a geo-blocking policy blocks or only records.\n\n- `Audit`: Records what the policy would block but blocks nothing, so you can see the effect first.\n- `Enforce`: Blocks traffic from the countries.",
        "x-enum-descriptions": {
          "Audit": "Records what the policy would block but blocks nothing, so you can see the effect first.",
          "Enforce": "Blocks traffic from the countries."
        }
      },
      "GeoPolicyUpdate": {
        "required": [
          "countries",
          "layer",
          "letCertificateChecksThrough",
          "mode",
          "protocol",
          "response",
          "selector"
        ],
        "type": "object",
        "properties": {
          "countries": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The countries the policy covers, as two-letter ISO codes (stored in upper case). At least one."
          },
          "protocol": {
            "allOf": [
              {
                "$ref": "#/components/schemas/GeoProtocol"
              }
            ],
            "description": "Which traffic a firewall policy covers: Any, Tcp or Udp. Only used by firewall policies."
          },
          "ports": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int32"
            },
            "description": "The ports a firewall policy covers, from 1 to 65535. Empty means all ports, which needs protocol Any; a list needs Tcp or Udp. Not used by engine policies.",
            "nullable": true
          },
          "mode": {
            "allOf": [
              {
                "$ref": "#/components/schemas/GeoMode"
              }
            ],
            "description": "Audit records what the policy would have done without blocking anything; Enforce blocks."
          },
          "layer": {
            "allOf": [
              {
                "$ref": "#/components/schemas/GeoLayer"
              }
            ],
            "description": "Where the policy is enforced: Firewall drops the traffic at the network layer on any port; DataPlane blocks web requests in the engine. Default Firewall."
          },
          "selector": {
            "allOf": [
              {
                "$ref": "#/components/schemas/GeoSelector"
              }
            ],
            "description": "Block refuses the listed countries; Allow admits only the listed countries and refuses everyone else. Allow needs layer DataPlane. Default Block."
          },
          "response": {
            "allOf": [
              {
                "$ref": "#/components/schemas/GeoResponse"
              }
            ],
            "description": "How an engine policy answers: Deny refuses with 403 Forbidden; Challenge gives visitors from the listed countries the browser check (on an Allow policy, requests from elsewhere are refused with 429). Not used by firewall policies. Default Deny."
          },
          "letCertificateChecksThrough": {
            "type": "boolean",
            "description": "For a firewall Block policy: leave ports 80 and 443 to the engine, which blocks the countries there but lets certificate checks through, so certificate issuing and renewal keep working. Default false."
          }
        },
        "description": "The request body for creating or changing a geo-blocking policy; the policy ID comes from the URL."
      },
      "GeoProtocol": {
        "enum": [
          "Any",
          "Tcp",
          "Udp"
        ],
        "type": "string",
        "description": "Which traffic a firewall geo-blocking policy covers.\n\n- `Any`: All traffic, so no port list is allowed.\n- `Tcp`: TCP only, optionally limited to the ports listed.\n- `Udp`: UDP only, optionally limited to the ports listed.",
        "x-enum-descriptions": {
          "Any": "All traffic, so no port list is allowed.",
          "Tcp": "TCP only, optionally limited to the ports listed.",
          "Udp": "UDP only, optionally limited to the ports listed."
        }
      },
      "GeoResponse": {
        "enum": [
          "Deny",
          "Challenge"
        ],
        "type": "string",
        "description": "What a web request refused by geo-blocking receives.\n\n- `Deny`: Refuse the request with a 403 Forbidden.\n- `Challenge`: On a policy that blocks the listed countries, visitors from them get the browser check: a real browser passes in a moment, most attack tools do not. On a policy that admits only the listed countries, requests from elsewhere are refused with 429 Too Many Requests.",
        "x-enum-descriptions": {
          "Deny": "Refuse the request with a 403 Forbidden.",
          "Challenge": "On a policy that blocks the listed countries, visitors from them get the browser check: a real browser passes in a moment, most attack tools do not. On a policy that admits only the listed countries, requests from elsewhere are refused with 429 Too Many Requests."
        }
      },
      "GeoSearchEnginesUpdate": {
        "required": [
          "letThrough"
        ],
        "type": "object",
        "properties": {
          "letThrough": {
            "type": "boolean",
            "description": "True to let the crawlers of Google, Bing, Apple and DuckDuckGo through every geo policy (the default), false to block them like any other visitor from a blocked country."
          }
        },
        "description": "The request body for letting search-engine crawlers through geo blocking."
      },
      "GeoSelector": {
        "enum": [
          "Block",
          "Allow"
        ],
        "type": "string",
        "description": "Whether a geo-blocking policy refuses the listed countries or admits only them.\n\n- `Block`: Refuse traffic from the listed countries.\n- `Allow`: Admit only the listed countries and refuse everywhere else; allowlisted addresses are still admitted. Needs layer DataPlane.",
        "x-enum-descriptions": {
          "Block": "Refuse traffic from the listed countries.",
          "Allow": "Admit only the listed countries and refuse everywhere else; allowlisted addresses are still admitted. Needs layer DataPlane."
        }
      },
      "GeoSubsystemUpdate": {
        "required": [
          "enabled"
        ],
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "True to turn geo-blocking on, false to turn it off. While off no policy is enforced."
          }
        },
        "description": "The request body for the geo-blocking master switch."
      },
      "GrantRequest": {
        "required": [
          "role",
          "userId"
        ],
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "description": "The panel user who has access."
          },
          "role": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SiteRole"
              }
            ],
            "description": "What the user may do with the site: look only, or also change its settings within the tenant policy."
          }
        },
        "description": "Request body that gives a panel user a role on a site; the site is named in the URL."
      },
      "HealthView": {
        "required": [
          "status"
        ],
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "description": "Always ok while the management service is running."
          }
        },
        "description": "The reply of the health check."
      },
      "LicenseView": {
        "required": [
          "blockStatePreserved",
          "enabled",
          "enforcementActive",
          "enterprise",
          "herdImmunityActive",
          "herdImmunityContributing",
          "installId",
          "licenseKeyFromConsole",
          "licenseKeyMasked",
          "licenseKeyPresent",
          "managedCertRenewalActive",
          "premium",
          "premiumThreatIntelActive",
          "reason",
          "scanningActive",
          "smartTuningActive",
          "state",
          "tier"
        ],
        "type": "object",
        "properties": {
          "licenseKeyMasked": {
            "type": "string",
            "description": "The licence key with all but its last characters hidden, so you can tell which key is in use. Empty when there is none."
          },
          "licenseKeyPresent": {
            "type": "boolean",
            "description": "True when a licence key is set."
          },
          "licenseKeyFromConsole": {
            "type": "boolean",
            "description": "True when the key in use was entered in the console or through this API; false when it comes from the server's configuration file."
          },
          "enabled": {
            "type": "boolean",
            "description": "False only when licensing is not turned on for this installation; then state is Licensed and the other fields are absent."
          },
          "state": {
            "type": "string",
            "description": "Licensed (everything active), Grace (the licence could not be confirmed recently; protection stays fully on for at most 72 hours) or Unlicensed (blocking is switched off)."
          },
          "reason": {
            "type": "string",
            "description": "Why the licence is in this state, in words."
          },
          "installId": {
            "type": "string",
            "description": "This installation's ID. Quote it when you write to PortWall about a licence."
          },
          "enforcementActive": {
            "type": "boolean",
            "description": "Whether PortWall is blocking attacks. False when Unlicensed: attacks are still detected and recorded, but not blocked."
          },
          "scanningActive": {
            "type": "boolean",
            "description": "Whether uploaded-file scanning is allowed by the licence."
          },
          "tier": {
            "type": "string",
            "description": "The name of your edition, for example Windows Premium or Appliance Enterprise."
          },
          "premium": {
            "type": "boolean",
            "description": "True when your edition is Premium or higher."
          },
          "enterprise": {
            "type": "boolean",
            "description": "True when your edition is Enterprise, which adds API protection."
          },
          "herdImmunityActive": {
            "type": "boolean",
            "description": "True when this server receives protection from attacker data shared by other PortWall servers (herd immunity). Needs Premium or higher."
          },
          "herdImmunityContributing": {
            "type": "boolean",
            "description": "True when this server shares its attack data with other PortWall servers. A Standard server contributes without receiving."
          },
          "smartTuningActive": {
            "type": "boolean",
            "description": "True when the Smart Tuning Advisor, which suggests rule exclusions, is available."
          },
          "premiumThreatIntelActive": {
            "type": "boolean",
            "description": "True when Premium Threat Intelligence is available."
          },
          "managedCertRenewalActive": {
            "type": "boolean",
            "description": "True when PortWall may renew its managed TLS certificates."
          },
          "blockStatePreserved": {
            "type": "boolean",
            "description": "Always true: current blocks and your settings are kept when a licence lapses, and work again once it is renewed."
          }
        },
        "description": "The licence as this installation sees it: its state, its edition and which licensed features are in force. The key itself is never included."
      },
      "NotificationPreferences": {
        "required": [
          "alertKinds",
          "crsHitMinScore",
          "enabled",
          "kindsVersion",
          "minAnomalyScore"
        ],
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Master switch for notifications. When false, nothing is sent whatever else is set here."
          },
          "alertKinds": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SecurityEventKind"
            },
            "description": "The event types that always notify, whatever their score. Unknown values are dropped and repeats removed when you save."
          },
          "minAnomalyScore": {
            "type": "integer",
            "description": "An event of any type notifies when its anomaly score is at or above this, from 0 to 1000. Default 20.",
            "format": "int32"
          },
          "crsHitMinScore": {
            "type": "integer",
            "description": "An extra minimum score for single WAF rule hits, which are the noisiest event type: a rule hit notifies only at or above this score. From 0 to 1000; 0 means no extra minimum, which is the default.",
            "format": "int32"
          },
          "kindsVersion": {
            "type": "integer",
            "description": "Bookkeeping: which version of the event-type list these settings were saved against. Send back the value that GET returned; PortWall sets the current value when you save.",
            "format": "int32"
          }
        },
        "description": "Which events PortWall notifies you about; where the notifications go (email, webhooks) is set elsewhere."
      },
      "OkResult": {
        "required": [
          "ok"
        ],
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "Always true when the request was carried out."
          }
        },
        "description": "Confirmation that the request was carried out."
      },
      "PagedResultOfQuarantineEntry": {
        "required": [
          "hasMore",
          "hasPrevious",
          "items",
          "limit",
          "offset",
          "total"
        ],
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QuarantineEntry"
            },
            "description": "The records on this page."
          },
          "total": {
            "type": "integer",
            "description": "How many records match in all, across every page.",
            "format": "int32"
          },
          "offset": {
            "type": "integer",
            "description": "How many records were skipped before this page (the skip you asked for, never below 0).",
            "format": "int32"
          },
          "limit": {
            "type": "integer",
            "description": "The most records this page can hold (the take you asked for; at most 200, 50 when you gave none).",
            "format": "int32"
          },
          "hasMore": {
            "type": "boolean",
            "description": "True when more records follow this page. Ask for the next page by adding limit to the skip.",
            "readOnly": true
          },
          "hasPrevious": {
            "type": "boolean",
            "description": "True when records come before this page (offset above 0).",
            "readOnly": true
          }
        },
        "description": "One page of a list of QuarantineEntry."
      },
      "PagedResultOfScanRun": {
        "required": [
          "hasMore",
          "hasPrevious",
          "items",
          "limit",
          "offset",
          "total"
        ],
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ScanRun"
            },
            "description": "The records on this page."
          },
          "total": {
            "type": "integer",
            "description": "How many records match in all, across every page.",
            "format": "int32"
          },
          "offset": {
            "type": "integer",
            "description": "How many records were skipped before this page (the skip you asked for, never below 0).",
            "format": "int32"
          },
          "limit": {
            "type": "integer",
            "description": "The most records this page can hold (the take you asked for; at most 200, 50 when you gave none).",
            "format": "int32"
          },
          "hasMore": {
            "type": "boolean",
            "description": "True when more records follow this page. Ask for the next page by adding limit to the skip.",
            "readOnly": true
          },
          "hasPrevious": {
            "type": "boolean",
            "description": "True when records come before this page (offset above 0).",
            "readOnly": true
          }
        },
        "description": "One page of a list of ScanRun."
      },
      "PagedResultOfSecurityEventSummary": {
        "required": [
          "hasMore",
          "hasPrevious",
          "items",
          "limit",
          "offset",
          "total"
        ],
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SecurityEventSummary"
            },
            "description": "The records on this page."
          },
          "total": {
            "type": "integer",
            "description": "How many records match in all, across every page.",
            "format": "int32"
          },
          "offset": {
            "type": "integer",
            "description": "How many records were skipped before this page (the skip you asked for, never below 0).",
            "format": "int32"
          },
          "limit": {
            "type": "integer",
            "description": "The most records this page can hold (the take you asked for; at most 200, 50 when you gave none).",
            "format": "int32"
          },
          "hasMore": {
            "type": "boolean",
            "description": "True when more records follow this page. Ask for the next page by adding limit to the skip.",
            "readOnly": true
          },
          "hasPrevious": {
            "type": "boolean",
            "description": "True when records come before this page (offset above 0).",
            "readOnly": true
          }
        },
        "description": "One page of a list of SecurityEventSummary."
      },
      "PagedResultOfSitePolicy": {
        "required": [
          "hasMore",
          "hasPrevious",
          "items",
          "limit",
          "offset",
          "total"
        ],
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SitePolicy"
            },
            "description": "The records on this page."
          },
          "total": {
            "type": "integer",
            "description": "How many records match in all, across every page.",
            "format": "int32"
          },
          "offset": {
            "type": "integer",
            "description": "How many records were skipped before this page (the skip you asked for, never below 0).",
            "format": "int32"
          },
          "limit": {
            "type": "integer",
            "description": "The most records this page can hold (the take you asked for; at most 200, 50 when you gave none).",
            "format": "int32"
          },
          "hasMore": {
            "type": "boolean",
            "description": "True when more records follow this page. Ask for the next page by adding limit to the skip.",
            "readOnly": true
          },
          "hasPrevious": {
            "type": "boolean",
            "description": "True when records come before this page (offset above 0).",
            "readOnly": true
          }
        },
        "description": "One page of a list of SitePolicy."
      },
      "PatchStage": {
        "enum": [
          "DetectionOnly",
          "Blocking"
        ],
        "type": "string",
        "description": "Whether a virtual patch only records what it matches or also blocks it.\n\n- `DetectionOnly`: The patch records matching requests and blocks nothing, even when the site is On. Use it to check the patch first.\n- `Blocking`: The patch refuses matching requests with a 403.",
        "x-enum-descriptions": {
          "DetectionOnly": "The patch records matching requests and blocks nothing, even when the site is On. Use it to check the patch first.",
          "Blocking": "The patch refuses matching requests with a 403."
        }
      },
      "PatchStageRequest": {
        "required": [
          "stage"
        ],
        "type": "object",
        "properties": {
          "stage": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PatchStage"
              }
            ],
            "description": "The stage to switch the patch to: DetectionOnly to record only, or Blocking to refuse matching requests."
          }
        },
        "description": "A request to switch a virtual patch between detecting and blocking."
      },
      "ProblemDetails": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "A web address that identifies the kind of problem, when there is one.",
            "nullable": true
          },
          "title": {
            "type": "string",
            "description": "A short summary of the problem.",
            "nullable": true
          },
          "status": {
            "type": "integer",
            "description": "The HTTP status code.",
            "format": "int32",
            "nullable": true
          },
          "detail": {
            "type": "string",
            "description": "A longer explanation of this problem, when there is one.",
            "nullable": true
          },
          "instance": {
            "type": "string",
            "description": "A reference for this occurrence of the problem, when there is one.",
            "nullable": true
          }
        },
        "additionalProperties": { },
        "description": "The standard error shape (RFC 7807) that some unexpected errors use, such as a server error that has no more specific reply."
      },
      "ProtectionFeatureView": {
        "required": [
          "detail",
          "enabled",
          "enforcing",
          "id",
          "name",
          "severity",
          "stats"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "A stable short name for the protection, for example geo or brute-force."
          },
          "name": {
            "type": "string",
            "description": "The protection's name as shown in the console."
          },
          "enabled": {
            "type": "boolean",
            "description": "Whether the protection is turned on."
          },
          "enforcing": {
            "type": "boolean",
            "description": "Whether a protection that is on is actually doing its job. Only meaningful when enabled is true."
          },
          "severity": {
            "type": "string",
            "description": "The overall verdict: Ok, Disabled (deliberately off), Warning (working but degraded) or Critical (on but not enforcing)."
          },
          "detail": {
            "type": "string",
            "description": "The current state in plain words."
          },
          "fix": {
            "type": "string",
            "description": "What to do next when the protection is not working properly. Null when severity is Ok or Disabled.",
            "nullable": true
          },
          "stats": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProtectionStat"
            },
            "description": "A few short figures for the protection, such as how many rules are loaded."
          },
          "page": {
            "type": "string",
            "description": "The console page where this protection is managed. Null if it has none.",
            "nullable": true
          },
          "checkedAt": {
            "type": "string",
            "description": "When this protection was last checked. Null if it has not been.",
            "format": "date-time",
            "nullable": true
          }
        },
        "description": "The state of one protection: whether it is on, whether it is really enforcing, and what to do if it is not."
      },
      "ProtectionStat": {
        "required": [
          "label",
          "value"
        ],
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "description": "What the figure counts, for example firewall rules."
          },
          "value": {
            "type": "string",
            "description": "The figure, already formatted as text."
          }
        },
        "description": "One short figure shown on a protection's card, such as a count of rules."
      },
      "ProtectionStatusView": {
        "required": [
          "allHealthy",
          "available",
          "criticalCount",
          "features",
          "problemCount",
          "problems",
          "summary",
          "warningCount"
        ],
        "type": "object",
        "properties": {
          "available": {
            "type": "boolean",
            "description": "False only for a short time after PortWall starts, before the first check has finished; the other fields are then empty."
          },
          "generatedAt": {
            "type": "string",
            "description": "When these checks were made. Null while available is false.",
            "format": "date-time",
            "nullable": true
          },
          "summary": {
            "type": "string",
            "description": "One line saying how protection is doing, for example how many protections are on and enforcing."
          },
          "criticalCount": {
            "type": "integer",
            "description": "How many protections are turned on but not enforcing anything.",
            "format": "int32"
          },
          "warningCount": {
            "type": "integer",
            "description": "How many protections are enforcing but degraded.",
            "format": "int32"
          },
          "problemCount": {
            "type": "integer",
            "description": "criticalCount plus warningCount: the number of protections that need attention.",
            "format": "int32"
          },
          "allHealthy": {
            "type": "boolean",
            "description": "True when problemCount is 0. Protections that are deliberately off do not count as problems. False while available is false."
          },
          "features": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProtectionFeatureView"
            },
            "description": "Every protection and its state."
          },
          "problems": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProtectionFeatureView"
            },
            "description": "Only the protections that need attention, most serious first. Each has a fix."
          }
        },
        "description": "Whether every protection that is turned on is actually enforcing, as shown in the Overview's Protection status cards."
      },
      "QuarantineContent": {
        "required": [
          "isBinary",
          "ok",
          "originalPath",
          "sha256",
          "signature",
          "size",
          "text",
          "truncated"
        ],
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "True if the file could be read."
          },
          "error": {
            "type": "string",
            "description": "Why the file could not be read. Null when ok is true.",
            "nullable": true
          },
          "sha256": {
            "type": "string",
            "description": "SHA-256 hash of the file's content, as hex."
          },
          "originalPath": {
            "type": "string",
            "description": "Where the file was before it was quarantined."
          },
          "signature": {
            "type": "string",
            "description": "The name of the malware signature that matched the file."
          },
          "size": {
            "type": "integer",
            "description": "Size of the whole file in bytes. 0 if the file could not be read.",
            "format": "int64"
          },
          "truncated": {
            "type": "boolean",
            "description": "True if the file is larger than the preview, which holds only the first 64 KB."
          },
          "isBinary": {
            "type": "boolean",
            "description": "True if the file looks like binary data rather than text."
          },
          "text": {
            "type": "string",
            "description": "The start of the file, with each byte shown as one character. Empty if the file could not be read."
          }
        },
        "description": "A read-only preview of a quarantined file, so you can see what was caught without restoring it."
      },
      "QuarantineDone": {
        "required": [
          "done"
        ],
        "type": "object",
        "properties": {
          "done": {
            "type": "boolean",
            "description": "True when the file was restored or deleted."
          }
        },
        "description": "Reply confirming that the quarantined file was restored or deleted."
      },
      "QuarantineEntry": {
        "required": [
          "originalPath",
          "quarantinedAt",
          "sha256",
          "signature"
        ],
        "type": "object",
        "properties": {
          "sha256": {
            "type": "string",
            "description": "SHA-256 hash of the file's content, as hex. It identifies the quarantined file in the API."
          },
          "originalPath": {
            "type": "string",
            "description": "Where the file was before it was quarantined, and where a restore puts it back."
          },
          "site": {
            "type": "string",
            "description": "The site the file belonged to. Null if it was not tied to a site.",
            "nullable": true
          },
          "signature": {
            "type": "string",
            "description": "The name of the malware signature that matched the file."
          },
          "quarantinedAt": {
            "type": "string",
            "description": "When the file was quarantined.",
            "format": "date-time"
          }
        },
        "description": "A file that PortWall moved into quarantine because a scan found it malicious."
      },
      "QuarantineOp": {
        "enum": [
          "Restore",
          "Delete"
        ],
        "type": "string",
        "description": "What to do with a quarantined file.\n\n- `Restore`: Put the file back where it was found, as the original file.\n- `Delete`: Remove the file permanently.",
        "x-enum-descriptions": {
          "Restore": "Put the file back where it was found, as the original file.",
          "Delete": "Remove the file permanently."
        }
      },
      "QuarantineRequestDto": {
        "required": [
          "op"
        ],
        "type": "object",
        "properties": {
          "op": {
            "allOf": [
              {
                "$ref": "#/components/schemas/QuarantineOp"
              }
            ],
            "description": "Whether to restore the file to where it came from, or delete it for good."
          }
        },
        "description": "Request body that says whether to restore or delete a quarantined file."
      },
      "QuarantineRequested": {
        "required": [
          "id",
          "requested"
        ],
        "type": "object",
        "properties": {
          "requested": {
            "type": "boolean",
            "description": "True when the request was recorded for the host to decide."
          },
          "id": {
            "type": "string",
            "description": "The ID of the request."
          }
        },
        "description": "Reply confirming that the request was sent to the host for approval."
      },
      "ReportCount": {
        "required": [
          "count",
          "key"
        ],
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "What is being counted, such as an address, rule ID, site, country or kind of event."
          },
          "count": {
            "type": "integer",
            "description": "How many times it occurred in the period.",
            "format": "int32"
          }
        },
        "description": "A name and how many times it occurred, used for the top-10 and by-kind lists in a report."
      },
      "ReportPeriod": {
        "required": [
          "from",
          "label",
          "to"
        ],
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "description": "Start of the period; events at or after this time are counted.",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "description": "End of the period; events before this time are counted.",
            "format": "date-time"
          },
          "label": {
            "type": "string",
            "description": "A short name for the period, for example Last 7 days."
          }
        },
        "description": "The span of time a security report covers."
      },
      "RetryLater": {
        "required": [
          "error"
        ],
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Why the scan was refused, in a sentence."
          },
          "retryAt": {
            "type": "string",
            "description": "The earliest time a new scan is allowed.",
            "format": "date-time",
            "nullable": true
          }
        },
        "description": "Reply when a scan is refused because the site was scanned too recently."
      },
      "RollbackResult": {
        "required": [
          "newVersion",
          "restored"
        ],
        "type": "object",
        "properties": {
          "restored": {
            "type": "integer",
            "description": "The revision number whose settings were restored.",
            "format": "int32"
          },
          "newVersion": {
            "type": "integer",
            "description": "The new revision created by the rollback, holding the restored settings.",
            "format": "int32"
          }
        },
        "description": "The result of rolling a site back to an earlier saved revision."
      },
      "RuleExclusion": {
        "required": [
          "reason",
          "ruleId"
        ],
        "type": "object",
        "properties": {
          "ruleId": {
            "type": "string",
            "description": "The OWASP Core Rule Set rule to switch off, as a number such as 942100 (shown in the Attack Feed). When limited to a route or parameter, one id or one range such as 941000-941999."
          },
          "route": {
            "type": "string",
            "description": "Limits the exclusion to requests whose path starts with this text, for example /checkout. Empty to apply on every path.",
            "nullable": true
          },
          "param": {
            "type": "string",
            "description": "Limits the exclusion to one field, such as a form field, cookie or header name, instead of the whole rule. Empty to exclude the rule entirely.",
            "nullable": true
          },
          "reason": {
            "type": "string",
            "description": "Why the exclusion exists, so it can be reviewed later. Must not contain double quotes, backticks, braces or line breaks."
          },
          "target": {
            "type": "string",
            "description": "Where the parameter lives: ARGS (query and form fields, the default when empty), ARGS_NAMES, REQUEST_COOKIES, REQUEST_COOKIES_NAMES, REQUEST_HEADERS or REQUEST_HEADERS_NAMES. Only used with param.",
            "nullable": true
          }
        },
        "description": "A narrow exception that stops one OWASP Core Rule Set rule from firing, optionally only on one path or for one parameter, with the reason it exists."
      },
      "RuleHit": {
        "required": [
          "isApiProtection",
          "isGeo",
          "message",
          "owaspCategory",
          "redactedMatch",
          "ruleId",
          "severity"
        ],
        "type": "object",
        "properties": {
          "ruleId": {
            "type": "string",
            "description": "The ID of the rule that fired."
          },
          "severity": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RuleSeverity"
              }
            ],
            "description": "How serious the rule considers the match."
          },
          "owaspCategory": {
            "type": "string",
            "description": "The kind of attack the rule is for: an OWASP Top 10 code such as A03, an attack family such as attack-sqli, api-protection for an API protection finding, or empty when the rule has none."
          },
          "redactedMatch": {
            "type": "string",
            "description": "The part of the request that matched, shortened and with secrets removed."
          },
          "message": {
            "type": "string",
            "description": "A plain-language description of what the rule detected. May be empty."
          },
          "isApiProtection": {
            "type": "boolean",
            "description": "True when this is an API protection finding (a request outside your API specification, for example). These are shown in the feed but do not add to an address's score.",
            "readOnly": true
          },
          "isGeo": {
            "type": "boolean",
            "description": "True when the rule is a geo blocking rule: the request was refused, or logged, because of the country it came from. A request only geo blocking acted on is counted on the Geo Blocking page and is not listed in the attack feed.",
            "readOnly": true
          }
        },
        "description": "One rule that fired on a request, with how serious it is and what it matched."
      },
      "RuleSeverity": {
        "enum": [
          "Notice",
          "Warning",
          "Error",
          "Critical"
        ],
        "type": "string",
        "description": "How serious a WAF rule judges what it matched.\n\n- `Notice`: The lowest severity: unusual but rarely harmful on its own.\n- `Warning`: A suspicious request that may be harmless.\n- `Error`: A request that very likely carries an attack.\n- `Critical`: The highest severity: a clear attack.",
        "x-enum-descriptions": {
          "Notice": "The lowest severity: unusual but rarely harmful on its own.",
          "Warning": "A suspicious request that may be harmless.",
          "Error": "A request that very likely carries an attack.",
          "Critical": "The highest severity: a clear attack."
        }
      },
      "ScanRun": {
        "required": [
          "errors",
          "filesScanned",
          "filesSkipped",
          "id",
          "requestedBy",
          "startedAt",
          "status",
          "threats",
          "threatsFound"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The scan's ID, used to follow it."
          },
          "site": {
            "type": "string",
            "description": "The site that was scanned. Null when a folder was scanned by path instead of by site.",
            "nullable": true
          },
          "requestedBy": {
            "type": "string",
            "description": "Who started the scan: the name of the API key or user."
          },
          "status": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ScanRunStatus"
              }
            ],
            "description": "Whether the scan is still going, finished, or failed."
          },
          "startedAt": {
            "type": "string",
            "description": "When the scan started.",
            "format": "date-time"
          },
          "completedAt": {
            "type": "string",
            "description": "When the scan finished. Null while it is still running.",
            "format": "date-time",
            "nullable": true
          },
          "filesScanned": {
            "type": "integer",
            "description": "How many files were checked.",
            "format": "int32"
          },
          "filesSkipped": {
            "type": "integer",
            "description": "How many files were not checked, for example because they were over the size limit.",
            "format": "int32"
          },
          "errors": {
            "type": "integer",
            "description": "How many files could not be checked because of an error.",
            "format": "int32"
          },
          "threatsFound": {
            "type": "integer",
            "description": "How many malicious files the scan found.",
            "format": "int32"
          },
          "error": {
            "type": "string",
            "description": "Why the scan failed. Null unless the status is Failed.",
            "nullable": true
          },
          "threats": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ScanThreatInfo"
            },
            "description": "The malicious files found, each with its signature and whether it was quarantined. Empty when none were found."
          }
        },
        "description": "One scan of a site's files for malware, with its progress and results."
      },
      "ScanRunStatus": {
        "enum": [
          "Running",
          "Completed",
          "Failed"
        ],
        "type": "string",
        "description": "Where a file scan has got to.\n\n- `Running`: The scan is still going.\n- `Completed`: The scan finished and checked the files.\n- `Failed`: The scan stopped because of an error; the error field says why.",
        "x-enum-descriptions": {
          "Running": "The scan is still going.",
          "Completed": "The scan finished and checked the files.",
          "Failed": "The scan stopped because of an error; the error field says why."
        }
      },
      "ScanStarted": {
        "required": [
          "runId"
        ],
        "type": "object",
        "properties": {
          "runId": {
            "type": "string",
            "description": "The ID of the scan that was started. Find it in the site's list of scans."
          }
        },
        "description": "Reply to starting a scan, giving the ID to follow it by."
      },
      "ScanThreatInfo": {
        "required": [
          "path",
          "quarantined",
          "signature"
        ],
        "type": "object",
        "properties": {
          "path": {
            "type": "string",
            "description": "Full path of the malicious file on the server."
          },
          "signature": {
            "type": "string",
            "description": "The name of the malware signature that matched the file."
          },
          "quarantined": {
            "type": "boolean",
            "description": "True if the file was moved into quarantine; false if it was left in place on disk."
          }
        },
        "description": "One malicious file that a scan found."
      },
      "SearchEnginesState": {
        "required": [
          "letThrough"
        ],
        "type": "object",
        "properties": {
          "letThrough": {
            "type": "boolean",
            "description": "Whether search-engine crawlers are now let through."
          }
        },
        "description": "Whether search-engine crawlers are let through geo blocking, after you changed it."
      },
      "SecurityEventKind": {
        "enum": [
          "CrsHit",
          "RateLimitTrip",
          "Probe404",
          "HoneypotTouch",
          "ScanResult",
          "FingerprintAnomaly",
          "ThreatIntelMatch",
          "SelfTamper",
          "AuthFailure",
          "CredentialStuffing",
          "PortScan",
          "EgressAnomaly",
          "PrivilegeEscalation",
          "CampaignDetected",
          "BindingHazard",
          "ManagedCertExpiring",
          "ManagedCertReverted",
          "LicenseWarning",
          "LicenseViolation",
          "DataPlaneWedged",
          "HostNotServing",
          "ManagedCertIssuerChanged",
          "PanelCertRenewed",
          "PanelCertExpiring",
          "ConfigDiverged",
          "EnforcementGap",
          "ExcludedCrsHit",
          "VirtualPatchAdded",
          "DataPlaneSlow"
        ],
        "type": "string",
        "description": "What kind of event was recorded: attacks in the attack feed, and the operational incidents PortWall reports.\n\n- `CrsHit`: A web request tripped one or more OWASP Core Rule Set rules. The most common entry in the attack feed.\n- `RateLimitTrip`: A source sent requests faster than a rate limit allows.\n- `Probe404`: A source asked for pages that do not exist, a pattern typical of scanning for weak spots.\n- `HoneypotTouch`: A source requested a decoy path that no real visitor would, which marks it as hostile.\n- `ScanResult`: A file scan found malware in an upload.\n- `FingerprintAnomaly`: A client's TLS fingerprint or behaviour looks like an automated tool rather than a real browser.\n- `ThreatIntelMatch`: A source matched a threat-intelligence list.\n- `SelfTamper`: A PortWall file, rule set or configuration was changed outside PortWall.\n- `AuthFailure`: A failed login attempt on a monitored service, counted towards brute-force detection.\n- `CredentialStuffing`: Many failed logins against one site from many sources, which suggests a credential-stuffing attack.\n- `PortScan`: One source tried many different ports on this server in a short time, read from the firewall's dropped-packet log.\n- `EgressAnomaly`: A site made outbound connections that look like beaconing or that its outbound policy does not allow.\n- `PrivilegeEscalation`: The identity a website runs as did something that suggests a takeover, such as creating a local user, joining the Administrators group or starting a shell. A critical incident for you to investigate; it usually has no attacker address and nothing is blocked.\n- `CampaignDetected`: One attacker was seen hitting many different customers, which indicates a coordinated sweep. See the campaigns list.\n- `BindingHazard`: Operational incident, not an attack, kept out of the attack feed: an IIS site is bound to all addresses or the public address, so it could receive traffic that bypasses PortWall.\n- `ManagedCertExpiring`: Operational incident, not an attack, kept out of the attack feed: a PortWall-managed certificate is close to expiry because renewal is failing.\n- `ManagedCertReverted`: Operational incident, not an attack, kept out of the attack feed: a site's hostname now points at a different server, so PortWall stopped trying to issue its managed certificate.\n- `LicenseWarning`: Operational incident, not an attack, kept out of the attack feed: the licence could not be verified but is still in its grace period, and protection continues unchanged.\n- `LicenseViolation`: Operational incident, not an attack, kept out of the attack feed: the licence grace period has run out, so PortWall is detecting but no longer blocking until the licence is renewed.\n- `DataPlaneWedged`: Operational incident, not an attack, kept out of the attack feed: the engine is running but has stopped answering, so sites are not being served.\n- `HostNotServing`: Operational incident, not an attack, kept out of the attack feed: nothing is answering on the server's web address, so every site is down.\n- `ManagedCertIssuerChanged`: Operational incident, not an attack, kept out of the attack feed: a managed certificate is now issued by a different certificate authority than before.\n- `PanelCertRenewed`: Operational incident, not an attack, kept out of the attack feed: your hosting panel renewed a site's certificate and PortWall now serves the new one. Informational.\n- `PanelCertExpiring`: Operational incident, not an attack, kept out of the attack feed: a certificate issued by your hosting panel is close to expiry and has not been replaced; only the panel can renew it.\n- `ConfigDiverged`: Operational incident, not an attack, kept out of the attack feed: the engine is not running the configuration PortWall has saved and repeated attempts to apply it have failed.\n- `EnforcementGap`: Operational incident, not an attack, kept out of the attack feed: a protection is turned on but is not enforcing anything. See Protection status.\n- `ExcludedCrsHit`: A rule fired, but every rule it matched is on your exclusion list, so it is treated as a suppressed false positive. The console lists these separately; they never appear in the attack feed and never cause a block.\n- `VirtualPatchAdded`: Operational notice, not an attack, kept out of the attack feed: a new PortWall virtual patch for a known vulnerability now protects your sites.\n- `DataPlaneSlow`: Operational notice, not an attack, kept out of the attack feed: the engine is serving but answering PortWall's health check slowly. PortWall does not restart it.",
        "x-enum-descriptions": {
          "CrsHit": "A web request tripped one or more OWASP Core Rule Set rules. The most common entry in the attack feed.",
          "RateLimitTrip": "A source sent requests faster than a rate limit allows.",
          "Probe404": "A source asked for pages that do not exist, a pattern typical of scanning for weak spots.",
          "HoneypotTouch": "A source requested a decoy path that no real visitor would, which marks it as hostile.",
          "ScanResult": "A file scan found malware in an upload.",
          "FingerprintAnomaly": "A client's TLS fingerprint or behaviour looks like an automated tool rather than a real browser.",
          "ThreatIntelMatch": "A source matched a threat-intelligence list.",
          "SelfTamper": "A PortWall file, rule set or configuration was changed outside PortWall.",
          "AuthFailure": "A failed login attempt on a monitored service, counted towards brute-force detection.",
          "CredentialStuffing": "Many failed logins against one site from many sources, which suggests a credential-stuffing attack.",
          "PortScan": "One source tried many different ports on this server in a short time, read from the firewall's dropped-packet log.",
          "EgressAnomaly": "A site made outbound connections that look like beaconing or that its outbound policy does not allow.",
          "PrivilegeEscalation": "The identity a website runs as did something that suggests a takeover, such as creating a local user, joining the Administrators group or starting a shell. A critical incident for you to investigate; it usually has no attacker address and nothing is blocked.",
          "CampaignDetected": "One attacker was seen hitting many different customers, which indicates a coordinated sweep. See the campaigns list.",
          "BindingHazard": "Operational incident, not an attack, kept out of the attack feed: an IIS site is bound to all addresses or the public address, so it could receive traffic that bypasses PortWall.",
          "ManagedCertExpiring": "Operational incident, not an attack, kept out of the attack feed: a PortWall-managed certificate is close to expiry because renewal is failing.",
          "ManagedCertReverted": "Operational incident, not an attack, kept out of the attack feed: a site's hostname now points at a different server, so PortWall stopped trying to issue its managed certificate.",
          "LicenseWarning": "Operational incident, not an attack, kept out of the attack feed: the licence could not be verified but is still in its grace period, and protection continues unchanged.",
          "LicenseViolation": "Operational incident, not an attack, kept out of the attack feed: the licence grace period has run out, so PortWall is detecting but no longer blocking until the licence is renewed.",
          "DataPlaneWedged": "Operational incident, not an attack, kept out of the attack feed: the engine is running but has stopped answering, so sites are not being served.",
          "HostNotServing": "Operational incident, not an attack, kept out of the attack feed: nothing is answering on the server's web address, so every site is down.",
          "ManagedCertIssuerChanged": "Operational incident, not an attack, kept out of the attack feed: a managed certificate is now issued by a different certificate authority than before.",
          "PanelCertRenewed": "Operational incident, not an attack, kept out of the attack feed: your hosting panel renewed a site's certificate and PortWall now serves the new one. Informational.",
          "PanelCertExpiring": "Operational incident, not an attack, kept out of the attack feed: a certificate issued by your hosting panel is close to expiry and has not been replaced; only the panel can renew it.",
          "ConfigDiverged": "Operational incident, not an attack, kept out of the attack feed: the engine is not running the configuration PortWall has saved and repeated attempts to apply it have failed.",
          "EnforcementGap": "Operational incident, not an attack, kept out of the attack feed: a protection is turned on but is not enforcing anything. See Protection status.",
          "ExcludedCrsHit": "A rule fired, but every rule it matched is on your exclusion list, so it is treated as a suppressed false positive. The console lists these separately; they never appear in the attack feed and never cause a block.",
          "VirtualPatchAdded": "Operational notice, not an attack, kept out of the attack feed: a new PortWall virtual patch for a known vulnerability now protects your sites.",
          "DataPlaneSlow": "Operational notice, not an attack, kept out of the attack feed: the engine is serving but answering PortWall's health check slowly. PortWall does not restart it."
        }
      },
      "SecurityEventSummary": {
        "required": [
          "anomalyScore",
          "correlationId",
          "kind",
          "ruleIds",
          "site",
          "source",
          "sourceAllowlisted",
          "timestamp"
        ],
        "type": "object",
        "properties": {
          "timestamp": {
            "type": "string",
            "description": "When the event was detected.",
            "format": "date-time"
          },
          "source": {
            "type": "string",
            "description": "The IP address the event came from. 255.255.255.255 for operational incidents, which have no attacker address."
          },
          "site": {
            "type": "string",
            "description": "The site the event concerns, or a label such as host for events about the server itself."
          },
          "kind": {
            "type": "string",
            "description": "What sort of event this is, using the names listed under SecurityEventKind. Attack kinds appear in the attack feed; operational incidents are kept out of it."
          },
          "anomalyScore": {
            "type": "integer",
            "description": "How serious the event is; higher is worse. For a web request it is the total the OWASP Core Rule Set rules added up to, and 0 when no score applies.",
            "format": "int32"
          },
          "ruleIds": {
            "type": "string",
            "description": "The IDs of the rules that fired, separated by commas. Empty when the event did not come from a rule."
          },
          "correlationId": {
            "type": "string",
            "description": "An identifier tying the event to the request or detection it came from. May be empty."
          },
          "forensics": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ForensicSnapshot"
              }
            ],
            "description": "A copy of the request that caused the event. Null when none was captured, for example for events that are not web requests.",
            "nullable": true
          },
          "hits": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RuleHit"
            },
            "description": "The individual rules that fired, with their severity and what they matched. Null or empty when none apply.",
            "nullable": true
          },
          "country": {
            "type": "string",
            "description": "Two-letter ISO country code of the source, looked up when you read the feed. Null when unknown.",
            "nullable": true
          },
          "sourceAllowlisted": {
            "type": "boolean",
            "description": "True when the source is on the allowlist now: the event was flagged and logged but the address is deliberately never blocked."
          },
          "verdict": {
            "type": "string",
            "description": "What PortWall did with the request: Block when it refused it, Allow when it logged it and let it through (the site's protection only reports, or the rules that fired do not refuse on their own). Null for events that are not web requests, and for events recorded before PortWall 1.2.49.",
            "nullable": true
          },
          "responseStatus": {
            "type": "integer",
            "description": "The HTTP status PortWall answered the request with, for example 403 when it refused it. Null for events that are not web requests, and for events recorded before PortWall 1.2.49.",
            "format": "int32",
            "nullable": true
          }
        },
        "description": "One entry in the attack feed: a single detection against a site, with its source, the rules that fired and, where captured, the request itself."
      },
      "SecurityReport": {
        "required": [
          "activeBlocks",
          "blocksInPeriod",
          "criticalIncidents",
          "eventsByKind",
          "filesQuarantinedInPeriod",
          "generatedAt",
          "period",
          "scope",
          "topRules",
          "topSources",
          "totalEvents"
        ],
        "type": "object",
        "properties": {
          "period": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ReportPeriod"
              }
            ],
            "description": "The time span the report covers."
          },
          "generatedAt": {
            "type": "string",
            "description": "When the report was made.",
            "format": "date-time"
          },
          "scope": {
            "type": "string",
            "description": "What the report covers: All sites, or the number of sites for a tenant."
          },
          "totalEvents": {
            "type": "integer",
            "description": "How many attacks and other security events were detected in the period. Counted from the most recent 20,000 events.",
            "format": "int32"
          },
          "criticalIncidents": {
            "type": "integer",
            "description": "How many of those events were serious kinds: privilege escalation, tampering with PortWall, credential stuffing, unusual outbound traffic, or a malicious file found by a scan.",
            "format": "int32"
          },
          "eventsByKind": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReportCount"
            },
            "description": "How many events there were of each kind, most frequent first."
          },
          "topSources": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReportCount"
            },
            "description": "The ten addresses that caused the most events, most frequent first."
          },
          "topRules": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReportCount"
            },
            "description": "The ten rules that matched most often, most frequent first."
          },
          "activeBlocks": {
            "type": "integer",
            "description": "How many addresses are blocked right now. Always 0 in a tenant report, because blocks apply to the whole server.",
            "format": "int32"
          },
          "blocksInPeriod": {
            "type": "integer",
            "description": "How many address blocks were created in the period. Always 0 in a tenant report.",
            "format": "int32"
          },
          "filesQuarantinedInPeriod": {
            "type": "integer",
            "description": "How many files were quarantined in the period.",
            "format": "int32"
          },
          "topSites": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReportCount"
            },
            "description": "The ten sites with the most events, most frequent first.",
            "nullable": true
          },
          "topCountries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReportCount"
            },
            "description": "The ten countries (two-letter codes) the attacks came from, most frequent first. Empty when no country database is loaded.",
            "nullable": true
          }
        },
        "description": "A summary of the attacks PortWall saw and the blocks and quarantines it made over a period."
      },
      "SetLicenseKeyRequest": {
        "type": "object",
        "properties": {
          "licenseKey": {
            "type": "string",
            "description": "The licence key, as shown in the customer portal. Required; at most 200 characters.",
            "nullable": true
          }
        },
        "description": "The licence key to save for this installation."
      },
      "SetLicenseKeyResult": {
        "required": [
          "activating",
          "licenseKeyMasked",
          "saved"
        ],
        "type": "object",
        "properties": {
          "saved": {
            "type": "boolean",
            "description": "True when the key was saved."
          },
          "licenseKeyMasked": {
            "type": "string",
            "description": "The saved key with all but its last characters hidden."
          },
          "activating": {
            "type": "boolean",
            "description": "True when a licence check was asked for at once, so the new state shows within moments on GET /api/license. False when the key was saved but the check waits for the next scheduled one."
          }
        },
        "description": "The result of saving a licence key."
      },
      "SettingsSaved": {
        "required": [
          "applying",
          "applySeq",
          "saved"
        ],
        "type": "object",
        "properties": {
          "saved": {
            "type": "boolean",
            "description": "True when the settings were stored."
          },
          "applying": {
            "type": "boolean",
            "description": "True when PortWall is applying the settings in the background. The change is in force once the apply status reports it completed."
          },
          "applySeq": {
            "type": "integer",
            "description": "A number to wait for: the settings are in force when completed from GET /api/config-apply/status reaches this number.",
            "format": "int64"
          },
          "pending": {
            "type": "string",
            "description": "A note when the settings are saved but cannot be applied yet. Empty when there is nothing waiting.",
            "nullable": true
          }
        },
        "description": "The reply to saving settings that are applied in the background, with how to tell when they are in force."
      },
      "SiteAccess": {
        "required": [
          "role",
          "site"
        ],
        "type": "object",
        "properties": {
          "site": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SitePolicy"
              }
            ],
            "description": "The site's settings, as the tenant sees them."
          },
          "role": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SiteRole"
              }
            ],
            "description": "What the tenant may do with this site: look only, or also change its settings."
          }
        },
        "description": "A site a tenant can reach, together with the tenant's role on it."
      },
      "SiteDeletedWithWarning": {
        "required": [
          "deleted",
          "warning"
        ],
        "type": "object",
        "properties": {
          "deleted": {
            "type": "string",
            "description": "The name of the site whose PortWall settings were removed."
          },
          "warning": {
            "type": "string",
            "description": "Why the site is still in the running configuration and how to clear it."
          }
        },
        "description": "The reply when a site's PortWall settings were removed but the running configuration still contains the site."
      },
      "SiteModeResult": {
        "required": [
          "changed",
          "mode",
          "sites",
          "total"
        ],
        "type": "object",
        "properties": {
          "mode": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EngineMode"
              }
            ],
            "description": "The mode the sites were switched to."
          },
          "changed": {
            "type": "integer",
            "description": "How many sites actually changed. Sites already in that mode are not counted.",
            "format": "int32"
          },
          "total": {
            "type": "integer",
            "description": "How many sites the request covered.",
            "format": "int32"
          },
          "sites": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The names of the sites that changed."
          },
          "pending": {
            "type": "string",
            "description": "A note when the change is saved but cannot be applied yet. Empty when there is nothing waiting.",
            "nullable": true
          },
          "applying": {
            "type": "boolean",
            "description": "True when PortWall is applying the change in the background. Left out when no site needed changing.",
            "nullable": true
          },
          "applySeq": {
            "type": "integer",
            "description": "A number to wait for: the change is in force when completed from GET /api/config-apply/status reaches this number. Left out when no site needed changing.",
            "format": "int64",
            "nullable": true
          }
        },
        "description": "The result of switching several sites to one mode: which sites changed, and how to tell when the change is in force."
      },
      "SitePolicy": {
        "required": [
          "anomalyThreshold",
          "cmsProfile",
          "cmsRateOverrides",
          "exclusions",
          "failMode",
          "loginFailureBodyPattern",
          "loginPath",
          "loginProtectionOff",
          "loginSuccessStatuses",
          "mode",
          "paranoiaLevel",
          "runtime",
          "siteName"
        ],
        "type": "object",
        "properties": {
          "siteName": {
            "type": "string",
            "description": "The site's name, as it appears in PortWall and in the URL of the site endpoints."
          },
          "mode": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EngineMode"
              }
            ],
            "description": "What the WAF does for this site: Off, DetectionOnly (record attacks, block nothing) or On (block). New sites start in DetectionOnly."
          },
          "paranoiaLevel": {
            "type": "integer",
            "description": "How strict the OWASP Core Rule Set rules are, from 1 (default, fewest false positives) to 4 (strictest, more false positives). Raise it only after you have tuned exclusions.",
            "format": "int32"
          },
          "anomalyThreshold": {
            "type": "integer",
            "description": "The score a request must reach before it is blocked, from 1 to 1000 (default 5). A lower number blocks sooner; each rule that matches adds to the score.",
            "format": "int32"
          },
          "failMode": {
            "allOf": [
              {
                "$ref": "#/components/schemas/FailMode"
              }
            ],
            "description": "What happens to a request PortWall cannot fully inspect: Open lets it through; Closed refuses a body larger than the WAF inspection limit (413) and an upload the scanner could not check (403). Refuses only while mode is On. New sites have Open."
          },
          "exclusions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RuleExclusion"
            },
            "description": "The rule exclusions that apply to this site only. Use them to stop a rule that blocks legitimate requests. Empty for none."
          },
          "tenantId": {
            "type": "string",
            "description": "The tenant (hosting customer) that owns the site. Empty when the site belongs to no tenant. Set it with PUT /api/sites/{name}/owner; saving the site's settings does not change it.",
            "nullable": true
          },
          "runtime": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SiteRuntime"
              }
            ],
            "description": "The kind of server-side application the site runs. When set, uploads of executable files belonging to any other runtime are refused. Unknown applies no file-type rule."
          },
          "cmsProfile": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CmsProfile"
              }
            ],
            "description": "A ready-made protection preset for the site's CMS or forum, which adds request-rate limits and attack blocks for it. None applies no preset."
          },
          "loginPath": {
            "type": "string",
            "description": "The path of the site's login page, for example /wp-login.php. When set, only that page is watched for failed logins. Empty to let PortWall watch the pages that look like a login form."
          },
          "loginSuccessStatuses": {
            "type": "string",
            "description": "Comma-separated HTTP status codes that count as a successful login, for example 200 or 200,302, for an app or API that does not redirect after a login. A redirect always counts as a success. Needs a login path; empty for the default."
          },
          "loginFailureBodyPattern": {
            "type": "string",
            "description": "Text that appears only on the page a failed login returns, such as Invalid password. When set, the page text decides whether a login failed, not its status. Needs a login path; must not contain double quotes or backticks. Empty for the default."
          },
          "cmsRateOverrides": {
            "type": "string",
            "description": "Changes to the CMS preset's request-rate limits, as name=number pairs separated by commas, for example xmlrpc=15,login=30. Empty to use the preset's own limits."
          },
          "globalExclusionOptOuts": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The server-wide default exclusions this site does not use, each identified by its rule ID, route and parameter joined with a vertical bar. Empty for a site that uses all of them.",
            "nullable": true
          },
          "loginProtectionOff": {
            "type": "boolean",
            "description": "True to turn off login brute-force protection for this site. False (default) protects the site: its login path if one is set, otherwise the pages that look like a login form."
          }
        },
        "description": "One site's protection settings: its WAF mode, rule strictness, exclusions, login protection and CMS preset."
      },
      "SitePolicyRevision": {
        "required": [
          "actor",
          "at",
          "policy",
          "siteName",
          "version"
        ],
        "type": "object",
        "properties": {
          "version": {
            "type": "integer",
            "description": "The revision number. It counts up for each site and is the number you give to roll back.",
            "format": "int32"
          },
          "siteName": {
            "type": "string",
            "description": "The site the revision belongs to."
          },
          "at": {
            "type": "string",
            "description": "When the revision was saved.",
            "format": "date-time"
          },
          "actor": {
            "type": "string",
            "description": "Who saved it: the signed-in user or API key name."
          },
          "policy": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SitePolicy"
              }
            ],
            "description": "The site's settings exactly as they were in this revision."
          }
        },
        "description": "A saved copy of a site's settings, kept each time the settings are saved so you can roll back to it."
      },
      "SitePolicySaved": {
        "required": [
          "applying",
          "applySeq",
          "policy",
          "version"
        ],
        "type": "object",
        "properties": {
          "policy": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SitePolicy"
              }
            ],
            "description": "The site's settings as stored."
          },
          "version": {
            "type": "integer",
            "description": "The revision number created by this save. Use it to roll back later.",
            "format": "int32"
          },
          "applying": {
            "type": "boolean",
            "description": "True when PortWall is applying the change in the background. The change is in force once the apply status reports it completed."
          },
          "applySeq": {
            "type": "integer",
            "description": "A number to wait for: the change is in force when completed from GET /api/config-apply/status reaches this number.",
            "format": "int64"
          },
          "pending": {
            "type": "string",
            "description": "A note when the change is saved but cannot be applied yet. Empty when there is nothing waiting.",
            "nullable": true
          }
        },
        "description": "The reply to saving a site's settings: the settings as stored, the new revision number, and how to tell when the change is in force."
      },
      "SitePolicyUpdate": {
        "required": [
          "anomalyThreshold",
          "cmsProfile",
          "cmsRateOverrides",
          "failMode",
          "loginFailureBodyPattern",
          "loginPath",
          "loginProtectionOff",
          "loginSuccessStatuses",
          "mode",
          "paranoiaLevel",
          "runtime"
        ],
        "type": "object",
        "properties": {
          "mode": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EngineMode"
              }
            ],
            "description": "What the WAF does for this site: Off, DetectionOnly (record attacks, block nothing) or On (block). New sites start in DetectionOnly."
          },
          "paranoiaLevel": {
            "type": "integer",
            "description": "How strict the OWASP Core Rule Set rules are, from 1 (default, fewest false positives) to 4 (strictest, more false positives). Raise it only after you have tuned exclusions.",
            "format": "int32"
          },
          "anomalyThreshold": {
            "type": "integer",
            "description": "The score a request must reach before it is blocked, from 1 to 1000 (default 5). A lower number blocks sooner; each rule that matches adds to the score.",
            "format": "int32"
          },
          "failMode": {
            "allOf": [
              {
                "$ref": "#/components/schemas/FailMode"
              }
            ],
            "description": "What happens to a request PortWall cannot fully inspect: Open lets it through; Closed refuses a body larger than the WAF inspection limit (413) and an upload the scanner could not check (403). Refuses only while mode is On. New sites have Open."
          },
          "exclusions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RuleExclusion"
            },
            "description": "The rule exclusions that apply to this site only. Use them to stop a rule that blocks legitimate requests. Empty for none.",
            "nullable": true
          },
          "runtime": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SiteRuntime"
              }
            ],
            "description": "The kind of server-side application the site runs. When set, uploads of executable files belonging to any other runtime are refused. Unknown applies no file-type rule."
          },
          "cmsProfile": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CmsProfile"
              }
            ],
            "description": "A ready-made protection preset for the site's CMS or forum, which adds request-rate limits and attack blocks for it. None applies no preset."
          },
          "loginPath": {
            "type": "string",
            "description": "The path of the site's login page, for example /wp-login.php. When set, only that page is watched for failed logins. Empty to let PortWall watch the pages that look like a login form."
          },
          "loginSuccessStatuses": {
            "type": "string",
            "description": "Comma-separated HTTP status codes that count as a successful login, for example 200 or 200,302, for an app or API that does not redirect after a login. A redirect always counts as a success. Needs a login path; empty for the default."
          },
          "loginFailureBodyPattern": {
            "type": "string",
            "description": "Text that appears only on the page a failed login returns, such as Invalid password. When set, the page text decides whether a login failed, not its status. Needs a login path; must not contain double quotes or backticks. Empty for the default."
          },
          "cmsRateOverrides": {
            "type": "string",
            "description": "Changes to the CMS preset's request-rate limits, as name=number pairs separated by commas, for example xmlrpc=15,login=30. Empty to use the preset's own limits."
          },
          "globalExclusionOptOuts": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The server-wide default exclusions this site does not use, each identified by its rule ID, route and parameter joined with a vertical bar. Empty for a site that uses all of them.",
            "nullable": true
          },
          "loginProtectionOff": {
            "type": "boolean",
            "description": "True to turn off login brute-force protection for this site. False (default) protects the site: its login path if one is set, otherwise the pages that look like a login form."
          }
        },
        "description": "The settings to save for one site. Send the whole object, as GET returns it, with your changes; the site name comes from the URL."
      },
      "SiteRole": {
        "enum": [
          "Viewer",
          "Manager"
        ],
        "type": "string",
        "description": "What a tenant's user or key may do on a site.\n\n- `Viewer`: May look at the site, its attacks and its settings, but not change anything.\n- `Manager`: May do everything a Viewer can, and also change the site's settings within the tenant policy.",
        "x-enum-descriptions": {
          "Viewer": "May look at the site, its attacks and its settings, but not change anything.",
          "Manager": "May do everything a Viewer can, and also change the site's settings within the tenant policy."
        }
      },
      "SiteRuntime": {
        "enum": [
          "Unknown",
          "DotNet",
          "Php",
          "ClassicAsp",
          "Static"
        ],
        "type": "string",
        "description": "The kind of server-side application a site runs, used to refuse uploads of files that site should never serve.\n\n- `Unknown`: No runtime is declared, so no file-type rule is applied to uploads.\n- `DotNet`: An ASP.NET site. Uploads of PHP, classic ASP and other runtimes' executable files are refused.\n- `Php`: A PHP site. Uploads of ASP.NET, classic ASP and other runtimes' executable files are refused.\n- `ClassicAsp`: A classic ASP site. Uploads of ASP.NET, PHP and other runtimes' executable files are refused.\n- `Static`: A static site that should serve no server-side code. Uploads of any executable file type are refused.",
        "x-enum-descriptions": {
          "Unknown": "No runtime is declared, so no file-type rule is applied to uploads.",
          "DotNet": "An ASP.NET site. Uploads of PHP, classic ASP and other runtimes' executable files are refused.",
          "Php": "A PHP site. Uploads of ASP.NET, classic ASP and other runtimes' executable files are refused.",
          "ClassicAsp": "A classic ASP site. Uploads of ASP.NET, PHP and other runtimes' executable files are refused.",
          "Static": "A static site that should serve no server-side code. Uploads of any executable file type are refused."
        }
      },
      "SwitchState": {
        "required": [
          "enabled"
        ],
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Whether the switch is now on."
          }
        },
        "description": "The state of a master switch after you changed it."
      },
      "TenantErrors": {
        "required": [
          "errors"
        ],
        "type": "object",
        "properties": {
          "errors": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "One sentence for each reason the change was refused. Nothing was changed."
          }
        },
        "description": "Reply listing why the host's tenant policy refused a change."
      },
      "TenantExclusionPolicy": {
        "enum": [
          "SelfApply",
          "RequestApproval",
          "Disabled"
        ],
        "type": "string",
        "description": "Whether tenants may add rule exclusions to their own sites.\n\n- `SelfApply`: Tenants add and remove their own rule exclusions, up to the limit.\n- `RequestApproval`: Tenants cannot change exclusions themselves; changes need the host's approval.\n- `Disabled`: Tenants cannot change exclusions at all; only the host can.",
        "x-enum-descriptions": {
          "SelfApply": "Tenants add and remove their own rule exclusions, up to the limit.",
          "RequestApproval": "Tenants cannot change exclusions themselves; changes need the host's approval.",
          "Disabled": "Tenants cannot change exclusions at all; only the host can."
        }
      },
      "TenantPolicyOverride": {
        "required": [
          "tenantId"
        ],
        "type": "object",
        "properties": {
          "tenantId": {
            "type": "string",
            "description": "The tenant this override is for. The tenant ID in the URL is used if the two differ."
          },
          "allowedModes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EngineMode"
            },
            "description": "The WAF modes this tenant may switch its sites to. Null follows the tenant policy; if set, it must list at least one mode.",
            "nullable": true
          },
          "defaultMode": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EngineMode"
              }
            ],
            "description": "The mode a new site of this tenant starts in. Null follows the tenant policy.",
            "nullable": true
          },
          "allowParanoiaEdit": {
            "type": "boolean",
            "description": "Whether this tenant may change the paranoia level of its sites. Null follows the tenant policy.",
            "nullable": true
          },
          "paranoiaMin": {
            "type": "integer",
            "description": "Lowest paranoia level this tenant may choose, from 1 to 4. Null follows the tenant policy.",
            "format": "int32",
            "nullable": true
          },
          "paranoiaMax": {
            "type": "integer",
            "description": "Highest paranoia level this tenant may choose, from 1 to 4 and not below the lowest. Null follows the tenant policy.",
            "format": "int32",
            "nullable": true
          },
          "allowThresholdEdit": {
            "type": "boolean",
            "description": "Whether this tenant may change the blocking sensitivity of its sites. Null follows the tenant policy.",
            "nullable": true
          },
          "thresholdMin": {
            "type": "integer",
            "description": "Lowest blocking threshold this tenant may set, at least 1. A lower value blocks sooner. Null follows the tenant policy.",
            "format": "int32",
            "nullable": true
          },
          "thresholdMax": {
            "type": "integer",
            "description": "Highest blocking threshold this tenant may set, not below the lowest. A higher value blocks later. Null follows the tenant policy.",
            "format": "int32",
            "nullable": true
          },
          "exclusionPolicy": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TenantExclusionPolicy"
              }
            ],
            "description": "How this tenant may handle rule exclusions. Null follows the tenant policy.",
            "nullable": true
          },
          "maxTenantExclusions": {
            "type": "integer",
            "description": "Most rule exclusions this tenant may have on one site. Null follows the tenant policy.",
            "format": "int32",
            "nullable": true
          },
          "allowTenantScan": {
            "type": "boolean",
            "description": "Whether this tenant may start a malware scan of its own sites. Null follows the tenant policy.",
            "nullable": true
          },
          "tenantScanMinIntervalMinutes": {
            "type": "integer",
            "description": "Least time, in minutes, between two scans of the same site of this tenant. 0 means no wait. Null follows the tenant policy.",
            "format": "int32",
            "nullable": true
          },
          "maxScanFileSizeBytes": {
            "type": "integer",
            "description": "Largest file, in bytes, that this tenant's scans will check; bigger files are skipped. Null follows the tenant policy.",
            "format": "int64",
            "nullable": true
          },
          "quarantineAction": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TenantQuarantineAction"
              }
            ],
            "description": "What this tenant may do with a quarantined file. Null follows the tenant policy.",
            "nullable": true
          },
          "allowCmsProtection": {
            "type": "boolean",
            "description": "Whether this tenant may choose a CMS protection profile and change login protection and rate-limit settings. Null follows the tenant policy.",
            "nullable": true
          },
          "rateLimitEventsFloor": {
            "type": "integer",
            "description": "Lowest requests-per-minute value this tenant may set for a CMS protection rate limit. Null follows the tenant policy.",
            "format": "int32",
            "nullable": true
          },
          "allowRuntimeEdit": {
            "type": "boolean",
            "description": "Whether this tenant may declare the runtime of its sites. Null follows the tenant policy.",
            "nullable": true
          },
          "allowManagedCert": {
            "type": "boolean",
            "description": "Whether this tenant may turn on a PortWall-managed HTTPS certificate for its own sites. Null follows the tenant policy.",
            "nullable": true
          },
          "maxManagedCerts": {
            "type": "integer",
            "description": "Most managed certificates this tenant may have. 0 means no limit. Null follows the tenant policy.",
            "format": "int32",
            "nullable": true
          }
        },
        "description": "Changes to the tenant policy for one tenant; any field left null follows the tenant policy for all tenants."
      },
      "TenantQuarantineAction": {
        "enum": [
          "RequestApproval",
          "ViewOnly",
          "RestoreDelete"
        ],
        "type": "string",
        "description": "What tenants may do with their own quarantined files.\n\n- `RequestApproval`: Tenants see their quarantined files and may ask to restore or delete one; the host decides.\n- `ViewOnly`: Tenants may only look at quarantined files; the host does all restoring and deleting.\n- `RestoreDelete`: Tenants may restore or permanently delete their own quarantined files with no approval.",
        "x-enum-descriptions": {
          "RequestApproval": "Tenants see their quarantined files and may ask to restore or delete one; the host decides.",
          "ViewOnly": "Tenants may only look at quarantined files; the host does all restoring and deleting.",
          "RestoreDelete": "Tenants may restore or permanently delete their own quarantined files with no approval."
        }
      },
      "TenantSelfServicePolicy": {
        "required": [
          "allowCmsProtection",
          "allowedModes",
          "allowManagedCert",
          "allowParanoiaEdit",
          "allowRuntimeEdit",
          "allowTenantScan",
          "allowThresholdEdit",
          "defaultMode",
          "exclusionPolicy",
          "maxManagedCerts",
          "maxScanFileSizeBytes",
          "maxTenantExclusions",
          "paranoiaMax",
          "paranoiaMin",
          "quarantineAction",
          "rateLimitEventsFloor",
          "tenantScanMinIntervalMinutes",
          "thresholdMax",
          "thresholdMin"
        ],
        "type": "object",
        "properties": {
          "allowedModes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EngineMode"
            },
            "description": "The WAF modes a tenant may switch its sites to. A tenant asking for a mode not listed is refused."
          },
          "defaultMode": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EngineMode"
              }
            ],
            "description": "The mode a new tenant site starts in. Default: On (blocking)."
          },
          "allowParanoiaEdit": {
            "type": "boolean",
            "description": "Whether tenants may change the paranoia level of their sites. If false, only the host can. Default: true."
          },
          "paranoiaMin": {
            "type": "integer",
            "description": "Lowest paranoia level a tenant may choose, from 1 to 4. Default: 1.",
            "format": "int32"
          },
          "paranoiaMax": {
            "type": "integer",
            "description": "Highest paranoia level a tenant may choose, from 1 to 4 and not below the lowest. Default: 2.",
            "format": "int32"
          },
          "allowThresholdEdit": {
            "type": "boolean",
            "description": "Whether tenants may change the blocking sensitivity (the anomaly threshold) of their sites. If false, only the host can. Default: true."
          },
          "thresholdMin": {
            "type": "integer",
            "description": "Lowest blocking threshold a tenant may set, at least 1. A lower value blocks sooner. Default: 3.",
            "format": "int32"
          },
          "thresholdMax": {
            "type": "integer",
            "description": "Highest blocking threshold a tenant may set, not below the lowest. A higher value blocks later. Default: 7.",
            "format": "int32"
          },
          "exclusionPolicy": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TenantExclusionPolicy"
              }
            ],
            "description": "Whether tenants may add rule exclusions (to stop a rule blocking legitimate traffic) themselves, must ask the host, or may not at all. Default: SelfApply."
          },
          "maxTenantExclusions": {
            "type": "integer",
            "description": "Most rule exclusions a tenant may have on one site when exclusions are self-applied. Default: 25.",
            "format": "int32"
          },
          "allowTenantScan": {
            "type": "boolean",
            "description": "Whether tenants may start a malware scan of their own sites. Default: true."
          },
          "tenantScanMinIntervalMinutes": {
            "type": "integer",
            "description": "Least time, in minutes, between two scans of the same site; a scan started sooner is refused. 0 means no wait. Default: 360 (6 hours).",
            "format": "int32"
          },
          "maxScanFileSizeBytes": {
            "type": "integer",
            "description": "Largest file, in bytes, that a tenant's scan will check; bigger files are skipped. Default: 33554432 (32 MiB).",
            "format": "int64"
          },
          "quarantineAction": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TenantQuarantineAction"
              }
            ],
            "description": "What a tenant may do with a file that a scan quarantined. Default: RequestApproval."
          },
          "allowCmsProtection": {
            "type": "boolean",
            "description": "Whether tenants may choose a CMS protection profile for their sites and change their login protection and rate-limit settings. Default: true."
          },
          "rateLimitEventsFloor": {
            "type": "integer",
            "description": "Lowest requests-per-minute value a tenant may set for a CMS protection rate limit, so a tenant cannot lock out its own visitors. Default: 1.",
            "format": "int32"
          },
          "allowRuntimeEdit": {
            "type": "boolean",
            "description": "Whether tenants may say which runtime their site uses (PHP, .NET, classic ASP or static), which decides the file types uploads may carry. Default: true."
          },
          "allowManagedCert": {
            "type": "boolean",
            "description": "Whether tenants may turn on a PortWall-managed HTTPS certificate for their own sites that are not run through a hosting panel. Default: false."
          },
          "maxManagedCerts": {
            "type": "integer",
            "description": "Most managed certificates one tenant may have. 0 means no limit. Default: 0.",
            "format": "int32"
          }
        },
        "description": "The host's rules for what a tenant may change on its own sites, as set for all tenants unless a tenant has its own override."
      },
      "UserSiteGrant": {
        "required": [
          "role",
          "siteName",
          "userId"
        ],
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "description": "The panel user who has access."
          },
          "siteName": {
            "type": "string",
            "description": "The site the user has access to."
          },
          "role": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SiteRole"
              }
            ],
            "description": "What the user may do with the site: look only, or also change its settings within the tenant policy."
          }
        },
        "description": "Permission given to one panel user to see or manage one site."
      },
      "VirtualPatch": {
        "required": [
          "createdAt",
          "createdBy",
          "description",
          "operator",
          "pattern",
          "phase",
          "ruleId",
          "siteName",
          "stage",
          "targets"
        ],
        "type": "object",
        "properties": {
          "ruleId": {
            "type": "integer",
            "description": "The rule number PortWall gave the patch. Use it to switch the patch between detecting and blocking, or to delete it.",
            "format": "int32"
          },
          "siteName": {
            "type": "string",
            "description": "The site the patch belongs to."
          },
          "description": {
            "type": "string",
            "description": "What the patch protects against, for example the CVE or flaw it shields."
          },
          "targets": {
            "type": "string",
            "description": "Where the rule looks, as WAF variable names separated by a vertical bar, for example ARGS or REQUEST_HEADERS:User-Agent|REQUEST_URI."
          },
          "operator": {
            "type": "string",
            "description": "How the pattern is compared with the target. It starts with @, for example @rx (regular expression), @contains or @streq (equals)."
          },
          "pattern": {
            "type": "string",
            "description": "The text or regular expression the operator looks for in the targets."
          },
          "phase": {
            "type": "integer",
            "description": "When in the request the rule runs, from 1 to 5. Use 2 (after the request body is read) for most patches, or 1 to check the request headers first.",
            "format": "int32"
          },
          "stage": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PatchStage"
              }
            ],
            "description": "DetectionOnly records matches and blocks nothing, even when the site is On; Blocking refuses matching requests with a 403."
          },
          "expiresAt": {
            "type": "string",
            "description": "When the patch stops applying. Null for a patch that never expires.",
            "format": "date-time",
            "nullable": true
          },
          "createdBy": {
            "type": "string",
            "description": "Who added the patch: the signed-in user or API key name."
          },
          "createdAt": {
            "type": "string",
            "description": "When the patch was added.",
            "format": "date-time"
          }
        },
        "description": "A targeted rule you wrote for one site that refuses requests exploiting a specific flaw, until the flaw is fixed at its source."
      },
      "VirtualPatchRequest": {
        "required": [
          "description",
          "operator",
          "pattern",
          "phase",
          "stage",
          "targets"
        ],
        "type": "object",
        "properties": {
          "description": {
            "type": "string",
            "description": "What the patch protects against. Required; must not contain quotes, backticks or line breaks."
          },
          "targets": {
            "type": "string",
            "description": "Where the rule looks, as WAF variable names separated by a vertical bar, for example ARGS or REQUEST_HEADERS:User-Agent|REQUEST_URI. Required; no double quotes, backticks or line breaks."
          },
          "operator": {
            "type": "string",
            "description": "How the pattern is compared with the target. Must start with @, for example @rx, @contains or @streq. Required."
          },
          "pattern": {
            "type": "string",
            "description": "The text or regular expression the operator looks for. Required; no double quotes, backticks or line breaks."
          },
          "phase": {
            "type": "integer",
            "description": "When in the request the rule runs, from 1 to 5. Use 2 (after the request body is read) for most patches.",
            "format": "int32"
          },
          "stage": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PatchStage"
              }
            ],
            "description": "Start with DetectionOnly to see what the patch matches in the attack feed, then switch it to Blocking."
          },
          "expiresInMinutes": {
            "type": "integer",
            "description": "Minutes from now after which the patch stops applying. Leave out, or send 0 or less, for a patch that never expires.",
            "format": "int32",
            "nullable": true
          }
        },
        "description": "The details of a new virtual patch to add to a site; PortWall assigns the rule number."
      }
    },
    "securitySchemes": {
      "ApiKey": {
        "type": "apiKey",
        "description": "An API key from the console's API Keys page.",
        "name": "X-Api-Key",
        "in": "header"
      }
    }
  }
}
