package com.kaidi.oa.web; import com.kaidi.oa.common.ApiException; import com.kaidi.oa.common.ApiResp; import com.kaidi.oa.common.NotFoundException; import com.kaidi.oa.domain.ItVulnRecord; import com.kaidi.oa.repository.ItVulnRecordRepository; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.time.Instant; import java.time.LocalDate; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * 漏洞与补丁管理(需求 §4 漏洞与补丁管理)。定期扫描发现漏洞、记录漏洞等级修复状态, * 统一管理补丁更新,自动预警超期未修复高危漏洞。 * *

核心深水能力: *

*

* * 写口含安全信息,默认受 default-deny(ADMIN/APPROVER) 保护;读侧登记 SENSITIVE_READ_PREFIXES。 */ @RestController @RequestMapping("/api/oa/it-vuln-patches") public class ItVulnPatchController { private final ItVulnRecordRepository repo; public ItVulnPatchController(ItVulnRecordRepository repo) { this.repo = repo; } // ---------- CRUD ---------- @GetMapping public ApiResp> list(@RequestParam(required = false) String fixStatus, @RequestParam(required = false) String vulnLevel) { if (fixStatus != null && !fixStatus.isBlank() && vulnLevel != null && !vulnLevel.isBlank()) { return ApiResp.ok(repo.findByVulnLevelAndFixStatus(vulnLevel, fixStatus)); } if (fixStatus != null && !fixStatus.isBlank()) return ApiResp.ok(repo.findByFixStatus(fixStatus)); if (vulnLevel != null && !vulnLevel.isBlank()) return ApiResp.ok(repo.findByVulnLevel(vulnLevel)); return ApiResp.ok(repo.findAll()); } @GetMapping("/{id}") public ApiResp get(@PathVariable Long id) { return ApiResp.ok(require(id)); } public record VulnRequest( String vulnId, String title, String vulnLevel, String affectedTarget, String description, String discoveryMethod, String discoveredDate, String planFixDate, String note) { } @PostMapping public ApiResp create(@RequestBody VulnRequest req) { if (req.title() == null || req.title().isBlank()) { throw new ApiException(400, "漏洞标题(title) 不能为空"); } if (req.vulnLevel() == null || req.vulnLevel().isBlank()) { throw new ApiException(400, "漏洞等级(vulnLevel) 不能为空"); } ItVulnRecord v = new ItVulnRecord(); v.setVulnId(req.vulnId()); v.setTitle(req.title()); v.setVulnLevel(req.vulnLevel()); v.setAffectedTarget(req.affectedTarget()); v.setDescription(req.description()); v.setDiscoveryMethod(req.discoveryMethod() == null ? "定期扫描" : req.discoveryMethod()); v.setDiscoveredDate(req.discoveredDate() == null ? LocalDate.now().toString() : req.discoveredDate()); v.setPlanFixDate(req.planFixDate()); v.setNote(req.note()); v.setFixStatus("待修复"); v.setCreatedAt(Instant.now()); return ApiResp.ok(repo.save(v)); } @PatchMapping("/{id}") public ApiResp update(@PathVariable Long id, @RequestBody VulnRequest req) { ItVulnRecord v = require(id); if (req.vulnId() != null) v.setVulnId(req.vulnId()); if (req.title() != null && !req.title().isBlank()) v.setTitle(req.title()); if (req.vulnLevel() != null && !req.vulnLevel().isBlank()) v.setVulnLevel(req.vulnLevel()); if (req.affectedTarget() != null) v.setAffectedTarget(req.affectedTarget()); if (req.description() != null) v.setDescription(req.description()); if (req.discoveryMethod() != null) v.setDiscoveryMethod(req.discoveryMethod()); if (req.discoveredDate() != null) v.setDiscoveredDate(req.discoveredDate()); if (req.planFixDate() != null) v.setPlanFixDate(req.planFixDate()); if (req.note() != null) v.setNote(req.note()); return ApiResp.ok(repo.save(v)); } @DeleteMapping("/{id}") public ApiResp delete(@PathVariable Long id) { if (!repo.existsById(id)) throw new NotFoundException("vuln record not found: " + id); repo.deleteById(id); return ApiResp.ok(null); } // ---------- 状态机动作 ---------- public record PatchInstallRequest(String patchNo, String patchInstalledDate, String patchInstalledBy, String note) { } /** 补丁安装:记录补丁号/安装人/安装日期,置「已修复」待验证。 */ @Transactional @PostMapping("/{id}/install-patch") public ApiResp installPatch(@PathVariable Long id, @RequestBody PatchInstallRequest req) { ItVulnRecord v = require(id); if ("已验证".equals(v.getFixStatus()) || "风险接受".equals(v.getFixStatus())) { throw new ApiException(409, "漏洞已处置(" + v.getFixStatus() + "),无需重复安装"); } v.setPatchNo(req.patchNo()); v.setPatchInstalledDate(req.patchInstalledDate() == null ? LocalDate.now().toString() : req.patchInstalledDate()); v.setPatchInstalledBy(req.patchInstalledBy()); v.setFixStatus("已修复"); if (req.note() != null) v.setNote(req.note()); return ApiResp.ok(repo.save(v)); } public record VerifyRequest(String verifiedBy, String verifiedDate, String note) { } /** 修复验证:安全工程师确认漏洞已有效修复,置「已验证」。 */ @PostMapping("/{id}/verify") public ApiResp verify(@PathVariable Long id, @RequestBody VerifyRequest req) { ItVulnRecord v = require(id); if (!"已修复".equals(v.getFixStatus())) { throw new ApiException(409, "仅「已修复」的漏洞可进行验证,当前:" + v.getFixStatus()); } if (req.verifiedBy() == null || req.verifiedBy().isBlank()) { throw new ApiException(400, "验证人(verifiedBy) 不能为空"); } v.setVerifiedBy(req.verifiedBy()); v.setVerifiedDate(req.verifiedDate() == null ? LocalDate.now().toString() : req.verifiedDate()); v.setFixStatus("已验证"); if (req.note() != null) v.setNote(req.note()); return ApiResp.ok(repo.save(v)); } public record AcceptRiskRequest(String acceptedBy, String riskAcceptReason) { } /** 风险接受:经审批后接受漏洞风险(需填写原因),置「风险接受」。 */ @PostMapping("/{id}/accept-risk") public ApiResp acceptRisk(@PathVariable Long id, @RequestBody AcceptRiskRequest req) { ItVulnRecord v = require(id); if ("已验证".equals(v.getFixStatus())) { throw new ApiException(409, "已验证修复的漏洞无需接受风险"); } if (req.riskAcceptReason() == null || req.riskAcceptReason().isBlank()) { throw new ApiException(400, "风险接受必须填写原因(riskAcceptReason)"); } v.setRiskAcceptReason(req.riskAcceptReason()); v.setFixStatus("风险接受"); return ApiResp.ok(repo.save(v)); } // ---------- 预警与统计 ---------- public record OverdueVuln(Long id, String vulnId, String title, String vulnLevel, String affectedTarget, String planFixDate, long overdueDays) { } /** 超期未修复预警:计划修复日已过且未验证/未接受风险,按超期天数排序。 */ @GetMapping("/overdue-alerts") public ApiResp> overdueAlerts() { LocalDate today = LocalDate.now(); List out = new ArrayList<>(); for (ItVulnRecord v : repo.findAll()) { if ("已验证".equals(v.getFixStatus()) || "风险接受".equals(v.getFixStatus())) continue; if (v.getPlanFixDate() == null || v.getPlanFixDate().isBlank()) continue; try { LocalDate plan = LocalDate.parse(v.getPlanFixDate()); long days = ChronoUnit.DAYS.between(plan, today); if (days > 0) { out.add(new OverdueVuln(v.getId(), v.getVulnId(), v.getTitle(), v.getVulnLevel(), v.getAffectedTarget(), v.getPlanFixDate(), days)); } } catch (Exception ignored) { } } out.sort((a, b) -> Long.compare(b.overdueDays(), a.overdueDays())); return ApiResp.ok(out); } public record VulnSummary(int total, int pending, int fixing, int patched, int verified, int riskAccepted, Map byLevel) { } /** 漏洞修复统计看板:各状态数量与按等级分布。 */ @GetMapping("/summary") public ApiResp summary() { List all = repo.findAll(); int pending = 0, fixing = 0, patched = 0, verified = 0, accepted = 0; Map byLevel = new LinkedHashMap<>(); for (ItVulnRecord v : all) { switch (v.getFixStatus() == null ? "待修复" : v.getFixStatus()) { case "待修复" -> pending++; case "修复中" -> fixing++; case "已修复" -> patched++; case "已验证" -> verified++; case "风险接受" -> accepted++; } if (v.getVulnLevel() != null) byLevel.merge(v.getVulnLevel(), 1, Integer::sum); } return ApiResp.ok(new VulnSummary(all.size(), pending, fixing, patched, verified, accepted, byLevel)); } // ---------- helpers ---------- private ItVulnRecord require(Long id) { return repo.findById(id).orElseThrow(() -> new NotFoundException("vuln record not found: " + id)); } }