openclaw-plugin-vt-sentinel 0.12.0 → 0.12.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,58 @@
2
2
 
3
3
  All notable changes to `openclaw-plugin-vt-sentinel`.
4
4
 
5
+ ## 0.12.2 — Audit collector accuracy + legacy log sanitization
6
+
7
+ Small follow-up to 0.12.1. The static collector could only see
8
+ user-configured watch dirs (never the runtime auto-derived ones like
9
+ `/tmp`, `~/Downloads`, OpenClaw state subdirs), so its auto-scan finding
10
+ read "Watching 0 directories" on fresh installs — technically correct but
11
+ misleading to operators.
12
+
13
+ ### Fixed
14
+
15
+ - **Auto-scan finding text reflects the snapshot source.** The
16
+ `buildComplianceSnapshot` helper now accepts `source: 'runtime' | 'static'`.
17
+ Runtime (gateway) callers still report the live watcher list.
18
+ Static (CLI audit) callers now spell out that additional dirs are
19
+ auto-derived by the gateway and point to `vt_sentinel_status` for the
20
+ live list.
21
+ - **Legacy audit-log paths mentioned in `vt_sentinel_help` updated.** The
22
+ help text used to say `~/.openclaw/vt-sentinel-uploads.log` and
23
+ `vt-sentinel-detections.log`; since 0.12.0 they live in
24
+ `<stateDir>/vt-sentinel-audit/{uploads,detections}.log`.
25
+ - **Legacy log files tightened to `0o600` on plugin load.** Pre-0.12.0
26
+ installs left the old stateDir-root log files (`vt-sentinel-uploads.log`,
27
+ `vt-sentinel-detections.log`) at the process default (typically `0o664`).
28
+ On each gateway start the plugin now best-effort `chmod 0o600`s them and
29
+ logs the change. POSIX-only; no-op on Windows.
30
+ - **`package-lock.json` regenerated to the new version** — the shipped
31
+ tarball never included it, but a stale lock on disk caused confusion.
32
+
33
+ ## 0.12.1 — Security audit collector: dual-path wiring
34
+
35
+ Fix for the collector introduced in 0.12.0. The in-gateway registration via
36
+ `api.registerSecurityAuditCollector` was insufficient: `openclaw security audit
37
+ --deep` runs in a fresh Node process that doesn't share state with the gateway
38
+ and reads collectors from the plugin's **module-level** `securityAuditCollectors`
39
+ field instead. Verified empirically on the production Linux VM (OpenClaw
40
+ 2026.4.12): 0.12.0 shipped with a runtime-only collector that emitted zero
41
+ `vt-sentinel.*` findings under `openclaw security audit --deep --json`.
42
+
43
+ ### Fixed
44
+
45
+ - **`securityAuditCollectors` declared at module level.** The default export
46
+ is now a plugin-definition object: `{ id, name, register, securityAuditCollectors }`.
47
+ The CLI audit picks up the collector from the object's field; the gateway
48
+ still registers it at runtime via `api.registerSecurityAuditCollector` for
49
+ any in-process audit surface that uses `getActivePluginRegistry()`. Both
50
+ paths now light up.
51
+ - **`vtSentinelAuditCollector` is self-contained.** It rebuilds the
52
+ compliance snapshot exclusively from `ctx.config`, `ctx.stateDir`, and
53
+ `ctx.configPath` — no closure state, no shared-module variables. Usable
54
+ from a cold-loaded plugin metadata snapshot.
55
+ - Runtime behavior (scanning, hooks, policies) unchanged.
56
+
5
57
  ## 0.12.0 — Transparency surface + log hygiene
6
58
 
7
59
  **Headline:** what VT Sentinel does at runtime is now auditable from two
@@ -39,6 +39,16 @@ export interface ComplianceSnapshotInput {
39
39
  agentPublicHandle?: string;
40
40
  /** Optional: pre-collected log permission info (see `collectLogModes`). */
41
41
  logModes?: LogModesInfo;
42
+ /**
43
+ * Where the snapshot is being built from. `runtime` means the gateway is
44
+ * running and `watchDirs` reflects the live watcher state (including
45
+ * auto-derived dirs like `/tmp`, `~/Downloads`, OpenClaw state subdirs).
46
+ * `static` means the snapshot is being built by a fresh CLI process
47
+ * (e.g. `openclaw security audit --deep`) that cannot see auto-derived
48
+ * dirs — only the user-configured `config.watchDirs` entries.
49
+ * Defaults to `static`.
50
+ */
51
+ source?: 'runtime' | 'static';
42
52
  }
