Skip to main content

skill_veil_core/findings/
enums.rs

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