Skip to content

Commit b5d6e3a

Browse files
authored
Merge pull request #6 from crowdsecurity/feature/cap-remediations
Cap remediation to a capped value. Useful for sensitive websites (as e-commerce).
2 parents 56bc686 + c00a48e commit b5d6e3a

File tree

8 files changed

+66
-25
lines changed

8 files changed

+66
-25
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
name: Tests
22

33
on:
4-
pull_request:
5-
push: #TODO P3 No tests on push because it causes an unresolved bug (no space left on device). This Smell a race condition.
4+
push:
65

76
jobs:
87

docs/configuration.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Full configuration reference
2020
'live_mode'=> true,
2121
2222
// Optional. Cap the remediation to the selected one. Select from 'bypass' (minimum remediation), 'captcha' or 'ban' (maximum remediation). Defaults to 'ban'.
23-
'max_remediation'=> 'ban',
23+
'max_remediation_level'=> 'ban',
2424
2525
// Optional. Set the duration we keep in cache the fact that an IP is clean. In seconds. Defaults to 600 (10 minutes).
2626
'cache_expiration_for_clean_ip'=> '600',

docs/getting-started.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Use the bouncer library (live mode)
2929
$bouncer = new Bouncer();
3030
$bouncer->configure(['api_token'=> $apiToken], $cacheAdapter);
3131
32-
$remediation = $bouncer->getRemediationForIp($blockedIp);// Return "ban", "catpcha" or "bypass"
32+
$remediation = $bouncer->getRemediationForIp($blockedIp);// Return "ban", "captcha" or "bypass"
3333
3434
Use the bouncer library (stream mode)
3535
------------------------------------

