diff --git a/checkstyle.xml b/checkstyle.xml index 8f329d35b..0f5a18551 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -185,7 +185,7 @@ - + diff --git a/fb-excludes.xml b/fb-excludes.xml index 2cba28121..0a0a38438 100644 --- a/fb-excludes.xml +++ b/fb-excludes.xml @@ -18,6 +18,11 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubusercontent.com/spotbugs/spotbugs/3.1.0/spotbugs/etc/findbugsfilter.xsd"> + + + + + diff --git a/pom.xml b/pom.xml index faa5b1ae0..d07c6218a 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,8 @@ commons-release-plugin maven-plugin - 1.9.3-SNAPSHOT + + 1.9.3.slsa-SNAPSHOT Apache Commons Release Plugin Apache Maven Mojo for Apache Commons Release tasks. @@ -113,7 +114,22 @@ true true + + 2.21.1 + 2.21 + 2.0.17 + + + + org.slf4j + slf4j-bom + ${commons.slf4j.version} + pom + import + + + org.apache.commons @@ -151,6 +167,18 @@ maven-scm-api ${maven-scm.version} + + org.apache.maven.scm + maven-scm-manager-plexus + ${maven-scm.version} + compile + + + org.apache.maven.scm + maven-scm-provider-gitexe + ${maven-scm.version} + runtime + org.apache.maven.scm maven-scm-provider-svnexe @@ -171,6 +199,22 @@ commons-compress 1.28.0 + + com.fasterxml.jackson.core + jackson-databind + ${commons.jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${commons.jackson.annotations.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${commons.jackson.version} + runtime + org.apache.maven.plugin-testing maven-plugin-testing-harness @@ -188,11 +232,28 @@ junit-jupiter test + + net.javacrumbs.json-unit + json-unit-assertj + 2.40.1 + test + + + org.junit.jupiter + junit-jupiter-api + test + org.junit.vintage junit-vintage-engine test + + org.mockito + mockito-core + 4.11.0 + test + org.apache.maven @@ -223,6 +284,11 @@ + + org.slf4j + slf4j-simple + test + clean verify apache-rat:check checkstyle:check spotbugs:check javadoc:javadoc site diff --git a/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java new file mode 100644 index 000000000..7f07244e2 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java @@ -0,0 +1,118 @@ +/* + * 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.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.release.plugin.slsa.v1_2.ResourceDescriptor; +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 { + + /** No instances. */ + 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()); + if (artifact.getClassifier() != null) { + fileName.append("-").append(artifact.getClassifier()); + } + fileName.append(".").append(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()) + .append("?"); + String classifier = artifact.getClassifier(); + if (classifier != null) { + sb.append("classifier=").append(classifier).append("&"); + } + sb.append("type=").append(artifact.getType()); + return sb.toString(); + } + + /** + * Returns a map of checksum algorithm names to hex-encoded digest values for the given artifact file. + * + * @param artifact A Maven artifact. + * @return A map of checksum algorithm names to hex-encoded digest values. + * @throws IOException If an I/O error occurs reading the artifact file. + */ + private 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; + } + + /** + * 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)); + 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/release/plugin/internal/BuildToolDescriptors.java b/src/main/java/org/apache/commons/release/plugin/internal/BuildToolDescriptors.java new file mode 100644 index 000000000..5dac9a192 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/internal/BuildToolDescriptors.java @@ -0,0 +1,101 @@ +/* + * 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.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.apache.commons.release.plugin.slsa.v1_2.ResourceDescriptor; + +/** + * Factory methods for {@link ResourceDescriptor} instances representing build-tool dependencies. + */ +public final class BuildToolDescriptors { + + /** No instances. */ + 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.version.date", + "java.vendor", "java.vendor.url", "java.vendor.version", + "java.home", + "java.vm.specification.version", "java.vm.specification.vendor", "java.vm.specification.name", + "java.vm.version", "java.vm.vendor", "java.vm.name", + "java.specification.version", "java.specification.maintenance.version", + "java.specification.vendor", "java.specification.name", + }; + 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. + * + *

