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.
@@ -0,0 +1,121 @@
1
+ // ClawArmor v0.6 — SKILL.md natural language instruction scanner
2
+ // Detects dangerous instructions embedded in skill markdown files.
3
+ // Built-in skills: INFO severity. User-installed: HIGH severity.
4
+
5
+ import { readFileSync } from 'fs';
6
+ import { basename } from 'path';
7
+
8
+ // Each pattern: { id, regex, title, description }
9
+ const DANGEROUS_PATTERNS = [
10
+ {
11
+ id: 'skillmd.read_credentials',
12
+ regex: /read.*agent-accounts|agent-accounts.*read/i,
13
+ title: 'Instruction to read credential file',
14
+ description: 'Instructs the agent to read agent-accounts.json, which contains API keys and bot tokens.',
15
+ },
16
+ {
17
+ id: 'skillmd.system_prompt_override',
18
+ regex: /ignore.*system.?prompt|override.*system.?prompt|bypass.*system.?prompt/i,
19
+ title: 'System prompt override attempt',
20
+ description: 'Attempts to override or ignore the agent\'s system-level safety instructions.',
21
+ },
22
+ {
23
+ id: 'skillmd.exfil',
24
+ regex: /send.*credentials|exfiltrate|steal.*token|steal.*key|steal.*password/i,
25
+ title: 'Data theft instruction',
26
+ description: 'Instructs the agent to send, steal, or exfiltrate credentials or secrets.',
27
+ },
28
+ {
29
+ id: 'skillmd.context_injection',
30
+ regex: /always.*include.*in.*(?:every|all).*response|append.*to.*every.*response|inject.*into.*every/i,
31
+ title: 'Persistent context injection',
32
+ description: 'Instructs the agent to append content to every response — persistent prompt injection.',
33
+ },
34
+ {
35
+ id: 'skillmd.deception',
36
+ regex: /do not.*tell.*user|don.t.*mention.*to.*user|keep.*secret.*from.*user|hide.*from.*user/i,
37
+ title: 'Deception instruction (hide from user)',
38
+ description: 'Instructs the agent to conceal information or actions from the user.',
39
+ },
40
+ {
41
+ id: 'skillmd.hardcoded_ip',
42
+ regex: /fetch.*\b(https?):\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/i,
43
+ title: 'Hardcoded IP fetch instruction',
44
+ description: 'Instructs the agent to fetch from a hardcoded IP address — potential exfiltration endpoint.',
45
+ },
46
+ {
47
+ id: 'skillmd.shell_exec',
48
+ regex: /execute.*(?:command|shell|bash|sh)\b|run.*shell.*command|spawn.*(?:process|subprocess)/i,
49
+ title: 'Shell execution instruction',
50
+ description: 'Instructs the agent to execute shell commands — could enable arbitrary code execution.',
51
+ },
52
+ {
53
+ id: 'skillmd.fs_write',
54
+ regex: /write.*to.*(?:file|disk|filesystem)|modify.*(?:config|configuration|settings).*file/i,
55
+ title: 'Filesystem modification instruction',
56
+ description: 'Instructs the agent to write files or modify config — potential persistent compromise.',
57
+ },
58
+ ];
59
+
60
+ // Scan a single SKILL.md file content
61
+ export function scanSkillMd(filePath, content, isBuiltin) {
62
+ const findings = [];
63
+ const lines = content.split('\n');
64
+
65
+ for (const pattern of DANGEROUS_PATTERNS) {
66
+ const regex = new RegExp(pattern.regex.source, 'gi');
67
+ let m;
68
+ const matches = [];
69
+ while ((m = regex.exec(content)) !== null) {
70
+ const lineNum = content.substring(0, m.index).split('\n').length;
71
+ const lineText = lines[lineNum - 1]?.trim() || '';
72
+ // Skip lines that are clearly comments or code fences showing examples
73
+ if (lineText.startsWith('```') || lineText.startsWith('//') || lineText.startsWith('#!')) continue;
74
+ // Skip if this looks like documentation warning about the pattern (not instructing it)
75
+ if (/do not|don't|never|avoid|dangerous|warning|caution|example of/i.test(
76
+ lines.slice(Math.max(0, lineNum - 2), lineNum).join(' ')
77
+ )) continue;
78
+ matches.push({ line: lineNum, snippet: lineText.substring(0, 120) });
79
+ if (matches.length >= 3) break;
80
+ }
81
+
82
+ if (!matches.length) continue;
83
+
84
+ findings.push({
85
+ patternId: pattern.id,
86
+ severity: isBuiltin ? 'INFO' : 'HIGH',
87
+ title: pattern.title,
88
+ description: pattern.description,
89
+ file: filePath,
90
+ matches,
91
+ note: isBuiltin
92
+ ? 'Built-in skill — review only if recently updated or unexpected.'
93
+ : 'User-installed skill — treat dangerous instructions as HIGH risk.',
94
+ });
95
+ }
96
+
97
+ return findings;
98
+ }
99
+
100
+ // Find all SKILL.md files within a skill directory's file list
101
+ export function getSkillMdFiles(files) {
102
+ return files.filter(f => basename(f).toLowerCase() === 'skill.md');
103
+ }
104
+
105
+ // Scan all SKILL.md files for a skill
106
+ export function scanSkillMdFiles(skillFiles, isBuiltin) {
107
+ const results = [];
108
+ const mdFiles = getSkillMdFiles(skillFiles);
109
+
110
+ for (const filePath of mdFiles) {
111
+ let content;
112
+ try { content = readFileSync(filePath, 'utf8'); }
113
+ catch { continue; }
114
+ if (content.length > 200_000) continue;
115
+
116
+ const findings = scanSkillMd(filePath, content, isBuiltin);
117
+ if (findings.length) results.push({ filePath, findings });
118
+ }
119
+
120
+ return results;
121
+ }
package/lib/trend.js ADDED
@@ -0,0 +1,180 @@
1
+ // ClawArmor v1.1.0 — trend command
2
+ // Shows score over last N audits as an ASCII line chart.
3
+
4
+ import { paint } from './output/colors.js';
5
+ import { loadHistory } from './audit.js';
6
+
7
+ const SEP = paint.dim('─'.repeat(52));
8
+ const W52 = 52;
9
+
10
+ function box(title) {
11
+ const pad = W52 - 2 - title.length;
12
+ const l = Math.floor(pad/2), r = pad - l;
13
+ return [
14
+ paint.dim('╔' + '═'.repeat(W52-2) + '╗'),
15
+ paint.dim('║') + ' '.repeat(l) + paint.bold(title) + ' '.repeat(r) + paint.dim('║'),
16
+ paint.dim('╚' + '═'.repeat(W52-2) + '╝'),
17
+ ].join('\n');
18
+ }
19
+
20
+ function scoreColor(score) {
21
+ if (score >= 90) return paint.green;
22
+ if (score >= 75) return s => paint.pass ? paint.pass(s) : s;
23
+ if (score >= 60) return paint.yellow;
24
+ return paint.red;
25
+ }
26
+
27
+ function formatDate(iso) {
28
+ const d = new Date(iso);
29
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
30
+ }
31
+
32
+ // Build ASCII line chart from score data points
33
+ // chartWidth: number of columns, chartHeight: number of rows
34
+ function buildChart(scores, chartWidth = 36, chartHeight = 8) {
35
+ if (!scores.length) return [];
36
+ const minScore = 0;
37
+ const maxScore = 100;
38
+ const range = maxScore - minScore;
39
+
40
+ // Map each score to a row (0 = bottom, chartHeight-1 = top)
41
+ function scoreToRow(s) {
42
+ return Math.round(((s - minScore) / range) * (chartHeight - 1));
43
+ }
44
+
45
+ // Build a 2D grid: rows[row][col]
46
+ const grid = Array.from({ length: chartHeight }, () => Array(chartWidth).fill(' '));
47
+
48
+ // Place points and draw connecting lines
49
+ const n = scores.length;
50
+ const colStep = n === 1 ? 0 : (chartWidth - 1) / (n - 1);
51
+
52
+ for (let i = 0; i < n; i++) {
53
+ const col = Math.round(i * colStep);
54
+ const row = chartHeight - 1 - scoreToRow(scores[i]);
55
+
56
+ if (i > 0) {
57
+ const prevCol = Math.round((i - 1) * colStep);
58
+ const prevRow = chartHeight - 1 - scoreToRow(scores[i - 1]);
59
+ // Draw line between previous and current point
60
+ const dc = col - prevCol;
61
+ const dr = row - prevRow;
62
+ const steps = Math.max(Math.abs(dc), Math.abs(dr), 1);
63
+ for (let s = 0; s <= steps; s++) {
64
+ const ic = Math.round(prevCol + (dc * s) / steps);
65
+ const ir = Math.round(prevRow + (dr * s) / steps);
66
+ if (ic >= 0 && ic < chartWidth && ir >= 0 && ir < chartHeight) {
67
+ if (grid[ir][ic] === ' ') {
68
+ // Choose connecting character based on direction
69
+ if (dr === 0) grid[ir][ic] = '─';
70
+ else if (dc === 0) grid[ir][ic] = '│';
71
+ else if ((dr < 0 && dc > 0) || (dr > 0 && dc < 0)) grid[ir][ic] = '╯';
72
+ else grid[ir][ic] = '╭';
73
+ }
74
+ }
75
+ }
76
+ }
77
+ // Place data point (overwrite connection chars)
78
+ const r2 = chartHeight - 1 - scoreToRow(scores[i]);
79
+ if (col >= 0 && col < chartWidth && r2 >= 0 && r2 < chartHeight) {
80
+ grid[r2][col] = '●';
81
+ }
82
+ }
83
+
84
+ return grid;
85
+ }
86
+
87
+ export async function runTrend(flags = {}) {
88
+ const N = flags.n || 10;
89
+ const history = loadHistory();
90
+
91
+ console.log(''); console.log(box('ClawArmor Trend v1.1.0')); console.log('');
92
+
93
+ if (!history.length) {
94
+ console.log(` ${paint.dim('No audit history yet.')}`);
95
+ console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor audit')} ${paint.dim('to start tracking your score.')}`);
96
+ console.log('');
97
+ return 0;
98
+ }
99
+
100
+ const recent = history.slice(-N);
101
+ const scores = recent.map(h => h.score);
102
+ const first = scores[0];
103
+ const last = scores[scores.length - 1];
104
+ const delta = last - first;
105
+
106
+ console.log(` ${paint.dim('Last')} ${paint.bold(String(recent.length))} ${paint.dim('audit' + (recent.length !== 1 ? 's' : ''))}`);
107
+ console.log('');
108
+
109
+ if (recent.length === 1) {
110
+ const sc = scoreColor(last);
111
+ console.log(` ${paint.dim('Score:')} ${sc(String(last) + '/100')} ${paint.dim('(only one audit — run more to see trend)')}`);
112
+ console.log('');
113
+ return 0;
114
+ }
115
+
116
+ // Build chart
117
+ const CHART_W = 36;
118
+ const CHART_H = 8;
119
+ const grid = buildChart(scores, CHART_W, CHART_H);
120
+
121
+ // Y-axis labels: 0, 25, 50, 75, 100
122
+ const yLabels = [100, 75, 50, 25, 0];
123
+
124
+ // Print chart with Y-axis
125
+ for (let row = 0; row < CHART_H; row++) {
126
+ // Find nearest y label for this row
127
+ const scoreAtRow = Math.round(100 - (row / (CHART_H - 1)) * 100);
128
+ const showLabel = yLabels.includes(scoreAtRow);
129
+ const label = showLabel ? String(scoreAtRow).padStart(3) : ' ';
130
+ const line = grid[row].join('');
131
+ // Color the chart line
132
+ const colored = line.replace(/●/g, paint.cyan('●'));
133
+ console.log(` ${paint.dim(label)} ${paint.dim('┤')} ${colored}`);
134
+ }
135
+
136
+ // X-axis
137
+ const xAxis = '─'.repeat(CHART_W + 2);
138
+ console.log(` ${paint.dim('└' + xAxis)}`);
139
+
140
+ // Date labels: first and last
141
+ const dateFirst = formatDate(recent[0].timestamp);
142
+ const dateLast = formatDate(recent[recent.length - 1].timestamp);
143
+ const dateGap = Math.max(0, CHART_W - dateFirst.length - dateLast.length + 2);
144
+ console.log(` ${paint.dim(dateFirst)}${' '.repeat(dateGap)}${paint.dim(dateLast)}`);
145
+
146
+ console.log('');
147
+
148
+ // Summary line
149
+ const deltaStr = delta > 0 ? paint.green(`+${delta}`) : delta < 0 ? paint.red(String(delta)) : paint.dim('±0');
150
+ const sc = scoreColor(last);
151
+ console.log(` ${paint.dim('Current score:')} ${sc(String(last) + '/100')}`);
152
+
153
+ if (recent.length > 1) {
154
+ if (delta === 0) {
155
+ console.log(` ${paint.dim('Score unchanged since first audit.')}`);
156
+ } else {
157
+ const word = delta > 0 ? 'improved' : 'dropped';
158
+ console.log(` Score ${word} ${deltaStr} ${paint.dim('points since')} ${paint.dim(formatDate(recent[0].timestamp))}.`);
159
+ }
160
+ }
161
+
162
+ // Show per-audit table if ≤ 7 entries
163
+ if (recent.length <= 7) {
164
+ console.log('');
165
+ console.log(SEP);
166
+ console.log(` ${paint.dim('Date')} ${paint.dim('Score')} ${paint.dim('Grade')} ${paint.dim('Issues')}`);
167
+ console.log(SEP);
168
+ for (const h of recent) {
169
+ const sc2 = scoreColor(h.score);
170
+ const date = formatDate(h.timestamp).padEnd(16);
171
+ const score = sc2(String(h.score).padStart(3) + '/100');
172
+ const grade = h.grade || '-';
173
+ const issues = h.findings > 0 ? paint.yellow(String(h.findings)) : paint.green('0');
174
+ console.log(` ${paint.dim(date)} ${score} ${grade} ${issues}`);
175
+ }
176
+ }
177
+
178
+ console.log('');
179
+ return 0;
180
+ }
package/lib/verify.js ADDED
@@ -0,0 +1,149 @@
1
+ // ClawArmor v1.1.0 — verify command
2
+ // Re-runs only the checks that failed in the LAST audit run.
3
+ // Reads failed check IDs from ~/.clawarmor/history.json.
4
+
5
+ import { loadConfig } from './config.js';
6
+ import { paint, severityColor } from './output/colors.js';
7
+ import { loadHistory } from './audit.js';
8
+ import { probeGatewayLive } from './probes/gateway-probe.js';
9
+ import gatewayChecks from './checks/gateway.js';
10
+ import filesystemChecks from './checks/filesystem.js';
11
+ import channelChecks from './checks/channels.js';
12
+ import authChecks from './checks/auth.js';
13
+ import toolChecks from './checks/tools.js';
14
+ import versionChecks from './checks/version.js';
15
+ import hooksChecks from './checks/hooks.js';
16
+ import allowFromChecks from './checks/allowfrom.js';
17
+
18
+ const SEP = paint.dim('─'.repeat(52));
19
+ const W52 = 52;
20
+
21
+ const PROBE_IDS = new Set([
22
+ 'probe.gateway_running', 'probe.network_exposed', 'probe.ws_auth',
23
+ 'probe.health_leak', 'probe.cors',
24
+ ]);
25
+
26
+ function box(title) {
27
+ const pad = W52 - 2 - title.length;
28
+ const l = Math.floor(pad/2), r = pad - l;
29
+ return [
30
+ paint.dim('╔' + '═'.repeat(W52-2) + '╗'),
31
+ paint.dim('║') + ' '.repeat(l) + paint.bold(title) + ' '.repeat(r) + paint.dim('║'),
32
+ paint.dim('╚' + '═'.repeat(W52-2) + '╝'),
33
+ ].join('\n');
34
+ }
35
+
36
+ function buildCheckMap(staticResults, liveResults) {
37
+ const map = new Map();
38
+ for (const r of [...staticResults, ...liveResults]) {
39
+ if (r.id) map.set(r.id, r);
40
+ }
41
+ return map;
42
+ }
43
+
44
+ export async function runVerify() {
45
+ const history = loadHistory();
46
+
47
+ console.log(''); console.log(box('ClawArmor Verify v1.1.0')); console.log('');
48
+
49
+ if (!history.length) {
50
+ console.log(` ${paint.dim('No audit history found — running full audit.')}`);
51
+ console.log('');
52
+ const { runAudit } = await import('./audit.js');
53
+ return runAudit();
54
+ }
55
+
56
+ const last = history[history.length - 1];
57
+ const failedIds = last.failedIds || [];
58
+
59
+ if (!failedIds.length) {
60
+ console.log(` ${paint.green('✓')} Last audit had no failures — nothing to re-check.`);
61
+ console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor audit')} ${paint.dim('for a fresh full audit.')}`);
62
+ console.log('');
63
+ return 0;
64
+ }
65
+
66
+ console.log(` ${paint.dim('Last audit:')} ${new Date(last.timestamp).toLocaleString('en-US',{dateStyle:'medium',timeStyle:'short'})}`);
67
+ console.log(` ${paint.cyan('Re-checking')} ${paint.bold(String(failedIds.length))} ${paint.dim('previously failed item' + (failedIds.length>1?'s':'') + '...')}`);
68
+ console.log('');
69
+
70
+ const { config, error } = loadConfig();
71
+ if (error) {
72
+ console.log(` ${paint.red('✗')} ${error}`); console.log(''); return 2;
73
+ }
74
+
75
+ // Run all checks
76
+ const allStaticChecks = [
77
+ ...gatewayChecks, ...filesystemChecks, ...channelChecks,
78
+ ...authChecks, ...toolChecks, ...versionChecks, ...hooksChecks,
79
+ ...allowFromChecks,
80
+ ];
81
+
82
+ const staticResults = [];
83
+ for (const check of allStaticChecks) {
84
+ try { staticResults.push(await check(config)); }
85
+ catch (e) { staticResults.push({ id:'err', severity:'LOW', passed:true, passedMsg:`Check error: ${e.message}` }); }
86
+ }
87
+
88
+ let liveResults = [];
89
+ const needsLive = failedIds.some(id => PROBE_IDS.has(id));
90
+ if (needsLive) {
91
+ try { liveResults = await probeGatewayLive(config); }
92
+ catch { liveResults = []; }
93
+ }
94
+
95
+ const checkMap = buildCheckMap(staticResults, liveResults);
96
+
97
+ const nowPassed = [];
98
+ const stillFailed = [];
99
+
100
+ for (const id of failedIds) {
101
+ const result = checkMap.get(id);
102
+ if (!result) {
103
+ // Check not found in this run (may have been removed) — treat as passed
104
+ nowPassed.push({ id, title: id, passedMsg: 'Check no longer applicable' });
105
+ continue;
106
+ }
107
+ if (result.passed) nowPassed.push(result);
108
+ else stillFailed.push(result);
109
+ }
110
+
111
+ console.log(SEP);
112
+ if (nowPassed.length) {
113
+ console.log(` ${paint.green('FIXED')}${paint.dim(' ('+nowPassed.length+')')}`);
114
+ console.log(SEP);
115
+ for (const r of nowPassed) {
116
+ console.log(` ${paint.green('✓')} ${paint.dim(r.passedMsg || r.title || r.id)}`);
117
+ }
118
+ console.log('');
119
+ }
120
+
121
+ if (stillFailed.length) {
122
+ console.log(SEP);
123
+ console.log(` ${paint.red('STILL FAILING')}${paint.dim(' ('+stillFailed.length+')')}`);
124
+ console.log(SEP);
125
+ for (const f of stillFailed) {
126
+ console.log('');
127
+ const sc = severityColor[f.severity] || paint.dim;
128
+ console.log(` ${paint.red('✗')} ${sc('['+f.severity+']')} ${paint.bold(f.title)}`);
129
+ for (const line of (f.description||'').split('\n'))
130
+ console.log(` ${paint.dim(line)}`);
131
+ if (f.fix) {
132
+ const lines = f.fix.split('\n');
133
+ console.log('');
134
+ console.log(` ${paint.cyan('Fix:')} ${lines[0]}`);
135
+ for (let i=1;i<lines.length;i++) console.log(` ${lines[i]}`);
136
+ }
137
+ }
138
+ console.log('');
139
+ console.log(SEP);
140
+ console.log(` ${stillFailed.length} item${stillFailed.length>1?'s':''} still failing.`);
141
+ } else {
142
+ console.log(SEP);
143
+ console.log(` ${paint.green('✓')} ${paint.bold('All previously-failed checks now pass!')}`);
144
+ console.log(` ${paint.dim('Run')} ${paint.cyan('clawarmor audit')} ${paint.dim('for a complete re-scan.')}`);
145
+ }
146
+ console.log('');
147
+
148
+ return stillFailed.length > 0 ? 1 : 0;
149
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "clawarmor",
3
+ "version": "1.1.0",
4
+ "description": "Security armor for OpenClaw agents — audit, scan, monitor",
5
+ "bin": {
6
+ "clawarmor": "./cli.js"
7
+ },
8
+ "type": "module",
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "scripts": {
13
+ "start": "node cli.js"
14
+ },
15
+ "keywords": [
16
+ "openclaw",
17
+ "security",
18
+ "hardening",
19
+ "audit",
20
+ "agent",
21
+ "clawarmor"
22
+ ],
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/pinzasai/clawarmor"
27
+ },
28
+ "homepage": "https://clawarmor.dev"
29
+ }