This is an automated email from the ASF dual-hosted git repository.

pkarwasz pushed a commit to branch feat/slsa
in repository https://gitbox.apache.org/repos/asf/commons-release-plugin.git

commit d3094209f67f2a05d268956849c76d27ec5df41e
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Mon Mar 30 16:47:37 2026 +0200

    Add `build-attestation` target
    
    This PR was moved from apache/commons-build-plugin#417
    
    It adds a goal to generate a [SLSA](https://slsa.dev/) build attestation 
and attaches it to the build as a file with the `.intoto.json` extension.
    
    The attestation records the following information about the build 
environment:
    
    - The Java version used (vendor, version string)
    - The Maven version used
    - The `gitTree` hash of the unpacked Java distribution
    - The `gitTree` hash of the unpacked Maven distribution
---
 checkstyle.xml                                     |   2 +-
 fb-excludes.xml                                    |   5 +
 pom.xml                                            |  48 +++
 .../release/plugin/internal/ArtifactUtils.java     | 110 ++++++
 .../plugin/internal/BuildToolDescriptors.java      |  88 +++++
 .../commons/release/plugin/internal/GitUtils.java  | 109 ++++++
 .../release/plugin/internal/package-info.java      |  23 ++
 .../release/plugin/mojos/BuildAttestationMojo.java | 374 +++++++++++++++++++++
 .../release/plugin/slsa/v1_2/BuildDefinition.java  | 173 ++++++++++
 .../release/plugin/slsa/v1_2/BuildMetadata.java    | 140 ++++++++
 .../commons/release/plugin/slsa/v1_2/Builder.java  | 125 +++++++
 .../release/plugin/slsa/v1_2/Provenance.java       | 120 +++++++
 .../plugin/slsa/v1_2/ResourceDescriptor.java       | 227 +++++++++++++
 .../release/plugin/slsa/v1_2/RunDetails.java       | 137 ++++++++
 .../release/plugin/slsa/v1_2/Statement.java        | 131 ++++++++
 .../release/plugin/slsa/v1_2/package-info.java     |  34 ++
 .../commons/release/plugin/internal/MojoUtils.java |  71 ++++
 .../plugin/mojos/BuildAttestationMojoTest.java     | 129 +++++++
 src/test/resources/artifacts/artifact-jar.txt      |   2 +
 src/test/resources/artifacts/artifact-pom.txt      |   2 +
 20 files changed, 2049 insertions(+), 1 deletion(-)

diff --git a/checkstyle.xml b/checkstyle.xml
index 8f329d3..0f5a185 100644
--- a/checkstyle.xml
+++ b/checkstyle.xml
@@ -185,7 +185,7 @@
     <module name="UpperEll" />
     <module name="ImportOrder">
       <property name="option" value="top"/>
-      <property name="groups" value="java,javax,org"/>
+      <property name="groups" value="java,javax"/>
       <property name="ordered" value="true"/>
       <property name="separated" value="true"/>
     </module>
diff --git a/fb-excludes.xml b/fb-excludes.xml
index 2cba281..0a0a384 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";>
 
+  <!-- Mutable objects are not passed to untrusted methods, so we exclude 
these checks -->
+  <Match>
+    <Bug pattern="EI_EXPOSE_REP,EI_EXPOSE_REP2" />
+  </Match>
+
   <!-- Omit junit tests -->
   <Match>
     <Class name="~.*\.*Test.*"/>
diff --git a/pom.xml b/pom.xml
index faa5b1a..e3e5c8d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -113,6 +113,9 @@
     <!-- Until Maven plugins used here don't fail the Moditect plugin -->
     <moditect.skip>true</moditect.skip>
     <japicmp.skip>true</japicmp.skip>
+    <!-- Dependency versions -->
+    <commons.jackson.version>2.21.1</commons.jackson.version>
+    
<commons.jackson.annotations.version>2.21</commons.jackson.annotations.version>
   </properties>
   <dependencies>
     <dependency>
@@ -151,6 +154,18 @@
       <artifactId>maven-scm-api</artifactId>
       <version>${maven-scm.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.apache.maven.scm</groupId>
+      <artifactId>maven-scm-manager-plexus</artifactId>
+      <version>${maven-scm.version}</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.scm</groupId>
+      <artifactId>maven-scm-provider-gitexe</artifactId>
+      <version>${maven-scm.version}</version>
+      <scope>runtime</scope>
+    </dependency>
     <dependency>
       <groupId>org.apache.maven.scm</groupId>
       <artifactId>maven-scm-provider-svnexe</artifactId>
@@ -171,6 +186,22 @@
       <artifactId>commons-compress</artifactId>
       <version>1.28.0</version>
     </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+      <version>${commons.jackson.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-annotations</artifactId>
+      <version>${commons.jackson.annotations.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.datatype</groupId>
+      <artifactId>jackson-datatype-jsr310</artifactId>
+      <version>${commons.jackson.version}</version>
+      <scope>runtime</scope>
+    </dependency>
     <dependency>
       <groupId>org.apache.maven.plugin-testing</groupId>
       <artifactId>maven-plugin-testing-harness</artifactId>
@@ -188,11 +219,28 @@
       <artifactId>junit-jupiter</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>net.javacrumbs.json-unit</groupId>
+      <artifactId>json-unit-assertj</artifactId>
+      <version>2.40.1</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-api</artifactId>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.junit.vintage</groupId>
       <artifactId>junit-vintage-engine</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <version>4.11.0</version>
+      <scope>test</scope>
+    </dependency>
     <!--  A bit of jar-hell requires this to come last. -->
     <dependency>
       <groupId>org.apache.maven</groupId>
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 0000000..4996206
--- /dev/null
+++ 
b/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java
@@ -0,0 +1,110 @@
+/*
+ * 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 {
+
+    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();
+    }
+
+    private static Map<String, String> getChecksums(Artifact artifact) throws 
IOException {
+        Map<String, String> 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 0000000..5e66928
--- /dev/null
+++ 
b/src/main/java/org/apache/commons/release/plugin/internal/BuildToolDescriptors.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.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 {
+
+    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<String, String> digest = new HashMap<>();
+        digest.put("gitTree", GitUtils.gitTree(javaHome));
+        descriptor.setDigest(digest);
+        String[] propertyNames = {"java.version", "java.vendor", 
"java.vendor.version", "java.vm.name", "java.vm.version", "java.vm.vendor",
+                "java.runtime.name", "java.runtime.version", 
"java.specification.version"};
+        Map<String, Object> annotations = new HashMap<>();
+        for (String prop : propertyNames) {
+            annotations.put(prop.substring("java.".length()), 
System.getProperty(prop));
+        }
+        descriptor.setAnnotations(annotations);
+        return descriptor;
+    }
+
+    /**
+     * Creates a {@link ResourceDescriptor} for the Maven installation used 
during the build.
+     *
+     * @param version   Maven version string
+     * @param mavenHome path to the Maven home directory
+     * @return a descriptor for the Maven installation
+     * @throws IOException if hashing the Maven home directory fails
+     */
+    public static ResourceDescriptor maven(String version, Path mavenHome) 
throws IOException {
+        ResourceDescriptor descriptor = new ResourceDescriptor();
+        descriptor.setName("Maven");
+        descriptor.setUri("pkg:maven/org.apache.maven/apache-maven@" + 
version);
+        Map<String, String> digest = new HashMap<>();
+        digest.put("gitTree", GitUtils.gitTree(mavenHome));
+        descriptor.setDigest(digest);
+        Properties buildProps = new Properties();
+        try (InputStream in = 
BuildToolDescriptors.class.getResourceAsStream("/org/apache/maven/messages/build.properties"))
 {
+            if (in != null) {
+                buildProps.load(in);
+            }
+        }
+        if (!buildProps.isEmpty()) {
+            Map<String, Object> 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 0000000..ec9a77c
--- /dev/null
+++ b/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java
@@ -0,0 +1,109 @@
+/*
+ * 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+<url>@<branch>}.
+     * @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.
+     *
+     * <p>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;
+    }
+
+    private static Path findGitDir(Path path) throws IOException {
+        Path current = path.toAbsolutePath();
+        while (current != null) {
+            Path candidate = current.resolve(".git");
+            if (Files.isDirectory(candidate)) {
+                return candidate;
+            }
+            if (Files.isRegularFile(candidate)) {
+                // git worktree: .git is a file containing "gitdir: 
/path/to/real/.git"
+                String content = new String(Files.readAllBytes(candidate), 
StandardCharsets.UTF_8).trim();
+                if (content.startsWith("gitdir: ")) {
+                    return Paths.get(content.substring("gitdir: ".length()));
+                }
+            }
+            current = current.getParent();
+        }
+        throw new IOException("No .git directory found above: " + path);
+    }
+
+    private GitUtils() {
+        // no instantiation
+    }
+}
diff --git 
a/src/main/java/org/apache/commons/release/plugin/internal/package-info.java 
b/src/main/java/org/apache/commons/release/plugin/internal/package-info.java
new file mode 100644
index 0000000..9218ebf
--- /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.
+ *
+ * <p>Should not be referenced by external artifacts.</p>
+ */
+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 0000000..ea36335
--- /dev/null
+++ 
b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java
@@ -0,0 +1,374 @@
+/*
+ * 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.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.json";
+
+    /** 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);
+    }
+
+    /** 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;
+    }
+
+    void setOutputDirectory(File outputDirectory) {
+        this.outputDirectory = outputDirectory;
+    }
+
+    /**
+     * Returns the SCM directory.
+     *
+     * @return The SCM directory.
+     */
+    public File getScmDirectory() {
+        return scmDirectory;
+    }
+
+    /**
+     * Sets the SCM directory.
+     *
+     * @param scmDirectory The SCM directory.
+     */
+    public void setScmDirectory(File scmDirectory) {
+        this.scmDirectory = scmDirectory;
+    }
+
+    void setScmConnectionUrl(String scmConnectionUrl) {
+        this.scmConnectionUrl = scmConnectionUrl;
+    }
+
+    void setMavenHome(File mavenHome) {
+        this.mavenHome = mavenHome;
+    }
+
+    @Override
+    public void execute() throws MojoFailureException, MojoExecutionException {
+        getLog().info("This is a build attestation.");
+        // Build definition
+        BuildDefinition buildDefinition = new BuildDefinition();
+        buildDefinition.setExternalParameters(getExternalParameters());
+        buildDefinition.setResolvedDependencies(getBuildDependencies());
+        // Builder
+        Builder builder = new Builder();
+        // RunDetails
+        RunDetails runDetails = new RunDetails();
+        runDetails.setBuilder(builder);
+        runDetails.setMetadata(getBuildMetadata());
+        // Provenance
+        Provenance provenance = new Provenance();
+        provenance.setBuildDefinition(buildDefinition);
+        provenance.setRunDetails(runDetails);
+        // Statement
+        Statement statement = new Statement();
+        statement.setSubject(getSubjects());
+        statement.setPredicate(provenance);
+
+        writeStatement(statement);
+    }
+
+    private void writeStatement(Statement statement) throws 
MojoExecutionException {
+        final Path outputPath = outputDirectory.toPath();
+        try {
+            if (!Files.exists(outputPath)) {
+                Files.createDirectories(outputPath);
+            }
+        } catch (IOException e) {
+            throw new MojoExecutionException("Could not create output 
directory.", e);
+        }
+        final Artifact mainArtifact = project.getArtifact();
+        final Path artifactPath = 
outputPath.resolve(ArtifactUtils.getFileName(mainArtifact, 
ATTESTATION_EXTENSION));
+        getLog().info("Writing attestation statement to: " + artifactPath);
+        try (OutputStream os = Files.newOutputStream(artifactPath)) {
+            OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValue(os, 
statement);
+        } catch (IOException e) {
+            throw new MojoExecutionException("Could not write attestation 
statement to: " + artifactPath, e);
+        }
+        if (!skipAttach) {
+            getLog().info(String.format("Attaching attestation statement as 
%s-%s.%s", mainArtifact.getArtifactId(), mainArtifact.getVersion(),
+                    ATTESTATION_EXTENSION));
+            mavenProjectHelper.attachArtifact(project, ATTESTATION_EXTENSION, 
null, artifactPath.toFile());
+        }
+    }
+
+    /**
+     * Get the artifacts generated by the build.
+     *
+     * @return A list of resource descriptors for the build artifacts.
+     */
+    private List<ResourceDescriptor> getSubjects() throws 
MojoExecutionException {
+        List<ResourceDescriptor> subjects = new ArrayList<>();
+        
subjects.add(ArtifactUtils.toResourceDescriptor(project.getArtifact()));
+        for (Artifact artifact : project.getAttachedArtifacts()) {
+            subjects.add(ArtifactUtils.toResourceDescriptor(artifact));
+        }
+        return subjects;
+    }
+
+    private Map<String, Object> getExternalParameters() {
+        Map<String, Object> 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<String, Object> env = new HashMap<>();
+        params.put("env", env);
+        for (Map.Entry<String, String> entry : System.getenv().entrySet()) {
+            String key = entry.getKey();
+            if ("TZ".equals(key) || "LANG".equals(key) || 
key.startsWith("LC_")) {
+                env.put(key, entry.getValue());
+            }
+        }
+        return params;
+    }
+
+    private String getCommandLine(MavenExecutionRequest request) {
+        StringBuilder sb = new StringBuilder();
+        for (String goal : request.getGoals()) {
+            sb.append(goal);
+            sb.append(" ");
+        }
+        List<String> activeProfiles = request.getActiveProfiles();
+        if (activeProfiles != null && !activeProfiles.isEmpty()) {
+            sb.append("-P");
+            for (String profile : activeProfiles) {
+                sb.append(profile);
+                sb.append(",");
+            }
+            removeLast(sb);
+            sb.append(" ");
+        }
+        Properties userProperties = request.getUserProperties();
+        for (String propertyName : userProperties.stringPropertyNames()) {
+            sb.append("-D");
+            sb.append(propertyName);
+            sb.append("=");
+            sb.append(userProperties.get(propertyName));
+            sb.append(" ");
+        }
+        removeLast(sb);
+        return sb.toString();
+    }
+
+    private static void removeLast(StringBuilder sb) {
+        if (sb.length() > 0) {
+            sb.setLength(sb.length() - 1);
+        }
+    }
+
+    private List<ResourceDescriptor> getBuildDependencies() throws 
MojoExecutionException {
+        List<ResourceDescriptor> dependencies = new ArrayList<>();
+        try {
+            
dependencies.add(BuildToolDescriptors.jvm(Paths.get(System.getProperty("java.home"))));
+            
dependencies.add(BuildToolDescriptors.maven(runtimeInformation.getMavenVersion(),
 mavenHome.toPath()));
+            dependencies.add(getScmDescriptor());
+        } catch (IOException e) {
+            throw new MojoExecutionException(e);
+        }
+        dependencies.addAll(getProjectDependencies());
+        return dependencies;
+    }
+
+    private List<ResourceDescriptor> getProjectDependencies() throws 
MojoExecutionException {
+        List<ResourceDescriptor> dependencies = new ArrayList<>();
+        for (Artifact artifact : project.getArtifacts()) {
+            dependencies.add(ArtifactUtils.toResourceDescriptor(artifact));
+        }
+        return dependencies;
+    }
+
+    private ResourceDescriptor getScmDescriptor() throws IOException, 
MojoExecutionException {
+        ResourceDescriptor scmDescriptor = new ResourceDescriptor();
+        String scmUri = GitUtils.scmToDownloadUri(scmConnectionUrl, 
scmDirectory.toPath());
+        scmDescriptor.setUri(scmUri);
+        // Compute the revision
+        Map<String, String> digest = new HashMap<>();
+        digest.put("gitCommit", getScmRevision());
+        scmDescriptor.setDigest(digest);
+        return scmDescriptor;
+    }
+
+    private ScmRepository getScmRepository() throws MojoExecutionException {
+        try {
+            return scmManager.makeScmRepository(scmConnectionUrl);
+        } catch (ScmException e) {
+            throw new MojoExecutionException("Failed to create SCM 
repository", e);
+        }
+    }
+
+    private String getScmRevision() throws MojoExecutionException {
+        ScmRepository scmRepository = getScmRepository();
+        CommandParameters commandParameters = new CommandParameters();
+        try {
+            InfoScmResult result = 
scmManager.getProviderByRepository(scmRepository).info(scmRepository.getProviderRepository(),
+                    new ScmFileSet(scmDirectory), commandParameters);
+
+            return getScmRevision(result);
+        } catch (ScmException e) {
+            throw new MojoExecutionException("Failed to retrieve SCM 
revision", e);
+        }
+    }
+
+    private String getScmRevision(InfoScmResult result) throws 
MojoExecutionException {
+        if (!result.isSuccess()) {
+            throw new MojoExecutionException("Failed to retrieve SCM revision: 
" + result.getProviderMessage());
+        }
+
+        if (result.getInfoItems() == null || result.getInfoItems().isEmpty()) {
+            throw new MojoExecutionException("No SCM revision information 
found for " + scmDirectory);
+        }
+
+        InfoItem item = result.getInfoItems().get(0);
+
+        String revision = item.getRevision();
+        if (revision == null) {
+            throw new MojoExecutionException("Empty SCM revision returned for 
" + scmDirectory);
+        }
+        return revision;
+    }
+
+    private BuildMetadata getBuildMetadata() {
+        OffsetDateTime startedOn = 
session.getStartTime().toInstant().atOffset(ZoneOffset.UTC);
+        OffsetDateTime finishedOn = OffsetDateTime.now(ZoneOffset.UTC);
+        return new BuildMetadata(session.getRequest().getBuilderId(), 
startedOn, finishedOn);
+    }
+}
diff --git 
a/src/main/java/org/apache/commons/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 0000000..843bc0e
--- /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.
+ *
+ * <p>Specifies everything that influenced the build output. Together with 
{@link RunDetails}, it forms the complete
+ * {@link Provenance} record.</p>
+ *
+ * @see <a href="https://slsa.dev/spec/v1.2";>SLSA v1.2 Specification</a>
+ */
+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<String, Object> externalParameters = new HashMap<>();
+
+  /** Parameters set by the build platform. */
+  @JsonProperty("internalParameters")
+  private Map<String, Object> internalParameters = new HashMap<>();
+
+  /** Artifacts the build depends on, specified by URI and digest. */
+  @JsonProperty("resolvedDependencies")
+  private List<ResourceDescriptor> 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<String, Object> 
externalParameters) {
+    this.buildType = buildType;
+    this.externalParameters = externalParameters;
+  }
+
+  /**
+   * Returns the URI indicating what type of build was performed.
+   *
+   * <p>Determines the meaning of {@code externalParameters} and {@code 
internalParameters}.</p>
+   *
+   * @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<String, Object> getExternalParameters() {
+    return externalParameters;
+  }
+
+  /**
+   * Sets the inputs passed to the build.
+   *
+   * @param externalParameters the external parameters map
+   */
+  public void setExternalParameters(Map<String, Object> 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<String, Object> getInternalParameters() {
+    return internalParameters;
+  }
+
+  /**
+   * Sets the artifacts the build depends on.
+   *
+   * @param internalParameters the internal parameters map
+   */
+  public void setInternalParameters(Map<String, Object> internalParameters) {
+    this.internalParameters = internalParameters;
+  }
+
+  /**
+   * Returns the materials that influenced the build.
+   *
+   * <p>Considered incomplete unless resolved materials are present.</p>
+   *
+   * @return the list of resolved dependencies, or {@code null} if not set
+   */
+  public List<ResourceDescriptor> getResolvedDependencies() {
+    return resolvedDependencies;
+  }
+
+  /**
+   * Sets the materials that influenced the build.
+   *
+   * @param resolvedDependencies the list of resolved dependencies
+   */
+  public void setResolvedDependencies(List<ResourceDescriptor> 
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 0000000..345eb91
--- /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 <a href="https://slsa.dev/spec/v1.2";>SLSA v1.2 Specification</a>
+ */
+@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.
+     *
+     * <p>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.</p>
+     *
+     * @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 0000000..36e0f1a
--- /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 <a href="https://slsa.dev/spec/v1.2";>SLSA v1.2 Specification</a>
+ */
+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<ResourceDescriptor> builderDependencies = new ArrayList<>();
+
+    /** Map of build platform component names to their versions. */
+    @JsonProperty("version")
+    private Map<String, String> 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<ResourceDescriptor> 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<ResourceDescriptor> 
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<String, String> getVersion() {
+        return version;
+    }
+
+    /**
+     * Sets the map of build platform component names to their versions.
+     *
+     * @param version the version map
+     */
+    public void setVersion(Map<String, String> 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 0000000..8842460
--- /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.
+ *
+ * <p>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}.</p>
+ *
+ * @see <a href="https://slsa.dev/spec/v1.2";>SLSA v1.2 Specification</a>
+ */
+@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.
+     *
+     * <p>Includes source code, dependencies, build tools, base images, and 
other materials.</p>
+     *
+     * @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 0000000..55333f2
--- /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.
+ *
+ * <p>Used to represent inputs to, outputs from, or byproducts of the build 
process.</p>
+ *
+ * @see <a href="https://slsa.dev/spec/v1.2";>SLSA v1.2 Specification</a>
+ */
+@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<String, String> 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<String, Object> 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<String, String> 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.
+     *
+     * <p>Common keys include {@code "sha256"} and {@code "sha512"}.</p>
+     *
+     * @return the digest map, or {@code null} if not set
+     */
+    public Map<String, String> getDigest() {
+        return digest;
+    }
+
+    /**
+     * Sets the map of cryptographic digest algorithms to their hex-encoded 
values.
+     *
+     * @param digest the digest map
+     */
+    public void setDigest(Map<String, String> 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<String, Object> getAnnotations() {
+        return annotations;
+    }
+
+    /**
+     * Sets additional key-value metadata about the resource.
+     *
+     * @param annotations the annotations map
+     */
+    public void setAnnotations(Map<String, Object> 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 0000000..ffb1186
--- /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 <a href="https://slsa.dev/spec/v1.2";>SLSA v1.2 Specification</a>
+ */
+@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<ResourceDescriptor> 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.
+   *
+   * <p>Trusted to have correctly performed the operation and populated this 
provenance.</p>
+   *
+   * @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<ResourceDescriptor> 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<ResourceDescriptor> 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 0000000..00b3548
--- /dev/null
+++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java
@@ -0,0 +1,131 @@
+/*
+ * 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 <a 
href="https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md";>in-toto
 Statement v1</a>
+ */
+public class Statement {
+
+    /** The in-toto statement schema URI. */
+    @JsonProperty("_type")
+    private final String type = "https://in-toto.io/Statement/v1";;
+
+    /** Software artifacts that the attestation applies to. */
+    @JsonProperty("subject")
+    private List<ResourceDescriptor> 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 schema identifier for this statement.
+     *
+     * @return the fixed type URI {@code "https://in-toto.io/Statement/v1"}
+     */
+    public String getType() {
+        return type;
+    }
+
+    /**
+     * Returns the set of software artifacts that the attestation applies to.
+     *
+     * <p>Each element represents a single artifact. Artifacts are matched 
purely by digest, regardless of content
+     * type.</p>
+     *
+     * @return the list of subject artifacts, or {@code null} if not set
+     */
+    public List<ResourceDescriptor> 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<ResourceDescriptor> 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.
+     *
+     * <p>Unset is treated the same as set-but-empty. May be omitted if {@code 
predicateType} fully describes the
+     * predicate.</p>
+     *
+     * @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 0000000..69a5ce2
--- /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.
+ *
+ * <p>This package provides Jackson-annotated model classes that implement the 
<a href="https://slsa.dev/spec/v1.2";>Supply-chain Levels for Software Artifacts
+ * (SLSA) v1.2 specification</a>.</p>
+ *
+ * <h2>Overview</h2>
+ *
+ * <p>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.</p>
+ *
+ * @see <a href="https://slsa.dev/spec/v1.2";>SLSA v1.2 Specification</a>
+ * @see <a href="https://github.com/in-toto/attestation";>In-toto Attestation 
Framework</a>
+ * @see <a href="https://github.com/FasterXML/jackson";>Jackson JSON 
processor</a>
+ */
+package org.apache.commons.release.plugin.slsa.v1_2;
+
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 0000000..6a6c3f5
--- /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 0000000..5f7cb6a
--- /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.json".equals(a.getType()))
+                .findFirst()
+                .orElseThrow(() -> new AssertionError("No intoto.json artifact 
attached to project"));
+        String json = new 
String(Files.readAllBytes(attestation.getFile().toPath()), 
StandardCharsets.UTF_8);
+
+        String resolvedDeps = "predicate.buildDefinition.resolvedDependencies";
+        String javaVersion = System.getProperty("java.version");
+
+        assertThatJson(json)
+                .node(resolvedDeps).isArray()
+                .anySatisfy(dep -> {
+                    assertThatJson(dep).node("name").isEqualTo("JDK");
+                    
assertThatJson(dep).node("annotations.version").isEqualTo(javaVersion);
+                });
+
+        assertThatJson(json)
+                .node(resolvedDeps).isArray()
+                .anySatisfy(dep -> 
assertThatJson(dep).node("name").isEqualTo("Maven"));
+
+        assertThatJson(json)
+                .node(resolvedDeps).isArray()
+                .anySatisfy(dep -> 
assertThatJson(dep).node("uri").isString().startsWith("git+https://github.com/apache/commons-lang.git";));
+    }
+}
diff --git a/src/test/resources/artifacts/artifact-jar.txt 
b/src/test/resources/artifacts/artifact-jar.txt
new file mode 100644
index 0000000..103a21e
--- /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 0000000..48d1bbc
--- /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

Reply via email to