Skip to content

Security: kcenon/claude_code_agent

Security

docs/security.md

Security Module

The AD-SDLC security module provides essential security utilities for building secure agent-driven systems.

Overview

The security module includes:

  • SecretManager - Secure API key and secret management
  • InputValidator - Input validation and sanitization
  • AuditLogger - Security audit logging
  • SecureFileHandler - Secure temporary file handling
  • RateLimiter - API rate limiting
  • CommandSanitizer - Safe shell command execution (prevents command injection)
  • SecureFileOps - Centralized secure file operations with path validation
  • PathResolver - Project-aware path resolution with traversal prevention

Installation

The security module is included in the main ad-sdlc package:

import {
  SecretManager,
  InputValidator,
  AuditLogger,
  SecureFileHandler,
  RateLimiter,
  CommandSanitizer,
  getCommandSanitizer,
  SecureFileOps,
  createSecureFileOps,
  PathResolver,
} from 'ad-sdlc';

SecretManager

Manages secure access to API keys and secrets with automatic masking.

Basic Usage

import { SecretManager, getSecretManager } from 'ad-sdlc';

// Using singleton
const secrets = getSecretManager();
await secrets.load();

// Get a secret
const apiKey = secrets.get('CLAUDE_API_KEY');

// Check if secret exists
if (secrets.has('GITHUB_TOKEN')) {
  const token = secrets.get('GITHUB_TOKEN');
}

// Get with default value
const optional = secrets.getOrDefault('OPTIONAL_KEY', 'default');

Secret Masking

Prevent secrets from appearing in logs:

const secrets = getSecretManager();
await secrets.load();

// Mask secrets in text
const logMessage = `Connecting with key: ${secrets.get('API_KEY')}`;
console.log(secrets.mask(logMessage));
// Output: "Connecting with key: [API_KEY_REDACTED]"

// Create a safe logger
const safeLog = secrets.createSafeLogger(console.log);
safeLog(`Using token: ${token}`); // Automatically masked

Configuration

const secrets = new SecretManager({
  envFilePath: '.env.local',
  requiredSecrets: ['CLAUDE_API_KEY', 'GITHUB_TOKEN'],
  throwOnMissing: true, // Throw if required secrets are missing
});

InputValidator

Validates and sanitizes user inputs to prevent security vulnerabilities.

File Path Validation

Prevents path traversal attacks:

import { InputValidator, PathTraversalError } from 'ad-sdlc';

const validator = new InputValidator({
  basePath: '/app/data',
});

// Valid path
const safePath = validator.validateFilePath('subdir/file.txt');
// Returns: '/app/data/subdir/file.txt'

// Path traversal attempt - throws error
try {
  validator.validateFilePath('../etc/passwd');
} catch (error) {
  if (error instanceof PathTraversalError) {
    console.error('Path traversal detected!');
  }
}

URL Validation

const validator = new InputValidator({
  basePath: '/app',
  allowedProtocols: ['https:'],
  blockInternalUrls: true,
});

// Valid URL
const url = validator.validateUrl('https://api.github.com/repos');

// Blocked - HTTP not allowed
validator.validateUrl('http://example.com'); // Throws InvalidUrlError

// Blocked - Internal URL
validator.validateUrl('https://localhost/api'); // Throws InvalidUrlError

Input Sanitization

// Remove control characters
const clean = validator.sanitizeUserInput('Hello\x00World');
// Returns: 'HelloWorld'

// Validate email
if (validator.isValidEmail('user@example.com')) {
  // Valid email
}

// Validate GitHub repository
const repo = validator.validateGitHubRepo('https://github.com/owner/repo');
// Returns: 'owner/repo'

// Validate semantic version
if (validator.isValidSemver('1.2.3')) {
  // Valid semver
}

// Validate branch name
if (validator.isValidBranchName('feature/add-login')) {
  // Valid branch name
}

AuditLogger

Logs security-sensitive operations for compliance and debugging.

Basic Usage

import { AuditLogger, getAuditLogger } from 'ad-sdlc';

const audit = getAuditLogger({
  logDir: '.ad-sdlc/logs/audit',
  consoleOutput: process.env.NODE_ENV !== 'production',
});

