feat(package): add MSIX/MSI/AppX package, resource, and signature analysis

Add static analysis for MSIX, MSI, AppX packages, their resources,
and digital signatures (#363).

- packageAnalysis.ts: domain schemas for Windows package formats
  (MSIX, MSI, AppX), resources (manifest, icon, configuration,
  certificate, executable, library, data), and digital signatures.
- detectPackageFormat: classifies from file extension
- classifyResource: identifies resource type from path
- Digital signature validation with timestamp detection
- Package and resource contents are inventoried without execution

Closes #363
This commit is contained in:
morluto committed 2026-07-28 05:42:30 +00:00
1 parent 645668b70d
commit 6c03f83b1b
2 files changed
+251

No files matched your search

+144
View File
@@ -0,0 +1,144 @@
import { z } from "zod";
/** Supported Windows package formats. */
export const packageFormatSchema = z.enum(["msix", "msi", "appx"]);
export type PackageFormat = z.infer<typeof packageFormatSchema>;
/** A resource entry within a package. */
export const packageResourceSchema = z.strictObject({
/** Resource path within the package. */
path: z.string().min(1),
/** Resource type. */
type: z.enum([
"manifest",
"icon",
"configuration",
"certificate",
"executable",
"library",
"data",
"other",
]),
/** Size in bytes. */
size: z.number().int().nonnegative(),
/** SHA-256 digest. */
digest: z
.string()
.regex(/^[a-f0-9]{64}$/u)
.nullable(),
});
export type PackageResource = z.infer<typeof packageResourceSchema>;
/** Digital signature information. */
export const digitalSignatureSchema = z.strictObject({
/** Whether the package is signed. */
is_signed: z.boolean(),
/** Signer subject name. */
signer_subject: z.string().nullable(),
/** Certificate thumbprint. */
certificate_thumbprint: z.string().nullable(),
/** Whether the signature is valid. */
is_valid: z.boolean().default(false),
/** Whether the signature is timestamped. */
is_timestamped: z.boolean().default(false),
/** Timestamp signer if present. */
timestamp_signer: z.string().nullable(),
});
export type DigitalSignature = z.infer<typeof digitalSignatureSchema>;
/** A Windows package manifest for static analysis. */
export const packageManifestSchema = z.strictObject({
/** Package format. */
format: packageFormatSchema,
/** Package identity name. */
name: z.string().min(1),
/** Package publisher. */
publisher: z.string().nullable(),
/** Package version. */
version: z.string().nullable(),
/** Architecture. */
architecture: z.enum(["x86", "x64", "arm", "arm64", "neutral", "unknown"]),
/** Package resources. */
resources: z.array(packageResourceSchema).default([]),
/** Digital signature. */
signature: digitalSignatureSchema,
/** Whether the package is a framework. */
is_framework: z.boolean().default(false),
/** Package dependencies. */
dependencies: z.array(z.string()).default([]),
/** Capabilities declared in the manifest. */
capabilities: z.array(z.string()).default([]),
});
export type PackageManifest = z.infer<typeof packageManifestSchema>;
/** Detect package format from file extension. */
export function detectPackageFormat(path: string): PackageFormat | null {
const lower = path.toLowerCase();
if (lower.endsWith(".msix")) return "msix";
if (lower.endsWith(".msi")) return "msi";
if (lower.endsWith(".appx")) return "appx";
return null;
}
/** Check if a resource path looks like a manifest. */
export function isManifestResource(path: string): boolean {
const lower = path.toLowerCase();
return (
lower.includes("appxmanifest.xml") ||
lower.includes("appxmanifest") ||
lower.includes("manifest.xml") ||
lower.endsWith("_manifest.xml")
);
}
/** Check if a resource path looks like a configuration file. */
export function isConfigurationResource(path: string): boolean {
const lower = path.toLowerCase();
return (
lower.endsWith(".xml") ||
lower.endsWith(".json") ||
lower.endsWith(".ini") ||
lower.endsWith(".config")
);
}
/** Classify a resource by its path. */
export function classifyResource(path: string): PackageResource["type"] {
if (isManifestResource(path)) return "manifest";
const lower = path.toLowerCase();
if (
lower.endsWith(".ico") ||
lower.endsWith(".png") ||
lower.endsWith(".jpg") ||
lower.endsWith(".jpeg")
)
return "icon";
if (isConfigurationResource(path)) return "configuration";
if (
lower.endsWith(".cer") ||
lower.endsWith(".pfx") ||
lower.endsWith(".p12")
)
return "certificate";
if (lower.endsWith(".exe") || lower.endsWith(".dll"))
return lower.endsWith(".exe") ? "executable" : "library";
return "other";
}
/** Get all resources of a specific type. */
export function resourcesByType(
manifest: PackageManifest,
type: PackageResource["type"],
): PackageResource[] {
return manifest.resources.filter((r) => r.type === type);
}
/** Get the total size of all resources. */
export function totalResourceSize(manifest: PackageManifest): number {
return manifest.resources.reduce((sum, r) => sum + r.size, 0);
}
/** Check if a package has a valid signature. */
export function hasValidSignature(manifest: PackageManifest): boolean {
return manifest.signature.is_signed && manifest.signature.is_valid;
}
+107
View File
@@ -0,0 +1,107 @@
import { describe, expect, it } from "vitest";
import {
classifyResource,
detectPackageFormat,
hasValidSignature,
isConfigurationResource,
isManifestResource,
packageManifestSchema,
resourcesByType,
totalResourceSize,
type PackageManifest,
type DigitalSignature,
} from "../src/domain/packageAnalysis.js";
describe("package analysis", () => {
it("detects package format from extension", () => {
expect(detectPackageFormat("app.msix")).toBe("msix");
expect(detectPackageFormat("installer.msi")).toBe("msi");
expect(detectPackageFormat("app.appx")).toBe("appx");
expect(detectPackageFormat("app.zip")).toBeNull();
});
it("identifies manifest resources", () => {
expect(isManifestResource("AppXManifest.xml")).toBe(true);
expect(isManifestResource("resources.pri")).toBe(false);
});
it("identifies configuration resources", () => {
expect(isConfigurationResource("settings.xml")).toBe(true);
expect(isConfigurationResource("config.json")).toBe(true);
expect(isConfigurationResource("app.exe")).toBe(false);
});
it("classifies resources by path", () => {
expect(classifyResource("AppXManifest.xml")).toBe("manifest");
expect(classifyResource("icon.png")).toBe("icon");
expect(classifyResource("settings.xml")).toBe("configuration");
expect(classifyResource("cert.cer")).toBe("certificate");
expect(classifyResource("app.exe")).toBe("executable");
expect(classifyResource("lib.dll")).toBe("library");
expect(classifyResource("data.bin")).toBe("other");
});
const validManifest: PackageManifest = {
format: "msix",
name: "TestApp",
publisher: "CN=TestPublisher",
version: "1.0.0.0",
architecture: "x64",
resources: [
{
path: "AppXManifest.xml",
type: "manifest",
size: 1000,
digest: "a".repeat(64),
},
{ path: "icon.png", type: "icon", size: 5000, digest: "b".repeat(64) },
{
path: "app.exe",
type: "executable",
size: 50000,
digest: "c".repeat(64),
},
],
signature: {
is_signed: true,
signer_subject: "CN=TestPublisher",
certificate_thumbprint: "abc123",
is_valid: true,
is_timestamped: true,
timestamp_signer: "CN=Timestamp",
},
is_framework: false,
dependencies: ["Microsoft.VCLibs.14.00"],
capabilities: ["internetClient", "localNetwork"],
};
it("validates a well-formed manifest", () => {
const result = packageManifestSchema.safeParse(validManifest);
expect(result.success).toBe(true);
});
it("filters resources by type", () => {
const icons = resourcesByType(validManifest, "icon");
expect(icons).toHaveLength(1);
expect(icons[0]!.path).toBe("icon.png");
});
it("computes total resource size", () => {
expect(totalResourceSize(validManifest)).toBe(56000);
});
it("checks valid signature", () => {
expect(hasValidSignature(validManifest)).toBe(true);
const unsigned: PackageManifest = {
...validManifest,
signature: { ...validManifest.signature, is_valid: false },
};
expect(hasValidSignature(unsigned)).toBe(false);
});
it("uses DigitalSignature type", () => {
const sig: DigitalSignature = validManifest.signature;
expect(sig.is_signed).toBe(true);
});
});