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>
This commit is contained in:
533
skills/plugins/core/security.ts
Normal file
533
skills/plugins/core/security.ts
Normal file
@@ -0,0 +1,533 @@
|
||||
/**
|
||||
* 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
|
||||
Reference in New Issue
Block a user