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/LICENSE +21 -0
- package/README.md +163 -0
- package/SECURITY.md +40 -0
- package/SKILL.md +87 -0
- package/cli.js +155 -0
- package/lib/audit.js +257 -0
- package/lib/checks/allowfrom.js +104 -0
- package/lib/checks/auth.js +63 -0
- package/lib/checks/channels.js +69 -0
- package/lib/checks/filesystem.js +87 -0
- package/lib/checks/gateway.js +177 -0
- package/lib/checks/hooks.js +33 -0
- package/lib/checks/tools.js +75 -0
- package/lib/checks/version.js +61 -0
- package/lib/compare.js +96 -0
- package/lib/config.js +48 -0
- package/lib/discovery.js +92 -0
- package/lib/fix.js +175 -0
- package/lib/output/colors.js +24 -0
- package/lib/output/formatter.js +212 -0
- package/lib/output/progress.js +27 -0
- package/lib/probes/gateway-probe.js +260 -0
- package/lib/scan.js +129 -0
- package/lib/scanner/file-scanner.js +69 -0
- package/lib/scanner/patterns.js +74 -0
- package/lib/scanner/skill-finder.js +58 -0
- package/lib/scanner/skill-md-scanner.js +121 -0
- package/lib/trend.js +180 -0
- package/lib/verify.js +149 -0
- package/package.json +29 -0
|
@@ -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
|
+
}
|