src/ApiCache.php

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,9 @@ private function addRemediationToCacheItem(string $ip, string $type, int $expira
6969
$remediations = $item->get();
7070
$remediations = $remediations ?: [];
7171

72-
73-
// TODO P3 use constant for clean, ban or captcha single word
74-
// TODO P3 wording replace "clean" by "bypass"
75-
$index = array_search('clean', array_column($remediations, 0));
72+
$index = array_search(Constants::REMEDIATION_BYPASS, array_column($remediations, 0));
7673
if (false !== $index) {
77-
$this->logger->debug("cache#$ip: Previously clean IP but now bad, remove the \"clean\" remediation immediately");
74+
$this->logger->debug("cache#$ip: Previously clean IP but now bad, remove the ".Constants::REMEDIATION_BYPASS." remediation immediately");
7875
unset($remediations[$index]);
7976
}
8077

@@ -190,7 +187,7 @@ private static function parseDurationToSeconds(string $duration): int
190187
private function formatRemediationFromDecision(?array $decision): array
191188
{
192189
if (!$decision) {
193-
return ['clean', time() + $this->cacheExpirationForCleanIp, 0];
190+
return [Constants::REMEDIATION_BYPASS, time() + $this->cacheExpirationForCleanIp, 0];
194191
}
195192

196193
return [
@@ -347,7 +344,7 @@ private function hit(string $ip): string
347344
*
348345
* @return string the computed remediation string, or null if no decision was found
349346
*/
350-
public function get(string $ip): ?string
347+
public function get(string $ip): string
351348
{
352349
$this->logger->debug('IP to check: '.$ip);
353350
if (!$this->liveMode && !$this->warmedUp) {

src/Bouncer.php

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ class Bouncer
2929
/** @var ApiCache */
3030
private $apiCache;
3131

32+
/** @var int */
33+
private $maxRemediationLevelIndex;
34+
3235
public function __construct(ApiCache $apiCache = null, LoggerInterface $logger = null)
3336
{
3437
if (!$logger) {
@@ -49,6 +52,8 @@ public function configure(array $config, AbstractAdapter $cacheAdapter): void
4952
$processor = new Processor();
5053
$this->config = $processor->processConfiguration($configuration, [$config]);
5154

55+
$this->maxRemediationLevelIndex = array_search($this->config['max_remediation_level'], Constants::ORDERED_REMEDIATIONS);
56+
5257
// Configure Api Cache.
5358
$this->apiCache->configure(
5459
$cacheAdapter,
@@ -61,23 +66,37 @@ public function configure(array $config, AbstractAdapter $cacheAdapter): void
6166
);
6267
}
6368

69+
/**
70+
* Cap the remediation to a fixed value given in configuration
71+
*/
72+
private function capRemediationLevel($remediation): string
73+
{
74+
$currentIndex = array_search($remediation, Constants::ORDERED_REMEDIATIONS);
75+
if ($currentIndex < $this->maxRemediationLevelIndex) {
76+
return Constants::ORDERED_REMEDIATIONS[$this->maxRemediationLevelIndex];
77+
}
78+
return $remediation;
79+
}
80+
6481
/**
6582
* Get the remediation for the specified IP. This method use the cache layer.
6683
* In live mode, when no remediation was found in cache, the cache system will call the API to check if there is a decision.
6784
*
6885
* @return string the remediation to apply (ex: 'ban', 'captcha', 'bypass')
6986
*/
70-
public function getRemediationForIp(string $ip): ?string
87+
public function getRemediationForIp(string $ip): string
7188
{
7289
$intIp = ip2long($ip);
7390
if (false === $intIp) {
7491
throw new BouncerException("IP $ip should looks like x.x.x.x, with x in 0-255. Ex: 1.2.3.4");
7592
}
76-
return $this->apiCache->get(long2ip($intIp));
93+
$remediation = $this->apiCache->get(long2ip($intIp));
94+
$remediation = $this->capRemediationLevel($remediation);
95+
return $remediation;
7796
}
7897

7998
/**
80-
* Returns a default "CrowdSec 403" HTML template to display to a web browser using a ban IP.
99+
* Returns a default "CrowdSec 403" HTML template to display to a web browser using a banned IP.
81100
*/
82101
public function getDefault403Template(): string
83102
{

src/Configuration.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public function getConfigTreeBuilder()
3232
->scalarNode('api_user_agent')->defaultValue(Constants::BASE_USER_AGENT)->end()
3333
->integerNode('api_timeout')->defaultValue(Constants::API_TIMEOUT)->end()
3434
->booleanNode('live_mode')->defaultValue(true)->end()
35-
->enumNode('max_remediation')->values(['bypass', 'captcha', 'ban'])->defaultValue('ban')->end()
35+
->enumNode('max_remediation_level')->values(Constants::ORDERED_REMEDIATIONS)->defaultValue(Constants::REMEDIATION_BAN)->end()
3636
->integerNode('cache_expiration_for_clean_ip')->defaultValue(Constants::CACHE_EXPIRATION_FOR_CLEAN_IP)->end()
3737
->end();
3838

src/Constants.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ class Constants
2626
/** @var int The duration we keep a clean IP in cache 600s = 10m */
2727
const CACHE_EXPIRATION_FOR_CLEAN_IP = 600; // TODO P2 get the correct one
2828

29-
/** @var array The list of each known remediation, sorted by priority */
30-
const ORDERED_REMEDIATIONS = ['ban', 'captcha', 'clean']; // TODO P2 get the correct one
29+
/** @var string The ban remediation */
30+
const REMEDIATION_BAN = 'ban';
31+
32+
/** @var string The captcha remediation */
33+
const REMEDIATION_CAPTCHA = 'captcha';
34+
35+
/** @var string The bypass remediation */
36+
const REMEDIATION_BYPASS = 'bypass';
37+
38+
// TODO P2 get the correct list
39+
/** @var array<string> The list of each known remediation, sorted by priority */
40+
const ORDERED_REMEDIATIONS = [self::REMEDIATION_BAN, self::REMEDIATION_CAPTCHA, self::REMEDIATION_BYPASS];
3141
}

tests/IpVerificationTest.php

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,14 @@ public function testCanVerifyIpInLiveModeWithCacheSystem(AbstractAdapter $cacheA
107107

108108
$cleanRemediation1stCall = $bouncer->getRemediationForIp($cleanIp);
109109
$this->assertEquals(
110-
'clean',
110+
'bypass',
111111
$cleanRemediation1stCall,
112112
'Get decisions for a clean IP for the first time (it should be a cache miss)'
113113
);
114114

115115
// Call the same thing for the second time (now it should be a cache hit)
116116
$cleanRemediation2ndCall = $bouncer->getRemediationForIp($cleanIp);
117-
$this->assertEquals('clean', $cleanRemediation2ndCall);
117+
$this->assertEquals('bypass', $cleanRemediation2ndCall);
118118

119119
// Clear cache
120120
$cacheAdapter->clear();
@@ -123,6 +123,14 @@ public function testCanVerifyIpInLiveModeWithCacheSystem(AbstractAdapter $cacheA
123123

124124
$remediation3rdCall = $bouncer->getRemediationForIp($badIp);
125125
$this->assertEquals('ban', $remediation3rdCall);
126+
127+
// Reconfigure the bouncer to set maximum remediation level to "captcha"
128+
$config['max_remediation_level'] = 'captcha';
129+
$bouncer->configure($config, $cacheAdapter);
130+
$cappedRemediation = $bouncer->getRemediationForIp($badIp);
131+
$this->assertEquals('captcha', $cappedRemediation, 'The remediation for the banned IP should now be "captcha"');
132+
$config['max_remediation_level'] = 'ban';
133+
$bouncer->configure($config, $cacheAdapter);
126134
}
127135

128136
/**
@@ -170,22 +178,30 @@ public function testCanVerifyIpInStreamModeWithCacheSystem(AbstractAdapter $cach
170178
'Get decisions for a bad IP for the first time (as the cache has been warmed up should be a cache hit)'
171179
);
172180

181+
// Reconfigure the bouncer to set maximum remediation level to "captcha"
182+
$config['max_remediation_level'] = 'captcha';
183+
$bouncer->configure($config, $cacheAdapter);
184+
$cappedRemediation = $bouncer->getRemediationForIp($badIp);
185+
$this->assertEquals('captcha', $cappedRemediation, 'The remediation for the banned IP should now be "captcha"');
186+
$config['max_remediation_level'] = 'ban';
187+
$bouncer->configure($config, $cacheAdapter);
188+
173189
$this->assertEquals(
174-
'clean',
190+
'bypass',
175191
$bouncer->getRemediationForIp($cleanIp),
176192
'Get decisions for a clean IP for the first time (as the cache has been warmed up should be a cache hit)'
177193
);
178194

179195
// Preload the remediation to prepare the next tests.
180196
$this->assertEquals(
181-
'clean',
197+
'bypass',
182198
$bouncer->getRemediationForIp($newlyBadIp),
183-
'Preload the clean remediation to prepare the next tests'
199+
'Preload the bypass remediation to prepare the next tests'
184200
);
185-
201+
186202
// Add and remove decision
187203
$this->watcherClient->setSecondState();
188-
204+
189205
// Pull updates
190206
$bouncer->refreshBlocklistCache();
191207

@@ -213,7 +229,7 @@ public function testCanVerifyIpInStreamModeWithCacheSystem(AbstractAdapter $cach
213229
);
214230

215231
$this->assertEquals(
216-
'clean',
232+
'bypass',
217233
$bouncer->getRemediationForIp($badIp),
218234
'The old decisions should now be removed, so the previously bad IP should now be clean'
219235
);

0 commit comments

Comments
 (0)