1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct CostModel {
11 pub component_added: u32,
13 pub component_removed: u32,
15 pub version_patch: u32,
17 pub version_minor: u32,
19 pub version_major: u32,
21 pub license_changed: u32,
23 pub supplier_changed: u32,
25 pub vulnerability_introduced: u32,
27 pub vulnerability_resolved: i32,
29 pub dependency_added: u32,
31 pub dependency_removed: u32,
33 pub hash_mismatch: u32,
35 pub crypto_algorithm_changed: u32,
37 pub crypto_downgrade: u32,
39 pub crypto_key_rotated: u32,
41 pub crypto_cert_expiry_changed: u32,
43 pub crypto_protocol_changed: u32,
45 pub crypto_quantum_level_changed: u32,
47}
48
49impl Default for CostModel {
50 fn default() -> Self {
51 Self {
52 component_added: 10,
53 component_removed: 10,
54 version_patch: 2,
55 version_minor: 4,
56 version_major: 7,
57 license_changed: 6,
58 supplier_changed: 4,
59 vulnerability_introduced: 15,
60 vulnerability_resolved: -3,
61 dependency_added: 5,
62 dependency_removed: 5,
63 hash_mismatch: 8,
64 crypto_algorithm_changed: 8,
65 crypto_downgrade: 20,
66 crypto_key_rotated: 3,
67 crypto_cert_expiry_changed: 5,
68 crypto_protocol_changed: 6,
69 crypto_quantum_level_changed: 10,
70 }
71 }
72}
73
74impl CostModel {
75 #[must_use]
77 pub fn security_focused() -> Self {
78 Self {
79 vulnerability_introduced: 25,
80 vulnerability_resolved: -5,
81 hash_mismatch: 15,
82 supplier_changed: 8,
83 crypto_downgrade: 30,
84 crypto_quantum_level_changed: 15,
85 crypto_algorithm_changed: 12,
86 ..Default::default()
87 }
88 }
89
90 #[must_use]
92 pub fn compliance_focused() -> Self {
93 Self {
94 license_changed: 12,
95 supplier_changed: 8,
96 ..Default::default()
97 }
98 }
99
100 #[must_use]
102 pub const fn version_change_cost(
103 &self,
104 old: &Option<semver::Version>,
105 new: &Option<semver::Version>,
106 ) -> u32 {
107 match (old, new) {
108 (Some(old_ver), Some(new_ver)) => {
109 if old_ver.major != new_ver.major {
110 self.version_major
111 } else if old_ver.minor != new_ver.minor {
112 self.version_minor
113 } else if old_ver.patch != new_ver.patch {
114 self.version_patch
115 } else {
116 0
117 }
118 }
119 (None, Some(_)) | (Some(_), None) => self.version_minor,
120 (None, None) => 0,
121 }
122 }
123
124 #[allow(clippy::too_many_arguments)]
126 #[must_use]
127 pub const fn calculate_semantic_score(
128 &self,
129 components_added: usize,
130 components_removed: usize,
131 version_changes: usize,
132 license_changes: usize,
133 vulns_introduced: usize,
134 vulns_resolved: usize,
135 deps_added: usize,
136 deps_removed: usize,
137 ) -> f64 {
138 let score = (components_added as u32 * self.component_added)
139 + (components_removed as u32 * self.component_removed)
140 + (version_changes as u32 * self.version_minor)
141 + (license_changes as u32 * self.license_changed)
142 + (vulns_introduced as u32 * self.vulnerability_introduced)
143 + (deps_added as u32 * self.dependency_added)
144 + (deps_removed as u32 * self.dependency_removed);
145
146 let reward = vulns_resolved as i32 * self.vulnerability_resolved;
147
148 (score as i32 + reward) as f64
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn test_default_cost_model() {
158 let model = CostModel::default();
159 assert_eq!(model.component_added, 10);
160 assert_eq!(model.component_removed, 10);
161 assert_eq!(model.version_patch, 2);
162 assert_eq!(model.version_minor, 4);
163 assert_eq!(model.version_major, 7);
164 assert_eq!(model.license_changed, 6);
165 assert_eq!(model.vulnerability_introduced, 15);
166 assert_eq!(model.vulnerability_resolved, -3);
167 assert_eq!(model.hash_mismatch, 8);
168 assert_eq!(model.crypto_algorithm_changed, 8);
169 assert_eq!(model.crypto_downgrade, 20);
170 assert_eq!(model.crypto_key_rotated, 3);
171 assert_eq!(model.crypto_cert_expiry_changed, 5);
172 assert_eq!(model.crypto_protocol_changed, 6);
173 assert_eq!(model.crypto_quantum_level_changed, 10);
174 }
175
176 #[test]
177 fn test_security_focused_model() {
178 let model = CostModel::security_focused();
179 let default = CostModel::default();
180 assert!(model.vulnerability_introduced > default.vulnerability_introduced);
181 assert!(model.hash_mismatch > default.hash_mismatch);
182 assert!(model.vulnerability_resolved < default.vulnerability_resolved);
183 assert!(model.crypto_downgrade > default.crypto_downgrade);
184 assert!(model.crypto_quantum_level_changed > default.crypto_quantum_level_changed);
185 }
186
187 #[test]
188 fn test_compliance_focused_model() {
189 let model = CostModel::compliance_focused();
190 let default = CostModel::default();
191 assert!(model.license_changed > default.license_changed);
192 assert!(model.supplier_changed > default.supplier_changed);
193 }
194
195 fn ver(major: u64, minor: u64, patch: u64) -> semver::Version {
196 semver::Version::new(major, minor, patch)
197 }
198
199 #[test]
200 fn test_version_change_cost_major() {
201 let model = CostModel::default();
202 let old = Some(ver(1, 0, 0));
203 let new = Some(ver(2, 0, 0));
204 assert_eq!(model.version_change_cost(&old, &new), model.version_major);
205 }
206
207 #[test]
208 fn test_version_change_cost_minor() {
209 let model = CostModel::default();
210 let old = Some(ver(1, 0, 0));
211 let new = Some(ver(1, 1, 0));
212 assert_eq!(model.version_change_cost(&old, &new), model.version_minor);
213 }
214
215 #[test]
216 fn test_version_change_cost_patch() {
217 let model = CostModel::default();
218 let old = Some(ver(1, 0, 0));
219 let new = Some(ver(1, 0, 1));
220 assert_eq!(model.version_change_cost(&old, &new), model.version_patch);
221 }
222
223 #[test]
224 fn test_version_change_cost_same() {
225 let model = CostModel::default();
226 let old = Some(ver(1, 2, 3));
227 let new = Some(ver(1, 2, 3));
228 assert_eq!(model.version_change_cost(&old, &new), 0);
229 }
230
231 #[test]
232 fn test_version_change_cost_none_to_some() {
233 let model = CostModel::default();
234 let new = Some(ver(1, 0, 0));
235 assert_eq!(model.version_change_cost(&None, &new), model.version_minor);
236 }
237
238 #[test]
239 fn test_version_change_cost_some_to_none() {
240 let model = CostModel::default();
241 let old = Some(ver(1, 0, 0));
242 assert_eq!(model.version_change_cost(&old, &None), model.version_minor);
243 }
244
245 #[test]
246 fn test_version_change_cost_none_none() {
247 let model = CostModel::default();
248 assert_eq!(model.version_change_cost(&None, &None), 0);
249 }
250
251 #[test]
252 fn test_semantic_score_zero() {
253 let model = CostModel::default();
254 let score = model.calculate_semantic_score(0, 0, 0, 0, 0, 0, 0, 0);
255 assert_eq!(score, 0.0);
256 }
257
258 #[test]
259 fn test_semantic_score_basic() {
260 let model = CostModel::default();
261 let score = model.calculate_semantic_score(2, 1, 1, 0, 0, 0, 0, 0);
263 assert_eq!(score, 34.0);
264 }
265
266 #[test]
267 fn test_semantic_score_with_resolved() {
268 let model = CostModel::default();
269 let score = model.calculate_semantic_score(0, 0, 0, 0, 1, 2, 0, 0);
271 assert_eq!(score, 9.0);
272 }
273}