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]