mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-13 05:28:02 +03:00
fix(clawsec-clawhub-checker): remove suspicious install patterns (#197)
* fix(clawsec-clawhub-checker): remove mutating setup and install scraping * fix(clawsec-clawhub-checker): harden fail-closed reputation paths
This commit is contained in:
@@ -0,0 +1 @@
|
||||
test/
|
||||
@@ -5,6 +5,21 @@ All notable changes to the ClawSec ClawHub Checker will be documented in this fi
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.0.3] - 2026-04-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Converted setup flow to non-mutating preflight validation; the skill no longer rewrites or copies files into installed `clawsec-suite` directories.
|
||||
- Updated reputation collection to rely on `clawhub inspect --json` security metadata instead of probing `clawhub install` output.
|
||||
- Updated documentation and metadata to describe standalone wrapper usage for guarded install checks.
|
||||
- Added explicit documentation for optional manual advisory-hook wiring when operators want `reputationWarning` fields in advisory alert rendering.
|
||||
|
||||
### Security
|
||||
|
||||
- Removed in-place cross-skill source mutation behavior from setup.
|
||||
- Removed install-output scraping behavior used only to infer VirusTotal status.
|
||||
- Reputation scoring now fails closed when scanner metadata is missing, and hook-level reputation subprocess execution failures are treated as unsafe results.
|
||||
|
||||
## [0.0.2] - 2026-04-14
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,141 +1,78 @@
|
||||
# ClawSec ClawHub Checker
|
||||
|
||||
A ClawSec suite skill that enhances the guarded skill installer with ClawHub reputation checks and VirusTotal Code Insight integration.
|
||||
A `clawsec-suite` companion skill that adds a standalone reputation gate before guarded installs.
|
||||
|
||||
## Operational Notes
|
||||
|
||||
- Required runtime: `node`, `clawhub`, `openclaw`
|
||||
- Dependency: installed `clawsec-suite`
|
||||
- Setup mutates the installed suite in place by copying helper scripts and rewriting the advisory guardian hook handler
|
||||
- Reputation checks contact ClawHub and can surface heuristic false positives; risky installs still require explicit user confirmation
|
||||
- No in-place mutation of other skills
|
||||
- Advisory-hook wiring is optional and manual in this release
|
||||
- Reputation checks query ClawHub metadata and remain confirmation-gated
|
||||
|
||||
## Purpose
|
||||
|
||||
Adds a second layer of security to skill installation by:
|
||||
1. Checking ClawHub's VirusTotal Code Insight reputation scores
|
||||
2. Analyzing skill age, author reputation, and download statistics
|
||||
3. Requiring double confirmation for suspicious skills
|
||||
4. Integrating with existing ClawSec advisory checks
|
||||
Adds a second risk signal before install by:
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
clawsec-suite (base)
|
||||
└── clawsec-clawhub-checker (enhancement)
|
||||
├── enhanced_guarded_install.mjs - Main enhanced installer
|
||||
├── check_clawhub_reputation.mjs - Reputation checking logic
|
||||
├── setup_reputation_hook.mjs - Integration script
|
||||
└── hooks/ - Enhanced advisory guardian hook
|
||||
```
|
||||
1. Reading ClawHub inspect/security metadata
|
||||
2. Applying reputation heuristics (age, updates, author activity, downloads)
|
||||
3. Requiring `--confirm-reputation` for low-score installs
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# First install the base suite
|
||||
npx clawhub install clawsec-suite
|
||||
|
||||
# Then install the checker
|
||||
npx clawhub install clawsec-clawhub-checker
|
||||
|
||||
# Run setup to integrate with existing suite
|
||||
node scripts/setup_reputation_hook.mjs
|
||||
|
||||
# Restart OpenClaw gateway
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
The setup script prints a preflight review before it mutates the installed suite files.
|
||||
Optional preflight helper:
|
||||
|
||||
Setup installs these scripts into `clawsec-suite/scripts`:
|
||||
- `enhanced_guarded_install.mjs`
|
||||
- `guarded_skill_install_wrapper.mjs` (drop-in wrapper)
|
||||
- `check_clawhub_reputation.mjs`
|
||||
|
||||
The original `guarded_skill_install.mjs` remains unchanged.
|
||||
```bash
|
||||
node ~/.openclaw/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Enhanced Guarded Installer
|
||||
|
||||
```bash
|
||||
# Basic usage via wrapper (includes reputation checks)
|
||||
node scripts/guarded_skill_install_wrapper.mjs --skill some-skill --version 1.0.0
|
||||
|
||||
# Direct usage (enhanced script)
|
||||
node scripts/enhanced_guarded_install.mjs --skill some-skill --version 1.0.0
|
||||
|
||||
# With reputation confirmation override
|
||||
node scripts/guarded_skill_install_wrapper.mjs --skill suspicious-skill --version 1.0.0 --confirm-reputation
|
||||
|
||||
# Adjust reputation threshold (default: 70)
|
||||
node scripts/guarded_skill_install_wrapper.mjs --skill some-skill --reputation-threshold 80
|
||||
node ~/.openclaw/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs \
|
||||
--skill some-skill \
|
||||
--version 1.0.0
|
||||
```
|
||||
|
||||
### Reputation Check Only
|
||||
Override only after manual review:
|
||||
|
||||
```bash
|
||||
# Check reputation without installation
|
||||
node scripts/check_clawhub_reputation.mjs some-skill 1.0.0 70
|
||||
node ~/.openclaw/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs \
|
||||
--skill some-skill \
|
||||
--version 1.0.0 \
|
||||
--confirm-reputation
|
||||
```
|
||||
|
||||
## Optional Advisory-Hook Wiring
|
||||
|
||||
If you need advisory alerts to include `reputationWarning` / `reputationWarnings`, wire the checker module manually into the installed suite hook:
|
||||
|
||||
- Source: `~/.openclaw/skills/clawsec-clawhub-checker/hooks/clawsec-advisory-guardian/lib/reputation.mjs`
|
||||
- Target: `~/.openclaw/skills/clawsec-suite/hooks/clawsec-advisory-guardian/handler.ts`
|
||||
|
||||
The setup helper validates paths only and does not patch these files automatically.
|
||||
|
||||
## Exit Codes
|
||||
|
||||
- `0` - Safe to install
|
||||
- `42` - Advisory match found (requires `--confirm-advisory`)
|
||||
- `43` - Reputation warning (requires `--confirm-reputation`) - **NEW**
|
||||
- `1` - Error
|
||||
|
||||
## Reputation Signals Checked
|
||||
|
||||
1. **VirusTotal Code Insight** - Malicious code patterns
|
||||
2. **Skill Age** - New skills (<7 days) are riskier
|
||||
3. **Author Reputation** - Number of published skills
|
||||
4. **Update Frequency** - Stale skills (>90 days)
|
||||
5. **Download Statistics** - Low download counts
|
||||
6. **Version Existence** - Specified version availability
|
||||
- `0` safe to install
|
||||
- `42` advisory confirmation required
|
||||
- `43` reputation confirmation required
|
||||
- `1` error
|
||||
|
||||
## Configuration
|
||||
|
||||
Environment variables:
|
||||
- `CLAWHUB_REPUTATION_THRESHOLD` - Minimum score (0-100, default: 70)
|
||||
|
||||
## Integration Points
|
||||
|
||||
1. **Enhanced `guarded_skill_install.mjs`** - Wraps original with reputation checks
|
||||
via `guarded_skill_install_wrapper.mjs` and `enhanced_guarded_install.mjs`
|
||||
2. **Updated advisory guardian hook** - Adds reputation warnings to alerts
|
||||
3. **Catalog entry in clawsec-suite** - Listed as available enhancement
|
||||
|
||||
## Development
|
||||
|
||||
### Files
|
||||
|
||||
- `SKILL.md` - Main documentation
|
||||
- `skill.json` - Skill metadata and SBOM
|
||||
- `scripts/enhanced_guarded_install.mjs` - Enhanced installer
|
||||
- `scripts/check_clawhub_reputation.mjs` - Reputation logic
|
||||
- `scripts/setup_reputation_hook.mjs` - Integration script
|
||||
- `hooks/clawsec-advisory-guardian/lib/reputation.mjs` - Hook module
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Test reputation check
|
||||
node scripts/check_clawhub_reputation.mjs clawsec-suite
|
||||
|
||||
# Test enhanced installer (dry run)
|
||||
node scripts/enhanced_guarded_install.mjs --skill test-skill --dry-run
|
||||
|
||||
# Test setup
|
||||
node scripts/setup_reputation_hook.mjs
|
||||
```
|
||||
- `CLAWHUB_REPUTATION_THRESHOLD` (default: 70)
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Reputation checks are **heuristic**, not definitive
|
||||
- **False positives** possible with legitimate novel skills
|
||||
- Always **review skill code** before overriding warnings
|
||||
- This is **defense-in-depth**, not replacement for advisory feeds
|
||||
- Reputation is heuristic, not authoritative
|
||||
- False positives are possible
|
||||
- Always inspect code before confirming installation
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: clawsec-clawhub-checker
|
||||
version: 0.0.2
|
||||
description: ClawHub reputation checker for ClawSec suite. Enhances guarded skill installer with VirusTotal Code Insight reputation scores and additional safety checks.
|
||||
version: 0.0.3
|
||||
description: ClawHub reputation checker for clawsec-suite. Adds a standalone reputation gate before guarded skill installation.
|
||||
homepage: https://clawsec.prompt.security
|
||||
clawdis:
|
||||
emoji: "🛡️"
|
||||
@@ -12,149 +12,95 @@ clawdis:
|
||||
|
||||
# ClawSec ClawHub Checker
|
||||
|
||||
Enhances the ClawSec suite's guarded skill installer with ClawHub reputation checks. Adds a second layer of security by checking VirusTotal Code Insight scores and other reputation signals before allowing skill installation.
|
||||
Adds a reputation gate on top of the `clawsec-suite` guarded installer.
|
||||
|
||||
## Operational Notes
|
||||
|
||||
- Required runtime: `node`, `clawhub`, `openclaw`
|
||||
- Depends on: installed `clawsec-suite`
|
||||
- Side effects: `setup_reputation_hook.mjs` copies files into the installed suite and rewrites `hooks/clawsec-advisory-guardian/handler.ts`
|
||||
- Network behavior: reputation checks query ClawHub and may trigger remote metadata lookups during `inspect`/declined `install` flows
|
||||
- Trust model: reputation scores are heuristic, not authoritative; keep the double-confirmation flow enabled
|
||||
- Side effects: none on other skills; this package does not rewrite installed suite files
|
||||
- Advisory-hook wiring is optional and manual in this release
|
||||
- Network behavior: reputation checks call ClawHub inspect/search endpoints
|
||||
- Trust model: scores are heuristic and confirmation-gated
|
||||
|
||||
## What It Does
|
||||
|
||||
1. **Wraps `clawhub install`** - Intercepts skill installation requests
|
||||
2. **Checks VirusTotal reputation** - Uses ClawHub's built-in VirusTotal Code Insight
|
||||
3. **Adds double confirmation** - For suspicious skills (reputation score below threshold)
|
||||
4. **Integrates with advisory feed** - Works alongside existing clawsec-suite advisories
|
||||
5. **Provides detailed reports** - Shows why a skill is flagged as suspicious
|
||||
1. Reads skill metadata from ClawHub (`inspect --json`)
|
||||
2. Evaluates scanner status (including VirusTotal summary when present)
|
||||
3. Applies additional reputation heuristics (age, updates, author history, downloads)
|
||||
4. Requires explicit `--confirm-reputation` when score is below threshold
|
||||
|
||||
## Installation
|
||||
|
||||
This skill must be installed **after** `clawsec-suite`:
|
||||
Install after `clawsec-suite`:
|
||||
|
||||
```bash
|
||||
# First install the suite
|
||||
npx clawhub@latest install clawsec-suite
|
||||
|
||||
# Then install the checker
|
||||
npx clawhub@latest install clawsec-clawhub-checker
|
||||
|
||||
# Run the setup script to integrate with clawsec-suite
|
||||
node ~/.openclaw/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs
|
||||
|
||||
# Restart OpenClaw gateway for changes to take effect
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
The setup script prints a preflight review before it mutates the installed suite files.
|
||||
Optional preflight check (validates local paths and prints recommended command):
|
||||
|
||||
After setup, the checker adds `enhanced_guarded_install.mjs` and
|
||||
`guarded_skill_install_wrapper.mjs` under `clawsec-suite/scripts` and updates the advisory
|
||||
guardian hook. The original `guarded_skill_install.mjs` is not replaced.
|
||||
|
||||
Review the printed preflight summary before running setup. The script intentionally modifies the installed suite in place rather than operating on a temporary copy.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Enhanced Guarded Installer
|
||||
|
||||
After setup, run the wrapper (drop-in path) or the enhanced script directly:
|
||||
```bash
|
||||
# Recommended drop-in wrapper
|
||||
node scripts/guarded_skill_install_wrapper.mjs --skill some-skill --version 1.0.0
|
||||
|
||||
# Or call the enhanced script directly
|
||||
node scripts/enhanced_guarded_install.mjs --skill some-skill --version 1.0.0
|
||||
node ~/.openclaw/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs
|
||||
```
|
||||
|
||||
The enhanced flow:
|
||||
1. **Advisory check** (existing) - Checks clawsec advisory feed
|
||||
2. **Reputation check** (new) - Queries ClawHub for VirusTotal scores
|
||||
3. **Risk assessment** - Combines advisory + reputation signals
|
||||
4. **Double confirmation** - If risky, requires explicit `--confirm-reputation`
|
||||
## Usage
|
||||
|
||||
### Reputation Signals Checked
|
||||
Run the enhanced installer directly from this skill:
|
||||
|
||||
1. **VirusTotal Code Insight** - Malicious code patterns, external dependencies (Docker usage, network calls, eval usage, crypto keys)
|
||||
2. **Skill age & updates** - New skills vs established ones
|
||||
3. **Author reputation** - Other skills by same author
|
||||
4. **Download statistics** - Popularity signals
|
||||
```bash
|
||||
node ~/.openclaw/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs \
|
||||
--skill some-skill \
|
||||
--version 1.0.0
|
||||
```
|
||||
|
||||
### Exit Codes
|
||||
If a skill is below threshold, rerun only with explicit approval:
|
||||
|
||||
- `0` - Safe to install (no advisories, good reputation)
|
||||
- `42` - Advisory match found (existing behavior)
|
||||
- `43` - Reputation warning (new - requires `--confirm-reputation`)
|
||||
- `1` - Error
|
||||
```bash
|
||||
node ~/.openclaw/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs \
|
||||
--skill some-skill \
|
||||
--version 1.0.0 \
|
||||
--confirm-reputation
|
||||
```
|
||||
|
||||
## Optional Advisory-Hook Wiring (Manual)
|
||||
|
||||
This release does not auto-patch `clawsec-suite` hook files.
|
||||
If you rely on advisory alerts that include `reputationWarning` / `reputationWarnings`, wire the checker module manually:
|
||||
|
||||
- Source module: `~/.openclaw/skills/clawsec-clawhub-checker/hooks/clawsec-advisory-guardian/lib/reputation.mjs`
|
||||
- Target hook file: `~/.openclaw/skills/clawsec-suite/hooks/clawsec-advisory-guardian/handler.ts`
|
||||
|
||||
Treat that wiring as a deliberate local customization and review it before enabling.
|
||||
|
||||
## Exit Codes
|
||||
|
||||
- `0` safe to install
|
||||
- `42` advisory confirmation required (from clawsec-suite)
|
||||
- `43` reputation confirmation required
|
||||
- `1` error
|
||||
|
||||
## Configuration
|
||||
|
||||
Environment variables:
|
||||
- `CLAWHUB_REPUTATION_THRESHOLD` - Minimum reputation score (0-100, default: 70)
|
||||
|
||||
## Integration with Existing Suite
|
||||
|
||||
The checker enhances but doesn't replace existing security:
|
||||
- **Advisory feed still primary** - Known malicious skills blocked first
|
||||
- **Reputation is secondary** - Unknown/suspicious skills get extra scrutiny
|
||||
- **Double confirmation preserved** - Both layers require explicit user approval
|
||||
|
||||
## Example Usage
|
||||
|
||||
```bash
|
||||
# Try to install a skill
|
||||
node scripts/guarded_skill_install_wrapper.mjs --skill suspicious-skill --version 1.0.0
|
||||
|
||||
# Output might show:
|
||||
# WARNING: Skill "suspicious-skill" has low reputation score (45/100)
|
||||
# - Flagged by VirusTotal Code Insight: crypto keys, external APIs, eval usage
|
||||
# - Author has no other published skills
|
||||
# - Skill is less than 7 days old
|
||||
#
|
||||
# To install despite reputation warning, run:
|
||||
# node scripts/guarded_skill_install_wrapper.mjs --skill suspicious-skill --version 1.0.0 --confirm-reputation
|
||||
|
||||
# Install with confirmation
|
||||
node scripts/guarded_skill_install_wrapper.mjs --skill suspicious-skill --version 1.0.0 --confirm-reputation
|
||||
```
|
||||
- `CLAWHUB_REPUTATION_THRESHOLD` - Minimum score (0-100, default: 70)
|
||||
|
||||
## Safety Notes
|
||||
|
||||
- This is a **defense-in-depth** layer, not a replacement for advisory feeds
|
||||
- VirusTotal scores are **heuristic**, not definitive
|
||||
- **False positives possible** - Legitimate skills with novel patterns might be flagged
|
||||
- Always **review skill code** before installing with `--confirm-reputation`
|
||||
|
||||
## Current Limitations
|
||||
|
||||
### Missing OpenClaw Internal Check Data
|
||||
ClawHub shows two security badges on skill pages:
|
||||
1. **VirusTotal Code Insight** - ✅ Our checker catches these flags
|
||||
2. **OpenClaw internal check** - ❌ Not exposed via API (only on website)
|
||||
|
||||
Example from `clawsec-suite` page:
|
||||
- VirusTotal: "Benign" ✓
|
||||
- OpenClaw internal check: "The package is internally consistent with a feed-monitoring / advisory-guardian purpose, but a few operational details and optional bypasses deserve attention before installing."
|
||||
|
||||
**Our checker cannot access OpenClaw internal check warnings** as they're not exposed via `clawhub` CLI or API.
|
||||
|
||||
### Recommendation for ClawHub
|
||||
To enable complete reputation checking, ClawHub should expose internal check results via:
|
||||
- `clawhub inspect --json` endpoint
|
||||
- Additional API field for security tools
|
||||
- Or include in `clawhub install` warning output
|
||||
|
||||
### Workaround
|
||||
Our heuristic checks (skill age, author reputation, downloads, updates) provide similar risk assessment but miss specific operational warnings about bypasses, missing signatures, etc. Always check the ClawHub website for complete security assessment.
|
||||
- This is defense-in-depth, not a replacement for advisory matching
|
||||
- Scanner outputs can produce false positives and false negatives
|
||||
- Always review skill code before overriding warnings
|
||||
|
||||
## Development
|
||||
|
||||
To modify the reputation checking logic, edit:
|
||||
- `scripts/enhanced_guarded_install.mjs` - Main enhanced installer
|
||||
- `scripts/check_clawhub_reputation.mjs` - Reputation checking logic
|
||||
- `hooks/clawsec-advisory-guardian/lib/reputation.mjs` - Hook integration
|
||||
Key files:
|
||||
|
||||
- `scripts/enhanced_guarded_install.mjs`
|
||||
- `scripts/check_clawhub_reputation.mjs`
|
||||
- `scripts/setup_reputation_hook.mjs`
|
||||
- `hooks/clawsec-advisory-guardian/lib/reputation.mjs`
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { spawnSync as runProcessSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function checkReputation(skillName, version) {
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const checkerDir = path.resolve(__dirname, '../../..');
|
||||
|
||||
const reputationCheck = spawnSync(
|
||||
const reputationCheck = runProcessSync(
|
||||
"node",
|
||||
[
|
||||
`${checkerDir}/scripts/check_clawhub_reputation.mjs`,
|
||||
@@ -37,6 +37,20 @@ export async function checkReputation(skillName, version) {
|
||||
{ encoding: "utf-8", cwd: checkerDir }
|
||||
);
|
||||
|
||||
if (reputationCheck.error) {
|
||||
result.safe = false;
|
||||
result.score = 0;
|
||||
result.warnings.push(`Reputation check execution error: ${reputationCheck.error.message}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (typeof reputationCheck.status !== "number") {
|
||||
result.safe = false;
|
||||
result.score = 0;
|
||||
result.warnings.push("Reputation check did not return a process exit status");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (reputationCheck.status === 0) {
|
||||
try {
|
||||
const repResult = JSON.parse(reputationCheck.stdout);
|
||||
@@ -61,10 +75,16 @@ export async function checkReputation(skillName, version) {
|
||||
result.warnings.push("Skill flagged by reputation check");
|
||||
}
|
||||
} else {
|
||||
// Error running check
|
||||
result.warnings.push(`Reputation check failed: ${reputationCheck.stderr || 'Unknown error'}`);
|
||||
result.score = 60;
|
||||
result.safe = result.score >= 70;
|
||||
const stderr = (reputationCheck.stderr || "").trim();
|
||||
const stdout = (reputationCheck.stdout || "").trim();
|
||||
const output = [stderr, stdout].filter((entry) => entry).join(" | ");
|
||||
result.warnings.push(
|
||||
`Reputation check failed with exit code ${reputationCheck.status}${
|
||||
output ? `: ${output}` : ""
|
||||
}`,
|
||||
);
|
||||
result.score = 0;
|
||||
result.safe = false;
|
||||
}
|
||||
} catch (error) {
|
||||
result.warnings.push(`Reputation check error: ${error.message}`);
|
||||
|
||||
@@ -1,9 +1,106 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { spawnSync as runProcessSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
function runClawhub(args) {
|
||||
return runProcessSync("clawhub", args, { encoding: "utf-8" });
|
||||
}
|
||||
|
||||
function toPublicResult(result) {
|
||||
return {
|
||||
safe: result.safe,
|
||||
score: result.score,
|
||||
warnings: result.warnings,
|
||||
virustotal: result.virustotal,
|
||||
};
|
||||
}
|
||||
|
||||
function finalizeResult(result, threshold) {
|
||||
result.score = Math.max(0, Math.min(100, result.score));
|
||||
result.safe = !result.blocked && result.score >= threshold;
|
||||
if (!result.safe) {
|
||||
const thresholdWarning = `Reputation score ${result.score}/100 below threshold ${threshold}/100`;
|
||||
if (!result.warnings.includes(thresholdWarning)) {
|
||||
result.warnings.unshift(thresholdWarning);
|
||||
}
|
||||
}
|
||||
return toPublicResult(result);
|
||||
}
|
||||
|
||||
function blockOnMissingScannerData(result, warning) {
|
||||
result.warnings.push(warning);
|
||||
result.score = Math.min(result.score, 60);
|
||||
result.blocked = true;
|
||||
}
|
||||
|
||||
function parseJson(raw, label, warnings) {
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (error) {
|
||||
warnings.push(
|
||||
`Failed to parse ${label}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function maybeApplyVersionSecuritySignals(result, versionDetails) {
|
||||
if (!versionDetails || typeof versionDetails !== "object") {
|
||||
blockOnMissingScannerData(result, "ClawHub version security details are unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
const security = versionDetails.security;
|
||||
if (!security || typeof security !== "object") {
|
||||
blockOnMissingScannerData(result, "ClawHub version record does not include security scanner output");
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof security.status === "string" && security.status.toLowerCase() === "suspicious") {
|
||||
result.warnings.push("ClawHub static moderation marked the version as suspicious");
|
||||
result.score -= 30;
|
||||
}
|
||||
|
||||
const scanners = security.scanners;
|
||||
if (!scanners || typeof scanners !== "object") {
|
||||
blockOnMissingScannerData(result, "ClawHub scanner breakdown is missing from version metadata");
|
||||
return;
|
||||
}
|
||||
|
||||
const vt = scanners.vt;
|
||||
if (!vt || typeof vt !== "object") {
|
||||
blockOnMissingScannerData(result, "VirusTotal scanner data was not returned by ClawHub");
|
||||
return;
|
||||
}
|
||||
|
||||
const vtStatus =
|
||||
(typeof vt.normalizedStatus === "string" && vt.normalizedStatus) ||
|
||||
(typeof vt.status === "string" && vt.status) ||
|
||||
(typeof vt.verdict === "string" && vt.verdict) ||
|
||||
"";
|
||||
const normalizedStatus = vtStatus.toLowerCase();
|
||||
|
||||
if (normalizedStatus === "suspicious") {
|
||||
result.virustotal.push("ClawHub VirusTotal scan returned suspicious");
|
||||
result.score -= 40;
|
||||
|
||||
const vtSummary = typeof vt.analysis === "string" ? vt.analysis.trim() : "";
|
||||
if (vtSummary) {
|
||||
result.virustotal.push(vtSummary.split("\n")[0]);
|
||||
}
|
||||
} else if (normalizedStatus === "clean" || normalizedStatus === "benign") {
|
||||
result.virustotal.push("ClawHub VirusTotal scan returned clean");
|
||||
} else if (normalizedStatus) {
|
||||
result.warnings.push(`VirusTotal scanner status reported as: ${normalizedStatus}`);
|
||||
result.score -= 10;
|
||||
} else {
|
||||
result.warnings.push("VirusTotal scanner status was unavailable");
|
||||
result.score -= 10;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check ClawHub reputation for a skill
|
||||
* @param {string} skillSlug - Skill slug to check
|
||||
@@ -14,176 +111,133 @@ import { pathToFileURL } from "node:url";
|
||||
export async function checkClawhubReputation(skillSlug, version, threshold = 70) {
|
||||
const result = {
|
||||
safe: true,
|
||||
score: 100, // Default score if no checks fail
|
||||
score: 100,
|
||||
warnings: [],
|
||||
virustotal: [],
|
||||
blocked: false,
|
||||
};
|
||||
|
||||
// Input validation — reject anything that isn't a safe slug or semver
|
||||
if (!/^[a-z0-9][a-z0-9-]*$/.test(skillSlug)) {
|
||||
result.warnings.push(`Invalid skill slug: ${skillSlug}`);
|
||||
result.score = 0;
|
||||
result.safe = false;
|
||||
return result;
|
||||
result.blocked = true;
|
||||
return toPublicResult(result);
|
||||
}
|
||||
// Semver validation: supports major.minor.patch with optional pre-release and build metadata
|
||||
// Examples: 1.0.0, 1.0.0-alpha.1, 1.0.0-beta+20130313144700
|
||||
// More restrictive than full semver spec for security (prevents command injection)
|
||||
|
||||
if (version && !/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?$/.test(version)) {
|
||||
result.warnings.push(`Invalid version format: ${version}`);
|
||||
result.score = 0;
|
||||
result.safe = false;
|
||||
return result;
|
||||
result.blocked = true;
|
||||
return toPublicResult(result);
|
||||
}
|
||||
|
||||
try {
|
||||
// Check 1: Try to inspect the skill via clawhub
|
||||
const inspectResult = spawnSync(
|
||||
"clawhub",
|
||||
["inspect", skillSlug, "--json"],
|
||||
{ encoding: "utf-8" }
|
||||
);
|
||||
const inspectArgs = ["inspect", skillSlug, "--json"];
|
||||
if (version) inspectArgs.push("--version", version);
|
||||
const inspectResult = runClawhub(inspectArgs);
|
||||
|
||||
if (inspectResult.status !== 0) {
|
||||
// Skill doesn't exist or can't be inspected
|
||||
result.warnings.push(`Skill "${skillSlug}" not found or cannot be inspected`);
|
||||
result.score = Math.min(result.score, 50);
|
||||
} else {
|
||||
try {
|
||||
const skillInfo = JSON.parse(inspectResult.stdout);
|
||||
|
||||
// Check 2: Skill age (new skills are riskier)
|
||||
if (skillInfo.skill?.createdAt) {
|
||||
const createdMs = skillInfo.skill.createdAt;
|
||||
const ageDays = (Date.now() - createdMs) / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (ageDays < 7) {
|
||||
result.warnings.push(`Skill is less than 7 days old (${ageDays.toFixed(1)} days)`);
|
||||
result.score -= 15;
|
||||
} else if (ageDays < 30) {
|
||||
result.warnings.push(`Skill is less than 30 days old (${ageDays.toFixed(1)} days)`);
|
||||
result.score -= 5;
|
||||
}
|
||||
}
|
||||
|
||||
// Check 3: Update frequency (stale skills are riskier)
|
||||
if (skillInfo.skill?.updatedAt && skillInfo.skill?.createdAt) {
|
||||
const updatedMs = skillInfo.skill.updatedAt;
|
||||
const createdMs = skillInfo.skill.createdAt;
|
||||
const updateAgeDays = (Date.now() - updatedMs) / (1000 * 60 * 60 * 24);
|
||||
const totalAgeDays = (Date.now() - createdMs) / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (updateAgeDays > 90 && totalAgeDays > 90) {
|
||||
result.warnings.push(`Skill hasn't been updated in ${updateAgeDays.toFixed(0)} days`);
|
||||
result.score -= 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Check 4: Author reputation
|
||||
if (skillInfo.owner?.handle) {
|
||||
const authorResult = spawnSync(
|
||||
"clawhub",
|
||||
["search", skillInfo.owner.handle],
|
||||
{ encoding: "utf-8" }
|
||||
result.score = Math.min(result.score, 40);
|
||||
result.blocked = true;
|
||||
return finalizeResult(result, threshold);
|
||||
}
|
||||
|
||||
const skillInfo = parseJson(inspectResult.stdout, "skill inspection payload", result.warnings);
|
||||
if (!skillInfo) {
|
||||
result.score = Math.min(result.score, 40);
|
||||
result.blocked = true;
|
||||
return finalizeResult(result, threshold);
|
||||
}
|
||||
|
||||
if (skillInfo.skill?.createdAt) {
|
||||
const createdMs = skillInfo.skill.createdAt;
|
||||
const ageDays = (Date.now() - createdMs) / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (ageDays < 7) {
|
||||
result.warnings.push(`Skill is less than 7 days old (${ageDays.toFixed(1)} days)`);
|
||||
result.score -= 15;
|
||||
} else if (ageDays < 30) {
|
||||
result.warnings.push(`Skill is less than 30 days old (${ageDays.toFixed(1)} days)`);
|
||||
result.score -= 5;
|
||||
}
|
||||
}
|
||||
|
||||
if (skillInfo.skill?.updatedAt && skillInfo.skill?.createdAt) {
|
||||
const updatedMs = skillInfo.skill.updatedAt;
|
||||
const createdMs = skillInfo.skill.createdAt;
|
||||
const updateAgeDays = (Date.now() - updatedMs) / (1000 * 60 * 60 * 24);
|
||||
const totalAgeDays = (Date.now() - createdMs) / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (updateAgeDays > 90 && totalAgeDays > 90) {
|
||||
result.warnings.push(`Skill hasn't been updated in ${updateAgeDays.toFixed(0)} days`);
|
||||
result.score -= 10;
|
||||
}
|
||||
}
|
||||
|
||||
if (skillInfo.owner?.handle) {
|
||||
const authorResult = runClawhub(["search", skillInfo.owner.handle]);
|
||||
if (authorResult.status === 0) {
|
||||
const lines = authorResult.stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((line) => line);
|
||||
const skillCount = Math.max(0, lines.length - 1);
|
||||
|
||||
if (skillCount === 1) {
|
||||
result.warnings.push(`Author "${skillInfo.owner.handle}" has only 1 published skill`);
|
||||
result.score -= 10;
|
||||
} else if (skillCount > 1 && skillCount < 3) {
|
||||
result.warnings.push(
|
||||
`Author "${skillInfo.owner.handle}" has only ${skillCount} published skills`,
|
||||
);
|
||||
|
||||
if (authorResult.status === 0) {
|
||||
const lines = authorResult.stdout.trim().split('\n').filter(l => l);
|
||||
const skillCount = lines.length - 1; // First line is header
|
||||
|
||||
if (skillCount === 1) {
|
||||
result.warnings.push(`Author "${skillInfo.owner.handle}" has only 1 published skill`);
|
||||
result.score -= 10;
|
||||
} else if (skillCount < 3) {
|
||||
result.warnings.push(`Author "${skillInfo.owner.handle}" has only ${skillCount} published skills`);
|
||||
result.score -= 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check 5: Download statistics
|
||||
if (skillInfo.skill?.stats?.downloads !== undefined) {
|
||||
const downloads = skillInfo.skill.stats.downloads;
|
||||
if (downloads < 10) {
|
||||
result.warnings.push(`Low download count: ${downloads}`);
|
||||
result.score -= 10;
|
||||
} else if (downloads < 100) {
|
||||
result.warnings.push(`Moderate download count: ${downloads}`);
|
||||
result.score -= 5;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (parseError) {
|
||||
result.warnings.push(`Failed to parse skill information: ${parseError.message}`);
|
||||
result.score = Math.min(result.score, 60);
|
||||
}
|
||||
}
|
||||
|
||||
// Check 6: Try installation to detect VirusTotal Code Insight warnings
|
||||
// Note: This approach has potential side effects:
|
||||
// - May download/cache skill metadata before declining
|
||||
// - Depends on clawhub's prompting behavior (sending "n\n" to decline)
|
||||
// - If clawhub inspect provided security flags, we'd use that instead
|
||||
// This is the only way to programmatically access VirusTotal warnings currently
|
||||
const installArgs = ["install", skillSlug];
|
||||
if (version) installArgs.push("--version", version);
|
||||
const installCheck = spawnSync("clawhub", installArgs, {
|
||||
input: "n\n", // Automatically decline the installation prompt
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
const output = (installCheck.stdout || "") + (installCheck.stderr || "");
|
||||
if (output.includes("suspicious") || output.includes("VirusTotal") || output.includes("flagged")) {
|
||||
result.virustotal.push("Flagged by ClawHub's VirusTotal Code Insight");
|
||||
result.score -= 40; // More severe penalty for VirusTotal flag
|
||||
|
||||
// Extract specific warnings
|
||||
const lines = output.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.includes("Warning:") || line.includes("risky patterns") ||
|
||||
line.includes("crypto keys") || line.includes("external APIs") ||
|
||||
line.includes("eval") || line.includes("VirusTotal Code Insight")) {
|
||||
const cleanLine = line.trim().replace(/^⚠️\s*/, '').replace(/^\s*Warning:\s*/, '');
|
||||
if (cleanLine && !result.virustotal.includes(cleanLine)) {
|
||||
result.virustotal.push(cleanLine);
|
||||
}
|
||||
result.score -= 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check 7: If version specified, check if it exists
|
||||
if (version) {
|
||||
const versionCheck = spawnSync(
|
||||
"clawhub",
|
||||
["inspect", skillSlug, "--version", version, "--json"],
|
||||
{ encoding: "utf-8" }
|
||||
);
|
||||
|
||||
if (versionCheck.status !== 0) {
|
||||
result.warnings.push(`Version ${version} not found for skill ${skillSlug}`);
|
||||
result.score -= 20;
|
||||
if (skillInfo.skill?.stats?.downloads !== undefined) {
|
||||
const downloads = skillInfo.skill.stats.downloads;
|
||||
if (downloads < 10) {
|
||||
result.warnings.push(`Low download count: ${downloads}`);
|
||||
result.score -= 10;
|
||||
} else if (downloads < 100) {
|
||||
result.warnings.push(`Moderate download count: ${downloads}`);
|
||||
result.score -= 5;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure score is within bounds
|
||||
result.score = Math.max(0, Math.min(100, result.score));
|
||||
result.safe = result.score >= threshold;
|
||||
|
||||
// Add summary warning if below threshold
|
||||
if (!result.safe) {
|
||||
result.warnings.unshift(`Reputation score ${result.score}/100 below threshold ${threshold}/100`);
|
||||
let versionDetails = skillInfo.version ?? null;
|
||||
if (!versionDetails && !version && skillInfo.latestVersion?.version) {
|
||||
const latestVersionCheck = runClawhub([
|
||||
"inspect",
|
||||
skillSlug,
|
||||
"--version",
|
||||
String(skillInfo.latestVersion.version),
|
||||
"--json",
|
||||
]);
|
||||
if (latestVersionCheck.status === 0) {
|
||||
const latestInfo = parseJson(
|
||||
latestVersionCheck.stdout,
|
||||
"latest-version inspection payload",
|
||||
result.warnings,
|
||||
);
|
||||
versionDetails = latestInfo?.version ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
maybeApplyVersionSecuritySignals(result, versionDetails);
|
||||
return finalizeResult(result, threshold);
|
||||
} catch (error) {
|
||||
result.warnings.push(`Reputation check error: ${error.message}`);
|
||||
result.warnings.push(`Reputation check error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
result.score = 50;
|
||||
result.safe = result.score >= threshold;
|
||||
result.blocked = true;
|
||||
return finalizeResult(result, threshold);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// CLI interface for direct usage
|
||||
const isCliEntrypoint =
|
||||
process.argv[1] !== undefined &&
|
||||
import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
|
||||
@@ -195,29 +249,33 @@ if (isCliEntrypoint) {
|
||||
console.error("Usage: node check_clawhub_reputation.mjs <skill-slug> [version] [threshold]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
const skillSlug = args[0];
|
||||
const version = args[1] || "";
|
||||
let threshold = 70;
|
||||
|
||||
if (args[2] !== undefined) {
|
||||
const parsedThreshold = parseInt(args[2], 10);
|
||||
if (!Number.isInteger(parsedThreshold) || parsedThreshold < 0 || parsedThreshold > 100) {
|
||||
console.error(
|
||||
`Invalid threshold: "${args[2]}". Threshold must be an integer between 0 and 100.`
|
||||
`Invalid threshold: "${args[2]}". Threshold must be an integer between 0 and 100.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
threshold = parsedThreshold;
|
||||
}
|
||||
|
||||
|
||||
const result = await checkClawhubReputation(skillSlug, version, threshold);
|
||||
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
|
||||
|
||||
if (!result.safe) {
|
||||
process.exit(43);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { spawnSync as runProcessSync } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
@@ -146,7 +146,7 @@ async function runOriginalGuardedInstall(args) {
|
||||
|
||||
// Pass through environment without modification
|
||||
// The original guarded_skill_install.mjs handles --confirm-advisory properly
|
||||
const child = spawnSync(
|
||||
const child = runProcessSync(
|
||||
"node",
|
||||
[originalScript, ...args.originalArgs],
|
||||
{
|
||||
|
||||
@@ -1,173 +1,60 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
function printPreflightSummary({ suiteDir, checkerDir, hookLibDir }) {
|
||||
function printUsage() {
|
||||
console.log([
|
||||
"Usage:",
|
||||
" node scripts/setup_reputation_hook.mjs",
|
||||
"",
|
||||
"This helper no longer mutates installed clawsec-suite files.",
|
||||
"It validates local prerequisites and prints the standalone checker command.",
|
||||
"",
|
||||
].join("\n"));
|
||||
}
|
||||
|
||||
function printSummary({ suiteDir, checkerDir, enhancedInstaller }) {
|
||||
const lines = [
|
||||
"Preflight review:",
|
||||
`- This setup will rewrite installed clawsec-suite integration files under ${suiteDir}.`,
|
||||
`- It copies reputation helpers from ${checkerDir} and applies a string-based patch to handler.ts in ${hookLibDir}.`,
|
||||
"- Required runtime for the integrated flow: node, clawhub, openclaw.",
|
||||
"- After setup, reputation checks query ClawHub and may trigger remote metadata lookups; risky installs remain approval-gated with --confirm-reputation.",
|
||||
"- Restart OpenClaw gateway for hook changes to take effect.",
|
||||
"- This setup does not rewrite files in other skills.",
|
||||
`- It validates expected install paths: ${suiteDir} and ${checkerDir}.`,
|
||||
"- Required runtime for reputation checks: node + clawhub.",
|
||||
"- Advisory-hook reputation annotations are manual only in this release.",
|
||||
"- If you want hook alert annotations, wire checker lib/reputation.mjs into suite handler.ts yourself.",
|
||||
"- Reputation scoring is heuristic and must remain confirmation-gated.",
|
||||
"",
|
||||
"Recommended command:",
|
||||
` node ${enhancedInstaller} --skill <slug> [--version <semver>]`,
|
||||
"",
|
||||
"Optional shell alias (manual, not applied automatically):",
|
||||
` alias clawsec-guarded-install='node ${enhancedInstaller}'`,
|
||||
];
|
||||
|
||||
console.log(lines.join("\n") + "\n");
|
||||
console.log(lines.join("\n"));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("Setting up ClawHub reputation checker integration...");
|
||||
|
||||
// Paths
|
||||
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
||||
printUsage();
|
||||
return;
|
||||
}
|
||||
|
||||
const suiteDir = path.join(os.homedir(), ".openclaw", "skills", "clawsec-suite");
|
||||
const checkerDir = path.join(os.homedir(), ".openclaw", "skills", "clawsec-clawhub-checker");
|
||||
const hookLibDir = path.join(suiteDir, "hooks", "clawsec-advisory-guardian", "lib");
|
||||
const suiteScriptsDir = path.join(suiteDir, "scripts");
|
||||
const enhancedInstaller = path.join(checkerDir, "scripts", "enhanced_guarded_install.mjs");
|
||||
const suiteGuardedInstaller = path.join(suiteDir, "scripts", "guarded_skill_install.mjs");
|
||||
|
||||
printPreflightSummary({ suiteDir, checkerDir, hookLibDir });
|
||||
|
||||
try {
|
||||
// Check if clawsec-suite is installed
|
||||
await fs.access(suiteDir);
|
||||
console.log(`✓ Found clawsec-suite at ${suiteDir}`);
|
||||
|
||||
// Check if hook lib directory exists
|
||||
await fs.access(hookLibDir);
|
||||
console.log(`✓ Found advisory guardian hook at ${hookLibDir}`);
|
||||
|
||||
// Copy reputation module to hook lib
|
||||
const reputationModuleSrc = path.join(checkerDir, "hooks", "clawsec-advisory-guardian", "lib", "reputation.mjs");
|
||||
const reputationModuleDst = path.join(hookLibDir, "reputation.mjs");
|
||||
|
||||
await fs.copyFile(reputationModuleSrc, reputationModuleDst);
|
||||
console.log(`✓ Copied reputation module to ${reputationModuleDst}`);
|
||||
|
||||
// Update hook handler to import reputation module
|
||||
const hookHandlerPath = path.join(suiteDir, "hooks", "clawsec-advisory-guardian", "handler.ts");
|
||||
let handlerContent = await fs.readFile(hookHandlerPath, "utf8");
|
||||
await fs.access(checkerDir);
|
||||
await fs.access(enhancedInstaller);
|
||||
await fs.access(suiteDir);
|
||||
await fs.access(suiteGuardedInstaller);
|
||||
|
||||
// WARNING: This setup script uses string manipulation to modify handler.ts
|
||||
// This is fragile and may break if the handler structure changes
|
||||
// Consider using AST-based transformation or manual integration for production use
|
||||
let handlerChanged = false;
|
||||
const importLine = "import { checkReputation } from \"./lib/reputation.mjs\";";
|
||||
const reputationMarker = "// ClawHub reputation check for matched skills";
|
||||
|
||||
if (!handlerContent.includes(importLine)) {
|
||||
// Add import after other imports
|
||||
const importIndex = handlerContent.lastIndexOf("import");
|
||||
if (importIndex === -1) {
|
||||
throw new Error("Could not find import statements in handler.ts. Manual integration required.");
|
||||
}
|
||||
|
||||
const lineEndIndex = handlerContent.indexOf("\n", importIndex);
|
||||
handlerContent = handlerContent.slice(0, lineEndIndex + 1) + `${importLine}\n` + handlerContent.slice(lineEndIndex + 1);
|
||||
handlerChanged = true;
|
||||
} else {
|
||||
console.log("✓ Hook handler already imports reputation module");
|
||||
}
|
||||
|
||||
if (!handlerContent.includes(reputationMarker)) {
|
||||
const findMatchesAnchors = [
|
||||
{ line: "const allMatches = findMatches(feed, installedSkills);", variable: "allMatches" },
|
||||
{ line: "const matches = findMatches(feed, installedSkills);", variable: "matches" },
|
||||
];
|
||||
const matchedAnchor = findMatchesAnchors.find((entry) => handlerContent.includes(entry.line));
|
||||
|
||||
if (!matchedAnchor) {
|
||||
throw new Error(
|
||||
"Could not find findMatches assignment in handler.ts. Refusing partial setup. Manual integration required."
|
||||
);
|
||||
}
|
||||
|
||||
const anchorIndex = handlerContent.indexOf(matchedAnchor.line);
|
||||
const insertIndex = handlerContent.indexOf("\n", anchorIndex) + 1;
|
||||
const reputationCheckCode = `
|
||||
${reputationMarker}
|
||||
for (const match of ${matchedAnchor.variable}) {
|
||||
const repResult = await checkReputation(match.skill.name, match.skill.version);
|
||||
if (!repResult.safe) {
|
||||
match.reputationWarning = true;
|
||||
match.reputationScore = repResult.score;
|
||||
match.reputationWarnings = repResult.warnings;
|
||||
}
|
||||
}
|
||||
`;
|
||||
handlerContent = handlerContent.slice(0, insertIndex) + reputationCheckCode + handlerContent.slice(insertIndex);
|
||||
handlerChanged = true;
|
||||
} else {
|
||||
console.log("✓ Hook handler already has reputation scan block");
|
||||
}
|
||||
|
||||
if (handlerChanged) {
|
||||
await fs.writeFile(hookHandlerPath, handlerContent);
|
||||
console.log("✓ Updated hook handler with reputation checks");
|
||||
} else {
|
||||
console.log("✓ Hook handler already has required reputation integration");
|
||||
}
|
||||
|
||||
// Copy enhanced installer and reputation checker scripts
|
||||
const enhancedInstallerSrc = path.join(checkerDir, "scripts", "enhanced_guarded_install.mjs");
|
||||
const enhancedInstallerDst = path.join(suiteDir, "scripts", "enhanced_guarded_install.mjs");
|
||||
const reputationCheckSrc = path.join(checkerDir, "scripts", "check_clawhub_reputation.mjs");
|
||||
const reputationCheckDst = path.join(suiteScriptsDir, "check_clawhub_reputation.mjs");
|
||||
|
||||
await fs.copyFile(enhancedInstallerSrc, enhancedInstallerDst);
|
||||
console.log(`✓ Installed enhanced guarded installer at ${enhancedInstallerDst}`);
|
||||
|
||||
await fs.copyFile(reputationCheckSrc, reputationCheckDst);
|
||||
console.log(`✓ Installed reputation check script at ${reputationCheckDst}`);
|
||||
|
||||
// Create wrapper script that uses enhanced installer by default
|
||||
const wrapperScript = `#!/usr/bin/env node
|
||||
|
||||
// Wrapper that uses enhanced guarded installer with reputation checks
|
||||
// This replaces the original guarded_skill_install.mjs in usage
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "node:path";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const enhancedScript = path.join(__dirname, "enhanced_guarded_install.mjs");
|
||||
|
||||
const result = spawnSync("node", [enhancedScript, ...process.argv.slice(2)], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
process.exit(result.status ?? 1);
|
||||
`;
|
||||
|
||||
const wrapperPath = path.join(suiteDir, "scripts", "guarded_skill_install_wrapper.mjs");
|
||||
await fs.writeFile(wrapperPath, wrapperScript);
|
||||
await fs.chmod(wrapperPath, 0o755);
|
||||
console.log(`✓ Created wrapper script at ${wrapperPath}`);
|
||||
|
||||
console.log("\n" + "=".repeat(80));
|
||||
console.log("SETUP COMPLETE");
|
||||
console.log("=".repeat(80));
|
||||
console.log("\nThe ClawHub reputation checker has been integrated with clawsec-suite.");
|
||||
console.log("\nWhat changed:");
|
||||
console.log("1. Enhanced guarded installer with reputation checks installed");
|
||||
console.log("2. Reputation check helper script installed");
|
||||
console.log("3. Advisory guardian hook updated to include reputation warnings");
|
||||
console.log("4. Wrapper script created for backward compatibility");
|
||||
console.log("\nUsage:");
|
||||
console.log(" node scripts/enhanced_guarded_install.mjs --skill <name> [--version <ver>]");
|
||||
console.log(" node scripts/guarded_skill_install_wrapper.mjs --skill <name> [--version <ver>]");
|
||||
console.log("\nNew exit code: 43 = Reputation warning (requires --confirm-reputation)");
|
||||
console.log("\nRestart OpenClaw gateway for hook changes to take effect.");
|
||||
console.log("=".repeat(80));
|
||||
|
||||
} catch (error) {
|
||||
console.error("Setup failed:", error.message);
|
||||
console.error("\nMake sure:");
|
||||
console.error("1. clawsec-suite is installed (npx clawhub install clawsec-suite)");
|
||||
console.error("2. You have write permissions to the suite directory");
|
||||
process.exit(1);
|
||||
}
|
||||
printSummary({ suiteDir, checkerDir, enhancedInstaller });
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
main().catch((error) => {
|
||||
console.error(`Setup failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "clawsec-clawhub-checker",
|
||||
"version": "0.0.2",
|
||||
"description": "ClawHub reputation checker for ClawSec suite. Enhances guarded skill installer with VirusTotal Code Insight reputation scores and additional safety checks.",
|
||||
"version": "0.0.3",
|
||||
"description": "ClawHub reputation checker for clawsec-suite. Adds a standalone reputation gate before guarded skill installation.",
|
||||
"author": "abutbul",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"homepage": "https://clawsec.prompt.security/",
|
||||
@@ -36,12 +36,12 @@
|
||||
{
|
||||
"path": "scripts/setup_reputation_hook.mjs",
|
||||
"required": true,
|
||||
"description": "Setup script to enhance existing advisory guardian hook"
|
||||
"description": "Non-mutating preflight helper that validates paths and prints recommended commands"
|
||||
},
|
||||
{
|
||||
"path": "hooks/clawsec-advisory-guardian/lib/reputation.mjs",
|
||||
"required": true,
|
||||
"description": "Reputation checking module for advisory guardian hook"
|
||||
"required": false,
|
||||
"description": "Optional reputation module for advisory guardian integrations"
|
||||
},
|
||||
{
|
||||
"path": "README.md",
|
||||
@@ -61,7 +61,7 @@
|
||||
{
|
||||
"path": "test/setup_reputation_hook.test.mjs",
|
||||
"required": false,
|
||||
"description": "Regression coverage for setup preflight disclosure"
|
||||
"description": "Regression coverage for setup preflight behavior"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -71,8 +71,8 @@
|
||||
"integration": {
|
||||
"clawsec-suite": {
|
||||
"enhances": [
|
||||
"guarded_skill_install.mjs",
|
||||
"clawsec-advisory-guardian hook"
|
||||
"guarded_skill_install.mjs via external wrapper invocation",
|
||||
"optional manual advisory-guardian hook wiring for reputation annotations"
|
||||
],
|
||||
"adds_exit_codes": {
|
||||
"43": "Reputation warning - requires --confirm-reputation"
|
||||
@@ -87,7 +87,11 @@
|
||||
"emoji": "🛡️",
|
||||
"category": "security",
|
||||
"requires": {
|
||||
"bins": ["node", "clawhub", "openclaw"]
|
||||
"bins": [
|
||||
"node",
|
||||
"clawhub",
|
||||
"openclaw"
|
||||
]
|
||||
},
|
||||
"runtime": {
|
||||
"required_env": [],
|
||||
@@ -97,13 +101,14 @@
|
||||
},
|
||||
"execution": {
|
||||
"always": false,
|
||||
"persistence": "The setup script rewrites installed clawsec-suite integration files and augments the advisory guardian hook until removed or replaced.",
|
||||
"network_egress": "Reputation checks query ClawHub metadata and may trigger ClawHub install/inspect flows that contact remote services."
|
||||
"persistence": "No automatic persistence; setup helper performs validation only and does not rewrite other skills.",
|
||||
"network_egress": "Reputation checks query ClawHub inspect/search endpoints for metadata and scanner summaries."
|
||||
},
|
||||
"operator_review": [
|
||||
"Requires an installed clawsec-suite checkout because setup rewrites handler.ts and copies helper scripts into the suite.",
|
||||
"Requires an installed clawsec-suite checkout because the enhanced installer delegates to suite guarded install flow.",
|
||||
"This release does not auto-wire advisory-guardian hook annotations; if needed, wire hooks/clawsec-advisory-guardian/lib/reputation.mjs manually into the suite hook.",
|
||||
"Reputation results are heuristic and can produce false positives; installation still requires explicit user confirmation for risky skills.",
|
||||
"Review the modified suite files and restart OpenClaw gateway after setup so the hook changes load intentionally."
|
||||
"Run the setup helper to confirm local paths before using the enhanced installer command."
|
||||
],
|
||||
"triggers": [
|
||||
"clawhub reputation",
|
||||
|
||||
@@ -43,8 +43,8 @@ async function stageInstalledSkill(tempHome, skillName) {
|
||||
return destDir;
|
||||
}
|
||||
|
||||
async function testPreflightSummaryAndMutation() {
|
||||
const testName = "setup_reputation_hook: prints preflight review before mutating installed suite files";
|
||||
async function testPreflightSummaryNoMutation() {
|
||||
const testName = "setup_reputation_hook: prints preflight review without mutating installed suite files";
|
||||
const tmp = await createTempDir();
|
||||
const homeDir = path.join(tmp.path, "home");
|
||||
|
||||
@@ -80,15 +80,22 @@ async function testPreflightSummaryAndMutation() {
|
||||
"lib",
|
||||
"reputation.mjs",
|
||||
);
|
||||
|
||||
await fs.access(wrapperPath);
|
||||
await fs.access(reputationModulePath);
|
||||
const wrapperExists = await fs
|
||||
.access(wrapperPath)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
const reputationModuleExists = await fs
|
||||
.access(reputationModulePath)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
if (
|
||||
result.stdout.includes("Preflight review:") &&
|
||||
result.stdout.includes("rewrite installed clawsec-suite integration files") &&
|
||||
result.stdout.includes("string-based patch to handler.ts") &&
|
||||
result.stdout.includes("Restart OpenClaw gateway for hook changes to take effect")
|
||||
result.stdout.includes("does not rewrite files in other skills") &&
|
||||
result.stdout.includes("Recommended command:") &&
|
||||
result.stdout.includes("alias clawsec-guarded-install") &&
|
||||
wrapperExists === false &&
|
||||
reputationModuleExists === false
|
||||
) {
|
||||
pass(testName);
|
||||
} else {
|
||||
@@ -102,7 +109,7 @@ async function testPreflightSummaryAndMutation() {
|
||||
}
|
||||
|
||||
async function runAllTests() {
|
||||
await testPreflightSummaryAndMutation();
|
||||
await testPreflightSummaryNoMutation();
|
||||
report();
|
||||
exitWithResults();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user