Skip to content

Commit edea41d

Browse files
Merge pull request #87 from OWASP/dev
Dev RELEASE: v0.17.3
2 parents b06914b + 529038b commit edea41d

File tree

9 files changed

+444
-195
lines changed

9 files changed

+444
-195
lines changed

src/README.md

+36
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Automatically Tests for vulnerabilities after generating tests from openapi spec
1616
- [x] Broken Access Control
1717
- [x] Basic Command Injection
1818
- [x] Basic XSS/HTML Injection test
19+
- [x] Basic SSTI test
1920
- [ ] Broken Authentication
2021

2122
## Features
@@ -28,13 +29,48 @@ Automatically Tests for vulnerabilities after generating tests from openapi spec
2829
- Proxy Support
2930
- Secure Dockerized Project for Easy Usage
3031
- Open Source Tool with MIT License
32+
- Github Action
3133

3234
## Demo
3335

3436
[![asciicast](https://asciinema.org/a/9MSwl7UafIVT3iJn13OcvWXeF.svg)](https://asciinema.org/a/9MSwl7UafIVT3iJn13OcvWXeF)
3537

3638
> Note: 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.
3739
40+
## Github Action
41+
42+
- Create github action secret `url` for your repo
43+
- Setup github action workflow in your repo `.github/workflows/offat.yml`
44+
45+
```yml
46+
name: OWASP OFFAT Sample Workflow
47+
48+
on:
49+
push:
50+
branches:
51+
- dev
52+
- main
53+
54+
jobs:
55+
test:
56+
runs-on: ubuntu-latest
57+
58+
steps:
59+
- name: "download swagger/OAS file"
60+
run: curl ${url} -o /tmp/swagger.json
61+
env:
62+
url: ${{ secrets.url }}
63+
64+
- name: "OWASP OFFAT CICD Scanner"
65+
uses: OWASP/OFFAT@main # OWASP/[email protected]
66+
with:
67+
file: /tmp/swagger.json # or ${{ secrets.url }}
68+
rate_limit: 120
69+
artifact_retention_days: 1
70+
```
71+
72+
> Prefer locking action to specific version `OWASP/[email protected]` instead of using `OWASP/OFFAT@main` and bump OFFAT action version after testing.
73+
3874
## PyPi Downloads
3975

4076
| Period | Count |

src/offat/config_data_handler.py

+25-36
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,23 @@
1+
"""
2+
Module contains the functions to validate the test
3+
configuration data and populate user data for tests.
4+
"""
15
from copy import deepcopy
26
from .logger import logger
7+
from .utils import update_values
38

49

5-
def overwrite_user_params(list1: list[dict], list2: list[dict]) -> list[dict]:
10+
def validate_config_file_data(test_config_data: dict):
611
"""
7-
Update values in list1 based on the corresponding "name" values in list2.
12+
Validates the provided test configuration data.
813
914
Args:
10-
list1 (list of dict): The list of dictionaries to be updated.
11-
list2 (list of dict): The list of dictionaries containing values to update from.
15+
test_config_data (dict): The test configuration data to be validated.
1216
1317
Returns:
14-
list of dict: The updated list1 with values from list2.
15-
16-
Example:
17-
```python
18-
list1 = [{'name': 'id', 'value': 67}, {'name': 'email', 'value': '[email protected]'}]
19-
list2 = [{'name': 'id', 'value': 10}, {'name': 'email', 'value': '[email protected]'}]
20-
updated_list = update_values(list1, list2)
21-
print(updated_list)
22-
# Output: [{'name': 'id', 'value': 10}, {'name': 'email', 'value': '[email protected]'}]
23-
```
24-
"""
25-
# Create a dictionary for faster lookup
26-
lookup_dict = {item['name']: item['value'] for item in list2}
27-
28-
# Update values in list1 using index lookup
29-
for item in list1:
30-
if item['name'] in lookup_dict:
31-
item['value'] = lookup_dict[item['name']]
32-
33-
return list1
18+
bool or dict: Returns False if the data is invalid, otherwise returns the validated test configuration data.
3419
35-
36-
def validate_config_file_data(test_config_data: dict):
20+
"""
3721
if not isinstance(test_config_data, dict):
3822
logger.warning('Invalid data format')
3923
return False
@@ -42,9 +26,7 @@ def validate_config_file_data(test_config_data: dict):
4226
logger.warning('Error Occurred While reading file: %s', test_config_data)
4327
return False
4428

45-
if not test_config_data.get(
46-
'actors',
47-
):
29+
if not test_config_data.get('actors'):
4830
logger.warning('actors are required')
4931
return False
5032

@@ -57,6 +39,17 @@ def validate_config_file_data(test_config_data: dict):
5739

5840

5941
def populate_user_data(actor_data: dict, actor_name: str, tests: list[dict]):
42+
"""
43+
Populates user data for tests.
44+
45+
Args:
46+
actor_data (dict): The data of the actor.
47+
actor_name (str): The name of the actor.
48+
tests (list[dict]): The list of tests.
49+
50+
Returns:
51+
list[dict]: The updated list of tests.
52+
"""
6053
tests = deepcopy(tests)
6154
headers = actor_data.get('request_headers', [])
6255
body_params = actor_data.get('body', [])
@@ -69,15 +62,11 @@ def populate_user_data(actor_data: dict, actor_name: str, tests: list[dict]):
6962
request_headers[header.get('name')] = header.get('value')
7063

7164
for test in tests:
72-
test['body_params'] = overwrite_user_params(
73-
deepcopy(test['body_params']), body_params
74-
)
75-
test['query_params'] = overwrite_user_params(
65+
test['body_params'] = update_values(deepcopy(test['body_params']), body_params)
66+
test['query_params'] = update_values(
7667
deepcopy(test['query_params']), query_params
7768
)
78-
test['path_params'] += overwrite_user_params(
79-
deepcopy(test['path_params']), path_params
80-
)
69+
test['path_params'] += update_values(deepcopy(test['path_params']), path_params)
8170
# for post test processing tests such as broken authentication
8271
test['test_actor_name'] = actor_name
8372
if test.get('kwargs', {}).get('headers', {}).items():

src/offat/report/templates/table.py

+22-13
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33

44

55
class TestResultTable:
6-
def __init__(self, table_width_percentage: float = 98, ) -> None:
6+
def __init__(
7+
self,
8+
table_width_percentage: float = 98,
9+
) -> None:
710
self.console = console
811
self.table_width_percentage = table_width_percentage
912

@@ -19,7 +22,10 @@ def extract_result_table_cols(self, results: list[dict]) -> list[str]:
1922
return sorted({key for dictionary in results for key in dictionary.keys()})
2023

2124
def generate_result_cols(self, results_list: list[dict]) -> list[Column]:
22-
return [Column(header=col_header, overflow='fold') for col_header in self.extract_result_table_cols(results_list)]
25+
return [
26+
Column(header=col_header, overflow='fold')
27+
for col_header in self.extract_result_table_cols(results_list)
28+
]
2329

2430
def generate_result_table(self, results: list, filter_passed_results: bool = True):
2531
results = self._sanitize_results(results, filter_passed_results)
@@ -29,23 +35,28 @@ def generate_result_table(self, results: list, filter_passed_results: bool = Tru
2935
for result in results:
3036
table_row = []
3137
for col in cols:
32-
table_row.append(
33-
str(result.get(col.header, '[red]:bug: - [/red]')))
38+
table_row.append(str(result.get(col.header, '[red]:bug: - [/red]')))
3439
table.add_row(*table_row)
3540

3641
return table
3742

38-
def _sanitize_results(self, results: list, filter_passed_results: bool = True, is_leaking_data: bool = False):
43+
def _sanitize_results(
44+
self,
45+
results: list,
46+
filter_passed_results: bool = True,
47+
is_leaking_data: bool = False,
48+
):
3949
if filter_passed_results:
40-
results = list(filter(lambda x: not x.get(
41-
'result') or x.get('data_leak'), results))
50+
results = list(
51+
filter(lambda x: not x.get('result') or x.get('data_leak'), results)
52+
)
4253

4354
# remove keys based on conditions or update their values
4455
for result in results:
4556
if result['result']:
46-
result['result'] = u"[bold green]Passed \u2713[/bold green]"
57+
result['result'] = '[bold green]Passed \u2713[/bold green]'
4758
else:
48-
result['result'] = u"[bold red]Failed \u00d7[/bold red]"
59+
result['result'] = '[bold red]Failed \u00d7[/bold red]'
4960

5061
if not is_leaking_data:
5162
del result['response_headers']
@@ -65,16 +76,14 @@ def _sanitize_results(self, results: list, filter_passed_results: bool = True, i
6576
del result['response_match_regex']
6677

6778
if result.get('data_leak'):
68-
result['data_leak'] = u"[bold red]Leak Found \u00d7[/bold red]"
79+
result['data_leak'] = '[bold red]Leak Found \u00d7[/bold red]'
6980
else:
70-
result['data_leak'] = u"[bold green]No Leak \u2713[/bold green]"
81+
result['data_leak'] = '[bold green]No Leak \u2713[/bold green]'
7182

7283
if not isinstance(result.get('malicious_payload'), str):
7384
del result['malicious_payload']
7485

7586
del result['url']
76-
del result['args']
77-
del result['kwargs']
7887
del result['test_name']
7988
del result['response_filter']
8089
del result['body_params']

0 commit comments

Comments
 (0)