171 lines
5.4 KiB
TypeScript
171 lines
5.4 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
|
|
type ScanResult = Readonly<{
|
|
error?: Error;
|
|
status: number | null;
|
|
signal: NodeJS.Signals | null;
|
|
stdout: string;
|
|
stderr: string;
|
|
}>;
|
|
|
|
type SecurityFixtureCheckDependencies = Readonly<{
|
|
createTempDirectory?: () => Promise<string>;
|
|
runScan?: (artifactPath: string, policyPath: string) => ScanResult;
|
|
readArtifact?: (artifactPath: string) => Promise<string>;
|
|
cleanup?: (directory: string) => Promise<void>;
|
|
}>;
|
|
|
|
type Document = Record<string, unknown>;
|
|
|
|
function record(value: unknown, label: string): Document {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
throw new TypeError(`${label} must be an object`);
|
|
}
|
|
return value as Document;
|
|
}
|
|
|
|
function defaultScan(artifactPath: string, policyPath: string): ScanResult {
|
|
const scan = spawnSync(
|
|
"node",
|
|
[
|
|
"scripts/security-scan.ts",
|
|
"--policy",
|
|
policyPath,
|
|
"--artifact",
|
|
artifactPath,
|
|
],
|
|
{ encoding: "utf8" },
|
|
);
|
|
return {
|
|
...(scan.error ? { error: scan.error } : {}),
|
|
status: scan.status,
|
|
signal: scan.signal,
|
|
stdout: scan.stdout ?? "",
|
|
stderr: scan.stderr ?? "",
|
|
};
|
|
}
|
|
|
|
function assertExactFindings(rawArtifact: string): void {
|
|
const sarif = record(JSON.parse(rawArtifact), "security fixture SARIF");
|
|
const runs = Array.isArray(sarif.runs) ? sarif.runs : [];
|
|
const run = record(runs[0], "security fixture SARIF run");
|
|
const results = Array.isArray(run.results) ? run.results : [];
|
|
const actual = results
|
|
.map((rawResult) => {
|
|
const result = record(rawResult, "security fixture result");
|
|
const locations = Array.isArray(result.locations) ? result.locations : [];
|
|
const location = record(locations[0], "security fixture location");
|
|
const physical = record(
|
|
location.physicalLocation,
|
|
"security fixture physical location",
|
|
);
|
|
const artifactLocation = record(
|
|
physical.artifactLocation,
|
|
"security fixture artifact location",
|
|
);
|
|
return `${String(artifactLocation.uri)}:${String(result.ruleId)}`;
|
|
})
|
|
.sort();
|
|
const root = "tests/fixtures/security/secret-detection/forbidden";
|
|
const expected = [
|
|
`${root}/config.json:assigned-secret`,
|
|
`${root}/dist.ts:assigned-secret`,
|
|
`${root}/source.ts:aws-access-key`,
|
|
].sort();
|
|
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
throw new Error(
|
|
`security fixture findings mismatch: expected ${expected.join(", ")}; received ${actual.join(", ")}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function checkSecurityFixtures(
|
|
dependencies: SecurityFixtureCheckDependencies = {},
|
|
): Promise<void> {
|
|
const createTempDirectory =
|
|
dependencies.createTempDirectory ??
|
|
(() => mkdtemp(path.join(tmpdir(), "ca-security-fixture-")));
|
|
const runScan = dependencies.runScan ?? defaultScan;
|
|
const readArtifact = dependencies.readArtifact ?? ((target) => readFile(target, "utf8"));
|
|
const cleanup =
|
|
dependencies.cleanup ??
|
|
((directory) => rm(directory, { recursive: true, force: true }));
|
|
const directory = await createTempDirectory();
|
|
const artifactPath = path.join(directory, "scan-fixture.sarif");
|
|
try {
|
|
const scan = runScan(
|
|
artifactPath,
|
|
"tests/fixtures/security/secret-detection/forbidden-policy.json",
|
|
);
|
|
const expectedDiagnostic = "Security scan found 3 blocking result(s).";
|
|
if (
|
|
scan.error ||
|
|
scan.status !== 1 ||
|
|
scan.signal !== null ||
|
|
scan.stderr !== `${expectedDiagnostic}\n`
|
|
) {
|
|
throw new Error(
|
|
`forbidden security fixture did not fail exactly: ${scan.error?.message ?? scan.stderr}`,
|
|
);
|
|
}
|
|
assertExactFindings(await readArtifact(artifactPath));
|
|
} finally {
|
|
await cleanup(directory);
|
|
}
|
|
}
|
|
|
|
export async function checkNonmatchingSecurityIncludeFixture(
|
|
dependencies: SecurityFixtureCheckDependencies = {},
|
|
): Promise<void> {
|
|
const createTempDirectory =
|
|
dependencies.createTempDirectory ??
|
|
(() => mkdtemp(path.join(tmpdir(), "ca-security-include-fixture-")));
|
|
const runScan = dependencies.runScan ?? defaultScan;
|
|
const readArtifact =
|
|
dependencies.readArtifact ?? ((target) => readFile(target, "utf8"));
|
|
const cleanup =
|
|
dependencies.cleanup ??
|
|
((directory) => rm(directory, { recursive: true, force: true }));
|
|
const directory = await createTempDirectory();
|
|
const artifactPath = path.join(directory, "scan-fixture.sarif");
|
|
try {
|
|
const includedPath =
|
|
"tests/fixtures/security/secret-detection/misspelled";
|
|
const scan = runScan(
|
|
artifactPath,
|
|
"tests/fixtures/security/secret-detection/nonmatching-policy.json",
|
|
);
|
|
if (
|
|
scan.error ||
|
|
scan.status !== 1 ||
|
|
scan.signal !== null ||
|
|
!scan.stderr.includes(
|
|
`secret scan included path matches no inventory file: ${includedPath}`,
|
|
)
|
|
) {
|
|
throw new Error(
|
|
`nonmatching security include fixture did not fail closed: ${scan.error?.message ?? scan.stderr}`,
|
|
);
|
|
}
|
|
try {
|
|
await readArtifact(artifactPath);
|
|
} catch (error) {
|
|
if (
|
|
typeof error === "object" &&
|
|
error !== null &&
|
|
"code" in error &&
|
|
error.code === "ENOENT"
|
|
) {
|
|
return;
|
|
}
|
|
throw error;
|
|
}
|
|
throw new Error("nonmatching security include fixture wrote an artifact");
|
|
} finally {
|
|
await cleanup(directory);
|
|
}
|
|
}
|