Exemple #1
0
def single(target_url, json_output, debug, rule_file, merge, junit):
    """
    Scan a single http(s) endpoint with drheader.

    NOTE: URL parameters are currently only supported on bulk scans.
    """

    if debug:
        logging.basicConfig(level=logging.DEBUG)

    logging.debug('Validating: {}'.format(target_url))
    if not validators.url(target_url):
        raise click.ClickException(
            message='"{}" is not a valid URL.'.format(target_url))

    rules = load_rules(rule_file, merge)

    try:
        logging.debug('Querying headers...')
        drheader_instance = Drheader(url=target_url)
    except Exception as e:
        if debug:
            raise click.ClickException(e)
        else:
            raise click.ClickException('Failed to get headers.')

    try:
        logging.debug('Analyzing headers...')
        drheader_instance.analyze(rules)
    except Exception as e:
        if debug:
            raise click.ClickException(e)
        else:
            raise click.ClickException('Failed to analyze headers.')

    if json_output:
        click.echo(json.dumps(drheader_instance.report))
    else:
        click.echo()
        if not drheader_instance.report:
            click.echo('No issues found!')
        else:
            click.echo('{0} issues found'.format(len(
                drheader_instance.report)))
            for i in drheader_instance.report:
                values = []
                for k, v in i.items():
                    values.append([k, v])
                click.echo('----')
                click.echo(tabulate(values, tablefmt="presto"))
    if junit:
        file_junit_report(rules, drheader_instance.report)
    return 0
Exemple #2
0
def compare(file, json_output, debug, rule_file, rule_uri, merge):
    """
    If you have headers you would like to test with drheader, you can "compare" them with your ruleset this command.

    This command requires a valid json file as input.

    Example:

        \b
        [
            {
                "url": "https://test.com",
                "headers": {
                    "X-XSS-Protection": "1; mode=block",
                    "Content-Security-Policy": "default-src 'none'; script-src 'self' unsafe-inline; object-src 'self';"
                    "Strict-Transport-Security": "max-age=31536000; includeSubDomains",
                    "X-Frame-Options": "SAMEORIGIN",
                    "X-Content-Type-Options": "nosniff",
                    "Referrer-Policy": "strict-origin",
                    "Cache-Control": "no-cache, no-store, must-revalidate",
                    "Pragma": "no-cache",
                    "Set-Cookie": ["HttpOnly; Secure"]
                },
                "status_code": 200
            },
            ...
        ]
    """
    exit_code = EXIT_CODE_NO_ERROR
    audit = []
    schema = {
        "type": "array",
        "items": {
            "type": "object",
            "properties": {
                "url": {
                    "type": "string",
                    'format': 'uri'
                },
                "headers": {
                    "type": "object"
                },
                "status_code": {
                    "type": "integer"
                }
            },
            "required": ['headers', 'url']
        }
    }

    if debug:
        logging.basicConfig(level=logging.DEBUG)

    try:
        data = json.loads(file.read())
        jsonschema.validate(instance=data,
                            schema=schema,
                            format_checker=jsonschema.FormatChecker())
        logging.debug('Found {} URLs'.format(len(data)))
    except Exception as e:
        raise click.ClickException(e)

    if rule_uri and not rule_file:
        if not validators.url(rule_uri):
            raise click.ClickException(
                message='"{}" is not a valid URL.'.format(rule_uri))
        try:
            rule_file = get_rules_from_uri(rule_uri)
        except Exception as e:
            if debug:
                raise click.ClickException(e)
            else:
                raise click.ClickException(
                    'No content retrieved from rules-uri.')

    rules = load_rules(rule_file, merge)

    for i in data:
        logging.debug('Analysing : {}'.format(i['url']))
        drheader_instance = Drheader(url=i['url'], headers=i['headers'])
        drheader_instance.analyze(rules)
        audit.append({'url': i['url'], 'report': drheader_instance.report})
        if drheader_instance.report:
            exit_code = EXIT_CODE_FAILURE

    echo_bulk_report(audit, json_output)
    sys.exit(exit_code)
Exemple #3
0
def bulk(ctx, file, json_output, input_format, debug, rule_file, rule_uri,
         merge):
    """
    Scan multiple http(s) endpoints with drheader.

    The default file format is json:

        \b
        [
          {
            "url": "https://example.com",
            "params": {
                "example_parameter_key": "example_parameter_value"
            }
          },
          ...
        ]

    You can also use a txt file for input (using the "-ff txt" option):

        \b
        https://example.com
        https://example.co.uk

    NOTE: URL parameters are currently only supported on bulk scans.
    """
    exit_code = EXIT_CODE_NO_ERROR
    audit = []
    urls = []
    schema = {
        "type": "array",
        "items": {
            "type": "object",
            "properties": {
                "url": {
                    "type": "string",
                    'format': 'uri'
                },
                "params": {
                    "type": "string"
                },
            },
            "required": ['url']
        }
    }

    if debug:
        logging.basicConfig(level=logging.DEBUG)

    if input_format == 'txt':
        urls_temp = list(filter(None, file.read().splitlines()))
        for i in urls_temp:
            urls.append({'url': i})
        for i, v in enumerate(urls):
            logging.debug('Found: {}'.format(v))
            if not validators.url(v['url']):
                raise click.ClickException(
                    message='[line {}] "{}" is not a valid URL.'.format(
                        i + 1, v['url']))
    else:
        try:
            urls = json.loads(file.read())
            jsonschema.validate(instance=urls,
                                schema=schema,
                                format_checker=jsonschema.FormatChecker())
        except Exception as e:
            raise click.ClickException(e)

    logging.debug('Found {} URLs'.format(len(urls)))

    if rule_uri and not rule_file:
        if not validators.url(rule_uri):
            raise click.ClickException(
                message='"{}" is not a valid URL.'.format(rule_uri))
        try:
            rule_file = get_rules_from_uri(rule_uri)
        except Exception as e:
            if debug:
                raise click.ClickException(e)
            else:
                raise click.ClickException(
                    'No content retrieved from rules-uri.')

    rules = load_rules(rule_file, merge)

    for i, v in enumerate(urls):
        logging.debug('Querying: {}...'.format(v))
        drheader_instance = Drheader(url=v['url'],
                                     params=v.get('params', None),
                                     verify=ctx.obj['verify'])
        logging.debug('Analysing: {}...'.format(v))
        drheader_instance.analyze(rules)
        audit.append({'url': v['url'], 'report': drheader_instance.report})
        if drheader_instance.report:
            exit_code = EXIT_CODE_FAILURE

    echo_bulk_report(audit, json_output)
    sys.exit(exit_code)
