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.
@@ -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.1.0';
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 only)`);
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
+ }