{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://securityscanner.org/spec/v1.json",
  "title": "securityscanner.org scan result",
  "description": "The machine-readable result of one securityscanner.org light scan (schema_version 0.1). One JSON object per scan. Every top-level field is always present; a value is null when unknown. report.txt is the human version of the same data. The scan looks at the outside of a website only: it is not a penetration test, it is not part of SOC 2, and no audit has been done. Human-readable explanation: https://securityscanner.org/spec/",
  "type": "object",
  "additionalProperties": true,
  "required": ["schema_version", "scanner", "domain", "scanned_at", "duration_seconds", "target", "overall", "fix_first", "checks", "requests", "errors", "disclaimer"],
  "properties": {
    "schema_version": {
      "type": "string",
      "const": "0.1",
      "description": "Bumped when a field changes meaning or is removed. New fields may be added without a bump."
    },
    "scanner": {
      "type": "object",
      "required": ["name", "version", "user_agent", "mode"],
      "properties": {
        "name": { "type": "string", "const": "securityscanner.org" },
        "version": { "type": "string", "description": "Scanner code version." },
        "user_agent": { "type": "string", "description": "The exact User-Agent sent on every HTTP request to the target." },
        "mode": { "type": "string", "enum": ["light", "full"], "description": "light = passive checks only. full is reserved for when crawl findings are added." }
      }
    },
    "domain": {
      "type": "string",
      "description": "The scanned apex domain, lower case, no scheme, no www.",
      "pattern": "^[a-z0-9.-]+$"
    },
    "scanned_at": {
      "type": "string",
      "format": "date-time",
      "description": "Start time of the scan, ISO 8601 with a UTC offset."
    },
    "duration_seconds": { "type": "number", "minimum": 0 },
    "target": {
      "type": "object",
      "required": ["apex", "www", "apex_ips", "www_ips", "home_page_url", "home_page_status", "home_page_https", "home_page_error", "behind_cdn", "notes"],
      "properties": {
        "apex": { "type": "string" },
        "www": { "type": "string" },
        "apex_ips": { "type": "array", "items": { "type": "string" }, "description": "IPv4 A records of the apex. Empty when none." },
        "www_ips": { "type": "array", "items": { "type": "string" } },
        "home_page_url": { "type": ["string", "null"], "description": "Final URL after redirects of the home page fetch." },
        "home_page_status": { "type": ["integer", "null"] },
        "home_page_https": { "type": "boolean" },
        "home_page_error": { "type": ["string", "null"] },
        "behind_cdn": {
          "type": ["string", "null"],
          "enum": ["cloudflare", "cloudfront", "fastly", "vercel", "netlify", "akamai", "sucuri", "github pages", null],
          "description": "When set, open_ports describes the CDN edge, not the origin."
        },
        "notes": { "type": "array", "items": { "type": "string" } }
      }
    },
    "overall": {
      "type": "object",
      "required": ["grade", "score", "weighted_score", "cap", "cap_reason", "weights_used"],
      "properties": {
        "grade": { "$ref": "#/$defs/grade" },
        "score": { "$ref": "#/$defs/score", "description": "The overall score after any cap (the letter's ceiling when capped)." },
        "weighted_score": { "type": "number", "minimum": 0, "maximum": 100, "description": "Raw weighted average before caps." },
        "cap": { "oneOf": [{ "$ref": "#/$defs/grade" }, { "type": "null" }], "description": "The letter the grade was capped to, or null when no cap applied." },
        "cap_reason": { "type": ["string", "null"] },
        "weights_used": {
          "type": "object",
          "additionalProperties": { "type": "integer", "minimum": 0, "maximum": 100 },
          "description": "{check: weight} for the checks that counted. Not applicable and errored checks are absent."
        }
      },
      "description": "Rule: weighted average of applicable checks; capped at one letter above the worst grade among checks weighted 15 or more (tls, exposed_files, email_auth, security_headers); capped at D when any check has a critical finding."
    },
    "fix_first": {
      "type": "array",
      "maxItems": 5,
      "items": {
        "type": "object",
        "required": ["order", "check", "severity", "title", "fix"],
        "properties": {
          "order": { "type": "integer", "minimum": 1 },
          "check": { "$ref": "#/$defs/checkId" },
          "severity": { "type": "string", "enum": ["critical", "high", "medium", "low"] },
          "title": { "type": "string" },
          "fix": { "type": "string" }
        }
      },
      "description": "Up to five findings to act on first, ordered by severity then by the weight of the check they came from."
    },
    "checks": {
      "type": "array",
      "minItems": 8,
      "maxItems": 8,
      "items": { "$ref": "#/$defs/check" },
      "description": "Always the eight checks in this order: security_headers, tls, email_auth, dns_hygiene, exposed_files, wordpress, page_hygiene, open_ports."
    },
    "requests": {
      "type": "object",
      "required": ["http_requests_to_target", "http_requests_third_party", "budget", "dns_queries", "tcp_connects", "log"],
      "properties": {
        "http_requests_to_target": { "type": "integer", "minimum": 0, "description": "HTTP requests sent to the scanned domain, redirect hops included." },
        "http_requests_third_party": { "type": "integer", "minimum": 0, "description": "Requests to api.wordpress.org for version lookups. These never touch the target." },
        "budget": { "type": "integer", "minimum": 0, "description": "The hard cap on target requests for this scan." },
        "dns_queries": { "type": "integer", "minimum": 0 },
        "tcp_connects": { "type": "integer", "minimum": 0, "description": "Port probes attempted (17, or 0 when there was no A record)." },
        "log": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["url", "status", "hops", "third_party", "seconds", "error"],
            "properties": {
              "url": { "type": "string" },
              "status": { "type": ["integer", "null"] },
              "hops": { "type": "integer", "minimum": 1 },
              "third_party": { "type": "boolean" },
              "seconds": { "type": "number", "minimum": 0 },
              "error": { "type": ["string", "null"] }
            }
          },
          "description": "Every HTTP request the scanner made, in order."
        }
      }
    },
    "errors": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["check", "error"],
        "properties": {
          "check": { "$ref": "#/$defs/checkId" },
          "error": { "type": "string" },
          "trace": { "type": "string" }
        }
      },
      "description": "Checks that could not run or were skipped. Empty when all ran."
    },
    "disclaimer": {
      "type": "string",
      "pattern": "No audit has been done\\.$",
      "description": "The outside-of-the-building statement. Always ends with 'No audit has been done.'"
    }
  },
  "$defs": {
    "grade": { "type": "string", "enum": ["A", "B", "C", "D", "F"], "description": "A 90 to 100, B 80 to 89, C 70 to 79, D 60 to 69, F under 60." },
    "score": { "type": "integer", "minimum": 0, "maximum": 100 },
    "severity": { "type": "string", "enum": ["critical", "high", "medium", "low", "info", "good"], "description": "info and good never cost points." },
    "checkId": { "type": "string", "enum": ["security_headers", "tls", "email_auth", "dns_hygiene", "exposed_files", "wordpress", "page_hygiene", "open_ports"] },
    "finding": {
      "type": "object",
      "required": ["severity", "title", "detail", "fix"],
      "properties": {
        "severity": { "$ref": "#/$defs/severity" },
        "title": { "type": "string", "description": "Short, stable wording. Scripts can match on it." },
        "detail": { "type": "string", "description": "What was seen and why it matters, plain English." },
        "fix": { "type": "string", "description": "What to do. Empty for good and for some info findings." }
      }
    },
    "check": {
      "type": "object",
      "required": ["check", "name", "weight", "applicable", "score", "grade", "summary", "findings", "evidence", "error"],
      "properties": {
        "check": { "$ref": "#/$defs/checkId" },
        "name": { "type": "string", "description": "Human name, e.g. TLS (encryption)." },
        "weight": { "type": "integer", "enum": [5, 10, 15, 20], "description": "security_headers 15, tls 20, email_auth 15, dns_hygiene 5, exposed_files 20, wordpress 10, page_hygiene 5, open_ports 10." },
        "applicable": { "type": "boolean", "description": "false when the check does not apply (not a WordPress site, no A record) or could not run. Not applicable checks are excluded from the overall grade." },
        "score": { "$ref": "#/$defs/score", "description": "100 when not applicable." },
        "grade": { "$ref": "#/$defs/grade", "description": "Ignore when applicable is false." },
        "summary": { "type": "string", "description": "One line, used in the grades table." },
        "findings": { "type": "array", "items": { "$ref": "#/$defs/finding" }, "description": "Includes good and info entries so the positive results are visible too." },
        "evidence": {
          "type": "object",
          "description": "Check-specific raw data. Shapes are stable within a schema version and documented per check below.",
          "additionalProperties": true
        },
        "error": { "type": ["string", "null"], "description": "Why the check could not run, when it could not." }
      },
      "allOf": [
        {
          "if": { "properties": { "check": { "const": "security_headers" } }, "required": ["check"] },
          "then": { "properties": { "evidence": { "$ref": "#/$defs/evidenceSecurityHeaders" } } }
        },
        {
          "if": { "properties": { "check": { "const": "tls" } }, "required": ["check"] },
          "then": { "properties": { "evidence": { "$ref": "#/$defs/evidenceTls" } } }
        },
        {
          "if": { "properties": { "check": { "const": "email_auth" } }, "required": ["check"] },
          "then": { "properties": { "evidence": { "$ref": "#/$defs/evidenceEmailAuth" } } }
        },
        {
          "if": { "properties": { "check": { "const": "dns_hygiene" } }, "required": ["check"] },
          "then": { "properties": { "evidence": { "$ref": "#/$defs/evidenceDnsHygiene" } } }
        },
        {
          "if": { "properties": { "check": { "const": "exposed_files" } }, "required": ["check"] },
          "then": { "properties": { "evidence": { "$ref": "#/$defs/evidenceExposedFiles" } } }
        },
        {
          "if": { "properties": { "check": { "const": "wordpress" } }, "required": ["check"] },
          "then": { "properties": { "evidence": { "$ref": "#/$defs/evidenceWordpress" } } }
        },
        {
          "if": { "properties": { "check": { "const": "page_hygiene" } }, "required": ["check"] },
          "then": { "properties": { "evidence": { "$ref": "#/$defs/evidencePageHygiene" } } }
        },
        {
          "if": { "properties": { "check": { "const": "open_ports" } }, "required": ["check"] },
          "then": { "properties": { "evidence": { "$ref": "#/$defs/evidenceOpenPorts" } } }
        }
      ]
    },
    "evidenceSecurityHeaders": {
      "type": "object",
      "description": "Raw header values from the final home page response, or null when absent. Present only when the home page could be fetched.",
      "properties": {
        "strict-transport-security": { "type": ["string", "null"] },
        "content-security-policy": { "type": ["string", "null"], "description": "First 500 characters." },
        "content-security-policy-report-only": { "type": "boolean" },
        "x-content-type-options": { "type": ["string", "null"] },
        "x-frame-options": { "type": ["string", "null"] },
        "referrer-policy": { "type": ["string", "null"] },
        "permissions-policy": { "type": ["string", "null"], "description": "First 300 characters." },
        "server": { "type": ["string", "null"] },
        "x-powered-by": { "type": ["string", "null"] },
        "x-aspnet-version": { "type": ["string", "null"] },
        "page_https": { "type": "boolean" },
        "final_url": { "type": "string" },
        "status": { "type": "integer" },
        "redirects": { "type": "array", "items": { "type": "array", "prefixItems": [{ "type": "integer" }, { "type": "string" }], "minItems": 2, "maxItems": 2 }, "description": "[status, url] for each redirect hop." }
      }
    },
    "evidenceTls": {
      "type": "object",
      "properties": {
        "apex": {
          "type": "object",
          "properties": {
            "verified": { "type": "boolean" },
            "error": { "type": ["string", "null"] },
            "negotiated": { "type": ["string", "null"], "description": "e.g. TLSv1.3" },
            "issuer": { "type": ["string", "null"] },
            "names": { "type": "array", "items": { "type": "string" }, "maxItems": 20 },
            "not_after": { "type": ["string", "null"], "description": "ISO 8601" },
            "days_to_expiry": { "type": ["integer", "null"], "description": "Negative when expired." }
          }
        },
        "www": {
          "type": ["object", "null"],
          "properties": {
            "verified": { "type": "boolean" },
            "error": { "type": ["string", "null"] },
            "negotiated": { "type": ["string", "null"] }
          },
          "description": "null when www does not resolve."
        },
        "www_resolves": { "type": "boolean" },
        "tls_1_0_accepted": { "type": ["boolean", "null"], "description": "null when the scanner's own TLS library could not offer that version." },
        "tls_1_1_accepted": { "type": ["boolean", "null"] },
        "http_redirect_apex": { "type": ["string", "null"], "description": "Location header of the http:// response when it was a redirect." },
        "http_redirect_www": { "type": ["string", "null"] },
        "http_apex_status": { "type": ["integer", "null"] },
        "http_apex_error": { "type": ["string", "null"] }
      }
    },
    "evidenceEmailAuth": {
      "type": "object",
      "properties": {
        "spf": { "type": "array", "items": { "type": "string" }, "description": "The v=spf1 TXT records found, normally 0 or 1." },
        "spf_lookups": { "type": ["integer", "null"], "description": "DNS-lookup terms after following includes." },
        "dmarc": { "type": "array", "items": { "type": "string" } },
        "dkim_selectors_found": { "type": "array", "items": { "type": "string" } },
        "dkim_selectors_tried": { "type": "array", "items": { "type": "string" } },
        "mx": { "type": ["array", "null"], "items": { "type": "string" }, "description": "Empty when none, null when the lookup failed." },
        "dns_errors": { "type": "array", "items": { "type": "string" } }
      }
    },
    "evidenceDnsHygiene": {
      "type": "object",
      "properties": {
        "dnssec_validated": { "type": ["boolean", "null"], "description": "AD flag from a validating resolver." },
        "ds_records": { "type": "array", "items": { "type": "string" } },
        "caa": { "type": "array", "items": { "type": "string" }, "description": "CAA rdata text, possibly inherited from a parent name." },
        "caa_found_at": { "type": ["string", "null"] },
        "www_cname": { "type": ["string", "null"] },
        "www_cname_target_resolves": { "type": ["boolean", "null"] },
        "apex_a": { "type": "array", "items": { "type": "string" } }
      }
    },
    "evidenceExposedFiles": {
      "type": "object",
      "description": "One key per probed path (e.g. '/.git/HEAD'): {checked, status, exposed, error}. For '/.well-known/security.txt' and '/robots.txt': {checked, status, present}; robots.txt also carries interesting_disallows. Plus origin and, when the request budget ran out, skipped_for_budget.",
      "properties": {
        "origin": { "type": "string", "description": "The scheme and host the paths were fetched from." },
        "skipped_for_budget": { "type": "array", "items": { "type": "string" } }
      },
      "patternProperties": {
        "^/": {
          "type": "object",
          "properties": {
            "checked": { "type": "boolean" },
            "status": { "type": ["integer", "null"] },
            "exposed": { "type": "boolean" },
            "present": { "type": "boolean" },
            "error": { "type": ["string", "null"] },
            "interesting_disallows": { "type": "array", "items": { "type": "string" } }
          }
        }
      }
    },
    "evidenceWordpress": {
      "type": "object",
      "properties": {
        "is_wordpress": { "type": "boolean" },
        "core_version": { "type": ["string", "null"] },
        "core_version_source": { "type": ["string", "null"], "enum": ["generator meta", "?ver= on wp-includes assets", "/readme.html", null] },
        "latest_core": { "type": ["string", "null"] },
        "plugins": { "type": "object", "additionalProperties": { "type": ["string", "null"] }, "description": "{slug: version-or-null} as read from asset URLs." },
        "plugin_latest": { "type": "object", "additionalProperties": { "type": ["string", "null"] }, "description": "{slug: latest-or-null} for plugins with a visible version, max 10." },
        "themes": { "type": "array", "items": { "type": "string" } },
        "xmlrpc_enabled": { "type": ["boolean", "null"] }
      }
    },
    "evidencePageHygiene": {
      "type": "object",
      "properties": {
        "mixed_active": { "type": "array", "items": { "type": "string" }, "maxItems": 10, "description": "http:// script, stylesheet and frame URLs on the https page." },
        "mixed_passive": { "type": "array", "items": { "type": "string" }, "maxItems": 10, "description": "http:// image and media URLs." },
        "jquery_versions": { "type": "array", "items": { "type": "string" } },
        "cookies": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "name": { "type": "string" },
              "secure": { "type": "boolean" },
              "httponly": { "type": "boolean" },
              "samesite": { "type": ["string", "null"] }
            }
          }
        }
      }
    },
    "evidenceOpenPorts": {
      "type": "object",
      "properties": {
        "ip": { "type": ["string", "null"], "description": "The address probed (first A record)." },
        "ports": { "type": "object", "additionalProperties": { "type": "string", "enum": ["open", "closed", "filtered"] }, "description": "Keyed by port number as a string, for all 17 ports." },
        "open": { "type": "array", "items": { "type": "integer" } },
        "behind_cdn": { "type": ["string", "null"] }
      }
    }
  }
}
