-
Notifications
You must be signed in to change notification settings - Fork 68
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #19 from OWASP/validate-oas-scheme-in-api
FEATURE: validate OAS file and add rate limiting options for API
- Loading branch information
Showing
11 changed files
with
429 additions
and
388 deletions.
There are no files selected for viewing
This file contains 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 |
---|---|---|
@@ -1,101 +1,87 @@ | ||
from fastapi import status, Request, Response | ||
from json import loads as json_loads | ||
from yaml import SafeLoader, load as yaml_loads | ||
from .config import app, task_queue, task_timeout, auth_secret_key | ||
from .jobs import scan_api | ||
from .models import CreateScanModel | ||
from ..logger import create_logger | ||
from offat.api.config import app, task_queue, task_timeout, auth_secret_key | ||
from offat.api.jobs import scan_api | ||
from offat.api.models import CreateScanModel | ||
from offat.logger import create_logger | ||
from os import uname, environ | ||
|
||
|
||
logger = create_logger(__name__) | ||
logger.info(f'Secret Key: {auth_secret_key}') | ||
|
||
|
||
if uname().sysname == 'Darwin' and environ.get('OBJC_DISABLE_INITIALIZE_FORK_SAFETY') != 'YES': | ||
logger.warning('Mac Users might need to configure OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES in env\nVisit StackOverFlow link for more info: https://stackoverflow.com/questions/50168647/multiprocessing-causes-python-to-crash-and-gives-an-error-may-have-been-in-progr') | ||
|
||
|
||
@app.get('/', status_code=status.HTTP_200_OK) | ||
async def root(): | ||
return { | ||
"name":"OFFAT API", | ||
"project":"https://github.com/dmdhrumilmistry/offat", | ||
"license":"https://github.com/dmdhrumilmistry/offat/blob/main/LICENSE", | ||
"name": "OFFAT API", | ||
"project": "https://github.com/dmdhrumilmistry/offat", | ||
"license": "https://github.com/dmdhrumilmistry/offat/blob/main/LICENSE", | ||
} | ||
|
||
|
||
@app.post('/api/v1/scan', status_code=status.HTTP_201_CREATED) | ||
async def add_scan_task(scan_data: CreateScanModel, request:Request ,response: Response): | ||
async def add_scan_task(scan_data: CreateScanModel, request: Request, response: Response): | ||
# for auth | ||
client_ip = request.client.host | ||
secret_key = request.headers.get('SECRET-KEY', None) | ||
if secret_key != auth_secret_key: | ||
# return 404 for better endpoint security | ||
response.status_code = status.HTTP_401_UNAUTHORIZED | ||
logger.warning(f'INTRUSION: {client_ip} tried to create a new scan job') | ||
return {"message":"Unauthorized"} | ||
|
||
openapi_doc = scan_data.openAPI | ||
file_data_type = scan_data.type | ||
logger.warning( | ||
f'INTRUSION: {client_ip} tried to create a new scan job') | ||
return {"message": "Unauthorized"} | ||
|
||
msg = { | ||
"msg":"Scan Task Created", | ||
"msg": "Scan Task Created", | ||
"job_id": None | ||
} | ||
create_task = True | ||
|
||
match file_data_type: | ||
case 'json': | ||
openapi_doc = json_loads(openapi_doc) | ||
case 'yaml': | ||
openapi_doc = yaml_loads(openapi_doc, SafeLoader) | ||
case _: | ||
response.status_code = status.HTTP_400_BAD_REQUEST | ||
msg = { | ||
"msg":"Invalid Request Data" | ||
} | ||
create_task = False | ||
|
||
if create_task: | ||
job = task_queue.enqueue(scan_api, openapi_doc, job_timeout=task_timeout) | ||
msg['job_id'] = job.id | ||
|
||
logger.info(f'SUCCESS: {client_ip} created new scan job - {job.id}') | ||
else: | ||
logger.error(f'FAILED: {client_ip} tried creating new scan job but it failed due to unknown file data type') | ||
|
||
job = task_queue.enqueue(scan_api, scan_data, job_timeout=task_timeout) | ||
msg['job_id'] = job.id | ||
|
||
logger.info(f'SUCCESS: {client_ip} created new scan job - {job.id}') | ||
|
||
return msg | ||
|
||
|
||
@app.get('/api/v1/scan/{job_id}/result') | ||
async def get_scan_task_result(job_id:str, request: Request, response:Response): | ||
async def get_scan_task_result(job_id: str, request: Request, response: Response): | ||
# for auth | ||
client_ip = request.client.host | ||
secret_key = request.headers.get('SECRET-KEY', None) | ||
if secret_key != auth_secret_key: | ||
# return 404 for better endpoint security | ||
response.status_code = status.HTTP_401_UNAUTHORIZED | ||
logger.warning(f'INTRUSION: {client_ip} tried to access {job_id} job scan results') | ||
return {"message":"Unauthorized"} | ||
|
||
scan_results = task_queue.fetch_job(job_id=job_id) | ||
logger.warning( | ||
f'INTRUSION: {client_ip} tried to access {job_id} job scan results') | ||
return {"message": "Unauthorized"} | ||
|
||
scan_results_job = task_queue.fetch_job(job_id=job_id) | ||
|
||
logger.info(f'SUCCESS: {client_ip} accessed {job_id} job scan results') | ||
|
||
msg = { | ||
'msg':'Task Remaining or Invalid Job Id', | ||
'results': None, | ||
} | ||
msg = 'Task Remaining or Invalid Job Id' | ||
results = None | ||
response.status_code = status.HTTP_202_ACCEPTED | ||
|
||
if scan_results and scan_results.is_finished: | ||
msg = { | ||
'msg':'Task Completed', | ||
'results': scan_results.result, | ||
} | ||
response.status_code = status.HTTP_200_OK | ||
if scan_results_job and scan_results_job.is_started: | ||
msg = 'Job In Progress' | ||
|
||
elif scan_results_job and scan_results_job.is_finished: | ||
msg = 'Task Completed' | ||
results = scan_results_job.result | ||
response.status_code = status.HTTP_200_OK | ||
|
||
elif scan_results and scan_results.is_failed: | ||
msg = { | ||
'msg':'Task Failed. Try Creating Task Again.', | ||
'results': None, | ||
} | ||
elif scan_results_job and scan_results_job.is_failed: | ||
msg = 'Task Failed. Try Creating Task Again.' | ||
response.status_code = status.HTTP_200_OK | ||
|
||
return msg | ||
msg = { | ||
'msg': msg, | ||
'results': results, | ||
} | ||
return msg |
This file contains 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 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 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 |
---|---|---|
@@ -1,19 +1,28 @@ | ||
from ..tester.tester_utils import generate_and_run_tests | ||
from ..openapi import OpenAPIParser | ||
from traceback import print_exception | ||
from offat.api.models import CreateScanModel | ||
from offat.tester.tester_utils import generate_and_run_tests | ||
from offat.openapi import OpenAPIParser | ||
from offat.logger import create_logger | ||
|
||
|
||
def scan_api(open_api:dict): | ||
# TODO: validate `open_api` str against openapi specs. | ||
api_parser = OpenAPIParser(fpath=None,spec=open_api) | ||
logger = create_logger(__name__) | ||
|
||
# TODO: accept commented options from API | ||
results = generate_and_run_tests( | ||
api_parser=api_parser, | ||
# regex_pattern=args.path_regex_pattern, | ||
# output_file=args.output_file, | ||
# req_headers=headers_dict, | ||
# rate_limit=rate_limit, | ||
# delay=delay_rate, | ||
# test_data_config=test_data_config, | ||
) | ||
return results | ||
|
||
def scan_api(body_data: CreateScanModel): | ||
try: | ||
logger.info('test') | ||
api_parser = OpenAPIParser(fpath_or_url=None, spec=body_data.openAPI) | ||
|
||
results = generate_and_run_tests( | ||
api_parser=api_parser, | ||
regex_pattern=body_data.regex_pattern, | ||
req_headers=body_data.req_headers, | ||
rate_limit=body_data.rate_limit, | ||
delay=body_data.delay, | ||
test_data_config=body_data.test_data_config, | ||
) | ||
return results | ||
except Exception as e: | ||
logger.error(f'Error occurred while creating a job: {e}') | ||
print_exception(e) | ||
return [{'error': str(e)}] |
This file contains 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 |
---|---|---|
@@ -1,5 +1,11 @@ | ||
from typing import Optional | ||
from pydantic import BaseModel | ||
|
||
|
||
class CreateScanModel(BaseModel): | ||
openAPI: str | ||
type: str | ||
regex_pattern: Optional[str] = None | ||
req_headers: Optional[dict] = None | ||
rate_limit: Optional[int] = None | ||
delay: Optional[float] = None | ||
test_data_config: Optional[dict] = None |
This file contains 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 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
Oops, something went wrong.