From efffa9dd082cf14d48cd2b589cabb082a1bce1fe Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Fri, 7 Nov 2025 15:23:33 +0300 Subject: [PATCH 01/29] alert init (#3) * AlertsClient --- .../workflows/php-sdk-development-tests.yml | 2 + .gitignore | 2 + composer.json | 6 +- docs/DEVELOPER.md | 4 +- src/AbstractLapiClient.php | 128 ++++++ src/AlertsClient.php | 170 ++++++++ src/Bouncer.php | 133 +----- src/Configuration.php | 21 +- src/Configuration/Alert.php | 54 +++ src/Configuration/Alert/Decision.php | 42 ++ src/Configuration/Alert/Event.php | 39 ++ src/Configuration/Alert/Meta.php | 31 ++ src/Configuration/Alert/Source.php | 46 ++ src/Constants.php | 24 +- src/Metrics.php | 2 +- src/Payload/Alert.php | 237 ++++++++++ src/Storage/TokenStorage.php | 52 +++ src/Storage/TokenStorageInterface.php | 14 + src/WatcherClient.php | 55 +++ tests/Integration/AlertsClientTest.php | 406 ++++++++++++++++++ tests/Integration/BouncerTest.php | 4 +- ...atcherClient.php => TestWatcherClient.php} | 23 +- tests/Integration/WatcherClientTest.php | 76 ++++ tests/Unit/FileGetContentsTest.php | 1 - tests/Unit/Payload/AlertTest.php | 93 ++++ tests/Unit/Storage/TokenStorageTest.php | 34 ++ 26 files changed, 1543 insertions(+), 156 deletions(-) create mode 100644 src/AbstractLapiClient.php create mode 100644 src/AlertsClient.php create mode 100644 src/Configuration/Alert.php create mode 100644 src/Configuration/Alert/Decision.php create mode 100644 src/Configuration/Alert/Event.php create mode 100644 src/Configuration/Alert/Meta.php create mode 100644 src/Configuration/Alert/Source.php create mode 100644 src/Payload/Alert.php create mode 100644 src/Storage/TokenStorage.php create mode 100644 src/Storage/TokenStorageInterface.php create mode 100644 src/WatcherClient.php create mode 100644 tests/Integration/AlertsClientTest.php rename tests/Integration/{WatcherClient.php => TestWatcherClient.php} (93%) create mode 100644 tests/Integration/WatcherClientTest.php create mode 100644 tests/Unit/Payload/AlertTest.php create mode 100644 tests/Unit/Storage/TokenStorageTest.php diff --git a/.github/workflows/php-sdk-development-tests.yml b/.github/workflows/php-sdk-development-tests.yml index 3f9e9ed..50e1343 100644 --- a/.github/workflows/php-sdk-development-tests.yml +++ b/.github/workflows/php-sdk-development-tests.yml @@ -99,6 +99,8 @@ jobs: - name: Set BOUNCER_KEY env run: | echo "BOUNCER_KEY=$(ddev create-bouncer)" >> $GITHUB_ENV + - name: Create watcher + run: ddev create-watcher - name: Clone Lapi Client files if: inputs.is_call != true diff --git a/.gitignore b/.gitignore index 79f6bc0..0b8021a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ composer-dev* #log *.log + +/cfssl diff --git a/composer.json b/composer.json index 0943522..7f07a10 100644 --- a/composer.json +++ b/composer.json @@ -37,14 +37,16 @@ }, "require": { "php": "^7.2.5 || ^8.0", - "crowdsec/common": "^3.0.0", "ext-json": "*", + "crowdsec/common": "^3.0.0", + "psr/cache": "^1.0 || ^2.0 || ^3.0", "symfony/config": "^4.4.44 || ^5.4.11 || ^6.0.11 || ^7.2.0" }, "require-dev": { + "ext-curl": "*", "phpunit/phpunit": "^8.5.30 || ^9.3", "mikey179/vfsstream": "^1.6.11", - "ext-curl": "*" + "symfony/cache": "^5.4.11 || ^6.0.11 || ^7.2.1" }, "suggest": { "ext-curl": "*" diff --git a/docs/DEVELOPER.md b/docs/DEVELOPER.md index ae5bb72..c0f8dfc 100644 --- a/docs/DEVELOPER.md +++ b/docs/DEVELOPER.md @@ -82,8 +82,8 @@ ddev config --project-type=php --php-version=8.2 --project-name=crowdsec-lapi-cl - Add some DDEV add-ons: ```bash -ddev get julienloizelet/ddev-tools -ddev get julienloizelet/ddev-crowdsec-php +ddev add-on get julienloizelet/ddev-tools +ddev add-on get julienloizelet/ddev-crowdsec-php ``` - Clone this repo sources in a `my-code/lapi-client` folder: diff --git a/src/AbstractLapiClient.php b/src/AbstractLapiClient.php new file mode 100644 index 0000000..becbc3f --- /dev/null +++ b/src/AbstractLapiClient.php @@ -0,0 +1,128 @@ +configure($configs); + $this->headers = [Constants::HEADER_LAPI_USER_AGENT => $this->formatUserAgent($this->configs)]; + if (!empty($this->configs['api_key'])) { + $this->headers[Constants::HEADER_LAPI_API_KEY] = $this->configs['api_key']; + } + parent::__construct($this->configs, $requestHandler, $logger); + } + + /** + * Process and validate input configurations. + */ + private function configure(array $configs): void + { + $configuration = new Configuration(); + $processor = new Processor(); + $this->configs = $processor->processConfiguration($configuration, [$configuration->cleanConfigs($configs)]); + } + + /** + * Make a request to LAPI. + * + * @throws ClientException + */ + protected function manageRequest( + string $method, + string $endpoint, + array $parameters = [] + ): array { + try { + $this->logger->debug('Now processing a bouncer request', [ + 'type' => 'BOUNCER_CLIENT_REQUEST', + 'method' => $method, + 'endpoint' => $endpoint, + 'parameters' => $parameters, + ]); + + return $this->request($method, $endpoint, $parameters, $this->headers); + } catch (CommonTimeoutException $e) { + throw new TimeoutException($e->getMessage(), $e->getCode(), $e); + } catch (CommonClientException $e) { + throw new ClientException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Make a request to the AppSec component of LAPI. + * + * @throws ClientException + */ + protected function manageAppSecRequest( + string $method, + array $headers = [], + string $rawBody = '', + ): array { + try { + $this->logger->debug('Now processing a bouncer AppSec request', [ + 'type' => 'BOUNCER_CLIENT_APPSEC_REQUEST', + 'method' => $method, + 'raw body' => $this->cleanRawBodyForLog($rawBody, 200), + 'raw body length' => strlen($rawBody), + 'headers' => $this->cleanHeadersForLog($headers), + ]); + + return $this->requestAppSec($method, $headers, $rawBody); + } catch (CommonTimeoutException $e) { + throw new TimeoutException($e->getMessage(), $e->getCode(), $e); + } catch (CommonClientException $e) { + throw new ClientException($e->getMessage(), $e->getCode(), $e); + } + } + + protected function cleanHeadersForLog(array $headers): array + { + $cleanedHeaders = $headers; + if (array_key_exists(Constants::HEADER_APPSEC_API_KEY, $cleanedHeaders)) { + $cleanedHeaders[Constants::HEADER_APPSEC_API_KEY] = '***'; + } + + return $cleanedHeaders; + } + + protected function cleanRawBodyForLog(string $rawBody, int $maxLength): string + { + return strlen($rawBody) > $maxLength ? substr($rawBody, 0, $maxLength) . '...[TRUNCATED]' : $rawBody; + } + + /** + * Format User-Agent header. _/. + */ + protected function formatUserAgent(array $configs = []): string + { + $userAgentSuffix = !empty($configs['user_agent_suffix']) ? '_' . $configs['user_agent_suffix'] : ''; + $userAgentVersion = + !empty($configs['user_agent_version']) ? $configs['user_agent_version'] : Constants::VERSION; + + return Constants::USER_AGENT_PREFIX . $userAgentSuffix . '/' . $userAgentVersion; + } +} diff --git a/src/AlertsClient.php b/src/AlertsClient.php new file mode 100644 index 0000000..9c5b9e5 --- /dev/null +++ b/src/AlertsClient.php @@ -0,0 +1,170 @@ +, + * events: list, + * events_count: int, + * id: int, + * labels: null|array, + * leakspeed: string, + * machine_id: string, + * message: string, + * meta: list, + * scenario: string, + * scenario_hash: string, + * scenario_version: string, + * simulated: bool, + * source: TSource, + * start_at: string, + * stop_at: string, + * uuid: string + * } + */ +class AlertsClient extends AbstractLapiClient +{ + /** + * @var TokenStorageInterface + */ + private $tokenStorage; + + public function __construct( + array $configs, + TokenStorageInterface $tokenStorage, + ?RequestHandlerInterface $requestHandler = null, + ?LoggerInterface $logger = null + ) { + $this->tokenStorage = $tokenStorage; + parent::__construct($configs, $requestHandler, $logger); + } + + /** + * @param list $alerts + * + * @return list + */ + public function push(array $alerts): array + { + $this->login(); + return $this->manageRequest( + 'POST', + Constants::ALERTS, + $alerts + ); + } + + /** + * Search for alerts. + * + * scope - Show alerts for this scope. + * value - Show alerts for this value (used with scope). + * scenario - Show alerts for this scenario. + * ip - IP to search for (shorthand for scope=ip&value=). + * range - Range to search for (shorthand for scope=range&value=). + * since - Search alerts newer than delay (format must be compatible with time.ParseDuration). + * until - Search alerts older than delay (format must be compatible with time.ParseDuration). + * simulated - If set to true, decisions in simulation mode will be returned as well. + * has_active_decision: Only return alerts with decisions not expired yet. + * decision_type: Restrict results to alerts with decisions matching given type. + * limit: Number of alerts to return. + * origin: Restrict results to this origin (ie. lists,CAPI,cscli). + * + * @param TSearchQuery $query + * @return list + */ + public function search(array $query): array + { + $this->login(); + return $this->manageRequest( + 'GET', + Constants::ALERTS, + $query + ); + } + + /** + * Delete alerts by condition. Can be used only on the same machine than the local API. + * + * @param TDeleteQuery $query + */ + public function delete(array $query): array + { + $this->login(); + return $this->manageRequest( + 'DELETE', + Constants::ALERTS, + $query + ); + } + + /** + * @param positive-int $id + * @return TStoredAlert + */ + public function getById(int $id): ?array + { + $this->login(); + $result = $this->manageRequest( + 'GET', + \sprintf('%s/%d', Constants::ALERTS, $id) + ); + // workaround for mutes 404 status. + if (empty($result['id'])) { + \assert($result['message'] === 'object not found'); + return null; + } + return $result; + } + + private function login(): void + { + $token = $this->tokenStorage->retrieveToken(); + if (null === $token) { + throw new ClientException('Login fail'); + } + $this->headers['Authorization'] = "Bearer $token"; + } +} diff --git a/src/Bouncer.php b/src/Bouncer.php index a142aeb..00aaf05 100644 --- a/src/Bouncer.php +++ b/src/Bouncer.php @@ -4,13 +4,6 @@ namespace CrowdSec\LapiClient; -use CrowdSec\Common\Client\AbstractClient; -use CrowdSec\Common\Client\ClientException as CommonClientException; -use CrowdSec\Common\Client\RequestHandler\RequestHandlerInterface; -use CrowdSec\Common\Client\TimeoutException as CommonTimeoutException; -use Psr\Log\LoggerInterface; -use Symfony\Component\Config\Definition\Processor; - /** * The Bouncer Client. * @@ -21,35 +14,14 @@ * @copyright Copyright (c) 2022+ CrowdSec * @license MIT License * - * @psalm-import-type TMetric from Metrics - * @psalm-import-type TOS from Metrics - * @psalm-import-type TMeta from Metrics - * @psalm-import-type TItem from Metrics + * @psalm-import-type TMetric from \CrowdSec\LapiClient\Metrics + * @psalm-import-type TOS from \CrowdSec\LapiClient\Metrics + * @psalm-import-type TMeta from \CrowdSec\LapiClient\Metrics + * @psalm-import-type TItem from \CrowdSec\LapiClient\Metrics + * @psalm-import-type TBouncerConfig from \CrowdSec\LapiClient\Configuration */ -class Bouncer extends AbstractClient +class Bouncer extends AbstractLapiClient { - /** - * @var array - */ - protected $configs; - /** - * @var array - */ - private $headers; - - public function __construct( - array $configs, - ?RequestHandlerInterface $requestHandler = null, - ?LoggerInterface $logger = null - ) { - $this->configure($configs); - $this->headers = [Constants::HEADER_LAPI_USER_AGENT => $this->formatUserAgent($this->configs)]; - if (!empty($this->configs['api_key'])) { - $this->headers[Constants::HEADER_LAPI_API_KEY] = $this->configs['api_key']; - } - parent::__construct($this->configs, $requestHandler, $logger); - } - /** * Helper to create well formatted metrics array. * @@ -62,19 +34,18 @@ public function __construct( * 'version' => (string) Bouncer version * 'feature_flags' => (array) Should be empty for bouncer * 'utc_startup_timestamp' => (integer) Bouncer startup timestamp + * 'os' => (array) OS information * 'os' = [ * 'name' => (string) OS name * 'version' => (string) OS version * ] * ]; - * * @param TMeta $meta Array containing meta data. * * $meta = [ * 'window_size_seconds' => (integer) Window size in seconds * 'utc_now_timestamp' => (integer) Current timestamp * ]; - * * @param list $items Array of items. Each item is an array too. * * $items = [ @@ -196,43 +167,6 @@ public function pushUsageMetrics(array $usageMetrics): array ); } - private function cleanHeadersForLog(array $headers): array - { - $cleanedHeaders = $headers; - if (array_key_exists(Constants::HEADER_APPSEC_API_KEY, $cleanedHeaders)) { - $cleanedHeaders[Constants::HEADER_APPSEC_API_KEY] = '***'; - } - - return $cleanedHeaders; - } - - private function cleanRawBodyForLog(string $rawBody, int $maxLength): string - { - return strlen($rawBody) > $maxLength ? substr($rawBody, 0, $maxLength) . '...[TRUNCATED]' : $rawBody; - } - - /** - * Process and validate input configurations. - */ - private function configure(array $configs): void - { - $configuration = new Configuration(); - $processor = new Processor(); - $this->configs = $processor->processConfiguration($configuration, [$configuration->cleanConfigs($configs)]); - } - - /** - * Format User-Agent header. _/. - */ - private function formatUserAgent(array $configs = []): string - { - $userAgentSuffix = !empty($configs['user_agent_suffix']) ? '_' . $configs['user_agent_suffix'] : ''; - $userAgentVersion = - !empty($configs['user_agent_version']) ? $configs['user_agent_version'] : Constants::VERSION; - - return Constants::USER_AGENT_PREFIX . $userAgentSuffix . '/' . $userAgentVersion; - } - /** * @return TOS */ @@ -243,57 +177,4 @@ private function getOs(): array 'version' => php_uname('v'), ]; } - - /** - * Make a request to the AppSec component of LAPI. - * - * @throws ClientException - */ - private function manageAppSecRequest( - string $method, - array $headers = [], - string $rawBody = '' - ): array { - try { - $this->logger->debug('Now processing a bouncer AppSec request', [ - 'type' => 'BOUNCER_CLIENT_APPSEC_REQUEST', - 'method' => $method, - 'raw body' => $this->cleanRawBodyForLog($rawBody, 200), - 'raw body length' => strlen($rawBody), - 'headers' => $this->cleanHeadersForLog($headers), - ]); - - return $this->requestAppSec($method, $headers, $rawBody); - } catch (CommonTimeoutException $e) { - throw new TimeoutException($e->getMessage(), $e->getCode(), $e); - } catch (CommonClientException $e) { - throw new ClientException($e->getMessage(), $e->getCode(), $e); - } - } - - /** - * Make a request to LAPI. - * - * @throws ClientException - */ - private function manageRequest( - string $method, - string $endpoint, - array $parameters = [] - ): array { - try { - $this->logger->debug('Now processing a bouncer request', [ - 'type' => 'BOUNCER_CLIENT_REQUEST', - 'method' => $method, - 'endpoint' => $endpoint, - 'parameters' => $parameters, - ]); - - return $this->request($method, $endpoint, $parameters, $this->headers); - } catch (CommonTimeoutException $e) { - throw new TimeoutException($e->getMessage(), $e->getCode(), $e); - } catch (CommonClientException $e) { - throw new ClientException($e->getMessage(), $e->getCode(), $e); - } - } } diff --git a/src/Configuration.php b/src/Configuration.php index 732b518..82aac1a 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -25,7 +25,7 @@ * api_url?: string, * appsec_url?: string, * auth_type?: string, - * api_key: string, + * api_key?: string, * tls_cert_path?: string, * tls_key_path?: string, * tls_ca_cert_path?: string, @@ -34,6 +34,8 @@ * api_connect_timeout?: int, * appsec_timeout_ms?: int, * appsec_connect_timeout_ms?: int, + * machine_id?: non-empty-string, + * password?: non-empty-string * } */ class Configuration extends AbstractConfiguration @@ -54,6 +56,8 @@ class Configuration extends AbstractConfiguration 'api_connect_timeout', 'appsec_timeout_ms', 'appsec_connect_timeout_ms', + 'machine_id', + 'password', ]; /** @@ -92,6 +96,7 @@ public function getConfigTreeBuilder(): TreeBuilder $this->addConnectionNodes($rootNode); $this->addAppSecNodes($rootNode); $this->validate($rootNode); + $this->watcher($rootNode); return $treeBuilder; } @@ -105,7 +110,7 @@ public function getConfigTreeBuilder(): TreeBuilder * * @throws \InvalidArgumentException */ - private function addAppSecNodes($rootNode) + private function addAppSecNodes($rootNode): void { $rootNode->children() ->scalarNode('appsec_url')->cannotBeEmpty()->defaultValue(Constants::DEFAULT_APPSEC_URL)->end() @@ -123,7 +128,7 @@ private function addAppSecNodes($rootNode) * * @throws \InvalidArgumentException */ - private function addConnectionNodes($rootNode) + private function addConnectionNodes($rootNode): void { $rootNode->children() ->scalarNode('api_url')->cannotBeEmpty()->defaultValue(Constants::DEFAULT_LAPI_URL)->end() @@ -162,7 +167,7 @@ private function addConnectionNodes($rootNode) * @throws \InvalidArgumentException * @throws \RuntimeException */ - private function validate($rootNode) + private function validate($rootNode): void { $rootNode ->validate() @@ -196,4 +201,12 @@ private function validate($rootNode) ->thenInvalid('CA path is required for tls authentification with verify_peer.') ->end(); } + + private function watcher(ArrayNodeDefinition $rootNode): void + { + $rootNode->children() + ->stringNode('machine_id')->end() + ->stringNode('password')->end() + ->end(); + } } diff --git a/src/Configuration/Alert.php b/src/Configuration/Alert.php new file mode 100644 index 0000000..eeae807 --- /dev/null +++ b/src/Configuration/Alert.php @@ -0,0 +1,54 @@ + The list of each configuration tree key */ + protected $keys = [ + 'scenario', + 'scenario_hash', + 'scenario_version', + 'message', + 'events_count', + 'start_at', + 'stop_at', + 'capacity', + 'leakspeed', + 'simulated', + 'remediation', + ]; + + public function getConfigTreeBuilder(): TreeBuilder + { + $treeBuilder = new TreeBuilder('alert'); + /** @var ArrayNodeDefinition $rootNode */ + $rootNode = $treeBuilder->getRootNode(); + + // @formatter:off + $rootNode + ->children() + ->stringNode('scenario')->isRequired()->cannotBeEmpty()->end() + ->stringNode('scenario_hash')->isRequired()->cannotBeEmpty()->end() + ->stringNode('scenario_version')->isRequired()->cannotBeEmpty()->end() + ->stringNode('message')->isRequired()->cannotBeEmpty()->end() + ->integerNode('events_count')->isRequired()->min(0)->end() + ->scalarNode('start_at')->isRequired()->cannotBeEmpty()->end() + ->scalarNode('stop_at')->isRequired()->cannotBeEmpty()->end() + ->integerNode('capacity')->isRequired()->min(0)->end() + ->scalarNode('leakspeed')->isRequired()->cannotBeEmpty()->end() + ->booleanNode('simulated')->isRequired()->end() + ->booleanNode('remediation')->isRequired()->end() + ->end() + ; + // @formatter:on + + return $treeBuilder; + } +} diff --git a/src/Configuration/Alert/Decision.php b/src/Configuration/Alert/Decision.php new file mode 100644 index 0000000..5927275 --- /dev/null +++ b/src/Configuration/Alert/Decision.php @@ -0,0 +1,42 @@ + The list of each configuration tree key */ + protected $keys = [ + 'origin', + 'type', + 'scope', + 'value', + 'duration', + 'until', + 'scenario', + ]; + + public function getConfigTreeBuilder(): TreeBuilder + { + $treeBuilder = new TreeBuilder('decision'); + $rootNode = $treeBuilder->getRootNode(); + + // @formatter:off + $rootNode + ->children() + ->stringNode('origin')->isRequired()->cannotBeEmpty()->end() + ->stringNode('type')->isRequired()->cannotBeEmpty()->end() + ->stringNode('scope')->isRequired()->cannotBeEmpty()->end() + ->stringNode('value')->isRequired()->cannotBeEmpty()->end() + ->stringNode('duration')->isRequired()->cannotBeEmpty()->end() + ->stringNode('until')->cannotBeEmpty()->end() + ->stringNode('scenario')->isRequired()->cannotBeEmpty()->end() + ->end() + ; + // @formatter:on + + return $treeBuilder; + } +} diff --git a/src/Configuration/Alert/Event.php b/src/Configuration/Alert/Event.php new file mode 100644 index 0000000..75f4750 --- /dev/null +++ b/src/Configuration/Alert/Event.php @@ -0,0 +1,39 @@ + The list of each configuration tree key */ + protected $keys = [ + 'meta', + 'timestamp', + ]; + + public function getConfigTreeBuilder(): TreeBuilder + { + $treeBuilder = new TreeBuilder('event'); + $rootNode = $treeBuilder->getRootNode(); + + // @formatter:off + $rootNode + ->children() + ->arrayNode('meta')->isRequired() + ->arrayPrototype() + ->children() + ->stringNode('key')->isRequired()->cannotBeEmpty()->end() + ->stringNode('value')->isRequired()->cannotBeEmpty()->end() + ->end() + ->end() + ->end() + ->scalarNode('timestamp')->isRequired()->cannotBeEmpty()->end() + ->end() + ; + // @formatter:on + + return $treeBuilder; + } +} diff --git a/src/Configuration/Alert/Meta.php b/src/Configuration/Alert/Meta.php new file mode 100644 index 0000000..e56b064 --- /dev/null +++ b/src/Configuration/Alert/Meta.php @@ -0,0 +1,31 @@ + The list of each configuration tree key */ + protected $keys = [ + 'key', + 'value', + ]; + + public function getConfigTreeBuilder(): TreeBuilder + { + $treeBuilder = new TreeBuilder('meta'); + $root = $treeBuilder->getRootNode(); + + // @formatter:off + $root + ->children() + ->scalarNode('key')->isRequired()->cannotBeEmpty()->end() + ->scalarNode('value')->isRequired()->cannotBeEmpty()->end() + ->end(); + // @formatter:on + + return $treeBuilder; + } +} diff --git a/src/Configuration/Alert/Source.php b/src/Configuration/Alert/Source.php new file mode 100644 index 0000000..8b4215d --- /dev/null +++ b/src/Configuration/Alert/Source.php @@ -0,0 +1,46 @@ + The list of each configuration tree key */ + protected $keys = [ + 'scope', + 'value', + 'ip', + 'range', + 'as_number', + 'as_name', + 'cn', + 'latitude', + 'longitude', + ]; + + public function getConfigTreeBuilder(): TreeBuilder + { + $treeBuilder = new TreeBuilder('source'); + $rootNode = $treeBuilder->getRootNode(); + + // @formatter:off + $rootNode + ->children() + ->stringNode('scope')->isRequired()->cannotBeEmpty()->end() + ->stringNode('value')->isRequired()->cannotBeEmpty()->end() + ->stringNode('ip')->cannotBeEmpty()->end() + ->stringNode('range')->cannotBeEmpty()->end() + ->scalarNode('as_number')->cannotBeEmpty()->end() + ->stringNode('as_name')->cannotBeEmpty()->end() + ->stringNode('cn')->cannotBeEmpty()->end() + ->floatNode('latitude')->min(-90)->max(90)->end() + ->floatNode('longitude')->min(-180)->max(180)->end() + ->end() + ; + // @formatter:on + + return $treeBuilder; + } +} diff --git a/src/Constants.php b/src/Constants.php index 6a233eb..dfc5e2d 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -18,34 +18,50 @@ */ class Constants extends CommonConstants { + // /** * @var string The decisions endpoint */ public const DECISIONS_FILTER_ENDPOINT = '/v1/decisions'; + /** * @var string The decisions stream endpoint */ public const DECISIONS_STREAM_ENDPOINT = '/v1/decisions/stream'; + + public const ALERTS = '/v1/alerts'; + + /** + * @var string Authenticate current to get session ID + */ + public const WATCHER_LOGIN_ENDPOINT = '/v1/watchers/login'; + + /** + * @var string The usage metrics endpoint + */ + public const METRICS_ENDPOINT = '/v1/usage-metrics'; + // + /** * @var string The Default URL of the CrowdSec AppSec endpoint */ public const DEFAULT_APPSEC_URL = 'http://localhost:7422'; + /** * @var string The Default URL of the CrowdSec LAPI */ public const DEFAULT_LAPI_URL = 'http://localhost:8080'; - /** - * @var string The usage metrics endpoint - */ - public const METRICS_ENDPOINT = '/v1/usage-metrics'; + /** * @var string The metrics type */ public const METRICS_TYPE = 'crowdsec-php-bouncer'; + /** * @var string The user agent prefix used to send request to LAPI */ public const USER_AGENT_PREFIX = 'csphplapi'; + /** * @var string The current version of this library */ diff --git a/src/Metrics.php b/src/Metrics.php index 0697ac2..3bc9276 100644 --- a/src/Metrics.php +++ b/src/Metrics.php @@ -92,7 +92,7 @@ class Metrics public function __construct( array $properties, array $meta, - array $items = [] + array $items = [], ) { $this->configureProperties($properties); $this->configureMeta($meta); diff --git a/src/Payload/Alert.php b/src/Payload/Alert.php new file mode 100644 index 0000000..15e45e7 --- /dev/null +++ b/src/Payload/Alert.php @@ -0,0 +1,237 @@ +, + * timestamp: string + * } + * + * @psalm-type TAlertFull = array{ + * scenario: string, + * scenario_hash: string, + * scenario_version: string, + * message: string, + * events_count: int, + * start_at: string, + * stop_at: string, + * capacity: int, + * leakspeed: string, + * simulated: bool, + * remediation: bool, + * source: TSource, + * events: list, + * decisions: list, + * meta: list, + * labels: list + * } + */ +class Alert implements \JsonSerializable +{ + /** + * @var list + */ + private $properties; + + /** + * @var list + */ + private $events; + + /** + * @var list + */ + private $decisions = []; + + /** + * @var TSource + */ + private $source; + + /** + * @var list + */ + private $meta = []; + + /** + * @var list + */ + private $labels = []; + + /** + * @param TProps $properties + * @param TSource $source + * @param list $events + * @param list $decisions + * @param list $meta + * @param list $labels + */ + public function __construct( + array $properties, + array $source, + array $events = [], + array $decisions = [], + array $meta = [], + array $labels = [] + ) { + $processor = new Processor(); + $this->configureProperties($processor, $properties); + $this->configureSource($processor, $source); + $this->configureDecisions($processor, $decisions); + $this->configureEvents($processor, $events); + $this->configureMetaList($processor, $meta); + $this->labels = \array_filter($labels); + } + + /** + * @param TAlertFull $data + * @return void + */ + public static function fromArray(array $data): self + { + return new self( + $data, + $data['source'] ?? [], + $data['events'] ?? [], + $data['decisions'] ?? [], + $data['meta'] ?? [], + $data['labels'] ?? [] + ); + } + + /** + * @return TAlertFull + */ + public function toArray(): array + { + $result = $this->properties; + if ([] !== $this->decisions) { + $result['decisions'] = $this->decisions; + } + if ([] !== $this->events) { + $result['events'] = $this->events; + } + if (null !== $this->source) { + $result['source'] = $this->source; + } + if ([] !== $this->meta) { + $result['meta'] = $this->meta; + } + if ([] !== $this->labels) { + $result['labels'] = $this->labels; + } + return $result; + } + + private function configureProperties(Processor $processor, array $properties): void + { + $configuration = new AlertConf(); + $this->properties = $processor->processConfiguration( + $configuration, + [$configuration->cleanConfigs($properties)] + ); + } + + /** + * @param ?TSource $source + */ + private function configureSource(Processor $processor, ?array $source): void + { + if (null === $source) { + return; + } + + $configuration = new Source(); + $this->source = $processor->processConfiguration($configuration, [$configuration->cleanConfigs($source)]); + } + + /** + * @param list $list + */ + private function configureDecisions(Processor $processor, array $list): void + { + $this->decisions = $this->handleList($processor, new Decision(), $list); + } + + /** + * @param list $list + */ + private function configureEvents(Processor $processor, array $list): void + { + $this->events = $this->handleList($processor, new Event(), $list); + } + + /** + * @param list $list + */ + private function configureMetaList(Processor $processor, array $list): void + { + $this->meta = $this->handleList($processor, new Meta(), $list); + } + + private function handleList(Processor $processor, AbstractConfiguration $param, array $list): array + { + $result = []; + foreach ($list as $item) { + $result[] = $processor->processConfiguration($param, [$param->cleanConfigs($item)]); + } + return $result; + } + + public function jsonSerialize() + { + return $this->toArray(); + } +} diff --git a/src/Storage/TokenStorage.php b/src/Storage/TokenStorage.php new file mode 100644 index 0000000..0c629f8 --- /dev/null +++ b/src/Storage/TokenStorage.php @@ -0,0 +1,52 @@ +watcher = $watcher; + $this->cache = $cache; + $this->scenarios = $scenarios; + } + + public function retrieveToken(): ?string + { + $ci = $this->cache->getItem('crowdsec_token'); + if (!$ci->isHit()) { + $tokenInfo = $this->watcher->login($this->scenarios); + if (200 !== $tokenInfo['code']) { + return null; + } + \assert(!empty($tokenInfo['token'])); + $ci + ->set($tokenInfo['token']) + ->expiresAt(new \DateTime($tokenInfo['expire'])); + $this->cache->save($ci); + } + return $ci->get(); + } +} diff --git a/src/Storage/TokenStorageInterface.php b/src/Storage/TokenStorageInterface.php new file mode 100644 index 0000000..e16dab7 --- /dev/null +++ b/src/Storage/TokenStorageInterface.php @@ -0,0 +1,14 @@ + $scenarios, + ]; + if ($this->configs['auth_type'] === Constants::AUTH_KEY) { + $data['machine_id'] = $this->configs['machine_id']; + $data['password'] = $this->configs['password']; + } + + return $this->manageRequest( + 'POST', + Constants::WATCHER_LOGIN_ENDPOINT, + $data + ); + } +} diff --git a/tests/Integration/AlertsClientTest.php b/tests/Integration/AlertsClientTest.php new file mode 100644 index 0000000..57d954d --- /dev/null +++ b/tests/Integration/AlertsClientTest.php @@ -0,0 +1,406 @@ +useTls = (string)getenv('BOUNCER_TLS_PATH'); + + $bouncerConfigs = [ + 'auth_type' => $this->useTls ? Constants::AUTH_TLS : Constants::AUTH_KEY, + 'api_key' => getenv('BOUNCER_KEY'), + 'api_url' => getenv('LAPI_URL'), + 'appsec_url' => getenv('APPSEC_URL'), + 'user_agent_suffix' => TestConstants::USER_AGENT_SUFFIX, + ]; + if ($this->useTls) { + $this->addTlsConfig($bouncerConfigs, $this->useTls); + } + + $this->configs = $bouncerConfigs; + + $watcher = new WatcherClient($this->configs); + $tokenStorage = new TokenStorage($watcher, new ArrayAdapter()); + $this->alertsClient = new AlertsClient($this->configs, $tokenStorage); + } + + /** + * @covers ::delete + */ + public function testDelete(): void + { + self::expectException(\RuntimeException::class); + $this->alertsClient->delete([]); + } + + /** + * @covers ::push + */ + public function testPush(): array + { + $now = new \DateTimeImmutable(); + $alert01 = new Alert( + [ + 'scenario' => 'crowdsec-lapi-test/with-decision', + 'scenario_hash' => 'alert01', + 'scenario_version' => '1.0', + 'message' => 'alert01', + 'events_count' => 3, + 'start_at' => $now->format(self::DT_FORMAT), + 'stop_at' => $now + ->add(new \DateInterval('PT4H')) + ->format(self::DT_FORMAT), + 'capacity' => 10, + 'leakspeed' => '10/1s', + 'simulated' => false, + 'remediation' => false, + ], + // source + [ + 'scope' => 'ip', + 'value' => '1.1.0.1', + 'as_number' => 'AS12345', + 'as_name' => 'EXAMPLE-AS', + 'cn' => 'US', + 'latitude' => 40.7128, + 'longitude' => -74.0060, + ], + // events + [ + [ + 'meta' => [ + ['key' => 'path', 'value' => '/alert11'], + ], + 'timestamp' => $now->format(self::DT_FORMAT), + ], + ], + // decisions + [ + [ + 'origin' => 'lapi', + 'type' => 'ban', + 'scope' => 'ip', + 'value' => '1.1.0.1', + 'duration' => '4h', + 'until' => $now + ->add(new \DateInterval('PT4H')) + ->format(self::DT_FORMAT), + 'scenario' => 'crowdsec-lapi-test/with-decision', + ], + ], + [ + ['key' => 'service', 'value' => 'phpunit'], + ], + ['http', 'probing'] + ); + $alert02 = new Alert( + [ + 'scenario' => 'crowdsec-lapi-test/with-decision', + 'scenario_hash' => 'alert02', + 'scenario_version' => '1.0', + 'message' => 'alert02', + 'events_count' => 3, + 'start_at' => $now->format(self::DT_FORMAT), + 'stop_at' => $now + ->add(new \DateInterval('PT4H')) + ->format(self::DT_FORMAT), + 'capacity' => 10, + 'leakspeed' => '10/1s', + 'simulated' => true, + 'remediation' => true, + ], + // source + [ + 'scope' => 'range', + 'value' => '1.1.0.0/16', + 'as_number' => 'AS12345', + 'as_name' => 'EXAMPLE-AS', + 'cn' => 'US', + 'latitude' => 40.7128, + 'longitude' => -74.0060, + ], + // events + [ + [ + 'meta' => [ + ['key' => 'path', 'value' => '/alert12'], + ], + 'timestamp' => $now->format(self::DT_FORMAT), + ], + ], + // decisions + [ + [ + 'origin' => 'phpunit', + 'type' => 'captcha', + 'scope' => 'range', + 'value' => '1.1.0.0/16', + 'duration' => '4h', + 'until' => $now + ->add(new \DateInterval('PT4H')) + ->format(self::DT_FORMAT), + 'scenario' => 'crowdsec-lapi-test/with-decision', + ], + ] + ); + $alert11 = new Alert( + [ + 'scenario' => 'crowdsec-lapi-test/integration11', + 'scenario_hash' => 'alert11', + 'scenario_version' => '1.0', + 'message' => 'alert10', + 'events_count' => 3, + 'start_at' => $now->format(self::DT_FORMAT), + 'stop_at' => $now + ->add(new \DateInterval('PT4H')) + ->format(self::DT_FORMAT), + 'capacity' => 11, + 'leakspeed' => '10/2s', + 'simulated' => false, + 'remediation' => false, + ], + // source + [ + 'scope' => 'ip', + 'value' => '2.0.1.1', + 'as_number' => 'AS12345', + 'as_name' => 'EXAMPLE-AS', + 'cn' => 'US', + 'latitude' => 40.7128, + 'longitude' => -74.0060, + ], + // events + [ + [ + 'meta' => [ + ['key' => 'path', 'value' => '/alert21'], + ], + 'timestamp' => $now->format(self::DT_FORMAT), + ], + ] + ); + $alert12 = new Alert( + [ + 'scenario' => 'crowdsec-lapi-test/integration12', + 'scenario_hash' => 'alert12', + 'scenario_version' => '1.0', + 'message' => 'alert12', + 'events_count' => 3, + 'start_at' => $now->format(self::DT_FORMAT), + 'stop_at' => $now + ->add(new \DateInterval('PT4H')) + ->format(self::DT_FORMAT), + 'capacity' => 12, + 'leakspeed' => '10/2s', + 'simulated' => true, + 'remediation' => true, + ], + // source + [ + 'scope' => 'range', + 'value' => '2.0.0.0/16', + 'as_number' => 'AS12345', + 'as_name' => 'EXAMPLE-AS', + 'cn' => 'US', + 'latitude' => 40.7128, + 'longitude' => -74.0060, + ], + // events + [ + [ + 'meta' => [ + ['key' => 'path', 'value' => '/alert21'], + ], + 'timestamp' => $now->format(self::DT_FORMAT), + ], + ] + ); + $result = $this->alertsClient->push([ + // with decisions + $alert01, + $alert02, + // without decisions + $alert11, + $alert12, + ]); + self::assertIsArray($result); + self::assertCount(4, $result); + return $result; + } + + /** + * @covers ::search + * @depends testPush + * @dataProvider searchProvider + */ + public function testSearch(array $query, int $expectedCount): void + { + $result = $this->alertsClient->search($query); + self::assertCount($expectedCount, $result); + } + + public static function searchProvider(): iterable + { + yield 'empty' => [ + [], + 4 + ]; + + yield 'ip - no' => [ + ['ip' => '19.17.11.7'], + 0 + ]; + + yield 'ip - 1.1.0.1' => [ + ['ip' => '1.1.0.1'], // alert01 (scope=ip;value=1.1.0.1 +decision) and alert02(scope=range;value=1.1.0.0/16 +decision) + 2 + ]; + yield 'ip - 2.0.1.1' => [ + ['ip' => '2.0.1.1'], // alert12 (range no decision) + 1 + ]; + + yield 'scope - ip' => [ + ['scope' => 'ip'], + 2, + ]; + yield 'scope - range' => [ + ['scope' => 'range'], + 2, + ]; + + yield 'scope - ip:1.1.0.1' => [ + ['scope' => 'ip', 'value' => '1.1.0.1'], + 1, + ]; + + yield 'scenario' => [ + ['scenario' => 'crowdsec-lapi-test/with-decision'], + 2 + ]; + + yield 'has_active_decision=true' => [ + ['has_active_decision' => 'true'], + 0, + ]; + + yield 'has_active_decision=false' => [ + ['has_active_decision' => 'false'], + 1, //crowdsec-lapi-test/integration11 + ]; +// TODO: why 4 byt not 2 ? +// yield 'simulated=true' => [ +// ['simulated' => 'true'], +// 4, +// ]; + yield 'simulated=false' => [ + ['simulated' => 'false'], + 2, + ]; + + yield 'since -1h' => [ + [ + 'since' => '-1h', + ], + 0, + ]; + yield 'since 1s' => [ + ['since' => '1s'], + 0, + ]; + yield 'since 1h' => [ + ['since' => '10h'], + 4, + ]; + + yield 'until -1h' => [ + ['until' => '-1h'], + 4, + ]; + yield 'until 1s' => [ + ['until' => '1s'], + 4, + ]; + yield 'until 1h' => [ + ['until' => '1h'], + 0, + ]; + yield 'until 10h' => [ + ['until' => '10h'], + 0, + ]; + yield 'until 100h' => [ + ['until' => '10h'], + 0, + ]; + + yield 'origin=phpunit' => [ + ['origin' => 'phpunit'], + 1, + ]; + yield 'decision_type=ban' => [ + ['decision_type' => 'ban'], + 2, + ]; + } + + /** + * @depends testPush + */ + public function testGetById(array $idList): void + { + foreach ($idList as $id) { + self::assertIsNumeric($id); + $result = $this->alertsClient->getById(\intval($id)); + self::assertIsArray($result); + } + } + + public function testAlertInfoNotFound(): void + { + $result = $this->alertsClient->getById(PHP_INT_MAX); + self::assertNull($result); + } +} diff --git a/tests/Integration/BouncerTest.php b/tests/Integration/BouncerTest.php index 175d5e1..b4b15a9 100644 --- a/tests/Integration/BouncerTest.php +++ b/tests/Integration/BouncerTest.php @@ -36,7 +36,7 @@ final class BouncerTest extends TestCase */ protected $useTls; /** - * @var WatcherClient + * @var TestWatcherClient */ protected $watcherClient; @@ -64,7 +64,7 @@ protected function setUp(): void } $this->configs = $bouncerConfigs; - $this->watcherClient = new WatcherClient($this->configs); + $this->watcherClient = new TestWatcherClient($this->configs); // Delete all decisions $this->watcherClient->deleteAllDecisions(); usleep(200000); // 200ms diff --git a/tests/Integration/WatcherClient.php b/tests/Integration/TestWatcherClient.php similarity index 93% rename from tests/Integration/WatcherClient.php rename to tests/Integration/TestWatcherClient.php index e36323b..c36b943 100644 --- a/tests/Integration/WatcherClient.php +++ b/tests/Integration/TestWatcherClient.php @@ -7,11 +7,10 @@ use CrowdSec\Common\Client\AbstractClient; use CrowdSec\LapiClient\ClientException; use CrowdSec\LapiClient\Constants; +use CrowdSec\LapiClient\WatcherClient; -class WatcherClient extends AbstractClient +class TestWatcherClient extends AbstractClient { - public const WATCHER_LOGIN_ENDPOINT = '/v1/watchers/login'; - public const WATCHER_DECISIONS_ENDPOINT = '/v1/decisions'; public const WATCHER_ALERT_ENDPOINT = '/v1/alerts'; @@ -25,6 +24,9 @@ class WatcherClient extends AbstractClient */ protected $headers = []; + /** @var WatcherClient */ + protected $watcher; + public function __construct(array $configs) { $this->configs = $configs; @@ -38,6 +40,8 @@ public function __construct(array $configs) $this->configs['tls_key_path'] = $agentTlsPath . '/agent-key.pem'; $this->configs['tls_verify_peer'] = false; + $this->watcher = new WatcherClient($this->configs); + parent::__construct($this->configs); } @@ -96,15 +100,7 @@ public function setSecondState(): void private function ensureLogin(): void { if (!$this->token) { - $data = [ - 'scenarios' => [], - ]; - $credentials = $this->manageRequest( - 'POST', - self::WATCHER_LOGIN_ENDPOINT, - $data - ); - + $credentials = $this->watcher->login(); $this->token = $credentials['token']; $this->headers['Authorization'] = 'Bearer ' . $this->token; } @@ -160,8 +156,7 @@ public function addDecision( 'value' => $value, ], ], - 'events' => [ - ], + 'events' => [], 'events_count' => 1, 'labels' => null, 'leakspeed' => '0', diff --git a/tests/Integration/WatcherClientTest.php b/tests/Integration/WatcherClientTest.php new file mode 100644 index 0000000..eb62547 --- /dev/null +++ b/tests/Integration/WatcherClientTest.php @@ -0,0 +1,76 @@ + getenv('LAPI_URL'), + 'appsec_url' => getenv('APPSEC_URL'), + 'user_agent_suffix' => TestConstants::USER_AGENT_SUFFIX, + 'auth_type' => Constants::AUTH_TLS, + 'tls_cert_path' => "{$agentTlsPath}/agent.pem", + 'tls_key_path' => "{$agentTlsPath}/agent-key.pem", + 'tls_verify_peer' => false, + ]; + + $watcher = new WatcherClient($bouncerConfigs); + self::assertLoginResult($watcher->login()); + } + + public function testLoginApiKey(): void + { + $machineId = getenv('MACHINE_ID') ?: 'watcherLogin'; + $password = getenv('PASSWORD') ?: 'watcherPassword'; + + $bouncerConfigs = [ + 'api_url' => getenv('LAPI_URL'), + 'appsec_url' => getenv('APPSEC_URL'), + 'user_agent_suffix' => TestConstants::USER_AGENT_SUFFIX, + 'auth_type' => Constants::AUTH_KEY, + 'api_key' => getenv('BOUNCER_KEY'), + 'machine_id' => $machineId, + 'password' => $password, + ]; + + $watcher = new WatcherClient($bouncerConfigs); + self::assertLoginResult($watcher->login()); + } + + private static function assertLoginResult(array $data): void + { + self::assertArrayHasKey('code', $data); + self::assertArrayHasKey('expire', $data); + self::assertArrayHasKey('token', $data); + + self::assertSame(200, $data['code']); + self::assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/', $data['expire']); + // JWT + $parts = explode('.', $data['token']); + self::assertCount(3, $parts); + $payloadStr = \base64_decode($parts[1]); + self::assertNotSame(false, $payloadStr); + $payload = \json_decode($payloadStr, true); + self::assertNotEmpty($payload); + self::assertArrayHasKey('exp', $payload); + self::assertTrue(\is_int($payload['exp'])); + self::assertArrayHasKey('id', $payload); + self::assertTrue(\is_string($payload['id'])); + self::assertArrayHasKey('orig_iat', $payload); + self::assertTrue(\is_int($payload['orig_iat'])); + } +} diff --git a/tests/Unit/FileGetContentsTest.php b/tests/Unit/FileGetContentsTest.php index b034b88..27ea5b7 100644 --- a/tests/Unit/FileGetContentsTest.php +++ b/tests/Unit/FileGetContentsTest.php @@ -17,7 +17,6 @@ * @license MIT License */ -use CrowdSec\Common\Client\HttpMessage\Request; use CrowdSec\LapiClient\Bouncer; use CrowdSec\LapiClient\Tests\MockedData; use CrowdSec\LapiClient\TimeoutException; diff --git a/tests/Unit/Payload/AlertTest.php b/tests/Unit/Payload/AlertTest.php new file mode 100644 index 0000000..22fc26e --- /dev/null +++ b/tests/Unit/Payload/AlertTest.php @@ -0,0 +1,93 @@ +toArray()); + } + + public function dpConstruct(): iterable + { + $base = [ + 'scenario' => 'crowdsecurity/http-probing', + 'scenario_hash' => 'abc123', + 'scenario_version' => '1.0', + 'message' => 'Probing detected', + 'events_count' => 3, + 'start_at' => '2025-01-01T00:00:00Z', + 'stop_at' => '2025-01-01T00:10:00Z', + 'capacity' => 10, + 'leakspeed' => '10/1s', + 'simulated' => false, + 'remediation' => true, + 'source' => [ + 'scope' => 'ip', + 'value' => '1.2.3.4', + 'ip' => '1.2.3.4', + 'range' => '1.2.3.4/32', + 'as_number' => 'AS12345', + 'as_name' => 'EXAMPLE-AS', + 'cn' => 'US', + 'latitude' => 40.7128, + 'longitude' => -74.0060, + ], + 'decisions' => [ + [ + 'origin' => 'lapi', + 'type' => 'ban', + 'scope' => 'ip', + 'value' => '1.2.3.4', + 'duration' => '4h', + 'until' => '2025-01-01T04:00:00Z', + 'scenario' => 'crowdsecurity/http-probing', + ], + ], + 'events' => [ + [ + 'meta' => [ + ['key' => 'path', 'value' => '/admin'], + ], + 'timestamp' => '2025-01-01T00:00:01Z', + ], + ], + 'meta' => [ + ['key' => 'service', 'value' => 'nginx'], + ], + 'labels' => ['http', 'probing'], + ]; + yield 'full example' => [ + $base, + $base, + ]; + + $minimal = $base; + unset( + $minimal['event'], + $minimal['decisions'], + $minimal['source'], + $minimal['meta'], + $minimal['labels'], + ); + yield 'minimal example' => [ + $minimal, + $minimal, + ]; + } +} diff --git a/tests/Unit/Storage/TokenStorageTest.php b/tests/Unit/Storage/TokenStorageTest.php new file mode 100644 index 0000000..ece5d73 --- /dev/null +++ b/tests/Unit/Storage/TokenStorageTest.php @@ -0,0 +1,34 @@ +createMock(WatcherClient::class); + $expire = time() + 3600; + $watcher + ->expects(self::once()) + ->method('login') + ->willReturn([ + 'code' => 200, + 'expire' => $expire, + 'token' => 'j.w.t', + ]); + $cache = new ArrayAdapter(); + $storage = new TokenStorage($watcher, $cache); + self::assertSame('j.w.t', $storage->retrieveToken()); + self::assertTrue($cache->hasItem('crowdsec_token')); + $ci = $cache->getItem('crowdsec_token'); + self::assertSame('j.w.t', $ci->get()); + } +} From 491b40ddc8f9aa4fe996c52e6a764500244b978b Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Fri, 7 Nov 2025 15:46:19 +0300 Subject: [PATCH 02/29] try to fix phpmd --- src/Storage/TokenStorage.php | 3 ++- src/WatcherClient.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Storage/TokenStorage.php b/src/Storage/TokenStorage.php index 0c629f8..5d189d0 100644 --- a/src/Storage/TokenStorage.php +++ b/src/Storage/TokenStorage.php @@ -5,6 +5,7 @@ namespace CrowdSec\LapiClient\Storage; use CrowdSec\LapiClient\WatcherClient; +use DateTime; use Psr\Cache\CacheItemPoolInterface; final class TokenStorage implements TokenStorageInterface @@ -44,7 +45,7 @@ public function retrieveToken(): ?string \assert(!empty($tokenInfo['token'])); $ci ->set($tokenInfo['token']) - ->expiresAt(new \DateTime($tokenInfo['expire'])); + ->expiresAt(new DateTime($tokenInfo['expire'])); $this->cache->save($ci); } return $ci->get(); diff --git a/src/WatcherClient.php b/src/WatcherClient.php index f644936..bc37373 100644 --- a/src/WatcherClient.php +++ b/src/WatcherClient.php @@ -5,6 +5,7 @@ namespace CrowdSec\LapiClient; use CrowdSec\Common\Client\RequestHandler\RequestHandlerInterface; +use LogicException; use Psr\Log\LoggerInterface; /** @@ -25,7 +26,7 @@ public function __construct( ) { if ($configs['auth_type'] === Constants::AUTH_KEY) { if (empty($configs['machine_id']) || empty($configs['password'])) { - throw new \LogicException('Missing required config: machine_id or password.'); + throw new LogicException('Missing required config: machine_id or password.'); } } From 7edec8e26a426706f450d7739bf21bbc50210666 Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Fri, 7 Nov 2025 16:12:49 +0300 Subject: [PATCH 03/29] up --- src/Payload/Alert.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Payload/Alert.php b/src/Payload/Alert.php index 15e45e7..52673ca 100644 --- a/src/Payload/Alert.php +++ b/src/Payload/Alert.php @@ -230,7 +230,7 @@ private function handleList(Processor $processor, AbstractConfiguration $param, return $result; } - public function jsonSerialize() + public function jsonSerialize(): mixed { return $this->toArray(); } From 46c399d499b9c83ad676ee09e9bb645dcdd33139 Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Fri, 7 Nov 2025 16:18:49 +0300 Subject: [PATCH 04/29] up --- src/WatcherClient.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WatcherClient.php b/src/WatcherClient.php index bc37373..8617399 100644 --- a/src/WatcherClient.php +++ b/src/WatcherClient.php @@ -43,8 +43,8 @@ public function login(array $scenarios = []): array 'scenarios' => $scenarios, ]; if ($this->configs['auth_type'] === Constants::AUTH_KEY) { - $data['machine_id'] = $this->configs['machine_id']; - $data['password'] = $this->configs['password']; + $data['machine_id'] = $this->configs['machine_id'] ?? ''; + $data['password'] = $this->configs['password'] ?? ''; } return $this->manageRequest( From 444dd536ddb571a1a3e3ac07d5fac5b37857428d Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Fri, 7 Nov 2025 16:20:05 +0300 Subject: [PATCH 05/29] up --- src/Payload/Alert.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Payload/Alert.php b/src/Payload/Alert.php index 52673ca..5472f07 100644 --- a/src/Payload/Alert.php +++ b/src/Payload/Alert.php @@ -157,15 +157,11 @@ public static function fromArray(array $data): self public function toArray(): array { $result = $this->properties; + $result['events'] = $this->events; + $result['source'] = $this->source; if ([] !== $this->decisions) { $result['decisions'] = $this->decisions; } - if ([] !== $this->events) { - $result['events'] = $this->events; - } - if (null !== $this->source) { - $result['source'] = $this->source; - } if ([] !== $this->meta) { $result['meta'] = $this->meta; } From 046dc7807ff366f2f7c5a0dfc00c1cfd1d90a397 Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Fri, 7 Nov 2025 16:27:16 +0300 Subject: [PATCH 06/29] up --- src/Payload/Alert.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Payload/Alert.php b/src/Payload/Alert.php index 5472f07..55b933e 100644 --- a/src/Payload/Alert.php +++ b/src/Payload/Alert.php @@ -217,6 +217,9 @@ private function configureMetaList(Processor $processor, array $list): void $this->meta = $this->handleList($processor, new Meta(), $list); } + /** + * @return list + */ private function handleList(Processor $processor, AbstractConfiguration $param, array $list): array { $result = []; From a782589d5f410d913b90f0045188b7e6a19fc11c Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Fri, 7 Nov 2025 16:31:02 +0300 Subject: [PATCH 07/29] up --- src/Payload/Alert.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Payload/Alert.php b/src/Payload/Alert.php index 55b933e..5472f07 100644 --- a/src/Payload/Alert.php +++ b/src/Payload/Alert.php @@ -217,9 +217,6 @@ private function configureMetaList(Processor $processor, array $list): void $this->meta = $this->handleList($processor, new Meta(), $list); } - /** - * @return list - */ private function handleList(Processor $processor, AbstractConfiguration $param, array $list): array { $result = []; From dc0ce78e71c4ecc746617f6cec8d97214a969477 Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Fri, 7 Nov 2025 16:31:35 +0300 Subject: [PATCH 08/29] up --- src/Storage/TokenStorage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Storage/TokenStorage.php b/src/Storage/TokenStorage.php index 5d189d0..b577d1d 100644 --- a/src/Storage/TokenStorage.php +++ b/src/Storage/TokenStorage.php @@ -42,7 +42,7 @@ public function retrieveToken(): ?string if (200 !== $tokenInfo['code']) { return null; } - \assert(!empty($tokenInfo['token'])); + \assert(isset($tokenInfo['token'])); $ci ->set($tokenInfo['token']) ->expiresAt(new DateTime($tokenInfo['expire'])); From cb0e8b2ab07ffea80d1f16f9866fda8aaa3f9801 Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Fri, 7 Nov 2025 16:46:02 +0300 Subject: [PATCH 09/29] try to satisfy psalm --- src/Payload/Alert.php | 4 ++-- src/WatcherClient.php | 2 +- tools/coding-standards/psalm/psalm.xml | 9 +++++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Payload/Alert.php b/src/Payload/Alert.php index 5472f07..0944cea 100644 --- a/src/Payload/Alert.php +++ b/src/Payload/Alert.php @@ -131,7 +131,7 @@ public function __construct( $this->configureSource($processor, $source); $this->configureDecisions($processor, $decisions); $this->configureEvents($processor, $events); - $this->configureMetaList($processor, $meta); + $this->configureMeta($processor, $meta); $this->labels = \array_filter($labels); } @@ -212,7 +212,7 @@ private function configureEvents(Processor $processor, array $list): void /** * @param list $list */ - private function configureMetaList(Processor $processor, array $list): void + private function configureMeta(Processor $processor, array $list): void { $this->meta = $this->handleList($processor, new Meta(), $list); } diff --git a/src/WatcherClient.php b/src/WatcherClient.php index 8617399..41a7fc4 100644 --- a/src/WatcherClient.php +++ b/src/WatcherClient.php @@ -42,7 +42,7 @@ public function login(array $scenarios = []): array $data = [ 'scenarios' => $scenarios, ]; - if ($this->configs['auth_type'] === Constants::AUTH_KEY) { + if (isset($this->configs['auth_type']) && $this->configs['auth_type'] === Constants::AUTH_KEY) { $data['machine_id'] = $this->configs['machine_id'] ?? ''; $data['password'] = $this->configs['password'] ?? ''; } diff --git a/tools/coding-standards/psalm/psalm.xml b/tools/coding-standards/psalm/psalm.xml index a7780fa..7e6e52c 100644 --- a/tools/coding-standards/psalm/psalm.xml +++ b/tools/coding-standards/psalm/psalm.xml @@ -25,5 +25,14 @@ + + + + + + + + + From 10f445d307390b6888d9a0b07233ce64fe51d17b Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Fri, 7 Nov 2025 17:28:29 +0300 Subject: [PATCH 10/29] try to satisfy psalm --- tools/coding-standards/psalm/psalm.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/coding-standards/psalm/psalm.xml b/tools/coding-standards/psalm/psalm.xml index 7e6e52c..ceb459d 100644 --- a/tools/coding-standards/psalm/psalm.xml +++ b/tools/coding-standards/psalm/psalm.xml @@ -27,11 +27,11 @@ - - - - - + + + + + From 9cbce91cf4472482b0f1b3c0e4c9f4315ebcdd2f Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Fri, 7 Nov 2025 18:44:08 +0300 Subject: [PATCH 11/29] try to satisfy psalm --- tools/coding-standards/psalm/psalm.xml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tools/coding-standards/psalm/psalm.xml b/tools/coding-standards/psalm/psalm.xml index ceb459d..8bb3696 100644 --- a/tools/coding-standards/psalm/psalm.xml +++ b/tools/coding-standards/psalm/psalm.xml @@ -27,11 +27,7 @@ - - - - - + From b64740f2358fb7e3302bed05b5730fa50acff092 Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Fri, 7 Nov 2025 18:47:03 +0300 Subject: [PATCH 12/29] try to satisfy psalm --- tools/coding-standards/psalm/psalm.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/coding-standards/psalm/psalm.xml b/tools/coding-standards/psalm/psalm.xml index 8bb3696..0c26b8c 100644 --- a/tools/coding-standards/psalm/psalm.xml +++ b/tools/coding-standards/psalm/psalm.xml @@ -27,7 +27,10 @@ - + + + + From 4caf39ed12fd844c61783c3d33fb7c96e886e7e3 Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Sat, 8 Nov 2025 23:46:57 +0300 Subject: [PATCH 13/29] try to satisfy psalm --- src/Payload/Alert.php | 8 ++++---- tools/coding-standards/psalm/psalm.xml | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Payload/Alert.php b/src/Payload/Alert.php index 0944cea..99cb5fa 100644 --- a/src/Payload/Alert.php +++ b/src/Payload/Alert.php @@ -73,9 +73,9 @@ * remediation: bool, * source: TSource, * events: list, - * decisions: list, - * meta: list, - * labels: list + * decisions?: list, + * meta?: list, + * labels?: list * } */ class Alert implements \JsonSerializable @@ -157,8 +157,8 @@ public static function fromArray(array $data): self public function toArray(): array { $result = $this->properties; - $result['events'] = $this->events; $result['source'] = $this->source; + $result['events'] = $this->events; if ([] !== $this->decisions) { $result['decisions'] = $this->decisions; } diff --git a/tools/coding-standards/psalm/psalm.xml b/tools/coding-standards/psalm/psalm.xml index 0c26b8c..ceb459d 100644 --- a/tools/coding-standards/psalm/psalm.xml +++ b/tools/coding-standards/psalm/psalm.xml @@ -27,6 +27,7 @@ + From 7371ba58fe8680fd2f19408adab1e09152c70f2b Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Sat, 8 Nov 2025 23:50:41 +0300 Subject: [PATCH 14/29] try to satisfy psalm --- tools/coding-standards/psalm/psalm.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/coding-standards/psalm/psalm.xml b/tools/coding-standards/psalm/psalm.xml index ceb459d..16ac56d 100644 --- a/tools/coding-standards/psalm/psalm.xml +++ b/tools/coding-standards/psalm/psalm.xml @@ -34,5 +34,10 @@ + + + + + From 31ccc2e2a156f0918ad904b08cfc7abc6ebc08ff Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Sat, 8 Nov 2025 23:52:01 +0300 Subject: [PATCH 15/29] try to satisfy psalm --- src/Configuration/Alert.php | 2 +- src/Configuration/Alert/Decision.php | 2 +- src/Configuration/Alert/Event.php | 2 +- src/Configuration/Alert/Meta.php | 2 +- src/Configuration/Alert/Source.php | 2 +- src/Configuration/Metrics.php | 2 +- src/Configuration/Metrics/Items.php | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Configuration/Alert.php b/src/Configuration/Alert.php index eeae807..c210036 100644 --- a/src/Configuration/Alert.php +++ b/src/Configuration/Alert.php @@ -10,7 +10,7 @@ class Alert extends AbstractConfiguration { - /** @var list The list of each configuration tree key */ + /** @var string[] The list of each configuration tree key */ protected $keys = [ 'scenario', 'scenario_hash', diff --git a/src/Configuration/Alert/Decision.php b/src/Configuration/Alert/Decision.php index 5927275..9618b6d 100644 --- a/src/Configuration/Alert/Decision.php +++ b/src/Configuration/Alert/Decision.php @@ -7,7 +7,7 @@ class Decision extends AbstractConfiguration { - /** @var list The list of each configuration tree key */ + /** @var string[] The list of each configuration tree key */ protected $keys = [ 'origin', 'type', diff --git a/src/Configuration/Alert/Event.php b/src/Configuration/Alert/Event.php index 75f4750..09db054 100644 --- a/src/Configuration/Alert/Event.php +++ b/src/Configuration/Alert/Event.php @@ -7,7 +7,7 @@ class Event extends AbstractConfiguration { - /** @var list The list of each configuration tree key */ + /** @var string[] The list of each configuration tree key */ protected $keys = [ 'meta', 'timestamp', diff --git a/src/Configuration/Alert/Meta.php b/src/Configuration/Alert/Meta.php index e56b064..ea14e05 100644 --- a/src/Configuration/Alert/Meta.php +++ b/src/Configuration/Alert/Meta.php @@ -7,7 +7,7 @@ class Meta extends AbstractConfiguration { - /** @var list The list of each configuration tree key */ + /** @var string[] The list of each configuration tree key */ protected $keys = [ 'key', 'value', diff --git a/src/Configuration/Alert/Source.php b/src/Configuration/Alert/Source.php index 8b4215d..70542e1 100644 --- a/src/Configuration/Alert/Source.php +++ b/src/Configuration/Alert/Source.php @@ -7,7 +7,7 @@ class Source extends AbstractConfiguration { - /** @var list The list of each configuration tree key */ + /** @var string[] The list of each configuration tree key */ protected $keys = [ 'scope', 'value', diff --git a/src/Configuration/Metrics.php b/src/Configuration/Metrics.php index 279e7d2..80f46ab 100644 --- a/src/Configuration/Metrics.php +++ b/src/Configuration/Metrics.php @@ -21,7 +21,7 @@ */ class Metrics extends AbstractConfiguration { - /** @var list The list of each configuration tree key */ + /** @var string[] The list of each configuration tree key */ protected $keys = [ 'name', 'type', diff --git a/src/Configuration/Metrics/Items.php b/src/Configuration/Metrics/Items.php index 4b698e2..e01a646 100644 --- a/src/Configuration/Metrics/Items.php +++ b/src/Configuration/Metrics/Items.php @@ -20,7 +20,7 @@ */ class Items extends AbstractConfiguration { - /** @var list The list of each configuration tree key */ + /** @var string[] The list of each configuration tree key */ protected $keys = [ 'name', 'value', From be4a9906bfa74f334af17db92cfe58b840f452db Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Sun, 9 Nov 2025 00:01:37 +0300 Subject: [PATCH 16/29] try to satisfy psalm --- src/AbstractLapiClient.php | 4 ++-- src/Configuration.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AbstractLapiClient.php b/src/AbstractLapiClient.php index becbc3f..967cbc3 100644 --- a/src/AbstractLapiClient.php +++ b/src/AbstractLapiClient.php @@ -15,7 +15,7 @@ abstract class AbstractLapiClient extends AbstractClient { /** - * @var TBouncerConfig + * @var array|TBouncerConfig */ protected $configs; /** @@ -30,7 +30,7 @@ public function __construct( ) { $this->configure($configs); $this->headers = [Constants::HEADER_LAPI_USER_AGENT => $this->formatUserAgent($this->configs)]; - if (!empty($this->configs['api_key'])) { + if (isset($this->configs['api_key'])) { $this->headers[Constants::HEADER_LAPI_API_KEY] = $this->configs['api_key']; } parent::__construct($this->configs, $requestHandler, $logger); diff --git a/src/Configuration.php b/src/Configuration.php index 82aac1a..72d4505 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -40,7 +40,7 @@ */ class Configuration extends AbstractConfiguration { - /** @var list The list of each configuration tree key */ + /** @var string[] The list of each configuration tree key */ protected $keys = [ 'user_agent_suffix', 'user_agent_version', From 6d2f3b7d06c1a4411f764b0e10a19db0372ef3c6 Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Sun, 9 Nov 2025 00:02:25 +0300 Subject: [PATCH 17/29] try to satisfy psalm --- src/Payload/Alert.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Payload/Alert.php b/src/Payload/Alert.php index 99cb5fa..1a4e7af 100644 --- a/src/Payload/Alert.php +++ b/src/Payload/Alert.php @@ -137,7 +137,6 @@ public function __construct( /** * @param TAlertFull $data - * @return void */ public static function fromArray(array $data): self { From c3cdd1797acb4012eb632390a457aa06b2ebc7a2 Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Sun, 9 Nov 2025 00:09:51 +0300 Subject: [PATCH 18/29] try to satisfy psalm --- src/Configuration/Metrics/Items.php | 4 ++-- src/Payload/Alert.php | 2 +- tools/coding-standards/psalm/psalm.xml | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Configuration/Metrics/Items.php b/src/Configuration/Metrics/Items.php index e01a646..e8a67a5 100644 --- a/src/Configuration/Metrics/Items.php +++ b/src/Configuration/Metrics/Items.php @@ -63,13 +63,13 @@ public function getConfigTreeBuilder(): TreeBuilder ->variableNode('labels') // Remove empty labels totally ->beforeNormalization() - ->ifTrue(function ($value) { + ->ifTrue(function (mixed $value) { return empty($value); }) ->thenUnset() ->end() ->validate() - ->ifTrue(function ($value) { + ->ifTrue(function (mixed $value) { // Ensure all values in the array are strings if (!is_array($value)) { return true; diff --git a/src/Payload/Alert.php b/src/Payload/Alert.php index 1a4e7af..f52622a 100644 --- a/src/Payload/Alert.php +++ b/src/Payload/Alert.php @@ -132,7 +132,7 @@ public function __construct( $this->configureDecisions($processor, $decisions); $this->configureEvents($processor, $events); $this->configureMeta($processor, $meta); - $this->labels = \array_filter($labels); + $this->labels = $labels; } /** diff --git a/tools/coding-standards/psalm/psalm.xml b/tools/coding-standards/psalm/psalm.xml index 16ac56d..252d95b 100644 --- a/tools/coding-standards/psalm/psalm.xml +++ b/tools/coding-standards/psalm/psalm.xml @@ -18,6 +18,7 @@ + @@ -31,7 +32,7 @@ - + From 7d671210b2284ba6f66e9b6db4f59551ecba2852 Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Sun, 9 Nov 2025 00:36:05 +0300 Subject: [PATCH 19/29] try to satisfy psalm --- src/AbstractLapiClient.php | 2 +- tools/coding-standards/psalm/psalm.xml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/AbstractLapiClient.php b/src/AbstractLapiClient.php index 967cbc3..a84f95e 100644 --- a/src/AbstractLapiClient.php +++ b/src/AbstractLapiClient.php @@ -15,7 +15,7 @@ abstract class AbstractLapiClient extends AbstractClient { /** - * @var array|TBouncerConfig + * @var TBouncerConfig */ protected $configs; /** diff --git a/tools/coding-standards/psalm/psalm.xml b/tools/coding-standards/psalm/psalm.xml index 252d95b..98a7615 100644 --- a/tools/coding-standards/psalm/psalm.xml +++ b/tools/coding-standards/psalm/psalm.xml @@ -40,5 +40,10 @@ + + + + + From a6a6b7071b319b17b630ada3a090020aa08cd030 Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Mon, 10 Nov 2025 21:15:24 +0300 Subject: [PATCH 20/29] try to satisfy psalm --- tools/coding-standards/psalm/psalm.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tools/coding-standards/psalm/psalm.xml b/tools/coding-standards/psalm/psalm.xml index 98a7615..5a6bb4d 100644 --- a/tools/coding-standards/psalm/psalm.xml +++ b/tools/coding-standards/psalm/psalm.xml @@ -45,5 +45,15 @@ + + + + + + + + + + From 0d31985abb0f74556a8b01242de1711eda11a5a2 Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Mon, 10 Nov 2025 21:21:49 +0300 Subject: [PATCH 21/29] try to satisfy psalm --- tools/coding-standards/psalm/psalm.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/coding-standards/psalm/psalm.xml b/tools/coding-standards/psalm/psalm.xml index 5a6bb4d..65537b5 100644 --- a/tools/coding-standards/psalm/psalm.xml +++ b/tools/coding-standards/psalm/psalm.xml @@ -46,12 +46,12 @@ - + - + From b697bb4a35e6150b19fc3eb3257eadd8d99955c5 Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Mon, 10 Nov 2025 21:26:42 +0300 Subject: [PATCH 22/29] try to satisfy psalm --- tools/coding-standards/psalm/psalm.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tools/coding-standards/psalm/psalm.xml b/tools/coding-standards/psalm/psalm.xml index 65537b5..14def45 100644 --- a/tools/coding-standards/psalm/psalm.xml +++ b/tools/coding-standards/psalm/psalm.xml @@ -55,5 +55,20 @@ + + + + + + + + + + + + + + + From f94b4b16774282e761c3b2a75722f191eae5de22 Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Mon, 10 Nov 2025 21:30:13 +0300 Subject: [PATCH 23/29] try to satisfy psalm --- tools/coding-standards/psalm/psalm.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/coding-standards/psalm/psalm.xml b/tools/coding-standards/psalm/psalm.xml index 14def45..e435f73 100644 --- a/tools/coding-standards/psalm/psalm.xml +++ b/tools/coding-standards/psalm/psalm.xml @@ -62,7 +62,7 @@ - + From f95c73eeba389abeba8e507b41543244c7259758 Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Mon, 10 Nov 2025 21:37:10 +0300 Subject: [PATCH 24/29] try to satisfy psalm --- tools/coding-standards/psalm/psalm.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tools/coding-standards/psalm/psalm.xml b/tools/coding-standards/psalm/psalm.xml index e435f73..e8ef274 100644 --- a/tools/coding-standards/psalm/psalm.xml +++ b/tools/coding-standards/psalm/psalm.xml @@ -55,6 +55,11 @@ + + + + + @@ -70,5 +75,10 @@ + + + + + From 702ba3a1b19074207e15e03a2ef6a7f294986248 Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Mon, 10 Nov 2025 21:42:46 +0300 Subject: [PATCH 25/29] try to satisfy psalm --- tools/coding-standards/psalm/psalm.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/coding-standards/psalm/psalm.xml b/tools/coding-standards/psalm/psalm.xml index e8ef274..249b623 100644 --- a/tools/coding-standards/psalm/psalm.xml +++ b/tools/coding-standards/psalm/psalm.xml @@ -32,6 +32,7 @@ + @@ -63,6 +64,7 @@ + @@ -75,6 +77,11 @@ + + + + + From fb8586c180d7d2bb93107cc1bc69218dffbd8b44 Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Mon, 10 Nov 2025 21:49:09 +0300 Subject: [PATCH 26/29] try to satisfy psalm --- tools/coding-standards/psalm/psalm.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tools/coding-standards/psalm/psalm.xml b/tools/coding-standards/psalm/psalm.xml index 249b623..557185c 100644 --- a/tools/coding-standards/psalm/psalm.xml +++ b/tools/coding-standards/psalm/psalm.xml @@ -24,6 +24,8 @@ + + @@ -87,5 +89,13 @@ + + + + + + + + From cc31e1a2693fafe3f0f36c4715139b96f1ae8333 Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Mon, 10 Nov 2025 21:54:19 +0300 Subject: [PATCH 27/29] try to satisfy psalm --- tools/coding-standards/psalm/psalm.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/coding-standards/psalm/psalm.xml b/tools/coding-standards/psalm/psalm.xml index 557185c..c89cb61 100644 --- a/tools/coding-standards/psalm/psalm.xml +++ b/tools/coding-standards/psalm/psalm.xml @@ -25,6 +25,7 @@ + From d712b7d69fe8087ec9e27bffb53f0ac474aaac52 Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Mon, 10 Nov 2025 21:55:10 +0300 Subject: [PATCH 28/29] try to satisfy psalm --- src/Storage/TokenStorage.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Storage/TokenStorage.php b/src/Storage/TokenStorage.php index b577d1d..206fdb2 100644 --- a/src/Storage/TokenStorage.php +++ b/src/Storage/TokenStorage.php @@ -24,11 +24,7 @@ final class TokenStorage implements TokenStorageInterface */ private $scenarios; - public function __construct( - WatcherClient $watcher, - CacheItemPoolInterface $cache, - array $scenarios = [] - ) { + public function __construct(WatcherClient $watcher, CacheItemPoolInterface $cache, array $scenarios = []) { $this->watcher = $watcher; $this->cache = $cache; $this->scenarios = $scenarios; From e118d05c8ba2aeaa1bedfbd6cfe9fd6f6d681337 Mon Sep 17 00:00:00 2001 From: Alexander Strizhak Date: Mon, 10 Nov 2025 21:57:38 +0300 Subject: [PATCH 29/29] try to satisfy psalm --- src/Storage/TokenStorage.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Storage/TokenStorage.php b/src/Storage/TokenStorage.php index 206fdb2..3f9d9d9 100644 --- a/src/Storage/TokenStorage.php +++ b/src/Storage/TokenStorage.php @@ -24,7 +24,8 @@ final class TokenStorage implements TokenStorageInterface */ private $scenarios; - public function __construct(WatcherClient $watcher, CacheItemPoolInterface $cache, array $scenarios = []) { + public function __construct(WatcherClient $watcher, CacheItemPoolInterface $cache, array $scenarios = []) + { $this->watcher = $watcher; $this->cache = $cache; $this->scenarios = $scenarios;