Automating Administrative Tasks via the Anchore Enterprise API

Through the Looking Glass: Queen Alice

This is post 7 in a seven-part series on what the Anchore Enterprise API makes possible for container security teams.

By the final chapter of Through the Looking-Glass, Alice has crossed the entire chessboard, reached the eighth square, and been crowned. She has gone from a bewildered visitor to a Queen — someone who understands the rules of Wonderland well enough to command them. That’s the right frame for where we’ve arrived in this series. We’ve explored SBOM data, built event-driven pipelines, queried across the fleet with GraphQL, investigated zero-day blast radius, and diffed vulnerability profiles between container image versions. What remains is the layer underneath all of that: the platform itself.

Account creation, user provisioning, permission grants, registry configuration — these are the administrative tasks that typically live in a runbook or get performed manually through the UI when onboarding a new team or environment. Every one of them is available through the API, which means they can be scripted, version-controlled, and integrated into whatever infrastructure automation your organization already relies on. The kingdom runs better when the Queen doesn’t have to sign every scroll by hand.

Drawing the Borders: Account Management

In Anchore Enterprise, accounts provide the top-level isolation boundary — separate teams, environments, or tenants each get their own account with their own container images, policies, and users. They’re the territories on the chessboard. Creating one via the API is a single call, available only to admin users:

curl -s -u _api_key:<your-api-key> \
  -X POST "https://wonderland.example.com/v2/accounts" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "mad-hatter-team",
    "email": "[email protected]"
  }'

Accounts can be enabled or disabled independently of deletion — useful for temporarily suspending access without losing configuration:

curl -s -u _api_key:<your-api-key> \
  -X PUT "https://wonderland.example.com/v2/accounts/mad-hatter-team/state" \
  -H "Content-Type: application/json" \
  -d '{"state": "disabled"}'

Deleting an account requires it to be in a disabled state first — a safeguard against accidental removal of active resources:

# Disable first
curl -s -u _api_key:<your-api-key> \
  -X PUT "https://wonderland.example.com/v2/accounts/mad-hatter-team/state" \
  -H "Content-Type: application/json" \
  -d '{"state": "disabled"}'

# Then delete
curl -s -u _api_key:<your-api-key> \
  -X DELETE "https://wonderland.example.com/v2/accounts/mad-hatter-team"

Calling the Court Together: User Management

An account without users is a kingdom without subjects. Users are created within an account context, and because user and credential management cannot be performed with API key authentication, these calls must use username/password credentials:

curl -s -u admin:password \
  -X POST "https://wonderland.example.com/v2/accounts/mad-hatter-team/users" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "alice",
    "password": "changeme123",
    "user_type": "native"
  }'

For production use, users should authenticate with API keys rather than passwords. As covered in the first post in this series, API keys are created per user and use the _api_key username convention — and importantly, must be created using username/password credentials, not an existing API key.

Three Cheers for Queen Alice: Granting Roles with RBAC

At Alice’s coronation feast, the Red Queen and White Queen tested whether she deserved her crown. Anchore RBAC is the reverse — once you have the authority, you decide who gets what title. Creating a user doesn’t grant them any access on its own; permissions in Anchore Enterprise are managed through RBAC roles. To see the full list of available roles in your deployment:

curl -s -u _api_key:<your-api-key> \
  "https://wonderland.example.com/v2/rbac-manager/roles" \
  | jq '.[].name'

To grant a user a role within a specific account, POST to the role’s members endpoint with the username and the account as the domain_name:

curl -s -u _api_key:<your-api-key> \
  -X POST "https://wonderland.example.com/v2/rbac-manager/roles/full-control/members" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "alice",
    "domain_name": "mad-hatter-team"
  }'

Roles can be revoked just as easily:

curl -s -u _api_key:<your-api-key> \
  -X DELETE "https://wonderland.example.com/v2/rbac-manager/roles/full-control/members?username=alice&domain_name=mad-hatter-team"

Opening the Gates: Registry Configuration

For Anchore to analyze container images from private registries, the gates have to be opened — the registry needs to be configured with the appropriate credentials. This too is fully API-driven:

curl -s -u _api_key:<your-api-key> \
  -X POST "https://wonderland.example.com/v2/registries" \
  -H "Content-Type: application/json" \
  -H "x-anchore-account: mad-hatter-team" \
  -d '{
    "registry": "registry.wonderland.example.com:5000",
    "registry_name": "internal-registry",
    "registry_user": "anchore-svc",
    "registry_pass": "s3cr3t",
    "registry_verify": true
  }'

The x-anchore-account header scopes the registry to the target account — each account manages its own registry credentials independently.

Decreeing the Laws: System Configuration

A queen sets the rules of the kingdom. Anchore Enterprise exposes a set of platform-level configuration settings through the API — things that affect system-wide behavior rather than individual accounts or users. These settings are viewable and updatable without requiring a config file change or service restart in many cases, making them well-suited for automation.

To see everything that’s configurable along with current values, descriptions, and whether a restart is required to apply a change:

curl -s -u _api_key:<your-api-key> \
  "https://wonderland.example.com/v2/system/configurations" \
  | jq '.items[] | {key: .key, value: .value, requires_restart: .requires_system_restart, description: .description}'

The full schema for all configurable settings — including valid values, types, and constraints — is available at a dedicated endpoint, useful for tooling that needs to validate inputs before applying them:

curl -s -u _api_key:<your-api-key> \
  "https://wonderland.example.com/v2/system/configurations/schema" \
  | jq .

To read the current value of a specific configuration key:

curl -s -u _api_key:<your-api-key> \
  "https://wonderland.example.com/v2/system/configurations/<config_key>" \
  | jq .

To update a single setting:

curl -s -u _api_key:<your-api-key> \
  -X PUT "https://wonderland.example.com/v2/system/configurations/<config_key>" \
  -H "Content-Type: application/json" \
  -d '{"key": "<config_key>", "value": "<new_value>"}'

To update multiple settings atomically in a single call — if any key fails validation, none are applied:

curl -s -u _api_key:<your-api-key> \
  -X PATCH "https://wonderland.example.com/v2/system/configurations" \
  -H "Content-Type: application/json" \
  -d '[
    {"key": "<config_key_1>", "value": "<value_1>"},
    {"key": "<config_key_2>", "value": "<value_2>"}
  ]'

To reset a setting back to its system default:

curl -s -u _api_key:<your-api-key> \
  -X DELETE "https://wonderland.example.com/v2/system/configurations/<config_key>"

Each configuration response includes a requires_system_restart field — worth checking before applying changes in a production environment, as some settings only take effect after the relevant service is restarted. The source field on each setting tells you where the current value originated: default, api, or config_file — useful for understanding whether a setting has been previously overridden and by what mechanism.

The Royal Proclamation: Team Provisioning Script

The real value of administrative automation is composing these operations into a repeatable provisioning workflow — one proclamation that calls a whole new team into being. The script below accepts a team name and a list of users, and handles the full onboarding sequence: account creation, user creation, role assignment, and registry configuration:

