Through the Looking Glass: Who Stole the Tarts? — Chasing the Cheshire Cat
This is post 5 in a seven-part series on what the Anchore Enterprise API makes possible for container security teams.
“Well! I’ve often seen a cat without a grin,” thought Alice, “but a grin without a cat! It’s the most curious thing I ever saw in all my life!” A zero-day vulnerability has the same trick. It appears everywhere at once, sometimes leaving nothing behind but a CVE ID — a grin — long after the package itself has been patched, repackaged, or nested two JARs deep inside another dependency. Take Log4Shell: log4j-core turned up in container images nobody knew were running Java at all, and kept turning up for months.
When a new CVE drops, the first question is always the same: are we affected, and if so, where? Answering that manually — clicking through images one at a time, running ad hoc scans, waiting for analysts to compile results — is exactly the wrong approach when the clock is running. The query endpoints in Anchore Enterprise are designed for this moment. With two API calls and a small amount of Python, you can go from a CVE ID to a complete picture of your container fleet’s exposure.
Two Trails to the Cat: The Endpoints
Two endpoints do the heavy lifting for zero-day investigation. The first tells you what the Cat looks like; the second tells you where it’s been spotted.
/v2/query/vulnerabilities — Given one or more CVE IDs, this returns everything Anchore knows about the vulnerability: severity, affected packages and versions, fix availability, NVD and vendor data, and references. This is where you start — understand what the vulnerability actually affects before you go looking for it.
/v2/query/images/by-package — Given a package name (and optionally a type and version), this returns every container image in your fleet that contains that package. Cross-reference this with the affected packages from the first query and you have your blast radius.
Spotting the Cat: Query the CVE
Before you can chase something, you have to know what you’re chasing. The /query/vulnerabilities endpoint accepts one or more CVE IDs and returns the full vulnerability record for each:
curl -s -u _api_key:<your-api-key> \
"https://wonderland.example.com/v2/query/vulnerabilities?id=CVE-2021-44228" \
| jq .You can query multiple CVEs in a single call — useful when a vulnerability has related follow-on IDs (Log4Shell’s grin reappeared as CVE-2021-45046 a few days after the original disclosure):
curl -s -u _api_key:<your-api-key> \
"https://wonderland.example.com/v2/query/vulnerabilities?id=CVE-2021-44228&id=CVE-2021-45046" \
| jq .The response includes the severity rating, a list of affected packages with the vulnerable version ranges, fix availability, NVD and vendor CVSS scores, and external references. The affected_packages array is what you’ll use to drive the next step — each entry gives you a package name, type, and version to search for.
Where Has It Been Seen? Find Affected Images
With the affected package details in hand, ask Anchore where the package has shown up across your fleet:
curl -s -u _api_key:<your-api-key> \
"https://wonderland.example.com/v2/query/images/by-package?name=log4j-core&package_type=java" \
| jq .The name parameter is required. package_type and version are optional but useful for narrowing results when a package name is common across multiple ecosystems. The response returns a paginated list of images, each with the matching package details and the image’s tag history — every place the Cat has been seen, and under what name.
The Knave’s Defence: Understanding will_not_fix
At the trial of the Knave of Hearts, the Knave’s defence was simple: he hadn’t written the letter, his name wasn’t on it, and the whole thing was somebody else’s mess. Distro vendors sometimes file the same kind of plea, and the API reports it back to you as will_not_fix.
When will_not_fix is true on an affected package, the vendor of the image’s OS distribution has assessed the vulnerability and either disagrees with the upstream severity rating or has explicitly indicated they do not intend to ship a fix. This is common with distro-specific package assessments — a vulnerability that NVD rates Critical may be rated differently by, say, the Debian or Red Hat security teams based on how the package is actually used in their distribution.
Whether to include or exclude will_not_fix packages in your investigation depends on your organization’s policy. For a broad initial sweep during a zero-day event, you may want to include everything and triage later (the Queen’s “sentence first, verdict afterwards” approach, applied a little more responsibly). For a more targeted assessment focused on actionable findings, filtering them out reduces noise. The script below supports both via a flag.
Calling the Witnesses: Blast Radius in One Script
The trial wasn’t decided by speeches; it was decided by the witnesses called to the stand. The script below calls the witnesses: it takes one or more CVE IDs, extracts every affected package from the vulnerability record, queries for container images containing each package, and produces a complete blast radius summary.
import sys
import requests
from collections import defaultdict
ANCHORE_URL = "https://wonderland.example.com/v2"
AUTH = ("_api_key", "<your-api-key>")
def get_vulnerability(cve_ids):
params = [("id", cve_id) for cve_id in cve_ids]
resp = requests.get(
f"{ANCHORE_URL}/query/vulnerabilities",
params=params,
auth=AUTH,
)
resp.raise_for_status()
return resp.json().get("vulnerabilities", [])
def get_images_by_package(name, package_type=None, version=None):
params = {"name": name}
if package_type:
params["package_type"] = package_type
if version:
params["version"] = version
images = []
page = 1
while True:
params["page"] = page
resp = requests.get(
f"{ANCHORE_URL}/query/images/by-package",
params=params,
auth=AUTH,
)
resp.raise_for_status()
data = resp.json()
images.extend(data.get("images", []))
if not data.get("next_page"):
break
page += 1
return images
def investigate(cve_ids, skip_will_not_fix=False):
print(f"Querying: {', '.join(cve_ids)}")
if skip_will_not_fix:
print(" (excluding packages marked will_not_fix by vendor)\n")
else:
print(" (including packages marked will_not_fix by vendor)\n")
vulns = get_vulnerability(cve_ids)
if not vulns:
print("No vulnerability records found.")
return
affected_images = defaultdict(set) # image_digest -> set of CVEs
for vuln in vulns:
cve_id = vuln.get("id")
severity = vuln.get("severity", "Unknown")
affected_packages = vuln.get("affected_packages", [])
print(f"{cve_id} [{severity}]")
print(f" Affected packages: {len(affected_packages)}")
for pkg in affected_packages:
name = pkg.get("name")
pkg_type = pkg.get("type")
will_not_fix = pkg.get("will_not_fix", False)
if skip_will_not_fix and will_not_fix:
continue
images = get_images_by_package(name, package_type=pkg_type)
for img in images:
digest = img["image"].get("image_digest")
affected_images[digest].add(cve_id)
print()
if not affected_images:
print("No affected images found in fleet.")
return
print(
f"Blast radius: {len(affected_images)} image(s) affected\n"
)
for digest, cves in affected_images.items():
print(
f" {digest[:20]}... "
f"CVEs: {', '.join(sorted(cves))}"
)
if __name__ == "__main__":
args = sys.argv[1:]
skip = "--skip-will-not-fix" in args
cve_ids = (
[a for a in args if not a.startswith("--")]
or ["CVE-2021-44228"]
)
investigate(cve_ids, skip_will_not_fix=skip)Running it is as simple as passing one or more CVE IDs on the command line:
# Include all affected packages
python3 investigate.py CVE-2021-44228 CVE-2021-45046
# Exclude packages where the vendor won't provide a fix
python3 investigate.py CVE-2021-44228 CVE-2021-45046 --skip-will-not-fixThe script handles pagination automatically, so it works regardless of fleet size. It also deduplicates images across multiple CVEs — if an image is affected by both CVEs in a query, it appears once with both IDs listed.
Following the Trail: Narrowing by Tag History
The Cheshire Cat’s vanishings were never quite total — Alice could usually retrace its path by what it had said last and where. The images response includes a tag_history array for each image, giving you the full list of tags ever associated with that digest. This is useful for identifying which specific deployments are affected — not just that a digest is vulnerable, but which named services or versions carry it. You can extend the script to surface this:
for img in images:
image_data = img.get("image", {})
digest = image_data.get("image_digest")
tags = [
t.get("full_tag")
for t in image_data.get("tag_history", [])
]
print(
f" {digest[:20]}... "
f"tags: {', '.join(tags) if tags else 'none'}"
)Up Next
When a zero-day drops, the gap between “we’ve heard about this CVE” and “we know exactly which of our container images are affected” should be measured in seconds, not hours. The /query/vulnerabilities and /query/images/by-package endpoints make that possible — from a CVE ID to a complete blast radius in a single script execution.
Next in the series: Tweedledee and Tweedledum — Comparing Vulnerabilities Across Image Versions. Tweedledee and Tweedledum spent their time arguing about who looked the same and who looked different. Container image versions raise the same question every release — what actually changed between v1.2 and v1.3, which vulnerabilities got fixed, and which ones quietly came along for the ride? We’ll diff vulnerability findings between image versions and get a clear answer.
If you’re an Anchore Enterprise customer looking to build with the API, the Customer Success team is the fastest way to get unblocked — reach out through the Anchore Support Portal. If you’re not a customer yet but want to see what any of this looks like against your own container images, request a demo and we’ll walk you through it.