Skip to content

PR : Add reporting for API #25

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

Merged
merged 84 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from 82 commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
a36fc35
Create a basic new Agent class
Jul 24, 2024
5062dfe
Create the super class for API's
Jul 24, 2024
3b403fc
create an HTTP API class
Jul 24, 2024
4ba6703
Install and import requests
Jul 24, 2024
211421c
Linting
Jul 24, 2024
f81f549
Rename to timeout_in_sec
Jul 24, 2024
adbfa83
Send data using requests.post
Jul 24, 2024
1e88fcc
Create a class that encapsulates the token
Jul 24, 2024
465c746
Give back api response in http_api file
Jul 24, 2024
87a71ae
update the way to_api_response() function works
Jul 24, 2024
9689a29
Add tests for token class
Jul 24, 2024
9eecfc0
Add tests for ReportingApi class
Jul 24, 2024
434d830
Move token file into a helper module with new function get_token_from…
Jul 24, 2024
d25f1c1
Create a should_block helper function
Jul 24, 2024
58e0201
Linting
Jul 24, 2024
3ff2bf3
Create agent and add get_token_from_env func
Jul 24, 2024
24f1869
add send_heartbeat and some auxiliary funcs
Jul 24, 2024
20bf490
self.token needs to be defined for these 2 funcs
Jul 24, 2024
c6705e5
Use self.timeout_in_sec
Jul 24, 2024
41c5b05
add on_start function
Jul 24, 2024
db277ed
Update get_agent_info function
Jul 24, 2024
cd6a24f
Linting
Jul 24, 2024
164eae3
Add some stuff already to on_detected_attack
Jul 24, 2024
e0e5fef
Add new helper function limit_length_metadata
Jul 24, 2024
b04031b
Use new helper function in agent.py to limit metadata
Jul 24, 2024
78f6599
Change interval to 10 minutes
Jul 25, 2024
2305285
Merge remote-tracking branch 'origin/AIK-3167' into AIK-3210
Jul 25, 2024
d3914f4
Renaming agent to reporter and fixing left scars of merge
Jul 25, 2024
747d825
Validate token using Token class in Reporter
Jul 29, 2024
1bceaba
Use .timestamp() on datetime
Jul 29, 2024
0d3b1d3
Bugfix, should be json= instead of data=
Jul 29, 2024
ca25686
Only parse if status code is 200, use json.loads and debug if error
Jul 29, 2024
0a47b8f
Fix bug, unixtime needs to be sent in ms
Jul 29, 2024
dbfa324
Use a PKG_VERSION const
Jul 29, 2024
2bce5f6
Create a get_ip() function
Jul 29, 2024
66fcb28
Update blocking in the updateConfig function
Jul 29, 2024
224a864
Linting
Jul 29, 2024
177c9fc
Add a heartbeats.py file with heartbeat logic (interval, event sched)
Jul 29, 2024
f5e6a3b
Add missing data (empty) to the request
Jul 29, 2024
19cda6b
linting for if statement in token.py
Jul 29, 2024
1c8a006
Mistake with 2x token wrappin
Jul 29, 2024
00548d4
Merge branch 'AIK-3199' into AIK-3210
Jul 29, 2024
633e182
Merge branch 'AIK-3199' into AIK-3210
Jul 29, 2024
1ac32d0
Flask shouldn't reload, messes up our bg job
Jul 29, 2024
739f18a
Create a reporter with a local api url
Jul 29, 2024
caece69
Using an array as data for send_data function is pointless
Jul 29, 2024
bbc0b1e
Add aikido as host in docker
Jul 29, 2024
1b9fd0f
Run on_start when initiating a Reporter
Jul 29, 2024
0ffe068
Check if result is successfull of API
Jul 29, 2024
1e869ea
Fix bug with not detecting the timeout error
Jul 29, 2024
058ab89
Forgotten import + report attacks when emptying queue
Jul 29, 2024
3d2143e
Make context object picklable
Jul 29, 2024
0121d6a
Update the on_detected_attack function
Jul 29, 2024
bd98fe7
Linting
Jul 29, 2024
c5bce7d
Extract extra nformation and user-agent from headers
Jul 29, 2024
49e41f0
Allow polling for config, and do it to see if we should block
Jul 30, 2024
ae4c585
Report route to aikido server
Jul 30, 2024
cd3bfab
Fix bug with hung connection
Jul 30, 2024
771c949
Create get_subdomains_from_url helper function
Jul 30, 2024
f42108d
Add subdomains to context
Jul 30, 2024
c55eaaa
Create a scheduler in reporting thread, send out heartbeats using it
Jul 30, 2024
f5b5c3e
Fix bug with blocking not being updated by using hasattr
Jul 30, 2024
706ca0e
Merge branch 'AIK-3167' into AIK-3210
Jul 30, 2024
1bfa13a
Make sure the url is a string
Jul 30, 2024
b47aea8
Merge branch 'main' into AIK-3210
bitterpanda63 Jul 30, 2024
9877d00
Don't raise an exception at HTTPApi, just logger.error it
Jul 30, 2024
310f538
Rename to context_contains_sql_injection
Jul 30, 2024
5c50abf
Rename s to event_scheduler in heartbeats.py
Jul 30, 2024
a83ab91
Fix merge issues, add back lost code
Jul 30, 2024
f76ea56
Replac with contains_injection and add blocking code to mysqlclient
Jul 30, 2024
6a92882
Linting
Jul 30, 2024
87eb9f9
Heartbeat every 10 minutes
Jul 30, 2024
9523f51
Report sec interval needs to be near instant (5 seconds)
Jul 30, 2024
bde23b8
Split all helper functions for reporter up into files
Jul 30, 2024
d6a5d32
Add tests for get_ua_from_context
Jul 30, 2024
89dd55c
Add tests for get_unixtime_ms
Jul 30, 2024
7202571
Add tests for get_ip
Jul 30, 2024
cdf840d
Bugfix where http_api would still try and parse if an exception occured
Jul 30, 2024
6d8bae6
Add testing for http_api
Jul 30, 2024
fce720b
Add comments clarifying timeout_in_sec variable
Jul 30, 2024
a359dc3
Add tests for heartbeats.py
Jul 30, 2024
4e6dbba
REPORT_SEC_INTERVAL to EMPTY_QUEUE_INTERVAL
Jul 30, 2024
b2203d2
Update aikido_firewall/background_process/aikido_background_process.py
willem-delbare Jul 30, 2024
34ef86f
Create a new Reporter using named arguments for clarity
Jul 30, 2024
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 aikido_firewall/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from dotenv import load_dotenv

