Files

506 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* security-audit.cjs - Comprehensive security scanner for Clawdbot
* Usage: node audit.js [--full] [--json] [--credentials] [--ports] [--configs] [--permissions] [--docker]
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
// Configuration
const CLAWDBOT_DIR = '/root/clawd';
const CONFIG_DIR = '/root/clawd/skills/.env';
const DOCKER_DIR = '/root/clawd';
// Results collection
const findings = [];
let checkCount = 0;
let criticalCount = 0;
let highCount = 0;
// Helper functions
function log(level, category, message, details = null) {
const emoji = {
CRITICAL: '🔴',
HIGH: '🟠',
MEDIUM: '🟡',
LOW: '🟢',
INFO: '🔵'
};
findings.push({
level,
category,
message,
details,
timestamp: new Date().toISOString()
});
checkCount++;
if (level === 'CRITICAL') criticalCount++;
if (level === 'HIGH') highCount++;
}
function checkFileExists(filePath) {
try {
return fs.existsSync(filePath);
} catch {
return false;
}
}
function scanFileForPatterns(filePath, patterns, category) {
if (!checkFileExists(filePath)) return;
try {
const content = fs.readFileSync(filePath, 'utf8');
for (const pattern of patterns) {
if (pattern.regex.test(content)) {
log(pattern.level, category, pattern.message, {
file: filePath,
match: pattern.match
});
}
}
} catch (e) {
// Ignore unreadable files
}
}
function getFilesRecursively(dir, extensions = ['.js', '.ts', '.json', '.env', '.md', '.yml', '.yaml']) {
const files = [];
function traverse(currentDir) {
try {
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
if (!entry.name.startsWith('.') && !entry.name.includes('node_modules')) {
traverse(fullPath);
}
} else if (extensions.some(ext => entry.name.endsWith(ext))) {
files.push(fullPath);
}
}
} catch {
// Ignore inaccessible directories
}
}
traverse(dir);
return files;
}
// === CHECKS ===
function checkCredentials() {
log('INFO', 'CREDENTIALS', 'Starting credential scan...');
const credentialPatterns = [
{
level: 'CRITICAL',
message: 'Potential API key found in file',
regex: /api[_-]?key\s*[:=]\s*['"'][a-zA-Z0-9]{20,}['"']/gi,
match: 'API key pattern'
},
{
level: 'CRITICAL',
message: 'Potential secret token found',
regex: /(secret|token|auth)[_-]?key\s*[:=]\s*['"'][a-zA-Z0-9_\-]{30,}['"']/gi,
match: 'Secret pattern'
},
{
level: 'HIGH',
message: 'Hardcoded password found',
regex: /password\s*[:=]\s*['"'][^'"']{8,}['"']/gi,
match: 'Password pattern'
},
{
level: 'HIGH',
message: 'Private key detected',
regex: /-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,
match: 'Private key'
},
{
level: 'MEDIUM',
message: 'URL with credentials found',
regex: /https?:\/\/[^:]+:[^@]+@/g,
match: 'URL with credentials'
}
];
// Scan key files
const keyFiles = [
CONFIG_DIR,
path.join(CLAWDBOT_DIR, 'skills/.env'),
path.join(CLAWDBOT_DIR, '.env'),
path.join(CLAWDBOT_DIR, 'config.json')
];
for (const file of keyFiles) {
scanFileForPatterns(file, credentialPatterns, 'CREDENTIALS');
}
// Scan all code files
const codeFiles = getFilesRecursively(CLAWDBOT_DIR);
for (const file of codeFiles) {
if (file.includes('node_modules') || file.includes('.git')) continue;
scanFileForPatterns(file, credentialPatterns.filter(p => p.level !== 'CRITICAL'), 'CREDENTIALS');
}
log('INFO', 'CREDENTIALS', `Scanned ${codeFiles.length} files`);
}
function checkPorts() {
log('INFO', 'PORTS', 'Checking for open ports...');
try {
// Check if ss or netstat is available
const ssResult = execSync('ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null || echo "not available"',
{ encoding: 'utf8', timeout: 5000 });
const ports = [];
const lines = ssResult.split('\n');
for (const line of lines) {
const portMatch = line.match(/:(\d+)\s/);
if (portMatch) {
const port = parseInt(portMatch[1]);
if (port > 1024 && !ports.includes(port)) {
ports.push(port);
}
}
}
if (ports.length > 0) {
log('MEDIUM', 'PORTS', `Found ${ports.length} open ports`, { ports });
} else {
log('INFO', 'PORTS', 'No unexpected open ports detected');
}
} catch {
log('LOW', 'PORTS', 'Could not scan ports (tool not available)');
}
}
function checkConfigs() {
log('INFO', 'CONFIGS', 'Validating configuration security...');
// Check for .env file
if (!checkFileExists(CONFIG_DIR)) {
log('HIGH', 'CONFIGS', 'No .env file found - credentials may not be configured');
return;
}
try {
const envContent = fs.readFileSync(CONFIG_DIR, 'utf8');
// Check for rate limiting config
if (!envContent.includes('RATE_LIMIT')) {
log('MEDIUM', 'CONFIGS', 'No RATE_LIMIT configuration found');
}
// Check for auth settings
if (!envContent.includes('AUTH_') && !envContent.includes('API_KEY')) {
log('HIGH', 'CONFIGS', 'No authentication configuration detected');
}
// Check for log level
if (envContent.includes('LOG_LEVEL=debug') || envContent.includes('LOG_LEVEL=DEBUG')) {
log('MEDIUM', 'CONFIGS', 'Debug logging enabled - may expose sensitive data');
}
// Check for CORS
if (envContent.includes('CORS_ORIGIN=*') || envContent.includes('CORS_ALLOW_ALL=true')) {
log('HIGH', 'CONFIGS', 'CORS configured to allow all origins');
}
} catch (e) {
log('LOW', 'CONFIGS', 'Could not read configuration file');
}
}
function checkPermissions() {
log('INFO', 'PERMISSIONS', 'Checking file permissions...');
const sensitivePatterns = [
{ pattern: /\.env$/, level: 'CRITICAL', message: 'World-readable .env file' },
{ pattern: /\.json$/, level: 'HIGH', message: 'World-readable JSON config' },
{ pattern: /\.key$/, level: 'CRITICAL', message: 'World-readable key file' },
{ pattern: /\.pem$/, level: 'CRITICAL', message: 'World-readable PEM file' }
];
const files = getFilesRecursively(CLAWDBOT_DIR);
for (const file of files) {
try {
const stats = fs.statSync(file);
const mode = stats.mode & 0o777;
// Check if world-readable
if ((mode & 0o004) !== 0) {
for (const sp of sensitivePatterns) {
if (sp.pattern.test(file)) {
log(sp.level, 'PERMISSIONS', sp.message, { file, mode: mode.toString(8) });
}
}
}
// Check if executable by all
if ((mode & 0o001) !== 0 && file.endsWith('.js')) {
log('MEDIUM', 'PERMISSIONS', `Executable JS file: ${path.basename(file)}`);
}
} catch {
// Ignore inaccessible files
}
}
}
function checkDocker() {
log('INFO', 'DOCKER', 'Checking Docker security...');
const dockerFile = path.join(CLAWDBOT_DIR, 'Dockerfile');
if (!checkFileExists(dockerFile)) {
log('INFO', 'DOCKER', 'No Dockerfile found - skipping Docker checks');
return;
}
try {
const dockerContent = fs.readFileSync(dockerFile, 'utf8');
if (dockerContent.includes('USER root') || !dockerContent.includes('USER ')) {
log('HIGH', 'DOCKER', 'Container may run as root user');
}
if (dockerContent.includes('--privileged')) {
log('CRITICAL', 'DOCKER', 'Container has privileged mode enabled');
}
if (!dockerContent.includes('HEALTHCHECK')) {
log('LOW', 'DOCKER', 'No HEALTHCHECK instruction found');
}
if (dockerContent.includes(':latest') && !dockerContent.includes('BUILDARG')) {
log('MEDIUM', 'DOCKER', 'Using floating tag :latest - consider specific version');
}
} catch (e) {
log('LOW', 'DOCKER', 'Could not analyze Dockerfile');
}
}
function checkGit() {
log('INFO', 'GIT', 'Checking for exposed Git information...');
const gitDir = path.join(CLAWDBOT_DIR, '.git');
if (checkFileExists(gitDir)) {
log('MEDIUM', 'GIT', '.git directory exists - ensure it is not web-accessible');
}
const gitIgnore = path.join(CLAWDBOT_DIR, '.gitignore');
if (!checkFileExists(gitIgnore)) {
log('LOW', 'GIT', 'No .gitignore file found');
}
}
function checkRecentCommits() {
log('INFO', 'HISTORY', 'Checking for credential exposure in recent commits...');
try {
const logOutput = execSync('git log --oneline -20 2>/dev/null || echo "not a git repo"',
{ encoding: 'utf8', timeout: 5000 });
// Check for secrets in commit messages (paranoid check)
if (/secret|token|password|key|auth/i.test(logOutput)) {
log('LOW', 'HISTORY', 'Recent commits contain security-related keywords in messages');
}
} catch {
log('INFO', 'HISTORY', 'Not a Git repository or Git not available');
}
}
// === MAIN ===
async function runAudit(options = {}) {
const { full = false, json = false, credentials = false, ports = false,
configs = false, permissions = false, docker = false } = options;
const runAll = full || (!credentials && !ports && !configs && !permissions && !docker);
console.log('\n╔════════════════════════════════════════════════════════════╗');
console.log('║ CLAWDBOT SECURITY AUDIT v1.0 ║');
console.log('╚════════════════════════════════════════════════════════════╝\n');
const startTime = Date.now();
if (runAll || credentials) checkCredentials();
if (runAll || ports) checkPorts();
if (runAll || configs) checkConfigs();
if (runAll || permissions) checkPermissions();
if (runAll || docker) checkDocker();
checkGit();
checkRecentCommits();
const duration = Date.now() - startTime;
// Summary
console.log('\n╔════════════════════════════════════════════════════════════╗');
console.log('║ AUDIT SUMMARY ║');
console.log('╚════════════════════════════════════════════════════════════╝\n');
console.log(`Checks performed: ${checkCount}`);
console.log(`🔴 Critical: ${criticalCount}`);
console.log(`🟠 High: ${highCount}`);
console.log(`Total findings: ${findings.length}`);
console.log(`Duration: ${duration}ms\n`);
// Critical issues first
const criticalFindings = findings.filter(f => f.level === 'CRITICAL');
if (criticalFindings.length > 0) {
console.log('🔴 CRITICAL ISSUES (Immediate action required):');
for (const f of criticalFindings) {
console.log(`${f.message}`);
if (f.details?.file) console.log(` File: ${f.details.file}`);
}
console.log('');
}
if (json) {
console.log('\n=== JSON REPORT ===');
console.log(JSON.stringify({
summary: {
checks: checkCount,
critical: criticalCount,
high: highCount,
total: findings.length,
duration_ms: duration,
timestamp: new Date().toISOString()
},
findings
}, null, 2));
}
// Recommendation
if (criticalCount > 0) {
console.log('\n⚠ CRITICAL ISSUES FOUND - Do not deploy until fixed!');
process.exitCode = 1;
} else if (highCount > 0) {
console.log('\n⚠ High-risk issues found - Review recommended before deployment.');
} else {
console.log('\n✅ No critical issues found. Security posture looks reasonable.');
}
return { findings, criticalCount, highCount, checkCount };
}
// Auto-fix function
async function runAutoFix() {
console.log('\n╔════════════════════════════════════════════════════════════╗');
console.log('║ AUTO-FIX MODE ║');
console.log('╚════════════════════════════════════════════════════════════╝\n');
let fixedCount = 0;
// Fix 1: Secure .env file
const envFile = '/root/clawd/skills/.env';
if (checkFileExists(envFile)) {
try {
const stats = fs.statSync(envFile);
const mode = stats.mode & 0o777;
if ((mode & 0o077) !== 0) {
fs.chmodSync(envFile, 0o600);
console.log('✅ Fixed: Set 600 permissions on .env');
fixedCount++;
}
} catch (e) {
console.log('❌ Failed to fix .env permissions:', e.message);
}
}
// Fix 2: Secure other sensitive files
const sensitivePatterns = [
{ pattern: /\.env$/, perms: 0o600 },
{ pattern: /\.json$/, perms: 0o600 },
{ pattern: /\.key$/, perms: 0o600 },
{ pattern: /\.pem$/, perms: 0o600 }
];
const files = getFilesRecursively(CLAWDBOT_DIR);
for (const file of files) {
for (const sp of sensitivePatterns) {
if (sp.pattern.test(file)) {
try {
const stats = fs.statSync(file);
const mode = stats.mode & 0o777;
if (mode !== sp.perms) {
fs.chmodSync(file, sp.perms);
console.log(`✅ Fixed: Set ${sp.perms.toString(8)} on ${path.basename(file)}`);
fixedCount++;
}
} catch {
// Ignore
}
}
}
}
// Fix 3: Create .gitignore if missing
const gitignorePath = path.join(CLAWDBOT_DIR, '.gitignore');
if (!checkFileExists(gitignorePath)) {
const defaultGitignore = `# Clawdbot
.env
*.log
node_modules/
.DS_Store
*.pem
*.key
`;
fs.writeFileSync(gitignorePath, defaultGitignore);
console.log('✅ Fixed: Created .gitignore');
fixedCount++;
}
console.log(`\n✅ Auto-fix complete! ${fixedCount} issues resolved.`);
// Re-run audit to confirm
console.log('\n🔍 Re-running audit to verify...\n');
return fixedCount;
}
// Run if called directly
if (require.main === module) {
const args = process.argv.slice(2);
const shouldFix = args.includes('--fix');
if (shouldFix) {
runAutoFix().catch(e => {
console.error('Auto-fix error:', e.message);
process.exit(1);
});
} else {
runAudit({
full: args.includes('--full'),
json: args.includes('--json'),
credentials: args.includes('--credentials'),
ports: args.includes('--ports'),
configs: args.includes('--configs'),
permissions: args.includes('--permissions'),
docker: args.includes('--docker')
}).catch(e => {
console.error('Audit error:', e.message);
process.exit(1);
});
}
}
module.exports = { runAudit, checkCredentials, checkPorts, checkConfigs, checkPermissions, checkDocker };