feat: show DDS SQL execution evidence
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
package com.cloudhandson.ddsbackoffice.domain;
|
||||||
|
|
||||||
|
/** Read-only evidence collected immediately after a protected search executes. */
|
||||||
|
public record DdsSqlEvidence(
|
||||||
|
boolean available,
|
||||||
|
String requestId,
|
||||||
|
String submittedStatement,
|
||||||
|
String sqlId,
|
||||||
|
Integer childNumber,
|
||||||
|
String lastActiveTime,
|
||||||
|
Long executions,
|
||||||
|
String planPredicate,
|
||||||
|
String vpdPredicate,
|
||||||
|
String status
|
||||||
|
) {
|
||||||
|
public static DdsSqlEvidence unavailable(String requestId, String submittedStatement, String status) {
|
||||||
|
return new DdsSqlEvidence(false, requestId, submittedStatement, null, null, null, null, null, null, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,8 @@ public record DdsVectorSearchResult(
|
|||||||
String sessionUser,
|
String sessionUser,
|
||||||
String endUser,
|
String endUser,
|
||||||
Integer oracleCode,
|
Integer oracleCode,
|
||||||
List<Map<String, Object>> rows
|
List<Map<String, Object>> rows,
|
||||||
|
DdsSqlEvidence sqlEvidence
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public DdsVectorSearchResult {
|
public DdsVectorSearchResult {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.cloudhandson.ddsbackoffice.service;
|
||||||
|
|
||||||
|
import com.cloudhandson.ddsbackoffice.domain.DdsSqlEvidence;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/** Keeps a small, redacted in-memory audit trail for the console session. */
|
||||||
|
@Service
|
||||||
|
public class DdsSqlEvidenceHistory {
|
||||||
|
private static final int MAX_ENTRIES = 20;
|
||||||
|
private final ArrayDeque<DdsSqlEvidence> entries = new ArrayDeque<>();
|
||||||
|
|
||||||
|
public synchronized void record(DdsSqlEvidence evidence) {
|
||||||
|
if (evidence == null) return;
|
||||||
|
entries.removeIf(entry -> entry.requestId().equals(evidence.requestId()));
|
||||||
|
entries.addFirst(evidence);
|
||||||
|
while (entries.size() > MAX_ENTRIES) entries.removeLast();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized List<DdsSqlEvidence> recent() {
|
||||||
|
return List.copyOf(entries);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.cloudhandson.ddsbackoffice.service;
|
|||||||
|
|
||||||
import com.cloudhandson.ddsbackoffice.config.DdsProperties;
|
import com.cloudhandson.ddsbackoffice.config.DdsProperties;
|
||||||
import com.cloudhandson.ddsbackoffice.domain.DdsVectorSearchResult;
|
import com.cloudhandson.ddsbackoffice.domain.DdsVectorSearchResult;
|
||||||
|
import com.cloudhandson.ddsbackoffice.domain.DdsSqlEvidence;
|
||||||
import com.cloudhandson.vpdbackoffice.domain.vector.VectorIngestCommand;
|
import com.cloudhandson.vpdbackoffice.domain.vector.VectorIngestCommand;
|
||||||
import com.cloudhandson.vpdbackoffice.domain.vector.VectorIngestResult;
|
import com.cloudhandson.vpdbackoffice.domain.vector.VectorIngestResult;
|
||||||
import com.cloudhandson.vpdbackoffice.domain.vector.VectorKnowledgeSummary;
|
import com.cloudhandson.vpdbackoffice.domain.vector.VectorKnowledgeSummary;
|
||||||
@@ -21,6 +22,7 @@ import java.util.Locale;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -44,17 +46,20 @@ public class DdsVectorKnowledgeService {
|
|||||||
private final OpenAiCompatibleClient embeddingClient;
|
private final OpenAiCompatibleClient embeddingClient;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final DdsProperties properties;
|
private final DdsProperties properties;
|
||||||
|
private final DdsSqlEvidenceHistory evidenceHistory;
|
||||||
|
|
||||||
public DdsVectorKnowledgeService(
|
public DdsVectorKnowledgeService(
|
||||||
VectorKnowledgeService commonVectorService,
|
VectorKnowledgeService commonVectorService,
|
||||||
OpenAiCompatibleClient embeddingClient,
|
OpenAiCompatibleClient embeddingClient,
|
||||||
ObjectMapper objectMapper,
|
ObjectMapper objectMapper,
|
||||||
DdsProperties properties
|
DdsProperties properties,
|
||||||
|
DdsSqlEvidenceHistory evidenceHistory
|
||||||
) {
|
) {
|
||||||
this.commonVectorService = commonVectorService;
|
this.commonVectorService = commonVectorService;
|
||||||
this.embeddingClient = embeddingClient;
|
this.embeddingClient = embeddingClient;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
|
this.evidenceHistory = evidenceHistory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public VectorKnowledgeSummary summary() {
|
public VectorKnowledgeSummary summary() {
|
||||||
@@ -101,7 +106,11 @@ public class DdsVectorKnowledgeService {
|
|||||||
connectionProperties.setProperty("oracle.net.CONNECT_TIMEOUT", String.valueOf(timeoutSeconds * 1000));
|
connectionProperties.setProperty("oracle.net.CONNECT_TIMEOUT", String.valueOf(timeoutSeconds * 1000));
|
||||||
connectionProperties.setProperty("oracle.jdbc.ReadTimeout", String.valueOf(timeoutSeconds * 1000));
|
connectionProperties.setProperty("oracle.jdbc.ReadTimeout", String.valueOf(timeoutSeconds * 1000));
|
||||||
|
|
||||||
String sql = "SELECT chunk_id, document_id, chunk_no, title, chunk_text, source_uri, tech_tag, score "
|
String requestId = UUID.randomUUID().toString();
|
||||||
|
String marker = "DDS_EVIDENCE:" + requestId;
|
||||||
|
String submittedStatement = "지식자료 벡터 검색 · " + properties.vectorObject()
|
||||||
|
+ " · 유사도순 정렬 · 결과 " + limit + "건";
|
||||||
|
String sql = "SELECT /* " + marker + " */ chunk_id, document_id, chunk_no, title, chunk_text, source_uri, tech_tag, score "
|
||||||
+ "FROM (SELECT d.chunk_id, d.document_id, d.chunk_no, d.title, d.chunk_text, d.source_uri, "
|
+ "FROM (SELECT d.chunk_id, d.document_id, d.chunk_no, d.title, d.chunk_text, d.source_uri, "
|
||||||
+ "d.tech_tag, VECTOR_DISTANCE(d.embedding, TO_VECTOR(?), COSINE) AS score "
|
+ "d.tech_tag, VECTOR_DISTANCE(d.embedding, TO_VECTOR(?), COSINE) AS score "
|
||||||
+ "FROM " + properties.vectorObject() + " d "
|
+ "FROM " + properties.vectorObject() + " d "
|
||||||
@@ -135,10 +144,12 @@ public class DdsVectorKnowledgeService {
|
|||||||
String message = rows.isEmpty()
|
String message = rows.isEmpty()
|
||||||
? "DDS DATA GRANT를 통과한 검색 단위가 없습니다."
|
? "DDS DATA GRANT를 통과한 검색 단위가 없습니다."
|
||||||
: rows.size() + "개 검색 단위가 DDS DATA GRANT를 통과했습니다.";
|
: rows.size() + "개 검색 단위가 DDS DATA GRANT를 통과했습니다.";
|
||||||
|
DdsSqlEvidence evidence = collectEvidence(connection, marker, submittedStatement, timeoutSeconds);
|
||||||
|
evidenceHistory.record(evidence);
|
||||||
return new DdsVectorSearchResult(
|
return new DdsVectorSearchResult(
|
||||||
normalizedUserKey, userLabel, properties.vectorObject(), normalizedQuery, mode,
|
normalizedUserKey, userLabel, properties.vectorObject(), normalizedQuery, mode,
|
||||||
embeddingModel(mode), true, "DDS 권한으로 검색했습니다.", message,
|
embeddingModel(mode), true, "DDS 권한으로 검색했습니다.", message,
|
||||||
sessionUser, endUser, null, rows);
|
sessionUser, endUser, null, rows, evidence);
|
||||||
}
|
}
|
||||||
} catch (SQLException exception) {
|
} catch (SQLException exception) {
|
||||||
return failure(normalizedUserKey, userLabel, normalizedQuery, mode,
|
return failure(normalizedUserKey, userLabel, normalizedQuery, mode,
|
||||||
@@ -184,7 +195,11 @@ public class DdsVectorKnowledgeService {
|
|||||||
connectionProperties.setProperty("oracle.net.CONNECT_TIMEOUT", String.valueOf(timeoutSeconds * 1000));
|
connectionProperties.setProperty("oracle.net.CONNECT_TIMEOUT", String.valueOf(timeoutSeconds * 1000));
|
||||||
connectionProperties.setProperty("oracle.jdbc.ReadTimeout", String.valueOf(timeoutSeconds * 1000));
|
connectionProperties.setProperty("oracle.jdbc.ReadTimeout", String.valueOf(timeoutSeconds * 1000));
|
||||||
|
|
||||||
String sql = "SELECT chunk_id, document_id, chunk_no, title, chunk_text, source_uri, tech_tag, score "
|
String requestId = UUID.randomUUID().toString();
|
||||||
|
String marker = "DDS_EVIDENCE:" + requestId;
|
||||||
|
String submittedStatement = "지식자료 벡터 검색 · " + properties.vectorObject()
|
||||||
|
+ " · 유사도순 정렬 · 결과 " + limit + "건";
|
||||||
|
String sql = "SELECT /* " + marker + " */ chunk_id, document_id, chunk_no, title, chunk_text, source_uri, tech_tag, score "
|
||||||
+ "FROM (SELECT d.chunk_id, d.document_id, d.chunk_no, d.title, d.chunk_text, d.source_uri, "
|
+ "FROM (SELECT d.chunk_id, d.document_id, d.chunk_no, d.title, d.chunk_text, d.source_uri, "
|
||||||
+ "d.tech_tag, VECTOR_DISTANCE(d.embedding, TO_VECTOR(?), COSINE) AS score "
|
+ "d.tech_tag, VECTOR_DISTANCE(d.embedding, TO_VECTOR(?), COSINE) AS score "
|
||||||
+ "FROM " + properties.vectorObject() + " d "
|
+ "FROM " + properties.vectorObject() + " d "
|
||||||
@@ -229,10 +244,12 @@ public class DdsVectorKnowledgeService {
|
|||||||
: rows.size() + "개 검색 단위가 토큰으로 식별된 업무 사용자("
|
: rows.size() + "개 검색 단위가 토큰으로 식별된 업무 사용자("
|
||||||
+ (resolvedAppUser == null ? "확인 불가" : resolvedAppUser)
|
+ (resolvedAppUser == null ? "확인 불가" : resolvedAppUser)
|
||||||
+ ")의 DDS DATA GRANT를 통과했습니다.";
|
+ ")의 DDS DATA GRANT를 통과했습니다.";
|
||||||
|
DdsSqlEvidence evidence = collectEvidence(connection, marker, submittedStatement, timeoutSeconds);
|
||||||
|
evidenceHistory.record(evidence);
|
||||||
return new DdsVectorSearchResult(
|
return new DdsVectorSearchResult(
|
||||||
"token", userLabel, properties.vectorObject(), normalizedQuery, mode,
|
"token", userLabel, properties.vectorObject(), normalizedQuery, mode,
|
||||||
embeddingModel(mode), true, "토큰 기반 DDS 권한으로 검색했습니다.", message,
|
embeddingModel(mode), true, "토큰 기반 DDS 권한으로 검색했습니다.", message,
|
||||||
sessionUser, endUser, null, rows);
|
sessionUser, endUser, null, rows, evidence);
|
||||||
}
|
}
|
||||||
} catch (SQLException exception) {
|
} catch (SQLException exception) {
|
||||||
return failure("token", userLabel, normalizedQuery, mode,
|
return failure("token", userLabel, normalizedQuery, mode,
|
||||||
@@ -251,7 +268,72 @@ public class DdsVectorKnowledgeService {
|
|||||||
) {
|
) {
|
||||||
return new DdsVectorSearchResult(
|
return new DdsVectorSearchResult(
|
||||||
userKey, userLabel, properties.vectorObject(), query, mode, embeddingModel(mode),
|
userKey, userLabel, properties.vectorObject(), query, mode, embeddingModel(mode),
|
||||||
false, title, message, null, null, oracleCode, List.of());
|
false, title, message, null, null, oracleCode, List.of(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DdsSqlEvidence collectEvidence(Connection connection, String marker, String submittedStatement,
|
||||||
|
int timeoutSeconds) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement("""
|
||||||
|
SELECT sql_id, child_number, TO_CHAR(last_active_time, 'YYYY-MM-DD HH24:MI:SS'), executions
|
||||||
|
FROM v$sql WHERE sql_text LIKE ? ORDER BY last_active_time DESC FETCH FIRST 1 ROW ONLY
|
||||||
|
""")) {
|
||||||
|
statement.setQueryTimeout(timeoutSeconds);
|
||||||
|
statement.setString(1, "%" + marker + "%");
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
if (!result.next()) {
|
||||||
|
return DdsSqlEvidence.unavailable(marker, submittedStatement, "실행 SQL ID를 아직 찾지 못했습니다.");
|
||||||
|
}
|
||||||
|
String sqlId = result.getString(1);
|
||||||
|
int child = result.getInt(2);
|
||||||
|
String vpd = readPredicate(connection, "SELECT predicate FROM v$vpd_policy WHERE sql_id = ?", sqlId, timeoutSeconds);
|
||||||
|
String plan = readPlanPredicate(connection, sqlId, child, timeoutSeconds);
|
||||||
|
return new DdsSqlEvidence(true, marker, submittedStatement, sqlId, child, result.getString(3),
|
||||||
|
result.getLong(4), plan, vpd,
|
||||||
|
vpd != null ? "VPD 정책 predicate를 확인했습니다."
|
||||||
|
: "DDS DATA GRANT 경로의 실행 SQL을 확인했습니다.");
|
||||||
|
}
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
return DdsSqlEvidence.unavailable(marker, submittedStatement,
|
||||||
|
"실행 근거 조회 권한이 없습니다. V$SQL, V$VPD_POLICY, DBMS_XPLAN 권한을 확인하세요.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readPredicate(Connection connection, String sql, String sqlId, int timeoutSeconds) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||||
|
statement.setQueryTimeout(timeoutSeconds);
|
||||||
|
statement.setString(1, sqlId);
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
return result.next() ? compact(result.getString(1)) : null;
|
||||||
|
}
|
||||||
|
} catch (SQLException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readPlanPredicate(Connection connection, String sqlId, int child, int timeoutSeconds) {
|
||||||
|
StringBuilder output = new StringBuilder();
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(
|
||||||
|
"SELECT plan_table_output FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(?, ?, 'ALLSTATS LAST PREDICATE'))")) {
|
||||||
|
statement.setQueryTimeout(timeoutSeconds);
|
||||||
|
statement.setString(1, sqlId);
|
||||||
|
statement.setInt(2, child);
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
boolean predicate = false;
|
||||||
|
while (result.next()) {
|
||||||
|
String line = result.getString(1);
|
||||||
|
if (line != null && line.contains("Predicate Information")) predicate = true;
|
||||||
|
if (predicate && line != null && output.length() < 1800) output.append(line.trim()).append('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return compact(output.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String compact(String value) {
|
||||||
|
if (value == null || value.isBlank()) return null;
|
||||||
|
return value.replaceAll("\\s+", " ").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String readSingleValue(Connection connection, String sql, int timeoutSeconds)
|
private String readSingleValue(Connection connection, String sql, int timeoutSeconds)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.cloudhandson.ddsbackoffice.config.DdsProperties;
|
|||||||
import com.cloudhandson.ddsbackoffice.service.DdsProtectionStatusService;
|
import com.cloudhandson.ddsbackoffice.service.DdsProtectionStatusService;
|
||||||
import com.cloudhandson.ddsbackoffice.service.DdsProtectionValidationService;
|
import com.cloudhandson.ddsbackoffice.service.DdsProtectionValidationService;
|
||||||
import com.cloudhandson.ddsbackoffice.service.DdsProtectionSchemaService;
|
import com.cloudhandson.ddsbackoffice.service.DdsProtectionSchemaService;
|
||||||
|
import com.cloudhandson.ddsbackoffice.service.DdsSqlEvidenceHistory;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -18,17 +19,20 @@ public class DdsProtectionController {
|
|||||||
private final DdsProtectionStatusService protectionStatusService;
|
private final DdsProtectionStatusService protectionStatusService;
|
||||||
private final DdsProtectionValidationService validationService;
|
private final DdsProtectionValidationService validationService;
|
||||||
private final DdsProtectionSchemaService schemaService;
|
private final DdsProtectionSchemaService schemaService;
|
||||||
|
private final DdsSqlEvidenceHistory evidenceHistory;
|
||||||
|
|
||||||
public DdsProtectionController(
|
public DdsProtectionController(
|
||||||
DdsProperties properties,
|
DdsProperties properties,
|
||||||
DdsProtectionStatusService protectionStatusService,
|
DdsProtectionStatusService protectionStatusService,
|
||||||
DdsProtectionValidationService validationService,
|
DdsProtectionValidationService validationService,
|
||||||
DdsProtectionSchemaService schemaService
|
DdsProtectionSchemaService schemaService,
|
||||||
|
DdsSqlEvidenceHistory evidenceHistory
|
||||||
) {
|
) {
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
this.protectionStatusService = protectionStatusService;
|
this.protectionStatusService = protectionStatusService;
|
||||||
this.validationService = validationService;
|
this.validationService = validationService;
|
||||||
this.schemaService = schemaService;
|
this.schemaService = schemaService;
|
||||||
|
this.evidenceHistory = evidenceHistory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/dds-protection")
|
@GetMapping("/dds-protection")
|
||||||
@@ -38,6 +42,12 @@ public class DdsProtectionController {
|
|||||||
return "vpd-policies";
|
return "vpd-policies";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/dds-evidence")
|
||||||
|
public String evidence(Model model) {
|
||||||
|
model.addAttribute("evidenceEntries", evidenceHistory.recent());
|
||||||
|
return "dds-evidence";
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/dds-protection/direct")
|
@GetMapping("/dds-protection/direct")
|
||||||
public String directComparison(Model model) {
|
public String directComparison(Model model) {
|
||||||
model.addAttribute("directPaths", protectionStatusService.directComparison());
|
model.addAttribute("directPaths", protectionStatusService.directComparison());
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head th:replace="~{fragments/layout :: head('실행 근거')}"></head>
|
||||||
|
<body>
|
||||||
|
<nav th:replace="~{fragments/layout :: nav}"></nav>
|
||||||
|
<main class="container py-4">
|
||||||
|
<div class="page-title">
|
||||||
|
<h1>실행 근거</h1>
|
||||||
|
<p class="context-summary">최근 지식자료 검색의 SQL ID와 보호 조건</p>
|
||||||
|
<details class="explanation-details"><summary>도움말</summary><p class="mb-0">토큰·바인드 값·검색 본문은 기록하지 않습니다. SQL ID와 실행 계획 predicate는 DB 커서 캐시에서 읽으므로 캐시가 비워지면 조회할 수 없습니다.</p></details>
|
||||||
|
</div>
|
||||||
|
<section class="content-band" th:if="${#lists.isEmpty(evidenceEntries)}">
|
||||||
|
<h2>표시할 실행 내역이 없습니다.</h2><p class="section-subtitle">권한 검색 또는 직접 접근 검증을 실행하면 이 화면에 최근 내역이 표시됩니다.</p>
|
||||||
|
</section>
|
||||||
|
<section class="content-band" th:each="entry : ${evidenceEntries}">
|
||||||
|
<div class="section-heading"><div><h2 th:text="${entry.submittedStatement()}">지식자료 검색</h2><p class="section-subtitle" th:text="${entry.status()}">status</p></div><span class="badge text-bg-secondary" th:text="${entry.available()} ? '실행 확인' : '조회 제한'">status</span></div>
|
||||||
|
<dl class="protection-evidence-grid"><div><dt>SQL ID</dt><dd><code th:text="${entry.sqlId() ?: '-'}">-</code></dd></div><div><dt>마지막 실행</dt><dd th:text="${entry.lastActiveTime() ?: '-'}">-</dd></div><div><dt>실행 횟수</dt><dd th:text="${entry.executions() ?: '-'}">-</dd></div></dl>
|
||||||
|
<details class="technical-details"><summary>적용 조건 보기</summary><p th:if="${entry.planPredicate()}"><strong>실행 계획 predicate</strong><br><code th:text="${entry.planPredicate()}">predicate</code></p><p class="mb-0" th:if="${entry.vpdPredicate()}"><strong>VPD predicate</strong><br><code th:text="${entry.vpdPredicate()}">predicate</code></p></details>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -33,5 +33,18 @@
|
|||||||
<summary>기술 정보 보기</summary>
|
<summary>기술 정보 보기</summary>
|
||||||
<p class="mb-0">SESSION_USER: <code th:text="${searchResult.sessionUser() ?: '-'}">-</code> · ORA_END_USER_CONTEXT: <code th:text="${searchResult.endUser() ?: '-'}">-</code></p>
|
<p class="mb-0">SESSION_USER: <code th:text="${searchResult.sessionUser() ?: '-'}">-</code> · ORA_END_USER_CONTEXT: <code th:text="${searchResult.endUser() ?: '-'}">-</code></p>
|
||||||
</details>
|
</details>
|
||||||
|
<section class="registration-result mt-3" th:if="${searchResult.sqlEvidence() != null}" aria-label="실행 근거">
|
||||||
|
<div><strong>실행 근거</strong><span th:text="${searchResult.sqlEvidence().status()}">실행 근거</span></div>
|
||||||
|
<dl th:if="${searchResult.sqlEvidence().available()}">
|
||||||
|
<div><dt>SQL ID</dt><dd><code th:text="${searchResult.sqlEvidence().sqlId()}">sql id</code></dd></div>
|
||||||
|
<div><dt>실행 시각</dt><dd th:text="${searchResult.sqlEvidence().lastActiveTime()}">time</dd></div>
|
||||||
|
<div><dt>실행 횟수</dt><dd th:text="${searchResult.sqlEvidence().executions()}">0</dd></div>
|
||||||
|
</dl>
|
||||||
|
<details class="technical-details mt-2"><summary>실행 조건 보기</summary>
|
||||||
|
<p>요청: <span th:text="${searchResult.sqlEvidence().submittedStatement()}">statement</span></p>
|
||||||
|
<p th:if="${searchResult.sqlEvidence().planPredicate()}">계획 predicate: <code th:text="${searchResult.sqlEvidence().planPredicate()}">predicate</code></p>
|
||||||
|
<p class="mb-0" th:if="${searchResult.sqlEvidence().vpdPredicate()}">VPD predicate: <code th:text="${searchResult.sqlEvidence().vpdPredicate()}">predicate</code></p>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="submenu-verification" class="rw-submenu" data-submenu-panel="verification" role="navigation" aria-label="접근 검증 메뉴" hidden>
|
<div id="submenu-verification" class="rw-submenu" data-submenu-panel="verification" role="navigation" aria-label="접근 검증 메뉴" hidden>
|
||||||
<a class="nav-link" href="/dds-protection#direct-check-heading">직접 접근 검증</a>
|
<a class="nav-link" href="/dds-protection#direct-check-heading">직접 접근 검증</a>
|
||||||
|
<a class="nav-link" href="/dds-evidence">실행 근거</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user