feat: AI Agent Security Firewall MVP - CLI monitoring, sandboxing, audit logging, 11 tests passing

This commit is contained in:
Bun Bun
2026-06-15 12:24:35 +00:00
commit f1c4fe68e1
15 changed files with 1336 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
import { writeFileSync, appendFileSync, existsSync, mkdirSync, statSync, renameSync } from 'fs';
import { dirname } from 'path';
import type { AuditEvent, Config } from './types';
export class AuditLogger {
private config: Config;
private logPath: string;
constructor(config: Config) {
this.config = config;
this.logPath = config.auditLogPath;
mkdirSync(dirname(this.logPath), { recursive: true });
if (!existsSync(this.logPath)) {
writeFileSync(this.logPath, '', 'utf-8');
}
}
log(event: AuditEvent): void {
const line = JSON.stringify(event) + '\n';
appendFileSync(this.logPath, line, 'utf-8');
this.rotateIfNeeded();
}
logBulk(events: AuditEvent[]): void {
if (events.length === 0) return;
const lines = events.map(e => JSON.stringify(e)).join('\n') + '\n';
appendFileSync(this.logPath, lines, 'utf-8');
this.rotateIfNeeded();
}
readRecent(count: number = 100): AuditEvent[] {
if (!existsSync(this.logPath)) return [];
const data = require('fs').readFileSync(this.logPath, 'utf-8');
const lines: string[] = data.split('\n').filter(Boolean);
return lines.slice(-count).map((line: string): AuditEvent => JSON.parse(line)).reverse();
}
summary(): { total: number; violations: number; warnings: number; byType: Record<string, number> } {
if (!existsSync(this.logPath)) return { total: 0, violations: 0, warnings: 0, byType: {} };
const data = require('fs').readFileSync(this.logPath, 'utf-8');
const lines = data.split('\n').filter(Boolean);
const events = lines.map((line: string): AuditEvent => JSON.parse(line) as AuditEvent);
const summary = {
total: events.length,
violations: events.filter((e: AuditEvent) => e.severity === 'critical').length,
warnings: events.filter((e: AuditEvent) => e.severity === 'warning').length,
byType: {} as Record<string, number>
};
for (const event of events) {
summary.byType[event.type] = (summary.byType[event.type] || 0) + 1;
}
return summary;
}
private rotateIfNeeded(): void {
try {
const stats = statSync(this.logPath);
const maxBytes = this.config.maxLogSizeMB * 1024 * 1024;
if (stats.size > maxBytes) {
const rotated = this.logPath + '.1';
if (existsSync(rotated)) {
require('fs').unlinkSync(rotated);
}
renameSync(this.logPath, rotated);
writeFileSync(this.logPath, '', 'utf-8');
}
} catch (_e) {
// ignore rotation errors
}
}
}
+270
View File
@@ -0,0 +1,270 @@
#!/usr/bin/env node
import { readFileSync, existsSync, mkdirSync } from 'fs';
import { join, resolve } from 'path';
import { homedir } from 'os';
import { loadConfig, saveConfig, getDefaultConfig } from './config';
import { AuditLogger } from './audit';
import { FilesystemMonitor } from './fs-monitor';
import { NetworkMonitor } from './network-monitor';
import { RulesEngine, SandboxRunner, createLdPreloadLib } from './sandbox';
import type { Config } from './types';
const args = process.argv.slice(2);
const command = args[0];
function printHelp(): void {
console.log(`
AI Agent Security Firewall (aasf) — Monitor and sandbox AI coding agents
Usage:
aasf init Create default config in current directory
aasf monitor [paths...] Start monitoring filesystem and network
aasf run <command> [args...] Run a command inside the sandbox
aasf audit [count] Show recent audit events (default 100)
aasf summary Show audit summary
aasf status Show firewall status and rules
aasf rules List all active rules
aasf check <path> Check if a file access would be blocked
aasf build-ldpreload Build the LD_PRELOAD library (Linux only)
aasf help Show this help
Examples:
aasf init
aasf monitor ./src ./config
aasf run node my-ai-agent.js
aasf audit 50
aasf check ~/.ssh/id_rsa
`);
}
function printRules(config: Config): void {
console.log('\nActive Rules:');
console.log('─'.repeat(60));
for (const rule of config.rules) {
const actionColor = rule.action === 'block' ? 'BLOCK' : rule.action === 'flag' ? 'FLAG' : 'ALLOW';
console.log(` ${rule.id} [${actionColor}] ${rule.name}`);
console.log(` Type: ${rule.type}, Pattern: ${rule.pattern}`);
if (rule.description) console.log(` ${rule.description}`);
}
console.log('─'.repeat(60));
console.log(`Blocked hosts: ${config.blockedHosts.join(', ') || 'none'}`);
console.log(`Blocked file patterns: ${config.blockedFilePatterns.join(', ') || 'none'}`);
console.log(`Alert threshold: ${config.alertThreshold} violations`);
console.log();
}
function printStatus(config: Config): void {
console.log('\nFirewall Status:');
console.log('─'.repeat(60));
console.log(`Config loaded: ${config.auditLogPath}`);
console.log(`Watch paths: ${config.watchPaths.join(', ')}`);
console.log(`Network monitoring: ${config.enableNetworkMonitoring ? 'ON' : 'OFF'}`);
console.log(`Process monitoring: ${config.enableProcessMonitoring ? 'ON' : 'OFF'}`);
console.log(`Rules active: ${config.rules.length}`);
console.log('─'.repeat(60));
printRules(config);
}
async function handleMonitor(): Promise<void> {
const paths = args.slice(1).length > 0 ? args.slice(1).map(p => resolve(p)) : [process.cwd()];
const config = loadConfig();
config.watchPaths = paths;
const logger = new AuditLogger(config);
const fsMonitor = new FilesystemMonitor(logger, config);
const netMonitor = new NetworkMonitor(logger, config);
const rules = new RulesEngine(config, logger);
console.log('Starting AI Agent Security Firewall monitor...');
console.log(`Watching paths: ${paths.join(', ')}`);
fsMonitor.start();
if (config.enableNetworkMonitoring) {
netMonitor.start();
}
console.log('Press Ctrl+C to stop\n');
// Keep alive
const shutdown = () => {
console.log('\nShutting down monitor...');
fsMonitor.stop();
netMonitor.stop();
console.log('Monitor stopped.');
process.exit(0);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
// Keep alive
setInterval(() => {
if (rules.exceedsThreshold()) {
console.warn(`[ALERT] Violation threshold (${config.alertThreshold}) exceeded!`);
}
}, 5000);
}
async function handleRun(): Promise<void> {
if (args.length < 2) {
console.error('Usage: aasf run <command> [args...]');
process.exit(1);
}
const config = loadConfig();
const logger = new AuditLogger(config);
const runner = new SandboxRunner(config, logger);
const cmd = args[1];
const cmdArgs = args.slice(2);
console.log(`Running: ${cmd} ${cmdArgs.join(' ')}`);
console.log('Rules active:', config.rules.length);
const result = await runner.run(cmd, cmdArgs);
console.log(`\nCommand exited with code: ${result.exitCode}`);
console.log(`Duration: ${result.duration}ms`);
console.log(`Events logged: ${result.events.length}`);
if (result.violations.length > 0) {
console.log('\nViolations detected:');
for (const v of result.violations) {
console.log(` [${v.severity.toUpperCase()}] ${v.details}`);
}
}
process.exit(result.exitCode);
}
function handleAudit(): void {
const count = parseInt(args[1] || '100', 10);
const config = loadConfig();
const logger = new AuditLogger(config);
const events = logger.readRecent(count);
if (events.length === 0) {
console.log('No audit events found.');
return;
}
console.log(`\nLast ${events.length} audit events:`);
console.log('─'.repeat(80));
for (const event of events) {
const ts = new Date(event.timestamp).toLocaleTimeString();
const severity = event.severity.toUpperCase().padEnd(8);
console.log(`${ts} [${severity}] ${event.type.padEnd(12)} ${event.action.padEnd(6)} ${event.target}`);
if (event.details) console.log(`${event.details}`);
}
console.log('─'.repeat(80));
}
function handleSummary(): void {
const config = loadConfig();
const logger = new AuditLogger(config);
const summary = logger.summary();
console.log('\nAudit Summary:');
console.log('─'.repeat(40));
console.log(`Total events: ${summary.total}`);
console.log(`Violations: ${summary.violations}`);
console.log(`Warnings: ${summary.warnings}`);
console.log('\nBy type:');
for (const [type, count] of Object.entries(summary.byType)) {
console.log(` ${type}: ${count}`);
}
console.log('─'.repeat(40));
}
function handleCheck(): void {
if (args.length < 2) {
console.error('Usage: aasf check <path>');
process.exit(1);
}
const config = loadConfig();
const logger = new AuditLogger(config);
const rules = new RulesEngine(config, logger);
const path = resolve(args[1]);
const result = rules.checkFileAccess(path);
console.log(`\nChecking: ${path}`);
if (!result.allowed) {
console.log(`Result: BLOCKED by rule "${result.rule?.name}"`);
console.log(`Rule: ${result.rule?.id}${result.rule?.description || 'No description'}`);
} else if (result.flagged) {
console.log(`Result: FLAGGED by rule "${result.rule?.name}"`);
console.log(`Rule: ${result.rule?.id}${result.rule?.description || 'No description'}`);
} else {
console.log('Result: ALLOWED');
}
}
function handleInit(): void {
const configPath = join(process.cwd(), '.aasf.json');
if (existsSync(configPath)) {
console.log('Config already exists at .aasf.json');
return;
}
const config = getDefaultConfig();
saveConfig(config, configPath);
console.log(`Created default config at .aasf.json`);
console.log('Edit this file to customize rules and watch paths.');
}
function handleBuildLdPreload(): void {
const soPath = createLdPreloadLib();
if (soPath) {
console.log(`Built LD_PRELOAD library at: ${soPath}`);
console.log('Usage: LD_PRELOAD=/path/to/aafs_hook.so node your-ai-agent.js');
} else {
console.log('Failed to build LD_PRELOAD library. Ensure gcc and libc development headers are installed.');
console.log('This is an optional hardening layer. The firewall works without it.');
}
}
async function main(): Promise<void> {
try {
switch (command) {
case 'init':
handleInit();
break;
case 'monitor':
await handleMonitor();
break;
case 'run':
await handleRun();
break;
case 'audit':
handleAudit();
break;
case 'summary':
handleSummary();
break;
case 'status':
printStatus(loadConfig());
break;
case 'rules':
printRules(loadConfig());
break;
case 'check':
handleCheck();
break;
case 'build-ldpreload':
handleBuildLdPreload();
break;
case 'help':
case '--help':
case '-h':
default:
printHelp();
break;
}
} catch (e) {
console.error('Error:', e);
process.exit(1);
}
}
main();
+64
View File
@@ -0,0 +1,64 @@
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { homedir } from 'os';
import type { Config } from './types';
const DEFAULT_CONFIG: Config = {
watchPaths: [process.cwd()],
auditLogPath: join(homedir(), '.aasf', 'audit.log'),
rules: [
{ id: 'rule-001', name: 'Block SSH Keys', type: 'file', action: 'block', pattern: '**/.ssh/*', description: 'Prevent AI agents from reading SSH private keys' },
{ id: 'rule-002', name: 'Block .env Files', type: 'file', action: 'flag', pattern: '**/.env*', description: 'Flag access to environment files' },
{ id: 'rule-003', name: 'Block Credential Files', type: 'file', action: 'block', pattern: '**/*.{pem,key,p12,pfx}', description: 'Prevent reading of private keys and certificates' },
{ id: 'rule-004', name: 'Flag Suspicious Network', type: 'network', action: 'flag', pattern: 'pastebin.com', description: 'Flag connections to paste services' },
{ id: 'rule-005', name: 'Block Untrusted Host', type: 'network', action: 'block', pattern: 'requests.malicious.example.com', description: 'Block known malicious domains' },
{ id: 'rule-006', name: 'Flag Process Spawning', type: 'process', action: 'flag', pattern: 'curl|wget|nc|netcat', description: 'Flag network tool process spawning' }
],
maxLogSizeMB: 50,
enableNetworkMonitoring: true,
enableProcessMonitoring: true,
blockedHosts: ['pastebin.com', 'termbin.com', 'requestbin.net'],
blockedFilePatterns: ['**/.ssh/*', '**/.aws/*', '**/.config/gcloud/*', '**/*.pem', '**/*.key'],
alertThreshold: 5
};
export function loadConfig(): Config {
const configPaths = [
join(process.cwd(), '.aasf.json'),
join(process.cwd(), '.aasf.config.js'),
join(homedir(), '.aasf', 'config.json')
];
for (const path of configPaths) {
if (existsSync(path)) {
try {
if (path.endsWith('.json')) {
const raw = readFileSync(path, 'utf-8');
return mergeConfig(JSON.parse(raw));
}
} catch (e) {
console.warn(`Warning: Failed to parse config at ${path}: ${e}`);
}
}
}
return DEFAULT_CONFIG;
}
export function saveConfig(config: Config, path?: string): void {
const configPath = path || join(homedir(), '.aasf', 'config.json');
mkdirSync(dirname(configPath), { recursive: true });
writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
}
function mergeConfig(partial: Partial<Config>): Config {
return {
...DEFAULT_CONFIG,
...partial,
rules: partial.rules || DEFAULT_CONFIG.rules
};
}
export function getDefaultConfig(): Config {
return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
}
+87
View File
@@ -0,0 +1,87 @@
import { watch, existsSync, statSync, realpathSync } from 'fs';
import { resolve, sep } from 'path';
import type { AuditLogger } from './audit';
import type { Config, FileEvent, AuditEvent } from './types';
import { minimatch } from './minimatch';
export class FilesystemMonitor {
private watchers: ReturnType<typeof watch>[] = [];
private logger: AuditLogger;
private config: Config;
private active: boolean = false;
constructor(logger: AuditLogger, config: Config) {
this.logger = logger;
this.config = config;
}
start(): void {
if (this.active) return;
this.active = true;
for (const dir of this.config.watchPaths) {
if (!existsSync(dir)) {
console.warn(`Watch path does not exist: ${dir}`);
continue;
}
const watcher = watch(dir, { recursive: true }, (eventType, filename) => {
if (!filename || !this.active) return;
const fullPath = resolve(dir, filename);
this.handleEvent(eventType as string, fullPath);
});
this.watchers.push(watcher);
}
}
stop(): void {
this.active = false;
for (const w of this.watchers) {
w.close();
}
this.watchers = [];
}
private handleEvent(eventType: string, fullPath: string): void {
const event: FileEvent = {
path: fullPath,
eventType: this.mapEventType(eventType),
timestamp: new Date().toISOString()
};
const matchedRule = this.matchFileRule(fullPath);
const severity = matchedRule ? (matchedRule.action === 'block' ? 'critical' : 'warning') : 'info';
const action = matchedRule ? matchedRule.action : 'allow';
const auditEvent: AuditEvent = {
timestamp: new Date().toISOString(),
type: 'file',
severity,
action: action,
target: fullPath,
details: `File ${event.eventType}: ${fullPath}`,
ruleId: matchedRule?.id
};
this.logger.log(auditEvent);
if (matchedRule && matchedRule.action === 'block') {
console.error(`[BLOCKED] ${matchedRule.name}: ${fullPath}`);
}
}
private mapEventType(raw: string): 'create' | 'modify' | 'delete' | 'rename' {
if (raw === 'rename') return 'rename';
if (raw === 'change') return 'modify';
return 'create';
}
private matchFileRule(path: string): { id: string; name: string; action: string } | null {
for (const rule of this.config.rules) {
if (rule.type !== 'file') continue;
if (minimatch(path, rule.pattern)) {
return { id: rule.id, name: rule.name, action: rule.action };
}
}
return null;
}
}
+23
View File
@@ -0,0 +1,23 @@
/**
* Minimal glob matcher without external deps
*/
export function minimatch(path: string, pattern: string): boolean {
// Convert **/ and glob patterns to regex
const regexPattern = pattern
.replace(/\*\*/g, '<<<DOUBLESTAR>>>')
.replace(/\*/g, '[^/]*')
.replace(/<<<DOUBLESTAR>>>/g, '.*');
const parts = path.split('/');
const filename = parts[parts.length - 1];
const basename = filename;
// Check full path match
const re = new RegExp(regexPattern.replace(/\?/g, '.'));
if (re.test(path)) return true;
// Check basename match for patterns like *.pem
if (re.test(basename)) return true;
return false;
}
+133
View File
@@ -0,0 +1,133 @@
import { readFileSync, existsSync } from 'fs';
import type { AuditLogger } from './audit';
import type { Config, NetworkConnection, AuditEvent } from './types';
export class NetworkMonitor {
private logger: AuditLogger;
private config: Config;
private interval: ReturnType<typeof setInterval> | null = null;
constructor(logger: AuditLogger, config: Config) {
this.logger = logger;
this.config = config;
}
start(intervalMs: number = 5000): void {
if (this.interval) return;
this.interval = setInterval(() => this.scan(), intervalMs);
this.scan();
}
stop(): void {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
}
scan(): void {
if (!existsSync('/proc/net/tcp')) {
// On systems without /proc/net/tcp, try to at least detect via netstat if available
return;
}
const connections = this.parseTcpConnections();
for (const conn of connections) {
const matchedRule = this.matchNetworkRule(conn);
if (matchedRule) {
const severity = matchedRule.action === 'block' ? 'critical' : 'warning';
const event: AuditEvent = {
timestamp: new Date().toISOString(),
type: 'network',
severity,
action: matchedRule.action,
target: `${conn.remoteAddress}:${conn.remotePort}`,
details: `TCP connection to ${conn.remoteAddress}:${conn.remotePort} (state: ${conn.state})`,
ruleId: matchedRule.id
};
this.logger.log(event);
if (matchedRule.action === 'block') {
console.error(`[BLOCKED] ${matchedRule.name}: ${conn.remoteAddress}:${conn.remotePort}`);
}
}
}
}
parseTcpConnections(): NetworkConnection[] {
try {
const data = readFileSync('/proc/net/tcp', 'utf-8');
const lines = data.split('\n').slice(1); // skip header
const connections: NetworkConnection[] = [];
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length < 10) continue;
const localAddr = parts[1];
const remAddr = parts[2];
const state = parts[3];
const inode = parts[9];
const uid = parseInt(parts[7], 10);
const [localHex, localPort] = localAddr.split(':');
const [remHex, remPort] = remAddr.split(':');
connections.push({
localAddress: this.hexToIp(localHex),
localPort: parseInt(localPort, 16).toString(),
remoteAddress: this.hexToIp(remHex),
remotePort: parseInt(remPort, 16).toString(),
state: this.stateToString(state),
inode,
uid
});
}
return connections;
} catch (e) {
return [];
}
}
private hexToIp(hex: string): string {
if (!hex) return '';
const parts = hex.match(/../g);
if (!parts || parts.length !== 4) return '';
return parts.reverse().map(p => parseInt(p, 16)).join('.');
}
private stateToString(state: string): string {
const states: Record<string, string> = {
'01': 'ESTABLISHED',
'02': 'SYN_SENT',
'03': 'SYN_RECV',
'04': 'FIN_WAIT1',
'05': 'FIN_WAIT2',
'06': 'TIME_WAIT',
'07': 'CLOSE',
'08': 'CLOSE_WAIT',
'09': 'LAST_ACK',
'0A': 'LISTEN',
'0B': 'CLOSING'
};
return states[state] || state;
}
private matchNetworkRule(conn: NetworkConnection): { id: string; name: string; action: string } | null {
for (const rule of this.config.rules) {
if (rule.type !== 'network') continue;
// Check if remote address matches pattern
if (rule.pattern === conn.remoteAddress || conn.remoteAddress.includes(rule.pattern)) {
return { id: rule.id, name: rule.name, action: rule.action };
}
// Check blocked hosts list too
if (this.config.blockedHosts.includes(conn.remoteAddress)) {
return { id: 'config-block', name: 'Config Blocked Host', action: 'block' };
}
// Check if state is ESTABLISHED and host is suspicious
for (const host of this.config.blockedHosts) {
if (conn.remoteAddress.includes(host)) {
return { id: 'config-block', name: 'Config Blocked Host', action: 'block' };
}
}
}
return null;
}
}
+296
View File
@@ -0,0 +1,296 @@
import { spawn, execSync } from 'child_process';
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from 'fs';
import { join, dirname } from 'path';
import { homedir } from 'os';
import { AuditLogger } from './audit';
import type { Config, AuditEvent, SandboxResult, Rule } from './types';
import { minimatch } from './minimatch';
export class RulesEngine {
private config: Config;
private logger: AuditLogger;
private violationCount: number = 0;
constructor(config: Config, logger: AuditLogger) {
this.config = config;
this.logger = logger;
}
checkFileAccess(path: string): { allowed: boolean; flagged: boolean; rule?: Rule } {
for (const rule of this.config.rules) {
if (rule.type !== 'file') continue;
if (minimatch(path, rule.pattern)) {
if (rule.action === 'block') {
this.logViolation(rule, path, 'file');
return { allowed: false, flagged: true, rule };
}
if (rule.action === 'flag') {
this.logViolation(rule, path, 'file');
return { allowed: true, flagged: true, rule };
}
}
}
return { allowed: true, flagged: false };
}
checkNetwork(host: string): { allowed: boolean; flagged: boolean; rule?: Rule } {
for (const rule of this.config.rules) {
if (rule.type !== 'network') continue;
if (host.includes(rule.pattern) || rule.pattern === host) {
if (rule.action === 'block') {
this.logViolation(rule, host, 'network');
return { allowed: false, flagged: true, rule };
}
if (rule.action === 'flag') {
this.logViolation(rule, host, 'network');
return { allowed: true, flagged: true, rule };
}
}
}
// Check config-level blocked hosts
for (const blocked of this.config.blockedHosts) {
if (host.includes(blocked)) {
const rule: Rule = { id: 'config-block', name: 'Blocked Host', type: 'network', action: 'block', pattern: blocked };
this.logViolation(rule, host, 'network');
return { allowed: false, flagged: true, rule };
}
}
return { allowed: true, flagged: false };
}
checkProcess(command: string): { allowed: boolean; flagged: boolean; rule?: Rule } {
for (const rule of this.config.rules) {
if (rule.type !== 'process') continue;
const pattern = rule.pattern.replace(/\|/g, '|');
const parts = pattern.split('|');
for (const part of parts) {
if (command.includes(part.trim())) {
if (rule.action === 'block') {
this.logViolation(rule, command, 'process');
return { allowed: false, flagged: true, rule };
}
if (rule.action === 'flag') {
this.logViolation(rule, command, 'process');
return { allowed: true, flagged: true, rule };
}
}
}
}
return { allowed: true, flagged: false };
}
getViolationCount(): number {
return this.violationCount;
}
exceedsThreshold(): boolean {
return this.violationCount >= this.config.alertThreshold;
}
private logViolation(rule: Rule, target: string, type: 'file' | 'network' | 'process'): void {
this.violationCount++;
const event: AuditEvent = {
timestamp: new Date().toISOString(),
type: 'rule_violation',
severity: rule.action === 'block' ? 'critical' : 'warning',
action: rule.action,
target,
details: `Rule "${rule.name}" triggered for ${type}: ${target}`,
ruleId: rule.id
};
this.logger.log(event);
}
}
export class SandboxRunner {
private logger: AuditLogger;
private config: Config;
private rules: RulesEngine;
constructor(config: Config, logger: AuditLogger) {
this.config = config;
this.logger = logger;
this.rules = new RulesEngine(config, logger);
}
run(command: string, args: string[]): Promise<SandboxResult> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const events: AuditEvent[] = [];
const violations: AuditEvent[] = [];
// Pre-check: flag command itself
const processCheck = this.rules.checkProcess(command + ' ' + args.join(' '));
if (!processCheck.allowed) {
const event: AuditEvent = {
timestamp: new Date().toISOString(),
type: 'rule_violation',
severity: 'critical',
action: 'block',
target: command,
details: `Command blocked by rule: ${processCheck.rule?.name || 'unknown'}`
};
events.push(event);
violations.push(event);
this.logger.log(event);
return resolve({
command: command + ' ' + args.join(' '),
exitCode: 1,
events,
violations,
duration: 0
});
}
if (processCheck.flagged) {
const event: AuditEvent = {
timestamp: new Date().toISOString(),
type: 'process',
severity: 'warning',
action: 'flag',
target: command,
details: `Command flagged by rule: ${processCheck.rule?.name || 'unknown'}`
};
events.push(event);
this.logger.log(event);
}
const child = spawn(command, args, {
shell: false,
env: { ...process.env, AASF_SANDBOXED: '1' },
cwd: process.cwd()
});
child.stdout.on('data', (data) => {
// Monitor stdout for suspicious patterns (e.g., API keys, tokens)
const text = data.toString();
this.detectLeaks(text, events);
});
child.stderr.on('data', (data) => {
const text = data.toString();
this.detectLeaks(text, events);
});
child.on('exit', (code) => {
const duration = Date.now() - startTime;
this.logger.logBulk(events);
resolve({
command: command + ' ' + args.join(' '),
exitCode: code || 0,
events,
violations: events.filter(e => e.severity === 'critical'),
duration
});
});
child.on('error', (err) => {
const duration = Date.now() - startTime;
const event: AuditEvent = {
timestamp: new Date().toISOString(),
type: 'process',
severity: 'warning',
action: 'error',
target: command,
details: `Spawn error: ${err.message}`
};
events.push(event);
this.logger.logBulk(events);
resolve({
command: command + ' ' + args.join(' '),
exitCode: 1,
events,
violations: events.filter(e => e.severity === 'critical'),
duration
});
});
});
}
private detectLeaks(text: string, events: AuditEvent[]): void {
const leakPatterns = [
{ name: 'AWS Access Key', pattern: /AKIA[0-9A-Z]{16}/g },
{ name: 'Private Key Block', pattern: /-----BEGIN (RSA |DSA |EC |OPENSSH )?PRIVATE KEY-----/g },
{ name: 'API Key Pattern', pattern: /['"\s](sk-[a-zA-Z0-9]{20,})['"\s]/g },
{ name: 'GitHub Token', pattern: /gh[pousr]_[a-zA-Z0-9]{36}/g },
{ name: 'Generic Secret', pattern: /['"\s]([a-zA-Z0-9]{32,64})['"\s]/g }
];
for (const { name, pattern } of leakPatterns) {
const matches = text.match(pattern);
if (matches) {
events.push({
timestamp: new Date().toISOString(),
type: 'process',
severity: 'warning',
action: 'flag',
target: 'stdout/stderr',
details: `Potential ${name} detected in output`
});
}
}
}
}
export function generateLdPreloadScript(): string {
// Generate a C LD_PRELOAD library that intercepts file system calls
const c = `
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
static const char *BLOCKED_PATTERNS[] = {
"/.ssh/", "/.aws/", ".env", ".pem", ".key", ".p12", ".pfx", NULL
};
static int is_blocked(const char *path) {
if (!path) return 0;
for (int i = 0; BLOCKED_PATTERNS[i]; i++) {
if (strstr(path, BLOCKED_PATTERNS[i])) return 1;
}
return 0;
}
static int (*real_open)(const char *, int, ...) = NULL;
static int (*real_open64)(const char *, int, ...) = NULL;
static int (*real_fopen)(const char *, const char *) = NULL;
int open(const char *pathname, int flags, ...) {
if (!real_open) real_open = dlsym(RTLD_NEXT, "open");
if (is_blocked(pathname)) {
fprintf(stderr, "[AASF BLOCKED] open: %s\\n", pathname);
return -1;
}
return real_open(pathname, flags);
}
int open64(const char *pathname, int flags, ...) {
if (!real_open64) real_open64 = dlsym(RTLD_NEXT, "open64");
if (is_blocked(pathname)) {
fprintf(stderr, "[AASF BLOCKED] open64: %s\\n", pathname);
return -1;
}
return real_open64(pathname, flags);
}
`;
return c;
}
export function createLdPreloadLib(): string | null {
try {
const libDir = join(homedir(), '.aasf', 'lib');
mkdirSync(libDir, { recursive: true });
const cPath = join(libDir, 'aafs_hook.c');
const soPath = join(libDir, 'aafs_hook.so');
writeFileSync(cPath, generateLdPreloadScript(), 'utf-8');
execSync(`gcc -shared -fPIC -o ${soPath} ${cPath} -ldl`, { stdio: 'ignore' });
return soPath;
} catch (e) {
return null;
}
}
+128
View File
@@ -0,0 +1,128 @@
// @ts-nocheck
const { describe, it, beforeEach } = require('node:test');
const assert = require('node:assert');
const { loadConfig, getDefaultConfig, saveConfig } = require('../config');
const { AuditLogger } = require('../audit');
const { RulesEngine } = require('../sandbox');
const { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } = require('fs');
const { join } = require('path');
const { tmpdir } = require('os');
describe('config', () => {
it('returns default config', () => {
const config = getDefaultConfig();
assert.ok(config.watchPaths);
assert.ok(config.rules);
assert.strictEqual(config.rules.length > 0, true);
assert.ok(config.auditLogPath);
});
it('loads config from file', () => {
const tmpDir = join(tmpdir(), 'aasf-test-' + Date.now());
mkdirSync(tmpDir, { recursive: true });
const configPath = join(tmpDir, '.aasf.json');
const config = { ...getDefaultConfig(), watchPaths: ['/test/path'] };
saveConfig(config, configPath);
assert.ok(existsSync(configPath));
const loaded = JSON.parse(readFileSync(configPath, 'utf-8'));
assert.deepStrictEqual(loaded.watchPaths, ['/test/path']);
rmSync(tmpDir, { recursive: true, force: true });
});
});
describe('audit logger', () => {
it('logs and reads events', () => {
const tmpDir = join(tmpdir(), 'aasf-audit-test-' + Date.now());
mkdirSync(tmpDir, { recursive: true });
const config = getDefaultConfig();
config.auditLogPath = join(tmpDir, 'audit.log');
const logger = new AuditLogger(config);
const event = {
timestamp: new Date().toISOString(),
type: 'file',
severity: 'info',
action: 'allow',
target: '/test/file.txt',
details: 'Test event'
};
logger.log(event);
const events = logger.readRecent(10);
assert.strictEqual(events.length, 1);
assert.strictEqual(events[0].target, '/test/file.txt');
rmSync(tmpDir, { recursive: true, force: true });
});
it('returns summary', () => {
const tmpDir = join(tmpdir(), 'aasf-audit-test-' + Date.now());
mkdirSync(tmpDir, { recursive: true });
const config = getDefaultConfig();
config.auditLogPath = join(tmpDir, 'audit.log');
const logger = new AuditLogger(config);
logger.log({ timestamp: new Date().toISOString(), type: 'file', severity: 'info', action: 'allow', target: 'a', details: 'd' });
logger.log({ timestamp: new Date().toISOString(), type: 'file', severity: 'critical', action: 'block', target: 'b', details: 'd' });
const summary = logger.summary();
assert.strictEqual(summary.total, 2);
assert.strictEqual(summary.violations, 1);
rmSync(tmpDir, { recursive: true, force: true });
});
});
describe('rules engine', () => {
it('blocks SSH key access', () => {
const tmpDir = join(tmpdir(), 'aasf-rules-test-' + Date.now());
mkdirSync(tmpDir, { recursive: true });
const config = getDefaultConfig();
config.auditLogPath = join(tmpDir, 'audit.log');
const logger = new AuditLogger(config);
const rules = new RulesEngine(config, logger);
const result = rules.checkFileAccess('/home/user/.ssh/id_rsa');
assert.strictEqual(result.allowed, false);
assert.ok(result.flagged);
assert.ok(result.rule);
rmSync(tmpDir, { recursive: true, force: true });
});
it('allows benign file access', () => {
const tmpDir = join(tmpdir(), 'aasf-rules-test-' + Date.now());
mkdirSync(tmpDir, { recursive: true });
const config = getDefaultConfig();
config.auditLogPath = join(tmpDir, 'audit.log');
const logger = new AuditLogger(config);
const rules = new RulesEngine(config, logger);
const result = rules.checkFileAccess('/home/user/project/readme.md');
assert.strictEqual(result.allowed, true);
assert.strictEqual(result.flagged, false);
rmSync(tmpDir, { recursive: true, force: true });
});
it('flags .env access', () => {
const tmpDir = join(tmpdir(), 'aasf-rules-test-' + Date.now());
mkdirSync(tmpDir, { recursive: true });
const config = getDefaultConfig();
config.auditLogPath = join(tmpDir, 'audit.log');
const logger = new AuditLogger(config);
const rules = new RulesEngine(config, logger);
const result = rules.checkFileAccess('/home/user/project/.env');
assert.strictEqual(result.allowed, true);
assert.strictEqual(result.flagged, true);
rmSync(tmpDir, { recursive: true, force: true });
});
});
+25
View File
@@ -0,0 +1,25 @@
// @ts-nocheck
const { describe, it } = require('node:test');
const assert = require('node:assert');
const { minimatch } = require('../minimatch');
describe('minimatch', () => {
it('matches exact paths', () => {
assert.strictEqual(minimatch('/home/user/.ssh/id_rsa', '/home/user/.ssh/id_rsa'), true);
});
it('matches glob patterns', () => {
assert.strictEqual(minimatch('/home/user/.ssh/id_rsa', '**/.ssh/*'), true);
assert.strictEqual(minimatch('/home/user/.aws/credentials', '**/.aws/*'), true);
});
it('matches file extensions', () => {
assert.strictEqual(minimatch('/path/to/key.pem', '*.pem'), true);
assert.strictEqual(minimatch('/path/to/secret.key', '*.key'), true);
});
it('does not match unrelated paths', () => {
assert.strictEqual(minimatch('/home/user/readme.txt', '**/.ssh/*'), false);
assert.strictEqual(minimatch('/path/to/file.js', '*.pem'), false);
});
});
+55
View File
@@ -0,0 +1,55 @@
export interface Rule {
id: string;
name: string;
type: 'file' | 'network' | 'process';
action: 'allow' | 'block' | 'flag';
pattern: string;
description?: string;
}
export interface Config {
watchPaths: string[];
auditLogPath: string;
rules: Rule[];
maxLogSizeMB: number;
enableNetworkMonitoring: boolean;
enableProcessMonitoring: boolean;
blockedHosts: string[];
blockedFilePatterns: string[];
alertThreshold: number;
}
export interface AuditEvent {
timestamp: string;
type: 'file' | 'network' | 'process' | 'rule_violation';
severity: 'info' | 'warning' | 'critical';
action: string;
target: string;
details: string;
pid?: number;
ruleId?: string;
}
export interface NetworkConnection {
localAddress: string;
localPort: string;
remoteAddress: string;
remotePort: string;
state: string;
inode: string;
uid: number;
}
export interface FileEvent {
path: string;
eventType: 'create' | 'modify' | 'delete' | 'rename';
timestamp: string;
}
export interface SandboxResult {
command: string;
exitCode: number;
events: AuditEvent[];
violations: AuditEvent[];
duration: number;
}