Skip to content

Commit

Permalink
Merge pull request #2 from julienloizelet/feat/alert-observer
Browse files Browse the repository at this point in the history
feat(*): Add an observer for a crowdsec_engine_detected_alert event
  • Loading branch information
julienloizelet committed Aug 1, 2023
2 parents 42ba394 + a97d3e9 commit 56048a7
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 14 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/end-to-end-test-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ jobs:
- name: Run cron test
run: ddev playwright test cron

- name: Run add alert test
run: ddev playwright test add-alert
- name: Run alert test
run: ddev playwright test alert

- name: Keep Playwright report
uses: actions/upload-artifact@v3
Expand Down
74 changes: 74 additions & 0 deletions Observer/HandleDetectedAlert.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php declare(strict_types=1);
/**
* CrowdSec_Engine Extension
*
* NOTICE OF LICENSE
*
* This source file is subject to the MIT LICENSE
* that is bundled with this package in the file LICENSE
*
* @category CrowdSec
* @package CrowdSec_Engine
* @copyright Copyright (c) 2023+ CrowdSec
* @author CrowdSec team
* @see https://crowdsec.net CrowdSec Official Website
* @license MIT LICENSE
*
*/

/**
*
* @category CrowdSec
* @package CrowdSec_Engine
* @module Engine
* @author CrowdSec team
*
*/

namespace CrowdSec\Engine\Observer;

use CrowdSec\RemediationEngine\DecisionFactory;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use CrowdSec\Engine\Helper\Event as Helper;

class HandleDetectedAlert implements ObserverInterface
{

/**
* @var Helper
*/
private $helper;

/**
* Constructor.
*
* @param Helper $helper
*/
public function __construct(
Helper $helper
) {
$this->helper = $helper;
}

/**
* Handle detected alert (add alert to queue).
*
* @param Observer $observer
* @return $this
*/
public function execute(Observer $observer): HandleDetectedAlert
{
try {
$alert = $observer->getEvent()->getAlert();
$this->helper->addAlertToQueue($alert);
} catch (\Exception $e) {
$this->helper->getLogger()->error(
'Technical error while handling detected alert',
['message' => $e->getMessage()]
);
}

return $this;
}
}
107 changes: 107 additions & 0 deletions Test/EndToEnd/__tests__/add-alert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { test, expect } from "../fixtures";

import { deleteFileContent, getFileContent } from "../helpers/log";
import { wait } from "../helpers/time";
import { LOG_PATH, blockRegex } from "../helpers/constants";

test.describe("Add alert test", () => {
Expand Down Expand Up @@ -108,3 +109,109 @@ test.describe("Add alert test", () => {
);
});
});

test.describe("Add alert test with event", () => {
test.beforeEach(async () => {
// Clean log file
await deleteFileContent(LOG_PATH);
const logContent = await getFileContent(LOG_PATH);
expect(logContent).toBe("");
});

test("can set default config with event", async ({
adminCrowdSecSecurityConfigPage,
}) => {
await adminCrowdSecSecurityConfigPage.navigateTo();
await adminCrowdSecSecurityConfigPage.setDefaultConfig();
});

test("can add an alert with event", async ({
runActionPage,
homePage,
page,
adminCrowdSecSecurityReportPage,
adminCrowdSecSecurityConfigPage,
}) => {
const scenario = "test-playwright";
await runActionPage.clearCache();
const ip = await runActionPage.getIp();
// Delete all previous events fo IP
await runActionPage.deleteEvents(ip);
// Chek report page
await adminCrowdSecSecurityReportPage.navigateTo();
await expect(page.locator("body")).not.toHaveText(
new RegExp(`testAlertEvent/${scenario}`)
);

await runActionPage.addAlertByEvent(ip, scenario);

let logContent = await getFileContent(LOG_PATH);
expect(logContent).toMatch(
new RegExp(
`Triggered alert will be saved {"ip":"${ip}","scenario":"testAlertEvent/${scenario}"}`
)
);

await homePage.navigateTo(false);
await expect(page.locator("body")).toHaveText(blockRegex);
logContent = await getFileContent(LOG_PATH);
expect(logContent).toMatch(
new RegExp(
`Alert triggered {"ip":"${ip}","scenario":"testAlertEvent/${scenario}"}`
)
);
// Test that multiple add is not possible
await runActionPage.addAlertByEvent(ip, scenario);
logContent = await getFileContent(LOG_PATH);
expect(logContent).toMatch(new RegExp(`Alert already in queue`));

// Clear Cache to be able to access admin
await runActionPage.clearCache();
// Chek report page
await adminCrowdSecSecurityReportPage.navigateTo();
await expect(page.locator("body")).toHaveText(
new RegExp(`testAlertEvent/${scenario}`)
);
// Push signals manually
await wait(10000); // Wait 10 seconds before pushing again
await adminCrowdSecSecurityConfigPage.navigateTo();
await adminCrowdSecSecurityConfigPage.pushSignals();
await expect(page.locator("#signals_push_result")).toContainText(
/0 errors for 1 candidates/,
{
timeout: 30000,
}
);
logContent = await getFileContent(LOG_PATH);
expect(logContent).toMatch(
new RegExp(
`Signals have been pushed {"candidates":1,"pushed":1,"errors":0}`
)
);

// Test that event has been detected as in "black hole"
await deleteFileContent(LOG_PATH);
await runActionPage.addAlertByEvent(ip, scenario);
logContent = await getFileContent(LOG_PATH);
expect(logContent).toMatch(new RegExp(`Event is in black hole`));
expect(logContent).not.toMatch(
new RegExp(
`Alert triggered {"ip":"${ip}","scenario":"testAlertEvent/${scenario}"}`
)
);
});

test("should log error for event", async ({ runActionPage, page }) => {
const badNamedScenario = "test-playwright()";
await runActionPage.clearCache();
const ip = "5.6.7.8";

await runActionPage.addAlertByEvent(ip, badNamedScenario);
await expect(page.locator("body")).toHaveText("dispatched");

const logContent = await getFileContent(LOG_PATH);
expect(logContent).toMatch(
new RegExp(`Scenario name does not conform to the convention`)
);
});
});
2 changes: 2 additions & 0 deletions Test/EndToEnd/helpers/time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const wait = async (ms) =>
new Promise((resolve) => setTimeout(resolve, ms));
9 changes: 9 additions & 0 deletions Test/EndToEnd/pageObjects/runAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,13 @@ export default class RunActionPage {
const result = await this.page.locator("h1").innerText();
expect(result).toMatch(/true|false/);
}

