Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/scripts/VerifyMavenWrapperIntegrity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;

/**
* Fail-closed Maven Wrapper bootstrap preflight for GitHub Actions.
*
* <p>This source-file-mode utility runs on the JDK configured by the workflow before any
* {@code mvnw} or {@code mvnw.cmd} invocation. It verifies that the Maven distribution remains
* bound to the exact reviewed URL and SHA-256 checksum, so deleting or changing the checksum
* cannot silently bypass integrity verification before Maven starts.</p>
*/
public final class VerifyMavenWrapperIntegrity {

private static final String REVIEWED_WRAPPER_VERSION = "3.3.4";
private static final String REVIEWED_DISTRIBUTION_TYPE = "only-script";
private static final String REVIEWED_DISTRIBUTION_URL =
"https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/"
+ "apache-maven-3.9.11-bin.zip";
private static final String REVIEWED_DISTRIBUTION_SHA256 =
"0d7125e8c91097b36edb990ea5934e6c68b4440eef4ea96510a0f6815e7eeadb";

private VerifyMavenWrapperIntegrity() {
}

/**
* Validates the reviewed Maven Wrapper bootstrap inputs before Maven can execute.
*
* @param args ignored command-line arguments
* @throws IOException when the wrapper properties cannot be read
*/
public static void main(String[] args) throws IOException {
Path propertiesPath = Path.of(".mvn", "wrapper", "maven-wrapper.properties");
if (!Files.isRegularFile(propertiesPath)) {
throw new IllegalStateException("Missing Maven Wrapper properties: " + propertiesPath);
}

Properties properties = new Properties();
try (Reader reader = Files.newBufferedReader(propertiesPath, StandardCharsets.UTF_8)) {
properties.load(reader);
}

requireExact(properties, "wrapperVersion", REVIEWED_WRAPPER_VERSION);
requireExact(properties, "distributionType", REVIEWED_DISTRIBUTION_TYPE);
requireExact(properties, "distributionUrl", REVIEWED_DISTRIBUTION_URL);
requireExact(properties, "distributionSha256Sum", REVIEWED_DISTRIBUTION_SHA256);
System.out.println("Maven Wrapper integrity preflight passed.");
}

private static void requireExact(Properties properties, String key, String expected) {
String actual = properties.getProperty(key);
if (!expected.equals(actual)) {
throw new IllegalStateException(
"Maven Wrapper integrity preflight rejected " + key
+ ": expected reviewed value but found " + String.valueOf(actual)
);
}
}
}
20 changes: 18 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,21 @@ jobs:
java-version: "25"
cache: maven

- name: Verify Maven Wrapper integrity (Unix)
if: runner.os != 'Windows'
run: java .github/scripts/VerifyMavenWrapperIntegrity.java

- name: Run tests (Unix)
if: runner.os != 'Windows'
run: ./mvnw -B test

- name: Verify Maven Wrapper integrity (Windows)
if: runner.os == 'Windows'
run: java .github/scripts/VerifyMavenWrapperIntegrity.java

- name: Run tests (Windows)
if: runner.os == 'Windows'
run: .\\mvnw.cmd -B test
run: .\mvnw.cmd -B test

test_self_hosted:
if: ${{ github.event_name == 'workflow_dispatch' && inputs.use_self_hosted == true }}
Expand All @@ -60,10 +68,18 @@ jobs:
java-version: "25"
cache: maven

- name: Verify Maven Wrapper integrity (Unix)
if: runner.os != 'Windows'
run: java .github/scripts/VerifyMavenWrapperIntegrity.java

- name: Run tests (Unix)
if: runner.os != 'Windows'
run: ./mvnw -B test

- name: Verify Maven Wrapper integrity (Windows)
if: runner.os == 'Windows'
run: java .github/scripts/VerifyMavenWrapperIntegrity.java

