{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://hexblot.github.io/composer-remediate/schema/report.schema.json",
  "title": "Composer Remediate report",
  "description": "Machine-readable output of `composer remediate --format=json` (schema_version 1).",
  "type": "object",
  "required": ["schema_version", "analysis_metadata", "exit_code", "warnings", "summary", "findings", "unsolved_findings"],
  "additionalProperties": false,
  "properties": {
    "schema_version": {"type": "integer", "const": 1},
    "analysis_metadata": {
      "type": "object",
      "description": "Reproducibility metadata: project path, engine/Composer/PHP versions, advisory source, solver, total solver runs, sha256 of composer.json and composer.lock, timestamp, locked package count.",
      "additionalProperties": {"type": "string"}
    },
    "exit_code": {"type": "integer", "minimum": 0, "maximum": 5},
    "warnings": {"type": "array", "items": {"type": "string"}},
    "summary": {
      "type": "object",
      "required": ["advisories", "packages", "packages_with_fix", "fail_on", "packages_gated", "packages_baselined", "packages_with_constraint_drag", "packages_known_exploited", "packages_with_abandoned_dependency", "coverage_gaps", "coverage_gaps_accepted", "combined_command", "combined_fixes", "combined_total", "combined_fixes_all", "combined_changes", "combined_search"],
      "additionalProperties": false,
      "properties": {
        "advisories": {"type": "integer", "minimum": 0},
        "packages": {"type": "integer", "minimum": 0},
        "packages_with_fix": {"type": "integer", "minimum": 0},
        "fail_on": {"type": ["string", "null"], "enum": ["low", "medium", "moderate", "high", "critical", null], "description": "Severity threshold given with --fail-on, if any"},
        "packages_gated": {"type": "integer", "minimum": 0, "description": "Packages whose findings count towards the exit code under the threshold and baseline"},
        "packages_baselined": {"type": "integer", "minimum": 0, "description": "Packages whose findings are all in the --baseline file"},
        "packages_with_constraint_drag": {"type": "integer", "minimum": 0, "description": "Packages whose only fix requires widening a composer.json constraint"},
        "packages_known_exploited": {"type": "integer", "minimum": 0, "description": "Packages with at least one advisory in CISA's Known Exploited Vulnerabilities catalogue"},
        "packages_with_abandoned_dependency": {"type": "integer", "minimum": 0, "description": "Packages whose dependency paths (themselves included) contain a package Packagist marks abandoned"},
        "coverage_gaps": {"type": "integer", "minimum": 0, "description": "Advisory records about locked (or replaced/provided) packages the advisory source could not read; unless accepted, a lock with gaps and no findings exits 4"},
        "coverage_gaps_accepted": {"type": "boolean", "description": "Whether --accept-coverage-gaps let a lock with gaps exit clean"},
        "combined_command": {"type": ["string", "null"]},
        "combined_fixes": {"type": ["integer", "null"]},
        "combined_total": {"type": ["integer", "null"]},
        "combined_fixes_all": {"type": ["boolean", "null"]},
        "combined_changes": {"oneOf": [{"$ref": "#/definitions/changeSummary"}, {"type": "null"}]},
        "combined_search": {"type": "array", "items": {"$ref": "#/definitions/combinedAttempt"}, "description": "The global search for one command fixing everything, step by step: the merged winners, swaps of lower-ranked candidates for findings in the way, and drops of contributions a sibling's fix already covers. Empty when only one package has a fix."}
      }
    },
    "findings": {"type": "array", "items": {"$ref": "#/definitions/finding"}},
    "unsolved_findings": {"type": "array", "items": {"type": "string"}, "description": "Package names without a verified remediation."}
  },
  "definitions": {
    "changeSummary": {
      "type": "object",
      "required": ["changed", "added", "removed", "total", "major_change", "downgrade", "prerelease"],
      "additionalProperties": false,
      "properties": {
        "changed": {"type": "integer", "minimum": 0},
        "added": {"type": "integer", "minimum": 0},
        "removed": {"type": "integer", "minimum": 0},
        "total": {"type": "integer", "minimum": 0},
        "major_change": {"type": "boolean"},
        "downgrade": {"type": "boolean"},
        "prerelease": {"type": "boolean"}
      }
    },
    "change": {
      "type": "object",
      "required": ["package", "kind", "step", "from", "to"],
      "additionalProperties": false,
      "properties": {
        "package": {"type": "string"},
        "kind": {"type": "string", "enum": ["added", "removed", "upgraded", "downgraded", "changed"]},
        "step": {"type": ["string", "null"], "enum": ["major", "minor", "patch", "other", null]},
        "from": {"type": ["string", "null"]},
        "to": {"type": ["string", "null"]}
      }
    },
    "strategy": {"type": "string", "enum": ["lock-refresh", "with-dependencies", "parent-update", "root-constraint-widen", "direct-require"]},
    "advisory": {
      "type": "object",
      "required": ["id", "cve", "title", "link", "severity", "reported_at", "affected_versions", "sources", "epss", "epss_percentile", "kev_added"],
      "additionalProperties": false,
      "properties": {
        "id": {"type": "string"},
        "cve": {"type": ["string", "null"]},
        "title": {"type": ["string", "null"]},
        "link": {"type": ["string", "null"]},
        "severity": {"type": ["string", "null"]},
        "reported_at": {"type": ["string", "null"], "description": "RFC 3339"},
        "affected_versions": {"type": "string", "description": "Composer constraint expression"},
        "sources": {"type": "array", "items": {"type": "object", "required": ["name", "remoteId"], "properties": {"name": {"type": "string"}, "remoteId": {"type": "string"}}}},
        "epss": {"type": ["number", "null"], "minimum": 0, "maximum": 1, "description": "FIRST EPSS probability of exploitation in the next 30 days; null when the advisory source carries no exploit data"},
        "epss_percentile": {"type": ["number", "null"], "minimum": 0, "maximum": 1},
        "kev_added": {"type": ["string", "null"], "description": "Date (YYYY-MM-DD) CISA added the CVE to the Known Exploited Vulnerabilities catalogue; null when not listed or unknown"}
      }
    },
    "combinedAttempt": {
      "type": "object",
      "required": ["command", "note", "outcome", "fixed", "total", "reason", "chosen"],
      "additionalProperties": false,
      "properties": {
        "command": {"type": "string"},
        "note": {"type": "string", "description": "What this attempt changed compared with the previous one"},
        "outcome": {"type": "string", "enum": ["accepted", "partial", "unresolved", "rejected"]},
        "fixed": {"type": "integer", "minimum": 0},
        "total": {"type": "integer", "minimum": 0},
        "reason": {"type": ["string", "null"], "description": "The solver's explanation or the acceptance rule that failed; null when the attempt fixed everything"},
        "chosen": {"type": "boolean", "description": "Whether this is the combination the report recommends"}
      }
    },
    "abandonedPackage": {
      "type": "object",
      "required": ["package", "replacement"],
      "additionalProperties": false,
      "properties": {
        "package": {"type": "string"},
        "replacement": {"type": ["string", "null"], "description": "The replacement Packagist names, if any"}
      }
    },
    "pathSegment": {
      "type": "object",
      "required": ["package", "version", "requires"],
      "additionalProperties": false,
      "properties": {
        "package": {"type": "string", "description": "\"root\" for the root package"},
        "version": {"type": ["string", "null"]},
        "requires": {"type": "string", "description": "The constraint this package places on the next element of the chain"}
      }
    },
    "path": {
      "type": "object",
      "required": ["cyclic", "chain", "target"],
      "additionalProperties": false,
      "properties": {
        "cyclic": {"type": "boolean"},
        "chain": {"type": "array", "items": {"$ref": "#/definitions/pathSegment"}, "description": "From the root package down to the package requiring the vulnerable one"},
        "target": {"type": "object", "required": ["package", "version"], "properties": {"package": {"type": "string"}, "version": {"type": "string"}}}
      }
    },
    "rootConstraintChange": {
      "type": "object",
      "required": ["package", "from", "to", "dev"],
      "additionalProperties": false,
      "properties": {
        "package": {"type": "string"},
        "from": {"type": ["string", "null"]},
        "to": {"type": "string"},
        "dev": {"type": "boolean"}
      }
    },
    "remediation": {
      "oneOf": [
        {
          "type": "object",
          "required": ["status", "outcome", "blocker"],
          "additionalProperties": false,
          "properties": {
            "status": {"const": "none"},
            "outcome": {"type": "string", "description": "How to read the absence of a fix: \"none\" (no fix exists within the metadata), \"none found within the search budget\", or \"unknown: …\" when a solver or network failure prevented the search from completing"},
            "blocker": {"type": ["string", "null"]}
          }
        },
        {
          "type": "object",
          "required": ["status", "command", "strategy", "root_constraint_changes", "constraint_drag", "blocking_risk", "summary", "changes"],
          "additionalProperties": false,
          "properties": {
            "status": {"const": "verified"},
            "command": {"type": "string"},
            "strategy": {"$ref": "#/definitions/strategy"},
            "root_constraint_changes": {"type": "array", "items": {"$ref": "#/definitions/rootConstraintChange"}},
            "constraint_drag": {"type": ["string", "null"], "description": "Explanation when the fix requires widening a composer.json constraint"},
            "blocking_risk": {"type": "array", "items": {"type": "string"}, "description": "Packages this command changes whose new version still carries another advisory; Composer 2.10+ advisory blocking may refuse the update"},
            "summary": {"oneOf": [{"$ref": "#/definitions/changeSummary"}, {"type": "null"}]},
            "changes": {"type": "array", "items": {"$ref": "#/definitions/change"}}
          }
        }
      ]
    },
    "candidate": {
      "type": "object",
      "required": ["command", "strategy", "outcome"],
      "additionalProperties": false,
      "properties": {
        "command": {"type": "string"},
        "strategy": {"$ref": "#/definitions/strategy"},
        "outcome": {"type": "string", "enum": ["valid", "rejected", "skipped"]},
        "rank": {"type": "integer", "minimum": 1},
        "reason": {"type": "string"},
        "summary": {"$ref": "#/definitions/changeSummary"},
        "solver_status": {"type": "string", "enum": ["resolved", "conflict", "transport-error", "error"]}
      }
    },
    "finding": {
      "type": "object",
      "required": ["package", "version", "version_normalized", "direct", "dev", "baselined", "counts_for_exit", "via_replaced_package", "advisories", "abandoned", "paths_total", "paths", "solver_runs", "search_exhausted", "remediation", "candidates"],
      "additionalProperties": false,
      "properties": {
        "package": {"type": "string"},
        "version": {"type": "string"},
        "version_normalized": {"type": "string"},
        "direct": {"type": "boolean"},
        "dev": {"type": "boolean"},
        "baselined": {"type": "boolean", "description": "All advisories of this package are in the --baseline file"},
        "counts_for_exit": {"type": "boolean", "description": "Whether this package affects the exit code under --fail-on and the baseline"},
        "via_replaced_package": {"type": ["string", "null"]},
        "advisories": {"type": "array", "minItems": 1, "items": {"$ref": "#/definitions/advisory"}},
        "abandoned": {"type": "array", "items": {"$ref": "#/definitions/abandonedPackage"}, "description": "Packages on the dependency paths, the vulnerable one included, that Packagist marks abandoned"},
        "paths_total": {"type": "integer", "minimum": 0},
        "paths": {"type": "array", "maxItems": 10, "items": {"$ref": "#/definitions/path"}},
        "solver_runs": {"type": "integer", "minimum": 0, "description": "Solver invocations spent on this package, probes and simplification included"},
        "search_exhausted": {"type": "boolean", "description": "True when the candidate cap or the solve budget cut the search short"},
        "remediation": {"$ref": "#/definitions/remediation"},
        "candidates": {"type": "array", "items": {"$ref": "#/definitions/candidate"}}
      }
    }
  }
}