import requests

ANCHORE_URL = "https://wonderland.example.com/v2"

# admin API key for account, RBAC, and registry operations
AUTH = ("_api_key", "<your-admin-api-key>")

# username/password required for user + credential operations
USER_MGMT_AUTH = ("admin", "<your-admin-password>")

def create_account(name, email=None):
    body = {"name": name}
    if email:
        body["email"] = email
    resp = requests.post(
        f"{ANCHORE_URL}/accounts", json=body, auth=AUTH
    )
    resp.raise_for_status()
    print(f"  Created account: {name}")
    return resp.json()

def create_user(account, username, password, user_type="native"):
    resp = requests.post(
        f"{ANCHORE_URL}/accounts/{account}/users",
        json={
            "username": username,
            "password": password,
            "user_type": user_type,
        },
        auth=USER_MGMT_AUTH,  # API keys cannot manage users or credentials
    )
    resp.raise_for_status()
    print(f"  Created user: {username} in {account}")

def grant_role(username, account, role="full-control"):
    resp = requests.post(
        f"{ANCHORE_URL}/rbac-manager/roles/{role}/members",
        json={"username": username, "domain_name": account},
        auth=AUTH,
    )
    resp.raise_for_status()
    print(f"  Granted role '{role}' to {username} in {account}")

def add_registry(account, registry, user, password, verify_ssl=True):
    resp = requests.post(
        f"{ANCHORE_URL}/registries",
        json={
            "registry": registry,
            "registry_user": user,
            "registry_pass": password,
            "registry_verify": verify_ssl,
        },
        auth=AUTH,
        headers={"x-anchore-account": account},
    )
    resp.raise_for_status()
    print(f"  Configured registry: {registry} for {account}")

def provision_team(account_name, users, registry=None):
    print(f"\nProvisioning account: {account_name}")
    create_account(account_name)
    for user in users:
        create_user(account_name, user["username"], user["password"])
        grant_role(
            user["username"],
            account_name,
            user.get("role", "read-only"),
        )
    if registry:
        add_registry(
            account_name,
            registry["host"],
            registry["user"],
            registry["password"],
        )
    print(f"\nDone. Account '{account_name}' is ready.\n")

if __name__ == "__main__":
    provision_team(
        account_name="mad-hatter-team",
        users=[
            {
                "username": "alice",
                "password": "changeme123",
                "role": "full-control",
            },
            {
                "username": "hatter",
                "password": "changeme456",
                "role": "read-only",
            },
        ],
        registry={
            "host": "registry.wonderland.example.com:5000",
            "user": "anchore-svc",
            "password": "s3cr3t",
        },
    )

This kind of script slots naturally into an infrastructure-as-code workflow — driven by a configuration file, triggered by a CI pipeline, or invoked as part of a larger onboarding automation. The operations are idempotent enough to be re-run safely with appropriate error handling, and the entire provisioning history becomes auditable through your version control system rather than buried in UI click trails.

End of the Looking-Glass Country

Alice begins Through the Looking-Glass as a confused visitor and ends it as a Queen presiding over the chessboard she once didn’t understand. The arc of this series is meant to be similar. We’ve worked through the structure of the API, the data it surfaces, the events it emits, the questions you can ask of it, and now the platform itself. What you do with all of that is finally up to you.

The thread running through all seven posts is the same: Anchore Enterprise exposes a comprehensive, well-structured API, and the container security teams that get the most out of it are the ones who treat it as a platform to build on rather than a UI to click through. The examples in this series are starting points. Your environment, your tooling, and your workflows will take them somewhere specific to you.

Thanks for reading. 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.

Comparing Vulnerabilities Across Image Versions via the Anchore Enterprise API

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.

Chasing Zero-Day Vulnerabilities via the Anchore Enterprise API

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-fix

The 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.

Custom Reporting and GraphQL via the Anchore Enterprise API

Through the Looking Glass: Humpty Dumpty

This is post 4 in a seven-part series on what the Anchore Enterprise API makes possible for container security teams. 

“When I use a word,” Humpty Dumpty said in rather a scornful tone, “it means just what I choose it to mean — neither more nor less.” Anchore Enterprise’s GraphQL subsystem takes the same approach to data: you define exactly what you want back, in exactly the shape you want it, without over-fetching or stringing together multiple REST calls to assemble the picture yourself.

The REST endpoints we’ve explored in earlier posts are ideal for targeted, per-image operations. GraphQL is the right tool when you need to ask broader questions across your container fleet — things like “which images have Critical vulnerabilities and which runtime containers are they running in?” or “what is the policy compliance posture across all of my registered tags?” These are questions that would require multiple REST calls, response aggregation, and client-side filtering. With GraphQL, they become a single query.

Pick Your Wall: The Reporting Endpoints

Humpty Dumpty had one wall; you get two. Anchore Enterprise exposes two GraphQL endpoints depending on the scope you need.

https://<anchore-host>/v2/reports/graphql

This endpoint is scoped to the authenticated user’s account. The x-anchore-account header is required:

curl -s -u _api_key:<your-api-key> \
  -X POST "https://wonderland.example.com/v2/reports/graphql" \
  -H "Content-Type: application/json" \
  -H "x-anchore-account: mad-hatter-team" \
  -d '{"query": "{ __typename }"}'
https://<anchore-host>/v2/reports/global/graphql

This endpoint requires administrator access and returns data across all accounts. No account header required:

curl -s -u _api_key:<your-api-key> \
  -X POST "https://wonderland.example.com/v2/reports/global/graphql" \
  -H "Content-Type: application/json" \
  -d '{"query": "{ __typename }"}'

Glory For You: Exploring the Schema

“There’s glory for you!” Humpty Dumpty told Alice. “I don’t know what you mean by ‘glory,’” Alice replied. “Of course you don’t — till I tell you.” Before writing queries, let GraphQL tell you. Introspection returns the full list of available query types, filter inputs, and response fields directly from your deployment:

curl -s -u _api_key:<your-api-key> \
  -X POST "https://wonderland.example.com/v2/reports/graphql" \
  -H "Content-Type: application/json" \
  -H "x-anchore-account: mad-hatter-team" \
  -d '{
    "query": "{ __schema { queryType { fields { name description args { name type { name kind ofType { name kind } } } } } } }"
  }' | jq .

You can also import the endpoint into a tool like Insomnia or GraphQL Playground — both support introspection natively and give you an interactive schema explorer and query builder. The full list of available query types is summarised below. All queries accept limit, nextToken, and a typed filter object.