Exemple #4
0
def single(ctx, target_url, json_output, debug, rule_file, rule_uri, merge,
           junit):
    """
    Scan a single http(s) endpoint with drheader.

    NOTE: URL parameters are currently only supported on bulk scans.
    """
    exit_code = EXIT_CODE_NO_ERROR
    if debug:
        logging.basicConfig(level=logging.DEBUG)

    logging.debug('Validating: {}'.format(target_url))
    if not validators.url(target_url):
        raise click.ClickException(
            message='"{}" is not a valid URL.'.format(target_url))

    if rule_uri and not rule_file:
        if not validators.url(rule_uri):
            raise click.ClickException(
                message='"{}" is not a valid URL.'.format(rule_uri))
        try:
            rule_file = get_rules_from_uri(rule_uri)
        except Exception as e:
            if debug:
                raise click.ClickException(e)
            else:
                raise click.ClickException(
                    'No content retrieved from rules-uri.')

    rules = load_rules(rule_file, merge)

    try:
        logging.debug('Querying headers...')
        drheader_instance = Drheader(url=target_url, verify=ctx.obj['verify'])
    except Exception as e:
        if debug:
            raise click.ClickException(e)
        else:
            raise click.ClickException('Failed to get headers.')

    try:
        logging.debug('Analyzing headers...')
        drheader_instance.analyze(rules)
    except Exception as e:
        if debug:
            raise click.ClickException(e)
        else:
            raise click.ClickException('Failed to analyze headers.')

    if drheader_instance.report:
        exit_code = EXIT_CODE_FAILURE

    if json_output:
        click.echo(json.dumps(drheader_instance.report))
    else:
        click.echo()
        if not drheader_instance.report:
            click.echo('No issues found!')
        else:
            click.echo('{0} issues found'.format(len(
                drheader_instance.report)))
            for i in drheader_instance.report:
                values = []
                for k, v in i.items():
                    values.append([k, v])
                click.echo('----')
                click.echo(tabulate(values, tablefmt="presto"))
    if junit:
        file_junit_report(rules, drheader_instance.report)
    sys.exit(exit_code)
