Skip to content

Custom Rules

Jean-Marc Strauven edited this page Aug 1, 2025 · 1 revision

πŸ—οΈ Custom Rules Guide

Laravel Safeguard allows you to create custom security rules tailored to your application's specific requirements. This guide will walk you through creating, implementing, and managing custom security rules.

πŸš€ Quick Start: Creating Your First Custom Rule

Using the Artisan Command

Generate a new custom rule with the built-in generator:

php artisan safeguard:make-rule CustomSecurityRule

This creates app/SafeguardRules/CustomSecurityRule.php:

<?php

namespace App\SafeguardRules;

use Grazulex\LaravelSafeguard\Contracts\SafeguardRule;
use Grazulex\LaravelSafeguard\SafeguardResult;

class CustomSecurityRule implements SafeguardRule
{
    public function id(): string
    {
        return 'custom-security-rule';
    }

    public function description(): string
    {
        return 'Custom security validation for my application';
    }

    public function check(): SafeguardResult
    {
        // Your custom logic here
        if ($this->mySecurityCheck()) {
            return SafeguardResult::pass('Custom security check passed');
        }
        
        return SafeguardResult::fail('Custom security check failed');
    }

    public function appliesToEnvironment(string $environment): bool
    {
        return true; // Run in all environments
    }

    public function severity(): string
    {
        return 'error';
    }
    
    private function mySecurityCheck(): bool
    {
        // Implement your security logic
        return true;
    }
}

Register Your Custom Rule

Add your rule to the configuration file:

// config/safeguard.php
'rules' => [
    // Existing rules...
    'custom-security-rule' => true,
],

'custom_rules_path' => app_path('SafeguardRules'),
'custom_rules_namespace' => 'App\\SafeguardRules',

πŸ“š Real-World Examples

Database Security Rule

<?php

namespace App\SafeguardRules;

use Grazulex\LaravelSafeguard\Contracts\SafeguardRule;
use Grazulex\LaravelSafeguard\SafeguardResult;
use PDO;

class DatabaseSecurityRule implements SafeguardRule
{
    public function id(): string
    {
        return 'database-security-check';
    }

    public function description(): string
    {
        return 'Validates database security configuration';
    }

    public function check(): SafeguardResult
    {
        $issues = [];
        $dbConfig = config('database.connections.' . config('database.default'));
        
        // Check for default passwords
        if (in_array($dbConfig['password'], ['password', 'root', '', '123456'])) {
            $issues[] = 'Database is using a weak or default password';
        }

        // Check for SSL in production
        if (app()->environment('production')) {
            $sslEnabled = $dbConfig['options'][PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] ?? false;
            if (!$sslEnabled) {
                $issues[] = 'SSL not configured for database connection in production';
            }
        }
        
        // Check for root user in production
        if (app()->environment('production') && $dbConfig['username'] === 'root') {
            $issues[] = 'Using root database user in production is not recommended';
        }

        if (empty($issues)) {
            return SafeguardResult::pass('Database security configuration is valid');
        }

        return SafeguardResult::fail(
            'Database security issues found',
            [
                'issues' => $issues,
                'connection' => config('database.default'),
                'recommendations' => [
                    'Use strong, unique passwords',
                    'Enable SSL for production databases',
                    'Create dedicated database users with minimal privileges'
                ]
            ]
        );
    }

    public function appliesToEnvironment(string $environment): bool
    {
        return in_array($environment, ['production', 'staging', 'local']);
    }

    public function severity(): string
    {
        return 'critical';
    }
}

API Security Rule

<?php

namespace App\SafeguardRules;

use Grazulex\LaravelSafeguard\Contracts\SafeguardRule;
use Grazulex\LaravelSafeguard\SafeguardResult;

class ApiSecurityRule implements SafeguardRule
{
    public function id(): string
    {
        return 'api-security-check';
    }

    public function description(): string
    {
        return 'Validates API security configuration';
    }

