@deepwhale-cn/dsh-compliance-doc 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 深鲸 DeepWhale
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,139 @@
1
- # Temporary Holding Version
1
+ # @deepwhale-cn/dsh-compliance-doc
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![Listed on dsh-plugin.org](https://dsh-plugin.org/badges/listed.svg)](https://dsh-plugin.org/zh/plugins/feely0208/dsh-compliance-doc)
4
+
5
+ **检查结果别只躺在终端里 —— 自查表、整改说明、带边界声明的合规证明,一次生成三份能直接交出去的文档。**
6
+
7
+ **关键词**:合规报告 · 自查表 · 整改说明 · 合规证明 · 等保三级 · 安全自查 · 整改复核 · 一键体检 · 交付文档 · 审计留痕
8
+
9
+ ---
10
+
11
+ ## 先说清楚它是什么、不是什么
12
+
13
+ **不装的代价**:扫描结果只在终端滚一遍屏,领导要「自查表」、客户要「整改说明」、
14
+ 测评机构要「检查记录」,你还得手工从日志里一条条复制进 Excel —— 慢、容易漏、
15
+ 格式还不统一;更麻烦的是,手写文档很容易顺手写一句「已合规」,把提示项说成结论。
16
+
17
+ **是**:把一份本地静态检查报告,整理成三份**可以直接交付**的文件:
18
+
19
+ | 文件 | 内容 | 谁看 |
20
+ |---|---|---|
21
+ | `自查表.csv` | 序号 / 检查项 / 严重度 / 依据 / 位置 / 状态 / 建议,UTF-8 BOM,Excel 双击就能开 | 执行整改的人 |
22
+ | `整改说明.md` | 背景、检查范围与方法、逐条问题 + 整改建议、复核方式 | 领导 / 客户 / 项目经理 |
23
+ | `合规证明.md` | 检查范围、规则条数、依据清单(去重)、结论措辞 | 对外提交 / 归档 |
24
+
25
+ **不是**:
26
+ - ❌ **不是合规认证** —— 静态检查证明不了合规,它只如实记录「规则库这一次查到了什么」
27
+ - ❌ **不是等保测评 / 监管检查 / 第三方审计的替代品** —— 那需要测评机构按标准现场核查
28
+ - ❌ **不产生新判据** —— 规则来自你已安装的深鲸守护插件,它只做整理和成文
29
+ - ❌ **不写「已合规」「保证通过」** —— 三份文件都带同一条声明,结论措辞按「提示项」而非「违规」表述
30
+
31
+ **一句话分界**:别的插件负责「查」,它负责把查出来的东西写成**能交出去、且有边界说明**的文档。
32
+
33
+ ```
34
+ dsh-sbom / dsh-data-rights / ... = 查
35
+ dsh-fix-verify = 复核改前改后
36
+ dsh-compliance-doc = 把上面两步的结果变成可交付文档
37
+ ```
38
+
39
+ ---
40
+
41
+ ## 用法
42
+
43
+ ```bash
44
+ # 从一个普通扫描报告生成三份文档
45
+ dsh-compliance-doc --input report.json --out ./交付文档
46
+
47
+ # 带单位名、检查日期、等保三级文档档次
48
+ dsh-compliance-doc --input report.json --out ./交付 \
49
+ --org "某某科技有限公司" --date 2026-10-20 --profile 等保三级
50
+
51
+ # 配合 dsh-fix-verify:把改前改后复核结果整理成整改说明
52
+ dsh-fix-verify --before old --after new --json > fix-verify.json
53
+ dsh-compliance-doc --input fix-verify.json --out ./交付 --org "某某科技有限公司"
54
+ ```
55
+
56
+ 输出目录不存在会**自动创建**。
57
+
58
+ ### 输入兼容两种 JSON
59
+
60
+ **① 普通 / suite 扫描报告**(`dsh-guard-core` 的标准输出):
61
+
62
+ ```json
63
+ { "generatedAt": "...", "stats": { "scannedFiles": 34, "rulesRun": 12, ... }, "findings": [ ... ] }
64
+ ```
65
+
66
+ **② `dsh-fix-verify` 的复核报告**:顶层有 `fixed` / `remaining` / `regressed` 数组。
67
+
68
+ 字段不对、JSON 坏、输入缺失 → **exit 2**,不会硬编一份「看起来正常」的文档。
69
+
70
+ ---
71
+
72
+ ## 措辞红线(三份文件都写)
73
+
74
+ > 本文件由本地静态检查生成,不构成合规结论,不替代等保测评/监管检查/第三方审计。
75
+
76
+ - 不出现「已合规」「保证通过」「绝对合规」这类结论;
77
+ - 规则未命中时 `合规证明.md` 会写「本次检查覆盖 N 条规则、M 个文件,未发现提示项」,
78
+ 同时明确提醒「**未命中不等于没有问题**」;
79
+ - `--profile 等保三级` 只影响**文档组织方式**,不改变规则集,也不构成等保测评结论。
80
+
81
+ ---
82
+
83
+ ## 输出示例
84
+
85
+ ```
86
+ ✅ 自查表.csv 3 条检查项,Excel 可直接打开
87
+ ✅ 整改说明.md 背景 / 范围与方法 / 逐条问题与建议 / 复核方式
88
+ ✅ 合规证明.md 范围 / 规则条数 / 依据清单 / 结论措辞
89
+
90
+ 扫描 34 个文件 · 运行规则 12 条
91
+ 提示项 3 条(高 1 · 中 1 · 低 1)
92
+ ```
93
+
94
+ ### 退出码
95
+
96
+ | 码 | 含义 |
97
+ |---|---|
98
+ | `0` | 三份文件正常生成 |
99
+ | `2` | 输入缺失 / JSON 解析失败 / 字段不对 / 参数错误 |
100
+
101
+ ---
102
+
103
+ ## 不要做什么
104
+
105
+ - 不要把它生成的 `合规证明.md` 当成「合规证书」对外使用 —— 它只是检查记录
106
+ - 不要删掉文件里的免责声明 —— 那句话是这份文档能用的前提
107
+ - 不要在输入报告为空时断定「项目没问题」—— 未命中不等于没有问题
108
+ - 不要把它当扫描器 —— 它不跑规则,只整理报告
109
+ - 不要用 `--profile 等保三级` 暗示已经做过等保测评 —— 那只影响文档排版
110
+
111
+ ## 安装
112
+
113
+ ```bash
114
+ dsh plugin --profile web add @deepwhale-cn/dsh-compliance-doc
115
+ ```
116
+
117
+ ## License
118
+
119
+ MIT © 深鲸 DeepWhale
120
+
121
+ ## 深鲸网安守护系列(共 39 个)
122
+
123
+ > 一个插件做一件事。**安全问题是链式的** —— 配套用才完整。
124
+
125
+ **国内合规**(12)| [等保对照](https://github.com/feely0208/dsh-cn-mlps) · [日志留存](https://github.com/feely0208/dsh-cn-log-retention) · [备案公示](https://github.com/feely0208/dsh-cn-filing) · [用户同意](https://github.com/feely0208/dsh-consent-guard) · [个人信息权利](https://github.com/feely0208/dsh-data-rights) · [未成年人保护](https://github.com/feely0208/dsh-minor-guard) · [影响评估](https://github.com/feely0208/dsh-pipia) · [数据分类分级](https://github.com/feely0208/dsh-data-classify) · [算法备案](https://github.com/feely0208/dsh-algorithm-filing) · [数据出境](https://github.com/feely0208/dsh-cn-data-export) · [国密合规](https://github.com/feely0208/dsh-cn-crypto) · [AI 内容标识](https://github.com/feely0208/dsh-cn-ai-labeling)
126
+
127
+ **AI 与 Agent 治理**(6)| [AI 输出信任](https://github.com/feely0208/dsh-ai-output-trust) · [外部内容信任](https://github.com/feely0208/dsh-content-trust) · [MCP 安全](https://github.com/feely0208/dsh-mcp-audit) · [Agent 权限](https://github.com/feely0208/dsh-agent-scope) · [策略核对](https://github.com/feely0208/dsh-agent-policy) · [可疑指令](https://github.com/feely0208/dsh-phishing-guard)
128
+
129
+ **数据与凭据**(5)| [敏感数据](https://github.com/feely0208/dsh-data-scan) · [密钥轮换](https://github.com/feely0208/dsh-key-rotate) · [数据留存](https://github.com/feely0208/dsh-data-lifecycle) · [配置审计](https://github.com/feely0208/dsh-env-audit) · [密钥扫描](https://github.com/feely0208/dsh-secret-scan)
130
+
131
+ **供应链与构建**(5)| [开源许可](https://github.com/feely0208/dsh-license-guard) · [依赖投毒](https://github.com/feely0208/dsh-dep-audit) · [CI 安全](https://github.com/feely0208/dsh-ci-guard) · [工具审查](https://github.com/feely0208/dsh-tool-vet) · [物料清单](https://github.com/feely0208/dsh-sbom)
132
+
133
+ **权限与应急**(3)| [最小权限](https://github.com/feely0208/dsh-least-privilege) · [告警外发](https://github.com/feely0208/dsh-alert-relay) · [应急证据包](https://github.com/feely0208/dsh-incident-package)
134
+
135
+ **行业专项**(4)| [医疗行业专项](https://github.com/feely0208/dsh-industry-medical) · [教育行业专项](https://github.com/feely0208/dsh-industry-education) · [金融行业专项](https://github.com/feely0208/dsh-industry-finance) · [地方数据条例](https://github.com/feely0208/dsh-local-regulation)
136
+
137
+ **编排与交付**(4)| [一键体检](https://github.com/feely0208/dsh-guard-suite) · [改前改后复核](https://github.com/feely0208/dsh-fix-verify) · [合规可交付文档](https://github.com/feely0208/dsh-compliance-doc) · [整改建议](https://github.com/feely0208/dsh-remediation)
138
+
139
+ 都在 DSH 插件市场(dsh-plugin.org)、npm 和 deepwhale.org.cn/guard.html 上。
package/bin/cli.mjs ADDED
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dsh-compliance-doc —— 合规可交付文档
4
+ *
5
+ * 吃一份本地静态检查报告(普通扫描报告 或 dsh-fix-verify 的 JSON),
6
+ * 吐出三份能直接交出去的文件:
7
+ * 自查表.csv Excel 直接打开(UTF-8 BOM)
8
+ * 整改说明.md 背景 / 范围 / 逐条问题与整改建议 / 复核方式
9
+ * 合规证明.md 范围 / 规则条数 / 依据清单 / 结论措辞
10
+ *
11
+ * 措辞红线:三份文件都写明「本文件由本地静态检查生成,不构成合规结论,
12
+ * 不替代等保测评/监管检查/第三方审计」,不写「已合规」「保证通过」。
13
+ *
14
+ * 退出码:
15
+ * 0 正常
16
+ * 2 输入缺失 / JSON 解析失败 / 字段不对 / 参数错误
17
+ */
18
+
19
+ import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
20
+ import { dirname, join, resolve } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { buildDocuments, buildModel, DISCLAIMER, InputError, NO_HIT_NOTE, PROFILES, severityLabel } from '../lib/docs.mjs';
23
+
24
+ const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
25
+ const pkg = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf-8'));
26
+ const TOOL = 'dsh-compliance-doc';
27
+
28
+ function usage() {
29
+ console.log(`
30
+ ${TOOL} v${pkg.version} · 合规可交付文档
31
+
32
+ ${pkg.description}
33
+
34
+ 用法:
35
+ dsh-compliance-doc --input <报告.json> --out <输出目录> [选项]
36
+
37
+ 选项:
38
+ --input <file> 输入 JSON:普通扫描报告,或 dsh-fix-verify 的 --json 输出(必填)
39
+ --out <dir> 输出目录(不存在会自动创建,必填)
40
+ --profile <档次> 文档档次:通用(默认)| 等保三级
41
+ --org "<公司名>" 文档里的单位 / 组织名称
42
+ --date <YYYY-MM-DD> 检查日期(默认今天)
43
+ --quiet 只输出一行结果
44
+ -h, --help 显示帮助
45
+
46
+ 生成的三份文件:
47
+ 自查表.csv 序号 / 检查项 / 严重度 / 依据 / 位置 / 状态 / 建议(UTF-8 BOM,Excel 可直接打开)
48
+ 整改说明.md 背景 / 检查范围与方法 / 逐条问题与整改建议 / 复核方式
49
+ 合规证明.md 检查范围 / 规则条数 / 依据清单(去重)/ 结论措辞
50
+
51
+ 措辞边界:
52
+ 三份文件都会写明「本文件由本地静态检查生成,不构成合规结论,
53
+ 不替代等保测评/监管检查/第三方审计」。静态检查不是合规认证。
54
+
55
+ 退出码:
56
+ 0 正常生成
57
+ 2 输入缺失 / JSON 解析失败 / 字段不对 / 参数错误
58
+ `);
59
+ }
60
+
61
+ function die(msg) {
62
+ console.error(`\n ❌ ${msg}\n`);
63
+ process.exit(2);
64
+ }
65
+
66
+ function parseArgs(argv) {
67
+ const opt = { input: null, out: null, profile: '通用', org: '', date: null, quiet: false, help: false };
68
+ const need = (a, i) => {
69
+ const v = argv[i + 1];
70
+ if (!v || v.startsWith('--')) die(`参数 ${a} 缺少值`);
71
+ return v;
72
+ };
73
+ for (let i = 0; i < argv.length; i++) {
74
+ const a = argv[i];
75
+ if (a === '--input') { opt.input = need(a, i); i++; }
76
+ else if (a === '--out') { opt.out = need(a, i); i++; }
77
+ else if (a === '--profile') { opt.profile = need(a, i); i++; }
78
+ else if (a === '--org') { opt.org = need(a, i); i++; }
79
+ else if (a === '--date') { opt.date = need(a, i); i++; }
80
+ else if (a === '--quiet') opt.quiet = true;
81
+ else if (a === '-h' || a === '--help') opt.help = true;
82
+ else die(`未知参数:${a}(用 --help 看用法)`);
83
+ }
84
+ return opt;
85
+ }
86
+
87
+ function todayLocal() {
88
+ const d = new Date();
89
+ const p = (n) => String(n).padStart(2, '0');
90
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
91
+ }
92
+
93
+ function validDate(s) {
94
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return false;
95
+ const [y, m, d] = s.split('-').map(Number);
96
+ const dt = new Date(Date.UTC(y, m - 1, d));
97
+ return dt.getUTCFullYear() === y && dt.getUTCMonth() === m - 1 && dt.getUTCDate() === d;
98
+ }
99
+
100
+ function isFile(p) {
101
+ try { return statSync(p).isFile(); } catch { return false; }
102
+ }
103
+
104
+ function main() {
105
+ const opt = parseArgs(process.argv.slice(2));
106
+ if (opt.help) { usage(); process.exit(0); }
107
+
108
+ if (!opt.input) die('缺少 --input <报告.json>');
109
+ if (!opt.out) die('缺少 --out <输出目录>');
110
+ if (!PROFILES.includes(opt.profile)) {
111
+ die(`--profile 只支持:${PROFILES.join(' | ')}(收到:${opt.profile})`);
112
+ }
113
+ const date = opt.date || todayLocal();
114
+ if (!validDate(date)) die(`--date 不是合法日期(应为 YYYY-MM-DD):${date}`);
115
+
116
+ const inputPath = resolve(opt.input);
117
+ if (!isFile(inputPath)) die(`输入文件不存在:${inputPath}`);
118
+
119
+ let raw;
120
+ try {
121
+ raw = JSON.parse(readFileSync(inputPath, 'utf-8'));
122
+ } catch (e) {
123
+ die(`输入 JSON 解析失败:${inputPath}\n ${e && e.message}`);
124
+ }
125
+
126
+ let model;
127
+ try {
128
+ model = buildModel(raw, { org: opt.org, date, profile: opt.profile });
129
+ } catch (e) {
130
+ if (e instanceof InputError) die(`输入字段不对:${e.message}`);
131
+ die(`生成文档前处理输入失败:${e && e.message}`);
132
+ }
133
+
134
+ const outDir = resolve(opt.out);
135
+ try {
136
+ mkdirSync(outDir, { recursive: true });
137
+ } catch (e) {
138
+ die(`无法创建输出目录:${outDir}(${e && e.message})`);
139
+ }
140
+
141
+ const docs = buildDocuments(model);
142
+ const written = [];
143
+ for (const [name, content] of Object.entries(docs)) {
144
+ const full = join(outDir, name);
145
+ try {
146
+ writeFileSync(full, content, 'utf-8');
147
+ written.push({ name, full });
148
+ } catch (e) {
149
+ die(`写入失败:${full}(${e && e.message})`);
150
+ }
151
+ }
152
+
153
+ const ruleCount = model.rulesCount ?? 0;
154
+ const fileCount = model.scannedFiles ?? 0;
155
+
156
+ const countLabel = model.mode === 'fix-verify'
157
+ ? `已修复 ${model.fixedCount} / 遗留 ${model.byStatus['遗留'] || 0} / 新增回归 ${model.byStatus['新增回归'] || 0}`
158
+ : `提示项 ${model.total} 条`;
159
+ const inputLabel = model.mode === 'fix-verify'
160
+ ? `${model.total} 条比对记录`
161
+ : `${model.total} 条提示项`;
162
+
163
+ if (opt.quiet) {
164
+ console.log(`已生成 ${written.length} 份文件 → ${outDir}(${countLabel} / 规则 ${ruleCount} 条 / 文件 ${fileCount} 个)`);
165
+ } else {
166
+ console.log(`\n ${TOOL} v${pkg.version} · 合规可交付文档`);
167
+ console.log(' ────────────────────────────────────────────────');
168
+ console.log(` 输入 ${inputPath}(${model.modeLabel} · ${inputLabel})`);
169
+ console.log(` 输出 ${outDir}`);
170
+ console.log('');
171
+ console.log(' ✅ 自查表.csv ' + (model.total ? `${model.total} 条检查项,Excel 可直接打开` : '0 条提示项(含说明行)'));
172
+ console.log(' ✅ 整改说明.md 背景 / 范围与方法 / 逐条问题与建议 / 复核方式');
173
+ console.log(' ✅ 合规证明.md 范围 / 规则条数 / 依据清单 / 结论措辞');
174
+ console.log('');
175
+ console.log(` 扫描 ${fileCount} 个文件 · 运行规则 ${ruleCount} 条`);
176
+ if (model.mode === 'fix-verify') {
177
+ console.log(` 复核 ${countLabel}`);
178
+ } else if (model.total) {
179
+ const sev = ['critical', 'high', 'medium', 'low']
180
+ .filter((s) => model.bySeverity[s])
181
+ .map((s) => `${severityLabel(s)} ${model.bySeverity[s]}`)
182
+ .join(' · ');
183
+ console.log(` 提示项 ${model.total} 条${sev ? `(${sev})` : ''}`);
184
+ } else {
185
+ console.log(' 提示项 0 条 —— 但未命中不等于没有问题');
186
+ }
187
+ console.log('');
188
+ console.log(` > ${DISCLAIMER}`);
189
+ console.log(` > ${NO_HIT_NOTE}`);
190
+ console.log('');
191
+ }
192
+
193
+ process.exit(0);
194
+ }
195
+
196
+ main();
@@ -0,0 +1,4 @@
1
+ # @deepwhale-cn/dsh-compliance-doc 的组合层
2
+ - insert:
3
+ - id: compliance-doc
4
+ name: '@deepwhale-cn/dsh-compliance-doc'
@@ -0,0 +1,158 @@
1
+ /**
2
+ * 规则源发现器 —— dsh-compliance-doc 自带一份。
3
+ *
4
+ * 为什么每个工具插件都带一份、而不是互相 import:
5
+ * 每个插件要能**独立安装**。用户只装了 dsh-compliance-doc 时,它得自己找到
6
+ * 已经装好的规则插件;不能因为「另一个工具插件没装」而直接报错。
7
+ *
8
+ * 搜索顺序(先到先得,同一目录按 realpath 去重):
9
+ * 1) 显式传入的 extraDirs(--rules-from)
10
+ * 2) <pkgRoot>/node_modules/@deepwhale-cn/dsh-<插件名>/rules
11
+ * 3) <dirname(pkgRoot)>/dsh-<插件名>/rules(插件并排安装 / monorepo 开发时)
12
+ * 4) 环境变量 DSH_GUARD_PLUGINS(冒号分隔)
13
+ *
14
+ * 只接受「含 rules/*.json 且至少 1 条合法规则」的目录。
15
+ * 每条规则注入 `_plugin`(目录名去掉 dsh- 前缀),用于在报告里标明出处。
16
+ *
17
+ * 找不到规则源时,调用方必须 **报错退出**,绝不允许把「没检查」当成「0 命中」。
18
+ */
19
+
20
+ import { readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
21
+ import { basename, dirname, join, resolve } from 'node:path';
22
+
23
+ const SCOPE = '@deepwhale-cn';
24
+
25
+ function isDir(p) {
26
+ try { return statSync(p).isDirectory(); } catch { return false; }
27
+ }
28
+
29
+ function real(p) {
30
+ try { return realpathSync(p); } catch { return resolve(p); }
31
+ }
32
+
33
+ /** 目录名 dsh-sbom -> short 名 sbom */
34
+ function shortOf(dir) {
35
+ const n = basename(dir);
36
+ return n.startsWith('dsh-') ? n.slice(4) : n;
37
+ }
38
+
39
+ /**
40
+ * 读一个插件根目录下的 rules/*.json。
41
+ * 单个文件坏掉只跳过并记 warning —— 不能让一条坏规则把整个工具搞挂,
42
+ * 但也不能假装它不存在:warning 会打到 stderr。
43
+ */
44
+ function loadRulesFrom(root, warnings) {
45
+ const rdir = join(root, 'rules');
46
+ let files;
47
+ try {
48
+ files = readdirSync(rdir).filter((f) => f.endsWith('.json')).sort();
49
+ } catch {
50
+ return [];
51
+ }
52
+ const rules = [];
53
+ for (const f of files) {
54
+ const full = join(rdir, f);
55
+ let raw;
56
+ try {
57
+ raw = JSON.parse(readFileSync(full, 'utf-8'));
58
+ } catch (e) {
59
+ warnings.push(`规则文件无法解析,已跳过:${full}(${e && e.message})`);
60
+ continue;
61
+ }
62
+ if (!raw || typeof raw !== 'object' || !raw.id || !raw.check) {
63
+ warnings.push(`规则文件缺 id/check,已跳过:${full}`);
64
+ continue;
65
+ }
66
+ rules.push(raw);
67
+ }
68
+ return rules;
69
+ }
70
+
71
+ function childDirs(parent) {
72
+ try {
73
+ return readdirSync(parent, { withFileTypes: true })
74
+ .filter((e) => e.isDirectory())
75
+ .map((e) => join(parent, e.name));
76
+ } catch {
77
+ return [];
78
+ }
79
+ }
80
+
81
+ /**
82
+ * @param {object} o
83
+ * @param {string} o.pkgRoot 本插件根目录(绝对路径)
84
+ * @param {string[]} [o.extraDirs] --rules-from 传入的目录(可重复)
85
+ * @param {NodeJS.ProcessEnv} [o.env] 用于读 DSH_GUARD_PLUGINS
86
+ * @returns {{sources: Array<{name:string,short:string,dir:string,rules:Array}>, warnings:string[], candidates:Array}}
87
+ */
88
+ export function discoverRuleSources({ pkgRoot, extraDirs = [], env = process.env } = {}) {
89
+ const warnings = [];
90
+ const sources = [];
91
+ const candidates = [];
92
+ const seenRoots = new Set();
93
+ const seenCandidates = new Set();
94
+ const pkgReal = pkgRoot ? real(pkgRoot) : null;
95
+
96
+ function consider(pluginDir, via) {
97
+ if (!isDir(pluginDir)) return;
98
+ const rr = real(pluginDir);
99
+ if (pkgReal && rr === pkgReal) return; // 自己是工具插件,没有规则,也算不上来源
100
+ const rules = loadRulesFrom(rr, warnings);
101
+ if (!seenCandidates.has(rr)) {
102
+ seenCandidates.add(rr);
103
+ candidates.push({ name: basename(rr), short: shortOf(rr), dir: rr, rules: rules.length, via });
104
+ }
105
+ if (rules.length === 0) return;
106
+ if (seenRoots.has(rr)) return;
107
+ seenRoots.add(rr);
108
+ const short = shortOf(rr);
109
+ for (const r of rules) r._plugin = short;
110
+ sources.push({ name: basename(rr), short, dir: rr, rules });
111
+ }
112
+
113
+ /** extraDirs / env:可以是插件根,也可以是「装着多个插件的父目录」 */
114
+ function considerCandidateDir(dir, via) {
115
+ if (!dir) return;
116
+ const d = resolve(String(dir));
117
+ if (!isDir(d)) {
118
+ warnings.push(`规则源目录不存在或不是目录(已跳过):${d}`);
119
+ return;
120
+ }
121
+ if (isDir(join(d, 'rules'))) {
122
+ consider(d, via);
123
+ return;
124
+ }
125
+ let hit = false;
126
+ for (const child of childDirs(d)) {
127
+ if (isDir(join(child, 'rules'))) {
128
+ consider(child, via);
129
+ hit = true;
130
+ }
131
+ }
132
+ if (!hit) warnings.push(`目录里没有可用规则源(未找到 dsh-*/rules):${d}`);
133
+ }
134
+
135
+ // 1) 显式指定
136
+ for (const d of extraDirs || []) considerCandidateDir(d, 'extra');
137
+
138
+ if (pkgRoot) {
139
+ // 2) 当前插件自己的 node_modules(项目本地安装的守护插件)
140
+ const scopeDir = join(pkgRoot, 'node_modules', SCOPE);
141
+ for (const child of childDirs(scopeDir)) {
142
+ if (basename(child).startsWith('dsh-')) consider(child, 'node_modules');
143
+ }
144
+
145
+ // 3) 与当前插件并排安装的守护插件
146
+ for (const child of childDirs(dirname(pkgRoot))) {
147
+ if (basename(child).startsWith('dsh-')) consider(child, 'sibling');
148
+ }
149
+ }
150
+
151
+ // 4) 环境变量
152
+ const envVal = env && env.DSH_GUARD_PLUGINS;
153
+ if (envVal) {
154
+ for (const p of String(envVal).split(':').filter(Boolean)) considerCandidateDir(p, 'env');
155
+ }
156
+
157
+ return { sources, warnings, candidates };
158
+ }
package/lib/docs.mjs ADDED
@@ -0,0 +1,416 @@
1
+ /**
2
+ * 报告解析 + 三份交付文档生成。
3
+ *
4
+ * 输入兼容两种 JSON:
5
+ * ① 普通扫描报告 { generatedAt, stats, findings[] }(stats 来自 dsh-guard-core)
6
+ * ② dsh-fix-verify 的 JSON { fixed[], remaining[], regressed[], ... }
7
+ *
8
+ * 输出:自查表.csv / 整改说明.md / 合规证明.md
9
+ *
10
+ * ── 措辞红线 ─────────────────────────────────────────────────────
11
+ * 静态检查只是「把规则库能表达的东西查一遍」,它证明不了合规。
12
+ * 所以三份文件都必须带同一句声明;不得出现「已合规」「保证通过」这类结论。
13
+ */
14
+
15
+ export const DISCLAIMER =
16
+ '本文件由本地静态检查生成,不构成合规结论,不替代等保测评/监管检查/第三方审计。';
17
+
18
+ export const NO_HIT_NOTE =
19
+ '未命中不等于没有问题:静态检查只覆盖规则库能表达的判据,配置、流程、人员与实际运行情况仍需人工核查。';
20
+
21
+ export const PROFILES = ['通用', '等保三级'];
22
+
23
+ const SEV_LABEL = { critical: '严重', high: '高', medium: '中', low: '低' };
24
+ const SEV_ORDER = { critical: 0, high: 1, medium: 2, low: 3, unknown: 9 };
25
+ const STATUS_ORDER = { 遗留: 0, 新增回归: 0, 待确认: 0, 已修复: 1 };
26
+ const SEVERITIES = ['critical', 'high', 'medium', 'low'];
27
+
28
+ export class InputError extends Error {}
29
+
30
+ function str(v, fallback = '') {
31
+ return typeof v === 'string' ? v : fallback;
32
+ }
33
+
34
+ function num(v) {
35
+ return Number.isFinite(Number(v)) ? Number(v) : null;
36
+ }
37
+
38
+ export function severityLabel(sev) {
39
+ return SEV_LABEL[sev] || '未知';
40
+ }
41
+
42
+ function normalizeItem(f, status) {
43
+ if (!f || typeof f !== 'object' || Array.isArray(f)) {
44
+ throw new InputError('findings / 比对结果里存在非对象条目');
45
+ }
46
+ return {
47
+ ruleId: str(f.ruleId || f.id, '(未知规则)'),
48
+ title: str(f.title, '(未命名检查项)'),
49
+ severity: SEV_LABEL[f.severity] ? f.severity : 'unknown',
50
+ source: str(f.source),
51
+ advice: str(f.advice),
52
+ file: str(f.file, '(全局)'),
53
+ line: num(f.line) && num(f.line) > 0 ? num(f.line) : 0,
54
+ status,
55
+ };
56
+ }
57
+
58
+ function countBy(items, keyFn) {
59
+ const out = {};
60
+ for (const it of items) {
61
+ const k = keyFn(it);
62
+ out[k] = (out[k] || 0) + 1;
63
+ }
64
+ return out;
65
+ }
66
+
67
+ export function locationOf(it) {
68
+ return it.line ? `${it.file}:${it.line}` : it.file;
69
+ }
70
+
71
+ /**
72
+ * 把任意一种输入 JSON 归一化成统一的 model。
73
+ * 字段不对直接抛 InputError(CLI 会转成 exit 2)—— 绝不硬编一份看起来正常的文档。
74
+ */
75
+ export function normalizeReport(raw) {
76
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
77
+ throw new InputError('输入 JSON 顶层必须是对象');
78
+ }
79
+
80
+ const hasFixShape = ['fixed', 'remaining', 'regressed'].every((k) => Array.isArray(raw[k]));
81
+ const hasScanShape = Array.isArray(raw.findings);
82
+
83
+ if (hasFixShape) {
84
+ const remaining = raw.remaining.map((f) => normalizeItem(f, '遗留'));
85
+ const regressed = raw.regressed.map((f) => normalizeItem(f, '新增回归'));
86
+ const fixed = raw.fixed.map((f) => normalizeItem(f, '已修复'));
87
+ const items = [...remaining, ...regressed, ...fixed];
88
+ const ruleIds = new Set(items.map((i) => i.ruleId));
89
+ const pluginSources = Array.isArray(raw.rules?.sources)
90
+ ? raw.rules.sources.map((s) => str(s?.name || s?.short)).filter(Boolean)
91
+ : [];
92
+ return {
93
+ mode: 'fix-verify',
94
+ modeLabel: '改前改后复核',
95
+ generatedAt: str(raw.generatedAt),
96
+ items,
97
+ scannedFiles: num(raw.after?.stats?.scannedFiles),
98
+ beforeScannedFiles: num(raw.before?.stats?.scannedFiles),
99
+ skippedCount: num(raw.after?.stats?.skippedCount),
100
+ rulesCount: num(raw.rules?.total) ?? ruleIds.size,
101
+ pluginSources,
102
+ };
103
+ }
104
+
105
+ if (hasScanShape) {
106
+ if (!raw.stats || typeof raw.stats !== 'object' || Array.isArray(raw.stats)) {
107
+ throw new InputError('扫描报告缺少合法的 stats 字段(应为对象)');
108
+ }
109
+ const items = raw.findings.map((f) => normalizeItem(f, '待确认'));
110
+ const ruleIds = new Set(items.map((i) => i.ruleId));
111
+ return {
112
+ mode: 'scan',
113
+ modeLabel: '静态扫描',
114
+ generatedAt: str(raw.generatedAt),
115
+ items,
116
+ scannedFiles: num(raw.stats.scannedFiles),
117
+ beforeScannedFiles: null,
118
+ skippedCount: num(raw.stats.skippedCount),
119
+ rulesCount: num(raw.stats.rulesRun) ?? num(raw.stats.rulesTotal) ?? ruleIds.size,
120
+ pluginSources: [],
121
+ };
122
+ }
123
+
124
+ throw new InputError(
125
+ '无法识别的输入:既不是 {generatedAt, stats, findings[]} 扫描报告,' +
126
+ '也不是含 fixed/remaining/regressed 数组的 dsh-fix-verify 报告',
127
+ );
128
+ }
129
+
130
+ /** 在归一化结果上补文档元信息与统计,并按「问题优先 + 严重度」排序 */
131
+ export function buildModel(raw, { org = '', date, profile = '通用' } = {}) {
132
+ const base = normalizeReport(raw);
133
+ const items = [...base.items];
134
+ items.sort((a, b) =>
135
+ (STATUS_ORDER[a.status] ?? 0) - (STATUS_ORDER[b.status] ?? 0) ||
136
+ (SEV_ORDER[a.severity] ?? 9) - (SEV_ORDER[b.severity] ?? 9) ||
137
+ a.ruleId.localeCompare(b.ruleId) ||
138
+ a.file.localeCompare(b.file));
139
+
140
+ const bySeverity = countBy(items, (i) => i.severity);
141
+ const byStatus = countBy(items, (i) => i.status);
142
+ const problems = items.filter((i) => i.status !== '已修复');
143
+
144
+ return {
145
+ ...base,
146
+ org: org || '(未填写)',
147
+ date,
148
+ profile,
149
+ items,
150
+ total: items.length,
151
+ problemCount: problems.length,
152
+ fixedCount: byStatus['已修复'] || 0,
153
+ bySeverity,
154
+ byStatus,
155
+ };
156
+ }
157
+
158
+ // ── 自查表.csv ───────────────────────────────────────────────────
159
+
160
+ function csvCell(v) {
161
+ const s = String(v ?? '');
162
+ return /[",\n\r]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
163
+ }
164
+
165
+ function csvRow(cells) {
166
+ return cells.map(csvCell).join(',');
167
+ }
168
+
169
+ export function buildCsv(model) {
170
+ const lines = ['序号,检查项,严重度,依据,位置,状态,建议'];
171
+ model.items.forEach((it, i) => {
172
+ lines.push(csvRow([
173
+ i + 1,
174
+ it.title,
175
+ severityLabel(it.severity),
176
+ it.source,
177
+ locationOf(it),
178
+ it.status,
179
+ it.advice,
180
+ ]));
181
+ });
182
+ if (!model.items.length) {
183
+ lines.push(csvRow(['', '本次检查未发现提示项(未命中不等于没有问题)', '', '', '', '—', NO_HIT_NOTE]));
184
+ }
185
+ lines.push(csvRow(['', '【声明】' + DISCLAIMER, '', '', '', '仅供参考', '']));
186
+ // UTF-8 BOM:Excel 直接双击打开不乱码
187
+ return '\uFEFF' + lines.join('\r\n') + '\r\n';
188
+ }
189
+
190
+ // ── 整改说明.md ──────────────────────────────────────────────────
191
+
192
+ function metaLines(model) {
193
+ const L = [];
194
+ L.push(`- 单位 / 组织:${model.org}`);
195
+ L.push(`- 检查日期:${model.date}`);
196
+ L.push(`- 检查方式:${model.modeLabel}(本地静态检查,不联网、不执行被检代码)`);
197
+ L.push(`- 文档生成时间:${new Date().toISOString()}`);
198
+ if (model.generatedAt) L.push(`- 报告生成时间:${model.generatedAt}`);
199
+ L.push(`- 文档档次(profile):${model.profile}`);
200
+ return L;
201
+ }
202
+
203
+ function problemItems(model) {
204
+ return model.items.filter((i) => i.status !== '已修复');
205
+ }
206
+
207
+ function renderItem(it, idx) {
208
+ const L = [];
209
+ L.push(`### ${idx}. [${severityLabel(it.severity)}] ${it.title}`, '');
210
+ L.push(`- 规则:\`${it.ruleId}\``);
211
+ L.push(`- 位置:\`${locationOf(it)}\``);
212
+ L.push(`- 状态:${it.status}`);
213
+ L.push(`- 依据:${it.source || '(该检查项未提供依据文本)'}`);
214
+ L.push(`- 整改建议:${it.advice || '(该检查项未提供整改建议,请按规则依据人工确认)'}`);
215
+ L.push('');
216
+ return L.join('\n');
217
+ }
218
+
219
+ export function buildRectificationMd(model) {
220
+ const L = [];
221
+ L.push('# 整改说明', '');
222
+ L.push('> **声明**:' + DISCLAIMER, '');
223
+ L.push('## 文档信息', '');
224
+ L.push(metaLines(model).join('\n'));
225
+ L.push('');
226
+ L.push('## 一、背景', '');
227
+ if (model.mode === 'fix-verify') {
228
+ L.push('本说明依据一次「改前改后复核」的本地静态检查结果整理:对同一项目的旧快照与新快照运行同一套规则,' +
229
+ '逐条比对两次结果,区分已修复、遗留与新增回归项。');
230
+ } else {
231
+ L.push('本说明依据一次本地静态检查结果整理:使用深鲸守护插件提供的规则,对目标项目做只读扫描,' +
232
+ '把命中的提示项汇总成可跟踪的整改清单。');
233
+ }
234
+ L.push('静态检查的作用是**提示需要确认的位置**,不是判定违规;本说明也不对合规状态作结论。');
235
+ L.push('');
236
+ L.push('## 二、检查范围与方法', '');
237
+ const scope = [];
238
+ if (model.scannedFiles != null) scope.push(`扫描文件 ${model.scannedFiles} 个(跳过非文本 ${model.skippedCount ?? 0} 个)`);
239
+ if (model.mode === 'fix-verify' && model.beforeScannedFiles != null) {
240
+ scope.push(`旧快照 ${model.beforeScannedFiles} 个文件、新快照 ${model.scannedFiles} 个文件`);
241
+ }
242
+ scope.push(`运行规则 ${model.rulesCount} 条`);
243
+ if (model.pluginSources.length) scope.push(`规则来源插件:${model.pluginSources.join('、')}`);
244
+ L.push('- ' + scope.join(';'));
245
+ L.push('- 方法:只读遍历文件内容并按规则匹配,不联网、不执行被检代码、不上传任何文件;' +
246
+ '每条提示项都带「文件:行号」定位与规则依据,便于逐条核对。');
247
+ if (model.profile === '等保三级') {
248
+ L.push('- 说明:文档按「等保三级」关注点组织呈现,但规则集不变,本文件**不构成等保测评结论**。');
249
+ }
250
+ L.push('- 扫描范围只覆盖文本类文件;被扫描器跳过的目录(node_modules、dist 等)不在本次范围。');
251
+ L.push('');
252
+ L.push('## 三、发现的问题与整改建议', '');
253
+ const problems = problemItems(model);
254
+ if (!problems.length) {
255
+ L.push(model.mode === 'fix-verify'
256
+ ? '改后快照未发现提示项(遗留与新增回归均为 0)。'
257
+ : '本次静态检查未发现提示项。');
258
+ L.push('');
259
+ L.push('> ' + NO_HIT_NOTE);
260
+ } else {
261
+ L.push(`本次共整理 ${problems.length} 个提示项,按严重度从高到低列出:`);
262
+ L.push('');
263
+ problems.forEach((it, i) => L.push(renderItem(it, i + 1)));
264
+ }
265
+ L.push('');
266
+ const fixed = model.items.filter((i) => i.status === '已修复');
267
+ L.push('## 四、本轮已修复项(不计入待整改)', '');
268
+ if (!fixed.length) {
269
+ L.push('无。');
270
+ } else {
271
+ L.push(`本轮复核确认 ${fixed.length} 项在改后快照中已不再命中:`);
272
+ L.push('');
273
+ fixed.forEach((it, i) => {
274
+ L.push(`${i + 1}. [${severityLabel(it.severity)}] ${it.title}(\`${it.ruleId}\`,\`${locationOf(it)}\`)—— 改后快照未再命中`);
275
+ });
276
+ }
277
+ L.push('');
278
+ L.push('## 五、复核方式', '');
279
+ L.push('1. **重新跑一遍扫描**:用同一套规则、同一扫描范围重新生成报告;');
280
+ L.push('2. **改前改后对比**:用 `dsh-fix-verify --before <旧快照> --after <新快照>` 复核,' +
281
+ '确认「遗留」与「新增回归」是否归零;');
282
+ L.push('3. **逐条对照**:把新报告与本说明第三章逐条比对,确认状态是否由「遗留 / 待确认」变为「已修复」;');
283
+ L.push('4. **留痕**:旧报告、新报告与本说明一并归档,作为整改过程的证据;');
284
+ L.push('5. **人工复核**:规则覆盖不到的管理制度、流程、人员与运行情况,仍需人工核查。');
285
+ L.push('');
286
+ L.push('## 六、声明', '');
287
+ L.push('> ' + DISCLAIMER);
288
+ L.push('>');
289
+ L.push('> ' + NO_HIT_NOTE);
290
+ L.push('');
291
+ return L.join('\n');
292
+ }
293
+
294
+ // ── 合规证明.md ──────────────────────────────────────────────────
295
+
296
+ function uniqueSources(items) {
297
+ const seen = new Set();
298
+ const out = [];
299
+ for (const it of items) {
300
+ const s = it.source.trim();
301
+ if (!s || seen.has(s)) continue;
302
+ seen.add(s);
303
+ out.push(s);
304
+ }
305
+ return out;
306
+ }
307
+
308
+ function conclusionText(model) {
309
+ const rules = model.rulesCount ?? 0;
310
+ const files = model.scannedFiles ?? 0;
311
+ if (model.problemCount === 0) {
312
+ const extra = (model.mode === 'fix-verify' && model.fixedCount)
313
+ ? ` 本轮复核另确认已修复 ${model.fixedCount} 项。`
314
+ : '';
315
+ return [
316
+ `本次检查覆盖 ${rules} 条规则、${files} 个文件,未发现提示项。` + extra,
317
+ '',
318
+ '**措辞边界**:上述表述仅指本次静态检查在其规则覆盖范围内没有报出提示项。' +
319
+ '未命中不等于没有问题;' + DISCLAIMER,
320
+ ].join('\n');
321
+ }
322
+ if (model.mode === 'fix-verify') {
323
+ return [
324
+ `本轮复核结果:已修复 ${model.fixedCount} 项,遗留 ${model.byStatus['遗留'] || 0} 项,` +
325
+ `新增回归 ${model.byStatus['新增回归'] || 0} 项。`,
326
+ '',
327
+ '**措辞边界**:上述数字只描述同一套规则在改前/改后两份快照上的比对结果,' +
328
+ '不代表项目整体安全或合规状态。' + DISCLAIMER,
329
+ ].join('\n');
330
+ }
331
+ const sev = SEVERITIES
332
+ .filter((s) => model.bySeverity[s])
333
+ .map((s) => `${severityLabel(s)} ${model.bySeverity[s]} 项`)
334
+ .join('、');
335
+ return [
336
+ `本次检查覆盖 ${rules} 条规则、${files} 个文件,共发现 ${model.total} 个提示项` +
337
+ (sev ? `(${sev})` : '') + ',已逐条列入《整改说明.md》。',
338
+ '',
339
+ '**措辞边界**:上述结果只是静态检查提示,不是违规判定,也不代表项目整体合规状态。' +
340
+ '在完成整改与人工复核前,不应引用本文件作为合规结论。' + DISCLAIMER,
341
+ ].join('\n');
342
+ }
343
+
344
+ export function buildAttestationMd(model) {
345
+ const L = [];
346
+ L.push('# 合规证明', '');
347
+ L.push('> **声明**:' + DISCLAIMER, '');
348
+ L.push('## 一、检查范围', '');
349
+ L.push(metaLines(model).join('\n'));
350
+ L.push('');
351
+ L.push('本次检查的对象与边界:');
352
+ L.push('');
353
+ L.push(`- 扫描文件数:${model.scannedFiles ?? '未提供'}` +
354
+ (model.mode === 'fix-verify' && model.beforeScannedFiles != null
355
+ ? `(旧快照 ${model.beforeScannedFiles} 个)`
356
+ : ''));
357
+ L.push('- 扫描方式:本地只读静态检查,不联网、不执行被检代码、不上传文件;');
358
+ L.push('- 规则来源:深鲸守护插件规则库(可在报告中核对每条规则 ID 与依据);');
359
+ if (model.profile === '等保三级') {
360
+ L.push('- 文档档次:等保三级(仅影响文档组织方式,**不构成等保测评结论**)');
361
+ }
362
+ L.push('');
363
+ L.push('## 二、检查方法与规则条数', '');
364
+ L.push(`- 运行规则:${model.rulesCount ?? 0} 条`);
365
+ if (model.pluginSources.length) L.push(`- 规则来源插件:${model.pluginSources.join('、')}`);
366
+ L.push('- 判定方法:逐文件匹配规则库中的判据;命中项输出文件、行号、依据与建议;');
367
+ L.push('- 规则未命中不代表该项目不存在问题 —— 静态规则只能覆盖可文本化、可自动化的判据。');
368
+ L.push('');
369
+ L.push('## 三、依据清单(去重)', '');
370
+ const sources = uniqueSources(model.items);
371
+ if (!sources.length) {
372
+ L.push('本次检查未发现提示项,无对应依据条目。');
373
+ } else {
374
+ sources.forEach((s, i) => L.push(`${i + 1}. ${s}`));
375
+ }
376
+ L.push('');
377
+ L.push('## 四、检查结果', '');
378
+ if (model.problemCount === 0) {
379
+ L.push(`本次检查覆盖 ${model.rulesCount ?? 0} 条规则、${model.scannedFiles ?? 0} 个文件,未发现提示项。`);
380
+ if (model.mode === 'fix-verify' && model.fixedCount) {
381
+ L.push('');
382
+ L.push(`本轮复核另确认已修复 ${model.fixedCount} 项(改后快照不再命中)。`);
383
+ }
384
+ L.push('');
385
+ L.push('> ' + NO_HIT_NOTE);
386
+ } else if (model.mode === 'fix-verify') {
387
+ L.push(`- 已修复:${model.fixedCount} 项`);
388
+ L.push(`- 遗留:${model.byStatus['遗留'] || 0} 项`);
389
+ L.push(`- 新增回归:${model.byStatus['新增回归'] || 0} 项`);
390
+ L.push('');
391
+ L.push('> 逐条明细见《整改说明.md》。');
392
+ } else {
393
+ L.push(`- 提示项合计:${model.total} 项`);
394
+ L.push(`- 按状态:${Object.entries(model.byStatus).map(([k, v]) => `${k} ${v} 项`).join('、')}`);
395
+ L.push(`- 按严重度:${SEVERITIES.filter((s) => model.bySeverity[s]).map((s) => `${severityLabel(s)} ${model.bySeverity[s]} 项`).join('、') || '—'}`);
396
+ L.push('');
397
+ L.push('> 逐条明细与整改建议见《整改说明.md》,清单见《自查表.csv》。');
398
+ }
399
+ L.push('');
400
+ L.push('## 五、结论措辞', '');
401
+ L.push(conclusionText(model));
402
+ L.push('');
403
+ L.push('---', '');
404
+ L.push('本文件为机器生成的检查记录,可作为内部自查材料归档;对外提交前请由责任人核对并补充人工复核意见。');
405
+ L.push('');
406
+ return L.join('\n');
407
+ }
408
+
409
+ /** 生成三份文件:文件名 -> 内容 */
410
+ export function buildDocuments(model) {
411
+ return {
412
+ '自查表.csv': buildCsv(model),
413
+ '整改说明.md': buildRectificationMd(model),
414
+ '合规证明.md': buildAttestationMd(model),
415
+ };
416
+ }
package/lib/index.js ADDED
@@ -0,0 +1,14 @@
1
+ import { createPlugin, pkgRootFrom } from '@deepwhale-cn/dsh-guard-core/plugin';
2
+
3
+ // 把本地静态检查结果变成能交出去的文档:
4
+ // 自查表.csv / 整改说明.md / 合规证明.md,三份都带「不构成合规结论」声明。
5
+ export const { name, inject, apply } = createPlugin({
6
+ name: 'compliance-doc',
7
+ pkgRoot: pkgRootFrom(import.meta.url),
8
+ skillFile: 'skill/compliance-doc.md',
9
+ binFile: 'bin/cli.mjs',
10
+ description: '一份能交出去的自查表、整改说明和合规证明',
11
+ whenToUse: '用户要把安全检查结果整理成可交付文档时使用:' +
12
+ '「出一份自查表」「写个整改说明」「做一份合规证明」「检查报告要给领导/客户/测评机构看」。' +
13
+ '输入可以是普通扫描报告,也可以是 dsh-fix-verify 的 --json 复核结果。',
14
+ });
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "@deepwhale-cn/dsh-compliance-doc",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "一份能交出去的自查表、整改说明和合规证明",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "dsh",
9
+ "dsh-plugin",
10
+ "deepseek-harness",
11
+ "plugin",
12
+ "skill",
13
+ "security",
14
+ "guard",
15
+ "compliance-doc",
16
+ "合规报告",
17
+ "自查表",
18
+ "整改说明",
19
+ "合规证明",
20
+ "等保"
21
+ ],
22
+ "files": [
23
+ "lib",
24
+ "skill",
25
+ "bin",
26
+ "docs",
27
+ "cordis.patch.yml",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "main": "./lib/index.js",
32
+ "exports": {
33
+ ".": "./lib/index.js",
34
+ "./discover": "./lib/discover.mjs",
35
+ "./package.json": "./package.json"
36
+ },
37
+ "bin": {
38
+ "dsh-compliance-doc": "bin/cli.mjs"
39
+ },
40
+ "engines": {
41
+ "dsh": ">=0.1.0-rc.6 <0.3.0",
42
+ "node": ">=18"
43
+ },
44
+ "dsh": {
45
+ "bundle": {
46
+ "patch": "./cordis.patch.yml"
47
+ }
48
+ },
49
+ "dependencies": {
50
+ "@deepwhale-cn/dsh-guard-core": "^0.10.1"
51
+ },
52
+ "peerDependencies": {
53
+ "@deepseek-ai/dsh-skill": "*"
54
+ },
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "git+https://github.com/feely0208/dsh-compliance-doc.git"
58
+ },
59
+ "homepage": "https://www.deepwhale.org.cn/skills.html",
60
+ "bugs": {
61
+ "url": "https://github.com/feely0208/dsh-compliance-doc/issues"
62
+ }
6
63
  }
@@ -0,0 +1,71 @@
1
+ # 合规可交付文档(dsh-compliance-doc)
2
+
3
+ **什么时候用**:安全检查已经跑完,要把结果整理成能交出去的文档。
4
+ 用户会说:「出一份自查表」「写个整改说明」「做一份合规证明」「报告要给领导 / 客户 / 测评机构看」
5
+ 「扫描结果导出成 Excel」「整改完要留个记录」。
6
+
7
+ ## 怎么用
8
+
9
+ ```bash
10
+ <cli 路径> --input <报告.json> --out <输出目录> # 三份文件
11
+ <cli 路径> --input <报告.json> --out <目录> --org "公司名" # 写上单位
12
+ <cli 路径> --input <报告.json> --out <目录> --date 2026-10-20 # 指定检查日期
13
+ <cli 路径> --input <报告.json> --out <目录> --profile 等保三级 # 文档档次
14
+ <cli 路径> --input <报告.json> --out <目录> --quiet # 只输出一行结果
15
+ ```
16
+
17
+ 输入有两种来源:
18
+
19
+ ```bash
20
+ # ① 普通 / suite 扫描报告({ generatedAt, stats, findings[] })
21
+ <cli 路径> --input report.json --out ./交付
22
+
23
+ # ② dsh-fix-verify 的 --json 输出(含 fixed / remaining / regressed)
24
+ dsh-fix-verify --before old --after new --json > fix-verify.json
25
+ <cli 路径> --input fix-verify.json --out ./交付
26
+ ```
27
+
28
+ ## 生成什么
29
+
30
+ | 文件 | 内容 |
31
+ |---|---|
32
+ | `自查表.csv` | 序号 / 检查项 / 严重度 / 依据 / 位置 / 状态 / 建议;UTF-8 BOM,Excel 直接打开 |
33
+ | `整改说明.md` | 背景 / 检查范围与方法 / 逐条问题与整改建议 / 复核方式说明 |
34
+ | `合规证明.md` | 检查范围 / 规则条数 / 依据清单(去重)/ 结论措辞 |
35
+
36
+ fix-verify 输入时,状态列会区分「已修复 / 遗留 / 新增回归」;
37
+ 整改说明会单列「本轮已修复项」,不和待整改问题混在一起。
38
+
39
+ ## 输入格式
40
+
41
+ **扫描报告**:必须有 `findings` 数组和 `stats` 对象(`scannedFiles`、`rulesRun` 等来自
42
+ `dsh-guard-core`)。0 命中就是 `findings: []`。
43
+
44
+ **fix-verify 报告**:顶层有 `fixed` / `remaining` / `regressed` 三个数组;
45
+ 文件数取自 `after.stats.scannedFiles`,规则数取自 `rules.total`。
46
+
47
+ 字段不对、JSON 解析失败、输入不存在 → **exit 2**(不会生成半份假文档)。
48
+
49
+ ## 退出码
50
+
51
+ | 码 | 含义 |
52
+ |---|---|
53
+ | `0` | 三份文件正常生成 |
54
+ | `2` | 输入缺失 / JSON 解析失败 / 字段不对 / 参数错误(含非法 profile、非法日期) |
55
+
56
+ ## 措辞边界(必须保留)
57
+
58
+ 三份文件都会写明:
59
+
60
+ > 本文件由本地静态检查生成,不构成合规结论,不替代等保测评/监管检查/第三方审计。
61
+
62
+ - 0 命中时写「本次检查覆盖 N 条规则、M 个文件,未发现提示项」,并提醒「未命中不等于没有问题」;
63
+ - 不写「已合规」「保证通过」;
64
+ - `--profile 等保三级` 只改变文档组织方式,不构成等保测评结论。
65
+
66
+ ## 不要做什么
67
+
68
+ - 不要把生成的 `合规证明.md` 当合规证书用
69
+ - 不要删掉免责声明或把「提示项」改写成「违规项」
70
+ - 不要用它代替人工复核与真实测评
71
+ - 不要在没有扫描报告时手编输入 —— 先用扫描插件 / `dsh-fix-verify` 生成真实报告