@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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pt - Project Template CLI
2
2
 
3
- A CLI tool to record directory structures as templates and initialize new projects from them.
3
+ A CLI tool to record directory structures as templates and initialize new projects from them. A [GUI](https://garylritchie.gumroad.com/l/pt-gui) is also in development.
4
4
 
5
5
  ```mermaid
6
6
  graph LR
@@ -133,4 +133,4 @@ Always back up these directories before installing new versions or making signif
133
133
 
134
134
  ## Example Templates
135
135
 
136
- There are a couple of [example templates](https://github.com/search?q=topic%3Atemplate-project+org%3Agaryritchie&type=Repositories) for you to try. These include utilities written in python to help streamline common file management tasks.
136
+ [Example templates](https://github.com/search?q=topic%3Atemplate-project+org%3Agaryritchie&type=Repositories) are available. These include useful python scripts to help streamline common file management tasks.
package/dist/config.js CHANGED
@@ -156,6 +156,23 @@ export function getDefaultPostConfig(config) {
156
156
  checked: t.checked !== false // default to true if not explicitly false
157
157
  }));
158
158
  }
159
+ /**
160
+ * Get security policy from config or use defaults
161
+ */
162
+ export function getSecurityPolicy(config) {
163
+ const defaultPolicy = {
164
+ maxExecutionTime: 30000, // 30 seconds
165
+ enableAuditLogging: true,
166
+ trustedSources: [
167
+ 'github.com/garyritchie',
168
+ 'gitea.lyonritchie.com/garyritchie',
169
+ 'github.com/lyonritchie',
170
+ ],
171
+ maxCommandsPerRun: 50,
172
+ securityLevel: 'warn',
173
+ };
174
+ return config.security || defaultPolicy;
175
+ }
159
176
  // Default exclusions for template scanning
160
177
  export const DEFAULT_EXCLUDES = [
161
178
  '.git',
@@ -1,12 +1,22 @@
1
- import { execSync } from 'child_process';
1
+ import path from 'path';
2
+ import os from 'os';
2
3
  import chalk from 'chalk';
3
4
  import inquirer from 'inquirer';
5
+ import { isBlockedCommand, isDangerousCommand, executeWithTimeout, logSecurityEvent, canExecute, showDangerousCommandWarning, getSecurityPolicy, } from './safety.js';
4
6
  /**
5
7
  * Runs post-configuration tasks for a project.
6
8
  */
7
9
  export async function runPostConfig(destPath, tasks, projectType, options = {}) {
8
10
  if (options.skipPostConfig)
9
11
  return;
12
+ // Load security policy
13
+ const configPath = path.join(process.env.HOME || os.homedir(), '.pt', 'config.yaml');
14
+ const securityPolicy = getSecurityPolicy(configPath);
15
+ // Check rate limiting
16
+ if (!canExecute('init', securityPolicy.maxCommandsPerRun)) {
17
+ console.log(chalk.yellow('⚠️ Rate limit reached: max commands per run exceeded'));
18
+ return;
19
+ }
10
20
  // 1. Filter tasks by type
11
21
  const applicableTasks = tasks.filter(t => !t.type || t.type === projectType);
12
22
  if (applicableTasks.length === 0) {
@@ -37,20 +47,53 @@ export async function runPostConfig(destPath, tasks, projectType, options = {})
37
47
  const task = applicableTasks[i];
38
48
  const progress = `[${i + 1}/${applicableTasks.length}]`;
39
49
  if (task.command) {
50
+ // SECURITY CHECK 1: Blocklist check (NEVER allow these)
51
+ if (isBlockedCommand(task.command)) {
52
+ console.log(chalk.red(`${progress} ⚠️ BLOCKED: ${task.command}`));
53
+ logSecurityEvent('command_blocked', task.command, projectType, 'blocked');
54
+ continue;
55
+ }
56
+ // SECURITY CHECK 2: Dangerous command warning (but allow execution)
57
+ if (isDangerousCommand(task.command)) {
58
+ console.log(chalk.yellow(`${progress} ⚠️ WARNING: This command may be dangerous: ${task.command}`));
59
+ console.log(chalk.yellow(` Press CTRL+C to cancel, or wait 5s to continue...`));
60
+ // Wait for user to cancel or timeout
61
+ const allowContinue = await showDangerousCommandWarning(task.command, 5);
62
+ if (!allowContinue) {
63
+ console.log(chalk.yellow(`${progress} ⊘ Command cancelled by user`));
64
+ logSecurityEvent('command_blocked', task.command, projectType, 'blocked');
65
+ continue;
66
+ }
67
+ }
68
+ // SECURITY CHECK 3: Rate limiting
69
+ if (!canExecute(task.command, securityPolicy.maxCommandsPerRun)) {
70
+ console.log(chalk.red(`${progress} ⚠️ Rate limited: too many commands executed`));
71
+ continue;
72
+ }
40
73
  if (options.dryRun) {
41
74
  console.log(chalk.gray(` [DRY RUN] Would run: ${task.command}`));
42
75
  }
43
76
  else {
44
77
  try {
45
78
  console.log(chalk.yellow(`\n${progress} Running: ${task.command}`));
46
- execSync(task.command, {
47
- cwd: destPath,
48
- stdio: 'inherit'
49
- });
50
- console.log(chalk.green(' Command completed successfully'));
79
+ // SECURITY CHECK 4: Execution timeout
80
+ const result = await executeWithTimeout(task.command, destPath, securityPolicy.maxExecutionTime);
81
+ if (result.timedOut) {
82
+ console.log(chalk.red(` ✗ Command timed out after ${securityPolicy.maxExecutionTime / 1000}s`));
83
+ logSecurityEvent('command_timed_out', task.command, projectType, 'timedout');
84
+ }
85
+ else if (result.success) {
86
+ console.log(chalk.green(' ✓ Command completed successfully'));
87
+ logSecurityEvent('command_executed', task.command, projectType, 'success');
88
+ }
89
+ else {
90
+ console.log(chalk.red(` ✗ Command failed: ${result.stderr}`));
91
+ logSecurityEvent('command_executed', task.command, projectType, 'failed');
92
+ }
51
93
  }
52
94
  catch (err) {
53
95
  console.log(chalk.red(' ✗ Command failed'));
96
+ logSecurityEvent('command_executed', task.command, projectType, 'failed');
54
97
  }
55
98
  }
56
99
  }
package/dist/remote.js CHANGED
@@ -5,7 +5,31 @@ import os from 'os';
5
5
  import { Readable } from 'stream';
6
6
  import { finished } from 'stream/promises';
7
7
  import { extract } from 'tar'; // You'll need: npm install tar
8
+ import chalk from 'chalk';
9
+ import { isTrustedSource, logSecurityEvent } from './safety.js';
10
+ import { loadConfig } from './config.js';
8
11
  export async function downloadAndExtract(url) {
12
+ // Load security policy
13
+ const configPath = path.join(process.env.HOME || os.homedir(), '.pt', 'config.yaml');
14
+ const config = loadConfig();
15
+ const securityPolicy = config.security || {
16
+ trustedSources: ['github.com/garyritchie', 'gitea.lyonritchie.com/garyritchie', 'github.com/lyonritchie'],
17
+ };
18
+ // SECURITY CHECK: Verify source is trusted
19
+ if (!isTrustedSource(url, securityPolicy.trustedSources)) {
20
+ console.log(chalk.yellow(`⚠️ Warning: Template from untrusted source: ${url}`));
21
+ console.log(chalk.yellow(' Only use templates from trusted sources'));
22
+ const inquirer = (await import('inquirer')).default;
23
+ const response = await inquirer.prompt({
24
+ type: 'confirm',
25
+ name: 'proceed',
26
+ message: chalk.red('Continue anyway?'),
27
+ default: false
28
+ });
29
+ if (!response.proceed) {
30
+ throw new Error('Download cancelled by user due to untrusted source');
31
+ }
32
+ }
9
33
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pt-template-'));
10
34
  let downloadUrl = url;
11
35
  // Strip trailing slash and .git suffix before converting to archive URL
@@ -23,9 +47,17 @@ export async function downloadAndExtract(url) {
23
47
  const dest = path.join(tempDir, 'template.tar.gz');
24
48
  const fileStream = fs.createWriteStream(dest);
25
49
  await finished(Readable.fromWeb(response.body).pipe(fileStream));
50
+ // SECURITY: Validate downloaded file before extraction
51
+ const stats = fs.statSync(dest);
52
+ if (stats.size > 50 * 1024 * 1024) { // 50MB limit
53
+ throw new Error('Downloaded template is too large (>50MB)');
54
+ }
26
55
  // Extract tarball
27
56
  await extract({ file: dest, cwd: tempDir });
28
57
  // Find the actual content folder (archives usually wrap content in a folder)
29
58
  const dirs = fs.readdirSync(tempDir).filter(f => fs.statSync(path.join(tempDir, f)).isDirectory());
30
- return path.join(tempDir, dirs[0]);
59
+ const extractedPath = path.join(tempDir, dirs[0]);
60
+ // SECURITY: Log successful download
61
+ logSecurityEvent('template_loaded', downloadUrl, 'remote', 'success');
62
+ return extractedPath;
31
63
  }
package/dist/safety.js ADDED
@@ -0,0 +1,250 @@
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
+ // === BLOCKLIST ===
9
+ // Commands that are NEVER allowed (safety-critical operations)
10
+ const BLOCKED_COMMANDS = [
11
+ // Privilege escalation
12
+ 'sudo', 'su', 'su -', 'su root',
13
+ // Disk operations that could destroy data
14
+ 'dd', 'mkfs', 'fdisk', 'mount', 'umount',
15
+ // Shell injection patterns
16
+ ';', '|', '&', '&&', '||',
17
+ // Dangerous chmod
18
+ 'chmod 777', 'chmod -R 777', 'chmod 666',
19
+ // System commands that could kill processes
20
+ 'kill', 'killall', 'pkill', 'fuser',
21
+ // Network operations that could exfiltrate data
22
+ 'nc', 'netcat', 'socat',
23
+ // Package manager with dangerous flags
24
+ 'apt purge', 'apt remove', 'yum remove', 'brew uninstall',
25
+ ];
26
+ // === DANGEROUS PATTERNS ===
27
+ // Commands that should trigger a warning but are NOT blocked
28
+ const DANGEROUS_PATTERNS = [
29
+ // Destructive file operations
30
+ 'rm -rf', 'rm -r', 'rm --no-preserve-root', 'rm -rf /',
31
+ // Remote downloads + execution
32
+ 'curl', 'wget', 'wget -O', 'curl |', 'wget |',
33
+ // Script execution
34
+ 'bash', 'sh', 'python', 'python3', 'node -e', 'node -p',
35
+ // Shell operations
36
+ 'eval', 'exec', 'source',
37
+ // File system manipulation
38
+ 'chmod -R', 'chown -R', 'chgrp -R',
39
+ // PowerShell (Windows)
40
+ 'powershell', 'pwsh', 'Invoke-Expression', 'IEX',
41
+ // macOS-specific
42
+ 'diskutil', 'hdiutil', 'csrutil',
43
+ ];
44
+ // Default security policy - warning focused
45
+ const DEFAULT_SECURITY_POLICY = {
46
+ maxExecutionTime: 30000, // 30 seconds
47
+ enableAuditLogging: true,
48
+ trustedSources: [
49
+ 'github.com/garyritchie',
50
+ 'git.lyonritchie.com',
51
+ 'github.com/lyonritchie',
52
+ ],
53
+ maxCommandsPerRun: 50,
54
+ securityLevel: 'warn', // Warning-focused mode
55
+ };
56
+ /**
57
+ * Check if a command is in the blocklist (NEVER allowed)
58
+ * Only truly dangerous operations that could destroy data
59
+ */
60
+ export function isBlockedCommand(command) {
61
+ for (const blocked of BLOCKED_COMMANDS) {
62
+ if (command.includes(blocked)) {
63
+ return true;
64
+ }
65
+ }
66
+ return false;
67
+ }
68
+ /**
69
+ * Check if a command should trigger a warning (but is allowed)
70
+ */
71
+ export function isDangerousCommand(command) {
72
+ for (const pattern of DANGEROUS_PATTERNS) {
73
+ if (command.includes(pattern)) {
74
+ return true;
75
+ }
76
+ }
77
+ return false;
78
+ }
79
+ /**
80
+ * Execute a command with timeout and error handling
81
+ */
82
+ export async function executeWithTimeout(command, cwd, timeoutMs = 30000) {
83
+ const { execSync } = await import('child_process');
84
+ try {
85
+ const output = execSync(command, {
86
+ cwd,
87
+ stdio: 'pipe',
88
+ timeout: timeoutMs,
89
+ encoding: 'utf-8',
90
+ });
91
+ return { success: true, stdout: output };
92
+ }
93
+ catch (err) {
94
+ if (err.code === 'ETIMEDOUT') {
95
+ return { success: false, timedOut: true };
96
+ }
97
+ return {
98
+ success: false,
99
+ stderr: err.stderr || err.message,
100
+ };
101
+ }
102
+ }
103
+ /**
104
+ * Log a security event to audit log
105
+ */
106
+ export function logSecurityEvent(eventType, command, templateName, result) {
107
+ const logEntry = {
108
+ timestamp: new Date().toISOString(),
109
+ eventType,
110
+ command,
111
+ template: templateName,
112
+ user: os.userInfo().username,
113
+ result,
114
+ hostname: os.hostname(),
115
+ };
116
+ const logDir = path.join(os.homedir(), '.pt');
117
+ const logFile = path.join(logDir, 'security-audit.log');
118
+ try {
119
+ // Ensure log directory exists
120
+ if (!fs.existsSync(logDir)) {
121
+ fs.mkdirSync(logDir, { recursive: true });
122
+ }
123
+ // Append to audit log
124
+ fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n');
125
+ }
126
+ catch (err) {
127
+ // Silently fail if logging fails
128
+ console.warn('Warning: Failed to write security audit log');
129
+ }
130
+ }
131
+ /**
132
+ * Check if a URL is from a trusted source
133
+ */
134
+ export function isTrustedSource(url, trustedSources = DEFAULT_SECURITY_POLICY.trustedSources) {
135
+ return trustedSources.some(source => url.includes(source));
136
+ }
137
+ /**
138
+ * Check if a command has been executed too many times (rate limiting)
139
+ */
140
+ const executionCounts = new Map();
141
+ const RATE_LIMIT_MS = 1000; // 1 second between identical commands
142
+ export function canExecute(command, maxCommandsPerRun = 50) {
143
+ // Check total count per run
144
+ const totalExecuted = Array.from(executionCounts.values()).reduce((sum, count) => sum + count, 0);
145
+ if (totalExecuted >= maxCommandsPerRun) {
146
+ return false;
147
+ }
148
+ // Check rate limit for this specific command
149
+ const hash = crypto.createHash('md5').update(command).digest('hex');
150
+ const last = executionCounts.get(hash) || 0;
151
+ if (Date.now() - last < RATE_LIMIT_MS) {
152
+ return false;
153
+ }
154
+ // Update count
155
+ executionCounts.set(hash, Date.now());
156
+ return true;
157
+ }
158
+ /**
159
+ * Reset execution counts (for testing or between runs)
160
+ */
161
+ export function resetExecutionCounts() {
162
+ executionCounts.clear();
163
+ }
164
+ /**
165
+ * Validate a template's security before execution
166
+ */
167
+ export function validateTemplateSecurity(templateConfig, securityPolicy = DEFAULT_SECURITY_POLICY) {
168
+ const errors = [];
169
+ const warnings = [];
170
+ // Check post_config tasks
171
+ const postConfigTasks = templateConfig.post_config || [];
172
+ for (const task of postConfigTasks) {
173
+ const command = task.command;
174
+ if (!command)
175
+ continue;
176
+ // Check for blocked commands
177
+ if (isBlockedCommand(command)) {
178
+ errors.push(`Blocked command in template: ${command}`);
179
+ }
180
+ // Warn about dangerous commands
181
+ if (isDangerousCommand(command)) {
182
+ warnings.push(`Dangerous command in template: ${command}`);
183
+ }
184
+ // Check for shell injection patterns
185
+ if (command.includes(';') || command.includes('|') || command.includes('&')) {
186
+ warnings.push(`Shell injection pattern in command: ${command}`);
187
+ }
188
+ // Check for remote downloads
189
+ if (command.includes('curl') || command.includes('wget')) {
190
+ warnings.push(`Remote download in command: ${command}`);
191
+ }
192
+ }
193
+ return {
194
+ valid: errors.length === 0,
195
+ errors,
196
+ warnings,
197
+ };
198
+ }
199
+ /**
200
+ * Get security policy from config or use defaults
201
+ */
202
+ export function getSecurityPolicy(configPath) {
203
+ // Try to load from config
204
+ if (configPath) {
205
+ try {
206
+ const YAML = require('yaml');
207
+ const config = YAML.parse(fs.readFileSync(configPath, 'utf-8'));
208
+ if (config.security) {
209
+ return { ...DEFAULT_SECURITY_POLICY, ...config.security };
210
+ }
211
+ }
212
+ catch (err) {
213
+ // Fall back to defaults
214
+ }
215
+ }
216
+ return DEFAULT_SECURITY_POLICY;
217
+ }
218
+ /**
219
+ * Show warning about dangerous command and wait for user to cancel
220
+ */
221
+ export async function showDangerousCommandWarning(command, timeoutSeconds = 5) {
222
+ const inquirer = (await import('inquirer')).default;
223
+ const readline = await import('readline');
224
+ console.log(chalk.red('\n⚠️ DANGEROUS COMMAND DETECTED'));
225
+ console.log(chalk.red(` Command: ${command}`));
226
+ console.log(chalk.red(' This could potentially harm your system.'));
227
+ console.log(chalk.yellow(` Press CTRL+C to cancel, or wait ${timeoutSeconds}s to continue...`));
228
+ // Set up timeout
229
+ const timeout = setTimeout(() => {
230
+ return true; // Continue after timeout
231
+ }, timeoutSeconds * 1000);
232
+ // Set up readline for immediate cancel
233
+ const rl = readline.createInterface({
234
+ input: process.stdin,
235
+ output: process.stdout,
236
+ });
237
+ return new Promise((resolve) => {
238
+ rl.on('SIGINT', () => {
239
+ clearTimeout(timeout);
240
+ rl.close();
241
+ console.log(chalk.yellow('\n⚠️ Command cancelled by user'));
242
+ resolve(false);
243
+ });
244
+ // If timeout expires, resolve without waiting for readline
245
+ setTimeout(() => {
246
+ rl.close();
247
+ resolve(true);
248
+ }, timeoutSeconds * 1000);
249
+ });
250
+ }
@@ -8,6 +8,10 @@ Config is stored at `~/.pt/config.yaml` and contains:
8
8
  - `ignore`: Global folder ignore patterns for `pt learn`
9
9
  - `variables`: Global variable suggestions for `pt learn` (name, prompt, default, required)
10
10
 
11
+ ## Security Policy
12
+
13
+ Please see [[security]].
14
+
11
15
  ## Template Variables
12
16
 
13
17
  When learning a template, you can define variables that will be prompted during initialization:
@@ -52,6 +56,13 @@ If the directory contains a `.pt-template.json` or `template.json` file with a `
52
56
  2. Manually edit `~/.pt/config.yaml` to refine `copy_files`, `post_config` commands, or add specific `chmod` requirements.
53
57
  3. Alternatively, initialize a temporary project from your learned template (`pt init`), refine it manually, and then use `pt update` from that directory to "re-learn" the refined state.
54
58
 
59
+ **Security Note:** All post-config commands are subject to security validation:
60
+ - Dangerous commands (e.g., `curl`, `python`, `chmod`) trigger warnings with 5-second cancellation
61
+ - Absolute blocks (e.g., `sudo`, `rm -rf`, `dd`) are never allowed
62
+ - Rate limiting prevents runaway execution (50 commands per run)
63
+ - Execution timeout (30 seconds) prevents hung processes
64
+ - All events are logged to `~/.pt/security-audit.log`
65
+
55
66
  ```
56
67
  javascript: [git init, npm install]
57
68
  python: [git init, python -m venv .venv, pip install -r requirements.txt]
@@ -0,0 +1,132 @@
1
+ # Security Guide
2
+
3
+ ## Overview
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.
6
+
7
+ ## Security Policy Configuration
8
+
9
+ Security settings are configured in `~/.pt/config.yaml` under the `security` key:
10
+
11
+ ```yaml
12
+ security:
13
+ securityLevel: "warn" # "warn" (default) or "strict"
14
+ trustedSources:
15
+ - "github.com/garyritchie"
16
+ - "git.lyonritchie.com/garyritchie"
17
+ - "github.com/lyonritchie"
18
+ maxExecutionTime: 30000 # 30 seconds per command
19
+ maxCommandsPerRun: 50 # rate limit per init session
20
+ enableAuditLogging: true # write events to security-audit.log
21
+ ```
22
+
23
+ ### Security Levels
24
+
25
+ - **`warn`** (default): Warning-based approach with cancellation prompts
26
+ - **`strict`**: More conservative defaults, enabled by default for new installations
27
+
28
+ ### Trusted Sources
29
+
30
+ When downloading templates from remote URLs, `pt-cli` verifies the source against the `trustedSources` list. Untrusted sources trigger a warning and require explicit user confirmation before proceeding.
31
+
32
+ ## Command Security
33
+
34
+ ### Absolute Blocks (Never Allowed)
35
+
36
+ The following commands are **always blocked** regardless of security level:
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)
42
+
43
+ ### Dangerous Commands (Warning Only)
44
+
45
+ The following commands trigger a **5-second countdown** with CTRL+C cancellation:
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)
50
+
51
+ **Example interaction:**
52
+
53
+ ```bash
54
+ ⚠️ WARNING: This command may be dangerous: npm install
55
+ Press CTRL+C to cancel, or wait 5s to continue...
56
+ ```
57
+
58
+ ### Rate Limiting
59
+
60
+ - **50 commands per run**: Prevents runaway command execution
61
+ - If limit is reached, subsequent commands are skipped with a warning
62
+
63
+ ### Execution Timeout
64
+
65
+ - **30 seconds per command**: Prevents hung processes
66
+ - Timed-out commands are logged and skipped
67
+
68
+ ## Remote Template Security
69
+
70
+ When downloading templates from remote URLs:
71
+
72
+ 1. **Source Verification**: Checks against `trustedSources` list
73
+ 2. **File Size Validation**: Maximum 50MB download limit
74
+ 3. **Archive Extraction**: Extracts to secure temporary directory
75
+ 4. **Audit Logging**: All downloads are logged with timestamps and outcomes
76
+
77
+ ## Audit Logging
78
+
79
+ All security events are logged to `~/.pt/security-audit.log`:
80
+
81
+ ```
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
+ ```
86
+
87
+ ## Security Best Practices
88
+
89
+ ### For Users
90
+
91
+ 1. **Review post-config tasks**: Always review commands before executing
92
+ 2. **Use trusted sources**: Only download templates from known repositories
93
+ 3. **Monitor audit logs**: Check `~/.pt/security-audit.log` for suspicious activity
94
+ 4. **Update regularly**: Keep `pt-cli` updated for latest security improvements
95
+
96
+ ### For Template Authors
97
+
98
+ 1. **Avoid dangerous commands**: Don't include `sudo`, `rm -rf`, or privilege escalation in templates
99
+ 2. **Use safe defaults**: Prefer `npm install` over custom scripts
100
+ 3. **Provide clear descriptions**: Explain what each post-config task does
101
+ 4. **Test thoroughly**: Verify templates work in isolated environments
102
+
103
+ ## Troubleshooting
104
+
105
+ ### Security Events Not Logging
106
+
107
+ 1. Check write permissions to `~/.pt/` directory
108
+ 2. Verify `enableAuditLogging: true` in config
109
+ 3. Check for disk space issues
110
+
111
+ ### Commands Blocked Unexpectedly
112
+
113
+ 1. Check if command matches absolute blocklist
114
+ 2. Review security policy configuration
115
+ 3. Consult audit log for specific reasons
116
+
117
+ ### Remote Template Download Failed
118
+
119
+ 1. Verify URL is in `trustedSources` list
120
+ 2. Check network connectivity
121
+ 3. Verify file size is under 50MB limit
122
+ 4. Check for valid archive format
123
+
124
+ ## Security Policy Reference
125
+
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 |
package/doc/testing.md CHANGED
@@ -8,18 +8,36 @@ The test suite uses Node.js's native test runner (`node:test`) and assertion lib
8
8
 
9
9
  ## Running Tests
10
10
 
11
+ ### 0. Security Testing
12
+
13
+ Security features can be tested by:
14
+
15
+ 1. **Testing command blocks**: Try running templates with dangerous commands like `sudo rm -rf` or `dd`
16
+ 2. **Testing remote downloads**: Use untrusted URLs to verify source verification
17
+ 3. **Testing rate limiting**: Execute more than 50 commands in a single init session
18
+ 4. **Testing timeouts**: Run commands that hang to verify timeout behavior
19
+ 5. **Reviewing audit logs**: Check `~/.pt/security-audit.log` for security events
20
+
21
+ For more details, see the [Security Guide](security.md).
22
+
11
23
  ### 1. Run the Entire Test Suite
24
+
12
25
  To execute all tests:
26
+
13
27
  ```bash
14
28
  npm test
15
29
  ```
30
+
16
31
  This runs the underlying command:
32
+
17
33
  ```bash
18
34
  node --import tsx --test tests/**/*.test.ts
19
35
  ```
20
36
 
21
37
  ### 2. Run Individual Test Files
38
+
22
39
  To run a specific test suite, use `tsx`:
40
+
23
41
  ```bash
24
42
  npx tsx --test tests/config.test.ts
25
43
  npx tsx --test tests/learn.test.ts
@@ -27,7 +45,9 @@ npx tsx --test tests/substitute.test.ts
27
45
  ```
28
46
 
29
47
  ### 3. Run with Test Coverage
48
+
30
49
  To generate a test coverage report directly in the terminal:
50
+
31
51
  ```bash
32
52
  node --experimental-test-coverage --import tsx --test tests/**/*.test.ts
33
53
  ```
@@ -42,7 +62,7 @@ When writing new tests, please adhere to these guidelines:
42
62
  2. **ESM Imports**: Since this is an ESM (ECMAScript Modules) project, file imports within tests must use the `.js` extension (e.g., `import { learn } from '../src/commands/learnCommand.js';`).
43
63
  3. **Environment Isolation**: The configuration path relies on `process.env.HOME`. To prevent tests from polluting your user config directory, override the home directory before importing any CLI files:
44
64
  ```typescript
45
- const testHome = path.join(process.cwd(), '.test-home-custom');
65
+ const testHome = path.join(process.cwd(), ".test-home-custom");
46
66
  process.env.HOME = testHome;
47
67
  ```
48
68
  4. **Cleanup**: Always ensure temporary files, workspace directories, and test home directories are deleted after tests complete (e.g., in a `finally` block or `after` hook).