Skip to content

Commit feba796

Browse files
author
Wout Feys
committed
Merge branch 'main' into publish-to-pypi
2 parents 16a3cf8 + ee8d8d0 commit feba796

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+855
-89
lines changed

.github/workflows/end2end.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Run End-2-End Tests
2+
3+
on: [pull_request]
4+
5+
jobs:
6+
test:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- name: Checkout code
10+
uses: actions/checkout@v2
11+
12+
- name: Start django-mysql
13+
working-directory: ./sample-apps/django-mysql
14+
run: |
15+
docker compose -f docker-compose.yml -f docker-compose.benchmark.yml up --build -d
16+
- name: Start django-mysql-gunicorn
17+
working-directory: ./sample-apps/django-mysql-gunicorn
18+
run: |
19+
docker compose -f docker-compose.yml -f docker-compose.benchmark.yml up --build -d
20+
- name: Start flask-mongo
21+
working-directory: ./sample-apps/flask-mongo
22+
run: |
23+
docker compose -f docker-compose.yml -f docker-compose.benchmark.yml up --build -d
24+
- name: Start flask-mysql
25+
working-directory: ./sample-apps/flask-mysql
26+
run: |
27+
docker compose -f docker-compose.yml -f docker-compose.benchmark.yml up --build -d
28+
- name: Start flask-mysql-uwsgi
29+
working-directory: ./sample-apps/flask-mysql-uwsgi
30+
run: |
31+
docker compose -f docker-compose.yml -f docker-compose.benchmark.yml up --build -d
32+
- name: Start flask-postgres
33+
working-directory: ./sample-apps/flask-postgres
34+
run: |
35+
docker compose -f docker-compose.yml -f docker-compose.benchmark.yml up --build -d
36+
37+
- name: Set up Python ${{ matrix.python-version }}
38+
uses: actions/setup-python@v2
39+
with:
40+
python-version: ${{ matrix.python-version }}
41+
42+
- name: Install dependencies
43+
run: |
44+
python -m pip install --upgrade pip
45+
make install
46+
47+
- name: Run end2end tests
48+
run: |
49+
make end2end

Makefile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@ install:
1818

1919
.PHONY: test
2020
test:
21-
poetry run pytest
21+
poetry run pytest aikido_firewall/
22+
23+
.PHONY: end2end
24+
end2end:
25+
poetry run pytest end2end/
2226

2327
.PHONY: cov
2428
cov:
25-
poetry run pytest --cov=aikido_firewall --cov-report=xml
29+
poetry run pytest aikido_firewall/ --cov=aikido_firewall --cov-report=xml
2630

2731
.PHONY: benchmark
2832
e2e:

aikido_firewall/background_process/aikido_background_process.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ def send_to_reporter(self, event_scheduler):
8585
EMPTY_QUEUE_INTERVAL, 1, self.send_to_reporter, (event_scheduler,)
8686
)
8787
while not self.queue.empty():
88-
attack = self.queue.get()
89-
logger.debug("Reporting attack : %s", attack)
90-
self.reporter.on_detected_attack(attack[0], attack[1])
88+
queue_attack_item = self.queue.get()
89+
self.reporter.on_detected_attack(
90+
attack=queue_attack_item[0],
91+
context=queue_attack_item[1],
92+
blocked=queue_attack_item[2],
93+
stack=queue_attack_item[3],
94+
)

aikido_firewall/background_process/commands/attack.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
def process_attack(bg_process, data, conn):
55
"""
66
Adds ATTACK data object to queue
7-
Expected data object : [injection_results, context, blocked_or_not]
7+
Expected data object : [injection_results, context, blocked_or_not, stacktrace]
88
"""
99
bg_process.queue.put(data)
1010
if bg_process.reporter.statistics:

aikido_firewall/background_process/heartbeats.py

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
from aikido_firewall.helpers.logging import logger
6+
from aikido_firewall.helpers.create_interval import create_interval
67

