Files
ERP/oa-backend/src/main/java/com/kaidi/oa/web/ItVulnPatchController.java
T
QiufengandClaude Opus 4.8 5e51dc3f56 SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。

== 此快照内容 ==
- 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091)
- 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static)
- 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB)
- 交接文档 go.md + go-code-reference/endpoints/entities/database.md
- 多代理建设脚本 .claude/wf-*.js

== 状态 ==
- 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板)
- 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用
- W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成)
- 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456

== 排除(gitignore, 可再生) ==
node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui)
完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules)

时间戳: 20260615-191517

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:19:15 +08:00

242 lines
11 KiB
Java

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 漏洞与补丁管理)。定期扫描发现漏洞、记录漏洞等级修复状态,
* 统一管理补丁更新,自动预警超期未修复高危漏洞。
*
* <p>核心深水能力:
* <ul>
* <li>{@code POST /{id}/install-patch} 记录补丁安装:更新补丁信息,置「已修复」;</li>
* <li>{@code POST /{id}/verify} 修复验证:安全工程师确认修复有效,置「已验证」;</li>
* <li>{@code POST /{id}/accept-risk} 风险接受:高层审批后标记接受(需填接受原因);</li>
* <li>{@code GET /overdue-alerts} 超期未修复预警(按计划修复日期);</li>
* <li>{@code GET /summary} 漏洞修复统计看板;</li>
* </ul>
* </p>
*
* 写口含安全信息,默认受 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<ItVulnRecord>> 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<ItVulnRecord> 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<ItVulnRecord> 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<ItVulnRecord> 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<Void> 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<ItVulnRecord> 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<ItVulnRecord> 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<ItVulnRecord> 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<List<OverdueVuln>> overdueAlerts() {
LocalDate today = LocalDate.now();
List<OverdueVuln> 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<String, Integer> byLevel) {
}
/** 漏洞修复统计看板:各状态数量与按等级分布。 */
@GetMapping("/summary")
public ApiResp<VulnSummary> summary() {
List<ItVulnRecord> all = repo.findAll();
int pending = 0, fixing = 0, patched = 0, verified = 0, accepted = 0;
Map<String, Integer> 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));
}
}