bulwark_security/security/decision.rs
1use crate::logging::simple::SimpleLogger;
2use crate::request::context::RequestContext;
3use crate::security::FindingSeverity;
4use crate::{BulwarkError, BulwarkResult, Decision};
5
6use super::inspector::{Inspector, InspectorFinding};
7
8/// DecisionEngine
9///
10/// Satu-satunya komponen yang BERHAK
11/// menentukan keputusan security.
12///
13/// Alur:
14/// RequestContext -> Inspectors -> Decision -> Logging -> Server
15pub struct DecisionEngine {
16 inspectors: Vec<Box<dyn Inspector>>,
17}
18
19impl DecisionEngine {
20 /// Membuat decision engine kosong tanpa inspector.
21 pub fn new() -> Self {
22 Self {
23 inspectors: Vec::new(),
24 }
25 }
26
27 /// Menambahkan inspector ke pipeline decision.
28 pub fn add<I>(&mut self, inspector: I)
29 where
30 I: Inspector + 'static,
31 {
32 self.inspectors.push(Box::new(inspector));
33 }
34
35 /// Menjalankan seluruh inspector dan menghasilkan keputusan final.
36 ///
37 /// Prioritas keputusan:
38 /// BLOCK > LOG > ALLOW
39 ///
40 /// ⚠️ Catatan API:
41 /// - Jika keputusan akhir adalah `Decision::Block`,
42 /// fungsi ini akan mengembalikan `Err(BulwarkError::Blocked)`.
43 /// - Isi pesan error **TIDAK dijamin stabil** dan bukan bagian dari API.
44 pub fn decide(&self, ctx: &RequestContext) -> BulwarkResult<Decision> {
45 let mut findings: Vec<InspectorFinding> = Vec::new();
46
47 // 1. Jalankan seluruh inspector
48 for inspector in &self.inspectors {
49 match inspector.inspect(ctx) {
50 Ok(Some(finding)) => findings.push(finding),
51 Ok(None) => {}
52 Err(err) => {
53 // Error dari inspector dianggap fatal
54 SimpleLogger::log(ctx, &Decision::Block, "inspector error");
55 return Err(err);
56 }
57 }
58 }
59
60 // 2. Evaluasi semua findings
61 let decision = Self::evaluate_findings(&findings);
62
63 // 3. Logging terpusat
64 for finding in &findings {
65 SimpleLogger::log(ctx, &decision, &finding.reason);
66 }
67
68 // 4. Kembalikan keputusan
69 match decision {
70 Decision::Block => Err(BulwarkError::blocked(
71 // ⚠️ Message is NOT part of the public API
72 "request blocked by security decision",
73 )),
74 _ => Ok(decision),
75 }
76 }
77
78 /// Mengevaluasi semua temuan inspector menjadi satu keputusan final.
79 fn evaluate_findings(findings: &[InspectorFinding]) -> Decision {
80 let mut decision = Decision::Allow;
81
82 for finding in findings {
83 match finding.severity {
84 FindingSeverity::High => return Decision::Block,
85 FindingSeverity::Medium => decision = Decision::Log,
86 FindingSeverity::Low => {}
87 }
88 }
89
90 decision
91 }
92}
93
94/// Tingkat keparahan temuan inspector.
95///
96/// ## Kontrak API (STABLE sejak v0.3.0)
97///
98/// - `High`
99/// Temuan berbahaya yang **WAJIB memblokir request**.
100///
101/// - `Medium`
102/// Temuan mencurigakan namun tidak fatal.
103/// Request **tidak diblokir**, tetapi **WAJIB dilog**.
104///
105/// - `Low`
106/// Sinyal ringan atau informasi.
107/// Tidak mempengaruhi keputusan akhir.
108impl Default for DecisionEngine {
109 fn default() -> Self {
110 Self::new()
111 }
112}