1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
use super::weights::{
EVIDENCE_WEIGHT_BEHAVIOR, EVIDENCE_WEIGHT_CONTEXT, EVIDENCE_WEIGHT_INTENT, EVIDENCE_WEIGHT_IOC,
SEVERITY_WEIGHT_CRITICAL, SEVERITY_WEIGHT_HIGH, SEVERITY_WEIGHT_LOW, SEVERITY_WEIGHT_MEDIUM,
};
use serde::{Deserialize, Serialize};
use std::fmt;
use strum_macros::Display;
/// Threat category classification
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Display,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum ThreatCategory {
/// Remote code execution risks
RemoteExec,
/// Supply chain security risks
SupplyChain,
/// Persistent prompt or instruction tampering.
PersistentPromptTampering,
/// Credential/secret exposure
CredentialExposure,
/// Tool invocation or tool permission abuse.
ToolAbuse,
/// Agent autonomy increases without clear control.
AutonomyEscalation,
/// Privilege escalation attempts
PrivilegeEscalation,
/// Data exfiltration indicators
DataExfiltration,
/// Persuasive/manipulative language
PersuasiveLanguage,
/// Social manipulation of the operator or agent.
SocialManipulation,
/// Scope creep in permissions
ScopeCreep,
/// Obfuscation techniques
Obfuscation,
/// Unsafe binary execution
UnsafeBinary,
/// Generic security concern
Generic,
}
/// Operational context affected by a finding.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum OperationalContext {
Install,
Network,
Secrets,
CodeModification,
ExternalComms,
}
/// Severity level of a finding
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Display,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum Severity {
/// Low severity - informational
Low,
/// Medium severity - requires attention
Medium,
/// High severity - should be addressed
High,
/// Critical severity - must be blocked
Critical,
}
impl Severity {
/// Get the numeric weight for risk scoring
pub fn weight(&self) -> u32 {
match self {
Severity::Low => SEVERITY_WEIGHT_LOW,
Severity::Medium => SEVERITY_WEIGHT_MEDIUM,
Severity::High => SEVERITY_WEIGHT_HIGH,
Severity::Critical => SEVERITY_WEIGHT_CRITICAL,
}
}
/// Get the default recommended action for this severity level
pub fn default_action(&self) -> RecommendedAction {
match self {
Severity::Critical | Severity::High => RecommendedAction::Block,
Severity::Medium => RecommendedAction::RequireApproval,
Severity::Low => RecommendedAction::Log,
}
}
/// Get the action string representation for policy recommendations
pub fn action_str(&self) -> &'static str {
match self {
Severity::Critical | Severity::High => "BLOCK",
Severity::Medium => "REQUIRE_APPROVAL",
Severity::Low => "LOG",
}
}
}
/// What was matched by the rule
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MatchTarget {
/// Match in the raw document content
Document,
/// Match in a specific section
Section { name: String },
/// Match in a code block
CodeBlock { language: Option<String> },
/// Match in a referenced file
ReferencedFile { path: String },
}
impl fmt::Display for MatchTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MatchTarget::Document => write!(f, "document"),
MatchTarget::Section { name } => write!(f, "section:{}", name),
MatchTarget::CodeBlock { language } => {
write!(f, "code_block:{}", language.as_deref().unwrap_or("unknown"))
}
MatchTarget::ReferencedFile { path } => write!(f, "file:{}", path),
}
}
}
/// Classification of the evidence behind a finding.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Display)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum EvidenceKind {
/// A known IOC such as a hash, domain, publisher, or infrastructure indicator.
Ioc,
/// A concrete behavioral pattern such as execution or exfiltration.
Behavior,
/// Language that indicates malicious or manipulative intent.
Intent,
/// Environmental or contextual signals that increase risk.
Context,
}
impl EvidenceKind {
/// Additional weight used by explainable risk scoring.
pub fn weight(&self) -> u32 {
match self {
Self::Ioc => EVIDENCE_WEIGHT_IOC,
Self::Behavior => EVIDENCE_WEIGHT_BEHAVIOR,
Self::Intent => EVIDENCE_WEIGHT_INTENT,
Self::Context => EVIDENCE_WEIGHT_CONTEXT,
}
}
/// Short explanation for reports and UIs.
pub fn description(&self) -> &'static str {
match self {
Self::Ioc => "Known malicious indicator",
Self::Behavior => "Concrete risky behavior",
Self::Intent => "Manipulative or coercive intent",
Self::Context => "Contextual risk signal",
}
}
}
/// Artifact type where the finding was observed.
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Display,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum ArtifactKind {
/// The primary skill or prompt document.
SkillDocument,
/// Persistent instruction document such as AGENTS.md or SYSTEM.md.
AgentInstruction,
/// Prompt pack document or prompt bundle entry.
PromptPackDocument,
/// MCP server manifest or MCP descriptor.
McpServerManifest,
/// A code snippet embedded in the document.
CodeSnippet,
/// A referenced script or executable artifact.
ReferencedArtifact,
/// A package manifest or infrastructure descriptor.
PackageManifest,
/// A dependency lockfile associated with a package manifest.
Lockfile,
/// Any other text artifact.
GenericArtifact,
}
impl ArtifactKind {
/// Specificity score: higher = more specific classification.
///
/// Used by `ArtifactGraph::add_node_with_capabilities` to track the
/// most specific kind observed across pipelines that touch the same
/// path. Pipeline ordering is not deterministic — a "first wins" rule
/// would let `AgentInstruction` shadow a later, more specific
/// `McpServerManifest` classification, silencing analysis branches
/// that key on the kind (e.g. `INTERNAL_NETWORK_ACCESS` checks limited
/// to `ReferencedArtifact | McpServerManifest`).
///
/// Tie groups (same numeric score) are treated as compatible: the
/// first-seen kind sticks. Across tiers we always upgrade.
#[must_use]
pub fn specificity(self) -> u8 {
match self {
Self::GenericArtifact => 0,
Self::CodeSnippet => 1,
Self::ReferencedArtifact | Self::PromptPackDocument => 2,
// SkillDocument is the package's primary entrypoint, not a
// helper file. Promoting it to tier 3 means a later
// `SkillDocument` insertion correctly upgrades over an
// earlier `ReferencedArtifact` registration that happened
// because the script analyzer ran first.
Self::SkillDocument | Self::AgentInstruction => 3,
Self::PackageManifest | Self::McpServerManifest | Self::Lockfile => 4,
}
}
}
/// High-level scope of the artifact within the package.
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Display,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum ArtifactScope {
AgentEntrypoint,
PackageRootArtifact,
SupportingArtifact,
}
/// Coarse signal family used for final package verdicts.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Display)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum SignalClass {
Hygiene,
SuspiciousPackageBehavior,
MaliciousBehavior,
ReviewSignal,
}
/// Final package-level judgment.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum Verdict {
Benign,
Suspicious,
Malicious,
}
/// Recommended action based on findings
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Display)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum RecommendedAction {
/// Log the finding for awareness
Log,
/// Require human approval before proceeding
RequireApproval,
/// Block the skill from being used
Block,
}
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Display,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum BlastRadiusLevel {
#[default]
Low,
Medium,
High,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum PackageHealth {
Healthy,
NeedsReview,
Elevated,
}