@goplus/agentguard 1.1.9 → 1.1.13

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.
Files changed (65) hide show
  1. package/README.md +25 -8
  2. package/dist/adapters/common.d.ts.map +1 -1
  3. package/dist/adapters/common.js +3 -1
  4. package/dist/adapters/common.js.map +1 -1
  5. package/dist/adapters/openclaw-plugin.d.ts.map +1 -1
  6. package/dist/adapters/openclaw-plugin.js +17 -3
  7. package/dist/adapters/openclaw-plugin.js.map +1 -1
  8. package/dist/cli.js +170 -11
  9. package/dist/cli.js.map +1 -1
  10. package/dist/config.d.ts +3 -0
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +18 -0
  13. package/dist/config.js.map +1 -1
  14. package/dist/feed/cron.d.ts +27 -0
  15. package/dist/feed/cron.d.ts.map +1 -1
  16. package/dist/feed/cron.js +721 -54
  17. package/dist/feed/cron.js.map +1 -1
  18. package/dist/installers.d.ts +1 -1
  19. package/dist/installers.d.ts.map +1 -1
  20. package/dist/installers.js +164 -13
  21. package/dist/installers.js.map +1 -1
  22. package/dist/postinstall.js +29 -0
  23. package/dist/postinstall.js.map +1 -1
  24. package/dist/registry/storage.d.ts.map +1 -1
  25. package/dist/registry/storage.js +5 -1
  26. package/dist/registry/storage.js.map +1 -1
  27. package/dist/runtime/types.d.ts +1 -1
  28. package/dist/runtime/types.d.ts.map +1 -1
  29. package/dist/tests/cli-init.test.d.ts +2 -0
  30. package/dist/tests/cli-init.test.d.ts.map +1 -0
  31. package/dist/tests/cli-init.test.js +130 -0
  32. package/dist/tests/cli-init.test.js.map +1 -0
  33. package/dist/tests/cli-policy.test.js +47 -0
  34. package/dist/tests/cli-policy.test.js.map +1 -1
  35. package/dist/tests/cli-subscribe.test.js +33 -0
  36. package/dist/tests/cli-subscribe.test.js.map +1 -1
  37. package/dist/tests/feed-cron.test.js +441 -13
  38. package/dist/tests/feed-cron.test.js.map +1 -1
  39. package/dist/tests/installer.test.js +37 -2
  40. package/dist/tests/installer.test.js.map +1 -1
  41. package/dist/tests/integration.test.js +9 -5
  42. package/dist/tests/integration.test.js.map +1 -1
  43. package/dist/tests/postinstall.test.d.ts +2 -0
  44. package/dist/tests/postinstall.test.d.ts.map +1 -0
  45. package/dist/tests/postinstall.test.js +31 -0
  46. package/dist/tests/postinstall.test.js.map +1 -0
  47. package/dist/tests/setup-script.test.d.ts +2 -0
  48. package/dist/tests/setup-script.test.d.ts.map +1 -0
  49. package/dist/tests/setup-script.test.js +63 -0
  50. package/dist/tests/setup-script.test.js.map +1 -0
  51. package/dist/tests/smoke.test.js +88 -1
  52. package/dist/tests/smoke.test.js.map +1 -1
  53. package/docs/codex.md +1 -1
  54. package/docs/hermes.md +3 -3
  55. package/package.json +1 -1
  56. package/skills/agentguard/SKILL.md +424 -194
  57. package/skills/agentguard/hermes-hooks.yaml +2 -2
  58. package/skills/agentguard/scan-rules.md +13 -2
  59. package/skills/agentguard/scripts/{action-cli.ts → action-cli.js} +13 -18
  60. package/skills/agentguard/scripts/auto-scan.js +3 -1
  61. package/skills/agentguard/scripts/checkup-score.js +369 -0
  62. package/skills/agentguard/scripts/hermes-hook.js +103 -16
  63. package/skills/agentguard/scripts/scan-to-sarif.js +195 -0
  64. package/skills/agentguard/scripts/{trust-cli.ts → trust-cli.js} +12 -16
  65. package/skills/agentguard/suppress.example.yaml +67 -0
@@ -19,12 +19,12 @@ hooks:
19
19
  - matcher: "read_file"
20
20
  command: "node \"AGENTGUARD_SKILL_DIR/scripts/hermes-hook.js\""
21
21
  timeout: 10
