clawarmor 1.1.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/lib/fix.js ADDED
@@ -0,0 +1,175 @@
1
+ // clawarmor fix — auto-apply safe one-liner fixes
2
+ import { readFileSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { join } from 'path';
5
+ import { execSync, spawnSync } from 'child_process';
6
+ import { paint } from './output/colors.js';
7
+ import { loadConfig, get } from './config.js';
8
+
9
+ const HISTORY_PATH = join(homedir(), '.clawarmor', 'history.json');
10
+ const SEP = paint.dim('─'.repeat(52));
11
+
12
+ // Fixes that are safe to auto-apply (single config set command, no restart risk)
13
+ const AUTO_FIXABLE = {
14
+ 'browser.ssrf': {
15
+ cmd: 'openclaw config set browser.ssrfPolicy.dangerouslyAllowPrivateNetwork false',
16
+ desc: 'Block browser SSRF to private networks',
17
+ needsRestart: true,
18
+ },
19
+ 'discovery.mdns': {
20
+ cmd: 'openclaw config set discovery.mdns.mode minimal',
21
+ desc: 'Set mDNS to minimal mode',
22
+ needsRestart: true,
23
+ },
24
+ 'logging.redact': {
25
+ cmd: 'openclaw config set logging.redactSensitive tools',
26
+ desc: 'Enable log redaction',
27
+ needsRestart: false,
28
+ },
29
+ 'tools.fs.workspaceOnly': {
30
+ cmd: 'openclaw config set tools.fs.workspaceOnly true',
31
+ desc: 'Restrict filesystem to workspace',
32
+ needsRestart: true,
33
+ },
34
+ 'tools.applyPatch.workspaceOnly': {
35
+ cmd: 'openclaw config set tools.exec.applyPatch.workspaceOnly true',
36
+ desc: 'Restrict apply_patch to workspace',
37
+ needsRestart: true,
38
+ },
39
+ 'fs.config.perms': {
40
+ cmd: 'chmod 600 ~/.openclaw/openclaw.json',
41
+ desc: 'Lock down config file permissions',
42
+ needsRestart: false,
43
+ shell: true,
44
+ },
45
+ 'fs.accounts.perms': {
46
+ cmd: 'chmod 600 ~/.openclaw/agent-accounts.json',
47
+ desc: 'Lock down credentials file permissions',
48
+ needsRestart: false,
49
+ shell: true,
50
+ },
51
+ 'agents.sandbox': {
52
+ // Multi-step fix: enables sandbox WITH workspace access so Telegram sessions don't lose memory files
53
+ cmd: "openclaw config set agents.defaults.sandbox.mode non-main && openclaw config set agents.defaults.sandbox.workspaceAccess rw && openclaw config set agents.defaults.sandbox.scope session",
54
+ desc: 'Enable sandbox isolation (with workspace access preserved for Telegram/group sessions)',
55
+ needsRestart: true,
56
+ requiresDocker: true,
57
+ },
58
+ 'fs.dir.perms': {
59
+ cmd: 'chmod 700 ~/.openclaw',
60
+ desc: 'Lock down ~/.openclaw directory',
61
+ needsRestart: false,
62
+ shell: true,
63
+ },
64
+ };
65
+
66
+ function box(title) {
67
+ const W=52, pad=W-2-title.length, l=Math.floor(pad/2), r=pad-l;
68
+ return [paint.dim('╔'+'═'.repeat(W-2)+'╗'),
69
+ paint.dim('║')+' '.repeat(l)+paint.bold(title)+' '.repeat(r)+paint.dim('║'),
70
+ paint.dim('╚'+'═'.repeat(W-2)+'╝')].join('\n');
71
+ }
72
+
73
+ export async function runFix(flags = {}) {
74
+ console.log(''); console.log(box('ClawArmor Fix v1.1.0')); console.log('');
75
+
76
+ // Load last audit failures
77
+ let lastFailed = [];
78
+ try {
79
+ const h = JSON.parse(readFileSync(HISTORY_PATH,'utf8'));
80
+ const last = h[h.length-1];
81
+ lastFailed = last?.failedIds || [];
82
+ } catch { /* no history */ }
83
+
84
+ if (!lastFailed.length) {
85
+ console.log(` ${paint.dim('No previous audit found. Run')} ${paint.cyan('clawarmor audit')} ${paint.dim('first.')}`);
86
+ console.log(''); return 0;
87
+ }
88
+
89
+ const fixable = lastFailed.filter(id => AUTO_FIXABLE[id]);
90
+ const manual = lastFailed.filter(id => !AUTO_FIXABLE[id]);
91
+
92
+ console.log(` ${paint.dim('Last audit had')} ${paint.bold(String(lastFailed.length))} ${paint.dim('failing checks.')}`);
93
+ console.log(` ${paint.bold(String(fixable.length))} ${paint.dim('can be auto-fixed.')} ${paint.dim(String(manual.length))} ${paint.dim('require manual steps.')}`);
94
+
95
+ if (!fixable.length) {
96
+ console.log('');
97
+ console.log(` ${paint.yellow('!')} No auto-fixable issues. Fix manually:`);
98
+ for (const id of manual) console.log(` ${paint.dim('•')} ${id}`);
99
+ console.log(''); return 0;
100
+ }
101
+
102
+ // Load config for mainKey check (needed by both dry-run and apply paths)
103
+ const { config: cfg } = loadConfig();
104
+ const mainKey = get(cfg, 'agents.mainKey', 'main');
105
+
106
+ console.log('');
107
+ if (flags.dryRun) {
108
+ console.log(` ${paint.cyan('Dry run — would apply:')}`);
109
+ for (const id of fixable) {
110
+ const f = AUTO_FIXABLE[id];
111
+ if (id === 'agents.sandbox' && mainKey !== 'main') {
112
+ console.log(` ${paint.yellow('⚠')} ${paint.bold('Custom mainKey detected:')} agents.mainKey="${mainKey}"`);
113
+ console.log(` ${paint.dim('Verify that sandbox.mode=non-main won\'t affect your main session.')}`);
114
+ }
115
+ console.log(` ${paint.dim('→')} ${f.desc}`);
116
+ console.log(` ${paint.dim(f.cmd)}`);
117
+ if (f.requiresDocker) console.log(` ${paint.yellow('⚠')} ${paint.dim('Requires Docker Desktop to be installed and running')}`);
118
+ if (f.needsRestart) console.log(` ${paint.dim('↻ gateway restart required after applying')}`);
119
+ }
120
+ console.log('');
121
+ console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor fix --apply')} ${paint.dim('to apply these fixes.')}`);
122
+ console.log(''); return 0;
123
+ }
124
+
125
+ if (!flags.apply) {
126
+ console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor fix --dry-run')} ${paint.dim('to preview fixes.')}`);
127
+ console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor fix --apply')} ${paint.dim('to apply them.')}`);
128
+ console.log(''); return 0;
129
+ }
130
+
131
+ // Apply fixes
132
+ let applied = 0, failed = 0, needsRestart = false;
133
+ console.log(SEP);
134
+ for (const id of fixable) {
135
+ const f = AUTO_FIXABLE[id];
136
+
137
+ // Warn about custom mainKey before sandbox fix
138
+ if (id === 'agents.sandbox' && mainKey !== 'main') {
139
+ console.log(` ${paint.yellow('⚠')} ${paint.bold('Custom mainKey detected:')} agents.mainKey="${mainKey}"`);
140
+ console.log(` ${paint.dim('Verify that sandbox.mode=non-main won\'t affect your main session.')}`);
141
+ }
142
+
143
+ process.stdout.write(` ${paint.dim('→')} ${f.desc}...`);
144
+ // Check Docker requirement
145
+ if (f.requiresDocker) {
146
+ const dockerCheck = spawnSync('docker', ['info'], { stdio: 'ignore' });
147
+ if (dockerCheck.error || dockerCheck.status !== 0) {
148
+ process.stdout.write(` ${paint.yellow('⚠')} Docker not running — install Docker Desktop first\n`);
149
+ failed++;
150
+ continue;
151
+ }
152
+ }
153
+ try {
154
+ execSync(f.cmd, { stdio: 'ignore', shell: true });
155
+ process.stdout.write(` ${paint.green('✓')}\n`);
156
+ applied++;
157
+ if (f.needsRestart) needsRestart = true;
158
+ } catch (e) {
159
+ process.stdout.write(` ${paint.red('✗')} ${e.message.split('\n')[0]}\n`);
160
+ failed++;
161
+ }
162
+ }
163
+
164
+ console.log('');
165
+ if (needsRestart) {
166
+ console.log(` ${paint.yellow('!')} ${paint.bold('Restart required:')} ${paint.dim('openclaw gateway restart')}`);
167
+ }
168
+ if (manual.length) {
169
+ console.log(` ${paint.dim('Still needs manual fix:')}`);
170
+ for (const id of manual) console.log(` ${paint.dim('•')} ${id}`);
171
+ }
172
+ console.log('');
173
+ console.log(` Applied ${applied} fix${applied!==1?'es':''}. Run ${paint.cyan('clawarmor verify')} to confirm.`);
174
+ console.log(''); return failed > 0 ? 1 : 0;
175
+ }
@@ -0,0 +1,24 @@
1
+ const C = {
2
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
3
+ red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
4
+ blue: '\x1b[34m', cyan: '\x1b[36m', white: '\x1b[37m',
5
+ bgRed: '\x1b[41m',
6
+ };
7
+ const noColor = process.env.NO_COLOR || !process.stdout.isTTY;
8
+ const c = (code, s) => noColor ? s : `${code}${s}${C.reset}`;
9
+
10
+ export const paint = {
11
+ bold: s => c(C.bold, s), dim: s => c(C.dim, s),
12
+ red: s => c(C.red, s), green: s => c(C.green, s),
13
+ yellow: s => c(C.yellow, s), cyan: s => c(C.cyan, s),
14
+ pass: s => c(C.green, s), high: s => c(C.yellow, s),
15
+ critical: s => c(C.red, s),
16
+ };
17
+
18
+ export const severityColor = {
19
+ CRITICAL: s => c(C.red + C.bold, s),
20
+ HIGH: s => c(C.yellow, s),
21
+ MEDIUM: s => c(C.cyan, s),
22
+ LOW: s => c(C.dim, s),
23
+ INFO: s => c(C.dim, s),
24
+ };
@@ -0,0 +1,212 @@
1
+ // Beautiful terminal output for audit + scan results
2
+ import { paint, severityColor } from './colors.js';
3
+ import { progressBar, scoreColor, gradeColor } from './progress.js';
4
+
5
+ const WIDTH = 46;
6
+ const HR = '─'.repeat(WIDTH);
7
+
8
+ function line(s = '') {
9
+ return s;
10
+ }
11
+
12
+ function box(title) {
13
+ const pad = Math.max(0, WIDTH - 2 - title.length);
14
+ const left = Math.floor(pad / 2);
15
+ const right = pad - left;
16
+ return [
17
+ `╔${'═'.repeat(WIDTH - 2)}╗`,
18
+ `║${' '.repeat(left)}${paint.bold(title)}${' '.repeat(right)}║`,
19
+ `╚${'═'.repeat(WIDTH - 2)}╝`,
20
+ ].join('\n');
21
+ }
22
+
23
+ function sectionHeader(label, count) {
24
+ const countStr = count !== undefined ? ` (${count} ${count === 1 ? 'finding' : count === 0 ? 'checks' : 'findings'})` : '';
25
+ const colorFn = severityColor[label] || paint.bold;
26
+ return [
27
+ line(HR),
28
+ line(` ${colorFn(label)}${paint.dim(countStr)}`),
29
+ line(HR),
30
+ ].join('\n');
31
+ }
32
+
33
+ function passedSection(findings) {
34
+ const lines = [
35
+ line(HR),
36
+ line(` ${paint.green('PASSED')}${paint.dim(` (${findings.length} checks)`)}`),
37
+ line(HR),
38
+ ];
39
+ for (const f of findings) {
40
+ lines.push(` ${paint.pass('✓')} ${paint.dim(f.passedMsg || f.title)}`);
41
+ }
42
+ return lines.join('\n');
43
+ }
44
+
45
+ function finding(f) {
46
+ const icon = paint.fail('✗');
47
+ const title = paint.bold(f.title);
48
+ const lines = [``, ` ${icon} ${title}`];
49
+
50
+ if (f.description) {
51
+ for (const descLine of f.description.split('\n')) {
52
+ lines.push(` ${paint.dim(descLine)}`);
53
+ }
54
+ }
55
+
56
+ if (f.fix) {
57
+ lines.push('');
58
+ const fixLines = f.fix.split('\n');
59
+ lines.push(` ${paint.cyan('Fix:')} ${fixLines[0]}`);
60
+ for (let i = 1; i < fixLines.length; i++) {
61
+ lines.push(` ${fixLines[i]}`);
62
+ }
63
+ }
64
+
65
+ return lines.join('\n');
66
+ }
67
+
68
+ export function formatAuditReport({ configPath, score, grade, findings, passed, scannedAt }) {
69
+ const out = [];
70
+
71
+ out.push('');
72
+ out.push(box('ClawArmor Audit Report'));
73
+ out.push('');
74
+ out.push(` ${paint.dim('Config:')} ${paint.white(configPath)}`);
75
+ out.push(` ${paint.dim('Scanned:')} ${paint.white(scannedAt)}`);
76
+ out.push('');
77
+
78
+ const scoreStr = `${score}/100`;
79
+ const gradeStr = `Grade: ${grade}`;
80
+ const colorScore = scoreColor(score);
81
+ const colorGrade = gradeColor(grade);
82
+ out.push(` ${paint.bold('Security Score:')} ${colorScore(scoreStr)} ${paint.dim('┃')} ${colorGrade(gradeStr)}`);
83
+ out.push(` ${progressBar(score)} ${paint.dim(`${score}%`)}`);
84
+ out.push('');
85
+
86
+ // Group findings by severity
87
+ const bySeverity = { CRITICAL: [], HIGH: [], MEDIUM: [], LOW: [] };
88
+ for (const f of findings) {
89
+ if (bySeverity[f.severity]) bySeverity[f.severity].push(f);
90
+ }
91
+
92
+ for (const [sev, sevFindings] of Object.entries(bySeverity)) {
93
+ if (sevFindings.length === 0) continue;
94
+ out.push(sectionHeader(sev, sevFindings.length));
95
+ for (const f of sevFindings) {
96
+ out.push(finding(f));
97
+ }
98
+ out.push('');
99
+ }
100
+
101
+ if (passed.length > 0) {
102
+ out.push(passedSection(passed));
103
+ out.push('');
104
+ }
105
+
106
+ out.push(line(HR));
107
+ out.push('');
108
+
109
+ if (findings.length === 0) {
110
+ out.push(` ${paint.pass('✓')} ${paint.bold('All checks passed. Your config looks secure.')}`);
111
+ } else {
112
+ out.push(` ${paint.dim('Run')} ${paint.cyan('clawarmor scan')} ${paint.dim('to check installed skills.')}`);
113
+ out.push(` ${paint.dim('Continuous monitoring:')} ${paint.cyan('github.com/pinzasai/clawarmor')}`);
114
+ }
115
+ out.push('');
116
+
117
+ return out.join('\n');
118
+ }
119
+
120
+ // ─── Scan formatter ──────────────────────────────────────────────────────────
121
+
122
+ function scanFinding(f) {
123
+ const icon = paint.fail('✗');
124
+ const title = paint.bold(f.title);
125
+ const lines = [``, ` ${icon} ${title}`];
126
+ lines.push(` ${paint.dim('File:')} ${paint.white(f.file)}${f.line ? paint.dim(`:${f.line}`) : ''}`);
127
+ if (f.snippet) {
128
+ lines.push(` ${paint.dim('Code:')} ${paint.yellow(f.snippet.slice(0, 120))}`);
129
+ }
130
+ if (f.description) {
131
+ lines.push(` ${paint.dim(f.description)}`);
132
+ }
133
+ return lines.join('\n');
134
+ }
135
+
136
+ export function formatScanReport({ skillsScanned, findings, scannedAt, skillsDir }) {
137
+ const out = [];
138
+
139
+ out.push('');
140
+ out.push(box('ClawArmor Skill Scan'));
141
+ out.push('');
142
+ out.push(` ${paint.dim('Directory:')} ${paint.white(skillsDir || 'multiple locations')}`);
143
+ out.push(` ${paint.dim('Scanned:')} ${paint.white(scannedAt)}`);
144
+ out.push(` ${paint.dim('Skills:')} ${paint.white(String(skillsScanned))}`);
145
+ out.push('');
146
+
147
+ if (findings.length === 0) {
148
+ out.push(line(HR));
149
+ out.push('');
150
+ out.push(` ${paint.pass('✓')} ${paint.bold('No suspicious patterns found in installed skills.')}`);
151
+ out.push('');
152
+ out.push(line(HR));
153
+ out.push('');
154
+ return out.join('\n');
155
+ }
156
+
157
+ // Group by skill, then severity
158
+ const bySkill = {};
159
+ for (const f of findings) {
160
+ if (!bySkill[f.skill]) bySkill[f.skill] = [];
161
+ bySkill[f.skill].push(f);
162
+ }
163
+
164
+ for (const [skillName, skillFindings] of Object.entries(bySkill)) {
165
+ const critCount = skillFindings.filter(f => f.severity === 'CRITICAL').length;
166
+ const highCount = skillFindings.filter(f => f.severity === 'HIGH').length;
167
+ const badge = critCount > 0 ? paint.critical(`CRITICAL`) : highCount > 0 ? paint.high('HIGH') : paint.medium('MEDIUM');
168
+ out.push(line(HR));
169
+ out.push(` ${paint.bold(skillName)} ${badge}`);
170
+ out.push(line(HR));
171
+
172
+ const bySev = { CRITICAL: [], HIGH: [], MEDIUM: [], LOW: [] };
173
+ for (const f of skillFindings) {
174
+ if (bySev[f.severity]) bySev[f.severity].push(f);
175
+ }
176
+
177
+ for (const [sev, sevFindings] of Object.entries(bySev)) {
178
+ if (sevFindings.length === 0) continue;
179
+ const colorFn = severityColor[sev];
180
+ out.push(` ${colorFn(sev)} ${paint.dim(`(${sevFindings.length})`)}`);
181
+ for (const f of sevFindings) {
182
+ out.push(scanFinding(f));
183
+ }
184
+ out.push('');
185
+ }
186
+ }
187
+
188
+ out.push(line(HR));
189
+ out.push('');
190
+ out.push(` ${paint.bold(`${findings.length} suspicious ${findings.length === 1 ? 'pattern' : 'patterns'} found`)} across ${Object.keys(bySkill).length} ${Object.keys(bySkill).length === 1 ? 'skill' : 'skills'}.`);
191
+ out.push(` ${paint.dim('Review findings carefully before using these skills.')}`);
192
+ out.push('');
193
+
194
+ return out.join('\n');
195
+ }
196
+
197
+ export function formatNoSkills(dirs) {
198
+ const out = [];
199
+ out.push('');
200
+ out.push(box('ClawArmor Skill Scan'));
201
+ out.push('');
202
+ out.push(` ${paint.dim('No installed skills found.')}`);
203
+ out.push('');
204
+ out.push(` ${paint.dim('Checked:')}`);
205
+ for (const d of dirs) {
206
+ out.push(` ${paint.dim('•')} ${d}`);
207
+ }
208
+ out.push('');
209
+ out.push(` ${paint.dim('Install skills via OpenClaw, then run')} ${paint.cyan('clawarmor scan')} ${paint.dim('again.')}`);
210
+ out.push('');
211
+ return out.join('\n');
212
+ }
@@ -0,0 +1,27 @@
1
+ import { paint } from './colors.js';
2
+
3
+ export function progressBar(score, width = 20) {
4
+ const filled = Math.round((score / 100) * width);
5
+ return '█'.repeat(filled) + '░'.repeat(width - filled);
6
+ }
7
+
8
+ export function scoreToGrade(score) {
9
+ if (score >= 90) return 'A';
10
+ if (score >= 75) return 'B';
11
+ if (score >= 60) return 'C';
12
+ if (score >= 40) return 'D';
13
+ return 'F';
14
+ }
15
+
16
+ export function scoreColor(score) {
17
+ if (score >= 90) return paint.green;
18
+ if (score >= 75) return paint.pass;
19
+ if (score >= 60) return paint.yellow;
20
+ if (score >= 40) return paint.high;
21
+ return paint.critical;
22
+ }
23
+
24
+ export function gradeColor(grade) {
25
+ const map = { A: paint.green, B: paint.pass, C: paint.yellow, D: paint.high, F: paint.critical };
26
+ return (map[grade] || paint.dim)(grade);
27
+ }