JSON report¶
upgradescope scan --output json writes the report as one JSON object;
--write-baseline writes the same document to a file, and the GitHub
Action's report-json output points to it. It is the contract for CI
pipelines and tools built on the scan.
The schema is
api/report.schema.json
(JSON Schema draft 2020-12). TestJSONReportMatchesSchema validates real
scan output against it, with findings, folded callers, suppressed findings,
a baseline, not-assessed gaps and unrecognized images, and fails on any field the
schema does not list.
Versioning¶
schemaVersion is 1. Within a schema version, fields are only added:
never renamed, removed, retyped or given a new meaning. Ignore fields you do
not know; the schema allows them for that reason. Finding categories may be
added too (unknown-api was), so the schema gives category as a string
and lists the known ones in its description: handle a category you do not
know by its severity. The severity, verdict and baselineState
values are fixed. A breaking change bumps
schemaVersion and is listed in the changelog
(compatibility policy). toolVersion names
the binary that wrote the report and carries no compatibility meaning.
The server's report-shaped responses lead with the same two fields: a
cluster's report (GET /api/v1/clusters/{id}/report, stored or what-if)
and the gate's JSON answer (POST /api/v1/gate). There toolVersion is
the serving server's build, also for an evaluation an older server stored
(REST API).
The MCP tools return these documents too: get_report is this schema, and
the output schemas of scan and list_findings refer to its definitions
(AI assistants).
Example¶
scan --files rendered --target 1.37 --output json on one
networking.k8s.io/v1beta1 Ingress (the test validates this example too;
the digests in kbVersion are illustrative, as a digest changes with any
edit to its dataset):
{
"schemaVersion": 1,
"toolVersion": "0.2.0",
"filesBase": "rendered",
"clusterId": "files",
"target": "1.37",
"kbVersion": "k8s.io/api v0.37.1; lifecycle 696a4b81; registry de96a5da",
"score": 75,
"ready": false,
"verdict": "blocked",
"findings": [
{
"category": "removed-api",
"severity": "blocker",
"key": "removed-api/networking.k8s.io/v1beta1/Ingress",
"title": "networking.k8s.io/v1beta1 Ingress removed in 1.22 (1 object)",
"detail": "1 manifest object(s) use this API: shop (1).",
"namespaces": ["shop"],
"remediation": "migrate to networking.k8s.io/v1 Ingress; networking.k8s.io/v1 is not a rename of the apiVersion: spec.backend becomes spec.defaultBackend, a backend's serviceName becomes service.name and its servicePort becomes service.port.number (numeric) or service.port.name (string), and pathType (Prefix, Exact or ImplementationSpecific, which matches the undefined v1beta1 behaviour) is required on every path",
"citations": ["https://kubernetes.io/docs/reference/using-api/deprecation-guide/"],
"objects": [{"namespace": "shop", "name": "web", "file": "all.yaml", "line": 1}]
}
],
"notAssessed": [
{"capability": "deprecated-calls", "reason": "files mode"},
{"capability": "helm", "reason": "files mode"},
{"capability": "versions", "reason": "files mode"}
],
"teams": {"(unattributed)": {"score": 75, "ready": false, "verdict": "blocked", "blockers": 1, "warnings": 0}}
}
Fields¶
| Field | Meaning |
|---|---|
schemaVersion |
1. |
toolVersion |
The upgradescope version that wrote it. |
filesBase |
--files only: the scanned directory that object file paths are relative to; relative to the working directory when inside it, absolute otherwise. |
clusterId |
The cluster's kube-system namespace UID, or files. |
target |
The target minor. |
serverVersion |
Live scans only: the kube-apiserver's gitVersion (v1.34.2-gke.100), the version the target was judged against. |
kubeContext |
Live scans only: the kubeconfig context the scan read. |
apiServer |
Live scans only: the API server the scan read, as scheme, host and port (https://10.0.0.1:6443); never the kubeconfig's credentials, path or query. |
kbVersion |
The knowledge base the report was judged with. |
score |
0–100 (formula). |
verdict |
ready, blocked or unknown (rules). |
ready |
verdict == "ready", kept for v0.1 readers. |
findings[] |
Sorted by severity, then category, then title. Each has category (list), severity, key (stable across runs: baselines and notifications match on it), title, detail, and where they apply teams (of every affected namespace), namespaces (at most 100, with namespacesOmitted), unnamespaced (true on an add-on finding that also covers an install in no named namespace, a manifest object without metadata.namespace or a cluster-scoped IngressClass, which a namespace-scoped ignore rule cannot be shown to match), remediation, citations, objects (at most 100, with objectsOmitted), callers and baselineState (new or unchanged, with --baseline). callers is on API usage findings only: the apiserver_requested_deprecated_apis rows folded into the finding, in row order, each with group (absent for the core group), version, resource, subresource, key, severity, title and detail, which are what its standalone deprecated-api-in-use finding would be (how suppression treats them). |
notAssessed[] |
What the scan could not see: capability, reason, and required when the gap makes the verdict unknown, partial and skipped when a capability read only part of what it covers. |
suppressed[] |
Findings an ignore rule or annotation accepted, with reason, source and expires. Not in the score or verdict. An entry keeps its callers only when a rule without object selectors took it; when annotations or rules with namespace, name or file took it, its callers stand in findings[] as deprecated-api-in-use findings of their own. |
unrecognizedImages[] |
Image repositories (host/path, no tag) that no add-on image matcher recognised, sorted, at most 200. A gap in add-on detection, not a finding: not in the score or verdict, and an add-on running one may still have been found by its labels or Helm release. An image that a newer registry (the server's, when it differs from the agent's) claims can hide a blocker, such as a retired product: the report then has a required addons gap naming the knowledge base skew. |
unrecognizedImagesOmitted |
How many more unrecognized repositories there were beyond the 200 listed. |
addOnEvidenceAgeSeconds |
How old, in seconds (at least 1), the pod evidence the add-ons were detected from was when the agent reused its last full pod pass instead of listing every pod again (--pod-pass-every, --pod-pass-max-age): an add-on installed or upgraded since is reported as it was then, for at most the agent's --pod-pass-max-age. Absent when every pod was read for this report, as a scan's always are, and from collectors that predate the field. The verdict does not depend on it. See the agent's cost. |
support |
A cluster on EKS, GKE or AKS whose Kubernetes minor the knowledge base dates: provider, minor, phase (standard, ending, extended, ended), extendedSupportFrom (the day extended support begins) and extendedSupportEnds, and, only where the provider's price is cited, annualCostDelta (what extended support adds per cluster per year, a decimal string), currency and priceAsOf: a list price as of that day, not a bill, with annualCostNote when the provider charges it only to some clusters. Where the provider's extended support is opt-in (GKE, AKS) extendedSupportCondition names the configuration it applies under, and phases ending and extended then describe the provider's window, not a confirmed enrolment. Present whether or not it is a finding; absent for other clusters and manifests. The same for every target. See managed-provider support. |
hops[] |
scan --plan only: the upgrade plan, one entry per control-plane upgrade from the cluster's minor (or --from) to target, in order. Each has from, to, the score and verdict at to, findings (the ones first seen at this upgrade, in full), changed and carried (findings listed at an earlier upgrade, by key, with their severity and title here, since, the upgrade that lists them, and in changed, was, the earlier severity) and notAssessed. Every other field is the report at target, the verdict included. |
teams |
Per-team scores; findings without a team are under (unattributed), which is every finding of a --files scan (it reads no Namespace labels; team attribution needs a live cluster or an agent push). A team's verdict is blocked by a blocker of its own or by an unattributed one (it cannot be ruled out as the team's), otherwise unknown when the report has a required not-assessed gap, otherwise ready; another team's blockers do not lower it, and ready is verdict == "ready". score, blockers and warnings count only the team's own findings. |
The server's report endpoint serves the same report fields (except
kubeContext, apiServer and hops, which only scan sets), plus where the
report came from (GET /api/v1/clusters/{id}/report);
the SARIF output carries the same findings as results, keyed by key.