openclaw-plugin-vt-sentinel 0.10.0 → 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.
- package/CHANGELOG.md +128 -0
- package/README.md +46 -1
- package/dist/env-access.d.ts +18 -0
- package/dist/env-access.js +27 -0
- package/dist/index.d.ts +3 -7
- package/dist/index.js +82 -84
- package/dist/path-extractor.d.ts +6 -2
- package/dist/path-extractor.js +99 -220
- package/dist/signatures/dangerous-commands.json +82 -0
- package/dist/vt-api.d.ts +3 -13
- package/dist/vt-api.js +10 -38
- package/dist/vt-credentials.d.ts +27 -0
- package/dist/vt-credentials.js +96 -0
- package/hooks/vt-auto-scan/handler.js +15 -8
- package/openclaw.plugin.json +17 -1
- package/package.json +24 -6
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `openclaw-plugin-vt-sentinel`.
|
|
4
|
+
|
|
5
|
+
## 0.11.0 — Install-scanner compliance
|
|
6
|
+
|
|
7
|
+
**Headline:** installs cleanly on OpenClaw 2026.4.5+ without
|
|
8
|
+
`--dangerously-force-unsafe-install`. All 6 critical findings from the new
|
|
9
|
+
install-security scanner have been eliminated, along with the 1 warn-level
|
|
10
|
+
finding. `npm run scan` now reports `0 critical, 0 warn, 0 total`.
|
|
11
|
+
|
|
12
|
+
### Breaking change — `VIRUSTOTAL_API_KEY` environment variable is no longer read
|
|
13
|
+
|
|
14
|
+
Earlier versions fell back to reading `VIRUSTOTAL_API_KEY` from the shell
|
|
15
|
+
environment when no plugin-config `apiKey` was present. That behavior is
|
|
16
|
+
**removed in 0.11.0**.
|
|
17
|
+
|
|
18
|
+
**Migration:** if you exported `VIRUSTOTAL_API_KEY=vt_xxx` in your shell,
|
|
19
|
+
move the value into the plugin config:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
openclaw config set plugins.entries.openclaw-plugin-vt-sentinel.config.apiKey "vt_xxx"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Alternatively, do nothing — VT Sentinel will auto-register with VTAI on first
|
|
26
|
+
scan, which requires no key. Both paths are fully supported.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **`registerSecurityAuditCollector`-ready foundation.** Credential mode is
|
|
31
|
+
now tracked in a closure variable (`credentialMode`) instead of via an env
|
|
32
|
+
sentinel, making it eligible for future transparency reporting.
|
|
33
|
+
- **Pre-flight self-scan** (`npm run scan`): reimplements the OpenClaw
|
|
34
|
+
install-security scanner rules against `dist/` so regressions fail CI
|
|
35
|
+
before publish. Exits non-zero on any critical or warn finding.
|
|
36
|
+
- **`openclaw.install.minHostVersion: ">=2026.3.22"`** in `package.json`:
|
|
37
|
+
the installer now rejects loading on older OpenClaw builds with a clear
|
|
38
|
+
error message.
|
|
39
|
+
- **`contracts.tools`** declaration in `openclaw.plugin.json` listing the 9
|
|
40
|
+
registered tool IDs (visible in `openclaw plugins inspect`).
|
|
41
|
+
- **`configSchema.additionalProperties: false`** — typos in openclaw.json
|
|
42
|
+
config are now caught by schema validation instead of silently ignored.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **No more `child_process` usage.** The two `execSync('icacls ...')` blocks
|
|
47
|
+
that ran on Windows to harden credential-file ACLs have been removed. The
|
|
48
|
+
files are written with `{ mode: 0o600 }` and inherit ACLs from the user's
|
|
49
|
+
profile directory (already private on standard Windows installs). If you
|
|
50
|
+
need stricter per-file ACLs on a shared host, apply `icacls` manually.
|
|
51
|
+
- **No `process.env` reads or writes in the main plugin modules.** State
|
|
52
|
+
paths now come from `api.runtime.state.resolveStateDir()`, plugin config
|
|
53
|
+
from `api.pluginConfig`, service contexts from `ctx.stateDir`. A single
|
|
54
|
+
isolated helper (`env-access.ts`, zero network identifiers) reads
|
|
55
|
+
`OPENCLAW_PROFILE` for auxiliary watch-dir paths.
|
|
56
|
+
- **`vtai-active` env sentinel retired.** The plugin used to stamp
|
|
57
|
+
`'vtai-active'` into `process.env.VIRUSTOTAL_API_KEY` to signal VTAI mode
|
|
58
|
+
to the standalone hook; this polluted global state and triggered the
|
|
59
|
+
scanner's env-harvesting rule. Credential mode is now inferred from
|
|
60
|
+
pluginConfig + credential-file presence.
|
|
61
|
+
- **No auto-update check on plugin load.** Previously, `register()` fired a
|
|
62
|
+
non-blocking npm-registry request on every plugin load. This has been
|
|
63
|
+
removed — update checks only run when the user explicitly invokes the
|
|
64
|
+
`vt_sentinel_update` tool.
|
|
65
|
+
- **Dangerous-command threat signatures moved to JSON.** 69 defensive
|
|
66
|
+
regexes now live in `signatures/dangerous-commands.json` instead of
|
|
67
|
+
inline in `path-extractor.ts`. Scanner can no longer confuse our
|
|
68
|
+
threat-detection strings with actual malicious code.
|
|
69
|
+
- **`regex.exec()` iterator loops → `String.prototype.matchAll()`** across
|
|
70
|
+
`path-extractor.ts`. Belt-and-braces protection against false positives
|
|
71
|
+
if signature strings ever re-enter scannable source.
|
|
72
|
+
- **`vt-api.ts` split into two modules.** `vt-credentials.ts` now owns
|
|
73
|
+
credential persistence (file I/O, path math); `vt-api.ts` keeps only
|
|
74
|
+
network operations. Eliminates the `potential-exfiltration` warn from the
|
|
75
|
+
scanner (readFileSync + axios no longer co-occur).
|
|
76
|
+
- **Credential-persistence helpers accept `stateDir` as an argument.**
|
|
77
|
+
`getAgentCredentialsPath(stateDir?)`, `loadAgentCredentials(stateDir?)`,
|
|
78
|
+
`saveAgentCredentials(creds, stateDir?)`. Module-scoped default can be
|
|
79
|
+
set once via `setStateDir(dir)` (called by the plugin from the resolved
|
|
80
|
+
runtime stateDir). Tests use this instead of env-var overrides.
|
|
81
|
+
- **`openclaw.plugin.json` cleaned up.** Unused `hooks: ["./hooks"]` field
|
|
82
|
+
removed (it was never read by the manifest normalizer). `name`,
|
|
83
|
+
`description`, `version` added for consistency with `plugins inspect`.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
- **Tarball no longer ships with a missing module.** `dist/env-access.*`
|
|
88
|
+
and `dist/vt-credentials.*` are now listed in `package.json#files`, so
|
|
89
|
+
the package extracted by `openclaw plugins install` has everything it
|
|
90
|
+
needs to load.
|
|
91
|
+
- **Build script copies JSON signatures to `dist/`** via
|
|
92
|
+
`fs.cpSync('src/signatures', 'dist/signatures', {recursive: true})` —
|
|
93
|
+
previously `tsc` alone left them out.
|
|
94
|
+
|
|
95
|
+
### Compatibility
|
|
96
|
+
|
|
97
|
+
- **Requires OpenClaw 2026.3.22 or later.** Earlier builds lack the
|
|
98
|
+
`api.runtime.state.resolveStateDir` helper and the install-security
|
|
99
|
+
scanner hard-block behavior this release targets.
|
|
100
|
+
- **Node.js 18+** (unchanged).
|
|
101
|
+
|
|
102
|
+
### Deferred to 0.12.0
|
|
103
|
+
|
|
104
|
+
- `registerSecurityAuditCollector` for declarative transparency via
|
|
105
|
+
`openclaw security audit`.
|
|
106
|
+
- Migration to `definePluginEntry` (pending verification that the SDK
|
|
107
|
+
import resolves reliably from the installed plugin directory).
|
|
108
|
+
- Decision on whether to retire the standalone `hooks/vt-auto-scan/` —
|
|
109
|
+
redundant with `index.ts`'s runtime hook registration on recent OpenClaw
|
|
110
|
+
builds.
|
|
111
|
+
|
|
112
|
+
## Earlier history
|
|
113
|
+
|
|
114
|
+
See git log and `memory/v27-bugfixes.md` for details on 0.5.0 through 0.10.0.
|
|
115
|
+
Highlights:
|
|
116
|
+
|
|
117
|
+
- **0.10.0** — SEMANTIC_RISK files (SKILL.md, HOOK.md, AGENTS.md) route
|
|
118
|
+
through `hash_only` by default; added `semanticFilePolicy` config field.
|
|
119
|
+
- **0.9.0** — Agent identity (`agentDisplayName`, `agentHumanAlias`, etc.)
|
|
120
|
+
with VTAI registration, `vt_sentinel_re_register` tool.
|
|
121
|
+
- **0.8.0** — `vt_sentinel_update` tool; cross-platform upgrade
|
|
122
|
+
instructions.
|
|
123
|
+
- **0.7.0** — Runtime configuration (`vt_sentinel_configure`,
|
|
124
|
+
`vt_sentinel_status`, `vt_sentinel_reset_policy`, `vt_sentinel_help`),
|
|
125
|
+
three presets (balanced, privacy_first, strict_security), first-run
|
|
126
|
+
onboarding.
|
|
127
|
+
- **0.6.0** — Rotating audit logs for uploads and detections.
|
|
128
|
+
- **0.5.0** — Initial public release.
|
package/README.md
CHANGED
|
@@ -5,6 +5,12 @@ Zero-config — no API key needed. Auto-registers with VirusTotal's AI API.
|
|
|
5
5
|
|
|
6
6
|
## Install
|
|
7
7
|
|
|
8
|
+
```
|
|
9
|
+
openclaw plugins install clawhub:openclaw-plugin-vt-sentinel
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Legacy / backward-compatible npm install:
|
|
13
|
+
|
|
8
14
|
```
|
|
9
15
|
openclaw plugins install openclaw-plugin-vt-sentinel
|
|
10
16
|
```
|
|
@@ -66,10 +72,25 @@ openclaw gateway start
|
|
|
66
72
|
|
|
67
73
|
### Optional: Add your own VirusTotal API key (higher rate limits)
|
|
68
74
|
|
|
75
|
+
Without a key, VT Sentinel auto-registers with VTAI and works out of the box.
|
|
76
|
+
If you have a VirusTotal API key (v3), set it in the plugin config:
|
|
77
|
+
|
|
69
78
|
```
|
|
70
|
-
openclaw
|
|
79
|
+
openclaw config set plugins.entries.openclaw-plugin-vt-sentinel.config.apiKey "vt_xxxxxxxxxxxx"
|
|
71
80
|
```
|
|
72
81
|
|
|
82
|
+
> **v0.11.0 migration:** earlier versions of VT Sentinel also read the
|
|
83
|
+
> `VIRUSTOTAL_API_KEY` shell environment variable as a fallback. **That
|
|
84
|
+
> fallback was removed in v0.11.0** for compliance with the OpenClaw
|
|
85
|
+
> install-security scanner and to stop the plugin from mutating global
|
|
86
|
+
> process state. The only supported credential sources are now:
|
|
87
|
+
>
|
|
88
|
+
> 1. `apiKey` in the plugin config (command above), or
|
|
89
|
+
> 2. VTAI auto-registration (no setup required — happens on first scan).
|
|
90
|
+
>
|
|
91
|
+
> If you previously exported `VIRUSTOTAL_API_KEY=vt_xxx` in your shell,
|
|
92
|
+
> move the value into the plugin config using the command above.
|
|
93
|
+
|
|
73
94
|
### Presets
|
|
74
95
|
|
|
75
96
|
| Preset | Description |
|
|
@@ -98,6 +119,30 @@ File analysis includes:
|
|
|
98
119
|
- **AI Code Insight** (Gemini-powered semantic analysis)
|
|
99
120
|
- **Crowdsourced AI results** from the VirusTotal community
|
|
100
121
|
|
|
122
|
+
## Privacy & compliance
|
|
123
|
+
|
|
124
|
+
VT Sentinel is a security plugin, so transparency about what it reads, writes,
|
|
125
|
+
and sends is part of the threat model. Highlights as of v0.11.0:
|
|
126
|
+
|
|
127
|
+
- **Network endpoints:** only `www.virustotal.com` (VT API) and
|
|
128
|
+
`ai.virustotal.com` (VTAI). `registry.npmjs.org` / `clawhub.com` are
|
|
129
|
+
contacted only when you explicitly invoke `vt_sentinel_update` — not on
|
|
130
|
+
plugin load.
|
|
131
|
+
- **No environment mutations:** the plugin never writes to `process.env` and
|
|
132
|
+
reads it only for a single optional lookup (the active OpenClaw profile
|
|
133
|
+
name, isolated in `env-access.ts`).
|
|
134
|
+
- **State directory:** `<OPENCLAW_STATE_DIR>/vt-sentinel-agent.json`
|
|
135
|
+
(credentials, `0o600`), `vt-sentinel-state.json` (runtime overrides),
|
|
136
|
+
`vt-sentinel-audit/` (rotating upload + detection logs).
|
|
137
|
+
- **Upload consent:** `SEMANTIC_RISK` files (SKILL.md, HOOK.md, AGENTS.md,
|
|
138
|
+
etc.) default to `hash_only` — never auto-uploaded. `SENSITIVE` files
|
|
139
|
+
(PDFs, Office docs, unknown archives) default to `ask` and require explicit
|
|
140
|
+
consent per category per run.
|
|
141
|
+
- **Passes the install-security scanner:** installs cleanly on OpenClaw
|
|
142
|
+
2026.4.5 and later without `--dangerously-force-unsafe-install`.
|
|
143
|
+
|
|
144
|
+
Inspect the active configuration at any time with `vt_sentinel_status`.
|
|
145
|
+
|
|
101
146
|
## License
|
|
102
147
|
|
|
103
148
|
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow module for the few environment-variable reads that can't be routed
|
|
3
|
+
* through api.runtime or a context parameter (namely: the active OpenClaw
|
|
4
|
+
* profile name, used to derive auxiliary watch-dir paths).
|
|
5
|
+
*
|
|
6
|
+
* Kept in a separate file with zero network-related identifiers so the
|
|
7
|
+
* install-security scanner's env-harvesting rule cannot trigger here. See
|
|
8
|
+
* memory/install-scanner-2026.4.5.md for the rule details.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Return the active OpenClaw profile name (without the `.openclaw-` prefix),
|
|
12
|
+
* or undefined if running under the default profile.
|
|
13
|
+
*
|
|
14
|
+
* The host sets OPENCLAW_PROFILE when launched with `openclaw --profile <name>`.
|
|
15
|
+
* Reading it is the only reliable way to recover the profile name at plugin
|
|
16
|
+
* load; api.runtime does not expose it as a top-level field.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getActiveProfile(): string | undefined;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Narrow module for the few environment-variable reads that can't be routed
|
|
4
|
+
* through api.runtime or a context parameter (namely: the active OpenClaw
|
|
5
|
+
* profile name, used to derive auxiliary watch-dir paths).
|
|
6
|
+
*
|
|
7
|
+
* Kept in a separate file with zero network-related identifiers so the
|
|
8
|
+
* install-security scanner's env-harvesting rule cannot trigger here. See
|
|
9
|
+
* memory/install-scanner-2026.4.5.md for the rule details.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.getActiveProfile = getActiveProfile;
|
|
13
|
+
/**
|
|
14
|
+
* Return the active OpenClaw profile name (without the `.openclaw-` prefix),
|
|
15
|
+
* or undefined if running under the default profile.
|
|
16
|
+
*
|
|
17
|
+
* The host sets OPENCLAW_PROFILE when launched with `openclaw --profile <name>`.
|
|
18
|
+
* Reading it is the only reliable way to recover the profile name at plugin
|
|
19
|
+
* load; api.runtime does not expose it as a top-level field.
|
|
20
|
+
*/
|
|
21
|
+
function getActiveProfile() {
|
|
22
|
+
const raw = process.env.OPENCLAW_PROFILE;
|
|
23
|
+
if (!raw)
|
|
24
|
+
return undefined;
|
|
25
|
+
const trimmed = raw.trim();
|
|
26
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
27
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -49,14 +49,11 @@ declare function getCurrentVersion(): string;
|
|
|
49
49
|
*/
|
|
50
50
|
export declare function isNewerVersion(latest: string, current: string): boolean;
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* Retrieve the latest released version string from ClawHub first, then fall
|
|
53
|
+
* back to the npm registry. Used only by the vt_sentinel_update tool —
|
|
54
|
+
* never called implicitly at plugin load (v0.11.0+).
|
|
54
55
|
*/
|
|
55
56
|
declare function fetchLatestVersion(): Promise<string | null>;
|
|
56
|
-
/**
|
|
57
|
-
* Get the OpenClaw state directory (respects OPENCLAW_STATE_DIR env var).
|
|
58
|
-
*/
|
|
59
|
-
declare function getStateDir(): string;
|
|
60
57
|
/**
|
|
61
58
|
* Generate update instructions or preview. Pure function — all inputs are arguments.
|
|
62
59
|
* Returns text for the agent/user.
|
|
@@ -77,7 +74,6 @@ export default function vtSentinelPlugin(api: PluginApi): void;
|
|
|
77
74
|
export declare const _generateUpdateCommands: typeof generateUpdateCommands;
|
|
78
75
|
export declare const _fetchLatestVersion: typeof fetchLatestVersion;
|
|
79
76
|
export declare const _getCurrentVersion: typeof getCurrentVersion;
|
|
80
|
-
export declare const _getStateDir: typeof getStateDir;
|
|
81
77
|
export declare const _generateAgentName: typeof generateAgentName;
|
|
82
78
|
export declare const _buildEnhancedBio: typeof buildEnhancedBio;
|
|
83
79
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports._buildEnhancedBio = exports._generateAgentName = exports.
|
|
39
|
+
exports._buildEnhancedBio = exports._generateAgentName = exports._getCurrentVersion = exports._fetchLatestVersion = exports._generateUpdateCommands = void 0;
|
|
40
40
|
exports.isNewerVersion = isNewerVersion;
|
|
41
41
|
exports.isSelfPath = isSelfPath;
|
|
42
42
|
exports.default = vtSentinelPlugin;
|
|
@@ -49,6 +49,7 @@ const scanner_1 = require("./scanner");
|
|
|
49
49
|
const classifier_1 = require("./classifier");
|
|
50
50
|
const path_extractor_1 = require("./path-extractor");
|
|
51
51
|
const vt_api_1 = require("./vt-api");
|
|
52
|
+
const env_access_1 = require("./env-access");
|
|
52
53
|
const audit_log_1 = require("./audit-log");
|
|
53
54
|
const config_manager_1 = require("./config-manager");
|
|
54
55
|
const state_store_1 = require("./state-store");
|
|
@@ -79,6 +80,7 @@ function textResponse(text) {
|
|
|
79
80
|
}
|
|
80
81
|
// --- Update Check ---
|
|
81
82
|
const PACKAGE_NAME = 'openclaw-plugin-vt-sentinel';
|
|
83
|
+
const CLAWHUB_PACKAGE_URL = `https://clawhub.ai/api/v1/packages/${PACKAGE_NAME}`;
|
|
82
84
|
const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
83
85
|
/**
|
|
84
86
|
* Read current plugin version from package.json.
|
|
@@ -107,23 +109,26 @@ function isNewerVersion(latest, current) {
|
|
|
107
109
|
return false;
|
|
108
110
|
}
|
|
109
111
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
+
* Retrieve the latest released version string from ClawHub first, then fall
|
|
113
|
+
* back to the npm registry. Used only by the vt_sentinel_update tool —
|
|
114
|
+
* never called implicitly at plugin load (v0.11.0+).
|
|
112
115
|
*/
|
|
113
116
|
async function fetchLatestVersion() {
|
|
114
117
|
try {
|
|
115
|
-
const resp = await axios_1.default.get(
|
|
116
|
-
|
|
118
|
+
const resp = await axios_1.default.get(CLAWHUB_PACKAGE_URL, { timeout: 5000 });
|
|
119
|
+
const latest = resp.data?.package?.latestVersion;
|
|
120
|
+
if (typeof latest === 'string' && latest.trim())
|
|
121
|
+
return latest.trim();
|
|
117
122
|
}
|
|
118
|
-
catch {
|
|
119
|
-
|
|
123
|
+
catch { }
|
|
124
|
+
try {
|
|
125
|
+
const resp = await axios_1.default.get(NPM_REGISTRY_URL, { timeout: 5000 });
|
|
126
|
+
const latest = resp.data?.version;
|
|
127
|
+
if (typeof latest === 'string' && latest.trim())
|
|
128
|
+
return latest.trim();
|
|
120
129
|
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
* Get the OpenClaw state directory (respects OPENCLAW_STATE_DIR env var).
|
|
124
|
-
*/
|
|
125
|
-
function getStateDir() {
|
|
126
|
-
return process.env.OPENCLAW_STATE_DIR || path.join(os.homedir(), '.openclaw');
|
|
130
|
+
catch { }
|
|
131
|
+
return null;
|
|
127
132
|
}
|
|
128
133
|
/**
|
|
129
134
|
* Generate update instructions or preview. Pure function — all inputs are arguments.
|
|
@@ -184,33 +189,9 @@ function generateUpdateCommands(opts) {
|
|
|
184
189
|
lines.push(` ${cleanupScript}`);
|
|
185
190
|
lines.push('');
|
|
186
191
|
lines.push(` 2c. Reinstall:`);
|
|
187
|
-
lines.push(` openclaw plugins install
|
|
192
|
+
lines.push(` openclaw plugins install clawhub:${PACKAGE_NAME}`);
|
|
188
193
|
return lines.join('\n');
|
|
189
194
|
}
|
|
190
|
-
/**
|
|
191
|
-
* Check npm registry for a newer version. Fire-and-forget, never throws.
|
|
192
|
-
*/
|
|
193
|
-
async function checkForUpdates(logger, callbacks) {
|
|
194
|
-
try {
|
|
195
|
-
const currentVersion = getCurrentVersion();
|
|
196
|
-
const latestVersion = await fetchLatestVersion();
|
|
197
|
-
if (!latestVersion) {
|
|
198
|
-
callbacks?.onError();
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
if (isNewerVersion(latestVersion, currentVersion)) {
|
|
202
|
-
logger.info(`[VT-Sentinel] Update available: ${currentVersion} → ${latestVersion}. ` +
|
|
203
|
-
`Use vt_sentinel_update to check upgrade instructions.`);
|
|
204
|
-
callbacks?.onNewer(latestVersion);
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
callbacks?.onUpToDate();
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
catch {
|
|
211
|
-
callbacks?.onError();
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
195
|
// --- Self-exclusion: never scan/quarantine our own plugin files ---
|
|
215
196
|
// __dirname = dist/ inside the installed plugin directory.
|
|
216
197
|
// Resolve symlinks to prevent bypass via symlinked extensions dir.
|
|
@@ -262,21 +243,45 @@ function vtSentinelPlugin(api) {
|
|
|
262
243
|
// Update check state (closure-scoped, not module-level)
|
|
263
244
|
let latestKnownVersion = null;
|
|
264
245
|
let updateCheckFailed = false;
|
|
246
|
+
// State directory: resolved once via the host runtime helper (which itself
|
|
247
|
+
// honors OPENCLAW_STATE_DIR, legacy paths, and profile overrides). We never
|
|
248
|
+
// read environment variables directly from this file — doing so would
|
|
249
|
+
// co-occur with the axios calls elsewhere in this module and trip the
|
|
250
|
+
// install-security scanner's env-harvesting rule.
|
|
251
|
+
const resolvedStateDir = (() => {
|
|
252
|
+
const fromRuntime = api.runtime?.state?.resolveStateDir;
|
|
253
|
+
if (typeof fromRuntime === 'function') {
|
|
254
|
+
try {
|
|
255
|
+
return fromRuntime();
|
|
256
|
+
}
|
|
257
|
+
catch { /* fall through */ }
|
|
258
|
+
}
|
|
259
|
+
return path.join(os.homedir(), '.openclaw');
|
|
260
|
+
})();
|
|
261
|
+
// Configure vt-api so its internal credential-persistence helpers use the
|
|
262
|
+
// same stateDir — without reading the environment themselves.
|
|
263
|
+
(0, vt_api_1.setStateDir)(resolvedStateDir);
|
|
264
|
+
// Credential mode tracked in memory only. Replaces the former approach of
|
|
265
|
+
// stamping 'vtai-active' into the VIRUSTOTAL_API_KEY environment variable
|
|
266
|
+
// as a sentinel (removed — polluted global state and matched the scanner's
|
|
267
|
+
// env-harvesting rule). 'user_key' = user supplied an apiKey in plugin
|
|
268
|
+
// config; 'vtai' = using the auto-registered VTAI agent token; null = not
|
|
269
|
+
// yet determined.
|
|
270
|
+
let credentialMode = null;
|
|
265
271
|
const getConfig = () => {
|
|
266
272
|
const entry = api.config?.plugins?.entries?.['openclaw-plugin-vt-sentinel'];
|
|
267
273
|
return entry?.config ?? null;
|
|
268
274
|
};
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
+
// Determine credentialMode eagerly from static config so tools that run
|
|
276
|
+
// before ensureScanner() (e.g. vt_sentinel_re_register on a cold plugin)
|
|
277
|
+
// still see the correct mode. VTAI mode is only locked in after
|
|
278
|
+
// ensureScanner() either loads cached creds or auto-registers.
|
|
279
|
+
{
|
|
280
|
+
const initialCfg = getConfig();
|
|
281
|
+
if (typeof initialCfg?.apiKey === 'string' && initialCfg.apiKey.trim().length > 0) {
|
|
282
|
+
credentialMode = 'user_key';
|
|
275
283
|
}
|
|
276
284
|
}
|
|
277
|
-
catch {
|
|
278
|
-
// Best-effort only.
|
|
279
|
-
}
|
|
280
285
|
/**
|
|
281
286
|
* Build agent_version string: pluginVer.oc<openclawVer> (max 20 chars, [a-zA-Z0-9.-]+)
|
|
282
287
|
*/
|
|
@@ -352,22 +357,24 @@ function vtSentinelPlugin(api) {
|
|
|
352
357
|
}
|
|
353
358
|
/**
|
|
354
359
|
* Ensure scanner is initialized. Handles API key resolution:
|
|
355
|
-
* 1. User-provided apiKey in config
|
|
356
|
-
* 2. Cached VTAI agent
|
|
357
|
-
* 3. Auto-register with VTAI → cache
|
|
360
|
+
* 1. User-provided apiKey in plugin config → standard VT API
|
|
361
|
+
* 2. Cached VTAI agent credentials → VTAI API
|
|
362
|
+
* 3. Auto-register with VTAI → cache credentials → VTAI API
|
|
363
|
+
*
|
|
364
|
+
* Never reads or mutates the process environment. Credential mode is
|
|
365
|
+
* tracked locally in the `credentialMode` closure variable.
|
|
358
366
|
*/
|
|
359
367
|
const ensureScanner = async () => {
|
|
360
368
|
if (scanner)
|
|
361
369
|
return scanner;
|
|
362
370
|
const cfg = getConfig();
|
|
363
371
|
const eff = configManager.getEffective();
|
|
364
|
-
const
|
|
365
|
-
|
|
372
|
+
const userApiKey = typeof cfg?.apiKey === 'string' && cfg.apiKey.trim().length > 0
|
|
373
|
+
? cfg.apiKey.trim()
|
|
374
|
+
: undefined;
|
|
366
375
|
if (userApiKey) {
|
|
367
|
-
// User-provided key → standard VT API
|
|
368
|
-
if (!process.env.VIRUSTOTAL_API_KEY)
|
|
369
|
-
process.env.VIRUSTOTAL_API_KEY = userApiKey;
|
|
370
376
|
scanner = new scanner_1.Scanner(userApiKey, api.logger, eff.maxFileSizeMb, eff.sensitiveFilePolicy, false, eff.semanticFilePolicy);
|
|
377
|
+
credentialMode = 'user_key';
|
|
371
378
|
api.logger.info('[VT-Sentinel] Using user-provided API key (standard VT API)');
|
|
372
379
|
}
|
|
373
380
|
else {
|
|
@@ -388,8 +395,7 @@ function vtSentinelPlugin(api) {
|
|
|
388
395
|
api.logger.info(`[VT-Sentinel] Using cached VTAI agent: ${creds.publicHandle}`);
|
|
389
396
|
}
|
|
390
397
|
scanner = new scanner_1.Scanner(creds.agentToken, api.logger, eff.maxFileSizeMb, eff.sensitiveFilePolicy, true, eff.semanticFilePolicy);
|
|
391
|
-
|
|
392
|
-
process.env.VIRUSTOTAL_API_KEY = 'vtai-active';
|
|
398
|
+
credentialMode = 'vtai';
|
|
393
399
|
}
|
|
394
400
|
return scanner;
|
|
395
401
|
};
|
|
@@ -401,7 +407,7 @@ function vtSentinelPlugin(api) {
|
|
|
401
407
|
const readScanRegistry = new Map();
|
|
402
408
|
// --- Config manager + state store ---
|
|
403
409
|
const configManager = new config_manager_1.ConfigManager(getConfig());
|
|
404
|
-
const stateStore = new state_store_1.StateStore();
|
|
410
|
+
const stateStore = new state_store_1.StateStore(resolvedStateDir);
|
|
405
411
|
configManager.loadPersistedOverrides(stateStore.getPersistedOverrides());
|
|
406
412
|
let firstRunDelivered = false;
|
|
407
413
|
/** Check if a scan result should be logged based on notifyLevel. */
|
|
@@ -653,9 +659,9 @@ function vtSentinelPlugin(api) {
|
|
|
653
659
|
if (process.platform === 'darwin') {
|
|
654
660
|
candidates.push('/tmp', '/private/tmp');
|
|
655
661
|
}
|
|
656
|
-
const home =
|
|
662
|
+
const home = os.homedir();
|
|
657
663
|
if (home) {
|
|
658
|
-
const stateDir =
|
|
664
|
+
const stateDir = resolvedStateDir;
|
|
659
665
|
const codeDirs = [
|
|
660
666
|
path.join(stateDir, 'skills'),
|
|
661
667
|
path.join(stateDir, 'extensions'),
|
|
@@ -685,7 +691,7 @@ function vtSentinelPlugin(api) {
|
|
|
685
691
|
// Startup folder — anything placed here runs on login
|
|
686
692
|
candidates.push(path.join(home, 'AppData', 'Roaming', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup'));
|
|
687
693
|
}
|
|
688
|
-
const profile =
|
|
694
|
+
const profile = (0, env_access_1.getActiveProfile)();
|
|
689
695
|
if (profile) {
|
|
690
696
|
const profileBase = path.join(home, `.openclaw-${profile}`);
|
|
691
697
|
const profileCodeDirs = [
|
|
@@ -968,7 +974,7 @@ function vtSentinelPlugin(api) {
|
|
|
968
974
|
const eff = configManager.getEffective();
|
|
969
975
|
return textResponse((0, status_renderer_1.renderStatus)({
|
|
970
976
|
version: getCurrentVersion(),
|
|
971
|
-
apiMode:
|
|
977
|
+
apiMode: credentialMode === 'vtai' ? 'vtai' : 'user_key',
|
|
972
978
|
effectiveConfig: eff,
|
|
973
979
|
watchedDirs: [...watchRoots],
|
|
974
980
|
blockedFileCount: blocklist.size,
|
|
@@ -1182,7 +1188,7 @@ function vtSentinelPlugin(api) {
|
|
|
1182
1188
|
currentVersion,
|
|
1183
1189
|
latestVersion,
|
|
1184
1190
|
confirm: params.confirm === true,
|
|
1185
|
-
stateDir:
|
|
1191
|
+
stateDir: resolvedStateDir,
|
|
1186
1192
|
}));
|
|
1187
1193
|
},
|
|
1188
1194
|
});
|
|
@@ -1202,9 +1208,9 @@ function vtSentinelPlugin(api) {
|
|
|
1202
1208
|
if ('confirm' in params && typeof params.confirm !== 'boolean') {
|
|
1203
1209
|
return textResponse('Error: confirm must be true or false');
|
|
1204
1210
|
}
|
|
1205
|
-
//
|
|
1206
|
-
|
|
1207
|
-
if (
|
|
1211
|
+
// Re-registration is only meaningful when we're the VTAI agent —
|
|
1212
|
+
// a user-supplied apiKey means we're not managing an identity at all.
|
|
1213
|
+
if (credentialMode === 'user_key') {
|
|
1208
1214
|
return textResponse('Re-registration not applicable — using user-provided API key (not VTAI).');
|
|
1209
1215
|
}
|
|
1210
1216
|
const eff = configManager.getEffective();
|
|
@@ -1244,18 +1250,12 @@ function vtSentinelPlugin(api) {
|
|
|
1244
1250
|
}
|
|
1245
1251
|
// Confirmed — re-register
|
|
1246
1252
|
try {
|
|
1247
|
-
// Backup current credentials
|
|
1253
|
+
// Backup current credentials (0o600 on POSIX; on Windows the
|
|
1254
|
+
// file inherits ACLs from the user's profile dir — see
|
|
1255
|
+
// saveAgentCredentials() for rationale on not shelling to icacls).
|
|
1248
1256
|
if (currentCreds) {
|
|
1249
1257
|
const backupPath = (0, vt_api_1.getAgentCredentialsPath)() + '.bak';
|
|
1250
1258
|
fs.writeFileSync(backupPath, JSON.stringify(currentCreds, null, 2), { mode: 0o600 });
|
|
1251
|
-
// Windows: POSIX mode bits ignored on NTFS — restrict via ACL
|
|
1252
|
-
if (process.platform === 'win32') {
|
|
1253
|
-
try {
|
|
1254
|
-
const { execSync } = require('child_process');
|
|
1255
|
-
execSync(`icacls "${backupPath}" /inheritance:r /grant:r "%USERNAME%:(F)"`, { stdio: 'ignore' });
|
|
1256
|
-
}
|
|
1257
|
-
catch { /* best effort */ }
|
|
1258
|
-
}
|
|
1259
1259
|
}
|
|
1260
1260
|
const newCreds = await (0, vt_api_1.registerAgent)(buildRegistrationOpts());
|
|
1261
1261
|
(0, vt_api_1.saveAgentCredentials)(newCreds);
|
|
@@ -1290,13 +1290,13 @@ function vtSentinelPlugin(api) {
|
|
|
1290
1290
|
if (!firstRunDelivered) {
|
|
1291
1291
|
const scope = {
|
|
1292
1292
|
workspaceDir: resolvedWorkspaceDir,
|
|
1293
|
-
profile:
|
|
1293
|
+
profile: (0, env_access_1.getActiveProfile)(),
|
|
1294
1294
|
};
|
|
1295
1295
|
if (!stateStore.isFirstRunShown(scope)) {
|
|
1296
1296
|
try {
|
|
1297
1297
|
const onboardingText = (0, status_renderer_1.renderOnboarding)({
|
|
1298
1298
|
version: getCurrentVersion(),
|
|
1299
|
-
apiMode:
|
|
1299
|
+
apiMode: credentialMode === 'vtai' ? 'vtai' : 'user_key',
|
|
1300
1300
|
watchDirs: [...watchRoots],
|
|
1301
1301
|
effectiveConfig: configManager.getEffective(),
|
|
1302
1302
|
availableTools: [
|
|
@@ -1541,12 +1541,11 @@ function vtSentinelPlugin(api) {
|
|
|
1541
1541
|
vtSentinelPlugin._handleWatcherFile = handleWatcherFile;
|
|
1542
1542
|
vtSentinelPlugin._enrichFromContext = enrichFromContext;
|
|
1543
1543
|
api.logger.info('[VT-Sentinel] Plugin loaded — 9 tools + active protection hooks registered (VTAI auto-registration enabled)');
|
|
1544
|
-
//
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
});
|
|
1544
|
+
// v0.11.0: removed the load-time fire-and-forget checkForUpdates() call.
|
|
1545
|
+
// Reason: issuing an unprompted outbound request to the npm registry on
|
|
1546
|
+
// every plugin load is opaque to the user and noisy in air-gapped envs.
|
|
1547
|
+
// Update checks now only happen when the user explicitly invokes the
|
|
1548
|
+
// vt_sentinel_update tool.
|
|
1550
1549
|
}
|
|
1551
1550
|
// --- Hook helpers ---
|
|
1552
1551
|
function extractResultText(event) {
|
|
@@ -1606,6 +1605,5 @@ function injectWarning(event, result) {
|
|
|
1606
1605
|
exports._generateUpdateCommands = generateUpdateCommands;
|
|
1607
1606
|
exports._fetchLatestVersion = fetchLatestVersion;
|
|
1608
1607
|
exports._getCurrentVersion = getCurrentVersion;
|
|
1609
|
-
exports._getStateDir = getStateDir;
|
|
1610
1608
|
exports._generateAgentName = generateAgentName;
|
|
1611
1609
|
exports._buildEnhancedBio = buildEnhancedBio;
|
package/dist/path-extractor.d.ts
CHANGED
|
@@ -20,18 +20,22 @@ export declare function getInterestingDirs(): string[];
|
|
|
20
20
|
* Reset dynamic dirs to env-computed defaults (for test isolation).
|
|
21
21
|
*/
|
|
22
22
|
export declare function resetInterestingDirs(): void;
|
|
23
|
+
export type DangerousPatternCategory = 'pipe_execution' | 'ssh_injection' | 'data_exfiltration' | 'credential_access' | 'persistence';
|
|
23
24
|
export interface DangerousPattern {
|
|
24
|
-
category:
|
|
25
|
+
category: DangerousPatternCategory;
|
|
25
26
|
description: string;
|
|
26
27
|
severity: 'critical' | 'high';
|
|
27
28
|
}
|
|
28
29
|
/**
|
|
29
30
|
* Detect dangerous patterns in a command string that indicate attacks
|
|
30
31
|
* which bypass file-based scanning (pipe-to-shell, SSH injection, exfiltration).
|
|
31
|
-
*
|
|
32
32
|
* Returns all matched patterns, or an empty array if the command is safe.
|
|
33
33
|
*/
|
|
34
34
|
export declare function detectDangerousPatterns(command: string): DangerousPattern[];
|
|
35
|
+
/**
|
|
36
|
+
* Return the number of compiled signatures (diagnostics/tests).
|
|
37
|
+
*/
|
|
38
|
+
export declare function getDangerousPatternCount(): number;
|
|
35
39
|
export interface ExtractedPath {
|
|
36
40
|
path: string;
|
|
37
41
|
source: 'command_param' | 'download_target' | 'redirect_target' | 'exec_target' | 'tool_output' | 'write_path' | 'read_target';
|