-
-
Notifications
You must be signed in to change notification settings - Fork 0
Custom Rules
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.
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;
}
}
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',
<?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';
}
}
<?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';
}
}
<?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;
}
}
}
<?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
}
}
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'
]);
Choose the appropriate severity level for your custom rules:
Use for issues that pose immediate security risks:
public function severity(): string
{
return 'critical';
}
Use for important security issues that should be addressed:
public function severity(): string
{
return 'error';
}
Use for recommendations and best practices:
public function severity(): string
{
return 'warning';
}
Use for informational messages:
public function severity(): string
{
return 'info';
}
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;
}
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');
}
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');
}
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');
}
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
<?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',
],
<?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());
}
}
<?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);
}
}
- Single Responsibility: Each rule should check one specific security aspect
- Clear Naming: Use descriptive rule IDs and descriptions
- Appropriate Severity: Choose severity levels that match the actual risk
- Environment Awareness: Consider which environments the rule should apply to
- Error Handling: Gracefully handle exceptions and edge cases
- Performance: Avoid expensive operations in rules that run frequently
- Documentation: Include clear descriptions and recommendations
- Testability: Write testable code with proper separation of concerns
- Actionable Messages: Provide clear, actionable error messages
- Context: Include relevant details and recommendations
- Consistency: Use consistent messaging and formatting
- Severity Alignment: Ensure result severity matches rule severity
- π Rules Reference - Learn about built-in security rules
- βοΈ Configuration Guide - Configure your custom rules
- π Environment Rules - Environment-specific rule configuration
- π‘ Examples Collection - More real-world examples
- π CI/CD Integration - Use custom rules in pipelines
Next Step: π Configure environment-specific rules
π Home | π Rules Reference | βοΈ Configuration | π Environment Rules
Laravel Safeguard - Configurable Security Checks for Laravel Applications
π Home | π¦ Installation | β‘ Quick Start | π‘ Examples | π Full Docs
Made with β€οΈ for the Laravel community
Β© 2025 - Laravel Safeguard by Grazulex