# Constants
PKG_VERSION = "0.0.1"

# Import logger
from aikido_firewall.helpers.logging import logger
Expand Down
42 changes: 32 additions & 10 deletions aikido_firewall/background_process/aikido_background_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@
import os
import time
import signal
import sched
from threading import Thread
from queue import Queue
from aikido_firewall.helpers.logging import logger
from aikido_firewall.background_process.reporter import Reporter
from aikido_firewall.helpers.should_block import should_block
from aikido_firewall.helpers.token import get_token_from_env
from aikido_firewall.background_process.api.http_api import ReportingApiHTTP

REPORT_SEC_INTERVAL = 600 # 10 minutes

EMPTY_QUEUE_INTERVAL = 5 # 5 seconds


class AikidoBackgroundProcess:
Expand All @@ -31,6 +37,7 @@ def __init__(self, address, key):
pid = os.getpid()
os.kill(pid, signal.SIGTERM) # Kill this subprocess
self.queue = Queue()
self.reporter = None
# Start reporting thread :
Thread(target=self.reporting_thread).start()

Expand All @@ -52,21 +59,36 @@ def __init__(self, address, key):
conn.close()
pid = os.getpid()
os.kill(pid, signal.SIGTERM) # Kill this subprocess
elif data[0] == "READ_PROPERTY":
if hasattr(self.reporter, data[1]):
conn.send(self.reporter.__dict__[data[1]])

def reporting_thread(self):
"""Reporting thread"""
logger.debug("Started reporting thread")
while True:
self.send_to_reporter()
time.sleep(REPORT_SEC_INTERVAL)
event_scheduler = sched.scheduler(
time.time, time.sleep
) # Create an event scheduler
self.send_to_reporter(event_scheduler)

api = ReportingApiHTTP("http://app.local.aikido.io/")
# We need to pass along the scheduler so that the heartbeat also gets sent
self.reporter = Reporter(
should_block(), api, get_token_from_env(), False, event_scheduler
)

event_scheduler.run()