    public function check(): SafeguardResult
    {
        $issues = [];
        
        // Check API rate limiting
        if (!config('api.rate_limiting.enabled', false)) {
            $issues[] = 'API rate limiting is not enabled';
        }
        
        // Check API authentication
        $authMiddleware = config('api.middleware.auth', []);
        if (empty($authMiddleware)) {
            $issues[] = 'No authentication middleware configured for API routes';
        }
        
        // Check CORS configuration
        $corsConfig = config('cors');
        if ($corsConfig['allowed_origins'] === ['*']) {
            $issues[] = 'CORS is configured to allow all origins (*)';
        }
        
        // Check API versioning
        if (!config('api.versioning.enabled', false)) {
            $issues[] = 'API versioning is not enabled';
        }
        
        // Check for API documentation
        if (!file_exists(base_path('api-docs.json'))) {
            $issues[] = 'API documentation not found';
        }

        if (empty($issues)) {
            return SafeguardResult::pass('API security configuration is valid');
        }

        return SafeguardResult::warning(
            'API security improvements recommended',
            [
                'issues' => $issues,
                'recommendations' => [
                    'Enable API rate limiting to prevent abuse',
                    'Configure proper authentication middleware',
                    'Restrict CORS to specific domains',
                    'Implement API versioning strategy',
                    'Maintain up-to-date API documentation'
                ]
            ]
        );
    }

    public function appliesToEnvironment(string $environment): bool
    {
        return true;
    }

    public function severity(): string
    {
        return 'warning';
    }
}

File Upload Security Rule

<?php

namespace App\SafeguardRules;

use Grazulex\LaravelSafeguard\Contracts\SafeguardRule;
use Grazulex\LaravelSafeguard\SafeguardResult;

class FileUploadSecurityRule implements SafeguardRule
{
    public function id(): string
    {
        return 'file-upload-security-check';
    }

    public function description(): string
    {
        return 'Validates file upload security configuration';
    }

    public function check(): SafeguardResult
    {
        $issues = [];
        
        // Check upload limits
        $maxSize = ini_get('upload_max_filesize');
        if ($this->convertToBytes($maxSize) > 50 * 1024 * 1024) { // 50MB
            $issues[] = \"Upload limit is very high ({$maxSize}) - consider reducing for security\";
        }
        
        // Check for file type validation
        if (!class_exists('App\\Http\\Requests\\FileUploadRequest')) {
            $issues[] = 'No dedicated file upload request validation found';
        }
        
        // Check upload directory permissions
        $uploadDir = storage_path('app/uploads');
        if (is_dir($uploadDir) && substr(sprintf('%o', fileperms($uploadDir)), -4) !== '0755') {
            $issues[] = 'Upload directory has incorrect permissions';
        }
        
        // Check for antivirus scanning
        if (!config('filesystems.antivirus_enabled', false)) {
            $issues[] = 'Antivirus scanning not configured for file uploads';
        }

        if (empty($issues)) {
            return SafeguardResult::pass('File upload security configuration is valid');
        }

        return SafeguardResult::warning(
            'File upload security improvements recommended',
            [
                'issues' => $issues,
                'recommendations' => [
                    'Set reasonable upload size limits',
                    'Validate file types and extensions',
                    'Set proper directory permissions (755)',
                    'Implement antivirus scanning',
                    'Store uploads outside web root'
                ]
            ]
        );
    }

    public function appliesToEnvironment(string $environment): bool
    {
        return true;
    }

    public function severity(): string
    {
        return 'warning';
    }
    
    private function convertToBytes(string $value): int
    {
        $unit = strtolower(substr($value, -1));
        $number = (int) $value;
        
        switch ($unit) {
            case 'g': return $number * 1024 * 1024 * 1024;
            case 'm': return $number * 1024 * 1024;
            case 'k': return $number * 1024;
            default: return $number;
        }
    }
}

Third-Party Service Security Rule

<?php

namespace App\SafeguardRules;

use Grazulex\LaravelSafeguard\Contracts\SafeguardRule;
use Grazulex\LaravelSafeguard\SafeguardResult;

class ThirdPartyServiceSecurityRule implements SafeguardRule
{
    public function id(): string
    {
        return 'third-party-service-security-check';
    }

    public function description(): string
    {
        return 'Validates third-party service integration security';
    }

