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
81 changes: 81 additions & 0 deletions .github/scripts/VerifyMavenWrapperIntegrity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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.List;
import java.util.Properties;

/**
* Fail-closed preflight for the Maven Wrapper distribution trust binding.
*
* <p>This source-file program executes with the JDK before Maven Wrapper bootstrap. It verifies
* that the repository still binds the reviewed Maven distribution URL to its reviewed SHA-256
* checksum. Maven Wrapper then verifies the downloaded archive against the same checksum.</p>
*/
public final class VerifyMavenWrapperIntegrity {
private static final Path WRAPPER_PROPERTIES =
Path.of(".mvn", "wrapper", "maven-wrapper.properties");
private static final String EXPECTED_WRAPPER_VERSION = "3.3.4";
private static final String EXPECTED_DISTRIBUTION_TYPE = "only-script";
private static final String EXPECTED_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 EXPECTED_DISTRIBUTION_SHA256 =
"0d7125e8c91097b36edb990ea5934e6c68b4440eef4ea96510a0f6815e7eeadb";

private VerifyMavenWrapperIntegrity() {
// Utility class.
}

/**
* Verifies the reviewed Maven Wrapper distribution binding and exits non-zero on drift.
*
* @param args ignored command-line arguments
* @throws IOException when the wrapper properties cannot be read
*/
public static void main(String[] args) throws IOException {
if (!Files.isRegularFile(WRAPPER_PROPERTIES)) {
throw new IllegalStateException("Maven Wrapper properties file is missing");
}

List<String> sourceLines = Files.readAllLines(WRAPPER_PROPERTIES, StandardCharsets.UTF_8);
Properties properties = new Properties();
try (Reader reader = Files.newBufferedReader(WRAPPER_PROPERTIES, StandardCharsets.UTF_8)) {
properties.load(reader);
}

requireUniqueProperty(sourceLines, "wrapperVersion");
requireUniqueProperty(sourceLines, "distributionType");
requireUniqueProperty(sourceLines, "distributionUrl");
requireUniqueProperty(sourceLines, "distributionSha256Sum");

requireExact(properties, "wrapperVersion", EXPECTED_WRAPPER_VERSION);
requireExact(properties, "distributionType", EXPECTED_DISTRIBUTION_TYPE);
requireExact(properties, "distributionUrl", EXPECTED_DISTRIBUTION_URL);
requireExact(properties, "distributionSha256Sum", EXPECTED_DISTRIBUTION_SHA256);

System.out.println("Maven Wrapper integrity preflight passed.");
}

private static void requireUniqueProperty(List<String> sourceLines, String key) {
long matches = sourceLines.stream()
.map(String::trim)
.filter(line -> line.startsWith(key + "="))
.count();
if (matches != 1) {
throw new IllegalStateException(
"Expected exactly one canonical " + key + " property, found " + matches
);
}
}

private static void requireExact(Properties properties, String key, String expectedValue) {
String actualValue = properties.getProperty(key);
if (!expectedValue.equals(actualValue)) {
throw new IllegalStateException(
"Maven Wrapper integrity drift for " + key + ": expected reviewed value"
);
}
}
}
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,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
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
11 changes: 11 additions & 0 deletions .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,10 +62,18 @@ 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
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>
*/
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 on lines +138 to +140

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "--- 워크플로 실제 텍스트 ---"
rg -n 'mvnw' .github/workflows/ci.yml .github/workflows/sbom.yml
echo "--- 단일 백슬래시 패턴 존재 여부 (0건이면 테스트가 Windows 스텝을 못 잡음) ---"
rg -nF '.\mvnw.cmd ' .github/workflows/ci.yml .github/workflows/sbom.yml || echo "matches=0"
echo "--- 이중 백슬래시 패턴 ---"
rg -nF '.\\mvnw.cmd ' .github/workflows/ci.yml .github/workflows/sbom.yml || true

Repository: ContextualWisdomLab/mightyETL

Length of output: 1324


Windows 래퍼 호출 매칭 조건을 수정하세요.

.github/workflows/ci.yml.github/workflows/sbom.yml의 Windows 명령은 텍스트 .\\mvnw.cmd 를 사용합니다. 현재 containsWrapperInvocation의 Java 리터럴 ".\\mvnw.cmd "는 실행 시 .\mvnw.cmd 가 되므로 Windows 호출을 매칭하지 못합니다.

Windows 호출을 mvnw.cmd 기준으로 매칭하도록 수정하세요. 워크플로별 호출 수가 다르므로 wrapperInvocations == 2로 고정하지 말고, Unix 및 Windows 호출이 각각 존재하는지 검증하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@etl-service/src/test/java/com/xtrmetl/etl/documentation/MavenWrapperIntegrityTest.java`
around lines 138 - 140, Update containsWrapperInvocation to match Windows
wrapper commands using the mvnw.cmd pattern so workflow text is detected
correctly, while preserving Unix detection. Revise the related invocation-count
validation to require at least one Unix and one Windows invocation rather than
assuming wrapperInvocations == 2.

Sources: Coding guidelines, Learnings


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");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.xtrmetl.etl.documentation;

import org.junit.jupiter.api.Test;

import java.lang.reflect.Method;

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

/**
* Verifies that the workflow matcher recognizes the Windows Maven Wrapper command form.
*/
class MavenWrapperWindowsInvocationMatcherTest {

@Test
void recognizesWindowsWrapperInvocation() throws ReflectiveOperationException {
Method matcher = MavenWrapperIntegrityTest.class.getDeclaredMethod(
"containsWrapperInvocation",
String.class
);
matcher.setAccessible(true);

boolean matched = (boolean) matcher.invoke(null, "run: .\\\\mvnw.cmd -B test");

assertTrue(matched, "Windows Maven Wrapper workflow command must be recognized");
}
}
Loading