AuditYote 是一款全栈 GRC Web 应用,帮助安全团队将安全发现项映射到主流合规框架的控制项,并通过角色分离的审查签署流程和风险评分生成可审计的合规报告。
# AuditYote
一个用于记录安全发现并将其映射到合规控制项的 Web 应用。它为安全团队提供了一个统一的平台,用于记录漏洞、将每个漏洞与其影响的控制项(跨 ISO/IEC 27001、OWASP Top 10 和 NIST CSF)关联起来、推动发现项经过审查和签署流程,并生成作为治理工作基础的覆盖率和审计报告。
在线演示:https://audityote.pasin.dev
大多数团队通常在电子表格中跟踪这些内容。AuditYote 用一个多用户系统取代了电子表格,该系统保留审计追踪、强制执行审批权限、评估风险得分,并展示控制项的缺口所在。这是一个独立完成的、为期四周的大学系统软件构建课程毕业设计项目,整个应用程序已部署并通过 HTTPS 运行。
## 演示访问
该在线应用开放探索,无需注册。在 [audityote.pasin.dev](https://audityote.pasin.dev) 使用以下演示账号之一登录。这些是演示实例上的临时登录凭据,并非真实凭据。
| 角色 | 邮箱 | 密码 |
|----------|-----------------------------------|-----------------|
| Analyst | `analystdemo@audityote.pasin.dev` | `AuditYoteDemo` |
| Reviewer | `reviewerdemo@audityote.pasin.dev` | `AuditYoteDemo` |
最值得尝试的部分是职责分离,这在服务器端得到了强制执行。以 Analyst 身份登录并提交一个发现项以待审查,然后以 Reviewer 身份登录来批准或退回它。Analyst 无法批准自己的工作,并且 API 会直接拒绝该尝试,而不是仅仅隐藏按钮。Admin 角色也存在,但未在此处公开。
## 功能简介
AuditYote 具有三种角色,您能执行的操作取决于您的角色。
每个已登录的用户都会进入一个发现项仪表板,其中包含过滤栏(状态、严重性、框架和自由文本搜索),位于可排序表格之上。他们可以浏览控制项目录、检查每个框架的覆盖率以查看哪些控制项已覆盖以及哪些存在缺口、阅读风险态势,并导出 CSV 或 PDF 报告。账号设置允许他们更改显示名称和密码。
The findings dashboard: severity, CVSS, a CVSS-based or severity-derived risk score, workflow status, and the controls each finding maps to, all behind a filter bar.
Analyst 负责创建和编辑发现项。每个发现项都会获得一个可读的引用编号,例如 `CM-2026-0001`,并且当它具有 CVSS 分数时,其严重性会根据 CVSS 区间自动设定。Analyst 通过可搜索的选择器(可选择从 AI 建议的控制项开始)将发现项映射到一个或多个控制项,然后驱动其通过工作流:提交审查、退回后重新提交、标记为已修复或重新打开。当 Reviewer 退回发现项时,Analyst 会在通知下拉菜单和仪表板徽章中看到它,以及 Reviewer 的评论。
Reviewer 从按最旧优先排序的审查队列开始工作。他们可以批准发现项或将其退回以进行修改,并且退回必须附带评论。Reviewer 不能批准自己的发现项,该规则存在于服务器端,而不是界面中。已批准的发现项也可以被标记为已接受风险。
Admin 通过专门的界面管理用户:更改角色、停用或重新激活账号,或重置密码。已停用的用户会在其下一次请求时被登出。Admin 不能降级或停用自己,并且每个 Admin 操作都会被写入单独的用户审计日志中。
## 审查工作流
发现项会经历七个状态,合法的操作由状态机定义,而不是零散的检查。每个状态声明它允许的转换,并且 `WorkflowStateMachine` 会在允许操作之前验证动作、调用者的角色以及任何前置条件。非法转换返回 409,角色错误尝试返回 403。
A finding under review: its mapped controls on the left, the reviewer's approve-or-return decision and lifecycle tracker on the right.
| 起点 | 动作 | 目标状态 | 执行者 | 前置条件 |
|---|---|---|---|---|
| Open 或 In progress | Submit | Submitted | Owner (Analyst) | 至少映射一个控制项 |
| Open、In progress 或 Returned | Edit | 保持原样 (Open 会变为 In progress) | Owner | 编辑处于 Open 状态的发现项会将其提升 |
| Submitted | Approve | Approved | Reviewer,且不能是 Owner | |
| Submitted | Return for changes | Returned | Reviewer,且不能是 Owner | 必须填写评论 |
| Returned | Resubmit | Submitted | Owner | |
| Approved | Mark remediated | Remediated | Owner | |
| Approved | Accept risk | Accepted | Reviewer | |
| Remediated 或 Accepted | Reopen | In progress | Owner | |
最重要的规则是职责分离。只有 Reviewer 才能批准或退回已提交的发现项,并且 Reviewer 永远不能对他们自己拥有的发现项执行操作。此检查在后端强制执行,因此从 UI 中移除按钮绝不是阻止它的原因。
The reviewer's queue: findings awaiting sign-off, oldest first, with the approve-or-return panel alongside.
## 风险评分与态势
每个发现项都会获得一个 0 到 10 的风险评分,由 Strategy 选择。如果发现项具有 CVSS 基础分数,则使用该值。否则,回退机制会将严重性映射到一个数字(Critical 9.0、High 7.5、Medium 5.0、Low 2.0)。响应会记录产生该分数的方法,因此在 UI 中派生的分数会显示为 derived。
在程序级别,态势量表的范围从 0 到 100,数值越高越糟。它对活动的发现项进行加权(Critical 10、High 6、Medium 3、Low 1),除以可配置的上限,并将结果限制在从 Low 到 Severe 的五个区间之一。态势视图按严重性和状态细分发现项,并显示严重性 x 状态的热力图。
Program risk posture: the 0–100 gauge and its band, the headline counts, and the severity and status breakdowns underneath.
## 控制项覆盖率与报告
覆盖率按框架计算。对于每个控制项,它显示映射到其上的发现项数量、其中最严重的严重性,以及它是否处于风险中(意味着它仍然与活动的 High 或 Critical 发现项相关联)。摘要磁贴提供了覆盖百分比以及已覆盖、处于风险中和存在缺口的控制项计数。
Control coverage for one framework, control by control: what is covered, what is at risk, and where the gaps are.
报告以 CSV 或 PDF 格式跨多个视图导出:发现项登记表、控制项覆盖率表、审计追踪、风险态势报告(其图表直接使用 PDFBox 绘制),以及仅限管理员的用户管理审计。CSV 输出遵循 RFC 4180 标准,带有 UTF-8 Byte Order Mark,并防范电子表格公式注入。表格形式的 PDF 采用 A4 横向排版,带有彩色标题和自动换行的单元格,全部使用 Apache PDFBox 生成。
One page of the exported eight-page posture report, rendered here by pdf.js. The gauge, bars, and this heatmap are drawn straight in PDFBox with no chart library; the insights and recommended actions are synthesized from the data by fixed rules, deliberately not by AI, so an auditor can reproduce them.
该报告作为实际的 PDF 存放在 [`sample-reports/`](sample-reports/) 中,您可以打开查看,同时还包括发现项登记表(PDF 和 CSV)以及 ISO 27001 覆盖率报告。它们是使用与上述截图相同的演示数据导出的。
## AI 辅助控制项映射
将发现项映射到正确的控制项是 GRC 工作中繁琐的部分,因此 AuditYote 可以执行第一轮操作。在发现项的详情页面上,Analyst 请求建议,后端将发现项和控制项目录发送给 Claude,然后返回一个简短的候选控制项列表,每个控制项都带有置信度和一句话的理由。在 Analyst 接受建议之前,不会映射任何内容。
AI suggestions on a SQL-injection finding. Each proposal carries a confidence score and a rationale, and becomes a mapping only when the analyst accepts it. Every code is checked against the catalog first, and controls already mapped are left out.
每个建议的代码都会与目录进行比对(Grounding):模型返回的任何非真实控制项代码的内容都会被丢弃,因此幻觉产生的引用永远不可能成为映射。当 Analyst 接受建议时,该映射会被标记为 AI 建议,并存储模型、置信度和理由。此来源信息是在服务器端根据缓存建议写入的,因此客户端无法伪造 AI 来源或将其附加到手动映射中。AI 只提供建议;它从不批准发现项,从不绕过工作流,也从不干涉职责分离。
它也是可选且低成本的。整个功能位于一个标志之后,默认情况下是关闭的,因此评分核心从不依赖于它;一个 `GET /api/config` 布尔值告诉 SPA 是否显示该按钮(在线演示运行时已开启)。Anthropic API 密钥是后端机密,永远不会到达浏览器。建议按用户进行速率限制并按发现项进行缓存,目录作为缓存的 prompt 前缀发送,因此重复调用的费用大约减少百分之九十,并且 CI 模拟了客户端,因此流水线永远不会进行实时调用或产生任何费用。该策略可以在 `MappingSuggestionStrategy` 接口后进行替换,通过一个狭窄的 `SuggestionModelClient` 端口将唯一感知 Anthropic 的类与比对逻辑隔离开来,后者使用 fake 对象进行了单元测试。
## 审计追踪
对发现项的每次更改都会发布一个领域事件:创建、带有更改摘要的编辑、映射或取消映射控制项、删除以及每次工作流转换。一个监听器在同一个数据库事务中写入一条 `AuditLog` 记录,因此该记录与其描述的更改是原子的,并且此后永远不会被触碰。发现项详情页面将此历史记录呈现为活动时间线。对账号执行的 Admin 操作以相同的方式记录在它们自己不可变的日志中。
## 架构
AuditYote 是两个独立的应用程序。前端是一个由 Vite 构建并由 nginx 作为静态文件提供的 React 单页应用。后端是一个独立的 Spring Boot REST API。它们通过 HTTP 和 JSON 进行通信,并且永远不会合并到一个进程中。
每个环境都在与 SPA 相同的源下提供 API。Vite 开发代理、容器中的 nginx 以及生产环境中主机的 nginx 都将 `/api` 转发到后端,因此浏览器永远不会处理 CORS,并且会话 Cookie 始终保持第一方状态。
```
Browser (React SPA, Vite build served by nginx)
| same-origin HTTPS
v
nginx on the host (TLS via Let's Encrypt) serves the SPA, proxies /api
|
v
Frontend container (nginx) --/api--> Backend container (Spring Boot)
| Spring Security (session + CSRF)
| JPA / Hibernate
v
PostgreSQL 16
```
在后端内部,请求流经处理 DTO 的 controller,进入包含领域逻辑的 service,最后到达 Spring Data JPA repository 和 PostgreSQL。一个 mapper 层将 JSON 网络传输格式与 JPA 实体分开并在它们之间进行转换,包括大小写的差异:API 使用小写的严重性和 kebab-case 的状态,而枚举则是大写的。
## 技术栈
| 领域 | 技术 |
|---|---|
| 后端 | Spring Boot 3.4.2, Java 21 (Temurin), Maven |
| 持久化 | Spring Data JPA / Hibernate, PostgreSQL 16, Flyway (6 次迁移) |
| 安全 | Spring Security, BCrypt, 基于 Cookie 的 CSRF |
| 报告 | Apache PDFBox 3.0.3 (PDF), RFC 4180 CSV |
| AI (可选) | Anthropic Java SDK 2.48.0, Claude Haiku 4.5, 基于目录的比对及 prompt 缓存,默认关闭 |
| 前端 | React 19, Vite 8, TypeScript, Tailwind CSS 3.4, react-router 7, lucide-react |
| 工具 | oxlint, Playwright (参考截图) |
| 容器 | Docker 多阶段构建,非 root 镜像 |
| 代理和 TLS | 生产环境中使用 nginx + Certbot,或使用带自动 TLS 的 Caddy 作为替代方案 |
| CI/CD | GitHub Actions,包含 Semgrep (SAST) 和 Trivy (CVE、密钥和错误配置扫描) |
其中有几个选择是经过深思熟虑的。前端特意选择了 Vite SPA 而不是 Next.js,这样任何前端框架都不会悄悄成为第二个后端;后端是 Spring Boot,并且仅仅是 Spring Boot。身份验证使用带有 CSRF 保护的有状态会话 Cookie,而不是 JWT,这非常适合从单一来源提供的应用程序。UI 组件是基于基于 token 的设计系统从头编写的,而不是从组件库中提取的。
## 数据模型
该模式包含七个实体。一个 `User` 拥有一个角色(Analyst、Reviewer 或 Admin)、一个 BCrypt 密码哈希和一个 active 标志。一个 `Framework` 对 `Control` 行进行分组;种子数据加载了三个框架和 125 个控制项:完整的 ISO/IEC 27001:2022 附录 A (93)、OWASP Top 10 (10) 和 NIST CSF 2.0 类别 (22)。一个 `Finding` 属于一个 owner,带有一个严重性和一个可选的 CVSS 分数,包含一个内嵌的 `Asset`(名称、环境、组件、URL),并具有一个软删除时间戳。发现项通过 `FindingControlMapping` 连接表连接到控制项,每个发现项和控制项对唯一,记录了每个映射是如何创建的:手动创建,还是从 AI 建议中接受。AI 来源的映射还存储模型、其置信度及其理由。两个表用于保存历史记录:`AuditLog` 用于记录发现项的每次更改及其转换,而 `UserAuditLog` 用于记录对账号执行的 Admin 操作。两者都是只写的。
Flyway 通过六次迁移管理模式,并且 Hibernate 在 validate 模式下运行,因此实体与数据库之间的任何不匹配都会在启动时被捕获,而不是被掩盖。
## 设计模式
少数模式在代码中具有实际的分量,而不是仅仅停留在注释中。工作流是一个状态机。风险评分是一个策略,按顺序选择。报告编写器来自一个以格式为键的工厂,因此添加一种格式一个新的 bean,而无需调用者进行任何更改。审计日志是一个观察者:Service 发布事件,监听器记录事件,这将审计排除在工作流代码之外。数据访问通过 Spring Data JPA repository 进行,并且 DTO 和 mapper 层使 API 契约独立于持久化模型。可选的 AI 控制项映射助手是第二个策略(`MappingSuggestionStrategy`),位于一个狭窄的 `SuggestionModelClient` 端口之后,因此可以在不影响比对逻辑的情况下交换或移除提供商。这些在实践中符合 SOLID 原则:轻量级的 controller、隔离在 mapper 中的转换,以及通过添加类而不是编辑现有类来扩展的行为。
## 设计系统
界面运行在一组映射到 Tailwind 的 CSS 自定义属性(颜色、字体缩放、间距、圆角和阴影)之上,具有两个实时切换的主题:一个名为 Sovereign 的温暖默认主题,以及一个名为 Carbon 的冷色调替代方案。所有数字、标识符、CVSS 分数和控制项引用都使用等宽字体,以便它们在表格中对齐。严重性和状态颜色由含义固定,永远不会被重用于其他任何用途。这里没有表情符号;图标来自单一的线性图标组件。布局专为桌面工作而设计得非常紧凑,并在较窄的屏幕上会适度降级。
The same dashboard in the Carbon theme; the default is the warmer Sovereign. Both are the same components driven by different tokens.
## 安全性
安全性是本项目的核心,因此它贯穿于整个项目。身份验证使用带有会话 Cookie(httpOnly、SameSite Lax,在生产环境中为 Secure)的 Spring Security,以及从不被记录的 BCrypt 密码哈希。授权在服务器端强制执行,在 Reviewer 和 Admin 端点上具有方法级别的检查,并且安全过滤器链要求对所有内容进行身份验证,除了健康检查、登录和注册。
CSRF 保护使用一个基于 Cookie 的 token,SPA 在不安全的请求中将其回显到 Header 中。注销会使服务器会话失效,过期的会话会将 SPA 发送到登录屏幕,而不是静默失败。离职处理会立即生效:每个请求的过滤器都会重新检查账号是否仍然处于活动状态,因此被停用的用户会在下一次调用时被登出,并且无法重新登录。
输入使用 Jakarta Bean Validation 和领域防护机制进行验证,并且错误以统一一致的格式返回,没有堆栈跟踪或内部细节。登录失败会返回通用消息,因此无法枚举账号。所有数据访问都通过 JPA 进行参数化,没有字符串拼接的 SQL。代码库中不存在任何机密:`.env.example` 已提交,而真实的 `.env` 被忽略。可选的 AI 集成也坚持同样的原则:Anthropic 密钥永远不会离开服务器,每一个建议在成为映射之前都要与目录进行比对,并且每个映射的 AI 来源都是在服务器端设置的,因此无法伪造。
CI 扫描器被视为关卡。当 Semgrep 或 Trivy 标记某些内容时,它会得到修复,而不是被静默。例如,PostgreSQL JDBC 驱动程序被固定在其托管版本之上,以修补已知的 CVE,并且前端被转移到一个非特权 nginx 镜像上。SPA 提供了一组严格的响应 Header:HSTS、Content-Security Policy、`X-Frame-Options`、`X-Content-Type-Options`,以及锁定的 Referrer 和 Permissions 策略。生产流量是带有从 HTTP 重定向的 HTTPS,并且两个审计日志都是不可变的。
## 在本地运行
Docker 和 Docker Compose 是您唯一需要安装的东西。Java、Node 和 Postgres 都在构建内部运行。
```
# 1. 配置环境
cp .env.example .env
# 编辑 .env:database credentials、session secret 和 seed demo users
# 2. 启动 Postgres、backend 和 frontend
docker compose --profile app up --build
```
前端通过 http://localhost:5173 提供,API 通过 http://localhost:8080 提供,Postgres 监听 5432 端口。演示 Analyst、Reviewer 和 Admin 账号来自您在 `.env` 中设置的种子值。`.env.example` 记录了每个配置键,包括数据源、会话密钥、种子用户、注册邮箱域名白名单、态势上限以及可选的 AI 设置(Anthropic API 密钥、启用标志和模型)。
## 持续集成与部署
每次推送都会运行一个 GitHub Actions 流水线。一个检测作业对其余作业进行控制,因此构建在 monorepo 中保持快速。后端作业构建并运行针对 Java 21 上的 PostgreSQL 服务的完整测试套件。前端作业安装依赖项,使用 oxlint 进行 lint,并使用 Vite 进行构建。两个安全扫描器运行并且可能导致构建失败:Semgrep 使用默认的 OWASP Top Ten 和机密规则集,而 Trivy 针对高和严重严重性的依赖项、机密和错误配置进行扫描。
这两个镜像都是多阶段的,并以非 root 用户身份运行。技术栈运行在反向代理后面的 Linux VPS 上的 Docker 中,TLS 来自 Let's Encrypt,并且具有从 HTTP 到 HTTPS 的重定向。Flyway 在启动时应用其迁移,因此在无需手动步骤的情况下即可创建模式并进行版本控制。反向代理配置和部署运行手册位于 `deploy/` 目录中。
## 测试
后端有 34 个测试类和 179 个测试方法(JUnit 5、Mockito 和 MockMvc,在 CI 中针对真实的 PostgreSQL 运行)。它们涵盖了身份验证和注册、发现项 CRUD 及其编辑约束、控制项映射和唯一性、完整的工作流状态机(包括角色限制和职责分离)、审计追踪生成和不可变性、覆盖率汇总、风险评分、态势规范化、审查队列排序、Admin 用户管理及其自锁防护、通知、CSV 和 PDF 报告导出、软删除行为、健康检查和种子幂等性。一组专门的测试涵盖了 AI 控制项映射策略:剔除幻觉产生的代码、服务器权威的来源信息、速率限制和缓存失效,其中实时的 Claude 调用被模拟,因此 CI 永远不会产生花费。前端构建在 CI 中经过了 lint 和类型检查,Playwright 捕获了主屏幕的参考截图。
## 项目布局
```
backend/ Spring Boot 3.4.2, Java 21, Maven
src/main/java/io/muzoo/ssc/controlmap/
ai/ audit/ config/ domain/ health/ report/ repository/
risk/ security/ seed/ web/ workflow/
src/main/resources/ application.yml, db/migration (V1-V6), catalog + seed data
src/test/java/... 34 test classes
Dockerfile
frontend/ React 19 + Vite 8 + TypeScript
src/ auth/ components/ design/tokens lib/ pages/
Dockerfile nginx.conf tailwind.config.js vite.config.ts
deploy/ nginx vhost + deployment runbook
.github/workflows/ci.yml
docker-compose.yml docker-compose.prod.yml docker-compose.deploy.yml Caddyfile
.env.example
```
作为参考,该代码库包含跨两个层级的约 13,300 行生产代码:约 129 个后端 Java 文件和 34 个测试类,以及约 51 个前端 TypeScript 和 TSX 文件。它具有 7 个实体、12 个带有 32 个端点的 REST controller、6 次数据库迁移、3 个带有 125 个控制项的种子框架,以及跨十几个界面的约 24 个 React 组件,通过 65 次提交构建完成。
## 关于本项目
AuditYote 是作为大学系统软件构建课程的毕业设计独立完成的。代码库在其 Java 包名(`io.muzoo.ssc.controlmap`)、其配置键以及发现项引用的 `CM-` 前缀中沿用了早期的工作名称 ControlMap;AuditYote 是产品名称。