mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
feat(ghidra): define the Windows P0 admission boundary
Admit only native x86-64 PE applications when the Ghidra candidate runs on Windows while preserving the existing Linux format matrix. Add precise rejection codes for executable roles and managed targets, publish a machine-readable Windows capability report, and keep missing no-follow, ACL, Unix-socket, and PTY controls as independent observations rather than inferred support.
This commit is contained in:
@@ -148,18 +148,20 @@ Candidate output is sorted by provider ID for determinism. Sort order is not
|
||||
selection priority. Availability and support use stable codes plus a human
|
||||
diagnostic:
|
||||
|
||||
| Code | Meaning |
|
||||
| --------------------------- | -------------------------------------------------------------- |
|
||||
| `not_configured` | Required provider location or configuration is absent |
|
||||
| `executable_missing` | The configured provider entry point is missing or not runnable |
|
||||
| `runtime_missing` | A required provider runtime, such as Java, is unavailable |
|
||||
| `unsupported_host` | The host platform or architecture is unsupported |
|
||||
| `unsupported_version` | The discovered provider or runtime version is unsupported |
|
||||
| `target_kind_unsupported` | The provider does not accept this target kind |
|
||||
| `target_format_unsupported` | The provider does not accept this target format |
|
||||
| `architecture_unsupported` | The selected target architecture is unsupported |
|
||||
| `open_options_invalid` | The selected adapter rejected its open options |
|
||||
| `version_unresolved` | A concrete analysis-engine version could not be committed |
|
||||
| Code | Meaning |
|
||||
| ---------------------------- | --------------------------------------------------------------- |
|
||||
| `not_configured` | Required provider location or configuration is absent |
|
||||
| `executable_missing` | The configured provider entry point is missing or not runnable |
|
||||
| `runtime_missing` | A required provider runtime, such as Java, is unavailable |
|
||||
| `unsupported_host` | The host platform or architecture is unsupported |
|
||||
| `unsupported_version` | The discovered provider or runtime version is unsupported |
|
||||
| `target_kind_unsupported` | The provider does not accept this target kind |
|
||||
| `target_format_unsupported` | The provider does not accept this target format |
|
||||
| `architecture_unsupported` | The selected target architecture is unsupported |
|
||||
| `target_role_unsupported` | The provider rejects this executable image role |
|
||||
| `managed_target_unsupported` | The provider does not admit managed code through this deep path |
|
||||
| `open_options_invalid` | The selected adapter rejected its open options |
|
||||
| `version_unresolved` | A concrete analysis-engine version could not be committed |
|
||||
|
||||
Diagnostics retain useful local paths, observed versions, architectures, and
|
||||
mismatch details. Credentials, authorization headers, license secrets, and
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"build:fixtures": "node scripts/build-conformance-fixtures.mjs",
|
||||
"verify:fixtures": "npm run build:fixtures && node scripts/verify-conformance-fixtures.mjs",
|
||||
"verify:package": "npm run build && node scripts/verify-package.mjs",
|
||||
"probe:windows-capabilities": "npm run build && node scripts/windows-capabilities.mjs",
|
||||
"verify:published": "node scripts/verify-published-package.mjs",
|
||||
"config:print": "node scripts/print-mcp-config.mjs",
|
||||
"docs:generate": "npm run build && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && typedoc && node scripts/generate-error-schema.mjs",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { probeWindowsCapabilities } from "../dist/application/WindowsCapabilities.js";
|
||||
|
||||
const report = await probeWindowsCapabilities();
|
||||
await new Promise((resolveOutput) =>
|
||||
process.stdout.write(`${JSON.stringify(report)}\n`, resolveOutput),
|
||||
);
|
||||
|
||||
// node-pty can retain a Windows helper handle after its probe exits. This
|
||||
// standalone command has no remaining cleanup after its report is flushed.
|
||||
process.exit(0);
|
||||
@@ -175,7 +175,9 @@ export type ProviderTargetSupport =
|
||||
readonly code:
|
||||
| "target_kind_unsupported"
|
||||
| "target_format_unsupported"
|
||||
| "architecture_unsupported";
|
||||
| "architecture_unsupported"
|
||||
| "target_role_unsupported"
|
||||
| "managed_target_unsupported";
|
||||
readonly reason: string;
|
||||
readonly diagnostics: Readonly<Record<string, JsonValue>>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { constants } from "node:fs";
|
||||
import { mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import {
|
||||
probeProcessCaptureCapability,
|
||||
type ProcessCaptureCapability,
|
||||
} from "./ProcessCaptureCapability.js";
|
||||
|
||||
/** One named Windows host observation without inferred authority. */
|
||||
export interface WindowsCapabilityOutcome {
|
||||
readonly available: boolean;
|
||||
readonly reason: string | null;
|
||||
}
|
||||
|
||||
/** Injectable host probes used by CI and deterministic unit tests. */
|
||||
export interface WindowsCapabilityDependencies {
|
||||
readonly platform: NodeJS.Platform;
|
||||
readonly architecture: NodeJS.Architecture;
|
||||
probeSymlinkCreation(): Promise<WindowsCapabilityOutcome>;
|
||||
probeNoFollowOpen(): WindowsCapabilityOutcome;
|
||||
probePrivateAcl(): WindowsCapabilityOutcome;
|
||||
probeUnixDomainSocket(): WindowsCapabilityOutcome;
|
||||
probePty(): Promise<ProcessCaptureCapability>;
|
||||
}
|
||||
|
||||
/** Machine-readable facts for features that need Windows-specific security. */
|
||||
export interface WindowsCapabilityReport {
|
||||
readonly platform: NodeJS.Platform;
|
||||
readonly architecture: NodeJS.Architecture;
|
||||
readonly capabilities: Readonly<{
|
||||
symlink_creation: WindowsCapabilityOutcome;
|
||||
no_follow_open: WindowsCapabilityOutcome;
|
||||
private_acl: WindowsCapabilityOutcome;
|
||||
unix_domain_socket: WindowsCapabilityOutcome;
|
||||
pty: WindowsCapabilityOutcome;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** Probe named prerequisites without converting missing controls into support. */
|
||||
export const probeWindowsCapabilities = async (
|
||||
dependencies: WindowsCapabilityDependencies = systemDependencies(),
|
||||
): Promise<WindowsCapabilityReport> => {
|
||||
const [symlinkCreation, pty] = await Promise.all([
|
||||
dependencies.probeSymlinkCreation(),
|
||||
dependencies.probePty(),
|
||||
]);
|
||||
return {
|
||||
platform: dependencies.platform,
|
||||
architecture: dependencies.architecture,
|
||||
capabilities: {
|
||||
symlink_creation: symlinkCreation,
|
||||
no_follow_open: dependencies.probeNoFollowOpen(),
|
||||
private_acl: dependencies.probePrivateAcl(),
|
||||
unix_domain_socket: dependencies.probeUnixDomainSocket(),
|
||||
pty: {
|
||||
available: pty.available,
|
||||
reason: pty.available ? null : pty.reason,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const systemDependencies = (): WindowsCapabilityDependencies => ({
|
||||
platform: process.platform,
|
||||
architecture: process.arch,
|
||||
probeSymlinkCreation,
|
||||
probeNoFollowOpen: () =>
|
||||
typeof constants.O_NOFOLLOW === "number"
|
||||
? { available: true, reason: null }
|
||||
: {
|
||||
available: false,
|
||||
reason: "O_NOFOLLOW is unavailable in this Node runtime",
|
||||
},
|
||||
probePrivateAcl: () => ({
|
||||
available: false,
|
||||
reason: "Windows ACL enforcement is not implemented by this REA build",
|
||||
}),
|
||||
probeUnixDomainSocket: () =>
|
||||
process.platform === "win32"
|
||||
? {
|
||||
available: false,
|
||||
reason: "Node path-based IPC uses named pipes on Windows",
|
||||
}
|
||||
: { available: true, reason: null },
|
||||
probePty: probeProcessCaptureCapability,
|
||||
});
|
||||
|
||||
const probeSymlinkCreation = async (): Promise<WindowsCapabilityOutcome> => {
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-symlink-probe-"));
|
||||
const target = join(root, "target");
|
||||
const link = join(root, "link");
|
||||
try {
|
||||
await writeFile(target, "probe");
|
||||
await symlink(target, link, "file");
|
||||
return { available: true, reason: null };
|
||||
} catch (cause: unknown) {
|
||||
return {
|
||||
available: false,
|
||||
reason:
|
||||
cause instanceof Error &&
|
||||
"code" in cause &&
|
||||
typeof cause.code === "string"
|
||||
? cause.code
|
||||
: "symlink creation failed",
|
||||
};
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
};
|
||||
@@ -26,6 +26,8 @@ export const PROVIDER_REJECTION_CODES = [
|
||||
"target_kind_unsupported",
|
||||
"target_format_unsupported",
|
||||
"architecture_unsupported",
|
||||
"target_role_unsupported",
|
||||
"managed_target_unsupported",
|
||||
"open_options_invalid",
|
||||
"version_unresolved",
|
||||
] as const;
|
||||
|
||||
@@ -155,6 +155,8 @@ const providerTargetSupport = z.discriminatedUnion("status", [
|
||||
"target_kind_unsupported",
|
||||
"target_format_unsupported",
|
||||
"architecture_unsupported",
|
||||
"target_role_unsupported",
|
||||
"managed_target_unsupported",
|
||||
]),
|
||||
reason: z.string().min(1),
|
||||
diagnostics: providerDiagnostics,
|
||||
|
||||
@@ -237,7 +237,7 @@ export type GhidraProviderClientFactory = (
|
||||
options: GhidraClientOptions,
|
||||
) => Pick<GhidraClient, "start" | "callTool" | "close">;
|
||||
|
||||
/** Linux Ghidra candidate backed by an isolated read-only headless import. */
|
||||
/** Ghidra candidate backed by an isolated read-only headless import. */
|
||||
export class GhidraProvider implements AnalysisProviderCandidate {
|
||||
#installation: GhidraInstallationInspection | undefined;
|
||||
|
||||
@@ -277,10 +277,14 @@ export class GhidraProvider implements AnalysisProviderCandidate {
|
||||
}
|
||||
|
||||
inspectTargetSupport(target: BinaryTarget): ProviderTargetSupport {
|
||||
const hostPlatform = this.installationHost?.platform ?? process.platform;
|
||||
const diagnostics = {
|
||||
host_platform: hostPlatform,
|
||||
target_kind: target.kind,
|
||||
target_format: target.format,
|
||||
architecture: target.architecture ?? null,
|
||||
executable_role: target.executableRole ?? null,
|
||||
managed: target.managed ?? null,
|
||||
};
|
||||
if (target.kind !== "executable")
|
||||
return {
|
||||
@@ -289,6 +293,8 @@ export class GhidraProvider implements AnalysisProviderCandidate {
|
||||
reason: `Ghidra v1 imports executable targets, not ${target.kind} targets.`,
|
||||
diagnostics,
|
||||
};
|
||||
if (hostPlatform === "win32")
|
||||
return inspectWindowsP0TargetSupport(target, diagnostics);
|
||||
if (!SUPPORTED_FORMATS.has(target.format))
|
||||
return {
|
||||
status: "unsupported",
|
||||
@@ -430,6 +436,48 @@ export class GhidraProvider implements AnalysisProviderCandidate {
|
||||
}
|
||||
}
|
||||
|
||||
const inspectWindowsP0TargetSupport = (
|
||||
target: BinaryTarget,
|
||||
diagnostics: Readonly<Record<string, string | boolean | null>>,
|
||||
): ProviderTargetSupport => {
|
||||
if (target.format !== "pe")
|
||||
return {
|
||||
status: "unsupported",
|
||||
code: "target_format_unsupported",
|
||||
reason: "Windows Ghidra P0 accepts PE targets only.",
|
||||
diagnostics,
|
||||
};
|
||||
if (target.architecture !== "x86_64")
|
||||
return {
|
||||
status: "unsupported",
|
||||
code: "architecture_unsupported",
|
||||
reason: "Windows Ghidra P0 accepts x86-64 PE targets only.",
|
||||
diagnostics,
|
||||
};
|
||||
if (target.executableRole !== "application")
|
||||
return {
|
||||
status: "unsupported",
|
||||
code: "target_role_unsupported",
|
||||
reason:
|
||||
"Windows Ghidra P0 accepts PE applications, not DLL or non-executable images.",
|
||||
diagnostics,
|
||||
};
|
||||
if (target.managed !== false)
|
||||
return {
|
||||
status: "unsupported",
|
||||
code: "managed_target_unsupported",
|
||||
reason:
|
||||
"Windows Ghidra P0 accepts native PE applications; managed or unclassified PE targets are unsupported.",
|
||||
diagnostics,
|
||||
};
|
||||
return {
|
||||
status: "supported",
|
||||
code: null,
|
||||
reason: null,
|
||||
diagnostics,
|
||||
};
|
||||
};
|
||||
|
||||
interface GhidraClientCoordinates {
|
||||
readonly analyzeHeadlessPath: string;
|
||||
readonly providerVersion: string;
|
||||
|
||||
@@ -128,6 +128,54 @@ describe("Ghidra provider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("admits only native x86-64 PE applications on Windows P0", () => {
|
||||
const ghidra = provider({ ...installationHost(), platform: "win32" });
|
||||
const nativeApplication: BinaryTarget = {
|
||||
...executableTarget("pe", "x86_64"),
|
||||
executableRole: "application",
|
||||
managed: false,
|
||||
};
|
||||
|
||||
expect(ghidra.inspectTargetSupport(nativeApplication)).toMatchObject({
|
||||
status: "supported",
|
||||
diagnostics: {
|
||||
host_platform: "win32",
|
||||
executable_role: "application",
|
||||
managed: false,
|
||||
},
|
||||
});
|
||||
expect(
|
||||
ghidra.inspectTargetSupport({
|
||||
...nativeApplication,
|
||||
executableRole: "shared-library",
|
||||
}),
|
||||
).toMatchObject({
|
||||
status: "unsupported",
|
||||
code: "target_role_unsupported",
|
||||
});
|
||||
expect(
|
||||
ghidra.inspectTargetSupport({ ...nativeApplication, managed: true }),
|
||||
).toMatchObject({
|
||||
status: "unsupported",
|
||||
code: "managed_target_unsupported",
|
||||
});
|
||||
expect(
|
||||
ghidra.inspectTargetSupport({ ...nativeApplication, format: "elf" }),
|
||||
).toMatchObject({
|
||||
status: "unsupported",
|
||||
code: "target_format_unsupported",
|
||||
});
|
||||
expect(
|
||||
ghidra.inspectTargetSupport({
|
||||
...nativeApplication,
|
||||
architecture: "arm64",
|
||||
}),
|
||||
).toMatchObject({
|
||||
status: "unsupported",
|
||||
code: "architecture_unsupported",
|
||||
});
|
||||
});
|
||||
|
||||
it("commits exact provider, isolation, and resource semantics", async () => {
|
||||
const callTool = vi.fn().mockResolvedValue(
|
||||
ok({
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
probeWindowsCapabilities,
|
||||
type WindowsCapabilityDependencies,
|
||||
} from "../src/application/WindowsCapabilities.js";
|
||||
|
||||
const dependencies = (
|
||||
overrides: Partial<WindowsCapabilityDependencies> = {},
|
||||
): WindowsCapabilityDependencies => ({
|
||||
platform: "win32",
|
||||
architecture: "x64",
|
||||
probeSymlinkCreation: () =>
|
||||
Promise.resolve({ available: false, reason: "EPERM" }),
|
||||
probeNoFollowOpen: () => ({
|
||||
available: false,
|
||||
reason: "O_NOFOLLOW is unavailable in this Node runtime",
|
||||
}),
|
||||
probePrivateAcl: () => ({
|
||||
available: false,
|
||||
reason: "Windows ACL enforcement is not implemented by this REA build",
|
||||
}),
|
||||
probeUnixDomainSocket: () => ({
|
||||
available: false,
|
||||
reason: "Node path-based IPC uses named pipes on Windows",
|
||||
}),
|
||||
probePty: () =>
|
||||
Promise.resolve({
|
||||
available: false,
|
||||
backend: "node-pty",
|
||||
reason: "probe unavailable",
|
||||
}),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("Windows host capability report", () => {
|
||||
it("keeps unavailable security controls explicit", async () => {
|
||||
await expect(probeWindowsCapabilities(dependencies())).resolves.toEqual({
|
||||
platform: "win32",
|
||||
architecture: "x64",
|
||||
capabilities: {
|
||||
symlink_creation: { available: false, reason: "EPERM" },
|
||||
no_follow_open: {
|
||||
available: false,
|
||||
reason: "O_NOFOLLOW is unavailable in this Node runtime",
|
||||
},
|
||||
private_acl: {
|
||||
available: false,
|
||||
reason:
|
||||
"Windows ACL enforcement is not implemented by this REA build",
|
||||
},
|
||||
unix_domain_socket: {
|
||||
available: false,
|
||||
reason: "Node path-based IPC uses named pipes on Windows",
|
||||
},
|
||||
pty: { available: false, reason: "probe unavailable" },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("reports successful probes independently", async () => {
|
||||
const result = await probeWindowsCapabilities(
|
||||
dependencies({
|
||||
probeSymlinkCreation: () =>
|
||||
Promise.resolve({ available: true, reason: null }),
|
||||
probePty: () =>
|
||||
Promise.resolve({ available: true, backend: "node-pty" }),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.capabilities.symlink_creation).toEqual({
|
||||
available: true,
|
||||
reason: null,
|
||||
});
|
||||
expect(result.capabilities.pty).toEqual({
|
||||
available: true,
|
||||
reason: null,
|
||||
});
|
||||
expect(result.capabilities.private_acl.available).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user