Initial commit with translated description

This commit is contained in:
2026-03-29 09:37:07 +08:00
commit ab6e61ccf2
3 changed files with 613 additions and 0 deletions

505
scripts/audit.cjs Normal file
View File

@@ -0,0 +1,505 @@
#!/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 };