Skip to content

Commit 131cc5f

Browse files
Merge pull request #110 from OWASP/dev
Dev RELEASE: v0.18.0
2 parents ead2035 + 335e327 commit 131cc5f

File tree

14 files changed

+635
-568
lines changed

14 files changed

+635
-568
lines changed

src/offat/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from .parsers.openapi import OpenAPIv3Parser
44
from .parsers.swagger import SwaggerParser
55
from .config_data_handler import validate_config_file_data
6-
from .tester.tester_utils import generate_and_run_tests
6+
from .tester.handler import generate_and_run_tests
77
from .parsers import create_parser
88
from .utils import get_package_version, headers_list_to_dict, read_yaml
99

src/offat/api/jobs.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from sys import exc_info
22
from offat.api.models import CreateScanModel
3-
from offat.tester.tester_utils import generate_and_run_tests
3+
from offat.tester.handler import generate_and_run_tests
44
from offat.parsers import create_parser
55
from offat.logger import logger
66

@@ -18,6 +18,6 @@ def scan_api(body_data: CreateScanModel):
1818
)
1919
return results
2020
except Exception as e:
21-
logger.error("Error occurred while creating a job: %s", repr(e))
22-
logger.debug("Debug Data:", exc_info=exc_info())
23-
return [{"error": str(e)}]
21+
logger.error('Error occurred while creating a job: %s', repr(e))
22+
logger.debug('Debug Data:', exc_info=exc_info())
23+
return [{'error': str(e)}]

