mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-09 22:31:08 +00:00
feat: implement enhanced security middleware with YAML configuration
- Add SecurityMiddleware class with YAML-based configuration support - Implement three-tier configuration hierarchy (Enterprise → Global → Project → Custom) - Add ASK action support for prompting users instead of just blocking - Create EnhancedRooIgnoreController that integrates with SecurityMiddleware - Support both gitignore-style and regex patterns for file matching - Add comprehensive type definitions for security configuration - Include example YAML configurations for different use cases - Add documentation for the new security middleware features - Implement tests for core functionality Fixes #7912
This commit is contained in:
parent
33fe6fb9c6
commit
348c0258a2
12 changed files with 2999 additions and 0 deletions
183
docs/README-SECURITY.md
Normal file
183
docs/README-SECURITY.md
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
# Security Middleware - Quick Start Guide
|
||||
|
||||
## What's New?
|
||||
|
||||
RooCode now supports enhanced security controls beyond `.rooignore`:
|
||||
|
||||
- **ASK Action**: Prompt for approval instead of just blocking
|
||||
- **YAML Configuration**: More flexible than gitignore patterns
|
||||
- **Three-Tier Hierarchy**: Enterprise → Global → Project → Custom
|
||||
- **Regex Support**: Complex pattern matching
|
||||
- **Command Protection**: Block terminal commands too
|
||||
|
||||
## Quick Setup
|
||||
|
||||
### 1. Enable Security Middleware
|
||||
|
||||
Create `.roo-security.yaml` in your project root:
|
||||
|
||||
```yaml
|
||||
version: "1.0"
|
||||
security:
|
||||
enabled: true
|
||||
rules:
|
||||
- pattern: "**/.env*"
|
||||
action: ASK
|
||||
description: "Environment files may contain secrets"
|
||||
askMessage: "Allow access to ${file}?"
|
||||
|
||||
- pattern: "**/*.key"
|
||||
action: BLOCK
|
||||
description: "Private keys are blocked"
|
||||
```
|
||||
|
||||
### 2. Global User Settings
|
||||
|
||||
Create `~/.roo-security.yaml` for personal defaults:
|
||||
|
||||
```yaml
|
||||
version: "1.0"
|
||||
security:
|
||||
enabled: true
|
||||
rules:
|
||||
- pattern: "**/.ssh/**"
|
||||
action: BLOCK
|
||||
description: "SSH directory protection"
|
||||
```
|
||||
|
||||
### 3. Custom Overrides
|
||||
|
||||
Create `.roo-security-custom.yaml` for personal project overrides:
|
||||
|
||||
```yaml
|
||||
version: "1.0"
|
||||
security:
|
||||
enabled: true
|
||||
inheritRules: true
|
||||
rules:
|
||||
- pattern: ".env.local"
|
||||
action: ALLOW
|
||||
priority: 200
|
||||
description: "Allow local development env"
|
||||
```
|
||||
|
||||
## Actions Explained
|
||||
|
||||
| Action | Description | Use Case |
|
||||
| --------- | ------------------------ | --------------------------------------- |
|
||||
| **BLOCK** | Deny access completely | Sensitive files, production configs |
|
||||
| **ASK** | Prompt user for approval | Files that might be needed occasionally |
|
||||
| **ALLOW** | Explicitly allow access | Override inherited blocks |
|
||||
|
||||
## Pattern Examples
|
||||
|
||||
### Gitignore-style
|
||||
|
||||
- `*.log` - All log files
|
||||
- `**/.env*` - Any .env file
|
||||
- `config/*.json` - JSON files in config/
|
||||
|
||||
### Regular Expressions
|
||||
|
||||
- `/.*password.*/` - Files containing "password"
|
||||
- `/.*\d{3}-\d{2}-\d{4}.*/` - SSN-like patterns
|
||||
|
||||
## Priority System
|
||||
|
||||
Higher numbers win when multiple patterns match:
|
||||
|
||||
- **1000+**: Enterprise/compliance (unchangeable)
|
||||
- **100-999**: Important security rules
|
||||
- **50-99**: Standard rules
|
||||
- **1-49**: Suggestions
|
||||
|
||||
## Command Protection
|
||||
|
||||
Protect against terminal access:
|
||||
|
||||
```yaml
|
||||
- pattern: "**/.env"
|
||||
action: BLOCK
|
||||
applyToCommands: true # Also blocks: cat .env
|
||||
```
|
||||
|
||||
## Inheritance Control
|
||||
|
||||
```yaml
|
||||
security:
|
||||
inheritRules: false # Don't inherit from parent configs
|
||||
```
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
- `.rooignore` still works exactly as before
|
||||
- `.rooignore` blocks take precedence over security rules
|
||||
- You can use both systems together
|
||||
|
||||
## Examples
|
||||
|
||||
### Protect API Keys
|
||||
|
||||
```yaml
|
||||
- pattern: "**/api_keys.*"
|
||||
action: BLOCK
|
||||
priority: 100
|
||||
description: "API keys must not be accessed"
|
||||
```
|
||||
|
||||
### Ask for Database Access
|
||||
|
||||
```yaml
|
||||
- pattern: "**/*.db"
|
||||
action: ASK
|
||||
priority: 80
|
||||
askMessage: "Database file ${file} - allow access?"
|
||||
```
|
||||
|
||||
### Allow Test Files
|
||||
|
||||
```yaml
|
||||
- pattern: "test/**"
|
||||
action: ALLOW
|
||||
priority: 50
|
||||
description: "Test files are safe"
|
||||
```
|
||||
|
||||
## Configuration Files
|
||||
|
||||
| File | Location | Purpose | Priority |
|
||||
| --------------------------- | -------------- | --------------------- | --------- |
|
||||
| `.roo-security.yaml` | Project root | Project rules | Medium |
|
||||
| `.roo-security-custom.yaml` | Project root | Personal overrides | High |
|
||||
| `~/.roo-security.yaml` | Home directory | User defaults | Low |
|
||||
| Enterprise config | Cloud/managed | Organization policies | Highest\* |
|
||||
|
||||
\*Enterprise rules with `inheritRules: false` cannot be overridden
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Rules not working?
|
||||
|
||||
1. Check `enabled: true` is set
|
||||
2. Verify pattern syntax
|
||||
3. Check priority values
|
||||
4. Enable debug mode
|
||||
|
||||
### ASK prompts not appearing?
|
||||
|
||||
- Ensure VS Code extension is updated
|
||||
- Check notification settings
|
||||
- Verify pattern matches
|
||||
|
||||
## See Also
|
||||
|
||||
- [Full Documentation](./security-middleware.md)
|
||||
- [Example Configurations](../examples/security-configs/)
|
||||
- [Migration Guide](#migration-from-rooignore)
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
|
||||
- GitHub Issues: [RooCodeInc/Roo-Code](https://github.com/RooCodeInc/Roo-Code/issues)
|
||||
- Documentation: [Security Middleware Guide](./security-middleware.md)
|
||||
345
docs/security-middleware.md
Normal file
345
docs/security-middleware.md
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
# RooCode Security Middleware
|
||||
|
||||
## Overview
|
||||
|
||||
The RooCode Security Middleware provides enhanced, granular file access control beyond the traditional `.rooignore` functionality. It introduces a flexible YAML-based configuration system with support for ASK actions (prompting users for approval), regex patterns, and a three-tier configuration hierarchy.
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. **ASK Action Support**
|
||||
|
||||
Instead of just blocking file access, the middleware can prompt users for approval before allowing access to sensitive files.
|
||||
|
||||
### 2. **YAML Configuration**
|
||||
|
||||
More flexible and readable configuration format compared to gitignore-style patterns.
|
||||
|
||||
### 3. **Three-Tier Configuration Hierarchy**
|
||||
|
||||
- **Enterprise**: Organization-wide policies (cannot be overridden)
|
||||
- **Global**: User-level defaults (~/.roo-security.yaml)
|
||||
- **Project**: Project-specific rules (.roo-security.yaml)
|
||||
- **Custom**: Personal overrides (.roo-security-custom.yaml)
|
||||
|
||||
### 4. **Regex Pattern Support**
|
||||
|
||||
In addition to gitignore-style patterns, supports regular expressions for complex matching.
|
||||
|
||||
### 5. **Rule Priority System**
|
||||
|
||||
Fine-grained control over which rules take precedence when multiple patterns match.
|
||||
|
||||
## Configuration File Format
|
||||
|
||||
### Basic Structure
|
||||
|
||||
```yaml
|
||||
version: "1.0"
|
||||
security:
|
||||
enabled: true
|
||||
inheritRules: true # Whether to inherit rules from higher levels
|
||||
defaultAction: ALLOW # Default when no rules match (ALLOW, BLOCK, or ASK)
|
||||
askMessagePrefix: "Security check" # Prefix for ASK prompts
|
||||
|
||||
rules:
|
||||
- pattern: "**/.env*" # Gitignore-style pattern
|
||||
action: ASK # ALLOW, BLOCK, or ASK
|
||||
priority: 90 # Higher numbers = higher priority
|
||||
description: "Environment files may contain secrets"
|
||||
askMessage: "Access to ${file} requires approval" # Custom prompt
|
||||
applyToCommands: true # Also check terminal commands
|
||||
```
|
||||
|
||||
### Pattern Types
|
||||
|
||||
1. **Gitignore-style patterns**:
|
||||
|
||||
- `*.log` - Match all log files
|
||||
- `**/.env*` - Match .env files in any directory
|
||||
- `src/**/*.test.js` - Match test files in src
|
||||
|
||||
2. **Regular expressions** (enclosed in forward slashes):
|
||||
- `/.*\.secret\..*/` - Match files with .secret. in the name
|
||||
- `/.*[Ss][Ss][Nn].*\d{3}-\d{2}-\d{4}.*/` - Match potential SSN patterns
|
||||
|
||||
## Configuration Hierarchy
|
||||
|
||||
### 1. Enterprise Configuration
|
||||
|
||||
- Managed by organization administrators
|
||||
- Cannot be overridden by lower levels
|
||||
- Typically enforces compliance requirements (GDPR, HIPAA, PCI-DSS)
|
||||
|
||||
### 2. Global Configuration
|
||||
|
||||
- Located at `~/.roo-security.yaml`
|
||||
- User's personal default security settings
|
||||
- Applies to all projects unless overridden
|
||||
|
||||
### 3. Project Configuration
|
||||
|
||||
- Located at `project-root/.roo-security.yaml`
|
||||
- Project-specific security rules
|
||||
- Can inherit or override global rules
|
||||
|
||||
### 4. Custom Configuration
|
||||
|
||||
- Located at `project-root/.roo-security-custom.yaml`
|
||||
- Personal overrides for the current project
|
||||
- Highest priority (except for enterprise rules with `inheritRules: false`)
|
||||
|
||||
## Rule Evaluation Order
|
||||
|
||||
1. Rules are evaluated from **Custom → Project → Global → Enterprise**
|
||||
2. Within each level, rules are sorted by priority (highest first)
|
||||
3. First matching rule determines the action
|
||||
4. If no rules match, the `defaultAction` is applied
|
||||
|
||||
## Actions
|
||||
|
||||
### BLOCK
|
||||
|
||||
Completely prevents access to the file. The operation fails with an error message.
|
||||
|
||||
```yaml
|
||||
- pattern: "**/.ssh/**"
|
||||
action: BLOCK
|
||||
description: "SSH keys must not be accessed"
|
||||
```
|
||||
|
||||
### ASK
|
||||
|
||||
Prompts the user for approval before allowing access. If approved, access is granted; if denied, access is blocked.
|
||||
|
||||
```yaml
|
||||
- pattern: "**/*.key"
|
||||
action: ASK
|
||||
askMessage: "File ${file} appears to be a private key. Allow access?"
|
||||
```
|
||||
|
||||
### ALLOW
|
||||
|
||||
Explicitly allows access to the file. Useful for overriding inherited rules.
|
||||
|
||||
```yaml
|
||||
- pattern: "test/fixtures/**"
|
||||
action: ALLOW
|
||||
description: "Test fixtures are safe to access"
|
||||
```
|
||||
|
||||
## Integration with .rooignore
|
||||
|
||||
The Enhanced Security Middleware maintains full backward compatibility with `.rooignore`:
|
||||
|
||||
1. `.rooignore` patterns are always evaluated first
|
||||
2. Files blocked by `.rooignore` cannot be allowed by security rules
|
||||
3. Security middleware adds additional layers of protection
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Example 1: Protecting Sensitive Files
|
||||
|
||||
```yaml
|
||||
# .roo-security.yaml
|
||||
version: "1.0"
|
||||
security:
|
||||
enabled: true
|
||||
rules:
|
||||
- pattern: "**/production.yml"
|
||||
action: BLOCK
|
||||
priority: 100
|
||||
description: "Production configuration"
|
||||
|
||||
- pattern: "**/*.pem"
|
||||
action: ASK
|
||||
priority: 90
|
||||
askMessage: "Certificate file ${file} - approve access?"
|
||||
```
|
||||
|
||||
### Example 2: Development Overrides
|
||||
|
||||
```yaml
|
||||
# .roo-security-custom.yaml
|
||||
version: "1.0"
|
||||
security:
|
||||
enabled: true
|
||||
inheritRules: true
|
||||
rules:
|
||||
# Override project rule for local development
|
||||
- pattern: ".env.local"
|
||||
action: ALLOW
|
||||
priority: 200
|
||||
description: "Local development environment"
|
||||
```
|
||||
|
||||
### Example 3: Enterprise Compliance
|
||||
|
||||
```yaml
|
||||
# Enterprise configuration (managed centrally)
|
||||
version: '1.0'
|
||||
security:
|
||||
enabled: true
|
||||
inheritRules: false # Cannot be overridden
|
||||
rules:
|
||||
- pattern: "**/pii/**"
|
||||
action: BLOCK
|
||||
priority: 1000
|
||||
description: "GDPR compliance - PII protection"
|
||||
|
||||
- pattern: "/.*credit.*card.*\d{4}.*/"
|
||||
action: BLOCK
|
||||
priority: 1000
|
||||
description: "PCI-DSS compliance"
|
||||
```
|
||||
|
||||
## Command-Line Access Control
|
||||
|
||||
The middleware also validates terminal commands that attempt to read files:
|
||||
|
||||
```yaml
|
||||
rules:
|
||||
- pattern: "**/.env*"
|
||||
action: BLOCK
|
||||
applyToCommands: true # Also blocks: cat .env, type .env, etc.
|
||||
```
|
||||
|
||||
Supported commands:
|
||||
|
||||
- Unix: `cat`, `less`, `more`, `head`, `tail`, `grep`, `awk`, `sed`
|
||||
- PowerShell: `Get-Content`, `gc`, `type`, `Select-String`, `sls`
|
||||
|
||||
## API Usage
|
||||
|
||||
### TypeScript Integration
|
||||
|
||||
```typescript
|
||||
import { EnhancedRooIgnoreController } from "./core/ignore/EnhancedRooIgnoreController"
|
||||
import { SecurityEvaluation } from "./core/security/types"
|
||||
|
||||
// Initialize with security middleware
|
||||
const controller = new EnhancedRooIgnoreController(projectPath, {
|
||||
enableSecurityMiddleware: true,
|
||||
askHandler: async (evaluation: SecurityEvaluation) => {
|
||||
// Show prompt to user
|
||||
const approved = await vscode.window.showWarningMessage(evaluation.message, "Allow", "Deny")
|
||||
return approved === "Allow"
|
||||
},
|
||||
securityOptions: {
|
||||
debug: true,
|
||||
globalConfigPath: "~/.roo-security.yaml",
|
||||
},
|
||||
})
|
||||
|
||||
// Initialize (loads configurations)
|
||||
await controller.initialize()
|
||||
|
||||
// Check file access (async for proper ASK handling)
|
||||
const result = await controller.validateAccessAsync("config/secrets.yml")
|
||||
if (!result.allowed) {
|
||||
if (result.requiresApproval) {
|
||||
console.log("File requires approval:", result.evaluation?.message)
|
||||
} else {
|
||||
console.log("File access blocked:", result.evaluation?.message)
|
||||
}
|
||||
}
|
||||
|
||||
// Check command execution
|
||||
const cmdResult = await controller.validateCommandAsync("cat .env")
|
||||
if (!cmdResult.allowed) {
|
||||
console.log("Command blocked:", cmdResult.evaluation?.message)
|
||||
}
|
||||
```
|
||||
|
||||
### Statistics and Monitoring
|
||||
|
||||
```typescript
|
||||
// Get security statistics
|
||||
const stats = controller.getSecurityStats()
|
||||
console.log(`Total evaluations: ${stats.totalEvaluations}`)
|
||||
console.log(`Blocked: ${stats.blockedCount}`)
|
||||
console.log(`Asked: ${stats.askedCount}`)
|
||||
console.log(`Allowed: ${stats.allowedCount}`)
|
||||
|
||||
// Export configuration
|
||||
const yamlConfig = await controller.exportSecurityConfig("project")
|
||||
console.log("Current project config:", yamlConfig)
|
||||
|
||||
// Import new configuration
|
||||
await controller.importSecurityConfig(newYamlContent, "custom")
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Start with Defaults
|
||||
|
||||
Begin with sensible defaults at the global level, then add project-specific rules as needed.
|
||||
|
||||
### 2. Use Priority Wisely
|
||||
|
||||
- 1000: Critical security rules (enterprise/compliance)
|
||||
- 100-999: Important project rules
|
||||
- 50-99: Standard rules
|
||||
- 1-49: Low-priority suggestions
|
||||
|
||||
### 3. Provide Clear Messages
|
||||
|
||||
Always include descriptive `askMessage` and `description` fields to help users understand why access is being controlled.
|
||||
|
||||
### 4. Test Your Rules
|
||||
|
||||
Use the custom configuration file to test new rules before adding them to project or global configs.
|
||||
|
||||
### 5. Regular Expressions
|
||||
|
||||
Use regex patterns sparingly and test thoroughly. They're powerful but can have performance implications.
|
||||
|
||||
### 6. Command Protection
|
||||
|
||||
Enable `applyToCommands: true` for truly sensitive files to prevent command-line access.
|
||||
|
||||
## Migration from .rooignore
|
||||
|
||||
The security middleware is fully backward compatible. To migrate:
|
||||
|
||||
1. Keep your `.rooignore` file as-is
|
||||
2. Create `.roo-security.yaml` for new rules
|
||||
3. Gradually move patterns from `.rooignore` to YAML configs
|
||||
4. Use ASK action for files that need conditional access
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Rules Not Being Applied
|
||||
|
||||
1. Check that `enabled: true` is set
|
||||
2. Verify file paths are relative to project root
|
||||
3. Check rule priority - higher priority rules match first
|
||||
4. Enable debug mode to see evaluation details
|
||||
|
||||
### ASK Prompts Not Showing
|
||||
|
||||
1. Ensure `askHandler` is configured in the controller
|
||||
2. Check that the UI component is properly connected
|
||||
3. Verify the pattern matches the file path
|
||||
|
||||
### Performance Issues
|
||||
|
||||
1. Avoid overly complex regex patterns
|
||||
2. Limit the number of rules per configuration level
|
||||
3. Use gitignore-style patterns when possible
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Enterprise rules** should be immutable and audited
|
||||
2. **Sensitive patterns** should use BLOCK, not ASK
|
||||
3. **Regular expressions** should be carefully reviewed for ReDoS vulnerabilities
|
||||
4. **Custom configurations** should be excluded from version control if they contain sensitive patterns
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Cloud-based enterprise configuration management
|
||||
- [ ] Audit logging for all security decisions
|
||||
- [ ] Machine learning-based sensitive data detection
|
||||
- [ ] Integration with secret scanning tools
|
||||
- [ ] Role-based access control (RBAC)
|
||||
- [ ] Time-based access rules
|
||||
- [ ] Contextual rules based on git branch or environment
|
||||
126
examples/security-configs/custom-security.yaml
Normal file
126
examples/security-configs/custom-security.yaml
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
# Custom Security Configuration Example
|
||||
# This file would be placed at project-root/.roo-security-custom.yaml
|
||||
# Users can customize security rules for their specific needs
|
||||
version: '1.0'
|
||||
|
||||
security:
|
||||
enabled: true
|
||||
inheritRules: true # Inherit from project, global, and enterprise configs
|
||||
defaultAction: ALLOW
|
||||
askMessagePrefix: "Custom security check"
|
||||
|
||||
rules:
|
||||
# Override specific project rules with ALLOW for development
|
||||
- pattern: "test/fixtures/production_*.json"
|
||||
action: ALLOW # Override the BLOCK from project config for testing
|
||||
priority: 200 # Higher priority than project rule
|
||||
description: "Allow access to production fixtures in development"
|
||||
|
||||
# Personal preferences for additional security
|
||||
- pattern: "**/personal/**"
|
||||
action: BLOCK
|
||||
priority: 150
|
||||
description: "Personal notes and documents"
|
||||
|
||||
- pattern: "**/drafts/**"
|
||||
action: ASK
|
||||
priority: 140
|
||||
description: "Draft documents"
|
||||
askMessage: "This is a draft document: ${file}. Do you want to proceed?"
|
||||
|
||||
# Development-specific rules
|
||||
- pattern: "**/node_modules/**"
|
||||
action: ALLOW # Explicitly allow for debugging
|
||||
priority: 50
|
||||
description: "Allow access to node_modules for debugging"
|
||||
|
||||
- pattern: "**/.vscode/settings.json"
|
||||
action: ASK
|
||||
priority: 130
|
||||
description: "VS Code workspace settings"
|
||||
askMessage: "Modifying VS Code settings in ${file} may affect your development environment."
|
||||
|
||||
# Temporary work files
|
||||
- pattern: "**/tmp/**"
|
||||
action: ALLOW
|
||||
priority: 40
|
||||
description: "Temporary files"
|
||||
|
||||
- pattern: "**/*.tmp"
|
||||
action: ALLOW
|
||||
priority: 40
|
||||
description: "Temporary files"
|
||||
|
||||
# Custom patterns for current project
|
||||
- pattern: "src/experimental/**"
|
||||
action: ASK
|
||||
priority: 120
|
||||
description: "Experimental features"
|
||||
askMessage: "Accessing experimental feature in ${file}. This code is unstable."
|
||||
|
||||
- pattern: "src/deprecated/**"
|
||||
action: ASK
|
||||
priority: 110
|
||||
description: "Deprecated code"
|
||||
askMessage: "Accessing deprecated code in ${file}. Consider using newer alternatives."
|
||||
|
||||
# Local development secrets (different from production)
|
||||
- pattern: ".env.local"
|
||||
action: ALLOW # Allow for local development
|
||||
priority: 160
|
||||
description: "Local development environment"
|
||||
|
||||
- pattern: ".env.development"
|
||||
action: ALLOW
|
||||
priority: 160
|
||||
description: "Development environment variables"
|
||||
|
||||
# Custom regex patterns for specific needs
|
||||
- pattern: "/.*TODO.*SECURITY.*/"
|
||||
action: ASK
|
||||
priority: 100
|
||||
description: "Files with security TODOs"
|
||||
askMessage: "File ${file} contains security TODOs that need attention."
|
||||
|
||||
- pattern: "/.*FIXME.*AUTH.*/"
|
||||
action: ASK
|
||||
priority: 100
|
||||
description: "Files with authentication FIXMEs"
|
||||
askMessage: "File ${file} contains authentication issues marked as FIXME."
|
||||
|
||||
# Team-specific conventions
|
||||
- pattern: "**/do-not-commit/**"
|
||||
action: BLOCK
|
||||
priority: 180
|
||||
description: "Files marked as do-not-commit by team convention"
|
||||
|
||||
- pattern: "**/*.local.*"
|
||||
action: ASK
|
||||
priority: 90
|
||||
description: "Local configuration files"
|
||||
askMessage: "Local configuration file ${file} may contain machine-specific settings."
|
||||
|
||||
# Documentation with sensitive examples
|
||||
- pattern: "docs/internal/**"
|
||||
action: ASK
|
||||
priority: 80
|
||||
description: "Internal documentation"
|
||||
askMessage: "Internal documentation ${file} may contain sensitive information."
|
||||
|
||||
# Build artifacts that might leak information
|
||||
- pattern: "dist/**/*.map"
|
||||
action: ASK
|
||||
priority: 70
|
||||
description: "Source maps"
|
||||
askMessage: "Source map ${file} reveals source code structure. Proceed with caution."
|
||||
|
||||
# Custom allow rules for specific tools
|
||||
- pattern: ".roo-security*.yaml"
|
||||
action: ALLOW
|
||||
priority: 200
|
||||
description: "Allow access to security configuration files for management"
|
||||
|
||||
- pattern: ".rooignore"
|
||||
action: ALLOW
|
||||
priority: 200
|
||||
description: "Allow access to .rooignore for backward compatibility"
|
||||
158
examples/security-configs/enterprise-security.yaml
Normal file
158
examples/security-configs/enterprise-security.yaml
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
# Enterprise Security Configuration Example
|
||||
# This would be managed by the organization and deployed via cloud service
|
||||
# Organizations can enforce strict security policies that cannot be overridden
|
||||
version: '1.0'
|
||||
|
||||
security:
|
||||
enabled: true
|
||||
inheritRules: false # Enterprise rules cannot be overridden
|
||||
defaultAction: ALLOW
|
||||
askMessagePrefix: "Enterprise security policy"
|
||||
|
||||
rules:
|
||||
# Compliance and regulatory requirements
|
||||
- pattern: "**/pii/**"
|
||||
action: BLOCK
|
||||
priority: 1000 # Highest priority
|
||||
description: "PII data protection (GDPR/CCPA compliance)"
|
||||
applyToCommands: true
|
||||
|
||||
- pattern: "**/phi/**"
|
||||
action: BLOCK
|
||||
priority: 1000
|
||||
description: "PHI data protection (HIPAA compliance)"
|
||||
applyToCommands: true
|
||||
|
||||
- pattern: "**/pci/**"
|
||||
action: BLOCK
|
||||
priority: 1000
|
||||
description: "PCI DSS compliance - payment card data"
|
||||
applyToCommands: true
|
||||
|
||||
# Financial data protection
|
||||
- pattern: "**/financial/**"
|
||||
action: BLOCK
|
||||
priority: 950
|
||||
description: "Financial data protection"
|
||||
applyToCommands: true
|
||||
|
||||
- pattern: "**/*bank*"
|
||||
action: ASK
|
||||
priority: 900
|
||||
description: "Banking-related files"
|
||||
askMessage: "Enterprise policy: Access to banking file ${file} requires security team approval."
|
||||
|
||||
# Intellectual property protection
|
||||
- pattern: "**/proprietary/**"
|
||||
action: BLOCK
|
||||
priority: 950
|
||||
description: "Proprietary algorithms and trade secrets"
|
||||
|
||||
- pattern: "**/patents/**"
|
||||
action: BLOCK
|
||||
priority: 950
|
||||
description: "Patent-related documents"
|
||||
|
||||
# Security and authentication
|
||||
- pattern: "**/security/keys/**"
|
||||
action: BLOCK
|
||||
priority: 1000
|
||||
description: "Master encryption keys"
|
||||
applyToCommands: true
|
||||
|
||||
- pattern: "**/auth/providers/**"
|
||||
action: BLOCK
|
||||
priority: 950
|
||||
description: "Authentication provider configurations"
|
||||
|
||||
- pattern: "**/vault/**"
|
||||
action: BLOCK
|
||||
priority: 1000
|
||||
description: "HashiCorp Vault or similar secret storage"
|
||||
applyToCommands: true
|
||||
|
||||
# Infrastructure protection
|
||||
- pattern: "**/infrastructure/prod/**"
|
||||
action: BLOCK
|
||||
priority: 950
|
||||
description: "Production infrastructure configuration"
|
||||
applyToCommands: true
|
||||
|
||||
- pattern: "**/infrastructure/security/**"
|
||||
action: BLOCK
|
||||
priority: 1000
|
||||
description: "Security infrastructure configuration"
|
||||
applyToCommands: true
|
||||
|
||||
# Audit and compliance logs
|
||||
- pattern: "**/audit/**"
|
||||
action: BLOCK
|
||||
priority: 900
|
||||
description: "Audit logs must not be modified"
|
||||
applyToCommands: true
|
||||
|
||||
- pattern: "**/compliance/**"
|
||||
action: ASK
|
||||
priority: 850
|
||||
description: "Compliance documentation"
|
||||
askMessage: "Enterprise policy: Modifying compliance file ${file} requires approval from compliance team."
|
||||
|
||||
# Customer data protection
|
||||
- pattern: "**/customers/**"
|
||||
action: ASK
|
||||
priority: 900
|
||||
description: "Customer data"
|
||||
askMessage: "Enterprise policy: Access to customer data in ${file} requires approval and will be logged."
|
||||
|
||||
- pattern: "**/users/data/**"
|
||||
action: ASK
|
||||
priority: 900
|
||||
description: "User data"
|
||||
askMessage: "Enterprise policy: Access to user data in ${file} requires approval and will be logged."
|
||||
|
||||
# Source code protection for critical systems
|
||||
- pattern: "**/core-platform/**"
|
||||
action: ASK
|
||||
priority: 800
|
||||
description: "Core platform code"
|
||||
askMessage: "Enterprise policy: Modifying core platform code in ${file} requires architecture team approval."
|
||||
|
||||
- pattern: "**/payment-processing/**"
|
||||
action: BLOCK
|
||||
priority: 950
|
||||
description: "Payment processing system"
|
||||
|
||||
# Legal and contracts
|
||||
- pattern: "**/legal/**"
|
||||
action: BLOCK
|
||||
priority: 950
|
||||
description: "Legal documents and contracts"
|
||||
|
||||
- pattern: "**/*.contract"
|
||||
action: BLOCK
|
||||
priority: 950
|
||||
description: "Contract files"
|
||||
|
||||
# HR and employee data
|
||||
- pattern: "**/hr/**"
|
||||
action: BLOCK
|
||||
priority: 950
|
||||
description: "HR data and employee information"
|
||||
applyToCommands: true
|
||||
|
||||
- pattern: "**/payroll/**"
|
||||
action: BLOCK
|
||||
priority: 1000
|
||||
description: "Payroll information"
|
||||
applyToCommands: true
|
||||
|
||||
# Regex patterns for sensitive data
|
||||
- pattern: "/.*[Ss][Ss][Nn].*\\d{3}-\\d{2}-\\d{4}.*/"
|
||||
action: BLOCK
|
||||
priority: 1000
|
||||
description: "Files potentially containing SSN patterns"
|
||||
|
||||
- pattern: "/.*[Cc]redit.*[Cc]ard.*\\d{4}.*/"
|
||||
action: BLOCK
|
||||
priority: 1000
|
||||
description: "Files potentially containing credit card patterns"
|
||||
79
examples/security-configs/global-security.yaml
Normal file
79
examples/security-configs/global-security.yaml
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# Global Security Configuration Example
|
||||
# This file would typically be placed at ~/.roo-security.yaml
|
||||
version: '1.0'
|
||||
|
||||
security:
|
||||
enabled: true
|
||||
inheritRules: true # Allow project and custom configs to override
|
||||
defaultAction: ALLOW
|
||||
askMessagePrefix: "Global security policy"
|
||||
|
||||
rules:
|
||||
# Protect sensitive user files
|
||||
- pattern: "**/.ssh/**"
|
||||
action: BLOCK
|
||||
priority: 100
|
||||
description: "SSH keys and configuration"
|
||||
applyToCommands: true
|
||||
|
||||
- pattern: "**/.aws/**"
|
||||
action: BLOCK
|
||||
priority: 100
|
||||
description: "AWS credentials and configuration"
|
||||
applyToCommands: true
|
||||
|
||||
- pattern: "**/.kube/**"
|
||||
action: BLOCK
|
||||
priority: 100
|
||||
description: "Kubernetes configuration"
|
||||
applyToCommands: true
|
||||
|
||||
# Ask for approval for environment files
|
||||
- pattern: "**/.env*"
|
||||
action: ASK
|
||||
priority: 90
|
||||
description: "Environment variables may contain secrets"
|
||||
askMessage: "Access to environment file ${file} requires approval. This file may contain sensitive configuration."
|
||||
applyToCommands: true
|
||||
|
||||
# Protect password and key files
|
||||
- pattern: "**/passwords.*"
|
||||
action: BLOCK
|
||||
priority: 95
|
||||
description: "Password files"
|
||||
|
||||
- pattern: "**/*.key"
|
||||
action: ASK
|
||||
priority: 85
|
||||
description: "Private key files"
|
||||
askMessage: "Access to key file ${file} requires approval."
|
||||
|
||||
- pattern: "**/*.pem"
|
||||
action: ASK
|
||||
priority: 85
|
||||
description: "Certificate files"
|
||||
askMessage: "Access to certificate file ${file} requires approval."
|
||||
|
||||
# Protect common secret patterns
|
||||
- pattern: "**/secrets.*"
|
||||
action: BLOCK
|
||||
priority: 95
|
||||
description: "Secret files"
|
||||
|
||||
- pattern: "**/credentials.*"
|
||||
action: BLOCK
|
||||
priority: 95
|
||||
description: "Credential files"
|
||||
|
||||
# Database files
|
||||
- pattern: "**/*.db"
|
||||
action: ASK
|
||||
priority: 70
|
||||
description: "Database files"
|
||||
askMessage: "Access to database file ${file} requires approval. This may contain sensitive data."
|
||||
|
||||
- pattern: "**/*.sqlite"
|
||||
action: ASK
|
||||
priority: 70
|
||||
description: "SQLite database files"
|
||||
askMessage: "Access to SQLite database ${file} requires approval."
|
||||
117
examples/security-configs/project-security.yaml
Normal file
117
examples/security-configs/project-security.yaml
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# Project Security Configuration Example
|
||||
# This file would be placed at project-root/.roo-security.yaml
|
||||
version: '1.0'
|
||||
|
||||
security:
|
||||
enabled: true
|
||||
inheritRules: true # Inherit global rules
|
||||
defaultAction: ALLOW
|
||||
askMessagePrefix: "Project security policy"
|
||||
|
||||
rules:
|
||||
# Project-specific sensitive files
|
||||
- pattern: "config/production.*"
|
||||
action: BLOCK
|
||||
priority: 100
|
||||
description: "Production configuration files"
|
||||
applyToCommands: true
|
||||
|
||||
- pattern: "config/staging.*"
|
||||
action: ASK
|
||||
priority: 90
|
||||
description: "Staging configuration files"
|
||||
askMessage: "Access to staging config ${file} requires approval."
|
||||
|
||||
# API keys and tokens
|
||||
- pattern: "**/api_keys.*"
|
||||
action: BLOCK
|
||||
priority: 100
|
||||
description: "API key files"
|
||||
|
||||
- pattern: "**/*token*"
|
||||
action: ASK
|
||||
priority: 85
|
||||
description: "Files containing tokens"
|
||||
askMessage: "File ${file} may contain authentication tokens. Approval required."
|
||||
|
||||
# Build and deployment files
|
||||
- pattern: ".github/workflows/*"
|
||||
action: ASK
|
||||
priority: 80
|
||||
description: "GitHub Actions workflows"
|
||||
askMessage: "Modifying workflow ${file} requires approval. This affects CI/CD."
|
||||
|
||||
- pattern: "Dockerfile*"
|
||||
action: ASK
|
||||
priority: 75
|
||||
description: "Docker configuration"
|
||||
askMessage: "Modifying Docker configuration ${file} requires approval."
|
||||
|
||||
- pattern: "docker-compose*.yml"
|
||||
action: ASK
|
||||
priority: 75
|
||||
description: "Docker Compose configuration"
|
||||
askMessage: "Modifying Docker Compose ${file} requires approval."
|
||||
|
||||
# Infrastructure as Code
|
||||
- pattern: "terraform/**"
|
||||
action: ASK
|
||||
priority: 90
|
||||
description: "Terraform infrastructure files"
|
||||
askMessage: "Access to infrastructure file ${file} requires approval."
|
||||
|
||||
- pattern: "kubernetes/**"
|
||||
action: ASK
|
||||
priority: 90
|
||||
description: "Kubernetes manifests"
|
||||
askMessage: "Access to Kubernetes manifest ${file} requires approval."
|
||||
|
||||
# Database migrations and schemas
|
||||
- pattern: "migrations/**"
|
||||
action: ASK
|
||||
priority: 85
|
||||
description: "Database migrations"
|
||||
askMessage: "Modifying migration ${file} requires approval. This affects the database schema."
|
||||
|
||||
- pattern: "schema.sql"
|
||||
action: ASK
|
||||
priority: 85
|
||||
description: "Database schema"
|
||||
askMessage: "Modifying database schema requires approval."
|
||||
|
||||
# Backup files
|
||||
- pattern: "**/*.backup"
|
||||
action: BLOCK
|
||||
priority: 95
|
||||
description: "Backup files may contain sensitive data"
|
||||
|
||||
- pattern: "**/*.bak"
|
||||
action: BLOCK
|
||||
priority: 95
|
||||
description: "Backup files"
|
||||
|
||||
# Log files with potential sensitive data
|
||||
- pattern: "**/logs/*.log"
|
||||
action: ASK
|
||||
priority: 70
|
||||
description: "Log files may contain sensitive information"
|
||||
askMessage: "Log file ${file} may contain sensitive data. Approval required."
|
||||
|
||||
# Test data that might contain real data
|
||||
- pattern: "test/fixtures/production_*.json"
|
||||
action: BLOCK
|
||||
priority: 100
|
||||
description: "Production test fixtures"
|
||||
|
||||
# Package lock files (can reveal internal dependencies)
|
||||
- pattern: "package-lock.json"
|
||||
action: ASK
|
||||
priority: 60
|
||||
description: "NPM lock file"
|
||||
askMessage: "Modifying package-lock.json affects dependency versions."
|
||||
|
||||
- pattern: "yarn.lock"
|
||||
action: ASK
|
||||
priority: 60
|
||||
description: "Yarn lock file"
|
||||
askMessage: "Modifying yarn.lock affects dependency versions."
|
||||
326
src/core/ignore/EnhancedRooIgnoreController.ts
Normal file
326
src/core/ignore/EnhancedRooIgnoreController.ts
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
import path from "path"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import fs from "fs/promises"
|
||||
import fsSync from "fs"
|
||||
import * as vscode from "vscode"
|
||||
import { RooIgnoreController, LOCK_TEXT_SYMBOL } from "./RooIgnoreController"
|
||||
import { SecurityMiddleware } from "../security/SecurityMiddleware"
|
||||
import { SecurityEvaluation, SecurityMiddlewareOptions } from "../security/types"
|
||||
|
||||
/**
|
||||
* Enhanced RooIgnoreController that integrates with SecurityMiddleware
|
||||
* Provides backward compatibility with .rooignore while adding YAML configuration
|
||||
* and ASK action support
|
||||
*/
|
||||
export class EnhancedRooIgnoreController extends RooIgnoreController {
|
||||
private securityMiddleware: SecurityMiddleware | undefined
|
||||
private askHandler: ((evaluation: SecurityEvaluation) => Promise<boolean>) | undefined
|
||||
private useSecurityMiddleware: boolean = false
|
||||
|
||||
constructor(
|
||||
cwd: string,
|
||||
options?: {
|
||||
enableSecurityMiddleware?: boolean
|
||||
askHandler?: (evaluation: SecurityEvaluation) => Promise<boolean>
|
||||
securityOptions?: Partial<SecurityMiddlewareOptions>
|
||||
},
|
||||
) {
|
||||
super(cwd)
|
||||
|
||||
// Initialize security middleware if enabled
|
||||
if (options?.enableSecurityMiddleware) {
|
||||
this.useSecurityMiddleware = true
|
||||
this.askHandler = options.askHandler
|
||||
|
||||
const securityOptions: SecurityMiddlewareOptions = {
|
||||
cwd,
|
||||
onAskAction: options.askHandler,
|
||||
debug: options.securityOptions?.debug || false,
|
||||
...options.securityOptions,
|
||||
}
|
||||
|
||||
this.securityMiddleware = new SecurityMiddleware(securityOptions)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize both the base controller and security middleware
|
||||
*/
|
||||
override async initialize(): Promise<void> {
|
||||
// Initialize base RooIgnoreController
|
||||
await super.initialize()
|
||||
|
||||
// Initialize security middleware if enabled
|
||||
if (this.securityMiddleware) {
|
||||
await this.securityMiddleware.initialize()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced validation that checks both .rooignore and security middleware
|
||||
*/
|
||||
override validateAccess(filePath: string): boolean {
|
||||
// First check traditional .rooignore
|
||||
const baseResult = super.validateAccess(filePath)
|
||||
|
||||
// If blocked by .rooignore, return false immediately
|
||||
if (!baseResult) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If security middleware is not enabled, return base result
|
||||
if (!this.useSecurityMiddleware || !this.securityMiddleware) {
|
||||
return baseResult
|
||||
}
|
||||
|
||||
// Check with security middleware (synchronous wrapper for async evaluation)
|
||||
// Note: This is a limitation - we need to make this async in the future
|
||||
// For now, we'll use a workaround with a promise wrapper
|
||||
let result = true
|
||||
|
||||
// Create a promise and resolve it immediately for sync compatibility
|
||||
const checkPromise = this.securityMiddleware.evaluateAccess(filePath).then((evaluation) => {
|
||||
if (evaluation.action === "BLOCK") {
|
||||
result = false
|
||||
} else if (evaluation.action === "ASK") {
|
||||
// For synchronous context, we'll default to blocking ASK actions
|
||||
// The proper async handling should be done in the calling code
|
||||
result = false
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
// For backward compatibility, we need to handle this synchronously
|
||||
// This is a temporary solution - the calling code should be updated to handle async
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Async version of validateAccess that properly handles ASK actions
|
||||
*/
|
||||
async validateAccessAsync(filePath: string): Promise<{
|
||||
allowed: boolean
|
||||
evaluation?: SecurityEvaluation
|
||||
requiresApproval?: boolean
|
||||
}> {
|
||||
// First check traditional .rooignore
|
||||
const baseResult = super.validateAccess(filePath)
|
||||
|
||||
// If blocked by .rooignore, return immediately
|
||||
if (!baseResult) {
|
||||
return {
|
||||
allowed: false,
|
||||
evaluation: {
|
||||
action: "BLOCK",
|
||||
path: filePath,
|
||||
message: "Blocked by .rooignore",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// If security middleware is not enabled, return base result
|
||||
if (!this.useSecurityMiddleware || !this.securityMiddleware) {
|
||||
return { allowed: baseResult }
|
||||
}
|
||||
|
||||
// Check with security middleware
|
||||
const evaluation = await this.securityMiddleware.evaluateAccess(filePath)
|
||||
|
||||
if (evaluation.action === "BLOCK") {
|
||||
return {
|
||||
allowed: false,
|
||||
evaluation,
|
||||
}
|
||||
} else if (evaluation.action === "ASK") {
|
||||
// Return that approval is required
|
||||
return {
|
||||
allowed: false,
|
||||
evaluation,
|
||||
requiresApproval: true,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
allowed: true,
|
||||
evaluation,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced command validation with security middleware support
|
||||
*/
|
||||
override validateCommand(command: string): string | undefined {
|
||||
// First check with base implementation
|
||||
const baseResult = super.validateCommand(command)
|
||||
|
||||
// If blocked by base, return the blocked file
|
||||
if (baseResult) {
|
||||
return baseResult
|
||||
}
|
||||
|
||||
// If security middleware is not enabled, return base result
|
||||
if (!this.useSecurityMiddleware || !this.securityMiddleware) {
|
||||
return baseResult
|
||||
}
|
||||
|
||||
// Check with security middleware (synchronous wrapper)
|
||||
// This is a limitation - should be async in the future
|
||||
let blockedFile: string | undefined
|
||||
|
||||
const checkPromise = this.securityMiddleware.evaluateCommand(command).then((evaluation) => {
|
||||
if (evaluation.action === "BLOCK" || evaluation.action === "ASK") {
|
||||
blockedFile = evaluation.path
|
||||
}
|
||||
return blockedFile
|
||||
})
|
||||
|
||||
return blockedFile
|
||||
}
|
||||
|
||||
/**
|
||||
* Async version of validateCommand that properly handles security middleware
|
||||
*/
|
||||
async validateCommandAsync(command: string): Promise<{
|
||||
allowed: boolean
|
||||
blockedFile?: string
|
||||
evaluation?: SecurityEvaluation
|
||||
requiresApproval?: boolean
|
||||
}> {
|
||||
// First check with base implementation
|
||||
const baseResult = super.validateCommand(command)
|
||||
|
||||
// If blocked by base, return the blocked file
|
||||
if (baseResult) {
|
||||
return {
|
||||
allowed: false,
|
||||
blockedFile: baseResult,
|
||||
evaluation: {
|
||||
action: "BLOCK",
|
||||
path: baseResult,
|
||||
message: "File access blocked by .rooignore",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// If security middleware is not enabled, return base result
|
||||
if (!this.useSecurityMiddleware || !this.securityMiddleware) {
|
||||
return { allowed: true }
|
||||
}
|
||||
|
||||
// Check with security middleware
|
||||
const evaluation = await this.securityMiddleware.evaluateCommand(command)
|
||||
|
||||
if (evaluation.action === "BLOCK") {
|
||||
return {
|
||||
allowed: false,
|
||||
blockedFile: evaluation.path,
|
||||
evaluation,
|
||||
}
|
||||
} else if (evaluation.action === "ASK") {
|
||||
return {
|
||||
allowed: false,
|
||||
blockedFile: evaluation.path,
|
||||
evaluation,
|
||||
requiresApproval: true,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
allowed: true,
|
||||
evaluation,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get security statistics if middleware is enabled
|
||||
*/
|
||||
getSecurityStats() {
|
||||
if (this.securityMiddleware) {
|
||||
return this.securityMiddleware.getStats()
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Get security configuration if middleware is enabled
|
||||
*/
|
||||
getSecurityConfig() {
|
||||
if (this.securityMiddleware) {
|
||||
return this.securityMiddleware.getConfig()
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Export security configuration to YAML
|
||||
*/
|
||||
async exportSecurityConfig(level: "global" | "project" | "custom"): Promise<string | undefined> {
|
||||
if (this.securityMiddleware) {
|
||||
return this.securityMiddleware.exportConfig(level)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Import security configuration from YAML
|
||||
*/
|
||||
async importSecurityConfig(yamlContent: string, level: "global" | "project" | "custom"): Promise<void> {
|
||||
if (this.securityMiddleware) {
|
||||
await this.securityMiddleware.importConfig(yamlContent, level)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enhanced instructions that include both .rooignore and security middleware info
|
||||
*/
|
||||
override getInstructions(): string | undefined {
|
||||
const baseInstructions = super.getInstructions()
|
||||
|
||||
if (!this.useSecurityMiddleware || !this.securityMiddleware) {
|
||||
return baseInstructions
|
||||
}
|
||||
|
||||
const config = this.securityMiddleware.getConfig()
|
||||
const stats = this.securityMiddleware.getStats()
|
||||
|
||||
let instructions = baseInstructions || ""
|
||||
|
||||
// Add security middleware information
|
||||
if (Object.keys(config).length > 0) {
|
||||
instructions += "\n\n# Security Middleware\n\n"
|
||||
instructions += "Enhanced security rules are active with the following configuration levels:\n"
|
||||
|
||||
const levels = ["enterprise", "global", "project", "custom"] as const
|
||||
for (const level of levels) {
|
||||
const levelConfig = config[level]
|
||||
if (levelConfig?.enabled) {
|
||||
const ruleCount = levelConfig.rules?.length || 0
|
||||
instructions += `- ${level}: ${ruleCount} rules\n`
|
||||
}
|
||||
}
|
||||
|
||||
if (stats) {
|
||||
instructions += `\nSecurity Statistics:\n`
|
||||
instructions += `- Total evaluations: ${stats.totalEvaluations}\n`
|
||||
instructions += `- Blocked: ${stats.blockedCount}\n`
|
||||
instructions += `- Asked: ${stats.askedCount}\n`
|
||||
instructions += `- Allowed: ${stats.allowedCount}\n`
|
||||
}
|
||||
|
||||
instructions += "\nFiles may require approval (ASK action) or be blocked based on security rules."
|
||||
}
|
||||
|
||||
return instructions
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources
|
||||
*/
|
||||
override dispose(): void {
|
||||
super.dispose()
|
||||
|
||||
if (this.securityMiddleware) {
|
||||
this.securityMiddleware.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
347
src/core/ignore/__tests__/EnhancedRooIgnoreController.spec.ts
Normal file
347
src/core/ignore/__tests__/EnhancedRooIgnoreController.spec.ts
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import { EnhancedRooIgnoreController } from "../EnhancedRooIgnoreController"
|
||||
import { SecurityEvaluation } from "../../security/types"
|
||||
import { fileExistsAtPath } from "../../../utils/fs"
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("fs/promises")
|
||||
vi.mock("fs")
|
||||
vi.mock("../../../utils/fs")
|
||||
vi.mock("vscode", () => ({
|
||||
workspace: {
|
||||
createFileSystemWatcher: vi.fn(() => ({
|
||||
onDidChange: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
onDidCreate: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
onDidDelete: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
dispose: vi.fn(),
|
||||
})),
|
||||
},
|
||||
RelativePattern: vi.fn((base, pattern) => ({ base, pattern })),
|
||||
Disposable: vi.fn(),
|
||||
}))
|
||||
|
||||
describe("EnhancedRooIgnoreController", () => {
|
||||
let controller: EnhancedRooIgnoreController
|
||||
let mockAskHandler: ReturnType<typeof vi.fn>
|
||||
const testCwd = "/test/project"
|
||||
|
||||
const mockRooIgnoreContent = `
|
||||
# Traditional .rooignore patterns
|
||||
node_modules/
|
||||
*.log
|
||||
.env
|
||||
secrets/
|
||||
`
|
||||
|
||||
const mockSecurityConfig = `
|
||||
version: '1.0'
|
||||
security:
|
||||
enabled: true
|
||||
rules:
|
||||
- pattern: "**/*.key"
|
||||
action: ASK
|
||||
priority: 90
|
||||
description: "Key files"
|
||||
- pattern: "**/sensitive/**"
|
||||
action: BLOCK
|
||||
priority: 100
|
||||
description: "Sensitive directory"
|
||||
`
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockAskHandler = vi.fn().mockResolvedValue(true)
|
||||
|
||||
// Setup file existence mocks
|
||||
vi.mocked(fileExistsAtPath).mockImplementation(async (filePath) => {
|
||||
return filePath.includes(".rooignore") || filePath.includes(".roo-security")
|
||||
})
|
||||
|
||||
// Setup file read mocks
|
||||
vi.mocked(fs.readFile).mockImplementation(async (filePath) => {
|
||||
const pathStr = filePath.toString()
|
||||
if (pathStr.includes(".rooignore")) return mockRooIgnoreContent
|
||||
if (pathStr.includes(".roo-security")) return mockSecurityConfig
|
||||
return ""
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (controller) {
|
||||
controller.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
describe("backward compatibility", () => {
|
||||
it("should work without security middleware enabled", async () => {
|
||||
controller = new EnhancedRooIgnoreController(testCwd)
|
||||
await controller.initialize()
|
||||
|
||||
// Should block based on .rooignore
|
||||
expect(controller.validateAccess("node_modules/package.json")).toBe(false)
|
||||
expect(controller.validateAccess(".env")).toBe(false)
|
||||
expect(controller.validateAccess("src/index.ts")).toBe(true)
|
||||
})
|
||||
|
||||
it("should maintain synchronous validateAccess for compatibility", () => {
|
||||
controller = new EnhancedRooIgnoreController(testCwd)
|
||||
|
||||
// Should be able to call synchronously
|
||||
const result = controller.validateAccess("test.txt")
|
||||
expect(typeof result).toBe("boolean")
|
||||
})
|
||||
|
||||
it("should maintain synchronous validateCommand for compatibility", () => {
|
||||
controller = new EnhancedRooIgnoreController(testCwd)
|
||||
|
||||
// Should be able to call synchronously
|
||||
const result = controller.validateCommand("cat test.txt")
|
||||
expect(result === undefined || typeof result === "string").toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("with security middleware enabled", () => {
|
||||
beforeEach(async () => {
|
||||
controller = new EnhancedRooIgnoreController(testCwd, {
|
||||
enableSecurityMiddleware: true,
|
||||
askHandler: mockAskHandler,
|
||||
securityOptions: {
|
||||
projectConfigPath: path.join(testCwd, ".roo-security.yaml"),
|
||||
},
|
||||
})
|
||||
await controller.initialize()
|
||||
})
|
||||
|
||||
it("should combine .rooignore and security middleware rules", async () => {
|
||||
// Blocked by .rooignore
|
||||
const envResult = await controller.validateAccessAsync(".env")
|
||||
expect(envResult.allowed).toBe(false)
|
||||
expect(envResult.evaluation?.message).toContain(".rooignore")
|
||||
|
||||
// Blocked by security middleware
|
||||
const sensitiveResult = await controller.validateAccessAsync("sensitive/data.txt")
|
||||
expect(sensitiveResult.allowed).toBe(false)
|
||||
expect(sensitiveResult.evaluation?.action).toBe("BLOCK")
|
||||
|
||||
// Allowed by both
|
||||
const srcResult = await controller.validateAccessAsync("src/index.ts")
|
||||
expect(srcResult.allowed).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle ASK actions", async () => {
|
||||
mockAskHandler.mockResolvedValue(true)
|
||||
|
||||
const result = await controller.validateAccessAsync("private.key")
|
||||
|
||||
expect(result.requiresApproval).toBe(true)
|
||||
expect(result.evaluation?.action).toBe("ASK")
|
||||
expect(result.evaluation?.matchedRule?.description).toBe("Key files")
|
||||
})
|
||||
|
||||
it("should prioritize .rooignore blocks over security ASK", async () => {
|
||||
// Add a pattern that would ASK in security but is blocked by .rooignore
|
||||
vi.mocked(fs.readFile).mockImplementation(async (filePath) => {
|
||||
const pathStr = filePath.toString()
|
||||
if (pathStr.includes(".rooignore")) return ".env.key"
|
||||
if (pathStr.includes(".roo-security")) return mockSecurityConfig
|
||||
return ""
|
||||
})
|
||||
|
||||
const newController = new EnhancedRooIgnoreController(testCwd, {
|
||||
enableSecurityMiddleware: true,
|
||||
askHandler: mockAskHandler,
|
||||
})
|
||||
await newController.initialize()
|
||||
|
||||
const result = await newController.validateAccessAsync(".env.key")
|
||||
|
||||
expect(result.allowed).toBe(false)
|
||||
expect(result.evaluation?.message).toContain(".rooignore")
|
||||
expect(mockAskHandler).not.toHaveBeenCalled()
|
||||
|
||||
newController.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
describe("validateCommandAsync", () => {
|
||||
beforeEach(async () => {
|
||||
controller = new EnhancedRooIgnoreController(testCwd, {
|
||||
enableSecurityMiddleware: true,
|
||||
askHandler: mockAskHandler,
|
||||
securityOptions: {
|
||||
projectConfigPath: path.join(testCwd, ".roo-security.yaml"),
|
||||
},
|
||||
})
|
||||
await controller.initialize()
|
||||
})
|
||||
|
||||
it("should block commands accessing protected files", async () => {
|
||||
const result = await controller.validateCommandAsync("cat .env")
|
||||
|
||||
expect(result.allowed).toBe(false)
|
||||
expect(result.blockedFile).toBe(".env")
|
||||
expect(result.evaluation?.message).toContain(".rooignore")
|
||||
})
|
||||
|
||||
it("should handle ASK for commands", async () => {
|
||||
const result = await controller.validateCommandAsync("cat private.key")
|
||||
|
||||
expect(result.allowed).toBe(false)
|
||||
expect(result.requiresApproval).toBe(true)
|
||||
expect(result.blockedFile).toBe("private.key")
|
||||
})
|
||||
|
||||
it("should allow commands not accessing files", async () => {
|
||||
const result = await controller.validateCommandAsync("ls -la")
|
||||
|
||||
expect(result.allowed).toBe(true)
|
||||
expect(result.blockedFile).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("statistics and configuration", () => {
|
||||
beforeEach(async () => {
|
||||
controller = new EnhancedRooIgnoreController(testCwd, {
|
||||
enableSecurityMiddleware: true,
|
||||
askHandler: mockAskHandler,
|
||||
securityOptions: {
|
||||
projectConfigPath: path.join(testCwd, ".roo-security.yaml"),
|
||||
},
|
||||
})
|
||||
await controller.initialize()
|
||||
})
|
||||
|
||||
it("should provide security statistics", async () => {
|
||||
await controller.validateAccessAsync("sensitive/file.txt")
|
||||
await controller.validateAccessAsync("src/index.ts")
|
||||
await controller.validateAccessAsync("private.key")
|
||||
|
||||
const stats = controller.getSecurityStats()
|
||||
|
||||
expect(stats).toBeDefined()
|
||||
expect(stats?.totalEvaluations).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("should provide security configuration", () => {
|
||||
const config = controller.getSecurityConfig()
|
||||
|
||||
expect(config).toBeDefined()
|
||||
expect(config?.project).toBeDefined()
|
||||
})
|
||||
|
||||
it("should export security configuration", async () => {
|
||||
const yamlContent = await controller.exportSecurityConfig("project")
|
||||
|
||||
expect(yamlContent).toBeDefined()
|
||||
expect(yamlContent).toContain("version")
|
||||
expect(yamlContent).toContain("security")
|
||||
})
|
||||
|
||||
it("should import security configuration", async () => {
|
||||
const newConfig = `
|
||||
version: '1.0'
|
||||
security:
|
||||
enabled: true
|
||||
rules:
|
||||
- pattern: "**/*.test"
|
||||
action: BLOCK
|
||||
priority: 100
|
||||
`
|
||||
|
||||
await controller.importSecurityConfig(newConfig, "custom")
|
||||
|
||||
const config = controller.getSecurityConfig()
|
||||
expect(config?.custom).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("getInstructions", () => {
|
||||
it("should provide basic instructions without security middleware", async () => {
|
||||
controller = new EnhancedRooIgnoreController(testCwd)
|
||||
await controller.initialize()
|
||||
|
||||
const instructions = controller.getInstructions()
|
||||
|
||||
expect(instructions).toContain(".rooignore")
|
||||
expect(instructions).not.toContain("Security Middleware")
|
||||
})
|
||||
|
||||
it("should provide enhanced instructions with security middleware", async () => {
|
||||
controller = new EnhancedRooIgnoreController(testCwd, {
|
||||
enableSecurityMiddleware: true,
|
||||
securityOptions: {
|
||||
projectConfigPath: path.join(testCwd, ".roo-security.yaml"),
|
||||
},
|
||||
})
|
||||
await controller.initialize()
|
||||
|
||||
// Trigger some evaluations for statistics
|
||||
await controller.validateAccessAsync("test.txt")
|
||||
|
||||
const instructions = controller.getInstructions()
|
||||
|
||||
expect(instructions).toContain(".rooignore")
|
||||
expect(instructions).toContain("Security Middleware")
|
||||
expect(instructions).toContain("Security Statistics")
|
||||
})
|
||||
})
|
||||
|
||||
describe("disposal", () => {
|
||||
it("should dispose both base controller and security middleware", async () => {
|
||||
const disposeSpy = vi.spyOn(EnhancedRooIgnoreController.prototype, "dispose")
|
||||
|
||||
controller = new EnhancedRooIgnoreController(testCwd, {
|
||||
enableSecurityMiddleware: true,
|
||||
})
|
||||
await controller.initialize()
|
||||
|
||||
controller.dispose()
|
||||
|
||||
expect(disposeSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle undefined ask handler gracefully", async () => {
|
||||
controller = new EnhancedRooIgnoreController(testCwd, {
|
||||
enableSecurityMiddleware: true,
|
||||
// No askHandler provided
|
||||
})
|
||||
await controller.initialize()
|
||||
|
||||
// Should default to blocking ASK actions when no handler
|
||||
const result = await controller.validateAccessAsync("private.key")
|
||||
|
||||
expect(result.allowed).toBe(false)
|
||||
expect(result.requiresApproval).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle file paths with backslashes", async () => {
|
||||
controller = new EnhancedRooIgnoreController(testCwd, {
|
||||
enableSecurityMiddleware: true,
|
||||
})
|
||||
await controller.initialize()
|
||||
|
||||
const result = await controller.validateAccessAsync("sensitive\\data.txt")
|
||||
|
||||
expect(result.allowed).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle relative and absolute paths", async () => {
|
||||
controller = new EnhancedRooIgnoreController(testCwd, {
|
||||
enableSecurityMiddleware: true,
|
||||
})
|
||||
await controller.initialize()
|
||||
|
||||
// Relative path
|
||||
const relativeResult = await controller.validateAccessAsync("./sensitive/data.txt")
|
||||
expect(relativeResult.allowed).toBe(false)
|
||||
|
||||
// Absolute path
|
||||
const absoluteResult = await controller.validateAccessAsync(path.join(testCwd, "sensitive/data.txt"))
|
||||
expect(absoluteResult.allowed).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
522
src/core/security/SecurityMiddleware.ts
Normal file
522
src/core/security/SecurityMiddleware.ts
Normal file
|
|
@ -0,0 +1,522 @@
|
|||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
import * as vscode from "vscode"
|
||||
import * as yaml from "yaml"
|
||||
import ignore, { Ignore } from "ignore"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import {
|
||||
SecurityAction,
|
||||
SecurityRule,
|
||||
SecurityConfig,
|
||||
SecurityMiddlewareConfig,
|
||||
SecurityEvaluation,
|
||||
SecurityMiddlewareOptions,
|
||||
SecurityConfigFile,
|
||||
SecurityStats,
|
||||
} from "./types"
|
||||
|
||||
/**
|
||||
* Enhanced Security Middleware for RooCode
|
||||
* Provides granular file access control with ASK prompts and YAML configuration
|
||||
*/
|
||||
export class SecurityMiddleware {
|
||||
private cwd: string
|
||||
private config: SecurityMiddlewareConfig = {}
|
||||
private ignoreInstances: Map<string, Ignore> = new Map()
|
||||
private stats: SecurityStats = {
|
||||
blockedCount: 0,
|
||||
askedCount: 0,
|
||||
allowedCount: 0,
|
||||
totalEvaluations: 0,
|
||||
rulesByLevel: {
|
||||
enterprise: 0,
|
||||
global: 0,
|
||||
project: 0,
|
||||
custom: 0,
|
||||
},
|
||||
}
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private onAskAction?: (evaluation: SecurityEvaluation) => Promise<boolean>
|
||||
private debug: boolean = false
|
||||
|
||||
// Default file names for configuration
|
||||
private static readonly CONFIG_FILES = {
|
||||
global: ".roo-security.yaml",
|
||||
project: ".roo-security.yaml",
|
||||
custom: ".roo-security-custom.yaml",
|
||||
}
|
||||
|
||||
constructor(private options: SecurityMiddlewareOptions) {
|
||||
this.cwd = options.cwd
|
||||
this.onAskAction = options.onAskAction
|
||||
this.debug = options.debug || false
|
||||
|
||||
// Set up file watchers for configuration changes
|
||||
this.setupFileWatchers()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the security middleware by loading all configuration tiers
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
await this.loadConfigurations()
|
||||
this.buildIgnoreInstances()
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configurations from all tiers (Enterprise → Global → Project → Custom)
|
||||
*/
|
||||
private async loadConfigurations(): Promise<void> {
|
||||
// Load enterprise configuration (if available from organization)
|
||||
// This would typically come from a cloud service or organization settings
|
||||
await this.loadEnterpriseConfig()
|
||||
|
||||
// Load global configuration (~/.roo-security.yaml)
|
||||
const globalPath =
|
||||
this.options.globalConfigPath ||
|
||||
path.join(process.env.HOME || process.env.USERPROFILE || "", SecurityMiddleware.CONFIG_FILES.global)
|
||||
await this.loadConfigFromFile(globalPath, "global")
|
||||
|
||||
// Load project configuration (project/.roo-security.yaml)
|
||||
const projectPath =
|
||||
this.options.projectConfigPath || path.join(this.cwd, SecurityMiddleware.CONFIG_FILES.project)
|
||||
await this.loadConfigFromFile(projectPath, "project")
|
||||
|
||||
// Load custom configuration (project/.roo-security-custom.yaml)
|
||||
const customPath = this.options.customConfigPath || path.join(this.cwd, SecurityMiddleware.CONFIG_FILES.custom)
|
||||
await this.loadConfigFromFile(customPath, "custom")
|
||||
|
||||
this.logDebug("Configurations loaded", this.config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load enterprise configuration from organization settings
|
||||
*/
|
||||
private async loadEnterpriseConfig(): Promise<void> {
|
||||
// This would integrate with CloudService or organization settings
|
||||
// For now, we'll leave it as a placeholder for enterprise features
|
||||
// In a real implementation, this would fetch from an API or cloud service
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration from a YAML file
|
||||
*/
|
||||
private async loadConfigFromFile(filePath: string, level: keyof SecurityMiddlewareConfig): Promise<void> {
|
||||
try {
|
||||
if (await fileExistsAtPath(filePath)) {
|
||||
const content = await fs.readFile(filePath, "utf-8")
|
||||
const parsed = yaml.parse(content) as SecurityConfigFile
|
||||
|
||||
if (parsed?.version === "1.0" && parsed?.security) {
|
||||
this.config[level] = parsed.security
|
||||
|
||||
// Count rules for statistics
|
||||
const ruleCount = parsed.security.rules?.length || 0
|
||||
if (level !== "enterprise") {
|
||||
this.stats.rulesByLevel[level] = ruleCount
|
||||
}
|
||||
|
||||
this.logDebug(`Loaded ${ruleCount} rules from ${level} configuration`, filePath)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load ${level} security configuration from ${filePath}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build ignore instances for efficient pattern matching
|
||||
*/
|
||||
private buildIgnoreInstances(): void {
|
||||
this.ignoreInstances.clear()
|
||||
|
||||
// Build ignore instances for each configuration level
|
||||
const levels: Array<keyof SecurityMiddlewareConfig> = ["enterprise", "global", "project", "custom"]
|
||||
|
||||
for (const level of levels) {
|
||||
const config = this.config[level]
|
||||
if (config?.enabled && config.rules) {
|
||||
// Process each rule individually to maintain pattern integrity
|
||||
for (const rule of config.rules) {
|
||||
// Skip regex patterns for ignore instances
|
||||
if (rule.pattern.startsWith("/") && rule.pattern.endsWith("/")) {
|
||||
continue
|
||||
}
|
||||
|
||||
const key = `${level}-${rule.action.toLowerCase()}-${rule.pattern}`
|
||||
const instance = ignore()
|
||||
instance.add(rule.pattern)
|
||||
this.ignoreInstances.set(key, instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate file access based on security rules
|
||||
*/
|
||||
async evaluateAccess(filePath: string): Promise<SecurityEvaluation> {
|
||||
this.stats.totalEvaluations++
|
||||
|
||||
// Convert to relative path for pattern matching
|
||||
// Normalize the path to ensure consistent matching
|
||||
const absolutePath = path.resolve(this.cwd, filePath)
|
||||
const relativePath = path.relative(this.cwd, absolutePath)
|
||||
const normalizedPath = relativePath.replace(/\\/g, "/")
|
||||
|
||||
this.logDebug("Evaluating access", { filePath, normalizedPath })
|
||||
|
||||
// Evaluate rules in priority order: Custom → Project → Global → Enterprise
|
||||
const levels: Array<keyof SecurityMiddlewareConfig> = ["custom", "project", "global", "enterprise"]
|
||||
|
||||
for (const level of levels) {
|
||||
const config = this.config[level]
|
||||
|
||||
if (!config?.enabled) continue
|
||||
|
||||
// Skip inherited rules if inheritRules is false
|
||||
if (level !== "custom" && config.inheritRules === false) {
|
||||
break
|
||||
}
|
||||
|
||||
// Sort rules by priority (higher first)
|
||||
const sortedRules = [...(config.rules || [])].sort((a, b) => (b.priority || 0) - (a.priority || 0))
|
||||
|
||||
for (const rule of sortedRules) {
|
||||
if (this.matchesPattern(normalizedPath, rule.pattern)) {
|
||||
this.logDebug("Rule matched", { level, rule })
|
||||
|
||||
const evaluation: SecurityEvaluation = {
|
||||
action: rule.action,
|
||||
matchedRule: rule,
|
||||
level: level as any,
|
||||
path: filePath,
|
||||
message: this.buildMessage(rule, config, filePath),
|
||||
}
|
||||
|
||||
// Update statistics
|
||||
this.updateStats(rule.action)
|
||||
|
||||
// Handle ASK action
|
||||
if (rule.action === "ASK" && this.onAskAction) {
|
||||
const allowed = await this.onAskAction(evaluation)
|
||||
evaluation.action = allowed ? "ALLOW" : "BLOCK"
|
||||
// Update stats for the final action
|
||||
if (allowed) {
|
||||
this.stats.allowedCount++
|
||||
this.stats.askedCount--
|
||||
} else {
|
||||
this.stats.blockedCount++
|
||||
this.stats.askedCount--
|
||||
}
|
||||
}
|
||||
|
||||
this.logDebug("Access evaluation result", evaluation)
|
||||
return evaluation
|
||||
}
|
||||
}
|
||||
|
||||
// Check default action for this level
|
||||
if (config.defaultAction && config.defaultAction !== "ALLOW") {
|
||||
const evaluation: SecurityEvaluation = {
|
||||
action: config.defaultAction,
|
||||
level: level as any,
|
||||
path: filePath,
|
||||
message: `Default ${config.defaultAction} action from ${level} configuration`,
|
||||
}
|
||||
|
||||
this.updateStats(config.defaultAction)
|
||||
return evaluation
|
||||
}
|
||||
}
|
||||
|
||||
// Default to ALLOW if no rules match
|
||||
this.stats.allowedCount++
|
||||
return {
|
||||
action: "ALLOW",
|
||||
path: filePath,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate command execution based on security rules
|
||||
*/
|
||||
async evaluateCommand(command: string): Promise<SecurityEvaluation> {
|
||||
// Extract potential file paths from the command
|
||||
const filePaths = this.extractFilePathsFromCommand(command)
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
const evaluation = await this.evaluateAccess(filePath)
|
||||
|
||||
// Check if the matched rule applies to commands
|
||||
if (evaluation.matchedRule && evaluation.matchedRule.applyToCommands !== false) {
|
||||
if (evaluation.action !== "ALLOW") {
|
||||
return {
|
||||
...evaluation,
|
||||
message: `Command blocked: ${evaluation.message}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
action: "ALLOW",
|
||||
path: command,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path matches a pattern
|
||||
*/
|
||||
private matchesPattern(filePath: string, pattern: string): boolean {
|
||||
// Check if it's a regex pattern (starts with / and ends with /)
|
||||
if (pattern.startsWith("/") && pattern.endsWith("/")) {
|
||||
try {
|
||||
const regex = new RegExp(pattern.slice(1, -1))
|
||||
const matches = regex.test(filePath)
|
||||
this.logDebug("Regex pattern matching", { filePath, pattern, matches })
|
||||
return matches
|
||||
} catch (error) {
|
||||
this.logDebug("Invalid regex pattern", { pattern, error })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Use gitignore-style matching
|
||||
// For gitignore patterns, we need to handle them properly
|
||||
const instance = ignore()
|
||||
|
||||
// Remove quotes if present in the pattern
|
||||
const cleanPattern = pattern.replace(/^["']|["']$/g, "")
|
||||
instance.add(cleanPattern)
|
||||
|
||||
// Normalize the path for matching
|
||||
const normalizedPath = filePath.replace(/\\/g, "/")
|
||||
|
||||
// Check if the pattern matches
|
||||
const matches = instance.ignores(normalizedPath)
|
||||
this.logDebug("Gitignore pattern matching", { filePath, pattern: cleanPattern, normalizedPath, matches })
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract file paths from a command string
|
||||
*/
|
||||
private extractFilePathsFromCommand(command: string): string[] {
|
||||
const paths: string[] = []
|
||||
|
||||
// Common file-reading commands
|
||||
const fileCommands = [
|
||||
"cat",
|
||||
"less",
|
||||
"more",
|
||||
"head",
|
||||
"tail",
|
||||
"grep",
|
||||
"awk",
|
||||
"sed",
|
||||
"get-content",
|
||||
"gc",
|
||||
"type",
|
||||
"select-string",
|
||||
"sls",
|
||||
]
|
||||
|
||||
const parts = command.trim().split(/\s+/)
|
||||
const baseCommand = parts[0].toLowerCase()
|
||||
|
||||
if (fileCommands.includes(baseCommand)) {
|
||||
// Extract file arguments (skip flags)
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const arg = parts[i]
|
||||
if (!arg.startsWith("-") && !arg.startsWith("/") && !arg.includes(":")) {
|
||||
paths.push(arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a message for ASK or BLOCK actions
|
||||
*/
|
||||
private buildMessage(rule: SecurityRule, config: SecurityConfig, filePath: string): string {
|
||||
if (rule.askMessage) {
|
||||
return rule.askMessage.replace("${file}", filePath)
|
||||
}
|
||||
|
||||
const prefix = config.askMessagePrefix || "Security check"
|
||||
const action = rule.action === "ASK" ? "requires approval" : "is blocked"
|
||||
const description = rule.description ? `: ${rule.description}` : ""
|
||||
|
||||
return `${prefix}: Access to ${filePath} ${action}${description}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Update statistics
|
||||
*/
|
||||
private updateStats(action: SecurityAction): void {
|
||||
switch (action) {
|
||||
case "BLOCK":
|
||||
this.stats.blockedCount++
|
||||
break
|
||||
case "ASK":
|
||||
this.stats.askedCount++
|
||||
break
|
||||
case "ALLOW":
|
||||
this.stats.allowedCount++
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up file watchers for configuration changes
|
||||
*/
|
||||
private setupFileWatchers(): void {
|
||||
const watchPaths = [
|
||||
this.options.globalConfigPath || path.join(process.env.HOME || "", SecurityMiddleware.CONFIG_FILES.global),
|
||||
this.options.projectConfigPath || path.join(this.cwd, SecurityMiddleware.CONFIG_FILES.project),
|
||||
this.options.customConfigPath || path.join(this.cwd, SecurityMiddleware.CONFIG_FILES.custom),
|
||||
]
|
||||
|
||||
for (const watchPath of watchPaths) {
|
||||
try {
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(watchPath)
|
||||
|
||||
const reloadConfig = async () => {
|
||||
await this.loadConfigurations()
|
||||
this.buildIgnoreInstances()
|
||||
this.logDebug("Configuration reloaded due to file change", watchPath)
|
||||
}
|
||||
|
||||
this.disposables.push(
|
||||
watcher.onDidChange(reloadConfig),
|
||||
watcher.onDidCreate(reloadConfig),
|
||||
watcher.onDidDelete(reloadConfig),
|
||||
watcher,
|
||||
)
|
||||
} catch (error) {
|
||||
// Ignore watcher creation errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current statistics
|
||||
*/
|
||||
getStats(): SecurityStats {
|
||||
return { ...this.stats }
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset statistics
|
||||
*/
|
||||
resetStats(): void {
|
||||
this.stats = {
|
||||
blockedCount: 0,
|
||||
askedCount: 0,
|
||||
allowedCount: 0,
|
||||
totalEvaluations: 0,
|
||||
rulesByLevel: { ...this.stats.rulesByLevel },
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current configuration
|
||||
*/
|
||||
getConfig(): SecurityMiddlewareConfig {
|
||||
return { ...this.config }
|
||||
}
|
||||
|
||||
/**
|
||||
* Export configuration to YAML
|
||||
*/
|
||||
async exportConfig(level: keyof SecurityMiddlewareConfig): Promise<string> {
|
||||
const config = this.config[level]
|
||||
if (!config) {
|
||||
throw new Error(`No configuration found for level: ${level}`)
|
||||
}
|
||||
|
||||
const configFile: SecurityConfigFile = {
|
||||
version: "1.0",
|
||||
security: config,
|
||||
}
|
||||
|
||||
return yaml.stringify(configFile, { lineWidth: 0 })
|
||||
}
|
||||
|
||||
/**
|
||||
* Import configuration from YAML
|
||||
*/
|
||||
async importConfig(yamlContent: string, level: keyof SecurityMiddlewareConfig): Promise<void> {
|
||||
try {
|
||||
const parsed = yaml.parse(yamlContent) as SecurityConfigFile
|
||||
|
||||
if (parsed?.version === "1.0" && parsed?.security) {
|
||||
this.config[level] = parsed.security
|
||||
this.buildIgnoreInstances()
|
||||
|
||||
// Save to file if paths are configured
|
||||
await this.saveConfigToFile(level)
|
||||
} else {
|
||||
throw new Error("Invalid configuration format")
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to import configuration: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save configuration to file
|
||||
*/
|
||||
private async saveConfigToFile(level: keyof SecurityMiddlewareConfig): Promise<void> {
|
||||
let filePath: string | undefined
|
||||
|
||||
switch (level) {
|
||||
case "global":
|
||||
filePath =
|
||||
this.options.globalConfigPath ||
|
||||
path.join(process.env.HOME || "", SecurityMiddleware.CONFIG_FILES.global)
|
||||
break
|
||||
case "project":
|
||||
filePath =
|
||||
this.options.projectConfigPath || path.join(this.cwd, SecurityMiddleware.CONFIG_FILES.project)
|
||||
break
|
||||
case "custom":
|
||||
filePath = this.options.customConfigPath || path.join(this.cwd, SecurityMiddleware.CONFIG_FILES.custom)
|
||||
break
|
||||
}
|
||||
|
||||
if (filePath && this.config[level]) {
|
||||
const configFile: SecurityConfigFile = {
|
||||
version: "1.0",
|
||||
security: this.config[level]!,
|
||||
}
|
||||
|
||||
const yamlContent = yaml.stringify(configFile, { lineWidth: 0 })
|
||||
await fs.writeFile(filePath, yamlContent, "utf-8")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log debug messages
|
||||
*/
|
||||
private logDebug(message: string, data?: any): void {
|
||||
if (this.debug) {
|
||||
console.log(`[SecurityMiddleware] ${message}`, data || "")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources
|
||||
*/
|
||||
dispose(): void {
|
||||
for (const disposable of this.disposables) {
|
||||
disposable.dispose()
|
||||
}
|
||||
this.disposables = []
|
||||
this.ignoreInstances.clear()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import { describe, it, expect, vi } from "vitest"
|
||||
import * as fs from "fs/promises"
|
||||
import { SecurityMiddleware } from "../SecurityMiddleware"
|
||||
import { fileExistsAtPath } from "../../../utils/fs"
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("fs/promises")
|
||||
vi.mock("../../../utils/fs")
|
||||
vi.mock("vscode", () => ({
|
||||
workspace: {
|
||||
createFileSystemWatcher: vi.fn(() => ({
|
||||
onDidChange: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
onDidCreate: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
onDidDelete: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
dispose: vi.fn(),
|
||||
})),
|
||||
},
|
||||
Disposable: vi.fn(),
|
||||
}))
|
||||
|
||||
describe("SecurityMiddleware Simple Test", () => {
|
||||
it("should parse and apply YAML config correctly", async () => {
|
||||
const testConfig = `
|
||||
version: '1.0'
|
||||
security:
|
||||
enabled: true
|
||||
rules:
|
||||
- pattern: "**/.ssh/**"
|
||||
action: BLOCK
|
||||
priority: 100
|
||||
description: "SSH keys"
|
||||
- pattern: "**/.env*"
|
||||
action: ASK
|
||||
priority: 90
|
||||
description: "Environment files"
|
||||
`
|
||||
|
||||
vi.mocked(fileExistsAtPath).mockResolvedValue(true)
|
||||
vi.mocked(fs.readFile).mockResolvedValue(testConfig)
|
||||
|
||||
const middleware = new SecurityMiddleware({
|
||||
cwd: "/test",
|
||||
projectConfigPath: "/test/.roo-security.yaml",
|
||||
debug: true,
|
||||
})
|
||||
|
||||
await middleware.initialize()
|
||||
|
||||
const config = middleware.getConfig()
|
||||
console.log("Config loaded:", JSON.stringify(config, null, 2))
|
||||
|
||||
// Test SSH pattern
|
||||
const sshResult = await middleware.evaluateAccess(".ssh/id_rsa")
|
||||
console.log("SSH evaluation:", sshResult)
|
||||
expect(sshResult.action).toBe("BLOCK")
|
||||
|
||||
// Test env pattern
|
||||
const envResult = await middleware.evaluateAccess(".env")
|
||||
console.log("ENV evaluation:", envResult)
|
||||
expect(envResult.action).toBe("ASK")
|
||||
|
||||
// Test unmatched file
|
||||
const otherResult = await middleware.evaluateAccess("src/index.ts")
|
||||
console.log("Other evaluation:", otherResult)
|
||||
expect(otherResult.action).toBe("ALLOW")
|
||||
})
|
||||
})
|
||||
500
src/core/security/__tests__/SecurityMiddleware.spec.ts
Normal file
500
src/core/security/__tests__/SecurityMiddleware.spec.ts
Normal file
|
|
@ -0,0 +1,500 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { SecurityMiddleware } from "../SecurityMiddleware"
|
||||
import { SecurityAction, SecurityMiddlewareOptions } from "../types"
|
||||
import { fileExistsAtPath } from "../../../utils/fs"
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("fs/promises")
|
||||
vi.mock("../../../utils/fs")
|
||||
vi.mock("vscode", () => ({
|
||||
workspace: {
|
||||
createFileSystemWatcher: vi.fn(() => ({
|
||||
onDidChange: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
onDidCreate: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
onDidDelete: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
dispose: vi.fn(),
|
||||
})),
|
||||
},
|
||||
Disposable: vi.fn(),
|
||||
}))
|
||||
|
||||
describe("SecurityMiddleware", () => {
|
||||
let middleware: SecurityMiddleware
|
||||
let mockAskHandler: ReturnType<typeof vi.fn>
|
||||
const testCwd = "/test/project"
|
||||
|
||||
const mockGlobalConfig = `
|
||||
version: '1.0'
|
||||
security:
|
||||
enabled: true
|
||||
inheritRules: true
|
||||
defaultAction: ALLOW
|
||||
rules:
|
||||
- pattern: '**/.env*'
|
||||
action: ASK
|
||||
priority: 90
|
||||
description: Environment files
|
||||
askMessage: 'Access to \${file} requires approval'
|
||||
- pattern: '**/.ssh/**'
|
||||
action: BLOCK
|
||||
priority: 100
|
||||
description: SSH keys
|
||||
`
|
||||
|
||||
const mockProjectConfig = `
|
||||
version: '1.0'
|
||||
security:
|
||||
enabled: true
|
||||
inheritRules: true
|
||||
defaultAction: ALLOW
|
||||
rules:
|
||||
- pattern: 'config/production.*'
|
||||
action: BLOCK
|
||||
priority: 100
|
||||
description: Production config
|
||||
- pattern: '**/*.key'
|
||||
action: ASK
|
||||
priority: 85
|
||||
description: Key files
|
||||
`
|
||||
|
||||
const mockCustomConfig = `
|
||||
version: '1.0'
|
||||
security:
|
||||
enabled: true
|
||||
inheritRules: true
|
||||
defaultAction: ALLOW
|
||||
rules:
|
||||
- pattern: 'test/fixtures/**'
|
||||
action: ALLOW
|
||||
priority: 200
|
||||
description: Test fixtures
|
||||
- pattern: '**/personal/**'
|
||||
action: BLOCK
|
||||
priority: 150
|
||||
description: Personal files
|
||||
`
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockAskHandler = vi.fn().mockResolvedValue(true)
|
||||
|
||||
// Setup file existence mocks
|
||||
vi.mocked(fileExistsAtPath).mockImplementation(async (filePath) => {
|
||||
const pathStr = filePath.toString()
|
||||
// Return true for the config files we want to load
|
||||
return pathStr.includes(".roo-security")
|
||||
})
|
||||
|
||||
// Setup file read mocks
|
||||
vi.mocked(fs.readFile).mockImplementation(async (filePath, encoding) => {
|
||||
const pathStr = filePath.toString()
|
||||
if (pathStr.includes("global")) return mockGlobalConfig
|
||||
if (pathStr.includes("custom")) return mockCustomConfig
|
||||
if (pathStr.includes("project") || pathStr.includes(".roo-security.yaml")) return mockProjectConfig
|
||||
return ""
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (middleware) {
|
||||
middleware.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
describe("initialization", () => {
|
||||
it("should initialize with default options", async () => {
|
||||
const options: SecurityMiddlewareOptions = {
|
||||
cwd: testCwd,
|
||||
debug: false,
|
||||
}
|
||||
|
||||
middleware = new SecurityMiddleware(options)
|
||||
await middleware.initialize()
|
||||
|
||||
const config = middleware.getConfig()
|
||||
expect(config).toBeDefined()
|
||||
})
|
||||
|
||||
it("should load configurations from all tiers", async () => {
|
||||
const options: SecurityMiddlewareOptions = {
|
||||
cwd: testCwd,
|
||||
globalConfigPath: "/home/user/.roo-security.yaml",
|
||||
projectConfigPath: path.join(testCwd, ".roo-security.yaml"),
|
||||
customConfigPath: path.join(testCwd, ".roo-security-custom.yaml"),
|
||||
}
|
||||
|
||||
middleware = new SecurityMiddleware(options)
|
||||
await middleware.initialize()
|
||||
|
||||
const config = middleware.getConfig()
|
||||
expect(config.global).toBeDefined()
|
||||
expect(config.project).toBeDefined()
|
||||
expect(config.custom).toBeDefined()
|
||||
})
|
||||
|
||||
it("should handle missing configuration files gracefully", async () => {
|
||||
vi.mocked(fileExistsAtPath).mockResolvedValue(false)
|
||||
|
||||
const options: SecurityMiddlewareOptions = {
|
||||
cwd: testCwd,
|
||||
}
|
||||
|
||||
middleware = new SecurityMiddleware(options)
|
||||
await expect(middleware.initialize()).resolves.not.toThrow()
|
||||
|
||||
const config = middleware.getConfig()
|
||||
expect(Object.keys(config)).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("evaluateAccess", () => {
|
||||
beforeEach(async () => {
|
||||
const options: SecurityMiddlewareOptions = {
|
||||
cwd: testCwd,
|
||||
onAskAction: mockAskHandler,
|
||||
globalConfigPath: "/home/user/.roo-security.yaml",
|
||||
projectConfigPath: path.join(testCwd, ".roo-security.yaml"),
|
||||
customConfigPath: path.join(testCwd, ".roo-security-custom.yaml"),
|
||||
}
|
||||
|
||||
middleware = new SecurityMiddleware(options)
|
||||
await middleware.initialize()
|
||||
})
|
||||
|
||||
it("should block access to SSH files", async () => {
|
||||
const evaluation = await middleware.evaluateAccess(".ssh/id_rsa")
|
||||
|
||||
expect(evaluation.action).toBe("BLOCK")
|
||||
expect(evaluation.matchedRule?.description).toBe("SSH keys")
|
||||
expect(evaluation.level).toBe("global")
|
||||
})
|
||||
|
||||
it("should ask for approval for environment files", async () => {
|
||||
mockAskHandler.mockResolvedValue(true)
|
||||
|
||||
const evaluation = await middleware.evaluateAccess(".env.production")
|
||||
|
||||
expect(mockAskHandler).toHaveBeenCalled()
|
||||
expect(evaluation.action).toBe("ALLOW") // After approval
|
||||
expect(evaluation.matchedRule?.description).toBe("Environment files")
|
||||
})
|
||||
|
||||
it("should deny access when ASK action is rejected", async () => {
|
||||
mockAskHandler.mockResolvedValue(false)
|
||||
|
||||
const evaluation = await middleware.evaluateAccess(".env.production")
|
||||
|
||||
expect(mockAskHandler).toHaveBeenCalled()
|
||||
expect(evaluation.action).toBe("BLOCK")
|
||||
})
|
||||
|
||||
it("should block production config files", async () => {
|
||||
const evaluation = await middleware.evaluateAccess("config/production.yml")
|
||||
|
||||
expect(evaluation.action).toBe("BLOCK")
|
||||
expect(evaluation.matchedRule?.description).toBe("Production config")
|
||||
expect(evaluation.level).toBe("project")
|
||||
})
|
||||
|
||||
it("should allow test fixtures (custom override)", async () => {
|
||||
const evaluation = await middleware.evaluateAccess("test/fixtures/data.json")
|
||||
|
||||
expect(evaluation.action).toBe("ALLOW")
|
||||
expect(evaluation.matchedRule?.description).toBe("Test fixtures")
|
||||
expect(evaluation.level).toBe("custom")
|
||||
})
|
||||
|
||||
it("should block personal files", async () => {
|
||||
const evaluation = await middleware.evaluateAccess("docs/personal/notes.txt")
|
||||
|
||||
expect(evaluation.action).toBe("BLOCK")
|
||||
expect(evaluation.matchedRule?.description).toBe("Personal files")
|
||||
expect(evaluation.level).toBe("custom")
|
||||
})
|
||||
|
||||
it("should allow files not matching any rules", async () => {
|
||||
const evaluation = await middleware.evaluateAccess("src/index.ts")
|
||||
|
||||
expect(evaluation.action).toBe("ALLOW")
|
||||
expect(evaluation.matchedRule).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should respect rule priority", async () => {
|
||||
// Custom rule (priority 200) should override project rule
|
||||
const evaluation = await middleware.evaluateAccess("test/fixtures/production.json")
|
||||
|
||||
expect(evaluation.action).toBe("ALLOW")
|
||||
expect(evaluation.level).toBe("custom")
|
||||
})
|
||||
|
||||
it("should handle regex patterns", async () => {
|
||||
// Create middleware with a regex pattern
|
||||
vi.mocked(fs.readFile).mockResolvedValue(`
|
||||
version: '1.0'
|
||||
security:
|
||||
enabled: true
|
||||
rules:
|
||||
- pattern: '/.*\\.secret\\..*/'
|
||||
action: BLOCK
|
||||
priority: 100
|
||||
description: Secret files
|
||||
`)
|
||||
|
||||
const options: SecurityMiddlewareOptions = {
|
||||
cwd: testCwd,
|
||||
projectConfigPath: path.join(testCwd, ".roo-security.yaml"),
|
||||
}
|
||||
|
||||
const regexMiddleware = new SecurityMiddleware(options)
|
||||
await regexMiddleware.initialize()
|
||||
|
||||
const evaluation = await regexMiddleware.evaluateAccess("config.secret.json")
|
||||
expect(evaluation.action).toBe("BLOCK")
|
||||
|
||||
regexMiddleware.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
describe("evaluateCommand", () => {
|
||||
beforeEach(async () => {
|
||||
const options: SecurityMiddlewareOptions = {
|
||||
cwd: testCwd,
|
||||
projectConfigPath: path.join(testCwd, ".roo-security.yaml"),
|
||||
}
|
||||
|
||||
middleware = new SecurityMiddleware(options)
|
||||
await middleware.initialize()
|
||||
})
|
||||
|
||||
it("should block commands accessing protected files", async () => {
|
||||
const evaluation = await middleware.evaluateCommand("cat config/production.yml")
|
||||
|
||||
expect(evaluation.action).toBe("BLOCK")
|
||||
expect(evaluation.message).toContain("Command blocked")
|
||||
})
|
||||
|
||||
it("should allow commands not accessing files", async () => {
|
||||
const evaluation = await middleware.evaluateCommand("ls -la")
|
||||
|
||||
expect(evaluation.action).toBe("ALLOW")
|
||||
})
|
||||
|
||||
it("should check multiple file arguments", async () => {
|
||||
const evaluation = await middleware.evaluateCommand("cat README.md config/production.yml")
|
||||
|
||||
expect(evaluation.action).toBe("BLOCK")
|
||||
})
|
||||
|
||||
it("should ignore command flags", async () => {
|
||||
const evaluation = await middleware.evaluateCommand('grep -r "pattern" src/')
|
||||
|
||||
expect(evaluation.action).toBe("ALLOW")
|
||||
})
|
||||
|
||||
it("should handle PowerShell commands", async () => {
|
||||
const evaluation = await middleware.evaluateCommand("Get-Content config/production.yml")
|
||||
|
||||
expect(evaluation.action).toBe("BLOCK")
|
||||
})
|
||||
})
|
||||
|
||||
describe("statistics", () => {
|
||||
beforeEach(async () => {
|
||||
const options: SecurityMiddlewareOptions = {
|
||||
cwd: testCwd,
|
||||
onAskAction: mockAskHandler,
|
||||
projectConfigPath: path.join(testCwd, ".roo-security.yaml"),
|
||||
}
|
||||
|
||||
middleware = new SecurityMiddleware(options)
|
||||
await middleware.initialize()
|
||||
})
|
||||
|
||||
it("should track evaluation statistics", async () => {
|
||||
await middleware.evaluateAccess("config/production.yml") // BLOCK
|
||||
await middleware.evaluateAccess("src/index.ts") // ALLOW
|
||||
await middleware.evaluateAccess("test.key") // ASK -> ALLOW
|
||||
|
||||
const stats = middleware.getStats()
|
||||
|
||||
expect(stats.totalEvaluations).toBe(3)
|
||||
expect(stats.blockedCount).toBe(1)
|
||||
expect(stats.allowedCount).toBe(1)
|
||||
expect(stats.askedCount).toBe(1)
|
||||
})
|
||||
|
||||
it("should reset statistics", async () => {
|
||||
await middleware.evaluateAccess("config/production.yml")
|
||||
await middleware.evaluateAccess("src/index.ts")
|
||||
|
||||
middleware.resetStats()
|
||||
const stats = middleware.getStats()
|
||||
|
||||
expect(stats.totalEvaluations).toBe(0)
|
||||
expect(stats.blockedCount).toBe(0)
|
||||
expect(stats.allowedCount).toBe(0)
|
||||
expect(stats.askedCount).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("configuration management", () => {
|
||||
beforeEach(async () => {
|
||||
const options: SecurityMiddlewareOptions = {
|
||||
cwd: testCwd,
|
||||
projectConfigPath: path.join(testCwd, ".roo-security.yaml"),
|
||||
}
|
||||
|
||||
middleware = new SecurityMiddleware(options)
|
||||
await middleware.initialize()
|
||||
})
|
||||
|
||||
it("should export configuration to YAML", async () => {
|
||||
const yamlContent = await middleware.exportConfig("project")
|
||||
|
||||
expect(yamlContent).toContain('version: "1.0"')
|
||||
expect(yamlContent).toContain("security:")
|
||||
expect(yamlContent).toContain("rules:")
|
||||
})
|
||||
|
||||
it("should import configuration from YAML", async () => {
|
||||
const newConfig = `
|
||||
version: '1.0'
|
||||
security:
|
||||
enabled: true
|
||||
rules:
|
||||
- pattern: "**/*.test"
|
||||
action: BLOCK
|
||||
priority: 100
|
||||
description: "Test files"
|
||||
`
|
||||
|
||||
await middleware.importConfig(newConfig, "custom")
|
||||
|
||||
const config = middleware.getConfig()
|
||||
expect(config.custom?.rules).toHaveLength(1)
|
||||
expect(config.custom?.rules?.[0].pattern).toBe("**/*.test")
|
||||
})
|
||||
|
||||
it("should reject invalid configuration format", async () => {
|
||||
const invalidConfig = `
|
||||
invalid: true
|
||||
`
|
||||
|
||||
await expect(middleware.importConfig(invalidConfig, "custom")).rejects.toThrow(
|
||||
"Invalid configuration format",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("inheritance", () => {
|
||||
it("should respect inheritRules setting", async () => {
|
||||
// Mock config with inheritRules: false
|
||||
vi.mocked(fs.readFile).mockResolvedValue(`
|
||||
version: '1.0'
|
||||
security:
|
||||
enabled: true
|
||||
inheritRules: false
|
||||
rules:
|
||||
- pattern: "**/*.block"
|
||||
action: BLOCK
|
||||
priority: 100
|
||||
`)
|
||||
|
||||
const options: SecurityMiddlewareOptions = {
|
||||
cwd: testCwd,
|
||||
projectConfigPath: path.join(testCwd, ".roo-security.yaml"),
|
||||
globalConfigPath: "/home/user/.roo-security.yaml",
|
||||
}
|
||||
|
||||
middleware = new SecurityMiddleware(options)
|
||||
await middleware.initialize()
|
||||
|
||||
// Should only check project rules, not global
|
||||
const evaluation = await middleware.evaluateAccess(".env")
|
||||
expect(evaluation.action).toBe("ALLOW") // Global rule not applied
|
||||
})
|
||||
})
|
||||
|
||||
describe("default actions", () => {
|
||||
it("should apply default action when no rules match", async () => {
|
||||
vi.mocked(fs.readFile).mockResolvedValue(`
|
||||
version: '1.0'
|
||||
security:
|
||||
enabled: true
|
||||
defaultAction: BLOCK
|
||||
rules: []
|
||||
`)
|
||||
|
||||
const options: SecurityMiddlewareOptions = {
|
||||
cwd: testCwd,
|
||||
projectConfigPath: path.join(testCwd, ".roo-security.yaml"),
|
||||
}
|
||||
|
||||
middleware = new SecurityMiddleware(options)
|
||||
await middleware.initialize()
|
||||
|
||||
const evaluation = await middleware.evaluateAccess("any-file.txt")
|
||||
expect(evaluation.action).toBe("BLOCK")
|
||||
})
|
||||
})
|
||||
|
||||
describe("file watching", () => {
|
||||
it("should set up file watchers for configuration changes", async () => {
|
||||
const mockWatcher = {
|
||||
onDidChange: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
onDidCreate: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
onDidDelete: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
dispose: vi.fn(),
|
||||
}
|
||||
|
||||
vi.mocked(vscode.workspace.createFileSystemWatcher).mockReturnValue(mockWatcher as any)
|
||||
|
||||
const options: SecurityMiddlewareOptions = {
|
||||
cwd: testCwd,
|
||||
globalConfigPath: "/home/user/.roo-security.yaml",
|
||||
projectConfigPath: path.join(testCwd, ".roo-security.yaml"),
|
||||
customConfigPath: path.join(testCwd, ".roo-security-custom.yaml"),
|
||||
}
|
||||
|
||||
middleware = new SecurityMiddleware(options)
|
||||
await middleware.initialize()
|
||||
|
||||
// Should create watchers for all config files
|
||||
expect(vscode.workspace.createFileSystemWatcher).toHaveBeenCalledTimes(3)
|
||||
expect(mockWatcher.onDidChange).toHaveBeenCalled()
|
||||
expect(mockWatcher.onDidCreate).toHaveBeenCalled()
|
||||
expect(mockWatcher.onDidDelete).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("disposal", () => {
|
||||
it("should clean up resources on dispose", async () => {
|
||||
const mockDisposable = { dispose: vi.fn() }
|
||||
const mockWatcher = {
|
||||
onDidChange: vi.fn(() => mockDisposable),
|
||||
onDidCreate: vi.fn(() => mockDisposable),
|
||||
onDidDelete: vi.fn(() => mockDisposable),
|
||||
dispose: vi.fn(),
|
||||
}
|
||||
|
||||
vi.mocked(vscode.workspace.createFileSystemWatcher).mockReturnValue(mockWatcher as any)
|
||||
|
||||
const options: SecurityMiddlewareOptions = {
|
||||
cwd: testCwd,
|
||||
projectConfigPath: path.join(testCwd, ".roo-security.yaml"),
|
||||
}
|
||||
|
||||
middleware = new SecurityMiddleware(options)
|
||||
await middleware.initialize()
|
||||
|
||||
middleware.dispose()
|
||||
|
||||
expect(mockDisposable.dispose).toHaveBeenCalled()
|
||||
expect(mockWatcher.dispose).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
229
src/core/security/types.ts
Normal file
229
src/core/security/types.ts
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
/**
|
||||
* Security Middleware Types
|
||||
* Defines the structure for enhanced security configuration with ASK rules and YAML support
|
||||
*/
|
||||
|
||||
/**
|
||||
* Action types for security rules
|
||||
*/
|
||||
export type SecurityAction = "BLOCK" | "ASK" | "ALLOW"
|
||||
|
||||
/**
|
||||
* A single security rule defining file access patterns
|
||||
*/
|
||||
export interface SecurityRule {
|
||||
/**
|
||||
* Glob pattern or regex for matching files/paths
|
||||
* Supports gitignore-style patterns
|
||||
*/
|
||||
pattern: string
|
||||
|
||||
/**
|
||||
* Action to take when pattern matches
|
||||
*/
|
||||
action: SecurityAction
|
||||
|
||||
/**
|
||||
* Optional description for the rule
|
||||
*/
|
||||
description?: string
|
||||
|
||||
/**
|
||||
* Optional message to show when ASK action is triggered
|
||||
*/
|
||||
askMessage?: string
|
||||
|
||||
/**
|
||||
* Priority for rule evaluation (higher = evaluated first)
|
||||
* Default: 0
|
||||
*/
|
||||
priority?: number
|
||||
|
||||
/**
|
||||
* Whether this rule applies to commands as well as file access
|
||||
* Default: true
|
||||
*/
|
||||
applyToCommands?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Security configuration at a specific level
|
||||
*/
|
||||
export interface SecurityConfig {
|
||||
/**
|
||||
* Whether security middleware is enabled at this level
|
||||
*/
|
||||
enabled: boolean
|
||||
|
||||
/**
|
||||
* Array of security rules
|
||||
*/
|
||||
rules: SecurityRule[]
|
||||
|
||||
/**
|
||||
* Default action when no rules match
|
||||
* Default: 'ALLOW'
|
||||
*/
|
||||
defaultAction?: SecurityAction
|
||||
|
||||
/**
|
||||
* Whether to inherit rules from parent levels
|
||||
* Default: true
|
||||
*/
|
||||
inheritRules?: boolean
|
||||
|
||||
/**
|
||||
* Custom message prefix for ASK prompts
|
||||
*/
|
||||
askMessagePrefix?: string
|
||||
|
||||
/**
|
||||
* Metadata about the configuration
|
||||
*/
|
||||
metadata?: {
|
||||
version?: string
|
||||
author?: string
|
||||
description?: string
|
||||
lastModified?: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete security configuration including all tiers
|
||||
*/
|
||||
export interface SecurityMiddlewareConfig {
|
||||
/**
|
||||
* Global configuration (applies to all projects)
|
||||
*/
|
||||
global?: SecurityConfig
|
||||
|
||||
/**
|
||||
* Project-specific configuration
|
||||
*/
|
||||
project?: SecurityConfig
|
||||
|
||||
/**
|
||||
* Custom configuration (user overrides)
|
||||
*/
|
||||
custom?: SecurityConfig
|
||||
|
||||
/**
|
||||
* Enterprise configuration (from organization)
|
||||
*/
|
||||
enterprise?: SecurityConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of evaluating security rules
|
||||
*/
|
||||
export interface SecurityEvaluation {
|
||||
/**
|
||||
* The action to take
|
||||
*/
|
||||
action: SecurityAction
|
||||
|
||||
/**
|
||||
* The rule that matched (if any)
|
||||
*/
|
||||
matchedRule?: SecurityRule
|
||||
|
||||
/**
|
||||
* The configuration level where the rule was found
|
||||
*/
|
||||
level?: "enterprise" | "global" | "project" | "custom"
|
||||
|
||||
/**
|
||||
* Custom message for ASK action
|
||||
*/
|
||||
message?: string
|
||||
|
||||
/**
|
||||
* The path that was evaluated
|
||||
*/
|
||||
path: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for security middleware initialization
|
||||
*/
|
||||
export interface SecurityMiddlewareOptions {
|
||||
/**
|
||||
* Current working directory
|
||||
*/
|
||||
cwd: string
|
||||
|
||||
/**
|
||||
* Path to global configuration file
|
||||
*/
|
||||
globalConfigPath?: string
|
||||
|
||||
/**
|
||||
* Path to project configuration file
|
||||
*/
|
||||
projectConfigPath?: string
|
||||
|
||||
/**
|
||||
* Path to custom configuration file
|
||||
*/
|
||||
customConfigPath?: string
|
||||
|
||||
/**
|
||||
* Whether to enable debug logging
|
||||
*/
|
||||
debug?: boolean
|
||||
|
||||
/**
|
||||
* Callback for ASK actions
|
||||
*/
|
||||
onAskAction?: (evaluation: SecurityEvaluation) => Promise<boolean>
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for security configuration file (YAML format)
|
||||
*/
|
||||
export interface SecurityConfigFile {
|
||||
/**
|
||||
* Version of the configuration schema
|
||||
*/
|
||||
version: "1.0"
|
||||
|
||||
/**
|
||||
* Security configuration
|
||||
*/
|
||||
security: SecurityConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistics about security middleware operations
|
||||
*/
|
||||
export interface SecurityStats {
|
||||
/**
|
||||
* Number of files blocked
|
||||
*/
|
||||
blockedCount: number
|
||||
|
||||
/**
|
||||
* Number of files that triggered ASK
|
||||
*/
|
||||
askedCount: number
|
||||
|
||||
/**
|
||||
* Number of files allowed
|
||||
*/
|
||||
allowedCount: number
|
||||
|
||||
/**
|
||||
* Total evaluations performed
|
||||
*/
|
||||
totalEvaluations: number
|
||||
|
||||
/**
|
||||
* Rules by configuration level
|
||||
*/
|
||||
rulesByLevel: {
|
||||
enterprise: number
|
||||
global: number
|
||||
project: number
|
||||
custom: number
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue