Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(*): Add methods to build and push usage metrics #16

Merged
merged 1 commit into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ As far as possible, we try to adhere to [Symfony guidelines](https://symfony.com

---

## [4.0.0](https://github.com/crowdsecurity/php-lapi-client/releases/tag/v4.0.0) - 202?-??-??
[_Compare with previous release_](https://github.com/crowdsecurity/php-lapi-client/compare/v3.3.2...HEAD)

**This release is not yet published**

### Added

- Add `pushUsageMetrics` method to `Bouncer` class

### Changed

- *Breaking change*: Move configuration classes to `CrowdSec\LapiClient\Configuration` namespace

---

## [3.3.2](https://github.com/crowdsecurity/php-lapi-client/releases/tag/v3.3.2) - 2024-10-21
[_Compare with previous release_](https://github.com/crowdsecurity/php-lapi-client/compare/v3.3.1...v3.3.2)

Expand Down
30 changes: 30 additions & 0 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
- [Get AppSec decision](#get-appsec-decision)
- [Command usage](#command-usage-2)
- [Example](#example-1)
- [Push usage metrics](#push-usage-metrics)
- [Command usage](#command-usage-3)
- [Example](#example-2)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

Expand All @@ -60,6 +63,7 @@ This client allows you to interact with the CrowdSec Local API (LAPI).
- Retrieve decisions stream list
- Retrieve decisions for some filter
- Retrieve AppSec decision
- Push usage metrics
- Overridable request handler (`curl` by default, `file_get_contents` also available)


Expand Down Expand Up @@ -151,6 +155,18 @@ The `$rawBody` parameter is optional and must be used if the forwarded request c

Please see the [CrowdSec AppSec documentation](https://docs.crowdsec.net/docs/appsec/intro/) for more details.

##### Push usage metrics

To push usage metrics, you can do the following call:

```php
$client->pushUsageMetrics($usageMetrics);
```

The `$usageMetrics` parameter is an array containing the usage metrics to push. Please see the [CrowdSec LAPI documentation](https://crowdsecurity.github.io/api_doc/index.html?urls.primaryName=LAPI#/bouncers/postUsageMetrics) for more details.

We provide a `buildUsageMetrics` method to help you build the `$usageMetrics` array.


## Bouncer client configurations

Expand Down Expand Up @@ -521,3 +537,17 @@ php tests/scripts/bouncer/appsec-decision.php <BOUNCER_KEY> <HEADERS_JSON> <APPS
```bash
php tests/scripts/bouncer/appsec-decision.php 'KWurslwIaE2aZSZjYU9mQAWTFb6AHiPTFNTsYTZvoAU' '{"X-Crowdsec-Appsec-Ip":"1.2.3.4","X-Crowdsec-Appsec-Uri":"/login","X-Crowdsec-Appsec-Host":"example.com","X-Crowdsec-Appsec-Verb":"POST","X-Crowdsec-Appsec-User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0"}' http://crowdsec:7422 'class.module.classLoader.resources.'
```

### Push usage metrics

#### Command usage

```bash
php tests/scripts/bouncer/build-and-push-metrics.php <METRICS_JSON> <BOUNCER_KEY> <LAPI_URL>
```

#### Example

```bash
php tests/scripts/bouncer/build-and-push-metrics.php '{"name":"TEST BOUNCER","type":"crowdsec-test-php-bouncer","version":"v0.0.0","items":[{"name":"dropped","value":12,"unit":"request","labels":{"origin":"CAPI"}}],"meta":{"window_size_seconds":900,"utc_now_timestamp":12}}' 92d3de1dde6d354b771d63035cf5ef83 https://crowdsec:8080
```
99 changes: 99 additions & 0 deletions src/Bouncer.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use CrowdSec\Common\Client\ClientException as CommonClientException;
use CrowdSec\Common\Client\RequestHandler\RequestHandlerInterface;
use CrowdSec\Common\Client\TimeoutException as CommonTimeoutException;
use CrowdSec\LapiClient\Configuration\Bouncer as Configuration;
use Psr\Log\LoggerInterface;
use Symfony\Component\Config\Definition\Processor;

Expand Down Expand Up @@ -45,6 +46,79 @@ public function __construct(
parent::__construct($this->configs, $requestHandler, $logger);
}

/**
* Helper to create well formatted metrics array.
*
* @param array $properties
* Array containing metrics properties
* $properties = [
* 'name' => (string) Bouncer name
* 'type' => (string) Bouncer type (crowdsec-php-bouncer)
* 'last_pull' => (integer) last pull timestamp,
* '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 array $meta
* Array containing meta data
* $meta = [
* 'window_size_seconds' => (integer) Window size in seconds
* 'utc_now_timestamp' => (integer) Current timestamp
* ];
* @param array[] $items
* Array of items. Each item is an array too.
* $items = [
* [
* 'name' => (string) Name of the metric
* 'value' => (integer) Value of the metric
* 'type' => (string) Type of the metric
* 'labels' => (array) Labels of the metric
* 'labels' = [
* 'key' => (string) Tag key
* 'value' => (string) Tag value
* ],
* ],
* ...
* ]
*
* @throws ClientException
*/
public function buildUsageMetrics(array $properties, array $meta, array $items = [[]]): array
{
$finalProperties = [
'name' => $properties['name'] ?? '',
'type' => $properties['type'] ?? Constants::METRICS_TYPE,
'version' => $properties['version'] ?? '',
'feature_flags' => $properties['feature_flags'] ?? [],
'utc_startup_timestamp' => $properties['utc_startup_timestamp'] ?? 0,
];
$lastPull = $properties['last_pull'] ?? 0;
$os = $properties['os'] ?? $this->getOs();
if ($lastPull) {
$finalProperties['last_pull'] = $lastPull;
}
if (!empty($os['name']) && !empty($os['version'])) {
$finalProperties['os'] = $os;
}
$meta = [
'window_size_seconds' => $meta['window_size_seconds'] ?? 0,
'utc_now_timestamp' => $meta['utc_now_timestamp'] ?? time(),
];

try {
$metrics = new Metrics($finalProperties, $meta, $items);
} catch (\Exception $e) {
throw new ClientException('Something went wrong while creating metrics: ' . $e->getMessage());
}

return $metrics->toArray();
}

/**
* Process a call to AppSec component.
*
Expand Down Expand Up @@ -99,6 +173,23 @@ public function getStreamDecisions(
);
}

/**
* Push usage metrics to LAPI.
*
* @see https://crowdsecurity.github.io/api_doc/index.html?urls.primaryName=LAPI#/Remediation%20component/usage-metrics
*
* @throws ClientException
* @codeCoverageIgnore
*/
public function pushUsageMetrics(array $usageMetrics): array
{
return $this->manageRequest(
'POST',
Constants::METRICS_ENDPOINT,
$usageMetrics
);
}

private function cleanHeadersForLog(array $headers): array
{
$cleanedHeaders = $headers;
Expand Down Expand Up @@ -136,6 +227,14 @@ private function formatUserAgent(array $configs = []): string
return Constants::USER_AGENT_PREFIX . $userAgentSuffix . '/' . $userAgentVersion;
}

private function getOs(): array
{
return [
'name' => php_uname('s'),
'version' => php_uname('v'),
];
}

/**
* Make a request to the AppSec component of LAPI.
*
Expand Down
41 changes: 21 additions & 20 deletions src/Configuration.php → src/Configuration/Bouncer.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

declare(strict_types=1);

namespace CrowdSec\LapiClient;
namespace CrowdSec\LapiClient\Configuration;

use CrowdSec\Common\Configuration\AbstractConfiguration;
use CrowdSec\LapiClient\Constants;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
Expand All @@ -19,7 +20,7 @@
* @copyright Copyright (c) 2022+ CrowdSec
* @license MIT License
*/
class Configuration extends AbstractConfiguration
class Bouncer extends AbstractConfiguration
{
/** @var array<string> The list of each configuration tree key */
protected $keys = [
Expand Down Expand Up @@ -79,6 +80,24 @@ public function getConfigTreeBuilder(): TreeBuilder
return $treeBuilder;
}

/**
* AppSec settings.
*
* @param NodeDefinition|ArrayNodeDefinition $rootNode
*
* @return void
*
* @throws \InvalidArgumentException
*/
private function addAppSecNodes($rootNode)
{
$rootNode->children()
->scalarNode('appsec_url')->cannotBeEmpty()->defaultValue(Constants::DEFAULT_APPSEC_URL)->end()
->integerNode('appsec_timeout_ms')->defaultValue(Constants::APPSEC_TIMEOUT_MS)->end()
->integerNode('appsec_connect_timeout_ms')->defaultValue(Constants::APPSEC_CONNECT_TIMEOUT_MS)->end()
->end();
}

/**
* LAPI connection settings.
*
Expand Down Expand Up @@ -117,24 +136,6 @@ private function addConnectionNodes($rootNode)
->end();
}

/**
* AppSec settings.
*
* @param NodeDefinition|ArrayNodeDefinition $rootNode
*
* @return void
*
* @throws \InvalidArgumentException
*/
private function addAppSecNodes($rootNode)
{
$rootNode->children()
->scalarNode('appsec_url')->cannotBeEmpty()->defaultValue(Constants::DEFAULT_APPSEC_URL)->end()
->integerNode('appsec_timeout_ms')->defaultValue(Constants::APPSEC_TIMEOUT_MS)->end()
->integerNode('appsec_connect_timeout_ms')->defaultValue(Constants::APPSEC_CONNECT_TIMEOUT_MS)->end()
->end();
}

/**
* Conditional validation.
*
Expand Down
65 changes: 65 additions & 0 deletions src/Configuration/Metrics.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace CrowdSec\LapiClient\Configuration;

use CrowdSec\Common\Configuration\AbstractConfiguration;
use CrowdSec\LapiClient\Constants;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;

/**
* The LAPI client metrics properties configuration.
*
* @author CrowdSec team
*
* @see https://crowdsec.net CrowdSec Official Website
*
* @copyright Copyright (c) 2022+ CrowdSec
* @license MIT License
*/
class Metrics extends AbstractConfiguration
{
/** @var array<string> The list of each configuration tree key */
protected $keys = [
'name',
'type',
'last_pull',
'version',
'os',
'feature_flags',
'utc_startup_timestamp',
];

/**
* @throws \InvalidArgumentException
* @throws \RuntimeException
*/
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('metricsConfig');
/** @var ArrayNodeDefinition $rootNode */
$rootNode = $treeBuilder->getRootNode();
$rootNode->children()
->scalarNode('name')->isRequired()->cannotBeEmpty()->end()
->scalarNode('type')->isRequired()->defaultValue(Constants::METRICS_TYPE)->end()
->integerNode('last_pull')->end()
->scalarNode('version')->isRequired()->cannotBeEmpty()->end()
->integerNode('utc_startup_timestamp')->isRequired()->min(0)->end()
->arrayNode('os')
->children()
->scalarNode('name')->isRequired()->cannotBeEmpty()->end()
->scalarNode('version')->isRequired()->cannotBeEmpty()->end()
->end()
->end()
->arrayNode('feature_flags')
->scalarPrototype()->end()
->defaultValue([])
->end()
->end()
;

return $treeBuilder;
}
}
Loading
Loading