# AKS Security Advisories > Community, open-source security-advisory site for Azure Kubernetes Service > (AKS) artifacts. It tracks per-CVE fix status sourced from public data. It is > NOT an official Microsoft/Azure product and NOT an authoritative source of > truth for AKS security posture. Base URL: https://chrischangcode.github.io/aks-security-advisory/ ## Important for agents A GENERATED, build-consistent companion to this file is published at `data/llms.txt` (OPTIONAL -- a NEW endpoint that may be absent on an older feed artifact; a site-only deploy re-serves the newest EXISTING feed, so the site can be newer than the feed it renders. Treat a 404 as "this feed predates it", then use `data/manifest.json` + `data/glossary.json` instead). When present it additionally carries the live per-endpoint byte sizes, the current VHD release-label list, and a worked `(path, CVE)` example whose identifiers are resolved from that same build (so replaying that example is reliable). This static file states only stable facts; for any exact byte size, count or release label read `data/manifest.json` / `data/glossary.json` / `data/by-release/index.json` (or `data/llms.txt` when the feed publishes it), which are generated next to the feed they describe. The human-facing pages under vhd/ are a hash-routed single-page app (SPA). The content of an advisory route such as `vhd/#/CVE-2025-1234` is rendered client-side by JavaScript; fetching that URL WITHOUT a JS runtime returns only an empty HTML shell. Do NOT scrape the rendered HTML. Read the JSON feed directly (below) -- it is the machine-readable source and needs no JavaScript. The site root (this page's directory) is static HTML and is safe to fetch: it is a hub linking to each advisory set. Legacy links of the form `/aks-security-advisory/#/` are redirected by the hub into `vhd/`. ## Sections - VHD (node image) advisory -- LIVE. Per-CVE fix status for AKS node images (Azure Linux and Ubuntu base packages baked into the VHD). Human UI: /aks-security-advisory/vhd/#/ - Container image advisory -- PLANNED, not yet published. Will cover the container/OCI images AKS ships. Tracked in issue #40. ## JSON feed (read this, not the HTML) The feed is deployed at `/aks-security-advisory/data/` (relative to the site root). All files are plain JSON. Endpoint shapes: - data/index.json Listing + facets: { schema_version, generated, count, releases[], statuses[], severities[], advisories[] } Each advisories[] entry: { id, headline_status, severity, updated, releases[], packages[], summary? } - data/manifest.json (OPTIONAL -- may be absent on older feeds) Generated inventory of every endpoint this feed publishes, with real byte sizes -- read this FIRST to plan fetches and avoid the large index.json (its exact byte size, and every other endpoint's, is recorded here). The inventory is exhaustive: it also lists the discovery descriptors themselves (llms.txt, schema.json, dataset.jsonld, sitemap.xml), so enumerating it reveals the whole discovery layer: { schema_version, type, generated, data_root, count, endpoints[] } Each endpoints[] item is a document or a collection: { path, kind, bytes, media_type, schema_version, optional, entries? } or { path, kind:"collection", files, bytes, schema_version, optional, pattern?, index? } pattern (e.g. advisories/{id}.json) is present ONLY when item names are used verbatim; by-package/ and by-release/ have NO pattern -- their filenames are sanitized, so resolve a name through the collection's index, never build it. - data/index-slim.json (OPTIONAL -- may be absent on older feeds) id + headline_status + severity only (a small fraction of index.json). Enough to enumerate + triage without the packages[]/releases[] bulk: { schema_version, type, count, generated, advisories[{ id, headline_status, severity? }] } - data/by-package/index.json (OPTIONAL -- may be absent on older feeds) Reverse index: which advisories name a package. Resolve a name to a file here (filenames are sanitized; do not guess), then fetch the small per-package file. A (package, CVE) lookup is <=2 fetches, <100 KB: { schema_version, type, generated, count, packages[{ name, file, count }], aliases[{ name, alias_of, file }]? } - data/by-package/.json Advisories naming this package. Release labels are interned into a document- top releases[] table addressed by integer index: { schema_version, type, package, count, generated, releases[], advisories[{ id, headline_status, severity?, releases:[int,...] }] } - data/by-release/index.json (OPTIONAL -- may be absent on older feeds) Same, keyed by VHD lineage label (label -> file mapping): { schema_version, type, generated, count, releases[{ name, file, count }] } - data/by-release/.json Advisories touching this lineage. Package names are interned: { schema_version, type, release, count, generated, packages[], advisories[{ id, headline_status, severity?, packages:[int,...] }] } - data/advisory-pages/.html (OPTIONAL -- may be absent on older feeds) The same advisory as a pre-rendered page, for clients that cannot run the browser's JavaScript. The browser deep link (vhd/#/) is hash-routed, so the server never receives the id and returns an empty shell; this path returns the advisory itself. Carries the summary, the packages[] table and a schema.org JSON-LD block. The JSON below stays authoritative -- prefer it whenever you can parse JSON. - data/advisories/.json Full per-advisory detail. is uppercase and may be CVE-*, GO-* (Go vuln DB) or GHSA-* (GitHub advisories): { schema_version, id, severity, headline_status, headline_status_note, updated, description?, references[], packages[], assessed_releases?, assessment_scope, additive } Each packages[] row: { package, releases[], status, severity, advisory_id?, fixed_version?, upstream_fixed_version?, first_fixed_build?, latest_build?, justification?, statement?, evidence? } releases[] lists EVERY lineage this assessment covers. Rows identical apart from the lineage are merged into one, so match your SKU by MEMBERSHIP (your_label in row.releases), never by equality on a single field. Any genuine per-lineage difference (a different first_fixed_build or fixed_version) keeps the rows separate, so the merge hides nothing. (release, package) is USUALLY unique, so normally exactly one row answers "what is the status of package P on lineage L" -- but do not assume it. A rare overlay artefact can leave two rows for one SKU with conflicting statuses. If more than one row matches, apply the same rule the feed uses for headline_status: take the MOST ACTIONABLE status, ordering affected > fix_available_upstream > under_investigation > fixed > not_affected. Never just take the first match. headline_status is the single most-actionable status across ALL OS/SKU lineages -- a cross-lineage triage summary, NOT the answer for one SKU. For a specific SKU read packages[].status on the row whose releases[] CONTAINS your lineage label; headline_status_note repeats this caveat inline on every advisory. assessed_releases (OPTIONAL -- may be absent on older feeds) lists every VHD lineage label GENUINELY assessed for this CVE. A label present here with NO packages[] row means "assessed and NOT affected on that lineage"; a label ABSENT was never assessed (no data source -- shared distro feed or that lineage's own binary scan -- covered it for that lineage), so its absence is UNKNOWN, never "clean". It is always a superset of the union of every packages[].releases[]. Use it so you never have to infer coverage from the mere absence of a lineage from the package rows. assessment_scope tells you WHICH build produced the document, by value (never by a missing key): "vhd-lineage-feed" = the published feed, assessed_releases is present and authoritative; "single-cve-query" = an ad-hoc single-CVE build that did NOT compute the assessed set, so assessed_releases is absent (means "not computed", never "assessed nowhere"). Feed documents are always "vhd-lineage-feed". - data/pathmap/index.json (OPTIONAL -- may be absent on older feeds) Installed-path -> package map index: { schema_version, generated, default_lineage, lineages{label->key}, maps[] } - data/pathmap/.json One installed-path -> package map (key comes from pathmap/index.json). Usually scoped to packages installed on the node (see owner_scope) and usr-merge normalized (both /bin and /usr/bin spellings of a path are present as real keys): { schema_version, type, key, os_family, release, path_count, package_count, owner_scope, indexed_prefixes[], usr_merge, path_normalization{}, coverage_note, ambiguous_paths[], alias_of{derived->canonical}, by_path{path->[package,...]}, by_base{basename->[package,...]}, packages[name,...] } A MISS is NOT "not vulnerable": see coverage_note. owner_scope is always present: "installed" means owners were filtered to packages baked on the node; "archive" means no roster was available and every distro-archive owner is still listed (a listed owner is then NOT evidence the package is on the node). indexed_prefixes lists the executable prefixes indexed (paths containing /bin/ or /sbin/); a probe outside them is out of scope, not clean. Both usr-merge spellings of a path are real keys in by_path; alias_of maps each materialized twin to the spelling the distro recorded (collapse for display, but either key resolves). packages is a sorted array of the installed owners surviving the filter, emitted only when owner_scope is "installed". - data/components.json (OPTIONAL -- may be absent on older feeds) Extended-binary coverage manifest: { schema_version, type, generated, families{family->{components[], counts{}}} } Each components[] item: { name, coverage, version?, package?, note? } - data/glossary.json (OPTIONAL -- may be absent on older feeds) The status / coverage / field vocabulary, with per-value counts and shares MEASURED from the advisories in THIS build (so "how common is a status" is a fact about the build, not a hand-written claim): { schema_version, type, generated, status{labels{}, help{}, counts{status->n}, shares{status->fraction}, total, counts_basis}, coverage{labels{}, help{}, why{}, counts{}, shares{}, total}, field{help{}} } Use status.help for what each status means and status.counts/shares for how often each status is emitted in this build. Do NOT assume `affected` is rare -- consult status.counts/shares rather than any prior expectation of its magnitude. IMPORTANT: status.counts/shares are the per-advisory HEADLINE distribution (one most-actionable status per advisory, via headline_status), NOT per-package-row prevalence -- do not read status.shares.affected as "the share of my packages that are affected" (that is the F7 misread). The status.counts_basis field states this basis inline. - data/llms.txt (OPTIONAL -- may be absent on older feeds) This file's build-consistent companion, generated with the feed. NEW endpoint: a feed artifact built before it existed does not carry it, and a site-only deploy re-serves the newest EXISTING feed, so the site can be newer than the feed it renders. Treat a 404 as "this feed predates it", then fall back to data/manifest.json + data/glossary.json. Adds live per-endpoint byte sizes, the VHD release-label list and a build-verified worked example. - data/schema.json (OPTIONAL -- may be absent on older feeds) JSON Schema (draft-07) for the feed documents. NEW endpoint; treat a 404 as "this feed predates it". - data/dataset.jsonld (OPTIONAL -- may be absent on older feeds) schema.org Dataset whose distribution[] mirrors the data endpoints. NEW endpoint; treat a 404 as "this feed predates it". - data/sitemap.xml (OPTIONAL -- may be absent on older feeds) urlset of the site pages, feed endpoints and per-advisory URLs. NEW endpoint; treat a 404 as "this feed predates it". A stable, human-readable data contract is published as DATA-CONTRACT.md at the site root (same directory as this file). It states what consumers may rely on, what may change, and how versioning is signalled; it defers to `data/manifest.json` for the authoritative endpoint list and optional flags, so the two can never disagree about whether a 404 is an error. ## Controlled vocabularies Full meanings + per-value counts MEASURED from the current build are in `data/glossary.json` (status.help / coverage.help / field.help, with status.counts / status.shares). The stable status names are: - fixed -- a scanned VHD build ships a package at/above the distro's fixed version. - fix_available_upstream -- the DISTRO (Azure Linux / Ubuntu) published a fixed package, but no scanned VHD build ships it yet. Only distro-packaged rows ever carry this; a Trivy-scanned AKS binary never does. - affected -- vulnerable, with no fix AKS can ship yet. This also covers a Trivy-scanned binary whose bundled module has an upstream fix but which has not been rebuilt and shipped: a module fix is not a shippable artifact, so the row stays affected and records the module fix in evidence.module_fixed_version. This is NOT rare; read glossary.json status.counts rather than assuming its magnitude. - not_affected -- excluded by a vendor verdict (with justification). - under_investigation -- triage incomplete / no verdict yet. - will_not_fix -- the vendor will not ship a fix. The extended-binary coverage names (components.json / glossary.json coverage): covered_by_package, covered_by_scan, scan_pending, aks_built, container_image, not_baked, not_assessed. ## VHD release labels The lineage labels (OS family + generation/SKU, e.g. `AKSAzureLinuxV3/gen2`) this feed assessed appear in advisories[].packages[].releases[], are the keys of `data/by-release/`, and are listed with counts in `data/by-release/index.json`. The generated `data/llms.txt`, when the feed publishes it (OPTIONAL -- treat a 404 as "this feed predates it"), enumerates the exact set for the current build. Resolve a scanner path WITHIN one of these lineages (see F4/F5 below), never against a merged/global map. ## Hard rules for agents (do not violate) - F4 / F5 -- INSTALL PATHS ARE PER-FAMILY. Each VHD family (Ubuntu vs Azure Linux, and each generation/SKU lineage) has DIFFERENT binary install paths. A path that owns package X on one family may be absent, or map elsewhere, on another. Resolve a path using the pathmap for THAT lineage, never a global map. Both usr-merge spellings (`/bin/x` and `/usr/bin/x`) are real keys in `by_path`; fall back to `by_base` on the basename. - F6 -- A MISS IS "UNKNOWN", NOT "NOT VULNERABLE". If a path, a package, or a (CVE, lineage) pair finds nothing, that is unknown / not-assessed, never safe. The pathmap says so via `coverage_note` and `indexed_prefixes` (only /bin and /sbin executables are indexed, so a .so / JAR / Go-binary path is out of scope, not clean). Report unknown. - F7 -- `headline_status` IS NOT PER-SKU. It is the single most-actionable status across ALL lineages (ranked). For a specific SKU, read the `packages[]` row in `data/advisories/.json` whose `releases[]` CONTAINS your lineage label -- one row covers every lineage sharing that assessment, so test membership, not equality. Absence of your lineage from every row means NOT ASSESSED, not not-affected. `status.counts` in glossary.json is likewise headline-derived -- its `counts_basis` field says so; do not read `status.shares.affected` as "the share of my packages affected". ## Worked recipe -- a (binary path, CVE) spreadsheet row Given a scanner row (absolute binary path, CVE) and the node's VHD lineage, report the AKS fix status without guessing: 1. GET data/manifest.json -- inventory endpoints + byte sizes; plan fetches. 2. GET data/pathmap/index.json, map your lineage label to its key, then GET data/pathmap/.json. Look up the exact path in `by_path` (try both usr-merge spellings; fall back to `by_base` on the basename) to get the owning package(s) FOR THAT FAMILY (F4/F5). No owner -> UNKNOWN (F6). 3. GET data/by-package/index.json, resolve the package to its file (or alias) -- do NOT construct the filename -- then GET data/by-package/.json and find your CVE id in advisories[]. (<=2 fetches, <100 KB.) 4. GET data/advisories/.json and read the `packages[]` row whose `releases[]` CONTAINS your lineage label -- that row's `status` is the SKU-level answer (NOT `headline_status`, F7). Rows identical apart from the lineage are merged, so match by membership, not equality. No row listing your lineage -> NOT ASSESSED (F6). When the feed publishes it, `data/llms.txt` contains a concrete, build-verified instance of this recipe (a NEW, OPTIONAL endpoint -- treat a 404 as "this feed predates it"). ## Suggested fetch order 1. GET data/manifest.json -- inventory every endpoint (with real byte sizes) so you can plan fetches and avoid the large index.json. Optional; treat a 404 as "this feed predates the manifest", then fall back to the endpoints below. 2. GET data/index-slim.json -- enumerate every advisory (id + headline_status + severity) cheaply, or data/index.json for the full listing + facets. 3. To answer "which CVEs affect ?": GET data/by-package/index.json, resolve to its file (or its alias), then GET data/by-package/.json. <=2 fetches, <100 KB. Symmetrically, data/by-release/ answers "which CVEs touch ?". 4. GET data/advisories/.json -- per-CVE detail on demand. 5. GET data/pathmap/index.json then data/pathmap/.json -- map a scanner path (e.g. /usr/bin/curl) to owning packages. Both are optional; treat a 404 as "this feed does not publish path maps", not as an error. 6. GET data/components.json -- extended-binary coverage. Optional; treat a 404 as "this feed does not publish a coverage manifest", not as an error. 7. GET data/glossary.json -- status/coverage/field vocabulary with per-value counts measured from this build. Optional; treat a 404 as "this feed does not publish a glossary", not as an error.