diff --git a/.github/workflows/end2end.yml b/.github/workflows/end2end.yml
index b0aa349bb..da9c0fb22 100644
--- a/.github/workflows/end2end.yml
+++ b/.github/workflows/end2end.yml
@@ -25,16 +25,15 @@ jobs:
strategy:
matrix:
app:
- - { name: django-mysql, testfile: end2end/django_mysql_test.py }
- - { name: django-mysql-gunicorn, testfile: end2end/django_mysql_gunicorn_test.py }
- - { name: django-postgres-gunicorn, testfile: end2end/django_postgres_gunicorn_test.py }
- - { name: flask-mongo, testfile: end2end/flask_mongo_test.py }
+ - { name: django-mysql, testfile: end2end/django_mysql.py }
+ - { name: django-mysql-gunicorn, testfile: end2end/django_mysql_gunicorn.py }
+ - { name: django-postgres-gunicorn, testfile: end2end/django_postgres_gunicorn.py }
+ - { name: flask-mongo, testfile: end2end/flask_mongo.py }
- { name: flask-mysql, testfile: end2end/flask_mysql_test.py }
- - { name: flask-mysql-uwsgi, testfile: end2end/flask_mysql_uwsgi_test.py }
- - { name: flask-postgres, testfile: end2end/flask_postgres_test.py }
- - { name: flask-postgres-xml, testfile: end2end/flask_postgres_xml_test.py }
- - { name: flask-postgres-xml, testfile: end2end/flask_postgres_xml_lxml_test.py }
- - { name: quart-postgres-uvicorn, testfile: end2end/quart_postgres_uvicorn_test.py }
+ - { name: flask-mysql-uwsgi, testfile: end2end/flask_mysql_uwsgi.py }
+ - { name: flask-postgres, testfile: end2end/flask_postgres.py }
+ - { name: flask-postgres-xml, testfile: end2end/flask_postgres_xml.py }
+ - { name: quart-postgres-uvicorn, testfile: end2end/quart_postgres_uvicorn.py }
- { name: starlette-postgres-uvicorn, testfile: end2end/starlette_postgres_uvicorn_test.py }
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
@@ -65,7 +64,7 @@ jobs:
- name: Start application
working-directory: ./sample-apps/${{ matrix.app.name }}
run: |
- nohup make run > output.log & tail -f output.log & sleep 20
- nohup make runZenDisabled & sleep 20
+ nohup make run > output.log & sleep 1
+ nohup make runZenDisabled & sleep 1
- name: Run end2end tests for application
- run: tail -f ./sample-apps/${{ matrix.app.name }}/output.log & poetry run pytest ./${{ matrix.app.testfile }}
+ run: tail -f ./sample-apps/${{ matrix.app.name }}/output.log & poetry run python ./${{ matrix.app.testfile }}
diff --git a/end2end/django_mysql.py b/end2end/django_mysql.py
new file mode 100644
index 000000000..05ace97b5
--- /dev/null
+++ b/end2end/django_mysql.py
@@ -0,0 +1,80 @@
+from end2end.utils import assert_eq
+from utils import App, Request
+
+django_mysql_app = App(8080)
+
+django_mysql_app.add_payload(
+ "test_sql_injection",
+ safe_request=Request("/app/create", data_type="form", body={"dog_name": "Bobby Tables"}),
+ unsafe_request=Request("/app/create", data_type="form", body={"dog_name": 'Dangerous bobby", 1); -- '}),
+ test_event={
+ "blocked": True,
+ "kind": "sql_injection",
+ 'metadata': {
+ 'dialect': 'mysql',
+ 'sql': 'INSERT INTO sample_app_dogs (dog_name, dog_boss) VALUES ("Dangerous bobby", 1); -- ", "N/A")'
+ },
+ 'operation': 'MySQLdb.Cursor.execute',
+ 'pathToPayload': '.dog_name.[0]',
+ 'payload': '"Dangerous bobby\\", 1); -- "',
+ 'source': "body",
+ }
+)
+
+django_mysql_app.add_payload(
+ "test_shell_injection",
+ safe_request=None, # Don't test safeness
+ unsafe_request=Request("/app/shell/ls -la", "GET"),
+ test_event={
+ "blocked": True,
+ "kind": "shell_injection",
+ 'metadata': {'command': 'ls -la'},
+ 'operation': 'subprocess.Popen',
+ 'pathToPayload': '.[0]',
+ 'payload': '"ls -la"',
+ 'source': "route_params",
+ }
+)
+
+
+def test_heartbeat(app):
+ heartbeat = app.get_heartbeat()
+ route1 = heartbeat["routes"][0]
+ stats = heartbeat["stats"]
+ packages = set(map(lambda x: x["name"], heartbeat["packages"]))
+
+ # Validate routes
+ assert_eq(route1["path"], "/app/create")
+ assert_eq(route1["method"], "POST")
+ assert_eq(route1["hits"], 1)
+
+ assert_eq(route1["apispec"]["body"]["type"], "form-urlencoded")
+ assert_eq(route1["apispec"]["body"]["schema"], {
+ 'type': 'object',
+ 'properties': {
+ 'dog_name': {
+ 'items': {'type': 'string'},
+ 'type': 'array'
+ }
+ }
+ })
+ assert_eq(route1["apispec"]["query"], None)
+ assert_eq(route1["apispec"]["auth"], None)
+
+ # Validate stats
+ assert_eq(stats["requests"]["attacksDetected"]["blocked"], 2)
+ assert_eq(stats["requests"]["attacksDetected"]["total"], 2)
+ # There are 3-4 requests :
+ # 1. is website live request, first request not always counted
+ # 2. /app/create safe
+ # 3. /app/create sql inj
+ # 4. /app/shell/ls -la shell inj
+ total = stats["requests"]["total"]
+ assert 3 <= total <= 4, f"Unexpected amount of total requests {total}"
+
+ # Validate packages
+ assert_eq(packages, {'wrapt', 'asgiref', 'aikido_zen', 'django', 'sqlparse', 'regex', 'mysqlclient'})
+
+
+django_mysql_app.test_all_payloads()
+test_heartbeat(django_mysql_app)
diff --git a/end2end/django_mysql_gunicorn.py b/end2end/django_mysql_gunicorn.py
new file mode 100644
index 000000000..79f37557d
--- /dev/null
+++ b/end2end/django_mysql_gunicorn.py
@@ -0,0 +1,23 @@
+from utils import App, Request
+
+django_mysql_gunicorn_app = App(8082)
+
+django_mysql_gunicorn_app.add_payload(
+ "test_sql_injection",
+ safe_request=Request("/app/create/", data_type="form", body={"dog_name": "Bobby Tables"}),
+ unsafe_request=Request("/app/create/", data_type="form", body={"dog_name": 'Dangerous bobby", 1); -- '}),
+ test_event={
+ "blocked": True,
+ "kind": "sql_injection",
+ 'metadata': {
+ 'dialect': 'mysql',
+ 'sql': 'INSERT INTO sample_app_dogs (dog_name, dog_boss) VALUES ("Dangerous bobby", 1); -- ", "N/A")'
+ },
+ 'operation': 'MySQLdb.Cursor.execute',
+ 'pathToPayload': '.dog_name.[0]',
+ 'payload': '"Dangerous bobby\\", 1); -- "',
+ 'source': "body",
+ }
+)
+
+django_mysql_gunicorn_app.test_all_payloads()
diff --git a/end2end/django_mysql_gunicorn_test.py b/end2end/django_mysql_gunicorn_test.py
deleted file mode 100644
index 8b68f1978..000000000
--- a/end2end/django_mysql_gunicorn_test.py
+++ /dev/null
@@ -1,61 +0,0 @@
-import pytest
-import requests
-import time
-from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type
-
-# e2e tests for django_mysql_gunicorn sample app
-post_url_fw = "http://localhost:8082/app/create/"
-post_url_nofw = "http://localhost:8083/app/create/"
-
-def test_firewall_started_okay():
- events = fetch_events_from_mock("http://localhost:5000")
- started_events = filter_on_event_type(events, "started")
- assert len(started_events) == 1
- validate_started_event(started_events[0], ["gunicorn", "django"])
-
-def test_safe_response_with_firewall():
- dog_name = "Bobby Tables"
- res = requests.post(post_url_fw, data={'dog_name': dog_name})
- assert res.status_code == 200
-
-
-def test_safe_response_without_firewall():
- dog_name = "Bobby Tables"
- res = requests.post(post_url_nofw, data={'dog_name': dog_name})
- assert res.status_code == 200
-
-
-def test_dangerous_response_with_firewall():
- dog_name = 'Dangerous bobby", 1); -- '
- res = requests.post(post_url_fw, data={'dog_name': dog_name})
- assert res.status_code == 500
-
- time.sleep(5) # Wait for attack to be reported
- events = fetch_events_from_mock("http://localhost:5000")
- attacks = filter_on_event_type(events, "detected_attack")
-
- assert len(attacks) == 1
- del attacks[0]["attack"]["stack"]
- assert attacks[0]["attack"] == {
- "blocked": True,
- "kind": "sql_injection",
- 'metadata': {
- 'dialect': 'mysql',
- 'sql': 'INSERT INTO sample_app_dogs (dog_name, dog_boss) VALUES ("Dangerous bobby", 1); -- ", "N/A")'
- },
- 'operation': 'MySQLdb.Cursor.execute',
- 'pathToPayload': '.dog_name.[0]',
- 'payload': '"Dangerous bobby\\", 1); -- "',
- 'source': "body",
- 'user': None
- }
-
- assert attacks[0]["request"]["source"] == "django"
- assert attacks[0]["request"]["route"] == "/app/create"
- assert attacks[0]["request"]["userAgent"] == "python-requests/2.32.3"
-
-def test_dangerous_response_without_firewall():
- dog_name = 'Dangerous bobby", 1); -- '
- res = requests.post(post_url_nofw, data={'dog_name': dog_name})
- assert res.status_code == 200
-
diff --git a/end2end/django_mysql_test.py b/end2end/django_mysql_test.py
deleted file mode 100644
index 4dab9ec02..000000000
--- a/end2end/django_mysql_test.py
+++ /dev/null
@@ -1,114 +0,0 @@
-import time
-import pytest
-import requests
-from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type, validate_heartbeat
-
-# e2e tests for django_mysql sample app
-base_url_fw = "http://localhost:8080/app"
-base_url_nofw = "http://localhost:8081/app"
-
-def test_firewall_started_okay():
- events = fetch_events_from_mock("http://localhost:5000")
- started_events = filter_on_event_type(events, "started")
- assert len(started_events) == 1
- validate_started_event(started_events[0], ["django", "mysqlclient"])
-
-def test_safe_response_with_firewall():
- dog_name = "Bobby Tables"
- res = requests.post(base_url_fw + "/create", data={'dog_name': dog_name})
- assert res.status_code == 200
-
-def test_safe_response_without_firewall():
- dog_name = "Bobby Tables"
- res = requests.post(base_url_nofw + "/create", data={'dog_name': dog_name})
- assert res.status_code == 200
-
-
-def test_dangerous_response_with_firewall():
- dog_name = 'Dangerous bobby", 1); -- '
- res = requests.post(base_url_fw + "/create", data={'dog_name': dog_name})
- assert res.status_code == 500
- time.sleep(5) # Wait for attack to be reported
- events = fetch_events_from_mock("http://localhost:5000")
- attacks = filter_on_event_type(events, "detected_attack")
-
- assert len(attacks) == 1
- del attacks[0]["attack"]["stack"]
- assert attacks[0]["attack"] == {
- "blocked": True,
- "kind": "sql_injection",
- 'metadata': {
- 'dialect': 'mysql',
- 'sql': 'INSERT INTO sample_app_dogs (dog_name, dog_boss) VALUES ("Dangerous bobby", 1); -- ", "N/A")'
- },
- 'operation': 'MySQLdb.Cursor.execute',
- 'pathToPayload': '.dog_name.[0]',
- 'payload': '"Dangerous bobby\\", 1); -- "',
- 'source': "body",
- 'user': None
- }
-
-def test_dangerous_response_with_firewall_shell():
- dog_name = 'Dangerous bobby", 1); -- '
- res = requests.get(base_url_fw + "/shell/ls -la")
- assert res.status_code == 500
- time.sleep(5) # Wait for attack to be reported
- events = fetch_events_from_mock("http://localhost:5000")
- attacks = filter_on_event_type(events, "detected_attack")
-
- assert len(attacks) == 2
- del attacks[0] # Previous attack
- del attacks[0]["attack"]["stack"]
- assert attacks[0]["attack"] == {
- "blocked": True,
- "kind": "shell_injection",
- 'metadata': {'command': 'ls -la'},
- 'operation': 'subprocess.Popen',
- 'pathToPayload': '.[0]',
- 'payload': '"ls -la"',
- 'source': "route_params",
- 'user': None
- }
-
-def test_dangerous_response_without_firewall():
- dog_name = 'Dangerous bobby", 1); -- '
- res = requests.post(base_url_nofw + "/create", data={'dog_name': dog_name})
- assert res.status_code == 200
-
-def test_initial_heartbeat():
- time.sleep(55) # Sleep 5 + 55 seconds for heartbeat
- events = fetch_events_from_mock("http://localhost:5000")
- heartbeat_events = filter_on_event_type(events, "heartbeat")
- assert len(heartbeat_events) == 1
- validate_heartbeat(
- heartbeat_events[0],
- [{
- "apispec": {
- 'body': {
- 'type': 'form-urlencoded',
- 'schema': {
- 'type': 'object',
- 'properties': {
- 'dog_name': {
- 'items': {'type': 'string'},
- 'type': 'array'
- }
- }
- }
- },
- 'query': None,
- 'auth': None
- },
- "hits": 1,
- "hits_delta_since_sync": 0,
- "method": "POST",
- "path": "/app/create"
- }],
- {
- "aborted": 0,
- "attacksDetected": {"blocked": 2, "total": 2},
- "total": 3,
- 'rateLimited': 0
- },
- {'wrapt', 'asgiref', 'aikido_zen', 'django', 'sqlparse', 'regex', 'mysqlclient'}
- )
diff --git a/end2end/django_postgres_gunicorn.py b/end2end/django_postgres_gunicorn.py
new file mode 100644
index 000000000..88f423814
--- /dev/null
+++ b/end2end/django_postgres_gunicorn.py
@@ -0,0 +1,45 @@
+from utils import App, Request
+
+django_postgres_gunicorn_app = App(8100)
+
+django_postgres_gunicorn_app.add_payload(
+ "test_sql_injection",
+ safe_request=Request("/app/create", data_type="form", body={"dog_name": "Bobby Tables"}),
+ unsafe_request=Request("/app/create", data_type="form", body={"dog_name": "Dangerous bobby', TRUE); -- "}),
+ test_event={
+ "blocked": True,
+ "kind": "sql_injection",
+ 'metadata': {
+ 'dialect': "postgres",
+ 'sql': "INSERT INTO sample_app_Dogs (dog_name, is_admin) VALUES ('Dangerous bobby', TRUE); -- ', FALSE)"
+ },
+ 'operation': "psycopg2.Connection.Cursor.execute",
+ 'pathToPayload': '.dog_name.[0]',
+ 'payload': "\"Dangerous bobby', TRUE); -- \"",
+ 'source': "body",
+ }
+)
+
+django_postgres_gunicorn_app.add_payload(
+ "test_sql_injection_via_cookies",
+ safe_request=Request("/app/create/via_cookies", "GET", headers={
+ "Cookie": "dog_name=Safe Dog"
+ }),
+ unsafe_request=Request("/app/create/via_cookies", "GET", headers={
+ "Cookie": "dog_name=Dangerous bobby', TRUE) --; ,2=2"
+ }),
+ test_event={
+ "blocked": True,
+ "kind": "sql_injection",
+ 'metadata': {
+ 'dialect': "postgres",
+ 'sql': "INSERT INTO sample_app_Dogs (dog_name, is_admin) VALUES ('Dangerous bobby', TRUE) --', FALSE)"
+ },
+ 'operation': "psycopg2.Connection.Cursor.execute",
+ 'pathToPayload': '.dog_name',
+ 'payload': "\"Dangerous bobby', TRUE) --\"",
+ 'source': "cookies",
+ }
+)
+
+django_postgres_gunicorn_app.test_all_payloads()
diff --git a/end2end/django_postgres_gunicorn_test.py b/end2end/django_postgres_gunicorn_test.py
deleted file mode 100644
index 9317775c0..000000000
--- a/end2end/django_postgres_gunicorn_test.py
+++ /dev/null
@@ -1,84 +0,0 @@
-import pytest
-import requests
-import time
-from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type, \
- clear_events_from_mock
-
-# e2e tests for django_postgres_gunicorn sample app
-post_url_fw = "http://localhost:8100/app/create"
-post_url_nofw = "http://localhost:8101/app/create"
-
-def test_firewall_started_okay():
- events = fetch_events_from_mock("http://localhost:5000")
- started_events = filter_on_event_type(events, "started")
- assert len(started_events) == 1
- validate_started_event(started_events[0], ["gunicorn", "django", "psycopg2-binary"])
-
-def test_safe_response_with_firewall():
- dog_name = "Bobby Tables"
- res = requests.post(post_url_fw, data={'dog_name': dog_name})
- assert res.status_code == 200
-
-
-def test_safe_response_without_firewall():
- dog_name = "Bobby Tables"
- res = requests.post(post_url_nofw, data={'dog_name': dog_name})
- assert res.status_code == 200
-
-
-def test_dangerous_response_with_firewall():
- clear_events_from_mock("http://localhost:5000")
- dog_name = "Dangerous bobby', TRUE); -- "
- res = requests.post(post_url_fw, data={'dog_name': dog_name})
- assert res.status_code == 500
-
- time.sleep(5) # Wait for attack to be reported
- events = fetch_events_from_mock("http://localhost:5000")
- attacks = filter_on_event_type(events, "detected_attack")
-
- assert len(attacks) == 1
- del attacks[0]["attack"]["stack"]
- assert attacks[0]["attack"] == {
- "blocked": True,
- "kind": "sql_injection",
- 'metadata': {
- 'dialect': "postgres",
- 'sql': "INSERT INTO sample_app_Dogs (dog_name, is_admin) VALUES ('Dangerous bobby', TRUE); -- ', FALSE)"
- },
- 'operation': "psycopg2.Connection.Cursor.execute",
- 'pathToPayload': '.dog_name.[0]',
- 'payload': "\"Dangerous bobby', TRUE); -- \"",
- 'source': "body",
- 'user': None
- }
-
-
-def test_dangerous_response_with_firewall():
- clear_events_from_mock("http://localhost:5000")
- cookie_header = "dog_name=Dangerous bobby', TRUE) --; ,2=2"
- res = requests.get(f"{post_url_fw}/via_cookies", headers={
- "Cookie": cookie_header
- })
- assert res.status_code == 500
-
- time.sleep(5) # Wait for attack to be reported
- events = fetch_events_from_mock("http://localhost:5000")
- attacks = filter_on_event_type(events, "detected_attack")
-
- assert len(attacks) == 1
- assert attacks[0]["attack"]["blocked"]
- assert attacks[0]["attack"]["kind"] == "sql_injection"
- assert attacks[0]["attack"]["metadata"] == {
- 'dialect': "postgres",
- 'sql': "INSERT INTO sample_app_Dogs (dog_name, is_admin) VALUES ('Dangerous bobby', TRUE) --', FALSE)"
- }
- assert attacks[0]["attack"]["pathToPayload"] == ".dog_name"
- assert attacks[0]["attack"]["source"] == "cookies"
- assert attacks[0]["attack"]["payload"] == "\"Dangerous bobby', TRUE) --\""
-
-
-def test_dangerous_response_without_firewall():
- dog_name = "Dangerous bobby', TRUE); -- "
- res = requests.post(post_url_nofw, data={'dog_name': dog_name})
- assert res.status_code == 200
-
diff --git a/end2end/flask_mongo.py b/end2end/flask_mongo.py
new file mode 100644
index 000000000..8669b955a
--- /dev/null
+++ b/end2end/flask_mongo.py
@@ -0,0 +1,32 @@
+import json
+
+from utils import App, Request
+
+flask_mongo_app = App(8094)
+
+def create_test_dog():
+ Request("/create", data_type="form", body=json.dumps({
+ {"dog_name": "bobby", "pswd": "1234"}
+ })).execute(flask_mongo_app.urls["enabled"])
+
+flask_mongo_app.add_payload(
+ "test_nosql_injection",
+ safe_request=Request("/auth", data_type="form", body=json.dumps(
+ {"dog_name": "bobby", "pswd": "1234"}
+ )),
+ unsafe_request=Request("/auth", data_type="form", body=json.dumps(
+ {"dog_name": "bobby", "pswd": { "$ne": ""}}
+ )),
+ test_event={
+ "blocked": True,
+ "kind": "nosql_injection",
+ 'metadata': {'filter': '{"dog_name": "bobby_tables", "pswd": {"$ne": ""}}'},
+ 'operation': "pymongo.collection.Collection.find",
+ 'pathToPayload': ".pswd",
+ 'payload': '{"$ne": ""}',
+ 'source': "body",
+ }
+)
+
+create_test_dog()
+flask_mongo_app.test_all_payloads()
diff --git a/end2end/flask_mysql_uwsgi.py b/end2end/flask_mysql_uwsgi.py
new file mode 100644
index 000000000..45e2c0a06
--- /dev/null
+++ b/end2end/flask_mysql_uwsgi.py
@@ -0,0 +1,23 @@
+from utils import App, Request
+
+flask_mysql_uwsgi_app = App(8088)
+
+flask_mysql_uwsgi_app.add_payload(
+ "test_sql_injection",
+ safe_request=Request("/create", data_type="form", body={"dog_name": "Bobby Tables"}),
+ unsafe_request=Request("/create", data_type="form", body={"dog_name": 'Dangerous bobby", 1); -- '}),
+ test_event={
+ "blocked": True,
+ "kind": "sql_injection",
+ 'metadata': {
+ 'dialect': 'mysql',
+ 'sql': 'INSERT INTO dogs (dog_name, isAdmin) VALUES ("Dangerous bobby", 1); -- ", 0)',
+ },
+ 'operation': 'MySQLdb.Cursor.execute',
+ 'pathToPayload': '.dog_name',
+ 'payload': '"Dangerous bobby\\", 1); -- "',
+ 'source': "body",
+ }
+)
+
+flask_mysql_uwsgi_app.test_all_payloads()
diff --git a/end2end/flask_mysql_uwsgi_test.py b/end2end/flask_mysql_uwsgi_test.py
deleted file mode 100644
index c3ce0c1ae..000000000
--- a/end2end/flask_mysql_uwsgi_test.py
+++ /dev/null
@@ -1,61 +0,0 @@
-import time
-import pytest
-import requests
-from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type
-
-# e2e tests for flask_mysql_uwsgi sample app
-post_url_fw = "http://localhost:8088/create"
-post_url_nofw = "http://localhost:8089/create"
-
-def test_firewall_started_okay():
- events = fetch_events_from_mock("http://localhost:5000")
- started_events = filter_on_event_type(events, "started")
- assert len(started_events) == 1
- validate_started_event(started_events[0], ["flask", "pymysql", "uwsgi"])
-
-def test_safe_response_with_firewall():
- dog_name = "Bobby Tables"
- res = requests.post(post_url_fw, data={'dog_name': dog_name})
- assert res.status_code == 200
-
-
-def test_safe_response_without_firewall():
- dog_name = "Bobby Tables"
- res = requests.post(post_url_nofw, data={'dog_name': dog_name})
- assert res.status_code == 200
-
-
-def test_dangerous_response_with_firewall():
- dog_name = 'Dangerous bobby", 1); -- '
- res = requests.post(post_url_fw, data={'dog_name': dog_name})
- assert res.status_code == 500
-
- time.sleep(5) # Wait for attack to be reported
- events = fetch_events_from_mock("http://localhost:5000")
- attacks = filter_on_event_type(events, "detected_attack")
-
- assert len(attacks) == 1
- del attacks[0]["attack"]["stack"]
- assert attacks[0]["attack"] == {
- "blocked": True,
- "kind": "sql_injection",
- 'metadata': {
- 'dialect': 'mysql',
- 'sql': 'INSERT INTO dogs (dog_name, isAdmin) VALUES ("Dangerous bobby", 1); -- ", 0)'
- },
- 'operation': 'pymysql.Cursor.execute',
- 'pathToPayload': '.dog_name',
- 'payload': '"Dangerous bobby\\", 1); -- "',
- 'source': "body",
- 'user': None
- }
-
- assert attacks[0]["request"]["source"] == "flask"
- assert attacks[0]["request"]["route"] == "/create"
- assert attacks[0]["request"]["userAgent"] == "python-requests/2.32.3"
-
-def test_dangerous_response_without_firewall():
- dog_name = 'Dangerous bobby", 1); -- '
- res = requests.post(post_url_nofw, data={'dog_name': dog_name})
- assert res.status_code == 200
-
diff --git a/end2end/flask_postgres.py b/end2end/flask_postgres.py
new file mode 100644
index 000000000..24175af94
--- /dev/null
+++ b/end2end/flask_postgres.py
@@ -0,0 +1,45 @@
+from utils import App, Request
+
+flask_postgres_app = App(8090)
+
+flask_postgres_app.add_payload(
+ "test_sql_injection",
+ safe_request=Request("/create", data_type="form", body={"dog_name": "Bobby Tables"}),
+ unsafe_request=Request("/create", data_type="form", body={"dog_name": "Dangerous Bobby', TRUE); -- "}),
+ test_event={
+ "blocked": True,
+ "kind": "sql_injection",
+ "metadata": {
+ "dialect": "postgres",
+ "sql": "INSERT INTO dogs (dog_name, isAdmin) VALUES ('Dangerous Bobby', TRUE); -- ', FALSE)"
+ },
+ "operation": "psycopg2.Connection.Cursor.execute",
+ "pathToPayload": '.dog_name',
+ "payload": '"Dangerous Bobby\', TRUE); -- "',
+ "source": "body",
+ }
+)
+
+flask_postgres_app.add_payload(
+ "test_sql_injection_via_cookies",
+ safe_request=Request("/create_with_cookie", "GET", headers={
+ "Cookie": "dog_name=Safe Dog; ,2=2"
+ }),
+ unsafe_request=Request("/create_with_cookie", "GET", headers={
+ "Cookie": "dog_name=Dangerous bobby', TRUE) --; ,2=2"
+ }),
+ test_event={
+ "blocked": True,
+ "kind": "sql_injection",
+ 'metadata': {
+ 'dialect': "postgres",
+ 'sql': "INSERT INTO sample_app_Dogs (dog_name, is_admin) VALUES ('Dangerous bobby', TRUE) --', FALSE)"
+ },
+ 'operation': "psycopg2.Connection.Cursor.execute",
+ 'pathToPayload': '.dog_name',
+ 'payload': "\"Dangerous bobby', TRUE) --\"",
+ 'source': "cookies",
+ }
+)
+
+flask_postgres_app.test_all_payloads()
diff --git a/end2end/flask_postgres_test.py b/end2end/flask_postgres_test.py
deleted file mode 100644
index 72d668da3..000000000
--- a/end2end/flask_postgres_test.py
+++ /dev/null
@@ -1,110 +0,0 @@
-import time
-import pytest
-import json
-import requests
-from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type
-
-# e2e tests for flask_postgres sample app
-post_url_fw = "http://localhost:8090/create"
-post_url_nofw = "http://localhost:8091/create"
-get_url_cookie_fw = "http://localhost:8090/create_with_cookie"
-get_url_cookie_nofw = "http://localhost:8091/create_with_cookie"
-
-def test_firewall_started_okay():
- events = fetch_events_from_mock("http://localhost:5000")
- started_events = filter_on_event_type(events, "started")
- assert len(started_events) == 1
- validate_started_event(started_events[0], ["flask", "psycopg2-binary"])
-
-def test_safe_response_with_firewall():
- dog_name = "Bobby Tables"
- res = requests.post(post_url_fw, data={'dog_name': dog_name})
- assert res.status_code == 200
-
-
-def test_safe_response_without_firewall():
- dog_name = "Bobby Tables"
- res = requests.post(post_url_nofw, data={'dog_name': dog_name})
- assert res.status_code == 200
-
-
-def test_dangerous_response_with_firewall():
- dog_name = "Dangerous Bobby', TRUE); -- "
- res = requests.post(post_url_fw, data={'dog_name': dog_name})
- assert res.status_code == 500
-
-def test_dangerous_response_without_firewall():
- dog_name = "Dangerous Bobby', TRUE); -- "
- res = requests.post(post_url_nofw, data={'dog_name': dog_name})
- assert res.status_code == 200
-
-
-def test_safe_cookie_creation_with_firewall():
- cookies = {
- "dog_name": "Bobby Tables",
- "corrupt_data": ";;;;;;;;;;;;;"
- }
- res = requests.get(get_url_cookie_fw, cookies=cookies)
- assert res.status_code == 200
-
-def test_safe_cookie_creation_without_firewall():
- cookies = {
- "dog_name": "Bobby Tables",
- "corrupt_data": ";;;;;;;;;;;;;"
-
- }
- res = requests.get(get_url_cookie_nofw, cookies=cookies)
- assert res.status_code == 200
-
-
-def test_dangerous_cookie_creation_with_firewall():
- cookies = {
- "dog_name": "Bobby', TRUE) -- ",
- "corrupt_data": ";;;;;;;;;;;;;"
- }
- res = requests.get(get_url_cookie_fw, cookies=cookies)
- assert res.status_code == 500
-
-def test_dangerous_cookie_creation_without_firewall():
- cookies = {
- "dog_name": "Bobby', TRUE) -- ",
- "corrupt_data": ";;;;;;;;;;;;;"
- }
- res = requests.get(get_url_cookie_nofw, cookies=cookies)
- assert res.status_code == 200
-
-def test_attacks_detected():
- time.sleep(5) # Wait for attack to be reported
- events = fetch_events_from_mock("http://localhost:5000")
- attacks = filter_on_event_type(events, "detected_attack")
-
- assert len(attacks) == 2
- del attacks[0]["attack"]["stack"]
- del attacks[1]["attack"]["stack"]
-
- assert attacks[0]["attack"] == {
- "blocked": True,
- "kind": "sql_injection",
- 'metadata': {
- 'dialect': "postgres",
- 'sql': "INSERT INTO dogs (dog_name, isAdmin) VALUES ('Dangerous Bobby', TRUE); -- ', FALSE)"
- },
- 'operation': "psycopg2.Connection.Cursor.execute",
- 'pathToPayload': '.dog_name',
- 'payload': '"Dangerous Bobby\', TRUE); -- "',
- 'source': "body",
- 'user': None
- }
- assert attacks[1]["attack"] == {
- "blocked": True,
- "kind": "sql_injection",
- 'metadata': {
- 'dialect': "postgres",
- 'sql': "INSERT INTO dogs (dog_name, isAdmin) VALUES ('Bobby', TRUE) --', FALSE)"
- },
- 'operation': "psycopg2.Connection.Cursor.execute",
- 'pathToPayload': '.dog_name',
- 'payload': "\"Bobby', TRUE) --\"",
- 'source': "cookies",
- 'user': None
- }
diff --git a/end2end/flask_postgres_xml.py b/end2end/flask_postgres_xml.py
new file mode 100644
index 000000000..f902422cb
--- /dev/null
+++ b/end2end/flask_postgres_xml.py
@@ -0,0 +1,32 @@
+from utils import App, Request
+
+flask_postgres_xml_app = App(8092)
+
+SQL_INJ_ATTACK_EVENT = {
+ "blocked": True,
+ "kind": "sql_injection",
+ "metadata": {
+ "dialect": "postgres",
+ "sql": "INSERT INTO dogs (dog_name, isAdmin) VALUES ('Malicious dog', TRUE); -- ', FALSE)"
+ },
+ "operation": "psycopg2.Connection.Cursor.execute",
+ "pathToPayload": ".dog_name.[0]",
+ "payload": "\"Malicious dog', TRUE); -- \"",
+ "source": "xml",
+}
+
+# Test both xml and lxml
+flask_postgres_xml_app.add_payload(
+ "test_xml_sql_inj",
+ safe_request=Request("/xml_post", data_type="form", body=''),
+ unsafe_request=Request("/xml_post", data_type="form", body=''),
+ test_event=SQL_INJ_ATTACK_EVENT
+)
+flask_postgres_xml_app.add_payload(
+ "test_xml_sql_inj_(lxml)",
+ safe_request=Request("/xml_post_lxml", data_type="form", body=''),
+ unsafe_request=Request("/xml_post_lxml", data_type="form", body=''),
+ test_event=SQL_INJ_ATTACK_EVENT
+)
+
+flask_postgres_xml_app.test_all_payloads()
diff --git a/end2end/flask_postgres_xml_lxml_test.py b/end2end/flask_postgres_xml_lxml_test.py
deleted file mode 100644
index 4f471b86e..000000000
--- a/end2end/flask_postgres_xml_lxml_test.py
+++ /dev/null
@@ -1,51 +0,0 @@
-import time
-import pytest
-import requests
-from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type
-
-# e2e tests for flask_postgres sample app
-post_url_fw = "http://localhost:8092/xml_post_lxml"
-post_url_nofw = "http://localhost:8093/xml_post_lxml"
-
-def test_safe_response_with_firewall():
- xml_data = ''
- res = requests.post(post_url_fw, data=xml_data)
- assert res.status_code == 200
-
-def test_safe_response_without_firewall():
- xml_data = ''
- res = requests.post(post_url_nofw, data=xml_data)
- assert res.status_code == 200
-
-
-def test_dangerous_response_with_firewall():
- xml_data = ''
- res = requests.post(post_url_fw, data=xml_data)
- assert res.status_code == 500
-
- time.sleep(5) # Wait for attack to be reported
- events = fetch_events_from_mock("http://localhost:5000")
- attacks = filter_on_event_type(events, "detected_attack")
-
- assert len(attacks) == 1
- del attacks[0]["attack"]["stack"]
-
- assert attacks[0]["attack"] == {
- "blocked": True,
- "kind": "sql_injection",
- 'metadata': {
- 'dialect': "postgres",
- 'sql': "INSERT INTO dogs (dog_name, isAdmin) VALUES ('Malicious dog', TRUE); -- ', FALSE)"
- },
- 'operation': "psycopg2.Connection.Cursor.execute",
- 'pathToPayload': ".dog_name.[0]",
- 'payload': "\"Malicious dog', TRUE); -- \"",
- 'source': "xml",
- 'user': None
- }
-
-def test_dangerous_response_without_firewall():
- xml_data = ''
- res = requests.post(post_url_nofw, data=xml_data)
- assert res.status_code == 200
-
diff --git a/end2end/flask_postgres_xml_test.py b/end2end/flask_postgres_xml_test.py
deleted file mode 100644
index 1fceb92fe..000000000
--- a/end2end/flask_postgres_xml_test.py
+++ /dev/null
@@ -1,56 +0,0 @@
-import time
-import pytest
-import requests
-from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type
-
-# e2e tests for flask_postgres sample app
-post_url_fw = "http://localhost:8092/xml_post"
-post_url_nofw = "http://localhost:8093/xml_post"
-
-def test_firewall_started_okay():
- events = fetch_events_from_mock("http://localhost:5000")
- started_events = filter_on_event_type(events, "started")
- assert len(started_events) == 1
- validate_started_event(started_events[0], ["flask", "psycopg2-binary", "lxml"])
-
-def test_safe_response_with_firewall():
- xml_data = ''
- res = requests.post(post_url_fw, data=xml_data)
- assert res.status_code == 200
-
-def test_safe_response_without_firewall():
- xml_data = ''
- res = requests.post(post_url_nofw, data=xml_data)
- assert res.status_code == 200
-
-
-def test_dangerous_response_with_firewall():
- xml_data = ''
- res = requests.post(post_url_fw, data=xml_data)
- assert res.status_code == 500
-
- time.sleep(5) # Wait for attack to be reported
- events = fetch_events_from_mock("http://localhost:5000")
- attacks = filter_on_event_type(events, "detected_attack")
-
- assert len(attacks) == 1
- del attacks[0]["attack"]["stack"]
-
- assert attacks[0]["attack"] == {
- "blocked": True,
- "kind": "sql_injection",
- 'metadata': {
- 'dialect': "postgres",
- 'sql': "INSERT INTO dogs (dog_name, isAdmin) VALUES ('Malicious dog', TRUE); -- ', FALSE)"
- },
- 'operation': "psycopg2.Connection.Cursor.execute",
- 'pathToPayload': ".dog_name.[0]",
- 'payload': "\"Malicious dog', TRUE); -- \"",
- 'source': "xml",
- 'user': None
- }
-def test_dangerous_response_without_firewall():
- xml_data = ''
- res = requests.post(post_url_nofw, data=xml_data)
- assert res.status_code == 200
-
diff --git a/end2end/quart_postgres_uvicorn.py b/end2end/quart_postgres_uvicorn.py
new file mode 100644
index 000000000..1263f0348
--- /dev/null
+++ b/end2end/quart_postgres_uvicorn.py
@@ -0,0 +1,24 @@
+from utils import App, Request
+
+quart_postgres_uvicorn_app = App(8096)
+
+quart_postgres_uvicorn_app.add_payload(
+ "test_sql_injection",
+ safe_request=Request("/create", data_type="form", body={"dog_name": "Bobby Tables"}),
+ unsafe_request=Request("/create", data_type="form", body={"dog_name": "Dangerous Bobby', TRUE); -- "}),
+ test_event={
+ "blocked": True,
+ "kind": "sql_injection",
+ "metadata": {
+ 'dialect': "postgres",
+ 'sql': "INSERT INTO dogs (dog_name, isAdmin) VALUES ('Dangerous Bobby', TRUE); -- ', FALSE)"
+ },
+ "operation": "asyncpg.connection.Connection.execute",
+ "pathToPayload": ".dog_name",
+ "payload": "\"Dangerous Bobby', TRUE); -- \"",
+ "source": "body",
+ "user_id": "user123"
+ }
+)
+
+quart_postgres_uvicorn_app.test_all_payloads()
diff --git a/end2end/quart_postgres_uvicorn_test.py b/end2end/quart_postgres_uvicorn_test.py
deleted file mode 100644
index cef39f695..000000000
--- a/end2end/quart_postgres_uvicorn_test.py
+++ /dev/null
@@ -1,58 +0,0 @@
-import time
-import pytest
-import requests
-from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type
-
-# e2e tests for flask_postgres sample app
-post_url_fw = "http://localhost:8096/create"
-post_url_nofw = "http://localhost:8097/create"
-
-def test_firewall_started_okay():
- events = fetch_events_from_mock("http://localhost:5000")
- started_events = filter_on_event_type(events, "started")
- assert len(started_events) == 1
- validate_started_event(started_events[0], None)
-
-def test_safe_response_with_firewall():
- dog_name = "Bobby Tables"
- res = requests.post(post_url_fw, data={'dog_name': dog_name})
- assert res.status_code == 201
-
-def test_safe_response_without_firewall():
- dog_name = "Bobby Tables"
- res = requests.post(post_url_nofw, data={'dog_name': dog_name})
- assert res.status_code == 201
-
-
-def test_dangerous_response_with_firewall():
- dog_name = "Dangerous Bobby', TRUE); -- "
- res = requests.post(post_url_fw, data={'dog_name': dog_name})
- assert res.status_code == 500
-
- time.sleep(5) # Wait for attack to be reported
- events = fetch_events_from_mock("http://localhost:5000")
- attacks = filter_on_event_type(events, "detected_attack")
-
- assert len(attacks) == 1
- del attacks[0]["attack"]["stack"]
- assert attacks[0]["attack"]["blocked"] == True
- assert attacks[0]["attack"]["kind"] == "sql_injection"
- assert attacks[0]["attack"]["metadata"]["sql"] == "INSERT INTO dogs (dog_name, isAdmin) VALUES ('Dangerous Bobby', TRUE); -- ', FALSE)"
- assert attacks[0]["attack"]["metadata"]["dialect"] == "postgres"
- assert attacks[0]["attack"]["operation"] == "asyncpg.connection.Connection.execute"
- assert attacks[0]["attack"]["pathToPayload"] == '.dog_name'
- assert attacks[0]["attack"]["payload"] == "\"Dangerous Bobby', TRUE); -- \""
- assert attacks[0]["attack"]["source"] == "body"
- assert attacks[0]["attack"]["user"]["id"] == "user123"
- assert attacks[0]["attack"]["user"]["name"] == "John Doe"
-
- assert attacks[0]["request"]["source"] == "quart"
- assert attacks[0]["request"]["route"] == "/create"
- assert attacks[0]["request"]["userAgent"] == "python-requests/2.32.3"
-
-
-def test_dangerous_response_without_firewall():
- dog_name = "Dangerous Bobby', TRUE); -- "
- res = requests.post(post_url_nofw, data={'dog_name': dog_name})
- assert res.status_code == 201
-
diff --git a/end2end/utils/__init__.py b/end2end/utils/__init__.py
new file mode 100644
index 000000000..c32becde1
--- /dev/null
+++ b/end2end/utils/__init__.py
@@ -0,0 +1,84 @@
+import time
+import requests
+
+from .event_handler import EventHandler
+from .assert_equals import assert_eq
+from .request import Request
+from .test_payloads_safe_vs_unsafe import test_payloads_safe_vs_unsafe
+
+class App:
+ def __init__(self, port):
+ self.urls = {
+ "enabled": f"http://localhost:{port}",
+ "disabled": f"http://localhost:{port + 1}"
+ }
+ self.payloads = {}
+ self.event_handler = EventHandler()
+ if not wait_until_live(self.urls["enabled"]):
+ raise Exception(self.urls["enabled"] + " is not turning on.")
+ if not wait_until_live(self.urls["disabled"]):
+ raise Exception(self.urls["disabled"] + " is not turning on.")
+
+ def add_payload(self,key, safe_request, unsafe_request=None, test_event=None):
+ self.payloads[key] = {
+ "safe": safe_request,
+ "unsafe": unsafe_request,
+ "test_event": test_event
+ }
+
+ def test_payload(self, key):
+ if key not in self.payloads:
+ raise Exception("Payload " + key + " not found.")
+ payload = self.payloads.get(key)
+
+ self.event_handler.reset()
+ test_payloads_safe_vs_unsafe(payload, self.urls)
+ print("✅ Tested payload: " + key)
+
+ if not payload["test_event"]:
+ return # Finished tests.
+
+ time.sleep(5)
+ attacks = self.event_handler.fetch_attacks()
+ assert_eq(len(attacks), equals=1)
+ if isinstance(payload["test_event"], dict):
+ for k, v in payload["test_event"].items():
+ if k == "user_id": # exemption rule for user ids
+ assert_eq(attacks[0]["attack"]["user"]["id"], v)
+ else:
+ assert_eq(attacks[0]["attack"][k], equals=v)
+ print("✅ Tested accurate event reporting for: " + key)
+
+ def test_all_payloads(self):
+ for key in self.payloads.keys():
+ self.test_payload(key)
+
+ def get_heartbeat(self):
+ print("↺ Fetching latest heartbeat")
+
+ start_time = time.time()
+
+ heartbeats = self.event_handler.fetch_heartbeats()
+ while len(heartbeats) == 0:
+ heartbeats = self.event_handler.fetch_heartbeats()
+ time.sleep(2)
+ assert_eq(len(heartbeats), equals=1)
+
+ delta = time.time() - start_time
+ print(f"✅ Fetched heartbeat, time spent: {delta:.2f}s")
+
+ return heartbeats[0]
+
+def wait_until_live(url):
+ for i in range(10):
+ try:
+ res = requests.get(url, timeout=5)
+ if res.status_code in [200, 404]:
+ print("Server is live: " + url)
+ return True
+ else:
+ print("Status code " + str(res.status_code) + " for " + url)
+ except requests.RequestException as e:
+ print(f"Request failed: {e}")
+ time.sleep(5)
+ return False
diff --git a/end2end/utils/assert_equals.py b/end2end/utils/assert_equals.py
new file mode 100644
index 000000000..b9cc41ad9
--- /dev/null
+++ b/end2end/utils/assert_equals.py
@@ -0,0 +1,8 @@
+def assert_eq(val1, equals=None, val2=None, inside=None):
+ if inside:
+ assert val1 in inside, f"Assertion failed: Expected {val1} in {inside}"
+ else:
+ assert val1 == equals, f"Assertion failed: Expected {equals} != {val1}"
+ if val2 is not None:
+ # Pass along val2 as a val1
+ assert_eq(val2, equals=equals, inside=inside)
diff --git a/end2end/utils/event_handler.py b/end2end/utils/event_handler.py
new file mode 100644
index 000000000..7f6832a8c
--- /dev/null
+++ b/end2end/utils/event_handler.py
@@ -0,0 +1,25 @@
+import time
+import requests
+import json
+
+class EventHandler:
+ def __init__(self, url="http://localhost:5000"):
+ self.url = url
+ def reset(self):
+ print("Resetting stored events on mock server")
+ res = requests.get(self.url + "/mock/reset", timeout=5)
+ time.sleep(1)
+ def fetch_events_from_mock(self):
+ res = requests.get(self.url + "/mock/events", timeout=5)
+ json_events = json.loads(res.content.decode("utf-8"))
+ return json_events
+
+ def fetch_attacks(self):
+ return filter_on_event_type(self.fetch_events_from_mock(), "detected_attack")
+
+ def fetch_heartbeats(self):
+ return filter_on_event_type(self.fetch_events_from_mock(), "heartbeat")
+
+
+def filter_on_event_type(events, type):
+ return [event for event in events if event["type"] == type]
diff --git a/end2end/utils/request.py b/end2end/utils/request.py
new file mode 100644
index 000000000..4a4eff6a8
--- /dev/null
+++ b/end2end/utils/request.py
@@ -0,0 +1,30 @@
+import requests
+
+class Request:
+ def __init__(self, route, method='POST', headers=None, data_type='json', body=None):
+ self.method = method
+ self.route = route
+ self.headers = headers if headers is not None else {}
+ self.data_type = data_type # 'json' or 'form'
+ self.body = body
+
+ def execute(self, base_url):
+ """Execute the request and return the status code."""
+ url = f"{base_url}{self.route}"
+
+ if self.method.upper() == 'POST':
+ if self.data_type == 'json':
+ response = requests.post(url, json=self.body, headers=self.headers)
+ elif self.data_type == 'form':
+ response = requests.post(url, data=self.body, headers=self.headers)
+ else:
+ raise ValueError("Unsupported data type. Use 'json' or 'form'.")
+ elif self.method.upper() == 'GET':
+ response = requests.get(url, headers=self.headers)
+ else:
+ raise ValueError("Unsupported HTTP method. Use 'GET' or 'POST'.")
+
+ return response.status_code
+
+ def __str__(self):
+ return f"Request(method={self.method}, route={self.route}, headers={self.headers}, data_type={self.data_type}, body={self.body})"
diff --git a/end2end/utils/test_payloads_safe_vs_unsafe.py b/end2end/utils/test_payloads_safe_vs_unsafe.py
new file mode 100644
index 000000000..aaa497db7
--- /dev/null
+++ b/end2end/utils/test_payloads_safe_vs_unsafe.py
@@ -0,0 +1,13 @@
+from . import assert_eq
+
+def test_payloads_safe_vs_unsafe(payloads, urls):
+ if payloads["safe"]:
+ print("Safe req to : (1) " + urls["enabled"] + payloads["safe"].route)
+ assert_eq(val1=payloads["safe"].execute(urls["enabled"]), inside=[200, 201])
+ print("Safe req to : (0) " + urls["disabled"] + payloads["safe"].route)
+ assert_eq(val1=payloads["safe"].execute(urls["disabled"]), inside=[200, 201])
+ if payloads["unsafe"]:
+ print("Unsafe req to : (1) " + urls["enabled"] + payloads["unsafe"].route)
+ assert_eq(val1=payloads["unsafe"].execute(urls["enabled"]), inside=[200, 201])
+ print("Unsafe req to : (0) " + urls["disabled"] + payloads["unsafe"].route)
+ assert_eq(val1=payloads["unsafe"].execute(urls["disabled"]), inside=[200, 201])