AID-M-001.002: AI System Dependency Mapping

Version 1.20261015 · Actionable sub-technique

Systematically identify and document every component and service an AI system depends on, including software libraries, external data and APIs, internal models, serving runtimes, and the exact model-interface bundle used to turn messages into tokens and raw generations into structured fields. Reconcile the approved release composition with every deployed replica so hidden dependency or interface drift remains visible.

Pillars: infra, app

Lifecycle: building, validation, operation

Scope boundary

Owns direct and transitive dependency relationships, external service and data dependencies, trust-boundary interfaces, SBOM composition, the release-bound model-interface tuple (model, tokenizer, special tokens, chat template, generation configuration, tool/reasoning parser configuration, and runtime selectors), and deployed release-composition reconciliation for agent and non-agent services. It reports composition facts and drift but does not admit runtime configuration or decide release promotion.

AID-M-001.001: AID-M-001.002 maps how components and services depend on one another; AID-M-001.001 records the authoritative identity and attributes of each asset. Dependency completeness and asset-inventory completeness are separate claims and require different populations and evidence.

AID-H-021.002: AID-M-001.002 inventories the approved model-interface tuple and reconciles the effective composition of deployed replicas; AID-H-021.002 verifies and admits signed startup or runtime control artifacts for an agent. For an agent deployment, the same configuration bytes may support both controls, but composition reconciliation must not be scored again as runtime artifact admission.

Implementation guidance

AID-M-001.002-G001

Pin every direct and transitive software dependency to an immutable reviewed version and verify that the resolved build population exactly matches the lock state.
Create one authoritative dependency lock per build target

Commit ecosystem-native lockfiles generated by a pinned resolver, prohibit mutable ranges in release inputs, and record registry, package, version, platform selector, integrity digest, and resolver build. For container bases, actions, plugins, and downloaded binaries, pin an immutable content digest rather than a tag. Generate locks only in a reviewed update workflow; production builds run in frozen mode and cannot rewrite them.

Reconcile the complete installed population

Export the resolved dependency inventory from every build target and compare package identity, version, digest, and platform with the signed lock-derived expectation. Exercise transitive changes, alternate registries, optional/platform dependencies, duplicate packages, resolver-version drift, and missing hashes. Preserve signed lock and resolver digests, expected and observed population roots, mismatch records, and an independent frozen-resolution replay receipt.

AID-M-001.002-G002

Document every internal service, external service, model-provider API, data feed, and runtime dependency with an accountable owner and a deployable interface and trust-boundary record.
Maintain the service dependency manifest

For each release, record the consuming component, provider identity and owner, environment, protocol, authenticated endpoint class, data classification, required scopes, timeout and failure behavior, region, version or compatibility contract, and whether the dependency can cause an external side effect. Derive entries from infrastructure, gateway, service-mesh, workflow, and application configuration; do not rely on an architecture diagram alone.

Compare declared and effective dependencies

A separately credentialed inventory job reads deployed routes, service bindings, DNS or mesh policy, provider configuration, and workload egress observations, then reconciles them with the signed manifest. Unknown, undeclared, ownerless, or stale dependencies are findings and prevent a complete claim. Retain manifest and policy digests, declared/effective population roots, endpoint identities, source revisions, and the signed reconciliation result without retaining secrets.

AID-M-001.002-G003

Generate a validated dependency SBOM for every releasable software artifact and bind it to the exact build inputs and output digest.
Produce a dependency-focused SBOM

Generate CycloneDX or SPDX from the signed lockfiles and the final built artifact with a pinned tool. Include direct and transitive packages, package URLs, versions, hashes, licenses, dependency relationships, source lock digests, builder identity, source revision, and output artifact digest. Consume the AI/ML asset inventory from AID-M-003.001 when available, but do not treat this dependency SBOM as a substitute for an AI/ML-BOM.

Validate and independently reproduce it

Schema-validate the document, reject duplicate or unidentifiable components, reconcile its dependency graph with both lock-derived and artifact-observed inventories, and sign the canonical bytes. A separate verifier regenerates the SBOM from the same immutable inputs and compares canonical component, relationship, and artifact-binding digests. Preserve both SBOMs, tool and policy versions, validation results, population counts, signatures, and replay receipt.

AID-M-001.002-G004

Maintain a version-controlled dependency map, bind the complete model-interface tuple into the release composition, and independently read back every deployed replica.
Scope and ownership

This step closes the preceding dependency inventories into one release-composition and deployed-readback result. The build identity binds the exact software lock, dependency SBOM, service dependency manifest, system topology, model artifacts, runtime, model-interface tuple, prompts, routing, tools, guardrails, adapters, and quantization state. The interface tuple includes the tokenizer, special-token map, chat template, generation configuration, tool parser, reasoning parser, and explicit container command plus arguments. Require a nonempty Kubernetes command so the approved vector replaces, rather than silently inherits, the image entrypoint. A separately credentialed deployment verifier then reads the complete live replica population back. This step verifies dependency/composition completeness and equality with the deployed candidate; parser behavior, security regression, vulnerability remediation, and runtime behavior remain separate controls.

