- Created skills/ directory - Moved 272 skills to skills/ subfolder - Kept agents/ at root level - Kept installation scripts and docs at root level Repository structure: - skills/ - All 272 skills from skills.sh - agents/ - Agent definitions - *.sh, *.ps1 - Installation scripts - README.md, etc. - Documentation Co-Authored-By: Claude <noreply@anthropic.com>
534 lines
14 KiB
TypeScript
534 lines
14 KiB
TypeScript
/**
|
|
* Claude Code Plugin Security System
|
|
*
|
|
* Features:
|
|
* - Permission validation
|
|
* - File system sandboxing
|
|
* - Command execution validation
|
|
* - Network access control
|
|
* - Resource limits
|
|
* - Code injection prevention
|
|
*/
|
|
|
|
import fs from 'fs/promises'
|
|
import path from 'path'
|
|
import { createHash } from 'crypto'
|
|
|
|
// ============================================================================
|
|
// TYPES AND INTERFACES
|
|
// ============================================================================
|
|
|
|
export type Permission =
|
|
| 'read:files'
|
|
| 'write:files'
|
|
| 'execute:commands'
|
|
| 'network:request'
|
|
| 'read:config'
|
|
| 'write:config'
|
|
| 'hook:events'
|
|
| 'read:secrets'
|
|
|
|
export interface SecurityPolicy {
|
|
allowedPaths: string[]
|
|
deniedPaths: string[]
|
|
allowedCommands: string[]
|
|
deniedCommands: string[]
|
|
allowedDomains: string[]
|
|
deniedDomains: string[]
|
|
maxFileSize: number
|
|
maxExecutionTime: number
|
|
requireCodeSigning: boolean
|
|
}
|
|
|
|
export interface SecurityContext {
|
|
pluginName: string
|
|
permissions: Permission[]
|
|
workingDirectory: string
|
|
startTime: number
|
|
}
|
|
|
|
export interface ValidationResult {
|
|
allowed: boolean
|
|
reason?: string
|
|
modifiedValue?: any
|
|
}
|
|
|
|
// ============================================================================
|
|
// SECURITY MANAGER CLASS
|
|
// ============================================================================
|
|
|
|
export class SecurityManager {
|
|
private policy: SecurityPolicy
|
|
private contexts: Map<string, SecurityContext> = new Map()
|
|
private auditLog: AuditLog[] = []
|
|
|
|
constructor(policy?: Partial<SecurityPolicy>) {
|
|
this.policy = {
|
|
allowedPaths: [],
|
|
deniedPaths: [],
|
|
allowedCommands: [],
|
|
deniedCommands: ['rm -rf /', 'format', 'mkfs'],
|
|
allowedDomains: [],
|
|
deniedDomains: [],
|
|
maxFileSize: 100 * 1024 * 1024, // 100MB
|
|
maxExecutionTime: 30000, // 30 seconds
|
|
requireCodeSigning: false,
|
|
...policy,
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// CONTEXT MANAGEMENT
|
|
// ============================================================================
|
|
|
|
createContext(pluginName: string, permissions: Permission[]): SecurityContext {
|
|
const context: SecurityContext = {
|
|
pluginName,
|
|
permissions,
|
|
workingDirectory: process.cwd(),
|
|
startTime: Date.now(),
|
|
}
|
|
|
|
this.contexts.set(pluginName, context)
|
|
return context
|
|
}
|
|
|
|
getContext(pluginName: string): SecurityContext | undefined {
|
|
return this.contexts.get(pluginName)
|
|
}
|
|
|
|
removeContext(pluginName: string): void {
|
|
this.contexts.delete(pluginName)
|
|
}
|
|
|
|
// ============================================================================
|
|
// PERMISSION VALIDATION
|
|
// ============================================================================
|
|
|
|
hasPermission(pluginName: string, permission: Permission): boolean {
|
|
const context = this.getContext(pluginName)
|
|
if (!context) return false
|
|
|
|
return context.permissions.includes(permission)
|
|
}
|
|
|
|
validatePermissions(
|
|
pluginName: string,
|
|
requiredPermissions: Permission[]
|
|
): ValidationResult {
|
|
const context = this.getContext(pluginName)
|
|
|
|
if (!context) {
|
|
return {
|
|
allowed: false,
|
|
reason: 'Plugin context not found',
|
|
}
|
|
}
|
|
|
|
const missing = requiredPermissions.filter(
|
|
(perm) => !context.permissions.includes(perm)
|
|
)
|
|
|
|
if (missing.length > 0) {
|
|
return {
|
|
allowed: false,
|
|
reason: `Missing permissions: ${missing.join(', ')}`,
|
|
}
|
|
}
|
|
|
|
return { allowed: true }
|
|
}
|
|
|
|
// ============================================================================
|
|
// FILE SYSTEM VALIDATION
|
|
// ============================================================================
|
|
|
|
async validateFileAccess(
|
|
pluginName: string,
|
|
filePath: string,
|
|
mode: 'read' | 'write'
|
|
): Promise<ValidationResult> {
|
|
const permission = mode === 'read' ? 'read:files' : 'write:files'
|
|
|
|
if (!this.hasPermission(pluginName, permission)) {
|
|
return {
|
|
allowed: false,
|
|
reason: `Missing permission: ${permission}`,
|
|
}
|
|
}
|
|
|
|
const resolvedPath = path.resolve(filePath)
|
|
|
|
// Check denied paths first
|
|
for (const denied of this.policy.deniedPaths) {
|
|
if (resolvedPath.startsWith(path.resolve(denied))) {
|
|
return {
|
|
allowed: false,
|
|
reason: `Access denied to path: ${filePath}`,
|
|
}
|
|
}
|
|
}
|
|
|
|
// If allowed paths are specified, check against them
|
|
if (this.policy.allowedPaths.length > 0) {
|
|
const allowed = this.policy.allowedPaths.some((allowedPath) =>
|
|
resolvedPath.startsWith(path.resolve(allowedPath))
|
|
)
|
|
|
|
if (!allowed) {
|
|
return {
|
|
allowed: false,
|
|
reason: `Path not in allowed list: ${filePath}`,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check file size for writes
|
|
if (mode === 'write') {
|
|
try {
|
|
const stats = await fs.stat(resolvedPath)
|
|
if (stats.size > this.policy.maxFileSize) {
|
|
return {
|
|
allowed: false,
|
|
reason: `File too large: ${stats.size} bytes`,
|
|
}
|
|
}
|
|
} catch {
|
|
// File doesn't exist yet, that's fine for writes
|
|
}
|
|
}
|
|
|
|
return { allowed: true }
|
|
}
|
|
|
|
// ============================================================================
|
|
// COMMAND VALIDATION
|
|
// ============================================================================
|
|
|
|
validateCommand(pluginName: string, command: string): ValidationResult {
|
|
if (!this.hasPermission(pluginName, 'execute:commands')) {
|
|
return {
|
|
allowed: false,
|
|
reason: 'Missing permission: execute:commands',
|
|
}
|
|
}
|
|
|
|
// Check denied commands
|
|
for (const denied of this.policy.deniedCommands) {
|
|
if (command.includes(denied)) {
|
|
return {
|
|
allowed: false,
|
|
reason: `Command contains denied pattern: ${denied}`,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for dangerous patterns
|
|
const dangerousPatterns = [
|
|
/\brm\s+-rf\s+\//,
|
|
/\bformat\s+[a-z]:/i,
|
|
/\bdel\s+\/[sq]/i,
|
|
/\>\s*\/dev\/[a-z]+/,
|
|
/\|.*\brm\b/,
|
|
/&&.*\brm\b/,
|
|
/;.*\brm\b/,
|
|
]
|
|
|
|
for (const pattern of dangerousPatterns) {
|
|
if (pattern.test(command)) {
|
|
return {
|
|
allowed: false,
|
|
reason: 'Command contains dangerous pattern',
|
|
}
|
|
}
|
|
}
|
|
|
|
return { allowed: true }
|
|
}
|
|
|
|
// ============================================================================
|
|
// NETWORK VALIDATION
|
|
// ============================================================================
|
|
|
|
validateNetworkRequest(pluginName: string, url: string): ValidationResult {
|
|
if (!this.hasPermission(pluginName, 'network:request')) {
|
|
return {
|
|
allowed: false,
|
|
reason: 'Missing permission: network:request',
|
|
}
|
|
}
|
|
|
|
try {
|
|
const urlObj = new URL(url)
|
|
const domain = urlObj.hostname
|
|
|
|
// Check denied domains
|
|
for (const denied of this.policy.deniedDomains) {
|
|
if (domain === denied || domain.endsWith(`.${denied}`)) {
|
|
return {
|
|
allowed: false,
|
|
reason: `Access denied to domain: ${domain}`,
|
|
}
|
|
}
|
|
}
|
|
|
|
// If allowed domains are specified, check against them
|
|
if (this.policy.allowedDomains.length > 0) {
|
|
const allowed = this.policy.allowedDomains.some(
|
|
(allowed) => domain === allowed || domain.endsWith(`.${allowed}`)
|
|
)
|
|
|
|
if (!allowed) {
|
|
return {
|
|
allowed: false,
|
|
reason: `Domain not in allowed list: ${domain}`,
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
return {
|
|
allowed: false,
|
|
reason: 'Invalid URL',
|
|
}
|
|
}
|
|
|
|
return { allowed: true }
|
|
}
|
|
|
|
// ============================================================================
|
|
// CODE INJECTION PREVENTION
|
|
// ============================================================================
|
|
|
|
sanitizeInput(input: string): string {
|
|
// Remove potential code injection patterns
|
|
return input
|
|
.replace(/<script[^>]*>.*?<\/script>/gi, '')
|
|
.replace(/javascript:/gi, '')
|
|
.replace(/on\w+\s*=/gi, '')
|
|
.replace(/\${.*?}/g, '') // Template literals
|
|
.replace(/`.*?`/g, '') // Backtick expressions
|
|
.replace(/eval\s*\(/gi, '')
|
|
.replace(/Function\s*\(/gi, '')
|
|
.replace(/setTimeout\s*\(/gi, '')
|
|
.replace(/setInterval\s*\(/gi, '')
|
|
}
|
|
|
|
validateScriptCode(code: string): ValidationResult {
|
|
const dangerousPatterns = [
|
|
/eval\s*\(/,
|
|
/Function\s*\(/,
|
|
/require\s*\(\s*['"`]fs['"`]\s*\)/,
|
|
/require\s*\(\s*['"`]child_process['"`]\s*\)/,
|
|
/process\s*\.\s*exit/,
|
|
/\.\.\//, // Path traversal
|
|
/~\//, // Home directory access
|
|
]
|
|
|
|
for (const pattern of dangerousPatterns) {
|
|
if (pattern.test(code)) {
|
|
return {
|
|
allowed: false,
|
|
reason: `Code contains dangerous pattern: ${pattern.source}`,
|
|
}
|
|
}
|
|
}
|
|
|
|
return { allowed: true }
|
|
}
|
|
|
|
// ============================================================================
|
|
// INTEGRITY CHECKING
|
|
// ============================================================================
|
|
|
|
async calculateFileIntegrity(filePath: string): Promise<string> {
|
|
const content = await fs.readFile(filePath)
|
|
return createHash('sha256').update(content).digest('hex')
|
|
}
|
|
|
|
async verifyPluginIntegrity(
|
|
pluginPath: string,
|
|
expectedIntegrity: string
|
|
): Promise<boolean> {
|
|
const actualIntegrity = await this.calculateDirectoryIntegrity(pluginPath)
|
|
return actualIntegrity === expectedIntegrity
|
|
}
|
|
|
|
async calculateDirectoryIntegrity(dirPath: string): Promise<string> {
|
|
const hash = createHash('sha256')
|
|
const files = await this.getAllFiles(dirPath)
|
|
|
|
for (const file of files.sort()) {
|
|
const content = await fs.readFile(file)
|
|
hash.update(content)
|
|
}
|
|
|
|
return hash.digest('hex')
|
|
}
|
|
|
|
private async getAllFiles(dirPath: string): Promise<string[]> {
|
|
const files: string[] = []
|
|
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(dirPath, entry.name)
|
|
if (entry.isDirectory()) {
|
|
files.push(...(await this.getAllFiles(fullPath)))
|
|
} else {
|
|
files.push(fullPath)
|
|
}
|
|
}
|
|
|
|
return files
|
|
}
|
|
|
|
// ============================================================================
|
|
// AUDIT LOGGING
|
|
// ============================================================================
|
|
|
|
logAccess(
|
|
pluginName: string,
|
|
action: string,
|
|
resource: string,
|
|
allowed: boolean
|
|
): void {
|
|
const entry: AuditLog = {
|
|
timestamp: new Date().toISOString(),
|
|
pluginName,
|
|
action,
|
|
resource,
|
|
allowed,
|
|
}
|
|
|
|
this.auditLog.push(entry)
|
|
|
|
// Keep only last 1000 entries
|
|
if (this.auditLog.length > 1000) {
|
|
this.auditLog = this.auditLog.slice(-1000)
|
|
}
|
|
}
|
|
|
|
getAuditLog(pluginName?: string): AuditLog[] {
|
|
if (pluginName) {
|
|
return this.auditLog.filter((entry) => entry.pluginName === pluginName)
|
|
}
|
|
return [...this.auditLog]
|
|
}
|
|
|
|
clearAuditLog(): void {
|
|
this.auditLog = []
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// TYPES
|
|
// ============================================================================
|
|
|
|
interface AuditLog {
|
|
timestamp: string
|
|
pluginName: string
|
|
action: string
|
|
resource: string
|
|
allowed: boolean
|
|
}
|
|
|
|
// ============================================================================
|
|
// SANDBOX CLASS - Isolated Execution Environment
|
|
// ============================================================================
|
|
|
|
export class Sandbox {
|
|
private security: SecurityManager
|
|
private context: SecurityContext
|
|
|
|
constructor(security: SecurityManager, context: SecurityContext) {
|
|
this.security = security
|
|
this.context = context
|
|
}
|
|
|
|
async readFile(filePath: string): Promise<string> {
|
|
const validation = await this.security.validateFileAccess(
|
|
this.context.pluginName,
|
|
filePath,
|
|
'read'
|
|
)
|
|
|
|
if (!validation.allowed) {
|
|
this.security.logAccess(
|
|
this.context.pluginName,
|
|
'read:file',
|
|
filePath,
|
|
false
|
|
)
|
|
throw new Error(validation.reason)
|
|
}
|
|
|
|
this.security.logAccess(
|
|
this.context.pluginName,
|
|
'read:file',
|
|
filePath,
|
|
true
|
|
)
|
|
|
|
return await fs.readFile(filePath, 'utf-8')
|
|
}
|
|
|
|
async writeFile(filePath: string, content: string): Promise<void> {
|
|
const validation = await this.security.validateFileAccess(
|
|
this.context.pluginName,
|
|
filePath,
|
|
'write'
|
|
)
|
|
|
|
if (!validation.allowed) {
|
|
this.security.logAccess(
|
|
this.context.pluginName,
|
|
'write:file',
|
|
filePath,
|
|
false
|
|
)
|
|
throw new Error(validation.reason)
|
|
}
|
|
|
|
this.security.logAccess(
|
|
this.context.pluginName,
|
|
'write:file',
|
|
filePath,
|
|
true
|
|
)
|
|
|
|
await fs.writeFile(filePath, content, 'utf-8')
|
|
}
|
|
|
|
executeCommand(command: string): Promise<string> {
|
|
const validation = this.security.validateCommand(
|
|
this.context.pluginName,
|
|
command
|
|
)
|
|
|
|
if (!validation.allowed) {
|
|
this.security.logAccess(
|
|
this.context.pluginName,
|
|
'execute:command',
|
|
command,
|
|
false
|
|
)
|
|
throw new Error(validation.reason)
|
|
}
|
|
|
|
this.security.logAccess(
|
|
this.context.pluginName,
|
|
'execute:command',
|
|
command,
|
|
true
|
|
)
|
|
|
|
// Command execution would be implemented here
|
|
return Promise.resolve('')
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// EXPORTS
|
|
// ============================================================================
|
|
|
|
export default SecurityManager
|