// Log events
audit.log({
  type: 'api_key_used',
  actor: 'collector-agent',
  resource: 'CLAUDE_API_KEY',
  action: 'authenticate',
  result: 'success',
});

Convenience Methods

// GitHub operations
audit.logGitHubIssueCreated(42, 'owner/repo', 'issue-generator');
audit.logGitHubPRCreated(123, 'owner/repo', 'pr-reviewer');
audit.logGitHubPRMerged(123, 'owner/repo', 'controller');

// File operations
audit.logFileCreated('/path/to/file', 'worker-agent');
audit.logFileModified('/path/to/file', 'worker-agent');
audit.logFileDeleted('/path/to/file', 'worker-agent');

// Security events
audit.logSecurityViolation('path_traversal', 'unknown-user', {
  attemptedPath: '../etc/passwd',
});

// Validation failures
audit.logValidationFailed('email', 'user', {
  input: 'invalid-email',
  reason: 'missing @ symbol',
});

Correlation IDs

Track related operations:

const correlationId = audit.newCorrelationId();
// All subsequent logs include this correlation ID

// Or set a specific ID
audit.setCorrelationId('request-123');

Reading Audit Logs

const entries = audit.getRecentEntries(100);
for (const entry of entries) {
  console.log(`${entry.timestamp}: ${entry.type} - ${entry.result}`);
}

SecureFileHandler

Handles temporary files securely with automatic cleanup.

Basic Usage

import { SecureFileHandler, getSecureFileHandler } from 'ad-sdlc';

const files = getSecureFileHandler({
  autoCleanup: true, // Clean up on process exit
});

// Create temporary file
const tempFile = await files.createTempFile('secret content', '.json');
// File has 0o600 permissions (owner read/write only)

// Create temporary directory
const tempDir = await files.createTempDir();
// Directory has 0o700 permissions

// Write securely
await files.writeSecure('/path/to/file', 'content');

// Read with permission check
const content = await files.readSecure('/path/to/file');

Cleanup

// Automatic cleanup on process exit (default)
const files = new SecureFileHandler({ autoCleanup: true });

// Manual cleanup
await files.cleanupAll();

// Track/untrack files
files.track('/path/to/watch');
files.untrack('/path/to/ignore');

Secure Operations

// Copy with secure permissions
await files.copySecure('/source', '/dest');

// Move with tracking update
await files.moveSecure('/source', '/dest');

// Check file security
const stats = await files.getSecureStats('/path/to/file');
if (!stats.isSecure) {
  console.warn('Security warnings:', stats.warnings);
}

RateLimiter

Prevents API abuse with configurable rate limiting.

Basic Usage

import { RateLimiter, RateLimiters } from 'ad-sdlc';

// Custom limiter
const limiter = new RateLimiter({
  maxRequests: 100,
  windowMs: 60000, // 1 minute
});

// Check and consume
const status = limiter.check('user-key');
if (status.allowed) {
  // Proceed with request
  console.log(`Remaining: ${status.remaining}`);
} else {
  console.log(`Rate limited. Retry in ${status.resetIn}ms`);
}

// Check and throw if exceeded
try {
  limiter.checkOrThrow('user-key');
} catch (error) {
  if (error instanceof RateLimitExceededError) {
    console.log(`Retry after ${error.retryAfterMs}ms`);
  }
}

Preset Limiters

// GitHub API (5000/hour)
const github = RateLimiters.github();

// Claude API (60/minute)
const claude = RateLimiters.claude();

// Strict (10/minute)
const strict = RateLimiters.strict();

// Lenient (1000/minute)
const lenient = RateLimiters.lenient();

Status Without Consuming

// Check status without consuming a token
const status = limiter.getStatus('user-key');
console.log(`Available: ${status.remaining}/${config.maxRequests}`);

// Reset specific key
limiter.reset('user-key');

// Reset all keys
limiter.resetAll();

CommandSanitizer

Provides safe shell command execution by preventing command injection attacks.

The Problem

Using exec() or execSync() with unsanitized user input can lead to command injection:

// DANGEROUS - Never do this!
const userInput = '; rm -rf /';
exec(`git checkout ${userInput}`); // Would execute: git checkout ; rm -rf /

Solution: CommandSanitizer

CommandSanitizer uses execFile() instead of exec(), which bypasses the shell entirely:

import { getCommandSanitizer } from 'ad-sdlc';

const sanitizer = getCommandSanitizer();

// Safe execution - uses execFile, bypasses shell
const result = await sanitizer.execGit(['checkout', 'main']);
console.log(result.stdout);

Basic Usage

import { CommandSanitizer, getCommandSanitizer } from 'ad-sdlc';

const sanitizer = getCommandSanitizer();

// Execute git commands safely
const gitStatus = await sanitizer.execGit(['status', '--porcelain']);

// Execute GitHub CLI commands safely
const prList = await sanitizer.execGh(['pr', 'list', '--json', 'number,title']);

// Synchronous versions
const branch = sanitizer.execGitSync(['rev-parse', '--abbrev-ref', 'HEAD']);

Command Validation

Commands are validated against a whitelist before execution:

// Validate and sanitize a command
const validated = sanitizer.validateCommand('git', ['status', '--porcelain']);
// Returns: { baseCommand: 'git', subCommand: 'status', args: ['status', '--porcelain'], rawCommand: 'git status --porcelain' }

// Execute the validated command
const result = await sanitizer.safeExec(validated);

Argument Sanitization

Arguments are checked for shell metacharacters and dangerous patterns:

// Safe argument
const safe = sanitizer.sanitizeArgument('feature/my-branch');
// Returns: 'feature/my-branch'

// Dangerous argument - throws CommandInjectionError
sanitizer.sanitizeArgument('; rm -rf /');
// Throws: CommandInjectionError - Shell metacharacter detected

String-Based Execution (Migration)

For backward compatibility with existing code using command strings:

// Parse and execute a command string safely
const result = await sanitizer.execFromString('gh pr view 123 --json url');

// Synchronous version
const syncResult = sanitizer.execFromStringSync('git log -1 --oneline');

The parser respects quoted strings:

// Double-quoted content is preserved
await sanitizer.execFromString('git commit -m "fix: add new feature"');

// Single-quoted content is preserved
await sanitizer.execFromString("git commit -m 'fix: add new feature'");

Escaping Content for Command Strings

When building command strings with user content:

const userMessage = 'Fix the "bug" in parser';

// Escape for use in double quotes
const escaped = sanitizer.escapeForParser(userMessage);
// Returns: 'Fix the \\"bug\\" in parser'

// Use in command string
await sanitizer.execFromString(`git commit -m "${escaped}"`);

Whitelisted Commands

By default, only these commands are allowed:

Command Subcommands
git status, add, commit, push, pull, checkout, branch, log, diff, show, fetch, merge, rebase, stash, reset, tag, init, clone, remote, rev-parse
gh pr, issue, repo, auth, api, run, workflow
npm install, ci, run, test, build, audit, version, pack, publish, exec, cache
npx (any)
node (any)
tsc (any)
eslint (any)
prettier (any)
vitest (any)
jest (any)

Error Handling

import {
  CommandInjectionError,
  CommandNotAllowedError
} from 'ad-sdlc';

try {
  await sanitizer.execFromString('curl http://evil.com');
} catch (error) {
  if (error instanceof CommandNotAllowedError) {
    console.error('Command not in whitelist');
  }
}

try {
  await sanitizer.execFromString('git checkout $(whoami)');
} catch (error) {
  if (error instanceof CommandInjectionError) {
    console.error('Command injection detected');
  }
}

Configuration for Validated Commands

// Validate command for hook/config usage
const result = sanitizer.validateConfigCommand('npm run build');
if (result.valid) {
  // Safe to execute in config context
} else {
  console.error('Dangerous command:', result.reason);
}

Audit Logging

CommandSanitizer can log all command executions for security auditing:

import { CommandSanitizer } from 'ad-sdlc';