class DrheaderRules(unittest2.TestCase):
    def setUp(self):
        # this is run each time before each test_ method is invoked
        self.logger = logging.Logger
        self.instance = ''
        self.report = list

        # configuration 

    def _process_test(self, url=None, headers=None, status_code=None):
        # all tests use this method to run the test and analyze the results.
        self.instance = Drheader(url=url, headers=headers, status_code=status_code)
        self.instance.analyze()

        # test can then make assertions against the contents of self.instance.report to determine success of failure.

    def test_get_headers_ok(self):
        url = 'https://example.com'
        self._process_test(url=url)
        self.assertNotEqual(self.report, None, msg="A Report was generated")

    def test_compare_rules_ok(self):
        with open(os.path.join(os.path.dirname(__file__), 'testfiles/header_ok.json'), 'r') as f:
            file = json.loads(f.read())

        self._process_test(headers=file, status_code=200)
        self.assertEqual(len(self.instance.report), 0, msg="No issues reported in Rules tests")

    def test_compare_rules_ok_with_case_insensitive(self):
        with open(os.path.join(os.path.dirname(__file__), 'testfiles/header_ok_test_case.json'), 'r') as f:
            file = json.loads(f.read())

        self._process_test(headers=file, status_code=200)
        self.assertEqual(len(self.instance.report), 0, msg="No issues reported in Rules tests")

    def test_compare_rules_enforce_ko(self):
        headers = {
            'X-XSS-Protection': '1; mode=bloc',
            'Content-Security-Policy': "default-src 'none'; script-src 'self'; object-src 'self';"
        }
        expected_response = {
            'severity': 'high',
            'rule': 'X-XSS-Protection',
            'message': 'Value does not match security policy',
            'expected': ['1', 'mode=block'],
            'delimiter': ';',
            'value': '1; mode=bloc'
        }

        self._process_test(headers=headers, status_code=200)
        self.assertIn(expected_response, self.instance.report, msg="X-XSS")

    def test_compare_rules_required_ko(self):
        headers = {
            'X-XSS-Protection': '1; mode=block'
        }
        # this expected response has been changed to fit output now delivered,
        # plz to review if test still makes sense in PR
        expected_response = {
            'severity': 'high',
            'rule': 'Content-Security-Policy',
            'message': 'Header not included in response'
        }
        self._process_test(headers=headers, status_code=200)
        self.assertIn(expected_response, self.instance.report, msg="Generated Rules")

    def test_compare_rules_not_required_ko(self):
        headers = {
            'X-XSS-Protection': '1; mode=block',
            'Content-Security-Policy': "default-src 'none'; script-src 'self'; object-src 'self';",
            'Server': 'Apache',
            'X-Generator': 'Drupal 7 (http://drupal.org)'
        }
        server_response = {
            'severity': 'high',
            'rule': 'Server',
            'message': 'Header should not be returned'
        }
        generator_response = {
            'severity': 'high',
            'rule': 'X-Generator',
            'message': 'Header should not be returned'
        }

        self._process_test(headers=headers, status_code=200)
        self.assertIn(server_response, self.instance.report, msg="Server Rule was triggered")
        self.assertIn(generator_response, self.instance.report, msg="Generator Rule was triggered")

    def test_compare_must_contain_ko(self):
        headers = {
            'X-XSS-Protection': '1; mode=block',
            'Content-Security-Policy': "default-src 'random'; script-src 'self'"
        }
        csp_contain_reponse = {
            'severity': 'high',
            'rule': 'Content-Security-Policy',
            'message': 'Must-Contain directive missed',
            'expected': ["default-src 'none'", "default-src 'self'"],
            'delimiter': ';',
            'value': "default-src 'random'; script-src 'self'",
            'anomaly': ["default-src 'none'", "default-src 'self'"]
        }

        self._process_test(headers=headers, status_code=200)
        self.assertIn(csp_contain_reponse, self.instance.report, msg="CSP Contain Rule was triggered")

    def test_compare_must_avoid_ko(self):
        headers = {
            'X-XSS-Protection': '1; mode=block',
            'Content-Security-Policy': "default-src 'none'; script-src 'self'; object-src 'self'; "
                                       "unsafe-inline 'self;"
        }
        csp_avoid_response = {
            'severity': 'medium',
            'rule': 'Content-Security-Policy',
            'message': 'Must-Avoid directive included',
            'expected': ['unsafe-inline', 'unsafe-eval'], 'delimiter': ';',
            'value': "default-src 'none'; script-src 'self'; object-src 'self'; unsafe-inline 'self;",
            'anomaly': 'unsafe-inline'
        }
        self._process_test(headers=headers, status_code=200)
        self.assertIn(csp_avoid_response, self.instance.report, msg="CSP Avoid Rule was triggered")

    def test_compare_optional(self):
        headers = {
            'X-XSS-Protection': '1; mode=block',
            'Set-Cookie': ['Test']
        }
        medium_contain_response = {
            'severity': 'medium',
            'rule': 'Set-Cookie',
            'message': 'Must-Contain directive missed',
            'expected': ['httponly', 'secure'],
            'value': 'test',
            'delimiter': ';',
            'anomaly': 'httponly'
        }
        high_contain_response = {
            'severity': 'high',
            'rule': 'Set-Cookie',
            'message': 'Must-Contain directive missed',
            'expected': ['httponly', 'secure'],
            'delimiter': ';',
            'value': 'test',
            'anomaly': 'secure'
        }
        self._process_test(headers=headers, status_code=200)
        self.assertIn(medium_contain_response, self.instance.report, msg="Medium Rule was triggered")
        self.assertIn(high_contain_response, self.instance.report, msg="High Rule was triggered")

    def test_compare_optional_not_exist(self):
        headers = {
            'X-XSS-Protection': '1; mode=block'
        }
        header_not_included_response = {
            'rule': 'Set-Cookie',
            'severity': 'high',
            'message': 'Header not included in response',
        }

        self._process_test(headers=headers, status_code=200)
        self.assertIn(header_not_included_response, self.instance.report, msg="Httponly Rule was triggered")

    def test_referrer_policy_invalid_values(self):
        headers = {'Referrer-Policy': 'origin'}
        referrer_response = {
            'severity': 'high',
            'rule': 'Referrer-Policy',
            'message': 'Value does not match security policy',
            'expected': ['strict-origin', 'strict-origin-when-cross-origin', 'no-referrer'],
            'delimiter': ',',
            'value': 'origin'
        }
        self._process_test(headers=headers)
        self.assertIn(referrer_response, self.instance.report, msg="Referrer Policy Rule was triggered")

    def test_referrer_policy_valid_values(self):
        headers = {'Referrer-Policy': 'no-referrer'}

        # this need updating as there is no referrer-policy rule in the output
        no_referrer_response = {
            'severity': 'high',
            'rule': 'Referrer-Policy',
            'message': 'Value does not match security policy',
            'expected': ['strict-origin', 'strict-origin-when-cross-origin', 'no-referrer'],
            'value': 'no-referrer'
        }

        self._process_test(headers=headers)
        self.assertNotIn(no_referrer_response, self.instance.report, msg="No Referrer Policy Rule was triggered")

    def test_referrer_policy_invalid_values(self):
        headers = {'Referrer-Policy': 'no-referrerr'}

        # this need updating as there is no referrer-policy rule in the output
        no_referrer_response = {
            'severity': 'high',
            'rule': 'Referrer-Policy',
            'message': 'Value does not match security policy',
            'expected': ['strict-origin', 'strict-origin-when-cross-origin', 'no-referrer'],
            'value': 'no-referrerr'
        }

        self._process_test(headers=headers)
        self.assertNotIn(no_referrer_response, self.instance.report, msg="No Referrer Policy Rule was triggered")

    def test_referrer_policy_strict_origin(self):
        headers = {'Referrer-Policy': 'strict-origin'}

        # this needs updating because there is no refferer policy in output 
        no_referrer_response = {
            'severity': 'high',
            'rule': 'Referrer-Policy',
            'message': 'value does not match security policy',
            'expected': ['strict-origin', 'strict-origin-when-cross-origin', 'no-referrer'],
            'delimiter': ',',
            'value': 'strict-origin'
        }

        self._process_test(headers=headers)
        self.assertNotIn(no_referrer_response, self.instance.report, msg="Referrer SO Policy Rule was triggered")

    def test_referrer_policy_strict_cross_origin(self):
        headers = {'Referrer-Policy': 'strict-origin-when-cross-origin'}

        # this needs updating because there is no refferer policy in output 
        referrer_strict_orgin_response = {
            'severity': 'high',
            'rule': 'Referrer-Policy',
            'message': 'Value does not match security policy',
            'expected': ['strict-origin', 'strict-origin-when-cross-origin', 'no-referrer'],
            'delimiter': ';', 'value':
                'strict-origin-when-cross-origin'
        }

        self._process_test(headers=headers)
        self.assertNotIn(referrer_strict_orgin_response, self.instance.report,
                         msg="Refered SOWCO Policy Rule was triggred")

    def test_csp_invalid_default_directive(self):
        headers = {'Content-Security-Policy': "default-src 'random';"}

        # this needs updating because there is no Content-Security-Warining in output 
        csp_invalid_default_response = {
            'severity': 'high',
            'rule': 'Content-Security-Policy',
            'message': 'Must-Contain directive missed',
            'expected': ["default-src 'none'", "default-src 'self'"],
            'delimiter': ';',
            'value': "default-src 'random';",
            'anomaly': ["default-src 'none'", "default-src 'self'"]
        }

        self._process_test(headers=headers, status_code=200)
        self.assertIn(csp_invalid_default_response, self.instance.report, msg="CSP directive Policy Rule was triggered")

    def test_csp_valid_default_directive_none(self):
        headers = {'Content-Security-Policy': "default-src 'none';"}

        # this needs updating because there is no Content-Security-Warining in output
        csp_response_none = {
            'severity': 'high',
            'rule': 'Content-Security-Policy',
            'message': 'Must-Contain directive missed',
            'expected': ["default-src 'none'", "default-src 'self'"],
            'delimiter': ';',
            'value': "default-src 'none';",
            'anomaly': ["default-src 'none'", "default-src 'self'"]
        }

        self._process_test(headers=headers, status_code=200)
        self.assertNotIn(csp_response_none, self.instance.report, msg="CSP directive policy none was caught")

    def test_csp_invalid_default_directive_none(self):
        headers = {'Content-Security-Policy': "default-src 'non';"}

        # this needs updating because there is no Content-Security-Warining in output 
        csp_response_none = {
            'severity': 'high',
            'rule': 'Content-Security-Policy',
            'message': 'Must-Contain directive missed',
            'expected': ["default-src 'none'", "default-src 'self'"],
            'delimiter': ';',
            'value': "default-src 'non';",
            'anomaly': ["default-src 'none'", "default-src 'self'"]
        }

        self._process_test(headers=headers, status_code=200)
        self.assertIn(csp_response_none, self.instance.report, msg="CSP directive policy none was caught")

    def test_csp_valid_default_directive_self(self):
        headers = {'Content-Security-Policy': "default-src 'self';"}
        csp_response_self = {
            'severity': 'high',
            'rule': 'Content-Security-Policy',
            'message': 'Must-Contain directive missed',
            'expected': ["default-src 'none'", "default-src 'self'"],
            'delimiter': ';',
            'value': "default-src 'self';",
            'anomaly': ["default-src 'none'", "default-src 'self'"]
        }

        self._process_test(headers=headers, status_code=200)
        self.assertNotIn(csp_response_self, self.instance.report, msg="CSP directive policy self was caught")

    def test_csp_invalid_default_directive_self(self):
        headers = {'Content-Security-Policy': "default-src 'selfie';"}
        csp_response_self = {
            'severity': 'high',
            'rule': 'Content-Security-Policy',
            'message': 'Must-Contain directive missed',
            'expected': ["default-src 'none'", "default-src 'self'"],
            'delimiter': ';',
            'value': "default-src 'selfie';",
            'anomaly': ["default-src 'none'", "default-src 'self'"]
        }

        self._process_test(headers=headers, status_code=200)
        self.assertIn(csp_response_self, self.instance.report, msg="CSP directive policy self was caught")

    def test_compare_rules_full_output(self):
        headers = {
            'Server': 'Apache',
            'X-Generator': 'Drupal 7 (http://drupal.org)',
            'X-XSS-Protection': '1; mode=bloc',
            'Content-Security-Policy': "default-src 'random'; script-sr 'self'; object-src 'self'; unsafe-inline 'self;"
        }

        expected_report = [
            {'severity': 'high',
             'rule': 'Content-Security-Policy',
             'message': 'Must-Contain directive missed',
             'expected': ["default-src 'none'", "default-src 'self'"],
             'delimiter': ';',
             'value': "default-src 'random'; script-sr 'self'; object-src 'self'; unsafe-inline 'self;",
             'anomaly': ["default-src 'none'", "default-src 'self'"]
             },
            {'severity': 'medium',
             'rule': 'Content-Security-Policy',
             'message': 'Must-Avoid directive included',
             'expected': ['unsafe-inline', 'unsafe-eval'],
             'delimiter': ';',
             'value': "default-src 'random'; script-sr 'self'; object-src 'self'; unsafe-inline 'self;",
             'anomaly': 'unsafe-inline'
             },
            {'severity': 'high', 'rule': 'X-XSS-Protection',
             'message': 'Value does not match security policy',
             'expected': ['1', 'mode=block'],
             'delimiter': ';',
             'value': '1; mode=bloc'
             },
            {'severity': 'high',
             'rule': 'Server',
             'message': 'Header should not be returned'
             },
            {'severity': 'high',
             'rule': 'Strict-Transport-Security',
             'message': 'Header not included in response',
             'expected': ['max-age=31536000', 'includesubdomains'],
             'delimiter': ';'
             },
            {'severity': 'high',

             'rule': 'X-Frame-Options',
             'message': 'Header not included in response',
             'expected': ['sameorigin', 'deny'],
             'delimiter': ';'
             },
            {'severity': 'high',
             'rule': 'X-Content-Type-Options',
             'message': 'Header not included in response',
             'expected': ['nosniff'],
             'delimiter': ';'
             },

            {'message': 'Header not included in response',
             'rule': 'Set-Cookie',
             'severity': 'high',
             },

            {'severity': 'high',
             'rule': 'Referrer-Policy',
             'message': 'Header not included in response',
             'expected': ['strict-origin', 'strict-origin-when-cross-origin', 'no-referrer'],
             'delimiter': ','
             },
            {'severity': 'high',
             'rule': 'Cache-Control',
             'message': 'Header not included in response',
             # modified this to account for list value rather then string
             'expected': ['no-cache', 'no-store', 'must-revalidate'],
             'delimiter': ','
             },
            {'severity': 'high',
             'rule': 'Pragma',
             'message': 'Header not included in response',
             'expected': ['no-cache'],
             'delimiter': ';'},
            {'severity': 'high',
             'rule': 'X-Generator',
             'message': 'Header should not be returned'
             }]

        self._process_test(headers=headers, status_code=200)
        self.assertEqual(self.instance.report, expected_report, msg="Full report results matched")
