Copilot commented on code in PR #422:
URL: 
https://github.com/apache/commons-release-plugin/pull/422#discussion_r3112132915


##########
pom.xml:
##########
@@ -26,7 +26,8 @@
   </parent>
   <artifactId>commons-release-plugin</artifactId>
   <packaging>maven-plugin</packaging>
-  <version>1.9.3-SNAPSHOT</version>
+  <!-- Temporary version change to publish independent snapshot -->
+  <version>1.9.3.slsa-SNAPSHOT</version>

Review Comment:
   The project version is changed to `1.9.3.slsa-SNAPSHOT` with a note that it 
is temporary. If this PR is intended to land on the main development line, this 
will change the published coordinates and may break downstream consumers/CI 
expecting `1.9.3-SNAPSHOT`. Consider reverting the version change (or moving it 
to a separate, non-merged branch/workflow).



##########
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>

Review Comment:
   This SpotBugs filter disables EI_EXPOSE_REP/EI_EXPOSE_REP2 globally for the 
entire project. That hides real findings outside the new SLSA model classes. 
Please scope the suppression to the specific package/classes that intentionally 
expose mutable state (e.g., the `slsa.v1_2` models) rather than suppressing the 
patterns unconditionally.
   ```suggestion
     <!-- Mutable objects are intentionally exposed only by the SLSA v1_2 model 
classes -->
     <Match>
       <Class name="~.*\.slsa\.v1_2\..*" />
   ```



