diff --git a/src/macaron/console.py b/src/macaron/console.py index f1d9a1a18..725a8af0b 100644 --- a/src/macaron/console.py +++ b/src/macaron/console.py @@ -37,6 +37,7 @@ def __init__(self, *args: Any, verbose: bool = False, **kwargs: Any) -> None: self.setLevel(logging.DEBUG) self.command = "" self.logs: list[str] = [] + self.error_logs: list[str] = [] self.description_table = Table(show_header=False, box=None) self.description_table_content: dict[str, str | Status] = { "Package URL:": Status("[green]Processing[/]"), @@ -118,6 +119,7 @@ def emit(self, record: logging.LogRecord) -> None: if record.levelno >= logging.ERROR: self.logs.append(f"[red][ERROR][/red] {log_time} {msg}") + self.error_logs.append(f"[red][ERROR][/red] {log_time} {msg}") elif record.levelno >= logging.WARNING: self.logs.append(f"[yellow][WARNING][/yellow] {log_time} {msg}") else: @@ -386,10 +388,17 @@ def make_layout(self) -> Group: A rich Group object containing the layout for the live console display. """ layout: list[RenderableType] = [] + if self.error_logs: + error_log_panel = Panel( + "\n".join(self.error_logs), + title="Error Logs", + title_align="left", + border_style="red", + ) + layout = layout + [error_log_panel] if self.command == "analyze": - layout = layout + [Rule(" DESCRIPTION", align="left")] if self.description_table.row_count > 0: - layout = layout + ["", self.description_table] + layout = layout + [Rule(" DESCRIPTION", align="left"), "", self.description_table] if self.progress_table.row_count > 0: layout = layout + ["", self.progress, "", self.progress_table] if self.failed_checks_table.row_count > 0: @@ -418,25 +427,25 @@ def make_layout(self) -> Group: ] elif self.command == "verify-policy": if self.policy_summary_table.row_count > 0: - if self.components_violates_table.row_count > 0: + if self.components_satisfy_table.row_count > 0: layout = layout + [ - "[bold red] Components Violate Policy[/]", - self.components_violates_table, + "[bold green] Components Satisfy Policy[/]", + self.components_satisfy_table, ] else: layout = layout + [ - "[bold red] Components Violate Policy[/] [white not italic]None[/]", + "[bold green] Components Satisfy Policy[/] [white not italic]None[/]", ] - if self.components_satisfy_table.row_count > 0: + if self.components_violates_table.row_count > 0: layout = layout + [ "", - "[bold green] Components Satisfy Policy[/]", - self.components_satisfy_table, + "[bold red] Components Violate Policy[/]", + self.components_violates_table, ] else: layout = layout + [ "", - "[bold green] Components Satisfy Policy[/] [white not italic]None[/]", + "[bold red] Components Violate Policy[/] [white not italic]None[/]", ] layout = layout + ["", self.policy_summary_table] if self.verification_summary_attestation: @@ -448,10 +457,11 @@ def make_layout(self) -> Group: "[bold blue]Verification Summary Attestation[/]", self.verification_summary_attestation, ) - vsa_table.add_row( - "[bold blue]Decode and Inspect the Content[/]", - f"cat {self.verification_summary_attestation} | jq -r [white]'.payload'[/] | base64 -d | jq", - ) + if self.verification_summary_attestation != "No VSA generated.": + vsa_table.add_row( + "[bold blue]Decode and Inspect the Content[/]", + f"cat {self.verification_summary_attestation} | jq -r [white]'.payload'[/] | base64 -d | jq", + ) layout = layout + [vsa_table] elif self.command == "find-source": diff --git a/src/macaron/dependency_analyzer/cyclonedx.py b/src/macaron/dependency_analyzer/cyclonedx.py index 704256e73..9fec0536f 100644 --- a/src/macaron/dependency_analyzer/cyclonedx.py +++ b/src/macaron/dependency_analyzer/cyclonedx.py @@ -250,7 +250,7 @@ def add_latest_version( ): latest_deps[key] = item except ValueError as error: - logger.error("Could not parse dependency version number: %s", error) + logger.debug("Could not parse dependency version number: %s", error) @staticmethod def to_configs(resolved_deps: dict[str, DependencyInfo]) -> list[Configuration]: @@ -344,7 +344,7 @@ def resolve_dependencies(main_ctx: Any, sbom_path: str, recursive: bool = False) # We allow dependency analysis if SBOM is provided but no repository is found. dep_analyzer = build_tool.get_dep_analyzer() except DependencyAnalyzerError as error: - logger.error("Unable to find a dependency analyzer for %s: %s", build_tool.name, error) + logger.debug("Unable to find a dependency analyzer for %s: %s", build_tool.name, error) return {} if isinstance(dep_analyzer, NoneDependencyAnalyzer): @@ -381,11 +381,11 @@ def resolve_dependencies(main_ctx: Any, sbom_path: str, recursive: bool = False) log_file.write(analyzer_output.stdout.decode("utf-8")) except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as error: - logger.error(error) + logger.debug(error) with open(log_path, mode="a", encoding="utf-8") as log_file: log_file.write(error.output.decode("utf-8")) except FileNotFoundError as error: - logger.error(error) + logger.debug(error) # We collect the generated SBOM as a best effort, even if the build exits with errors. # TODO: add improvements to help the SBOM build succeed as much as possible. @@ -437,12 +437,12 @@ def get_root_component(self, root_bom_path: Path) -> CDXComponent | None: try: root_bom = deserialize_bom_json(root_bom_path) except CycloneDXParserError as error: - logger.error(error) + logger.debug(error) return None try: return root_bom.metadata.component except AttributeError as error: - logger.error(error) + logger.debug(error) return None @@ -482,7 +482,7 @@ def _is_target_cmp(cmp: CDXComponent | None) -> bool: if _is_target_cmp(root_bom.metadata.component): return root_bom.metadata.component if root_bom.metadata.component: - logger.error( + logger.debug( ( "The analysis target %s and the metadata component %s in the BOM file do not match." " Please fix the PURL input and try again." @@ -494,7 +494,7 @@ def _is_target_cmp(cmp: CDXComponent | None) -> bool: ) return None - logger.error( + logger.debug( "Unable to find the analysis target %s in the BOM file. Please fix the PURL input and try again.", target_component.purl, ) @@ -528,11 +528,11 @@ def get_dep_components( try: root_bom = deserialize_bom_json(root_bom_path) except CycloneDXParserError as error: - logger.error(error) + logger.debug(error) return if root_bom.components is None: - logger.error("The BOM file at %s misses components.", str(root_bom_path)) + logger.debug("The BOM file at %s misses components.", str(root_bom_path)) return dependencies: list[CDXDependency] = [] @@ -559,7 +559,7 @@ def get_dep_components( try: child_bom_objects.append(deserialize_bom_json(child_path)) except CycloneDXParserError as error: - logger.error(error) + logger.debug(error) continue for bom in child_bom_objects: @@ -663,7 +663,7 @@ def convert_components_to_artifacts( with open(os.path.join(global_config.output_path, "sbom_debug.json"), "w", encoding="utf8") as debug_file: debug_file.write(json.dumps(all_versions, indent=4)) except OSError as error: - logger.error(error) + logger.debug(error) return latest_deps diff --git a/src/macaron/parsers/yaml/loader.py b/src/macaron/parsers/yaml/loader.py index 30712caa0..e99102bc9 100644 --- a/src/macaron/parsers/yaml/loader.py +++ b/src/macaron/parsers/yaml/loader.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2022, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, Oracle and/or its affiliates. All rights reserved. # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. """This module contains the loader for YAML files.""" @@ -77,10 +77,10 @@ def validate_yaml_data(cls, schema: Schema, data: list) -> bool: yamale.validate(schema, data) return True except yamale.YamaleError as error: - logger.error("Yaml data validation failed.") + logger.debug("Yaml data validation failed.") for result in error.results: for err_str in result.errors: - logger.error("\t%s", err_str) + logger.debug("\t%s", err_str) return False @classmethod @@ -105,11 +105,11 @@ def load(cls, path: os.PathLike | str, schema: Schema = None) -> Any: logger.info("Loading yaml content for %s", path) loaded_data = YamlLoader._load_yaml_content(path=path) if not loaded_data: - logger.error("Error while loading the config yaml file %s.", path) + logger.debug("Error while loading the config yaml file %s.", path) return None if schema and not YamlLoader.validate_yaml_data(schema, loaded_data): - logger.error("The yaml content in %s is invalid according to the schema.", path) + logger.debug("The yaml content in %s is invalid according to the schema.", path) return None result = None diff --git a/src/macaron/provenance/provenance_finder.py b/src/macaron/provenance/provenance_finder.py index 99e5009f2..8bf9937e1 100644 --- a/src/macaron/provenance/provenance_finder.py +++ b/src/macaron/provenance/provenance_finder.py @@ -189,7 +189,7 @@ def find_npm_provenance(purl: PackageURL, registry: NPMRegistry) -> list[Provena # Load the provenance file (provenance attestation). provenance_payload = load_provenance_payload(download_path) except LoadIntotoAttestationError as error: - logger.error("Error while loading provenance attestation: %s", error) + logger.debug("Error while loading provenance attestation: %s", error) return [] signed_download_path = f"{download_path}.signed" @@ -197,7 +197,7 @@ def find_npm_provenance(purl: PackageURL, registry: NPMRegistry) -> list[Provena # Load the other npm provenance file (publish attestation). publish_payload = load_provenance_payload(signed_download_path) except LoadIntotoAttestationError as error: - logger.error("Error while loading publish attestation: %s", error) + logger.debug("Error while loading publish attestation: %s", error) return [ProvenanceAsset(provenance_payload, npm_provenance_asset.name, npm_provenance_asset.url)] return [ @@ -206,7 +206,7 @@ def find_npm_provenance(purl: PackageURL, registry: NPMRegistry) -> list[Provena ] except OSError as error: - logger.error("Error while storing provenance in the temporary directory: %s", error) + logger.debug("Error while storing provenance in the temporary directory: %s", error) return [] @@ -331,7 +331,7 @@ def find_pypi_provenance(purl: PackageURL) -> list[ProvenanceAsset]: payload.verified = verified return [ProvenanceAsset(payload, purl.name, url)] except LoadIntotoAttestationError as load_error: - logger.error("Error while loading provenance: %s", load_error) + logger.debug("Error while loading provenance: %s", load_error) return [] @@ -484,7 +484,7 @@ def download_provenances_from_ci_service(ci_info: CIInfo, download_path: str) -> try: payload = load_provenance_payload(provenance_filepath) except LoadIntotoAttestationError as error: - logger.error("Error logging provenance: %s", error) + logger.debug("Error logging provenance: %s", error) continue # Add the provenance file. diff --git a/src/macaron/repo_finder/repo_finder.py b/src/macaron/repo_finder/repo_finder.py index 3bc0d1f6c..16c39c7a8 100644 --- a/src/macaron/repo_finder/repo_finder.py +++ b/src/macaron/repo_finder/repo_finder.py @@ -372,11 +372,11 @@ def get_latest_purl_if_different(purl: PackageURL) -> PackageURL | None: latest_version_purl, _ = DepsDevRepoFinder.get_latest_version(no_version_purl) if not latest_version_purl: - logger.error("Latest version PURL could not be found.") + logger.debug("Latest version PURL could not be found.") return None if latest_version_purl == purl: - logger.error("Latest version PURL is the same as the current.") + logger.debug("Latest version PURL is the same as the current.") return None logger.debug("Found new version of PURL: %s", latest_version_purl) @@ -400,11 +400,11 @@ def get_latest_repo_if_different(latest_version_purl: PackageURL, original_repo: """ latest_repo, _ = find_repo(latest_version_purl, False) if not latest_repo: - logger.error("Could not find repository from latest PURL: %s", latest_version_purl) + logger.debug("Could not find repository from latest PURL: %s", latest_version_purl) return "" if check_repo_urls_are_equivalent(original_repo, latest_repo): - logger.error( + logger.debug( "Repository from latest PURL is equivalent to original repository: %s ~= %s", latest_repo, original_repo, @@ -470,7 +470,7 @@ def prepare_repo( logger.info("The path to repo %s is a remote path.", repo_path) resolved_remote_path = get_remote_vcs_url(repo_path) if not resolved_remote_path: - logger.error("The provided path to repo %s is not a valid remote path.", repo_path) + logger.debug("The provided path to repo %s is not a valid remote path.", repo_path) return None, commit_finder_outcome git_service = get_git_service(resolved_remote_path) diff --git a/src/macaron/slsa_analyzer/analyzer.py b/src/macaron/slsa_analyzer/analyzer.py index 774ec4c66..c2a0fbc5a 100644 --- a/src/macaron/slsa_analyzer/analyzer.py +++ b/src/macaron/slsa_analyzer/analyzer.py @@ -673,7 +673,7 @@ def add_repository(self, branch_name: str | None, git_obj: Git) -> Repository | # We only allow complete_name's length to be 2 or 3 because we need to construct PURL # strings using the complete_name, i.e., type/namespace/name@commitsha if (parts_len := len(Path(complete_name).parts)) < 2 or parts_len > 3: - logger.error("The repository path %s is not valid.", complete_name) + logger.debug("The repository path %s is not valid.", complete_name) return None repository = Repository( diff --git a/src/macaron/slsa_analyzer/build_tool/poetry.py b/src/macaron/slsa_analyzer/build_tool/poetry.py index 0363f3cbb..c12a40e33 100644 --- a/src/macaron/slsa_analyzer/build_tool/poetry.py +++ b/src/macaron/slsa_analyzer/build_tool/poetry.py @@ -86,11 +86,11 @@ def is_detected(self, repo_path: str) -> bool: if ("tool" in data) and ("poetry" in data["tool"]): return True except tomllib.TOMLDecodeError: - logger.error("Failed to read the %s file: invalid toml file.", conf) + logger.debug("Failed to read the %s file: invalid toml file.", conf) return False return False except FileNotFoundError: - logger.error("Failed to read the %s file.", conf) + logger.debug("Failed to read the %s file.", conf) return False return False diff --git a/src/macaron/slsa_analyzer/checks/infer_artifact_pipeline_check.py b/src/macaron/slsa_analyzer/checks/infer_artifact_pipeline_check.py index a33ba4586..ce9f6bd2f 100644 --- a/src/macaron/slsa_analyzer/checks/infer_artifact_pipeline_check.py +++ b/src/macaron/slsa_analyzer/checks/infer_artifact_pipeline_check.py @@ -160,7 +160,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: try: build_def = ProvenancePredicate.find_build_def(prov_payload.statement) except ProvenanceError as error: - logger.error(error) + logger.debug(error) return CheckResultData(result_tables=[], result_type=CheckResultType.FAILED) prov_workflow, prov_trigger_run = build_def.get_build_invocation(prov_payload.statement) diff --git a/src/macaron/slsa_analyzer/ci_service/github_actions/analyzer.py b/src/macaron/slsa_analyzer/ci_service/github_actions/analyzer.py index 4565c2098..3c234d755 100644 --- a/src/macaron/slsa_analyzer/ci_service/github_actions/analyzer.py +++ b/src/macaron/slsa_analyzer/ci_service/github_actions/analyzer.py @@ -398,7 +398,7 @@ def build_call_graph_from_path(root: BaseNode, workflow_path: str, repo_path: st try: parsed_obj: Workflow = parse_action(workflow_path) except ParseError as error: - logger.error("Unable to parse GitHub Actions at the target %s: %s", repo_path, error) + logger.debug("Unable to parse GitHub Actions at the target %s: %s", repo_path, error) raise ParseError from error # Add internal workflows. diff --git a/src/macaron/slsa_analyzer/ci_service/github_actions/github_actions_ci.py b/src/macaron/slsa_analyzer/ci_service/github_actions/github_actions_ci.py index 43c4e3f0e..c0fd6aa46 100644 --- a/src/macaron/slsa_analyzer/ci_service/github_actions/github_actions_ci.py +++ b/src/macaron/slsa_analyzer/ci_service/github_actions/github_actions_ci.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, Oracle and/or its affiliates. All rights reserved. # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. """This module analyzes GitHub Actions CI.""" @@ -186,13 +186,13 @@ def has_latest_run_passed( workflow_data = self.api_client.get_repo_workflow_data(repo_full_name, workflow) if not workflow_data: - logger.error("Cannot find data of workflow %s.", workflow) + logger.debug("Cannot find data of workflow %s.", workflow) return "" try: workflow_id = workflow_data["id"] except KeyError: - logger.error("Cannot get unique ID of workflow %s.", workflow) + logger.debug("Cannot get unique ID of workflow %s.", workflow) return "" logger.info("The unique ID of workflow %s is %s", workflow, workflow_id) @@ -540,7 +540,7 @@ def search_for_workflow_run( full_name, branch_name=branch_name, created_after=created_after, page=query_page ) except KeyError: - logger.error("Error while reading run data. Skipping ...") + logger.debug("Error while reading run data. Skipping ...") continue return {} diff --git a/src/macaron/slsa_analyzer/git_url.py b/src/macaron/slsa_analyzer/git_url.py index d5b473685..62a40833f 100644 --- a/src/macaron/slsa_analyzer/git_url.py +++ b/src/macaron/slsa_analyzer/git_url.py @@ -582,7 +582,7 @@ def get_remote_origin_of_local_repo(git_obj: Git) -> str: try: url_parse_result = urllib.parse.urlparse(remote_origin_path) except ValueError: - logger.error("Error occurs while processing the remote URL of repo %s.", git_obj.project_name) + logger.debug("Error occurs while processing the remote URL of repo %s.", git_obj.project_name) return "" _, _, hostname = url_parse_result.netloc.rpartition("@") diff --git a/src/macaron/slsa_analyzer/registry.py b/src/macaron/slsa_analyzer/registry.py index 868f88a4c..564b761e7 100644 --- a/src/macaron/slsa_analyzer/registry.py +++ b/src/macaron/slsa_analyzer/registry.py @@ -126,7 +126,7 @@ def _add_relationship_entry(self, check_id: str, relationship: tuple[str, CheckR else: existed_label = parent.get(check_id) if existed_label: - logger.error( + logger.debug( "The relationship between %s and parent %s has been defined with label %s.", check_id, parent_id, diff --git a/src/macaron/util.py b/src/macaron/util.py index 845117651..6509e2f67 100644 --- a/src/macaron/util.py +++ b/src/macaron/util.py @@ -44,7 +44,7 @@ def send_get_http(url: str, headers: dict) -> dict: retry_counter = error_retries response = requests.get(url=url, headers=headers, timeout=timeout) while response.status_code != 200: - logger.error( + logger.debug( "Receiving error code %s from server. Message: %s.", response.status_code, response.text, @@ -500,8 +500,8 @@ def copy_file(src: str, dest_dir: str) -> bool: shutil.copy2(src, dest_dir) return True except shutil.Error as error: - logger.error("Error while copying %s to %s", src, dest_dir) - logger.error(str(error)) + logger.debug("Error while copying %s to %s", src, dest_dir) + logger.debug(error) return False