AID-D-016.001: Agent Graph Baseline & New-Agent Discovery

Version 1.20261015 · Actionable sub-technique

Discover previously unseen agent identities and new communication edges by comparing authenticated service-mesh and registry observations with a versioned approved agent graph.

Pillars: infra, app

Lifecycle: operation

Scope boundary

Owns unknown or newly emergent agent identity and communication-edge discovery against a versioned approved graph. It does not score reputation or classify drift within identities already registered.

AID-D-011.003: AID-D-016.001 discovers identities and edges absent from the approved graph; AID-D-011.003 detects replica, namespace, workload, and image-lineage drift for identities already registered. A new identity is a rogue-agent discovery candidate rather than an ordinary registered-population variance.

AID-D-016.002: AID-D-016.001 establishes that an identity or edge is new or unknown; AID-D-016.002 combines that and other security signals into reputation and escalation evidence. Discovery is one input to scoring, not the containment recommendation itself.

AID-M-001.006: AID-D-016.001 discovers active or observed agent identities and communication edges from runtime graph behavior; AID-M-001.006 discovers organization-owned internal AI endpoints from private-network and control-plane scope. A reachable service endpoint is not necessarily an active agent node, and agent behavior can reveal identities that endpoint enumeration misses.

Implementation guidance

AID-D-016.001-G001

Maintain a versioned agent registry baseline and alert on unknown agent identities or new communication edges, using stable identity keys (SPIFFE IDs/service accounts) and time-windowed anomaly detection.
When and where to run this detector

Apply this detector where independently authenticated AI-agent workloads communicate through a service mesh, gateway, message bus, or agent protocol. The workload-identity/platform owner exports source-authenticated observations; the agent-governance owner signs the approved identity/edge baseline; detection engineering evaluates a fixed complete window at the telemetry fan-in point. Bind every result to the exact application or agent ecosystem asset/version, baseline, identity schemes, required collectors, and window. A signed architecture inventory proving no agent-to-agent communication path exists yields NOT_APPLICABLE.

Production graph-baseline evaluator
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")
AGENT_GRAPH_COMMAND_TIMEOUT_SECONDS = _aidefend_positive_float("D016001_AGENT_GRAPH_COMMAND_TIMEOUT_SECONDS")

# File: detection/evaluate_agent_graph.py

import hashlib
import json
import os
import subprocess
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
import hmac
import re
import tempfile


BASELINE = Path("policy/approved-agent-graph.json")
EVENTS = Path("artifacts/agent-graph/observations.jsonl")
RECEIPT = Path("artifacts/agent-graph/collection-receipt.json")
EVIDENCE = Path("artifacts/agent-graph/AID-D-016.001-evidence.json")


def digest(path: Path) -> str:
    captured = _VERIFIED_INPUTS.get(Path(path))
    payload = captured if captured is not None else Path(path).read_bytes()
    return hashlib.sha256(payload).hexdigest()


_VERIFIED_INPUTS: dict[Path, bytes] = {}


def _write_private_snapshot(path: Path, payload: bytes) -> None:
    descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
    with os.fdopen(descriptor, "wb") as stream:
        stream.write(payload)
        stream.flush()
        os.fsync(stream.fileno())
    os.chmod(path, 0o400)


def _strict_json(raw: bytes | str) -> object:
    text = raw.decode("utf-8", errors="strict") if isinstance(raw, bytes) else raw

    def unique_object(pairs):
        value = {}
        for key, item in pairs:
            if key in value:
                raise ValueError("duplicate JSON key: " + key)
            value[key] = item
        return value

    return json.loads(
        text,
        object_pairs_hook=unique_object,
        parse_constant=lambda value: (_ for _ in ()).throw(
            ValueError("non-finite JSON constant: " + value)
        ),
    )


