Skip to content

Aik 3171 : Get context from WSGI to an Agent #10

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 33 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
edc5aeb
Create empty function "parse_query_params"
Jul 17, 2024
00d449c
Start with creation of a context object for Flask
Jul 17, 2024
63773d0
Use werkzeug.wrappers.Request, way easier
Jul 17, 2024
c1b6c83
Create a Context class
Jul 17, 2024
a123a30
Linting
Jul 17, 2024
c9b8f32
Add agent file
Jul 17, 2024
56634d0
Create a protect function which will start this agent
Jul 17, 2024
f61d6db
Make sure werkzeug is installed
Jul 17, 2024
a9b0d7d
Linting
Jul 17, 2024
afaa495
Fix queue bugs
Jul 17, 2024
09719d8
Linting
Jul 17, 2024
8375be4
Create an AikidoThread class with actions that handles web context
Jul 17, 2024
b020c3e
remove "start" funcs, just import agent
Jul 17, 2024
2c900ef
Report sql query and dialect to agent (pymysql sink)
Jul 17, 2024
c4b2560
Update flask sample app to use agent and run threaded
Jul 17, 2024
9f514b9
Remove broken tests
Jul 17, 2024
990d380
Linting
Jul 17, 2024
78d7e2e
Add the try_parse_url_path function
Jul 17, 2024
81e6923
add looks_like_a_secret helper
Jul 17, 2024
ffe3922
Add build_route_from_url with testing
Jul 17, 2024
cf383d7
Linting w/ black
Jul 17, 2024
d5c896d
Create a form instead for flask-mysql app
Jul 17, 2024
ba53bc0
Update flask and Context to use flask-http-middleware
Jul 17, 2024
3a4ec48
Update flask readme to reflect changes made to injection
Jul 17, 2024
150b053
aikido_firewall init update, only protect needs to import sources and…
Jul 17, 2024
948cc74
Remove flask test file (it needs to be rewritten entirely)
Jul 17, 2024
b7c0c3a
Linting
Jul 17, 2024
dbac991
Make interval 60 seconds
Jul 18, 2024
eef7b8e
Remove useless constant
Jul 18, 2024
dcf1273
Remove these actions which should not be passed to agent
Jul 18, 2024
f340824
Create a global variable in context and add set and get
Jul 18, 2024
2d96fc2
Remove comments on PR codecov
Jul 18, 2024
b38ade3
Use threading.local
Jul 18, 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
1 change: 1 addition & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
comment: false
25 changes: 15 additions & 10 deletions aikido_firewall/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@

from dotenv import load_dotenv

# Import sources
import aikido_firewall.sources.django
import aikido_firewall.sources.flask

# Import sinks
import aikido_firewall.sinks.pymysql

# Import middleware
import aikido_firewall.middleware.django

# Import logger
from aikido_firewall.helpers.logging import logger

# Import agent
from aikido_firewall.agent import start_agent

# Load environment variables
load_dotenv()

logger.info("Aikido python firewall started")

def protect():
"""Start Aikido agent"""
# Import sources
import aikido_firewall.sources.django
import aikido_firewall.sources.flask

Check warning on line 22 in aikido_firewall/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/__init__.py#L21-L22

Added lines #L21 - L22 were not covered by tests

# Import sinks
import aikido_firewall.sinks.pymysql

Check warning on line 25 in aikido_firewall/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/__init__.py#L25

Added line #L25 was not covered by tests

logger.info("Aikido python firewall started")
start_agent()

Check warning on line 28 in aikido_firewall/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/__init__.py#L27-L28

Added lines #L27 - L28 were not covered by tests
74 changes: 74 additions & 0 deletions aikido_firewall/agent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""
Aikido agent, this will create a new thread and listen for stuff sent by our sources and sinks
"""

import time
import queue
from threading import Thread
from aikido_firewall.helpers.logging import logger

AGENT_SEC_INTERVAL = 60


class AikidoThread:
"""
Our agent thread
"""

def __init__(self, q):
logger.debug("Agent thread started")
while True:
while not q.empty():
self.process_data(q.get())

Check warning on line 22 in aikido_firewall/agent/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/agent/__init__.py#L19-L22

Added lines #L19 - L22 were not covered by tests
time.sleep(AGENT_SEC_INTERVAL)
self.q = q
self.current_context = None

def process_data(self, item):
"""Will process the data added to the queue"""
action, data = item
logger.debug("Action %s, Data %s", action, data)
if action == "REPORT":
logger.debug("Report")
self.current_context = data

Check warning on line 33 in aikido_firewall/agent/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/agent/__init__.py#L29-L33

Added lines #L29 - L33 were not covered by tests
else:
logger.error("Action `%s` is not defined. (Aikido Agent)", action)

Check warning on line 35 in aikido_firewall/agent/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/agent/__init__.py#L35

Added line #L35 was not covered by tests


# pylint: disable=invalid-name # This variable does change
agent = None


def get_agent():
"""Returns the globally stored agent"""
return agent

Check warning on line 44 in aikido_firewall/agent/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/agent/__init__.py#L44

Added line #L44 was not covered by tests


def start_agent():
"""
Starts a thread to handle incoming/outgoing data
"""
# pylint: disable=global-statement # We need this to be global
global agent

# This creates a queue for Inter-Process Communication
logger.debug("Creating IPC Queue")
q = queue.Queue()

Check warning on line 56 in aikido_firewall/agent/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/agent/__init__.py#L55-L56

Added lines #L55 - L56 were not covered by tests

logger.debug("Starting a new agent thread")
agent_thread = Thread(target=AikidoThread, args=(q,))
agent_thread.start()
agent = Agent(q)

Check warning on line 61 in aikido_firewall/agent/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/agent/__init__.py#L58-L61

Added lines #L58 - L61 were not covered by tests


class Agent:
"""Agent class"""

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

Check warning on line 68 in aikido_firewall/agent/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/agent/__init__.py#L68

Added line #L68 was not covered by tests

def report(self, obj, action):
"""
Report something to the agent
"""
self.q.put((action, obj))

Check warning on line 74 in aikido_firewall/agent/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/agent/__init__.py#L74

Added line #L74 was not covered by tests
50 changes: 50 additions & 0 deletions aikido_firewall/context/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""
Provides all the functionality for contexts
"""

