feat: AI Agent Security Firewall MVP - CLI monitoring, sandboxing, audit logging, 11 tests passing
This commit is contained in:
+72
@@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"watchPaths": [
|
||||||
|
"/home/node/.openclaw/workspace/builds/ai-agent-security-firewall"
|
||||||
|
],
|
||||||
|
"auditLogPath": "/home/node/.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
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.venv/
|
||||||
|
*.log
|
||||||
|
.verdict
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
coverage/
|
||||||
|
*.tsbuildinfo
|
||||||
Generated
+52
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"name": "ai-agent-security-firewall",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "ai-agent-security-firewall",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"aasf": "dist/cli.js",
|
||||||
|
"ai-agent-firewall": "dist/cli.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"typescript": "^5.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.19.43",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
|
||||||
|
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "ai-agent-security-firewall",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "CLI tool that monitors, sandboxes, and restricts AI coding agents' file system and network access",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"bin": {
|
||||||
|
"aasf": "dist/cli.js",
|
||||||
|
"ai-agent-firewall": "dist/cli.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"test": "node --test dist/**/*.test.js",
|
||||||
|
"start": "node dist/cli.js"
|
||||||
|
},
|
||||||
|
"keywords": ["security", "ai-agent", "sandbox", "monitor", "firewall"],
|
||||||
|
"author": "BunBun Labs",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"typescript": "^5.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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();
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user