def verify(path: Path, key_env: str) -> None:
    payload_path = Path(path)
    signature_path = Path(str(payload_path) + ".sig")
    payload_raw = payload_path.read_bytes()
    signature_raw = signature_path.read_bytes()
    key_reference = os.environ[key_env]
    remote_key = bool(re.match(
        r"^[A-Za-z][A-Za-z0-9+.-]*://", key_reference
    )) or key_reference.startswith("pkcs11:")
    key_raw = None if remote_key else (
        key_reference.encode("utf-8")
        if key_reference.startswith("-----BEGIN ")
        else Path(key_reference).read_bytes()
    )
    with tempfile.TemporaryDirectory(prefix="aidefend-d016001-") as directory:
        root = Path(directory)
        os.chmod(root, 0o700)
        payload_snapshot = root / "payload"
        signature_snapshot = root / "signature"
        _write_private_snapshot(payload_snapshot, payload_raw)
        _write_private_snapshot(signature_snapshot, signature_raw)
        captured = [(payload_snapshot, payload_raw), (signature_snapshot, signature_raw)]
        key_argument = key_reference
        if key_raw is not None:
            key_snapshot = root / "verify-key.pub"
            _write_private_snapshot(key_snapshot, key_raw)
            captured.append((key_snapshot, key_raw))
            key_argument = str(key_snapshot)
        try:
            subprocess.run(
                [
                    "cosign", "verify-blob", "--key", key_argument,
                    "--bundle", str(signature_snapshot), str(payload_snapshot),
                ],
                check=True, capture_output=True, text=True, timeout=AGENT_GRAPH_COMMAND_TIMEOUT_SECONDS,
            )
            if any(
                not hmac.compare_digest(candidate.read_bytes(), expected)
                for candidate, expected in captured
            ):
                raise RuntimeError("private signature-verification snapshot changed")
        finally:
            for candidate, _ in captured:
                os.chmod(candidate, 0o600)
    _VERIFIED_INPUTS[payload_path] = payload_raw


def _verified_text(path: Path) -> str:
    try:
        return _VERIFIED_INPUTS[Path(path)].decode("utf-8", errors="strict")
    except KeyError as error:
        raise RuntimeError("signed input was not captured and verified") from error


def timestamp(value: object) -> datetime:
    if not isinstance(value, str):
        raise ValueError("timestamp is absent")
    parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
    if parsed.tzinfo is None:
        raise ValueError("timestamp lacks timezone")
    return parsed.astimezone(timezone.utc)


