Refactor: Remove SensitiveDataMasker and fix PaginationRequestValidator
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m55s
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m55s
This commit is contained in:
@@ -16,7 +16,6 @@ package io.shinhanlife.dap.mcg.audit;
|
||||
* </pre>
|
||||
*/
|
||||
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
|
||||
import io.shinhanlife.dap.mcg.guardrail.SensitiveDataMasker;
|
||||
import io.shinhanlife.dap.mcg.security.McpRequestContext;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.slf4j.Logger;
|
||||
@@ -32,11 +31,8 @@ import org.springframework.stereotype.Service;
|
||||
public class AuditLogService {
|
||||
private static final Logger audit = LoggerFactory.getLogger("MCP_AUDIT");
|
||||
private final McpGatewayProperties properties;
|
||||
private final SensitiveDataMasker masker;
|
||||
|
||||
public AuditLogService(McpGatewayProperties properties, SensitiveDataMasker masker) {
|
||||
public AuditLogService(McpGatewayProperties properties) {
|
||||
this.properties = properties;
|
||||
this.masker = masker;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,7 +43,7 @@ public class AuditLogService {
|
||||
return;
|
||||
}
|
||||
audit.info("event=tool_started requestId={} agentId={} userId={} clientAddress={} tool={} arguments={}",
|
||||
context.requestId(), context.agentId(), context.userId(), context.clientAddress(), toolName, masker.mask(arguments));
|
||||
context.requestId(), context.agentId(), context.userId(), context.clientAddress(), toolName, arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
package io.shinhanlife.dap.mcg.guardrail;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.guardrail
|
||||
* @className SensitiveDataMasker
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Audit Log, Redis Trace, Agent 응답 preview에 남으면 안 되는 민감정보를 마스킹합니다.
|
||||
*/
|
||||
@Component
|
||||
public class SensitiveDataMasker {
|
||||
private static final Set<String> SENSITIVE_KEYS = Set.of(
|
||||
"password", "passwd", "pwd", "token", "accessToken", "refreshToken", "secret",
|
||||
"ssn", "rrn", "residentNumber", "cardNumber", "accountNumber", "accountNo",
|
||||
"phone", "mobile", "email", "idempotencyKey");
|
||||
private static final Pattern EMAIL = Pattern.compile("([a-zA-Z0-9._%+-]{2})[a-zA-Z0-9._%+-]*(@[a-zA-Z0-9.-]+)");
|
||||
private static final Pattern CARD_OR_ACCOUNT = Pattern.compile("\\b(\\d{4})\\d{4,12}(\\d{2,4})\\b");
|
||||
private final ObjectMapper json;
|
||||
|
||||
public SensitiveDataMasker(ObjectMapper json) {
|
||||
this.json = json;
|
||||
}
|
||||
|
||||
/**
|
||||
* JsonNode 전체를 재귀적으로 순회하며 민감 key와 민감 패턴을 마스킹합니다.
|
||||
*/
|
||||
public JsonNode mask(JsonNode input) {
|
||||
if (input == null || input.isMissingNode() || input.isNull()) {
|
||||
return json.createObjectNode();
|
||||
}
|
||||
if (input.isArray()) {
|
||||
ArrayNode masked = json.createArrayNode();
|
||||
for (JsonNode item : input) {
|
||||
masked.add(mask(item));
|
||||
}
|
||||
return masked;
|
||||
}
|
||||
if (input.isObject()) {
|
||||
ObjectNode masked = json.createObjectNode();
|
||||
Iterator<Map.Entry<String, JsonNode>> fields = input.fields();
|
||||
while (fields.hasNext()) {
|
||||
Map.Entry<String, JsonNode> entry = fields.next();
|
||||
String key = entry.getKey();
|
||||
JsonNode value = entry.getValue();
|
||||
if (isSensitiveKey(key)) {
|
||||
masked.put(key, "***");
|
||||
} else {
|
||||
masked.set(key, mask(value));
|
||||
}
|
||||
}
|
||||
return masked;
|
||||
}
|
||||
if (input.isTextual()) {
|
||||
return json.valueToTree(maskText(input.asText()));
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
private boolean isSensitiveKey(String key) {
|
||||
return key != null && SENSITIVE_KEYS.stream().anyMatch(sensitive -> sensitive.equalsIgnoreCase(key));
|
||||
}
|
||||
|
||||
private String maskText(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return value;
|
||||
}
|
||||
String masked = EMAIL.matcher(value).replaceAll("$1***$2");
|
||||
return CARD_OR_ACCOUNT.matcher(masked).replaceAll("$1********$2");
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ package io.shinhanlife.dap.mcg.redis;
|
||||
* </pre>
|
||||
*/
|
||||
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
|
||||
import io.shinhanlife.dap.mcg.guardrail.SensitiveDataMasker;
|
||||
import io.shinhanlife.dap.mcg.security.McpRequestContext;
|
||||
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
|
||||
import org.slf4j.Logger;
|
||||
@@ -49,19 +48,16 @@ public class RedisToolTraceService {
|
||||
private final ObjectProvider<StringRedisTemplate> redisProvider;
|
||||
private final ObjectMapper json;
|
||||
private final McpMonitorEventService monitorEvents;
|
||||
private final SensitiveDataMasker masker;
|
||||
private final Map<String, AttemptState> attemptStates = new ConcurrentHashMap<>();
|
||||
|
||||
public RedisToolTraceService(McpGatewayProperties properties,
|
||||
ObjectProvider<StringRedisTemplate> redisProvider,
|
||||
ObjectMapper json,
|
||||
McpMonitorEventService monitorEvents,
|
||||
SensitiveDataMasker masker) {
|
||||
McpMonitorEventService monitorEvents) {
|
||||
this.properties = properties;
|
||||
this.redisProvider = redisProvider;
|
||||
this.json = json;
|
||||
this.monitorEvents = monitorEvents;
|
||||
this.masker = masker;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,7 +202,7 @@ public class RedisToolTraceService {
|
||||
arguments.fieldNames().forEachRemaining(argNames::add);
|
||||
trace.put("argumentNames", argNames);
|
||||
|
||||
trace.put("arguments", masker.mask(arguments).toString());
|
||||
trace.put("arguments", arguments.toString());
|
||||
trace.put("responseSummary", responseSummary(responseText));
|
||||
trace.put("timestamp", Instant.now().toString());
|
||||
return json.writeValueAsString(trace);
|
||||
|
||||
@@ -23,7 +23,6 @@ import io.shinhanlife.dap.mcg.resilience.RetryPolicy;
|
||||
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
|
||||
import io.shinhanlife.dap.mcg.dto.OperationType;
|
||||
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
|
||||
import io.shinhanlife.dap.mcg.guardrail.SensitiveDataMasker;
|
||||
import io.shinhanlife.dap.mcg.guardrail.GuardrailService;
|
||||
import io.shinhanlife.dap.mcg.security.McpRequestContext;
|
||||
import io.shinhanlife.dap.mcg.security.McpRequestContextResolver;
|
||||
@@ -58,7 +57,6 @@ public class ExecuteService {
|
||||
private final ToolPlanner planner;
|
||||
private final KillSwitchService killSwitchService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final SensitiveDataMasker dataMasker;
|
||||
private final GuardrailService guardrailService;
|
||||
private final McpRequestContextResolver contextResolver;
|
||||
private final AuditLogService auditLogService;
|
||||
@@ -76,7 +74,6 @@ public class ExecuteService {
|
||||
public ExecuteService(ToolPlanner planner,
|
||||
KillSwitchService killSwitchService,
|
||||
ObjectMapper objectMapper,
|
||||
SensitiveDataMasker dataMasker,
|
||||
GuardrailService guardrailService,
|
||||
McpRequestContextResolver contextResolver,
|
||||
AuditLogService auditLogService,
|
||||
@@ -92,7 +89,6 @@ public class ExecuteService {
|
||||
this.planner = planner;
|
||||
this.killSwitchService = killSwitchService;
|
||||
this.objectMapper = objectMapper;
|
||||
this.dataMasker = dataMasker;
|
||||
this.guardrailService = guardrailService;
|
||||
this.contextResolver = contextResolver;
|
||||
this.auditLogService = auditLogService;
|
||||
@@ -266,14 +262,14 @@ public class ExecuteService {
|
||||
headers.put("trace-id", context.requestId());
|
||||
headers.put("request-id", java.util.UUID.randomUUID().toString());
|
||||
|
||||
ObjectNode pageArguments = paginationValidator.normalize(arguments);
|
||||
ObjectNode pageArguments = paginationValidator.normalize(metadata, arguments);
|
||||
LargeToolResponseService.Collector collector = largeResponses.newCollector(metadata.getName(), context.requestId());
|
||||
|
||||
while (true) {
|
||||
Map<String, Object> pagePayload = objectMapper.convertValue(pageArguments, Map.class);
|
||||
|
||||
try {
|
||||
log.info(" [ExecuteService] 요청 페이로드(마스킹 적용): {}", objectMapper.writeValueAsString(dataMasker.mask(objectMapper.valueToTree(pagePayload))));
|
||||
log.info(" [ExecuteService] 요청 페이로드: {}", objectMapper.writeValueAsString(pagePayload));
|
||||
} catch (Exception ignore) {}
|
||||
|
||||
JsonNode data = null;
|
||||
|
||||
@@ -19,7 +19,6 @@ import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
import io.shinhanlife.dap.mcg.config.AgentResponseBudgetProperties;
|
||||
import io.shinhanlife.dap.mcg.guardrail.SensitiveDataMasker;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -30,12 +29,9 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
public class AgentResponseBudgetService {
|
||||
private final AgentResponseBudgetProperties properties;
|
||||
private final ObjectMapper json;
|
||||
private final SensitiveDataMasker masker;
|
||||
|
||||
public AgentResponseBudgetService(AgentResponseBudgetProperties properties, ObjectMapper json, SensitiveDataMasker masker) {
|
||||
public AgentResponseBudgetService(AgentResponseBudgetProperties properties, ObjectMapper json) {
|
||||
this.properties = properties;
|
||||
this.json = json;
|
||||
this.masker = masker;
|
||||
}
|
||||
|
||||
public ObjectNode apply(ObjectNode response) {
|
||||
@@ -109,7 +105,7 @@ public class AgentResponseBudgetService {
|
||||
}
|
||||
|
||||
private JsonNode budgetItem(JsonNode item, BudgetStats stats) {
|
||||
JsonNode masked = masker.mask(item);
|
||||
JsonNode masked = item;
|
||||
if (!masked.isObject()) {
|
||||
return truncateByBytes(masked, stats);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ package io.shinhanlife.dap.mcg.tool.large;
|
||||
import java.util.Iterator;
|
||||
|
||||
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
|
||||
import io.shinhanlife.dap.mcg.guardrail.SensitiveDataMasker;
|
||||
import io.shinhanlife.dap.mcg.resilience.FailureType;
|
||||
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -34,16 +33,13 @@ import java.time.Instant;
|
||||
public class LargeToolResponseService {
|
||||
private final McpGatewayProperties properties;
|
||||
private final ObjectMapper json;
|
||||
private final SensitiveDataMasker masker;
|
||||
private final AgentResponseBudgetService agentBudget;
|
||||
|
||||
public LargeToolResponseService(McpGatewayProperties properties,
|
||||
ObjectMapper json,
|
||||
SensitiveDataMasker masker,
|
||||
AgentResponseBudgetService agentBudget) {
|
||||
this.properties = properties;
|
||||
this.json = json;
|
||||
this.masker = masker;
|
||||
this.agentBudget = agentBudget;
|
||||
}
|
||||
|
||||
@@ -157,8 +153,7 @@ public class LargeToolResponseService {
|
||||
}
|
||||
Page page = pageFrom(data);
|
||||
if (!page.paginated() && pageCount == 0 && count(page.items()) <= pageSize()) {
|
||||
JsonNode masked = masker.mask(data);
|
||||
normalData = masked;
|
||||
normalData = data;
|
||||
pageCount = 1;
|
||||
returnedCount = count(page.items());
|
||||
totalCount = returnedCount;
|
||||
@@ -271,19 +266,18 @@ public class LargeToolResponseService {
|
||||
}
|
||||
|
||||
private JsonNode previewItem(JsonNode item) {
|
||||
JsonNode masked = masker.mask(item);
|
||||
long itemBytes = jsonBytes(masked);
|
||||
long itemBytes = jsonBytes(item);
|
||||
if (itemBytes <= properties.largeResponseMaxItemBytes()) {
|
||||
return masked;
|
||||
return item;
|
||||
}
|
||||
truncated = true;
|
||||
ObjectNode preview = json.createObjectNode();
|
||||
preview.put("truncated", true);
|
||||
preview.put("originalBytes", itemBytes);
|
||||
preview.put("maxItemBytes", properties.largeResponseMaxItemBytes());
|
||||
if (masked.isObject()) {
|
||||
if (item.isObject()) {
|
||||
ArrayNode fieldNames = json.createArrayNode();
|
||||
Iterator<String> fieldNamesIter = masked.fieldNames();
|
||||
Iterator<String> fieldNamesIter = item.fieldNames();
|
||||
while (fieldNamesIter.hasNext()) {
|
||||
fieldNames.add(fieldNamesIter.next());
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ package io.shinhanlife.dap.mcg.tool.large;
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
|
||||
import io.shinhanlife.dap.mcg.resilience.FailureType;
|
||||
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
|
||||
@@ -40,12 +41,18 @@ public class PaginationRequestValidator {
|
||||
/**
|
||||
* pageSize/cursor를 검증한 뒤 Tool 서버에 넘길 안전한 arguments 복사본을 만듭니다.
|
||||
*/
|
||||
public ObjectNode normalize(ObjectNode arguments) {
|
||||
public ObjectNode normalize(ToolMetadata metadata, ObjectNode arguments) {
|
||||
try {
|
||||
ObjectNode normalized = arguments == null
|
||||
? json.createObjectNode()
|
||||
: (ObjectNode) json.readTree(json.writeValueAsString(arguments));
|
||||
|
||||
if (metadata != null && metadata.allowedArguments().contains("pageSize")) {
|
||||
normalizePageSize(normalized);
|
||||
} else if (normalized.has("pageSize")) {
|
||||
normalizePageSize(normalized);
|
||||
}
|
||||
|
||||
validateCursor(normalized);
|
||||
return normalized;
|
||||
} catch (ToolExecutionException error) {
|
||||
|
||||
@@ -15,7 +15,6 @@ package io.shinhanlife.dap.mcg.tool.result;
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import io.shinhanlife.dap.mcg.guardrail.SensitiveDataMasker;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -34,11 +33,8 @@ public class ToolExecutionResultFormatter {
|
||||
private static final int TEXT_PREVIEW_LIMIT = 2_000;
|
||||
|
||||
private final ObjectMapper json;
|
||||
private final SensitiveDataMasker masker;
|
||||
|
||||
public ToolExecutionResultFormatter(ObjectMapper json, SensitiveDataMasker masker) {
|
||||
public ToolExecutionResultFormatter(ObjectMapper json) {
|
||||
this.json = json;
|
||||
this.masker = masker;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,7 +67,7 @@ public class ToolExecutionResultFormatter {
|
||||
}
|
||||
|
||||
public ToolExecutionResult fromJson(String toolName, JsonNode parsed, long sizeBytes) {
|
||||
JsonNode masked = masker.mask(parsed);
|
||||
JsonNode masked = parsed;
|
||||
if (masked.isObject()) {
|
||||
ObjectNode object = (ObjectNode) masked;
|
||||
if (object.path("isError").asBoolean(false) || object.has("error") || object.has("failureType")) {
|
||||
|
||||
Reference in New Issue
Block a user