QueryDescription
imagesByVulnerabilityUnique vulnerabilities and the images affected
tagsByVulnerabilityUnique vulnerabilities and affected tags (hierarchical)
artifactsByVulnerabilityUnique vulnerabilities and affected artifacts
runtimeInventoryImagesByVulnerabilityVulnerabilities in runtime inventory images
kubernetesRuntimeVulnerabilitiesByNamespaceVulnerabilities by Kubernetes namespace
vulnerabilitiesByKubernetesContainerVulnerabilities by Kubernetes container
vulnerabilitiesByEcsContainerVulnerabilities by ECS container
runtimeInventoryUnscannedImagesRuntime inventory images not yet analyzed
policyEvaluationsByTagPolicy evaluations for tags (hierarchical)
policyEvaluationsByRuntimeInventoryImagePolicy evaluations for runtime inventory images
imagesWithStigImages with STIG compliance information
runtimeImagesWithStigRuntime images with STIG compliance information
metricsAvailable metrics in the system
metricDataMetric data points, chronologically descending
scheduledQueriesConfigured scheduled queries
scheduledQueryExecutionsExecutions for a given scheduled query

Putting Humpty Back Together: Cross-Image Vulnerability Summaries

All the King’s horses and all the King’s men couldn’t put Humpty together again. A single GraphQL query can. Where REST would need a series of round trips — list images, fetch vulnerabilities per image, fetch tags per image, join client-side — imagesByVulnerability returns a list of unique vulnerabilities alongside every container image affected by each one, in one response.

Filtering is done via nested filter objects — severity filtering lives under vulnerability, with separate filter objects available for artifact, registry, repository, tag, and image. Note that severity values are GraphQL enum literals and are written without quotes.

{
  imagesByVulnerability(
    limit: 500
    filter: { vulnerability: { severity: Critical } }
  ) {
    pageInfo { nextToken count }
    results {
      vulnerabilityId
      cve
      imagesCount
      images {
        digest
        distro
        tags {
          registryName
          repositoryName
          tagName
          current
        }
        artifacts {
          artifactName
          artifactVersion
          artifactType
          severity
          fixedIn
          isKev
          epssScore
        }
      }
    }
  }
}

To filter on multiple severities, use severities instead of severity:

filter: { vulnerability: { severities: [Critical, High] } }

In Python, with pagination handled via pageInfo.nextToken:

import requests

ANCHORE_URL = "https://wonderland.example.com/v2/reports/graphql"
AUTH = ("_api_key", "<your-api-key>")
HEADERS = {"x-anchore-account": "mad-hatter-team"}

VULN_QUERY = """
query($nextToken: String) {
  imagesByVulnerability(
    limit: 500
    nextToken: $nextToken
    filter: { vulnerability: { severity: Critical } }
  ) {
    pageInfo { nextToken count }
    results {
      vulnerabilityId
      cve
      imagesCount
      images {
        digest
        distro
        tags {
          registryName
          repositoryName
          tagName
          current
        }
        artifacts {
          artifactName
          artifactVersion
          artifactType
          severity
          fixedIn
          isKev
          epssScore
        }
      }
    }
  }
}
"""

def get_images_by_vulnerability():
    results = []
    next_token = None
    while True:
        resp = requests.post(
            ANCHORE_URL,
            json={
                "query": VULN_QUERY,
                "variables": {"nextToken": next_token},
            },
            auth=AUTH,
            headers=HEADERS,
        )
        resp.raise_for_status()
        data = resp.json()["data"]["imagesByVulnerability"]
        results.extend(data.get("results", []))
        next_token = data.get("pageInfo", {}).get("nextToken")
        if not next_token:
            break
    return results

def main():
    findings = get_images_by_vulnerability()
    print(
        f"Critical vulnerabilities affecting images: "
        f"{len(findings)}\n"
    )
    for f in findings:
        cve = f.get("cve") or f.get("vulnerabilityId")
        print(f"{cve} — {f.get('imagesCount')} image(s)")
        for img in f.get("images", []):
            for tag in img.get("tags", []):
                if tag.get("current"):
                    print(
                        f"  {tag['registryName']}/"
                        f"{tag['repositoryName']}:{tag['tagName']}"
                    )
            for artifact in img.get("artifacts", []):
                fix = artifact.get("fixedIn")
                fix_str = f"fix: {fix}" if fix else "no fix"
                kev_str = " [KEV]" if artifact.get("isKev") else ""
                print(
                    f"    {artifact['artifactName']} "
                    f"{artifact['artifactVersion']} "
                    f"— {fix_str}{kev_str}"
                )

if __name__ == "__main__":
    main()

From the Wall: Policy Compliance Across the Fleet

What the cross-image vulnerability query did for CVE exposure, policyEvaluationsByTag does for compliance — give you the whole fleet’s view from one vantage point. It returns policy evaluations in a hierarchical view: registry → repository → tag → evaluations. Each tag may have multiple evaluation records; use the latest field to identify the most current result.

{
  policyEvaluationsByTag(limit: 1000) {
    pageInfo { nextToken count }
    results {
      registryName
      repositoriesCount
      tagsCount
      account
      repositories {
        repositoryName
        tagsCount
        tags {
          tagName
          imageDigest
          current
          evaluations {
            result
            reason
            lastEvaluatedAt
            latest
          }
        }
      }
    }
  }
}

In Python, flattening the hierarchy to produce a compliance summary:

import requests
from collections import Counter

ANCHORE_URL = "https://wonderland.example.com/v2/reports/graphql"
AUTH = ("_api_key", "<your-api-key>")
HEADERS = {"x-anchore-account": "mad-hatter-team"}

POLICY_QUERY = """
query($nextToken: String) {
  policyEvaluationsByTag(limit: 1000, nextToken: $nextToken) {
    pageInfo { nextToken count }
    results {
      registryName
      account
      repositories {
        repositoryName
        tags {
          tagName
          imageDigest
          current
          evaluations {
            result
            reason
            lastEvaluatedAt
            latest
          }
        }
      }
    }
  }
}
"""

def get_policy_evaluations():
    all_registries = []
    next_token = None
    while True:
        resp = requests.post(
            ANCHORE_URL,
            json={
                "query": POLICY_QUERY,
                "variables": {"nextToken": next_token},
            },            auth=AUTH,
            headers=HEADERS,
        )
        resp.raise_for_status()
        data = resp.json()["data"]["policyEvaluationsByTag"]
        all_registries.extend(data.get("results", []))
        next_token = data.get("pageInfo", {}).get("nextToken")
        if not next_token:
            break
    return all_registries
<br>def flatten_latest_evaluations(registries):
    flat = []
    for registry in registries:
        for repo in registry.get("repositories", []):
            for tag in repo.get("tags", []):
                for evaluation in tag.get("evaluations", []):
                    if evaluation.get("latest"):
                        flat.append({<br>                            "full_tag": (
                                f"{registry['registryName']}/"
                                f"{repo['repositoryName']}:"
                                f"{tag['tagName']}"                            ),
                            "image_digest": tag.get("imageDigest"),
                            "result": evaluation.get("result"),
                            "reason": evaluation.get("reason"),
                            "last_evaluated_at": evaluation.get(
                                "lastEvaluatedAt"
                            ),
                        })
    return flat

