Open-source marketplace for AI Agent skills. Features: - Next.js 15 web app with i18n (en/fa) - CLI tool for skill installation (npx skillhub) - GitHub crawler/indexer with multi-strategy discovery - Security scanning for all indexed skills - Self-hostable with Docker Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
445 lines
11 KiB
TypeScript
445 lines
11 KiB
TypeScript
import type { SecurityReport, SecurityIssue, SecurityIssueType, SecurityStatus } from './types.js';
|
|
|
|
interface ScanInput {
|
|
content: string;
|
|
scripts?: Array<{ name: string; content: string }>;
|
|
}
|
|
|
|
interface PatternCheck {
|
|
pattern: RegExp;
|
|
severity: SecurityIssue['severity'];
|
|
type: SecurityIssueType;
|
|
description: string;
|
|
}
|
|
|
|
const DANGEROUS_SHELL_PATTERNS: PatternCheck[] = [
|
|
{
|
|
pattern: /rm\s+-rf\s+[/~]/,
|
|
severity: 'critical',
|
|
type: 'destructive_command',
|
|
description: 'Recursive force delete from root or home directory',
|
|
},
|
|
{
|
|
pattern: /rm\s+-rf\s+\$\{?\w+\}?\/?\s*$/,
|
|
severity: 'high',
|
|
type: 'destructive_command',
|
|
description: 'Recursive delete with variable path (potential injection)',
|
|
},
|
|
{
|
|
pattern: /curl.*\|\s*(ba)?sh/,
|
|
severity: 'critical',
|
|
type: 'remote_execution',
|
|
description: 'Piping curl output directly to shell',
|
|
},
|
|
{
|
|
pattern: /wget.*\|\s*(ba)?sh/,
|
|
severity: 'critical',
|
|
type: 'remote_execution',
|
|
description: 'Piping wget output directly to shell',
|
|
},
|
|
{
|
|
pattern: /wget.*&&.*chmod.*\+x/,
|
|
severity: 'high',
|
|
type: 'download_execute',
|
|
description: 'Download and make executable pattern',
|
|
},
|
|
{
|
|
pattern: /eval\s*\(/,
|
|
severity: 'high',
|
|
type: 'eval_usage',
|
|
description: 'Use of eval() function',
|
|
},
|
|
{
|
|
pattern: /eval\s+["'`$]/,
|
|
severity: 'high',
|
|
type: 'eval_usage',
|
|
description: 'Shell eval with dynamic content',
|
|
},
|
|
{
|
|
pattern: /exec\s*\(/,
|
|
severity: 'medium',
|
|
type: 'exec_usage',
|
|
description: 'Use of exec() function',
|
|
},
|
|
{
|
|
pattern: /subprocess\.call.*shell\s*=\s*True/,
|
|
severity: 'medium',
|
|
type: 'shell_injection',
|
|
description: 'Python subprocess with shell=True',
|
|
},
|
|
{
|
|
pattern: /os\.system\s*\(/,
|
|
severity: 'medium',
|
|
type: 'shell_injection',
|
|
description: 'Python os.system call',
|
|
},
|
|
{
|
|
pattern: /child_process\.exec\(/,
|
|
severity: 'medium',
|
|
type: 'shell_injection',
|
|
description: 'Node.js child_process.exec (prefer execFile)',
|
|
},
|
|
];
|
|
|
|
const PROMPT_INJECTION_PATTERNS: PatternCheck[] = [
|
|
{
|
|
pattern: /ignore\s+(all\s+)?previous\s+instructions/i,
|
|
severity: 'high',
|
|
type: 'prompt_injection',
|
|
description: 'Prompt injection: ignore previous instructions',
|
|
},
|
|
{
|
|
pattern: /disregard\s+(all\s+)?prior\s+instructions/i,
|
|
severity: 'high',
|
|
type: 'prompt_injection',
|
|
description: 'Prompt injection: disregard prior instructions',
|
|
},
|
|
{
|
|
pattern: /you\s+are\s+now\s+in\s+.*mode/i,
|
|
severity: 'medium',
|
|
type: 'prompt_injection',
|
|
description: 'Prompt injection: mode switching attempt',
|
|
},
|
|
{
|
|
pattern: /system\s*:\s*you\s+are/i,
|
|
severity: 'high',
|
|
type: 'prompt_injection',
|
|
description: 'Prompt injection: fake system message',
|
|
},
|
|
{
|
|
pattern: /\[SYSTEM\]/i,
|
|
severity: 'medium',
|
|
type: 'prompt_injection',
|
|
description: 'Prompt injection: system tag in content',
|
|
},
|
|
{
|
|
pattern: /forget\s+(everything|all)\s+(you\s+)?know/i,
|
|
severity: 'high',
|
|
type: 'prompt_injection',
|
|
description: 'Prompt injection: memory wipe attempt',
|
|
},
|
|
];
|
|
|
|
const DATA_EXFILTRATION_PATTERNS: PatternCheck[] = [
|
|
{
|
|
pattern: /send.*to.*external/i,
|
|
severity: 'high',
|
|
type: 'data_exfiltration',
|
|
description: 'Potential data exfiltration instruction',
|
|
},
|
|
{
|
|
pattern: /upload.*credentials/i,
|
|
severity: 'critical',
|
|
type: 'data_exfiltration',
|
|
description: 'Instruction to upload credentials',
|
|
},
|
|
{
|
|
pattern: /transmit.*api[_-]?key/i,
|
|
severity: 'critical',
|
|
type: 'data_exfiltration',
|
|
description: 'Instruction to transmit API keys',
|
|
},
|
|
{
|
|
pattern: /exfiltrate/i,
|
|
severity: 'critical',
|
|
type: 'data_exfiltration',
|
|
description: 'Explicit exfiltration instruction',
|
|
},
|
|
{
|
|
pattern: /base64.*encode.*secret/i,
|
|
severity: 'high',
|
|
type: 'data_exfiltration',
|
|
description: 'Encoding secrets pattern',
|
|
},
|
|
];
|
|
|
|
const CREDENTIAL_PATTERNS: PatternCheck[] = [
|
|
{
|
|
pattern: /password\s*[=:]\s*["'][^"']+["']/i,
|
|
severity: 'critical',
|
|
type: 'credential_exposure',
|
|
description: 'Hardcoded password detected',
|
|
},
|
|
{
|
|
pattern: /api[_-]?key\s*[=:]\s*["'][a-zA-Z0-9]{20,}["']/i,
|
|
severity: 'critical',
|
|
type: 'credential_exposure',
|
|
description: 'Hardcoded API key detected',
|
|
},
|
|
{
|
|
pattern: /secret\s*[=:]\s*["'][^"']{10,}["']/i,
|
|
severity: 'high',
|
|
type: 'credential_exposure',
|
|
description: 'Hardcoded secret detected',
|
|
},
|
|
{
|
|
pattern: /private[_-]?key\s*[=:]/i,
|
|
severity: 'critical',
|
|
type: 'credential_exposure',
|
|
description: 'Private key assignment detected',
|
|
},
|
|
];
|
|
|
|
/**
|
|
* Scan a skill for security issues
|
|
*/
|
|
export function scanSecurity(input: ScanInput): SecurityReport {
|
|
const issues: SecurityIssue[] = [];
|
|
const recommendations: string[] = [];
|
|
|
|
// Scan main content
|
|
issues.push(...scanContent(input.content));
|
|
|
|
// Scan scripts
|
|
if (input.scripts) {
|
|
for (const script of input.scripts) {
|
|
const scriptIssues = scanScript(script.name, script.content);
|
|
issues.push(...scriptIssues);
|
|
}
|
|
}
|
|
|
|
// Generate recommendations based on issues
|
|
recommendations.push(...generateRecommendations(issues));
|
|
|
|
// Calculate score and status
|
|
const score = calculateScore(issues);
|
|
const status = calculateStatus(issues);
|
|
|
|
return {
|
|
score,
|
|
status,
|
|
issues,
|
|
recommendations,
|
|
scannedAt: new Date(),
|
|
};
|
|
}
|
|
|
|
function scanContent(content: string): SecurityIssue[] {
|
|
const issues: SecurityIssue[] = [];
|
|
|
|
// Check prompt injection patterns
|
|
for (const check of PROMPT_INJECTION_PATTERNS) {
|
|
const match = check.pattern.exec(content);
|
|
if (match) {
|
|
issues.push({
|
|
severity: check.severity,
|
|
type: check.type,
|
|
description: check.description,
|
|
line: getLineNumber(content, match.index),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check data exfiltration patterns
|
|
for (const check of DATA_EXFILTRATION_PATTERNS) {
|
|
const match = check.pattern.exec(content);
|
|
if (match) {
|
|
issues.push({
|
|
severity: check.severity,
|
|
type: check.type,
|
|
description: check.description,
|
|
line: getLineNumber(content, match.index),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check credential patterns
|
|
for (const check of CREDENTIAL_PATTERNS) {
|
|
const match = check.pattern.exec(content);
|
|
if (match) {
|
|
issues.push({
|
|
severity: check.severity,
|
|
type: check.type,
|
|
description: check.description,
|
|
line: getLineNumber(content, match.index),
|
|
});
|
|
}
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
function scanScript(name: string, content: string): SecurityIssue[] {
|
|
const issues: SecurityIssue[] = [];
|
|
|
|
// Check dangerous shell patterns
|
|
for (const check of DANGEROUS_SHELL_PATTERNS) {
|
|
const match = check.pattern.exec(content);
|
|
if (match) {
|
|
issues.push({
|
|
severity: check.severity,
|
|
type: check.type,
|
|
description: check.description,
|
|
location: name,
|
|
line: getLineNumber(content, match.index),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check credential patterns in scripts
|
|
for (const check of CREDENTIAL_PATTERNS) {
|
|
const match = check.pattern.exec(content);
|
|
if (match) {
|
|
issues.push({
|
|
severity: check.severity,
|
|
type: check.type,
|
|
description: check.description,
|
|
location: name,
|
|
line: getLineNumber(content, match.index),
|
|
});
|
|
}
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
function getLineNumber(content: string, index: number): number {
|
|
return content.substring(0, index).split('\n').length;
|
|
}
|
|
|
|
function calculateScore(issues: SecurityIssue[]): number {
|
|
let score = 100;
|
|
|
|
for (const issue of issues) {
|
|
switch (issue.severity) {
|
|
case 'critical':
|
|
score -= 30;
|
|
break;
|
|
case 'high':
|
|
score -= 20;
|
|
break;
|
|
case 'medium':
|
|
score -= 10;
|
|
break;
|
|
case 'low':
|
|
score -= 5;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return Math.max(0, Math.min(100, score));
|
|
}
|
|
|
|
/**
|
|
* Calculate security status based on issues
|
|
* - FAIL: Any critical issue
|
|
* - WARNING: Any high severity issue (no critical)
|
|
* - PASS: Only medium/low or no issues
|
|
*/
|
|
function calculateStatus(issues: SecurityIssue[]): SecurityStatus {
|
|
const hasCritical = issues.some((i) => i.severity === 'critical');
|
|
const hasHigh = issues.some((i) => i.severity === 'high');
|
|
|
|
if (hasCritical) return 'fail';
|
|
if (hasHigh) return 'warning';
|
|
return 'pass';
|
|
}
|
|
|
|
function generateRecommendations(issues: SecurityIssue[]): string[] {
|
|
const recommendations: string[] = [];
|
|
const issueTypes = new Set(issues.map((i) => i.type));
|
|
|
|
if (issueTypes.has('remote_execution')) {
|
|
recommendations.push(
|
|
'Review and verify all remote code execution patterns. Consider downloading scripts first and reviewing them before execution.'
|
|
);
|
|
}
|
|
|
|
if (issueTypes.has('prompt_injection')) {
|
|
recommendations.push(
|
|
'Remove or sanitize prompt injection patterns. These can cause unpredictable AI behavior.'
|
|
);
|
|
}
|
|
|
|
if (issueTypes.has('data_exfiltration')) {
|
|
recommendations.push(
|
|
'Remove instructions that could lead to data exfiltration. Never instruct AI to send sensitive data externally.'
|
|
);
|
|
}
|
|
|
|
if (issueTypes.has('credential_exposure')) {
|
|
recommendations.push(
|
|
'Remove all hardcoded credentials. Use environment variables or secure credential management.'
|
|
);
|
|
}
|
|
|
|
if (issueTypes.has('eval_usage') || issueTypes.has('exec_usage')) {
|
|
recommendations.push(
|
|
'Avoid using eval() and exec(). Use safer alternatives that do not execute arbitrary code.'
|
|
);
|
|
}
|
|
|
|
if (issueTypes.has('shell_injection')) {
|
|
recommendations.push(
|
|
'Use parameterized commands instead of shell string interpolation. Prefer execFile over exec.'
|
|
);
|
|
}
|
|
|
|
if (issueTypes.has('destructive_command')) {
|
|
recommendations.push(
|
|
'Review destructive commands carefully. Add safeguards and confirmations before deletion operations.'
|
|
);
|
|
}
|
|
|
|
if (recommendations.length === 0 && issues.length === 0) {
|
|
recommendations.push('No security issues detected. The skill appears safe.');
|
|
}
|
|
|
|
return recommendations;
|
|
}
|
|
|
|
/**
|
|
* Quick security check - returns true if no critical issues found
|
|
*/
|
|
export function isSecure(input: ScanInput): boolean {
|
|
const report = scanSecurity(input);
|
|
return !report.issues.some((i) => i.severity === 'critical');
|
|
}
|
|
|
|
/**
|
|
* Get security score color for display (deprecated, use getStatusColor)
|
|
*/
|
|
export function getScoreColor(score: number): 'green' | 'yellow' | 'orange' | 'red' {
|
|
if (score >= 90) return 'green';
|
|
if (score >= 70) return 'yellow';
|
|
if (score >= 50) return 'orange';
|
|
return 'red';
|
|
}
|
|
|
|
/**
|
|
* Get color for security status
|
|
*/
|
|
export function getStatusColor(status: SecurityStatus): 'green' | 'yellow' | 'red' {
|
|
switch (status) {
|
|
case 'pass':
|
|
return 'green';
|
|
case 'warning':
|
|
return 'yellow';
|
|
case 'fail':
|
|
return 'red';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert numeric score to status (for migrating existing data)
|
|
*/
|
|
export function scoreToStatus(score: number): SecurityStatus {
|
|
if (score >= 70) return 'pass';
|
|
if (score >= 30) return 'warning';
|
|
return 'fail';
|
|
}
|
|
|
|
/**
|
|
* Get display label for status
|
|
*/
|
|
export function getStatusLabel(status: SecurityStatus): { en: string; fa: string } {
|
|
switch (status) {
|
|
case 'pass':
|
|
return { en: 'Safe', fa: 'امن' };
|
|
case 'warning':
|
|
return { en: 'Caution', fa: 'احتیاط' };
|
|
case 'fail':
|
|
return { en: 'Unsafe', fa: 'ناامن' };
|
|
}
|
|
}
|