def send_to_reporter(self):
def send_to_reporter(self, event_scheduler):
"""
Reports the found data to an Aikido server
"""
items_to_report = []
# Add back to event scheduler in EMPTY_QUEUE_INTERVAL secs :
event_scheduler.enter(
EMPTY_QUEUE_INTERVAL, 1, self.send_to_reporter, (event_scheduler,)
)
logger.debug("Checking queue")
while not self.queue.empty():
items_to_report.append(self.queue.get())
logger.debug("Reporting to aikido server")
logger.critical("Items to report : %s", items_to_report)
# Currently not making API calls
attack = self.queue.get()
logger.debug("Reporting attack : %s", attack)
self.reporter.on_detected_attack(attack[0], attack[1])
28 changes: 28 additions & 0 deletions aikido_firewall/background_process/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""
init.py file for api/ folder. Includes abstract class ReportingApi
"""

import json
from aikido_firewall.helpers.logging import logger


class ReportingApi:
"""This is the super class for the reporting API's"""

def to_api_response(self, res):
"""Converts results into an Api response obj"""
status = res.status_code
if status == 429:
return {"success": False, "error": "rate_limited"}
elif status == 401:
return {"success": False, "error": "invalid_token"}
elif status == 200:
try:
return json.loads(res.text)
except Exception as e:
logger.debug(e)
logger.debug(res.text)
return {"success": False, "error": "unknown_error"}

def report(self, token, event, timeout_in_sec):
"""Report event to aikido server"""
34 changes: 34 additions & 0 deletions aikido_firewall/background_process/api/http_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""
Exports the HTTP API class
"""

import requests
from aikido_firewall.background_process.api import ReportingApi
from aikido_firewall.helpers.logging import logger


class ReportingApiHTTP(ReportingApi):
"""HTTP Reporting API"""

def __init__(self, reporting_url):
self.reporting_url = reporting_url

def report(self, token, event, timeout_in_sec):
try:
res = requests.post(
self.reporting_url + "api/runtime/events",
json=event,
timeout=timeout_in_sec,
headers=get_headers(token),
)
except requests.exceptions.ConnectionError:
return {"success": False, "error": "timeout"}
except Exception as e:
logger.error(e)
return {"success": False, "error": "unknown"}
return self.to_api_response(res)


def get_headers(token):
"""Returns headers"""
return {"Content-Type": "application/json", "Authorization": str(token)}
72 changes: 72 additions & 0 deletions aikido_firewall/background_process/api/http_api_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import pytest
import requests
from unittest.mock import patch
from aikido_firewall.background_process.api.http_api import (
ReportingApiHTTP,
) # Replace with the actual module name

# Sample event data for testing
sample_event = {"event_type": "test_event", "data": {"key": "value"}}


def test_report_data_401_code(monkeypatch):
# Create an instance of ReportingApiHTTP
api = ReportingApiHTTP("http://mocked-url.com/")

# Mock the requests.post method to return a successful response
class MockResponse:
def __init__(self, json_data, status_code):
self.json_data = json_data
self.status_code = status_code

def json(self):
return self.json_data

def mock_post(url, json, timeout, headers):
return MockResponse({"success": False}, 401)

monkeypatch.setattr(requests, "post", mock_post)

# Call the report method
response = api.report("mocked_token", sample_event, 5)

# Assert the response
assert response == {"success": False, "error": "invalid_token"}


def test_report_connection_error(monkeypatch):
# Create an instance of ReportingApiHTTP
api = ReportingApiHTTP("http://mocked-url.com/")

# Mock the requests.post method to raise a ConnectionError
monkeypatch.setattr(
requests,
"post",
lambda *args, **kwargs: (_ for _ in ()).throw(
requests.exceptions.ConnectionError
),
)

# Call the report method
response = api.report("mocked_token", sample_event, 5)

# Assert the response
assert response == {"success": False, "error": "timeout"}


def test_report_other_exception(monkeypatch):
# Create an instance of ReportingApiHTTP
api = ReportingApiHTTP("http://mocked-url.com/")

# Mock the requests.post method to raise a generic exception
def mock_post(url, json, timeout, headers):
raise Exception("Some error occurred")

monkeypatch.setattr(requests, "post", mock_post)

# Call the report method
response = api.report("mocked_token", sample_event, 5)

# Assert that the response is None (or however you want to handle it)
assert response["error"] is "unknown"
assert not response["success"]
44 changes: 44 additions & 0 deletions aikido_firewall/background_process/api/init_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import pytest
from aikido_firewall.background_process.api import ReportingApi

# Test ReportingApi Class :
from requests.models import Response


@pytest.fixture
def reporting_api():
return ReportingApi()


def test_to_api_response_rate_limited(reporting_api):
res = Response()
res.status_code = 429
assert reporting_api.to_api_response(res) == {
"success": False,
"error": "rate_limited",
}


def test_to_api_response_invalid_token(reporting_api):
res = Response()
res.status_code = 401
assert reporting_api.to_api_response(res) == {
"success": False,
"error": "invalid_token",
}


def test_to_api_response_unknown_error(reporting_api):
res = Response()
res.status_code = 500 # Simulating an unknown error status code
assert reporting_api.to_api_response(res) == {
"success": False,
"error": "unknown_error",
}


def test_to_api_response_valid_json(reporting_api):
res = Response()
res.status_code = 200
res._content = b'{"key": "value"}' # Simulating valid JSON response
assert reporting_api.to_api_response(res) == {"key": "value"}
12 changes: 12 additions & 0 deletions aikido_firewall/background_process/comms.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,15 @@ def target(address, key, data_array):
t.start()
# This joins the thread for 3 seconds, afterwards the thread is forced to close (daemon=True)
t.join(timeout=3)

def poll_config(self, prop):
"""
This will poll the config from the Background Process
"""
conn = con.Client(self.address, authkey=self.key)
conn.send(("READ_PROPERTY", prop))
prop_value = conn.recv()
conn.send(("CLOSE", {}))
conn.close()
logger.debug("Received property %s as %s", prop, prop_value)
return prop_value
37 changes: 37 additions & 0 deletions aikido_firewall/background_process/heartbeats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
The code to send out a heartbeat is in here
"""