78

89
def send_heartbeats_every_x_secs(reporter, interval_in_secs, event_scheduler):
@@ -18,22 +19,10 @@ def send_heartbeats_every_x_secs(reporter, interval_in_secs, event_scheduler):
1819

1920
logger.debug("Starting heartbeats")
2021

21-
# Start the interval by booting the first settimeout
22-
send_heartbeat_wrapper(reporter, interval_in_secs, event_scheduler, True)
23-
24-
25-
def send_heartbeat_wrapper(rep, interval_in_secs, event_scheduler, boot=False):
26-
"""
27-
Wrapper function for send_heartbeat so we get an interval
28-
"""
29-
event_scheduler.enter(
30-
interval_in_secs,
31-
1,
32-
send_heartbeat_wrapper,
33-
(rep, interval_in_secs, event_scheduler),
22+
# Create an interval for "interval_in_secs" seconds :
23+
create_interval(
24+
event_scheduler=event_scheduler,
25+
interval_in_secs=interval_in_secs,
26+
function=lambda reporter: reporter.send_heartbeat(),
27+
args=(reporter,),
3428
)
35-
if not boot:
36-
# If boot is true it means it's the first time this gets executed, so we
37-
# need to wait for the interval in the event scheduler to finish
38-
logger.debug("Heartbeat...")
39-
rep.send_heartbeat()

aikido_firewall/background_process/heartbeats_test.py

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import pytest
22
from unittest.mock import Mock, patch
3-
from aikido_firewall.background_process.heartbeats import (
4-
send_heartbeats_every_x_secs,
5-
send_heartbeat_wrapper,
6-
)
3+
from aikido_firewall.background_process.heartbeats import send_heartbeats_every_x_secs
74

85

96
def test_send_heartbeats_serverless():
@@ -42,16 +39,3 @@ def test_send_heartbeats_success():
4239

4340
with patch("aikido_firewall.helpers.logging.logger.debug") as mock_debug:
4441
send_heartbeats_every_x_secs(reporter, 5, event_scheduler)
45-
46-
47-
def test_send_heartbeat_wrapper():
48-
reporter = Mock()
49-
reporter.send_heartbeat = Mock()
50-
event_scheduler = Mock()
51-
52-
send_heartbeat_wrapper(reporter, 5, event_scheduler)
53-
54-
reporter.send_heartbeat.assert_called_once()
55-
event_scheduler.enter.assert_called_once_with(
56-
5, 1, send_heartbeat_wrapper, (reporter, 5, event_scheduler)
57-
)

aikido_firewall/background_process/reporter/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@ def report_initial_stats(self):
7070
if should_report_initial_stats:
7171
self.send_heartbeat()
7272

73-
def on_detected_attack(self, attack, context):
73+
def on_detected_attack(self, attack, context, blocked, stack):
7474
"""This will send something to the API when an attack is detected"""
75-
return on_detected_attack(self, attack, context)
75+
return on_detected_attack(self, attack, context, blocked, stack)
7676

7777
def on_start(self):
7878
"""This will send out an Event signalling the start to the server"""

aikido_firewall/background_process/reporter/on_detected_attack.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from aikido_firewall.helpers.get_ua_from_context import get_ua_from_context
88

99

10-
def on_detected_attack(reporter, attack, context):
10+
def on_detected_attack(reporter, attack, context, blocked, stack):
1111
"""
1212
This will send something to the API when an attack is detected
1313
"""
@@ -18,7 +18,8 @@ def on_detected_attack(reporter, attack, context):
1818
attack["user"] = None
1919
attack["payload"] = json.dumps(attack["payload"])[:4096]
2020
attack["metadata"] = limit_length_metadata(attack["metadata"], 4096)
21-
attack["blocked"] = reporter.block
21+
attack["blocked"] = blocked
22+
attack["stack"] = stack
2223

2324
payload = {
2425
"type": "detected_attack",

aikido_firewall/background_process/reporter/on_detected_attack_test.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class Context:
3131
def test_on_detected_attack_no_token(mock_context):
3232
reporter = MagicMock()
3333
reporter.token = None
34-
on_detected_attack(reporter, {}, mock_context)
34+
on_detected_attack(reporter, {}, mock_context, blocked=False, stack=None)
3535
reporter.api.report.assert_not_called()
3636

3737

@@ -42,7 +42,7 @@ def test_on_detected_attack_with_long_payload(mock_reporter, mock_context):
4242
"metadata": {"test": "1"},
4343
}
4444

45-
on_detected_attack(mock_reporter, attack, mock_context)
45+
on_detected_attack(mock_reporter, attack, mock_context, blocked=False, stack=None)
4646
assert len(attack["payload"]) == 4096 # Ensure payload is truncated
4747
mock_reporter.api.report.assert_called_once()
4848

@@ -54,7 +54,7 @@ def test_on_detected_attack_with_long_metadata(mock_reporter, mock_context):
5454
"metadata": {"test": long_metadata},
5555
}
5656

