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, RepoPathText, RepositoryIdentity,
9 TreeIdentity, 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: RepoPathText,
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<RepoPathText>,
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<RepoPathText>,
589 pub protected_control_paths: Vec<RepoPathText>,
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: RepoPathText,
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: RepoPathText,
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: RepoPathText,
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<RepoPathText>,
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<RepoPathText>, Error> {
1274 decode_path_items(path, de::array(path, value)?)
1275}
1276
1277fn decode_path_items(path: &str, raw: Vec<Value>) -> Result<Vec<RepoPathText>, 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<RepoPathText, Error> {
1290 RepoPathText::new(de::string(path, value)?)
1291 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1292}
1293
1294fn decode_artifact_id(path: &str, value: Value) -> Result<ArtifactId, Error> {
1295 ArtifactId::new(de::string(path, value)?)
1296 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1297}
1298
1299fn decode_owner(path: &str, value: Value) -> Result<OwnerId, Error> {
1300 OwnerId::new(de::string(path, value)?).ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1301}
1302
1303fn decode_branch_ref(path: &str, value: Value) -> Result<BranchRef, Error> {
1304 BranchRef::new(de::string(path, value)?)
1305 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1306}
1307
1308fn decode_instant(path: &str, value: Value) -> Result<UtcInstant, Error> {
1309 UtcInstant::new(de::string(path, value)?)
1310 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1311}
1312
1313fn decode_digest(path: &str, value: Value) -> Result<Digest, Error> {
1314 let raw = de::string(path, value)?;
1315 Digest::from_wire(&raw).ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1316}
1317
1318fn decode_nullable_digest(path: &str, value: Value) -> Result<Option<Digest>, Error> {
1319 de::nullable(value)
1320 .map(|v| decode_digest(path, v))
1321 .transpose()
1322}
1323
1324pub(crate) fn decode_repository(path: &str, value: Value) -> Result<RepositoryIdentity, Error> {
1325 let mut obj = Obj::new(path, value)?;
1326 de::const_str(&obj.field("host"), obj.take("host")?, "github.com")?;
1327 let owner = de::string(&obj.field("owner"), obj.take("owner")?)?;
1328 let name = de::string(&obj.field("name"), obj.take("name")?)?;
1329 obj.finish()?;
1330 RepositoryIdentity::github(owner, name).ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1331}
1332
1333fn decode_tree(path: &str, value: Value) -> Result<TreeIdentity, Error> {
1334 let mut obj = Obj::new(path, value)?;
1335 let format_path = obj.field("object_format");
1336 let object_format = match de::string(&format_path, obj.take("object_format")?)?.as_str() {
1337 "sha1" => ObjectFormat::Sha1,
1338 "sha256" => ObjectFormat::Sha256,
1339 _ => return fail(&format_path, ErrorKind::InvalidValue),
1340 };
1341 let tree_oid = de::string(&obj.field("tree_oid"), obj.take("tree_oid")?)?;
1342 obj.finish()?;
1343 TreeIdentity::new(object_format, tree_oid)
1344 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
1345}
1346
1347fn decode_reason(path: &str, value: Value) -> Result<String, Error> {
1348 let raw = de::string(path, value)?;
1349 let length = raw.chars().count();
1350 if (1..=1024).contains(&length) && raw.chars().any(|c| !c.is_whitespace()) {
1351 Ok(raw)
1352 } else {
1353 fail(path, ErrorKind::InvalidValue)
1354 }
1355}
1356
1357fn decode_intent(path: &str, value: Value) -> Result<TargetIntent, Error> {
1358 let mut obj = Obj::new(path, value)?;
1359 de::const_str(&obj.field("kind"), obj.take("kind")?, "repository-path")?;
1360 let target_path = decode_repo_path(&obj.field("path"), obj.take("path")?)?;
1361 let target_kind = TargetKind::decode(&obj.field("target_kind"), obj.take("target_kind")?)?;
1362 let query_digest =
1363 decode_nullable_digest(&obj.field("query_digest"), obj.take("query_digest")?)?;
1364 let fragment_digest =
1365 decode_nullable_digest(&obj.field("fragment_digest"), obj.take("fragment_digest")?)?;
1366 obj.finish()?;
1367 Ok(TargetIntent {
1368 path: target_path,
1369 target_kind,
1370 query_digest,
1371 fragment_digest,
1372 })
1373}
1374
1375fn decode_scope(path: &str, value: Value) -> Result<FindingScope, Error> {
1376 let mut obj = Obj::new(path, value)?;
1377 de::const_str(&obj.field("kind"), obj.take("kind")?, "reference")?;
1378 let document = decode_repo_path(&obj.field("document"), obj.take("document")?)?;
1379 let source_construct = SourceConstruct::decode(
1380 &obj.field("source_construct"),
1381 obj.take("source_construct")?,
1382 )?;
1383 let normalized_target_intent = decode_intent(
1384 &obj.field("normalized_target_intent"),
1385 obj.take("normalized_target_intent")?,
1386 )?;
1387 let occurrence_path = obj.field("occurrence");
1388 let mut occurrence = Obj::new(&occurrence_path, obj.take("occurrence")?)?;
1389 de::const_str(
1390 &occurrence.field("kind"),
1391 occurrence.take("kind")?,
1392 "source-projection",
1393 )?;
1394 let source_projection_digest = decode_digest(
1395 &occurrence.field("source_projection_digest"),
1396 occurrence.take("source_projection_digest")?,
1397 )?;
1398 occurrence.finish()?;
1399 obj.finish()?;
1400 Ok(FindingScope {
1401 document,
1402 source_construct,
1403 normalized_target_intent,
1404 source_projection_digest,
1405 })
1406}
1407
1408fn decode_key_input(path: &str, value: Value) -> Result<(FindingKeyInput, Digest), Error> {
1409 let digest = hj(FINDING_KEY_DOMAIN, &value);
1410 let mut obj = Obj::new(path, value)?;
1411 de::const_str(
1412 &obj.field("schema"),
1413 obj.take("schema")?,
1414 FINDING_KEY_INPUT_SCHEMA,
1415 )?;
1416 let finding_kind =
1417 EligibleFindingKind::decode(&obj.field("finding_kind"), obj.take("finding_kind")?)?;
1418 let scope = decode_scope(&obj.field("scope"), obj.take("scope")?)?;
1419 obj.finish()?;
1420 Ok((
1421 FindingKeyInput {
1422 finding_kind,
1423 scope,
1424 },
1425 digest,
1426 ))
1427}
1428
1429fn decode_resolution(path: &str, value: Value) -> Result<Resolution, Error> {
1430 let mut obj = Obj::new(path, value)?;
1431 let status_path = obj.field("status");
1432 let status = de::string(&status_path, obj.take("status")?)?;
1433 let code = de::string(&obj.field("code"), obj.take("code")?)?;
1434 let kind = match (status.as_str(), code.as_str()) {
1435 ("missing", "path-not-found") => ResolutionKind::Missing,
1436 ("type-mismatch", "target-type-mismatch") => ResolutionKind::TypeMismatch,
1437 _ => return fail(&status_path, ErrorKind::Inconsistent),
1438 };
1439 let res_path = de::nullable(obj.take("path")?)
1440 .map(|v| decode_repo_path(&obj.field("path"), v))
1441 .transpose()?;
1442 let entry_kind = de::nullable(obj.take("entry_kind")?)
1443 .map(|v| EntryKind::decode(&obj.field("entry_kind"), v))
1444 .transpose()?;
1445 let git_mode = de::nullable(obj.take("git_mode")?)
1446 .map(|v| GitMode::decode(&obj.field("git_mode"), v))
1447 .transpose()?;
1448 let raw_digest = decode_nullable_digest(&obj.field("raw_digest"), obj.take("raw_digest")?)?;
1449 let projection_digest = decode_nullable_digest(
1450 &obj.field("projection_digest"),
1451 obj.take("projection_digest")?,
1452 )?;
1453 let content_availability = ContentAvailability::decode(
1454 &obj.field("content_availability"),
1455 obj.take("content_availability")?,
1456 )?;
1457 obj.finish()?;
1458
1459 let shape_ok = match kind {
1460 ResolutionKind::Missing => {
1461 entry_kind.is_none()
1462 && git_mode.is_none()
1463 && raw_digest.is_none()
1464 && projection_digest.is_none()
1465 && content_availability == ContentAvailability::NotApplicable
1466 }
1467 ResolutionKind::TypeMismatch => entry_kind.is_some() && git_mode.is_some(),
1468 };
1469 if !shape_ok {
1470 return fail(path, ErrorKind::Inconsistent);
1471 }
1472 Ok(Resolution {
1473 kind,
1474 path: res_path,
1475 entry_kind,
1476 git_mode,
1477 raw_digest,
1478 projection_digest,
1479 content_availability,
1480 })
1481}
1482
1483fn decode_fact(path: &str, value: Value) -> Result<(Fact, Digest), Error> {
1484 let digest = hj(FACT_DOMAIN, &value);
1485 let mut obj = Obj::new(path, value)?;
1486 de::const_str(&obj.field("schema"), obj.take("schema")?, FACT_SCHEMA)?;
1487 let finding_kind =
1488 EligibleFindingKind::decode(&obj.field("finding_kind"), obj.take("finding_kind")?)?;
1489 let key_path = obj.field("key_input");
1490 let (key_input, _) = decode_key_input(&key_path, obj.take("key_input")?)?;
1491 let evidence_path = obj.field("evidence");
1492 let mut evidence = Obj::new(&evidence_path, obj.take("evidence")?)?;
1493 de::const_str(&evidence.field("kind"), evidence.take("kind")?, "reference")?;
1494 let resolution =
1495 decode_resolution(&evidence.field("resolution"), evidence.take("resolution")?)?;
1496 let multiplicity_path = evidence.field("occurrence_multiplicity");
1497 if de::integer(
1498 &multiplicity_path,
1499 evidence.take("occurrence_multiplicity")?,
1500 )? != 1
1501 {
1502 return fail(&multiplicity_path, ErrorKind::InvalidValue);
1503 }
1504 evidence.finish()?;
1505 obj.finish()?;
1506
1507 let kind_matches = match resolution.kind {
1508 ResolutionKind::Missing => finding_kind == EligibleFindingKind::ExplicitTargetMissing,
1509 ResolutionKind::TypeMismatch => {
1510 finding_kind == EligibleFindingKind::ExplicitTargetTypeMismatch
1511 }
1512 };
1513 if !kind_matches || key_input.finding_kind != finding_kind {
1514 return fail(path, ErrorKind::Inconsistent);
1515 }
1516 Ok((
1517 Fact {
1518 finding_kind,
1519 key_input,
1520 resolution,
1521 },
1522 digest,
1523 ))
1524}
1525
1526struct ItemCore {
1527 finding_kind: EligibleFindingKind,
1528 key_input: FindingKeyInput,
1529 finding_key: Digest,
1530 fact: Fact,
1531 fact_digest: Digest,
1532 owner: OwnerId,
1533 reason: String,
1534 created_at: UtcInstant,
1535 expires_at: UtcInstant,
1536}
1537
1538fn decode_item_core(path: &str, obj: &mut Obj, fact_field: &str) -> Result<ItemCore, Error> {
1539 let finding_kind =
1540 EligibleFindingKind::decode(&obj.field("finding_kind"), obj.take("finding_kind")?)?;
1541 let key_path = obj.field("key_input");
1542 let (key_input, computed_key) = decode_key_input(&key_path, obj.take("key_input")?)?;
1543 let finding_key_path = obj.field("finding_key");
1544 let finding_key = decode_digest(&finding_key_path, obj.take("finding_key")?)?;
1545 if finding_key != computed_key {
1546 return fail(&finding_key_path, ErrorKind::DigestMismatch);
1547 }
1548 let fact_path = obj.field(fact_field);
1549 let (fact, computed_fact) = decode_fact(&fact_path, obj.take(fact_field)?)?;
1550 let fact_digest_field = format!("{fact_field}_digest");
1551 let fact_digest_path = obj.field(&fact_digest_field);
1552 let fact_digest = decode_digest(&fact_digest_path, obj.take(&fact_digest_field)?)?;
1553 if fact_digest != computed_fact {
1554 return fail(&fact_digest_path, ErrorKind::DigestMismatch);
1555 }
1556 if key_input.finding_kind != finding_kind || fact.key_input != key_input {
1557 return fail(path, ErrorKind::Inconsistent);
1558 }
1559 let owner = decode_owner(&obj.field("owner"), obj.take("owner")?)?;
1560 let reason = decode_reason(&obj.field("reason"), obj.take("reason")?)?;
1561 let created_at = decode_instant(&obj.field("created_at"), obj.take("created_at")?)?;
1562 let expires_at = decode_instant(&obj.field("expires_at"), obj.take("expires_at")?)?;
1563 Ok(ItemCore {
1564 finding_kind,
1565 key_input,
1566 finding_key,
1567 fact,
1568 fact_digest,
1569 owner,
1570 reason,
1571 created_at,
1572 expires_at,
1573 })
1574}
1575
1576fn decode_debt_item(path: &str, value: Value) -> Result<DebtItem, Error> {
1577 let mut obj = Obj::new(path, value)?;
1578 let debt_id = decode_artifact_id(&obj.field("debt_id"), obj.take("debt_id")?)?;
1579 let core = decode_item_core(path, &mut obj, "accepted_fact")?;
1580 obj.finish()?;
1581 if core.created_at >= core.expires_at {
1582 return fail(path, ErrorKind::Inconsistent);
1583 }
1584 Ok(DebtItem {
1585 debt_id,
1586 finding_kind: core.finding_kind,
1587 key_input: core.key_input,
1588 finding_key: core.finding_key,
1589 accepted_fact: core.fact,
1590 accepted_fact_digest: core.fact_digest,
1591 owner: core.owner,
1592 reason: core.reason,
1593 created_at: core.created_at,
1594 expires_at: core.expires_at,
1595 })
1596}
1597
1598fn decode_waiver_item(path: &str, value: Value) -> Result<WaiverItem, Error> {
1599 let mut obj = Obj::new(path, value)?;
1600 let waiver_id = decode_artifact_id(&obj.field("waiver_id"), obj.take("waiver_id")?)?;
1601 let core = decode_item_core(path, &mut obj, "authorized_fact")?;
1602 let candidate_tree = decode_tree(&obj.field("candidate_tree"), obj.take("candidate_tree")?)?;
1603 let issuer = decode_owner(&obj.field("issuer"), obj.take("issuer")?)?;
1604 let not_before = decode_instant(&obj.field("not_before"), obj.take("not_before")?)?;
1605 de::const_str(
1606 &obj.field("residual_disposition"),
1607 obj.take("residual_disposition")?,
1608 "warn",
1609 )?;
1610 obj.finish()?;
1611 if core.created_at > not_before || not_before >= core.expires_at {
1612 return fail(path, ErrorKind::Inconsistent);
1613 }
1614 Ok(WaiverItem {
1615 waiver_id,
1616 finding_kind: core.finding_kind,
1617 key_input: core.key_input,
1618 finding_key: core.finding_key,
1619 authorized_fact: core.fact,
1620 authorized_fact_digest: core.fact_digest,
1621 candidate_tree,
1622 owner: core.owner,
1623 issuer,
1624 reason: core.reason,
1625 created_at: core.created_at,
1626 not_before,
1627 expires_at: core.expires_at,
1628 })
1629}