From c3f9029a28e6e9f64405fa21acba71b847667862 Mon Sep 17 00:00:00 2001 From: llh <756459687@qq.com> Date: Thu, 5 Mar 2026 16:11:39 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8D=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E7=AE=A1=E7=90=86=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ConfigContentConfigController.java | 9 + .../controller/SecurityPostureController.java | 16 + .../param/ContentAuditCorpusQueryParam.java | 20 + .../domain/resp/ContentAuditCorpusResp.java | 27 ++ .../resp/IntelligentRiskAnalysisResp.java | 49 +++ .../resp/SecurityGovernanceLoopResp.java | 37 ++ .../llm/guard/biz/entity/LogAlertEvent.java | 40 ++ .../guard/biz/mapper/LogAlertEventMapper.java | 289 ++++++++++++++ .../biz/service/ContentAuditPageService.java | 71 ++++ .../biz/service/SecurityPostureService.java | 355 ++++++++++++------ 10 files changed, 801 insertions(+), 112 deletions(-) create mode 100644 llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/domain/param/ContentAuditCorpusQueryParam.java create mode 100644 llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/domain/resp/ContentAuditCorpusResp.java create mode 100644 llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/domain/resp/IntelligentRiskAnalysisResp.java create mode 100644 llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/domain/resp/SecurityGovernanceLoopResp.java create mode 100644 llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/entity/LogAlertEvent.java create mode 100644 llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/mapper/LogAlertEventMapper.java diff --git a/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/controller/ConfigContentConfigController.java b/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/controller/ConfigContentConfigController.java index af8b930..4434807 100644 --- a/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/controller/ConfigContentConfigController.java +++ b/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/controller/ConfigContentConfigController.java @@ -2,6 +2,7 @@ package com.llm.guard.biz.controller; import com.llm.guard.biz.domain.param.ContentDlpRuleParam; import com.llm.guard.biz.domain.param.ContentAuditCorpusParam; +import com.llm.guard.biz.domain.param.ContentAuditCorpusQueryParam; import com.llm.guard.biz.domain.param.ContentAuditPageQueryParam; import com.llm.guard.biz.domain.param.ContentAuditPolicyCreateParam; import com.llm.guard.biz.domain.param.ContentAuditRecallConfigParam; @@ -11,6 +12,7 @@ import com.llm.guard.biz.domain.param.ContentMaskPolicyParam; import com.llm.guard.biz.domain.param.ContentPolicyParam; import com.llm.guard.biz.domain.param.StatusUpdateParam; import com.llm.guard.biz.domain.resp.ContentAuditPageResp; +import com.llm.guard.biz.domain.resp.ContentAuditCorpusResp; import com.llm.guard.biz.domain.resp.ContentDlpRuleResp; import com.llm.guard.biz.domain.resp.ContentMaskPolicyResp; import com.llm.guard.biz.domain.resp.ContentPolicyResp; @@ -161,6 +163,13 @@ public class ConfigContentConfigController { return AjaxResult.success(resp); } + @Operation(summary = "词库分页查询") + @GetMapping("/audit/corpus/list") + public AjaxResult listAuditCorpus(@Validated @ParameterObject ContentAuditCorpusQueryParam query) { + PageResp resp = contentAuditPageService.corpusPage(query); + return AjaxResult.success(resp); + } + @Operation(summary = "更新合规检测范围") @PutMapping("/audit/scope") public AjaxResult updateAuditScope(@RequestBody ContentAuditScopeConfigParam param) { diff --git a/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/controller/SecurityPostureController.java b/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/controller/SecurityPostureController.java index 5516884..adf873d 100644 --- a/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/controller/SecurityPostureController.java +++ b/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/controller/SecurityPostureController.java @@ -1,6 +1,8 @@ package com.llm.guard.biz.controller; import com.llm.guard.biz.domain.param.SecurityPostureQueryParam; +import com.llm.guard.biz.domain.resp.IntelligentRiskAnalysisResp; +import com.llm.guard.biz.domain.resp.SecurityGovernanceLoopResp; import com.llm.guard.biz.domain.resp.SecurityPostureResp; import com.llm.guard.biz.service.SecurityPostureService; import com.llm.guard.common.core.web.domain.AjaxResult; @@ -27,4 +29,18 @@ public class SecurityPostureController { SecurityPostureResp resp = securityPostureService.queryPosture(query); return AjaxResult.success(resp); } + + @Operation(summary = "获取智能风险分析中心数据") + @GetMapping("/risk-analysis") + public AjaxResult riskAnalysis(@Validated @ParameterObject SecurityPostureQueryParam query) { + IntelligentRiskAnalysisResp resp = securityPostureService.queryIntelligentRiskAnalysis(query); + return AjaxResult.success(resp); + } + + @Operation(summary = "获取安全治理闭环数据") + @GetMapping("/governance-loop") + public AjaxResult governanceLoop(@Validated @ParameterObject SecurityPostureQueryParam query) { + SecurityGovernanceLoopResp resp = securityPostureService.querySecurityGovernanceLoop(query); + return AjaxResult.success(resp); + } } diff --git a/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/domain/param/ContentAuditCorpusQueryParam.java b/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/domain/param/ContentAuditCorpusQueryParam.java new file mode 100644 index 0000000..857f30c --- /dev/null +++ b/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/domain/param/ContentAuditCorpusQueryParam.java @@ -0,0 +1,20 @@ +package com.llm.guard.biz.domain.param; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "词库分页查询参数") +public class ContentAuditCorpusQueryParam extends PageQueryParam { + + @Schema(description = "作用域编码:GLOBAL/APP/TENANT", example = "GLOBAL") + private String scopeCode = "GLOBAL"; + + @Schema(description = "词条内容(模糊匹配)", example = "内部架构") + private String corpusText; + + @Schema(description = "词条标签(精确匹配)", example = "Confidential") + private String tag; +} diff --git a/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/domain/resp/ContentAuditCorpusResp.java b/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/domain/resp/ContentAuditCorpusResp.java new file mode 100644 index 0000000..fb1c490 --- /dev/null +++ b/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/domain/resp/ContentAuditCorpusResp.java @@ -0,0 +1,27 @@ +package com.llm.guard.biz.domain.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "词库词条分页项") +public class ContentAuditCorpusResp { + + @Schema(description = "词条ID") + private String id; + + @Schema(description = "作用域编码") + private String scopeCode; + + @Schema(description = "词条内容") + private String corpusText; + + @Schema(description = "词条标签") + private String tag; + + @Schema(description = "到期日期") + private String expireDate; + + @Schema(description = "状态") + private String status; +} diff --git a/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/domain/resp/IntelligentRiskAnalysisResp.java b/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/domain/resp/IntelligentRiskAnalysisResp.java new file mode 100644 index 0000000..09c2262 --- /dev/null +++ b/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/domain/resp/IntelligentRiskAnalysisResp.java @@ -0,0 +1,49 @@ +package com.llm.guard.biz.domain.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Schema(description = "智能风险分析中心响应") +public class IntelligentRiskAnalysisResp { + + @Schema(description = "统计周期(小时)") + private Integer periodHours; + + @Schema(description = "周期内活跃资产数") + private Long activeAssetCount; + + @Schema(description = "高危规则统计") + private List highRiskRules = new ArrayList<>(); + + @Schema(description = "风险返回码展示") + private List riskCodeDistribution = new ArrayList<>(); + + @Schema(description = "调用者统计") + private List apiConsumers = new ArrayList<>(); + + @Data + public static class RuleStatItem { + private Integer rankNo; + private String ruleName; + private Long count; + } + + @Data + public static class RiskCodeItem { + private Integer riskCode; + private Long count; + } + + @Data + public static class ApiConsumerItem { + private Integer rankNo; + private String callerId; + private String consumerType; + private Long count; + } +} + diff --git a/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/domain/resp/SecurityGovernanceLoopResp.java b/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/domain/resp/SecurityGovernanceLoopResp.java new file mode 100644 index 0000000..07fbb46 --- /dev/null +++ b/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/domain/resp/SecurityGovernanceLoopResp.java @@ -0,0 +1,37 @@ +package com.llm.guard.biz.domain.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Schema(description = "安全治理闭环响应") +public class SecurityGovernanceLoopResp { + + @Schema(description = "统计周期(小时)") + private Integer periodHours; + + @Schema(description = "治理卡片列表") + private List cards = new ArrayList<>(); + + @Data + public static class GovernanceCard { + @Schema(description = "治理域编码") + private String domainKey; + + @Schema(description = "治理域标题") + private String title; + + @Schema(description = "治理域副标题") + private String subtitle; + + @Schema(description = "影响量") + private Long impact; + + @Schema(description = "成功率(百分比)") + private Double successRate; + } +} + diff --git a/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/entity/LogAlertEvent.java b/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/entity/LogAlertEvent.java new file mode 100644 index 0000000..249ef0f --- /dev/null +++ b/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/entity/LogAlertEvent.java @@ -0,0 +1,40 @@ +package com.llm.guard.biz.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.llm.guard.common.mybatisplus.entity.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +@Data +@EqualsAndHashCode(callSuper = true) +@Accessors(chain = true) +@TableName("d_log_alert_event") +public class LogAlertEvent extends BaseEntity { + + private String eventNo; + private String requestId; + private String traceId; + private String scopeCode; + private String moduleType; + private String eventType; + private String ruleType; + private String ruleId; + private String ruleCode; + private String switchKey; + private String severity; + private String actionTaken; + private String hitMessage; + private String requestPath; + private String requestMethod; + private String sourceIp; + private String callerId; + private Integer isStream; + private String alertStatus; + private LocalDateTime occurredAt; + private LocalDateTime partitionDate; + private Object hitDetailJson; +} + diff --git a/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/mapper/LogAlertEventMapper.java b/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/mapper/LogAlertEventMapper.java new file mode 100644 index 0000000..0a4429d --- /dev/null +++ b/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/mapper/LogAlertEventMapper.java @@ -0,0 +1,289 @@ +package com.llm.guard.biz.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.llm.guard.biz.entity.LogAlertEvent; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Mapper +public interface LogAlertEventMapper extends BaseMapper { + + @Select(""" + + """) + Long countDistinctRequests(@Param("scopeCode") String scopeCode, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + @Select(""" + + """) + Long countDistinctBlockedRequests(@Param("scopeCode") String scopeCode, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + @Select(""" + + """) + Long countMatchEvents(@Param("scopeCode") String scopeCode, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + @Select(""" + + """) + Double avgLatencyMs(@Param("scopeCode") String scopeCode, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + @Select(""" + + """) + List> selectTrendBuckets(@Param("scopeCode") String scopeCode, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + @Select(""" + + """) + List> selectThreatRows(@Param("scopeCode") String scopeCode, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + @Select(""" + + """) + List> selectSourceIpRank(@Param("scopeCode") String scopeCode, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end, + @Param("topN") Integer topN); + + @Select(""" + + """) + List> selectPathRank(@Param("scopeCode") String scopeCode, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end, + @Param("topN") Integer topN); + + @Select(""" + + """) + List> selectHitDetailRows(@Param("scopeCode") String scopeCode, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + @Select(""" + + """) + Long countActiveAssets(@Param("scopeCode") String scopeCode, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + @Select(""" + + """) + List> selectTopRules(@Param("scopeCode") String scopeCode, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end, + @Param("topN") Integer topN, + @Param("onlyHigh") boolean onlyHigh); + + @Select(""" + + """) + List> selectRiskCodeRows(@Param("scopeCode") String scopeCode, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + @Select(""" + + """) + List> selectCallerRank(@Param("scopeCode") String scopeCode, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end, + @Param("topN") Integer topN); + + @Select(""" + + """) + List> selectRejectCodeRows(@Param("scopeCode") String scopeCode); + + @Select(""" + + """) + List> selectGovernanceRows(@Param("scopeCode") String scopeCode, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); +} + diff --git a/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/service/ContentAuditPageService.java b/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/service/ContentAuditPageService.java index a866b6b..5b1c7ab 100644 --- a/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/service/ContentAuditPageService.java +++ b/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/service/ContentAuditPageService.java @@ -3,11 +3,13 @@ package com.llm.guard.biz.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.llm.guard.biz.domain.param.ContentAuditCorpusParam; +import com.llm.guard.biz.domain.param.ContentAuditCorpusQueryParam; import com.llm.guard.biz.domain.param.ContentAuditPageQueryParam; import com.llm.guard.biz.domain.param.ContentAuditPolicyCreateParam; import com.llm.guard.biz.domain.param.ContentAuditRecallConfigParam; import com.llm.guard.biz.domain.param.ContentAuditRejectConfigParam; import com.llm.guard.biz.domain.param.ContentAuditScopeConfigParam; +import com.llm.guard.biz.domain.resp.ContentAuditCorpusResp; import com.llm.guard.biz.domain.resp.ContentAuditPageResp; import com.llm.guard.biz.domain.resp.ContentPolicyResp; import com.llm.guard.biz.domain.resp.PageResp; @@ -43,6 +45,59 @@ public class ContentAuditPageService { return resp; } + public PageResp corpusPage(ContentAuditCorpusQueryParam query) { + ContentAuditCorpusQueryParam q = normalizeCorpusQuery(query); + StringBuilder fromWhere = new StringBuilder(" FROM d_content_corpus WHERE is_deleted = 0 AND scope_code = ?"); + List args = new ArrayList<>(); + args.add(q.getScopeCode()); + if (StringUtils.hasText(q.getCorpusText())) { + fromWhere.append(" AND corpus_text LIKE ?"); + args.add("%" + q.getCorpusText().trim() + "%"); + } + if (StringUtils.hasText(q.getTag())) { + fromWhere.append(" AND tag = ?"); + args.add(q.getTag().trim()); + } + Long total = jdbcTemplate.queryForObject("SELECT COUNT(1)" + fromWhere, args.toArray(), Long.class); + PageResp resp = new PageResp<>(); + Integer pageNum = q.getPageNum(); + resp.setPageSize(q.getPageSize()); + resp.setTotal(total == null ? 0L : total); + long pages = (resp.getTotal() + q.getPageSize() - 1) / q.getPageSize(); + resp.setPages(pages); + if (pages > 0 && pageNum > pages) { + pageNum = (int) pages; + } + resp.setPageNum(pageNum); + if (resp.getTotal() <= 0L) { + resp.setItems(List.of()); + return resp; + } + int offset = (pageNum - 1) * q.getPageSize(); + List pageArgs = new ArrayList<>(args); + pageArgs.add(q.getPageSize()); + pageArgs.add(offset); + List> rows = jdbcTemplate.queryForList( + "SELECT id, scope_code, corpus_text, tag, expire_date, status" + + fromWhere + + " ORDER BY update_time DESC LIMIT ? OFFSET ?", + pageArgs.toArray() + ); + List items = new ArrayList<>(); + for (Map row : rows) { + ContentAuditCorpusResp item = new ContentAuditCorpusResp(); + item.setId(str(row.get("id"))); + item.setScopeCode(str(row.get("scope_code"))); + item.setCorpusText(str(row.get("corpus_text"))); + item.setTag(str(row.get("tag"))); + item.setExpireDate(str(row.get("expire_date"))); + item.setStatus(str(row.get("status"))); + items.add(item); + } + resp.setItems(items); + return resp; + } + public boolean updateScopeConfig(ContentAuditScopeConfigParam param) { if (param == null) { return false; @@ -399,6 +454,22 @@ public class ContentAuditPageService { return q; } + private ContentAuditCorpusQueryParam normalizeCorpusQuery(ContentAuditCorpusQueryParam query) { + ContentAuditCorpusQueryParam q = query == null ? new ContentAuditCorpusQueryParam() : query; + if (!StringUtils.hasText(q.getScopeCode())) { + q.setScopeCode("GLOBAL"); + } else { + q.setScopeCode(q.getScopeCode().trim().toUpperCase(Locale.ROOT)); + } + if (q.getPageNum() == null || q.getPageNum() < 1) { + q.setPageNum(1); + } + if (q.getPageSize() == null || q.getPageSize() < 1) { + q.setPageSize(10); + } + return q; + } + private String defaultScope(String scopeCode) { return StringUtils.hasText(scopeCode) ? scopeCode : "GLOBAL"; } diff --git a/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/service/SecurityPostureService.java b/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/service/SecurityPostureService.java index 91dcb8b..c288d15 100644 --- a/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/service/SecurityPostureService.java +++ b/llm-guard-modules/llm-guard-biz/src/main/java/com/llm/guard/biz/service/SecurityPostureService.java @@ -2,10 +2,12 @@ package com.llm.guard.biz.service; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.llm.guard.biz.domain.resp.IntelligentRiskAnalysisResp; +import com.llm.guard.biz.domain.resp.SecurityGovernanceLoopResp; import com.llm.guard.biz.domain.param.SecurityPostureQueryParam; import com.llm.guard.biz.domain.resp.SecurityPostureResp; +import com.llm.guard.biz.mapper.LogAlertEventMapper; import lombok.AllArgsConstructor; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import java.math.BigDecimal; @@ -27,7 +29,7 @@ public class SecurityPostureService { private static final String METRIC_COUNT = "COUNT"; private static final String METRIC_TOKEN = "TOKEN"; - private final JdbcTemplate jdbcTemplate; + private final LogAlertEventMapper logAlertEventMapper; private final ObjectMapper objectMapper; public SecurityPostureResp queryPosture(SecurityPostureQueryParam query) { @@ -45,6 +47,59 @@ public class SecurityPostureService { return resp; } + public IntelligentRiskAnalysisResp queryIntelligentRiskAnalysis(SecurityPostureQueryParam query) { + SecurityPostureQueryParam q = normalize(query); + LocalDateTime end = LocalDateTime.now(); + LocalDateTime start = end.minusHours(q.getHours()); + + IntelligentRiskAnalysisResp resp = new IntelligentRiskAnalysisResp(); + resp.setPeriodHours(q.getHours()); + resp.setActiveAssetCount(countActiveAssets(q, start, end)); + resp.setHighRiskRules(buildHighRiskRules(q, start, end)); + resp.setRiskCodeDistribution(buildRiskCodeDistribution(q, start, end)); + resp.setApiConsumers(buildApiConsumerRank(q, start, end)); + return resp; + } + + public SecurityGovernanceLoopResp querySecurityGovernanceLoop(SecurityPostureQueryParam query) { + SecurityPostureQueryParam q = normalize(query); + LocalDateTime end = LocalDateTime.now(); + LocalDateTime start = end.minusHours(q.getHours()); + + List> rows = logAlertEventMapper.selectGovernanceRows(q.getScopeCode(), start, end); + + Map statByDomain = new LinkedHashMap<>(); + statByDomain.put("INJECTION", new long[]{0L, 0L}); + statByDomain.put("PROMPT", new long[]{0L, 0L}); + statByDomain.put("DDOS", new long[]{0L, 0L}); + statByDomain.put("PROTOCOL", new long[]{0L, 0L}); + statByDomain.put("VULN", new long[]{0L, 0L}); + + for (Map row : rows) { + String domain = mapGovernanceDomain( + str(row.get("module_type")), + str(row.get("event_type")), + str(row.get("rule_code")), + str(row.get("hit_message")) + ); + long cnt = toLong(row.get("cnt")); + long[] stat = statByDomain.computeIfAbsent(domain, k -> new long[]{0L, 0L}); + stat[0] += cnt; + if ("BLOCK".equalsIgnoreCase(str(row.get("action_taken")))) { + stat[1] += cnt; + } + } + + SecurityGovernanceLoopResp resp = new SecurityGovernanceLoopResp(); + resp.setPeriodHours(q.getHours()); + resp.getCards().add(buildGovernanceCard("INJECTION", "注入攻击防护", "SQL / 命令 / 代码注入", statByDomain.get("INJECTION"))); + resp.getCards().add(buildGovernanceCard("PROMPT", "提示词安全", "诱导越狱 / 角色扮演", statByDomain.get("PROMPT"))); + resp.getCards().add(buildGovernanceCard("DDOS", "DDoS / 滥用", "TOKEN 消耗 / 频率控制", statByDomain.get("DDOS"))); + resp.getCards().add(buildGovernanceCard("PROTOCOL", "协议合规", "HEADER 检查 / 字段篡改", statByDomain.get("PROTOCOL"))); + resp.getCards().add(buildGovernanceCard("VULN", "漏洞与溢出", "XSS / 缓冲区溢出防护", statByDomain.get("VULN"))); + return resp; + } + private SecurityPostureResp.Overview buildOverview(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) { SecurityPostureResp.Overview overview = new SecurityPostureResp.Overview(); @@ -62,17 +117,7 @@ public class SecurityPostureService { private List buildTrend(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) { List trend = new ArrayList<>(); - List> rows = queryForListByScope( - q.getScopeCode(), - "SELECT date_trunc('hour', occurred_at) bucket, COUNT(1) cnt " + - "FROM d_log_alert_event WHERE occurred_at BETWEEN ? AND ? AND is_deleted = 0 " + - "GROUP BY date_trunc('hour', occurred_at) ORDER BY bucket", - "SELECT date_trunc('hour', occurred_at) bucket, COUNT(1) cnt " + - "FROM d_log_alert_event WHERE scope_code = ? AND occurred_at BETWEEN ? AND ? AND is_deleted = 0 " + - "GROUP BY date_trunc('hour', occurred_at) ORDER BY bucket", - start, - end - ); + List> rows = logAlertEventMapper.selectTrendBuckets(q.getScopeCode(), start, end); Map bucketMap = new LinkedHashMap<>(); for (Map row : rows) { LocalDateTime bucket = toDateTime(row.get("bucket")); @@ -95,17 +140,7 @@ public class SecurityPostureService { } private List buildThreatDistribution(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) { - List> rows = queryForListByScope( - q.getScopeCode(), - "SELECT module_type, event_type, rule_code, hit_message, COUNT(1) cnt " + - "FROM d_log_alert_event WHERE occurred_at BETWEEN ? AND ? AND is_deleted = 0 " + - "GROUP BY module_type, event_type, rule_code, hit_message", - "SELECT module_type, event_type, rule_code, hit_message, COUNT(1) cnt " + - "FROM d_log_alert_event WHERE scope_code = ? AND occurred_at BETWEEN ? AND ? AND is_deleted = 0 " + - "GROUP BY module_type, event_type, rule_code, hit_message", - start, - end - ); + List> rows = logAlertEventMapper.selectThreatRows(q.getScopeCode(), start, end); Map dist = new LinkedHashMap<>(); dist.put("注入攻击", 0L); dist.put("提示词注入", 0L); @@ -138,18 +173,7 @@ public class SecurityPostureService { } private List buildSourceIpRank(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) { - List> rows = queryForListByScope( - q.getScopeCode(), - "SELECT source_ip, COUNT(1) cnt, SUM(CASE WHEN UPPER(action_taken) = 'BLOCK' THEN 1 ELSE 0 END) block_cnt " + - "FROM d_log_alert_event WHERE occurred_at BETWEEN ? AND ? AND is_deleted = 0 AND source_ip IS NOT NULL AND source_ip <> '' " + - "GROUP BY source_ip ORDER BY cnt DESC LIMIT ?", - "SELECT source_ip, COUNT(1) cnt, SUM(CASE WHEN UPPER(action_taken) = 'BLOCK' THEN 1 ELSE 0 END) block_cnt " + - "FROM d_log_alert_event WHERE scope_code = ? AND occurred_at BETWEEN ? AND ? AND is_deleted = 0 AND source_ip IS NOT NULL AND source_ip <> '' " + - "GROUP BY source_ip ORDER BY cnt DESC LIMIT ?", - start, - end, - q.getTopN() - ); + List> rows = logAlertEventMapper.selectSourceIpRank(q.getScopeCode(), start, end, q.getTopN()); List list = new ArrayList<>(); for (Map row : rows) { long cnt = toLong(row.get("cnt")); @@ -164,18 +188,7 @@ public class SecurityPostureService { } private List buildInterfaceRank(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) { - List> rows = queryForListByScope( - q.getScopeCode(), - "SELECT request_path, COUNT(1) cnt FROM d_log_alert_event " + - "WHERE occurred_at BETWEEN ? AND ? AND is_deleted = 0 AND request_path IS NOT NULL AND request_path <> '' " + - "GROUP BY request_path ORDER BY cnt DESC LIMIT ?", - "SELECT request_path, COUNT(1) cnt FROM d_log_alert_event " + - "WHERE scope_code = ? AND occurred_at BETWEEN ? AND ? AND is_deleted = 0 AND request_path IS NOT NULL AND request_path <> '' " + - "GROUP BY request_path ORDER BY cnt DESC LIMIT ?", - start, - end, - q.getTopN() - ); + List> rows = logAlertEventMapper.selectPathRank(q.getScopeCode(), start, end, q.getTopN()); List list = new ArrayList<>(); for (Map row : rows) { SecurityPostureResp.PathRankItem item = new SecurityPostureResp.PathRankItem(); @@ -187,13 +200,7 @@ public class SecurityPostureService { } private List buildModelRank(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) { - List> rows = queryForListByScope( - q.getScopeCode(), - "SELECT hit_detail_json FROM d_log_alert_event WHERE occurred_at BETWEEN ? AND ? AND is_deleted = 0 AND hit_detail_json IS NOT NULL", - "SELECT hit_detail_json FROM d_log_alert_event WHERE scope_code = ? AND occurred_at BETWEEN ? AND ? AND is_deleted = 0 AND hit_detail_json IS NOT NULL", - start, - end - ); + List> rows = logAlertEventMapper.selectHitDetailRows(q.getScopeCode(), start, end); Map agg = new LinkedHashMap<>(); for (Map row : rows) { String detail = str(row.get("hit_detail_json")); @@ -222,6 +229,70 @@ public class SecurityPostureService { return list; } + private Long countActiveAssets(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) { + return logAlertEventMapper.countActiveAssets(q.getScopeCode(), start, end); + } + + private List buildHighRiskRules(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) { + List> rows = logAlertEventMapper.selectTopRules(q.getScopeCode(), start, end, q.getTopN(), true); + if (rows.isEmpty()) { + rows = logAlertEventMapper.selectTopRules(q.getScopeCode(), start, end, q.getTopN(), false); + } + + List list = new ArrayList<>(); + int idx = 1; + for (Map row : rows) { + IntelligentRiskAnalysisResp.RuleStatItem item = new IntelligentRiskAnalysisResp.RuleStatItem(); + item.setRankNo(idx++); + item.setRuleName(resolveRuleName(str(row.get("rule_code")), str(row.get("event_type")), str(row.get("hit_message")))); + item.setCount(toLong(row.get("cnt"))); + list.add(item); + } + return list; + } + + private List buildRiskCodeDistribution(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) { + Map rejectCodeByScope = loadRejectCodeByScope(q.getScopeCode()); + List> rows = logAlertEventMapper.selectRiskCodeRows(q.getScopeCode(), start, end); + Map dist = new LinkedHashMap<>(); + for (Map row : rows) { + int code = mapRiskCode( + str(row.get("scope_code")), + str(row.get("module_type")), + str(row.get("event_type")), + str(row.get("action_taken")), + rejectCodeByScope + ); + dist.put(code, dist.getOrDefault(code, 0L) + toLong(row.get("cnt"))); + } + + List list = new ArrayList<>(); + for (Map.Entry entry : dist.entrySet()) { + IntelligentRiskAnalysisResp.RiskCodeItem item = new IntelligentRiskAnalysisResp.RiskCodeItem(); + item.setRiskCode(entry.getKey()); + item.setCount(entry.getValue()); + list.add(item); + } + return list; + } + + private List buildApiConsumerRank(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) { + List> rows = logAlertEventMapper.selectCallerRank(q.getScopeCode(), start, end, q.getTopN()); + + List list = new ArrayList<>(); + int idx = 1; + for (Map row : rows) { + String callerId = str(row.get("caller_id")); + IntelligentRiskAnalysisResp.ApiConsumerItem item = new IntelligentRiskAnalysisResp.ApiConsumerItem(); + item.setRankNo(idx++); + item.setCallerId(callerId); + item.setConsumerType(classifyConsumerType(callerId)); + item.setCount(toLong(row.get("cnt"))); + list.add(item); + } + return list; + } + private long metricValue(long[] stat, String metric) { return METRIC_TOKEN.equals(metric) ? stat[1] : stat[0]; } @@ -235,49 +306,19 @@ public class SecurityPostureService { } private Long countDistinctRequests(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) { - return queryForObjectByScope( - q.getScopeCode(), - Long.class, - "SELECT COUNT(DISTINCT request_id) FROM d_log_alert_event WHERE occurred_at BETWEEN ? AND ? AND is_deleted = 0", - "SELECT COUNT(DISTINCT request_id) FROM d_log_alert_event WHERE scope_code = ? AND occurred_at BETWEEN ? AND ? AND is_deleted = 0", - start, - end - ); + return logAlertEventMapper.countDistinctRequests(q.getScopeCode(), start, end); } private Long countDistinctBlockedRequests(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) { - return queryForObjectByScope( - q.getScopeCode(), - Long.class, - "SELECT COUNT(DISTINCT request_id) FROM d_log_alert_event WHERE occurred_at BETWEEN ? AND ? AND is_deleted = 0 AND UPPER(action_taken) = 'BLOCK'", - "SELECT COUNT(DISTINCT request_id) FROM d_log_alert_event WHERE scope_code = ? AND occurred_at BETWEEN ? AND ? AND is_deleted = 0 AND UPPER(action_taken) = 'BLOCK'", - start, - end - ); + return logAlertEventMapper.countDistinctBlockedRequests(q.getScopeCode(), start, end); } private Long countMatchEvents(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) { - return queryForObjectByScope( - q.getScopeCode(), - Long.class, - "SELECT COUNT(1) FROM d_log_alert_event WHERE occurred_at BETWEEN ? AND ? AND is_deleted = 0", - "SELECT COUNT(1) FROM d_log_alert_event WHERE scope_code = ? AND occurred_at BETWEEN ? AND ? AND is_deleted = 0", - start, - end - ); + return logAlertEventMapper.countMatchEvents(q.getScopeCode(), start, end); } private Long queryAvgLatencyMs(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) { - Double avg = queryForObjectByScope( - q.getScopeCode(), - Double.class, - "SELECT AVG(ABS(EXTRACT(EPOCH FROM (COALESCE(create_time, occurred_at) - occurred_at))) * 1000) " + - "FROM d_log_alert_event WHERE occurred_at BETWEEN ? AND ? AND is_deleted = 0", - "SELECT AVG(ABS(EXTRACT(EPOCH FROM (COALESCE(create_time, occurred_at) - occurred_at))) * 1000) " + - "FROM d_log_alert_event WHERE scope_code = ? AND occurred_at BETWEEN ? AND ? AND is_deleted = 0", - start, - end - ); + Double avg = logAlertEventMapper.avgLatencyMs(q.getScopeCode(), start, end); return avg == null ? 0L : Math.round(avg); } @@ -291,6 +332,105 @@ public class SecurityPostureService { return "一般"; } + private String resolveRuleName(String ruleCode, String eventType, String hitMessage) { + if (ruleCode != null && !ruleCode.isBlank()) { + return ruleCode; + } + if (hitMessage != null && !hitMessage.isBlank()) { + return hitMessage; + } + return eventType == null ? "UNKNOWN_RULE" : eventType; + } + + private int mapRiskCode(String scopeCode, + String moduleType, + String eventType, + String actionTaken, + Map rejectCodeByScope) { + String m = upper(moduleType); + String e = upper(eventType); + String a = upper(actionTaken); + if (!"BLOCK".equals(a)) { + return 500; + } + if ("CONTENT".equals(m)) { + return rejectCodeByScope.getOrDefault(scopeCode, 403); + } + if ("ACL".equals(m) && "ACL_IP_WHITELIST".equals(e)) { + return 401; + } + if ("ACL".equals(m) || "ATTACK".equals(m)) { + return 429; + } + return 500; + } + + private Map loadRejectCodeByScope(String scopeCode) { + List> rows = logAlertEventMapper.selectRejectCodeRows(scopeCode); + Map map = new LinkedHashMap<>(); + for (Map row : rows) { + String key = str(row.get("scope_code")); + Integer code = toInt(row.get("reject_code"), 403); + if (key != null && !key.isBlank()) { + map.put(key, code); + } + } + return map; + } + + private String classifyConsumerType(String callerId) { + String val = callerId == null ? "" : callerId.trim().toLowerCase(Locale.ROOT); + if (val.contains("admin") || val.contains("internal") || val.contains("sys")) { + return "Internal"; + } + if (val.contains("public") || val.contains("frontend") || val.contains("web")) { + return "Public"; + } + if (val.contains("api") || val.contains("service")) { + return "API"; + } + return "External"; + } + + private SecurityGovernanceLoopResp.GovernanceCard buildGovernanceCard(String domainKey, + String title, + String subtitle, + long[] stat) { + long impact = stat == null ? 0L : stat[0]; + long success = stat == null ? 0L : stat[1]; + SecurityGovernanceLoopResp.GovernanceCard card = new SecurityGovernanceLoopResp.GovernanceCard(); + card.setDomainKey(domainKey); + card.setTitle(title); + card.setSubtitle(subtitle); + card.setImpact(impact); + card.setSuccessRate(ratio(success, impact == 0 ? 1 : impact)); + return card; + } + + private String mapGovernanceDomain(String moduleType, String eventType, String ruleCode, String hitMessage) { + String m = upper(moduleType); + String e = upper(eventType); + String r = upper(ruleCode); + String msg = upper(hitMessage); + + if ("ATTACK".equals(m)) { + if (r.contains("PROMPT") || r.contains("JAILBREAK") || msg.contains("提示词") || msg.contains("越狱")) { + return "PROMPT"; + } + return "INJECTION"; + } + if ("ACL".equals(m)) { + if (e.contains("IP_") || r.contains("RATE") || r.contains("TOKEN") || msg.contains("频率") || msg.contains("滥用") || msg.contains("DDOS")) { + return "DDOS"; + } + return "PROTOCOL"; + } + if ("CONTENT".equals(m)) { + return "VULN"; + } + return "PROTOCOL"; + } + private String mapThreatType(String moduleType, String eventType, String ruleCode, String hitMessage) { String m = upper(moduleType); String e = upper(eventType); @@ -314,29 +454,6 @@ public class SecurityPostureService { return "协议漏洞"; } - private T queryForObjectByScope(String scopeCode, Class cls, String noScopeSql, String withScopeSql, Object... params) { - if (scopeCode == null || scopeCode.isBlank()) { - return jdbcTemplate.queryForObject(noScopeSql, cls, params); - } - Object[] args = prepend(scopeCode, params); - return jdbcTemplate.queryForObject(withScopeSql, cls, args); - } - - private List> queryForListByScope(String scopeCode, String noScopeSql, String withScopeSql, Object... params) { - if (scopeCode == null || scopeCode.isBlank()) { - return jdbcTemplate.queryForList(noScopeSql, params); - } - Object[] args = prepend(scopeCode, params); - return jdbcTemplate.queryForList(withScopeSql, args); - } - - private Object[] prepend(Object first, Object[] tail) { - Object[] args = new Object[tail.length + 1]; - args[0] = first; - System.arraycopy(tail, 0, args, 1, tail.length); - return args; - } - private SecurityPostureQueryParam normalize(SecurityPostureQueryParam query) { SecurityPostureQueryParam q = query == null ? new SecurityPostureQueryParam() : query; if (q.getHours() == null || q.getHours() <= 0) { @@ -441,4 +558,18 @@ public class SecurityPostureService { return 0L; } } + + private Integer toInt(Object value, int defaultVal) { + if (value == null) { + return defaultVal; + } + if (value instanceof Number n) { + return n.intValue(); + } + try { + return Integer.parseInt(String.valueOf(value)); + } catch (Exception ignored) { + return defaultVal; + } + } }