From e7ef12a0aae5b70d0eb547dceceddd7f1cf1d116 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Fri, 27 Mar 2026 13:08:16 +0100 Subject: [PATCH 1/8] Add SLSA Provenance model classes Adds Jackson-annotated classes to generate SLSA Provenance annotations for Commons builds. --- pom.xml | 12 + .../models/slsa/v1_2/BuildDefinition.java | 168 +++++++++++++ .../build/models/slsa/v1_2/BuildMetadata.java | 136 +++++++++++ .../build/models/slsa/v1_2/Builder.java | 119 ++++++++++ .../build/models/slsa/v1_2/Provenance.java | 116 +++++++++ .../models/slsa/v1_2/ResourceDescriptor.java | 220 ++++++++++++++++++ .../build/models/slsa/v1_2/RunDetails.java | 132 +++++++++++ .../build/models/slsa/v1_2/Statement.java | 126 ++++++++++ .../build/models/slsa/v1_2/package-info.java | 120 ++++++++++ 9 files changed, 1149 insertions(+) create mode 100644 src/main/java/org/apache/commons/build/models/slsa/v1_2/BuildDefinition.java create mode 100644 src/main/java/org/apache/commons/build/models/slsa/v1_2/BuildMetadata.java create mode 100644 src/main/java/org/apache/commons/build/models/slsa/v1_2/Builder.java create mode 100644 src/main/java/org/apache/commons/build/models/slsa/v1_2/Provenance.java create mode 100644 src/main/java/org/apache/commons/build/models/slsa/v1_2/ResourceDescriptor.java create mode 100644 src/main/java/org/apache/commons/build/models/slsa/v1_2/RunDetails.java create mode 100644 src/main/java/org/apache/commons/build/models/slsa/v1_2/Statement.java create mode 100644 src/main/java/org/apache/commons/build/models/slsa/v1_2/package-info.java diff --git a/pom.xml b/pom.xml index 408d67a7a..408b7df5c 100644 --- a/pom.xml +++ b/pom.xml @@ -87,6 +87,8 @@ RC1 true scm:svn:https://dist.apache.org/repos/dist/dev/commons/${commons.componentid} + 2.21.1 + 2.21 3.9.12 org.apache.maven diff --git a/src/main/java/org/apache/commons/build/models/slsa/v1_2/BuildDefinition.java b/src/main/java/org/apache/commons/build/models/slsa/v1_2/BuildDefinition.java new file mode 100644 index 000000000..12bbab7b7 --- /dev/null +++ b/src/main/java/org/apache/commons/build/models/slsa/v1_2/BuildDefinition.java @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.build.models.slsa.v1_2; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Inputs that define the build: the build type, external and internal parameters, and resolved dependencies. + * + *

Specifies everything that influenced the build output. Together with {@link RunDetails}, it forms the complete + * {@link Provenance} record.

