AID-D-001.006: Prompt Gate Decision Telemetry Correlation
Pillars: app
Lifecycle: operation
Scope boundary
Owns cross-request, cross-session, and cross-identity correlation of synchronous prompt-gate verdict telemetry to detect evasive probing, repeated near misses, and cumulative policy risk. It does not block traffic or replace the inline gate.
AID-D-001.001: AID-D-001.006 correlates the history of prompt-analysis and gate outcomes; AID-D-001.001 analyzes the content, intent, and obfuscation of an individual prompt. Correlation detects campaign behavior that no single request establishes.
AID-D-001.004: AID-D-001.006 detects repeated or cumulative patterns across gate events; AID-D-001.004 emits a request-scoped canary integrity finding. A canary anomaly is one input to correlation, not the correlation control itself.
Implementation guidance
- AID-D-001.006-G001: Ingest and analyze synchronous gate decisions to detect evasions, correlate incidents, and escalate.
AID-D-001.006-G001
Verify the signed policy and authenticated gate event
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")
DECISION_ANALYSIS_COMMAND_TIMEOUT_SECONDS = _aidefend_positive_float("D001006_DECISION_ANALYSIS_COMMAND_TIMEOUT_SECONDS")
# File: detect/gate_decision_consumer.py
import hashlib
import json
import math
import os
import subprocess
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from uuid import UUID
import jwt
import redis
POLICY_PATH = Path(os.environ["D001006_POLICY_PATH"])
POLICY_SIGNATURE = Path(os.environ["D001006_POLICY_SIGNATURE"])
POLICY_VERIFY_KEY = os.environ["D001006_POLICY_VERIFY_KEY"]
EVENT_VERIFY_KEY = Path(os.environ["D001006_EVENT_VERIFY_KEY"]).read_text(encoding="utf-8")
EVENT_ISSUER = os.environ["D001006_EVENT_ISSUER"]
EVENT_AUDIENCE = "aidefend:d001006"
REDIS = redis.Redis.from_url(os.environ["D001006_REDIS_URL"], decode_responses=True)
POLICY_FIELDS = {
"schema_version", "policy_version", "applicable_routes",
"applicable_model_prefixes", "window_seconds", "minimum_events",
"max_event_age_seconds", "max_future_skew_seconds", "replay_ttl_seconds",
"deny_burst_threshold", "soft_flag_variant_threshold",
"cumulative_risk_threshold", "policy_code_weights",
}
EVENT_FIELDS = {
"schema_version", "event_id", "request_id", "session_id", "user_id",
"observed_at", "verdict", "policy_code", "normalized_hash", "route",
"model", "signals", "iss", "aud", "iat", "nbf", "exp", "jti",
}
def sha256_bytes(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def canonical(value: dict) -> str:
return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False)
def verify_blob(path: Path, signature: Path, key: str) -> None:
subprocess.run([
"cosign", "verify-blob", "--key", key,
"--bundle", str(signature), str(path),
], check=True, capture_output=True, text=True, timeout=DECISION_ANALYSIS_COMMAND_TIMEOUT_SECONDS)
def positive_int(value: object, field: str) -> int:
if isinstance(value, bool) or not isinstance(value, int) or value < 1:
raise ValueError(f"{field} must be a positive integer")
return value
def reject_duplicate_keys(pairs):
value = {}
for key, item in pairs:
if key in value:
raise ValueError(f"duplicate JSON key: {key}")
value[key] = item
return value
def load_policy() -> tuple[dict, str]:
# Authenticate and parse the same immutable byte snapshot.
raw = POLICY_PATH.read_bytes()
signature_raw = POLICY_SIGNATURE.read_bytes()
with tempfile.TemporaryDirectory(prefix="aidefend-d001006-") as directory:
snapshot = Path(directory) / "policy.json"
signature = Path(directory) / "policy.sig"
snapshot.write_bytes(raw)
signature.write_bytes(signature_raw)
verify_blob(snapshot, signature, POLICY_VERIFY_KEY)
policy = json.loads(
raw.decode("utf-8", errors="strict"),
object_pairs_hook=reject_duplicate_keys, parse_constant=lambda value: (_ for _ in ()).throw(ValueError('non-finite JSON constant: ' + value)),
)
if not isinstance(policy, dict) or set(policy) != POLICY_FIELDS:
raise ValueError("correlation policy schema mismatch")
if policy["schema_version"] != "aidefend.d001006-policy.v1":
raise ValueError("unsupported correlation policy")
for field in (
"window_seconds", "minimum_events", "max_event_age_seconds",
"max_future_skew_seconds", "replay_ttl_seconds",
"deny_burst_threshold", "soft_flag_variant_threshold",
):
positive_int(policy[field], field)
threshold = policy["cumulative_risk_threshold"]
if isinstance(threshold, bool) or not isinstance(threshold, (int, float)) or not math.isfinite(float(threshold)) or threshold <= 0:
raise ValueError("cumulative risk threshold invalid")
for field in ("applicable_routes", "applicable_model_prefixes"):
values = policy[field]
if not isinstance(values, list) or not values or len(values) != len(set(values)) or any(not isinstance(item, str) or not item for item in values):
raise ValueError(f"{field} invalid")
weights = policy["policy_code_weights"]
if not isinstance(weights, dict) or not weights:
raise ValueError("policy-code weights missing")
for code, weight in weights.items():
if not isinstance(code, str) or not code or isinstance(weight, bool) or not isinstance(weight, (int, float)) or not math.isfinite(float(weight)) or weight < 0:
raise ValueError("policy-code weight invalid")
return policy, sha256_bytes(raw)
def parse_time(value: object) -> datetime:
if not isinstance(value, str):
raise ValueError("observed_at missing")
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
if parsed.tzinfo is None:
raise ValueError("observed_at lacks timezone")
return parsed.astimezone(timezone.utc)
def decode_event(compact_jws: bytes) -> dict[str, Any]:
token = compact_jws.decode("ascii", errors="strict")
header = jwt.get_unverified_header(token)
if header.get("alg") != "EdDSA" or header.get("typ") != "gate-event+jwt":
raise ValueError("unexpected gate-event envelope")
event = jwt.decode(
token, EVENT_VERIFY_KEY, algorithms=["EdDSA"],
issuer=EVENT_ISSUER, audience=EVENT_AUDIENCE,
options={"require": ["iss", "aud", "iat", "nbf", "exp", "jti"]},
)
if not isinstance(event, dict) or set(event) != EVENT_FIELDS:
raise ValueError("gate-event schema mismatch")
if event["schema_version"] != "aidefend.gate-event.v1":
raise ValueError("unsupported gate-event schema")
for field in ("event_id", "request_id", "session_id", "normalized_hash", "route", "model"):
if not isinstance(event[field], str) or not event[field]:
raise ValueError(f"{field} missing")
UUID(event["event_id"])
UUID(event["request_id"])
if event["verdict"] not in {"allow", "deny", "soft_flag"}:
raise ValueError("invalid gate verdict")
if event["policy_code"] is not None and (not isinstance(event["policy_code"], str) or not event["policy_code"]):
raise ValueError("invalid policy code")
if not isinstance(event["signals"], dict):
raise ValueError("signals must be an object")
if len(event["normalized_hash"]) != 64:
raise ValueError("normalized_hash must be SHA-256")
int(event["normalized_hash"], 16)
return event
Correlate idempotently across replicas
# File: detect/gate_decision_runner.py
import json
from datetime import datetime, timezone
from detect.gate_decision_consumer import (
REDIS,
canonical,
decode_event,
load_policy,
parse_time,
sha256_bytes,
)
def outcome(
control_status: str,
finding_status: str,
reason: str,
**evidence: object,
) -> dict:
return {
"schema_version": "aidefend.d001006-result.v1",
"control": "AID-D-001.006",
"control_status": control_status,
"finding_status": finding_status,
"reason": reason,
**evidence,
}
def correlate(compact_jws: bytes, *, now: datetime | None = None) -> dict:
current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
policy, policy_sha256 = load_policy()
event = decode_event(compact_jws)
observed = parse_time(event["observed_at"])
age = (current - observed).total_seconds()
if age > policy["max_event_age_seconds"] or age < -policy["max_future_skew_seconds"]:
raise ValueError("gate event outside signed freshness bounds")
if event["route"] not in policy["applicable_routes"] or not any(event["model"].startswith(prefix) for prefix in policy["applicable_model_prefixes"]):
return outcome("NOT_APPLICABLE", "NOT_EVALUATED", "route or model outside signed applicability", event_id=event["event_id"], policy_sha256=policy_sha256)
event_digest = sha256_bytes(compact_jws)
replay_key = f"d001006:event:{event['event_id']}"
claimed = REDIS.set(replay_key, event_digest, nx=True, ex=policy["replay_ttl_seconds"])
if not claimed:
if REDIS.get(replay_key) != event_digest:
raise ValueError("event_id replayed with different authenticated bytes")
cached = REDIS.get(f"d001006:result:{event['event_id']}")
if cached is None:
raise RuntimeError("idempotent event is still being evaluated")
return json.loads(cached)
window_key = f"d001006:session:{event['session_id']}"
member = canonical({
"event_id": event["event_id"], "verdict": event["verdict"],
"policy_code": event["policy_code"], "normalized_hash": event["normalized_hash"],
})
score = observed.timestamp()
with REDIS.pipeline(transaction=True) as pipe:
pipe.zadd(window_key, {member: score})
pipe.zremrangebyscore(window_key, "-inf", score - policy["window_seconds"])
pipe.expire(window_key, policy["replay_ttl_seconds"])
pipe.zrange(window_key, 0, -1)
window = [json.loads(item) for item in pipe.execute()[-1]]
findings: list[str] = []
deny_count = sum(item["verdict"] == "deny" for item in window)
soft_hashes = {item["normalized_hash"] for item in window if item["verdict"] == "soft_flag"}
cumulative = 0.0
unmapped_codes = set()
for item in window:
if item["verdict"] not in {"deny", "soft_flag"} or item["policy_code"] is None:
continue
if item["policy_code"] not in policy["policy_code_weights"]:
unmapped_codes.add(item["policy_code"])
continue
cumulative += float(policy["policy_code_weights"][item["policy_code"]])
if unmapped_codes:
result = outcome("ERROR", "UNKNOWN", "window contains unmapped policy codes", event_id=event["event_id"], unmapped_policy_codes=sorted(unmapped_codes), policy_sha256=policy_sha256)
elif len(window) < policy["minimum_events"]:
result = outcome("INSUFFICIENT_DATA", "UNKNOWN", "valid window below signed minimum", event_id=event["event_id"], observation_count=len(window), policy_sha256=policy_sha256)
else:
if deny_count >= policy["deny_burst_threshold"]:
findings.append("deny_burst")
if len(soft_hashes) >= policy["soft_flag_variant_threshold"]:
findings.append("soft_flag_variant_evasion")
if cumulative >= float(policy["cumulative_risk_threshold"]):
findings.append("cumulative_policy_risk")
result = outcome("PASS", "FINDING" if findings else "NO_FINDING", "correlation finding" if findings else "no signed threshold breached", event_id=event["event_id"], session_id=event["session_id"], observation_count=len(window), findings=findings, policy_sha256=policy_sha256)
REDIS.set(f"d001006:result:{event['event_id']}", canonical(result), ex=policy["replay_ttl_seconds"])
return result
def safe_correlate(compact_jws: bytes) -> dict:
try:
return correlate(compact_jws)
except Exception as exc:
return outcome("ERROR", "UNKNOWN", "gate event, policy, replay, storage, or evaluation invalid", error_class=type(exc).__name__)
Verify safely
Retain the compact JWS digest, signed policy/signature, policy digest, exact event and session identifiers, window population, Redis replay result, findings, and evaluator version. An independent identity re-verifies the event and policy signatures, replays the same bounded window from immutable events, and compares the result digest.
Verify complete coverage
For this detector, evaluate every applicable gate-decision event between signed source checkpoints for each route, model, session, and correlation window, including allow, deny, soft-flag, duplicate, late, and processing-error events. Require a source-authenticated completeness manifest binding the asset and version, request or window, start and end checkpoints, expected IDs, evaluated IDs, exclusions, and canonical digests.
Open-source tools
- Apache Kafka
- Prometheus
- Grafana
- Valkey
- Redis 8+ AGPLv3 source build
- ClickHouse
- OpenSearch
- Elasticsearch 8.16+ AGPLv3 source build
- PyJWT (event-claim verification)
- Cosign (signed inventory and policy verification)
Commercial / hosted tools
- Splunk Enterprise Security
- Microsoft Sentinel
- Google Security Operations
- Datadog Cloud SIEM
Threat mappings
MITRE ATLAS
- AML.T0051 LLM Prompt Injection
- AML.T0051.000 LLM Prompt Injection: Direct
- AML.T0051.001 LLM Prompt Injection: Indirect
- AML.T0051.002 LLM Prompt Injection: Triggered
- AML.T0054 LLM Jailbreak
- AML.T0068 LLM Prompt Obfuscation
- AML.T0015 Evade AI Model
MAESTRO
- Input Validation Attacks (L3)
- Reprogramming Attacks (L1)
- Evasion of Security AI Agents (L6)
OWASP LLM Top 10 2026
- LLM01:2026 Prompt Injection (cross-session gate correlation exposes evasive probing and repeated near misses)
OWASP ML Top 10 2023
- ML01:2023 Input Manipulation Attack
OWASP Top 10 for Agentic Applications 2026
- ASI01:2026 Agent Goal Hijack
NIST Adversarial Machine Learning 2025
- NISTAML.018 Prompt Injection
- NISTAML.015 Indirect Prompt Injection
- NISTAML.022 Evasion
Cisco Integrated AI Security and Safety Framework
- AITech-1.1 Direct Prompt Injection and Jailbreak (Prompt-based Manipulation) (stateful cross-turn prompt-risk accumulation within the retained session)
- AITech-9.2 Detection Evasion (stateful cross-turn prompt-risk accumulation within the retained session)
Google Secure AI Framework 2.0 - Risks
- PIJ: Prompt Injection
- MEV: Model Evasion
Databricks AI Security Framework 3.0
- Model Serving - Inference requests 9.1: Prompt inject
- Model Serving - Inference requests 9.12: LLM Jailbreak
- Agents - Core 13.6: Intent Breaking & Goal Manipulation