安全扫描告警最容易出现的治理断层,是“代码没修,但外层控制已经降低风险”这类状态。
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 = CriticalWAF 只是让可利用概率下降。
所以 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 写 Outboxbegin;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那只是把安全债从代码里搬到了一个更不容易被看见的地方。