target spring

# 0715 변경사항 대상 프로젝트: target-spring-oracle 생성 시각: 2026-07-15 09:34:26 범위: target/local-json/scenario-store 제외, 현재 소스 및 설정 파일 원문 전체 ------------------------------ 파일명: target-spring-oracle/pom.xml 상태: M 크기: 2929 bytes ------------------------------ 내용 ------------------------------ 4.0.0 com.samsung crypt-dynamic-verify-target 1.0.0 jar 17 3.3.6 3.0.4 org.springframework.boot spring-boot-dependencies ${spring.boot.version} pom import org.springframework.boot spring-boot-starter-web org.mybatis.spring.boot mybatis-spring-boot-starter ${mybatis.spring.boot.version} com.oracle.database.jdbc ojdbc11 23.5.0.24.07 p6spy p6spy 3.9.1 org.springframework.boot spring-boot-starter-validation org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin ${spring.boot.version} org.apache.maven.plugins maven-compiler-plugin 3.13.0 ${java.version} ${java.version} UTF-8 ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/business/customer/CustomerController.java 상태: clean 크기: 755 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.business.customer; import java.util.Map; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/customers") public class CustomerController { private final CustomerMapper customerMapper; public CustomerController(CustomerMapper customerMapper) { this.customerMapper = customerMapper; } @PostMapping public Map insert(@RequestBody CustomerDto customer) { return Map.of("inserted", customerMapper.insertCustomer(customer)); } @GetMapping("/by-name-hash") public CustomerDto selectByNameHash(@RequestParam("customerName") String customerName) { return customerMapper.selectCustomerByNameHash(customerName); } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/business/customer/CustomerDto.java 상태: clean 크기: 972 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.business.customer; public class CustomerDto { private String customerId; private String customerName; private String address; private String gradeCode; private String verificationRunId; public String getCustomerId() { return customerId; } public void setCustomerId(String customerId) { this.customerId = customerId; } public String getCustomerName() { return customerName; } public void setCustomerName(String customerName) { this.customerName = customerName; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getGradeCode() { return gradeCode; } public void setGradeCode(String gradeCode) { this.gradeCode = gradeCode; } public String getVerificationRunId() { return verificationRunId; } public void setVerificationRunId(String verificationRunId) { this.verificationRunId = verificationRunId; } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/business/customer/CustomerMapper.java 상태: clean 크기: 490 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.business.customer; import java.util.List; import java.util.Map; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @Mapper public interface CustomerMapper { int insertCustomer(CustomerDto customer); int updateCustomer(CustomerDto customer); CustomerDto selectCustomerByNameHash(@Param("customerName") String customerName); List selectCustomerGroupByName(Map condition); } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/business/order/OrderController.java 상태: clean 크기: 718 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.business.order; import java.util.List; import java.util.Map; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/orders") public class OrderController { private final OrderMapper orderMapper; public OrderController(OrderMapper orderMapper) { this.orderMapper = orderMapper; } @PostMapping public Map insert(@RequestBody OrderDto order) { return Map.of("inserted", orderMapper.insertOrder(order)); } @GetMapping("/by-buyer-join") public List selectJoin(@RequestParam("buyerName") String buyerName) { return orderMapper.selectOrdersByBuyerJoin(buyerName); } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/business/order/OrderDto.java 상태: clean 크기: 938 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.business.order; import java.math.BigDecimal; public class OrderDto { private String orderId; private String userId; private String buyerName; private BigDecimal amount; private String verificationRunId; public String getOrderId() { return orderId; } public void setOrderId(String orderId) { this.orderId = orderId; } public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getBuyerName() { return buyerName; } public void setBuyerName(String buyerName) { this.buyerName = buyerName; } public BigDecimal getAmount() { return amount; } public void setAmount(BigDecimal amount) { this.amount = amount; } public String getVerificationRunId() { return verificationRunId; } public void setVerificationRunId(String verificationRunId) { this.verificationRunId = verificationRunId; } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/business/order/OrderMapper.java 상태: clean 크기: 323 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.business.order; import java.util.List; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @Mapper public interface OrderMapper { int insertOrder(OrderDto order); List selectOrdersByBuyerJoin(@Param("buyerName") String buyerName); } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/business/user/UserController.java 상태: clean 크기: 1473 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.business.user; import java.util.List; import java.util.Map; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/users") public class UserController { private final UserMapper userMapper; public UserController(UserMapper userMapper) { this.userMapper = userMapper; } // 운영 API 샘플: Vue/TypeScript에서 평문을 보내면 MyBatis TypeHandler가 암호화/해시 저장을 수행한다. @PostMapping public Map insert(@RequestBody UserDto user) { userMapper.insertUser(user); return Map.of("inserted", true, "userId", user.getUserId()); } @PutMapping("/{userId}") public Map update(@PathVariable("userId") String userId, @RequestBody UserDto user) { user.setUserId(userId); return Map.of("updated", userMapper.updateUserDynamic(user)); } @GetMapping("/{userId}/plain-select") public UserDto selectPlainColumn(@PathVariable("userId") String userId) { return userMapper.selectUserPlainColumn(userId); } @GetMapping("/by-name-hash") public UserDto selectByNameHash(@RequestParam("userName") String userName) { return userMapper.selectUserByNameHash(userName); } @PostMapping("/batch") public Map batch(@RequestBody List users) { return Map.of("inserted", userMapper.batchInsertUsers(users)); } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/business/user/UserDto.java 상태: clean 크기: 866 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.business.user; public class UserDto { private String userId; private String userName; private String phone; private String email; private String verificationRunId; public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getVerificationRunId() { return verificationRunId; } public void setVerificationRunId(String verificationRunId) { this.verificationRunId = verificationRunId; } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/business/user/UserMapper.java 상태: clean 크기: 666 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.business.user; import java.util.List; import java.util.Map; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @Mapper public interface UserMapper { int insertUser(UserDto user); int batchInsertUsers(@Param("list") List users); int updateUserDynamic(UserDto user); int mergeUser(UserDto user); UserDto selectUserPlainColumn(@Param("userId") String userId); UserDto selectUserByNameHash(@Param("userName") String userName); List selectUsersDynamic(Map condition); int deleteUserByNameHash(@Param("userName") String userName); } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/CryptDynamicVerifyApplication.java 상태: clean 크기: 438 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan("com.samsung.cryptverify") public class CryptDynamicVerifyApplication { public static void main(String[] args) { SpringApplication.run(CryptDynamicVerifyApplication.class, args); } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/crypto/AddressEncryptTypeHandler.java 상태: clean 크기: 115 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.crypto; public class AddressEncryptTypeHandler extends NameEncryptTypeHandler { } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/crypto/AddressHashTypeHandler.java 상태: clean 크기: 109 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.crypto; public class AddressHashTypeHandler extends NameHashTypeHandler { } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/crypto/CryptoProperties.java 상태: clean 크기: 420 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.crypto; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @Configuration @ConfigurationProperties(prefix = "crypto") public class CryptoProperties { private String key; public String getKey() { return key; } public void setKey(String key) { this.key = key; } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/crypto/CryptoSupport.java 상태: clean 크기: 2262 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.crypto; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.Arrays; import java.util.Base64; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import org.springframework.stereotype.Component; @Component public class CryptoSupport { private final SecretKeySpec keySpec; public CryptoSupport(CryptoProperties properties) { byte[] digest = sha256Bytes(properties.getKey()); this.keySpec = new SecretKeySpec(Arrays.copyOf(digest, 16), "AES"); } // 검증 샘플을 단순하게 재현하기 위해 결정적 AES/ECB를 사용한다. 운영에서는 GCM/CBC+IV와 키 관리 체계를 적용해야 한다. public String encrypt(String plainText) { if (plainText == null) { return null; } try { Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, keySpec); return Base64.getEncoder().encodeToString(cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8))); } catch (Exception e) { throw new IllegalStateException("암호화 실패", e); } } public String decrypt(String cipherText) { if (cipherText == null) { return null; } try { Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, keySpec); return new String(cipher.doFinal(Base64.getDecoder().decode(cipherText)), StandardCharsets.UTF_8); } catch (Exception e) { throw new IllegalStateException("복호화 실패", e); } } public String hash(String plainText) { if (plainText == null) { return null; } return Base64.getEncoder().encodeToString(sha256Bytes(plainText + "|search|" + Base64.getEncoder().encodeToString(keySpec.getEncoded()))); } private static byte[] sha256Bytes(String value) { try { return MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)); } catch (Exception e) { throw new IllegalStateException("해시 생성 실패", e); } } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/crypto/NameEncryptTypeHandler.java 상태: clean 크기: 1528 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.crypto; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; public class NameEncryptTypeHandler extends BaseTypeHandler { // 이름 컬럼의 setParameter 암호화와 getResult 복호화를 수행하고 호출 횟수를 기록한다. @Override public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { SpringContext.getBean(TypeHandlerCallRegistry.class).markSetParameter(getClass().getSimpleName()); ps.setString(i, SpringContext.getBean(CryptoSupport.class).encrypt(parameter)); } @Override public String getNullableResult(ResultSet rs, String columnName) throws SQLException { return decrypt(rs.getString(columnName)); } @Override public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return decrypt(rs.getString(columnIndex)); } @Override public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return decrypt(cs.getString(columnIndex)); } private String decrypt(String value) { SpringContext.getBean(TypeHandlerCallRegistry.class).markGetResult(getClass().getSimpleName()); return value == null ? null : SpringContext.getBean(CryptoSupport.class).decrypt(value); } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/crypto/NameHashTypeHandler.java 상태: clean 크기: 1572 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.crypto; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; public class NameHashTypeHandler extends BaseTypeHandler { // 검색용 HASH 컬럼에 평문을 결정적 해시값으로 변환해서 바인딩한다. HASH는 복호화하지 않는다. @Override public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { SpringContext.getBean(TypeHandlerCallRegistry.class).markSetParameter(getClass().getSimpleName()); ps.setString(i, SpringContext.getBean(CryptoSupport.class).hash(parameter)); } @Override public String getNullableResult(ResultSet rs, String columnName) throws SQLException { SpringContext.getBean(TypeHandlerCallRegistry.class).markGetResult(getClass().getSimpleName()); return rs.getString(columnName); } @Override public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { SpringContext.getBean(TypeHandlerCallRegistry.class).markGetResult(getClass().getSimpleName()); return rs.getString(columnIndex); } @Override public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { SpringContext.getBean(TypeHandlerCallRegistry.class).markGetResult(getClass().getSimpleName()); return cs.getString(columnIndex); } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/crypto/PhoneEncryptTypeHandler.java 상태: clean 크기: 113 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.crypto; public class PhoneEncryptTypeHandler extends NameEncryptTypeHandler { } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/crypto/PhoneHashTypeHandler.java 상태: clean 크기: 107 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.crypto; public class PhoneHashTypeHandler extends NameHashTypeHandler { } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/crypto/SpringContext.java 상태: clean 크기: 562 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.crypto; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; @Component public class SpringContext implements ApplicationContextAware { private static ApplicationContext context; @Override public void setApplicationContext(ApplicationContext applicationContext) { context = applicationContext; } public static T getBean(Class type) { return context.getBean(type); } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/crypto/TypeHandlerCallRegistry.java 상태: clean 크기: 1243 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.crypto; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import org.springframework.stereotype.Component; @Component public class TypeHandlerCallRegistry { private final Map setParameterCounts = new ConcurrentHashMap<>(); private final Map getResultCounts = new ConcurrentHashMap<>(); // 검증 실행 전 TypeHandler 호출 횟수를 초기화한다. public void reset() { setParameterCounts.clear(); getResultCounts.clear(); } public void markSetParameter(String handlerName) { setParameterCounts.computeIfAbsent(handlerName, key -> new AtomicInteger()).incrementAndGet(); } public void markGetResult(String handlerName) { getResultCounts.computeIfAbsent(handlerName, key -> new AtomicInteger()).incrementAndGet(); } public int getSetParameterCount(String handlerName) { return setParameterCounts.getOrDefault(handlerName, new AtomicInteger()).get(); } public int getGetResultCount(String handlerName) { return getResultCounts.getOrDefault(handlerName, new AtomicInteger()).get(); } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/verification/controller/CommonExceptionHandler.java 상태: clean 크기: 843 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.verification.controller; import java.time.LocalDateTime; import java.util.LinkedHashMap; import java.util.Map; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @RestControllerAdvice public class CommonExceptionHandler { // 검증 API에서 발생한 예외를 공통 JSON으로 변환한다. @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Map handle(Exception e) { Map response = new LinkedHashMap(); response.put("timestamp", LocalDateTime.now().toString()); response.put("result", "FAIL"); response.put("message", e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()); return response; } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/verification/controller/CryptoVerificationController.java 상태: clean 크기: 3856 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.verification.controller; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.samsung.cryptverify.verification.dto.CryptoTargetDto; import com.samsung.cryptverify.verification.dto.ScenarioDefinition; import com.samsung.cryptverify.verification.dto.VerificationResultDto; import com.samsung.cryptverify.verification.dto.VerificationRunRequest; import com.samsung.cryptverify.verification.engine.CryptoTargetStore; import com.samsung.cryptverify.verification.engine.CryptoVerificationService; @RestController @RequestMapping("/crypto-verify") @CrossOrigin(originPatterns = {"http://localhost:1234", "http://127.0.0.1:1234"}) public class CryptoVerificationController { private static final Logger log = LoggerFactory.getLogger(CryptoVerificationController.class); private final CryptoVerificationService verificationService; private final CryptoTargetStore targetStore; public CryptoVerificationController( CryptoVerificationService verificationService, CryptoTargetStore targetStore ) { this.verificationService = verificationService; this.targetStore = targetStore; } @PostMapping("/run") public VerificationResultDto run(@RequestBody VerificationRunRequest request) { Map safeOverrides = request == null || request.getOverrides() == null ? new LinkedHashMap() : request.getOverrides(); log.info("[CryptoVerify][REQUEST] run all verificationRunId={}", safeOverrides.get("verificationRunId")); VerificationResultDto result = verificationService.runAll(request == null ? null : request.getScenarios(), safeOverrides); log.info("[CryptoVerify][RESPONSE] run all status={} progress={} elapsedMs={}", result.getStatus(), result.getProgress(), result.getElapsedMs()); return result; } @PostMapping("/run/{scenarioId}") public VerificationResultDto runSingle( @PathVariable Map pathVariables, @RequestBody VerificationRunRequest request ) { String scenarioId = pathVariables.get("scenarioId"); Map safeOverrides = request == null || request.getOverrides() == null ? new LinkedHashMap() : request.getOverrides(); ScenarioDefinition scenario = request == null ? null : request.getScenario(); if (scenario != null && scenarioId != null && !scenarioId.equals(scenario.getScenarioId())) { throw new IllegalArgumentException("Path scenarioId and request scenarioId do not match."); } log.info("[CryptoVerify][REQUEST] run single scenarioId={} verificationRunId={}", scenarioId, safeOverrides.get("verificationRunId")); VerificationResultDto result = verificationService.runSingle(scenario, safeOverrides); log.info("[CryptoVerify][RESPONSE] run single scenarioId={} status={} elapsedMs={}", scenarioId, result.getStatus(), result.getElapsedMs()); return result; } @GetMapping("/targets") public List targets() { return targetStore.findAll(); } @PostMapping("/targets") public List saveTargets(@RequestBody List targets) { return targetStore.replaceAll(targets); } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/verification/dto/CryptoTargetDto.java 상태: clean 크기: 1380 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.verification.dto; public class CryptoTargetDto { private String tableName; private String plainColumn; private String encColumn; private String hashColumn; private String javaProperty; private String encryptTypeHandler; private String hashTypeHandler; public String getTableName() { return tableName; } public void setTableName(String tableName) { this.tableName = tableName; } public String getPlainColumn() { return plainColumn; } public void setPlainColumn(String plainColumn) { this.plainColumn = plainColumn; } public String getEncColumn() { return encColumn; } public void setEncColumn(String encColumn) { this.encColumn = encColumn; } public String getHashColumn() { return hashColumn; } public void setHashColumn(String hashColumn) { this.hashColumn = hashColumn; } public String getJavaProperty() { return javaProperty; } public void setJavaProperty(String javaProperty) { this.javaProperty = javaProperty; } public String getEncryptTypeHandler() { return encryptTypeHandler; } public void setEncryptTypeHandler(String encryptTypeHandler) { this.encryptTypeHandler = encryptTypeHandler; } public String getHashTypeHandler() { return hashTypeHandler; } public void setHashTypeHandler(String hashTypeHandler) { this.hashTypeHandler = hashTypeHandler; } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/verification/dto/ScenarioDefinition.java 상태: clean 크기: 3615 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.verification.dto; import java.util.ArrayList; import java.util.List; import java.util.Map; public class ScenarioDefinition { private String scenarioId; private String displayName; private String mapperFile; private String sqlId; private String sqlCommandType; private String scenarioType; private String targetTable; private String targetColumn; private String encColumn; private String hashColumn; private String javaProperty; private String encryptTypeHandler; private String hashTypeHandler; private String executionMode; private Map defaultParameters; private List> operations = new ArrayList<>(); private Map verification; private List atoms = new ArrayList<>(); public String getScenarioId() { return scenarioId; } public void setScenarioId(String scenarioId) { this.scenarioId = scenarioId; } public String getDisplayName() { return displayName; } public void setDisplayName(String displayName) { this.displayName = displayName; } public String getMapperFile() { return mapperFile; } public void setMapperFile(String mapperFile) { this.mapperFile = mapperFile; } public String getSqlId() { return sqlId; } public void setSqlId(String sqlId) { this.sqlId = sqlId; } public String getSqlCommandType() { return sqlCommandType; } public void setSqlCommandType(String sqlCommandType) { this.sqlCommandType = sqlCommandType; } public String getScenarioType() { return scenarioType; } public void setScenarioType(String scenarioType) { this.scenarioType = scenarioType; } public String getTargetTable() { return targetTable; } public void setTargetTable(String targetTable) { this.targetTable = targetTable; } public String getTargetColumn() { return targetColumn; } public void setTargetColumn(String targetColumn) { this.targetColumn = targetColumn; } public String getEncColumn() { return encColumn; } public void setEncColumn(String encColumn) { this.encColumn = encColumn; } public String getHashColumn() { return hashColumn; } public void setHashColumn(String hashColumn) { this.hashColumn = hashColumn; } public String getJavaProperty() { return javaProperty; } public void setJavaProperty(String javaProperty) { this.javaProperty = javaProperty; } public String getEncryptTypeHandler() { return encryptTypeHandler; } public void setEncryptTypeHandler(String encryptTypeHandler) { this.encryptTypeHandler = encryptTypeHandler; } public String getHashTypeHandler() { return hashTypeHandler; } public void setHashTypeHandler(String hashTypeHandler) { this.hashTypeHandler = hashTypeHandler; } public String getExecutionMode() { return executionMode; } public void setExecutionMode(String executionMode) { this.executionMode = executionMode; } public Map getDefaultParameters() { return defaultParameters; } public void setDefaultParameters(Map defaultParameters) { this.defaultParameters = defaultParameters; } public List> getOperations() { return operations; } public void setOperations(List> operations) { this.operations = operations; } public Map getVerification() { return verification; } public void setVerification(Map verification) { this.verification = verification; } public List getAtoms() { return atoms; } public void setAtoms(List atoms) { this.atoms = atoms; } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/verification/dto/VerificationResultDto.java 상태: clean 크기: 3719 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.verification.dto; import java.util.ArrayList; import java.util.List; import java.util.Map; public class VerificationResultDto { private String id; private String label; private String type; private String status; private int progress; private String mapperName; private String sqlId; private String sql; private String targetColumn; private String typeHandlerName; private String inputPlainText; private String dbCipherText; private String decryptedText; private String exceptionMessage; private long elapsedMs; private int setParameterCount; private int getResultCount; private Map details; private List children = new ArrayList<>(); public static VerificationResultDto node(String id, String label, String type) { VerificationResultDto node = new VerificationResultDto(); node.id = id; node.label = label; node.type = type; node.status = "READY"; return node; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getLabel() { return label; } public void setLabel(String label) { this.label = label; } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public int getProgress() { return progress; } public void setProgress(int progress) { this.progress = progress; } public String getMapperName() { return mapperName; } public void setMapperName(String mapperName) { this.mapperName = mapperName; } public String getSqlId() { return sqlId; } public void setSqlId(String sqlId) { this.sqlId = sqlId; } public String getSql() { return sql; } public void setSql(String sql) { this.sql = sql; } public String getTargetColumn() { return targetColumn; } public void setTargetColumn(String targetColumn) { this.targetColumn = targetColumn; } public String getTypeHandlerName() { return typeHandlerName; } public void setTypeHandlerName(String typeHandlerName) { this.typeHandlerName = typeHandlerName; } public String getInputPlainText() { return inputPlainText; } public void setInputPlainText(String inputPlainText) { this.inputPlainText = inputPlainText; } public String getDbCipherText() { return dbCipherText; } public void setDbCipherText(String dbCipherText) { this.dbCipherText = dbCipherText; } public String getDecryptedText() { return decryptedText; } public void setDecryptedText(String decryptedText) { this.decryptedText = decryptedText; } public String getExceptionMessage() { return exceptionMessage; } public void setExceptionMessage(String exceptionMessage) { this.exceptionMessage = exceptionMessage; } public long getElapsedMs() { return elapsedMs; } public void setElapsedMs(long elapsedMs) { this.elapsedMs = elapsedMs; } public int getSetParameterCount() { return setParameterCount; } public void setSetParameterCount(int setParameterCount) { this.setParameterCount = setParameterCount; } public int getGetResultCount() { return getResultCount; } public void setGetResultCount(int getResultCount) { this.getResultCount = getResultCount; } public Map getDetails() { return details; } public void setDetails(Map details) { this.details = details; } public List getChildren() { return children; } public void setChildren(List children) { this.children = children; } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/verification/dto/VerificationRunRequest.java 상태: clean 크기: 808 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.verification.dto; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; public class VerificationRunRequest { private List scenarios; private ScenarioDefinition scenario; private Map overrides = new LinkedHashMap<>(); public List getScenarios() { return scenarios; } public void setScenarios(List scenarios) { this.scenarios = scenarios; } public ScenarioDefinition getScenario() { return scenario; } public void setScenario(ScenarioDefinition scenario) { this.scenario = scenario; } public Map getOverrides() { return overrides; } public void setOverrides(Map overrides) { this.overrides = overrides; } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/verification/engine/CryptoTargetStore.java 상태: clean 크기: 3822 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.verification.engine; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.springframework.stereotype.Component; import org.springframework.beans.factory.annotation.Value; import com.fasterxml.jackson.databind.ObjectMapper; import com.samsung.cryptverify.verification.dto.CryptoTargetDto; @Component public class CryptoTargetStore { private final ObjectMapper objectMapper; private final Path targetPath; public CryptoTargetStore( ObjectMapper objectMapper, @Value("${crypto-verification.target-store:scenario-store/crypto-targets.json}") String targetStorePath ) { this.objectMapper = objectMapper; this.targetPath = Paths.get(targetStorePath); } // 암호화 대상 테이블/컬럼 메타데이터를 JSON으로 저장/로드한다. public List findAll() { try { ensureStoreExists(); return Arrays.asList(objectMapper.readValue(readString(targetPath), CryptoTargetDto[].class)); } catch (IOException e) { throw new IllegalStateException("암호화 대상 JSON 로드 실패", e); } } public List replaceAll(List nextTargets) { try { Files.createDirectories(targetPath.getParent()); List normalizedTargets = nextTargets == null ? Collections.emptyList() : nextTargets; writeString(targetPath, objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(normalizedTargets)); return normalizedTargets; } catch (IOException e) { throw new IllegalStateException("암호화 대상 JSON 저장 실패", e); } } private void ensureStoreExists() throws IOException { if (Files.exists(targetPath)) { return; } Files.createDirectories(targetPath.getParent()); writeString(targetPath, objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(defaultTargets())); } private List defaultTargets() { List targets = new ArrayList<>(); targets.add(target("TB_USER", "USER_NAME", "userName", "NameEncryptTypeHandler", "NameHashTypeHandler")); targets.add(target("TB_USER", "PHONE", "phone", "PhoneEncryptTypeHandler", "PhoneHashTypeHandler")); targets.add(target("TB_CUSTOMER", "CUSTOMER_NAME", "customerName", "NameEncryptTypeHandler", "NameHashTypeHandler")); targets.add(target("TB_CUSTOMER", "ADDRESS", "address", "AddressEncryptTypeHandler", "AddressHashTypeHandler")); targets.add(target("TB_ORDER", "BUYER_NAME", "buyerName", "NameEncryptTypeHandler", "NameHashTypeHandler")); return targets; } private CryptoTargetDto target(String table, String plainColumn, String property, String encryptHandler, String hashHandler) { CryptoTargetDto dto = new CryptoTargetDto(); dto.setTableName(table); dto.setPlainColumn(plainColumn); dto.setEncColumn(plainColumn + "_ENC"); dto.setHashColumn(plainColumn + "_HASH"); dto.setJavaProperty(property); dto.setEncryptTypeHandler(encryptHandler); dto.setHashTypeHandler(hashHandler); return dto; } private String readString(Path path) throws IOException { return new String(Files.readAllBytes(path), StandardCharsets.UTF_8); } private void writeString(Path path, String value) throws IOException { Files.write(path, value.getBytes(StandardCharsets.UTF_8)); } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/verification/engine/CryptoVerificationService.java 상태: M 크기: 71652 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.verification.engine; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Random; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.session.SqlSession; import org.mybatis.spring.SqlSessionTemplate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.fasterxml.jackson.databind.ObjectMapper; import com.samsung.cryptverify.crypto.TypeHandlerCallRegistry; import com.samsung.cryptverify.verification.dto.ScenarioDefinition; import com.samsung.cryptverify.verification.dto.VerificationResultDto; @Service public class CryptoVerificationService { private static final Logger log = LoggerFactory.getLogger(CryptoVerificationService.class); private static final Pattern SQL_TAG_PATTERN = Pattern.compile( "<(select|insert|update|delete)\\b[^>]*\\bid\\s*=\\s*['\"]([^'\"]+)['\"][\\s\\S]*?", Pattern.CASE_INSENSITIVE ); private final ApplicationContext applicationContext; private final SqlSessionTemplate sqlSessionTemplate; private final NamedParameterJdbcTemplate jdbcTemplate; private final ObjectMapper objectMapper; private final TypeHandlerCallRegistry callRegistry; private final VerificationIndexStore indexStore; private final Path mapperRoot; private final Random random = new Random(); public CryptoVerificationService( ApplicationContext applicationContext, SqlSessionTemplate sqlSessionTemplate, NamedParameterJdbcTemplate jdbcTemplate, ObjectMapper objectMapper, TypeHandlerCallRegistry callRegistry, VerificationIndexStore indexStore, @Value("${crypto-verification.mapper-root:src/main/resources/mapper/business}") String mapperRoot ) { this.applicationContext = applicationContext; this.sqlSessionTemplate = sqlSessionTemplate; this.jdbcTemplate = jdbcTemplate; this.objectMapper = objectMapper; this.callRegistry = callRegistry; this.indexStore = indexStore; this.mapperRoot = Paths.get(mapperRoot).normalize(); } @Transactional public VerificationResultDto runSingle(ScenarioDefinition scenario, Map overrides) { if (scenario == null) { throw new IllegalArgumentException("scenario is required."); } String runId = value(overrides, "verificationRunId", "VERIFY_" + System.currentTimeMillis() + "_" + UUID.randomUUID()); return runScenario(scenario, runId, overrides); } @Transactional public VerificationResultDto runAll(List scenarios, Map overrides) { if (scenarios == null || scenarios.isEmpty()) { throw new IllegalArgumentException("scenarios is required."); } long started = System.currentTimeMillis(); String runId = value(overrides, "verificationRunId", "VERIFY_" + System.currentTimeMillis() + "_" + UUID.randomUUID()); log.info("[CryptoVerify][RUN] start all scenarios runId={}", runId); VerificationResultDto root = VerificationResultDto.node("root", "Verification Result", "ROOT"); List scenarioNodes = new ArrayList(); for (ScenarioDefinition scenario : scenarios) { scenarioNodes.add(runScenario(scenario, runId, overrides)); } root.setChildren(groupByMapperAndSql(scenarioNodes)); fillAggregateStatus(root); root.setElapsedMs(System.currentTimeMillis() - started); return root; } public VerificationResultDto runScenario(ScenarioDefinition scenario, String runId, Map overrides) { long started = System.currentTimeMillis(); callRegistry.reset(); long testIndex = indexStore.nextIndex(scenario.getScenarioId()); String testRunId = runId + "_" + String.format("%06d", testIndex); VerificationResultDto result = VerificationResultDto.node(scenario.getScenarioId(), scenario.getDisplayName(), "SCENARIO"); result.setMapperName(emptyToDefault(scenario.getMapperFile(), executionMode(scenario))); result.setSqlId(emptyToDefault(scenario.getSqlId(), scenario.getScenarioType())); result.setTargetColumn(scenario.getTargetColumn()); result.setTypeHandlerName(scenario.getEncryptTypeHandler()); Map context = buildContext(scenario, runId, testRunId, testIndex, overrides); boolean cleanupEnabled = cleanupInsertedRowsEnabled(scenario, overrides); context.put("cleanupInsertedRows", cleanupEnabled); Map operationResults = new LinkedHashMap(); List insertedRows = new ArrayList(); try { if ("TYPEHANDLER_MISSING_DETECTION".equals(upper(scenario.getScenarioType()))) { verifyTypeHandlerMissingDetection(scenario, result); return result; } prepareExecutableScenario(scenario); if (scenario.getOperations() == null || scenario.getOperations().isEmpty()) { throw new IllegalArgumentException("operations is empty. Add mapper/sqlSession/dynamicSql operation info to scenario JSON."); } preparePrimaryKeysForInsertOperations(scenario, context, testIndex); for (Map operation : scenario.getOperations()) { Object operationResult = executeOperation(scenario, operation, context, operationResults); collectInsertedRow(scenario, operation, context, operationResult, insertedRows); } verifyByJson(scenario, result, context, operationResults); if ("MAX_LENGTH_INSERT_UPDATE".equals(upper(scenario.getScenarioType()))) { appendMaxLengthDetail(scenario, result, context); } } catch (Exception e) { result.setStatus("FAIL"); result.setProgress(0); result.setExceptionMessage(rootMessage(e)); result.setDetails(details(context, operationResults)); log.warn("[CryptoVerify][SCENARIO] failed scenarioId={} mode={} message={}", scenario.getScenarioId(), executionMode(scenario), e.getMessage()); } finally { if (cleanupEnabled) { cleanupInsertedRows(insertedRows, operationResults, context, result); } else { markCleanupSkipped(insertedRows, operationResults, context, result); } result.setElapsedMs(System.currentTimeMillis() - started); result.setSetParameterCount(callRegistry.getSetParameterCount(scenario.getEncryptTypeHandler()) + callRegistry.getSetParameterCount(scenario.getHashTypeHandler())); result.setGetResultCount(callRegistry.getGetResultCount(scenario.getEncryptTypeHandler())); } return result; } private void prepareExecutableScenario(ScenarioDefinition scenario) { String scenarioType = upper(scenario.getScenarioType()); if ("MAX_LENGTH_INSERT_UPDATE".equals(scenarioType)) { if (scenario.getOperations() == null || scenario.getOperations().isEmpty()) { scenario.setOperations(defaultMaxLengthOperations(scenario)); } if (scenario.getVerification() == null || scenario.getVerification().isEmpty()) { boolean hasUpdate = !isBlank(defaultUpdateSqlId(scenario)); scenario.setVerification(defaultVerification( scenario, hasUpdate ? "${updatedPlain}" : "${" + scenario.getJavaProperty() + "}", hasUpdate ? "rawAfterUpdate" : "raw" )); } } else if (scenario.getVerification() == null && scenario.getOperations() != null && !scenario.getOperations().isEmpty()) { scenario.setVerification(defaultVerification(scenario, "${" + scenario.getJavaProperty() + "}", "raw")); } } private List> defaultMaxLengthOperations(ScenarioDefinition scenario) { List> operations = new ArrayList>(); String mapperNamespace = mapperNamespace(scenario.getMapperFile()); String idProperty = idProperty(scenario); String idColumn = idColumn(scenario); String insertSqlId = isBlank(scenario.getSqlId()) || !"INSERT".equals(upper(scenario.getSqlCommandType())) ? defaultInsertSqlId(scenario) : scenario.getSqlId(); operations.add(operation( "insert", "SQL_SESSION", "INSERT", mapperNamespace + "." + insertSqlId, parameterType(scenario), dtoParameter(scenario, idProperty, "${" + scenario.getJavaProperty() + "}"), "insertCount" )); operations.add(rawSelectOperation("raw", scenario, idColumn, idProperty)); String updateSqlId = defaultUpdateSqlId(scenario); if (!isBlank(updateSqlId)) { operations.add(operation( "update", "SQL_SESSION", "UPDATE", mapperNamespace + "." + updateSqlId, parameterType(scenario), dtoParameter(scenario, idProperty, "${updatedPlain}"), "updateCount" )); operations.add(rawSelectOperation("rawAfterUpdate", scenario, idColumn, idProperty)); } operations.add(operation( "decrypted", "SQL_SESSION", "SELECT_FIRST", mapperNamespace + "." + defaultSelectByHashSqlId(scenario), "", Map.of(scenario.getJavaProperty(), isBlank(updateSqlId) ? "${" + scenario.getJavaProperty() + "}" : "${updatedPlain}"), "decrypted" )); return operations; } private Map rawSelectOperation(String name, ScenarioDefinition scenario, String idColumn, String idProperty) { Map operation = new LinkedHashMap(); operation.put("name", name); operation.put("mode", "DYNAMIC_SQL"); operation.put("command", "SELECT_ONE"); operation.put("table", scenario.getTargetTable()); operation.put("columns", Arrays.asList(idColumn, scenario.getTargetColumn(), effectiveEncColumn(scenario), effectiveHashColumn(scenario), "VERIFICATION_RUN_ID")); operation.put("where", Map.of(idColumn, idProperty)); operation.put("parameter", Map.of(idProperty, "${" + idProperty + "}")); operation.put("resultKey", name); return operation; } private Map operation(String name, String mode, String command, String statement, String parameterType, Object parameter, String resultKey) { Map operation = new LinkedHashMap(); operation.put("name", name); operation.put("mode", mode); operation.put("command", command); operation.put("statement", statement); if (!isBlank(parameterType)) { operation.put("parameterType", parameterType); } operation.put("parameter", parameter); operation.put("resultKey", resultKey); return operation; } private Map dtoParameter(ScenarioDefinition scenario, String idProperty, String plainValue) { Map parameter = new LinkedHashMap(); parameter.put(idProperty, "${" + idProperty + "}"); parameter.put(scenario.getJavaProperty(), plainValue); parameter.put("verificationRunId", "${verificationRunId}"); String table = upper(scenario.getTargetTable()); if ("TB_USER".equals(table)) { parameter.put("phone", "${phone}"); parameter.put("email", "${email}"); } else if ("TB_CUSTOMER".equals(table)) { parameter.put("address", "${address}"); parameter.put("gradeCode", "${gradeCode}"); } else if ("TB_ORDER".equals(table)) { parameter.put("userId", "${userId}"); parameter.put("amount", "${amount}"); } return parameter; } private Map defaultVerification(ScenarioDefinition scenario, String plainExpression, String rawResultKey) { Map verification = new LinkedHashMap(); verification.put("plain", plainExpression); verification.put("rawResult", rawResultKey); verification.put("decryptedResult", "decrypted"); verification.put("cipherColumn", effectiveEncColumn(scenario)); verification.put("hashColumn", effectiveHashColumn(scenario)); verification.put("decryptedProperty", scenario.getJavaProperty()); verification.put("checks", Arrays.asList( "CIPHER_NOT_PLAIN", "HASH_NOT_PLAIN", "DECRYPTED_EQUALS_PLAIN", "ENCRYPT_TYPEHANDLER_SET_CALLED", "HASH_TYPEHANDLER_SET_CALLED", "ENCRYPT_TYPEHANDLER_GET_CALLED" )); return verification; } private void verifyTypeHandlerMissingDetection(ScenarioDefinition scenario, VerificationResultDto result) throws Exception { String sqlText = mapperSqlText(scenario.getMapperFile(), scenario.getSqlId()); String encColumn = effectiveEncColumn(scenario); String hashColumn = effectiveHashColumn(scenario); boolean hasEnc = containsIgnoreCase(sqlText, encColumn); boolean hasHash = containsIgnoreCase(sqlText, hashColumn); boolean missingHash = hasEnc && !hasHash; result.setProgress(missingHash ? 0 : 100); result.setStatus(missingHash ? "FAIL" : "PASS"); result.setInputPlainText(scenario.getTargetColumn()); result.setDbCipherText(encColumn); result.setDecryptedText(hashColumn); result.setExceptionMessage(missingHash ? "Hash column update is missing: " + hashColumn : null); Map details = new LinkedHashMap(); details.put("mapperFile", scenario.getMapperFile()); details.put("sqlId", scenario.getSqlId()); details.put("encryptColumnFound", hasEnc); details.put("hashColumnFound", hasHash); details.put("missingHashColumn", missingHash); details.put("hashColumnHint", hashColumn + " = #{" + scenario.getJavaProperty() + ", typeHandler=com.samsung.cryptverify.crypto." + scenario.getHashTypeHandler() + "}"); details.put("sqlText", sqlText); result.setDetails(details); } private Object executeOperation( ScenarioDefinition scenario, Map operation, Map context, Map operationResults ) throws Exception { String mode = upper(string(operation.getOrDefault("mode", executionMode(scenario)))); captureExecutedQuery(scenario, operation, context, operationResults, mode); Object result; if ("REFLECTION".equals(mode)) { result = executeReflection(operation, context); } else if ("SQL_SESSION".equals(mode) || "SQLSESSION".equals(mode) || "MYBATIS".equals(mode)) { result = executeSqlSession(operation, context); } else if ("DYNAMIC_SQL".equals(mode) || "DYNAMIC".equals(mode) || "JDBC".equals(mode)) { result = executeDynamicSql(operation, context); } else { throw new IllegalArgumentException("Unsupported executionMode: " + mode); } String resultKey = string(operation.get("resultKey")); if (!isBlank(resultKey)) { operationResults.put(resultKey, result); context.put(resultKey, result); } return result; } private Object executeReflection(Map operation, Map context) throws Exception { String mapperClassName = required(operation, "mapperClass"); String methodName = required(operation, "method"); Object mapper = applicationContext.getBean(Class.forName(mapperClassName)); Object[] args = reflectionArgs(operation, context); Method method = findMethod(mapper, mapperClassName, methodName, args); return method.invoke(mapper, args); } private Object[] reflectionArgs(Map operation, Map context) throws Exception { if (operation.containsKey("args")) { List rawArgs = asList(operation.get("args")); Object[] args = new Object[rawArgs.size()]; List paramTypes = asList(operation.get("argTypes")); for (int i = 0; i < rawArgs.size(); i += 1) { Object resolved = resolve(rawArgs.get(i), context); if (paramTypes != null && i < paramTypes.size()) { args[i] = convertToType(resolved, Class.forName(string(paramTypes.get(i)))); } else { args[i] = resolved; } } return args; } Object parameter = resolve(operation.get("parameter"), context); String parameterType = string(operation.get("parameterType")); if (isBlank(parameterType)) { return new Object[] { parameter }; } return new Object[] { convertToType(parameter, Class.forName(parameterType)) }; } private Method findMethod(Object bean, String mapperClassName, String methodName, Object[] args) throws Exception { Class mapperClass = Class.forName(mapperClassName); for (Method method : mapperClass.getMethods()) { if (method.getName().equals(methodName) && method.getParameterCount() == args.length) { return method; } } Class targetClass = AopUtils.getTargetClass(bean); for (Method method : targetClass.getMethods()) { if (method.getName().equals(methodName) && method.getParameterCount() == args.length) { return method; } } throw new NoSuchMethodException(mapperClassName + "." + methodName + " args=" + args.length); } private Object executeSqlSession(Map operation, Map context) { String statement = required(operation, "statement"); String command = upper(string(operation.getOrDefault("command", "SELECT_ONE"))); Object parameter = resolve(operation.get("parameter"), context); String parameterType = string(operation.get("parameterType")); if (!isBlank(parameterType)) { try { parameter = convertToType(parameter, Class.forName(parameterType)); } catch (ClassNotFoundException e) { throw new IllegalArgumentException("parameterType class not found: " + parameterType, e); } } SqlSession session = sqlSessionTemplate; if ("INSERT".equals(command)) { return session.insert(statement, parameter); } if ("UPDATE".equals(command)) { return session.update(statement, parameter); } if ("DELETE".equals(command)) { return session.delete(statement, parameter); } if ("SELECT".equals(command) || "SELECT_ONE".equals(command)) { return session.selectOne(statement, parameter); } if ("SELECT_FIRST".equals(command) || "FIRST".equals(command)) { return selectFirst(session, statement, parameter); } if ("SELECT_LIST".equals(command) || "LIST".equals(command)) { return session.selectList(statement, parameter); } throw new IllegalArgumentException("Unsupported sqlSession command: " + command); } private Object selectFirst(SqlSession session, String statement, Object parameter) { List rows = session.selectList(statement, parameter); return rows.isEmpty() ? null : rows.get(0); } @SuppressWarnings("unchecked") private void captureExecutedQuery( ScenarioDefinition scenario, Map operation, Map context, Map operationResults, String mode ) { String key = operationKey(operation); Map queries = operationResults.get("executedQueries") instanceof Map ? (Map) operationResults.get("executedQueries") : new LinkedHashMap(); Map capture = new LinkedHashMap(); String queryId = "Q" + UUID.randomUUID().toString().replace("-", "").substring(0, 12).toUpperCase(Locale.ROOT); capture.put("queryId", queryId); capture.put("mode", mode); capture.put("command", upper(string(operation.getOrDefault("command", "SELECT_ONE")))); capture.put("name", string(operation.get("name"))); capture.put("resultKey", string(operation.get("resultKey"))); try { if ("SQL_SESSION".equals(mode) || "SQLSESSION".equals(mode) || "MYBATIS".equals(mode)) { capture.putAll(captureMyBatisQuery(operation, context)); } else if ("DYNAMIC_SQL".equals(mode) || "DYNAMIC".equals(mode) || "JDBC".equals(mode)) { capture.putAll(captureDynamicQuery(operation, context)); } else if ("REFLECTION".equals(mode)) { capture.put("statement", string(operation.get("mapperClass")) + "." + string(operation.get("method"))); capture.put("parameter", resolve(firstNonNull(operation.get("args"), operation.get("parameter")), context)); capture.put("fullSql", "REFLECTION " + capture.get("statement")); } else { capture.put("fullSql", "Unsupported executionMode: " + mode); } } catch (Exception e) { capture.put("captureError", rootMessage(e)); capture.put("fullSql", operationQueryFallback(scenario, operation, context, mode)); } capture.put("fullSql", withQueryIdComment(queryId, string(capture.get("fullSql")))); queries.put(key, capture); operationResults.put("executedQueries", queries); context.put("executedQueries", queries); } private Map captureMyBatisQuery(Map operation, Map context) throws Exception { String statement = required(operation, "statement"); Object parameter = sqlSessionParameter(operation, context); MappedStatement mappedStatement = sqlSessionTemplate.getConfiguration().getMappedStatement(statement); BoundSql boundSql = mappedStatement.getBoundSql(parameter); Map capture = new LinkedHashMap(); capture.put("statement", statement); capture.put("sql", compactSql(boundSql.getSql())); capture.put("parameter", parameter); capture.put("fullSql", inlineQuestionParameters(boundSql, parameter)); return capture; } private Map captureDynamicQuery(Map operation, Map context) { String command = upper(string(operation.getOrDefault("command", "SELECT_ONE"))); Map parameter = parameterMap(operation, context); String sql = string(resolve(operation.get("sql"), context)); if (isBlank(sql)) { sql = buildDynamicSql(operation, command); } Map capture = new LinkedHashMap(); capture.put("sql", compactSql(sql)); capture.put("parameter", parameter); capture.put("fullSql", inlineNamedParameters(sql, parameter)); return capture; } private Object sqlSessionParameter(Map operation, Map context) throws Exception { Object parameter = resolve(operation.get("parameter"), context); String parameterType = string(operation.get("parameterType")); if (isBlank(parameterType)) { return parameter; } return convertToType(parameter, Class.forName(parameterType)); } private Object executeDynamicSql(Map operation, Map context) { String command = upper(string(operation.getOrDefault("command", "SELECT_ONE"))); Map parameter = parameterMap(operation, context); String sql = string(resolve(operation.get("sql"), context)); if (isBlank(sql)) { sql = buildDynamicSql(operation, command); } if ("INSERT".equals(command) || "UPDATE".equals(command) || "DELETE".equals(command)) { return jdbcTemplate.update(sql, parameter); } if ("SELECT".equals(command) || "SELECT_ONE".equals(command) || "SELECT_FIRST".equals(command) || "FIRST".equals(command)) { return jdbcSelectFirst(sql, parameter); } if ("SELECT_LIST".equals(command) || "LIST".equals(command)) { return jdbcTemplate.queryForList(sql, parameter); } throw new IllegalArgumentException("Unsupported DYNAMIC_SQL command: " + command); } @SuppressWarnings("unchecked") private Map parameterMap(Map operation, Map context) { Object resolved = resolve(operation.get("parameter"), context); if (resolved == null) { return new LinkedHashMap(); } if (resolved instanceof Map) { Map map = (Map) resolved; Map parameters = new LinkedHashMap(); for (Map.Entry entry : map.entrySet()) { parameters.put(string(entry.getKey()), entry.getValue()); } return parameters; } return objectMapper.convertValue(resolved, Map.class); } private Object jdbcSelectFirst(String sql, Map parameter) { List> rows = jdbcTemplate.queryForList(sql, parameter); return rows.isEmpty() ? null : rows.get(0); } private String buildDynamicSql(Map operation, String command) { String table = required(operation, "table"); assertSafeIdentifier(table, "table"); if ("INSERT".equals(command)) { return buildInsertSql(table, operation); } if ("UPDATE".equals(command)) { return buildUpdateSql(table, operation); } if ("DELETE".equals(command)) { return buildDeleteSql(table, operation); } if ("SELECT".equals(command) || "SELECT_ONE".equals(command) || "SELECT_FIRST".equals(command) || "FIRST".equals(command) || "SELECT_LIST".equals(command) || "LIST".equals(command)) { return buildSelectSql(table, operation); } throw new IllegalArgumentException("Cannot build DYNAMIC_SQL command: " + command); } private String buildSelectSql(String table, Map operation) { List columns = stringList(operation.get("columns")); String selectColumns = columns.isEmpty() ? "*" : String.join(", ", safeIdentifiers(columns, "columns")); return "SELECT " + selectColumns + " FROM " + table + whereClause(operation); } private String buildInsertSql(String table, Map operation) { List columns = stringList(operation.get("columns")); if (columns.isEmpty()) { throw new IllegalArgumentException("DYNAMIC_SQL INSERT requires columns."); } List safeColumns = safeIdentifiers(columns, "columns"); List binds = new ArrayList(); for (String column : columns) { binds.add(":" + bindName(column)); } return "INSERT INTO " + table + " (" + String.join(", ", safeColumns) + ") VALUES (" + String.join(", ", binds) + ")"; } private String buildUpdateSql(String table, Map operation) { List columns = stringList(operation.get("columns")); if (columns.isEmpty()) { throw new IllegalArgumentException("DYNAMIC_SQL UPDATE requires columns."); } List assignments = new ArrayList(); for (String column : safeIdentifiers(columns, "columns")) { assignments.add(column + " = :" + bindName(column)); } return "UPDATE " + table + " SET " + String.join(", ", assignments) + whereClause(operation); } private String buildDeleteSql(String table, Map operation) { return "DELETE FROM " + table + whereClause(operation); } private String whereClause(Map operation) { Object whereObject = operation.get("where"); if (!(whereObject instanceof Map)) { return ""; } Map where = (Map) whereObject; if (where.isEmpty()) { return ""; } List predicates = new ArrayList(); for (Map.Entry entry : where.entrySet()) { String column = string(entry.getKey()); String parameterName = string(entry.getValue()); assertSafeIdentifier(column, "where column"); assertSafeParameterName(parameterName); predicates.add(column + " = :" + parameterName); } return " WHERE " + String.join(" AND ", predicates); } private void preparePrimaryKeysForInsertOperations(ScenarioDefinition scenario, Map context, long testIndex) { for (Map operation : scenario.getOperations()) { if (!isInsertOperation(scenario, operation)) { continue; } String table = insertTable(scenario, operation); if (isBlank(table)) { continue; } List columns = primaryKeyColumns(table); for (PrimaryKeyColumn column : columns) { String property = propertyName(column.columnName); context.put(property, randomPrimaryKeyValue(column, testIndex)); } if (!columns.isEmpty()) { context.put("__primaryKeyColumns", columns); } } } private void collectInsertedRow( ScenarioDefinition scenario, Map operation, Map context, Object operationResult, List insertedRows ) { if (!isInsertOperation(scenario, operation) || !isPositiveMutationResult(operationResult)) { return; } String table = insertTable(scenario, operation); if (isBlank(table)) { return; } List columns = primaryKeyColumns(table); if (columns.isEmpty()) { return; } Map parameter = parameterMap(operation, context); Map pkValues = new LinkedHashMap(); for (PrimaryKeyColumn column : columns) { String property = propertyName(column.columnName); Object value = firstNonNull(parameter.get(property), context.get(property), parameter.get(column.columnName)); if (value == null) { return; } pkValues.put(column.columnName, value); } insertedRows.add(new InsertedRow(table, columns, pkValues)); } private void cleanupInsertedRows( List insertedRows, Map operationResults, Map context, VerificationResultDto result ) { if (insertedRows.isEmpty()) { return; } List> cleanupResults = new ArrayList>(); try { for (int i = insertedRows.size() - 1; i >= 0; i -= 1) { InsertedRow row = insertedRows.get(i); String sql = cleanupSql(row); int count = jdbcTemplate.update(sql, row.pkValues); Map cleanupResult = new LinkedHashMap(); cleanupResult.put("table", row.tableName); cleanupResult.put("pkValues", row.pkValues); cleanupResult.put("deleteCount", count); cleanupResults.add(cleanupResult); } } catch (Exception e) { result.setStatus("FAIL"); result.setProgress(0); String message = "Inserted row cleanup by PK failed: " + rootMessage(e); result.setExceptionMessage(isBlank(result.getExceptionMessage()) ? message : result.getExceptionMessage() + " / " + message); cleanupResults.add(Map.of("cleanupError", message)); } finally { operationResults.put("autoCleanupByPk", cleanupResults); context.put("autoCleanupByPk", cleanupResults); if (result.getDetails() != null) { result.getDetails().put("autoCleanupByPk", cleanupResults); } } } private void markCleanupSkipped( List insertedRows, Map operationResults, Map context, VerificationResultDto result ) { if (insertedRows.isEmpty()) { return; } List> cleanupResults = new ArrayList>(); for (InsertedRow row : insertedRows) { Map cleanupResult = new LinkedHashMap(); cleanupResult.put("table", row.tableName); cleanupResult.put("pkValues", row.pkValues); cleanupResult.put("skipped", true); cleanupResults.add(cleanupResult); } operationResults.put("autoCleanupByPk", cleanupResults); context.put("autoCleanupByPk", cleanupResults); if (result.getDetails() != null) { result.getDetails().put("autoCleanupByPk", cleanupResults); } } private boolean cleanupInsertedRowsEnabled(ScenarioDefinition scenario, Map overrides) { Object byScenario = overrides == null ? null : overrides.get("cleanupInsertedRowsByScenario"); if (byScenario instanceof Map) { Object value = ((Map) byScenario).get(scenario.getScenarioId()); if (value != null) { return booleanValue(value, true); } } return booleanValue(overrides == null ? null : overrides.get("cleanupInsertedRows"), true); } private boolean booleanValue(Object value, boolean fallback) { if (value == null) { return fallback; } if (value instanceof Boolean) { return (Boolean) value; } String text = value.toString().trim(); if (isBlank(text)) { return fallback; } return "true".equalsIgnoreCase(text) || "Y".equalsIgnoreCase(text) || "YES".equalsIgnoreCase(text) || "1".equals(text); } private String cleanupSql(InsertedRow row) { assertSafeIdentifier(row.tableName, "table"); List predicates = new ArrayList(); for (PrimaryKeyColumn column : row.columns) { assertSafeIdentifier(column.columnName, "where column"); predicates.add(column.columnName + " = :" + column.columnName); } return "DELETE FROM " + row.tableName + " WHERE " + String.join(" AND ", predicates); } private String inlineQuestionParameters(BoundSql boundSql, Object parameter) { String sql = compactSql(boundSql.getSql()); List mappings = boundSql.getParameterMappings(); if (mappings == null || mappings.isEmpty()) { return sql; } MetaObject metaObject = parameter == null ? null : sqlSessionTemplate.getConfiguration().newMetaObject(parameter); StringBuilder rendered = new StringBuilder(); int searchFrom = 0; for (ParameterMapping mapping : mappings) { int placeholderAt = sql.indexOf('?', searchFrom); if (placeholderAt < 0) { break; } rendered.append(sql, searchFrom, placeholderAt); rendered.append(sqlLiteral(parameterValue(boundSql, parameter, metaObject, mapping.getProperty()))); searchFrom = placeholderAt + 1; } rendered.append(sql.substring(searchFrom)); return rendered.toString(); } private Object parameterValue(BoundSql boundSql, Object parameter, MetaObject metaObject, String property) { if (boundSql.hasAdditionalParameter(property)) { return boundSql.getAdditionalParameter(property); } if (parameter == null) { return null; } if (parameter instanceof Map) { Map map = (Map) parameter; if (map.containsKey(property)) { return map.get(property); } } if (metaObject != null && metaObject.hasGetter(property)) { return metaObject.getValue(property); } return parameter; } private String inlineNamedParameters(String sql, Map parameter) { String rendered = compactSql(sql); for (Map.Entry entry : parameter.entrySet()) { String name = Pattern.quote(entry.getKey()); rendered = rendered.replaceAll(":" + name + "\\b", Matcher.quoteReplacement(sqlLiteral(entry.getValue()))); } return rendered; } private String sqlLiteral(Object value) { if (value == null) { return "NULL"; } if (value instanceof Number || value instanceof Boolean) { return value.toString(); } String text = value.toString().replace("'", "''"); return "'" + text + "'"; } private String compactSql(String sql) { return sql == null ? "" : sql.replaceAll("\\s+", " ").trim(); } private String withQueryIdComment(String queryId, String sql) { return "/* VERIFY_QUERY_ID: " + queryId + " */\n" + (sql == null ? "" : sql); } private String operationKey(Map operation) { String resultKey = string(operation.get("resultKey")); if (!isBlank(resultKey)) { return resultKey; } String name = string(operation.get("name")); return isBlank(name) ? "operation" : name; } private String operationQueryFallback(ScenarioDefinition scenario, Map operation, Map context, String mode) { if ("SQL_SESSION".equals(mode) || "SQLSESSION".equals(mode) || "MYBATIS".equals(mode)) { return "statement: " + string(firstNonNull(operation.get("statement"), mapperNamespace(scenario.getMapperFile()) + "." + scenario.getSqlId())); } if ("DYNAMIC_SQL".equals(mode) || "DYNAMIC".equals(mode) || "JDBC".equals(mode)) { String command = upper(string(operation.getOrDefault("command", "SELECT_ONE"))); String sql = string(resolve(operation.get("sql"), context)); return isBlank(sql) ? buildDynamicSql(operation, command) : sql; } return mode + " " + string(operation.get("name")); } private List primaryKeyColumns(String tableName) { String table = upper(tableName); assertSafeIdentifier(table, "table"); List> rows = jdbcTemplate.queryForList( "SELECT cols.column_name, tc.data_type, tc.data_length, tc.char_length, tc.char_used, cols.position " + "FROM all_constraints cons " + "JOIN all_cons_columns cols " + " ON cons.owner = cols.owner " + " AND cons.constraint_name = cols.constraint_name " + " AND cons.table_name = cols.table_name " + "JOIN all_tab_columns tc " + " ON tc.owner = cols.owner " + " AND tc.table_name = cols.table_name " + " AND tc.column_name = cols.column_name " + "WHERE cons.constraint_type = 'P' " + " AND cons.owner = SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') " + " AND cons.table_name = :tableName " + "ORDER BY cols.position", Map.of("tableName", table) ); List columns = new ArrayList(); for (Map row : rows) { columns.add(new PrimaryKeyColumn( string(mapValue(row, "COLUMN_NAME")), string(mapValue(row, "DATA_TYPE")), number(mapValue(row, "DATA_LENGTH")), number(mapValue(row, "CHAR_LENGTH")), string(mapValue(row, "CHAR_USED")), number(mapValue(row, "POSITION")) )); } return columns; } private Object randomPrimaryKeyValue(PrimaryKeyColumn column, long testIndex) { String dataType = upper(column.dataType); if (dataType.contains("NUMBER") || dataType.contains("INTEGER") || dataType.contains("DECIMAL") || dataType.contains("NUMERIC") || dataType.contains("FLOAT") || dataType.contains("DOUBLE")) { long base = Math.abs(System.currentTimeMillis() % 1_000_000_000L); return base + testIndex + random.nextInt(10_000); } if (dataType.contains("DATE") || dataType.contains("TIMESTAMP")) { return new java.sql.Timestamp(System.currentTimeMillis() + testIndex); } int maxLength = column.charLength != null && column.charLength > 0 ? column.charLength : column.dataLength != null && column.dataLength > 0 ? column.dataLength : 40; String randomPart = Long.toUnsignedString(random.nextLong(), 36).toUpperCase(Locale.ROOT) + Long.toString(System.nanoTime(), 36).toUpperCase(Locale.ROOT) + testIndex; String value = "CV" + randomPart; if (maxLength > 0 && value.length() > maxLength) { if (maxLength <= 2) { return randomPart.substring(0, Math.max(1, maxLength)); } return value.substring(0, maxLength); } return value; } private boolean isInsertOperation(ScenarioDefinition scenario, Map operation) { String command = upper(string(operation.get("command"))); if ("INSERT".equals(command)) { return true; } String name = upper(string(operation.get("name"))); String method = upper(string(operation.get("method"))); String statement = upper(string(operation.get("statement"))); if (name.startsWith("INSERT") || method.startsWith("INSERT")) { return true; } if (statement.contains(".INSERT")) { return true; } return "INSERT".equals(upper(scenario.getSqlCommandType())) && operation == scenario.getOperations().get(0); } private String insertTable(ScenarioDefinition scenario, Map operation) { String table = string(operation.get("table")); if (!isBlank(table)) { return upper(table); } String statement = string(operation.get("statement")); if (!isBlank(statement)) { table = tableFromMapperName(statement); if (!isBlank(table)) { return table; } } String mapperClass = string(operation.get("mapperClass")); if (!isBlank(mapperClass)) { table = tableFromMapperName(mapperClass); if (!isBlank(table)) { return table; } } return upper(scenario.getTargetTable()); } private String tableFromMapperName(String value) { String normalized = upper(value); if (normalized.contains(".USERMAPPER")) return "TB_USER"; if (normalized.contains(".CUSTOMERMAPPER")) return "TB_CUSTOMER"; if (normalized.contains(".ORDERMAPPER")) return "TB_ORDER"; return ""; } private boolean isPositiveMutationResult(Object result) { if (result instanceof Number) { return ((Number) result).intValue() > 0; } return result != null; } private void verifyByJson( ScenarioDefinition scenario, VerificationResultDto result, Map context, Map operationResults ) { Map verification = scenario.getVerification(); if (verification == null || verification.isEmpty()) { throw new IllegalArgumentException("verification is empty. Add verification rules to scenario JSON."); } String plain = string(resolve(verification.getOrDefault("plain", "${plain}"), context)); Object rawObject = lookupResult(operationResults, string(verification.getOrDefault("rawResult", "raw"))); Object decryptedObject = lookupResult(operationResults, string(verification.getOrDefault("decryptedResult", "decrypted"))); String cipher = extract(rawObject, string(verification.getOrDefault("cipherColumn", effectiveEncColumn(scenario)))); String hash = extract(rawObject, string(verification.getOrDefault("hashColumn", effectiveHashColumn(scenario)))); String decrypted = extract(decryptedObject, string(verification.getOrDefault("decryptedProperty", scenario.getJavaProperty()))); List checks = checks(verification); List failures = new ArrayList(); for (String check : checks) { if (!passes(check, plain, cipher, hash, decrypted, scenario)) { failures.add(check); } } boolean pass = failures.isEmpty(); result.setStatus(pass ? "PASS" : "FAIL"); result.setProgress(pass ? 100 : 0); result.setInputPlainText(plain); result.setDbCipherText(mask(cipher)); result.setDecryptedText(decrypted); Map detail = details(context, operationResults); detail.put("executionMode", executionMode(scenario)); detail.put("cipher", mask(cipher)); detail.put("hash", mask(hash)); detail.put("failedChecks", failures); result.setDetails(detail); if (!pass) { result.setExceptionMessage("Verification failed: " + String.join(", ", failures)); } } private boolean passes(String check, String plain, String cipher, String hash, String decrypted, ScenarioDefinition scenario) { String normalized = upper(check); if ("CIPHER_NOT_NULL".equals(normalized)) { return cipher != null; } if ("CIPHER_NOT_PLAIN".equals(normalized)) { return cipher != null && plain != null && !cipher.equals(plain); } if ("HASH_NOT_NULL".equals(normalized)) { return hash != null; } if ("HASH_NOT_PLAIN".equals(normalized)) { return hash != null && plain != null && !hash.equals(plain); } if ("DECRYPTED_EQUALS_PLAIN".equals(normalized)) { return plain != null && plain.equals(decrypted); } if ("ENCRYPT_TYPEHANDLER_SET_CALLED".equals(normalized)) { return callRegistry.getSetParameterCount(scenario.getEncryptTypeHandler()) > 0; } if ("HASH_TYPEHANDLER_SET_CALLED".equals(normalized)) { return callRegistry.getSetParameterCount(scenario.getHashTypeHandler()) > 0; } if ("ENCRYPT_TYPEHANDLER_GET_CALLED".equals(normalized)) { return callRegistry.getGetResultCount(scenario.getEncryptTypeHandler()) > 0; } throw new IllegalArgumentException("Unsupported verification check: " + check); } private Map buildContext( ScenarioDefinition scenario, String runId, String testRunId, long testIndex, Map overrides ) { Map context = new LinkedHashMap(); if (scenario.getDefaultParameters() != null) { context.putAll(scenario.getDefaultParameters()); } if (overrides != null) { for (Map.Entry entry : overrides.entrySet()) { context.put(entry.getKey(), overrideValue(scenario, entry.getKey(), entry.getValue(), testIndex)); } } if ("MAX_LENGTH_INSERT_UPDATE".equals(upper(scenario.getScenarioType()))) { String maxPlain = maxLengthText(scenario, testIndex, false); context.put(scenario.getJavaProperty(), maxPlain); context.put("updatedPlain", maxLengthText(scenario, testIndex, true)); } context.put("runId", runId); context.put("testRunId", testRunId); context.put("verificationRunId", testRunId); context.put("index", testIndex); context.put("scenarioId", scenario.getScenarioId()); Map resolved = new LinkedHashMap(); for (Map.Entry entry : context.entrySet()) { resolved.put(entry.getKey(), resolve(entry.getValue(), context)); } context.putAll(resolved); return context; } private Object overrideValue(ScenarioDefinition scenario, String key, Object value, long testIndex) { Object defaultValue = scenario.getDefaultParameters() == null ? null : scenario.getDefaultParameters().get(key); if (value instanceof String && defaultValue instanceof String) { String text = (String) value; String defaultText = (String) defaultValue; if (defaultText.contains("${index}") && !isBlank(text) && !text.contains("${index}")) { return text + "_" + testIndex; } } return value; } private Object resolve(Object value, Map context) { if (value instanceof String) { String resolved = (String) value; for (int i = 0; i < 5; i += 1) { String next = replaceTokens(resolved, context); if (next.equals(resolved)) { return next; } resolved = next; } return resolved; } if (value instanceof Map) { Map map = (Map) value; Map resolved = new LinkedHashMap(); for (Map.Entry entry : map.entrySet()) { resolved.put(string(entry.getKey()), resolve(entry.getValue(), context)); } return resolved; } if (value instanceof List) { List list = (List) value; List resolved = new ArrayList(); for (Object item : list) { resolved.add(resolve(item, context)); } return resolved; } return value; } private String replaceTokens(String text, Map context) { String resolved = text; for (Map.Entry entry : context.entrySet()) { Object value = entry.getValue(); if (value == null || value instanceof Map || value instanceof List) { continue; } resolved = resolved.replace("${" + entry.getKey() + "}", string(value)); } return resolved; } private Object convertToType(Object value, Class type) { if (value == null) { return null; } if (type.isInstance(value)) { return value; } if (type == String.class) { return string(value); } return objectMapper.convertValue(value, type); } private Object lookupResult(Map operationResults, String key) { if (isBlank(key)) { return null; } Object result = operationResults.get(key); if (result == null && "rawAfterUpdate".equals(key)) { return operationResults.get("raw"); } return result; } private String extract(Object source, String path) { if (source == null || isBlank(path)) { return source == null ? null : string(source); } Object current = source; for (String part : path.split("\\.")) { current = extractPart(current, part); if (current == null) { return null; } } return string(current); } @SuppressWarnings("unchecked") private Object extractPart(Object source, String part) { if (source instanceof List) { List list = (List) source; if (list.isEmpty()) { return null; } if ("first".equalsIgnoreCase(part) || "0".equals(part)) { return list.get(0); } return extractPart(list.get(0), part); } if (source instanceof Map) { Map values = (Map) source; Object value = values.get(part); if (value == null) value = values.get(part.toUpperCase(Locale.ROOT)); if (value == null) value = values.get(part.toLowerCase(Locale.ROOT)); return value; } Map converted = objectMapper.convertValue(source, Map.class); return extractPart(converted, part); } private List checks(Map verification) { List configured = asList(verification.get("checks")); if (configured == null || configured.isEmpty()) { return Arrays.asList("CIPHER_NOT_PLAIN", "HASH_NOT_PLAIN", "DECRYPTED_EQUALS_PLAIN"); } return configured.stream().map(this::string).collect(Collectors.toList()); } private Map details(Map context, Map operationResults) { Map details = new LinkedHashMap(); details.put("context", context); details.put("operationResults", operationResults); return details; } private List groupByMapperAndSql(List scenarios) { Map mapperNodes = new LinkedHashMap(); for (VerificationResultDto scenario : scenarios) { String mapperName = emptyToDefault(scenario.getMapperName(), "UNKNOWN"); VerificationResultDto mapper = mapperNodes.get(mapperName); if (mapper == null) { mapper = VerificationResultDto.node(mapperName, mapperName, "MAPPER"); mapperNodes.put(mapperName, mapper); } String sqlId = emptyToDefault(scenario.getSqlId(), "UNKNOWN"); String sqlKey = mapperName + ":" + sqlId; VerificationResultDto sql = findSqlNode(mapper, sqlKey, mapperName, sqlId); sql.getChildren().add(scenario); } for (VerificationResultDto mapper : mapperNodes.values()) { fillAggregateStatus(mapper); } return new ArrayList(mapperNodes.values()); } private VerificationResultDto findSqlNode(VerificationResultDto mapper, String sqlKey, String mapperName, String sqlId) { for (VerificationResultDto child : mapper.getChildren()) { if (sqlKey.equals(child.getId())) { return child; } } VerificationResultDto node = VerificationResultDto.node(sqlKey, commandLabel(sqlId), "SQL"); node.setMapperName(mapperName); node.setSqlId(sqlId); mapper.getChildren().add(node); return node; } private void fillAggregateStatus(VerificationResultDto node) { for (VerificationResultDto child : node.getChildren()) { fillAggregateStatus(child); } if (node.getChildren().isEmpty()) { return; } LeafAggregate aggregate = collectLeafAggregate(node); int progress = aggregate.count == 0 ? node.getProgress() : (int) Math.round(aggregate.progressSum * 1.0 / aggregate.count); node.setProgress(progress); node.setStatus(aggregate.fail ? "FAIL" : progress == 100 ? "PASS" : "READY"); } private LeafAggregate collectLeafAggregate(VerificationResultDto node) { if (node.getChildren().isEmpty()) { return new LeafAggregate(1, node.getProgress(), "FAIL".equals(node.getStatus()) || "UNSUPPORTED".equals(node.getStatus())); } LeafAggregate aggregate = new LeafAggregate(0, 0, false); for (VerificationResultDto child : node.getChildren()) { LeafAggregate childAggregate = collectLeafAggregate(child); aggregate.count += childAggregate.count; aggregate.progressSum += childAggregate.progressSum; aggregate.fail = aggregate.fail || childAggregate.fail; } return aggregate; } private String commandLabel(String sqlId) { if (sqlId == null) return "execute"; if (sqlId.startsWith("insert")) return "insert " + sqlId; if (sqlId.startsWith("update")) return "update " + sqlId; if (sqlId.startsWith("merge")) return "merge " + sqlId; if (sqlId.startsWith("delete")) return "delete " + sqlId; return "select " + sqlId; } private String executionMode(ScenarioDefinition scenario) { return emptyToDefault(scenario.getExecutionMode(), "SQL_SESSION").toUpperCase(Locale.ROOT); } private String effectiveEncColumn(ScenarioDefinition scenario) { if (!isBlank(scenario.getEncColumn())) { return scenario.getEncColumn(); } return upper(scenario.getTargetColumn()) + "_ENC"; } private String effectiveHashColumn(ScenarioDefinition scenario) { if (!isBlank(scenario.getHashColumn())) { return scenario.getHashColumn(); } return upper(scenario.getTargetColumn()) + "_HASH"; } private void appendMaxLengthDetail(ScenarioDefinition scenario, VerificationResultDto result, Map context) { Map detail = result.getDetails() == null ? new LinkedHashMap() : result.getDetails(); String maxPlain = string(context.get(scenario.getJavaProperty())); String updatedPlain = string(context.get("updatedPlain")); detail.put("maxLength", maxLength(scenario)); detail.put("insertPlainLength", maxPlain == null ? 0 : maxPlain.length()); detail.put("updatePlainLength", updatedPlain == null ? 0 : updatedPlain.length()); detail.put("maxLengthScenario", true); result.setDetails(detail); } private String mapperSqlText(String mapperFile, String sqlId) throws Exception { if (isBlank(mapperFile) || isBlank(sqlId)) { throw new IllegalArgumentException("mapperFile and sqlId are required for static detection."); } Path file = mapperRoot.resolve(mapperFile).normalize(); if (!file.startsWith(mapperRoot.normalize())) { throw new IllegalArgumentException("Mapper path is outside configured mapper root: " + mapperFile); } String xml = Files.readString(file, StandardCharsets.UTF_8); Matcher matcher = SQL_TAG_PATTERN.matcher(xml); while (matcher.find()) { if (sqlId.equals(matcher.group(2))) { return matcher.group(0); } } throw new IllegalArgumentException("Mapper sqlId not found: " + mapperFile + "." + sqlId); } private boolean containsIgnoreCase(String text, String value) { return text != null && value != null && text.toUpperCase(Locale.ROOT).contains(value.toUpperCase(Locale.ROOT)); } private String mapperNamespace(String mapperFile) { String base = mapperBase(mapperFile); return "com.samsung.cryptverify.business." + base.toLowerCase(Locale.ROOT) + "." + base + "Mapper"; } private String mapperBase(String mapperFile) { if (isBlank(mapperFile)) { return "User"; } String fileName = mapperFile.replace("\\", "/"); fileName = fileName.substring(fileName.lastIndexOf('/') + 1); return fileName.replace("Mapper.xml", ""); } private String parameterType(ScenarioDefinition scenario) { String base = mapperBase(scenario.getMapperFile()); return "com.samsung.cryptverify.business." + base.toLowerCase(Locale.ROOT) + "." + base + "Dto"; } private String idProperty(ScenarioDefinition scenario) { String table = upper(scenario.getTargetTable()); if ("TB_CUSTOMER".equals(table)) return "customerId"; if ("TB_ORDER".equals(table)) return "orderId"; return "userId"; } private String idColumn(ScenarioDefinition scenario) { String table = upper(scenario.getTargetTable()); if ("TB_CUSTOMER".equals(table)) return "CUSTOMER_ID"; if ("TB_ORDER".equals(table)) return "ORDER_ID"; return "USER_ID"; } private String defaultInsertSqlId(ScenarioDefinition scenario) { String table = upper(scenario.getTargetTable()); if ("TB_CUSTOMER".equals(table)) return "insertCustomer"; if ("TB_ORDER".equals(table)) return "insertOrder"; return "insertUser"; } private String defaultUpdateSqlId(ScenarioDefinition scenario) { String table = upper(scenario.getTargetTable()); if ("TB_CUSTOMER".equals(table)) return "updateCustomer"; if ("TB_USER".equals(table)) return "updateUserDynamic"; return ""; } private String defaultSelectByHashSqlId(ScenarioDefinition scenario) { String table = upper(scenario.getTargetTable()); if ("TB_CUSTOMER".equals(table)) return "selectCustomerByNameHash"; if ("TB_ORDER".equals(table)) return "selectOrdersByBuyerJoin"; return "selectUserByNameHash"; } private String maxLengthText(ScenarioDefinition scenario, long testIndex, boolean updated) { int length = maxLength(scenario); String prefix = updated ? "U" : "I"; String seed = prefix + "_" + scenario.getScenarioId() + "_" + testIndex + "_"; if (seed.length() >= length) { return seed.substring(0, length); } StringBuilder value = new StringBuilder(seed); while (value.length() < length) { value.append(updated ? "Z" : "X"); } return value.toString(); } private int maxLength(ScenarioDefinition scenario) { Object value = scenario.getDefaultParameters() == null ? null : scenario.getDefaultParameters().get("maxLength"); if (value instanceof Number) { return ((Number) value).intValue(); } if (value != null) { try { return Integer.parseInt(value.toString()); } catch (NumberFormatException ignored) { return inferMaxLength(scenario.getTargetColumn()); } } return inferMaxLength(scenario.getTargetColumn()); } private int inferMaxLength(String column) { String normalized = upper(column); if (normalized.contains("ADDRESS")) return 500; if (normalized.contains("PHONE")) return 40; if (normalized.contains("NAME")) return 300; return 100; } private String required(Map map, String key) { String value = string(map.get(key)); if (isBlank(value)) { throw new IllegalArgumentException(key + " is required."); } return value; } private String value(Map map, String key, String fallback) { Object value = map == null ? null : map.get(key); return value == null || isBlank(value.toString()) ? fallback : value.toString(); } private String rootMessage(Exception e) { Throwable root = e; while (root.getCause() != null) { root = root.getCause(); } return root.getMessage() == null ? e.toString() : root.getMessage(); } private String emptyToDefault(String value, String fallback) { return isBlank(value) ? fallback : value; } private String upper(String value) { return value == null ? "" : value.toUpperCase(Locale.ROOT); } private String string(Object value) { return value == null ? null : value.toString(); } private List asList(Object value) { if (value instanceof List) { return (List) value; } return null; } private List stringList(Object value) { List list = asList(value); if (list == null) { return Collections.emptyList(); } List result = new ArrayList(); for (Object item : list) { String text = string(item); if (!isBlank(text)) { result.add(text); } } return result; } private List safeIdentifiers(List identifiers, String label) { for (String identifier : identifiers) { assertSafeIdentifier(identifier, label); } return identifiers; } private void assertSafeIdentifier(String identifier, String label) { if (identifier == null || !identifier.matches("[A-Za-z_][A-Za-z0-9_$#]*(\\.[A-Za-z_][A-Za-z0-9_$#]*)?")) { throw new IllegalArgumentException("Unsafe " + label + ": " + identifier); } } private void assertSafeParameterName(String parameterName) { if (parameterName == null || !parameterName.matches("[A-Za-z_][A-Za-z0-9_]*")) { throw new IllegalArgumentException("Unsafe parameter name: " + parameterName); } } private String bindName(String column) { String bindName = column.contains(".") ? column.substring(column.lastIndexOf('.') + 1) : column; assertSafeParameterName(bindName); return bindName; } private String propertyName(String columnName) { if (isBlank(columnName)) { return ""; } String lower = columnName.toLowerCase(Locale.ROOT); StringBuilder value = new StringBuilder(); boolean upperNext = false; for (int i = 0; i < lower.length(); i += 1) { char ch = lower.charAt(i); if (ch == '_') { upperNext = true; continue; } value.append(upperNext ? Character.toUpperCase(ch) : ch); upperNext = false; } return value.toString(); } private Integer number(Object value) { if (value == null) { return null; } if (value instanceof Number) { return ((Number) value).intValue(); } try { return Integer.parseInt(value.toString()); } catch (NumberFormatException e) { return null; } } private Object firstNonNull(Object... values) { for (Object value : values) { if (value != null) { return value; } } return null; } private Object mapValue(Map map, String key) { Object value = map.get(key); if (value != null) { return value; } value = map.get(key.toLowerCase(Locale.ROOT)); if (value != null) { return value; } for (Map.Entry entry : map.entrySet()) { if (key.equalsIgnoreCase(entry.getKey())) { return entry.getValue(); } } return null; } private boolean isBlank(String value) { return value == null || value.trim().isEmpty(); } private String mask(String value) { if (value == null || value.length() <= 8) { return value; } return value.substring(0, 4) + "****" + value.substring(value.length() - 4); } private static class PrimaryKeyColumn { private final String columnName; private final String dataType; private final Integer dataLength; private final Integer charLength; private final String charUsed; private final Integer position; private PrimaryKeyColumn(String columnName, String dataType, Integer dataLength, Integer charLength, String charUsed, Integer position) { this.columnName = columnName; this.dataType = dataType; this.dataLength = dataLength; this.charLength = charLength; this.charUsed = charUsed; this.position = position; } public String getColumnName() { return columnName; } public String getDataType() { return dataType; } public Integer getDataLength() { return dataLength; } public Integer getCharLength() { return charLength; } public String getCharUsed() { return charUsed; } public Integer getPosition() { return position; } } private static class InsertedRow { private final String tableName; private final List columns; private final Map pkValues; private InsertedRow(String tableName, List columns, Map pkValues) { this.tableName = tableName; this.columns = columns; this.pkValues = pkValues; } } private static class LeafAggregate { private int count; private int progressSum; private boolean fail; private LeafAggregate(int count, int progressSum, boolean fail) { this.count = count; this.progressSum = progressSum; this.fail = fail; } } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/verification/engine/VerificationIndexStore.java 상태: clean 크기: 2390 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.verification.engine; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.LinkedHashMap; import java.util.Map; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @Component public class VerificationIndexStore { private final ObjectMapper objectMapper; private final Path indexPath; public VerificationIndexStore( ObjectMapper objectMapper, @Value("${crypto-verification.index-store:local-json/verification-index.json}") String indexStorePath ) { this.objectMapper = objectMapper; this.indexPath = Paths.get(indexStorePath); } // 시나리오별 테스트값 인덱스를 증가시키고 로컬 JSON 파일에 저장한다. public synchronized long nextIndex(String scenarioId) { try { Map indexes = load(); long next = indexes.getOrDefault(scenarioId, 0L) + 1L; indexes.put(scenarioId, next); save(indexes); return next; } catch (IOException e) { throw new IllegalStateException("검증 인덱스 JSON 저장 실패", e); } } private Map load() throws IOException { ensureFileExists(); return objectMapper.readValue(readString(indexPath), new TypeReference>() {}); } private void save(Map indexes) throws IOException { Files.createDirectories(indexPath.getParent()); writeString(indexPath, objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(indexes)); } private void ensureFileExists() throws IOException { if (Files.exists(indexPath)) { return; } Files.createDirectories(indexPath.getParent()); writeString(indexPath, "{}"); } private String readString(Path path) throws IOException { return new String(Files.readAllBytes(path), StandardCharsets.UTF_8); } private void writeString(Path path, String value) throws IOException { Files.write(path, value.getBytes(StandardCharsets.UTF_8)); } } ------------------------------ 파일명: target-spring-oracle/src/main/java/com/samsung/cryptverify/verification/mapper/CryptoVerificationMapper.java 상태: clean 크기: 865 bytes ------------------------------ 내용 ------------------------------ package com.samsung.cryptverify.verification.mapper; import java.util.Map; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import com.samsung.cryptverify.business.user.UserDto; @Mapper public interface CryptoVerificationMapper { Map selectUserRaw(@Param("userId") String userId); Map selectCustomerRaw(@Param("customerId") String customerId); Map selectOrderRaw(@Param("orderId") String orderId); UserDto selectUserEncAliasDecrypt(@Param("userId") String userId); int cleanupOrdersByVerificationRunId(@Param("verificationRunId") String verificationRunId); int cleanupCustomersByVerificationRunId(@Param("verificationRunId") String verificationRunId); int cleanupUsersByVerificationRunId(@Param("verificationRunId") String verificationRunId); } ------------------------------ 파일명: target-spring-oracle/src/main/resources/application.yml 상태: M 크기: 884 bytes ------------------------------ 내용 ------------------------------ server: port: 8080 tomcat: accesslog: enabled: true pattern: '%h %l %u %t "%r" %s %b %D' spring: datasource: driver-class-name: com.p6spy.engine.spy.P6SpyDriver url: jdbc:p6spy:oracle:thin:@localhost:1521:XE username: CRYPT_VERIFY password: CRYPT_VERIFY mybatis: mapper-locations: classpath*:mapper/**/*.xml type-aliases-package: com.samsung.cryptverify.business configuration: map-underscore-to-camel-case: true jdbc-type-for-null: "NULL" crypto: key: askdfjqlwkerqlkwrq crypto-verification: enabled: true mapper-root: src/main/resources/mapper/business target-store: local-json/crypto-targets.json index-store: local-json/verification-index.json mask-sensitive-values: true logging: level: root: INFO com.samsung.cryptverify: INFO p6spy: INFO org.springframework.web: INFO org.mybatis: INFO ------------------------------ 파일명: target-spring-oracle/src/main/resources/mapper/business/CustomerMapper.xml 상태: clean 크기: 3108 bytes ------------------------------ 내용 ------------------------------ INSERT INTO TB_CUSTOMER ( CUSTOMER_ID, CUSTOMER_NAME, CUSTOMER_NAME_ENC, CUSTOMER_NAME_HASH, ADDRESS, ADDRESS_ENC, ADDRESS_HASH, GRADE_CODE, VERIFICATION_RUN_ID, CREATED_AT ) VALUES ( #{customerId}, #{customerName}, #{customerName, typeHandler=com.samsung.cryptverify.crypto.NameEncryptTypeHandler}, #{customerName, typeHandler=com.samsung.cryptverify.crypto.NameHashTypeHandler}, #{address}, #{address, typeHandler=com.samsung.cryptverify.crypto.AddressEncryptTypeHandler}, #{address, typeHandler=com.samsung.cryptverify.crypto.AddressHashTypeHandler}, #{gradeCode}, #{verificationRunId}, SYSTIMESTAMP ) UPDATE TB_CUSTOMER SET CUSTOMER_NAME = #{customerName}, CUSTOMER_NAME_ENC = #{customerName, typeHandler=com.samsung.cryptverify.crypto.NameEncryptTypeHandler}, ADDRESS = #{address}, ADDRESS_ENC = #{address, typeHandler=com.samsung.cryptverify.crypto.AddressEncryptTypeHandler} WHERE CUSTOMER_ID = #{customerId} ------------------------------ 파일명: target-spring-oracle/src/main/resources/mapper/business/OrderMapper.xml 상태: clean 크기: 1857 bytes ------------------------------ 내용 ------------------------------ INSERT INTO TB_ORDER ( ORDER_ID, USER_ID, BUYER_NAME, BUYER_NAME_ENC, BUYER_NAME_HASH, AMOUNT, VERIFICATION_RUN_ID, CREATED_AT ) VALUES ( #{orderId}, #{userId}, #{buyerName}, #{buyerName, typeHandler=com.samsung.cryptverify.crypto.NameEncryptTypeHandler}, #{buyerName, typeHandler=com.samsung.cryptverify.crypto.NameHashTypeHandler}, #{amount}, #{verificationRunId}, SYSTIMESTAMP ) ------------------------------ 파일명: target-spring-oracle/src/main/resources/mapper/business/UserMapper.xml 상태: clean 크기: 7378 bytes ------------------------------ 내용 ------------------------------ USER_ID, USER_NAME, USER_NAME_ENC AS USER_NAME_ENC, USER_NAME_HASH, PHONE, PHONE_ENC AS PHONE_ENC, PHONE_HASH, EMAIL, VERIFICATION_RUN_ID INSERT INTO TB_USER ( USER_ID, USER_NAME, PHONE, EMAIL, VERIFICATION_RUN_ID, CREATED_AT ) VALUES ( #{userId}, #{userName}, #{phone}, #{email}, #{verificationRunId}, SYSTIMESTAMP ) INSERT ALL INTO TB_USER ( USER_ID, USER_NAME, USER_NAME_ENC, USER_NAME_HASH, PHONE, PHONE_ENC, PHONE_HASH, EMAIL, VERIFICATION_RUN_ID, CREATED_AT ) VALUES ( #{item.userId}, #{item.userName}, #{item.userName, typeHandler=com.samsung.cryptverify.crypto.NameEncryptTypeHandler}, #{item.userName, typeHandler=com.samsung.cryptverify.crypto.NameHashTypeHandler}, #{item.phone}, #{item.phone, typeHandler=com.samsung.cryptverify.crypto.PhoneEncryptTypeHandler}, #{item.phone, typeHandler=com.samsung.cryptverify.crypto.PhoneHashTypeHandler}, #{item.email}, #{item.verificationRunId}, SYSTIMESTAMP ) SELECT 1 FROM DUAL UPDATE TB_USER USER_NAME = #{userName}, USER_NAME_ENC = #{userName, typeHandler=com.samsung.cryptverify.crypto.NameEncryptTypeHandler}, USER_NAME_HASH = #{userName, typeHandler=com.samsung.cryptverify.crypto.NameHashTypeHandler}, PHONE = #{phone}, PHONE_ENC = #{phone, typeHandler=com.samsung.cryptverify.crypto.PhoneEncryptTypeHandler}, PHONE_HASH = #{phone, typeHandler=com.samsung.cryptverify.crypto.PhoneHashTypeHandler}, EMAIL = #{email}, WHERE USER_ID = #{userId} MERGE INTO TB_USER T USING (SELECT #{userId} AS USER_ID FROM DUAL) S ON (T.USER_ID = S.USER_ID) WHEN MATCHED THEN UPDATE SET T.USER_NAME = #{userName}, T.USER_NAME_ENC = #{userName, typeHandler=com.samsung.cryptverify.crypto.NameEncryptTypeHandler}, T.USER_NAME_HASH = #{userName, typeHandler=com.samsung.cryptverify.crypto.NameHashTypeHandler}, T.PHONE = #{phone}, T.PHONE_ENC = #{phone, typeHandler=com.samsung.cryptverify.crypto.PhoneEncryptTypeHandler}, T.PHONE_HASH = #{phone, typeHandler=com.samsung.cryptverify.crypto.PhoneHashTypeHandler} WHEN NOT MATCHED THEN INSERT ( USER_ID, USER_NAME, USER_NAME_ENC, USER_NAME_HASH, PHONE, PHONE_ENC, PHONE_HASH, EMAIL, VERIFICATION_RUN_ID, CREATED_AT ) VALUES ( #{userId}, #{userName}, #{userName, typeHandler=com.samsung.cryptverify.crypto.NameEncryptTypeHandler}, #{userName, typeHandler=com.samsung.cryptverify.crypto.NameHashTypeHandler}, #{phone}, #{phone, typeHandler=com.samsung.cryptverify.crypto.PhoneEncryptTypeHandler}, #{phone, typeHandler=com.samsung.cryptverify.crypto.PhoneHashTypeHandler}, #{email}, #{verificationRunId}, SYSTIMESTAMP ) DELETE FROM TB_USER WHERE USER_NAME_HASH = #{userName, typeHandler=com.samsung.cryptverify.crypto.NameHashTypeHandler} ------------------------------ 파일명: target-spring-oracle/src/main/resources/mapper/business/UserMapper.xml.20260711142950.bak 상태: ?? 크기: 7378 bytes ------------------------------ 내용 ------------------------------ USER_ID, USER_NAME, USER_NAME_ENC AS USER_NAME_ENC, USER_NAME_HASH, PHONE, PHONE_ENC AS PHONE_ENC, PHONE_HASH, EMAIL, VERIFICATION_RUN_ID INSERT INTO TB_USER ( USER_ID, USER_NAME, PHONE, EMAIL, VERIFICATION_RUN_ID, CREATED_AT ) VALUES ( #{userId}, #{userName}, #{phone}, #{email}, #{verificationRunId}, SYSTIMESTAMP ) INSERT ALL INTO TB_USER ( USER_ID, USER_NAME, USER_NAME_ENC, USER_NAME_HASH, PHONE, PHONE_ENC, PHONE_HASH, EMAIL, VERIFICATION_RUN_ID, CREATED_AT ) VALUES ( #{item.userId}, #{item.userName}, #{item.userName, typeHandler=com.samsung.cryptverify.crypto.NameEncryptTypeHandler}, #{item.userName, typeHandler=com.samsung.cryptverify.crypto.NameHashTypeHandler}, #{item.phone}, #{item.phone, typeHandler=com.samsung.cryptverify.crypto.PhoneEncryptTypeHandler}, #{item.phone, typeHandler=com.samsung.cryptverify.crypto.PhoneHashTypeHandler}, #{item.email}, #{item.verificationRunId}, SYSTIMESTAMP ) SELECT 1 FROM DUAL UPDATE TB_USER USER_NAME = #{userName}, USER_NAME_ENC = #{userName, typeHandler=com.samsung.cryptverify.crypto.NameEncryptTypeHandler}, USER_NAME_HASH = #{userName, typeHandler=com.samsung.cryptverify.crypto.NameHashTypeHandler}, PHONE = #{phone}, PHONE_ENC = #{phone, typeHandler=com.samsung.cryptverify.crypto.PhoneEncryptTypeHandler}, PHONE_HASH = #{phone, typeHandler=com.samsung.cryptverify.crypto.PhoneHashTypeHandler}, EMAIL = #{email}, WHERE USER_ID = #{userId} MERGE INTO TB_USER T USING (SELECT #{userId} AS USER_ID FROM DUAL) S ON (T.USER_ID = S.USER_ID) WHEN MATCHED THEN UPDATE SET T.USER_NAME = #{userName}, T.USER_NAME_ENC = #{userName, typeHandler=com.samsung.cryptverify.crypto.NameEncryptTypeHandler}, T.USER_NAME_HASH = #{userName, typeHandler=com.samsung.cryptverify.crypto.NameHashTypeHandler}, T.PHONE = #{phone}, T.PHONE_ENC = #{phone, typeHandler=com.samsung.cryptverify.crypto.PhoneEncryptTypeHandler}, T.PHONE_HASH = #{phone, typeHandler=com.samsung.cryptverify.crypto.PhoneHashTypeHandler} WHEN NOT MATCHED THEN INSERT ( USER_ID, USER_NAME, USER_NAME_ENC, USER_NAME_HASH, PHONE, PHONE_ENC, PHONE_HASH, EMAIL, VERIFICATION_RUN_ID, CREATED_AT ) VALUES ( #{userId}, #{userName}, #{userName, typeHandler=com.samsung.cryptverify.crypto.NameEncryptTypeHandler}, #{userName, typeHandler=com.samsung.cryptverify.crypto.NameHashTypeHandler}, #{phone}, #{phone, typeHandler=com.samsung.cryptverify.crypto.PhoneEncryptTypeHandler}, #{phone, typeHandler=com.samsung.cryptverify.crypto.PhoneHashTypeHandler}, #{email}, #{verificationRunId}, SYSTIMESTAMP ) DELETE FROM TB_USER WHERE USER_NAME_HASH = #{userName, typeHandler=com.samsung.cryptverify.crypto.NameHashTypeHandler} ------------------------------ 파일명: target-spring-oracle/src/main/resources/mapper/verification/CryptoVerificationMapper.xml 상태: clean 크기: 1737 bytes ------------------------------ 내용 ------------------------------ DELETE FROM TB_ORDER WHERE VERIFICATION_RUN_ID = #{verificationRunId} DELETE FROM TB_CUSTOMER WHERE VERIFICATION_RUN_ID = #{verificationRunId} DELETE FROM TB_USER WHERE VERIFICATION_RUN_ID = #{verificationRunId} ------------------------------ 파일명: target-spring-oracle/src/main/resources/spy.properties 상태: ?? 크기: 337 bytes ------------------------------ 내용 ------------------------------ modulelist=com.p6spy.engine.spy.P6SpyFactory appender=com.p6spy.engine.spy.appender.Slf4JLogger logMessageFormat=com.p6spy.engine.spy.appender.SingleLineFormat excludecategories=info,debug,result,resultset,batch,commit,rollback deregisterdrivers=true useprefix=true dateformat=yyyy-MM-dd HH:mm:ss.SSS driverlist=oracle.jdbc.OracleDriver ------------------------------ 파일명: target-spring-oracle/src/main/resources/verification-scenarios.json 상태: clean 크기: 8928 bytes ------------------------------ 내용 ------------------------------ [ { "scenarioId": "USER_INSERT_NAME_SQL_SESSION", "displayName": "USER_NAME SQL_SESSION insert/select round trip", "mapperFile": "UserMapper.xml", "sqlId": "insertUser", "sqlCommandType": "INSERT", "scenarioType": "INSERT_ROUND_TRIP", "executionMode": "SQL_SESSION", "targetTable": "TB_USER", "targetColumn": "USER_NAME", "encColumn": "USER_NAME_ENC", "hashColumn": "USER_NAME_HASH", "javaProperty": "userName", "encryptTypeHandler": "NameEncryptTypeHandler", "hashTypeHandler": "NameHashTypeHandler", "defaultParameters": { "userId": "U_${index}", "userName": "user_verify_${index}", "phone": "010-1234-0001", "email": "verify@example.com", "verificationRunId": "${testRunId}" }, "operations": [ { "name": "insert", "command": "INSERT", "statement": "com.samsung.cryptverify.business.user.UserMapper.insertUser", "parameterType": "com.samsung.cryptverify.business.user.UserDto", "parameter": { "userId": "${userId}", "userName": "${userName}", "phone": "${phone}", "email": "${email}", "verificationRunId": "${verificationRunId}" }, "resultKey": "insertCount" }, { "name": "raw", "mode": "DYNAMIC_SQL", "command": "SELECT_ONE", "table": "TB_USER", "columns": [ "USER_ID", "USER_NAME", "USER_NAME_ENC", "USER_NAME_HASH", "PHONE", "PHONE_ENC", "PHONE_HASH", "EMAIL", "VERIFICATION_RUN_ID" ], "where": { "USER_ID": "userId" }, "parameter": { "userId": "${userId}" }, "resultKey": "raw" }, { "name": "decrypted", "command": "SELECT_FIRST", "statement": "com.samsung.cryptverify.business.user.UserMapper.selectUserByNameHash", "parameter": { "userName": "${userName}" }, "resultKey": "decrypted" } ], "verification": { "plain": "${userName}", "rawResult": "raw", "decryptedResult": "decrypted", "cipherColumn": "USER_NAME_ENC", "hashColumn": "USER_NAME_HASH", "decryptedProperty": "userName", "checks": [ "CIPHER_NOT_PLAIN", "HASH_NOT_PLAIN", "DECRYPTED_EQUALS_PLAIN", "ENCRYPT_TYPEHANDLER_SET_CALLED", "HASH_TYPEHANDLER_SET_CALLED", "ENCRYPT_TYPEHANDLER_GET_CALLED" ] }, "atoms": [ "PARAMETER_ENCRYPTION", "DB_CIPHER_STORAGE", "HASH_STORAGE", "RESULT_DECRYPTION", "CALL_COUNT" ] }, { "scenarioId": "CUSTOMER_INSERT_NAME_REFLECTION", "displayName": "CUSTOMER_NAME REFLECTION insert/select round trip", "mapperFile": "CustomerMapper.xml", "sqlId": "insertCustomer", "sqlCommandType": "INSERT", "scenarioType": "INSERT_ROUND_TRIP", "executionMode": "REFLECTION", "targetTable": "TB_CUSTOMER", "targetColumn": "CUSTOMER_NAME", "encColumn": "CUSTOMER_NAME_ENC", "hashColumn": "CUSTOMER_NAME_HASH", "javaProperty": "customerName", "encryptTypeHandler": "NameEncryptTypeHandler", "hashTypeHandler": "NameHashTypeHandler", "defaultParameters": { "customerId": "C_${index}", "customerName": "customer_verify_${index}", "address": "address_verify_${index}", "gradeCode": "A", "verificationRunId": "${testRunId}" }, "operations": [ { "name": "insert", "mapperClass": "com.samsung.cryptverify.business.customer.CustomerMapper", "method": "insertCustomer", "parameterType": "com.samsung.cryptverify.business.customer.CustomerDto", "parameter": { "customerId": "${customerId}", "customerName": "${customerName}", "address": "${address}", "gradeCode": "${gradeCode}", "verificationRunId": "${verificationRunId}" }, "resultKey": "insertCount" }, { "name": "raw", "mode": "DYNAMIC_SQL", "command": "SELECT_ONE", "table": "TB_CUSTOMER", "columns": [ "CUSTOMER_ID", "CUSTOMER_NAME", "CUSTOMER_NAME_ENC", "CUSTOMER_NAME_HASH", "ADDRESS", "ADDRESS_ENC", "ADDRESS_HASH", "GRADE_CODE", "VERIFICATION_RUN_ID" ], "where": { "CUSTOMER_ID": "customerId" }, "parameter": { "customerId": "${customerId}" }, "resultKey": "raw" }, { "name": "decrypted", "mode": "SQL_SESSION", "command": "SELECT_FIRST", "statement": "com.samsung.cryptverify.business.customer.CustomerMapper.selectCustomerByNameHash", "parameter": { "customerName": "${customerName}" }, "resultKey": "decrypted" } ], "verification": { "plain": "${customerName}", "rawResult": "raw", "decryptedResult": "decrypted", "cipherColumn": "CUSTOMER_NAME_ENC", "hashColumn": "CUSTOMER_NAME_HASH", "decryptedProperty": "customerName", "checks": [ "CIPHER_NOT_PLAIN", "HASH_NOT_PLAIN", "DECRYPTED_EQUALS_PLAIN", "ENCRYPT_TYPEHANDLER_SET_CALLED", "HASH_TYPEHANDLER_SET_CALLED", "ENCRYPT_TYPEHANDLER_GET_CALLED" ] }, "atoms": [ "PARAMETER_ENCRYPTION", "DB_CIPHER_STORAGE", "HASH_STORAGE", "RESULT_DECRYPTION", "CALL_COUNT" ] }, { "scenarioId": "ORDER_INSERT_BUYER_SQL_SESSION", "displayName": "BUYER_NAME SQL_SESSION insert/join round trip", "mapperFile": "OrderMapper.xml", "sqlId": "insertOrder", "sqlCommandType": "INSERT", "scenarioType": "INSERT_ROUND_TRIP", "executionMode": "SQL_SESSION", "targetTable": "TB_ORDER", "targetColumn": "BUYER_NAME", "encColumn": "BUYER_NAME_ENC", "hashColumn": "BUYER_NAME_HASH", "javaProperty": "buyerName", "encryptTypeHandler": "NameEncryptTypeHandler", "hashTypeHandler": "NameHashTypeHandler", "defaultParameters": { "userId": "OU_${index}", "orderId": "O_${index}", "buyerName": "order_verify_${index}", "phone": "010-3333-0001", "email": "order@example.com", "amount": 2000, "verificationRunId": "${testRunId}" }, "operations": [ { "name": "insertUser", "command": "INSERT", "statement": "com.samsung.cryptverify.business.user.UserMapper.insertUser", "parameterType": "com.samsung.cryptverify.business.user.UserDto", "parameter": { "userId": "${userId}", "userName": "${buyerName}", "phone": "${phone}", "email": "${email}", "verificationRunId": "${verificationRunId}" }, "resultKey": "insertUserCount" }, { "name": "insertOrder", "command": "INSERT", "statement": "com.samsung.cryptverify.business.order.OrderMapper.insertOrder", "parameterType": "com.samsung.cryptverify.business.order.OrderDto", "parameter": { "orderId": "${orderId}", "userId": "${userId}", "buyerName": "${buyerName}", "amount": "${amount}", "verificationRunId": "${verificationRunId}" }, "resultKey": "insertOrderCount" }, { "name": "raw", "mode": "DYNAMIC_SQL", "command": "SELECT_ONE", "table": "TB_ORDER", "columns": [ "ORDER_ID", "USER_ID", "BUYER_NAME", "BUYER_NAME_ENC", "BUYER_NAME_HASH", "AMOUNT", "VERIFICATION_RUN_ID" ], "where": { "ORDER_ID": "orderId" }, "parameter": { "orderId": "${orderId}" }, "resultKey": "raw" }, { "name": "decrypted", "command": "SELECT_FIRST", "statement": "com.samsung.cryptverify.business.order.OrderMapper.selectOrdersByBuyerJoin", "parameter": { "buyerName": "${buyerName}" }, "resultKey": "decrypted" } ], "verification": { "plain": "${buyerName}", "rawResult": "raw", "decryptedResult": "decrypted", "cipherColumn": "BUYER_NAME_ENC", "hashColumn": "BUYER_NAME_HASH", "decryptedProperty": "buyerName", "checks": [ "CIPHER_NOT_PLAIN", "HASH_NOT_PLAIN", "DECRYPTED_EQUALS_PLAIN", "ENCRYPT_TYPEHANDLER_SET_CALLED", "HASH_TYPEHANDLER_SET_CALLED", "ENCRYPT_TYPEHANDLER_GET_CALLED" ] }, "atoms": [ "PARAMETER_ENCRYPTION", "DB_CIPHER_STORAGE", "HASH_STORAGE", "RESULT_DECRYPTION", "CALL_COUNT" ] } ]

댓글

이 블로그의 인기 게시물

food eff privacy

판다 스픽 , 개인정보 처리방침 , Panda Speak Privacy Term

판다 수학 개인정보 처리방침 , Privacy , Panda Math