/* * Copyright (c) 2016 Network New Technologies Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * You may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.networknt.mask; import com.fasterxml.jackson.databind.JsonNode; import com.jayway.jsonpath.*; import com.networknt.config.Config; import com.networknt.utility.ModuleRegistry; import org.apache.commons.lang.StringUtils; import org.owasp.encoder.Encode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A utility to mask sensitive data based on regex pattern before logging */ public class Mask { static Map<String, Pattern> patternCache = new ConcurrentHashMap<>(); private static final String MASK_CONFIG = "mask"; public static final String MASK_REPLACEMENT_CHAR = "*"; public static final String MASK_TYPE_STRING = "string"; public static final String MASK_TYPE_REGEX = "regex"; public static final String MASK_TYPE_JSON = "json"; static final Logger logger = LoggerFactory.getLogger(Mask.class); private static Map<String, Object> config = null; static { config = Config.getInstance().getJsonMapConfigNoCache(MASK_CONFIG); ModuleRegistry.registerModule(Mask.class.getName(), config, null); } /** * Mask the input string with a list of patterns indexed by key in string section in mask.json * This is usually used to mask header values, query parameters and uri parameters * * @param input String The source of the string that needs to be masked * @param key String The key that maps to a list of patterns for masking in config file * @return Masked result */ public static String maskString(String input, String key) { String output = input; Map<String, Object> stringConfig = (Map<String, Object>) config.get(MASK_TYPE_STRING); if (stringConfig != null) { Map<String, Object> keyConfig = (Map<String, Object>) stringConfig.get(key); if (keyConfig != null) { Set<String> patterns = keyConfig.keySet(); for (String pattern : patterns) { output = output.replaceAll(pattern, (String) keyConfig.get(pattern)); } } } return output; } /** * Replace a string input with a pattern found in regex section with key as index. Usually, * it is used to replace header, query parameter, uri parameter to same length of stars(*) * * @param input String The source of the string that needs to be masked * @param key String The key maps to a list of name to pattern pair * @param name String The name of the pattern in the key list * @return String Masked result */ public static String maskRegex(String input, String key, String name) { Map<String, Object> regexConfig = (Map<String, Object>) config.get(MASK_TYPE_REGEX); if (regexConfig != null) { Map<String, Object> keyConfig = (Map<String, Object>) regexConfig.get(key); if (keyConfig != null) { String regex = (String) keyConfig.get(name); if (regex != null && regex.length() > 0) { return replaceWithMask(input, MASK_REPLACEMENT_CHAR.charAt(0), regex); } } } return input; } /** * Replace values in JSON using json path * @param input String The source of the string that needs to be masked * @param key String The key maps to a list of json path for masking * @return String Masked result */ public static String maskJson(String input, String key) { DocumentContext ctx = JsonPath.parse(input); Map<String, Object> jsonConfig = (Map<String, Object>) config.get(MASK_TYPE_JSON); if (jsonConfig != null) { Map<String, Object> patternMap = (Map<String, Object>) jsonConfig.get(key); if (patternMap != null) { JsonNode configNode = Config.getInstance().getMapper().valueToTree(patternMap); Iterator<Map.Entry<String, JsonNode>> iterator = configNode.fields(); while (iterator.hasNext()) { Map.Entry<String, JsonNode> entry = iterator.next(); applyMask(entry, ctx); } return ctx.jsonString(); } else { logger.warn("mask.json doesn't contain the key {} ", Encode.forJava(key)); return input; } } return input; } private static void applyMask(Map.Entry<String, JsonNode> entry, DocumentContext ctx) { Object value; String jsonPath = entry.getKey(); try { value = ctx.read(jsonPath); if (!(value instanceof String || value instanceof Integer || value instanceof List<?>)) { logger.error("The value specified by path {} cannot be masked", jsonPath); } else { if (!(value instanceof List<?>)) { ctx.set(jsonPath, replaceWithMask(value.toString(), MASK_REPLACEMENT_CHAR.charAt(0), entry.getValue().asText())); } else { maskList(ctx, jsonPath, entry.getValue().asText()); } } } catch (PathNotFoundException e) { logger.warn("JsonPath {} could not be found.", jsonPath); } } private static String replaceWithMask(String stringToBeMasked, char maskingChar, String regex) { if (stringToBeMasked.length() == 0) { return stringToBeMasked; } String replacementString = ""; String padGroup; if (!StringUtils.isEmpty(regex)) { try { Pattern pattern = patternCache.get(regex); if (pattern == null) { pattern = Pattern.compile(regex); patternCache.put(regex, pattern); } Matcher matcher = pattern.matcher(stringToBeMasked); if (matcher.matches()) { String currentGroup; for (int i = 0; i < matcher.groupCount(); i++) { currentGroup = matcher.group(i + 1); padGroup = StringUtils.rightPad("", currentGroup.length(), maskingChar); stringToBeMasked = StringUtils.replace(stringToBeMasked, currentGroup, padGroup, 1); } replacementString = stringToBeMasked; } } catch (Exception e) { replacementString = StringUtils.rightPad("", stringToBeMasked.length(), maskingChar); } } else { replacementString = StringUtils.rightPad("", stringToBeMasked.length(), maskingChar); } return replacementString; } private static void maskList(DocumentContext ctx, String jsonPath, String expression) { ctx.configuration().addOptions(Option.AS_PATH_LIST); Configuration conf = Configuration.builder().options(Option.AS_PATH_LIST).build(); DocumentContext context = JsonPath.using(conf).parse(ctx.jsonString()); List<String> pathList = context.read(jsonPath); for (String path : pathList) { Object value = ctx.read(path); ctx.set(path, replaceWithMask(value.toString(), MASK_REPLACEMENT_CHAR.charAt(0), expression)); } } }