def main():
    registries = get_policy_evaluations()
    evaluations = flatten_latest_evaluations(registries)
    counts = Counter(e["result"] for e in evaluations)

    print("Fleet Policy Compliance Summary")
    print(f"  Total tags evaluated: {len(evaluations)}")
    print(f"  Pass: {counts.get('Pass', 0)}")
    print(f"  Fail: {counts.get('Fail', 0)}")
    print()

    failing = [e for e in evaluations if e["result"] == "Fail"]
    if failing:
        print("Failing tags:")
        for e in failing:
            print(
                f"  {e['full_tag']}  "
                f"(evaluated: {e['last_evaluated_at']})"
            )

if __name__ == "__main__":
    main()

For administrator access across all accounts, swap the endpoint and remove the account header:

ANCHORE_URL = "https://wonderland.example.com/v2/reports/global/graphql"
HEADERS = {}

Beyond the Wall: Runtime Inventory Queries

Sitting on the wall, Humpty Dumpty had a view of the static landscape. Runtime inventory queries let you see what’s actually moving on the other side. One of the more distinctive aspects of this GraphQL schema is its deep integration with runtime inventory. runtimeInventoryImagesByVulnerability combines vulnerability data with live Kubernetes and ECS inventory, answering not just “which container images are vulnerable?” but “which vulnerable images are currently running in my clusters?”

{
  runtimeInventoryImagesByVulnerability(
    limit: 500
    filter: { vulnerability: { severity: Critical } }
  ) {
    pageInfo { nextToken count }
    results {
      vulnerabilityId
      cve
      imagesCount
      images {
        digest
        tags {
          registryName
          repositoryName
          tagName
          current
        }
        artifacts {
          artifactName
          artifactVersion
          severity
          fixedIn
          isKev
        }
      }
    }
  }
}

Use introspection to explore kubernetesRuntimeVulnerabilitiesByNamespace and vulnerabilitiesByKubernetesContainer for even more granular Kubernetes-scoped views.

Up Next

The GraphQL reporting interface gives you a query language designed around the questions container security and platform teams actually ask — not individual image lookups, but fleet-wide views of vulnerability exposure, policy compliance, and runtime context. The typed filter inputs and paginated responses are consistent across all query types, so once you’re comfortable with the pattern, exploring the rest of the schema is straightforward.

Next in the series: Who Stole the Tarts? — Chasing the Cheshire Cat Through Zero-Day Vulnerabilities. When a new CVE drops, the question changes from “what does our fleet look like?” to “are we on fire?” — and you need an answer in minutes, not hours. We’ll use the /query/vulnerabilities and /query/images/by-package endpoints to rapidly assess blast radius the moment a vulnerability is disclosed.

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.

Event-Driven Workflows with Anchore Enterprise Notifications

Through the Looking Glass: A Mad Tea-Party

This is post 3 in a seven-part series on what the Anchore Enterprise API makes possible for container security teams. 

The fastest way to react to a new Critical CVE is to not have to ask. When a vulnerability is disclosed, a policy evaluation fails, or a container image finishes analysing, you want your tooling to respond on its own — the moment Anchore knows about it, not the next time your cron job wakes up. Anchore Enterprise’s webhook notifications make that loop fast and direct.

Anchore Events are the platform’s internal log of everything notable that happens — image analyses completing, policy evaluations changing, vulnerabilities being identified, system errors being logged — and webhook notifications are how you subscribe to the slice that matters to you. Events don’t arrive on your schedule — they arrive on theirs. The Mad Tea-Party in Wonderland is the canonical scene for this: Alice walks up to a table already in progress, where things happen when they happen and not before. Anchore’s notifications work the same way, and your job as the receiver is to be ready when the cup is passed.

Anchore Enterprise supports outbound webhooks that fire when key events occur. This makes it straightforward to build event-driven workflows that connect Anchore to the rest of your tooling — ticket systems, deployment pipelines, Slack channels, SIEMs, or anything else that accepts an HTTP request.

Setting the Table: Configuring a Webhook

Before any guest can sit down, the Hatter has to set out the cups. Webhooks in Anchore Enterprise are managed through the notifications API and work the same way: first register a webhook endpoint configuration (the cup), then attach a selector that defines which events should be delivered to it (who gets a seat at that end of the table).

Register your webhook endpoint configuration:

curl -s -u _api_key:<your-api-key> \
  -X POST "https://wonderland.example.com/v2/notifications/endpoints/webhook/configurations" \
  -H "Content-Type: application/json" \ 
  -d '{
    "name": "tea-party-receiver",
    "url": "https://hatter-table.wonderland.example.com/anchore/webhook",
    "verify_ssl": true
  }'

The response will include a uuid for the newly created configuration. Use that to attach a selector that maps policy evaluation events to this endpoint:

curl -s -u _api_key:<your-api-key> \
  -X POST "https://wonderland.example.com/v2/notifications/endpoints/webhook/configurations/<uuid>/selectors" \
  -H "Content-Type: application/json" \
  -d '{
    "scope": "account",
    "event": {
      "level": "*",
      "resource_type": "image_digest",
      "type": "user.image.policy_eval.*"
    }
  }'

The event filter uses a structured <category>.<subcategory>.<event> format and supports wildcards, so user.image.policy_eval.* captures all policy evaluation outcomes for images in your account. You can retrieve the full list of supported event types from the /v2/event_types endpoint on your deployment. The scope field controls breadth — account limits events to your own account, while global (admin only) captures events across the entire system.

Anchore will POST to your registered URL whenever a matching event fires. Before putting it into production, you can verify your endpoint is reachable using the built-in test endpoint:

curl -s -u _api_key:<your-api-key> \
  "https://wonderland.example.com/v2/notifications/endpoints/webhook/configurations/<uuid>/test"

What’s in the Cup: Understanding the Payload

Before you write logic against the events, it’s worth looking at what’s actually in the cup. When Anchore fires a webhook, it POSTs a JSON payload to your registered URL with the following structure:

{
  "id": "211c5fff5641456d935e60f905c46a66",
  "type": "user.image.analysis_update",
  "level": "info",
  "message": "Image analysis complete",
  "details": {},
  "timestamp": "2025-10-01T09:08:54.749806",
  "resource": {
    "account_name": "mad-hatter-team",
    "type": "image_digest",
    "id": "sha256:<digest>"
  },
  "source": {
    "request_id": null,
    "service_name": "catalog",
    "host_id": "anchore-enterprise-catalog-5654fb8d84-5ln8r",
    "base_url": "http://anchore-enterprise-catalog.anchore.svc.cluster.local:8082"
  }
}

The key fields for building a receiver are type (the event that fired, matching the pattern you set in your selector), level (info, warn, or error), and resource.id (the identifier for the affected resource — for image events, this will be the image digest). The details object carries event-specific additional data that varies by event type.

Is Your Watch Right? Testing Your Receiver

