|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | +""" |
| 4 | +CodeQL for Python. |
| 5 | +""" |
| 6 | + |
| 7 | +import os |
| 8 | +import shutil |
| 9 | +import tempfile |
| 10 | + |
| 11 | +import codeql |
| 12 | +from .common import * |
| 13 | + |
| 14 | +# Constants |
| 15 | +CODEQL_QLPACK = ''' |
| 16 | +name: codeql-python |
| 17 | +version: 0.0.0 |
| 18 | +libraryPathDependencies: {} |
| 19 | +''' |
| 20 | + |
| 21 | +class Database(object): |
| 22 | + def __init__(self, path, temp=False): |
| 23 | + """ |
| 24 | + Arguments: |
| 25 | + path -- Path of the database |
| 26 | + temp -- Remove database path in destructor |
| 27 | + """ |
| 28 | + self.path = path |
| 29 | + self.temp = temp |
| 30 | + |
| 31 | + def __del__(self): |
| 32 | + if self.temp: |
| 33 | + shutil.rmtree(self.path) |
| 34 | + |
| 35 | + # Helpers |
| 36 | + def run_command(self, command, options=[], post=[]): |
| 37 | + run(['database', command] + options + [self.path] + post) |
| 38 | + |
| 39 | + @staticmethod |
| 40 | + def create_from_cpp(code, command=None): |
| 41 | + # Get default compiler |
| 42 | + compilers = ['cxx', 'clang++', 'g++', 'cc', 'clang', 'gcc'] |
| 43 | + if command is None: |
| 44 | + for compiler in compilers: |
| 45 | + if shutil.which(compiler) is not None: |
| 46 | + command = [compiler, '-c'] |
| 47 | + break |
| 48 | + # Create database |
| 49 | + directory = temporary_dir() |
| 50 | + fpath = os.path.join(directory, 'source.cpp') |
| 51 | + with open(fpath, 'w') as f: |
| 52 | + f.write(code) |
| 53 | + command.append(fpath) |
| 54 | + return Database.create('cpp', directory, command) |
| 55 | + |
| 56 | + def query(self, ql): |
| 57 | + """ |
| 58 | + Syntactic sugar to execute a CodeQL snippet and parse the results. |
| 59 | + """ |
| 60 | + # Prepare query directory |
| 61 | + if not hasattr(self, 'qldir'): |
| 62 | + self.qldir = temporary_dir() |
| 63 | + qlpack_path = os.path.join(self.qldir, 'qlpack.yml') |
| 64 | + with open(qlpack_path, mode='w') as f: |
| 65 | + qlpack_text = CODEQL_QLPACK.format('codeql-cpp') |
| 66 | + f.write(qlpack_text) |
| 67 | + # Perform query |
| 68 | + query_path = os.path.join(self.qldir, 'query.ql') |
| 69 | + reply_path = os.path.join(self.qldir, 'reply.csv') |
| 70 | + with open(query_path, mode='w') as f: |
| 71 | + f.write(ql) |
| 72 | + query = codeql.Query(query_path) |
| 73 | + bqrs = query.run(database=self) |
| 74 | + return bqrs.parse() |
| 75 | + |
| 76 | + # Interface |
| 77 | + @staticmethod |
| 78 | + def create(language, source, command=None, location=None): |
| 79 | + """ |
| 80 | + Create a CodeQL database instance for a source tree that can be analyzed |
| 81 | + using one of the CodeQL products. |
| 82 | +
|
| 83 | + Arguments: |
| 84 | + language -- The language that the new database will be used to analyze. |
| 85 | + source -- The root source code directory. |
| 86 | + In many cases, this will be the checkout root. Files within it are |
| 87 | + considered to be the primary source files for this database. |
| 88 | + In some output formats, files will be referred to by their relative path |
| 89 | + from this directory. |
| 90 | + command -- For compiled languages, build commands that will cause the |
| 91 | + compiler to be invoked on the source code to analyze. These commands |
| 92 | + will be executed under an instrumentation environment that allows |
| 93 | + analysis of generated code and (in some cases) standard libraries. |
| 94 | + database -- Path to generated database |
| 95 | + """ |
| 96 | + # Syntactic sugar: Default location to temporary directory |
| 97 | + if location is None: |
| 98 | + location = temporary_dir() |
| 99 | + |
| 100 | + # Create and submit command |
| 101 | + args = ['database', 'create', '-l', language, '-s', source] |
| 102 | + if command is not None: |
| 103 | + if type(command) == list: |
| 104 | + command = ' '.join(map(lambda x: f'"{x}"' if ' ' in x else x, command)) |
| 105 | + args += ['-c', command] |
| 106 | + args.append(location) |
| 107 | + run(args) |
| 108 | + |
| 109 | + # Return database instance |
| 110 | + return Database(location) |
| 111 | + |
| 112 | + |
| 113 | + def analyze(self, queries, format, output): |
| 114 | + """ |
| 115 | + Analyze a database, producing meaningful results in the context of the |
| 116 | + source code. |
| 117 | +
|
| 118 | + Run a query suite (or some individual queries) against a CodeQL |
| 119 | + database, producing results, styled as alerts or paths, in SARIF or |
| 120 | + another interpreted format. |
| 121 | +
|
| 122 | + This command combines the effect of the codeql database run-queries |
| 123 | + and codeql database interpret-results commands. If you want to run |
| 124 | + queries whose results don't meet the requirements for being interpreted |
| 125 | + as source-code alerts, use codeql database run-queries or codeql query |
| 126 | + run instead, and then codeql bqrs decode to convert the raw results to a |
| 127 | + readable notation. |
| 128 | + """ |
| 129 | + # Support single query or list of queries |
| 130 | + if type(queries) is not list: |
| 131 | + queries = [queries] |
| 132 | + # Prepare options |
| 133 | + options = [f'--format={format}', '-o', output] |
| 134 | + if search_path is not None: |
| 135 | + options += ['--search-path', search_path] |
| 136 | + # Dispatch command |
| 137 | + self.run_command('analyze', options, post=queries) |
| 138 | + |
| 139 | + def upgrade(self): |
| 140 | + """ |
| 141 | + Upgrade a database so it is usable by the current tools. |
| 142 | +
|
| 143 | + This rewrites a CodeQL database to be compatible with the QL libraries |
| 144 | + that are found on the QL pack search path, if necessary. |
| 145 | +
|
| 146 | + If an upgrade is necessary, it is irreversible. The database will |
| 147 | + subsequently be unusable with the libraries that were current when it |
| 148 | + was created. |
| 149 | + """ |
| 150 | + self.run_command('upgrade') |
| 151 | + |
| 152 | + def cleanup(self): |
| 153 | + """ |
| 154 | + Compact a CodeQL database on disk. |
| 155 | +
|
| 156 | + Delete temporary data, and generally make a database as small as |
| 157 | + possible on disk without degrading its future usefulness. |
| 158 | + """ |
| 159 | + self.run_command('cleanup') |
| 160 | + |
| 161 | + def bundle(self, output): |
| 162 | + """ |
| 163 | + Create a relocatable archive of a CodeQL database. |
| 164 | +
|
| 165 | + A command that zips up the useful parts of the database. This will only |
| 166 | + include the mandatory components, unless the user specifically requests |
| 167 | + that results, logs, TRAP, or similar should be included. |
| 168 | + """ |
| 169 | + options = ['-o', output] |
| 170 | + self.run_command('bundle', options) |
0 commit comments