- name: Run tests (Windows)
if: runner.os == 'Windows'
run: .\\mvnw.cmd -B test
run: .\mvnw.cmd -B test
13 changes: 12 additions & 1 deletion .github/workflows/sbom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ jobs:
java-version: "25"
cache: maven

- name: Verify Maven Wrapper integrity
run: java .github/scripts/VerifyMavenWrapperIntegrity.java

- name: Generate CycloneDX SBOM (aggregate)
run: ./mvnw -B -DskipTests org.cyclonedx:cyclonedx-maven-plugin:2.9.1:makeAggregateBom -DoutputFormat=all -Dcyclonedx.skipAttach=true

Expand All @@ -59,13 +62,21 @@ jobs:
java-version: "25"
cache: maven

- name: Verify Maven Wrapper integrity (Unix)
if: runner.os != 'Windows'
run: java .github/scripts/VerifyMavenWrapperIntegrity.java

- name: Generate CycloneDX SBOM (aggregate)
if: runner.os != 'Windows'
run: ./mvnw -B -DskipTests org.cyclonedx:cyclonedx-maven-plugin:2.9.1:makeAggregateBom -DoutputFormat=all -Dcyclonedx.skipAttach=true

- name: Verify Maven Wrapper integrity (Windows)
if: runner.os == 'Windows'
run: java .github/scripts/VerifyMavenWrapperIntegrity.java

- name: Generate CycloneDX SBOM (aggregate, Windows)
if: runner.os == 'Windows'
run: .\\mvnw.cmd -B -DskipTests org.cyclonedx:cyclonedx-maven-plugin:2.9.1:makeAggregateBom -DoutputFormat=all -Dcyclonedx.skipAttach=true
run: .\mvnw.cmd -B -DskipTests org.cyclonedx:cyclonedx-maven-plugin:2.9.1:makeAggregateBom -DoutputFormat=all -Dcyclonedx.skipAttach=true

- name: Upload SBOM artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand Down
1 change: 1 addition & 0 deletions .mvn/wrapper/maven-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
distributionSha256Sum=0d7125e8c91097b36edb990ea5934e6c68b4440eef4ea96510a0f6815e7eeadb
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package com.xtrmetl.etl.documentation;

