Skill: autogame-17/security-sentinel (version 1.0.1)

--- name: security-sentinel description: Scan the workspace for security vulnerabilities, exposed secrets, and misconfigurations. --- # Security Sentinel A unified security scanner for OpenClaw workspaces. Detects vulnerabilities in dependencies (npm audit), exposed secrets (regex patterns), and unsafe file permissions. ## Usage ### CLI Run a full security scan: ```bash node skills/security-sentinel/index.js ``` This will output a JSON report to stdout. If risks are detected (high/critical vulnerabilities, secrets, or bad permissions), it exits with code 1. ### Options - `--skip-audit`: Skip the npm audit step (faster) - `--no-fail`: Do not exit with code 1 even if risks are detected (useful for monitoring only) ### Programmatic ```javascript const sentinel = require('./skills/security-sentinel'); const report = await sentinel.scan(); if (report.status === 'risk_detected') { console.error('Security issues found:', report); } ``` ## Features 1. **Dependency Audit**: Runs `npm audit` to check `package.json` dependencies for known CVEs. 2. **Secret Detection**: Scans workspace files for patterns resembling API keys, passwords, and private keys. 3. **Permission Check**: Verifies critical files (`package.json`, `.env`) are not world-writable. ## Configuration - **Ignored Paths**: `node_modules`, `.git`, `logs`, `temp`, `.openclaw/cache`. - **Secret Patterns**: Generic API Key, Password, Private Key, Feishu App Secret.

path: _meta.json
content: ownerId: kn7apafdj4thknczrgxdzfd2v1808svf
slug: security-sentinel
version: 1.1.2
publishedAt: 1770118194971

path: index.js
content: const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

/**
 * Security Sentinel - Unified Scanner
 * Checks for:
 * 1. Dependency vulnerabilities (npm audit)
 * 2. Exposed secrets (API keys, Tokens)
 * 3. File permission risks (world-writable)
 */

const SECRET_PATTERNS = [
  { name: 'Generic API Key', regex: /api[_-]?key['"]?\s*[:=]\s*['"]?[\w\-]{20,}['"]?/i },
  { name: 'Password', regex: /password['"]?\s*[:=]\s*['"]?[\w\-!@#$%^&*()]{8,}['"]?/i },
  { name: 'Private Key', regex: /-----BEGIN PRIVATE KEY-----/ },
  { name: 'Feishu App Secret', regex: /app_secret['"]?\s*[:=]\s*['"]?[\w\-]{20,}['"]?/i }
];

const IGNORED_PATHS = [
  'node_modules',
  '.git',
  'logs',
  'temp',
  '.openclaw/cache',
  'memory' // Add memory to ignored paths to reduce noise
];

