Spring Boot实现漏洞补偿控制台
2026/8/25 4:33:57 网站建设 项目流程

安全扫描告警最容易出现的治理断层,是“代码没修,但外层控制已经降低风险”这类状态。

GitHub Code Scanning 新增Mitigateddismissal reason 后,这个状态终于可以和Won't fix分开。但如果企业只在 GitHub 页面里点一下 Mitigated,治理仍然不完整。

真正需要的是一套补偿控制台,把四个对象连起来:

Vulnerability ↓ Compensating Control ↓ Evidence ↓ Expiry / Revalidation

目标不是再做一个漏洞管理平台,而是确保:

漏洞还在的时候, 补偿控制真的有效; 补偿控制失效的时候, 漏洞能重新进入风险队列。

下面直接用 Spring Boot 做一个最小可运行版本。

数据模型先从Alert开始

@Entity@Table(name="security_alert")publicclassSecurityAlertEntity{@IdprivateStringalertId;privateStringrepository;privateStringruleId;@Enumerated(EnumType.STRING)privateSeverityseverity;@Enumerated(EnumType.STRING)privateAlertStatusstatus;privateStringsourceRef;privateInstantdiscoveredAt;privateInstantupdatedAt;@VersionprivatelongrowVersion;}

状态:

publicenumAlertStatus{OPEN,MITIGATION_PROPOSED,MITIGATED,FIXED,REOPENED,ACCEPTED_RISK}

注意:

MITIGATED

必须是独立状态。

不能直接映射成:

CLOSED

因为代码风险仍存在。

补偿控制实体

@Entity@Table(name="compensating_control")publicclassCompensatingControlEntity{@IdprivateStringcontrolId;@Enumerated(EnumType.STRING)privateControlTypetype;privateStringexternalControlRef;privateStringowner;@Enumerated(EnumType.STRING)privateControlStatusstatus;privateInstanteffectiveAt;privateInstantexpiresAt;privateInstantlastVerifiedAt;privateInstantnextReviewAt;privateStringverificationPolicyVersion;@VersionprivatelongrowVersion;}

类型:

