1use libverify_core::control::{Control, ControlFinding, builtin, evaluate_all};
10use libverify_core::controls::codeowners_coverage::CodeownersCoverageControl;
11use libverify_core::controls::secret_scanning::SecretScanningControl;
12use libverify_core::controls::security_policy::SecurityPolicyControl;
13use libverify_core::controls::vulnerability_scanning::VulnerabilityScanningControl;
14use libverify_core::evidence::{CodeownersEntry, EvidenceBundle, EvidenceState, RepositoryPosture};
15use libverify_core::profile::{GateDecision, apply_profile};
16use libverify_policy::OpaProfile;
17
18struct RepoProfile {
23 name: &'static str,
24 description: &'static str,
25 posture: RepositoryPosture,
26}
27
28fn repo_profiles() -> Vec<RepoProfile> {
29 vec![
30 RepoProfile {
32 name: "frontend-app",
33 description: "Public SPA, Dependabot only, no ownership or policy",
34 posture: RepositoryPosture {
35 codeowners_entries: vec![],
36 secret_scanning_enabled: false,
37 secret_push_protection_enabled: false,
38 vulnerability_scanning_enabled: true, code_scanning_enabled: false,
40 security_policy_present: false,
41 security_policy_has_disclosure: false,
42 default_branch_protected: false,
43 },
44 },
45 RepoProfile {
48 name: "core-api",
49 description: "Private API, full GHAS, CODEOWNERS, SECURITY.md",
50 posture: RepositoryPosture {
51 codeowners_entries: vec![
52 CodeownersEntry {
53 pattern: "*".to_string(),
54 owners: vec!["@org/backend-team".to_string()],
55 },
56 CodeownersEntry {
57 pattern: "/src/auth/".to_string(),
58 owners: vec!["@org/security-team".to_string()],
59 },
60 CodeownersEntry {
61 pattern: "/infra/".to_string(),
62 owners: vec!["@org/platform-team".to_string()],
63 },
64 ],
65 secret_scanning_enabled: true,
66 secret_push_protection_enabled: true,
67 vulnerability_scanning_enabled: true, code_scanning_enabled: true, security_policy_present: true,
70 security_policy_has_disclosure: true,
71 default_branch_protected: true,
72 },
73 },
74 RepoProfile {
77 name: "infra-terraform",
78 description: "Private IaC, partial CODEOWNERS, detection-only scanning",
79 posture: RepositoryPosture {
80 codeowners_entries: vec![
81 CodeownersEntry {
82 pattern: "/modules/".to_string(),
83 owners: vec!["@org/platform-team".to_string()],
84 },
85 CodeownersEntry {
86 pattern: "/environments/".to_string(),
87 owners: vec!["@org/sre-team".to_string()],
88 },
89 ],
90 secret_scanning_enabled: true,
91 secret_push_protection_enabled: false,
92 vulnerability_scanning_enabled: false,
93 code_scanning_enabled: false,
94 security_policy_present: false,
95 security_policy_has_disclosure: false,
96 default_branch_protected: false,
97 },
98 },
99 RepoProfile {
101 name: "archived-legacy",
102 description: "Unmaintained legacy service, no controls",
103 posture: RepositoryPosture {
104 codeowners_entries: vec![],
105 secret_scanning_enabled: false,
106 secret_push_protection_enabled: false,
107 vulnerability_scanning_enabled: false,
108 code_scanning_enabled: false,
109 security_policy_present: false,
110 security_policy_has_disclosure: false,
111 default_branch_protected: false,
112 },
113 },
114 RepoProfile {
116 name: "new-microservice",
117 description: "New service, Dependabot + ownership, basic policy",
118 posture: RepositoryPosture {
119 codeowners_entries: vec![
120 CodeownersEntry {
121 pattern: "/src/".to_string(),
122 owners: vec!["@org/backend-team".to_string()],
123 },
124 CodeownersEntry {
125 pattern: "/deploy/".to_string(),
126 owners: vec!["@org/platform-team".to_string()],
127 },
128 CodeownersEntry {
129 pattern: "/.github/".to_string(),
130 owners: vec!["@org/devops".to_string()],
131 },
132 ],
133 secret_scanning_enabled: false,
134 secret_push_protection_enabled: false,
135 vulnerability_scanning_enabled: true, code_scanning_enabled: false,
137 security_policy_present: true,
138 security_policy_has_disclosure: false, default_branch_protected: false,
140 },
141 },
142 ]
143}
144
145fn aspm_controls() -> Vec<Box<dyn Control>> {
150 vec![
151 Box::new(CodeownersCoverageControl),
152 Box::new(SecretScanningControl),
153 Box::new(VulnerabilityScanningControl),
154 Box::new(SecurityPolicyControl),
155 ]
156}
157
158struct RepoAssessment {
159 name: &'static str,
160 description: &'static str,
161 pass: u32,
162 review: u32,
163 fail: u32,
164 details: Vec<(String, GateDecision, String)>, }
166
167fn assess_repo(profile: &RepoProfile, soc2: &OpaProfile) -> RepoAssessment {
168 let bundle = EvidenceBundle {
169 repository_posture: EvidenceState::complete(profile.posture.clone()),
170 ..Default::default()
171 };
172
173 let controls = aspm_controls();
174 let findings: Vec<ControlFinding> = evaluate_all(&controls, &bundle);
175 let outcomes = apply_profile(soc2, &findings);
176
177 let mut pass = 0u32;
178 let mut review = 0u32;
179 let mut fail = 0u32;
180 let mut details = Vec::new();
181
182 for outcome in &outcomes {
183 match outcome.decision {
184 GateDecision::Pass => pass += 1,
185 GateDecision::Review => review += 1,
186 GateDecision::Fail => fail += 1,
187 }
188 details.push((
189 outcome.control_id.to_string(),
190 outcome.decision,
191 outcome.rationale.clone(),
192 ));
193 }
194
195 RepoAssessment {
196 name: profile.name,
197 description: profile.description,
198 pass,
199 review,
200 fail,
201 details,
202 }
203}
204
205fn risk_score(a: &RepoAssessment) -> u32 {
210 a.fail * 3 + a.review
212}
213
214fn risk_tier(score: u32) -> &'static str {
215 match score {
216 0 => "COMPLIANT",
217 1..=3 => "LOW",
218 4..=6 => "MEDIUM",
219 7..=9 => "HIGH",
220 _ => "CRITICAL",
221 }
222}
223
224fn decision_symbol(d: GateDecision) -> &'static str {
225 match d {
226 GateDecision::Pass => "PASS",
227 GateDecision::Review => "REVIEW",
228 GateDecision::Fail => "FAIL",
229 }
230}
231
232fn main() {
233 let soc2 = OpaProfile::soc2_preset().expect("SOC2 preset should load");
234 let profiles = repo_profiles();
235
236 let mut assessments: Vec<RepoAssessment> =
237 profiles.iter().map(|p| assess_repo(p, &soc2)).collect();
238
239 assessments.sort_by(|a, b| risk_score(b).cmp(&risk_score(a)));
241
242 println!("==========================================================================");
244 println!(" FLEET-WIDE ASPM ASSESSMENT — SOC2 Preset (4 ASPM Controls x 5 Repos)");
245 println!("==========================================================================");
246 println!();
247 println!(
248 " {:<20} {:>4} {:>6} {:>4} {:>5} {:<10}",
249 "Repository", "Pass", "Review", "Fail", "Score", "Risk Tier"
250 );
251 println!(
252 " {:-<20} {:->4} {:->6} {:->4} {:->5} {:-<10}",
253 "", "", "", "", "", ""
254 );
255
256 for a in &assessments {
257 let score = risk_score(a);
258 println!(
259 " {:<20} {:>4} {:>6} {:>4} {:>5} {:<10}",
260 a.name,
261 a.pass,
262 a.review,
263 a.fail,
264 score,
265 risk_tier(score),
266 );
267 }
268
269 let total_pass: u32 = assessments.iter().map(|a| a.pass).sum();
271 let total_review: u32 = assessments.iter().map(|a| a.review).sum();
272 let total_fail: u32 = assessments.iter().map(|a| a.fail).sum();
273 let total = total_pass + total_review + total_fail;
274
275 println!();
276 println!(
277 " Fleet totals: {} pass / {} review / {} fail out of {} checks",
278 total_pass, total_review, total_fail, total
279 );
280 println!(
281 " Fleet pass rate: {:.0}%",
282 if total > 0 {
283 (total_pass as f64 / total as f64) * 100.0
284 } else {
285 0.0
286 }
287 );
288
289 println!();
291 println!("==========================================================================");
292 println!(" PER-REPO FINDINGS (ordered by risk, worst first)");
293 println!("==========================================================================");
294
295 for a in &assessments {
296 let score = risk_score(a);
297 println!();
298 println!(
299 " [{:<10}] {} — {}",
300 risk_tier(score),
301 a.name,
302 a.description
303 );
304
305 for (control_id, decision, rationale) in &a.details {
306 let symbol = decision_symbol(*decision);
307 println!(" {:<6} {:<26} {}", symbol, control_id, rationale);
308 }
309 }
310
311 println!();
313 println!("==========================================================================");
314 println!(" REMEDIATION PRIORITIES");
315 println!("==========================================================================");
316 println!();
317
318 let control_ids = [
320 builtin::CODEOWNERS_COVERAGE,
321 builtin::SECRET_SCANNING,
322 builtin::VULNERABILITY_SCANNING,
323 builtin::SECURITY_POLICY,
324 ];
325
326 for cid in &control_ids {
327 let failing_repos: Vec<&str> = assessments
328 .iter()
329 .filter(|a| {
330 a.details
331 .iter()
332 .any(|(id, d, _)| id == *cid && *d == GateDecision::Fail)
333 })
334 .map(|a| a.name)
335 .collect();
336
337 let review_repos: Vec<&str> = assessments
338 .iter()
339 .filter(|a| {
340 a.details
341 .iter()
342 .any(|(id, d, _)| id == *cid && *d == GateDecision::Review)
343 })
344 .map(|a| a.name)
345 .collect();
346
347 if !failing_repos.is_empty() || !review_repos.is_empty() {
348 println!(" {}", cid);
349 if !failing_repos.is_empty() {
350 println!(
351 " FAIL ({} repos): {}",
352 failing_repos.len(),
353 failing_repos.join(", ")
354 );
355 }
356 if !review_repos.is_empty() {
357 println!(
358 " REVIEW ({} repos): {}",
359 review_repos.len(),
360 review_repos.join(", ")
361 );
362 }
363 println!();
364 }
365 }
366
367 if let Some(worst) = assessments.first() {
369 let score = risk_score(worst);
370 println!(
371 " Worst-posture repo: {} (score={}, tier={})",
372 worst.name,
373 score,
374 risk_tier(score)
375 );
376 println!(" Recommended: prioritize enabling secret scanning and CODEOWNERS");
377 println!(" for repos in CRITICAL/HIGH tiers before next SOC2 audit window.");
378 }
379
380 println!();
381}