from aikido_firewall.helpers.logging import logger


def send_heartbeats_every_x_secs(reporter, interval_in_secs, event_scheduler):
"""
Start sending out heartbeats every x seconds
"""
if reporter.serverless:
logger.debug("Running in serverless environment, not starting heartbeats")
return
if not reporter.token:
logger.debug("No token provided, not starting heartbeats")
return

logger.debug("Starting heartbeats")

event_scheduler.enter(
0, 1, send_heartbeat_wrapper, (reporter, interval_in_secs, event_scheduler)
)


def send_heartbeat_wrapper(rep, interval_in_secs, event_scheduler):
"""
Wrapper function for send_heartbeat so we get an interval
"""
event_scheduler.enter(
interval_in_secs,
1,
send_heartbeat_wrapper,
(rep, interval_in_secs, event_scheduler),
)
logger.debug("Heartbeat...")
rep.send_heartbeat()
62 changes: 62 additions & 0 deletions aikido_firewall/background_process/heartbeats_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import pytest
from unittest.mock import Mock, patch
from aikido_firewall.background_process.heartbeats import (
send_heartbeats_every_x_secs,
send_heartbeat_wrapper,
)


def test_send_heartbeats_serverless():
reporter = Mock()
reporter.serverless = True
reporter.token = "mocked_token"
event_scheduler = Mock()

with patch("aikido_firewall.helpers.logging.logger.debug") as mock_debug:
send_heartbeats_every_x_secs(reporter, 5, event_scheduler)

mock_debug.assert_called_once_with(
"Running in serverless environment, not starting heartbeats"
)
event_scheduler.enter.assert_not_called()


def test_send_heartbeats_no_token():
reporter = Mock()
reporter.serverless = False
reporter.token = None
event_scheduler = Mock()

with patch("aikido_firewall.helpers.logging.logger.debug") as mock_debug:
send_heartbeats_every_x_secs(reporter, 5, event_scheduler)

mock_debug.assert_called_once_with("No token provided, not starting heartbeats")
event_scheduler.enter.assert_not_called()


def test_send_heartbeats_success():
reporter = Mock()
reporter.serverless = False
reporter.token = "mocked_token"
event_scheduler = Mock()

with patch("aikido_firewall.helpers.logging.logger.debug") as mock_debug:
send_heartbeats_every_x_secs(reporter, 5, event_scheduler)

mock_debug.assert_called_with("Starting heartbeats")
event_scheduler.enter.assert_called_once_with(
0, 1, send_heartbeat_wrapper, (reporter, 5, event_scheduler)
)


def test_send_heartbeat_wrapper():
reporter = Mock()
reporter.send_heartbeat = Mock()
event_scheduler = Mock()

send_heartbeat_wrapper(reporter, 5, event_scheduler)

reporter.send_heartbeat.assert_called_once()
event_scheduler.enter.assert_called_once_with(
5, 1, send_heartbeat_wrapper, (reporter, 5, event_scheduler)
)
Loading
Loading