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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/php-sdk-development-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ composer-dev*

#log
*.log

/cfssl
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "*"
Expand Down
4 changes: 2 additions & 2 deletions docs/DEVELOPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
128 changes: 128 additions & 0 deletions src/AbstractLapiClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php

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;

/**
* @psalm-import-type TBouncerConfig from Configuration
*/
abstract class AbstractLapiClient extends AbstractClient
{
/**
* @var TBouncerConfig
*/
protected $configs;
/**
* @var array
*/
protected $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 (isset($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. <PHP LAPI client prefix>_<custom suffix>/<vX.Y.Z>.
*/
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;
}
}
170 changes: 170 additions & 0 deletions src/AlertsClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<?php

declare(strict_types=1);

namespace CrowdSec\LapiClient;

use CrowdSec\Common\Client\RequestHandler\RequestHandlerInterface;
use CrowdSec\LapiClient\Storage\TokenStorageInterface;
use Psr\Log\LoggerInterface;

/**
* @psalm-import-type TAlertFull from \CrowdSec\LapiClient\Payload\Alert
* @psalm-import-type TDecision from \CrowdSec\LapiClient\Payload\Alert
* @psalm-import-type TEvent from \CrowdSec\LapiClient\Payload\Alert
* @psalm-import-type TMeta from \CrowdSec\LapiClient\Payload\Alert
* @psalm-import-type TSource from \CrowdSec\LapiClient\Payload\Alert
*
* @psalm-type TSearchQuery = array{
* scope?: string,
* value?: string,
* scenario?: string,
* ip?: string,
* range?: string,
* since?: string,
* until?: string,
* simulated?: boolean,
* has_active_decision?: boolean,
* decision_type?: string,
* limit?: number,
* origin?: string
* }
*
* @psalm-type TDeleteQuery = array{
* scope?: string,
* value?: string,
* scenario?: string,
* ip?: string,
* range?: string,
* since?: string,
* until?: string,
* has_active_decision?: boolean,
* alert_source?: string
* }
*
* @psalm-type TStoredAlert = array{
* capacity: int,
* created_at: string,
* decisions: list<TDecision>,
* events: list<TEvent>,
* events_count: int,
* id: int,
* labels: null|array<string, mixed>,
* leakspeed: string,
* machine_id: string,
* message: string,
* meta: list<TMeta>,
* 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<TAlertFull> $alerts
*
* @return list<string>
*/
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<TStoredAlert>
*/
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";
}
}
Loading
Loading