use crate::security::SecurityIssueType;
use serde::{Deserialize, Serialize};
pub const CURRENT_TAXONOMY_VERSION: &str = "2025-draft";
pub const CURRENT_AST_VERSION: &str = "2026";
pub const FRAMEWORK_MCP: &str = "owasp-mcp-top-10";
pub const FRAMEWORK_AST: &str = "owasp-agentic-skills-top-10";
fn default_framework() -> String {
FRAMEWORK_MCP.to_string()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OwaspTag {
#[serde(default = "default_framework")]
pub framework: String,
pub id: String,
pub version: String,
}
impl OwaspTag {
pub fn new(id: &'static str) -> Self {
Self {
framework: FRAMEWORK_MCP.to_string(),
id: id.to_string(),
version: CURRENT_TAXONOMY_VERSION.to_string(),
}
}
pub fn ast(id: &'static str) -> Self {
Self {
framework: FRAMEWORK_AST.to_string(),
id: id.to_string(),
version: CURRENT_AST_VERSION.to_string(),
}
}
}
pub fn tags_for_security_issue(issue: SecurityIssueType) -> Vec<OwaspTag> {
use SecurityIssueType::*;
match issue {
ToolPoisoning => vec![OwaspTag::new("MCP02"), OwaspTag::new("MCP03")],
SQLInjection | CommandInjection => vec![OwaspTag::new("MCP07")],
PathTraversal => vec![OwaspTag::new("MCP04")],
AuthBypass => vec![OwaspTag::new("MCP08")],
PromptInjection => vec![OwaspTag::new("MCP01")],
Jailbreak => vec![OwaspTag::new("MCP01"), OwaspTag::new("MCP03")],
PIILeakage => vec![OwaspTag::new("MCP09")],
SecretsLeakage => vec![OwaspTag::new("MCP06"), OwaspTag::new("MCP09")],
}
}
pub fn tags_for_yara_rule(rule_name: &str) -> Vec<OwaspTag> {
match rule_name {
"SecretsLeakage" | "EnvironmentVariableLeakage" => {
vec![OwaspTag::new("MCP06"), OwaspTag::new("MCP09")]
}
"SSHKeyExposure" | "PEMFileAccess" => vec![OwaspTag::new("MCP06")],
"CommandInjection" => vec![OwaspTag::new("MCP07")],
"SQLInjection" => vec![OwaspTag::new("MCP07")],
"PathTraversalVulnerability" => vec![OwaspTag::new("MCP04")],
"MCPConfigRisk" => vec![OwaspTag::new("MCP10"), OwaspTag::new("MCP07")],
"MCPConfigChanged" => vec![OwaspTag::new("MCP02")],
"MCPToolChanged" => vec![OwaspTag::new("MCP02"), OwaspTag::new("MCP10")],
"SkillContentChanged" => vec![OwaspTag::new("MCP10")],
"AgentIdentityFileWrite" => vec![OwaspTag::new("MCP03"), OwaspTag::new("MCP09")],
"ExternalReferenceInventory" => vec![OwaspTag::new("MCP10")],
"UndeclaredNetworkEgress" => vec![OwaspTag::new("MCP02"), OwaspTag::new("MCP09")],
"VulnerableDependency" => vec![OwaspTag::new("MCP10")],
"CrossOriginEscalation"
| "CrossDomainContamination"
| "DomainOutlier"
| "MixedSecuritySchemes" => vec![OwaspTag::new("MCP05")],
"PromptInjectionSignature" | "UnicodeSteganography" | "IndirectPromptInjection" => {
vec![OwaspTag::new("MCP01")]
}
"CoerciveInjection" => vec![OwaspTag::new("MCP01"), OwaspTag::new("MCP02")],
"CovertExfiltration" => vec![OwaspTag::new("MCP01"), OwaspTag::new("MCP08")],
"AutonomyAbuse" => vec![OwaspTag::new("MCP03")],
"CapabilityInflation" => vec![OwaspTag::new("MCP02"), OwaspTag::new("MCP03")],
"SkillCredentialHarvesting" => {
vec![OwaspTag::new("MCP06"), OwaspTag::new("MCP09")]
}
"SkillToolChainingExfiltration" => {
vec![OwaspTag::new("MCP06"), OwaspTag::new("MCP09")]
}
"SkillSystemManipulation" => {
vec![OwaspTag::new("MCP03"), OwaspTag::new("MCP04")]
}
"CryptoStratumProtocol"
| "CryptoMiningPools"
| "CryptoMinerSoftware"
| "CryptoCoinjacking" => vec![OwaspTag::new("MCP02")],
"ReverseShell" | "C2FrameworkIndicators" => {
vec![OwaspTag::new("MCP02"), OwaspTag::new("MCP07")]
}
"BackdoorPersistence" | "RansomwareBehavior" => {
vec![OwaspTag::new("MCP02"), OwaspTag::new("MCP03")]
}
"KeyloggerIndicators" => vec![OwaspTag::new("MCP02"), OwaspTag::new("MCP09")],
"InfoStealer" => vec![OwaspTag::new("MCP06"), OwaspTag::new("MCP09")],
"PHPWebshellGeneric"
| "PHPWebshellObfuscated"
| "PHPWebshellKnown"
| "PythonWebshell"
| "JSPWebshell"
| "ASPXWebshell" => {
vec![OwaspTag::new("MCP02"), OwaspTag::new("MCP07")]
}
"OffensiveToolReferences" | "NetworkReconnaissance" | "ExploitFramework" => {
vec![OwaspTag::new("MCP02")]
}
"PrivilegeEscalationTools" => vec![OwaspTag::new("MCP02"), OwaspTag::new("MCP03")],
"PhishingKit" => vec![OwaspTag::new("MCP02"), OwaspTag::new("MCP06")],
"OverbroadAllowedTools" => vec![OwaspTag::new("MCP03")],
"VagueSkillTrigger" => vec![OwaspTag::new("MCP02"), OwaspTag::new("MCP03")],
"GenericSkillTrigger" => vec![OwaspTag::new("MCP02"), OwaspTag::new("MCP03")],
"DataExfiltrationGrant" => vec![OwaspTag::new("MCP06"), OwaspTag::new("MCP09")],
"SkillSensitiveFileReference" => vec![OwaspTag::new("MCP06"), OwaspTag::new("MCP09")],
"SkillNameCollision" => vec![OwaspTag::new("MCP02"), OwaspTag::new("MCP03")],
"SkillEmbeddedPayload" => vec![OwaspTag::new("MCP01"), OwaspTag::new("MCP10")],
"AgentskillsNameMismatch"
| "AgentskillsInvalidName"
| "AgentskillsMissingName"
| "AgentskillsUnknownFrontmatterField" => vec![OwaspTag::new("MCP02")],
"UnsafeYamlDeserialization" => vec![OwaspTag::new("MCP01"), OwaspTag::new("MCP10")],
"BrandImpersonation" => vec![OwaspTag::new("MCP02")],
"JsonPrototypePollution" => vec![OwaspTag::new("MCP10")],
_ => Vec::new(),
}
}
pub fn ast_tags_for_rule(rule_name: &str) -> Vec<OwaspTag> {
match rule_name {
"PromptInjectionSignature"
| "UnicodeSteganography"
| "CoerciveInjection"
| "CovertExfiltration"
| "SkillEmbeddedPayload"
| "SkillCredentialHarvesting"
| "SkillToolChainingExfiltration"
| "SkillSystemManipulation"
| "ReverseShell"
| "C2FrameworkIndicators"
| "BackdoorPersistence"
| "RansomwareBehavior"
| "KeyloggerIndicators"
| "InfoStealer"
| "PHPWebshellGeneric"
| "PHPWebshellObfuscated"
| "PHPWebshellKnown"
| "PythonWebshell"
| "JSPWebshell"
| "ASPXWebshell"
| "OffensiveToolReferences"
| "NetworkReconnaissance"
| "ExploitFramework"
| "PhishingKit"
| "CryptoStratumProtocol"
| "CryptoMiningPools"
| "CryptoMinerSoftware"
| "CryptoCoinjacking" => vec![OwaspTag::ast("AST01")],
"VulnerableDependency" => vec![OwaspTag::ast("AST02")],
"UndeclaredNetworkEgress" => vec![OwaspTag::ast("AST02"), OwaspTag::ast("AST04")],
"OverbroadAllowedTools" | "AutonomyAbuse" => vec![OwaspTag::ast("AST03")],
"DataExfiltrationGrant" | "SkillSensitiveFileReference" => {
vec![OwaspTag::ast("AST03")]
}
"AgentIdentityFileWrite" => vec![OwaspTag::ast("AST01"), OwaspTag::ast("AST03")],
"PrivilegeEscalationTools" => vec![OwaspTag::ast("AST01"), OwaspTag::ast("AST03")],
"AgentskillsNameMismatch"
| "AgentskillsInvalidName"
| "AgentskillsMissingName"
| "AgentskillsUnknownFrontmatterField"
| "CapabilityInflation"
| "VagueSkillTrigger"
| "GenericSkillTrigger"
| "UnsafeYamlDeserialization"
| "BrandImpersonation"
| "JsonPrototypePollution" => vec![OwaspTag::ast("AST04")],
"IndirectPromptInjection" | "ExternalReferenceInventory" => {
vec![OwaspTag::ast("AST05")]
}
"SkillNameCollision" => vec![OwaspTag::ast("AST06")],
"SkillContentChanged" => vec![OwaspTag::ast("AST07")],
"ScanCoverageIncomplete" => vec![OwaspTag::ast("AST08")],
_ => Vec::new(),
}
}
pub fn ast_tags_for_security_issue(issue: SecurityIssueType) -> Vec<OwaspTag> {
use SecurityIssueType::*;
match issue {
ToolPoisoning | Jailbreak | SQLInjection | CommandInjection | PathTraversal
| SecretsLeakage | PIILeakage => vec![OwaspTag::ast("AST01")],
PromptInjection => vec![OwaspTag::ast("AST01"), OwaspTag::ast("AST05")],
AuthBypass => vec![OwaspTag::ast("AST03")],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_security_issue_type_has_at_least_one_tag() {
use SecurityIssueType::*;
let all = [
ToolPoisoning,
SQLInjection,
CommandInjection,
PathTraversal,
AuthBypass,
PromptInjection,
Jailbreak,
PIILeakage,
SecretsLeakage,
];
for issue in all {
assert!(
!tags_for_security_issue(issue).is_empty(),
"SecurityIssueType::{issue:?} has no OWASP tags — every \
detection class should map to at least one Top 10 entry"
);
}
}
#[test]
fn known_yara_rules_have_tags() {
for name in [
"SecretsLeakage",
"EnvironmentVariableLeakage",
"SSHKeyExposure",
"PEMFileAccess",
"CommandInjection",
"SQLInjection",
"PathTraversalVulnerability",
"MCPConfigRisk",
"CrossOriginEscalation",
"CrossDomainContamination",
"DomainOutlier",
"MixedSecuritySchemes",
"PromptInjectionSignature",
"UnicodeSteganography",
"CoerciveInjection",
"IndirectPromptInjection",
"AutonomyAbuse",
"CapabilityInflation",
"SkillCredentialHarvesting",
"SkillToolChainingExfiltration",
"SkillSystemManipulation",
"CryptoStratumProtocol",
"CryptoMiningPools",
"CryptoMinerSoftware",
"CryptoCoinjacking",
"ReverseShell",
"BackdoorPersistence",
"KeyloggerIndicators",
"RansomwareBehavior",
"C2FrameworkIndicators",
"InfoStealer",
"PHPWebshellGeneric",
"PHPWebshellObfuscated",
"PHPWebshellKnown",
"PythonWebshell",
"JSPWebshell",
"ASPXWebshell",
"OffensiveToolReferences",
"NetworkReconnaissance",
"PrivilegeEscalationTools",
"ExploitFramework",
"PhishingKit",
] {
assert!(
!tags_for_yara_rule(name).is_empty(),
"YARA rule {name} should be tagged"
);
}
}
#[test]
fn unknown_yara_rule_returns_empty_not_panic() {
assert!(tags_for_yara_rule("totally-made-up-rule").is_empty());
}
#[test]
fn ast_tags_use_the_ast_framework_and_version() {
for tag in ast_tags_for_rule("SkillContentChanged") {
assert_eq!(tag.framework, FRAMEWORK_AST);
assert_eq!(tag.version, CURRENT_AST_VERSION);
assert!(tag.id.starts_with("AST"));
}
}
#[test]
fn mcp_tags_use_the_mcp_framework() {
let tag = &tags_for_yara_rule("SkillContentChanged")[0];
assert_eq!(tag.framework, FRAMEWORK_MCP);
assert!(tag.id.starts_with("MCP"));
}
#[test]
fn skill_surface_rules_carry_ast_tags() {
for (rule, expected) in [
("UnicodeSteganography", "AST01"),
("VulnerableDependency", "AST02"),
("OverbroadAllowedTools", "AST03"),
("AgentskillsNameMismatch", "AST04"),
("IndirectPromptInjection", "AST05"),
("SkillNameCollision", "AST06"),
("SkillContentChanged", "AST07"),
("ScanCoverageIncomplete", "AST08"),
] {
let ids: Vec<String> = ast_tags_for_rule(rule).into_iter().map(|t| t.id).collect();
assert!(
ids.iter().any(|id| id == expected),
"rule {rule} should carry {expected}; got {ids:?}"
);
}
}
#[test]
fn every_security_issue_type_has_an_ast_tag() {
use SecurityIssueType::*;
for issue in [
ToolPoisoning,
SQLInjection,
CommandInjection,
PathTraversal,
AuthBypass,
PromptInjection,
Jailbreak,
PIILeakage,
SecretsLeakage,
] {
assert!(
!ast_tags_for_security_issue(issue).is_empty(),
"SecurityIssueType::{issue:?} has no AST tag"
);
}
}
#[test]
fn generic_appsec_rules_are_not_ast_tagged_by_name() {
for rule in [
"CommandInjection",
"SQLInjection",
"PathTraversalVulnerability",
] {
assert!(
ast_tags_for_rule(rule).is_empty(),
"{rule} should not be AST-tagged by rule name"
);
}
}
}