Files
admin b723e2bd7d Reorganize: Move all skills to skills/ folder
- 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>
2026-01-23 18:05:17 +00:00

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