22
- - matcher: "web_search|web_extract|browser_navigate"
22
+ - matcher: "web_search|web_extract|browser_navigate|browser_open|web_open|open_url|visit_url|open"
23
23
  command: "node \"AGENTGUARD_SKILL_DIR/scripts/hermes-hook.js\""
24
24
  timeout: 10
25
25
 
26
26
  post_tool_call:
27
- - matcher: "terminal|execute_code|write_file|patch|skill_manage|read_file|web_search|web_extract|browser_navigate"
27
+ - matcher: "terminal|execute_code|write_file|patch|skill_manage|read_file|web_search|web_extract|browser_navigate|browser_open|web_open|open_url|visit_url|open"
28
28
  command: "node \"AGENTGUARD_SKILL_DIR/scripts/hermes-hook.js\""
29
29
  timeout: 5
30
30
 
@@ -98,7 +98,7 @@ Detailed Grep patterns for all 24 detection rules. Use this as reference when ex
98
98
  | `Windows.*Credentials` (i) | Windows credentials |
99
99
  | `credential.*manager` (i) | Credential manager |
100
100
 
101
- ## Rule 7: PRIVATE_KEY_PATTERN (CRITICAL)
101
+ ## Rule 7: PRIVATE_KEY_PATTERN (CRITICAL*)
102
102
  **Files**: All
103
103
 
104
104
  | Pattern | Description |
@@ -107,7 +107,16 @@ Detailed Grep patterns for all 24 detection rules. Use this as reference when ex
107
107
  | `private[_\s]?key\s*[:=]\s*['"\x60]0x[a-fA-F0-9]{64}` (i) | Named private key |
108
108
  | `PRIVATE_KEY\s*[:=]\s*['"\x60][a-fA-F0-9]{64}` (i) | PRIVATE_KEY assignment |
109
109
 
110
- ## Rule 8: MNEMONIC_PATTERN (CRITICAL)
110
+ **Git-aware severity** (apply after initial match — see SKILL.md "Git Context Check"):
111
+
112
+ | Condition | Severity |
113
+ |-----------|----------|
114
+ | File ever appeared in git history (`git log --all`) | **CRITICAL** |
115
+ | File tracked by git but NOT gitignored | **HIGH** |
116
+ | File gitignored (`git check-ignore`) | **MEDIUM** |
117
+ | Not inside a git repository | **CRITICAL** |
118
+
119
+ ## Rule 8: MNEMONIC_PATTERN (CRITICAL*)
111
120
  **Files**: All
112
121
 
113
122
  | Pattern | Description |
@@ -117,6 +126,8 @@ Detailed Grep patterns for all 24 detection rules. Use this as reference when ex
117
126
  | `mnemonic\s*[:=]\s*['"\x60]` (i) | Mnemonic assignment |
118
127
  | `recovery[_\s]?phrase\s*[:=]\s*['"\x60]` (i) | Recovery phrase assignment |
119
128
 
129
+ **Git-aware severity**: same table as Rule 7 above.
130
+
120
131
  ## Rule 9: WALLET_DRAINING (CRITICAL)
121
132
  **Files**: `*.js`, `*.ts`, `*.sol`
122
133
 
@@ -4,8 +4,8 @@
4
4
  * GoPlus AgentGuard Action CLI — lightweight wrapper for ActionScanner operations.
5
5
  *
6
6
  * Usage:
7
- * node action-cli.ts decide --type <action_type> [action-specific args]
8
- * node action-cli.ts simulate --chain-id <id> --from <addr> --to <addr> --value <wei> [--data <hex>] [--origin <url>]
7
+ * node action-cli.js decide --type <action_type> [action-specific args]
8
+ * node action-cli.js simulate --chain-id <id> --from <addr> --to <addr> --value <wei> [--data <hex>] [--origin <url>]
9
9
  *
10
10
  * Action-specific args for `decide`:
11
11
  *
@@ -29,27 +29,22 @@
29
29
  */
30
30
 
31
31
  import { createAgentGuard } from '@goplus/agentguard';
32
- import type {
33
- ActionEnvelope,
34
- Web3Intent,
35
- ActionType,
36
- } from '@goplus/agentguard';
37
32
 
38
33
  const args = process.argv.slice(2);
39
34
  const command = args[0];
40
35
 
