1use std::cmp::Ordering;
2use std::collections::BTreeSet;
3
4use crate::de::{self, Error, ErrorKind, Obj, fail};
5use crate::digest::{Digest, hj};
6use crate::json::{self, Value};
7use crate::model::{
8 ArtifactId, BranchRef, ObjectFormat, Oid, OwnerId, RepoPath, RepositoryIdentity, TreeIdentity,
9 UtcInstant,
10};
11
12pub const SCANNER_POLICY_PATH: &str = ".amiss/scanner-policy.json";
13
14const SCANNER_POLICY_SCHEMA: &str = "amiss/scanner-policy/v1";
15const ORGANIZATION_FLOOR_SCHEMA: &str = "amiss/organization-floor/v1";
16const DEBT_SNAPSHOT_SCHEMA: &str = "amiss/debt-snapshot/v1";
17const WAIVER_BUNDLE_SCHEMA: &str = "amiss/waiver-bundle/v1";
18const TRUSTED_TIME_STATEMENT_SCHEMA: &str = "amiss/scanner-trusted-time-statement/v1";
19const TRUSTED_TIME_CONTROLLER: &str = "github-actions-required-workflow-clock-v1";
20const EXECUTION_CONSTRAINT_SCHEMA: &str = "amiss/scanner-execution-constraint/v1";
21const ACTION_BOOTSTRAP_CONTRACT: &str = "amiss-action-bootstrap-v1";
22
23pub const STATEMENT_TTL_MAX_SECONDS: i64 = 600;
26const FINDING_KEY_INPUT_SCHEMA: &str = "amiss/scanner-finding-key-input/v1";
27const FACT_SCHEMA: &str = "amiss/scanner-fact/v1";
28pub const FINDING_KEY_DOMAIN: &str = "amiss/scanner-finding-key/v1";
29pub const FACT_DOMAIN: &str = "amiss/scanner-fact/v1";
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
32pub enum IncludeKind {
33 Document,
34 Tree,
35}
36
37impl IncludeKind {
38 #[must_use]
39 pub const fn as_str(self) -> &'static str {
40 match self {
41 Self::Document => "document",
42 Self::Tree => "tree",
43 }
44 }
45
46 fn decode(path: &str, value: Value) -> Result<Self, Error> {
47 match de::string(path, value)?.as_str() {
48 "document" => Ok(Self::Document),
49 "tree" => Ok(Self::Tree),
50 _ => fail(path, ErrorKind::InvalidValue),
51 }
52 }
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
56pub enum Disposition {
57 Warn,
58 Fail,
59}
60
61impl Disposition {
62 fn decode(path: &str, value: Value) -> Result<Self, Error> {
63 match de::string(path, value)?.as_str() {
64 "warn" => Ok(Self::Warn),
65 "fail" => Ok(Self::Fail),
66 _ => fail(path, ErrorKind::InvalidValue),
67 }
68 }
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
72pub enum Profile {
73 Observe,
74 Enforce,
75}
76
77impl Profile {
78 pub fn decode(path: &str, value: Value) -> Result<Self, Error> {
82 match de::string(path, value)?.as_str() {
83 "observe" => Ok(Self::Observe),
84 "enforce" => Ok(Self::Enforce),
85 _ => fail(path, ErrorKind::InvalidValue),
86 }
87 }
88}
89
90#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
91pub enum PromotableFindingKind {
92 ExplicitTargetMissing,
93 ExplicitTargetTypeMismatch,
94 InvalidReference,
95}
96
97impl PromotableFindingKind {
98 #[must_use]
99 pub const fn as_str(self) -> &'static str {
100 match self {
101 Self::ExplicitTargetMissing => "explicit-target-missing",
102 Self::ExplicitTargetTypeMismatch => "explicit-target-type-mismatch",
103 Self::InvalidReference => "invalid-reference",
104 }
105 }
106
107 fn decode(path: &str, value: Value) -> Result<Self, Error> {
108 match de::string(path, value)?.as_str() {
109 "explicit-target-missing" => Ok(Self::ExplicitTargetMissing),
110 "explicit-target-type-mismatch" => Ok(Self::ExplicitTargetTypeMismatch),
111 "invalid-reference" => Ok(Self::InvalidReference),
112 _ => fail(path, ErrorKind::InvalidValue),
113 }
114 }
115}
116
117#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
118pub enum EligibleFindingKind {
119 ExplicitTargetMissing,
120 ExplicitTargetTypeMismatch,
121}
122
123impl EligibleFindingKind {
124 #[must_use]
125 pub const fn as_str(self) -> &'static str {
126 match self {
127 Self::ExplicitTargetMissing => "explicit-target-missing",
128 Self::ExplicitTargetTypeMismatch => "explicit-target-type-mismatch",
129 }
130 }
131
132 fn decode(path: &str, value: Value) -> Result<Self, Error> {
133 match de::string(path, value)?.as_str() {
134 "explicit-target-missing" => Ok(Self::ExplicitTargetMissing),
135 "explicit-target-type-mismatch" => Ok(Self::ExplicitTargetTypeMismatch),
136 _ => fail(path, ErrorKind::InvalidValue),
137 }
138 }
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
142pub enum SourceConstruct {
143 InlineLink,
144 FullReferenceLink,
145 CollapsedReferenceLink,
146 ShortcutReferenceLink,
147 Autolink,
148 InlineImage,
149 FullReferenceImage,
150 CollapsedReferenceImage,
151 ShortcutReferenceImage,
152}
153
154impl SourceConstruct {
155 #[must_use]
158 pub const fn is_image(self) -> bool {
159 match self {
160 Self::InlineImage
161 | Self::FullReferenceImage
162 | Self::CollapsedReferenceImage
163 | Self::ShortcutReferenceImage => true,
164 Self::InlineLink
165 | Self::FullReferenceLink
166 | Self::CollapsedReferenceLink
167 | Self::ShortcutReferenceLink
168 | Self::Autolink => false,
169 }
170 }
171
172 #[must_use]
173 pub const fn as_str(self) -> &'static str {
174 match self {
175 Self::InlineLink => "markdown-inline-link",
176 Self::FullReferenceLink => "markdown-full-reference-link",
177 Self::CollapsedReferenceLink => "markdown-collapsed-reference-link",
178 Self::ShortcutReferenceLink => "markdown-shortcut-reference-link",
179 Self::Autolink => "markdown-autolink",
180 Self::InlineImage => "markdown-inline-image",
181 Self::FullReferenceImage => "markdown-full-reference-image",
182 Self::CollapsedReferenceImage => "markdown-collapsed-reference-image",
183 Self::ShortcutReferenceImage => "markdown-shortcut-reference-image",
184 }
185 }
186
187 fn decode(path: &str, value: Value) -> Result<Self, Error> {
188 match de::string(path, value)?.as_str() {
189 "markdown-inline-link" => Ok(Self::InlineLink),
190 "markdown-full-reference-link" => Ok(Self::FullReferenceLink),
191 "markdown-collapsed-reference-link" => Ok(Self::CollapsedReferenceLink),
192 "markdown-shortcut-reference-link" => Ok(Self::ShortcutReferenceLink),
193 "markdown-autolink" => Ok(Self::Autolink),
194 "markdown-inline-image" => Ok(Self::InlineImage),
195 "markdown-full-reference-image" => Ok(Self::FullReferenceImage),
196 "markdown-collapsed-reference-image" => Ok(Self::CollapsedReferenceImage),
197 "markdown-shortcut-reference-image" => Ok(Self::ShortcutReferenceImage),
198 _ => fail(path, ErrorKind::InvalidValue),
199 }
200 }
201}
202
203#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
204pub enum TargetKind {
205 Blob,
206 Tree,
207 Either,
208}
209
210impl TargetKind {
211 #[must_use]
212 pub const fn as_str(self) -> &'static str {
213 match self {
214 Self::Blob => "blob",
215 Self::Tree => "tree",
216 Self::Either => "either",
217 }
218 }
219
220 fn decode(path: &str, value: Value) -> Result<Self, Error> {
221 match de::string(path, value)?.as_str() {
222 "blob" => Ok(Self::Blob),
223 "tree" => Ok(Self::Tree),
224 "either" => Ok(Self::Either),
225 _ => fail(path, ErrorKind::InvalidValue),
226 }
227 }
228}
229
230#[derive(Clone, Copy, Debug, PartialEq, Eq)]
231pub enum EntryKind {
232 Blob,
233 Tree,
234 Symlink,
235 Gitlink,
236}
237
238impl EntryKind {
239 #[must_use]
240 pub const fn as_str(self) -> &'static str {
241 match self {
242 Self::Blob => "blob",
243 Self::Tree => "tree",
244 Self::Symlink => "symlink",
245 Self::Gitlink => "gitlink",
246 }
247 }
248
249 fn decode(path: &str, value: Value) -> Result<Self, Error> {
250 match de::string(path, value)?.as_str() {
251 "blob" => Ok(Self::Blob),
252 "tree" => Ok(Self::Tree),
253 "symlink" => Ok(Self::Symlink),
254 "gitlink" => Ok(Self::Gitlink),
255 _ => fail(path, ErrorKind::InvalidValue),
256 }
257 }
258}
259
260#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
261pub enum GitMode {
262 RegularFile,
263 ExecutableFile,
264 Tree,
265 Symlink,
266 Gitlink,
267}
268
269impl GitMode {
270 #[must_use]
271 pub const fn as_str(self) -> &'static str {
272 match self {
273 Self::RegularFile => "100644",
274 Self::ExecutableFile => "100755",
275 Self::Tree => "040000",
276 Self::Symlink => "120000",
277 Self::Gitlink => "160000",
278 }
279 }
280
281 fn decode(path: &str, value: Value) -> Result<Self, Error> {
282 match de::string(path, value)?.as_str() {
283 "100644" => Ok(Self::RegularFile),
284 "100755" => Ok(Self::ExecutableFile),
285 "040000" => Ok(Self::Tree),
286 "120000" => Ok(Self::Symlink),
287 "160000" => Ok(Self::Gitlink),
288 _ => fail(path, ErrorKind::InvalidValue),
289 }
290 }
291}
292
293#[derive(Clone, Copy, Debug, PartialEq, Eq)]
294pub enum ContentAvailability {
295 Available,
296 NotRead,
297 NotApplicable,
298 LfsPointerOnly,
299}
300
301impl ContentAvailability {
302 #[must_use]
303 pub const fn as_str(self) -> &'static str {
304 match self {
305 Self::Available => "available",
306 Self::NotRead => "not-read",
307 Self::NotApplicable => "not-applicable",
308 Self::LfsPointerOnly => "lfs-pointer-only",
309 }
310 }
311
312 fn decode(path: &str, value: Value) -> Result<Self, Error> {
313 match de::string(path, value)?.as_str() {
314 "available" => Ok(Self::Available),
315 "not-read" => Ok(Self::NotRead),
316 "not-applicable" => Ok(Self::NotApplicable),
317 "lfs-pointer-only" => Ok(Self::LfsPointerOnly),
318 _ => fail(path, ErrorKind::InvalidValue),
319 }
320 }
321}
322
323#[derive(Clone, Copy, Debug, PartialEq, Eq)]
324pub enum ResolutionKind {
325 Missing,
326 TypeMismatch,
327}
328
329#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
330pub enum ResourceName {
331 GitObjectBytes,
332 GitCompressedObjectBytes,
333 AggregateGitCompressedObjectBytesPerEvaluation,
334 GitPackDirectoryEntries,
335 GitPackFiles,
336 GitPackIndexBytes,
337 AggregateGitPackIndexBytes,
338 GitDeltaDepth,
339 GitIndexBytes,
340 GitTreeEntriesPerSnapshot,
341 DocumentsPerSnapshot,
342 ControlInputBytes,
343 SelectedControlBlobBytes,
344 AggregateSelectedControlBytesPerSnapshot,
345 RepositoryPolicyEntries,
346 DebtItems,
347 WaiverItems,
348 RawPathBytes,
349 DocumentBlobBytes,
350 ReferencedTargetBlobBytes,
351 AggregateReferencedTargetBytesPerSnapshot,
352 AggregateDocumentBytesPerSnapshot,
353 RawLinkDestinationBytes,
354 ParserNesting,
355 ParserNodesPerDocument,
356 ParserNodesPerSnapshot,
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 const fn phase(self) -> &'static str {
371 match self {
372 Self::ControlInputBytes
373 | Self::RepositoryPolicyEntries
374 | Self::DebtItems
375 | Self::WaiverItems
376 | Self::OrganizationPolicyEntries => "configuration",
377 Self::GitObjectBytes
378 | Self::GitCompressedObjectBytes
379 | Self::AggregateGitCompressedObjectBytesPerEvaluation
380 | Self::GitPackDirectoryEntries
381 | Self::GitPackFiles
382 | Self::GitPackIndexBytes
383 | Self::AggregateGitPackIndexBytes
384 | Self::GitDeltaDepth
385 | Self::GitIndexBytes
386 | Self::GitTreeEntriesPerSnapshot
387 | Self::RawPathBytes => "git",
388 Self::DocumentsPerSnapshot
389 | Self::DocumentBlobBytes
390 | Self::AggregateDocumentBytesPerSnapshot
391 | Self::SelectedControlBlobBytes
392 | Self::AggregateSelectedControlBytesPerSnapshot => "discovery",
393 Self::RawLinkDestinationBytes
394 | Self::ParserNesting
395 | Self::ParserNodesPerDocument
396 | Self::ParserNodesPerSnapshot
397 | Self::ReferencesPerDocument
398 | Self::ReferencesPerSnapshot => "parse",
399 Self::ReferencedTargetBlobBytes | Self::AggregateReferencedTargetBytesPerSnapshot => {
400 "resolution"
401 }
402 Self::CompleteFindings => "policy",
403 Self::MachineJsonBytes => "output",
404 Self::TypedAnalysisErrorsRetained
405 | Self::PrivateTemporaryStorageBytes
406 | Self::EvaluatorManagedMemoryBytes => "internal",
407 }
408 }
409
410 const ALL: [(&'static str, Self); 34] = [
411 ("git-object-bytes", Self::GitObjectBytes),
412 (
413 "git-compressed-object-bytes",
414 Self::GitCompressedObjectBytes,
415 ),
416 (
417 "aggregate-git-compressed-object-bytes-per-evaluation",
418 Self::AggregateGitCompressedObjectBytesPerEvaluation,
419 ),
420 ("git-pack-directory-entries", Self::GitPackDirectoryEntries),
421 ("git-pack-files", Self::GitPackFiles),
422 ("git-pack-index-bytes", Self::GitPackIndexBytes),
423 (
424 "aggregate-git-pack-index-bytes",
425 Self::AggregateGitPackIndexBytes,
426 ),
427 ("git-delta-depth", Self::GitDeltaDepth),
428 ("git-index-bytes", Self::GitIndexBytes),
429 (
430 "git-tree-entries-per-snapshot",
431 Self::GitTreeEntriesPerSnapshot,
432 ),
433 ("documents-per-snapshot", Self::DocumentsPerSnapshot),
434 ("control-input-bytes", Self::ControlInputBytes),
435 (
436 "selected-control-blob-bytes",
437 Self::SelectedControlBlobBytes,
438 ),
439 (
440 "aggregate-selected-control-bytes-per-snapshot",
441 Self::AggregateSelectedControlBytesPerSnapshot,
442 ),
443 ("repository-policy-entries", Self::RepositoryPolicyEntries),
444 ("debt-items", Self::DebtItems),
445 ("waiver-items", Self::WaiverItems),
446 ("raw-path-bytes", Self::RawPathBytes),
447 ("document-blob-bytes", Self::DocumentBlobBytes),
448 (
449 "referenced-target-blob-bytes",
450 Self::ReferencedTargetBlobBytes,
451 ),
452 (
453 "aggregate-referenced-target-bytes-per-snapshot",
454 Self::AggregateReferencedTargetBytesPerSnapshot,
455 ),
456 (
457 "aggregate-document-bytes-per-snapshot",
458 Self::AggregateDocumentBytesPerSnapshot,
459 ),
460 ("raw-link-destination-bytes", Self::RawLinkDestinationBytes),
461 ("parser-nesting", Self::ParserNesting),
462 ("parser-nodes-per-document", Self::ParserNodesPerDocument),
463 ("parser-nodes-per-snapshot", Self::ParserNodesPerSnapshot),
464 ("references-per-document", Self::ReferencesPerDocument),
465 ("references-per-snapshot", Self::ReferencesPerSnapshot),
466 (
467 "organization-policy-entries",
468 Self::OrganizationPolicyEntries,
469 ),
470 ("complete-findings", Self::CompleteFindings),
471 (
472 "typed-analysis-errors-retained",
473 Self::TypedAnalysisErrorsRetained,
474 ),
475 ("machine-json-bytes", Self::MachineJsonBytes),
476 (
477 "private-temporary-storage-bytes",
478 Self::PrivateTemporaryStorageBytes,
479 ),
480 (
481 "evaluator-managed-memory-bytes",
482 Self::EvaluatorManagedMemoryBytes,
483 ),
484 ];
485
486 #[must_use]
487 pub fn as_str(self) -> &'static str {
488 Self::ALL
489 .iter()
490 .find(|(_, variant)| *variant == self)
491 .map_or("", |(name, _)| name)
492 }
493
494 fn decode(path: &str, value: Value) -> Result<Self, Error> {
495 let raw = de::string(path, value)?;
496 Self::ALL
497 .iter()
498 .find(|(name, _)| *name == raw)
499 .map(|(_, variant)| *variant)
500 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
501 }
502}
503
504#[derive(Clone, Debug, PartialEq, Eq)]
505pub struct DocumentInclude {
506 pub path: RepoPath,
507 pub kind: IncludeKind,
508}
509
510#[derive(Clone, Debug, PartialEq, Eq)]
511pub struct FindingDisposition {
512 pub finding_kind: PromotableFindingKind,
513 pub disposition: Disposition,
514}
515
516#[derive(Clone, Debug, PartialEq, Eq)]
517pub struct ScannerPolicy {
518 pub digest: Digest,
519 pub document_includes: Vec<DocumentInclude>,
520 pub protected_inventory: Vec<RepoPath>,
521 pub finding_dispositions: Vec<FindingDisposition>,
522}
523
524impl ScannerPolicy {
525 pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
530 let value = root(bytes)?;
531 let digest = hj(SCANNER_POLICY_SCHEMA, &value);
532 let mut obj = Obj::new("$", value)?;
533 de::const_str(
534 &obj.field("schema"),
535 obj.take("schema")?,
536 SCANNER_POLICY_SCHEMA,
537 )?;
538
539 let includes_path = obj.field("document_includes");
540 let includes = de::array(&includes_path, obj.take("document_includes")?)?;
541 let document_includes = decode_items(&includes_path, includes, 100_000, decode_include)?;
542 sorted_set(&includes_path, &document_includes, |a, b| {
543 (a.path.as_str(), a.kind).cmp(&(b.path.as_str(), b.kind))
544 })?;
545
546 let inventory_path = obj.field("protected_inventory");
547 let protected_inventory =
548 decode_path_set(&inventory_path, obj.take("protected_inventory")?)?;
549
550 let dispositions_path = obj.field("finding_dispositions");
551 let raw = de::array(&dispositions_path, obj.take("finding_dispositions")?)?;
552 let finding_dispositions =
553 decode_items(&dispositions_path, raw, 3, decode_disposition_rule)?;
554 sorted_set(&dispositions_path, &finding_dispositions, |a, b| {
555 a.finding_kind.as_str().cmp(b.finding_kind.as_str())
556 })?;
557
558 obj.finish()?;
559 Ok(Self {
560 digest,
561 document_includes,
562 protected_inventory,
563 finding_dispositions,
564 })
565 }
566}
567
568#[derive(Clone, Debug, PartialEq, Eq)]
569pub struct ResourceLimit {
570 pub resource: ResourceName,
571 pub maximum: i64,
572}
573
574#[derive(Clone, Debug, PartialEq, Eq)]
575pub struct FloorDisposition {
576 pub finding_kind: PromotableFindingKind,
577 pub disposition: Disposition,
578}
579
580#[derive(Clone, Debug, PartialEq, Eq)]
581pub struct OrganizationFloor {
582 pub digest: Digest,
583 pub floor_id: ArtifactId,
584 pub repository: RepositoryIdentity,
585 pub ref_name: BranchRef,
586 pub minimum_profile: Profile,
587 pub minimum_dispositions: Vec<FindingDisposition>,
588 pub protected_inventory: Vec<RepoPath>,
589 pub protected_control_paths: Vec<RepoPath>,
590 pub waivable_finding_kinds: Vec<EligibleFindingKind>,
591 pub authorized_debt_owners: Vec<OwnerId>,
592 pub authorized_waiver_issuers: Vec<OwnerId>,
593 pub resource_limits: Vec<ResourceLimit>,
594}
595
596#[derive(Clone, Debug, PartialEq, Eq)]
599pub enum FloorDefect {
600 Schema(Error),
601 Entries {
602 configured_limit: u64,
603 observed_lower_bound: u64,
604 },
605}
606
607impl From<Error> for FloorDefect {
608 fn from(error: Error) -> Self {
609 Self::Schema(error)
610 }
611}
612
613pub const ORGANIZATION_POLICY_ENTRIES_LIMIT: u64 = 100_000;
614
615impl OrganizationFloor {
616 pub fn parse(bytes: &[u8]) -> Result<Self, FloorDefect> {
623 let value = root(bytes)?;
624 let digest = hj(ORGANIZATION_FLOOR_SCHEMA, &value);
625 let mut obj = Obj::new("$", value)?;
626 de::const_str(
627 &obj.field("schema"),
628 obj.take("schema")?,
629 ORGANIZATION_FLOOR_SCHEMA,
630 )?;
631
632 let floor_id = decode_artifact_id(&obj.field("floor_id"), obj.take("floor_id")?)?;
633 let repository = decode_repository(&obj.field("repository"), obj.take("repository")?)?;
634 let ref_name = decode_branch_ref(&obj.field("ref"), obj.take("ref")?)?;
635 let minimum_profile =
636 Profile::decode(&obj.field("minimum_profile"), obj.take("minimum_profile")?)?;
637
638 let dispositions_path = obj.field("minimum_dispositions");
639 let dispositions_raw = de::array(&dispositions_path, obj.take("minimum_dispositions")?)?;
640 let inventory_path = obj.field("protected_inventory");
641 let inventory_raw = de::array(&inventory_path, obj.take("protected_inventory")?)?;
642 let control_paths_path = obj.field("protected_control_paths");
643 let control_paths_raw =
644 de::array(&control_paths_path, obj.take("protected_control_paths")?)?;
645 let waivable_path = obj.field("waivable_finding_kinds");
646 let waivable_raw = de::array(&waivable_path, obj.take("waivable_finding_kinds")?)?;
647 let owners_path = obj.field("authorized_debt_owners");
648 let owners_raw = de::array(&owners_path, obj.take("authorized_debt_owners")?)?;
649 let issuers_path = obj.field("authorized_waiver_issuers");
650 let issuers_raw = de::array(&issuers_path, obj.take("authorized_waiver_issuers")?)?;
651 let limits_path = obj.field("resource_limits");
652 let limits_raw = de::array(&limits_path, obj.take("resource_limits")?)?;
653
654 let combined = [
655 dispositions_raw.len(),
656 inventory_raw.len(),
657 control_paths_raw.len(),
658 waivable_raw.len(),
659 owners_raw.len(),
660 issuers_raw.len(),
661 limits_raw.len(),
662 ]
663 .iter()
664 .map(|&len| u64::try_from(len).unwrap_or(u64::MAX))
665 .fold(0_u64, u64::saturating_add);
666 if combined > ORGANIZATION_POLICY_ENTRIES_LIMIT {
667 return Err(FloorDefect::Entries {
668 configured_limit: ORGANIZATION_POLICY_ENTRIES_LIMIT,
669 observed_lower_bound: ORGANIZATION_POLICY_ENTRIES_LIMIT.saturating_add(1),
670 });
671 }
672
673 let minimum_dispositions = decode_items(
674 &dispositions_path,
675 dispositions_raw,
676 3,
677 decode_disposition_rule,
678 )?;
679 sorted_set(&dispositions_path, &minimum_dispositions, |a, b| {
680 a.finding_kind.as_str().cmp(b.finding_kind.as_str())
681 })?;
682 let protected_inventory = decode_path_items(&inventory_path, inventory_raw)?;
683 let protected_control_paths = decode_path_items(&control_paths_path, control_paths_raw)?;
684 let waivable_finding_kinds =
685 decode_items(&waivable_path, waivable_raw, 2, |path, value| {
686 EligibleFindingKind::decode(path, value)
687 })?;
688 sorted_set(&waivable_path, &waivable_finding_kinds, |a, b| {
689 a.as_str().cmp(b.as_str())
690 })?;
691 let authorized_debt_owners = decode_owner_items(&owners_path, owners_raw)?;
692 let authorized_waiver_issuers = decode_owner_items(&issuers_path, issuers_raw)?;
693 let resource_limits = decode_items(&limits_path, limits_raw, 34, decode_resource_limit)?;
694 sorted_set(&limits_path, &resource_limits, |a, b| {
695 a.resource.as_str().cmp(b.resource.as_str())
696 })?;
697
698 obj.finish()?;
699 if let Some(declared) = resource_limits
700 .iter()
701 .find(|row| row.resource == ResourceName::OrganizationPolicyEntries)
702 {
703 let declared = u64::try_from(declared.maximum).unwrap_or(u64::MAX);
704 if combined > declared {
705 return Err(FloorDefect::Entries {
706 configured_limit: declared,
707 observed_lower_bound: declared.saturating_add(1),
708 });
709 }
710 }
711 Ok(Self {
712 digest,
713 floor_id,
714 repository,
715 ref_name,
716 minimum_profile,
717 minimum_dispositions,
718 protected_inventory,
719 protected_control_paths,
720 waivable_finding_kinds,
721 authorized_debt_owners,
722 authorized_waiver_issuers,
723 resource_limits,
724 })
725 }
726}
727
728#[derive(Clone, Debug, PartialEq, Eq)]
733pub struct TrustedTimeStatement {
734 pub digest: Digest,
735 pub repository: RepositoryIdentity,
736 pub ref_name: BranchRef,
737 pub candidate_identity_digest: Digest,
738 pub provider_run_id: String,
739 pub provider_run_attempt: u64,
740 pub evaluation_instant: UtcInstant,
741 pub valid_until: UtcInstant,
742}
743
744impl TrustedTimeStatement {
745 pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
751 let value = root(bytes)?;
752 let digest = hj(TRUSTED_TIME_STATEMENT_SCHEMA, &value);
753 let mut obj = Obj::new("$", value)?;
754 de::const_str(
755 &obj.field("schema"),
756 obj.take("schema")?,
757 TRUSTED_TIME_STATEMENT_SCHEMA,
758 )?;
759 de::const_str(
760 &obj.field("controller"),
761 obj.take("controller")?,
762 TRUSTED_TIME_CONTROLLER,
763 )?;
764 let repository = decode_repository(&obj.field("repository"), obj.take("repository")?)?;
765 let ref_name = decode_branch_ref(&obj.field("ref"), obj.take("ref")?)?;
766 let candidate_identity_digest = decode_digest(
767 &obj.field("candidate_identity_digest"),
768 obj.take("candidate_identity_digest")?,
769 )?;
770 let run_id_path = obj.field("provider_run_id");
771 let provider_run_id = de::string(&run_id_path, obj.take("provider_run_id")?)?;
772 let run_id_bytes = provider_run_id.as_bytes();
773 if run_id_bytes.is_empty()
774 || run_id_bytes.len() > 32
775 || !matches!(run_id_bytes.first(), Some(b'1'..=b'9'))
776 || !run_id_bytes.iter().all(u8::is_ascii_digit)
777 {
778 return fail(&run_id_path, ErrorKind::InvalidValue);
779 }
780 let attempt_path = obj.field("provider_run_attempt");
781 let attempt_raw = de::integer(&attempt_path, obj.take("provider_run_attempt")?)?;
782 let provider_run_attempt = u64::try_from(attempt_raw)
783 .ok()
784 .filter(|attempt| *attempt >= 1)
785 .ok_or_else(|| Error::new(&attempt_path, ErrorKind::InvalidValue))?;
786 let evaluation_instant = decode_instant(
787 &obj.field("evaluation_instant"),
788 obj.take("evaluation_instant")?,
789 )?;
790 let until_path = obj.field("valid_until");
791 let valid_until = decode_instant(&until_path, obj.take("valid_until")?)?;
792 obj.finish()?;
793 let lifetime = valid_until
794 .epoch_seconds()
795 .saturating_sub(evaluation_instant.epoch_seconds());
796 if lifetime <= 0 || lifetime > STATEMENT_TTL_MAX_SECONDS {
797 return fail(&until_path, ErrorKind::InvalidValue);
798 }
799 Ok(Self {
800 digest,
801 repository,
802 ref_name,
803 candidate_identity_digest,
804 provider_run_id,
805 provider_run_attempt,
806 evaluation_instant,
807 valid_until,
808 })
809 }
810}
811
812#[derive(Clone, Copy, Debug, PartialEq, Eq)]
813pub enum ConstraintPlatform {
814 LinuxX8664,
815 LinuxAarch64,
816 MacosX8664,
817 MacosAarch64,
818 WindowsX8664,
819 WindowsAarch64,
820}
821
822impl ConstraintPlatform {
823 #[must_use]
824 pub const fn as_str(self) -> &'static str {
825 match self {
826 Self::LinuxX8664 => "linux-x86_64",
827 Self::LinuxAarch64 => "linux-aarch64",
828 Self::MacosX8664 => "macos-x86_64",
829 Self::MacosAarch64 => "macos-aarch64",
830 Self::WindowsX8664 => "windows-x86_64",
831 Self::WindowsAarch64 => "windows-aarch64",
832 }
833 }
834
835 pub fn decode(path: &str, value: Value) -> Result<Self, Error> {
839 match de::string(path, value)?.as_str() {
840 "linux-x86_64" => Ok(Self::LinuxX8664),
841 "linux-aarch64" => Ok(Self::LinuxAarch64),
842 "macos-x86_64" => Ok(Self::MacosX8664),
843 "macos-aarch64" => Ok(Self::MacosAarch64),
844 "windows-x86_64" => Ok(Self::WindowsX8664),
845 "windows-aarch64" => Ok(Self::WindowsAarch64),
846 _ => fail(path, ErrorKind::InvalidValue),
847 }
848 }
849}
850
851#[derive(Clone, Debug, PartialEq, Eq)]
854pub struct ExecutionConstraintDescriptor {
855 pub digest: Digest,
856 pub action_repository: RepositoryIdentity,
857 pub action_object_format: ObjectFormat,
858 pub action_commit_oid: Oid,
859 pub action_tree_oid: Oid,
860 pub manifest_path: RepoPath,
861 pub release_manifest_digest: Digest,
862 pub selected_platform: ConstraintPlatform,
863 pub required_status_name: String,
864 pub bootstrap_digest: Digest,
865}
866
867fn decode_status_name(path: &str, value: Value) -> Result<String, Error> {
868 let raw = de::string(path, value)?;
869 let bytes = raw.as_bytes();
870 let interior = |byte: &u8| {
871 byte.is_ascii_alphanumeric() || matches!(byte, b' ' | b'.' | b'_' | b'/' | b'-')
872 };
873 let edge =
874 |byte: &u8| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'/' | b'-');
875 let valid = match (bytes.first(), bytes.last()) {
876 (Some(first), Some(last)) => {
877 bytes.len() <= 160
878 && first.is_ascii_alphanumeric()
879 && (bytes.len() == 1 || edge(last))
880 && bytes.iter().all(interior)
881 }
882 _ => false,
883 };
884 if valid {
885 Ok(raw)
886 } else {
887 fail(path, ErrorKind::InvalidValue)
888 }
889}
890
891impl ExecutionConstraintDescriptor {
892 pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
897 let value = root(bytes)?;
898 let digest = hj(EXECUTION_CONSTRAINT_SCHEMA, &value);
899 let mut obj = Obj::new("$", value)?;
900 de::const_str(
901 &obj.field("schema"),
902 obj.take("schema")?,
903 EXECUTION_CONSTRAINT_SCHEMA,
904 )?;
905 let action_repository = decode_repository(
906 &obj.field("action_repository"),
907 obj.take("action_repository")?,
908 )?;
909 let format_path = obj.field("action_object_format");
910 let action_object_format =
911 match de::string(&format_path, obj.take("action_object_format")?)?.as_str() {
912 "sha1" => ObjectFormat::Sha1,
913 "sha256" => ObjectFormat::Sha256,
914 _ => return fail(&format_path, ErrorKind::InvalidValue),
915 };
916 let commit_path = obj.field("action_commit_oid");
917 let action_commit_oid = Oid::new(
918 action_object_format,
919 de::string(&commit_path, obj.take("action_commit_oid")?)?,
920 )
921 .ok_or_else(|| Error::new(&commit_path, ErrorKind::InvalidValue))?;
922 let tree_path = obj.field("action_tree_oid");
923 let action_tree_oid = Oid::new(
924 action_object_format,
925 de::string(&tree_path, obj.take("action_tree_oid")?)?,
926 )
927 .ok_or_else(|| Error::new(&tree_path, ErrorKind::InvalidValue))?;
928 let manifest_path =
929 decode_repo_path(&obj.field("manifest_path"), obj.take("manifest_path")?)?;
930 let release_manifest_digest = decode_digest(
931 &obj.field("release_manifest_digest"),
932 obj.take("release_manifest_digest")?,
933 )?;
934 let selected_platform = ConstraintPlatform::decode(
935 &obj.field("selected_platform"),
936 obj.take("selected_platform")?,
937 )?;
938 let required_status_name = decode_status_name(
939 &obj.field("required_status_name"),
940 obj.take("required_status_name")?,
941 )?;
942 de::const_str(
943 &obj.field("bootstrap_contract"),
944 obj.take("bootstrap_contract")?,
945 ACTION_BOOTSTRAP_CONTRACT,
946 )?;
947 let bootstrap_digest = decode_digest(
948 &obj.field("bootstrap_digest"),
949 obj.take("bootstrap_digest")?,
950 )?;
951 obj.finish()?;
952 Ok(Self {
953 digest,
954 action_repository,
955 action_object_format,
956 action_commit_oid,
957 action_tree_oid,
958 manifest_path,
959 release_manifest_digest,
960 selected_platform,
961 required_status_name,
962 bootstrap_digest,
963 })
964 }
965}
966
967#[derive(Clone, Debug, PartialEq, Eq)]
968pub struct TargetIntent {
969 pub path: RepoPath,
970 pub target_kind: TargetKind,
971 pub query_digest: Option<Digest>,
972 pub fragment_digest: Option<Digest>,
973}
974
975#[derive(Clone, Debug, PartialEq, Eq)]
976pub struct FindingScope {
977 pub document: RepoPath,
978 pub source_construct: SourceConstruct,
979 pub normalized_target_intent: TargetIntent,
980 pub source_projection_digest: Digest,
981}
982
983#[derive(Clone, Debug, PartialEq, Eq)]
984pub struct FindingKeyInput {
985 pub finding_kind: EligibleFindingKind,
986 pub scope: FindingScope,
987}
988
989#[derive(Clone, Debug, PartialEq, Eq)]
990pub struct Resolution {
991 pub kind: ResolutionKind,
992 pub path: Option<RepoPath>,
993 pub entry_kind: Option<EntryKind>,
994 pub git_mode: Option<GitMode>,
995 pub raw_digest: Option<Digest>,
996 pub projection_digest: Option<Digest>,
997 pub content_availability: ContentAvailability,
998}
999
1000#[derive(Clone, Debug, PartialEq, Eq)]
1001pub struct Fact {
1002 pub finding_kind: EligibleFindingKind,
1003 pub key_input: FindingKeyInput,
1004 pub resolution: Resolution,
1005}
1006
1007#[derive(Clone, Debug, PartialEq, Eq)]
1008pub struct DebtItem {
1009 pub debt_id: ArtifactId,
1010 pub finding_kind: EligibleFindingKind,
1011 pub key_input: FindingKeyInput,
1012 pub finding_key: Digest,
1013 pub accepted_fact: Fact,
1014 pub accepted_fact_digest: Digest,
1015 pub owner: OwnerId,
1016 pub reason: String,
1017 pub created_at: UtcInstant,
1018 pub expires_at: UtcInstant,
1019}
1020
1021#[derive(Clone, Debug, PartialEq, Eq)]
1022pub struct DebtSnapshot {
1023 pub digest: Digest,
1024 pub repository: RepositoryIdentity,
1025 pub ref_name: BranchRef,
1026 pub organization_floor_digest: Digest,
1027 pub adoption_tree: TreeIdentity,
1028 pub adoption_report_payload_digest: Digest,
1029 pub created_at: UtcInstant,
1030 pub items: Vec<DebtItem>,
1031}
1032
1033impl DebtSnapshot {
1034 pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
1040 let value = root(bytes)?;
1041 let digest = hj(DEBT_SNAPSHOT_SCHEMA, &value);
1042 let mut obj = Obj::new("$", value)?;
1043 de::const_str(
1044 &obj.field("schema"),
1045 obj.take("schema")?,
1046 DEBT_SNAPSHOT_SCHEMA,
1047 )?;
1048
1049 let repository = decode_repository(&obj.field("repository"), obj.take("repository")?)?;
1050 let ref_name = decode_branch_ref(&obj.field("ref"), obj.take("ref")?)?;
1051 let organization_floor_digest = decode_digest(
1052 &obj.field("organization_floor_digest"),
1053 obj.take("organization_floor_digest")?,
1054 )?;
1055 let adoption_tree = decode_tree(&obj.field("adoption_tree"), obj.take("adoption_tree")?)?;
1056 let adoption_report_payload_digest = decode_digest(
1057 &obj.field("adoption_report_payload_digest"),
1058 obj.take("adoption_report_payload_digest")?,
1059 )?;
1060 let created_at = decode_instant(&obj.field("created_at"), obj.take("created_at")?)?;
1061
1062 let items_path = obj.field("items");
1063 let raw = de::array(&items_path, obj.take("items")?)?;
1064 let items = decode_items(&items_path, raw, 100_000, decode_debt_item)?;
1065 sorted_set(&items_path, &items, |a, b| {
1066 a.debt_id.as_str().cmp(b.debt_id.as_str())
1067 })?;
1068 let mut keys: BTreeSet<Digest> = BTreeSet::new();
1069 for item in &items {
1070 if !keys.insert(item.finding_key) {
1071 return fail(&items_path, ErrorKind::DuplicateMember);
1072 }
1073 if item.created_at > created_at {
1074 return fail(&items_path, ErrorKind::Inconsistent);
1075 }
1076 }
1077
1078 obj.finish()?;
1079 Ok(Self {
1080 digest,
1081 repository,
1082 ref_name,
1083 organization_floor_digest,
1084 adoption_tree,
1085 adoption_report_payload_digest,
1086 created_at,
1087 items,
1088 })
1089 }
1090}
1091
1092#[derive(Clone, Debug, PartialEq, Eq)]
1093pub struct WaiverItem {
1094 pub waiver_id: ArtifactId,
1095 pub finding_kind: EligibleFindingKind,
1096 pub key_input: FindingKeyInput,
1097 pub finding_key: Digest,
1098 pub authorized_fact: Fact,
1099 pub authorized_fact_digest: Digest,
1100 pub candidate_tree: TreeIdentity,
1101 pub owner: OwnerId,
1102 pub issuer: OwnerId,
1103 pub reason: String,
1104 pub created_at: UtcInstant,
1105 pub not_before: UtcInstant,
1106 pub expires_at: UtcInstant,
1107}
1108
1109#[derive(Clone, Debug, PartialEq, Eq)]
1110pub struct WaiverBundle {
1111 pub digest: Digest,
1112 pub repository: RepositoryIdentity,
1113 pub ref_name: BranchRef,
1114 pub organization_floor_digest: Digest,
1115 pub created_at: UtcInstant,
1116 pub items: Vec<WaiverItem>,
1117}
1118
1119impl WaiverBundle {
1120 pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
1127 let value = root(bytes)?;
1128 let digest = hj(WAIVER_BUNDLE_SCHEMA, &value);
1129 let mut obj = Obj::new("$", value)?;
1130 de::const_str(
1131 &obj.field("schema"),
1132 obj.take("schema")?,
1133 WAIVER_BUNDLE_SCHEMA,
1134 )?;
1135
1136 let repository = decode_repository(&obj.field("repository"), obj.take("repository")?)?;
1137 let ref_name = decode_branch_ref(&obj.field("ref"), obj.take("ref")?)?;
1138 let organization_floor_digest = decode_digest(
1139 &obj.field("organization_floor_digest"),
1140 obj.take("organization_floor_digest")?,
1141 )?;
1142 let created_at = decode_instant(&obj.field("created_at"), obj.take("created_at")?)?;
1143
1144 let items_path = obj.field("items");
1145 let raw = de::array(&items_path, obj.take("items")?)?;
1146 let items = decode_items(&items_path, raw, 100_000, decode_waiver_item)?;
1147 sorted_set(&items_path, &items, |a, b| {
1148 waiver_sort_key(a).cmp(&waiver_sort_key(b))
1149 })?;
1150 for pair in items.windows(2) {
1151 if let [left, right] = pair
1152 && left.candidate_tree == right.candidate_tree
1153 && left.finding_key == right.finding_key
1154 {
1155 return fail(&items_path, ErrorKind::DuplicateMember);
1156 }
1157 }
1158 let mut ids: BTreeSet<&str> = BTreeSet::new();
1159 for item in &items {
1160 if !ids.insert(item.waiver_id.as_str()) {
1161 return fail(&items_path, ErrorKind::DuplicateMember);
1162 }
1163 if item.created_at > created_at {
1164 return fail(&items_path, ErrorKind::Inconsistent);
1165 }
1166 }
1167
1168 obj.finish()?;
1169 Ok(Self {
1170 digest,
1171 repository,
1172 ref_name,
1173 organization_floor_digest,
1174 created_at,
1175 items,
1176 })
1177 }
1178}
1179
1180fn waiver_sort_key(item: &WaiverItem) -> (ObjectFormat, &str, Digest, &str) {
1181 (
1182 item.candidate_tree.object_format,
1183 item.candidate_tree.tree_oid.as_str(),
1184 item.finding_key,
1185 item.waiver_id.as_str(),
1186 )
1187}
1188
1189pub fn root(bytes: &[u8]) -> Result<Value, Error> {
1195 json::parse(bytes).map_err(|defect| Error::new("$", ErrorKind::Json(defect)))
1196}
1197
1198fn decode_items<T>(
1199 path: &str,
1200 raw: Vec<Value>,
1201 limit: usize,
1202 decode: impl Fn(&str, Value) -> Result<T, Error>,
1203) -> Result<Vec<T>, Error> {
1204 if raw.len() > limit {
1205 return fail(path, ErrorKind::LimitExceeded);
1206 }
1207 raw.into_iter()
1208 .enumerate()
1209 .map(|(index, value)| decode(&format!("{path}[{index}]"), value))
1210 .collect()
1211}
1212
1213fn sorted_set<T>(
1214 path: &str,
1215 items: &[T],
1216 compare: impl Fn(&T, &T) -> Ordering,
1217) -> Result<(), Error> {
1218 for pair in items.windows(2) {
1219 if let [left, right] = pair {
1220 match compare(left, right) {
1221 Ordering::Less => {}
1222 Ordering::Equal => return fail(path, ErrorKind::DuplicateMember),
1223 Ordering::Greater => return fail(path, ErrorKind::UnsortedSet),
1224 }
1225 }
1226 }
1227 Ok(())
1228}
1229
1230fn decode_include(path: &str, value: Value) -> Result<DocumentInclude, Error> {
1231 let mut obj = Obj::new(path, value)?;
1232 let include_path = decode_repo_path(&obj.field("path"), obj.take("path")?)?;
1233 let kind = IncludeKind::decode(&obj.field("kind"), obj.take("kind")?)?;
1234 obj.finish()?;
1235 Ok(DocumentInclude {
1236 path: include_path,
1237 kind,
1238 })
1239}
1240
1241fn decode_disposition_rule(path: &str, value: Value) -> Result<FindingDisposition, Error> {
1242 let mut obj = Obj::new(path, value)?;
1243 let finding_kind =
1244 PromotableFindingKind::decode(&obj.field("finding_kind"), obj.take("finding_kind")?)?;
1245 let disposition = Disposition::decode(&obj.field("disposition"), obj.take("disposition")?)?;
1246 obj.finish()?;
1247 Ok(FindingDisposition {
1248 finding_kind,
1249 disposition,
1250 })
1251}
1252
1253fn decode_resource_limit(path: &str, value: Value) -> Result<ResourceLimit, Error> {
1254 let mut obj = Obj::new(path, value)?;
1255 let resource = ResourceName::decode(&obj.field("resource"), obj.take("resource")?)?;
1256 let maximum_path = obj.field("maximum");
1257 let maximum = de::integer(&maximum_path, obj.take("maximum")?)?;
1258 obj.finish()?;
1259 let in_bounds = if resource == ResourceName::TypedAnalysisErrorsRetained {
1260 (1..=64).contains(&maximum)
1261 } else if resource == ResourceName::MachineJsonBytes {
1262 maximum == 67_108_864
1263 } else {
1264 maximum >= 0
1265 };
1266 if in_bounds {
1267 Ok(ResourceLimit { resource, maximum })
1268 } else {
1269 fail(&maximum_path, ErrorKind::InvalidValue)
1270 }
1271}
1272
1273fn decode_path_set(path: &str, value: Value) -> Result<Vec<RepoPath>, Error> {
1274 decode_path_items(path, de::array(path, value)?)
1275}
1276
1277fn decode_path_items(path: &str, raw: Vec<Value>) -> Result<Vec<RepoPath>, Error> {
1278 let paths = decode_items(path, raw, 100_000, decode_repo_path)?;
1279 sorted_set(path, &paths, |a, b| a.as_str().cmp(b.as_str()))?;
1280 Ok(paths)
1281}
1282
1283fn decode_owner_items(path: &str, raw: Vec<Value>) -> Result<Vec<OwnerId>, Error> {
1284 let owners = decode_items(path, raw, 10_000, decode_owner)?;
1285 sorted_set(path, &owners, |a, b| a.as_str().cmp(b.as_str()))?;
1286 Ok(owners)
1287}
1288
1289fn decode_repo_path(path: &str, value: Value) -> Result<RepoPath, Error> {
1290 RepoPath::new(de::string(path, value)?).ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1291}
1292
1293fn decode_artifact_id(path: &str, value: Value) -> Result<ArtifactId, Error> {
1294 ArtifactId::new(de::string(path, value)?)
1295 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1296}
1297
1298fn decode_owner(path: &str, value: Value) -> Result<OwnerId, Error> {
1299 OwnerId::new(de::string(path, value)?).ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1300}
1301
1302fn decode_branch_ref(path: &str, value: Value) -> Result<BranchRef, Error> {
1303 BranchRef::new(de::string(path, value)?)
1304 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1305}
1306
1307fn decode_instant(path: &str, value: Value) -> Result<UtcInstant, Error> {
1308 UtcInstant::new(de::string(path, value)?)
1309 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1310}
1311
1312fn decode_digest(path: &str, value: Value) -> Result<Digest, Error> {
1313 let raw = de::string(path, value)?;
1314 Digest::from_wire(&raw).ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1315}
1316
1317fn decode_nullable_digest(path: &str, value: Value) -> Result<Option<Digest>, Error> {
1318 de::nullable(value)
1319 .map(|v| decode_digest(path, v))
1320 .transpose()
1321}
1322
1323pub(crate) fn decode_repository(path: &str, value: Value) -> Result<RepositoryIdentity, Error> {
1324 let mut obj = Obj::new(path, value)?;
1325 de::const_str(&obj.field("host"), obj.take("host")?, "github.com")?;
1326 let owner = de::string(&obj.field("owner"), obj.take("owner")?)?;
1327 let name = de::string(&obj.field("name"), obj.take("name")?)?;
1328 obj.finish()?;
1329 RepositoryIdentity::new(owner, name).ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1330}
1331
1332fn decode_tree(path: &str, value: Value) -> Result<TreeIdentity, Error> {
1333 let mut obj = Obj::new(path, value)?;
1334 let format_path = obj.field("object_format");
1335 let object_format = match de::string(&format_path, obj.take("object_format")?)?.as_str() {
1336 "sha1" => ObjectFormat::Sha1,
1337 "sha256" => ObjectFormat::Sha256,
1338 _ => return fail(&format_path, ErrorKind::InvalidValue),
1339 };
1340 let tree_oid = de::string(&obj.field("tree_oid"), obj.take("tree_oid")?)?;
1341 obj.finish()?;
1342 TreeIdentity::new(object_format, tree_oid)
1343 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1344}
1345
1346fn decode_reason(path: &str, value: Value) -> Result<String, Error> {
1347 let raw = de::string(path, value)?;
1348 let length = raw.chars().count();
1349 if (1..=1024).contains(&length) && raw.chars().any(|c| !c.is_whitespace()) {
1350 Ok(raw)
1351 } else {
1352 fail(path, ErrorKind::InvalidValue)
1353 }
1354}
1355
1356fn decode_intent(path: &str, value: Value) -> Result<TargetIntent, Error> {
1357 let mut obj = Obj::new(path, value)?;
1358 de::const_str(&obj.field("kind"), obj.take("kind")?, "repository-path")?;
1359 let target_path = decode_repo_path(&obj.field("path"), obj.take("path")?)?;
1360 let target_kind = TargetKind::decode(&obj.field("target_kind"), obj.take("target_kind")?)?;
1361 let query_digest =
1362 decode_nullable_digest(&obj.field("query_digest"), obj.take("query_digest")?)?;
1363 let fragment_digest =
1364 decode_nullable_digest(&obj.field("fragment_digest"), obj.take("fragment_digest")?)?;
1365 obj.finish()?;
1366 Ok(TargetIntent {
1367 path: target_path,
1368 target_kind,
1369 query_digest,
1370 fragment_digest,
1371 })
1372}
1373
1374fn decode_scope(path: &str, value: Value) -> Result<FindingScope, Error> {
1375 let mut obj = Obj::new(path, value)?;
1376 de::const_str(&obj.field("kind"), obj.take("kind")?, "reference")?;
1377 let document = decode_repo_path(&obj.field("document"), obj.take("document")?)?;
1378 let source_construct = SourceConstruct::decode(
1379 &obj.field("source_construct"),
1380 obj.take("source_construct")?,
1381 )?;
1382 let normalized_target_intent = decode_intent(
1383 &obj.field("normalized_target_intent"),
1384 obj.take("normalized_target_intent")?,
1385 )?;
1386 let occurrence_path = obj.field("occurrence");
1387 let mut occurrence = Obj::new(&occurrence_path, obj.take("occurrence")?)?;
1388 de::const_str(
1389 &occurrence.field("kind"),
1390 occurrence.take("kind")?,
1391 "source-projection",
1392 )?;
1393 let source_projection_digest = decode_digest(
1394 &occurrence.field("source_projection_digest"),
1395 occurrence.take("source_projection_digest")?,
1396 )?;
1397 occurrence.finish()?;
1398 obj.finish()?;
1399 Ok(FindingScope {
1400 document,
1401 source_construct,
1402 normalized_target_intent,
1403 source_projection_digest,
1404 })
1405}
1406
1407fn decode_key_input(path: &str, value: Value) -> Result<(FindingKeyInput, Digest), Error> {
1408 let digest = hj(FINDING_KEY_DOMAIN, &value);
1409 let mut obj = Obj::new(path, value)?;
1410 de::const_str(
1411 &obj.field("schema"),
1412 obj.take("schema")?,
1413 FINDING_KEY_INPUT_SCHEMA,
1414 )?;
1415 let finding_kind =
1416 EligibleFindingKind::decode(&obj.field("finding_kind"), obj.take("finding_kind")?)?;
1417 let scope = decode_scope(&obj.field("scope"), obj.take("scope")?)?;
1418 obj.finish()?;
1419 Ok((
1420 FindingKeyInput {
1421 finding_kind,
1422 scope,
1423 },
1424 digest,
1425 ))
1426}
1427
1428fn decode_resolution(path: &str, value: Value) -> Result<Resolution, Error> {
1429 let mut obj = Obj::new(path, value)?;
1430 let status_path = obj.field("status");
1431 let status = de::string(&status_path, obj.take("status")?)?;
1432 let code = de::string(&obj.field("code"), obj.take("code")?)?;
1433 let kind = match (status.as_str(), code.as_str()) {
1434 ("missing", "path-not-found") => ResolutionKind::Missing,
1435 ("type-mismatch", "target-type-mismatch") => ResolutionKind::TypeMismatch,
1436 _ => return fail(&status_path, ErrorKind::Inconsistent),
1437 };
1438 let res_path = de::nullable(obj.take("path")?)
1439 .map(|v| decode_repo_path(&obj.field("path"), v))
1440 .transpose()?;
1441 let entry_kind = de::nullable(obj.take("entry_kind")?)
1442 .map(|v| EntryKind::decode(&obj.field("entry_kind"), v))
1443 .transpose()?;
1444 let git_mode = de::nullable(obj.take("git_mode")?)
1445 .map(|v| GitMode::decode(&obj.field("git_mode"), v))
1446 .transpose()?;
1447 let raw_digest = decode_nullable_digest(&obj.field("raw_digest"), obj.take("raw_digest")?)?;
1448 let projection_digest = decode_nullable_digest(
1449 &obj.field("projection_digest"),
1450 obj.take("projection_digest")?,
1451 )?;
1452 let content_availability = ContentAvailability::decode(
1453 &obj.field("content_availability"),
1454 obj.take("content_availability")?,
1455 )?;
1456 obj.finish()?;
1457
1458 let shape_ok = match kind {
1459 ResolutionKind::Missing => {
1460 entry_kind.is_none()
1461 && git_mode.is_none()
1462 && raw_digest.is_none()
1463 && projection_digest.is_none()
1464 && content_availability == ContentAvailability::NotApplicable
1465 }
1466 ResolutionKind::TypeMismatch => entry_kind.is_some() && git_mode.is_some(),
1467 };
1468 if !shape_ok {
1469 return fail(path, ErrorKind::Inconsistent);
1470 }
1471 Ok(Resolution {
1472 kind,
1473 path: res_path,
1474 entry_kind,
1475 git_mode,
1476 raw_digest,
1477 projection_digest,
1478 content_availability,
1479 })
1480}
1481
1482fn decode_fact(path: &str, value: Value) -> Result<(Fact, Digest), Error> {
1483 let digest = hj(FACT_DOMAIN, &value);
1484 let mut obj = Obj::new(path, value)?;
1485 de::const_str(&obj.field("schema"), obj.take("schema")?, FACT_SCHEMA)?;
1486 let finding_kind =
1487 EligibleFindingKind::decode(&obj.field("finding_kind"), obj.take("finding_kind")?)?;
1488 let key_path = obj.field("key_input");
1489 let (key_input, _) = decode_key_input(&key_path, obj.take("key_input")?)?;
1490 let evidence_path = obj.field("evidence");
1491 let mut evidence = Obj::new(&evidence_path, obj.take("evidence")?)?;
1492 de::const_str(&evidence.field("kind"), evidence.take("kind")?, "reference")?;
1493 let resolution =
1494 decode_resolution(&evidence.field("resolution"), evidence.take("resolution")?)?;
1495 let multiplicity_path = evidence.field("occurrence_multiplicity");
1496 if de::integer(
1497 &multiplicity_path,
1498 evidence.take("occurrence_multiplicity")?,
1499 )? != 1
1500 {
1501 return fail(&multiplicity_path, ErrorKind::InvalidValue);
1502 }
1503 evidence.finish()?;
1504 obj.finish()?;
1505
1506 let kind_matches = match resolution.kind {
1507 ResolutionKind::Missing => finding_kind == EligibleFindingKind::ExplicitTargetMissing,
1508 ResolutionKind::TypeMismatch => {
1509 finding_kind == EligibleFindingKind::ExplicitTargetTypeMismatch
1510 }
1511 };
1512 if !kind_matches || key_input.finding_kind != finding_kind {
1513 return fail(path, ErrorKind::Inconsistent);
1514 }
1515 Ok((
1516 Fact {
1517 finding_kind,
1518 key_input,
1519 resolution,
1520 },
1521 digest,
1522 ))
1523}
1524
1525struct ItemCore {
1526 finding_kind: EligibleFindingKind,
1527 key_input: FindingKeyInput,
1528 finding_key: Digest,
1529 fact: Fact,
1530 fact_digest: Digest,
1531 owner: OwnerId,
1532 reason: String,
1533 created_at: UtcInstant,
1534 expires_at: UtcInstant,
1535}
1536
1537fn decode_item_core(path: &str, obj: &mut Obj, fact_field: &str) -> Result<ItemCore, Error> {
1538 let finding_kind =
1539 EligibleFindingKind::decode(&obj.field("finding_kind"), obj.take("finding_kind")?)?;
1540 let key_path = obj.field("key_input");
1541 let (key_input, computed_key) = decode_key_input(&key_path, obj.take("key_input")?)?;
1542 let finding_key_path = obj.field("finding_key");
1543 let finding_key = decode_digest(&finding_key_path, obj.take("finding_key")?)?;
1544 if finding_key != computed_key {
1545 return fail(&finding_key_path, ErrorKind::DigestMismatch);
1546 }
1547 let fact_path = obj.field(fact_field);
1548 let (fact, computed_fact) = decode_fact(&fact_path, obj.take(fact_field)?)?;
1549 let fact_digest_field = format!("{fact_field}_digest");
1550 let fact_digest_path = obj.field(&fact_digest_field);
1551 let fact_digest = decode_digest(&fact_digest_path, obj.take(&fact_digest_field)?)?;
1552 if fact_digest != computed_fact {
1553 return fail(&fact_digest_path, ErrorKind::DigestMismatch);
1554 }
1555 if key_input.finding_kind != finding_kind || fact.key_input != key_input {
1556 return fail(path, ErrorKind::Inconsistent);
1557 }
1558 let owner = decode_owner(&obj.field("owner"), obj.take("owner")?)?;
1559 let reason = decode_reason(&obj.field("reason"), obj.take("reason")?)?;
1560 let created_at = decode_instant(&obj.field("created_at"), obj.take("created_at")?)?;
1561 let expires_at = decode_instant(&obj.field("expires_at"), obj.take("expires_at")?)?;
1562 Ok(ItemCore {
1563 finding_kind,
1564 key_input,
1565 finding_key,
1566 fact,
1567 fact_digest,
1568 owner,
1569 reason,
1570 created_at,
1571 expires_at,
1572 })
1573}
1574
1575fn decode_debt_item(path: &str, value: Value) -> Result<DebtItem, Error> {
1576 let mut obj = Obj::new(path, value)?;
1577 let debt_id = decode_artifact_id(&obj.field("debt_id"), obj.take("debt_id")?)?;
1578 let core = decode_item_core(path, &mut obj, "accepted_fact")?;
1579 obj.finish()?;
1580 if core.created_at >= core.expires_at {
1581 return fail(path, ErrorKind::Inconsistent);
1582 }
1583 Ok(DebtItem {
1584 debt_id,
1585 finding_kind: core.finding_kind,
1586 key_input: core.key_input,
1587 finding_key: core.finding_key,
1588 accepted_fact: core.fact,
1589 accepted_fact_digest: core.fact_digest,
1590 owner: core.owner,
1591 reason: core.reason,
1592 created_at: core.created_at,
1593 expires_at: core.expires_at,
1594 })
1595}
1596
1597fn decode_waiver_item(path: &str, value: Value) -> Result<WaiverItem, Error> {
1598 let mut obj = Obj::new(path, value)?;
1599 let waiver_id = decode_artifact_id(&obj.field("waiver_id"), obj.take("waiver_id")?)?;
1600 let core = decode_item_core(path, &mut obj, "authorized_fact")?;
1601 let candidate_tree = decode_tree(&obj.field("candidate_tree"), obj.take("candidate_tree")?)?;
1602 let issuer = decode_owner(&obj.field("issuer"), obj.take("issuer")?)?;
1603 let not_before = decode_instant(&obj.field("not_before"), obj.take("not_before")?)?;
1604 de::const_str(
1605 &obj.field("residual_disposition"),
1606 obj.take("residual_disposition")?,
1607 "warn",
1608 )?;
1609 obj.finish()?;
1610 if core.created_at > not_before || not_before >= core.expires_at {
1611 return fail(path, ErrorKind::Inconsistent);
1612 }
1613 Ok(WaiverItem {
1614 waiver_id,
1615 finding_kind: core.finding_kind,
1616 key_input: core.key_input,
1617 finding_key: core.finding_key,
1618 authorized_fact: core.fact,
1619 authorized_fact_digest: core.fact_digest,
1620 candidate_tree,
1621 owner: core.owner,
1622 issuer,
1623 reason: core.reason,
1624 created_at: core.created_at,
1625 not_before,
1626 expires_at: core.expires_at,
1627 })
1628}