Skip to main content

amiss_wire/
controls.rs

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
23/// The controller's maximum statement lifetime: `evaluation_instant <
24/// valid_until <= evaluation_instant + 600` whole seconds.
25pub 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    /// # Errors
79    ///
80    /// A value outside the closed `observe`/`enforce` pair.
81    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    /// Whether the consuming syntax node is an image form, which fixes the
156    /// authored target kind.
157    #[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    /// The phase a resource crossing reports, from the closed partition.
369    #[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    /// # Errors
526    ///
527    /// Fails on strict-JSON defects, schema-shape violations, unknown fields,
528    /// invalid grammar values, and unsorted or duplicate set members.
529    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/// A floor rejection: a schema-layer defect, or the combined
597/// `organization-policy-entries` count crossing its effective limit.
598#[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    /// # Errors
617    ///
618    /// Fails on strict-JSON defects, schema-shape violations, unknown fields,
619    /// invalid grammar values, per-resource bound violations, unsorted or
620    /// duplicate set members, and a combined entry count over the built-in
621    /// `organization-policy-entries` limit or a tighter self-declared one.
622    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/// A trusted-time statement issued by the required-workflow clock inside the
729/// externally controlled run. Parsing establishes shape and the TTL law; the
730/// evaluation-side bindings (repository, ref, candidate identity, run,
731/// attempt) are separate verification.
732#[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    /// # Errors
746    ///
747    /// Fails on strict-JSON defects, schema-shape violations, invalid grammar
748    /// values, and a lifetime outside `0 < valid_until - evaluation_instant
749    /// <= 600` seconds.
750    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    /// # Errors
836    ///
837    /// A value outside the closed six-platform table.
838    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/// The externally protected allow-list entry for one scanner action tree,
852/// release manifest, bootstrap contract, and required provider status name.
853#[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    /// # Errors
893    ///
894    /// Fails on strict-JSON defects, schema-shape violations, and invalid
895    /// grammar values.
896    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    /// # Errors
1035    ///
1036    /// Fails on strict-JSON defects, schema-shape violations, embedded key or
1037    /// fact digests that do not recompute, kind or preimage inconsistencies,
1038    /// causal time-order violations, and unsorted or duplicate items or keys.
1039    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    /// # Errors
1121    ///
1122    /// Fails on strict-JSON defects, schema-shape violations, embedded key or
1123    /// fact digests that do not recompute, kind or preimage inconsistencies,
1124    /// causal time-order violations, duplicate waiver IDs, and duplicate
1125    /// `(candidate_tree, finding_key)` pairs.
1126    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
1189/// The one restricted-JSON root every control document parses through.
1190///
1191/// # Errors
1192///
1193/// Any strict-JSON defect, carried as `ErrorKind::Json`.
1194pub 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}