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>
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user