mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 01:41:21 +00:00
feat(ntfs): expose the journal timeline through CLI and MCP
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
1 parent
2e34aa0433
commit
01a7e2c3b7
9 files changed
+251
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { registerPlaywrightTraceCommand } from "./playwrightTraceCommand.js";
|
||||
import { registerApiContractCommand } from "./apiContractCommand.js";
|
||||
import { registerNtfsJournalCommand } from "./ntfsJournalCommand.js";
|
||||
import { Cli, z } from "incur";
|
||||
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
@@ -77,6 +78,7 @@ export const registerApplicationCommands = (
|
||||
registerSourceToBundleCommand(cli, logger);
|
||||
registerStaticObservedEndpointsCommand(cli, logger);
|
||||
registerApiContractCommand(cli, logger);
|
||||
registerNtfsJournalCommand(cli, logger);
|
||||
registerPlaywrightTraceCommand(cli, logger);
|
||||
registerEvidenceJsonCommand({
|
||||
cli,
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { z } from "incur";
|
||||
import type { Logger } from "pino";
|
||||
|
||||
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
import { projectArtifactCloseFailure } from "../artifacts/StableArtifactFailure.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import { localPathStringSchema } from "../domain/localPath.js";
|
||||
import {
|
||||
NTFS_JOURNAL_OPERATION as operation,
|
||||
NTFS_JOURNAL_PROVIDER,
|
||||
ntfsJournalInputSchema,
|
||||
} from "../domain/windows/ntfsJournalQuery.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import { deliverCliAnalysisEvidence } from "./analysisEvidenceDelivery.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { cliDecimalField } from "./options.js";
|
||||
import type { CliInstance } from "./types.js";
|
||||
|
||||
export const registerNtfsJournalCommand = (
|
||||
cli: CliInstance,
|
||||
logger: Logger,
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.inspectNtfsJournal, {
|
||||
description: "Inspect paired offline NTFS MFT and USN journal acquisitions",
|
||||
args: z.object({
|
||||
mftPath: localPathStringSchema.describe(
|
||||
"Acquired NTFS Master File Table stream",
|
||||
),
|
||||
journalPath: localPathStringSchema.describe(
|
||||
"Acquired NTFS change journal data stream",
|
||||
),
|
||||
}),
|
||||
options: z.object({
|
||||
startFileOffset: cliDecimalField(
|
||||
ntfsJournalInputSchema.shape.start_file_offset,
|
||||
),
|
||||
maxRows: cliDecimalField(ntfsJournalInputSchema.shape.max_rows),
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
withCommandCancellation((signal) =>
|
||||
logCliCommand(logger, CLI_COMMANDS.inspectNtfsJournal, async () => {
|
||||
const resources = new ArtifactResourceScope();
|
||||
const { inspectNtfsJournalEvidence } =
|
||||
await import("../application/ntfs/NtfsJournalService.js");
|
||||
return deliverCliAnalysisEvidence({
|
||||
callerInputPath: args.journalPath,
|
||||
callerInputPaths: { mft_path: args.mftPath },
|
||||
run: () =>
|
||||
inspectNtfsJournalEvidence(
|
||||
{
|
||||
mft_path: absoluteCallerPath(args.mftPath),
|
||||
journal_path: absoluteCallerPath(args.journalPath),
|
||||
start_file_offset: options.startFileOffset,
|
||||
max_rows: options.maxRows,
|
||||
},
|
||||
resources,
|
||||
signal,
|
||||
),
|
||||
service: {
|
||||
close: () =>
|
||||
resources.close().catch((cause: unknown) => {
|
||||
throw projectArtifactCloseFailure(
|
||||
cause,
|
||||
NTFS_JOURNAL_PROVIDER.id,
|
||||
operation,
|
||||
);
|
||||
}),
|
||||
},
|
||||
providerId: NTFS_JOURNAL_PROVIDER.id,
|
||||
operation,
|
||||
signal,
|
||||
detail: "complete",
|
||||
evidenceOutput: undefined,
|
||||
});
|
||||
}),
|
||||
),
|
||||
});
|
||||
};
|
||||
@@ -70,6 +70,7 @@ export const CLI_COMMANDS = Object.freeze({
|
||||
inspectIl2cppMetadata: "inspect-il2cpp-metadata",
|
||||
inspectModelArtifact: "inspect-model-artifact",
|
||||
inspectApiContract: "inspect-api-contract",
|
||||
inspectNtfsJournal: "inspect-ntfs-journal",
|
||||
inspectPlaywrightTrace: "inspect-playwright-trace",
|
||||
inspectGpuCodeObjects: "inspect-gpu-code-objects",
|
||||
inspectRustBinary: "inspect-rust-binary",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PLAYWRIGHT_TRACE_TOOL_CONTRACTS } from "./playwrightTraceToolContracts.js";
|
||||
import { API_CONTRACT_TOOL_CONTRACTS } from "./apiContractToolContracts.js";
|
||||
import { NTFS_JOURNAL_TOOL_CONTRACTS } from "./ntfsJournalToolContracts.js";
|
||||
import { applicationInventoryProjectionInputSchema } from "../domain/mobileApplicationGraphProjection.js";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
@@ -95,6 +96,7 @@ const retained = (evidence_id: string) => ({
|
||||
/** Provider-neutral graph workflow contracts shared by MCP and CLI adapters. */
|
||||
export const APPLICATION_TOOL_CONTRACTS = [
|
||||
...API_CONTRACT_TOOL_CONTRACTS,
|
||||
...NTFS_JOURNAL_TOOL_CONTRACTS,
|
||||
...PLAYWRIGHT_TRACE_TOOL_CONTRACTS,
|
||||
{
|
||||
name: "search_guidance",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
NTFS_JOURNAL_OPERATION,
|
||||
ntfsJournalInputSchema,
|
||||
ntfsJournalResultSchema,
|
||||
} from "../domain/windows/ntfsJournalQuery.js";
|
||||
import type { ToolContract } from "./toolContractTypes.js";
|
||||
import { toolContractMetadata } from "./toolEffects.js";
|
||||
import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js";
|
||||
|
||||
export const NTFS_JOURNAL_TOOL_CONTRACTS = [
|
||||
{
|
||||
name: NTFS_JOURNAL_OPERATION,
|
||||
...toolContractMetadata(NTFS_JOURNAL_OPERATION),
|
||||
kind: "application",
|
||||
description:
|
||||
"Correlate USN V2 records from a local acquired NTFS journal with a caller-paired MFT from the same volume. Reads at most 64 MiB per file. Returns source-order events, explicit coverage rows, both SHA-256 identities, and nextFileOffset for another page using the same inputs. Full file references include sequence numbers. Other journal versions and unverifiable references remain explicit. Reads only the selected acquisition files.",
|
||||
inputSchema: ntfsJournalInputSchema,
|
||||
outputSchema: evidenceResultOf(ntfsJournalResultSchema),
|
||||
examples: [
|
||||
{
|
||||
title: "Read a page of a paired NTFS acquisition",
|
||||
input: {
|
||||
mft_path: "/artifacts/$MFT",
|
||||
journal_path: "/artifacts/UsnJrnl-J",
|
||||
max_rows: 50,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly ToolContract[];
|
||||
@@ -159,6 +159,7 @@ const TOOL_EFFECT_AUDIT: Readonly<Record<string, EffectAudit>> = {
|
||||
inspect_il2cpp_metadata: evidence,
|
||||
inspect_model_artifact: evidence,
|
||||
inspect_api_contract: evidence,
|
||||
inspect_ntfs_journal: evidence,
|
||||
inspect_playwright_trace: evidence,
|
||||
inspect_gpu_code_objects: evidence,
|
||||
inspect_rust_binary: evidence,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { registerPlaywrightTraceTool } from "./registerApplicationTools/playwrightTrace.js";
|
||||
import { registerApiContractTool } from "./registerApplicationTools/apiContract.js";
|
||||
import { registerNtfsJournalTool } from "./registerApplicationTools/ntfsJournal.js";
|
||||
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
|
||||
|
||||
import { registerTraceFeatureTool } from "./registerApplicationTools/traceFeature.js";
|
||||
@@ -28,6 +29,7 @@ export const registerApplicationTools = (
|
||||
registerCompareApplicationVersionsTool(server, options);
|
||||
registerCompareEndpointsTool(server, options);
|
||||
registerApiContractTool(server, options);
|
||||
registerNtfsJournalTool(server, options);
|
||||
registerPlaywrightTraceTool(server, options);
|
||||
registerCompareSourceToBundleTool(server, options);
|
||||
registerCompareJavaScriptExportShapesTool(server, options);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { inspectNtfsJournalEvidence } from "../../application/ntfs/NtfsJournalService.js";
|
||||
import { toolContract } from "../../contracts/toolContracts.js";
|
||||
import type { EvidenceMcpServer } from "../EvidenceMcpServer.js";
|
||||
import { runAdmittedToolOperation } from "../admittedToolOperation.js";
|
||||
import { toolRegistrationOptions } from "../toolRegistrationOptions.js";
|
||||
import { logToolExecution } from "../toolLogging.js";
|
||||
import { deliverApplicationEvidence } from "./helpers.js";
|
||||
import type { ApplicationToolRegistration } from "./types.js";
|
||||
|
||||
const contract = toolContract("inspect_ntfs_journal");
|
||||
|
||||
export const registerNtfsJournalTool = (
|
||||
server: EvidenceMcpServer,
|
||||
options: ApplicationToolRegistration,
|
||||
): void => {
|
||||
server.registerTool(
|
||||
contract.name,
|
||||
toolRegistrationOptions(contract),
|
||||
async (input, context) =>
|
||||
runAdmittedToolOperation(
|
||||
server,
|
||||
options.withAdmittedAnalysis,
|
||||
contract.name,
|
||||
context.mcpReq.signal,
|
||||
async () => {
|
||||
const signal = context.mcpReq.signal;
|
||||
const result = await logToolExecution(
|
||||
options.logger,
|
||||
contract.name,
|
||||
() =>
|
||||
inspectNtfsJournalEvidence(
|
||||
input,
|
||||
options.artifactResources,
|
||||
signal,
|
||||
),
|
||||
);
|
||||
if (!result.ok)
|
||||
return server.delivery.toPartialErrorToolResult(
|
||||
result.error,
|
||||
contract.returnsTargetContent,
|
||||
options.recordEvidence,
|
||||
);
|
||||
return deliverApplicationEvidence(
|
||||
server,
|
||||
options,
|
||||
contract,
|
||||
{ evidence: result.value, sources: [] },
|
||||
undefined,
|
||||
signal,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { createBinarySession } from "../../../src/composition/binary.js";
|
||||
import { parseConfig } from "../../../src/config/parseConfig.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { ntfsJournalResultSchema } from "../../../src/domain/windows/ntfsJournalQuery.js";
|
||||
import { silentLogger } from "../../../src/logger.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { mcpTest } from "../../support/mcp/mcpFixture.js";
|
||||
import { serveCli } from "../cli/cliServeHarness.js";
|
||||
|
||||
mcpTest(
|
||||
"CLI and MCP inspect the same paired NTFS acquisition",
|
||||
async ({ mcp, onTestFinished }) => {
|
||||
const directory = await createTestTempDirectory("rea-ntfs-journal-");
|
||||
const mftPath = join(directory, "records");
|
||||
const journalPath = join(directory, "journal");
|
||||
const record = Buffer.alloc(1024);
|
||||
record.write("FILE");
|
||||
record.writeUInt16LE(48, 4);
|
||||
record.writeUInt16LE(3, 6);
|
||||
record.writeUInt16LE(2, 16);
|
||||
record.writeUInt16LE(56, 20);
|
||||
record.writeUInt32LE(64, 24);
|
||||
record.writeUInt32LE(1024, 28);
|
||||
record.writeUInt32LE(0xffff_ffff, 56);
|
||||
const journal = Buffer.alloc(64);
|
||||
journal.writeUInt32LE(64);
|
||||
journal.writeUInt16LE(2, 4);
|
||||
journal.writeBigUInt64LE(0x0002_0000_0000_0000n, 8);
|
||||
journal.writeUInt16LE(60, 58);
|
||||
await writeFile(mftPath, record);
|
||||
await writeFile(journalPath, journal);
|
||||
|
||||
const cli = await serveCli([
|
||||
"inspect-ntfs-journal",
|
||||
mftPath,
|
||||
journalPath,
|
||||
"--json",
|
||||
]);
|
||||
expect(cli.exitCode, cli.stdout).toBe(0);
|
||||
const fromCli = parseEvidence(JSON.parse(cli.stdout));
|
||||
const expected = ntfsJournalResultSchema.parse(fromCli.normalized_result);
|
||||
expect(expected).toMatchObject({
|
||||
mft: { path: mftPath, bytes: 1024 },
|
||||
journal: { path: journalPath, bytes: 64 },
|
||||
rows: [
|
||||
{
|
||||
kind: "event",
|
||||
event: {
|
||||
mft: { file: { kind: "matched", record: { sequenceNumber: 2 } } },
|
||||
},
|
||||
},
|
||||
],
|
||||
nextFileOffset: null,
|
||||
});
|
||||
|
||||
const config = parseConfig({});
|
||||
if (!config.ok) throw config.error;
|
||||
const session = createBinarySession(config.value, silentLogger, {});
|
||||
onTestFinished(async () => {
|
||||
await session.close();
|
||||
});
|
||||
const client = await mcp.connect(
|
||||
createServer({ kind: "session", session }),
|
||||
);
|
||||
const reply = await client.callTool({
|
||||
name: "inspect_ntfs_journal",
|
||||
arguments: { mft_path: mftPath, journal_path: journalPath },
|
||||
});
|
||||
expect(reply.isError, JSON.stringify(reply.content)).not.toBe(true);
|
||||
expect(parseEvidence(reply.structuredContent).normalized_result).toEqual(
|
||||
expected,
|
||||
);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user