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 +21 -0
- package/README.en.md +29 -27
- package/README.md +22 -67
- package/docs/CHANGELOG.en.md +15 -0
- package/docs/USAGE.en.md +31 -0
- package/docs/USAGE.md +77 -0
- package/docs/VALIDATION.md +15 -0
- package/lib/config.d.ts +1 -0
- package/lib/config.js +2 -1
- package/lib/diff.js +1 -1
- package/lib/extra.d.ts +1 -1
- package/lib/extra.js +2 -2
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/policy.d.ts +1 -1
- package/lib/policy.js +17 -5
- package/lib/runner.d.ts +2 -0
- package/lib/runner.js +4 -2
- package/lib/scanner.d.ts +1 -0
- package/lib/scanner.js +8 -1
- package/lib/state.d.ts +2 -2
- package/lib/state.js +72 -18
- package/lib/tools.d.ts +1 -0
- package/lib/tools.js +53 -23
- package/package.json +10 -4
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
|
-
|
|
3
|
+
[中文](README.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Run local static security checks on code or Git changes and verify fixes.
|
|
8
8
|
|
|
9
|
-

|
|
9
|
+
[](https://www.npmjs.com/package/dsh-code-security) [](https://www.npmjs.com/package/dsh-code-security)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## What it does
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
##
|
|
17
|
+
## Install
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
In DSH Desktop, install `dsh-code-security` from the Plugins panel. If the bundled dsh command is available:
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
```bash
|
|
22
|
+
dsh plugin --profile desktop add dsh-code-security
|
|
23
|
+
```
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
For the web version, replace `desktop` with `web`. Restart DSH after installation.
|
|
22
26
|
|
|
23
|
-
|
|
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
|
-
|
|
29
|
+
Ask: “Review the current Git diff for security findings, show evidence and recheck the fixes.”
|
|
36
30
|
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
46
|
+
[MIT](LICENSE)
|
package/README.md
CHANGED
|
@@ -1,91 +1,46 @@
|
|
|
1
1
|
# dsh-code-security
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[English](README.en.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
对代码与 Git 改动进行本地静态安全检查,并追踪修复结果。
|
|
8
8
|
|
|
9
|
-

|
|
9
|
+
[](https://www.npmjs.com/package/dsh-code-security) [](https://www.npmjs.com/package/dsh-code-security)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## 功能
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
|
|
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
|
|
22
|
+
dsh plugin --profile desktop add dsh-code-security
|
|
56
23
|
```
|
|
57
24
|
|
|
58
|
-
|
|
25
|
+
网页版把命令中的 `desktop` 改为 `web`。安装后重启 DSH。
|
|
59
26
|
|
|
27
|
+
## 开始使用
|
|
60
28
|
|
|
61
|
-
|
|
29
|
+
安装后可说:“审查当前 Git 改动的安全问题,给出位置和证据,再复查修复结果。”
|
|
62
30
|
|
|
63
|
-
|
|
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
|
-
|
|
33
|
+
默认在本机执行确定性规则检查。规则覆盖与基线、策略配置见使用说明。
|
|
72
34
|
|
|
73
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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
|
|
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.
|
package/docs/USAGE.en.md
ADDED
|
@@ -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
|
-
|
|
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-' +
|
|
48
|
-
|
|
49
|
-
|
|
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
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
|
|
21
|
+
signal,
|
|
20
22
|
});
|
|
21
23
|
const outcome = await handle.done;
|
|
22
24
|
return {
|
package/lib/scanner.d.ts
CHANGED
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
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-' +
|
|
56
|
-
|
|
57
|
-
|
|
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
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: '
|
|
92
|
-
parameters: compileParameters({
|
|
93
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
"description": "
|
|
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
|
}
|