43
53
  export interface ComplianceSnapshot {
44
54
  credentialMode: CredentialMode;
@@ -103,3 +113,16 @@ export declare function collectLogModes(stateDir: string): LogModesInfo;
103
113
  * first and pass the result as `input.logModes`.
104
114
  */
105
115
  export declare function buildComplianceSnapshot(input: ComplianceSnapshotInput): ComplianceSnapshot;
116
+ interface AuditCollectorCtx {
117
+ config: any;
118
+ sourceConfig: any;
119
+ env: NodeJS.ProcessEnv;
120
+ stateDir: string;
121
+ configPath: string;
122
+ }
123
+ /**
124
+ * Static security-audit collector. Pass this to OpenClaw via the plugin's
125
+ * module-level default export (`securityAuditCollectors: [vtSentinelAuditCollector]`).
126
+ */
127
+ export declare function vtSentinelAuditCollector(ctx: AuditCollectorCtx): AuditFinding[];
128
+ export {};
@@ -53,8 +53,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
53
53
  exports.computePaths = computePaths;
54
54
  exports.collectLogModes = collectLogModes;
55
55
  exports.buildComplianceSnapshot = buildComplianceSnapshot;
56
+ exports.vtSentinelAuditCollector = vtSentinelAuditCollector;
56
57
  const fs = __importStar(require("fs"));
57
58
  const path = __importStar(require("path"));
59
+ const config_manager_1 = require("./config-manager");
58
60
  // --- Core paths helper (pure) ---
59
61
  function computePaths(stateDir) {
60
62
  const auditDir = path.join(stateDir, 'vt-sentinel-audit');
@@ -131,6 +133,7 @@ function isBroadWatchDir(dir) {
131
133
  */
132
134
  function buildComplianceSnapshot(input) {
133
135
  const { config, stateDir, credentialMode, watchDirs, agentPublicHandle, logModes } = input;
136
+ const source = input.source ?? 'static';
134
137
  const paths = computePaths(stateDir);
135
138
  const identity = {
136
139
  displayNameSet: !!config.agentDisplayName,
@@ -161,13 +164,34 @@ function buildComplianceSnapshot(input) {
161
164
  (credentialMode === 'vtai' ? 'ai.virustotal.com (scan requests)\n' : '') +
162
165
  'registry.npmjs.org + clawhub.ai — only when the user explicitly invokes vt_sentinel_update.',
163
166
  });
167
+ // Auto-scan finding. Detail text branches on (a) whether autoScan is on
168
+ // and (b) whether we're building the snapshot at runtime (live watcher
169
+ // list available) or statically (CLI audit process — only config.watchDirs
170
+ // is visible; auto-derived dirs like /tmp/Downloads/Desktop/OpenClaw state
171
+ // subdirs are computed by the gateway at runtime and cannot be recovered
172
+ // here).
173
+ const autoScanDetail = (() => {
174
+ if (!config.autoScan) {
175
+ return `Active protection hooks remain registered (block mode: ${config.blockMode}) but no background watcher is running.`;
176
+ }
177
+ const suffix = ` block mode: ${config.blockMode}. notify level: ${config.notifyLevel}.`;
178
+ if (source === 'runtime') {
179
+ return `Watching ${watchDirs.length} director${watchDirs.length === 1 ? 'y' : 'ies'}.${suffix}`;
180
+ }
181
+ // Static/CLI snapshot.
182
+ if (watchDirs.length > 0) {
183
+ return `User-configured watch dirs (${watchDirs.length}): ${watchDirs.join(', ')}. ` +
184
+ `Additional dirs auto-derived at gateway runtime (OS temp, ~/Downloads, ~/Desktop, OpenClaw state subdirs, workspace). ` +
185
+ `Run vt_sentinel_status inside the gateway for the live list.${suffix}`;
186
+ }
187
+ return `No user-configured watch dirs. At runtime the gateway auto-derives monitors from OS temp, ~/Downloads, ~/Desktop, and the OpenClaw state subdirs (skills, extensions, hooks, workspace). ` +
188
+ `Run vt_sentinel_status inside the gateway for the live list.${suffix}`;
189
+ })();
164
190
  baseline.push({
165
191
  checkId: 'vt-sentinel.auto-scan',
166
192
  severity: 'info',
167
193
  title: `Auto-scan: ${config.autoScan ? 'enabled' : 'disabled'}`,
168
- detail: config.autoScan
169
- ? `Watching ${watchDirs.length} director${watchDirs.length === 1 ? 'y' : 'ies'}. block mode: ${config.blockMode}. notify level: ${config.notifyLevel}.`
170
- : `Active protection hooks remain registered (block mode: ${config.blockMode}) but no background watcher is running.`,
194
+ detail: autoScanDetail,
171
195
  });
172
196
  baseline.push({
173
197
  checkId: 'vt-sentinel.upload-policies',
@@ -301,3 +325,80 @@ function buildComplianceSnapshot(input) {
301
325
  risks,
302
326
  };
303
327
  }
328
+ // --- Static (module-level) security audit collector ---
329
+ //
330
+ // The OpenClaw `security audit --deep` CLI runs in a fresh process that
331
+ // does NOT share state with the gateway. It therefore reads
332
+ // `securityAuditCollectors` from the plugin's module-level definition, NOT
333
+ // from runtime `api.registerSecurityAuditCollector(...)` calls.
334
+ //
335
+ // This function is the canonical collector used by the CLI. It derives the
336
+ // same snapshot as the gateway-side collector, but exclusively from the
337
+ // audit context (ctx.config, ctx.stateDir, ctx.configPath) — no closure
338
+ // state, no cross-module shared variables. Everything is reconstructable
339
+ // from what the CLI hands us.
340
+ //
341
+ // Differences from the gateway-side invocation:
342
+ // - `watchDirs` only includes user-configured dirs (`cfg.watchDirs`); the
343
+ // auto-derived dirs (tmp, ~/Downloads, etc.) are a runtime concept.
344
+ // - `credentialMode` is inferred from (a) presence of `cfg.apiKey` and
345
+ // (b) presence of the persisted agent credentials file on disk.
346
+ const PLUGIN_ID = 'openclaw-plugin-vt-sentinel';
347
+ function extractPluginConfig(cfg) {
348
+ try {
349
+ return cfg?.plugins?.entries?.[PLUGIN_ID]?.config ?? null;
350
+ }
351
+ catch {
352
+ return null;
353
+ }
354
+ }
355
+ function readAgentPublicHandle(stateDir) {
356
+ try {
357
+ const raw = fs.readFileSync(path.join(stateDir, 'vt-sentinel-agent.json'), 'utf-8');
358
+ const parsed = JSON.parse(raw);
359
+ return typeof parsed?.publicHandle === 'string' ? parsed.publicHandle : undefined;
360
+ }
361
+ catch {
362
+ return undefined;
363
+ }
364
+ }
365
+ function credentialsFileExists(stateDir) {
366
+ try {
367
+ return fs.statSync(path.join(stateDir, 'vt-sentinel-agent.json')).isFile();
368
+ }
369
+ catch {
370
+ return false;
371
+ }
372
+ }
373
+ /**
374
+ * Static security-audit collector. Pass this to OpenClaw via the plugin's
375
+ * module-level default export (`securityAuditCollectors: [vtSentinelAuditCollector]`).
376
+ */
377
+ function vtSentinelAuditCollector(ctx) {
378
+ try {
379
+ const staticCfg = extractPluginConfig(ctx.config) ?? extractPluginConfig(ctx.sourceConfig);
380
+ const cm = new config_manager_1.ConfigManager(staticCfg);
381
+ const eff = cm.getEffective();
382
+ const hasUserKey = typeof staticCfg?.['apiKey'] === 'string' && staticCfg['apiKey'].trim().length > 0;
383
+ const hasCachedVtai = credentialsFileExists(ctx.stateDir);
384
+ const credentialMode = hasUserKey ? 'user_key' : (hasCachedVtai ? 'vtai' : 'none');
385
+ const watchDirs = Array.isArray(eff.watchDirs) ? [...eff.watchDirs] : [];
386
+ const snap = buildComplianceSnapshot({
387
+ config: eff,
388
+ stateDir: ctx.stateDir,
389
+ credentialMode,
390
+ watchDirs,
391
+ agentPublicHandle: readAgentPublicHandle(ctx.stateDir),
392
+ logModes: collectLogModes(ctx.stateDir),
393
+ });
394
+ return [...snap.baseline, ...snap.risks];
395
+ }
396
+ catch (err) {
397
+ return [{
398
+ checkId: 'vt-sentinel.audit-collector-error',
399
+ severity: 'warn',
400
+ title: 'VT Sentinel compliance snapshot failed to build',
401
+ detail: `Collector threw while assembling audit findings: ${err?.message || String(err)}`,
402
+ }];
403
+ }
404
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { SensitiveFilePolicy } from './scanner';
2
2
  import { getCurrentVersion } from './version';
3
3
  import { generateUpdateCommands } from './update-commands';
4
+ import { vtSentinelAuditCollector } from './compliance-snapshot';
4
5
  interface VTSentinelConfig {
5
6
  apiKey?: string;
6
7
  watchDirs?: string[];
@@ -77,10 +78,34 @@ declare function buildEnhancedBio(eff: {
77
78
  configPreset?: string;
78
79
  autoScan?: boolean;
79
80
  }): string;
80
- export default function vtSentinelPlugin(api: PluginApi): void;
81
+ declare function vtSentinelPlugin(api: PluginApi): void;
81
82
  export declare const _generateUpdateCommands: typeof generateUpdateCommands;
82
83
  export declare const _fetchLatestVersion: typeof fetchLatestVersion;
83
84
  export declare const _getCurrentVersion: typeof getCurrentVersion;
84
85
  export declare const _generateAgentName: typeof generateAgentName;
85
86
  export declare const _buildEnhancedBio: typeof buildEnhancedBio;
86
- export {};
87
+ /**
88
+ * Module-level plugin definition.
89
+ *
90
+ * `register(api)` runs inside the gateway — it's where the plugin wires up
91
+ * tools, hooks, the service, the runtime security-audit collector, and
92
+ * everything else that needs access to the live gateway API.
93
+ *
94
+ * `securityAuditCollectors` is read by `openclaw security audit --deep` in
95
+ * a FRESH Node process that does NOT share state with the gateway. That
96
+ * collector (`vtSentinelAuditCollector`) is self-contained: it rebuilds the
97
+ * compliance snapshot from ctx alone. Keeping it at module level is how
98
+ * plugin-declared findings land in `security audit` output.
99
+ *
100
+ * The dual-path wiring (runtime via `api.registerSecurityAuditCollector`
101
+ * inside `register`, static via `securityAuditCollectors` here) lets both
102
+ * the in-gateway audit flow and the out-of-process CLI flow surface the
103
+ * same data.
104
+ */
105
+ declare const _default: {
106
+ id: string;
107
+ name: string;
108
+ register: typeof vtSentinelPlugin;
109
+ securityAuditCollectors: (typeof vtSentinelAuditCollector)[];
110
+ };
111
+ export default _default;
package/dist/index.js CHANGED
@@ -39,7 +39,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports._buildEnhancedBio = exports._generateAgentName = exports._getCurrentVersion = exports._fetchLatestVersion = exports._generateUpdateCommands = void 0;
40
40
  exports.isNewerVersion = isNewerVersion;
41
41
  exports.isSelfPath = isSelfPath;
42
- exports.default = vtSentinelPlugin;
43
42
  const axios_1 = __importDefault(require("axios"));
44
43
  const chokidar = __importStar(require("chokidar"));
45
44
  const fs = __importStar(require("fs"));
@@ -360,6 +359,26 @@ function vtSentinelPlugin(api) {
360
359
  const logDir = (0, audit_log_1.getLogDir)(resolvedStateDir);
361
360
  const uploadLog = new audit_log_1.AuditLog(path.join(logDir, 'uploads.log'));
362
361
  const detectionLog = new audit_log_1.AuditLog(path.join(logDir, 'detections.log'));
362
+ // v0.12.2: pre-0.12.0 installs left `vt-sentinel-uploads.log` and
363
+ // `vt-sentinel-detections.log` at the stateDir root with the process
364
+ // umask (typically 0o664). Upgrading doesn't rewrite them. Best-effort
365
+ // tighten-to-0o600 on load so operators who upgraded in place don't
366
+ // keep world-readable audit history. POSIX-only — no-op on Windows.
367
+ if (process.platform !== 'win32') {
368
+ for (const legacyName of ['vt-sentinel-uploads.log', 'vt-sentinel-detections.log']) {
369
+ const legacyPath = path.join(resolvedStateDir, legacyName);
370
+ try {
371
+ if (fs.existsSync(legacyPath)) {
372
+ const mode = fs.statSync(legacyPath).mode & 0o777;
373
+ if (mode !== 0o600) {
374
+ fs.chmodSync(legacyPath, 0o600);
375
+ api.logger.info(`[VT-Sentinel] Tightened permissions on legacy audit log ${legacyPath} (0o${mode.toString(8)} → 0o600)`);
376
+ }
377
+ }
378
+ }
379
+ catch { /* best-effort */ }
380
+ }
381
+ }
363
382
  /** Log a scan result to the appropriate audit log(s). */
364
383
  const auditResult = (result) => {
365
384
  if (!result.sha256)
@@ -765,6 +784,7 @@ function vtSentinelPlugin(api) {
765
784
  watchDirs: [...watchRoots],
766
785
  agentPublicHandle: (0, vt_api_1.loadAgentCredentials)()?.publicHandle,
767
786
  logModes,
787
+ source: 'runtime',
768
788
  });
769
789
  return [...snap.baseline, ...snap.risks].map(f => ({
770
790
  checkId: f.checkId,
@@ -959,6 +979,7 @@ function vtSentinelPlugin(api) {
959
979
  watchDirs: [...watchRoots],
960
980
  agentPublicHandle: (0, vt_api_1.loadAgentCredentials)()?.publicHandle,
961
981
  logModes: (0, compliance_snapshot_1.collectLogModes)(resolvedStateDir),
982
+ source: 'runtime',
962
983
  });
963
984
  return textResponse((0, status_renderer_1.renderStatus)({
964
985
  version: (0, version_1.getCurrentVersion)(),
@@ -1612,3 +1633,27 @@ exports._fetchLatestVersion = fetchLatestVersion;
1612
1633
  exports._getCurrentVersion = version_1.getCurrentVersion;
1613
1634
  exports._generateAgentName = generateAgentName;
1614
1635
  exports._buildEnhancedBio = buildEnhancedBio;
1636
+ /**
1637
+ * Module-level plugin definition.
1638
+ *
1639
+ * `register(api)` runs inside the gateway — it's where the plugin wires up
1640
+ * tools, hooks, the service, the runtime security-audit collector, and
1641
+ * everything else that needs access to the live gateway API.
1642
+ *
1643
+ * `securityAuditCollectors` is read by `openclaw security audit --deep` in
1644
+ * a FRESH Node process that does NOT share state with the gateway. That
1645
+ * collector (`vtSentinelAuditCollector`) is self-contained: it rebuilds the
1646
+ * compliance snapshot from ctx alone. Keeping it at module level is how
1647
+ * plugin-declared findings land in `security audit` output.
1648
+ *
1649
+ * The dual-path wiring (runtime via `api.registerSecurityAuditCollector`
1650
+ * inside `register`, static via `securityAuditCollectors` here) lets both
1651
+ * the in-gateway audit flow and the out-of-process CLI flow surface the
1652
+ * same data.
1653
+ */
1654
+ exports.default = {
1655
+ id: 'openclaw-plugin-vt-sentinel',
1656
+ name: 'VT Sentinel',
1657
+ register: vtSentinelPlugin,
1658
+ securityAuditCollectors: [compliance_snapshot_1.vtSentinelAuditCollector],
1659
+ };
@@ -224,7 +224,7 @@ function renderHelp() {
224
224
  lines.push(' - Files read by the agent (read tool) are hash-checked only, never auto-uploaded.');
225
225
  lines.push(' - Session memory files are NEVER uploaded (privacy protection).');
226
226
  lines.push(' - autoScan=false disables hook scanning (active blocking remains on).');
227
- lines.push(' - Audit logs: ~/.openclaw/vt-sentinel-uploads.log + vt-sentinel-detections.log');
227
+ lines.push(' - Audit logs: <stateDir>/vt-sentinel-audit/uploads.log + detections.log (rotating; dir 0o700, files 0o600).');
228
228
  return lines.join('\n');
229
229
  }
230
230
  // --- Config Change Result ---
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-plugin-vt-sentinel",
3
3
  "name": "VT Sentinel",
4
4
  "description": "VirusTotal Sentinel for OpenClaw — malware detection, active protection, and AI-powered code analysis.",
5
- "version": "0.12.0",
5
+ "version": "0.12.2",
6
6
  "skills": ["./skills"],
7
7
  "contracts": {
8
8
  "tools": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-plugin-vt-sentinel",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "displayName": "VT Sentinel",
5
5
  "description": "VirusTotal Sentinel for OpenClaw - Malware detection and AI-powered code analysis",
6
6
  "main": "dist/index.js",