def load_inputs() -> tuple[dict, list[dict], dict]:
    verify(BASELINE, "D016001_BASELINE_VERIFY_KEY")
    verify(EVENTS, "D016001_COLLECTOR_VERIFY_KEY")
    verify(RECEIPT, "D016001_COMPLETENESS_VERIFY_KEY")
    baseline = _strict_json(_VERIFIED_INPUTS[BASELINE])
    receipt = _strict_json(_VERIFIED_INPUTS[RECEIPT])
    baseline_fields = {
        "schema_version", "baseline_version", "asset_id", "asset_version",
        "directed", "identity_schemes", "identities", "edges",
        "required_sources", "window_seconds", "minimum_observation_events",
        "maximum_evidence_age_seconds", "issued_at", "expires_at",
    }
    if (
        not isinstance(baseline, dict)
        or set(baseline) != baseline_fields
        or baseline["schema_version"] != "aidefend.agent-graph-baseline.v1"
        or not isinstance(baseline["directed"], bool)
    ):
        raise ValueError("agent graph baseline schema differs")
    for field in ("baseline_version", "asset_id", "asset_version"):
        if not isinstance(baseline[field], str) or not baseline[field]:
            raise ValueError(f"baseline {field} is absent")
    schemes = baseline["identity_schemes"]
    identities = baseline["identities"]
    sources = baseline["required_sources"]
    if (
        not isinstance(schemes, list)
        or not schemes
        or any(not isinstance(value, str) or not value.endswith("://") for value in schemes)
        or len(schemes) != len(set(schemes))
        or not isinstance(identities, list)
        or not identities
        or any(
            not isinstance(value, str)
            or not value
            or not any(value.startswith(scheme) for scheme in schemes)
            for value in identities
        )
        or len(identities) != len(set(identities))
        or not isinstance(sources, list)
        or not sources
        or any(not isinstance(value, str) or not value for value in sources)
        or len(sources) != len(set(sources))
    ):
        raise ValueError("baseline identity or source registry is invalid")
    for field in (
        "window_seconds", "minimum_observation_events",
        "maximum_evidence_age_seconds",
    ):
        if (
            isinstance(baseline[field], bool)
            or not isinstance(baseline[field], int)
            or baseline[field] < 1
        ):
            raise ValueError(f"baseline {field} is invalid")
    now = datetime.now(timezone.utc)
    issued, expires = timestamp(baseline["issued_at"]), timestamp(baseline["expires_at"])
    if not issued <= now < expires:
        raise ValueError("agent graph baseline is not currently valid")
    identity_set = set(identities)
    edge_set = set()
    if not isinstance(baseline["edges"], list):
        raise ValueError("baseline edges are not a list")
    for edge in baseline["edges"]:
        if (
            not isinstance(edge, list)
            or len(edge) != 2
            or edge[0] not in identity_set
            or edge[1] not in identity_set
        ):
            raise ValueError("baseline edge is invalid")
        key = tuple(edge) if baseline["directed"] else tuple(sorted(edge))
        if key in edge_set:
            raise ValueError("baseline edge is duplicated")
        edge_set.add(key)

    receipt_fields = {
        "schema_version", "asset_id", "asset_version", "events_sha256",
        "window_start", "window_end", "expected_sources", "source_counts",
        "source_checkpoints", "event_count", "collection_complete",
    }
    if (
        not isinstance(receipt, dict)
        or set(receipt) != receipt_fields
        or receipt["schema_version"] != "aidefend.agent-graph-collection.v1"
        or receipt["asset_id"] != baseline["asset_id"]
        or receipt["asset_version"] != baseline["asset_version"]
        or receipt["events_sha256"] != digest(EVENTS)
        or receipt["expected_sources"] != sources
        or not isinstance(receipt["collection_complete"], bool)
        or isinstance(receipt["event_count"], bool)
        or not isinstance(receipt["event_count"], int)
        or receipt["event_count"] < 0
        or not isinstance(receipt["source_counts"], dict)
        or set(receipt["source_counts"]) != set(sources)
        or any(
            isinstance(value, bool) or not isinstance(value, int) or value < 0
            for value in receipt["source_counts"].values()
        )
        or not isinstance(receipt["source_checkpoints"], dict)
        or set(receipt["source_checkpoints"]) != set(sources)
    ):
        raise ValueError("agent graph receipt is invalid or misbound")
    start, end = timestamp(receipt["window_start"]), timestamp(receipt["window_end"])
    if int((end - start).total_seconds()) != baseline["window_seconds"]:
        raise ValueError("observation window differs from signed baseline")
    if (
        end > now
        or (now - end).total_seconds()
        > baseline["maximum_evidence_age_seconds"]
    ):
        raise ValueError("agent graph window is future-dated or stale")

    lines = _verified_text(EVENTS).splitlines()
    if any(not line.strip() for line in lines):
        raise ValueError("agent graph JSONL contains a blank record")
    events = [_strict_json(line) for line in lines]
    if len(events) != receipt["event_count"]:
        raise ValueError("agent graph event count differs")
    event_fields = {
        "schema_version", "event_id", "source", "source_sequence",
        "observed_at", "source_identity", "target_identity",
        "source_authn_evidence_sha256", "target_authn_evidence_sha256",
    }
    if any(
        not isinstance(item, dict)
        or set(item) != event_fields
        or item["schema_version"] != "aidefend.agent-graph-observation.v1"
        for item in events
    ):
        raise ValueError("agent graph event schema differs")
    event_ids = [item["event_id"] for item in events]
    if len(event_ids) != len(set(event_ids)) or any(not isinstance(value, str) or not value for value in event_ids):
        raise ValueError("agent graph event IDs are absent or duplicated")
    for item in events:
        if item["source"] not in sources or not start <= timestamp(item["observed_at"]) < end:
            raise ValueError("agent graph event source or time is outside receipt")
        if (
            isinstance(item["source_sequence"], bool)
            or not isinstance(item["source_sequence"], int)
            or item["source_sequence"] < 0
        ):
            raise ValueError("agent graph source sequence is invalid")
        for field in ("source_identity", "target_identity"):
            value = item[field]
            if (
                not isinstance(value, str)
                or not any(value.startswith(scheme) for scheme in schemes)
            ):
                raise ValueError("agent identity scheme is invalid")
        for field in ("source_authn_evidence_sha256", "target_authn_evidence_sha256"):
            value = item[field]
            if (
                not isinstance(value, str)
                or re.fullmatch(r"[0-9a-f]{64}", value) is None
            ):
                raise ValueError("authentication evidence digest is invalid")
    observed_counts = dict(sorted(Counter(item["source"] for item in events).items()))
    expected_counts = {
        source: receipt["source_counts"][source]
        for source in sources
        if receipt["source_counts"][source] > 0
    }
    if observed_counts != dict(sorted(expected_counts.items())):
        raise ValueError("per-source population differs")
    checkpoint_fields = {"first", "last", "complete"}
    for source in sources:
        checkpoint = receipt["source_checkpoints"][source]
        if (
            not isinstance(checkpoint, dict)
            or set(checkpoint) != checkpoint_fields
            or not isinstance(checkpoint["complete"], bool)
        ):
            raise ValueError("source checkpoint schema differs")
        source_sequences = sorted(
            item["source_sequence"] for item in events if item["source"] == source
        )
        if source_sequences:
            if (
                isinstance(checkpoint["first"], bool)
                or not isinstance(checkpoint["first"], int)
                or isinstance(checkpoint["last"], bool)
                or not isinstance(checkpoint["last"], int)
                or source_sequences
                != list(range(checkpoint["first"], checkpoint["last"] + 1))
            ):
                raise ValueError("source sequence population is incomplete")
        elif checkpoint["first"] is not None or checkpoint["last"] is not None:
            raise ValueError("empty source has non-empty checkpoint bounds")
    return baseline, events, receipt


