mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
Analyze exact selected sources in first-request order before unrelated static scopes, preserving full artifact identity and byte verification. Retain selection in CLI/MCP Evidence and summaries, with explicit unknowns and partial coverage for unselected sources. Keep default full analysis. Fixes #1848 Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Kaoru0822 <[email protected]>
237 lines
8.2 KiB
TypeScript
237 lines
8.2 KiB
TypeScript
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;
|
|
|
|
/** Statically analyze one local JavaScript/Electron application. */
|
|
export const analyzeJavaScriptApplication = async (
|
|
rawInput: unknown,
|
|
scope: ArtifactResourceScope,
|
|
options: ExecutionOptions = {},
|
|
): 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: ExecutionOptions = {},
|
|
): Promise<Result<Evidence, AnalysisError>> => {
|
|
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_path: input.input_path,
|
|
format: input.format,
|
|
integrity_policy: input.integrity_policy,
|
|
...(input.source_paths === undefined
|
|
? {}
|
|
: { source_paths: input.source_paths }),
|
|
...(input.max_heap_mb === undefined
|
|
? {}
|
|
: { max_heap_mb: input.max_heap_mb }),
|
|
...(input.analysis_timeout_ms === undefined
|
|
? {}
|
|
: { analysis_timeout_ms: input.analysis_timeout_ms }),
|
|
},
|
|
scope,
|
|
options.signal,
|
|
options.progress,
|
|
);
|
|
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 }),
|
|
},
|
|
);
|
|
|
|
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",
|
|
]);
|