# Copyright 2016 IBM Corp.
#
# SPDX-License-Identifier: Apache-2.0
import os
import shutil
import subprocess

import fixtures
import testtools

new_candidates_all_total_lines = "Total lines of code: 12"
new_candidates_some_total_lines = "Total lines of code: 9"
new_candidates_no_nosec_lines = "Total lines skipped (#nosec): 0"
new_candidates_skip_nosec_lines = "Total lines skipped (#nosec): 3"
baseline_no_skipped_files = "Files skipped (0):"
baseline_no_issues_found = "No issues identified."
xml_sax_issue_id = "Issue: [B317:blacklist]"
yaml_load_issue_id = "Issue: [B506:yaml_load]"
shell_issue_id = "Issue: [B602:subprocess_popen_with_shell_equals_true]"
candidate_example_one = "subprocess.Popen('/bin/ls *', shell=True)"
candidate_example_two = "subprocess.Popen('/bin/ls *', shell=True) # nosec"
candidate_example_three = "y = yaml.load(temp_str)"
candidate_example_four = "y = yaml.load(temp_str) # nosec"
candidate_example_five = "xml.sax.make_parser()"
candidate_example_six = "xml.sax.make_parser() # nosec"


class BaselineFunctionalTests(testtools.TestCase):
    """Functional tests for Bandit baseline.

    This set of tests is used to verify that the baseline comparison handles
    finding and comparing results appropriately. The only comparison is the
    number of candidates per file, meaning that any candidates found may
    already exist in the baseline. In this case, all candidates are flagged
    and a user will need to investigate the candidates related to that file.
    """

    def setUp(self):
        super().setUp()
        self.examples_path = "examples"
        self.baseline_commands = ["bandit", "-r"]
        self.baseline_report_file = "baseline_report.json"

    def _run_bandit_baseline(self, target_directory, baseline_file):
        """A helper method to run bandit baseline

        This method will run the bandit baseline test provided an existing
        baseline report and the target directory containing the content to be
        tested.
        :param target_directory: Directory containing content to be compared
        :param baseline_file: File containing an existing baseline report
        :return The baseline test results and return code
        """
        cmds = self.baseline_commands + ["-b", baseline_file, target_directory]
        process = subprocess.Popen(
            cmds,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            close_fds=True,
        )
        stdout, stderr = process.communicate()
        return (stdout.decode("utf-8"), process.poll())

    def _create_baseline(self, baseline_paired_files):
        """A helper method to create a baseline to use during baseline test

        This method will run bandit to create an initial baseline that can
        then be used during the bandit baseline test. Since the file contents
        of the baseline report can be extremely dynamic and difficult to create
        ahead of time, we do this at runtime to reduce the risk of missing
        something. To do this, we must temporary replace the file contents
        with different code which will produce the proper baseline results to
        be used during the baseline test.
        :param baseline_paired_files A dictionary based set of files for which
        to create the baseline report with. For each key file, a value file
        is provided, which contains content to use in place of the key file
        when the baseline report is created initially.
        :return The target directory for the baseline test and the return code
        of the bandit run to help determine whether the baseline report was
        populated
        """
        target_directory = self.useFixture(fixtures.TempDir()).path
        baseline_results = os.path.join(
            target_directory, self.baseline_report_file
        )
        for key_file, value_file in baseline_paired_files.items():
            shutil.copy(
                os.path.join(self.examples_path, value_file),
                os.path.join(target_directory, key_file),
            )
        cmds = self.baseline_commands + [
            "-f",
            "json",
            "-o",
            baseline_results,
            target_directory,
        ]
        process = subprocess.Popen(
            cmds,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            close_fds=True,
        )
        stdout, stderr = process.communicate()
        return_code = process.poll()
        for key_file, value_file in baseline_paired_files.items():
            shutil.copy(
                os.path.join(self.examples_path, key_file),
                os.path.join(target_directory, key_file),
            )
        return (target_directory, return_code)

    def test_no_new_candidates(self):
        """Tests when there are no new candidates

        Test that bandit returns no issues found, as there are no new
        candidates found compared with those found from the baseline.
        """
        baseline_report_files = {
            "new_candidates-all.py": "new_candidates-all.py"
        }
        target_directory, baseline_code = self._create_baseline(
            baseline_report_files
        )
        # assert the initial baseline found results
        self.assertEqual(1, baseline_code)
        baseline_report = os.path.join(
            target_directory, self.baseline_report_file
        )
        return_value, return_code = self._run_bandit_baseline(
            target_directory, baseline_report
        )
        # assert there were no results (no candidates found)
        self.assertEqual(0, return_code)
        self.assertIn(new_candidates_all_total_lines, return_value)
        self.assertIn(new_candidates_skip_nosec_lines, return_value)
        self.assertIn(baseline_no_skipped_files, return_value)
        self.assertIn(baseline_no_issues_found, return_value)

    def test_no_existing_no_new_candidates(self):
        """Tests when there are no new or existing candidates

        Test file with no existing candidates from baseline and no new
        candidates.
        """
        baseline_report_files = {"okay.py": "okay.py"}
        target_directory, baseline_code = self._create_baseline(
            baseline_report_files
        )
        # assert the initial baseline found nothing
        self.assertEqual(0, baseline_code)
        baseline_report = os.path.join(
            target_directory, self.baseline_report_file
        )
        return_value, return_code = self._run_bandit_baseline(
            target_directory, baseline_report
        )
        # assert there were no results (no candidates found)
        self.assertEqual(0, return_code)
        self.assertIn("Total lines of code: 1", return_value)
        self.assertIn(new_candidates_no_nosec_lines, return_value)
        self.assertIn(baseline_no_skipped_files, return_value)
        self.assertIn(baseline_no_issues_found, return_value)

    def test_no_existing_with_new_candidates(self):
        """Tests when there are new candidates and no existing candidates

        Test that bandit returns issues found in file that had no existing
        candidates from baseline but now contain candidates.
        """
        baseline_report_files = {
            "new_candidates-all.py": "new_candidates-none.py"
        }
        target_directory, baseline_code = self._create_baseline(
            baseline_report_files
        )
        # assert the initial baseline found nothing
        self.assertEqual(0, baseline_code)
        baseline_report = os.path.join(
            target_directory, self.baseline_report_file
        )
        return_value, return_code = self._run_bandit_baseline(
            target_directory, baseline_report
        )
        # assert there were results (candidates found)
        self.assertEqual(1, return_code)
        self.assertIn(new_candidates_all_total_lines, return_value)
        self.assertIn(new_candidates_skip_nosec_lines, return_value)
        self.assertIn(baseline_no_skipped_files, return_value)
        self.assertIn(xml_sax_issue_id, return_value)
        self.assertIn(yaml_load_issue_id, return_value)
        self.assertIn(shell_issue_id, return_value)
        # candidate #1
        self.assertIn(candidate_example_one, return_value)
        # candidate #3
        self.assertIn(candidate_example_three, return_value)
        # candidate #5
        self.assertIn(candidate_example_five, return_value)

    def test_existing_and_new_candidates(self):
        """Tests when tere are new candidates and existing candidates

        Test that bandit returns issues found in file with existing
        candidates. The new candidates should be returned in this case.
        """
        baseline_report_files = {
            "new_candidates-all.py": "new_candidates-some.py"
        }
        target_directory, baseline_code = self._create_baseline(
            baseline_report_files
        )
        # assert the initial baseline found results
        self.assertEqual(1, baseline_code)
        baseline_report = os.path.join(
            target_directory, self.baseline_report_file
        )
        return_value, return_code = self._run_bandit_baseline(
            target_directory, baseline_report
        )
        # assert there were results (candidates found)
        self.assertEqual(1, return_code)
        self.assertIn(new_candidates_all_total_lines, return_value)
        self.assertIn(new_candidates_skip_nosec_lines, return_value)
        self.assertIn(baseline_no_skipped_files, return_value)
        self.assertIn(xml_sax_issue_id, return_value)
        self.assertIn(yaml_load_issue_id, return_value)
        # candidate #3
        self.assertIn(candidate_example_three, return_value)
        # candidate #5
        self.assertIn(candidate_example_five, return_value)

    def test_no_new_candidates_include_nosec(self):
        """Test to check nosec references with no new candidates

        Test that nosec references are included during a baseline test, which
        would normally be ignored. In this test case, there are no new
        candidates even while including the nosec references.
        """
        self.baseline_commands.append("--ignore-nosec")
        baseline_report_files = {
            "new_candidates-all.py": "new_candidates-all.py"
        }
        target_directory, baseline_code = self._create_baseline(
            baseline_report_files
        )
        # assert the initial baseline found results
        self.assertEqual(1, baseline_code)
        baseline_report = os.path.join(
            target_directory, self.baseline_report_file
        )
        return_value, return_code = self._run_bandit_baseline(
            target_directory, baseline_report
        )
        # assert there were no results (candidates found)
        self.assertEqual(0, return_code)
        self.assertIn(new_candidates_all_total_lines, return_value)
        self.assertIn(new_candidates_no_nosec_lines, return_value)
        self.assertIn(baseline_no_skipped_files, return_value)
        self.assertIn(baseline_no_issues_found, return_value)

    def test_new_candidates_include_nosec_only_nosecs(self):
        """Test to check nosec references with new only nosec candidates

        Test that nosec references are included during a baseline test, which
        would normally be ignored. In this test case, there are new candidates
        which are specifically nosec references.
        """
        self.baseline_commands.append("--ignore-nosec")
        baseline_report_files = {
            "new_candidates-nosec.py": "new_candidates-none.py"
        }
        target_directory, baseline_code = self._create_baseline(
            baseline_report_files
        )
        # assert the initial baseline found nothing
        self.assertEqual(0, baseline_code)
        baseline_report = os.path.join(
            target_directory, self.baseline_report_file
        )
        return_value, return_code = self._run_bandit_baseline(
            target_directory, baseline_report
        )
        # assert there were results (candidates found)
        self.assertEqual(1, return_code)
        self.assertIn(new_candidates_some_total_lines, return_value)
        self.assertIn(new_candidates_no_nosec_lines, return_value)
        self.assertIn(baseline_no_skipped_files, return_value)
        self.assertIn(xml_sax_issue_id, return_value)
        self.assertIn(yaml_load_issue_id, return_value)
        self.assertIn(shell_issue_id, return_value)
        # candidate #2
        self.assertIn(candidate_example_two, return_value)
        # candidate #4
        self.assertIn(candidate_example_four, return_value)
        # candidate #6
        self.assertIn(candidate_example_six, return_value)

    def test_new_candidates_include_nosec_new_nosecs(self):
        """Test to check nosec references with new candidates, including nosecs

        Test that nosec references are included during a baseline test, which
        would normally be ignored. In this test case, there are new candidates
        that also includes new nosec references as well.
        """
        self.baseline_commands.append("--ignore-nosec")
        baseline_report_files = {
            "new_candidates-all.py": "new_candidates-none.py"
        }
        target_directory, baseline_code = self._create_baseline(
            baseline_report_files
        )
        # assert the initial baseline found nothing
        self.assertEqual(0, baseline_code)
        baseline_report = os.path.join(
            target_directory, self.baseline_report_file
        )
        return_value, return_code = self._run_bandit_baseline(
            target_directory, baseline_report
        )
        # assert there were results (candidates found)
        self.assertEqual(1, return_code)
        self.assertIn(new_candidates_all_total_lines, return_value)
        self.assertIn(new_candidates_no_nosec_lines, return_value)
        self.assertIn(baseline_no_skipped_files, return_value)
        self.assertIn(xml_sax_issue_id, return_value)
        self.assertIn(yaml_load_issue_id, return_value)
        self.assertIn(shell_issue_id, return_value)
        # candidate #1
        self.assertIn(candidate_example_one, return_value)
        # candidate #2
        self.assertIn(candidate_example_two, return_value)
        # candidate #3
        self.assertIn(candidate_example_three, return_value)
        # candidate #4
        self.assertIn(candidate_example_four, return_value)
        # candidate #5
        self.assertIn(candidate_example_five, return_value)
        # candidate #6
        self.assertIn(candidate_example_six, return_value)