async function scan(options = {}) {
  const report = {
    timestamp: new Date().toISOString(),
    vulnerabilities: {},
    secrets: [],
    permissions: [],
    status: 'clean'
  };

  console.log('[Sentinel] Starting security scan...');

  // 1. Dependency Scan (npm audit)
  // Skip if --skip-audit
  if (!options.skipAudit) {
    try {
      console.log('[Sentinel] Running npm audit...');
      const auditOutput = execSync('npm audit --json', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
      const audit = JSON.parse(auditOutput);
      report.vulnerabilities = {
        total: audit.metadata?.vulnerabilities?.total || 0,
        severity: audit.metadata?.vulnerabilities || {},
        details: audit.advisories || {}
      };
    } catch (err) {
      // npm audit returns non-zero exit code if vulns found, but stdout is valid JSON
      if (err.stdout) {
        try {
          const audit = JSON.parse(err.stdout);
          report.vulnerabilities = {
            total: audit.metadata?.vulnerabilities?.total || 0,
            severity: audit.metadata?.vulnerabilities || {},
            details: audit.advisories || {}
          };
        } catch (parseErr) {
          report.vulnerabilities = { error: 'Failed to parse npm audit output' };
        }
      } else {
        report.vulnerabilities = { error: 'npm audit failed: ' + err.message };
      }
    }
  }

  // 2. Secret Scan (Recursive)
  console.log('[Sentinel] Scanning for secrets...');
  const files = getAllFiles(process.cwd());
  for (const file of files) {
    try {
      const content = fs.readFileSync(file, 'utf8');
      for (const pattern of SECRET_PATTERNS) {
        if (pattern.regex.test(content)) {
          // Verify false positives (e.g., example values)
          if (!content.includes('EXAMPLE') && !content.includes('YOUR_API_KEY') && !file.includes('security-sentinel/index.js')) {
             report.secrets.push({
               file: path.relative(process.cwd(), file),
               type: pattern.name
             });
          }
        }
      }
    } catch (readErr) {
      // Ignore binary or unreadable
    }
  }

  // 3. Permission Scan
  console.log('[Sentinel] Checking permissions...');
  const CRITICAL_FILES = ['package.json', '.env', 'openclaw.json'];
  for (const crit of CRITICAL_FILES) {
    if (fs.existsSync(crit)) {
      const stats = fs.statSync(crit);
      const mode = stats.mode & 0o777; // Octal
      // Check if world writable (xx2 or xx6 or xx7)
      if ((mode & 0o002) !== 0) {
        report.permissions.push({
          file: crit,
          mode: mode.toString(8),
          issue: 'World Writable'
        });
      }
    }
  }

  // Summary Status
  if (
    (report.vulnerabilities.total > 0 && (report.vulnerabilities.severity.high > 0 || report.vulnerabilities.severity.critical > 0)) ||
    report.secrets.length > 0 ||
    report.permissions.length > 0
  ) {
    report.status = 'risk_detected';
  }

  return report;
}

function getAllFiles(dir, fileList = []) {
  const files = fs.readdirSync(dir);
  for (const file of files) {
    const filePath = path.join(dir, file);
    if (IGNORED_PATHS.some(ignored => filePath.includes(ignored))) continue;

    if (fs.existsSync(filePath)) {
      const stat = fs.statSync(filePath);
      if (stat.isDirectory()) {
        getAllFiles(filePath, fileList);
      } else {
        if (/\.(js|json|md|txt|sh|yml|yaml|env)$/.test(file)) {
          fileList.push(filePath);
        }
      }
    }
  }
  return fileList;
}

// CLI Support
if (require.main === module) {
  const args = process.argv.slice(2);
  const options = {
    skipAudit: args.includes('--skip-audit')
  };
  
  scan(options).then(report => {
    console.log(JSON.stringify(report, null, 2));
    // If risks are detected, default to exit 1 for CI/CD unless --no-fail is passed
    if (report.status === 'risk_detected' && !args.includes('--no-fail')) {
      process.exit(1); 
    }
  }).catch(err => {
    console.error(err);
    process.exit(1);
  });
}

module.exports = { scan };


path: package-lock.json
content: {
  "name": "security-sentinel",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "security-sentinel",
      "version": "1.0.0",
      "dependencies": {
        "fs-extra": "^11.1.0",
        "glob": "^8.1.0"
      }
    },
    "node_modules/balanced-match": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/brace-expansion": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "balanced-match": "^1.0.0"
      }
    },
    "node_modules/fs-extra": {
      "version": "11.3.3",
      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "graceful-fs": "^4.2.0",
        "jsonfile": "^6.0.1",
        "universalify": "^2.0.0"
      },
      "engines": {
        "node": ">=14.14"
      }
    },
    "node_modules/fs.realpath": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/glob": {
      "version": "8.1.0",
      "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting [REDACTED_SECRET]",
      "license": "ISC",
      "dependencies": {
        "fs.realpath": "^1.0.0",
        "inflight": "^1.0.4",
        "inherits": "2",
        "minimatch": "^5.0.1",
        "once": "^1.3.0"
      },
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/isaacs"
      }
    },
    "node_modules/graceful-fs": {
      "version": "4.2.11",
      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/inflight": {
      "version": "1.0.6",
      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
      "license": "ISC",
      "dependencies": {
        "once": "^1.3.0",
        "wrappy": "1"
      }
    },
    "node_modules/inherits": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/jsonfile": {
      "version": "6.2.0",
      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "universalify": "^2.0.0"
      },
      "optionalDependencies": {
        "graceful-fs": "^4.1.6"
      }
    },
    "node_modules/minimatch": {
      "version": "5.1.6",
      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "brace-expansion": "^2.0.1"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/once": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "wrappy": "1"
      }
    },
    "node_modules/universalify": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 10.0.0"
      }
    },
    "node_modules/wrappy": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    }
  }
}


path: package.json
content: {
  "name": "security-sentinel",
  "version": "1.0.1",
  "description": "Unified security scanner for OpenClaw workspace (dependencies, secrets, permissions).",
  "main": "index.js",
  "scripts": {
    "test": "node index.js"
  },
  "dependencies": {
    "fs-extra": "^11.1.0",
    "glob": "^8.1.0"
  }
}


path: scan.js
content: #!/usr/bin/env node
const fs = require("fs");
const path = require("path");

const foundThreats = [];
const warnings = [];