+ * + * @see SLSA v1.2 Specification + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class BuildDefinition { + + @JsonProperty("buildType") + private String buildType = "https://commons.apache.org/builds/0.1.0"; + + @JsonProperty("externalParameters") + private Map externalParameters; + + @JsonProperty("internalParameters") + private Map internalParameters; + + @JsonProperty("resolvedDependencies") + private List resolvedDependencies; + + /** Creates a new BuildDefinition instance with the default build type. */ + public BuildDefinition() {} + + /** + * Creates a new BuildDefinition with the given build type and external parameters. + * + * @param buildType URI indicating what type of build was performed + * @param externalParameters inputs passed to the build + */ + public BuildDefinition(String buildType, Map externalParameters) { + this.buildType = buildType; + this.externalParameters = externalParameters; + } + + /** + * Returns the URI indicating what type of build was performed. + * + *

Determines the meaning of {@code externalParameters} and {@code internalParameters}.

+ * + * @return the build type URI + */ + public String getBuildType() { + return buildType; + } + + /** + * Sets the URI indicating what type of build was performed. + * + * @param buildType the build type URI + */ + public void setBuildType(String buildType) { + this.buildType = buildType; + } + + /** + * Returns the inputs passed to the build, such as command-line arguments or environment variables. + * + * @return the external parameters map, or {@code null} if not set + */ + public Map getExternalParameters() { + return externalParameters; + } + + /** + * Sets the inputs passed to the build. + * + * @param externalParameters the external parameters map + */ + public void setExternalParameters(Map externalParameters) { + this.externalParameters = externalParameters; + } + + /** + * Returns the artifacts the build depends on, such as sources, dependencies, build tools, and base images, + * specified by URI and digest. + * + * @return the internal parameters map, or {@code null} if not set + */ + public Map getInternalParameters() { + return internalParameters; + } + + /** + * Sets the artifacts the build depends on. + * + * @param internalParameters the internal parameters map + */ + public void setInternalParameters(Map internalParameters) { + this.internalParameters = internalParameters; + } + + /** + * Returns the materials that influenced the build. + * + *

Considered incomplete unless resolved materials are present.

+ * + * @return the list of resolved dependencies, or {@code null} if not set + */ + public List getResolvedDependencies() { + return resolvedDependencies; + } + + /** + * Sets the materials that influenced the build. + * + * @param resolvedDependencies the list of resolved dependencies + */ + public void setResolvedDependencies(List resolvedDependencies) { + this.resolvedDependencies = resolvedDependencies; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BuildDefinition that = (BuildDefinition) o; + return Objects.equals(buildType, that.buildType) + && Objects.equals(externalParameters, that.externalParameters) + && Objects.equals(internalParameters, that.internalParameters) + && Objects.equals(resolvedDependencies, that.resolvedDependencies); + } + + @Override + public int hashCode() { + return Objects.hash(buildType, externalParameters, internalParameters, resolvedDependencies); + } + + @Override + public String toString() { + return "BuildDefinition{" + + "buildType='" + buildType + '\'' + + ", externalParameters=" + externalParameters + + ", internalParameters=" + internalParameters + + ", resolvedDependencies=" + resolvedDependencies + + '}'; + } +} diff --git a/src/main/java/org/apache/commons/build/models/slsa/v1_2/BuildMetadata.java b/src/main/java/org/apache/commons/build/models/slsa/v1_2/BuildMetadata.java new file mode 100644 index 000000000..591339542 --- /dev/null +++ b/src/main/java/org/apache/commons/build/models/slsa/v1_2/BuildMetadata.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.build.models.slsa.v1_2; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.time.OffsetDateTime; +import java.util.Objects; + +/** + * Metadata about a build invocation: its identifier and start and finish timestamps. + * + * @see SLSA v1.2 Specification + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class BuildMetadata { + + @JsonProperty("invocationId") + private String invocationId; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'") + @JsonProperty("startedOn") + private OffsetDateTime startedOn; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'") + @JsonProperty("finishedOn") + private OffsetDateTime finishedOn; + + /** Creates a new BuildMetadata instance. */ + public BuildMetadata() { + } + + /** + * Creates a new BuildMetadata instance with all fields set. + * + * @param invocationId identifier for this build invocation + * @param startedOn timestamp when the build started + * @param finishedOn timestamp when the build completed + */ + public BuildMetadata(String invocationId, OffsetDateTime startedOn, OffsetDateTime finishedOn) { + this.invocationId = invocationId; + this.startedOn = startedOn; + this.finishedOn = finishedOn; + } + + /** + * Returns the identifier for this build invocation. + * + *

Useful for finding associated logs or other ad-hoc analysis. The exact meaning and format is defined by the + * builder and is treated as opaque and case-sensitive. The value SHOULD be globally unique.

+ * + * @return the invocation identifier, or {@code null} if not set + */ + public String getInvocationId() { + return invocationId; + } + + /** + * Sets the identifier for this build invocation. + * + * @param invocationId the invocation identifier + */ + public void setInvocationId(String invocationId) { + this.invocationId = invocationId; + } + + /** + * Returns the timestamp of when the build started, serialized as RFC 3339 in UTC ({@code "Z"} suffix). + * + * @return the start timestamp, or {@code null} if not set + */ + public OffsetDateTime getStartedOn() { + return startedOn; + } + + /** + * Sets the timestamp of when the build started. + * + * @param startedOn the start timestamp + */ + public void setStartedOn(OffsetDateTime startedOn) { + this.startedOn = startedOn; + } + + /** + * Returns the timestamp of when the build completed, serialized as RFC 3339 in UTC ({@code "Z"} suffix). + * + * @return the completion timestamp, or {@code null} if not set + */ + public OffsetDateTime getFinishedOn() { + return finishedOn; + } + + /** + * Sets the timestamp of when the build completed. + * + * @param finishedOn the completion timestamp + */ + public void setFinishedOn(OffsetDateTime finishedOn) { + this.finishedOn = finishedOn; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof BuildMetadata)) + return false; + BuildMetadata that = (BuildMetadata) o; + return Objects.equals(invocationId, that.invocationId) && Objects.equals(startedOn, that.startedOn) && Objects.equals(finishedOn, that.finishedOn); + } + + @Override + public int hashCode() { + return Objects.hash(invocationId, startedOn, finishedOn); + } + + @Override + public String toString() { + return "BuildMetadata{invocationId='" + invocationId + "', startedOn=" + startedOn + ", finishedOn=" + finishedOn + '}'; + } +} diff --git a/src/main/java/org/apache/commons/build/models/slsa/v1_2/Builder.java b/src/main/java/org/apache/commons/build/models/slsa/v1_2/Builder.java new file mode 100644 index 000000000..d7835928b --- /dev/null +++ b/src/main/java/org/apache/commons/build/models/slsa/v1_2/Builder.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.build.models.slsa.v1_2; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Entity that executed the build and is trusted to have correctly performed the operation and populated the provenance. + * + * @see SLSA v1.2 Specification + */ +public class Builder { + + @JsonProperty("id") + private String id; + + @JsonProperty("builderDependencies") + private List builderDependencies; + + @JsonProperty("version") + private Map version; + + /** Creates a new Builder instance. */ + public Builder() { + } + + /** + * Returns the identifier of the builder. + * + * @return the builder identifier URI + */ + public String getId() { + return id; + } + + /** + * Sets the identifier of the builder. + * + * @param id the builder identifier URI + */ + public void setId(String id) { + this.id = id; + } + + /** + * Returns orchestrator dependencies that do not run within the build workload and do not affect the build output, + * but may affect provenance generation or security guarantees. + * + * @return the list of builder dependencies, or {@code null} if not set + */ + public List getBuilderDependencies() { + return builderDependencies; + } + + /** + * Sets the orchestrator dependencies that may affect provenance generation or security guarantees. + * + * @param builderDependencies the list of builder dependencies + */ + public void setBuilderDependencies(List builderDependencies) { + this.builderDependencies = builderDependencies; + } + + /** + * Returns a map of build platform component names to their versions. + * + * @return the version map, or {@code null} if not set + */ + public Map getVersion() { + return version; + } + + /** + * Sets the map of build platform component names to their versions. + * + * @param version the version map + */ + public void setVersion(Map version) { + this.version = version; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Builder)) + return false; + Builder that = (Builder) o; + return Objects.equals(id, that.id) + && Objects.equals(builderDependencies, that.builderDependencies) + && Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(id, builderDependencies, version); + } + + @Override + public String toString() { + return "Builder{id='" + id + "', builderDependencies=" + builderDependencies + ", version=" + version + '}'; + } +} diff --git a/src/main/java/org/apache/commons/build/models/slsa/v1_2/Provenance.java b/src/main/java/org/apache/commons/build/models/slsa/v1_2/Provenance.java new file mode 100644 index 000000000..91d1d184c --- /dev/null +++ b/src/main/java/org/apache/commons/build/models/slsa/v1_2/Provenance.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.build.models.slsa.v1_2; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; + +/** + * Root predicate of an SLSA v1.2 provenance attestation, describing what was built and how. + * + *

Combines a {@link BuildDefinition} (the inputs) with {@link RunDetails} (the execution context). Intended to be + * used as the {@code predicate} field of an in-toto {@link Statement}.

+ * + * @see SLSA v1.2 Specification + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Provenance { + + /** Predicate type URI used in the in-toto {@link Statement} wrapping this provenance. */ + public static final String PREDICATE_TYPE = "https://slsa.dev/provenance/v1"; + + @JsonProperty("buildDefinition") + private BuildDefinition buildDefinition; + + @JsonProperty("runDetails") + private RunDetails runDetails; + + /** Creates a new Provenance instance. */ + public Provenance() {} + + /** + * Creates a new Provenance with the given build definition and run details. + * + * @param buildDefinition inputs that defined the build + * @param runDetails details about the build invocation + */ + public Provenance(BuildDefinition buildDefinition, RunDetails runDetails) { + this.buildDefinition = buildDefinition; + this.runDetails = runDetails; + } + + /** + * Returns the build definition describing all inputs that produced the build output. + * + *

Includes source code, dependencies, build tools, base images, and other materials.

+ * + * @return the build definition, or {@code null} if not set + */ + public BuildDefinition getBuildDefinition() { + return buildDefinition; + } + + /** + * Sets the build definition describing all inputs that produced the build output. + * + * @param buildDefinition the build definition + */ + public void setBuildDefinition(BuildDefinition buildDefinition) { + this.buildDefinition = buildDefinition; + } + + /** + * Returns the details about the invocation of the build tool and the environment in which it was run. + * + * @return the run details, or {@code null} if not set + */ + public RunDetails getRunDetails() { + return runDetails; + } + + /** + * Sets the details about the invocation of the build tool and the environment in which it was run. + * + * @param runDetails the run details + */ + public void setRunDetails(RunDetails runDetails) { + this.runDetails = runDetails; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Provenance that = (Provenance) o; + return Objects.equals(buildDefinition, that.buildDefinition) && Objects.equals(runDetails, that.runDetails); + } + + @Override + public int hashCode() { + return Objects.hash(buildDefinition, runDetails); + } + + @Override + public String toString() { + return "Provenance{buildDefinition=" + buildDefinition + ", runDetails=" + runDetails + '}'; + } +} diff --git a/src/main/java/org/apache/commons/build/models/slsa/v1_2/ResourceDescriptor.java b/src/main/java/org/apache/commons/build/models/slsa/v1_2/ResourceDescriptor.java new file mode 100644 index 000000000..a34d4dae7 --- /dev/null +++ b/src/main/java/org/apache/commons/build/models/slsa/v1_2/ResourceDescriptor.java @@ -0,0 +1,220 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.build.models.slsa.v1_2; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; +import java.util.Objects; + +/** + * Description of an artifact or resource referenced in the build, identified by URI and cryptographic digest. + * + *

Used to represent inputs to, outputs from, or byproducts of the build process.

+ * + * @see SLSA v1.2 Specification + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ResourceDescriptor { + + @JsonProperty("name") + private String name; + + @JsonProperty("uri") + private String uri; + + @JsonProperty("digest") + private Map digest; + + @JsonProperty("content") + private byte[] content; + + @JsonProperty("downloadLocation") + private String downloadLocation; + + @JsonProperty("mediaType") + private String mediaType; + + @JsonProperty("annotations") + private Map annotations; + + /** Creates a new ResourceDescriptor instance. */ + public ResourceDescriptor() { + } + + /** + * Creates a new ResourceDescriptor with the given URI and digest. + * + * @param uri URI identifying the resource + * @param digest map of digest algorithm names to their hex-encoded values + */ + public ResourceDescriptor(String uri, Map digest) { + this.uri = uri; + this.digest = digest; + } + + /** + * Returns the name of the resource. + * + * @return the resource name, or {@code null} if not set + */ + public String getName() { + return name; + } + + /** + * Sets the name of the resource. + * + * @param name the resource name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns the URI identifying the resource. + * + * @return the resource URI, or {@code null} if not set + */ + public String getUri() { + return uri; + } + + /** + * Sets the URI identifying the resource. + * + * @param uri the resource URI + */ + public void setUri(String uri) { + this.uri = uri; + } + + /** + * Returns the map of cryptographic digest algorithms to their corresponding hex-encoded values for this resource. + * + *

Common keys include {@code "sha256"} and {@code "sha512"}.

+ * + * @return the digest map, or {@code null} if not set + */ + public Map getDigest() { + return digest; + } + + /** + * Sets the map of cryptographic digest algorithms to their hex-encoded values. + * + * @param digest the digest map + */ + public void setDigest(Map digest) { + this.digest = digest; + } + + /** + * Returns the raw contents of the resource, base64-encoded when serialized to JSON. + * + * @return the resource content, or {@code null} if not set + */ + public byte[] getContent() { + return content; + } + + /** + * Sets the raw contents of the resource. + * + * @param content the resource content + */ + public void setContent(byte[] content) { + this.content = content; + } + + /** + * Returns the download URI for the resource, if different from {@link #getUri()}. + * + * @return the download location URI, or {@code null} if not set + */ + public String getDownloadLocation() { + return downloadLocation; + } + + /** + * Sets the download URI for the resource. + * + * @param downloadLocation the download location URI + */ + public void setDownloadLocation(String downloadLocation) { + this.downloadLocation = downloadLocation; + } + + /** + * Returns the media type of the resource (e.g., {@code "application/octet-stream"}). + * + * @return the media type, or {@code null} if not set + */ + public String getMediaType() { + return mediaType; + } + + /** + * Sets the media type of the resource. + * + * @param mediaType the media type + */ + public void setMediaType(String mediaType) { + this.mediaType = mediaType; + } + + /** + * Returns additional key-value metadata about the resource, such as filename, size, or builder-specific attributes. + * + * @return the annotations map, or {@code null} if not set + */ + public Map getAnnotations() { + return annotations; + } + + /** + * Sets additional key-value metadata about the resource. + * + * @param annotations the annotations map + */ + public void setAnnotations(Map annotations) { + this.annotations = annotations; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ResourceDescriptor that = (ResourceDescriptor) o; + return Objects.equals(uri, that.uri) && Objects.equals(digest, that.digest); + } + + @Override + public int hashCode() { + return Objects.hash(uri, digest); + } + + @Override + public String toString() { + return "ResourceDescriptor{uri='" + uri + '\'' + ", digest=" + digest + '}'; + } +} diff --git a/src/main/java/org/apache/commons/build/models/slsa/v1_2/RunDetails.java b/src/main/java/org/apache/commons/build/models/slsa/v1_2/RunDetails.java new file mode 100644 index 000000000..92872c960 --- /dev/null +++ b/src/main/java/org/apache/commons/build/models/slsa/v1_2/RunDetails.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.build.models.slsa.v1_2; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Objects; + +/** + * Details about the build invocation: the builder identity, execution metadata, and any byproduct artifacts. + * + * @see SLSA v1.2 Specification + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class RunDetails { + + @JsonProperty("builder") + private Builder builder; + + @JsonProperty("metadata") + private BuildMetadata metadata; + + @JsonProperty("byproducts") + private List byproducts; + + /** Creates a new RunDetails instance. */ + public RunDetails() {} + + /** + * Creates a new RunDetails with the given builder and metadata. + * + * @param builder entity that executed the build + * @param metadata metadata about the build invocation + */ + public RunDetails(Builder builder, BuildMetadata metadata) { + this.builder = builder; + this.metadata = metadata; + } + + /** + * Returns the builder that executed the invocation. + * + *

Trusted to have correctly performed the operation and populated this provenance.

+ * + * @return the builder, or {@code null} if not set + */ + public Builder getBuilder() { + return builder; + } + + /** + * Sets the builder that executed the invocation. + * + * @param builder the builder + */ + public void setBuilder(Builder builder) { + this.builder = builder; + } + + /** + * Returns the metadata about the build invocation, including its identifier and timing. + * + * @return the build metadata, or {@code null} if not set + */ + public BuildMetadata getMetadata() { + return metadata; + } + + /** + * Sets the metadata about the build invocation. + * + * @param metadata the build metadata + */ + public void setMetadata(BuildMetadata metadata) { + this.metadata = metadata; + } + + /** + * Returns artifacts produced as a side effect of the build that are not the primary output. + * + * @return the list of byproduct artifacts, or {@code null} if not set + */ + public List getByproducts() { + return byproducts; + } + + /** + * Sets the artifacts produced as a side effect of the build that are not the primary output. + * + * @param byproducts the list of byproduct artifacts + */ + public void setByproducts(List byproducts) { + this.byproducts = byproducts; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RunDetails that = (RunDetails) o; + return Objects.equals(builder, that.builder) && Objects.equals(metadata, that.metadata) && Objects.equals(byproducts, that.byproducts); + } + + @Override + public int hashCode() { + return Objects.hash(builder, metadata, byproducts); + } + + @Override + public String toString() { + return "RunDetails{builder=" + builder + ", metadata=" + metadata + ", byproducts=" + byproducts + '}'; + } +} diff --git a/src/main/java/org/apache/commons/build/models/slsa/v1_2/Statement.java b/src/main/java/org/apache/commons/build/models/slsa/v1_2/Statement.java new file mode 100644 index 000000000..a6d690c8f --- /dev/null +++ b/src/main/java/org/apache/commons/build/models/slsa/v1_2/Statement.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.build.models.slsa.v1_2; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Objects; + +/** + * In-toto v1 attestation envelope that binds a set of subject artifacts to an SLSA provenance predicate. + * + * @see in-toto Statement v1 + */ +public class Statement { + + @JsonProperty("_type") + private final String _type = "https://in-toto.io/Statement/v1"; + + @JsonProperty("subject") + private List subject; + + @JsonProperty("predicateType") + private String predicateType; + + @JsonProperty("predicate") + private Provenance predicate; + + /** Creates a new Statement instance. */ + public Statement() { + } + + /** + * Returns the schema identifier for this statement. + * + * @return the fixed type URI {@code "https://in-toto.io/Statement/v1"} + */ + public String get_type() { + return _type; + } + + /** + * Returns the set of software artifacts that the attestation applies to. + * + *

Each element represents a single artifact. Artifacts are matched purely by digest, regardless of content + * type.

+ * + * @return the list of subject artifacts, or {@code null} if not set + */ + public List getSubject() { + return subject; + } + + /** + * Sets the set of software artifacts that the attestation applies to. + * + * @param subject the list of subject artifacts + */ + public void setSubject(List subject) { + this.subject = subject; + } + + /** + * Returns the URI identifying the type of the predicate. + * + * @return the predicate type URI, or {@code null} if no predicate has been set + */ + public String getPredicateType() { + return predicateType; + } + + /** + * Returns the provenance predicate. + * + *

Unset is treated the same as set-but-empty. May be omitted if {@code predicateType} fully describes the + * predicate.

+ * + * @return the provenance predicate, or {@code null} if not set + */ + public Provenance getPredicate() { + return predicate; + } + + /** + * Sets the provenance predicate and automatically assigns {@code predicateType} to the SLSA provenance v1 URI. + * + * @param predicate the provenance predicate + */ + public void setPredicate(Provenance predicate) { + this.predicate = predicate; + this.predicateType = Provenance.PREDICATE_TYPE; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Statement)) + return false; + Statement statement = (Statement) o; + return Objects.equals(subject, statement.subject) && Objects.equals(predicateType, statement.predicateType) && Objects.equals(predicate, + statement.predicate); + } + + @Override + public int hashCode() { + return Objects.hash(subject, predicateType, predicate); + } + + @Override + public String toString() { + return "Statement{_type='" + _type + "', subject=" + subject + ", predicateType='" + predicateType + "', predicate=" + predicate + '}'; + } +} diff --git a/src/main/java/org/apache/commons/build/models/slsa/v1_2/package-info.java b/src/main/java/org/apache/commons/build/models/slsa/v1_2/package-info.java new file mode 100644 index 000000000..eb77ffe97 --- /dev/null +++ b/src/main/java/org/apache/commons/build/models/slsa/v1_2/package-info.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * SLSA 1.2 Build Attestation Models. + * + *

