Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .agents/skills/espocrm-guard/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ description: Use when an agent works in Guard-Core/espocrm-guard or touches its
## Quick Reference

- EspoCRM extension "Guard Core", module `GuardCore`, namespace `Espo\Modules\GuardCore\Api`. EspoCRM `>=9.0.0`, PHP `>=8.2`.
- Registers `Espo\Modules\GuardCore\Api\Middleware` as a global API middleware. It does nothing until `guardEnabled` is true in `data/config.php`.
- `guard` is the settings map: every engine option is a `SecurityConfig` argument, every route option a `RouteConfig` argument. Unknown keys throw. Closures and objects cannot be configured.
- `guard` and `guardEnabled` are level `system` in `Resources/metadata/app/config.json`, so the Settings API never returns them. `src/scripts/AfterUninstall.php` removes them on uninstall.
- Registers `Espo\Modules\GuardCore\Api\Middleware` as a global API middleware. It does nothing until `guardCore.enabled` is true in `data/config-internal.php`.
- `guardCore` is the settings map (`enabled` plus every setting): every engine option is a `SecurityConfig` argument, every route option a `RouteConfig` argument. Unknown keys throw. Closures and objects cannot be configured.
- `guardCore`, `guard` and `guardEnabled` are level `system` in `Resources/metadata/app/config.json`, so the Settings API never returns them. `src/scripts/AfterUninstall.php` removes them on uninstall. `guardEnabled` and `guard` are the deprecated 1.0.0 keys: they still work with a warning, and `src/scripts/AfterInstall.php` moves them under `guardCore` on upgrade. Both at once while enabled is an error.
- The extension holds no security logic. Every verdict comes from guard-core-php through psr15-guard.
- Commands: `make prepare-test`, `make lint`, `make local-test` (Redis on 127.0.0.1:6420), `make test` (Docker), `make extension`, `make bump-version VERSION=x.y.z`.

## Default policy

With only `guardEnabled` set: passive mode, attack screening on, no body scan on `POST /api/v1/Attachment/chunk/`, a ban after more than 10 responses with status 401 in 5 minutes on `GET /api/v1/App/user` and `/api/v1/`, rate limits on `POST /api/v1/LeadCapture/` (10 per minute) and the two password routes (5 per 5 minutes). Operator keys override key by key, a `null` route entry switches a default route off.
With only `guardCore.enabled` set: passive mode, attack screening on, no body scan on `POST /api/v1/Attachment/chunk/`, a ban after more than 10 responses with status 401 in 5 minutes on `GET /api/v1/App/user` and `/api/v1/`, rate limits on `POST /api/v1/LeadCapture/` (10 per minute) and the two password routes (5 per 5 minutes). Operator keys override key by key, a `null` route entry switches a default route off.

## Gotchas

