@garyr/pt-cli 0.40.1 → 0.41.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/.test-home-remote/.pt/config.yaml +10 -0
- package/README.md +85 -65
- package/dist/commands/initCommand.js +4 -4
- package/dist/commands/learnCommand.js +120 -432
- package/dist/commands/template-detection.js +256 -0
- package/dist/commands/template-prompts.js +332 -0
- package/dist/commands/template-utils.js +565 -0
- package/dist/commands/updateCommand.js +78 -532
- package/dist/config.js +2 -5
- package/dist/safety.js +203 -42
- package/doc/security.md +96 -36
- package/package.json +1 -1
- package/skills/agency-pt-operator/SKILL.md +54 -9
- package/src/commands/initCommand.ts +9 -9
- package/src/commands/learnCommand.ts +157 -423
- package/src/commands/template-detection.ts +269 -0
- package/src/commands/template-prompts.ts +406 -0
- package/src/commands/template-utils.ts +651 -0
- package/src/commands/updateCommand.ts +136 -577
- package/src/config.ts +2 -5
- package/src/safety.ts +239 -53
- package/tests/remote.test.ts +110 -0
- package/tests/safety.test.ts +296 -0
- package/tests/update.test.ts +417 -0
package/dist/config.js
CHANGED
|
@@ -88,11 +88,8 @@ export function loadConfig() {
|
|
|
88
88
|
if (fs.existsSync(backupPath)) {
|
|
89
89
|
console.error(chalk.yellow(`A backup exists at ${backupPath}. You may want to restore it.`));
|
|
90
90
|
}
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
process.exit(1);
|
|
94
|
-
}, 5);
|
|
95
|
-
// 👇 Add this return statement to satisfy the TypeScript compiler
|
|
91
|
+
// Exit synchronously - the test expects process.exit to be called immediately
|
|
92
|
+
process.exit(1);
|
|
96
93
|
// The application will terminate before this empty config can be used.
|
|
97
94
|
return {
|
|
98
95
|
version: '3.0',
|
package/dist/safety.js
CHANGED
|
@@ -12,8 +12,6 @@ const BLOCKED_COMMANDS = [
|
|
|
12
12
|
'sudo', 'su', 'su -', 'su root',
|
|
13
13
|
// Disk operations that could destroy data
|
|
14
14
|
'dd', 'mkfs', 'fdisk', 'mount', 'umount',
|
|
15
|
-
// Shell injection patterns - removed from blocklist to allow command chaining.
|
|
16
|
-
// These are validated as warnings instead.
|
|
17
15
|
// Dangerous chmod
|
|
18
16
|
'chmod 777', 'chmod -R 777', 'chmod 666',
|
|
19
17
|
// System commands that could kill processes
|
|
@@ -22,6 +20,8 @@ const BLOCKED_COMMANDS = [
|
|
|
22
20
|
'nc', 'netcat', 'socat',
|
|
23
21
|
// Package manager with dangerous flags
|
|
24
22
|
'apt purge', 'apt remove', 'yum remove', 'brew uninstall',
|
|
23
|
+
// Aliases for the above
|
|
24
|
+
'apt-get purge', 'apt-get remove',
|
|
25
25
|
];
|
|
26
26
|
// === DANGEROUS PATTERNS ===
|
|
27
27
|
// Commands that should trigger a warning but are NOT blocked
|
|
@@ -31,70 +31,219 @@ const DANGEROUS_PATTERNS = [
|
|
|
31
31
|
// Script execution
|
|
32
32
|
'bash', 'sh', 'python', 'python3', 'node -e', 'node -p',
|
|
33
33
|
// Shell operations
|
|
34
|
-
'eval', 'exec', 'source',
|
|
34
|
+
'eval', 'exec', 'source', '.',
|
|
35
35
|
// File system manipulation
|
|
36
36
|
'chmod -R', 'chown -R', 'chgrp -R',
|
|
37
37
|
// PowerShell (Windows)
|
|
38
38
|
'powershell', 'pwsh', 'Invoke-Expression', 'IEX',
|
|
39
39
|
// macOS-specific
|
|
40
40
|
'diskutil', 'hdiutil', 'csrutil',
|
|
41
|
+
// Aliases
|
|
42
|
+
'python2', 'python3.10', 'python3.11', 'python3.12',
|
|
43
|
+
'node', 'npm run', 'npx',
|
|
41
44
|
];
|
|
42
|
-
//
|
|
43
|
-
const
|
|
44
|
-
maxExecutionTime: 30000, // 30 seconds
|
|
45
|
-
enableAuditLogging: true,
|
|
46
|
-
trustedSources: [
|
|
47
|
-
'github.com/garyritchie',
|
|
48
|
-
'git.lyonritchie.com',
|
|
49
|
-
'github.com/lyonritchie',
|
|
50
|
-
],
|
|
51
|
-
maxCommandsPerRun: 50,
|
|
52
|
-
securityLevel: 'warn', // Warning-focused mode
|
|
53
|
-
};
|
|
45
|
+
// Shell metacharacters that indicate command chaining/injection attempts
|
|
46
|
+
const SHELL_METACHARACTERS = [';', '|', '&', '||', '&&', '|&', '<', '>', '>>', '$(', '`'];
|
|
54
47
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
48
|
+
* Parse a shell command into its base command and arguments, handling basic shell syntax.
|
|
49
|
+
* This is a simplified parser that splits by shell metacharacters and parses the first command.
|
|
57
50
|
*/
|
|
58
|
-
|
|
51
|
+
function parseShellCommand(command) {
|
|
52
|
+
// Check for shell metacharacters that could chain commands
|
|
53
|
+
let hasMetacharacters = false;
|
|
54
|
+
for (const char of SHELL_METACHARACTERS) {
|
|
55
|
+
if (command.includes(char)) {
|
|
56
|
+
hasMetacharacters = true;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Simple approach: split by whitespace, but respect quoted strings
|
|
61
|
+
const parts = [];
|
|
62
|
+
let current = '';
|
|
63
|
+
let inSingleQuote = false;
|
|
64
|
+
let inDoubleQuote = false;
|
|
65
|
+
let escapeNext = false;
|
|
66
|
+
for (let i = 0; i < command.length; i++) {
|
|
67
|
+
const char = command[i];
|
|
68
|
+
if (escapeNext) {
|
|
69
|
+
current += char;
|
|
70
|
+
escapeNext = false;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (char === '\\' && !inSingleQuote) {
|
|
74
|
+
escapeNext = true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (char === "'" && !inDoubleQuote) {
|
|
78
|
+
inSingleQuote = !inSingleQuote;
|
|
79
|
+
current += char;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (char === '"' && !inSingleQuote) {
|
|
83
|
+
inDoubleQuote = !inDoubleQuote;
|
|
84
|
+
current += char;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if ((char === ' ' || char === '\t') && !inSingleQuote && !inDoubleQuote) {
|
|
88
|
+
if (current) {
|
|
89
|
+
parts.push(current);
|
|
90
|
+
current = '';
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
current += char;
|
|
95
|
+
}
|
|
96
|
+
if (current) {
|
|
97
|
+
parts.push(current);
|
|
98
|
+
}
|
|
99
|
+
if (parts.length === 0) {
|
|
100
|
+
return { baseCommand: '', args: [], hasMetacharacters };
|
|
101
|
+
}
|
|
102
|
+
// Find the first actual command (skip environment variable assignments like VAR=value)
|
|
103
|
+
let commandIndex = 0;
|
|
104
|
+
while (commandIndex < parts.length && parts[commandIndex].includes('=') && !parts[commandIndex].startsWith('-')) {
|
|
105
|
+
commandIndex++;
|
|
106
|
+
}
|
|
107
|
+
if (commandIndex >= parts.length) {
|
|
108
|
+
return { baseCommand: '', args: [], hasMetacharacters };
|
|
109
|
+
}
|
|
110
|
+
let baseCommand = parts[commandIndex];
|
|
111
|
+
// Strip surrounding quotes if present
|
|
112
|
+
if ((baseCommand.startsWith('"') && baseCommand.endsWith('"')) ||
|
|
113
|
+
(baseCommand.startsWith("'") && baseCommand.endsWith("'"))) {
|
|
114
|
+
baseCommand = baseCommand.slice(1, -1);
|
|
115
|
+
}
|
|
116
|
+
const args = parts.slice(commandIndex + 1);
|
|
117
|
+
return { baseCommand, args, hasMetacharacters };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Check if a base command matches a blocked command (exact or prefix match)
|
|
121
|
+
*/
|
|
122
|
+
function isCommandBlocked(baseCommand, args = []) {
|
|
123
|
+
// Normalize the command (resolve path if needed)
|
|
124
|
+
const cmd = baseCommand.toLowerCase();
|
|
59
125
|
for (const blocked of BLOCKED_COMMANDS) {
|
|
60
|
-
|
|
126
|
+
const blockedLower = blocked.toLowerCase();
|
|
127
|
+
// Exact match or prefix match (e.g., "sudo" blocks "sudo rm -rf")
|
|
128
|
+
if (cmd === blockedLower || cmd.startsWith(blockedLower + ' ') || cmd.startsWith(blockedLower + '/')) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
// Handle multi-word blocked commands like "apt purge", "apt remove"
|
|
132
|
+
// Check if the command matches the first word and the first arg matches the rest
|
|
133
|
+
const blockedParts = blockedLower.split(' ');
|
|
134
|
+
if (blockedParts.length > 1) {
|
|
135
|
+
if (cmd === blockedParts[0] && args.length > 0 && args[0].toLowerCase() === blockedParts[1]) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Handle commands like "mkfs.ext4" where "mkfs" is blocked
|
|
140
|
+
// Check if the blocked command is a prefix of the base command (e.g., "mkfs" matches "mkfs.ext4")
|
|
141
|
+
if (blockedLower.length > 2 && cmd.startsWith(blockedLower)) {
|
|
142
|
+
// Make sure it's a real prefix (e.g., "mkfs" not "mkf")
|
|
143
|
+
const nextChar = cmd.charAt(blockedLower.length);
|
|
144
|
+
if (!nextChar || nextChar === '.' || nextChar === '/' || nextChar === ' ' || nextChar === '-') {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Check if a base command matches a dangerous pattern
|
|
153
|
+
*/
|
|
154
|
+
function isCommandDangerous(baseCommand, fullCommand) {
|
|
155
|
+
const cmd = baseCommand.toLowerCase();
|
|
156
|
+
const fullCmd = fullCommand.toLowerCase();
|
|
157
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
158
|
+
const patternLower = pattern.toLowerCase();
|
|
159
|
+
// Check base command exact/prefix match
|
|
160
|
+
if (cmd === patternLower || cmd.startsWith(patternLower + ' ') || cmd.startsWith(patternLower + '/')) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
// Also check full command for multi-word patterns like "chmod -R"
|
|
164
|
+
if (patternLower.includes(' ') && fullCmd.includes(patternLower)) {
|
|
61
165
|
return true;
|
|
62
166
|
}
|
|
63
167
|
}
|
|
64
168
|
return false;
|
|
65
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Check if a command is in the blocklist (NEVER allowed)
|
|
172
|
+
* Uses proper shell parsing to prevent bypasses like "sud o" or "curl|sh"
|
|
173
|
+
*/
|
|
174
|
+
export function isBlockedCommand(command) {
|
|
175
|
+
const parsed = parseShellCommand(command);
|
|
176
|
+
// Check each command in a chained command sequence
|
|
177
|
+
if (parsed.hasMetacharacters) {
|
|
178
|
+
// Split by metacharacters and check each command
|
|
179
|
+
const parts = command.split(/[;&|]|&&|\|\|/);
|
|
180
|
+
for (const part of parts) {
|
|
181
|
+
const trimmed = part.trim();
|
|
182
|
+
if (!trimmed)
|
|
183
|
+
continue;
|
|
184
|
+
const subParsed = parseShellCommand(trimmed);
|
|
185
|
+
if (isCommandBlocked(subParsed.baseCommand, subParsed.args)) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
return isCommandBlocked(parsed.baseCommand, parsed.args);
|
|
192
|
+
}
|
|
66
193
|
/**
|
|
67
194
|
* Check if a command should trigger a warning (but is allowed)
|
|
195
|
+
* Uses proper shell parsing to prevent bypasses
|
|
68
196
|
*/
|
|
69
197
|
export function isDangerousCommand(command) {
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (rmIndex !== -1) {
|
|
78
|
-
// Check subsequent arguments
|
|
79
|
-
for (let i = rmIndex + 1; i < parts.length; i++) {
|
|
80
|
-
const arg = parts[i];
|
|
81
|
-
if (!arg)
|
|
198
|
+
const parsed = parseShellCommand(command);
|
|
199
|
+
// Check each command in a chained sequence
|
|
200
|
+
if (parsed.hasMetacharacters) {
|
|
201
|
+
const parts = command.split(/[;&|]|&&|\|\|/);
|
|
202
|
+
for (const part of parts) {
|
|
203
|
+
const trimmed = part.trim();
|
|
204
|
+
if (!trimmed)
|
|
82
205
|
continue;
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
// Check absolute path patterns
|
|
88
|
-
const isAbsolute = arg.startsWith('/') ||
|
|
89
|
-
(process.platform === 'win32' && (/^[a-zA-Z]:\\/.test(arg) || arg.startsWith('\\\\') || arg.startsWith('\\')));
|
|
90
|
-
if (isAbsolute) {
|
|
206
|
+
const subParsed = parseShellCommand(trimmed);
|
|
207
|
+
if (isCommandDangerous(subParsed.baseCommand, trimmed)) {
|
|
91
208
|
return true;
|
|
92
209
|
}
|
|
93
210
|
}
|
|
94
211
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
212
|
+
if (isCommandDangerous(parsed.baseCommand, command)) {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
// Check for destructive file operations targeting absolute paths
|
|
216
|
+
return checkDestructiveAbsolutePath(command);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Check for 'rm', 'rmdir', 'del' followed by absolute paths
|
|
220
|
+
*/
|
|
221
|
+
function checkDestructiveAbsolutePath(command) {
|
|
222
|
+
// Split by shell metacharacters to check each command separately
|
|
223
|
+
const parts = command.split(/[;&|]|&&|\|\|/);
|
|
224
|
+
for (const part of parts) {
|
|
225
|
+
const trimmed = part.trim();
|
|
226
|
+
if (!trimmed)
|
|
227
|
+
continue;
|
|
228
|
+
const parsed = parseShellCommand(trimmed);
|
|
229
|
+
const baseCmd = parsed.baseCommand.toLowerCase();
|
|
230
|
+
if (baseCmd === 'rm' || baseCmd === 'rmdir' || baseCmd === 'del') {
|
|
231
|
+
// Check arguments for absolute paths
|
|
232
|
+
for (const arg of parsed.args) {
|
|
233
|
+
if (!arg)
|
|
234
|
+
continue;
|
|
235
|
+
// Skip flags
|
|
236
|
+
if (arg.startsWith('-') || (process.platform === 'win32' && arg.startsWith('/'))) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
// Check absolute path patterns
|
|
240
|
+
const isAbsolute = arg.startsWith('/') ||
|
|
241
|
+
(process.platform === 'win32' &&
|
|
242
|
+
(/^[a-zA-Z]:\\/.test(arg) || arg.startsWith('\\\\') || arg.startsWith('\\')));
|
|
243
|
+
if (isAbsolute) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
98
247
|
}
|
|
99
248
|
}
|
|
100
249
|
return false;
|
|
@@ -184,6 +333,18 @@ export function canExecute(command, maxCommandsPerRun = 50) {
|
|
|
184
333
|
export function resetExecutionCounts() {
|
|
185
334
|
lastExecutionTimes.clear();
|
|
186
335
|
}
|
|
336
|
+
// Default security policy - warning focused
|
|
337
|
+
const DEFAULT_SECURITY_POLICY = {
|
|
338
|
+
maxExecutionTime: 30000, // 30 seconds
|
|
339
|
+
enableAuditLogging: true,
|
|
340
|
+
trustedSources: [
|
|
341
|
+
'github.com/garyritchie',
|
|
342
|
+
'git.lyonritchie.com',
|
|
343
|
+
'github.com/lyonritchie',
|
|
344
|
+
],
|
|
345
|
+
maxCommandsPerRun: 50,
|
|
346
|
+
securityLevel: 'warn', // Warning-focused mode
|
|
347
|
+
};
|
|
187
348
|
/**
|
|
188
349
|
* Validate a template's security before execution
|
|
189
350
|
*/
|
package/doc/security.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
`pt-cli` implements a multi-layered security model to protect users when running post-config commands and downloading remote templates. The system uses a warning-based approach rather than
|
|
5
|
+
`pt-cli` implements a multi-layered security model to protect users when running post-config commands and downloading remote templates. The system uses a **warning-based approach** with intelligent shell parsing rather than simple string matching, allowing legitimate workflows while providing clear warnings for potentially dangerous operations.
|
|
6
6
|
|
|
7
7
|
## Security Policy Configuration
|
|
8
8
|
|
|
@@ -22,8 +22,8 @@ security:
|
|
|
22
22
|
|
|
23
23
|
### Security Levels
|
|
24
24
|
|
|
25
|
-
- **`warn`** (default): Warning-based approach with cancellation prompts
|
|
26
|
-
- **`strict`**: More conservative defaults
|
|
25
|
+
- **`warn`** (default): Warning-based approach with cancellation prompts for dangerous commands
|
|
26
|
+
- **`strict`**: More conservative defaults; enables stricter default policies
|
|
27
27
|
|
|
28
28
|
### Trusted Sources
|
|
29
29
|
|
|
@@ -33,25 +33,57 @@ When downloading templates from remote URLs, `pt-cli` verifies the source agains
|
|
|
33
33
|
|
|
34
34
|
### Absolute Blocks (Never Allowed)
|
|
35
35
|
|
|
36
|
-
The following commands are **always blocked** regardless of security level
|
|
36
|
+
The following commands are **always blocked** regardless of security level. The blocklist uses **proper shell parsing** — commands are split by shell metacharacters (`;`, `&`, `|`, `&&`, `||`, etc.), quoted strings are respected, and each sub-command is checked individually. This prevents bypasses like `"sudo" rm -rf /`, `sudo; rm -rf /`, or `mkfs.ext4`.
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
- `
|
|
40
|
-
- `rm -rf /`, `rm -r --no-preserve-root` (massive deletion)
|
|
41
|
-
- `eval`, `exec`, `source` (code execution)
|
|
38
|
+
**Privilege Escalation:**
|
|
39
|
+
- `sudo`, `su`, `su -`, `su root`
|
|
42
40
|
|
|
43
|
-
|
|
41
|
+
**Disk Operations:**
|
|
42
|
+
- `dd`, `mkfs` (and variants like `mkfs.ext4`, `mkfs.xfs`), `fdisk`, `mount`, `umount`
|
|
44
43
|
|
|
45
|
-
|
|
44
|
+
**Dangerous Permissions:**
|
|
45
|
+
- `chmod 777`, `chmod -R 777`, `chmod 666`
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
- `
|
|
49
|
-
- `chmod 777`, `chmod -R`, `chmod +x`, `chmod 755`, `chmod 644` (permission changes)
|
|
47
|
+
**Process Killing:**
|
|
48
|
+
- `kill`, `killall`, `pkill`, `fuser`
|
|
50
49
|
|
|
51
|
-
**
|
|
50
|
+
**Network Exfiltration:**
|
|
51
|
+
- `nc`, `netcat`, `socat`
|
|
52
52
|
|
|
53
|
+
**Package Manager (Destructive Operations):**
|
|
54
|
+
- `apt purge`, `apt remove`, `apt-get purge`, `apt-get remove`, `yum remove`, `brew uninstall`
|
|
55
|
+
|
|
56
|
+
### Dangerous Commands (Warning + 5-Second Countdown)
|
|
57
|
+
|
|
58
|
+
The following commands trigger a **5-second countdown** with CTRL+C cancellation. These are allowed but require explicit user confirmation.
|
|
59
|
+
|
|
60
|
+
**Remote Downloads + Execution:**
|
|
61
|
+
- `curl`, `wget`, `wget -O`, `curl |`, `wget |`
|
|
62
|
+
|
|
63
|
+
**Script Execution:**
|
|
64
|
+
- `bash`, `sh`, `python`, `python3`, `python2`, `python3.10`, `python3.11`, `python3.12`
|
|
65
|
+
- `node`, `node -e`, `node -p`, `npm run`, `npx`
|
|
66
|
+
|
|
67
|
+
**Shell Operations:**
|
|
68
|
+
- `eval`, `exec`, `source`, `.`
|
|
69
|
+
|
|
70
|
+
**Recursive File Operations:**
|
|
71
|
+
- `chmod -R`, `chown -R`, `chgrp -R`
|
|
72
|
+
|
|
73
|
+
**PowerShell (Windows):**
|
|
74
|
+
- `powershell`, `pwsh`, `Invoke-Expression`, `IEX`
|
|
75
|
+
|
|
76
|
+
**macOS-Specific:**
|
|
77
|
+
- `diskutil`, `hdiutil`, `csrutil`
|
|
78
|
+
|
|
79
|
+
**Destructive File Operations on Absolute Paths:**
|
|
80
|
+
- `rm`, `rmdir`, `del` followed by absolute paths (e.g., `rm -rf /tmp`, `rm /etc/passwd`)
|
|
81
|
+
|
|
82
|
+
**Example Interaction:**
|
|
53
83
|
```bash
|
|
54
|
-
⚠️
|
|
84
|
+
⚠️ DANGEROUS COMMAND DETECTED
|
|
85
|
+
Command: curl https://example.com/install.sh | bash
|
|
86
|
+
This could potentially harm your system.
|
|
55
87
|
Press CTRL+C to cancel, or wait 5s to continue...
|
|
56
88
|
```
|
|
57
89
|
|
|
@@ -59,6 +91,7 @@ The following commands trigger a **5-second countdown** with CTRL+C cancellation
|
|
|
59
91
|
|
|
60
92
|
- **50 commands per run**: Prevents runaway command execution
|
|
61
93
|
- If limit is reached, subsequent commands are skipped with a warning
|
|
94
|
+
- Counter resets each `pt init` session (in-memory only)
|
|
62
95
|
|
|
63
96
|
### Execution Timeout
|
|
64
97
|
|
|
@@ -69,36 +102,41 @@ The following commands trigger a **5-second countdown** with CTRL+C cancellation
|
|
|
69
102
|
|
|
70
103
|
When downloading templates from remote URLs:
|
|
71
104
|
|
|
72
|
-
1. **Source Verification**: Checks against `trustedSources` list
|
|
105
|
+
1. **Source Verification**: Checks against `trustedSources` list (configurable in `config.yaml`)
|
|
73
106
|
2. **File Size Validation**: Maximum 50MB download limit
|
|
74
107
|
3. **Archive Extraction**: Extracts to secure temporary directory
|
|
75
108
|
4. **Audit Logging**: All downloads are logged with timestamps and outcomes
|
|
76
109
|
|
|
77
110
|
## Audit Logging
|
|
78
111
|
|
|
79
|
-
All security events are logged to `~/.pt/security-audit.log
|
|
112
|
+
All security events are logged to `~/.pt/security-audit.log` in JSON format:
|
|
80
113
|
|
|
114
|
+
```json
|
|
115
|
+
{"timestamp":"2026-06-27T10:01:23.456Z","eventType":"command_executed","command":"npm install","template":"javascript","user":"gary","result":"success","hostname":"host"}
|
|
116
|
+
{"timestamp":"2026-06-27T10:01:24.123Z","eventType":"command_blocked","command":"sudo rm -rf /","template":"all","user":"gary","result":"blocked","hostname":"host"}
|
|
117
|
+
{"timestamp":"2026-06-27T10:01:25.789Z","eventType":"template_loaded","command":"https://github.com/user/template","template":"remote","user":"gary","result":"success","hostname":"host"}
|
|
81
118
|
```
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
119
|
+
|
|
120
|
+
Event types: `command_executed`, `command_blocked`, `command_timed_out`, `template_loaded`
|
|
121
|
+
|
|
122
|
+
Results: `success`, `failed`, `timedout`, `blocked`
|
|
86
123
|
|
|
87
124
|
## Security Best Practices
|
|
88
125
|
|
|
89
126
|
### For Users
|
|
90
127
|
|
|
91
|
-
1. **Review post-config tasks**: Always review commands before executing
|
|
92
|
-
2. **Use trusted sources**: Only download templates from known repositories
|
|
128
|
+
1. **Review post-config tasks**: Always review commands before executing (use `--dry-run` to preview)
|
|
129
|
+
2. **Use trusted sources**: Only download templates from known repositories; add your own to `trustedSources`
|
|
93
130
|
3. **Monitor audit logs**: Check `~/.pt/security-audit.log` for suspicious activity
|
|
94
131
|
4. **Update regularly**: Keep `pt-cli` updated for latest security improvements
|
|
95
132
|
|
|
96
133
|
### For Template Authors
|
|
97
134
|
|
|
98
|
-
1. **Avoid dangerous commands**: Don't include `sudo`, `rm -rf
|
|
135
|
+
1. **Avoid dangerous commands**: Don't include `sudo`, `rm -rf /`, or privilege escalation in templates
|
|
99
136
|
2. **Use safe defaults**: Prefer `npm install` over custom scripts
|
|
100
137
|
3. **Provide clear descriptions**: Explain what each post-config task does
|
|
101
138
|
4. **Test thoroughly**: Verify templates work in isolated environments
|
|
139
|
+
5. **Use relative paths**: Avoid absolute paths in `rm`/`rmdir`/`del` commands
|
|
102
140
|
|
|
103
141
|
## Troubleshooting
|
|
104
142
|
|
|
@@ -110,23 +148,45 @@ All security events are logged to `~/.pt/security-audit.log`:
|
|
|
110
148
|
|
|
111
149
|
### Commands Blocked Unexpectedly
|
|
112
150
|
|
|
113
|
-
1. Check if command matches absolute blocklist
|
|
114
|
-
2. Review
|
|
115
|
-
3.
|
|
151
|
+
1. Check if command matches absolute blocklist (see above)
|
|
152
|
+
2. Review if command uses absolute paths with `rm`/`rmdir`/`del`
|
|
153
|
+
3. Check for shell metacharacter splitting (commands separated by `;`, `&&`, `||`, `|`)
|
|
154
|
+
4. Consult audit log for specific reasons
|
|
116
155
|
|
|
117
156
|
### Remote Template Download Failed
|
|
118
157
|
|
|
119
|
-
1. Verify URL is in `trustedSources` list
|
|
158
|
+
1. Verify URL is in `trustedSources` list (or use `--allow-untrusted`)
|
|
120
159
|
2. Check network connectivity
|
|
121
160
|
3. Verify file size is under 50MB limit
|
|
122
|
-
4. Check for valid archive format
|
|
161
|
+
4. Check for valid archive format (tar.gz)
|
|
162
|
+
|
|
163
|
+
### Bypass Attempts Detected
|
|
164
|
+
|
|
165
|
+
The parser specifically handles these common bypass attempts:
|
|
166
|
+
- Quoted commands: `"sudo" rm -rf /` → detected
|
|
167
|
+
- Spaced commands: `sud o rm -rf /` → detected (not in blocklist)
|
|
168
|
+
- Chained commands: `echo hello; sudo rm -rf /` → detected via metacharacter split
|
|
169
|
+
- Pipeline injection: `curl | bash` → detected as dangerous pattern
|
|
123
170
|
|
|
124
171
|
## Security Policy Reference
|
|
125
172
|
|
|
126
|
-
| Setting
|
|
127
|
-
|
|
128
|
-
| `securityLevel`
|
|
129
|
-
| `trustedSources`
|
|
130
|
-
| `maxExecutionTime`
|
|
131
|
-
| `maxCommandsPerRun` | number
|
|
132
|
-
| `enableAuditLogging
|
|
173
|
+
| Setting | Type | Default | Description |
|
|
174
|
+
|---------|------|---------|-------------|
|
|
175
|
+
| `securityLevel` | string | `"warn"` | Security enforcement level |
|
|
176
|
+
| `trustedSources` | array | See above | List of trusted template sources |
|
|
177
|
+
| `maxExecutionTime` | number | 30000 | Max milliseconds per command (30s) |
|
|
178
|
+
| `maxCommandsPerRun` | number | 50 | Rate limit per init session |
|
|
179
|
+
| `enableAuditLogging` | boolean | true | Enable security event logging |
|
|
180
|
+
|
|
181
|
+
## Implementation Details
|
|
182
|
+
|
|
183
|
+
The security model is implemented in `src/safety.ts` with these key functions:
|
|
184
|
+
|
|
185
|
+
- `parseShellCommand()` — Splits commands by shell metacharacters, respects quotes/escapes, extracts base command and args
|
|
186
|
+
- `isCommandBlocked(baseCommand, args)` — Checks blocklist with multi-word matching (e.g., `apt remove`) and prefix matching (e.g., `mkfs.ext4` matches `mkfs`)
|
|
187
|
+
- `isDangerousCommand(command)` — Checks dangerous patterns with full-command substring matching for multi-word patterns
|
|
188
|
+
- `checkDestructiveAbsolutePath()` — Detects `rm`/`rmdir`/`del` targeting absolute paths
|
|
189
|
+
- `validateTemplateSecurity()` — Validates all `post_config` tasks in a template
|
|
190
|
+
- `isTrustedSource()` — Checks URL against trusted sources list
|
|
191
|
+
- `canExecute()` — Rate limiting per command hash
|
|
192
|
+
- `logSecurityEvent()` — Writes structured JSON audit entries
|
package/package.json
CHANGED
|
@@ -160,8 +160,8 @@ security:
|
|
|
160
160
|
|
|
161
161
|
### Security Levels
|
|
162
162
|
|
|
163
|
-
- **`warn`** (default): Warning-based approach with cancellation prompts
|
|
164
|
-
- **`strict`**: More conservative defaults
|
|
163
|
+
- **`warn`** (default): Warning-based approach with cancellation prompts for dangerous commands
|
|
164
|
+
- **`strict`**: More conservative defaults
|
|
165
165
|
|
|
166
166
|
### Trusted Sources
|
|
167
167
|
|
|
@@ -169,20 +169,65 @@ When downloading templates from remote URLs, `pt-cli` verifies the source agains
|
|
|
169
169
|
|
|
170
170
|
### Command Security
|
|
171
171
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
172
|
+
#### Absolute Blocks (Never Allowed)
|
|
173
|
+
|
|
174
|
+
The following commands are **always blocked** regardless of security level. The blocklist uses **proper shell parsing** — commands are split by shell metacharacters (`;`, `&`, `|`, `&&`, `||`, etc.), quoted strings are respected, and each sub-command is checked individually. This prevents bypasses like `"sudo" rm -rf /`, `sudo; rm -rf /`, or `mkfs.ext4`.
|
|
175
|
+
|
|
176
|
+
**Privilege Escalation:** `sudo`, `su`, `su -`, `su root`
|
|
177
|
+
|
|
178
|
+
**Disk Operations:** `dd`, `mkfs` (and variants like `mkfs.ext4`, `mkfs.xfs`), `fdisk`, `mount`, `umount`
|
|
179
|
+
|
|
180
|
+
**Dangerous Permissions:** `chmod 777`, `chmod -R 777`, `chmod 666`
|
|
181
|
+
|
|
182
|
+
**Process Killing:** `kill`, `killall`, `pkill`, `fuser`
|
|
183
|
+
|
|
184
|
+
**Network Exfiltration:** `nc`, `netcat`, `socat`
|
|
185
|
+
|
|
186
|
+
**Package Manager (Destructive Operations):** `apt purge`, `apt remove`, `apt-get purge`, `apt-get remove`, `yum remove`, `brew uninstall`
|
|
187
|
+
|
|
188
|
+
#### Dangerous Commands (Warning + 5-Second Countdown)
|
|
189
|
+
|
|
190
|
+
The following commands trigger a **5-second countdown** with CTRL+C cancellation. These are allowed but require explicit user confirmation.
|
|
191
|
+
|
|
192
|
+
**Remote Downloads + Execution:** `curl`, `wget`, `wget -O`, `curl |`, `wget |`
|
|
193
|
+
|
|
194
|
+
**Script Execution:** `bash`, `sh`, `python`, `python3`, `python2`, `python3.10`, `python3.11`, `python3.12`, `node`, `node -e`, `node -p`, `npm run`, `npx`
|
|
195
|
+
|
|
196
|
+
**Shell Operations:** `eval`, `exec`, `source`, `.`
|
|
197
|
+
|
|
198
|
+
**Recursive File Operations:** `chmod -R`, `chown -R`, `chgrp -R`
|
|
199
|
+
|
|
200
|
+
**PowerShell (Windows):** `powershell`, `pwsh`, `Invoke-Expression`, `IEX`
|
|
201
|
+
|
|
202
|
+
**macOS-Specific:** `diskutil`, `hdiutil`, `csrutil`
|
|
203
|
+
|
|
204
|
+
**Destructive File Operations on Absolute Paths:** `rm`, `rmdir`, `del` followed by absolute paths (e.g., `rm -rf /tmp`, `rm /etc/passwd`)
|
|
205
|
+
|
|
206
|
+
### Rate Limiting
|
|
207
|
+
|
|
208
|
+
- **50 commands per run**: Prevents runaway command execution (in-memory only, resets each `pt init` session)
|
|
209
|
+
|
|
210
|
+
### Execution Timeout
|
|
211
|
+
|
|
212
|
+
- **30 seconds per command**: Prevents hung processes
|
|
176
213
|
|
|
177
214
|
### Audit Logging
|
|
178
215
|
|
|
179
|
-
All security events are logged to `~/.pt/security-audit.log`
|
|
216
|
+
All security events are logged to `~/.pt/security-audit.log` in JSON format with event types: `command_executed`, `command_blocked`, `command_timed_out`, `template_loaded` and results: `success`, `failed`, `timedout`, `blocked`.
|
|
217
|
+
|
|
218
|
+
### Bypass Protection
|
|
219
|
+
|
|
220
|
+
The parser specifically handles these common bypass attempts:
|
|
221
|
+
- Quoted commands: `"sudo" rm -rf /` → detected (quotes stripped)
|
|
222
|
+
- Chained commands: `echo hello; sudo rm -rf /` → detected via metacharacter split
|
|
223
|
+
- Pipeline injection: `curl | bash` → detected as dangerous pattern
|
|
224
|
+
- Multi-word commands: `apt remove`, `mkfs.ext4`, `chmod -R` → properly matched
|
|
180
225
|
|
|
181
|
-
|
|
226
|
+
### Security Testing
|
|
182
227
|
|
|
183
228
|
Security features can be tested by:
|
|
184
229
|
|
|
185
|
-
1. **Testing command blocks**: Try running templates with dangerous commands like `sudo rm -rf
|
|
230
|
+
1. **Testing command blocks**: Try running templates with dangerous commands like `sudo rm -rf /` or `dd`
|
|
186
231
|
2. **Testing remote downloads**: Use untrusted URLs to verify source verification
|
|
187
232
|
3. **Testing rate limiting**: Execute more than 50 commands in a single init session
|
|
188
233
|
4. **Testing timeouts**: Run commands that hang to verify timeout behavior
|