#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ CodeQL for Python. """ import os import shutil import tempfile import codeql from .common import * # Constants CODEQL_QLPACK = ''' name: codeql-python version: 0.0.0 libraryPathDependencies: {} ''' class Database(object): def __init__(self, path, temp=False): """ Arguments: path -- Path of the database temp -- Remove database path in destructor """ self.path = path self.temp = temp def __del__(self): if self.temp: shutil.rmtree(self.path) # Helpers def run_command(self, command, options=[], post=[]): run(['database', command] + options + [self.path] + post) @staticmethod def from_cpp(code, command=None): # Get default compiler compilers = ['cxx', 'clang++', 'g++', 'cc', 'clang', 'gcc'] if command is None: for compiler in compilers: if shutil.which(compiler) is not None: command = [compiler, '-c'] break # Create database directory = temporary_dir() fpath = os.path.join(directory, 'source.cpp') with open(fpath, 'w') as f: f.write(code) command.append(fpath) return Database.create('cpp', directory, command) def query(self, ql): """ Syntactic sugar to execute a CodeQL snippet and parse the results. """ # Prepare query directory if not hasattr(self, 'qldir'): self.qldir = temporary_dir() qlpack_path = os.path.join(self.qldir, 'qlpack.yml') with open(qlpack_path, mode='w') as f: qlpack_text = CODEQL_QLPACK.format('codeql-cpp') f.write(qlpack_text) # Perform query query_path = os.path.join(self.qldir, 'query.ql') reply_path = os.path.join(self.qldir, 'reply.csv') with open(query_path, mode='w') as f: f.write(ql) query = codeql.Query(query_path) bqrs = query.run(database=self) return bqrs.parse() # Interface @staticmethod def create(language, source, command=None, location=None): """ Create a CodeQL database instance for a source tree that can be analyzed using one of the CodeQL products. Arguments: language -- The language that the new database will be used to analyze. source -- The root source code directory. In many cases, this will be the checkout root. Files within it are considered to be the primary source files for this database. In some output formats, files will be referred to by their relative path from this directory. command -- For compiled languages, build commands that will cause the compiler to be invoked on the source code to analyze. These commands will be executed under an instrumentation environment that allows analysis of generated code and (in some cases) standard libraries. database -- Path to generated database """ # Syntactic sugar: Default location to temporary directory if location is None: location = temporary_dir() # Create and submit command args = ['database', 'create', '-l', language, '-s', source] if command is not None: if type(command) == list: command = ' '.join(map(lambda x: f'"{x}"' if ' ' in x else x, command)) args += ['-c', command] args.append(location) run(args) # Return database instance return Database(location) def analyze(self, queries, format, output): """ Analyze a database, producing meaningful results in the context of the source code. Run a query suite (or some individual queries) against a CodeQL database, producing results, styled as alerts or paths, in SARIF or another interpreted format. This command combines the effect of the codeql database run-queries and codeql database interpret-results commands. If you want to run queries whose results don't meet the requirements for being interpreted as source-code alerts, use codeql database run-queries or codeql query run instead, and then codeql bqrs decode to convert the raw results to a readable notation. """ # Support single query or list of queries if type(queries) is not list: queries = [queries] # Prepare options options = [f'--format={format}', '-o', output] if search_path is not None: options += ['--search-path', search_path] # Dispatch command self.run_command('analyze', options, post=queries) def upgrade(self): """ Upgrade a database so it is usable by the current tools. This rewrites a CodeQL database to be compatible with the QL libraries that are found on the QL pack search path, if necessary. If an upgrade is necessary, it is irreversible. The database will subsequently be unusable with the libraries that were current when it was created. """ self.run_command('upgrade') def cleanup(self): """ Compact a CodeQL database on disk. Delete temporary data, and generally make a database as small as possible on disk without degrading its future usefulness. """ self.run_command('cleanup') def bundle(self, output): """ Create a relocatable archive of a CodeQL database. A command that zips up the useful parts of the database. This will only include the mandatory components, unless the user specifically requests that results, logs, TRAP, or similar should be included. """ options = ['-o', output] self.run_command('bundle', options)