Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions skills/clawsec-clawhub-checker/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## [0.0.5] - 2026-06-07

### Security
- Treat explicit malicious ClawHub and VirusTotal verdicts as blocking signals regardless of the numeric reputation score.

## [0.0.4] - 2026-05-13

### Security
Expand Down
2 changes: 1 addition & 1 deletion skills/clawsec-clawhub-checker/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: clawsec-clawhub-checker
version: 0.0.4
version: 0.0.5
description: ClawHub reputation checker for clawsec-suite. Adds a standalone reputation gate before guarded skill installation.
homepage: https://clawsec.prompt.security
clawdis:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ function blockOnMissingScannerData(result, warning) {
result.blocked = true;
}

function blockOnMaliciousScannerData(result, warning) {
result.warnings.push(warning);
result.score = 0;
result.blocked = true;
}

function parseJson(raw, label, warnings) {
try {
return JSON.parse(raw);
Expand All @@ -58,7 +64,10 @@ function maybeApplyVersionSecuritySignals(result, versionDetails) {
return;
}

if (typeof security.status === "string" && security.status.toLowerCase() === "suspicious") {
const securityStatus = typeof security.status === "string" ? security.status.toLowerCase() : "";
if (securityStatus === "malicious") {
blockOnMaliciousScannerData(result, "ClawHub static moderation marked the version as malicious");
} else if (securityStatus === "suspicious") {
result.warnings.push("ClawHub static moderation marked the version as suspicious");
result.score -= 30;
}
Expand All @@ -82,7 +91,15 @@ function maybeApplyVersionSecuritySignals(result, versionDetails) {
"";
const normalizedStatus = vtStatus.toLowerCase();

if (normalizedStatus === "suspicious") {
if (normalizedStatus === "malicious") {
result.virustotal.push("ClawHub VirusTotal scan returned malicious");
blockOnMaliciousScannerData(result, "ClawHub VirusTotal scan returned malicious");

const vtSummary = typeof vt.analysis === "string" ? vt.analysis.trim() : "";
if (vtSummary) {
result.virustotal.push(vtSummary.split("\n")[0]);
}
} else if (normalizedStatus === "suspicious") {
result.virustotal.push("ClawHub VirusTotal scan returned suspicious");
result.score -= 40;

Expand Down
2 changes: 1 addition & 1 deletion skills/clawsec-clawhub-checker/skill.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "clawsec-clawhub-checker",
"version": "0.0.4",
"version": "0.0.5",
"description": "ClawHub reputation checker for clawsec-suite. Adds a standalone reputation gate before guarded skill installation.",
"author": "abutbul",
"license": "AGPL-3.0-or-later",
Expand Down
87 changes: 87 additions & 0 deletions skills/clawsec-clawhub-checker/test/reputation_check.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
*/

import { fileURLToPath } from "node:url";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { spawn } from "node:child_process";

Expand Down Expand Up @@ -58,6 +60,37 @@ function runScript(scriptPath, args, env) {
});
}

async function createMockClawhub(payload) {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawhub-reputation-test-"));
const binDir = path.join(tmpDir, "bin");
const mockPath = path.join(binDir, "clawhub");
await fs.mkdir(binDir, { recursive: true });
await fs.writeFile(
mockPath,
`#!/usr/bin/env node
const payload = ${JSON.stringify(JSON.stringify(payload))};
const command = process.argv[2] || "";
if (command === "inspect") {
process.stdout.write(payload);
process.exit(0);
}
if (command === "search") {
process.stdout.write("name\\nmock-skill\\nother-skill\\n");
process.exit(0);
}
process.stderr.write("unexpected clawhub command: " + process.argv.slice(2).join(" ") + "\\n");
process.exit(2);
`,
"utf8",
);
await fs.chmod(mockPath, 0o755);

return {
env: { PATH: `${binDir}:${process.env.PATH}` },
cleanup: async () => fs.rm(tmpDir, { recursive: true, force: true }),
};
}

// -----------------------------------------------------------------------------
// Test: Invalid skill slug is rejected (command injection prevention)
// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -208,6 +241,59 @@ async function testPreReleaseVersionAccepted() {
}
}

// -----------------------------------------------------------------------------
// Test: Explicit malicious scanner verdict blocks regardless of score
// -----------------------------------------------------------------------------
async function testMaliciousVirusTotalVerdictBlocks() {
const testName = "reputation_check: malicious VirusTotal verdict blocks install";
const now = Date.now();
const mock = await createMockClawhub({
skill: {
createdAt: now - (120 * 24 * 60 * 60 * 1000),
updatedAt: now - (2 * 24 * 60 * 60 * 1000),
stats: { downloads: 1000 },
},
owner: { handle: "trusted-publisher" },
version: {
security: {
status: "clean",
scanners: {
vt: {
normalizedStatus: "malicious",
analysis: "malicious verdict from scanner",
},
},
},
},
});

try {
const result = await runScript(CHECKER_SCRIPT, ['malicious-skill', '1.0.0', '70'], mock.env);
let parsed;
try {
parsed = JSON.parse(result.stdout);
} catch {
fail(testName, `Could not parse output: ${result.stdout}`);
return;
}

if (
result.code === 43 &&
parsed.safe === false &&
parsed.warnings.some((w) => w.toLowerCase().includes("malicious")) &&
parsed.virustotal.some((v) => v.toLowerCase().includes("malicious"))
) {
pass(testName);
} else {
fail(testName, `Expected malicious verdict to block, got code ${result.code}: ${JSON.stringify(parsed)}`);
}
} catch (error) {
fail(testName, error);
} finally {
await mock.cleanup();
}
}

// -----------------------------------------------------------------------------
// Test: CLI entrypoint guard works when script path is relative
// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -411,6 +497,7 @@ async function runTests() {
await testUppercaseSlugRejected();
await testEmptySlugShowsUsage();
await testPreReleaseVersionAccepted();
await testMaliciousVirusTotalVerdictBlocks();
await testRelativePathCliEntrypointWorks();
await testInvalidThresholdRejected();
await testEnhancedInstallerRejectsInvalidSkill();
Expand Down
6 changes: 6 additions & 0 deletions skills/clawsec-nanoclaw/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [0.0.7] - 2026-06-07

### Security
- Added comparator range support for NanoClaw advisory matching and fail-closed handling for malformed affected specifiers.
- Added strict integrity IPC request ID validation and result path containment before host-side result writes.

## [0.0.6] - 2026-05-24

### Changed
Expand Down
2 changes: 1 addition & 1 deletion skills/clawsec-nanoclaw/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: clawsec-nanoclaw
version: 0.0.6
version: 0.0.7
description: Use when checking for security vulnerabilities in NanoClaw skills, before installing new skills, or when asked about security advisories affecting the bot
---

Expand Down
68 changes: 50 additions & 18 deletions skills/clawsec-nanoclaw/host-services/integrity-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import fs from 'fs';
import path from 'path';
import { IntegrityMonitor } from '../guardian/integrity-monitor';

const RESULT_DIR = '/workspace/ipc/clawsec_results';
const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;

// ============================================================================
// Integrity Service (Singleton)
// ============================================================================
Expand Down Expand Up @@ -84,15 +87,21 @@ export async function handleIntegrityIpc(
logger: any
): Promise<void> {
const { type, requestId, groupFolder: _groupFolder } = task;
const validatedRequestId = validateRequestId(requestId);

if (!validatedRequestId) {
logger.warn({ type, requestId }, 'Invalid integrity IPC request id');
return;
}

const safeTask = { ...task, requestId: validatedRequestId };

if (!deps.integrityService) {
logger.warn({ task }, 'IntegrityService not available');
if (requestId) {
writeResult(requestId, {
success: false,
error: 'IntegrityService not initialized'
});
}
writeResult(validatedRequestId, {
success: false,
error: 'IntegrityService not initialized'
});
return;
}

Expand All @@ -103,31 +112,29 @@ export async function handleIntegrityIpc(
await service.initialize();
} catch (error) {
logger.error({ error }, 'Failed to initialize IntegrityService');
if (requestId) {
writeResult(requestId, {
success: false,
error: `Initialization failed: ${error instanceof Error ? error.message : String(error)}`
});
}
writeResult(validatedRequestId, {
success: false,
error: `Initialization failed: ${error instanceof Error ? error.message : String(error)}`
});
return;
}
}

switch (type) {
case 'integrity_check':
await handleIntegrityCheck(task, service, logger);
await handleIntegrityCheck(safeTask, service, logger);
break;

case 'integrity_approve':
await handleIntegrityApprove(task, service, logger);
await handleIntegrityApprove(safeTask, service, logger);
break;

case 'integrity_status':
await handleIntegrityStatus(task, service, logger);
await handleIntegrityStatus(safeTask, service, logger);
break;

case 'integrity_verify_audit':
await handleIntegrityVerifyAudit(task, service, logger);
await handleIntegrityVerifyAudit(safeTask, service, logger);
break;

default:
Expand Down Expand Up @@ -280,15 +287,40 @@ async function handleIntegrityVerifyAudit(
// Helper Functions
// ============================================================================

function validateRequestId(requestId: unknown): string | null {
if (typeof requestId !== 'string') return null;
const normalized = requestId.trim();
if (!REQUEST_ID_PATTERN.test(normalized)) return null;
return normalized;
}

function resolveResultPath(requestId: string): string {
const safeRequestId = validateRequestId(requestId);
if (!safeRequestId) {
throw new Error('Invalid integrity IPC request id');
}

const resultDir = RESULT_DIR;
const normalizedResultDir = path.resolve(resultDir);
const resultPath = path.resolve(normalizedResultDir, `${safeRequestId}.json`);
const relativePath = path.relative(normalizedResultDir, resultPath);

if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
throw new Error('Integrity IPC result path escapes result directory');
}

return resultPath;
}

function writeResult(requestId: string, result: any): void {
const resultDir = '/workspace/ipc/clawsec_results';
const resultPath = resolveResultPath(requestId);
const resultDir = path.dirname(resultPath);

// Ensure directory exists
if (!fs.existsSync(resultDir)) {
fs.mkdirSync(resultDir, { recursive: true });
}

const resultPath = path.join(resultDir, `${requestId}.json`);
fs.writeFileSync(resultPath, JSON.stringify(result, null, 2));
}

Expand Down
Loading
Loading