Step 1: Sign the policy and exact release inputs before parsing
# File: release/release-policy.json
{
  "schema_version": "aidefend.release_composition_policy.v1",
  "policy_version": "ai-release-policy-12",
  "required_components": [
    "software_lock",
    "dependency_sbom",
    "service_dependencies",
    "system_dependency_map",
    "base_model_weights",
    "tokenizer",
    "special_tokens",
    "chat_template",
    "generation_config",
    "tool_parser_config",
    "reasoning_parser_config",
    "system_policy_prompt",
    "routing_fallback",
    "tool_catalog",
    "guardrail_config"
  ],
  "conditional_components": ["adapters", "quantization"],
  "allowed_applicability": ["APPLICABLE", "NOT_APPLICABLE"],
  "max_readback_age_seconds": 3600
}
# File: release/release-inputs.json
{
  "schema_version": "aidefend.release_inputs.v2",
  "release_id": "fraud-agent-2026-07-16.1",
  "policy_version": "ai-release-policy-12",
  "policy_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "target": {
    "environment": "production",
    "namespace": "fraud-ai",
    "deployment": "fraud-agent",
    "container": "server"
  },
  "runtime": {
    "container_image_digest": "registry.example.com/fraud-agent@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "inference_runtime_name": "vllm",
    "inference_runtime_version": "0.10.1",
    "launch_argv": ["vllm", "serve", "/opt/model", "--chat-template", "/etc/aidefend/chat-template.jinja", "--tool-call-parser", "hermes", "--reasoning-parser", "deepseek_r1"]
  },
  "components": {
    "software_lock": {"version": "git:8a4c2d1", "path": "requirements.txt", "runtime_path": "/opt/aidefend/release/requirements.txt"},
    "dependency_sbom": {"version": "cyclonedx-1.7", "path": "evidence/sbom.cdx.json", "runtime_path": "/opt/aidefend/release/sbom.cdx.json"},
    "service_dependencies": {"version": "git:8a4c2d1", "path": "config/service-dependencies.json", "runtime_path": "/opt/aidefend/release/service-dependencies.json"},
    "system_dependency_map": {"version": "git:8a4c2d1", "path": "config/system-dependency-map.json", "runtime_path": "/opt/aidefend/release/system-dependency-map.json"},
    "base_model_weights": {"version": "fraud-base-7", "path": "dist/model.safetensors", "runtime_path": "/opt/model/model.safetensors"},
    "tokenizer": {"version": "fraud-tokenizer-7", "path": "dist/tokenizer.json", "runtime_path": "/opt/model/tokenizer.json"},
    "special_tokens": {"version": "fraud-tokenizer-7", "path": "dist/special_tokens_map.json", "runtime_path": "/opt/model/special_tokens_map.json"},
    "chat_template": {"version": "chat-template-4", "path": "config/chat-template.jinja", "runtime_path": "/etc/aidefend/chat-template.jinja"},
    "generation_config": {"version": "generation-6", "path": "config/generation-config.json", "runtime_path": "/etc/aidefend/generation-config.json"},
    "tool_parser_config": {"version": "tool-parser-3", "path": "config/tool-parser.json", "runtime_path": "/etc/aidefend/tool-parser.json"},
    "reasoning_parser_config": {"version": "reasoning-parser-2", "path": "config/reasoning-parser.json", "runtime_path": "/etc/aidefend/reasoning-parser.json"},
    "system_policy_prompt": {"version": "system-policy-19", "path": "config/system-prompt.txt", "runtime_path": "/etc/aidefend/system-prompt.txt"},
    "routing_fallback": {"version": "routing-8", "path": "config/routing.json", "runtime_path": "/etc/aidefend/routing.json"},
    "tool_catalog": {"version": "tools-11", "path": "config/tools.json", "runtime_path": "/etc/aidefend/tools.json"},
    "guardrail_config": {"version": "guardrails-23", "path": "config/guardrails.json", "runtime_path": "/etc/aidefend/guardrails.json"}
  },
  "conditional": {
    "adapters": {
      "applicability": "APPLICABLE",
      "not_applicable_reason": null,
      "approval_reference": "REL-4821",
      "items": [
        {"name": "fraud-lora", "version": "3", "path": "dist/fraud-lora.safetensors", "runtime_path": "/opt/model/fraud-lora.safetensors"}
      ]
    },
    "quantization": {
      "applicability": "NOT_APPLICABLE",
      "not_applicable_reason": "Release uses unquantized weights",
      "approval_reference": "REL-4821",
      "items": []
    }
  }
}
cosign sign-blob --yes --key env://RELEASE_POLICY_SIGNING_KEY --bundle release/release-policy.sig release/release-policy.json
cosign sign-blob --yes --key env://RELEASE_INPUT_SIGNING_KEY --bundle release/release-inputs.sig release/release-inputs.json

