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