The Hatter’s watch was two days wrong, even with butter in the works. Before wiring real logic to the events Anchore sends, prove your receiver is reachable and the payloads look the way you expect.

For a one-off smoke test, point your webhook configuration at webhook.site — it gives you a disposable URL and a live view of every request that arrives, no code required. That’s enough to confirm the event is firing and to eyeball the payload shape.

When you need to run logic against the events — for example to see exactly what details contains across multiple event types, since its content varies and isn’t fully enumerated in the documentation — stand up a minimal local logger. Here’s a Flask service that accepts any POST and logs the full payload:

import json
import logging
from flask import Flask, request, jsonify

app = Flask(__name__)
logging.basicConfig(level=logging.INFO)

@app.route("/anchore/webhook", methods=["POST"])
def webhook():
    payload = request.get_json(force=True)
    logging.info(json.dumps(payload, indent=2))
    return jsonify({"status": "received"}), 200

if __name__ == "__main__":
    app.run(port=5000)

Point your webhook configuration at this endpoint, trigger a few events, and inspect what arrives before writing production logic against it.

Pouring the Tea: Building a Webhook Receiver in Python

Once you know what’s in the cup, doing something with it is straightforward. The example below routes events by type, extracts the image digest from resource.id, and queries the API for the full vulnerability list:

import logging
import requests
from flask import Flask, request, jsonify

>app = Flask(__name__)
logging.basicConfig(level=logging.INFO)

ANCHORE_URL = "https://wonderland.example.com/v2"
AUTH = ("_api_key", "<your-api-key>")

>def get_vulnerabilities(digest):
    resp = requests.get(
        f"{ANCHORE_URL}/images/{digest}/vuln/all",
        auth=AUTH,
    )
    resp.raise_for_status()
    return resp.json().get("vulnerabilities", [])

@app.route("/anchore/webhook", methods=["POST"])
def webhook():
    payload = request.get_json(force=True)
    event_type = payload.get("type", "")
    level = payload.get("level", "")
    resource = payload.get("resource", {})
    digest = resource.get("id")

    if not digest or resource.get("type") != "image_digest":
        logging.info(f"Ignoring non-image event: {event_type}")
        return jsonify({"status": "ignored"}), 200

    logging.info(f"Event: {event_type} | Level: {level} | Digest: {digest}")

    if "analysis_update" in event_type and level == "info":
        vulns = get_vulnerabilities(digest)
        critical = [v for v in vulns if v.get("severity") == "Critical"]
        if critical:
            logging.warning(
                f"{len(critical)} Critical vulnerabilities in {digest}"
            )
            # open a ticket, page on-call, block promotion, etc.
        else:
            logging.info(
                f"No Critical vulnerabilities in {digest} "
               "— eligible for promotion"
            )
            # trigger promotion pipeline, notify Slack, update CMDB, etc.

    return jsonify({"status": "received"}), 200

if __name__ == "__main__":
    app.run(port=5000)

The notification tells you something happened and which container image it happened to; the API call tells you what was found. The logic in the if critical branch is entirely yours — open a Jira ticket, post a formatted message to Slack, block a deployment pipeline, or trigger a rollback. Anchore fires the event and holds the data; your service decides what happens next.

“Move Down, Move Down!”: Transforming and Forwarding to a SIEM

Whenever the Hatter ran out of clean cups, everyone shifted one seat to the right and used the next person’s place. A natural extension of the webhook receiver follows the same idea: take the enriched data — the Anchore event combined with the vulnerability findings you’ve just fetched — and pass it along to a SIEM or other downstream system in a normalized format. This is the transform-and-forward pattern: receive the raw event, enrich it with context from the API, reshape it into the structure your tooling expects, and send it on.

The transformation step is where you decide what the downstream system needs to know. A SIEM doesn’t need the full Anchore vulnerability object for every finding — it typically needs a structured event with enough context to correlate, alert, and drive investigation. Here’s a function that builds that normalized event from the Anchore webhook payload and vulnerability data:

from collections import Counter
from datetime import datetime, timezone

def build_siem_event(payload, vulns):
    resource = payload.get("resource", {})
    severity_counts = Counter(
        v.get("severity", "Unknown") for v in vulns
    )
    critical_findings = [
        {
            "cve": v.get("vuln"),
            "severity": v.get("severity"),
            "package": v.get("package"),
            "package_version": v.get("package_version"),
            "fix_available": v.get("fix") not in (None, "None"),
            "fix_version": (
                v.get("fix") if v.get("fix") not in (None, "None") else None
           ),
        }
        for v in vulns if v.get("severity") == "Critical"
    ]
    return {
        "event_id": payload.get("id"),
        "event_type": payload.get("type"),
        "level": payload.get("level"),
        "timestamp": payload.get("timestamp"),
        "forwarded_at": datetime.now(timezone.utc).isoformat(),
        "image": {
            "digest": resource.get("id"),
           "account": resource.get("account_name"),
        },
        "vulnerability_summary": {
            "total": len(vulns),
            "critical": severity_counts.get("Critical", 0),
            "high": severity_counts.get("High", 0),
            "medium": severity_counts.get("Medium", 0),
            "low": severity_counts.get("Low", 0),
            "negligible": severity_counts.get("Negligible", 0),
        },
        "critical_findings": critical_findings,
        "source": payload.get("source", {}),
    }

With a normalized event in hand, forwarding it is a straightforward HTTP POST to whatever endpoint your tooling exposes. The example below targets an Elasticsearch index using the _doc endpoint — a common SIEM ingestion pattern — but the same approach applies to Microsoft Sentinel, Datadog, or any platform that accepts JSON over HTTP:

import os
import requests

ES_URL = "https://looking-glass-search.wonderland.example.com:9200"
ES_INDEX = "anchore-events"
ES_API_KEY = os.environ["ELASTIC_API_KEY"]

def forward_to_siem(event):
    resp = requests.post(
        f"{ES_URL}/{ES_INDEX}/_doc",
        json=event,
        headers={"Authorization": f"ApiKey {ES_API_KEY}"},
        timeout=5,
    )
    resp.raise_for_status()
    logging.info(f"Forwarded event {event['event_id']} to SIEM")

Bringing it all together, the updated webhook route chains the full pipeline — receive, enrich, transform, forward:

@app.route("/anchore/webhook", methods=["POST"])
def webhook():
    payload = request.get_json(force=True)
    resource = payload.get("resource", {})
    digest = resource.get("id")

    if not digest or resource.get("type") != "image_digest":
        logging.info(
            f"Ignoring non-image event: {payload.get('type')}"
        )
        return jsonify({"status": "ignored"}), 200

    # Enrich: fetch vulnerability data from Anchore
    vulns = get_vulnerabilities(digest)

    # Transform: build a normalized SIEM event
    event = build_siem_event(payload, vulns)

    # Forward: send to SIEM
    try:
        forward_to_siem(event)
    except Exception as e:
        logging.error(f"Failed to forward event to SIEM: {e}")

    return jsonify({"status": "received"}), 200

