-
Notifications
You must be signed in to change notification settings - Fork 0
test(supply-chain): reproduce Maven wrapper integrity gap on current develop #311
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
77b5aa5
test(supply-chain): prove Maven wrapper integrity is unenforced
seonghobae 417f6bc
fix(supply-chain): verify Maven wrapper distribution integrity
seonghobae 44df932
chore: refresh branch base
seonghobae 09460c2
test(ci): cover Windows Maven wrapper command
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| ); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
183 changes: 183 additions & 0 deletions
183
etl-service/src/test/java/com/xtrmetl/etl/documentation/MavenWrapperIntegrityTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 "); | ||
| } | ||
|
|
||
| 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"); | ||
| } | ||
| } | ||
26 changes: 26 additions & 0 deletions
26
...src/test/java/com/xtrmetl/etl/documentation/MavenWrapperWindowsInvocationMatcherTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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:
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
Sources: Coding guidelines, Learnings