Repository navigation
Expand file tree
/
Copy pathdelivery_gate.rs
More file actions
389 lines (373 loc) · 15.1 KB
/
Copy pathdelivery_gate.rs
File metadata and controls
389 lines (373 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
//! 交付门禁的纯领域判定;外部服务负责提供可信的发现、身份和原生检查证据。
use crate::{
AllowlistDisposition, AllowlistTarget, ApprovalScope, Completion, Finding, FindingLocation,
GateImpact, NativeCheckerBinding, ObligationResult, match_false_positive_identity,
};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
/// 调度前声明的检查义务及其精确目标集合。
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ObligationSpec {
/// 同一请求中唯一的稳定标识。
pub id: String,
/// 本义务应覆盖的目标身份;不是数量或通配文字。
pub expected_targets: Vec<String>,
}
/// 原生检查结果与观察到的目标集合。
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ObligationEvidence {
/// 原生证据解释后的完整性及发现。
pub result: ObligationResult,
/// 结果实际覆盖的目标身份。
pub observed_targets: Vec<String>,
}
/// 请求范围与交付证据;身份核验必须由受信边界完成。
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct DeliveryInput {
/// 独立于处置输入冻结的当前批准范围;不能从候选字段反向生成。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approval_scope: Option<ApprovalScope>,
/// 仅完整项目交付请求有资格签发 allow。
pub full_project: bool,
/// 静态发现确已覆盖本次项目范围。
pub discovery_complete: bool,
/// 受信边界已核验内容、批准策略、工具和来源身份。
pub trusted_bindings_verified: bool,
/// 最终判定时由可信宿主重新取得的 UTC Unix 秒;白名单不能沿用扫描开始时间。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gate_time_unix: Option<u64>,
/// 宿主独立冻结的当前批准策略修订;不能从待应用处置反向推断。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub policy_revision: Option<String>,
/// 从已冻结的请求契约独立取得的义务 ID;外部受信边界须核对其完整来源。
pub frozen_obligation_ids: Vec<String>,
/// 调度前冻结的全部适用义务。
pub obligations: Vec<ObligationSpec>,
/// 执行或等价复用后收集的结果。
pub evidence: Vec<ObligationEvidence>,
/// 受保护服务核验后的精确误报处置;工作区候选不得直接作为该输入。
#[serde(default)]
pub allowlist_dispositions: Vec<AllowlistDisposition>,
/// 可信策略冻结的 checker/tool/category/obligation 关联;缺失时不能应用白名单。
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub native_checker_bindings: Vec<NativeCheckerBinding>,
}
/// 项目级门禁结果,局部检查只能得到 not_evaluated。
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DeliveryDecision {
/// 全部适用义务完整且无阻断发现。
Allow,
/// 全部义务完整,仅剩经独立批准的精确误报例外。
AllowWithExceptions,
/// 全部必要证据完整,但存在阻断发现。
Deny,
/// 覆盖、身份、证据或政策判定存在缺口。
Incomplete,
/// 完整发现证明没有适用义务。
NotApplicable,
/// 本次仅检查了局部范围或类别。
NotEvaluated,
}
/// 门禁结论与可诊断的证据缺口。
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct DeliveryGate {
/// 交付判定。
pub decision: DeliveryDecision,
/// 所有已确认的阻断发现,包括未完成请求中的有效发现。
pub blocking_finding_ids: Vec<String>,
/// 保留在原始发现内、但本轮经精确批准处置的阻断发现。
pub whitelisted_false_positive_ids: Vec<String>,
/// 本轮仍然阻断的原生发现。
pub active_blocking_finding_ids: Vec<String>,
/// 缺权威、失配、过期或冲突的处置目标。
pub invalid_allowlist_finding_ids: Vec<String>,
/// 缺少、未完成或政策无法判定的义务。
pub incomplete_obligation_ids: Vec<String>,
/// 目标集合不同或含重复目标的义务。
pub coverage_mismatch_ids: Vec<String>,
/// 未声明、重复或无效的义务/结果标识。
pub invalid_ledger_ids: Vec<String>,
}
/// 在不可变输入上计算交付判定;缺失的证明一律不能变成 allow。
#[must_use]
pub fn evaluate_delivery(input: &DeliveryInput) -> DeliveryGate {
let mut declared = BTreeMap::new();
let mut observed = BTreeMap::new();
let mut invalid = BTreeSet::new();
let mut incomplete = BTreeSet::new();
let mut coverage_mismatch = BTreeSet::new();
let mut blocking = BTreeSet::new();
let mut whitelisted = BTreeSet::new();
let mut invalid_allowlist = BTreeSet::new();
let conflicting_disposition_ids = repeated_values(
input
.allowlist_dispositions
.iter()
.map(|item| item.observed_identity.finding_id.as_str()),
);
let conflicting_decision_ids = repeated_values(
input
.allowlist_dispositions
.iter()
.map(|item| item.decision_id.as_str()),
);
let mut applicable_count = 0;
let mut finding_id_counts = BTreeMap::<&str, usize>::new();
for item in &input.evidence {
for finding in &item.result.findings {
*finding_id_counts.entry(&finding.id).or_default() += 1;
}
}
let colliding_finding_ids: BTreeSet<&str> = finding_id_counts
.iter()
.filter(|(_, count)| **count > 1)
.map(|(id, _)| *id)
.collect();
let mut frozen = BTreeSet::new();
for id in &input.frozen_obligation_ids {
if id.is_empty() || !frozen.insert(id.as_str()) {
invalid.insert(id.clone());
}
}
for obligation in &input.obligations {
if obligation.id.is_empty() || declared.insert(&obligation.id, obligation).is_some() {
invalid.insert(obligation.id.clone());
}
if obligation.expected_targets.iter().any(String::is_empty)
|| unique_count(&obligation.expected_targets) != obligation.expected_targets.len()
{
coverage_mismatch.insert(obligation.id.clone());
}
}
let declared_ids: BTreeSet<&str> = declared.keys().map(|id| id.as_str()).collect();
for id in frozen.symmetric_difference(&declared_ids) {
invalid.insert((*id).to_owned());
}
for item in &input.evidence {
let id = &item.result.id;
if id.is_empty() || observed.insert(id, item).is_some() || !declared.contains_key(id) {
invalid.insert(id.clone());
}
if item.observed_targets.iter().any(String::is_empty)
|| unique_count(&item.observed_targets) != item.observed_targets.len()
{
coverage_mismatch.insert(id.clone());
}
for finding in &item.result.findings {
if finding.obligation_id != *id
|| finding.id.is_empty()
|| colliding_finding_ids.contains(finding.id.as_str())
{
invalid.insert(id.clone());
}
match finding.gate_impact {
GateImpact::Blocking => {
blocking.insert(finding.id.clone());
}
GateImpact::Undetermined => {
incomplete.insert(id.clone());
}
GateImpact::NonBlocking => {}
}
}
}
for (id, obligation) in declared {
let Some(item) = observed.get(id) else {
incomplete.insert(id.clone());
continue;
};
match item.result.completion {
Completion::Incomplete => {
incomplete.insert(id.clone());
}
Completion::NotApplicable => {
if !obligation.expected_targets.is_empty()
|| item.result.reason.as_ref().is_none_or(String::is_empty)
|| !item.observed_targets.is_empty()
|| !item.result.findings.is_empty()
{
incomplete.insert(id.clone());
}
}
Completion::Complete => {
if obligation.expected_targets.is_empty() {
coverage_mismatch.insert(id.clone());
continue;
}
applicable_count += 1;
let expected: BTreeSet<&str> = obligation
.expected_targets
.iter()
.map(String::as_str)
.collect();
let actual: BTreeSet<&str> =
item.observed_targets.iter().map(String::as_str).collect();
if expected != actual {
coverage_mismatch.insert(id.clone());
}
}
}
}
for disposition in &input.allowlist_dispositions {
let finding_id = &disposition.observed_identity.finding_id;
let matching_findings = input
.evidence
.iter()
.flat_map(|item| {
item.result
.findings
.iter()
.map(move |finding| (item, finding))
})
.filter(|(item, finding)| {
item.result.completion == Completion::Complete
&& item.result.id == finding.obligation_id
&& !invalid.contains(&item.result.id)
&& !incomplete.contains(&item.result.id)
&& !coverage_mismatch.contains(&item.result.id)
&& finding.id == *finding_id
&& finding.gate_impact == GateImpact::Blocking
&& finding.native_rule_id == disposition.observed_identity.native_rule_id
&& matches_native_finding(input, disposition, finding)
})
.count();
let valid = input.trusted_bindings_verified
&& disposition.independent_approval_verified
&& input.approval_scope.as_ref().is_some_and(|scope| {
scope.valid() && disposition.approval_scope.as_ref() == Some(scope)
})
&& input.policy_revision.as_deref()
== Some(disposition.approved_policy_revision.as_str())
&& disposition.observed_at > 0
&& disposition.expires_at > disposition.observed_at
&& input.gate_time_unix.is_some_and(|now| {
now > 0 && now >= disposition.observed_at && now < disposition.expires_at
})
&& [
disposition.decision_id.as_str(),
disposition.approval_ref.as_str(),
disposition.approved_policy_revision.as_str(),
]
.into_iter()
.all(safe_public_reference)
&& matching_findings == 1
&& !colliding_finding_ids.contains(finding_id.as_str())
&& !conflicting_disposition_ids.contains(finding_id.as_str())
&& !conflicting_decision_ids.contains(disposition.decision_id.as_str())
&& match_false_positive_identity(
&disposition.observed_identity,
&disposition.decision_identity,
)
.is_ok();
if valid {
whitelisted.insert(finding_id.clone());
} else {
invalid_allowlist.insert(finding_id.clone());
}
}
let active_blocking: BTreeSet<_> = blocking.difference(&whitelisted).cloned().collect();
let has_gap = !input.discovery_complete
|| !input.trusted_bindings_verified
|| !invalid.is_empty()
|| !incomplete.is_empty()
|| !coverage_mismatch.is_empty();
let has_gap = has_gap || !invalid_allowlist.is_empty();
let decision = if !input.full_project {
DeliveryDecision::NotEvaluated
} else if has_gap {
DeliveryDecision::Incomplete
} else if !active_blocking.is_empty() {
DeliveryDecision::Deny
} else if applicable_count == 0 {
DeliveryDecision::NotApplicable
} else if !whitelisted.is_empty() {
DeliveryDecision::AllowWithExceptions
} else {
DeliveryDecision::Allow
};
DeliveryGate {
decision,
blocking_finding_ids: blocking.into_iter().collect(),
whitelisted_false_positive_ids: whitelisted.into_iter().collect(),
active_blocking_finding_ids: active_blocking.into_iter().collect(),
invalid_allowlist_finding_ids: invalid_allowlist.into_iter().collect(),
incomplete_obligation_ids: incomplete.into_iter().collect(),
coverage_mismatch_ids: coverage_mismatch.into_iter().collect(),
invalid_ledger_ids: invalid.into_iter().collect(),
}
}
fn safe_public_reference(value: &str) -> bool {
!value.is_empty()
&& value.bytes().all(|byte| {
byte.is_ascii_alphanumeric()
|| matches!(byte, b'-' | b'_' | b'.' | b':' | b'/' | b'#' | b'@')
})
}
fn matches_native_finding(
input: &DeliveryInput,
disposition: &AllowlistDisposition,
finding: &Finding,
) -> bool {
let identity = &disposition.observed_identity;
if !finding
.native_identity
.as_ref()
.is_some_and(|native| match_false_positive_identity(native, identity).is_ok())
{
return false;
}
let bindings = input
.native_checker_bindings
.iter()
.filter(|binding| {
binding.checker_id == identity.checker_id
&& binding.obligation_id == finding.obligation_id
})
.collect::<Vec<_>>();
let [binding] = bindings.as_slice() else {
return false;
};
if binding.tool_id != finding.tool_id
|| binding.category != identity.category
|| !safe_public_reference(&binding.checker_id)
|| !safe_public_reference(&binding.tool_id)
|| !safe_public_reference(&binding.obligation_id)
{
return false;
}
// 第一处为主定位;不能借另一个次要位置将不同主目标的发现处置为误报。
match (&identity.target, finding.locations.first()) {
(
AllowlistTarget::Source { path, .. },
Some(
FindingLocation::Source { path: actual, .. }
| FindingLocation::Project { path: actual },
),
) => path == actual,
(
AllowlistTarget::Dependency {
component, version, ..
},
Some(FindingLocation::Dependency {
component: actual,
version: Some(actual_version),
..
}),
) => component == actual && version == actual_version,
_ => false,
}
}
fn unique_count(items: &[String]) -> usize {
items.iter().collect::<BTreeSet<_>>().len()
}
fn repeated_values<'a>(items: impl Iterator<Item = &'a str>) -> BTreeSet<&'a str> {
let mut seen = BTreeSet::new();
let mut repeated = BTreeSet::new();
for item in items {
if !seen.insert(item) {
repeated.insert(item);
}
}
repeated
}