Skip to content

spec: add agent-threat-scan predicate type - #552

Open
eeee2345 wants to merge 1 commit into
in-toto:mainfrom
eeee2345:add-agent-threat-scan-predicate
Open

spec: add agent-threat-scan predicate type#552
eeee2345 wants to merge 1 commit into
in-toto:mainfrom
eeee2345:add-agent-threat-scan-predicate

Conversation

@eeee2345

@eeee2345 eeee2345 commented May 9, 2026

Copy link
Copy Markdown

This PR adds a new vetted predicate type, agent-threat-scan, that records the result of scanning an AI agent artifact (such as a Model Context Protocol server, a Claude Code skill, or a similar agent configuration manifest) against a named detection ruleset for agent-specific threats including prompt injection, tool poisoning, MCP request forgery, and skill compromise. Existing predicates do not capture this shape: vulns is oriented around CVE scanners on conventional packages, test-result is generic, and svr records that policies passed without enumerating which rules matched on a fail.

The predicate is ruleset-agnostic. Any detection ruleset that issues stable rule identifiers and a citable version may be referenced through the ruleset.uri and ruleset.version fields. One concrete example of such a ruleset is Agent Threat Rules (ATR), an open detection standard for AI agent threats licensed Apache-2.0, available at https://github.com/Agent-Threat-Rule/agent-threat-rules. ATR is currently shipped in production at Cisco AI Defense and Microsoft agent-governance-toolkit. The predicate is not coupled to ATR; the same shape applies to any other ruleset.

The schema follows the in-toto predicate conventions (lowerCamelCase fields, RFC 3339 timestamp on scannedAt, ResourceURI for scanner.uri and ruleset.uri). The Example block parses as valid JSON. New file is spec/predicates/agent-threat-scan.md, registered through spec/predicates/README.md.

Predicate URI is https://in-toto.io/attestation/agent-threat-scan/v0.1. If accepted I will follow the documented step 4 and submit the in-toto.io URL redirect addition. Happy to revise field names, severity enum, or threat-class taxonomy based on maintainer feedback. I have read the New Predicate Guidelines and the existing predicates including svr, vulns, and test-result.

Adds a new predicate type for recording the result of scanning an AI
agent artifact (MCP server, Claude Code skill, or similar) against a
named detection ruleset. Ruleset-agnostic: references ruleset.uri and
ruleset.version so any detection standard issuing stable rule IDs can
be used.

Registers spec/predicates/agent-threat-scan.md and adds it to the
vetted predicates list in spec/predicates/README.md.

Predicate URI: https://in-toto.io/attestation/agent-threat-scan/v0.1

Signed-off-by: Adam Lin <adam@agentthreatrule.org>
@eeee2345
eeee2345 force-pushed the add-agent-threat-scan-predicate branch from 87c0499 to fcc16c1 Compare June 5, 2026 21:03
@eeee2345

Copy link
Copy Markdown
Author

Friendly ping on this one — it's been a few weeks since the last activity and I wanted to check whether anything's blocking on my end. Happy to rebase, split it into something smaller, or adjust the scope to match your conventions.

For context, Agent Threat Rules has seen meaningful ecosystem uptake since this PR opened — it was recently merged into Microsoft PyRIT, Tencent AI-Infra-Guard, and the Google ADK docs, among others — so I'd love to get this landed if it's a fit. No rush at all; just flagging that it's ready whenever you have a moment. Thanks for maintaining this!

@eeee2345

Copy link
Copy Markdown
Author

@SantiagoTorres @adityasaky — flagging this in case it slipped by; tagging you as spec/predicates maintainers, please redirect if predicate review sits elsewhere.

It proposes an agent-threat-scan predicate type — an attestation that a given artifact was scanned for agent/LLM/MCP threats and what the result was. It follows the existing predicate conventions; no changes to the core spec.

If a predicate of this kind is not something in-toto wants to host, that is a fine answer and I would rather know than leave it open. If it needs reshaping to fit the predicate model, tell me where and I will revise.

@eeee2345

