Skip to content
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

AYS-509 | Restricted Fields Have Been Masked while Saving Logs #423

Merged
merged 5 commits into from
Jan 3, 2025
Merged
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
4 changes: 2 additions & 2 deletions src/main/java/org/ays/common/model/AysAuditLog.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand Down
11 changes: 10 additions & 1 deletion src/main/java/org/ays/common/model/request/AysHttpHeader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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("; "));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -35,8 +34,4 @@ public String toString() {
.formatted(this.countryCode, this.lineNumber);
}

public boolean isBlank() {
return StringUtils.isBlank(this.countryCode) && StringUtils.isBlank(this.lineNumber);
}

}
20 changes: 13 additions & 7 deletions src/main/java/org/ays/common/util/AysJsonUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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:
* <ul>
* <li>Parses the input JSON string into a tree structure using the {@link ObjectMapper}.</li>
* <li>Masks sensitive fields in the JSON using {@link AysMaskUtil}.</li>
* <li>Escapes double quotes in the JSON string by replacing them with backslash-escaped quotes.</li>
* </ul>
* If an exception occurs during parsing or masking, an empty string is returned.
* </p>
*
* @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 "";
Expand Down
206 changes: 206 additions & 0 deletions src/main/java/org/ays/common/util/AysMaskUtil.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
* </p>
*
* <p><strong>Unmasked JSON:</strong></p>
* <pre>
* {
* "emailAddress": "[email protected]",
* "password": "123456789",
* "lineNumber": "1234567890",
* "address": "123 Main Street, Springfield",
* "firstName": "John",
* "lastName": "Doe"
* }
* </pre>
*
* <p><strong>Masked JSON:</strong></p>
* <pre>
* {
* "emailAddress": "tes******com",
* "password": "******",
* "lineNumber": "******7890",
* "address": "123 Main ******field",
* "firstName": "Joh******ohn",
* "lastName": "Doe******oe"
* }
* </pre>
*/
@UtilityClass
public class AysMaskUtil {

private static final String MASKED_VALUE = "******";

/**
* Masks sensitive fields in the given {@link JsonNode}.
* <p>
* This method recursively iterates through the fields in the JSON object or array,
* and applies masking to fields identified as sensitive.
* </p>
*
* @param jsonNode the JSON node to process for masking
*/
public static void mask(final JsonNode jsonNode) {

if (jsonNode.isObject()) {

ObjectNode objectNode = (ObjectNode) jsonNode;
Iterator<String> 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.
* <p>
* This method identifies sensitive fields by their names and applies the appropriate
* masking strategy. Fields not recognized as sensitive are returned without modification.
* </p>
*
* @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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -111,15 +114,15 @@ 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;
}

if (!this.sourceCity.equalsIgnoreCase(this.targetCity)){
return true;
}

return !this.sourceDistrict.equalsIgnoreCase(this.targetDistrict);
}

Expand Down
Loading