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 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.1.0';
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 HISTORY_PATH = join(homedir(), '.clawarmor', 'history.json');
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
+ }
@@ -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.2.0
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
  /**
@@ -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 = [