1use crate::{FindingKind, normalize_path, source_tree_path::normalize_source_tree_scope};
2use std::path::PathBuf;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct LastSeen {
6 pub line: u32,
7 pub column: u32,
8}
9
10#[derive(Debug, Clone, Default, PartialEq, Eq)]
11pub struct Selector {
12 pub ast_kind: Option<String>,
13 pub container: Option<String>,
14 pub callee: Option<String>,
15 pub macro_name: Option<String>,
16 pub lint: Option<String>,
17 pub symbol: Option<String>,
18 pub receiver_fingerprint: Option<String>,
19 pub target_fingerprint: Option<String>,
20 pub normalized_snippet_hash: Option<String>,
21 pub line_hint: Option<u32>,
22 pub glob: Option<String>,
23}
24
25impl Selector {
26 pub fn has_structural_identity(&self) -> bool {
27 [
28 self.ast_kind.as_deref(),
29 self.container.as_deref(),
30 self.callee.as_deref(),
31 self.macro_name.as_deref(),
32 self.lint.as_deref(),
33 self.symbol.as_deref(),
34 self.receiver_fingerprint.as_deref(),
35 self.target_fingerprint.as_deref(),
36 self.normalized_snippet_hash.as_deref(),
37 ]
38 .into_iter()
39 .any(|value| value.is_some_and(|text| !text.trim().is_empty()))
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Lifecycle {
45 pub created: Option<String>,
46 pub review_after: Option<String>,
47 pub expires: Option<String>,
48}
49
50impl Lifecycle {
51 pub fn empty() -> Self {
52 Self {
53 created: None,
54 review_after: None,
55 expires: None,
56 }
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct AllowEntry {
62 pub id: String,
63 pub kind: FindingKind,
64 pub family: Option<String>,
65 pub path: Option<PathBuf>,
66 pub glob: Option<String>,
67 pub owner: String,
68 pub classification: String,
69 pub reason: String,
70 pub evidence: Vec<String>,
71 pub links: Vec<String>,
72 pub occurrence_limit: Option<u32>,
73 pub lifecycle: Lifecycle,
74 pub selector: Selector,
75 pub last_seen: Option<LastSeen>,
76}
77
78impl AllowEntry {
79 pub fn path_or_glob(&self) -> String {
80 if let Some(path) = &self.path {
81 normalize_path(path)
82 } else if let Some(glob) = &self.glob {
83 normalize_source_tree_scope(glob)
84 } else if let Some(glob) = &self.selector.glob {
85 normalize_source_tree_scope(glob)
86 } else {
87 String::new()
88 }
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct Requirements {
94 pub owner_required: bool,
95 pub reason_required: bool,
96 pub classification_required: bool,
97 pub evidence_required: bool,
98 pub expires_or_review_after_required: bool,
99 pub allow_bare_allow_attributes: bool,
100 pub lint_policy_id_required: bool,
101 pub stale_entries_fail: bool,
102 pub unsafe_evidence_required: bool,
103 pub unsafe_safety_comment_required: bool,
104}
105
106impl Default for Requirements {
107 fn default() -> Self {
108 Self {
109 owner_required: true,
110 reason_required: true,
111 classification_required: true,
112 evidence_required: false,
113 expires_or_review_after_required: true,
114 allow_bare_allow_attributes: false,
115 lint_policy_id_required: false,
116 stale_entries_fail: false,
117 unsafe_evidence_required: true,
118 unsafe_safety_comment_required: false,
119 }
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct WorkspaceConfig {
125 pub root: String,
126 pub inventory: String,
127 pub ignored: Vec<String>,
128 pub generated: Vec<String>,
129 pub default_mode: String,
130}
131
132impl Default for WorkspaceConfig {
133 fn default() -> Self {
134 Self {
135 root: ".".to_string(),
136 inventory: "git-tracked".to_string(),
137 ignored: vec![".git/**".to_string(), "target/**".to_string()],
138 generated: vec!["target/**".to_string(), "vendor/**".to_string()],
139 default_mode: "no-new".to_string(),
140 }
141 }
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct AllowConfig {
146 pub schema_version: String,
147 pub policy: String,
148 pub owner: Option<String>,
149 pub status: Option<String>,
150 pub workspace: WorkspaceConfig,
151 pub requirements: Requirements,
152 pub allow: Vec<AllowEntry>,
153}
154
155impl AllowConfig {
156 pub fn empty() -> Self {
157 Self {
158 schema_version: "0.1".to_string(),
159 policy: "cargo-allow".to_string(),
160 owner: None,
161 status: Some("active".to_string()),
162 workspace: WorkspaceConfig::default(),
163 requirements: Requirements::default(),
164 allow: Vec::new(),
165 }
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
170pub enum MatchStatus {
171 Matched,
172 New,
173 Stale,
174 Expired,
175 ReviewDue,
176 Ambiguous,
177 InvalidSelector,
178 MissingRequiredField,
179 EvidenceMissing,
180 BaselineDebt,
181}
182
183impl MatchStatus {
184 pub const ALL: &[Self] = &[
185 Self::Matched,
186 Self::New,
187 Self::Stale,
188 Self::Expired,
189 Self::ReviewDue,
190 Self::Ambiguous,
191 Self::InvalidSelector,
192 Self::MissingRequiredField,
193 Self::EvidenceMissing,
194 Self::BaselineDebt,
195 ];
196
197 pub fn as_str(self) -> &'static str {
198 match self {
199 Self::Matched => "matched",
200 Self::New => "new",
201 Self::Stale => "stale",
202 Self::Expired => "expired",
203 Self::ReviewDue => "review_due",
204 Self::Ambiguous => "ambiguous",
205 Self::InvalidSelector => "invalid_selector",
206 Self::MissingRequiredField => "missing_required_field",
207 Self::EvidenceMissing => "evidence_missing",
208 Self::BaselineDebt => "baseline_debt",
209 }
210 }
211
212 pub fn is_failure_in_strict(self) -> bool {
213 !matches!(self, Self::Matched | Self::ReviewDue)
214 }
215
216 pub fn is_failure_in_no_new(self) -> bool {
217 matches!(
218 self,
219 Self::New
220 | Self::Expired
221 | Self::Ambiguous
222 | Self::InvalidSelector
223 | Self::MissingRequiredField
224 | Self::EvidenceMissing
225 )
226 }
227}
228
229#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct MatchOutcome {
231 pub status: MatchStatus,
232 pub allow_id: Option<String>,
233 pub finding_index: Option<usize>,
234 pub message: String,
235 pub score: u32,
236}