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 ReferencesPerDocument,
357 ReferencesPerSnapshot,
358 OrganizationPolicyEntries,
359 CompleteFindings,
360 TypedAnalysisErrorsRetained,
361 MachineJsonBytes,
362 PrivateTemporaryStorageBytes,
363 EvaluatorManagedMemoryBytes,
364}
365
366impl ResourceName {
367 #[must_use]
369 pub fn all() -> impl ExactSizeIterator<Item = Self> {
370 Self::iter()
371 }
372
373 #[must_use]
375 pub const fn phase(self) -> &'static str {
376 match self {
377 Self::ControlInputBytes
378 | Self::RepositoryPolicyEntries
379 | Self::DebtItems
380 | Self::WaiverItems
381 | Self::OrganizationPolicyEntries => "configuration",
382 Self::GitObjectBytes
383 | Self::GitCompressedObjectBytes
384 | Self::AggregateGitCompressedObjectBytesPerEvaluation
385 | Self::GitPackDirectoryEntries
386 | Self::GitPackFiles
387 | Self::GitPackIndexBytes
388 | Self::AggregateGitPackIndexBytes
389 | Self::GitDeltaDepth
390 | Self::GitIndexBytes
391 | Self::GitTreeEntriesPerSnapshot
392 | Self::RawPathBytes => "git",
393 Self::DocumentsPerSnapshot
394 | Self::DocumentBlobBytes
395 | Self::AggregateDocumentBytesPerSnapshot
396 | Self::SelectedControlBlobBytes
397 | Self::AggregateSelectedControlBytesPerSnapshot => "discovery",
398 Self::RawLinkDestinationBytes
399 | Self::ParserNesting
400 | Self::ParserNodesPerDocument
401 | Self::ParserNodesPerSnapshot
402 | Self::ReferencesPerDocument
403 | Self::ReferencesPerSnapshot => "parse",
404 Self::ReferencedTargetBlobBytes
405 | Self::AggregateReferencedTargetBytesPerSnapshot
406 | Self::AggregateLineFragmentEvaluationBytesPerSnapshot => "resolution",
407 Self::CompleteFindings => "policy",
408 Self::MachineJsonBytes => "output",
409 Self::TypedAnalysisErrorsRetained
410 | Self::PrivateTemporaryStorageBytes
411 | Self::EvaluatorManagedMemoryBytes => "internal",
412 }
413 }
414
415 #[must_use]
416 pub fn as_str(self) -> &'static str {
417 self.into()
418 }
419
420 fn decode(path: &str, value: Value) -> Result<Self, Error> {
421 let raw = de::string(path, value)?;
422 let Ok(resource) = raw.parse() else {
423 return fail(path, ErrorKind::InvalidValue);
424 };
425 Ok(resource)
426 }
427}
428
429#[derive(Clone, Debug, PartialEq, Eq)]
430pub struct DocumentInclude {
431 pub path: RepoPathText,
432 pub kind: IncludeKind,
433}
434
435#[derive(Clone, Debug, PartialEq, Eq)]
436pub struct FindingDisposition {
437 pub finding_kind: PromotableFindingKind,
438 pub disposition: Disposition,
439}
440
441#[derive(Clone, Debug, PartialEq, Eq)]
442pub struct ScannerPolicy {
443 pub digest: Digest,
444 pub document_includes: Vec<DocumentInclude>,
445 pub protected_inventory: Vec<RepoPathText>,
446 pub finding_dispositions: Vec<FindingDisposition>,
447}
448
449impl ScannerPolicy {
450 pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
455 let value = root(bytes)?;
456 let digest = hj(SCANNER_POLICY_SCHEMA, &value);
457 let mut obj = Obj::new("$", value)?;
458 de::const_str(
459 &obj.field("schema"),
460 obj.take("schema")?,
461 SCANNER_POLICY_SCHEMA,
462 )?;
463
464 let includes_path = obj.field("document_includes");
465 let includes = de::array(&includes_path, obj.take("document_includes")?)?;
466 let document_includes = decode_items(&includes_path, includes, 100_000, decode_include)?;
467 sorted_set(&includes_path, &document_includes, |a, b| {
468 (a.path.as_str(), a.kind).cmp(&(b.path.as_str(), b.kind))
469 })?;
470
471 let inventory_path = obj.field("protected_inventory");
472 let protected_inventory =
473 decode_path_set(&inventory_path, obj.take("protected_inventory")?)?;
474
475 let dispositions_path = obj.field("finding_dispositions");
476 let raw = de::array(&dispositions_path, obj.take("finding_dispositions")?)?;
477 let finding_dispositions =
478 decode_items(&dispositions_path, raw, 3, decode_disposition_rule)?;
479 sorted_set(&dispositions_path, &finding_dispositions, |a, b| {
480 a.finding_kind.as_str().cmp(b.finding_kind.as_str())
481 })?;
482
483 obj.finish()?;
484 Ok(Self {
485 digest,
486 document_includes,
487 protected_inventory,
488 finding_dispositions,
489 })
490 }
491}
492
493#[derive(Clone, Debug, PartialEq, Eq)]
494pub struct ResourceLimit {
495 pub resource: ResourceName,
496 pub maximum: i64,
497}
498
499#[derive(Clone, Debug, PartialEq, Eq)]
500pub struct FloorDisposition {
501 pub finding_kind: PromotableFindingKind,
502 pub disposition: Disposition,
503}
504
505#[derive(Clone, Debug, PartialEq, Eq)]
506pub struct OrganizationFloor {
507 pub digest: Digest,
508 pub floor_id: ArtifactId,
509 pub repository: RepositoryIdentity,
510 pub ref_name: BranchRef,
511 pub minimum_profile: Profile,
512 pub minimum_dispositions: Vec<FindingDisposition>,
513 pub protected_inventory: Vec<RepoPathText>,
514 pub protected_control_paths: Vec<RepoPathText>,
515 pub waivable_finding_kinds: Vec<EligibleFindingKind>,
516 pub authorized_debt_owners: Vec<OwnerId>,
517 pub authorized_waiver_issuers: Vec<OwnerId>,
518 pub resource_limits: Vec<ResourceLimit>,
519}
520
521#[derive(Clone, Debug, PartialEq, Eq)]
524pub enum FloorDefect {
525 Schema(Error),
526 Entries {
527 configured_limit: u64,
528 observed_lower_bound: u64,
529 },
530}
531
532impl From<Error> for FloorDefect {
533 fn from(error: Error) -> Self {
534 Self::Schema(error)
535 }
536}
537
538pub const ORGANIZATION_POLICY_ENTRIES_LIMIT: u64 = 100_000;
539
540impl OrganizationFloor {
541 #[must_use]
542 pub const fn schema(&self) -> &'static str {
543 ORGANIZATION_FLOOR_SCHEMA
544 }
545
546 pub fn parse(bytes: &[u8]) -> Result<Self, FloorDefect> {
553 let value = root(bytes)?;
554 let digest = hj(ORGANIZATION_FLOOR_SCHEMA, &value);
555 let mut obj = Obj::new("$", value)?;
556 de::const_str(
557 &obj.field("schema"),
558 obj.take("schema")?,
559 ORGANIZATION_FLOOR_SCHEMA,
560 )?;
561
562 let floor_id = decode_artifact_id(&obj.field("floor_id"), obj.take("floor_id")?)?;
563 let repository = decode_repository(&obj.field("repository"), obj.take("repository")?)?;
564 let ref_name = decode_branch_ref(&obj.field("ref"), obj.take("ref")?)?;
565 let minimum_profile =
566 Profile::decode(&obj.field("minimum_profile"), obj.take("minimum_profile")?)?;
567
568 let dispositions_path = obj.field("minimum_dispositions");
569 let dispositions_raw = de::array(&dispositions_path, obj.take("minimum_dispositions")?)?;
570 let inventory_path = obj.field("protected_inventory");
571 let inventory_raw = de::array(&inventory_path, obj.take("protected_inventory")?)?;
572 let control_paths_path = obj.field("protected_control_paths");
573 let control_paths_raw =
574 de::array(&control_paths_path, obj.take("protected_control_paths")?)?;
575 let waivable_path = obj.field("waivable_finding_kinds");
576 let waivable_raw = de::array(&waivable_path, obj.take("waivable_finding_kinds")?)?;
577 let owners_path = obj.field("authorized_debt_owners");
578 let owners_raw = de::array(&owners_path, obj.take("authorized_debt_owners")?)?;
579 let issuers_path = obj.field("authorized_waiver_issuers");
580 let issuers_raw = de::array(&issuers_path, obj.take("authorized_waiver_issuers")?)?;
581 let limits_path = obj.field("resource_limits");
582 let limits_raw = de::array(&limits_path, obj.take("resource_limits")?)?;
583
584 let combined = [
585 dispositions_raw.len(),
586 inventory_raw.len(),
587 control_paths_raw.len(),
588 waivable_raw.len(),
589 owners_raw.len(),
590 issuers_raw.len(),
591 limits_raw.len(),
592 ]
593 .iter()
594 .map(|&len| u64::try_from(len).unwrap_or(u64::MAX))
595 .fold(0_u64, u64::saturating_add);
596 if combined > ORGANIZATION_POLICY_ENTRIES_LIMIT {
597 return Err(FloorDefect::Entries {
598 configured_limit: ORGANIZATION_POLICY_ENTRIES_LIMIT,
599 observed_lower_bound: ORGANIZATION_POLICY_ENTRIES_LIMIT.saturating_add(1),
600 });
601 }
602
603 let minimum_dispositions = decode_items(
604 &dispositions_path,
605 dispositions_raw,
606 3,
607 decode_disposition_rule,
608 )?;
609 sorted_set(&dispositions_path, &minimum_dispositions, |a, b| {
610 a.finding_kind.as_str().cmp(b.finding_kind.as_str())
611 })?;
612 let protected_inventory = decode_path_items(&inventory_path, inventory_raw)?;
613 let protected_control_paths = decode_path_items(&control_paths_path, control_paths_raw)?;
614 let waivable_finding_kinds =
615 decode_items(&waivable_path, waivable_raw, 2, |path, value| {
616 EligibleFindingKind::decode(path, value)
617 })?;
618 sorted_set(&waivable_path, &waivable_finding_kinds, |a, b| {
619 a.as_str().cmp(b.as_str())
620 })?;
621 let authorized_debt_owners = decode_owner_items(&owners_path, owners_raw)?;
622 let authorized_waiver_issuers = decode_owner_items(&issuers_path, issuers_raw)?;
623 let cap = ResourceName::all().len();
624 let resource_limits = decode_items(&limits_path, limits_raw, cap, decode_resource_limit)?;
625 sorted_set(&limits_path, &resource_limits, |a, b| {
626 a.resource.as_str().cmp(b.resource.as_str())
627 })?;
628
629 obj.finish()?;
630 if let Some(declared) = resource_limits
631 .iter()
632 .find(|row| row.resource == ResourceName::OrganizationPolicyEntries)
633 {
634 let declared = u64::try_from(declared.maximum).unwrap_or(u64::MAX);
635 if combined > declared {
636 return Err(FloorDefect::Entries {
637 configured_limit: declared,
638 observed_lower_bound: declared.saturating_add(1),
639 });
640 }
641 }
642 Ok(Self {
643 digest,
644 floor_id,
645 repository,
646 ref_name,
647 minimum_profile,
648 minimum_dispositions,
649 protected_inventory,
650 protected_control_paths,
651 waivable_finding_kinds,
652 authorized_debt_owners,
653 authorized_waiver_issuers,
654 resource_limits,
655 })
656 }
657}
658
659#[derive(Clone, Debug, PartialEq, Eq)]
660pub struct TargetIntent {
661 pub path: RepoPathText,
662 pub target_kind: TargetKind,
663 pub query_digest: Option<Digest>,
664 pub fragment_digest: Option<Digest>,
665}
666
667#[derive(Clone, Debug, PartialEq, Eq)]
668pub struct FindingScope {
669 pub document: RepoPathText,
670 pub source_construct: SourceConstruct,
671 pub normalized_target_intent: TargetIntent,
672 pub source_projection_digest: Digest,
673}
674
675#[derive(Clone, Debug, PartialEq, Eq)]
676pub struct FindingKeyInput {
677 pub finding_kind: EligibleFindingKind,
678 pub scope: FindingScope,
679}
680
681#[derive(Clone, Debug, PartialEq, Eq)]
682pub struct Fact {
683 key_input: FindingKeyInput,
684 resolution: Resolution<RepoPathText>,
685}
686
687impl Fact {
688 #[must_use]
691 pub fn new(key_input: FindingKeyInput, resolution: Resolution<RepoPathText>) -> Option<Self> {
692 let expected = match &resolution {
693 Resolution::Missing(_) => EligibleFindingKind::ExplicitTargetMissing,
694 Resolution::TypeMismatch(_) => EligibleFindingKind::ExplicitTargetTypeMismatch,
695 Resolution::Resolved(_)
696 | Resolution::UnsupportedTarget(_)
697 | Resolution::UnsupportedSemantics(_)
698 | Resolution::UnsupportedVersion(_)
699 | Resolution::Invalid(_)
700 | Resolution::External(_) => return None,
701 };
702 (key_input.finding_kind == expected).then_some(Self {
703 key_input,
704 resolution,
705 })
706 }
707
708 #[must_use]
710 pub const fn finding_kind(&self) -> EligibleFindingKind {
711 self.key_input.finding_kind
712 }
713
714 #[must_use]
716 pub const fn key_input(&self) -> &FindingKeyInput {
717 &self.key_input
718 }
719
720 #[must_use]
722 pub const fn resolution(&self) -> &Resolution<RepoPathText> {
723 &self.resolution
724 }
725}
726
727#[derive(Clone, Debug, PartialEq, Eq)]
728pub struct DebtItem {
729 pub debt_id: ArtifactId,
730 pub finding_key: Digest,
731 pub accepted_fact: Fact,
732 pub accepted_fact_digest: Digest,
733 pub owner: OwnerId,
734 pub reason: String,
735 pub created_at: UtcInstant,
736 pub expires_at: UtcInstant,
737}
738
739#[derive(Clone, Debug, PartialEq, Eq)]
740pub struct DebtSnapshot {
741 pub digest: Digest,
742 pub repository: RepositoryIdentity,
743 pub ref_name: BranchRef,
744 pub organization_floor_digest: Digest,
745 pub adoption_tree: TreeIdentity,
746 pub adoption_report_payload_digest: Digest,
747 pub created_at: UtcInstant,
748 pub items: Vec<DebtItem>,
749}
750
751impl DebtSnapshot {
752 #[must_use]
753 pub const fn schema(&self) -> &'static str {
754 DEBT_SNAPSHOT_SCHEMA
755 }
756
757 pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
763 let value = root(bytes)?;
764 let digest = hj(DEBT_SNAPSHOT_SCHEMA, &value);
765 let mut obj = Obj::new("$", value)?;
766 de::const_str(
767 &obj.field("schema"),
768 obj.take("schema")?,
769 DEBT_SNAPSHOT_SCHEMA,
770 )?;
771
772 let repository = decode_repository(&obj.field("repository"), obj.take("repository")?)?;
773 let ref_name = decode_branch_ref(&obj.field("ref"), obj.take("ref")?)?;
774 let organization_floor_digest = decode_digest(
775 &obj.field("organization_floor_digest"),
776 obj.take("organization_floor_digest")?,
777 )?;
778 let adoption_tree = decode_tree(&obj.field("adoption_tree"), obj.take("adoption_tree")?)?;
779 let adoption_report_payload_digest = decode_digest(
780 &obj.field("adoption_report_payload_digest"),
781 obj.take("adoption_report_payload_digest")?,
782 )?;
783 let created_at = decode_instant(&obj.field("created_at"), obj.take("created_at")?)?;
784
785 let items_path = obj.field("items");
786 let raw = de::array(&items_path, obj.take("items")?)?;
787 let items = decode_items(&items_path, raw, 100_000, decode_debt_item)?;
788 sorted_set(&items_path, &items, |a, b| {
789 a.debt_id.as_str().cmp(b.debt_id.as_str())
790 })?;
791 let mut keys: BTreeSet<Digest> = BTreeSet::new();
792 for item in &items {
793 if !keys.insert(item.finding_key) {
794 return fail(&items_path, ErrorKind::DuplicateMember);
795 }
796 if item.created_at > created_at {
797 return fail(&items_path, ErrorKind::Inconsistent);
798 }
799 }
800
801 obj.finish()?;
802 Ok(Self {
803 digest,
804 repository,
805 ref_name,
806 organization_floor_digest,
807 adoption_tree,
808 adoption_report_payload_digest,
809 created_at,
810 items,
811 })
812 }
813}
814
815#[derive(Clone, Debug, PartialEq, Eq)]
816pub struct WaiverItem {
817 pub waiver_id: ArtifactId,
818 pub finding_key: Digest,
819 pub authorized_fact: Fact,
820 pub authorized_fact_digest: Digest,
821 pub candidate_tree: TreeIdentity,
822 pub owner: OwnerId,
823 pub issuer: OwnerId,
824 pub reason: String,
825 pub created_at: UtcInstant,
826 pub not_before: UtcInstant,
827 pub expires_at: UtcInstant,
828}
829
830#[derive(Clone, Debug, PartialEq, Eq)]
831pub struct WaiverBundle {
832 pub digest: Digest,
833 pub repository: RepositoryIdentity,
834 pub ref_name: BranchRef,
835 pub organization_floor_digest: Digest,
836 pub created_at: UtcInstant,
837 pub items: Vec<WaiverItem>,
838}
839
840impl WaiverBundle {
841 #[must_use]
842 pub const fn schema(&self) -> &'static str {
843 WAIVER_BUNDLE_SCHEMA
844 }
845
846 pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
853 let value = root(bytes)?;
854 let digest = hj(WAIVER_BUNDLE_SCHEMA, &value);
855 let mut obj = Obj::new("$", value)?;
856 de::const_str(
857 &obj.field("schema"),
858 obj.take("schema")?,
859 WAIVER_BUNDLE_SCHEMA,
860 )?;
861
862 let repository = decode_repository(&obj.field("repository"), obj.take("repository")?)?;
863 let ref_name = decode_branch_ref(&obj.field("ref"), obj.take("ref")?)?;
864 let organization_floor_digest = decode_digest(
865 &obj.field("organization_floor_digest"),
866 obj.take("organization_floor_digest")?,
867 )?;
868 let created_at = decode_instant(&obj.field("created_at"), obj.take("created_at")?)?;
869
870 let items_path = obj.field("items");
871 let raw = de::array(&items_path, obj.take("items")?)?;
872 let items = decode_items(&items_path, raw, 100_000, decode_waiver_item)?;
873 sorted_set(&items_path, &items, |a, b| {
874 waiver_sort_key(a).cmp(&waiver_sort_key(b))
875 })?;
876 for pair in items.windows(2) {
877 if let [left, right] = pair
878 && left.candidate_tree == right.candidate_tree
879 && left.finding_key == right.finding_key
880 {
881 return fail(&items_path, ErrorKind::DuplicateMember);
882 }
883 }
884 let mut ids: BTreeSet<&str> = BTreeSet::new();
885 for item in &items {
886 if !ids.insert(item.waiver_id.as_str()) {
887 return fail(&items_path, ErrorKind::DuplicateMember);
888 }
889 if item.created_at > created_at {
890 return fail(&items_path, ErrorKind::Inconsistent);
891 }
892 }
893
894 obj.finish()?;
895 Ok(Self {
896 digest,
897 repository,
898 ref_name,
899 organization_floor_digest,
900 created_at,
901 items,
902 })
903 }
904}
905
906fn waiver_sort_key(item: &WaiverItem) -> (ObjectFormat, &str, Digest, &str) {
907 (
908 item.candidate_tree.object_format,
909 item.candidate_tree.tree_oid.as_str(),
910 item.finding_key,
911 item.waiver_id.as_str(),
912 )
913}
914
915pub fn root(bytes: &[u8]) -> Result<Value, Error> {
921 json::parse(bytes).map_err(|defect| Error::new("$", ErrorKind::Json(defect)))
922}
923
924fn decode_items<T>(
925 path: &str,
926 raw: Vec<Value>,
927 limit: usize,
928 decode: impl Fn(&str, Value) -> Result<T, Error>,
929) -> Result<Vec<T>, Error> {
930 if raw.len() > limit {
931 return fail(path, ErrorKind::LimitExceeded);
932 }
933 raw.into_iter()
934 .enumerate()
935 .map(|(index, value)| decode(&format!("{path}[{index}]"), value))
936 .collect()
937}
938
939fn sorted_set<T>(
940 path: &str,
941 items: &[T],
942 compare: impl Fn(&T, &T) -> Ordering,
943) -> Result<(), Error> {
944 for pair in items.windows(2) {
945 if let [left, right] = pair {
946 match compare(left, right) {
947 Ordering::Less => {}
948 Ordering::Equal => return fail(path, ErrorKind::DuplicateMember),
949 Ordering::Greater => return fail(path, ErrorKind::UnsortedSet),
950 }
951 }
952 }
953 Ok(())
954}
955
956fn decode_include(path: &str, value: Value) -> Result<DocumentInclude, Error> {
957 let mut obj = Obj::new(path, value)?;
958 let include_path = decode_repo_path(&obj.field("path"), obj.take("path")?)?;
959 let kind = IncludeKind::decode(&obj.field("kind"), obj.take("kind")?)?;
960 obj.finish()?;
961 Ok(DocumentInclude {
962 path: include_path,
963 kind,
964 })
965}
966
967fn decode_disposition_rule(path: &str, value: Value) -> Result<FindingDisposition, Error> {
968 let mut obj = Obj::new(path, value)?;
969 let finding_kind =
970 PromotableFindingKind::decode(&obj.field("finding_kind"), obj.take("finding_kind")?)?;
971 let disposition = Disposition::decode(&obj.field("disposition"), obj.take("disposition")?)?;
972 obj.finish()?;
973 Ok(FindingDisposition {
974 finding_kind,
975 disposition,
976 })
977}
978
979fn decode_resource_limit(path: &str, value: Value) -> Result<ResourceLimit, Error> {
980 let mut obj = Obj::new(path, value)?;
981 let resource = ResourceName::decode(&obj.field("resource"), obj.take("resource")?)?;
982 let maximum_path = obj.field("maximum");
983 let maximum = de::integer(&maximum_path, obj.take("maximum")?)?;
984 obj.finish()?;
985 let in_bounds = if resource == ResourceName::TypedAnalysisErrorsRetained {
986 (1..=64).contains(&maximum)
987 } else if resource == ResourceName::MachineJsonBytes {
988 maximum == 67_108_864
989 } else {
990 maximum >= 0
991 };
992 if in_bounds {
993 Ok(ResourceLimit { resource, maximum })
994 } else {
995 fail(&maximum_path, ErrorKind::InvalidValue)
996 }
997}
998
999fn decode_path_set(path: &str, value: Value) -> Result<Vec<RepoPathText>, Error> {
1000 decode_path_items(path, de::array(path, value)?)
1001}
1002
1003fn decode_path_items(path: &str, raw: Vec<Value>) -> Result<Vec<RepoPathText>, Error> {
1004 let paths = decode_items(path, raw, 100_000, decode_repo_path)?;
1005 sorted_set(path, &paths, |a, b| a.as_str().cmp(b.as_str()))?;
1006 Ok(paths)
1007}
1008
1009fn decode_owner_items(path: &str, raw: Vec<Value>) -> Result<Vec<OwnerId>, Error> {
1010 let owners = decode_items(path, raw, 10_000, decode_owner)?;
1011 sorted_set(path, &owners, |a, b| a.as_str().cmp(b.as_str()))?;
1012 Ok(owners)
1013}
1014
1015fn decode_repo_path(path: &str, value: Value) -> Result<RepoPathText, Error> {
1016 RepoPathText::new(de::string(path, value)?)
1017 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1018}
1019
1020fn decode_artifact_id(path: &str, value: Value) -> Result<ArtifactId, Error> {
1021 ArtifactId::new(de::string(path, value)?)
1022 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1023}
1024
1025fn decode_owner(path: &str, value: Value) -> Result<OwnerId, Error> {
1026 OwnerId::new(de::string(path, value)?).ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1027}
1028
1029fn decode_branch_ref(path: &str, value: Value) -> Result<BranchRef, Error> {
1030 BranchRef::new(de::string(path, value)?)
1031 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1032}
1033
1034fn decode_instant(path: &str, value: Value) -> Result<UtcInstant, Error> {
1035 UtcInstant::new(de::string(path, value)?)
1036 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1037}
1038
1039fn decode_digest(path: &str, value: Value) -> Result<Digest, Error> {
1040 let raw = de::string(path, value)?;
1041 Digest::from_wire(&raw).ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1042}
1043
1044fn decode_nullable_digest(path: &str, value: Value) -> Result<Option<Digest>, Error> {
1045 de::nullable(value)
1046 .map(|v| decode_digest(path, v))
1047 .transpose()
1048}
1049
1050pub(crate) fn decode_repository(path: &str, value: Value) -> Result<RepositoryIdentity, Error> {
1051 let mut obj = Obj::new(path, value)?;
1052 let host = de::string(&obj.field("host"), obj.take("host")?)?;
1053 let owner = de::string(&obj.field("owner"), obj.take("owner")?)?;
1054 let name = de::string(&obj.field("name"), obj.take("name")?)?;
1055 obj.finish()?;
1056 RepositoryIdentity::new(host, owner, name)
1057 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1058}
1059
1060pub(crate) fn decode_provider_run_id(path: &str, value: Value) -> Result<String, Error> {
1061 let raw = de::string(path, value)?;
1062 let bytes = raw.as_bytes();
1063 let allowed = |byte: &u8| {
1064 byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b':' | b'/' | b'-')
1065 };
1066 if bytes.is_empty()
1067 || bytes.len() > 128
1068 || !bytes.first().is_some_and(u8::is_ascii_alphanumeric)
1069 || !bytes.last().is_some_and(u8::is_ascii_alphanumeric)
1070 || !bytes.iter().all(allowed)
1071 {
1072 return fail(path, ErrorKind::InvalidValue);
1073 }
1074 Ok(raw)
1075}
1076
1077pub(crate) fn decode_provider_id(path: &str, value: Value) -> Result<String, Error> {
1078 let raw = de::string(path, value)?;
1079 if ArtifactId::new(raw.clone()).is_some() {
1080 Ok(raw)
1081 } else {
1082 fail(path, ErrorKind::InvalidValue)
1083 }
1084}
1085
1086fn decode_tree(path: &str, value: Value) -> Result<TreeIdentity, Error> {
1087 let mut obj = Obj::new(path, value)?;
1088 let format_path = obj.field("object_format");
1089 let object_format = match de::string(&format_path, obj.take("object_format")?)?.as_str() {
1090 "sha1" => ObjectFormat::Sha1,
1091 "sha256" => ObjectFormat::Sha256,
1092 _ => return fail(&format_path, ErrorKind::InvalidValue),
1093 };
1094 let tree_oid = de::string(&obj.field("tree_oid"), obj.take("tree_oid")?)?;
1095 obj.finish()?;
1096 TreeIdentity::new(object_format, tree_oid)
1097 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1098}
1099
1100fn decode_reason(path: &str, value: Value) -> Result<String, Error> {
1101 let raw = de::string(path, value)?;
1102 let length = raw.chars().count();
1103 if (1..=1024).contains(&length) && raw.chars().any(|c| !c.is_whitespace()) {
1104 Ok(raw)
1105 } else {
1106 fail(path, ErrorKind::InvalidValue)
1107 }
1108}
1109
1110fn decode_intent(path: &str, value: Value) -> Result<TargetIntent, Error> {
1111 let mut obj = Obj::new(path, value)?;
1112 de::const_str(&obj.field("kind"), obj.take("kind")?, "repository-path")?;
1113 let target_path = decode_repo_path(&obj.field("path"), obj.take("path")?)?;
1114 let target_kind = TargetKind::decode(&obj.field("target_kind"), obj.take("target_kind")?)?;
1115 let query_digest =
1116 decode_nullable_digest(&obj.field("query_digest"), obj.take("query_digest")?)?;
1117 let fragment_digest =
1118 decode_nullable_digest(&obj.field("fragment_digest"), obj.take("fragment_digest")?)?;
1119 obj.finish()?;
1120 Ok(TargetIntent {
1121 path: target_path,
1122 target_kind,
1123 query_digest,
1124 fragment_digest,
1125 })
1126}
1127
1128fn decode_scope(path: &str, value: Value) -> Result<FindingScope, Error> {
1129 let mut obj = Obj::new(path, value)?;
1130 de::const_str(&obj.field("kind"), obj.take("kind")?, "reference")?;
1131 let document = decode_repo_path(&obj.field("document"), obj.take("document")?)?;
1132 let source_construct = SourceConstruct::decode(
1133 &obj.field("source_construct"),
1134 obj.take("source_construct")?,
1135 )?;
1136 let normalized_target_intent = decode_intent(
1137 &obj.field("normalized_target_intent"),
1138 obj.take("normalized_target_intent")?,
1139 )?;
1140 let occurrence_path = obj.field("occurrence");
1141 let mut occurrence = Obj::new(&occurrence_path, obj.take("occurrence")?)?;
1142 de::const_str(
1143 &occurrence.field("kind"),
1144 occurrence.take("kind")?,
1145 "source-projection",
1146 )?;
1147 let source_projection_digest = decode_digest(
1148 &occurrence.field("source_projection_digest"),
1149 occurrence.take("source_projection_digest")?,
1150 )?;
1151 occurrence.finish()?;
1152 obj.finish()?;
1153 Ok(FindingScope {
1154 document,
1155 source_construct,
1156 normalized_target_intent,
1157 source_projection_digest,
1158 })
1159}
1160
1161fn decode_key_input(path: &str, value: Value) -> Result<(FindingKeyInput, Digest), Error> {
1162 let digest = hj(FINDING_KEY_DOMAIN, &value);
1163 let mut obj = Obj::new(path, value)?;
1164 de::const_str(
1165 &obj.field("schema"),
1166 obj.take("schema")?,
1167 FINDING_KEY_INPUT_SCHEMA,
1168 )?;
1169 let finding_kind =
1170 EligibleFindingKind::decode(&obj.field("finding_kind"), obj.take("finding_kind")?)?;
1171 let scope = decode_scope(&obj.field("scope"), obj.take("scope")?)?;
1172 obj.finish()?;
1173 Ok((
1174 FindingKeyInput {
1175 finding_kind,
1176 scope,
1177 },
1178 digest,
1179 ))
1180}
1181
1182fn decode_resolution(path: &str, value: Value) -> Result<Resolution<RepoPathText>, Error> {
1183 let mut obj = Obj::new(path, value)?;
1184 let kind_path = obj.field("kind");
1185 let kind_text = de::string(&kind_path, obj.take("kind")?)?;
1186 let Ok(kind) = kind_text.parse::<ResolutionTag>() else {
1187 return fail(&kind_path, ErrorKind::InvalidValue);
1188 };
1189 match kind {
1190 ResolutionTag::Missing => {
1191 let reason_path = obj.field("reason");
1192 let reason_text = de::string(&reason_path, obj.take("reason")?)?;
1193 let Ok(reason) = reason_text.parse::<MissingTag>() else {
1194 return fail(&reason_path, ErrorKind::InvalidValue);
1195 };
1196 let resolved_path = decode_repo_path(&obj.field("path"), obj.take("path")?)?;
1197 obj.finish()?;
1198 Ok(Resolution::Missing(match reason {
1199 MissingTag::PathNotFound => Missing::PathNotFound {
1200 path: resolved_path,
1201 },
1202 MissingTag::LineFragmentOutOfRange => Missing::LineFragmentOutOfRange {
1203 path: resolved_path,
1204 },
1205 }))
1206 }
1207 ResolutionTag::TypeMismatch => {
1208 let target = decode_resolution_target(&obj.field("target"), obj.take("target")?)?;
1209 obj.finish()?;
1210 Ok(Resolution::TypeMismatch(target))
1211 }
1212 ResolutionTag::Resolved
1213 | ResolutionTag::UnsupportedTarget
1214 | ResolutionTag::UnsupportedSemantics
1215 | ResolutionTag::UnsupportedVersion
1216 | ResolutionTag::Invalid
1217 | ResolutionTag::External => fail(&kind_path, ErrorKind::InvalidValue),
1218 }
1219}
1220
1221fn decode_resolution_target(path: &str, value: Value) -> Result<Target<RepoPathText>, Error> {
1222 let mut obj = Obj::new(path, value)?;
1223 let kind_path = obj.field("kind");
1224 let kind_text = de::string(&kind_path, obj.take("kind")?)?;
1225 let Ok(kind) = kind_text.parse::<TargetTag>() else {
1226 return fail(&kind_path, ErrorKind::InvalidValue);
1227 };
1228 let resolved_path = decode_repo_path(&obj.field("path"), obj.take("path")?)?;
1229 match kind {
1230 TargetTag::Tree => {
1231 obj.finish()?;
1232 Ok(Target::Tree {
1233 path: resolved_path,
1234 })
1235 }
1236 TargetTag::Blob => {
1237 let mode_path = obj.field("mode");
1238 let mode_text = de::string(&mode_path, obj.take("mode")?)?;
1239 let Ok(mode) = mode_text.parse::<BlobMode>() else {
1240 return fail(&mode_path, ErrorKind::InvalidValue);
1241 };
1242 let content = decode_resolution_content(&obj.field("content"), obj.take("content")?)?;
1243 obj.finish()?;
1244 Ok(Target::Blob(BlobTarget {
1245 path: resolved_path,
1246 mode,
1247 content,
1248 }))
1249 }
1250 }
1251}
1252
1253fn decode_resolution_content(path: &str, value: Value) -> Result<BlobContent, Error> {
1254 let mut obj = Obj::new(path, value)?;
1255 let kind_path = obj.field("kind");
1256 let kind_text = de::string(&kind_path, obj.take("kind")?)?;
1257 let Ok(kind) = kind_text.parse::<BlobContentTag>() else {
1258 return fail(&kind_path, ErrorKind::InvalidValue);
1259 };
1260 let raw_digest = decode_digest(&obj.field("raw_digest"), obj.take("raw_digest")?)?;
1261 match kind {
1262 BlobContentTag::Available => {
1263 let projection_digest = decode_digest(
1264 &obj.field("projection_digest"),
1265 obj.take("projection_digest")?,
1266 )?;
1267 obj.finish()?;
1268 Ok(BlobContent::Available {
1269 raw_digest,
1270 projection_digest,
1271 })
1272 }
1273 BlobContentTag::LfsPointer => {
1274 obj.finish()?;
1275 Ok(BlobContent::LfsPointer { raw_digest })
1276 }
1277 }
1278}
1279
1280struct DecodedFact {
1281 fact: Fact,
1282 fact_digest: Digest,
1283 finding_key: Digest,
1284}
1285
1286fn decode_fact(path: &str, value: Value) -> Result<DecodedFact, Error> {
1287 let fact_digest = hj(FACT_DOMAIN, &value);
1288 let mut obj = Obj::new(path, value)?;
1289 de::const_str(&obj.field("schema"), obj.take("schema")?, FACT_SCHEMA)?;
1290 let finding_kind =
1291 EligibleFindingKind::decode(&obj.field("finding_kind"), obj.take("finding_kind")?)?;
1292 let key_path = obj.field("key_input");
1293 let (key_input, finding_key) = decode_key_input(&key_path, obj.take("key_input")?)?;
1294 let evidence_path = obj.field("evidence");
1295 let mut evidence = Obj::new(&evidence_path, obj.take("evidence")?)?;
1296 de::const_str(&evidence.field("kind"), evidence.take("kind")?, "reference")?;
1297 let resolution =
1298 decode_resolution(&evidence.field("resolution"), evidence.take("resolution")?)?;
1299 let multiplicity_path = evidence.field("occurrence_multiplicity");
1300 if de::integer(
1301 &multiplicity_path,
1302 evidence.take("occurrence_multiplicity")?,
1303 )? != 1
1304 {
1305 return fail(&multiplicity_path, ErrorKind::InvalidValue);
1306 }
1307 evidence.finish()?;
1308 obj.finish()?;
1309
1310 let Some(fact) = Fact::new(key_input, resolution) else {
1311 return fail(path, ErrorKind::Inconsistent);
1312 };
1313 if fact.finding_kind() != finding_kind {
1314 return fail(path, ErrorKind::Inconsistent);
1315 }
1316 Ok(DecodedFact {
1317 fact,
1318 fact_digest,
1319 finding_key,
1320 })
1321}
1322
1323struct ItemCore {
1324 finding_key: Digest,
1325 fact: Fact,
1326 fact_digest: Digest,
1327 owner: OwnerId,
1328 reason: String,
1329 created_at: UtcInstant,
1330 expires_at: UtcInstant,
1331}
1332
1333fn decode_item_core(obj: &mut Obj, fact_field: &str) -> Result<ItemCore, Error> {
1334 let finding_key_path = obj.field("finding_key");
1335 let finding_key = decode_digest(&finding_key_path, obj.take("finding_key")?)?;
1336 let fact_path = obj.field(fact_field);
1337 let decoded_fact = decode_fact(&fact_path, obj.take(fact_field)?)?;
1338 if finding_key != decoded_fact.finding_key {
1339 return fail(&finding_key_path, ErrorKind::DigestMismatch);
1340 }
1341 let fact_digest_field = format!("{fact_field}_digest");
1342 let fact_digest_path = obj.field(&fact_digest_field);
1343 let fact_digest = decode_digest(&fact_digest_path, obj.take(&fact_digest_field)?)?;
1344 if fact_digest != decoded_fact.fact_digest {
1345 return fail(&fact_digest_path, ErrorKind::DigestMismatch);
1346 }
1347 let owner = decode_owner(&obj.field("owner"), obj.take("owner")?)?;
1348 let reason = decode_reason(&obj.field("reason"), obj.take("reason")?)?;
1349 let created_at = decode_instant(&obj.field("created_at"), obj.take("created_at")?)?;
1350 let expires_at = decode_instant(&obj.field("expires_at"), obj.take("expires_at")?)?;
1351 Ok(ItemCore {
1352 finding_key,
1353 fact: decoded_fact.fact,
1354 fact_digest,
1355 owner,
1356 reason,
1357 created_at,
1358 expires_at,
1359 })
1360}
1361
1362fn decode_debt_item(path: &str, value: Value) -> Result<DebtItem, Error> {
1363 let mut obj = Obj::new(path, value)?;
1364 let debt_id = decode_artifact_id(&obj.field("debt_id"), obj.take("debt_id")?)?;
1365 let core = decode_item_core(&mut obj, "accepted_fact")?;
1366 obj.finish()?;
1367 if core.created_at >= core.expires_at {
1368 return fail(path, ErrorKind::Inconsistent);
1369 }
1370 Ok(DebtItem {
1371 debt_id,
1372 finding_key: core.finding_key,
1373 accepted_fact: core.fact,
1374 accepted_fact_digest: core.fact_digest,
1375 owner: core.owner,
1376 reason: core.reason,
1377 created_at: core.created_at,
1378 expires_at: core.expires_at,
1379 })
1380}
1381
1382fn decode_waiver_item(path: &str, value: Value) -> Result<WaiverItem, Error> {
1383 let mut obj = Obj::new(path, value)?;
1384 let waiver_id = decode_artifact_id(&obj.field("waiver_id"), obj.take("waiver_id")?)?;
1385 let core = decode_item_core(&mut obj, "authorized_fact")?;
1386 let candidate_tree = decode_tree(&obj.field("candidate_tree"), obj.take("candidate_tree")?)?;
1387 let issuer = decode_owner(&obj.field("issuer"), obj.take("issuer")?)?;
1388 let not_before = decode_instant(&obj.field("not_before"), obj.take("not_before")?)?;
1389 de::const_str(
1390 &obj.field("residual_disposition"),
1391 obj.take("residual_disposition")?,
1392 "warn",
1393 )?;
1394 obj.finish()?;
1395 if core.created_at > not_before || not_before >= core.expires_at {
1396 return fail(path, ErrorKind::Inconsistent);
1397 }
1398 Ok(WaiverItem {
1399 waiver_id,
1400 finding_key: core.finding_key,
1401 authorized_fact: core.fact,
1402 authorized_fact_digest: core.fact_digest,
1403 candidate_tree,
1404 owner: core.owner,
1405 issuer,
1406 reason: core.reason,
1407 created_at: core.created_at,
1408 not_before,
1409 expires_at: core.expires_at,
1410 })
1411}