@thinkingos/vsl-sdk 0.4.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +381 -20
- package/dist/index.d.ts +381 -20
- package/dist/index.js +630 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +623 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,594 @@
|
|
|
1
|
+
// src/utils/idGenerator.ts
|
|
2
|
+
var TYPE_ABBREVIATIONS = {
|
|
3
|
+
button: "btn",
|
|
4
|
+
input: "inp",
|
|
5
|
+
link: "link",
|
|
6
|
+
container: "cont",
|
|
7
|
+
image: "img",
|
|
8
|
+
select: "sel",
|
|
9
|
+
textarea: "txt",
|
|
10
|
+
heading: "h",
|
|
11
|
+
text: "txt",
|
|
12
|
+
file_input: "file",
|
|
13
|
+
modal: "modal",
|
|
14
|
+
tab: "tab",
|
|
15
|
+
dropdown_toggle: "dropdown",
|
|
16
|
+
footer: "footer",
|
|
17
|
+
scrollable_container: "scroll",
|
|
18
|
+
toolbar: "toolbar",
|
|
19
|
+
list: "list",
|
|
20
|
+
grid: "grid",
|
|
21
|
+
form_field: "form",
|
|
22
|
+
tab_bar: "tabs",
|
|
23
|
+
layout: "layout",
|
|
24
|
+
nav: "nav",
|
|
25
|
+
header: "header",
|
|
26
|
+
main: "main",
|
|
27
|
+
icon: "icon",
|
|
28
|
+
chart: "chart",
|
|
29
|
+
custom_widget: "widget",
|
|
30
|
+
unknown: "el"
|
|
31
|
+
};
|
|
32
|
+
var IdGenerator = class {
|
|
33
|
+
typeCounters = /* @__PURE__ */ new Map();
|
|
34
|
+
/**
|
|
35
|
+
* Generate ID for an element.
|
|
36
|
+
*
|
|
37
|
+
* @param elementId - Existing DOM id attribute (if present)
|
|
38
|
+
* @param vslType - VSL type of the element (for fallback abbreviation)
|
|
39
|
+
* @returns Generated ID string
|
|
40
|
+
*/
|
|
41
|
+
generate(elementId2, vslType) {
|
|
42
|
+
if (elementId2 && elementId2.trim().length > 0) {
|
|
43
|
+
return elementId2;
|
|
44
|
+
}
|
|
45
|
+
const shortType = vslType ? TYPE_ABBREVIATIONS[vslType] || "el" : "el";
|
|
46
|
+
const count = (this.typeCounters.get(shortType) || 0) + 1;
|
|
47
|
+
this.typeCounters.set(shortType, count);
|
|
48
|
+
return `${shortType}_${count}`;
|
|
49
|
+
}
|
|
50
|
+
/** Reset counters (for testing or new snapshot). */
|
|
51
|
+
reset() {
|
|
52
|
+
this.typeCounters.clear();
|
|
53
|
+
}
|
|
54
|
+
/** Get current counter value for a type (for debugging). */
|
|
55
|
+
getCounter(type) {
|
|
56
|
+
if (type) {
|
|
57
|
+
return this.typeCounters.get(type) || 0;
|
|
58
|
+
}
|
|
59
|
+
let total = 0;
|
|
60
|
+
for (const count of this.typeCounters.values()) {
|
|
61
|
+
total += count;
|
|
62
|
+
}
|
|
63
|
+
return total;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
function createIdGenerator() {
|
|
67
|
+
return new IdGenerator();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/security/promptInjectionFilter.ts
|
|
71
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, appendFileSync } from "fs";
|
|
72
|
+
import { join as join2 } from "path";
|
|
73
|
+
import { homedir as homedir2 } from "os";
|
|
74
|
+
|
|
75
|
+
// src/security/remotePatternsLoader.ts
|
|
76
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
77
|
+
import { join, dirname } from "path";
|
|
78
|
+
import { homedir } from "os";
|
|
79
|
+
var DEFAULT_REMOTE_URL = "https://raw.githubusercontent.com/MaxZhadobin/visual-scene-language/vsl_mcp/patterns/latest.json";
|
|
80
|
+
var DEFAULT_CACHE_PATH = join(homedir(), ".vsl", "cache", "remote-patterns.json");
|
|
81
|
+
var DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
82
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
83
|
+
var RemotePatternsLoader = class {
|
|
84
|
+
remoteUrl;
|
|
85
|
+
cachePath;
|
|
86
|
+
cacheTtlMs;
|
|
87
|
+
enabled;
|
|
88
|
+
timeoutMs;
|
|
89
|
+
constructor(options = {}) {
|
|
90
|
+
this.remoteUrl = options.remoteUrl ?? DEFAULT_REMOTE_URL;
|
|
91
|
+
this.cachePath = options.cachePath ?? DEFAULT_CACHE_PATH;
|
|
92
|
+
this.cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
93
|
+
this.enabled = options.enabled ?? true;
|
|
94
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Загружает remote patterns с fallback на кэш.
|
|
98
|
+
*
|
|
99
|
+
* Логика:
|
|
100
|
+
* 1. Если enabled === false — возвращаем empty
|
|
101
|
+
* 2. Проверяем кэш — если валиден (TTL не истёк), используем кэш
|
|
102
|
+
* 3. Пытаемся загрузить с remote URL
|
|
103
|
+
* 4. Если загрузка успешна — обновляем кэш, возвращаем remote patterns
|
|
104
|
+
* 5. Если загрузка не удалась — используем кэш (fallback)
|
|
105
|
+
* 6. Если кэша нет — возвращаем empty
|
|
106
|
+
*/
|
|
107
|
+
async load() {
|
|
108
|
+
if (!this.enabled) {
|
|
109
|
+
return { patterns: [], source: "none", fallbackUsed: false };
|
|
110
|
+
}
|
|
111
|
+
const cachedData = this.readCache();
|
|
112
|
+
const cacheIsValid = this.isCacheValid(cachedData);
|
|
113
|
+
if (cacheIsValid) {
|
|
114
|
+
return {
|
|
115
|
+
patterns: cachedData.patterns,
|
|
116
|
+
source: "cache",
|
|
117
|
+
fallbackUsed: false
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const patterns = await this.fetchRemotePatterns();
|
|
122
|
+
this.writeCache(patterns);
|
|
123
|
+
return {
|
|
124
|
+
patterns,
|
|
125
|
+
source: "remote",
|
|
126
|
+
fallbackUsed: false
|
|
127
|
+
};
|
|
128
|
+
} catch (error) {
|
|
129
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
130
|
+
if (cachedData) {
|
|
131
|
+
return {
|
|
132
|
+
patterns: cachedData.patterns,
|
|
133
|
+
source: "cache",
|
|
134
|
+
fallbackUsed: true,
|
|
135
|
+
error: errorMessage
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
patterns: [],
|
|
140
|
+
source: "none",
|
|
141
|
+
fallbackUsed: true,
|
|
142
|
+
error: errorMessage
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Загружает паттерны с remote URL.
|
|
148
|
+
*/
|
|
149
|
+
async fetchRemotePatterns() {
|
|
150
|
+
const controller = new AbortController();
|
|
151
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
152
|
+
try {
|
|
153
|
+
const response = await fetch(this.remoteUrl, {
|
|
154
|
+
signal: controller.signal,
|
|
155
|
+
headers: {
|
|
156
|
+
"Accept": "application/json",
|
|
157
|
+
"User-Agent": "VSL-Patterns-Loader/1.0"
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
162
|
+
}
|
|
163
|
+
const data = await response.json();
|
|
164
|
+
let patternsArray;
|
|
165
|
+
if (Array.isArray(data)) {
|
|
166
|
+
patternsArray = data;
|
|
167
|
+
} else if (typeof data === "object" && data !== null && Array.isArray(data.patterns)) {
|
|
168
|
+
patternsArray = data.patterns;
|
|
169
|
+
} else {
|
|
170
|
+
throw new Error("Invalid patterns format: expected array or { patterns: [] }");
|
|
171
|
+
}
|
|
172
|
+
const patterns = [];
|
|
173
|
+
for (const item of patternsArray) {
|
|
174
|
+
if (this.isValidPattern(item)) {
|
|
175
|
+
patterns.push(item);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return patterns;
|
|
179
|
+
} finally {
|
|
180
|
+
clearTimeout(timeoutId);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Проверяет валидность паттерна.
|
|
185
|
+
*/
|
|
186
|
+
isValidPattern(item) {
|
|
187
|
+
if (typeof item !== "object" || item === null) return false;
|
|
188
|
+
const obj = item;
|
|
189
|
+
return typeof obj.id === "string" && typeof obj.pattern === "string" && typeof obj.type === "string" && (obj.type === "regex" || obj.type === "ml") && typeof obj.severity === "string" && ["low", "medium", "high", "critical"].includes(obj.severity) && typeof obj.action === "string" && ["strip", "log", "block"].includes(obj.action) && typeof obj.description === "string";
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Читает кэш из файла.
|
|
193
|
+
*/
|
|
194
|
+
readCache() {
|
|
195
|
+
if (!existsSync(this.cachePath)) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const content = readFileSync(this.cachePath, "utf-8");
|
|
200
|
+
const data = JSON.parse(content);
|
|
201
|
+
if (!data.metadata || !Array.isArray(data.patterns)) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
return data;
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Записывает паттерны в кэш.
|
|
211
|
+
*/
|
|
212
|
+
writeCache(patterns) {
|
|
213
|
+
const cacheData = {
|
|
214
|
+
metadata: {
|
|
215
|
+
lastFetched: (/* @__PURE__ */ new Date()).toISOString(),
|
|
216
|
+
sourceUrl: this.remoteUrl,
|
|
217
|
+
patternCount: patterns.length
|
|
218
|
+
},
|
|
219
|
+
patterns
|
|
220
|
+
};
|
|
221
|
+
try {
|
|
222
|
+
const cacheDir = dirname(this.cachePath);
|
|
223
|
+
if (!existsSync(cacheDir)) {
|
|
224
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
225
|
+
}
|
|
226
|
+
writeFileSync(this.cachePath, JSON.stringify(cacheData, null, 2), "utf-8");
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error(`[RemotePatternsLoader] Failed to write cache: ${error}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Проверяет валидность кэша (TTL не истёк).
|
|
233
|
+
*/
|
|
234
|
+
isCacheValid(cachedData) {
|
|
235
|
+
if (!cachedData) return false;
|
|
236
|
+
try {
|
|
237
|
+
const lastFetched = new Date(cachedData.metadata.lastFetched).getTime();
|
|
238
|
+
const now = Date.now();
|
|
239
|
+
return now - lastFetched < this.cacheTtlMs;
|
|
240
|
+
} catch {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Очищает кэш (для тестирования).
|
|
246
|
+
*/
|
|
247
|
+
clearCache() {
|
|
248
|
+
try {
|
|
249
|
+
if (existsSync(this.cachePath)) {
|
|
250
|
+
writeFileSync(this.cachePath, "", "utf-8");
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error(`[RemotePatternsLoader] Failed to clear cache: ${error}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Получает информацию о кэше (для диагностики).
|
|
258
|
+
*/
|
|
259
|
+
getCacheInfo() {
|
|
260
|
+
const cachedData = this.readCache();
|
|
261
|
+
if (!cachedData) {
|
|
262
|
+
return { exists: false };
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
exists: true,
|
|
266
|
+
lastFetched: cachedData.metadata.lastFetched,
|
|
267
|
+
patternCount: cachedData.metadata.patternCount
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Получает URL для загрузки remote patterns.
|
|
272
|
+
*/
|
|
273
|
+
getRemoteUrl() {
|
|
274
|
+
return this.remoteUrl;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Устанавливает URL для загрузки remote patterns.
|
|
278
|
+
*/
|
|
279
|
+
setRemoteUrl(url) {
|
|
280
|
+
this.remoteUrl = url;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// src/security/promptInjectionFilter.ts
|
|
285
|
+
var DEFAULT_LOG_DIR = join2(homedir2(), ".vsl", "logs");
|
|
286
|
+
var DEFAULT_LOG_PATH = join2(DEFAULT_LOG_DIR, "security-audit.log");
|
|
287
|
+
var DEFAULT_BUNDLED_PATTERNS_PATH = join2(__dirname, "..", "..", "patterns", "patterns_v1.json");
|
|
288
|
+
var DEFAULT_REMOTE_PATTERNS_PATH = join2(homedir2(), ".vsl", "cache", "remote-patterns.json");
|
|
289
|
+
var DEFAULT_CONFIDENCE_THRESHOLD = 0.7;
|
|
290
|
+
var DEFAULT_MAX_LOG_ENTRIES = 1e4;
|
|
291
|
+
var PromptInjectionFilter = class {
|
|
292
|
+
enabled;
|
|
293
|
+
confidenceThreshold;
|
|
294
|
+
domainWhitelist;
|
|
295
|
+
patterns = [];
|
|
296
|
+
logPath;
|
|
297
|
+
maxLogEntries;
|
|
298
|
+
logEntries = [];
|
|
299
|
+
remoteLoader = null;
|
|
300
|
+
constructor(options = {}) {
|
|
301
|
+
this.enabled = options.enabled ?? true;
|
|
302
|
+
this.confidenceThreshold = options.confidenceThreshold ?? DEFAULT_CONFIDENCE_THRESHOLD;
|
|
303
|
+
this.domainWhitelist = new Set(options.domainWhitelist ?? []);
|
|
304
|
+
this.logPath = options.logPath ?? DEFAULT_LOG_PATH;
|
|
305
|
+
this.maxLogEntries = options.maxLogEntries ?? DEFAULT_MAX_LOG_ENTRIES;
|
|
306
|
+
if (options.remoteLoader?.enabled !== false) {
|
|
307
|
+
this.remoteLoader = new RemotePatternsLoader({
|
|
308
|
+
remoteUrl: options.remoteLoader?.remoteUrl,
|
|
309
|
+
cachePath: options.remoteLoader?.cachePath ?? options.remotePatternsPath,
|
|
310
|
+
cacheTtlMs: options.remoteLoader?.cacheTtlMs,
|
|
311
|
+
enabled: options.remoteLoader?.enabled ?? true,
|
|
312
|
+
timeoutMs: options.remoteLoader?.timeoutMs
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
this.loadPatterns(options);
|
|
316
|
+
this.loadExistingLogs();
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Загружает паттерны из всех источников (bundled, remote, custom).
|
|
320
|
+
*/
|
|
321
|
+
loadPatterns(options) {
|
|
322
|
+
const allPatterns = [];
|
|
323
|
+
const bundledPath = options.bundledPatternsPath ?? DEFAULT_BUNDLED_PATTERNS_PATH;
|
|
324
|
+
if (existsSync2(bundledPath)) {
|
|
325
|
+
try {
|
|
326
|
+
const content = readFileSync2(bundledPath, "utf-8");
|
|
327
|
+
const bundled = JSON.parse(content);
|
|
328
|
+
allPatterns.push(...bundled);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.error(`[PromptInjectionFilter] Failed to load bundled patterns: ${error}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const remotePath = options.remotePatternsPath ?? DEFAULT_REMOTE_PATTERNS_PATH;
|
|
334
|
+
if (existsSync2(remotePath)) {
|
|
335
|
+
try {
|
|
336
|
+
const content = readFileSync2(remotePath, "utf-8");
|
|
337
|
+
const remote = JSON.parse(content);
|
|
338
|
+
allPatterns.push(...remote);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
console.error(`[PromptInjectionFilter] Failed to load remote patterns: ${error}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (options.customPatterns && options.customPatterns.length > 0) {
|
|
344
|
+
allPatterns.push(...options.customPatterns);
|
|
345
|
+
}
|
|
346
|
+
this.patterns = allPatterns.filter((p) => p.type === "regex").map((p) => ({
|
|
347
|
+
...p,
|
|
348
|
+
regex: new RegExp(p.pattern, "gi"),
|
|
349
|
+
// case-insensitive, global
|
|
350
|
+
confidenceThreshold: p.confidence ?? this.confidenceThreshold
|
|
351
|
+
}));
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Асинхронно загружает remote patterns через RemotePatternsLoader.
|
|
355
|
+
* Вызывается опционально для обновления паттернов с CDN/GitHub.
|
|
356
|
+
*
|
|
357
|
+
* @returns Результат загрузки remote patterns
|
|
358
|
+
*/
|
|
359
|
+
async loadRemotePatterns() {
|
|
360
|
+
if (!this.remoteLoader) {
|
|
361
|
+
return { patterns: 0, source: "none", fallbackUsed: false };
|
|
362
|
+
}
|
|
363
|
+
const result = await this.remoteLoader.load();
|
|
364
|
+
if (result.patterns.length > 0) {
|
|
365
|
+
const remoteCompiled = result.patterns.filter((p) => p.type === "regex").map((p) => ({
|
|
366
|
+
...p,
|
|
367
|
+
regex: new RegExp(p.pattern, "gi"),
|
|
368
|
+
confidenceThreshold: p.confidence ?? this.confidenceThreshold
|
|
369
|
+
}));
|
|
370
|
+
this.patterns.push(...remoteCompiled);
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
patterns: result.patterns.length,
|
|
374
|
+
source: result.source,
|
|
375
|
+
fallbackUsed: result.fallbackUsed,
|
|
376
|
+
error: result.error
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Получает информацию о кэше remote patterns.
|
|
381
|
+
*/
|
|
382
|
+
getRemoteCacheInfo() {
|
|
383
|
+
if (!this.remoteLoader) {
|
|
384
|
+
return { exists: false };
|
|
385
|
+
}
|
|
386
|
+
return this.remoteLoader.getCacheInfo();
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Очищает кэш remote patterns.
|
|
390
|
+
*/
|
|
391
|
+
clearRemoteCache() {
|
|
392
|
+
if (this.remoteLoader) {
|
|
393
|
+
this.remoteLoader.clearCache();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Загружает существующие логи из файла.
|
|
398
|
+
*/
|
|
399
|
+
loadExistingLogs() {
|
|
400
|
+
if (!existsSync2(this.logPath)) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
const content = readFileSync2(this.logPath, "utf-8");
|
|
405
|
+
const lines = content.trim().split("\n").filter((line) => line.length > 0);
|
|
406
|
+
this.logEntries = lines.map((line) => JSON.parse(line));
|
|
407
|
+
} catch (error) {
|
|
408
|
+
console.error(`[PromptInjectionFilter] Failed to load existing logs: ${error}`);
|
|
409
|
+
this.logEntries = [];
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Проверяет, находится ли URL в whitelist.
|
|
414
|
+
*/
|
|
415
|
+
isWhitelisted(url) {
|
|
416
|
+
if (!url) return false;
|
|
417
|
+
try {
|
|
418
|
+
const hostname = new URL(url).hostname;
|
|
419
|
+
return this.domainWhitelist.has(hostname);
|
|
420
|
+
} catch {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Сканирует текст на наличие инъекций.
|
|
426
|
+
*
|
|
427
|
+
* @param text — текст для сканирования
|
|
428
|
+
* @param url — URL страницы (для whitelist и логирования)
|
|
429
|
+
* @returns Результат сканирования с очищенным текстом и списком обнаружений
|
|
430
|
+
*/
|
|
431
|
+
scan(text, url) {
|
|
432
|
+
const startTime = performance.now();
|
|
433
|
+
if (!this.enabled || this.isWhitelisted(url)) {
|
|
434
|
+
return {
|
|
435
|
+
cleanText: text,
|
|
436
|
+
detections: [],
|
|
437
|
+
blocked: false,
|
|
438
|
+
scanTimeMs: performance.now() - startTime
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
const detections = [];
|
|
442
|
+
let cleanText = text;
|
|
443
|
+
let blocked = false;
|
|
444
|
+
for (const pattern of this.patterns) {
|
|
445
|
+
pattern.regex.lastIndex = 0;
|
|
446
|
+
let match;
|
|
447
|
+
while ((match = pattern.regex.exec(text)) !== null) {
|
|
448
|
+
const matchedText = match[0];
|
|
449
|
+
const index = match.index;
|
|
450
|
+
const contextStart = Math.max(0, index - 50);
|
|
451
|
+
const contextEnd = Math.min(text.length, index + matchedText.length + 50);
|
|
452
|
+
const context = text.slice(contextStart, contextEnd);
|
|
453
|
+
const detection = {
|
|
454
|
+
patternId: pattern.id,
|
|
455
|
+
description: pattern.description,
|
|
456
|
+
severity: pattern.severity,
|
|
457
|
+
action: pattern.action,
|
|
458
|
+
matchedText,
|
|
459
|
+
index,
|
|
460
|
+
context
|
|
461
|
+
};
|
|
462
|
+
detections.push(detection);
|
|
463
|
+
this.logDetection(detection, url);
|
|
464
|
+
if (pattern.action === "block") {
|
|
465
|
+
blocked = true;
|
|
466
|
+
cleanText = "";
|
|
467
|
+
break;
|
|
468
|
+
} else if (pattern.action === "strip") {
|
|
469
|
+
cleanText = cleanText.replace(matchedText, "");
|
|
470
|
+
}
|
|
471
|
+
if (match.index === pattern.regex.lastIndex) {
|
|
472
|
+
pattern.regex.lastIndex++;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (detections.some((d) => d.action === "strip")) {
|
|
477
|
+
cleanText = cleanText.replace(/\s+/g, " ").trim();
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
cleanText,
|
|
481
|
+
detections,
|
|
482
|
+
blocked,
|
|
483
|
+
scanTimeMs: performance.now() - startTime
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Логирует обнаружение в security audit trail.
|
|
488
|
+
*/
|
|
489
|
+
logDetection(detection, url) {
|
|
490
|
+
const entry = {
|
|
491
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
492
|
+
url,
|
|
493
|
+
patternId: detection.patternId,
|
|
494
|
+
description: detection.description,
|
|
495
|
+
severity: detection.severity,
|
|
496
|
+
action: detection.action,
|
|
497
|
+
matchedText: detection.matchedText,
|
|
498
|
+
context: detection.context
|
|
499
|
+
};
|
|
500
|
+
this.logEntries.push(entry);
|
|
501
|
+
if (this.logEntries.length > this.maxLogEntries) {
|
|
502
|
+
this.logEntries = this.logEntries.slice(-this.maxLogEntries);
|
|
503
|
+
}
|
|
504
|
+
this.writeLogEntry(entry);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Записывает одну запись в лог-файл.
|
|
508
|
+
*/
|
|
509
|
+
writeLogEntry(entry) {
|
|
510
|
+
try {
|
|
511
|
+
const logDir = join2(this.logPath, "..");
|
|
512
|
+
if (!existsSync2(logDir)) {
|
|
513
|
+
mkdirSync2(logDir, { recursive: true });
|
|
514
|
+
}
|
|
515
|
+
appendFileSync(this.logPath, JSON.stringify(entry) + "\n", "utf-8");
|
|
516
|
+
} catch (error) {
|
|
517
|
+
console.error(`[PromptInjectionFilter] Failed to write log entry: ${error}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Получает все логи (для аудита).
|
|
522
|
+
*/
|
|
523
|
+
getLogs() {
|
|
524
|
+
return [...this.logEntries];
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Очищает логи (для тестирования).
|
|
528
|
+
*/
|
|
529
|
+
clearLogs() {
|
|
530
|
+
this.logEntries = [];
|
|
531
|
+
try {
|
|
532
|
+
if (existsSync2(this.logPath)) {
|
|
533
|
+
writeFileSync2(this.logPath, "", "utf-8");
|
|
534
|
+
}
|
|
535
|
+
} catch (error) {
|
|
536
|
+
console.error(`[PromptInjectionFilter] Failed to clear logs: ${error}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Добавляет custom pattern динамически.
|
|
541
|
+
*/
|
|
542
|
+
addCustomPattern(pattern) {
|
|
543
|
+
if (pattern.type !== "regex") {
|
|
544
|
+
console.warn(`[PromptInjectionFilter] Only regex patterns are supported, skipping: ${pattern.id}`);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const compiled = {
|
|
548
|
+
...pattern,
|
|
549
|
+
regex: new RegExp(pattern.pattern, "gi"),
|
|
550
|
+
confidenceThreshold: pattern.confidence ?? this.confidenceThreshold
|
|
551
|
+
};
|
|
552
|
+
this.patterns.push(compiled);
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Удаляет custom pattern по ID.
|
|
556
|
+
*/
|
|
557
|
+
removePattern(patternId) {
|
|
558
|
+
const initialLength = this.patterns.length;
|
|
559
|
+
this.patterns = this.patterns.filter((p) => p.id !== patternId);
|
|
560
|
+
return this.patterns.length < initialLength;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Получает количество загруженных паттернов.
|
|
564
|
+
*/
|
|
565
|
+
getPatternCount() {
|
|
566
|
+
return this.patterns.length;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Включает/выключает фильтр.
|
|
570
|
+
*/
|
|
571
|
+
setEnabled(enabled) {
|
|
572
|
+
this.enabled = enabled;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Проверяет, включён ли фильтр.
|
|
576
|
+
*/
|
|
577
|
+
isEnabled() {
|
|
578
|
+
return this.enabled;
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
function createFilter(options) {
|
|
582
|
+
return new PromptInjectionFilter(options);
|
|
583
|
+
}
|
|
584
|
+
var globalFilter = null;
|
|
585
|
+
function getGlobalFilter() {
|
|
586
|
+
if (!globalFilter) {
|
|
587
|
+
globalFilter = new PromptInjectionFilter();
|
|
588
|
+
}
|
|
589
|
+
return globalFilter;
|
|
590
|
+
}
|
|
591
|
+
|
|
1
592
|
// src/capture/domExtractor.ts
|
|
2
593
|
var NON_RENDERABLE_TAGS = /* @__PURE__ */ new Set([
|
|
3
594
|
"script",
|
|
@@ -29,6 +620,16 @@ function ownText(el) {
|
|
|
29
620
|
}
|
|
30
621
|
return raw.replace(/\s+/g, " ").trim();
|
|
31
622
|
}
|
|
623
|
+
function filteredOwnText(el, url) {
|
|
624
|
+
const raw = ownText(el);
|
|
625
|
+
if (!raw) return raw;
|
|
626
|
+
const filter = getGlobalFilter();
|
|
627
|
+
if (!filter.isEnabled()) {
|
|
628
|
+
return raw;
|
|
629
|
+
}
|
|
630
|
+
const result = filter.scan(raw, url);
|
|
631
|
+
return result.cleanText;
|
|
632
|
+
}
|
|
32
633
|
function extractAttributes(el) {
|
|
33
634
|
const attributes = {};
|
|
34
635
|
for (const attr of Array.from(el.attributes)) {
|
|
@@ -73,7 +674,7 @@ function toRect(rect) {
|
|
|
73
674
|
function hasZeroBox(rect) {
|
|
74
675
|
return rect.width <= 0 || rect.height <= 0;
|
|
75
676
|
}
|
|
76
|
-
function collectVisibleChildren(parent, parentPath) {
|
|
677
|
+
function collectVisibleChildren(parent, parentPath, url) {
|
|
77
678
|
const result = [];
|
|
78
679
|
Array.from(parent.children).forEach((child, index) => {
|
|
79
680
|
const path = [...parentPath, index];
|
|
@@ -83,17 +684,17 @@ function collectVisibleChildren(parent, parentPath) {
|
|
|
83
684
|
}
|
|
84
685
|
const rect = toRect(child.getBoundingClientRect());
|
|
85
686
|
if (isVisibilityHidden(child) || hasZeroBox(rect)) {
|
|
86
|
-
result.push(...collectVisibleChildren(child, path));
|
|
687
|
+
result.push(...collectVisibleChildren(child, path, url));
|
|
87
688
|
return;
|
|
88
689
|
}
|
|
89
690
|
result.push({
|
|
90
691
|
tag,
|
|
91
692
|
indexPath: path,
|
|
92
693
|
rect,
|
|
93
|
-
text:
|
|
694
|
+
text: filteredOwnText(child, url) || null,
|
|
94
695
|
attributes: extractAttributes(child),
|
|
95
696
|
css: captureCss(child),
|
|
96
|
-
children: collectVisibleChildren(child, path)
|
|
697
|
+
children: collectVisibleChildren(child, path, url)
|
|
97
698
|
});
|
|
98
699
|
});
|
|
99
700
|
return result;
|
|
@@ -105,8 +706,8 @@ function defaultRoot() {
|
|
|
105
706
|
}
|
|
106
707
|
return body;
|
|
107
708
|
}
|
|
108
|
-
function extractDomTree(root = defaultRoot()) {
|
|
109
|
-
return collectVisibleChildren(root, []);
|
|
709
|
+
function extractDomTree(root = defaultRoot(), url) {
|
|
710
|
+
return collectVisibleChildren(root, [], url);
|
|
110
711
|
}
|
|
111
712
|
|
|
112
713
|
// src/segmentation/level1.ts
|
|
@@ -319,7 +920,7 @@ var INTERACTIVE_ATTRS = [
|
|
|
319
920
|
];
|
|
320
921
|
function relative(value, dimension) {
|
|
321
922
|
if (dimension <= 0) return 0;
|
|
322
|
-
return Math.round(value / dimension *
|
|
923
|
+
return Math.round(value / dimension * 100) / 100;
|
|
323
924
|
}
|
|
324
925
|
function defaultActions(t, st, attributes) {
|
|
325
926
|
if (st === "disabled") return void 0;
|
|
@@ -387,10 +988,10 @@ function extractVisualStyles(css) {
|
|
|
387
988
|
}
|
|
388
989
|
return hasContent ? style : void 0;
|
|
389
990
|
}
|
|
390
|
-
function toVslObject(el, viewport, includedChildren, textBlocks, counter) {
|
|
991
|
+
function toVslObject(el, viewport, includedChildren, textBlocks, counter, idGenerator) {
|
|
391
992
|
const t = el.t ?? "container";
|
|
392
993
|
const object = {
|
|
393
|
-
id:
|
|
994
|
+
id: idGenerator.generate(el.attributes["id"], t),
|
|
394
995
|
t,
|
|
395
996
|
p: [relative(el.rect.x, viewport.width), relative(el.rect.y, viewport.height)],
|
|
396
997
|
s: [Math.round(el.rect.width), Math.round(el.rect.height)]
|
|
@@ -424,12 +1025,12 @@ function toVslObject(el, viewport, includedChildren, textBlocks, counter) {
|
|
|
424
1025
|
}
|
|
425
1026
|
return object;
|
|
426
1027
|
}
|
|
427
|
-
function convert(el, viewport, textBlocks, counter) {
|
|
428
|
-
const includedChildren = el.ch.map((child) => convert(child, viewport, textBlocks, counter)).filter((obj) => obj !== null);
|
|
429
|
-
if (el.t !== null) return toVslObject(el, viewport, includedChildren, textBlocks, counter);
|
|
1028
|
+
function convert(el, viewport, textBlocks, counter, idGenerator) {
|
|
1029
|
+
const includedChildren = el.ch.map((child) => convert(child, viewport, textBlocks, counter, idGenerator)).filter((obj) => obj !== null);
|
|
1030
|
+
if (el.t !== null) return toVslObject(el, viewport, includedChildren, textBlocks, counter, idGenerator);
|
|
430
1031
|
const score = baseSemanticScore(el) + (includedChildren.length > 0 ? 1 : 0);
|
|
431
1032
|
if (score >= 3 || includedChildren.length > 0) {
|
|
432
|
-
return toVslObject(el, viewport, includedChildren, textBlocks, counter);
|
|
1033
|
+
return toVslObject(el, viewport, includedChildren, textBlocks, counter, idGenerator);
|
|
433
1034
|
}
|
|
434
1035
|
return null;
|
|
435
1036
|
}
|
|
@@ -478,7 +1079,8 @@ function buildVslDocument(elements, options = {}) {
|
|
|
478
1079
|
};
|
|
479
1080
|
const textBlocks = /* @__PURE__ */ new Map();
|
|
480
1081
|
const counter = { value: 0 };
|
|
481
|
-
const
|
|
1082
|
+
const idGenerator = new IdGenerator();
|
|
1083
|
+
const objects = elements.map((el) => convert(el, viewport, textBlocks, counter, idGenerator)).filter((obj) => obj !== null);
|
|
482
1084
|
const vslDocument = {
|
|
483
1085
|
vsl_version: VSL_VERSION,
|
|
484
1086
|
canvas: buildCanvas(options, viewport),
|
|
@@ -1937,10 +2539,14 @@ export {
|
|
|
1937
2539
|
ActionExecutionError,
|
|
1938
2540
|
AlibabaAdapter,
|
|
1939
2541
|
AnthropicAdapter,
|
|
2542
|
+
IdGenerator,
|
|
1940
2543
|
LEVEL1_TAG_MAP,
|
|
1941
2544
|
LlmError,
|
|
1942
2545
|
LlmValidationError,
|
|
1943
2546
|
OpenAIAdapter,
|
|
2547
|
+
PromptInjectionFilter,
|
|
2548
|
+
RemotePatternsLoader,
|
|
2549
|
+
TYPE_ABBREVIATIONS,
|
|
1944
2550
|
VALID_ACTIONS,
|
|
1945
2551
|
VSL_SDK_VERSION,
|
|
1946
2552
|
VSL_VERSION,
|
|
@@ -1953,9 +2559,12 @@ export {
|
|
|
1953
2559
|
computeContentHash,
|
|
1954
2560
|
computeCoordHash,
|
|
1955
2561
|
createCacheStore,
|
|
2562
|
+
createFilter,
|
|
2563
|
+
createIdGenerator,
|
|
1956
2564
|
diffVslDocuments,
|
|
1957
2565
|
executeAction,
|
|
1958
2566
|
extractDomTree,
|
|
2567
|
+
getGlobalFilter,
|
|
1959
2568
|
isAriaHidden,
|
|
1960
2569
|
isVslDiff,
|
|
1961
2570
|
ownText,
|