Skip to main content

skill_veil_core/policy/
eval.rs

1use super::{ContextPolicy, PolicyGenerator, PolicyProfile, ShieldPolicy, POLICY_EXPIRY_DAYS};
2use crate::artifact_graph::ArtifactCapability;
3use crate::findings::{
4    default_operational_contexts, Finding, OperationalContext, RecommendedAction,
5};
6use chrono::Utc;
7use std::collections::HashMap;
8
9impl PolicyGenerator {
10    #[must_use]
11    pub(crate) fn generate_policies(&self) -> Vec<ShieldPolicy> {
12        let mut policy_map: HashMap<String, ShieldPolicy> = HashMap::new();
13
14        // Keyed by rule_id: each PolicyGenerator operates on a single skill, so
15        // rule_id is sufficient for deduplication. The policy_id embeds skill_name
16        // for external uniqueness but is not used as the merge key.
17        for finding in self.findings() {
18            let policy_id = format!("{}-{}", finding.rule_id.to_lowercase(), self.skill_name());
19            let recommendation = format!(
20                "{}: skill name equals \"{}\"",
21                finding.severity.action_str(),
22                self.skill_name()
23            );
24
25            policy_map
26                .entry(finding.rule_id.clone())
27                .and_modify(|p| {
28                    if !p.recommendation_agent.contains(&recommendation) {
29                        p.recommendation_agent.push(recommendation.clone());
30                    }
31                    if finding.severity > p.severity {
32                        p.severity = finding.severity;
33                    }
34                    if finding.confidence > p.confidence {
35                        p.confidence = finding.confidence;
36                    }
37                    p.action = p.action.max(finding.recommended_action);
38                })
39                .or_insert(ShieldPolicy {
40                    id: policy_id,
41                    category: finding.category,
42                    severity: finding.severity,
43                    confidence: finding.confidence,
44                    action: finding.recommended_action,
45                    recommendation_agent: vec![recommendation],
46                    expires_at: Some(Utc::now() + chrono::Duration::days(POLICY_EXPIRY_DAYS)),
47                    revoked: false,
48                });
49        }
50
51        let mut policies: Vec<_> = policy_map.into_values().collect();
52        policies.sort_by(|left, right| left.id.cmp(&right.id));
53        policies
54    }
55
56    #[must_use]
57    pub fn generate_context_policies(&self) -> Vec<ContextPolicy> {
58        let mut context_map: HashMap<OperationalContext, ContextPolicy> = HashMap::new();
59
60        for finding in self.findings() {
61            for context in contexts_for_finding(finding) {
62                let action = finding.recommended_action.max(
63                    self.profile()
64                        .map(|profile| resolve_context_action(self, profile, context))
65                        .unwrap_or(RecommendedAction::Log),
66                );
67                let rationale = format!(
68                    "{} via {} ({})",
69                    finding.rule_id, finding.reason, finding.category
70                );
71                upsert_context_policy(&mut context_map, context, action, rationale);
72            }
73        }
74
75        for node in &self.artifact_graph().nodes {
76            for capability in &node.capabilities {
77                for context in contexts_for_capability(capability.capability)
78                    .iter()
79                    .copied()
80                {
81                    let action = self
82                        .profile()
83                        .map(|profile| resolve_context_action(self, profile, context))
84                        .unwrap_or(RecommendedAction::Log);
85                    let rationale = format!(
86                        "{} exposes {:?} ({:?})",
87                        node.path, capability.capability, capability.source
88                    );
89                    upsert_context_policy(&mut context_map, context, action, rationale);
90                }
91            }
92        }
93
94        let mut policies: Vec<_> = context_map.into_values().collect();
95        policies.sort_by_key(|policy| context_sort_key(policy.context));
96        policies
97    }
98}
99
100fn resolve_context_action(
101    generator: &PolicyGenerator,
102    profile: PolicyProfile,
103    context: OperationalContext,
104) -> RecommendedAction {
105    generator.policy().map_or_else(
106        || profile.default_action_for_context(context),
107        |policy| policy.resolve_context_action(profile, context),
108    )
109}
110
111fn upsert_context_policy(
112    context_map: &mut HashMap<OperationalContext, ContextPolicy>,
113    context: OperationalContext,
114    action: RecommendedAction,
115    rationale: String,
116) {
117    context_map
118        .entry(context)
119        .and_modify(|policy| {
120            policy.action = policy.action.max(action);
121            if !policy.rationale.contains(&rationale) {
122                policy.rationale.push(rationale.clone());
123            }
124        })
125        .or_insert(ContextPolicy {
126            context,
127            action,
128            rationale: vec![rationale],
129        });
130}
131
132fn context_sort_key(context: OperationalContext) -> u8 {
133    match context {
134        OperationalContext::Install => 0,
135        OperationalContext::Network => 1,
136        OperationalContext::Secrets => 2,
137        OperationalContext::CodeModification => 3,
138        OperationalContext::ExternalComms => 4,
139    }
140}
141
142fn contexts_for_finding(finding: &Finding) -> Vec<OperationalContext> {
143    if finding.operational_contexts.is_empty() {
144        default_operational_contexts(finding.category, finding.artifact_kind)
145    } else {
146        finding.operational_contexts.clone()
147    }
148}
149
150fn contexts_for_capability(capability: ArtifactCapability) -> &'static [OperationalContext] {
151    match capability {
152        ArtifactCapability::InstallExecution | ArtifactCapability::ExposesBinary => {
153            &[OperationalContext::Install]
154        }
155        ArtifactCapability::NetworkAccess => &[
156            OperationalContext::Network,
157            OperationalContext::ExternalComms,
158        ],
159        ArtifactCapability::BrowserAccess => &[
160            OperationalContext::Network,
161            OperationalContext::CodeModification,
162        ],
163        ArtifactCapability::IdentityAccess => &[
164            OperationalContext::Secrets,
165            OperationalContext::ExternalComms,
166        ],
167        ArtifactCapability::InboundNetworkSurface => &[
168            OperationalContext::Network,
169            OperationalContext::ExternalComms,
170        ],
171        ArtifactCapability::PrivilegedRuntime | ArtifactCapability::HostFilesystemAccess => {
172            &[OperationalContext::CodeModification]
173        }
174        ArtifactCapability::ProcessExecution | ArtifactCapability::FilesystemWrite => {
175            &[OperationalContext::CodeModification]
176        }
177        ArtifactCapability::SecretAccess => &[OperationalContext::Secrets],
178        ArtifactCapability::PersistenceSurface => &[
179            OperationalContext::CodeModification,
180            OperationalContext::ExternalComms,
181        ],
182    }
183}