The AD-SDLC security module provides essential security utilities for building secure agent-driven systems.
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
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';Manages secure access to API keys and secrets with automatic masking.
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');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 maskedconst secrets = new SecretManager({
envFilePath: '.env.local',
requiredSecrets: ['CLAUDE_API_KEY', 'GITHUB_TOKEN'],
throwOnMissing: true, // Throw if required secrets are missing
});Validates and sanitizes user inputs to prevent security vulnerabilities.
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!');
}
}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// 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
}Logs security-sensitive operations for compliance and debugging.
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',
});// 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',
});Track related operations:
const correlationId = audit.newCorrelationId();
// All subsequent logs include this correlation ID
// Or set a specific ID
audit.setCorrelationId('request-123');const entries = audit.getRecentEntries(100);
for (const entry of entries) {
console.log(`${entry.timestamp}: ${entry.type} - ${entry.result}`);
}Handles temporary files securely with automatic cleanup.
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');// 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');// 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);
}Prevents API abuse with configurable rate limiting.
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`);
}
}// 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();// 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();Provides safe shell command execution by preventing command injection attacks.
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 /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);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']);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);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 detectedFor 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'");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}"`);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) |
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');
}
}// 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);
}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]'CommandSanitizer supports runtime whitelist updates without restarting the application:
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 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 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
);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 });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"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"
}Provides a centralized, secure wrapper for all file operations with automatic path validation.
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!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!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');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');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!');
}
}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',
});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 fineSecureFileOps 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');
}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,
});File watching includes built-in security protections:
- Path Validation: Only paths within the project root can be watched
- Security Boundary: Changed files outside allowed directories are filtered out
- Symlink Validation: Symlink targets are validated to prevent escaping boundaries
- 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!// 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);Low-level path resolution with security validation.
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 PathTraversalErrorconst resolver = new PathResolver({
projectRoot: '/project',
allowedExternalDirs: ['/tmp'], // Allow specific external paths
validateSymlinks: true, // Check symlink targets
});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}`);
}
}- Never log secrets - Always use
SecretManager.mask()before logging - Validate all inputs - Use
InputValidatorfor paths, URLs, and user input - Audit sensitive operations - Log API key usage, file operations, and security events
- Use secure file handling - Always use
SecureFileHandlerfor temporary files - Implement rate limiting - Protect APIs from abuse with
RateLimiter - Prevent command injection - Always use
CommandSanitizerfor shell command execution - Never use exec() with user input - Use
execGit(),execGh(), orexecFromString()instead - Use SecureFileOps for all file operations - Prevents path traversal attacks by validating all paths against project root
- Configure project root - Always specify
projectRootwhen creatingSecureFileOpsorPathResolverinstances
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.