shroud-privacy 2.2.11 → 2.2.13
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/README.md +19 -10
- package/dist/hooks.js +246 -14
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -2
- package/dist/agent-session.d.ts +0 -259
- package/dist/agent-session.js +0 -693
- package/dist/compliance.d.ts +0 -44
- package/dist/compliance.js +0 -76
- package/dist/dashboard.d.ts +0 -42
- package/dist/dashboard.js +0 -1558
- package/dist/detectors/injection-multilingual.d.ts +0 -27
- package/dist/detectors/injection-multilingual.js +0 -399
- package/dist/detectors/injection-signatures.d.ts +0 -26
- package/dist/detectors/injection-signatures.js +0 -508
- package/dist/detectors/injection.d.ts +0 -56
- package/dist/detectors/injection.js +0 -269
- package/dist/detectors/tool-guard.d.ts +0 -27
- package/dist/detectors/tool-guard.js +0 -418
- package/dist/event-grader.d.ts +0 -97
- package/dist/event-grader.js +0 -214
- package/dist/exposure.d.ts +0 -29
- package/dist/exposure.js +0 -72
- package/dist/policy.d.ts +0 -99
- package/dist/policy.js +0 -212
- package/dist/profiler-analysis.d.ts +0 -35
- package/dist/profiler-analysis.js +0 -230
- package/dist/profiler-store.d.ts +0 -33
- package/dist/profiler-store.js +0 -118
- package/dist/profiler-types.d.ts +0 -128
- package/dist/profiler-types.js +0 -16
- package/dist/profiler.d.ts +0 -81
- package/dist/profiler.js +0 -392
- package/dist/security-event.d.ts +0 -70
- package/dist/security-event.js +0 -80
- package/dist/siem.d.ts +0 -49
- package/dist/siem.js +0 -113
- package/dist/signature-loader.d.ts +0 -113
- package/dist/signature-loader.js +0 -255
- package/dist/store-file.d.ts +0 -26
- package/dist/store-file.js +0 -79
package/dist/siem.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SIEM integration — ships security events to external systems.
|
|
3
|
-
*
|
|
4
|
-
* Two output modes:
|
|
5
|
-
* 1. Webhook POST: sends events as JSON to a configured URL (Splunk HEC, Grafana Loki, custom)
|
|
6
|
-
* 2. JSONL file: appends events as newline-delimited JSON to a local file
|
|
7
|
-
*
|
|
8
|
-
* Both are async fire-and-forget — never blocks the request pipeline.
|
|
9
|
-
* Zero runtime dependencies.
|
|
10
|
-
*/
|
|
11
|
-
import { appendFileSync, mkdirSync } from "node:fs";
|
|
12
|
-
import { dirname } from "node:path";
|
|
13
|
-
import { request as httpRequest } from "node:http";
|
|
14
|
-
import { request as httpsRequest } from "node:https";
|
|
15
|
-
/**
|
|
16
|
-
* SIEM event shipper. Subscribes to SecurityEventBus and ships events
|
|
17
|
-
* to configured destinations.
|
|
18
|
-
*/
|
|
19
|
-
export class SiemShipper {
|
|
20
|
-
_config;
|
|
21
|
-
_buffer = [];
|
|
22
|
-
_flushTimer = null;
|
|
23
|
-
_stats = { shipped: 0, webhookErrors: 0, fileErrors: 0 };
|
|
24
|
-
constructor(config) {
|
|
25
|
-
this._config = config;
|
|
26
|
-
// Start flush timer if batching
|
|
27
|
-
if (config.batchSize > 1 && config.flushIntervalMs > 0) {
|
|
28
|
-
this._flushTimer = setInterval(() => this.flush(), config.flushIntervalMs);
|
|
29
|
-
}
|
|
30
|
-
// Ensure JSONL directory exists
|
|
31
|
-
if (config.jsonlPath) {
|
|
32
|
-
try {
|
|
33
|
-
mkdirSync(dirname(config.jsonlPath), { recursive: true });
|
|
34
|
-
}
|
|
35
|
-
catch { }
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
/** Called for each security event. Buffers and flushes. */
|
|
39
|
-
onEvent(event) {
|
|
40
|
-
this._buffer.push(event);
|
|
41
|
-
if (this._buffer.length >= this._config.batchSize) {
|
|
42
|
-
this.flush();
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
/** Flush buffered events to all configured destinations. */
|
|
46
|
-
flush() {
|
|
47
|
-
if (this._buffer.length === 0)
|
|
48
|
-
return;
|
|
49
|
-
const events = this._buffer.splice(0);
|
|
50
|
-
// JSONL file output
|
|
51
|
-
if (this._config.jsonlPath) {
|
|
52
|
-
try {
|
|
53
|
-
const lines = events.map(e => JSON.stringify(e)).join("\n") + "\n";
|
|
54
|
-
appendFileSync(this._config.jsonlPath, lines);
|
|
55
|
-
this._stats.shipped += events.length;
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
this._stats.fileErrors++;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
// Webhook POST
|
|
62
|
-
if (this._config.webhookUrl) {
|
|
63
|
-
this._postWebhook(events);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
/** Stop the flush timer and flush remaining events. */
|
|
67
|
-
stop() {
|
|
68
|
-
if (this._flushTimer) {
|
|
69
|
-
clearInterval(this._flushTimer);
|
|
70
|
-
this._flushTimer = null;
|
|
71
|
-
}
|
|
72
|
-
this.flush();
|
|
73
|
-
}
|
|
74
|
-
/** Get shipping stats. */
|
|
75
|
-
getStats() {
|
|
76
|
-
return { ...this._stats, buffered: this._buffer.length };
|
|
77
|
-
}
|
|
78
|
-
_postWebhook(events) {
|
|
79
|
-
try {
|
|
80
|
-
const url = new URL(this._config.webhookUrl);
|
|
81
|
-
const isHttps = url.protocol === "https:";
|
|
82
|
-
const reqFn = isHttps ? httpsRequest : httpRequest;
|
|
83
|
-
const body = JSON.stringify({ events, count: events.length, timestamp: new Date().toISOString() });
|
|
84
|
-
const options = {
|
|
85
|
-
hostname: url.hostname,
|
|
86
|
-
port: url.port || (isHttps ? 443 : 80),
|
|
87
|
-
path: url.pathname + url.search,
|
|
88
|
-
method: "POST",
|
|
89
|
-
headers: {
|
|
90
|
-
"Content-Type": "application/json",
|
|
91
|
-
"Content-Length": Buffer.byteLength(body),
|
|
92
|
-
...(this._config.webhookAuth ? { Authorization: this._config.webhookAuth } : {}),
|
|
93
|
-
},
|
|
94
|
-
};
|
|
95
|
-
const req = reqFn(options, (res) => {
|
|
96
|
-
// Drain response
|
|
97
|
-
res.resume();
|
|
98
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
99
|
-
this._stats.shipped += events.length;
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
this._stats.webhookErrors++;
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
req.on("error", () => { this._stats.webhookErrors++; });
|
|
106
|
-
req.write(body);
|
|
107
|
-
req.end();
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
this._stats.webhookErrors++;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hot-refresh signature loader.
|
|
3
|
-
*
|
|
4
|
-
* Fetches external signature definitions from a URL or local file,
|
|
5
|
-
* merges them with built-in signatures at runtime. Supports periodic
|
|
6
|
-
* polling with atomic swap — no restart needed.
|
|
7
|
-
*
|
|
8
|
-
* Zero runtime dependencies — uses Node.js built-in http/https + fs.
|
|
9
|
-
*
|
|
10
|
-
* Signature JSON schema:
|
|
11
|
-
* {
|
|
12
|
-
* "version": "1.0",
|
|
13
|
-
* "updated": "2026-03-30T12:00:00Z",
|
|
14
|
-
* "injectionSignatures": [
|
|
15
|
-
* { "id": "custom_sig_1", "threatClass": "instruction_override",
|
|
16
|
-
* "pattern": "ignore all safety", "flags": "gi",
|
|
17
|
-
* "severity": "high", "description": "...", "direction": "request" }
|
|
18
|
-
* ],
|
|
19
|
-
* "toolGuardPatterns": [
|
|
20
|
-
* { "id": "custom_tg_1", "paramPattern": "kubectl delete ns",
|
|
21
|
-
* "flags": "gi", "severity": "high", "description": "...",
|
|
22
|
-
* "block": true, "toolName": null }
|
|
23
|
-
* ]
|
|
24
|
-
* }
|
|
25
|
-
*/
|
|
26
|
-
/** External signature definition (JSON-friendly — pattern is a string, not RegExp). */
|
|
27
|
-
export interface ExternalSignature {
|
|
28
|
-
id: string;
|
|
29
|
-
threatClass: string;
|
|
30
|
-
pattern: string;
|
|
31
|
-
flags?: string;
|
|
32
|
-
severity: "low" | "medium" | "high";
|
|
33
|
-
description: string;
|
|
34
|
-
direction: "request" | "response" | "both";
|
|
35
|
-
}
|
|
36
|
-
/** External tool guard pattern (JSON-friendly). */
|
|
37
|
-
export interface ExternalToolGuard {
|
|
38
|
-
id: string;
|
|
39
|
-
toolName: string | null;
|
|
40
|
-
paramPattern: string;
|
|
41
|
-
flags?: string;
|
|
42
|
-
severity: "low" | "medium" | "high";
|
|
43
|
-
description: string;
|
|
44
|
-
block: boolean;
|
|
45
|
-
}
|
|
46
|
-
/** The full signature feed payload. */
|
|
47
|
-
export interface SignatureFeed {
|
|
48
|
-
version: string;
|
|
49
|
-
updated: string;
|
|
50
|
-
injectionSignatures?: ExternalSignature[];
|
|
51
|
-
toolGuardPatterns?: ExternalToolGuard[];
|
|
52
|
-
}
|
|
53
|
-
/** Compiled signatures ready for the scanner. */
|
|
54
|
-
export interface CompiledSignatures {
|
|
55
|
-
injection: Array<{
|
|
56
|
-
id: string;
|
|
57
|
-
threatClass: string;
|
|
58
|
-
pattern: RegExp;
|
|
59
|
-
severity: "low" | "medium" | "high";
|
|
60
|
-
description: string;
|
|
61
|
-
direction: "request" | "response" | "both";
|
|
62
|
-
}>;
|
|
63
|
-
toolGuard: Array<{
|
|
64
|
-
id: string;
|
|
65
|
-
toolName: string | null;
|
|
66
|
-
paramPattern: RegExp;
|
|
67
|
-
severity: "low" | "medium" | "high";
|
|
68
|
-
description: string;
|
|
69
|
-
block: boolean;
|
|
70
|
-
}>;
|
|
71
|
-
feedVersion: string;
|
|
72
|
-
feedUpdated: string;
|
|
73
|
-
loadedAt: number;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Signature loader with hot-refresh support.
|
|
77
|
-
*/
|
|
78
|
-
export declare class SignatureLoader {
|
|
79
|
-
private _url;
|
|
80
|
-
private _filePath;
|
|
81
|
-
private _refreshMs;
|
|
82
|
-
private _timer;
|
|
83
|
-
private _current;
|
|
84
|
-
private _onUpdate;
|
|
85
|
-
private _cacheDir;
|
|
86
|
-
constructor(opts: {
|
|
87
|
-
url?: string | null;
|
|
88
|
-
filePath?: string | null;
|
|
89
|
-
refreshSec?: number;
|
|
90
|
-
cacheDir?: string;
|
|
91
|
-
});
|
|
92
|
-
/** Register a callback for when signatures are refreshed. */
|
|
93
|
-
onUpdate(cb: (sigs: CompiledSignatures) => void): void;
|
|
94
|
-
/** Get the currently loaded external signatures (null if none loaded). */
|
|
95
|
-
getCurrent(): CompiledSignatures | null;
|
|
96
|
-
/** Start polling. Does an immediate load, then polls on interval. */
|
|
97
|
-
start(): Promise<void>;
|
|
98
|
-
/** Stop polling. */
|
|
99
|
-
stop(): void;
|
|
100
|
-
/** Force a manual refresh. */
|
|
101
|
-
refresh(): Promise<CompiledSignatures | null>;
|
|
102
|
-
private _refresh;
|
|
103
|
-
/** Compile JSON signature definitions into RegExp objects with safety validation. */
|
|
104
|
-
private _compile;
|
|
105
|
-
/** Validate an injection signature before compilation. */
|
|
106
|
-
private _validateSig;
|
|
107
|
-
/** Validate a tool guard pattern before compilation. */
|
|
108
|
-
private _validateToolGuard;
|
|
109
|
-
/** Sanitize regex flags — only allow safe flags. */
|
|
110
|
-
private _sanitizeFlags;
|
|
111
|
-
/** Fetch a URL using Node.js built-in http/https. Zero dependencies. */
|
|
112
|
-
private _fetchUrl;
|
|
113
|
-
}
|
package/dist/signature-loader.js
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hot-refresh signature loader.
|
|
3
|
-
*
|
|
4
|
-
* Fetches external signature definitions from a URL or local file,
|
|
5
|
-
* merges them with built-in signatures at runtime. Supports periodic
|
|
6
|
-
* polling with atomic swap — no restart needed.
|
|
7
|
-
*
|
|
8
|
-
* Zero runtime dependencies — uses Node.js built-in http/https + fs.
|
|
9
|
-
*
|
|
10
|
-
* Signature JSON schema:
|
|
11
|
-
* {
|
|
12
|
-
* "version": "1.0",
|
|
13
|
-
* "updated": "2026-03-30T12:00:00Z",
|
|
14
|
-
* "injectionSignatures": [
|
|
15
|
-
* { "id": "custom_sig_1", "threatClass": "instruction_override",
|
|
16
|
-
* "pattern": "ignore all safety", "flags": "gi",
|
|
17
|
-
* "severity": "high", "description": "...", "direction": "request" }
|
|
18
|
-
* ],
|
|
19
|
-
* "toolGuardPatterns": [
|
|
20
|
-
* { "id": "custom_tg_1", "paramPattern": "kubectl delete ns",
|
|
21
|
-
* "flags": "gi", "severity": "high", "description": "...",
|
|
22
|
-
* "block": true, "toolName": null }
|
|
23
|
-
* ]
|
|
24
|
-
* }
|
|
25
|
-
*/
|
|
26
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
27
|
-
import { join } from "node:path";
|
|
28
|
-
import * as nodeHttps from "node:https";
|
|
29
|
-
import * as nodeHttp from "node:http";
|
|
30
|
-
/**
|
|
31
|
-
* Signature loader with hot-refresh support.
|
|
32
|
-
*/
|
|
33
|
-
export class SignatureLoader {
|
|
34
|
-
_url;
|
|
35
|
-
_filePath;
|
|
36
|
-
_refreshMs;
|
|
37
|
-
_timer = null;
|
|
38
|
-
_current = null;
|
|
39
|
-
_onUpdate = null;
|
|
40
|
-
_cacheDir;
|
|
41
|
-
constructor(opts) {
|
|
42
|
-
this._url = opts.url || null;
|
|
43
|
-
this._filePath = opts.filePath || null;
|
|
44
|
-
this._refreshMs = (opts.refreshSec || 3600) * 1000;
|
|
45
|
-
this._cacheDir = opts.cacheDir || "/tmp/shroud-signatures";
|
|
46
|
-
}
|
|
47
|
-
/** Register a callback for when signatures are refreshed. */
|
|
48
|
-
onUpdate(cb) {
|
|
49
|
-
this._onUpdate = cb;
|
|
50
|
-
}
|
|
51
|
-
/** Get the currently loaded external signatures (null if none loaded). */
|
|
52
|
-
getCurrent() {
|
|
53
|
-
return this._current;
|
|
54
|
-
}
|
|
55
|
-
/** Start polling. Does an immediate load, then polls on interval. */
|
|
56
|
-
async start() {
|
|
57
|
-
if (!this._url && !this._filePath)
|
|
58
|
-
return;
|
|
59
|
-
// Immediate load
|
|
60
|
-
await this._refresh();
|
|
61
|
-
// Periodic poll
|
|
62
|
-
if (this._refreshMs > 0) {
|
|
63
|
-
this._timer = setInterval(() => this._refresh(), this._refreshMs);
|
|
64
|
-
// Don't keep the process alive for the timer
|
|
65
|
-
if (this._timer.unref)
|
|
66
|
-
this._timer.unref();
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
/** Stop polling. */
|
|
70
|
-
stop() {
|
|
71
|
-
if (this._timer) {
|
|
72
|
-
clearInterval(this._timer);
|
|
73
|
-
this._timer = null;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
/** Force a manual refresh. */
|
|
77
|
-
async refresh() {
|
|
78
|
-
await this._refresh();
|
|
79
|
-
return this._current;
|
|
80
|
-
}
|
|
81
|
-
async _refresh() {
|
|
82
|
-
try {
|
|
83
|
-
let json = null;
|
|
84
|
-
// Try URL first
|
|
85
|
-
if (this._url) {
|
|
86
|
-
json = await this._fetchUrl(this._url);
|
|
87
|
-
// Cache to local file for offline resilience
|
|
88
|
-
if (json) {
|
|
89
|
-
try {
|
|
90
|
-
mkdirSync(this._cacheDir, { recursive: true });
|
|
91
|
-
writeFileSync(join(this._cacheDir, "signatures-cache.json"), json, "utf-8");
|
|
92
|
-
}
|
|
93
|
-
catch { }
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
// Fall back to local file
|
|
97
|
-
if (!json && this._filePath) {
|
|
98
|
-
try {
|
|
99
|
-
json = readFileSync(this._filePath, "utf-8");
|
|
100
|
-
}
|
|
101
|
-
catch { }
|
|
102
|
-
}
|
|
103
|
-
// Fall back to cached copy
|
|
104
|
-
if (!json) {
|
|
105
|
-
const cachePath = join(this._cacheDir, "signatures-cache.json");
|
|
106
|
-
if (existsSync(cachePath)) {
|
|
107
|
-
try {
|
|
108
|
-
json = readFileSync(cachePath, "utf-8");
|
|
109
|
-
}
|
|
110
|
-
catch { }
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if (!json)
|
|
114
|
-
return;
|
|
115
|
-
// Max feed size: 500KB — reject bloated/malicious payloads
|
|
116
|
-
if (json.length > 512_000)
|
|
117
|
-
return;
|
|
118
|
-
const feed = JSON.parse(json);
|
|
119
|
-
// Max 500 signatures per feed — prevent resource exhaustion
|
|
120
|
-
if ((feed.injectionSignatures?.length || 0) > 500)
|
|
121
|
-
return;
|
|
122
|
-
if ((feed.toolGuardPatterns?.length || 0) > 500)
|
|
123
|
-
return;
|
|
124
|
-
const compiled = this._compile(feed);
|
|
125
|
-
// Only update if version changed or first load
|
|
126
|
-
if (!this._current || compiled.feedVersion !== this._current.feedVersion ||
|
|
127
|
-
compiled.feedUpdated !== this._current.feedUpdated) {
|
|
128
|
-
this._current = compiled;
|
|
129
|
-
if (this._onUpdate)
|
|
130
|
-
this._onUpdate(compiled);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
// Non-fatal — keep using existing signatures
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
/** Compile JSON signature definitions into RegExp objects with safety validation. */
|
|
138
|
-
_compile(feed) {
|
|
139
|
-
const injection = (feed.injectionSignatures || [])
|
|
140
|
-
.filter(sig => this._validateSig(sig))
|
|
141
|
-
.map(sig => ({
|
|
142
|
-
id: sig.id,
|
|
143
|
-
threatClass: sig.threatClass,
|
|
144
|
-
pattern: new RegExp(sig.pattern, this._sanitizeFlags(sig.flags)),
|
|
145
|
-
severity: sig.severity,
|
|
146
|
-
description: sig.description,
|
|
147
|
-
direction: sig.direction,
|
|
148
|
-
}));
|
|
149
|
-
const toolGuard = (feed.toolGuardPatterns || [])
|
|
150
|
-
.filter(tg => this._validateToolGuard(tg))
|
|
151
|
-
.map(tg => ({
|
|
152
|
-
id: tg.id,
|
|
153
|
-
toolName: tg.toolName,
|
|
154
|
-
paramPattern: new RegExp(tg.paramPattern, this._sanitizeFlags(tg.flags)),
|
|
155
|
-
severity: tg.severity,
|
|
156
|
-
description: tg.description,
|
|
157
|
-
block: tg.block,
|
|
158
|
-
}));
|
|
159
|
-
return {
|
|
160
|
-
injection,
|
|
161
|
-
toolGuard,
|
|
162
|
-
feedVersion: feed.version || "unknown",
|
|
163
|
-
feedUpdated: feed.updated || new Date().toISOString(),
|
|
164
|
-
loadedAt: Date.now(),
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
/** Validate an injection signature before compilation. */
|
|
168
|
-
_validateSig(sig) {
|
|
169
|
-
// Must have required fields
|
|
170
|
-
if (!sig.id || !sig.pattern || !sig.severity || !sig.direction)
|
|
171
|
-
return false;
|
|
172
|
-
// ID must be prefixed with ext_ (external signatures can't impersonate built-ins)
|
|
173
|
-
if (!sig.id.startsWith("ext_"))
|
|
174
|
-
return false;
|
|
175
|
-
// Severity must be valid
|
|
176
|
-
if (!["low", "medium", "high"].includes(sig.severity))
|
|
177
|
-
return false;
|
|
178
|
-
// Direction must be valid
|
|
179
|
-
if (!["request", "response", "both"].includes(sig.direction))
|
|
180
|
-
return false;
|
|
181
|
-
// Pattern length cap — prevents ReDoS via catastrophic backtracking
|
|
182
|
-
if (sig.pattern.length > 500)
|
|
183
|
-
return false;
|
|
184
|
-
// Block nested quantifiers (ReDoS: (a+)+ or (a*)*b)
|
|
185
|
-
if (/\([^)]*[+*][^)]*\)[+*]/.test(sig.pattern))
|
|
186
|
-
return false;
|
|
187
|
-
// Test compile — reject if regex is invalid
|
|
188
|
-
try {
|
|
189
|
-
new RegExp(sig.pattern);
|
|
190
|
-
}
|
|
191
|
-
catch {
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
// Description length cap
|
|
195
|
-
if ((sig.description || "").length > 300)
|
|
196
|
-
return false;
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
199
|
-
/** Validate a tool guard pattern before compilation. */
|
|
200
|
-
_validateToolGuard(tg) {
|
|
201
|
-
if (!tg.id || !tg.paramPattern || !tg.severity)
|
|
202
|
-
return false;
|
|
203
|
-
if (!tg.id.startsWith("ext_"))
|
|
204
|
-
return false;
|
|
205
|
-
if (!["low", "medium", "high"].includes(tg.severity))
|
|
206
|
-
return false;
|
|
207
|
-
if (tg.paramPattern.length > 500)
|
|
208
|
-
return false;
|
|
209
|
-
if (/\([^)]*[+*][^)]*\)[+*]/.test(tg.paramPattern))
|
|
210
|
-
return false;
|
|
211
|
-
try {
|
|
212
|
-
new RegExp(tg.paramPattern);
|
|
213
|
-
}
|
|
214
|
-
catch {
|
|
215
|
-
return false;
|
|
216
|
-
}
|
|
217
|
-
if ((tg.description || "").length > 300)
|
|
218
|
-
return false;
|
|
219
|
-
return true;
|
|
220
|
-
}
|
|
221
|
-
/** Sanitize regex flags — only allow safe flags. */
|
|
222
|
-
_sanitizeFlags(flags) {
|
|
223
|
-
if (!flags)
|
|
224
|
-
return "gi";
|
|
225
|
-
return flags.replace(/[^gimsuy]/g, "") || "gi";
|
|
226
|
-
}
|
|
227
|
-
/** Fetch a URL using Node.js built-in http/https. Zero dependencies. */
|
|
228
|
-
_fetchUrl(url) {
|
|
229
|
-
return new Promise((resolve) => {
|
|
230
|
-
try {
|
|
231
|
-
const mod = url.startsWith("https") ? nodeHttps : nodeHttp;
|
|
232
|
-
const req = mod.get(url, { timeout: 10_000 }, (res) => {
|
|
233
|
-
// Follow redirects (1 level)
|
|
234
|
-
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
235
|
-
this._fetchUrl(res.headers.location).then(resolve);
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
if (res.statusCode !== 200) {
|
|
239
|
-
resolve(null);
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
const chunks = [];
|
|
243
|
-
res.on("data", (c) => chunks.push(c));
|
|
244
|
-
res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
245
|
-
res.on("error", () => resolve(null));
|
|
246
|
-
});
|
|
247
|
-
req.on("error", () => resolve(null));
|
|
248
|
-
req.on("timeout", () => { req.destroy(); resolve(null); });
|
|
249
|
-
}
|
|
250
|
-
catch {
|
|
251
|
-
resolve(null);
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
}
|
package/dist/store-file.d.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File-backed mapping store — persists to JSON on disk.
|
|
3
|
-
* Wraps MemoryStore with periodic flush and load-on-startup.
|
|
4
|
-
*/
|
|
5
|
-
import { Category } from "./types.js";
|
|
6
|
-
import { MappingStore } from "./store.js";
|
|
7
|
-
export declare class FileBackedStore implements MappingStore {
|
|
8
|
-
private _inner;
|
|
9
|
-
private _filePath;
|
|
10
|
-
private _dirty;
|
|
11
|
-
private _flushTimer;
|
|
12
|
-
private _flushIntervalMs;
|
|
13
|
-
constructor(filePath: string, maxSize?: number, flushIntervalMs?: number);
|
|
14
|
-
put(real: string, fake: string, category: Category): void;
|
|
15
|
-
getFake(real: string): string | undefined;
|
|
16
|
-
getReal(fake: string): string | undefined;
|
|
17
|
-
getCategory(real: string): Category | undefined;
|
|
18
|
-
allMappings(): Map<string, string>;
|
|
19
|
-
size(): number;
|
|
20
|
-
clear(): void;
|
|
21
|
-
/** Flush dirty mappings to disk. */
|
|
22
|
-
flush(): void;
|
|
23
|
-
/** Stop the periodic flush timer. */
|
|
24
|
-
stop(): void;
|
|
25
|
-
private _loadFromDisk;
|
|
26
|
-
}
|
package/dist/store-file.js
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File-backed mapping store — persists to JSON on disk.
|
|
3
|
-
* Wraps MemoryStore with periodic flush and load-on-startup.
|
|
4
|
-
*/
|
|
5
|
-
import { writeFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
6
|
-
import { dirname } from "node:path";
|
|
7
|
-
import { MemoryStore } from "./store.js";
|
|
8
|
-
export class FileBackedStore {
|
|
9
|
-
_inner;
|
|
10
|
-
_filePath;
|
|
11
|
-
_dirty = false;
|
|
12
|
-
_flushTimer = null;
|
|
13
|
-
_flushIntervalMs;
|
|
14
|
-
constructor(filePath, maxSize = 0, flushIntervalMs = 5000) {
|
|
15
|
-
this._inner = new MemoryStore(maxSize);
|
|
16
|
-
this._filePath = filePath;
|
|
17
|
-
this._flushIntervalMs = flushIntervalMs;
|
|
18
|
-
// Ensure directory exists
|
|
19
|
-
const dir = dirname(filePath);
|
|
20
|
-
if (!existsSync(dir)) {
|
|
21
|
-
mkdirSync(dir, { recursive: true });
|
|
22
|
-
}
|
|
23
|
-
// Load existing data
|
|
24
|
-
this._loadFromDisk();
|
|
25
|
-
// Periodic flush
|
|
26
|
-
this._flushTimer = setInterval(() => this.flush(), this._flushIntervalMs);
|
|
27
|
-
if (this._flushTimer.unref)
|
|
28
|
-
this._flushTimer.unref();
|
|
29
|
-
}
|
|
30
|
-
put(real, fake, category) {
|
|
31
|
-
this._inner.put(real, fake, category);
|
|
32
|
-
this._dirty = true;
|
|
33
|
-
}
|
|
34
|
-
getFake(real) { return this._inner.getFake(real); }
|
|
35
|
-
getReal(fake) { return this._inner.getReal(fake); }
|
|
36
|
-
getCategory(real) { return this._inner.getCategory(real); }
|
|
37
|
-
allMappings() { return this._inner.allMappings(); }
|
|
38
|
-
size() { return this._inner.size(); }
|
|
39
|
-
clear() {
|
|
40
|
-
this._inner.clear();
|
|
41
|
-
this._dirty = true;
|
|
42
|
-
this.flush();
|
|
43
|
-
}
|
|
44
|
-
/** Flush dirty mappings to disk. */
|
|
45
|
-
flush() {
|
|
46
|
-
if (!this._dirty)
|
|
47
|
-
return;
|
|
48
|
-
try {
|
|
49
|
-
const data = this._inner.export("", undefined);
|
|
50
|
-
writeFileSync(this._filePath, JSON.stringify(data, null, 2) + "\n");
|
|
51
|
-
this._dirty = false;
|
|
52
|
-
}
|
|
53
|
-
catch (e) {
|
|
54
|
-
// best-effort — log but don't crash
|
|
55
|
-
process.stderr.write(`[shroud-store] Flush failed: ${e.message}\n`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
/** Stop the periodic flush timer. */
|
|
59
|
-
stop() {
|
|
60
|
-
if (this._flushTimer) {
|
|
61
|
-
clearInterval(this._flushTimer);
|
|
62
|
-
this._flushTimer = null;
|
|
63
|
-
}
|
|
64
|
-
this.flush(); // final flush
|
|
65
|
-
}
|
|
66
|
-
_loadFromDisk() {
|
|
67
|
-
if (!existsSync(this._filePath))
|
|
68
|
-
return;
|
|
69
|
-
try {
|
|
70
|
-
const raw = readFileSync(this._filePath, "utf-8");
|
|
71
|
-
const data = JSON.parse(raw);
|
|
72
|
-
this._inner.import(data);
|
|
73
|
-
process.stderr.write(`[shroud-store] Loaded ${data.mappings.length} mappings from ${this._filePath}\n`);
|
|
74
|
-
}
|
|
75
|
-
catch (e) {
|
|
76
|
-
process.stderr.write(`[shroud-store] Load failed: ${e.message}\n`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|