summaryrefslogtreecommitdiffstats
path: root/BaseTools/Plugin/CodeQL/CodeQlAnalyzePlugin.py
blob: 9734478f8b78b879f054620e7d25bbf812b4e959 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# @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