AID-M-004: AI Threat Modeling & Risk Assessment
Pillars: data, infra, model, app
Lifecycle: scoping, operation
Scope boundary
Owns version-controlled technical threat models and risk registers for an exact AI system, release, and operating context, including assets, trust boundaries, flows, capabilities, scenarios, assumptions, owners, priorities, and selected defensive requirements. It identifies and prioritizes risk but does not implement defenses or accept enterprise risk.
AID-M-007.001: AID-M-004 models adversarial threats, technical failure scenarios, and resulting security risk across the system; AID-M-007.001 defines the intended-use safety boundary and machine-readable policy contract for allowed outcomes. Threat scenarios explain how harm or compromise may occur, while a safety boundary defines what behavior and use remain acceptable.
Implementation guidance
- AID-M-004-G001: Utilize established threat modeling methodologies (STRIDE, PASTA, OCTAVE) adapted for AI.
- AID-M-004-G002: Leverage AI-specific threat frameworks (ATLAS, MAESTRO, OWASP).
- AID-M-004-G003: For agentic AI, consider tool misuse, memory tampering, goal manipulation, etc.
- AID-M-004-G004: Explicitly include the model training process, environment, and MLOps pipeline components in threat modeling exercises, considering threats of training data manipulation, training code compromise, and environment exploitation (relevant to defenses like AID-H-007).
- AID-M-004-G005: For systems employing federated learning, specifically model threats related to malicious client participation, insecure aggregation protocols, and potential inference attacks against client data, and evaluate countermeasures like AID-H-008.
- AID-M-004-G006: Explicitly model threats related to AI hardware security, including side-channel attacks, fault injection, and physical tampering against AI accelerators (addressed by AID-H-009).
- AID-M-004-G007: Maintain a required participant roster and signed review record for each AI threat-model session.
- AID-M-004-G008: Prioritize risks based on likelihood and impact.
- AID-M-004-G009: Store the approved threat model, risk register, and trust-boundary diagrams as version-controlled artifacts.
- AID-M-004-G010: Integrate threat-model currency and structure checks into the MLOps workflow before major changes are promoted.
- AID-M-004-G011: Regularly review and update threat models.
AID-M-004-G001
Complete-system deliverable
Use STRIDE, PASTA, OCTAVE, attack trees, or another reviewed method, but bind the result to one exact AI system scope and system release. The deliverable is complete only when its asset, component, relationship, data/control-flow, trust-boundary, and privileged-identity populations exactly reconcile with the governed architecture snapshot. Record attacker prerequisites, assumptions, security properties, scenarios, control objectives, owners, unknowns, and evidence-backed exclusions; one component worksheet or a “top risks” list is not system closure.
Verify exact scope and threat population
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
ARCHITECTURE_POPULATIONS = {
"assets", "components", "relationships", "data_flows",
"control_flows", "trust_boundaries", "privileged_identities",
}
MODEL_FIELDS = {
"schema_version", "scenario_id", "release_digest",
"architecture_sha256", "scope_populations", "assumptions",
"security_properties", "threat_scenarios", "control_objectives",
"owners", "unknowns", "exclusions",
}
THREAT_FIELDS = {
"threat_id", "attacker_prerequisites", "affected_scope_ids",
"violated_security_properties", "scenario", "control_objective_ids",
"owner_id",
}
def strict_json(path: Path) -> dict:
def unique(pairs):
value = {}
for key, item in pairs:
if key in value:
raise ValueError(f"duplicate JSON key: {key}")
value[key] = item
return value
value = json.loads(
path.read_text(encoding="utf-8"),
object_pairs_hook=unique,
parse_constant=lambda token: (_ for _ in ()).throw(ValueError(token)),
)
if not isinstance(value, dict):
raise ValueError("input must be an object")
return value
def sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def identity_set(rows: object, label: str) -> set[str]:
if not isinstance(rows, list):
raise ValueError(f"{label} must be an array")
identities = []
for row in rows:
if not isinstance(row, dict) or not isinstance(row.get("id"), str) or not row["id"]:
raise ValueError(f"{label} member lacks an ID")
identities.append(row["id"])
if len(identities) != len(set(identities)):
raise ValueError(f"{label} contains duplicate IDs")
return set(identities)
parser = argparse.ArgumentParser()
parser.add_argument("--architecture", type=Path, required=True)
parser.add_argument("--threat-model", type=Path, required=True)
parser.add_argument("--scenario-id", required=True)
parser.add_argument("--release-digest", required=True)
args = parser.parse_args()
architecture = strict_json(args.architecture)
model = strict_json(args.threat_model)
if set(model) != MODEL_FIELDS or (
model["schema_version"] != "aidefend.system-threat-model.v2"
or model["scenario_id"] != args.scenario_id
or model["release_digest"] != args.release_digest
or model["architecture_sha256"] != sha256(args.architecture)
):
raise SystemExit("threat model system scope, release, or architecture binding differs")
if not ARCHITECTURE_POPULATIONS.issubset(architecture):
raise SystemExit("architecture snapshot lacks a required population")
if not isinstance(model["scope_populations"], dict) or (
set(model["scope_populations"]) != ARCHITECTURE_POPULATIONS
):
raise SystemExit("threat-model scope population schema differs")
all_scope_ids: set[str] = set()
for population in sorted(ARCHITECTURE_POPULATIONS):
expected = identity_set(architecture[population], "architecture." + population)
declared = model["scope_populations"][population]
if (
not isinstance(declared, list)
or len(declared) != len(set(declared))
or set(declared) != expected
):
raise SystemExit("threat-model scope differs: " + population)
all_scope_ids.update(expected)
objective_ids = identity_set(model["control_objectives"], "control_objectives")
owner_ids = identity_set(model["owners"], "owners")
property_ids = identity_set(model["security_properties"], "security_properties")
threat_ids: set[str] = set()
for threat in model["threat_scenarios"]:
if not isinstance(threat, dict) or set(threat) != THREAT_FIELDS:
raise SystemExit("threat scenario schema differs")
if (
threat["threat_id"] in threat_ids
or not set(threat["affected_scope_ids"]).issubset(all_scope_ids)
or not set(threat["violated_security_properties"]).issubset(property_ids)
or not set(threat["control_objective_ids"]).issubset(objective_ids)
or threat["owner_id"] not in owner_ids
or not threat["attacker_prerequisites"]
or not str(threat["scenario"]).strip()
):
raise SystemExit("threat scenario binding is incomplete")
threat_ids.add(threat["threat_id"])
for exclusion in model["exclusions"]:
if (
not isinstance(exclusion, dict)
or set(exclusion) != {"scope_id", "reason", "evidence_sha256"}
or exclusion["scope_id"] not in all_scope_ids
or len(str(exclusion["evidence_sha256"])) != 64
or not str(exclusion["reason"]).strip()
):
raise SystemExit("scope exclusion lacks evidence")
outcome = "INSUFFICIENT_DATA" if model["unknowns"] else "PASS"
print(json.dumps({
"scenario_id": model["scenario_id"],
"release_digest": model["release_digest"],
"architecture_sha256": model["architecture_sha256"],
"scope_member_count": len(all_scope_ids),
"threat_count": len(threat_ids),
"unknown_count": len(model["unknowns"]),
"outcome": outcome,
}, sort_keys=True))Action: Run this reconciliation whenever the system scope, release, architecture, identity, flow, or trust-boundary population changes. PASS means this bounded threat-model package reconciles to the exact architecture and has no recorded unknowns; it does not mean every listed threat is prevented or that downstream controls passed.
AID-M-004-G002
Concept:
Use frameworks created by security experts to understand known adversary behaviors and common vulnerabilities in AI systems.
Step 1: Identify Relevant TTPs and Vulnerabilities
Review the frameworks and identify items relevant to your system's architecture.
- MITRE ATLAS: Look for specific Tactics, Techniques, and Procedures (TTPs) adversaries use against ML systems. (e.g., AML.T0020 Training Data Poisoning).
- MAESTRO: Use the 7-layer model to analyze threats at each level of your AI agent, from the foundation model to the agentic ecosystem.
- OWASP Top 10 for LLM/ML: Use these lists as a checklist for the most common and critical security risks. (e.g., LLM01: Prompt Injection).
Step 2: Create a Threat Mapping Template
Document threats using a structured approach that references these frameworks.
# File: threat_register_template.md
## Threat ID: THR-001
**Description:** Attacker could poison the RAG knowledge base with false information
**Framework References:**
- MAESTRO: L2 (Data Operations) - Compromised RAG Pipelines
- ATLAS: AML.T0070 (RAG Poisoning); AML.T0071 (False RAG Entry Injection)
- OWASP LLM: LLM05:2026 (Data and Model Poisoning)
**Attack Vector:** External data source compromise leading to injection of false documents
**Impact:** High - Could lead to widespread misinformation in model outputs
**Likelihood:** Medium - Requires access to data pipeline or upstream sources
**Mitigation:** Implement data validation, source verification, content scanningUse AML.T0020 Training Data Poisoning only when the same content actually enters a training, fine-tuning, or adaptation path; inference-time retrieval-corpus poisoning remains mapped to the RAG-specific techniques above.
Step 3: Use Framework-Specific Tools
Leverage available tools like the MITRE ATLAS Navigator to visualize attack paths and identify gaps in your defenses.
# Example: Using ATLAS Navigator workflow
1. Navigate to https://mitre-atlas.github.io/atlas-navigator/
2. Load the ATLAS matrix
3. Select techniques relevant to your ML system type
4. Export selected techniques as a JSON file
5. Import into your threat modeling documentation
6. Map each technique to specific components in your architectureAction: Incorporate these frameworks into your process to benefit from community knowledge and avoid reinventing the wheel.
AID-M-004-G003
Purpose and boundary
This step produces one agent-specific threat-profile artifact. It does not implement runtime monitoring, detection thresholds, memory isolation, or tool enforcement; those controls remain with the referenced Detect, Harden, and Isolate techniques.
Define the versioned profile contract
# File: threat_model/schemas/agent-threat-profile.schema.yaml
schema_version: aidefend.agent-threat-profile-schema.v1
required_bindings:
agent_release_digest:
type: string
pattern: "^sha256:[0-9a-f]{64}$"
capability_inventory_receipt_digest:
type: string
pattern: "^sha256:[0-9a-f]{64}$"
tool_catalog_receipt_digest:
type: string
pattern: "^sha256:[0-9a-f]{64}$"
authority_envelope_receipt_digest:
type: string
pattern: "^sha256:[0-9a-f]{64}$"
required_threat_profiles:
tool_misuse:
required_fields: [applicability, capability_ids, misuse_scenarios, control_ids]
memory_tampering:
required_fields: [applicability, memory_store_ids, write_paths, control_ids]
goal_manipulation:
required_fields: [applicability, goal_manifest_digest, mutation_paths, control_ids]
excessive_agency:
required_fields: [applicability, high_impact_action_ids, approval_modes, control_ids]
rogue_agent:
required_fields: [applicability, peer_and_registry_paths, lifecycle_scenarios, control_ids]
applicability_states: [APPLICABLE, NOT_APPLICABLE]
prohibited_completion_states:
- UNKNOWN_CAPABILITY
- UNMAPPED_TOOL
- UNSUPPORTED_PROFILEPopulate from authoritative inventories
Generate one profile row for every capability, tool, memory write path, goal mutation path, high-impact action, peer-agent relationship, and registry introduction path in the signed scope. Each NOT_APPLICABLE decision must bind evidence proving the prerequisite is absent. Map scenarios to concrete AIDEFEND controls by ID; do not embed fake detection expressions or claim those downstream controls passed.
Action: Validate and sign the profile together with the exact inventory receipts and agent release digest. An unknown capability, unclassified path, missing binding, or failed schema validation blocks threat-model approval.
AID-M-004-G004
Topology-derived MLOps scope
Model the stages that actually exist in the exact training, fine-tuning, adaptation, evaluation, packaging, promotion, export, provider, and serving topology. Do not require a universal seven-stage pipeline. Batch/export-only, external-provider, offline-training, separately governed promotion, and not-yet-serving candidates can have different stage populations; every omitted stage class needs a source-backed absence in the governed topology.
Reconcile every actual stage and cross-boundary dependency
from __future__ import annotations
import hashlib
import json
from pathlib import Path
TOPOLOGY = Path("threat-model/mlops-topology.json")
ASSESSMENT = Path("threat-model/mlops-stage-assessment.json")
def strict(path: Path) -> dict:
def unique(pairs):
value = {}
for key, item in pairs:
if key in value:
raise ValueError(f"duplicate key: {key}")
value[key] = item
return value
result = json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=unique)
if not isinstance(result, dict):
raise ValueError("document must be an object")
return result
topology, assessment = strict(TOPOLOGY), strict(ASSESSMENT)
if set(topology) != {
"schema_version", "scenario_id", "release_digest", "stages",
"stage_class_absences",
} or topology["schema_version"] != "aidefend.mlops-topology.v2":
raise SystemExit("MLOps topology schema differs")
if set(assessment) != {
"schema_version", "scenario_id", "release_digest",
"topology_sha256", "stages",
} or assessment["schema_version"] != "aidefend.mlops-stage-assessment.v2":
raise SystemExit("MLOps assessment schema differs")
if (
assessment["scenario_id"] != topology["scenario_id"]
or assessment["release_digest"] != topology["release_digest"]
or assessment["topology_sha256"] != hashlib.sha256(TOPOLOGY.read_bytes()).hexdigest()
):
raise SystemExit("MLOps assessment binding differs")
required_stage_fields = {
"stage_id", "stage_type", "provider_mode", "dependency_ids",
"privileged_identity_ids", "input_flow_ids", "output_flow_ids",
"trust_boundary_ids",
}
topology_by_id = {}
for stage in topology["stages"]:
if not isinstance(stage, dict) or set(stage) != required_stage_fields:
raise SystemExit("topology stage schema differs")
if (
not stage["stage_id"] or stage["stage_id"] in topology_by_id
or stage["provider_mode"] not in {"customer", "provider", "shared"}
or any(not isinstance(stage[field], list)
for field in required_stage_fields - {"stage_id", "stage_type", "provider_mode"})
):
raise SystemExit("topology stage identity or population differs")
topology_by_id[stage["stage_id"]] = stage
assessment_by_id = {}
for stage in assessment["stages"]:
if not isinstance(stage, dict) or set(stage) != {
"stage_id", "threat_ids", "control_objective_ids",
"owner_id", "evidence_refs",
} or stage.get("stage_id") in assessment_by_id:
raise SystemExit("stage assessment schema or identity differs")
if not stage["owner_id"] or not stage["threat_ids"] or not stage["evidence_refs"]:
raise SystemExit("stage assessment is incomplete")
assessment_by_id[stage["stage_id"]] = stage
if set(assessment_by_id) != set(topology_by_id):
raise SystemExit("actual MLOps stage population is missing or contains extras")
for absence in topology["stage_class_absences"]:
if (
not isinstance(absence, dict)
or set(absence) != {"stage_type", "reason", "evidence_sha256"}
or not all(str(absence[field]).strip() for field in absence)
or len(absence["evidence_sha256"]) != 64
):
raise SystemExit("omitted stage class lacks source-backed absence evidence")
print(json.dumps({
"scenario_id": topology["scenario_id"],
"release_digest": topology["release_digest"],
"assessed_stage_ids": sorted(assessment_by_id),
"topology_sha256": assessment["topology_sha256"],
"outcome": "PASS",
}, sort_keys=True))Action: Threat-model each topology member's exact identities, dependencies, flows, and boundaries. Unknown or unmapped nodes remain INSUFFICIENT_DATA or ERROR; an absent stage supported by architecture evidence is not a failure and must not be invented merely to match an illustrative pipeline.
AID-M-004-G005
Purpose and boundary
This conditional profile applies only when federated learning is present. Its output is a complete threat and trust-boundary model for the exact federation release; robust aggregation, differential privacy, secure aggregation, and client admission remain separate Harden implementations.
Define the federated-learning profile contract
# File: threat_model/schemas/federated-learning-threat-profile.schema.yaml
schema_version: aidefend.fl-threat-profile-schema.v1
applicability:
allowed_states: [APPLICABLE, NOT_APPLICABLE]
not_applicable_requires:
- signed_architecture_receipt_without_federation
required_bindings:
federation_release_digest:
type: string
pattern: "^sha256:[0-9a-f]{64}$"
participant_roster_receipt_digest:
type: string
pattern: "^sha256:[0-9a-f]{64}$"
aggregation_protocol_digest:
type: string
pattern: "^sha256:[0-9a-f]{64}$"
required_populations:
- coordinator_identities
- participant_identities
- enrollment_and_revocation_paths
- update_submission_paths
- aggregation_rounds
- model_distribution_paths
- telemetry_and_audit_paths
required_scenario_families:
- malicious_or_sybil_participant
- poisoned_or_replayed_update
- coordinator_compromise
- insecure_or_bypassed_aggregation
- participant_update_inference
- global_model_substitution
- availability_and_dropout_abuse
scenario_required_fields:
- affected_population
- preconditions
- trust_boundary
- attack_path
- impact
- control_ids
- residual_risk_ownerDo not publish a universal malicious-client percentage, privacy budget, aggregation choice, or risk score. Resolve them from the signed federation threat and risk policies and preserve unsupported participant or round coverage as INSUFFICIENT_DATA.
Action: Generate, validate, and sign the profile from the authoritative participant roster, aggregation protocol, identity, and deployment receipts. Every participant type, round path, and scenario family must have an explicit applicability decision before approval.
AID-M-004-G006
Purpose and boundary
This conditional profile models hardware trust boundaries for the exact deployment. It does not imply that every cloud or software-only deployment has a practical physical-attack risk, and it does not implement the hardware protections owned by Harden.
Define the hardware threat-profile contract
# File: threat_model/schemas/ai-hardware-threat-profile.schema.yaml
schema_version: aidefend.ai-hardware-threat-profile-schema.v1
applicability:
allowed_states: [APPLICABLE, NOT_APPLICABLE]
not_applicable_requires:
- signed_deployment_receipt_without_in_scope_accelerator_or_physical_boundary
required_bindings:
deployment_release_digest:
type: string
pattern: "^sha256:[0-9a-f]{64}$"
accelerator_inventory_receipt_digest:
type: string
pattern: "^sha256:[0-9a-f]{64}$"
firmware_inventory_receipt_digest:
type: string
pattern: "^sha256:[0-9a-f]{64}$"
required_asset_fields:
- device_id
- hardware_model
- firmware_version
- driver_version
- physical_location_class
- physical_custody_owner
- tenant_or_co_residency_model
- debug_interface_state
- boot_and_attestation_capabilities
conditional_scenario_families:
physically_accessible_device:
- physical_tampering
- debug_interface_abuse
- fault_injection
- power_or_emission_side_channel
shared_compute_or_accelerator:
- cross_tenant_residue
- cache_or_timing_side_channel
- firmware_or_driver_compromise
supply_chain_or_field_update:
- malicious_firmware
- counterfeit_or_substituted_device
- update_channel_compromise
scenario_required_fields:
- affected_device_population
- realistic_preconditions
- observation_or_attack_point
- impact
- control_ids
- residual_risk_ownerInclude a scenario only when the deployment and attacker prerequisites are realistic. A laboratory-only technique with no credible access path must not inflate the operational risk register; preserve its signed NOT_APPLICABLE rationale instead.
Action: Validate and sign the profile against independent accelerator, firmware, deployment, custody, and co-residency readbacks. An unknown device, firmware, custody state, or conditional applicability decision blocks approval.
AID-M-004-G007
Bind review authority to the governed release roster
Do not infer authority from meeting attendance, e-mail addresses, Git authorship, or a mutable YAML file. Capture and verify the signed review policy, identity-directory roster readback, and review record as stable bytes. The policy must bind the exact system_scope_id, release digest, threat-model package digest, authority generation, currentness window, required roles, and permitted delegation scopes.
{
"schema_version": "aidefend.threat-review-policy.v1",
"system_scope_id": "fraud-prod",
"release_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"threat_model_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"authority_generation": "review-authority-2026-08-28",
"required_roles": ["ml_owner", "security_reviewer", "service_owner"],
"minimum_acceptances": 3,
"valid_until": "2026-09-30T00:00:00Z"
}Validate the exact role population
from datetime import datetime, timezone
def validate_review(policy, roster, review):
if set(policy["required_roles"]) != set(roster["required_roles"]):
raise ValueError("roster role population differs from policy")
if len(policy["required_roles"]) != len(set(policy["required_roles"])):
raise ValueError("duplicate required role")
if any(review.get(k) != policy[k] for k in (
"system_scope_id", "release_digest", "threat_model_digest",
"authority_generation"
)):
raise ValueError("review scope or authority generation differs")
now = datetime.now(timezone.utc)
expires = datetime.fromisoformat(policy["valid_until"].replace("Z", "+00:00"))
if now >= expires or roster["read_at"] > policy["valid_until"]:
raise ValueError("authority roster is not current")
active = {}
for assignment in roster["assignments"]:
principal, role = assignment["principal"], assignment["role"]
if not assignment["active"] or role not in policy["required_roles"]:
continue
if (principal, role) in active:
raise ValueError("duplicate active assignment")
active[principal, role] = assignment
accepted_roles = set()
for decision in review["decisions"]:
key = (decision["principal"], decision["role"])
assignment = active.get(key)
if assignment is None:
delegation = next((item for item in roster.get("delegations", [])
if item["delegate"] == decision["principal"]
and item["role"] == decision["role"]
and item["scope_digest"] == policy["threat_model_digest"]
and item["active"]), None)
if delegation is None:
raise ValueError("decision principal lacks current authority")
if decision["decision"] != "ACCEPT":
raise ValueError("required review role did not accept")
if decision["role"] in accepted_roles:
raise ValueError("duplicate role decision")
accepted_roles.add(decision["role"])
if accepted_roles != set(policy["required_roles"]):
raise ValueError("required review-role population is incomplete")
if len(review["decisions"]) < policy["minimum_acceptances"]:
raise ValueError("minimum acceptance count not met")
return {"outcome": "REVIEW_COMPLETE",
"review_id": review["review_id"],
"roles": sorted(accepted_roles)}
Action: Run this semantic check only over signature-verified snapshots and preserve their digests with the result. Missing, stale, inactive, ambiguous, out-of-scope, or rejected authority is REVIEW_INCOMPLETE; a commit or meeting record alone cannot complete the review. This guidance proves review completeness, not that each modeled risk is controlled.
AID-M-004-G008
Use one policy-declared prioritization method
Prioritization orders work; it is not a control PASS/FAIL decision or a risk-acceptance record. The signed risk policy chooses either an ordinal matrix or a numeric model for the exact threat-model population and binds its scales, factors, uncertainty rules, tie-breaks, model/configuration digest, and current generation.
{
"schema_version": "aidefend.risk-prioritization-policy.v2",
"system_scope_id": "fraud-prod",
"threat_model_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"method_type": "ordinal_matrix",
"threat_ids": ["THR-001", "THR-002"],
"allowed_priority_bands": ["critical", "high", "medium", "low"],
"tie_break_fields": ["priority_band", "threat_id"],
"policy_generation": "risk-policy-2026-08-28"
}Enforce typed outputs and complete population coverage
import math
def validate_priority(policy, results):
expected = policy["threat_ids"]
if len(expected) != len(set(expected)):
raise ValueError("duplicate policy threat id")
by_id = {}
for row in results:
threat_id = row["threat_id"]
if threat_id in by_id:
raise ValueError("duplicate result threat id")
if row["policy_generation"] != policy["policy_generation"]:
raise ValueError("policy generation differs")
if row["method_type"] != policy["method_type"]:
raise ValueError("prioritization method differs")
state = row["result_state"]
if state not in {"PRIORITIZED", "UNRESOLVED", "ERROR"}:
raise ValueError("invalid prioritization state")
if policy["method_type"] == "ordinal_matrix":
if "computed_score" in row:
raise ValueError("ordinal result must not invent a numeric score")
if state == "PRIORITIZED" and row.get("priority_band") not in policy["allowed_priority_bands"]:
raise ValueError("invalid priority band")
elif policy["method_type"] == "numeric_model":
score = row.get("computed_score")
if state == "PRIORITIZED" and (
not isinstance(score, (int, float)) or isinstance(score, bool)
or not math.isfinite(score)
):
raise ValueError("numeric result lacks a finite score")
if row.get("model_config_digest") != policy["model_config_digest"]:
raise ValueError("numeric model configuration differs")
else:
raise ValueError("unsupported prioritization method")
if row.get("uncertainty_state") not in {"NONE", "OPEN", "CONFLICT"}:
raise ValueError("invalid uncertainty state")
if row.get("uncertainty_state") != "NONE" and state == "PRIORITIZED":
raise ValueError("uncertainty cannot be silently prioritized")
by_id[threat_id] = row
if set(by_id) != set(expected):
raise ValueError("threat population is incomplete or contains extras")
return by_id
Action: Independently recompute the normalized inputs and ordered result from the same signed policy and exact threat population. Missing factors, disagreement, unsupported methods, stale policy, or non-finite numeric values remain UNRESOLVED or ERROR, never a low priority and never PASS. Risk acceptance remains a separate authorized decision.
AID-M-004-G009
Publish a content-addressed threat-model package
A mutable directory, branch name, wiki page, commit, or tag is only a candidate input. Build a canonical manifest over every applicable regular file in the governed package root, including the threat model, risk register, trust-boundary diagrams, assumptions, exclusions, and the review result from AID-M-004-G007. Reject symlinks, special files, path escapes, normalized-path collisions, duplicate members, and files that change during capture.
{
"schema_version": "aidefend.threat-model-package.v2",
"system_scope_id": "fraud-prod",
"release_digest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
"review_result_digest": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"members": [
{"path": "THREAT_MODEL.md", "sha256": "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "size": 8421},
{"path": "risk_register.json", "sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", "size": 3150},
{"path": "trust_boundaries.mmd", "sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", "size": 941}
]
}Sort members by Unicode-normalized relative path and hash the canonical JSON bytes to obtain package_digest. The manifest must enumerate the complete policy-applicable member population; an unlisted file or missing required document is an incomplete package.
Advance and read back the governed current head
{
"schema_version": "aidefend.threat-model-head.v1",
"system_scope_id": "fraud-prod",
"release_digest": "sha256:3333333333333333333333333333333333333333333333333333333333333333",
"authority_generation": "threat-model-publisher-2026-08-28",
"previous_package_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444",
"current_package_digest": "sha256:5555555555555555555555555555555555555555555555555555555555555555",
"head_generation": 42,
"published_at": "2026-08-28T22:10:00Z"
}Action: Submit the immutable package digest to the governed publisher with compare-and-swap against the prior head, then independently read back the current head and require the exact system_scope_id, release digest, package digest, authority generation, and monotonically advanced head generation. Preserve the verified manifest and readback receipt. A locally signed package that was not published as the current governed head cannot produce PASS; conflict, stale currentness, missing members, or readback mismatch is a finding.
AID-M-004-G010
Purpose and boundary
This step defines the material-change gate that requires a current, structurally complete threat model before promotion. CI product syntax is environment-specific; the portable deliverable is the signed change-trigger policy, validator contract, and gate receipt rather than a workflow that calls undefined local scripts.
Define the material-change gate contract
# File: threat_model/schemas/material-change-gate.schema.yaml
schema_version: aidefend.material-change-gate-schema.v1
policy_required_fields:
- policy_id
- policy_version
- policy_digest
- material_change_categories
- required_threat_model_sections
- required_risk_fields
- freshness_policy
- authorized_exception_roles
change_input_required_fields:
- candidate_release_digest
- base_release_digest
- changed_artifact_manifest_digest
- architecture_diff_digest
- dependency_diff_digest
- data_flow_diff_digest
- capability_diff_digest
- threat_model_package_digest
gate_checks:
- classify_every_changed_artifact
- reject_unknown_change_category
- bind_threat_model_to_candidate_release
- require_all_policy_sections
- require_owner_for_every_open_risk
- require_signed_exception_for_deferred_risk
- verify_review_freshness_from_policy
receipt_required_fields:
- candidate_release_digest
- policy_digest
- expected_change_count
- evaluated_change_count
- uncovered_change_ids
- threat_model_package_digest
- validator_version_digest
- result
- verifier_identityThe CI adapter must fail closed when it cannot produce the architecture, dependency, data-flow, capability, or changed-artifact diff. A threat-model file timestamp or upstream workflow success is not proof of currency; the independent validator must verify its signature, release binding, structure, risk ownership, and exact change coverage.
Action: Connect the gate contract to the organization's promotion system and retain the signed decision and raw diffs. No candidate release may advance on a missing, stale, malformed, or partially evaluated threat-model package.
AID-M-004-G011
Purpose and boundary
This step defines event-driven threat-model review and closure. Do not poll mutable public web pages, trust an unsigned ETag, or treat a network failure as no change. Threat-catalog inputs must be immutable, detached-signed releases admitted by a separate source-verification process.
Define the signed review-trigger contract
# File: threat_model/schemas/review-trigger-contract.schema.yaml
schema_version: aidefend.threat-model-review-trigger-schema.v1
policy_required_fields:
- policy_id
- policy_version
- policy_digest
- freshness_policy
- material_change_categories
- admitted_threat_catalog_signers
- required_reviewer_roles
trigger_types:
- candidate_architecture_change
- data_source_or_schema_change
- deployment_or_user_scope_change
- agent_capability_or_tool_change
- identity_or_authority_change
- dependency_or_supplier_change
- admitted_threat_catalog_release
- policy_freshness_expiry
trigger_required_fields:
- trigger_id
- trigger_type
- affected_asset_id
- affected_version_or_digest
- source_receipt_digest
- observed_at
- required_by
review_closure_required_fields:
- trigger_id
- threat_model_before_digest
- threat_model_after_digest
- changed_scenario_ids
- changed_risk_ids
- reviewer_role_population_digest
- approval_receipt_digest
- closed_at
catalog_release_required_fields:
- catalog_id
- catalog_version
- immutable_bundle_digest
- detached_signature_digest
- verified_signer_identity
- admission_receipt_digestReconcile the complete trigger lifecycle
Build expected triggers from independently signed architecture, inventory, dependency, data-flow, capability, identity, policy-time, and admitted threat-catalog receipts. Compare them to review records by stable trigger ID. Include missed, duplicate, rejected, expired, and errored triggers; an issue, reminder, or owner self-report does not prove review completion.
Action: Sign the review closure only after an independent verifier confirms the updated threat-model digest, affected scenario and risk deltas, required reviewer-role coverage, and exact trigger-population reconciliation. Any unsigned catalog, failed source readback, or unmatched trigger is INSUFFICIENT_DATA or ERROR, never a quiet no-change result.
Open-source tools
- MITRE ATLAS Navigator
- OWASP Threat Dragon
- OWASP pytm
- Threat Composer
- Threagile
- PlantUML
Commercial / hosted tools
- Microsoft Threat Modeling Tool
- IriusRisk
- OneTrust AI Governance
Threat mappings
MITRE ATLAS
- N/A
MAESTRO
- N/A
OWASP LLM Top 10 2026
- N/A
OWASP ML Top 10 2023
- N/A
OWASP Top 10 for Agentic Applications 2026
- N/A
NIST Adversarial Machine Learning 2025
- N/A
Cisco Integrated AI Security and Safety Framework
- N/A
Google Secure AI Framework 2.0 - Risks
- N/A
Databricks AI Security Framework 3.0
- Governance 4.1: Lack of traceability and transparency of model assets
- Governance 4.2: Lack of end-to-end ML lifecycle
- Platform 12.5: Poor security in the software development lifecycle