:py:mod:`slsa.provenance` ========================= .. py:module:: slsa.provenance .. autoapi-nested-parse:: SLSA provenance package. Implementing https://slsa.dev/spec/v1.0/provenance. Purpose ======= Describe how an artifact or set of artifacts was produced so that: - Consumers of the provenance can verify that the artifact was built according to expectations. - Others can rebuild the artifact, if desired. This predicate is the *RECOMMENDED* way to satisfy the `SLSA v1.0 provenance requirements `_. .. _SLSA: https://slsa.dev .. |ResourceDescriptor| replace:: :class:`ResourceDescriptor` .. |ResourceURI| replace:: :class:`ResourceURI` .. |SLSA| replace:: `SLSA`_ .. |bool| replace:: :class:`bool` .. |bytes| replace:: :class:`bytes` .. |datetime| replace:: :class:`~datetime.datetime` .. |dict| replace:: :class:`dict` .. |json.dumps| replace:: :func:`json.dumps()` .. |json.load| replace:: :func:`json.load()` .. |str| replace:: :class:`str` Module Contents --------------- Classes ~~~~~~~ .. autoapisummary:: slsa.provenance.Builder slsa.provenance.BuildMetadata slsa.provenance.Statement slsa.provenance.ResourceDescriptor slsa.provenance.Predicate slsa.provenance.TypeURI slsa.provenance.ResourceURI .. py:class:: Builder(build_id: TypeURI | str, builder_dependencies: list[ResourceDescriptor], version: dict[str, str]) Predicate run details builder object. The build platform, or builder for short, represents the transitive closure of all the entities that are, by necessity, `trusted `_ to faithfully run the build and record the provenance. This includes not only the software but the hardware and people involved in running the service. For example, a particular instance of `Tekton `_ could be a build platform, while Tekton itself is not. For more info, see `Build model `_. The |id| **MUST** reflect the trust base that consumers care about. How detailed to be is a judgement call. For example, GitHub Actions supports both GitHub-hosted runners and self-hosted runners. The GitHub-hosted runner might be a single identity because it's all GitHub from the consumer's perspective. Meanwhile, each self-hosted runner might have its own identity because not all runners are trusted by all consumers. Consumers MUST accept only specific signer-builder pairs. For example, ``GitHub`` can sign provenance for the ``GitHub Actions`` builder, and ``Google`` can sign provenance for the ``Google Cloud Build`` builder, but ``GitHub`` cannot sign for the ``Google Cloud Build`` builder. Design rationale ---------------- The builder is distinct from the signer in order to support the case where one signer generates attestations for more than one builder, as in the ``GitHub Actions`` example above. The field is **REQUIRED**, even if it is implicit from the signer, to aid readability and debugging. It is an object to allow additional fields in the future, in case one URI is not sufficient. .. |builder.as_dict| replace:: :meth:`~Builder.as_dict` .. |builder.as_json| replace:: :meth:`~Builder.as_json` .. |id| replace:: :attr:`~Builder.id` .. |builder.load_dict| replace:: :meth:`~Builder.load_dict` .. |builder.load_json| replace:: :meth:`~Builder.load_json` .. py:property:: builder_dependencies :type: list[ResourceDescriptor] Builder dependencies. Dependencies used by the orchestrator that are not run within the workload and that do not affect the build, but might affect the provenance generation or security guarantees. .. py:property:: id :type: TypeURI Build platform ID. URI indicating the transitive closure of the trusted build platform. This is intended to be the sole determiner of the SLSA Build level. If a build platform has multiple modes of operations that have differing security attributes or SLSA Build levels, each mode **MUST** have a different |id| and **SHOULD** have a different signer identity. This is to minimize the risk that a less secure mode compromises a more secure one. The |id| URI **SHOULD** resolve to documentation explaining: - The scope of what this ID represents. - The claimed SLSA Build level. - The accuracy and completeness guarantees of the fields in the provenance. - Any fields that are generated by the tenant-controlled build process and not verified by the trusted control plane, except for the ``subject``. - The interpretation of any extension fields. .. py:property:: version :type: dict[str, str] Builder version mapping. Map of names of components of the build platform to their version. .. py:attribute:: ATTR_BUILD_ID :type: str :value: 'id' .. py:attribute:: ATTR_BUILDER_DEPENDENCIES :type: str :value: 'builderDependencies' .. py:attribute:: ATTR_VERSION :type: str :value: 'version' .. py:attribute:: __hash__ .. py:method:: __eq__(other: object) -> bool Check if this builder object is equal to *other*. :param other: The builder object to compare this with. :return: A |bool| set to **True** if both builders are equal, **False** else. .. py:method:: as_dict() -> dict Get the dictionary representation of this builder. :return: The dictionary representation of this builder. This should be a valid JSON object (call to |json.load| succeeds). .. seealso:: |json.load| .. py:method:: as_json() -> str Get the representation of this builder as a JSON string. The dictionary representing this builder (as returned by |builder.as_dict|) is turned into a JSON string using |json.dumps| with *sort_keys* set to **True**. .. seealso:: |builder.as_dict|, |builder.load_json| .. py:method:: load_dict(initializer: dict[str, Any]) -> Builder :classmethod: Initialize a builder from a dictionary. :param initializer: The dictionary to initialize this builder with. :return: A builder object created from the input *initializer*. :raise ValueError: if the build ID is not defined in *initializer*. .. seealso:: |builder.as_dict| .. py:method:: load_json(initializer: str) -> Builder :classmethod: Initialize a builder from a JSON string. :param initializer: The JSON string to initialize this builder with. :return: A builder object created from the input *initializer*. :raise ValueError: if the build ID is not defined in *initializer*. .. seealso:: |builder.as_json|, |builder.load_dict| .. py:class:: BuildMetadata(invocation_id: str, started_on: datetime.datetime, finished_on: datetime.datetime) Build metadata representation. When the timestamp parameters (*started_on* or *finished_on*) are strings, the |TIMESTAMP_FORMAT| format is used to convert them to |datetime| classes using |strptime|. :param invocation_id: Identifier of this particular build invocation. :param started_on: The timestamp of this build invocation start time. :param finished_on: The timestamp of this build invocation finish time. .. |TIMESTAMP_FORMAT| replace:: :attr:`TIMESTAMP_FORMAT` .. |bm.as_dict| replace:: :meth:`~BuildMetadata.as_dict` .. |bm.as_json| replace:: :meth:`~BuildMetadata.as_json` .. |bm.load_dict| replace:: :meth:`~BuildMetadata.load_dict` .. |bm.load_json| replace:: :meth:`~BuildMetadata.load_json` .. |strptime| replace:: :meth:`~datetime.strptime` .. py:property:: finished_on :type: datetime.datetime The timestamp of when the build completed. .. py:property:: invocation_id :type: str Build invocation identifier. Identifies this particular build invocation, which can be useful for finding associated logs or other ad-hoc analysis. The exact meaning and format is defined by |builder.id|; by default it is treated as opaque and case-sensitive. The value **SHOULD** be globally unique. .. |builder.id| replace:: :attr:`Builder.id` .. py:property:: started_on :type: datetime.datetime The timestamp of when the build started. .. py:attribute:: ATTR_INVOCATION_ID :type: str :value: 'invocationId' .. py:attribute:: ATTR_STARTED_ON :type: str :value: 'startedOn' .. py:attribute:: ATTR_FINISHED_ON :type: str :value: 'finishedOn' .. py:attribute:: TIMESTAMP_FORMAT :type: str :value: '%Y-%m-%dT%H:%M:%SZ' Timestamp format used to read/write |datetime| structures to strings. .. py:attribute:: __hash__ .. py:method:: __eq__(other: object) -> bool Check if this build metadata object is equal to *other*. :param other: The build metadata object to compare this with. :return: A |bool| set to **True** if both build metadatas are equal, **False** else. .. py:method:: as_dict() -> dict Get the dictionary representation of this build metadata. :return: The dictionary representation of this build metadata. This should be a valid JSON object (call to |json.load| succeeds). .. seealso:: |bm.as_json|, |json.load|, |bm.load_dict| .. py:method:: as_json() -> str Get the representation of this build metadata as a JSON string. The dictionary representing this build metadata (as returned by |bm.as_dict|) is turned into a JSON string using |json.dumps| with *sort_keys* set to **True**. .. seealso:: |bm.as_dict|, |bm.load_json| .. py:method:: load_dict(initializer: dict[str, Any]) -> BuildMetadata :classmethod: Initialize a build metadata from a dictionary. :param initializer: The dictionary to initialize this build metadata with. :return: A build metadata object created from the input *initializer*. :raise ValueError: if the invocation ID is not defined in *initializer*, or if the timestamps are invalid. :raise TypeError: if the timestamps types are invalid. .. seealso:: |bm.as_dict|, |bm.load_json| .. py:method:: load_json(initializer: str) -> BuildMetadata :classmethod: Initialize a build metadata from a JSON string. :param initializer: The JSON string to initialize this build metadata with. :return: A build metadata object created from the input *initializer*. :raise ValueError: if the build ID is not defined in *initializer*. .. seealso:: |builder.as_json|, |builder.load_dict| .. py:method:: __validate_timestamp(timestamp: datetime.datetime | str) -> datetime.datetime :staticmethod: Validate a timestamp. :param timestamp: The timestamp to validate .. py:class:: Statement(statement_type: TypeURI | str, subject: list[ResourceDescriptor], predicate_type: TypeURI | str = PREDICATE_TYPE_VALUE, predicate: Predicate | None = None) SLSA statement object. The Statement is the middle layer of the attestation, binding it to a particular subject and unambiguously identifying the types of the |Predicate|. :param statement_type: The URI identifier for the schema of the Statement. :param subject: Set of software artifacts that the attestation applies to. :param predicate_type: URI identifying the type of *predicate*. :param predicate: Additional parameters of the |Predicate|. .. |Predicate| replace:: :class:`Predicate` .. |predicateType| replace:: :attr:`~Predicate.predicatType` .. |st.as_dict| replace:: :meth:`~Statement.as_dict` .. |st.as_json| replace:: :meth:`~Statement.as_json` .. |st.load_dict| replace:: :meth:`~Statement.load_dict` .. |st.load_json| replace:: :meth:`~Statement.load_json` .. py:property:: type :type: TypeURI Identifier for the schema of the Statement. Always https://in-toto.io/Statement/v1 for this version of the spec. .. py:property:: predicate :type: Predicate | None Additional parameters of the |Predicate|. Unset is treated the same as set-but-empty. **MAY** be omitted if |predicateType| fully describes the predicate. .. py:property:: predicate_type :type: TypeURI URI identifying the type of the |Predicate|. .. py:property:: subject :type: list[ResourceDescriptor] Set of software artifacts that the attestation applies to. Each element represents a single software artifact. Each element **MUST** have digest set. The name field may be used as an identifier to distinguish this artifact from others within the subject. Similarly, other |ResourceDescriptor| fields may be used as required by the context. The semantics are up to the producer and consumer and they **MAY** use them when evaluating policy. If the name is not meaningful, leave the field unset or use ``_``. For example, a SLSA Provenance attestation might use the name to specify output filename, expecting the consumer to only consider entries with a particular name. Alternatively, a vulnerability scan attestation might leave name unset because the results apply regardless of what the artifact is named. If set, name and uri **SHOULD** be unique within subject. .. warning:: Subject artifacts are matched purely by digest, regardless of content type. If this matters to you, please comment on `GitHub Issue #28 `_ .. py:attribute:: ATTR_PREDICATE :type: str :value: 'predicate' .. py:attribute:: ATTR_PREDICATE_TYPE :type: str :value: 'predicateType' .. py:attribute:: ATTR_SUBJECT :type: str :value: 'subject' .. py:attribute:: ATTR_TYPE :type: str :value: '_type' .. py:attribute:: SCHEMA_TYPE_VALUE :type: str :value: 'https://in-toto.io/Statement/v1' .. py:attribute:: PREDICATE_TYPE_VALUE :type: str :value: 'https://slsa.dev/provenance/v1' .. py:attribute:: __hash__ .. py:method:: __eq__(other: object) -> bool Check if this statement is equal to *other*. :param other: The statement object to compare this with. :return: A |bool| set to **True** if both statements are equal, **False** else. .. py:method:: as_dict() -> dict Get the dictionary representation of this statement. :return: The dictionary representation of this statement. This should be a valid JSON object (call to |json.load| succeeds). .. seealso:: |json.load| .. py:method:: as_json() -> str Get the representation of this statement as a JSON string. The dictionary representing this statement (as returned by |st.as_dict|) is turned into a JSON string using |json.dumps| with *sort_keys* set to **True**. .. seealso:: |st.as_dict|, |st.load_json| .. py:method:: load_dict(initializer: dict[str, Any]) -> Statement :classmethod: Initialize a statement from a dictionary. :param initializer: The dictionary to initialize this statement with. :return: A statement object created from the input *initializer*. :raise ValueError: if the statement type is not defined in *initializer*. .. seealso:: |st.as_dict|, |st.load_json| .. py:method:: load_json(initializer: str) -> Statement :classmethod: Initialize a statement from a JSON string. :param initializer: The JSON string to initialize this statement with. :return: A statement object created from the input *initializer*. :raise ValueError: if the statement type is not defined in *initializer*. .. seealso:: |st.as_json|, |st.load_dict| .. py:class:: ResourceDescriptor(uri: ResourceURI | str | None = None, digest: dict[str, str] | None = None, name: str | None = None, download_location: ResourceURI | str | None = None, media_type: str | None = None, content: bytes | None = None, resource_annotations: dict[str, Any] | None = None) Resource descriptor object. A size-efficient description of any software artifact or resource (mutable or immutable). Though all fields are optional, a |ResourceDescriptor| **MUST** specify one of |uri|, |digest| or |content| at a minimum. Further, a context that uses the |ResourceDescriptor| can require one or more fields. For example, a predicate **MAY** require the name and digest fields. :param uri: see |uri| :param digest: see |digest| :param name: see |name| :param download_location: see |download_location| :param media_type: see |media_type| :param content: see |content| :param resource_annotations: see |annotations| .. note:: Those requirements cannot override the minimum requirement of one of |uri|, |digest|, or |content| specified here. .. |annotations| replace:: :attr:`~ResourceDescriptor.annotations` .. |rd.as_dict| replace:: :meth:`~ResourceDescriptor.as_dict` .. |rd.as_json| replace:: :meth:`~ResourceDescriptor.as_json` .. |content| replace:: :attr:`~ResourceDescriptor.content` .. |digest| replace:: :attr:`~ResourceDescriptor.digest` .. |download_location| replace:: :attr:`~ResourceDescriptor.download_location` .. |is_valid| replace:: :attr:`~ResourceDescriptor.is_valid` .. |rd.load_dict| replace:: :meth:`~ResourceDescriptor.load_dict` .. |rd.load_json| replace:: :meth:`~ResourceDescriptor.load_json` .. |media_type| replace:: :attr:`~ResourceDescriptor.media_type` .. |name| replace:: :attr:`~ResourceDescriptor.name` .. |uri| replace:: :attr:`~ResourceDescriptor.uri` .. py:property:: annotations :type: dict[str, Any] Resource descriptor additional information. This field MAY be used to provide additional information or metadata about the resource or artifact that may be useful to the consumer when evaluating the attestation against a policy. For maximum flexibility annotations may be any mapping from a field name to any JSON value (string, number, object, array, boolean or null). The producer and consumer **SHOULD** agree on the semantics, and acceptable fields and values in the annotations map. Producers **SHOULD** follow the same naming conventions for annotation fields as for extension fields. .. py:property:: content :type: bytes | None The contents of the resource or artifact. This field is **REQUIRED** unless either |uri| or |digest| is set. The producer **MAY** use this field in scenarios where including the contents of the resource/artifact directly in the attestation is deemed more efficient for consumers than providing a pointer to another location. To maintain size efficiency, the size of content **SHOULD** be less than 1KB. The semantics are up to the producer and consumer. The |uri| or |media_type| **MAY** be used by the producer as hints for how consumers should parse content. :raise TypeError: When setting this field to something else than a |bytes| or ``None``. .. py:property:: digest :type: dict[str, str] A set of digests for this resource descriptor. A set of cryptographic digests of the contents of the resource or artifact. This field is **REQUIRED** unless either |uri|, or |content| is set. When known, the producer **SHOULD** set this field to denote an immutable artifact or resource. The producer and consumer **SHOULD** agree on acceptable algorithms. :raise TypeError: When setting this field to something else than a |dict|. .. py:property:: download_location :type: ResourceURI | None Artifact download location. The location of the described resource or artifact, if different from the |uri|. To enable automated downloads by consumers, the specified location **SHOULD** be resolvable. :raise TypeError: When setting this field to something else than a |ResourceURI| or ``None``. .. py:property:: is_valid :type: bool Check if this resource descriptor is valid. To be valid, a resource descriptor should define at least one of the following: - |content| - |digest| - |uri| :return: A |bool| set to **True** if at least one of the above-mentioned field is defined, **False** else. .. py:property:: name :type: str | None Machine-readable identifier for distinguishing between descriptors. The semantics are up to the producer and consumer. The |name| name **SHOULD** be stable, such as a filename, to allow consumers to reliably use the |name| as part of their policy. .. py:property:: media_type :type: str | None Resource descriptor media type. The `MIME Type `_ (i.e., media type) of the described resource or artifact. For resources or artifacts that do not have a standardized MIME type, producers **SHOULD** follow `RFC 6838 (Sections 3.2-3.4) `_ conventions of prefixing types with ``x.``, ``prs.``, or ``vnd.`` to avoid collisions with other producers. .. py:property:: uri :type: ResourceURI | None A URI used to identify the resource or artifact globally. This field is **REQUIRED** unless either digest or content is set. .. py:attribute:: ATTR_ANNOTATIONS :type: str :value: 'annotations' .. py:attribute:: ATTR_CONTENT :type: str :value: 'content' .. py:attribute:: ATTR_DIGEST :type: str :value: 'digest' .. py:attribute:: ATTR_DOWNLOAD_LOCATION :type: str :value: 'downloadLocation' .. py:attribute:: ATTR_MEDIA_TYPE :type: str :value: 'mediaType' .. py:attribute:: ATTR_NAME :type: str :value: 'name' .. py:attribute:: ATTR_URI :type: str :value: 'uri' .. py:attribute:: ATTRIBUTES :type: tuple :value: () JSON attributes returned by the |rd.as_dict| method (if the given attribute defines a value). .. py:attribute:: __hash__ .. py:method:: __eq__(other: object) -> bool Check if this resource descriptor is equal to *other*. :param other: The resource descriptor object to compare this with. :return: A |bool| set to **True** if both resource descriptors are equal, **False** else. .. py:method:: add_digest(algorithm: str, digest: str) -> None Add a new digest to the digest set. :param algorithm: The algorithm the new digest has been computed with. :param digest: The new digest to add to the digest set. :raise KeyError: if *algorithm* already defines a digest in the current digest set. .. py:method:: as_dict() -> dict Get the dictionary representation of this resource descriptor. :return: The dictionary representation of this resource descriptor. This should be a valid JSON object. :raise ValueError: If this resource descriptor is not valid (see |is_valid|). .. seealso:: |rd.as_json|, |is_valid|, |rd.load_dict| .. py:method:: as_json() -> str Get the representation of this resource descriptor as a JSON string. The dictionary representing this resource descriptor (as returned by |rd.as_dict|) is turned into a JSON string using |json.dumps| with *sort_keys* set to **True**. .. seealso:: |rd.as_dict|, |rd.load_json| .. py:method:: dir_hash(path: pathlib.Path, algorithm: str) -> str :staticmethod: Directory hash. :param path: The path to the directory to hash :param algorithm: The hashing algorithm to use The `directory Hash1 `_ function, omitting the ``h1:`` prefix and output in lowercase hexadecimal instead of base64. This algorithm was designed for go modules but can be used to digest the contents of an arbitrary archive or file tree. Equivalent to extracting the archive to an empty directory and running the following command in that directory:: find . -type f | cut -c3- | LC_ALL=C sort | xargs -r sha256sum \\ | sha256sum | cut -f1 -d' ' For example, the module dirhash ``h1:Khu2En+0gcYPZ2kuIihfswbzxv/mIHXgzPZ018Oty48=`` would be encoded as ``{"dirHash1": "2a1bb6127fb481c60f67692e22285fb306f3c6ffe62075e0ccf674d7c3adcb8f"}``. .. py:method:: load_dict(initializer: dict[str, Any]) -> ResourceDescriptor :classmethod: Initialize a resource descriptor from a dictionary. :param initializer: The dictionary to initialize this resource descriptor with. :return: A resource descriptor object created from the input *initializer*. :raise ValueError: If this resource descriptor is not valid once initialized with the dictionary content (see |is_valid|). .. seealso:: |rd.as_dict|, |is_valid| .. py:method:: load_json(initializer: str) -> ResourceDescriptor :classmethod: Initialize a resource descriptor from a JSON string. :param initializer: The JSON string to initialize this resource descriptor with. :return: A resource descriptor object created from the input *initializer*. :raise ValueError: If this resource descriptor is not valid once initialized with the dictionary content (see |is_valid|). .. seealso:: |rd.load_dict|, |is_valid| .. py:class:: Predicate(build_definition: Predicate, run_details: Predicate) Predicate object. .. |p.as_dict| replace:: :meth:`as_dict` .. |p.as_json| replace:: :meth:`as_json` .. |p.load_dict| replace:: :meth:`load_dict` .. |p.load_json| replace:: :meth:`load_json` .. py:class:: BuildDefinition(build_type: TypeURI | str, external_parameters: object, internal_parameters: object, resolved_dependencies: list[ResourceDescriptor]) The BuildDefinition describes all the inputs to the build. It **SHOULD** contain all the information necessary and sufficient to initialize the build and begin execution. The |externalParameters| and |internalParameters| are the top-level inputs to the template, meaning inputs not derived from another input. Each is an arbitrary JSON object, though it is **RECOMMENDED** to keep the structure simple with string values to aid verification. The same field name **SHOULD NOT** be used for both |externalParameters| and |internalParameters|. The parameters **SHOULD** only contain the actual values passed in through the interface to the build platform. Metadata about those parameter values, particularly digests of artifacts referenced by those parameters, **SHOULD** instead go in |resolvedDependencies|. The documentation for |buildType| **SHOULD** explain how to convert from a parameter to the dependency uri. For example:: "externalParameters": { "repository": "https://github.com/octocat/hello-world", "ref": "refs/heads/main" }, "resolvedDependencies": [{ "uri": "git+https://github.com/octocat/hello-world@refs/heads/main", "digest": {"gitCommit": "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d"} }] Guidelines ---------- - Maximize the amount of information that is implicit from the meaning of |buildType|. In particular, any value that is boilerplate and the same for every build **SHOULD** be implicit. - Reduce parameters by moving configuration to input artifacts whenever possible. For example, instead of passing in compiler flags via an external parameter that has to be verified separately, require the flags to live next to the source code or build configuration so that verifying the latter automatically verifies the compiler flags. - In some cases, additional external parameters might exist that do not impact the behavior of the build, such as a deadline or priority. These extra parameters **SHOULD** be excluded from the provenance after careful analysis that they indeed pose no security impact. - If possible, architect the build platform to use this definition as its sole top-level input, in order to guarantee that the information is sufficient to run the build. - When build configuration is evaluated client-side before being sent to the server, such as transforming version-controlled YAML into ephemeral JSON, some solution is needed to make verification practical. Consumers need a way to know what configuration is expected and the usual way to do that is to map it back to version control, but that is not possible if the server cannot verify the configuration's origins. Possible solutions: - (**RECOMMENDED**) Rearchitect the build platform to read configuration directly from version control, recording the server-verified URI in |externalParameters| and the digest in |resolvedDependencies|. - Record the digest in the provenance and use a separate provenance attestation to link that digest back to version control. In this solution, the client-side evaluation is considered a separate *build* that **SHOULD** be independently secured using |SLSA|, though securing it can be difficult since it usually runs on an untrusted workstation. - The purpose of |resolvedDependencies| is to facilitate recursive analysis of the software supply chain. Where practical, it is valuable to record the URI and digest of artifacts that, if compromised, could impact the build. At |SLSA| Build L3, completeness is considered *best effort*. .. |buildType| replace:: :attr:`build_type` .. |bd.as_dict| replace:: :meth:`as_dict` .. |bd.as_json| replace:: :meth:`as_json` .. |externalParameters| replace:: :attr:`external_parameters` .. |internalParameters| replace:: :attr:`internal_parameters` .. |bd.load_dict| replace:: :meth:`load_dict` .. |bd.load_json| replace:: :meth:`load_json` .. |resolvedDependencies| replace:: :attr:`resolved_dependencies` .. py:property:: build_type :type: TypeURI | None Predicate build type. Identifies the template for how to perform the build and interpret the parameters and dependencies. The URI **SHOULD** resolve to a human-readable specification that includes: - overall description of the build type - schema for externalParameters and internalParameters - unambiguous instructions for how to initiate the build given this BuildDefinition, and a complete example. Example: ------- :: https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1 .. py:property:: external_parameters :type: object | None The parameters that are under external control. Such as those set by a user or tenant of the build platform. They **MUST** be complete at SLSA Build L3, meaning that there is no additional mechanism for an external party to influence the build. (At lower SLSA Build levels, the completeness **MAY** be best effort.) The build platform **SHOULD** be designed to minimize the size and complexity of ``externalParameters``, in order to reduce fragility and ease verification. Consumers **SHOULD** have an expectation of what **good** looks like; the more information that they need to check, the harder that task becomes. Verifiers **SHOULD** reject unrecognized or unexpected fields within ``externalParameters``. .. py:property:: internal_parameters :type: object | None Internal parameters. The parameters that are under the control of the entity represented by ``builder.id``. The primary intention of this field is for debugging, incident response, and vulnerability management. The values here **MAY** be necessary for reproducing the build. There is no need to verify these parameters because the build platform is already trusted, and in many cases it is not practical to do so. .. py:property:: resolved_dependencies :type: list[ResourceDescriptor] Unordered collection of artifacts needed at build time. Completeness is best effort, at least through SLSA Build L3. For example, if the build script fetches and executes ``example.com/foo.sh``, which in turn fetches ``example.com/bar.tar.gz``, then both ``foo.sh`` and ``bar.tar.gz`` **SHOULD** be listed here. .. py:attribute:: ATTR_BUILD_TYPE :type: str :value: 'buildType' .. py:attribute:: ATTR_EXTERNAL_PARAMETERS :type: str :value: 'externalParameters' .. py:attribute:: ATTR_INTERNAL_PARAMETERS :type: str :value: 'internalParameters' .. py:attribute:: ATTR_RESOLVED_DEPENDENCIES :type: str :value: 'resolvedDependencies' .. py:attribute:: __hash__ .. py:method:: __eq__(other: object) -> bool Check if this build definition is equal to *other*. :param other: The build definition object to compare this with. :return: A |bool| set to **True** if both build definitions are equal, **False** else. .. py:method:: as_dict() -> dict Get the dictionary representation of this build definition. :return: The dictionary representation of this build definition. This should be a valid JSON object (call to |json.load| succeeds). .. seealso:: |bd.as_json|, |json.load|, |bd.load_dict| .. py:method:: as_json() -> str Get the representation of this build definition as a JSON string. The dictionary representing this build defintion (as returned by |bd.as_dict|) is turned into a JSON string using |json.dumps| with *sort_keys* set to **True**. .. seealso:: |bd.as_dict|, |bd.load_json| .. py:method:: load_dict(initializer: dict[str, Any]) -> Predicate :classmethod: Initialize a build definition from a dictionary. :param initializer: The dictionary to initialize this build definition with. :return: A build definition object created from the input *initializer*. .. seealso:: |bd.as_dict|, |bd.load_json| .. py:method:: load_json(initializer: str) -> Predicate :classmethod: Initialize a build definition from a JSON string. :param initializer: The JSON string to initialize this build definition with. :return: A build definition object created from the input *initializer*. .. seealso:: |bd.as_json|, |bd.load_dict| .. py:class:: RunDetails(builder: Builder, metadata: BuildMetadata, by_products: list[ResourceDescriptor]) Details specific to this particular execution of the build. :param builder: Run details builder description. :param metadata: The metadata for this run details object. :param by_products: Run details additional artifacts. .. |rund.as_dict| replace:: :meth:`as_dict` .. |rund.as_json| replace:: :meth:`as_json` .. |rund.load_dict| replace:: :meth:`load_dict` .. |rund.load_json| replace:: :meth:`load_json` .. py:property:: builder :type: Builder Run details builder. Identifies the build platform that executed the invocation, which is trusted to have correctly performed the operation and populated this provenance. .. py:property:: by_products :type: list[ResourceDescriptor] Run details additional artifacts. Additional artifacts generated during the build that are not considered the **output** of the build but that might be needed during debugging or incident response. For example, this might reference logs generated during the build and/or a digest of the fully evaluated build configuration. In most cases, this **SHOULD NOT** contain all intermediate files generated during the build. Instead, this **SHOULD** only contain files that are likely to be useful later and that cannot be easily reproduced. .. py:property:: metadata :type: BuildMetadata Run details build metadata. Metadata about this particular execution of the build. .. py:attribute:: ATTR_BUILDER :type: str :value: 'builder' .. py:attribute:: ATTR_METADATA :type: str :value: 'metadata' .. py:attribute:: ATTR_BY_PRODUCTS :type: str :value: 'byproducts' .. py:attribute:: __hash__ .. py:method:: __eq__(other: object) -> bool Check if this run details object is equal to *other*. :param other: The run details object to compare this with. :return: A |bool| set to **True** if both run details are equal, **False** else. .. py:method:: as_dict() -> dict Get the dictionary representation of this run details. :return: The dictionary representation of this run details. This should be a valid JSON object (call to |json.load| succeeds). .. seealso:: |rund.as_json|, |json.load|, |rund.load_dict| .. py:method:: as_json() -> str Get the representation of this run details as a JSON string. The dictionary representing this run details (as returned by |rund.as_dict|) is turned into a JSON string using |json.dumps| with *sort_keys* set to **True**. .. seealso:: |rund.as_dict|, |rund.load_json| .. py:method:: load_dict(initializer: dict[str, Any]) -> Predicate :classmethod: Initialize a run details from a dictionary. :param initializer: The dictionary to initialize this run details with. :return: A run details object created from the input *initializer*. .. seealso:: |rund.as_dict|, |rund.load_json| .. py:method:: load_json(initializer: str) -> Predicate :classmethod: Initialize a run details from a JSON string. :param initializer: The JSON string to initialize this run details with. :return: A run details object created from the input *initializer*. .. seealso:: |rund.as_json|, |rund.load_dict| .. py:property:: build_definition :type: Predicate The input to the build. The accuracy and completeness are implied by runDetails.builder.id. .. py:property:: run_details :type: Predicate Details specific to this particular execution of the build. .. py:attribute:: ATTR_BUILD_DEFINITION :type: str :value: 'buildDefinition' .. py:attribute:: ATTR_RUN_DETAILS :type: str :value: 'runDetails' .. py:attribute:: __hash__ .. py:method:: __eq__(other: object) -> bool Check if this predicate is equal to *other*. :param other: The predicate object to compare this with. :return: A |bool| set to **True** if both predicates are equal, **False** else. .. py:method:: as_dict() -> dict Get the dictionary representation of this predicate. :return: The dictionary representation of this statement. This should be a valid JSON object (call to |json.load| succeeds). .. seealso:: |p.as_json|, |json.load|, |p.load_dict| .. py:method:: as_json() -> str Get the representation of this predicate as a JSON string. The dictionary representing this predicate (as returned by |p.as_dict|) is turned into a JSON string using |json.dumps| with *sort_keys* set to **True**. .. seealso:: |p.as_dict|, |p.load_json| .. py:method:: load_dict(initializer: dict[str, Any]) -> Predicate :classmethod: Initialize a predicate from a dictionary. :param initializer: The dictionary to initialize this predicat with. :return: A predicate object created from the input *initializer*. .. seealso:: |p.as_dict|, |p.load_json| .. py:method:: load_json(initializer: str) -> Predicate :classmethod: Initialize a predicate from a JSON string. :param initializer: The JSON string to initialize this predicate with. :return: A predictae object created from the input *initializer*. .. seealso:: |p.as_json|, |p.load_dict| .. py:class:: TypeURI(uri: str) Uniform Resource Identifier as specified in RFC 3986. Used as a collision-resistant type identifier. Format ------ A TypeURI is represented as a case-sensitive string and **MUST** be case normalized as per section 6.2.2.1 of RFC 3986, meaning that the scheme and authority **MUST** be in lowercase. **SHOULD** resolve to a human-readable description, but **MAY** be unresolvable. **SHOULD** include a version number to allow for revisions. TypeURIs are not registered. The natural namespacing of URIs is sufficient to prevent collisions. Example: ------- :: https://in-toto.io/Statement/v1 .. py:property:: uri :type: str Actual URI of this TypeURI. :return: The actual URI of this TypeURI. .. py:method:: __eq__(other: object) -> bool Check if this type uri is equal to *other*. :param other: The object to compare this with .. py:method:: __hash__() -> int Return the hash of this type URI. :return: The hash value based on the URI string .. py:method:: __str__() -> str Return the string representation of this TypeURI. .. py:class:: ResourceURI(uri: str) Bases: :py:obj:`TypeURI` Uniform Resource Identifier as specified in RFC 3986. Used to identify and locate any resource, service, or software artifact. Format ------ A ResourceURI is represented as a case-sensitive string and **MUST** be case normalized as per section 6.2.2.1 of RFC 3986, meaning that the scheme and authority **MUST** be in lowercase. **SHOULD** resolve to the artifact, but **MAY** be unresolvable. It is **RECOMMENDED** to use `Package URL `_ (``pkg:``) or `SPDX Download Location `_ (e.g. ``git+https:``). Example: ------- :: pkg:deb/debian/stunnel@5.50-3?arch=amd64