clawarmor 2.0.0-alpha.3 โ 2.0.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 +50 -146
- package/cli.js +1 -1
- package/lib/prescan.js +166 -70
- package/lib/protect.js +60 -4
- package/lib/status.js +38 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,163 +1,67 @@
|
|
|
1
|
-
|
|
1
|
+
# ClawArmor
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
**The security auditor for OpenClaw agents.**
|
|
6
|
-
|
|
7
|
-
Checks your config. Probes your live gateway. Scans your skills.
|
|
8
|
-
Runs in 30 seconds. Finds what config-only tools miss. Free forever.
|
|
3
|
+
Security armor for OpenClaw agents โ audit, scan, monitor.
|
|
9
4
|
|
|
10
5
|
[](https://www.npmjs.com/package/clawarmor)
|
|
11
6
|
[](LICENSE)
|
|
12
|
-
[](package.json)
|
|
49
8
|
|
|
50
|
-
##
|
|
9
|
+
## What it does
|
|
51
10
|
|
|
52
|
-
|
|
11
|
+
- Audits your OpenClaw config and live gateway with 30+ checks โ scored 0โ100
|
|
12
|
+
- Scans every installed skill file for malicious code and prompt injection patterns
|
|
13
|
+
- Guards every install: intercepts `openclaw clawhub install`, pre-scans before activation
|
|
53
14
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
Config says `bind: loopback`. Is your gateway *actually* unreachable on LAN? Config says auth is enabled. Does the live WebSocket endpoint *actually* reject unauthenticated connections? A misconfigured nginx in front can make your config lie. Live probes can't be faked.
|
|
57
|
-
|
|
58
|
-
> All probes connect from your machine to `127.0.0.1` (and your local network interfaces). Nothing leaves your machine.
|
|
59
|
-
|
|
60
|
-
---
|
|
61
|
-
|
|
62
|
-
## Five commands
|
|
15
|
+
## Quick start
|
|
63
16
|
|
|
64
17
|
```bash
|
|
65
|
-
|
|
66
|
-
clawarmor
|
|
67
|
-
clawarmor
|
|
68
|
-
clawarmor verify # Re-run only previously-failed checks. Exit 0 if all fixed (CI-friendly).
|
|
69
|
-
clawarmor trend # ASCII chart of your security score over time.
|
|
18
|
+
npm install -g clawarmor
|
|
19
|
+
clawarmor protect --install
|
|
20
|
+
clawarmor audit
|
|
70
21
|
```
|
|
71
22
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
## What it checks
|
|
75
|
-
|
|
76
|
-
### Live gateway probes (behavioral โ not just config reads)
|
|
23
|
+
## Commands
|
|
77
24
|
|
|
78
|
-
|
|
|
25
|
+
| Command | Description |
|
|
79
26
|
|---|---|
|
|
80
|
-
|
|
|
81
|
-
|
|
|
82
|
-
|
|
|
83
|
-
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
---
|
|
102
|
-
|
|
103
|
-
## What it protects against
|
|
104
|
-
|
|
105
|
-
| Threat | Covered | Notes |
|
|
27
|
+
| `audit` | Score your OpenClaw config (0โ100), live gateway probes, plain-English verdict |
|
|
28
|
+
| `scan` | Scan all installed skill files for malicious code and SKILL.md instructions |
|
|
29
|
+
| `prescan <skill>` | Pre-scan a skill before installing โ blocks on CRITICAL findings |
|
|
30
|
+
| `protect --install` | Install guard hook, shell intercept (zsh/bash/fish), and watch daemon |
|
|
31
|
+
| `protect --uninstall` | Remove all ClawArmor protection components |
|
|
32
|
+
| `protect --status` | Show current protection state |
|
|
33
|
+
| `watch` | Monitor config and skill changes in real time |
|
|
34
|
+
| `watch --daemon` | Start the watcher as a background daemon |
|
|
35
|
+
| `harden` | Interactive hardening wizard (--dry-run, --auto) |
|
|
36
|
+
| `status` | One-screen security posture dashboard |
|
|
37
|
+
| `log` | View the audit event log |
|
|
38
|
+
| `digest` | Show weekly security digest |
|
|
39
|
+
| `verify` | Re-run only previously-failed checks (CI-friendly, exit 0 = all fixed) |
|
|
40
|
+
| `trend` | ASCII chart of your security score over time |
|
|
41
|
+
| `compare` | Compare coverage vs openclaw security audit |
|
|
42
|
+
| `fix` | Auto-apply safe fixes (--dry-run to preview, --apply to run) |
|
|
43
|
+
|
|
44
|
+
## What it catches
|
|
45
|
+
|
|
46
|
+
| Threat | Description | Coverage |
|
|
106
47
|
|---|---|---|
|
|
107
|
-
|
|
|
108
|
-
|
|
|
109
|
-
|
|
|
110
|
-
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
Sandbox isolation is enabled safely: if Docker is installed, `fix --apply` sets `sandbox.mode=non-main` + `workspaceAccess=rw` so your Telegram/group sessions keep workspace access.
|
|
124
|
-
|
|
125
|
-
---
|
|
126
|
-
|
|
127
|
-
## CI integration
|
|
128
|
-
|
|
129
|
-
```bash
|
|
130
|
-
# Fail CI if security score drops
|
|
131
|
-
clawarmor verify # exit 0 = all previously-failed checks now pass
|
|
132
|
-
# exit 1 = still failing
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Score history persists in `~/.clawarmor/history.json`.
|
|
136
|
-
|
|
137
|
-
---
|
|
138
|
-
|
|
139
|
-
## Privacy & security
|
|
140
|
-
|
|
141
|
-
- `audit`, `scan`, `fix`, `verify`, `trend` run **entirely locally**
|
|
142
|
-
- One optional network call: `registry.npmjs.org` for version check (skippable with `--offline`)
|
|
143
|
-
- Every run prints exactly what files it reads and what network calls it makes before executing
|
|
144
|
-
- Nothing is sent anywhere
|
|
145
|
-
|
|
146
|
-
**Found a vulnerability in ClawArmor itself?** Please email `pinzasrojas@proton.me` before public disclosure.
|
|
147
|
-
|
|
148
|
-
---
|
|
149
|
-
|
|
150
|
-
## Installation
|
|
151
|
-
|
|
152
|
-
```bash
|
|
153
|
-
npm install -g clawarmor # requires Node.js 18+
|
|
154
|
-
clawarmor audit
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
Zero runtime npm dependencies. Node.js built-ins only (`net`, `http`, `os`, `fs`, `crypto`).
|
|
158
|
-
|
|
159
|
-
---
|
|
48
|
+
| Token/config exposure | File permission checks, config hardening | Full |
|
|
49
|
+
| Malicious skill supply chain | All skill files scanned โ not just SKILL.md | Full |
|
|
50
|
+
| Credential hygiene | Token age, rotation reminders, access scope | Full |
|
|
51
|
+
| Config drift | Baseline hashing, change detection on every startup | Full |
|
|
52
|
+
| Obfuscation | Base64 blobs, dynamic eval, encoded payloads | Partial |
|
|
53
|
+
| Prompt injection via SKILL.md | Instruction patterns, exfil, deception, system overrides | Full |
|
|
54
|
+
| Live gateway auth | WebSocket probe โ does server actually reject unauthenticated connections? | Full |
|
|
55
|
+
| CORS misconfiguration | OPTIONS probe with arbitrary origin | Full |
|
|
56
|
+
| Gateway exposure | TCP-connects to every non-loopback interface | Full |
|
|
57
|
+
| Runtime policy enforcement | Requires a runtime layer (SupraWall) | None |
|
|
58
|
+
|
|
59
|
+
## Philosophy
|
|
60
|
+
|
|
61
|
+
ClawArmor runs entirely on your machine โ no telemetry, no cloud, no accounts.
|
|
62
|
+
It has zero npm runtime dependencies, using only Node.js built-ins.
|
|
63
|
+
Every run prints exactly what files it reads and what network calls it makes before executing anything.
|
|
160
64
|
|
|
161
65
|
## License
|
|
162
66
|
|
|
163
|
-
MIT
|
|
67
|
+
MIT
|
package/cli.js
CHANGED
package/lib/prescan.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
// ClawArmor v2.0 โ Pre-scan a skill before installing
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// Supports both npm packages and ClawHub skills.
|
|
3
|
+
// ClawHub skills are checked locally first; npm is used as fallback.
|
|
4
4
|
|
|
5
|
-
import { mkdirSync, rmSync, readdirSync } from 'fs';
|
|
5
|
+
import { mkdirSync, rmSync, readdirSync, existsSync } from 'fs';
|
|
6
6
|
import { join } from 'path';
|
|
7
|
-
import { tmpdir } from 'os';
|
|
7
|
+
import { tmpdir, homedir } from 'os';
|
|
8
8
|
import { execSync } from 'child_process';
|
|
9
9
|
import { scanFile } from './scanner/file-scanner.js';
|
|
10
10
|
import { scanSkillMdFiles } from './scanner/skill-md-scanner.js';
|
|
11
11
|
import { paint, severityColor } from './output/colors.js';
|
|
12
12
|
import { append } from './audit-log.js';
|
|
13
13
|
|
|
14
|
+
const HOME = homedir();
|
|
14
15
|
const SEP = paint.dim('โ'.repeat(52));
|
|
15
16
|
|
|
16
17
|
function getAllFiles(dir, files = []) {
|
|
@@ -29,91 +30,83 @@ function cleanupTmp(dir) {
|
|
|
29
30
|
try { rmSync(dir, { recursive: true, force: true }); } catch { /* non-fatal */ }
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
console.log('');
|
|
34
|
-
console.log(` ${paint.bold('ClawArmor Prescan')} โ ${paint.cyan(skillName)}`);
|
|
35
|
-
console.log(` ${paint.dim('Fetching package from npm registry...')}`);
|
|
36
|
-
console.log('');
|
|
33
|
+
// โโ ClawHub skill detection โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
function isNpmScoped(name) {
|
|
36
|
+
// Scoped npm package: @org/pkg
|
|
37
|
+
return name.startsWith('@') && name.includes('/');
|
|
38
|
+
}
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
function looksLikeClawHubSkill(name) {
|
|
41
|
+
// Plain name, no scope, no slash โ could be a ClawHub skill; try local first
|
|
42
|
+
return !name.startsWith('@') && !name.includes('/');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Returns the local install path for a ClawHub skill, or null if not found.
|
|
46
|
+
function findLocalClawHubSkill(name) {
|
|
47
|
+
// Path 1: ~/.openclaw/skills/<name>/
|
|
48
|
+
const userSkillsPath = join(HOME, '.openclaw', 'skills', name);
|
|
49
|
+
if (existsSync(userSkillsPath)) return userSkillsPath;
|
|
50
|
+
|
|
51
|
+
// Path 2: openclaw npm module's skills directory
|
|
52
|
+
// Try common global npm locations
|
|
53
|
+
const candidates = [
|
|
54
|
+
join(HOME, '.npm-global', 'lib', 'node_modules', 'openclaw', 'skills', name),
|
|
55
|
+
'/usr/local/lib/node_modules/openclaw/skills/' + name,
|
|
56
|
+
'/usr/lib/node_modules/openclaw/skills/' + name,
|
|
57
|
+
join(HOME, '.nvm', 'versions', 'node'), // nvm โ we check dirs below
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// Try to resolve openclaw via node resolution from this file's location
|
|
43
61
|
try {
|
|
44
|
-
execSync(
|
|
45
|
-
|
|
46
|
-
timeout:
|
|
62
|
+
const result = execSync('node -e "console.log(require.resolve(\'openclaw/package.json\'))"', {
|
|
63
|
+
encoding: 'utf8',
|
|
64
|
+
timeout: 5000,
|
|
47
65
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
console.log('');
|
|
57
|
-
return 0;
|
|
58
|
-
}
|
|
66
|
+
}).trim();
|
|
67
|
+
if (result) {
|
|
68
|
+
// result is like /path/to/node_modules/openclaw/package.json
|
|
69
|
+
const ocDir = result.replace(/[\\/]package\.json$/, '');
|
|
70
|
+
const skillPath = join(ocDir, 'skills', name);
|
|
71
|
+
if (existsSync(skillPath)) return skillPath;
|
|
72
|
+
}
|
|
73
|
+
} catch { /* openclaw may not be installed */ }
|
|
59
74
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
mkdirSync(extractDir, { recursive: true });
|
|
63
|
-
try {
|
|
64
|
-
execSync(`tar -xzf "${tarball}" -C "${extractDir}"`, {
|
|
65
|
-
timeout: 15000,
|
|
66
|
-
stdio: ['ignore', 'ignore', 'ignore'],
|
|
67
|
-
});
|
|
68
|
-
} catch {
|
|
69
|
-
cleanupTmp(tmpDir);
|
|
70
|
-
console.log(` ${paint.dim('โน')} Could not extract skill package โ install not blocked`);
|
|
71
|
-
console.log('');
|
|
72
|
-
return 0;
|
|
75
|
+
for (const candidate of candidates) {
|
|
76
|
+
if (existsSync(candidate)) return candidate;
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// โโ Scan a directory of files โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
79
83
|
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
function scanDirectory(dir) {
|
|
85
|
+
const allFiles = getAllFiles(dir);
|
|
82
86
|
const codeFindings = allFiles.flatMap(f => scanFile(f, false));
|
|
83
87
|
const mdResults = scanSkillMdFiles(allFiles, false);
|
|
84
88
|
const mdFindings = mdResults.flatMap(r => r.findings);
|
|
85
|
-
|
|
89
|
+
return { allFiles, allFindings: [...codeFindings, ...mdFindings] };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// โโ Result printer โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
86
93
|
|
|
94
|
+
function printResult(skillName, allFiles, allFindings) {
|
|
87
95
|
const criticals = allFindings.filter(f => f.severity === 'CRITICAL');
|
|
88
96
|
const highs = allFindings.filter(f => f.severity === 'HIGH');
|
|
89
97
|
const mediums = allFindings.filter(f => f.severity === 'MEDIUM');
|
|
90
98
|
const lows = allFindings.filter(f => f.severity === 'LOW' || f.severity === 'INFO');
|
|
99
|
+
const fileCount = allFiles.length;
|
|
91
100
|
|
|
92
|
-
// โโ Cleanup (always) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
93
|
-
cleanupTmp(tmpDir);
|
|
94
|
-
|
|
95
|
-
// โโ Audit log โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
96
|
-
append({
|
|
97
|
-
cmd: 'prescan',
|
|
98
|
-
trigger: 'prescan',
|
|
99
|
-
score: null,
|
|
100
|
-
delta: null,
|
|
101
|
-
findings: allFindings.map(f => ({ id: f.patternId || f.id || '?', severity: f.severity })),
|
|
102
|
-
blocked: criticals.length > 0,
|
|
103
|
-
skill: skillName,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// โโ Output โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
107
101
|
if (!allFindings.length) {
|
|
108
|
-
console.log(` ${paint.green('โ')}
|
|
102
|
+
console.log(` ${paint.green('โ')} ${paint.bold(skillName)} ${paint.dim('โ')} clean ${paint.dim('(' + fileCount + ' file' + (fileCount !== 1 ? 's' : '') + ' scanned, 0 findings)')}`);
|
|
109
103
|
console.log('');
|
|
110
104
|
return 0;
|
|
111
105
|
}
|
|
112
106
|
|
|
113
|
-
// CRITICAL โ print details, block install (exit 1)
|
|
114
107
|
if (criticals.length) {
|
|
115
|
-
console.log(
|
|
116
|
-
console.log(
|
|
108
|
+
console.log(` ${paint.red('โ')} ${paint.bold(skillName)} ${paint.dim('โ')} ${paint.red('BLOCKED')} ${paint.dim('(' + criticals.length + ' critical finding' + (criticals.length !== 1 ? 's' : '') + ')')}`);
|
|
109
|
+
console.log('');
|
|
117
110
|
console.log(SEP);
|
|
118
111
|
for (const f of criticals) {
|
|
119
112
|
console.log('');
|
|
@@ -138,10 +131,10 @@ export async function runPrescan(skillName) {
|
|
|
138
131
|
return 1;
|
|
139
132
|
}
|
|
140
133
|
|
|
141
|
-
// HIGH โ warn, allow
|
|
134
|
+
// HIGH โ warn, allow
|
|
142
135
|
if (highs.length) {
|
|
143
|
-
console.log(
|
|
144
|
-
console.log(
|
|
136
|
+
console.log(` ${paint.yellow('โ ')} ${paint.bold(skillName)} ${paint.dim('โ')} ${paint.yellow('review recommended')} ${paint.dim('(' + highs.length + ' high finding' + (highs.length !== 1 ? 's' : '') + ', ' + fileCount + ' files)')}`);
|
|
137
|
+
console.log('');
|
|
145
138
|
console.log(SEP);
|
|
146
139
|
for (const f of highs) {
|
|
147
140
|
console.log('');
|
|
@@ -152,9 +145,10 @@ export async function runPrescan(skillName) {
|
|
|
152
145
|
}
|
|
153
146
|
}
|
|
154
147
|
console.log('');
|
|
148
|
+
} else {
|
|
149
|
+
console.log(` ${paint.yellow('!')} ${paint.bold(skillName)} ${paint.dim('โ')} ${paint.dim(fileCount + ' files scanned, ' + (mediums.length + lows.length) + ' low/medium findings')}`);
|
|
155
150
|
}
|
|
156
151
|
|
|
157
|
-
// MEDIUM/LOW โ summary line only
|
|
158
152
|
if (mediums.length || lows.length) {
|
|
159
153
|
const parts = [];
|
|
160
154
|
if (mediums.length) parts.push(`${mediums.length} medium`);
|
|
@@ -165,3 +159,105 @@ export async function runPrescan(skillName) {
|
|
|
165
159
|
|
|
166
160
|
return 0;
|
|
167
161
|
}
|
|
162
|
+
|
|
163
|
+
// โโ Download via npm pack โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
164
|
+
|
|
165
|
+
async function scanViaNpm(skillName, tmpDir) {
|
|
166
|
+
let tarball;
|
|
167
|
+
try {
|
|
168
|
+
execSync(`npm pack ${skillName}`, {
|
|
169
|
+
cwd: tmpDir,
|
|
170
|
+
timeout: 30000,
|
|
171
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
172
|
+
});
|
|
173
|
+
const tarballs = readdirSync(tmpDir).filter(f => f.endsWith('.tgz'));
|
|
174
|
+
if (!tarballs.length) throw new Error('npm pack produced no tarball');
|
|
175
|
+
tarball = join(tmpDir, tarballs[0]);
|
|
176
|
+
} catch {
|
|
177
|
+
return null; // not found or network error
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const extractDir = join(tmpDir, 'extracted');
|
|
181
|
+
mkdirSync(extractDir, { recursive: true });
|
|
182
|
+
try {
|
|
183
|
+
execSync(`tar -xzf "${tarball}" -C "${extractDir}"`, {
|
|
184
|
+
timeout: 15000,
|
|
185
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
186
|
+
});
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return extractDir;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// โโ Main export โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
195
|
+
|
|
196
|
+
export async function runPrescan(skillName) {
|
|
197
|
+
console.log('');
|
|
198
|
+
console.log(` ${paint.bold('ClawArmor Prescan')} โ ${paint.cyan(skillName)}`);
|
|
199
|
+
console.log('');
|
|
200
|
+
|
|
201
|
+
let scanDir = null;
|
|
202
|
+
let usedTmp = null;
|
|
203
|
+
let source = 'npm';
|
|
204
|
+
|
|
205
|
+
// โโ Step 1: Check local ClawHub install (for plain skill names) โโโโโโโโโโโ
|
|
206
|
+
if (looksLikeClawHubSkill(skillName)) {
|
|
207
|
+
const localPath = findLocalClawHubSkill(skillName);
|
|
208
|
+
if (localPath) {
|
|
209
|
+
console.log(` ${paint.dim('Found locally:')} ${paint.dim(localPath)}`);
|
|
210
|
+
console.log(` ${paint.dim('Scanning local files...')}`);
|
|
211
|
+
console.log('');
|
|
212
|
+
scanDir = localPath;
|
|
213
|
+
source = 'local';
|
|
214
|
+
} else {
|
|
215
|
+
console.log(` ${paint.dim('Not found locally โ fetching from npm registry...')}`);
|
|
216
|
+
console.log('');
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
console.log(` ${paint.dim('Fetching package from npm registry...')}`);
|
|
220
|
+
console.log('');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// โโ Step 2: Fallback to npm pack if not local โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
224
|
+
if (!scanDir) {
|
|
225
|
+
const tmpDir = join(tmpdir(), `clawarmor-prescan-${Date.now()}`);
|
|
226
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
227
|
+
usedTmp = tmpDir;
|
|
228
|
+
|
|
229
|
+
const extractDir = await scanViaNpm(skillName, tmpDir);
|
|
230
|
+
if (!extractDir) {
|
|
231
|
+
cleanupTmp(tmpDir);
|
|
232
|
+
console.log(` ${paint.dim('โน')} Could not fetch skill for scanning`);
|
|
233
|
+
console.log(` ${paint.dim('(package not found or network error โ install not blocked)')}`);
|
|
234
|
+
console.log('');
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
scanDir = extractDir;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// โโ Step 3: Scan โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
241
|
+
const { allFiles, allFindings } = scanDirectory(scanDir);
|
|
242
|
+
console.log(` ${paint.dim('Scanning')} ${allFiles.length} file${allFiles.length !== 1 ? 's' : ''}...`);
|
|
243
|
+
console.log('');
|
|
244
|
+
|
|
245
|
+
// โโ Cleanup tmp (always, only if we created one) โโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
246
|
+
if (usedTmp) cleanupTmp(usedTmp);
|
|
247
|
+
|
|
248
|
+
// โโ Audit log โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
249
|
+
const criticals = allFindings.filter(f => f.severity === 'CRITICAL');
|
|
250
|
+
append({
|
|
251
|
+
cmd: 'prescan',
|
|
252
|
+
trigger: 'prescan',
|
|
253
|
+
score: null,
|
|
254
|
+
delta: null,
|
|
255
|
+
findings: allFindings.map(f => ({ id: f.patternId || f.id || '?', severity: f.severity })),
|
|
256
|
+
blocked: criticals.length > 0,
|
|
257
|
+
skill: skillName,
|
|
258
|
+
source,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// โโ Output โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
262
|
+
return printResult(skillName, allFiles, allFindings);
|
|
263
|
+
}
|
package/lib/protect.js
CHANGED
|
@@ -18,6 +18,9 @@ const HOOKS_DIR = join(OC_DIR, 'hooks');
|
|
|
18
18
|
const GUARD_HOOK_DIR = join(HOOKS_DIR, 'clawarmor-guard');
|
|
19
19
|
const CLI_PATH = new URL('../cli.js', import.meta.url).pathname;
|
|
20
20
|
|
|
21
|
+
const FISH_FUNCTIONS_DIR = join(HOME, '.config', 'fish', 'functions');
|
|
22
|
+
const FISH_FUNCTION_FILE = join(FISH_FUNCTIONS_DIR, 'openclaw.fish');
|
|
23
|
+
|
|
21
24
|
const SHELL_FUNCTION = `
|
|
22
25
|
# ClawArmor intercept โ added by: clawarmor protect --install
|
|
23
26
|
# Wraps 'openclaw clawhub install' to scan skills before activation.
|
|
@@ -34,6 +37,17 @@ openclaw() {
|
|
|
34
37
|
const SHELL_MARKER_START = '# ClawArmor intercept โ added by: clawarmor protect --install';
|
|
35
38
|
const SHELL_MARKER_END = '# End ClawArmor intercept';
|
|
36
39
|
|
|
40
|
+
const FISH_FUNCTION = `# ClawArmor intercept โ added by: clawarmor protect --install
|
|
41
|
+
function openclaw
|
|
42
|
+
if test (count $argv) -ge 3 -a "$argv[1]" = clawhub -a "$argv[2]" = install
|
|
43
|
+
echo "๐ก ClawArmor: scanning $argv[3] before install..."
|
|
44
|
+
clawarmor prescan $argv[3]; or begin; echo "โ Blocked."; return 1; end
|
|
45
|
+
end
|
|
46
|
+
command openclaw $argv
|
|
47
|
+
end
|
|
48
|
+
`;
|
|
49
|
+
const FISH_MARKER = '# ClawArmor intercept โ added by: clawarmor protect --install';
|
|
50
|
+
|
|
37
51
|
const HOOK_MD = `---
|
|
38
52
|
name: clawarmor-guard
|
|
39
53
|
description: Runs a silent security audit on gateway startup and alerts on regressions
|
|
@@ -148,6 +162,39 @@ export default async function handler(event) {
|
|
|
148
162
|
}
|
|
149
163
|
`;
|
|
150
164
|
|
|
165
|
+
// โโ Fish shell โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
166
|
+
|
|
167
|
+
function fishConfigExists() {
|
|
168
|
+
return existsSync(join(HOME, '.config', 'fish', 'config.fish'));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function fishFunctionPresent() {
|
|
172
|
+
if (!existsSync(FISH_FUNCTION_FILE)) return false;
|
|
173
|
+
try {
|
|
174
|
+
return readFileSync(FISH_FUNCTION_FILE, 'utf8').includes(FISH_MARKER);
|
|
175
|
+
} catch { return false; }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function injectFishFunction() {
|
|
179
|
+
if (!fishConfigExists()) return false;
|
|
180
|
+
if (fishFunctionPresent()) return true;
|
|
181
|
+
try {
|
|
182
|
+
mkdirSync(FISH_FUNCTIONS_DIR, { recursive: true });
|
|
183
|
+
writeFileSync(FISH_FUNCTION_FILE, FISH_FUNCTION, 'utf8');
|
|
184
|
+
return true;
|
|
185
|
+
} catch { return false; }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function removeFishFunction() {
|
|
189
|
+
if (!existsSync(FISH_FUNCTION_FILE)) return false;
|
|
190
|
+
try {
|
|
191
|
+
const content = readFileSync(FISH_FUNCTION_FILE, 'utf8');
|
|
192
|
+
if (!content.includes(FISH_MARKER)) return false;
|
|
193
|
+
rmSync(FISH_FUNCTION_FILE, { force: true });
|
|
194
|
+
return true;
|
|
195
|
+
} catch { return false; }
|
|
196
|
+
}
|
|
197
|
+
|
|
151
198
|
// โโ Install โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
152
199
|
|
|
153
200
|
function writeHookFiles() {
|
|
@@ -251,10 +298,14 @@ export async function runProtect(flags = {}) {
|
|
|
251
298
|
shellPath = shellPath ? shellPath + ', ~/.bashrc' : '~/.bashrc';
|
|
252
299
|
shellInstalled = true;
|
|
253
300
|
}
|
|
301
|
+
if (injectFishFunction()) {
|
|
302
|
+
shellPath = shellPath ? shellPath + ', ~/.config/fish' : '~/.config/fish';
|
|
303
|
+
shellInstalled = true;
|
|
304
|
+
}
|
|
254
305
|
if (shellInstalled) {
|
|
255
306
|
console.log(` โ Shell intercept added (${shellPath})`);
|
|
256
307
|
} else {
|
|
257
|
-
console.log(` ! No ~/.zshrc
|
|
308
|
+
console.log(` ! No ~/.zshrc, ~/.bashrc, or fish config found โ shell intercept skipped`);
|
|
258
309
|
}
|
|
259
310
|
|
|
260
311
|
// 4. Weekly digest cron
|
|
@@ -296,6 +347,10 @@ export async function runProtect(flags = {}) {
|
|
|
296
347
|
console.log(` โ Shell intercept removed from ~/.bashrc`);
|
|
297
348
|
shellRemoved = true;
|
|
298
349
|
}
|
|
350
|
+
if (removeFishFunction()) {
|
|
351
|
+
console.log(` โ Shell intercept removed from ~/.config/fish/functions/openclaw.fish`);
|
|
352
|
+
shellRemoved = true;
|
|
353
|
+
}
|
|
299
354
|
if (!shellRemoved) {
|
|
300
355
|
console.log(` - No shell intercept found to remove`);
|
|
301
356
|
}
|
|
@@ -322,14 +377,15 @@ export async function runProtect(flags = {}) {
|
|
|
322
377
|
// Shell function
|
|
323
378
|
const inZsh = shellFunctionPresent(zshrc);
|
|
324
379
|
const inBash = shellFunctionPresent(bashrc);
|
|
325
|
-
|
|
326
|
-
|
|
380
|
+
const inFish = fishFunctionPresent();
|
|
381
|
+
if (inZsh || inBash || inFish) {
|
|
382
|
+
const where = [inZsh && '~/.zshrc', inBash && '~/.bashrc', inFish && '~/.config/fish'].filter(Boolean).join(', ');
|
|
327
383
|
console.log(` Shell intercept โ active (${where})`);
|
|
328
384
|
} else {
|
|
329
385
|
console.log(` Shell intercept โ not installed`);
|
|
330
386
|
}
|
|
331
387
|
|
|
332
|
-
const allActive = hookOk && daemon.running && (inZsh || inBash);
|
|
388
|
+
const allActive = hookOk && daemon.running && (inZsh || inBash || inFish);
|
|
333
389
|
console.log('');
|
|
334
390
|
if (allActive) {
|
|
335
391
|
console.log(` Full protection active.`);
|
package/lib/status.js
CHANGED
|
@@ -16,9 +16,11 @@ const HISTORY_FILE = join(CLAWARMOR_DIR, 'history.json');
|
|
|
16
16
|
const AUDIT_LOG = join(CLAWARMOR_DIR, 'audit.log');
|
|
17
17
|
const ZSHRC = join(HOME, '.zshrc');
|
|
18
18
|
const BASHRC = join(HOME, '.bashrc');
|
|
19
|
+
const FISH_FUNCTION_FILE = join(HOME, '.config', 'fish', 'functions', 'openclaw.fish');
|
|
19
20
|
const SHELL_MARKER = '# ClawArmor intercept โ added by: clawarmor protect --install';
|
|
20
21
|
const CRON_JOBS_FILE = join(OC_DIR, 'cron', 'jobs.json');
|
|
21
|
-
const
|
|
22
|
+
const HOOKS_DIR = join(OC_DIR, 'hooks', 'clawarmor-guard');
|
|
23
|
+
const VERSION = '2.0.0';
|
|
22
24
|
|
|
23
25
|
const SEP = paint.dim('โ'.repeat(52));
|
|
24
26
|
|
|
@@ -54,13 +56,19 @@ function trendArrow(delta) {
|
|
|
54
56
|
function intercept() {
|
|
55
57
|
const inZsh = existsSync(ZSHRC) && readFileSync(ZSHRC, 'utf8').includes(SHELL_MARKER);
|
|
56
58
|
const inBash = existsSync(BASHRC) && readFileSync(BASHRC, 'utf8').includes(SHELL_MARKER);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
const inFish = existsSync(FISH_FUNCTION_FILE) && readFileSync(FISH_FUNCTION_FILE, 'utf8').includes(SHELL_MARKER);
|
|
60
|
+
if (inZsh || inBash || inFish) {
|
|
61
|
+
const where = [inZsh && '~/.zshrc', inBash && '~/.bashrc', inFish && '~/.config/fish'].filter(Boolean).join(', ');
|
|
59
62
|
return { active: true, where };
|
|
60
63
|
}
|
|
61
64
|
return { active: false, where: null };
|
|
62
65
|
}
|
|
63
66
|
|
|
67
|
+
function hookFilesExist() {
|
|
68
|
+
return existsSync(join(HOOKS_DIR, 'HOOK.md')) &&
|
|
69
|
+
existsSync(join(HOOKS_DIR, 'handler.js'));
|
|
70
|
+
}
|
|
71
|
+
|
|
64
72
|
function parseAuditLog() {
|
|
65
73
|
if (!existsSync(AUDIT_LOG)) return { count: 0, lastEntry: null };
|
|
66
74
|
try {
|
|
@@ -91,7 +99,6 @@ function credentialSummary() {
|
|
|
91
99
|
if (!existsSync(credFile)) return { count: 0, oldestDays: null };
|
|
92
100
|
try {
|
|
93
101
|
const data = JSON.parse(readFileSync(credFile, 'utf8'));
|
|
94
|
-
// Count token entries โ format varies; try common patterns
|
|
95
102
|
let tokens = [];
|
|
96
103
|
if (Array.isArray(data)) tokens = data;
|
|
97
104
|
else if (data.accounts) tokens = Object.values(data.accounts);
|
|
@@ -99,7 +106,6 @@ function credentialSummary() {
|
|
|
99
106
|
|
|
100
107
|
const count = tokens.length;
|
|
101
108
|
|
|
102
|
-
// Try to find oldest by looking for date fields
|
|
103
109
|
let oldest = null;
|
|
104
110
|
for (const tok of tokens) {
|
|
105
111
|
if (tok && typeof tok === 'object') {
|
|
@@ -120,15 +126,13 @@ function configBaselineStatus() {
|
|
|
120
126
|
if (!existsSync(baselineFile)) return { status: 'unknown' };
|
|
121
127
|
try {
|
|
122
128
|
const baseline = JSON.parse(readFileSync(baselineFile, 'utf8'));
|
|
123
|
-
// Simple presence check โ full integrity is handled by lib/integrity.js
|
|
124
129
|
return { status: 'baseline', at: baseline.at || null };
|
|
125
130
|
} catch { return { status: 'unknown' }; }
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
function nextDigestDate() {
|
|
129
|
-
// Next Sunday at 9am
|
|
130
134
|
const now = new Date();
|
|
131
|
-
const dayOfWeek = now.getDay();
|
|
135
|
+
const dayOfWeek = now.getDay();
|
|
132
136
|
const daysUntilSunday = dayOfWeek === 0 ? 7 : 7 - dayOfWeek;
|
|
133
137
|
const next = new Date(now);
|
|
134
138
|
next.setDate(now.getDate() + daysUntilSunday);
|
|
@@ -146,6 +150,15 @@ function digestInstalled() {
|
|
|
146
150
|
return jobs.some(j => j.id === 'clawarmor-weekly-digest');
|
|
147
151
|
}
|
|
148
152
|
|
|
153
|
+
// โโ Grade color (A+/A=green, B=yellow, C=orange/yellow, D/F=red) โโโโโโโโโโโโโ
|
|
154
|
+
|
|
155
|
+
function gradeStatusColor(grade) {
|
|
156
|
+
if (grade === 'A+' || grade === 'A') return paint.green;
|
|
157
|
+
if (grade === 'B') return paint.yellow;
|
|
158
|
+
if (grade === 'C') return paint.yellow; // no orange in ANSI; yellow is closest
|
|
159
|
+
return paint.red; // D, F
|
|
160
|
+
}
|
|
161
|
+
|
|
149
162
|
// โโ Main export โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
150
163
|
|
|
151
164
|
export async function runStatus() {
|
|
@@ -158,12 +171,10 @@ export async function runStatus() {
|
|
|
158
171
|
const history = readJson(HISTORY_FILE) || [];
|
|
159
172
|
const latestHistoryForPosture = history.length ? history[history.length - 1] : null;
|
|
160
173
|
|
|
161
|
-
// Prefer last-score.json (written by watch/guard hook), fall back to history.json
|
|
162
174
|
let score = lastScore?.score ?? latestHistoryForPosture?.score ?? null;
|
|
163
175
|
let grade = lastScore?.grade ?? latestHistoryForPosture?.grade ?? null;
|
|
164
176
|
let scoreTs = lastScore?.timestamp ?? latestHistoryForPosture?.timestamp ?? null;
|
|
165
177
|
|
|
166
|
-
// Trend: compare to ~7 days ago from history
|
|
167
178
|
let weekDelta = null;
|
|
168
179
|
if (history.length >= 2) {
|
|
169
180
|
const weekAgo = Date.now() - 7 * 86_400_000;
|
|
@@ -173,13 +184,13 @@ export async function runStatus() {
|
|
|
173
184
|
}
|
|
174
185
|
}
|
|
175
186
|
|
|
176
|
-
// Score line
|
|
177
187
|
if (score != null) {
|
|
178
188
|
const grade2 = grade || scoreToGrade(score);
|
|
179
189
|
const colorFn = scoreColor(score);
|
|
190
|
+
const gradeFn = gradeStatusColor(grade2);
|
|
180
191
|
const arrow = trendArrow(weekDelta);
|
|
181
192
|
const weekNote = weekDelta != null ? paint.dim(' vs last week') : '';
|
|
182
|
-
console.log(` ${paint.dim('Posture')} ${
|
|
193
|
+
console.log(` ${paint.dim('Posture')} ${gradeFn(grade2)} ${colorFn(score + '/100')} ${arrow}${weekNote}`);
|
|
183
194
|
} else {
|
|
184
195
|
console.log(` ${paint.dim('Posture')} ${paint.dim('No audit data โ run: clawarmor audit')}`);
|
|
185
196
|
}
|
|
@@ -193,9 +204,12 @@ export async function runStatus() {
|
|
|
193
204
|
|
|
194
205
|
// โโ Watcher โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
195
206
|
const daemon = watchDaemonStatus();
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
207
|
+
let watchStr;
|
|
208
|
+
if (daemon.running) {
|
|
209
|
+
watchStr = `${paint.green('โ')} ${paint.bold('running')} ${paint.dim('(PID ' + daemon.pid + ')')}`;
|
|
210
|
+
} else {
|
|
211
|
+
watchStr = `${paint.red('โ')} ${paint.red('stopped')} ${paint.dim('โ run: clawarmor watch --daemon')}`;
|
|
212
|
+
}
|
|
199
213
|
console.log(` ${paint.dim('Watcher')} ${watchStr}`);
|
|
200
214
|
|
|
201
215
|
// โโ Shell intercept โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
@@ -245,6 +259,15 @@ export async function runStatus() {
|
|
|
245
259
|
console.log(` ${paint.dim('Next digest')} ${paint.dim('not scheduled')} ${paint.dim('(run: clawarmor protect --install)')}`);
|
|
246
260
|
}
|
|
247
261
|
|
|
262
|
+
// โโ Full protection footer โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
263
|
+
const hookOk = hookFilesExist();
|
|
264
|
+
const fullProtection = hookOk && daemon.running && icp.active;
|
|
265
|
+
console.log('');
|
|
266
|
+
if (fullProtection) {
|
|
267
|
+
console.log(` Full protection: ${paint.green('[โ YES]')}`);
|
|
268
|
+
} else {
|
|
269
|
+
console.log(` Full protection: ${paint.red('[โ NO')} ${paint.dim('โ run clawarmor protect --install]')}`);
|
|
270
|
+
}
|
|
248
271
|
console.log('');
|
|
249
272
|
return 0;
|
|
250
273
|
}
|