clawarmor 3.2.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/CHANGELOG.md +89 -0
- package/README.md +78 -1
- package/clawgear-skills/skill-security-scanner/SKILL.md +1 -1
- package/cli.js +43 -2
- package/lib/harden.js +225 -11
- package/lib/invariant-sync.js +668 -0
- 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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,94 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.4.0] — 2026-03-08
|
|
4
|
+
|
|
5
|
+
### New Features
|
|
6
|
+
|
|
7
|
+
#### `clawarmor harden --report` — Structured Hardening Reports
|
|
8
|
+
Export a portable, structured summary of every hardening run — what was hardened, what was
|
|
9
|
+
skipped, why, and what was already good. The #1 feature gap for enterprise adoption.
|
|
10
|
+
|
|
11
|
+
**Flags:**
|
|
12
|
+
- `--report [path]` — Write JSON report (default: `~/.openclaw/clawarmor-harden-report-YYYY-MM-DD.json`)
|
|
13
|
+
- `--report-format text` — Write Markdown report instead of JSON
|
|
14
|
+
|
|
15
|
+
**JSON report structure:**
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"version": "3.4.0",
|
|
19
|
+
"timestamp": "...",
|
|
20
|
+
"system": { "os": "...", "openclaw_version": "..." },
|
|
21
|
+
"summary": { "total_checks": N, "hardened": N, "already_good": N, "skipped": N },
|
|
22
|
+
"items": [
|
|
23
|
+
{ "check": "exec.ask.off", "status": "hardened", "before": "off", "after": "on-miss", "action": "..." },
|
|
24
|
+
{ "check": "gateway.host.open", "status": "skipped", "skipped_reason": "Breaking fix..." }
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Examples:**
|
|
30
|
+
```bash
|
|
31
|
+
clawarmor harden --report
|
|
32
|
+
clawarmor harden --report /tmp/my-report.json
|
|
33
|
+
clawarmor harden --report /tmp/report.md --report-format text
|
|
34
|
+
clawarmor harden --auto --report
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Existing `clawarmor harden` behavior unchanged when `--report` is not passed.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## [3.3.0] — 2026-03-07
|
|
42
|
+
|
|
43
|
+
### New Features
|
|
44
|
+
|
|
45
|
+
#### `clawarmor invariant sync` — Invariant Deep Integration
|
|
46
|
+
The Invariant integration in v3.0 detected presence of `invariant-ai`. v3.3.0 does the real work:
|
|
47
|
+
it reads your latest audit findings and generates severity-tiered Invariant DSL policies that
|
|
48
|
+
actually enforce behavioral guardrails at runtime.
|
|
49
|
+
|
|
50
|
+
**Severity tiers:**
|
|
51
|
+
- `CRITICAL`/`HIGH` findings → `raise "..."` hard enforcement rules (blocks the trace)
|
|
52
|
+
- `MEDIUM` findings → `warn "..."` monitoring/alerting rules (logs but allows)
|
|
53
|
+
- `LOW`/`INFO` findings → `# informational` comments (guidance only)
|
|
54
|
+
|
|
55
|
+
**Policy mappings (finding → Invariant rule):**
|
|
56
|
+
| Finding type | Generated policy |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `exec.ask=off` / unrestricted exec | `raise` on any `exec` tool call |
|
|
59
|
+
| Credential files world-readable | `raise` on `read_file` to sensitive paths (`.ssh`, `.aws`, `agent-accounts`, `.openclaw`) |
|
|
60
|
+
| Open channel policy (no `allowFrom`) | `raise`/`warn` on `read_file → send_message` without channel restriction |
|
|
61
|
+
| Elevated tool calls unrestricted | `raise`/`warn` on elevated calls with no `allowFrom_restricted` metadata |
|
|
62
|
+
| Skill supply chain / unpinned | `raise`/`warn` on tool calls lacking `skill_verified` or `skill_pinned` metadata |
|
|
63
|
+
| API key/secret in config files | `raise`/`warn` on `read_file` output containing secret patterns → `send_message` |
|
|
64
|
+
| Baseline: prompt injection | `raise` on web content → outbound message (always included) |
|
|
65
|
+
|
|
66
|
+
**New commands:**
|
|
67
|
+
```
|
|
68
|
+
clawarmor invariant sync # generate tiered policies from latest audit
|
|
69
|
+
clawarmor invariant sync --dry-run # preview without writing
|
|
70
|
+
clawarmor invariant sync --push # generate + validate + push to Invariant instance
|
|
71
|
+
clawarmor invariant sync --push --host <host> --port <port>
|
|
72
|
+
clawarmor invariant sync --json # machine-readable output
|
|
73
|
+
clawarmor invariant status # show current policy file + last sync report
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Policy output:**
|
|
77
|
+
- Policy file: `~/.clawarmor/invariant-policies/clawarmor.inv`
|
|
78
|
+
- Sync report: `~/.clawarmor/invariant-policies/sync-report.json`
|
|
79
|
+
|
|
80
|
+
**`--push` behavior:**
|
|
81
|
+
1. Validates policy syntax via `LocalPolicy.from_file()` (requires `pip3 install invariant-ai`)
|
|
82
|
+
2. If Invariant instance running on `localhost:8000` → live-reloads policy immediately
|
|
83
|
+
3. If not running → policy written to disk, enforces on next Invariant start
|
|
84
|
+
|
|
85
|
+
**Relationship to `clawarmor stack`:**
|
|
86
|
+
- `stack deploy/sync` generates basic `.inv` rules in `~/.clawarmor/invariant-rules.inv`
|
|
87
|
+
- `invariant sync` generates richer severity-tiered policies in `~/.clawarmor/invariant-policies/clawarmor.inv`
|
|
88
|
+
- They are complementary; `invariant sync` is the recommended path for serious deployments
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
3
92
|
## [3.2.0] — 2026-03-03
|
|
4
93
|
|
|
5
94
|
### New Features
|
package/README.md
CHANGED
|
@@ -51,9 +51,12 @@ ClawArmor sits at the foundation and orchestrates the layers above it:
|
|
|
51
51
|
|---|---|
|
|
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
|
+
| `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) |
|
|
54
56
|
| `prescan <skill>` | Pre-scan a skill before installing — blocks on CRITICAL findings |
|
|
57
|
+
| `skill verify <name>` | Deep-verify a specific installed skill — checks SKILL.md + all referenced scripts |
|
|
55
58
|
| `fix` | Auto-apply safe fixes (--dry-run to preview, --apply to run) |
|
|
56
|
-
| `harden` | Interactive hardening wizard (--dry-run, --auto, --monitor) |
|
|
59
|
+
| `harden` | Interactive hardening wizard (--dry-run, --auto, --monitor, --report) |
|
|
57
60
|
| `status` | One-screen security posture dashboard |
|
|
58
61
|
| `verify` | Re-run only previously-failed checks (CI-friendly, exit 0 = all fixed) |
|
|
59
62
|
|
|
@@ -67,6 +70,31 @@ ClawArmor sits at the foundation and orchestrates the layers above it:
|
|
|
67
70
|
| `stack sync` | Regenerate stack configs from latest audit — run after harden/fix |
|
|
68
71
|
| `stack teardown` | Remove deployed stack components |
|
|
69
72
|
|
|
73
|
+
### Invariant Deep Integration (v3.3.0)
|
|
74
|
+
|
|
75
|
+
| Command | Description |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `invariant sync` | Generate severity-tiered Invariant policies from latest audit findings |
|
|
78
|
+
| `invariant sync --dry-run` | Preview policies without writing |
|
|
79
|
+
| `invariant sync --push` | Generate + validate + push to running Invariant instance |
|
|
80
|
+
| `invariant sync --json` | Machine-readable output for scripting |
|
|
81
|
+
| `invariant status` | Show current policy file and last sync report |
|
|
82
|
+
|
|
83
|
+
**Severity tiers:**
|
|
84
|
+
- `CRITICAL`/`HIGH` findings → `raise "..."` (hard enforcement — blocks trace)
|
|
85
|
+
- `MEDIUM` findings → `warn "..."` (monitoring/alerting — logged)
|
|
86
|
+
- `LOW`/`INFO` findings → `# comment` (informational only)
|
|
87
|
+
|
|
88
|
+
Policies are written to `~/.clawarmor/invariant-policies/clawarmor.inv`. With `--push`, ClawArmor validates the policy syntax via `invariant-ai` and live-reloads a running Invariant instance. If no instance is running, the policy is written to disk and enforces on next start.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pip3 install invariant-ai # required for --push validation
|
|
92
|
+
clawarmor audit # run audit to capture findings
|
|
93
|
+
clawarmor invariant sync # generate tiered policies
|
|
94
|
+
clawarmor invariant sync --push # push to running Invariant instance
|
|
95
|
+
clawarmor invariant status # check what's deployed
|
|
96
|
+
```
|
|
97
|
+
|
|
70
98
|
### History & Monitoring
|
|
71
99
|
|
|
72
100
|
| Command | Description |
|
|
@@ -76,6 +104,9 @@ ClawArmor sits at the foundation and orchestrates the layers above it:
|
|
|
76
104
|
| `log` | View the audit event log |
|
|
77
105
|
| `digest` | Show weekly security digest |
|
|
78
106
|
| `watch` | Monitor config and skill changes in real time |
|
|
107
|
+
| `baseline save` | Save current scan results as baseline |
|
|
108
|
+
| `baseline diff` | Compare current scan against saved baseline — see what changed |
|
|
109
|
+
| `incident create` | Log a security incident with timestamp, findings, and remediation notes |
|
|
79
110
|
| `protect --install` | Install guard hook, shell intercept (zsh/bash/fish), and watch daemon |
|
|
80
111
|
| `snapshot` | Save a config snapshot manually (auto-saved before every harden/fix) |
|
|
81
112
|
| `rollback` | Restore config from auto-snapshot (--list, --id <id>) |
|
|
@@ -116,6 +147,52 @@ clawarmor harden --monitor-report # see what it observed
|
|
|
116
147
|
clawarmor harden --monitor-off # stop monitoring
|
|
117
148
|
```
|
|
118
149
|
|
|
150
|
+
**Hardening reports** (v3.4.0) — Export a structured report after hardening:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# Write JSON report to default location (~/.openclaw/clawarmor-harden-report-YYYY-MM-DD.json)
|
|
154
|
+
clawarmor harden --report
|
|
155
|
+
|
|
156
|
+
# Write JSON report to a custom path
|
|
157
|
+
clawarmor harden --report /path/to/report.json
|
|
158
|
+
|
|
159
|
+
# Write Markdown report (human-readable, shareable)
|
|
160
|
+
clawarmor harden --report /path/to/report.md --report-format text
|
|
161
|
+
|
|
162
|
+
# Combine with auto mode
|
|
163
|
+
clawarmor harden --auto --report
|
|
164
|
+
```
|
|
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.
|
|
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
|
+
|
|
119
196
|
## Philosophy
|
|
120
197
|
|
|
121
198
|
ClawArmor runs entirely on your machine — no telemetry, no cloud, no accounts.
|
|
@@ -12,7 +12,7 @@ A security gate that sits between you and any skill you're about to install. Bef
|
|
|
12
12
|
|
|
13
13
|
## The problem it solves
|
|
14
14
|
|
|
15
|
-
In early 2026, a malicious skill called `openclaw-web-search` was distributed on a third-party registry. It appeared functional but contained an obfuscated payload that
|
|
15
|
+
In early 2026, a malicious skill called `openclaw-web-search` was distributed on a third-party registry. It appeared functional but contained an obfuscated payload that sent session tokens to an attacker-controlled DNS server. It was installed by dozens of operators without inspection.
|
|
16
16
|
|
|
17
17
|
This scanner would have caught it. The obfuscation patterns and DNS module import are both CRITICAL-severity matches in ClawArmor's pattern library.
|
|
18
18
|
|
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) {
|
|
@@ -54,6 +54,7 @@ function usage() {
|
|
|
54
54
|
console.log(` ${paint.cyan('baseline')} Save, list, and diff security baselines (save|list|diff)`);
|
|
55
55
|
console.log(` ${paint.cyan('incident')} Log and manage security incidents (create|list)`);
|
|
56
56
|
console.log(` ${paint.cyan('stack')} Security orchestrator — deploy Invariant + IronCurtain from audit data`);
|
|
57
|
+
console.log(` ${paint.cyan('invariant')} Invariant deep integration — sync findings → runtime policies (v3.3.0)`);
|
|
57
58
|
console.log(` ${paint.cyan('skill')} Skill utilities — verify a skill directory (skill verify <dir>)`);
|
|
58
59
|
console.log(` ${paint.cyan('skill-report')} Show post-install audit impact of last skill install`);
|
|
59
60
|
console.log(` ${paint.cyan('profile')} Manage contextual hardening profiles`);
|
|
@@ -143,8 +144,13 @@ if (cmd === 'audit') {
|
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
if (cmd === 'scan') {
|
|
147
|
+
const scanReportIdx = args.indexOf('--report');
|
|
148
|
+
const scanFlags = {
|
|
149
|
+
json: flags.json,
|
|
150
|
+
report: scanReportIdx !== -1,
|
|
151
|
+
};
|
|
146
152
|
const { runScan } = await import('./lib/scan.js');
|
|
147
|
-
process.exit(await runScan(
|
|
153
|
+
process.exit(await runScan(scanFlags));
|
|
148
154
|
}
|
|
149
155
|
|
|
150
156
|
if (cmd === 'verify') {
|
|
@@ -211,6 +217,12 @@ if (cmd === 'log') {
|
|
|
211
217
|
|
|
212
218
|
if (cmd === 'harden') {
|
|
213
219
|
const hardenProfileIdx = args.indexOf('--profile');
|
|
220
|
+
const reportIdx = args.indexOf('--report');
|
|
221
|
+
const reportFormatIdx = args.indexOf('--report-format');
|
|
222
|
+
// --report can be a flag alone or --report <path>
|
|
223
|
+
const reportNext = reportIdx !== -1 ? args[reportIdx + 1] : null;
|
|
224
|
+
const reportPath = (reportNext && !reportNext.startsWith('--')) ? reportNext : null;
|
|
225
|
+
const reportFormat = reportFormatIdx !== -1 ? (args[reportFormatIdx + 1] || 'json') : 'json';
|
|
214
226
|
const hardenFlags = {
|
|
215
227
|
dryRun: args.includes('--dry-run'),
|
|
216
228
|
auto: args.includes('--auto'),
|
|
@@ -219,6 +231,9 @@ if (cmd === 'harden') {
|
|
|
219
231
|
monitorReport: args.includes('--monitor-report'),
|
|
220
232
|
monitorOff: args.includes('--monitor-off'),
|
|
221
233
|
profile: hardenProfileIdx !== -1 ? args[hardenProfileIdx + 1] : null,
|
|
234
|
+
report: reportIdx !== -1,
|
|
235
|
+
reportPath: reportPath,
|
|
236
|
+
reportFormat: reportFormat,
|
|
222
237
|
};
|
|
223
238
|
const { runHarden } = await import('./lib/harden.js');
|
|
224
239
|
process.exit(await runHarden(hardenFlags));
|
|
@@ -285,5 +300,31 @@ if (cmd === 'skill') {
|
|
|
285
300
|
process.exit(1);
|
|
286
301
|
}
|
|
287
302
|
|
|
303
|
+
|
|
304
|
+
if (cmd === 'invariant') {
|
|
305
|
+
const sub = args[1];
|
|
306
|
+
const invArgs = args.slice(2);
|
|
307
|
+
|
|
308
|
+
if (!sub || sub === 'status') {
|
|
309
|
+
const { runInvariantStatus } = await import('./lib/invariant-sync.js');
|
|
310
|
+
process.exit(await runInvariantStatus());
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (sub === 'sync') {
|
|
314
|
+
const { runInvariantSync } = await import('./lib/invariant-sync.js');
|
|
315
|
+
process.exit(await runInvariantSync(invArgs));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
console.log('');
|
|
319
|
+
console.log(` Invariant subcommands:`);
|
|
320
|
+
console.log(` clawarmor invariant status # show policy + last sync`);
|
|
321
|
+
console.log(` clawarmor invariant sync # generate policies from latest audit`);
|
|
322
|
+
console.log(` clawarmor invariant sync --dry-run # preview without writing`);
|
|
323
|
+
console.log(` clawarmor invariant sync --push # generate + push to Invariant instance`);
|
|
324
|
+
console.log(` clawarmor invariant sync --push --host <host> --port <port>`);
|
|
325
|
+
console.log('');
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
|
|
288
329
|
console.log(` ${paint.red('✗')} Unknown command: ${paint.bold(cmd)}`);
|
|
289
330
|
usage(); process.exit(1);
|
package/lib/harden.js
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
// --dry-run: show what WOULD be fixed, no writes
|
|
5
5
|
// --auto: apply all safe + caution fixes without confirmation (skips breaking)
|
|
6
6
|
// --auto --force: apply ALL fixes including breaking ones
|
|
7
|
+
// --report [path]: write a structured report (JSON or Markdown) after hardening
|
|
7
8
|
|
|
8
|
-
import { existsSync, readdirSync, statSync, readFileSync } from 'fs';
|
|
9
|
-
import { join } from 'path';
|
|
10
|
-
import { homedir } from 'os';
|
|
9
|
+
import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
10
|
+
import { join, dirname } from 'path';
|
|
11
|
+
import { homedir, platform, release } from 'os';
|
|
11
12
|
import { execSync, spawnSync } from 'child_process';
|
|
12
13
|
import { createInterface } from 'readline';
|
|
13
14
|
import { paint } from './output/colors.js';
|
|
@@ -24,6 +25,8 @@ const HISTORY_FILE = join(CLAWARMOR_DIR, 'history.json');
|
|
|
24
25
|
const CLI_PATH = new URL('../cli.js', import.meta.url).pathname;
|
|
25
26
|
const SEP = paint.dim('─'.repeat(52));
|
|
26
27
|
|
|
28
|
+
const VERSION = '3.4.0';
|
|
29
|
+
|
|
27
30
|
// ── Impact levels ─────────────────────────────────────────────────────────────
|
|
28
31
|
// SAFE: No functionality impact. Pure security improvement.
|
|
29
32
|
// CAUTION: May change agent behavior. User should be aware.
|
|
@@ -83,8 +86,6 @@ function buildFixes(config) {
|
|
|
83
86
|
// Fix 1: world/group-readable credential files — chmod 600
|
|
84
87
|
const badFiles = findWorldReadableCredFiles();
|
|
85
88
|
for (const f of badFiles) {
|
|
86
|
-
// Classify: credential files (tokens, keys) are safe to lock down.
|
|
87
|
-
// Config files that other tools might read need caution.
|
|
88
89
|
const isSensitive = /\.(env|json|key|pem|token|secret)$/i.test(f.name) ||
|
|
89
90
|
f.name === 'agent-accounts.json' ||
|
|
90
91
|
f.name === 'openclaw.json';
|
|
@@ -101,6 +102,9 @@ function buildFixes(config) {
|
|
|
101
102
|
? 'Only restricts other system users. Scripts will still run as you.'
|
|
102
103
|
: 'Only restricts other system users from reading this file. Your agent is unaffected.',
|
|
103
104
|
manualNote: null,
|
|
105
|
+
// for report: capture before state
|
|
106
|
+
_reportBefore: f.mode,
|
|
107
|
+
_reportAfter: '600',
|
|
104
108
|
});
|
|
105
109
|
}
|
|
106
110
|
|
|
@@ -119,6 +123,8 @@ function buildFixes(config) {
|
|
|
119
123
|
' tablet, or another computer), those connections will be blocked.\n' +
|
|
120
124
|
' Only localhost access will work after this change.',
|
|
121
125
|
manualNote: 'Restart gateway after applying: openclaw gateway restart',
|
|
126
|
+
_reportBefore: '0.0.0.0',
|
|
127
|
+
_reportAfter: '127.0.0.1',
|
|
122
128
|
});
|
|
123
129
|
}
|
|
124
130
|
|
|
@@ -137,6 +143,8 @@ function buildFixes(config) {
|
|
|
137
143
|
' shell commands may pause waiting for approval.\n' +
|
|
138
144
|
' You\'ll need to approve commands via the web UI or CLI.',
|
|
139
145
|
manualNote: 'Restart gateway after applying: openclaw gateway restart',
|
|
146
|
+
_reportBefore: String(execAsk),
|
|
147
|
+
_reportAfter: 'on-miss',
|
|
140
148
|
});
|
|
141
149
|
}
|
|
142
150
|
|
|
@@ -236,6 +244,171 @@ function printImpactSummary(fixes) {
|
|
|
236
244
|
return parts.join(paint.dim(' · '));
|
|
237
245
|
}
|
|
238
246
|
|
|
247
|
+
// ── Report support ────────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
function getSystemInfo() {
|
|
250
|
+
let osInfo = `${platform()} ${release()}`;
|
|
251
|
+
let ocVersion = 'unknown';
|
|
252
|
+
try {
|
|
253
|
+
const r = spawnSync('openclaw', ['--version'], { encoding: 'utf8', timeout: 5000 });
|
|
254
|
+
if (r.stdout) ocVersion = r.stdout.trim().split('\n')[0] || 'unknown';
|
|
255
|
+
} catch { /* non-fatal */ }
|
|
256
|
+
return { os: osInfo, openclaw_version: ocVersion };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function defaultReportPath(format) {
|
|
260
|
+
const date = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
|
261
|
+
const ext = format === 'text' ? 'md' : 'json';
|
|
262
|
+
return join(HOME, '.openclaw', `clawarmor-harden-report-${date}.${ext}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function buildReportItems({ fixes, applied, skipped, failed, skippedBreaking, applyResults }) {
|
|
266
|
+
const items = [];
|
|
267
|
+
const appliedSet = new Set(applied);
|
|
268
|
+
const skippedSet = new Set(skipped);
|
|
269
|
+
const failedSet = new Set(failed);
|
|
270
|
+
|
|
271
|
+
for (const fix of fixes) {
|
|
272
|
+
if (failedSet.has(fix.id)) {
|
|
273
|
+
const res = applyResults[fix.id];
|
|
274
|
+
items.push({
|
|
275
|
+
check: fix.id,
|
|
276
|
+
status: 'failed',
|
|
277
|
+
action: fix.description,
|
|
278
|
+
error: res?.err || 'unknown error',
|
|
279
|
+
});
|
|
280
|
+
} else if (skippedSet.has(fix.id)) {
|
|
281
|
+
const isBreaking = fix.impact === IMPACT.BREAKING;
|
|
282
|
+
items.push({
|
|
283
|
+
check: fix.id,
|
|
284
|
+
status: 'skipped',
|
|
285
|
+
skipped_reason: isBreaking
|
|
286
|
+
? 'Breaking fix — skipped in auto mode (use --auto --force to include)'
|
|
287
|
+
: 'User declined',
|
|
288
|
+
});
|
|
289
|
+
} else if (appliedSet.has(fix.id)) {
|
|
290
|
+
items.push({
|
|
291
|
+
check: fix.id,
|
|
292
|
+
status: 'hardened',
|
|
293
|
+
before: fix._reportBefore ?? null,
|
|
294
|
+
after: fix._reportAfter ?? null,
|
|
295
|
+
action: fix.description,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return items;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function writeJsonReport(reportPath, items) {
|
|
303
|
+
const sysInfo = getSystemInfo();
|
|
304
|
+
const hardened = items.filter(i => i.status === 'hardened').length;
|
|
305
|
+
const already_good = items.filter(i => i.status === 'already_good').length;
|
|
306
|
+
const skipped = items.filter(i => i.status === 'skipped').length;
|
|
307
|
+
const failed = items.filter(i => i.status === 'failed').length;
|
|
308
|
+
|
|
309
|
+
const report = {
|
|
310
|
+
version: VERSION,
|
|
311
|
+
timestamp: new Date().toISOString(),
|
|
312
|
+
system: sysInfo,
|
|
313
|
+
summary: {
|
|
314
|
+
total_checks: items.length,
|
|
315
|
+
hardened,
|
|
316
|
+
already_good,
|
|
317
|
+
skipped,
|
|
318
|
+
failed,
|
|
319
|
+
},
|
|
320
|
+
items,
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
try { mkdirSync(dirname(reportPath), { recursive: true }); } catch {}
|
|
324
|
+
writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf8');
|
|
325
|
+
return report;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function writeMarkdownReport(reportPath, items) {
|
|
329
|
+
const sysInfo = getSystemInfo();
|
|
330
|
+
const now = new Date();
|
|
331
|
+
const dateStr = now.toLocaleString('en-US', {
|
|
332
|
+
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
333
|
+
hour: '2-digit', minute: '2-digit', hour12: false
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const hardened = items.filter(i => i.status === 'hardened');
|
|
337
|
+
const alreadyGood = items.filter(i => i.status === 'already_good');
|
|
338
|
+
const skippedItems = items.filter(i => i.status === 'skipped');
|
|
339
|
+
const failedItems = items.filter(i => i.status === 'failed');
|
|
340
|
+
|
|
341
|
+
let md = `# ClawArmor Hardening Report
|
|
342
|
+
Generated: ${dateStr}
|
|
343
|
+
ClawArmor: v${VERSION} | OS: ${sysInfo.os} | OpenClaw: ${sysInfo.openclaw_version}
|
|
344
|
+
|
|
345
|
+
## Summary
|
|
346
|
+
- ✅ ${alreadyGood.length} check${alreadyGood.length !== 1 ? 's' : ''} already good
|
|
347
|
+
- 🔧 ${hardened.length} hardened
|
|
348
|
+
- ⚠️ ${skippedItems.length} skipped
|
|
349
|
+
${failedItems.length ? `- ❌ ${failedItems.length} failed\n` : ''}`;
|
|
350
|
+
|
|
351
|
+
if (hardened.length) {
|
|
352
|
+
md += `
|
|
353
|
+
## Actions Taken
|
|
354
|
+
|
|
355
|
+
| Check | Before | After | Action |
|
|
356
|
+
|-------|--------|-------|--------|
|
|
357
|
+
`;
|
|
358
|
+
for (const item of hardened) {
|
|
359
|
+
md += `| ${item.check} | ${item.before ?? '—'} | ${item.after ?? '—'} | ${item.action ?? '—'} |\n`;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (alreadyGood.length) {
|
|
364
|
+
md += `
|
|
365
|
+
## Already Good
|
|
366
|
+
|
|
367
|
+
`;
|
|
368
|
+
for (const item of alreadyGood) {
|
|
369
|
+
md += `- ${item.check}\n`;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (skippedItems.length) {
|
|
374
|
+
md += `
|
|
375
|
+
## Skipped
|
|
376
|
+
|
|
377
|
+
`;
|
|
378
|
+
for (const item of skippedItems) {
|
|
379
|
+
md += `- **${item.check}**: ${item.skipped_reason || 'no reason given'}\n`;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (failedItems.length) {
|
|
384
|
+
md += `
|
|
385
|
+
## Failed
|
|
386
|
+
|
|
387
|
+
`;
|
|
388
|
+
for (const item of failedItems) {
|
|
389
|
+
md += `- **${item.check}**: ${item.error || 'unknown error'}\n`;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
try { mkdirSync(dirname(reportPath), { recursive: true }); } catch {}
|
|
394
|
+
writeFileSync(reportPath, md, 'utf8');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function printReportSummary(items, reportPath, format) {
|
|
398
|
+
const hardened = items.filter(i => i.status === 'hardened').length;
|
|
399
|
+
const alreadyGood = items.filter(i => i.status === 'already_good').length;
|
|
400
|
+
const skipped = items.filter(i => i.status === 'skipped').length;
|
|
401
|
+
const failed = items.filter(i => i.status === 'failed').length;
|
|
402
|
+
|
|
403
|
+
console.log('');
|
|
404
|
+
console.log(SEP);
|
|
405
|
+
console.log(` ${paint.bold('Hardening Report')}`);
|
|
406
|
+
console.log(` ${paint.green('✅')} ${alreadyGood} already good ${paint.cyan('🔧')} ${hardened} hardened ${paint.yellow('⚠️')} ${skipped} skipped${failed ? ` ${paint.red('❌')} ${failed} failed` : ''}`);
|
|
407
|
+
console.log(` ${paint.dim('Report written:')} ${reportPath}`);
|
|
408
|
+
console.log(` ${paint.dim('Format:')} ${format === 'text' ? 'Markdown (.md)' : 'JSON'}`);
|
|
409
|
+
console.log('');
|
|
410
|
+
}
|
|
411
|
+
|
|
239
412
|
// ── Main export ───────────────────────────────────────────────────────────────
|
|
240
413
|
|
|
241
414
|
export async function runHarden(flags = {}) {
|
|
@@ -285,12 +458,9 @@ export async function runHarden(flags = {}) {
|
|
|
285
458
|
const overrideSev = getOverriddenSeverity(profile.name, fix.id);
|
|
286
459
|
const expected = isExpectedFinding(profile.name, fix.id);
|
|
287
460
|
if (expected) {
|
|
288
|
-
// Mark as expected — skip entirely from harden output
|
|
289
461
|
return { ...fix, _skipForProfile: true };
|
|
290
462
|
}
|
|
291
463
|
if (overrideSev) {
|
|
292
|
-
// Upgrade to higher severity if unexpected for this profile
|
|
293
|
-
const currentWeight = { SAFE: 1, CAUTION: 2, BREAKING: 3 };
|
|
294
464
|
const upgradeMap = { HIGH: IMPACT.BREAKING, MEDIUM: IMPACT.CAUTION, INFO: IMPACT.SAFE };
|
|
295
465
|
const newImpact = upgradeMap[overrideSev] || fix.impact;
|
|
296
466
|
return { ...fix, impact: newImpact, _profileOverride: overrideSev };
|
|
@@ -380,11 +550,24 @@ export async function runHarden(flags = {}) {
|
|
|
380
550
|
console.log(` ${paint.dim('•')} ${item}`);
|
|
381
551
|
}
|
|
382
552
|
console.log('');
|
|
553
|
+
|
|
554
|
+
// If report requested but nothing to harden, still write an empty/all-good report
|
|
555
|
+
if (flags.report) {
|
|
556
|
+
const format = flags.reportFormat || 'json';
|
|
557
|
+
const reportPath = flags.reportPath || defaultReportPath(format);
|
|
558
|
+
const items = []; // no fixes, no items (could add "already_good" items if we tracked checks)
|
|
559
|
+
if (format === 'text') {
|
|
560
|
+
writeMarkdownReport(reportPath, items);
|
|
561
|
+
} else {
|
|
562
|
+
writeJsonReport(reportPath, items);
|
|
563
|
+
}
|
|
564
|
+
printReportSummary(items, reportPath, format);
|
|
565
|
+
}
|
|
566
|
+
|
|
383
567
|
return 0;
|
|
384
568
|
}
|
|
385
569
|
|
|
386
570
|
if (flags.auto) {
|
|
387
|
-
// --auto mode: apply safe + caution, SKIP breaking unless --force
|
|
388
571
|
const autoLabel = flags.force
|
|
389
572
|
? paint.cyan('Auto mode (--force) — applying ALL fixes including breaking')
|
|
390
573
|
: paint.cyan('Auto mode — applying safe + caution fixes (skipping breaking)');
|
|
@@ -406,6 +589,12 @@ export async function runHarden(flags = {}) {
|
|
|
406
589
|
let applied = 0, skipped = 0, failed = 0, skippedBreaking = 0;
|
|
407
590
|
const restartNotes = [];
|
|
408
591
|
|
|
592
|
+
// Report tracking
|
|
593
|
+
const appliedIds = [];
|
|
594
|
+
const skippedIds = [];
|
|
595
|
+
const failedIds = [];
|
|
596
|
+
const applyResults = {};
|
|
597
|
+
|
|
409
598
|
for (const fix of fixes) {
|
|
410
599
|
const badge = IMPACT_BADGE[fix.impact]();
|
|
411
600
|
|
|
@@ -420,17 +609,16 @@ export async function runHarden(flags = {}) {
|
|
|
420
609
|
let doApply;
|
|
421
610
|
|
|
422
611
|
if (flags.auto) {
|
|
423
|
-
// In auto mode: apply safe + caution, skip breaking unless --force
|
|
424
612
|
if (fix.impact === IMPACT.BREAKING && !flags.force) {
|
|
425
613
|
console.log(` ${paint.red('⊘ Skipped')} ${paint.dim('(breaking — use --auto --force to include)')}`);
|
|
426
614
|
skippedBreaking++;
|
|
427
615
|
skipped++;
|
|
616
|
+
skippedIds.push(fix.id);
|
|
428
617
|
console.log('');
|
|
429
618
|
continue;
|
|
430
619
|
}
|
|
431
620
|
doApply = true;
|
|
432
621
|
} else {
|
|
433
|
-
// Interactive mode: always ask, but warn about breaking
|
|
434
622
|
if (fix.impact === IMPACT.BREAKING) {
|
|
435
623
|
console.log(` ${paint.red('⚠ This fix will change how your agent works.')}`);
|
|
436
624
|
console.log(` ${paint.red(' Read the impact above carefully before applying.')}`);
|
|
@@ -442,18 +630,22 @@ export async function runHarden(flags = {}) {
|
|
|
442
630
|
if (!doApply) {
|
|
443
631
|
console.log(` ${paint.dim('✗ Skipped')}`);
|
|
444
632
|
skipped++;
|
|
633
|
+
skippedIds.push(fix.id);
|
|
445
634
|
console.log('');
|
|
446
635
|
continue;
|
|
447
636
|
}
|
|
448
637
|
|
|
449
638
|
const result = applyFix(fix);
|
|
639
|
+
applyResults[fix.id] = result;
|
|
450
640
|
if (result.ok) {
|
|
451
641
|
console.log(` ${paint.green('✓ Fixed')}`);
|
|
452
642
|
applied++;
|
|
643
|
+
appliedIds.push(fix.id);
|
|
453
644
|
if (fix.manualNote) restartNotes.push(fix.manualNote);
|
|
454
645
|
} else {
|
|
455
646
|
console.log(` ${paint.red('✗ Failed:')} ${result.err}`);
|
|
456
647
|
failed++;
|
|
648
|
+
failedIds.push(fix.id);
|
|
457
649
|
}
|
|
458
650
|
console.log('');
|
|
459
651
|
}
|
|
@@ -505,6 +697,28 @@ export async function runHarden(flags = {}) {
|
|
|
505
697
|
}
|
|
506
698
|
}
|
|
507
699
|
|
|
700
|
+
// ── Write report if requested ──────────────────────────────────────────────
|
|
701
|
+
if (flags.report) {
|
|
702
|
+
const format = flags.reportFormat || 'json';
|
|
703
|
+
const reportPath = flags.reportPath || defaultReportPath(format);
|
|
704
|
+
|
|
705
|
+
const reportItems = buildReportItems({
|
|
706
|
+
fixes,
|
|
707
|
+
applied: appliedIds,
|
|
708
|
+
skipped: skippedIds,
|
|
709
|
+
failed: failedIds,
|
|
710
|
+
applyResults,
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
if (format === 'text') {
|
|
714
|
+
writeMarkdownReport(reportPath, reportItems);
|
|
715
|
+
} else {
|
|
716
|
+
writeJsonReport(reportPath, reportItems);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
printReportSummary(reportItems, reportPath, format);
|
|
720
|
+
}
|
|
721
|
+
|
|
508
722
|
console.log('');
|
|
509
723
|
return failed > 0 ? 1 : 0;
|
|
510
724
|
}
|