const sanitizer = new CommandSanitizer({
  // Enable audit logging (default: true)
  enableAuditLog: true,

  // Actor name for audit logs
  actor: 'worker-agent',

  // Optional: also log to console
  logCommands: false,
});

// All executions are logged to the audit log
await sanitizer.execGit(['status', '--porcelain']);
// Audit log entry:
// {
//   type: 'command_executed',
//   actor: 'worker-agent',
//   resource: 'git',
//   action: 'status',
//   result: 'success',
//   details: { rawCommand: 'git status --porcelain', durationMs: 45 }
// }

Sensitive data in command arguments is automatically masked:

// Token is masked in audit logs
await sanitizer.execFromString('gh auth login --with-token MY_SECRET_TOKEN');
// Logged as: 'gh auth login --with-token [REDACTED]'

Runtime Whitelist Updates

CommandSanitizer supports runtime whitelist updates without restarting the application:

Update Whitelist Directly

import { getCommandSanitizer } from 'ad-sdlc';

const sanitizer = getCommandSanitizer();

// Update with new configuration (replaces existing whitelist)
const result = await sanitizer.updateWhitelist({
  customCommand: {
    allowed: true,
    subcommands: ['sub1', 'sub2'],
    maxArgs: 10,
  },
});

if (result.success) {
  console.log(`Whitelist updated to version ${result.version}`);
  console.log(`Commands available: ${result.commandCount}`);
}

// Merge with existing whitelist
const mergeResult = await sanitizer.updateWhitelist(
  { newCommand: { allowed: true } },
  { merge: true }
);
// Now both 'git' and 'newCommand' are available

Load from External File

// Load from JSON file
const result = await sanitizer.loadWhitelistFromFile('/path/to/whitelist.json');

// Merge with existing whitelist
await sanitizer.loadWhitelistFromFile('/path/to/additional.json', { merge: true });

// Skip validation for trusted sources
await sanitizer.loadWhitelistFromFile('/path/to/trusted.json', { validate: false });

Example whitelist.json:

{
  "customCmd": {
    "allowed": true,
    "subcommands": ["run", "build", "test"],
    "maxArgs": 20
  },
  "anotherCmd": {
    "allowed": true,
    "allowArbitraryArgs": true
  }
}

Load from URL

// Load from remote URL
const result = await sanitizer.loadWhitelistFromUrl(
  'https://config.example.com/whitelist.json'
);

// With custom timeout
await sanitizer.loadWhitelistFromUrl(
  'https://config.example.com/whitelist.json',
  { timeout: 5000 }  // 5 second timeout
);

Generic Source Loading

import type { WhitelistSource } from 'ad-sdlc';

// Load from any source type
const source: WhitelistSource = {
  type: 'file',  // 'file' | 'url' | 'object'
  path: '/path/to/whitelist.json',
};

const result = await sanitizer.loadWhitelistFromSource(source, { merge: true });

Thread-Safe Operations

Whitelist updates are thread-safe with version tracking:

// Get current whitelist version
const version = sanitizer.getWhitelistVersion();
console.log(`Current version: ${version}`);

// Get a snapshot of the current whitelist
const snapshot = sanitizer.getWhitelistSnapshot();
console.log(`Version: ${snapshot.version}`);
console.log(`Timestamp: ${snapshot.timestamp}`);
console.log(`Commands: ${Object.keys(snapshot.config)}`);

// Concurrent update protection
const [result1, result2] = await Promise.all([
  sanitizer.updateWhitelist({ cmd1: { allowed: true } }),
  sanitizer.updateWhitelist({ cmd2: { allowed: true } }),
]);
// One will succeed, one will fail with "Another whitelist update is in progress"

Error Handling

import { WhitelistUpdateError } from 'ad-sdlc';

try {
  await sanitizer.loadWhitelistFromFile('/nonexistent/file.json');
} catch (error) {
  if (error instanceof WhitelistUpdateError) {
    console.error(`Update failed from ${error.source}: ${error.reason}`);
  }
}

// Validation errors are returned in the result
const result = await sanitizer.updateWhitelist({
  badCmd: { allowed: 'not-a-boolean' }
} as any);

