diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 29eefb7..964f46e 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -12,12 +12,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Set up JDK 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 17 distribution: 'temurin' @@ -39,10 +39,10 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: ${{ matrix.java }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 65db6be..93476cf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,10 +15,10 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: ${{ matrix.java }} @@ -42,9 +42,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Apache Maven Central - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index db54d6e..3203b11 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -28,13 +28,13 @@ jobs: core.setOutput('base_ref', pr.data.base.ref); core.setOutput('head_sha', pr.data.head.sha); - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: ref: ${{ steps.pr.outputs.head_sha }} fetch-depth: 0 - name: Set up JDK 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 17 distribution: 'temurin' diff --git a/README.md b/README.md index 245109b..3c6a3ee 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ The `DotPathQL` is the core component of this project that allows you to extract - 🎯 **Selective Property Extraction**: Extract only the properties you need - 🚫 **Property Exclusion**: Exclude specific properties and return everything else +- πŸ” **Property Obfuscation**: Replace sensitive property values with "****" while preserving structure - πŸ” **Deep Nested Support**: Navigate through multiple levels of object nesting - πŸ“‹ **Collection Handling**: Process Lists, Arrays, and other Collections - πŸ—ΊοΈ **Map Support**: Handle both simple and complex Map structures @@ -25,10 +26,9 @@ The `DotPathQL` is the core component of this project that allows you to extract ## Quick Start -## Install -- Using the source code `mvn clean install` -- Adding as a dependency - Maven +### Library coordinates +Maven ```xml ca.trackerforce @@ -37,6 +37,11 @@ The `DotPathQL` is the core component of this project that allows you to extract ``` +Gradle +```groovy +implementation 'ca.trackerforce:dot-path-ql:${dot-path-ql.version}' +``` + ### Filter Usage ```java @@ -59,6 +64,17 @@ Map result = new DotPathQL().exclude(userObject, List.of( )); ``` +### Obfuscate Usage + +```java +// Obfuscate specific properties by replacing their values with "****" +Map result = new DotPathQL().obfuscate(userObject, List.of( + "password", + "ssn", + "creditCard.number" +)); +``` + ## Supported Data Structures - Simple Properties (primitive and object types) @@ -178,6 +194,34 @@ List reportFields = List.of( ); ``` +### Data Obfuscation for Security +Mask sensitive information while maintaining data structure for logging, debugging, or sharing with third parties: + +```java +// Obfuscate sensitive fields while keeping the structure intact +List sensitiveFields = List.of( + "password", + "ssn", + "creditCard.number", + "bankAccount.accountNumber", + "personalInfo.phoneNumber" +); + +Map obfuscatedData = doPathQl.obfuscate(userObject, sensitiveFields); + +// Result preserves structure but replaces sensitive values with "****" +// { +// "username": "john_doe", +// "password": "****", +// "ssn": "****", +// "creditCard": { +// "number": "****", +// "expiryDate": "12/25" +// }, +// "email": "john@example.com" +// } +``` + ## JSON Output Convert your filtered or excluded results to JSON format using the built-in `toJson` method. This feature supports both pretty-formatted (indented) and compact (single-line) output. diff --git a/pom.xml b/pom.xml index 8f0d229..1077348 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ ca.trackerforce dot-path-ql - 1.2.0 + 1.2.1 dot-path-ql dotPathQL allows object attribute filtering diff --git a/src/main/java/ca/trackerforce/DotPathQL.java b/src/main/java/ca/trackerforce/DotPathQL.java index cc1ac55..ea74382 100644 --- a/src/main/java/ca/trackerforce/DotPathQL.java +++ b/src/main/java/ca/trackerforce/DotPathQL.java @@ -18,6 +18,7 @@ public class DotPathQL { private final DotPath pathFilter; private final DotPath pathExclude; + private final DotPath pathObfuscate; private final DotPrinter pathPrinter; /** @@ -26,6 +27,7 @@ public class DotPathQL { public DotPathQL() { pathFilter = DotPathFactory.buildFilter(); pathExclude = DotPathFactory.buildExclude(); + pathObfuscate = DotPathFactory.buildObfuscate(); pathPrinter = DotPathFactory.buildPrinter(2); } @@ -58,6 +60,20 @@ public Map exclude(T source, List excludePaths) { return pathExclude.run(source, excludePaths); } + /** + * Obfuscates the given source object based on the specified paths. + * The paths can include nested properties, collections, and arrays. + * Also supports grouped paths syntax like "parent[child1.prop,child2.prop]" + * + * @param the type of the source object + * @param source the source object to obfuscate + * @param obfuscatePaths the list of paths to obfuscate + * @return a map containing the obfuscated properties + */ + public Map obfuscate(T source, List obfuscatePaths) { + return pathObfuscate.run(source, obfuscatePaths); + } + /** * Adds default filter paths that will be included in every filtering operation. * @@ -76,6 +92,15 @@ public void addDefaultExcludePaths(List paths) { pathExclude.addDefaultPaths(paths); } + /** + * Adds default obfuscate paths that will be included in every obfuscation operation. + * + * @param paths the list of default obfuscate paths to add + */ + public void addDefaultObfuscatePaths(List paths) { + pathObfuscate.addDefaultPaths(paths); + } + /** * Converts the source object to a map representation. * diff --git a/src/main/java/ca/trackerforce/DotUtils.java b/src/main/java/ca/trackerforce/DotUtils.java index d949a2d..855eb4d 100644 --- a/src/main/java/ca/trackerforce/DotUtils.java +++ b/src/main/java/ca/trackerforce/DotUtils.java @@ -66,6 +66,8 @@ public static List> listFrom(Map source, Str * * @param source the source map * @param property the property to extract or a dot-notated path for nested properties + * @param clazz the class type of the list elements + * @param the type of the list elements * @return the extracted list of maps or an empty list if not found * @throws ClassCastException if the property is not a list of maps */ @@ -132,5 +134,3 @@ private static Object[] convertToObjectArray(Object array) { return result; } } - - diff --git a/src/main/java/ca/trackerforce/path/DotPathFactory.java b/src/main/java/ca/trackerforce/path/DotPathFactory.java index fbb43e1..17b6178 100644 --- a/src/main/java/ca/trackerforce/path/DotPathFactory.java +++ b/src/main/java/ca/trackerforce/path/DotPathFactory.java @@ -29,6 +29,17 @@ public static DotPath buildExclude() { return new PathExclude(); } + /** + * Builds and returns a new instance of PathExclude configured for obfuscation mode. + * + * @return a new PathExclude instance with obfuscation enabled + */ + public static PathExclude buildObfuscate() { + PathExclude pathExclude = new PathExclude(); + pathExclude.setObfuscateMode(true); + return pathExclude; + } + /** * Builds and returns a new instance of PathPrinter with the specified indentation size. * diff --git a/src/main/java/ca/trackerforce/path/PathExclude.java b/src/main/java/ca/trackerforce/path/PathExclude.java index a9fe2db..8870eb0 100644 --- a/src/main/java/ca/trackerforce/path/PathExclude.java +++ b/src/main/java/ca/trackerforce/path/PathExclude.java @@ -10,6 +10,12 @@ private enum SkipValue { INSTANCE } + private boolean obfuscateMode = false; + + public void setObfuscateMode(boolean obfuscateMode) { + this.obfuscateMode = obfuscateMode; + } + public Map execute(T source, List excludePaths) { Map result = new LinkedHashMap<>(); excludePaths.addAll(0, defaultPaths); @@ -48,9 +54,16 @@ private void buildExcluding(Map target, Object source, String cu return; } + excludeFromNode(target, source, currentPath, node); + } + + private void excludeFromNode(Map target, Object source, String currentPath, ExclusionNode node) { for (String prop : getPropertyNames(source.getClass())) { ExclusionNode childNode = node == null ? null : node.getChildren().get(prop); if (childNode != null && childNode.isExcludeSelf() && childNode.getChildren().isEmpty()) { + if (obfuscateMode) { + target.put(prop, "****"); + } continue; } @@ -69,6 +82,9 @@ private void excludeFromMap(Map target, String currentPath, Excl ExclusionNode childNode = node == null ? null : node.getChildren().get(key); if (childNode != null && childNode.isExcludeSelf() && childNode.getChildren().isEmpty()) { + if (obfuscateMode) { + target.put(key, "****"); + } continue; } diff --git a/src/test/java/ca/trackerforce/ExcludeTypeClassTest.java b/src/test/java/ca/trackerforce/ExcludeTypeClassTest.java index f3013b3..fecfa29 100644 --- a/src/test/java/ca/trackerforce/ExcludeTypeClassTest.java +++ b/src/test/java/ca/trackerforce/ExcludeTypeClassTest.java @@ -17,14 +17,12 @@ void shouldExcludeAndNotReturnPrivateAttributes() { var customer = Customer.of(); // When - var result = dotPathQL.exclude(customer, List.of("metadata.tags")); + var result = dotPathQL.exclude(customer, List.of("email")); // Then - var metadata = DotUtils.mapFrom(result, "metadata"); - assertEquals(0, metadata.size()); // No fields should remain after excluding tags and private password + var metadata = DotUtils.mapFrom(result, "metadata"); // private field + assertEquals(0, metadata.size()); - var features = DotUtils.listFrom(result, "features"); - assertEquals(2, features.size()); - assertTrue(features.get(0).containsKey("isEnabled")); // checking boolean field + assertFalse(result.containsKey("email")); } } diff --git a/src/test/java/ca/trackerforce/ObfuscateTypeClassRecordTest.java b/src/test/java/ca/trackerforce/ObfuscateTypeClassRecordTest.java new file mode 100644 index 0000000..a3f7609 --- /dev/null +++ b/src/test/java/ca/trackerforce/ObfuscateTypeClassRecordTest.java @@ -0,0 +1,161 @@ +package ca.trackerforce; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +class ObfuscateTypeClassRecordTest { + + DotPathQL dotPathQL = new DotPathQL(); + + static Stream userDetailProvider() { + return Stream.of( + Arguments.of("Record type", ca.trackerforce.fixture.record.UserDetail.of()), + Arguments.of("Class type", ca.trackerforce.fixture.clazz.UserDetail.of()) + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("userDetailProvider") + void shouldObfuscateSimpleNestedField(String implementation, Object userDetail) { + // When + var result = dotPathQL.obfuscate(userDetail, List.of("orders.orderId")); + + // Then + var orders = DotUtils.listFrom(result, "orders"); + assertNotNull(orders); + assertEquals(2, orders.size()); + assertEquals("****", orders.get(0).get("orderId")); + assertEquals("****", orders.get(1).get("orderId")); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("userDetailProvider") + void shouldObfuscateMultipleSameBranch(String implementation, Object userDetail) { + // When + var result = dotPathQL.obfuscate(userDetail, List.of( + "address.street", + "address.city" + )); + + // Then + var address = DotUtils.mapFrom(result, "address"); + assertNotNull(address); + assertEquals("****", address.get("street")); + assertEquals("****", address.get("city")); + assertTrue(address.containsKey("zipCode")); + assertTrue(address.containsKey("country")); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("userDetailProvider") + void shouldObfuscateMultipleDifferentBranches(String implementation, Object userDetail) { + // When + var result = dotPathQL.obfuscate(userDetail, List.of( + "address.street", + "orders.products.description", + "additionalInfo.lastLogin" + )); + + // Then + var address = DotUtils.mapFrom(result, "address"); + assertNotNull(address); + assertEquals("****", address.get("street")); + assertTrue(address.containsKey("city")); + + // orders products have no description + var orders = DotUtils.listFrom(result, "orders"); + var firstOrderProducts = DotUtils.listFrom(orders.get(0), "products"); + assertNotNull(firstOrderProducts); + assertEquals(2, firstOrderProducts.size()); + assertEquals("****", firstOrderProducts.get(0).get("description")); + assertEquals("****", firstOrderProducts.get(1).get("description")); + + // additionalInfo without lastLogin + var addInfo = DotUtils.mapFrom(result, "additionalInfo"); + assertNotNull(addInfo); + assertEquals("****", addInfo.get("lastLogin")); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("userDetailProvider") + void shouldObfuscateGroupedPaths(String implementation, Object userDetail) { + // When + var result = dotPathQL.obfuscate(userDetail, List.of("locations[home.street,work.city]")); + + // Then + var locations = DotUtils.mapFrom(result, "locations"); + assertNotNull(locations); + + var home = DotUtils.mapFrom(locations, "home"); + assertNotNull(home); + assertEquals("****", home.get("street")); + assertTrue(home.containsKey("city")); + + var work = DotUtils.mapFrom(locations, "work"); + assertNotNull(work); + assertEquals("****", work.get("city")); + assertTrue(work.containsKey("street")); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("userDetailProvider") + void shouldAddDefaultObfuscatePaths(String implementation, Object userDetail) { + // When + dotPathQL.addDefaultObfuscatePaths(List.of("username")); + var result = dotPathQL.obfuscate(userDetail, List.of("address.city")); + + // Then + assertEquals("****", result.get("username")); + var address = DotUtils.mapFrom(result, "address"); + assertNotNull(address); + assertEquals("****", address.get("city")); + assertTrue(address.containsKey("street")); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("userDetailProvider") + void shouldObfuscateSimpleNestedFieldFromMapSource(String implementation, Object userDetail) { + // When + var source = dotPathQL.toMap(userDetail); // Convert to Map for testing + var result = dotPathQL.obfuscate(source, List.of("orders.orderId")); + + // Then + var orders = DotUtils.listFrom(result, "orders"); + assertNotNull(orders); + assertEquals(2, orders.size()); + assertEquals("****", orders.get(0).get("orderId")); + assertEquals("****", orders.get(1).get("orderId")); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("userDetailProvider") + void shouldNotObfuscateWhenPathNotExists(String implementation, Object userDetail) { + // When + var source = dotPathQL.toMap(userDetail); // Convert to Map for testing + var result = dotPathQL.obfuscate(source, List.of( + "invalidProperty" // Non-existent property + )); + + // Then + assertNotNull(result); + assertEquals(source, result); // Should be unchanged + } + + @Test + void shouldReturnEmptyMapWhenSourceIsNull() { + // When + var result = dotPathQL.obfuscate(null, List.of("orders.orderId")); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + } + +} diff --git a/src/test/java/ca/trackerforce/ObfuscateTypeClassTest.java b/src/test/java/ca/trackerforce/ObfuscateTypeClassTest.java new file mode 100644 index 0000000..3d0a52e --- /dev/null +++ b/src/test/java/ca/trackerforce/ObfuscateTypeClassTest.java @@ -0,0 +1,29 @@ +package ca.trackerforce; + +import ca.trackerforce.fixture.clazz.customer.Customer; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ObfuscateTypeClassTest { + + DotPathQL dotPathQL = new DotPathQL(); + + @Test + void shouldObfuscateAndNotReturnPrivateAttributes() { + // Given + var customer = Customer.of(); + + // When + var result = dotPathQL.obfuscate(customer, List.of("email")); + + // Then + var metadata = DotUtils.mapFrom(result, "metadata"); // private field + assertEquals(0, metadata.size()); + + var email = result.get("email"); + assertEquals("****", email); + } +}