The try/except around the forwarding call is intentional — you always want to return 200 to Anchore regardless of what happens downstream. If your SIEM endpoint is temporarily unavailable, that shouldn’t cause Anchore to retry the notification indefinitely.

This pattern — receive, enrich, transform, forward — is the foundation for connecting Anchore to virtually any downstream system. Swap out forward_to_siem for a function that opens a Jira ticket, posts to a Slack channel, or writes to an S3 bucket, and the rest of the pipeline stays the same.

Up Next

Anchore’s notifications API turns a system you query into a system that talks back. Combined with the SBOM and vulnerability data from the previous post, you have everything you need to build container security workflows that are both deeply informed and fully automated.

Next in the series: Humpty Dumpty — Custom Reporting and GraphQL via the API. Where REST gives you what each endpoint chose to hand back, GraphQL lets you ask for exactly the shape of data you want — “when I use a word,” said Humpty Dumpty, “it means just what I choose it to mean.” We’ll work through Anchore’s embedded GraphQL subsystem and build custom reports against it.

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.

Working with SBOM Data via the Anchore Enterprise API

Through the Looking Glass: A Caucus-Race and a Long Tale

This is post 2 in a seven-part series on what the Anchore Enterprise API makes possible for container security teams. 

Who Are You? Why SBOMs Matter

The Caterpillar’s first question to Alice was “Who are you?” — and Alice, who had changed size three times that morning, struggled to answer. When a Critical CVE drops, that’s exactly the question a security team needs to answer about every container image in their fleet: who is in here, at what version, where. Without a reliable inventory, the answer comes from guessing, grepping, or manually inspecting images one at a time. With a complete Software Bill of Materials (SBOM), it becomes a query.

An SBOM is exactly what it sounds like — a complete, structured inventory of every component that makes up a piece of software. For container images, that means every OS package, every language-ecosystem library, every binary, every framework — regardless of how it got there or which layer of the image it lives in. It’s the answer to the Caterpillar’s question, for software.

Beyond vulnerability response, SBOMs underpin license compliance, supply chain attestation, regulatory requirements, and informed conversations with development teams about what they’re actually shipping. The value compounds quickly once you have SBOM data that’s accurate, current, and — critically — accessible programmatically.

Anchore Enterprise generates a detailed SBOM for every container image it analyzes, covering more than 35 package ecosystems. All of that data is available through the API, which means it can flow into whatever workflows, tooling, and processes your organization already relies on.

Retrieving SBOM Content

Like the Mouse’s Long Tale, the list of ecosystems Anchore understands runs on and on — but you query them all through the same shape of request. The /images/{imageDigest}/content endpoint returns the package inventory for an analyzed image, scoped to a single ecosystem at a time. Retrieving OS packages with curl looks like this:

curl -s -u _api_key:
   "https://wonderland.example.com/v2/images/sha256:/content/os" \
   | jq .

Substitute the content type for any of the ecosystems Anchore understands — OS packages, Java, Python, Go, npm, NuGet, Ruby gems, Rust crates, and many more, along with non-package content types like files (a complete file inventory, useful for secret detection and configuration auditing) and malware. A single image digest can yield package data spanning dozens of ecosystems, all queryable through the same endpoint pattern. For the canonical list of supported content types, see the Anchore content-types reference in the official documentation.

Exporting SBOMs in Standard Formats

Alice spent her time in Wonderland trying bottles labelled Drink Me and cakes labelled Eat Me — same Alice, different containers, different effects. Downstream consumers of SBOM data are similar: same image inventory, different format expected. If you need to share SBOM data with external tools, auditors, or downstream systems, Anchore can pour the same SBOM into whichever bottle the asker brought — three industry-standard formats, directly from the API.

SPDX JSON — widely used for license compliance and supply chain documentation:

curl -s -u _api_key:<your-api-key> \
  "https://wonderland.example.com/v2/images/sha256:<digest>/sboms/spdx-json" \
  | jq .

CycloneDX JSON — commonly used for vulnerability exchange and DevSecOps toolchains:

curl -s -u _api_key:<your-api-key> \
  "https://wonderland.example.com/v2/images/sha256:<digest>/sboms/cyclonedx-json" \
  | jq .

Native Anchore JSON — the full internal representation, useful when you want every detail Anchore captured:

curl -s -u _api_key:<your-api-key> \
  "https://wonderland.example.com/v2/images/sha256:<digest>/sboms/native-json" \
  | jq .

Having these available programmatically means generating and delivering a compliance artifact for any image in your fleet is a one-liner — no manual export step required.

License Data

The Queen’s gardeners spent Chapter 8 hastily painting white roses red because they’d planted the wrong colour and an audit was imminent. License compliance is the equivalent in container land — much easier to catch a copyleft component before it ships than to repaint the roses after the fact. Anchore surfaces per-package license information through a dedicated endpoint:

curl -s -u _api_key:<your-api-key> \
  "https://wonderland.example.com/v2/images/sha256:<digest>/content/licenses" \
  | jq .

The response includes each package alongside its identified license or licenses. With that data in hand you can build automated checks for licenses incompatible with your distribution model, flag images that include packages under copyleft licenses, or generate license manifests for your legal and compliance teams — all without manual inspection.

Vulnerability Data

The /images/{imageDigest}/vuln/{vuln_type} endpoint returns vulnerability findings for an analyzed image. To retrieve all vulnerabilities:

curl -s -u _api_key:<your-api-key> \
  "https://wonderland.example.com/v2/images/sha256:<digest>/vuln/all" \
  | jq .

You can narrow the scope by replacing all with os or non-os depending on what you’re interested in. The response includes CVE IDs, severity ratings, affected package names and versions, and fix information where available.

With vulnerability data accessible via the API you can do things the UI isn’t designed for: correlate findings across your entire image fleet, track which vulnerabilities have fixes available and which don’t, filter by severity thresholds, or extract the data into whatever system your security team uses to manage their work.

In Python, pulling Critical and High findings for an image looks like this:

import requests

ANCHORE_URL = "https://wonderland.example.com/v2"
AUTH = ("_api_key", "<your-api-key>")

def get_vulnerabilities(digest, min_severity=("Critical", "High")):
    resp = requests.get(
        f"{ANCHORE_URL}/images/{digest}/vuln/all",
        auth=AUTH,
    )
    resp.raise_for_status()
    vulns = resp.json().get("vulnerabilities", [])
    return [v for v in vulns if v.get("severity") in min_severity]

digest = "sha256:<digest>"
findings = get_vulnerabilities(digest)
for v in findings:
    print(
        f"{v['vuln']} | {v['severity']} | "
        f"{v['package']} {v['package_version']} | "
        f"fix: {v.get('fix', 'none')}"
    )

Policy Evaluation

