* 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>
257 lines
7.8 KiB
TypeScript
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
|
|
>;
|