Skip to content

Commit 007719c

Browse files
committed
Add sample app and e2e test for FastAPI 0.70.0
Pins FastAPI 0.70.0 (starlette 0.16.0) to reproduce and verify route discovery on older starlette versions.
1 parent bf3acdb commit 007719c

4 files changed

Lines changed: 141 additions & 0 deletions

File tree

end2end/fastapi_old_test.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import time
2+
import requests
3+
from .server.check_events_from_mock import (
4+
fetch_events_from_mock,
5+
filter_on_event_type,
6+
validate_started_event,
7+
validate_heartbeat,
8+
)
9+
10+
base_url_fw = "http://localhost:8114"
11+
base_url_nofw = "http://localhost:8115"
12+
13+
14+
def test_firewall_started_okay():
15+
events = fetch_events_from_mock("http://localhost:5000")
16+
started_events = filter_on_event_type(events, "started")
17+
assert len(started_events) == 1
18+
validate_started_event(started_events[0], None)
19+
20+
21+
def test_homepage_with_firewall():
22+
res = requests.get(f"{base_url_fw}/")
23+
assert res.status_code == 200
24+
25+
26+
def test_homepage_without_firewall():
27+
res = requests.get(f"{base_url_nofw}/")
28+
assert res.status_code == 200
29+
30+
31+
def test_sync_route_with_firewall():
32+
res = requests.get(f"{base_url_fw}/sync_route")
33+
assert res.status_code == 200
34+
35+
36+
def test_sync_route_without_firewall():
37+
res = requests.get(f"{base_url_nofw}/sync_route")
38+
assert res.status_code == 200
39+
40+
41+
def test_shell_injection_with_firewall():
42+
res = requests.get(f"{base_url_fw}/shell/ls;echo test")
43+
assert res.status_code == 500
44+
45+
time.sleep(5)
46+
events = fetch_events_from_mock("http://localhost:5000")
47+
attacks = filter_on_event_type(events, "detected_attack")
48+
49+
assert len(attacks) == 1
50+
assert attacks[0]["attack"]["blocked"] == True
51+
assert attacks[0]["attack"]["kind"] == "shell_injection"
52+
assert attacks[0]["attack"]["operation"] == "subprocess.Popen"
53+
assert attacks[0]["attack"]["source"] == "routeParams"
54+
55+
56+
def test_shell_injection_without_firewall():
57+
res = requests.get(f"{base_url_nofw}/shell/ls")
58+
assert res.status_code == 200
59+
60+
61+
def test_initial_heartbeat():
62+
time.sleep(55)
63+
events = fetch_events_from_mock("http://localhost:5000")
64+
heartbeat_events = filter_on_event_type(events, "heartbeat")
65+
assert len(heartbeat_events) == 1
66+
routes = heartbeat_events[0]["routes"]
67+
route_paths = sorted([r["path"] for r in routes])
68+
assert "/" in route_paths
69+
assert "/sync_route" in route_paths

sample-apps/fastapi-old/Makefile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
include ../common.mk
2+
3+
PORT = 8114
4+
PORT_DISABLED = 8115
5+
6+
.PHONY: run
7+
run: install
8+
@echo "Running sample app fastapi-old with Zen on port $(PORT)"
9+
$(AIKIDO_ENV_COMMON) \
10+
poetry run uvicorn app:app --host 0.0.0.0 --port $(PORT) --workers 4
11+
12+
.PHONY: runZenDisabled
13+
runZenDisabled: install
14+
@echo "Running sample app fastapi-old without Zen on port $(PORT_DISABLED)"
15+
$(AIKIDO_ENV_DISABLED) \
16+
poetry run uvicorn app:app --host 0.0.0.0 --port $(PORT_DISABLED) --workers 4

sample-apps/fastapi-old/app.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import aikido_zen
2+
aikido_zen.protect()
3+
4+
import subprocess
5+
from fastapi import FastAPI, Request
6+
from fastapi.responses import JSONResponse
7+
from aikido_zen.middleware import AikidoFastAPIMiddleware
8+
9+
app = FastAPI()
10+
app.add_middleware(AikidoFastAPIMiddleware)
11+
12+
13+
@app.get("/")
14+
async def homepage():
15+
return JSONResponse({"message": "Hello from old FastAPI"})
16+
17+
18+
@app.get("/users/{user_id}")
19+
async def get_user(user_id: int):
20+
return JSONResponse({"user_id": user_id, "name": "Test User"})
21+
22+
23+
@app.post("/users")
24+
async def create_user():
25+
return JSONResponse({"message": "User created"}, status_code=201)
26+
27+
28+
@app.get("/sync_route")
29+
def sync_route():
30+
return JSONResponse({"message": "This is a sync route"})
31+
32+
33+
@app.get("/shell/{command}")
34+
async def execute_command(command: str):
35+
result = subprocess.run(command, capture_output=True, text=True, shell=True)
36+
return JSONResponse({"output": result.stdout})
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[project]
2+
name = "fastapi-old"
3+
version = "0.1.0"
4+
description = "Test app with FastAPI 0.70.0 (starlette 0.16.0) to verify route discovery"
5+
requires-python = ">3.9.1,<4.0"
6+
dependencies = [
7+
"aikido_zen",
8+
"fastapi (>=0.70.0,<0.71.0)",
9+
"uvicorn (>=0.34.0,<0.35.0)",
10+
]
11+
12+
[build-system]
13+
requires = ["poetry-core>=2.0.0,<3.0.0"]
14+
build-backend = "poetry.core.masonry.api"
15+
16+
[tool.poetry]
17+
package-mode = false
18+
19+
[tool.poetry.dependencies]
20+
aikido_zen = { path = "../../", develop = true }

0 commit comments

Comments
 (0)