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:
N0zoM1z0
2026-07-17 00:32:09 +08:00
committed by N0zoM1z0
parent 24274fe6b6
commit 6b1fd166b0
10 changed files with 323 additions and 14 deletions
@@ -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
+1
View File
@@ -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",
+12
View File
@@ -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);
+3 -1
View File
@@ -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>>;
};
+111
View File
@@ -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 });
}
};
+2
View File
@@ -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;
+2
View File
@@ -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,
+49 -1
View File
@@ -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;
+48
View File
@@ -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({
+81
View File
@@ -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);
});
});