def merge(self, other): """Merge the artifacts and evidence from another StepResult into this StepResult. The other StepResult must have the same step name, sub step name and environment. Parameters ---------- step_result : StepResult The second StepResult instance to merge into this one Raises ------ StepRunnerException if the StepResult to merge does not have a matching step name, sub-step name, or environment. """ if not isinstance(other, StepResult): raise StepRunnerException('expect StepResult instance type') if other.step_name != self.step_name or \ other.sub_step_name != self.sub_step_name or \ other.environment != self.environment: raise StepRunnerException( 'Other StepResult does not have matching ' \ 'step name, sub step name, or environment.' ) for artifact in other.artifacts.values(): self.add_artifact(artifact.name, artifact.value, artifact.description) for evidence in other.evidence.values(): self.add_evidence(evidence.name, evidence.value, evidence.description)
def test_fail(self, mock_write_working_file, mock_run_maven_step): with TempDirectory() as test_dir: parent_work_dir_path = os.path.join(test_dir.path, 'working') step_config = {} step_implementer = self.create_step_implementer( step_config=step_config, parent_work_dir_path=parent_work_dir_path, ) # run step with mock failure mock_run_maven_step.side_effect = StepRunnerException( 'Mock error running maven') actual_step_result = step_implementer._run_step() # create expected step result expected_step_result = StepResult( step_name='foo', sub_step_name='MavenGeneric', sub_step_implementer_name='MavenGeneric') expected_step_result.add_artifact( description="Standard out and standard error from maven.", name='maven-output', value='/mock/mvn_output.txt') expected_step_result.message = "Error running maven. " \ "More details maybe found in 'maven-output' report artifact: "\ "Mock error running maven" expected_step_result.success = False # verify step result self.assertEqual(actual_step_result, expected_step_result) mock_write_working_file.assert_called_once() mock_run_maven_step.assert_called_with( mvn_output_file_path='/mock/mvn_output.txt')
def test_dynamically_determined_test_reports_dir_errors( self, mock_attempt_get_test_report_directory): with TempDirectory() as test_dir: # setup test parent_work_dir_path = os.path.join(test_dir.path, 'working') step_config = {} step_implementer = self.create_step_implementer( step_config=step_config, parent_work_dir_path=parent_work_dir_path, ) # setup mocks mock_attempt_get_test_report_directory.side_effect = StepRunnerException( 'mock error') # run test actual_test_report_dir = step_implementer._MavenIntegrationTest__get_test_report_dir( ) # verify results self.assertEqual(actual_test_report_dir, None) mock_attempt_get_test_report_directory.assert_has_calls([ call(plugin_name='maven-failsafe-plugin', configuration_key='reportsDirectory', default='target/failsafe-reports'), call(plugin_name='maven-surefire-plugin', configuration_key='reportsDirectory', default='target/surefire-reports', require_phase_execution_config=True) ])
def test_fail_set_version(self, mock_settings_file, mock_write_working_file, mock_run_maven_step, mock_run_maven): with TempDirectory() as test_dir: parent_work_dir_path = os.path.join(test_dir.path, 'working') pom_file = os.path.join(test_dir.path, 'mock-pom.xml') maven_push_artifact_repo_id = 'mock-repo-id' maven_push_artifact_repo_url = 'https://mock-repo.ploigos.com' version = '0.42.0-mock' step_config = { 'pom-file': pom_file, 'maven-push-artifact-repo-id': maven_push_artifact_repo_id, 'maven-push-artifact-repo-url': maven_push_artifact_repo_url, 'version': version } step_implementer = self.create_step_implementer( step_config=step_config, parent_work_dir_path=parent_work_dir_path, ) # run step with mvn version:set failure mock_run_maven.side_effect = StepRunnerException( 'mock error setting new pom version') actual_step_result = step_implementer._run_step() # create expected step result expected_step_result = StepResult( step_name='deploy', sub_step_name='MavenDeploy', sub_step_implementer_name='MavenDeploy') expected_step_result.success = False expected_step_result.message = "Error running 'maven deploy' to push artifacts. " \ "More details maybe found in 'maven-output' report artifact: " \ "mock error setting new pom version" expected_step_result.add_artifact( description= "Standard out and standard error from running maven to update version.", name='maven-update-version-output', value='/mock/mvn_versions_set_output.txt') expected_step_result.add_artifact( description="Standard out and standard error from running maven to " \ "push artifacts to repository.", name='maven-push-artifacts-output', value='/mock/mvn_deploy_output.txt' ) # verify step result self.assertEqual(actual_step_result, expected_step_result) mock_write_working_file.assert_called() mock_run_maven.assert_called_with( mvn_output_file_path='/mock/mvn_versions_set_output.txt', settings_file='/fake/settings.xml', pom_file=pom_file, phases_and_goals=['versions:set'], additional_arguments=[f'-DnewVersion={version}']) mock_run_maven_step.assert_not_called()
def test_fail_with_report_dir( self, mock_gather_evidence, mock_get_test_report_dir, mock_write_working_file, mock_run_maven_step ): with TempDirectory() as test_dir: # setup test parent_work_dir_path = os.path.join(test_dir.path, 'working') pom_file = os.path.join(test_dir.path, 'mock-pom.xml') step_config = { 'pom-file': pom_file } step_implementer = self.create_step_implementer( step_config=step_config, parent_work_dir_path=parent_work_dir_path, ) # setup mocks mock_run_maven_step.side_effect = StepRunnerException('mock error') # run test actual_step_result = step_implementer._run_step() # verify results expected_step_result = StepResult( step_name='unit-test', sub_step_name='MavenTest', sub_step_implementer_name='MavenTest' ) expected_step_result.success = False expected_step_result.message = "Error running maven. " \ "More details maybe found in report artifacts: " \ "mock error" expected_step_result.add_artifact( description="Standard out and standard error from maven.", name='maven-output', value='/mock/mvn_output.txt' ) expected_step_result.add_artifact( description="Test report generated when running unit tests.", name='test-report', value='/mock/test-results-dir' ) self.assertEqual(actual_step_result, expected_step_result) mock_run_maven_step.assert_called_once_with( mvn_output_file_path='/mock/mvn_output.txt' ) mock_gather_evidence.assert_called_once_with( step_result=Any(StepResult), test_report_dir='/mock/test-results-dir' )
def add_evidence(self, name, value, description=''): """Add evidence to this StepResult. Parameters ---------- name : str Name of the result evidence. value : str Arbitrary value of the evidence. description : str, optional Human readable description of the result evidence (defaults to empty). """ if not name: raise StepRunnerException('Name is required to add evidence') # False can be the value if value == '' or value is None: raise StepRunnerException('Value is required to add evidence') self.__evidence[name] = StepResultEvidence(name=name, value=value, description=description)
def add_artifact(self, name, value, description=''): """Add an artifact to this StepResult. Parameters ---------- name : str Name of the result artifact. value : str Arbitrary value of the artifact. description : str, optional Human readable description of the result artifact (defaults to empty). """ if not name: raise StepRunnerException('Name is required to add artifact') # False can be the value if value == '' or value is None: raise StepRunnerException('Value is required to add artifact') self.__artifacts[name] = StepResultArtifact(name=name, value=value, description=description)
def test_fail_no_report_dir(self, mock_gather_evidence, mock_get_test_report_dir, mock_write_working_file, mock_run_maven_step): with TempDirectory() as test_dir: # setup test parent_work_dir_path = os.path.join(test_dir.path, 'working') pom_file = os.path.join(test_dir.path, 'mock-pom.xml') step_config = { 'pom-file': pom_file, 'target-host-url-maven-argument-name': 'mock.target-host-url-param', 'deployed-host-urls': ['https://mock.ploigos.org/mock-app-1'] } step_implementer = self.create_step_implementer( step_config=step_config, parent_work_dir_path=parent_work_dir_path, ) # setup mocks mock_run_maven_step.side_effect = StepRunnerException('mock error') mock_get_test_report_dir.return_value = None # run test actual_step_result = step_implementer._run_step() # verify results expected_step_result = StepResult( step_name='unit-test', sub_step_name='MavenIntegrationTest', sub_step_implementer_name='MavenIntegrationTest') expected_step_result.success = False expected_step_result.message = "Error running maven. " \ "More details maybe found in report artifacts: " \ "mock error" expected_step_result.add_artifact( description="Standard out and standard error from maven.", name='maven-output', value='/mock/mvn_output.txt') self.assertEqual(actual_step_result, expected_step_result) mock_run_maven_step.assert_called_once_with( mvn_output_file_path='/mock/mvn_output.txt', step_implementer_additional_arguments=[ '-Dmock.target-host-url-param=https://mock.ploigos.org/mock-app-1' ]) mock_gather_evidence.assert_not_called()
def test_fail_maven_run(self, mock_effective_pom_element, mock_write_working_file, mock_run_maven_step): with TempDirectory() as test_dir: parent_work_dir_path = os.path.join(test_dir.path, 'working') pom_file = os.path.join(test_dir.path, 'mock-pom.xml') step_config = {'pom-file': pom_file} step_implementer = self.create_step_implementer( step_config=step_config, parent_work_dir_path=parent_work_dir_path, ) # run step with mock failure mock_run_maven_step.side_effect = StepRunnerException( 'Mock error running maven') actual_step_result = step_implementer._run_step() # create expected step result surefire_reports_dir = os.path.join(test_dir.path, 'target/surefire-reports') expected_step_result = StepResult( step_name='unit-test', sub_step_name='MavenTest', sub_step_implementer_name='MavenTest') expected_step_result.success = False expected_step_result.message = "Error running 'maven test' to run unit tests. " \ "More details maybe found in 'maven-output' and `surefire-reports` " \ f"report artifact: Mock error running maven" expected_step_result.add_artifact( description="Standard out and standard error from maven.", name='maven-output', value='/mock/mvn_output.txt') expected_step_result.add_artifact( description="Surefire reports generated by maven.", name='surefire-reports', value=surefire_reports_dir) # verify step result self.assertEqual(actual_step_result, expected_step_result) mock_write_working_file.assert_called_once() mock_run_maven_step.assert_called_with( mvn_output_file_path='/mock/mvn_output.txt')
def __buildah_mount_container(buildah_unshare_command, container_id): """Use buildah to mount a container. Parameters ---------- buildah_unshare_command : 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. Raises ------ StepRunnerException If error mounting the 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_command.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 StepRunnerException( f'Error mounting container ({container_id}): {error}' ) from error return mount_path
def __get_oscap_document_type(oscap_input_file): """Gets the OpenSCAP document type for a given input file. Parameters ---------- oscap_input_file : path Path to OSCAP file to determine the OpenSCAP document type of. Returns ------- str OpenSCAP document type. For example: * Source Data Stream * XCCDF Checklist * OVAL Definitions Raises ------ StepRunnerException If error getting document type of oscap input file. """ oscap_document_type = None try: oscap_info_out_buff = StringIO() sh.oscap.info( # pylint: disable=no-member oscap_input_file, _out=oscap_info_out_buff) oscap_info_out = oscap_info_out_buff.getvalue().rstrip() oscap_document_type_match = OpenSCAPGeneric.OSCAP_INFO_DOC_TYPE_PATTERN.search( oscap_info_out) oscap_document_type = oscap_document_type_match.groupdict( )['doctype'] except sh.ErrorReturnCode as error: raise StepRunnerException( f"Error getting document type of oscap input file" f" ({oscap_input_file}): {error}") from error return oscap_document_type
def __buildah_import_image_from_tar(image_tar_file, container_name): """Import a container image using buildah form a TAR file. Parameters ---------- image_tar_file : str Path to TAR file to import as a container image. container_name : str name for the working container. Returns ------- str Name of the imported container. Raises ------ StepRunnerException If error importing image. """ # import image tar file to vfs file system try: sh.buildah( # pylint: disable=no-member 'from', '--storage-driver', 'vfs', '--name', container_name, f"docker-archive:{image_tar_file}", _out=sys.stdout, _err=sys.stderr, _tee='err') except sh.ErrorReturnCode as error: raise StepRunnerException( f'Error importing the image ({image_tar_file}): {error}' ) from error return container_name
def raise_StepRunnerException(): raise StepRunnerException('test')
def __run_oscap_scan( # pylint: disable=too-many-arguments,too-many-locals,too-many-branches,too-many-statements buildah_unshare_command, 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_command : 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 ------ StepRunnerException 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_command.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 StepRunnerException( f"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
def _run_step(self): # pylint: disable=too-many-locals,too-many-statements """Runs the OpenSCAP eval for a given input file against a given container. """ step_result = StepResult.from_step_implementer(self) image_tar_file = self.get_value('image-tar-file') oscap_profile = self.get_value('oscap-profile') oscap_fetch_remote_resources = self.get_value( 'oscap-fetch-remote-resources') # create a container name from the tar file name, step name, and sub step name container_name = os.path.splitext(os.path.basename(image_tar_file))[0] container_name += f"-{self.step_name}-{self.sub_step_name}" try: # import image tar file to vfs file system print(f"\nImport image: {image_tar_file}") OpenSCAPGeneric.__buildah_import_image_from_tar( image_tar_file=image_tar_file, container_name=container_name) print(f"Imported image: {image_tar_file}") # baking `buildah unshare` command to wrap other buildah commands with # so that container does not need to be running in a privileged mode to be able # to function buildah_unshare_command = sh.buildah.bake('unshare') # pylint: disable=no-member # mount the container filesystem and get mount path # # NOTE: run in the context of `buildah unshare` so that container does not # need to be run in a privileged mode print(f"\nMount container: {container_name}") container_mount_path = OpenSCAPGeneric.__buildah_mount_container( buildah_unshare_command=buildah_unshare_command, container_id=container_name) print( f"Mounted container ({container_name}) with mount path: '{container_mount_path}'" ) try: # download the open scap input file oscap_input_definitions_uri = self.get_value( 'oscap-input-definitions-uri') print( f"\nDownload input definitions: {oscap_input_definitions_uri}" ) oscap_input_file = download_and_decompress_source_to_destination( source_url=oscap_input_definitions_uri, destination_dir=self.work_dir_path_step) print(f"Downloaded input definitions to: {oscap_input_file}") except (RuntimeError, AssertionError) as error: raise StepRunnerException( f"Error downloading OpenSCAP input file: {error}" ) from error try: # if specified download oscap tailoring file oscap_tailoring_file = None oscap_tailoring_file_uri = self.get_value( 'oscap-tailoring-uri') if oscap_tailoring_file_uri: print( f"\nDownload oscap tailoring file: {oscap_tailoring_file_uri}" ) oscap_tailoring_file = download_and_decompress_source_to_destination( source_url=oscap_tailoring_file_uri, destination_dir=self.work_dir_path_step) print( f"Download oscap tailoring file to: {oscap_tailoring_file}" ) except (RuntimeError, AssertionError) as error: raise StepRunnerException( f"Error downloading OpenSCAP tailoring file: {error}" ) from error # determine oscap eval type based on document type print( f"\nDetermine OpenSCAP document type of input file: {oscap_input_file}" ) oscap_document_type = OpenSCAPGeneric.__get_oscap_document_type( oscap_input_file=oscap_input_file) print("Determined OpenSCAP document type of input file" f" ({oscap_input_file}): {oscap_document_type}") print( f"\nDetermine OpenSCAP eval type for input file ({oscap_input_file}) " f"of document type: {oscap_document_type}") oscap_eval_type = OpenSCAPGeneric.__get_oscap_eval_type_based_on_document_type( oscap_document_type=oscap_document_type) print("Determined OpenSCAP eval type of input file" f" ({oscap_input_file}): {oscap_eval_type}") # Execute scan in the context of buildah unshare # # NOTE: run in the context of `buildah unshare` so that container does not # need to be run in a privilaged mode oscap_out_file_path = self.write_working_file( f'oscap-{oscap_eval_type}-out') oscap_xml_results_file_path = self.write_working_file( f'oscap-{oscap_eval_type}-results.xml') oscap_html_report_path = self.write_working_file( f'oscap-{oscap_eval_type}-report.html') print("\nRun oscap scan") oscap_eval_success, oscap_eval_fails = OpenSCAPGeneric.__run_oscap_scan( buildah_unshare_command=buildah_unshare_command, oscap_eval_type=oscap_eval_type, oscap_input_file=oscap_input_file, oscap_out_file_path=oscap_out_file_path, oscap_xml_results_file_path=oscap_xml_results_file_path, oscap_html_report_path=oscap_html_report_path, container_mount_path=container_mount_path, oscap_profile=oscap_profile, oscap_tailoring_file=oscap_tailoring_file, oscap_fetch_remote_resources=oscap_fetch_remote_resources) print( f"OpenSCAP scan completed with eval success: {oscap_eval_success}" ) # save scan results step_result.success = oscap_eval_success if not oscap_eval_success: step_result.message = f"OSCAP eval found issues:\n{oscap_eval_fails}" step_result.add_artifact(name='html-report', value=oscap_html_report_path) step_result.add_artifact(name='xml-report', value=oscap_xml_results_file_path) step_result.add_artifact(name='stdout-report', value=oscap_out_file_path) except StepRunnerException as error: step_result.success = False step_result.message = str(error) return step_result