-
Notifications
You must be signed in to change notification settings - Fork 2
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
Changes from 31 commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
edc5aeb
Create empty function "parse_query_params"
00d449c
Start with creation of a context object for Flask
63773d0
Use werkzeug.wrappers.Request, way easier
c1b6c83
Create a Context class
a123a30
Linting
c9b8f32
Add agent file
56634d0
Create a protect function which will start this agent
f61d6db
Make sure werkzeug is installed
a9b0d7d
Linting
afaa495
Fix queue bugs
09719d8
Linting
8375be4
Create an AikidoThread class with actions that handles web context
b020c3e
remove "start" funcs, just import agent
2c900ef
Report sql query and dialect to agent (pymysql sink)
c4b2560
Update flask sample app to use agent and run threaded
9f514b9
Remove broken tests
990d380
Linting
78d7e2e
Add the try_parse_url_path function
81e6923
add looks_like_a_secret helper
ffe3922
Add build_route_from_url with testing
cf383d7
Linting w/ black
d5c896d
Create a form instead for flask-mysql app
ba53bc0
Update flask and Context to use flask-http-middleware
3a4ec48
Update flask readme to reflect changes made to injection
150b053
aikido_firewall init update, only protect needs to import sources and…
948cc74
Remove flask test file (it needs to be rewritten entirely)
b7c0c3a
Linting
dbac991
Make interval 60 seconds
eef7b8e
Remove useless constant
dcf1273
Remove these actions which should not be passed to agent
f340824
Create a global variable in context and add set and get
2d96fc2
Remove comments on PR codecov
b38ade3
Use threading.local
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
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 | ||
else: | ||
logger.error("Action `%s` is not defined. (Aikido Agent)", action) | ||
|
||
|
||
# pylint: disable=invalid-name # This variable does change | ||
agent = None | ||
|
||
|
||
def get_agent(): | ||
"""Returns the globally stored agent""" | ||
return agent | ||
|
||
|
||
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() | ||
|
||
logger.debug("Starting a new agent thread") | ||
agent_thread = Thread(target=AikidoThread, args=(q,)) | ||
agent_thread.start() | ||
agent = Agent(q) | ||
|
||
|
||
class Agent: | ||
"""Agent class""" | ||
|
||
def __init__(self, q): | ||
self.q = q | ||
|
||
def report(self, obj, action): | ||
""" | ||
Report something to the agent | ||
""" | ||
self.q.put((action, obj)) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
""" | ||
Provides all the functionality for contexts | ||
""" | ||
|
||
# pylint: disable=invalid-name # This is not a const | ||
current_context = None | ||
|
||
|
||
def get_current_context(): | ||
"""Returns the current context""" | ||
return current_context | ||
|
||
|
||
class Context: | ||
""" | ||
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" | ||
|
||
def __reduce__(self): | ||
return ( | ||
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): | ||
""" | ||
Set the current context | ||
""" | ||
# pylint: disable=global-statement # We need this so that | ||
# Sinks can access context | ||
global current_context | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is dit dan threadsafe? |
||
current_context = self | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
|
||
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" | ||
) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.