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 漏洞与补丁管理)。定期扫描发现漏洞、记录漏洞等级修复状态,
* 统一管理补丁更新,自动预警超期未修复高危漏洞。
*
*
核心深水能力:
*
* - {@code POST /{id}/install-patch} 记录补丁安装:更新补丁信息,置「已修复」;
* - {@code POST /{id}/verify} 修复验证:安全工程师确认修复有效,置「已验证」;
* - {@code POST /{id}/accept-risk} 风险接受:高层审批后标记接受(需填接受原因);
* - {@code GET /overdue-alerts} 超期未修复预警(按计划修复日期);
* - {@code GET /summary} 漏洞修复统计看板;
*
*
*
* 写口含安全信息,默认受 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));
}
}