Through the Looking Glass: Tweedledee and Tweedledum
This is post 6 in a seven-part series on what the Anchore Enterprise API makes possible for container security teams.
Tweedledee and Tweedledum stood under their tree looking nearly identical — same coat, same posture, same expression — and Alice could only tell them apart by the collars embroidered DUM and DEE. Two consecutive versions of a container image look the same way at first glance. Same name, same base, similar contents — but not quite the same. The difference between whiterabbit-api:1.0 and whiterabbit-api:1.1 might be a handful of updated packages, a changed dependency, or a patched OS layer.
From a security standpoint, what matters is what changed in the vulnerability profile: what got fixed, what got introduced, and what persisted unchanged. Anchore Enterprise has a built-in image-comparison capability for separating vulnerabilities inherited from a parent image, and supports versioning applications for future comparison — but for a single image, version-to-version isn’t something the UI is designed to surface directly. The API makes it straightforward: with two vulnerability queries and some set operations in Python, you can answer “what did this release actually fix?” with precision.
Reading the Collars: Looking Up Image Digests by Tag
Alice had to look at the embroidered collars to keep DUM and DEE straight. Container images are identified the same way: the tag is the collar; the digest is the actual identity underneath. Before comparing vulnerability data, you need the digest for each image version. The /v2/images endpoint accepts a full_tag query parameter for exactly this:
curl -s -u _api_key:<your-api-key> \
"https://wonderland.example.com/v2/images?full_tag=docker.io/wonderland/whiterabbit-api:1.0" \
| jq '.[0].imageDigest'Repeat for the second version:
curl -s -u _api_key:<your-api-key> \
"https://wonderland.example.com/v2/images?full_tag=docker.io/wonderland/whiterabbit-api:1.1" \
| jq '.[0].imageDigest'With both digests in hand, you can query the vulnerability data for each.
Asking Each Twin What They Know: Fetching Vulnerability Data
The vulnerability endpoint is the same one we’ve used throughout this series. Ask each twin in turn:
curl -s -u _api_key:<your-api-key> \
"https://wonderland.example.com/v2/images/sha256:<digest>/vuln/all" \
| jq '.vulnerabilities[].vuln'Run this for both digests and you have everything you need to diff.
Contrariwise: The Version Diff Script
“Contrariwise,” Tweedledee insisted whenever something needed to be looked at from the other side. A version diff is the same exercise: from one side, what got fixed; from the other, what got introduced; and from the middle, what stuck around in both. The script below takes two image tags, resolves their digests, fetches their vulnerability data, and produces a structured diff — vulnerabilities that were fixed between versions, vulnerabilities that were newly introduced, and those that persist in both:
import sys
import requests
ANCHORE_URL = "https://wonderland.example.com/v2"
AUTH = ("_api_key", "<your-api-key>")
def get_digest_for_tag(full_tag):
resp = requests.get(
f"{ANCHORE_URL}/images",
params={"full_tag": full_tag},
auth=AUTH,
)
resp.raise_for_status()
images = resp.json()
if not images:
raise ValueError(f"No image found for tag: {full_tag}")
return images[0]["imageDigest"]
def get_vulnerabilities(digest):
resp = requests.get(
f"{ANCHORE_URL}/images/{digest}/vuln/all",
auth=AUTH,
)
resp.raise_for_status()
vulns = resp.json().get("vulnerabilities", [])
return {v["vuln"]: v for v in vulns}
def diff_vulnerabilities(tag_a, tag_b):
print("Resolving digests...")
digest_a = get_digest_for_tag(tag_a)
digest_b = get_digest_for_tag(tag_b)
print(f" {tag_a}: {digest_a[:20]}...")
print(f" {tag_b}: {digest_b[:20]}...")
print("\nFetching vulnerability data...")
vulns_a = get_vulnerabilities(digest_a)
vulns_b = get_vulnerabilities(digest_b)
ids_a = set(vulns_a.keys())
ids_b = set(vulns_b.keys())
fixed = ids_a - ids_b
introduced = ids_b - ids_a
persisting = ids_a & ids_b
print(f"\nVulnerability diff: {tag_a} → {tag_b}")
print(f" Fixed: {len(fixed)}")
print(f" Introduced: {len(introduced)}")
print(f" Persisting: {len(persisting)}")
if fixed:
print(f"\nFixed in {tag_b}:")
for vuln_id in sorted(fixed):
v = vulns_a[vuln_id]
print(
f" {vuln_id} [{v.get('severity')}] "
f"— {v.get('package')} {v.get('package_version')}"
)
if introduced:
print(f"\nIntroduced in {tag_b}:")
for vuln_id in sorted(introduced):
v = vulns_b[vuln_id]
print(
f" {vuln_id} [{v.get('severity')}] "
f"— {v.get('package')} {v.get('package_version')}"
)
if persisting:
print("\nPersisting in both:")
for vuln_id in sorted(persisting):
v = vulns_b[vuln_id]
fix = v.get("fix")
fix_str = (
f" (fix available: {fix})"
if fix and fix != "None"
else ""
)
print(
f" {vuln_id} [{v.get('severity')}] "
f"— {v.get('package')} {v.get('package_version')}"
f"{fix_str}"
)
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python3 diff.py <tag_a> <tag_b>")
print(
" e.g: python3 diff.py "
"docker.io/wonderland/whiterabbit-api:1.0 "
"docker.io/wonderland/whiterabbit-api:1.1"
)
sys.exit(1)
diff_vulnerabilities(sys.argv[1], sys.argv[2])Running it produces output like:
Resolving digests...
docker.io/wonderland/whiterabbit-api:1.0: sha256:3a1f2e8dc4...
docker.io/wonderland/whiterabbit-api:1.1: sha256:9b7c4f1ae2...
Fetching vulnerability data...
Vulnerability diff: docker.io/wonderland/whiterabbit-api:1.0 → docker.io/wonderland/whiterabbit-api:1.1
Fixed: 12
Introduced: 2
Persisting: 34
Fixed in docker.io/wonderland/whiterabbit-api:1.1:
CVE-2023-1234 [High] — libssl 1.1.1n-0+deb11u4
CVE-2023-5678 [Critical] — zlib1g 1:1.2.11.dfsg-2
...
Introduced in docker.io/wonderland/whiterabbit-api:1.1:
CVE-2024-0001 [Medium] — curl 7.88.1-10+deb12u4
...
Persisting in both:
CVE-2022-9876 [High] — openssl 3.0.2-0ubuntu1 (fix available: 3.0.2-0ubuntu1.10)
...The persisting section is particularly actionable — vulnerabilities that survived the update and have a fix available are exactly what should feed back into the next release cycle. The broken rattle from Carroll’s chapter is the right reference: every quarrel needs to end with somebody picking up the pieces.
What’s Worth a Battle: Filtering by Severity
Tweedledee and Tweedledum agreed to fight over a rattle and then immediately called it off when a monstrous crow appeared overhead — proportion matters. For release validation or compliance documentation, you may only care about the diff at specific severity levels. The script can be extended with a severity filter before the diff output:
SEVERITY_FILTER = {"Critical", "High"}
fixed_filtered = {
k for k in fixed
if vulns_a[k].get("severity") in SEVERITY_FILTER
}
introduced_filtered = {
k for k in introduced
if vulns_b[k].get("severity") in SEVERITY_FILTER
}This keeps the full diff logic intact while surfacing only the findings that matter for your use case.
Up Next
A version diff turns a vulnerability scan from a point-in-time snapshot into a meaningful measure of progress. “We fixed 12 vulnerabilities in this release, introduced 2, and still have 34 to address — 8 of which now have fixes available” is a far more useful statement than a raw count. It also gives development teams a clear, honest picture of where things stand without requiring anyone to manually compare two scan results.
Final post in the series: Queen Alice — Automating Administrative Tasks via the API. Alice ends Through the Looking-Glass by being crowned and presiding over a chessboard’s worth of new responsibilities. We’ll turn from analysis to operations and use the API to handle the queenly tasks of running an Anchore deployment — creating users, managing accounts, granting permissions, and keeping the whole thing tidy.
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.