Tutorial

AutoGen Security Tool: CVE and Supply Chain Checks with Attestd

RobertUpdated Jun 5, 20264 min read
"Dark terminal-style feature image. White text reads: Multi-agent workflows need a security gate. Below it in teal monospace: autogen-agentchat 0.7.5. Attestd branding bottom left

How to Give Your AutoGen Agent a Security Gate#

Microsoft is moving long-term agent development toward the Microsoft Agent Framework. AutoGen 0.7.5 is still where most Python multi-agent code runs today: roughly 1.3 million monthly downloads, stable APIs, and the patterns teams already ship.

AutoGen multi-agent workflows are where security gates matter most. A coding agent that deploys a compromised dependency can propagate that compromise across every other agent in the workflow that trusts its output. Without a structured check, the LLM is guessing from training data.

This tutorial builds an AutoGen FunctionTool that wraps the Attestd API and shows both a single-agent check and a multi-agent safety gate pattern.


What you will need#

bash
pip install "autogen-agentchat==0.7.5" autogen-core "autogen-ext[openai]" attestd

An Attestd API key from api.attestd.io/portal/login. Free tier, 1,000 calls a month.


What the tool returns#

The Attestd API returns a deterministic risk_state derived from NVD CVE data and the CISA KEV catalog. For PyPI and npm packages it also returns a supply_chain object indicating whether the version was a known malicious publish.

Two signals, independent of each other. A package can have risk_state: "none" and supply_chain.compromised: true at the same time. litellm 1.82.7 is exactly that case:

json
{
  "product": "litellm",
  "version": "1.82.7",
  "risk_state": "none",
  "supply_chain": {
    "compromised": true,
    "sources": ["osv", "registry"],
    "malware_type": "backdoor",
    "description": "TeamPCP supply chain attack. Credential stealer in proxy_server.py",
    "compromised_at": "2026-03-24T10:39:00Z",
    "removed_at": "2026-03-24T16:00:00Z"
  }
}

No CVEs. Still malicious. A risk_state check alone would have passed it.


Pattern 1: Building the tool#

Instantiate Client once at module level. Never instantiate inside the function on every call.

python
import os
import attestd
from autogen_core.tools import FunctionTool
from attestd import AttestdUnsupportedProductError

_client = attestd.Client(api_key=os.environ["ATTESTD_API_KEY"])

def check_package_vulnerability(product: str, version: str) -> dict:
    """Check whether a software package version has known CVE vulnerabilities
    or supply chain compromise. Use before deploying or recommending any
    software dependency. outside_coverage=True means no data. Treat as
    unknown risk, not safe."""
    try:
        result = _client.check(product, version)
        return {
            "outside_coverage": False,
            "risk_state": result.risk_state,
            "actively_exploited": result.actively_exploited,
            "patch_available": result.patch_available,
            "fixed_version": result.fixed_version,
            "supply_chain_compromised": (
                result.supply_chain.compromised
                if result.supply_chain is not None
                else False
            ),
        }
    except AttestdUnsupportedProductError:
        return {
            "outside_coverage": True,
            "risk_state": None,
            "message": f"No Attestd coverage for '{product}'. Treat as unknown risk.",
        }

attestd_tool = FunctionTool(
    check_package_vulnerability,
    description=(
        "Check CVE risk and supply chain integrity for a software dependency. "
        "outside_coverage=True means unknown risk, not safe."
    ),
)

AttestdUnsupportedProductError must be caught. An agent that raises on an unsupported product is not production-ready. The outside_coverage: True return gives the agent a signal it can branch on: no data from Attestd is unknown risk, not a clean bill of health.


Pattern 2: Single agent#

One AssistantAgent with the Attestd tool in a RoundRobinGroupChat. AutoGen 0.7.5 is async-first: use await team.run_stream(...).

python
import asyncio
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.conditions import TextMentionTermination
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_agentchat.ui import Console
from autogen_ext.models.openai import OpenAIChatCompletionClient

model_client = OpenAIChatCompletionClient(model="gpt-4o-mini")

security_agent = AssistantAgent(
    name="security_agent",
    model_client=model_client,
    tools=[attestd_tool],
    system_message=(
        "You are a security-aware deployment assistant. "
        "Before approving any software dependency, call check_package_vulnerability. "
        "Block if risk_state is 'critical' or 'high', or if supply_chain_compromised is True. "
        "If outside_coverage is True, state that explicitly. Do not treat it as safe. "
        "End with APPROVED or BLOCKED and your reasoning."
    ),
)

termination = (
    TextMentionTermination("APPROVED", sources=["security_agent"])
    | TextMentionTermination("BLOCKED", sources=["security_agent"])
)

team = RoundRobinGroupChat([security_agent], termination_condition=termination)

async def main() -> None:
    await Console(
        team.run_stream(task="Is it safe to deploy with runc 1.0.0 and litellm 1.82.7?")
    )

asyncio.run(main())

Use sources= on TextMentionTermination so the team does not terminate early if the user task happens to contain APPROVED or BLOCKED.


Pattern 3: Multi-agent safety gate#

A dedicated security_gate agent runs Attestd checks before a deployment_agent can proceed. In a multi-agent workflow, a compromised dependency approved by one agent propagates to every agent that trusts its output.

python
security_gate = AssistantAgent(
    name="security_gate",
    model_client=model_client