From dbcf80a3a4f18dfa75cdd123baa1e1f28766f794 Mon Sep 17 00:00:00 2001
From: alexkrivonos
Date: Mon, 14 Sep 2026 13:05:14 +0200
Subject: [PATCH 1/2] docs: update changelog and readme with recent security
and config changes
---
.env.example | 123 +---------------
CHANGELOG.md | 39 ++++-
README.md | 46 +++++-
src/Actions/ActionGuardAction.php | 9 +-
src/Bridge/BusinessCoreGuardAdapter.php | 28 +---
src/Results/CheckResolution.php | 15 +-
src/Support/ResolutionUrlSanitizer.php | 33 +++++
tests/Feature/ActionGuardActionTest.php | 70 +++++++++
.../ResolutionLinkXssSecurityTest.php | 52 +++++++
.../FilamentLivewireIntegrationTest.php | 134 ++++++++++++++++++
tests/TestCase.php | 34 ++++-
tests/Unit/ActionGuardServiceProviderTest.php | 26 ++++
tests/Unit/Support/ActionGuardAuditTest.php | 17 +++
tests/Unit/Traits/HasActionGuardsTest.php | 97 +++++++++++++
14 files changed, 566 insertions(+), 157 deletions(-)
create mode 100644 src/Support/ResolutionUrlSanitizer.php
create mode 100644 tests/Feature/Workbench/FilamentLivewireIntegrationTest.php
create mode 100644 tests/Unit/ActionGuardServiceProviderTest.php
diff --git a/.env.example b/.env.example
index 27e98eb..a0c62c0 100644
--- a/.env.example
+++ b/.env.example
@@ -1,12 +1,6 @@
-# ==============================================================================
-# FILAMENT ACTIONGUARD - ENTERPRISE ENVIRONMENT CONFIGURATION
-# ==============================================================================
-# Local-development example only. Do not copy it to production unchanged.
-# ==============================================================================
+# Filament ActionGuard local Workbench configuration.
+# Local development only; review every value before using it elsewhere.
-# ------------------------------------------------------------------------------
-# 1. APPLICATION & GENERAL SECURITY
-# ------------------------------------------------------------------------------
APP_NAME="Filament ActionGuard"
APP_ENV=local
APP_KEY=
@@ -15,133 +9,30 @@ APP_URL=http://localhost:8000
APP_TIMEZONE=UTC
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
-APP_FAKER_LOCALE=en_US
-APP_MAINTENANCE_DRIVER=cache
-# ------------------------------------------------------------------------------
-# 2. LOGGING & AUDIT TRAIL
-# ------------------------------------------------------------------------------
LOG_CHANNEL=stack
-LOG_STACK=single
-LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
-LOG_DAILY_DAYS=14
-# ------------------------------------------------------------------------------
-# 3. DATABASE CONFIGURATION (Multi-Engine Resilience)
-# ------------------------------------------------------------------------------
-# Default: SQLite for zero-latency local development & testbench
DB_CONNECTION=sqlite
DB_DATABASE="${APP_BASE_PATH:-.}/workbench/database.sqlite"
DB_FOREIGN_KEYS=true
-# Enterprise PostgreSQL Profile (Supports pgvector for Semantic Search)
-# DB_CONNECTION=pgsql
-# DB_HOST=127.0.0.1
-# DB_PORT=5432
-# DB_DATABASE=actionguard_enterprise
-# DB_USERNAME=actionguard_app
-# DB_PASSWORD=your_secure_password
-# DB_SSLMODE=prefer
-
-# Enterprise MySQL/MariaDB Profile (Strict Mode Compliant)
-# DB_CONNECTION=mysql
-# DB_HOST=127.0.0.1
-# DB_PORT=3306
-# DB_DATABASE=actionguard_enterprise
-# DB_USERNAME=actionguard_app
-# DB_PASSWORD=your_secure_password
-
-# ------------------------------------------------------------------------------
-# 4. SESSION & COOKIE SETTINGS
-# ------------------------------------------------------------------------------
SESSION_DRIVER=file
-SESSION_LIFETIME=120
-SESSION_ENCRYPT=true
-# Laravel 13 default: json serialization prevents PHP deserialization gadget chain attacks
-SESSION_SERIALIZATION=json
-SESSION_PATH=/
-SESSION_DOMAIN=null
-# Set true in every HTTPS deployment.
-SESSION_SECURE_COOKIE=false
-SESSION_HTTP_ONLY=true
-SESSION_SAME_SITE=lax
-
-# ------------------------------------------------------------------------------
-# 5. CACHING & APPLICATION ACCELERATION (Laravel 13 Standards)
-# ------------------------------------------------------------------------------
CACHE_STORE=file
-# Laravel 13 uses hyphenated prefix standard: {app-name}-cache-
-CACHE_PREFIX="actionguard-cache-"
-# Laravel 13 security: restricts arbitrary cached object unserialization
-CACHE_SERIALIZABLE_CLASSES=false
-
-# ------------------------------------------------------------------------------
-# 6. QUEUE & ASYNC TASK PROCESSING (Laravel 13 Queue Routing)
-# ------------------------------------------------------------------------------
QUEUE_CONNECTION=sync
-QUEUE_FAILED_DRIVER=database-uuids
-
-# ------------------------------------------------------------------------------
-# 7. REDIS CONFIGURATION (Clustering & TLS Support)
-# ------------------------------------------------------------------------------
-REDIS_CLIENT=phpredis
-REDIS_SCHEME=tcp
-REDIS_HOST=127.0.0.1
-REDIS_PASSWORD=null
-REDIS_PORT=6379
-REDIS_DB=0
-REDIS_CACHE_DB=1
-REDIS_PREFIX="actionguard-database-"
-# ------------------------------------------------------------------------------
-# 8. FILAMENT 5 PANEL CONFIGURATION
-# ------------------------------------------------------------------------------
-FILAMENT_FILESYSTEM_DISK=public
-FILAMENT_SERVE_ASSETS_FROM_VITE=false
-FILAMENT_BROADCASTING_ENABLED=false
-
-# ------------------------------------------------------------------------------
-# 9. MAIL & NOTIFICATION DELIVERY
-# ------------------------------------------------------------------------------
-MAIL_MAILER=log
-MAIL_SCHEME=null
-MAIL_HOST=127.0.0.1
-MAIL_PORT=1025
-MAIL_USERNAME=null
-MAIL_PASSWORD=null
-MAIL_ENCRYPTION=null
-MAIL_FROM_ADDRESS="no-reply@actionguard.example.com"
-MAIL_FROM_NAME="${APP_NAME}"
-
-# ------------------------------------------------------------------------------
-# 10. OBJECT STORAGE (AWS S3 / S3-Compatible Storage)
-# ------------------------------------------------------------------------------
-FILESYSTEM_DISK=local
-AWS_ACCESS_KEY_ID=
-AWS_SECRET_ACCESS_KEY=
-AWS_DEFAULT_REGION=eu-central-1
-AWS_BUCKET=
-AWS_USE_PATH_STYLE_ENDPOINT=false
-
-# ------------------------------------------------------------------------------
-# 11. ACTIONGUARD POLICY SETTINGS
-# ------------------------------------------------------------------------------
-# Enable or disable ActionGuard evaluation globally. Keep true in production.
+# Keep ActionGuard and fail-closed behavior enabled in production.
ACTIONGUARD_ENABLED=true
-
-# Security posture: Fail-closed (true) blocks actions if a check throws an unhandled exception
ACTIONGUARD_FAIL_CLOSED=true
-# Disabled by default. Enable only for a controlled, logged maintenance task.
+# Enable only for a controlled and documented maintenance callback.
ACTIONGUARD_ALLOW_BYPASS=false
-# Dispatch user-facing Filament danger notifications on state invariant violations
ACTIONGUARD_NOTIFICATIONS_ENABLED=true
-# Enable data-minimised structured audit events; route the channel to your SIEM.
-ACTIONGUARD_AUDIT_TRAIL=true
+# Audit output is opt-in and data-minimised. Configure a suitable channel when enabled.
+ACTIONGUARD_AUDIT_TRAIL=false
ACTIONGUARD_AUDIT_CHANNEL=stack
-# Relative and HTTPS resolution links are allowed. HTTP is blocked by default.
+# Relative paths and HTTPS are allowed. HTTP requires this explicit opt-in.
ACTIONGUARD_ALLOW_INSECURE_RESOLUTION_URLS=false
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e415322..495aaa1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,40 @@ All notable changes to `filament-actionguard` will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [Unreleased]
+
+### Added
+
+- Publishable runtime configuration for enablement, fail-closed behavior,
+ notifications, guarded bypasses, audit logging, and resolution URL policy.
+- Data-minimised structured audit events for action evaluation, invariant
+ blocking, and explicitly enabled bypass usage.
+- Locked and prefer-lowest dependency verification in CI.
+- Privacy responsibility and AI-generated artwork disclosures.
+- Central resolution URL sanitization shared by Community and Enterprise checks.
+- Filament Livewire integration coverage for table actions, page header actions,
+ modal halting, model validation errors, and notification configuration.
+
+### Changed
+
+- Composer now resolves stable dependencies by default and locks against the
+ minimum supported PHP 8.3 platform.
+- ActionGuard bypasses are disabled by default and scoped to their callback.
+- Resolution links allow HTTPS and relative URLs by default; HTTP requires an
+ explicit compatibility opt-in.
+- Security policy now documents supported versions and response expectations.
+- The local environment example now contains only Workbench and ActionGuard
+ settings and keeps audit logging disabled by default.
+
+### Security
+
+- Check exceptions are reported server-side with correlation IDs instead of
+ exposing internal exception messages in the Filament interface.
+- Explicit checks are validated against `ActionGuardCheckContract`, and rich
+ action labels are stripped of HTML before use as modal headings.
+- Direct `CheckResolution` URLs now use the same scheme allow-list and malformed
+ URL rejection as Enterprise resolutions.
+
## [1.0.0] - 2026-09-11
### Added
@@ -20,7 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `RequiredFieldCheck`: Validates presence and non-null status of critical model attributes.
- `NotEmptyCheck`: Validates strings, arrays, and collections with precise numeric zero (`0`, `'0'`, `0.00`) preservation.
- `ConditionCheck`: Custom boolean and closure-based rule evaluations with contextual failure messages.
- - `RelationshipCheck`: Ensures required relationships exist and meet minimum count constraints.
+ - `RelationshipCheck`: Ensures required relationships resolve to a model or non-empty collection.
- `MediaCheck`: Verifies attachments via Spatie MediaLibrary collections or standard URL/path attributes.
- `CallbackCheck`: Flexible closure-driven check returning custom `CheckResult` instances with explicit severity and descriptions.
@@ -55,6 +89,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Developer Experience & Tooling**
- Root `artisan` CLI bridge to Orchestra Testbench and Workbench environment.
- - Comprehensive test suite with 103 Pest tests and 234 assertions covering architecture, unit checks, traits, enterprise bridge hardening, and demo product scenarios.
+ - Comprehensive test suite with 132 Pest tests and 308 assertions covering architecture, unit checks, traits, enterprise bridge hardening, Livewire integration, security, and demo product scenarios.
[1.0.0]: https://github.com/allgorithm/filament-actionguard/releases/tag/v1.0.0
+[Unreleased]: https://github.com/allgorithm/filament-actionguard/compare/v1.0.0...HEAD
diff --git a/README.md b/README.md
index affc0a2..8dc3384 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
-
+
@@ -62,7 +62,13 @@ ActionGuard introduces a robust **Two-Phase Invariant Defense System**:
- `MediaCheck`: Checks media collections (Spatie MediaLibrary or file upload paths).
- `CallbackCheck`: Flexible check with full `CheckResult` control.
- ๐ **Fully Localized (i18n):** Complete English and German translations included out-of-the-box.
-- ๐ **Bulletproof Quality:** PHPStan **Level 8** (0 errors), 100% PSR-12 code style, and 103 comprehensive Pest tests.
+- ๐ **Bulletproof Quality:** PHPStan **Level 8** (0 errors), 100% PSR-12 code style, and 132 comprehensive Pest tests.
+
+## Compatibility
+
+| ActionGuard | PHP | Laravel | Filament |
+| :--- | :--- | :--- | :--- |
+| `1.x` | `8.3โ8.5` | `11.28โ13.x` (through Filament) | `5.x` |
---
@@ -207,15 +213,23 @@ ActionGuardAction::make('publish')
| `RequiredFieldCheck` | Attribute is not `null` | `RequiredFieldCheck::make('title')` |
| `NotEmptyCheck` | Value is not empty string/whitespace/empty array/empty collection (safely preserves `0`, `'0'`, `0.00`) | `NotEmptyCheck::make('price')` |
| `ConditionCheck` | Boolean callback closure | `ConditionCheck::make('min_stock', fn ($record) => $record->stock > 0, 'Stock must be greater than zero')` |
-| `RelationshipCheck` | Eloquent relationship exists and is loaded | `RelationshipCheck::make('category')` |
+| `RelationshipCheck` | Eloquent relationship resolves to a model or non-empty collection; it may lazy-load the relation | `RelationshipCheck::make('category')` |
| `MediaCheck` | Spatie MediaLibrary or array of image URLs | `MediaCheck::make('image_url')` |
| `CallbackCheck` | Evaluates custom logic returning `CheckResult` | `CallbackCheck::make('vat_id', fn ($record) => ...)` |
---
-## โ๏ธ Fluent Customization & Zero-Config
+## โ๏ธ Secure defaults & optional configuration
+
+ActionGuard works with secure defaults without publishing a configuration file.
+To review or override its operational safeguards, publish the package config:
+
+```bash
+php artisan vendor:publish --tag="filament-actionguard-config"
+```
-ActionGuard is designed as **Zero-Configuration by Default**. It requires no separate configuration file. You can customize the modal appearance, heading, and width directly on the action using Filament's fluent API:
+You can customize the modal appearance, heading, and width directly on the
+action using Filament's fluent API:
```php
ActionGuardAction::make('publish')
@@ -224,6 +238,26 @@ ActionGuardAction::make('publish')
->checks([...]);
```
+The operational defaults and environment variables are:
+
+| Setting | Environment variable | Default |
+| :--- | :--- | :---: |
+| `enabled` | `ACTIONGUARD_ENABLED` | `true` |
+| `fail_closed` | `ACTIONGUARD_FAIL_CLOSED` | `true` |
+| `allow_bypass` | `ACTIONGUARD_ALLOW_BYPASS` | `false` |
+| `notifications` | `ACTIONGUARD_NOTIFICATIONS_ENABLED` | `true` |
+| `audit.enabled` | `ACTIONGUARD_AUDIT_TRAIL` | `false` |
+| `audit.channel` | `ACTIONGUARD_AUDIT_CHANNEL` | `null` |
+| `allow_insecure_resolution_urls` | `ACTIONGUARD_ALLOW_INSECURE_RESOLUTION_URLS` | `false` |
+
+Resolution links accept local absolute paths and HTTPS URLs. HTTP is available
+only through its explicit compatibility switch; executable, protocol-relative,
+malformed, and control-character URLs are discarded.
+
+ActionGuard evaluates checks when the modal is rendered, when confirmation is
+prepared, and immediately before execution. Checks must therefore be
+side-effect-free and safe to run repeatedly. The final evaluation is decisive.
+
---
## ๐งช Testing & Quality Assurance
@@ -231,7 +265,7 @@ ActionGuardAction::make('publish')
ActionGuard is built with strict quality standards:
```bash
-# Run automated test suite (103 tests, 234 assertions)
+# Run automated test suite (132 tests, 308 assertions)
composer test
# Run code style fixer and static analysis (PHPStan Level 8)
diff --git a/src/Actions/ActionGuardAction.php b/src/Actions/ActionGuardAction.php
index 73686dd..2a7d21e 100644
--- a/src/Actions/ActionGuardAction.php
+++ b/src/Actions/ActionGuardAction.php
@@ -31,9 +31,12 @@ protected function setUp(): void
$this->modalDescription(null);
$this->modalWidth('lg');
- $rawLabel = $this->getLabel();
- $label = $rawLabel instanceof Htmlable ? strip_tags($rawLabel->toHtml()) : (string) ($rawLabel ?? '');
- $this->modalHeading(__('filament-actionguard::ui.modal.heading', ['label' => $label]));
+ $this->modalHeading(function (): string {
+ $rawLabel = $this->getLabel();
+ $label = $rawLabel instanceof Htmlable ? strip_tags($rawLabel->toHtml()) : (string) ($rawLabel ?? '');
+
+ return __('filament-actionguard::ui.modal.heading', ['label' => $label]);
+ });
$this->modalContent(function (ActionGuardAction $action, ?Model $record) {
return $action->evaluateAndRender($record);
});
diff --git a/src/Bridge/BusinessCoreGuardAdapter.php b/src/Bridge/BusinessCoreGuardAdapter.php
index 98f12e6..a8ccbf6 100644
--- a/src/Bridge/BusinessCoreGuardAdapter.php
+++ b/src/Bridge/BusinessCoreGuardAdapter.php
@@ -9,6 +9,7 @@
use Allgorithm\FilamentActionGuard\Results\CheckResolution;
use Allgorithm\FilamentActionGuard\Results\CheckResult;
use Allgorithm\FilamentActionGuard\Results\CheckStatus;
+use Allgorithm\FilamentActionGuard\Support\ResolutionUrlSanitizer;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
@@ -106,7 +107,7 @@ protected function mapCoreResult(mixed $coreResult): CheckResult
$resolution = $coreResult->resolution;
} elseif (is_object($coreResult->resolution) && property_exists($coreResult->resolution, 'label')) {
$action = property_exists($coreResult->resolution, 'action')
- ? $this->sanitizeResolutionUrl($coreResult->resolution->action)
+ ? ResolutionUrlSanitizer::sanitize($coreResult->resolution->action)
: null;
$resolution = new CheckResolution(
label: (string) $coreResult->resolution->label,
@@ -137,29 +138,4 @@ protected function makeOperationContext(Model $record, string $correlationId): o
$correlationId,
);
}
-
- protected function sanitizeResolutionUrl(mixed $url): ?string
- {
- if (! is_string($url)) {
- return null;
- }
-
- $url = trim($url);
- if ($url === '' || preg_match('/[\x00-\x20\x7F\\\\]/', $url) === 1 || str_starts_with($url, '//')) {
- return null;
- }
-
- if (str_starts_with($url, '/')) {
- return $url;
- }
-
- $scheme = parse_url($url, PHP_URL_SCHEME);
-
- $allowedSchemes = ['https'];
- if (config('filament-actionguard.allow_insecure_resolution_urls', false)) {
- $allowedSchemes[] = 'http';
- }
-
- return in_array(strtolower((string) $scheme), $allowedSchemes, true) ? $url : null;
- }
}
diff --git a/src/Results/CheckResolution.php b/src/Results/CheckResolution.php
index ec81cf1..b9d7cda 100644
--- a/src/Results/CheckResolution.php
+++ b/src/Results/CheckResolution.php
@@ -4,10 +4,19 @@
namespace Allgorithm\FilamentActionGuard\Results;
+use Allgorithm\FilamentActionGuard\Support\ResolutionUrlSanitizer;
+
final readonly class CheckResolution
{
+ public string $label;
+
+ public ?string $url;
+
public function __construct(
- public string $label,
- public ?string $url = null,
- ) {}
+ string $label,
+ ?string $url = null,
+ ) {
+ $this->label = $label;
+ $this->url = ResolutionUrlSanitizer::sanitize($url);
+ }
}
diff --git a/src/Support/ResolutionUrlSanitizer.php b/src/Support/ResolutionUrlSanitizer.php
new file mode 100644
index 0000000..019d5e5
--- /dev/null
+++ b/src/Support/ResolutionUrlSanitizer.php
@@ -0,0 +1,33 @@
+and($result->checks[0]->message)->toContain('Reference:');
});
+it('allows an action to continue after a technical error only with explicit fail-open configuration', function () {
+ config()->set('filament-actionguard.fail_closed', false);
+ $executed = false;
+ $record = new class extends Model {};
+ $action = ActionGuardAction::make('publish')
+ ->checks([
+ new class implements ActionGuardCheckContract
+ {
+ public function evaluate(Model $record): CheckResult
+ {
+ throw new RuntimeException('technical outage');
+ }
+ },
+ ])
+ ->action(function () use (&$executed): void {
+ $executed = true;
+ })
+ ->record($record);
+
+ $action->callBefore();
+ $action->call(['record' => $record]);
+
+ expect($executed)->toBeTrue();
+});
+
it('renders preflight modal view with correct result payload', function () {
$action = ActionGuardAction::make('publish')
->checks([
@@ -181,3 +207,47 @@ public function evaluate(Model $record): CheckResult
expect($data['passed'])->toBeTrue()
->and($data['result']->summary['passed'])->toBe(1);
});
+
+it('rejects invalid explicit checks before evaluation', function (mixed $invalidCheck) {
+ expect(fn () => ActionGuardAction::make('publish')->checks([$invalidCheck]))
+ ->toThrow(InvalidArgumentException::class);
+})->with([
+ 'object' => fn () => new stdClass,
+ 'existing incompatible class string' => stdClass::class,
+ 'unknown class string' => 'Tests\\MissingActionGuardCheck',
+ 'scalar' => 42,
+]);
+
+it('strips HTML from a late-bound Htmlable action label in the modal heading', function () {
+ $action = ActionGuardAction::make('publish')
+ ->label(new HtmlString('Publish safely'));
+
+ expect($action->getModalHeading())
+ ->toBe('Publish safelyalert(1)')
+ ->not->toContain('')
+ ->not->toContain('