clawarmor 2.1.0 → 2.2.1
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 -0
- package/cli.js +16 -2
- package/lib/fix.js +31 -3
- package/lib/harden.js +60 -1
- package/lib/monitor.js +131 -0
- package/lib/rollback.js +98 -0
- package/lib/scanner/obfuscation.js +45 -1
- package/lib/scanner/patterns.js +31 -0
- package/lib/snapshot.js +104 -0
- package/lib/status.js +10 -1
- package/package.json +2 -2
- package/demo-preview.gif +0 -0
- package/demo.cast +0 -680
- package/demo.gif +0 -0
package/README.md
CHANGED
|
@@ -40,6 +40,11 @@ clawarmor audit
|
|
|
40
40
|
| `trend` | ASCII chart of your security score over time |
|
|
41
41
|
| `compare` | Compare coverage vs openclaw security audit |
|
|
42
42
|
| `fix` | Auto-apply safe fixes (--dry-run to preview, --apply to run) |
|
|
43
|
+
| `snapshot` | Save a config snapshot manually (auto-saved before every harden/fix) |
|
|
44
|
+
| `rollback` | Restore config from auto-snapshot (--list, --id <id>) |
|
|
45
|
+
| `harden --monitor` | Enable monitor mode — observe before enforcing |
|
|
46
|
+
| `harden --monitor-report` | Show what monitor mode has observed |
|
|
47
|
+
| `harden --monitor-off` | Disable monitor mode |
|
|
43
48
|
|
|
44
49
|
## What it catches
|
|
45
50
|
|
|
@@ -56,6 +61,26 @@ clawarmor audit
|
|
|
56
61
|
| Gateway exposure | TCP-connects to every non-loopback interface | Full |
|
|
57
62
|
| Runtime policy enforcement | Requires a runtime layer (SupraWall) | None |
|
|
58
63
|
|
|
64
|
+
## Safety features
|
|
65
|
+
|
|
66
|
+
**Impact classification** — Every fix is tagged 🟢 Safe, 🟡 Caution, or 🔴 Breaking. `--auto` mode skips breaking changes unless you pass `--force`.
|
|
67
|
+
|
|
68
|
+
**Config snapshots** — ClawArmor auto-saves your config before every `harden` or `fix` run. If something breaks, roll back instantly:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
clawarmor rollback --list # see all snapshots
|
|
72
|
+
clawarmor rollback # restore the latest
|
|
73
|
+
clawarmor rollback --id <n> # restore a specific one
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Monitor mode** — Observe what `harden` would do before enforcing:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
clawarmor harden --monitor # start monitoring
|
|
80
|
+
clawarmor harden --monitor-report # see what it observed
|
|
81
|
+
clawarmor harden --monitor-off # stop monitoring
|
|
82
|
+
```
|
|
83
|
+
|
|
59
84
|
## Philosophy
|
|
60
85
|
|
|
61
86
|
ClawArmor runs entirely on your machine — no telemetry, no cloud, no accounts.
|
package/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { paint } from './lib/output/colors.js';
|
|
5
5
|
|
|
6
|
-
const VERSION = '2.
|
|
6
|
+
const VERSION = '2.2.0';
|
|
7
7
|
const GATEWAY_PORT_DEFAULT = 18789;
|
|
8
8
|
|
|
9
9
|
function isLocalhost(host) {
|
|
@@ -45,7 +45,8 @@ function usage() {
|
|
|
45
45
|
console.log(` ${paint.cyan('trend')} Show score over last N audits (ASCII chart)`);
|
|
46
46
|
console.log(` ${paint.cyan('compare')} Compare coverage vs openclaw security audit`);
|
|
47
47
|
console.log(` ${paint.cyan('fix')} Auto-apply safe fixes (--dry-run to preview, --apply to run)`);
|
|
48
|
-
console.log(` ${paint.cyan('harden')} Interactive hardening wizard (--dry-run, --auto)`);
|
|
48
|
+
console.log(` ${paint.cyan('harden')} Interactive hardening wizard (--dry-run, --auto, --monitor)`);
|
|
49
|
+
console.log(` ${paint.cyan('rollback')} Restore config from a snapshot (--list, --id <id>)`);
|
|
49
50
|
console.log(` ${paint.cyan('status')} One-screen security posture dashboard`);
|
|
50
51
|
console.log(` ${paint.cyan('watch')} Monitor config and skill changes in real time`);
|
|
51
52
|
console.log(` ${paint.cyan('protect')} Install/uninstall/status the full guard system`);
|
|
@@ -203,11 +204,24 @@ if (cmd === 'harden') {
|
|
|
203
204
|
dryRun: args.includes('--dry-run'),
|
|
204
205
|
auto: args.includes('--auto'),
|
|
205
206
|
force: args.includes('--force'),
|
|
207
|
+
monitor: args.includes('--monitor'),
|
|
208
|
+
monitorReport: args.includes('--monitor-report'),
|
|
209
|
+
monitorOff: args.includes('--monitor-off'),
|
|
206
210
|
};
|
|
207
211
|
const { runHarden } = await import('./lib/harden.js');
|
|
208
212
|
process.exit(await runHarden(hardenFlags));
|
|
209
213
|
}
|
|
210
214
|
|
|
215
|
+
if (cmd === 'rollback') {
|
|
216
|
+
const idIdx = args.indexOf('--id');
|
|
217
|
+
const rollbackFlags = {
|
|
218
|
+
list: args.includes('--list'),
|
|
219
|
+
id: idIdx !== -1 ? args[idIdx + 1] : null,
|
|
220
|
+
};
|
|
221
|
+
const { runRollback } = await import('./lib/rollback.js');
|
|
222
|
+
process.exit(await runRollback(rollbackFlags));
|
|
223
|
+
}
|
|
224
|
+
|
|
211
225
|
if (cmd === 'status') {
|
|
212
226
|
const { runStatus } = await import('./lib/status.js');
|
|
213
227
|
process.exit(await runStatus());
|
package/lib/fix.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// clawarmor fix — auto-apply safe one-liner fixes
|
|
2
2
|
// Now with impact classification: safe / caution / breaking
|
|
3
|
-
import { readFileSync } from 'fs';
|
|
3
|
+
import { readFileSync, existsSync, statSync } from 'fs';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { join } from 'path';
|
|
6
6
|
import { execSync, spawnSync } from 'child_process';
|
|
7
7
|
import { paint } from './output/colors.js';
|
|
8
8
|
import { loadConfig, get } from './config.js';
|
|
9
|
+
import { saveSnapshot } from './snapshot.js';
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
+
const HOME = homedir();
|
|
12
|
+
const HISTORY_PATH = join(HOME, '.clawarmor', 'history.json');
|
|
11
13
|
const SEP = paint.dim('─'.repeat(52));
|
|
12
14
|
|
|
13
15
|
// Impact levels
|
|
@@ -97,6 +99,26 @@ const AUTO_FIXABLE = {
|
|
|
97
99
|
},
|
|
98
100
|
};
|
|
99
101
|
|
|
102
|
+
function expandHome(p) {
|
|
103
|
+
return p.startsWith('~') ? HOME + p.slice(1) : p;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function collectFilePermissions(ids) {
|
|
107
|
+
const perms = {};
|
|
108
|
+
for (const id of ids) {
|
|
109
|
+
const f = AUTO_FIXABLE[id];
|
|
110
|
+
if (!f || !f.shell) continue;
|
|
111
|
+
const m = f.cmd.match(/^chmod\s+\d+\s+(.+)$/);
|
|
112
|
+
if (!m) continue;
|
|
113
|
+
const p = expandHome(m[1].trim());
|
|
114
|
+
try {
|
|
115
|
+
const mode = statSync(p).mode & 0o777;
|
|
116
|
+
perms[p] = mode.toString(8).padStart(3, '0');
|
|
117
|
+
} catch { /* file may not exist */ }
|
|
118
|
+
}
|
|
119
|
+
return perms;
|
|
120
|
+
}
|
|
121
|
+
|
|
100
122
|
function box(title) {
|
|
101
123
|
const W=52, pad=W-2-title.length, l=Math.floor(pad/2), r=pad-l;
|
|
102
124
|
return [paint.dim('╔'+'═'.repeat(W-2)+'╗'),
|
|
@@ -139,7 +161,7 @@ export async function runFix(flags = {}) {
|
|
|
139
161
|
}
|
|
140
162
|
|
|
141
163
|
// Load config for mainKey check
|
|
142
|
-
const { config: cfg } = loadConfig();
|
|
164
|
+
const { config: cfg, configPath } = loadConfig();
|
|
143
165
|
const mainKey = get(cfg, 'agents.mainKey', 'main');
|
|
144
166
|
|
|
145
167
|
console.log('');
|
|
@@ -176,6 +198,12 @@ export async function runFix(flags = {}) {
|
|
|
176
198
|
console.log(''); return 0;
|
|
177
199
|
}
|
|
178
200
|
|
|
201
|
+
// ── Snapshot before applying any fix ──────────────────────────────────────
|
|
202
|
+
const applyTrigger = flags.force ? 'fix --apply --force' : 'fix --apply';
|
|
203
|
+
const toApply = fixable.filter(id => flags.force || AUTO_FIXABLE[id].impact !== IMPACT.BREAKING);
|
|
204
|
+
const configContent = (() => { try { return readFileSync(configPath, 'utf8'); } catch { return null; } })();
|
|
205
|
+
saveSnapshot({ trigger: applyTrigger, configPath, configContent, filePermissions: collectFilePermissions(toApply), appliedFixes: toApply });
|
|
206
|
+
|
|
179
207
|
// Apply fixes
|
|
180
208
|
let applied = 0, failed = 0, skippedBreaking = 0, needsRestart = false;
|
|
181
209
|
console.log(SEP);
|
package/lib/harden.js
CHANGED
|
@@ -13,6 +13,8 @@ import { createInterface } from 'readline';
|
|
|
13
13
|
import { paint } from './output/colors.js';
|
|
14
14
|
import { scoreToGrade, scoreColor, gradeColor } from './output/progress.js';
|
|
15
15
|
import { loadConfig, get } from './config.js';
|
|
16
|
+
import { saveSnapshot } from './snapshot.js';
|
|
17
|
+
import { enableMonitor, disableMonitor, getMonitorStatus, printMonitorReport } from './monitor.js';
|
|
16
18
|
|
|
17
19
|
const HOME = homedir();
|
|
18
20
|
const OC_DIR = join(HOME, '.openclaw');
|
|
@@ -202,6 +204,23 @@ function getManualItems() {
|
|
|
202
204
|
];
|
|
203
205
|
}
|
|
204
206
|
|
|
207
|
+
// ── Collect current file permissions for shell chmod fixes ────────────────────
|
|
208
|
+
|
|
209
|
+
function collectFilePermissions(fixes) {
|
|
210
|
+
const perms = {};
|
|
211
|
+
for (const fix of fixes) {
|
|
212
|
+
if (fix.type !== 'shell') continue;
|
|
213
|
+
const m = fix.action.match(/^chmod\s+\d+\s+(.+)$/);
|
|
214
|
+
if (!m) continue;
|
|
215
|
+
const p = m[1].trim();
|
|
216
|
+
try {
|
|
217
|
+
const mode = statSync(p).mode & 0o777;
|
|
218
|
+
perms[p] = mode.toString(8).padStart(3, '0');
|
|
219
|
+
} catch { /* file may not exist */ }
|
|
220
|
+
}
|
|
221
|
+
return perms;
|
|
222
|
+
}
|
|
223
|
+
|
|
205
224
|
// ── Print impact summary ──────────────────────────────────────────────────────
|
|
206
225
|
|
|
207
226
|
function printImpactSummary(fixes) {
|
|
@@ -219,11 +238,44 @@ function printImpactSummary(fixes) {
|
|
|
219
238
|
// ── Main export ───────────────────────────────────────────────────────────────
|
|
220
239
|
|
|
221
240
|
export async function runHarden(flags = {}) {
|
|
241
|
+
// ── Monitor advisory flags (early return, no box) ──────────────────────────
|
|
242
|
+
if (flags.monitorOff) {
|
|
243
|
+
const ok = disableMonitor();
|
|
244
|
+
console.log('');
|
|
245
|
+
console.log(ok
|
|
246
|
+
? ` ${paint.green('✓')} Monitor mode disabled.`
|
|
247
|
+
: ` ${paint.red('✗')} Failed to disable monitor mode.`);
|
|
248
|
+
console.log('');
|
|
249
|
+
return ok ? 0 : 1;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (flags.monitorReport) {
|
|
253
|
+
const status = getMonitorStatus();
|
|
254
|
+
printMonitorReport(status);
|
|
255
|
+
return 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
222
258
|
console.log(''); console.log(box('ClawArmor Harden v2.1')); console.log('');
|
|
223
259
|
|
|
224
|
-
const { config } = loadConfig();
|
|
260
|
+
const { config, configPath } = loadConfig();
|
|
225
261
|
const fixes = buildFixes(config);
|
|
226
262
|
|
|
263
|
+
// ── Monitor enable (advisory only, no apply) ───────────────────────────────
|
|
264
|
+
if (flags.monitor) {
|
|
265
|
+
const fixIds = fixes.map(f => f.id);
|
|
266
|
+
const ok = enableMonitor(fixIds);
|
|
267
|
+
if (ok) {
|
|
268
|
+
console.log(` ${paint.green('✓')} Monitor mode enabled.`);
|
|
269
|
+
console.log(` ${paint.dim('Observing:')} ${fixIds.length ? fixIds.join(', ') : 'no current fixes found'}`);
|
|
270
|
+
console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor harden --monitor-report')} ${paint.dim('to see what would have changed.')}`);
|
|
271
|
+
console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor harden --monitor-off')} ${paint.dim('to disable.')}`);
|
|
272
|
+
} else {
|
|
273
|
+
console.log(` ${paint.red('✗')} Failed to enable monitor mode.`);
|
|
274
|
+
}
|
|
275
|
+
console.log('');
|
|
276
|
+
return ok ? 0 : 1;
|
|
277
|
+
}
|
|
278
|
+
|
|
227
279
|
// Snapshot before score
|
|
228
280
|
const before = loadLastHistoryEntry();
|
|
229
281
|
const beforeScore = before?.score ?? null;
|
|
@@ -306,6 +358,13 @@ export async function runHarden(flags = {}) {
|
|
|
306
358
|
}
|
|
307
359
|
console.log('');
|
|
308
360
|
|
|
361
|
+
// ── Snapshot before applying any fix ──────────────────────────────────────
|
|
362
|
+
const trigger = flags.auto
|
|
363
|
+
? (flags.force ? 'harden --auto --force' : 'harden --auto')
|
|
364
|
+
: 'harden --interactive';
|
|
365
|
+
const configContent = (() => { try { return readFileSync(configPath, 'utf8'); } catch { return null; } })();
|
|
366
|
+
saveSnapshot({ trigger, configPath, configContent, filePermissions: collectFilePermissions(fixes), appliedFixes: fixes.map(f => f.id) });
|
|
367
|
+
|
|
309
368
|
let applied = 0, skipped = 0, failed = 0, skippedBreaking = 0;
|
|
310
369
|
const restartNotes = [];
|
|
311
370
|
|
package/lib/monitor.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// clawarmor monitor — Monitor mode state management and reporting.
|
|
2
|
+
// Monitor state stored in ~/.clawarmor/monitor.json:
|
|
3
|
+
// { enabled: true, startedAt: ISO, fixes: [fix ids being monitored] }
|
|
4
|
+
// Advisory only — no config changes are applied.
|
|
5
|
+
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
import { paint } from './output/colors.js';
|
|
10
|
+
|
|
11
|
+
const HOME = homedir();
|
|
12
|
+
const CLAWARMOR_DIR = join(HOME, '.clawarmor');
|
|
13
|
+
const MONITOR_FILE = join(CLAWARMOR_DIR, 'monitor.json');
|
|
14
|
+
const AUDIT_LOG = join(CLAWARMOR_DIR, 'audit.log');
|
|
15
|
+
|
|
16
|
+
const SEP = paint.dim('─'.repeat(52));
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get current monitor mode status.
|
|
20
|
+
* @returns {{ enabled: boolean, startedAt?: string, fixes?: string[] }}
|
|
21
|
+
*/
|
|
22
|
+
export function getMonitorStatus() {
|
|
23
|
+
if (!existsSync(MONITOR_FILE)) return { enabled: false };
|
|
24
|
+
try { return JSON.parse(readFileSync(MONITOR_FILE, 'utf8')); } catch { return { enabled: false }; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Enable monitor mode, recording the fix ids to observe.
|
|
29
|
+
* @param {string[]} fixIds
|
|
30
|
+
* @returns {boolean} success
|
|
31
|
+
*/
|
|
32
|
+
export function enableMonitor(fixIds = []) {
|
|
33
|
+
try {
|
|
34
|
+
if (!existsSync(CLAWARMOR_DIR)) mkdirSync(CLAWARMOR_DIR, { recursive: true });
|
|
35
|
+
const data = { enabled: true, startedAt: new Date().toISOString(), fixes: fixIds };
|
|
36
|
+
writeFileSync(MONITOR_FILE, JSON.stringify(data, null, 2), 'utf8');
|
|
37
|
+
return true;
|
|
38
|
+
} catch { return false; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Disable monitor mode by removing the state file.
|
|
43
|
+
* @returns {boolean} success
|
|
44
|
+
*/
|
|
45
|
+
export function disableMonitor() {
|
|
46
|
+
try {
|
|
47
|
+
if (existsSync(MONITOR_FILE)) unlinkSync(MONITOR_FILE);
|
|
48
|
+
return true;
|
|
49
|
+
} catch { return false; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @returns {Array<Object>} parsed audit log entries since given ISO timestamp */
|
|
53
|
+
function readAuditsSince(sinceIso) {
|
|
54
|
+
if (!existsSync(AUDIT_LOG)) return [];
|
|
55
|
+
try {
|
|
56
|
+
const since = new Date(sinceIso).getTime();
|
|
57
|
+
return readFileSync(AUDIT_LOG, 'utf8')
|
|
58
|
+
.split('\n').filter(Boolean)
|
|
59
|
+
.map(l => { try { return JSON.parse(l); } catch { return null; } })
|
|
60
|
+
.filter(l => l && new Date(l.ts).getTime() >= since);
|
|
61
|
+
} catch { return []; }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Print a monitor mode report showing audit activity since monitoring started.
|
|
66
|
+
* @param {{ enabled: boolean, startedAt?: string, fixes?: string[] }} status
|
|
67
|
+
*/
|
|
68
|
+
export function printMonitorReport(status) {
|
|
69
|
+
console.log('');
|
|
70
|
+
console.log(` ${paint.bold('Monitor Mode Report')}`);
|
|
71
|
+
console.log('');
|
|
72
|
+
|
|
73
|
+
if (!status.enabled || !status.startedAt) {
|
|
74
|
+
console.log(` ${paint.dim('Monitor mode is not active.')}`);
|
|
75
|
+
console.log(` ${paint.dim('Enable with:')} ${paint.cyan('clawarmor harden --monitor')}`);
|
|
76
|
+
console.log('');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const monitoredFixes = status.fixes || [];
|
|
81
|
+
const audits = readAuditsSince(status.startedAt);
|
|
82
|
+
|
|
83
|
+
console.log(` ${paint.dim('Started:')} ${new Date(status.startedAt).toLocaleString()}`);
|
|
84
|
+
console.log(` ${paint.dim('Monitoring:')} ${monitoredFixes.length ? monitoredFixes.join(', ') : 'all available fixes'}`);
|
|
85
|
+
console.log(` ${paint.dim('Audits run:')} ${audits.length}`);
|
|
86
|
+
console.log('');
|
|
87
|
+
|
|
88
|
+
if (!audits.length) {
|
|
89
|
+
console.log(` ${paint.dim('No audits found since monitoring started.')}`);
|
|
90
|
+
console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor audit')} ${paint.dim('to generate data.')}`);
|
|
91
|
+
console.log('');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(SEP);
|
|
96
|
+
console.log('');
|
|
97
|
+
|
|
98
|
+
const scores = audits.map(a => a.score).filter(s => typeof s === 'number');
|
|
99
|
+
if (scores.length >= 2) {
|
|
100
|
+
const delta = scores[scores.length - 1] - scores[0];
|
|
101
|
+
const deltaStr = delta > 0 ? paint.green(`+${delta}`) : delta < 0 ? paint.red(String(delta)) : paint.dim('±0');
|
|
102
|
+
console.log(` ${paint.dim('Score change:')} ${scores[0]} → ${scores[scores.length - 1]} (${deltaStr})`);
|
|
103
|
+
} else if (scores.length === 1) {
|
|
104
|
+
console.log(` ${paint.dim('Score observed:')} ${scores[0]}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let criticalCount = 0, highCount = 0;
|
|
108
|
+
for (const a of audits) {
|
|
109
|
+
if (Array.isArray(a.findings)) {
|
|
110
|
+
for (const f of a.findings) {
|
|
111
|
+
if (f.severity === 'CRITICAL') criticalCount++;
|
|
112
|
+
else if (f.severity === 'HIGH') highCount++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (criticalCount > 0 || highCount > 0) {
|
|
117
|
+
console.log(` ${paint.dim('Findings:')} ${paint.red(String(criticalCount) + ' critical')} ${paint.dim('·')} ${paint.yellow(String(highCount) + ' high')}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log('');
|
|
121
|
+
|
|
122
|
+
const lastScore = scores.length ? scores[scores.length - 1] : null;
|
|
123
|
+
if (lastScore !== null && lastScore < 75) {
|
|
124
|
+
console.log(` ${paint.yellow('!')} ${paint.bold('Recommendation:')} Score is below 75. Consider applying fixes:`);
|
|
125
|
+
console.log(` ${paint.dim('Run:')} ${paint.cyan('clawarmor harden')}`);
|
|
126
|
+
} else {
|
|
127
|
+
console.log(` ${paint.green('✓')} Monitoring period looks stable.`);
|
|
128
|
+
console.log(` ${paint.dim('When ready, apply fixes with:')} ${paint.cyan('clawarmor harden')}`);
|
|
129
|
+
}
|
|
130
|
+
console.log('');
|
|
131
|
+
}
|
package/lib/rollback.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// clawarmor rollback — Restore config from a saved snapshot.
|
|
2
|
+
// clawarmor rollback Restore most recent snapshot
|
|
3
|
+
// clawarmor rollback --list List all available snapshots with details
|
|
4
|
+
// clawarmor rollback --id <id> Restore a specific snapshot by id
|
|
5
|
+
|
|
6
|
+
import { paint } from './output/colors.js';
|
|
7
|
+
import { listSnapshots, loadSnapshot, loadLatestSnapshot, restoreSnapshot } from './snapshot.js';
|
|
8
|
+
|
|
9
|
+
const SEP = paint.dim('─'.repeat(52));
|
|
10
|
+
|
|
11
|
+
function box(title) {
|
|
12
|
+
const W = 52, pad = W - 2 - title.length, l = Math.floor(pad / 2), r = pad - l;
|
|
13
|
+
return [
|
|
14
|
+
paint.dim('╔' + '═'.repeat(W - 2) + '╗'),
|
|
15
|
+
paint.dim('║') + ' '.repeat(l) + paint.bold(title) + ' '.repeat(r) + paint.dim('║'),
|
|
16
|
+
paint.dim('╚' + '═'.repeat(W - 2) + '╝'),
|
|
17
|
+
].join('\n');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function fmtDate(isoString) {
|
|
21
|
+
if (!isoString) return 'unknown';
|
|
22
|
+
try { return new Date(isoString).toLocaleString(); } catch { return isoString; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Main rollback command.
|
|
27
|
+
* @param {{ list?: boolean, id?: string }} flags
|
|
28
|
+
*/
|
|
29
|
+
export async function runRollback(flags = {}) {
|
|
30
|
+
console.log(''); console.log(box('ClawArmor Rollback')); console.log('');
|
|
31
|
+
|
|
32
|
+
// ── LIST mode ─────────────────────────────────────────────────────────────
|
|
33
|
+
if (flags.list) {
|
|
34
|
+
const snapshots = listSnapshots();
|
|
35
|
+
if (!snapshots.length) {
|
|
36
|
+
console.log(` ${paint.dim('No snapshots found.')}`);
|
|
37
|
+
console.log(` ${paint.dim('Snapshots are created automatically before every harden or fix run.')}`);
|
|
38
|
+
console.log('');
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(` ${paint.bold(String(snapshots.length))} snapshot${snapshots.length !== 1 ? 's' : ''} available:`);
|
|
43
|
+
console.log('');
|
|
44
|
+
for (const s of snapshots) {
|
|
45
|
+
const fixList = s.appliedFixes.length ? s.appliedFixes.join(', ') : 'no fixes recorded';
|
|
46
|
+
console.log(` ${paint.cyan(s.id)}`);
|
|
47
|
+
console.log(` ${paint.dim('Date:')} ${fmtDate(s.timestamp)}`);
|
|
48
|
+
console.log(` ${paint.dim('Trigger:')} ${s.trigger}`);
|
|
49
|
+
console.log(` ${paint.dim('Fixes:')} ${fixList}`);
|
|
50
|
+
console.log('');
|
|
51
|
+
}
|
|
52
|
+
console.log(` ${paint.dim('To restore a specific snapshot:')} clawarmor rollback --id <id>`);
|
|
53
|
+
console.log('');
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── RESTORE specific snapshot or most recent ───────────────────────────────
|
|
58
|
+
let snapshot;
|
|
59
|
+
if (flags.id) {
|
|
60
|
+
snapshot = loadSnapshot(flags.id);
|
|
61
|
+
if (!snapshot) {
|
|
62
|
+
console.log(` ${paint.red('✗')} Snapshot not found: ${paint.bold(flags.id)}`);
|
|
63
|
+
console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor rollback --list')} ${paint.dim('to see available snapshots.')}`);
|
|
64
|
+
console.log('');
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
snapshot = loadLatestSnapshot();
|
|
69
|
+
if (!snapshot) {
|
|
70
|
+
console.log(` ${paint.dim('No snapshots found.')}`);
|
|
71
|
+
console.log(` ${paint.dim('Snapshots are created automatically before every harden or fix run.')}`);
|
|
72
|
+
console.log('');
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(` ${paint.dim('Snapshot:')} ${fmtDate(snapshot.timestamp)}`);
|
|
78
|
+
console.log(` ${paint.dim('Trigger:')} ${snapshot.trigger}`);
|
|
79
|
+
if (snapshot.appliedFixes?.length) {
|
|
80
|
+
console.log(` ${paint.dim('Fixes:')} ${snapshot.appliedFixes.join(', ')}`);
|
|
81
|
+
}
|
|
82
|
+
console.log('');
|
|
83
|
+
console.log(SEP);
|
|
84
|
+
console.log('');
|
|
85
|
+
|
|
86
|
+
const result = restoreSnapshot(snapshot);
|
|
87
|
+
if (!result.ok) {
|
|
88
|
+
console.log(` ${paint.red('✗')} Restore failed: ${result.err}`);
|
|
89
|
+
console.log('');
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const dateStr = fmtDate(snapshot.timestamp);
|
|
94
|
+
console.log(` ${paint.green('✓')} Restored to snapshot from ${paint.bold(dateStr)}.`);
|
|
95
|
+
console.log(` ${paint.dim('Run')} ${paint.cyan('openclaw gateway restart')} ${paint.dim('to apply.')}`);
|
|
96
|
+
console.log('');
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// obfuscation.js — v1.
|
|
1
|
+
// obfuscation.js — v1.3.0
|
|
2
2
|
// Detects obfuscated code patterns that bypass naive string-grep analysis.
|
|
3
3
|
// Zero external dependencies. Pure regex, adversarially reviewed.
|
|
4
4
|
//
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
// - globalThis/global bracket access to dangerous names
|
|
11
11
|
// - ['constructor'] escape pattern
|
|
12
12
|
// - Unicode/hex escape sequences for dangerous keywords
|
|
13
|
+
// - Dynamic import() with runtime-assembled module name
|
|
14
|
+
// - eval/exec called with interpolated template literal
|
|
15
|
+
// - Proxy/Reflect wrapping of dangerous objects
|
|
16
|
+
// - Variable aliasing of dangerous functions (const e = eval)
|
|
13
17
|
|
|
14
18
|
export const OBFUSCATION_PATTERNS = [
|
|
15
19
|
{
|
|
@@ -81,6 +85,46 @@ export const OBFUSCATION_PATTERNS = [
|
|
|
81
85
|
description: "Calls require() or import() with a runtime-decoded string argument (atob, fromCharCode, etc.).",
|
|
82
86
|
regex: /(?:require|import)\s*\(\s*(?:atob|String\.fromCharCode|Buffer\.from)\s*\(/,
|
|
83
87
|
},
|
|
88
|
+
{
|
|
89
|
+
// Pattern: const mod = 'child' + '_process'; import(mod)
|
|
90
|
+
// The module name is never visible as a literal string, bypassing child_process regex.
|
|
91
|
+
id: 'obfus-dynamic-import-concat',
|
|
92
|
+
severity: 'CRITICAL',
|
|
93
|
+
title: 'Dynamic import() with runtime-assembled module name',
|
|
94
|
+
description: "import() called with a variable or concatenated string — module name assembled at runtime, bypassing static child_process/net detection.",
|
|
95
|
+
note: "Pattern: const mod = 'child' + '_process'; import(mod). The dangerous module name never appears intact in source.",
|
|
96
|
+
regex: /\bimport\s*\(\s*(?:[a-zA-Z_$][a-zA-Z0-9_$]*\s*\)|['"`][^'"`]*['"`]\s*\+)/,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
// Pattern: eval(`(function() { ${userCode} })()`)
|
|
100
|
+
// Template literal interpolation allows runtime code injection hidden from literal-string scanners.
|
|
101
|
+
id: 'obfus-template-literal',
|
|
102
|
+
severity: 'HIGH',
|
|
103
|
+
title: 'eval/exec called with interpolated template literal',
|
|
104
|
+
description: "eval or exec invoked with a template literal containing ${...} interpolation — injects runtime values into executed code.",
|
|
105
|
+
note: "eval(`code ${var}`) assembles executable code from runtime values. Evades scanners that only check string literals.",
|
|
106
|
+
regex: /\b(?:eval|Function|exec|execSync)\s*\(\s*`[^`]*\$\{/,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
// Pattern: new Proxy(process, handler) or Reflect.get(globalThis, 'eval')
|
|
110
|
+
// Proxying dangerous objects intercepts property access for exfiltration or modification.
|
|
111
|
+
id: 'obfus-proxy-reflect',
|
|
112
|
+
severity: 'HIGH',
|
|
113
|
+
title: 'Proxy/Reflect wrapping of dangerous object',
|
|
114
|
+
description: "Wrapping process, require, or globalThis in a Proxy intercepts all property access — used for covert exfiltration or to modify dangerous function behavior.",
|
|
115
|
+
note: "new Proxy(process, handler) can log every process property access. Reflect.get(globalThis, 'eval') accesses eval indirectly.",
|
|
116
|
+
regex: /new\s+Proxy\s*\(\s*(?:process|require|global|globalThis)\b|Reflect\s*\.\s*(?:get|apply)\s*\(\s*(?:globalThis|global|process)\b/,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
// Pattern: const e = eval; e(code) or const {execSync: run} = require('child_process')
|
|
120
|
+
// Alias hides the dangerous function name at all call sites, bypassing keyword scanners.
|
|
121
|
+
id: 'obfus-var-alias',
|
|
122
|
+
severity: 'HIGH',
|
|
123
|
+
title: 'Variable aliasing of dangerous function',
|
|
124
|
+
description: "Assigning eval, exec, or spawn to a new variable name so call sites evade keyword detection.",
|
|
125
|
+
note: "const e = eval; e(code) — the dangerous eval() call is hidden as e(). Destructuring rename: const {execSync: run} = require('child_process').",
|
|
126
|
+
regex: /(?:const|let|var)\s+\w+\s*=\s*eval\b|(?:const|let|var)\s+\{[^}]*(?:exec|spawn)[^}]*:\s*\w+[^}]*\}\s*=/,
|
|
127
|
+
},
|
|
84
128
|
];
|
|
85
129
|
|
|
86
130
|
/**
|
package/lib/scanner/patterns.js
CHANGED
|
@@ -12,6 +12,12 @@ export const CRITICAL_PATTERNS = [
|
|
|
12
12
|
title: 'Pipe-to-shell pattern', description: 'curl|bash or wget|sh — classic RCE.' },
|
|
13
13
|
{ id: 'vm-run', regex: /vm\.(runInNewContext|runInThisContext)\s*\(/,
|
|
14
14
|
title: 'vm module code execution', description: 'Executes code in Node.js VM.' },
|
|
15
|
+
// Binding a raw TCP server is the primary reverse-shell / C2 setup technique.
|
|
16
|
+
// net.createServer in skill code has virtually no legitimate use case.
|
|
17
|
+
{ id: 'reverse-shell',
|
|
18
|
+
regex: /net\.createServer\s*\(|(?:require\(['"`]net['"`]\)|import\(['"`]net['"`]\))[\s\S]{0,300}\.createServer\s*\(/,
|
|
19
|
+
title: 'net.createServer() — reverse shell / port binding',
|
|
20
|
+
description: 'Creating a raw TCP server is the primary mechanism for reverse shells and covert C2 listeners.' },
|
|
15
21
|
];
|
|
16
22
|
|
|
17
23
|
export const HIGH_PATTERNS = [
|
|
@@ -27,6 +33,31 @@ export const HIGH_PATTERNS = [
|
|
|
27
33
|
{ id: 'exfil-combo', regex: /process\.env[\s\S]{0,200}(fetch|axios|http|request)\s*\(/,
|
|
28
34
|
title: 'Env vars + network call (exfil pattern)',
|
|
29
35
|
description: 'Reading env vars then making network calls — credential exfiltration pattern.' },
|
|
36
|
+
// WebSocket bypasses fetch/axios-based detection entirely — a silent exfil channel.
|
|
37
|
+
{ id: 'websocket-exfil',
|
|
38
|
+
regex: /new\s+WebSocket\s*\(|(?:ws|socket)\s*\.send\s*\(/,
|
|
39
|
+
title: 'WebSocket usage (potential data exfiltration)',
|
|
40
|
+
description: 'WebSocket connections can silently exfiltrate data — not caught by fetch/axios-based detection rules.' },
|
|
41
|
+
// DNS can encode secrets in subdomain queries; no HTTP logs, evades most monitoring.
|
|
42
|
+
{ id: 'dns-exfil',
|
|
43
|
+
regex: /require\(['"`](?:dns|node:dns)['"`]\)|from\s+['"`](?:dns|node:dns)['"`]/,
|
|
44
|
+
title: 'DNS module imported (covert channel risk)',
|
|
45
|
+
description: 'DNS can encode data in subdomain queries — a covert exfiltration channel that evades HTTP monitoring.' },
|
|
46
|
+
// __proto__ assignment or Object.prototype mutation corrupts the global object graph.
|
|
47
|
+
{ id: 'proto-pollution',
|
|
48
|
+
regex: /__proto__\s*["'`]|Object\.prototype\s*\[/,
|
|
49
|
+
title: 'Prototype pollution',
|
|
50
|
+
description: 'Assigning to __proto__ or Object.prototype mutates all JS objects — enables object injection attacks.' },
|
|
51
|
+
// Extends exfil-combo to cover outbound channels beyond fetch: WebSocket and DNS.
|
|
52
|
+
{ id: 'exfil-combo-broad',
|
|
53
|
+
regex: /process\.env[\s\S]{0,200}(?:new\s+WebSocket|ws\.send\s*\(|dns\.resolve\s*\(|dns\.lookup\s*\()/,
|
|
54
|
+
title: 'Env vars + WebSocket/DNS outbound (broad exfil)',
|
|
55
|
+
description: 'process.env followed by WebSocket or DNS send — exfiltration path not caught by fetch-only rules.' },
|
|
56
|
+
// Credential file + network call within same scope = high-confidence theft combo.
|
|
57
|
+
{ id: 'cred-read-network',
|
|
58
|
+
regex: /readFileSync\s*\(['"`][^'"`]*(?:\.openclaw|agent-accounts|credentials)[^'"`]*['"`][\s\S]{0,500}(?:fetch|axios|new\s+WebSocket|ws\.send|http\.request)/,
|
|
59
|
+
title: 'Credential file read + outbound network call',
|
|
60
|
+
description: 'Reading a credential file then making a network call in the same scope — credential theft combo.' },
|
|
30
61
|
];
|
|
31
62
|
|
|
32
63
|
export const MEDIUM_PATTERNS = [
|