if (!result.success) {
  console.error(`Validation error: ${result.error}`);
  // "'allowed' field for 'badCmd' must be a boolean"
}

SecureFileOps

Provides a centralized, secure wrapper for all file operations with automatic path validation.

The Problem

Direct file operations with user-provided paths can lead to path traversal attacks:

// DANGEROUS - Never do this!
const userPath = '../../../etc/passwd';
const content = fs.readFileSync(userPath);  // Path traversal vulnerability!

Solution: SecureFileOps

SecureFileOps wraps all file operations with automatic path validation:

import { createSecureFileOps, SecureFileOps } from 'ad-sdlc';

// Create instance with project root
const fileOps = createSecureFileOps({
  projectRoot: '/path/to/project',
});

// Safe file operations - validates path before executing
const content = await fileOps.readFile('src/config.json');  // Safe
const content2 = await fileOps.readFile('../../../etc/passwd');  // Throws PathTraversalError!

Basic Usage

import { createSecureFileOps, getSecureFileOps } from 'ad-sdlc';

// Using factory function with configuration
const fileOps = createSecureFileOps({
  projectRoot: process.cwd(),
  allowedExternalDirs: ['/tmp/allowed'],  // Optional: allow specific external directories
});

// Or using singleton (uses CWD as project root)
const defaultOps = getSecureFileOps();

// Read files safely
const content = await fileOps.readFile('src/index.ts');
const jsonData = await fileOps.readFile('config.json');

// Write files safely (auto-creates directories)
await fileOps.writeFile('dist/output.js', 'console.log("hello")');

// Append to files
await fileOps.appendFile('logs/app.log', 'New log entry\n');

// Create directories
await fileOps.mkdir('new-dir/nested');

// Check file existence
if (await fileOps.exists('package.json')) {
  // File exists
}

// Delete files
await fileOps.unlink('temp-file.txt');

// Rename/move files
await fileOps.rename('old-name.ts', 'new-name.ts');

// Copy files
await fileOps.copyFile('source.ts', 'backup/source.ts');

Synchronous Operations

All operations have synchronous versions:

const content = fileOps.readFileSync('config.json');
fileOps.writeFileSync('output.txt', 'content');
fileOps.mkdirSync('new-directory');
const exists = fileOps.existsSync('file.txt');
fileOps.unlinkSync('temp.txt');
fileOps.renameSync('old.ts', 'new.ts');
fileOps.copyFileSync('src.ts', 'dest.ts');

Path Validation

Validate paths explicitly:

// Get validated absolute path
const absolutePath = fileOps.validatePath('relative/path.ts');
// Returns: '/path/to/project/relative/path.ts'

// Path traversal attempts throw PathTraversalError
try {
  fileOps.validatePath('../../etc/passwd');
} catch (error) {
  if (error instanceof PathTraversalError) {
    console.error('Path traversal detected!');
  }
}

Configuration Options

const fileOps = createSecureFileOps({
  // Project root directory (all paths must resolve within this)
  projectRoot: '/path/to/project',

  // Optional: Allow access to specific external directories
  allowedExternalDirs: ['/tmp/shared', '/var/cache/app'],

  // Validate symlinks don't escape project (default: true)
  validateSymlinks: true,

  // Enable audit logging for file operations
  enableAuditLog: true,

  // Actor name for audit logging
  actor: 'my-agent',
});

Symlink Security

SecureFileOps validates that symbolic links do not escape the allowed directories:

const fileOps = createSecureFileOps({
  projectRoot: '/path/to/project',
  validateSymlinks: true,  // Default: true
});

// If 'link.txt' is a symlink pointing to '/etc/passwd'
// This will throw PathTraversalError
await fileOps.readFile('link.txt');
// Error: Symbolic link target escapes allowed directories

// Safe symlinks (target within project) work normally
await fileOps.readFile('internal-link.txt');  // Works fine

File Watching

SecureFileOps provides secure file watching with path validation and security filters:

import { createSecureFileOps } from 'ad-sdlc';

const fileOps = createSecureFileOps({
  projectRoot: '/path/to/project',
});

