Files
rea/src/domain/artifactGraph.ts
T
曾亮andmorluto 4d8d76b253 fix(electron): support packaged app.asar pages and rewritten unpacked companions (#1400)
* fix(electron): resolve pages served from inside app.asar

Packaged Electron applications load their renderer pages from file URLs
inside app.asar. The host filesystem cannot stat those paths, so
list_electron_targets excluded every such page as unsupported_url and
inspect_electron_page could not select it.

Resolve these URLs the way Electron's file protocol does: the first .asar
path component that is a regular file is the archive, and the remainder
must name a regular file in its header. Directories, links, and missing
members stay rejected. Parsed headers are cached by archive identity.

* fix(artifacts): extract and explain rewritten ASAR unpacked companions

Packaging tools commonly sign or strip .asar.unpacked native binaries after
writing the archive header, so their declared hashes no longer match.
analyze_javascript_application already accepts integrity_policy, but
extract_artifact did not, and inspect_artifact and extract_artifact still
told callers to "get a fresh copy", which cannot help for a shipped app.

- Accept integrity_policy on extract_artifact (MCP and CLI). The default
  still fails closed; record-and-continue writes the observed bytes and
  returns the extracted files' integrity_contradictions records.
- Name the unpacked entry and the post-packaging cause in integrity
  failures, and point every operation that accepts integrity_policy at
  record-and-continue.
- Report an active unpacked entry whose companion file is absent as
  unavailable with its logical path instead of a format failure.
- Share one integrity_policy schema across the inventory, extraction, and
  JavaScript application inputs.

* fix(electron): bound ASAR headers before allocation

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-10 23:01:54 +08:00

257 lines
7.8 KiB
TypeScript

import { z } from "zod";
import { prefixedDigestSchema } from "./../domain/digests.js";
const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/u);
const artifactIdSchema = prefixedDigestSchema("art");
const occurrenceIdSchema = prefixedDigestSchema("occ");
const edgeIdSchema = prefixedDigestSchema("edge");
const manifestIdSchema = prefixedDigestSchema("agm");
const extractionIdSchema = prefixedDigestSchema("aex");
const contradictionIdSchema = prefixedDigestSchema("ic");
const boundedRelativePathSchema = z
.string()
.min(1)
.refine(
(path) =>
!path.startsWith("/") &&
!path.includes("\\") &&
path.split("/").every((part) => part !== "" && part !== ".."),
"Expected a normalized relative POSIX path without traversal",
);
/** Closed artifact categories used to traverse application containers. */
const artifactKindSchema = z.enum([
"container",
"executable",
"dynamic-library",
"framework",
"native-addon",
"javascript",
"source-map",
"plist",
"entitlements",
"resource",
"universal-slice",
"package-metadata",
"unknown",
]);
/** Recognized storage or application formats; unknown bytes remain explicit. */
const artifactFormatSchema = z.enum([
"dmg",
"zip",
"pkg",
"asar",
"javascript-bundle",
"source-map",
"mach-o-universal",
"mach-o",
"elf",
"pe",
"dos-mz",
"dos-com",
"hopper",
"analysis-database",
"ipa",
"apk",
"msix",
"appx",
"plist",
"entitlements",
"directory",
"file",
"unknown",
]);
/** Formats established from bytes or a virtual directory tree, independent of path roles. */
const artifactContentFormatSchema = artifactFormatSchema.extract([
"zip",
"mach-o-universal",
"mach-o",
"elf",
"pe",
"dos-mz",
"plist",
"directory",
"file",
"unknown",
]);
/** File byte interval for an artifact derived without writing it to disk. */
const artifactByteRangeSchema = z.object({
offset: z.number().int().min(0),
length: z.number().int().min(0),
});
/** External command provenance, including observable side effects. */
const artifactCommandSchema = z.object({
tool: z.string().min(1),
arguments: z.array(z.string()),
tool_version: z.string().nullable(),
executable_sha256: sha256Schema.nullable(),
exit_code: z.number().int().nullable(),
effects: z.array(z.enum(["read", "write", "mount"])),
});
/** Parsed artifact-producing command provenance. */
export type ArtifactCommand = z.infer<typeof artifactCommandSchema>;
/** One content-addressed application artifact in deterministic manifest order. */
const artifactNodeSchema = z.object({
artifact_id: artifactIdSchema,
format: artifactContentFormatSchema,
sha256: sha256Schema,
size: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
media_type: z.string().min(1).nullable(),
architecture: z.string().min(1).nullable(),
content_state: z.enum(["materialized", "embedded", "virtual"]),
limitations: z.array(z.string()),
});
/** One path or virtual location where an artifact occurs in its parent. */
const artifactOccurrenceSchema = z.object({
occurrence_id: occurrenceIdSchema,
artifact_id: artifactIdSchema.nullable(),
parent_occurrence_id: occurrenceIdSchema.nullable(),
logical_path: boundedRelativePathSchema,
entry_kind: z.enum(["file", "directory", "symlink", "slice"]),
artifact_kind: artifactKindSchema,
artifact_format: artifactFormatSchema,
declared_size: z
.number()
.int()
.min(0)
.max(Number.MAX_SAFE_INTEGER)
.nullable(),
compressed_size: z
.number()
.int()
.min(0)
.max(Number.MAX_SAFE_INTEGER)
.nullable(),
executable: z.boolean(),
encrypted: z.boolean(),
hash_status: z.enum(["verified", "mismatched", "not-hashed", "unavailable"]),
source_location: artifactByteRangeSchema.nullable(),
limitations: z.array(z.string()),
});
/** Observed bytes that contradict a container's declared integrity identity. */
export const integrityContradictionSchema = z.object({
contradiction_id: contradictionIdSchema,
occurrence_id: occurrenceIdSchema,
parent_artifact_id: artifactIdSchema,
logical_path: boundedRelativePathSchema,
declared_sha256: sha256Schema,
observed_sha256: sha256Schema,
entry_kind: z.enum(["file", "slice"]),
unpacked: z.boolean(),
trust: z.literal("observed-untrusted"),
provenance: z.literal("container-integrity-metadata-versus-observed-bytes"),
limitations: z.array(z.string()).min(1),
});
/**
* Parent-child derivation relation between two content-addressed artifacts.
* The child artifact and its logical path are those of the edge's occurrence.
*/
const artifactEdgeSchema = z.object({
edge_id: edgeIdSchema,
ordinal: z.number().int().min(0),
parent_artifact_id: artifactIdSchema,
relation: z.enum([
"contains",
"extracts",
"slice-of",
"embeds",
"loads",
"maps-source",
"derived-from",
]),
occurrence_id: occurrenceIdSchema,
producer: artifactCommandSchema.nullable(),
});
/** Stable cryptographic commitment and counts for a complete artifact graph. */
const artifactGraphManifestSchema = z.object({
manifest_id: manifestIdSchema,
root_artifact_id: artifactIdSchema,
root_sha256: sha256Schema,
root_format: artifactFormatSchema,
graph_sha256: sha256Schema,
node_count: z.number().int().min(1),
occurrence_count: z.number().int().min(1),
edge_count: z.number().int().min(0),
});
/** Complete inventory observation containing the stable artifact graph. */
export const artifactInventoryResultSchema = z.object({
manifest: artifactGraphManifestSchema,
nodes: z.array(artifactNodeSchema),
occurrences: z.array(artifactOccurrenceSchema),
edges: z.array(artifactEdgeSchema),
provenance: z.array(artifactCommandSchema),
integrity_contradictions: z.array(integrityContradictionSchema).default([]),
limitations: z.array(z.string()),
});
/** One safely materialized artifact beneath caller-approved output root. */
const extractedArtifactSchema = z.object({
artifact_id: artifactIdSchema,
relative_path: boundedRelativePathSchema,
sha256: sha256Schema,
bytes_written: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
created: z.boolean(),
});
/** Path-independent commitment to one selected extraction operation. */
const artifactExtractionManifestSchema = z.object({
extraction_id: extractionIdSchema,
source_manifest_id: manifestIdSchema,
selected_occurrence_ids: z.array(occurrenceIdSchema).min(1),
files_sha256: sha256Schema,
output_root_alias: z.literal("$OUTPUT_ROOT"),
});
/** Safe extraction observation with containment and cleanup verification. */
export const artifactExtractionResultSchema = z.object({
manifest: artifactGraphManifestSchema,
extraction_manifest: artifactExtractionManifestSchema,
output_root: z.string().min(1),
artifacts: z.array(extractedArtifactSchema),
containment_verified: z.boolean(),
cleanup: z.object({
attempted: z.boolean(),
verified: z.boolean(),
residual_paths: z.array(boundedRelativePathSchema),
}),
provenance: z.array(artifactCommandSchema),
integrity_contradictions: z.array(integrityContradictionSchema),
limitations: z.array(z.string()),
});
/** Parsed artifact graph node. */
export type ArtifactNode = z.infer<typeof artifactNodeSchema>;
/** Parsed artifact derivation edge. */
export type ArtifactEdge = z.infer<typeof artifactEdgeSchema>;
/** Parsed artifact occurrence. */
export type ArtifactOccurrence = z.infer<typeof artifactOccurrenceSchema>;
export type IntegrityContradiction = z.infer<
typeof integrityContradictionSchema
>;
/** Parsed deterministic artifact graph manifest. */
export type ArtifactGraphManifest = z.infer<typeof artifactGraphManifestSchema>;
/** Parsed paginated inventory result. */
export type ArtifactInventoryResult = z.infer<
typeof artifactInventoryResultSchema
>;
/** Parsed paginated extraction result. */
export type ArtifactExtractionResult = z.infer<
typeof artifactExtractionResultSchema
>;