├── tests ├── end-to-end │ ├── .eslintignore │ ├── .gitignore │ ├── utils │ │ ├── icon.png │ │ ├── constants.js │ │ ├── helpers.js │ │ └── watcherClient.js │ ├── .prettierrc │ ├── assets │ │ ├── too-big.jpg │ │ └── small-enough.jpg │ ├── jest.config.js │ ├── __scripts__ │ │ ├── test-init.sh │ │ └── run-tests.sh │ ├── jest-playwright.config.js │ ├── .eslintrc │ ├── testSequencer.js │ ├── __tests__ │ │ ├── 6-display-error-on.js │ │ ├── 5-display-error-off.js │ │ ├── 9-appsec-timeout-ban.js │ │ ├── 10-appsec-timeout-bypass.js │ │ ├── 4-geolocation.js │ │ ├── 8-appsec-timeout-captcha.js │ │ ├── 2-live-mode-with-geolocation.js │ │ ├── 11-appsec-max-body-ban.js │ │ ├── 12-appsec-upload.js │ │ ├── 3-stream-mode.js │ │ ├── 7-appsec.js │ │ └── 1-live-mode.js │ ├── package.json │ ├── CustomEnvironment.js │ └── settings │ │ └── base.php.dist ├── scripts │ ├── public │ │ ├── testappsec.php │ │ ├── protected-page.php │ │ ├── geolocation-test.php │ │ ├── cache-actions.php │ │ └── testappsec-upload.php │ ├── clear-cache.php │ └── standalone-check-ip-live.php ├── Integration │ ├── StandaloneBouncerNoResponse.php │ ├── TestHelpers.php │ ├── WatcherClient.php │ └── GeolocationTest.php ├── PHPUnitUtil.php └── Unit │ └── BouncerTest.php ├── tools └── coding-standards │ ├── phpstan │ └── phpstan.neon │ ├── .gitignore │ ├── composer.json │ ├── php-cs-fixer │ └── .php-cs-fixer.dist.php │ ├── psalm │ └── psalm.xml │ ├── phpmd │ └── rulesets.xml │ └── phpunit │ └── phpunit.xml ├── docs ├── images │ ├── logo_crowdsec.png │ └── screenshots │ │ ├── front-ban.jpg │ │ └── front-captcha.jpg ├── INSTALLATION_GUIDE.md ├── USER_GUIDE.md └── DEVELOPER.md ├── .gitignore ├── scripts ├── clear-cache.php ├── prune-cache.php ├── refresh-cache.php ├── push-usage-metrics.php ├── bounce.php └── settings.php.dist ├── .githooks └── commit-msg ├── patches └── gregwar-captcha-constructor.patch ├── src ├── Constants.php └── Bouncer.php ├── LICENSE ├── .github └── workflows │ ├── doc-links.yml │ ├── release.yml │ └── coding-standards.yml ├── composer.json ├── README.md └── CHANGELOG.md /tests/end-to-end/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /tools/coding-standards/phpstan/phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | bootstrapFiles: 3 | - ../../../vendor/autoload.php -------------------------------------------------------------------------------- /docs/images/logo_crowdsec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crowdsecurity/cs-standalone-php-bouncer/HEAD/docs/images/logo_crowdsec.png -------------------------------------------------------------------------------- /tests/end-to-end/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cookies.json 3 | .test-results*.json 4 | *.lock 5 | *.log 6 | *.jpg 7 | !assets/*.jpg 8 | -------------------------------------------------------------------------------- /tests/end-to-end/utils/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crowdsecurity/cs-standalone-php-bouncer/HEAD/tests/end-to-end/utils/icon.png -------------------------------------------------------------------------------- /tests/end-to-end/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "printWidth": 80, 5 | "tabWidth": 4 6 | } 7 | -------------------------------------------------------------------------------- /tests/end-to-end/assets/too-big.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crowdsecurity/cs-standalone-php-bouncer/HEAD/tests/end-to-end/assets/too-big.jpg -------------------------------------------------------------------------------- /docs/images/screenshots/front-ban.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crowdsecurity/cs-standalone-php-bouncer/HEAD/docs/images/screenshots/front-ban.jpg -------------------------------------------------------------------------------- /docs/images/screenshots/front-captcha.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crowdsecurity/cs-standalone-php-bouncer/HEAD/docs/images/screenshots/front-captcha.jpg -------------------------------------------------------------------------------- /tests/end-to-end/assets/small-enough.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crowdsecurity/cs-standalone-php-bouncer/HEAD/tests/end-to-end/assets/small-enough.jpg -------------------------------------------------------------------------------- /tools/coding-standards/.gitignore: -------------------------------------------------------------------------------- 1 | #Tools 2 | .php-cs-fixer.cache 3 | .php-cs-fixer.php 4 | .phpunit.cache 5 | *.html 6 | *.css 7 | *.js 8 | *.svg 9 | *.txt 10 | -------------------------------------------------------------------------------- /tests/end-to-end/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "jest-playwright-preset", 3 | testRunner: "jest-circus/runner", 4 | testEnvironment: "./CustomEnvironment.js", 5 | testSequencer: "./testSequencer.js", 6 | setupFilesAfterEnv: ["expect-playwright"], 7 | }; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Composer 2 | vendor 3 | composer.lock 4 | composer-dev.* 5 | 6 | #Tools 7 | .php-cs-fixer.cache 8 | .php-cs-fixer.php 9 | 10 | # Scripts 11 | scripts/settings.php 12 | .logs 13 | .cache 14 | 15 | # MaxMind databases 16 | *.mmdb 17 | 18 | # Uploads test 19 | tests/scripts/public/uploads/* 20 | -------------------------------------------------------------------------------- /tests/scripts/public/testappsec.php: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 |In this example page, if you can see this text, AppSec considers your request as clean.
14 | 15 | 16 | '; 17 | -------------------------------------------------------------------------------- /scripts/clear-cache.php: -------------------------------------------------------------------------------- 1 | clearCache(); 14 | } 15 | -------------------------------------------------------------------------------- /scripts/prune-cache.php: -------------------------------------------------------------------------------- 1 | pruneCache(); 14 | } 15 | -------------------------------------------------------------------------------- /scripts/refresh-cache.php: -------------------------------------------------------------------------------- 1 | refreshBlocklistCache(); 14 | } 15 | -------------------------------------------------------------------------------- /tests/end-to-end/__scripts__/test-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Prepare Playwright container before testing 3 | # Usage : ./test-init.sh 4 | 5 | YELLOW='\033[33m' 6 | RESET='\033[0m' 7 | if ! ddev --version >/dev/null 2>&1; then 8 | printf "%bDdev is required for this script. Please see docs/ddev.md.%b\n" "${YELLOW}" "${RESET}" 9 | exit 1 10 | fi 11 | 12 | ddev exec -s playwright yarn --cwd ./my-code/standalone-bouncer/tests/end-to-end --force && \ 13 | ddev exec -s playwright yarn global add cross-env 14 | -------------------------------------------------------------------------------- /scripts/push-usage-metrics.php: -------------------------------------------------------------------------------- 1 | pushUsageMetrics(Constants::BOUNCER_NAME, Constants::VERSION); 15 | } 16 | -------------------------------------------------------------------------------- /tests/end-to-end/jest-playwright.config.js: -------------------------------------------------------------------------------- 1 | const headless = process.env.HEADLESS; 2 | const slowMo = parseFloat(process.env.SLOWMO); 3 | module.exports = { 4 | launchOptions: { 5 | headless, 6 | }, 7 | connectOptions: { slowMo }, 8 | exitOnPageError: false, 9 | contextOptions: { 10 | ignoreHTTPSErrors: true, 11 | viewport: { 12 | width: 1920, 13 | height: 1080, 14 | }, 15 | }, 16 | browsers: ["chromium"], 17 | devices: ["Desktop Chrome"], 18 | }; 19 | -------------------------------------------------------------------------------- /tests/end-to-end/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | // Required for certain syntax usages 4 | "ecmaVersion": 2018 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "airbnb-base", 9 | "plugin:prettier/recommended" 10 | ], 11 | "rules": { 12 | "no-unused-vars": 1, 13 | "no-underscore-dangle": 0, 14 | "import/no-dynamic-require": 0, 15 | "no-console": [1, { "allow": ["warn", "error", "debug"] }] 16 | }, 17 | "env": { 18 | "jest": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/end-to-end/testSequencer.js: -------------------------------------------------------------------------------- 1 | const Sequencer = require("@jest/test-sequencer").default; 2 | 3 | class CustomSequencer extends Sequencer { 4 | sort(tests) { 5 | // Test structure information 6 | // https://github.com/facebook/jest/blob/6b8b1404a1d9254e7d5d90a8934087a9c9899dab/packages/jest-runner/src/types.ts#L17-L21 7 | const copyTests = Array.from(tests); 8 | return copyTests.sort((testA, testB) => 9 | testA.path > testB.path ? 1 : -1, 10 | ); 11 | } 12 | } 13 | 14 | module.exports = CustomSequencer; 15 | -------------------------------------------------------------------------------- /.githooks/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ -z "$1" ]; then 4 | echo "Missing argument (commit message). Did you try to run this manually?" 5 | exit 1 6 | fi 7 | 8 | commitTitle="$(head -n 1 < "$1")" 9 | 10 | # ignore merge 11 | if echo "$commitTitle" | grep -qE "^Merge"; then 12 | echo "Commit hook: ignoring merge" 13 | exit 0 14 | fi 15 | 16 | # check commit message 17 | REGEX='^(feat|fix|docs|style|refactor|ci|test|chore|comment)\(.*\)\:.*' 18 | if ! echo "$commitTitle" | grep -qE "${REGEX}"; then 19 | echo "Your commit title '$commitTitle' did not follow conventional commit message rules:" 20 | echo "Please comply with the regex ${REGEX}" 21 | exit 1 22 | fi 23 | -------------------------------------------------------------------------------- /patches/gregwar-captcha-constructor.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/Gregwar/Captcha/CaptchaBuilder.php b/src/Gregwar/Captcha/CaptchaBuilder.php 2 | index ab8d93f..67e77cf 100644 3 | --- a/src/Gregwar/Captcha/CaptchaBuilder.php 4 | +++ b/src/Gregwar/Captcha/CaptchaBuilder.php 5 | @@ -133,7 +133,7 @@ class CaptchaBuilder implements CaptchaBuilderInterface 6 | */ 7 | public $tempDir = 'temp/'; 8 | 9 | - public function __construct($phrase = null, PhraseBuilderInterface $builder = null) 10 | + public function __construct($phrase = null, ?PhraseBuilderInterface $builder = null) 11 | { 12 | if ($builder === null) { 13 | $this->builder = new PhraseBuilder; 14 | -------------------------------------------------------------------------------- /tests/end-to-end/__tests__/6-display-error-on.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | const { goToPublicPage } = require("../utils/helpers"); 3 | const { CLEAN_CACHE_DURATION } = require("../utils/constants"); 4 | 5 | describe(`Should display errors`, () => { 6 | it("Should have correct settings", async () => { 7 | if (CLEAN_CACHE_DURATION !== "1") { 8 | const errorMessage = `clean_ip_cache_duration setting must be exactly 1 for this test`; 9 | console.error(errorMessage); 10 | throw new Error(errorMessage); 11 | } 12 | }); 13 | it("Should display error (if settings ko or something wrong while bouncing)", async () => { 14 | await goToPublicPage(); 15 | await expect(page).toHaveText("body", "Fatal error"); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/end-to-end/__tests__/5-display-error-off.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | const { publicHomepageShouldBeAccessible } = require("../utils/helpers"); 3 | const { CLEAN_CACHE_DURATION } = require("../utils/constants"); 4 | 5 | describe(`Should not display errors`, () => { 6 | it("Should have correct settings", async () => { 7 | if (CLEAN_CACHE_DURATION !== "1") { 8 | const errorMessage = `clean_ip_cache_duration setting must be exactly 1 for this test`; 9 | console.error(errorMessage); 10 | throw new Error(errorMessage); 11 | } 12 | }); 13 | 14 | it("Should not display error", async () => { 15 | await publicHomepageShouldBeAccessible(); 16 | await expect(page).not.toHaveText("body", "Fatal error"); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/Constants.php: -------------------------------------------------------------------------------- 1 | run(); 17 | } 18 | } catch (Throwable $e) { 19 | $displayErrors = !empty($settings['display_errors']); 20 | if ($displayErrors) { 21 | throw new BouncerException($e->getMessage(), (int) $e->getCode(), $e); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/end-to-end/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "scripts": { 4 | "test": "jest" 5 | }, 6 | "devDependencies": { 7 | "eslint": "^7.32.0", 8 | "eslint-config-airbnb-base": "^14.2.1", 9 | "eslint-config-prettier": "^8.3.0", 10 | "eslint-plugin-import": "^2.24.1", 11 | "eslint-plugin-prettier": "^3.4.0", 12 | "prettier": "^2.3.2" 13 | }, 14 | "dependencies": { 15 | "@jest/test-sequencer": "^26.6.3", 16 | "axios": "^0.21.1", 17 | "cross-env": "^7.0.3", 18 | "expect-playwright": "^0.8.0", 19 | "hosted-git-info": "^2.8.9", 20 | "jest": "^26.6.3", 21 | "jest-circus": "^26.6.3", 22 | "jest-environment-node": "^27.2.0", 23 | "jest-playwright-preset": "^1.4.3", 24 | "jest-runner": "^26.6.3", 25 | "lodash": "^4.17.21", 26 | "playwright-chromium": "1.52.0", 27 | "ws": "^7.4.6" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tools/coding-standards/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "php": ">=7.4", 4 | "friendsofphp/php-cs-fixer": "^v3.8.0", 5 | "phpstan/phpstan": "^1.8.0", 6 | "phpmd/phpmd": "^2.12.0", 7 | "squizlabs/php_codesniffer": "3.7.1", 8 | "vimeo/psalm": "^4.24.0 || ^5.26.0", 9 | "nikic/php-parser": "^4.18", 10 | "phpunit/phpunit": "^9.3", 11 | "phpunit/php-code-coverage": "^9.2.15", 12 | "mikey179/vfsstream": "^1.6.11", 13 | "crowdsec/standalone-bouncer": "@dev" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "CrowdSecStandalone\\Tests\\": "../../tests/" 18 | } 19 | }, 20 | "repositories": { 21 | "crowdsec-standalone-bouncer": { 22 | "type": "path", 23 | "url": "../../" 24 | } 25 | }, 26 | "config": { 27 | "allow-plugins": { 28 | "cweagans/composer-patches": true 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Integration/StandaloneBouncerNoResponse.php: -------------------------------------------------------------------------------- 1 | setRules([ 10 | '@Symfony' => true, 11 | '@PSR12:risky' => true, 12 | '@PHPUnit75Migration:risky' => true, 13 | 'array_syntax' => ['syntax' => 'short'], 14 | 'fopen_flags' => false, 15 | 'protected_to_private' => false, 16 | 'native_constant_invocation' => true, 17 | 'combine_nested_dirname' => true, 18 | 'phpdoc_to_comment' => false, 19 | 'concat_space' => ['spacing'=> 'one'], 20 | ]) 21 | ->setRiskyAllowed(true) 22 | ->setFinder( 23 | PhpCsFixer\Finder::create() 24 | ->in(__DIR__ . '/../../../src') 25 | ->in(__DIR__ . '/../../../scripts') 26 | ->in(__DIR__ . '/../../../tests')->exclude(['end-to-end','scripts']) 27 | ) 28 | ; -------------------------------------------------------------------------------- /tools/coding-standards/psalm/psalm.xml: -------------------------------------------------------------------------------- 1 | 2 |In this example page, if you can see this text, the bouncer considers your IP as clean.
33 | 34 | 35 |Use the below form to send a POST request to the current url.
36 | 37 | 38 | 39 | 40 | 41 |
',
89 | );
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Create Release
2 | # example: gh workflow run release.yml -f tag_name=v1.1.4
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | tag_name:
7 | type: string
8 | required: true
9 |
10 | jobs:
11 | prepare-release:
12 | name: Prepare release
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Check naming convention
17 | run: |
18 | VERIF=$(echo ${{ github.event.inputs.tag_name }} | grep -E "^v([0-9]{1,}\.)([0-9]{1,}\.)([0-9]{1,})(-(alpha|beta)\.[0-9]{1,})?$")
19 | if [ ! ${VERIF} ]
20 | then
21 | echo "Tag name '${{ github.event.inputs.tag_name }}' does not comply with naming convention vX.Y.Z"
22 | exit 1
23 | fi
24 |
25 | - name: Set version number without v
26 | run: |
27 | echo "VERSION_NUMBER=$(echo ${{ github.event.inputs.tag_name }} | sed 's/v//g' )" >> $GITHUB_ENV
28 |
29 | - name: Clone sources
30 | uses: actions/checkout@v4
31 |
32 | - name: Check version ${{ env.VERSION_NUMBER }} consistency in files
33 | # Check src/Constants.php and CHANGELOG.md
34 | run: |
35 | # Check public const VERSION = 'vVERSION_NUMBER'; in CHANGELOG.md in src/Constants.php
36 | CONSTANT_VERSION=$(grep -E "public const VERSION = 'v(.*)';" src/Constants.php | sed 's/ //g')
37 | if [[ $CONSTANT_VERSION == "publicconstVERSION='v${{ env.VERSION_NUMBER }}';" ]]
38 | then
39 | echo "CONSTANT VERSION OK"
40 | else
41 | echo "CONSTANT VERSION KO"
42 | exit 1
43 | fi
44 |
45 | # Check top ## [VERSION_NUMBER](GITHUB_URL/releases/tag/vVERSION_NUMBER) - yyyy-mm-dd in CHANGELOG.md
46 | CURRENT_DATE=$(date +'%Y-%m-%d')
47 | echo $CURRENT_DATE
48 | CHANGELOG_VERSION=$(grep -o -E "## \[(.*)\].* - $CURRENT_DATE" CHANGELOG.md | head -1 | sed 's/ //g')
49 | echo $CHANGELOG_VERSION
50 | if [[ $CHANGELOG_VERSION == "##[${{ env.VERSION_NUMBER }}]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/v${{ env.VERSION_NUMBER }})-$CURRENT_DATE" ]]
51 | then
52 | echo "CHANGELOG VERSION OK"
53 | else
54 | echo "CHANGELOG VERSION KO"
55 | exit 1
56 | fi
57 |
58 | # Check top [_Compare with previous release_](GITHUB_URL/compare/vLAST_TAG...vVERSION_NUMBER) in CHANGELOG.md
59 | COMPARISON=$(grep -oP "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/compare/\K(.*)$" CHANGELOG.md | head -1)
60 | LAST_TAG=$(curl -Ls -o /dev/null -w %{url_effective} $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/latest | grep -oP "\/tag\/\K(.*)$")
61 | if [[ $COMPARISON == "$LAST_TAG...v${{ env.VERSION_NUMBER }})" ]]
62 | then
63 | echo "VERSION COMPARISON OK"
64 | else
65 | echo "VERSION COMPARISON KO"
66 | echo $COMPARISON
67 | echo "$LAST_TAG...v${{ env.VERSION_NUMBER }})"
68 | exit 1
69 | fi
70 |
71 | - name: Create Tag ${{ github.event.inputs.tag_name }}
72 | uses: actions/github-script@v7
73 | with:
74 | github-token: ${{ github.token }}
75 | script: |
76 | github.rest.git.createRef({
77 | owner: context.repo.owner,
78 | repo: context.repo.repo,
79 | ref: "refs/tags/${{ github.event.inputs.tag_name }}",
80 | sha: context.sha
81 | })
82 |
83 | - name: Prepare release notes
84 | run: |
85 | VERSION_RELEASE_NOTES=$(awk -v ver="[${{ env.VERSION_NUMBER }}]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/v${{ env.VERSION_NUMBER }})" '/^## / { if (p) { exit }; if ($2 == ver) { p=1; next} } p && NF' CHANGELOG.md | sed ':a;N;$!ba;s/\n---/ /g')
86 | echo "$VERSION_RELEASE_NOTES" >> CHANGELOG.txt
87 |
88 | - name: Create release ${{ env.VERSION_NUMBER }}
89 | uses: softprops/action-gh-release@v1
90 | with:
91 | body_path: CHANGELOG.txt
92 | name: ${{ env.VERSION_NUMBER }}
93 | tag_name: ${{ github.event.inputs.tag_name }}
94 | draft: ${{ github.event.inputs.draft }}
95 | prerelease: ${{ github.event.inputs.prerelease }}
96 |
--------------------------------------------------------------------------------
/.github/workflows/coding-standards.yml:
--------------------------------------------------------------------------------
1 | name: Coding standards
2 | on:
3 | push:
4 | branches:
5 | - main
6 | paths-ignore:
7 | - '**.md'
8 | workflow_dispatch:
9 | inputs:
10 | coverage_report:
11 | type: boolean
12 | description: Generate PHPUNIT Code Coverage report
13 | default: false
14 |
15 | permissions:
16 | contents: read
17 |
18 | env:
19 | # Allow ddev get to use a GitHub token to prevent rate limiting by tests
20 | DDEV_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21 |
22 | jobs:
23 | coding-standards:
24 | strategy:
25 | fail-fast: false
26 | matrix:
27 | php-version: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4']
28 |
29 | name: Coding standards
30 | runs-on: ubuntu-latest
31 | if: ${{ !contains(github.event.head_commit.message, 'chore(') }}
32 | env:
33 | EXTENSION_PATH: "my-code/standalone-bouncer"
34 | DDEV_PROJECT: "crowdsec-standalone-bouncer"
35 |
36 | steps:
37 |
38 | - name: Install DDEV
39 | # @see https://ddev.readthedocs.io/en/stable/#installationupgrade-script-linux-and-macos-armarm64-and-amd64-architectures
40 | run: |
41 | curl -fsSL https://apt.fury.io/drud/gpg.key | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/ddev.gpg > /dev/null
42 | echo "deb [signed-by=/etc/apt/trusted.gpg.d/ddev.gpg] https://apt.fury.io/drud/ * *" | sudo tee /etc/apt/sources.list.d/ddev.list
43 | sudo apt-get -q update
44 | sudo apt-get -q -y install libnss3-tools ddev
45 | mkcert -install
46 | ddev config global --instrumentation-opt-in=false --omit-containers=ddev-ssh-agent
47 |
48 | - name: Create empty PHP DDEV project
49 | run: ddev config --project-type=php --project-name=${{env.DDEV_PROJECT}} --php-version=${{ matrix.php-version }}
50 |
51 | - name: Add Redis, Memcached and Crowdsec
52 | run: |
53 | ddev get ddev/ddev-redis
54 | ddev get ddev/ddev-memcached
55 | # override redis.conf
56 | ddev get julienloizelet/ddev-tools
57 | ddev get julienloizelet/ddev-crowdsec-php
58 |
59 | - name: Start DDEV
60 | uses: nick-fields/retry@v2
61 | with:
62 | timeout_minutes: 5
63 | max_attempts: 3
64 | shell: bash
65 | command: ddev start
66 |
67 | - name: Some DEBUG information
68 | run: |
69 | ddev --version
70 | ddev exec php -v
71 | ddev php -r "echo phpversion('memcached');"
72 |
73 | - name: Clone PHP lib Crowdsec files
74 | uses: actions/checkout@v4
75 | with:
76 | path: my-code/standalone-bouncer
77 |
78 | - name: Install CrowdSec lib dependencies
79 | run: ddev composer update --working-dir ./${{env.EXTENSION_PATH}}
80 |
81 | - name: Install Coding standards tools
82 | run: ddev composer update --working-dir=./${{env.EXTENSION_PATH}}/tools/coding-standards
83 |
84 | - name: Run PHPCS
85 | run: ddev phpcs ./${{env.EXTENSION_PATH}}/tools/coding-standards ${{env.EXTENSION_PATH}}/src PSR12
86 |
87 | - name: Run PHPSTAN
88 | run: ddev phpstan /var/www/html/${{env.EXTENSION_PATH}}/tools/coding-standards phpstan/phpstan.neon /var/www/html/${{env.EXTENSION_PATH}}/src
89 |
90 | - name: Run PHPMD
91 | run: ddev phpmd ./${{env.EXTENSION_PATH}}/tools/coding-standards phpmd/rulesets.xml ../../src
92 |
93 | - name: Run PSALM
94 | if: contains(fromJson('["7.4","8.0","8.1","8.2","8.3"]'),matrix.php-version)
95 | run: ddev psalm ./${{env.EXTENSION_PATH}}/tools/coding-standards ./${{env.EXTENSION_PATH}}/tools/coding-standards/psalm
96 |
97 | - name: Prepare for Code Coverage
98 | if: github.event.inputs.coverage_report == 'true'
99 | run: |
100 | mkdir ${{ github.workspace }}/cfssl
101 | cp -r .ddev/okaeli-add-on/custom_files/crowdsec/cfssl/* ${{ github.workspace }}/cfssl
102 | ddev maxmind-download DEFAULT GeoLite2-City /var/www/html/${{env.EXTENSION_PATH}}/tests
103 | ddev maxmind-download DEFAULT GeoLite2-Country /var/www/html/${{env.EXTENSION_PATH}}/tests
104 | cd ${{env.EXTENSION_PATH}}/tests
105 | sha256sum -c GeoLite2-Country.tar.gz.sha256.txt
106 | sha256sum -c GeoLite2-City.tar.gz.sha256.txt
107 | tar -xf GeoLite2-Country.tar.gz
108 | tar -xf GeoLite2-City.tar.gz
109 | rm GeoLite2-Country.tar.gz GeoLite2-Country.tar.gz.sha256.txt GeoLite2-City.tar.gz GeoLite2-City.tar.gz.sha256.txt
110 | echo "BOUNCER_KEY=$(ddev create-bouncer)" >> $GITHUB_ENV
111 |
112 | - name: Run PHPUNIT Code Coverage
113 | if: github.event.inputs.coverage_report == 'true'
114 | run: |
115 | ddev xdebug
116 | ddev exec XDEBUG_MODE=coverage BOUNCER_KEY=${{ env.BOUNCER_KEY }} APPSEC_URL=http://crowdsec:7422 AGENT_TLS_PATH=/var/www/html/cfssl LAPI_URL=https://crowdsec:8080 MEMCACHED_DSN=memcached://memcached:11211 REDIS_DSN=redis://redis:6379 /usr/bin/php ./${{env.EXTENSION_PATH}}/tools/coding-standards/vendor/bin/phpunit --configuration ./${{env.EXTENSION_PATH}}/tools/coding-standards/phpunit/phpunit.xml --coverage-text=./${{env.EXTENSION_PATH}}/coding-standards/phpunit/code-coverage/report.txt
117 | cat ${{env.EXTENSION_PATH}}/coding-standards/phpunit/code-coverage/report.txt
118 |
--------------------------------------------------------------------------------
/tests/end-to-end/__scripts__/run-tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Run test suite
3 | # Usage: ./run-tests.sh ', $result, 'Should be no error message'); 253 | 254 | $result = PHPUnitUtil::callMethod( 255 | $bouncer, 256 | 'getCaptchaHtml', 257 | [true, 'fake-inline-image', 'fake-url'] 258 | ); 259 | $this->assertStringContainsString('
', $result, 'Should be no error message');
260 |
261 | // shouldNotCheckResolution
262 | $result = PHPUnitUtil::callMethod(
263 | $bouncer,
264 | 'shouldNotCheckResolution',
265 | [['has_to_be_resolved' => false]]
266 | );
267 |
268 | $this->assertEquals(true, $result, 'No check if no flagged has_to_be_resolved');
269 |
270 | $_SERVER['REQUEST_METHOD'] = 'GET';
271 | $result = PHPUnitUtil::callMethod(
272 | $bouncer,
273 | 'shouldNotCheckResolution',
274 | [['has_to_be_resolved' => true]]
275 | );
276 |
277 | $this->assertEquals(true, $result, 'No check if method is not POST');
278 |
279 | $_SERVER['REQUEST_METHOD'] = 'POST';
280 | $_POST['crowdsec_captcha'] = 'test';
281 | $result = PHPUnitUtil::callMethod(
282 | $bouncer,
283 | 'shouldNotCheckResolution',
284 | [['has_to_be_resolved' => true]]
285 | );
286 |
287 | $this->assertEquals(false, $result, 'Check if method is POST and posted crowdsec_captcha');
288 | unset($_POST['crowdsec_captcha']);
289 | $result = PHPUnitUtil::callMethod(
290 | $bouncer,
291 | 'shouldNotCheckResolution',
292 | [['has_to_be_resolved' => true]]
293 | );
294 |
295 | $this->assertEquals(true, $result, 'No check if method is POST and no posted crowdsec_captcha');
296 |
297 | // shouldTrustXforwardedFor
298 | unset($_POST['crowdsec_captcha']);
299 | $result = PHPUnitUtil::callMethod(
300 | $bouncer,
301 | 'shouldTrustXforwardedFor',
302 | ['not-an-ip']
303 | );
304 | $this->assertEquals(false, $result, 'Should return false if ip is invalid');
305 |
306 | // handleTrustedIpsConfig
307 | $result = PHPUnitUtil::callMethod(
308 | $bouncer,
309 | 'handleTrustedIpsConfig',
310 | [['trust_ip_forward_array' => ['1.2.3.4']]]
311 | );
312 |
313 | $this->assertEquals(['trust_ip_forward_array' => [['001.002.003.004', '001.002.003.004']]], $result, 'Should
314 | return comparable array');
315 |
316 | $error = '';
317 |
318 | try {
319 | PHPUnitUtil::callMethod(
320 | $bouncer,
321 | 'handleTrustedIpsConfig',
322 | [['trust_ip_forward_array' => [['001.002.003.004', '001.002.003.004']]]]
323 | );
324 | } catch (BouncerException $e) {
325 | $error = $e->getMessage();
326 | }
327 |
328 | PHPUnitUtil::assertRegExp(
329 | $this,
330 | '/config must be an array of string/',
331 | $error,
332 | 'Should have throw an error'
333 | );
334 | if (PHP_VERSION_ID >= 80400 && isset($originalErrorReporting)) {
335 | // Restore the original error reporting level
336 | error_reporting($originalErrorReporting);
337 | }
338 | }
339 | }
340 |
--------------------------------------------------------------------------------
/scripts/settings.php.dist:
--------------------------------------------------------------------------------
1 | Constants::BOUNCING_LEVEL_NORMAL,
30 |
31 | /** If you use a CDN, a reverse proxy or a load balancer, you can use this setting to whitelist their IPs.
32 | *
33 | * For other IPs, the bouncer will not trust the X-Forwarded-For header.
34 | *
35 | * With the Standalone bouncer, you have to set an array of Ips : ['1.2.3.4', '5.6.7.8'] for example
36 | * The standalone bouncer will automatically transform this array to an array of comparable IPs arrays:
37 | * [['001.002.003.004', '001.002.003.004'], ['005.006.007.008', '005.006.007.008']]
38 | *
39 | * If you use your own bouncer, you should have to set directly an array of comparable IPs arrays
40 | */
41 | 'trust_ip_forward_array' => [],
42 | /**
43 | * By default, the lib call the REST LAPI using file_get_contents method (allow_url_fopen is required).
44 | * Set 'use_curl' to true in order to use cURL request instead (curl is in then required).
45 | */
46 | 'use_curl' => false,
47 | /**
48 | * By default, we use the LAPI to get the decision. Set 'use_appsec' to true in order to also the AppSec endpoint,
49 | * in case of no decision from LAPI ("bypass" remediation).
50 | * If true, the AppSec URL must be set in the 'appsec_url' setting below.
51 | */
52 | 'use_appsec' => false,
53 | /**
54 | * Maximum size of request body (in kilobytes) which, if exceeded,
55 | * will trigger the action defined by the "appsec_body_size_exceeded_action" setting below.
56 | *
57 | * This setting is not required. By default, the maximum body size is set to 1024KB (1MB).
58 | */
59 | 'appsec_max_body_size_kb' => Constants::APPSEC_DEFAULT_MAX_BODY_SIZE,
60 | /**
61 | * Action to take when the request body size exceeds the maximum size defined by the "appsec_max_body_size_kb" setting above.
62 | *
63 | * Select from:
64 | * - `headers_only` (recommended and default value): only the headers of the original request are forwarded to AppSec,
65 | * not the body.
66 | * - `allow` (not recommended): the request is considered as safe and a bypass remediation is returned, without calling AppSec.
67 | * - `block`: the request is considered as malicious and a ban remediation is returned, without calling AppSec.
68 | */
69 | 'appsec_body_size_exceeded_action' => Constants::APPSEC_ACTION_HEADERS_ONLY,
70 | /**
71 | * array of URIs that will not be bounced.
72 | */
73 | 'excluded_uris' => ['/favicon.ico'],
74 |
75 | // Select from 'phpfs' (File system cache), 'redis' or 'memcached'.
76 | 'cache_system' => Constants::CACHE_SYSTEM_PHPFS,
77 |
78 | // Set the duration we keep in cache the captcha flow variables for an IP. In seconds. Defaults to 86400.
79 | 'captcha_cache_duration' => Constants::CACHE_EXPIRATION_FOR_CAPTCHA,
80 |
81 | // true to enable verbose debug log.
82 | 'debug_mode' => false,
83 | // true to disable prod log
84 | 'disable_prod_log' => false,
85 |
86 | /** Absolute path to store log files.
87 | *
88 | * Important note: be sure this path won't be publicly accessible
89 | */
90 | 'log_directory_path' => __DIR__ . '/.logs',
91 |
92 | // true to stop the process and display errors if any.
93 | 'display_errors' => false,
94 |
95 | /** Only for test or debug purpose. Default to empty.
96 | *
97 | * If not empty, it will be used instead of the real remote ip.
98 | */
99 | 'forced_test_ip' => '',
100 |
101 | /** Only for test or debug purpose. Default to empty.
102 | *
103 | * If not empty, it will be used instead of the real forwarded ip.
104 | * If set to "no_forward", the x-forwarded-for mechanism will not be used at all.
105 | */
106 | 'forced_test_forwarded_ip' => '',
107 |
108 | // Settings for ban and captcha walls
109 | 'custom_css' => '',
110 | // true to hide CrowdSec mentions on ban and captcha walls.
111 | 'hide_mentions' => false,
112 | 'color' => [
113 | 'text' => [
114 | 'primary' => 'black',
115 | 'secondary' => '#AAA',
116 | 'button' => 'white',
117 | 'error_message' => '#b90000',
118 | ],
119 | 'background' => [
120 | 'page' => '#eee',
121 | 'container' => 'white',
122 | 'button' => '#626365',
123 | 'button_hover' => '#333',
124 | ],
125 | ],
126 | 'text' => [
127 | // Settings for captcha wall
128 | 'captcha_wall' => [
129 | 'tab_title' => 'Oops..',
130 | 'title' => 'Hmm, sorry but...',
131 | 'subtitle' => 'Please complete the security check.',
132 | 'refresh_image_link' => 'refresh image',
133 | 'captcha_placeholder' => 'Type here...',
134 | 'send_button' => 'CONTINUE',
135 | 'error_message' => 'Please try again.',
136 | 'footer' => '',
137 | ],
138 | // Settings for ban wall
139 | 'ban_wall' => [
140 | 'tab_title' => 'Oops..',
141 | 'title' => '🤭 Oh!',
142 | 'subtitle' => 'This page is protected against cyber attacks and your IP has been banned by our system.',
143 | 'footer' => '',
144 | ],
145 | ],
146 |
147 | // ============================================================================#
148 | // Client configs
149 | // ============================================================================#
150 |
151 | /** Select from 'api_key' and 'tls'.
152 | *
153 | * Choose if you want to use an API-KEY or a TLS (pki) authentification
154 | * TLS authentication is only available if you use CrowdSec agent with a version superior to 1.4.0
155 | */
156 | 'auth_type' => Constants::AUTH_KEY,
157 |
158 | /** Absolute path to the bouncer certificate.
159 | *
160 | * Only required if you choose tls as "auth_type"
161 | * Important note: be sure this path won't be publicly accessible
162 | */
163 | 'tls_cert_path' => '',
164 |
165 | /** Absolute path to the bouncer key.
166 | *
167 | * Only required if you choose tls as "auth_type"
168 | * Important note: be sure this path won't be publicly accessible
169 | */
170 | 'tls_key_path' => '',
171 |
172 | /** This option determines whether request handler verifies the authenticity of the peer's certificate.
173 | *
174 | * When negotiating a TLS or SSL connection, the server sends a certificate indicating its identity.
175 | * If "tls_verify_peer" is set to true, request handler verifies whether the certificate is authentic.
176 | * This trust is based on a chain of digital signatures,
177 | * rooted in certification authority (CA) certificates you supply using the "tls_ca_cert_path" setting below.
178 | */
179 | 'tls_verify_peer' => true,
180 |
181 | /** Absolute path to the CA used to process peer verification.
182 | *
183 | * Only required if you choose tls as "auth_type" and "tls_verify_peer" is true
184 | * Important note: be sure this path won't be publicly accessible
185 | */
186 | 'tls_ca_cert_path' => '',
187 |
188 | /** The bouncer api key to access LAPI.
189 | *
190 | * Key generated by the cscli (CrowdSec cli) command like "cscli bouncers add standalone-bouncer"
191 | * Only required if you choose api_key as "auth_type"
192 | */
193 | 'api_key' => 'YOUR_BOUNCER_API_KEY',
194 |
195 | /** Define the URL to your LAPI server, default to http://localhost:8080.
196 | *
197 | * If you have installed the CrowdSec agent on your server, it should be "http://localhost:8080" or
198 | * "https://localhost:8080"
199 | */
200 | 'api_url' => LapiConstants::DEFAULT_LAPI_URL,
201 | // Define the AppSec URL of your LAPI server, default to http://localhost:7422.
202 | 'appsec_url' => LapiConstants::DEFAULT_APPSEC_URL,
203 |
204 | // In seconds. The timeout when calling LAPI. Defaults to 120 sec.
205 | 'api_timeout' => Constants::API_TIMEOUT,
206 |
207 | // In seconds. The connection timeout when calling LAPI. Defaults to 300 sec.
208 | 'api_connect_timeout' => Constants::API_CONNECT_TIMEOUT,
209 |
210 | // In milliseconds. The timeout when calling AppSec. Defaults to 400 ms.
211 | 'appsec_timeout_ms' => Constants::APPSEC_TIMEOUT_MS,
212 |
213 | // In milliseconds. The connection timeout when calling APPSEC. Defaults to 150 ms.
214 | 'appsec_connect_timeout_ms' => Constants::APPSEC_CONNECT_TIMEOUT_MS,
215 |
216 | // ============================================================================#
217 | // Remediation engine configs
218 | // ============================================================================#
219 |
220 | /** Select from 'bypass' (minimum remediation), 'captcha' or 'ban' (maximum remediation).
221 | * Default to 'captcha'.
222 | *
223 | * Handle unknown remediations as.
224 | */
225 | 'fallback_remediation' => Constants::REMEDIATION_CAPTCHA,
226 |
227 | /** Select from 'bypass' (minimum remediation), 'captcha' (recommended) or 'ban' (maximum remediation).
228 | * Default to 'captcha'.
229 | *
230 | * Will be used as remediation in case of AppSec failure (timeout))
231 | */
232 | 'appsec_fallback_remediation' => Constants::REMEDIATION_CAPTCHA,
233 |
234 | /**
235 | * The `ordered_remediations` setting accepts an array of remediations ordered by priority.
236 | * If there are more than one decision for an IP, remediation with the highest priority will be return.
237 | * The specific remediation `bypass` will always be considered as the lowest priority (there is no need to
238 | * specify it in this setting).
239 | * This setting is not required. If you don't set any value, `['ban']` will be used by default for CAPI remediation
240 | * and `['ban', 'captcha']` for LAPI remediation.
241 | */
242 | 'ordered_remediations' => [Constants::REMEDIATION_BAN, Constants::REMEDIATION_CAPTCHA],
243 |
244 | /** Will be used only if you choose File system as cache_system.
245 | *
246 | * Important note: be sure this path won't be publicly accessible
247 | */
248 | 'fs_cache_path' => __DIR__ . '/.cache',
249 |
250 | // Will be used only if you choose Redis cache as cache_system
251 | 'redis_dsn' => 'redis://localhost:6379',
252 |
253 | // Will be used only if you choose Memcached as cache_system
254 | 'memcached_dsn' => 'memcached://localhost:11211',
255 |
256 | // Set the duration we keep in cache the fact that an IP is clean. In seconds. Defaults to 5.
257 | 'clean_ip_cache_duration' => Constants::CACHE_EXPIRATION_FOR_CLEAN_IP,
258 |
259 | // Set the duration we keep in cache the fact that an IP is bad. In seconds. Defaults to 20.
260 | 'bad_ip_cache_duration' => Constants::CACHE_EXPIRATION_FOR_BAD_IP,
261 |
262 | /** true to enable stream mode, false to enable the live mode. Default to false.
263 | *
264 | * By default, the `live mode` is enabled. The first time a stranger connects to your website, this mode
265 | * means that the IP will be checked directly by the CrowdSec API. The rest of your user’s browsing will be
266 | * even more transparent thanks to the fully customizable cache system.
267 | *
268 | * But you can also activate the `stream mode`. This mode allows you to constantly feed the bouncer with the
269 | * malicious IP list via a background task (CRON), making it to be even faster when checking the IP of your
270 | * visitors. Besides, if your site has a lot of unique visitors at the same time, this will not influence the
271 | * traffic to the API of your CrowdSec instance.
272 | */
273 | 'stream_mode' => false,
274 |
275 | // Settings for geolocation remediation (i.e. country based remediation).
276 | 'geolocation' => [
277 | // true to enable remediation based on country. Default to false.
278 | 'enabled' => false,
279 | // Geolocation system. Only 'maxmind' is available for the moment. Default to 'maxmind'
280 | 'type' => Constants::GEOLOCATION_TYPE_MAXMIND,
281 | /**
282 | * This setting will be used to set the lifetime (in seconds) of a cached country
283 | * associated to an IP. The purpose is to avoid multiple call to the geolocation system (e.g. maxmind database)
284 | * . Default to 86400. Set 0 to disable caching.
285 | */
286 | 'cache_duration' => Constants::CACHE_EXPIRATION_FOR_GEO,
287 | // MaxMind settings
288 | 'maxmind' => [
289 | /**Select from 'country' or 'city'. Default to 'country'
290 | *
291 | * These are the two available MaxMind database types.
292 | */
293 | 'database_type' => Constants::MAXMIND_COUNTRY,
294 | // Absolute path to the MaxMind database (mmdb file).
295 | // Important note: be sure this path won't be publicly accessible
296 | 'database_path' => '/some/path/GeoLite2-Country.mmdb',
297 | ],
298 | ],
299 | ];
300 |
--------------------------------------------------------------------------------
/docs/USER_GUIDE.md:
--------------------------------------------------------------------------------
1 | 
2 | # CrowdSec standalone PHP bouncer
3 |
4 | ## User Guide
5 |
6 | **Table of Contents**
7 |
8 |
9 |
10 | - [Description](#description)
11 | - [Features](#features)
12 | - [Usage](#usage)
13 | - [Configurations](#configurations)
14 | - [Bouncer behavior](#bouncer-behavior)
15 | - [Local API Connection](#local-api-connection)
16 | - [Cache](#cache)
17 | - [Geolocation](#geolocation)
18 | - [Captcha and ban wall settings](#captcha-and-ban-wall-settings)
19 | - [Debug](#debug)
20 | - [Security note](#security-note)
21 |
22 |
23 |
24 |
25 | ## Description
26 |
27 | This project provides a CrowdSec "standalone" bouncer for PHP-based websites. It is intended to be used with [an `auto_prepend_file` directive.](https://www.php.net/manual/en/ini.core.php#ini.auto-prepend-file)
28 |
29 | ## Features
30 |
31 | - CrowdSec Local API Support
32 | - Handle `ip`, `range` and `country` scoped decisions
33 | - `Live mode` or `Stream mode`
34 | - API key or TLS authentication
35 | - AppSec remediation support (only with API key authentication)
36 |
37 | - Handle different tasks:
38 | - Clear or prune cache
39 | - Refresh decisions
40 | - Push usage metrics
41 |
42 | - Large PHP matrix compatibility: from 7.2 to 8.4
43 | - Built-in support for the most known cache systems Redis, Memcached and PhpFiles
44 | - Support IpV4 and Ipv6 (Ipv6 range decisions are yet only supported in `Live mode`)
45 |
46 |
47 |
48 | ## Usage
49 |
50 | When a user is suspected by CrowdSec to be malevolent, the bouncer would either display a captcha to resolve or
51 | simply a page notifying that access is denied. If the user is considered as a clean user, he/she will access the page
52 | as normal.
53 |
54 | A ban wall could look like:
55 |
56 | 
57 |
58 | A captcha wall could look like:
59 |
60 | 
61 |
62 | With the provided standalone bouncer, it is possible to customize all the colors of these pages so that they integrate best with your design.
63 |
64 | On the other hand, all texts are also fully customizable. This will allow you, for example, to present translated pages in your users' language.
65 |
66 | ## Configurations
67 |
68 | Here is the list of available settings that you could define in the `scripts/settings.php` file:
69 |
70 | ### Bouncer behavior
71 |
72 | - `bouncing_level`: Select from `bouncing_disabled`, `normal_bouncing` or `flex_bouncing`. Choose if you want to apply CrowdSec directives (Normal bouncing) or be more permissive (Flex bouncing). With the `Flex mode`, it is impossible to accidentally block access to your site to people who don’t deserve it. This mode makes it possible to never ban an IP but only to offer a captcha, in the worst-case scenario.
73 |
74 |
75 | - `fallback_remediation`: Select from `bypass` (minimum remediation), `captcha` or `ban` (maximum remediation). Default to 'captcha'. Handle unknown remediations as.
76 |
77 |
78 | - `appsec_fallback_remediation`: Select from `bypass` (minimum remediation), `captcha` (recommended) or `ban` (maximum remediation).
79 | Default to 'captcha'. Will be used as remediation in case of AppSec failure (timeout).
80 |
81 |
82 | - `appsec_max_body_size_kb`: Maximum body size in KB to send to AppSec. Default to 1024 KB.
83 | If exceeded, the action defined by the `appsec_body_size_exceeded_action` setting below will be applied.
84 |
85 |
86 | - `appsec_body_size_exceeded_action`: Action to take when the request body size exceeds the maximum size defined by the `appsec_max_body_size_kb` setting above.
87 |
88 | Possible values are:
89 |
90 | - `headers_only` (recommended and default value): only the headers of the original request are forwarded to AppSec, not the body.
91 | - `allow` (not recommended): the request is considered as safe and a bypass remediation is returned, without calling AppSec.
92 | - `block`: the request is considered as malicious and a ban remediation is returned, without calling AppSec.
93 |
94 |
95 | - `trust_ip_forward_array`: If you use a CDN, a reverse proxy or a load balancer, set an array of IPs. For other IPs, the bouncer will not trust the X-Forwarded-For header.
96 |
97 |
98 | - `excluded_uris`: array of URIs that will not be bounced.
99 |
100 |
101 | - `stream_mode`: true to enable stream mode, false to enable the live mode. Default to false. By default, the `live mode` is enabled. The first time a user connects to your website, this mode means that the IP will be checked directly by the CrowdSec API. The rest of your user’s browsing will be even more transparent thanks to the fully customizable cache system. But you can also activate the `stream mode`. This mode allows you to constantly feed the bouncer with the malicious IP list via a background task (CRON), making it to be even faster when checking the IP of your visitors. Besides, if your site has a lot of unique visitors at the same time, this will not influence the traffic to the API of your CrowdSec instance.
102 |
103 | - `use_appsec`: true to enable AppSec remediation. Default to false. If you enable this setting, you need to define the `appsec_url` setting below. If true, and if the initial Lapi remediation is a `bypass`, a remediation based on the current request will be retrieved from the AppSec endpoint and will be used as final remediation. This feature is only available if you use `api_key` as `auth_type`.
104 |
105 | ### Local API Connection
106 |
107 | - `auth_type`: Select from `api_key` and `tls`. Choose if you want to use an API-KEY or a TLS (pki) authentification.
108 | TLS authentication is only available if you use CrowdSec agent with a version superior to 1.4.0.
109 |
110 |
111 | - `api_key`: Key generated by the cscli (CrowdSec cli) command like `cscli bouncers add standlone-php-bouncer`.
112 | Only required if you choose `api_key` as `auth_type`.
113 |
114 |
115 | - `tls_cert_path`: absolute path to the bouncer certificate (e.g. pem file).
116 | Only required if you choose `tls` as `auth_type`.
117 | **Make sure this path is not publicly accessible.** [See security note below](#security-note).
118 |
119 |
120 | - `tls_key_path`: Absolute path to the bouncer key (e.g. pem file).
121 | Only required if you choose `tls` as `auth_type`.
122 | **Make sure this path is not publicly accessible.** [See security note below](#security-note).
123 |
124 |
125 | - `tls_verify_peer`: This option determines whether request handler verifies the authenticity of the peer's certificate.
126 | Only required if you choose `tls` as `auth_type`.
127 | When negotiating a TLS or SSL connection, the server sends a certificate indicating its identity.
128 | If `tls_verify_peer` is set to true, request handler verifies whether the certificate is authentic.
129 | This trust is based on a chain of digital signatures,
130 | rooted in certification authority (CA) certificates you supply using the `tls_ca_cert_path` setting below.
131 |
132 |
133 | - `tls_ca_cert_path`: Absolute path to the CA used to process peer verification.
134 | Only required if you choose `tls` as `auth_type` and `tls_verify_peer` is set to true.
135 | **Make sure this path is not publicly accessible.** [See security note below](#security-note).
136 |
137 |
138 | - `api_url`: Define the URL to your Local API server, default to `http://localhost:8080`.
139 |
140 | - `appsec_url`: Define the URL to your AppSec server, default to `http://localhost:7422`. Only needed if you use AppSec remediation (see `use_appsec` setting above).
141 |
142 | - `api_timeout`: In seconds. The global timeout when calling Local API. Default to 120 sec. If set to a negative value
143 | or 0, timeout will be unlimited.
144 |
145 |
146 | - `api_connect_timeout`: In seconds. **Only for curl**. The timeout for the connection phase when calling Local
147 | API. Default to 300 sec. If set to a 0, timeout will be unlimited.
148 |
149 | - `appsec_timeout_ms`: In milliseconds. The global timeout when calling AppSec. Default to 400 ms. If set to a negative value or 0, timeout will be unlimited.
150 |
151 | - `appsec_connect_timeout_ms`: In milliseconds. **Only for curl**. The timeout for the connection phase when calling AppSec. Default to 150 ms. If set to a 0, timeout will be unlimited.
152 |
153 |
154 | - `use_curl`: By default, this lib call the REST Local API using `file_get_contents` method (`allow_url_fopen` is required).
155 | You can set `use_curl` to `true` in order to use `cURL` request instead (`ext-curl` is in then required)
156 |
157 | ### Cache
158 |
159 | - `cache_system`: Select from `phpfs` (PHP file cache), `redis` or `memcached`.
160 |
161 |
162 | - `fs_cache_path`: Will be used only if you choose PHP file cache as `cache_system`.
163 | **Make sure this path is not publicly accessible.** [See security note below](#security-note).
164 |
165 |
166 | - `redis_dsn`: Will be used only if you choose Redis cache as `cache_system`.
167 |
168 |
169 | - `memcached_dsn`: Will be used only if you choose Memcached as `cache_system`.
170 |
171 |
172 | - `clean_ip_cache_duration`: Set the duration we keep in cache the fact that an IP is clean. In seconds. Defaults to 5.
173 |
174 |
175 | - `bad_ip_cache_duration`: Set the duration we keep in cache the fact that an IP is bad. In seconds. Defaults to 20.
176 |
177 |
178 | - `captcha_cache_duration`: Set the duration we keep in cache the captcha flow variables for an IP. In seconds.
179 | Defaults to 86400.. In seconds. Defaults to 20.
180 |
181 |
182 | ### Geolocation
183 |
184 | - `geolocation`: Settings for geolocation remediation (i.e. country based remediation).
185 |
186 | - `geolocation[enabled]`: true to enable remediation based on country. Default to false.
187 |
188 | - `geolocation[type]`: Geolocation system. Only 'maxmind' is available for the moment. Default to `maxmind`.
189 |
190 | - `geolocation[cache_duration]`: This setting will be used to set the lifetime (in seconds) of a cached country
191 | associated to an IP. The purpose is to avoid multiple call to the geolocation system (e.g. maxmind database). Default to 86400. Set 0 to disable caching.
192 |
193 | - `geolocation[maxmind]`: MaxMind settings.
194 |
195 | - `geolocation[maxmind][database_type]`: Select from `country` or `city`. Default to `country`. These are the two available MaxMind database types.
196 |
197 | - `geolocation[maxmind][database_path]`: Absolute path to the MaxMind database (e.g. mmdb file)
198 | **Make sure this path is not publicly accessible.** [See security note below](#security-note).
199 |
200 |
201 | ### Captcha and ban wall settings
202 |
203 |
204 | - `hide_mentions`: true to hide CrowdSec mentions on ban and captcha walls.
205 |
206 |
207 | - `custom_css`: Custom css directives for ban and captcha walls
208 |
209 |
210 | - `color`: Array of settings for ban and captcha walls colors.
211 |
212 | - `color[text][primary]`
213 |
214 | - `color[text][secondary]`
215 |
216 | - `color[text][button]`
217 |
218 | - `color[text][error_message]`
219 |
220 | - `color[background][page]`
221 |
222 | - `color[background][container]`
223 |
224 | - `color[background][button]`
225 |
226 | - `color[background][button_hover]`
227 |
228 |
229 | - `text`: Array of settings for ban and captcha walls texts.
230 |
231 | - `text[captcha_wall][tab_title]`
232 |
233 | - `text[captcha_wall][title]`
234 |
235 | - `text[captcha_wall][subtitle]`
236 |
237 | - `text[captcha_wall][refresh_image_link]`
238 |
239 | - `text[captcha_wall][captcha_placeholder]`
240 |
241 | - `text[captcha_wall][send_button]`
242 |
243 | - `text[captcha_wall][error_message]`
244 |
245 | - `text[captcha_wall][footer]`
246 |
247 | - `text[ban_wall][tab_title]`
248 |
249 | - `text[ban_wall][title]`
250 |
251 | - `text[ban_wall][subtitle]`
252 |
253 | - `text[ban_wall][footer]`
254 |
255 |
256 | ### Debug
257 | - `debug_mode`: `true` to enable verbose debug log. Default to `false`.
258 |
259 |
260 | - `disable_prod_log`: `true` to disable prod log. Default to `false`.
261 |
262 |
263 | - `log_directory_path`: Absolute path to store log files.
264 | **Make sure this path is not publicly accessible.** [See security note below](#security-note).
265 |
266 |
267 | - `display_errors`: true to stop the process and display errors on browser if any.
268 |
269 |
270 | - `forced_test_ip`: Only for test or debug purpose. Default to empty. If not empty, it will be used instead of the
271 | real remote ip.
272 |
273 |
274 | - `forced_test_forwarded_ip`: Only for test or debug purpose. Default to empty. If not empty, it will be used
275 | instead of the real forwarded ip. If set to `no_forward`, the x-forwarded-for mechanism will not be used at all.
276 |
277 | ### Security note
278 |
279 | Some files should not be publicly accessible because they may contain sensitive data:
280 |
281 | - Setting file `settings.php`
282 | - Log files
283 | - Cache files of the File system cache
284 | - TLS authentication files
285 | - Geolocation database files
286 |
287 | If you define publicly accessible folders in the settings, be sure to add rules to deny access to these files.
288 |
289 | In the following example, we will suppose that you use a folder `crowdsec` with sub-folders `logs`, `cache`, `tls` and `geolocation`.
290 |
291 | If you are using Nginx, you could use the following snippet to modify your website configuration file:
292 |
293 | ```nginx
294 | server {
295 | ...
296 | ...
297 | ...
298 | # Deny all attempts to access some folders of the crowdsec standalone bouncer
299 | location ~ /crowdsec/(settings|logs|cache|tls|geolocation) {
300 | deny all;
301 | }
302 | ...
303 | ...
304 | }
305 | ```
306 |
307 | If you are using Apache, you could add this kind of directive in a `.htaccess` file:
308 |
309 | ```htaccess
310 | Redirectmatch 403 crowdsec/settings
311 | Redirectmatch 403 crowdsec/logs/
312 | Redirectmatch 403 crowdsec/cache/
313 | Redirectmatch 403 crowdsec/tls/
314 | Redirectmatch 403 crowdsec/geolocation/
315 | ```
316 |
317 | **N.B.:**
318 | - There is no need to protect the `cache` folder if you are using Redis or Memcached cache systems.
319 | - There is no need to protect the `logs` folder if you disable debug and prod logging.
320 | - There is no need to protect the `tls` folder if you use Bouncer API key authentication type.
321 | - There is no need to protect the `geolocation` folder if you don't use the geolocation feature.
322 |
--------------------------------------------------------------------------------
/docs/DEVELOPER.md:
--------------------------------------------------------------------------------
1 | 
2 | # CrowdSec standalone PHP bouncer
3 |
4 | ## Developer guide
5 |
6 | **Table of Contents**
7 |
8 |
9 |
10 | - [Local development](#local-development)
11 | - [DDEV setup](#ddev-setup)
12 | - [DDEV installation](#ddev-installation)
13 | - [Prepare DDEV PHP environment](#prepare-ddev-php-environment)
14 | - [DDEV Usage](#ddev-usage)
15 | - [Add CrowdSec bouncer and watcher](#add-crowdsec-bouncer-and-watcher)
16 | - [Use composer to update or install the lib](#use-composer-to-update-or-install-the-lib)
17 | - [Find IP of your docker services](#find-ip-of-your-docker-services)
18 | - [Unit test](#unit-test)
19 | - [Integration test](#integration-test)
20 | - [Auto-prepend mode (standalone mode)](#auto-prepend-mode-standalone-mode)
21 | - [End-to-end tests](#end-to-end-tests)
22 | - [Coding standards](#coding-standards)
23 | - [Generate CrowdSec tools and settings on start](#generate-crowdsec-tools-and-settings-on-start)
24 | - [Redis debug](#redis-debug)
25 | - [Memcached debug](#memcached-debug)
26 | - [Example scripts](#example-scripts)
27 | - [Clear cache script](#clear-cache-script)
28 | - [Full Live mode example](#full-live-mode-example)
29 | - [Set up the context](#set-up-the-context)
30 | - [Get the remediation the clean IP "1.2.3.4"](#get-the-remediation-the-clean-ip-1234)
31 | - [Now ban range 1.2.3.4 to 1.2.3.7 for 12h](#now-ban-range-1234-to-1237-for-12h)
32 | - [Clear cache and get the new remediation](#clear-cache-and-get-the-new-remediation)
33 | - [Discover the CrowdSec LAPI](#discover-the-crowdsec-lapi)
34 | - [Use the CrowdSec cli (`cscli`)](#use-the-crowdsec-cli-cscli)
35 | - [Add decision for an IP or a range of IPs](#add-decision-for-an-ip-or-a-range-of-ips)
36 | - [Add decision to ban or captcha a country](#add-decision-to-ban-or-captcha-a-country)
37 | - [Delete decisions](#delete-decisions)
38 | - [Create a bouncer](#create-a-bouncer)
39 | - [Create a watcher](#create-a-watcher)
40 | - [Use the web container to call LAPI](#use-the-web-container-to-call-lapi)
41 | - [Update documentation table of contents](#update-documentation-table-of-contents)
42 | - [Commit message](#commit-message)
43 | - [Allowed message `type` values](#allowed-message-type-values)
44 | - [Release process](#release-process)
45 |
46 |
47 |
48 |
49 |
50 | ## Local development
51 |
52 | There are many ways to install this library on a local PHP environment.
53 |
54 | We are using [DDEV](https://ddev.readthedocs.io/en/stable/) because it is quite simple to use and customize.
55 |
56 | Of course, you may use your own local stack, but we provide here some useful tools that depends on DDEV.
57 |
58 |
59 | ### DDEV setup
60 |
61 | For a quick start, follow the below steps.
62 |
63 |
64 | #### DDEV installation
65 |
66 | For the DDEV installation, please follow the [official instructions](https://ddev.readthedocs.io/en/stable/users/install/ddev-installation/).
67 |
68 |
69 | #### Prepare DDEV PHP environment
70 |
71 | The final structure of the project will look like below.
72 |
73 | ```
74 | crowdsec-bouncer-project (choose the name you want for this folder)
75 | │
76 | │ (your php project sources; could be a simple index.php file)
77 | │
78 | └───.ddev
79 | │ │
80 | │ │ (DDEV files)
81 | │
82 | └───my-code (do not change this folder name)
83 | │
84 | │
85 | └───standalone-bouncer (do not change this folder name)
86 | │
87 | │ (Clone of this repo)
88 |
89 | ```
90 |
91 | - Create an empty folder that will contain all necessary sources:
92 | ```bash
93 | mkdir crowdsec-bouncer-project
94 | ```
95 |
96 | - Create a DDEV php project:
97 |
98 | ```bash
99 | cd crowdsec-bouncer-project
100 | ddev config --project-type=php --php-version=8.2 --project-name=crowdsec-standalone-bouncer
101 | ```
102 |
103 | - Add some DDEV add-ons:
104 |
105 | ```bash
106 | ddev get ddev/ddev-redis
107 | ddev get ddev/ddev-memcached
108 | ddev get julienloizelet/ddev-tools
109 | ddev get julienloizelet/ddev-crowdsec-php
110 | ```
111 |
112 | - Clone this repo sources in a `my-code/standalone-bouncer` folder:
113 |
114 | ```bash
115 | mkdir -p my-code/standalone-bouncer
116 | cd my-code/standalone-bouncer && git clone git@github.com:crowdsecurity/cs-standalone-php-bouncer.git ./
117 | ```
118 |
119 | - Launch DDEV
120 |
121 | ```bash
122 | ddev start
123 | ```
124 | This should take some times on the first launch as this will download all necessary docker images.
125 |
126 |
127 | ### DDEV Usage
128 |
129 |
130 | #### Add CrowdSec bouncer and watcher
131 |
132 | - To create a new bouncer in the CrowdSec container, run:
133 |
134 | ```bash
135 | ddev create-bouncer [name]
136 | ```
137 |
138 | It will return the bouncer key.
139 |
140 | - To create a new watcher, run:
141 |
142 | ```bash
143 | ddev create-watcher [name] [password]
144 | ```
145 |
146 | **N.B.** : Since we are using TLS authentication for agent, you should avoid to create a watcher with this method.
147 |
148 |
149 | #### Use composer to update or install the lib
150 |
151 | Run:
152 |
153 | ```bash
154 | ddev composer update --working-dir ./my-code/standalone-bouncer
155 | ```
156 |
157 | For advanced usage, you can create a `composer-dev.json` file in the `my-code/standalone-bouncer` folder and run:
158 |
159 | ```bash
160 | ddev exec COMPOSER=composer-dev.json composer update --working-dir ./my-code/standalone-bouncer
161 | ```
162 |
163 | #### Find IP of your docker services
164 |
165 | In most cases, you will test to bounce your current IP. As we are running on a docker stack, this is the local host IP.
166 |
167 | To find it, just run:
168 |
169 | ```bash
170 | ddev find-ip
171 | ```
172 |
173 | You will have to know also the IP of the `ddev-router` container as it acts as a proxy, and you should set it in the `trust_ip_forward_array` setting.
174 |
175 | To find this IP, just run:
176 |
177 | ```bash
178 | ddev find-ip ddev-router
179 | ```
180 |
181 |
182 | #### Unit test
183 |
184 |
185 | ```bash
186 | ddev php ./my-code/standalone-bouncer/vendor/bin/phpunit ./my-code/standalone-bouncer/tests/Unit --testdox
187 | ```
188 |
189 | #### Integration test
190 |
191 | First, create a bouncer and keep the result key.
192 |
193 | ```bash
194 | ddev create-bouncer
195 | ```
196 |
197 | Then, as we use a TLS ready CrowdSec container, you have to copy some certificates and key:
198 |
199 | ```bash
200 | cd crowdsec-bouncer-project
201 | mkdir cfssl
202 | cp -r .ddev/okaeli-add-on/custom_files/crowdsec/cfssl/* cfssl
203 | ```
204 |
205 | Finally, run
206 |
207 |
208 | ```bash
209 | ddev exec BOUNCER_KEY=your-bouncer-key AGENT_TLS_PATH=/var/www/html/cfssl APPSEC_URL=http://crowdsec:7422 LAPI_URL=https://crowdsec:8080 MEMCACHED_DSN=memcached://memcached:11211 REDIS_DSN=redis://redis:6379 /usr/bin/php ./my-code/standalone-bouncer/vendor/bin/phpunit --testdox --colors --exclude-group ignore ./my-code/standalone-bouncer/tests/Integration/IpVerificationTest.php
210 | ```
211 |
212 | For geolocation Unit Test, you should first put 2 free MaxMind databases in the `tests` folder : `GeoLite2-City.mmdb`
213 | and `GeoLite2-Country.mmdb`. You can download these databases by [creating a MaxMind account](https://support.maxmind.com/hc/en-us/articles/4407099783707-Create-an-Account) and browse to the
214 | download page.
215 |
216 |
217 | Then, you can run:
218 |
219 | ```bash
220 | ddev exec BOUNCER_KEY=your-bouncer-key AGENT_TLS_PATH=/var/www/html/cfssl APPSEC_URL=http://crowdsec:7422 LAPI_URL=https://crowdsec:8080 MEMCACHED_DSN=memcached://memcached:11211 REDIS_DSN=redis://redis:6379 /usr/bin/php ./my-code/standalone-bouncer/vendor/bin/phpunit --testdox --colors --exclude-group ignore ./my-code/standalone-bouncer/tests/Integration/GeolocationTest.php
221 | ```
222 |
223 | **N.B.**: If you want to test with `curl` instead of `file_get_contents` calls to LAPI, you have to add `USE_CURL=1` in
224 | the previous commands.
225 |
226 | **N.B**.: If you want to test with `tls` authentication, you have to add `BOUNCER_TLS_PATH` environment variable
227 | and specify the path where you store certificates and keys. For example:
228 |
229 | ```bash
230 | ddev exec USE_CURL=1 AGENT_TLS_PATH=/var/www/html/cfssl BOUNCER_TLS_PATH=/var/www/html/cfssl APPSEC_URL=http://crowdsec:7422 LAPI_URL=https://crowdsec:8080 MEMCACHED_DSN=memcached://memcached:11211 REDIS_DSN=redis://redis:6379 /usr/bin/php ./my-code/standalone-bouncer/vendor/bin/phpunit --testdox --colors --exclude-group ignore ./my-code/standalone-bouncer/tests/Integration/IpVerificationTest.php
231 | ```
232 |
233 |
234 | #### Auto-prepend mode (standalone mode)
235 |
236 | Before using the bouncer in a standalone mode (i.e. with an `auto_prepend_file` directive), you should copy the [`scripts/settings.php.dist`](../scripts/settings.php.dist) file to a `scripts/settings.php` file and edit it depending on your needs.
237 |
238 | Then, to configure the Nginx service in order that it uses an `auto_prepend_file` directive pointing to the [`scripts/bounce.php`](../scripts/bounce.php) script, please run the following command from the `.ddev` folder:
239 |
240 | ```bash
241 | ddev nginx-config okaeli-add-on/native/custom_files/crowdsec/crowdsec-prepend-nginx-site.conf
242 | ```
243 |
244 | With that done, every access to your ddev url (i.e. `https://crowdsec-standalone-bouncer.ddev.site`) will be bounce.
245 |
246 | For example, you should try to browse the following url:
247 |
248 | ```
249 | https://crowdsec-standalone-bouncer/my-code/standalone-bouncer/tests/scripts/public/protected-page.php
250 | ```
251 |
252 | #### End-to-end tests
253 |
254 | In auto-prepend mode, you can run some end-to-end tests.
255 |
256 | To enable auto_prepend_file directive, run:
257 |
258 | ```bash
259 | cd crowdsec-bouncer-project/.ddev && ddev nginx-config okaeli-add-on/native/custom_files/crowdsec/crowdsec-prepend-nginx-site.conf
260 | ```
261 |
262 | We are using a Jest/Playwright Node.js stack to launch a suite of end-to-end tests.
263 |
264 | Tests code is in the `tests/end-to-end` folder. You should have to `chmod +x` the scripts you will find in `tests/end-to-end/__scripts__`.
265 |
266 |
267 | ```
268 | cd crowdsec-bouncer-project
269 | cp -r .ddev/okaeli-add-on/custom_files/crowdsec/cfssl/* cfssl
270 | ```
271 |
272 | Then you can use the `run-test.sh` script to run the tests:
273 |
274 | - the first parameter specifies if you want to run the test on your machine (`host`) or in the
275 | docker containers (`docker`). You can also use `ci` if you want to have the same behavior as in GitHub action.
276 | - the second parameter list the test files you want to execute. If empty, all the test suite will be launched.
277 |
278 | For example:
279 |
280 | ./run-tests.sh host "./__tests__/1-live-mode.js"
281 | ./run-tests.sh docker "./__tests__/1-live-mode.js"
282 | ./run-tests.sh host
283 |
284 | ##### Test in docker
285 |
286 | Before testing with the `docker` or `ci` parameter, you have to install all the required dependencies
287 | in the playwright container with this command :
288 |
289 | ./test-init.sh
290 |
291 | ##### Test on host
292 |
293 |
294 | If you want to test with the `host` parameter, you will have to install manually all the required dependencies:
295 |
296 | ```
297 | yarn --cwd ./tests/end-to-end --force
298 | yarn global add cross-env
299 | ```
300 |
301 | ##### Testing timeout in the CrowdSec container
302 |
303 | If you need to test a timeout, you can use the following command:
304 |
305 | Install `iproute2`
306 | ```bash
307 | ddev exec -s crowdsec apk add iproute2
308 | ```
309 | Add the delay you want:
310 | ```bash
311 | ddev exec -s crowdsec tc qdisc add dev eth0 root netem delay 500ms
312 | ```
313 |
314 | To remove the delay:
315 | ```bash
316 | ddev exec -s crowdsec tc qdisc del dev eth0 root netem
317 | ```
318 |
319 |
320 | #### Coding standards
321 |
322 | We set up some coding standards tools that you will find in the `tools/coding-standards` folder. In order to use these, you will need to work with a PHP version >= 7.4 and run first:
323 |
324 | ```
325 | ddev composer update --working-dir=./my-code/standalone-bouncer/tools/coding-standards
326 | ```
327 |
328 | ##### PHPCS Fixer
329 |
330 | We are using the [PHP Coding Standards Fixer](https://cs.symfony.com/). With ddev, you can do the following:
331 |
332 |
333 | ```bash
334 | ddev phpcsfixer my-code/standalone-bouncer/tools/coding-standards/php-cs-fixer ../
335 | ```
336 |
337 | ##### PHPSTAN
338 |
339 | To use the [PHPSTAN](https://github.com/phpstan/phpstan) tool, you can run:
340 |
341 |
342 | ```bash
343 | ddev phpstan /var/www/html/my-code/standalone-bouncer/tools/coding-standards phpstan/phpstan.neon /var/www/html/my-code/standalone-bouncer/src
344 |
345 | ```
346 |
347 |
348 | ##### PHP Mess Detector
349 |
350 | To use the [PHPMD](https://github.com/phpmd/phpmd) tool, you can run:
351 |
352 | ```bash
353 | ddev phpmd ./my-code/standalone-bouncer/tools/coding-standards phpmd/rulesets.xml ../../src
354 | ```
355 |
356 | ##### PHPCS and PHPCBF
357 |
358 | To use [PHP Code Sniffer](https://github.com/squizlabs/PHP_CodeSniffer) tools, you can run:
359 |
360 | ```bash
361 | ddev phpcs ./my-code/standalone-bouncer/tools/coding-standards my-code/standalone-bouncer/src PSR12
362 | ```
363 |
364 | and:
365 |
366 | ```bash
367 | ddev phpcbf ./my-code/standalone-bouncer/tools/coding-standards my-code/standalone-bouncer/src PSR12
368 | ```
369 |
370 |
371 | ##### PSALM
372 |
373 | To use [PSALM](https://github.com/vimeo/psalm) tools, you can run:
374 |
375 | ```bash
376 | ddev psalm ./my-code/standalone-bouncer/tools/coding-standards ./my-code/standalone-bouncer/tools/coding-standards/psalm
377 | ```
378 |
379 | ##### PHP Unit Code coverage
380 |
381 | In order to generate a code coverage report, you have to:
382 |
383 | - Enable `xdebug`:
384 | ```bash
385 | ddev xdebug
386 | ```
387 |
388 | To generate a html report, you can run:
389 | ```bash
390 | ddev exec XDEBUG_MODE=coverage BOUNCER_KEY=your-bouncer-key AGENT_TLS_PATH=/var/www/html/cfssl APPSEC_URL=http://crowdsec:7422 LAPI_URL=https://crowdsec:8080 REDIS_DSN=redis://redis:6379 MEMCACHED_DSN=memcached://memcached:11211 /usr/bin/php ./my-code/standalone-bouncer/tools/coding-standards/vendor/bin/phpunit --configuration ./my-code/standalone-bouncer/tools/coding-standards/phpunit/phpunit.xml
391 |
392 | ```
393 |
394 | You should find the main report file `dashboard.html` in `tools/coding-standards/phpunit/code-coverage` folder.
395 |
396 |
397 | If you want to generate a text report in the same folder:
398 |
399 | ```bash
400 | ddev exec XDEBUG_MODE=coverage BOUNCER_KEY=your-bouncer-key AGENT_TLS_PATH=/var/www/html/cfssl
401 | LAPI_URL=https://crowdsec:8080 APPSEC_URL=http://crowdsec:7422 MEMCACHED_DSN=memcached://memcached:11211
402 | REDIS_DSN=redis://redis:6379 /usr/bin/php ./my-code/standalone-bouncer/tools/coding-standards/vendor/bin/phpunit --configuration ./my-code/standalone-bouncer/tools/coding-standards/phpunit/phpunit.xml --coverage-text=./my-code/standalone-bouncer/tools/coding-standards/phpunit/code-coverage/report.txt
403 | ```
404 |
405 | #### Generate CrowdSec tools and settings on start
406 |
407 | We use a post-start DDEV hook to:
408 | - Create a bouncer
409 | - Set bouncer key, api url and other needed values in the `scripts/auto-prepend/settings.php` file (useful to test
410 | standalone mode).
411 | - Create a watcher that we use in end-to-end tests
412 |
413 | Just copy the file and restart:
414 | ```bash
415 | cp .ddev/config_overrides/config.crowdsec.yaml .ddev/config.crowdsec.yaml
416 | ddev restart
417 | ```
418 |
419 | #### Redis debug
420 |
421 | You should enter the `Redis` container:
422 |
423 | ```bash
424 | ddev redis-cli
425 | ```
426 |
427 | Then, you could play with the `redis-cli` command line tool:
428 |
429 | - Display keys and databases: `INFO keyspace`
430 |
431 | - Display stored keys: `KEYS *`
432 |
433 | - Display key value: `GET [key]`
434 |
435 | - Remove a key: `DEL [key]`
436 |
437 | #### Memcached debug
438 |
439 | @see https://lzone.de/#/LZone%20Cheat%20Sheets/DevOps%20Services/memcached
440 |
441 | First, find the IP of the `Memcached` container:
442 |
443 | ```bash
444 | ddev find-ip memcached
445 | ```
446 |
447 | Then, you could use `telnet` to interact with memcached:
448 |
449 | ```
450 | telnet