drheaderplus 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
drheader/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+
2
+ """Top-level package for drHEADer core."""
3
+
4
+ from drheader.core import Drheader # noqa
File without changes
drheader/cli/cli.py ADDED
@@ -0,0 +1,252 @@
1
+ """Console script for drheader."""
2
+ import ast
3
+ import json
4
+ import logging
5
+ import os
6
+ import sys
7
+ from json import JSONDecodeError
8
+
9
+ import click
10
+ import jsonschema
11
+ from click import Choice, File, ParamType
12
+
13
+ from drheader import Drheader
14
+ from drheader.cli import utils
15
+
16
+ _OUTPUT_TYPES = ['json', 'table']
17
+
18
+
19
+ class URLParamType(ParamType):
20
+
21
+ name = 'URL'
22
+
23
+ def convert(self, value, param, ctx):
24
+ if not value.startswith('http'):
25
+ scheme = click.prompt('Please select a scheme', type=Choice(['http', 'https'], case_sensitive=False))
26
+ value = f'{scheme}://{value}'
27
+ return value
28
+
29
+
30
+ @click.group(context_settings={'show_default': True})
31
+ @click.version_option()
32
+ def main():
33
+ """Console script for drheader."""
34
+
35
+
36
+ @main.group()
37
+ def compare():
38
+ """Compare headers with drheader."""
39
+
40
+
41
+ @main.group()
42
+ def scan():
43
+ """Scan endpoints with drheader."""
44
+
45
+
46
+ @compare.command(context_settings={
47
+ 'default_map': {
48
+ 'output': 'table'
49
+ }
50
+ })
51
+ @click.argument('file', type=File(), required=True)
52
+ @click.option('--cross-origin-isolated', '-co', is_flag=True, help='Enable cross-origin isolation validations')
53
+ @click.option('--debug', '-d', is_flag=True, help='Enable debug logging')
54
+ @click.option('--junit', '-j', is_flag=True, help='Generate a JUnit report')
55
+ @click.option('--merge', '-m', is_flag=True, help='Merge a custom ruleset with the default rules')
56
+ @click.option('--output', '-o', type=Choice(_OUTPUT_TYPES, case_sensitive=False), help='Report output format')
57
+ @click.option('--rules-file', '-rf', type=File(), help='Use a custom ruleset, loaded from file')
58
+ @click.option('--rules-uri', '-ru', metavar='URI', help='Use a custom ruleset, downloaded from URI')
59
+ def single(file, cross_origin_isolated, debug, junit, merge, output, rules_file, rules_uri):
60
+ if debug:
61
+ logging.basicConfig(level=logging.DEBUG)
62
+
63
+ scanner = Drheader(headers=json.loads(file.read()))
64
+ rules = utils.get_rules(rules_file=rules_file, rules_uri=rules_uri, merge_default=merge)
65
+ report = scanner.analyze(rules=rules, cross_origin_isolated=cross_origin_isolated)
66
+
67
+ if output == 'json':
68
+ click.echo(json.dumps(report, indent=4))
69
+ else:
70
+ click.echo()
71
+ if not report:
72
+ click.echo('No issues found!')
73
+ else:
74
+ click.echo(f'{len(report)} issues found')
75
+ click.echo(utils.tabulate_report(report))
76
+ if junit:
77
+ utils.file_junit_report(rules, report)
78
+
79
+ sys.exit(os.EX_SOFTWARE if report else os.EX_OK)
80
+
81
+
82
+ @compare.command(context_settings={
83
+ 'default_map': {
84
+ 'output': 'table'
85
+ }
86
+ })
87
+ @click.argument('file', type=File(), required=True)
88
+ @click.option('--cross-origin-isolated', '-co', is_flag=True, help='Enable cross-origin isolation validations')
89
+ @click.option('--debug', '-d', is_flag=True, help='Enable debug logging')
90
+ @click.option('--merge', '-m', is_flag=True, help='Merge a custom ruleset with the default rules')
91
+ @click.option('--output', '-o', type=Choice(_OUTPUT_TYPES, case_sensitive=False), help='Report output format')
92
+ @click.option('--rules-file', '-rf', type=File(), help='Use a custom ruleset, loaded from file')
93
+ @click.option('--rules-uri', '-ru', metavar='URI', help='Use a custom ruleset, downloaded from URI')
94
+ def bulk(file, cross_origin_isolated, debug, merge, output, rules_file, rules_uri):
95
+ if debug:
96
+ logging.basicConfig(level=logging.DEBUG)
97
+
98
+ data = json.loads(file.read())
99
+ with open(os.path.join(os.path.dirname(__file__), '../resources/cli/bulk_compare_schema.json')) as schema:
100
+ schema = json.load(schema)
101
+ jsonschema.validate(instance=data, schema=schema)
102
+
103
+ audit = []
104
+ rules = utils.get_rules(rules_file=rules_file, rules_uri=rules_uri, merge_default=merge)
105
+ for target in data:
106
+ try:
107
+ scanner = Drheader(headers=target['headers'])
108
+ report = scanner.analyze(rules=rules, cross_origin_isolated=cross_origin_isolated)
109
+ audit.append({'url': target['url'], 'report': report})
110
+ except Exception as e:
111
+ audit.append({'url': target['url'], 'report': [], 'error': str(e)})
112
+
113
+ if output == 'json':
114
+ click.echo(json.dumps(audit, indent=4))
115
+ else:
116
+ for target in audit:
117
+ click.echo()
118
+ if target.get('error'):
119
+ click.echo(f"{target['url']}: {target['error']}")
120
+ elif not target['report']:
121
+ click.echo(f"{target['url']}: No issues found!")
122
+ else:
123
+ click.echo(f"{target['url']}: {len(target['report'])} issues found")
124
+ click.echo(utils.tabulate_report(target['report']))
125
+
126
+ for target in audit:
127
+ if target.get('report') or target.get('error'):
128
+ sys.exit(os.EX_SOFTWARE)
129
+ else:
130
+ sys.exit(os.EX_OK)
131
+
132
+
133
+ @scan.command(context_settings={
134
+ 'default_map': {
135
+ 'output': 'table'
136
+ },
137
+ 'ignore_unknown_options': True
138
+ })
139
+ @click.argument('target_url', type=URLParamType(), required=True)
140
+ @click.argument('request_args', type=click.UNPROCESSED, nargs=-1)
141
+ @click.option('--cross-origin-isolated', '-co', is_flag=True, help='Enable cross-origin isolation validations')
142
+ @click.option('--debug', '-d', is_flag=True, help='Enable debug logging')
143
+ @click.option('--junit', '-j', is_flag=True, help='Generate a JUnit report')
144
+ @click.option('--merge', '-m', is_flag=True, help='Merge a custom ruleset with the default rules')
145
+ @click.option('--output', '-o', type=Choice(_OUTPUT_TYPES, case_sensitive=False), help='Report output format')
146
+ @click.option('--rules-file', '-rf', type=File(), help='Use a custom ruleset, loaded from file')
147
+ @click.option('--rules-uri', '-ru', metavar='URI', help='Use a custom ruleset, downloaded from URI')
148
+ def single(target_url, request_args, cross_origin_isolated, debug, junit, merge, output, rules_file, rules_uri): # noqa: E501, F811
149
+ if debug:
150
+ logging.basicConfig(level=logging.DEBUG)
151
+
152
+ kwargs = {}
153
+ for i in range(0, len(request_args), 2):
154
+ key = request_args[i].strip('-').replace('-', '_')
155
+ try:
156
+ kwargs[key] = json.loads(request_args[i + 1])
157
+ except JSONDecodeError:
158
+ try:
159
+ kwargs[key] = ast.literal_eval(request_args[i + 1]) # This handles bytes and tuples
160
+ except (SyntaxError, ValueError):
161
+ kwargs[key] = request_args[i + 1]
162
+
163
+ scanner = Drheader(url=target_url, **kwargs)
164
+ rules = utils.get_rules(rules_file=rules_file, rules_uri=rules_uri, merge_default=merge)
165
+ report = scanner.analyze(rules=rules, cross_origin_isolated=cross_origin_isolated)
166
+
167
+ if output == 'json':
168
+ click.echo(json.dumps(report, indent=4))
169
+ else:
170
+ click.echo()
171
+ if not report:
172
+ click.echo('No issues found!')
173
+ else:
174
+ click.echo(f"{target_url}: {len(report)} issues found")
175
+ click.echo(utils.tabulate_report(report))
176
+ if junit:
177
+ utils.file_junit_report(rules, report)
178
+
179
+ sys.exit(os.EX_SOFTWARE if report else os.EX_OK)
180
+
181
+
182
+ @scan.command(context_settings={
183
+ 'default_map': {
184
+ 'file_format': 'json',
185
+ 'output': 'table'
186
+ }
187
+ })
188
+ @click.argument('file', type=File(), required=True)
189
+ @click.option('--cross-origin-isolated', '-co', is_flag=True, help='Enable cross-origin isolation validations')
190
+ @click.option('--debug', '-d', is_flag=True, help='Enable debug logging')
191
+ @click.option('--file-format', '-ff', type=Choice(['json', 'txt'], case_sensitive=False), help='FILE input format')
192
+ @click.option('--merge', '-m', is_flag=True, help='Merge a custom ruleset with the default rules')
193
+ @click.option('--output', '-o', type=Choice(_OUTPUT_TYPES, case_sensitive=False), help='Report output format')
194
+ @click.option('--rules-file', '-rf', type=File(), help='Use a custom ruleset, loaded from file')
195
+ @click.option('--rules-uri', '-ru', metavar='URI', help='Use a custom ruleset, downloaded from URI')
196
+ def bulk(file, cross_origin_isolated, debug, file_format, merge, output, rules_file, rules_uri): # noqa: F811
197
+ if debug:
198
+ logging.basicConfig(level=logging.DEBUG)
199
+
200
+ if file_format == 'txt':
201
+ urls = [{'url': url} for url in list(filter(None, file.read().splitlines()))]
202
+ else:
203
+ urls = json.loads(file.read())
204
+ with open(os.path.join(os.path.dirname(__file__), '../resources/cli/bulk_scan_schema.json')) as schema:
205
+ schema = json.load(schema)
206
+ jsonschema.validate(instance=urls, schema=schema)
207
+
208
+ audit = []
209
+ rules = utils.get_rules(rules_file=rules_file, rules_uri=rules_uri, merge_default=merge)
210
+ for target in urls:
211
+ for key, value in target.items():
212
+ try:
213
+ target[key] = ast.literal_eval(value) # This handles bytes and tuples
214
+ except (SyntaxError, ValueError):
215
+ target[key] = value
216
+
217
+ try:
218
+ scanner = Drheader(**target)
219
+ report = scanner.analyze(rules=rules, cross_origin_isolated=cross_origin_isolated)
220
+ audit.append({'url': target['url'], 'report': report})
221
+ except Exception as e:
222
+ audit.append({'url': target['url'], 'report': [], 'error': str(e)})
223
+
224
+ if output == 'json':
225
+ click.echo(json.dumps(audit, indent=4))
226
+ else:
227
+ for target in audit:
228
+ click.echo()
229
+ if target.get('error'):
230
+ click.echo(f"{target['url']}: {target['error']}")
231
+ elif not target['report']:
232
+ click.echo(f"{target['url']}: No issues found!")
233
+ else:
234
+ click.echo(f"{target['url']}: {len(target['report'])} issues found")
235
+ click.echo(utils.tabulate_report(target['report']))
236
+
237
+ for target in audit:
238
+ if target.get('report') or target.get('error'):
239
+ sys.exit(os.EX_SOFTWARE)
240
+ else:
241
+ sys.exit(os.EX_OK)
242
+
243
+
244
+ def start():
245
+ try:
246
+ main()
247
+ except Exception as e:
248
+ click.secho(str(e), fg='red')
249
+
250
+
251
+ if __name__ == '__main__':
252
+ start()
drheader/cli/utils.py ADDED
@@ -0,0 +1,58 @@
1
+ """Utility functions for cli module."""
2
+
3
+ import os
4
+
5
+ import tabulate
6
+ from junitparser import Failure, JUnitXml, TestCase, TestSuite
7
+
8
+ from drheader import utils
9
+
10
+
11
+ def get_rules(rules_file=None, rules_uri=None, merge_default=False):
12
+ if rules_file or rules_uri:
13
+ return utils.load_rules(rules_file=rules_file, rules_uri=rules_uri, merge_default=merge_default)
14
+ else:
15
+ return utils.default_rules()
16
+
17
+
18
+ def tabulate_report(report):
19
+ rows = []
20
+ final_string = ''
21
+
22
+ for validation_error in report:
23
+ values = [[k, v] for k, v in validation_error.items()]
24
+ rows.append(values)
25
+ for validation_error in rows:
26
+ final_string += '----\n'
27
+ final_string += tabulate.tabulate(validation_error, tablefmt='presto') + '\n'
28
+
29
+ return final_string
30
+
31
+
32
+ def file_junit_report(rules, report):
33
+ """Generates a JUnit XML report from a scan result.
34
+
35
+ Args:
36
+ rules (dict): The rules used to perform the scan.
37
+ report (list): The report generated from the scan.
38
+ """
39
+ test_suite = TestSuite('drHEADer')
40
+
41
+ for header in rules:
42
+ test_case = None
43
+ for validation_error in report:
44
+ if (title := validation_error.get('rule')).startswith(header):
45
+ validation_error = {k: v for k, v in validation_error.items() if k != 'rule'}
46
+ test_case = TestCase(title)
47
+ failure = Failure(message=validation_error.pop('message'))
48
+ failure.text = str(validation_error)
49
+ test_case.result = [failure]
50
+ test_suite.add_testcase(test_case)
51
+ if not test_case:
52
+ test_case = TestCase(header)
53
+ test_suite.add_testcase(test_case)
54
+
55
+ os.makedirs('reports', exist_ok=True)
56
+ xml = JUnitXml()
57
+ xml.add_testsuite(test_suite)
58
+ xml.write('reports/junit.xml')
drheader/core.py ADDED
@@ -0,0 +1,185 @@
1
+ """Main module and entry point for analysis."""
2
+ import json
3
+ import logging
4
+ import os
5
+
6
+ import requests
7
+ from requests.structures import CaseInsensitiveDict
8
+
9
+ from drheader import utils
10
+ from drheader.report import Reporter
11
+ from drheader.validators.cookie_validator import CookieValidator
12
+ from drheader.validators.directive_validator import DirectiveValidator
13
+ from drheader.validators.header_validator import HeaderValidator
14
+
15
+ _ALLOWED_HTTP_METHODS = ['delete', 'get', 'head', 'options', 'patch', 'post', 'put']
16
+ _CROSS_ORIGIN_HEADERS = ['cross-origin-embedder-policy', 'cross-origin-opener-policy']
17
+
18
+ with open(os.path.join(os.path.dirname(__file__), 'resources/delimiters.json')) as delimiters:
19
+ _DELIMITERS = CaseInsensitiveDict(json.load(delimiters))
20
+
21
+
22
+ class Drheader:
23
+ """Main class and entry point for analysis.
24
+
25
+ Attributes:
26
+ headers (CaseInsensitiveDict): The headers to analyse.
27
+ cookies (CaseInsensitiveDict): The cookies to analyse.
28
+ reporter (Reporter): Reporter instance that generates and holds the final report.
29
+ """
30
+
31
+ def __init__(self, headers=None, url=None, **kwargs):
32
+ """Initialises a Drheader instance.
33
+
34
+ At least one of <headers> and <url> must be defined. The value passed in <headers> is treated differently
35
+ depending on whether a URL is provided. If a URL is provided, the headers passed are treated as request headers
36
+ and are sent with the HTTP request to <url>. Otherwise, they are the raw headers that are analysed.
37
+
38
+ Args:
39
+ headers (dict): Either headers to analyse or a dict of headers to send with the request to <url>.
40
+ url (str): URL from which to retrieve the headers to analyse.
41
+
42
+ Raises:
43
+ ValueError: If neither headers nor url is provided, or if url is not a valid URL.
44
+ """
45
+ if not url:
46
+ if not headers:
47
+ raise ValueError("Nothing provided for analysis. Either 'headers' or 'url' must be defined")
48
+ else:
49
+ headers_to_analyse = json.loads(headers) if isinstance(headers, str) else headers
50
+ else:
51
+ headers_to_analyse = _get_headers_from_url(url, headers=headers, **kwargs)
52
+
53
+ self.cookies = CaseInsensitiveDict()
54
+ self.headers = CaseInsensitiveDict(headers_to_analyse)
55
+ self.reporter = Reporter()
56
+
57
+ for cookie in self.headers.get('set-cookie', []):
58
+ cookie = cookie.split('=', 1)
59
+ self.cookies[cookie[0]] = cookie[1]
60
+
61
+ def analyze(self, rules=None, cross_origin_isolated=False):
62
+ """Analyses headers against a drHEADer ruleset.
63
+
64
+ Args:
65
+ rules (dict): (optional) The rules against which to assess the headers. Default rules are used if undefined.
66
+ cross_origin_isolated (bool): (optional) A flag to enable cross-origin isolation rules. Default is False.
67
+
68
+ Returns:
69
+ A list containing all the rule violations found during analysis. The report consists of individual dict
70
+ items per header and rule. Each item in the report will detail the non-compliant header, the rule violated
71
+ and its associated severity, and, if applicable, the observed value of the header, any expected, disallowed
72
+ or anomalous values, and the correct delimiter. For example:
73
+ {
74
+ 'rule': 'Referrer-Policy',
75
+ 'message': 'Value does not match security policy. Exactly one of the expected items was expected',
76
+ 'severity': 'high',
77
+ 'value': 'origin-when-cross-origin'
78
+ 'expected': ['same-origin', 'strict-origin-when-cross-origin']
79
+ }
80
+ """
81
+ if not rules:
82
+ rules = _translate_to_case_insensitive_dict(utils.default_rules())
83
+ else:
84
+ rules = _translate_to_case_insensitive_dict(rules)
85
+
86
+ header_validator = HeaderValidator(self.headers)
87
+ directive_validator = DirectiveValidator(self.headers)
88
+ cookie_validator = CookieValidator(self.cookies)
89
+
90
+ for header, rule_config in rules.items():
91
+ if header.lower() in _CROSS_ORIGIN_HEADERS and not cross_origin_isolated:
92
+ logging.info(f"Cross-origin isolation validations are not enabled. Skipping header '{header}'")
93
+ continue
94
+
95
+ if header.lower() != 'set-cookie':
96
+ self._validate_rules(rule_config, header_validator, header)
97
+ elif header in self.headers: # Validates global rules for cookies e.g. all cookies must contain 'secure'
98
+ for cookie in self.cookies:
99
+ self._validate_rules(rule_config, cookie_validator, header, cookie=cookie)
100
+
101
+ if 'directives' in rule_config and header in self.headers:
102
+ for directive, directive_config in rule_config['directives'].items():
103
+ self._validate_rules(directive_config, directive_validator, header, directive=directive)
104
+ if 'cookies' in rule_config and header.lower() == 'set-cookie': # Validates individual rules for cookies e.g. cookie session_id must contain 'samesite=strict' # noqa:E501
105
+ for cookie, cookie_config in rule_config['cookies'].items():
106
+ self._validate_rules(cookie_config, cookie_validator, header, cookie=cookie)
107
+ return self.reporter.report
108
+
109
+ def _validate_rules(self, config, validator, header, **kwargs):
110
+ """Validates rules for a single header, directive or cookie."""
111
+ config['delimiters'] = _DELIMITERS.get(header)
112
+ required = str(config['required']).strip().lower()
113
+
114
+ if required == 'true':
115
+ if report_item := validator.exists(config, header, **kwargs):
116
+ self._add_to_report(report_item)
117
+ else:
118
+ self._validate_value_rules(config, validator, header, **kwargs)
119
+ elif required == 'false':
120
+ if report_item := validator.not_exists(config, header, **kwargs):
121
+ self._add_to_report(report_item)
122
+ elif required == 'optional':
123
+ if cookie := kwargs.get('cookie'):
124
+ is_present = cookie in self.cookies
125
+ elif directive := kwargs.get('directive'):
126
+ is_present = directive in utils.parse_policy(self.headers[header], **_DELIMITERS[header], keys_only=True) # noqa: E501
127
+ else:
128
+ is_present = header in self.headers
129
+
130
+ if is_present:
131
+ self._validate_value_rules(config, validator, header, **kwargs)
132
+
133
+ def _validate_value_rules(self, config, validator, header, **kwargs):
134
+ """Validates rules for a single header, directive or cookie."""
135
+ if 'value' in config:
136
+ if report_item := validator.value(config, header, **kwargs):
137
+ self._add_to_report(report_item)
138
+ elif 'value-any-of' in config:
139
+ if report_item := validator.value_any_of(config, header, **kwargs):
140
+ self._add_to_report(report_item)
141
+ elif 'value-one-of' in config:
142
+ if report_item := validator.value_one_of(config, header, **kwargs):
143
+ self._add_to_report(report_item)
144
+ else:
145
+ if 'must-avoid' in config:
146
+ if report_item := validator.must_avoid(config, header, **kwargs):
147
+ self._add_to_report(report_item)
148
+ if 'must-contain' in config:
149
+ if report_item := validator.must_contain(config, header, **kwargs):
150
+ self._add_to_report(report_item)
151
+ if 'must-contain-one' in config:
152
+ if report_item := validator.must_contain_one(config, header, **kwargs):
153
+ self._add_to_report(report_item)
154
+
155
+ def _add_to_report(self, report_item):
156
+ """Adds a finding or list of findings to the final report."""
157
+ try:
158
+ self.reporter.add_item(report_item)
159
+ except AttributeError: # For must-avoid rules on policy headers (CSP, Permissions-Policy)
160
+ for item in report_item: # A separate report item is created for each directive that violates the must-avoid rule e.g. multiple directives containing 'unsafe-inline' # noqa:E501
161
+ self.reporter.add_item(item)
162
+
163
+
164
+ def _get_headers_from_url(url, method='head', **kwargs):
165
+ """Retrieves headers from a URL."""
166
+ if method.strip().lower() not in _ALLOWED_HTTP_METHODS:
167
+ raise ValueError(f"'{method}' is not an allowed HTTP method")
168
+
169
+ if 'timeout' not in kwargs:
170
+ kwargs['timeout'] = 5
171
+ if 'allow_redirects' not in kwargs:
172
+ kwargs['allow_redirects'] = True
173
+
174
+ response = requests.request(method, url, **kwargs) # noqa: S113
175
+ response_headers = response.headers
176
+ response_headers['set-cookie'] = response.raw.headers.getlist('Set-Cookie')
177
+ return response_headers
178
+
179
+
180
+ def _translate_to_case_insensitive_dict(dict_to_translate):
181
+ """Recursively transforms a dict into a case-insensitive dict."""
182
+ for key, value in dict_to_translate.items():
183
+ if isinstance(value, dict):
184
+ dict_to_translate[key] = _translate_to_case_insensitive_dict(value)
185
+ return CaseInsensitiveDict(dict_to_translate)
drheader/report.py ADDED
@@ -0,0 +1,70 @@
1
+ """Primary module for report generation and storage."""
2
+ from enum import Enum
3
+ from typing import NamedTuple
4
+
5
+
6
+ class Reporter:
7
+ """Class to generate and store reports from a scan.
8
+
9
+ Attributes:
10
+ report (list): The report detailing validation failures encountered during a scan.
11
+ """
12
+
13
+ def __init__(self):
14
+ """Initialises a Reporter instance with an empty report."""
15
+ self.report = []
16
+
17
+ def add_item(self, item):
18
+ """Adds a validation failure to the report.
19
+
20
+ Args:
21
+ item (ReportItem): The validation failure to be added.
22
+ """
23
+ finding = {}
24
+ if item.directive:
25
+ finding['rule'] = f'{item.header} - {item.directive}'
26
+ finding['message'] = item.error_type.value.format('Directive')
27
+ elif item.cookie:
28
+ finding['rule'] = f'{item.header} - {item.cookie}'
29
+ finding['message'] = item.error_type.value.format('Cookie')
30
+ else:
31
+ finding['rule'] = item.header
32
+ finding['message'] = item.error_type.value.format('Header')
33
+
34
+ finding['severity'] = item.severity
35
+
36
+ if item.value:
37
+ finding['value'] = item.value
38
+ if item.expected:
39
+ finding['expected'] = item.expected
40
+ if len(item.expected) > 1 and item.delimiter:
41
+ finding['delimiter'] = item.delimiter
42
+ elif item.avoid:
43
+ finding['avoid'] = item.avoid
44
+ if item.anomalies:
45
+ finding['anomalies'] = item.anomalies
46
+ self.report.append(finding)
47
+
48
+
49
+ class ErrorType(Enum):
50
+ AVOID = 'Must-Avoid directive included'
51
+ CONTAIN = 'Must-Contain directive missed'
52
+ CONTAIN_ONE = 'Must-Contain-One directive missed. At least one of the expected items was expected'
53
+ DISALLOWED = '{} should not be returned'
54
+ REQUIRED = '{} not included in response'
55
+ VALUE = 'Value does not match security policy'
56
+ VALUE_ANY = 'Value does not match security policy. At least one of the expected items was expected'
57
+ VALUE_ONE = 'Value does not match security policy. Exactly one of the expected items was expected'
58
+
59
+
60
+ class ReportItem(NamedTuple):
61
+ severity: str
62
+ error_type: ErrorType
63
+ header: str
64
+ directive: str = None
65
+ cookie: str = None
66
+ value: str = None
67
+ avoid: list = None
68
+ expected: list = None
69
+ anomalies: list = None
70
+ delimiter: str = None
@@ -0,0 +1,20 @@
1
+ {
2
+ "type": "array",
3
+ "items": {
4
+ "type": "object",
5
+ "properties": {
6
+ "url": {
7
+ "type": "string",
8
+ "format": "uri"
9
+ },
10
+ "headers": {
11
+ "type": "object"
12
+ }
13
+ },
14
+ "required": [
15
+ "url",
16
+ "headers"
17
+ ],
18
+ "additionalProperties": false
19
+ }
20
+ }
@@ -0,0 +1,52 @@
1
+ {
2
+ "type": "array",
3
+ "items": {
4
+ "type": "object",
5
+ "properties": {
6
+ "url": {
7
+ "type": "string",
8
+ "format": "uri"
9
+ },
10
+ "allow_redirects": {
11
+ "type": "boolean"
12
+ },
13
+ "auth": {
14
+ "type": "string"
15
+ },
16
+ "cert": {
17
+ "type": "string"
18
+ },
19
+ "cookies": {
20
+ "type": "object"
21
+ },
22
+ "data": {
23
+ },
24
+ "headers": {
25
+ "type": "object"
26
+ },
27
+ "json": {
28
+ "type": "object"
29
+ },
30
+ "method": {
31
+ "enum": [
32
+ "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"
33
+ ]
34
+ },
35
+ "params": {
36
+ },
37
+ "proxies": {
38
+ "type": "object"
39
+ },
40
+ "timeout": {
41
+ "type": ["number", "string"]
42
+ },
43
+ "verify": {
44
+ "type": ["boolean", "string"]
45
+ }
46
+ },
47
+ "required": [
48
+ "url"
49
+ ],
50
+ "additionalProperties": false
51
+ }
52
+ }