41
- function getArg(name: string): string | undefined {
36
+ function getArg(name) {
42
37
  const idx = args.indexOf(`--${name}`);
43
38
  if (idx === -1 || idx + 1 >= args.length) return undefined;
44
39
  return args[idx + 1];
45
40
  }
46
41
 
47
- function hasFlag(name: string): boolean {
42
+ function hasFlag(name) {
48
43
  return args.includes(`--${name}`);
49
44
  }
50
45
 
51
- function printUsage(): void {
52
- console.error(`Usage: action-cli.ts <decide|simulate> [options]
46
+ function printUsage() {
47
+ console.error(`Usage: action-cli.js <decide|simulate> [options]
53
48
 
54
49
  Commands:
55
50
  decide Evaluate an action and return a policy decision
@@ -104,8 +99,8 @@ simulate options:
104
99
  process.exit(1);
105
100
  }
106
101
 
107
- function buildEnvelope(): ActionEnvelope {
108
- const type = getArg('type') as ActionType;
102
+ function buildEnvelope() {
103
+ const type = getArg('type');
109
104
  if (!type) {
110
105
  console.error('Error: --type is required for decide');
111
106
  printUsage();
@@ -113,7 +108,7 @@ function buildEnvelope(): ActionEnvelope {
113
108
  }
114
109
 
115
110
  const userPresent = hasFlag('user-present');
116
- let data: Record<string, unknown>;
111
+ let data;
117
112
 
118
113
  switch (type) {
119
114
  case 'web3_tx':
@@ -133,7 +128,7 @@ function buildEnvelope(): ActionEnvelope {
133
128
  signer: getArg('signer') || '',
134
129
  message: getArg('message'),
135
130
  typed_data: getArg('typed-data')
136
- ? JSON.parse(getArg('typed-data')!)
131
+ ? JSON.parse(getArg('typed-data'))
137
132
  : undefined,
138
133
  origin: getArg('origin'),
139
134
  };
@@ -142,7 +137,7 @@ function buildEnvelope(): ActionEnvelope {
142
137
  case 'exec_command':
143
138
  data = {
144
139
  command: getArg('command') || '',
145
- args: getArg('args') ? JSON.parse(getArg('args')!) : undefined,
140
+ args: getArg('args') ? JSON.parse(getArg('args')) : undefined,
146
141
  cwd: getArg('cwd'),
147
142
  };
148
143
  break;
@@ -186,7 +181,7 @@ function buildEnvelope(): ActionEnvelope {
186
181
  },
187
182
  action: {
188
183
  type,
189
- data: data as any,
184
+ data,
190
185
  },
191
186
  context: {
192
187
  session_id: `cli-${Date.now()}`,
@@ -214,7 +209,7 @@ async function main() {
214
209
  }
215
210
 
216
211
  case 'simulate': {
217
- const intent: Web3Intent = {
212
+ const intent = {
218
213
  chain_id: Number(getArg('chain-id') || '1'),
219
214
  from: getArg('from') || '',
220
215
  to: getArg('to') || '',
@@ -62,7 +62,9 @@ const SKILLS_DIRS = [
62
62
  join(homedir(), '.hermes', 'skills'),
63
63
  join(homedir(), '.openclaw', 'skills'),
64
64
  ];
65
- const AGENTGUARD_DIR = join(homedir(), '.agentguard');
65
+ const AGENTGUARD_DIR = process.env.OPENCLAW_STATE_DIR
66
+ ? join(process.env.OPENCLAW_STATE_DIR, 'agentguard')
67
+ : process.env.AGENTGUARD_HOME || join(homedir(), '.agentguard');
66
68
  const AUDIT_PATH = join(AGENTGUARD_DIR, 'audit.jsonl');
67
69
 
68
70
  function ensureDir() {
@@ -0,0 +1,369 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * checkup-score.js — deterministic scoring engine for /agentguard checkup
4
+ *
5
+ * Accepts raw check facts collected by the LLM and computes all dimension
6
+ * scores, the composite score, and tier assignment without any LLM arithmetic.
7
+ *
8
+ * Usage:
9
+ * node scripts/checkup-score.js --file <raw-facts.json>
10
+ * cat raw-facts.json | node scripts/checkup-score.js
11
+ *
12
+ * Input schema (raw-facts.json):
13
+ * {
14
+ * "skills": [
15
+ * { "name": "skill-name", "risk_level": "low|medium|high|critical", "findings": [
16
+ * { "rule": "RULE_ID", "severity": "CRITICAL|HIGH|MEDIUM|LOW", "file": "...", "line": 0 }
17
+ * ]}
18
+ * ],
19
+ * "credential_files": {
20
+ * "ssh_dir": { "exists": true, "permissions": "700" },
21
+ * "gnupg_dir": { "exists": false },
22
+ * "openclaw_config": { "exists": false }
23
+ * },
24
+ * "dlp": {
25
+ * "private_keys_found": false,
26
+ * "mnemonics_found": false,
27
+ * "api_keys_found": false
28
+ * },
29
+ * "network": {
30
+ * "dangerous_ports": [],
31
+ * "suspicious_crons": [],
32
+ * "sensitive_env_vars": []
33
+ * },
34
+ * "runtime": {
35
+ * "hooks_installed": false,
36
+ * "audit_log_exists": false,
37
+ * "skills_ever_scanned": false
38
+ * },
39
+ * "web3": {
40
+ * "detected": false,
41
+ * "wallet_draining_found": false,
42
+ * "unlimited_approval_found": false,
43
+ * "goplus_configured": false
44
+ * }
45
+ * }
46
+ *
47
+ * Output: JSON with computed scores, findings, composite, and tier.
48
+ */
49
+
50
+ import { readFileSync } from 'node:fs';
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Input
54
+ // ---------------------------------------------------------------------------
55
+
56
+ function readInput() {
57
+ const fileIdx = process.argv.indexOf('--file');
58
+ if (fileIdx !== -1 && process.argv[fileIdx + 1]) {
59
+ return JSON.parse(readFileSync(process.argv[fileIdx + 1], 'utf-8'));
60
+ }
61
+ return JSON.parse(readFileSync('/dev/stdin', 'utf-8'));
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Dimension 1: Skill & Code Safety (weight 25%)
66
+ // ---------------------------------------------------------------------------
67
+
68
+ function scoreCodeSafety(skills) {
69
+ const findings = [];
70
+
71
+ if (!skills || skills.length === 0) {
72
+ findings.push({ severity: 'LOW', text: 'No third-party skills installed — no code to audit' });
73
+ return { score: 70, findings };
74
+ }
75
+
76
+ let score = 100;
77
+
78
+ for (const skill of skills) {
79
+ const isAgentGuard = (skill.name || '').toLowerCase().includes('agentguard');
80
+
81
+ for (const f of (skill.findings || [])) {
82
+ // Suppress READ_ENV_SECRETS for agentguard itself
83
+ if (isAgentGuard && f.rule === 'READ_ENV_SECRETS') continue;
84
+
85
+ const sev = (f.severity || '').toUpperCase();
86
+ if (sev === 'CRITICAL') {
87
+ score -= 15;
88
+ findings.push({ severity: 'CRITICAL', text: `${f.rule} in ${skill.name}:${f.file || '?'}:${f.line || '?'}` });
89
+ } else if (sev === 'HIGH') {
90
+ score -= 8;
91
+ findings.push({ severity: 'HIGH', text: `${f.rule} in ${skill.name}:${f.file || '?'}:${f.line || '?'}` });
92
+ } else if (sev === 'MEDIUM') {
93
+ score -= 3;
94
+ findings.push({ severity: 'MEDIUM', text: `${f.rule} in ${skill.name}:${f.file || '?'}:${f.line || '?'}` });
95
+ }
96
+ }
97
+ }
98
+
99
+ return { score: Math.max(0, score), findings };
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Dimension 2: Credential & Secret Safety (weight 25%)
104
+ // ---------------------------------------------------------------------------
105
+
106
+ function scoreCredentialSafety(credentialFiles, dlp) {
107
+ const findings = [];
108
+ let score = 0;
109
+
110
+ const cf = credentialFiles || {};
111
+ const dlpData = dlp || {};
112
+
113
+ // ~/.ssh/ permissions (25 pts)
114
+ const ssh = cf.ssh_dir;
115
+ if (!ssh || !ssh.exists) {
116
+ score += 25; // N/A — dir doesn't exist
117
+ } else {
118
+ const perms = parseInt(ssh.permissions || '0', 8);
119
+ if (perms <= 0o700) {
120
+ score += 25;
121
+ } else {
122
+ findings.push({ severity: 'HIGH', text: `~/.ssh/ permissions too open (${ssh.permissions}) — should be 700` });
123
+ }
124
+ }
125
+
126
+ // ~/.gnupg/ permissions (15 pts)
127
+ const gnupg = cf.gnupg_dir;
128
+ if (!gnupg || !gnupg.exists) {
129
+ score += 15; // N/A
130
+ } else {
131
+ const perms = parseInt(gnupg.permissions || '0', 8);
132
+ if (perms <= 0o700) {
133
+ score += 15;
134
+ } else {
135
+ findings.push({ severity: 'MEDIUM', text: `~/.gnupg/ permissions too open (${gnupg.permissions}) — should be 700` });
136
+ }
137
+ }
138
+
139
+ // No private keys (25 pts)
140
+ if (!dlpData.private_keys_found) {
141
+ score += 25;
142
+ } else {
143
+ findings.push({ severity: 'CRITICAL', text: 'Plaintext private key found in skill code or workspace' });
144
+ }
145
+
146
+ // No mnemonics (20 pts)
147
+ if (!dlpData.mnemonics_found) {
148
+ score += 20;
149
+ } else {
150
+ findings.push({ severity: 'CRITICAL', text: 'Plaintext mnemonic found in skill code or workspace' });
151
+ }
152
+
153
+ // No API keys (15 pts)
154
+ if (!dlpData.api_keys_found) {
155
+ score += 15;
156
+ } else {
157
+ findings.push({ severity: 'HIGH', text: 'API key/token found in skill code or workspace' });
158
+ }
159
+
160
+ return { score: Math.min(100, score), findings };
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Dimension 3: Network & System Exposure (weight 20%)
165
+ // ---------------------------------------------------------------------------
166
+
167
+ function scoreNetworkExposure(network) {
168
+ const findings = [];
169
+ let score = 0;
170
+
171
+ const net = network || {};
172
+ const ports = net.dangerous_ports || [];
173
+ const crons = net.suspicious_crons || [];
174
+ const envVars = net.sensitive_env_vars || [];
175
+
176
+ // No dangerous ports (35 pts)
177
+ if (ports.length === 0) {
178
+ score += 35;
179
+ } else {
180
+ for (const p of ports) {
181
+ findings.push({ severity: 'HIGH', text: `Dangerous port exposed: ${p}` });
182
+ }
183
+ }
184
+
185
+ // No suspicious crons (30 pts)
186
+ if (crons.length === 0) {
187
+ score += 30;
188
+ } else {
189
+ for (const c of crons) {
190
+ findings.push({ severity: 'HIGH', text: `Suspicious cron job: ${c}` });
191
+ }
192
+ }
193
+
194
+ // No sensitive env vars (20 pts)
195
+ if (envVars.length === 0) {
196
+ score += 20;
197
+ } else {
198
+ for (const v of envVars) {
199
+ findings.push({ severity: 'MEDIUM', text: `Sensitive env var exposed: ${v}` });
200
+ }
201
+ }
202
+
203
+ // OpenClaw config permissions — always award 15 pts if not OpenClaw (N/A)
204
+ const oc = (network || {}).openclaw_config_ok;
205
+ if (oc === undefined || oc === null || oc === true) {
206
+ score += 15; // N/A or passing
207
+ } else {
208
+ findings.push({ severity: 'MEDIUM', text: 'OpenClaw config file permissions too open' });
209
+ }
210
+
211
+ return { score: Math.min(100, score), findings };
212
+ }
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // Dimension 4: Runtime Protection (weight 15%)
216
+ // ---------------------------------------------------------------------------
217
+
218
+ function scoreRuntimeProtection(runtime) {
219
+ const findings = [];
220
+ let score = 0;
221
+
222
+ const rt = runtime || {};
223
+
224
+ // Hooks installed (40 pts)
225
+ if (rt.hooks_installed) {
226
+ score += 40;
227
+ } else {
228
+ findings.push({ severity: 'HIGH', text: 'No security hooks installed — actions are unmonitored' });
229
+ }
230
+
231
+ // Audit log exists (30 pts)
232
+ if (rt.audit_log_exists) {
233
+ score += 30;
234
+ } else {
235
+ findings.push({ severity: 'MEDIUM', text: 'No security audit log — no threat history available' });
236
+ }
237
+
238
+ // Skills ever scanned (30 pts)
239
+ if (rt.skills_ever_scanned) {
240
+ score += 30;
241
+ } else {
242
+ findings.push({ severity: 'MEDIUM', text: 'Installed skills have never been security-scanned' });
243
+ }
244
+
245
+ return { score: Math.min(100, score), findings };
246
+ }
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // Dimension 5: Web3 Safety (weight 15%, only if detected)
250
+ // ---------------------------------------------------------------------------
251
+
252
+ function scoreWeb3Safety(web3) {
253
+ if (!web3 || !web3.detected) {
254
+ return { score: null, na: true, findings: [] };
255
+ }
256
+
257
+ const findings = [];
258
+ let score = 0;
259
+
260
+ // No wallet-draining patterns (40 pts)
261
+ if (!web3.wallet_draining_found) {
262
+ score += 40;
263
+ } else {
264
+ findings.push({ severity: 'CRITICAL', text: 'Wallet-draining pattern detected in skill code' });
265
+ }
266
+
267
+ // No unlimited approvals (30 pts)
268
+ if (!web3.unlimited_approval_found) {
269
+ score += 30;
270
+ } else {
271
+ findings.push({ severity: 'HIGH', text: 'Unlimited approval pattern detected in skill code' });
272
+ }
273
+
274
+ // GoPlus or equivalent configured (30 pts)
275
+ if (web3.goplus_configured) {
276
+ score += 30;
277
+ } else {
278
+ findings.push({ severity: 'MEDIUM', text: 'No transaction security API — Web3 calls are unverified' });
279
+ }
280
+
281
+ return { score: Math.min(100, score), na: false, findings };
282
+ }
283
+
284
+ // ---------------------------------------------------------------------------
285
+ // Composite score + tier
286
+ // ---------------------------------------------------------------------------
287
+
288
+ function computeComposite(dims) {
289
+ const { code_safety, credential_safety, network_exposure, runtime_protection, web3_safety } = dims;
290
+
291
+ let composite;
292
+ if (web3_safety.na) {
293
+ // Redistribute 15% across 4 dimensions
294
+ composite =
295
+ code_safety.score * 0.294 +
296
+ credential_safety.score * 0.294 +
297
+ network_exposure.score * 0.235 +
298
+ runtime_protection.score * 0.176;
299
+ } else {
300
+ composite =
301
+ code_safety.score * 0.25 +
302
+ credential_safety.score * 0.25 +
303
+ network_exposure.score * 0.20 +
304
+ runtime_protection.score * 0.15 +
305
+ web3_safety.score * 0.15;
306
+ }
307
+
308
+ return Math.round(composite);
309
+ }
310
+
311
+ function assignTier(score) {
312
+ if (score >= 90) return { tier: 'S', label: 'JACKED' };
313
+ if (score >= 70) return { tier: 'A', label: 'Healthy' };
314
+ if (score >= 50) return { tier: 'B', label: 'Tired' };
315
+ return { tier: 'F', label: 'Critical' };
316
+ }
317
+
318
+ // ---------------------------------------------------------------------------
319
+ // Main
320
+ // ---------------------------------------------------------------------------
321
+
322
+ const raw = readInput();
323
+
324
+ const dimensions = {
325
+ code_safety: scoreCodeSafety(raw.skills),
326
+ credential_safety: scoreCredentialSafety(raw.credential_files, raw.dlp),
327
+ network_exposure: scoreNetworkExposure(raw.network),
328
+ runtime_protection: scoreRuntimeProtection(raw.runtime),
329
+ web3_safety: scoreWeb3Safety(raw.web3),
330
+ };
331
+
332
+ const composite_score = computeComposite(dimensions);
333
+ const { tier, label } = assignTier(composite_score);
334
+
335
+ const totalFindings = Object.values(dimensions).reduce(
336
+ (acc, d) => acc + (d.findings ? d.findings.length : 0), 0
337
+ );
338
+
339
+ const output = {
340
+ composite_score,
341
+ tier,
342
+ tier_label: label,
343
+ total_findings: totalFindings,
344
+ dimensions: {
345
+ code_safety: {
346
+ score: dimensions.code_safety.score,
347
+ findings: dimensions.code_safety.findings,
348
+ },
349
+ credential_safety: {
350
+ score: dimensions.credential_safety.score,
351
+ findings: dimensions.credential_safety.findings,
352
+ },
353
+ network_exposure: {
354
+ score: dimensions.network_exposure.score,
355
+ findings: dimensions.network_exposure.findings,
356
+ },
357
+ runtime_protection: {
358
+ score: dimensions.runtime_protection.score,
359
+ findings: dimensions.runtime_protection.findings,
360
+ },
361
+ web3_safety: {
362
+ score: dimensions.web3_safety.score,
363
+ na: dimensions.web3_safety.na,
364
+ findings: dimensions.web3_safety.findings,
365
+ },
366
+ },
367
+ };
368
+
369
+ process.stdout.write(JSON.stringify(output, null, 2) + '\n');