skill_veil_core/policy/
eval.rs1use 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 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}