1use std::cmp::Ordering;
2use std::collections::BTreeSet;
3
4use strum::{AsRefStr, EnumIter, EnumString, IntoEnumIterator, IntoStaticStr};
5
6use crate::de::{self, Error, ErrorKind, Obj, fail};
7use crate::digest::{Digest, hj};
8use crate::json::{self, Value};
9use crate::model::{
10 ArtifactId, BranchRef, ObjectFormat, OwnerId, RepoPathText, RepositoryIdentity, TreeIdentity,
11 UtcInstant,
12};
13use crate::resolution::{
14 BlobContent, BlobContentTag, BlobMode, BlobTarget, Missing, MissingTag, Resolution,
15 ResolutionTag, Target, TargetTag,
16};
17
18mod execution_constraint;
21mod trusted_time;
23
24pub use execution_constraint::{ConstraintPlatform, ExecutionConstraintDescriptor};
25pub use trusted_time::{STATEMENT_TTL_MAX_SECONDS, TrustedTimeStatement};
26
27pub const SCANNER_POLICY_PATH: &str = ".amiss/scanner-policy.json";
28
29const SCANNER_POLICY_SCHEMA: &str = "amiss/scanner-policy";
30const ORGANIZATION_FLOOR_SCHEMA: &str = "amiss/organization-floor";
31const DEBT_SNAPSHOT_SCHEMA: &str = "amiss/debt-snapshot";
32const WAIVER_BUNDLE_SCHEMA: &str = "amiss/waiver-bundle";
33
34const FINDING_KEY_INPUT_SCHEMA: &str = "amiss/scanner-finding-key-input";
35const FACT_SCHEMA: &str = "amiss/scanner-fact";
36pub const FINDING_KEY_DOMAIN: &str = "amiss/scanner-finding-key";
37pub const FACT_DOMAIN: &str = "amiss/scanner-fact";
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
40pub enum IncludeKind {
41 Document,
42 Tree,
43}
44
45impl IncludeKind {
46 #[must_use]
47 pub const fn as_str(self) -> &'static str {
48 match self {
49 Self::Document => "document",
50 Self::Tree => "tree",
51 }
52 }
53
54 fn decode(path: &str, value: Value) -> Result<Self, Error> {
55 match de::string(path, value)?.as_str() {
56 "document" => Ok(Self::Document),
57 "tree" => Ok(Self::Tree),
58 _ => fail(path, ErrorKind::InvalidValue),
59 }
60 }
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
64pub enum Disposition {
65 Warn,
66 Fail,
67}
68
69impl Disposition {
70 fn decode(path: &str, value: Value) -> Result<Self, Error> {
71 match de::string(path, value)?.as_str() {
72 "warn" => Ok(Self::Warn),
73 "fail" => Ok(Self::Fail),
74 _ => fail(path, ErrorKind::InvalidValue),
75 }
76 }
77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
80pub enum Profile {
81 Observe,
82 Enforce,
83}
84
85impl Profile {
86 pub fn decode(path: &str, value: Value) -> Result<Self, Error> {
90 match de::string(path, value)?.as_str() {
91 "observe" => Ok(Self::Observe),
92 "enforce" => Ok(Self::Enforce),
93 _ => fail(path, ErrorKind::InvalidValue),
94 }
95 }
96}
97
98#[derive(
99 Clone,
100 Copy,
101 Debug,
102 PartialEq,
103 Eq,
104 PartialOrd,
105 Ord,
106 AsRefStr,
107 EnumIter,
108 EnumString,
109 IntoStaticStr,
110)]
111#[strum(serialize_all = "kebab-case")]
112pub enum PromotableFindingKind {
113 ExplicitTargetMissing,
114 ExplicitTargetTypeMismatch,
115 InvalidReference,
116}
117
118impl PromotableFindingKind {
119 #[must_use]
120 pub fn as_str(self) -> &'static str {
121 self.into()
122 }
123
124 fn decode(path: &str, value: Value) -> Result<Self, Error> {
125 let raw = de::string(path, value)?;
126 raw.parse()
127 .map_err(|_unknown| Error::new(path, ErrorKind::InvalidValue))
128 }
129}
130
131#[derive(
132 Clone,
133 Copy,
134 Debug,
135 PartialEq,
136 Eq,
137 PartialOrd,
138 Ord,
139 AsRefStr,
140 EnumIter,
141 EnumString,
142 IntoStaticStr,
143)]
144#[strum(serialize_all = "kebab-case")]
145pub enum EligibleFindingKind {
146 ExplicitTargetMissing,
147 ExplicitTargetTypeMismatch,
148}
149
150impl EligibleFindingKind {
151 #[must_use]
152 pub fn as_str(self) -> &'static str {
153 self.into()
154 }
155
156 fn decode(path: &str, value: Value) -> Result<Self, Error> {
157 let raw = de::string(path, value)?;
158 raw.parse()
159 .map_err(|_unknown| Error::new(path, ErrorKind::InvalidValue))
160 }
161}
162
163#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
164pub enum SourceConstruct {
165 InlineLink,
166 FullReferenceLink,
167 CollapsedReferenceLink,
168 ShortcutReferenceLink,
169 Autolink,
170 InlineImage,
171 FullReferenceImage,
172 CollapsedReferenceImage,
173 ShortcutReferenceImage,
174}
175
176impl SourceConstruct {
177 #[must_use]
180 pub const fn is_image(self) -> bool {
181 match self {
182 Self::InlineImage
183 | Self::FullReferenceImage
184 | Self::CollapsedReferenceImage
185 | Self::ShortcutReferenceImage => true,
186 Self::InlineLink
187 | Self::FullReferenceLink
188 | Self::CollapsedReferenceLink
189 | Self::ShortcutReferenceLink
190 | Self::Autolink => false,
191 }
192 }
193
194 #[must_use]
195 pub const fn as_str(self) -> &'static str {
196 match self {
197 Self::InlineLink => "markdown-inline-link",
198 Self::FullReferenceLink => "markdown-full-reference-link",
199 Self::CollapsedReferenceLink => "markdown-collapsed-reference-link",
200 Self::ShortcutReferenceLink => "markdown-shortcut-reference-link",
201 Self::Autolink => "markdown-autolink",
202 Self::InlineImage => "markdown-inline-image",
203 Self::FullReferenceImage => "markdown-full-reference-image",
204 Self::CollapsedReferenceImage => "markdown-collapsed-reference-image",
205 Self::ShortcutReferenceImage => "markdown-shortcut-reference-image",
206 }
207 }
208
209 fn decode(path: &str, value: Value) -> Result<Self, Error> {
210 match de::string(path, value)?.as_str() {
211 "markdown-inline-link" => Ok(Self::InlineLink),
212 "markdown-full-reference-link" => Ok(Self::FullReferenceLink),
213 "markdown-collapsed-reference-link" => Ok(Self::CollapsedReferenceLink),
214 "markdown-shortcut-reference-link" => Ok(Self::ShortcutReferenceLink),
215 "markdown-autolink" => Ok(Self::Autolink),
216 "markdown-inline-image" => Ok(Self::InlineImage),
217 "markdown-full-reference-image" => Ok(Self::FullReferenceImage),
218 "markdown-collapsed-reference-image" => Ok(Self::CollapsedReferenceImage),
219 "markdown-shortcut-reference-image" => Ok(Self::ShortcutReferenceImage),
220 _ => fail(path, ErrorKind::InvalidValue),
221 }
222 }
223}
224
225#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
226pub enum TargetKind {
227 Blob,
228 Tree,
229 Either,
230}
231
232impl TargetKind {
233 #[must_use]
234 pub const fn as_str(self) -> &'static str {
235 match self {
236 Self::Blob => "blob",
237 Self::Tree => "tree",
238 Self::Either => "either",
239 }
240 }
241
242 fn decode(path: &str, value: Value) -> Result<Self, Error> {
243 match de::string(path, value)?.as_str() {
244 "blob" => Ok(Self::Blob),
245 "tree" => Ok(Self::Tree),
246 "either" => Ok(Self::Either),
247 _ => fail(path, ErrorKind::InvalidValue),
248 }
249 }
250}
251
252#[derive(Clone, Copy, Debug, PartialEq, Eq)]
253pub enum EntryKind {
254 Blob,
255 Tree,
256 Symlink,
257 Gitlink,
258}
259
260impl EntryKind {
261 #[must_use]
262 pub const fn as_str(self) -> &'static str {
263 match self {
264 Self::Blob => "blob",
265 Self::Tree => "tree",
266 Self::Symlink => "symlink",
267 Self::Gitlink => "gitlink",
268 }
269 }
270}
271
272#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
273pub enum GitMode {
274 RegularFile,
275 ExecutableFile,
276 Tree,
277 Symlink,
278 Gitlink,
279}
280
281impl GitMode {
282 #[must_use]
283 pub const fn as_str(self) -> &'static str {
284 match self {
285 Self::RegularFile => "100644",
286 Self::ExecutableFile => "100755",
287 Self::Tree => "040000",
288 Self::Symlink => "120000",
289 Self::Gitlink => "160000",
290 }
291 }
292}
293
294#[derive(Clone, Copy, Debug, PartialEq, Eq)]
295pub enum ContentAvailability {
296 Available,
297 NotRead,
298 NotApplicable,
299 LfsPointerOnly,
300}
301
302impl ContentAvailability {
303 #[must_use]
304 pub const fn as_str(self) -> &'static str {
305 match self {
306 Self::Available => "available",
307 Self::NotRead => "not-read",
308 Self::NotApplicable => "not-applicable",
309 Self::LfsPointerOnly => "lfs-pointer-only",
310 }
311 }
312}
313
314#[derive(
315 Clone,
316 Copy,
317 Debug,
318 PartialEq,
319 Eq,
320 PartialOrd,
321 Ord,
322 AsRefStr,
323 EnumString,
324 EnumIter,
325 IntoStaticStr,
326)]
327#[strum(serialize_all = "kebab-case")]
328pub enum ResourceName {
329 GitObjectBytes,
330 GitCompressedObjectBytes,
331 AggregateGitCompressedObjectBytesPerEvaluation,
332 GitPackDirectoryEntries,
333 GitPackFiles,
334 GitPackIndexBytes,
335 AggregateGitPackIndexBytes,
336 GitDeltaDepth,
337 GitIndexBytes,
338 GitTreeEntriesPerSnapshot,
339 DocumentsPerSnapshot,
340 ControlInputBytes,
341 SelectedControlBlobBytes,
342 AggregateSelectedControlBytesPerSnapshot,
343 RepositoryPolicyEntries,
344 DebtItems,
345 WaiverItems,
346 RawPathBytes,
347 DocumentBlobBytes,
348 ReferencedTargetBlobBytes,
349 AggregateReferencedTargetBytesPerSnapshot,
350 AggregateLineFragmentEvaluationBytesPerSnapshot,
351 AggregateDocumentBytesPerSnapshot,
352 RawLinkDestinationBytes,
353 ParserNesting,
354 ParserNodesPerDocument,
355 ParserNodesPerSnapshot,
356 AggregateEmbeddedCodeEvaluationBytesPerSnapshot,
357 ReferencesPerDocument,
358 ReferencesPerSnapshot,
359 OrganizationPolicyEntries,
360 CompleteFindings,
361 TypedAnalysisErrorsRetained,
362 MachineJsonBytes,
363 PrivateTemporaryStorageBytes,
364 EvaluatorManagedMemoryBytes,
365}
366
367impl ResourceName {
368 #[must_use]
370 pub fn all() -> impl ExactSizeIterator<Item = Self> {
371 Self::iter()
372 }
373
374 #[must_use]
376 pub const fn phase(self) -> &'static str {
377 match self {
378 Self::ControlInputBytes
379 | Self::RepositoryPolicyEntries
380 | Self::DebtItems
381 | Self::WaiverItems
382 | Self::OrganizationPolicyEntries => "configuration",
383 Self::GitObjectBytes
384 | Self::GitCompressedObjectBytes
385 | Self::AggregateGitCompressedObjectBytesPerEvaluation
386 | Self::GitPackDirectoryEntries
387 | Self::GitPackFiles
388 | Self::GitPackIndexBytes
389 | Self::AggregateGitPackIndexBytes
390 | Self::GitDeltaDepth
391 | Self::GitIndexBytes
392 | Self::GitTreeEntriesPerSnapshot
393 | Self::RawPathBytes => "git",
394 Self::DocumentsPerSnapshot
395 | Self::DocumentBlobBytes
396 | Self::AggregateDocumentBytesPerSnapshot
397 | Self::SelectedControlBlobBytes
398 | Self::AggregateSelectedControlBytesPerSnapshot => "discovery",
399 Self::RawLinkDestinationBytes
400 | Self::ParserNesting
401 | Self::ParserNodesPerDocument
402 | Self::ParserNodesPerSnapshot
403 | Self::AggregateEmbeddedCodeEvaluationBytesPerSnapshot
404 | Self::ReferencesPerDocument
405 | Self::ReferencesPerSnapshot => "parse",
406 Self::ReferencedTargetBlobBytes
407 | Self::AggregateReferencedTargetBytesPerSnapshot
408 | Self::AggregateLineFragmentEvaluationBytesPerSnapshot => "resolution",
409 Self::CompleteFindings => "policy",
410 Self::MachineJsonBytes => "output",
411 Self::TypedAnalysisErrorsRetained
412 | Self::PrivateTemporaryStorageBytes
413 | Self::EvaluatorManagedMemoryBytes => "internal",
414 }
415 }
416
417 #[must_use]
418 pub fn as_str(self) -> &'static str {
419 self.into()
420 }
421
422 fn decode(path: &str, value: Value) -> Result<Self, Error> {
423 let raw = de::string(path, value)?;
424 let Ok(resource) = raw.parse() else {
425 return fail(path, ErrorKind::InvalidValue);
426 };
427 Ok(resource)
428 }
429}
430
431#[derive(Clone, Debug, PartialEq, Eq)]
432pub struct DocumentInclude {
433 pub path: RepoPathText,
434 pub kind: IncludeKind,
435}
436
437#[derive(Clone, Debug, PartialEq, Eq)]
438pub struct FindingDisposition {
439 pub finding_kind: PromotableFindingKind,
440 pub disposition: Disposition,
441}
442
443#[derive(Clone, Debug, PartialEq, Eq)]
444pub struct ScannerPolicy {
445 pub digest: Digest,
446 pub document_includes: Vec<DocumentInclude>,
447 pub protected_inventory: Vec<RepoPathText>,
448 pub finding_dispositions: Vec<FindingDisposition>,
449}
450
451impl ScannerPolicy {
452 pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
457 let value = root(bytes)?;
458 let digest = hj(SCANNER_POLICY_SCHEMA, &value);
459 let mut obj = Obj::new("$", value)?;
460 de::const_str(
461 &obj.field("schema"),
462 obj.take("schema")?,
463 SCANNER_POLICY_SCHEMA,
464 )?;
465
466 let includes_path = obj.field("document_includes");
467 let includes = de::array(&includes_path, obj.take("document_includes")?)?;
468 let document_includes = decode_items(&includes_path, includes, 100_000, decode_include)?;
469 sorted_set(&includes_path, &document_includes, |a, b| {
470 (a.path.as_str(), a.kind).cmp(&(b.path.as_str(), b.kind))
471 })?;
472
473 let inventory_path = obj.field("protected_inventory");
474 let protected_inventory =
475 decode_path_set(&inventory_path, obj.take("protected_inventory")?)?;
476
477 let dispositions_path = obj.field("finding_dispositions");
478 let raw = de::array(&dispositions_path, obj.take("finding_dispositions")?)?;
479 let finding_dispositions =
480 decode_items(&dispositions_path, raw, 3, decode_disposition_rule)?;
481 sorted_set(&dispositions_path, &finding_dispositions, |a, b| {
482 a.finding_kind.as_str().cmp(b.finding_kind.as_str())
483 })?;
484
485 obj.finish()?;
486 Ok(Self {
487 digest,
488 document_includes,
489 protected_inventory,
490 finding_dispositions,
491 })
492 }
493}
494
495#[derive(Clone, Debug, PartialEq, Eq)]
496pub struct ResourceLimit {
497 pub resource: ResourceName,
498 pub maximum: i64,
499}
500
501#[derive(Clone, Debug, PartialEq, Eq)]
502pub struct FloorDisposition {
503 pub finding_kind: PromotableFindingKind,
504 pub disposition: Disposition,
505}
506
507#[derive(Clone, Debug, PartialEq, Eq)]
508pub struct OrganizationFloor {
509 pub digest: Digest,
510 pub floor_id: ArtifactId,
511 pub repository: RepositoryIdentity,
512 pub ref_name: BranchRef,
513 pub minimum_profile: Profile,
514 pub minimum_dispositions: Vec<FindingDisposition>,
515 pub protected_inventory: Vec<RepoPathText>,
516 pub protected_control_paths: Vec<RepoPathText>,
517 pub waivable_finding_kinds: Vec<EligibleFindingKind>,
518 pub authorized_debt_owners: Vec<OwnerId>,
519 pub authorized_waiver_issuers: Vec<OwnerId>,
520 pub resource_limits: Vec<ResourceLimit>,
521}
522
523#[derive(Clone, Debug, PartialEq, Eq)]
526pub enum FloorDefect {
527 Schema(Error),
528 Entries {
529 configured_limit: u64,
530 observed_lower_bound: u64,
531 },
532}
533
534impl From<Error> for FloorDefect {
535 fn from(error: Error) -> Self {
536 Self::Schema(error)
537 }
538}
539
540pub const ORGANIZATION_POLICY_ENTRIES_LIMIT: u64 = 100_000;
541
542impl OrganizationFloor {
543 #[must_use]
544 pub const fn schema(&self) -> &'static str {
545 ORGANIZATION_FLOOR_SCHEMA
546 }
547
548 pub fn parse(bytes: &[u8]) -> Result<Self, FloorDefect> {
555 let value = root(bytes)?;
556 let digest = hj(ORGANIZATION_FLOOR_SCHEMA, &value);
557 let mut obj = Obj::new("$", value)?;
558 de::const_str(
559 &obj.field("schema"),
560 obj.take("schema")?,
561 ORGANIZATION_FLOOR_SCHEMA,
562 )?;
563
564 let floor_id = decode_artifact_id(&obj.field("floor_id"), obj.take("floor_id")?)?;
565 let repository = decode_repository(&obj.field("repository"), obj.take("repository")?)?;
566 let ref_name = decode_branch_ref(&obj.field("ref"), obj.take("ref")?)?;
567 let minimum_profile =
568 Profile::decode(&obj.field("minimum_profile"), obj.take("minimum_profile")?)?;
569
570 let dispositions_path = obj.field("minimum_dispositions");
571 let dispositions_raw = de::array(&dispositions_path, obj.take("minimum_dispositions")?)?;
572 let inventory_path = obj.field("protected_inventory");
573 let inventory_raw = de::array(&inventory_path, obj.take("protected_inventory")?)?;
574 let control_paths_path = obj.field("protected_control_paths");
575 let control_paths_raw =
576 de::array(&control_paths_path, obj.take("protected_control_paths")?)?;
577 let waivable_path = obj.field("waivable_finding_kinds");
578 let waivable_raw = de::array(&waivable_path, obj.take("waivable_finding_kinds")?)?;
579 let owners_path = obj.field("authorized_debt_owners");
580 let owners_raw = de::array(&owners_path, obj.take("authorized_debt_owners")?)?;
581 let issuers_path = obj.field("authorized_waiver_issuers");
582 let issuers_raw = de::array(&issuers_path, obj.take("authorized_waiver_issuers")?)?;
583 let limits_path = obj.field("resource_limits");
584 let limits_raw = de::array(&limits_path, obj.take("resource_limits")?)?;
585
586 let combined = [
587 dispositions_raw.len(),
588 inventory_raw.len(),
589 control_paths_raw.len(),
590 waivable_raw.len(),
591 owners_raw.len(),
592 issuers_raw.len(),
593 limits_raw.len(),
594 ]
595 .iter()
596 .map(|&len| u64::try_from(len).unwrap_or(u64::MAX))
597 .fold(0_u64, u64::saturating_add);
598 if combined > ORGANIZATION_POLICY_ENTRIES_LIMIT {
599 return Err(FloorDefect::Entries {
600 configured_limit: ORGANIZATION_POLICY_ENTRIES_LIMIT,
601 observed_lower_bound: ORGANIZATION_POLICY_ENTRIES_LIMIT.saturating_add(1),
602 });
603 }
604
605 let minimum_dispositions = decode_items(
606 &dispositions_path,
607 dispositions_raw,
608 3,
609 decode_disposition_rule,
610 )?;
611 sorted_set(&dispositions_path, &minimum_dispositions, |a, b| {
612 a.finding_kind.as_str().cmp(b.finding_kind.as_str())
613 })?;
614 let protected_inventory = decode_path_items(&inventory_path, inventory_raw)?;
615 let protected_control_paths = decode_path_items(&control_paths_path, control_paths_raw)?;
616 let waivable_finding_kinds =
617 decode_items(&waivable_path, waivable_raw, 2, |path, value| {
618 EligibleFindingKind::decode(path, value)
619 })?;
620 sorted_set(&waivable_path, &waivable_finding_kinds, |a, b| {
621 a.as_str().cmp(b.as_str())
622 })?;
623 let authorized_debt_owners = decode_owner_items(&owners_path, owners_raw)?;
624 let authorized_waiver_issuers = decode_owner_items(&issuers_path, issuers_raw)?;
625 let cap = ResourceName::all().len();
626 let resource_limits = decode_items(&limits_path, limits_raw, cap, decode_resource_limit)?;
627 sorted_set(&limits_path, &resource_limits, |a, b| {
628 a.resource.as_str().cmp(b.resource.as_str())
629 })?;
630
631 obj.finish()?;
632 if let Some(declared) = resource_limits
633 .iter()
634 .find(|row| row.resource == ResourceName::OrganizationPolicyEntries)
635 {
636 let declared = u64::try_from(declared.maximum).unwrap_or(u64::MAX);
637 if combined > declared {
638 return Err(FloorDefect::Entries {
639 configured_limit: declared,
640 observed_lower_bound: declared.saturating_add(1),
641 });
642 }
643 }
644 Ok(Self {
645 digest,
646 floor_id,
647 repository,
648 ref_name,
649 minimum_profile,
650 minimum_dispositions,
651 protected_inventory,
652 protected_control_paths,
653 waivable_finding_kinds,
654 authorized_debt_owners,
655 authorized_waiver_issuers,
656 resource_limits,
657 })
658 }
659}
660
661#[derive(Clone, Debug, PartialEq, Eq)]
662pub struct TargetIntent {
663 pub path: RepoPathText,
664 pub target_kind: TargetKind,
665 pub query_digest: Option<Digest>,
666 pub fragment_digest: Option<Digest>,
667}
668
669#[derive(Clone, Debug, PartialEq, Eq)]
670pub struct FindingScope {
671 pub document: RepoPathText,
672 pub source_construct: SourceConstruct,
673 pub normalized_target_intent: TargetIntent,
674 pub source_projection_digest: Digest,
675}
676
677#[derive(Clone, Debug, PartialEq, Eq)]
678pub struct FindingKeyInput {
679 pub finding_kind: EligibleFindingKind,
680 pub scope: FindingScope,
681}
682
683#[derive(Clone, Debug, PartialEq, Eq)]
684pub struct Fact {
685 key_input: FindingKeyInput,
686 resolution: Resolution<RepoPathText>,
687}
688
689impl Fact {
690 #[must_use]
693 pub fn new(key_input: FindingKeyInput, resolution: Resolution<RepoPathText>) -> Option<Self> {
694 let expected = match &resolution {
695 Resolution::Missing(_) => EligibleFindingKind::ExplicitTargetMissing,
696 Resolution::TypeMismatch(_) => EligibleFindingKind::ExplicitTargetTypeMismatch,
697 Resolution::Resolved(_)
698 | Resolution::UnsupportedTarget(_)
699 | Resolution::UnsupportedSemantics(_)
700 | Resolution::UnsupportedVersion(_)
701 | Resolution::Invalid(_)
702 | Resolution::External(_) => return None,
703 };
704 (key_input.finding_kind == expected).then_some(Self {
705 key_input,
706 resolution,
707 })
708 }
709
710 #[must_use]
712 pub const fn finding_kind(&self) -> EligibleFindingKind {
713 self.key_input.finding_kind
714 }
715
716 #[must_use]
718 pub const fn key_input(&self) -> &FindingKeyInput {
719 &self.key_input
720 }
721
722 #[must_use]
724 pub const fn resolution(&self) -> &Resolution<RepoPathText> {
725 &self.resolution
726 }
727}
728
729#[derive(Clone, Debug, PartialEq, Eq)]
730pub struct DebtItem {
731 pub debt_id: ArtifactId,
732 pub finding_key: Digest,
733 pub accepted_fact: Fact,
734 pub accepted_fact_digest: Digest,
735 pub owner: OwnerId,
736 pub reason: String,
737 pub created_at: UtcInstant,
738 pub expires_at: UtcInstant,
739}
740
741#[derive(Clone, Debug, PartialEq, Eq)]
742pub struct DebtSnapshot {
743 pub digest: Digest,
744 pub repository: RepositoryIdentity,
745 pub ref_name: BranchRef,
746 pub organization_floor_digest: Digest,
747 pub adoption_tree: TreeIdentity,
748 pub adoption_report_payload_digest: Digest,
749 pub created_at: UtcInstant,
750 pub items: Vec<DebtItem>,
751}
752
753impl DebtSnapshot {
754 #[must_use]
755 pub const fn schema(&self) -> &'static str {
756 DEBT_SNAPSHOT_SCHEMA
757 }
758
759 pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
765 let value = root(bytes)?;
766 let digest = hj(DEBT_SNAPSHOT_SCHEMA, &value);
767 let mut obj = Obj::new("$", value)?;
768 de::const_str(
769 &obj.field("schema"),
770 obj.take("schema")?,
771 DEBT_SNAPSHOT_SCHEMA,
772 )?;
773
774 let repository = decode_repository(&obj.field("repository"), obj.take("repository")?)?;
775 let ref_name = decode_branch_ref(&obj.field("ref"), obj.take("ref")?)?;
776 let organization_floor_digest = decode_digest(
777 &obj.field("organization_floor_digest"),
778 obj.take("organization_floor_digest")?,
779 )?;
780 let adoption_tree = decode_tree(&obj.field("adoption_tree"), obj.take("adoption_tree")?)?;
781 let adoption_report_payload_digest = decode_digest(
782 &obj.field("adoption_report_payload_digest"),
783 obj.take("adoption_report_payload_digest")?,
784 )?;
785 let created_at = decode_instant(&obj.field("created_at"), obj.take("created_at")?)?;
786
787 let items_path = obj.field("items");
788 let raw = de::array(&items_path, obj.take("items")?)?;
789 let items = decode_items(&items_path, raw, 100_000, decode_debt_item)?;
790 sorted_set(&items_path, &items, |a, b| {
791 a.debt_id.as_str().cmp(b.debt_id.as_str())
792 })?;
793 let mut keys: BTreeSet<Digest> = BTreeSet::new();
794 for item in &items {
795 if !keys.insert(item.finding_key) {
796 return fail(&items_path, ErrorKind::DuplicateMember);
797 }
798 if item.created_at > created_at {
799 return fail(&items_path, ErrorKind::Inconsistent);
800 }
801 }
802
803 obj.finish()?;
804 Ok(Self {
805 digest,
806 repository,
807 ref_name,
808 organization_floor_digest,
809 adoption_tree,
810 adoption_report_payload_digest,
811 created_at,
812 items,
813 })
814 }
815}
816
817#[derive(Clone, Debug, PartialEq, Eq)]
818pub struct WaiverItem {
819 pub waiver_id: ArtifactId,
820 pub finding_key: Digest,
821 pub authorized_fact: Fact,
822 pub authorized_fact_digest: Digest,
823 pub candidate_tree: TreeIdentity,
824 pub owner: OwnerId,
825 pub issuer: OwnerId,
826 pub reason: String,
827 pub created_at: UtcInstant,
828 pub not_before: UtcInstant,
829 pub expires_at: UtcInstant,
830}
831
832#[derive(Clone, Debug, PartialEq, Eq)]
833pub struct WaiverBundle {
834 pub digest: Digest,
835 pub repository: RepositoryIdentity,
836 pub ref_name: BranchRef,
837 pub organization_floor_digest: Digest,
838 pub created_at: UtcInstant,
839 pub items: Vec<WaiverItem>,
840}
841
842impl WaiverBundle {
843 #[must_use]
844 pub const fn schema(&self) -> &'static str {
845 WAIVER_BUNDLE_SCHEMA
846 }
847
848 pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
855 let value = root(bytes)?;
856 let digest = hj(WAIVER_BUNDLE_SCHEMA, &value);
857 let mut obj = Obj::new("$", value)?;
858 de::const_str(
859 &obj.field("schema"),
860 obj.take("schema")?,
861 WAIVER_BUNDLE_SCHEMA,
862 )?;
863
864 let repository = decode_repository(&obj.field("repository"), obj.take("repository")?)?;
865 let ref_name = decode_branch_ref(&obj.field("ref"), obj.take("ref")?)?;
866 let organization_floor_digest = decode_digest(
867 &obj.field("organization_floor_digest"),
868 obj.take("organization_floor_digest")?,
869 )?;
870 let created_at = decode_instant(&obj.field("created_at"), obj.take("created_at")?)?;
871
872 let items_path = obj.field("items");
873 let raw = de::array(&items_path, obj.take("items")?)?;
874 let items = decode_items(&items_path, raw, 100_000, decode_waiver_item)?;
875 sorted_set(&items_path, &items, |a, b| {
876 waiver_sort_key(a).cmp(&waiver_sort_key(b))
877 })?;
878 for pair in items.windows(2) {
879 if let [left, right] = pair
880 && left.candidate_tree == right.candidate_tree
881 && left.finding_key == right.finding_key
882 {
883 return fail(&items_path, ErrorKind::DuplicateMember);
884 }
885 }
886 let mut ids: BTreeSet<&str> = BTreeSet::new();
887 for item in &items {
888 if !ids.insert(item.waiver_id.as_str()) {
889 return fail(&items_path, ErrorKind::DuplicateMember);
890 }
891 if item.created_at > created_at {
892 return fail(&items_path, ErrorKind::Inconsistent);
893 }
894 }
895
896 obj.finish()?;
897 Ok(Self {
898 digest,
899 repository,
900 ref_name,
901 organization_floor_digest,
902 created_at,
903 items,
904 })
905 }
906}
907
908fn waiver_sort_key(item: &WaiverItem) -> (ObjectFormat, &str, Digest, &str) {
909 (
910 item.candidate_tree.object_format,
911 item.candidate_tree.tree_oid.as_str(),
912 item.finding_key,
913 item.waiver_id.as_str(),
914 )
915}
916
917pub fn root(bytes: &[u8]) -> Result<Value, Error> {
923 json::parse(bytes).map_err(|defect| Error::new("$", ErrorKind::Json(defect)))
924}
925
926fn decode_items<T>(
927 path: &str,
928 raw: Vec<Value>,
929 limit: usize,
930 decode: impl Fn(&str, Value) -> Result<T, Error>,
931) -> Result<Vec<T>, Error> {
932 if raw.len() > limit {
933 return fail(path, ErrorKind::LimitExceeded);
934 }
935 raw.into_iter()
936 .enumerate()
937 .map(|(index, value)| decode(&format!("{path}[{index}]"), value))
938 .collect()
939}
940
941fn sorted_set<T>(
942 path: &str,
943 items: &[T],
944 compare: impl Fn(&T, &T) -> Ordering,
945) -> Result<(), Error> {
946 for pair in items.windows(2) {
947 if let [left, right] = pair {
948 match compare(left, right) {
949 Ordering::Less => {}
950 Ordering::Equal => return fail(path, ErrorKind::DuplicateMember),
951 Ordering::Greater => return fail(path, ErrorKind::UnsortedSet),
952 }
953 }
954 }
955 Ok(())
956}
957
958fn decode_include(path: &str, value: Value) -> Result<DocumentInclude, Error> {
959 let mut obj = Obj::new(path, value)?;
960 let include_path = decode_repo_path(&obj.field("path"), obj.take("path")?)?;
961 let kind = IncludeKind::decode(&obj.field("kind"), obj.take("kind")?)?;
962 obj.finish()?;
963 Ok(DocumentInclude {
964 path: include_path,
965 kind,
966 })
967}
968
969fn decode_disposition_rule(path: &str, value: Value) -> Result<FindingDisposition, Error> {
970 let mut obj = Obj::new(path, value)?;
971 let finding_kind =
972 PromotableFindingKind::decode(&obj.field("finding_kind"), obj.take("finding_kind")?)?;
973 let disposition = Disposition::decode(&obj.field("disposition"), obj.take("disposition")?)?;
974 obj.finish()?;
975 Ok(FindingDisposition {
976 finding_kind,
977 disposition,
978 })
979}
980
981fn decode_resource_limit(path: &str, value: Value) -> Result<ResourceLimit, Error> {
982 let mut obj = Obj::new(path, value)?;
983 let resource = ResourceName::decode(&obj.field("resource"), obj.take("resource")?)?;
984 let maximum_path = obj.field("maximum");
985 let maximum = de::integer(&maximum_path, obj.take("maximum")?)?;
986 obj.finish()?;
987 let in_bounds = if resource == ResourceName::TypedAnalysisErrorsRetained {
988 (1..=64).contains(&maximum)
989 } else if resource == ResourceName::MachineJsonBytes {
990 maximum == 67_108_864
991 } else {
992 maximum >= 0
993 };
994 if in_bounds {
995 Ok(ResourceLimit { resource, maximum })
996 } else {
997 fail(&maximum_path, ErrorKind::InvalidValue)
998 }
999}
1000
1001fn decode_path_set(path: &str, value: Value) -> Result<Vec<RepoPathText>, Error> {
1002 decode_path_items(path, de::array(path, value)?)
1003}
1004
1005fn decode_path_items(path: &str, raw: Vec<Value>) -> Result<Vec<RepoPathText>, Error> {
1006 let paths = decode_items(path, raw, 100_000, decode_repo_path)?;
1007 sorted_set(path, &paths, |a, b| a.as_str().cmp(b.as_str()))?;
1008 Ok(paths)
1009}
1010
1011fn decode_owner_items(path: &str, raw: Vec<Value>) -> Result<Vec<OwnerId>, Error> {
1012 let owners = decode_items(path, raw, 10_000, decode_owner)?;
1013 sorted_set(path, &owners, |a, b| a.as_str().cmp(b.as_str()))?;
1014 Ok(owners)
1015}
1016
1017fn decode_repo_path(path: &str, value: Value) -> Result<RepoPathText, Error> {
1018 RepoPathText::new(de::string(path, value)?)
1019 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1020}
1021
1022fn decode_artifact_id(path: &str, value: Value) -> Result<ArtifactId, Error> {
1023 ArtifactId::new(de::string(path, value)?)
1024 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1025}
1026
1027fn decode_owner(path: &str, value: Value) -> Result<OwnerId, Error> {
1028 OwnerId::new(de::string(path, value)?).ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1029}
1030
1031fn decode_branch_ref(path: &str, value: Value) -> Result<BranchRef, Error> {
1032 BranchRef::new(de::string(path, value)?)
1033 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1034}
1035
1036fn decode_instant(path: &str, value: Value) -> Result<UtcInstant, Error> {
1037 UtcInstant::new(de::string(path, value)?)
1038 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1039}
1040
1041fn decode_digest(path: &str, value: Value) -> Result<Digest, Error> {
1042 let raw = de::string(path, value)?;
1043 Digest::from_wire(&raw).ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1044}
1045
1046fn decode_nullable_digest(path: &str, value: Value) -> Result<Option<Digest>, Error> {
1047 de::nullable(value)
1048 .map(|v| decode_digest(path, v))
1049 .transpose()
1050}
1051
1052pub(crate) fn decode_repository(path: &str, value: Value) -> Result<RepositoryIdentity, Error> {
1053 let mut obj = Obj::new(path, value)?;
1054 let host = de::string(&obj.field("host"), obj.take("host")?)?;
1055 let owner = de::string(&obj.field("owner"), obj.take("owner")?)?;
1056 let name = de::string(&obj.field("name"), obj.take("name")?)?;
1057 obj.finish()?;
1058 RepositoryIdentity::new(host, owner, name)
1059 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1060}
1061
1062pub(crate) fn decode_provider_run_id(path: &str, value: Value) -> Result<String, Error> {
1063 let raw = de::string(path, value)?;
1064 let bytes = raw.as_bytes();
1065 let allowed = |byte: &u8| {
1066 byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b':' | b'/' | b'-')
1067 };
1068 if bytes.is_empty()
1069 || bytes.len() > 128
1070 || !bytes.first().is_some_and(u8::is_ascii_alphanumeric)
1071 || !bytes.last().is_some_and(u8::is_ascii_alphanumeric)
1072 || !bytes.iter().all(allowed)
1073 {
1074 return fail(path, ErrorKind::InvalidValue);
1075 }
1076 Ok(raw)
1077}
1078
1079pub(crate) fn decode_provider_id(path: &str, value: Value) -> Result<String, Error> {
1080 let raw = de::string(path, value)?;
1081 if ArtifactId::new(raw.clone()).is_some() {
1082 Ok(raw)
1083 } else {
1084 fail(path, ErrorKind::InvalidValue)
1085 }
1086}
1087
1088fn decode_tree(path: &str, value: Value) -> Result<TreeIdentity, Error> {
1089 let mut obj = Obj::new(path, value)?;
1090 let format_path = obj.field("object_format");
1091 let object_format = match de::string(&format_path, obj.take("object_format")?)?.as_str() {
1092 "sha1" => ObjectFormat::Sha1,
1093 "sha256" => ObjectFormat::Sha256,
1094 _ => return fail(&format_path, ErrorKind::InvalidValue),
1095 };
1096 let tree_oid = de::string(&obj.field("tree_oid"), obj.take("tree_oid")?)?;
1097 obj.finish()?;
1098 TreeIdentity::new(object_format, tree_oid)
1099 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1100}
1101
1102fn decode_reason(path: &str, value: Value) -> Result<String, Error> {
1103 let raw = de::string(path, value)?;
1104 let length = raw.chars().count();
1105 if (1..=1024).contains(&length) && raw.chars().any(|c| !c.is_whitespace()) {
1106 Ok(raw)
1107 } else {
1108 fail(path, ErrorKind::InvalidValue)
1109 }
1110}
1111
1112fn decode_intent(path: &str, value: Value) -> Result<TargetIntent, Error> {
1113 let mut obj = Obj::new(path, value)?;
1114 de::const_str(&obj.field("kind"), obj.take("kind")?, "repository-path")?;
1115 let target_path = decode_repo_path(&obj.field("path"), obj.take("path")?)?;
1116 let target_kind = TargetKind::decode(&obj.field("target_kind"), obj.take("target_kind")?)?;
1117 let query_digest =
1118 decode_nullable_digest(&obj.field("query_digest"), obj.take("query_digest")?)?;
1119 let fragment_digest =
1120 decode_nullable_digest(&obj.field("fragment_digest"), obj.take("fragment_digest")?)?;
1121 obj.finish()?;
1122 Ok(TargetIntent {
1123 path: target_path,
1124 target_kind,
1125 query_digest,
1126 fragment_digest,
1127 })
1128}
1129
1130fn decode_scope(path: &str, value: Value) -> Result<FindingScope, Error> {
1131 let mut obj = Obj::new(path, value)?;
1132 de::const_str(&obj.field("kind"), obj.take("kind")?, "reference")?;
1133 let document = decode_repo_path(&obj.field("document"), obj.take("document")?)?;
1134 let source_construct = SourceConstruct::decode(
1135 &obj.field("source_construct"),
1136 obj.take("source_construct")?,
1137 )?;
1138 let normalized_target_intent = decode_intent(
1139 &obj.field("normalized_target_intent"),
1140 obj.take("normalized_target_intent")?,
1141 )?;
1142 let occurrence_path = obj.field("occurrence");
1143 let mut occurrence = Obj::new(&occurrence_path, obj.take("occurrence")?)?;
1144 de::const_str(
1145 &occurrence.field("kind"),
1146 occurrence.take("kind")?,
1147 "source-projection",
1148 )?;
1149 let source_projection_digest = decode_digest(
1150 &occurrence.field("source_projection_digest"),
1151 occurrence.take("source_projection_digest")?,
1152 )?;
1153 occurrence.finish()?;
1154 obj.finish()?;
1155 Ok(FindingScope {
1156 document,
1157 source_construct,
1158 normalized_target_intent,
1159 source_projection_digest,
1160 })
1161}
1162
1163fn decode_key_input(path: &str, value: Value) -> Result<(FindingKeyInput, Digest), Error> {
1164 let digest = hj(FINDING_KEY_DOMAIN, &value);
1165 let mut obj = Obj::new(path, value)?;
1166 de::const_str(
1167 &obj.field("schema"),
1168 obj.take("schema")?,
1169 FINDING_KEY_INPUT_SCHEMA,
1170 )?;
1171 let finding_kind =
1172 EligibleFindingKind::decode(&obj.field("finding_kind"), obj.take("finding_kind")?)?;
1173 let scope = decode_scope(&obj.field("scope"), obj.take("scope")?)?;
1174 obj.finish()?;
1175 Ok((
1176 FindingKeyInput {
1177 finding_kind,
1178 scope,
1179 },
1180 digest,
1181 ))
1182}
1183
1184fn decode_resolution(path: &str, value: Value) -> Result<Resolution<RepoPathText>, Error> {
1185 let mut obj = Obj::new(path, value)?;
1186 let kind_path = obj.field("kind");
1187 let kind_text = de::string(&kind_path, obj.take("kind")?)?;
1188 let Ok(kind) = kind_text.parse::<ResolutionTag>() else {
1189 return fail(&kind_path, ErrorKind::InvalidValue);
1190 };
1191 match kind {
1192 ResolutionTag::Missing => {
1193 let reason_path = obj.field("reason");
1194 let reason_text = de::string(&reason_path, obj.take("reason")?)?;
1195 let Ok(reason) = reason_text.parse::<MissingTag>() else {
1196 return fail(&reason_path, ErrorKind::InvalidValue);
1197 };
1198 let resolved_path = decode_repo_path(&obj.field("path"), obj.take("path")?)?;
1199 obj.finish()?;
1200 Ok(Resolution::Missing(match reason {
1201 MissingTag::PathNotFound => Missing::PathNotFound {
1202 path: resolved_path,
1203 },
1204 MissingTag::LineFragmentOutOfRange => Missing::LineFragmentOutOfRange {
1205 path: resolved_path,
1206 },
1207 }))
1208 }
1209 ResolutionTag::TypeMismatch => {
1210 let target = decode_resolution_target(&obj.field("target"), obj.take("target")?)?;
1211 obj.finish()?;
1212 Ok(Resolution::TypeMismatch(target))
1213 }
1214 ResolutionTag::Resolved
1215 | ResolutionTag::UnsupportedTarget
1216 | ResolutionTag::UnsupportedSemantics
1217 | ResolutionTag::UnsupportedVersion
1218 | ResolutionTag::Invalid
1219 | ResolutionTag::External => fail(&kind_path, ErrorKind::InvalidValue),
1220 }
1221}
1222
1223fn decode_resolution_target(path: &str, value: Value) -> Result<Target<RepoPathText>, Error> {
1224 let mut obj = Obj::new(path, value)?;
1225 let kind_path = obj.field("kind");
1226 let kind_text = de::string(&kind_path, obj.take("kind")?)?;
1227 let Ok(kind) = kind_text.parse::<TargetTag>() else {
1228 return fail(&kind_path, ErrorKind::InvalidValue);
1229 };
1230 let resolved_path = decode_repo_path(&obj.field("path"), obj.take("path")?)?;
1231 match kind {
1232 TargetTag::Tree => {
1233 obj.finish()?;
1234 Ok(Target::Tree {
1235 path: resolved_path,
1236 })
1237 }
1238 TargetTag::Blob => {
1239 let mode_path = obj.field("mode");
1240 let mode_text = de::string(&mode_path, obj.take("mode")?)?;
1241 let Ok(mode) = mode_text.parse::<BlobMode>() else {
1242 return fail(&mode_path, ErrorKind::InvalidValue);
1243 };
1244 let content = decode_resolution_content(&obj.field("content"), obj.take("content")?)?;
1245 obj.finish()?;
1246 Ok(Target::Blob(BlobTarget {
1247 path: resolved_path,
1248 mode,
1249 content,
1250 }))
1251 }
1252 }
1253}
1254
1255fn decode_resolution_content(path: &str, value: Value) -> Result<BlobContent, Error> {
1256 let mut obj = Obj::new(path, value)?;
1257 let kind_path = obj.field("kind");
1258 let kind_text = de::string(&kind_path, obj.take("kind")?)?;
1259 let Ok(kind) = kind_text.parse::<BlobContentTag>() else {
1260 return fail(&kind_path, ErrorKind::InvalidValue);
1261 };
1262 let raw_digest = decode_digest(&obj.field("raw_digest"), obj.take("raw_digest")?)?;
1263 match kind {
1264 BlobContentTag::Available => {
1265 let projection_digest = decode_digest(
1266 &obj.field("projection_digest"),
1267 obj.take("projection_digest")?,
1268 )?;
1269 obj.finish()?;
1270 Ok(BlobContent::Available {
1271 raw_digest,
1272 projection_digest,
1273 })
1274 }
1275 BlobContentTag::LfsPointer => {
1276 obj.finish()?;
1277 Ok(BlobContent::LfsPointer { raw_digest })
1278 }
1279 }
1280}
1281
1282struct DecodedFact {
1283 fact: Fact,
1284 fact_digest: Digest,
1285 finding_key: Digest,
1286}
1287
1288fn decode_fact(path: &str, value: Value) -> Result<DecodedFact, Error> {
1289 let fact_digest = hj(FACT_DOMAIN, &value);
1290 let mut obj = Obj::new(path, value)?;
1291 de::const_str(&obj.field("schema"), obj.take("schema")?, FACT_SCHEMA)?;
1292 let finding_kind =
1293 EligibleFindingKind::decode(&obj.field("finding_kind"), obj.take("finding_kind")?)?;
1294 let key_path = obj.field("key_input");
1295 let (key_input, finding_key) = decode_key_input(&key_path, obj.take("key_input")?)?;
1296 let evidence_path = obj.field("evidence");
1297 let mut evidence = Obj::new(&evidence_path, obj.take("evidence")?)?;
1298 de::const_str(&evidence.field("kind"), evidence.take("kind")?, "reference")?;
1299 let resolution =
1300 decode_resolution(&evidence.field("resolution"), evidence.take("resolution")?)?;
1301 let multiplicity_path = evidence.field("occurrence_multiplicity");
1302 if de::integer(
1303 &multiplicity_path,
1304 evidence.take("occurrence_multiplicity")?,
1305 )? != 1
1306 {
1307 return fail(&multiplicity_path, ErrorKind::InvalidValue);
1308 }
1309 evidence.finish()?;
1310 obj.finish()?;
1311
1312 let Some(fact) = Fact::new(key_input, resolution) else {
1313 return fail(path, ErrorKind::Inconsistent);
1314 };
1315 if fact.finding_kind() != finding_kind {
1316 return fail(path, ErrorKind::Inconsistent);
1317 }
1318 Ok(DecodedFact {
1319 fact,
1320 fact_digest,
1321 finding_key,
1322 })
1323}
1324
1325struct ItemCore {
1326 finding_key: Digest,
1327 fact: Fact,
1328 fact_digest: Digest,
1329 owner: OwnerId,
1330 reason: String,
1331 created_at: UtcInstant,
1332 expires_at: UtcInstant,
1333}
1334
1335fn decode_item_core(obj: &mut Obj, fact_field: &str) -> Result<ItemCore, Error> {
1336 let finding_key_path = obj.field("finding_key");
1337 let finding_key = decode_digest(&finding_key_path, obj.take("finding_key")?)?;
1338 let fact_path = obj.field(fact_field);
1339 let decoded_fact = decode_fact(&fact_path, obj.take(fact_field)?)?;
1340 if finding_key != decoded_fact.finding_key {
1341 return fail(&finding_key_path, ErrorKind::DigestMismatch);
1342 }
1343 let fact_digest_field = format!("{fact_field}_digest");
1344 let fact_digest_path = obj.field(&fact_digest_field);
1345 let fact_digest = decode_digest(&fact_digest_path, obj.take(&fact_digest_field)?)?;
1346 if fact_digest != decoded_fact.fact_digest {
1347 return fail(&fact_digest_path, ErrorKind::DigestMismatch);
1348 }
1349 let owner = decode_owner(&obj.field("owner"), obj.take("owner")?)?;
1350 let reason = decode_reason(&obj.field("reason"), obj.take("reason")?)?;
1351 let created_at = decode_instant(&obj.field("created_at"), obj.take("created_at")?)?;
1352 let expires_at = decode_instant(&obj.field("expires_at"), obj.take("expires_at")?)?;
1353 Ok(ItemCore {
1354 finding_key,
1355 fact: decoded_fact.fact,
1356 fact_digest,
1357 owner,
1358 reason,
1359 created_at,
1360 expires_at,
1361 })
1362}
1363
1364fn decode_debt_item(path: &str, value: Value) -> Result<DebtItem, Error> {
1365 let mut obj = Obj::new(path, value)?;
1366 let debt_id = decode_artifact_id(&obj.field("debt_id"), obj.take("debt_id")?)?;
1367 let core = decode_item_core(&mut obj, "accepted_fact")?;
1368 obj.finish()?;
1369 if core.created_at >= core.expires_at {
1370 return fail(path, ErrorKind::Inconsistent);
1371 }
1372 Ok(DebtItem {
1373 debt_id,
1374 finding_key: core.finding_key,
1375 accepted_fact: core.fact,
1376 accepted_fact_digest: core.fact_digest,
1377 owner: core.owner,
1378 reason: core.reason,
1379 created_at: core.created_at,
1380 expires_at: core.expires_at,
1381 })
1382}
1383
1384fn decode_waiver_item(path: &str, value: Value) -> Result<WaiverItem, Error> {
1385 let mut obj = Obj::new(path, value)?;
1386 let waiver_id = decode_artifact_id(&obj.field("waiver_id"), obj.take("waiver_id")?)?;
1387 let core = decode_item_core(&mut obj, "authorized_fact")?;
1388 let candidate_tree = decode_tree(&obj.field("candidate_tree"), obj.take("candidate_tree")?)?;
1389 let issuer = decode_owner(&obj.field("issuer"), obj.take("issuer")?)?;
1390 let not_before = decode_instant(&obj.field("not_before"), obj.take("not_before")?)?;
1391 de::const_str(
1392 &obj.field("residual_disposition"),
1393 obj.take("residual_disposition")?,
1394 "warn",
1395 )?;
1396 obj.finish()?;
1397 if core.created_at > not_before || not_before >= core.expires_at {
1398 return fail(path, ErrorKind::Inconsistent);
1399 }
1400 Ok(WaiverItem {
1401 waiver_id,
1402 finding_key: core.finding_key,
1403 authorized_fact: core.fact,
1404 authorized_fact_digest: core.fact_digest,
1405 candidate_tree,
1406 owner: core.owner,
1407 issuer,
1408 reason: core.reason,
1409 created_at: core.created_at,
1410 not_before,
1411 expires_at: core.expires_at,
1412 })
1413}