Expand Down
8 changes: 4 additions & 4 deletions .github/ISSUE_TEMPLATE/bug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ body:
attributes:
label: Steps to reproduce
placeholder: |
1. Set the 'guard' config to '...'
1. Set the 'guardCore' config to '...'
2. Send a request to '...'
3. See error
validations:
Expand All @@ -40,11 +40,11 @@ body:
id: config
attributes:
label: Configuration
description: The `guard` array from data/config.php with secrets (Redis URL, agent API key) masked.
description: The `guardCore` array from data/config-internal.php with secrets (Redis URL, agent API key) masked.
render: php
placeholder: |
'guardEnabled' => true,
'guard' => [
'guardCore' => [
'enabled' => true,
'passiveMode' => false,
],
- type: textarea
Expand Down
8 changes: 4 additions & 4 deletions .github/scripts/smoke-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ expect() {
echo "ok: $name -> $code"
}

# settings_hide_guard <base url>: GET /api/v1/Settings must not carry guard or guardEnabled, for the admin.
# settings_hide_guard <base url>: GET /api/v1/Settings must not carry guardCore, guard or guardEnabled, for the admin.
settings_hide_guard() {
expect "GET /api/v1/Settings answers" 200 -u "$SMOKE_USER:$SMOKE_PASSWORD" "$1/api/v1/Settings"
if grep -Eq '"guard(Enabled)?"' "$SMOKE_BODY"; then
echo "FAIL: the Settings API exposes guard or guardEnabled"
if grep -Eq '"guard(Core|Enabled)?"' "$SMOKE_BODY"; then
echo "FAIL: the Settings API exposes guardCore, guard or guardEnabled"
exit 1
fi
echo "ok: guard and guardEnabled are absent from GET /api/v1/Settings"
echo "ok: guardCore, guard and guardEnabled are absent from GET /api/v1/Settings"
}

# log_contains <compose args...> -- <pattern>: greps EspoCRM's log files in the espocrm service.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/greetings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
Thanks for opening your first issue on **espocrm-guard**!

To help us triage quickly, please make sure your report includes:
- A clear reproduction (the `guard` config and the request you sent are ideal)
- A clear reproduction (the `guardCore` config and the request you sent are ideal)
- Output of `php -v` (the extension requires PHP 8.2 or later) and your EspoCRM version
- The extension version and the `rennf93/psr15-guard` and `rennf93/guard-core-php` versions in the module's `vendor/`
- Whether EspoCRM runs behind a reverse proxy, and the `guard.trustedProxies` you set
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/stale.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
If it's still relevant, please leave a comment, otherwise it will be closed in 14 days.

Things that help keep an issue moving:
- A current reproduction (the `guard` config and the request)
- A current reproduction (the `guardCore` config and the request)
- Output of `php -v` and your EspoCRM and extension versions
- Any new findings since the last activity

Expand Down
17 changes: 9 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ Guidance for AI agents (including Claude Code) working in this repository.

## Project Overview

Guard-Core/espocrm-guard (https://github.com/Guard-Core/espocrm-guard) is an EspoCRM extension, "Guard Core for EspoCRM". It registers the psr15-guard middleware as a global API middleware (`globalMiddlewareClassNameList`) so the guard-core-php engine screens `public/api/v1/` and the portal API. It is off until `guardEnabled` is true, and the keys `guardEnabled` and `guard` are hidden from the Settings API (level `system`).
Guard-Core/espocrm-guard (https://github.com/Guard-Core/espocrm-guard) is an EspoCRM extension, "Guard Core for EspoCRM". It registers the psr15-guard middleware as a global API middleware (`globalMiddlewareClassNameList`) so the guard-core-php engine screens `public/api/v1/` and the portal API. It is off until `guardCore.enabled` is true. The single root key `guardCore` (a map: `enabled` plus every setting) is hidden from the Settings API (level `system`), like the deprecated 1.0.0 keys `guardEnabled` and `guard`, which still work, log a deprecation warning and are migrated automatically on upgrade by `src/scripts/AfterInstall.php`.

- The extension is distributed as a zip (`build/guard-core-<version>.zip`, built by `npm run extension`) that an administrator uploads under Administration > Extensions. It is not on Packagist or any registry. The module `composer.json` is named `rennf93/espocrm-guard-core` and only bundles `rennf93/psr15-guard` and, transitively, `rennf93/guard-core-php`.
- This repository contains NO security logic. Detection, rate limiting, bans, headers and responses live in guard-core-php. The module reads the `guard` config, merges it with a default policy, builds the engine objects and wires them to EspoCRM's logger and config.
- This repository contains NO security logic. Detection, rate limiting, bans, headers and responses live in guard-core-php. The module reads the `guardCore` config, merges it with a default policy, builds the engine objects and wires them to EspoCRM's logger and config.
- EspoCRM `>=9.0.0` (`extension.json` `acceptableVersions`) and PHP `>=8.2`. The version lives in `package.json` (and `package-lock.json`); `extension.json` carries none, and the zip name takes it from `package.json`.
- Releases are tag driven. `master` is protected: never push to it, never merge into it, never create tags or releases as part of agent work. Releases are immutable, so `release.yml` attaches the zip and its `.sha256` to a DRAFT release that the maintainer publishes.
- Docs site: MkDocs Material in `docs/`, versioned with mike (`.mike.yml`), deployed by `docs.yml`. `mkdocs.yml` sets `site_dir: docs_site` because `site/` is the EspoCRM checkout: a default `mkdocs build` would wipe it.
Expand All @@ -15,7 +15,7 @@ Guard-Core/espocrm-guard (https://github.com/Guard-Core/espocrm-guard) is an Esp

- `rennf93/guard-core-php` (https://github.com/Guard-Core/guard-core-php) is the engine. It owns `SecurityConfig`, `RouteConfig`, `GuardEngine`, `RedisHandler`, `BehaviorRule` and every verdict.
- `rennf93/psr15-guard` (https://github.com/Guard-Core/psr15-guard) is the PSR-15 adapter this module composes: `GuardMiddleware` takes the engine, PSR-17 factories, the route map and an optional agent handler. The module floors it at `^1.6.1`.
- This repository is the EspoCRM adapter layer: `Api\Middleware` is the EspoCRM middleware, `Api\GuardMiddlewareBuilder` wires logger, Redis, country database and agent, `Api\SecurityConfigBuilder` and `Api\RouteConfigBuilder` map the `guard` array onto `SecurityConfig` and `RouteConfig`, `Api\DefaultPolicy` is the policy applied when `guardEnabled` is true, `Api\GuardAgentFactory` creates the optional agent.
- This repository is the EspoCRM adapter layer: `Api\Middleware` is the EspoCRM middleware, `Api\GuardMiddlewareBuilder` wires logger, Redis, country database and agent, `Api\SecurityConfigBuilder` and `Api\RouteConfigBuilder` map the `guardCore` settings onto `SecurityConfig` and `RouteConfig`, `Api\DefaultPolicy` is the policy applied when the guard is enabled, `Api\GuardAgentFactory` creates the optional agent.
- `rennf93/guard-agent-php` is the optional telemetry agent. It is resolved by name and is not a dependency; the operator runs `composer require rennf93/guard-agent-php` in the EspoCRM root.
- `psr/http-*` and Slim come from EspoCRM itself. The module `composer.json` `replace`s the four `psr/http-*` packages so none ends up in the zip, and `GuardMiddlewareBuilder` uses EspoCRM's `Slim\Psr7` factories.

Expand All @@ -24,9 +24,9 @@ Guard-Core/espocrm-guard (https://github.com/Guard-Core/espocrm-guard) is an Esp
The extension wires; the engine decides.

- MUST NOT implement detection rules, penetration signatures, rate limiting, IP banning, security headers or CORS. The extension only translates config into engine objects and copies nothing but the engine's verdicts.
- MUST stay opt-in: `Middleware::process()` returns `$handler->handle($request)` and builds nothing unless `guardEnabled` is true.
- MUST fail loudly on bad config: an unknown `guard` key, an unknown route option, an invalid behavior rule, an unsupported Redis URL or an `agent` map without the agent package throws instead of being ignored.
- MUST keep `guard` and `guardEnabled` out of every API response: `Resources/metadata/app/config.json` declares both at level `system`, and `src/scripts/AfterUninstall.php` removes both from the config files when the module is uninstalled.
- MUST stay opt-in: `Middleware::process()` returns `$handler->handle($request)` and builds nothing unless `guardCore.enabled` (or the deprecated `guardEnabled`) is true.
- MUST fail loudly on bad config: an unknown `guardCore` key, `guardCore` that is not a map, settings split between `guardCore` and the old keys, an unknown route option, an invalid behavior rule, an unsupported Redis URL or an `agent` map without the agent package throws instead of being ignored.
- MUST keep `guardCore`, `guard` and `guardEnabled` out of every API response: `Resources/metadata/app/config.json` declares all three at level `system`, and `src/scripts/AfterUninstall.php` removes all three from the config files when the module is uninstalled.
- MUST NOT bundle `psr/*` or Slim in the zip. `.github/scripts/check-extension-zip.sh` checks the zip contents and size on every CI build and release.
- MUST NOT log or store anything beyond the engine verdict lines (check name, reason, client IP, method, path, status) it writes through EspoCRM's logger.

Expand Down Expand Up @@ -59,9 +59,10 @@ The extension wires; the engine decides.
src/files/custom/Espo/Modules/GuardCore/Api/ the module classes
src/files/custom/Espo/Modules/GuardCore/Resources/ module.json, autoload.json, metadata/app/{api,config}.json
src/files/custom/Espo/Modules/GuardCore/composer.json, composer.lock module libraries (the lock is what ships)
src/scripts/AfterUninstall.php removes guardEnabled and guard on uninstall
src/scripts/AfterInstall.php migrates guardEnabled and guard into guardCore on install and upgrade
src/scripts/AfterUninstall.php removes guardCore, guardEnabled and guard on uninstall
.agents/skills/espocrm-guard/SKILL.md agent skill (outside src/, so it is not in the zip)
tests/unit/Espo/Modules/GuardCore/ PHPUnit suite (MiddlewareTest, GuardAgentFactoryTest, AfterUninstallTest)
tests/unit/Espo/Modules/GuardCore/ PHPUnit suite (MiddlewareTest, GuardAgentFactoryTest, AfterInstallTest, AfterUninstallTest)
php_scripts/, build.js, config-default.json espo-extension-tools helpers and build
examples/simple_app, examples/advanced_app Docker Compose stacks with a real EspoCRM and the built zip
docs/, mkdocs.yml, .mike.yml docs site
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,36 @@ Release Notes

___

v1.1.0 (2026-10-31)
-------------------

One extension-owned config key, `guardCore`, with an automatic migration (v1.1.0)
---------------------------------------------------------------------------------

### Added

- **One root config key, `guardCore`.** It is a map that holds `enabled` plus every setting that lived in `guard`, Redis settings included, so nothing collides with the config keys of other extensions. It is declared at level `system`, so the Settings API never returns it, and EspoCRM stores it in `data/config-internal.php`.
- **An automatic migration on upgrade.** A new `AfterInstall` script moves `guardEnabled` and `guard` into `guardCore` (`enabled` first, then the settings) and removes the old keys. It changes nothing on a fresh install, when `guardCore` is already set, or when `guard` is not a map (the middleware reports that one loudly). Running it twice changes nothing.
- **A docs page on route policies** (#15): how a request finds its route, what switching a default route off does, and every route option and behavior rule key with its unit.

### Changed

- **`guardEnabled` and `guard` are deprecated.** They keep working, and the guard logs one warning when it is built, pointing to docs/configuration.md. Setting `guardCore` and an old key at the same time while the guard is on throws an error that asks to move everything under `guardCore`. When both say the guard is off, nothing happens.
- **The uninstall also removes `guardCore`.** An upgrade does not run the old version's uninstall script, so the migration is what moves the old keys.
- **The docs, the examples and the live smoke checks use `guardCore`.** The configuration docs say to put it in `data/config-internal.php`, because EspoCRM stores level `system` parameters there and rewrites them there on any config save.

### Fixed

- **Uninstall and the migration now clear the old keys from both config files.** On EspoCRM 9.0 to 9.2, removing a level `system` parameter only touched `data/config-internal.php`, so uninstalling left `guardEnabled` and `guard` in `data/config.php`, where they lose their `system` level and the Settings API can return them. The migration had the same gap and would have left them next to `guardCore`. Both scripts now remove them from both files.
- **The migration leaves old keys in an override file alone.** EspoCRM never writes `data/config-override.php` or `data/config-internal-override.php`, so when either holds `guardEnabled` or `guard` nothing is migrated and the old keys keep working with the deprecation warning.

### Test

- New unit tests run both scripts against EspoCRM's real `Config` and `ConfigWriter` and assert the file contents (keys in `config.php`, in `config-internal.php`, split across both, in an override file, a fresh install, a second run and an uninstall).
- New unit tests cover the `guardCore` resolution (enabled, disabled, missing `enabled`, not a bool, not a map, split settings), the deprecation warning once per build, every case of the migration including a second run, and the three-key uninstall. Line coverage of the module stays at 100%.

___

v1.0.0 (2026-10-20)
-------------------

Expand Down
Loading
Loading