import threading

Check warning on line 5 in aikido_firewall/context/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/context/__init__.py#L5

Added line #L5 was not covered by tests

local = threading.local()

Check warning on line 7 in aikido_firewall/context/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/context/__init__.py#L7

Added line #L7 was not covered by tests


def get_current_context():

Check warning on line 10 in aikido_firewall/context/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/context/__init__.py#L10

Added line #L10 was not covered by tests
"""Returns the current context"""
return local.current_context

Check warning on line 12 in aikido_firewall/context/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/context/__init__.py#L12

Added line #L12 was not covered by tests


class Context:

Check warning on line 15 in aikido_firewall/context/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/context/__init__.py#L15

Added line #L15 was not covered by tests
"""
A context object, it stores everything that is important
for vulnerability detection
"""

def __init__(self, req):
self.method = req.method
self.remote_address = req.remote_addr
self.url = req.url
self.body = req.form
self.headers = req.headers
self.query = req.args
self.cookies = req.cookies
self.source = "flask"

Check warning on line 29 in aikido_firewall/context/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/context/__init__.py#L21-L29

Added lines #L21 - L29 were not covered by tests

def __reduce__(self):
return (

Check warning on line 32 in aikido_firewall/context/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/context/__init__.py#L31-L32

Added lines #L31 - L32 were not covered by tests
self.__class__,
(
self.method,
self.remote_address,
self.url,
self.body,
self.headers,
self.query,
self.cookies,
self.source,
),
)

def set_as_current_context(self):

Check warning on line 46 in aikido_firewall/context/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/context/__init__.py#L46

Added line #L46 was not covered by tests
"""
Set the current context
"""
local.current_context = self

Check warning on line 50 in aikido_firewall/context/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/context/__init__.py#L50

