Files
skillhub/packages/core/src/security.ts
airano 97b427831a Initial release v1.0.0
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>
2026-02-12 06:42:07 +03:30

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: 'ناامن' };
}
}