ccs-mcp-server 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,17 @@
1
- # Agent Runtime Guard · Agent 运行时全栈拦截
1
+ # CCS MCP Server · Agent 运行时验证与收据验签
2
2
 
3
3
  [![IETF Internet-Draft](https://img.shields.io/badge/IETF-draft--correctover--ccs-blue)](https://datatracker.ietf.org/doc/draft-correctover-ccs/)
4
- [![npm version](https://img.shields.io/npm/v/agent-runtime-guard.svg)](https://www.npmjs.com/package/agent-runtime-guard)
4
+ [![npm version](https://img.shields.io/npm/v/ccs-mcp-server.svg)](https://www.npmjs.com/package/ccs-mcp-server)
5
+ [![npm downloads](https://img.shields.io/npm/dm/ccs-mcp-server.svg)](https://www.npmjs.com/package/ccs-mcp-server)
5
6
 
6
7
  > **给 AI Agent 配一个运行时保安:它删库、偷密钥、闯内网、被注入诱导——当场拦下。**
7
8
  > 它是一道**运行时拦截层**,不是事后审计工具:危险动作在执行点就被挡住;每张 Ed25519 签名收据,都是拦截/放行时当场附带的裁决凭证。
8
9
 
10
+ > **本包同时以 `agent-runtime-guard` 之名发布**,两者是同一份代码、同一个 bin 入口。已装 `agent-runtime-guard` 的用户无需迁移。
11
+
9
12
  你给 Agent 接上 shell、网络、云凭证这些工具后,最大的风险是:**它手一抖,一条命令删掉整台机器;或被诱导,把密钥和数据偷偷传走。** 一个容易点过的"确认弹窗"挡不住。
10
13
 
11
- `agent-runtime-guard` 在 **Agent 与危险操作之间**站一道确定性的拦截层——工具调用、shell 命令、网络请求、输出结果全栈覆盖,危险动作在**真正执行前**被 fail-closed 拦下,无需人工点击。
14
+ `ccs-mcp-server` 在 **Agent 与危险操作之间**站一道确定性的拦截层——工具调用、shell 命令、网络请求、输出结果全栈覆盖,危险动作在**真正执行前**被 fail-closed 拦下,无需人工点击。
12
15
 
13
16
  - ✅ **零运行时依赖**,纯 Node.js 标准库(Node ≥ 18),无 install 脚本
14
17
  - ✅ **全栈拦截**:命令注入 / 路径穿越 / SSRF(含云元数据)/ 凭证外泄 / 提示注入 / 跨工具攻击链
@@ -18,10 +21,20 @@
18
21
 
19
22
  ---
20
23
 
21
- ## ⚡ 30 秒跑起来
24
+ ## ⚡ 一键安装
25
+
26
+ **Cursor:**
27
+
28
+ [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=ccs-mcp-server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImNjcy1tY3Atc2VydmVyIl19)
29
+
30
+ **VS Code:**
31
+
32
+ [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=ccs-mcp-server&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22ccs-mcp-server%22%5D%7D)
33
+
34
+ 手动安装:
22
35
 
23
36
  ```bash
24
- npx -y agent-runtime-guard
37
+ npx -y ccs-mcp-server
25
38
  ```
26
39
 
27
40
  把它加进任意 MCP 客户端的 `mcpServers` 配置:
@@ -29,9 +42,9 @@ npx -y agent-runtime-guard
29
42
  ```json
30
43
  {
31
44
  "mcpServers": {
32
- "agent-runtime-guard": {
45
+ "ccs-mcp-server": {
33
46
  "command": "npx",
34
- "args": ["-y", "agent-runtime-guard"]
47
+ "args": ["-y", "ccs-mcp-server"]
35
48
  }
36
49
  }
37
50
  }
@@ -41,12 +54,26 @@ npx -y agent-runtime-guard
41
54
 
42
55
  ---
43
56
 
57
+ ## 🔍 命令行自检
58
+
59
+ 不确定装没装对?不用起 MCP 客户端,直接问它:
60
+
61
+ ```bash
62
+ npx -y ccs-mcp-server --version # 打印版本号
63
+ npx -y ccs-mcp-server --tools # 列出全部工具及用途(不启动服务)
64
+ npx -y ccs-mcp-server --help # 完整用法
65
+ ```
66
+
67
+ `--tools` 会输出工具名与一句话说明,方便你在接入前先确认能力边界。
68
+
69
+ ---
70
+
44
71
  ## 三种用法
45
72
 
46
73
  | 场景 | 怎么用 |
47
74
  |---|---|
48
75
  | **MCP 客户端**(Claude Code、Cursor、Cline、Windsurf) | 配置上面的 `mcpServers`,工具调用先过保安 |
49
- | **DeepSeek Harness** | `dsh plugin add agent-runtime-guard`,默认 bundle patch 自动启用,住进 `tools/pre-execute` / `post-execute` |
76
+ | **DeepSeek Harness** | `dsh plugin add ccs-mcp-server`,默认 bundle patch 自动启用,住进 `tools/pre-execute` / `post-execute` |
50
77
  | **网络边界独立拦一道** | 用透明 stdio 代理 [`agent-runtime-proxy`](https://www.npmjs.com/package/agent-runtime-proxy) 包住目标 MCP server,不信宿主内任何插件 |
51
78
 
52
79
  ---
@@ -71,6 +98,7 @@ npx -y agent-runtime-guard
71
98
  | `audit_mcp_config` | 审计 MCP 配置的安全风险 |
72
99
  | `verify_intent_binding` | 校验实际参数与声明意图是否一致——零容忍、零 LLM,抓跨模型参数漂移(规划写 `amount:100`,执行写成 `amount:10000` → 拦截) |
73
100
  | `verify_receipt` | 离线校验 Ed25519 收据签名、检测篡改,可固定可信签名者 |
101
+ | `locate_tampering` | 字段级定位篡改点(RFC 6901 JSON Pointer),报出是哪个字段、多严重 |
74
102
 
75
103
  ## 证据链
76
104
 
@@ -78,7 +106,7 @@ npx -y agent-runtime-guard
78
106
 
79
107
  ## FAQ(给 AI Agent 配运行时保安,常见疑问)
80
108
 
81
- **黑名单为什么永远拦不全?** 危险命令可以用同义词、编码、拼接绕过,且新型调用不断出现。`agent-runtime-guard` 用语义判定(这段代码的意图是不是危险执行)而不是匹配关键词,未知攻击靠意图异常也能识别。
109
+ **黑名单为什么永远拦不全?** 危险命令可以用同义词、编码、拼接绕过,且新型调用不断出现。`ccs-mcp-server` 用语义判定(这段代码的意图是不是危险执行)而不是匹配关键词,未知攻击靠意图异常也能识别。
82
110
 
83
111
  **`Bash(prefix*)` 放行规则够安全吗?** 不够。`git status*` 这类前缀规则会被 `git status; rm -rf /`、`&&`、管道拼接绕过。判定应基于 shell 解析后的实际命令结构,而不是字符串前缀。
84
112
 
@@ -92,9 +120,19 @@ npx -y agent-runtime-guard
92
120
 
93
121
  ---
94
122
 
95
- ## 🇨🇳 免费验证之外 · 支付宝 SkillPay
123
+ ## 免费 vs 付费
124
+
125
+ 运行时验证、证据签发、收据验签**全部免费**,工具全量开放,无需注册。
126
+
127
+ 如果你需要**面向业务方的正式凭证**(而不是给开发者看的日志),下面三档在售:
128
+
129
+ | 档位 | 价格 | 你拿到什么 | 入口 |
130
+ |---|---|---|---|
131
+ | **单次扫描** | **¥9.9 / 次** | 上传你的 MCP 配置,出一份带评分的静态审计报告 + Ed25519 签名收据,可用于合规留档 | [correctover-pay](https://correctover-pay.pages.dev) |
132
+ | **配置安全体检** | **¥49.90 / 月** | 支付宝 SkillPay 货架订阅,MCP 配置静态审计与 SSRF 检测,按月出具凭证 | [支付宝 SkillPay 货架](https://skillpay.alipay.com/public/correctover) |
133
+ | **收据批量审计** | 按量 | 大量收据的哈希链验签与合规证据集打包,适合审计场景 | [支付宝 SkillPay 货架](https://skillpay.alipay.com/public/correctover) |
96
134
 
97
- 运行时验证与证据签发免费使用。如需人工深度审计或正式合规凭证,可在支付宝 SkillPay 货架获取:https://skillpay.alipay.com/public/correctover
135
+ > 三者都是**同一套引擎**的托管服务——免费版在自己机器上跑,付费版把结果和凭证托管出来。选哪个取决于你需要"自己看"还是"拿给别人看"。
98
136
 
99
137
  ## 协议背景
100
138
 
@@ -104,8 +142,10 @@ npx -y agent-runtime-guard
104
142
 
105
143
  ## Links
106
144
 
107
- - npm: https://www.npmjs.com/package/agent-runtime-guard
108
- - GitHub: https://github.com/DSHCorrectover/agent-runtime-guard
145
+ - npm: https://www.npmjs.com/package/ccs-mcp-server
146
+ - 同源别名 npm: https://www.npmjs.com/package/agent-runtime-guard
147
+ - 官网: https://correctover.com
148
+ - GitHub: https://github.com/DSHCorrectover/ccs-mcp-server
109
149
  - 透明代理: https://github.com/DSHCorrectover/agent-runtime-proxy
110
150
  - IETF Draft: https://datatracker.ietf.org/doc/draft-correctover-ccs/
111
151
  - PyPI 完整验证器: https://pypi.org/project/ccs-verifier/
package/dsh/cmdi.js ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @file Command injection detection — inspects subprocess argv and shell strings.
3
+ *
4
+ * Detects:
5
+ * - Shell metacharacter injection (;, |, &&, ||, backticks, $())
6
+ * - Reverse shell patterns
7
+ * - Download-and-execute chains (curl|wget | sh, pip install from URL)
8
+ * - Obfuscated commands (base64 -d | sh, hex encoding)
9
+ * - Fileless execution (/dev/tcp, /dev/shm)
10
+ */
11
+
12
+ const DANGEROUS_PATTERNS = [
13
+ // Shell chaining / injection
14
+ { pattern: /[;&|`$]\s*(?:\$\(|`|sh\b|bash\b|zsh\b|dash\b|python\b|perl\b|ruby\b|node\b)/i, weight: 5, label: 'shell metachar + interpreter' },
15
+ { pattern: /\|\s*(?:sh|bash|zsh|dash|python|perl|ruby|node)\b/i, weight: 6, label: 'pipe to interpreter' },
16
+ { pattern: /`[^`]{2,}`/, weight: 4, label: 'backtick command substitution' },
17
+ { pattern: /\$\([^)]{2,}\)/, weight: 4, label: '$() command substitution' },
18
+ { pattern: /;\s*(?:rm|mv|cp|chmod|chown|curl|wget|nc|bash|sh|python|cat|echo)\b/i, weight: 4, label: 'chained dangerous command' },
19
+
20
+ // Reverse shells
21
+ { pattern: /(?:bash|sh|zsh)\s+-i\s*>&?\s*\/dev\/(?:tcp|udp)\//i, weight: 7, label: 'reverse shell (/dev/tcp)' },
22
+ { pattern: /nc\b.{0,40}-e\s+(?:sh|bash|bin\/sh)/i, weight: 7, label: 'netcat reverse shell' },
23
+ { pattern: /ncat\b.{0,40}-e\s+(?:sh|bash)/i, weight: 7, label: 'ncat reverse shell' },
24
+ { pattern: /python.{0,60}socket.{0,40}(?:connect|send|recv)/i, weight: 6, label: 'python reverse shell' },
25
+
26
+ // Download-and-execute
27
+ { pattern: /(?:curl|wget)\b[^|;]{0,100}\|\s*(?:sh|bash|zsh|python|perl)/i, weight: 6, label: 'download-and-execute' },
28
+ { pattern: /(?:curl|wget)\b[^|;]{0,200}(?:-o|--output)[^|;]{0,60}(?:\/tmp|\/dev\/shm|\/var\/tmp)/i, weight: 4, label: 'download to temp (possible staging)' },
29
+
30
+ // Fileless / memory-only
31
+ { pattern: /\/dev\/(?:tcp|udp)\//i, weight: 5, label: '/dev/tcp or /dev/udp (network socket)' },
32
+ { pattern: /\/dev\/shm\//i, weight: 2, label: '/dev/shm (tmpfs, no disk trace)' },
33
+
34
+ // Obfuscation
35
+ { pattern: /base64\s+(?:-d|--decode)\s*\|?\s*(?:sh|bash|python|perl|eval)/i, weight: 6, label: 'base64 decode + execute' },
36
+ { pattern: /xxd\s+-p?\s*-r?\s*\|?\s*(?:sh|bash)/i, weight: 5, label: 'hex decode + execute' },
37
+ { pattern: /\\x[0-9a-fA-F]{2}\\x[0-9a-fA-F]{2}/, weight: 3, label: 'hex-encoded characters' },
38
+ { pattern: /\$\{IFS\}/, weight: 4, label: '${IFS} obfuscation' },
39
+ { pattern: /\$\{PATH:[\d:]+}/, weight: 4, label: 'PATH substring obfuscation' },
40
+
41
+ // Destructive
42
+ { pattern: /\brm\s+-[rf]+\s+(?:\/|~|\*|\.\.|\/\*|~\/\*)/i, weight: 7, label: 'destructive rm (root/home/glob)' },
43
+ { pattern: /\bmkfs\b/, weight: 7, label: 'mkfs (filesystem format)' },
44
+ { pattern: /\bdd\s+if=\/dev\/(?:zero|random|urandom)\s+of=\/dev\//i, weight: 7, label: 'dd to device' },
45
+ { pattern: /:\(\)\s*{\s*:\s*\|\s*:\s*&\s*};/, weight: 7, label: 'fork bomb' },
46
+
47
+ // Persistence
48
+ { pattern: /(?:>>|>)\s*(?:~\/)?\.(?:bashrc|zshrc|profile|bash_profile)/i, weight: 5, label: 'shell profile modification (persistence)' },
49
+ { pattern: /(?:chmod|chown)\s+[0-7]*777/, weight: 2, label: 'chmod 777 (overly permissive)' },
50
+
51
+ // Credential theft
52
+ { pattern: /cat\s+(?:~\/)?\.ssh\/id_rsa/, weight: 7, label: 'SSH private key read' },
53
+ { pattern: /cat\s+(?:~\/)?\.aws\/credentials/, weight: 7, label: 'AWS credentials read' },
54
+ { pattern: /(?:env|printenv)\s*\|/, weight: 3, label: 'environment dump piped' },
55
+ ];
56
+
57
+ /**
58
+ * Scan a command string for injection patterns.
59
+ * @param {string} command
60
+ * @param {number} [threshold=8]
61
+ * @returns {{ score: number, hits: Array<{label: string, weight: number}>, blocked: boolean, reason?: string }}
62
+ */
63
+ export function detectCommandInjection(command, threshold = 6) {
64
+ if (!command || typeof command !== 'string') return { score: 0, hits: [], blocked: false };
65
+
66
+ let score = 0;
67
+ const hits = [];
68
+
69
+ for (const { pattern, weight, label } of DANGEROUS_PATTERNS) {
70
+ pattern.lastIndex = 0;
71
+ if (pattern.test(command)) {
72
+ score += weight;
73
+ hits.push({ label, weight });
74
+ }
75
+ }
76
+
77
+ const blocked = score >= threshold;
78
+ return {
79
+ score,
80
+ hits,
81
+ blocked,
82
+ reason: blocked ? `command injection detected (score ${score} ≥ ${threshold}): ${hits.map(h => h.label).join(', ')}` : undefined,
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Inspect a subprocess spawn spec for dangerous commands.
88
+ * @param {object} spec - spawn spec with argv and/or command.
89
+ * @param {number} [threshold]
90
+ * @returns {{ score: number, hits: Array<{label: string, weight: number}>, blocked: boolean, reason?: string }}
91
+ */
92
+ export function inspectSpawn(spec, threshold = 6) {
93
+ const argv = Array.isArray(spec?.argv) ? spec.argv : [];
94
+ const command = typeof spec?.command === 'string' ? spec.command : '';
95
+ // Join argv into a string for pattern matching
96
+ const combined = command || argv.join(' ');
97
+ return detectCommandInjection(combined, threshold);
98
+ }
@@ -0,0 +1,5 @@
1
+ # agent-runtime-guard DSH plugin bundle layer:
2
+ # auto-mounts when installed via `dsh plugin add agent-runtime-guard`.
3
+ - insert:
4
+ - id: agent-runtime-guard
5
+ name: agent-runtime-guard/dsh
@@ -0,0 +1,169 @@
1
+ /**
2
+ * @file Credential protection — detects and blocks credential exfiltration.
3
+ *
4
+ * Scans tool arguments and subprocess commands for:
5
+ * - API keys, tokens, passwords in environment variables or arguments
6
+ * - Credential file paths (~/.ssh, ~/.aws/credentials, .credentials.yaml)
7
+ * - Patterns that look like secrets being sent to network destinations
8
+ */
9
+
10
+ /** Patterns that indicate credential-like values. */
11
+ const SECRET_PATTERNS = [
12
+ // OpenAI / Anthropic / generic API keys
13
+ { pattern: /sk-[A-Za-z0-9_-]{20,}/g, label: 'API key (sk-...)' },
14
+ { pattern: /ghp_[A-Za-z0-9]{36,}/g, label: 'GitHub token' },
15
+ { pattern: /gho_[A-Za-z0-9]{36,}/g, label: 'GitHub OAuth token' },
16
+ { pattern: /github_pat_[A-Za-z0-9_]{82,}/g, label: 'GitHub fine-grained PAT' },
17
+ { pattern: /xox[baprs]-[A-Za-z0-9-]{10,}/g, label: 'Slack token' },
18
+ { pattern: /AKIA[0-9A-Z]{16}/g, label: 'AWS access key' },
19
+ { pattern: /ASIA[0-9A-Z]{16}/g, label: 'AWS session token' },
20
+ // Private key blocks
21
+ { pattern: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/g, label: 'private key block' },
22
+ // Generic high-entropy assignment patterns
23
+ { pattern: /(?:api[_-]?key|secret|token|password|passwd|auth|credential)["'\s:=]+["']?[A-Za-z0-9_\-]{16,}["']?/gi, label: 'credential assignment' },
24
+ ];
25
+
26
+ /** Credential file path patterns that should never be read by untrusted tools. */
27
+ const CREDENTIAL_PATHS = [
28
+ /\/\.ssh\//,
29
+ /\/\.aws\/credentials/,
30
+ /\/\.gnupg\//,
31
+ /\/\.netrc$/,
32
+ /\/\.pgpass$/,
33
+ /\.credentials\.ya?ml/,
34
+ /\/\.env(\.|$)/,
35
+ /\/\.docker\/config\.json/,
36
+ /\/\.kube\/config/,
37
+ /\/\.config\/gcloud\//,
38
+ /\/\.azure\//,
39
+ /id_rsa\b/,
40
+ /id_ed25519\b/,
41
+ /authorized_keys\b/,
42
+ ];
43
+
44
+ /** Process environment variable names that hold secrets. */
45
+ const SECRET_ENV_VARS = /^(?:.*_)?(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|CREDENTIAL|AUTH|PRIVATE_KEY|ACCESS_KEY|SECRET_KEY)$/i;
46
+
47
+ /**
48
+ * Scan a value (recursively) for secret patterns.
49
+ * @param {unknown} value - value to scan.
50
+ * @returns {Array<{label: string, snippet: string}>} findings.
51
+ */
52
+ export function scanForSecrets(value) {
53
+ const findings = [];
54
+ const seen = new WeakSet();
55
+
56
+ function walk(val, path) {
57
+ if (val === null || val === undefined) return;
58
+ if (typeof val === 'string') {
59
+ for (const { pattern, label } of SECRET_PATTERNS) {
60
+ pattern.lastIndex = 0;
61
+ let match;
62
+ while ((match = pattern.exec(val)) !== null) {
63
+ // Redact in snippet
64
+ const start = Math.max(0, match.index - 4);
65
+ const end = Math.min(val.length, match.index + match[0].length + 4);
66
+ const snippet = val.slice(start, end).replace(match[0], '[REDACTED]');
67
+ findings.push({ label, snippet: `...${snippet}...` });
68
+ }
69
+ }
70
+ } else if (typeof val === 'object') {
71
+ if (seen.has(val)) return;
72
+ seen.add(val);
73
+ if (Array.isArray(val)) {
74
+ val.forEach((v, i) => walk(v, `${path}[${i}]`));
75
+ } else {
76
+ for (const [k, v] of Object.entries(val)) {
77
+ // Check if the key name itself is secret-like
78
+ if (SECRET_ENV_VARS.test(k) && typeof v === 'string' && v.length > 4) {
79
+ findings.push({ label: `secret env/field "${k}"`, snippet: '[REDACTED]' });
80
+ }
81
+ walk(v, path ? `${path}.${k}` : k);
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ walk(value, '');
88
+ return findings;
89
+ }
90
+
91
+ /**
92
+ * Redact every known secret pattern inside a text string.
93
+ * Used by the post-execute hook to return cleaned output via
94
+ * `{ kind: 'accept', content }` under the v0.2.1 PostToolDecision contract.
95
+ *
96
+ * @param {string} text - text to clean.
97
+ * @returns {{ text: string, found: number, labels: string[] }} cleaned text,
98
+ * match count, and de-duplicated pattern labels.
99
+ */
100
+ export function redactSecretsInText(text) {
101
+ if (typeof text !== 'string' || text === '') return { text: text ?? '', found: 0, labels: [] };
102
+ let out = text;
103
+ let found = 0;
104
+ const labels = [];
105
+ for (const { pattern, label } of SECRET_PATTERNS) {
106
+ pattern.lastIndex = 0;
107
+ out = out.replace(pattern, (match) => {
108
+ found++;
109
+ if (!labels.includes(label)) labels.push(label);
110
+ return '[REDACTED]';
111
+ });
112
+ }
113
+ return { text: out, found, labels };
114
+ }
115
+
116
+ /**
117
+ * Check whether a file path targets a known credential location.
118
+ * @param {string} filePath
119
+ * @returns {string|null} matched pattern label, or null.
120
+ */
121
+ export function isCredentialPath(filePath) {
122
+ for (const pattern of CREDENTIAL_PATHS) {
123
+ if (pattern.test(filePath)) {
124
+ return pattern.source;
125
+ }
126
+ }
127
+ return null;
128
+ }
129
+
130
+ /**
131
+ * Check whether a subprocess command or environment includes secrets.
132
+ * @param {object} spec - subprocess spawn spec.
133
+ * @returns {Array<{label: string, detail: string}>} findings.
134
+ */
135
+ export function scanSubprocessForCredentials(spec) {
136
+ const findings = [];
137
+
138
+ // Check argv for secret patterns
139
+ const argv = Array.isArray(spec?.argv) ? spec.argv : [];
140
+ const cmdStr = argv.join(' ');
141
+ if (cmdStr) {
142
+ const secretHits = scanForSecrets(cmdStr);
143
+ for (const hit of secretHits) {
144
+ findings.push({ label: hit.label, detail: `in command: ${hit.snippet}` });
145
+ }
146
+ }
147
+
148
+ // Check for credential file access in the command
149
+ for (const arg of argv) {
150
+ if (typeof arg === 'string') {
151
+ const credPath = isCredentialPath(arg);
152
+ if (credPath) {
153
+ findings.push({ label: 'credential file access', detail: `command references ${arg}` });
154
+ }
155
+ }
156
+ }
157
+
158
+ // Check environment variables being passed
159
+ const env = spec?.env;
160
+ if (env && typeof env === 'object') {
161
+ for (const [key, val] of Object.entries(env)) {
162
+ if (SECRET_ENV_VARS.test(key)) {
163
+ findings.push({ label: 'secret in subprocess env', detail: `env var ${key} exposed to child process` });
164
+ }
165
+ }
166
+ }
167
+
168
+ return findings;
169
+ }