The release-input signer is the composition approver. It must be independent of the workload and must approve each NOT_APPLICABLE decision. Detached signatures stay outside the signed JSON.

Step 2: Verify, normalize, and sign the release bundle
from __future__ import annotations

import math as _aidefend_math
import os as _aidefend_os

# Admission injects these required values from one signature-verified, versioned runtime profile.
def _aidefend_required(name):
    value = _aidefend_os.environ.get(name)
    if value is None or not value.strip():
        raise RuntimeError(f"required runtime-profile field is absent: {name}")
    return value.strip()


def _aidefend_positive_float(name):
    try:
        value = float(_aidefend_required(name))
    except ValueError as error:
        raise RuntimeError(f"runtime-profile field is not numeric: {name}") from error
    if not _aidefend_math.isfinite(value) or value <= 0:
        raise RuntimeError(f"runtime-profile field must be finite and positive: {name}")
    return value


def _aidefend_positive_int(name):
    try:
        value = int(_aidefend_required(name))
    except ValueError as error:
        raise RuntimeError(f"runtime-profile field is not an integer: {name}") from error
    if value < 1:
        raise RuntimeError(f"runtime-profile field must be positive: {name}")
    return value


def _aidefend_nonnegative_int(name):
    try:
        value = int(_aidefend_required(name))
    except ValueError as error:
        raise RuntimeError(f"runtime-profile field is not an integer: {name}") from error
    if value < 0:
        raise RuntimeError(f"runtime-profile field cannot be negative: {name}")
    return value


def _aidefend_fraction(name):
    value = _aidefend_positive_float(name)
    if value > 1:
        raise RuntimeError(f"runtime-profile field must be in (0, 1]: {name}")
    return value


RUNTIME_PROFILE_VERSION = _aidefend_required("AIDEFEND_RUNTIME_PROFILE_VERSION")
RUNTIME_PROFILE_SHA256 = _aidefend_required("AIDEFEND_RUNTIME_PROFILE_SHA256").lower()
if (len(RUNTIME_PROFILE_SHA256) != 64
        or set(RUNTIME_PROFILE_SHA256) - set("0123456789abcdef")):
    raise RuntimeError("runtime-profile digest must be lowercase SHA-256")
COMPOSITION_COMMAND_TIMEOUT_SECONDS = _aidefend_positive_float("M001002_COMPOSITION_COMMAND_TIMEOUT_SECONDS")

# File: release/build_release_bundle.py

import hashlib
import json
import os
import re
import stat
import subprocess
import tempfile
from datetime import datetime, timezone
from pathlib import Path

INPUT = Path("release/release-inputs.json")
INPUT_SIG = Path("release/release-inputs.sig")
INPUT_KEY = Path("keys/release-inputs.pub")
POLICY = Path("release/release-policy.json")
POLICY_SIG = Path("release/release-policy.sig")
POLICY_KEY = Path("keys/release-policy.pub")
OUTPUT = Path("evidence/deployed-release-bundle.json")
IMAGE = re.compile(r"^.+@sha256:[0-9a-f]{64}$")
MANDATORY_COMPONENTS = {
    "software_lock", "dependency_sbom", "service_dependencies",
    "system_dependency_map", "base_model_weights", "tokenizer",
    "special_tokens", "chat_template", "generation_config",
    "tool_parser_config", "reasoning_parser_config",
    "system_policy_prompt", "routing_fallback",
    "tool_catalog", "guardrail_config",
}
MANDATORY_CONDITIONAL = {"adapters", "quantization"}


def strict_json_bytes(raw: bytes, label: str) -> object:
    def unique_object(pairs):
        value = {}
        for key, item in pairs:
            if key in value:
                raise ValueError(f"duplicate JSON key in {label}: {key}")
            value[key] = item
        return value

    def reject_constant(value):
        raise ValueError(f"non-finite JSON constant in {label}: {value}")

    try:
        return json.loads(
            raw.decode("utf-8", errors="strict"),
            object_pairs_hook=unique_object,
            parse_constant=reject_constant,
        )
    except (UnicodeDecodeError, json.JSONDecodeError, ValueError) as error:
        raise ValueError(f"{label} is not strict UTF-8 JSON") from error


def read_regular_bytes(path: Path) -> bytes:
    flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
    descriptor = os.open(path, flags)
    with os.fdopen(descriptor, "rb") as handle:
        status = os.fstat(handle.fileno())
        if not stat.S_ISREG(status.st_mode) or status.st_size == 0:
            raise ValueError(f"missing, empty, or unsafe file: {path}")
        return handle.read()


