crs_api: initial skeleton of crs_api component

This commit is contained in:
Riccardo Schirone
2025-01-13 18:43:31 +01:00
parent 96e92962d7
commit 9601bb52e8
9 changed files with 375 additions and 0 deletions
+37
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
update-crs-api:
@echo "Updating crs-api..."
@./scripts/update_crs_api.sh $(shell pwd)/crs_api
View File
+3
View File
@@ -0,0 +1,3 @@
# generated by fastapi-codegen:
# filename: openapi.json
# timestamp: 2025-01-13T17:43:03+00:00
+80
View File
@@ -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]
+88
View File
@@ -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
+83
View File
@@ -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
"""
+34
View File
@@ -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",
]
+47
View File
@@ -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"