fix(javascript): retain selected-root access and filesystem failure facts

This commit is contained in:
morluto committed 2026-10-24 03:59:44 +08:00
1 parent b2cb9b6596
commit 77ed32fabc
5 files changed
+189 -23

No files matched your search

@@ -12,7 +12,10 @@ import {
type JavaScriptApplicationAnalysisResult,
parseOwnedJavaScriptApplicationAnalysisSteps,
} from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
import {
AnalysisAccessDeniedError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import { ArtifactOperationError } from "../../domain/artifactOperationError.js";
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import type { Evidence } from "../../domain/evidence.js";
@@ -170,8 +173,32 @@ export const analyzeJavaScriptApplicationValidated = async (
"code" in cause &&
typeof cause.code === "string" &&
FILESYSTEM_ERROR_CODES.has(cause.code)
)
return err(new ArtifactOperationError(OPERATION, "io"));
) {
const path: unknown = Reflect.get(cause, "path");
const syscall: unknown = Reflect.get(cause, "syscall");
const diagnostics = {
input_path: input.input_path,
system_code: cause.code,
...(typeof path === "string" ? { path } : {}),
...(typeof syscall === "string" ? { syscall } : {}),
};
return err(
cause.code === "EACCES" || cause.code === "EPERM"
? new AnalysisAccessDeniedError(
OPERATION,
input.input_path,
cause.code,
{ cause, diagnostics },
)
: new ArtifactOperationError(
OPERATION,
"io",
undefined,
cause.message,
{ cause, diagnostics },
),
);
}
return err(
new ProviderAdapterError(JAVASCRIPT_APPLICATION_PROVIDER.id, OPERATION, {
cause,
@@ -2,7 +2,7 @@ import {
DEFAULT_ASAR_HEADER_ADMISSION,
type AsarHeaderAdmission,
} from "../../config/asarHeaderAdmission.js";
import { constants } from "node:fs";
import { constants, type Stats } from "node:fs";
import {
buildJavaScriptRouteInventorySteps,
type JavaScriptRouteInventory,
@@ -102,8 +102,8 @@ export const reconstructJavaScriptArtifact = async (
};
const input = javascriptArtifactReconstructionInputSchema.parse(rawInput);
abortIfNeeded(signal);
const path = await resolveSelectedInput(input.input_path);
const format = await resolveFormat(path, input);
const { path, metadata } = await resolveSelectedInput(input.input_path);
const format = resolveFormat(path, metadata, input);
const snapshot = await scanCanonicalArtifactInventoryInScope(path, {
resourceScope: scope,
signal,
@@ -425,11 +425,18 @@ const OPERATION = "analyze_javascript_application";
* unreadable selection is a caller or host-resolution failure, not a damaged
* artifact.
*/
const resolveSelectedInput = async (inputPath: string): Promise<string> => {
const resolveSelectedInput = async (
inputPath: string,
): Promise<{ readonly path: string; readonly metadata: Stats }> => {
try {
const path = await realpath(inputPath);
await access(path, constants.R_OK);
return path;
const metadata = await lstat(path);
// Directory enumeration needs read access; resolving its entries also needs search access.
await access(
path,
metadata.isDirectory() ? constants.R_OK | constants.X_OK : constants.R_OK,
);
return { path, metadata };
} catch (cause: unknown) {
const code =
cause instanceof Error && "code" in cause ? String(cause.code) : "";
@@ -466,11 +473,11 @@ const selectedInputResolutionMessage = (
: `Selected input path could not be resolved because symbolic-link traversal looped or exceeded the host limit (ELOOP): ${inputPath}`;
/** The selected input's kind, checked against the caller's requested format. */
const resolveFormat = async (
const resolveFormat = (
path: string,
metadata: Stats,
input: JavaScriptArtifactReconstructionInput,
): Promise<"asar" | "directory"> => {
const metadata = await lstat(path);
): "asar" | "directory" => {
const observed = metadata.isDirectory()
? "directory"
: metadata.isFile() && path.toLowerCase().endsWith(".asar")