dsh-code-security 0.3.4 → 0.3.7

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/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # 更新记录
2
+
3
+ [返回简介](README.md) · [使用说明](docs/USAGE.md) · [验证记录](docs/VALIDATION.md)
4
+
5
+ [历史英文记录](docs/CHANGELOG.en.md)
6
+
7
+ ## 0.3.7 (2026-10-05)
8
+
9
+ - 扫描、策略、报告和 git diff 使用当前会话工作区;支持取消并跳过目录联接。锁定合并历史与基线写入,损坏记录和无效策略明确报错并保留原文件。
10
+
11
+ ## 0.3.6 (2026-09-28)
12
+
13
+ - 更新官方 Harness 0.2.0-rc.1 的兼容声明和共同加载验证;运行时代码未变。验证范围见[验证记录](docs/VALIDATION.md)。
14
+
15
+ ## 0.3.5 (2026-09-28)
16
+
17
+ 扫描参数写错时明确报错,避免把 `paths`、`files` 或空目标误当成扫描整个工作区。修复标准模式中结果卡片误显示“0 个文件、未通过”的问题;文件数量、结论与问题列表现在使用实际扫描结果。
18
+
19
+ ## 更早的改动
20
+
21
+ 完整历史可查阅 [GitHub 提交记录](https://github.com/STARDUSTLC666/dsh-code-security/commits/master)。
package/README.en.md CHANGED
@@ -1,44 +1,46 @@
1
1
  # dsh-code-security
2
2
 
3
- ## 0.3.4 update (2026-09-27)
3
+ [中文](README.md)
4
4
 
5
- Fixes scan output validation when no baseline exists. SARIF now uses workspace file URIs, relative locations and structured rule descriptions, including paths with spaces and Unicode.
5
+ ![dsh-code-security whale girl plugin cover](https://raw.githubusercontent.com/STARDUSTLC666/dsh-code-security/master/assets/cover-whale-girl.png)
6
6
 
7
- Validation host: Harness 0.1.7-rc.2 built from official sources, retaining the local tool-scheduler fix. Build and automated checks pass; interactive coverage and external-service limits are recorded in this release round.
7
+ Run local static security checks on code or Git changes and verify fixes.
8
8
 
9
- ![npm](https://img.shields.io/npm/v/dsh-code-security) ![downloads](https://img.shields.io/npm/dm/dsh-code-security) ![license](https://img.shields.io/github/license/STARDUSTLC666/dsh-code-security) ![stars](https://img.shields.io/github/stars/STARDUSTLC666/dsh-code-security?style=social)
9
+ [![npm](https://img.shields.io/npm/v/dsh-code-security)](https://www.npmjs.com/package/dsh-code-security) [![downloads](https://raw.githubusercontent.com/STARDUSTLC666/dsh-suite/npm-downloads/assets/dsh-code-security-downloads.svg)](https://www.npmjs.com/package/dsh-code-security)
10
10
 
11
- > Every agent code change passes a local security scan before delivery.
11
+ ## What it does
12
12
 
13
- A DeepSeek Harness plugin for AI code security review: deterministic rule engine, git-diff incremental review, fix-verify loop, and policy gate. Methodology borrows from Codex security skills (evidence-first findings, severity ordering, supply-chain layers). Zero runtime dependencies.
13
+ - Scan files, directories or Git diffs with locations and code evidence.
14
+ - Recheck fixes and manage baselines or scan policy.
15
+ - Export Markdown or SARIF reports and inspect dependency manifests.
14
16
 
15
- ## Compatibility
17
+ ## Install
16
18
 
17
- Verified with official `@deepseek-ai/dsh@0.1.5-rc.1` and Node `24.16.0` on 2026-09-11: all 18 components load alongside Modlens, with passing tool-schema, skill-registration and offline read-only invocation checks. Uses the `cordis.patch.yml` + `dsh.bundle.patch` bundle model. Node requirements match this Harness release: 22.19 or later within 22.x, or 24 or later. Live external-service workflows require separate configuration and validation.
19
+ In DSH Desktop, install `dsh-code-security` from the Plugins panel. If the bundled dsh command is available:
18
20
 
19
- 2026-09-13 fix: retain the service receiver when calling `subprocess.spawn`, preventing failures caused by passing the method as an unbound callback. Verified with official `0.1.5-rc.1` and `0.1.5-rc.2` on Node `24.16.0`. `secure_diff` reads a real isolated Git repository through the host subprocess service and reports the changed file and added line.
21
+ ```bash
22
+ dsh plugin --profile desktop add dsh-code-security
23
+ ```
20
24
 
21
- ## Tools
25
+ For the web version, replace `desktop` with `web`. Restart DSH after installation.
22
26
 
23
- | Tool | Purpose | Write |
24
- | :-- | :-- | :-- |
25
- | `secure_scan` | Scan files with CWE/severity/line/snippet evidence | state |
26
- | `secure_diff` | Review only added lines of git diff | state |
27
- | `secure_fix_verify` | Compare with baseline: closed / remaining / fresh | state |
28
- | `secure_report` | Aggregate by rule/file with gate verdict | no |
29
- | `secure_export` | Export SARIF 2.1.0 / Markdown | file write approval |
30
- | `secure_baseline` | Accept current findings as baseline; gate on new issues only | approval |
31
- | `secure_deps` | SBOM-lite: parse dependency manifests and version-risk flags | no |
32
- | `secure_policy_show` | Show .code-security.json | no |
33
- | `secure_policy_set` | Replace policy JSON | approval |
27
+ ## Start using it
34
28
 
35
- 40+ deterministic rules: injection, deserialization, weak crypto (including shell TLS bypass flags), secrets, dangerous config, sensitive logging, path traversal, SSRF.
29
+ Ask: “Review the current Git diff for security findings, show evidence and recheck the fixes.”
36
30
 
37
- ```bash
38
- dsh plugin --profile web add dsh-code-security
39
- ```
31
+ ## Requirements and configuration
32
+
33
+ Deterministic rules run locally. See the usage guide for rule coverage, baselines and policy configuration.
34
+
35
+ Detailed configuration, tool arguments and troubleshooting are in the [usage guide](docs/USAGE.en.md). For standalone development, follow the Node requirement in [package.json](package.json).
36
+
37
+ ## Documentation
38
+
39
+ - [Usage and troubleshooting](docs/USAGE.en.md)
40
+ - [Changelog](CHANGELOG.md)
41
+ - [Validation scope and history](docs/VALIDATION.md)
42
+ - [Report a problem or suggest a feature](https://github.com/STARDUSTLC666/dsh-code-security/issues)
40
43
 
41
- MIT
42
44
  ## License
43
45
 
44
- MIT (see [LICENSE](LICENSE))
46
+ [MIT](LICENSE)
package/README.md CHANGED
@@ -1,91 +1,46 @@
1
1
  # dsh-code-security
2
2
 
3
- ## 0.3.4 更新(2026-09-27)
3
+ [English](README.en.md)
4
4
 
5
- 修复未设置基线时扫描结果被新版 Harness 拒绝的问题。SARIF 正确使用工作区文件 URI、相对路径和规则描述对象,支持空格与中文文件名。
5
+ ![dsh-code-security 鲸鱼娘插件封面](https://raw.githubusercontent.com/STARDUSTLC666/dsh-code-security/master/assets/cover-whale-girl.png)
6
6
 
7
- 验证宿主:官方源码构建的 Harness 0.1.7-rc.2(保留本地工具调度器修复)。构建与自动测试通过;实际操作和外部服务限制见本轮验收记录。
7
+ 对代码与 Git 改动进行本地静态安全检查,并追踪修复结果。
8
8
 
9
- ![npm](https://img.shields.io/npm/v/dsh-code-security) ![downloads](https://img.shields.io/npm/dm/dsh-code-security) ![license](https://img.shields.io/github/license/STARDUSTLC666/dsh-code-security) ![stars](https://img.shields.io/github/stars/STARDUSTLC666/dsh-code-security?style=social)
9
+ [![npm](https://img.shields.io/npm/v/dsh-code-security)](https://www.npmjs.com/package/dsh-code-security) [![downloads](https://raw.githubusercontent.com/STARDUSTLC666/dsh-suite/npm-downloads/assets/dsh-code-security-downloads.svg)](https://www.npmjs.com/package/dsh-code-security)
10
10
 
11
- > 让 agent 每次改完代码,先过一道本地安全扫描,修复完了才准交付。
11
+ ## 功能
12
12
 
13
- DeepSeek Harness AI 代码安全审查插件:确定性规则引擎 + git diff 增量审查 + 修复闭环 + 策略门禁。借鉴 Codex 安全技能的方法论(漏洞分级、证据优先、供应链分层、Agent 安全),实现为可复现工具。零运行时依赖。
14
-
15
- ## 工具
16
-
17
- | 工具 | 作用 | 写操作 |
18
- | :-- | :-- | :-- |
19
- | `secure_scan` | 扫描文件/目录,输出 CWE/严重度/行号/代码片段证据 | 写状态 |
20
- | `secure_diff` | 只审查 git diff 新增行 | 写状态 |
21
- | `secure_fix_verify` | 修复后复扫:关闭 / 仍存在 / 新引入 | 写状态 |
22
- | `secure_report` | 按规则/文件聚合 + 门禁结论 | 否 |
23
- | `secure_export` | 导出 SARIF 2.1.0 / Markdown 报告 | 写文件审批 |
24
- | `secure_baseline` | 接受当前已知问题为基线,之后只按新增判定 | 审批门 |
25
- | `secure_deps` | SBOM-lite:解析依赖清单与版本约束风险 | 否 |
26
- | `secure_policy_show` | 查看 .code-security.json 策略 | 否 |
27
- | `secure_policy_set` | 写入策略(排除/忽略/阈值) | 审批门 |
28
-
29
- ## 规则覆盖(40+)
30
-
31
- - 注入:eval / exec / shell=True / SQL 拼接 / innerHTML
32
- - 反序列化:pickle / yaml.load / ObjectInputStream / Marshal / unserialize
33
- - 加密:弱哈希、ECB、硬编码 IV/密钥、JWT none、TLS 校验关闭、Shell TLS 绕过(curl -k / wget --no-check-certificate / git sslVerify=false)
34
- - 凭据:硬编码密码、令牌、私钥、高熵密钥
35
- - 配置:chmod 777、Docker privileged / latest、npm audit=false、CORS *
36
- - 泄露:敏感日志、错误堆栈外发、路径穿越、SSRF
37
-
38
- 发现只包含客观证据与 CWE 编号,不附修复建议——修复方案由 agent 基于证据生成。
39
-
40
- ## 兼容性
41
-
42
- 已在官方 `@deepseek-ai/dsh@0.1.5-rc.1`、Node `24.16.0` 上验证(2026-09-11):18 个组件与 Modlens 同载,工具 schema、技能注册及离线只读调用检查通过。采用 `cordis.patch.yml` + `dsh.bundle.patch` 组合包模型。Node 要求与该版本 Harness 一致:22.19 及以上的 22.x,或 24 及以上。外部服务的实际业务操作需按各组件配置单独验证。
43
-
44
- 2026-09-13 修复:保留宿主 `subprocess.spawn` 的服务对象,避免方法独立传递后因 `this` 丢失而报错。已在官方 `0.1.5-rc.1` 和 `0.1.5-rc.2`、Node `24.16.0` 上验证。`secure_diff` 已通过真实宿主进程服务读取隔离 Git 仓库差异,正确识别变更文件和新增行。
13
+ - 扫描文件、目录或 Git diff,报告规则、位置与代码证据。
14
+ - 复扫修复结果,维护问题基线和检查策略。
15
+ - 导出 Markdown / SARIF 报告,检查依赖清单。
45
16
 
46
17
  ## 安装
47
18
 
48
- ```bash
49
- dsh plugin --profile web add dsh-code-security
50
- ```
51
-
52
- ## 卸载
19
+ 桌面版可在「插件」面板按包名 `dsh-code-security` 安装。已配置 dsh 命令时也可使用:
53
20
 
54
21
  ```bash
55
- dsh plugin --profile web remove dsh-code-security
22
+ dsh plugin --profile desktop add dsh-code-security
56
23
  ```
57
24
 
58
- 卸载后重启 Web 服务。如需彻底清理,可再手动删除自己 profile `cordis.patch.yml` 中覆盖的插件行。
25
+ 网页版把命令中的 `desktop` 改为 `web`。安装后重启 DSH。
59
26
 
27
+ ## 开始使用
60
28
 
61
- ## 使用
29
+ 安装后可说:“审查当前 Git 改动的安全问题,给出位置和证据,再复查修复结果。”
62
30
 
63
- ```text
64
- secure_scan { target: src }
65
- secure_diff { base: HEAD }
66
- secure_fix_verify { target: src }
67
- secure_baseline { reason: 历史遗留 }
68
- secure_deps { target: . }
69
- ```
31
+ ## 依赖与配置
70
32
 
71
- 策略示例(`.code-security.json`):
33
+ 默认在本机执行确定性规则检查。规则覆盖与基线、策略配置见使用说明。
72
34
 
73
- ```json
74
- {
75
- "version": 1,
76
- "exclude": ["vendor/**", "generated/**"],
77
- "ignore": [{ "ruleId": "SEC-206", "file": "test/**", "reason": "非安全敏感" }],
78
- "failOn": "high"
79
- }
80
- ```
35
+ 详细配置、工具参数与排错见[使用说明](docs/USAGE.md)。从源码独立开发时,Node 要求以 [package.json](package.json) 为准。
81
36
 
82
- ## 工程
37
+ ## 文档
83
38
 
84
- ```bash
85
- pnpm test # 构建 + 24 个测试
86
- ```
39
+ - [使用与排错](docs/USAGE.md)
40
+ - [更新记录](CHANGELOG.md)
41
+ - [验证范围与历史记录](docs/VALIDATION.md)
42
+ - [问题反馈与功能建议](https://github.com/STARDUSTLC666/dsh-code-security/issues)
87
43
 
88
- MIT
89
44
  ## License
90
45
 
91
- MIT(见 [LICENSE](LICENSE))
46
+ [MIT](LICENSE)
@@ -0,0 +1,15 @@
1
+ # Historical release notes
2
+
3
+ [Current changelog](../CHANGELOG.md) · [Overview](../README.en.md)
4
+
5
+ These English notes preserve the earlier translations. The main changelog contains the consolidated version history.
6
+
7
+ ## 0.3.7 (2026-10-05)
8
+
9
+ - Use the session workspace for scans, policy, reports and git diff. Respect cancellation and skip directory junctions. Lock history/baseline merges and preserve damaged files with explicit errors.
10
+
11
+ ## 0.3.5 (2026-09-28)
12
+
13
+ Rejects unknown scan arguments and empty targets before scanning or replacing state, preventing misspelled scopes from expanding to the whole workspace. Fixes standard-mode output incorrectly displaying zero files and a failed verdict: counts, verdicts and findings now come from the actual scan result.
14
+
15
+ Validation host: Harness `0.2.0-rc.1` built from official sources (commit `407e65c8`) with Node `24.16.0` on 2026-09-28. All 31 plugin tests pass in an isolated environment; all 18 plugins mount together in one host registering 10 tools, with tool schemas and health-check contracts passing. No live ports or external services were exercised in this round.
@@ -0,0 +1,31 @@
1
+ # dsh-code-security usage guide
2
+
3
+ [Overview](../README.en.md) · [Changelog](../CHANGELOG.md) · [Validation](VALIDATION.md)
4
+
5
+ ## Current improvements
6
+
7
+ Relative stateDir defaults to .code-security in the session workspace. Concurrent scans retain both histories. Damaged records must be inspected after backup rather than silently recreated. Scans skip symlinks and junctions; static rules do not replace a full security audit.
8
+
9
+ ## Tools
10
+
11
+ | Tool | Purpose | Write |
12
+ | :-- | :-- | :-- |
13
+ | `secure_scan` | Scan files with CWE/severity/line/snippet evidence | state |
14
+ | `secure_diff` | Review only added lines of git diff | state |
15
+ | `secure_fix_verify` | Compare with baseline: closed / remaining / fresh | state |
16
+ | `secure_report` | Aggregate by rule/file with gate verdict | no |
17
+ | `secure_export` | Export SARIF 2.1.0 / Markdown | file write approval |
18
+ | `secure_baseline` | Accept current findings as baseline; gate on new issues only | approval |
19
+ | `secure_deps` | SBOM-lite: parse dependency manifests and version-risk flags | no |
20
+ | `secure_policy_show` | Show .code-security.json | no |
21
+ | `secure_policy_set` | Replace policy JSON | approval |
22
+
23
+ 40+ deterministic rules: injection, deserialization, weak crypto (including shell TLS bypass flags), secrets, dangerous config, sensitive logging, path traversal, SSRF.
24
+
25
+ ```bash
26
+ dsh plugin --profile web add dsh-code-security
27
+ ```
28
+
29
+ ## License
30
+
31
+ MIT (see [LICENSE](../LICENSE))
package/docs/USAGE.md ADDED
@@ -0,0 +1,77 @@
1
+ # dsh-code-security 使用说明
2
+
3
+ [返回简介](../README.md) · [更新记录](../CHANGELOG.md) · [验证记录](VALIDATION.md)
4
+
5
+ ## 本次改进
6
+
7
+ 相对 stateDir 默认在会话工作区的 .code-security。并发扫描保留双方历史;记录损坏时先备份排查,不会默默重建。扫描不跟随符号链接或目录联接;静态规则不能代替完整安全审计。
8
+
9
+ ## 工具
10
+
11
+ | 工具 | 作用 | 写操作 |
12
+ | :-- | :-- | :-- |
13
+ | `secure_scan` | 扫描文件/目录,输出 CWE/严重度/行号/代码片段证据 | 写状态 |
14
+ | `secure_diff` | 只审查 git diff 新增行 | 写状态 |
15
+ | `secure_fix_verify` | 修复后复扫:关闭 / 仍存在 / 新引入 | 写状态 |
16
+ | `secure_report` | 按规则/文件聚合 + 门禁结论 | 否 |
17
+ | `secure_export` | 导出 SARIF 2.1.0 / Markdown 报告 | 写文件审批 |
18
+ | `secure_baseline` | 接受当前已知问题为基线,之后只按新增判定 | 审批门 |
19
+ | `secure_deps` | SBOM-lite:解析依赖清单与版本约束风险 | 否 |
20
+ | `secure_policy_show` | 查看 .code-security.json 策略 | 否 |
21
+ | `secure_policy_set` | 写入策略(排除/忽略/阈值) | 审批门 |
22
+
23
+ ## 规则覆盖(40+)
24
+
25
+ - 注入:eval / exec / shell=True / SQL 拼接 / innerHTML
26
+ - 反序列化:pickle / yaml.load / ObjectInputStream / Marshal / unserialize
27
+ - 加密:弱哈希、ECB、硬编码 IV/密钥、JWT none、TLS 校验关闭、Shell TLS 绕过(curl -k / wget --no-check-certificate / git sslVerify=false)
28
+ - 凭据:硬编码密码、令牌、私钥、高熵密钥
29
+ - 配置:chmod 777、Docker privileged / latest、npm audit=false、CORS *
30
+ - 泄露:敏感日志、错误堆栈外发、路径穿越、SSRF
31
+
32
+ 发现只包含客观证据与 CWE 编号,不附修复建议——修复方案由 agent 基于证据生成。
33
+
34
+ ## 安装
35
+
36
+ ```bash
37
+ dsh plugin --profile web add dsh-code-security
38
+ ```
39
+
40
+ ## 卸载
41
+
42
+ ```bash
43
+ dsh plugin --profile web remove dsh-code-security
44
+ ```
45
+
46
+ 卸载后重启 Web 服务。如需彻底清理,可再手动删除自己 profile `cordis.patch.yml` 中覆盖的插件行。
47
+
48
+ ## 使用
49
+
50
+ ```text
51
+ secure_scan { target: src }
52
+ secure_diff { base: HEAD }
53
+ secure_fix_verify { target: src }
54
+ secure_baseline { reason: 历史遗留 }
55
+ secure_deps { target: . }
56
+ ```
57
+
58
+ 策略示例(`.code-security.json`):
59
+
60
+ ```json
61
+ {
62
+ "version": 1,
63
+ "exclude": ["vendor/**", "generated/**"],
64
+ "ignore": [{ "ruleId": "SEC-206", "file": "test/**", "reason": "非安全敏感" }],
65
+ "failOn": "high"
66
+ }
67
+ ```
68
+
69
+ ## 工程
70
+
71
+ ```bash
72
+ pnpm test # 构建并执行测试
73
+ ```
74
+
75
+ ## License
76
+
77
+ MIT(见 [LICENSE](../LICENSE))
@@ -0,0 +1,15 @@
1
+ # dsh-code-security 验证记录
2
+
3
+ 本页整理原 README 的历史验证说明,保留当时的版本、日期与范围。自动测试、启动检查、浏览器操作和真实服务验收分别记录,不能相互替代。更详细的版本验收文件仍保留在仓库中。
4
+
5
+ ## 原中文记录
6
+
7
+ 2026-09-13 修复:保留宿主 `subprocess.spawn` 的服务对象,避免方法独立传递后因 `this` 丢失而报错。该路径已在隔离宿主进程服务中实测通过。`secure_diff` 已通过真实宿主进程服务读取隔离 Git 仓库差异,正确识别变更文件和新增行。
8
+
9
+ ## Original English record
10
+
11
+ 2026-09-13 fix: retain the service receiver when calling `subprocess.spawn`, preventing failures caused by passing the method as an unbound callback. Verified against a real isolated host subprocess service. `secure_diff` reads a real isolated Git repository through the host subprocess service and reports the changed file and added line.
12
+
13
+ ## 0.3.6 原验证说明
14
+
15
+ 验证宿主:官方源码构建的 Harness `0.2.0-rc.1`(commit `407e65c8`)+ Node `24.16.0`(2026-09-28)。31 项插件测试在隔离环境全部通过;同一个宿主里 18 个插件共同加载,注册 10 个工具,工具 schema 与健康检查契约通过。本轮未启用真实端口与外部服务。
package/lib/config.d.ts CHANGED
@@ -15,6 +15,7 @@ export interface ResolvedSecureConfig {
15
15
  maxFileBytes: number;
16
16
  failOn: Severity;
17
17
  stateDir: string;
18
+ stateDirRelative?: string;
18
19
  }
19
20
  export declare const DEFAULT_EXCLUDE_DIRS: string[];
20
21
  export declare function resolveConfig(config: SecureConfig | undefined | null, cwd?: string): ResolvedSecureConfig;
package/lib/config.js CHANGED
@@ -21,7 +21,8 @@ export function resolveConfig(config, cwd = process.cwd()) {
21
21
  }
22
22
  const failOn = cfg.failOn === 'critical' || cfg.failOn === 'high' || cfg.failOn === 'medium' || cfg.failOn === 'low' ? cfg.failOn : 'medium';
23
23
  const stateDir = typeof cfg.stateDir === 'string' && cfg.stateDir.trim() !== '' ? path.resolve(cwd, cfg.stateDir.trim()) : path.join(cwd, '.code-security');
24
- return { maxFiles, maxFileBytes, failOn, stateDir };
24
+ const rawStateDir = typeof cfg.stateDir === 'string' && cfg.stateDir.trim() ? cfg.stateDir.trim() : '.code-security';
25
+ return { maxFiles, maxFileBytes, failOn, stateDir, ...(path.isAbsolute(rawStateDir) ? {} : { stateDirRelative: rawStateDir }) };
25
26
  }
26
27
  export function optionalString(args, key) {
27
28
  const value = args[key];
package/lib/diff.js CHANGED
@@ -14,7 +14,7 @@ export async function runGitDiff(runner, cwd, base, target, timeoutMs = 30000, s
14
14
  argv.push('--no-ext-diff', '--unified=0', base);
15
15
  if (target !== undefined && target.trim() !== '')
16
16
  argv.push('--', target.trim());
17
- const result = await runner.run(argv, { timeoutMs });
17
+ const result = await runner.run(argv, { timeoutMs, cwd });
18
18
  if (result.exitCode !== 0 && result.exitCode !== 1) {
19
19
  throw new Error('git diff 失败(退出码 ' + String(result.exitCode) + '):' + result.stderr.trim().split(/\r?\n/).slice(-4).join(' | '));
20
20
  }
package/lib/extra.d.ts CHANGED
@@ -5,4 +5,4 @@
5
5
  */
6
6
  import { type ResolvedSecureConfig } from './config.js';
7
7
  import type { SecureToolDefinition } from './tools.js';
8
- export declare function buildExtraTools(cfg: ResolvedSecureConfig, cwd: string): SecureToolDefinition[];
8
+ export declare function buildExtraTools(cfg: ResolvedSecureConfig, cwd: string, signal?: AbortSignal): SecureToolDefinition[];
package/lib/extra.js CHANGED
@@ -22,7 +22,7 @@ function compileParameters(spec) {
22
22
  }
23
23
  return { type: 'object', properties, ...(required.length > 0 ? { required } : {}) };
24
24
  }
25
- export function buildExtraTools(cfg, cwd) {
25
+ export function buildExtraTools(cfg, cwd, signal) {
26
26
  const stateDir = cfg.stateDir;
27
27
  const secureBaseline = {
28
28
  name: 'secure_baseline',
@@ -51,7 +51,7 @@ export function buildExtraTools(cfg, cwd) {
51
51
  fingerprints: state.last.fingerprints,
52
52
  counts: state.last.counts,
53
53
  };
54
- const file = await saveBaseline(stateDir, baseline);
54
+ const file = await saveBaseline(stateDir, baseline, signal);
55
55
  return { file, acceptedCount: baseline.findings.length, baseline };
56
56
  },
57
57
  timeoutMs: 10000,
package/lib/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 插件导出 apply(ctx, config):注册 secure_scan / secure_diff / secure_fix_verify /
5
5
  * secure_report / secure_export / secure_policy_show / secure_policy_set 七个工具。扫描为纯确定性规则,
6
- * git diff 通过官方 subprocess 服务执行,零运行时依赖。
6
+ * git diff 通过官方 subprocess 服务执行,仅依赖文件锁库。
7
7
  *
8
8
  * @module dsh-code-security
9
9
  */
package/lib/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 插件导出 apply(ctx, config):注册 secure_scan / secure_diff / secure_fix_verify /
5
5
  * secure_report / secure_export / secure_policy_show / secure_policy_set 七个工具。扫描为纯确定性规则,
6
- * git diff 通过官方 subprocess 服务执行,零运行时依赖。
6
+ * git diff 通过官方 subprocess 服务执行,仅依赖文件锁库。
7
7
  *
8
8
  * @module dsh-code-security
9
9
  */
package/lib/policy.d.ts CHANGED
@@ -19,6 +19,6 @@ export declare const POLICY_FILE = ".code-security.json";
19
19
  export declare function policyPath(cwd: string): string;
20
20
  export declare function loadPolicy(cwd: string): Promise<SecurePolicy>;
21
21
  export declare function normalizePolicy(obj: Record<string, unknown>): SecurePolicy;
22
- export declare function savePolicy(cwd: string, policy: SecurePolicy): Promise<string>;
22
+ export declare function savePolicy(cwd: string, policy: SecurePolicy, signal?: AbortSignal): Promise<string>;
23
23
  export declare function policyExcludesDir(policy: SecurePolicy, rel: string): boolean;
24
24
  export declare function policyIgnores(policy: SecurePolicy, ruleId: string, file: string): boolean;
package/lib/policy.js CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import fs from 'node:fs/promises';
7
7
  import path from 'node:path';
8
+ import { randomUUID } from 'node:crypto';
8
9
  export const DEFAULT_POLICY = Object.freeze({
9
10
  version: 1,
10
11
  exclude: [],
@@ -21,7 +22,9 @@ export async function loadPolicy(cwd) {
21
22
  const obj = (parsed ?? {});
22
23
  return normalizePolicy(obj);
23
24
  }
24
- catch {
25
+ catch (error) {
26
+ if (error.code !== 'ENOENT')
27
+ throw new Error('安全策略无法读取或格式无效,原文件已保留:' + (error instanceof Error ? error.message : String(error)));
25
28
  return { ...DEFAULT_POLICY, exclude: [...DEFAULT_POLICY.exclude], ignore: [] };
26
29
  }
27
30
  }
@@ -33,6 +36,8 @@ export function normalizePolicy(obj) {
33
36
  if (typeof item !== 'object' || item === null)
34
37
  continue;
35
38
  const rec = item;
39
+ if (!(typeof rec.ruleId === 'string' && rec.ruleId.trim() !== '') && !(typeof rec.file === 'string' && rec.file.trim() !== ''))
40
+ throw new Error('每条忽略规则必须指定 ruleId 或 file,空规则不能禁用全部检查。');
36
41
  ignore.push({
37
42
  ruleId: typeof rec.ruleId === 'string' && rec.ruleId !== '' ? rec.ruleId : undefined,
38
43
  file: typeof rec.file === 'string' && rec.file !== '' ? rec.file : undefined,
@@ -42,11 +47,18 @@ export function normalizePolicy(obj) {
42
47
  const failOn = obj.failOn === 'critical' || obj.failOn === 'high' || obj.failOn === 'medium' || obj.failOn === 'low' ? obj.failOn : undefined;
43
48
  return { version: 1, exclude, ignore, failOn };
44
49
  }
45
- export async function savePolicy(cwd, policy) {
50
+ export async function savePolicy(cwd, policy, signal) {
51
+ signal?.throwIfAborted();
46
52
  const file = policyPath(cwd);
47
- const tmp = file + '.tmp-' + process.pid;
48
- await fs.writeFile(tmp, JSON.stringify(policy, null, 2) + '\n', 'utf8');
49
- await fs.rename(tmp, file);
53
+ const tmp = file + '.tmp-' + randomUUID();
54
+ try {
55
+ await fs.writeFile(tmp, JSON.stringify(policy, null, 2) + '\n', { flag: 'wx', mode: 0o600, signal });
56
+ signal?.throwIfAborted();
57
+ await fs.rename(tmp, file);
58
+ }
59
+ finally {
60
+ await fs.unlink(tmp).catch(() => { });
61
+ }
50
62
  return file;
51
63
  }
52
64
  function globToRegex(glob) {
package/lib/runner.d.ts CHANGED
@@ -12,6 +12,8 @@ export interface RunResult {
12
12
  export interface ProcessRunner {
13
13
  run(argv: readonly string[], options?: {
14
14
  timeoutMs?: number;
15
+ cwd?: string;
16
+ signal?: AbortSignal;
15
17
  }): Promise<RunResult>;
16
18
  }
17
19
  export interface SubprocessHandleLike {
package/lib/runner.js CHANGED
@@ -9,14 +9,16 @@ export function createSubprocessRunner(spawn, graceMs, defaultTimeoutMs) {
9
9
  async run(argv, options) {
10
10
  const timeoutMs = options?.timeoutMs ?? defaultTimeoutMs;
11
11
  const controller = new AbortController();
12
+ const signal = options?.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
13
+ signal.throwIfAborted();
12
14
  const timer = setTimeout(() => controller.abort(new Error('git diff timed out')), timeoutMs);
13
15
  try {
14
16
  const handle = spawn({
15
17
  argv,
16
- cwd: process.cwd(),
18
+ cwd: options?.cwd ?? process.cwd(),
17
19
  stdio: { stdin: 'ignore', stdout: { maxBytes: COLLECT_BYTES }, stderr: { maxBytes: COLLECT_BYTES } },
18
20
  graceMs,
19
- signal: controller.signal,
21
+ signal,
20
22
  });
21
23
  const outcome = await handle.done;
22
24
  return {
package/lib/scanner.d.ts CHANGED
@@ -30,6 +30,7 @@ export interface ScanOptions {
30
30
  maxFiles: number;
31
31
  maxFileBytes: number;
32
32
  policy: SecurePolicy;
33
+ signal?: AbortSignal;
33
34
  }
34
35
  export interface LineEntry {
35
36
  line: number;
package/lib/scanner.js CHANGED
@@ -179,14 +179,20 @@ export async function scanPath(options) {
179
179
  const findings = [];
180
180
  const relRoot = path.relative(cwd, root) || '.';
181
181
  const visit = async (absolute, rel) => {
182
+ options.signal?.throwIfAborted();
182
183
  let stat;
183
184
  try {
184
- stat = await fs.stat(absolute);
185
+ stat = await fs.lstat(absolute);
185
186
  }
186
187
  catch {
187
188
  filesSkipped += 1;
188
189
  return;
189
190
  }
191
+ options.signal?.throwIfAborted();
192
+ if (stat.isSymbolicLink()) {
193
+ filesSkipped += 1;
194
+ return;
195
+ }
190
196
  if (stat.isDirectory()) {
191
197
  if (DEFAULT_EXCLUDE_DIRS.includes(path.basename(absolute)) || policyExcludesDir(policy, rel))
192
198
  return;
@@ -226,6 +232,7 @@ export async function scanPath(options) {
226
232
  return;
227
233
  }
228
234
  filesScanned += 1;
235
+ options.signal?.throwIfAborted();
229
236
  const text = new TextDecoder('utf-8', { fatal: false }).decode(buffer);
230
237
  findings.push(...scanText(text, absolute, rel, policy));
231
238
  };
package/lib/state.d.ts CHANGED
@@ -40,8 +40,8 @@ export interface StateDocument {
40
40
  export declare function findingFingerprint(finding: Finding): string;
41
41
  export declare function statePath(stateDir: string): string;
42
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>;
43
+ export declare function saveState(stateDir: string, entry: ScanStateEntry, signal?: AbortSignal): Promise<string>;
44
+ export declare function saveBaseline(stateDir: string, baseline: BaselineEntry, signal?: AbortSignal): Promise<string>;
45
45
  export declare function countFindings(findings: Finding[]): {
46
46
  critical: number;
47
47
  high: number;
package/lib/state.js CHANGED
@@ -6,7 +6,8 @@
6
6
  */
7
7
  import fs from 'node:fs/promises';
8
8
  import path from 'node:path';
9
- import { createHash } from 'node:crypto';
9
+ import { createHash, randomUUID } from 'node:crypto';
10
+ import lockfile from 'proper-lockfile';
10
11
  const STATE_FILE = 'state.json';
11
12
  const HISTORY_LIMIT = 50;
12
13
  export function findingFingerprint(finding) {
@@ -18,43 +19,96 @@ export function statePath(stateDir) {
18
19
  }
19
20
  export async function loadState(stateDir) {
20
21
  try {
22
+ const info = await fs.lstat(statePath(stateDir));
23
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 16 * 1024 * 1024)
24
+ throw new Error('记录不是普通文件或超过 16 MiB');
21
25
  const text = await fs.readFile(statePath(stateDir), 'utf8');
22
26
  const parsed = JSON.parse(text);
23
27
  const obj = (parsed ?? {});
28
+ if (!Array.isArray(obj.history) || obj.history.some(entry => !isEntry(entry)) || obj.last !== null && !isEntry(obj.last) || obj.baseline !== undefined && obj.baseline !== null && !isBaseline(obj.baseline))
29
+ throw new Error('记录结构无效');
24
30
  const last = isEntry(obj.last) ? obj.last : null;
25
31
  const history = Array.isArray(obj.history) ? obj.history.filter(isEntry) : [];
26
32
  const baseline = isBaseline(obj.baseline) ? obj.baseline : null;
27
33
  return { last, history, baseline };
28
34
  }
29
- catch {
35
+ catch (error) {
36
+ if (error.code !== 'ENOENT')
37
+ throw new Error('安全扫描记录无法读取,原文件已保留:' + (error instanceof Error ? error.message : String(error)));
30
38
  return { last: null, history: [], baseline: null };
31
39
  }
32
40
  }
33
41
  function isEntry(value) {
34
- return typeof value === 'object' && value !== null && Array.isArray(value.findings);
42
+ if (typeof value !== 'object' || value === null)
43
+ return false;
44
+ const row = value;
45
+ return typeof row.mode === 'string' && typeof row.target === 'string' && typeof row.time === 'string'
46
+ && Number.isInteger(row.filesScanned) && row.filesScanned >= 0 && validFindings(row.findings)
47
+ && validFingerprints(row.fingerprints) && validCounts(row.counts);
35
48
  }
36
49
  function isBaseline(value) {
37
- return typeof value === 'object' && value !== null && Array.isArray(value.findings) && Array.isArray(value.fingerprints);
50
+ if (typeof value !== 'object' || value === null)
51
+ return false;
52
+ const row = value;
53
+ return typeof row.time === 'string' && typeof row.target === 'string' && typeof row.reason === 'string'
54
+ && validFindings(row.findings) && validFingerprints(row.fingerprints) && validCounts(row.counts);
38
55
  }
39
- export async function saveState(stateDir, entry) {
56
+ function validFingerprints(value) { return Array.isArray(value) && value.every(item => typeof item === 'string'); }
57
+ function validCounts(value) {
58
+ if (typeof value !== 'object' || value === null)
59
+ return false;
60
+ const counts = value;
61
+ return ['critical', 'high', 'medium', 'low'].every(key => typeof counts[key] === 'number' && Number.isInteger(counts[key]) && counts[key] >= 0);
62
+ }
63
+ function validFindings(value) {
64
+ return Array.isArray(value) && value.every(item => typeof item === 'object' && item !== null
65
+ && typeof item.ruleId === 'string' && typeof item.file === 'string' && typeof item.snippet === 'string'
66
+ && Number.isInteger(item.line) && item.line >= 1 && ['critical', 'high', 'medium', 'low'].includes(item.severity));
67
+ }
68
+ export async function saveState(stateDir, entry, signal) {
69
+ signal?.throwIfAborted();
40
70
  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);
71
+ const unlock = await lockfile.lock(stateDir, { retries: { retries: 30, minTimeout: 25, maxTimeout: 100 } });
72
+ try {
73
+ signal?.throwIfAborted();
74
+ const state = await loadState(stateDir);
75
+ state.last = entry;
76
+ state.history.unshift(entry);
77
+ state.history = state.history.slice(0, HISTORY_LIMIT);
78
+ return await writeState(stateDir, state, signal);
79
+ }
80
+ finally {
81
+ await unlock();
82
+ }
46
83
  }
47
- export async function saveBaseline(stateDir, baseline) {
84
+ export async function saveBaseline(stateDir, baseline, signal) {
85
+ signal?.throwIfAborted();
48
86
  await fs.mkdir(stateDir, { recursive: true });
49
- const state = await loadState(stateDir);
50
- state.baseline = baseline;
51
- return writeState(stateDir, state);
87
+ const unlock = await lockfile.lock(stateDir, { retries: { retries: 30, minTimeout: 25, maxTimeout: 100 } });
88
+ try {
89
+ signal?.throwIfAborted();
90
+ const state = await loadState(stateDir);
91
+ state.baseline = baseline;
92
+ return await writeState(stateDir, state, signal);
93
+ }
94
+ finally {
95
+ await unlock();
96
+ }
52
97
  }
53
- async function writeState(stateDir, state) {
98
+ async function writeState(stateDir, state, signal) {
99
+ signal?.throwIfAborted();
54
100
  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);
101
+ const tmp = file + '.tmp-' + randomUUID(), bytes = JSON.stringify(state, null, 2);
102
+ if (Buffer.byteLength(bytes) > 16 * 1024 * 1024)
103
+ throw new Error('安全扫描记录超过 16 MiB,本次没有保存。');
104
+ try {
105
+ await fs.writeFile(tmp, bytes, { flag: 'wx', mode: 0o600, signal });
106
+ signal?.throwIfAborted();
107
+ await fs.rename(tmp, file);
108
+ }
109
+ finally {
110
+ await fs.unlink(tmp).catch(() => { });
111
+ }
58
112
  return file;
59
113
  }
60
114
  export function countFindings(findings) {
package/lib/tools.d.ts CHANGED
@@ -18,6 +18,7 @@ export interface SecureToolDefinition {
18
18
  type: 'object';
19
19
  properties: Record<string, unknown>;
20
20
  required?: string[];
21
+ additionalProperties?: boolean;
21
22
  };
22
23
  output: {
23
24
  schema: Record<string, unknown>;
package/lib/tools.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * @module dsh-code-security/tools
7
7
  */
8
8
  import { writeFile } from 'node:fs/promises';
9
+ import { readFileSync } from 'node:fs';
9
10
  import path from 'node:path';
10
11
  import { pathToFileURL } from 'node:url';
11
12
  import { optionalString, requiredString } from './config.js';
@@ -16,6 +17,7 @@ import { ruleCategory } from './rules.js';
16
17
  import { scanPath } from './scanner.js';
17
18
  import { compareFingerprints, countFindings, findingFingerprint, loadState, saveState, splitByBaseline } from './state.js';
18
19
  const SEVERITY_ORDER = { critical: 4, high: 3, medium: 2, low: 1 };
20
+ const PACKAGE_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
19
21
  function compileParameters(spec) {
20
22
  const properties = {};
21
23
  const required = [];
@@ -33,6 +35,21 @@ function compileParameters(spec) {
33
35
  }
34
36
  return { type: 'object', properties, ...(required.length > 0 ? { required } : {}) };
35
37
  }
38
+ /** Reject ambiguous scopes before reading files, running git, or replacing scan state. */
39
+ function scanArguments(raw, allowed = ['target']) {
40
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
41
+ throw new Error('扫描参数必须是对象。请用 { target: "文件或目录" } 限定范围;扫描整个工作区请传 {}。');
42
+ }
43
+ const args = raw;
44
+ const unknown = Object.keys(args).filter(key => !allowed.includes(key));
45
+ if (unknown.length > 0) {
46
+ throw new Error('不支持的扫描参数:' + unknown.join('、') + '。请使用 target 指定一个文件或目录;仅省略 target 或使用 target: "." 才扫描整个工作区。');
47
+ }
48
+ if (Object.hasOwn(args, 'target') && (typeof args.target !== 'string' || args.target.trim() === '')) {
49
+ throw new Error('扫描参数 target 必须是非空的文件或目录字符串。扫描整个工作区请省略 target 或使用 "."。');
50
+ }
51
+ return args;
52
+ }
36
53
  const findingSchema = {
37
54
  type: 'object',
38
55
  properties: {
@@ -70,7 +87,7 @@ function baselineSummary(value) {
70
87
  accepted: Array.isArray(rec.acceptedFindings) ? rec.acceptedFindings : [],
71
88
  };
72
89
  }
73
- function renderFindings(value) {
90
+ function renderFindings(_args, value) {
74
91
  const rec = (value ?? {});
75
92
  const findings = Array.isArray(rec.findings) ? rec.findings : [];
76
93
  const counts = rec.counts;
@@ -85,20 +102,32 @@ function renderFindings(value) {
85
102
  return [{ type: 'text', text: lines.join('\n') }];
86
103
  }
87
104
  export function buildSecureTools(cfg, cwd, runner) {
105
+ return buildWorkspaceTools(cfg, cwd, runner).map(tool => ({ ...tool, async execute(args, exec) {
106
+ const context = exec;
107
+ const signal = context?.signal;
108
+ signal?.throwIfAborted();
109
+ const workspace = context?.agent?.session?.header?.cwd || cwd;
110
+ const resolved = { ...cfg, stateDir: cfg.stateDirRelative ? path.resolve(workspace, cfg.stateDirRelative) : cfg.stateDir };
111
+ const scopedRunner = { run: (argv, options) => runner.run(argv, { ...options, cwd: workspace, signal }) };
112
+ const selected = buildWorkspaceTools(resolved, workspace, scopedRunner, signal).find(row => row.name === tool.name);
113
+ return selected.execute(args, exec);
114
+ } }));
115
+ }
116
+ function buildWorkspaceTools(cfg, cwd, runner, signal) {
88
117
  const stateDir = cfg.stateDir;
89
118
  const secureScan = {
90
119
  name: 'secure_scan',
91
- description: '扫描工作区(或指定文件/目录),用确定性规则检测注入、弱加密、硬编码密钥、危险配置等问题。结果写入 .code-security 状态供 secure_fix_verify 对比。',
92
- parameters: compileParameters({
93
- target: { type: 'string', description: '要扫描的文件或目录(可选,缺省扫描整个工作区)。' },
94
- }),
120
+ description: '扫描工作区(或指定文件/目录),用确定性规则检测注入、弱加密、硬编码密钥、危险配置等问题。使用 target 字符串限定范围,不接受 paths/files 等参数;{} 表示整个工作区。结果写入 .code-security 状态供 secure_fix_verify 对比。',
121
+ parameters: { ...compileParameters({
122
+ target: { type: 'string', description: '要扫描的文件或目录(可选,缺省扫描整个工作区)。' },
123
+ }), additionalProperties: false },
95
124
  output: { schema: scanSchema, render: renderFindings },
96
125
  async execute(rawArgs) {
97
- const args = (rawArgs ?? {});
126
+ const args = scanArguments(rawArgs);
98
127
  const target = optionalString(args, 'target');
99
128
  const policy = await loadPolicy(cwd);
100
129
  const failOn = policy.failOn ?? cfg.failOn;
101
- const result = await scanPath({ cwd, target, maxFiles: cfg.maxFiles, maxFileBytes: cfg.maxFileBytes, policy });
130
+ const result = await scanPath({ cwd, target, maxFiles: cfg.maxFiles, maxFileBytes: cfg.maxFileBytes, policy, signal });
102
131
  const state = await loadState(stateDir);
103
132
  const counts = countFindings(result.findings);
104
133
  const split = splitByBaseline(result.findings, state.baseline);
@@ -111,7 +140,7 @@ export function buildSecureTools(cfg, cwd, runner) {
111
140
  fingerprints: result.findings.map(findingFingerprint),
112
141
  counts,
113
142
  };
114
- const stateFile = await saveState(stateDir, entry);
143
+ const stateFile = await saveState(stateDir, entry, signal);
115
144
  return { ...result, counts, newFindings: split.fresh, newCounts: countFindings(split.fresh), acceptedFindings: split.accepted, baseline: state.baseline, passed: verdictOf(split.fresh, failOn), failOn, stateFile };
116
145
  },
117
146
  timeoutMs: 120000,
@@ -119,14 +148,14 @@ export function buildSecureTools(cfg, cwd, runner) {
119
148
  const secureDiff = {
120
149
  name: 'secure_diff',
121
150
  description: '只审查 git diff 的新增行(默认 HEAD,即未提交改动)。base 可用任意 git ref;staged=true 审查已暂存改动;target 可限定文件。结果同样写入状态。',
122
- parameters: compileParameters({
123
- base: { type: 'string', description: 'git 基线(默认 HEAD)。' },
124
- target: { type: 'string', description: '限定文件路径(可选)。' },
125
- staged: { type: 'boolean', description: '审查已暂存改动(git diff --cached,默认 false)。' },
126
- }),
151
+ parameters: { ...compileParameters({
152
+ base: { type: 'string', description: 'git 基线(默认 HEAD)。' },
153
+ target: { type: 'string', description: '限定文件路径(可选)。' },
154
+ staged: { type: 'boolean', description: '审查已暂存改动(git diff --cached,默认 false)。' },
155
+ }), additionalProperties: false },
127
156
  output: { schema: scanSchema, render: renderFindings },
128
157
  async execute(rawArgs) {
129
- const args = (rawArgs ?? {});
158
+ const args = scanArguments(rawArgs, ['base', 'target', 'staged']);
130
159
  const base = optionalString(args, 'base') ?? 'HEAD';
131
160
  const target = optionalString(args, 'target');
132
161
  const policy = await loadPolicy(cwd);
@@ -145,7 +174,7 @@ export function buildSecureTools(cfg, cwd, runner) {
145
174
  fingerprints: result.findings.map(findingFingerprint),
146
175
  counts,
147
176
  };
148
- await saveState(stateDir, entry);
177
+ await saveState(stateDir, entry, signal);
149
178
  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 };
150
179
  },
151
180
  timeoutMs: 60000,
@@ -153,9 +182,9 @@ export function buildSecureTools(cfg, cwd, runner) {
153
182
  const secureFixVerify = {
154
183
  name: 'secure_fix_verify',
155
184
  description: '复扫并与上次扫描基线对比,输出已关闭、仍存在、新引入的问题指纹。用于修复后确认没有按下葫芦浮起瓢。',
156
- parameters: compileParameters({
157
- target: { type: 'string', description: '复扫文件或目录(可选,缺省扫描整个工作区)。' },
158
- }),
185
+ parameters: { ...compileParameters({
186
+ target: { type: 'string', description: '复扫文件或目录(可选,缺省扫描整个工作区)。' },
187
+ }), additionalProperties: false },
159
188
  output: {
160
189
  schema: {
161
190
  type: 'object',
@@ -173,11 +202,11 @@ export function buildSecureTools(cfg, cwd, runner) {
173
202
  },
174
203
  },
175
204
  async execute(rawArgs) {
176
- const args = (rawArgs ?? {});
205
+ const args = scanArguments(rawArgs);
177
206
  const target = optionalString(args, 'target');
178
207
  const policy = await loadPolicy(cwd);
179
208
  const state = await loadState(stateDir);
180
- const result = await scanPath({ cwd, target, maxFiles: cfg.maxFiles, maxFileBytes: cfg.maxFileBytes, policy });
209
+ const result = await scanPath({ cwd, target, maxFiles: cfg.maxFiles, maxFileBytes: cfg.maxFileBytes, policy, signal });
181
210
  if (state.last === null) {
182
211
  return { baselineMissing: true, baselineTime: '', closedCount: 0, remainingCount: 0, freshCount: result.findings.length, closed: [], remaining: [], fresh: result.findings, passed: false };
183
212
  }
@@ -291,7 +320,7 @@ export function buildSecureTools(cfg, cwd, runner) {
291
320
  throw new Error('policy 参数不是合法 JSON。');
292
321
  }
293
322
  const policy = normalizePolicy((parsed ?? {}));
294
- const file = await savePolicy(cwd, policy);
323
+ const file = await savePolicy(cwd, policy, signal);
295
324
  return { file, policy };
296
325
  },
297
326
  timeoutMs: 10000,
@@ -322,6 +351,7 @@ export function buildSecureTools(cfg, cwd, runner) {
322
351
  if (state.last === null)
323
352
  throw new Error('尚无扫描结果,请先执行 secure_scan 或 secure_diff。');
324
353
  const text = format === 'sarif' ? buildSarif(state.last.findings, cwd) : buildMarkdown(state.last.findings, state.last.target);
354
+ signal?.throwIfAborted();
325
355
  if (target !== undefined)
326
356
  await writeFile(target, text, 'utf8');
327
357
  return { format, path: target ?? '', text, findingCount: state.last.findings.length };
@@ -354,7 +384,7 @@ export function buildSecureTools(cfg, cwd, runner) {
354
384
  return { ok: true, plugin: 'dsh-code-security', checks };
355
385
  },
356
386
  };
357
- return [secureScan, secureDiff, secureFixVerify, secureReport, secureExport, ...buildExtraTools(cfg, cwd), policyShow, policySet, secureHealth];
387
+ return [secureScan, secureDiff, secureFixVerify, secureReport, secureExport, ...buildExtraTools(cfg, cwd, signal), policyShow, policySet, secureHealth];
358
388
  }
359
389
  function buildMarkdown(findings, target) {
360
390
  const lines = ['# 安全审查报告', '', '目标:' + target, '', '| 严重度 | 规则 | CWE | 文件:行 | 说明 |', '| :-- | :-- | :-- | :-- | :-- |'];
@@ -379,7 +409,7 @@ function buildSarif(findings, cwd) {
379
409
  version: '2.1.0',
380
410
  $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
381
411
  runs: [{
382
- tool: { driver: { name: 'dsh-code-security', version: '0.1.0', informationUri: 'https://github.com/STARDUSTLC666/dsh-code-security', rules: [...rules.values()] } },
412
+ tool: { driver: { name: 'dsh-code-security', version: PACKAGE_VERSION, informationUri: 'https://github.com/STARDUSTLC666/dsh-code-security', rules: [...rules.values()] } },
383
413
  originalUriBaseIds: { ROOTPATH: { uri: pathToFileURL(path.resolve(cwd) + path.sep).href } },
384
414
  results,
385
415
  }],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-code-security",
3
- "version": "0.3.4",
4
- "description": "DSH 代码安全审查插件:40+ 确定性规则与密钥熵检测,支持 git diff/staged 审查、SARIF 导出、基线接受与 SBOM-lite。",
3
+ "version": "0.3.7",
4
+ "description": "对代码与 Git 改动进行本地静态安全检查,并追踪修复结果。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -17,7 +17,9 @@
17
17
  "lib",
18
18
  "cordis.patch.yml",
19
19
  "README.md",
20
- "README.en.md"
20
+ "README.en.md",
21
+ "docs",
22
+ "CHANGELOG.md"
21
23
  ],
22
24
  "scripts": {
23
25
  "build": "tsc -p tsconfig.json",
@@ -46,6 +48,7 @@
46
48
  },
47
49
  "devDependencies": {
48
50
  "@types/node": "^24.0.0",
51
+ "@types/proper-lockfile": "4.1.4",
49
52
  "typescript": "^5.6.0"
50
53
  },
51
54
  "repository": {
@@ -57,5 +60,8 @@
57
60
  },
58
61
  "homepage": "https://github.com/STARDUSTLC666/dsh-code-security#readme",
59
62
  "author": "stardustlc",
60
- "packageManager": "pnpm@11.7.0"
63
+ "packageManager": "pnpm@11.7.0",
64
+ "dependencies": {
65
+ "proper-lockfile": "4.1.2"
66
+ }
61
67
  }