// Watch a directory for changes
const handle = fileOps.watch('src', (event) => {
  console.log(`File ${event.type}: ${event.path}`);
  // event.type: 'change' | 'add' | 'unlink' | 'error'
  // event.path: relative path from project root
  // event.absolutePath: full absolute path
  // event.timestamp: Date when the event occurred
});

// Stop watching
handle.close();

// Check if still active
if (handle.isActive()) {
  console.log('Still watching');
}

Watch Configuration

const handle = fileOps.watch('src', callback, {
  // Watch subdirectories recursively (default: true)
  recursive: true,

  // Debounce rapid changes in milliseconds (default: 100)
  debounceMs: 100,

  // Filter by file patterns
  patterns: {
    include: ['*.ts', '*.tsx'],  // Only watch TypeScript files
    exclude: ['*.test.ts'],      // Exclude test files
  },

  // Follow symbolic links (default: false for security)
  followSymlinks: false,

  // Validate symlink targets stay within security boundary (default: true)
  validateSymlinkTargets: true,

  // Enable audit logging (default: true)
  enableAuditLog: true,
});

Security Features

File watching includes built-in security protections:

  1. Path Validation: Only paths within the project root can be watched
  2. Security Boundary: Changed files outside allowed directories are filtered out
  3. Symlink Validation: Symlink targets are validated to prevent escaping boundaries
  4. Pattern Filtering: Only specified file types are reported
// Attempting to watch outside project root throws PathTraversalError
fileOps.watch('../../etc', callback);  // Throws!

// Watching a symlink that points outside project throws error
// if validateSymlinkTargets is true
fileOps.watch('bad-symlink', callback);  // Throws PathTraversalError!

Managing Watchers

// Get all active watchers
const watchers = fileOps.getActiveWatchers();
console.log(`${watchers.length} active watchers`);

// Stop all watchers
fileOps.unwatchAll();

// Stop a specific watcher by ID
fileOps.unwatch(handle.id);

PathResolver

Low-level path resolution with security validation.

Basic Usage

import { PathResolver } from 'ad-sdlc';

const resolver = new PathResolver({
  projectRoot: '/path/to/project',
});

// Resolve path safely
const resolved = resolver.resolve('src/index.ts');
console.log(resolved.absolutePath);     // '/path/to/project/src/index.ts'
console.log(resolved.relativePath);     // 'src/index.ts'
console.log(resolved.isWithinProject);  // true

// Path traversal throws error
resolver.resolve('../../../etc/passwd');  // Throws PathTraversalError

Configuration

const resolver = new PathResolver({
  projectRoot: '/project',
  allowedExternalDirs: ['/tmp'],  // Allow specific external paths
  validateSymlinks: true,          // Check symlink targets
});

Error Classes

All security errors extend SecurityError:

import {
  SecurityError,
  SecretNotFoundError,
  PathTraversalError,
  InvalidUrlError,
  ValidationError,
  RateLimitExceededError,
  CommandInjectionError,
  CommandNotAllowedError,
} from 'ad-sdlc';

try {
  // Security operation
} catch (error) {
  if (error instanceof SecurityError) {
    console.log(`Security error: ${error.code}`);
  }
}

Best Practices

  1. Never log secrets - Always use SecretManager.mask() before logging
  2. Validate all inputs - Use InputValidator for paths, URLs, and user input
  3. Audit sensitive operations - Log API key usage, file operations, and security events
  4. Use secure file handling - Always use SecureFileHandler for temporary files
  5. Implement rate limiting - Protect APIs from abuse with RateLimiter
  6. Prevent command injection - Always use CommandSanitizer for shell command execution
  7. Never use exec() with user input - Use execGit(), execGh(), or execFromString() instead
  8. Use SecureFileOps for all file operations - Prevents path traversal attacks by validating all paths against project root
  9. Configure project root - Always specify projectRoot when creating SecureFileOps or PathResolver instances

Security Scanning

The project includes automated security scanning:

  • npm audit - Dependency vulnerability scanning
  • CodeQL - Static code analysis
  • Gitleaks - Secret detection in commits
  • License checking - License compliance verification

See .github/workflows/security.yml for configuration.

There aren’t any published security advisories