diff --git a/src/offat/__main__.py b/src/offat/__main__.py index 8b6dde2..4f9aa9e 100644 --- a/src/offat/__main__.py +++ b/src/offat/__main__.py @@ -3,7 +3,7 @@ from .parsers.openapi import OpenAPIv3Parser from .parsers.swagger import SwaggerParser from .config_data_handler import validate_config_file_data -from .tester.tester_utils import generate_and_run_tests +from .tester.handler import generate_and_run_tests from .parsers import create_parser from .utils import get_package_version, headers_list_to_dict, read_yaml diff --git a/src/offat/api/jobs.py b/src/offat/api/jobs.py index 08e6c17..2c01d01 100644 --- a/src/offat/api/jobs.py +++ b/src/offat/api/jobs.py @@ -1,6 +1,6 @@ from sys import exc_info from offat.api.models import CreateScanModel -from offat.tester.tester_utils import generate_and_run_tests +from offat.tester.handler import generate_and_run_tests from offat.parsers import create_parser from offat.logger import logger @@ -18,6 +18,6 @@ def scan_api(body_data: CreateScanModel): ) return results except Exception as e: - logger.error("Error occurred while creating a job: %s", repr(e)) - logger.debug("Debug Data:", exc_info=exc_info()) - return [{"error": str(e)}] + logger.error('Error occurred while creating a job: %s', repr(e)) + logger.debug('Debug Data:', exc_info=exc_info()) + return [{'error': str(e)}] diff --git a/src/offat/report/summary.py b/src/offat/report/summary.py index f101711..a46027b 100644 --- a/src/offat/report/summary.py +++ b/src/offat/report/summary.py @@ -22,26 +22,26 @@ def get_counts(results: list[dict], filter_errors: bool = False) -> dict[str, in dict: name (str) as key and its associated count (int) """ if filter_errors: - results = list(filter(lambda result: result.get("error", False), results)) + results = list(filter(lambda result: result.get('error', False), results)) error_count = 0 data_leak_count = 0 - failed_count = 0 - success_count = 0 + immune_count = 0 + vulnerable_count = 0 for result in results: - error_count += 1 if result.get("error", False) else 0 - data_leak_count += 1 if result.get("data_leak", False) else 0 + error_count += 1 if result.get('error', False) else 0 + data_leak_count += 1 if result.get('data_leak', False) else 0 - if result.get("result"): - success_count += 1 + if result.get('vulnerable'): + vulnerable_count += 1 else: - failed_count += 1 + immune_count += 1 count_dict = { - "errors": error_count, - "data_leaks": data_leak_count, - "failed": failed_count, - "success": success_count, + 'errors': error_count, + 'data_leaks': data_leak_count, + 'immune': immune_count, + 'vulnerable': vulnerable_count, } return count_dict @@ -50,7 +50,7 @@ def get_counts(results: list[dict], filter_errors: bool = False) -> dict[str, in def generate_count_summary( results: list[dict], filter_errors: bool = False, - output_format: str = "table", + output_format: str = 'table', table_title: str | None = None, ) -> Table | str: """ @@ -70,8 +70,8 @@ def generate_count_summary( results=results, filter_errors=filter_errors ) match output_format: - case "markdown": - output = "" + case 'markdown': + output = '' if table_title: output += f"**{table_title}**\n" @@ -80,8 +80,8 @@ def generate_count_summary( case _: # table format output = Table( - Column(header="⚔️", overflow="fold", justify="center"), - Column(header="Endpoints Count", overflow="fold"), + Column(header='⚔️', overflow='fold', justify='center'), + Column(header='Endpoints Count', overflow='fold'), title=table_title, ) diff --git a/src/offat/report/templates/table.py b/src/offat/report/templates/table.py index a2a6a62..3f17b62 100644 --- a/src/offat/report/templates/table.py +++ b/src/offat/report/templates/table.py @@ -48,15 +48,33 @@ def _sanitize_results( ): if filter_passed_results: results = list( - filter(lambda x: not x.get('result') or x.get('data_leak'), results) + filter( + lambda result: result.get('vulnerable') or result.get('data_leak'), + results, + ) ) + keys_to_remove = [ + 'url', + 'test_name', + 'response_filter', + 'body_params', + 'request_headers', + 'redirection', + 'query_params', + 'path_params', + 'curl_command', + 'response_match_regex', + 'regex_match_result', + 'success_codes', + ] + # remove keys based on conditions or update their values for result in results: - if result['result']: - result['result'] = '[bold green]Passed \u2713[/bold green]' + if result['vulnerable']: + result['vulnerable'] = '[bold red]True \u00d7[/bold red]' else: - result['result'] = '[bold red]Failed \u00d7[/bold red]' + result['vulnerable'] = '[bold green]False \u2713[/bold green]' if not is_leaking_data: del result['response_headers'] @@ -66,15 +84,6 @@ def _sanitize_results( result['status_code'] = result.get('response_status_code') del result['response_status_code'] - if result.get('success_codes'): - del result['success_codes'] - - if result.get('regex_match_result'): - del result['regex_match_result'] - - if result.get('response_match_regex'): - del result['response_match_regex'] - if result.get('security') or result.get('security') == []: del result['security'] @@ -86,13 +95,8 @@ def _sanitize_results( if not isinstance(result.get('malicious_payload'), str): del result['malicious_payload'] - del result['url'] - del result['test_name'] - del result['response_filter'] - del result['body_params'] - del result['request_headers'] - del result['redirection'] - del result['query_params'] - del result['path_params'] + for key in keys_to_remove: + if key in result: + del result[key] return results diff --git a/src/offat/tester/fuzzer.py b/src/offat/tester/fuzzer.py index d9a09bb..ff8c086 100644 --- a/src/offat/tester/fuzzer.py +++ b/src/offat/tester/fuzzer.py @@ -99,7 +99,7 @@ def fuzz_type_value(param_type: str, param_name: str): def fill_params(params: list[dict], is_v3: bool) -> list[dict]: """fills params for OAS/swagger specs""" schema_params = [] - for index in range(len(params)): + for index, _ in enumerate(params): param_type = ( params[index].get("schema", {}).get("type") if is_v3 @@ -125,6 +125,7 @@ def fill_params(params: list[dict], is_v3: bool) -> list[dict]: "name": param_name, "required": param_is_required, "value": param_value, + "type": param_type } ] diff --git a/src/offat/tester/generator.py b/src/offat/tester/generator.py index 420ef2e..3c990dd 100644 --- a/src/offat/tester/generator.py +++ b/src/offat/tester/generator.py @@ -122,9 +122,9 @@ def check_unsupported_http_methods( 'malicious_payload': [], 'args': args, 'kwargs': kwargs, - 'result_details': { - True: "Endpoint doesn't perform any HTTP verb which is not documented", - False: 'Endpoint performs HTTP verb which is not documented', + 'vuln_details': { + True: 'Endpoint performs HTTP verb which is not documented', + False: "Endpoint doesn't perform any HTTP verb which is not documented", }, 'body_params': body_params, 'query_params': query_params, @@ -297,9 +297,9 @@ def sqli_fuzz_params_test( request_obj['malicious_payload'] = sqli_payload - request_obj['result_details'] = { - True: 'Parameters are not vulnerable to SQLi Payload', # passed - False: 'One or more parameter is vulnerable to SQL Injection Attack', # failed + request_obj['vuln_details'] = { + True: 'One or more parameter is vulnerable to SQL Injection Attack', + False: 'Parameters are not vulnerable to SQLi Payload', } request_obj['success_codes'] = success_codes request_obj[ @@ -404,9 +404,9 @@ def sqli_in_uri_path_fuzz_test( 'malicious_payload': sqli_payload, 'args': args, 'kwargs': kwargs, - 'result_details': { - True: 'Endpoint is not vulnerable to SQLi', # passed - False: 'Endpoint might be vulnerable to SQli', # failed + 'vuln_details': { + True: 'Endpoint might be vulnerable to SQli', + False: 'Endpoint is not vulnerable to SQLi', }, 'success_codes': success_codes, 'response_filter': PostTestFiltersEnum.STATUS_CODE_FILTER.name, @@ -498,9 +498,9 @@ def bola_fuzz_path_test( 'malicious_payload': path_params, 'args': args, 'kwargs': kwargs, - 'result_details': { - True: 'Endpoint is not vulnerable to BOLA', # passed - False: 'Endpoint might be vulnerable to BOLA', # failed + 'vuln_details': { + True: 'Endpoint might be vulnerable to BOLA', + False: 'Endpoint is not vulnerable to BOLA', }, 'success_codes': success_codes, 'response_filter': PostTestFiltersEnum.STATUS_CODE_FILTER.name, @@ -594,9 +594,9 @@ def bola_fuzz_trailing_slash_path_test( 'malicious_payload': malicious_payload, 'args': args, 'kwargs': kwargs, - 'result_details': { - True: 'Endpoint might not vulnerable to BOLA', # passed - False: 'Endpoint might be vulnerable to BOLA', # failed + 'vuln_details': { + True: 'Endpoint might be vulnerable to BOLA', + False: 'Endpoint might not vulnerable to BOLA', }, 'success_codes': success_codes, 'response_filter': PostTestFiltersEnum.STATUS_CODE_FILTER.name, @@ -680,6 +680,9 @@ def bopla_fuzz_test( filter(lambda x: x.get('in') == 'path', request_params) ) + if len(request_body_params) == 0 and len(request_query_params) == 0: + continue + # handle path params from path_params # and replace path params by value in # endpoint path @@ -718,9 +721,9 @@ def bopla_fuzz_test( 'malicious_payload': response_body_params, 'args': args, 'kwargs': kwargs, - 'result_details': { - True: 'Endpoint might not vulnerable to BOPLA', # passed - False: 'Endpoint might be vulnerable to BOPLA', # failed + 'vuln_details': { + True: 'Endpoint might be vulnerable to BOPLA', + False: 'Endpoint might not vulnerable to BOPLA', }, 'success_codes': success_codes, 'response_filter': PostTestFiltersEnum.STATUS_CODE_FILTER.name, @@ -774,7 +777,7 @@ def __generate_injection_fuzz_params_test( self, openapi_parser: SwaggerParser | OpenAPIv3Parser, test_name: str, - result_details: dict, + vuln_details: dict, payloads_data: list[dict], *args, **kwargs, @@ -804,19 +807,19 @@ def __generate_injection_fuzz_params_test( for payload_dict in payloads_data: for request_obj in fuzzed_request_list: payload = payload_dict['request_payload'] - - # handle body request params body_request_params = request_obj.get('body_params', []) + query_request_params = request_obj.get('query_params', []) + # endpoint can be fuzzed if it has query/body params + if len(body_request_params) == 0 and len(query_request_params) == 0: + continue + + # handle body and query request params malicious_body_request_params = self.__inject_payload_in_params( body_request_params, payload ) - - # handle query request params - query_request_params = request_obj.get('query_params', []) malicious_query_request_params = self.__inject_payload_in_params( query_request_params, payload ) - request_obj['test_name'] = test_name request_obj['body_params'] = malicious_body_request_params @@ -826,7 +829,7 @@ def __generate_injection_fuzz_params_test( request_obj['malicious_payload'] = payload - request_obj['result_details'] = result_details + request_obj['vuln_details'] = vuln_details request_obj[ 'response_filter' ] = PostTestFiltersEnum.BODY_REGEX_FILTER.name @@ -865,15 +868,15 @@ def os_command_injection_fuzz_params_test( {'request_payload': 'ls -la', 'response_match_regex': r'total\s\d+'}, ] - result_details = { - True: 'Parameters are not vulnerable to OS Command Injection', # passed - False: 'One or more parameter is vulnerable to OS Command Injection Attack', # failed + vuln_details = { + True: 'One or more parameter is vulnerable to OS Command Injection Attack', + False: 'Parameters are not vulnerable to OS Command Injection', } return self.__generate_injection_fuzz_params_test( openapi_parser=openapi_parser, test_name=test_name, - result_details=result_details, + vuln_details=vuln_details, payloads_data=payloads_data, ) @@ -912,15 +915,15 @@ def xss_html_injection_fuzz_params_test( }, ] - result_details = { - True: 'Parameters are not vulnerable to XSS/HTML Injection Attack', # passed - False: 'One or more parameter is vulnerable to XSS/HTML Injection Attack', # failed + vuln_details = { + False: 'Parameters are not vulnerable to XSS/HTML Injection Attack', + True: 'One or more parameter is vulnerable to XSS/HTML Injection Attack', } return self.__generate_injection_fuzz_params_test( openapi_parser=openapi_parser, test_name=test_name, - result_details=result_details, + vuln_details=vuln_details, payloads_data=payloads_data, ) @@ -968,15 +971,15 @@ def ssti_fuzz_params_test(self, openapi_parser: SwaggerParser | OpenAPIv3Parser) {'request_payload': r'*{7*7}', 'response_match_regex': r'49'}, ] - result_details = { - True: 'Parameters are not vulnerable to SSTI Attack', # passed - False: 'One or more parameter is vulnerable to SSTI Attack', # failed + vuln_details = { + True: 'One or more parameter is vulnerable to SSTI Attack', + False: 'Parameters are not vulnerable to SSTI Attack', } return self.__generate_injection_fuzz_params_test( openapi_parser=openapi_parser, test_name=test_name, - result_details=result_details, + vuln_details=vuln_details, payloads_data=payloads_data, ) @@ -993,7 +996,7 @@ def missing_auth_fuzz_test( openapi_parser (OpenAPIParser): An instance of the OpenAPIParser class containing the parsed OpenAPI specification. success_codes (list[int], optional): A list of HTTP success codes to consider - as successful BOLA responses. Defaults to [200, 201, 301]. + as test failed responses. Defaults to [200, 201, 301]. *args: Variable-length positional arguments. **kwargs: Arbitrary keyword arguments. @@ -1069,9 +1072,9 @@ def missing_auth_fuzz_test( 'malicious_payload': 'Security Payload Missing', 'args': args, 'kwargs': kwargs, - 'result_details': { - True: 'Endpoint implements security authentication as defined', # passed - False: 'Endpoint fails to implement security authentication as defined', # failed + 'vuln_details': { + True: 'Endpoint fails to implement security authentication as defined', + False: 'Endpoint implements security authentication as defined', }, 'success_codes': success_codes, 'response_filter': PostTestFiltersEnum.STATUS_CODE_FILTER.name, diff --git a/src/offat/tester/handler.py b/src/offat/tester/handler.py new file mode 100644 index 0000000..5d9faea --- /dev/null +++ b/src/offat/tester/handler.py @@ -0,0 +1,338 @@ +""" +module to handle the test generation and running of tests +""" +from .generator import TestGenerator +from .tester_utils import run_test, is_host_up, reduce_data_list +from .post_test_processor import PostRunTests +from .runner import TestRunner +from ..parsers.openapi import OpenAPIv3Parser +from ..parsers.swagger import SwaggerParser +from ..report.generator import ReportGenerator +from ..report.summary import ResultSummarizer +from ..logger import logger, console + +# create tester obj +test_generator = TestGenerator() + + +# Note: redirects are allowed by default making it easier for pentesters/researchers +def generate_and_run_tests( + api_parser: SwaggerParser | OpenAPIv3Parser, + regex_pattern: str | None = None, + output_file: str | None = None, + output_file_format: str | None = None, + rate_limit: int | None = None, + req_headers: dict | None = None, + proxies: list[str] | None = None, + test_data_config: dict | None = None, + ssl: bool = False, + capture_failed: bool = False, + remove_unused_data: bool = True, +): + """ + Generates and runs tests for the provided OAS/Swagger file. + + Args: + api_parser: An instance of SwaggerParser or OpenAPIv3Parser + representing the parsed API specification. + regex_pattern: A string representing the regex pattern to + match against the response body (optional). + output_file: A string representing the path to the output + file (optional). + output_file_format: A string representing the format of the + output file (optional). + rate_limit: An integer representing the rate limit for the + tests (optional). + req_headers: A dictionary representing the request headers + (optional). + proxies: A list of strings representing the proxies to be used + (optional). + test_data_config: A dictionary representing the configuration + for user-provided test data (optional). + ssl: A boolean indicating whether to use SSL for the requests + (default: False). + capture_failed: A boolean indicating whether to capture failed + tests in the report (default: False). + remove_unused_data: A boolean indicating whether to remove + unused data (default: True). + + Returns: + A list of test results. + """ + if not is_host_up(openapi_parser=api_parser): + logger.error( + 'Stopping tests due to unavailibility of host: %s', api_parser.host + ) + return + + logger.info('Host %s is up', api_parser.host) + + test_runner = TestRunner( + rate_limit=rate_limit, + headers=req_headers, + proxies=proxies, + ssl=ssl, + ) + + results: list = [] + + test_list = [] + + # test for unsupported http methods + test_list.append( + { + 'test_name': 'Checking for Unsupported HTTP Methods/Verbs', + 'tests': test_generator.check_unsupported_http_methods(api_parser), + 'type': 'FUZZED', + } + ) + + # sqli fuzz test + test_list.append( + { + 'test_name': 'Checking for SQLi vulnerability', + 'tests': test_generator.sqli_fuzz_params_test(api_parser), + 'type': 'FUZZED', + } + ) + + test_list.append( + { + 'test_name': 'Checking for SQLi vulnerability in URI Path', + 'tests': test_generator.sqli_in_uri_path_fuzz_test(api_parser), + 'type': 'FUZZED', + } + ) + + # OS Command Injection Fuzz Test + test_list.append( + { + 'test_name': 'Checking for OS Command Injection Vulnerability with fuzzed params and checking response body', # noqa: E501 + 'tests': test_generator.os_command_injection_fuzz_params_test(api_parser), + 'type': 'FUZZED', + 'post_run_matcher_test': True, + } + ) + + # XSS/HTML Injection Fuzz Test + test_list.append( + { + 'test_name': 'Checking for XSS/HTML Injection Vulnerability with fuzzed params and checking response body', # noqa: E501 + 'tests': test_generator.xss_html_injection_fuzz_params_test(api_parser), + 'type': 'FUZZED', + 'post_run_matcher_test': True, + } + ) + + # BOLA path tests with fuzzed data + test_list.append( + { + 'test_name': 'Checking for BOLA in PATH using fuzzed params', + 'tests': test_generator.bola_fuzz_path_test( + api_parser, success_codes=[200, 201, 301] + ), + 'type': 'FUZZED', + } + ) + + # BOLA path test with fuzzed data + trailing slash + test_list.append( + { + 'test_name': 'Checking for BOLA in PATH with trailing slash and id using fuzzed params', + 'tests': test_generator.bola_fuzz_trailing_slash_path_test( + api_parser, success_codes=[200, 201, 301] + ), + 'type': 'FUZZED', + } + ) + + # Mass Assignment / BOPLA + test_list.append( + { + 'test_name': 'Checking for Mass Assignment Vulnerability with fuzzed params and checking response status codes:', # noqa: E501 + 'tests': test_generator.bopla_fuzz_test( + api_parser, success_codes=[200, 201, 301] + ), + 'type': 'FUZZED', + } + ) + + # SSTI Vulnerability + test_list.append( + { + 'test_name': 'Checking for SSTI vulnerability with fuzzed params and checking response body', # noqa: E501 + 'tests': test_generator.ssti_fuzz_params_test(api_parser), + 'type': 'FUZZED', + 'post_run_matcher_test': True, + } + ) + + # Missing Authorization Test + test_list.append( + { + 'test_name': 'Checking for Missing Authorization', + 'tests': test_generator.missing_auth_fuzz_test(api_parser), + 'type': 'FUZZED', + } + ) + + # Tests with User provided Data + if bool(test_data_config): + logger.info('[bold] Testing with user provided data [/bold]') + + # BOLA path tests with fuzzed + user provided data + test_list.append( + { + 'test_name': 'Checking for BOLA in PATH using fuzzed and user provided params', + 'tests': test_generator.test_with_user_data( + test_data_config, + test_generator.bola_fuzz_path_test, + openapi_parser=api_parser, + success_codes=[200, 201, 301], + ), + 'type': 'USER + FUZZED', + } + ) + + # BOLA path test with fuzzed + user data + trailing slash + test_list.append( + { + 'test_name': 'Checking for BOLA in PATH with trailing slash id using fuzzed and user provided params:', # noqa: E501 + 'tests': test_generator.test_with_user_data( + test_data_config, + test_generator.bola_fuzz_trailing_slash_path_test, + openapi_parser=api_parser, + success_codes=[200, 201, 301], + ), + 'type': 'USER + FUZZED', + } + ) + + # OS Command Injection Fuzz Test + test_list.append( + { + 'test_name': 'Checking for OS Command Injection Vulnerability with fuzzed & user params and checking response body', # noqa: E501 + 'tests': test_generator.test_with_user_data( + test_data_config, + test_generator.os_command_injection_fuzz_params_test, + openapi_parser=api_parser, + ), + 'type': 'USER + FUZZED', + 'post_run_matcher_test': True, + } + ) + + # XSS/HTML Injection Fuzz Test + test_list.append( + { + 'test_name': 'Checking for XSS/HTML Injection Vulnerability with fuzzed & user params and checking response body', # noqa: E501 + 'tests': test_generator.test_with_user_data( + test_data_config, + test_generator.xss_html_injection_fuzz_params_test, + openapi_parser=api_parser, + ), + 'type': 'USER + FUZZED', + 'post_run_matcher_test': True, + } + ) + + # STTI Vulnerability + test_list.append( + { + 'test_name': 'Checking for SSTI vulnerability with fuzzed params & user data and checking response body', # noqa: E501 + 'tests': test_generator.test_with_user_data( + test_data_config, + test_generator.ssti_fuzz_params_test, + openapi_parser=api_parser, + ), + 'type': 'USER + FUZZED', + 'post_run_matcher_test': True, + } + ) + + # Missing Authorization Test + test_list.append( + { + 'test_name': 'Checking for Missing Authorization with user data', + 'tests': test_generator.test_with_user_data( + test_data_config, + test_generator.missing_auth_fuzz_test, + openapi_parser=api_parser, + ), + 'type': 'USER + FUZZED', + 'post_run_matcher_test': True, + } + ) + + for test in test_list: + if 'post_run_matcher_test' not in test: + test['post_run_matcher_test'] = False + + logger.info(test['test_name']) + + results += run_test( + test_runner=test_runner, + tests=test['tests'], + regex_pattern=regex_pattern, + post_run_matcher_test=test['post_run_matcher_test'], + description=f'({test["type"]}) {test["test_name"]}', + ) + + # After we collected all the results, we can now test them for + # access without restrictions + if bool(test_data_config): + # Broken Access Control Test + test_name = 'Checking for Broken Access Control' + logger.info(test_name) + bac_results = PostRunTests.run_broken_access_control_tests( + results, test_data_config + ) + + results += run_test( + test_runner=test_runner, + tests=bac_results, + regex_pattern=regex_pattern, + skip_test_run=True, + description=test_name, + ) + + if remove_unused_data: + for result in results: + result.pop('kwargs', None) + result.pop('args', None) + + result['body_params'] = reduce_data_list(result.get('body_params', [{}])) + result['query_params'] = reduce_data_list(result.get('query_params', [{}])) + result['path_params'] = reduce_data_list(result.get('path_params', [{}])) + result['malicious_payload'] = reduce_data_list( + result.get('malicious_payload', []) + ) + + # save file to output if output flag is present + if output_file_format != 'table': + ReportGenerator.generate_report( + results=results, + report_format=output_file_format, + report_path=output_file, + capture_failed=capture_failed, + ) + + ReportGenerator.generate_report( + results=results, + report_format='table', + report_path=None, + capture_failed=capture_failed, + ) + + console.print( + "The columns for 'data_leak' and 'vulnerable' in the table represent independent aspects. It's possible for there to be a data leak in the endpoint, yet the result for that endpoint may still be marked as 'Success'. This is because the 'vulnerable' column doesn't necessarily reflect the overall test result; it may indicate success even in the presence of a data leak." + ) + + console.rule() + result_summary = ResultSummarizer.generate_count_summary( + results, table_title='Results Summary' + ) + + console.print(result_summary) + + return results diff --git a/src/offat/tester/post_test_processor.py b/src/offat/tester/post_test_processor.py index f7b6ceb..de0d812 100644 --- a/src/offat/tester/post_test_processor.py +++ b/src/offat/tester/post_test_processor.py @@ -52,27 +52,27 @@ def re_match(patterns: list[str], endpoint: str) -> bool: return False actor_based_tests = [] - actors = test_data_config.get("actors", [{}]) + actors = test_data_config.get('actors', [{}]) actor_names = [] for actor in actors: actor_name = list(actor.keys())[-1] - unauth_endpoint_regex = actor[actor_name].get("unauthorized_endpoints", []) + unauth_endpoint_regex = actor[actor_name].get('unauthorized_endpoints', []) for result in results: - if result.get("test_actor_name") != actor_name: + if result.get('test_actor_name') != actor_name: continue - endpoint = result.get("endpoint", "endpoint path not found") + endpoint = result.get('endpoint', 'endpoint path not found') if not re_match(unauth_endpoint_regex, endpoint): continue actor_names.append(actor_name) actor_test_result = deepcopy(result) - actor_test_result["test_name"] = "Broken Access Control" - actor_test_result["result_details"] = { - True: "Endpoint might not vulnerable to BAC", # passed - False: f"BAC: Endpoint is accessible to {actor_name}", # failed + actor_test_result['test_name'] = 'Broken Access Control' + actor_test_result['vuln_details'] = { + True: f"BAC: Endpoint is accessible to {actor_name}", + False: 'Endpoint might not vulnerable to BAC', } actor_based_tests.append(actor_test_result) @@ -97,15 +97,21 @@ def detect_exposure(data: str) -> dict: for pattern_name, pattern in sensitive_data_regex_patterns.items(): matches = findall(pattern, data) if matches: + if isinstance(matches, list) and isinstance(matches[0], tuple): + matches = set.union( + *[set(match_tuple) for match_tuple in matches] + ) + matches.discard('') + matches = list(matches) detected_exposures[pattern_name] = matches return detected_exposures new_results = [] for result in results: - res_body = result.get("response_body") + res_body = result.get('response_body') data_exposures_dict = detect_exposure(str(res_body)) - result["data_leak"] = data_exposures_dict + result['data_leak'] = data_exposures_dict new_results.append(result) return new_results @@ -117,8 +123,8 @@ def filter_status_code_based_results(results: list[dict]) -> list[dict]: for result in results: new_result = deepcopy(result) - response_status_code = result.get("response_status_code") - success_codes = result.get("success_codes") + 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 @@ -126,15 +132,9 @@ def filter_status_code_based_results(results: list[dict]) -> list[dict]: 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_result['vulnerable'] = ( + response_status_code in success_codes + ) # True-> vulnerable, False-> not vulnerable new_results.append(new_result) return new_results @@ -144,10 +144,9 @@ 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_result['vuln_details'] = result['vuln_details'].get( + result['vulnerable'] ) - new_results.append(new_result) return new_results @@ -172,26 +171,26 @@ def matcher(results: list[dict]): new_results = [] for result in results: - match_location = result.get("response_filter") - match_regex = result.get("response_match_regex") + match_location = result.get('response_filter') + match_regex = result.get('response_match_regex') # skip test if match regex not found if not match_regex or not match_location: continue match match_location: - case PostTestFiltersEnum.STATUS_CODE_FILTER: - target_data = result.get("response_status_code") - case PostTestFiltersEnum.HEADER_REGEX_FILTER: - target_data = result.get("response_body") + case PostTestFiltersEnum.STATUS_CODE_FILTER.name: + target_data = result.get('response_status_code') + case PostTestFiltersEnum.HEADER_REGEX_FILTER.name: + target_data = result.get('response_body') case _: # PostTestFiltersEnum.BODY_REGEX_FILTER.name: - target_data = result.get("response_body") + target_data = result.get('response_body') match_response = re_search(match_regex, target_data) new_result = deepcopy(result) - new_result["regex_match_result"] = str(match_response) - # None (no match) -> False (Vulnerable) -> Not False (not Vulnerable) - new_result["result"] = not bool(match_response) + new_result['regex_match_result'] = str(match_response) + # True (Vulnerable) / False (Not Vulnerable) + new_result['vulnerable'] = bool(match_response) new_results.append(new_result) return new_results diff --git a/src/offat/tester/regexs.py b/src/offat/tester/regexs.py index ed602b1..b42090a 100644 --- a/src/offat/tester/regexs.py +++ b/src/offat/tester/regexs.py @@ -25,4 +25,13 @@ 'AWSSecretKey': r'\b[0-9a-zA-Z/+]{40}\b', 'AWSResourceURL': r'\b([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.amazonaws.com*)\b', 'AWSArnId': r'\barn:aws:[A-Za-z0-9-_]*\:[A-Za-z0-9-_]*\:[A-Za-z0-9-_]*\:[A-Za-z0-9-/_]*\b', + # Google Tokens + 'Google' : r'google_oauth_token|google_oauth|google_b64', + # Slack + 'Slack' : r'xoxo-[0-9a-z]+-[0-9a-z]+-[0-9a-z]+-[0-9a-z]+', + # Postgres DSN postgresql://[user[:password]@][netloc][:port][/dbname][?param1=value1&...] + 'PostgresDSN' : r'postgresql:\/\/|pgsql:', + 'MySQLDSN' : 'mysql://', + 'RedisDSN' : 'redis://', + 'OutlookWebhook' : 'https://outlook.office.com/webhook/' } diff --git a/src/offat/tester/runner.py b/src/offat/tester/runner.py index c51ead0..b171b29 100644 --- a/src/offat/tester/runner.py +++ b/src/offat/tester/runner.py @@ -4,9 +4,9 @@ from sys import exc_info, exit from rich.progress import Progress, TaskID - from ..http import AsyncRequests from ..logger import logger, console +from ..utils import result_to_curl class PayloadFor(Enum): @@ -147,6 +147,9 @@ async def send_request(self, test_task: dict): logger.error('Unable to send request due to error: %s', e) logger.error(locals()) + # generate curl command for reproducing result + test_result['curl_command'] = result_to_curl(test_result) + # advance progress bar if self.progress_task_id: self.progress.update(self.progress_task_id, advance=1, refresh=True) diff --git a/src/offat/tester/tester_utils.py b/src/offat/tester/tester_utils.py index fb30d60..f9a1b3c 100644 --- a/src/offat/tester/tester_utils.py +++ b/src/offat/tester/tester_utils.py @@ -8,19 +8,13 @@ from asyncio.exceptions import CancelledError from re import search as regex_search + from .post_test_processor import PostRunTests -from .generator import TestGenerator from .runner import TestRunner -from ..report.generator import ReportGenerator -from ..report.summary import ResultSummarizer -from ..logger import logger, console +from ..logger import logger from ..parsers import SwaggerParser, OpenAPIv3Parser -# create tester objs -test_generator = TestGenerator() - - def is_host_up(openapi_parser: SwaggerParser | OpenAPIv3Parser) -> bool: '''checks whether the host from openapi doc is available or not. Returns True is host is available else returns False''' @@ -128,335 +122,3 @@ def reduce_data_list(data_list: list[dict] | str) -> list[dict] | str: ] return data_list - - -# Note: redirects are allowed by default making it easier for pentesters/researchers -def generate_and_run_tests( - api_parser: SwaggerParser | OpenAPIv3Parser, - regex_pattern: str | None = None, - output_file: str | None = None, - output_file_format: str | None = None, - rate_limit: int | None = None, - req_headers: dict | None = None, - proxies: list[str] | None = None, - test_data_config: dict | None = None, - ssl: bool = False, - capture_failed: bool = False, - remove_unused_data: bool = True, -): - """ - Generates and runs tests for the provided OAS/Swagger file. - - Args: - api_parser: An instance of SwaggerParser or OpenAPIv3Parser representing the parsed API specification. - regex_pattern: A string representing the regex pattern to match against the response body (optional). - output_file: A string representing the path to the output file (optional). - output_file_format: A string representing the format of the output file (optional). - rate_limit: An integer representing the rate limit for the tests (optional). - req_headers: A dictionary representing the request headers (optional). - proxies: A list of strings representing the proxies to be used (optional). - test_data_config: A dictionary representing the configuration for user-provided test data (optional). - ssl: A boolean indicating whether to use SSL for the requests (default: False). - capture_failed: A boolean indicating whether to capture failed tests in the report (default: False). - remove_unused_data: A boolean indicating whether to remove unused data (default: True). - - Returns: - A list of test results. - """ - if not is_host_up(openapi_parser=api_parser): - logger.error( - 'Stopping tests due to unavailibility of host: %s', api_parser.host - ) - return - - logger.info('Host %s is up', api_parser.host) - - test_runner = TestRunner( - rate_limit=rate_limit, - headers=req_headers, - proxies=proxies, - ssl=ssl, - ) - - results: list = [] - - # test for unsupported http methods - test_name = 'Checking for Unsupported HTTP Methods/Verbs' - logger.info(test_name) - unsupported_http_endpoint_tests = test_generator.check_unsupported_http_methods( - api_parser - ) - - results += run_test( - test_runner=test_runner, - tests=unsupported_http_endpoint_tests, - regex_pattern=regex_pattern, - description=f'(FUZZED) {test_name}', - ) - - # sqli fuzz test - test_name = 'Checking for SQLi vulnerability' - logger.info(test_name) - sqli_fuzz_tests = test_generator.sqli_fuzz_params_test(api_parser) - results += run_test( - test_runner=test_runner, - tests=sqli_fuzz_tests, - regex_pattern=regex_pattern, - description=f'(FUZZED) {test_name}', - ) - - test_name = 'Checking for SQLi vulnerability in URI Path' - logger.info(test_name) - sqli_fuzz_tests = test_generator.sqli_in_uri_path_fuzz_test(api_parser) - results += run_test( - test_runner=test_runner, - tests=sqli_fuzz_tests, - regex_pattern=regex_pattern, - description=f'(FUZZED) {test_name}', - ) - - # OS Command Injection Fuzz Test - test_name = 'Checking for OS Command Injection Vulnerability with fuzzed params and checking response body' # noqa: E501 - logger.info(test_name) - os_command_injection_tests = test_generator.os_command_injection_fuzz_params_test( - api_parser - ) - results += run_test( - test_runner=test_runner, - tests=os_command_injection_tests, - regex_pattern=regex_pattern, - post_run_matcher_test=True, - description='(FUZZED) Checking for OS Command Injection', - ) - - # XSS/HTML Injection Fuzz Test - test_name = 'Checking for XSS/HTML Injection Vulnerability with fuzzed params and checking response body' # noqa: E501 - logger.info(test_name) - xss_injection_tests = test_generator.xss_html_injection_fuzz_params_test(api_parser) - results += run_test( - test_runner=test_runner, - tests=xss_injection_tests, - regex_pattern=regex_pattern, - post_run_matcher_test=True, - description='(FUZZED) Checking for XSS/HTML Injection', - ) - - # BOLA path tests with fuzzed data - test_name = 'Checking for BOLA in PATH using fuzzed params' - logger.info(test_name) - bola_fuzzed_path_tests = test_generator.bola_fuzz_path_test( - api_parser, success_codes=[200, 201, 301] - ) - results += run_test( - test_runner=test_runner, - tests=bola_fuzzed_path_tests, - regex_pattern=regex_pattern, - description='(FUZZED) Checking for BOLA in PATH:', - ) - - # BOLA path test with fuzzed data + trailing slash - test_name = ( - 'Checking for BOLA in PATH with trailing slash and id using fuzzed params' - ) - logger.info(test_name) - bola_trailing_slash_path_tests = test_generator.bola_fuzz_trailing_slash_path_test( - api_parser, success_codes=[200, 201, 301] - ) - results += run_test( - test_runner=test_runner, - tests=bola_trailing_slash_path_tests, - regex_pattern=regex_pattern, - description='(FUZZED) Checking for BOLA in PATH with trailing slash', - ) - - # Mass Assignment / BOPLA - test_name = 'Checking for Mass Assignment Vulnerability with fuzzed params and checking response status codes:' # noqa: E501 - logger.info(test_name) - bopla_tests = test_generator.bopla_fuzz_test( - api_parser, success_codes=[200, 201, 301] - ) - results += run_test( - test_runner=test_runner, - tests=bopla_tests, - regex_pattern=regex_pattern, - description='(FUZZED) Checking for BOPLA/Mass Assignment Vulnerability', - ) - - # SSTI Vulnerability - test_name = 'Checking for SSTI vulnerability with fuzzed params and checking response body' # noqa: E501 - logger.info(test_name) - ssti_tests = test_generator.ssti_fuzz_params_test(api_parser) - results += run_test( - test_runner=test_runner, - tests=ssti_tests, - regex_pattern=regex_pattern, - description='(FUZZED) Checking for SSTI Vulnerability', - post_run_matcher_test=True, - ) - - # Missing Authorization Test - test_name = 'Checking for Missing Authorization' - logger.info(test_name) - missing_auth_tests = test_generator.missing_auth_fuzz_test(api_parser) - results += run_test( - test_runner=test_runner, - tests=missing_auth_tests, - regex_pattern=regex_pattern, - description=f'(FUZZED) {test_name}', - post_run_matcher_test=False, - ) - - # Tests with User provided Data - if bool(test_data_config): - logger.info('[bold] Testing with user provided data [/bold]') - - # # BOLA path tests with fuzzed + user provided data - test_name = 'Checking for BOLA in PATH using fuzzed and user provided params' - logger.info(test_name) - bola_fuzzed_user_data_tests = test_generator.test_with_user_data( - test_data_config, - test_generator.bola_fuzz_path_test, - openapi_parser=api_parser, - success_codes=[200, 201, 301], - ) - results += run_test( - test_runner=test_runner, - tests=bola_fuzzed_user_data_tests, - regex_pattern=regex_pattern, - description='(USER + FUZZED) Checking for BOLA in PATH', - ) - - # BOLA path test with fuzzed + user data + trailing slash - test_name = 'Checking for BOLA in PATH with trailing slash id using fuzzed and user provided params:' # noqa: E501 - logger.info(test_name) - bola_trailing_slash_path_user_data_tests = test_generator.test_with_user_data( - test_data_config, - test_generator.bola_fuzz_trailing_slash_path_test, - openapi_parser=api_parser, - success_codes=[200, 201, 301], - ) - results += run_test( - test_runner=test_runner, - tests=bola_trailing_slash_path_user_data_tests, - regex_pattern=regex_pattern, - description='(USER + FUZZED) Checking for BOLA in PATH with trailing slash', - ) - - # OS Command Injection Fuzz Test - test_name = 'Checking for OS Command Injection Vulnerability with fuzzed & user params and checking response body' # noqa: E501 - logger.info(test_name) - os_command_injection_with_user_data_tests = test_generator.test_with_user_data( - test_data_config, - test_generator.os_command_injection_fuzz_params_test, - openapi_parser=api_parser, - ) - results += run_test( - test_runner=test_runner, - tests=os_command_injection_with_user_data_tests, - regex_pattern=regex_pattern, - post_run_matcher_test=True, - description='(USER + FUZZED) Checking for OS Command Injection Vulnerability:', - ) - - # XSS/HTML Injection Fuzz Test - test_name = 'Checking for XSS/HTML Injection Vulnerability with fuzzed & user params and checking response body' # noqa: E501 - logger.info(test_name) - os_command_injection_with_user_data_tests = test_generator.test_with_user_data( - test_data_config, - test_generator.xss_html_injection_fuzz_params_test, - openapi_parser=api_parser, - ) - results += run_test( - test_runner=test_runner, - tests=os_command_injection_with_user_data_tests, - regex_pattern=regex_pattern, - post_run_matcher_test=True, - description='(USER + FUZZED) Checking for XSS/HTML Injection Vulnerability', - ) - - # STTI Vulnerability - test_name = 'Checking for SSTI vulnerability with fuzzed params & user data and checking response body' # noqa: E501 - logger.info(test_name) - ssti_with_user_data_tests = test_generator.test_with_user_data( - test_data_config, - test_generator.ssti_fuzz_params_test, - openapi_parser=api_parser, - ) - results += run_test( - test_runner=test_runner, - tests=ssti_with_user_data_tests, - regex_pattern=regex_pattern, - description='(USER + FUZZED) Checking for SSTI Vulnerability', - post_run_matcher_test=True, - ) - - # Missing Authorization Test - test_name = 'Checking for Missing Authorization with user data' - logger.info(test_name) - missing_auth_tests = test_generator.test_with_user_data( - test_data_config, - test_generator.missing_auth_fuzz_test, - openapi_parser=api_parser, - ) - results += run_test( - test_runner=test_runner, - tests=missing_auth_tests, - regex_pattern=regex_pattern, - description=f'(USER + FUZZED) {test_name}', - post_run_matcher_test=False, - ) - - # Broken Access Control Test - test_name = 'Checking for Broken Access Control' - logger.info(test_name) - bac_results = PostRunTests.run_broken_access_control_tests( - results, test_data_config - ) - results += run_test( - test_runner=test_runner, - tests=bac_results, - regex_pattern=regex_pattern, - skip_test_run=True, - description=test_name, - ) - - if remove_unused_data: - for result in results: - result.pop('kwargs', None) - result.pop('args', None) - - result['body_params'] = reduce_data_list(result.get('body_params', [{}])) - result['query_params'] = reduce_data_list(result.get('query_params', [{}])) - result['path_params'] = reduce_data_list(result.get('path_params', [{}])) - result['malicious_payload'] = reduce_data_list( - result.get('malicious_payload', []) - ) - - # save file to output if output flag is present - if output_file_format != 'table': - ReportGenerator.generate_report( - results=results, - report_format=output_file_format, - report_path=output_file, - capture_failed=capture_failed, - ) - - ReportGenerator.generate_report( - results=results, - report_format='table', - report_path=None, - capture_failed=capture_failed, - ) - - console.print( - "The columns for 'data_leak' and 'result' in the table represent independent aspects. It's possible for there to be a data leak in the endpoint, yet the result for that endpoint may still be marked as 'Success'. This is because the 'result' column doesn't necessarily reflect the overall test result; it may indicate success even in the presence of a data leak." - ) - - console.rule() - result_summary = ResultSummarizer.generate_count_summary( - results, table_title='Results Summary' - ) - - console.print(result_summary) - - return results diff --git a/src/offat/utils.py b/src/offat/utils.py index 443da5d..eeb0198 100644 --- a/src/offat/utils.py +++ b/src/offat/utils.py @@ -3,11 +3,12 @@ """ from json import loads as json_load, dumps as json_dumps, JSONDecodeError from re import compile as re_compile, match -from urllib.parse import urlparse, urljoin +from urllib.parse import urlparse, urljoin, quote_plus from os.path import isfile from importlib.metadata import version from yaml import safe_load, YAMLError + from .logger import logger @@ -320,3 +321,49 @@ def get_unique_params(list1: list[dict], list2: list[dict]) -> list[dict]: unique_params_names.append(param_name) return unique_params + + +def result_to_curl(result: dict): + """ + Converts a dictionary representing an HTTP request to a cURL command. + + Args: + result (dict): A dictionary containing the details of the HTTP request. + + Returns: + str: The cURL command generated from the given request details. + """ + url = result.get('url') + method = result.get('method') + query_params = result.get('query_params', None) + request_headers: dict = result.get('request_headers', {}) + body_params: dict = result.get('body_params', {}) + + # generate query params + query_param_str = ( + '&'.join( + [ + f'{param.get("name")}={quote_plus(str(param.get("value")))}' + for param in query_params + ] + ) + if query_params + else '' + ) + + # generate headers str + if isinstance(request_headers, dict): + request_headers.pop('Content-Length', None) + request_headers_str = ( + '-H '.join([f'"{hkey}: {hval}" ' for hkey, hval in request_headers.items()]) + if request_headers + else '' + ) + + # generate JSON body params + body = {bparam.get('name'): bparam.get('value') for bparam in body_params} + body_str = f"-d '{json_dumps(body)}'" if body else '' + + curl_command = f"curl -X {method} \"{url}?{query_param_str}\" {request_headers_str} {body_str}".strip() + + return curl_command diff --git a/src/poetry.lock b/src/poetry.lock index 5b50680..bcc9f1b 100644 --- a/src/poetry.lock +++ b/src/poetry.lock @@ -1159,111 +1159,111 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rpds-py" -version = "0.18.0" +version = "0.18.1" description = "Python bindings to Rust's persistent data structures (rpds)" category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "rpds_py-0.18.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5b4e7d8d6c9b2e8ee2d55c90b59c707ca59bc30058269b3db7b1f8df5763557e"}, - {file = "rpds_py-0.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c463ed05f9dfb9baebef68048aed8dcdc94411e4bf3d33a39ba97e271624f8f7"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01e36a39af54a30f28b73096dd39b6802eddd04c90dbe161c1b8dbe22353189f"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d62dec4976954a23d7f91f2f4530852b0c7608116c257833922a896101336c51"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd18772815d5f008fa03d2b9a681ae38d5ae9f0e599f7dda233c439fcaa00d40"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:923d39efa3cfb7279a0327e337a7958bff00cc447fd07a25cddb0a1cc9a6d2da"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39514da80f971362f9267c600b6d459bfbbc549cffc2cef8e47474fddc9b45b1"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a34d557a42aa28bd5c48a023c570219ba2593bcbbb8dc1b98d8cf5d529ab1434"}, - {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:93df1de2f7f7239dc9cc5a4a12408ee1598725036bd2dedadc14d94525192fc3"}, - {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:34b18ba135c687f4dac449aa5157d36e2cbb7c03cbea4ddbd88604e076aa836e"}, - {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0b5dcf9193625afd8ecc92312d6ed78781c46ecbf39af9ad4681fc9f464af88"}, - {file = "rpds_py-0.18.0-cp310-none-win32.whl", hash = "sha256:c4325ff0442a12113a6379af66978c3fe562f846763287ef66bdc1d57925d337"}, - {file = "rpds_py-0.18.0-cp310-none-win_amd64.whl", hash = "sha256:7223a2a5fe0d217e60a60cdae28d6949140dde9c3bcc714063c5b463065e3d66"}, - {file = "rpds_py-0.18.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3a96e0c6a41dcdba3a0a581bbf6c44bb863f27c541547fb4b9711fd8cf0ffad4"}, - {file = "rpds_py-0.18.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30f43887bbae0d49113cbaab729a112251a940e9b274536613097ab8b4899cf6"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcb25daa9219b4cf3a0ab24b0eb9a5cc8949ed4dc72acb8fa16b7e1681aa3c58"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d68c93e381010662ab873fea609bf6c0f428b6d0bb00f2c6939782e0818d37bf"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b34b7aa8b261c1dbf7720b5d6f01f38243e9b9daf7e6b8bc1fd4657000062f2c"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e6d75ab12b0bbab7215e5d40f1e5b738aa539598db27ef83b2ec46747df90e1"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8612cd233543a3781bc659c731b9d607de65890085098986dfd573fc2befe5"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aec493917dd45e3c69d00a8874e7cbed844efd935595ef78a0f25f14312e33c6"}, - {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:661d25cbffaf8cc42e971dd570d87cb29a665f49f4abe1f9e76be9a5182c4688"}, - {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1df3659d26f539ac74fb3b0c481cdf9d725386e3552c6fa2974f4d33d78e544b"}, - {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1ce3ba137ed54f83e56fb983a5859a27d43a40188ba798993812fed73c70836"}, - {file = "rpds_py-0.18.0-cp311-none-win32.whl", hash = "sha256:69e64831e22a6b377772e7fb337533c365085b31619005802a79242fee620bc1"}, - {file = "rpds_py-0.18.0-cp311-none-win_amd64.whl", hash = "sha256:998e33ad22dc7ec7e030b3df701c43630b5bc0d8fbc2267653577e3fec279afa"}, - {file = "rpds_py-0.18.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7f2facbd386dd60cbbf1a794181e6aa0bd429bd78bfdf775436020172e2a23f0"}, - {file = "rpds_py-0.18.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d9a5be316c15ffb2b3c405c4ff14448c36b4435be062a7f578ccd8b01f0c4d8"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd5bf1af8efe569654bbef5a3e0a56eca45f87cfcffab31dd8dde70da5982475"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5417558f6887e9b6b65b4527232553c139b57ec42c64570569b155262ac0754f"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56a737287efecafc16f6d067c2ea0117abadcd078d58721f967952db329a3e5c"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f03bccbd8586e9dd37219bce4d4e0d3ab492e6b3b533e973fa08a112cb2ffc9"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4457a94da0d5c53dc4b3e4de1158bdab077db23c53232f37a3cb7afdb053a4e3"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ab39c1ba9023914297dd88ec3b3b3c3f33671baeb6acf82ad7ce883f6e8e157"}, - {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9d54553c1136b50fd12cc17e5b11ad07374c316df307e4cfd6441bea5fb68496"}, - {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0af039631b6de0397ab2ba16eaf2872e9f8fca391b44d3d8cac317860a700a3f"}, - {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84ffab12db93b5f6bad84c712c92060a2d321b35c3c9960b43d08d0f639d60d7"}, - {file = "rpds_py-0.18.0-cp312-none-win32.whl", hash = "sha256:685537e07897f173abcf67258bee3c05c374fa6fff89d4c7e42fb391b0605e98"}, - {file = "rpds_py-0.18.0-cp312-none-win_amd64.whl", hash = "sha256:e003b002ec72c8d5a3e3da2989c7d6065b47d9eaa70cd8808b5384fbb970f4ec"}, - {file = "rpds_py-0.18.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:08f9ad53c3f31dfb4baa00da22f1e862900f45908383c062c27628754af2e88e"}, - {file = "rpds_py-0.18.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0013fe6b46aa496a6749c77e00a3eb07952832ad6166bd481c74bda0dcb6d58"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e32a92116d4f2a80b629778280103d2a510a5b3f6314ceccd6e38006b5e92dcb"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e541ec6f2ec456934fd279a3120f856cd0aedd209fc3852eca563f81738f6861"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bed88b9a458e354014d662d47e7a5baafd7ff81c780fd91584a10d6ec842cb73"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2644e47de560eb7bd55c20fc59f6daa04682655c58d08185a9b95c1970fa1e07"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e8916ae4c720529e18afa0b879473049e95949bf97042e938530e072fde061d"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:465a3eb5659338cf2a9243e50ad9b2296fa15061736d6e26240e713522b6235c"}, - {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ea7d4a99f3b38c37eac212dbd6ec42b7a5ec51e2c74b5d3223e43c811609e65f"}, - {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:67071a6171e92b6da534b8ae326505f7c18022c6f19072a81dcf40db2638767c"}, - {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:41ef53e7c58aa4ef281da975f62c258950f54b76ec8e45941e93a3d1d8580594"}, - {file = "rpds_py-0.18.0-cp38-none-win32.whl", hash = "sha256:fdea4952db2793c4ad0bdccd27c1d8fdd1423a92f04598bc39425bcc2b8ee46e"}, - {file = "rpds_py-0.18.0-cp38-none-win_amd64.whl", hash = "sha256:7cd863afe7336c62ec78d7d1349a2f34c007a3cc6c2369d667c65aeec412a5b1"}, - {file = "rpds_py-0.18.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5307def11a35f5ae4581a0b658b0af8178c65c530e94893345bebf41cc139d33"}, - {file = "rpds_py-0.18.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77f195baa60a54ef9d2de16fbbfd3ff8b04edc0c0140a761b56c267ac11aa467"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39f5441553f1c2aed4de4377178ad8ff8f9d733723d6c66d983d75341de265ab"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a00312dea9310d4cb7dbd7787e722d2e86a95c2db92fbd7d0155f97127bcb40"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f2fc11e8fe034ee3c34d316d0ad8808f45bc3b9ce5857ff29d513f3ff2923a1"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:586f8204935b9ec884500498ccc91aa869fc652c40c093bd9e1471fbcc25c022"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddc2f4dfd396c7bfa18e6ce371cba60e4cf9d2e5cdb71376aa2da264605b60b9"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ddcba87675b6d509139d1b521e0c8250e967e63b5909a7e8f8944d0f90ff36f"}, - {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7bd339195d84439cbe5771546fe8a4e8a7a045417d8f9de9a368c434e42a721e"}, - {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d7c36232a90d4755b720fbd76739d8891732b18cf240a9c645d75f00639a9024"}, - {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6b0817e34942b2ca527b0e9298373e7cc75f429e8da2055607f4931fded23e20"}, - {file = "rpds_py-0.18.0-cp39-none-win32.whl", hash = "sha256:99f70b740dc04d09e6b2699b675874367885217a2e9f782bdf5395632ac663b7"}, - {file = "rpds_py-0.18.0-cp39-none-win_amd64.whl", hash = "sha256:6ef687afab047554a2d366e112dd187b62d261d49eb79b77e386f94644363294"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad36cfb355e24f1bd37cac88c112cd7730873f20fb0bdaf8ba59eedf8216079f"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:36b3ee798c58ace201289024b52788161e1ea133e4ac93fba7d49da5fec0ef9e"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8a2f084546cc59ea99fda8e070be2fd140c3092dc11524a71aa8f0f3d5a55ca"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e4461d0f003a0aa9be2bdd1b798a041f177189c1a0f7619fe8c95ad08d9a45d7"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8db715ebe3bb7d86d77ac1826f7d67ec11a70dbd2376b7cc214199360517b641"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793968759cd0d96cac1e367afd70c235867831983f876a53389ad869b043c948"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66e6a3af5a75363d2c9a48b07cb27c4ea542938b1a2e93b15a503cdfa8490795"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ef0befbb5d79cf32d0266f5cff01545602344eda89480e1dd88aca964260b18"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d4acf42190d449d5e89654d5c1ed3a4f17925eec71f05e2a41414689cda02d1"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:a5f446dd5055667aabaee78487f2b5ab72e244f9bc0b2ffebfeec79051679984"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9dbbeb27f4e70bfd9eec1be5477517365afe05a9b2c441a0b21929ee61048124"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:22806714311a69fd0af9b35b7be97c18a0fc2826e6827dbb3a8c94eac6cf7eeb"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b34ae4636dfc4e76a438ab826a0d1eed2589ca7d9a1b2d5bb546978ac6485461"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c8370641f1a7f0e0669ddccca22f1da893cef7628396431eb445d46d893e5cd"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c8362467a0fdeccd47935f22c256bec5e6abe543bf0d66e3d3d57a8fb5731863"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11a8c85ef4a07a7638180bf04fe189d12757c696eb41f310d2426895356dcf05"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b316144e85316da2723f9d8dc75bada12fa58489a527091fa1d5a612643d1a0e"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf1ea2e34868f6fbf070e1af291c8180480310173de0b0c43fc38a02929fc0e3"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e546e768d08ad55b20b11dbb78a745151acbd938f8f00d0cfbabe8b0199b9880"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4901165d170a5fde6f589acb90a6b33629ad1ec976d4529e769c6f3d885e3e80"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:618a3d6cae6ef8ec88bb76dd80b83cfe415ad4f1d942ca2a903bf6b6ff97a2da"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ed4eb745efbff0a8e9587d22a84be94a5eb7d2d99c02dacf7bd0911713ed14dd"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c81e5f372cd0dc5dc4809553d34f832f60a46034a5f187756d9b90586c2c307"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:43fbac5f22e25bee1d482c97474f930a353542855f05c1161fd804c9dc74a09d"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d7faa6f14017c0b1e69f5e2c357b998731ea75a442ab3841c0dbbbfe902d2c4"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:08231ac30a842bd04daabc4d71fddd7e6d26189406d5a69535638e4dcb88fe76"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:044a3e61a7c2dafacae99d1e722cc2d4c05280790ec5a05031b3876809d89a5c"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f26b5bd1079acdb0c7a5645e350fe54d16b17bfc5e71f371c449383d3342e17"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:482103aed1dfe2f3b71a58eff35ba105289b8d862551ea576bd15479aba01f66"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1374f4129f9bcca53a1bba0bb86bf78325a0374577cf7e9e4cd046b1e6f20e24"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:635dc434ff724b178cb192c70016cc0ad25a275228f749ee0daf0eddbc8183b1"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:bc362ee4e314870a70f4ae88772d72d877246537d9f8cb8f7eacf10884862432"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4832d7d380477521a8c1644bbab6588dfedea5e30a7d967b5fb75977c45fd77f"}, - {file = "rpds_py-0.18.0.tar.gz", hash = "sha256:42821446ee7a76f5d9f71f9e33a4fb2ffd724bb3e7f93386150b61a43115788d"}, + {file = "rpds_py-0.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d31dea506d718693b6b2cffc0648a8929bdc51c70a311b2770f09611caa10d53"}, + {file = "rpds_py-0.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:732672fbc449bab754e0b15356c077cc31566df874964d4801ab14f71951ea80"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a98a1f0552b5f227a3d6422dbd61bc6f30db170939bd87ed14f3c339aa6c7c9"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f1944ce16401aad1e3f7d312247b3d5de7981f634dc9dfe90da72b87d37887d"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38e14fb4e370885c4ecd734f093a2225ee52dc384b86fa55fe3f74638b2cfb09"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08d74b184f9ab6289b87b19fe6a6d1a97fbfea84b8a3e745e87a5de3029bf944"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d70129cef4a8d979caa37e7fe957202e7eee8ea02c5e16455bc9808a59c6b2f0"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce0bb20e3a11bd04461324a6a798af34d503f8d6f1aa3d2aa8901ceaf039176d"}, + {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81c5196a790032e0fc2464c0b4ab95f8610f96f1f2fa3d4deacce6a79852da60"}, + {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f3027be483868c99b4985fda802a57a67fdf30c5d9a50338d9db646d590198da"}, + {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d44607f98caa2961bab4fa3c4309724b185b464cdc3ba6f3d7340bac3ec97cc1"}, + {file = "rpds_py-0.18.1-cp310-none-win32.whl", hash = "sha256:c273e795e7a0f1fddd46e1e3cb8be15634c29ae8ff31c196debb620e1edb9333"}, + {file = "rpds_py-0.18.1-cp310-none-win_amd64.whl", hash = "sha256:8352f48d511de5f973e4f2f9412736d7dea76c69faa6d36bcf885b50c758ab9a"}, + {file = "rpds_py-0.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6b5ff7e1d63a8281654b5e2896d7f08799378e594f09cf3674e832ecaf396ce8"}, + {file = "rpds_py-0.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8927638a4d4137a289e41d0fd631551e89fa346d6dbcfc31ad627557d03ceb6d"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:154bf5c93d79558b44e5b50cc354aa0459e518e83677791e6adb0b039b7aa6a7"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07f2139741e5deb2c5154a7b9629bc5aa48c766b643c1a6750d16f865a82c5fc"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c7672e9fba7425f79019db9945b16e308ed8bc89348c23d955c8c0540da0a07"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:489bdfe1abd0406eba6b3bb4fdc87c7fa40f1031de073d0cfb744634cc8fa261"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c20f05e8e3d4fc76875fc9cb8cf24b90a63f5a1b4c5b9273f0e8225e169b100"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:967342e045564cef76dfcf1edb700b1e20838d83b1aa02ab313e6a497cf923b8"}, + {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2cc7c1a47f3a63282ab0f422d90ddac4aa3034e39fc66a559ab93041e6505da7"}, + {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f7afbfee1157e0f9376c00bb232e80a60e59ed716e3211a80cb8506550671e6e"}, + {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e6934d70dc50f9f8ea47081ceafdec09245fd9f6032669c3b45705dea096b88"}, + {file = "rpds_py-0.18.1-cp311-none-win32.whl", hash = "sha256:c69882964516dc143083d3795cb508e806b09fc3800fd0d4cddc1df6c36e76bb"}, + {file = "rpds_py-0.18.1-cp311-none-win_amd64.whl", hash = "sha256:70a838f7754483bcdc830444952fd89645569e7452e3226de4a613a4c1793fb2"}, + {file = "rpds_py-0.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3dd3cd86e1db5aadd334e011eba4e29d37a104b403e8ca24dcd6703c68ca55b3"}, + {file = "rpds_py-0.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05f3d615099bd9b13ecf2fc9cf2d839ad3f20239c678f461c753e93755d629ee"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35b2b771b13eee8729a5049c976197ff58a27a3829c018a04341bcf1ae409b2b"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee17cd26b97d537af8f33635ef38be873073d516fd425e80559f4585a7b90c43"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b646bf655b135ccf4522ed43d6902af37d3f5dbcf0da66c769a2b3938b9d8184"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19ba472b9606c36716062c023afa2484d1e4220548751bda14f725a7de17b4f6"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e30ac5e329098903262dc5bdd7e2086e0256aa762cc8b744f9e7bf2a427d3f8"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d58ad6317d188c43750cb76e9deacf6051d0f884d87dc6518e0280438648a9ac"}, + {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e1735502458621921cee039c47318cb90b51d532c2766593be6207eec53e5c4c"}, + {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f5bab211605d91db0e2995a17b5c6ee5edec1270e46223e513eaa20da20076ac"}, + {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2fc24a329a717f9e2448f8cd1f960f9dac4e45b6224d60734edeb67499bab03a"}, + {file = "rpds_py-0.18.1-cp312-none-win32.whl", hash = "sha256:1805d5901779662d599d0e2e4159d8a82c0b05faa86ef9222bf974572286b2b6"}, + {file = "rpds_py-0.18.1-cp312-none-win_amd64.whl", hash = "sha256:720edcb916df872d80f80a1cc5ea9058300b97721efda8651efcd938a9c70a72"}, + {file = "rpds_py-0.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:c827576e2fa017a081346dce87d532a5310241648eb3700af9a571a6e9fc7e74"}, + {file = "rpds_py-0.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:aa3679e751408d75a0b4d8d26d6647b6d9326f5e35c00a7ccd82b78ef64f65f8"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0abeee75434e2ee2d142d650d1e54ac1f8b01e6e6abdde8ffd6eeac6e9c38e20"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed402d6153c5d519a0faf1bb69898e97fb31613b49da27a84a13935ea9164dfc"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:338dee44b0cef8b70fd2ef54b4e09bb1b97fc6c3a58fea5db6cc083fd9fc2724"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7750569d9526199c5b97e5a9f8d96a13300950d910cf04a861d96f4273d5b104"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:607345bd5912aacc0c5a63d45a1f73fef29e697884f7e861094e443187c02be5"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:207c82978115baa1fd8d706d720b4a4d2b0913df1c78c85ba73fe6c5804505f0"}, + {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6d1e42d2735d437e7e80bab4d78eb2e459af48c0a46e686ea35f690b93db792d"}, + {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5463c47c08630007dc0fe99fb480ea4f34a89712410592380425a9b4e1611d8e"}, + {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:06d218939e1bf2ca50e6b0ec700ffe755e5216a8230ab3e87c059ebb4ea06afc"}, + {file = "rpds_py-0.18.1-cp38-none-win32.whl", hash = "sha256:312fe69b4fe1ffbe76520a7676b1e5ac06ddf7826d764cc10265c3b53f96dbe9"}, + {file = "rpds_py-0.18.1-cp38-none-win_amd64.whl", hash = "sha256:9437ca26784120a279f3137ee080b0e717012c42921eb07861b412340f85bae2"}, + {file = "rpds_py-0.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:19e515b78c3fc1039dd7da0a33c28c3154458f947f4dc198d3c72db2b6b5dc93"}, + {file = "rpds_py-0.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7b28c5b066bca9a4eb4e2f2663012debe680f097979d880657f00e1c30875a0"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:673fdbbf668dd958eff750e500495ef3f611e2ecc209464f661bc82e9838991e"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d960de62227635d2e61068f42a6cb6aae91a7fe00fca0e3aeed17667c8a34611"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352a88dc7892f1da66b6027af06a2e7e5d53fe05924cc2cfc56495b586a10b72"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e0ee01ad8260184db21468a6e1c37afa0529acc12c3a697ee498d3c2c4dcaf3"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4c39ad2f512b4041343ea3c7894339e4ca7839ac38ca83d68a832fc8b3748ab"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aaa71ee43a703c321906813bb252f69524f02aa05bf4eec85f0c41d5d62d0f4c"}, + {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6cd8098517c64a85e790657e7b1e509b9fe07487fd358e19431cb120f7d96338"}, + {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4adec039b8e2928983f885c53b7cc4cda8965b62b6596501a0308d2703f8af1b"}, + {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32b7daaa3e9389db3695964ce8e566e3413b0c43e3394c05e4b243a4cd7bef26"}, + {file = "rpds_py-0.18.1-cp39-none-win32.whl", hash = "sha256:2625f03b105328729f9450c8badda34d5243231eef6535f80064d57035738360"}, + {file = "rpds_py-0.18.1-cp39-none-win_amd64.whl", hash = "sha256:bf18932d0003c8c4d51a39f244231986ab23ee057d235a12b2684ea26a353590"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cbfbea39ba64f5e53ae2915de36f130588bba71245b418060ec3330ebf85678e"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a3d456ff2a6a4d2adcdf3c1c960a36f4fd2fec6e3b4902a42a384d17cf4e7a65"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7700936ef9d006b7ef605dc53aa364da2de5a3aa65516a1f3ce73bf82ecfc7ae"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:51584acc5916212e1bf45edd17f3a6b05fe0cbb40482d25e619f824dccb679de"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:942695a206a58d2575033ff1e42b12b2aece98d6003c6bc739fbf33d1773b12f"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b906b5f58892813e5ba5c6056d6a5ad08f358ba49f046d910ad992196ea61397"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f8e3fecca256fefc91bb6765a693d96692459d7d4c644660a9fff32e517843"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7732770412bab81c5a9f6d20aeb60ae943a9b36dcd990d876a773526468e7163"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:bd1105b50ede37461c1d51b9698c4f4be6e13e69a908ab7751e3807985fc0346"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:618916f5535784960f3ecf8111581f4ad31d347c3de66d02e728de460a46303c"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:17c6d2155e2423f7e79e3bb18151c686d40db42d8645e7977442170c360194d4"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c4c4c3f878df21faf5fac86eda32671c27889e13570645a9eea0a1abdd50922"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:fab6ce90574645a0d6c58890e9bcaac8d94dff54fb51c69e5522a7358b80ab64"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:531796fb842b53f2695e94dc338929e9f9dbf473b64710c28af5a160b2a8927d"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:740884bc62a5e2bbb31e584f5d23b32320fd75d79f916f15a788d527a5e83644"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:998125738de0158f088aef3cb264a34251908dd2e5d9966774fdab7402edfab7"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2be6e9dd4111d5b31ba3b74d17da54a8319d8168890fbaea4b9e5c3de630ae5"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0cee71bc618cd93716f3c1bf56653740d2d13ddbd47673efa8bf41435a60daa"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c3caec4ec5cd1d18e5dd6ae5194d24ed12785212a90b37f5f7f06b8bedd7139"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:27bba383e8c5231cd559affe169ca0b96ec78d39909ffd817f28b166d7ddd4d8"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:a888e8bdb45916234b99da2d859566f1e8a1d2275a801bb8e4a9644e3c7e7909"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6031b25fb1b06327b43d841f33842b383beba399884f8228a6bb3df3088485ff"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48c2faaa8adfacefcbfdb5f2e2e7bdad081e5ace8d182e5f4ade971f128e6bb3"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d85164315bd68c0806768dc6bb0429c6f95c354f87485ee3593c4f6b14def2bd"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6afd80f6c79893cfc0574956f78a0add8c76e3696f2d6a15bca2c66c415cf2d4"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa242ac1ff583e4ec7771141606aafc92b361cd90a05c30d93e343a0c2d82a89"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21be4770ff4e08698e1e8e0bce06edb6ea0626e7c8f560bc08222880aca6a6f"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c45a639e93a0c5d4b788b2613bd637468edd62f8f95ebc6fcc303d58ab3f0a8"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910e71711d1055b2768181efa0a17537b2622afeb0424116619817007f8a2b10"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b9bb1f182a97880f6078283b3505a707057c42bf55d8fca604f70dedfdc0772a"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d54f74f40b1f7aaa595a02ff42ef38ca654b1469bef7d52867da474243cc633"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:8d2e182c9ee01135e11e9676e9a62dfad791a7a467738f06726872374a83db49"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:636a15acc588f70fda1661234761f9ed9ad79ebed3f2125d44be0862708b666e"}, + {file = "rpds_py-0.18.1.tar.gz", hash = "sha256:dc48b479d540770c811fbd1eb9ba2bb66951863e448efec2e2c102625328e92f"}, ] [[package]] @@ -1343,18 +1343,19 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7 [[package]] name = "tenacity" -version = "8.2.3" +version = "8.3.0" description = "Retry code until it succeeds" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tenacity-8.2.3-py3-none-any.whl", hash = "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c"}, - {file = "tenacity-8.2.3.tar.gz", hash = "sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a"}, + {file = "tenacity-8.3.0-py3-none-any.whl", hash = "sha256:3649f6443dbc0d9b01b9d8020a9c4ec7a1ff5f6f3c6c8a036ef371f573fe9185"}, + {file = "tenacity-8.3.0.tar.gz", hash = "sha256:953d4e6ad24357bceffbc9707bc74349aca9d245f68eb65419cf0c249a1949a2"}, ] [package.extras] -doc = ["reno", "sphinx", "tornado (>=4.5)"] +doc = ["reno", "sphinx"] +test = ["pytest", "tornado (>=4.5)", "typeguard"] [[package]] name = "tomli" diff --git a/src/pyproject.toml b/src/pyproject.toml index 009f21e..e989c2c 100644 --- a/src/pyproject.toml +++ b/src/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "offat" -version = "0.17.5" +version = "0.18.0" description = "Offensive API tester tool automates checks for common API vulnerabilities" authors = ["Dhrumil Mistry "] license = "MIT"