def write_private_snapshot(path: Path, raw: bytes) -> None:
    flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
    descriptor = os.open(path, flags, 0o400)
    with os.fdopen(descriptor, "wb") as handle:
        handle.write(raw)
        handle.flush()
        os.fsync(handle.fileno())


def capture_verified_signature(
    payload_path: Path,
    signature_path: Path,
    key_path: Path,
    timeout_seconds: float,
) -> tuple[bytes, bytes]:
    payload = read_regular_bytes(payload_path)
    signature = read_regular_bytes(signature_path)
    with tempfile.TemporaryDirectory(prefix="aidefend-release-input-") as directory:
        snapshot_root = Path(directory)
        snapshot_root.chmod(0o700)
        payload_snapshot = snapshot_root / "payload"
        signature_snapshot = snapshot_root / "payload.sig"
        write_private_snapshot(payload_snapshot, payload)
        write_private_snapshot(signature_snapshot, signature)
        subprocess.run(
            [
                "cosign", "verify-blob", "--key", str(key_path),
                "--bundle", str(signature_snapshot), str(payload_snapshot),
            ],
            check=True, capture_output=True, text=True, timeout=timeout_seconds,
        )
        if read_regular_bytes(payload_snapshot) != payload or read_regular_bytes(signature_snapshot) != signature:
            raise ValueError("private signed-input snapshot changed during verification")
    return payload, signature

def digest(path: Path) -> str:
    if not path.is_file() or path.is_symlink() or path.stat().st_size == 0:
        raise ValueError(f"missing, empty, or unsafe release file: {path}")
    value = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            value.update(chunk)
    return value.hexdigest()


def validate_cyclonedx(path: Path) -> None:
    raw = read_regular_bytes(path)
    with tempfile.TemporaryDirectory(prefix="aidefend-cyclonedx-") as directory:
        snapshot = Path(directory) / "sbom.cdx.json"
        write_private_snapshot(snapshot, raw)
        subprocess.run(
            [
                "cyclonedx-cli", "validate",
                "--input-file", str(snapshot),
                "--input-format", "json",
                "--input-version", "v1_7",
                "--fail-on-errors",
            ],
            check=True, capture_output=True, text=True,
            timeout=COMPOSITION_COMMAND_TIMEOUT_SECONDS,
        )
        if read_regular_bytes(snapshot) != raw:
            raise ValueError("CycloneDX snapshot changed during validation")
    document = strict_json_bytes(raw, f"CycloneDX document {path}")
    if (
        not isinstance(document, dict)
        or document.get("bomFormat") != "CycloneDX"
        or document.get("specVersion") != "1.7"
    ):
        raise ValueError(f"CycloneDX version differs: {path}")

def bind_file(item: dict) -> dict:
    if set(item) != {"version", "path", "runtime_path"}:
        raise ValueError("component schema differs")
    if not str(item["version"]).strip() or not str(item["runtime_path"]).startswith("/"):
        raise ValueError("version and absolute runtime_path are required")
    return {
        "version": item["version"],
        "runtime_path": item["runtime_path"],
        "sha256": digest(Path(item["path"])),
    }


def bind_conditional(name: str, value: dict, allowed: set[str]) -> dict:
    required = {"applicability", "not_applicable_reason", "approval_reference", "items"}
    if set(value) != required or value["applicability"] not in allowed:
        raise ValueError(f"{name} applicability schema differs")
    if not str(value["approval_reference"]).strip():
        raise ValueError(f"{name} approval reference is required")
    items = value["items"]
    if not isinstance(items, list):
        raise ValueError(f"{name} items must be an array")
    if value["applicability"] == "NOT_APPLICABLE":
        if items or not str(value["not_applicable_reason"] or "").strip():
            raise ValueError(f"{name} NOT_APPLICABLE requires no items and a reason")
        return {
            "applicability": "NOT_APPLICABLE",
            "not_applicable_reason": value["not_applicable_reason"],
            "approval_reference": value["approval_reference"],
            "items": [],
        }
    if value["not_applicable_reason"] is not None or not items:
        raise ValueError(f"{name} APPLICABLE requires a non-empty population")
    names = [str(item.get("name", "")).strip() for item in items]
    if any(not item for item in names) or len(names) != len(set(names)):
        raise ValueError(f"{name} item names must be non-empty and unique")
    bound = []
    for item in items:
        if set(item) != {"name", "version", "path", "runtime_path"}:
            raise ValueError(f"{name} item schema differs")
        bound.append({
            "name": item["name"],
            **bind_file({key: item[key] for key in ("version", "path", "runtime_path")}),
        })
    return {
        "applicability": "APPLICABLE",
        "not_applicable_reason": None,
        "approval_reference": value["approval_reference"],
        "items": sorted(bound, key=lambda item: item["name"]),
    }