// Definition of Secret Patterns
const secretPatterns = [
    { name: "AWS Access Key", regex: /AKIA[0-9A-Z]{16}/ },
    { name: "Private Key Block", regex: /-----BEGIN [A-Z]+ PRIVATE KEY-----/ },
    { name: "Generic API Key (Variable)", regex: /(api_key|apiKey|API_KEY)\s*[:=]\s*['"][a-zA-Z0-9_\-]{20,}['"]/ },
    { name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-\._~\+\/]{20,}=*/ }, // Enforced min length to avoid false positives
    { name: "OpenAI Key", regex: /sk-[a-zA-Z0-9]{20,}/ },
    { name: "Feishu/Lark Tenant Token", regex: /t-[a-z0-9]{10,}/ } // Removed ou_ (User ID) as it's not a secret
];

// Helper: Check file existence
function checkExists(filePath) {
    if (!fs.existsSync(filePath)) {
        return false;
    }
    return true;
}

// 1. Check Core Integrity
if (!checkExists("SECURITY.md")) {
    foundThreats.push("🚨 CRITICAL: SECURITY.md is MISSING!");
}
if (!checkExists("AGENTS.md")) {
    foundThreats.push("🚨 CRITICAL: AGENTS.md is MISSING!");
}

// 2. Check for Forbidden Directories (Shadow IT)
const forbiddenPaths = ["memory/private", "fmw/.shadow_protocol.md", ".hidden_context"];
forbiddenPaths.forEach(p => {
    if (checkExists(p)) {
        foundThreats.push(`🚨 Found forbidden/suspicious path: ${p}`);
    }
});

// 3. Scan for Secrets (Basic Patterns) in Config Files
// Recursive Scan Function
function recursiveScan(dir) {
    let results = [];
    const files = fs.readdirSync(dir);
    for (const file of files) {
        const fullPath = path.join(dir, file);
        
        // Skip ignored directories
        if (['node_modules', '.git', 'media', 'dist', 'coverage', '.openclaw', 'memory', 'cache', 'ai-game-engine', 'repo'].includes(file)) continue;
        
        // Skip self, env files, and lock files
        if (file === 'index.js' || file === 'scan.js' || file.endsWith('.env')) continue;
        if (['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock'].includes(file)) continue;
        if (file.endsWith('.tmLanguage.json')) continue;

        try {
            const stats = fs.statSync(fullPath);
            if (stats.isDirectory()) {
                results = results.concat(recursiveScan(fullPath));
            } else if (stats.isFile() && stats.size < 500 * 1024) { // Limit to 500KB files
                // Check extension
                if (!['.md', '.js', '.json', '.yml', '.yaml', '.sh', '.env', '.txt'].includes(path.extname(file))) continue;
                
                const content = fs.readFileSync(fullPath, 'utf8');
                secretPatterns.forEach(pat => {
                    if (pat.regex.test(content)) {
                        results.push(`⚠️ Potential ${pat.name} exposed in ${fullPath}`);
                    }
                });
            } else if (stats.isFile() && file === 'package.json') {
                // Dependency Security Scan
                try {
                    const pkg = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
                    const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
                    
                    for (const [dep, ver] of Object.entries(allDeps)) {
                        if (ver === '*' || ver === 'latest') {
                            results.push(`⚠️ Risky dependency version in ${fullPath}: ${dep}@${ver} (Wildcard/Latest is unstable)`);
                        }
                        if (ver.startsWith('http://') || ver.startsWith('ftp://')) {
                            results.push(`🚨 Insecure dependency protocol in ${fullPath}: ${dep}@${ver}`);
                        }
                        if (ver.includes('file:')) {
                            results.push(`ℹ️ Local file dependency in ${fullPath}: ${dep} (Check if intended)`);
                        }
                    }

                    if (pkg.scripts) {
                        for (const [name, cmd] of Object.entries(pkg.scripts)) {
                            if (cmd.includes('curl') && cmd.includes('| bash')) {
                                results.push(`🚨 Dangerous script detected in ${fullPath} script '${name}': curl | bash`);
                            }
                            if (cmd.includes('rm -rf /')) {
                                results.push(`🚨 Critical Risk: 'rm -rf /' detected in ${fullPath} script '${name}'`);
                            }
                        }
                    }
                } catch (e) {
                    results.push(`⚠️ Failed to parse package.json at ${fullPath}: ${e.message}`);
                }
            }
        } catch (e) {
            // Ignore access errors
        }
    }
    return results;
}

console.log("🔍 Starting recursive secret scan...");
const secretWarnings = recursiveScan(process.cwd());
warnings.push(...secretWarnings);

/* Legacy specific file check removed in favor of recursive scan */


// 4. Report
console.log("🛡️ Security Sentinel Scan Report");
console.log("===============================");

if (foundThreats.length > 0) {
    console.log("\n🛑 THREATS DETECTED (ACTION REQUIRED):");
    foundThreats.forEach(t => console.log(t));
}

if (warnings.length > 0) {
    console.log("\n⚠️ WARNINGS (INVESTIGATE):");
    warnings.forEach(w => console.log(w));
}

if (foundThreats.length === 0 && warnings.length === 0) {
    console.log("\n✅ System Clean. No active threats or warnings.");
} else {
    // Exit code 1 if threats found (useful for CI/hooks)
    if (foundThreats.length > 0) process.exit(1);
}


Security scan verdicts — ClawScan: clean | static analysis:  | VirusTotal: suspicious | SkillSpector: suspicious (MEDIUM)