@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.
- package/README.md +25 -8
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +3 -1
- package/dist/adapters/common.js.map +1 -1
- package/dist/adapters/openclaw-plugin.d.ts.map +1 -1
- package/dist/adapters/openclaw-plugin.js +17 -3
- package/dist/adapters/openclaw-plugin.js.map +1 -1
- package/dist/cli.js +170 -11
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -0
- package/dist/config.js.map +1 -1
- package/dist/feed/cron.d.ts +27 -0
- package/dist/feed/cron.d.ts.map +1 -1
- package/dist/feed/cron.js +721 -54
- package/dist/feed/cron.js.map +1 -1
- package/dist/installers.d.ts +1 -1
- package/dist/installers.d.ts.map +1 -1
- package/dist/installers.js +164 -13
- package/dist/installers.js.map +1 -1
- package/dist/postinstall.js +29 -0
- package/dist/postinstall.js.map +1 -1
- package/dist/registry/storage.d.ts.map +1 -1
- package/dist/registry/storage.js +5 -1
- package/dist/registry/storage.js.map +1 -1
- package/dist/runtime/types.d.ts +1 -1
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/tests/cli-init.test.d.ts +2 -0
- package/dist/tests/cli-init.test.d.ts.map +1 -0
- package/dist/tests/cli-init.test.js +130 -0
- package/dist/tests/cli-init.test.js.map +1 -0
- package/dist/tests/cli-policy.test.js +47 -0
- package/dist/tests/cli-policy.test.js.map +1 -1
- package/dist/tests/cli-subscribe.test.js +33 -0
- package/dist/tests/cli-subscribe.test.js.map +1 -1
- package/dist/tests/feed-cron.test.js +441 -13
- package/dist/tests/feed-cron.test.js.map +1 -1
- package/dist/tests/installer.test.js +37 -2
- package/dist/tests/installer.test.js.map +1 -1
- package/dist/tests/integration.test.js +9 -5
- package/dist/tests/integration.test.js.map +1 -1
- package/dist/tests/postinstall.test.d.ts +2 -0
- package/dist/tests/postinstall.test.d.ts.map +1 -0
- package/dist/tests/postinstall.test.js +31 -0
- package/dist/tests/postinstall.test.js.map +1 -0
- package/dist/tests/setup-script.test.d.ts +2 -0
- package/dist/tests/setup-script.test.d.ts.map +1 -0
- package/dist/tests/setup-script.test.js +63 -0
- package/dist/tests/setup-script.test.js.map +1 -0
- package/dist/tests/smoke.test.js +88 -1
- package/dist/tests/smoke.test.js.map +1 -1
- package/docs/codex.md +1 -1
- package/docs/hermes.md +3 -3
- package/package.json +1 -1
- package/skills/agentguard/SKILL.md +424 -194
- package/skills/agentguard/hermes-hooks.yaml +2 -2
- package/skills/agentguard/scan-rules.md +13 -2
- package/skills/agentguard/scripts/{action-cli.ts → action-cli.js} +13 -18
- package/skills/agentguard/scripts/auto-scan.js +3 -1
- package/skills/agentguard/scripts/checkup-score.js +369 -0
- package/skills/agentguard/scripts/hermes-hook.js +103 -16
- package/skills/agentguard/scripts/scan-to-sarif.js +195 -0
- package/skills/agentguard/scripts/{trust-cli.ts → trust-cli.js} +12 -16
- 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
|
-
|
|
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.
|
|
8
|
-
* node action-cli.
|
|
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
|
|
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
|
|
42
|
+
function hasFlag(name) {
|
|
48
43
|
return args.includes(`--${name}`);
|
|
49
44
|
}
|
|
50
45
|
|
|
51
|
-
function printUsage()
|
|
52
|
-
console.error(`Usage: action-cli.
|
|
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()
|
|
108
|
-
const type = getArg('type')
|
|
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
|
|
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')
|
|
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
|
|
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
|
|
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 =
|
|
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');
|