{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://hexblot.github.io/composer-remediate/schema/report-v3.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": 3
    },
    "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_declared_exposed",
        "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",
        "packages_running_new_code"
      ],
      "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_running_new_code": {
          "type": "integer",
          "description": "Packages whose recommended fix lets code run that could not run before: a package becoming a composer-plugin, or gaining autoload.files entries or binaries. Gate on this when an unattended apply should stop for review."
        },
        "packages_declared_exposed": {
          "type": "integer",
          "minimum": 0,
          "description": "Packages declared exposed with --exposed (they handle untrusted input in this application) that have a finding; they are listed first. Added in schema_version 3"
        },
        "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, records it could not attribute to any package, and, when accepted, records about packages the recommended commands add; 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 standalone verified remediation (remediation.status is none for them). Such a package may still be fixed by the combined command when summary.combined_fixes_all is true or its advisories appear among the keys the combined command fixes; the exit code counts that."
    }
  },
  "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",
            "coverage_gaps",
            "summary",
            "changes",
            "no_fix_within_locked_major",
            "conflict_entries",
            "capability_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"
            },
            "no_fix_within_locked_major": {
              "type": [
                "string",
                "null"
              ],
              "description": "Set on a constraint-drag finding whose fix also leaves the locked major behind: no release within that major carries the fix, so the widening is the only route that can be verified. Names the alternative the planner cannot see, a fix published under a different package name. Null when the fix stays within the locked major."
            },
            "conflict_entries": {
              "type": "array",
              "description": "The persistent form of this recommendation's --with constraints: the conflict entries for composer.json that keep a later update from resolving back below the fix. Composer has no subcommand that writes one, so it is an edit rather than part of the verified command.",
              "items": {
                "type": "object",
                "required": [
                  "package",
                  "constraint"
                ],
                "additionalProperties": false,
                "properties": {
                  "package": {
                    "type": "string"
                  },
                  "constraint": {
                    "type": "string",
                    "description": "Everything below the fix, e.g. \"<3.14.0\""
                  }
                }
              }
            },
            "capability_changes": {
              "type": "array",
              "description": "What this update changes about what a package is allowed to do, as opposed to how much it changes. Read from metadata the lock file and the solve already carry, so nothing is downloaded or unpacked. Reported rather than ranked on: the count of changed packages measures review burden and this measures reach.",
              "items": {
                "type": "object",
                "required": [
                  "package",
                  "kind",
                  "from",
                  "to",
                  "runs_new_code",
                  "description"
                ],
                "additionalProperties": false,
                "properties": {
                  "package": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "type",
                      "autoload_files",
                      "binaries",
                      "source_host",
                      "dist_host"
                    ]
                  },
                  "from": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Null when the package had none of this before"
                  },
                  "to": {
                    "type": "string"
                  },
                  "runs_new_code": {
                    "type": "boolean",
                    "description": "True for the changes that let code run which could not run before"
                  },
                  "description": {
                    "type": "string",
                    "description": "The same change written for a person"
                  }
                }
              }
            },
            "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"
            },
            "coverage_gaps": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "description": "Coverage-gap warnings about packages this command adds to the lock whose advisory records the source could not read; present only when --accept-coverage-gaps allowed the candidate, which is otherwise rejected"
            },
            "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",
        "declared_exposed",
        "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"
        },
        "declared_exposed": {
          "type": "boolean",
          "description": "The operator declared this package, or the package it replaces, exposed with --exposed. It orders the report and changes nothing else. Added in schema_version 3"
        },
        "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"
          }
        }
      }
    }
  }
}