This package provides Jackson-annotated model classes that implement the Supply-chain Levels for Software Artifacts (SLSA) v1.2 + * specification. + * + *

Overview

+ * + *

SLSA is a framework for evaluating and improving the security posture of build systems. SLSA + * v1.2 defines a standard for recording build provenance - information about how software + * artifacts were produced. + * + *

Core Models

+ * + *
    + *
  • {@link org.apache.commons.build.models.slsa.v1_2.Provenance} - Root object + * describing the build provenance. Contains BuildDefinition and RunDetails. + *
  • {@link org.apache.commons.build.models.slsa.v1_2.BuildDefinition} - Specifies + * the inputs that define the build, including build type, configuration, external + * parameters, and resolved dependencies. + *
  • {@link org.apache.commons.build.models.slsa.v1_2.RunDetails} - Specifies the + * details about the build invocation and environment, including the builder identity and + * build metadata. + *
+ * + *

Supporting Models

+ * + *
    + *
  • {@link org.apache.commons.build.models.slsa.v1_2.Builder} - Identifies the + * entity that executed the build. + *
  • {@link org.apache.commons.build.models.slsa.v1_2.BuildMetadata} - Contains + * metadata about the build invocation, including timing information. + *
  • {@link org.apache.commons.build.models.slsa.v1_2.ResourceDescriptor} - Describes + * an artifact or resource referenced in the build by URI and cryptographic digest. + *
+ * + *

Usage Example

+ * + *
+ * // Create a builder
+ * Builder builder = new Builder();
+ * builder.setId("https://github.com/actions");
+ * builder.setVersion("1.0");
+ *
+ * // Create build metadata
+ * BuildMetadata buildMetadata = new BuildMetadata();
+ * buildMetadata.setInvocationId("build-12345");
+ * buildMetadata.setStartedOn(OffsetDateTime.now(ZoneOffset.UTC));
+ * buildMetadata.setFinishedOn(OffsetDateTime.now(ZoneOffset.UTC));
+ *
+ * // Create run details
+ * RunDetails runDetails = new RunDetails();
+ * runDetails.setBuilder(builder);
+ * runDetails.setMetadata(buildMetadata);
+ *
+ * // Create build definition
+ * BuildDefinition buildDefinition = new BuildDefinition();
+ * buildDefinition.setBuildType("https://github.com/actions");
+ * buildDefinition.setExternalParameters(new HashMap<>());
+ *
+ * // Create provenance
+ * Provenance provenance = new Provenance();
+ * provenance.setBuildDefinition(buildDefinition);
+ * provenance.setRunDetails(runDetails);
+ *
+ * // Serialize with Jackson
+ * ObjectMapper mapper = new ObjectMapper();
+ * String json = mapper.writeValueAsString(provenance);
+ * 
+ * + *

Jackson Integration

+ * + *

All models use Jackson annotations for JSON serialization/deserialization: + * + *

    + *
  • {@code @JsonProperty} - Maps field names to JSON properties + *
  • {@code @JsonInclude} - Controls inclusion of null/empty values in serialization + *
  • {@code @JsonFormat} - Specifies date/time formatting + *
+ * + *

The models are compatible with Jackson's ObjectMapper and support both serialization to + * JSON and deserialization from JSON. + * + *

Validation

+ * + *

Some models include Jakarta Validation annotations: + * + *

    + *
  • {@code @NotBlank} - Ensures required string fields are not empty + *
+ * + *

Users can enable validation using a Jakarta Validation provider to ensure provenance + * integrity. + * + *

Reference