Copy link
Copy Markdown
Author

@adityasaky @puerco — open since May with no review. Asking for a decision rather than bumping it again.

It adds an agent-threat-scan predicate type to the spec: two files, CI green, no conflicts.

The case for it in one line: attestations about AI-agent artefacts (skills, MCP server manifests, tool definitions) currently have nowhere to record what a security scan of that artefact found, and the existing predicate types do not fit the shape.

If a predicate for agent-security scan results is out of scope for in-toto, or belongs somewhere other than this repo, a clear no is more useful to me than an open PR and I will close it. What I would rather avoid is it sitting here indefinitely.

@Santoshkumarpuppala

Copy link
Copy Markdown

Review of the outcome field, since that is the part a consumer ends up trusting.

As written, outcome: "pass" is the only required positive content of a clean attestation, and nothing in the predicate records what the scanner actually evaluated. matches "MAY be empty when outcome is pass" (L142-143), and ruleset.version names the ruleset — its stated purpose at L125-127 is so a consumer "can re-resolve any ruleId in matches", which is about the rules that fired, not the rules that ran.

The failure case isn't a crashed scanner; that plausibly emits nothing. It's an honest partial one. A scanner implementing a subset of a ruleset, citing ruleset.version: "v2.0.17" accurately, produces a signed pass that is byte-identical to a full clean scan. The registry use case at L31-33 — gate listing on "scanned with a named open ruleset at or above a minimum version" — is satisfied by that scanner, and the registry cannot tell.

Why this bites here specifically. The predicate opts into the parsing rules (L96-97), and the monotonic principle they carry (spec/v1/README.md:53-55) tells consumers to write policies as "deny unless a 'no vulnerabilities' attestation exists". Under that recommended pattern outcome: pass is the allow-granting record — so the missing enumeration is load-bearing at exactly the point the spec directs consumers to place their trust.

The obvious objection, and why I don't think it settles it. One can answer that this is producer honesty, not schema: a producer willing to overstate pass will equally overstate an evaluatedRules field. True, and no field fixes a liar. But that isn't the gap. Two things are wrong independently of honesty:

  1. An honest partial scanner has no vocabulary to say so. The enum offers pass / warn / fail, so the correct thing for it to emit is pass — the schema requires it to misrepresent itself.
  2. A consumer cannot express the policy it actually wants. "Scanned at ≥ v2.0.17" is expressible; "…and actually evaluated the rules I care about" is not.

test-result.md faces the identical honesty question and still enumerates. Its use case 1 is "that all tests were in fact run" (L21), backed by passedTests/warnedTests/failedTests — and L40-48 is explicit that the point of the enumeration is coverage policy: when only a subset matters, "the verification policy ensures that tests for A are all listed in passedTests or possibly warnedTests." The Use Cases section here sets Test Result aside (L22-28) for lacking ruleset-versioning and a threat-class taxonomy, which is fair — but the enumeration went out with it.

Pre-empting the vulns comparison, since it's the nearest precedent and looks like a counterexample. vulns does have the same shape — "an empty list means the scanner found no vulnerabilities" (vuln.md:75). But it pins the corpus state around that emptiness: scanner.db.uri, scanner.db.version, a required scanner.db.lastUpdate (L61-69), plus required scanStartedOn/scanFinishedOn (L103-109). A consumer can therefore ask "was the DB fresh enough for this claim to mean anything." Here there is no analogue: ruleset.version pins what existed, not what ran, and a rule-based detector implements rules one at a time in a way a CVE-database matcher does not.

Two suggestions, both in the predicate's existing style:

  1. evaluatedRules — a count plus ruleIds, REQUIRED when outcome is pass, mirroring how passedTests supports test-result's use case 1. It turns a bare enum into a checkable claim: a registry can require that the evaluated set covers the rules it cares about at the version cited.
  2. An incomplete (or error) value in the outcome enum, so a scan that did not cover the ruleset has somewhere to say so instead of spelling itself pass.

Neither adds a new object; both are additive and monotonic-safe.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants