1use serde::{Deserialize, Serialize};
2
3use super::ontology::{SoftwareEvidenceRef, SoftwareSourceKind};
4use super::validation::stable_software_id;
5use super::vocabulary::SOFTWARE_PROPERTIES;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10#[repr(usize)]
11pub enum SoftwarePredicate {
12 Contains,
13 ProvidesApi,
14 ConsumesApi,
15 DependsOn,
16 Configures,
17 Builds,
18 Produces,
19 Packages,
20 Deploys,
21 RunsAs,
22 Tests,
23 Documents,
24 DerivedFrom,
25 ObservedAs,
26 Supersedes,
27}
28
29impl SoftwarePredicate {
30 pub const fn as_str(self) -> &'static str {
31 SOFTWARE_PROPERTIES[self as usize].id
32 }
33
34 pub const fn rdf_local_name(self) -> &'static str {
36 SOFTWARE_PROPERTIES[self as usize].rdf_local_name
37 }
38
39 pub fn parse(value: &str) -> Option<Self> {
40 match value {
41 "contains" => Some(Self::Contains),
42 "provides_api" => Some(Self::ProvidesApi),
43 "consumes_api" => Some(Self::ConsumesApi),
44 "depends_on" => Some(Self::DependsOn),
45 "configures" => Some(Self::Configures),
46 "builds" => Some(Self::Builds),
47 "produces" => Some(Self::Produces),
48 "packages" => Some(Self::Packages),
49 "deploys" => Some(Self::Deploys),
50 "runs_as" => Some(Self::RunsAs),
51 "tests" => Some(Self::Tests),
52 "documents" => Some(Self::Documents),
53 "derived_from" => Some(Self::DerivedFrom),
54 "observed_as" => Some(Self::ObservedAs),
55 "supersedes" => Some(Self::Supersedes),
56 _ => None,
57 }
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
63#[serde(rename_all = "snake_case")]
64pub enum SoftwareAssertionMode {
65 Declared,
66 Extracted,
67 Observed,
68 Verified,
69 Inferred,
70}
71
72impl SoftwareAssertionMode {
73 pub const fn as_str(self) -> &'static str {
74 match self {
75 Self::Declared => "declared",
76 Self::Extracted => "extracted",
77 Self::Observed => "observed",
78 Self::Verified => "verified",
79 Self::Inferred => "inferred",
80 }
81 }
82
83 pub fn parse(value: &str) -> Option<Self> {
84 match value {
85 "declared" => Some(Self::Declared),
86 "extracted" => Some(Self::Extracted),
87 "observed" => Some(Self::Observed),
88 "verified" => Some(Self::Verified),
89 "inferred" => Some(Self::Inferred),
90 _ => None,
91 }
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case")]
98pub enum SoftwareStatementResolution {
99 Resolved,
100 Unresolved,
101 Ambiguous,
102 External,
103 Conflicting,
104}
105
106impl SoftwareStatementResolution {
107 pub const fn as_str(self) -> &'static str {
108 match self {
109 Self::Resolved => "resolved",
110 Self::Unresolved => "unresolved",
111 Self::Ambiguous => "ambiguous",
112 Self::External => "external",
113 Self::Conflicting => "conflicting",
114 }
115 }
116
117 pub fn parse(value: &str) -> Option<Self> {
118 match value {
119 "resolved" => Some(Self::Resolved),
120 "unresolved" => Some(Self::Unresolved),
121 "ambiguous" => Some(Self::Ambiguous),
122 "external" => Some(Self::External),
123 "conflicting" => Some(Self::Conflicting),
124 _ => None,
125 }
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
131#[serde(rename_all = "snake_case")]
132pub enum SoftwareFactState {
133 Active,
134 Conflicting,
135 Superseded,
136 Rejected,
137}
138
139impl SoftwareFactState {
140 pub const fn as_str(self) -> &'static str {
141 match self {
142 Self::Active => "active",
143 Self::Conflicting => "conflicting",
144 Self::Superseded => "superseded",
145 Self::Rejected => "rejected",
146 }
147 }
148
149 pub fn parse(value: &str) -> Option<Self> {
150 match value {
151 "active" => Some(Self::Active),
152 "conflicting" => Some(Self::Conflicting),
153 "superseded" => Some(Self::Superseded),
154 "rejected" => Some(Self::Rejected),
155 _ => None,
156 }
157 }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
162pub struct SoftwareStatement {
163 pub statement_id: String,
164 pub subject_id: String,
165 pub predicate: SoftwarePredicate,
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub object_id: Option<String>,
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub object_value: Option<String>,
170 pub source_scope: String,
171 pub source_kind: SoftwareSourceKind,
172 pub evidence_refs: Vec<SoftwareEvidenceRef>,
173 pub assertion_mode: SoftwareAssertionMode,
174 pub resolution_state: SoftwareStatementResolution,
175 #[serde(skip_serializing_if = "Option::is_none")]
176 pub valid_from: Option<u64>,
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub valid_to: Option<u64>,
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub observed_at: Option<u64>,
181 pub extractor_id: String,
182 pub extractor_version: String,
183 pub confidence_basis_points: u16,
184 pub fact_state: SoftwareFactState,
185}
186
187impl SoftwareStatement {
188 pub fn candidate(input: SoftwareStatementInput) -> Self {
190 let subject_id = input.subject_id.trim().to_owned();
191 let object_id = normalized_optional(input.object_id);
192 let object_value = normalized_optional(input.object_value);
193 let source_scope = input.source_scope.trim().to_owned();
194 let extractor_id = input.extractor_id.trim().to_owned();
195 let extractor_version = input.extractor_version.trim().to_owned();
196 let object_identity = object_id
197 .as_deref()
198 .or(object_value.as_deref())
199 .unwrap_or("missing-object");
200 let evidence_identity = input
201 .evidence_refs
202 .iter()
203 .map(|reference| reference.evidence_id.as_str())
204 .collect::<Vec<_>>()
205 .join("|");
206 let statement_id = stable_software_id(
207 "software_statement",
208 [
209 subject_id.as_str(),
210 input.predicate.as_str(),
211 object_identity,
212 source_scope.as_str(),
213 evidence_identity.as_str(),
214 extractor_id.as_str(),
215 extractor_version.as_str(),
216 ],
217 );
218
219 Self {
220 statement_id,
221 subject_id,
222 predicate: input.predicate,
223 object_id,
224 object_value,
225 source_scope,
226 source_kind: input.source_kind,
227 evidence_refs: input.evidence_refs,
228 assertion_mode: input.assertion_mode,
229 resolution_state: input.resolution_state,
230 valid_from: input.valid_from,
231 valid_to: input.valid_to,
232 observed_at: input.observed_at,
233 extractor_id,
234 extractor_version,
235 confidence_basis_points: input.confidence_basis_points,
236 fact_state: input.fact_state,
237 }
238 }
239
240 pub fn object_identity(&self) -> Option<&str> {
242 self.object_id.as_deref().or(self.object_value.as_deref())
243 }
244}
245
246#[derive(Debug, Clone, PartialEq, Eq)]
248pub struct SoftwareStatementInput {
249 pub subject_id: String,
250 pub predicate: SoftwarePredicate,
251 pub object_id: Option<String>,
252 pub object_value: Option<String>,
253 pub source_scope: String,
254 pub source_kind: SoftwareSourceKind,
255 pub evidence_refs: Vec<SoftwareEvidenceRef>,
256 pub assertion_mode: SoftwareAssertionMode,
257 pub resolution_state: SoftwareStatementResolution,
258 pub valid_from: Option<u64>,
259 pub valid_to: Option<u64>,
260 pub observed_at: Option<u64>,
261 pub extractor_id: String,
262 pub extractor_version: String,
263 pub confidence_basis_points: u16,
264 pub fact_state: SoftwareFactState,
265}
266
267fn normalized_optional(value: Option<String>) -> Option<String> {
268 value
269 .map(|value| value.trim().to_owned())
270 .filter(|value| !value.is_empty())
271}