{
  "openapi": "3.1.0",
  "info": {
    "title": "Penthera live check",
    "version": "1.9.0",
    "summary": "Check a public website's passive security basics: HTTPS, TLS, headers, cookies, scripts, security.txt and DNS.",
    "description": "One endpoint, POST /api/check, with a JSON body. It loads only what a browser would (the page, up to 14 of its scripts and stylesheets, /.well-known/security.txt) plus public DNS records, at most 20 requests, and grades what came back. No other paths, no payloads. Nothing is stored or logged. A site whose robots.txt disallows the user agent Penthera-QuickCheck is not checked.\n\nOnly public http(s) sites on ports 80 and 443. For localhost, internal hosts or a full scan, run `npx penthera@1.9.0 <url>` on your own machine.\n\nEvery request must carry \"consent\": true, which states that you own the site or are authorized to test it and that you accept the terms: https://penthera.org/terms.\n\nRate limits: per client address and per target site, per minute.",
    "termsOfService": "https://penthera.org/terms",
    "license": {
      "name": "MIT",
      "identifier": "MIT"
    },
    "contact": {
      "name": "Penthera on GitHub",
      "url": "https://github.com/danoszz/penthera/issues"
    }
  },
  "servers": [
    {
      "url": "https://penthera.org"
    }
  ],
  "externalDocs": {
    "description": "What Penthera is and every doc, for agents",
    "url": "https://penthera.org/llms.txt"
  },
  "paths": {
    "/api/check": {
      "post": {
        "operationId": "checkWebsiteSecurity",
        "summary": "Check a public website's security basics",
        "description": "Grades a public website's HTTPS, TLS, security headers, cookies, scripts (Subresource Integrity, mixed content, exposed secrets, outdated libraries), security.txt and DNS records (SPF, DMARC, CAA), from what a browser loads. Use it to answer \"is this site's basic security set up?\" for a URL the user owns or is authorized to test.\nSend {\"url\": \"example.com\", \"consent\": true}. consent: true means the user owns the site or is authorized to test it, and accepts the terms; without it the answer is 400 consent_required.\nErrors are JSON: {\"error\": {\"code\", \"message\", \"hint\"}}. Read hint for what to do next.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckRequest"
              },
              "examples": {
                "passive": {
                  "summary": "Check a site you own",
                  "value": {
                    "url": "example.com",
                    "consent": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The passive report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Report"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_url`: The url is empty, too long, not http(s), has credentials or a non-standard port. `not_public`: The host is localhost, a private or reserved address, or a name that is not public. `invalid_json`: The body is not JSON. `consent_required`: consent is missing or not true. Send \"consent\": true only if you own the site or are authorized to test it, and accept the terms.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "invalid_url",
                    "message": "…",
                    "hint": "…"
                  }
                }
              }
            }
          },
          "403": {
            "description": "`opted_out`: The site's robots.txt opts out of Penthera-QuickCheck, so it is not checked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "opted_out",
                    "message": "…",
                    "hint": "…"
                  }
                }
              }
            }
          },
          "405": {
            "description": "`method_not_allowed`: Only POST is allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "method_not_allowed",
                    "message": "…",
                    "hint": "…"
                  }
                }
              }
            }
          },
          "413": {
            "description": "`too_large`: The request body is over 4 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "too_large",
                    "message": "…",
                    "hint": "…"
                  }
                }
              }
            }
          },
          "422": {
            "description": "`not_found`: The domain does not resolve. `too_many_redirects`: The site redirected more than four times. `tls_invalid`: The site's certificate is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "…",
                    "hint": "…"
                  }
                }
              }
            }
          },
          "429": {
            "description": "`budget`: The check used up its request or time budget for this site. `rate_limited`: Too many checks in a minute, from one address or of one site.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "budget",
                    "message": "…",
                    "hint": "…"
                  }
                }
              }
            }
          },
          "500": {
            "description": "`internal`: The check failed on penthera.org's side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "internal",
                    "message": "…",
                    "hint": "…"
                  }
                }
              }
            }
          },
          "502": {
            "description": "`unreachable`: penthera.org could not connect to the site.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "unreachable",
                    "message": "…",
                    "hint": "…"
                  }
                }
              }
            }
          },
          "504": {
            "description": "`timeout`: The site did not answer in time.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "timeout",
                    "message": "…",
                    "hint": "…"
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "CheckRequest": {
        "type": "object",
        "required": [
          "url",
          "consent"
        ],
        "additionalProperties": false,
        "properties": {
          "url": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2048,
            "description": "The site: a domain (example.com means https://example.com/) or an http(s) URL on port 80 or 443.",
            "examples": [
              "example.com",
              "https://example.com/login"
            ]
          },
          "consent": {
            "const": true,
            "description": "true states that you own the site or are authorized to test it, and that you accept the terms at https://penthera.org/terms. Ask the user before you send it."
          }
        }
      },
      "Report": {
        "type": "object",
        "required": [
          "url",
          "tier",
          "score",
          "verdict",
          "redirects",
          "scope",
          "categories",
          "checks"
        ],
        "properties": {
          "url": {
            "type": "string",
            "description": "The final URL, after redirects."
          },
          "status": {
            "type": "integer",
            "description": "The HTTP status of the final response."
          },
          "tier": {
            "const": "passive"
          },
          "score": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "description": "The share of check weight that passed, as a percentage."
          },
          "verdict": {
            "type": "string",
            "description": "The score in words, such as \"Solid basics\" (90 or more) or \"Unsafe defaults\" (under 45)."
          },
          "redirects": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Each URL that redirected, in order."
          },
          "scope": {
            "type": "string",
            "description": "What was and was not checked."
          },
          "categories": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Category"
            }
          },
          "checks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Check"
            }
          }
        }
      },
      "Category": {
        "type": "object",
        "required": [
          "id",
          "label",
          "passed",
          "total"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "passed": {
            "type": "integer",
            "minimum": 0
          },
          "total": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "Check": {
        "type": "object",
        "required": [
          "id",
          "category",
          "passed",
          "severity",
          "title",
          "evidence",
          "fix",
          "verify",
          "weight"
        ],
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "https",
              "hsts",
              "csp"
            ]
          },
          "category": {
            "type": "string",
            "description": "The id of a category in categories."
          },
          "passed": {
            "type": "boolean"
          },
          "severity": {
            "type": "string",
            "enum": [
              "critical",
              "high",
              "medium",
              "low",
              "info"
            ],
            "description": "How much a failure matters."
          },
          "title": {
            "type": "string"
          },
          "evidence": {
            "type": "string",
            "description": "What the response showed; empty when there is nothing to show."
          },
          "fix": {
            "type": "string",
            "description": "What to change."
          },
          "verify": {
            "type": "string",
            "description": "A command that shows whether the fix worked, such as a curl."
          },
          "weight": {
            "type": "integer",
            "minimum": 0,
            "description": "This check's share of the score."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "hint"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "budget",
                  "consent_required",
                  "internal",
                  "invalid_json",
                  "invalid_url",
                  "method_not_allowed",
                  "not_found",
                  "not_public",
                  "opted_out",
                  "rate_limited",
                  "timeout",
                  "tls_invalid",
                  "too_large",
                  "too_many_redirects",
                  "unreachable"
                ]
              },
              "message": {
                "type": "string",
                "description": "What happened."
              },
              "hint": {
                "type": "string",
                "description": "What to do next."
              }
            }
          }
        }
      }
    }
  }
}
