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:
N0zoM1z0andgensokyo-dev authored and gensokyo-dev committed 2026-11-11 13:03:32 +08:00
1 parent 2e34aa0433
commit 01a7e2c3b7
9 files changed
+251

No files matched your search

+2
View File
@@ -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,
+79
View File
@@ -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,
});
}),
),
});
};
+1
View File
@@ -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",
+30
View File
@@ -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[];
+1
View File
@@ -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,
+2
View File
@@ -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,
);
},
),
);
};
+80
View File
@@ -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,
);
},
);