Add packs and queries from input

This commit adds the packs and queries from the actions input to the
config file used by the CodeQL CLI.

When the `+` is used, the actions input value is combined with the
config value and when it is not used, the input value overrides the
config value.

This commit also adds a bunch of integration tests for this feature.
In order to avoid adding too many new jobs, all of the tests are
run sequentially in a single job (matrixed across relevant operating
systems and OSes).
This commit is contained in:
Andrew Eisenberg 2022-06-19 16:44:24 -07:00
parent 237260b693
commit 6fabde2be8
53 changed files with 2072 additions and 219 deletions

View file

@ -25,7 +25,11 @@ test("emptyPaths", async (t) => {
debugMode: false,
debugArtifactName: util.DEFAULT_DEBUG_ARTIFACT_NAME,
debugDatabaseName: util.DEFAULT_DEBUG_DATABASE_NAME,
injectedMlQueries: false,
augmentationProperties: {
injectedMlQueries: false,
packsInputCombines: false,
queriesInputCombines: false,
},
};
analysisPaths.includeAndExcludeAnalysisPaths(config);
t.is(process.env["LGTM_INDEX_INCLUDE"], undefined);
@ -51,7 +55,11 @@ test("nonEmptyPaths", async (t) => {
debugMode: false,
debugArtifactName: util.DEFAULT_DEBUG_ARTIFACT_NAME,
debugDatabaseName: util.DEFAULT_DEBUG_DATABASE_NAME,
injectedMlQueries: false,
augmentationProperties: {
injectedMlQueries: false,
packsInputCombines: false,
queriesInputCombines: false,
},
};
analysisPaths.includeAndExcludeAnalysisPaths(config);
t.is(process.env["LGTM_INDEX_INCLUDE"], "path1\npath2");
@ -81,7 +89,11 @@ test("exclude temp dir", async (t) => {
debugMode: false,
debugArtifactName: util.DEFAULT_DEBUG_ARTIFACT_NAME,
debugDatabaseName: util.DEFAULT_DEBUG_DATABASE_NAME,
injectedMlQueries: false,
augmentationProperties: {
injectedMlQueries: false,
packsInputCombines: false,
queriesInputCombines: false,
},
};
analysisPaths.includeAndExcludeAnalysisPaths(config);
t.is(process.env["LGTM_INDEX_INCLUDE"], undefined);

View file

@ -113,7 +113,11 @@ test("status report fields and search path setting", async (t) => {
debugMode: false,
debugArtifactName: util.DEFAULT_DEBUG_ARTIFACT_NAME,
debugDatabaseName: util.DEFAULT_DEBUG_DATABASE_NAME,
injectedMlQueries: false,
augmentationProperties: {
injectedMlQueries: false,
packsInputCombines: false,
queriesInputCombines: false,
},
};
fs.mkdirSync(util.getCodeQLDatabasePath(config, language), {
recursive: true,
@ -269,7 +273,11 @@ const stubConfig: Config = {
debugMode: false,
debugArtifactName: util.DEFAULT_DEBUG_ARTIFACT_NAME,
debugDatabaseName: util.DEFAULT_DEBUG_DATABASE_NAME,
injectedMlQueries: false,
augmentationProperties: {
injectedMlQueries: false,
packsInputCombines: false,
queriesInputCombines: false,
},
};
for (const options of [

View file

@ -8,7 +8,6 @@ import * as yaml from "js-yaml";
import * as analysisPaths from "./analysis-paths";
import {
CodeQL,
CODEQL_VERSION_CONFIG_FILES,
CODEQL_VERSION_COUNTS_LINES,
CODEQL_VERSION_NEW_TRACING,
getCodeQL,
@ -246,7 +245,7 @@ export async function runQueries(
try {
if (
hasPackWithCustomQueries &&
!(await util.codeQlVersionAbove(codeql, CODEQL_VERSION_CONFIG_FILES))
!(await util.useCodeScanningConfigInCli(codeql))
) {
logger.info("Performing analysis with custom CodeQL Packs.");
logger.startGroup(`Downloading custom packs for ${language}`);

View file

@ -1,13 +1,16 @@
import * as fs from "fs";
import * as path from "path";
import * as toolrunner from "@actions/exec/lib/toolrunner";
import * as toolcache from "@actions/tool-cache";
import test from "ava";
import test, { ExecutionContext } from "ava";
import del from "del";
import * as yaml from "js-yaml";
import nock from "nock";
import * as sinon from "sinon";
import * as codeql from "./codeql";
import { Config } from "./config-utils";
import { AugmentationProperties, Config } from "./config-utils";
import * as defaults from "./defaults.json";
import { createFeatureFlags, FeatureFlag } from "./feature-flags";
import { Language } from "./languages";
@ -28,8 +31,34 @@ const sampleGHAEApiDetails = {
url: "https://example.githubenterprise.com",
};
let stubConfig: Config;
test.beforeEach(() => {
initializeEnvironment(Mode.actions, "1.2.3");
stubConfig = {
languages: [Language.cpp],
queries: {},
pathsIgnore: [],
paths: [],
originalUserInput: {},
tempDir: "",
toolCacheDir: "",
codeQLCmd: "",
gitHubVersion: {
type: util.GitHubVariant.DOTCOM,
} as util.GitHubVersion,
dbLocation: "",
packs: {},
debugMode: false,
debugArtifactName: util.DEFAULT_DEBUG_ARTIFACT_NAME,
debugDatabaseName: util.DEFAULT_DEBUG_DATABASE_NAME,
augmentationProperties: {
injectedMlQueries: false,
packsInputCombines: false,
queriesInputCombines: false,
},
};
});
test("download codeql bundle cache", async (t) => {
@ -428,26 +457,6 @@ test("databaseInterpretResults() sets --sarif-add-query-help for 2.7.1", async (
);
});
const stubConfig: Config = {
languages: [Language.cpp],
queries: {},
pathsIgnore: [],
paths: [],
originalUserInput: {},
tempDir: "",
toolCacheDir: "",
codeQLCmd: "",
gitHubVersion: {
type: util.GitHubVariant.DOTCOM,
} as util.GitHubVersion,
dbLocation: "",
packs: {},
debugMode: false,
debugArtifactName: util.DEFAULT_DEBUG_ARTIFACT_NAME,
debugDatabaseName: util.DEFAULT_DEBUG_DATABASE_NAME,
injectedMlQueries: false,
};
test("databaseInitCluster() Lua feature flag enabled, but old CLI", async (t) => {
const runnerConstructorStub = stubToolRunnerConstructor();
const codeqlObject = await codeql.getCodeQLForTesting();
@ -540,6 +549,392 @@ test("databaseInitCluster() Lua feature flag disabled, compatible CLI", async (t
);
});
test("databaseInitCluster() without injected codescanning config", async (t) => {
await util.withTmpDir(async (tempDir) => {
const runnerConstructorStub = stubToolRunnerConstructor();
const codeqlObject = await codeql.getCodeQLForTesting();
sinon.stub(codeqlObject, "getVersion").resolves("2.8.1");
const thisStubConfig: Config = {
...stubConfig,
tempDir,
augmentationProperties: {
injectedMlQueries: false,
queriesInputCombines: false,
packsInputCombines: false,
},
};
await codeqlObject.databaseInitCluster(
thisStubConfig,
"",
undefined,
undefined,
createFeatureFlags([])
);
const args = runnerConstructorStub.firstCall.args[1];
// should NOT have used an config file
const configArg = args.find((arg: string) =>
arg.startsWith("--codescanning-config=")
);
t.falsy(configArg, "Should have injected a codescanning config");
});
});
// Test macro for ensuring different variants of injected augmented configurations
const injectedConfigMacro = test.macro({
exec: async (
t: ExecutionContext<unknown>,
augmentationProperties: AugmentationProperties,
configOverride: Partial<Config>,
expectedConfig: any
) => {
const origCODEQL_PASS_CONFIG_TO_CLI = process.env.CODEQL_PASS_CONFIG_TO_CLI;
process.env["CODEQL_PASS_CONFIG_TO_CLI"] = "true";
try {
await util.withTmpDir(async (tempDir) => {
const runnerConstructorStub = stubToolRunnerConstructor();
const codeqlObject = await codeql.getCodeQLForTesting();
sinon
.stub(codeqlObject, "getVersion")
.resolves(codeql.CODEQL_VERSION_CONFIG_FILES);
const thisStubConfig: Config = {
...stubConfig,
...configOverride,
tempDir,
augmentationProperties,
};
await codeqlObject.databaseInitCluster(
thisStubConfig,
"",
undefined,
undefined,
createFeatureFlags([])
);
const args = runnerConstructorStub.firstCall.args[1];
// should have used an config file
const configArg = args.find((arg: string) =>
arg.startsWith("--codescanning-config=")
);
t.truthy(configArg, "Should have injected a codescanning config");
const configFile = configArg.split("=")[1];
const augmentedConfig = yaml.load(fs.readFileSync(configFile, "utf8"));
t.deepEqual(augmentedConfig, expectedConfig);
await del(configFile, { force: true });
});
} finally {
process.env["CODEQL_PASS_CONFIG_TO_CLI"] = origCODEQL_PASS_CONFIG_TO_CLI;
}
},
title: (providedTitle = "") =>
`databaseInitCluster() injected config: ${providedTitle}`,
});
test(
"basic",
injectedConfigMacro,
{
injectedMlQueries: false,
queriesInputCombines: false,
packsInputCombines: false,
},
{},
{}
);
test(
"injected ML queries",
injectedConfigMacro,
{
injectedMlQueries: true,
queriesInputCombines: false,
packsInputCombines: false,
},
{},
{
packs: ["codeql/javascript-experimental-atm-queries@~0.1.0"],
}
);
test(
"injected ML queries with existing packs",
injectedConfigMacro,
{
injectedMlQueries: true,
queriesInputCombines: false,
packsInputCombines: false,
},
{
originalUserInput: {
packs: { javascript: ["codeql/something-else"] },
},
},
{
packs: {
javascript: [
"codeql/something-else",
"codeql/javascript-experimental-atm-queries@~0.1.0",
],
},
}
);
test(
"injected ML queries with existing packs of different language",
injectedConfigMacro,
{
injectedMlQueries: true,
queriesInputCombines: false,
packsInputCombines: false,
},
{
originalUserInput: {
packs: { cpp: ["codeql/something-else"] },
},
},
{
packs: {
cpp: ["codeql/something-else"],
javascript: ["codeql/javascript-experimental-atm-queries@~0.1.0"],
},
}
);
test(
"injected packs from input",
injectedConfigMacro,
{
injectedMlQueries: false,
queriesInputCombines: false,
packsInputCombines: false,
packsInput: ["xxx", "yyy"],
},
{},
{
packs: ["xxx", "yyy"],
}
);
test(
"injected packs from input with existing packs combines",
injectedConfigMacro,
{
injectedMlQueries: false,
queriesInputCombines: false,
packsInputCombines: true,
packsInput: ["xxx", "yyy"],
},
{
originalUserInput: {
packs: {
cpp: ["codeql/something-else"],
},
},
},
{
packs: {
cpp: ["codeql/something-else", "xxx", "yyy"],
},
}
);
test(
"injected packs from input with existing packs overrides",
injectedConfigMacro,
{
injectedMlQueries: false,
queriesInputCombines: false,
packsInputCombines: false,
packsInput: ["xxx", "yyy"],
},
{
originalUserInput: {
packs: {
cpp: ["codeql/something-else"],
},
},
},
{
packs: ["xxx", "yyy"],
}
);
test(
"injected packs from input with existing packs overrides and ML model inject",
injectedConfigMacro,
{
injectedMlQueries: true,
queriesInputCombines: false,
packsInputCombines: false,
packsInput: ["xxx", "yyy"],
},
{
originalUserInput: {
packs: {
cpp: ["codeql/something-else"],
},
},
},
{
packs: ["xxx", "yyy", "codeql/javascript-experimental-atm-queries@~0.1.0"],
}
);
// similar, but with queries
test(
"injected queries from input",
injectedConfigMacro,
{
injectedMlQueries: false,
queriesInputCombines: false,
packsInputCombines: false,
queriesInput: [{ uses: "xxx" }, { uses: "yyy" }],
},
{},
{
queries: [
{
uses: "xxx",
},
{
uses: "yyy",
},
],
}
);
test(
"injected queries from input overrides",
injectedConfigMacro,
{
injectedMlQueries: false,
queriesInputCombines: false,
packsInputCombines: false,
queriesInput: [{ uses: "xxx" }, { uses: "yyy" }],
},
{
originalUserInput: {
queries: [{ uses: "zzz" }],
},
},
{
queries: [
{
uses: "xxx",
},
{
uses: "yyy",
},
],
}
);
test(
"injected queries from input combines",
injectedConfigMacro,
{
injectedMlQueries: false,
queriesInputCombines: true,
packsInputCombines: false,
queriesInput: [{ uses: "xxx" }, { uses: "yyy" }],
},
{
originalUserInput: {
queries: [{ uses: "zzz" }],
},
},
{
queries: [
{
uses: "zzz",
},
{
uses: "xxx",
},
{
uses: "yyy",
},
],
}
);
test(
"injected queries from input combines 2",
injectedConfigMacro,
{
injectedMlQueries: false,
queriesInputCombines: true,
packsInputCombines: true,
queriesInput: [{ uses: "xxx" }, { uses: "yyy" }],
},
{},
{
queries: [
{
uses: "xxx",
},
{
uses: "yyy",
},
],
}
);
test(
"injected queries and packs, but empty",
injectedConfigMacro,
{
injectedMlQueries: false,
queriesInputCombines: true,
packsInputCombines: true,
queriesInput: [],
packsInput: [],
},
{
originalUserInput: {
packs: [],
queries: [],
},
},
{}
);
test("does not use injected confg", async (t: ExecutionContext<unknown>) => {
const origCODEQL_PASS_CONFIG_TO_CLI = process.env.CODEQL_PASS_CONFIG_TO_CLI;
process.env["CODEQL_PASS_CONFIG_TO_CLI"] = "false";
try {
const runnerConstructorStub = stubToolRunnerConstructor();
const codeqlObject = await codeql.getCodeQLForTesting();
sinon
.stub(codeqlObject, "getVersion")
.resolves(codeql.CODEQL_VERSION_CONFIG_FILES);
await codeqlObject.databaseInitCluster(
stubConfig,
"",
undefined,
undefined,
createFeatureFlags([])
);
const args = runnerConstructorStub.firstCall.args[1];
// should have used an config file
const configArg = args.find((arg: string) =>
arg.startsWith("--codescanning-config=")
);
t.falsy(configArg, "Should NOT have injected a codescanning config");
} finally {
process.env["CODEQL_PASS_CONFIG_TO_CLI"] = origCODEQL_PASS_CONFIG_TO_CLI;
}
});
export function stubToolRunnerConstructor(): sinon.SinonStub<
any[],
toolrunner.ToolRunner

View file

@ -763,26 +763,12 @@ async function getCodeQLForCmd(
}
}
}
if (await util.codeQlVersionAbove(codeql, CODEQL_VERSION_CONFIG_FILES)) {
const configLocation = path.resolve(config.tempDir, "user-config.yaml");
const augmentedConfig = config.originalUserInput;
if (config.injectedMlQueries) {
// We need to inject the ML queries into the original user input before
// we pass this on to the CLI, to make sure these get run.
const packString = await util.getMlPoweredJsQueriesPack(codeql);
if (augmentedConfig.packs === undefined) augmentedConfig.packs = [];
if (Array.isArray(augmentedConfig.packs)) {
augmentedConfig.packs.push(packString);
} else {
if (!augmentedConfig.packs.javascript)
augmentedConfig.packs["javascript"] = [];
augmentedConfig.packs["javascript"].push(packString);
}
}
fs.writeFileSync(configLocation, yaml.dump(augmentedConfig));
const configLocation = await generateCodescanningConfig(codeql, config);
if (configLocation) {
extraArgs.push(`--codescanning-config=${configLocation}`);
}
await runTool(cmd, [
"database",
"init",
@ -955,7 +941,7 @@ async function getCodeQLForCmd(
if (extraSearchPath !== undefined) {
codeqlArgs.push("--additional-packs", extraSearchPath);
}
if (!(await util.codeQlVersionAbove(this, CODEQL_VERSION_CONFIG_FILES))) {
if (!(await util.useCodeScanningConfigInCli(this))) {
codeqlArgs.push(querySuitePath);
}
await runTool(cmd, codeqlArgs);
@ -993,7 +979,7 @@ async function getCodeQLForCmd(
codeqlArgs.push("--sarif-category", automationDetailsId);
}
codeqlArgs.push(databasePath);
if (!(await util.codeQlVersionAbove(this, CODEQL_VERSION_CONFIG_FILES))) {
if (!(await util.useCodeScanningConfigInCli(this))) {
codeqlArgs.push(...querySuitePaths);
}
// capture stdout, which contains analysis summaries
@ -1187,3 +1173,77 @@ async function runTool(cmd: string, args: string[] = []) {
throw new CommandInvocationError(cmd, args, exitCode, error);
return output;
}
/**
* If appropriate, generates a code scanning configuration that is to be used for a scan.
* If the configuration is not to be generated, returns undefined.
*
* @param codeql The CodeQL object to use.
* @param config The configuration to use.
* @returns the path to the generated user configuration file.
*/
async function generateCodescanningConfig(codeql: CodeQL, config: Config) {
if (!(await util.useCodeScanningConfigInCli(codeql))) {
return;
}
const configLocation = path.resolve(config.tempDir, "user-config.yaml");
// make a copy so we can modify it
const augmentedConfig = JSON.parse(JSON.stringify(config.originalUserInput));
// Inject the queries from the input
if (config.augmentationProperties.queriesInput) {
if (config.augmentationProperties.queriesInputCombines) {
augmentedConfig.queries = (augmentedConfig.queries || []).concat(
config.augmentationProperties.queriesInput
);
} else {
augmentedConfig.queries = config.augmentationProperties.queriesInput;
}
}
if (augmentedConfig.queries?.length === 0) {
delete augmentedConfig.queries;
}
// Inject the packs from the input
if (config.augmentationProperties.packsInput) {
if (config.augmentationProperties.packsInputCombines) {
// At this point, we already know that this is a single-language analysis
if (Array.isArray(augmentedConfig.packs)) {
augmentedConfig.packs = (augmentedConfig.packs || []).concat(
config.augmentationProperties.packsInput
);
} else if (!augmentedConfig.packs) {
augmentedConfig.packs = config.augmentationProperties.packsInput;
} else {
// At this point, we know there is only one language.
// If there were more than one language, an error would already have been thrown.
const language = Object.keys(augmentedConfig.packs)[0];
augmentedConfig.packs[language] = augmentedConfig.packs[
language
].concat(config.augmentationProperties.packsInput);
}
} else {
augmentedConfig.packs = config.augmentationProperties.packsInput;
}
}
if (Array.isArray(augmentedConfig.packs) && !augmentedConfig.packs.length) {
delete augmentedConfig.packs;
}
if (config.augmentationProperties.injectedMlQueries) {
// We need to inject the ML queries into the original user input before
// we pass this on to the CLI, to make sure these get run.
const packString = await util.getMlPoweredJsQueriesPack(codeql);
if (augmentedConfig.packs === undefined) augmentedConfig.packs = [];
if (Array.isArray(augmentedConfig.packs)) {
augmentedConfig.packs.push(packString);
} else {
if (!augmentedConfig.packs.javascript)
augmentedConfig.packs["javascript"] = [];
augmentedConfig.packs["javascript"].push(packString);
}
}
fs.writeFileSync(configLocation, yaml.dump(augmentedConfig));
return configLocation;
}

View file

@ -173,7 +173,9 @@ test("loading config saves config", async (t) => {
const config2 = await configUtils.getConfig(tmpDir, logger);
t.not(config2, undefined);
if (config2 !== undefined) {
t.deepEqual(config1, config2);
// removes properties assigned to undefined.
const expectedConfig = JSON.parse(JSON.stringify(config1));
t.deepEqual(expectedConfig, config2);
}
});
});
@ -356,7 +358,13 @@ test("load non-empty input", async (t) => {
debugMode: false,
debugArtifactName: "my-artifact",
debugDatabaseName: "my-db",
injectedMlQueries: false,
augmentationProperties: {
injectedMlQueries: false,
packsInputCombines: false,
queriesInputCombines: false,
packsInput: undefined,
queriesInput: undefined,
},
};
const languages = "javascript";
@ -1598,6 +1606,7 @@ function parseInputAndConfigMacro(
configUtils.parsePacks(
packsFromConfig,
packsFromInput,
!!packsFromInput?.trim().startsWith("+"),
languages,
"/a/b",
mockLogger
@ -1619,6 +1628,7 @@ function parseInputAndConfigErrorMacro(
packsFromConfig: string[] | Record<string, string[]>,
packsFromInput: string | undefined,
languages: Language[],
packsFromInputOverride: boolean,
expected: RegExp
) {
t.throws(
@ -1626,6 +1636,7 @@ function parseInputAndConfigErrorMacro(
configUtils.parsePacks(
packsFromConfig,
packsFromInput,
packsFromInputOverride,
languages,
"/a/b",
mockLogger
@ -1704,6 +1715,7 @@ test(
{},
"c/d",
[],
false,
/No languages specified/
);
@ -1713,6 +1725,7 @@ test(
{},
"c/d",
[Language.cpp, Language.csharp],
false,
/multi-language analysis/
);
@ -1722,6 +1735,7 @@ test(
{},
" + ",
[Language.cpp],
true,
/remove the '\+'/
);
@ -1731,6 +1745,7 @@ test(
{},
" xxx",
[Language.cpp],
false,
/"xxx" is not a valid pack/
);
@ -1910,3 +1925,164 @@ test(
"security-and-quality",
"~0.3.0"
);
const calculateAugmentationMacro = test.macro({
exec: async (
t: ExecutionContext,
_title: string,
rawPacksInput: string | undefined,
rawQueriesInput: string | undefined,
languages: Language[],
expectedAugmentationProperties: configUtils.AugmentationProperties
) => {
const actualAugmentationProperties = configUtils.calculateAugmentation(
rawPacksInput,
rawQueriesInput,
languages
);
t.deepEqual(actualAugmentationProperties, expectedAugmentationProperties);
},
title: (_, title) => `Calculate Augmentation: ${title}`,
});
test(
calculateAugmentationMacro,
"All empty",
undefined,
undefined,
[Language.javascript],
{
queriesInputCombines: false,
queriesInput: undefined,
packsInputCombines: false,
packsInput: undefined,
injectedMlQueries: false,
} as configUtils.AugmentationProperties
);
test(
calculateAugmentationMacro,
"With queries",
undefined,
" a, b , c, d",
[Language.javascript],
{
queriesInputCombines: false,
queriesInput: [{ uses: "a" }, { uses: "b" }, { uses: "c" }, { uses: "d" }],
packsInputCombines: false,
packsInput: undefined,
injectedMlQueries: false,
} as configUtils.AugmentationProperties
);
test(
calculateAugmentationMacro,
"With queries combining",
undefined,
" + a, b , c, d ",
[Language.javascript],
{
queriesInputCombines: true,
queriesInput: [{ uses: "a" }, { uses: "b" }, { uses: "c" }, { uses: "d" }],
packsInputCombines: false,
packsInput: undefined,
injectedMlQueries: false,
} as configUtils.AugmentationProperties
);
test(
calculateAugmentationMacro,
"With packs",
" codeql/a , codeql/b , codeql/c , codeql/d ",
undefined,
[Language.javascript],
{
queriesInputCombines: false,
queriesInput: undefined,
packsInputCombines: false,
packsInput: ["codeql/a", "codeql/b", "codeql/c", "codeql/d"],
injectedMlQueries: false,
} as configUtils.AugmentationProperties
);
test(
calculateAugmentationMacro,
"With packs combining",
" + codeql/a, codeql/b, codeql/c, codeql/d",
undefined,
[Language.javascript],
{
queriesInputCombines: false,
queriesInput: undefined,
packsInputCombines: true,
packsInput: ["codeql/a", "codeql/b", "codeql/c", "codeql/d"],
injectedMlQueries: false,
} as configUtils.AugmentationProperties
);
const calculateAugmentationErrorMacro = test.macro({
exec: async (
t: ExecutionContext,
_title: string,
rawPacksInput: string | undefined,
rawQueriesInput: string | undefined,
languages: Language[],
expectedError: RegExp | string
) => {
t.throws(
() =>
configUtils.calculateAugmentation(
rawPacksInput,
rawQueriesInput,
languages
),
{ message: expectedError }
);
},
title: (_, title) => `Calculate Augmentation Error: ${title}`,
});
test(
calculateAugmentationErrorMacro,
"Plus (+) with nothing else (queries)",
undefined,
" + ",
[Language.javascript],
/The workflow property "queries" is invalid/
);
test(
calculateAugmentationErrorMacro,
"Plus (+) with nothing else (packs)",
" + ",
undefined,
[Language.javascript],
/The workflow property "packs" is invalid/
);
test(
calculateAugmentationErrorMacro,
"Packs input with multiple languages",
" + a/b, c/d ",
undefined,
[Language.javascript, Language.java],
/Cannot specify a 'packs' input in a multi-language analysis/
);
test(
calculateAugmentationErrorMacro,
"Packs input with no languages",
" + a/b, c/d ",
undefined,
[],
/No languages specified/
);
test(
calculateAugmentationErrorMacro,
"Invalid packs",
" a-pack-without-a-scope ",
undefined,
[Language.javascript],
/"a-pack-without-a-scope" is not a valid pack/
);

View file

@ -149,6 +149,39 @@ export interface Config {
* Specifies the name of the database in the debugging artifact.
*/
debugDatabaseName: string;
augmentationProperties: AugmentationProperties;
}
/**
* Describes how to augment the user config with inputs from the action.
*
* When running a CodeQL analysis, the user can supply a config file. When
* running a CodeQL analysis from a GitHub action, the user can supply a
* config file _and_ a set of inputs.
*
* The inputs from the action are used to augment the user config before
* passing the user config to the CodeQL CLI invocation.
*/
export interface AugmentationProperties {
/**
* Whether or not the queries input combines with the queries in the config.
*/
queriesInputCombines: boolean;
/**
* The queries input from the `with` block of the action declaration
*/
queriesInput?: Array<{ uses: string }>;
/**
* Whether or not the packs input combines with the packs in the config.
*/
packsInputCombines: boolean;
/**
* The packs input from the `with` block of the action declaration
*/
packsInput?: string[];
/**
* Whether we injected ML queries into this configuration.
*/
@ -880,8 +913,8 @@ function shouldAddConfigFileQueries(queriesInput: string | undefined): boolean {
*/
export async function getDefaultConfig(
languagesInput: string | undefined,
queriesInput: string | undefined,
packsInput: string | undefined,
rawQueriesInput: string | undefined,
rawPacksInput: string | undefined,
dbLocation: string | undefined,
debugMode: boolean,
debugArtifactName: string,
@ -911,21 +944,30 @@ export async function getDefaultConfig(
};
}
await addDefaultQueries(codeQL, languages, queries);
const packs = parsePacksFromInput(packsInput, languages) ?? {};
let injectedMlQueries = false;
if (queriesInput) {
injectedMlQueries = await addQueriesAndPacksFromWorkflow(
codeQL,
queriesInput,
languages,
queries,
packs,
tempDir,
workspacePath,
apiDetails,
featureFlags,
logger
);
const augmentationProperties = calculateAugmentation(
rawPacksInput,
rawQueriesInput,
languages
);
const packs = augmentationProperties.packsInput
? {
[languages[0]]: augmentationProperties.packsInput,
}
: {};
if (rawQueriesInput) {
augmentationProperties.injectedMlQueries =
await addQueriesAndPacksFromWorkflow(
codeQL,
rawQueriesInput,
languages,
queries,
packs,
tempDir,
workspacePath,
apiDetails,
featureFlags,
logger
);
}
return {
@ -943,7 +985,7 @@ export async function getDefaultConfig(
debugMode,
debugArtifactName,
debugDatabaseName,
injectedMlQueries,
augmentationProperties,
};
}
@ -952,8 +994,8 @@ export async function getDefaultConfig(
*/
async function loadConfig(
languagesInput: string | undefined,
queriesInput: string | undefined,
packsInput: string | undefined,
rawQueriesInput: string | undefined,
rawPacksInput: string | undefined,
configFile: string,
dbLocation: string | undefined,
debugMode: boolean,
@ -1018,10 +1060,15 @@ async function loadConfig(
if (!disableDefaultQueries) {
await addDefaultQueries(codeQL, languages, queries);
}
const augmentationProperties = calculateAugmentation(
rawPacksInput,
rawQueriesInput,
languages
);
const packs = parsePacks(
parsedYAML[PACKS_PROPERTY] ?? {},
packsInput,
rawPacksInput,
augmentationProperties.packsInputCombines,
languages,
configFile,
logger
@ -1031,23 +1078,23 @@ async function loadConfig(
// they should take precedence over the queries in the config file
// unless they're prefixed with "+", in which case they supplement those
// in the config file.
let injectedMlQueries = false;
if (queriesInput) {
injectedMlQueries = await addQueriesAndPacksFromWorkflow(
codeQL,
queriesInput,
languages,
queries,
packs,
tempDir,
workspacePath,
apiDetails,
featureFlags,
logger
);
if (rawQueriesInput) {
augmentationProperties.injectedMlQueries =
await addQueriesAndPacksFromWorkflow(
codeQL,
rawQueriesInput,
languages,
queries,
packs,
tempDir,
workspacePath,
apiDetails,
featureFlags,
logger
);
}
if (
shouldAddConfigFileQueries(queriesInput) &&
shouldAddConfigFileQueries(rawQueriesInput) &&
QUERIES_PROPERTY in parsedYAML
) {
const queriesArr = parsedYAML[QUERIES_PROPERTY];
@ -1125,10 +1172,78 @@ async function loadConfig(
debugMode,
debugArtifactName,
debugDatabaseName,
injectedMlQueries,
augmentationProperties,
};
}
/**
* Calculates how the codeql config file needs to be augmented before passing
* it to the CLI. The reason this is necessary is the codeql-action can be called
* with extra inputs from the workflow. These inputs are not part of the config
* and the CLI does not know about these inputs so we need to inject them into
* the config file sent to the CLI.
*
* @param rawPacksInput The packs input from the action configuration.
* @param rawQueriesInput The queries input from the action configuration.
* @param languages The languages that the config file is for. If the packs input
* is non-empty, then there must be exactly one language. Otherwise, an
* error is thrown.
*
* @returns The properties that need to be augmented in the config file.
*
* @throws An error if the packs input is non-empty and the languages input does
* not have exactly one language.
*/
// exported for testing.
export function calculateAugmentation(
rawPacksInput: string | undefined,
rawQueriesInput: string | undefined,
languages: Language[]
): AugmentationProperties {
const packsInputCombines = shouldCombine(rawPacksInput);
const packsInput = parsePacksFromInput(
rawPacksInput,
languages,
packsInputCombines
);
const queriesInputCombines = shouldCombine(rawQueriesInput);
const queriesInput = parseQueriesFromInput(
rawQueriesInput,
queriesInputCombines
);
return {
injectedMlQueries: false, // filled in later
packsInputCombines,
packsInput: packsInput?.[languages[0]],
queriesInput,
queriesInputCombines,
};
}
function parseQueriesFromInput(
rawQueriesInput: string | undefined,
queriesInputCombines: boolean
) {
if (!rawQueriesInput) {
return undefined;
}
const trimmedInput = queriesInputCombines
? rawQueriesInput.trim().slice(1).trim()
: rawQueriesInput?.trim();
if (queriesInputCombines && trimmedInput.length === 0) {
throw new Error(
getConfigFilePropertyError(
undefined,
"queries",
"A '+' was used in the 'queries' input to specify that you wished to add some packs to your CodeQL analysis. However, no packs were specified. Please either remove the '+' or specify some packs."
)
);
}
return trimmedInput.split(",").map((query) => ({ uses: query.trim() }));
}
/**
* 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.
@ -1187,10 +1302,11 @@ export function parsePacksFromConfig(
}
function parsePacksFromInput(
packsInput: string | undefined,
languages: Language[]
rawPacksInput: string | undefined,
languages: Language[],
packsInputCombines: boolean
): Packs | undefined {
if (!packsInput?.trim()) {
if (!rawPacksInput?.trim()) {
return undefined;
}
@ -1202,19 +1318,23 @@ function parsePacksFromInput(
throw new Error("No languages specified. Cannot process the packs input.");
}
packsInput = packsInput.trim();
if (packsInput.startsWith("+")) {
packsInput = packsInput.substring(1).trim();
if (!packsInput) {
rawPacksInput = rawPacksInput.trim();
if (packsInputCombines) {
rawPacksInput = rawPacksInput.trim().substring(1).trim();
if (!rawPacksInput) {
throw new Error(
"A '+' was used in the 'packs' input to specify that you wished to add some packs to your CodeQL analysis. However, no packs were specified. Please either remove the '+' or specify some packs."
getConfigFilePropertyError(
undefined,
"packs",
"A '+' was used in the 'packs' input to specify that you wished to add some packs to your CodeQL analysis. However, no packs were specified. Please either remove the '+' or specify some packs."
)
);
}
}
return {
[languages[0]]: packsInput.split(",").reduce((packs, pack) => {
packs.push(validatePacksSpecification(pack, ""));
[languages[0]]: rawPacksInput.split(",").reduce((packs, pack) => {
packs.push(validatePacksSpecification(pack));
return packs;
}, [] as string[]),
};
@ -1302,12 +1422,12 @@ export function validatePacksSpecification(
// exported for testing
export function parsePacks(
rawPacksFromConfig: string[] | Record<string, string[]>,
rawPacksInput: string | undefined,
rawPacksFromInput: string | undefined,
packsInputCombines: boolean,
languages: Language[],
configFile: string,
logger: Logger
) {
const packsFromInput = parsePacksFromInput(rawPacksInput, languages);
): Packs {
const packsFomConfig = parsePacksFromConfig(
rawPacksFromConfig,
languages,
@ -1315,18 +1435,35 @@ export function parsePacks(
logger
);
const packsFromInput = parsePacksFromInput(
rawPacksFromInput,
languages,
packsInputCombines
);
if (!packsFromInput) {
return packsFomConfig;
}
if (!shouldCombinePacks(rawPacksInput)) {
if (!packsInputCombines) {
if (!packsFromInput) {
throw new Error(getPacksInvalid(configFile));
}
return packsFromInput;
}
return combinePacks(packsFromInput, packsFomConfig);
}
function shouldCombinePacks(packsInput?: string): boolean {
return !!packsInput?.trim().startsWith("+");
/**
* The convention in this action is that an input value that is prefixed with a '+' will
* be combined with the corresponding value in the config file.
*
* Without a '+', an input value will override the corresponding value in the config file.
*
* @param inputValue The input value to process.
* @returns true if the input value should replace the corresponding value in the config file, false if it should be appended.
*/
function shouldCombine(inputValue?: string): boolean {
return !!inputValue?.trim().startsWith("+");
}
function combinePacks(packs1: Packs, packs2: Packs): Packs {

View file

@ -56,7 +56,11 @@ function getTestConfig(tmpDir: string): Config {
debugMode: false,
debugArtifactName: DEFAULT_DEBUG_ARTIFACT_NAME,
debugDatabaseName: DEFAULT_DEBUG_DATABASE_NAME,
injectedMlQueries: false,
augmentationProperties: {
injectedMlQueries: false,
packsInputCombines: false,
queriesInputCombines: false,
},
};
}

View file

@ -32,7 +32,11 @@ function getTestConfig(tmpDir: string): configUtils.Config {
debugMode: false,
debugArtifactName: util.DEFAULT_DEBUG_ARTIFACT_NAME,
debugDatabaseName: util.DEFAULT_DEBUG_DATABASE_NAME,
injectedMlQueries: false,
augmentationProperties: {
injectedMlQueries: false,
packsInputCombines: false,
queriesInputCombines: false,
},
};
}

View file

@ -347,7 +347,11 @@ for (const [packs, expectedStatus] of ML_POWERED_JS_STATUS_TESTS) {
debugMode: false,
debugArtifactName: util.DEFAULT_DEBUG_ARTIFACT_NAME,
debugDatabaseName: util.DEFAULT_DEBUG_DATABASE_NAME,
injectedMlQueries: false,
augmentationProperties: {
injectedMlQueries: false,
packsInputCombines: false,
queriesInputCombines: false,
},
};
t.is(util.getMlPoweredJsQueriesStatus(config), expectedStatus);

View file

@ -10,7 +10,11 @@ import * as semver from "semver";
import * as api from "./api-client";
import { getApiClient, GitHubApiDetails } from "./api-client";
import * as apiCompatibility from "./api-compatibility.json";
import { CodeQL, CODEQL_VERSION_NEW_TRACING } from "./codeql";
import {
CodeQL,
CODEQL_VERSION_CONFIG_FILES,
CODEQL_VERSION_NEW_TRACING,
} from "./codeql";
import { Config } from "./config-utils";
import { Language } from "./languages";
import { Logger } from "./logging";
@ -510,6 +514,13 @@ enum EnvVar {
* own sandwiched workflow mechanism
*/
FEATURE_SANDWICH = "CODEQL_ACTION_FEATURE_SANDWICH",
/**
* If set to the "true" string and the codeql CLI version is greater than
* `CODEQL_VERSION_CONFIG_FILES`, then the codeql-action will pass the
* the codeql-config file to the codeql CLI to be processed there.
*/
CODEQL_PASS_CONFIG_TO_CLI = "CODEQL_PASS_CONFIG_TO_CLI",
}
const exportVar = (mode: Mode, name: string, value: string) => {
@ -760,3 +771,17 @@ export async function checkActionVersion(version: string) {
export function isInTestMode(): boolean {
return process.env["TEST_MODE"] === "true" || false;
}
/**
* @returns true if the action should generate a conde-scanning config file
* that gets passed to the CLI.
*/
export async function useCodeScanningConfigInCli(
codeql: CodeQL
): Promise<boolean> {
return (
(process.env[EnvVar.CODEQL_PASS_CONFIG_TO_CLI] === "true" &&
(await codeQlVersionAbove(codeql, CODEQL_VERSION_CONFIG_FILES))) ||
false
);
}