termivar_scanner/api_observation/
model.rs1use serde::Serialize;
2use std::fmt;
3use termivar_core::{EntityId, EvidenceId, RelationId};
4use thiserror::Error;
5
6use crate::{
7 knowledge::{KnowledgeBaseError, KnowledgeWrite},
8 rules::{RuleApplication, RuleEngineError},
9};
10
11#[derive(Clone, PartialEq, Eq, Serialize)]
18pub struct ApiObservationCommitReceipt {
19 pub(super) comparison_subject: EntityId,
20 pub(super) resource_scope: EntityId,
21 pub(super) evidence_id: EvidenceId,
22 pub(super) relation_id: RelationId,
23 pub(super) evidence_write: KnowledgeWrite,
24 pub(super) relation_write: KnowledgeWrite,
25}
26
27impl fmt::Debug for ApiObservationCommitReceipt {
28 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29 formatter
30 .debug_struct("ApiObservationCommitReceipt")
31 .field("comparison_subject", &"<redacted>")
32 .field("resource_scope", &"<redacted>")
33 .field("evidence_id", &"<redacted>")
34 .field("relation_id", &"<redacted>")
35 .field("evidence_write", &self.evidence_write)
36 .field("relation_write", &self.relation_write)
37 .finish()
38 }
39}
40
41impl ApiObservationCommitReceipt {
42 pub fn comparison_subject(&self) -> &EntityId {
44 &self.comparison_subject
45 }
46
47 pub fn resource_scope(&self) -> &EntityId {
49 &self.resource_scope
50 }
51
52 pub fn evidence_id(&self) -> &EvidenceId {
54 &self.evidence_id
55 }
56
57 pub fn relation_id(&self) -> &RelationId {
59 &self.relation_id
60 }
61
62 pub const fn evidence_write(&self) -> KnowledgeWrite {
64 self.evidence_write
65 }
66
67 pub const fn relation_write(&self) -> KnowledgeWrite {
69 self.relation_write
70 }
71}
72
73#[derive(Clone, PartialEq, Eq, Serialize)]
75pub struct ApiObservationReceipt {
76 pub(super) commit: ApiObservationCommitReceipt,
77 pub(super) applications: Vec<RuleApplication>,
78}
79
80impl fmt::Debug for ApiObservationReceipt {
81 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
82 formatter
83 .debug_struct("ApiObservationReceipt")
84 .field("commit", &self.commit)
85 .field("application_count", &self.applications.len())
86 .finish()
87 }
88}
89
90impl ApiObservationReceipt {
91 pub fn commit(&self) -> &ApiObservationCommitReceipt {
93 &self.commit
94 }
95
96 pub fn applications(&self) -> &[RuleApplication] {
104 &self.applications
105 }
106
107 pub fn into_parts(self) -> (ApiObservationCommitReceipt, Vec<RuleApplication>) {
109 (self.commit, self.applications)
110 }
111}
112
113#[derive(Error)]
115#[non_exhaustive]
116pub enum ApiObservationError {
117 #[error("API visibility observation resource does not match expected resource")]
119 ResourceMismatch {
120 expected: EntityId,
122 actual: EntityId,
124 },
125
126 #[error("API visibility review scan limit must be greater than zero")]
128 ZeroReviewScanLimit,
129
130 #[error("API visibility review scan limit {actual} exceeds hard ceiling {maximum}")]
132 ReviewScanLimitExceeded {
133 actual: u16,
135 maximum: u16,
137 },
138
139 #[error("API visibility review cursor is {actual} bytes, above hard ceiling {maximum}")]
141 ReviewCursorTooLong {
142 actual: usize,
144 maximum: usize,
146 },
147
148 #[error("API visibility resource-bound review cursor is {actual} bytes, above hard ceiling {maximum}")]
150 ResourceBoundReviewCursorTooLong {
151 actual: usize,
153 maximum: usize,
155 },
156
157 #[error("invalid API visibility resource-bound review cursor: {reason}")]
159 InvalidResourceBoundReviewCursor {
160 reason: &'static str,
162 },
163
164 #[error("unsupported API visibility resource-bound review cursor version")]
166 UnsupportedResourceBoundReviewCursorVersion,
167
168 #[error("API visibility resource-bound review cursor does not match requested resource")]
170 ResourceBoundReviewCursorMismatch,
171
172 #[error("API visibility observation {field} size {actual} exceeds hard ceiling {maximum}")]
174 ObservationLimitExceeded {
175 field: &'static str,
177 actual: usize,
179 maximum: usize,
181 },
182
183 #[error(transparent)]
185 Knowledge(#[from] KnowledgeBaseError),
186
187 #[error("API visibility observation committed before reasoning failed: {source}")]
189 ReasoningAfterCommit {
190 commit: Box<ApiObservationCommitReceipt>,
192 #[source]
194 source: RuleEngineError,
195 },
196}
197
198impl fmt::Debug for ApiObservationError {
199 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
200 match self {
201 Self::ResourceMismatch { .. } => formatter
202 .debug_struct("ResourceMismatch")
203 .field("expected", &"<redacted>")
204 .field("actual", &"<redacted>")
205 .finish(),
206 Self::ZeroReviewScanLimit => formatter.write_str("ZeroReviewScanLimit"),
207 Self::ReviewScanLimitExceeded { actual, maximum } => formatter
208 .debug_struct("ReviewScanLimitExceeded")
209 .field("actual", actual)
210 .field("maximum", maximum)
211 .finish(),
212 Self::ReviewCursorTooLong { actual, maximum } => formatter
213 .debug_struct("ReviewCursorTooLong")
214 .field("actual", actual)
215 .field("maximum", maximum)
216 .finish(),
217 Self::ResourceBoundReviewCursorTooLong { actual, maximum } => formatter
218 .debug_struct("ResourceBoundReviewCursorTooLong")
219 .field("actual", actual)
220 .field("maximum", maximum)
221 .finish(),
222 Self::InvalidResourceBoundReviewCursor { reason } => formatter
223 .debug_struct("InvalidResourceBoundReviewCursor")
224 .field("reason", reason)
225 .finish(),
226 Self::UnsupportedResourceBoundReviewCursorVersion => {
227 formatter.write_str("UnsupportedResourceBoundReviewCursorVersion")
228 },
229 Self::ResourceBoundReviewCursorMismatch => {
230 formatter.write_str("ResourceBoundReviewCursorMismatch")
231 },
232 Self::ObservationLimitExceeded {
233 field,
234 actual,
235 maximum,
236 } => formatter
237 .debug_struct("ObservationLimitExceeded")
238 .field("field", field)
239 .field("actual", actual)
240 .field("maximum", maximum)
241 .finish(),
242 Self::Knowledge(source) => formatter.debug_tuple("Knowledge").field(source).finish(),
243 Self::ReasoningAfterCommit { commit, source } => formatter
244 .debug_struct("ReasoningAfterCommit")
245 .field("commit", commit)
246 .field("source", source)
247 .finish(),
248 }
249 }
250}