Files
reverse-engineering-anything/src/application/binaryDiagnostics/ByteEntropyService.ts
T
morluto d69752fd52 refactor(filesystem): share stable read state comparisons
Reuse sameRegularFileState for artifact, native plist and entropy readback.
Remove private copies of identity/size/timestamp checks and consistently include
mode in the existing stable-state contract.

Keep initial identity-only artifact admission, byte-count checks, capacity and
error precedence, symlink choices, cancellation and descriptor cleanup intact.

Validation: stable-file, plist, artifact outcome, entropy and public CLI/MCP
lifecycle regressions passed; static and documentation checks passed.
2026-10-24 04:18:07 +08:00

388 lines
15 KiB
TypeScript

import { createHash } from "node:crypto";
import { constants } from "node:fs";
import { open, stat } from "node:fs/promises";
import { isAbsolute } from "node:path";
import type { ExecutionOptions } from "../AnalysisProvider.js";
import { BYTE_ENTROPY_PROVIDER } from "../../domain/investigationProviders.js";
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisOutputError,
AnalysisAccessDeniedError,
AnalysisArtifactChangedError,
AnalysisCancelledError,
AnalysisInputError,
AnalysisResourceConstraintError,
} from "../../domain/analysisErrorCore.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import {
byteEntropySchema,
computeBlockEntropy,
entropyFromCounts,
inspectByteEntropyInputSchema,
mergeEntropyRuns,
type ByteEntropy,
} from "../../domain/native/byteEntropy.js";
import { err, type Result } from "../../domain/result.js";
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import {
withArtifactFileHandle,
bindArtifactFileCleanup,
ArtifactFileCleanupFailure,
artifactFileCompletion,
assertArtifactReadActive,
artifactFileCancellationCause,
artifactSelectedPathErrno,
} from "../../artifacts/ArtifactFileOperation.js";
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { sameRegularFileState } from "../../filesystem/RegularFile.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
/** Bound retained result rows before constructing the Evidence representation. */
export const MAX_BYTE_ENTROPY_BLOCKS = 32_768;
/** Generated files such as Linux /proc entries report size 0 but still hold bytes. */
const ZERO_SIZE_LIMITATION =
"The file reports a size of 0, so no bytes were read. Generated files such as Linux /proc entries can report 0 while holding content; this result does not show that the file is empty.";
const OPERATION = "inspect_byte_entropy";
const invalid = (field: string, message: string) =>
new AnalysisInputError(OPERATION, undefined, [
{ path: [field], reason: "invalid_value", message },
]);
/** Offline byte observations shared by CLI and MCP, with one file-wide read. */
export class ByteEntropyService {
readonly #resources: ArtifactResourceScope;
constructor(
private readonly files: Pick<
typeof import("node:fs/promises"),
"open" | "stat"
> = { open, stat },
resources: ArtifactResourceScope = new ArtifactResourceScope(),
) {
this.#resources = resources;
}
async inspect(
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
try {
return await this.#resources.run(() => this.#inspect(rawInput, options));
} catch (cause: unknown) {
const failure = new AnalysisOutputError(
OPERATION,
"A retained byte-entropy file descriptor could not be released before inspection.",
{
cause,
...(cause instanceof ArtifactReaderFailure &&
cause.cleanup !== undefined
? { cleanup: cause.cleanup }
: {}),
},
);
bindArtifactFileCleanup(cause, failure);
return err(failure);
}
}
close(): Promise<void> {
return this.#resources.close();
}
async #inspect(
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted)
return err(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
const parsed = inspectByteEntropyInputSchema.safeParse(rawInput);
if (!parsed.success)
return err(
analysisInputErrorFromIssues(OPERATION, parsed.error.issues, rawInput, {
cause: parsed.error,
}),
);
const input = parsed.data;
if (!isAbsolute(input.path))
return err(
invalid("path", "Expected an absolute filesystem path on this host."),
);
const evidenceFromReport = (report: ByteEntropy): Evidence =>
createEvidence(
{ path: input.path, format: "file", sha256: report.artifact.sha256 },
BYTE_ENTROPY_PROVIDER,
{
operation: OPERATION,
parameters: {
path: input.path,
block_size: input.block_size,
offset: input.offset,
...(input.length === undefined ? {} : { length: input.length }),
},
result: report,
rawResult: null,
confidence: "observed",
limitations: report.limitations,
locations: [{ kind: "artifact-path", path: input.path }],
},
);
// Cleanup errors erase the callback's generic type. Validate their completed
// value before publishing it as this operation's observation.
const completedObservation = (value: unknown): Evidence | undefined => {
const report = byteEntropySchema.safeParse(value);
return report.success && report.data.artifact.path === input.path
? evidenceFromReport(report.data)
: undefined;
};
let admitted = false;
const readFailure = (cause: unknown): AnalysisError => {
if (cause instanceof AnalysisError) return cause;
if (cause instanceof Error && "code" in cause) {
if (cause.code === "EACCES" || cause.code === "EPERM")
return new AnalysisAccessDeniedError(
OPERATION,
input.path,
cause.code,
{ cause },
);
const pathErrno = artifactSelectedPathErrno(cause);
if (pathErrno !== undefined)
return admitted
? new AnalysisArtifactChangedError(
OPERATION,
input.path,
"Selected file path changed during entropy inspection.",
{ cause },
)
: new AnalysisInputError(OPERATION, { cause }, [
{
path: ["path"],
reason: "invalid_value",
message:
pathErrno === "ENOENT" || pathErrno === "ENOTDIR"
? "Selected file could not be found."
: pathErrno === "ELOOP"
? `Selected file path could not be resolved because symbolic-link traversal looped or exceeded the host limit (ELOOP): ${input.path}`
: `Selected file path exceeds the host path or component length limit (ENAMETOOLONG): ${input.path}`,
},
]);
}
return new AnalysisOutputError(
OPERATION,
cause instanceof ArtifactReaderFailure
? cause.message
: "Could not read complete byte entropy evidence.",
{ cause },
);
};
try {
const before = await this.files.stat(input.path, { bigint: true });
if (!before.isFile()) throw invalid("path", "Expected a regular file.");
admitted = true;
const file = await this.files.open(
input.path,
constants.O_RDONLY | constants.O_NONBLOCK,
);
const report = await withArtifactFileHandle(
input.path,
file,
this.#resources,
options?.signal,
async (file) => {
const initial = await file.stat({ bigint: true });
const changed = () =>
new AnalysisArtifactChangedError(
OPERATION,
input.path,
"File identity, size or timestamps changed during entropy inspection.",
);
if (!initial.isFile() || !sameRegularFileState(initial, before))
throw changed();
const size = Number(initial.size);
if (!Number.isSafeInteger(size))
throw invalid(
"path",
"File size exceeds exact numeric offset representation.",
);
if (input.offset > size)
throw invalid(
"offset",
"Selected byte window starts beyond the end of the file.",
);
const length = input.length ?? size - input.offset;
if (length > size - input.offset)
throw invalid(
"length",
"Selected byte window extends beyond the end of the file.",
);
const selectedBlocks = Math.ceil(length / input.block_size);
if (selectedBlocks > MAX_BYTE_ENTROPY_BLOCKS)
throw new AnalysisResourceConstraintError(
OPERATION,
"capacity",
`Selected window would produce ${String(selectedBlocks)} blocks; the limit is ${String(MAX_BYTE_ENTROPY_BLOCKS)}. Choose a shorter window or a larger block size.`,
{
max_blocks: MAX_BYTE_ENTROPY_BLOCKS,
requested_blocks: selectedBlocks,
block_size: input.block_size,
window_length: length,
},
{
retryable: false,
remediationAction:
"Reduce the selected window length or increase block_size so the selected window contains at most 32768 blocks.",
},
);
const end = input.offset + length;
const blocks: ByteEntropy["blocks"] = [];
const buffer = Buffer.alloc(input.block_size);
const counts = new Float64Array(256);
const hash = createHash("sha256");
let position = 0;
while (position < size) {
assertArtifactReadActive(options?.signal);
const selected = position >= input.offset && position < end;
const boundary =
position < input.offset ? input.offset : selected ? end : size;
const wanted = Math.min(input.block_size, boundary - position);
let filled = 0;
while (filled < wanted) {
assertArtifactReadActive(options?.signal);
const { bytesRead } = await file.read(
buffer,
filled,
wanted - filled,
position + filled,
);
if (bytesRead === 0) throw changed();
filled += bytesRead;
}
const bytes = buffer.subarray(0, filled);
hash.update(bytes);
for (const byte of bytes) counts[byte] = (counts[byte] ?? 0) + 1;
if (selected)
blocks.push({
offset: position,
length: filled,
entropy: computeBlockEntropy(bytes),
});
position += filled;
}
if (
!sameRegularFileState(initial, await file.stat({ bigint: true })) ||
!sameRegularFileState(
initial,
await this.files.stat(input.path, { bigint: true }),
)
)
throw changed();
assertArtifactReadActive(options?.signal);
const thresholds = { high: 7.2, low: 1.0 };
return {
artifact: {
path: input.path,
bytes: size,
sha256: hash.digest("hex"),
},
block_size: input.block_size,
window: { offset: input.offset, length },
file_entropy: entropyFromCounts(counts, size),
thresholds,
blocks,
runs: mergeEntropyRuns(blocks, thresholds),
limitations: [
"Entropy does not prove encryption; compressed and encrypted bytes cannot be distinguished by this measure.",
"Entropy is rounded to four decimal places; thresholds classify those reported values. Small blocks can underestimate entropy.",
"The file-wide hash and entropy cover the entire file; blocks and runs cover only the selected window. Empty input has entropy 0 by convention.",
...(size === 0 ? [ZERO_SIZE_LIMITATION] : []),
],
};
},
);
return completeProviderEvidence(
evidenceFromReport(report),
options?.signal,
);
} catch (cause) {
const completion = artifactFileCompletion(cause);
if (cause instanceof ArtifactFileCleanupFailure) {
const outcome = cause.fileCleanupFailure.outcome;
const cleanup = new AnalysisOutputError(
OPERATION,
"Could not confirm closing the selected file.",
{
cause,
cleanup: cause.cleanup,
...(completion === undefined
? {}
: { partialObservation: completedObservation(completion.value) }),
},
);
bindArtifactFileCleanup(cause, cleanup);
if (
outcome.kind === "failed" &&
outcome.cause instanceof ArtifactReaderFailure &&
outcome.cause.reason === "cancelled"
) {
const cancelled = new AnalysisCancelledError(OPERATION, {
cause: artifactFileCancellationCause(outcome.cause),
cleanup: cause.cleanup,
}).retainSecondaryFailure(cleanup);
bindArtifactFileCleanup(cause, cancelled);
return err(cancelled);
}
const cancellation = completion?.cancellation;
if (cancellation !== undefined) {
const cancelled = new AnalysisCancelledError(OPERATION, {
cause: cancellation.reason,
partialObservation: cleanup.partialObservation,
cleanup: cause.cleanup,
}).retainSecondaryFailure(cleanup);
bindArtifactFileCleanup(cause, cancelled);
return err(cancelled);
}
if (outcome.kind === "failed") {
const primary = readFailure(outcome.cause);
return err(
analysisErrorWithCleanupFailure(
primary,
cleanup,
OPERATION,
).bindCleanupDependencies([primary, cleanup]),
);
}
return err(cleanup);
}
if (cause instanceof ArtifactReaderFailure) {
if (cause.reason === "cancelled") {
const cancelled = new AnalysisCancelledError(OPERATION, {
cause: artifactFileCancellationCause(cause),
...(completion === undefined
? {}
: { partialObservation: completedObservation(completion.value) }),
});
bindArtifactFileCleanup(cause, cancelled);
return err(cancelled);
}
const retry = new AnalysisOutputError(
OPERATION,
"A previous byte-entropy file descriptor is still owned; cleanup must succeed before another file can be inspected.",
{
cause,
...(cause.cleanup === undefined ? {} : { cleanup: cause.cleanup }),
},
);
bindArtifactFileCleanup(cause, retry);
return err(retry);
}
return err(readFailure(cause));
}
}
}