AID-M-001.002: AI System Dependency Mapping
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.
- 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.
- 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.
- 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.
AID-M-001.002-G001
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
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
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
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.jsonThe 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
- Syft
- CycloneDX CLI
- OWASP Dependency-Track
- Trivy
- Cosign
- kubectl
Commercial / hosted tools
- Snyk Open Source
- Mend SCA
- JFrog Xray
Threat mappings
MITRE ATLAS
- AML.T0010 AI Supply Chain Compromise
- AML.T0011 User Execution
- AML.T0010.001 AI Supply Chain Compromise: AI Software
- AML.T0010.004 AI Supply Chain Compromise: Container Registry
- AML.T0011.001 User Execution: Malicious Package
MAESTRO
- Supply Chain Attacks (Cross-Layer)
- Compromised Framework Components (L3)
- Integration Risks (L7)
- Compromised Agent Registry (L7)
OWASP LLM Top 10 2026
- LLM04:2026 Supply Chain
OWASP ML Top 10 2023
- ML06:2023 AI Supply Chain Attacks
OWASP Top 10 for Agentic Applications 2026
- ASI04:2026 Agentic Supply Chain Vulnerabilities
NIST Adversarial Machine Learning 2025
- NISTAML.051 Model Poisoning (Supply Chain)
Cisco Integrated AI Security and Safety Framework
- AISubtech-5.2.1 Agent Profile Tampering (Deployed agent configuration drift is detected against the signed release)
- AISubtech-9.3.1 Malicious Package / Tool Injection (Frozen dependency resolution detects substituted or unreviewed package updates)
- AISubtech-9.3.3 Dependency Replacement / Rug Pull (Frozen dependency resolution detects substituted or unreviewed package updates)
- AITech-5.2 Configuration Persistence (Deployed agent configuration drift is detected against the signed release)
- AITech-9.3 Dependency / Plugin Compromise (Frozen dependency resolution detects substituted or unreviewed package updates)
Google Secure AI Framework 2.0 - Risks
- MST: Model Source Tampering (dependency pinning and SBOMs prevent supply chain attacks on code)
- IIC: Insecure Integrated Component (mapping external APIs and third-party services)
- MDT: Model Deployment Tampering (SBOM verification prevents tampered deployment artifacts)
Databricks AI Security Framework 3.0
- Algorithms 5.4: Malicious libraries
- Model 7.3: ML Supply chain vulnerabilities
- Model 7.4: Source code control attack
- Governance 4.1: Lack of traceability and transparency of model assets
- Platform 12.1: Lack of vulnerability management
- Platform 12.5: Poor security in the software development lifecycle
- Operations 11.1: Lack of MLOps - repeatable enforced standards
- Agents - Tools MCP Server 13.21: Supply Chain Attacks
- Agents - Tools MCP Server 13.18: Tool Poisoning