Added line #L50 was not covered by tests
82 changes: 82 additions & 0 deletions aikido_firewall/helpers/build_route_from_url.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""
Module with logic to build a route, i.e. find route params
from a simple URL string
"""

import re
import ipaddress
from aikido_firewall.helpers.looks_like_a_secret import looks_like_a_secret
from aikido_firewall.helpers.try_parse_url_path import try_parse_url_path

UUID_REGEX = re.compile(
r"(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
re.I,
)
NUMBER_REGEX = re.compile(r"^\d+$")
DATE_REGEX = re.compile(r"^\d{4}-\d{2}-\d{2}|\d{2}-\d{2}-\d{4}$")
EMAIL_REGEX = re.compile(
r"^[a-zA-Z0-9.!#$%&\'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
)
HASH_REGEX = re.compile(
r"^(?:[a-f0-9]{32}|[a-f0-9]{40}|[a-f0-9]{64}|[a-f0-9]{128})$", re.I
)
HASH_LENGTHS = [32, 40, 64, 128]


def build_route_from_url(url):
"""
Main helper function which will build the route
from a URL string as input
"""
path = try_parse_url_path(url)

if not path:
return None

route = "/".join(
[replace_url_segment_with_param(segment) for segment in path.split("/")]
)

if route == "/":
return "/"

if route.endswith("/"):
return route[:-1]

return route


def replace_url_segment_with_param(segment):
"""
??????????
"""
if not segment: # Check if segment is empty
return segment # Return the segment as is if it's empty
char_code = ord(segment[0])
starts_with_number = 48 <= char_code <= 57 # ASCII codes for '0' to '9'

if starts_with_number and NUMBER_REGEX.match(segment):
return ":number"

if len(segment) == 36 and UUID_REGEX.match(segment):
return ":uuid"

if starts_with_number and DATE_REGEX.match(segment):
return ":date"

if "@" in segment and EMAIL_REGEX.match(segment):
return ":email"

try:
ipaddress.ip_address(segment)
return ":ip"
except ValueError:
pass

if len(segment) in HASH_LENGTHS and HASH_REGEX.match(segment):
return ":hash"

if looks_like_a_secret(segment):
return ":secret"

return segment
116 changes: 116 additions & 0 deletions aikido_firewall/helpers/build_route_from_url_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from aikido_firewall.helpers.build_route_from_url import build_route_from_url
import pytest
import hashlib


def generate_hash(algorithm):
data = "test"
if algorithm == "md5":
return hashlib.md5(data.encode()).hexdigest()
elif algorithm == "sha1":
return hashlib.sha1(data.encode()).hexdigest()
elif algorithm == "sha256":
return hashlib.sha256(data.encode()).hexdigest()
elif algorithm == "sha512":
return hashlib.sha512(data.encode()).hexdigest()
else:
return None

Check warning on line 17 in aikido_firewall/helpers/build_route_from_url_test.py

View check run for this annotation

Codecov / codecov/patch

aikido_firewall/helpers/build_route_from_url_test.py#L17

Added line #L17 was not covered by tests


def test_invalid_urls():
assert build_route_from_url("") == None
assert build_route_from_url("http") == None


def test_root_urls():
assert build_route_from_url("/") == "/"
assert build_route_from_url("http://localhost/") == "/"


def test_replace_numbers():
assert build_route_from_url("/posts/3") == "/posts/:number"
assert build_route_from_url("http://localhost/posts/3") == "/posts/:number"
assert build_route_from_url("http://localhost/posts/3/") == "/posts/:number"
assert (
build_route_from_url("http://localhost/posts/3/comments/10")
== "/posts/:number/comments/:number"
)
assert (
build_route_from_url("/blog/2023/05/great-article")
== "/blog/:number/:number/great-article"
)


def test_replace_dates():
assert build_route_from_url("/posts/2023-05-01") == "/posts/:date"
assert build_route_from_url("/posts/2023-05-01/") == "/posts/:date"
assert (
build_route_from_url("/posts/2023-05-01/comments/2023-05-01")
== "/posts/:date/comments/:date"
)
assert build_route_from_url("/posts/01-05-2023") == "/posts/:date"


def test_ignore_comma_numbers():
assert build_route_from_url("/posts/3,000") == "/posts/3,000"


def test_ignore_api_version_numbers():
assert build_route_from_url("/v1/posts/3") == "/v1/posts/:number"


def test_replace_uuids():
uuids = [
"d9428888-122b-11e1-b85c-61cd3cbb3210",
"000003e8-2363-21ef-b200-325096b39f47",
"a981a0c2-68b1-35dc-bcfc-296e52ab01ec",
"109156be-c4fb-41ea-b1b4-efe1671c5836",
"90123e1c-7512-523e-bb28-76fab9f2f73d",
"1ef21d2f-1207-6660-8c4f-419efbd44d48",
"017f22e2-79b0-7cc3-98c4-dc0c0c07398f",
"0d8f23a0-697f-83ae-802e-48f3756dd581",
]
for uuid in uuids:
assert build_route_from_url(f"/posts/{uuid}") == "/posts/:uuid"


def test_ignore_invalid_uuids():
assert (
build_route_from_url("/posts/00000000-0000-1000-6000-000000000000")
== "/posts/00000000-0000-1000-6000-000000000000"
)


def test_ignore_strings():
assert build_route_from_url("/posts/abc") == "/posts/abc"


def test_replace_email_addresses():
assert build_route_from_url("/login/john.doe@acme.com") == "/login/:email"
assert build_route_from_url("/login/john.doe+alias@acme.com") == "/login/:email"


def test_replace_ip_addresses():
assert build_route_from_url("/block/1.2.3.4") == "/block/:ip"
assert (
build_route_from_url("/block/2001:2:ffff:ffff:ffff:ffff:ffff:ffff")
== "/block/:ip"
)
assert build_route_from_url("/block/64:ff9a::255.255.255.255") == "/block/:ip"
assert build_route_from_url("/block/100::") == "/block/:ip"
assert build_route_from_url("/block/fec0::") == "/block/:ip"
assert build_route_from_url("/block/227.202.96.196") == "/block/:ip"


def test_replace_hashes():
assert build_route_from_url(f"/files/{generate_hash('md5')}") == "/files/:hash"
assert build_route_from_url(f"/files/{generate_hash('sha1')}") == "/files/:hash"
assert build_route_from_url(f"/files/{generate_hash('sha256')}") == "/files/:hash"
assert build_route_from_url(f"/files/{generate_hash('sha512')}") == "/files/:hash"


def test_replace_secrets():
assert (
build_route_from_url("/confirm/CnJ4DunhYfv2db6T1FRfciRBHtlNKOYrjoz")
== "/confirm/:secret"
)
Loading
Loading