mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(cli): reject empty artifact command targets before resolution (#8834)
This commit is contained in:
1 parent
6c3c5ebc69
commit
47cedcc93a
3 files changed
+147
-7
No files matched your search
@@ -13,6 +13,7 @@ import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { ARTIFACT_GRAPH_PROVIDER } from "../domain/investigationProviders.js";
|
||||
import { keyedArchiveInputSchema } from "../domain/apple/keyedArchive.js";
|
||||
import { localPathStringSchema } from "../domain/localPath.js";
|
||||
import { z } from "incur";
|
||||
|
||||
import type { DirectAnalysis } from "../composition/directAnalysis.js";
|
||||
@@ -43,7 +44,9 @@ export const registerArtifactCommands = (
|
||||
description:
|
||||
"Inspect a Foundation keyed archive as an object-reference graph",
|
||||
args: z.object({
|
||||
path: z.string().describe("Foundation plist archive or Apple app bundle"),
|
||||
path: localPathStringSchema.describe(
|
||||
"Foundation plist archive or Apple app bundle",
|
||||
),
|
||||
archive: keyedArchiveInputSchema.shape.path.describe(
|
||||
"Relative archive path when the target is a bundle",
|
||||
),
|
||||
@@ -87,7 +90,9 @@ const registerDylibResolutionCommand = (
|
||||
description:
|
||||
"Trace dyld load-path resolution for a Mach-O or every executable in an app bundle",
|
||||
args: z.object({
|
||||
path: z.string().describe("Mach-O file or Apple .app bundle path"),
|
||||
path: localPathStringSchema.describe(
|
||||
"Mach-O file or Apple .app bundle path",
|
||||
),
|
||||
}),
|
||||
options: z.object({
|
||||
root: z
|
||||
@@ -125,7 +130,9 @@ const registerAssetCatalogCommand = (
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.inspectAssetCatalog, {
|
||||
description: "Inspect compiled Apple asset catalog metadata",
|
||||
args: z.object({ path: z.string().describe("Apple .app bundle path") }),
|
||||
args: z.object({
|
||||
path: localPathStringSchema.describe("Apple .app bundle path"),
|
||||
}),
|
||||
options: z.object({
|
||||
offset: z
|
||||
.number()
|
||||
@@ -162,7 +169,7 @@ const registerInterfaceBuilderCommand = (
|
||||
description:
|
||||
"Decode compiled storyboard and nib archives from an app bundle",
|
||||
args: z.object({
|
||||
path: z.string().describe("Apple .app bundle path"),
|
||||
path: localPathStringSchema.describe("Apple .app bundle path"),
|
||||
}),
|
||||
options: z.object({
|
||||
maxDocuments: z
|
||||
@@ -257,7 +264,7 @@ const registerExtractionCommand = (
|
||||
description:
|
||||
"Extract regular contents of supported artifacts; DMG extraction is unavailable",
|
||||
args: z.object({
|
||||
path: z.string().describe("Application or package path"),
|
||||
path: localPathStringSchema.describe("Application or package path"),
|
||||
}),
|
||||
options: z.object({
|
||||
integrityPolicy: z
|
||||
@@ -293,7 +300,7 @@ const registerInspectionCommand = (
|
||||
description:
|
||||
"Inspect one artifact and return all available observations and next probes",
|
||||
args: z.object({
|
||||
path: z.string().min(1).describe("Application or package path"),
|
||||
path: localPathStringSchema.describe("Application or package path"),
|
||||
}),
|
||||
options: z.object({
|
||||
...analysisEvidenceDeliveryOptions,
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { build } from "plist";
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { thinMach } from "../../../src/domain/binaryTarget.fixture.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
const createWorkingBundleFixture = async (prefix: string) => {
|
||||
const root = await createTestTempDirectory(prefix);
|
||||
const bundle = join(root, "Example.app");
|
||||
const temp = join(root, "temp");
|
||||
const contents = join(bundle, "Contents");
|
||||
await mkdir(join(contents, "MacOS"), { recursive: true });
|
||||
await mkdir(join(contents, "Resources"), { recursive: true });
|
||||
await mkdir(temp, { recursive: true });
|
||||
await writeFile(
|
||||
join(contents, "Info.plist"),
|
||||
build({ CFBundleExecutable: "Example" }),
|
||||
);
|
||||
await writeFile(
|
||||
join(contents, "MacOS", "Example"),
|
||||
thinMach(0xcffaedfe, 0x0100000c),
|
||||
);
|
||||
await writeFile(
|
||||
join(contents, "Resources", "Archive.plist"),
|
||||
build({
|
||||
$archiver: "NSKeyedArchiver",
|
||||
$version: 100000,
|
||||
$objects: ["$null", "resource-only-value"],
|
||||
$top: { root: { CF$UID: 1 } },
|
||||
}),
|
||||
);
|
||||
const environment = {
|
||||
HOME: join(root, "home"),
|
||||
USERPROFILE: join(root, "home"),
|
||||
TMPDIR: temp,
|
||||
TMP: temp,
|
||||
TEMP: temp,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
};
|
||||
return { bundle, temp, environment };
|
||||
};
|
||||
|
||||
cliTest(
|
||||
"compiled CLI rejects empty inspect-keyed-archive target in a bundle cwd while preserving explicit dot",
|
||||
async ({ cli }) => {
|
||||
const { bundle, environment } = await createWorkingBundleFixture(
|
||||
"rea-empty-keyed-bundle-",
|
||||
);
|
||||
const archive = "Contents/Resources/Archive.plist";
|
||||
const empty = await cli.run({
|
||||
arguments: ["inspect-keyed-archive", "", archive, "--json"],
|
||||
cwd: bundle,
|
||||
environment,
|
||||
});
|
||||
expect(empty.exitCode).not.toBe(0);
|
||||
expect(empty.json).toMatchObject({
|
||||
code: "VALIDATION_ERROR",
|
||||
fieldErrors: [
|
||||
expect.objectContaining({ path: "path", code: "too_small" }),
|
||||
],
|
||||
});
|
||||
|
||||
const explicitDot = await cli.run({
|
||||
arguments: ["inspect-keyed-archive", ".", archive, "--json"],
|
||||
cwd: bundle,
|
||||
environment,
|
||||
});
|
||||
expect(explicitDot.exitCode, explicitDot.stderr).toBe(0);
|
||||
const evidence = parseEvidence(explicitDot.json);
|
||||
expect(evidence.parameters).toMatchObject({ input_path: "." });
|
||||
expect(evidence.normalized_result).toMatchObject({
|
||||
total_objects: 2,
|
||||
archive_path: archive,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"compiled CLI rejects empty extract-artifact and bundle artifact targets before touching the working directory",
|
||||
async ({ cli }) => {
|
||||
const { bundle, temp, environment } = await createWorkingBundleFixture(
|
||||
"rea-empty-artifact-bundle-",
|
||||
);
|
||||
for (const command of [
|
||||
"extract-artifact",
|
||||
"decode-interface-builder",
|
||||
"inspect-asset-catalog",
|
||||
"trace-dylib-resolution",
|
||||
]) {
|
||||
const empty = await cli.run({
|
||||
arguments: [command, "", "--json"],
|
||||
cwd: bundle,
|
||||
environment,
|
||||
});
|
||||
expect(empty.exitCode, command).not.toBe(0);
|
||||
expect(empty.json).toMatchObject({
|
||||
code: "VALIDATION_ERROR",
|
||||
fieldErrors: [
|
||||
expect.objectContaining({ path: "path", code: "too_small" }),
|
||||
],
|
||||
});
|
||||
}
|
||||
expect(await readdir(temp)).toEqual([]);
|
||||
|
||||
const explicitExtract = await cli.run({
|
||||
arguments: ["extract-artifact", ".", "--json"],
|
||||
cwd: bundle,
|
||||
environment,
|
||||
});
|
||||
expect(explicitExtract.exitCode, explicitExtract.stderr).toBe(0);
|
||||
expect(parseEvidence(explicitExtract.json).parameters).toMatchObject({
|
||||
input_path: ".",
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Cli } from "incur";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { registerArtifactCommands } from "../../../src/cli/artifactCommands.js";
|
||||
import { registerCoreBinaryCommands } from "../../../src/cli/coreBinaryCommands.js";
|
||||
import { registerCoreNativeCommands } from "../../../src/cli/coreNativeCommands.js";
|
||||
import { registerManagedCommands } from "../../../src/cli/managedCommands.js";
|
||||
@@ -16,6 +17,7 @@ const registry = () => {
|
||||
const runDirectAnalysis = run();
|
||||
const runProviderAnalysis = run();
|
||||
const cli = Cli.create("empty-target-admission", { sync: false });
|
||||
registerArtifactCommands(cli, silentLogger, runProviderAnalysis);
|
||||
registerCoreBinaryCommands(cli, silentLogger, runDirectAnalysis, {});
|
||||
registerCoreNativeCommands(cli, silentLogger, runDirectAnalysis);
|
||||
registerManagedCommands(cli, silentLogger, runProviderAnalysis);
|
||||
@@ -44,6 +46,12 @@ const registry = () => {
|
||||
|
||||
// Each command with its other required arguments; the target comes first.
|
||||
const commands: readonly (readonly string[])[] = [
|
||||
["inspect-artifact"],
|
||||
["extract-artifact"],
|
||||
["inspect-keyed-archive"],
|
||||
["trace-dylib-resolution"],
|
||||
["inspect-asset-catalog"],
|
||||
["decode-interface-builder"],
|
||||
["inspect"],
|
||||
["decompile", "main"],
|
||||
["xrefs", "0x1000"],
|
||||
@@ -83,7 +91,7 @@ const commands: readonly (readonly string[])[] = [
|
||||
];
|
||||
|
||||
it.each(commands)(
|
||||
"rejects an empty %s target before resolution and admits explicit .",
|
||||
"rejects an empty or NUL %s target before resolution and admits explicit .",
|
||||
async (command, ...rest) => {
|
||||
const { serve, runs } = registry();
|
||||
// docs/cli.md: an empty string never selects the working directory.
|
||||
@@ -93,6 +101,12 @@ it.each(commands)(
|
||||
expect.objectContaining({ path: "path", code: "too_small" }),
|
||||
],
|
||||
});
|
||||
expect(await serve([command, "target\x00name", ...rest])).toMatchObject({
|
||||
code: "VALIDATION_ERROR",
|
||||
fieldErrors: [
|
||||
expect.objectContaining({ path: "path", code: "invalid_format" }),
|
||||
],
|
||||
});
|
||||
for (const run of runs) expect(run).not.toHaveBeenCalled();
|
||||
await serve([command, ".", ...rest]);
|
||||
expect(runs.flatMap((run) => run.mock.calls.map(([path]) => path))).toEqual(
|
||||
|
||||
Reference in new issue
Block a user