{@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); + Map digest = new HashMap<>(); + digest.put("gitTree", GitUtils.gitTree(mavenHome)); + descriptor.setDigest(digest); + Properties buildProps = new Properties(); + try (InputStream in = coreClassLoader.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/release/plugin/internal/GitUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java new file mode 100644 index 000000000..246027c49 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java @@ -0,0 +1,117 @@ +/* + * 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.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; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; + +/** + * Utilities for Git operations. + */ +public final class GitUtils { + + /** The SCM URI prefix for Git repositories. */ + private static final String SCM_GIT_PREFIX = "scm:git:"; + + /** + * 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); + } + MessageDigest digest = DigestUtils.getSha1Digest(); + return Hex.encodeHexString(DigestUtils.gitTree(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_PREFIX)) { + throw new IllegalArgumentException("Invalid scmUri: " + scmUri); + } + String currentBranch = getCurrentBranch(repositoryPath); + return "git+" + scmUri.substring(SCM_GIT_PREFIX.length()) + "@" + 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(); + 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 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; + } + + /** + * Gets map of external build parameters captured from the current JVM and Maven session. + * + * @return A map of parameter names to values. + */ + 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; + } + + /** + * Reconstructs the Maven command line string from the given execution request. + * + * @param request The Maven execution request. + * @return A string representation of the Maven command line. + */ + private String getCommandLine(final 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(); + } + + /** + * Removes the last character from the given {@link StringBuilder} if it is non-empty. + * + * @param sb The string builder to trim. + */ + private static void removeLast(final StringBuilder sb) { + if (sb.length() > 0) { + sb.setLength(sb.length() - 1); + } + } + + /** + * Returns resource descriptors for the JVM, Maven installation, SCM source, and project dependencies. + * + * @return A list of resolved build dependencies. + * @throws MojoExecutionException If any dependency cannot be resolved or hashed. + */ + 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(), + runtimeInformation.getClass().getClassLoader())); + dependencies.add(getScmDescriptor()); + } catch (IOException e) { + throw new MojoExecutionException(e); + } + dependencies.addAll(getProjectDependencies()); + return dependencies; + } + + /** + * Returns resource descriptors for all resolved project dependencies. + * + * @return A list of resource descriptors for the project's resolved artifacts. + * @throws MojoExecutionException If a dependency artifact cannot be described. + */ + private List getProjectDependencies() throws MojoExecutionException { + List dependencies = new ArrayList<>(); + for (Artifact artifact : project.getArtifacts()) { + dependencies.add(ArtifactUtils.toResourceDescriptor(artifact)); + } + return dependencies; + } + + /** + * Returns a resource descriptor for the current SCM source, including the URI and Git commit digest. + * + * @return A resource descriptor for the SCM source. + * @throws IOException If the current branch cannot be determined. + * @throws MojoExecutionException If the SCM revision cannot be retrieved. + */ + 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; + } + + /** + * Creates and returns an SCM repository from the configured connection URL. + * + * @return The SCM repository. + * @throws MojoExecutionException If the SCM repository cannot be created. + */ + private ScmRepository getScmRepository() throws MojoExecutionException { + try { + return scmManager.makeScmRepository(scmConnectionUrl); + } catch (ScmException e) { + throw new MojoExecutionException("Failed to create SCM repository", e); + } + } + + /** + * Returns the current SCM revision (commit hash) for the configured SCM directory. + * + * @return The current SCM revision string. + * @throws MojoExecutionException If the revision cannot be retrieved from SCM. + */ + 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); + } + } + + /** + * Extracts the revision string from an SCM info result. + * + * @param result The SCM info result. + * @return The revision string. + * @throws MojoExecutionException If the result is unsuccessful or contains no revision. + */ + private String getScmRevision(final 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; + } + + /** + * Returns build metadata derived from the current Maven session, including start and finish timestamps. + * + * @return The build metadata. + */ + 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/release/plugin/slsa/v1_2/BuildDefinition.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java new file mode 100644 index 000000000..843bc0e17 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java @@ -0,0 +1,173 @@ +/* + * 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.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. + * + *

Specifies 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 Map externalParameters = new HashMap<>(); + + /** Parameters set by the build platform. */ + @JsonProperty("internalParameters") + private Map internalParameters = new HashMap<>(); + + /** Artifacts the build depends on, specified by URI and digest. */ + @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/release/plugin/slsa/v1_2/BuildMetadata.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildMetadata.java new file mode 100644 index 000000000..345eb91ee --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildMetadata.java @@ -0,0 +1,140 @@ +/* + * 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.time.OffsetDateTime; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * 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 { + + /** Identifier for this build invocation. */ + @JsonProperty("invocationId") + private String invocationId; + + /** Timestamp when the build started. */ + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'") + @JsonProperty("startedOn") + private OffsetDateTime startedOn; + + /** Timestamp when the build completed. */ + @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/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 List builderDependencies = new ArrayList<>(); + + /** Map of build platform component names to their versions. */ + @JsonProperty("version") + private Map version = new HashMap<>(); + + /** 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/release/plugin/slsa/v1_2/Provenance.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Provenance.java new file mode 100644 index 000000000..884246006 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Provenance.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. + */ +package org.apache.commons.release.plugin.slsa.v1_2; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * 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"; + + /** 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 Map digest; + + /** Raw contents of the resource, base64-encoded in JSON. */ + @JsonProperty("content") + private byte[] content; + + /** Download URI for the resource, if different from {@link #uri}. */ + @JsonProperty("downloadLocation") + private String downloadLocation; + + /** Media type of the resource. */ + @JsonProperty("mediaType") + private String mediaType; + + /** Additional key-value metadata about the resource. */ + @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/release/plugin/slsa/v1_2/RunDetails.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java new file mode 100644 index 000000000..ffb118677 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java @@ -0,0 +1,137 @@ +/* + * 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.List; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * 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 { + + /** Entity that executed the build. */ + @JsonProperty("builder") + private Builder builder; + + /** Metadata about the build invocation. */ + @JsonProperty("metadata") + private BuildMetadata metadata; + + /** Artifacts produced as a side effect of the build. */ + @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/release/plugin/slsa/v1_2/Statement.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java new file mode 100644 index 000000000..88aeb8ae8 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java @@ -0,0 +1,122 @@ +/* + * 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.List; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * 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 { + + /** The in-toto statement schema URI. */ + @JsonProperty("_type") + public static final String TYPE = "https://in-toto.io/Statement/v1"; + + /** Software artifacts that the attestation applies to. */ + @JsonProperty("subject") + private List subject; + + /** URI identifying the type of the predicate. */ + @JsonProperty("predicateType") + private String predicateType; + + /** The provenance predicate. */ + @JsonProperty("predicate") + private Provenance predicate; + + /** Creates a new Statement instance. */ + public Statement() { + } + + /** + * 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/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.

+ * + *

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.

+ * + * @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