def evaluate(baseline: dict, events: list[dict], receipt: dict) -> dict:
    if (
        receipt["collection_complete"] is not True
        or any(
            checkpoint["complete"] is not True
            for checkpoint in receipt["source_checkpoints"].values()
        )
    ):
        return {"status": "INSUFFICIENT_DATA", "reason": "required source collection is incomplete", "findings": []}
    if len(events) < baseline["minimum_observation_events"]:
        return {"status": "INSUFFICIENT_DATA", "reason": "observation population is below signed minimum", "findings": []}
    known_identities = set(baseline["identities"])
    known_edges = {
        tuple(edge) if baseline["directed"] else tuple(sorted(edge))
        for edge in baseline["edges"]
    }
    unknown_identities, new_edges, findings = set(), set(), []
    for item in events:
        source, target = item["source_identity"], item["target_identity"]
        for identity in (source, target):
            if identity not in known_identities:
                unknown_identities.add(identity)
        edge = (source, target) if baseline["directed"] else tuple(sorted((source, target)))
        if edge not in known_edges:
            new_edges.add(edge)
    if unknown_identities:
        findings.append("unknown_agent_identity")
    if new_edges:
        findings.append("new_agent_communication_edge")
    return {
        "status": "FAIL" if findings else "PASS",
        "reason": "agent graph baseline finding exists" if findings
        else "complete agent graph window matched the signed baseline",
        "findings": findings,
        "unknown_identities": sorted(unknown_identities),
        "new_edges": [list(edge) for edge in sorted(new_edges)],
    }


def main() -> None:
    try:
        baseline, events, receipt = load_inputs()
        result = evaluate(baseline, events, receipt)
        evidence = {
            "schema_version": "aidefend.d016001-evidence.v1",
            "control": "AID-D-016.001",
            "asset_id": baseline["asset_id"],
            "asset_version": baseline["asset_version"],
            "baseline_version": baseline["baseline_version"],
            "window_start": receipt["window_start"],
            "window_end": receipt["window_end"],
            "event_population": len(events),
            "baseline_sha256": digest(BASELINE),
            "events_sha256": digest(EVENTS),
            "receipt_sha256": digest(RECEIPT),
            **result,
            "evaluated_at": datetime.now(timezone.utc).isoformat(),
        }
    except Exception as error:
        evidence = {
            "schema_version": "aidefend.d016001-evidence.v1",
            "control": "AID-D-016.001",
            "status": "ERROR",
            "reason": f"{type(error).__name__}: {error}",
            "evaluated_at": datetime.now(timezone.utc).isoformat(),
        }
    EVIDENCE.parent.mkdir(parents=True, exist_ok=True)
    EVIDENCE.write_text(json.dumps(evidence, sort_keys=True, separators=(",", ":")) + "\n", encoding="utf-8")
    subprocess.run(
        [
            "cosign", "sign-blob", "--yes",
            "--key", "env://D016001_EVIDENCE_SIGNING_KEY",
            "--bundle", str(EVIDENCE) + ".sig", str(EVIDENCE),
        ],
        check=True, timeout=AGENT_GRAPH_COMMAND_TIMEOUT_SECONDS,
    )
    verify(EVIDENCE, "D016001_EVIDENCE_VERIFY_KEY")


if __name__ == "__main__":
    main()
Replay and interpret results

Action: At the authenticated agent-graph collector, compare every observed identity and communication edge in the reconciled window with the signed baseline, persist the signed delta result, and emit unknown-identity and new-edge findings to the agent-security queue; containment remains with AID-I-003.

Cross-run mutable-state detection

Include authenticated observations for applicable shared registries, mutable tags, manifests, notes, queues, and coordination metadata. Key them by tenant, principal, run, object generation, and operation, and flag a new communication or state edge when one run writes state another run can consume without an approved handoff. Do not flag a digest-addressed immutable cache merely because several runs read it. Authorization and write denial remain with AID-H-018.002 and service IAM; this guidance emits the finding and preserves the observed edge only.

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-D-016.001: Agent Graph Baseline & New-Agent Discovery | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version