policy_raw, policy_signature_raw = capture_verified_signature(
    POLICY, POLICY_SIG, POLICY_KEY, COMPOSITION_COMMAND_TIMEOUT_SECONDS,
)
input_raw, input_signature_raw = capture_verified_signature(
    INPUT, INPUT_SIG, INPUT_KEY, COMPOSITION_COMMAND_TIMEOUT_SECONDS,
)
policy = strict_json_bytes(policy_raw, "signed release-composition policy")
source = strict_json_bytes(input_raw, "signed release inputs")
policy_keys = {
    "schema_version", "policy_version", "required_components",
    "conditional_components", "allowed_applicability",
    "max_readback_age_seconds",
}
source_keys = {
    "schema_version", "release_id", "policy_version", "policy_sha256",
    "target", "runtime", "components", "conditional",
}
if not isinstance(policy, dict) or set(policy) != policy_keys or policy.get("schema_version") != "aidefend.release_composition_policy.v1":
    raise ValueError("policy schema differs")
if not isinstance(source, dict) or set(source) != source_keys or source.get("schema_version") != "aidefend.release_inputs.v2":
    raise ValueError("release input schema differs")
policy_sha256 = hashlib.sha256(policy_raw).hexdigest()
input_sha256 = hashlib.sha256(input_raw).hexdigest()
policy_signature_sha256 = hashlib.sha256(policy_signature_raw).hexdigest()
input_signature_sha256 = hashlib.sha256(input_signature_raw).hexdigest()
if source.get("policy_version") != policy.get("policy_version"):
    raise ValueError("policy version differs")
if source.get("policy_sha256") != policy_sha256:
    raise ValueError("release input is not bound to the verified policy")
required_components = policy.get("required_components")
conditional_components = policy.get("conditional_components")
allowed = set(policy.get("allowed_applicability", []))
if not isinstance(required_components, list) or not required_components or len(required_components) != len(set(required_components)):
    raise ValueError("required component policy is empty or duplicated")
if not isinstance(conditional_components, list) or not conditional_components or len(conditional_components) != len(set(conditional_components)):
    raise ValueError("conditional component policy is empty or duplicated")
if set(required_components) != MANDATORY_COMPONENTS:
    raise ValueError("release policy omits or adds a mandatory component")
if set(conditional_components) != MANDATORY_CONDITIONAL:
    raise ValueError("release policy conditional population differs")
if allowed != {"APPLICABLE", "NOT_APPLICABLE"}:
    raise ValueError("applicability policy differs")
if set(source.get("components", {})) != set(required_components):
    raise ValueError("release component population differs from policy")
if set(source.get("conditional", {})) != set(conditional_components):
    raise ValueError("conditional component population differs from policy")
validate_cyclonedx(Path(source["components"]["dependency_sbom"]["path"]))
if set(source.get("target", {})) != {"environment", "namespace", "deployment", "container"}:
    raise ValueError("deployment target schema differs")
if not source.get("release_id") or any(not str(value).strip() for value in source["target"].values()):
    raise ValueError("release identity or deployment target is empty")
runtime = source.get("runtime", {})
if set(runtime) != {"container_image_digest", "inference_runtime_name", "inference_runtime_version", "launch_argv"}:
    raise ValueError("runtime schema differs")
if not IMAGE.fullmatch(runtime["container_image_digest"]):
    raise ValueError("container image is not digest-pinned")
if not runtime["inference_runtime_name"] or not runtime["inference_runtime_version"]:
    raise ValueError("runtime name and version are required")
launch_argv = runtime["launch_argv"]
if (not isinstance(launch_argv, list) or not launch_argv or len(launch_argv) > 128
        or any(not isinstance(item, str) or not item or len(item) > 4096 or "\x00" in item for item in launch_argv)):
    raise ValueError("runtime launch_argv is invalid")
max_readback_age_seconds = int(policy.get("max_readback_age_seconds", 0))
if max_readback_age_seconds <= 0:
    raise ValueError("max_readback_age_seconds must be positive")

bundle = {
    "schema_version": "aidefend.deployed_release_bundle.v2",
    "release_id": source["release_id"],
    "generated_at": datetime.now(timezone.utc).isoformat(),
    "target": source["target"],
    "runtime": runtime,
    "policy": {
        "version": policy["policy_version"],
        "sha256": policy_sha256,
        "signature_sha256": policy_signature_sha256,
        "max_readback_age_seconds": max_readback_age_seconds,
    },
    "approved_inputs": {
        "sha256": input_sha256,
        "signature_sha256": input_signature_sha256,
        "verification_key_sha256": digest(INPUT_KEY),
    },
    "components": {
        name: bind_file(source["components"][name])
        for name in sorted(required_components)
    },
    "conditional": {
        name: bind_conditional(name, source["conditional"][name], allowed)
        for name in sorted(conditional_components)
    },
}
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
OUTPUT.write_text(
    json.dumps(bundle, sort_keys=True, separators=(",", ":"), allow_nan=False) + "\n",
    encoding="utf-8",
)
python release/build_release_bundle.py
cosign sign-blob --yes --key env://RELEASE_BUNDLE_SIGNING_KEY --bundle evidence/deployed-release-bundle.sig evidence/deployed-release-bundle.json
cosign verify-blob --key keys/release-bundle.pub --bundle evidence/deployed-release-bundle.sig evidence/deployed-release-bundle.json
Step 3: Independently read back every deployed replica
from __future__ import annotations

