clawarmor 3.4.0 → 3.5.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 +29 -0
- package/cli.js +7 -2
- package/lib/scan.js +249 -10
- package/package.json +2 -2
- package/sprint-output/v340-done.txt +28 -0
- package/sprint-output/v340-sprint-report.md +130 -0
package/README.md
CHANGED
|
@@ -52,6 +52,7 @@ ClawArmor sits at the foundation and orchestrates the layers above it:
|
|
|
52
52
|
| `audit` | Score your OpenClaw config (0–100), live gateway probes, plain-English verdict |
|
|
53
53
|
| `scan` | Scan all installed skill files for malicious code and SKILL.md instructions |
|
|
54
54
|
| `scan --json` | Machine-readable scan output — pipe to CI, scripts, or dashboards |
|
|
55
|
+
| `scan --report` | Write structured JSON + Markdown reports after scanning (v3.5.0) |
|
|
55
56
|
| `prescan <skill>` | Pre-scan a skill before installing — blocks on CRITICAL findings |
|
|
56
57
|
| `skill verify <name>` | Deep-verify a specific installed skill — checks SKILL.md + all referenced scripts |
|
|
57
58
|
| `fix` | Auto-apply safe fixes (--dry-run to preview, --apply to run) |
|
|
@@ -164,6 +165,34 @@ clawarmor harden --auto --report
|
|
|
164
165
|
|
|
165
166
|
Report structure includes: version, timestamp, OS/OpenClaw info, summary counts (hardened/skipped/already-good), and per-check action details with before/after values.
|
|
166
167
|
|
|
168
|
+
**Scan reports** (v3.5.0) — Export a structured report after scanning skills:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# Write JSON + Markdown reports (e.g. ~/.openclaw/clawarmor-scan-report-2025-03-08.json + .md)
|
|
172
|
+
clawarmor scan --report
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Two files are always written together:
|
|
176
|
+
- `clawarmor-scan-report-YYYY-MM-DD.json` — machine-readable, includes per-skill status, severity, findings, and overall score
|
|
177
|
+
- `clawarmor-scan-report-YYYY-MM-DD.md` — human-readable with executive summary table, findings detail, and remediation steps
|
|
178
|
+
|
|
179
|
+
Example JSON structure:
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"version": "3.5.0",
|
|
183
|
+
"timestamp": "2025-03-08T12:00:00.000Z",
|
|
184
|
+
"system": { "hostname": "myhost", "platform": "darwin", "node_version": "v20.0.0", "openclaw_version": "1.2.0" },
|
|
185
|
+
"verdict": "PASS",
|
|
186
|
+
"score": 100,
|
|
187
|
+
"summary": { "total": 12, "passed": 12, "failed": 0, "warnings": 0, "critical_findings": 0, "high_findings": 0 },
|
|
188
|
+
"checks": [
|
|
189
|
+
{ "name": "weather", "status": "pass", "severity": "NONE", "detail": "No findings", "type": "user" }
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Terminal output is still shown when `--report` is used — the flag only adds file output on top.
|
|
195
|
+
|
|
167
196
|
## Philosophy
|
|
168
197
|
|
|
169
198
|
ClawArmor runs entirely on your machine — no telemetry, no cloud, no accounts.
|
package/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { paint } from './lib/output/colors.js';
|
|
5
5
|
|
|
6
|
-
const VERSION = '3.
|
|
6
|
+
const VERSION = '3.5.0';
|
|
7
7
|
const GATEWAY_PORT_DEFAULT = 18789;
|
|
8
8
|
|
|
9
9
|
function isLocalhost(host) {
|
|
@@ -144,8 +144,13 @@ if (cmd === 'audit') {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
if (cmd === 'scan') {
|
|
147
|
+
const scanReportIdx = args.indexOf('--report');
|
|
148
|
+
const scanFlags = {
|
|
149
|
+
json: flags.json,
|
|
150
|
+
report: scanReportIdx !== -1,
|
|
151
|
+
};
|
|
147
152
|
const { runScan } = await import('./lib/scan.js');
|
|
148
|
-
process.exit(await runScan(
|
|
153
|
+
process.exit(await runScan(scanFlags));
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
if (cmd === 'verify') {
|
package/lib/scan.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { homedir, hostname, platform } from 'os';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
1
5
|
import { paint, severityColor } from './output/colors.js';
|
|
2
6
|
import { scanFile } from './scanner/file-scanner.js';
|
|
3
7
|
import { findInstalledSkills } from './scanner/skill-finder.js';
|
|
@@ -5,7 +9,8 @@ import { scanSkillMdFiles } from './scanner/skill-md-scanner.js';
|
|
|
5
9
|
import { append as auditLogAppend } from './audit-log.js';
|
|
6
10
|
|
|
7
11
|
const SEP = paint.dim('─'.repeat(52));
|
|
8
|
-
const HOME =
|
|
12
|
+
const HOME = homedir();
|
|
13
|
+
const VERSION = '3.5.0';
|
|
9
14
|
|
|
10
15
|
function short(p) { return p.replace(HOME,'~'); }
|
|
11
16
|
|
|
@@ -16,8 +21,224 @@ function box(title) {
|
|
|
16
21
|
paint.dim('╚'+'═'.repeat(W-2)+'╝')].join('\n');
|
|
17
22
|
}
|
|
18
23
|
|
|
24
|
+
// ── Report support ────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
function getSystemInfo() {
|
|
27
|
+
let ocVersion = 'unknown';
|
|
28
|
+
try {
|
|
29
|
+
const r = spawnSync('openclaw', ['--version'], { encoding: 'utf8', timeout: 5000 });
|
|
30
|
+
if (r.stdout) ocVersion = r.stdout.trim().split('\n')[0] || 'unknown';
|
|
31
|
+
} catch { /* non-fatal */ }
|
|
32
|
+
return {
|
|
33
|
+
hostname: hostname(),
|
|
34
|
+
platform: platform(),
|
|
35
|
+
node_version: process.version,
|
|
36
|
+
openclaw_version: ocVersion,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function defaultReportBasePath() {
|
|
41
|
+
const date = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
|
42
|
+
return join(HOME, '.openclaw', `clawarmor-scan-report-${date}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function computeScanScore(findings) {
|
|
46
|
+
let score = 100;
|
|
47
|
+
for (const f of findings) {
|
|
48
|
+
if (f.severity === 'CRITICAL') score -= 25;
|
|
49
|
+
else if (f.severity === 'HIGH') score -= 10;
|
|
50
|
+
else if (f.severity === 'MEDIUM') score -= 3;
|
|
51
|
+
}
|
|
52
|
+
return Math.max(0, score);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildReportChecks(skills, allJsonFindings) {
|
|
56
|
+
// Build per-skill check entries
|
|
57
|
+
const checks = [];
|
|
58
|
+
|
|
59
|
+
// Skills with no findings
|
|
60
|
+
for (const skill of skills) {
|
|
61
|
+
const skillFindings = allJsonFindings.filter(f => f.skill === skill.name);
|
|
62
|
+
if (!skillFindings.length) {
|
|
63
|
+
checks.push({
|
|
64
|
+
name: skill.name,
|
|
65
|
+
status: 'pass',
|
|
66
|
+
severity: 'NONE',
|
|
67
|
+
detail: 'No findings',
|
|
68
|
+
type: skill.isBuiltin ? 'builtin' : 'user',
|
|
69
|
+
});
|
|
70
|
+
} else {
|
|
71
|
+
const maxSev = ['CRITICAL','HIGH','MEDIUM','LOW','INFO'].find(s =>
|
|
72
|
+
skillFindings.some(f => f.severity === s)
|
|
73
|
+
) || 'INFO';
|
|
74
|
+
const status = maxSev === 'CRITICAL' ? 'block' : maxSev === 'HIGH' ? 'warn' : 'info';
|
|
75
|
+
checks.push({
|
|
76
|
+
name: skill.name,
|
|
77
|
+
status,
|
|
78
|
+
severity: maxSev,
|
|
79
|
+
detail: `${skillFindings.length} finding(s): ${skillFindings.map(f => f.patternId).join(', ')}`,
|
|
80
|
+
type: skill.isBuiltin ? 'builtin' : 'user',
|
|
81
|
+
findings: skillFindings.map(f => ({
|
|
82
|
+
patternId: f.patternId,
|
|
83
|
+
severity: f.severity,
|
|
84
|
+
message: f.message,
|
|
85
|
+
file: f.file,
|
|
86
|
+
line: f.line,
|
|
87
|
+
})),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return checks;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function writeJsonReport(reportPath, { skills, allJsonFindings, totalCritical, totalHigh }) {
|
|
95
|
+
const sysInfo = getSystemInfo();
|
|
96
|
+
const checks = buildReportChecks(skills, allJsonFindings);
|
|
97
|
+
const score = computeScanScore(allJsonFindings);
|
|
98
|
+
const passed = checks.filter(c => c.status === 'pass').length;
|
|
99
|
+
const failed = checks.filter(c => c.status === 'block').length;
|
|
100
|
+
const warnings = checks.filter(c => c.status === 'warn').length;
|
|
101
|
+
|
|
102
|
+
let verdict = 'PASS';
|
|
103
|
+
if (totalCritical > 0) verdict = 'BLOCK';
|
|
104
|
+
else if (totalHigh > 0) verdict = 'WARN';
|
|
105
|
+
|
|
106
|
+
const report = {
|
|
107
|
+
version: VERSION,
|
|
108
|
+
timestamp: new Date().toISOString(),
|
|
109
|
+
system: sysInfo,
|
|
110
|
+
verdict,
|
|
111
|
+
score,
|
|
112
|
+
summary: {
|
|
113
|
+
total: checks.length,
|
|
114
|
+
passed,
|
|
115
|
+
failed,
|
|
116
|
+
warnings,
|
|
117
|
+
critical_findings: totalCritical,
|
|
118
|
+
high_findings: totalHigh,
|
|
119
|
+
},
|
|
120
|
+
checks,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
try { mkdirSync(dirname(reportPath), { recursive: true }); } catch {}
|
|
124
|
+
writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf8');
|
|
125
|
+
return report;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function writeMarkdownReport(reportPath, { skills, allJsonFindings, totalCritical, totalHigh }) {
|
|
129
|
+
const sysInfo = getSystemInfo();
|
|
130
|
+
const checks = buildReportChecks(skills, allJsonFindings);
|
|
131
|
+
const score = computeScanScore(allJsonFindings);
|
|
132
|
+
const passed = checks.filter(c => c.status === 'pass').length;
|
|
133
|
+
const failed = checks.filter(c => c.status === 'block').length;
|
|
134
|
+
const warnings = checks.filter(c => c.status === 'warn').length;
|
|
135
|
+
|
|
136
|
+
let verdict = 'PASS';
|
|
137
|
+
if (totalCritical > 0) verdict = 'BLOCK';
|
|
138
|
+
else if (totalHigh > 0) verdict = 'WARN';
|
|
139
|
+
|
|
140
|
+
const now = new Date();
|
|
141
|
+
const dateStr = now.toLocaleString('en-US', {
|
|
142
|
+
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
143
|
+
hour: '2-digit', minute: '2-digit', hour12: false
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
let verdictEmoji = verdict === 'BLOCK' ? '🔴' : verdict === 'WARN' ? '🟡' : '🟢';
|
|
147
|
+
|
|
148
|
+
let md = `# ClawArmor Skill Scan Report
|
|
149
|
+
Generated: ${dateStr}
|
|
150
|
+
ClawArmor: v${VERSION} | Hostname: ${sysInfo.hostname} | Platform: ${sysInfo.platform} | Node: ${sysInfo.node_version} | OpenClaw: ${sysInfo.openclaw_version}
|
|
151
|
+
|
|
152
|
+
## Executive Summary
|
|
153
|
+
|
|
154
|
+
| Metric | Value |
|
|
155
|
+
|--------|-------|
|
|
156
|
+
| Verdict | ${verdictEmoji} **${verdict}** |
|
|
157
|
+
| Score | ${score}/100 |
|
|
158
|
+
| Total Skills | ${skills.length} |
|
|
159
|
+
| Passed (clean) | ✅ ${passed} |
|
|
160
|
+
| Warnings | ⚠️ ${warnings} |
|
|
161
|
+
| Blocked (critical) | ❌ ${failed} |
|
|
162
|
+
| Critical Findings | ${totalCritical} |
|
|
163
|
+
| High Findings | ${totalHigh} |
|
|
164
|
+
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
if (checks.some(c => c.status !== 'pass')) {
|
|
168
|
+
md += `## Skill Check Results
|
|
169
|
+
|
|
170
|
+
| Skill | Type | Status | Severity | Detail |
|
|
171
|
+
|-------|------|--------|----------|--------|
|
|
172
|
+
`;
|
|
173
|
+
for (const check of checks) {
|
|
174
|
+
const statusEmoji = check.status === 'pass' ? '✅ pass' : check.status === 'block' ? '❌ block' : '⚠️ warn';
|
|
175
|
+
md += `| ${check.name} | ${check.type} | ${statusEmoji} | ${check.severity} | ${check.detail} |\n`;
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
md += `## Skill Check Results
|
|
179
|
+
|
|
180
|
+
All ${skills.length} skills are clean. No findings detected.
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Detailed findings for flagged skills
|
|
185
|
+
const flaggedChecks = checks.filter(c => c.status !== 'pass' && c.findings && c.findings.length);
|
|
186
|
+
if (flaggedChecks.length) {
|
|
187
|
+
md += `
|
|
188
|
+
## Findings Detail
|
|
189
|
+
|
|
190
|
+
`;
|
|
191
|
+
for (const check of flaggedChecks) {
|
|
192
|
+
md += `### ${check.name} (${check.type})\n\n`;
|
|
193
|
+
md += `| Pattern ID | Severity | Message | File | Line |\n`;
|
|
194
|
+
md += `|------------|----------|---------|------|------|\n`;
|
|
195
|
+
for (const f of check.findings) {
|
|
196
|
+
md += `| ${f.patternId} | ${f.severity} | ${f.message || '—'} | ${f.file || '—'} | ${f.line ?? '—'} |\n`;
|
|
197
|
+
}
|
|
198
|
+
md += '\n';
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Remediation
|
|
203
|
+
md += `## Remediation Steps
|
|
204
|
+
|
|
205
|
+
`;
|
|
206
|
+
if (totalCritical > 0) {
|
|
207
|
+
md += `### 🔴 Critical — Immediate Action Required
|
|
208
|
+
|
|
209
|
+
- Remove or replace any skill with CRITICAL findings immediately
|
|
210
|
+
- Run \`clawarmor prescan <skill-name>\` before reinstalling
|
|
211
|
+
- Check for data exfiltration attempts: look for external HTTP calls, encoded payloads
|
|
212
|
+
- Review git history of the skill if available
|
|
213
|
+
|
|
214
|
+
`;
|
|
215
|
+
}
|
|
216
|
+
if (totalHigh > 0) {
|
|
217
|
+
md += `### 🟡 High — Review Before Next Session
|
|
218
|
+
|
|
219
|
+
- Audit HIGH-severity skills before running your agent
|
|
220
|
+
- Run \`clawarmor skill verify <name>\` for a deeper inspection
|
|
221
|
+
- Consider disabling suspect skills temporarily: check your OpenClaw skill config
|
|
222
|
+
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
md += `### General
|
|
226
|
+
|
|
227
|
+
- Run \`clawarmor scan\` regularly to catch new findings
|
|
228
|
+
- Use \`clawarmor prescan <skill-name>\` before installing any new skill
|
|
229
|
+
- Keep skills updated — malicious patterns are added to ClawArmor signatures continuously
|
|
230
|
+
- Run \`clawarmor audit\` to check your broader OpenClaw configuration
|
|
231
|
+
`;
|
|
232
|
+
|
|
233
|
+
try { mkdirSync(dirname(reportPath), { recursive: true }); } catch {}
|
|
234
|
+
writeFileSync(reportPath, md, 'utf8');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Main export ───────────────────────────────────────────────────────────────
|
|
238
|
+
|
|
19
239
|
export async function runScan(flags = {}) {
|
|
20
240
|
const jsonMode = flags.json || false;
|
|
241
|
+
const reportMode = flags.report || false;
|
|
21
242
|
|
|
22
243
|
if (!jsonMode) {
|
|
23
244
|
console.log(''); console.log(box('ClawArmor Skill Scan v0.6')); console.log('');
|
|
@@ -33,6 +254,17 @@ export async function runScan(flags = {}) {
|
|
|
33
254
|
} else {
|
|
34
255
|
console.log(` ${paint.dim('No installed skills found.')}`); console.log('');
|
|
35
256
|
}
|
|
257
|
+
|
|
258
|
+
if (reportMode) {
|
|
259
|
+
const basePath = defaultReportBasePath();
|
|
260
|
+
const jsonPath = basePath + '.json';
|
|
261
|
+
const mdPath = basePath + '.md';
|
|
262
|
+
writeJsonReport(jsonPath, { skills: [], allJsonFindings: [], totalCritical: 0, totalHigh: 0 });
|
|
263
|
+
writeMarkdownReport(mdPath, { skills: [], allJsonFindings: [], totalCritical: 0, totalHigh: 0 });
|
|
264
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
265
|
+
console.log(`\n ${paint.dim('Report saved:')} clawarmor-scan-report-${date}.json + .md`);
|
|
266
|
+
}
|
|
267
|
+
|
|
36
268
|
return 0;
|
|
37
269
|
}
|
|
38
270
|
|
|
@@ -46,7 +278,7 @@ export async function runScan(flags = {}) {
|
|
|
46
278
|
let totalCritical = 0, totalHigh = 0;
|
|
47
279
|
const flagged = [];
|
|
48
280
|
const auditFindings = []; // accumulated for audit log
|
|
49
|
-
const jsonFindings = []; // for --json output
|
|
281
|
+
const jsonFindings = []; // for --json output and --report
|
|
50
282
|
|
|
51
283
|
for (const skill of skills) {
|
|
52
284
|
if (!jsonMode) process.stdout.write(` ${skill.isBuiltin ? paint.dim('⊙') : paint.cyan('▶')} ${paint.bold(skill.name)}${paint.dim(skill.isBuiltin?' [built-in]':' [user]')}...`);
|
|
@@ -68,7 +300,7 @@ export async function runScan(flags = {}) {
|
|
|
68
300
|
|
|
69
301
|
totalCritical += critical.length; totalHigh += high.length;
|
|
70
302
|
|
|
71
|
-
// Collect findings for JSON output
|
|
303
|
+
// Collect findings for JSON output and report
|
|
72
304
|
for (const f of allFindings) {
|
|
73
305
|
if (['CRITICAL','HIGH','MEDIUM','LOW'].includes(f.severity)) {
|
|
74
306
|
for (const m of (f.matches || [])) {
|
|
@@ -113,13 +345,7 @@ export async function runScan(flags = {}) {
|
|
|
113
345
|
// JSON output mode
|
|
114
346
|
if (jsonMode) {
|
|
115
347
|
// Compute scan score: start 100, -25 per CRITICAL, -10 per HIGH, -3 per MEDIUM
|
|
116
|
-
|
|
117
|
-
for (const f of jsonFindings) {
|
|
118
|
-
if (f.severity === 'CRITICAL') scanScore -= 25;
|
|
119
|
-
else if (f.severity === 'HIGH') scanScore -= 10;
|
|
120
|
-
else if (f.severity === 'MEDIUM') scanScore -= 3;
|
|
121
|
-
}
|
|
122
|
-
scanScore = Math.max(0, scanScore);
|
|
348
|
+
const scanScore = computeScanScore(jsonFindings);
|
|
123
349
|
|
|
124
350
|
let verdict = 'PASS';
|
|
125
351
|
if (totalCritical > 0) verdict = 'BLOCK';
|
|
@@ -210,5 +436,18 @@ export async function runScan(flags = {}) {
|
|
|
210
436
|
skill: null,
|
|
211
437
|
});
|
|
212
438
|
|
|
439
|
+
// ── Write report if requested ──────────────────────────────────────────────
|
|
440
|
+
if (reportMode) {
|
|
441
|
+
const basePath = defaultReportBasePath();
|
|
442
|
+
const jsonPath = basePath + '.json';
|
|
443
|
+
const mdPath = basePath + '.md';
|
|
444
|
+
const reportData = { skills, allJsonFindings: jsonFindings, totalCritical, totalHigh };
|
|
445
|
+
writeJsonReport(jsonPath, reportData);
|
|
446
|
+
writeMarkdownReport(mdPath, reportData);
|
|
447
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
448
|
+
console.log(` ${paint.dim('Report saved:')} clawarmor-scan-report-${date}.json + .md`);
|
|
449
|
+
console.log('');
|
|
450
|
+
}
|
|
451
|
+
|
|
213
452
|
return totalCritical > 0 ? 1 : 0;
|
|
214
453
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawarmor",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "Security armor for OpenClaw agents
|
|
3
|
+
"version": "3.5.0",
|
|
4
|
+
"description": "Security armor for OpenClaw agents \u2014 audit, scan, monitor",
|
|
5
5
|
"bin": {
|
|
6
6
|
"clawarmor": "cli.js"
|
|
7
7
|
},
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
## What Was Shipped
|
|
2
|
+
|
|
3
|
+
`clawarmor harden --report` — structured hardening report command
|
|
4
|
+
|
|
5
|
+
### Changes:
|
|
6
|
+
- `lib/harden.js`: Added full report support — JSON + Markdown writers, per-check tracking,
|
|
7
|
+
system info capture, report summary printer. Existing behavior unchanged when --report not passed.
|
|
8
|
+
- `cli.js`: Added --report, --report-format flag parsing for harden command
|
|
9
|
+
- `package.json`: Version bumped 3.3.0 → 3.4.0
|
|
10
|
+
- `README.md`: Added "Hardening reports (v3.4.0)" section with all usage examples
|
|
11
|
+
- `CHANGELOG.md`: Added [3.4.0] entry
|
|
12
|
+
|
|
13
|
+
### Commands now supported:
|
|
14
|
+
clawarmor harden --report
|
|
15
|
+
clawarmor harden --report /path/to/report.json
|
|
16
|
+
clawarmor harden --report /path/to/report.md --report-format text
|
|
17
|
+
clawarmor harden --auto --report
|
|
18
|
+
|
|
19
|
+
## npm Publish Status
|
|
20
|
+
|
|
21
|
+
✅ SUCCESS — clawarmor@3.4.0 published to npm (tag: latest)
|
|
22
|
+
|
|
23
|
+
## Issues Encountered
|
|
24
|
+
|
|
25
|
+
None. All three test cases passed:
|
|
26
|
+
1. `clawarmor harden --report` → wrote valid JSON to default path
|
|
27
|
+
2. `clawarmor harden --report /tmp/test-report.md --report-format text` → wrote clean Markdown
|
|
28
|
+
3. `clawarmor harden --auto --report /tmp/test-report.json` → wrote valid JSON
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# ClawArmor v3.4.0 Sprint Report
|
|
2
|
+
**Sprint:** `harden --report` feature
|
|
3
|
+
**Date:** 2026-03-08
|
|
4
|
+
**Status:** ✅ SHIPPED
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Summary
|
|
9
|
+
|
|
10
|
+
Shipped `clawarmor harden --report` — a portable hardening report command that exports a structured summary of every hardening run.
|
|
11
|
+
|
|
12
|
+
## Done Criteria
|
|
13
|
+
|
|
14
|
+
- [x] `clawarmor harden --report` runs and writes JSON report to `~/.openclaw/clawarmor-harden-report-YYYY-MM-DD.json`
|
|
15
|
+
- [x] `clawarmor harden --report /tmp/test.md --report-format text` writes Markdown report
|
|
16
|
+
- [x] `npm publish` succeeded — v3.4.0 live on npm
|
|
17
|
+
- [x] README updated with new `--report` flag and example output
|
|
18
|
+
- [x] Sprint report written to `~/clawarmor/sprint-output/v340-sprint-report.md`
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Changes Shipped
|
|
23
|
+
|
|
24
|
+
### `lib/harden.js`
|
|
25
|
+
- Added `--report` flag support via `flags.report`, `flags.reportPath`, `flags.reportFormat`
|
|
26
|
+
- Added `getSystemInfo()` — captures OS and OpenClaw version for report metadata
|
|
27
|
+
- Added `defaultReportPath(format)` — auto-generates path `~/.openclaw/clawarmor-harden-report-YYYY-MM-DD.{json|md}`
|
|
28
|
+
- Added `buildReportItems()` — assembles per-check items from applied/skipped/failed tracking
|
|
29
|
+
- Added `writeJsonReport()` — writes structured JSON with version, timestamp, system, summary, items
|
|
30
|
+
- Added `writeMarkdownReport()` — writes human-readable Markdown table with summary, actions, skipped, failed
|
|
31
|
+
- Added `printReportSummary()` — prints inline summary to stdout after writing the report
|
|
32
|
+
- Modified main loop to track `appliedIds`, `skippedIds`, `failedIds`, `applyResults` per fix
|
|
33
|
+
- Added `_reportBefore` and `_reportAfter` fields to each fix for before/after capture
|
|
34
|
+
- Existing behavior **fully preserved** when `--report` not passed
|
|
35
|
+
|
|
36
|
+
### `cli.js`
|
|
37
|
+
- Added `--report`, `--report-format`, `--report-path` flag parsing for `harden` command
|
|
38
|
+
- `--report` can be bare flag or `--report <path>` (path detected if next arg doesn't start with `--`)
|
|
39
|
+
- Version constant bumped: `3.3.0` → `3.4.0`
|
|
40
|
+
|
|
41
|
+
### `package.json`
|
|
42
|
+
- Version bumped to `3.4.0`
|
|
43
|
+
|
|
44
|
+
### `README.md`
|
|
45
|
+
- Updated harden command row to include `--report`
|
|
46
|
+
- Added new "Hardening reports (v3.4.0)" section with all usage examples
|
|
47
|
+
|
|
48
|
+
### `CHANGELOG.md`
|
|
49
|
+
- Added `[3.4.0]` entry with full feature description and examples
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Report Format: JSON
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"version": "3.4.0",
|
|
58
|
+
"timestamp": "2026-03-08T07:11:00.023Z",
|
|
59
|
+
"system": {
|
|
60
|
+
"os": "darwin 24.3.0",
|
|
61
|
+
"openclaw_version": "2026.2.26"
|
|
62
|
+
},
|
|
63
|
+
"summary": {
|
|
64
|
+
"total_checks": 3,
|
|
65
|
+
"hardened": 1,
|
|
66
|
+
"already_good": 0,
|
|
67
|
+
"skipped": 2,
|
|
68
|
+
"failed": 0
|
|
69
|
+
},
|
|
70
|
+
"items": [
|
|
71
|
+
{
|
|
72
|
+
"check": "exec.ask.off",
|
|
73
|
+
"status": "hardened",
|
|
74
|
+
"before": "off",
|
|
75
|
+
"after": "on-miss",
|
|
76
|
+
"action": "Enable exec approval for unrecognized commands"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"check": "gateway.host.open",
|
|
80
|
+
"status": "skipped",
|
|
81
|
+
"skipped_reason": "Breaking fix — skipped in auto mode (use --auto --force to include)"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Report Format: Markdown
|
|
88
|
+
|
|
89
|
+
```markdown
|
|
90
|
+
# ClawArmor Hardening Report
|
|
91
|
+
Generated: 03/07/2026, 23:07
|
|
92
|
+
ClawArmor: v3.4.0 | OS: darwin 24.3.0 | OpenClaw: 2026.2.26
|
|
93
|
+
|
|
94
|
+
## Summary
|
|
95
|
+
- ✅ 0 checks already good
|
|
96
|
+
- 🔧 1 hardened
|
|
97
|
+
- ⚠️ 2 skipped
|
|
98
|
+
|
|
99
|
+
## Actions Taken
|
|
100
|
+
|
|
101
|
+
| Check | Before | After | Action |
|
|
102
|
+
|-------|--------|-------|--------|
|
|
103
|
+
| exec.ask.off | off | on-miss | Enable exec approval... |
|
|
104
|
+
|
|
105
|
+
## Skipped
|
|
106
|
+
|
|
107
|
+
- **gateway.host.open**: Breaking fix — skipped in auto mode
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## npm Publish
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
npm notice name: clawarmor
|
|
116
|
+
npm notice version: 3.4.0
|
|
117
|
+
npm notice total files: 67
|
|
118
|
+
+ clawarmor@3.4.0
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Published with tag: `latest`
|
|
122
|
+
Registry: https://registry.npmjs.org/
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Git
|
|
127
|
+
|
|
128
|
+
- Commit: `d856232`
|
|
129
|
+
- Branch: `main`
|
|
130
|
+
- Pushed: `origin/main`
|