* feat(artifacts): make ASAR header admission configurable ASAR header/index admission was a fixed 16 MiB with no override, so operators with large Electron indexes could only extract the archive. REA_ASAR_MAX_HEADER_BYTES now sets it (default 16777216, maximum 268435456). One owner resolves the effective admission for the classification probe, inventory, ASAR reader and Electron file scope, including the Electron header cache budget. Startup and doctor validate the setting; admission refuses an invalid value instead of falling back. Recovery and error details name the effective value and its source. Fixes #6176 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(artifacts): resolve ASAR header admission from the caller's environment The admission read process.env, so composition roots that select their own environment (createDirectAnalysis, createServer, CLI, optional observation factories) could not raise or lower it, and an ambient value leaked into callers that left it unset. Nothing reads process.env now. ArtifactProvider and extraction resolve the admission from their environment; JavaScript analysis parses it from the environment its server or CLI caller passes and refuses an invalid value; the Electron and V8 providers receive it from their factories. Inventory, nested ASAR readers, classification probes and Electron page authorization carry that one value. A header cached by Electron under a larger admission is refused for a smaller one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
240 lines
8.4 KiB
TypeScript
240 lines
8.4 KiB
TypeScript
import {
|
|
ASAR_HEADER_ADMISSION_SETTING,
|
|
parseAsarHeaderAdmission,
|
|
} from "../../config/asarHeaderAdmission.js";
|
|
import { z } from "zod";
|
|
|
|
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
|
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
|
import {
|
|
analyzeJavaScriptApplicationInputSchema,
|
|
type JavaScriptApplicationAnalysisResult,
|
|
parseOwnedJavaScriptApplicationAnalysisSteps,
|
|
} from "../../domain/javascript/javascriptApplicationAnalysis.js";
|
|
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
|
|
import { ArtifactOperationError } from "../../domain/artifactOperationError.js";
|
|
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
|
import type { Evidence } from "../../domain/evidence.js";
|
|
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
|
|
import { err, ok, type Result } from "../../domain/result.js";
|
|
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
|
|
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
|
import { createOwnedJavaScriptApplicationEvidenceCooperatively } from "./JavaScriptApplicationEvidence.js";
|
|
import {
|
|
assertJavaScriptAnalysisActive,
|
|
checkpointJavaScriptAnalysis,
|
|
completeJavaScriptAnalysisSteps,
|
|
} from "./JavaScriptAnalysisControl.js";
|
|
import {
|
|
reconstructJavaScriptArtifact,
|
|
type JavaScriptArtifactReconstructionResult,
|
|
} from "./JavaScriptArtifactReconstruction.js";
|
|
import { JAVASCRIPT_APPLICATION_PROVIDER } from "../InvestigationProviders.js";
|
|
|
|
const OPERATION = "analyze_javascript_application" as const;
|
|
|
|
/** Execution options plus the composition root's environment for admission settings. */
|
|
export interface JavaScriptApplicationOptions extends ExecutionOptions {
|
|
readonly environment?: Readonly<Record<string, string | undefined>>;
|
|
}
|
|
|
|
/** Statically analyze one local JavaScript/Electron application. */
|
|
export const analyzeJavaScriptApplication = async (
|
|
rawInput: unknown,
|
|
scope: ArtifactResourceScope,
|
|
options: JavaScriptApplicationOptions = {},
|
|
): Promise<Result<Evidence, AnalysisError>> => {
|
|
const parsed = analyzeJavaScriptApplicationInputSchema.safeParse(rawInput);
|
|
if (!parsed.success)
|
|
return err(
|
|
analysisInputErrorFromIssues(OPERATION, parsed.error.issues, rawInput),
|
|
);
|
|
return analyzeJavaScriptApplicationValidated(parsed.data, scope, options);
|
|
};
|
|
|
|
/** Analyze input already parsed by a trusted adapter boundary. */
|
|
export const analyzeJavaScriptApplicationValidated = async (
|
|
input: z.output<typeof analyzeJavaScriptApplicationInputSchema>,
|
|
scope: ArtifactResourceScope,
|
|
options: JavaScriptApplicationOptions = {},
|
|
): Promise<Result<Evidence, AnalysisError>> => {
|
|
// An invalid setting is refused before analysis, never replaced by the default.
|
|
const asarHeaderAdmission = parseAsarHeaderAdmission(
|
|
options.environment?.[ASAR_HEADER_ADMISSION_SETTING],
|
|
);
|
|
if (!asarHeaderAdmission.ok) return asarHeaderAdmission;
|
|
let reconstructed: JavaScriptArtifactReconstructionResult | undefined;
|
|
let result: JavaScriptApplicationAnalysisResult | undefined;
|
|
let evidence: Evidence | undefined;
|
|
try {
|
|
assertJavaScriptAnalysisActive(options.signal);
|
|
await options.progress?.report({
|
|
phase: "analyze_javascript_application",
|
|
completed: 0,
|
|
total: 1,
|
|
message: "Inventorying and parsing application artifacts",
|
|
});
|
|
await checkpointJavaScriptAnalysis(options.signal);
|
|
reconstructed = await reconstructJavaScriptArtifact(
|
|
input,
|
|
scope,
|
|
options.signal,
|
|
options.progress,
|
|
asarHeaderAdmission.value,
|
|
);
|
|
const { electron_summary: summary, ...application } = reconstructed;
|
|
await options.progress?.report({
|
|
phase: "validate_javascript_application_result",
|
|
completed: 0,
|
|
total: 1,
|
|
message: "Validating result metadata and cross-graph bindings",
|
|
});
|
|
await checkpointJavaScriptAnalysis(options.signal);
|
|
result = await completeJavaScriptAnalysisSteps(
|
|
parseOwnedJavaScriptApplicationAnalysisSteps({
|
|
...application,
|
|
summary,
|
|
limitations: reconstructed.graph.limitations,
|
|
}),
|
|
options.signal,
|
|
);
|
|
await options.progress?.report({
|
|
phase: "create_javascript_application_evidence",
|
|
completed: 0,
|
|
total: 1,
|
|
message: "Creating and hashing application analysis Evidence",
|
|
});
|
|
await checkpointJavaScriptAnalysis(options.signal);
|
|
evidence = await createOwnedJavaScriptApplicationEvidenceCooperatively(
|
|
input,
|
|
result,
|
|
options.signal,
|
|
options.progress,
|
|
);
|
|
await checkpointJavaScriptAnalysis(options.signal);
|
|
await options.progress?.report({
|
|
phase: "analyze_javascript_application",
|
|
completed: 1,
|
|
total: 1,
|
|
message: "Application graph and Electron boundaries reconstructed",
|
|
terminal: true,
|
|
});
|
|
assertJavaScriptAnalysisActive(options.signal);
|
|
return ok(evidence);
|
|
} catch (cause: unknown) {
|
|
if (
|
|
cause instanceof ArtifactReaderFailure &&
|
|
cause.reason === "cancelled" &&
|
|
reconstructed !== undefined
|
|
) {
|
|
try {
|
|
if (result === undefined) {
|
|
const { electron_summary: summary, ...application } = reconstructed;
|
|
result = await completeJavaScriptAnalysisSteps(
|
|
parseOwnedJavaScriptApplicationAnalysisSteps({
|
|
...application,
|
|
summary,
|
|
limitations: reconstructed.graph.limitations,
|
|
}),
|
|
);
|
|
}
|
|
evidence ??=
|
|
await createOwnedJavaScriptApplicationEvidenceCooperatively(
|
|
input,
|
|
result,
|
|
);
|
|
return err(
|
|
artifactFailureToOperationError(cause).retainPartialObservation(
|
|
evidence,
|
|
),
|
|
);
|
|
} catch (completionCause: unknown) {
|
|
// A validation or Evidence failure while finishing the partial result
|
|
// remains the primary failure instead of being hidden by cancellation.
|
|
cause = completionCause;
|
|
}
|
|
}
|
|
// Preserve execution constraints and their completed partial Evidence.
|
|
if (cause instanceof AnalysisError) return err(cause);
|
|
if (cause instanceof ArtifactReaderFailure)
|
|
return err(artifactFailureToOperationError(cause));
|
|
if (cause instanceof z.ZodError)
|
|
return err(
|
|
new AnalysisOutputError(OPERATION, describeResultSchemaFailure(cause), {
|
|
cause,
|
|
}),
|
|
);
|
|
if (
|
|
cause instanceof Error &&
|
|
"code" in cause &&
|
|
typeof cause.code === "string" &&
|
|
FILESYSTEM_ERROR_CODES.has(cause.code)
|
|
)
|
|
return err(new ArtifactOperationError(OPERATION, "io"));
|
|
return err(
|
|
new ProviderAdapterError(JAVASCRIPT_APPLICATION_PROVIDER.id, OPERATION, {
|
|
cause,
|
|
diagnostics: {
|
|
input_path: input.input_path,
|
|
error_name: cause instanceof Error ? cause.name : "UnknownError",
|
|
error_message:
|
|
cause instanceof Error
|
|
? cause.message
|
|
: typeof cause === "string"
|
|
? cause
|
|
: "JavaScript analysis failed with a non-Error value",
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
};
|
|
|
|
const artifactFailureToOperationError = (
|
|
cause: ArtifactReaderFailure,
|
|
): ArtifactOperationError =>
|
|
new ArtifactOperationError(
|
|
OPERATION,
|
|
cause.reason,
|
|
cause.details,
|
|
cause.message,
|
|
{
|
|
cause,
|
|
...(cause.cleanup === undefined ? {} : { cleanup: cause.cleanup }),
|
|
...(cause.partialObservation === undefined
|
|
? {}
|
|
: { partialObservation: cause.partialObservation }),
|
|
...(cause.admission === undefined ? {} : { admission: cause.admission }),
|
|
},
|
|
);
|
|
|
|
const describeResultSchemaFailure = (cause: z.ZodError): string => {
|
|
const issue = cause.issues[0];
|
|
if (issue === undefined) return "Result schema rejected an unknown issue";
|
|
const path = issue.path.flatMap((part) =>
|
|
typeof part === "string" || typeof part === "number" ? [part] : [],
|
|
);
|
|
const pointer =
|
|
path.length === 0
|
|
? "/"
|
|
: `/${path.map((part) => escapePointerPart(String(part))).join("/")}`;
|
|
return `Result schema rejected ${String(cause.issues.length)} ${cause.issues.length === 1 ? "issue" : "issues"} at ${pointer} (${issue.code})`;
|
|
};
|
|
|
|
const escapePointerPart = (part: string): string =>
|
|
part.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
|
|
const FILESYSTEM_ERROR_CODES = new Set([
|
|
"ENOENT",
|
|
"EACCES",
|
|
"EPERM",
|
|
"ENOTDIR",
|
|
"EISDIR",
|
|
"EIO",
|
|
"ELOOP",
|
|
"ENAMETOOLONG",
|
|
"EMFILE",
|
|
"ENFILE",
|
|
"ENOSPC",
|
|
"EROFS",
|
|
]);
|