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.
- package/CHANGELOG.md +128 -0
- package/README.md +50 -2
- package/dist/config-manager.d.ts +2 -0
- package/dist/config-manager.js +17 -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 +124 -100
- package/dist/path-extractor.d.ts +6 -2
- package/dist/path-extractor.js +99 -220
- package/dist/scanner.d.ts +19 -5
- package/dist/scanner.js +62 -14
- package/dist/signatures/dangerous-commands.json +82 -0
- package/dist/status-renderer.js +14 -4
- 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/HOOK.md +8 -0
- package/hooks/vt-auto-scan/handler.js +78 -10
- package/openclaw.plugin.json +24 -1
- package/package.json +24 -6
- package/skills/vt-sentinel/SKILL.md +6 -3
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;
|
|
@@ -46,8 +46,10 @@ const fs = __importStar(require("fs"));
|
|
|
46
46
|
const os = __importStar(require("os"));
|
|
47
47
|
const path = __importStar(require("path"));
|
|
48
48
|
const scanner_1 = require("./scanner");
|
|
49
|
+
const classifier_1 = require("./classifier");
|
|
49
50
|
const path_extractor_1 = require("./path-extractor");
|
|
50
51
|
const vt_api_1 = require("./vt-api");
|
|
52
|
+
const env_access_1 = require("./env-access");
|
|
51
53
|
const audit_log_1 = require("./audit-log");
|
|
52
54
|
const config_manager_1 = require("./config-manager");
|
|
53
55
|
const state_store_1 = require("./state-store");
|
|
@@ -78,6 +80,7 @@ function textResponse(text) {
|
|
|
78
80
|
}
|
|
79
81
|
// --- Update Check ---
|
|
80
82
|
const PACKAGE_NAME = 'openclaw-plugin-vt-sentinel';
|
|
83
|
+
const CLAWHUB_PACKAGE_URL = `https://clawhub.ai/api/v1/packages/${PACKAGE_NAME}`;
|
|
81
84
|
const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
82
85
|
/**
|
|
83
86
|
* Read current plugin version from package.json.
|
|
@@ -106,23 +109,26 @@ function isNewerVersion(latest, current) {
|
|
|
106
109
|
return false;
|
|
107
110
|
}
|
|
108
111
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
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+).
|
|
111
115
|
*/
|
|
112
116
|
async function fetchLatestVersion() {
|
|
113
117
|
try {
|
|
114
|
-
const resp = await axios_1.default.get(
|
|
115
|
-
|
|
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();
|
|
116
122
|
}
|
|
117
|
-
catch {
|
|
118
|
-
|
|
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();
|
|
119
129
|
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
* Get the OpenClaw state directory (respects OPENCLAW_STATE_DIR env var).
|
|
123
|
-
*/
|
|
124
|
-
function getStateDir() {
|
|
125
|
-
return process.env.OPENCLAW_STATE_DIR || path.join(os.homedir(), '.openclaw');
|
|
130
|
+
catch { }
|
|
131
|
+
return null;
|
|
126
132
|
}
|
|
127
133
|
/**
|
|
128
134
|
* Generate update instructions or preview. Pure function — all inputs are arguments.
|
|
@@ -183,33 +189,9 @@ function generateUpdateCommands(opts) {
|
|
|
183
189
|
lines.push(` ${cleanupScript}`);
|
|
184
190
|
lines.push('');
|
|
185
191
|
lines.push(` 2c. Reinstall:`);
|
|
186
|
-
lines.push(` openclaw plugins install
|
|
192
|
+
lines.push(` openclaw plugins install clawhub:${PACKAGE_NAME}`);
|
|
187
193
|
return lines.join('\n');
|
|
188
194
|
}
|
|
189
|
-
/**
|
|
190
|
-
* Check npm registry for a newer version. Fire-and-forget, never throws.
|
|
191
|
-
*/
|
|
192
|
-
async function checkForUpdates(logger, callbacks) {
|
|
193
|
-
try {
|
|
194
|
-
const currentVersion = getCurrentVersion();
|
|
195
|
-
const latestVersion = await fetchLatestVersion();
|
|
196
|
-
if (!latestVersion) {
|
|
197
|
-
callbacks?.onError();
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
if (isNewerVersion(latestVersion, currentVersion)) {
|
|
201
|
-
logger.info(`[VT-Sentinel] Update available: ${currentVersion} → ${latestVersion}. ` +
|
|
202
|
-
`Use vt_sentinel_update to check upgrade instructions.`);
|
|
203
|
-
callbacks?.onNewer(latestVersion);
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
callbacks?.onUpToDate();
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
callbacks?.onError();
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
195
|
// --- Self-exclusion: never scan/quarantine our own plugin files ---
|
|
214
196
|
// __dirname = dist/ inside the installed plugin directory.
|
|
215
197
|
// Resolve symlinks to prevent bypass via symlinked extensions dir.
|
|
@@ -261,21 +243,45 @@ function vtSentinelPlugin(api) {
|
|
|
261
243
|
// Update check state (closure-scoped, not module-level)
|
|
262
244
|
let latestKnownVersion = null;
|
|
263
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;
|
|
264
271
|
const getConfig = () => {
|
|
265
272
|
const entry = api.config?.plugins?.entries?.['openclaw-plugin-vt-sentinel'];
|
|
266
273
|
return entry?.config ?? null;
|
|
267
274
|
};
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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';
|
|
274
283
|
}
|
|
275
284
|
}
|
|
276
|
-
catch {
|
|
277
|
-
// Best-effort only.
|
|
278
|
-
}
|
|
279
285
|
/**
|
|
280
286
|
* Build agent_version string: pluginVer.oc<openclawVer> (max 20 chars, [a-zA-Z0-9.-]+)
|
|
281
287
|
*/
|
|
@@ -351,22 +357,24 @@ function vtSentinelPlugin(api) {
|
|
|
351
357
|
}
|
|
352
358
|
/**
|
|
353
359
|
* Ensure scanner is initialized. Handles API key resolution:
|
|
354
|
-
* 1. User-provided apiKey in config
|
|
355
|
-
* 2. Cached VTAI agent
|
|
356
|
-
* 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.
|
|
357
366
|
*/
|
|
358
367
|
const ensureScanner = async () => {
|
|
359
368
|
if (scanner)
|
|
360
369
|
return scanner;
|
|
361
370
|
const cfg = getConfig();
|
|
362
371
|
const eff = configManager.getEffective();
|
|
363
|
-
const
|
|
364
|
-
|
|
372
|
+
const userApiKey = typeof cfg?.apiKey === 'string' && cfg.apiKey.trim().length > 0
|
|
373
|
+
? cfg.apiKey.trim()
|
|
374
|
+
: undefined;
|
|
365
375
|
if (userApiKey) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
process.env.VIRUSTOTAL_API_KEY = userApiKey;
|
|
369
|
-
scanner = new scanner_1.Scanner(userApiKey, api.logger, eff.maxFileSizeMb, eff.sensitiveFilePolicy);
|
|
376
|
+
scanner = new scanner_1.Scanner(userApiKey, api.logger, eff.maxFileSizeMb, eff.sensitiveFilePolicy, false, eff.semanticFilePolicy);
|
|
377
|
+
credentialMode = 'user_key';
|
|
370
378
|
api.logger.info('[VT-Sentinel] Using user-provided API key (standard VT API)');
|
|
371
379
|
}
|
|
372
380
|
else {
|
|
@@ -386,9 +394,8 @@ function vtSentinelPlugin(api) {
|
|
|
386
394
|
else {
|
|
387
395
|
api.logger.info(`[VT-Sentinel] Using cached VTAI agent: ${creds.publicHandle}`);
|
|
388
396
|
}
|
|
389
|
-
scanner = new scanner_1.Scanner(creds.agentToken, api.logger, eff.maxFileSizeMb, eff.sensitiveFilePolicy, true);
|
|
390
|
-
|
|
391
|
-
process.env.VIRUSTOTAL_API_KEY = 'vtai-active';
|
|
397
|
+
scanner = new scanner_1.Scanner(creds.agentToken, api.logger, eff.maxFileSizeMb, eff.sensitiveFilePolicy, true, eff.semanticFilePolicy);
|
|
398
|
+
credentialMode = 'vtai';
|
|
392
399
|
}
|
|
393
400
|
return scanner;
|
|
394
401
|
};
|
|
@@ -400,7 +407,7 @@ function vtSentinelPlugin(api) {
|
|
|
400
407
|
const readScanRegistry = new Map();
|
|
401
408
|
// --- Config manager + state store ---
|
|
402
409
|
const configManager = new config_manager_1.ConfigManager(getConfig());
|
|
403
|
-
const stateStore = new state_store_1.StateStore();
|
|
410
|
+
const stateStore = new state_store_1.StateStore(resolvedStateDir);
|
|
404
411
|
configManager.loadPersistedOverrides(stateStore.getPersistedOverrides());
|
|
405
412
|
let firstRunDelivered = false;
|
|
406
413
|
/** Check if a scan result should be logged based on notifyLevel. */
|
|
@@ -652,9 +659,9 @@ function vtSentinelPlugin(api) {
|
|
|
652
659
|
if (process.platform === 'darwin') {
|
|
653
660
|
candidates.push('/tmp', '/private/tmp');
|
|
654
661
|
}
|
|
655
|
-
const home =
|
|
662
|
+
const home = os.homedir();
|
|
656
663
|
if (home) {
|
|
657
|
-
const stateDir =
|
|
664
|
+
const stateDir = resolvedStateDir;
|
|
658
665
|
const codeDirs = [
|
|
659
666
|
path.join(stateDir, 'skills'),
|
|
660
667
|
path.join(stateDir, 'extensions'),
|
|
@@ -684,7 +691,7 @@ function vtSentinelPlugin(api) {
|
|
|
684
691
|
// Startup folder — anything placed here runs on login
|
|
685
692
|
candidates.push(path.join(home, 'AppData', 'Roaming', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup'));
|
|
686
693
|
}
|
|
687
|
-
const profile =
|
|
694
|
+
const profile = (0, env_access_1.getActiveProfile)();
|
|
688
695
|
if (profile) {
|
|
689
696
|
const profileBase = path.join(home, `.openclaw-${profile}`);
|
|
690
697
|
const profileCodeDirs = [
|
|
@@ -862,7 +869,7 @@ function vtSentinelPlugin(api) {
|
|
|
862
869
|
// --- Tool: vt_upload_consent ---
|
|
863
870
|
api.registerTool({
|
|
864
871
|
name: 'vt_upload_consent',
|
|
865
|
-
description: 'Confirm or deny uploading a
|
|
872
|
+
description: 'Confirm or deny uploading a file to VirusTotal after a needs_consent verdict. Call this after asking the user whether they want to upload a file that was flagged as sensitive (PDF, Office, unknown archive) or as an instruction file (SKILL.md, TOOLS.md, AGENTS.md, etc.).',
|
|
866
873
|
parameters: {
|
|
867
874
|
type: 'object',
|
|
868
875
|
properties: {
|
|
@@ -881,13 +888,16 @@ function vtSentinelPlugin(api) {
|
|
|
881
888
|
const s = await ensureScanner();
|
|
882
889
|
if (!s)
|
|
883
890
|
return textResponse('Error: VT-Sentinel not configured (API key missing and VTAI registration failed)');
|
|
891
|
+
// Determine consent group from file category
|
|
892
|
+
const fileCategory = classifier_1.FileClassifier.classify(params.path);
|
|
893
|
+
const consentGroup = fileCategory === classifier_1.FileCategory.SEMANTIC_RISK ? 'semantic' : 'sensitive';
|
|
884
894
|
if (!params.upload) {
|
|
885
|
-
s.recordConsent(false);
|
|
895
|
+
s.recordConsent(false, consentGroup);
|
|
886
896
|
return textResponse(`Upload declined. File hash was already checked (not found in VT). ` +
|
|
887
897
|
`The file was NOT uploaded — your privacy is preserved.`);
|
|
888
898
|
}
|
|
889
899
|
try {
|
|
890
|
-
s.recordConsent(true);
|
|
900
|
+
s.recordConsent(true, consentGroup);
|
|
891
901
|
const result = await s.uploadWithConsent(params.path);
|
|
892
902
|
auditResult(result);
|
|
893
903
|
return textResponse(formatResult(result));
|
|
@@ -902,6 +912,7 @@ function vtSentinelPlugin(api) {
|
|
|
902
912
|
if (diff.scannerNeedsRebuild && scanner) {
|
|
903
913
|
scanner.updateMaxFileSizeMb(newConfig.maxFileSizeMb);
|
|
904
914
|
scanner.updateSensitivePolicy(newConfig.sensitiveFilePolicy);
|
|
915
|
+
scanner.updateSemanticPolicy(newConfig.semanticFilePolicy);
|
|
905
916
|
api.logger.info('[VT-Sentinel] Scanner config updated');
|
|
906
917
|
}
|
|
907
918
|
if (diff.changedFields.includes('autoScan')) {
|
|
@@ -963,7 +974,7 @@ function vtSentinelPlugin(api) {
|
|
|
963
974
|
const eff = configManager.getEffective();
|
|
964
975
|
return textResponse((0, status_renderer_1.renderStatus)({
|
|
965
976
|
version: getCurrentVersion(),
|
|
966
|
-
apiMode:
|
|
977
|
+
apiMode: credentialMode === 'vtai' ? 'vtai' : 'user_key',
|
|
967
978
|
effectiveConfig: eff,
|
|
968
979
|
watchedDirs: [...watchRoots],
|
|
969
980
|
blockedFileCount: blocklist.size,
|
|
@@ -991,6 +1002,7 @@ function vtSentinelPlugin(api) {
|
|
|
991
1002
|
preset: { type: 'string', enum: ['balanced', 'privacy_first', 'strict_security'], description: 'Configuration preset' },
|
|
992
1003
|
notifyLevel: { type: 'string', enum: ['all', 'threats_only', 'silent'], description: 'Notification verbosity' },
|
|
993
1004
|
sensitiveFilePolicy: { type: 'string', enum: ['ask', 'ask_once', 'always_upload', 'hash_only'], description: 'Policy for sensitive files' },
|
|
1005
|
+
semanticFilePolicy: { type: 'string', enum: ['ask', 'ask_once', 'always_upload', 'hash_only'], description: 'Policy for instruction files (SKILL.md, HOOK.md, TOOLS.md, AGENTS.md). Default: hash_only' },
|
|
994
1006
|
autoScan: { type: 'boolean', description: 'Enable/disable auto-scan' },
|
|
995
1007
|
maxFileSizeMb: { type: 'number', description: 'Max file size to scan (MB)' },
|
|
996
1008
|
watchDirsAdd: { type: 'array', items: { type: 'string' }, description: 'Directories to add to watch list' },
|
|
@@ -1103,6 +1115,7 @@ function vtSentinelPlugin(api) {
|
|
|
1103
1115
|
if (scanner) {
|
|
1104
1116
|
scanner.updateMaxFileSizeMb(newConfig.maxFileSizeMb);
|
|
1105
1117
|
scanner.updateSensitivePolicy(newConfig.sensitiveFilePolicy);
|
|
1118
|
+
scanner.updateSemanticPolicy(newConfig.semanticFilePolicy);
|
|
1106
1119
|
}
|
|
1107
1120
|
// Reconcile watcher state with restored config
|
|
1108
1121
|
if (newConfig.autoScan && !watcher) {
|
|
@@ -1175,7 +1188,7 @@ function vtSentinelPlugin(api) {
|
|
|
1175
1188
|
currentVersion,
|
|
1176
1189
|
latestVersion,
|
|
1177
1190
|
confirm: params.confirm === true,
|
|
1178
|
-
stateDir:
|
|
1191
|
+
stateDir: resolvedStateDir,
|
|
1179
1192
|
}));
|
|
1180
1193
|
},
|
|
1181
1194
|
});
|
|
@@ -1195,9 +1208,9 @@ function vtSentinelPlugin(api) {
|
|
|
1195
1208
|
if ('confirm' in params && typeof params.confirm !== 'boolean') {
|
|
1196
1209
|
return textResponse('Error: confirm must be true or false');
|
|
1197
1210
|
}
|
|
1198
|
-
//
|
|
1199
|
-
|
|
1200
|
-
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') {
|
|
1201
1214
|
return textResponse('Re-registration not applicable — using user-provided API key (not VTAI).');
|
|
1202
1215
|
}
|
|
1203
1216
|
const eff = configManager.getEffective();
|
|
@@ -1237,25 +1250,19 @@ function vtSentinelPlugin(api) {
|
|
|
1237
1250
|
}
|
|
1238
1251
|
// Confirmed — re-register
|
|
1239
1252
|
try {
|
|
1240
|
-
// 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).
|
|
1241
1256
|
if (currentCreds) {
|
|
1242
1257
|
const backupPath = (0, vt_api_1.getAgentCredentialsPath)() + '.bak';
|
|
1243
1258
|
fs.writeFileSync(backupPath, JSON.stringify(currentCreds, null, 2), { mode: 0o600 });
|
|
1244
|
-
// Windows: POSIX mode bits ignored on NTFS — restrict via ACL
|
|
1245
|
-
if (process.platform === 'win32') {
|
|
1246
|
-
try {
|
|
1247
|
-
const { execSync } = require('child_process');
|
|
1248
|
-
execSync(`icacls "${backupPath}" /inheritance:r /grant:r "%USERNAME%:(F)"`, { stdio: 'ignore' });
|
|
1249
|
-
}
|
|
1250
|
-
catch { /* best effort */ }
|
|
1251
|
-
}
|
|
1252
1259
|
}
|
|
1253
1260
|
const newCreds = await (0, vt_api_1.registerAgent)(buildRegistrationOpts());
|
|
1254
1261
|
(0, vt_api_1.saveAgentCredentials)(newCreds);
|
|
1255
1262
|
// Update scanner with new token
|
|
1256
1263
|
if (scanner) {
|
|
1257
1264
|
const scanEff = configManager.getEffective();
|
|
1258
|
-
scanner = new scanner_1.Scanner(newCreds.agentToken, api.logger, scanEff.maxFileSizeMb, scanEff.sensitiveFilePolicy, true);
|
|
1265
|
+
scanner = new scanner_1.Scanner(newCreds.agentToken, api.logger, scanEff.maxFileSizeMb, scanEff.sensitiveFilePolicy, true, scanEff.semanticFilePolicy);
|
|
1259
1266
|
}
|
|
1260
1267
|
const lines = ['Agent re-registered successfully:'];
|
|
1261
1268
|
lines.push(` New handle: ${newCreds.publicHandle}`);
|
|
@@ -1276,23 +1283,20 @@ function vtSentinelPlugin(api) {
|
|
|
1276
1283
|
});
|
|
1277
1284
|
// --- Hook: auto-scan tool results ---
|
|
1278
1285
|
const handleToolResult = async (event) => {
|
|
1279
|
-
|
|
1280
|
-
if (!s)
|
|
1281
|
-
return;
|
|
1282
|
-
// Enrich interesting dirs from context on first event
|
|
1286
|
+
// Enrich interesting dirs from context on first event (no scanner needed)
|
|
1283
1287
|
if (!contextEnriched)
|
|
1284
1288
|
enrichFromContext(event);
|
|
1285
|
-
// First-run onboarding: deliver once per workspace scope
|
|
1289
|
+
// First-run onboarding: deliver once per workspace scope (no scanner needed)
|
|
1286
1290
|
if (!firstRunDelivered) {
|
|
1287
1291
|
const scope = {
|
|
1288
1292
|
workspaceDir: resolvedWorkspaceDir,
|
|
1289
|
-
profile:
|
|
1293
|
+
profile: (0, env_access_1.getActiveProfile)(),
|
|
1290
1294
|
};
|
|
1291
1295
|
if (!stateStore.isFirstRunShown(scope)) {
|
|
1292
1296
|
try {
|
|
1293
1297
|
const onboardingText = (0, status_renderer_1.renderOnboarding)({
|
|
1294
1298
|
version: getCurrentVersion(),
|
|
1295
|
-
apiMode:
|
|
1299
|
+
apiMode: credentialMode === 'vtai' ? 'vtai' : 'user_key',
|
|
1296
1300
|
watchDirs: [...watchRoots],
|
|
1297
1301
|
effectiveConfig: configManager.getEffective(),
|
|
1298
1302
|
availableTools: [
|
|
@@ -1314,6 +1318,14 @@ function vtSentinelPlugin(api) {
|
|
|
1314
1318
|
firstRunDelivered = true;
|
|
1315
1319
|
}
|
|
1316
1320
|
}
|
|
1321
|
+
// autoScan=false disables hook scanning (active blocking in handleBeforeToolCall remains always-on)
|
|
1322
|
+
const hookEff = configManager.getEffective();
|
|
1323
|
+
if (!hookEff.autoScan)
|
|
1324
|
+
return;
|
|
1325
|
+
// Initialize scanner only when we're actually going to scan
|
|
1326
|
+
const s = await ensureScanner();
|
|
1327
|
+
if (!s)
|
|
1328
|
+
return;
|
|
1317
1329
|
const toolName = event.toolName || event.tool || '';
|
|
1318
1330
|
const toolParams = event.toolParams || event.params || event.input || {};
|
|
1319
1331
|
const toolResultText = extractResultText(event);
|
|
@@ -1323,13 +1335,25 @@ function vtSentinelPlugin(api) {
|
|
|
1323
1335
|
if (shouldLog('pending')) {
|
|
1324
1336
|
api.logger.info(`[VT-Sentinel] Auto-scan: ${targets.length} file(s) from ${toolName} tool`);
|
|
1325
1337
|
}
|
|
1326
|
-
const hookEff = configManager.getEffective();
|
|
1327
1338
|
for (const target of targets) {
|
|
1328
1339
|
if (isSelfPath(target.path))
|
|
1329
1340
|
continue;
|
|
1341
|
+
// excludeGlobs: skip files matching any exclude pattern
|
|
1342
|
+
if (hookEff.excludeGlobs.length > 0) {
|
|
1343
|
+
let excluded = false;
|
|
1344
|
+
for (const glob of hookEff.excludeGlobs) {
|
|
1345
|
+
if ((0, config_manager_1.matchGlob)(target.path, glob)) {
|
|
1346
|
+
excluded = true;
|
|
1347
|
+
break;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
if (excluded)
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1330
1353
|
try {
|
|
1331
1354
|
let precomputedHash;
|
|
1332
|
-
|
|
1355
|
+
const isReadTarget = target.source === 'read_target';
|
|
1356
|
+
if (isReadTarget) {
|
|
1333
1357
|
try {
|
|
1334
1358
|
const currentHash = await (0, vt_api_1.calculateSHA256)(target.path);
|
|
1335
1359
|
const previousHash = readScanRegistry.get(target.path);
|
|
@@ -1344,7 +1368,7 @@ function vtSentinelPlugin(api) {
|
|
|
1344
1368
|
// If hash computation fails, proceed with scan anyway
|
|
1345
1369
|
}
|
|
1346
1370
|
}
|
|
1347
|
-
const result = await s.scanFile(target.path, false, precomputedHash);
|
|
1371
|
+
const result = await s.scanFile(target.path, false, precomputedHash, isReadTarget);
|
|
1348
1372
|
auditResult(result);
|
|
1349
1373
|
if (result.verdict === 'malicious') {
|
|
1350
1374
|
if (shouldLog('malicious')) {
|
|
@@ -1388,8 +1412,10 @@ function vtSentinelPlugin(api) {
|
|
|
1388
1412
|
api.logger.warn(`[VT-Sentinel] Unknown: ${result.fileName} — ${result.message}`);
|
|
1389
1413
|
}
|
|
1390
1414
|
// Update read-scan registry AFTER successful scan.
|
|
1391
|
-
//
|
|
1392
|
-
|
|
1415
|
+
// For hashOnly (read_target): 'unknown' means "hash not in VT" — stable, cache it.
|
|
1416
|
+
// For non-hashOnly: 'unknown' may be transient API error → allow retry on next read.
|
|
1417
|
+
const cacheableVerdict = result.verdict !== 'unknown' || isReadTarget;
|
|
1418
|
+
if (precomputedHash && cacheableVerdict) {
|
|
1393
1419
|
readScanRegistry.set(target.path, precomputedHash);
|
|
1394
1420
|
if (readScanRegistry.size > READ_SCAN_REGISTRY_MAX) {
|
|
1395
1421
|
const toEvict = Math.floor(READ_SCAN_REGISTRY_MAX / 2);
|
|
@@ -1515,12 +1541,11 @@ function vtSentinelPlugin(api) {
|
|
|
1515
1541
|
vtSentinelPlugin._handleWatcherFile = handleWatcherFile;
|
|
1516
1542
|
vtSentinelPlugin._enrichFromContext = enrichFromContext;
|
|
1517
1543
|
api.logger.info('[VT-Sentinel] Plugin loaded — 9 tools + active protection hooks registered (VTAI auto-registration enabled)');
|
|
1518
|
-
//
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
});
|
|
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.
|
|
1524
1549
|
}
|
|
1525
1550
|
// --- Hook helpers ---
|
|
1526
1551
|
function extractResultText(event) {
|
|
@@ -1580,6 +1605,5 @@ function injectWarning(event, result) {
|
|
|
1580
1605
|
exports._generateUpdateCommands = generateUpdateCommands;
|
|
1581
1606
|
exports._fetchLatestVersion = fetchLatestVersion;
|
|
1582
1607
|
exports._getCurrentVersion = getCurrentVersion;
|
|
1583
|
-
exports._getStateDir = getStateDir;
|
|
1584
1608
|
exports._generateAgentName = generateAgentName;
|
|
1585
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';
|