public async addAlertByEvent(ip: string, scenario: string) {
await this.navigateTo(
"add-alert-by-event",
`&ip=${ip}&scenario=${scenario}`
);
const result = await this.page.locator("h1").innerText();
expect(result).toMatch(/dispatched/);
}
}
27 changes: 15 additions & 12 deletions doc/TECHNICAL_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,47 +41,50 @@ _Update_: Since Magento `2.4.6`, it is possible to install `symfony/cache` becau
`web-token/jwt-framework` is `3.1`. But, in order to keep compatibility with `2.4.4` and `2.4.5`, we have to
keep this `crowdsec/magento-symfony-cache` dependency.

## The `addAlertToQueue` helper method

This module is supplied with a `addAlertToQueue` method whose purpose is to send a ban signal for a given IP and a
given scenario.
## The `crowdsec_engine_detected_alert` event

You have to use the `CrowdSec\Engine\Helper\Event` class and pass an array with at least two required indexes:
This module listens to a `crowdsec_engine_detected_alert` event whose purpose is to send a ban signal for a given IP
and a given scenario.

You have to dispatch this event and pass an `alert` array with at least two required indexes:

- `ip`: the IP you want to signal
- `scenario`: the name of the scenario that triggered the alert.

Optionally, you can pass a timestamp (integer) as a value of a `last_event_date` key.

For example, you can have your own class based on the helper:
For example, you can have your own class that will dispatch the `crowdsec_engine_detected_alert` event:

```php
<?php declare(strict_types=1);

use CrowdSec\Engine\Helper\Event as EventHelper;
use Magento\Framework\Event\Manager;

class YourClass
{
private $eventHelper;
/**
* @var Manager
*/
protected $_eventManager;

public function __construct(EventHelper $eventHelper) {
$this->eventHelper = $eventHelper;
public function __construct(Manager $eventManager) {
$this->_eventManager = $eventManager;
}

public function someMethod()
{
/**
* Your method does some logic, and if an IP is detected as suspicious,
* you can use the addAlertToQueue method to signal it.
* you can use the crowdsec_engine_detected_alert event to signal it.
*/
$alert = ['ip' => 'your.suspicious.detected.ip', 'scenario' => 'your/scenario_name'];
$this->eventHelper->addAlertToQueue($alert);
$this->_eventManager->dispatch('crowdsec_engine_detected_alert', ['alert' => $alert]);
/**
* Some other logic
*/
}
}

```

This way, an event will be stored in the `crowdsec_event` table with an `alert_triggered` status and the following `crowdsec_engine_push_signals` executed cron job will push it as a ban signal.
3 changes: 3 additions & 0 deletions etc/events.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,7 @@
<event name="crowdsec_engine_alert_triggered">
<observer name="crowdsec_engine_locally_ban" instance="CrowdSec\Engine\Observer\BanLocally" />
</event>
<event name="crowdsec_engine_detected_alert">
<observer name="crowdsec_engine_handle_detected_alert" instance="CrowdSec\Engine\Observer\HandleDetectedAlert" />
</event>
</config>

0 comments on commit 56048a7

Please sign in to comment.