Files
codeql-action/src/config/db-config.test.ts
2026-10-22 11:42:14 +01:00

713 lines
18 KiB
TypeScript

import test, { ExecutionContext } from "ava";
import { RepositoryProperties } from "../feature-flags/properties";
import { BuiltInLanguage, Language } from "../languages";
import {
checkExpectedLogMessages,
getRecordingLogger,
LoggedMessage,
makeMacro,
RecordingLogger,
setupActionsVars,
} from "../testing-utils";
import { ConfigurationError, prettyPrintPack, withTmpDir } from "../util";
import * as dbConfig from "./db-config";
/**
* Test macro for ensuring the packs block is valid
*/
const parsePacksMacro = makeMacro({
exec: (
t: ExecutionContext<unknown>,
packsInput: string,
languages: Language[],
expected: dbConfig.Packs | undefined,
) =>
t.deepEqual(
dbConfig.parsePacksFromInput(packsInput, languages, false),
expected,
),
title: (providedTitle = "") => `Parse Packs: ${providedTitle}`,
});
/**
* Test macro for testing when the packs block is invalid
*/
const parsePacksErrorMacro = makeMacro({
exec: (
t: ExecutionContext<unknown>,
packsInput: string,
languages: Language[],
expected: RegExp,
) =>
t.throws(() => dbConfig.parsePacksFromInput(packsInput, languages, false), {
message: expected,
}),
title: (providedTitle = "") => `Parse Packs Error: ${providedTitle}`,
});
/**
* Test macro for testing when the packs block is invalid
*/
const invalidPackNameMacro = makeMacro({
exec: (t: ExecutionContext, arg: string) =>
parsePacksErrorMacro.fn(
t,
arg,
[BuiltInLanguage.cpp],
new RegExp(`^"${arg}" is not a valid pack$`),
),
title: (_providedTitle: string | undefined, arg: string | undefined) =>
`Invalid pack string: ${arg}`,
});
parsePacksMacro("no packs", "", [], undefined);
parsePacksMacro("two packs", "a/b,c/d@1.2.3", [BuiltInLanguage.cpp], {
[BuiltInLanguage.cpp]: ["a/b", "c/d@1.2.3"],
});
parsePacksMacro(
"two packs with spaces",
" a/b , c/d@1.2.3 ",
[BuiltInLanguage.cpp],
{
[BuiltInLanguage.cpp]: ["a/b", "c/d@1.2.3"],
},
);
parsePacksErrorMacro(
"two packs with language",
"a/b,c/d@1.2.3",
[BuiltInLanguage.cpp, BuiltInLanguage.java],
new RegExp(
"Cannot specify a 'packs' input in a multi-language analysis. " +
"Use a codeql-config.yml file instead and specify packs by language.",
),
);
parsePacksMacro(
"packs with other valid names",
[
// ranges are ok
"c/d@1.0",
"c/d@~1.0.0",
"c/d@~1.0.0:a/b",
"c/d@~1.0.0+abc:a/b",
"c/d@~1.0.0-abc:a/b",
"c/d:a/b",
// whitespace is removed
" c/d @ ~1.0.0 : b.qls ",
// and it is retained within a path
" c/d @ ~1.0.0 : b/a path with/spaces.qls ",
// this is valid. the path is '@'. It will probably fail when passed to the CLI
"c/d@1.2.3:@",
// this is valid, too. It will fail if it doesn't match a path
// (globbing is not done)
"c/d@1.2.3:+*)_(",
].join(","),
[BuiltInLanguage.cpp],
{
[BuiltInLanguage.cpp]: [
"c/d@1.0",
"c/d@~1.0.0",
"c/d@~1.0.0:a/b",
"c/d@~1.0.0+abc:a/b",
"c/d@~1.0.0-abc:a/b",
"c/d:a/b",
"c/d@~1.0.0:b.qls",
"c/d@~1.0.0:b/a path with/spaces.qls",
"c/d@1.2.3:@",
"c/d@1.2.3:+*)_(",
],
},
);
invalidPackNameMacro.test("c"); // all packs require at least a scope and a name
invalidPackNameMacro.test("c-/d");
invalidPackNameMacro.test("-c/d");
invalidPackNameMacro.test("c/d_d");
invalidPackNameMacro.test("c/d@@");
invalidPackNameMacro.test("c/d@1.0.0:");
invalidPackNameMacro.test("c/d:");
invalidPackNameMacro.test("c/d:/a");
invalidPackNameMacro.test("@1.0.0:a");
invalidPackNameMacro.test("c/d@../a");
invalidPackNameMacro.test("c/d@b/../a");
invalidPackNameMacro.test("c/d:z@1");
/**
* Test macro for pretty printing pack specs
*/
const packSpecPrettyPrintingMacro = makeMacro({
exec: (t: ExecutionContext, packStr: string, packObj: dbConfig.Pack) => {
const parsed = dbConfig.parsePacksSpecification(packStr);
t.deepEqual(parsed, packObj, "parsed pack spec is correct");
const stringified = prettyPrintPack(packObj);
t.deepEqual(
stringified,
packStr.trim(),
"pretty-printed pack spec is correct",
);
t.deepEqual(
dbConfig.validatePackSpecification(packStr),
packStr.trim(),
"pack spec is valid",
);
},
title: (
_providedTitle: string | undefined,
packStr: string,
_packObj: dbConfig.Pack,
) => `Prettyprint pack spec: '${packStr}'`,
});
packSpecPrettyPrintingMacro.test("a/b", {
name: "a/b",
version: undefined,
path: undefined,
});
packSpecPrettyPrintingMacro.test("a/b@~1.2.3", {
name: "a/b",
version: "~1.2.3",
path: undefined,
});
packSpecPrettyPrintingMacro.test("a/b@~1.2.3:abc/def", {
name: "a/b",
version: "~1.2.3",
path: "abc/def",
});
packSpecPrettyPrintingMacro.test("a/b:abc/def", {
name: "a/b",
version: undefined,
path: "abc/def",
});
packSpecPrettyPrintingMacro.test(" a/b:abc/def ", {
name: "a/b",
version: undefined,
path: "abc/def",
});
const calculateAugmentationMacro = makeMacro({
exec: async (
t: ExecutionContext,
rawPacksInput: string | undefined,
rawQueriesInput: string | undefined,
languages: Language[],
repositoryProperties: RepositoryProperties,
expectedAugmentationProperties: dbConfig.AugmentationProperties,
) => {
const actualAugmentationProperties = await dbConfig.calculateAugmentation(
rawPacksInput,
rawQueriesInput,
repositoryProperties,
languages,
);
t.deepEqual(actualAugmentationProperties, expectedAugmentationProperties);
},
title: (title) => `Calculate Augmentation: ${title}`,
});
calculateAugmentationMacro(
"All empty",
undefined,
undefined,
[BuiltInLanguage.javascript],
{},
{
...dbConfig.defaultAugmentationProperties,
},
);
calculateAugmentationMacro(
"With queries",
undefined,
" a, b , c, d",
[BuiltInLanguage.javascript],
{},
{
...dbConfig.defaultAugmentationProperties,
queriesInput: [{ uses: "a" }, { uses: "b" }, { uses: "c" }, { uses: "d" }],
},
);
calculateAugmentationMacro(
"With queries combining",
undefined,
" + a, b , c, d ",
[BuiltInLanguage.javascript],
{},
{
...dbConfig.defaultAugmentationProperties,
queriesInputCombines: true,
queriesInput: [{ uses: "a" }, { uses: "b" }, { uses: "c" }, { uses: "d" }],
},
);
calculateAugmentationMacro(
"With packs",
" codeql/a , codeql/b , codeql/c , codeql/d ",
undefined,
[BuiltInLanguage.javascript],
{},
{
...dbConfig.defaultAugmentationProperties,
packsInput: ["codeql/a", "codeql/b", "codeql/c", "codeql/d"],
},
);
calculateAugmentationMacro(
"With packs combining",
" + codeql/a, codeql/b, codeql/c, codeql/d",
undefined,
[BuiltInLanguage.javascript],
{},
{
...dbConfig.defaultAugmentationProperties,
packsInputCombines: true,
packsInput: ["codeql/a", "codeql/b", "codeql/c", "codeql/d"],
},
);
calculateAugmentationMacro(
"With repo property queries",
undefined,
undefined,
[BuiltInLanguage.javascript],
{
"github-codeql-extra-queries": "a, b, c, d",
},
{
...dbConfig.defaultAugmentationProperties,
repoPropertyQueries: {
combines: false,
input: [{ uses: "a" }, { uses: "b" }, { uses: "c" }, { uses: "d" }],
},
},
);
calculateAugmentationMacro(
"With repo property queries combining",
undefined,
undefined,
[BuiltInLanguage.javascript],
{
"github-codeql-extra-queries": "+ a, b, c, d",
},
{
...dbConfig.defaultAugmentationProperties,
repoPropertyQueries: {
combines: true,
input: [{ uses: "a" }, { uses: "b" }, { uses: "c" }, { uses: "d" }],
},
},
);
const calculateAugmentationErrorMacro = makeMacro({
exec: async (
t: ExecutionContext,
rawPacksInput: string | undefined,
rawQueriesInput: string | undefined,
languages: Language[],
repositoryProperties: RepositoryProperties,
expectedError: RegExp | string,
) => {
await t.throwsAsync(
() =>
dbConfig.calculateAugmentation(
rawPacksInput,
rawQueriesInput,
repositoryProperties,
languages,
),
{ message: expectedError },
);
},
title: (title) => `Calculate Augmentation Error: ${title}`,
});
calculateAugmentationErrorMacro(
"Plus (+) with nothing else (queries)",
undefined,
" + ",
[BuiltInLanguage.javascript],
{},
/The workflow property "queries" is invalid/,
);
calculateAugmentationErrorMacro(
"Plus (+) with nothing else (packs)",
" + ",
undefined,
[BuiltInLanguage.javascript],
{},
/The workflow property "packs" is invalid/,
);
calculateAugmentationErrorMacro(
"Plus (+) with nothing else (repo property queries)",
undefined,
undefined,
[BuiltInLanguage.javascript],
{
"github-codeql-extra-queries": " + ",
},
/The repository property "github-codeql-extra-queries" is invalid/,
);
calculateAugmentationErrorMacro(
"Packs input with multiple languages",
" + a/b, c/d ",
undefined,
[BuiltInLanguage.javascript, BuiltInLanguage.java],
{},
/Cannot specify a 'packs' input in a multi-language analysis/,
);
calculateAugmentationErrorMacro(
"Packs input with no languages",
" + a/b, c/d ",
undefined,
[],
{},
/No languages specified/,
);
calculateAugmentationErrorMacro(
"Invalid packs",
" a-pack-without-a-scope ",
undefined,
[BuiltInLanguage.javascript],
{},
/"a-pack-without-a-scope" is not a valid pack/,
);
test("parseUserConfig - successfully parses valid YAML", (t) => {
const result = dbConfig.parseUserConfig(
new RecordingLogger(),
"test",
`
paths-ignore:
- "some/path"
queries:
- uses: foo
some-unknown-option: true
`,
true,
);
t.truthy(result);
if (t.truthy(result["paths-ignore"])) {
t.is(result["paths-ignore"].length, 1);
t.is(result["paths-ignore"][0], "some/path");
}
if (t.truthy(result["queries"])) {
t.is(result["queries"].length, 1);
t.deepEqual(result["queries"][0], { uses: "foo" });
}
});
test("parseUserConfig - throws a ConfigurationError if the file is not valid YAML", (t) => {
t.throws(
() =>
dbConfig.parseUserConfig(
new RecordingLogger(),
"test",
`
paths-ignore:
- "some/path"
queries:
- foo
`,
true,
),
{
instanceOf: ConfigurationError,
},
);
});
test("parseUserConfig - validation isn't picky about `query-filters`", (t) => {
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
t.notThrows(() =>
dbConfig.parseUserConfig(
logger,
"test",
`
query-filters:
- something
- include: foo
- exclude: bar
`,
true,
),
);
});
test("parseUserConfig - throws a ConfigurationError if validation fails", (t) => {
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
t.throws(
() =>
dbConfig.parseUserConfig(
logger,
"test",
`
paths-ignore:
- "some/path"
queries: true
`,
true,
),
{
instanceOf: ConfigurationError,
message:
'The configuration file "test" is invalid: instance.queries is not of a type(s) array.',
},
);
const expectedMessages = ["instance.queries is not of a type(s) array"];
checkExpectedLogMessages(t, loggedMessages, expectedMessages);
});
test("parseUserConfig - throws no ConfigurationError if validation should fail, but feature is disabled", (t) => {
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
t.notThrows(() =>
dbConfig.parseUserConfig(
logger,
"test",
`
paths-ignore:
- "some/path"
queries: true
`,
false,
),
);
});
test("mergeDefaultSetupAndUserConfigs - combines threat models", async (t) => {
const logger = new RecordingLogger();
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
logger,
{ "threat-models": ["a", "b"] },
{ "threat-models": ["local", "remote"] },
);
const threatModels = result["threat-models"];
if (t.truthy(threatModels)) {
t.deepEqual(threatModels, ["a", "b", "local", "remote"]);
}
});
test("mergeDefaultSetupAndUserConfigs - warns if user-supplied config contains default setup key", async (t) => {
const logger = new RecordingLogger();
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
logger,
{},
{ "default-setup": {} },
);
// User-supplied value is ignored.
t.deepEqual(result, {});
// Warning is logged.
t.true(
logger.hasMessage(
"The 'default-setup' configuration key is not supported in user-supplied configuration files",
),
);
});
test("mergeDefaultSetupAndUserConfigs - keeps default setup key from 'config' input", async (t) => {
const logger = new RecordingLogger();
const expected: dbConfig.DefaultSetupConfig = {
org: { "model-packs": ["some-pack"] },
};
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
logger,
{ "default-setup": expected },
{},
);
// Result matches the input.
t.deepEqual(result["default-setup"], expected);
// No warning is logged.
t.false(
logger.hasMessage(
"The 'default-setup' configuration key is not supported in user-supplied configuration files",
),
);
});
test("mergeDefaultSetupAndUserConfigs - keeps other properties from user-supplied configuration", async (t) => {
const logger = new RecordingLogger();
const configFile: dbConfig.UserConfig = {
"query-filters": [{ exclude: { a: "b" } }],
"paths-ignore": ["path"],
};
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
logger,
{},
configFile,
);
t.deepEqual(result, configFile);
});
test.serial(
"mergeDefaultSetupAndUserConfigs - ignores, but warns about, unknown keys from Default Setup",
async (t) => {
const logger = new RecordingLogger();
const configFile: dbConfig.UserConfig = {
"query-filters": [{ exclude: { a: "b" } }],
"paths-ignore": ["path"],
};
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
logger,
{
"default-setup": {
borg: [],
org: {
unknown: "foo",
"model-packs": [],
},
} as unknown as dbConfig.DefaultSetupConfig,
"paths-ignore": ["other-path"],
},
configFile,
);
t.deepEqual(result, {
...configFile,
"default-setup": { org: { "model-packs": [] } },
});
const expectedUnrecognisedKeys = [
".default-setup.org.unknown",
".default-setup.borg",
".paths-ignore",
].join(", ");
checkExpectedLogMessages(t, logger.messages, [
`Unrecognised keys in Default Setup configuration: ${expectedUnrecognisedKeys}`,
]);
});
},
);
test.serial(
"mergeDefaultSetupAndUserConfigs - warns about invalid keys from Default Setup",
async (t) => {
const logger = new RecordingLogger();
const configFile: dbConfig.UserConfig = {};
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
logger,
{
"default-setup": {
org: {
"model-packs": [123],
},
} as unknown as dbConfig.DefaultSetupConfig,
},
configFile,
);
t.deepEqual(result, {
...configFile,
"default-setup": { org: { "model-packs": [123] } },
});
const expectedInvalidKeys = [".default-setup.org.model-packs[0]"].join(
", ",
);
checkExpectedLogMessages(t, logger.messages, [
`Invalid keys in Default Setup configuration: ${expectedInvalidKeys}`,
]);
});
},
);
/** Parses `contents` as a configuration without validating it. */
function parseUnvalidatedConfig(contents: string): dbConfig.UserConfig {
return dbConfig.parseUserConfig(
new RecordingLogger(),
"test",
contents,
false,
);
}
test("matchesDefaultSetupConfigSchema - returns true for configurations that only use Default Setup properties", (t) => {
for (const contents of [
[
"default-setup:",
" org:",
" model-packs: [ github/immutable-actions-list@0.0.1 ]",
"threat-models: [ ]",
].join("\n"),
"threat-models: [ local ]",
"{}",
]) {
t.true(
dbConfig.matchesDefaultSetupConfigSchema(
parseUnvalidatedConfig(contents),
),
contents,
);
}
});
test("matchesDefaultSetupConfigSchema - returns false for configurations that use other properties", (t) => {
for (const contents of [
"queries: [ { uses: ./queries/show_ifs.ql } ]",
"paths-ignore: [ tests ]",
"default-setup: { org: { model-packs: [], queries: [] } }",
]) {
t.false(
dbConfig.matchesDefaultSetupConfigSchema(
parseUnvalidatedConfig(contents),
),
contents,
);
}
});
test("matchesDefaultSetupConfigSchema - returns false for invalid Default Setup properties", (t) => {
for (const contents of [
"threat-models: local",
"default-setup: { org: { model-packs: [ 1 ] } }",
]) {
t.false(
dbConfig.matchesDefaultSetupConfigSchema(
parseUnvalidatedConfig(contents),
),
contents,
);
}
});
test("matchesDefaultSetupConfigSchema - returns false for configurations that aren't mappings", (t) => {
for (const contents of ["- threat-models", "local", "null"]) {
t.false(
dbConfig.matchesDefaultSetupConfigSchema(
parseUnvalidatedConfig(contents),
),
contents,
);
}
});