Merge branch 'main' into upload-database
This commit is contained in:
commit
33ac512514
30 changed files with 1177 additions and 158 deletions
|
|
@ -21,6 +21,7 @@ test("emptyPaths", async (t) => {
|
|||
codeQLCmd: "",
|
||||
gitHubVersion: { type: util.GitHubVariant.DOTCOM } as util.GitHubVersion,
|
||||
dbLocation: path.resolve(tmpDir, "codeql_databases"),
|
||||
packs: {},
|
||||
};
|
||||
analysisPaths.includeAndExcludeAnalysisPaths(config);
|
||||
t.is(process.env["LGTM_INDEX_INCLUDE"], undefined);
|
||||
|
|
@ -42,6 +43,7 @@ test("nonEmptyPaths", async (t) => {
|
|||
codeQLCmd: "",
|
||||
gitHubVersion: { type: util.GitHubVariant.DOTCOM } as util.GitHubVersion,
|
||||
dbLocation: path.resolve(tmpDir, "codeql_databases"),
|
||||
packs: {},
|
||||
};
|
||||
analysisPaths.includeAndExcludeAnalysisPaths(config);
|
||||
t.is(process.env["LGTM_INDEX_INCLUDE"], "path1\npath2");
|
||||
|
|
@ -67,6 +69,7 @@ test("exclude temp dir", async (t) => {
|
|||
codeQLCmd: "",
|
||||
gitHubVersion: { type: util.GitHubVariant.DOTCOM } as util.GitHubVersion,
|
||||
dbLocation: path.resolve(tempDir, "codeql_databases"),
|
||||
packs: {},
|
||||
};
|
||||
analysisPaths.includeAndExcludeAnalysisPaths(config);
|
||||
t.is(process.env["LGTM_INDEX_INCLUDE"], undefined);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import * as fs from "fs";
|
|||
import * as path from "path";
|
||||
|
||||
import test from "ava";
|
||||
import * as yaml from "js-yaml";
|
||||
import { clean } from "semver";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { runQueries } from "./analyze";
|
||||
|
|
@ -33,9 +35,24 @@ test("status report fields and search path setting", async (t) => {
|
|||
const memoryFlag = "";
|
||||
const addSnippetsFlag = "";
|
||||
const threadsFlag = "";
|
||||
const packs = {
|
||||
[Language.cpp]: [
|
||||
{
|
||||
packName: "a/b",
|
||||
version: clean("1.0.0")!,
|
||||
},
|
||||
],
|
||||
[Language.java]: [
|
||||
{
|
||||
packName: "c/d",
|
||||
version: clean("2.0.0")!,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
for (const language of Object.values(Language)) {
|
||||
setCodeQL({
|
||||
packDownload: async () => ({ packs: [] }),
|
||||
databaseRunQueries: async (
|
||||
_db: string,
|
||||
searchPath: string | undefined
|
||||
|
|
@ -99,6 +116,7 @@ test("status report fields and search path setting", async (t) => {
|
|||
type: util.GitHubVariant.DOTCOM,
|
||||
} as util.GitHubVersion,
|
||||
dbLocation: path.resolve(tmpDir, "codeql_databases"),
|
||||
packs,
|
||||
};
|
||||
fs.mkdirSync(util.getCodeQLDatabasePath(config, language), {
|
||||
recursive: true,
|
||||
|
|
@ -117,13 +135,32 @@ test("status report fields and search path setting", async (t) => {
|
|||
config,
|
||||
getRunnerLogger(true)
|
||||
);
|
||||
t.deepEqual(Object.keys(builtinStatusReport).length, 2);
|
||||
t.true(
|
||||
`analyze_builtin_queries_${language}_duration_ms` in builtinStatusReport
|
||||
);
|
||||
t.true(
|
||||
`interpret_results_${language}_duration_ms` in builtinStatusReport
|
||||
);
|
||||
const hasPacks = language in packs;
|
||||
const statusReportKeys = Object.keys(builtinStatusReport).sort();
|
||||
if (hasPacks) {
|
||||
t.deepEqual(statusReportKeys.length, 3, statusReportKeys.toString());
|
||||
t.deepEqual(
|
||||
statusReportKeys[0],
|
||||
`analyze_builtin_queries_${language}_duration_ms`
|
||||
);
|
||||
t.deepEqual(
|
||||
statusReportKeys[1],
|
||||
`analyze_custom_queries_${language}_duration_ms`
|
||||
);
|
||||
t.deepEqual(
|
||||
statusReportKeys[2],
|
||||
`interpret_results_${language}_duration_ms`
|
||||
);
|
||||
} else {
|
||||
t.deepEqual(
|
||||
statusReportKeys[0],
|
||||
`analyze_builtin_queries_${language}_duration_ms`
|
||||
);
|
||||
t.deepEqual(
|
||||
statusReportKeys[1],
|
||||
`interpret_results_${language}_duration_ms`
|
||||
);
|
||||
}
|
||||
|
||||
config.queries[language] = {
|
||||
builtin: [],
|
||||
|
|
@ -151,11 +188,15 @@ test("status report fields and search path setting", async (t) => {
|
|||
t.true(
|
||||
`analyze_custom_queries_${language}_duration_ms` in customStatusReport
|
||||
);
|
||||
const expectedSearchPathsUsed = hasPacks
|
||||
? [undefined, undefined, "/1", "/2", undefined]
|
||||
: [undefined, "/1", "/2"];
|
||||
t.deepEqual(searchPathsUsed, expectedSearchPathsUsed);
|
||||
t.true(`interpret_results_${language}_duration_ms` in customStatusReport);
|
||||
t.deepEqual(searchPathsUsed, [undefined, "/1", "/2"]);
|
||||
}
|
||||
|
||||
verifyLineCounts(tmpDir);
|
||||
verifyQuerySuites(tmpDir);
|
||||
});
|
||||
|
||||
function verifyLineCounts(tmpDir: string) {
|
||||
|
|
@ -192,7 +233,59 @@ test("status report fields and search path setting", async (t) => {
|
|||
baseline: lineCount,
|
||||
},
|
||||
]);
|
||||
// when the rule doesn't exists, it should not be added
|
||||
// when the rule doesn't exist, it should not be added
|
||||
t.deepEqual(sarif.runs[2].properties.metricResults, []);
|
||||
}
|
||||
|
||||
function verifyQuerySuites(tmpDir: string) {
|
||||
const qlsContent = [
|
||||
{
|
||||
query: "foo.ql",
|
||||
},
|
||||
];
|
||||
const qlsContent2 = [
|
||||
{
|
||||
query: "bar.ql",
|
||||
},
|
||||
];
|
||||
const qlsPackContentCpp = [
|
||||
{
|
||||
qlpack: "a/b",
|
||||
version: "1.0.0",
|
||||
},
|
||||
];
|
||||
const qlsPackContentJava = [
|
||||
{
|
||||
qlpack: "c/d",
|
||||
version: "2.0.0",
|
||||
},
|
||||
];
|
||||
for (const lang of Object.values(Language)) {
|
||||
t.deepEqual(readContents(`${lang}-queries-builtin.qls`), qlsContent);
|
||||
t.deepEqual(readContents(`${lang}-queries-custom-0.qls`), qlsContent);
|
||||
t.deepEqual(readContents(`${lang}-queries-custom-1.qls`), qlsContent2);
|
||||
const packSuiteName = `${lang}-queries-packs.qls`;
|
||||
if (lang === Language.cpp) {
|
||||
t.deepEqual(readContents(packSuiteName), qlsPackContentCpp);
|
||||
} else if (lang === Language.java) {
|
||||
t.deepEqual(readContents(packSuiteName), qlsPackContentJava);
|
||||
} else {
|
||||
t.false(
|
||||
fs.existsSync(path.join(tmpDir, "codeql_databases", packSuiteName))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function readContents(name: string) {
|
||||
const x = fs.readFileSync(
|
||||
path.join(tmpDir, "codeql_databases", name),
|
||||
"utf8"
|
||||
);
|
||||
console.log(x);
|
||||
|
||||
return yaml.safeLoad(
|
||||
fs.readFileSync(path.join(tmpDir, "codeql_databases", name), "utf8")
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -176,19 +176,41 @@ export async function runQueries(
|
|||
);
|
||||
|
||||
for (const language of config.languages) {
|
||||
logger.startGroup(`Running queries for ${language}`);
|
||||
|
||||
const queries = config.queries[language];
|
||||
if (
|
||||
queries === undefined ||
|
||||
(queries.builtin.length === 0 && queries.custom.length === 0)
|
||||
) {
|
||||
const packsWithVersion = config.packs[language] || [];
|
||||
|
||||
const hasBuiltinQueries = queries?.builtin.length > 0;
|
||||
const hasCustomQueries = queries?.custom.length > 0;
|
||||
const hasPackWithCustomQueries = packsWithVersion.length > 0;
|
||||
|
||||
if (!hasBuiltinQueries && !hasCustomQueries && !hasPackWithCustomQueries) {
|
||||
throw new Error(
|
||||
`Unable to analyse ${language} as no queries were selected for this language`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
if (hasPackWithCustomQueries) {
|
||||
logger.info("*************");
|
||||
logger.info(
|
||||
"Performing analysis with custom QL Packs. QL Packs are an experimental feature."
|
||||
);
|
||||
logger.info("And should not be used in production yet.");
|
||||
logger.info("*************");
|
||||
logger.startGroup(`Downloading custom packs for ${language}`);
|
||||
|
||||
const codeql = getCodeQL(config.codeQLCmd);
|
||||
const results = await codeql.packDownload(packsWithVersion);
|
||||
logger.info(
|
||||
`Downloaded packs: ${results.packs
|
||||
.map((r) => `${r.name}@${r.version || "latest"}`)
|
||||
.join(", ")}`
|
||||
);
|
||||
|
||||
logger.endGroup();
|
||||
}
|
||||
|
||||
logger.startGroup(`Running queries for ${language}`);
|
||||
const querySuitePaths: string[] = [];
|
||||
if (queries["builtin"].length > 0) {
|
||||
const startTimeBuiltIn = new Date().getTime();
|
||||
|
|
@ -196,7 +218,7 @@ export async function runQueries(
|
|||
await runQueryGroup(
|
||||
language,
|
||||
"builtin",
|
||||
queries["builtin"],
|
||||
createQuerySuiteContents(queries["builtin"]),
|
||||
undefined
|
||||
)
|
||||
);
|
||||
|
|
@ -211,13 +233,24 @@ export async function runQueries(
|
|||
await runQueryGroup(
|
||||
language,
|
||||
`custom-${i}`,
|
||||
queries["custom"][i].queries,
|
||||
createQuerySuiteContents(queries["custom"][i].queries),
|
||||
queries["custom"][i].searchPath
|
||||
)
|
||||
);
|
||||
ranCustom = true;
|
||||
}
|
||||
}
|
||||
if (packsWithVersion.length > 0) {
|
||||
querySuitePaths.push(
|
||||
await runQueryGroup(
|
||||
language,
|
||||
"packs",
|
||||
createPackSuiteContents(packsWithVersion),
|
||||
undefined
|
||||
)
|
||||
);
|
||||
ranCustom = true;
|
||||
}
|
||||
if (ranCustom) {
|
||||
statusReport[`analyze_custom_queries_${language}_duration_ms`] =
|
||||
new Date().getTime() - startTimeCustom;
|
||||
|
|
@ -239,6 +272,7 @@ export async function runQueries(
|
|||
printLinesOfCodeSummary(logger, language, await locPromise);
|
||||
} catch (e) {
|
||||
logger.info(e);
|
||||
logger.info(e.stack);
|
||||
statusReport.analyze_failure_language = language;
|
||||
throw new CodeQLAnalysisError(
|
||||
statusReport,
|
||||
|
|
@ -269,18 +303,17 @@ export async function runQueries(
|
|||
async function runQueryGroup(
|
||||
language: Language,
|
||||
type: string,
|
||||
queries: string[],
|
||||
querySuiteContents: string,
|
||||
searchPath: string | undefined
|
||||
): Promise<string> {
|
||||
const databasePath = util.getCodeQLDatabasePath(config, language);
|
||||
// Pass the queries to codeql using a file instead of using the command
|
||||
// line to avoid command line length restrictions, particularly on windows.
|
||||
const querySuitePath = `${databasePath}-queries-${type}.qls`;
|
||||
const querySuiteContents = queries
|
||||
.map((q: string) => `- query: ${q}`)
|
||||
.join("\n");
|
||||
fs.writeFileSync(querySuitePath, querySuiteContents);
|
||||
logger.debug(`Query suite file for ${language}...\n${querySuiteContents}`);
|
||||
logger.debug(
|
||||
`Query suite file for ${language}-${type}...\n${querySuiteContents}`
|
||||
);
|
||||
|
||||
const codeql = getCodeQL(config.codeQLCmd);
|
||||
await codeql.databaseRunQueries(
|
||||
|
|
@ -296,6 +329,26 @@ export async function runQueries(
|
|||
}
|
||||
}
|
||||
|
||||
function createQuerySuiteContents(queries: string[]) {
|
||||
return queries.map((q: string) => `- query: ${q}`).join("\n");
|
||||
}
|
||||
|
||||
function createPackSuiteContents(
|
||||
packsWithVersion: configUtils.PackWithVersion[]
|
||||
) {
|
||||
return packsWithVersion.map(packWithVersionToQuerySuiteEntry).join("\n");
|
||||
}
|
||||
|
||||
function packWithVersionToQuerySuiteEntry(
|
||||
pack: configUtils.PackWithVersion
|
||||
): string {
|
||||
let text = `- qlpack: ${pack.packName}`;
|
||||
if (pack.version) {
|
||||
text += `\n version: ${pack.version}`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
export async function runAnalyze(
|
||||
outputDir: string,
|
||||
memoryFlag: string,
|
||||
|
|
@ -310,10 +363,8 @@ export async function runAnalyze(
|
|||
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
logger.info("Finalizing database creation");
|
||||
await finalizeDatabaseCreation(config, threadsFlag, logger);
|
||||
|
||||
logger.info("Analyzing database");
|
||||
const queriesStats = await runQueries(
|
||||
outputDir,
|
||||
memoryFlag,
|
||||
|
|
@ -376,7 +427,7 @@ function printLinesOfCodeSummary(
|
|||
) {
|
||||
if (language in lineCounts) {
|
||||
logger.info(
|
||||
`Counted ${lineCounts[language]} lines of code for ${language} as a baseline.`
|
||||
`Counted a baseline of ${lineCounts[language]} lines of code for ${language}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
133
src/codeql.ts
133
src/codeql.ts
|
|
@ -13,6 +13,7 @@ import { v4 as uuidV4 } from "uuid";
|
|||
|
||||
import { isRunningLocalAction, getRelativeScriptPath } from "./actions-util";
|
||||
import * as api from "./api-client";
|
||||
import { PackWithVersion } from "./config-utils";
|
||||
import * as defaults from "./defaults.json"; // Referenced from codeql-action-sync-tool!
|
||||
import { errorMatchers } from "./error-matcher";
|
||||
import { Language } from "./languages";
|
||||
|
|
@ -88,6 +89,12 @@ export interface CodeQL {
|
|||
queries: string[],
|
||||
extraSearchPath: string | undefined
|
||||
): Promise<ResolveQueriesOutput>;
|
||||
|
||||
/**
|
||||
* Run 'codeql pack download'.
|
||||
*/
|
||||
packDownload(packs: PackWithVersion[]): Promise<PackDownloadOutput>;
|
||||
|
||||
/**
|
||||
* Run 'codeql database cleanup'.
|
||||
*/
|
||||
|
|
@ -137,6 +144,17 @@ export interface ResolveQueriesOutput {
|
|||
};
|
||||
}
|
||||
|
||||
export interface PackDownloadOutput {
|
||||
packs: PackDownloadItem[];
|
||||
}
|
||||
|
||||
interface PackDownloadItem {
|
||||
name: string;
|
||||
version: string;
|
||||
packDir: string;
|
||||
installResult: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the CodeQL object, and is populated by `setupCodeQL` or `getCodeQL`.
|
||||
* Can be overridden in tests using `setCodeQL`.
|
||||
|
|
@ -496,6 +514,7 @@ export function setCodeQL(partialCodeql: Partial<CodeQL>): CodeQL {
|
|||
finalizeDatabase: resolveFunction(partialCodeql, "finalizeDatabase"),
|
||||
resolveLanguages: resolveFunction(partialCodeql, "resolveLanguages"),
|
||||
resolveQueries: resolveFunction(partialCodeql, "resolveQueries"),
|
||||
packDownload: resolveFunction(partialCodeql, "packDownload"),
|
||||
databaseCleanup: resolveFunction(partialCodeql, "databaseCleanup"),
|
||||
databaseBundle: resolveFunction(partialCodeql, "databaseBundle"),
|
||||
databaseRunQueries: resolveFunction(partialCodeql, "databaseRunQueries"),
|
||||
|
|
@ -527,7 +546,7 @@ function getCodeQLForCmd(cmd: string): CodeQL {
|
|||
return cmd;
|
||||
},
|
||||
async printVersion() {
|
||||
await new toolrunner.ToolRunner(cmd, ["version", "--format=json"]).exec();
|
||||
await runTool(cmd, ["version", "--format=json"]);
|
||||
},
|
||||
async getTracerEnv(databasePath: string) {
|
||||
// Write tracer-env.js to a temp location.
|
||||
|
|
@ -568,7 +587,7 @@ function getCodeQLForCmd(cmd: string): CodeQL {
|
|||
// _and_ is present in the latest supported CLI release.)
|
||||
const envFile = path.resolve(databasePath, "working", "env.tmp");
|
||||
|
||||
await new toolrunner.ToolRunner(cmd, [
|
||||
await runTool(cmd, [
|
||||
"database",
|
||||
"trace-command",
|
||||
databasePath,
|
||||
|
|
@ -576,7 +595,7 @@ function getCodeQLForCmd(cmd: string): CodeQL {
|
|||
process.execPath,
|
||||
tracerEnvJs,
|
||||
envFile,
|
||||
]).exec();
|
||||
]);
|
||||
return JSON.parse(fs.readFileSync(envFile, "utf-8"));
|
||||
},
|
||||
async databaseInit(
|
||||
|
|
@ -584,14 +603,14 @@ function getCodeQLForCmd(cmd: string): CodeQL {
|
|||
language: Language,
|
||||
sourceRoot: string
|
||||
) {
|
||||
await new toolrunner.ToolRunner(cmd, [
|
||||
await runTool(cmd, [
|
||||
"database",
|
||||
"init",
|
||||
databasePath,
|
||||
`--language=${language}`,
|
||||
`--source-root=${sourceRoot}`,
|
||||
...getExtraOptionsFromEnv(["database", "init"]),
|
||||
]).exec();
|
||||
]);
|
||||
},
|
||||
async runAutobuild(language: Language) {
|
||||
const cmdName =
|
||||
|
|
@ -615,7 +634,7 @@ function getCodeQLForCmd(cmd: string): CodeQL {
|
|||
"-Dmaven.wagon.http.pool=false",
|
||||
].join(" ");
|
||||
|
||||
await new toolrunner.ToolRunner(autobuildCmd).exec();
|
||||
await runTool(autobuildCmd);
|
||||
},
|
||||
async extractScannedLanguage(databasePath: string, language: Language) {
|
||||
// Get extractor location
|
||||
|
|
@ -680,14 +699,7 @@ function getCodeQLForCmd(cmd: string): CodeQL {
|
|||
},
|
||||
async resolveLanguages() {
|
||||
const codeqlArgs = ["resolve", "languages", "--format=json"];
|
||||
let output = "";
|
||||
await new toolrunner.ToolRunner(cmd, codeqlArgs, {
|
||||
listeners: {
|
||||
stdout: (data: Buffer) => {
|
||||
output += data.toString();
|
||||
},
|
||||
},
|
||||
}).exec();
|
||||
const output = await runTool(cmd, codeqlArgs);
|
||||
|
||||
try {
|
||||
return JSON.parse(output);
|
||||
|
|
@ -711,14 +723,7 @@ function getCodeQLForCmd(cmd: string): CodeQL {
|
|||
if (extraSearchPath !== undefined) {
|
||||
codeqlArgs.push("--additional-packs", extraSearchPath);
|
||||
}
|
||||
let output = "";
|
||||
await new toolrunner.ToolRunner(cmd, codeqlArgs, {
|
||||
listeners: {
|
||||
stdout: (data: Buffer) => {
|
||||
output += data.toString();
|
||||
},
|
||||
},
|
||||
}).exec();
|
||||
const output = await runTool(cmd, codeqlArgs);
|
||||
|
||||
try {
|
||||
return JSON.parse(output);
|
||||
|
|
@ -733,7 +738,7 @@ function getCodeQLForCmd(cmd: string): CodeQL {
|
|||
memoryFlag: string,
|
||||
threadsFlag: string
|
||||
): Promise<void> {
|
||||
const args = [
|
||||
const codeqlArgs = [
|
||||
"database",
|
||||
"run-queries",
|
||||
memoryFlag,
|
||||
|
|
@ -744,10 +749,10 @@ function getCodeQLForCmd(cmd: string): CodeQL {
|
|||
...getExtraOptionsFromEnv(["database", "run-queries"]),
|
||||
];
|
||||
if (extraSearchPath !== undefined) {
|
||||
args.push("--additional-packs", extraSearchPath);
|
||||
codeqlArgs.push("--additional-packs", extraSearchPath);
|
||||
}
|
||||
args.push(querySuitePath);
|
||||
await new toolrunner.ToolRunner(cmd, args).exec();
|
||||
codeqlArgs.push(querySuitePath);
|
||||
await runTool(cmd, codeqlArgs);
|
||||
},
|
||||
async databaseInterpretResults(
|
||||
databasePath: string,
|
||||
|
|
@ -757,7 +762,7 @@ function getCodeQLForCmd(cmd: string): CodeQL {
|
|||
threadsFlag: string,
|
||||
automationDetailsId: string | undefined
|
||||
): Promise<string> {
|
||||
const args = [
|
||||
const codeqlArgs = [
|
||||
"database",
|
||||
"interpret-results",
|
||||
threadsFlag,
|
||||
|
|
@ -770,31 +775,64 @@ function getCodeQLForCmd(cmd: string): CodeQL {
|
|||
...getExtraOptionsFromEnv(["database", "interpret-results"]),
|
||||
];
|
||||
if (automationDetailsId !== undefined) {
|
||||
args.push("--sarif-category", automationDetailsId);
|
||||
codeqlArgs.push("--sarif-category", automationDetailsId);
|
||||
}
|
||||
args.push(databasePath, ...querySuitePaths);
|
||||
codeqlArgs.push(databasePath, ...querySuitePaths);
|
||||
// capture stdout, which contains analysis summaries
|
||||
let output = "";
|
||||
await new toolrunner.ToolRunner(cmd, args, {
|
||||
listeners: {
|
||||
stdout: (data: Buffer) => {
|
||||
output += data.toString("utf8");
|
||||
},
|
||||
},
|
||||
}).exec();
|
||||
return output;
|
||||
return await runTool(cmd, codeqlArgs);
|
||||
},
|
||||
|
||||
/**
|
||||
* Download specified packs into the package cache. If the specified
|
||||
* package and version already exists (e.g., from a previous analysis run),
|
||||
* then it is not downloaded again (unless the extra option `--force` is
|
||||
* specified).
|
||||
*
|
||||
* If no version is specified, then the latest version is
|
||||
* downloaded. The check to determine what the latest version is is done
|
||||
* each time this package is requested.
|
||||
*/
|
||||
async packDownload(packs: PackWithVersion[]): Promise<PackDownloadOutput> {
|
||||
const codeqlArgs = [
|
||||
"pack",
|
||||
"download",
|
||||
"--format=json",
|
||||
...getExtraOptionsFromEnv(["pack", "download"]),
|
||||
...packs.map(packWithVersionToString),
|
||||
];
|
||||
|
||||
const output = await runTool(cmd, codeqlArgs);
|
||||
|
||||
try {
|
||||
const parsedOutput: PackDownloadOutput = JSON.parse(output);
|
||||
if (
|
||||
Array.isArray(parsedOutput.packs) &&
|
||||
// TODO PackDownloadOutput will not include the version if it is not specified
|
||||
// in the input. The version is always the latest version available.
|
||||
// It should be added to the output, but this requires a CLI change
|
||||
parsedOutput.packs.every((p) => p.name /* && p.version */)
|
||||
) {
|
||||
return parsedOutput;
|
||||
} else {
|
||||
throw new Error("Unexpected output from pack download");
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Attempted to download specified packs but got an error:\n${output}\n${e}`
|
||||
);
|
||||
}
|
||||
},
|
||||
async databaseCleanup(
|
||||
databasePath: string,
|
||||
cleanupLevel: string
|
||||
): Promise<void> {
|
||||
const args = [
|
||||
const codeqlArgs = [
|
||||
"database",
|
||||
"cleanup",
|
||||
databasePath,
|
||||
`--mode=${cleanupLevel}`,
|
||||
];
|
||||
await new toolrunner.ToolRunner(cmd, args).exec();
|
||||
await runTool(cmd, codeqlArgs);
|
||||
},
|
||||
async databaseBundle(
|
||||
databasePath: string,
|
||||
|
|
@ -811,6 +849,9 @@ function getCodeQLForCmd(cmd: string): CodeQL {
|
|||
};
|
||||
}
|
||||
|
||||
function packWithVersionToString(pack: PackWithVersion): string {
|
||||
return pack.version ? `${pack.packName}@${pack.version}` : pack.packName;
|
||||
}
|
||||
/**
|
||||
* Gets the options for `path` of `options` as an array of extra option strings.
|
||||
*/
|
||||
|
|
@ -870,3 +911,15 @@ export function getExtraOptions(
|
|||
);
|
||||
return all.concat(specific);
|
||||
}
|
||||
|
||||
async function runTool(cmd: string, args: string[] = []) {
|
||||
let output = "";
|
||||
await new toolrunner.ToolRunner(cmd, args, {
|
||||
listeners: {
|
||||
stdout: (data: Buffer) => {
|
||||
output += data.toString();
|
||||
},
|
||||
},
|
||||
}).exec();
|
||||
return output;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import * as fs from "fs";
|
|||
import * as path from "path";
|
||||
|
||||
import * as github from "@actions/github";
|
||||
import test from "ava";
|
||||
import test, { ExecutionContext } from "ava";
|
||||
import { clean } from "semver";
|
||||
import sinon from "sinon";
|
||||
|
||||
import * as api from "./api-client";
|
||||
|
|
@ -318,6 +319,7 @@ test("load non-empty input", async (t) => {
|
|||
codeQLCmd: codeQL.getPath(),
|
||||
gitHubVersion,
|
||||
dbLocation: path.resolve(tmpDir, "codeql_databases"),
|
||||
packs: {} as configUtils.Packs,
|
||||
};
|
||||
|
||||
const languages = "javascript";
|
||||
|
|
@ -983,6 +985,137 @@ test("Unknown languages", async (t) => {
|
|||
});
|
||||
});
|
||||
|
||||
test("Config specifies packages", async (t) => {
|
||||
return await util.withTmpDir(async (tmpDir) => {
|
||||
const codeQL = setCodeQL({
|
||||
async resolveQueries() {
|
||||
return {
|
||||
byLanguage: {},
|
||||
noDeclaredLanguage: {},
|
||||
multipleDeclaredLanguages: {},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const inputFileContents = `
|
||||
name: my config
|
||||
disable-default-queries: true
|
||||
packs:
|
||||
- a/b@1.2.3
|
||||
`;
|
||||
|
||||
const configFile = path.join(tmpDir, "codeql-config.yaml");
|
||||
fs.writeFileSync(configFile, inputFileContents);
|
||||
|
||||
const languages = "javascript";
|
||||
|
||||
const { packs } = await configUtils.initConfig(
|
||||
languages,
|
||||
undefined,
|
||||
configFile,
|
||||
undefined,
|
||||
{ owner: "github", repo: "example " },
|
||||
tmpDir,
|
||||
tmpDir,
|
||||
codeQL,
|
||||
tmpDir,
|
||||
gitHubVersion,
|
||||
sampleApiDetails,
|
||||
getRunnerLogger(true)
|
||||
);
|
||||
t.deepEqual(packs as unknown, {
|
||||
[Language.javascript]: [
|
||||
{
|
||||
packName: "a/b",
|
||||
version: clean("1.2.3"),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("Config specifies packages for multiple languages", async (t) => {
|
||||
return await util.withTmpDir(async (tmpDir) => {
|
||||
const codeQL = setCodeQL({
|
||||
async resolveQueries() {
|
||||
return {
|
||||
byLanguage: {
|
||||
cpp: { "/foo/a.ql": {} },
|
||||
},
|
||||
noDeclaredLanguage: {},
|
||||
multipleDeclaredLanguages: {},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const inputFileContents = `
|
||||
name: my config
|
||||
disable-default-queries: true
|
||||
queries:
|
||||
- uses: ./foo
|
||||
packs:
|
||||
javascript:
|
||||
- a/b@1.2.3
|
||||
python:
|
||||
- c/d@1.2.3
|
||||
`;
|
||||
|
||||
const configFile = path.join(tmpDir, "codeql-config.yaml");
|
||||
fs.writeFileSync(configFile, inputFileContents);
|
||||
fs.mkdirSync(path.join(tmpDir, "foo"));
|
||||
|
||||
const languages = "javascript,python,cpp";
|
||||
|
||||
const { packs, queries } = await configUtils.initConfig(
|
||||
languages,
|
||||
undefined,
|
||||
configFile,
|
||||
undefined,
|
||||
{ owner: "github", repo: "example" },
|
||||
tmpDir,
|
||||
tmpDir,
|
||||
codeQL,
|
||||
tmpDir,
|
||||
gitHubVersion,
|
||||
sampleApiDetails,
|
||||
getRunnerLogger(true)
|
||||
);
|
||||
t.deepEqual(packs as unknown, {
|
||||
[Language.javascript]: [
|
||||
{
|
||||
packName: "a/b",
|
||||
version: clean("1.2.3"),
|
||||
},
|
||||
],
|
||||
[Language.python]: [
|
||||
{
|
||||
packName: "c/d",
|
||||
version: clean("1.2.3"),
|
||||
},
|
||||
],
|
||||
});
|
||||
t.deepEqual(queries, {
|
||||
cpp: {
|
||||
builtin: [],
|
||||
custom: [
|
||||
{
|
||||
queries: ["/foo/a.ql"],
|
||||
searchPath: tmpDir,
|
||||
},
|
||||
],
|
||||
},
|
||||
javascript: {
|
||||
builtin: [],
|
||||
custom: [],
|
||||
},
|
||||
python: {
|
||||
builtin: [],
|
||||
custom: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function doInvalidInputTest(
|
||||
testName: string,
|
||||
inputFileContents: string,
|
||||
|
|
@ -1177,3 +1310,109 @@ test("path sanitisation", (t) => {
|
|||
"foo/"
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Test macro for ensuring the packs block is valid
|
||||
*/
|
||||
function parsePacksMacro(
|
||||
t: ExecutionContext<unknown>,
|
||||
packsByLanguage: string[] | Record<string, string[]>,
|
||||
languages: Language[],
|
||||
expected
|
||||
) {
|
||||
t.deepEqual(
|
||||
configUtils.parsePacks(packsByLanguage, languages, "/a/b"),
|
||||
expected
|
||||
);
|
||||
}
|
||||
parsePacksMacro.title = (providedTitle: string) =>
|
||||
`Parse Packs: ${providedTitle}`;
|
||||
|
||||
/**
|
||||
* Test macro for testing when the packs block is invalid
|
||||
*/
|
||||
function parsePacksErrorMacro(
|
||||
t: ExecutionContext<unknown>,
|
||||
packsByLanguage,
|
||||
languages: Language[],
|
||||
expected: RegExp
|
||||
) {
|
||||
t.throws(
|
||||
() => {
|
||||
configUtils.parsePacks(packsByLanguage, languages, "/a/b");
|
||||
},
|
||||
{
|
||||
message: expected,
|
||||
}
|
||||
);
|
||||
}
|
||||
parsePacksErrorMacro.title = (providedTitle: string) =>
|
||||
`Parse Packs Error: ${providedTitle}`;
|
||||
|
||||
function invalidPackNameMacro(t: ExecutionContext<unknown>, name: string) {
|
||||
parsePacksErrorMacro(
|
||||
t,
|
||||
{ [Language.cpp]: [name] },
|
||||
[Language.cpp],
|
||||
new RegExp(
|
||||
`The configuration file "/a/b" is invalid: property "packs" "${name}" is not a valid pack`
|
||||
)
|
||||
);
|
||||
}
|
||||
invalidPackNameMacro.title = (_: string, arg: string) =>
|
||||
`Invalid pack string: ${arg}`;
|
||||
|
||||
test("no packs", parsePacksMacro, {}, [], {});
|
||||
test("two packs", parsePacksMacro, ["a/b", "c/d@1.2.3"], [Language.cpp], {
|
||||
[Language.cpp]: [
|
||||
{ packName: "a/b", version: undefined },
|
||||
{ packName: "c/d", version: clean("1.2.3") },
|
||||
],
|
||||
});
|
||||
test(
|
||||
"two packs with language",
|
||||
parsePacksMacro,
|
||||
{
|
||||
[Language.cpp]: ["a/b", "c/d@1.2.3"],
|
||||
[Language.java]: ["d/e", "f/g@1.2.3"],
|
||||
},
|
||||
[Language.cpp, Language.java, Language.csharp],
|
||||
{
|
||||
[Language.cpp]: [
|
||||
{ packName: "a/b", version: undefined },
|
||||
{ packName: "c/d", version: clean("1.2.3") },
|
||||
],
|
||||
[Language.java]: [
|
||||
{ packName: "d/e", version: undefined },
|
||||
{ packName: "f/g", version: clean("1.2.3") },
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
test(
|
||||
"no language",
|
||||
parsePacksErrorMacro,
|
||||
["a/b@1.2.3"],
|
||||
[Language.java, Language.python],
|
||||
/The configuration file "\/a\/b" is invalid: property "packs" must split packages by language/
|
||||
);
|
||||
test(
|
||||
"invalid language",
|
||||
parsePacksErrorMacro,
|
||||
{ [Language.java]: ["c/d"] },
|
||||
[Language.cpp],
|
||||
/The configuration file "\/a\/b" is invalid: property "packs" has "java", but it is not one of the languages to analyze/
|
||||
);
|
||||
test(
|
||||
"not an array",
|
||||
parsePacksErrorMacro,
|
||||
{ [Language.cpp]: "c/d" },
|
||||
[Language.cpp],
|
||||
/The configuration file "\/a\/b" is invalid: property "packs" must be an array of non-empty strings/
|
||||
);
|
||||
|
||||
test(invalidPackNameMacro, "c"); // all packs require at least a scope and a name
|
||||
test(invalidPackNameMacro, "c-/d");
|
||||
test(invalidPackNameMacro, "-c/d");
|
||||
test(invalidPackNameMacro, "c/d_d");
|
||||
test(invalidPackNameMacro, "c/d@x");
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import * as fs from "fs";
|
|||
import * as path from "path";
|
||||
|
||||
import * as yaml from "js-yaml";
|
||||
import * as semver from "semver";
|
||||
|
||||
import * as api from "./api-client";
|
||||
import { CodeQL, ResolveQueriesOutput } from "./codeql";
|
||||
|
|
@ -18,6 +19,7 @@ const QUERIES_PROPERTY = "queries";
|
|||
const QUERIES_USES_PROPERTY = "uses";
|
||||
const PATHS_IGNORE_PROPERTY = "paths-ignore";
|
||||
const PATHS_PROPERTY = "paths";
|
||||
const PACKS_PROPERTY = "packs";
|
||||
|
||||
/**
|
||||
* Format of the config file supplied by the user.
|
||||
|
|
@ -31,6 +33,11 @@ export interface UserConfig {
|
|||
}>;
|
||||
"paths-ignore"?: string[];
|
||||
paths?: string[];
|
||||
|
||||
// If this is a multi-language analysis, then the packages must be split by
|
||||
// language. If this is a single language analysis, then no split by
|
||||
// language is necessary.
|
||||
packs?: Record<string, string[]> | string[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -114,6 +121,19 @@ export interface Config {
|
|||
* The location where CodeQL databases should be stored.
|
||||
*/
|
||||
dbLocation: string;
|
||||
/**
|
||||
* List of packages, separated by language to download before any analysis.
|
||||
*/
|
||||
packs: Packs;
|
||||
}
|
||||
|
||||
export type Packs = Partial<Record<Language, PackWithVersion[]>>;
|
||||
|
||||
export interface PackWithVersion {
|
||||
/** qualified name of a package reference */
|
||||
packName: string;
|
||||
/** version of the package, or undefined, which means latest version */
|
||||
version?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -536,6 +556,44 @@ export function getPathsInvalid(configFile: string): string {
|
|||
);
|
||||
}
|
||||
|
||||
export function getPacksRequireLanguage(
|
||||
lang: string,
|
||||
configFile: string
|
||||
): string {
|
||||
return getConfigFilePropertyError(
|
||||
configFile,
|
||||
PACKS_PROPERTY,
|
||||
`has "${lang}", but it is not one of the languages to analyze`
|
||||
);
|
||||
}
|
||||
|
||||
export function getPacksInvalidSplit(configFile: string): string {
|
||||
return getConfigFilePropertyError(
|
||||
configFile,
|
||||
PACKS_PROPERTY,
|
||||
"must split packages by language"
|
||||
);
|
||||
}
|
||||
|
||||
export function getPacksInvalid(configFile: string): string {
|
||||
return getConfigFilePropertyError(
|
||||
configFile,
|
||||
PACKS_PROPERTY,
|
||||
"must be an array of non-empty strings"
|
||||
);
|
||||
}
|
||||
|
||||
export function getPacksStrInvalid(
|
||||
packStr: string,
|
||||
configFile: string
|
||||
): string {
|
||||
return getConfigFilePropertyError(
|
||||
configFile,
|
||||
PACKS_PROPERTY,
|
||||
`"${packStr}" is not a valid pack`
|
||||
);
|
||||
}
|
||||
|
||||
export function getLocalPathOutsideOfRepository(
|
||||
configFile: string | undefined,
|
||||
localPath: string
|
||||
|
|
@ -787,6 +845,7 @@ export async function getDefaultConfig(
|
|||
queries,
|
||||
pathsIgnore: [],
|
||||
paths: [],
|
||||
packs: {},
|
||||
originalUserInput: {},
|
||||
tempDir,
|
||||
toolCacheDir,
|
||||
|
|
@ -883,10 +942,11 @@ async function loadConfig(
|
|||
shouldAddConfigFileQueries(queriesInput) &&
|
||||
QUERIES_PROPERTY in parsedYAML
|
||||
) {
|
||||
if (!(parsedYAML[QUERIES_PROPERTY] instanceof Array)) {
|
||||
const queriesArr = parsedYAML[QUERIES_PROPERTY];
|
||||
if (!Array.isArray(queriesArr)) {
|
||||
throw new Error(getQueriesInvalid(configFile));
|
||||
}
|
||||
for (const query of parsedYAML[QUERIES_PROPERTY]!) {
|
||||
for (const query of queriesArr) {
|
||||
if (
|
||||
!(QUERIES_USES_PROPERTY in query) ||
|
||||
typeof query[QUERIES_USES_PROPERTY] !== "string"
|
||||
|
|
@ -908,7 +968,7 @@ async function loadConfig(
|
|||
}
|
||||
|
||||
if (PATHS_IGNORE_PROPERTY in parsedYAML) {
|
||||
if (!(parsedYAML[PATHS_IGNORE_PROPERTY] instanceof Array)) {
|
||||
if (!Array.isArray(parsedYAML[PATHS_IGNORE_PROPERTY])) {
|
||||
throw new Error(getPathsIgnoreInvalid(configFile));
|
||||
}
|
||||
for (const ignorePath of parsedYAML[PATHS_IGNORE_PROPERTY]!) {
|
||||
|
|
@ -927,7 +987,7 @@ async function loadConfig(
|
|||
}
|
||||
|
||||
if (PATHS_PROPERTY in parsedYAML) {
|
||||
if (!(parsedYAML[PATHS_PROPERTY] instanceof Array)) {
|
||||
if (!Array.isArray(parsedYAML[PATHS_PROPERTY])) {
|
||||
throw new Error(getPathsInvalid(configFile));
|
||||
}
|
||||
for (const includePath of parsedYAML[PATHS_PROPERTY]!) {
|
||||
|
|
@ -940,11 +1000,18 @@ async function loadConfig(
|
|||
}
|
||||
}
|
||||
|
||||
const packs = parsePacks(
|
||||
parsedYAML[PACKS_PROPERTY] ?? {},
|
||||
languages,
|
||||
configFile
|
||||
);
|
||||
|
||||
return {
|
||||
languages,
|
||||
queries,
|
||||
pathsIgnore,
|
||||
paths,
|
||||
packs,
|
||||
originalUserInput: parsedYAML,
|
||||
tempDir,
|
||||
toolCacheDir,
|
||||
|
|
@ -954,6 +1021,78 @@ async function loadConfig(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack names must be in the form of `scope/name`, with only alpha-numeric characters,
|
||||
* and `-` allowed as long as not the first or last char.
|
||||
**/
|
||||
const PACK_IDENTIFIER_PATTERN = (function () {
|
||||
const alphaNumeric = "[a-z0-9]";
|
||||
const alphaNumericDash = "[a-z0-9-]";
|
||||
const component = `${alphaNumeric}(${alphaNumericDash}*${alphaNumeric})?`;
|
||||
return new RegExp(`^${component}/${component}$`);
|
||||
})();
|
||||
|
||||
// Exported for testing
|
||||
export function parsePacks(
|
||||
packsByLanguage: string[] | Record<string, string[]>,
|
||||
languages: Language[],
|
||||
configFile: string
|
||||
): Packs {
|
||||
const packs = {};
|
||||
|
||||
if (Array.isArray(packsByLanguage)) {
|
||||
if (languages.length === 1) {
|
||||
// single language analysis, so language is implicit
|
||||
packsByLanguage = {
|
||||
[languages[0]]: packsByLanguage,
|
||||
};
|
||||
} else {
|
||||
// this is an error since multi-language analysis requires
|
||||
// packs split by language
|
||||
throw new Error(getPacksInvalidSplit(configFile));
|
||||
}
|
||||
}
|
||||
|
||||
for (const [lang, packsArr] of Object.entries(packsByLanguage)) {
|
||||
if (!Array.isArray(packsArr)) {
|
||||
throw new Error(getPacksInvalid(configFile));
|
||||
}
|
||||
if (!languages.includes(lang as Language)) {
|
||||
throw new Error(getPacksRequireLanguage(lang, configFile));
|
||||
}
|
||||
packs[lang] = [];
|
||||
for (const packStr of packsArr) {
|
||||
packs[lang].push(toPackWithVersion(packStr, configFile));
|
||||
}
|
||||
}
|
||||
return packs;
|
||||
}
|
||||
|
||||
function toPackWithVersion(packStr, configFile: string): PackWithVersion {
|
||||
if (typeof packStr !== "string") {
|
||||
throw new Error(getPacksStrInvalid(packStr, configFile));
|
||||
}
|
||||
|
||||
const nameWithVersion = packStr.split("@");
|
||||
let version: string | undefined;
|
||||
if (
|
||||
nameWithVersion.length > 2 ||
|
||||
!PACK_IDENTIFIER_PATTERN.test(nameWithVersion[0])
|
||||
) {
|
||||
throw new Error(getPacksStrInvalid(packStr, configFile));
|
||||
} else if (nameWithVersion.length === 2) {
|
||||
version = semver.clean(nameWithVersion[1]) || undefined;
|
||||
if (!version) {
|
||||
throw new Error(getPacksStrInvalid(packStr, configFile));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
packName: nameWithVersion[0],
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
function dbLocationOrDefault(
|
||||
dbLocation: string | undefined,
|
||||
tempDir: string
|
||||
|
|
@ -1019,11 +1158,10 @@ export async function initConfig(
|
|||
// The list of queries should not be empty for any language. If it is then
|
||||
// it is a user configuration error.
|
||||
for (const language of config.languages) {
|
||||
if (
|
||||
config.queries[language] === undefined ||
|
||||
(config.queries[language].builtin.length === 0 &&
|
||||
config.queries[language].custom.length === 0)
|
||||
) {
|
||||
const hasBuiltinQueries = config.queries[language]?.builtin.length > 0;
|
||||
const hasCustomQueries = config.queries[language]?.custom.length > 0;
|
||||
const hasPacks = (config.packs[language]?.length || 0) > 0;
|
||||
if (!hasPacks && !hasBuiltinQueries && !hasCustomQueries) {
|
||||
throw new Error(
|
||||
`Did not detect any queries to run for ${language}. ` +
|
||||
"Please make sure that the default queries are enabled, or you are specifying queries to run."
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ test("ensure lines of code works for cpp and js", async (t) => {
|
|||
|
||||
t.deepEqual(results, {
|
||||
cpp: 6,
|
||||
javascript: 3,
|
||||
javascript: 9,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ test("ensure lines of code can handle undefined language", async (t) => {
|
|||
);
|
||||
|
||||
t.deepEqual(results, {
|
||||
javascript: 3,
|
||||
javascript: 9,
|
||||
python: 5,
|
||||
});
|
||||
});
|
||||
|
|
@ -93,6 +93,6 @@ test("ensure lines of code can handle exclude", async (t) => {
|
|||
);
|
||||
|
||||
t.deepEqual(results, {
|
||||
javascript: 3,
|
||||
javascript: 9,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ function getTestConfig(tmpDir: string): configUtils.Config {
|
|||
codeQLCmd: "",
|
||||
gitHubVersion: { type: util.GitHubVariant.DOTCOM } as util.GitHubVersion,
|
||||
dbLocation: path.resolve(tmpDir, "codeql_databases"),
|
||||
packs: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue