# @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 |