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>
This commit is contained in:
444
packages/core/src/security.ts
Normal file
444
packages/core/src/security.ts
Normal file
@@ -0,0 +1,444 @@
|
||||
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: 'ناامن' };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user