Spring Boot实现漏洞补偿控制台
安全扫描告警最容易出现的治理断层,是“代码没修,但外层控制已经降低风险”这类状态。
GitHub Code Scanning 新增 Mitigated dismissal reason 后,这个状态终于可以和 Won't fix 分开。但如果企业只在 GitHub 页面里点一下 Mitigated,治理仍然不完整。
真正需要的是一套补偿控制台,把四个对象连起来:
Vulnerability
↓
Compensating Control
↓
Evidence
↓
Expiry / Revalidation
目标不是再做一个漏洞管理平台,而是确保:
漏洞还在的时候,
补偿控制真的有效;
补偿控制失效的时候,
漏洞能重新进入风险队列。
下面直接用 Spring Boot 做一个最小可运行版本。
数据模型先从Alert开始
@Entity
@Table(name = "security_alert")
public class SecurityAlertEntity {
@Id
private String alertId;
private String repository;
private String ruleId;
@Enumerated(EnumType.STRING)
private Severity severity;
@Enumerated(EnumType.STRING)
private AlertStatus status;
private String sourceRef;
private Instant discoveredAt;
private Instant updatedAt;
@Version
private long rowVersion;
}
状态:
public enum AlertStatus {
OPEN,
MITIGATION_PROPOSED,
MITIGATED,
FIXED,
REOPENED,
ACCEPTED_RISK
}
注意:
MITIGATED
必须是独立状态。
不能直接映射成:
CLOSED
因为代码风险仍存在。
补偿控制实体
@Entity
@Table(name = "compensating_control")
public class CompensatingControlEntity {
@Id
private String controlId;
@Enumerated(EnumType.STRING)
private ControlType type;
private String externalControlRef;
private String owner;
@Enumerated(EnumType.STRING)
private ControlStatus status;
private Instant effectiveAt;
private Instant expiresAt;
private Instant lastVerifiedAt;
private Instant nextReviewAt;
private String verificationPolicyVersion;
@Version
private long rowVersion;
}
类型:
public enum ControlType {
WAF_RULE,
NETWORK_POLICY,
RATE_LIMIT,
FEATURE_FLAG,
ACCESS_POLICY,
FIREWALL_RULE,
TEMPORARY_ISOLATION
}
为什么Alert和Control要分表
一个补偿控制可能覆盖多个漏洞。
例如:
WAF Rule 8821
可能同时保护:
Alert A
Alert B
Alert C
如果每个 Alert 都复制一份 WAF 信息,很快会出现:
A说Rule v17
B说Rule v18
C根本没更新
所以关系应该是:
Alert
many-to-many
Control
关联表
@Entity
@Table(name = "alert_control_binding")
public class AlertControlBindingEntity {
@EmbeddedId
private AlertControlKey id;
private String rationale;
private String approvedBy;
private Instant approvedAt;
private Instant invalidatedAt;
}
Key:
@Embeddable
public record AlertControlKey(
String alertId,
String controlId) implements Serializable {
}
Evidence单独存
@Entity
@Table(name = "control_evidence")
public class ControlEvidenceEntity {
@Id
private String evidenceId;
private String controlId;
@Enumerated(EnumType.STRING)
private EvidenceType type;
private String contentRef;
private String contentHash;
private String collectedBy;
private Instant collectedAt;
}
Evidence Type:
public enum EvidenceType {
CONFIG_SNAPSHOT,
POLICY_EXPORT,
TEST_RESULT,
SCREENSHOT,
TRACE,
CHANGE_TICKET
}
最重要的是:
contentHash
这样 Evidence 后续被替换,可以检测。
一条Mitigation申请应该长什么样
API:
POST /api/alerts/{alertId}/mitigations
请求:
{
"controlType": "WAF_RULE",
"externalControlRef": "waf/rule-8821",
"owner": "platform-security",
"expiresAt": "2026-11-20T00:00:00Z",
"rationale": "Block the vulnerable request pattern before origin",
"evidence": [
{
"type": "TEST_RESULT",
"contentRef": "artifact/security-test-918"
}
]
}
这里有三个字段不能允许为空:
owner
expiresAt
evidence
没有它们就不允许进入 Mitigated。
Bean Validation
public record ProposeMitigationRequest(
@NotNull
ControlType controlType,
@NotBlank
String externalControlRef,
@NotBlank
String owner,
@Future
Instant expiresAt,
@NotBlank
String rationale,
@NotEmpty
List evidence) {
}
不同严重度限制不同有效期
@Component
public class MitigationValidityPolicy {
public Duration maximumValidity(
Severity severity) {
return switch (severity) {
case CRITICAL ->
Duration.ofDays(30);
case HIGH ->
Duration.ofDays(60);
case MEDIUM ->
Duration.ofDays(90);
case LOW ->
Duration.ofDays(180);
};
}
}
申请:
Critical
却填:
expiresAt = 1年后
直接拒绝。
Approval也要按Severity分级
public record ApprovalRequirement(
boolean securityApproval,
boolean serviceOwnerApproval,
boolean riskOwnerApproval) {
}
规则:
public ApprovalRequirement requirement(
Severity severity) {
return switch (severity) {
case CRITICAL ->
new ApprovalRequirement(
true,
true,
true);
case HIGH ->
new ApprovalRequirement(
true,
true,
false);
default ->
new ApprovalRequirement(
false,
true,
false);
};
}
不要让开发者自己点一下就把 Critical 漏洞设成 Mitigated。
Control Verification是这套系统的核心
存在一个 WAF Rule 不代表:
它真的拦住漏洞
所以每种 Control 都需要 Verifier。
public interface ControlVerifier {
boolean supports(
ControlType type);
VerificationResult verify(
CompensatingControlEntity control,
SecurityAlertEntity alert);
}
WAF:
@Component
public class WafControlVerifier
implements ControlVerifier {
@Override
public boolean supports(
ControlType type) {
return type == ControlType.WAF_RULE;
}
@Override
public VerificationResult verify(
CompensatingControlEntity control,
SecurityAlertEntity alert) {
// 1. 读取配置快照
// 2. 检查规则是否启用
// 3. 运行安全测试
// 4. 确认请求未到达 Origin
return VerificationResult.passed(
"artifact/test-918");
}
}
生产里 Verifier 可以接:
- WAF API;
- Kubernetes API;
- Network Policy;
- API Gateway;
- 测试环境。
Verification Result
public record VerificationResult(
VerificationStatus status,
String evidenceRef,
List details,
Instant verifiedAt) {
public static VerificationResult passed(
String evidenceRef) {
return new VerificationResult(
VerificationStatus.PASSED,
evidenceRef,
List.of(),
Instant.now());
}
}
只有Verification PASS才能变MITIGATED
@Transactional
public void activateMitigation(
String alertId,
String controlId) {
SecurityAlertEntity alert =
alertRepository.findById(alertId)
.orElseThrow();
CompensatingControlEntity control =
controlRepository.findById(controlId)
.orElseThrow();
VerificationResult result =
verifierRegistry
.forType(control.getType())
.verify(control, alert);
verificationRepository.save(
VerificationEntity.from(
alertId,
controlId,
result));
if (result.status()
!= VerificationStatus.PASSED) {
throw new MitigationVerificationException();
}
alert.setStatus(
AlertStatus.MITIGATED);
control.setStatus(
ControlStatus.ACTIVE);
control.setLastVerifiedAt(
result.verifiedAt());
control.setNextReviewAt(
reviewPolicy.nextReview(
alert.getSeverity(),
result.verifiedAt()));
}
定期复审不能靠人工记日历
Scheduler:
@Scheduled(cron = "0 0 * * * *")
public void scheduleReviews() {
List due =
controlRepository
.findDueForReview(
Instant.now());
due.forEach(
controlReviewQueue::publish);
}
查询:
select *
from compensating_control
where status = 'ACTIVE'
and next_review_at alertIds =
bindingRepository
.findActiveAlertIds(
controlId);
alertIds.forEach(alertId -> {
SecurityAlertEntity alert =
alertRepository
.lockById(alertId);
if (alert.getStatus()
== AlertStatus.MITIGATED) {
alert.setStatus(
AlertStatus.REOPENED);
}
});
outboxRepository.save(
SecurityEvent
.controlInvalidated(
controlId,
alertIds,
reason));
}
这是整套系统最重要的逻辑。
如果没有:
Control Invalid
→ Alert Reopen
所谓补偿控制治理只完成了一半。
多个Control怎么办
一个漏洞可能同时依赖:
WAF
+
Network Policy
要明确关系是:
AND
还是:
OR
例如:
两个都必须存在
才能缓解:
AND
任一存在就够:
OR
不要隐含在备注里。
public enum ControlComposition {
ALL_REQUIRED,
ANY_SUFFICIENT
}
风险重新计算
补偿控制可以降低:
Likelihood
但不一定改变:
Impact
例如 RCE:
Impact = Critical
WAF 只是让可利用概率下降。
所以 Dashboard 可以展示:
Inherent Risk:
Critical
Residual Risk:
Medium
Mitigation:
WAF Rule 8821
不要直接把 Severity 改成 Medium,丢失原始风险。
一个Residual Risk模型
public record RiskScore(
int likelihood,
int impact) {
public int value() {
return likelihood * impact;
}
}
Control 只修改:
Residual Likelihood
原始 Inherent Risk 永久保留。
Dashboard最少显示这些列
Alert
Severity
Repository
Control
Owner
Last Verified
Next Review
Expiry
Age
Residual Risk
特别是:
Age
一眼看出哪些 Mitigation 已经长期存在。
告警
14d before expiry
7d before expiry
1d before expiry
expired
verification failed
control changed
owner missing
如果 WAF 配置发生变更,也应该主动触发重新验证。
可以让配置系统发送:
ControlChanged
事件。
Control Hash
例如保存:
WAF Policy Hash
每次检查:
current_hash != verified_hash
说明 Evidence 已经陈旧。
立即:
REVIEW_REQUIRED
不要等下个季度复审。
Outbox保证状态变更不会漏通知
事务里同时:
更新 Control
更新 Alert
写 Outbox
begin;
update compensating_control ...;
update security_alert ...;
insert into outbox_event ...;
commit;
避免数据库已经 Reopen,但通知消息没发出去。
GitHub同步最好做成Adapter
public interface SecurityAlertProvider {
void dismissAsMitigated(
String alertId,
String comment);
void reopen(
String alertId);
}
Control Plane 自己是真实状态。
GitHub 是外部执行端。
不要把所有治理数据只存 GitHub Comment。
同步失败状态
public enum SyncStatus {
PENDING,
SYNCED,
FAILED
}
如果内部已经批准 Mitigation,但 GitHub API 调用失败:
内部状态不能假装同步成功
由 Outbox Worker 重试。
一个审计事件
public record MitigationAuditEvent(
String eventId,
String alertId,
String controlId,
String eventType,
String actor,
String evidenceHash,
String policyVersion,
Instant occurredAt) {
}
事件:
PROPOSED
APPROVED
VERIFIED
ACTIVATED
REVERIFIED
EXPIRED
INVALIDATED
REOPENED
FIXED
以后可以完整还原。
生产指标
security_mitigation_active_total
security_mitigation_expiring_total
security_mitigation_verification_failed_total
security_alert_reopened_total
security_mitigation_age_days
再加:
linked_alert_count / control
找控制集中度。
单元测试
至少:
Critical超过30天有效期 → 拒绝
没有Evidence → 拒绝
Verification失败 → 不能Mitigated
Control失效 → Alert Reopen
Control过期 → Alert Reopen
多个AND Control少一个 → Reopen
多个OR Control仍有一个有效 → 保持
配置Hash变化 → Review Required
并发测试也要做
两个 Reviewer 同时批准。
两个 Scheduler 同时处理 Expiry。
需要:
Optimistic Lock
或
SELECT FOR UPDATE
避免状态跳跃。
最终状态不是Mitigated,而是Fixed
补偿控制台应该一直推动永久修复。
可以为 Mitigated Alert 保存:
permanent_fix_ticket
例如:
{
"ticket": "SEC-1842",
"target_release": "2026.09"
}
Dashboard 里如果:
Mitigated
+
没有 Permanent Fix Plan
单独标红。
GitHub 新增 Mitigated reason 解决了一个现实问题:代码漏洞和业务风险之间,并不总是“未修=完全暴露”。
但真正生产化以后,Mitigated 必须被看成一个新的治理对象,而不是一个关闭理由。
一套合格的补偿控制台至少应该保证:
有Owner
有Evidence
有Expiry
能自动复验
失效能Reopen
最终仍推动Code Fix
如果只做了第一步:
把告警状态改成Mitigated
那只是把安全债从代码里搬到了一个更不容易被看见的地方。
更多企业级 AI 应用、Agent、RAG 与模型工程化内容,我会继续整理在 智元界:
https://www.zyentor.com/