publicenumControlType{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")publicclassAlertControlBindingEntity{@EmbeddedIdprivateAlertControlKeyid;privateStringrationale;privateStringapprovedBy;privateInstantapprovedAt;privateInstantinvalidatedAt;}

Key:

@EmbeddablepublicrecordAlertControlKey(StringalertId,StringcontrolId)implementsSerializable{}

Evidence单独存

@Entity@Table(name="control_evidence")publicclassControlEvidenceEntity{@IdprivateStringevidenceId;privateStringcontrolId;@Enumerated(EnumType.STRING)privateEvidenceTypetype;privateStringcontentRef;privateStringcontentHash;privateStringcollectedBy;privateInstantcollectedAt;}

Evidence Type:

publicenumEvidenceType{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

publicrecordProposeMitigationRequest(@NotNullControlTypecontrolType,@NotBlankStringexternalControlRef,@NotBlankStringowner,@FutureInstantexpiresAt,@NotBlankStringrationale,@NotEmptyList<EvidenceRequest>evidence){}

不同严重度限制不同有效期

@ComponentpublicclassMitigationValidityPolicy{publicDurationmaximumValidity(Severityseverity){returnswitch(severity){caseCRITICAL->Duration.ofDays(30);caseHIGH->Duration.ofDays(60);caseMEDIUM->Duration.ofDays(90);caseLOW->Duration.ofDays(180);};}}

申请:

Critical

却填:

expiresAt = 1年后

直接拒绝。

Approval也要按Severity分级

publicrecordApprovalRequirement(booleansecurityApproval,booleanserviceOwnerApproval,booleanriskOwnerApproval){}

规则:

publicApprovalRequirementrequirement(Severityseverity){returnswitch(severity){caseCRITICAL->newApprovalRequirement(true,true,true);caseHIGH->newApprovalRequirement(true,true,false);default->newApprovalRequirement(false,true,false);};}

不要让开发者自己点一下就把 Critical 漏洞设成 Mitigated。

Control Verification是这套系统的核心

存在一个 WAF Rule 不代表:

它真的拦住漏洞

所以每种 Control 都需要 Verifier。

publicinterfaceControlVerifier{booleansupports(ControlTypetype);VerificationResultverify(CompensatingControlEntitycontrol,SecurityAlertEntityalert);}

WAF:

@ComponentpublicclassWafControlVerifierimplementsControlVerifier{@Overridepublicbooleansupports(ControlTypetype){returntype==ControlType.WAF_RULE;}@OverridepublicVerificationResultverify(CompensatingControlEntitycontrol,SecurityAlertEntityalert){// 1. 读取配置快照// 2. 检查规则是否启用// 3. 运行安全测试// 4. 确认请求未到达 OriginreturnVerificationResult.passed("artifact/test-918");}}

生产里 Verifier 可以接:

  • WAF API;
  • Kubernetes API;
  • Network Policy;
  • API Gateway;
  • 测试环境。

Verification Result

publicrecordVerificationResult(VerificationStatusstatus,StringevidenceRef,List<String>details,InstantverifiedAt){publicstaticVerificationResultpassed(StringevidenceRef){returnnewVerificationResult(VerificationStatus.PASSED,evidenceRef,List.of(),Instant.now());}}

只有Verification PASS才能变MITIGATED

@TransactionalpublicvoidactivateMitigation(StringalertId,StringcontrolId){SecurityAlertEntityalert=alertRepository.findById(alertId).orElseThrow();CompensatingControlEntitycontrol=controlRepository.findById(controlId).orElseThrow();VerificationResultresult=verifierRegistry.forType(control.getType()).verify(control,alert);verificationRepository.save(VerificationEntity.from(alertId,controlId,result));if(result.status()!=VerificationStatus.PASSED){thrownewMitigationVerificationException();}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 * * * *")publicvoidscheduleReviews(){List<CompensatingControlEntity>due=controlRepository.findDueForReview(Instant.now());due.forEach(controlReviewQueue::publish);}

查询:

select*fromcompensating_controlwherestatus='ACTIVE'andnext_review_at<=now();

复审失败必须重新打开Alert

@TransactionalpublicvoidmarkControlInvalid(StringcontrolId,Stringreason){CompensatingControlEntitycontrol=controlRepository.lockById(controlId);control.setStatus(ControlStatus.INVALID);List<String>alertIds=bindingRepository.findActiveAlertIds(controlId);alertIds.forEach(alertId->{SecurityAlertEntityalert=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

不要隐含在备注里。

publicenumControlComposition{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模型

publicrecordRiskScore(intlikelihood,intimpact){publicintvalue(){returnlikelihood*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;updatecompensating_control...;updatesecurity_alert...;insertintooutbox_event...;commit;

避免数据库已经 Reopen,但通知消息没发出去。

GitHub同步最好做成Adapter

publicinterfaceSecurityAlertProvider{voiddismissAsMitigated(StringalertId,Stringcomment);voidreopen(StringalertId);}

Control Plane 自己是真实状态。

GitHub 是外部执行端。

不要把所有治理数据只存 GitHub Comment。

同步失败状态

publicenumSyncStatus{PENDING,SYNCED,FAILED}

如果内部已经批准 Mitigation,但 GitHub API 调用失败:

内部状态不能假装同步成功

由 Outbox Worker 重试。

一个审计事件

publicrecordMitigationAuditEvent(StringeventId,StringalertId,StringcontrolId,StringeventType,Stringactor,StringevidenceHash,StringpolicyVersion,InstantoccurredAt){}

事件:

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 新增Mitigatedreason 解决了一个现实问题:代码漏洞和业务风险之间,并不总是“未修=完全暴露”。

但真正生产化以后,Mitigated 必须被看成一个新的治理对象,而不是一个关闭理由。

一套合格的补偿控制台至少应该保证:

有Owner 有Evidence 有Expiry 能自动复验 失效能Reopen 最终仍推动Code Fix

如果只做了第一步:

把告警状态改成Mitigated

那只是把安全债从代码里搬到了一个更不容易被看见的地方。


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询