mirror of
https://github.com/trailofbits/buttercup
synced 2026-06-21 14:11:39 +00:00
crs_api: initial skeleton of crs_api component
This commit is contained in:
+37
@@ -0,0 +1,37 @@
|
||||
example-crs-architecture/
|
||||
|
||||
## Localized environment variables
|
||||
**/.env
|
||||
**/env
|
||||
|
||||
## Generated Kubernetes Resources
|
||||
.k8s
|
||||
|
||||
## Python
|
||||
__pycache__/
|
||||
|
||||
# Devenv
|
||||
# This is another solution for managing dev environments
|
||||
# And these entries are ment to prevent accidentally
|
||||
# committing stuff that others may not be utilizing.
|
||||
.devenv*
|
||||
devenv.local.nix
|
||||
devenv.lock
|
||||
|
||||
# direnv
|
||||
.direnv
|
||||
|
||||
env/
|
||||
pip-wheel-metadata/
|
||||
*.egg-info/
|
||||
__pycache__/
|
||||
.coverage*
|
||||
.idea
|
||||
html/
|
||||
dist/
|
||||
.pdm-*
|
||||
pdm.lock
|
||||
.langchain.db
|
||||
|
||||
*.bak
|
||||
*.orig
|
||||
@@ -0,0 +1,3 @@
|
||||
update-crs-api:
|
||||
@echo "Updating crs-api..."
|
||||
@./scripts/update_crs_api.sh $(shell pwd)/crs_api
|
||||
@@ -0,0 +1,3 @@
|
||||
# generated by fastapi-codegen:
|
||||
# filename: openapi.json
|
||||
# timestamp: 2025-01-13T17:43:03+00:00
|
||||
@@ -0,0 +1,80 @@
|
||||
# generated by fastapi-codegen:
|
||||
# filename: openapi.json
|
||||
# timestamp: 2025-01-13T17:43:03+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class SourceType(Enum):
|
||||
SourceTypeRepo = 'repo'
|
||||
SourceTypeFuzzTooling = 'fuzz-tooling'
|
||||
SourceTypeDiff = 'diff'
|
||||
|
||||
|
||||
class StatusTasksState(BaseModel):
|
||||
canceled: int
|
||||
errored: int
|
||||
pending: int
|
||||
running: int
|
||||
succeeded: int
|
||||
|
||||
|
||||
class TaskType(Enum):
|
||||
TaskTypeFull = 'full'
|
||||
TaskTypeDelta = 'delta'
|
||||
|
||||
|
||||
class VulnDetail(BaseModel):
|
||||
sarif: Dict[str, Any] = Field(
|
||||
...,
|
||||
description='SARIF Report compliant with "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json"',
|
||||
)
|
||||
task_id: str
|
||||
vuln_id: str
|
||||
|
||||
|
||||
class SourceDetail(BaseModel):
|
||||
sha256: str
|
||||
type: SourceType
|
||||
url: str
|
||||
|
||||
|
||||
class StatusState(BaseModel):
|
||||
tasks: StatusTasksState
|
||||
|
||||
|
||||
class TaskDetail(BaseModel):
|
||||
deadline: int
|
||||
source: List[SourceDetail]
|
||||
task_id: str
|
||||
type: TaskType
|
||||
|
||||
|
||||
class VulnBroadcast(BaseModel):
|
||||
message_id: str
|
||||
message_time: int
|
||||
vulns: List[VulnDetail]
|
||||
|
||||
|
||||
class Status(BaseModel):
|
||||
details: Optional[Dict[str, Dict[str, Any]]] = Field(
|
||||
None,
|
||||
description='Keep in mind this endpoint is unauthenticated. Do not place sensitive details in this object. This is optional arbitrary content. This may be logged in error cases, but is mainly for interactive troubleshooting.',
|
||||
)
|
||||
ready: bool
|
||||
state: StatusState
|
||||
version: str = Field(
|
||||
...,
|
||||
description='Version string for verification and reproducibility. Git commit, SemVer, etc...',
|
||||
)
|
||||
|
||||
|
||||
class Task(BaseModel):
|
||||
message_id: str
|
||||
message_time: int
|
||||
tasks: List[TaskDetail]
|
||||
@@ -0,0 +1,88 @@
|
||||
# generated by fastapi-codegen:
|
||||
# filename: openapi.json
|
||||
# timestamp: 2025-01-13T17:43:03+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
from typing import Annotated, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import Depends, FastAPI, status, HTTPException
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
|
||||
from crs_api.models.types import Status, Task, VulnBroadcast
|
||||
|
||||
app = FastAPI(
|
||||
title='Example CRS API',
|
||||
contact={},
|
||||
version='0.1',
|
||||
servers=[{'url': '/'}],
|
||||
)
|
||||
|
||||
# The exposed endpoints must be authenticated using HTTP Basic.
|
||||
# Credentials will be composed of an API key and token which will be used as the username
|
||||
# and password in HTTP Basic Auth.
|
||||
# API keys will be UUIds, tokens will be random alphanumeric strings of at least 32 chars.
|
||||
# Tokens should be stored using Argon2ID.
|
||||
security = HTTPBasic()
|
||||
|
||||
def check_auth(credentials: Annotated[HTTPBasicCredentials, Depends(security)]):
|
||||
"""
|
||||
Reference: https://fastapi.tiangolo.com/advanced/security/http-basic-auth/
|
||||
"""
|
||||
current_username_bytes = credentials.username.encode("utf8")
|
||||
correct_username_bytes = b"api_key_id" # FIXME: Change username as desired
|
||||
is_correct_username = secrets.compare_digest(
|
||||
current_username_bytes, correct_username_bytes
|
||||
)
|
||||
|
||||
current_password_bytes = credentials.password.encode("utf8")
|
||||
correct_password_bytes = (
|
||||
b"api_key_token" # FIXME: Change password as desired and use hash
|
||||
)
|
||||
is_correct_password = secrets.compare_digest(
|
||||
current_password_bytes, correct_password_bytes
|
||||
)
|
||||
|
||||
if not (is_correct_username and is_correct_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
return credentials.username
|
||||
|
||||
|
||||
@app.get('/status/', response_model=Status, tags=['status'])
|
||||
def get_status_() -> Status:
|
||||
"""
|
||||
CRS Status
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@app.post('/v1/sarif/', response_model=str, tags=['sarif'])
|
||||
def post_v1_sarif_(credentials: Annotated[HTTPBasicCredentials, Depends(check_auth)], body: VulnBroadcast) -> str:
|
||||
"""
|
||||
Submit Sarif Broadcast
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@app.post(
|
||||
'/v1/task/', response_model=None, responses={'202': {'model': str}}, tags=['task']
|
||||
)
|
||||
def post_v1_task_(credentials: Annotated[HTTPBasicCredentials, Depends(check_auth)], body: Task) -> Optional[str]:
|
||||
"""
|
||||
Submit Task
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@app.delete('/v1/task/{task_id}/', response_model=str, tags=['task'])
|
||||
def delete_v1_task_task_id_(credentials: Annotated[HTTPBasicCredentials, Depends(check_auth)], task_id: UUID) -> str:
|
||||
"""
|
||||
Cancel Task
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,83 @@
|
||||
--- server.py.orig 2025-01-13 18:41:54
|
||||
+++ server.py 2025-01-13 18:41:52
|
||||
@@ -1,8 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
+import secrets
|
||||
+from typing import Annotated, Optional
|
||||
from uuid import UUID
|
||||
|
||||
-from fastapi import FastAPI
|
||||
+from fastapi import Depends, FastAPI, status, HTTPException
|
||||
+from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
|
||||
from crs_api.models.types import Status, Task, VulnBroadcast
|
||||
|
||||
@@ -13,7 +16,40 @@
|
||||
servers=[{'url': '/'}],
|
||||
)
|
||||
|
||||
+# The exposed endpoints must be authenticated using HTTP Basic.
|
||||
+# Credentials will be composed of an API key and token which will be used as the username
|
||||
+# and password in HTTP Basic Auth.
|
||||
+# API keys will be UUIds, tokens will be random alphanumeric strings of at least 32 chars.
|
||||
+# Tokens should be stored using Argon2ID.
|
||||
+security = HTTPBasic()
|
||||
|
||||
+def check_auth(credentials: Annotated[HTTPBasicCredentials, Depends(security)]):
|
||||
+ """
|
||||
+ Reference: https://fastapi.tiangolo.com/advanced/security/http-basic-auth/
|
||||
+ """
|
||||
+ current_username_bytes = credentials.username.encode("utf8")
|
||||
+ correct_username_bytes = b"api_key_id" # FIXME: Change username as desired
|
||||
+ is_correct_username = secrets.compare_digest(
|
||||
+ current_username_bytes, correct_username_bytes
|
||||
+ )
|
||||
+
|
||||
+ current_password_bytes = credentials.password.encode("utf8")
|
||||
+ correct_password_bytes = (
|
||||
+ b"api_key_token" # FIXME: Change password as desired and use hash
|
||||
+ )
|
||||
+ is_correct_password = secrets.compare_digest(
|
||||
+ current_password_bytes, correct_password_bytes
|
||||
+ )
|
||||
+
|
||||
+ if not (is_correct_username and is_correct_password):
|
||||
+ raise HTTPException(
|
||||
+ status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
+ detail="Incorrect username or password",
|
||||
+ headers={"WWW-Authenticate": "Basic"},
|
||||
+ )
|
||||
+ return credentials.username
|
||||
+
|
||||
+
|
||||
@app.get('/status/', response_model=Status, tags=['status'])
|
||||
def get_status_() -> Status:
|
||||
"""
|
||||
@@ -23,7 +59,7 @@
|
||||
|
||||
|
||||
@app.post('/v1/sarif/', response_model=str, tags=['sarif'])
|
||||
-def post_v1_sarif_(body: VulnBroadcast) -> str:
|
||||
+def post_v1_sarif_(credentials: Annotated[HTTPBasicCredentials, Depends(check_auth)], body: VulnBroadcast) -> str:
|
||||
"""
|
||||
Submit Sarif Broadcast
|
||||
"""
|
||||
@@ -33,7 +69,7 @@
|
||||
@app.post(
|
||||
'/v1/task/', response_model=None, responses={'202': {'model': str}}, tags=['task']
|
||||
)
|
||||
-def post_v1_task_(body: Task) -> Optional[str]:
|
||||
+def post_v1_task_(credentials: Annotated[HTTPBasicCredentials, Depends(check_auth)], body: Task) -> Optional[str]:
|
||||
"""
|
||||
Submit Task
|
||||
"""
|
||||
@@ -41,7 +77,7 @@
|
||||
|
||||
|
||||
@app.delete('/v1/task/{task_id}/', response_model=str, tags=['task'])
|
||||
-def delete_v1_task_task_id_(task_id: UUID) -> str:
|
||||
+def delete_v1_task_task_id_(credentials: Annotated[HTTPBasicCredentials, Depends(check_auth)], task_id: UUID) -> str:
|
||||
"""
|
||||
Cancel Task
|
||||
"""
|
||||
@@ -0,0 +1,34 @@
|
||||
[tool.black]
|
||||
line-length = 80
|
||||
target-version = ['py312']
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools.packages]
|
||||
find = {}
|
||||
|
||||
[project]
|
||||
name = "crs_api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"uvicorn",
|
||||
"fastapi",
|
||||
"urllib3 >= 1.25.3, < 3.0.0",
|
||||
"python_dateutil >= 2.8.2",
|
||||
"pydantic >= 2",
|
||||
"typing-extensions >= 4.7.1",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
tests = [
|
||||
"pytest",
|
||||
"pytest-xdist",
|
||||
"pytest-asyncio",
|
||||
"pytest-cov",
|
||||
"types-python-dateutil",
|
||||
"mypy",
|
||||
"tox",
|
||||
"flake8",
|
||||
]
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash -x
|
||||
|
||||
OUTPUT_DIR=$1
|
||||
|
||||
# Ensure python3.12 is installed
|
||||
PYTHON_CMD="python3.12"
|
||||
if ! command -v python3.12 &> /dev/null
|
||||
then
|
||||
# Test if python refers to python3.12
|
||||
if ! python3 --version | grep "Python 3.12" &> /dev/null
|
||||
then
|
||||
echo "Python 3.12 is not installed"
|
||||
exit 1
|
||||
fi
|
||||
PYTHON_CMD="python3"
|
||||
fi
|
||||
|
||||
TEMPDIR=$(mktemp -d)
|
||||
cd "$TEMPDIR" || exit 1
|
||||
|
||||
git clone git@github.com:aixcc-finals/example-crs-architecture.git
|
||||
|
||||
$PYTHON_CMD -m venv venv
|
||||
. ./venv/bin/activate
|
||||
|
||||
pip install git+https://github.com/trail-of-forks/fastapi-code-generator
|
||||
pip install uvicorn
|
||||
pip install fastapi
|
||||
|
||||
curl -o openapi.json -X POST https://converter.swagger.io/api/convert -H "Content-Type: application/json" --data-binary "@example-crs-architecture/docs/api/crs-swagger-v0.1.json"
|
||||
|
||||
fastapi-codegen --input openapi.json --output $OUTPUT_DIR/crs_api
|
||||
|
||||
# replace `from .models` with `from crs_api.models`
|
||||
mv $OUTPUT_DIR/crs_api/main.py $OUTPUT_DIR/crs_api/server.py
|
||||
|
||||
# replace `from .models` with `from crs_api.models` in server.py
|
||||
sed -i.bak 's/from .models/from crs_api.models/g' $OUTPUT_DIR/crs_api/server.py
|
||||
# remove `from uuid import UUID` and replace UUID with str
|
||||
sed -i.bak '/from uuid import UUID/d' $OUTPUT_DIR/crs_api/models/types.py
|
||||
sed -i.bak 's/UUID/str/g' $OUTPUT_DIR/crs_api/models/types.py
|
||||
|
||||
# apply HTTP auth patch
|
||||
cd "$OUTPUT_DIR/crs_api" || exit 1
|
||||
patch < "$OUTPUT_DIR/crs_api_auth.patch"
|
||||
|
||||
rm -rf "$TEMPDIR"
|
||||
Reference in New Issue
Block a user