import math as _aidefend_math
import os as _aidefend_os

# Admission injects these required values from one signature-verified, versioned runtime profile.
def _aidefend_required(name):
    value = _aidefend_os.environ.get(name)
    if value is None or not value.strip():
        raise RuntimeError(f"required runtime-profile field is absent: {name}")
    return value.strip()


def _aidefend_positive_float(name):
    try:
        value = float(_aidefend_required(name))
    except ValueError as error:
        raise RuntimeError(f"runtime-profile field is not numeric: {name}") from error
    if not _aidefend_math.isfinite(value) or value <= 0:
        raise RuntimeError(f"runtime-profile field must be finite and positive: {name}")
    return value


def _aidefend_positive_int(name):
    try:
        value = int(_aidefend_required(name))
    except ValueError as error:
        raise RuntimeError(f"runtime-profile field is not an integer: {name}") from error
    if value < 1:
        raise RuntimeError(f"runtime-profile field must be positive: {name}")
    return value


def _aidefend_nonnegative_int(name):
    try:
        value = int(_aidefend_required(name))
    except ValueError as error:
        raise RuntimeError(f"runtime-profile field is not an integer: {name}") from error
    if value < 0:
        raise RuntimeError(f"runtime-profile field cannot be negative: {name}")
    return value


def _aidefend_fraction(name):
    value = _aidefend_positive_float(name)
    if value > 1:
        raise RuntimeError(f"runtime-profile field must be in (0, 1]: {name}")
    return value


RUNTIME_PROFILE_VERSION = _aidefend_required("AIDEFEND_RUNTIME_PROFILE_VERSION")
RUNTIME_PROFILE_SHA256 = _aidefend_required("AIDEFEND_RUNTIME_PROFILE_SHA256").lower()
if (len(RUNTIME_PROFILE_SHA256) != 64
        or set(RUNTIME_PROFILE_SHA256) - set("0123456789abcdef")):
    raise RuntimeError("runtime-profile digest must be lowercase SHA-256")
READBACK_COMMAND_TIMEOUT_SECONDS = _aidefend_positive_float("M001002_READBACK_COMMAND_TIMEOUT_SECONDS")

# File: release/verify_deployed_bundle.py

import hashlib
import json
import os
import re
import stat
import subprocess
import tempfile
from datetime import datetime, timedelta, timezone
from pathlib import Path

BUNDLE = Path("evidence/deployed-release-bundle.json")
SIGNATURE = Path("evidence/deployed-release-bundle.sig")
PUBLIC_KEY = Path("keys/release-bundle.pub")
RAW = Path("evidence/deployed-release-platform-readback.json")
OUTPUT = Path("evidence/deployed-release-readback.json")
SHA = re.compile(r"^[0-9a-f]{64}$")


def strict_json_bytes(raw: bytes, label: str) -> object:
    def unique_object(pairs):
        value = {}
        for key, item in pairs:
            if key in value:
                raise ValueError(f"duplicate JSON key in {label}: {key}")
            value[key] = item
        return value

    def reject_constant(value):
        raise ValueError(f"non-finite JSON constant in {label}: {value}")

    try:
        return json.loads(
            raw.decode("utf-8", errors="strict"),
            object_pairs_hook=unique_object,
            parse_constant=reject_constant,
        )
    except (UnicodeDecodeError, json.JSONDecodeError, ValueError) as error:
        raise ValueError(f"{label} is not strict UTF-8 JSON") from error


def read_regular_bytes(path: Path) -> bytes:
    flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
    descriptor = os.open(path, flags)
    with os.fdopen(descriptor, "rb") as handle:
        status = os.fstat(handle.fileno())
        if not stat.S_ISREG(status.st_mode) or status.st_size == 0:
            raise ValueError(f"missing, empty, or unsafe file: {path}")
        return handle.read()


def write_private_snapshot(path: Path, raw: bytes) -> None:
    flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
    descriptor = os.open(path, flags, 0o400)
    with os.fdopen(descriptor, "wb") as handle:
        handle.write(raw)
        handle.flush()
        os.fsync(handle.fileno())


