clawarmor 2.0.0-alpha.2 → 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 +23 -1
- package/lib/digest.js +157 -0
- package/lib/harden.js +312 -0
- package/lib/output/progress.js +2 -1
- package/lib/prescan.js +166 -70
- package/lib/protect.js +90 -15
- package/lib/status.js +273 -0
- package/package.json +1 -1
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() {
|
|
@@ -222,29 +269,52 @@ export async function runProtect(flags = {}) {
|
|
|
222
269
|
|
|
223
270
|
// 1. Hook files
|
|
224
271
|
writeHookFiles();
|
|
225
|
-
console.log(` ✓
|
|
272
|
+
console.log(` ✓ Gateway hook installed (clawarmor-guard)`);
|
|
226
273
|
|
|
227
|
-
// 2.
|
|
274
|
+
// 2. Start watch daemon
|
|
275
|
+
const daemonStarted = startWatchDaemon();
|
|
276
|
+
let daemonPid = null;
|
|
277
|
+
if (daemonStarted) {
|
|
278
|
+
// Read the PID that was written by the daemon
|
|
279
|
+
try {
|
|
280
|
+
const pidFile = join(CLAWARMOR_DIR, 'watch.pid');
|
|
281
|
+
if (existsSync(pidFile)) daemonPid = readFileSync(pidFile, 'utf8').trim();
|
|
282
|
+
} catch { /* non-fatal */ }
|
|
283
|
+
const pidStr = daemonPid ? ` (PID ${daemonPid})` : '';
|
|
284
|
+
console.log(` ✓ Watch daemon started${pidStr}`);
|
|
285
|
+
} else {
|
|
286
|
+
console.log(` ! Watch daemon could not be started automatically`);
|
|
287
|
+
console.log(` Run manually: clawarmor watch --daemon`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 3. Shell intercept
|
|
228
291
|
let shellInstalled = false;
|
|
292
|
+
let shellPath = null;
|
|
229
293
|
if (injectShellFunction(zshrc)) {
|
|
230
|
-
|
|
294
|
+
shellPath = '~/.zshrc';
|
|
231
295
|
shellInstalled = true;
|
|
232
296
|
}
|
|
233
297
|
if (injectShellFunction(bashrc)) {
|
|
234
|
-
|
|
298
|
+
shellPath = shellPath ? shellPath + ', ~/.bashrc' : '~/.bashrc';
|
|
299
|
+
shellInstalled = true;
|
|
300
|
+
}
|
|
301
|
+
if (injectFishFunction()) {
|
|
302
|
+
shellPath = shellPath ? shellPath + ', ~/.config/fish' : '~/.config/fish';
|
|
235
303
|
shellInstalled = true;
|
|
236
304
|
}
|
|
237
|
-
if (
|
|
238
|
-
console.log(`
|
|
305
|
+
if (shellInstalled) {
|
|
306
|
+
console.log(` ✓ Shell intercept added (${shellPath})`);
|
|
307
|
+
} else {
|
|
308
|
+
console.log(` ! No ~/.zshrc, ~/.bashrc, or fish config found — shell intercept skipped`);
|
|
239
309
|
}
|
|
240
310
|
|
|
241
|
-
//
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
311
|
+
// 4. Weekly digest cron
|
|
312
|
+
const { installDigestCron } = await import('./digest.js');
|
|
313
|
+
const cronOk = installDigestCron();
|
|
314
|
+
if (cronOk) {
|
|
315
|
+
console.log(` ✓ Weekly digest scheduled (Sundays 9am)`);
|
|
245
316
|
} else {
|
|
246
|
-
console.log(` !
|
|
247
|
-
console.log(` Run manually: clawarmor watch --daemon`);
|
|
317
|
+
console.log(` ! Could not write digest cron job`);
|
|
248
318
|
}
|
|
249
319
|
|
|
250
320
|
console.log('\n ClawArmor Protect is now active.\n');
|
|
@@ -277,6 +347,10 @@ export async function runProtect(flags = {}) {
|
|
|
277
347
|
console.log(` ✓ Shell intercept removed from ~/.bashrc`);
|
|
278
348
|
shellRemoved = true;
|
|
279
349
|
}
|
|
350
|
+
if (removeFishFunction()) {
|
|
351
|
+
console.log(` ✓ Shell intercept removed from ~/.config/fish/functions/openclaw.fish`);
|
|
352
|
+
shellRemoved = true;
|
|
353
|
+
}
|
|
280
354
|
if (!shellRemoved) {
|
|
281
355
|
console.log(` - No shell intercept found to remove`);
|
|
282
356
|
}
|
|
@@ -303,14 +377,15 @@ export async function runProtect(flags = {}) {
|
|
|
303
377
|
// Shell function
|
|
304
378
|
const inZsh = shellFunctionPresent(zshrc);
|
|
305
379
|
const inBash = shellFunctionPresent(bashrc);
|
|
306
|
-
|
|
307
|
-
|
|
380
|
+
const inFish = fishFunctionPresent();
|
|
381
|
+
if (inZsh || inBash || inFish) {
|
|
382
|
+
const where = [inZsh && '~/.zshrc', inBash && '~/.bashrc', inFish && '~/.config/fish'].filter(Boolean).join(', ');
|
|
308
383
|
console.log(` Shell intercept ✓ active (${where})`);
|
|
309
384
|
} else {
|
|
310
385
|
console.log(` Shell intercept ✗ not installed`);
|
|
311
386
|
}
|
|
312
387
|
|
|
313
|
-
const allActive = hookOk && daemon.running && (inZsh || inBash);
|
|
388
|
+
const allActive = hookOk && daemon.running && (inZsh || inBash || inFish);
|
|
314
389
|
console.log('');
|
|
315
390
|
if (allActive) {
|
|
316
391
|
console.log(` Full protection active.`);
|
package/lib/status.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// clawarmor status — One-screen security posture dashboard.
|
|
2
|
+
// Shows: score+grade+trend, last audit, watcher, intercept, log, skills, config, credentials, next digest.
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { paint } from './output/colors.js';
|
|
8
|
+
import { scoreToGrade, scoreColor, gradeColor } from './output/progress.js';
|
|
9
|
+
import { watchDaemonStatus } from './watch.js';
|
|
10
|
+
|
|
11
|
+
const HOME = homedir();
|
|
12
|
+
const OC_DIR = join(HOME, '.openclaw');
|
|
13
|
+
const CLAWARMOR_DIR = join(HOME, '.clawarmor');
|
|
14
|
+
const LAST_SCORE_FILE = join(CLAWARMOR_DIR, 'last-score.json');
|
|
15
|
+
const HISTORY_FILE = join(CLAWARMOR_DIR, 'history.json');
|
|
16
|
+
const AUDIT_LOG = join(CLAWARMOR_DIR, 'audit.log');
|
|
17
|
+
const ZSHRC = join(HOME, '.zshrc');
|
|
18
|
+
const BASHRC = join(HOME, '.bashrc');
|
|
19
|
+
const FISH_FUNCTION_FILE = join(HOME, '.config', 'fish', 'functions', 'openclaw.fish');
|
|
20
|
+
const SHELL_MARKER = '# ClawArmor intercept — added by: clawarmor protect --install';
|
|
21
|
+
const CRON_JOBS_FILE = join(OC_DIR, 'cron', 'jobs.json');
|
|
22
|
+
const HOOKS_DIR = join(OC_DIR, 'hooks', 'clawarmor-guard');
|
|
23
|
+
const VERSION = '2.0.0';
|
|
24
|
+
|
|
25
|
+
const SEP = paint.dim('─'.repeat(52));
|
|
26
|
+
|
|
27
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
function readJson(file) {
|
|
30
|
+
try {
|
|
31
|
+
if (!existsSync(file)) return null;
|
|
32
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
33
|
+
} catch { return null; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function timeAgo(isoString) {
|
|
37
|
+
if (!isoString) return 'never';
|
|
38
|
+
const ms = Date.now() - new Date(isoString).getTime();
|
|
39
|
+
const secs = Math.floor(ms / 1000);
|
|
40
|
+
if (secs < 60) return `${secs}s ago`;
|
|
41
|
+
const mins = Math.floor(secs / 60);
|
|
42
|
+
if (mins < 60) return `${mins}m ago`;
|
|
43
|
+
const hours = Math.floor(mins / 60);
|
|
44
|
+
if (hours < 24) return `${hours}h ago`;
|
|
45
|
+
const days = Math.floor(hours / 24);
|
|
46
|
+
return `${days}d ago`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function trendArrow(delta) {
|
|
50
|
+
if (delta == null) return paint.dim('—');
|
|
51
|
+
if (delta > 0) return paint.green(`↑+${delta}`);
|
|
52
|
+
if (delta < 0) return paint.red(`↓${delta}`);
|
|
53
|
+
return paint.dim('→±0');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function intercept() {
|
|
57
|
+
const inZsh = existsSync(ZSHRC) && readFileSync(ZSHRC, 'utf8').includes(SHELL_MARKER);
|
|
58
|
+
const inBash = existsSync(BASHRC) && readFileSync(BASHRC, 'utf8').includes(SHELL_MARKER);
|
|
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(', ');
|
|
62
|
+
return { active: true, where };
|
|
63
|
+
}
|
|
64
|
+
return { active: false, where: null };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hookFilesExist() {
|
|
68
|
+
return existsSync(join(HOOKS_DIR, 'HOOK.md')) &&
|
|
69
|
+
existsSync(join(HOOKS_DIR, 'handler.js'));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseAuditLog() {
|
|
73
|
+
if (!existsSync(AUDIT_LOG)) return { count: 0, lastEntry: null };
|
|
74
|
+
try {
|
|
75
|
+
const lines = readFileSync(AUDIT_LOG, 'utf8')
|
|
76
|
+
.split('\n')
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.map(l => { try { return JSON.parse(l); } catch { return null; } })
|
|
79
|
+
.filter(Boolean);
|
|
80
|
+
return { count: lines.length, lastEntry: lines[lines.length - 1] || null };
|
|
81
|
+
} catch { return { count: 0, lastEntry: null }; }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function countInstalledSkills() {
|
|
85
|
+
const dirs = [];
|
|
86
|
+
const userSkillsDir = join(OC_DIR, 'skills');
|
|
87
|
+
if (existsSync(userSkillsDir)) {
|
|
88
|
+
try {
|
|
89
|
+
dirs.push(...readdirSync(userSkillsDir, { withFileTypes: true })
|
|
90
|
+
.filter(e => e.isDirectory())
|
|
91
|
+
.map(e => join(userSkillsDir, e.name)));
|
|
92
|
+
} catch { /* skip */ }
|
|
93
|
+
}
|
|
94
|
+
return dirs.length;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function credentialSummary() {
|
|
98
|
+
const credFile = join(OC_DIR, 'agent-accounts.json');
|
|
99
|
+
if (!existsSync(credFile)) return { count: 0, oldestDays: null };
|
|
100
|
+
try {
|
|
101
|
+
const data = JSON.parse(readFileSync(credFile, 'utf8'));
|
|
102
|
+
let tokens = [];
|
|
103
|
+
if (Array.isArray(data)) tokens = data;
|
|
104
|
+
else if (data.accounts) tokens = Object.values(data.accounts);
|
|
105
|
+
else if (typeof data === 'object') tokens = Object.values(data);
|
|
106
|
+
|
|
107
|
+
const count = tokens.length;
|
|
108
|
+
|
|
109
|
+
let oldest = null;
|
|
110
|
+
for (const tok of tokens) {
|
|
111
|
+
if (tok && typeof tok === 'object') {
|
|
112
|
+
const dateStr = tok.createdAt || tok.created_at || tok.timestamp || null;
|
|
113
|
+
if (dateStr) {
|
|
114
|
+
const d = new Date(dateStr);
|
|
115
|
+
if (!isNaN(d) && (!oldest || d < oldest)) oldest = d;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const oldestDays = oldest ? Math.floor((Date.now() - oldest.getTime()) / 86_400_000) : null;
|
|
120
|
+
return { count, oldestDays };
|
|
121
|
+
} catch { return { count: 0, oldestDays: null }; }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function configBaselineStatus() {
|
|
125
|
+
const baselineFile = join(CLAWARMOR_DIR, 'config-baseline.json');
|
|
126
|
+
if (!existsSync(baselineFile)) return { status: 'unknown' };
|
|
127
|
+
try {
|
|
128
|
+
const baseline = JSON.parse(readFileSync(baselineFile, 'utf8'));
|
|
129
|
+
return { status: 'baseline', at: baseline.at || null };
|
|
130
|
+
} catch { return { status: 'unknown' }; }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function nextDigestDate() {
|
|
134
|
+
const now = new Date();
|
|
135
|
+
const dayOfWeek = now.getDay();
|
|
136
|
+
const daysUntilSunday = dayOfWeek === 0 ? 7 : 7 - dayOfWeek;
|
|
137
|
+
const next = new Date(now);
|
|
138
|
+
next.setDate(now.getDate() + daysUntilSunday);
|
|
139
|
+
next.setHours(9, 0, 0, 0);
|
|
140
|
+
const daysUntil = Math.ceil((next - now) / 86_400_000);
|
|
141
|
+
return {
|
|
142
|
+
label: next.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' }),
|
|
143
|
+
daysUntil,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function digestInstalled() {
|
|
148
|
+
const jobs = readJson(CRON_JOBS_FILE);
|
|
149
|
+
if (!Array.isArray(jobs)) return false;
|
|
150
|
+
return jobs.some(j => j.id === 'clawarmor-weekly-digest');
|
|
151
|
+
}
|
|
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
|
+
|
|
162
|
+
// ── Main export ───────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
export async function runStatus() {
|
|
165
|
+
console.log('');
|
|
166
|
+
console.log(` ${paint.bold('ClawArmor')} ${paint.dim('v' + VERSION)} ${paint.dim('—')} ${paint.bold('Security Status')}`);
|
|
167
|
+
console.log('');
|
|
168
|
+
|
|
169
|
+
// ── Posture ──────────────────────────────────────────────────────────────
|
|
170
|
+
const lastScore = readJson(LAST_SCORE_FILE);
|
|
171
|
+
const history = readJson(HISTORY_FILE) || [];
|
|
172
|
+
const latestHistoryForPosture = history.length ? history[history.length - 1] : null;
|
|
173
|
+
|
|
174
|
+
let score = lastScore?.score ?? latestHistoryForPosture?.score ?? null;
|
|
175
|
+
let grade = lastScore?.grade ?? latestHistoryForPosture?.grade ?? null;
|
|
176
|
+
let scoreTs = lastScore?.timestamp ?? latestHistoryForPosture?.timestamp ?? null;
|
|
177
|
+
|
|
178
|
+
let weekDelta = null;
|
|
179
|
+
if (history.length >= 2) {
|
|
180
|
+
const weekAgo = Date.now() - 7 * 86_400_000;
|
|
181
|
+
const weekEntry = [...history].reverse().find(h => new Date(h.timestamp).getTime() < weekAgo);
|
|
182
|
+
if (weekEntry && score != null) {
|
|
183
|
+
weekDelta = score - weekEntry.score;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (score != null) {
|
|
188
|
+
const grade2 = grade || scoreToGrade(score);
|
|
189
|
+
const colorFn = scoreColor(score);
|
|
190
|
+
const gradeFn = gradeStatusColor(grade2);
|
|
191
|
+
const arrow = trendArrow(weekDelta);
|
|
192
|
+
const weekNote = weekDelta != null ? paint.dim(' vs last week') : '';
|
|
193
|
+
console.log(` ${paint.dim('Posture')} ${gradeFn(grade2)} ${colorFn(score + '/100')} ${arrow}${weekNote}`);
|
|
194
|
+
} else {
|
|
195
|
+
console.log(` ${paint.dim('Posture')} ${paint.dim('No audit data — run: clawarmor audit')}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Last audit ────────────────────────────────────────────────────────────
|
|
199
|
+
const { count: logCount, lastEntry } = parseAuditLog();
|
|
200
|
+
const lastAuditTs = lastEntry?.ts ?? scoreTs ?? null;
|
|
201
|
+
const trigger = lastEntry?.trigger ?? 'manual';
|
|
202
|
+
const auditAgo = lastAuditTs ? timeAgo(lastAuditTs) : 'never';
|
|
203
|
+
console.log(` ${paint.dim('Last audit')} ${paint.bold(auditAgo)} ${paint.dim('(' + trigger + ')')}`);
|
|
204
|
+
|
|
205
|
+
// ── Watcher ───────────────────────────────────────────────────────────────
|
|
206
|
+
const daemon = watchDaemonStatus();
|
|
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
|
+
}
|
|
213
|
+
console.log(` ${paint.dim('Watcher')} ${watchStr}`);
|
|
214
|
+
|
|
215
|
+
// ── Shell intercept ───────────────────────────────────────────────────────
|
|
216
|
+
const icp = intercept();
|
|
217
|
+
const icpStr = icp.active
|
|
218
|
+
? `${paint.green('✓')} ${paint.bold('active')} ${paint.dim('(' + icp.where + ')')}`
|
|
219
|
+
: `${paint.red('✗')} ${paint.dim('not installed')}`;
|
|
220
|
+
console.log(` ${paint.dim('Intercept')} ${icpStr}`);
|
|
221
|
+
|
|
222
|
+
// ── Audit log ─────────────────────────────────────────────────────────────
|
|
223
|
+
console.log(` ${paint.dim('Audit log')} ${paint.bold(String(logCount))} ${paint.dim('events')} ${paint.dim('(clawarmor log to view)')}`);
|
|
224
|
+
|
|
225
|
+
console.log('');
|
|
226
|
+
console.log(SEP);
|
|
227
|
+
|
|
228
|
+
// ── Skills ────────────────────────────────────────────────────────────────
|
|
229
|
+
const skillCount = countInstalledSkills();
|
|
230
|
+
console.log(` ${paint.dim('Skills')} ${paint.bold(String(skillCount))} ${paint.dim('installed')} ${paint.dim('(clawarmor scan to check)')}`);
|
|
231
|
+
|
|
232
|
+
// ── Config baseline ───────────────────────────────────────────────────────
|
|
233
|
+
const baseline = configBaselineStatus();
|
|
234
|
+
const configStr = baseline.status === 'baseline'
|
|
235
|
+
? `${paint.green('Baseline match')} ${paint.green('✓')}`
|
|
236
|
+
: paint.dim('No baseline yet — run: clawarmor audit');
|
|
237
|
+
console.log(` ${paint.dim('Config')} ${configStr}`);
|
|
238
|
+
|
|
239
|
+
// ── Credentials ───────────────────────────────────────────────────────────
|
|
240
|
+
const creds = credentialSummary();
|
|
241
|
+
let credStr = creds.count > 0
|
|
242
|
+
? `${paint.bold(String(creds.count))} ${paint.dim('tokens')}`
|
|
243
|
+
: paint.dim('none found');
|
|
244
|
+
if (creds.oldestDays != null) {
|
|
245
|
+
const ageMark = creds.oldestDays > 90 ? paint.yellow('!') : paint.green('✓');
|
|
246
|
+
credStr += `${paint.dim(', oldest:')} ${creds.oldestDays}d ${ageMark}`;
|
|
247
|
+
}
|
|
248
|
+
console.log(` ${paint.dim('Credentials')} ${credStr}`);
|
|
249
|
+
|
|
250
|
+
console.log('');
|
|
251
|
+
console.log(SEP);
|
|
252
|
+
|
|
253
|
+
// ── Next digest ───────────────────────────────────────────────────────────
|
|
254
|
+
const digestOk = digestInstalled();
|
|
255
|
+
const nextDigest = nextDigestDate();
|
|
256
|
+
if (digestOk) {
|
|
257
|
+
console.log(` ${paint.dim('Next digest')} ${paint.bold(nextDigest.label)} ${paint.dim('(' + nextDigest.daysUntil + ' days)')}`);
|
|
258
|
+
} else {
|
|
259
|
+
console.log(` ${paint.dim('Next digest')} ${paint.dim('not scheduled')} ${paint.dim('(run: clawarmor protect --install)')}`);
|
|
260
|
+
}
|
|
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
|
+
}
|
|
271
|
+
console.log('');
|
|
272
|
+
return 0;
|
|
273
|
+
}
|