src/offat/report/summary.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,26 +22,26 @@ def get_counts(results: list[dict], filter_errors: bool = False) -> dict[str, in
2222
dict: name (str) as key and its associated count (int)
2323
"""
2424
if filter_errors:
25-
results = list(filter(lambda result: result.get("error", False), results))
25+
results = list(filter(lambda result: result.get('error', False), results))
2626

2727
error_count = 0
2828
data_leak_count = 0
29-
failed_count = 0
30-
success_count = 0
29+
immune_count = 0
30+
vulnerable_count = 0
3131
for result in results:
32-
error_count += 1 if result.get("error", False) else 0
33-
data_leak_count += 1 if result.get("data_leak", False) else 0
32+
error_count += 1 if result.get('error', False) else 0
33+
data_leak_count += 1 if result.get('data_leak', False) else 0
3434

35-
if result.get("result"):
36-
success_count += 1
35+
if result.get('vulnerable'):
36+
vulnerable_count += 1
3737
else:
38-
failed_count += 1
38+
immune_count += 1
3939

4040
count_dict = {
41-
"errors": error_count,
42-
"data_leaks": data_leak_count,
43-
"failed": failed_count,
44-
"success": success_count,
41+
'errors': error_count,
42+
'data_leaks': data_leak_count,
43+
'immune': immune_count,
44+
'vulnerable': vulnerable_count,
4545
}
4646

4747
return count_dict
@@ -50,7 +50,7 @@ def get_counts(results: list[dict], filter_errors: bool = False) -> dict[str, in
5050
def generate_count_summary(
5151
results: list[dict],
5252
filter_errors: bool = False,
53-
output_format: str = "table",
53+
output_format: str = 'table',
5454
table_title: str | None = None,
5555
) -> Table | str:
5656
"""
@@ -70,8 +70,8 @@ def generate_count_summary(
7070
results=results, filter_errors=filter_errors
7171
)
7272
match output_format:
73-
case "markdown":
74-
output = ""
73+
case 'markdown':
74+
output = ''
7575
if table_title:
7676
output += f"**{table_title}**\n"
7777

@@ -80,8 +80,8 @@ def generate_count_summary(
8080

8181
case _: # table format
8282
output = Table(
83-
Column(header="⚔️", overflow="fold", justify="center"),
84-
Column(header="Endpoints Count", overflow="fold"),
83+
Column(header='⚔️', overflow='fold', justify='center'),
84+
Column(header='Endpoints Count', overflow='fold'),
8585
title=table_title,
8686
)
8787

src/offat/report/templates/table.py

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,33 @@ def _sanitize_results(
4848
):
4949
if filter_passed_results:
5050
results = list(
51-
filter(lambda x: not x.get('result') or x.get('data_leak'), results)
51+
filter(
52+
lambda result: result.get('vulnerable') or result.get('data_leak'),
53+
results,
54+
)
5255
)
5356

57+
keys_to_remove = [
58+
'url',
59+
'test_name',
60+
'response_filter',
61+
'body_params',
62+
'request_headers',
63+
'redirection',
64+
'query_params',
65+
'path_params',
66+
'curl_command',
67+
'response_match_regex',
68+
'regex_match_result',
69+
'success_codes',
70+
]
71+
5472
# remove keys based on conditions or update their values
5573
for result in results:
56-
if result['result']:
57-
result['result'] = '[bold green]Passed \u2713[/bold green]'
74+
if result['vulnerable']:
75+
result['vulnerable'] = '[bold red]True \u00d7[/bold red]'
5876
else:
59-
result['result'] = '[bold red]Failed \u00d7[/bold red]'
77+
result['vulnerable'] = '[bold green]False \u2713[/bold green]'
6078

6179
if not is_leaking_data:
6280
del result['response_headers']
@@ -66,15 +84,6 @@ def _sanitize_results(
6684
result['status_code'] = result.get('response_status_code')
6785
del result['response_status_code']
6886

69-
if result.get('success_codes'):
70-
del result['success_codes']
71-
72-
if result.get('regex_match_result'):
73-
del result['regex_match_result']
74-
75-
if result.get('response_match_regex'):
76-
del result['response_match_regex']
77-
7887
if result.get('security') or result.get('security') == []:
7988
del result['security']
8089

@@ -86,13 +95,8 @@ def _sanitize_results(
8695
if not isinstance(result.get('malicious_payload'), str):
8796
del result['malicious_payload']
8897

89-
del result['url']
90-
del result['test_name']
91-
del result['response_filter']
92-
del result['body_params']
93-
del result['request_headers']
94-
del result['redirection']
95-
del result['query_params']
96-
del result['path_params']
98+
for key in keys_to_remove:
99+
if key in result:
100+
del result[key]
97101

98102
return results

src/offat/tester/fuzzer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def fuzz_type_value(param_type: str, param_name: str):
9999
def fill_params(params: list[dict], is_v3: bool) -> list[dict]:
100100
"""fills params for OAS/swagger specs"""
101101
schema_params = []
102-
for index in range(len(params)):
102+
for index, _ in enumerate(params):
103103
param_type = (
104104
params[index].get("schema", {}).get("type")
105105
if is_v3
@@ -125,6 +125,7 @@ def fill_params(params: list[dict], is_v3: bool) -> list[dict]:
125125
"name": param_name,
126126
"required": param_is_required,
127127
"value": param_value,
128+
"type": param_type
128129
}
129130
]
130131

src/offat/tester/generator.py

Lines changed: 45 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,9 @@ def check_unsupported_http_methods(
122122
'malicious_payload': [],
123123
'args': args,
124124
'kwargs': kwargs,
125-
'result_details': {
126-
True: "Endpoint doesn't perform any HTTP verb which is not documented",
127-
False: 'Endpoint performs HTTP verb which is not documented',
125+
'vuln_details': {
126+
True: 'Endpoint performs HTTP verb which is not documented',
127+
False: "Endpoint doesn't perform any HTTP verb which is not documented",
128128
},
129129
'body_params': body_params,
130130
'query_params': query_params,
@@ -297,9 +297,9 @@ def sqli_fuzz_params_test(
297297

298298
request_obj['malicious_payload'] = sqli_payload
299299

300-
request_obj['result_details'] = {
301-
True: 'Parameters are not vulnerable to SQLi Payload', # passed
302-
False: 'One or more parameter is vulnerable to SQL Injection Attack', # failed
300+
request_obj['vuln_details'] = {
301+
True: 'One or more parameter is vulnerable to SQL Injection Attack',
302+
False: 'Parameters are not vulnerable to SQLi Payload',
303303
}
304304
request_obj['success_codes'] = success_codes
305305
request_obj[
@@ -404,9 +404,9 @@ def sqli_in_uri_path_fuzz_test(
404404
'malicious_payload': sqli_payload,
405405
'args': args,
406406
'kwargs': kwargs,
407-
'result_details': {
408-
True: 'Endpoint is not vulnerable to SQLi', # passed
409-
False: 'Endpoint might be vulnerable to SQli', # failed
407+
'vuln_details': {
408+
True: 'Endpoint might be vulnerable to SQli',
409+
False: 'Endpoint is not vulnerable to SQLi',
410410
},
411411
'success_codes': success_codes,
412412
'response_filter': PostTestFiltersEnum.STATUS_CODE_FILTER.name,
@@ -498,9 +498,9 @@ def bola_fuzz_path_test(
498498
'malicious_payload': path_params,
499499
'args': args,
500500
'kwargs': kwargs,
501-
'result_details': {
502-
True: 'Endpoint is not vulnerable to BOLA', # passed
503-
False: 'Endpoint might be vulnerable to BOLA', # failed
501+
'vuln_details': {
502+
True: 'Endpoint might be vulnerable to BOLA',
503+
False: 'Endpoint is not vulnerable to BOLA',
504504
},
505505
'success_codes': success_codes,
506506
'response_filter': PostTestFiltersEnum.STATUS_CODE_FILTER.name,
@@ -594,9 +594,9 @@ def bola_fuzz_trailing_slash_path_test(
594594
'malicious_payload': malicious_payload,
595595
'args': args,
596596
'kwargs': kwargs,
597-
'result_details': {
598-
True: 'Endpoint might not vulnerable to BOLA', # passed
599-
False: 'Endpoint might be vulnerable to BOLA', # failed
597+
'vuln_details': {
598+
True: 'Endpoint might be vulnerable to BOLA',
599+
False: 'Endpoint might not vulnerable to BOLA',
600600
},
601601
'success_codes': success_codes,
602602
'response_filter': PostTestFiltersEnum.STATUS_CODE_FILTER.name,
@@ -680,6 +680,9 @@ def bopla_fuzz_test(
680680
filter(lambda x: x.get('in') == 'path', request_params)
681681
)
682682

683+
if len(request_body_params) == 0 and len(request_query_params) == 0:
684+
continue
685+
683686
# handle path params from path_params
684687
# and replace path params by value in
685688
# endpoint path
@@ -718,9 +721,9 @@ def bopla_fuzz_test(
718721
'malicious_payload': response_body_params,
719722
'args': args,
720723
'kwargs': kwargs,
721-
'result_details': {
722-
True: 'Endpoint might not vulnerable to BOPLA', # passed
723-
False: 'Endpoint might be vulnerable to BOPLA', # failed
724+
'vuln_details': {
725+
True: 'Endpoint might be vulnerable to BOPLA',
726+
False: 'Endpoint might not vulnerable to BOPLA',
724727
},
725728
'success_codes': success_codes,
726729
'response_filter': PostTestFiltersEnum.STATUS_CODE_FILTER.name,
@@ -774,7 +777,7 @@ def __generate_injection_fuzz_params_test(
774777
self,
775778
openapi_parser: SwaggerParser | OpenAPIv3Parser,
776779
test_name: str,
777-
result_details: dict,
780+
vuln_details: dict,
778781
payloads_data: list[dict],
779782
*args,
780783
**kwargs,
@@ -804,19 +807,19 @@ def __generate_injection_fuzz_params_test(
804807
for payload_dict in payloads_data:
805808
for request_obj in fuzzed_request_list:
806809
payload = payload_dict['request_payload']
807-
808-
# handle body request params
809810
body_request_params = request_obj.get('body_params', [])
811+
query_request_params = request_obj.get('query_params', [])
812+
# endpoint can be fuzzed if it has query/body params
813+
if len(body_request_params) == 0 and len(query_request_params) == 0:
814+
continue
815+
816+
# handle body and query request params
810817
malicious_body_request_params = self.__inject_payload_in_params(
811818
body_request_params, payload
812819
)
813-
814-
# handle query request params
815-
query_request_params = request_obj.get('query_params', [])
816820
malicious_query_request_params = self.__inject_payload_in_params(
817821
query_request_params, payload
818822
)
819-
820823
request_obj['test_name'] = test_name
821824

822825
request_obj['body_params'] = malicious_body_request_params
@@ -826,7 +829,7 @@ def __generate_injection_fuzz_params_test(
826829

827830
request_obj['malicious_payload'] = payload
828831

829-
request_obj['result_details'] = result_details
832+
request_obj['vuln_details'] = vuln_details
830833
request_obj[
831834
'response_filter'
832835
] = PostTestFiltersEnum.BODY_REGEX_FILTER.name
@@ -865,15 +868,15 @@ def os_command_injection_fuzz_params_test(
865868
{'request_payload': 'ls -la', 'response_match_regex': r'total\s\d+'},
866869
]
867870

868-
result_details = {
869-
True: 'Parameters are not vulnerable to OS Command Injection', # passed
870-
False: 'One or more parameter is vulnerable to OS Command Injection Attack', # failed
871+
vuln_details = {
872+
True: 'One or more parameter is vulnerable to OS Command Injection Attack',
873+
False: 'Parameters are not vulnerable to OS Command Injection',
871874
}
872875

873876
return self.__generate_injection_fuzz_params_test(
874877
openapi_parser=openapi_parser,
875878
test_name=test_name,
876-
result_details=result_details,
879+
vuln_details=vuln_details,
877880
payloads_data=payloads_data,
878881
)
879882

@@ -912,15 +915,15 @@ def xss_html_injection_fuzz_params_test(
912915
},
913916
]
914917

915-
result_details = {
916-
True: 'Parameters are not vulnerable to XSS/HTML Injection Attack', # passed
917-
False: 'One or more parameter is vulnerable to XSS/HTML Injection Attack', # failed
918+
vuln_details = {
919+
False: 'Parameters are not vulnerable to XSS/HTML Injection Attack',
920+
True: 'One or more parameter is vulnerable to XSS/HTML Injection Attack',
918921
}
919922

920923
return self.__generate_injection_fuzz_params_test(
921924
openapi_parser=openapi_parser,
922925
test_name=test_name,
923-
result_details=result_details,
926+
vuln_details=vuln_details,
924927
payloads_data=payloads_data,
925928
)
926929

@@ -968,15 +971,15 @@ def ssti_fuzz_params_test(self, openapi_parser: SwaggerParser | OpenAPIv3Parser)
968971
{'request_payload': r'*{7*7}', 'response_match_regex': r'49'},
969972
]
970973

971-
result_details = {
972-
True: 'Parameters are not vulnerable to SSTI Attack', # passed
973-
False: 'One or more parameter is vulnerable to SSTI Attack', # failed
974+
vuln_details = {
975+
True: 'One or more parameter is vulnerable to SSTI Attack',
976+
False: 'Parameters are not vulnerable to SSTI Attack',
974977
}
975978

976979
return self.__generate_injection_fuzz_params_test(
977980
openapi_parser=openapi_parser,
978981
test_name=test_name,
979-
result_details=result_details,
982+
vuln_details=vuln_details,
980983
payloads_data=payloads_data,
981984
)
982985

@@ -993,7 +996,7 @@ def missing_auth_fuzz_test(
993996
openapi_parser (OpenAPIParser): An instance of the OpenAPIParser class
994997
containing the parsed OpenAPI specification.
995998
success_codes (list[int], optional): A list of HTTP success codes to consider
996-
as successful BOLA responses. Defaults to [200, 201, 301].
999+
as test failed responses. Defaults to [200, 201, 301].
9971000
*args: Variable-length positional arguments.
9981001
**kwargs: Arbitrary keyword arguments.
9991002
@@ -1069,9 +1072,9 @@ def missing_auth_fuzz_test(
10691072
'malicious_payload': 'Security Payload Missing',
10701073
'args': args,
10711074
'kwargs': kwargs,
1072-
'result_details': {
1073-
True: 'Endpoint implements security authentication as defined', # passed
1074-
False: 'Endpoint fails to implement security authentication as defined', # failed
1075+
'vuln_details': {
1076+
True: 'Endpoint fails to implement security authentication as defined',
1077+
False: 'Endpoint implements security authentication as defined',
10751078
},
10761079
'success_codes': success_codes,
10771080
'response_filter': PostTestFiltersEnum.STATUS_CODE_FILTER.name,

0 commit comments

Comments
 (0)