    public function check(): SafeguardResult
    {
        $issues = [];
        
        // Check for hardcoded API keys
        $services = config('services');
        foreach ($services as $service => $config) {
            if (isset($config['key']) && !str_starts_with($config['key'], 'env(')) {
                $issues[] = \"Hardcoded API key found for {$service} service\";
            }
        }
        
        // Check Stripe configuration
        if (config('services.stripe.secret')) {
            $stripeSecret = config('services.stripe.secret');
            if (str_contains($stripeSecret, 'sk_test_') && app()->environment('production')) {
                $issues[] = 'Stripe test keys are being used in production';
            }
        }
        
        // Check for webhook signature verification
        $webhookRoutes = $this->getWebhookRoutes();
        foreach ($webhookRoutes as $route) {
            if (!$this->hasSignatureVerification($route)) {
                $issues[] = \"Webhook route {$route} lacks signature verification\";
            }
        }
        
        // Check OAuth redirect URIs
        $oauthConfig = config('services.oauth', []);
        foreach ($oauthConfig as $provider => $config) {
            if (isset($config['redirect']) && str_contains($config['redirect'], 'localhost')) {
                if (app()->environment('production')) {
                    $issues[] = \"OAuth {$provider} redirect URI points to localhost in production\";
                }
            }
        }

        if (empty($issues)) {
            return SafeguardResult::pass('Third-party service security configuration is valid');
        }

        return SafeguardResult::fail(
            'Third-party service security issues found',
            [
                'issues' => $issues,
                'recommendations' => [
                    'Move all API keys to environment variables',
                    'Use production keys in production environment',
                    'Implement webhook signature verification',
                    'Use production redirect URIs in production'
                ]
            ]
        );
    }

    public function appliesToEnvironment(string $environment): bool
    {
        return true;
    }

    public function severity(): string
    {
        return 'error';
    }
    
    private function getWebhookRoutes(): array
    {
        // Implementation to find webhook routes
        return []; // Simplified for example
    }
    
    private function hasSignatureVerification(string $route): bool
    {
        // Implementation to check if route has signature verification
        return true; // Simplified for example
    }
}

πŸ“Š SafeguardResult Methods

The SafeguardResult class provides several static methods for different result types:

// Pass result
SafeguardResult::pass('Everything is secure');

// Fail result with details
SafeguardResult::fail('Security issue found', [
    'details' => 'Additional information',
    'recommendations' => ['Fix suggestion 1', 'Fix suggestion 2']
]);

// Critical security issue
SafeguardResult::critical('Critical vulnerability detected', [
    'severity' => 'immediate attention required',
    'impact' => 'High security risk'
]);

// Warning result
SafeguardResult::warning('Security improvement recommended', [
    'priority' => 'low',
    'enhancement' => 'Optional security enhancement'
]);

// Information result
SafeguardResult::info('Security configuration noted', [
    'note' => 'For informational purposes'
]);

🎯 Severity Levels

Choose the appropriate severity level for your custom rules:

critical 🚨

Use for issues that pose immediate security risks:

public function severity(): string
{
    return 'critical';
}

error ❌

Use for important security issues that should be addressed:

public function severity(): string
{
    return 'error';
}

warning ⚠️

Use for recommendations and best practices:

public function severity(): string
{
    return 'warning';
}

info ℹ️

Use for informational messages:

public function severity(): string
{
    return 'info';
}

🌍 Environment-Specific Rules

Control which environments your rule applies to:

public function appliesToEnvironment(string $environment): bool
{
    // Run only in production
    return $environment === 'production';
    
    // Run in production and staging
    return in_array($environment, ['production', 'staging']);
    
    // Run everywhere except local
    return $environment !== 'local';
    
    // Run everywhere
    return true;
}

πŸ”§ Advanced Techniques

Accessing Laravel Services

public function check(): SafeguardResult
{
    // Access configuration
    $config = config('app.debug');
    
    // Access environment
    $environment = app()->environment();
    
    // Access container services
    $cache = app('cache');
    $db = app('db');
    
    // Access facades
    $user = Auth::user();
    $request = request();
    
    return SafeguardResult::pass('Check completed');
}

File System Checks

public function check(): SafeguardResult
{
    // Check file permissions
    $file = base_path('.env');
    $permissions = substr(sprintf('%o', fileperms($file)), -4);
    
    if ($permissions !== '0600') {
        return SafeguardResult::fail('.env file has incorrect permissions');
    }
    
    // Check directory existence
    if (!is_dir(storage_path('logs'))) {
        return SafeguardResult::fail('Logs directory does not exist');
    }
    
    // Check file contents
    $content = file_get_contents(config_path('app.php'));
    if (str_contains($content, 'hardcoded-secret')) {
        return SafeguardResult::critical('Hardcoded secret found in configuration');
    }
    
    return SafeguardResult::pass('File system checks passed');
}

Network and External Checks

public function check(): SafeguardResult
{
    // Check external service availability
    try {
        $response = Http::timeout(5)->get('https://api.external-service.com/health');
        if (!$response->successful()) {
            return SafeguardResult::warning('External service is not responding');
        }
    } catch (Exception $e) {
        return SafeguardResult::warning('Cannot connect to external service');
    }
    
    return SafeguardResult::pass('External services are accessible');
}

