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/lib/snapshot.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// clawarmor snapshot — Config snapshot save/load/list/restore.
|
|
2
|
+
// Snapshots are saved to ~/.clawarmor/snapshots/<timestamp>.json.
|
|
3
|
+
// Max 20 snapshots are kept; oldest are pruned on new save.
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync, chmodSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
|
|
9
|
+
const HOME = homedir();
|
|
10
|
+
const SNAPSHOTS_DIR = join(HOME, '.clawarmor', 'snapshots');
|
|
11
|
+
const MAX_SNAPSHOTS = 20;
|
|
12
|
+
|
|
13
|
+
/** @returns {string[]} snapshot filenames sorted oldest-first */
|
|
14
|
+
function listSnapshotFiles() {
|
|
15
|
+
if (!existsSync(SNAPSHOTS_DIR)) return [];
|
|
16
|
+
try {
|
|
17
|
+
return readdirSync(SNAPSHOTS_DIR).filter(f => f.endsWith('.json')).sort();
|
|
18
|
+
} catch { return []; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Prune to MAX_SNAPSHOTS, removing oldest entries. */
|
|
22
|
+
function pruneSnapshots() {
|
|
23
|
+
const files = listSnapshotFiles();
|
|
24
|
+
if (files.length <= MAX_SNAPSHOTS) return;
|
|
25
|
+
for (const f of files.slice(0, files.length - MAX_SNAPSHOTS)) {
|
|
26
|
+
try { unlinkSync(join(SNAPSHOTS_DIR, f)); } catch { /* non-fatal */ }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Save a snapshot before applying fixes.
|
|
32
|
+
* @param {{ trigger: string, configPath: string, configContent: string|null, filePermissions: Object, appliedFixes: string[] }} opts
|
|
33
|
+
* @returns {string|null} snapshot id or null on failure
|
|
34
|
+
*/
|
|
35
|
+
export function saveSnapshot({ trigger, configPath, configContent, filePermissions = {}, appliedFixes = [] }) {
|
|
36
|
+
try {
|
|
37
|
+
if (!existsSync(SNAPSHOTS_DIR)) mkdirSync(SNAPSHOTS_DIR, { recursive: true });
|
|
38
|
+
const timestamp = new Date().toISOString();
|
|
39
|
+
const id = timestamp.replace(/[:.]/g, '-');
|
|
40
|
+
const snapshot = { timestamp, trigger, configPath, configContent, filePermissions, appliedFixes };
|
|
41
|
+
writeFileSync(join(SNAPSHOTS_DIR, `${id}.json`), JSON.stringify(snapshot, null, 2), 'utf8');
|
|
42
|
+
pruneSnapshots();
|
|
43
|
+
return id;
|
|
44
|
+
} catch { return null; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* List all snapshots, newest first.
|
|
49
|
+
* @returns {Array<{ id: string, timestamp: string, trigger: string, appliedFixes: string[], configPath: string }>}
|
|
50
|
+
*/
|
|
51
|
+
export function listSnapshots() {
|
|
52
|
+
return listSnapshotFiles().reverse().map(f => {
|
|
53
|
+
try {
|
|
54
|
+
const data = JSON.parse(readFileSync(join(SNAPSHOTS_DIR, f), 'utf8'));
|
|
55
|
+
return {
|
|
56
|
+
id: f.replace('.json', ''),
|
|
57
|
+
timestamp: data.timestamp,
|
|
58
|
+
trigger: data.trigger || 'unknown',
|
|
59
|
+
appliedFixes: data.appliedFixes || [],
|
|
60
|
+
configPath: data.configPath || null,
|
|
61
|
+
};
|
|
62
|
+
} catch { return null; }
|
|
63
|
+
}).filter(Boolean);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Load a specific snapshot by id.
|
|
68
|
+
* @param {string} id
|
|
69
|
+
* @returns {Object|null}
|
|
70
|
+
*/
|
|
71
|
+
export function loadSnapshot(id) {
|
|
72
|
+
const filePath = join(SNAPSHOTS_DIR, `${id}.json`);
|
|
73
|
+
if (!existsSync(filePath)) return null;
|
|
74
|
+
try { return JSON.parse(readFileSync(filePath, 'utf8')); } catch { return null; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Load the most recent snapshot.
|
|
79
|
+
* @returns {Object|null}
|
|
80
|
+
*/
|
|
81
|
+
export function loadLatestSnapshot() {
|
|
82
|
+
const files = listSnapshotFiles();
|
|
83
|
+
if (!files.length) return null;
|
|
84
|
+
try { return JSON.parse(readFileSync(join(SNAPSHOTS_DIR, files[files.length - 1]), 'utf8')); } catch { return null; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Restore a snapshot: writes config content and restores file permissions.
|
|
89
|
+
* @param {Object} snapshot
|
|
90
|
+
* @returns {{ ok: boolean, err?: string }}
|
|
91
|
+
*/
|
|
92
|
+
export function restoreSnapshot(snapshot) {
|
|
93
|
+
try {
|
|
94
|
+
if (snapshot.configPath && snapshot.configContent != null) {
|
|
95
|
+
writeFileSync(snapshot.configPath, snapshot.configContent, 'utf8');
|
|
96
|
+
}
|
|
97
|
+
for (const [filePath, octalStr] of Object.entries(snapshot.filePermissions || {})) {
|
|
98
|
+
try {
|
|
99
|
+
if (existsSync(filePath)) chmodSync(filePath, parseInt(octalStr, 8));
|
|
100
|
+
} catch { /* non-fatal per-file */ }
|
|
101
|
+
}
|
|
102
|
+
return { ok: true };
|
|
103
|
+
} catch (e) { return { ok: false, err: e.message }; }
|
|
104
|
+
}
|
package/lib/status.js
CHANGED
|
@@ -7,6 +7,7 @@ import { homedir } from 'os';
|
|
|
7
7
|
import { paint } from './output/colors.js';
|
|
8
8
|
import { scoreToGrade, scoreColor, gradeColor } from './output/progress.js';
|
|
9
9
|
import { watchDaemonStatus } from './watch.js';
|
|
10
|
+
import { getMonitorStatus } from './monitor.js';
|
|
10
11
|
|
|
11
12
|
const HOME = homedir();
|
|
12
13
|
const OC_DIR = join(HOME, '.openclaw');
|
|
@@ -20,7 +21,7 @@ const FISH_FUNCTION_FILE = join(HOME, '.config', 'fish', 'functions', 'openclaw.
|
|
|
20
21
|
const SHELL_MARKER = '# ClawArmor intercept — added by: clawarmor protect --install';
|
|
21
22
|
const CRON_JOBS_FILE = join(OC_DIR, 'cron', 'jobs.json');
|
|
22
23
|
const HOOKS_DIR = join(OC_DIR, 'hooks', 'clawarmor-guard');
|
|
23
|
-
const VERSION = '2.
|
|
24
|
+
const VERSION = '2.2.0';
|
|
24
25
|
|
|
25
26
|
const SEP = paint.dim('─'.repeat(52));
|
|
26
27
|
|
|
@@ -212,6 +213,14 @@ export async function runStatus() {
|
|
|
212
213
|
}
|
|
213
214
|
console.log(` ${paint.dim('Watcher')} ${watchStr}`);
|
|
214
215
|
|
|
216
|
+
// ── Monitor mode ──────────────────────────────────────────────────────────
|
|
217
|
+
const monitorStatus = getMonitorStatus();
|
|
218
|
+
if (monitorStatus.enabled) {
|
|
219
|
+
const startedAgo = timeAgo(monitorStatus.startedAt);
|
|
220
|
+
const fixCount = monitorStatus.fixes?.length || 0;
|
|
221
|
+
console.log(` ${paint.dim('Monitor')} ${paint.yellow('●')} ${paint.bold('active')} ${paint.dim('(started ' + startedAgo + ', ' + fixCount + ' fix' + (fixCount !== 1 ? 'es' : '') + ')')} ${paint.dim('→ clawarmor harden --monitor-report')}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
215
224
|
// ── Shell intercept ───────────────────────────────────────────────────────
|
|
216
225
|
const icp = intercept();
|
|
217
226
|
const icpStr = icp.active
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawarmor",
|
|
3
|
-
"version": "2.1
|
|
4
|
-
"description": "Security armor for OpenClaw agents
|
|
3
|
+
"version": "2.2.1",
|
|
4
|
+
"description": "Security armor for OpenClaw agents — audit, scan, monitor",
|
|
5
5
|
"bin": {
|
|
6
6
|
"clawarmor": "cli.js"
|
|
7
7
|
},
|
package/demo-preview.gif
DELETED
|
Binary file
|