openclaw-plugin-vt-sentinel 0.9.2 → 0.11.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.
@@ -10,7 +10,7 @@
10
10
  const path = require('path');
11
11
  const fs = require('fs');
12
12
 
13
- let Scanner, extractPaths, loadAgentCredentials;
13
+ let Scanner, extractPaths, loadAgentCredentials, ConfigManager, matchGlob, isSelfPath, StateStore;
14
14
 
15
15
  try {
16
16
  // Try to load from the compiled plugin
@@ -18,21 +18,51 @@ try {
18
18
  Scanner = require(path.join(distDir, 'scanner')).Scanner;
19
19
  extractPaths = require(path.join(distDir, 'path-extractor')).extractPaths;
20
20
  loadAgentCredentials = require(path.join(distDir, 'vt-api')).loadAgentCredentials;
21
+ ConfigManager = require(path.join(distDir, 'config-manager')).ConfigManager;
22
+ matchGlob = require(path.join(distDir, 'config-manager')).matchGlob;
23
+ isSelfPath = require(path.join(distDir, 'index')).isSelfPath;
24
+ StateStore = require(path.join(distDir, 'state-store')).StateStore;
21
25
  } catch (e) {
22
26
  // Modules not available — this hook won't function standalone
23
27
  console.error('[vt-auto-scan] Could not load scanner modules:', e.message);
24
28
  }
25
29
 
30
+ /**
31
+ * Read static plugin config from OpenClaw config file.
32
+ * Returns null if not found or on error.
33
+ */
34
+ function readStaticConfig() {
35
+ try {
36
+ const stateDir = process.env.OPENCLAW_STATE_DIR || path.join(require('os').homedir(), '.openclaw');
37
+ const configPath = path.join(stateDir, 'openclaw.json');
38
+ if (!fs.existsSync(configPath)) return null;
39
+ const raw = fs.readFileSync(configPath, 'utf-8');
40
+ const parsed = (() => {
41
+ try { return require('json5').parse(raw); } catch { return JSON.parse(raw); }
42
+ })();
43
+ return parsed?.plugins?.entries?.['openclaw-plugin-vt-sentinel']?.config || null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
26
49
  /**
27
50
  * Create a scanner instance using available credentials.
28
- * Priority: user API key > cached VTAI agent token.
51
+ * Priority: user apiKey from static config > cached VTAI agent token.
52
+ *
53
+ * v0.11.0 change: removed the `process.env.VIRUSTOTAL_API_KEY` lookup + the
54
+ * `'vtai-active'` env sentinel. The main plugin no longer mutates the
55
+ * environment, so this standalone hook mirrors that stance: credential source
56
+ * is 100% derived from openclaw.json (user apiKey) or the persisted agent
57
+ * credentials file (VTAI mode).
29
58
  */
30
- function createScanner(logger) {
31
- const apiKey = process.env.VIRUSTOTAL_API_KEY;
59
+ function createScanner(logger, eff, staticConfig) {
60
+ const userKey = staticConfig && typeof staticConfig.apiKey === 'string' && staticConfig.apiKey.trim().length > 0
61
+ ? staticConfig.apiKey.trim()
62
+ : null;
32
63
 
33
- // User has their own VT API key (and it's not the 'active' placeholder)
34
- if (apiKey && apiKey !== 'vtai-active') {
35
- return new Scanner(apiKey, logger);
64
+ if (userKey) {
65
+ return new Scanner(userKey, logger, eff.maxFileSizeMb, eff.sensitiveFilePolicy, false, eff.semanticFilePolicy);
36
66
  }
37
67
 
38
68
  // Try VTAI cached credentials
@@ -40,7 +70,7 @@ function createScanner(logger) {
40
70
  try {
41
71
  const creds = loadAgentCredentials();
42
72
  if (creds) {
43
- return new Scanner(creds.agentToken, logger, 32, 'ask', true);
73
+ return new Scanner(creds.agentToken, logger, eff.maxFileSizeMb, eff.sensitiveFilePolicy, true, eff.semanticFilePolicy);
44
74
  }
45
75
  } catch (e) {
46
76
  // Credentials not available
@@ -56,13 +86,37 @@ const handler = async (event) => {
56
86
 
57
87
  if (!Scanner || !extractPaths) return;
58
88
 
89
+ // Load config (static + persisted overrides) and check autoScan
90
+ let configManager = null;
91
+ const staticConfig = readStaticConfig();
92
+ if (ConfigManager) {
93
+ configManager = new ConfigManager(staticConfig);
94
+ // Apply persisted runtime overrides from vt-sentinel-state.json
95
+ if (StateStore) {
96
+ try {
97
+ const stateStore = new StateStore();
98
+ configManager.loadPersistedOverrides(stateStore.getPersistedOverrides());
99
+ } catch {
100
+ // State file missing or corrupt — proceed with static config only
101
+ }
102
+ }
103
+ }
104
+ const eff = configManager ? configManager.getEffective() : {
105
+ autoScan: true, maxFileSizeMb: 32,
106
+ sensitiveFilePolicy: 'ask', semanticFilePolicy: 'hash_only',
107
+ excludeGlobs: [],
108
+ };
109
+
110
+ // autoScan=false disables hook scanning
111
+ if (!eff.autoScan) return;
112
+
59
113
  const logger = {
60
114
  info: (m) => console.log(m),
61
115
  warn: (m) => console.warn(m),
62
116
  error: (m) => console.error(m),
63
117
  };
64
118
 
65
- const scanner = createScanner(logger);
119
+ const scanner = createScanner(logger, eff, staticConfig);
66
120
  if (!scanner) return;
67
121
 
68
122
  const toolName = event.toolName || event.tool || '';
@@ -83,8 +137,22 @@ const handler = async (event) => {
83
137
  if (targets.length === 0) return;
84
138
 
85
139
  for (const target of targets) {
140
+ // Self-exclusion
141
+ if (isSelfPath && isSelfPath(target.path)) continue;
142
+
143
+ // excludeGlobs: skip files matching any exclude pattern
144
+ if (matchGlob && eff.excludeGlobs && eff.excludeGlobs.length > 0) {
145
+ let excluded = false;
146
+ for (const glob of eff.excludeGlobs) {
147
+ if (matchGlob(target.path, glob)) { excluded = true; break; }
148
+ }
149
+ if (excluded) continue;
150
+ }
151
+
86
152
  try {
87
- const result = await scanner.scanFile(target.path);
153
+ // read_target hash-only (never upload files the agent merely reads)
154
+ const isReadTarget = target.source === 'read_target';
155
+ const result = await scanner.scanFile(target.path, false, undefined, isReadTarget);
88
156
  if (result.verdict === 'malicious' || result.verdict === 'suspicious') {
89
157
  const warning = `\n\n⚠️ VT-SENTINEL SECURITY ALERT ⚠️\n` +
90
158
  `File: ${result.fileName} | Verdict: ${result.verdict.toUpperCase()}\n` +
@@ -1,9 +1,25 @@
1
1
  {
2
2
  "id": "openclaw-plugin-vt-sentinel",
3
+ "name": "VT Sentinel",
4
+ "description": "VirusTotal Sentinel for OpenClaw — malware detection, active protection, and AI-powered code analysis.",
5
+ "version": "0.11.0",
3
6
  "skills": ["./skills"],
4
- "hooks": ["./hooks"],
7
+ "contracts": {
8
+ "tools": [
9
+ "vt_scan_file",
10
+ "vt_check_hash",
11
+ "vt_upload_consent",
12
+ "vt_sentinel_status",
13
+ "vt_sentinel_configure",
14
+ "vt_sentinel_reset_policy",
15
+ "vt_sentinel_help",
16
+ "vt_sentinel_update",
17
+ "vt_sentinel_re_register"
18
+ ]
19
+ },
5
20
  "configSchema": {
6
21
  "type": "object",
22
+ "additionalProperties": false,
7
23
  "properties": {
8
24
  "apiKey": {
9
25
  "type": "string",
@@ -30,6 +46,12 @@
30
46
  "default": "ask",
31
47
  "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
48
  },
49
+ "semanticFilePolicy": {
50
+ "type": "string",
51
+ "enum": ["ask", "ask_once", "always_upload", "hash_only"],
52
+ "default": "hash_only",
53
+ "description": "Policy for instruction files (SKILL.md, HOOK.md, TOOLS.md, AGENTS.md, etc.): ask = prompt each time, ask_once = prompt once, always_upload = auto-upload, hash_only = never upload (default, privacy-safe)"
54
+ },
33
55
  "notifyLevel": {
34
56
  "type": "string",
35
57
  "enum": ["all", "threats_only", "silent"],
@@ -94,6 +116,7 @@
94
116
  "autoScan": { "label": "Auto-scan new files" },
95
117
  "maxFileSizeMb": { "label": "Max File Size (MB)" },
96
118
  "sensitiveFilePolicy": { "label": "Sensitive File Policy" },
119
+ "semanticFilePolicy": { "label": "Instruction File Policy" },
97
120
  "notifyLevel": { "label": "Notification Level" },
98
121
  "excludeDirs": { "label": "Exclude Directories" },
99
122
  "excludeGlobs": { "label": "Exclude File Patterns" },
package/package.json CHANGED
@@ -1,21 +1,25 @@
1
1
  {
2
2
  "name": "openclaw-plugin-vt-sentinel",
3
- "version": "0.9.2",
3
+ "version": "0.11.0",
4
+ "displayName": "VT Sentinel",
4
5
  "description": "VirusTotal Sentinel for OpenClaw - Malware detection and AI-powered code analysis",
5
6
  "main": "dist/index.js",
6
7
  "types": "dist/index.d.ts",
7
8
  "homepage": "https://ai.virustotal.com",
8
9
  "repository": {
9
10
  "type": "git",
10
- "url": "https://github.com/VirusTotal/openclaw-plugin-vt-sentinel"
11
+ "url": "https://github.com/king-tero/VT-sentinel"
11
12
  },
12
13
  "bugs": {
13
- "url": "https://github.com/VirusTotal/openclaw-plugin-vt-sentinel/issues"
14
+ "url": "https://github.com/king-tero/VT-sentinel/issues"
14
15
  },
15
16
  "scripts": {
16
- "build": "tsc",
17
+ "build": "tsc && node -e \"require('fs').cpSync('src/signatures','dist/signatures',{recursive:true})\"",
17
18
  "watch": "tsc -w",
18
- "test": "node dist/test_runner.js"
19
+ "test": "node dist/test_runner.js",
20
+ "scan": "node dist/self-scan.js",
21
+ "scan:json": "node dist/self-scan.js --json",
22
+ "prescan": "npm run build"
19
23
  },
20
24
  "keywords": [
21
25
  "openclaw",
@@ -32,9 +36,11 @@
32
36
  "license": "MIT",
33
37
  "files": [
34
38
  "README.md",
39
+ "CHANGELOG.md",
35
40
  "dist/index.*",
36
41
  "dist/scanner.*",
37
42
  "dist/vt-api.*",
43
+ "dist/vt-credentials.*",
38
44
  "dist/classifier.*",
39
45
  "dist/cache.*",
40
46
  "dist/path-extractor.*",
@@ -42,12 +48,24 @@
42
48
  "dist/config-manager.*",
43
49
  "dist/state-store.*",
44
50
  "dist/status-renderer.*",
51
+ "dist/env-access.*",
52
+ "dist/signatures/**/*.json",
45
53
  "skills/",
46
54
  "hooks/",
47
55
  "openclaw.plugin.json"
48
56
  ],
49
57
  "openclaw": {
50
- "extensions": ["./dist/index.js"]
58
+ "extensions": ["./dist/index.js"],
59
+ "install": {
60
+ "minHostVersion": ">=2026.3.22"
61
+ },
62
+ "compat": {
63
+ "pluginApi": ">=2026.3.22",
64
+ "minGatewayVersion": ">=2026.3.22"
65
+ },
66
+ "build": {
67
+ "openclawVersion": "2026.4.5"
68
+ }
51
69
  },
52
70
  "dependencies": {
53
71
  "axios": "^1.6.0",
@@ -90,11 +90,11 @@ Code Insight works on any file type VT can analyze — scripts, skills, binaries
90
90
 
91
91
  Classification uses magic bytes and content analysis (never extensions alone):
92
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
93
+ - **SEMANTIC_RISK**: SKILL.md, HOOK.md, TOOLS.md, AGENTS.md, SOUL.md, skill ZIPs → hash checked, upload needs consent (default: hash_only)
94
+ - **SENSITIVE**: PDF, Office docs, unknown ZIPs → hash checked, upload needs consent (default: ask)
95
95
  - **MEDIA/SAFE**: Images, video, audio, plain text → skipped in auto-scan, checked in manual scan
96
96
 
97
- ## Consent Flow for Sensitive Files
97
+ ## Consent Flow for Sensitive / Semantic Files
98
98
 
99
99
  When `vt_scan_file` returns `NEEDS_CONSENT`:
100
100
 
@@ -103,6 +103,8 @@ When `vt_scan_file` returns `NEEDS_CONSENT`:
103
103
  3. Ask: "Would you like me to upload this file for a full scan?"
104
104
  4. Call `vt_upload_consent` with their answer.
105
105
 
106
+ **Note**: Files read by the agent (via the `read` tool) are only hash-checked, never auto-uploaded. This protects instruction files like TOOLS.md and AGENTS.md from being uploaded to VT during normal agent operations.
107
+
106
108
  ## Admin Tools
107
109
 
108
110
  ### `vt_sentinel_status` — Current Status
@@ -118,6 +120,7 @@ Update any setting immediately. Changes persist to disk by default.
118
120
  ```
119
121
  vt_sentinel_configure { "preset": "privacy_first" }
120
122
  vt_sentinel_configure { "sensitiveFilePolicy": "hash_only", "notifyLevel": "threats_only" }
123
+ vt_sentinel_configure { "semanticFilePolicy": "ask" }
121
124
  vt_sentinel_configure { "watchDirsAdd": ["/extra/dir"], "excludeGlobs": ["*.log"] }
122
125
  vt_sentinel_configure { "blockMode": "log_only", "persist": "session" }
123
126
  ```