##########
src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java:
##########
@@ -0,0 +1,550 @@
+/*
+ * 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.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.Collections;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+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.BuildDefinitions;
+import org.apache.commons.release.plugin.internal.DsseUtils;
+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.DsseEnvelope;
+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.Signature;
+import org.apache.commons.release.plugin.slsa.v1_2.Statement;
+import org.apache.maven.artifact.Artifact;
+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.plugin.descriptor.PluginDescriptor;
+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.plugins.gpg.AbstractGpgSigner;
+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 used to serialize SLSA statements and DSSE 
envelopes to JSON.
+     *
+     * <p>Each attestation is written as a single JSON value followed by a 
line separator, matching
+     * the <a href="https://jsonlines.org/";>JSON Lines</a> format used by 
{@code .intoto.jsonl}
+     * files. The mapper is configured not to auto-close the output stream so 
the caller can append
+     * the trailing newline, and to emit ISO-8601 timestamps rather than 
numeric ones.</p>
+     */
+    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);
+    }
+
+    /**
+     * Checksum algorithms used in the generated attestation.
+     */
+    @Parameter(property = "commons.release.checksums.algorithms", defaultValue 
= "SHA-512,SHA-256,SHA-1,MD5")
+    private String algorithmNames;
+    /**
+     * Whether to include the default GPG keyring.
+     *
+     * <p>When {@code false}, passes {@code --no-default-keyring} to the GPG 
command.</p>
+     */
+    @Parameter(property = "gpg.defaultKeyring", defaultValue = "true")
+    private boolean defaultKeyring;
+    /**
+     * Path to the GPG executable; if not set, {@code gpg} is resolved from 
{@code PATH}.
+     */
+    @Parameter(property = "gpg.executable")
+    private String executable;
+    /**
+     * Name or fingerprint of the GPG key to use for signing.
+     *
+     * <p>Passed as {@code --local-user} to the GPG command; uses the default 
key when not set.</p>
+     */
+    @Parameter(property = "gpg.keyname")
+    private String keyname;
+    /**
+     * GPG database lock mode passed via {@code --lock-once}, {@code 
--lock-multiple}, or
+     * {@code --lock-never}; no lock flag is added when not set.
+     */
+    @Parameter(property = "gpg.lockMode")
+    private String lockMode;
+    /**
+     * The Maven home directory.
+     */
+    @Parameter(defaultValue = "${maven.home}", readonly = true)
+    private File mavenHome;
+    /**
+     * Helper to attach artifacts to the project.
+     */
+    private final MavenProjectHelper mavenProjectHelper;
+    /**
+     * The output directory for the attestation file.
+     */
+    @Parameter(property = "commons.release.outputDirectory", defaultValue = 
"${project.build.directory}")
+    private File outputDirectory;
+    /**
+     * The current Maven project.
+     */
+    private final MavenProject project;
+    /**
+     * Runtime information.
+     */
+    private final RuntimeInformation runtimeInformation;
+    /**
+     * The SCM connection URL for the current project.
+     */
+    @Parameter(defaultValue = "${project.scm.connection}", readonly = true)
+    private String scmConnectionUrl;
+    /**
+     * Issue SCM actions at this local directory.
+     */
+    @Parameter(property = "commons.release.scmDirectory", defaultValue = 
"${basedir}")
+    private File scmDirectory;
+    /**
+     * SCM manager to detect the Git revision.
+     */
+    private final ScmManager scmManager;
+    /**
+     * The current Maven session, used to resolve plugin dependencies.
+     */
+    private final MavenSession session;
+    /**
+     * Whether to sign the attestation envelope with GPG.
+     */
+    @Parameter(property = "commons.release.signAttestation", defaultValue = 
"true")
+    private boolean signAttestation;
+    /**
+     * Descriptor of this plugin; used to fill in {@code builder.id} with the 
plugin's own
+     * Package URL so that consumers can resolve the exact code that produced 
the provenance.
+     */
+    @Parameter(defaultValue = "${plugin}", readonly = true)
+    private PluginDescriptor pluginDescriptor;
+    /**
+     * GPG signer used for signing; lazily initialized from plugin parameters 
when {@code null}.
+     */
+    private AbstractGpgSigner signer;
+    /**
+     * Whether to skip attaching the attestation artifact to the project.
+     */
+    @Parameter(property = "commons.release.skipAttach")
+    private boolean skipAttach;
+    /**
+     * Whether to use gpg-agent for passphrase management.
+     *
+     * <p>For GPG versions before 2.1, passes {@code --use-agent} or {@code 
--no-use-agent}
+     * accordingly; ignored for GPG 2.1 and later where the agent is always 
used.</p>
+     */
+    @Parameter(property = "gpg.useagent", defaultValue = "true")
+    private boolean useAgent;
+
+    /**
+     * 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(final MavenProject project, final ScmManager 
scmManager, final RuntimeInformation runtimeInformation,
+            final MavenSession session, final MavenProjectHelper 
mavenProjectHelper) {
+        this.project = project;
+        this.scmManager = scmManager;
+        this.runtimeInformation = runtimeInformation;
+        this.session = session;
+        this.mavenProjectHelper = mavenProjectHelper;
+    }
+
+    /**
+     * Creates the output directory if it does not already exist and returns 
its path.
+     *
+     * @return the output directory path
+     * @throws MojoExecutionException if the directory cannot be created
+     */
+    private Path ensureOutputDirectory() throws MojoExecutionException {
+        final Path outputPath = outputDirectory.toPath();
+        try {
+            if (!Files.exists(outputPath)) {
+                Files.createDirectories(outputPath);
+            }
+        } catch (final IOException e) {
+            throw new MojoExecutionException("Could not create output 
directory.", e);
+        }
+        return outputPath;
+    }
+
+    @Override
+    public void execute() throws MojoFailureException, MojoExecutionException {
+        final BuildDefinition buildDefinition = new BuildDefinition()
+                
.setExternalParameters(BuildDefinitions.externalParameters(session))
+                .setResolvedDependencies(getBuildDependencies());
+        final String builderId = String.format("pkg:maven/%s/%s@%s",
+                pluginDescriptor.getGroupId(), 
pluginDescriptor.getArtifactId(), pluginDescriptor.getVersion());
+        final RunDetails runDetails = new RunDetails()
+                .setBuilder(new Builder().setId(builderId))
+                .setMetadata(getBuildMetadata());
+        final Provenance provenance = new Provenance()
+                .setBuildDefinition(buildDefinition)
+                .setRunDetails(runDetails);
+        final Statement statement = new Statement()
+                .setSubject(getSubjects())
+                .setPredicate(provenance);
+
+        final Path outputPath = ensureOutputDirectory();
+        final Path artifactPath = 
outputPath.resolve(ArtifactUtils.getFileName(project.getArtifact(), 
ATTESTATION_EXTENSION));
+        if (signAttestation) {
+            signAndWriteStatement(statement, outputPath, artifactPath);
+        } else {
+            writeStatement(statement, artifactPath);
+        }
+    }
+
+    /**
+     * Gets 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<ResourceDescriptor> getBuildDependencies() throws 
MojoExecutionException {
+        final List<ResourceDescriptor> dependencies = new ArrayList<>();
+        try {
+            
dependencies.add(BuildDefinitions.jvm(Paths.get(System.getProperty("java.home"))));
+            
dependencies.add(BuildDefinitions.maven(runtimeInformation.getMavenVersion(), 
mavenHome.toPath(),
+                    runtimeInformation.getClass().getClassLoader()));
+            dependencies.add(getScmDescriptor());
+        } catch (final IOException e) {
+            throw new MojoExecutionException(e);
+        }
+        dependencies.addAll(getProjectDependencies());
+        return dependencies;
+    }
+
+    /**
+     * Gets build metadata derived from the current Maven session, including 
start and finish timestamps.
+     *
+     * @return The build metadata.
+     */
+    private BuildMetadata getBuildMetadata() {
+        final OffsetDateTime startedOn = 
session.getStartTime().toInstant().atOffset(ZoneOffset.UTC);
+        final OffsetDateTime finishedOn = OffsetDateTime.now(ZoneOffset.UTC);
+        return new BuildMetadata(null, startedOn, finishedOn);
+    }
+
+    /**
+     * Gets 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<ResourceDescriptor> getProjectDependencies() throws 
MojoExecutionException {
+        final List<ResourceDescriptor> dependencies = new ArrayList<>();
+        for (final Artifact artifact : project.getArtifacts()) {
+            dependencies.add(ArtifactUtils.toResourceDescriptor(artifact, 
algorithmNames));
+        }
+        return dependencies;
+    }
+
+    /**
+     * Gets 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 {
+        return new ResourceDescriptor()
+                .setUri(GitUtils.scmToDownloadUri(scmConnectionUrl, 
scmDirectory.toPath()))
+                .setDigest(Collections.singletonMap("gitCommit", 
getScmRevision()));
+    }
+
+    /**
+     * Gets the SCM directory.
+     *
+     * @return The SCM directory.
+     */
+    public File getScmDirectory() {
+        return scmDirectory;
+    }
+
+    /**
+     * Gets 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 (final ScmException e) {
+            throw new MojoExecutionException("Failed to create SCM 
repository", e);
+        }
+    }
+
+    /**
+     * Gets 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 {
+        final ScmRepository scmRepository = getScmRepository();
+        final CommandParameters commandParameters = new CommandParameters();
+        try {
+            final InfoScmResult result = 
scmManager.getProviderByRepository(scmRepository).info(scmRepository.getProviderRepository(),
+                    new ScmFileSet(scmDirectory), commandParameters);
+
+            return getScmRevision(result);
+        } catch (final 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);
+        }
+
+        final InfoItem item = result.getInfoItems().get(0);
+
+        final String revision = item.getRevision();
+        if (revision == null) {
+            throw new MojoExecutionException("Empty SCM revision returned for 
" + scmDirectory);
+        }
+        return revision;
+    }
+
+    /**
+     * Gets the GPG signer, creating and preparing it from plugin parameters 
if not already set.
+     *
+     * @return the prepared signer
+     * @throws MojoFailureException if signer preparation fails
+     */
+    private AbstractGpgSigner getSigner() throws MojoFailureException {
+        if (signer == null) {
+            signer = DsseUtils.createGpgSigner(executable, defaultKeyring, 
lockMode, keyname, useAgent, getLog());
+        }
+        return signer;
+    }
+
+    /**
+     * 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<ResourceDescriptor> getSubjects() throws 
MojoExecutionException {
+        final List<ResourceDescriptor> subjects = new ArrayList<>();
+        subjects.add(ArtifactUtils.toResourceDescriptor(project.getArtifact(), 
algorithmNames));
+        for (final Artifact artifact : project.getAttachedArtifacts()) {
+            subjects.add(ArtifactUtils.toResourceDescriptor(artifact, 
algorithmNames));
+        }
+        return subjects;
+    }
+
+    /**
+     * Sets the list of checksum algorithms to use.
+     *
+     * @param algorithmNames A comma-separated list of {@link 
java.security.MessageDigest} algorithm names to use.
+     */
+    void setAlgorithmNames(final String algorithmNames) {
+        this.algorithmNames = algorithmNames;
+    }
+
+    /**
+     * Sets the Maven home directory.
+     *
+     * @param mavenHome The Maven home directory.
+     */
+    void setMavenHome(final File mavenHome) {
+        this.mavenHome = mavenHome;
+    }
+
+    /**
+     * Sets the output directory for the attestation file.
+     *
+     * @param outputDirectory The output directory.
+     */
+    void setOutputDirectory(final File outputDirectory) {
+        this.outputDirectory = outputDirectory;
+    }
+
+    /**
+     * Sets the public SCM connection URL.
+     *
+     * @param scmConnectionUrl The SCM connection URL.
+     */
+    void setScmConnectionUrl(final String scmConnectionUrl) {
+        this.scmConnectionUrl = scmConnectionUrl;
+    }
+
+    /**
+     * Sets the SCM directory.
+     *
+     * @param scmDirectory The SCM directory.
+     */
+    public void setScmDirectory(final File scmDirectory) {
+        this.scmDirectory = scmDirectory;
+    }
+
+    /**
+     * Sets whether to sign the attestation envelope.
+     *
+     * @param signAttestation {@code true} to sign, {@code false} to skip 
signing
+     */
+    void setSignAttestation(final boolean signAttestation) {
+        this.signAttestation = signAttestation;
+    }
+
+    /**
+     * Sets the plugin descriptor. Intended for testing.
+     *
+     * @param pluginDescriptor the plugin descriptor
+     */
+    void setPluginDescriptor(final PluginDescriptor pluginDescriptor) {
+        this.pluginDescriptor = pluginDescriptor;
+    }
+
+    /**
+     * Sets the GPG signer used for signing. Intended for testing.
+     *
+     * @param signer the signer to use
+     */
+    void setSigner(final AbstractGpgSigner signer) {
+        this.signer = signer;
+    }
+
+    /**
+     * Signs the attestation statement with GPG and writes it to {@code 
artifactPath}.
+     *
+     * @param statement    the attestation statement to sign and write
+     * @param outputPath   directory used for intermediate PAE and signature 
files
+     * @param artifactPath the destination file path for the envelope
+     * @throws MojoExecutionException if serialization, signing, or file I/O 
fails
+     * @throws MojoFailureException   if the GPG signer cannot be prepared
+     */
+    private void signAndWriteStatement(final Statement statement, final Path 
outputPath,
+            final Path artifactPath) throws MojoExecutionException, 
MojoFailureException {
+        final byte[] statementBytes;
+        try {
+            statementBytes = OBJECT_MAPPER.writeValueAsBytes(statement);
+        } catch (final JsonProcessingException e) {
+            throw new MojoExecutionException("Failed to serialize attestation 
statement", e);
+        }
+        final AbstractGpgSigner signer = getSigner();
+        final Path paeFile = DsseUtils.writePaeFile(statementBytes, 
outputPath);
+        final byte[] sigBytes = DsseUtils.signFile(signer, paeFile);
+
+        final Signature sig = new Signature()
+                .setKeyid(DsseUtils.getKeyId(sigBytes))
+                .setSig(sigBytes);
+        final DsseEnvelope envelope = new DsseEnvelope()
+                .setPayload(statementBytes)
+                .setSignatures(Collections.singletonList(sig));
+
+        getLog().info("Writing signed attestation envelope to: " + 
artifactPath);
+        writeAndAttach(envelope, artifactPath);

Review Comment:
   `signAndWriteStatement` writes an intermediate `statement.pae` file (via 
`DsseUtils.writePaeFile`) but never deletes it. This leaves extra files in the 
build output directory and may leak build provenance bytes outside the intended 
`.intoto.jsonl` artifact. Consider deleting the PAE file after signing (ideally 
in a finally block) or writing it to a temporary file that is always cleaned up.
   ```suggestion
           try {
               final byte[] sigBytes = DsseUtils.signFile(signer, paeFile);
   
               final Signature sig = new Signature()
                       .setKeyid(DsseUtils.getKeyId(sigBytes))
                       .setSig(sigBytes);
               final DsseEnvelope envelope = new DsseEnvelope()
                       .setPayload(statementBytes)
                       .setSignatures(Collections.singletonList(sig));
   
               getLog().info("Writing signed attestation envelope to: " + 
artifactPath);
               writeAndAttach(envelope, artifactPath);
           } finally {
               try {
                   Files.deleteIfExists(paeFile);
               } catch (final IOException e) {
                   getLog().warn("Failed to delete intermediate PAE file: " + 
paeFile, e);
               }
           }
   ```



##########
src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java:
##########
@@ -0,0 +1,141 @@
+/*
+ * 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.lang.management.ManagementFactory;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TreeMap;
+
+import org.apache.commons.release.plugin.slsa.v1_2.ResourceDescriptor;
+import org.apache.maven.execution.MavenExecutionRequest;
+import org.apache.maven.execution.MavenSession;
+
+/**
+ * Factory methods for the SLSA {@code BuildDefinition} fields: JVM, Maven 
descriptors and external build parameters.
+ */
+public final class BuildDefinitions {
+
+    /**
+     * 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
+     */
+    static String commandLine(final MavenExecutionRequest request) {
+        final List<String> args = new ArrayList<>(request.getGoals());
+        final String profiles = String.join(",", request.getActiveProfiles());
+        if (!profiles.isEmpty()) {
+            args.add("-P" + profiles);
+        }
+        request.getUserProperties().forEach((key, value) -> args.add("-D" + 
key + "=" + value));

Review Comment:
   `commandLine()` appends `-D` user properties using 
`request.getUserProperties().forEach(...)`. `Properties` iteration order is not 
deterministic, so the reconstructed command line can vary run-to-run when 
multiple `-D` flags are present, which hurts attestation reproducibility. 
Consider sorting properties by key before appending them.
   ```suggestion
           final Map<String, String> userProperties = new TreeMap<>();
           request.getUserProperties().forEach((key, value) -> 
userProperties.put(String.valueOf(key), String.valueOf(value)));
           userProperties.forEach((key, value) -> args.add("-D" + key + "=" + 
value));
   ```



##########
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 <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")
+    public static final String TYPE = "https://in-toto.io/Statement/v1";;

Review Comment:
   `Statement.TYPE` is declared `static final` and annotated with 
`@JsonProperty("_type")`. Jackson does not serialize static fields as POJO 
properties, so the emitted statement will likely omit the required in-toto 
`_type` field. Consider making `_type` an instance property (e.g., a non-static 
field initialized to `TYPE`, or an annotated getter) so it is always serialized.



##########
src/main/java/org/apache/commons/release/plugin/internal/GitUtils.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.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;
+import org.apache.commons.codec.digest.GitIdentifiers;
+
+/**
+ * Utilities for Git operations.
+ */
+public final class GitUtils {
+
+    /** The SCM URI prefix for Git repositories. */
+    private static final String SCM_GIT_PREFIX = "scm:git:";
+
+    /**
+     * 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(final Path path) throws IOException {
+        Path current = path.toAbsolutePath();
+        while (current != null) {
+            final 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"
+                final String content = new 
String(Files.readAllBytes(candidate), StandardCharsets.UTF_8).trim();
+                if (content.startsWith("gitdir: ")) {
+                    return Paths.get(content.substring("gitdir: ".length()));

Review Comment:
   `findGitDir()` parses git worktree `.git` files and returns `Paths.get(...)` 
of the `gitdir:` value. In worktrees this path is often relative to the 
worktree directory, so returning it as-is can resolve incorrectly (relative to 
the process CWD). Consider resolving relative paths against 
`candidate.getParent()` (the directory containing the `.git` file).
   ```suggestion
                       final Path gitDir = Paths.get(content.substring("gitdir: 
".length()));
                       return gitDir.isAbsolute() ? gitDir : 
candidate.getParent().resolve(gitDir).normalize();
   ```



##########
src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java:
##########
@@ -0,0 +1,178 @@
+/*
+ * 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.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Locale;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.release.plugin.slsa.v1_2.DsseEnvelope;
+import org.apache.commons.release.plugin.slsa.v1_2.Statement;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugin.logging.Log;
+import org.apache.maven.plugins.gpg.AbstractGpgSigner;
+import org.apache.maven.plugins.gpg.GpgSigner;
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.bcpg.sig.IssuerFingerprint;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.PGPSignatureList;
+import org.bouncycastle.openpgp.PGPSignatureSubpacketVector;
+import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
+
+/**
+ * Utility methods for creating DSSE (Dead Simple Signing Envelope) envelopes 
signed with a PGP key.
+ */
+public final class DsseUtils {
+
+    /**
+     * Creates and prepares a {@link GpgSigner} from the given configuration.
+     *
+     * <p>The returned signer has {@link AbstractGpgSigner#prepare()} already 
called and is ready for use with {@link #signFile(AbstractGpgSigner, Path)}.</p>
+     *
+     * @param executable     path to the GPG executable, or {@code null} to 
use {@code gpg} from {@code PATH}
+     * @param defaultKeyring whether to include the default GPG keyring
+     * @param lockMode       GPG lock mode ({@code "once"}, {@code 
"multiple"}, or {@code "never"}), or {@code null} for no explicit lock flag
+     * @param keyname        name or fingerprint of the signing key, or {@code 
null} for the default key
+     * @param useAgent       whether to use gpg-agent for passphrase management
+     * @param log            Maven logger to attach to the signer
+     * @return a prepared {@link AbstractGpgSigner}
+     * @throws MojoFailureException if {@link AbstractGpgSigner#prepare()} 
fails
+     */
+    public static AbstractGpgSigner createGpgSigner(final String executable, 
final boolean defaultKeyring, final String lockMode, final String keyname,
+            final boolean useAgent, final Log log) throws MojoFailureException 
{
+        final GpgSigner signer = new GpgSigner(executable);
+        signer.setDefaultKeyring(defaultKeyring);
+        signer.setLockMode(lockMode);
+        signer.setKeyName(keyname);
+        signer.setUseAgent(useAgent);
+        signer.setLog(log);
+        signer.prepare();
+        return signer;
+    }
+
+    /**
+     * Extracts the key identifier from a binary OpenPGP Signature Packet.
+     *
+     * @param sigBytes raw binary OpenPGP Signature Packet bytes
+     * @return uppercase hex-encoded fingerprint or key ID string
+     * @throws MojoExecutionException if {@code sigBytes} cannot be parsed as 
an OpenPGP signature
+     */
+    public static String getKeyId(final byte[] sigBytes) throws 
MojoExecutionException {
+        try {
+            final PGPSignatureList sigList = (PGPSignatureList) new 
BcPGPObjectFactory(sigBytes).nextObject();
+            final PGPSignature sig = sigList.get(0);
+            final PGPSignatureSubpacketVector hashed = 
sig.getHashedSubPackets();
+            if (hashed != null) {
+                final IssuerFingerprint fp = hashed.getIssuerFingerprint();
+                if (fp != null) {
+                    return Hex.encodeHexString(fp.getFingerprint());

Review Comment:
   `getKeyId()` Javadoc promises an uppercase hex-encoded fingerprint, but 
`Hex.encodeHexString(...)` returns lowercase. Either normalize the fingerprint 
to uppercase (to match the fallback branch) or adjust the Javadoc so it matches 
the actual output.
   ```suggestion
                       return 
Hex.encodeHexString(fp.getFingerprint()).toUpperCase(Locale.ROOT);
   ```



##########
src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java:
##########
@@ -0,0 +1,550 @@
+/*
+ * 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.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.Collections;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+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.BuildDefinitions;
+import org.apache.commons.release.plugin.internal.DsseUtils;
+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.DsseEnvelope;
+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.Signature;
+import org.apache.commons.release.plugin.slsa.v1_2.Statement;
+import org.apache.maven.artifact.Artifact;
+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.plugin.descriptor.PluginDescriptor;
+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.plugins.gpg.AbstractGpgSigner;
+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";
+

Review Comment:
   PR description says the attestation is attached with a `.intoto.json` 
extension, but the implementation uses JSON Lines (`intoto.jsonl`) for the 
attached artifact type/extension. Either update the PR description (and any 
user-facing docs) or change the output naming/type so the behavior matches the 
stated extension.



##########
src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java:
##########
@@ -0,0 +1,141 @@
+/*
+ * 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.lang.management.ManagementFactory;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TreeMap;
+
+import org.apache.commons.release.plugin.slsa.v1_2.ResourceDescriptor;
+import org.apache.maven.execution.MavenExecutionRequest;
+import org.apache.maven.execution.MavenSession;
+
+/**
+ * Factory methods for the SLSA {@code BuildDefinition} fields: JVM, Maven 
descriptors and external build parameters.
+ */
+public final class BuildDefinitions {
+
+    /**
+     * 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
+     */
+    static String commandLine(final MavenExecutionRequest request) {
+        final List<String> args = new ArrayList<>(request.getGoals());
+        final String profiles = String.join(",", request.getActiveProfiles());
+        if (!profiles.isEmpty()) {
+            args.add("-P" + profiles);
+        }
+        request.getUserProperties().forEach((key, value) -> args.add("-D" + 
key + "=" + value));
+        return String.join(" ", args);
+    }
+
+    /**
+     * Returns a map of external build parameters captured from the current 
JVM and Maven session.
+     *
+     * @param session the current Maven session
+     * @return a map of parameter names to values
+     */
+    public static Map<String, Object> externalParameters(final MavenSession 
session) {
+        final Map<String, Object> params = new HashMap<>();
+        params.put("jvm.args", 
ManagementFactory.getRuntimeMXBean().getInputArguments());
+        final 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", commandLine(request));
+        final Map<String, Object> env = new HashMap<>();
+        params.put("env", env);
+        for (final Map.Entry<String, String> entry : 
System.getenv().entrySet()) {
+            final String key = entry.getKey();
+            if ("TZ".equals(key) || "LANG".equals(key) || 
key.startsWith("LC_")) {
+                env.put(key, entry.getValue());
+            }
+        }
+        return params;
+    }
+
+    /**
+     * 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(final Path javaHome) throws 
IOException {
+        final String[] propertyNames = {
+                "java.home", "java.specification.maintenance.version", 
"java.specification.name", "java.specification.vendor", 
"java.specification.version",
+                "java.vendor", "java.vendor.url", "java.vendor.version", 
"java.version", "java.version.date", "java.vm.name", 
"java.vm.specification.name",
+                "java.vm.specification.vendor", 
"java.vm.specification.version", "java.vm.vendor", "java.vm.version"
+        };
+        final Map<String, Object> annotations = new TreeMap<>();
+        for (final String prop : propertyNames) {
+            annotations.put(prop.substring("java.".length()), 
System.getProperty(prop));
+        }
+        return new ResourceDescriptor()
+                .setName("JDK")
+                .setDigest(Collections.singletonMap("gitTree", 
GitUtils.gitTree(javaHome)))
+                .setAnnotations(annotations);
+    }
+
+    /**
+     * Creates a {@link ResourceDescriptor} for the Maven installation used 
during the build.
+     *
+     * <p>{@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

Review Comment:
   Javadoc for the Maven descriptor says the plugin classloader "does see" 
Maven Core resources, but the surrounding text and implementation indicate the 
opposite (hence passing a core classloader). Please correct the wording (likely 
"does not see those resources") so the documentation matches the actual 
classloading behavior.
   ```suggestion
        * Plugin code runs in an isolated Plugin Classloader, which does not 
see those resources. Therefore, we need to pass the classloader from a class 
from
   ```



##########
src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java:
##########
@@ -0,0 +1,141 @@
+/*
+ * 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.lang.management.ManagementFactory;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TreeMap;
+
+import org.apache.commons.release.plugin.slsa.v1_2.ResourceDescriptor;
+import org.apache.maven.execution.MavenExecutionRequest;
+import org.apache.maven.execution.MavenSession;
+
+/**
+ * Factory methods for the SLSA {@code BuildDefinition} fields: JVM, Maven 
descriptors and external build parameters.
+ */
+public final class BuildDefinitions {
+
+    /**
+     * 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
+     */
+    static String commandLine(final MavenExecutionRequest request) {
+        final List<String> args = new ArrayList<>(request.getGoals());
+        final String profiles = String.join(",", request.getActiveProfiles());
+        if (!profiles.isEmpty()) {
+            args.add("-P" + profiles);
+        }
+        request.getUserProperties().forEach((key, value) -> args.add("-D" + 
key + "=" + value));
+        return String.join(" ", args);
+    }
+
+    /**
+     * Returns a map of external build parameters captured from the current 
JVM and Maven session.
+     *
+     * @param session the current Maven session
+     * @return a map of parameter names to values
+     */
+    public static Map<String, Object> externalParameters(final MavenSession 
session) {
+        final Map<String, Object> params = new HashMap<>();
+        params.put("jvm.args", 
ManagementFactory.getRuntimeMXBean().getInputArguments());
+        final 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", commandLine(request));

Review Comment:
   `externalParameters()` records `request.getUserProperties()` and the 
reconstructed command line verbatim. Since attestations are intended to be 
published, this can leak secrets passed via `-D` (e.g., passphrases, tokens, 
passwords). Consider filtering/redacting common sensitive keys (and/or 
switching to an allowlist that defaults to safe keys) before writing these 
values into the attestation.



##########
src/site/markdown/slsa/v0.1.0.md:
##########
@@ -0,0 +1,280 @@
+<!-- SPDX-License-Identifier: Apache-2.0 -->
+
+# Build Type: Apache Commons Maven Release
+
+```jsonc
+"buildType": 
"https://commons.apache.org/proper/commons-release-plugin/slsa/v0.1.0";
+```
+
+This document defines a [SLSA v1.2 Build 
Provenance](https://slsa.dev/spec/v1.2/build-provenance) **build type** for
+releases of Apache Commons components.
+
+Apache Commons releases are cut on a PMC release manager's workstation by 
invoking Maven against a checkout of the
+project's Git repository. The `commons-release-plugin` captures the build 
inputs and emits the result as an in-toto
+attestation covering every artifact attached to the project.
+
+Because the build runs on the release manager's own hardware rather than on a 
hosted build service, the provenance
+corresponds to [SLSA Build Level 1](https://slsa.dev/spec/v1.2/levels): it is 
generated by the same process that
+produces the artifacts and is signed with the release manager's OpenPGP key, 
but the build platform itself is not
+separately attested.
+
+The OpenPGP keys used to sign past and present artifacts are available at: 
https://downloads.apache.org/commons/KEYS
+
+Attestations are serialized in the [JSON Lines](https://jsonlines.org/) format 
used across the
+in-toto ecosystem, one JSON value per line, and published to Maven Central 
under the released
+artifact's coordinates with an `intoto.jsonl` type:
+
+```xml
+
+<dependency>
+  <groupId>org.apache.commons</groupId>
+  <artifactId>${artifactId}</artifactId>
+  <type>intoto.jsonl</type>
+  <version>${version}</version>
+</dependency>
+```
+
+## 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 (for example `["deploy"]`). |
+| `maven.profiles`        | string[] | The list of active profiles passed via 
`-P` (for example `["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                                                              |
+|-------------------------------------|------------------------------------------|--------------------------------------------------------------------------|
+| `home`                              | `java.home`                            
  | Java installation directory.                                             |
+| `specification.maintenance.version` | 
`java.specification.maintenance.version` | Java Runtime Environment 
specification maintenance version _(optional)_. |
+| `specification.name`                | `java.specification.name`              
  | Java Runtime Environment specification name.                             |
+| `specification.vendor`              | `java.specification.vendor`            
  | Java Runtime Environment specification vendor.                           |
+| `specification.version`             | `java.specification.version`           
  | Java Runtime Environment specification version.                          |
+| `vendor`                            | `java.vendor`                          
  | Java Runtime Environment vendor.                                         |
+| `vendor.url`                        | `java.vendor.url`                      
  | Java vendor URL.                                                         |
+| `vendor.version`                    | `java.vendor.version`                  
  | Java vendor version _(optional)_.                                        |
+| `version`                           | `java.version`                         
  | Java Runtime Environment version.                                        |
+| `version.date`                      | `java.version.date`                    
  | Java Runtime Environment version date, in ISO-8601 YYYY-MM-DD format.    |
+| `vm.name`                           | `java.vm.name`                         
  | Java Virtual Machine implementation name.                                |
+| `vm.specification.name`             | `java.vm.specification.name`           
  | Java Virtual Machine specification name.                                 |
+| `vm.specification.vendor`           | `java.vm.specification.vendor`         
  | Java Virtual Machine specification vendor.                               |
+| `vm.specification.version`          | `java.vm.specification.version`        
  | Java Virtual Machine specification version.                              |
+| `vm.vendor`                         | `java.vm.vendor`                       
  | Java Virtual Machine implementation vendor.                              |
+| `vm.version`                        | `java.vm.version`                      
  | Java Virtual Machine implementation version.                             |
+
+#### 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 Maven distribution.          
          |
+| `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, for example `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 the [Package 
URL](https://github.com/package-url/purl-spec) of the
+`commons-release-plugin` release that produced the attestation, for example
+`pkg:maven/org.apache.commons/[email protected]`. It identifies the 
trust boundary of
+the "build platform": the exact plugin code that emitted the provenance. 
Verifiers can resolve the
+PURL to the signed artifact on Maven Central to inspect the builder.
+
+## Subjects
+
+The 
[`subject`](https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md#fields)
 array
+lists every artifact produces by the build. It has the following properties

Review Comment:
   Grammar: "lists every artifact produces by the build" should be "lists every 
artifact produced by the build".
   ```suggestion
   lists every artifact produced by the build. It has the following properties
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to