diff --git a/.github/workflows/dev-push.yml b/.github/workflows/dev-push.yml new file mode 100644 index 0000000..793313a --- /dev/null +++ b/.github/workflows/dev-push.yml @@ -0,0 +1,54 @@ +name: "Dev Release: Build and Push OWASP OFFAT Docker Images to DockerHub" + +on: + push: + branches: + - "dev" + +jobs: + build-and-push-dev-docker-images: + runs-on: ubuntu-latest + steps: + - name: Branch Checkout + uses: actions/checkout@v2 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push offat-base docker image + uses: docker/build-push-action@v3 + with: + context: ./src/ + file: ./src/DockerFiles/base-Dockerfile + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-base:dev + platforms: linux/amd64,linux/arm64 + - name: Build and push offat docker image + uses: docker/build-push-action@v3 + with: + context: ./src/ + file: ./src/DockerFiles/dev/cli-Dockerfile + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat:dev + platforms: linux/amd64,linux/arm64 + - name: Build and push offat-api docker image + uses: docker/build-push-action@v3 + with: + context: ./src/ + file: ./src/DockerFiles/dev/backend-api-Dockerfile + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-api:dev + platforms: linux/amd64,linux/arm64 + - name: Build and push offat-api-worker docker image + uses: docker/build-push-action@v3 + with: + context: ./src/ + file: ./src/DockerFiles/dev/backend-api-worker-Dockerfile + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-api-worker:dev + platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml new file mode 100644 index 0000000..de5bd74 --- /dev/null +++ b/.github/workflows/pypi-publish.yml @@ -0,0 +1,44 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload OWASP OFFAT Python Package to PyPi + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.x' + - name: change cwd to src directory + run: cd src + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: | + cd src + python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + packages_dir: src/dist \ No newline at end of file diff --git a/.github/workflows/release-push.yml b/.github/workflows/release-push.yml new file mode 100644 index 0000000..9f8d1a7 --- /dev/null +++ b/.github/workflows/release-push.yml @@ -0,0 +1,53 @@ +name: "Release: Build and Push OWASP OFFAT Docker Images to DockerHub" + +on: + release: + types: [published] + +jobs: + build-and-push-main-docker-images: + runs-on: ubuntu-latest + steps: + - name: Branch Checkout + uses: actions/checkout@v2 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push offat-base docker image + uses: docker/build-push-action@v3 + with: + context: ./src/ + file: ./src/DockerFiles/base-Dockerfile + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-base:latest + platforms: linux/amd64,linux/arm64 + - name: Build and push offat docker image + uses: docker/build-push-action@v3 + with: + context: ./src/ + file: ./src/DockerFiles/main/cli-Dockerfile + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat:latest + platforms: linux/amd64,linux/arm64 + - name: Build and push offat-api docker image + uses: docker/build-push-action@v3 + with: + context: ./src/ + file: ./src/DockerFiles/main/backend-api-Dockerfile + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-api:latest + platforms: linux/amd64,linux/arm64 + - name: Build and push offat-api-worker docker image + uses: docker/build-push-action@v3 + with: + context: ./src/ + file: ./src/DockerFiles/main/backend-api-worker-Dockerfile + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-api-worker:latest + platforms: linux/amd64,linux/arm64 diff --git a/src/DockerFiles/dev/backend-api-Dockerfile b/src/DockerFiles/dev/backend-api-Dockerfile new file mode 100644 index 0000000..a48ae00 --- /dev/null +++ b/src/DockerFiles/dev/backend-api-Dockerfile @@ -0,0 +1,5 @@ +FROM dmdhrumilmistry/offat-base:dev + +EXPOSE 8000 + +ENTRYPOINT [ "python", "-m", "offat.api" ] diff --git a/src/DockerFiles/dev/backend-api-worker-Dockerfile b/src/DockerFiles/dev/backend-api-worker-Dockerfile new file mode 100644 index 0000000..1c50659 --- /dev/null +++ b/src/DockerFiles/dev/backend-api-worker-Dockerfile @@ -0,0 +1,5 @@ +FROM dmdhrumilmistry/offat-base:dev + +WORKDIR /offat + +ENTRYPOINT [ "rq", "worker", "offat_task_queue" ] diff --git a/src/DockerFiles/dev/cli-Dockerfile b/src/DockerFiles/dev/cli-Dockerfile new file mode 100644 index 0000000..ab0e0dc --- /dev/null +++ b/src/DockerFiles/dev/cli-Dockerfile @@ -0,0 +1,3 @@ +FROM dmdhrumilmistry/offat-base:dev + +ENTRYPOINT [ "offat" ] diff --git a/src/DockerFiles/backend-api-Dockerfile b/src/DockerFiles/main/backend-api-Dockerfile similarity index 100% rename from src/DockerFiles/backend-api-Dockerfile rename to src/DockerFiles/main/backend-api-Dockerfile diff --git a/src/DockerFiles/backend-api-worker-Dockerfile b/src/DockerFiles/main/backend-api-worker-Dockerfile similarity index 100% rename from src/DockerFiles/backend-api-worker-Dockerfile rename to src/DockerFiles/main/backend-api-worker-Dockerfile diff --git a/src/DockerFiles/cli-Dockerfile b/src/DockerFiles/main/cli-Dockerfile similarity index 100% rename from src/DockerFiles/cli-Dockerfile rename to src/DockerFiles/main/cli-Dockerfile diff --git a/src/README.md b/src/README.md index 31263b6..d2e138f 100644 --- a/src/README.md +++ b/src/README.md @@ -25,6 +25,7 @@ Automatically Tests for vulnerabilities after generating tests from openapi spec - User Config Based Testing - API for Automating tests and Integrating Tool with other platforms/tools - CLI tool +- Proxy Support - Dockerized Project for Easy Usage - Open Source Tool with MIT License @@ -164,6 +165,12 @@ The disclaimer advises users to use the open-source project for ethical and legi > `rl`: requests rate limit, `dr`: delay between requests +- Use along with proxy + +```bash +offat -f swagger_file.json -p http://localhost:8080 --no-ssl -o output.json +``` + - Use user provided inputs for generating tests ```bash diff --git a/src/offat/__main__.py b/src/offat/__main__.py index 4dc6790..5af4181 100644 --- a/src/offat/__main__.py +++ b/src/offat/__main__.py @@ -40,6 +40,8 @@ def start(): parser.add_argument('-o', '--output', dest='output_file', type=str, help='path to store test results in json format', required=False, default=None) parser.add_argument('-H', '--headers', dest='headers', type=str, help='HTTP requests headers that should be sent during testing eg: User-Agent: offat', required=False, default=None, action='append', nargs='*') parser.add_argument('-tdc','--test-data-config', dest='test_data_config',help='YAML file containing user test data for tests', required=False, type=str) + parser.add_argument('-p', '--proxy', dest='proxy', help='Proxy server URL to route HTTP requests through (e.g., "http://proxyserver:port")', required=False, type=str) + parser.add_argument('-ns', '--no-ssl', dest='no_ssl', help='Ignores SSL verification when enabled', action='store_true', required=False) # False -> ignore SSL, True -> enforce SSL check args = parser.parse_args() @@ -72,6 +74,8 @@ def start(): rate_limit=rate_limit, delay=delay_rate, test_data_config=test_data_config, + proxy=args.proxy, + ssl=args.no_ssl, ) diff --git a/src/offat/api/__main__.py b/src/offat/api/__main__.py index 3131a0e..924aa49 100644 --- a/src/offat/api/__main__.py +++ b/src/offat/api/__main__.py @@ -1,11 +1,13 @@ from uvicorn import run - -if __name__ == '__main__': +def start(): run( app='offat.api.app:app', host="0.0.0.0", port=8000, workers=2, reload=True - ) \ No newline at end of file + ) + +if __name__ == '__main__': + start() \ No newline at end of file diff --git a/src/offat/config_data_handler.py b/src/offat/config_data_handler.py index 57393ca..0e2189f 100644 --- a/src/offat/config_data_handler.py +++ b/src/offat/config_data_handler.py @@ -40,7 +40,7 @@ def populate_user_data(actor_data:dict, actor_name:str,tests:list[dict]): request_headers[header.get('name')] = header.get('value') for test in tests: - # TODO: replace key and value instead of appending + # replace key and value instead of appending test['body_params'] += body_params test['query_params'] += query_params test['path_params'] += path_params diff --git a/src/offat/http.py b/src/offat/http.py index dce19da..c6e39ab 100644 --- a/src/offat/http.py +++ b/src/offat/http.py @@ -1,4 +1,4 @@ -from aiohttp import ClientSession, ClientResponse +from aiohttp import ClientSession, ClientResponse, TCPConnector from os import name as os_name @@ -11,19 +11,29 @@ class AsyncRequests: ''' - AsyncRequests class helps to send HTTP requests. + AsyncRequests class helps to send HTTP requests with rate limiting options. ''' - def __init__(self, headers: dict = None) -> None: + def __init__(self, rate_limit:int=None, delay:float=None, headers: dict = None, proxy:str = None, ssl:bool=True, allow_redirects: bool=True) -> None: '''AsyncRequests class constructor Args: + rate_limit (int): number of concurrent requests at the same time + delay (float): delay between consecutive requests headers (dict): overrides default headers while sending HTTP requests + proxy (str): proxy URL to be used while sending requests + ssl (bool): ignores few SSL errors if value is False Returns: None ''' + self._rate_limit = rate_limit + self._delay = delay self._headers = headers + self._proxy = proxy if proxy else None + self._ssl = ssl if ssl else None + self._allow_redirects = allow_redirects + async def request(self, url: str, method: str = 'GET', session: ClientSession = None, *args, **kwargs) -> ClientResponse: '''Send HTTP requests asynchronously @@ -33,31 +43,34 @@ async def request(self, url: str, method: str = 'GET', session: ClientSession = method (str): HTTP methods (default: GET) supports GET, POST, PUT, HEAD, OPTIONS, DELETE session (aiohttp.ClientSession): aiohttp Client Session for sending requests + Returns: dict: returns request and response data as dict ''' is_new_session = False + connector = TCPConnector(ssl=self._ssl,limit=self._rate_limit,) + if not session: - session = ClientSession(headers=self._headers) + session = ClientSession(headers=self._headers, connector=connector) is_new_session = True method = str(method).upper() match method: case 'GET': - sent_req = session.get(url, *args, **kwargs) + sent_req = session.get(url, proxy=self._proxy, allow_redirects=self._allow_redirects, *args, **kwargs) case 'POST': - sent_req = session.post(url, *args, **kwargs) + sent_req = session.post(url, proxy=self._proxy, allow_redirects=self._allow_redirects, *args, **kwargs) case 'PUT': - sent_req = session.put(url, *args, **kwargs) + sent_req = session.put(url, proxy=self._proxy, allow_redirects=self._allow_redirects, *args, **kwargs) case 'PATCH': - sent_req = session.patch(url, *args, **kwargs) + sent_req = session.patch(url, proxy=self._proxy, allow_redirects=self._allow_redirects, *args, **kwargs) case 'HEAD': - sent_req = session.head(url, *args, **kwargs) + sent_req = session.head(url, proxy=self._proxy, allow_redirects=self._allow_redirects, *args, **kwargs) case 'OPTIONS': - sent_req = session.options(url, *args, **kwargs) + sent_req = session.options(url, proxy=self._proxy, allow_redirects=self._allow_redirects, *args, **kwargs) case 'DELETE': - sent_req = session.delete(url, *args, **kwargs) + sent_req = session.delete(url, proxy=self._proxy, allow_redirects=self._allow_redirects, *args, **kwargs) resp_data = None async with sent_req as response: @@ -76,45 +89,7 @@ async def request(self, url: str, method: str = 'GET', session: ClientSession = await session.close() del session - return resp_data - - -class AsyncRLRequests(AsyncRequests): - ''' - Send Asynchronous rate limited HTTP requests. - ''' - - def __init__(self, rate_limit: int = 20, delay: float = 0.05, headers: dict = None) -> None: - '''AsyncRLRequests constructor - - Args: - rate_limit (int): number of concurrent requests at the same time - delay (float): delay between consecutive requests - headers (dict): overrides default headers while sending HTTP requests - - Returns: - None - ''' - assert isinstance(delay, float) or isinstance(delay, int) - assert isinstance(rate_limit, float) or isinstance(rate_limit, int) - - self._delay = delay - self._semaphore = asyncio.Semaphore(rate_limit) - super().__init__(headers) - - async def request(self, url: str, method: str = 'GET', session: ClientSession = None, *args, **kwargs) -> ClientResponse: - '''Send HTTP requests asynchronously with rate limit and delay between the requests - - Args: - url (str): URL of the webpage/endpoint - method (str): HTTP methods (default: GET) supports GET, POST, - PUT, HEAD, OPTIONS, DELETE - session (aiohttp.ClientSession): aiohttp Client Session for sending requests - - Returns: - dict: returns request and response data as dict - ''' - async with self._semaphore: - response = await super().request(url, method, session, *args, **kwargs) + if self._delay: await asyncio.sleep(self._delay) - return response + + return resp_data diff --git a/src/offat/openapi.py b/src/offat/openapi.py index 8d5cc99..2e76638 100644 --- a/src/offat/openapi.py +++ b/src/offat/openapi.py @@ -19,7 +19,8 @@ def __init__(self, fpath:str, spec:dict=None) -> None: self._spec = spec self.host = self._spec.get('host') - self.base_url = f"http://{self.host}{self._spec.get('basePath','')}" + self.http_scheme = 'https' if 'https' in self._spec.get('schemes') else 'http' + self.base_url = f"{self.http_scheme}://{self.host}{self._spec.get('basePath','')}" self.request_response_params = self._get_request_response_params() diff --git a/src/offat/tester/data_exposure.py b/src/offat/tester/data_exposure.py deleted file mode 100644 index 5c36123..0000000 --- a/src/offat/tester/data_exposure.py +++ /dev/null @@ -1,37 +0,0 @@ -from re import findall -from .regexs import sensitive_data_regex_patterns - - -def detect_data_exposure(data:str)->dict: - '''Detects data exposure against sensitive data regex - patterns and returns dict of matched results - - Args: - data (str): data to be analyzed for exposure - - Returns: - dict: dictionary with tag as dict key and matched pattern as dict value - ''' - # Dictionary to store detected data exposures - detected_exposures = {} - - for pattern_name, pattern in sensitive_data_regex_patterns.items(): - matches = findall(pattern, data) - if matches: - detected_exposures[pattern_name] = matches - - return detected_exposures - - -if __name__ == '__main__': - from json import dumps - sample_test_data = dumps({ - "message" : "Please do not share your AWS Access Key: AKIAEXAMPLEKEY, AWS Secret Key: 9hsk24mv8wzJ3/78mx3p5x3E7N0P39n6Zq0RxTee, Aadhaar: 1234 5678 9012, PAN: ABCDE1234F, SSN: 123-45-6789, credit card: 1234-5678-9012-3456, or email: john.doe@example.com. You can reach me at +1 (555) 123-4567 or via email at contact@example.com. The event date is scheduled for 01/25/2023. The server IP is 192.168.1.1, and IPv6 is 2001:0db8:85a3:0000:0000:8a2e:0370:7334. Password examples: Passw0rd!, Strong@123, mySecret12#. My VISA Card: 4001778837951872" - }) - - # detect data exposure - exposures = detect_data_exposure(sample_test_data) - - # Display the detected exposures - for data_type, data_values in exposures.items(): - print(f"Detected {data_type}: {data_values}") \ No newline at end of file diff --git a/src/offat/tester/post_test_processor.py b/src/offat/tester/post_test_processor.py index 5e4539f..6c6ae83 100644 --- a/src/offat/tester/post_test_processor.py +++ b/src/offat/tester/post_test_processor.py @@ -1,9 +1,10 @@ from copy import deepcopy -from re import search as re_search -from pprint import pprint +from re import search as re_search, findall +from .regexs import sensitive_data_regex_patterns from .test_runner import TestRunnerFiltersEnum + class PostRunTests: '''class Includes tests that should be ran after running all the active test''' @staticmethod @@ -66,20 +67,81 @@ def re_match(patterns:list[str], endpoint:str) -> bool: actor_based_tests.append(PostRunTests.filter_status_code_based_results(actor_test_result)) return actor_based_tests + + + @staticmethod + def detect_data_exposure(results:list[dict])->list[dict]: + '''Detects data exposure against sensitive data regex + patterns and returns dict of matched results + + Args: + data (str): data to be analyzed for exposure + + Returns: + dict: dictionary with tag as dict key and matched pattern as dict value + ''' + def detect_exposure(data:str) -> dict: + # Dictionary to store detected data exposures + detected_exposures = {} + + for pattern_name, pattern in sensitive_data_regex_patterns.items(): + matches = findall(pattern, data) + if matches: + detected_exposures[pattern_name] = matches + return detected_exposures + + + new_results = [] + + for result in results: + res_body = result.get('response_body') + data_exposures_dict = detect_exposure(str(res_body)) + result['data_leak'] = data_exposures_dict + new_results.append(result) + + return new_results + @staticmethod - # TODO: use this everywhere instead of filtering data - def filter_status_code_based_results(result): - new_result = deepcopy(result) - if result.get('response_status_code') in result.get('success_codes'): - res_status = False # test failed - else: - res_status = True # test passed - new_result['result'] = res_status - new_result['result_details'] = result['result_details'].get(res_status) - - return new_result + def filter_status_code_based_results(results:list[dict]) -> list[dict]: # take a list and filter all at once + new_results = [] + for result in results: + new_result = deepcopy(result) + response_status_code = result.get('response_status_code') + success_codes = result.get('success_codes') + + # if response status code or success code is not + # found then continue updating status of remaining + # results + if not response_status_code or not success_codes: + continue + + if response_status_code in success_codes: + res_status = False # test failed + else: + res_status = True # test passed + + new_result['result'] = res_status + + # new_result['result_details'] = result['result_details'].get(res_status) + + new_results.append(new_result) + + return new_results + + + @staticmethod + def update_result_details(results:list[dict]): + new_results = [] + for result in results: + new_result = deepcopy(result) + new_result['result_details'] = result['result_details'].get(result['result']) + + new_results.append(new_result) + + return new_results + @staticmethod def matcher(results:list[dict]): diff --git a/src/offat/tester/test_generator.py b/src/offat/tester/test_generator.py index 6621f4c..ef6018d 100644 --- a/src/offat/tester/test_generator.py +++ b/src/offat/tester/test_generator.py @@ -316,18 +316,13 @@ def bola_fuzz_path_test( path_params_in_body = list(filter(lambda x: x.get('in') == 'path', request_params)) path_params += path_params_in_body path_params = fill_params(path_params) - # print(path_params) - # print('-'*30) for path_param in path_params: path_param_name = path_param.get('name') path_param_value = path_param.get('value') endpoint_path = endpoint_path.replace('{' + str(path_param_name) + '}', str(path_param_value)) - # TODO: handle request query params request_query_params = list(filter(lambda x: x.get('in') == 'query', request_params)) - # print(request_query_params) - # print('-'*30) tasks.append({ 'test_name':'BOLA Path Test with Fuzzed Params', diff --git a/src/offat/tester/test_runner.py b/src/offat/tester/test_runner.py index aca9965..39e1db1 100644 --- a/src/offat/tester/test_runner.py +++ b/src/offat/tester/test_runner.py @@ -1,7 +1,7 @@ from asyncio import ensure_future, gather +from aiohttp.client_exceptions import ClientProxyConnectionError from enum import Enum -from .data_exposure import detect_data_exposure -from ..http import AsyncRequests, AsyncRLRequests +from ..http import AsyncRequests from ..logger import create_logger logger = create_logger(__name__) @@ -19,11 +19,8 @@ class PayloadFor(Enum): class TestRunner: - def __init__(self, rate_limit:int=None, delay:float=None, headers:dict=None) -> None: - if rate_limit and delay: - self._client = AsyncRLRequests(rate_limit=rate_limit, delay=delay, headers=headers) - else: - self._client = AsyncRequests(headers=headers) + def __init__(self, rate_limit:int=None, delay:float=None, headers:dict=None, proxy: str = None, ssl: bool = True) -> None: + self._client = AsyncRequests(rate_limit=rate_limit, delay=delay, headers=headers, proxy=proxy, ssl=ssl) def _generate_payloads(self, params:list[dict], payload_for:PayloadFor=PayloadFor.BODY): @@ -71,7 +68,7 @@ def _generate_payloads(self, params:list[dict], payload_for:PayloadFor=PayloadFo return {} - async def status_code_filter_request(self, test_task): + async def send_request(self, test_task): url = test_task.get('url') http_method = test_task.get('method') success_codes = test_task.get('success_codes', [200, 301]) @@ -90,19 +87,13 @@ async def status_code_filter_request(self, test_task): response = await self._client.request(url=url, method=http_method, *args, **kwargs) except ConnectionRefusedError: logger.error('Connection Failed! Server refused Connection!!') + except ClientProxyConnectionError as e: + logger.error(f'Proxy Connection Error: {e}') - # TODO: move this filter to result processing module test_result = test_task - if isinstance(response, dict) and response.get('status') in success_codes: - result = False # test failed - else: - result = True # test passed - test_result['result'] = result - test_result['result_details'] = test_result['result_details'].get(result) # add request headers to result test_result['request_headers'] = response.get('req_headers',[]) - # append response headers and body for analyzing data leak res_body = response.get('res_body', 'No Response Body Found') test_result['response_headers'] = response.get('res_headers') @@ -110,19 +101,6 @@ async def status_code_filter_request(self, test_task): test_result['response_status_code'] = response.get('status') test_result['redirection'] = response.get('res_redirection', '') - # run data leak test - # TODO: run this test in result processing module - data_exposures_dict = detect_data_exposure(str(res_body)) - test_result['data_leak'] = data_exposures_dict - - # if data_exposures_dict: - # print(res_body) - # Display the detected exposures - # for data_type, data_values in data_exposures_dict.items(): - # print(f"Detected {data_type}: {data_values}") - # print('--'*30) - - return test_result @@ -131,13 +109,9 @@ async def run_tests(self, test_tasks:list): tasks = [] for test_task in test_tasks: - match test_task.get('response_filter', None): - case _: # default filter - task_filter = self.status_code_filter_request - tasks.append( ensure_future( - task_filter(test_task) + self.send_request(test_task) ) ) diff --git a/src/offat/tester/tester_utils.py b/src/offat/tester/tester_utils.py index d1d1960..67fdc31 100644 --- a/src/offat/tester/tester_utils.py +++ b/src/offat/tester/tester_utils.py @@ -37,19 +37,32 @@ def run_test(test_runner:TestRunner, tests:list[dict], regex_pattern:str=None, s if post_run_matcher_test: test_results = PostRunTests.matcher(test_results) + + # update test result for status based code filter + test_results = PostRunTests.filter_status_code_based_results(test_results) + + # update tests result success/failure details + test_results = PostRunTests.update_result_details(test_results) + # run data leak tests + test_results = PostRunTests.detect_data_exposure(test_results) + + # print results results = test_table_generator.generate_result_table(deepcopy(test_results)) print(results) return test_results -def generate_and_run_tests(api_parser:OpenAPIParser, regex_pattern:str=None, output_file:str=None, rate_limit:int=None,delay:float=None,req_headers:dict=None, test_data_config:dict=None): +# Note: redirects are allowed by default making it easier for pentesters/researchers +def generate_and_run_tests(api_parser:OpenAPIParser, regex_pattern:str=None, output_file:str=None, rate_limit:int=None,delay:float=None,req_headers:dict=None,proxy:str = None, ssl:bool = True, test_data_config:dict=None): global test_table_generator, logger test_runner = TestRunner( rate_limit=rate_limit, delay=delay, - headers=req_headers + headers=req_headers, + proxy=proxy, + ssl=ssl, ) results:list = [] diff --git a/src/pyproject.toml b/src/pyproject.toml index 71ca26c..60e47cf 100644 --- a/src/pyproject.toml +++ b/src/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "offat" -version = "0.9.0" +version = "0.10.0" description = "Offensive API tester tool automates checks for common API vulnerabilities" authors = ["Dhrumil Mistry "] license = "MIT" @@ -35,6 +35,7 @@ api = ["fastapi", "uvicorn", "redis", "rq", "python-dotenv"] [tool.poetry.scripts] offat = "offat.__main__:start" +offat-api = "offat.api.__main__:start" [build-system]