import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Properties;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Prevents Maven Wrapper bootstrap from executing an unverified Maven distribution.
*
* <p>The wrapper downloads Maven before project compilation and tests can run. This repository
* therefore treats the Maven distribution URL and its reviewed SHA-256 as one atomic build-input
* contract. A Maven version or URL change must carry a newly reviewed checksum in the same change;
* deleting the checksum must fail deterministically without network access.</p>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
*/
class MavenWrapperIntegrityTest {

private static final String REVIEWED_DISTRIBUTION_URL =
"https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/"
+ "apache-maven-3.9.11-bin.zip";
private static final String REVIEWED_DISTRIBUTION_SHA256 =
"0d7125e8c91097b36edb990ea5934e6c68b4440eef4ea96510a0f6815e7eeadb";
private static final String PREFLIGHT_COMMAND =
"run: java .github/scripts/VerifyMavenWrapperIntegrity.java";

/**
* Requires the fixed Maven 3.9.11 download to remain bound to its reviewed SHA-256 checksum.
*
* @throws IOException when the wrapper properties cannot be read as repository source
*/
@Test
void bindsMavenDistributionUrlToReviewedSha256() throws IOException {
Properties properties = new Properties();
Path wrapperProperties = projectRoot().resolve(
".mvn/wrapper/maven-wrapper.properties"
);
assertTrue(Files.isRegularFile(wrapperProperties), "Maven Wrapper properties must exist");

try (Reader reader = Files.newBufferedReader(wrapperProperties, StandardCharsets.UTF_8)) {
properties.load(reader);
}

assertEquals("3.3.4", properties.getProperty("wrapperVersion"));
assertEquals("only-script", properties.getProperty("distributionType"));
assertEquals(
REVIEWED_DISTRIBUTION_URL,
properties.getProperty("distributionUrl"),
"Changing the Maven distribution requires review of a matching checksum"
);

String distributionSha256 = properties.getProperty("distributionSha256Sum");
assertNotNull(
distributionSha256,
"Maven Wrapper must verify the downloaded Maven distribution with SHA-256"
);
assertTrue(
distributionSha256.matches("[0-9a-f]{64}"),
"distributionSha256Sum must be 64 lowercase hexadecimal characters"
);
assertEquals(
REVIEWED_DISTRIBUTION_SHA256,
distributionSha256,
"The checksum must match the reviewed Maven 3.9.11 distribution"
);
}

/**
* Requires a fail-closed integrity preflight immediately before every CI/SBOM wrapper step.
*
* <p>The preflight must have the same GitHub Actions {@code if:} condition as the wrapper step,
* so neither Unix nor Windows execution can bootstrap Maven without validating the reviewed
* distribution URL and checksum first.</p>
*
* @throws IOException when a workflow cannot be read as repository source
*/
@Test
void preflightsEveryCiAndSbomWrapperInvocation() throws IOException {
for (String workflow : List.of(
".github/workflows/ci.yml",
".github/workflows/sbom.yml"
)) {
List<String> lines = Files.readAllLines(
projectRoot().resolve(workflow),
StandardCharsets.UTF_8
);
int wrapperInvocations = 0;

for (int lineIndex = 0; lineIndex < lines.size(); lineIndex++) {
String line = lines.get(lineIndex).trim();
if (!line.startsWith("run:") || !containsWrapperInvocation(line)) {
continue;
}
wrapperInvocations++;

int wrapperStep = previousStepStart(lines, lineIndex);
int preflightStep = previousStepStart(lines, wrapperStep - 1);
assertTrue(
preflightStep >= 0,
workflow + ": wrapper invocation must have a preceding preflight step"
);

String preflightBlock = String.join(
"\n",
lines.subList(preflightStep, wrapperStep)
);
assertTrue(
preflightBlock.contains(PREFLIGHT_COMMAND),
workflow + ": every Maven Wrapper invocation must be immediately preceded "
+ "by the integrity preflight"
);

String wrapperCondition = stepCondition(lines, wrapperStep, lineIndex + 1);
String preflightCondition = stepCondition(lines, preflightStep, wrapperStep);
assertEquals(
wrapperCondition,
preflightCondition,
workflow + ": preflight and wrapper step must use the same condition"
);
}

assertTrue(
wrapperInvocations > 0,
workflow + ": expected at least one Maven Wrapper invocation"
);
}
}

private static boolean containsWrapperInvocation(String line) {
return line.contains("./mvnw ") || line.contains(".\\mvnw.cmd ");
}
Comment thread
seonghobae marked this conversation as resolved.

private static int previousStepStart(List<String> lines, int fromIndex) {
for (int lineIndex = fromIndex; lineIndex >= 0; lineIndex--) {
if (lines.get(lineIndex).trim().startsWith("- name:")) {
return lineIndex;
}
}
return -1;
}

private static String stepCondition(List<String> lines, int startInclusive, int endExclusive) {
for (int lineIndex = startInclusive; lineIndex < endExclusive; lineIndex++) {
String line = lines.get(lineIndex).trim();
if (line.startsWith("if:")) {
return line;
}
}
return "";
}

/**
* Finds the repository root from reactor-root or module-local Maven execution.
*
* @return absolute repository root containing the wrapper configuration
*/
private static Path projectRoot() {
Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath();
Path lastPomParent = null;
while (current != null) {
if (Files.exists(current.resolve(".git"))) {
return current;
}
if (Files.exists(current.resolve("pom.xml"))) {
lastPomParent = current;
}
current = current.getParent();
}
if (lastPomParent != null) {
return lastPomParent;
}
throw new IllegalStateException("Could not find project root");
}
}
Loading