πŸ“ Organizing Custom Rules

Directory Structure

Organize your rules by category:

app/SafeguardRules/
β”œβ”€β”€ Authentication/
β”‚   β”œβ”€β”€ PasswordPolicyRule.php
β”‚   β”œβ”€β”€ TwoFactorRule.php
β”‚   └── SessionSecurityRule.php
β”œβ”€β”€ Database/
β”‚   β”œβ”€β”€ DatabaseSecurityRule.php
β”‚   β”œβ”€β”€ BackupSecurityRule.php
β”‚   └── QueryOptimizationRule.php
β”œβ”€β”€ Api/
β”‚   β”œβ”€β”€ ApiSecurityRule.php
β”‚   β”œβ”€β”€ RateLimitingRule.php
β”‚   └── CorsConfigurationRule.php
└── Infrastructure/
    β”œβ”€β”€ ServerSecurityRule.php
    β”œβ”€β”€ MonitoringRule.php
    └── LoggingRule.php

Namespace Organization

<?php

namespace App\SafeguardRules\Authentication;

use Grazulex\LaravelSafeguard\Contracts\SafeguardRule;
use Grazulex\LaravelSafeguard\SafeguardResult;

class PasswordPolicyRule implements SafeguardRule
{
    // Implementation...
}

Update your configuration:

// config/safeguard.php
'custom_rules_path' => app_path('SafeguardRules'),
'custom_rules_namespace' => 'App\\SafeguardRules',

// Or for organized structure
'custom_rules_paths' => [
    app_path('SafeguardRules/Authentication') => 'App\\SafeguardRules\\Authentication',
    app_path('SafeguardRules/Database') => 'App\\SafeguardRules\\Database',
    app_path('SafeguardRules/Api') => 'App\\SafeguardRules\\Api',
],

πŸ§ͺ Testing Custom Rules

Unit Testing

<?php

namespace Tests\Unit\SafeguardRules;

use App\SafeguardRules\DatabaseSecurityRule;
use Tests\TestCase;

class DatabaseSecurityRuleTest extends TestCase
{
    public function test_passes_with_secure_database_configuration()
    {
        // Arrange
        config(['database.connections.mysql.password' => 'strong-password']);
        
        $rule = new DatabaseSecurityRule();
        
        // Act
        $result = $rule->check();
        
        // Assert
        $this->assertTrue($result->passed());
    }
    
    public function test_fails_with_weak_password()
    {
        // Arrange
        config(['database.connections.mysql.password' => 'password']);
        
        $rule = new DatabaseSecurityRule();
        
        // Act
        $result = $rule->check();
        
        // Assert
        $this->assertTrue($result->failed());
        $this->assertStringContains('weak or default password', $result->message());
    }
}

Integration Testing

<?php

namespace Tests\Feature;

use Tests\TestCase;

class CustomRulesTest extends TestCase
{
    public function test_custom_rules_are_loaded()
    {
        $this->artisan('safeguard:list')
             ->assertExitCode(0)
             ->expectsOutput('database-security-check');
    }
    
    public function test_custom_rule_execution()
    {
        $this->artisan('safeguard:check', ['--rule' => 'database-security-check'])
             ->assertExitCode(0);
    }
}

πŸ“š Best Practices

🎯 Rule Design

  1. Single Responsibility: Each rule should check one specific security aspect
  2. Clear Naming: Use descriptive rule IDs and descriptions
  3. Appropriate Severity: Choose severity levels that match the actual risk
  4. Environment Awareness: Consider which environments the rule should apply to

πŸ” Implementation

  1. Error Handling: Gracefully handle exceptions and edge cases
  2. Performance: Avoid expensive operations in rules that run frequently
  3. Documentation: Include clear descriptions and recommendations
  4. Testability: Write testable code with proper separation of concerns

πŸ“Š Results

  1. Actionable Messages: Provide clear, actionable error messages
  2. Context: Include relevant details and recommendations
  3. Consistency: Use consistent messaging and formatting
  4. Severity Alignment: Ensure result severity matches rule severity

πŸ“š Related Documentation


Next Step: 🌍 Configure environment-specific rules

🏠 Home | πŸ“ Rules Reference | βš™οΈ Configuration | 🌍 Environment Rules

Clone this wiki locally