@vtstech/pi-shared 1.1.4 → 1.1.5
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 +2 -2
- package/ollama.js +1 -1
- package/package.json +1 -1
- package/security.js +121 -46
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ This is an internal dependency — you don't need to install it directly. It's p
|
|
|
10
10
|
|--------|-------------|
|
|
11
11
|
| `format` | Section headers, indicators (ok/fail/warn/info), numeric formatters (bytes, ms, percentages), string utilities |
|
|
12
12
|
| `ollama` | Ollama base URL resolution, models.json I/O with TTL cache, model family detection, provider detection, Ollama API helpers |
|
|
13
|
-
| `security` |
|
|
13
|
+
| `security` | Security mode toggle (`basic`/`max`), partitioned command blocklist (41 CRITICAL + 25 EXTENDED), mode-aware SSRF (19 + 7 patterns), path validation with symlink dereference, URL validation, command sanitization, audit logging with mode tracking (`AUDIT_LOG_PATH` exported) |
|
|
14
14
|
| `types` | Type definitions (ToolSupportLevel, AuditEntry, etc.) |
|
|
15
15
|
|
|
16
16
|
## Usage
|
|
@@ -27,4 +27,4 @@ import { readModelsJson, getOllamaBaseUrl } from "@vtstech/pi-shared/ollama";
|
|
|
27
27
|
|
|
28
28
|
## License
|
|
29
29
|
|
|
30
|
-
MIT — [VTSTech](https://www.vts-tech.org)
|
|
30
|
+
MIT — [VTSTech](https://www.vts-tech.org)
|
package/ollama.js
CHANGED
|
@@ -12,7 +12,7 @@ function debugLog(module, message, ...args) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
// shared/ollama.ts
|
|
15
|
-
var EXTENSION_VERSION = "1.1.
|
|
15
|
+
var EXTENSION_VERSION = "1.1.5";
|
|
16
16
|
var MODELS_JSON_PATH = path.join(os.homedir(), ".pi", "agent", "models.json");
|
|
17
17
|
var _modelsJsonCache = null;
|
|
18
18
|
var _ollamaBaseUrlCache = null;
|
package/package.json
CHANGED
package/security.js
CHANGED
|
@@ -13,53 +13,69 @@ function debugLog(module, message, ...args) {
|
|
|
13
13
|
|
|
14
14
|
// shared/security.ts
|
|
15
15
|
var SETTINGS_PATH = path.join(os.homedir(), ".pi", "agent", "settings.json");
|
|
16
|
-
var
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
var SECURITY_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "security.json");
|
|
17
|
+
function getSecurityMode() {
|
|
18
|
+
try {
|
|
19
|
+
if (!fs.existsSync(SECURITY_CONFIG_PATH)) return "max";
|
|
20
|
+
const raw = fs.readFileSync(SECURITY_CONFIG_PATH, "utf-8");
|
|
21
|
+
const config = JSON.parse(raw);
|
|
22
|
+
if (config.mode === "basic" || config.mode === "max") return config.mode;
|
|
23
|
+
return "max";
|
|
24
|
+
} catch (err) {
|
|
25
|
+
debugLog("security", `failed to read security config at ${SECURITY_CONFIG_PATH}`, err);
|
|
26
|
+
return "max";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function setSecurityMode(mode) {
|
|
30
|
+
const configDir = path.dirname(SECURITY_CONFIG_PATH);
|
|
31
|
+
try {
|
|
32
|
+
if (!fs.existsSync(configDir)) {
|
|
33
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
const config = { mode, lastUpdated: (/* @__PURE__ */ new Date()).toISOString() };
|
|
36
|
+
fs.writeFileSync(SECURITY_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
37
|
+
const verify = JSON.parse(fs.readFileSync(SECURITY_CONFIG_PATH, "utf-8"));
|
|
38
|
+
if (verify.mode !== mode) {
|
|
39
|
+
debugLog("security", `security config write verification failed: expected ${mode}, got ${verify.mode}`);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
debugLog("security", `security mode set to ${mode}`, { path: SECURITY_CONFIG_PATH });
|
|
43
|
+
return true;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
debugLog("security", `failed to write security config to ${SECURITY_CONFIG_PATH}`, err);
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
var CRITICAL_COMMANDS = /* @__PURE__ */ new Set([
|
|
50
|
+
// Filesystem destruction (irrecoverable)
|
|
23
51
|
"mkfs",
|
|
24
52
|
"dd",
|
|
25
53
|
"shred",
|
|
26
54
|
"wipe",
|
|
27
55
|
"srm",
|
|
28
|
-
|
|
29
|
-
"
|
|
56
|
+
"format",
|
|
57
|
+
"fdisk",
|
|
58
|
+
// Privilege escalation (non-sudo)
|
|
30
59
|
"su",
|
|
31
60
|
"doas",
|
|
32
61
|
"pkexec",
|
|
33
62
|
"gksudo",
|
|
34
63
|
"kdesu",
|
|
35
|
-
// Network
|
|
64
|
+
// Network attack tools
|
|
36
65
|
"nmap",
|
|
37
66
|
"nc",
|
|
38
67
|
"netcat",
|
|
39
68
|
"telnet",
|
|
40
|
-
|
|
41
|
-
"curl",
|
|
69
|
+
// Remote access
|
|
42
70
|
"ssh",
|
|
43
71
|
"scp",
|
|
44
72
|
"sftp",
|
|
45
73
|
"rsync",
|
|
46
|
-
//
|
|
47
|
-
"apt",
|
|
48
|
-
"apt-get",
|
|
49
|
-
"yum",
|
|
50
|
-
"dnf",
|
|
51
|
-
"pacman",
|
|
52
|
-
"pip",
|
|
53
|
-
"npm",
|
|
54
|
-
"yarn",
|
|
55
|
-
"cargo",
|
|
56
|
-
// Process control
|
|
74
|
+
// Process killing
|
|
57
75
|
"kill",
|
|
58
76
|
"killall",
|
|
59
77
|
"pkill",
|
|
60
78
|
"xkill",
|
|
61
|
-
"systemctl",
|
|
62
|
-
"service",
|
|
63
79
|
// User management
|
|
64
80
|
"useradd",
|
|
65
81
|
"userdel",
|
|
@@ -73,30 +89,56 @@ var BLOCKED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
73
89
|
"source",
|
|
74
90
|
".",
|
|
75
91
|
"alias",
|
|
76
|
-
// Filesystem
|
|
92
|
+
// Filesystem control
|
|
77
93
|
"mount",
|
|
78
94
|
"umount",
|
|
79
|
-
"chown",
|
|
80
|
-
"chmod",
|
|
81
95
|
"chattr",
|
|
82
96
|
"lsattr",
|
|
83
|
-
//
|
|
97
|
+
// Permission modification
|
|
98
|
+
"chown",
|
|
99
|
+
"chmod"
|
|
100
|
+
]);
|
|
101
|
+
var EXTENDED_COMMANDS = /* @__PURE__ */ new Set([
|
|
102
|
+
// File deletion
|
|
103
|
+
"rm",
|
|
104
|
+
"rmdir",
|
|
105
|
+
"del",
|
|
106
|
+
// Privilege escalation
|
|
107
|
+
"sudo",
|
|
108
|
+
// Download tools
|
|
109
|
+
"wget",
|
|
110
|
+
"curl",
|
|
111
|
+
// Package management
|
|
112
|
+
"apt",
|
|
113
|
+
"apt-get",
|
|
114
|
+
"yum",
|
|
115
|
+
"dnf",
|
|
116
|
+
"pacman",
|
|
117
|
+
"pip",
|
|
118
|
+
"npm",
|
|
119
|
+
"yarn",
|
|
120
|
+
"cargo",
|
|
121
|
+
// System service control
|
|
122
|
+
"systemctl",
|
|
123
|
+
"service",
|
|
124
|
+
// Interactive editors (shell escape risk)
|
|
84
125
|
"vi",
|
|
85
126
|
"vim",
|
|
86
127
|
"nano",
|
|
87
128
|
"emacs",
|
|
88
129
|
"less",
|
|
89
130
|
"more",
|
|
90
|
-
"man"
|
|
131
|
+
"man",
|
|
132
|
+
// Version control
|
|
133
|
+
"git"
|
|
91
134
|
]);
|
|
92
|
-
var
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"
|
|
99
|
-
"::ffff:0.0.0.0",
|
|
135
|
+
var BLOCKED_COMMANDS = /* @__PURE__ */ new Set([
|
|
136
|
+
...CRITICAL_COMMANDS,
|
|
137
|
+
...EXTENDED_COMMANDS
|
|
138
|
+
]);
|
|
139
|
+
var BLOCKED_URL_ALWAYS = /* @__PURE__ */ new Set([
|
|
140
|
+
// Cloud metadata endpoints
|
|
141
|
+
"169.254.169.254",
|
|
100
142
|
// RFC1918 private ranges
|
|
101
143
|
"10.",
|
|
102
144
|
"192.168.",
|
|
@@ -116,14 +158,26 @@ var BLOCKED_URL_PATTERNS = /* @__PURE__ */ new Set([
|
|
|
116
158
|
"172.29.",
|
|
117
159
|
"172.30.",
|
|
118
160
|
"172.31.",
|
|
119
|
-
// Cloud metadata endpoints
|
|
120
|
-
"169.254.169.254",
|
|
121
161
|
// Internal service patterns
|
|
122
162
|
"internal.",
|
|
123
|
-
"local.",
|
|
124
163
|
"private.",
|
|
125
164
|
"intranet."
|
|
126
165
|
]);
|
|
166
|
+
var BLOCKED_URL_MAX_ONLY = /* @__PURE__ */ new Set([
|
|
167
|
+
// Loopback addresses (full 127.0.0.0/8 range)
|
|
168
|
+
"localhost",
|
|
169
|
+
"127.",
|
|
170
|
+
"0.0.0.0",
|
|
171
|
+
"::1",
|
|
172
|
+
"::ffff:127.0.0.1",
|
|
173
|
+
"::ffff:0.0.0.0",
|
|
174
|
+
// Local/internal patterns
|
|
175
|
+
"local."
|
|
176
|
+
]);
|
|
177
|
+
var BLOCKED_URL_PATTERNS = /* @__PURE__ */ new Set([
|
|
178
|
+
...BLOCKED_URL_ALWAYS,
|
|
179
|
+
...BLOCKED_URL_MAX_ONLY
|
|
180
|
+
]);
|
|
127
181
|
var CRITICAL_SYSTEM_DIRS = [
|
|
128
182
|
"/etc",
|
|
129
183
|
"/root",
|
|
@@ -166,7 +220,8 @@ function validatePath(filePath, allowedDirs) {
|
|
|
166
220
|
"/.gnupg/",
|
|
167
221
|
path.join(os.homedir(), ".ssh"),
|
|
168
222
|
path.join(os.homedir(), ".gnupg"),
|
|
169
|
-
SETTINGS_PATH
|
|
223
|
+
SETTINGS_PATH,
|
|
224
|
+
SECURITY_CONFIG_PATH
|
|
170
225
|
// NOTE: models.json is intentionally excluded from sensitivePaths.
|
|
171
226
|
// Extensions use readModelsJson()/writeModelsJson() from shared/ollama.ts
|
|
172
227
|
// for direct file I/O — not via Pi's tool system — so blocking it here
|
|
@@ -218,11 +273,19 @@ function isSafeUrl(url, blockSsrf = true) {
|
|
|
218
273
|
return { safe: false, error: "URL hostname uses non-decimal IP format" };
|
|
219
274
|
}
|
|
220
275
|
if (blockSsrf) {
|
|
221
|
-
|
|
276
|
+
const mode = getSecurityMode();
|
|
277
|
+
for (const pattern of BLOCKED_URL_ALWAYS) {
|
|
222
278
|
if (normalized === pattern || normalized.endsWith("." + pattern) || normalized.startsWith(pattern)) {
|
|
223
279
|
return { safe: false, error: `SSRF protection: blocked hostname pattern '${pattern}'` };
|
|
224
280
|
}
|
|
225
281
|
}
|
|
282
|
+
if (mode === "max") {
|
|
283
|
+
for (const pattern of BLOCKED_URL_MAX_ONLY) {
|
|
284
|
+
if (normalized === pattern || normalized.endsWith("." + pattern) || normalized.startsWith(pattern)) {
|
|
285
|
+
return { safe: false, error: `SSRF protection: blocked hostname pattern '${pattern}' (max mode)` };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
226
289
|
}
|
|
227
290
|
return { safe: true, error: "" };
|
|
228
291
|
}
|
|
@@ -249,8 +312,12 @@ function sanitizeCommand(command) {
|
|
|
249
312
|
let baseCmd = parts[0].toLowerCase();
|
|
250
313
|
if (baseCmd.includes("/")) baseCmd = baseCmd.split("/").pop();
|
|
251
314
|
if (baseCmd.includes("\\")) baseCmd = baseCmd.split("\\").pop();
|
|
252
|
-
if (
|
|
253
|
-
return { isSafe: false, error: `Blocked command: ${baseCmd}`, command: "" };
|
|
315
|
+
if (CRITICAL_COMMANDS.has(baseCmd)) {
|
|
316
|
+
return { isSafe: false, error: `Blocked command: ${baseCmd} (critical)`, command: "" };
|
|
317
|
+
}
|
|
318
|
+
const mode = getSecurityMode();
|
|
319
|
+
if (mode === "max" && EXTENDED_COMMANDS.has(baseCmd)) {
|
|
320
|
+
return { isSafe: false, error: `Blocked command: ${baseCmd} (max mode)`, command: "" };
|
|
254
321
|
}
|
|
255
322
|
const stripped = command.replace(/\n/g, " ").replace(/\r/g, " ");
|
|
256
323
|
if (stripped !== command) {
|
|
@@ -270,7 +337,8 @@ function appendAuditEntry(entry) {
|
|
|
270
337
|
if (!fs.existsSync(AUDIT_DIR)) {
|
|
271
338
|
fs.mkdirSync(AUDIT_DIR, { recursive: true });
|
|
272
339
|
}
|
|
273
|
-
const
|
|
340
|
+
const enriched = { ...entry, securityMode: getSecurityMode() };
|
|
341
|
+
const line = JSON.stringify(enriched) + "\n";
|
|
274
342
|
fs.appendFileSync(AUDIT_LOG_PATH, line, "utf-8");
|
|
275
343
|
} catch (err) {
|
|
276
344
|
debugLog("security", "audit log write failure", err);
|
|
@@ -352,15 +420,22 @@ function checkInjectionPatterns(input) {
|
|
|
352
420
|
export {
|
|
353
421
|
AUDIT_LOG_PATH,
|
|
354
422
|
BLOCKED_COMMANDS,
|
|
423
|
+
BLOCKED_URL_ALWAYS,
|
|
424
|
+
BLOCKED_URL_MAX_ONLY,
|
|
355
425
|
BLOCKED_URL_PATTERNS,
|
|
426
|
+
CRITICAL_COMMANDS,
|
|
427
|
+
EXTENDED_COMMANDS,
|
|
428
|
+
SECURITY_CONFIG_PATH,
|
|
356
429
|
SETTINGS_PATH,
|
|
357
430
|
appendAuditEntry,
|
|
358
431
|
checkBashToolInput,
|
|
359
432
|
checkFileToolInput,
|
|
360
433
|
checkHttpToolInput,
|
|
361
434
|
checkInjectionPatterns,
|
|
435
|
+
getSecurityMode,
|
|
362
436
|
isSafeUrl,
|
|
363
437
|
readRecentAuditEntries,
|
|
364
438
|
sanitizeCommand,
|
|
439
|
+
setSecurityMode,
|
|
365
440
|
validatePath
|
|
366
441
|
};
|