57-
on_detected_attack(mock_reporter, attack, mock_context)
57+
on_detected_attack(mock_reporter, attack, mock_context, blocked=False, stack=None)
5858

5959
assert (
6060
attack["metadata"]["test"] == long_metadata[:4096]
@@ -68,7 +68,7 @@ def test_on_detected_attack_success(mock_reporter, mock_context):
6868
"metadata": {},
6969
}
7070

71-
on_detected_attack(mock_reporter, attack, mock_context)
71+
on_detected_attack(mock_reporter, attack, mock_context, blocked=False, stack=None)
7272
assert mock_reporter.api.report.call_count == 1
7373

7474

@@ -81,6 +81,24 @@ def test_on_detected_attack_exception_handling(mock_reporter, mock_context, capl
8181
# Simulate an exception during the API call
8282
mock_reporter.api.report.side_effect = Exception("API error")
8383

84-
on_detected_attack(mock_reporter, attack, mock_context)
84+
on_detected_attack(mock_reporter, attack, mock_context, blocked=False, stack=None)
8585

8686
assert "Failed to report an attack" in caplog.text
87+
88+
89+
def test_on_detected_attack_with_blocked_and_stack(mock_reporter, mock_context):
90+
attack = {
91+
"payload": {"key": "value"},
92+
"metadata": {},
93+
}
94+
blocked = True
95+
stack = "sample stack trace"
96+
97+
on_detected_attack(
98+
mock_reporter, attack, mock_context, blocked=blocked, stack=stack
99+
)
100+
101+
# Check that the attack dictionary has the blocked and stack fields set
102+
assert attack["blocked"] is True
103+
assert attack["stack"] == stack
104+
assert mock_reporter.api.report.call_count == 1
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Exports create_interval"""
2+
3+
4+
def create_interval(event_scheduler, interval_in_secs, function, args):
5+
"""
6+
This function creates an interval which first runs "function"
7+
after waiting "interval_in_secs" seconds and after that keeps
8+
executing the function every "interval_in_secs" seconds.
9+
"""
10+
# Sleep interval_in_secs seconds before starting the loop :
11+
event_scheduler.enter(
12+
interval_in_secs,
13+
1,
14+
interval_loop,
15+
(event_scheduler, interval_in_secs, function, args),
16+
)
17+
18+
19+
def interval_loop(event_scheduler, interval_in_secs, function, args):
20+
"""
21+
This is the actual interval loop which executes and schedules the function
22+
"""
23+
# Execute function :
24+
function(*args)
25+
# Schedule the execution of the function in interval_in_secs seconds :
26+
event_scheduler.enter(
27+
interval_in_secs,
28+
1,
29+
interval_loop,
30+
(event_scheduler, interval_in_secs, function, args),
31+
)

0 commit comments

Comments
 (0)