openclaw-plugin-vt-sentinel 0.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/dist/cache.d.ts +23 -0
- package/dist/cache.js +61 -0
- package/dist/classifier.d.ts +56 -0
- package/dist/classifier.js +410 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +832 -0
- package/dist/path-extractor.d.ts +76 -0
- package/dist/path-extractor.js +761 -0
- package/dist/scanner.d.ts +96 -0
- package/dist/scanner.js +312 -0
- package/dist/vt-api.d.ts +69 -0
- package/dist/vt-api.js +231 -0
- package/hooks/vt-auto-scan/HOOK.md +25 -0
- package/hooks/vt-auto-scan/handler.js +106 -0
- package/openclaw.plugin.json +43 -0
- package/package.json +44 -0
- package/skills/vt-sentinel/SKILL.md +129 -0
package/dist/vt-api.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.VTApiClient = void 0;
|
|
40
|
+
exports.getAgentCredentialsPath = getAgentCredentialsPath;
|
|
41
|
+
exports.loadAgentCredentials = loadAgentCredentials;
|
|
42
|
+
exports.saveAgentCredentials = saveAgentCredentials;
|
|
43
|
+
exports.registerAgent = registerAgent;
|
|
44
|
+
exports.calculateSHA256 = calculateSHA256;
|
|
45
|
+
const axios_1 = __importDefault(require("axios"));
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const crypto = __importStar(require("crypto"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const os = __importStar(require("os"));
|
|
50
|
+
const form_data_1 = __importDefault(require("form-data"));
|
|
51
|
+
const VT_API_URL = 'https://www.virustotal.com/api/v3';
|
|
52
|
+
const VTAI_API_URL = 'https://ai.virustotal.com/api/v3';
|
|
53
|
+
const HTTP_TIMEOUT_MS = 30000; // 30s timeout for all API calls
|
|
54
|
+
/**
|
|
55
|
+
* Path to the cached agent credentials file.
|
|
56
|
+
* Stored in the OpenClaw state directory for persistence across sessions.
|
|
57
|
+
*/
|
|
58
|
+
function getAgentCredentialsPath() {
|
|
59
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR || path.join(os.homedir(), '.openclaw');
|
|
60
|
+
return path.join(stateDir, 'vt-sentinel-agent.json');
|
|
61
|
+
}
|
|
62
|
+
function loadAgentCredentials() {
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(fs.readFileSync(getAgentCredentialsPath(), 'utf-8'));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function saveAgentCredentials(creds) {
|
|
71
|
+
const credsPath = getAgentCredentialsPath();
|
|
72
|
+
const dir = path.dirname(credsPath);
|
|
73
|
+
if (!fs.existsSync(dir))
|
|
74
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
75
|
+
fs.writeFileSync(credsPath, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
76
|
+
// Windows: POSIX mode bits are ignored on NTFS. Use icacls to restrict access.
|
|
77
|
+
if (process.platform === 'win32') {
|
|
78
|
+
try {
|
|
79
|
+
const { execSync } = require('child_process');
|
|
80
|
+
execSync(`icacls "${credsPath}" /inheritance:r /grant:r "%USERNAME%:(F)"`, { stdio: 'ignore' });
|
|
81
|
+
}
|
|
82
|
+
catch { /* best effort — may fail without admin */ }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Register a new agent with VirusTotal AI API.
|
|
87
|
+
* No authentication required — zero-friction onboarding.
|
|
88
|
+
*/
|
|
89
|
+
async function registerAgent(version = '0.2.0') {
|
|
90
|
+
const resp = await axios_1.default.post(`${VTAI_API_URL}/agents/register`, {
|
|
91
|
+
agent_family: 'vt-sentinel',
|
|
92
|
+
agent_version: version,
|
|
93
|
+
display_name: 'VT Sentinel',
|
|
94
|
+
}, { timeout: HTTP_TIMEOUT_MS });
|
|
95
|
+
return {
|
|
96
|
+
agentId: resp.data.agent_id,
|
|
97
|
+
agentToken: resp.data.agent_token,
|
|
98
|
+
publicHandle: resp.data.public_handle,
|
|
99
|
+
registeredAt: new Date().toISOString(),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// --- Helpers ---
|
|
103
|
+
function calculateSHA256(filePath) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const hash = crypto.createHash('sha256');
|
|
106
|
+
const stream = fs.createReadStream(filePath);
|
|
107
|
+
stream.on('data', (data) => hash.update(data));
|
|
108
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
109
|
+
stream.on('error', reject);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// --- VT API Client (dual-mode: standard VT or VTAI) ---
|
|
113
|
+
class VTApiClient {
|
|
114
|
+
constructor(apiKey, useVtai = false) {
|
|
115
|
+
this.apiKey = apiKey;
|
|
116
|
+
this.vtai = useVtai;
|
|
117
|
+
this.baseUrl = useVtai ? VTAI_API_URL : VT_API_URL;
|
|
118
|
+
}
|
|
119
|
+
headers() {
|
|
120
|
+
return { 'x-apikey': this.apiKey };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Lookup a file hash in VirusTotal.
|
|
124
|
+
* Returns full report including AI results if available, or null if not found.
|
|
125
|
+
* Handles both standard VT and VTAI response formats.
|
|
126
|
+
*/
|
|
127
|
+
async checkHash(hash) {
|
|
128
|
+
if (!/^[a-fA-F0-9]{32,128}$/.test(hash)) {
|
|
129
|
+
throw new Error(`Invalid hash: expected 32-128 hex characters, got "${hash.substring(0, 20)}${hash.length > 20 ? '...' : ''}"`);
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const resp = await axios_1.default.get(`${this.baseUrl}/files/${hash}`, {
|
|
133
|
+
headers: this.headers(),
|
|
134
|
+
timeout: HTTP_TIMEOUT_MS,
|
|
135
|
+
});
|
|
136
|
+
return this.vtai
|
|
137
|
+
? this.parseVtaiReport(resp.data)
|
|
138
|
+
: this.parseStandardReport(resp.data);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
if (axios_1.default.isAxiosError(err) && err.response?.status === 404) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Parse standard VT API response (data.attributes.*)
|
|
149
|
+
*/
|
|
150
|
+
parseStandardReport(data) {
|
|
151
|
+
const attrs = data?.data?.attributes;
|
|
152
|
+
if (!attrs)
|
|
153
|
+
return null;
|
|
154
|
+
const report = {
|
|
155
|
+
hash: data.data.id,
|
|
156
|
+
stats: attrs.last_analysis_stats,
|
|
157
|
+
name: attrs.meaningful_name,
|
|
158
|
+
vtLink: `https://www.virustotal.com/gui/file/${data.data.id}`,
|
|
159
|
+
};
|
|
160
|
+
if (attrs.crowdsourced_ai_results && attrs.crowdsourced_ai_results.length > 0) {
|
|
161
|
+
report.crowdsourcedAiResults = attrs.crowdsourced_ai_results.map((r) => ({
|
|
162
|
+
source: r.source,
|
|
163
|
+
analysis: r.analysis,
|
|
164
|
+
verdict: r.verdict,
|
|
165
|
+
id: r.id,
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
return report;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Parse VTAI simplified response (data.* without attributes wrapper)
|
|
172
|
+
*/
|
|
173
|
+
parseVtaiReport(data) {
|
|
174
|
+
const fileData = data?.data;
|
|
175
|
+
if (!fileData)
|
|
176
|
+
return null;
|
|
177
|
+
const report = {
|
|
178
|
+
hash: fileData.id,
|
|
179
|
+
stats: fileData.last_analysis_stats || { malicious: 0, suspicious: 0, harmless: 0, undetected: 0 },
|
|
180
|
+
name: fileData.type_description,
|
|
181
|
+
vtLink: `https://www.virustotal.com/gui/file/${fileData.id}`,
|
|
182
|
+
};
|
|
183
|
+
if (fileData.ai_insights && fileData.ai_insights.length > 0) {
|
|
184
|
+
report.crowdsourcedAiResults = fileData.ai_insights.map((r) => ({
|
|
185
|
+
source: r.source,
|
|
186
|
+
analysis: r.analysis,
|
|
187
|
+
verdict: r.verdict,
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
return report;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Upload a file to VirusTotal for analysis.
|
|
194
|
+
* Standard VT: supports large files (>32MB) via upload_url endpoint.
|
|
195
|
+
* VTAI: max 32MB, no large file support.
|
|
196
|
+
*/
|
|
197
|
+
async uploadFile(filePath) {
|
|
198
|
+
const stat = fs.statSync(filePath);
|
|
199
|
+
const sizeMb = stat.size / (1024 * 1024);
|
|
200
|
+
if (this.vtai && sizeMb > 32) {
|
|
201
|
+
throw new Error(`File too large for VTAI (${sizeMb.toFixed(1)}MB > 32MB). Configure your own VT API key for large file uploads.`);
|
|
202
|
+
}
|
|
203
|
+
let uploadUrl = `${this.baseUrl}/files/`;
|
|
204
|
+
if (sizeMb > 32) {
|
|
205
|
+
const urlResp = await axios_1.default.get(`${this.baseUrl}/files/upload_url`, {
|
|
206
|
+
headers: this.headers(),
|
|
207
|
+
timeout: HTTP_TIMEOUT_MS,
|
|
208
|
+
});
|
|
209
|
+
uploadUrl = urlResp.data.data;
|
|
210
|
+
}
|
|
211
|
+
const form = new form_data_1.default();
|
|
212
|
+
form.append('file', fs.createReadStream(filePath));
|
|
213
|
+
if (this.vtai) {
|
|
214
|
+
form.append('agent_comments', 'Auto-scanned by VT Sentinel for OpenClaw');
|
|
215
|
+
}
|
|
216
|
+
const resp = await axios_1.default.post(uploadUrl, form, {
|
|
217
|
+
headers: {
|
|
218
|
+
...form.getHeaders(),
|
|
219
|
+
...this.headers(),
|
|
220
|
+
},
|
|
221
|
+
maxContentLength: Infinity,
|
|
222
|
+
maxBodyLength: Infinity,
|
|
223
|
+
timeout: HTTP_TIMEOUT_MS * 4, // 120s for uploads (large files)
|
|
224
|
+
});
|
|
225
|
+
return {
|
|
226
|
+
analysisId: resp.data.data.id,
|
|
227
|
+
message: `File uploaded. Analysis ID: ${resp.data.data.id}`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
exports.VTApiClient = VTApiClient;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vt-auto-scan
|
|
3
|
+
description: >-
|
|
4
|
+
Automatically scans files created or downloaded by agent tool calls
|
|
5
|
+
(exec, write, web_fetch) using VirusTotal. Detects malware downloads,
|
|
6
|
+
malicious scripts, and compromised skill files in real-time.
|
|
7
|
+
emoji: "\U0001F6E1\uFE0F"
|
|
8
|
+
events:
|
|
9
|
+
- tool_result_persist
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# VT Auto-Scan Hook
|
|
13
|
+
|
|
14
|
+
Intercepts tool execution results and automatically scans any files that were
|
|
15
|
+
created, downloaded, or modified during the tool call. This provides a
|
|
16
|
+
transparent security layer that catches malicious payloads regardless of
|
|
17
|
+
which skill or command originated the download.
|
|
18
|
+
|
|
19
|
+
## Detected Patterns
|
|
20
|
+
|
|
21
|
+
- `curl -o /path/file`, `wget -O /path/file` — download targets
|
|
22
|
+
- `> /path/file`, `tee /path/file` — redirect outputs
|
|
23
|
+
- `bash /path/script.sh`, `python /path/script.py` — executed scripts
|
|
24
|
+
- Files written via `write` / `edit` tools
|
|
25
|
+
- File paths appearing in tool output in monitored directories
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone hook handler for vt-auto-scan.
|
|
3
|
+
* This handler works when loaded independently by OpenClaw's hook discovery.
|
|
4
|
+
* It creates its own scanner instance from the environment.
|
|
5
|
+
*
|
|
6
|
+
* Supports both standard VT API (user key) and VTAI (cached agent credentials).
|
|
7
|
+
* For the integrated plugin path, the hook is registered directly in index.ts.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
|
|
13
|
+
let Scanner, extractPaths, loadAgentCredentials;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Try to load from the compiled plugin
|
|
17
|
+
const distDir = path.resolve(__dirname, '../../dist');
|
|
18
|
+
Scanner = require(path.join(distDir, 'scanner')).Scanner;
|
|
19
|
+
extractPaths = require(path.join(distDir, 'path-extractor')).extractPaths;
|
|
20
|
+
loadAgentCredentials = require(path.join(distDir, 'vt-api')).loadAgentCredentials;
|
|
21
|
+
} catch (e) {
|
|
22
|
+
// Modules not available — this hook won't function standalone
|
|
23
|
+
console.error('[vt-auto-scan] Could not load scanner modules:', e.message);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a scanner instance using available credentials.
|
|
28
|
+
* Priority: user API key > cached VTAI agent token.
|
|
29
|
+
*/
|
|
30
|
+
function createScanner(logger) {
|
|
31
|
+
const apiKey = process.env.VIRUSTOTAL_API_KEY;
|
|
32
|
+
|
|
33
|
+
// User has their own VT API key (and it's not the 'active' placeholder)
|
|
34
|
+
if (apiKey && apiKey !== 'active') {
|
|
35
|
+
return new Scanner(apiKey, logger);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Try VTAI cached credentials
|
|
39
|
+
if (loadAgentCredentials) {
|
|
40
|
+
try {
|
|
41
|
+
const creds = loadAgentCredentials();
|
|
42
|
+
if (creds) {
|
|
43
|
+
return new Scanner(creds.agentToken, logger, 32, 'ask', true);
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
// Credentials not available
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const handler = async (event) => {
|
|
54
|
+
// Only handle tool result events
|
|
55
|
+
if (event.type !== 'tool_result_persist' && event.type !== 'tool_result') return;
|
|
56
|
+
|
|
57
|
+
if (!Scanner || !extractPaths) return;
|
|
58
|
+
|
|
59
|
+
const logger = {
|
|
60
|
+
info: (m) => console.log(m),
|
|
61
|
+
warn: (m) => console.warn(m),
|
|
62
|
+
error: (m) => console.error(m),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const scanner = createScanner(logger);
|
|
66
|
+
if (!scanner) return;
|
|
67
|
+
|
|
68
|
+
const toolName = event.toolName || event.tool || '';
|
|
69
|
+
const toolParams = event.toolParams || event.params || event.input || {};
|
|
70
|
+
|
|
71
|
+
// Extract result text
|
|
72
|
+
let resultText = '';
|
|
73
|
+
if (typeof event.toolResult === 'string') resultText = event.toolResult;
|
|
74
|
+
else if (event.toolResult?.stdout) resultText = event.toolResult.stdout;
|
|
75
|
+
else if (event.toolResult?.content && Array.isArray(event.toolResult.content)) {
|
|
76
|
+
resultText = event.toolResult.content
|
|
77
|
+
.filter(p => p.type === 'text')
|
|
78
|
+
.map(p => p.text)
|
|
79
|
+
.join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const targets = extractPaths(toolName, toolParams, resultText);
|
|
83
|
+
if (targets.length === 0) return;
|
|
84
|
+
|
|
85
|
+
for (const target of targets) {
|
|
86
|
+
try {
|
|
87
|
+
const result = await scanner.scanFile(target.path);
|
|
88
|
+
if (result.verdict === 'malicious' || result.verdict === 'suspicious') {
|
|
89
|
+
const warning = `\n\n⚠️ VT-SENTINEL SECURITY ALERT ⚠️\n` +
|
|
90
|
+
`File: ${result.fileName} | Verdict: ${result.verdict.toUpperCase()}\n` +
|
|
91
|
+
`${result.vtLink || ''}`;
|
|
92
|
+
|
|
93
|
+
if (event.toolResult?.content && Array.isArray(event.toolResult.content)) {
|
|
94
|
+
event.toolResult.content.push({ type: 'text', text: warning });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
logger.error(`[VT-Sentinel] ${result.verdict.toUpperCase()}: ${result.fileName} — ${result.message}`);
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
logger.error(`[vt-auto-scan] Scan error: ${err.message}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
module.exports = handler;
|
|
106
|
+
module.exports.default = handler;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "openclaw-plugin-vt-sentinel",
|
|
3
|
+
"skills": ["./skills"],
|
|
4
|
+
"hooks": ["./hooks"],
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"apiKey": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "VirusTotal API Key (v3). Optional — without it, VT Sentinel auto-registers with VTAI for zero-config operation."
|
|
11
|
+
},
|
|
12
|
+
"watchDirs": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"items": { "type": "string" },
|
|
15
|
+
"description": "Directories to monitor for new files"
|
|
16
|
+
},
|
|
17
|
+
"autoScan": {
|
|
18
|
+
"type": "boolean",
|
|
19
|
+
"default": true,
|
|
20
|
+
"description": "Automatically scan new files in watched directories"
|
|
21
|
+
},
|
|
22
|
+
"maxFileSizeMb": {
|
|
23
|
+
"type": "number",
|
|
24
|
+
"default": 32,
|
|
25
|
+
"description": "Maximum file size to scan (MB)"
|
|
26
|
+
},
|
|
27
|
+
"sensitiveFilePolicy": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"enum": ["ask", "ask_once", "always_upload", "hash_only"],
|
|
30
|
+
"default": "ask",
|
|
31
|
+
"description": "Policy for sensitive files (PDF, Office, unknown archives): ask = prompt each time, ask_once = prompt once and remember, always_upload = auto-upload, hash_only = never upload"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"required": []
|
|
35
|
+
},
|
|
36
|
+
"uiHints": {
|
|
37
|
+
"apiKey": { "label": "VirusTotal API Key", "sensitive": true },
|
|
38
|
+
"watchDirs": { "label": "Watch Directories" },
|
|
39
|
+
"autoScan": { "label": "Auto-scan new files" },
|
|
40
|
+
"maxFileSizeMb": { "label": "Max File Size (MB)" },
|
|
41
|
+
"sensitiveFilePolicy": { "label": "Sensitive File Policy" }
|
|
42
|
+
}
|
|
43
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-plugin-vt-sentinel",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "VirusTotal Sentinel for OpenClaw - Malware detection and AI-powered code analysis",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"watch": "tsc -w",
|
|
10
|
+
"test": "node dist/test_runner.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"openclaw",
|
|
14
|
+
"plugin",
|
|
15
|
+
"security",
|
|
16
|
+
"virustotal",
|
|
17
|
+
"malware",
|
|
18
|
+
"code-insight"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist/index.*",
|
|
23
|
+
"dist/scanner.*",
|
|
24
|
+
"dist/vt-api.*",
|
|
25
|
+
"dist/classifier.*",
|
|
26
|
+
"dist/cache.*",
|
|
27
|
+
"dist/path-extractor.*",
|
|
28
|
+
"skills/",
|
|
29
|
+
"hooks/",
|
|
30
|
+
"openclaw.plugin.json"
|
|
31
|
+
],
|
|
32
|
+
"openclaw": {
|
|
33
|
+
"extensions": ["./dist/index.js"]
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"axios": "^1.6.0",
|
|
37
|
+
"chokidar": "^3.5.3",
|
|
38
|
+
"form-data": "^4.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^20.0.0",
|
|
42
|
+
"typescript": "^5.0.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vt-sentinel
|
|
3
|
+
description: >-
|
|
4
|
+
Security scanner using VirusTotal. Use when the user asks to scan a file for
|
|
5
|
+
malware, check if a downloaded file or script is safe, verify a file hash
|
|
6
|
+
against threat intelligence, or assess the security of any file. Returns both
|
|
7
|
+
antivirus engine detections AND AI-powered Code Insight analysis (when
|
|
8
|
+
available) in a single unified report.
|
|
9
|
+
metadata:
|
|
10
|
+
openclaw:
|
|
11
|
+
emoji: "\U0001F6E1\uFE0F"
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# VT Sentinel — VirusTotal Active Protection
|
|
15
|
+
|
|
16
|
+
Protects OpenClaw users with **active prevention**, not just detection:
|
|
17
|
+
|
|
18
|
+
1. **Antivirus engines** — 60+ vendors check file hashes for known malware
|
|
19
|
+
2. **AI Code Insight** — Gemini-based semantic analysis for scripts, skills, binaries
|
|
20
|
+
3. **Active blocking** — Files detected as malicious are blocklisted and quarantined. Any subsequent attempt to execute them is automatically blocked before it runs.
|
|
21
|
+
|
|
22
|
+
Both AV and AI sources are always checked. The final verdict is the worst of the two. Malicious files are quarantined (renamed to `.QUARANTINED`) and added to a runtime blocklist that prevents their execution via `exec` or `bash` tools.
|
|
23
|
+
|
|
24
|
+
## Available Tools
|
|
25
|
+
|
|
26
|
+
### `vt_scan_file` — Full File Scan
|
|
27
|
+
Classifies the file, computes SHA-256, checks VT (AV + Code Insight), uploads if unknown.
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
vt_scan_file { "path": "/absolute/path/to/file" }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### `vt_check_hash` — Quick Hash Lookup
|
|
34
|
+
Fast check of a SHA-256 hash against VT. Returns AV detections + Code Insight if available.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
vt_check_hash { "hash": "e3b0c44298fc1c149afbf4c8996fb924..." }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### `vt_upload_consent` — Confirm Sensitive File Upload
|
|
41
|
+
When `vt_scan_file` returns `needs_consent`, relay the user's decision.
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
vt_upload_consent { "path": "/path/to/document.pdf", "upload": true }
|
|
45
|
+
vt_upload_consent { "path": "/path/to/document.pdf", "upload": false }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## When to Use Which Tool
|
|
49
|
+
|
|
50
|
+
| Scenario | Tool |
|
|
51
|
+
|----------|------|
|
|
52
|
+
| User asks "is this file safe?" | `vt_scan_file` |
|
|
53
|
+
| User provides a SHA-256 hash | `vt_check_hash` |
|
|
54
|
+
| Evaluating a new SKILL.md or HOOK.md | `vt_scan_file` |
|
|
55
|
+
| Checking a downloaded script or binary | `vt_scan_file` |
|
|
56
|
+
| User said YES/NO to uploading a sensitive file | `vt_upload_consent` |
|
|
57
|
+
|
|
58
|
+
## Interpreting Results
|
|
59
|
+
|
|
60
|
+
Every result includes both AV detections and Code Insight (when available):
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
File: example.sh
|
|
64
|
+
Category: HIGH_RISK
|
|
65
|
+
Verdict: MALICIOUS
|
|
66
|
+
Detections: 12 malicious, 0 suspicious / 64 engines
|
|
67
|
+
Code Insight (Code Insight): MALICIOUS
|
|
68
|
+
Analysis: This script downloads and executes a remote payload...
|
|
69
|
+
VT Link: https://www.virustotal.com/gui/file/...
|
|
70
|
+
Summary: AV: 12/64 engines detected malware | AI: MALICIOUS — ...
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Verdicts
|
|
74
|
+
- **CLEAN** — No threats from AV engines or AI
|
|
75
|
+
- **MALICIOUS** — AV engines and/or AI flagged the file. Warn the user immediately.
|
|
76
|
+
- **SUSPICIOUS** — Some concerns raised. Recommend caution.
|
|
77
|
+
- **PENDING** — File uploaded, analysis not yet available. Check again later.
|
|
78
|
+
- **SKIPPED** — File classified as safe/media (auto-scan only; manual scans always check).
|
|
79
|
+
- **NEEDS_CONSENT** — Sensitive file. Hash checked (not found). Ask user before uploading.
|
|
80
|
+
|
|
81
|
+
### Code Insight
|
|
82
|
+
When present in the result, Code Insight provides:
|
|
83
|
+
- **Source**: Analysis engine (e.g., "Code Insight", "palm")
|
|
84
|
+
- **Verdict**: UNDETECTED / SUSPICIOUS / MALICIOUS
|
|
85
|
+
- **Analysis**: Free-text description of what the file does
|
|
86
|
+
|
|
87
|
+
Code Insight works on any file type VT can analyze — scripts, skills, binaries (decompiled), documents with macros, etc.
|
|
88
|
+
|
|
89
|
+
## File Categories
|
|
90
|
+
|
|
91
|
+
Classification uses magic bytes and content analysis (never extensions alone):
|
|
92
|
+
- **HIGH_RISK**: Binaries (PE, ELF, Mach-O), scripts (shebang/content patterns), ZIPs with executables → auto-scanned
|
|
93
|
+
- **SEMANTIC_RISK**: SKILL.md, HOOK.md, AGENTS.md, SOUL.md, skill ZIPs → auto-scanned
|
|
94
|
+
- **SENSITIVE**: PDF, Office docs, unknown ZIPs → hash checked, upload needs consent
|
|
95
|
+
- **MEDIA/SAFE**: Images, video, audio, plain text → skipped in auto-scan, checked in manual scan
|
|
96
|
+
|
|
97
|
+
## Consent Flow for Sensitive Files
|
|
98
|
+
|
|
99
|
+
When `vt_scan_file` returns `NEEDS_CONSENT`:
|
|
100
|
+
|
|
101
|
+
1. Tell the user: the file's hash was checked (no match), but the file was NOT uploaded.
|
|
102
|
+
2. Explain: uploading enables deep analysis (macros, embedded threats, AI), but content is shared with VirusTotal.
|
|
103
|
+
3. Ask: "Would you like me to upload this file for a full scan?"
|
|
104
|
+
4. Call `vt_upload_consent` with their answer.
|
|
105
|
+
|
|
106
|
+
## Active Protection
|
|
107
|
+
|
|
108
|
+
VT Sentinel automatically protects the system in real-time:
|
|
109
|
+
|
|
110
|
+
1. **Auto-scan**: Every file downloaded or created by tools (`exec`, `write`, `web_fetch`) is automatically scanned
|
|
111
|
+
2. **Blocklist**: Malicious and suspicious files are added to an in-memory blocklist
|
|
112
|
+
3. **Quarantine**: Malicious files are renamed to `.QUARANTINED` so they cannot be executed
|
|
113
|
+
4. **Execution blocking**: Any `exec`/`bash` command that references a blocked file is intercepted and prevented BEFORE execution
|
|
114
|
+
5. **Command pattern inspection**: Commands are analyzed for dangerous patterns BEFORE execution, even when no file is involved:
|
|
115
|
+
- **Pipe-to-shell**: `curl | bash`, `wget | sh`, `base64 -d | bash` — remote code execution without touching disk
|
|
116
|
+
- **SSH key injection**: Appending to `authorized_keys` — backdoor persistence
|
|
117
|
+
- **Data exfiltration**: Sending data to webhook.site, requestbin, pipedream, etc.
|
|
118
|
+
- **Credential theft**: Piping `.env`, SSH keys, or AWS credentials to network tools
|
|
119
|
+
|
|
120
|
+
If you see a "BLOCKED" message, it means VT Sentinel prevented a potentially dangerous operation. Do NOT attempt to work around the block — inform the user about the threat.
|
|
121
|
+
|
|
122
|
+
## Constraints
|
|
123
|
+
|
|
124
|
+
- Always use absolute file paths
|
|
125
|
+
- Never expose the VT API key in output
|
|
126
|
+
- Rate limit: 4 requests/minute (free tier) — handled automatically
|
|
127
|
+
- For SENSITIVE files, follow the consent flow — never upload without permission
|
|
128
|
+
- If verdict is MALICIOUS, always warn the user prominently
|
|
129
|
+
- Do not attempt to bypass quarantine or blocklist protections
|