dsh-code-security 0.2.2

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/lib/state.d.ts ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * 扫描状态持久化:供 secure_fix_verify 对比基线,secure_report 汇总历史,
3
+ * 以及 secure_baseline 持久化已接受的基线问题。
4
+ *
5
+ * @module dsh-code-security/state
6
+ */
7
+ import type { Finding } from './scanner.js';
8
+ export interface ScanStateEntry {
9
+ mode: string;
10
+ target: string;
11
+ time: string;
12
+ filesScanned: number;
13
+ findings: Finding[];
14
+ fingerprints: string[];
15
+ counts: {
16
+ critical: number;
17
+ high: number;
18
+ medium: number;
19
+ low: number;
20
+ };
21
+ }
22
+ export interface BaselineEntry {
23
+ time: string;
24
+ target: string;
25
+ reason: string;
26
+ findings: Finding[];
27
+ fingerprints: string[];
28
+ counts: {
29
+ critical: number;
30
+ high: number;
31
+ medium: number;
32
+ low: number;
33
+ };
34
+ }
35
+ export interface StateDocument {
36
+ last: ScanStateEntry | null;
37
+ history: ScanStateEntry[];
38
+ baseline: BaselineEntry | null;
39
+ }
40
+ export declare function findingFingerprint(finding: Finding): string;
41
+ export declare function statePath(stateDir: string): string;
42
+ export declare function loadState(stateDir: string): Promise<StateDocument>;
43
+ export declare function saveState(stateDir: string, entry: ScanStateEntry): Promise<string>;
44
+ export declare function saveBaseline(stateDir: string, baseline: BaselineEntry): Promise<string>;
45
+ export declare function countFindings(findings: Finding[]): {
46
+ critical: number;
47
+ high: number;
48
+ medium: number;
49
+ low: number;
50
+ };
51
+ export declare function compareFingerprints(baseline: string[], current: string[]): {
52
+ closed: string[];
53
+ remaining: string[];
54
+ fresh: string[];
55
+ };
56
+ /** 根据已接受基线把发现分为新增与已接受。 */
57
+ export declare function splitByBaseline(findings: Finding[], baseline: BaselineEntry | null): {
58
+ fresh: Finding[];
59
+ accepted: Finding[];
60
+ };
package/lib/state.js ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * 扫描状态持久化:供 secure_fix_verify 对比基线,secure_report 汇总历史,
3
+ * 以及 secure_baseline 持久化已接受的基线问题。
4
+ *
5
+ * @module dsh-code-security/state
6
+ */
7
+ import fs from 'node:fs/promises';
8
+ import path from 'node:path';
9
+ import { createHash } from 'node:crypto';
10
+ const STATE_FILE = 'state.json';
11
+ const HISTORY_LIMIT = 50;
12
+ export function findingFingerprint(finding) {
13
+ const raw = finding.ruleId + '|' + finding.file + '|' + finding.snippet.trim();
14
+ return createHash('sha256').update(raw).digest('hex');
15
+ }
16
+ export function statePath(stateDir) {
17
+ return path.join(stateDir, STATE_FILE);
18
+ }
19
+ export async function loadState(stateDir) {
20
+ try {
21
+ const text = await fs.readFile(statePath(stateDir), 'utf8');
22
+ const parsed = JSON.parse(text);
23
+ const obj = (parsed ?? {});
24
+ const last = isEntry(obj.last) ? obj.last : null;
25
+ const history = Array.isArray(obj.history) ? obj.history.filter(isEntry) : [];
26
+ const baseline = isBaseline(obj.baseline) ? obj.baseline : null;
27
+ return { last, history, baseline };
28
+ }
29
+ catch {
30
+ return { last: null, history: [], baseline: null };
31
+ }
32
+ }
33
+ function isEntry(value) {
34
+ return typeof value === 'object' && value !== null && Array.isArray(value.findings);
35
+ }
36
+ function isBaseline(value) {
37
+ return typeof value === 'object' && value !== null && Array.isArray(value.findings) && Array.isArray(value.fingerprints);
38
+ }
39
+ export async function saveState(stateDir, entry) {
40
+ await fs.mkdir(stateDir, { recursive: true });
41
+ const state = await loadState(stateDir);
42
+ state.last = entry;
43
+ state.history.unshift(entry);
44
+ state.history = state.history.slice(0, HISTORY_LIMIT);
45
+ return writeState(stateDir, state);
46
+ }
47
+ export async function saveBaseline(stateDir, baseline) {
48
+ await fs.mkdir(stateDir, { recursive: true });
49
+ const state = await loadState(stateDir);
50
+ state.baseline = baseline;
51
+ return writeState(stateDir, state);
52
+ }
53
+ async function writeState(stateDir, state) {
54
+ const file = statePath(stateDir);
55
+ const tmp = file + '.tmp-' + process.pid;
56
+ await fs.writeFile(tmp, JSON.stringify(state, null, 2), 'utf8');
57
+ await fs.rename(tmp, file);
58
+ return file;
59
+ }
60
+ export function countFindings(findings) {
61
+ const counts = { critical: 0, high: 0, medium: 0, low: 0 };
62
+ for (const finding of findings)
63
+ counts[finding.severity] += 1;
64
+ return counts;
65
+ }
66
+ export function compareFingerprints(baseline, current) {
67
+ const oldSet = new Set(baseline);
68
+ const newSet = new Set(current);
69
+ return {
70
+ closed: baseline.filter((x) => !newSet.has(x)),
71
+ remaining: baseline.filter((x) => newSet.has(x)),
72
+ fresh: current.filter((x) => !oldSet.has(x)),
73
+ };
74
+ }
75
+ /** 根据已接受基线把发现分为新增与已接受。 */
76
+ export function splitByBaseline(findings, baseline) {
77
+ if (baseline === null)
78
+ return { fresh: findings, accepted: [] };
79
+ const acceptedSet = new Set(baseline.fingerprints);
80
+ const fresh = [];
81
+ const accepted = [];
82
+ for (const finding of findings) {
83
+ if (acceptedSet.has(findingFingerprint(finding)))
84
+ accepted.push(finding);
85
+ else
86
+ fresh.push(finding);
87
+ }
88
+ return { fresh, accepted };
89
+ }
package/lib/tools.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * 九个面向模型的安全审查工具:
3
+ * secure_scan / secure_diff / secure_fix_verify / secure_report / secure_export /
4
+ * secure_baseline / secure_deps / secure_policy_show / secure_policy_set。
5
+ *
6
+ * @module dsh-code-security/tools
7
+ */
8
+ import { type ResolvedSecureConfig } from './config.js';
9
+ import type { ProcessRunner } from './runner.js';
10
+ export interface ContentBlock {
11
+ type: 'text';
12
+ text: string;
13
+ }
14
+ export interface SecureToolDefinition {
15
+ name: string;
16
+ description: string;
17
+ parameters: {
18
+ type: 'object';
19
+ properties: Record<string, unknown>;
20
+ required?: string[];
21
+ };
22
+ output: {
23
+ schema: Record<string, unknown>;
24
+ render(args: unknown, value: unknown): ContentBlock[];
25
+ };
26
+ execute(args: unknown, exec: unknown): Promise<unknown>;
27
+ gate?(exec: unknown, next: () => Promise<unknown>): Promise<unknown>;
28
+ timeoutMs?: number;
29
+ }
30
+ export declare function buildSecureTools(cfg: ResolvedSecureConfig, cwd: string, runner: ProcessRunner): SecureToolDefinition[];
package/lib/tools.js ADDED
@@ -0,0 +1,388 @@
1
+ /**
2
+ * 九个面向模型的安全审查工具:
3
+ * secure_scan / secure_diff / secure_fix_verify / secure_report / secure_export /
4
+ * secure_baseline / secure_deps / secure_policy_show / secure_policy_set。
5
+ *
6
+ * @module dsh-code-security/tools
7
+ */
8
+ import { writeFile } from 'node:fs/promises';
9
+ import path from 'node:path';
10
+ import { optionalString, requiredString } from './config.js';
11
+ import { buildExtraTools } from './extra.js';
12
+ import { runGitDiff, scanDiff } from './diff.js';
13
+ import { loadPolicy, normalizePolicy, POLICY_FILE, savePolicy } from './policy.js';
14
+ import { ruleCategory } from './rules.js';
15
+ import { scanPath } from './scanner.js';
16
+ import { compareFingerprints, countFindings, findingFingerprint, loadState, saveState, splitByBaseline } from './state.js';
17
+ const SEVERITY_ORDER = { critical: 4, high: 3, medium: 2, low: 1 };
18
+ function compileParameters(spec) {
19
+ const properties = {};
20
+ const required = [];
21
+ for (const [key, prop] of Object.entries(spec)) {
22
+ if (prop?.required === true)
23
+ required.push(key);
24
+ const node = {};
25
+ if (typeof prop?.type === 'string')
26
+ node.type = prop.type;
27
+ if (typeof prop?.description === 'string')
28
+ node.description = prop.description;
29
+ if (prop?.enum !== undefined)
30
+ node.enum = prop.enum;
31
+ properties[key] = node;
32
+ }
33
+ return { type: 'object', properties, ...(required.length > 0 ? { required } : {}) };
34
+ }
35
+ const findingSchema = {
36
+ type: 'object',
37
+ properties: {
38
+ id: { type: 'string' }, ruleId: { type: 'string' }, title: { type: 'string' }, cwe: { type: 'string' },
39
+ severity: { type: 'string' }, confidence: { type: 'string' }, file: { type: 'string' }, line: { type: 'integer' },
40
+ snippet: { type: 'string' }, message: { type: 'string' },
41
+ },
42
+ additionalProperties: true,
43
+ };
44
+ const countsSchema = {
45
+ type: 'object',
46
+ properties: { critical: { type: 'integer' }, high: { type: 'integer' }, medium: { type: 'integer' }, low: { type: 'integer' } },
47
+ additionalProperties: true,
48
+ };
49
+ const scanSchema = {
50
+ type: 'object',
51
+ properties: {
52
+ root: { type: 'string' }, filesScanned: { type: 'integer' }, filesSkipped: { type: 'integer' },
53
+ findings: { type: 'array', items: findingSchema }, counts: countsSchema, passed: { type: 'boolean' },
54
+ failOn: { type: 'string' }, durationMs: { type: 'integer' },
55
+ newFindings: { type: 'array', items: findingSchema }, newCounts: countsSchema,
56
+ acceptedFindings: { type: 'array', items: findingSchema },
57
+ baseline: { type: 'object', additionalProperties: true },
58
+ },
59
+ additionalProperties: true,
60
+ };
61
+ function verdictOf(findings, failOn) {
62
+ const threshold = SEVERITY_ORDER[failOn];
63
+ return !findings.some((finding) => SEVERITY_ORDER[finding.severity] >= threshold);
64
+ }
65
+ function baselineSummary(value) {
66
+ const rec = (value ?? {});
67
+ return {
68
+ fresh: Array.isArray(rec.newFindings) ? rec.newFindings : [],
69
+ accepted: Array.isArray(rec.acceptedFindings) ? rec.acceptedFindings : [],
70
+ };
71
+ }
72
+ function renderFindings(value) {
73
+ const rec = (value ?? {});
74
+ const findings = Array.isArray(rec.findings) ? rec.findings : [];
75
+ const counts = rec.counts;
76
+ const split = baselineSummary(value);
77
+ const lines = [
78
+ '安全审查:' + (rec.passed === true ? '通过' : '未通过'),
79
+ '文件 ' + String(rec.filesScanned ?? 0) + ' 个,发现 ' + findings.length + ' 个问题(critical ' + String(counts?.critical ?? 0) + ' / high ' + String(counts?.high ?? 0) + ' / medium ' + String(counts?.medium ?? 0) + ' / low ' + String(counts?.low ?? 0) + ');基线外新增 ' + split.fresh.length + ' 个。',
80
+ ];
81
+ for (const finding of findings) {
82
+ lines.push('- ' + finding.file + ':' + finding.line + ' [' + finding.severity + '] ' + finding.ruleId + ' ' + finding.title);
83
+ }
84
+ return [{ type: 'text', text: lines.join('\n') }];
85
+ }
86
+ export function buildSecureTools(cfg, cwd, runner) {
87
+ const stateDir = cfg.stateDir;
88
+ const secureScan = {
89
+ name: 'secure_scan',
90
+ description: '扫描工作区(或指定文件/目录),用确定性规则检测注入、弱加密、硬编码密钥、危险配置等问题。结果写入 .code-security 状态供 secure_fix_verify 对比。',
91
+ parameters: compileParameters({
92
+ target: { type: 'string', description: '要扫描的文件或目录(可选,缺省扫描整个工作区)。' },
93
+ }),
94
+ output: { schema: scanSchema, render: renderFindings },
95
+ async execute(rawArgs) {
96
+ const args = (rawArgs ?? {});
97
+ const target = optionalString(args, 'target');
98
+ const policy = await loadPolicy(cwd);
99
+ const failOn = policy.failOn ?? cfg.failOn;
100
+ const result = await scanPath({ cwd, target, maxFiles: cfg.maxFiles, maxFileBytes: cfg.maxFileBytes, policy });
101
+ const state = await loadState(stateDir);
102
+ const counts = countFindings(result.findings);
103
+ const split = splitByBaseline(result.findings, state.baseline);
104
+ const entry = {
105
+ mode: target === undefined ? 'scan' : 'scan:' + target,
106
+ target: target ?? '.',
107
+ time: new Date().toISOString(),
108
+ filesScanned: result.filesScanned,
109
+ findings: result.findings,
110
+ fingerprints: result.findings.map(findingFingerprint),
111
+ counts,
112
+ };
113
+ const stateFile = await saveState(stateDir, entry);
114
+ return { ...result, counts, newFindings: split.fresh, newCounts: countFindings(split.fresh), acceptedFindings: split.accepted, baseline: state.baseline, passed: verdictOf(split.fresh, failOn), failOn, stateFile };
115
+ },
116
+ timeoutMs: 120000,
117
+ };
118
+ const secureDiff = {
119
+ name: 'secure_diff',
120
+ description: '只审查 git diff 的新增行(默认 HEAD,即未提交改动)。base 可用任意 git ref;staged=true 审查已暂存改动;target 可限定文件。结果同样写入状态。',
121
+ parameters: compileParameters({
122
+ base: { type: 'string', description: 'git 基线(默认 HEAD)。' },
123
+ target: { type: 'string', description: '限定文件路径(可选)。' },
124
+ staged: { type: 'boolean', description: '审查已暂存改动(git diff --cached,默认 false)。' },
125
+ }),
126
+ output: { schema: scanSchema, render: renderFindings },
127
+ async execute(rawArgs) {
128
+ const args = (rawArgs ?? {});
129
+ const base = optionalString(args, 'base') ?? 'HEAD';
130
+ const target = optionalString(args, 'target');
131
+ const policy = await loadPolicy(cwd);
132
+ const failOn = policy.failOn ?? cfg.failOn;
133
+ const diff = await runGitDiff(runner, cwd, base, target, 30000, args.staged === true);
134
+ const result = scanDiff(diff, cwd, policy);
135
+ const state = await loadState(stateDir);
136
+ const counts = countFindings(result.findings);
137
+ const split = splitByBaseline(result.findings, state.baseline);
138
+ const entry = {
139
+ mode: 'diff:' + base,
140
+ target: target ?? '.',
141
+ time: new Date().toISOString(),
142
+ filesScanned: result.filesChanged,
143
+ findings: result.findings,
144
+ fingerprints: result.findings.map(findingFingerprint),
145
+ counts,
146
+ };
147
+ await saveState(stateDir, entry);
148
+ return { root: base, filesScanned: result.filesChanged, filesSkipped: 0, addedLines: result.addedLines, findings: result.findings, counts, newFindings: split.fresh, newCounts: countFindings(split.fresh), acceptedFindings: split.accepted, baseline: state.baseline, passed: verdictOf(split.fresh, failOn), failOn, durationMs: 0 };
149
+ },
150
+ timeoutMs: 60000,
151
+ };
152
+ const secureFixVerify = {
153
+ name: 'secure_fix_verify',
154
+ description: '复扫并与上次扫描基线对比,输出已关闭、仍存在、新引入的问题指纹。用于修复后确认没有按下葫芦浮起瓢。',
155
+ parameters: compileParameters({
156
+ target: { type: 'string', description: '复扫文件或目录(可选,缺省扫描整个工作区)。' },
157
+ }),
158
+ output: {
159
+ schema: {
160
+ type: 'object',
161
+ properties: {
162
+ baselineMissing: { type: 'boolean' }, baselineTime: { type: 'string' },
163
+ closedCount: { type: 'integer' }, remainingCount: { type: 'integer' }, freshCount: { type: 'integer' },
164
+ closed: { type: 'array', items: findingSchema }, remaining: { type: 'array', items: findingSchema }, fresh: { type: 'array', items: findingSchema },
165
+ passed: { type: 'boolean' },
166
+ },
167
+ additionalProperties: true,
168
+ },
169
+ render: (_args, value) => {
170
+ const rec = (value ?? {});
171
+ return [{ type: 'text', text: '修复验证:关闭 ' + String(rec.closedCount ?? 0) + ',仍存在 ' + String(rec.remainingCount ?? 0) + ',新增 ' + String(rec.freshCount ?? 0) + '。' }];
172
+ },
173
+ },
174
+ async execute(rawArgs) {
175
+ const args = (rawArgs ?? {});
176
+ const target = optionalString(args, 'target');
177
+ const policy = await loadPolicy(cwd);
178
+ const state = await loadState(stateDir);
179
+ const result = await scanPath({ cwd, target, maxFiles: cfg.maxFiles, maxFileBytes: cfg.maxFileBytes, policy });
180
+ if (state.last === null) {
181
+ return { baselineMissing: true, baselineTime: '', closedCount: 0, remainingCount: 0, freshCount: result.findings.length, closed: [], remaining: [], fresh: result.findings, passed: false };
182
+ }
183
+ const current = result.findings.map(findingFingerprint);
184
+ const cmp = compareFingerprints(state.last.fingerprints, current);
185
+ const byFingerprint = new Map(result.findings.map((finding) => [findingFingerprint(finding), finding]));
186
+ const closed = cmp.closed.map((fp) => state.last.findings.find((finding) => findingFingerprint(finding) === fp)).filter((x) => x !== undefined);
187
+ const remaining = cmp.remaining.map((fp) => byFingerprint.get(fp)).filter((x) => x !== undefined);
188
+ const fresh = cmp.fresh.map((fp) => byFingerprint.get(fp)).filter((x) => x !== undefined);
189
+ return {
190
+ baselineMissing: false,
191
+ baselineTime: state.last.time,
192
+ closedCount: closed.length,
193
+ remainingCount: remaining.length,
194
+ freshCount: fresh.length,
195
+ closed, remaining, fresh,
196
+ passed: fresh.length === 0 && remaining.length === 0,
197
+ };
198
+ },
199
+ timeoutMs: 120000,
200
+ };
201
+ const secureReport = {
202
+ name: 'secure_report',
203
+ description: '汇总最近一次扫描状态与策略:按严重度/规则/文件聚合,返回门禁结论与历史趋势。',
204
+ parameters: compileParameters({}),
205
+ output: {
206
+ schema: {
207
+ type: 'object',
208
+ properties: {
209
+ hasScan: { type: 'boolean' }, stateFile: { type: 'string' }, policy: { type: 'object', additionalProperties: true },
210
+ counts: countsSchema, byRule: { type: 'array', items: { type: 'object', additionalProperties: true } },
211
+ byFile: { type: 'array', items: { type: 'object', additionalProperties: true } },
212
+ passed: { type: 'boolean' }, historyCount: { type: 'integer' },
213
+ },
214
+ additionalProperties: true,
215
+ },
216
+ render: (_args, value) => {
217
+ const rec = (value ?? {});
218
+ return [{ type: 'text', text: '安全报告:' + (rec.hasScan === true ? '最近扫描包含 ' + String(rec.counts?.critical ?? 0) + ' 个 critical。' : '尚无扫描记录,请先 secure_scan。') }];
219
+ },
220
+ },
221
+ async execute() {
222
+ const state = await loadState(stateDir);
223
+ const policy = await loadPolicy(cwd);
224
+ if (state.last === null) {
225
+ return { hasScan: false, stateFile: '', policy, counts: { critical: 0, high: 0, medium: 0, low: 0 }, byRule: [], byFile: [], passed: false, historyCount: state.history.length };
226
+ }
227
+ const last = state.last;
228
+ const byRule = new Map();
229
+ const byFile = new Map();
230
+ for (const finding of last.findings) {
231
+ byRule.set(finding.ruleId, (byRule.get(finding.ruleId) ?? 0) + 1);
232
+ byFile.set(finding.file, (byFile.get(finding.file) ?? 0) + 1);
233
+ }
234
+ const failOn = policy.failOn ?? cfg.failOn;
235
+ const split = splitByBaseline(last.findings, state.baseline);
236
+ return {
237
+ hasScan: true,
238
+ stateFile: stateDir,
239
+ policy,
240
+ counts: last.counts,
241
+ baseline: state.baseline,
242
+ newCounts: countFindings(split.fresh),
243
+ acceptedCounts: countFindings(split.accepted),
244
+ byRule: [...byRule.entries()].sort((a, b) => b[1] - a[1]).map(([rule, count]) => ({ rule, count, category: ruleCategory(rule) })),
245
+ byFile: [...byFile.entries()].sort((a, b) => b[1] - a[1]).slice(0, 50).map(([file, count]) => ({ file, count })),
246
+ passed: verdictOf(split.fresh, failOn),
247
+ historyCount: state.history.length,
248
+ };
249
+ },
250
+ timeoutMs: 10000,
251
+ };
252
+ const policyShow = {
253
+ name: 'secure_policy_show',
254
+ description: '查看当前项目的 .code-security.json 策略:排除目录、忽略规则与门禁阈值。',
255
+ parameters: compileParameters({}),
256
+ output: {
257
+ schema: { type: 'object', properties: { file: { type: 'string' }, policy: { type: 'object', additionalProperties: true } }, additionalProperties: true },
258
+ render: (_args, value) => {
259
+ const rec = (value ?? {});
260
+ return [{ type: 'text', text: '策略文件:' + String(rec.file ?? '') }];
261
+ },
262
+ },
263
+ async execute() {
264
+ const policy = await loadPolicy(cwd);
265
+ return { file: path.join(cwd, POLICY_FILE), policy };
266
+ },
267
+ timeoutMs: 10000,
268
+ };
269
+ const policySet = {
270
+ name: 'secure_policy_set',
271
+ description: '写入新的 .code-security.json 策略(JSON 文本)。会整体替换当前策略,写操作默认需要审批。',
272
+ parameters: compileParameters({
273
+ policy: { type: 'string', required: true, description: '完整策略 JSON 文本(必填)。' },
274
+ }),
275
+ output: {
276
+ schema: { type: 'object', properties: { file: { type: 'string' }, policy: { type: 'object', additionalProperties: true } }, additionalProperties: true },
277
+ render: (_args, value) => {
278
+ const rec = (value ?? {});
279
+ return [{ type: 'text', text: '策略已写入:' + String(rec.file ?? '') }];
280
+ },
281
+ },
282
+ async execute(rawArgs) {
283
+ const args = (rawArgs ?? {});
284
+ const text = requiredString(args, 'policy', '策略 JSON');
285
+ let parsed;
286
+ try {
287
+ parsed = JSON.parse(text);
288
+ }
289
+ catch {
290
+ throw new Error('policy 参数不是合法 JSON。');
291
+ }
292
+ const policy = normalizePolicy((parsed ?? {}));
293
+ const file = await savePolicy(cwd, policy);
294
+ return { file, policy };
295
+ },
296
+ timeoutMs: 10000,
297
+ };
298
+ policySet.gate = async (exec, next) => {
299
+ const record = (typeof exec === 'object' && exec !== null ? exec : {});
300
+ const approval = record.approval;
301
+ if (approval === undefined)
302
+ return { kind: 'deny', reason: 'secure_policy_set 需要确认,但当前环境没有审批通道。如确需直接写入,请在受控终端手动编辑 .code-security.json。' };
303
+ const outcome = await approval.request({ reason: '覆盖写入项目 .code-security.json 安全策略' });
304
+ if (outcome === 'allowed-once')
305
+ return next();
306
+ if (outcome === 'cancelled')
307
+ return { kind: 'deny', reason: '策略写入被取消,未执行。' };
308
+ return { kind: 'deny', reason: '策略写入未获批准。' };
309
+ };
310
+ const secureExport = {
311
+ name: 'secure_export',
312
+ description: '把最近一次扫描结果导出为 SARIF 2.1.0 或 Markdown。path 可选:提供时写入文件(写操作需要审批)。',
313
+ parameters: compileParameters({
314
+ format: { type: 'string', enum: ['sarif', 'markdown'], description: '导出格式(默认 sarif)。' },
315
+ path: { type: 'string', description: '输出文件路径(可选,缺省只返回文本)。' },
316
+ }),
317
+ output: {
318
+ schema: {
319
+ type: 'object',
320
+ properties: { format: { type: 'string' }, path: { type: 'string' }, text: { type: 'string' }, findingCount: { type: 'integer' } },
321
+ additionalProperties: true,
322
+ },
323
+ render: (_args, value) => {
324
+ const rec = (value ?? {});
325
+ return [{ type: 'text', text: '已导出 ' + String(rec.findingCount ?? 0) + ' 条发现(' + String(rec.format ?? '') + ')。' }];
326
+ },
327
+ },
328
+ async execute(rawArgs) {
329
+ const args = (rawArgs ?? {});
330
+ const format = args.format === 'markdown' ? 'markdown' : 'sarif';
331
+ const target = optionalString(args, 'path');
332
+ const state = await loadState(stateDir);
333
+ if (state.last === null)
334
+ throw new Error('尚无扫描结果,请先执行 secure_scan 或 secure_diff。');
335
+ const text = format === 'sarif' ? buildSarif(state.last.findings, state.last.target) : buildMarkdown(state.last.findings, state.last.target);
336
+ if (target !== undefined)
337
+ await writeFile(target, text, 'utf8');
338
+ return { format, path: target ?? '', text, findingCount: state.last.findings.length };
339
+ },
340
+ timeoutMs: 30000,
341
+ };
342
+ secureExport.gate = async (exec, next) => {
343
+ const record = (typeof exec === 'object' && exec !== null ? exec : {});
344
+ const args = (typeof record.args === 'object' && record.args !== null ? record.args : {});
345
+ const target = typeof args.path === 'string' && args.path !== '' ? args.path : '';
346
+ if (target === '')
347
+ return next();
348
+ const approval = record.approval;
349
+ if (approval === undefined)
350
+ return { kind: 'deny', reason: 'secure_export 写入文件需要确认,但当前环境没有审批通道。' };
351
+ const outcome = await approval.request({ reason: '导出安全报告到 ' + target });
352
+ if (outcome === 'allowed-once')
353
+ return next();
354
+ if (outcome === 'cancelled')
355
+ return { kind: 'deny', reason: '导出被取消,未执行。' };
356
+ return { kind: 'deny', reason: '导出未获批准。' };
357
+ };
358
+ return [secureScan, secureDiff, secureFixVerify, secureReport, secureExport, ...buildExtraTools(cfg, cwd), policyShow, policySet];
359
+ }
360
+ function buildMarkdown(findings, target) {
361
+ const lines = ['# 安全审查报告', '', '目标:' + target, '', '| 严重度 | 规则 | CWE | 文件:行 | 说明 |', '| :-- | :-- | :-- | :-- | :-- |'];
362
+ for (const finding of findings) {
363
+ lines.push('| ' + finding.severity + ' | ' + finding.ruleId + ' | ' + finding.cwe + ' | ' + finding.file + ':' + finding.line + ' | ' + finding.message + ' |');
364
+ }
365
+ return lines.join('\n') + '\n';
366
+ }
367
+ function buildSarif(findings, target) {
368
+ const rules = new Map();
369
+ const results = findings.map((finding, index) => {
370
+ rules.set(finding.ruleId, { id: finding.ruleId, name: finding.title, shortDescription: finding.title });
371
+ return {
372
+ ruleId: finding.ruleId,
373
+ level: finding.severity === 'critical' || finding.severity === 'high' ? 'error' : finding.severity === 'medium' ? 'warning' : 'note',
374
+ message: { text: finding.message },
375
+ locations: [{ physicalLocation: { artifactLocation: { uri: finding.file.replace(/\\/g, '/') }, region: { startLine: finding.line } } }],
376
+ partialFingerprints: { primaryLocationLineHash: finding.id + ':' + String(index) },
377
+ };
378
+ });
379
+ return JSON.stringify({
380
+ version: '2.1.0',
381
+ $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
382
+ runs: [{
383
+ tool: { driver: { name: 'dsh-code-security', version: '0.1.0', informationUri: 'https://github.com/STARDUSTLC666/dsh-code-security', rules: [...rules.values()] } },
384
+ originalUriBaseIds: { ROOTPATH: { uri: 'file:///' + target.replace(/\\/g, '/') } },
385
+ results,
386
+ }],
387
+ }, null, 2);
388
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "dsh-code-security",
3
+ "version": "0.2.2",
4
+ "description": "DSH AI 代码安全审查插件:secure_scan/secure_diff/secure_fix_verify/secure_report/secure_export/secure_baseline/secure_deps/secure_policy_show/secure_policy_set 九工具,40+ 确定性规则、密钥熵检测、git diff/staged 审查、SARIF 导出、基线接受与 SBOM-lite,零运行时依赖。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./cordis.patch.yml": "./cordis.patch.yml",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "lib",
18
+ "cordis.patch.yml",
19
+ "README.md",
20
+ "README.en.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json",
24
+ "prepare": "tsc -p tsconfig.json",
25
+ "typecheck": "tsc -p tsconfig.json --noEmit",
26
+ "test": "pnpm run build && node --test \"test/*.test.mjs\"",
27
+ "prepublishOnly": "pnpm run build"
28
+ },
29
+ "dsh": {
30
+ "bundle": {
31
+ "patch": "./cordis.patch.yml"
32
+ }
33
+ },
34
+ "keywords": [
35
+ "dsh",
36
+ "deepseek-harness",
37
+ "plugin",
38
+ "security",
39
+ "code-review",
40
+ "sast",
41
+ "secrets",
42
+ "owasp"
43
+ ],
44
+ "license": "MIT",
45
+ "engines": {
46
+ "node": ">=22.13"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^24.0.0",
50
+ "typescript": "^5.6.0"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/STARDUSTLC666/dsh-code-security.git"
55
+ },
56
+ "bugs": {
57
+ "url": "https://github.com/STARDUSTLC666/dsh-code-security/issues"
58
+ },
59
+ "homepage": "https://github.com/STARDUSTLC666/dsh-code-security#readme",
60
+ "author": "stardustlc",
61
+ "packageManager": "pnpm@11.7.0"
62
+ }