Vulnerability data tells you what’s present in an image. Policy evaluation tells you whether that image meets your organization’s standards — three possible verdicts (go, warn, or stop), which in the Queen of Hearts’ more direct vocabulary translate to “carry on,” “be careful,” and “off with its head.” The /images/{imageDigest}/check endpoint returns the current policy evaluation result:

curl -s -u _api_key:<your-api-key> \
  "https://wonderland.example.com/v2/images/sha256:<digest>/check?tag=docker.io/wonderland/whiterabbit-api:latest" \
  | jq .

Note that tag is a required parameter — it determines which tag context to use for the policy evaluation. The response is a PolicyEvaluation object containing an evaluations array, each entry of which includes the final_action (go, warn, or stop), the overall status (pass or fail), and a details object with the full list of individual findings.

In Python, checking evaluation status and extracting stop-level findings looks like this:

import requests<br><br>ANCHORE_URL = "https://wonderland.example.com/v2"
AUTH = ("_api_key", "<your-api-key>")

def get_policy_evaluation(digest, tag):
    resp = requests.get(
       f"{ANCHORE_URL}/images/{digest}/check",
        params={"tag": tag, "detail": True},
        auth=AUTH,
    )
    resp.raise_for_status()
    return resp.json()

def parse_evaluation(result):
    tag = result.get("evaluated_tag")
    evaluations = result.get("evaluations", [])
    if not evaluations:
        return tag, None, []
    evaluation = evaluations[0]
    final_action = evaluation.get("final_action")
   findings = evaluation.get("details", {}).get("findings", [])
    stops = [f for f in findings if f.get("action") == "stop"]
    return tag, final_action, stops

digest = "sha256:<digest>"
tag = "docker.io/wonderland/whiterabbit-api:latest"
result = get_policy_evaluation(digest, tag)
tag, final_action, stops = parse_evaluation(result)

print(f"{tag}: {final_action}")
for f in stops:
    print(
        f"  STOP — gate: {f['gate']}, "
        f"trigger: {f['trigger_id']}, "
        f"message: {f['message']}"
    )

Putting It Together: A Caucus-Race Across the Fleet

Lewis Carroll never explains the rules of the Dodo’s Caucus-Race because there aren’t any — the runners just go in a circle until the Dodo decides they’re done, and at the end everyone gets a prize. The script below works on the same principle: it iterates over every image in your fleet, cross-references the package inventory against a watch list of internal libraries, and summarises the vulnerability posture and policy status for each image. One pass through the API, every image accounted for, no one left out.

import requests
from collections import defaultdict

ANCHORE_URL = "https://wonderland.example.com/v2"
AUTH = ("_api_key", "<your-api-key>")
TRACKED_PACKAGES = {"internal-auth-lib", "legacy-crypto-util"}

ECOSYSTEMS = [
    "os", "files", "java", "python", "go", "npm", "nuget", "gem",
    "rust-crate", "hex", "dart-pub", "php-composer", "php-pear", "php-pecl",
    "swift", "pod", "homebrew", "conan", "conda", "terraform",
    "linux-kernel", "linux-kernel-module", "github-action",
    "github-action-workflow", "wordpress-plugin", "lua-rocks",
    "graalvm-native-image", "erlang-otp", "hackage", "r-package",
    "opam", "swiplpack", "bitnami", "binary", "malware",
]

def get_images():
    resp = requests.get(f"{ANCHORE_URL}/images", auth=AUTH)
    resp.raise_for_status()
    return resp.json()

def get_content(digest, content_type):
    resp = requests.get(
        f"{ANCHORE_URL}/images/{digest}/content/{content_type}",
        auth=AUTH,
    )
    resp.raise_for_status()
    return resp.json().get("content", [])

def get_vuln_summary(digest):
    resp = requests.get(
        f"{ANCHORE_URL}/images/{digest}/vuln/all", auth=AUTH
    )
    resp.raise_for_status()
    vulns = resp.json().get("vulnerabilities", [])
    counts = defaultdict(int)
    for v in vulns:
        counts[v.get("severity", "Unknown")] += 1
    return counts

def get_policy_status(digest, tag):
   resp = requests.get(
        f"{ANCHORE_URL}/images/{digest}/check",
        params={"tag": tag},
        auth=AUTH,
    )
    resp.raise_for_status()
    evaluations = resp.json().get("evaluations", [])
    if evaluations:
        return evaluations[0].get("final_action", "unknown")
    return "unknown"

def analyze_image(image):
    digest = image["imageDigest"]
    tag = image["image_detail"][0].get("fulltag", digest[:20])
    flagged = []
    for content_type in ECOSYSTEMS:
        try:
            for pkg in get_content(digest, content_type):
                if pkg.get("package") in TRACKED_PACKAGES:
                    flagged.append(
                        f"{pkg['package']} ({pkg.get('version')})"
                    )
        except requests.HTTPError:
            pass
    vuln_counts = get_vuln_summary(digest)
    policy_status = get_policy_status(digest, tag)
    return tag, flagged, vuln_counts, policy_status

def main():
    for image in get_images():
        tag, flagged, vulns, policy = analyze_image(image)
        print(f"\n{tag}")
        print(f"  Policy:     {policy}")
        print(
            f"  Critical:   {vulns.get('Critical', 0)}  "
            f"High: {vulns.get('High', 0)}  "
            f"Medium: {vulns.get('Medium', 0)}"
        )
        print(f"  Tracked:    {', '.join(flagged) if flagged else '—'}")

if __name__ == "__main__":
    main()

This is the kind of fleet-wide visibility that benefits teams managing container images at scale — knowing not just what’s in each image, but how it’s affected by current vulnerabilities and whether it meets policy, all from a single pass through the API. No winners, no losers, every image accounted for.

Up Next

The Hatter’s table is already set. Next in the series: A Mad Tea-Party — Event-Driven Workflows with Anchore Notifications. We’ll shift from querying SBOM and vulnerability data to reacting to it — configuring webhooks to fire on security events, building receivers that connect Anchore’s event stream to Slack, Jira, or your own internal tooling, and closing the loop between detection and action.

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.

An Introduction to the Anchore Enterprise API

Through the Looking Glass: Down the Rabbit Hole

The UI in Anchore Enterprise gives you a fast, intuitive way to explore scan results, review vulnerabilities, manage policies, and understand the security posture of your container fleet. Think of it as the polished side of the looking glass. But for teams that want to go further — building custom workflows, feeding scan data into other systems, or reacting to security events automatically — the more interesting world is on the other side. That’s where the Anchore Enterprise API comes in.

The API gives you programmatic access to everything the platform knows about your container images: full software bills of materials, vulnerability findings, policy evaluations, and a real-time event stream. That’s the door to integrations and automations uniquely tailored to how your organization works — blocking a deployment the moment a Critical CVE lands in a base image, opening a Jira ticket against the team that owns each affected service, or pinging Slack the instant a fix becomes available for a vulnerability you’ve been tracking.

This post kicks off a seven-part series on what the Anchore Enterprise API makes possible for container security teams. By the end you should be able to look at almost any container security workflow in your organization and see how the API can help you build it.

