def test_one_stream(self):
        stream_one = StringIO()
        sh_redirect_to_multiple_streams_fn_callback = \
            create_sh_redirect_to_multiple_streams_fn_callback([
                stream_one
            ])

        sh_redirect_to_multiple_streams_fn_callback('data1')

        self.assertEqual('data1', stream_one.getvalue())
    def __curl_file(container_image_signature_file_path,
                    container_image_signature_name, signature_server_url,
                    signature_server_username, signature_server_password):
        """Sends the signature file"""
        # remove any trailing / from url
        signature_server_url = re.sub(r'/$', '', signature_server_url)
        container_image_signature_url = f"{signature_server_url}/{container_image_signature_name}"

        # calculate hashes
        with open(container_image_signature_file_path,
                  'rb') as container_image_signature_file:
            container_image_signature_file_contents = container_image_signature_file.read(
            )
            signature_file_md5 = hashlib.md5(
                container_image_signature_file_contents).hexdigest()
            signature_file_sha1 = hashlib.sha1(
                container_image_signature_file_contents).hexdigest()

        try:
            stdout_result = StringIO()
            stdout_callback = create_sh_redirect_to_multiple_streams_fn_callback(
                [sys.stdout, stdout_result])

            # -s: Silent
            # -S: Show error
            # -f: Don't print out failure document
            # -v: Verbose
            sh.curl(  # pylint: disable=no-member
                '-sSfv',
                '-X',
                'PUT',
                '--header',
                f'X-Checksum-Sha1:{signature_file_sha1}',
                '--header',
                f'X-Checksum-MD5:{signature_file_md5}',
                '--user',
                f"{signature_server_username}:{signature_server_password}",
                '--upload-file',
                container_image_signature_file_path,
                container_image_signature_url,
                _out=stdout_callback,
                _err_to_out=True,
                _tee='out')
        except sh.ErrorReturnCode as error:
            raise RuntimeError(
                f"Unexpected error curling signature file to signature server: {error}"
            ) from error

        return container_image_signature_url, signature_file_md5, signature_file_sha1
    def __import_pgp_key(
        pgp_private_key
    ):
        print("Import PGP private key to sign container image(s) with")
        try:
            # import the key

            # NOTE: GPG is weird in that it sends "none error" output to stderr even on success...
            #       so merge the stderr into stdout
            gpg_import_stdout_result = StringIO()
            gpg_import_stdout_callback = create_sh_redirect_to_multiple_streams_fn_callback([
                sys.stdout,
                gpg_import_stdout_result
            ])
            sh.gpg( # pylint: disable=no-member
                '--import',
                '--fingerprint',
                '--with-colons',
                '--import-options=import-show',
                _in=pgp_private_key,
                _out=gpg_import_stdout_callback,
                _err_to_out=True,
                _tee='out'
            )

            # get the fingerprint of the imported key
            #
            # NOTE: if more then one match just using first one...
            gpg_imported_pgp_private_key_fingerprints = re.findall(
                PodmanSign.GPG_IMPORT_FINGER_PRINT_REGEX,
                gpg_import_stdout_result.getvalue()
            )
            if len(gpg_imported_pgp_private_key_fingerprints) < 1:
                raise RuntimeError(
                    "Unexpected error getting PGP fingerprint for PGP key"
                    " to sign container image(s) with. See stdout and stderr for more info."
                )
            pgp_private_key_fingerprint = gpg_imported_pgp_private_key_fingerprints[0]

            print(
                "Imported PGP private key to sign container image(s) with: "
                f"fingerprint='{pgp_private_key_fingerprint}'"
            )
        except sh.ErrorReturnCode as error:
            raise RuntimeError(f"Unexpected error importing pgp private key: {error}") from error

        return pgp_private_key_fingerprint
    def __buildah_mount_container(buildah_unshare_comand, container_id):
        """Use buildah to mount a container.

        Parameters
        ----------
        buildah_unshare_comand : sh.buildah.unshare.bake()
            A baked sh.buildah.unshare command to use to run this command in the context off
            so that this can be done "rootless".
        container_id : str
            ID of the container to mount.

        Returns
        -------
        str
            Absolute path to the mounted container.
        """
        mount_path = None
        try:
            buildah_mount_out_buff = StringIO()
            buildah_mount_out_callback = create_sh_redirect_to_multiple_streams_fn_callback(
                [sys.stdout, buildah_mount_out_buff])
            buildah_mount_command = buildah_unshare_comand.bake(
                "buildah", "mount")
            buildah_mount_command('--storage-driver',
                                  'vfs',
                                  container_id,
                                  _out=buildah_mount_out_callback,
                                  _err=sys.stderr,
                                  _tee='err')
            mount_path = buildah_mount_out_buff.getvalue().rstrip()
        except sh.ErrorReturnCode as error:
            raise RuntimeError(
                f'Unexpected runtime error mounting container ({container_id}): {error}'
            ) from error

        return mount_path