def capture_verified_signature(
    payload_path: Path,
    signature_path: Path,
    key_path: Path,
    timeout_seconds: float,
) -> tuple[bytes, bytes]:
    payload = read_regular_bytes(payload_path)
    signature = read_regular_bytes(signature_path)
    with tempfile.TemporaryDirectory(prefix="aidefend-release-input-") as directory:
        snapshot_root = Path(directory)
        snapshot_root.chmod(0o700)
        payload_snapshot = snapshot_root / "payload"
        signature_snapshot = snapshot_root / "payload.sig"
        write_private_snapshot(payload_snapshot, payload)
        write_private_snapshot(signature_snapshot, signature)
        subprocess.run(
            [
                "cosign", "verify-blob", "--key", str(key_path),
                "--bundle", str(signature_snapshot), str(payload_snapshot),
            ],
            check=True, capture_output=True, text=True, timeout=timeout_seconds,
        )
        if read_regular_bytes(payload_snapshot) != payload or read_regular_bytes(signature_snapshot) != signature:
            raise ValueError("private signed-input snapshot changed during verification")
    return payload, signature


def run(args: list[str]) -> str:
    return subprocess.run(args, check=True, capture_output=True, text=True, timeout=READBACK_COMMAND_TIMEOUT_SECONDS).stdout


def digest(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def observed_sha(pod: str, container: str, runtime_path: str, namespace: str) -> str:
    if not runtime_path.startswith("/") or "\n" in runtime_path:
        raise ValueError("runtime path is invalid")
    value = run([
        "kubectl", "exec", "-n", namespace, pod, "-c", container,
        "--", "sha256sum", "--", runtime_path,
    ]).split()[0].lower()
    if not SHA.fullmatch(value):
        raise ValueError("runtime digest readback is invalid")
    return value


def string_population(value, label: str) -> list[str]:
    if (
        not isinstance(value, list) or not value
        or any(
            not isinstance(item, str)
            or re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}", item) is None
            for item in value
        )
        or len(value) != len(set(value))
    ):
        raise ValueError(f"{label} must be a unique canonical string array")
    return value


now = datetime.now(timezone.utc)
outcome = "ERROR"
reasons: list[str] = []
replica_readbacks: list[dict] = []
bundle: dict = {}
bundle_sha256 = None
bundle_signature_sha256 = None
verifier_identity = os.environ.get("VERIFIER_WORKLOAD_ID")
try:
    if not verifier_identity:
        raise ValueError("VERIFIER_WORKLOAD_ID is required")
    bundle_raw, signature_raw = capture_verified_signature(
        BUNDLE, SIGNATURE, PUBLIC_KEY, READBACK_COMMAND_TIMEOUT_SECONDS,
    )
    bundle = strict_json_bytes(bundle_raw, "signed deployed-release bundle")
    bundle_keys = {
        "schema_version", "release_id", "generated_at", "target", "runtime",
        "policy", "approved_inputs", "components", "conditional",
    }
    if (
        not isinstance(bundle, dict)
        or set(bundle) != bundle_keys
        or bundle.get("schema_version") != "aidefend.deployed_release_bundle.v2"
    ):
        raise ValueError("bundle schema differs")
    bundle_sha256 = hashlib.sha256(bundle_raw).hexdigest()
    bundle_signature_sha256 = hashlib.sha256(signature_raw).hexdigest()
    target = bundle["target"]
    namespace, deployment, container = target["namespace"], target["deployment"], target["container"]
    deployment_json = strict_json_bytes(
        run(["kubectl", "get", "deployment", deployment, "-n", namespace, "-o", "json"]).encode("utf-8"),
        "Kubernetes deployment readback",
    )
    labels = deployment_json["spec"]["selector"]["matchLabels"]
    if not labels:
        raise ValueError("deployment selector is empty")
    selector = ",".join(f"{key}={value}" for key, value in sorted(labels.items()))
    pods_json = strict_json_bytes(
        run(["kubectl", "get", "pods", "-n", namespace, "-l", selector, "-o", "json"]).encode("utf-8"),
        "Kubernetes pod-population readback",
    )
    RAW.parent.mkdir(parents=True, exist_ok=True)
    RAW.write_text(
        json.dumps({"deployment": deployment_json, "pods": pods_json}, sort_keys=True, separators=(",", ":")) + "\n",
        encoding="utf-8",
    )

    desired = int(deployment_json["spec"].get("replicas", 1))
    pods = pods_json.get("items", [])
    ready = []
    for pod in pods:
        conditions = {
            item["type"]: item["status"]
            for item in pod.get("status", {}).get("conditions", [])
        }
        if conditions.get("Ready") == "True" and pod.get("metadata", {}).get("deletionTimestamp") is None:
            ready.append(pod)
    if desired < 1 or len(pods) != desired or len(ready) != desired:
        outcome = "INSUFFICIENT_DATA"
        reasons.append("complete ready replica population is unavailable")
    else:
        expected_image = bundle["runtime"]["container_image_digest"].split("@", 1)[1]
        expected_bundle_sha = bundle_sha256
        annotation = deployment_json.get("metadata", {}).get("annotations", {}).get("aidefend.io/release-bundle-sha256")
        mismatches: list[dict] = []
        items = list(bundle["components"].items())
        for family, conditional in bundle["conditional"].items():
            if conditional["applicability"] == "APPLICABLE":
                items.extend(
                    (f"{family}:{item['name']}", item)
                    for item in conditional["items"]
                )
        for pod in sorted(ready, key=lambda item: item["metadata"]["name"]):
            container_specs = {
                item["name"]: item
                for item in pod.get("spec", {}).get("containers", [])
            }
            container_spec = container_specs.get(container)
            if container_spec is None:
                mismatches.append({
                    "pod_uid": pod["metadata"]["uid"],
                    "field": "container_spec",
                })
                continue
            command = container_spec.get("command")
            arguments = container_spec.get("args", [])
            if (not isinstance(command, list) or not command
                    or not isinstance(arguments, list)):
                mismatches.append({
                    "pod_uid": pod["metadata"]["uid"],
                    "field": "explicit_container_command",
                })
                continue
            observed_launch_argv = [
                *command,
                *arguments,
            ]
            if observed_launch_argv != bundle["runtime"]["launch_argv"]:
                mismatches.append({
                    "pod_uid": pod["metadata"]["uid"],
                    "field": "launch_argv",
                })
            statuses = {
                item["name"]: item
                for item in pod["status"]["containerStatuses"]
            }
            status = statuses.get(container)
            if status is None or not status.get("imageID", "").endswith(expected_image):
                mismatches.append({
                    "pod_uid": pod["metadata"]["uid"],
                    "field": "container_image_digest",
                })
                continue
            measured = {}
            for name, component in items:
                measured[name] = observed_sha(
                    pod["metadata"]["name"],
                    container,
                    component["runtime_path"],
                    namespace,
                )
                if measured[name] != component["sha256"]:
                    mismatches.append({
                        "pod_uid": pod["metadata"]["uid"],
                        "field": name,
                    })
            replica_readbacks.append({
                "pod_uid": pod["metadata"]["uid"],
                "image_id": status["imageID"],
                "launch_argv": observed_launch_argv,
                "component_sha256": measured,
            })
        if annotation != expected_bundle_sha:
            mismatches.append({"field": "release_bundle_annotation"})
        outcome = "FAIL" if mismatches else "PASS"
        reasons.extend(json.dumps(item, sort_keys=True) for item in mismatches)
