Files
pm-claude-skills/scripts/skill-audit.mjs
T
mohitagw15856 e9bc1d0626 Security auditor, personas, orchestration, docs catalog & roadmap (#35)
Closes the remaining gaps vs alirezarezvani/claude-skills across trust, content
types, discoverability, and community.

Security (trust signal + useful):
- scripts/skill-audit.mjs scans skills/*/SKILL.md + each skill's scripts/ for
  prompt injection, exfiltration, dynamic code exec, destructive shell, secrets,
  and hidden text. HIGH fails CI (.github/workflows/skill-audit.yml) + a badge.
- New skill-security-auditor skill teaches the same review (production tier).

Content types:
- output-styles/ — 4 personas (Startup CTO, Growth Marketer, Solo Founder,
  Product Leader) as Claude Code output styles; --agent claude installs them too.
- ORCHESTRATION.md — Skill Chain / Multi-Agent Handoff / Domain Deep-Dive /
  Solo Sprint patterns.

Discoverability:
- scripts/build-docs.mjs generates a server-rendered, SEO-indexable
  web/catalog.html of all skills (built in the Pages deploy; gitignored).
  Linked from README + playground.

Community:
- ROADMAP.md (now/next/later + good-first-issues).

README badges/sections, TIERS (47 production), CHANGELOG, package.json files,
and exports/web index all updated. SkillCheck + security audit + exports verified.


Claude-Session: https://claude.ai/code/session_016JWn5jRD5tcEFKrubjQ6Px

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-18 08:09:14 +01:00

131 lines
7.5 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
// Skill Security Auditor — scans installable skill content (skills/*/SKILL.md and
// each skill's scripts/) for patterns that could harm someone who installs them:
// prompt injection, data exfiltration, dynamic code execution, destructive shell,
// hardcoded secrets, and hidden/obfuscated text.
//
// Only HIGH-severity findings fail the build; medium/low are advisory. This keeps
// it useful without drowning legitimate skills in false positives.
//
// Usage:
// node scripts/skill-audit.mjs # audit all skills
// node scripts/skill-audit.mjs --json # machine-readable
// node scripts/skill-audit.mjs --all # also fail on medium findings
//
// No dependencies.
import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
import { join, dirname, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');
const skillsDir = join(root, 'skills');
const args = process.argv.slice(2);
const asJson = args.includes('--json');
const failOnMedium = args.includes('--all');
// severity: high (fail), medium, low. Each rule: {id, severity, re, why}
const RULES = [
// ── Prompt injection aimed at the model ──────────────────────────────────
{ id: 'inject.ignore', severity: 'high', why: 'Tries to override the model\'s prior/system instructions.',
re: /\b(ignore|disregard|forget)\b[^.\n]{0,40}\b(previous|prior|above|all|earlier|system)\b[^.\n]{0,20}\b(instructions?|prompts?|rules?|guidelines?)/i },
{ id: 'inject.devmode', severity: 'high', why: 'Jailbreak framing (developer mode / DAN / no restrictions).',
re: /\b(developer mode|do anything now|\bDAN\b|jailbreak|no (restrictions|guardrails|filters)|without (any )?(restrictions|limitations))\b/i },
{ id: 'inject.reveal', severity: 'high', why: 'Tries to extract the system prompt / hidden instructions.',
re: /\b(reveal|print|show|repeat|output)\b[^.\n]{0,30}\b(system prompt|your (instructions|system message|initial prompt)|hidden (instructions|prompt))/i },
{ id: 'inject.persona', severity: 'medium', why: 'Forces an unconstrained persona override.',
re: /\byou are now\b[^.\n]{0,40}\b(unrestricted|unfiltered|amoral|evil|no rules)\b/i },
// ── Data exfiltration ────────────────────────────────────────────────────
{ id: 'exfil.send', severity: 'high', why: 'Instructs sending user/conversation data to an external endpoint.',
re: /\b(send|post|upload|transmit|exfiltrate|forward)\b[^.\n]{0,40}\b(to )?(https?:\/\/|webhook|api\.|endpoint|server)\b[^.\n]{0,40}\b(conversation|messages?|data|credentials?|keys?|tokens?|history)/i },
{ id: 'exfil.beacon', severity: 'medium', why: 'Network call to a hardcoded external URL inside content.',
re: /\b(curl|wget|fetch\(|requests\.(get|post)|urllib|http\.client)\b[^.\n]{0,60}https?:\/\/(?!localhost|127\.0\.0\.1|\[|[a-z0-9.-]*example\.(com|org))/i },
// ── Code / command execution ─────────────────────────────────────────────
{ id: 'exec.dynamic', severity: 'medium', why: 'Executes dynamically-built code/commands.',
re: /\b(eval|exec)\s*\(|\bos\.system\s*\(|subprocess\.(run|call|Popen)\s*\(|child_process|\bFunction\s*\(\s*['"`]/ },
{ id: 'exec.destructive', severity: 'high', why: 'Destructive shell command.',
re: /\brm\s+-rf\s+(\/|~|\$HOME|\*)|\b(mkfs|dd\s+if=)|\b:\(\)\s*\{\s*:\|:&\s*\}|\bchmod\s+-R?\s*777\s+\// },
// ── Credentials / secrets ────────────────────────────────────────────────
{ id: 'secret.aws', severity: 'high', why: 'Looks like a hardcoded AWS access key.', re: /\bAKIA[0-9A-Z]{16}\b/ },
{ id: 'secret.private-key', severity: 'high', why: 'Embedded private key.', re: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/ },
{ id: 'secret.harvest', severity: 'medium', why: 'Asks the user/model to hand over secrets.',
re: /\b(send|share|paste|provide|enter)\b[^.\n]{0,30}\b(your )?(api[_ ]?key|password|secret|access token|ssh key|private key|seed phrase)\b/i },
// ── Obfuscation / hidden text ────────────────────────────────────────────
{ id: 'hidden.zerowidth', severity: 'high', why: 'Contains zero-width / invisible Unicode (can hide instructions).',
re: /[---]/ },
{ id: 'hidden.base64blob', severity: 'medium', why: 'Long base64 blob (possible hidden payload).',
re: /\b[A-Za-z0-9+/]{220,}={0,2}\b/ },
];
function auditText(rel, text, findings) {
const lines = text.split('\n');
for (const rule of RULES) {
// search line-by-line so we can report a location and a snippet
for (let i = 0; i < lines.length; i++) {
const m = lines[i].match(rule.re);
if (m) {
findings.push({ file: rel, line: i + 1, id: rule.id, severity: rule.severity, why: rule.why, snippet: lines[i].trim().slice(0, 120) });
break; // one hit per rule per file is enough
}
}
// zero-width can sit anywhere incl. between lines — also test whole text
if (rule.id === 'hidden.zerowidth' && !findings.some((f) => f.file === rel && f.id === rule.id) && rule.re.test(text)) {
findings.push({ file: rel, line: 0, id: rule.id, severity: rule.severity, why: rule.why, snippet: '(invisible characters)' });
}
}
}
function walk(dir, exts, out) {
for (const e of readdirSync(dir)) {
const p = join(dir, e);
if (statSync(p).isDirectory()) walk(p, exts, out);
else if (exts.some((x) => p.endsWith(x))) out.push(p);
}
}
// Skills whose job is to *document* attack patterns (so they legitimately contain
// the phrases the rules look for). Audited by humans, skipped by the scanner.
const ALLOWLIST = new Set(['skill-security-auditor']);
const findings = [];
if (existsSync(skillsDir)) {
for (const name of readdirSync(skillsDir)) {
if (ALLOWLIST.has(name)) continue;
const sdir = join(skillsDir, name);
if (!statSync(sdir).isDirectory()) continue;
const files = [];
const skillMd = join(sdir, 'SKILL.md');
if (existsSync(skillMd)) files.push(skillMd);
const scripts = join(sdir, 'scripts');
if (existsSync(scripts)) walk(scripts, ['.py', '.mjs', '.js', '.sh'], files);
for (const f of files) auditText(relative(root, f), readFileSync(f, 'utf8'), findings);
}
}
const counts = findings.reduce((a, f) => ((a[f.severity] = (a[f.severity] || 0) + 1), a), {});
const high = counts.high || 0, medium = counts.medium || 0, low = counts.low || 0;
if (asJson) {
console.log(JSON.stringify({ scanned: 'skills/**', high, medium, low, findings }, null, 2));
} else {
const icon = { high: '🔴', medium: '🟠', low: '🟡' };
for (const f of findings.sort((a, b) => (a.severity < b.severity ? -1 : 1))) {
console.log(` ${icon[f.severity]} [${f.severity}] ${f.file}:${f.line} (${f.id}) — ${f.why}`);
if (f.snippet) console.log(` ↳ ${f.snippet}`);
}
console.log(`\nSkill Security Audit — ${high} high · ${medium} medium · ${low} low across skills/**`);
}
const failed = high > 0 || (failOnMedium && medium > 0);
if (failed) {
if (!asJson) console.log('FAILED — review the findings above. (False positive? Tune scripts/skill-audit.mjs.)');
process.exit(1);
} else if (!asJson) {
console.log('No high-severity issues found. ✓');
}