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
//! BSI TR-03183-2 (German national CRA-aligned SBOM guideline) checks.
use super::*;
impl ComplianceChecker {
// ════════════════════════════════════════════════════════════════════
// BSI TR-03183-2 (German national SBOM guideline)
// ════════════════════════════════════════════════════════════════════
/// BSI TR-03183-2 compliance checks.
///
/// TR-03183-2 is the German Federal Office for Information Security's
/// SBOM technical guideline, free and ENISA-cited. It is functionally
/// equivalent to the CRA Annex I Part II SBOM obligations but stricter
/// than NTIA Minimum on hashes and identifiers.
///
/// Reference: BSI TR-03183-2 v2.0.0 §5 (mandatory) and §6 (recommended).
pub(crate) fn check_bsi_tr_03183_2(
&self,
sbom: &NormalizedSbom,
violations: &mut Vec<Violation>,
) {
use crate::model::{CreatorType, HashAlgorithm};
// §5.1 — Author/creator identification (mandatory)
if sbom.document.creators.is_empty() {
violations.push(Violation {
severity: ViolationSeverity::Error,
category: ViolationCategory::DocumentMetadata,
message: "[BSI TR-03183-2 §5.1] SBOM author/creator missing".to_string(),
element: None,
requirement: "BSI TR-03183-2 §5.1: Author/creator identification".to_string(),
rule_id: "SBOM-BSI-TR-03183-2-5-1",
standard_refs: Vec::new(),
});
}
// §5.1 — At least one tool creator (mandatory)
let has_tool_creator = sbom
.document
.creators
.iter()
.any(|c| c.creator_type == CreatorType::Tool);
if !has_tool_creator {
violations.push(Violation {
severity: ViolationSeverity::Error,
category: ViolationCategory::DocumentMetadata,
message: "[BSI TR-03183-2 §5.1] SBOM must identify the generation tool".to_string(),
element: None,
requirement: "BSI TR-03183-2 §5.1: Tool identification".to_string(),
rule_id: "SBOM-BSI-TR-03183-2-5-1",
standard_refs: Vec::new(),
});
}
// §5.2 — ISO-8601 timestamp (mandatory).
// Our `DocumentMetadata::created` is `DateTime<Utc>`, always ISO-8601
// when serialised; the practical risk is the `created` field being
// unset in the source SBOM. NormalizedSbom default is Utc::now(), so
// we look for tell-tale unix-epoch / very-old fallback values.
let created = sbom.document.created;
if created.timestamp() <= 0 {
violations.push(Violation {
severity: ViolationSeverity::Error,
category: ViolationCategory::DocumentMetadata,
message: "[BSI TR-03183-2 §5.2] SBOM created timestamp missing or invalid"
.to_string(),
element: None,
requirement: "BSI TR-03183-2 §5.2: ISO-8601 timestamp".to_string(),
rule_id: "SBOM-BSI-TR-03183-2-5-2",
standard_refs: Vec::new(),
});
}
// §5.3 — Component name (mandatory) — already enforced globally;
// we add a BSI-specific message only if many components are
// missing names (extreme case).
// §5.3 — Component identifier: PURL or other recognised ID (mandatory).
// Stricter than CRA: BSI requires a PURL where the ecosystem applies.
for comp in sbom.components.values() {
if !comp.identifiers.has_cra_identifier() {
violations.push(Violation {
severity: ViolationSeverity::Error,
category: ViolationCategory::ComponentIdentification,
message: format!(
"[BSI TR-03183-2 §5.3] Component '{}' missing unique identifier (PURL/CPE/SWHID/SWID)",
comp.name
),
element: Some(comp.name.clone()),
requirement: "BSI TR-03183-2 §5.3: Component identifier".to_string(),
rule_id: "SBOM-BSI-TR-03183-2-5-3",
standard_refs: Vec::new(),
});
}
}
// §5.4 — Cryptographic hash (SHA-256 or stronger) — mandatory.
let strong = |a: &HashAlgorithm| {
matches!(
a,
HashAlgorithm::Sha256
| HashAlgorithm::Sha384
| HashAlgorithm::Sha512
| HashAlgorithm::Sha3_256
| HashAlgorithm::Sha3_384
| HashAlgorithm::Sha3_512
| HashAlgorithm::Blake2b256
| HashAlgorithm::Blake2b384
| HashAlgorithm::Blake2b512
| HashAlgorithm::Blake3
)
};
for comp in sbom.components.values() {
let has_strong_hash = comp.hashes.iter().any(|h| strong(&h.algorithm));
if !has_strong_hash {
violations.push(Violation {
severity: ViolationSeverity::Error,
category: ViolationCategory::IntegrityInfo,
message: format!(
"[BSI TR-03183-2 §5.4] Component '{}' missing SHA-256+ cryptographic hash",
comp.name
),
element: Some(comp.name.clone()),
requirement: "BSI TR-03183-2 §5.4: Component cryptographic hash (SHA-256+)"
.to_string(),
rule_id: "SBOM-BSI-TR-03183-2-5-4",
standard_refs: Vec::new(),
});
}
}
// §5.5 — Dependencies (mandatory): explicit relationship graph required.
if sbom.edges.is_empty() && sbom.components.len() > 1 {
violations.push(Violation {
severity: ViolationSeverity::Error,
category: ViolationCategory::DependencyInfo,
message: "[BSI TR-03183-2 §5.5] SBOM declares multiple components but no dependency relationships"
.to_string(),
element: None,
requirement: "BSI TR-03183-2 §5.5: Dependency relationships".to_string(),
rule_id: "SBOM-BSI-TR-03183-2-5-5",
standard_refs: Vec::new(),
});
}
// §6 — Recommended: license information per component
let total = sbom.components.len();
let without_license = sbom
.components
.values()
.filter(|c| c.licenses.declared.is_empty() && c.licenses.concluded.is_none())
.count();
if without_license > 0 {
let pct = (without_license * 100) / total.max(1);
violations.push(Violation {
severity: if pct > 50 {
ViolationSeverity::Warning
} else {
ViolationSeverity::Info
},
category: ViolationCategory::LicenseInfo,
message: format!(
"[BSI TR-03183-2 §6] {without_license}/{total} components ({pct}%) missing license information"
),
element: None,
requirement: "BSI TR-03183-2 §6: Component license (recommended)".to_string(),
rule_id: "SBOM-BSI-TR-03183-2-6",
standard_refs: Vec::new(),
});
}
// §6 — Recommended: supplier per component
let without_supplier = sbom
.components
.values()
.filter(|c| c.supplier.is_none() && c.author.is_none())
.count();
if without_supplier > 0 {
let pct = (without_supplier * 100) / total.max(1);
violations.push(Violation {
severity: if pct > 50 {
ViolationSeverity::Warning
} else {
ViolationSeverity::Info
},
category: ViolationCategory::SupplierInfo,
message: format!(
"[BSI TR-03183-2 §6] {without_supplier}/{total} components ({pct}%) missing supplier information"
),
element: None,
requirement: "BSI TR-03183-2 §6: Component supplier (recommended)".to_string(),
rule_id: "SBOM-BSI-TR-03183-2-6",
standard_refs: Vec::new(),
});
}
}
}