# @file CodeQAnalyzePlugin.py # # A build plugin that analyzes a CodeQL database. # # Copyright (c) Microsoft Corporation. All rights reserved. # SPDX-License-Identifier: BSD-2-Clause-Patent ## import json import logging import os import yaml from analyze import analyze_filter from common import codeql_plugin from edk2toolext import edk2_logging from edk2toolext.environment.plugintypes.uefi_build_plugin import \ IUefiBuildPlugin from edk2toolext.environment.uefi_build import UefiBuilder from edk2toollib.uefi.edk2.path_utilities import Edk2Path from edk2toollib.utility_functions import RunCmd from pathlib import Path class CodeQlAnalyzePlugin(IUefiBuildPlugin): def do_post_build(self, builder: UefiBuilder) -> int: """CodeQL analysis post-build functionality. Args: builder (UefiBuilder): A UEFI builder object for this build. Returns: int: The number of CodeQL errors found. Zero indicates that AuditOnly mode is enabled or no failures were found. """ self.builder = builder self.package = builder.edk2path.GetContainingPackage( builder.edk2path.GetAbsolutePathOnThisSystemFromEdk2RelativePath( builder.env.GetValue("ACTIVE_PLATFORM") ) ) self.package_path = Path( builder.edk2path.GetAbsolutePathOnThisSystemFromEdk2RelativePath( self.package ) ) self.target = builder.env.GetValue("TARGET") self.codeql_db_path = codeql_plugin.get_codeql_db_path( builder.ws, self.package, self.target, new_path=False) self.codeql_path = codeql_plugin.get_codeql_cli_path() if not self.codeql_path: logging.critical("CodeQL build enabled but CodeQL CLI application " "not found.") return -1 codeql_sarif_dir_path = self.codeql_db_path[ :self.codeql_db_path.rindex('-')] codeql_sarif_dir_path = codeql_sarif_dir_path.replace( "-db-", "-analysis-") self.codeql_sarif_path = os.path.join( codeql_sarif_dir_path, (os.path.basename( self.codeql_db_path) + ".sarif")) edk2_logging.log_progress(f"Analyzing {self.package} ({self.target}) " f"CodeQL database at:\n" f" {self.codeql_db_path}") edk2_logging.log_progress(f"Results will be written to:\n" f" {self.codeql_sarif_path}") # Packages are allowed to specify package-specific query specifiers # in the package CI YAML file that override the global query specifier. audit_only = False query_specifiers = None package_config_file = Path(os.path.join( self.package_path, self.package + ".ci.yaml")) plugin_data = None if package_config_file.is_file(): with open(package_config_file, 'r') as cf: package_config_file_data = yaml.safe_load(cf) if "CodeQlAnalyze" in package_config_file_data: plugin_data = package_config_file_data["CodeQlAnalyze"] if "AuditOnly" in plugin_data: audit_only = plugin_data["AuditOnly"] if "QuerySpecifiers" in plugin_data: logging.debug(f"Loading CodeQL query specifiers in " f"{str(package_config_file)}") query_specifiers = plugin_data["QuerySpecifiers"] global_audit_only = builder.env.GetValue("STUART_CODEQL_AUDIT_ONLY") if global_audit_only: if global_audit_only.strip().lower() == "true": audit_only = True if audit_only: logging.info(f"CodeQL Analyze plugin is in audit only mode for " f"{self.package} ({self.target}).") # Builds can override the query specifiers defined in this plugin # by setting the value in the STUART_CODEQL_QUERY_SPECIFIERS # environment variable. if not query_specifiers: query_specifiers = builder.env.GetValue( "STUART_CODEQL_QUERY_SPECIFIERS") # Use this plugins query set file as the default fallback if it is # not overridden. It is possible the file is not present if modified # locally. In that case, skip the plugin. plugin_query_set = Path(Path(__file__).parent, "CodeQlQueries.qls") if not query_specifiers and plugin_query_set.is_file(): query_specifiers = str(plugin_query_set.resolve()) if not query_specifiers: logging.warning("Skipping CodeQL analysis since no CodeQL query " "specifiers were provided.") return 0 codeql_params = (f'database analyze {self.codeql_db_path} ' f'{query_specifiers} --format=sarifv2.1.0 ' f'--output={self.codeql_sarif_path} --download ' f'--threads=0') # CodeQL requires the sarif file parent directory to exist already. Path(self.codeql_sarif_path).parent.mkdir(exist_ok=True, parents=True) cmd_ret = RunCmd(self.codeql_path, codeql_params) if cmd_ret != 0: logging.critical(f"CodeQL CLI analysis failed with return code " f"{cmd_ret}.") if not os.path.isfile(self.codeql_sarif_path): logging.critical(f"The sarif file {self.codeql_sarif_path} was " f"not created. Analysis cannot continue.") return -1 filter_pattern_data = [] global_filter_file_value = builder.env.GetValue( "STUART_CODEQL_FILTER_FILES") if global_filter_file_value: global_filter_files = global_filter_file_value.strip().split(',') global_filter_files = [Path(f) for f in global_filter_files] for global_filter_file in global_filter_files: if global_filter_file.is_file(): with open(global_filter_file, 'r') as ff: global_filter_file_data = yaml.safe_load(ff) if "Filters" in global_filter_file_data: current_pattern_data = \ global_filter_file_data["Filters"] if type(current_pattern_data) is not list: logging.critical( f"CodeQL pattern data must be a list of " f"strings. Data in " f"{str(global_filter_file.resolve())} is " f"invalid. CodeQL analysis is incomplete.") return -1 filter_pattern_data += current_pattern_data else: logging.critical( f"CodeQL global filter file " f"{str(global_filter_file.resolve())} is " f"malformed. Missing Filters section. CodeQL " f"analysis is incomplete.") return -1 else: logging.critical( f"CodeQL global filter file " f"{str(global_filter_file.resolve())} was not found. " f"CodeQL analysis is incomplete.") return -1 if plugin_data and "Filters" in plugin_data: if type(plugin_data["Filters"]) is not list: logging.critical( "CodeQL pattern data must be a list of strings. " "CodeQL analysis is incomplete.") return -1 filter_pattern_data.extend(plugin_data["Filters"]) if filter_pattern_data: logging.info("Applying CodeQL SARIF result filters.") analyze_filter.filter_sarif( self.codeql_sarif_path, self.codeql_sarif_path, filter_pattern_data, split_lines=False) with open(self.codeql_sarif_path, 'r') as sf: sarif_file_data = json.load(sf) try: # Perform minimal JSON parsing to find the number of errors. total_errors = 0 for run in sarif_file_data['runs']: total_errors += len(run['results']) except KeyError: logging.critical("Sarif file does not contain expected data. " "Analysis cannot continue.") return -1 if total_errors > 0: if audit_only: # Show a warning message so CodeQL analysis is not forgotten. # If the repo owners truly do not want to fix CodeQL issues, # analysis should be disabled entirely. logging.warning(f"{self.package} ({self.target}) CodeQL " f"analysis ignored {total_errors} errors due " f"to audit mode being enabled.") return 0 else: logging.error(f"{self.package} ({self.target}) CodeQL " f"analysis failed with {total_errors} errors.") return total_errors