fix:修复内容管理的

This commit is contained in:
llh 2026-03-05 16:11:39 +08:00
parent d97d513598
commit c3f9029a28
10 changed files with 801 additions and 112 deletions

View File

@ -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<ContentAuditCorpusResp> resp = contentAuditPageService.corpusPage(query);
return AjaxResult.success(resp);
}
@Operation(summary = "更新合规检测范围")
@PutMapping("/audit/scope")
public AjaxResult updateAuditScope(@RequestBody ContentAuditScopeConfigParam param) {

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<RuleStatItem> highRiskRules = new ArrayList<>();
@Schema(description = "风险返回码展示")
private List<RiskCodeItem> riskCodeDistribution = new ArrayList<>();
@Schema(description = "调用者统计")
private List<ApiConsumerItem> 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;
}
}

View File

@ -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<GovernanceCard> 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;
}
}

View File

@ -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;
}

View File

@ -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<LogAlertEvent> {
@Select("""
<script>
SELECT COUNT(DISTINCT request_id)
FROM d_log_alert_event
WHERE is_deleted = 0
AND occurred_at BETWEEN #{start} AND #{end}
<if test="scopeCode != null and scopeCode != ''">
AND scope_code = #{scopeCode}
</if>
</script>
""")
Long countDistinctRequests(@Param("scopeCode") String scopeCode,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
@Select("""
<script>
SELECT COUNT(DISTINCT request_id)
FROM d_log_alert_event
WHERE is_deleted = 0
AND occurred_at BETWEEN #{start} AND #{end}
AND UPPER(action_taken) = 'BLOCK'
<if test="scopeCode != null and scopeCode != ''">
AND scope_code = #{scopeCode}
</if>
</script>
""")
Long countDistinctBlockedRequests(@Param("scopeCode") String scopeCode,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
@Select("""
<script>
SELECT COUNT(1)
FROM d_log_alert_event
WHERE is_deleted = 0
AND occurred_at BETWEEN #{start} AND #{end}
<if test="scopeCode != null and scopeCode != ''">
AND scope_code = #{scopeCode}
</if>
</script>
""")
Long countMatchEvents(@Param("scopeCode") String scopeCode,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
@Select("""
<script>
SELECT AVG(ABS(EXTRACT(EPOCH FROM (COALESCE(create_time, occurred_at) - occurred_at))) * 1000)
FROM d_log_alert_event
WHERE is_deleted = 0
AND occurred_at BETWEEN #{start} AND #{end}
<if test="scopeCode != null and scopeCode != ''">
AND scope_code = #{scopeCode}
</if>
</script>
""")
Double avgLatencyMs(@Param("scopeCode") String scopeCode,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
@Select("""
<script>
SELECT date_trunc('hour', occurred_at) bucket, COUNT(1) cnt
FROM d_log_alert_event
WHERE is_deleted = 0
AND occurred_at BETWEEN #{start} AND #{end}
<if test="scopeCode != null and scopeCode != ''">
AND scope_code = #{scopeCode}
</if>
GROUP BY date_trunc('hour', occurred_at)
ORDER BY bucket
</script>
""")
List<Map<String, Object>> selectTrendBuckets(@Param("scopeCode") String scopeCode,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
@Select("""
<script>
SELECT module_type, event_type, rule_code, hit_message, COUNT(1) cnt
FROM d_log_alert_event
WHERE is_deleted = 0
AND occurred_at BETWEEN #{start} AND #{end}
<if test="scopeCode != null and scopeCode != ''">
AND scope_code = #{scopeCode}
</if>
GROUP BY module_type, event_type, rule_code, hit_message
</script>
""")
List<Map<String, Object>> selectThreatRows(@Param("scopeCode") String scopeCode,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
@Select("""
<script>
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 is_deleted = 0
AND occurred_at BETWEEN #{start} AND #{end}
AND source_ip IS NOT NULL AND source_ip &lt;&gt; ''
<if test="scopeCode != null and scopeCode != ''">
AND scope_code = #{scopeCode}
</if>
GROUP BY source_ip
ORDER BY cnt DESC
LIMIT #{topN}
</script>
""")
List<Map<String, Object>> selectSourceIpRank(@Param("scopeCode") String scopeCode,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end,
@Param("topN") Integer topN);
@Select("""
<script>
SELECT request_path, COUNT(1) cnt
FROM d_log_alert_event
WHERE is_deleted = 0
AND occurred_at BETWEEN #{start} AND #{end}
AND request_path IS NOT NULL AND request_path &lt;&gt; ''
<if test="scopeCode != null and scopeCode != ''">
AND scope_code = #{scopeCode}
</if>
GROUP BY request_path
ORDER BY cnt DESC
LIMIT #{topN}
</script>
""")
List<Map<String, Object>> selectPathRank(@Param("scopeCode") String scopeCode,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end,
@Param("topN") Integer topN);
@Select("""
<script>
SELECT hit_detail_json
FROM d_log_alert_event
WHERE is_deleted = 0
AND hit_detail_json IS NOT NULL
AND occurred_at BETWEEN #{start} AND #{end}
<if test="scopeCode != null and scopeCode != ''">
AND scope_code = #{scopeCode}
</if>
</script>
""")
List<Map<String, Object>> selectHitDetailRows(@Param("scopeCode") String scopeCode,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
@Select("""
<script>
SELECT COUNT(DISTINCT asset_key) FROM (
SELECT caller_id AS asset_key
FROM d_log_alert_event
WHERE is_deleted = 0
AND caller_id IS NOT NULL AND caller_id &lt;&gt; ''
AND occurred_at BETWEEN #{start} AND #{end}
<if test="scopeCode != null and scopeCode != ''">
AND scope_code = #{scopeCode}
</if>
UNION ALL
SELECT source_ip AS asset_key
FROM d_log_alert_event
WHERE is_deleted = 0
AND source_ip IS NOT NULL AND source_ip &lt;&gt; ''
AND occurred_at BETWEEN #{start} AND #{end}
<if test="scopeCode != null and scopeCode != ''">
AND scope_code = #{scopeCode}
</if>
UNION ALL
SELECT request_path AS asset_key
FROM d_log_alert_event
WHERE is_deleted = 0
AND request_path IS NOT NULL AND request_path &lt;&gt; ''
AND occurred_at BETWEEN #{start} AND #{end}
<if test="scopeCode != null and scopeCode != ''">
AND scope_code = #{scopeCode}
</if>
) t
</script>
""")
Long countActiveAssets(@Param("scopeCode") String scopeCode,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
@Select("""
<script>
SELECT rule_code, event_type, hit_message, COUNT(1) cnt
FROM d_log_alert_event
WHERE is_deleted = 0
AND occurred_at BETWEEN #{start} AND #{end}
<if test="scopeCode != null and scopeCode != ''">
AND scope_code = #{scopeCode}
</if>
<if test="onlyHigh">
AND UPPER(COALESCE(severity, '')) IN ('HIGH','CRITICAL')
</if>
GROUP BY rule_code, event_type, hit_message
ORDER BY cnt DESC
LIMIT #{topN}
</script>
""")
List<Map<String, Object>> selectTopRules(@Param("scopeCode") String scopeCode,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end,
@Param("topN") Integer topN,
@Param("onlyHigh") boolean onlyHigh);
@Select("""
<script>
SELECT scope_code, module_type, event_type, action_taken, COUNT(1) cnt
FROM d_log_alert_event
WHERE is_deleted = 0
AND occurred_at BETWEEN #{start} AND #{end}
<if test="scopeCode != null and scopeCode != ''">
AND scope_code = #{scopeCode}
</if>
GROUP BY scope_code, module_type, event_type, action_taken
</script>
""")
List<Map<String, Object>> selectRiskCodeRows(@Param("scopeCode") String scopeCode,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
@Select("""
<script>
SELECT caller_id, COUNT(1) cnt
FROM d_log_alert_event
WHERE is_deleted = 0
AND caller_id IS NOT NULL AND caller_id &lt;&gt; ''
AND occurred_at BETWEEN #{start} AND #{end}
<if test="scopeCode != null and scopeCode != ''">
AND scope_code = #{scopeCode}
</if>
GROUP BY caller_id
ORDER BY cnt DESC
LIMIT #{topN}
</script>
""")
List<Map<String, Object>> selectCallerRank(@Param("scopeCode") String scopeCode,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end,
@Param("topN") Integer topN);
@Select("""
<script>
SELECT scope_code, reject_code
FROM d_content_audit_setting
WHERE is_deleted = 0
<if test="scopeCode != null and scopeCode != ''">
AND scope_code = #{scopeCode}
</if>
</script>
""")
List<Map<String, Object>> selectRejectCodeRows(@Param("scopeCode") String scopeCode);
@Select("""
<script>
SELECT module_type, event_type, rule_code, hit_message, action_taken, COUNT(1) cnt
FROM d_log_alert_event
WHERE is_deleted = 0
AND occurred_at BETWEEN #{start} AND #{end}
<if test="scopeCode != null and scopeCode != ''">
AND scope_code = #{scopeCode}
</if>
GROUP BY module_type, event_type, rule_code, hit_message, action_taken
</script>
""")
List<Map<String, Object>> selectGovernanceRows(@Param("scopeCode") String scopeCode,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
}

View File

@ -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<ContentAuditCorpusResp> corpusPage(ContentAuditCorpusQueryParam query) {
ContentAuditCorpusQueryParam q = normalizeCorpusQuery(query);
StringBuilder fromWhere = new StringBuilder(" FROM d_content_corpus WHERE is_deleted = 0 AND scope_code = ?");
List<Object> 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<ContentAuditCorpusResp> 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<Object> pageArgs = new ArrayList<>(args);
pageArgs.add(q.getPageSize());
pageArgs.add(offset);
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
"SELECT id, scope_code, corpus_text, tag, expire_date, status"
+ fromWhere
+ " ORDER BY update_time DESC LIMIT ? OFFSET ?",
pageArgs.toArray()
);
List<ContentAuditCorpusResp> items = new ArrayList<>();
for (Map<String, Object> 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";
}

View File

@ -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<Map<String, Object>> rows = logAlertEventMapper.selectGovernanceRows(q.getScopeCode(), start, end);
Map<String, long[]> 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<String, Object> 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<SecurityPostureResp.TrendPoint> buildTrend(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) {
List<SecurityPostureResp.TrendPoint> trend = new ArrayList<>();
List<Map<String, Object>> 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<Map<String, Object>> rows = logAlertEventMapper.selectTrendBuckets(q.getScopeCode(), start, end);
Map<String, Long> bucketMap = new LinkedHashMap<>();
for (Map<String, Object> row : rows) {
LocalDateTime bucket = toDateTime(row.get("bucket"));
@ -95,17 +140,7 @@ public class SecurityPostureService {
}
private List<SecurityPostureResp.ThreatSlice> buildThreatDistribution(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) {
List<Map<String, Object>> 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<Map<String, Object>> rows = logAlertEventMapper.selectThreatRows(q.getScopeCode(), start, end);
Map<String, Long> dist = new LinkedHashMap<>();
dist.put("注入攻击", 0L);
dist.put("提示词注入", 0L);
@ -138,18 +173,7 @@ public class SecurityPostureService {
}
private List<SecurityPostureResp.IpRankItem> buildSourceIpRank(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) {
List<Map<String, Object>> 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<Map<String, Object>> rows = logAlertEventMapper.selectSourceIpRank(q.getScopeCode(), start, end, q.getTopN());
List<SecurityPostureResp.IpRankItem> list = new ArrayList<>();
for (Map<String, Object> row : rows) {
long cnt = toLong(row.get("cnt"));
@ -164,18 +188,7 @@ public class SecurityPostureService {
}
private List<SecurityPostureResp.PathRankItem> buildInterfaceRank(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) {
List<Map<String, Object>> 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<Map<String, Object>> rows = logAlertEventMapper.selectPathRank(q.getScopeCode(), start, end, q.getTopN());
List<SecurityPostureResp.PathRankItem> list = new ArrayList<>();
for (Map<String, Object> row : rows) {
SecurityPostureResp.PathRankItem item = new SecurityPostureResp.PathRankItem();
@ -187,13 +200,7 @@ public class SecurityPostureService {
}
private List<SecurityPostureResp.ModelRankItem> buildModelRank(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) {
List<Map<String, Object>> 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<Map<String, Object>> rows = logAlertEventMapper.selectHitDetailRows(q.getScopeCode(), start, end);
Map<String, long[]> agg = new LinkedHashMap<>();
for (Map<String, Object> 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<IntelligentRiskAnalysisResp.RuleStatItem> buildHighRiskRules(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) {
List<Map<String, Object>> rows = logAlertEventMapper.selectTopRules(q.getScopeCode(), start, end, q.getTopN(), true);
if (rows.isEmpty()) {
rows = logAlertEventMapper.selectTopRules(q.getScopeCode(), start, end, q.getTopN(), false);
}
List<IntelligentRiskAnalysisResp.RuleStatItem> list = new ArrayList<>();
int idx = 1;
for (Map<String, Object> 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<IntelligentRiskAnalysisResp.RiskCodeItem> buildRiskCodeDistribution(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) {
Map<String, Integer> rejectCodeByScope = loadRejectCodeByScope(q.getScopeCode());
List<Map<String, Object>> rows = logAlertEventMapper.selectRiskCodeRows(q.getScopeCode(), start, end);
Map<Integer, Long> dist = new LinkedHashMap<>();
for (Map<String, Object> 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<IntelligentRiskAnalysisResp.RiskCodeItem> list = new ArrayList<>();
for (Map.Entry<Integer, Long> entry : dist.entrySet()) {
IntelligentRiskAnalysisResp.RiskCodeItem item = new IntelligentRiskAnalysisResp.RiskCodeItem();
item.setRiskCode(entry.getKey());
item.setCount(entry.getValue());
list.add(item);
}
return list;
}
private List<IntelligentRiskAnalysisResp.ApiConsumerItem> buildApiConsumerRank(SecurityPostureQueryParam q, LocalDateTime start, LocalDateTime end) {
List<Map<String, Object>> rows = logAlertEventMapper.selectCallerRank(q.getScopeCode(), start, end, q.getTopN());
List<IntelligentRiskAnalysisResp.ApiConsumerItem> list = new ArrayList<>();
int idx = 1;
for (Map<String, Object> 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<String, Integer> 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<String, Integer> loadRejectCodeByScope(String scopeCode) {
List<Map<String, Object>> rows = logAlertEventMapper.selectRejectCodeRows(scopeCode);
Map<String, Integer> map = new LinkedHashMap<>();
for (Map<String, Object> 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> T queryForObjectByScope(String scopeCode, Class<T> 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<Map<String, Object>> 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;
}
}
}