Esempio n. 5
0
    def _run_step(self):
        """
        Runs the TSSC step implemented by this StepImplementer.

        Parameters
        ----------
        runtime_step_config : dict
            Step configuration to use when the StepImplementer runs the step with all of the
            various static, runtime, defaults, and environment configuration munged together.

        Returns
        -------
        dict
            Results of running this step.
        """

        # yml_path is required
        yml_path = self.get_config_value('yml_path')
        if yml_path is None:
            current_step_results = self.current_step_results()
            if 'options' in current_step_results:
                if 'yml_path' in current_step_results['options']:
                    yml_path = current_step_results['options'].get('yml_path')

        if yml_path is None:
            raise ValueError(
                'yml_path not specified in runtime args or in options')

        if not os.path.exists(yml_path):
            raise ValueError(
                f'Specified file in yml_path not found: {yml_path}')

        # Required: rules and exists
        rules_file = self.get_config_value('rules')
        if not os.path.exists(rules_file):
            raise ValueError(
                f'Rules file specified in tssc config not found: {rules_file}')

        configlint_results_file_path = self.write_working_file(
            'configlint_results_file.txt')
        try:
            # run config-lint writing stdout and stderr to the standard streams
            # as well as to a results file.
            with open(configlint_results_file_path,
                      'w') as configlint_results_file:
                out_callback = create_sh_redirect_to_multiple_streams_fn_callback(
                    [sys.stdout, configlint_results_file])
                err_callback = create_sh_redirect_to_multiple_streams_fn_callback(
                    [sys.stderr, configlint_results_file])

                sh.config_lint(  # pylint: disable=no-member
                    "-verbose",
                    "-debug",
                    "-rules",
                    rules_file,
                    yml_path,
                    _encoding='UTF-8',
                    _out=out_callback,
                    _err=err_callback,
                    _tee='err')
        except sh.ErrorReturnCode_255 as error:  # pylint: disable=no-member
            # NOTE: expected failure condition,
            #       aka, the config lint run, but found an issue
            raise TSSCException(
                'Failed config-lint scan. See standard out and standard error.'
            ) from error
        except sh.ErrorReturnCode as error:
            # NOTE: un-expected failure condition
            #       aka, the config lint failed to run for some reason
            raise RuntimeError(
                f'Unexpected Error invoking config-lint: {error}') from error

        results = {
            'result': {
                'success': True,
                'message': 'configlint step completed',
            },
            'report-artifacts': [{
                'name':
                'configlint-result-set',
                'path':
                f'file://{configlint_results_file_path}'
            }],
            'options': {
                'yml_path': yml_path
            }
        }
        return results
    def __run_oscap_scan(  # pylint: disable=too-many-arguments,too-many-locals,too-many-branches,too-many-statements
            buildah_unshare_comand,
            oscap_eval_type,
            oscap_input_file,
            oscap_out_file_path,
            oscap_xml_results_file_path,
            oscap_html_report_path,
            container_mount_path,
            oscap_profile=None,
            oscap_tailoring_file=None,
            oscap_fetch_remote_resources=True):
        """Run an oscap scan in the context of a buildah unshare to run "rootless".

        Parameters
        ----------
        buildah_unshare_comand : sh.buildah.unshare.bake()
            A baked sh.buildah.unshare command to use to run this command in the context off
            so that this can be done "rootless".
        oscap_eval_type : str
            The type of oscap eval to perform. Must be a valid oscap eval type.
            EX: xccdf, oval
        oscap_input_file : str
            Path to rules file passed to the oscap command.
        oscap_out_file_path : str
            Path to write the stdout and stderr of running the oscap command to.
        oscap_xml_results_file_path : str
            Write the scan results into this file.
        oscap_html_report_path : str
            Write the human readable (HTML) report into this file.
        container_mount_path : str
            Path to the mounted container to scan.
        oscap_tailoring_file : str
            XCCF Tailoring file.
            See:
            - https://www.open-scap.org/security-policies/customization/
            - https://www.open-scap.org/resources/documentation/customizing-scap-security-guide-for-your-use-case/ # pylint: disable=line-too-long
            - https://static.open-scap.org/openscap-1.2/oscap_user_manual.html#_how_to_tailor_source_data_stream # pylint: disable=line-too-long
        oscap_profile : str
            OpenSCAP profile to evaluate. Must be a valid profile in the given oscap_input_file.
            EX: if you perform an `oscap info oscap_input_file` the profile must be listed.

        Returns
        -------
        oscap_eval_success : bool
            True if oscap eval passed all rules
            False if oscap eval failed any rules
        oscap_eval_fails : str
            If oscap_eval_success is True then indeterminate.
            If oscap_eval_success is False then string of all of the failed rules.

        Raises
        ------
        RuntimeError
            If unexpected error running oscap scan.
        """

        oscap_profile_flag = None
        if oscap_profile is not None:
            oscap_profile_flag = f"--profile={oscap_profile}"

        oscap_fetch_remote_resources_flag = None
        if isinstance(oscap_fetch_remote_resources, str):
            oscap_fetch_remote_resources = strtobool(
                oscap_fetch_remote_resources)
        if oscap_fetch_remote_resources:
            oscap_fetch_remote_resources_flag = "--fetch-remote-resources"

        oscap_tailoring_file_flag = None
        if oscap_tailoring_file is not None:
            oscap_tailoring_file_flag = f"--tailoring-file={oscap_tailoring_file}"

        oscap_eval_success = None
        oscap_eval_out_buff = StringIO()
        oscap_eval_out = ""
        oscap_eval_fails = None
        try:
            oscap_chroot_command = buildah_unshare_comand.bake("oscap-chroot")
            with open(oscap_out_file_path, 'w') as oscap_out_file:
                out_callback = create_sh_redirect_to_multiple_streams_fn_callback(
                    [oscap_eval_out_buff, oscap_out_file])
                err_callback = create_sh_redirect_to_multiple_streams_fn_callback(
                    [oscap_eval_out_buff, oscap_out_file])
                oscap_chroot_command(
                    container_mount_path,
                    oscap_eval_type,
                    'eval',
                    oscap_profile_flag,
                    oscap_fetch_remote_resources_flag,
                    oscap_tailoring_file_flag,
                    f'--results={oscap_xml_results_file_path}',
                    f'--report={oscap_html_report_path}',
                    oscap_input_file,
                    _out=out_callback,
                    _err=err_callback,
                    _tee='err')
                oscap_eval_success = True
        except sh.ErrorReturnCode_1 as error:  # pylint: disable=no-member
            oscap_eval_success = error
        except sh.ErrorReturnCode_2 as error:  # pylint: disable=no-member
            # XCCDF: If there is at least one rule with either fail or unknown result,
            #           oscap-scan finishes with return code 2.
            # OVAL:  Never returned
            #
            # Source: https://www.systutorials.com/docs/linux/man/8-oscap/
            if oscap_eval_type == 'xccdf':
                oscap_eval_success = False
            else:
                oscap_eval_success = error
        except sh.ErrorReturnCode as error:
            oscap_eval_success = error

        # get the oscap output
        oscap_eval_out = oscap_eval_out_buff.getvalue()

        # parse the oscap output
        # NOTE: oscap is puts carrage returns (\r / ^M) in their output, remove them
        oscap_eval_out = re.sub('\r', '', oscap_eval_out)

        # print the oscap output no matter the results
        print(oscap_eval_out)

        # if unexpected error throw error
        if isinstance(oscap_eval_success, Exception):
            raise RuntimeError(
                f"Unexpected error running 'oscap {oscap_eval_type} eval': {oscap_eval_success} "
            ) from oscap_eval_success

        # NOTE: oscap oval eval returns exit code 0 whether or not any rules failed
        #       need to search output to determine if there were any rule failures
        if oscap_eval_type == 'oval' and oscap_eval_success:
            oscap_eval_fails = ""
            for match in OpenSCAPGeneric.OSCAP_OVAL_STDOUT_PATTERN.finditer(
                    oscap_eval_out):
                # NOTE: need to do regex and not == because may contain xterm color chars
                if OpenSCAPGeneric.OSCAP_OVAL_STDOUT_FAIL_PATTERN.search(
                        match.groupdict()['ruleresult']):
                    oscap_eval_fails += match.groupdict()['ruleblock']
                    oscap_eval_fails += "\n"
                    oscap_eval_success = False

        # if failed xccdf eval then parse out the fails
        if oscap_eval_type == 'xccdf' and not oscap_eval_success:
            oscap_eval_fails = ""
            for match in OpenSCAPGeneric.OSCAP_XCCDF_STDOUT_PATTERN.finditer(
                    oscap_eval_out):
                # NOTE: need to do regex and not == because may contain xterm color chars
                if re.search(r'fail', match.groupdict()['ruleresult']):
                    oscap_eval_fails += "\n"
                    oscap_eval_fails += match.groupdict()['ruleblock']
                    oscap_eval_fails += "\n"

        return oscap_eval_success, oscap_eval_fails