@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/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
- // Allow the event loop to flush console streams out to Godot before dying
92
- setTimeout(() => {
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
- // Default security policy - warning focused
43
- const DEFAULT_SECURITY_POLICY = {
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
- * Check if a command is in the blocklist (NEVER allowed)
56
- * Only truly dangerous operations that could destroy data
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
- export function isBlockedCommand(command) {
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
- if (command.includes(blocked)) {
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
- // Check for destructive file operations targeting absolute paths
71
- // Regex matches 'rm', 'rmdir', or Windows 'del' followed by optional flags and then an absolute path.
72
- // Absolute path matches:
73
- // - Unix: starting with '/' (e.g. /tmp, /usr)
74
- // - Windows: starting with drive letter (e.g. C:\) or UNC path (\\) or drive-relative '\'
75
- const parts = command.split(/\s+/);
76
- const rmIndex = parts.findIndex(p => p === 'rm' || p === 'rmdir' || p === 'del');
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
- // Skip flags (starting with - or /flag on Windows)
84
- if (arg.startsWith('-') || (process.platform === 'win32' && arg.startsWith('/'))) {
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
- for (const pattern of DANGEROUS_PATTERNS) {
96
- if (command.includes(pattern)) {
97
- return true;
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 strict blocking, allowing legitimate workflows while providing clear warnings for potentially dangerous operations.
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, enabled by default for new installations
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
- - `sudo`, `su`, `su -` (privilege escalation)
39
- - `dd`, `mkfs`, `fdisk` (disk operations)
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
- ### Dangerous Commands (Warning Only)
41
+ **Disk Operations:**
42
+ - `dd`, `mkfs` (and variants like `mkfs.ext4`, `mkfs.xfs`), `fdisk`, `mount`, `umount`
44
43
 
45
- The following commands trigger a **5-second countdown** with CTRL+C cancellation:
44
+ **Dangerous Permissions:**
45
+ - `chmod 777`, `chmod -R 777`, `chmod 666`
46
46
 
47
- - `curl`, `wget`, `wget -O` (remote downloads)
48
- - `bash`, `sh`, `python`, `python3`, `node -e`, `node -p` (script execution)
49
- - `chmod 777`, `chmod -R`, `chmod +x`, `chmod 755`, `chmod 644` (permission changes)
47
+ **Process Killing:**
48
+ - `kill`, `killall`, `pkill`, `fuser`
50
49
 
51
- **Example interaction:**
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
- ⚠️ WARNING: This command may be dangerous: npm install
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
- 2026-06-27T10:01:23.456Z [WARNING] dangerous_command: npm install | type: javascript | status: warning
83
- 2026-06-27T10:01:24.123Z [BLOCKED] command_blocked: sudo rm -rf / | type: all | status: blocked
84
- 2026-06-27T10:01:25.789Z [INFO] template_loaded: https://github.com/user/template | type: remote | status: success
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`, or privilege escalation in templates
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 security policy configuration
115
- 3. Consult audit log for specific reasons
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 | Type | Default | Description |
127
- |---------------------|---------|---------|--------------------------------------|
128
- | `securityLevel` | string | `"warn"`| Security enforcement level |
129
- | `trustedSources` | array | [] | List of trusted template sources |
130
- | `maxExecutionTime` | number | 30000 | Max seconds per command (30s default)|
131
- | `maxCommandsPerRun` | number | 50 | Rate limit per init session |
132
- | `enableAuditLogging`| boolean | true | Enable security event logging |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@garyr/pt-cli",
3
- "version": "0.40.1",
3
+ "version": "0.41.0",
4
4
  "description": "Project Template CLI - Learn structures and initialize projects",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -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, enabled by default for new installations
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
- - **Absolute blocks**: Commands like `sudo`, `rm -rf`, `dd` are always blocked
173
- - **Dangerous commands**: Commands like `curl`, `python`, `chmod` trigger warnings
174
- - **Rate limiting**: 50 commands per run prevents runaway execution
175
- - **Execution timeout**: 30 seconds per command prevents hung processes
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` for monitoring and troubleshooting.
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
- ## Security Testing
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` or `dd`
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