except (OSError, subprocess.SubprocessError, KeyError, TypeError, ValueError, json.JSONDecodeError) as error:
    replica_readbacks = []
    reasons.append(str(error))

max_age = int(bundle.get("policy", {}).get("max_readback_age_seconds", 0) or 0)
result = {
    "schema_version": "aidefend.deployed_release_readback.v2",
    "control": "AID-M-001.002",
    "release_id": bundle.get("release_id"),
    "bundle_sha256": bundle_sha256,
    "bundle_signature_sha256": bundle_signature_sha256,
    "raw_platform_readback_sha256": digest(RAW) if RAW.is_file() else None,
    "verifier_identity": verifier_identity,
    "observed_at": now.isoformat(),
    "expires_at": (now + timedelta(seconds=max_age)).isoformat() if max_age > 0 else None,
    "outcome": outcome,
    "reasons": reasons,
    "replica_readbacks": replica_readbacks,
}
OUTPUT.write_text(
    json.dumps(result, sort_keys=True, separators=(",", ":")) + "\n",
    encoding="utf-8",
)
if outcome != "PASS":
    raise SystemExit(f"deployed release verification did not pass: {outcome}")
python release/verify_deployed_bundle.py
cosign sign-blob --yes --key env://DEPLOYMENT_VERIFIER_SIGNING_KEY --bundle evidence/deployed-release-readback.sig evidence/deployed-release-readback.json
cosign verify-blob --key keys/deployment-verifier.pub --bundle evidence/deployed-release-readback.sig evidence/deployed-release-readback.json
Independent replay and evidence

An independent read-only identity that cannot alter the build, workload, or first verifier re-verifies all signatures, checks receipt freshness, and repeats the full population readback. Retain the signed policy, approved inputs, bundle, raw platform response, both readback receipts, signer identities, timestamps, and all referenced artifacts in append-only storage. Never accept a caller-provided passed field or a successful deployment command as proof. Action: Publish signed reconciliation evidence. When the independent replay is not a fresh PASS, withhold the clean reconciliation receipt and emit the exact FAIL, INSUFFICIENT_DATA, or ERROR result; AID-H-003.002 owns any promotion block that consumes this evidence.

Open-source tools

Commercial / hosted tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-M-001.002: AI System Dependency Mapping | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version