{@code build.properties} resides in a JAR inside {@code ${maven.home}/lib/}, which is loaded by Maven's Core Classloader. + * Plugin code runs in an isolated Plugin Classloader, which does see that resources. Therefore, we need to pass the classloader from a class from + * Maven Core, such as {@link org.apache.maven.rtinfo.RuntimeInformation}.
+ * + * @param version Maven version string + * @param mavenHome path to the Maven home directory + * @param coreClassLoader a classloader from Maven's Core Classloader realm, used to load core resources + * @return a descriptor for the Maven installation + * @throws IOException if hashing the Maven home directory fails + */ + public static ResourceDescriptor maven(String version, Path mavenHome, ClassLoader coreClassLoader) throws IOException { + ResourceDescriptor descriptor = new ResourceDescriptor(); + descriptor.setName("Maven"); + descriptor.setUri("pkg:maven/org.apache.maven/apache-maven@" + version); + MapReturns 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(); + if (head.startsWith("ref: refs/heads/")) { + return head.substring("ref: refs/heads/".length()); + } + // detached HEAD — return the commit SHA + return head; + } + + /** + * Walks up the directory tree from {@code path} to find the {@code .git} directory. + * + * @param path A path inside the Git repository. + * @return The path to the {@code .git} directory (or file for worktrees). + * @throws IOException If no {@code .git} directory is found. + */ + 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); + } + + /** No instances. */ + private GitUtils() { + // no instantiation + } +} diff --git a/src/main/java/org/apache/commons/release/plugin/internal/package-info.java b/src/main/java/org/apache/commons/release/plugin/internal/package-info.java new file mode 100644 index 000000000..9218ebff4 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/internal/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * Internal utilities for the commons-release-plugin. + * + *
Should not be referenced by external artifacts.
+ */ +package org.apache.commons.release.plugin.internal; diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java new file mode 100644 index 000000000..254779981 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -0,0 +1,458 @@ +/* + * 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.release.plugin.mojos; + +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.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.apache.commons.release.plugin.internal.ArtifactUtils; +import org.apache.commons.release.plugin.internal.BuildToolDescriptors; +import org.apache.commons.release.plugin.internal.GitUtils; +import org.apache.commons.release.plugin.slsa.v1_2.BuildDefinition; +import org.apache.commons.release.plugin.slsa.v1_2.BuildMetadata; +import org.apache.commons.release.plugin.slsa.v1_2.Builder; +import org.apache.commons.release.plugin.slsa.v1_2.Provenance; +import org.apache.commons.release.plugin.slsa.v1_2.ResourceDescriptor; +import org.apache.commons.release.plugin.slsa.v1_2.RunDetails; +import org.apache.commons.release.plugin.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 { + + /** The file extension for in-toto attestation files. */ + private static final String ATTESTATION_EXTENSION = "intoto.jsonl"; + + /** Shared Jackson object mapper for serializing attestation statements. */ + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + static { + OBJECT_MAPPER.findAndRegisterModules(); + OBJECT_MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + OBJECT_MAPPER.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); + } + + /** The SCM connection URL for the current project. */ + @Parameter(defaultValue = "${project.scm.connection}", readonly = true) + private String scmConnectionUrl; + + /** The Maven home directory. */ + @Parameter(defaultValue = "${maven.home}", readonly = true) + private File mavenHome; + + /** + * Issue SCM actions at this local directory. + */ + @Parameter(property = "commons.release.scmDirectory", defaultValue = "${basedir}") + private File scmDirectory; + + /** The output directory for the attestation file. */ + @Parameter(property = "commons.release.outputDirectory", defaultValue = "${project.build.directory}") + private File outputDirectory; + + /** Whether to skip attaching the attestation artifact to the project. */ + @Parameter(property = "commons.release.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; + + /** + * 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) { + this.project = project; + this.scmManager = scmManager; + this.runtimeInformation = runtimeInformation; + this.session = session; + this.mavenProjectHelper = mavenProjectHelper; + } + + /** + * Sets the output directory for the attestation file. + * + * @param outputDirectory The output directory. + */ + void setOutputDirectory(final 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; + } + + /** + * Sets the public SCM connection URL. + * + * @param scmConnectionUrl The SCM connection URL. + */ + void setScmConnectionUrl(final String scmConnectionUrl) { + this.scmConnectionUrl = scmConnectionUrl; + } + + /** + * Sets the Maven home directory. + * + * @param mavenHome The Maven home directory. + */ + void setMavenHome(final File mavenHome) { + this.mavenHome = mavenHome; + } + + @Override + public void execute() throws MojoFailureException, MojoExecutionException { + // 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); + } + + /** + * Serializes the attestation statement to a file and optionally attaches it to the project. + * + * @param statement The attestation statement to write. + * @throws MojoExecutionException If the output directory cannot be created or the file cannot be written. + */ + private void writeStatement(final 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.writeValue(os, statement); + os.write('\n'); + } 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. + * @throws MojoExecutionException If artifact hashing fails. + */ + private ListSpecifies everything that influenced the build output. Together with {@link RunDetails}, it forms the complete + * {@link Provenance} record.
+ * + * @see SLSA v1.2 Specification + */ +public class BuildDefinition { + + /** URI indicating what type of build was performed. */ + @JsonProperty("buildType") + private String buildType = "https://commons.apache.org/builds/0.1.0"; + + /** Inputs passed to the build. */ + @JsonProperty("externalParameters") + private MapDetermines 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 MapConsidered incomplete unless resolved materials are present.
+ * + * @return the list of resolved dependencies, or {@code null} if not set + */ + public ListUseful 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/release/plugin/slsa/v1_2/Builder.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java new file mode 100644 index 000000000..36e0f1a89 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java @@ -0,0 +1,125 @@ +/* + * 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.release.plugin.slsa.v1_2; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * 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 { + + /** Identifier URI of the builder. */ + @JsonProperty("id") + private String id = "https://commons.apache.org/builds/0.1.0"; + + /** Orchestrator dependencies that may affect provenance generation. */ + @JsonProperty("builderDependencies") + private ListCombines 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"; + + /** Inputs that defined the build. */ + @JsonProperty("buildDefinition") + private BuildDefinition buildDefinition; + + /** Details about the build invocation. */ + @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/release/plugin/slsa/v1_2/ResourceDescriptor.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/ResourceDescriptor.java new file mode 100644 index 000000000..55333f220 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/ResourceDescriptor.java @@ -0,0 +1,227 @@ +/* + * 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.release.plugin.slsa.v1_2; + +import java.util.Map; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * 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 { + + /** Human-readable name of the resource. */ + @JsonProperty("name") + private String name; + + /** URI identifying the resource. */ + @JsonProperty("uri") + private String uri; + + /** Map of digest algorithm names to hex-encoded values. */ + @JsonProperty("digest") + private MapCommon keys include {@code "sha256"} and {@code "sha512"}.
+ * + * @return the digest map, or {@code null} if not set + */ + public MapTrusted 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 ListEach 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 ListUnset 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/release/plugin/slsa/v1_2/package-info.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/package-info.java new file mode 100644 index 000000000..69a5ce287 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/package-info.java @@ -0,0 +1,34 @@ +/* + * 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.
+ * + *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 + * @see Jackson JSON processor + */ +package org.apache.commons.release.plugin.slsa.v1_2; + 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..b9a569ef2 --- /dev/null +++ b/src/site/markdown/slsa/v0.1.0.md @@ -0,0 +1,131 @@ + + +# Build Type: Apache Commons Maven Release + +```jsonc +"buildType": "https://commons.apache.org/proper/commons-release-plugin/slsa/v0.1.0" +``` + +This is a [SLSA Build Provenance](https://slsa.dev/spec/v1.2/build-provenance) build type +that describes releases produced by Apache Commons PMC release managers running Maven on their own equipment. + +## Build definition + +Artifacts are generated by a single Maven execution, typically of the form: + +```shell +mvn -Prelease deploy +``` + +The provenance is recorded by the `build-attestation` goal of the +`commons-release-plugin`, which runs in the `verify` phase. + +### External parameters + +External parameters capture everything supplied by the release manager at invocation time. +All parameters are captured from the running Maven session. + +| Parameter | Type | Description | +|-------------------------|----------|-------------------------------------------------------------------------| +| `maven.goals` | string[] | The list of Maven goals passed on the command line (e.g. `["deploy"]`). | +| `maven.profiles` | string[] | The list of active profiles passed via `-P` (e.g. `["release"]`). | +| `maven.user.properties` | object | User-defined properties passed via `-D` flags. | +| `maven.cmdline` | string | The reconstructed Maven command line. | +| `jvm.args` | string[] | JVM input arguments. | +| `env` | object | A filtered subset of environment variables: `TZ` and locale variables. | + +### Internal parameters + +No internal parameters are recorded for this build type. + +### Resolved dependencies + +The `resolvedDependencies` list captures all inputs that contributed to the build output. +It always contains the following entries, in order: + +#### JDK + +Represents the Java Development Kit used to run Maven (`"name": "JDK"`). +To allow verification of the JDK's integrity, a `gitTree` digest is computed over the `java.home` directory. + +The following annotations are recorded from [ +`System.getProperties()`](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/System.html#getProperties()): + +| Annotation key | System property | Description | +|-------------------------------------|------------------------------------------|--------------------------------------------------------------------------| +| `version` | `java.version` | Java Runtime Environment version. | +| `version.date` | `java.version.date` | Java Runtime Environment version date, in ISO-8601 YYYY-MM-DD format. | +| `vendor` | `java.vendor` | Java Runtime Environment vendor. | +| `vendor.url` | `java.vendor.url` | Java vendor URL. | +| `vendor.version` | `java.vendor.version` | Java vendor version _(optional)_. | +| `home` | `java.home` | Java installation directory. | +| `vm.specification.version` | `java.vm.specification.version` | Java Virtual Machine specification version. | +| `vm.specification.vendor` | `java.vm.specification.vendor` | Java Virtual Machine specification vendor. | +| `vm.specification.name` | `java.vm.specification.name` | Java Virtual Machine specification name. | +| `vm.version` | `java.vm.version` | Java Virtual Machine implementation version. | +| `vm.vendor` | `java.vm.vendor` | Java Virtual Machine implementation vendor. | +| `vm.name` | `java.vm.name` | Java Virtual Machine implementation name. | +| `specification.version` | `java.specification.version` | Java Runtime Environment specification version. | +| `specification.maintenance.version` | `java.specification.maintenance.version` | Java Runtime Environment specification maintenance version _(optional)_. | +| `specification.vendor` | `java.specification.vendor` | Java Runtime Environment specification vendor. | +| `specification.name` | `java.specification.name` | Java Runtime Environment specification name. | + +#### Maven + +Represents the Maven installation used to run the build (`"name": "Maven"`). +To allow verification of the installation's integrity, a `gitTree` hash is computed over the `maven.home` directory. + +The `uri` key contains the Package URL of the Maven distribution, as published to Maven Central. + +The following annotations are sourced from Maven's `build.properties`, bundled inside the Maven distribution. +They are only present if the resource is accessible from Maven's Core Classloader at runtime. + +| Annotation key | Description | +|-------------------------|--------------------------------------------------------------| +| `distributionId` | The ID of the Maven distribution. | +| `distributionName` | The full name of the Maven distribution. | +| `distributionShortName` | The short name of the Mavendistribution. | +| `buildNumber` | The Git commit hash from which this Maven release was built. | +| `version` | The Maven version string. | + +#### Source repository + +Represents the source code being built. +The URI follows +the [SPDX Download Location](https://spdx.github.io/spdx-spec/v2.3/package-information/#77-package-download-location-field) +format. + +#### Project dependencies + +One entry per resolved Maven dependency (compile + runtime scope), as declared in the project's POM. +These are appended after the build tool entries above. + +| Field | Value | +|-----------------|-----------------------------------------------------| +| `name` | Artifact filename, e.g. `commons-lang3-3.14.0.jar`. | +| `uri` | Package URL. | +| `digest.sha256` | SHA-256 hex digest of the artifact file on disk. | + +## Run details + +### Builder + +The `builder.id` is always `https://commons.apache.org/builds/0.1.0`. +It represents the commons-release-plugin acting as the build platform. + +## Subjects + +The attestation covers all artifacts attached to the Maven project at the time the `verify` phase runs: +the primary artifact (e.g. the JAR) and any attached artifacts (e.g. sources JAR, javadoc JAR, POM). + +| Field | Value | +|-----------------|------------------------------------------| +| `name` | Artifact filename. | +| `uri` | Package URL. | +| `digest.sha256` | SHA-256 hex digest of the artifact file. | + +## Version history + +### v0.1.0 + +Initial version. \ No newline at end of file diff --git a/src/test/java/org/apache/commons/release/plugin/internal/MojoUtils.java b/src/test/java/org/apache/commons/release/plugin/internal/MojoUtils.java new file mode 100644 index 000000000..6a6c3f5bf --- /dev/null +++ b/src/test/java/org/apache/commons/release/plugin/internal/MojoUtils.java @@ -0,0 +1,71 @@ +/* + * 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.release.plugin.internal; + +import java.nio.file.Path; + +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; + +/** + * 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/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java new file mode 100644 index 000000000..dae20c43c --- /dev/null +++ b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java @@ -0,0 +1,129 @@ +/* + * 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.release.plugin.mojos; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Date; + +import org.apache.commons.release.plugin.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.setMavenHome(new File(System.getProperty("maven.home", "."))); + mojo.execute(); + + Artifact attestation = project.getAttachedArtifacts().stream() + .filter(a -> "intoto.jsonl".equals(a.getType())) + .findFirst() + .orElseThrow(() -> new AssertionError("No intoto.jsonl 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/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