The Road Ahead

Here’s where the series goes from here. Each post borrows its subtitle from a chapter of Alice in Wonderland:

  • A Caucus-Race and a Long Tale — Working with SBOM Data: Retrieving full package inventories across every ecosystem Anchore understands, and building custom cross-image reports with Python.
  • A Mad Tea-Party — Event-Driven Workflows with Anchore Notifications: Configuring webhooks to fire on security events and building receivers that connect Anchore to the rest of your toolchain.
  • Humpty Dumpty — Custom Reporting and GraphQL: Going beyond REST with Anchore’s embedded GraphQL subsystem to query exactly the data you need in exactly the shape you want.
  • Who Stole the Tarts? — Chasing the Cheshire Cat Through Zero-Day Vulnerabilities: Using the /query/vulnerabilities and /query/images/by-package endpoints to rapidly assess blast radius when a new CVE drops.
  • Tweedledee and Tweedledum — Comparing Vulnerabilities Across Image Versions: Diffing vulnerability findings between image versions to understand exactly what was fixed — and what wasn’t.
  • Queen Alice — Automating Administrative Tasks: Scripting user creation, account management, and permission grants to bring the same automation mindset to platform operations.

Each post stands on its own, so feel free to jump to whichever topic is most relevant to your work. If you’re new to the Anchore API entirely, reading in order will give you a solid foundation before the later posts get into more advanced territory. With the map in hand, let’s get started.

Finding Your Way Around

Before writing a single line of code, it helps to know what’s available. Wonderland is famously short on signposts; Anchore Enterprise comes with one built in. Every deployment serves its own OpenAPI schema at:

https://wonderland.example.com/v2/openapi.json

This machine-readable schema is the authoritative reference for every endpoint, request body, and response shape in your specific deployment. You can import it directly into tools like Postman or Insomnia for interactive exploration, or use it to generate a typed client library in your language of choice. It’s also a useful sanity check — if you’re ever unsure whether a field name or path is correct, the schema is the source of truth.

The official Anchore documentation covers the API in depth alongside the rest of the platform, and is a great companion to the hands-on examples in this series.

Authenticating with the API

Alice’s first obstacle in Wonderland was a locked door without a key. Yours is similar. Anchore Enterprise supports both standard username/password authentication and API key authentication. API keys are the recommended approach for any programmatic or production use — they can be rotated and revoked independently of user credentials, making them safer to embed in scripts and automation.

Generating an API Key

API keys can be generated through the UI or via the API. The Anchore documentation covers the UI workflow. To generate one via the API, POST to the API key management endpoint for your account and username:

curl -s -u alice:password \
  -X POST "https://wonderland.example.com/v2/accounts/mad-hatter-team/users/alice/api-keys" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "cheshire-cat",
    "description": "Pipeline automation key"
  }'

Important: API key credentials cannot be used to generate another API key. You must authenticate with your username and password when creating, listing, or deleting API keys.

Keys can also be revoked without deleting them using the PATCH endpoint, which is useful if you suspect a key has been compromised but want to preserve the audit trail:

curl -s -u alice:password \
  -X PATCH "https://wonderland.example.com/v2/accounts/mad-hatter-team/users/alice/api-keys/cheshire-cat" \
  -H "Content-Type: application/json" \
  -d '{"status": "revoked"}'

API Key Limitations

API keys inherit the permissions and roles of the user they were generated for, but there are two categories of operations that cannot be performed using an API key regardless of which user generated it:

  • User and credential management — creating, editing, or removing users and their credentials must use username/password authentication.
  • API key management — creating, editing, or revoking API keys (including the call to generate a new key shown above) must also use username/password authentication.

For any script or integration that needs to perform these operations, use a dedicated service account with username/password credentials scoped appropriately, and keep those credentials out of your codebase using environment variables or a secrets manager.

Using an API Key

Once you have a key, pass it using standard HTTP basic authentication with the literal string _api_key as the username and your token as the password:

curl -s -u _api_key:<your-api-key> \
  "https://wonderland.example.com/v2/images"

In Python, the same pattern applies:

import requests

AUTH = ("_api_key", "<your-api-key>")

resp = requests.get("https://wonderland.example.com/v2/images", auth=AUTH)

The API as a Platform

What makes the Anchore Enterprise API genuinely powerful isn’t any single endpoint — it’s the fact that it gives you a coherent, queryable representation of your entire container security posture that you can build against. Once you’ve stepped through, there are more rooms here than this series will cover. Here’s a taste of what’s behind the other doors.

Reason about a release, not a pile of images. If you ship a product made up of multiple containers, this is how you answer “what is the security posture of version 2.4 of our platform?” as a single question. The applications API lets you group images into versioned application definitions and retrieve a combined SBOM and unified vulnerability view across every artifact in a given version.

Tell which vulnerable images are actually running in production. It’s the difference between “is this image vulnerable?” and “is this vulnerable image running in production right now?” Anchore Enterprise ingests runtime inventory from Kubernetes and Amazon ECS, giving you API access to a live picture of which container images are deployed in which namespaces and pods across your entire infrastructure — and a way to correlate that with vulnerability data.

Extend the same security workflows to source code. Use one set of policies, queries, and triage workflows across both images and the repositories they’re built from — no new toolchain, no separate concept model. The sources API brings the same SBOM and vulnerability analysis Anchore applies to container images to your source code repositories, with support for Syft-generated SBOMs as an import path.

Get vulnerability findings without leaving an analysis record behind. Ideal for lightweight integrations, throwaway checks, and pre-merge gates where a persistent analysis would be more noise than signal. The /vulnerability-scan endpoint accepts an SBOM and returns vulnerability findings immediately, with nothing stored in the system.

Deliver compliance artifacts on demand, in the format the asker needs. When a customer, auditor, or downstream system asks for a machine-readable SBOM or VEX document, you can generate and deliver it programmatically. Vulnerability findings, SBOMs, and VEX documents all export in industry-standard formats — CycloneDX JSON and XML, SPDX JSON, and OpenVEX — directly from the API.

Push triage decisions and false-positive fixes back into Anchore automatically. When your team accepts a risk, records a remediation, or determines a finding is a CPE-collision false positive, those decisions belong back in Anchore — not stuck in your ticketing system. The API lets you attach annotations to specific vulnerabilities on an image and submit CPE corrections, so the round trip closes without manual UI work.

Whether you’re a security engineer building executive dashboards, a platform team automating promotion gates, or a developer integrating vulnerability data directly into your ticketing system, the API meets you where you are.

Up Next

Next in the series: A Caucus-Race and a Long Tale — Working with SBOM Data. The cleanest way into the API for most teams is the thing Anchore does best — giving you a complete, queryable view of every package across every image you’ve scanned. We’ll walk through retrieving SBOMs, querying across ecosystems, and stitching the results into custom cross-image reports with a small amount of Python.

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 images, request a demo and we’ll walk you through it.