class DrheaderRules(unittest2.TestCase):
    def setUp(self):
        # this is run each time before each test_ method is invoked
        self.logger = logging.Logger
        self.instance = ''
        self.report = list

        # configuration

    def tearDown(self):
        with open(os.path.join(os.path.dirname(__file__), 'testfiles/test_rules.yml'), 'w') as f_test,\
             open(os.path.join(os.path.dirname(__file__), 'testfiles/default_rules.yml')) as f_default:
            default_rules = yaml.safe_load(f_default.read())
            yaml.dump(default_rules, f_test, sort_keys=False)

    def _process_test(self, url=None, headers=None, status_code=None):
        # all tests use this method to run the test and analyze the results.
        with open(
                os.path.join(os.path.dirname(__file__),
                             'testfiles/test_rules.yml'), 'r') as f:
            rules = yaml.safe_load(f.read())['Headers']

        self.instance = Drheader(url=url,
                                 headers=headers,
                                 status_code=status_code)
        self.instance.analyze(rules=rules)

        # test can then make assertions against the contents of self.instance.report to determine success of failure.

    def test_get_headers_ok(self):
        url = 'https://google.com'
        self._process_test(url=url)
        self.assertNotEqual(self.report, None, msg="A Report was generated")

    def test_compare_rules_ok(self):
        with open(
                os.path.join(os.path.dirname(__file__),
                             'testfiles/header_ok.json'), 'r') as f:
            file = json.loads(f.read())

        self._process_test(headers=file, status_code=200)
        self.assertEqual(len(self.instance.report),
                         0,
                         msg=self.build_error_message(self.instance.report))

    def test_compare_rules_ok_with_case_insensitive_keys(self):
        with open(
                os.path.join(os.path.dirname(__file__),
                             'testfiles/header_ok.json'), 'r') as f:
            file = json.loads(f.read())

        file['x-xss-protection'] = file.pop('X-XSS-Protection')

        self._process_test(headers=file, status_code=200)
        self.assertEqual(len(self.instance.report),
                         0,
                         msg=self.build_error_message(self.instance.report))

    def test_compare_rules_ok_with_case_insensitive_values(self):
        with open(
                os.path.join(os.path.dirname(__file__),
                             'testfiles/header_ok.json'), 'r') as f:
            file = json.loads(f.read())

        file['Content-Security-Policy'] = file.pop(
            'Content-Security-Policy').upper()
        file['X-Frame-Options'] = file.pop('X-Frame-Options').lower()

        self._process_test(headers=file, status_code=200)
        self.assertEqual(len(self.instance.report),
                         0,
                         msg=self.build_error_message(self.instance.report))

    def test_compare_rules_enforce_ko(self):
        headers = {
            'X-XSS-Protection':
            '1; mode=bloc',
            'Content-Security-Policy':
            "default-src 'none'; script-src 'self'; object-src 'self';"
        }
        expected_response = {
            'severity': 'high',
            'rule': 'X-XSS-Protection',
            'message': 'Value does not match security policy',
            'expected': ['0'],
            'delimiter': ';',
            'value': '1; mode=bloc'
        }

        self._process_test(headers=headers, status_code=200)
        self.assertIn(expected_response, self.instance.report, msg="X-XSS")

    def test_compare_rules_required_ko(self):
        headers = {'X-XSS-Protection': '1; mode=block'}
        expected_response = {
            'severity': 'high',
            'rule': 'Content-Security-Policy',
            'message': 'Header not included in response'
        }

        self._process_test(headers=headers, status_code=200)
        self.assertIn(expected_response,
                      self.instance.report,
                      msg="Generated Rules")

    def test_compare_rules_not_required_ko(self):
        headers = {
            'X-XSS-Protection': '1; mode=block',
            'Content-Security-Policy':
            "default-src 'none'; script-src 'self'; object-src 'self';",
            'Server': 'Apache',
            'X-Generator': 'Drupal 7 (http://drupal.org)'
        }
        server_response = {
            'severity': 'high',
            'rule': 'Server',
            'message': 'Header should not be returned'
        }
        generator_response = {
            'severity': 'high',
            'rule': 'X-Generator',
            'message': 'Header should not be returned'
        }

        self._process_test(headers=headers, status_code=200)
        self.assertIn(server_response,
                      self.instance.report,
                      msg="Server Rule was triggered")
        self.assertIn(generator_response,
                      self.instance.report,
                      msg="Generator Rule was triggered")

    def test_compare_must_contain_ko(self):
        headers = {
            'X-XSS-Protection': '1; mode=block',
            'Content-Security-Policy':
            "default-src 'random'; script-src 'self'"
        }
        csp_contain_response = {
            'severity': 'high',
            'rule': 'Content-Security-Policy',
            'message': 'Must-Contain-One directive missed',
            'expected': ["default-src 'none'", "default-src 'self'"],
            'delimiter': ';',
            'value': "default-src 'random'; script-src 'self'",
            'anomaly': ["default-src 'none'", "default-src 'self'"]
        }

        self._process_test(headers=headers, status_code=200)
        self.assertIn(csp_contain_response,
                      self.instance.report,
                      msg="CSP Contain Rule was triggered")

    def test_compare_must_avoid_ko(self):
        headers = {
            'X-XSS-Protection':
            '1; mode=block',
            'Content-Security-Policy':
            "default-src 'none'; script-src 'self'; object-src 'self'; "
            "connect-src 'unsafe-inline';"
        }
        csp_avoid_response = {
            'severity': 'medium',
            'rule': 'Content-Security-Policy - connect-src',
            'message': 'Must-Avoid directive included',
            'avoid': ['unsafe-inline', 'unsafe-eval'],
            'delimiter': ';',
            'value': "unsafe-inline",
            'anomaly': 'unsafe-inline'
        }

        self._process_test(headers=headers, status_code=200)
        self.assertIn(csp_avoid_response,
                      self.instance.report,
                      msg="CSP Avoid Rule was triggered")

    def test_compare_optional(self):
        headers = {'X-XSS-Protection': '0', 'Set-Cookie': ['Test']}
        medium_contain_response = {
            'severity': 'medium',
            'rule': 'Set-Cookie',
            'message': 'Must-Contain directive missed',
            'expected': ['httponly', 'secure'],
            'value': 'test',
            'delimiter': ';',
            'anomaly': 'httponly'
        }
        high_contain_response = {
            'severity': 'high',
            'rule': 'Set-Cookie',
            'message': 'Must-Contain directive missed',
            'expected': ['httponly', 'secure'],
            'delimiter': ';',
            'value': 'test',
            'anomaly': 'secure'
        }

        self._process_test(headers=headers, status_code=200)
        self.assertIn(medium_contain_response,
                      self.instance.report,
                      msg="Medium Rule was triggered")
        self.assertIn(high_contain_response,
                      self.instance.report,
                      msg="High Rule was triggered")

    def test_compare_optional_not_exist(self):
        headers = {'X-XSS-Protection': '1; mode=block'}
        header_not_included_response = {
            'rule': 'Set-Cookie',
            'severity': 'high',
            'message': 'Header not included in response',
        }

        self._process_test(headers=headers, status_code=200)
        self.assertNotIn(header_not_included_response,
                         self.instance.report,
                         msg="Httponly Rule was triggered")

    def test_referrer_policy_invalid_values(self):
        headers = {'Referrer-Policy': 'origin'}
        referrer_response = {
            'severity':
            'high',
            'rule':
            'Referrer-Policy',
            'message':
            'Must-Contain-One directive missed',
            'expected': [
                'strict-origin', 'strict-origin-when-cross-origin',
                'no-referrer'
            ],
            'delimiter':
            ',',
            'value':
            'origin',
            'anomaly': [
                'strict-origin', 'strict-origin-when-cross-origin',
                'no-referrer'
            ]
        }

        self._process_test(headers=headers)
        self.assertIn(referrer_response,
                      self.instance.report,
                      msg="Referrer Policy Rule was triggered")

    def test_referrer_policy_valid_values(self):
        headers = {'Referrer-Policy': 'no-referrer'}

        # this need updating as there is no referrer-policy rule in the output
        no_referrer_response = {
            'severity':
            'high',
            'rule':
            'Referrer-Policy',
            'message':
            'Must-Contain-One directive missed',
            'expected': [
                'strict-origin', 'strict-origin-when-cross-origin',
                'no-referrer'
            ],
            'delimiter':
            ',',
            'value':
            'no-referrer',
            'anomaly': [
                'strict-origin', 'strict-origin-when-cross-origin',
                'no-referrer'
            ]
        }

        self._process_test(headers=headers)
        self.assertNotIn(no_referrer_response,
                         self.instance.report,
                         msg="No Referrer Policy Rule was triggered")

    def test_referrer_policy_invalid_values_typo(self):
        headers = {'Referrer-Policy': 'no-referrerr'}

        # this need updating as there is no referrer-policy rule in the output
        no_referrer_response = {
            'severity':
            'high',
            'rule':
            'Referrer-Policy',
            'message':
            'Value does not match security policy',
            'expected': [
                'strict-origin', 'strict-origin-when-cross-origin',
                'no-referrer'
            ],
            'value':
            'no-referrerr'
        }

        self._process_test(headers=headers)
        self.assertNotIn(no_referrer_response,
                         self.instance.report,
                         msg="No Referrer Policy Rule was triggered")

    def test_referrer_policy_strict_origin(self):
        headers = {'Referrer-Policy': 'strict-origin'}

        # this needs updating because there is no refferer policy in output
        no_referrer_response = {
            'severity':
            'high',
            'rule':
            'Referrer-Policy',
            'message':
            'value does not match security policy',
            'expected': [
                'strict-origin', 'strict-origin-when-cross-origin',
                'no-referrer'
            ],
            'delimiter':
            ',',
            'value':
            'strict-origin'
        }

        self._process_test(headers=headers)
        self.assertNotIn(no_referrer_response,
                         self.instance.report,
                         msg="Referrer SO Policy Rule was triggered")

    def test_referrer_policy_strict_cross_origin(self):
        headers = {'Referrer-Policy': 'strict-origin-when-cross-origin'}

        # this needs updating because there is no refferer policy in output
        referrer_strict_origin_response = {
            'severity':
            'high',
            'rule':
            'Referrer-Policy',
            'message':
            'Value does not match security policy',
            'expected': [
                'strict-origin', 'strict-origin-when-cross-origin',
                'no-referrer'
            ],
            'delimiter':
            ';',
            'value':
            'strict-origin-when-cross-origin'
        }

        self._process_test(headers=headers)
        self.assertNotIn(referrer_strict_origin_response,
                         self.instance.report,
                         msg="Refered SOWCO Policy Rule was triggred")

    def test_csp_invalid_default_directive(self):
        headers = {'Content-Security-Policy': "default-src 'random';"}

        # this needs updating because there is no Content-Security-Warining in output
        csp_invalid_default_response = {
            'severity': 'high',
            'rule': 'Content-Security-Policy',
            'message': 'Must-Contain-One directive missed',
            'expected': ["default-src 'none'", "default-src 'self'"],
            'delimiter': ';',
            'value': "default-src 'random';",
            'anomaly': ["default-src 'none'", "default-src 'self'"]
        }

        self._process_test(headers=headers, status_code=200)
        self.assertIn(csp_invalid_default_response,
                      self.instance.report,
                      msg="CSP directive Policy Rule was triggered")

    def test_csp_valid_default_directive_none(self):
        headers = {'Content-Security-Policy': "default-src 'none';"}

        # this needs updating because there is no Content-Security-Warining in output
        csp_response_none = {
            'severity': 'high',
            'rule': 'Content-Security-Policy',
            'message': 'Must-Contain directive missed',
            'expected': ["default-src 'none'", "default-src 'self'"],
            'delimiter': ';',
            'value': "default-src 'none';",
            'anomaly': ["default-src 'none'", "default-src 'self'"]
        }

        self._process_test(headers=headers, status_code=200)
        self.assertNotIn(csp_response_none,
                         self.instance.report,
                         msg="CSP directive policy none was caught")

    def test_csp_invalid_default_directive_none(self):
        headers = {'Content-Security-Policy': "default-src 'non';"}

        # this needs updating because there is no Content-Security-Warining in output
        csp_response_none = {
            'severity': 'high',
            'rule': 'Content-Security-Policy',
            'message': 'Must-Contain-One directive missed',
            'expected': ["default-src 'none'", "default-src 'self'"],
            'delimiter': ';',
            'value': "default-src 'non';",
            'anomaly': ["default-src 'none'", "default-src 'self'"]
        }

        self._process_test(headers=headers, status_code=200)
        self.assertIn(csp_response_none,
                      self.instance.report,
                      msg="CSP directive policy none was caught")

    def test_csp_valid_default_directive_self(self):
        headers = {'Content-Security-Policy': "default-src 'self';"}
        csp_response_self = {
            'severity': 'high',
            'rule': 'Content-Security-Policy',
            'message': 'Must-Contain-One directive missed',
            'expected': ["default-src 'none'", "default-src 'self'"],
            'delimiter': ';',
            'value': "default-src 'self';",
            'anomaly': ["default-src 'none'", "default-src 'self'"]
        }

        self._process_test(headers=headers, status_code=200)
        self.assertNotIn(csp_response_self,
                         self.instance.report,
                         msg="CSP directive policy self was caught")

    def test_csp_invalid_default_directive_self(self):
        headers = {'Content-Security-Policy': "default-src 'selfie';"}
        csp_response_self = {
            'severity': 'high',
            'rule': 'Content-Security-Policy',
            'message': 'Must-Contain-One directive missed',
            'expected': ["default-src 'none'", "default-src 'self'"],
            'delimiter': ';',
            'value': "default-src 'selfie';",
            'anomaly': ["default-src 'none'", "default-src 'self'"]
        }

        self._process_test(headers=headers, status_code=200)
        self.assertIn(csp_response_self,
                      self.instance.report,
                      msg="CSP directive policy self was caught")

    def test_compare_rules_full_output(self):
        headers = {
            'Server':
            'Apache',
            'X-Generator':
            'Drupal 7 (http://drupal.org)',
            'X-XSS-Protection':
            '1; mode=bloc',
            'Content-Security-Policy':
            "default-src 'random'; script-src 'self'; object-src 'self'; "
            "connect-src 'unsafe-inline';"
        }
        expected_report = [{
            'severity':
            'high',
            'rule':
            'Content-Security-Policy',
            'message':
            'Must-Contain-One directive missed',
            'expected': ["default-src 'none'", "default-src 'self'"],
            'delimiter':
            ';',
            'value':
            "default-src 'random'; script-src 'self'; object-src 'self'; connect-src 'unsafe-inline';",
            'anomaly': ["default-src 'none'", "default-src 'self'"]
        }, {
            'severity': 'medium',
            'rule': 'Content-Security-Policy - connect-src',
            'message': 'Must-Avoid directive included',
            'avoid': ['unsafe-inline', 'unsafe-eval'],
            'delimiter': ';',
            'value': "unsafe-inline",
            'anomaly': 'unsafe-inline'
        }, {
            'severity': 'high',
            'rule': 'X-XSS-Protection',
            'message': 'Value does not match security policy',
            'expected': ['0'],
            'delimiter': ';',
            'value': '1; mode=bloc'
        }, {
            'severity': 'high',
            'rule': 'Server',
            'message': 'Header should not be returned'
        }, {
            'severity':
            'high',
            'rule':
            'Strict-Transport-Security',
            'message':
            'Header not included in response',
            'expected': ['max-age=31536000', 'includesubdomains'],
            'delimiter':
            ';'
        }, {
            'severity': 'high',
            'rule': 'X-Frame-Options',
            'message': 'Header not included in response',
            'expected': ['sameorigin', 'deny'],
            'delimiter': ';'
        }, {
            'severity': 'high',
            'rule': 'X-Content-Type-Options',
            'message': 'Header not included in response',
            'expected': ['nosniff'],
            'delimiter': ';'
        }, {
            'severity': 'high',
            'rule': 'Referrer-Policy',
            'message': 'Header not included in response'
        }, {
            'severity': 'high',
            'rule': 'Cache-Control',
            'message': 'Header not included in response',
            'expected': ['no-store', 'max-age=0'],
            'delimiter': ','
        }, {
            'severity': 'high',
            'rule': 'Pragma',
            'message': 'Header not included in response',
            'expected': ['no-cache'],
            'delimiter': ';'
        }, {
            'severity': 'high',
            'rule': 'X-Generator',
            'message': 'Header should not be returned'
        }]

        self._process_test(headers=headers, status_code=200)
        self.assertEqual(self.instance.report,
                         expected_report,
                         msg=self.build_error_message(self.instance.report,
                                                      expected_report))

    def test_csp_required_directive_not_present(self):
        with open(os.path.join(os.path.dirname(__file__), 'testfiles/header_ok.json'), 'r') as f_headers,\
             open(os.path.join(os.path.dirname(__file__), 'testfiles/test_rules.yml'), 'r') as f_rules:
            headers = json.loads(f_headers.read())
            rules = yaml.safe_load(f_rules.read())

        rule_value = rules['Headers']['Content-Security-Policy']
        rule_value['Directives'] = {
            'script-src': {
                'Required': True,
                'Enforce': False
            }
        }
        self.modify_rules('Content-Security-Policy', rule_value)

        directive = re.search('script-src [^;]*(;)?',
                              headers['Content-Security-Policy']).group()
        headers['Content-Security-Policy'] = headers[
            'Content-Security-Policy'].replace(directive, '')

        expected_report = {
            'severity': 'high',
            'rule': 'Content-Security-Policy - script-src',
            'message': 'Directive not included in response'
        }
        self._process_test(headers=headers, status_code=200)
        self.assertIn(expected_report, self.instance.report)

    def test_csp_directive_invalid_value(self):
        with open(os.path.join(os.path.dirname(__file__), 'testfiles/header_ok.json'), 'r') as f_headers,\
             open(os.path.join(os.path.dirname(__file__), 'testfiles/test_rules.yml'), 'r') as f_rules:
            headers = json.loads(f_headers.read())
            rules = yaml.safe_load(f_rules.read())

        rule_value = rules['Headers']['Content-Security-Policy']
        rule_value['Directives'] = {
            'script-src': {
                'Required': True,
                'Enforce': True,
                'Delimiter': ' ',
                'Value': ['self']
            }
        }
        self.modify_rules('Content-Security-Policy', rule_value)

        directive = re.search('script-src [^;]*(;)?',
                              headers['Content-Security-Policy']).group()
        headers['Content-Security-Policy'] = headers[
            'Content-Security-Policy'].replace(
                directive,
                'script-src https://www.santander.co.uk https://www.google.com;'
            )

        expected_report = {
            'severity': 'high',
            'rule': 'Content-Security-Policy - script-src',
            'message': 'Value does not match security policy',
            'expected': ['self'],
            'delimiter': ' ',
            'value': 'https://www.santander.co.uk https://www.google.com'
        }
        self._process_test(headers=headers, status_code=200)
        self.assertIn(expected_report, self.instance.report)

    def test_csp_directive_must_avoid_value_included(self):
        with open(os.path.join(os.path.dirname(__file__), 'testfiles/header_ok.json'), 'r') as f_headers,\
             open(os.path.join(os.path.dirname(__file__), 'testfiles/test_rules.yml'), 'r') as f_rules:
            headers = json.loads(f_headers.read())
            rules = yaml.safe_load(f_rules.read())

        rule_value = rules['Headers']['Content-Security-Policy']
        rule_value['Directives'] = {
            'script-src': {
                'Required': True,
                'Enforce': False,
                'Delimiter': ' ',
                'Value': '',
                'Must-Avoid': ['https://www.santander.co.uk']
            }
        }
        self.modify_rules('Content-Security-Policy', rule_value)

        directive = re.search('script-src [^;]*(;)?',
                              headers['Content-Security-Policy']).group()
        headers['Content-Security-Policy'] = headers[
            'Content-Security-Policy'].replace(
                directive,
                'script-src https://www.santander.co.uk https://www.google.com;'
            )

        expected_report = {
            'severity': 'medium',
            'rule': 'Content-Security-Policy - script-src',
            'message': 'Must-Avoid directive included',
            'avoid': ['https://www.santander.co.uk'],
            'delimiter': ' ',
            'value': 'https://www.santander.co.uk https://www.google.com',
            'anomaly': 'https://www.santander.co.uk'
        }
        self._process_test(headers=headers, status_code=200)
        self.assertIn(expected_report, self.instance.report)

    def test_csp_directive_must_contain_value_not_included(self):
        with open(os.path.join(os.path.dirname(__file__), 'testfiles/header_ok.json'), 'r') as f_headers,\
             open(os.path.join(os.path.dirname(__file__), 'testfiles/test_rules.yml'), 'r') as f_rules:
            headers = json.loads(f_headers.read())
            rules = yaml.safe_load(f_rules.read())

        rule_value = rules['Headers']['Content-Security-Policy']
        rule_value['Directives'] = {
            'script-src': {
                'Required': True,
                'Enforce': False,
                'Delimiter': ' ',
                'Value': '',
                'Must-Contain': ['https://www.santander.co.uk']
            }
        }
        self.modify_rules('Content-Security-Policy', rule_value)

        directive = re.search('script-src [^;]*(;)?',
                              headers['Content-Security-Policy']).group()
        headers['Content-Security-Policy'] = headers[
            'Content-Security-Policy'].replace(directive,
                                               'script-src \'self\';')

        expected_report = {
            'severity': 'medium',
            'rule': 'Content-Security-Policy - script-src',
            'message': 'Must-Contain directive missed',
            'expected': ['https://www.santander.co.uk'],
            'delimiter': ' ',
            'value': 'self',
            'anomaly': 'https://www.santander.co.uk'
        }
        self._process_test(headers=headers, status_code=200)
        self.assertIn(expected_report, self.instance.report)

    def test_csp_directive_must_contain_one_value_not_included(self):
        with open(os.path.join(os.path.dirname(__file__), 'testfiles/header_ok.json'), 'r') as f_headers,\
             open(os.path.join(os.path.dirname(__file__), 'testfiles/test_rules.yml'), 'r') as f_rules:
            headers = json.loads(f_headers.read())
            rules = yaml.safe_load(f_rules.read())

        rule_value = rules['Headers']['Content-Security-Policy']
        rule_value['Directives'] = {
            'script-src': {
                'Required':
                True,
                'Enforce':
                False,
                'Delimiter':
                ' ',
                'Value':
                '',
                'Must-Contain-One':
                ['https://www.santander.co.uk', 'https://www.google.com']
            }
        }
        self.modify_rules('Content-Security-Policy', rule_value)

        directive = re.search('script-src [^;]*(;)?',
                              headers['Content-Security-Policy']).group()
        headers['Content-Security-Policy'] = headers[
            'Content-Security-Policy'].replace(directive,
                                               'script-src \'self\';')

        expected_report = {
            'severity': 'high',
            'rule': 'Content-Security-Policy - script-src',
            'message': 'Must-Contain-One directive missed',
            'expected':
            ['https://www.santander.co.uk', 'https://www.google.com'],
            'delimiter': ' ',
            'value': 'self',
            'anomaly':
            ['https://www.santander.co.uk', 'https://www.google.com']
        }
        self._process_test(headers=headers, status_code=200)
        self.assertIn(expected_report, self.instance.report)

    @staticmethod
    def modify_rules(rule, rule_value):
        with open(
                os.path.join(os.path.dirname(__file__),
                             'testfiles/test_rules.yml'), 'r+') as f:
            rules = yaml.safe_load(f.read())
            rules['Headers'][rule] = rule_value
            yaml.dump(rules, f)

    @staticmethod
    def build_error_message(report, expected_report=None):
        if expected_report is None:
            expected_report = []
        elif type(expected_report) is dict:
            expected_report = expected_report.items()

        unexpected_items = []
        for item in report:
            if item not in expected_report:
                unexpected_items.append(item)

        missing_items = []
        for item in expected_report:
            if item not in report:
                missing_items.append(item)

        error_message = ""
        if len(unexpected_items) > 0:
            error_message += "\nThe following items were found but were not expected in the report: \n"
            error_message += json.dumps(unexpected_items, indent=2)

        if len(missing_items) > 0:
            error_message += "\nThe following items were not found but were expected in the report: \n"
            error_message += json.dumps(missing_items, indent=2)

        return error_message