Spring Boot实现漏洞补偿控制台

发布时间:2026/8/25 4:34:02
Spring Boot实现漏洞补偿控制台 安全扫描告警最容易出现的治理断层是“代码没修但外层控制已经降低风险”这类状态。GitHub Code Scanning 新增Mitigateddismissal reason 后这个状态终于可以和Wont fix分开。但如果企业只在 GitHub 页面里点一下 Mitigated治理仍然不完整。真正需要的是一套补偿控制台把四个对象连起来Vulnerability ↓ Compensating Control ↓ Evidence ↓ Expiry / Revalidation目标不是再做一个漏洞管理平台而是确保漏洞还在的时候 补偿控制真的有效 补偿控制失效的时候 漏洞能重新进入风险队列。下面直接用 Spring Boot 做一个最小可运行版本。数据模型先从Alert开始EntityTable(namesecurity_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因为代码风险仍存在。补偿控制实体EntityTable(namecompensating_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关联表EntityTable(namealert_control_binding)publicclassAlertControlBindingEntity{EmbeddedIdprivateAlertControlKeyid;privateStringrationale;privateStringapprovedBy;privateInstantapprovedAt;privateInstantinvalidatedAt;}KeyEmbeddablepublicrecordAlertControlKey(StringalertId,StringcontrolId)implementsSerializable{}Evidence单独存EntityTable(namecontrol_evidence)publicclassControlEvidenceEntity{IdprivateStringevidenceId;privateStringcontrolId;Enumerated(EnumType.STRING)privateEvidenceTypetype;privateStringcontentRef;privateStringcontentHash;privateStringcollectedBy;privateInstantcollectedAt;}Evidence TypepublicenumEvidenceType{CONFIG_SNAPSHOT,POLICY_EXPORT,TEST_RESULT,SCREENSHOT,TRACE,CHANGE_TICKET}最重要的是contentHash这样 Evidence 后续被替换可以检测。一条Mitigation申请应该长什么样APIPOST /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 ValidationpublicrecordProposeMitigationRequest(NotNullControlTypecontrolType,NotBlankStringexternalControlRef,NotBlankStringowner,FutureInstantexpiresAt,NotBlankStringrationale,NotEmptyListEvidenceRequestevidence){}不同严重度限制不同有效期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);}WAFComponentpublicclassWafControlVerifierimplementsControlVerifier{Overridepublicbooleansupports(ControlTypetype){returntypeControlType.WAF_RULE;}OverridepublicVerificationResultverify(CompensatingControlEntitycontrol,SecurityAlertEntityalert){// 1. 读取配置快照// 2. 检查规则是否启用// 3. 运行安全测试// 4. 确认请求未到达 OriginreturnVerificationResult.passed(artifact/test-918);}}生产里 Verifier 可以接WAF APIKubernetes APINetwork PolicyAPI Gateway测试环境。Verification ResultpublicrecordVerificationResult(VerificationStatusstatus,StringevidenceRef,ListStringdetails,InstantverifiedAt){publicstaticVerificationResultpassed(StringevidenceRef){returnnewVerificationResult(VerificationStatus.PASSED,evidenceRef,List.of(),Instant.now());}}只有Verification PASS才能变MITIGATEDTransactionalpublicvoidactivateMitigation(StringalertId,StringcontrolId){SecurityAlertEntityalertalertRepository.findById(alertId).orElseThrow();CompensatingControlEntitycontrolcontrolRepository.findById(controlId).orElseThrow();VerificationResultresultverifierRegistry.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()));}定期复审不能靠人工记日历SchedulerScheduled(cron0 0 * * * *)publicvoidscheduleReviews(){ListCompensatingControlEntityduecontrolRepository.findDueForReview(Instant.now());due.forEach(controlReviewQueue::publish);}查询select*fromcompensating_controlwherestatusACTIVEandnext_review_atnow();复审失败必须重新打开AlertTransactionalpublicvoidmarkControlInvalid(StringcontrolId,Stringreason){CompensatingControlEntitycontrolcontrolRepository.lockById(controlId);control.setStatus(ControlStatus.INVALID);ListStringalertIdsbindingRepository.findActiveAlertIds(controlId);alertIds.forEach(alertId-{SecurityAlertEntityalertalertRepository.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例如 RCEImpact 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同步最好做成AdapterpublicinterfaceSecurityAlertProvider{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那只是把安全债从代码里搬到了一个更不容易被看见的地方。