diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityBaselineTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityBaselineTest.java new file mode 100644 index 00000000..582c3c18 --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityBaselineTest.java @@ -0,0 +1,163 @@ +package com.xtrmetl.etl.documentation; + +import org.junit.jupiter.api.Test; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.InputSource; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Guards the shared Maven dependency-management boundary against Jackson Databind versions below + * the selected patched 2.21.5 security baseline. + */ +class JacksonSecurityBaselineTest { + + private static final Path PROJECT_ROOT = projectRoot(); + + @Test + void jacksonSecurityBomPrecedesImportedSpringBootDependencyManagement() throws IOException { + String pom = Files.readString(PROJECT_ROOT.resolve("pom.xml"), StandardCharsets.UTF_8); + + assertJacksonBomContract(pom); + } + + @Test + void rejectsJacksonBomDecoysOutsideRootDependencyManagement() { + String pomWithDecoy = """ + + + 2.21.5 + + + + + + spring-boot-dependencies + + + + + """; + + assertThrows(AssertionError.class, () -> assertJacksonBomContract(pomWithDecoy)); + } + + private static void assertJacksonBomContract(String pom) { + Element project = parseProject(pom); + Element properties = requireDirectChild(project, "properties"); + assertEquals( + "2.21.5", + requireDirectChild(properties, "jackson-bom.version").getTextContent().trim(), + "Root properties must pin Jackson 2.21.5, the current patched 2.21 LTS baseline" + ); + + Element dependencyManagement = requireDirectChild(project, "dependencyManagement"); + Element dependencies = requireDirectChild(dependencyManagement, "dependencies"); + List managedDependencies = directChildren(dependencies, "dependency"); + + int jacksonIndex = -1; + int springBootIndex = -1; + int jacksonCount = 0; + for (int index = 0; index < managedDependencies.size(); index++) { + Element dependency = managedDependencies.get(index); + String artifactId = directChildText(dependency, "artifactId"); + if ("jackson-bom".equals(artifactId)) { + jacksonCount++; + jacksonIndex = index; + assertEquals("com.fasterxml.jackson", directChildText(dependency, "groupId")); + assertEquals("${jackson-bom.version}", directChildText(dependency, "version")); + assertEquals("pom", directChildText(dependency, "type")); + assertEquals("import", directChildText(dependency, "scope")); + } + if ("spring-boot-dependencies".equals(artifactId)) { + assertEquals("org.springframework.boot", directChildText(dependency, "groupId")); + springBootIndex = index; + } + } + + assertEquals(1, jacksonCount, "Root dependencyManagement must import exactly one Jackson BOM"); + assertTrue(springBootIndex >= 0, "Root dependencyManagement must continue importing Spring Boot dependencies"); + assertTrue( + jacksonIndex < springBootIndex, + "Without the Spring Boot parent POM, the explicit Jackson override BOM must precede spring-boot-dependencies" + ); + } + + private static Element parseProject(String pom) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + return factory.newDocumentBuilder() + .parse(new InputSource(new StringReader(pom))) + .getDocumentElement(); + } catch (Exception exception) { + throw new AssertionError("Root pom.xml must be parseable XML for structural dependency validation", exception); + } + } + + private static Element requireDirectChild(Element parent, String name) { + for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child instanceof Element element && name.equals(elementName(element))) { + return element; + } + } + throw new AssertionError("Missing direct <" + name + "> under <" + elementName(parent) + ">"); + } + + private static List directChildren(Element parent, String name) { + List children = new ArrayList<>(); + for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child instanceof Element element && name.equals(elementName(element))) { + children.add(element); + } + } + return children; + } + + private static String directChildText(Element parent, String name) { + return requireDirectChild(parent, name).getTextContent().trim(); + } + + private static String elementName(Node node) { + return node.getLocalName() == null ? node.getNodeName() : node.getLocalName(); + } + + /** Finds the repository root from root- or module-scoped Maven execution. */ + 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"); + } +} diff --git a/pom.xml b/pom.xml index 7c0ee7c2..c5c46ae4 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,7 @@ 25 3.5.16 2025.0.3 + 2.21.5 42.7.12 3.3.16 2.0.13 @@ -33,6 +34,13 @@ + + com.fasterxml.jackson + jackson-bom + ${jackson-bom.version} + pom + import + org.springframework.boot spring-boot-dependencies