diff --git a/src/main/java/org/ays/common/model/AysAuditLog.java b/src/main/java/org/ays/common/model/AysAuditLog.java index c586e2772..8696e4859 100644 --- a/src/main/java/org/ays/common/model/AysAuditLog.java +++ b/src/main/java/org/ays/common/model/AysAuditLog.java @@ -84,7 +84,7 @@ public AysAuditLogBuilder aysHttpServletRequest(final AysHttpServletRequest aysH this.request.httpMethod = aysHttpServletRequest.getMethod(); this.request.path = aysHttpServletRequest.getPath(); - this.request.body = AysJsonUtil.toEscapedJson(aysHttpServletRequest.getBody()); + this.request.body = AysJsonUtil.toMaskedEscapedJson(aysHttpServletRequest.getBody()); return this; } @@ -99,7 +99,7 @@ public AysAuditLogBuilder aysHttpHeader(final AysHttpHeader aysHttpHeader) { public AysAuditLogBuilder aysHttpServletResponse(final AysHttpServletResponse aysHttpServletResponse) { this.response.httpStatusCode = aysHttpServletResponse.getStatus(); - this.response.body = AysJsonUtil.toEscapedJson(aysHttpServletResponse.getBody()); + this.response.body = AysJsonUtil.toMaskedEscapedJson(aysHttpServletResponse.getBody()); return this; } diff --git a/src/main/java/org/ays/common/model/request/AysHttpHeader.java b/src/main/java/org/ays/common/model/request/AysHttpHeader.java index cfa1b1456..0baa27c75 100644 --- a/src/main/java/org/ays/common/model/request/AysHttpHeader.java +++ b/src/main/java/org/ays/common/model/request/AysHttpHeader.java @@ -3,6 +3,7 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.EqualsAndHashCode; import lombok.Getter; +import org.ays.common.util.AysMaskUtil; import org.springframework.http.HttpHeaders; import org.springframework.util.StringUtils; @@ -58,7 +59,15 @@ private String getAllHeaders(final HttpServletRequest httpServletRequest) { headerNames.remove(X_FORWARDED_FOR.toLowerCase()); return headerNames .stream() - .map(headerName -> headerName + ":" + httpServletRequest.getHeader(headerName)) + .map(headerName -> { + + final String headerValue = httpServletRequest.getHeader(headerName); + if (headerName.equalsIgnoreCase(AUTHORIZATION)) { + return headerName + ":" + AysMaskUtil.mask(headerName, headerValue); + } + + return headerName + ":" + headerValue; + }) .collect(Collectors.joining("; ")); } diff --git a/src/main/java/org/ays/common/model/request/AysPhoneNumberRequest.java b/src/main/java/org/ays/common/model/request/AysPhoneNumberRequest.java index 2e0a5ea08..74d993d40 100644 --- a/src/main/java/org/ays/common/model/request/AysPhoneNumberRequest.java +++ b/src/main/java/org/ays/common/model/request/AysPhoneNumberRequest.java @@ -3,7 +3,6 @@ import jakarta.validation.constraints.NotBlank; import lombok.Getter; import lombok.Setter; -import org.apache.commons.lang3.StringUtils; import org.ays.common.util.validation.PhoneNumber; /** @@ -35,8 +34,4 @@ public String toString() { .formatted(this.countryCode, this.lineNumber); } - public boolean isBlank() { - return StringUtils.isBlank(this.countryCode) && StringUtils.isBlank(this.lineNumber); - } - } diff --git a/src/main/java/org/ays/common/util/AysJsonUtil.java b/src/main/java/org/ays/common/util/AysJsonUtil.java index 8b6b4d8b6..2b61aab67 100644 --- a/src/main/java/org/ays/common/util/AysJsonUtil.java +++ b/src/main/java/org/ays/common/util/AysJsonUtil.java @@ -33,18 +33,24 @@ public static String toJson(final Object object) { } /** - * Converts a formatted JSON string into an unformatted (compact) JSON string. + * Converts a JSON string into a masked and escaped JSON string. *

- * This method parses the input JSON string into a tree structure using the {@link ObjectMapper} - * and then converts it back to a compact string representation. - * If an exception occurs during parsing, an empty string is returned. + * This method performs the following steps: + *

+ * If an exception occurs during parsing or masking, an empty string is returned. + *

* - * @param json the formatted JSON string to be converted - * @return a compact (unformatted) JSON string, or an empty string if the input is invalid + * @param json The JSON string to be masked and escaped. + * @return A masked and escaped JSON string, or an empty string if the input is invalid. */ - public static String toEscapedJson(final String json) { + public static String toMaskedEscapedJson(final String json) { try { final JsonNode jsonNode = OBJECT_MAPPER.readTree(json); + AysMaskUtil.mask(jsonNode); return jsonNode.toString().replace("\"", "\\\""); } catch (JsonProcessingException exception) { return ""; diff --git a/src/main/java/org/ays/common/util/AysMaskUtil.java b/src/main/java/org/ays/common/util/AysMaskUtil.java new file mode 100644 index 000000000..d153a8974 --- /dev/null +++ b/src/main/java/org/ays/common/util/AysMaskUtil.java @@ -0,0 +1,206 @@ +package org.ays.common.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.experimental.UtilityClass; +import org.apache.commons.lang3.StringUtils; + +import java.util.Iterator; + +/** + * Utility class for masking sensitive information in JSON data. + *

+ * This class provides methods for masking fields within a {@link JsonNode} or individual string values. + * It is designed to handle common sensitive fields such as tokens, passwords, email addresses, and more. + *

+ * + *

Unmasked JSON:

+ *
+ * {
+ *   "emailAddress": "test@example.com",
+ *   "password": "123456789",
+ *   "lineNumber": "1234567890",
+ *   "address": "123 Main Street, Springfield",
+ *   "firstName": "John",
+ *   "lastName": "Doe"
+ * }
+ * 
+ * + *

Masked JSON:

+ *
+ * {
+ *   "emailAddress": "tes******com",
+ *   "password": "******",
+ *   "lineNumber": "******7890",
+ *   "address": "123 Main ******field",
+ *   "firstName": "Joh******ohn",
+ *   "lastName": "Doe******oe"
+ * }
+ * 
+ */ +@UtilityClass +public class AysMaskUtil { + + private static final String MASKED_VALUE = "******"; + + /** + * Masks sensitive fields in the given {@link JsonNode}. + *

+ * This method recursively iterates through the fields in the JSON object or array, + * and applies masking to fields identified as sensitive. + *

+ * + * @param jsonNode the JSON node to process for masking + */ + public static void mask(final JsonNode jsonNode) { + + if (jsonNode.isObject()) { + + ObjectNode objectNode = (ObjectNode) jsonNode; + Iterator fieldNames = objectNode.fieldNames(); + + while (fieldNames.hasNext()) { + + String fieldName = fieldNames.next(); + JsonNode fieldValue = objectNode.get(fieldName); + + if (fieldValue.isValueNode()) { + String maskedValue = mask(fieldName, fieldValue.asText()); + objectNode.put(fieldName, maskedValue); + continue; + } + + mask(fieldValue); + } + + return; + } + + if (jsonNode.isArray()) { + ArrayNode arrayNode = (ArrayNode) jsonNode; + for (JsonNode arrayElement : arrayNode) { + mask(arrayElement); + } + } + } + + /** + * Masks the value of a specific field based on its name. + *

+ * This method identifies sensitive fields by their names and applies the appropriate + * masking strategy. Fields not recognized as sensitive are returned without modification. + *

+ * + * @param field the name of the field + * @param value the value to mask + * @return the masked value + */ + public static String mask(final String field, final String value) { + + if ("null".equalsIgnoreCase(value) || StringUtils.isBlank(value)) { + return value; + } + + return switch (field) { + case "Authorization", "accessToken", "refreshToken" -> maskToken(value); + case "password" -> maskPassword(); + case "emailAddress" -> maskEmailAddress(value); + case "lineNumber" -> maskLineNumber(value); + case "address" -> maskAddress(value); + case "firstName", "lastName", "applicantFirstName", "applicantLastName" -> maskName(value); + default -> value; + }; + } + + /** + * Masks token fields such as "Authorization", "accessToken", or "refreshToken". + * + * @param value the token value to mask + * @return the masked token + */ + private static String maskToken(String value) { + + if (value.length() <= 20) { + return value; + } + + return value.substring(0, 20) + MASKED_VALUE; + } + + /** + * Masks password fields, replacing their values with a fixed placeholder. + * + * @return the masked password placeholder + */ + private static String maskPassword() { + return MASKED_VALUE; + } + + /** + * Masks email addresses by revealing the first three and last three characters, + * replacing the rest with asterisks. + * + * @param value the email address to mask + * @return the masked email address + */ + private static String maskEmailAddress(String value) { + + if (value.length() <= 3) { + return value; + } + + int length = value.length(); + String firstThree = value.substring(0, 3); + String lastThree = value.substring(length - 3); + return firstThree + MASKED_VALUE + lastThree; + } + + /** + * Masks address fields by revealing the first ten and last ten characters, + * replacing the middle part with asterisks. + * + * @param value the address to mask + * @return the masked address + */ + private static String maskAddress(String value) { + + if (value.length() <= 10) { + return value; + } + + return value.substring(0, 10) + MASKED_VALUE + value.substring(value.length() - 10); + } + + /** + * Masks line numbers by revealing the last four digits, replacing the preceding digits with asterisks. + * + * @param value the line number to mask + * @return the masked line number + */ + private static String maskLineNumber(String value) { + + if (value.length() <= 4) { + return value; + } + + return MASKED_VALUE + value.substring(value.length() - 4); + } + + /** + * Masks name fields such as "firstName", "lastName", or similar by revealing the first three + * and last three characters, replacing the middle part with asterisks. + * + * @param value the name to mask + * @return the masked name + */ + private static String maskName(String value) { + + if (value.length() <= 3) { + return value; + } + + return value.substring(0, 3) + MASKED_VALUE + value.substring(value.length() - 3); + } + +} diff --git a/src/main/java/org/ays/emergency_application/model/request/EmergencyEvacuationApplicationRequest.java b/src/main/java/org/ays/emergency_application/model/request/EmergencyEvacuationApplicationRequest.java index 35471d1c8..7545a7a0a 100644 --- a/src/main/java/org/ays/emergency_application/model/request/EmergencyEvacuationApplicationRequest.java +++ b/src/main/java/org/ays/emergency_application/model/request/EmergencyEvacuationApplicationRequest.java @@ -86,9 +86,12 @@ private boolean isAllApplicantFieldsFilled() { return true; } + return !StringUtils.isBlank(this.applicantFirstName) && !StringUtils.isBlank(this.applicantLastName) && - this.applicantPhoneNumber != null && !this.applicantPhoneNumber.isBlank(); + this.applicantPhoneNumber != null + && + !(StringUtils.isBlank(this.applicantPhoneNumber.getCountryCode()) && StringUtils.isBlank(this.applicantPhoneNumber.getLineNumber())); } @JsonIgnore @@ -111,7 +114,7 @@ private boolean isPhoneNumberMustNotBeSameOne() { @AssertTrue(message = "source city/district and target city/district must be different") @SuppressWarnings("This method is unused by the application directly but Spring is using it in the background.") private boolean isSourceCityAndDistrictDifferentFromTargetCityAndDistrict() { - + if (this.sourceCity == null || this.sourceDistrict == null || this.targetCity == null || this.targetDistrict == null) { return true; } @@ -119,7 +122,7 @@ private boolean isSourceCityAndDistrictDifferentFromTargetCityAndDistrict() { if (!this.sourceCity.equalsIgnoreCase(this.targetCity)){ return true; } - + return !this.sourceDistrict.equalsIgnoreCase(this.targetDistrict); }