clawarmor 3.1.0 → 3.4.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 +147 -0
- package/README.md +49 -1
- package/clawgear-skills/clawarmor-live-monitor/SKILL.md +120 -0
- package/clawgear-skills/hardened-operator-baseline/SKILL.md +172 -0
- package/clawgear-skills/incident-response-playbook/SKILL.md +189 -0
- package/clawgear-skills/skill-security-scanner/SKILL.md +170 -0
- package/cli.js +65 -3
- package/lib/audit-quiet.js +89 -0
- package/lib/baseline-cmd.js +189 -0
- package/lib/baseline.js +106 -0
- package/lib/harden.js +225 -11
- package/lib/incident-cmd.js +201 -0
- package/lib/invariant-sync.js +668 -0
- package/lib/scan.js +88 -17
- package/lib/skill-verify.js +259 -0
- package/package.json +1 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: incident-response-playbook
|
|
3
|
+
description: Automated incident response for OpenClaw agents. When clawarmor audit finds a CRITICAL or HIGH finding, this playbook quarantines the affected extension, rolls back config to the last known-good snapshot, creates a structured incident log, and sends a Telegram alert. The action layer on top of ClawArmor detection.
|
|
4
|
+
category: security
|
|
5
|
+
tags: [security, incident-response, clawarmor, automation]
|
|
6
|
+
requires: clawarmor >= 3.2.0
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# incident-response-playbook
|
|
10
|
+
|
|
11
|
+
ClawArmor detects security issues. This skill responds to them. Automatically.
|
|
12
|
+
|
|
13
|
+
When a CRITICAL finding is detected, the playbook:
|
|
14
|
+
1. **Quarantines** the affected extension
|
|
15
|
+
2. **Rolls back** config to the last known-good snapshot
|
|
16
|
+
3. **Creates a structured incident log** via `clawarmor incident create`
|
|
17
|
+
4. **Sends a Telegram alert** via `openclaw system event`
|
|
18
|
+
|
|
19
|
+
Designed to run from your HEARTBEAT.md so it fires automatically on every cycle.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Scripts
|
|
24
|
+
|
|
25
|
+
### `quarantine.sh`
|
|
26
|
+
|
|
27
|
+
Disables a named extension and restarts the gateway.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
#!/usr/bin/env bash
|
|
31
|
+
# quarantine.sh — disable an extension immediately
|
|
32
|
+
# Usage: bash quarantine.sh <extension-name>
|
|
33
|
+
# requires: elevated
|
|
34
|
+
|
|
35
|
+
EXTENSION="$1"
|
|
36
|
+
|
|
37
|
+
if [ -z "$EXTENSION" ]; then
|
|
38
|
+
echo "Usage: bash quarantine.sh <extension-name>"
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
echo "[quarantine] Disabling extension: $EXTENSION"
|
|
43
|
+
openclaw config set "extensions.${EXTENSION}.disabled" true
|
|
44
|
+
openclaw gateway restart
|
|
45
|
+
|
|
46
|
+
echo "[quarantine] Extension '$EXTENSION' quarantined. Gateway restarted."
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### `respond.sh`
|
|
50
|
+
|
|
51
|
+
Full automated incident response: rollback, incident log, Telegram alert.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
#!/usr/bin/env bash
|
|
55
|
+
# respond.sh — full incident response for a security finding
|
|
56
|
+
# Usage: bash respond.sh "<finding-description>" <CRITICAL|HIGH|MEDIUM>
|
|
57
|
+
# requires: clawarmor >= 3.2.0, elevated
|
|
58
|
+
|
|
59
|
+
FINDING="$1"
|
|
60
|
+
SEVERITY="${2:-CRITICAL}"
|
|
61
|
+
|
|
62
|
+
if [ -z "$FINDING" ]; then
|
|
63
|
+
echo "Usage: bash respond.sh \"<finding-description>\" <CRITICAL|HIGH|MEDIUM>"
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
echo ""
|
|
68
|
+
echo "[respond] Incident Response Triggered"
|
|
69
|
+
echo "[respond] Finding: $FINDING"
|
|
70
|
+
echo "[respond] Severity: $SEVERITY"
|
|
71
|
+
echo ""
|
|
72
|
+
|
|
73
|
+
# Step 1: Rollback config
|
|
74
|
+
echo "[respond] Rolling back to last known-good snapshot..."
|
|
75
|
+
ROLLBACK_OUTPUT=$(clawarmor rollback 2>&1)
|
|
76
|
+
ROLLBACK_STATUS=$?
|
|
77
|
+
|
|
78
|
+
if [ $ROLLBACK_STATUS -ne 0 ]; then
|
|
79
|
+
echo "[respond] WARNING: Rollback failed or no snapshots available."
|
|
80
|
+
echo "[respond] Operator must review config manually."
|
|
81
|
+
ROLLBACK_NOTE="Rollback failed — manual review required"
|
|
82
|
+
else
|
|
83
|
+
echo "[respond] Config rolled back."
|
|
84
|
+
ROLLBACK_NOTE="Config rolled back via clawarmor rollback"
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# Step 2: Create incident log
|
|
88
|
+
echo "[respond] Creating incident log..."
|
|
89
|
+
INCIDENT_FILE=$(clawarmor incident create \
|
|
90
|
+
--finding "$FINDING" \
|
|
91
|
+
--severity "$SEVERITY" \
|
|
92
|
+
--action rollback \
|
|
93
|
+
2>&1 | grep "File:" | awk '{print $2}')
|
|
94
|
+
echo "[respond] Incident logged: ${INCIDENT_FILE:-unknown}"
|
|
95
|
+
|
|
96
|
+
# Step 3: Send Telegram alert
|
|
97
|
+
echo "[respond] Sending alert..."
|
|
98
|
+
openclaw system event \
|
|
99
|
+
--text "🚨 Security Incident: $FINDING (Severity: $SEVERITY) — Config rolled back. Check: clawarmor incident list" \
|
|
100
|
+
--mode now
|
|
101
|
+
|
|
102
|
+
echo ""
|
|
103
|
+
echo "[respond] Done. Incident response complete."
|
|
104
|
+
echo ""
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### `check-and-respond.sh`
|
|
108
|
+
|
|
109
|
+
Runs `clawarmor audit --json`, finds CRITICAL findings, auto-triggers `respond.sh` for each.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
#!/usr/bin/env bash
|
|
113
|
+
# check-and-respond.sh — auto-respond to new CRITICAL findings
|
|
114
|
+
# Add to HEARTBEAT.md to run automatically on every cycle
|
|
115
|
+
# requires: clawarmor >= 3.2.0, elevated
|
|
116
|
+
|
|
117
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
118
|
+
|
|
119
|
+
echo "[check-and-respond] Running security audit..."
|
|
120
|
+
AUDIT_JSON=$(clawarmor audit --json 2>/dev/null || echo '{"failed":[]}')
|
|
121
|
+
|
|
122
|
+
# Extract CRITICAL findings
|
|
123
|
+
CRITICALS=$(echo "$AUDIT_JSON" | python3 -c "
|
|
124
|
+
import sys, json
|
|
125
|
+
d = json.load(sys.stdin)
|
|
126
|
+
failed = d.get('failed', [])
|
|
127
|
+
for f in failed:
|
|
128
|
+
if f.get('severity') == 'CRITICAL' and not f.get('_profileExpected'):
|
|
129
|
+
print(f.get('title', f.get('id', 'Unknown finding')))
|
|
130
|
+
" 2>/dev/null)
|
|
131
|
+
|
|
132
|
+
if [ -z "$CRITICALS" ]; then
|
|
133
|
+
echo "[check-and-respond] No CRITICAL findings. Posture clean."
|
|
134
|
+
exit 0
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
echo "[check-and-respond] CRITICAL findings detected — triggering response..."
|
|
138
|
+
echo ""
|
|
139
|
+
|
|
140
|
+
while IFS= read -r finding; do
|
|
141
|
+
if [ -n "$finding" ]; then
|
|
142
|
+
echo "[check-and-respond] Responding to: $finding"
|
|
143
|
+
bash "$SCRIPT_DIR/respond.sh" "$finding" "CRITICAL"
|
|
144
|
+
fi
|
|
145
|
+
done <<< "$CRITICALS"
|
|
146
|
+
|
|
147
|
+
echo "[check-and-respond] All findings processed."
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Heartbeat integration
|
|
153
|
+
|
|
154
|
+
Add this to your `HEARTBEAT.md` to auto-respond to new CRITICAL findings on every cycle:
|
|
155
|
+
|
|
156
|
+
```markdown
|
|
157
|
+
## Security Incident Response
|
|
158
|
+
|
|
159
|
+
Run bash check-and-respond.sh from skills/incident-response-playbook/ — automatically respond to any CRITICAL security findings found during audit.
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Manual usage
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# Respond to a specific finding manually
|
|
168
|
+
bash ~/.openclaw/workspace/skills/incident-response-playbook/respond.sh \
|
|
169
|
+
"Malicious skill detected: bad-skill-v2" \
|
|
170
|
+
CRITICAL
|
|
171
|
+
|
|
172
|
+
# Quarantine a specific extension
|
|
173
|
+
bash ~/.openclaw/workspace/skills/incident-response-playbook/quarantine.sh \
|
|
174
|
+
bad-extension-name
|
|
175
|
+
|
|
176
|
+
# Run audit and auto-respond to everything critical
|
|
177
|
+
bash ~/.openclaw/workspace/skills/incident-response-playbook/check-and-respond.sh
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Notes
|
|
183
|
+
|
|
184
|
+
- **Rollback** uses ClawArmor's snapshot system (`clawarmor rollback`). If no snapshot exists (e.g., hardening was never run), rollback is skipped and the operator is alerted to review manually
|
|
185
|
+
- **Quarantine** requires `openclaw config set` and `openclaw gateway restart` to be available — these are standard OpenClaw commands
|
|
186
|
+
- **Telegram delivery** requires `openclaw system event` to be configured with a Telegram channel
|
|
187
|
+
- The `--action rollback` flag on `clawarmor incident create` also triggers rollback automatically — `respond.sh` calls both for belt-and-suspenders reliability
|
|
188
|
+
- Requires clawarmor 3.2.0+ for `clawarmor incident` and `clawarmor audit --json`
|
|
189
|
+
- The scripts use `set -e` for fail-fast behavior — if any step errors, the script exits immediately. Check logs if a step is skipped unexpectedly
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skill-security-scanner
|
|
3
|
+
description: Pre-install security gate for OpenClaw skills. Before installing any skill from ClawHub or ClawMart, run this scanner. Uses clawarmor scan --json to check for obfuscation, malicious patterns, and credential exposure. Blocks installs that score BLOCK verdict.
|
|
4
|
+
category: security
|
|
5
|
+
tags: [security, skills, pre-install, scanning, clawarmor]
|
|
6
|
+
requires: clawarmor >= 3.2.0
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# skill-security-scanner
|
|
10
|
+
|
|
11
|
+
A security gate that sits between you and any skill you're about to install. Before an untrusted skill hits your agent, run it through this scanner. BLOCK verdict means don't install. WARN means review first.
|
|
12
|
+
|
|
13
|
+
## The problem it solves
|
|
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 sent session tokens to an attacker-controlled DNS server. It was installed by dozens of operators without inspection.
|
|
16
|
+
|
|
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
|
+
|
|
19
|
+
**Never install a skill without scanning it first.**
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Scripts
|
|
24
|
+
|
|
25
|
+
### `scan-gate.sh`
|
|
26
|
+
|
|
27
|
+
The main gate script. Takes a skill directory path as argument.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
#!/usr/bin/env bash
|
|
31
|
+
# scan-gate.sh — pre-install security gate for OpenClaw skills
|
|
32
|
+
# Usage: bash scan-gate.sh <skill-path>
|
|
33
|
+
# Exit: 0=safe, 1=warn (manual review), 2=blocked
|
|
34
|
+
|
|
35
|
+
set -e
|
|
36
|
+
|
|
37
|
+
SKILL_PATH="$1"
|
|
38
|
+
|
|
39
|
+
if [ -z "$SKILL_PATH" ]; then
|
|
40
|
+
echo "Usage: bash scan-gate.sh <skill-path>"
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
if [ ! -d "$SKILL_PATH" ]; then
|
|
45
|
+
echo "Error: directory not found: $SKILL_PATH"
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
echo "[scan-gate] Scanning: $SKILL_PATH"
|
|
50
|
+
|
|
51
|
+
# Run clawarmor scan --json and capture output
|
|
52
|
+
SCAN_JSON=$(clawarmor scan --json 2>/dev/null || echo '{"verdict":"BLOCK","score":0,"findings":[]}')
|
|
53
|
+
|
|
54
|
+
VERDICT=$(echo "$SCAN_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('verdict','BLOCK'))")
|
|
55
|
+
SCORE=$(echo "$SCAN_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('score',0))")
|
|
56
|
+
FINDINGS=$(echo "$SCAN_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d.get('findings',[])))")
|
|
57
|
+
|
|
58
|
+
echo "[scan-gate] Verdict: $VERDICT | Score: $SCORE | Findings: $FINDINGS"
|
|
59
|
+
|
|
60
|
+
if [ "$VERDICT" = "BLOCK" ]; then
|
|
61
|
+
echo ""
|
|
62
|
+
echo "❌ BLOCKED — skill has CRITICAL findings and must not be installed."
|
|
63
|
+
echo ""
|
|
64
|
+
echo "$SCAN_JSON" | python3 -c "
|
|
65
|
+
import sys, json
|
|
66
|
+
d = json.load(sys.stdin)
|
|
67
|
+
for f in d.get('findings', []):
|
|
68
|
+
if f.get('severity') in ('CRITICAL', 'HIGH'):
|
|
69
|
+
print(f\" [{f['severity']}] {f.get('skill','?')} — {f.get('message','')}\")
|
|
70
|
+
"
|
|
71
|
+
echo ""
|
|
72
|
+
# Log to registry
|
|
73
|
+
bash "$(dirname "$0")/scan-registry.sh" "$SKILL_PATH" "$SCAN_JSON"
|
|
74
|
+
exit 2
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
if [ "$VERDICT" = "WARN" ]; then
|
|
78
|
+
echo ""
|
|
79
|
+
echo "⚠️ WARNING — skill has HIGH findings. Review before installing."
|
|
80
|
+
echo ""
|
|
81
|
+
echo "$SCAN_JSON" | python3 -c "
|
|
82
|
+
import sys, json
|
|
83
|
+
d = json.load(sys.stdin)
|
|
84
|
+
for f in d.get('findings', []):
|
|
85
|
+
print(f\" [{f['severity']}] {f.get('skill','?')} — {f.get('message','')}\")
|
|
86
|
+
"
|
|
87
|
+
echo ""
|
|
88
|
+
read -p "Install anyway? [y/N] " confirm
|
|
89
|
+
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
|
|
90
|
+
echo "Aborted."
|
|
91
|
+
bash "$(dirname "$0")/scan-registry.sh" "$SKILL_PATH" "$SCAN_JSON"
|
|
92
|
+
exit 1
|
|
93
|
+
fi
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
echo ""
|
|
97
|
+
echo "✅ PASS — skill cleared for install."
|
|
98
|
+
bash "$(dirname "$0")/scan-registry.sh" "$SKILL_PATH" "$SCAN_JSON"
|
|
99
|
+
exit 0
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### `scan-registry.sh`
|
|
103
|
+
|
|
104
|
+
Logs all scan results to a JSONL registry for audit trail.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
#!/usr/bin/env bash
|
|
108
|
+
# scan-registry.sh — log scan results to registry
|
|
109
|
+
# Usage: bash scan-registry.sh <skill-path> <scan-json>
|
|
110
|
+
|
|
111
|
+
SKILL_PATH="$1"
|
|
112
|
+
SCAN_JSON="$2"
|
|
113
|
+
REGISTRY="$HOME/.openclaw/workspace/memory/skill-scan-registry.jsonl"
|
|
114
|
+
|
|
115
|
+
mkdir -p "$(dirname "$REGISTRY")"
|
|
116
|
+
|
|
117
|
+
ENTRY=$(echo "$SCAN_JSON" | python3 -c "
|
|
118
|
+
import sys, json
|
|
119
|
+
d = json.load(sys.stdin)
|
|
120
|
+
import datetime
|
|
121
|
+
d['skillPath'] = '$SKILL_PATH'
|
|
122
|
+
d['loggedAt'] = datetime.datetime.utcnow().isoformat() + 'Z'
|
|
123
|
+
print(json.dumps(d))
|
|
124
|
+
" 2>/dev/null || echo '{}')
|
|
125
|
+
|
|
126
|
+
echo "$ENTRY" >> "$REGISTRY"
|
|
127
|
+
echo "[scan-registry] Logged to $REGISTRY"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Usage
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# Before installing a skill:
|
|
136
|
+
bash ~/.openclaw/workspace/skills/skill-security-scanner/scan-gate.sh \
|
|
137
|
+
~/.openclaw/workspace/skills/some-skill/
|
|
138
|
+
|
|
139
|
+
# If it exits 0 (PASS), safe to proceed with install
|
|
140
|
+
# If it exits 1 (WARN), review findings then decide
|
|
141
|
+
# If it exits 2 (BLOCK), do not install
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### In a CI/CD pipeline
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Fail pipeline if skill doesn't pass scan
|
|
148
|
+
bash scan-gate.sh ./my-new-skill/
|
|
149
|
+
if [ $? -eq 2 ]; then
|
|
150
|
+
echo "Skill failed security scan — aborting deploy"
|
|
151
|
+
exit 1
|
|
152
|
+
fi
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Trust levels
|
|
158
|
+
|
|
159
|
+
Even WARN skills from known sources (clawhub.com, shopclawmart.com) should be reviewed. The source of a skill doesn't guarantee its safety — supply chain attacks compromise trusted publishers too.
|
|
160
|
+
|
|
161
|
+
The only safe default is: **scan everything, trust nothing unverified**.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Notes
|
|
166
|
+
|
|
167
|
+
- Requires clawarmor 3.2.0+ for `scan --json`
|
|
168
|
+
- The registry at `~/.openclaw/workspace/memory/skill-scan-registry.jsonl` provides a complete audit trail of all scanned skills
|
|
169
|
+
- BLOCK = CRITICAL findings — patterns like eval, child_process with network, obfuscated code
|
|
170
|
+
- WARN = HIGH findings — patterns like credential file reads, WebSocket usage, cleartext HTTP
|
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.4.0';
|
|
7
7
|
const GATEWAY_PORT_DEFAULT = 18789;
|
|
8
8
|
|
|
9
9
|
function isLocalhost(host) {
|
|
@@ -51,7 +51,11 @@ function usage() {
|
|
|
51
51
|
console.log(` ${paint.cyan('watch')} Monitor config and skill changes in real time`);
|
|
52
52
|
console.log(` ${paint.cyan('protect')} Install/uninstall/status the full guard system`);
|
|
53
53
|
console.log(` ${paint.cyan('prescan')} Pre-scan a skill before installing it`);
|
|
54
|
+
console.log(` ${paint.cyan('baseline')} Save, list, and diff security baselines (save|list|diff)`);
|
|
55
|
+
console.log(` ${paint.cyan('incident')} Log and manage security incidents (create|list)`);
|
|
54
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)`);
|
|
58
|
+
console.log(` ${paint.cyan('skill')} Skill utilities — verify a skill directory (skill verify <dir>)`);
|
|
55
59
|
console.log(` ${paint.cyan('skill-report')} Show post-install audit impact of last skill install`);
|
|
56
60
|
console.log(` ${paint.cyan('profile')} Manage contextual hardening profiles`);
|
|
57
61
|
console.log(` ${paint.cyan('log')} View the audit event log`);
|
|
@@ -60,7 +64,7 @@ function usage() {
|
|
|
60
64
|
console.log(` ${paint.dim('Flags:')}`);
|
|
61
65
|
console.log(` ${paint.dim('--url <host:port>')} Probe a specific host:port instead of 127.0.0.1`);
|
|
62
66
|
console.log(` ${paint.dim('--config <path>')} Use a specific config file instead of ~/.openclaw/openclaw.json`);
|
|
63
|
-
console.log(` ${paint.dim('--json')} Machine-readable JSON output (audit
|
|
67
|
+
console.log(` ${paint.dim('--json')} Machine-readable JSON output (audit, scan)`);
|
|
64
68
|
console.log(` ${paint.dim('--explain-reads')} Print every file read and network call before executing
|
|
65
69
|
${paint.dim('--accept-changes')} Update config baseline after reviewing detected changes`);
|
|
66
70
|
console.log('');
|
|
@@ -141,7 +145,7 @@ if (cmd === 'audit') {
|
|
|
141
145
|
|
|
142
146
|
if (cmd === 'scan') {
|
|
143
147
|
const { runScan } = await import('./lib/scan.js');
|
|
144
|
-
process.exit(await runScan());
|
|
148
|
+
process.exit(await runScan({ json: flags.json }));
|
|
145
149
|
}
|
|
146
150
|
|
|
147
151
|
if (cmd === 'verify') {
|
|
@@ -208,6 +212,12 @@ if (cmd === 'log') {
|
|
|
208
212
|
|
|
209
213
|
if (cmd === 'harden') {
|
|
210
214
|
const hardenProfileIdx = args.indexOf('--profile');
|
|
215
|
+
const reportIdx = args.indexOf('--report');
|
|
216
|
+
const reportFormatIdx = args.indexOf('--report-format');
|
|
217
|
+
// --report can be a flag alone or --report <path>
|
|
218
|
+
const reportNext = reportIdx !== -1 ? args[reportIdx + 1] : null;
|
|
219
|
+
const reportPath = (reportNext && !reportNext.startsWith('--')) ? reportNext : null;
|
|
220
|
+
const reportFormat = reportFormatIdx !== -1 ? (args[reportFormatIdx + 1] || 'json') : 'json';
|
|
211
221
|
const hardenFlags = {
|
|
212
222
|
dryRun: args.includes('--dry-run'),
|
|
213
223
|
auto: args.includes('--auto'),
|
|
@@ -216,6 +226,9 @@ if (cmd === 'harden') {
|
|
|
216
226
|
monitorReport: args.includes('--monitor-report'),
|
|
217
227
|
monitorOff: args.includes('--monitor-off'),
|
|
218
228
|
profile: hardenProfileIdx !== -1 ? args[hardenProfileIdx + 1] : null,
|
|
229
|
+
report: reportIdx !== -1,
|
|
230
|
+
reportPath: reportPath,
|
|
231
|
+
reportFormat: reportFormat,
|
|
219
232
|
};
|
|
220
233
|
const { runHarden } = await import('./lib/harden.js');
|
|
221
234
|
process.exit(await runHarden(hardenFlags));
|
|
@@ -259,5 +272,54 @@ if (cmd === 'profile') {
|
|
|
259
272
|
process.exit(await runProfileCmd(profileArgs));
|
|
260
273
|
}
|
|
261
274
|
|
|
275
|
+
if (cmd === 'baseline') {
|
|
276
|
+
const { runBaseline } = await import('./lib/baseline-cmd.js');
|
|
277
|
+
const baselineArgs = args.slice(1);
|
|
278
|
+
process.exit(await runBaseline(baselineArgs));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (cmd === 'incident') {
|
|
282
|
+
const { runIncident } = await import('./lib/incident-cmd.js');
|
|
283
|
+
const incidentArgs = args.slice(1);
|
|
284
|
+
process.exit(await runIncident(incidentArgs));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (cmd === 'skill') {
|
|
288
|
+
const sub = args[1];
|
|
289
|
+
if (sub === 'verify') {
|
|
290
|
+
const skillDir = args[2];
|
|
291
|
+
const { runSkillVerify } = await import('./lib/skill-verify.js');
|
|
292
|
+
process.exit(await runSkillVerify(skillDir));
|
|
293
|
+
}
|
|
294
|
+
console.log(` ${paint.dim('Usage:')} clawarmor skill verify <skill-dir>`);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
if (cmd === 'invariant') {
|
|
300
|
+
const sub = args[1];
|
|
301
|
+
const invArgs = args.slice(2);
|
|
302
|
+
|
|
303
|
+
if (!sub || sub === 'status') {
|
|
304
|
+
const { runInvariantStatus } = await import('./lib/invariant-sync.js');
|
|
305
|
+
process.exit(await runInvariantStatus());
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (sub === 'sync') {
|
|
309
|
+
const { runInvariantSync } = await import('./lib/invariant-sync.js');
|
|
310
|
+
process.exit(await runInvariantSync(invArgs));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
console.log('');
|
|
314
|
+
console.log(` Invariant subcommands:`);
|
|
315
|
+
console.log(` clawarmor invariant status # show policy + last sync`);
|
|
316
|
+
console.log(` clawarmor invariant sync # generate policies from latest audit`);
|
|
317
|
+
console.log(` clawarmor invariant sync --dry-run # preview without writing`);
|
|
318
|
+
console.log(` clawarmor invariant sync --push # generate + push to Invariant instance`);
|
|
319
|
+
console.log(` clawarmor invariant sync --push --host <host> --port <port>`);
|
|
320
|
+
console.log('');
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
|
|
262
324
|
console.log(` ${paint.red('✗')} Unknown command: ${paint.bold(cmd)}`);
|
|
263
325
|
usage(); process.exit(1);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// audit-quiet.js — runs audit checks and returns results without printing.
|
|
2
|
+
// Used by baseline save to capture the current security posture.
|
|
3
|
+
|
|
4
|
+
import { loadConfig } from './config.js';
|
|
5
|
+
import { getProfile, isExpectedFinding } from './profiles.js';
|
|
6
|
+
import gatewayChecks from './checks/gateway.js';
|
|
7
|
+
import filesystemChecks from './checks/filesystem.js';
|
|
8
|
+
import channelChecks from './checks/channels.js';
|
|
9
|
+
import authChecks from './checks/auth.js';
|
|
10
|
+
import toolChecks from './checks/tools.js';
|
|
11
|
+
import versionChecks from './checks/version.js';
|
|
12
|
+
import hooksChecks from './checks/hooks.js';
|
|
13
|
+
import allowFromChecks from './checks/allowfrom.js';
|
|
14
|
+
import tokenAgeChecks from './checks/token-age.js';
|
|
15
|
+
import execApprovalChecks from './checks/exec-approval.js';
|
|
16
|
+
import skillPinningChecks from './checks/skill-pinning.js';
|
|
17
|
+
import gitCredentialLeakChecks from './checks/git-credential-leak.js';
|
|
18
|
+
import credentialFilesChecks from './checks/credential-files.js';
|
|
19
|
+
import { existsSync, readFileSync } from 'fs';
|
|
20
|
+
import { join } from 'path';
|
|
21
|
+
import { homedir } from 'os';
|
|
22
|
+
|
|
23
|
+
const W = { CRITICAL: 25, HIGH: 15, MEDIUM: 8, LOW: 3, INFO: 0 };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Run audit checks silently and return { score, findings, profile }.
|
|
27
|
+
* findings is an array of { id, severity, title, skill }.
|
|
28
|
+
*/
|
|
29
|
+
export async function runAuditQuiet(flags = {}) {
|
|
30
|
+
let profileName = flags.profile || null;
|
|
31
|
+
if (!profileName) {
|
|
32
|
+
try {
|
|
33
|
+
const pFile = join(homedir(), '.clawarmor', 'profile.json');
|
|
34
|
+
if (existsSync(pFile)) profileName = JSON.parse(readFileSync(pFile, 'utf8')).name || null;
|
|
35
|
+
} catch { /* non-fatal */ }
|
|
36
|
+
}
|
|
37
|
+
const activeProfile = profileName ? getProfile(profileName) : null;
|
|
38
|
+
|
|
39
|
+
const { config, error } = loadConfig(flags.configPath || null);
|
|
40
|
+
if (error) throw new Error(error);
|
|
41
|
+
|
|
42
|
+
const allChecks = [
|
|
43
|
+
...gatewayChecks, ...filesystemChecks, ...channelChecks,
|
|
44
|
+
...authChecks, ...toolChecks, ...versionChecks, ...hooksChecks,
|
|
45
|
+
...allowFromChecks,
|
|
46
|
+
...tokenAgeChecks, ...execApprovalChecks, ...skillPinningChecks,
|
|
47
|
+
...gitCredentialLeakChecks,
|
|
48
|
+
...credentialFilesChecks,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const staticResults = [];
|
|
52
|
+
for (const check of allChecks) {
|
|
53
|
+
try { staticResults.push(await check(config)); }
|
|
54
|
+
catch (e) { staticResults.push({ id: 'err', severity: 'LOW', passed: true, passedMsg: `Check error: ${e.message}` }); }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const failed = staticResults.filter(r => !r.passed);
|
|
58
|
+
|
|
59
|
+
const annotatedFailed = failed.map(f => {
|
|
60
|
+
if (activeProfile && isExpectedFinding(activeProfile.name, f.id)) {
|
|
61
|
+
return { ...f, _profileExpected: true };
|
|
62
|
+
}
|
|
63
|
+
return f;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const scoringFailed = activeProfile
|
|
67
|
+
? annotatedFailed.filter(f => !f._profileExpected)
|
|
68
|
+
: annotatedFailed;
|
|
69
|
+
|
|
70
|
+
const criticals = scoringFailed.filter(r => r.severity === 'CRITICAL').length;
|
|
71
|
+
|
|
72
|
+
let score = 100;
|
|
73
|
+
for (const f of scoringFailed) score -= (W[f.severity] || 0);
|
|
74
|
+
score = Math.max(0, score);
|
|
75
|
+
if (criticals >= 2) score = Math.min(score, 25);
|
|
76
|
+
else if (criticals >= 1) score = Math.min(score, 50);
|
|
77
|
+
|
|
78
|
+
const findings = annotatedFailed.map(f => ({
|
|
79
|
+
id: f.id,
|
|
80
|
+
severity: f.severity,
|
|
81
|
+
title: f.title || f.id,
|
|
82
|
+
patternId: f.id,
|
|
83
|
+
skill: f.skill || null,
|
|
84
|
+
message: f.title || f.description || '',
|
|
85
|
+
_profileExpected: f._profileExpected || false,
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
return { score, findings, profile: profileName };
|
|
89
|
+
}
|