@garyr/pt-cli 0.32.1 → 0.33.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/src/safety.ts ADDED
@@ -0,0 +1,299 @@
1
+ // pt-cli/src/safety.ts
2
+ // Security warnings and safeguards for post_config command execution
3
+ import crypto from 'crypto';
4
+ import os from 'os';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import chalk from 'chalk';
8
+
9
+ // === BLOCKLIST ===
10
+ // Commands that are NEVER allowed (safety-critical operations)
11
+ const BLOCKED_COMMANDS = [
12
+ // Privilege escalation
13
+ 'sudo', 'su', 'su -', 'su root',
14
+ // Disk operations that could destroy data
15
+ 'dd', 'mkfs', 'fdisk', 'mount', 'umount',
16
+ // Shell injection patterns
17
+ ';', '|', '&', '&&', '||',
18
+ // Dangerous chmod
19
+ 'chmod 777', 'chmod -R 777', 'chmod 666',
20
+ // System commands that could kill processes
21
+ 'kill', 'killall', 'pkill', 'fuser',
22
+ // Network operations that could exfiltrate data
23
+ 'nc', 'netcat', 'socat',
24
+ // Package manager with dangerous flags
25
+ 'apt purge', 'apt remove', 'yum remove', 'brew uninstall',
26
+ ];
27
+
28
+ // === DANGEROUS PATTERNS ===
29
+ // Commands that should trigger a warning but are NOT blocked
30
+ const DANGEROUS_PATTERNS = [
31
+ // Destructive file operations
32
+ 'rm -rf', 'rm -r', 'rm --no-preserve-root', 'rm -rf /',
33
+ // Remote downloads + execution
34
+ 'curl', 'wget', 'wget -O', 'curl |', 'wget |',
35
+ // Script execution
36
+ 'bash', 'sh', 'python', 'python3', 'node -e', 'node -p',
37
+ // Shell operations
38
+ 'eval', 'exec', 'source',
39
+ // File system manipulation
40
+ 'chmod -R', 'chown -R', 'chgrp -R',
41
+ // PowerShell (Windows)
42
+ 'powershell', 'pwsh', 'Invoke-Expression', 'IEX',
43
+ // macOS-specific
44
+ 'diskutil', 'hdiutil', 'csrutil',
45
+ ];
46
+
47
+ // === DEFAULT CONFIGURATION ===
48
+ export interface SecurityPolicy {
49
+ maxExecutionTime: number; // milliseconds
50
+ enableAuditLogging: boolean;
51
+ trustedSources: string[];
52
+ maxCommandsPerRun: number;
53
+ securityLevel: 'warn' | 'strict';
54
+ }
55
+
56
+ // Default security policy - warning focused
57
+ const DEFAULT_SECURITY_POLICY: SecurityPolicy = {
58
+ maxExecutionTime: 30000, // 30 seconds
59
+ enableAuditLogging: true,
60
+ trustedSources: [
61
+ 'github.com/garyritchie',
62
+ 'git.lyonritchie.com',
63
+ 'github.com/lyonritchie',
64
+ ],
65
+ maxCommandsPerRun: 50,
66
+ securityLevel: 'warn', // Warning-focused mode
67
+ };
68
+
69
+ /**
70
+ * Check if a command is in the blocklist (NEVER allowed)
71
+ * Only truly dangerous operations that could destroy data
72
+ */
73
+ export function isBlockedCommand(command: string): boolean {
74
+ for (const blocked of BLOCKED_COMMANDS) {
75
+ if (command.includes(blocked)) {
76
+ return true;
77
+ }
78
+ }
79
+ return false;
80
+ }
81
+
82
+ /**
83
+ * Check if a command should trigger a warning (but is allowed)
84
+ */
85
+ export function isDangerousCommand(command: string): boolean {
86
+ for (const pattern of DANGEROUS_PATTERNS) {
87
+ if (command.includes(pattern)) {
88
+ return true;
89
+ }
90
+ }
91
+ return false;
92
+ }
93
+
94
+ /**
95
+ * Execute a command with timeout and error handling
96
+ */
97
+ export async function executeWithTimeout(
98
+ command: string,
99
+ cwd: string,
100
+ timeoutMs: number = 30000
101
+ ): Promise<{ success: boolean; stdout?: string; stderr?: string; timedOut?: boolean }> {
102
+ const { execSync } = await import('child_process');
103
+
104
+ try {
105
+ const output = execSync(command, {
106
+ cwd,
107
+ stdio: 'pipe',
108
+ timeout: timeoutMs,
109
+ encoding: 'utf-8',
110
+ });
111
+ return { success: true, stdout: output };
112
+ } catch (err: any) {
113
+ if (err.code === 'ETIMEDOUT') {
114
+ return { success: false, timedOut: true };
115
+ }
116
+ return {
117
+ success: false,
118
+ stderr: err.stderr || err.message,
119
+ };
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Log a security event to audit log
125
+ */
126
+ export function logSecurityEvent(
127
+ eventType: 'command_executed' | 'command_blocked' | 'command_timed_out' | 'template_loaded',
128
+ command: string,
129
+ templateName: string,
130
+ result: 'success' | 'failed' | 'timedout' | 'blocked'
131
+ ): void {
132
+ const logEntry = {
133
+ timestamp: new Date().toISOString(),
134
+ eventType,
135
+ command,
136
+ template: templateName,
137
+ user: os.userInfo().username,
138
+ result,
139
+ hostname: os.hostname(),
140
+ };
141
+
142
+ const logDir = path.join(os.homedir(), '.pt');
143
+ const logFile = path.join(logDir, 'security-audit.log');
144
+
145
+ try {
146
+ // Ensure log directory exists
147
+ if (!fs.existsSync(logDir)) {
148
+ fs.mkdirSync(logDir, { recursive: true });
149
+ }
150
+
151
+ // Append to audit log
152
+ fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n');
153
+ } catch (err) {
154
+ // Silently fail if logging fails
155
+ console.warn('Warning: Failed to write security audit log');
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Check if a URL is from a trusted source
161
+ */
162
+ export function isTrustedSource(url: string, trustedSources: string[] = DEFAULT_SECURITY_POLICY.trustedSources): boolean {
163
+ return trustedSources.some(source => url.includes(source));
164
+ }
165
+
166
+ /**
167
+ * Check if a command has been executed too many times (rate limiting)
168
+ */
169
+ const executionCounts = new Map<string, number>();
170
+ const RATE_LIMIT_MS = 1000; // 1 second between identical commands
171
+
172
+ export function canExecute(command: string, maxCommandsPerRun: number = 50): boolean {
173
+ // Check total count per run
174
+ const totalExecuted = Array.from(executionCounts.values()).reduce((sum, count) => sum + count, 0);
175
+ if (totalExecuted >= maxCommandsPerRun) {
176
+ return false;
177
+ }
178
+
179
+ // Check rate limit for this specific command
180
+ const hash = crypto.createHash('md5').update(command).digest('hex');
181
+ const last = executionCounts.get(hash) || 0;
182
+ if (Date.now() - last < RATE_LIMIT_MS) {
183
+ return false;
184
+ }
185
+
186
+ // Update count
187
+ executionCounts.set(hash, Date.now());
188
+ return true;
189
+ }
190
+
191
+ /**
192
+ * Reset execution counts (for testing or between runs)
193
+ */
194
+ export function resetExecutionCounts(): void {
195
+ executionCounts.clear();
196
+ }
197
+
198
+ /**
199
+ * Validate a template's security before execution
200
+ */
201
+ export function validateTemplateSecurity(
202
+ templateConfig: any,
203
+ securityPolicy: SecurityPolicy = DEFAULT_SECURITY_POLICY
204
+ ): { valid: boolean; errors: string[]; warnings: string[] } {
205
+ const errors: string[] = [];
206
+ const warnings: string[] = [];
207
+
208
+ // Check post_config tasks
209
+ const postConfigTasks = templateConfig.post_config || [];
210
+ for (const task of postConfigTasks) {
211
+ const command = task.command;
212
+ if (!command) continue;
213
+
214
+ // Check for blocked commands
215
+ if (isBlockedCommand(command)) {
216
+ errors.push(`Blocked command in template: ${command}`);
217
+ }
218
+
219
+ // Warn about dangerous commands
220
+ if (isDangerousCommand(command)) {
221
+ warnings.push(`Dangerous command in template: ${command}`);
222
+ }
223
+
224
+ // Check for shell injection patterns
225
+ if (command.includes(';') || command.includes('|') || command.includes('&')) {
226
+ warnings.push(`Shell injection pattern in command: ${command}`);
227
+ }
228
+
229
+ // Check for remote downloads
230
+ if (command.includes('curl') || command.includes('wget')) {
231
+ warnings.push(`Remote download in command: ${command}`);
232
+ }
233
+ }
234
+
235
+ return {
236
+ valid: errors.length === 0,
237
+ errors,
238
+ warnings,
239
+ };
240
+ }
241
+
242
+ /**
243
+ * Get security policy from config or use defaults
244
+ */
245
+ export function getSecurityPolicy(configPath?: string): SecurityPolicy {
246
+ // Try to load from config
247
+ if (configPath) {
248
+ try {
249
+ const YAML = require('yaml');
250
+ const config = YAML.parse(fs.readFileSync(configPath, 'utf-8'));
251
+ if (config.security) {
252
+ return { ...DEFAULT_SECURITY_POLICY, ...config.security };
253
+ }
254
+ } catch (err) {
255
+ // Fall back to defaults
256
+ }
257
+ }
258
+
259
+ return DEFAULT_SECURITY_POLICY;
260
+ }
261
+
262
+ /**
263
+ * Show warning about dangerous command and wait for user to cancel
264
+ */
265
+ export async function showDangerousCommandWarning(command: string, timeoutSeconds: number = 5): Promise<boolean> {
266
+ const inquirer = (await import('inquirer')).default;
267
+ const readline = await import('readline');
268
+
269
+ console.log(chalk.red('\n⚠️ DANGEROUS COMMAND DETECTED'));
270
+ console.log(chalk.red(` Command: ${command}`));
271
+ console.log(chalk.red(' This could potentially harm your system.'));
272
+ console.log(chalk.yellow(` Press CTRL+C to cancel, or wait ${timeoutSeconds}s to continue...`));
273
+
274
+ // Set up timeout
275
+ const timeout = setTimeout(() => {
276
+ return true; // Continue after timeout
277
+ }, timeoutSeconds * 1000);
278
+
279
+ // Set up readline for immediate cancel
280
+ const rl = readline.createInterface({
281
+ input: process.stdin,
282
+ output: process.stdout,
283
+ });
284
+
285
+ return new Promise((resolve) => {
286
+ rl.on('SIGINT', () => {
287
+ clearTimeout(timeout);
288
+ rl.close();
289
+ console.log(chalk.yellow('\n⚠️ Command cancelled by user'));
290
+ resolve(false);
291
+ });
292
+
293
+ // If timeout expires, resolve without waiting for readline
294
+ setTimeout(() => {
295
+ rl.close();
296
+ resolve(true);
297
+ }, timeoutSeconds * 1000);
298
+ });
299
+ }
@@ -1,24 +0,0 @@
1
- {
2
- "name": "direct-json-test",
3
- "description": "A mock template for direct JSON scaffolding test",
4
- "folders": [
5
- {
6
- "name": "src",
7
- "info": "contains sources",
8
- "children": [
9
- {
10
- "name": "components",
11
- "info": "reusable components"
12
- },
13
- {
14
- "name": "utils",
15
- "info": "utility functions"
16
- }
17
- ]
18
- },
19
- {
20
- "name": "docs",
21
- "info": "documentation folder"
22
- }
23
- ]
24
- }