+ * + * @see SLSA v1.2 Specification + * @see In-toto Attestation Framework + * @see Jackson JSON processor + */ +package org.apache.commons.build.models.slsa.v1_2; + From a196a7d54eafd262724b8beeccfe1bea338062eb Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Fri, 27 Mar 2026 18:11:37 +0100 Subject: [PATCH 2/8] Add `build-attestation` goal This goal generates a [SLSA](https://slsa.dev/) build attestation and attaches it to the build as a file with the `.intoto.json` extension. The attestation records the following information about the build environment: - The Java version used (vendor, version string) - The Maven version used - The `gitTree` hash of the unpacked Java distribution - The `gitTree` hash of the unpacked Maven distribution The `gitTree` hashes uniquely and verifiably identify the exact content of the Java and Maven distributions used during the build, independently of how or where they were obtained. This allows consumers of the attestation to verify that the build environment matches a known distribution. --- pom.xml | 53 +++ .../commons/build/BuildAttestationMojo.java | 363 ++++++++++++++++++ .../commons/build/internal/ArtifactUtils.java | 82 ++++ .../build/internal/BuildToolDescriptors.java | 88 +++++ .../commons/build/internal/GitUtils.java | 87 +++++ .../models/slsa/v1_2/BuildDefinition.java | 10 +- .../build/models/slsa/v1_2/Builder.java | 8 +- .../build/BuildAttestationMojoTest.java | 134 +++++++ .../commons/build/internal/MojoUtils.java | 70 ++++ src/test/resources/artifacts/artifact-jar.txt | 2 + src/test/resources/artifacts/artifact-pom.txt | 2 + 11 files changed, 891 insertions(+), 8 deletions(-) create mode 100644 src/main/java/org/apache/commons/build/BuildAttestationMojo.java create mode 100644 src/main/java/org/apache/commons/build/internal/ArtifactUtils.java create mode 100644 src/main/java/org/apache/commons/build/internal/BuildToolDescriptors.java create mode 100644 src/main/java/org/apache/commons/build/internal/GitUtils.java create mode 100644 src/test/java/org/apache/commons/build/BuildAttestationMojoTest.java create mode 100644 src/test/java/org/apache/commons/build/internal/MojoUtils.java create mode 100644 src/test/resources/artifacts/artifact-jar.txt create mode 100644 src/test/resources/artifacts/artifact-pom.txt diff --git a/pom.xml b/pom.xml index 408b7df5c..f1e7ef5ae 100644 --- a/pom.xml +++ b/pom.xml @@ -90,6 +90,7 @@ 2.21.1 2.21 3.9.12 + 2.2.1 org.apache.maven @@ -134,6 +141,11 @@ ${commons.maven.version} provided + + org.apache.maven.plugin-tools + maven-plugin-annotations + 3.15.2 + org.apache.maven @@ -141,11 +153,52 @@ ${commons.maven.version} provided + + org.apache.maven + maven-embedder + ${commons.maven.version} + provided + org.apache.maven.plugin-tools maven-script-ant 3.15.2 + + org.apache.maven.scm + maven-scm-manager-plexus + ${commons.maven.scm.version} + compile + + + org.codehaus.plexus + plexus-container-default + + + + + org.apache.maven.scm + maven-scm-provider-gitexe + ${commons.maven.scm.version} + runtime + + + net.javacrumbs.json-unit + json-unit-assertj + 2.40.1 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.mockito + mockito-core + 5.18.0 + test + diff --git a/src/main/java/org/apache/commons/build/BuildAttestationMojo.java b/src/main/java/org/apache/commons/build/BuildAttestationMojo.java new file mode 100644 index 000000000..d021d2d8c --- /dev/null +++ b/src/main/java/org/apache/commons/build/BuildAttestationMojo.java @@ -0,0 +1,363 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.build; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.management.ManagementFactory; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.inject.Inject; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.apache.commons.build.internal.ArtifactUtils; +import org.apache.commons.build.internal.BuildToolDescriptors; +import org.apache.commons.build.internal.GitUtils; +import org.apache.commons.build.models.slsa.v1_2.BuildDefinition; +import org.apache.commons.build.models.slsa.v1_2.BuildMetadata; +import org.apache.commons.build.models.slsa.v1_2.Builder; +import org.apache.commons.build.models.slsa.v1_2.Provenance; +import org.apache.commons.build.models.slsa.v1_2.ResourceDescriptor; +import org.apache.commons.build.models.slsa.v1_2.RunDetails; +import org.apache.commons.build.models.slsa.v1_2.Statement; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.execution.MavenExecutionRequest; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.MavenProject; +import org.apache.maven.project.MavenProjectHelper; +import org.apache.maven.rtinfo.RuntimeInformation; +import org.apache.maven.scm.CommandParameters; +import org.apache.maven.scm.ScmException; +import org.apache.maven.scm.ScmFileSet; +import org.apache.maven.scm.command.info.InfoItem; +import org.apache.maven.scm.command.info.InfoScmResult; +import org.apache.maven.scm.manager.ScmManager; +import org.apache.maven.scm.repository.ScmRepository; + +/** + * This plugin generates an in-toto attestation for all the artifacts + */ +@Mojo(name = "build-attestation", defaultPhase = LifecyclePhase.VERIFY, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME) +public class BuildAttestationMojo extends AbstractMojo { + + private static final String ATTESTATION_EXTENSION = "intoto.json"; + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + static { + OBJECT_MAPPER.findAndRegisterModules(); + OBJECT_MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + } + + @Parameter(defaultValue = "${project.scm.connection}", readonly = true) + private String scmConnectionUrl; + + @Parameter(defaultValue = "${project.scm.developerConnection}", readonly = true) + private String scmDeveloperConnectionUrl; + + @Parameter(defaultValue = "${project.scm.tag}", readonly = true) + private String scmTag; + + @Parameter(defaultValue = "${maven.home}", readonly = true) + private File mavenHome; + + /** + * Issue SCM actions at this local directory + */ + @Parameter(property = "commons.build.scmDirectory", defaultValue = "${basedir}") + private File scmDirectory; + + @Parameter(property = "commons.build.outputDirectory", defaultValue = "${project.build.directory}") + private File outputDirectory; + + @Parameter(property = "commons.build.skipAttach") + private boolean skipAttach; + + /** + * The current Maven project. + */ + private final MavenProject project; + + /** + * SCM manager to detect the Git revision. + */ + private final ScmManager scmManager; + + /** + * Runtime information + */ + private final RuntimeInformation runtimeInformation; + + /** + * The current Maven session, used to resolve plugin dependencies. + */ + private final MavenSession session; + + /** + * Helper to attach artifacts to the project. + */ + private final MavenProjectHelper mavenProjectHelper; + + @Inject + public BuildAttestationMojo(MavenProject project, ScmManager scmManager, RuntimeInformation runtimeInformation, MavenSession session, + MavenProjectHelper mavenProjectHelper) { + this.project = project; + this.scmManager = scmManager; + this.runtimeInformation = runtimeInformation; + this.session = session; + this.mavenProjectHelper = mavenProjectHelper; + } + + void setOutputDirectory(File outputDirectory) { + this.outputDirectory = outputDirectory; + } + + public File getScmDirectory() { + return scmDirectory; + } + + public void setScmDirectory(File scmDirectory) { + this.scmDirectory = scmDirectory; + } + + void setScmConnectionUrl(String scmConnectionUrl) { + this.scmConnectionUrl = scmConnectionUrl; + } + + void setScmDeveloperConnectionUrl(String scmDeveloperConnectionUrl) { + this.scmDeveloperConnectionUrl = scmDeveloperConnectionUrl; + } + + void setScmTag(String scmTag) { + this.scmTag = scmTag; + } + + void setMavenHome(File mavenHome) { + this.mavenHome = mavenHome; + } + + @Override + public void execute() throws MojoFailureException, MojoExecutionException { + getLog().info("This is a build attestation."); + // Build definition + BuildDefinition buildDefinition = new BuildDefinition(); + buildDefinition.setExternalParameters(getExternalParameters()); + buildDefinition.setResolvedDependencies(getBuildDependencies()); + // Builder + Builder builder = new Builder(); + // RunDetails + RunDetails runDetails = new RunDetails(); + runDetails.setBuilder(builder); + runDetails.setMetadata(getBuildMetadata()); + // Provenance + Provenance provenance = new Provenance(); + provenance.setBuildDefinition(buildDefinition); + provenance.setRunDetails(runDetails); + // Statement + Statement statement = new Statement(); + statement.setSubject(getSubjects()); + statement.setPredicate(provenance); + + writeStatement(statement); + } + + private void writeStatement(Statement statement) throws MojoExecutionException { + final Path outputPath = outputDirectory.toPath(); + try { + if (!Files.exists(outputPath)) { + Files.createDirectories(outputPath); + } + } catch (IOException e) { + throw new MojoExecutionException("Could not create output directory.", e); + } + final Artifact mainArtifact = project.getArtifact(); + final Path artifactPath = outputPath.resolve(ArtifactUtils.getFileName(mainArtifact, ATTESTATION_EXTENSION)); + getLog().info("Writing attestation statement to: " + artifactPath); + try (OutputStream os = Files.newOutputStream(artifactPath)) { + OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValue(os, statement); + } catch (IOException e) { + throw new MojoExecutionException("Could not write attestation statement to: " + artifactPath, e); + } + if (!skipAttach) { + getLog().info(String.format("Attaching attestation statement as %s-%s.%s", mainArtifact.getArtifactId(), mainArtifact.getVersion(), + ATTESTATION_EXTENSION)); + mavenProjectHelper.attachArtifact(project, ATTESTATION_EXTENSION, null, artifactPath.toFile()); + } + } + + /** + * Get the artifacts generated by the build. + * + * @return A list of resource descriptors for the build artifacts. + */ + private List getSubjects() throws MojoExecutionException { + List subjects = new ArrayList<>(); + subjects.add(ArtifactUtils.toResourceDescriptor(project.getArtifact())); + for (Artifact artifact : project.getAttachedArtifacts()) { + subjects.add(ArtifactUtils.toResourceDescriptor(artifact)); + } + return subjects; + } + + private Map getExternalParameters() { + Map params = new HashMap<>(); + params.put("jvm.args", ManagementFactory.getRuntimeMXBean().getInputArguments()); + MavenExecutionRequest request = session.getRequest(); + params.put("maven.goals", request.getGoals()); + params.put("maven.profiles", request.getActiveProfiles()); + params.put("maven.user.properties", request.getUserProperties()); + params.put("maven.cmdline", getCommandLine(request)); + Map env = new HashMap<>(); + params.put("env", env); + for (Map.Entry entry : System.getenv().entrySet()) { + String key = entry.getKey(); + if ("TZ".equals(key) || "LANG".equals(key) || key.startsWith("LC_")) { + env.put(key, entry.getValue()); + } + } + return params; + } + + private String getCommandLine(MavenExecutionRequest request) { + StringBuilder sb = new StringBuilder(); + for (String goal : request.getGoals()) { + sb.append(goal); + sb.append(" "); + } + List activeProfiles = request.getActiveProfiles(); + if (activeProfiles != null && !activeProfiles.isEmpty()) { + sb.append("-P"); + for (String profile : activeProfiles) { + sb.append(profile); + sb.append(","); + } + removeLast(sb); + sb.append(" "); + } + Properties userProperties = request.getUserProperties(); + for (String propertyName : userProperties.stringPropertyNames()) { + sb.append("-D"); + sb.append(propertyName); + sb.append("="); + sb.append(userProperties.get(propertyName)); + sb.append(" "); + } + removeLast(sb); + return sb.toString(); + } + + private static void removeLast(StringBuilder sb) { + if (sb.length() > 0) { + sb.setLength(sb.length() - 1); + } + } + + private List getBuildDependencies() throws MojoExecutionException { + List dependencies = new ArrayList<>(); + try { + dependencies.add(BuildToolDescriptors.jvm(Paths.get(System.getProperty("java.home")))); + dependencies.add(BuildToolDescriptors.maven(runtimeInformation.getMavenVersion(), mavenHome.toPath())); + dependencies.add(getScmDescriptor()); + } catch (IOException e) { + throw new MojoExecutionException(e); + } + dependencies.addAll(getProjectDependencies()); + return dependencies; + } + + private List getProjectDependencies() throws MojoExecutionException { + List dependencies = new ArrayList<>(); + for (Artifact artifact : project.getArtifacts()) { + dependencies.add(ArtifactUtils.toResourceDescriptor(artifact)); + } + return dependencies; + } + + private ResourceDescriptor getScmDescriptor() throws IOException, MojoExecutionException { + ResourceDescriptor scmDescriptor = new ResourceDescriptor(); + String scmUri = GitUtils.scmToDownloadUri(scmConnectionUrl, scmDirectory.toPath()); + scmDescriptor.setUri(scmUri); + // Compute the revision + Map digest = new HashMap<>(); + digest.put("gitCommit", getScmRevision()); + scmDescriptor.setDigest(digest); + return scmDescriptor; + } + + private ScmRepository getScmRepository() throws MojoExecutionException { + try { + return scmManager.makeScmRepository(scmConnectionUrl); + } catch (ScmException e) { + throw new MojoExecutionException("Failed to create SCM repository", e); + } + } + + private String getScmRevision() throws MojoExecutionException { + ScmRepository scmRepository = getScmRepository(); + CommandParameters commandParameters = new CommandParameters(); + try { + InfoScmResult result = scmManager.getProviderByRepository(scmRepository).info(scmRepository.getProviderRepository(), new ScmFileSet(scmDirectory) + , commandParameters); + + return getScmRevision(result); + } catch (ScmException e) { + throw new MojoExecutionException("Failed to retrieve SCM revision", e); + } + } + + private String getScmRevision(InfoScmResult result) throws MojoExecutionException { + if (!result.isSuccess()) { + throw new MojoExecutionException("Failed to retrieve SCM revision: " + result.getProviderMessage()); + } + + if (result.getInfoItems() == null || result.getInfoItems().isEmpty()) { + throw new MojoExecutionException("No SCM revision information found for " + scmDirectory); + } + + InfoItem item = result.getInfoItems().get(0); + + String revision = item.getRevision(); + if (revision == null) { + throw new MojoExecutionException("Empty SCM revision returned for " + scmDirectory); + } + return revision; + } + + private BuildMetadata getBuildMetadata() { + OffsetDateTime startedOn = session.getStartTime().toInstant().atOffset(ZoneOffset.UTC); + OffsetDateTime finishedOn = OffsetDateTime.now(ZoneOffset.UTC); + return new BuildMetadata(session.getRequest().getBuilderId(), startedOn, finishedOn); + } +} diff --git a/src/main/java/org/apache/commons/build/internal/ArtifactUtils.java b/src/main/java/org/apache/commons/build/internal/ArtifactUtils.java new file mode 100644 index 000000000..9e1f047c4 --- /dev/null +++ b/src/main/java/org/apache/commons/build/internal/ArtifactUtils.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.build.internal; + +import org.apache.commons.build.models.slsa.v1_2.ResourceDescriptor; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public final class ArtifactUtils { + + private ArtifactUtils() { + // prevent instantiation + } + + public static String getFileName(Artifact artifact) { + return getFileName(artifact, artifact.getArtifactHandler().getExtension()); + } + + public static String getFileName(Artifact artifact, String extension) { + StringBuilder fileName = new StringBuilder(); + fileName.append(artifact.getArtifactId()).append("-").append(artifact.getVersion()); + if (artifact.getClassifier() != null) { + fileName.append("-").append(artifact.getClassifier()); + } + fileName.append(".").append(extension); + return fileName.toString(); + } + + public static String getPackageUrl(Artifact artifact) { + StringBuilder sb = new StringBuilder(); + sb.append("pkg:maven/").append(artifact.getGroupId()).append("/").append(artifact.getArtifactId()).append("@").append(artifact.getVersion()) + .append("?"); + String classifier = artifact.getClassifier(); + if (classifier != null) { + sb.append("classifier=").append(classifier).append("&"); + } + sb.append("type=").append(artifact.getType()); + return sb.toString(); + } + + public static Map getChecksums(Artifact artifact) throws IOException { + Map checksums = new HashMap<>(); + DigestUtils digest = new DigestUtils(DigestUtils.getSha256Digest()); + String sha256sum = digest.digestAsHex(artifact.getFile()); + checksums.put("sha256", sha256sum); + return checksums; + } + + public static ResourceDescriptor toResourceDescriptor(Artifact artifact) throws MojoExecutionException { + ResourceDescriptor descriptor = new ResourceDescriptor(); + descriptor.setName(getFileName(artifact)); + descriptor.setUri(getPackageUrl(artifact)); + if (artifact.getFile() != null) { + try { + descriptor.setDigest(getChecksums(artifact)); + } catch (IOException e) { + throw new MojoExecutionException("Unable to compute hash for artifact file: " + artifact.getFile(), e); + } + } + return descriptor; + } +} diff --git a/src/main/java/org/apache/commons/build/internal/BuildToolDescriptors.java b/src/main/java/org/apache/commons/build/internal/BuildToolDescriptors.java new file mode 100644 index 000000000..0bdc33334 --- /dev/null +++ b/src/main/java/org/apache/commons/build/internal/BuildToolDescriptors.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.build.internal; + +import org.apache.commons.build.models.slsa.v1_2.ResourceDescriptor; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * Factory methods for {@link ResourceDescriptor} instances representing build-tool dependencies. + */ +public final class BuildToolDescriptors { + + private BuildToolDescriptors() { + // no instantiation + } + + /** + * Creates a {@link ResourceDescriptor} for the JDK used during the build. + * + * @param javaHome path to the JDK home directory (value of the {@code java.home} system property) + * @return a descriptor with digest and annotations populated from system properties + * @throws IOException if hashing the JDK directory fails + */ + public static ResourceDescriptor jvm(Path javaHome) throws IOException { + ResourceDescriptor descriptor = new ResourceDescriptor(); + descriptor.setName("JDK"); + Map digest = new HashMap<>(); + digest.put("gitTree", GitUtils.gitTree(javaHome)); + descriptor.setDigest(digest); + String[] propertyNames = {"java.version", "java.vendor", "java.vendor.version", "java.vm.name", "java.vm.version", "java.vm.vendor", + "java.runtime.name", "java.runtime.version", "java.specification.version"}; + Map annotations = new HashMap<>(); + for (String prop : propertyNames) { + annotations.put(prop.substring("java.".length()), System.getProperty(prop)); + } + descriptor.setAnnotations(annotations); + return descriptor; + } + + /** + * Creates a {@link ResourceDescriptor} for the Maven installation used during the build. + * + * @param version Maven version string + * @param mavenHome path to the Maven home directory + * @return a descriptor for the Maven installation + * @throws IOException if hashing the Maven home directory fails + */ + public static ResourceDescriptor maven(String version, Path mavenHome) throws IOException { + ResourceDescriptor descriptor = new ResourceDescriptor(); + descriptor.setName("Maven"); + descriptor.setUri("pkg:maven/org.apache.maven/apache-maven@" + version); + Map digest = new HashMap<>(); + digest.put("gitTree", GitUtils.gitTree(mavenHome)); + descriptor.setDigest(digest); + Properties buildProps = new Properties(); + try (InputStream in = BuildToolDescriptors.class.getResourceAsStream("/org/apache/maven/messages/build.properties")) { + if (in != null) { + buildProps.load(in); + } + } + if (!buildProps.isEmpty()) { + Map annotations = new HashMap<>(); + buildProps.forEach((key, value) -> annotations.put((String) key, value)); + descriptor.setAnnotations(annotations); + } + return descriptor; + } +} diff --git a/src/main/java/org/apache/commons/build/internal/GitUtils.java b/src/main/java/org/apache/commons/build/internal/GitUtils.java new file mode 100644 index 000000000..01c987b0d --- /dev/null +++ b/src/main/java/org/apache/commons/build/internal/GitUtils.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.build.internal; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; + +public final class GitUtils { + + public static String gitTree(Path path) throws IOException { + if (!Files.isDirectory(path)) { + throw new IOException("Path is not a directory: " + path); + } + MessageDigest digest = DigestUtils.getSha1Digest(); + return Hex.encodeHexString(DigestUtils.gitTree(digest, path)); + } + + public static String gitBlob(Path path) throws IOException { + if (!Files.isRegularFile(path)) { + throw new IOException("Path is not a regular file: " + path); + } + MessageDigest digest = DigestUtils.getSha1Digest(); + return Hex.encodeHexString(DigestUtils.gitBlob(digest, path)); + } + + public static String scmToDownloadUri(String scmUri, Path repositoryPath) throws IOException { + if (!scmUri.startsWith("scm:git")) { + throw new IllegalArgumentException("Invalid scmUri: " + scmUri); + } + String currentBranch = getCurrentBranch(repositoryPath); + return "git+" + scmUri.substring(8) + "@" + currentBranch; + } + + public static String getCurrentBranch(Path repositoryPath) throws IOException { + Path gitDir = findGitDir(repositoryPath); + String head = new String(Files.readAllBytes(gitDir.resolve("HEAD")), StandardCharsets.UTF_8).trim(); + if (head.startsWith("ref: refs/heads/")) { + return head.substring("ref: refs/heads/".length()); + } + // detached HEAD — return the commit SHA + return head; + } + + private static Path findGitDir(Path path) throws IOException { + Path current = path.toAbsolutePath(); + while (current != null) { + Path candidate = current.resolve(".git"); + if (Files.isDirectory(candidate)) { + return candidate; + } + if (Files.isRegularFile(candidate)) { + // git worktree: .git is a file containing "gitdir: /path/to/real/.git" + String content = new String(Files.readAllBytes(candidate), StandardCharsets.UTF_8).trim(); + if (content.startsWith("gitdir: ")) { + return Paths.get(content.substring("gitdir: ".length())); + } + } + current = current.getParent(); + } + throw new IOException("No .git directory found above: " + path); + } + + private GitUtils() { + // no instantiation + } +} diff --git a/src/main/java/org/apache/commons/build/models/slsa/v1_2/BuildDefinition.java b/src/main/java/org/apache/commons/build/models/slsa/v1_2/BuildDefinition.java index 12bbab7b7..df0123e06 100644 --- a/src/main/java/org/apache/commons/build/models/slsa/v1_2/BuildDefinition.java +++ b/src/main/java/org/apache/commons/build/models/slsa/v1_2/BuildDefinition.java @@ -16,12 +16,13 @@ */ package org.apache.commons.build.models.slsa.v1_2; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; + /** * Inputs that define the build: the build type, external and internal parameters, and resolved dependencies. * @@ -30,17 +31,16 @@ * * @see SLSA v1.2 Specification */ -@JsonInclude(JsonInclude.Include.NON_NULL) public class BuildDefinition { @JsonProperty("buildType") private String buildType = "https://commons.apache.org/builds/0.1.0"; @JsonProperty("externalParameters") - private Map externalParameters; + private Map externalParameters = new HashMap<>(); @JsonProperty("internalParameters") - private Map internalParameters; + private Map internalParameters = new HashMap<>(); @JsonProperty("resolvedDependencies") private List resolvedDependencies; diff --git a/src/main/java/org/apache/commons/build/models/slsa/v1_2/Builder.java b/src/main/java/org/apache/commons/build/models/slsa/v1_2/Builder.java index d7835928b..7466a71ea 100644 --- a/src/main/java/org/apache/commons/build/models/slsa/v1_2/Builder.java +++ b/src/main/java/org/apache/commons/build/models/slsa/v1_2/Builder.java @@ -18,6 +18,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -30,13 +32,13 @@ public class Builder { @JsonProperty("id") - private String id; + private String id = "https://commons.apache.org/builds/0.1.0"; @JsonProperty("builderDependencies") - private List builderDependencies; + private List builderDependencies = new ArrayList<>(); @JsonProperty("version") - private Map version; + private Map version = new HashMap<>(); /** Creates a new Builder instance. */ public Builder() { diff --git a/src/test/java/org/apache/commons/build/BuildAttestationMojoTest.java b/src/test/java/org/apache/commons/build/BuildAttestationMojoTest.java new file mode 100644 index 000000000..46ed89224 --- /dev/null +++ b/src/test/java/org/apache/commons/build/BuildAttestationMojoTest.java @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.build; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Date; +import java.util.Properties; + +import org.apache.commons.build.internal.MojoUtils; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.bridge.MavenRepositorySystem; +import org.apache.maven.execution.DefaultMavenExecutionRequest; +import org.apache.maven.execution.DefaultMavenExecutionResult; +import org.apache.maven.execution.MavenExecutionRequest; +import org.apache.maven.execution.MavenExecutionResult; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Model; +import org.apache.maven.project.MavenProject; +import org.apache.maven.project.MavenProjectHelper; +import org.apache.maven.rtinfo.RuntimeInformation; +import org.apache.maven.scm.manager.ScmManager; +import org.codehaus.plexus.PlexusContainer; +import org.codehaus.plexus.component.repository.exception.ComponentLookupException; +import org.eclipse.aether.RepositorySystemSession; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class BuildAttestationMojoTest { + + @TempDir + private static Path localRepositoryPath; + + private static PlexusContainer container; + private static RepositorySystemSession repoSession; + + @BeforeAll + static void setup() throws Exception { + container = MojoUtils.setupContainer(); + repoSession = MojoUtils.createRepositorySystemSession(container, localRepositoryPath); + } + + private static MavenExecutionRequest createMavenExecutionRequest() { + DefaultMavenExecutionRequest request = new DefaultMavenExecutionRequest(); + request.setStartTime(new Date()); + return request; + } + + @SuppressWarnings("deprecation") + private static MavenSession createMavenSession(MavenExecutionRequest request, MavenExecutionResult result) { + return new MavenSession(container, repoSession, request, result); + } + + private static BuildAttestationMojo createBuildAttestationMojo(MavenProject project, MavenProjectHelper projectHelper) throws ComponentLookupException { + ScmManager scmManager = container.lookup(ScmManager.class); + RuntimeInformation runtimeInfo = container.lookup(RuntimeInformation.class); + return new BuildAttestationMojo(project, scmManager, runtimeInfo, + createMavenSession(createMavenExecutionRequest(), new DefaultMavenExecutionResult()), projectHelper); + } + + private static MavenProject createMavenProject(MavenProjectHelper projectHelper, MavenRepositorySystem repoSystem) throws ComponentLookupException { + MavenProject project = new MavenProject(new Model()); + Artifact artifact = repoSystem.createArtifact("groupId", "artifactId", "1.2.3", null, "jar"); + project.setArtifact(artifact); + project.setGroupId("groupId"); + project.setArtifactId("artifactId"); + project.setVersion("1.2.3"); + // Attach a couple of artifacts + projectHelper.attachArtifact(project, "pom", null, new File("src/test/resources/artifacts/artifact-pom.txt")); + artifact.setFile(new File("src/test/resources/artifacts/artifact-jar.txt")); + return project; + } + + @Test + void attestationTest() throws Exception { + MavenProjectHelper projectHelper = container.lookup(MavenProjectHelper.class); + MavenRepositorySystem repoSystem = container.lookup(MavenRepositorySystem.class); + MavenProject project = createMavenProject(projectHelper, repoSystem); + + BuildAttestationMojo mojo = createBuildAttestationMojo(project, projectHelper); + mojo.setOutputDirectory(new File("target/attestations")); + mojo.setScmDirectory(new File(".")); + mojo.setScmConnectionUrl("scm:git:https://github.com/apache/commons-lang.git"); + mojo.setScmDeveloperConnectionUrl("scm:git:ssh://git@github.com/apache/commons-lang.git"); + mojo.setScmTag("tag"); + mojo.setMavenHome(new File(System.getProperty("maven.home", "."))); + mojo.execute(); + + Artifact attestation = project.getAttachedArtifacts().stream() + .filter(a -> "intoto.json".equals(a.getType())) + .findFirst() + .orElseThrow(() -> new AssertionError("No intoto.json artifact attached to project")); + String json = new String(Files.readAllBytes(attestation.getFile().toPath()), StandardCharsets.UTF_8); + + String resolvedDeps = "predicate.buildDefinition.resolvedDependencies"; + String javaVersion = System.getProperty("java.version"); + + assertThatJson(json) + .node(resolvedDeps).isArray() + .anySatisfy(dep -> { + assertThatJson(dep).node("name").isEqualTo("JDK"); + assertThatJson(dep).node("annotations.version").isEqualTo(javaVersion); + }); + + assertThatJson(json) + .node(resolvedDeps).isArray() + .anySatisfy(dep -> assertThatJson(dep).node("name").isEqualTo("Maven")); + + assertThatJson(json) + .node(resolvedDeps).isArray() + .anySatisfy(dep -> assertThatJson(dep).node("uri").isString().startsWith("git+https://github.com/apache/commons-lang.git")); + } +} diff --git a/src/test/java/org/apache/commons/build/internal/MojoUtils.java b/src/test/java/org/apache/commons/build/internal/MojoUtils.java new file mode 100644 index 000000000..1ac3d25c7 --- /dev/null +++ b/src/test/java/org/apache/commons/build/internal/MojoUtils.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.build.internal; + +import org.codehaus.plexus.ContainerConfiguration; +import org.codehaus.plexus.DefaultContainerConfiguration; +import org.codehaus.plexus.DefaultPlexusContainer; +import org.codehaus.plexus.PlexusConstants; +import org.codehaus.plexus.PlexusContainer; +import org.codehaus.plexus.PlexusContainerException; +import org.codehaus.plexus.classworlds.ClassWorld; +import org.codehaus.plexus.component.repository.exception.ComponentLookupException; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositoryException; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.LocalRepository; +import org.eclipse.aether.repository.LocalRepositoryManager; +import org.eclipse.aether.repository.RepositoryPolicy; +import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory; + +import java.nio.file.Path; + +/** + * Utilities to instantiate Mojos in a test environment. + */ +public final class MojoUtils { + + private static ContainerConfiguration setupContainerConfiguration() { + ClassWorld classWorld = + new ClassWorld("plexus.core", Thread.currentThread().getContextClassLoader()); + return new DefaultContainerConfiguration() + .setClassWorld(classWorld) + .setClassPathScanning(PlexusConstants.SCANNING_INDEX) + .setAutoWiring(true) + .setName("maven"); + } + + public static PlexusContainer setupContainer() throws PlexusContainerException { + return new DefaultPlexusContainer(setupContainerConfiguration()); + } + + public static RepositorySystemSession createRepositorySystemSession( + PlexusContainer container, Path localRepositoryPath) throws ComponentLookupException, RepositoryException { + LocalRepositoryManagerFactory factory = container.lookup(LocalRepositoryManagerFactory.class, "simple"); + DefaultRepositorySystemSession repoSession = new DefaultRepositorySystemSession(); + LocalRepositoryManager manager = + factory.newInstance(repoSession, new LocalRepository(localRepositoryPath.toFile())); + repoSession.setLocalRepositoryManager(manager); + // Default policies + repoSession.setUpdatePolicy(RepositoryPolicy.UPDATE_POLICY_DAILY); + repoSession.setChecksumPolicy(RepositoryPolicy.CHECKSUM_POLICY_WARN); + return repoSession; + } + + private MojoUtils() {} +} diff --git a/src/test/resources/artifacts/artifact-jar.txt b/src/test/resources/artifacts/artifact-jar.txt new file mode 100644 index 000000000..103a21e64 --- /dev/null +++ b/src/test/resources/artifacts/artifact-jar.txt @@ -0,0 +1,2 @@ +// SPDX-License-Identifier: Apache-2.0 +A mock-up of a JAR file \ No newline at end of file diff --git a/src/test/resources/artifacts/artifact-pom.txt b/src/test/resources/artifacts/artifact-pom.txt new file mode 100644 index 000000000..48d1bbc32 --- /dev/null +++ b/src/test/resources/artifacts/artifact-pom.txt @@ -0,0 +1,2 @@ +// SPDX-License-Identifier: Apache-2.0 +A mock-up of a POM file \ No newline at end of file From d5296546e45f0707423f6e3b671b0bc5c8968baa Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Sun, 29 Mar 2026 20:19:28 +0200 Subject: [PATCH 3/8] Document Commons `buildType` --- src/site/markdown/slsa/v0.1.0.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/site/markdown/slsa/v0.1.0.md diff --git a/src/site/markdown/slsa/v0.1.0.md b/src/site/markdown/slsa/v0.1.0.md new file mode 100644 index 000000000..2462f6ae6 --- /dev/null +++ b/src/site/markdown/slsa/v0.1.0.md @@ -0,0 +1,21 @@ + +# SLSA Provenance Build Type + +This is a [SLSA Build Provenance](https://slsa.dev/spec/v1.2/build-provenance) build type to describe releases produced +by Apache Commons PMC release managers on their own equipment. + +## Build definition + +Artifacts are generated by a single Maven execution, usually of the form. + +```shell +mvn -Prelease deploy +``` + +### External parameters + +### Internal parameters + +### Resolved dependencies + +The build depends on two main tools installed on the release manager machine: JDK and \ No newline at end of file From 6ed242e38abed4395a2d27c8cf0d2dab074bcf1a Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Sun, 29 Mar 2026 20:51:40 +0200 Subject: [PATCH 4/8] fix: ignore `EI_EXPOSE_REP` Spotbugs warnings --- pom.xml | 2 +- src/conf/spotbugs-exclude-filter.xml | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/conf/spotbugs-exclude-filter.xml diff --git a/pom.xml b/pom.xml index f1e7ef5ae..64a976718 100644 --- a/pom.xml +++ b/pom.xml @@ -263,7 +263,7 @@ com.github.spotbugs spotbugs-maven-plugin - + ${basedir}/src/conf/spotbugs-exclude-filter.xml diff --git a/src/conf/spotbugs-exclude-filter.xml b/src/conf/spotbugs-exclude-filter.xml new file mode 100644 index 000000000..3d78d445b --- /dev/null +++ b/src/conf/spotbugs-exclude-filter.xml @@ -0,0 +1,24 @@ + + + + + + + From b03478fc9793aee212062e5e2248d7a678469c0e Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 30 Mar 2026 11:41:28 +0200 Subject: [PATCH 5/8] fix: PMD violations --- .../apache/commons/build/BuildAttestationMojo.java | 14 -------------- .../commons/build/BuildAttestationMojoTest.java | 2 -- 2 files changed, 16 deletions(-) diff --git a/src/main/java/org/apache/commons/build/BuildAttestationMojo.java b/src/main/java/org/apache/commons/build/BuildAttestationMojo.java index d021d2d8c..02cd3c3ff 100644 --- a/src/main/java/org/apache/commons/build/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/build/BuildAttestationMojo.java @@ -84,12 +84,6 @@ public class BuildAttestationMojo extends AbstractMojo { @Parameter(defaultValue = "${project.scm.connection}", readonly = true) private String scmConnectionUrl; - @Parameter(defaultValue = "${project.scm.developerConnection}", readonly = true) - private String scmDeveloperConnectionUrl; - - @Parameter(defaultValue = "${project.scm.tag}", readonly = true) - private String scmTag; - @Parameter(defaultValue = "${maven.home}", readonly = true) private File mavenHome; @@ -156,14 +150,6 @@ void setScmConnectionUrl(String scmConnectionUrl) { this.scmConnectionUrl = scmConnectionUrl; } - void setScmDeveloperConnectionUrl(String scmDeveloperConnectionUrl) { - this.scmDeveloperConnectionUrl = scmDeveloperConnectionUrl; - } - - void setScmTag(String scmTag) { - this.scmTag = scmTag; - } - void setMavenHome(File mavenHome) { this.mavenHome = mavenHome; } diff --git a/src/test/java/org/apache/commons/build/BuildAttestationMojoTest.java b/src/test/java/org/apache/commons/build/BuildAttestationMojoTest.java index 46ed89224..a119b956b 100644 --- a/src/test/java/org/apache/commons/build/BuildAttestationMojoTest.java +++ b/src/test/java/org/apache/commons/build/BuildAttestationMojoTest.java @@ -102,8 +102,6 @@ void attestationTest() throws Exception { mojo.setOutputDirectory(new File("target/attestations")); mojo.setScmDirectory(new File(".")); mojo.setScmConnectionUrl("scm:git:https://github.com/apache/commons-lang.git"); - mojo.setScmDeveloperConnectionUrl("scm:git:ssh://git@github.com/apache/commons-lang.git"); - mojo.setScmTag("tag"); mojo.setMavenHome(new File(System.getProperty("maven.home", "."))); mojo.execute(); From e617f28e063d1cd6ee4bc1c2a968220cb65b4225 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 30 Mar 2026 11:51:52 +0200 Subject: [PATCH 6/8] fix: PMD violations (2) --- .../org/apache/commons/build/internal/ArtifactUtils.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/apache/commons/build/internal/ArtifactUtils.java b/src/main/java/org/apache/commons/build/internal/ArtifactUtils.java index 9e1f047c4..30a538d73 100644 --- a/src/main/java/org/apache/commons/build/internal/ArtifactUtils.java +++ b/src/main/java/org/apache/commons/build/internal/ArtifactUtils.java @@ -16,15 +16,14 @@ */ package org.apache.commons.build.internal; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + import org.apache.commons.build.models.slsa.v1_2.ResourceDescriptor; import org.apache.commons.codec.digest.DigestUtils; import org.apache.maven.artifact.Artifact; import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.MojoFailureException; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; public final class ArtifactUtils { From f2308c6c1d3a4b6198d75ca876986d9417ab64af Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 30 Mar 2026 12:10:56 +0200 Subject: [PATCH 7/8] fix: missing Javadoc --- .../commons/build/BuildAttestationMojo.java | 19 ++++ .../commons/build/internal/ArtifactUtils.java | 31 +++++- .../commons/build/internal/GitUtils.java | 34 +++++++ .../build/models/slsa/v1_2/package-info.java | 94 +------------------ 4 files changed, 87 insertions(+), 91 deletions(-) diff --git a/src/main/java/org/apache/commons/build/BuildAttestationMojo.java b/src/main/java/org/apache/commons/build/BuildAttestationMojo.java index 02cd3c3ff..46ebd9f44 100644 --- a/src/main/java/org/apache/commons/build/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/build/BuildAttestationMojo.java @@ -124,6 +124,15 @@ public class BuildAttestationMojo extends AbstractMojo { */ private final MavenProjectHelper mavenProjectHelper; + /** + * Creates a new instance with the given dependencies. + * + * @param project A Maven project. + * @param scmManager A SCM manager. + * @param runtimeInformation Maven runtime information. + * @param session A Maven session. + * @param mavenProjectHelper A helper to attach artifacts to the project. + */ @Inject public BuildAttestationMojo(MavenProject project, ScmManager scmManager, RuntimeInformation runtimeInformation, MavenSession session, MavenProjectHelper mavenProjectHelper) { @@ -138,10 +147,20 @@ void setOutputDirectory(File outputDirectory) { this.outputDirectory = outputDirectory; } + /** + * Returns the SCM directory. + * + * @return The SCM directory. + */ public File getScmDirectory() { return scmDirectory; } + /** + * Sets the SCM directory. + * + * @param scmDirectory The SCM directory. + */ public void setScmDirectory(File scmDirectory) { this.scmDirectory = scmDirectory; } diff --git a/src/main/java/org/apache/commons/build/internal/ArtifactUtils.java b/src/main/java/org/apache/commons/build/internal/ArtifactUtils.java index 30a538d73..b5305a191 100644 --- a/src/main/java/org/apache/commons/build/internal/ArtifactUtils.java +++ b/src/main/java/org/apache/commons/build/internal/ArtifactUtils.java @@ -25,16 +25,32 @@ import org.apache.maven.artifact.Artifact; import org.apache.maven.plugin.MojoExecutionException; +/** + * Utilities to convert {@link Artifact} from and to other types. + */ public final class ArtifactUtils { private ArtifactUtils() { // prevent instantiation } + /** + * Returns the conventional filename for the given artifact. + * + * @param artifact A Maven artifact. + * @return A filename. + */ public static String getFileName(Artifact artifact) { return getFileName(artifact, artifact.getArtifactHandler().getExtension()); } + /** + * Returns the filename for the given artifact with a changed extension. + * + * @param artifact A Maven artifact. + * @param extension The file name extension. + * @return A filename. + */ public static String getFileName(Artifact artifact, String extension) { StringBuilder fileName = new StringBuilder(); fileName.append(artifact.getArtifactId()).append("-").append(artifact.getVersion()); @@ -45,6 +61,12 @@ public static String getFileName(Artifact artifact, String extension) { return fileName.toString(); } + /** + * Returns the Package URL corresponding to this artifact. + * + * @param artifact A maven artifact. + * @return A PURL for the given artifact. + */ public static String getPackageUrl(Artifact artifact) { StringBuilder sb = new StringBuilder(); sb.append("pkg:maven/").append(artifact.getGroupId()).append("/").append(artifact.getArtifactId()).append("@").append(artifact.getVersion()) @@ -57,7 +79,7 @@ public static String getPackageUrl(Artifact artifact) { return sb.toString(); } - public static Map getChecksums(Artifact artifact) throws IOException { + private static Map getChecksums(Artifact artifact) throws IOException { Map checksums = new HashMap<>(); DigestUtils digest = new DigestUtils(DigestUtils.getSha256Digest()); String sha256sum = digest.digestAsHex(artifact.getFile()); @@ -65,6 +87,13 @@ public static Map getChecksums(Artifact artifact) throws IOExcep return checksums; } + /** + * Converts a Maven artifact to a SLSA {@link ResourceDescriptor}. + * + * @param artifact A Maven artifact. + * @return A SLSA resource descriptor. + * @throws MojoExecutionException If an I/O error occurs retrieving the artifact. + */ public static ResourceDescriptor toResourceDescriptor(Artifact artifact) throws MojoExecutionException { ResourceDescriptor descriptor = new ResourceDescriptor(); descriptor.setName(getFileName(artifact)); diff --git a/src/main/java/org/apache/commons/build/internal/GitUtils.java b/src/main/java/org/apache/commons/build/internal/GitUtils.java index 01c987b0d..c5af8a39d 100644 --- a/src/main/java/org/apache/commons/build/internal/GitUtils.java +++ b/src/main/java/org/apache/commons/build/internal/GitUtils.java @@ -26,8 +26,18 @@ import java.nio.file.Paths; import java.security.MessageDigest; +/** + * Utilities for Git operations. + */ public final class GitUtils { + /** + * Returns the Git tree hash for the given directory. + * + * @param path A directory path. + * @return A hex-encoded SHA-1 tree hash. + * @throws IOException If the path is not a directory or an I/O error occurs. + */ public static String gitTree(Path path) throws IOException { if (!Files.isDirectory(path)) { throw new IOException("Path is not a directory: " + path); @@ -36,6 +46,13 @@ public static String gitTree(Path path) throws IOException { return Hex.encodeHexString(DigestUtils.gitTree(digest, path)); } + /** + * Returns the Git blob hash for the given file. + * + * @param path A regular file path. + * @return A hex-encoded SHA-1 blob hash. + * @throws IOException If the path is not a regular file or an I/O error occurs. + */ public static String gitBlob(Path path) throws IOException { if (!Files.isRegularFile(path)) { throw new IOException("Path is not a regular file: " + path); @@ -44,6 +61,14 @@ public static String gitBlob(Path path) throws IOException { return Hex.encodeHexString(DigestUtils.gitBlob(digest, path)); } + /** + * Converts an SCM URI to a download URI suffixed with the current branch name. + * + * @param scmUri A Maven SCM URI starting with {@code scm:git}. + * @param repositoryPath A path inside the Git repository. + * @return A download URI of the form {@code git+@}. + * @throws IOException If the current branch cannot be determined. + */ public static String scmToDownloadUri(String scmUri, Path repositoryPath) throws IOException { if (!scmUri.startsWith("scm:git")) { throw new IllegalArgumentException("Invalid scmUri: " + scmUri); @@ -52,6 +77,15 @@ public static String scmToDownloadUri(String scmUri, Path repositoryPath) throws return "git+" + scmUri.substring(8) + "@" + currentBranch; } + /** + * Returns the current branch name for the given repository path. + * + *

Returns the commit SHA if the repository is in a detached HEAD state. + * + * @param repositoryPath A path inside the Git repository. + * @return The current branch name, or the commit SHA for a detached HEAD. + * @throws IOException If the {@code .git} directory cannot be found or read. + */ public static String getCurrentBranch(Path repositoryPath) throws IOException { Path gitDir = findGitDir(repositoryPath); String head = new String(Files.readAllBytes(gitDir.resolve("HEAD")), StandardCharsets.UTF_8).trim(); diff --git a/src/main/java/org/apache/commons/build/models/slsa/v1_2/package-info.java b/src/main/java/org/apache/commons/build/models/slsa/v1_2/package-info.java index eb77ffe97..4b704fbce 100644 --- a/src/main/java/org/apache/commons/build/models/slsa/v1_2/package-info.java +++ b/src/main/java/org/apache/commons/build/models/slsa/v1_2/package-info.java @@ -18,99 +18,13 @@ /** * SLSA 1.2 Build Attestation Models. * - *

This package provides Jackson-annotated model classes that implement the Supply-chain Levels for Software Artifacts (SLSA) v1.2 - * specification. + *

This package provides Jackson-annotated model classes that implement the Supply-chain Levels for Software Artifacts + * (SLSA) v1.2 specification.

* *

Overview

* - *

SLSA is a framework for evaluating and improving the security posture of build systems. SLSA - * v1.2 defines a standard for recording build provenance - information about how software - * artifacts were produced. - * - *

Core Models

- * - *
    - *
  • {@link org.apache.commons.build.models.slsa.v1_2.Provenance} - Root object - * describing the build provenance. Contains BuildDefinition and RunDetails. - *
  • {@link org.apache.commons.build.models.slsa.v1_2.BuildDefinition} - Specifies - * the inputs that define the build, including build type, configuration, external - * parameters, and resolved dependencies. - *
  • {@link org.apache.commons.build.models.slsa.v1_2.RunDetails} - Specifies the - * details about the build invocation and environment, including the builder identity and - * build metadata. - *
- * - *

Supporting Models

- * - *
    - *
  • {@link org.apache.commons.build.models.slsa.v1_2.Builder} - Identifies the - * entity that executed the build. - *
  • {@link org.apache.commons.build.models.slsa.v1_2.BuildMetadata} - Contains - * metadata about the build invocation, including timing information. - *
  • {@link org.apache.commons.build.models.slsa.v1_2.ResourceDescriptor} - Describes - * an artifact or resource referenced in the build by URI and cryptographic digest. - *
- * - *

Usage Example

- * - *
- * // Create a builder
- * Builder builder = new Builder();
- * builder.setId("https://github.com/actions");
- * builder.setVersion("1.0");
- *
- * // Create build metadata
- * BuildMetadata buildMetadata = new BuildMetadata();
- * buildMetadata.setInvocationId("build-12345");
- * buildMetadata.setStartedOn(OffsetDateTime.now(ZoneOffset.UTC));
- * buildMetadata.setFinishedOn(OffsetDateTime.now(ZoneOffset.UTC));
- *
- * // Create run details
- * RunDetails runDetails = new RunDetails();
- * runDetails.setBuilder(builder);
- * runDetails.setMetadata(buildMetadata);
- *
- * // Create build definition
- * BuildDefinition buildDefinition = new BuildDefinition();
- * buildDefinition.setBuildType("https://github.com/actions");
- * buildDefinition.setExternalParameters(new HashMap<>());
- *
- * // Create provenance
- * Provenance provenance = new Provenance();
- * provenance.setBuildDefinition(buildDefinition);
- * provenance.setRunDetails(runDetails);
- *
- * // Serialize with Jackson
- * ObjectMapper mapper = new ObjectMapper();
- * String json = mapper.writeValueAsString(provenance);
- * 
- * - *

Jackson Integration

- * - *

All models use Jackson annotations for JSON serialization/deserialization: - * - *

    - *
  • {@code @JsonProperty} - Maps field names to JSON properties - *
  • {@code @JsonInclude} - Controls inclusion of null/empty values in serialization - *
  • {@code @JsonFormat} - Specifies date/time formatting - *
- * - *

The models are compatible with Jackson's ObjectMapper and support both serialization to - * JSON and deserialization from JSON. - * - *

Validation

- * - *

Some models include Jakarta Validation annotations: - * - *

    - *
  • {@code @NotBlank} - Ensures required string fields are not empty - *
- * - *

Users can enable validation using a Jakarta Validation provider to ensure provenance - * integrity. - * - *

Reference

+ *

SLSA is a framework for evaluating and improving the security posture of build systems. SLSA v1.2 defines a standard for recording build provenance: + * information about how software artifacts were produced.

* * @see SLSA v1.2 Specification * @see In-toto Attestation Framework From 2278e8c4d4a299c4b1cb84ec65af9a2710201a9d Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 30 Mar 2026 12:11:45 +0200 Subject: [PATCH 8/8] fix: remove unused `gitBlob` method --- .../apache/commons/build/internal/GitUtils.java | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/main/java/org/apache/commons/build/internal/GitUtils.java b/src/main/java/org/apache/commons/build/internal/GitUtils.java index c5af8a39d..6e99d2958 100644 --- a/src/main/java/org/apache/commons/build/internal/GitUtils.java +++ b/src/main/java/org/apache/commons/build/internal/GitUtils.java @@ -46,21 +46,6 @@ public static String gitTree(Path path) throws IOException { return Hex.encodeHexString(DigestUtils.gitTree(digest, path)); } - /** - * Returns the Git blob hash for the given file. - * - * @param path A regular file path. - * @return A hex-encoded SHA-1 blob hash. - * @throws IOException If the path is not a regular file or an I/O error occurs. - */ - public static String gitBlob(Path path) throws IOException { - if (!Files.isRegularFile(path)) { - throw new IOException("Path is not a regular file: " + path); - } - MessageDigest digest = DigestUtils.getSha1Digest(); - return Hex.encodeHexString(DigestUtils.gitBlob(digest, path)); - } - /** * Converts an SCM URI to a download URI suffixed with the current branch name. *