diff --git a/packages/endomoat/package.json b/packages/endomoat/package.json index a7e46737ff..aa6238d148 100644 --- a/packages/endomoat/package.json +++ b/packages/endomoat/package.json @@ -16,6 +16,9 @@ "node": "^16.20.0 || ^18.0.0 || ^20.0.0", "npm": ">=7.0.0" }, + "bin": { + "endomoat": "./src/cli.js" + }, "exports": { ".": { "types": "./types/index.d.ts", @@ -39,6 +42,7 @@ "@endo/compartment-mapper": "1.1.3", "@endo/evasive-transform": "1.0.4", "@types/node": "18.19.26", + "json-stable-stringify": "1.1.1", "lavamoat-core": "^15.0.0", "ses": "1.4.0", "type-fest": "4.10.3", @@ -47,6 +51,7 @@ "devDependencies": { "@endo/eslint-plugin": "2.1.0", "@jessie.js/eslint-plugin": "0.4.0", + "@types/json-stable-stringify": "1.0.36", "memfs": "4.7.7" }, "publishConfig": { diff --git a/packages/endomoat/src/constants.js b/packages/endomoat/src/constants.js index bd03f27541..27f0b04675 100644 --- a/packages/endomoat/src/constants.js +++ b/packages/endomoat/src/constants.js @@ -3,7 +3,6 @@ * * _Type a string more than once? Make it a constant!_ */ - import path from 'node:path' /** @@ -56,3 +55,13 @@ export const RSRC_POLICY_BUILTINS = 'builtins' * Name of the `globals` property of a `LavaMoatPackagePolicy` */ export const RSRC_POLICY_GLOBALS = 'globals' + +/** + * `builtin` module type for a `LavamoatModuleRecord` + */ +export const LMR_TYPE_BUILTIN = 'builtin' + +/** + * `js` module type for a `LavamoatModuleRecord` + */ +export const LMR_TYPE_SOURCE = 'js' diff --git a/packages/endomoat/src/index.js b/packages/endomoat/src/index.js index e00e9dc91a..2985229807 100644 --- a/packages/endomoat/src/index.js +++ b/packages/endomoat/src/index.js @@ -23,6 +23,7 @@ import { toEndoPolicy } from './policy-converter.js' import { defaultReadPowers } from './power.js' export * as constants from './constants.js' +export { generateAndWritePolicy, generatePolicy } from './policy-gen/index.js' export { loadPolicies } from './policy.js' export { toEndoPolicy } diff --git a/packages/endomoat/src/policy-gen/index.js b/packages/endomoat/src/policy-gen/index.js new file mode 100644 index 0000000000..7798ca4454 --- /dev/null +++ b/packages/endomoat/src/policy-gen/index.js @@ -0,0 +1,176 @@ +/** + * Provides Lavamoat policy generation facilities via {@link generatePolicy} + * + * @packageDocumentation + */ +import { loadCompartmentForArchive } from '@endo/compartment-mapper' +import assert from 'node:assert' +import nodeFs from 'node:fs' +import path from 'node:path' +import { pathToFileURL } from 'node:url' +import { DEFAULT_POLICY_DEBUG_PATH, DEFAULT_POLICY_PATH } from '../constants.js' +import { importHook } from '../import-hook.js' +import { moduleTransforms } from '../module-transforms.js' +import { defaultReadPowers, makeReadPowers } from '../power.js' +import { isFsAPI, writeJson } from '../util.js' +import { PolicyGenerator } from './policy-generator.js' + +const { fromEntries, entries } = Object + +/** + * Generates a LavaMoat debug policy from a given entry point using + * `@endo/compartment-mapper` + * + * @overload + * @param {string | URL} entrypointPath + * @param {import('./types.js').GeneratePolicyOptions & { debug: true }} opts + * @returns {Promise} + * @public + */ +/** + * Generates a LavaMoat policy from a given entry point using + * `@endo/compartment-mapper` + * + * @overload + * @param {string | URL} entrypointPath + * @param {import('./types.js').GeneratePolicyOptions} [opts] + * @returns {Promise} + * @public + */ + +/** + * Generates a LavaMoat policy or debug policy from a given entry point using + * `@endo/compartment-mapper` + * + * @param {string | URL} entrypointPath + * @param {import('./types.js').GeneratePolicyOptions} [opts] + * @returns {Promise} + * @public + */ +export async function generatePolicy( + entrypointPath, + { + readPowers = defaultReadPowers, + debug = false, + policyOverride, + ...archiveOpts + } = {} +) { + readPowers = isFsAPI(readPowers) ? makeReadPowers(readPowers) : readPowers + + const { compartmentMap, sources, renames } = await loadCompartmentMap( + entrypointPath, + { + ...archiveOpts, + readPowers, + } + ) + + /** @type {import('./types.js').PolicyGeneratorOptions} */ + const baseOpts = { readPowers, policyOverride } + + // this weird thing is to make TS happy about the overload + const opts = debug ? { debug: true, ...baseOpts } : baseOpts + + return await PolicyGenerator.generatePolicy( + compartmentMap, + sources, + renames, + opts + ) +} + +/** + * Generates a LavaMoat policy or debug policy from a given entry point using + * `@endo/compartment-mapper` + * + * @param {string | URL} entrypointPath + * @param {import('./types.js').GenerateAndWritePolicyOptions} [opts] + * @returns {Promise} + * @public + */ +export async function generateAndWritePolicy(entrypointPath, opts = {}) { + const { + policyDebugPath = DEFAULT_POLICY_DEBUG_PATH, + policyPath = DEFAULT_POLICY_PATH, + fs = nodeFs, + ...generateOpts + } = opts + + assert(path.isAbsolute(policyPath), 'policyPath must be an absolute path') + assert( + path.isAbsolute(policyDebugPath), + 'policyDebugPath must be an absolute path' + ) + + await Promise.resolve() + + /** @type {import('lavamoat-core').LavaMoatPolicy | undefined} */ + let policy + + // if the debug flag was true, then the result of generatePolicy + // will be a LavaMoatPolicyDebug. we will write that entire thing to the debug policy, + // then extract everything except the `debugInfo` prop, and write _that_ to the actual policy + if (generateOpts.debug) { + const debugPolicy = await generatePolicy(entrypointPath, { + ...generateOpts, + debug: true, + }) + await writeJson(policyDebugPath, debugPolicy, { fs }) + + // do not attempt to use the `delete` keyword with typescript. you have been warned! + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { debugInfo, ...corePolicy } = + /** @type {import('lavamoat-core').LavaMoatPolicyDebug} */ (debugPolicy) + policy = corePolicy + } + + policy = policy ?? (await generatePolicy(entrypointPath, generateOpts)) + + await writeJson(policyPath, policy, { fs }) + + return policy +} + +/** + * Loads compartment map and associated sources. + * + * @param {string | URL} entrypointPath + * @param {import('./types.js').LoadCompartmentMapOptions} opts + * @internal + */ +export async function loadCompartmentMap( + entrypointPath, + { readPowers = defaultReadPowers, ...archiveOpts } = {} +) { + const moduleLocation = + entrypointPath instanceof URL + ? `${entrypointPath}` + : `${pathToFileURL(entrypointPath)}` + + const { archiveCompartmentMap, archiveSources, compartmentRenames } = + await loadCompartmentForArchive({ + dev: true, + ...archiveOpts, + readPowers, + moduleLocation, + importHook, + moduleTransforms, + extraParsers: { + node: 'bytes', + }, + }) + + // `compartmentRenames` is a mapping of filepath to compartment name; + // we need the reverse mapping. + const renames = fromEntries( + entries(compartmentRenames).map(([filepath, id]) => [id, filepath]) + ) + + return { + compartmentMap: archiveCompartmentMap, + sources: archiveSources, + renames, + } +} diff --git a/packages/endomoat/src/policy-gen/lmr-cache.js b/packages/endomoat/src/policy-gen/lmr-cache.js new file mode 100644 index 0000000000..5bcdd0dcdf --- /dev/null +++ b/packages/endomoat/src/policy-gen/lmr-cache.js @@ -0,0 +1,57 @@ +/** + * Provides {@link LMRCache} + * + * @packageDescription + */ + +import { LavamoatModuleRecord } from 'lavamoat-core' + +/** + * This class represents a transient cache for + * {@link LavamoatModuleRecord LavamoatModuleRecords}. + * + * It's a thin wrapper around a `Map`; it's used to ensure we don't create + * duplicate `LavamoatModuleRecord` objects for the same specifiers. + */ +export class LMRCache { + /** @type {Map} */ + #cache + constructor() { + this.#cache = new Map() + } + + /** + * Computes the cache key for a given {@link LavamoatModuleRecord} or its + * options. + * + * @param {import('lavamoat-core').LavamoatModuleRecordOptions + * | LavamoatModuleRecord} opts + * @returns {string} + * @todo Determine if this is appropriately unique + */ + static keyFor({ specifier, file }) { + return `${specifier}:${file}` + } + + /** + * Gets or creates a new {@link LavamoatModuleRecord} for the given options. + * + * @param {import('lavamoat-core').LavamoatModuleRecordOptions} opts + */ + get(opts) { + const key = LMRCache.keyFor(opts) + if (this.#cache.has(key)) { + return /** @type {LavamoatModuleRecord} */ (this.#cache.get(key)) + } + const record = new LavamoatModuleRecord(opts) + this.#cache.set(key, record) + return record + } + + /** + * Clears the cache + */ + clear() { + this.#cache.clear() + } +} diff --git a/packages/endomoat/src/policy-gen/policy-generator-context.js b/packages/endomoat/src/policy-gen/policy-generator-context.js new file mode 100644 index 0000000000..1e58ca1c1f --- /dev/null +++ b/packages/endomoat/src/policy-gen/policy-generator-context.js @@ -0,0 +1,380 @@ +/** + * Provides {@link PolicyGeneratorContext} + * + * @packageDocumentation + */ + +import { isBuiltin as nodeIsBuiltin } from 'node:module' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { + LAVAMOAT_PKG_POLICY_ROOT, + LMR_TYPE_BUILTIN, + LMR_TYPE_SOURCE, +} from '../constants.js' +import { defaultReadPowers } from '../power.js' + +const { entries, keys, fromEntries } = Object + +/** + * Handles creation of {@link LavamoatModuleRecord} objects for individual + * compartment descriptors. + * + * @internal + */ +export class PolicyGeneratorContext { + /** + * If `true`, this is the entry compartment + * + * @type {boolean} + */ + #isEntry + + /** + * Internal cache for {@link LavamoatModuleRecord} objects + * + * @type {Readonly} + */ + #lmrCache + /** + * Read powers + * + * @type {import('@endo/compartment-mapper').ReadPowers} + */ + #readPowers + + /** + * Compartment descriptor + * + * @remarks + * Exposed for debugging + * @type {Readonly< + * import('@endo/compartment-mapper').CompartmentDescriptor + * >} + * @internal + */ + compartment + + /** + * Mapping of renamed compartments + * + * @remarks + * Exposed for debugging + * @type {Readonly>} + * @internal + */ + renames + + /** + * Used for reading source files + */ + static #decoder = new TextDecoder() + + /** + * Sets some properties + * + * @remarks + * A class w/ a private constructor is essentially "final" and may only be + * instantiated via a + * {@link PolicyGeneratorContext.create static factory method} of the same + * class. This is intentional, since inheritance is undesirable in this case. + * @private + * @param {Readonly< + * import('@endo/compartment-mapper').CompartmentDescriptor + * >} compartment + * @param {Readonly>} renames + * @param {Readonly} lmrCache + * @param {Readonly} opts + */ + constructor( + compartment, + renames, + lmrCache, + { readPowers = defaultReadPowers, isEntry = false } = {} + ) { + this.#readPowers = readPowers + this.#isEntry = isEntry + this.#lmrCache = lmrCache + this.compartment = compartment + this.renames = renames + } + + /** + * The package name which will appear in the source's `LavamoatModuleRecord` + * and eventual policy + */ + get packageName() { + return this.#isEntry ? LAVAMOAT_PKG_POLICY_ROOT : this.compartment.name + } + + /** + * Ensures a specifier is not just a package with the same name as a builtin + * + * @param {string} specifier + */ + isBuiltin(specifier) { + return !(specifier in this.compartment.modules) && nodeIsBuiltin(specifier) + } + + /** + * A `ModuleDescriptor` has a compartment and module, which can be joined to + * form an absolute path. + * + * @overload + * @param {Required} descriptor + * @returns {string} + * @internal + * @todo Evaluate windows compat + */ + + /** + * Converts a `string` `file://` URL to a path + * + * @overload + * @param {string} location + * @returns {string} + * @internal + */ + + /** + * @remarks + * In Endo, the `compartment` is stored as a _string_ `file://` URL; hence the + * conversion. + * @param {string + * | Required} descriptorOrLocation + * @returns {string} + * @internal + * @todo There may be a safer way to do this conversion + */ + toPath(descriptorOrLocation) { + if (typeof descriptorOrLocation === 'string') { + return fileURLToPath(new URL(descriptorOrLocation)) + } + const location = this.renames[descriptorOrLocation.compartment] + if (!location) { + throw new TypeError( + `Rename map missing location for compartment "${descriptorOrLocation.compartment}" in compartment ${this.compartment.name}` + ) + } + return fileURLToPath( + new URL(path.join(location, descriptorOrLocation.module)) + ) + } + + /** + * Attempts to find the filepath of a specifier + * + * Strips trailing slashes from specifiers, since Endo does not store them + * with trailing slashes + * + * @param {string} specifier + * @returns {string | undefined} + * @internal + */ + getFilepath(specifier) { + specifier = specifier.replace(/\/$/, '') + + const moduleDescriptor = this.compartment.modules[specifier] + if (PolicyGeneratorContext.isCompleteModuleDescriptor(moduleDescriptor)) { + return this.toPath(moduleDescriptor) + } + + return undefined + } + + /** + * Builds an import map for a {@link LavamoatModuleRecord} from a list of + * import specifiers. + * + * These specifiers can come from either `StaticModuleType` (as found in + * `Sources`) or `ModuleDescriptor` objects. + * + * Relative-path specifiers do not need an import map entry + * + * @param {string[]} imports + * @returns {Record} + * @internal + */ + buildImportMap(imports = []) { + return fromEntries( + imports + .filter((specifier) => !specifier.startsWith('.')) + .map((specifier) => { + if (this.isBuiltin(specifier)) { + return [specifier, specifier] + } + + /** @type {string | undefined} */ + let file = this.getFilepath(specifier) + + if (!file) { + throw new ReferenceError( + `Cannot find file for specifier "${specifier}" in compartment "${this.compartment.name}"` + ) + } + + return /** @type {[specifier: string, file: string]} */ ([ + specifier, + file, + ]) + }) + ) + } + + /** + * Type guard for a `Required`. + * + * The `compartment` and `module` props are optional in the original type, but + * we need both. + * + * @param {unknown} descriptor + * @returns {descriptor is Required} + * @this {void} + * @internal + */ + static isCompleteModuleDescriptor(descriptor) { + return Boolean( + descriptor && + typeof descriptor === 'object' && + 'compartment' in descriptor && + 'module' in descriptor + ) + } + + /** + * Factory to create a new {@link PolicyGeneratorContext} + * + * @param {Readonly< + * import('@endo/compartment-mapper').CompartmentDescriptor + * >} compartment + * @param {Readonly>} renames + * @param {Readonly} lmrCache + * @param {Readonly} opts + */ + static create(compartment, renames, lmrCache, opts = {}) { + return new PolicyGeneratorContext(compartment, renames, lmrCache, opts) + } + + /** + * Given an import map, creates an array of {@link LavamoatModuleRecord}s for + * each builtin found in there. + * + * @param {import('lavamoat-core').LavamoatModuleRecord['importMap']} importMap + * @returns {import('lavamoat-core').LavamoatModuleRecord[]} Zero or more LMRs + * @internal + */ + buildModuleRecordsFromImportMap(importMap) { + return keys(importMap) + .filter((specifier) => this.isBuiltin(specifier)) + .map((specifier) => + this.#lmrCache.get({ + type: LMR_TYPE_BUILTIN, + file: specifier, + specifier, + packageName: specifier, + }) + ) + } + + /** + * Creates one or more {@link LavamoatModuleRecord LavamoatModuleRecords} from + * a single `ModuleSource`. + * + * The resulting array will contain--at minimum--a LMR of the source itself. + * It will also contain zero (0) or more LMRs for any builtins the module + * references. + * + * @param {string} specifier + * @param {import('@endo/compartment-mapper').ModuleSource} source + * @returns {Promise} + * @internal + */ + async buildModuleRecordsForSource(specifier, { record, sourceLocation }) { + if (!sourceLocation) { + // XXX: why would we not have a sourceLocation? + throw new TypeError( + `Source descriptor "${specifier}" missing "sourceLocation" prop in compartment "${this.compartment.name}"` + ) + } + + if (!record) { + // XXX: under what circumstances does this occur? + throw new TypeError( + `Source descriptor "${specifier}" in compartment "${this.compartment.name}" missing prop: record` + ) + } + + // `record` can be several different types, but for our purposes, + // we can use `imports` as the discriminator + if (!('imports' in record)) { + // XXX: under what circumstances does this occur? + throw new TypeError( + `StaticModuleType for source descriptor "${specifier}" in compartment "${this.compartment.name} missing prop: imports` + ) + } + + /** + * The `ModuleSource.content` prop is already pre-processed by Endo, and we + * do not want that, since it befouls our AST crawling. + * + * @remarks + * Doing this first since it may be more likely to fail than the other + * operations below + * @type {NonNullable< + * import('lavamoat-core').LavamoatModuleRecord['content'] + * >} + * @todo Modify Endo to surface the original source + */ + const content = await this.#readPowers + .read(sourceLocation) + .then((buffer) => PolicyGeneratorContext.#decoder.decode(buffer)) + + /** + * The {@link LavamoatModuleRecord.file} prop + * + * @type {import('lavamoat-core').LavamoatModuleRecord['file']} + */ + const file = fileURLToPath(new URL(sourceLocation)) + + /** + * The {@link LavamoatModuleRecord.importMap} prop + * + * @type {import('lavamoat-core').LavamoatModuleRecord['importMap']} + */ + const importMap = this.buildImportMap(record.imports) + + const lmrs = [ + // because builtins do not have their own compartments, we need to + // look into the `importMap` and create LMRs for each builtin. + // we will add the source LMR to this array as the last step + ...this.buildModuleRecordsFromImportMap(importMap), + + // the LMR for the source module itself + this.#lmrCache.get({ + specifier: file, + file, + packageName: this.packageName, + importMap, + content, + type: LMR_TYPE_SOURCE, + }), + ] + + return lmrs + } + + /** + * Creates {@link LavamoatModuleRecord}s from CompartmentSources. + * + * @param {import('@endo/compartment-mapper').CompartmentSources} sources + * @returns {Promise} + */ + async buildModuleRecords(sources) { + const lmrs = await Promise.all( + entries(sources).map((entry) => + this.buildModuleRecordsForSource(...entry) + ) + ) + return lmrs.flat() + } +} diff --git a/packages/endomoat/src/policy-gen/policy-generator.js b/packages/endomoat/src/policy-gen/policy-generator.js new file mode 100644 index 0000000000..8db8982a9c --- /dev/null +++ b/packages/endomoat/src/policy-gen/policy-generator.js @@ -0,0 +1,307 @@ +import { createModuleInspector } from 'lavamoat-core' +import { isBuiltin as nodeIsBuiltin } from 'node:module' +import { defaultReadPowers } from '../power.js' +import { LMRCache } from './lmr-cache.js' +import { PolicyGeneratorContext } from './policy-generator-context.js' + +const { entries } = Object + +/** + * Service which generates a LavaMoat policy from a + */ +export class PolicyGenerator { + /** + * Cache of `LavamoatModuleRecord` objects + * + * @type {Readonly} + */ + #lmrCache + + /** + * Mapping of compartment names to {@link PolicyGeneratorContext} instances + * + * @type {Readonly>} + */ + #contexts + + /** + * Compartment sources + * + * @remarks + * Exposed for debugging + * @type {Readonly} + * @internal + */ + sources + + /** + * Compartment map + * + * @remarks + * Exposed for debugging + * @type {Readonly< + * import('@endo/compartment-mapper').CompartmentMapDescriptor + * >} + * @internal + */ + compartmentMap + + /** + * Override policy, if any + * + * @remarks + * Exposed for debugging + * @type {Readonly< + * import('lavamoat-core').LavaMoatPolicyOverrides | undefined + * >} + * @internal + */ + policyOverride + + /** + * Creates {@link PolicyGeneratorContext} instances for each compartment in + * `compartmentMap`. + * + * @remarks + * A class w/ a private constructor is essentially "final" and may only be + * instantiated via a {@link PolicyGenerator.create static factory method} of + * the same class. This is intentional, since inheritance is undesirable in + * this case. + * @private + * @param {import('@endo/compartment-mapper').CompartmentMapDescriptor} compartmentMap + * Compartment map descriptor + * @param {import('@endo/compartment-mapper').Sources} sources Sources + * @param {Record} renames Mapping of compartment name to + * filepath + * @param {import('./types.js').PolicyGeneratorOptions} opts Additional + * options + */ + constructor( + compartmentMap, + sources, + renames, + { readPowers = defaultReadPowers, policyOverride } = {} + ) { + this.sources = Object.freeze(sources) + this.compartmentMap = Object.freeze(compartmentMap) + this.policyOverride = Object.freeze(policyOverride) + this.#lmrCache = new LMRCache() + + const entryCompartment = + compartmentMap.compartments[compartmentMap.entry.compartment] + + if (!entryCompartment) { + throw new TypeError('Could not find entry compartment; this is a bug') + } + + const frozenRenames = Object.freeze(renames) + + this.#contexts = new Map( + entries(compartmentMap.compartments).map( + ([compartmentName, compartment]) => [ + compartmentName, + PolicyGeneratorContext.create( + compartment, + frozenRenames, + this.#lmrCache, + { + isEntry: entryCompartment === compartment, + readPowers, + } + ), + ] + ) + ) + } + + /** + * Builds `LavamoatModuleRecord` objects from the `CompartmentMapDescriptor` + * + * @returns {Promise} Module + * records + * @internal + */ + async buildModuleRecords() { + await Promise.resolve() + + let moduleRecords = + /** @type {import('lavamoat-core').LavamoatModuleRecord[]} */ ( + ( + await Promise.all( + entries(this.sources).map( + async ([compartmentName, compartmentSources]) => { + if (!this.#contexts.has(compartmentName)) { + // this means that the compartment with this name was not actually used + return + } + + const compartment = /** @type {PolicyGeneratorContext} */ ( + this.#contexts.get(compartmentName) + ) + + return compartment.buildModuleRecords(compartmentSources) + } + ) + ) + ) + .flat() + .filter(Boolean) + ) + + moduleRecords = [...new Set(moduleRecords)] + + this.#lmrCache.clear() + + return moduleRecords + } + + /** + * Uses `inspector` to inspect a compartment map and sources. + * + * @param {import('lavamoat-core').ModuleInspector} inspector Module inspector + * @param {import('lavamoat-core').LavamoatModuleRecord[]} moduleRecords + * Module records + * @returns {import('lavamoat-core').ModuleInspector} The inspector + * @internal + */ + inspectModuleRecords(inspector, moduleRecords) { + // FIXME: should we sort here? + for (const record of moduleRecords) { + inspector.inspectModule(record) + } + + return inspector + } + + /** + * Generates a LavaMoat debug policy + * + * Policy generation occurs in three (3) steps: + * + * 1. Build module records from the `CompartmentMapDescriptor` and associated + * `Sources` + * 2. Inspect the module records using LavaMoat's `ModuleInspector` + * 3. Generate the policy using the `ModuleInspector` + * + * @overload + * @param {true} debug - If `true`, the result will be a debug policy + * @returns {Promise} Generated + * policy + * @internal + */ + + /** + * Generates a LavaMoat policy + * + * Policy generation occurs in three (3) steps: + * + * 1. Build module records from the `CompartmentMapDescriptor` and associated + * `Sources` + * 2. Inspect the module records using LavaMoat's `ModuleInspector` + * 3. Generate the policy using the `ModuleInspector` + * + * @overload + * @param {boolean} [debug] - If `true`, the result will be a debug policy + * @returns {Promise} Generated policy + * @internal + */ + + /** + * Generates a LavaMoat policy or LavaMoat debug policy. + * + * Policy generation occurs in three (3) steps: + * + * 1. Build module records from the `CompartmentMapDescriptor` and associated + * `Sources` + * 2. Inspect the module records using LavaMoat's `ModuleInspector` + * 3. Generate the policy using the `ModuleInspector` + * + * @param {boolean} [debug] - If `true`, the result will be a debug policy + * @returns {Promise< + * | import('lavamoat-core').LavaMoatPolicy + * | import('lavamoat-core').LavaMoatPolicyDebug + * >} + * Generated policy + * @internal + */ + async generatePolicy(debug) { + const moduleRecords = await this.buildModuleRecords() + + const inspector = createModuleInspector({ + isBuiltin: nodeIsBuiltin, + includeDebugInfo: debug, + }) + + return this.inspectModuleRecords(inspector, moduleRecords).generatePolicy({ + policyOverride: this.policyOverride, + }) + } + + /** + * Instantiates a {@link PolicyGenerator} and generates a debug policy. + * + * @overload + * @param {Readonly< + * import('@endo/compartment-mapper').CompartmentMapDescriptor + * >} compartmentMap + * @param {Readonly} sources + * @param {Readonly>} renames + * @param {import('./types.js').PolicyGeneratorOptions & { debug: true }} opts + * @returns {Promise} Generated + * debug policy + * @public + */ + + /** + * Instantiates a {@link PolicyGenerator} and generates a policy. + * + * @overload + * @param {Readonly< + * import('@endo/compartment-mapper').CompartmentMapDescriptor + * >} compartmentMap + * @param {Readonly} sources + * @param {Readonly>} renames + * @param {import('./types.js').PolicyGeneratorOptions} [opts] + * @returns {Promise} Generated policy + */ + + /** + * Instantiates a {@link PolicyGenerator} and generates a policy. + * + * @param {Readonly< + * import('@endo/compartment-mapper').CompartmentMapDescriptor + * >} compartmentMap + * @param {Readonly} sources + * @param {Readonly>} renames + * @param {import('./types.js').PolicyGeneratorOptions & { + * debug?: boolean + * }} [opts] + * @returns {Promise} Generated policy + * @public + */ + static async generatePolicy(compartmentMap, sources, renames, opts) { + const { debug, ...restOpts } = opts ?? {} + const generator = PolicyGenerator.create( + compartmentMap, + sources, + renames, + restOpts + ) + return generator.generatePolicy(debug) + } + + /** + * Factory for {@link PolicyGenerator} + * + * @param {import('@endo/compartment-mapper').CompartmentMapDescriptor} compartmentMap + * Compartment map descriptor + * @param {import('@endo/compartment-mapper').Sources} sources Sources + * @param {Record} renames Mapping of compartment name to + * filepath + * @param {import('./types.js').PolicyGeneratorOptions} opts Additional + * options + */ + static create(compartmentMap, sources, renames, opts = {}) { + return new PolicyGenerator(compartmentMap, sources, renames, opts) + } +} diff --git a/packages/endomoat/src/policy-gen/types.ts b/packages/endomoat/src/policy-gen/types.ts new file mode 100644 index 0000000000..4c95df71af --- /dev/null +++ b/packages/endomoat/src/policy-gen/types.ts @@ -0,0 +1,112 @@ +import { + type ArchiveOptions as EndoArchiveOptions, + type FsAPI, + type ReadPowers, +} from '@endo/compartment-mapper' +import { type LavaMoatPolicyOverrides } from 'lavamoat-core' +import { type Simplify } from 'type-fest' +import { type WritableFsAPI } from '../types.js' + +/** + * Options to pass-through to Endo. + * + * Used by {@link GeneratePolicyOptions}. + * + * The {@link EndoArchiveOptions.dev dev} property defaults to `true`. + * + * @remarks + * Omitted properties cannot by overridden by the user. + */ +export type ArchiveOptions = Omit< + EndoArchiveOptions, + 'readPowers' | 'importHook' | 'moduleTransforms' | 'extraParsers' +> + +/** + * Options having a `readPowers` property. + */ +export interface WithReadPowers { + /** + * Read powers to use when loading the compartment map. + */ + readPowers?: ReadPowers +} + +/** + * Options having a `readPowers` property also accepting an `fs`-like object. + */ +export interface WithReadPowersOrFs { + /** + * Read powers to use when loading the compartment map. + */ + readPowers?: ReadPowers | FsAPI +} + +/** + * Options having a `debug` property. + */ +export interface WithDebug { + /** + * If `true`, generate a debug policy. + */ + debug?: boolean +} + +/** + * Options having a `policyOverride` property. + */ +export interface WithPolicyOverride { + /** + * Policy overrides, if any + */ + policyOverride?: LavaMoatPolicyOverrides +} + +/** + * Options for `generatePolicy` + */ +export type GeneratePolicyOptions = Simplify< + ArchiveOptions & WithReadPowersOrFs & WithPolicyOverride & WithDebug +> + +export interface WritePolicyOptions { + policyDebugPath?: string + policyPath?: string + fs?: WritableFsAPI +} + +export type GenerateAndWritePolicyOptions = Simplify< + GeneratePolicyOptions & WritePolicyOptions +> + +/** + * Options for `loadCompartmentMap` + * + * @internal + */ +export type LoadCompartmentMapOptions = Simplify< + ArchiveOptions & WithReadPowers +> + +/** + * Options for the `PolicyGeneratorContext` constructor + * + * @internal + */ +export type PolicyGeneratorContextOptions = Simplify< + WithReadPowers & { + /** + * If `true`, the `PolicyGeneratorContext` represents the entry compartment + */ + isEntry?: boolean + } +> + +/** + * Options for the `PolicyGenerator` constructor + * + * @internal + */ +export type PolicyGeneratorOptions = Simplify< + WithReadPowers & WithPolicyOverride +> diff --git a/packages/endomoat/test/fixture/json/builtins.json b/packages/endomoat/test/fixture/json/builtins.json new file mode 100644 index 0000000000..d7806d4b5e --- /dev/null +++ b/packages/endomoat/test/fixture/json/builtins.json @@ -0,0 +1,11 @@ +{ + "/index.js": "// this is an integration test\n// of importing a package with a name that overlaps with a builtin\n\nconst eventsA = require('a')\nconst eventsB = require('b')\n\neventsA.once('hello', () => console.log('hi from a'))\neventsB.once('hello', () => console.log('hi from b'))\n\neventsA.emit('hello')\neventsB.emit('hello')\n", + "/package.json": "{\n \"name\": \"builtins\",\n \"scripts\": {\n \"setup\": \"exit 0\"\n },\n \"dependencies\": {\n \"a\": \"file:./node_modules/a\",\n \"b\": \"file:./node_modules/b\"\n }\n}\n", + "/node_modules/events/package.json": "{\n \"name\": \"events\",\n \"version\": \"1.0.0\"\n}\n", + "/node_modules/events/index.js": "\nconst c = console;\n\nc.log('hello world!')\n", + "/node_modules/b/package.json": "{\n \"name\": \"b\",\n \"version\": \"1.0.0\"\n}\n", + "/node_modules/b/index.js": "const { EventEmitter } = require('events')\n\nconst ee = new EventEmitter()\n\nmodule.exports = ee\n", + "/node_modules/a/index.js": "const { EventEmitter } = require('events/')\n\nconst ee = new EventEmitter()\n\nmodule.exports = ee\n", + "/node_modules/a/package.json": "{\n \"name\": \"a\",\n \"version\": \"1.0.0\",\n \"dependencies\": {\n \"events\": \"file:../events\"\n }\n}\n", + "/lavamoat/node/policy.json": "{\n \"resources\": {\n \"a\": {\n \"packages\": {\n \"events\": true\n }\n },\n \"b\": {\n \"builtin\": {\n \"events.EventEmitter\": true\n }\n },\n \"events\": {\n \"globals\": {\n \"console\": true\n }\n }\n }\n}" +} diff --git a/packages/endomoat/test/fixture/json/kitchen-sink.json b/packages/endomoat/test/fixture/json/kitchen-sink.json new file mode 100644 index 0000000000..b53f722fc6 --- /dev/null +++ b/packages/endomoat/test/fixture/json/kitchen-sink.json @@ -0,0 +1,32 @@ +{ + "/package.json": "{\n \"name\": \"main\",\n \"version\": \"1.0.0\",\n \"type\": \"module\",\n \"description\": \"\",\n \"author\": \"\",\n \"license\": \"ISC\",\n \"private\": true,\n \"main\": \"index.js\",\n \"keywords\": [],\n \"scripts\": {\n \"install\": \"echo \\\"Do not run npm install in this package\\\" && exit 1\"\n },\n \"dependencies\": {\n \"app\": \"1.0.0\"\n }\n}\n", + "/index.js": "import * as App from 'app'\n\nexport const hello = 'world'\nexport { App }\n", + "/node_modules/ignored/package.json": "{\n \"name\": \"ignored\",\n \"version\": \"1.0.0\",\n \"type\": \"module\",\n \"main\": \"index.js\",\n \"scripts\": {\n \"preinstall\": \"echo DO NOT INSTALL TEST FIXTURES; exit -1\"\n }\n}\n", + "/node_modules/ignored/README.md": "This package should not appear in the compartment map.\n", + "/node_modules/ignored/index.js": "export const ignored = true\n", + "/node_modules/edwin/README.md": "This package is exported from `avery`, except via a relative filepath instead of a bare specifier.\n", + "/node_modules/edwin/package.json": "{\n \"name\": \"edwin\",\n \"version\": \"1.0.0\",\n \"main\": \"./index.js\",\n \"type\": \"module\",\n \"parsers\": {\n \"js\": \"mjs\"\n },\n \"scripts\": {\n \"preinstall\": \"echo DO NOT INSTALL TEST FIXTURES; exit -1\"\n }\n}\n", + "/node_modules/edwin/index.js": "export const edwin = ['Eddie', 'Ed'];\n", + "/node_modules/danny/package.json": "{\n \"name\": \"danny\",\n \"version\": \"1.0.0\",\n \"main\": \"./src/index.js\",\n \"type\": \"module\",\n \"parsers\": {\n \"js\": \"mjs\"\n },\n \"scripts\": {\n \"preinstall\": \"echo DO NOT INSTALL TEST FIXTURES; exit -1\"\n }\n}\n", + "/node_modules/danny/README.md": "This package covers the referrer module specifier for a module that has been\nredirected to the \"index.js\" file within a module named for the containing\ndirectory, specifically that it can import adjacent modules within that\ndirectory.\n", + "/node_modules/danny/src/index.js": "export * from 'danny/src/danny.js';\n\nif (!Object.isFrozen(globalThis)) {\n throw new Error('The global object must be frozen in all compartments');\n}\n", + "/node_modules/danny/src/danny.js": "export const danny = 'Danny';\n\nif (!Object.isFrozen(globalThis)) {\n throw new Error('The global object must be frozen in all compartments');\n}\n", + "/node_modules/clarke/package.json": "{\n \"name\": \"clarke\",\n \"version\": \"1.0.0\",\n \"description\": \"Clarke implicitly exports an index ESM.\",\n \"type\": \"module\",\n \"scripts\": {\n \"preinstall\": \"echo DO NOT INSTALL TEST FIXTURES; exit -1\"\n }\n}\n", + "/node_modules/clarke/index.js": "export const clarke = 'Clarke';\n\nif (!Object.isFrozen(globalThis)) {\n throw new Error('The global object must be frozen in all compartments');\n}\n", + "/node_modules/clarke/README.md": "This package covers the ability to import any module within this package even\nthough no modules are explcitly mentioned in an \"exports\" directive in\npackage.json.\n", + "/node_modules/cjs/package.json": "{\n \"name\": \"cjs\",\n \"version\": \"1.0.0\",\n \"main\": \"index.js\",\n \"dependencies\": {\n \"clarke\": \"^1.0.0\"\n },\n \"scripts\": {\n \"preinstall\": \"echo DO NOT INSTALL TEST FIXTURES; exit -1\"\n }\n}\n", + "/node_modules/cjs/index.js": "require('clarke')\n\nmodule.exports = {\n cjs: true\n}\n", + "/node_modules/cjs/README.md": "This package is a CJS module.\n", + "/node_modules/cjs/node_modules/clarke/package.json": "{\n \"name\": \"clarke\",\n \"version\": \"1.0.0\",\n \"description\": \"Like the other Clarke, but it is CJS\",\n \"scripts\": {\n \"preinstall\": \"echo DO NOT INSTALL TEST FIXTURES; exit -1\"\n }\n}\n", + "/node_modules/cjs/node_modules/clarke/README.md": "This package is the same name and version as another, but it is CJS instead of ESM.\n", + "/node_modules/cjs/node_modules/clarke/index.js": "exports.clarke = 'Clarke';\n\nif (!Object.isFrozen(globalThis)) {\n throw new Error('The global object must be frozen in all compartments');\n}\n", + "/node_modules/brooke/package.json": "{\n \"name\": \"brooke\",\n \"version\": \"1.0.0\",\n \"description\": \"Brooke uses `exports` to reveal its main ESM.\",\n \"type\": \"module\",\n \"exports\": {\n \".\": \"./brooke.js\"\n },\n \"peerDependencies\": {\n \"clarke\": \"^1.0.0\"\n },\n \"scripts\": {\n \"preinstall\": \"echo DO NOT INSTALL TEST FIXTURES; exit -1\"\n }\n}\n", + "/node_modules/brooke/brooke.js": "import { clarke } from 'clarke';\n\nexport const brooke = 'Brooke';\n\nif (!Object.isFrozen(globalThis)) {\n throw new Error('The global object must be frozen in all compartments');\n}\n", + "/node_modules/brooke/README.md": "This package covers use of the \"exports\" directive to mark a single public\nmodule interface.\n", + "/node_modules/avery/README.md": "This package covers use of a \"main\" directive in combination with the \"type\":\n\"module\" directive to name the module that represents the package as a whole.\n\n", + "/node_modules/avery/package.json": "{\n \"name\": \"avery\",\n \"version\": \"1.0.0\",\n \"description\": \"Avery exports a `main` ESM.\",\n \"type\": \"module\",\n \"main\": \"./avery.js\",\n \"dependencies\": {\n \"edwin\": \"^1.0.0\"\n },\n \"scripts\": {\n \"preinstall\": \"echo DO NOT INSTALL TEST FIXTURES; exit -1\"\n }\n}\n", + "/node_modules/avery/avery.js": "export const avery = 'Avery';\nexport { edwin } from '../edwin';\n", + "/node_modules/app/package.json": "{\n \"name\": \"app\",\n \"version\": \"1.0.0\",\n \"type\": \"module\",\n \"main\": \"./main.js\",\n \"dependencies\": {\n \"avery\": \"^1.0.0\",\n \"brooke\": \"^1.0.0\",\n \"clarke\": \"^1.0.0\",\n \"danny\": \"^1.0.0\",\n \"ignored\": \"^1.0.0\",\n \"cjs\": \"^1.0.0\"\n },\n \"scripts\": {\n \"preinstall\": \"echo DO NOT INSTALL TEST FIXTURES; exit -1\"\n }\n}\n", + "/node_modules/app/main.js": "/* global globalProperty */\n\nexport { avery } from 'avery';\n// My (kriskowal) theory is that the brooke module isn't resolvable because the\n// linter isn't aware of package.json's main property and seems to hope that\n// brooke/index.js stands in for it.\n// eslint-disable-next-line import/no-unresolved\nexport { brooke } from 'brooke';\nexport { clarke } from 'clarke';\nexport { danny } from 'danny';\n\nexport const receivedGlobalProperty = globalProperty;\n\nexport const importMetaUrl = import.meta.url;\n\nif (!Object.isFrozen(globalThis)) {\n throw new Error('The global object must be frozen in all compartments');\n}\n\n// reflexivity\nimport * as app from 'app';\nimport * as appMain from 'app';\nif (app !== appMain) {\n throw new Error(\n 'Import aliases for the reflexive module name should produce identical namespace objects',\n );\n}\n\nimport cjs from 'cjs';\nexport const cjsValue = cjs.cjs;\n\n", + "/node_modules/app/README.md": "This is the root application package for testing.\nThis package depends transitively upon other packages that exercise various\nscenarios.\nThe test scaffold uses this to confirm equivalent behavior for applications\nwhen run directly off the file system and from archives.\n" +} diff --git a/packages/endomoat/test/fixture/json/scaffold-module.json b/packages/endomoat/test/fixture/json/scaffold-module.json new file mode 100644 index 0000000000..a0abee1481 --- /dev/null +++ b/packages/endomoat/test/fixture/json/scaffold-module.json @@ -0,0 +1,5 @@ +{ + "/package.json": "{\n \"name\": \"scaffold\",\n \"version\": \"1.0.0\",\n \"type\": \"module\",\n \"license\": \"ISC\",\n \"private\": true,\n \"main\": \"entry.js\",\n \"dependencies\": {\"test\": \"*\"}\n}\n", + "/entry.js": "import 'test'", + "/node_modules/test/package.json": "{\n \"name\": \"test\",\n \"version\": \"1.0.0\",\n \"type\": \"module\",\n \"license\": \"ISC\",\n \"private\": true,\n \"main\": \"index.js\"\n}\n" +} diff --git a/packages/endomoat/test/fixture/json/scaffold-script.json b/packages/endomoat/test/fixture/json/scaffold-script.json new file mode 100644 index 0000000000..2930e53aab --- /dev/null +++ b/packages/endomoat/test/fixture/json/scaffold-script.json @@ -0,0 +1,5 @@ +{ + "/package.json": "{\n \"name\": \"scaffold\",\n \"version\": \"1.0.0\",\n \"license\": \"ISC\",\n \"private\": true,\n \"main\": \"entry.js\",\n \"dependencies\": {\"test\": \"*\"}\n}\n", + "/entry.js": "require('test')", + "/node_modules/test/package.json": "{\n \"name\": \"test\",\n \"version\": \"1.0.0\",\n \"license\": \"ISC\",\n \"private\": true,\n \"main\": \"index.js\",\n \"dependencies\": {}\n}\n" +} diff --git a/packages/endomoat/test/policy-gen/generate-policy.spec.js b/packages/endomoat/test/policy-gen/generate-policy.spec.js new file mode 100644 index 0000000000..c0346dce21 --- /dev/null +++ b/packages/endomoat/test/policy-gen/generate-policy.spec.js @@ -0,0 +1,107 @@ +import 'ses' + +import test from 'ava' +import { createGeneratePolicyMacros } from './macros.js' + +const { testPolicyForModule, testPolicyForScript, testPolicyForJSON } = + createGeneratePolicyMacros(test) + +test(testPolicyForJSON, 'builtins.json') + +test(testPolicyForJSON, 'kitchen-sink.json') + +test('basic nested global access', testPolicyForModule, 'location.href', { + resources: { + test: { + globals: { + 'location.href': true, + }, + }, + }, +}) + +test( + 'CJS-specific ignores', + testPolicyForScript, + ` +const js = [this] +const ignored = [global, require, module, exports, arguments] +const globalRefs = [typeof globalThis, typeof self, typeof window] +const xyz = nonIgnoredGlobal +`, + { + resources: { + test: { + globals: { + nonIgnoredGlobal: true, + }, + }, + }, + } +) + +test( + 'ESM-specific ignores', + testPolicyForModule, + ` +const js = [this] +const ignored = [global, require, module, exports, arguments] +const globalRefs = [typeof globalThis, typeof self, typeof window] +const xyz = nonIgnoredGlobal +export {xyz} +`, + { + resources: { + test: { + globals: { + nonIgnoredGlobal: true, + require: true, + module: true, + exports: true, + }, + }, + }, + } +) + +test( + 'ignored global refs', + testPolicyForModule, + ` + const href = window.location.href + const xhr = new window.XMLHttpRequest() + `, + { + resources: { + test: { + globals: { + 'location.href': true, + XMLHttpRequest: true, + }, + }, + }, + } +) + +test( + 'ignored global refs when properties are not accessed', + testPolicyForModule, + 'typeof window !== "undefined"', + { resources: {} } +) + +test( + 'ignored global refs accessed w/ whitelist items', + testPolicyForModule, + `window.Object === Object`, + { + resources: {}, + } +) + +test( + 'generatePolicy - ignore newer intrinsics', + testPolicyForModule, + 'BigInt(123)', + { resources: {} } +) diff --git a/packages/endomoat/test/policy-gen/load-compartment-map.spec.js b/packages/endomoat/test/policy-gen/load-compartment-map.spec.js new file mode 100644 index 0000000000..8a98478e05 --- /dev/null +++ b/packages/endomoat/test/policy-gen/load-compartment-map.spec.js @@ -0,0 +1,17 @@ +import 'ses' + +import test from 'ava' +import { loadCompartmentMap } from '../../src/policy-gen/index.js' +import { loadJSONFixture } from '../fixture-util.js' + +test('compartment map is deterministic', async (t) => { + const { readPowers } = await loadJSONFixture( + new URL('../fixture/json/kitchen-sink.json', import.meta.url) + ) + + const result = await loadCompartmentMap('/index.js', { + readPowers, + }) + + t.snapshot(result) +}) diff --git a/packages/endomoat/test/policy-gen/macros.js b/packages/endomoat/test/policy-gen/macros.js new file mode 100644 index 0000000000..36a9a766f1 --- /dev/null +++ b/packages/endomoat/test/policy-gen/macros.js @@ -0,0 +1,166 @@ +import { generatePolicy } from '../../src/policy-gen/index.js' +import { loadJSONFixture, scaffoldFixture } from '../fixture-util.js' + +/** + * Path to `DirectoryJSON` fixtures. + * + * @remarks + * The trailing slash is load-bearing. + * @internal + */ +const JSON_FIXTURE_DIR = new URL('../fixture/json/', import.meta.url) + +/** + * Used by inner function in {@link createGeneratePolicyMacros} to determine + * which scaffold to use + * + * @internal + */ +const InlineSourceTypes = /** @type {const} */ ({ + Module: 'module', + Script: 'script', +}) + +/** + * @typedef {import('type-fest').ValueOf} InlineSourceType + */ + +/** + * Given an AVA test function, returns a set of macros for testing policy + * generation in various contexts + * + * @template [Ctx=unknown] Custom execution context, if any. Default is + * `unknown` + * @param {import('ava').TestFn} test - AVA test function + */ +export function createGeneratePolicyMacros(test) { + /** + * @type {import('ava').MacroDeclarationOptions< + * [ + * content: string | Buffer, + * opts?: { + * expectedPolicy?: import('lavamoat-core').LavaMoatPolicy + * sourceType?: InlineSourceType + * }, + * ], + * Ctx + * >} + * @internal + */ + const testInlinePolicy = { + exec: async ( + t, + content, + { expectedPolicy, sourceType = InlineSourceTypes.Module } = {} + ) => { + const { readPowers } = await scaffoldFixture(content, { sourceType }) + const actualPolicy = await generatePolicy('/entry.js', { + readPowers, + }) + if (expectedPolicy) { + t.deepEqual(actualPolicy, expectedPolicy) + } else { + t.snapshot(actualPolicy) + } + }, + title: ( + title, + content, + { expectedPolicy, sourceType = InlineSourceTypes.Module } = {} + ) => { + return ( + expectedPolicy + ? `${title ?? 'policy for inline ' + sourceType + ' matches expected policy'}` + : `${title ?? 'policy for ' + sourceType + ' fixture matches snapshot'}` + ).trim() + }, + } + + /** + * Test policy generation for ESM module content provided inline. + * + * If `expectedPolicy` is provided, the actual policy is compared to it; + * otherwise a snapshot is taken. + */ + const testPolicyForInlineModule = test.macro({ + /** + * @param {import('ava').ExecutionContext} t + * @param {string | Buffer} content + * @param {import('lavamoat-core').LavaMoatPolicy} [expectedPolicy] + */ + exec: async (t, content, expectedPolicy) => + testInlinePolicy.exec(t, content, { + expectedPolicy, + sourceType: InlineSourceTypes.Module, + }), + title: (title, content, expectedPolicy) => + testInlinePolicy.title(title, content, { + expectedPolicy, + sourceType: InlineSourceTypes.Module, + }), + }) + + /** + * Test policy generation for ESM module content + * + * If `expectedPolicy` is provided, the actual policy is compared to it; + * otherwise a snapshot is taken. + */ + const testPolicyForInlineScript = test.macro({ + /** + * @param {import('ava').ExecutionContext} t + * @param {string | Buffer} content + * @param {import('lavamoat-core').LavaMoatPolicy} [expectedPolicy] + */ + exec: async (t, content, expectedPolicy) => + testInlinePolicy.exec(t, content, { + expectedPolicy, + sourceType: InlineSourceTypes.Script, + }), + title: (title, content, expectedPolicy) => + testInlinePolicy.title(title, content, { + expectedPolicy, + sourceType: InlineSourceTypes.Script, + }), + }) + + /** + * Test policy generation for a given DirectoryJSON fixture + * + * If `expectedPolicy` is provided, the actual policy is compared to it; + * otherwise a snapshot is taken. + */ + const testPolicyForJSON = test.macro({ + /** + * @param {import('ava').ExecutionContext} t + * @param {string} fixtureFilename + * @param {import('lavamoat-core').LavaMoatPolicy} [expectedPolicy] + */ + exec: async (t, fixtureFilename, expectedPolicy) => { + const { readPowers } = await loadJSONFixture( + new URL(fixtureFilename, JSON_FIXTURE_DIR) + ) + + const actualPolicy = await generatePolicy('/index.js', { + readPowers, + }) + + if (expectedPolicy) { + t.deepEqual(actualPolicy, expectedPolicy) + } else { + t.snapshot(actualPolicy) + } + }, + title: (title, fixtureFilename, expectedPolicy) => + (expectedPolicy + ? `${title ?? 'policy for fixture matches expected policy'}` + : `${title ?? 'policy for fixture matches snapshot'}` + ).trim() + ` (${fixtureFilename})`, + }) + + return { + testPolicyForJSON, + testPolicyForModule: testPolicyForInlineModule, + testPolicyForScript: testPolicyForInlineScript, + } +} diff --git a/packages/endomoat/test/policy-gen/policy-generator.spec.js b/packages/endomoat/test/policy-gen/policy-generator.spec.js new file mode 100644 index 0000000000..283623ff3c --- /dev/null +++ b/packages/endomoat/test/policy-gen/policy-generator.spec.js @@ -0,0 +1,27 @@ +import 'ses' + +import test from 'ava' +import stringify from 'json-stable-stringify' +import { loadCompartmentMap } from '../../src/policy-gen/index.js' +import { PolicyGenerator } from '../../src/policy-gen/policy-generator.js' +import { loadJSONFixture } from '../fixture-util.js' + +test('buildModuleRecords() - result is deterministic', async (t) => { + const { readPowers } = await loadJSONFixture( + new URL('../fixture/json/kitchen-sink.json', import.meta.url) + ) + + const { compartmentMap, sources, renames } = await loadCompartmentMap( + '/index.js', + { readPowers } + ) + const generator = PolicyGenerator.create(compartmentMap, sources, renames, { + readPowers, + }) + const moduleRecords = await generator.buildModuleRecords() + + t.snapshot( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + stringify(moduleRecords.map(({ content, ...record }) => record)) + ) +}) diff --git a/packages/endomoat/test/policy-gen/snapshots/generate-policy.spec.js.md b/packages/endomoat/test/policy-gen/snapshots/generate-policy.spec.js.md new file mode 100644 index 0000000000..8d24cc2d57 --- /dev/null +++ b/packages/endomoat/test/policy-gen/snapshots/generate-policy.spec.js.md @@ -0,0 +1,60 @@ +# Snapshot report for `test/policy-gen/generate-policy.spec.js` + +The actual snapshot is saved in `generate-policy.spec.js.snap`. + +Generated by [AVA](https://avajs.dev). + +## policy for fixture matches snapshot (builtins.json) + +> Snapshot 1 + + { + resources: { + a: { + packages: { + events: true, + }, + }, + b: { + builtin: { + 'events.EventEmitter': true, + }, + }, + events: { + globals: { + console: true, + }, + }, + }, + } + +## policy for fixture matches snapshot (kitchen-sink.json) + +> Snapshot 1 + + { + resources: { + app: { + globals: { + globalProperty: true, + }, + packages: { + avery: true, + brooke: true, + cjs: true, + clarke: true, + danny: true, + }, + }, + brooke: { + packages: { + clarke: true, + }, + }, + cjs: { + packages: { + clarke: true, + }, + }, + }, + } diff --git a/packages/endomoat/test/policy-gen/snapshots/generate-policy.spec.js.snap b/packages/endomoat/test/policy-gen/snapshots/generate-policy.spec.js.snap new file mode 100644 index 0000000000..a3d79d56e7 Binary files /dev/null and b/packages/endomoat/test/policy-gen/snapshots/generate-policy.spec.js.snap differ diff --git a/packages/endomoat/test/policy-gen/snapshots/load-compartment-map.spec.js.md b/packages/endomoat/test/policy-gen/snapshots/load-compartment-map.spec.js.md new file mode 100644 index 0000000000..ac2d6838e9 --- /dev/null +++ b/packages/endomoat/test/policy-gen/snapshots/load-compartment-map.spec.js.md @@ -0,0 +1,813 @@ +# Snapshot report for `test/policy-gen/load-compartment-map.spec.js` + +The actual snapshot is saved in `load-compartment-map.spec.js.snap`. + +Generated by [AVA](https://avajs.dev). + +## compartment map is deterministic + +> Snapshot 1 + + { + compartmentMap: { + compartments: { + 'app-v1.0.0': { + label: 'app-v1.0.0', + location: 'app-v1.0.0', + modules: { + './main.js': { + location: 'main.js', + parser: 'pre-mjs-json', + sha512: '255f54bf3c1ce7d6b847fa61f639d2e68c4479e3902f60aabc3874f3d29c96dd52df4efb9c414427b9ad2b6323ce494d220c24b8b7e4c5d956f6a039b2f19bde', + }, + app: { + compartment: 'app-v1.0.0', + module: './main.js', + }, + avery: { + compartment: 'avery-v1.0.0', + module: './avery.js', + }, + brooke: { + compartment: 'brooke-v1.0.0', + module: './brooke.js', + }, + cjs: { + compartment: 'cjs-v1.0.0', + module: './index.js', + }, + clarke: { + compartment: 'clarke-v1.0.0', + module: './index.js', + }, + danny: { + compartment: 'danny-v1.0.0', + module: './src/index.js', + }, + ignored: { + compartment: 'ignored-v1.0.0', + module: './index.js', + }, + }, + name: 'app', + policy: undefined, + }, + 'avery-v1.0.0': { + label: 'avery-v1.0.0', + location: 'avery-v1.0.0', + modules: { + './avery.js': { + location: 'avery.js', + parser: 'pre-mjs-json', + sha512: 'c1ce0832b3169e62f6b983027d769963470dcb699e904eb6cdd8524335f45ef74e5f6661cedd34ce39cdd3a26bf9d4594cb1e1eb3b4762ebb838c1fed0f6bd63', + }, + avery: { + compartment: 'avery-v1.0.0', + module: './avery.js', + }, + edwin: { + compartment: 'edwin-v1.0.0', + module: './index.js', + }, + }, + name: 'avery', + policy: undefined, + }, + 'brooke-v1.0.0': { + label: 'brooke-v1.0.0', + location: 'brooke-v1.0.0', + modules: { + './brooke.js': { + location: 'brooke.js', + parser: 'pre-mjs-json', + sha512: 'e06a978ff8b7065fe2230c52c025ca6e8e4d82c1c7c048ddaee3c4507d4c6e5cb8ded33f5e0cdee5392030aa7fa37391dbdb08cc1cc4f4b700dead5593c3b685', + }, + brooke: { + compartment: 'brooke-v1.0.0', + module: './brooke.js', + }, + clarke: { + compartment: 'clarke-v1.0.0', + module: './index.js', + }, + }, + name: 'brooke', + policy: undefined, + }, + 'cjs-v1.0.0': { + label: 'cjs-v1.0.0', + location: 'cjs-v1.0.0', + modules: { + '.': { + compartment: 'cjs-v1.0.0', + module: './index.js', + }, + './index.js': { + location: 'index.js', + parser: 'pre-cjs-json', + sha512: 'f931ea7295b4d9754481dded53e2513624e1c3b2b9b4d278c9d132656a0cee6a1f304d03dfdb5901aafc0a528643ab2933530fe2eb6132e32fd487d5772dae3c', + }, + cjs: { + compartment: 'cjs-v1.0.0', + module: './index.js', + }, + clarke: { + compartment: 'clarke-v1.0.0-n1', + module: './index.js', + }, + }, + name: 'cjs', + policy: undefined, + }, + 'clarke-v1.0.0': { + label: 'clarke-v1.0.0', + location: 'clarke-v1.0.0', + modules: { + './index.js': { + location: 'index.js', + parser: 'pre-mjs-json', + sha512: 'bba41dc9c98da34c1460123ec2a052acb4108bfd661560ebfef135c725ff49fbba93a8f28593cdc789b38b51c7f138aeef32fb267bdb87ca63c1676411ded460', + }, + clarke: { + compartment: 'clarke-v1.0.0', + module: './index.js', + }, + }, + name: 'clarke', + policy: undefined, + }, + 'clarke-v1.0.0-n1': { + label: 'clarke-v1.0.0', + location: 'clarke-v1.0.0-n1', + modules: { + '.': { + compartment: 'clarke-v1.0.0-n1', + module: './index.js', + }, + './index.js': { + location: 'index.js', + parser: 'pre-cjs-json', + sha512: '97055bde236eb9dc56c2b744a83ea6a9c1c85aef4077935b8f7c439ebf47f1c7dca0d49697844a67df18a50b6a745ef9a8bacf4c94da355575d550bfbac0fecd', + }, + clarke: { + compartment: 'clarke-v1.0.0-n1', + module: './index.js', + }, + }, + name: 'clarke', + policy: undefined, + }, + 'danny-v1.0.0': { + label: 'danny-v1.0.0', + location: 'danny-v1.0.0', + modules: { + './src/danny.js': { + location: 'src/danny.js', + parser: 'pre-mjs-json', + sha512: 'f91bd5735c1c6a3741cb159a3ce21e1ca69168c959c6788eb60813a79772ff45498ed5084e8aecd04e75ae36f1ccbf8d5ee85897c996bde952f66e7358825255', + }, + './src/index.js': { + location: 'src/index.js', + parser: 'pre-mjs-json', + sha512: 'db255297ee0330dc47dec12f0184bf72e38387e396fcc1cc8ec1f78cfad290f10e57f23fdfa50e8135a81b4bf36e4c7b14e696a95c83f1b593d34d9a1d857948', + }, + danny: { + compartment: 'danny-v1.0.0', + module: './src/index.js', + }, + 'danny/src/danny.js': { + compartment: 'danny-v1.0.0', + module: './src/danny.js', + }, + }, + name: 'danny', + policy: undefined, + }, + 'edwin-v1.0.0': { + label: 'edwin-v1.0.0', + location: 'edwin-v1.0.0', + modules: { + './index.js': { + location: 'index.js', + parser: 'pre-mjs-json', + sha512: '1d433e7d9ea3eea7ffd01e469cc7960d917d6aeea0e70fde961fee0281a66c82e6d9b2d857a3fcbcf09eda52ee5a07daf8fe205f27a206ca47d08e4a7ed937d1', + }, + edwin: { + compartment: 'edwin-v1.0.0', + module: './index.js', + }, + }, + name: 'edwin', + policy: undefined, + }, + 'main-v1.0.0': { + label: 'main-v1.0.0', + location: 'main-v1.0.0', + modules: { + './index.js': { + location: 'index.js', + parser: 'pre-mjs-json', + sha512: 'ee859eb35ed3baa77ade9eb727db0f31e43628c8318f45c0f6cecd65c02298ca8791baed26aa8262c394e36ccd431f496a108e749c12c550e35d17246a8b5cd2', + }, + app: { + compartment: 'app-v1.0.0', + module: './main.js', + }, + main: { + compartment: 'main-v1.0.0', + module: './index.js', + }, + }, + name: 'main', + policy: undefined, + }, + }, + entry: { + compartment: 'main-v1.0.0', + module: './index.js', + }, + tags: [], + }, + renames: { + 'app-v1.0.0': 'file:///node_modules/app/', + 'avery-v1.0.0': 'file:///node_modules/avery/', + 'brooke-v1.0.0': 'file:///node_modules/brooke/', + 'cjs-v1.0.0': 'file:///node_modules/cjs/', + 'clarke-v1.0.0': 'file:///node_modules/clarke/', + 'clarke-v1.0.0-n1': 'file:///node_modules/cjs/node_modules/clarke/', + 'danny-v1.0.0': 'file:///node_modules/danny/', + 'edwin-v1.0.0': 'file:///node_modules/edwin/', + 'ignored-v1.0.0': 'file:///node_modules/ignored/', + 'main-v1.0.0': 'file:///', + }, + sources: { + 'app-v1.0.0': { + './main.js': { + bytes: Uint8Array [ + 7b22696d 706f7274 73223a5b 22617665 7279222c 2262726f 6f6b6522 2c22636c + 61726b65 222c2264 616e6e79 222c2261 7070222c 22636a73 225d2c22 6578706f + 72747322 3a5b2263 6a735661 6c756522 2c22696d 706f7274 4d657461 55726c22 + 2c227265 63656976 6564476c 6f62616c 50726f70 65727479 222c6e75 6c6c2c6e + 756c6c2c 6e756c6c 2c6e756c 6c5d2c22 72656578 706f7274 73223a5b 5d2c225f + 5f73796e 634d6f64 756c6550 726f6772 616d5f5f 223a2228 7b202020 696d706f + 7274733a 202468e2 808d5f69 6d706f72 74732c20 20206c69 76655661 723a2024 + 68e2808d 5f6c6976 652c2020 206f6e63 65566172 3a202468 e2808d5f 6f6e6365 + 2c202020 696d706f 72744d65 74613a20 2468e280 8d5f5f5f 5f6d6574 612c207d + 29203d3e 20286675 6e637469 6f6e2028 29207b20 27757365 20737472 69637427 + 3b202020 6c657420 6170702c 6170704d 61696e2c 636a733b 2468e280 8d5f696d + 706f7274 73285b5b 5c226176 6572795c 222c205b 5d5d2c5b 5c226272 6f6f6b65 + 5c222c20 5b5d5d2c 5b5c2263 6c61726b 655c222c 205b5d5d 2c5b5c22 64616e6e + 795c222c 205b5d5d 2c5b5c22 6170705c 222c205b 5b5c222a 5c222c20 5b2468e2 + 808d5f61 203d3e20 28617070 203d2024 68e2808d 5f61292c 2468e280 8d5f6120 + 3d3e2028 6170704d 61696e20 3d202468 e2808d5f 61295d5d 5d5d2c5b 5c22636a + 735c222c 205b5b5c 22646566 61756c74 5c222c20 5b2468e2 808d5f61 203d3e20 + 28636a73 203d2024 68e2808d 5f61295d 5d5d5d5d 293b2020 205c6e5c 6e5c6e5c + 6e5c6e5c 6e5c6e5c 6e5c6e5c 6e5c6e63 6f6e7374 20202020 20202020 72656365 + 69766564 476c6f62 616c5072 6f706572 74793d67 6c6f6261 6c50726f 70657274 + 793b2468 e2808d5f 6f6e6365 2e726563 65697665 64476c6f 62616c50 726f7065 + 72747928 72656365 69766564 476c6f62 616c5072 6f706572 7479293b 5c6e5c6e + 636f6e73 74202020 20202020 20696d70 6f72744d 65746155 726c3d24 68e2808d + 5f5f5f5f 6d657461 2e75726c 3b2468e2 808d5f6f 6e63652e 696d706f 72744d65 + 74615572 6c28696d 706f7274 4d657461 55726c29 3b5c6e5c 6e696628 214f626a + 6563742e 69734672 6f7a656e 28676c6f 62616c54 68697329 297b5c6e 7468726f + 77206e65 77204572 726f7228 27546865 20676c6f 62616c20 6f626a65 6374206d + 75737420 62652066 726f7a65 6e20696e 20616c6c 20636f6d 70617274 6d656e74 + 7327293b 5c6e207d 5c6e5c6e 2f2a2072 65666c65 78697669 74792a2f 5c6e5c6e + 5c6e6966 28617070 213d3d61 70704d61 696e297b 5c6e7468 726f7720 6e657720 + 4572726f 72285c6e 27496d70 6f727420 616c6961 73657320 666f7220 74686520 + 7265666c 65786976 65206d6f 64756c65 206e616d 65207368 6f756c64 2070726f + 64756365 20696465 6e746963 616c206e 616d6573 70616365 206f626a 65637473 + 27293b5c 6e5c6e20 7d5c6e5c 6e5c6e63 6f6e7374 20202020 20202020 636a7356 + 616c7565 3d636a73 2e636a73 3b2468e2 808d5f6f 6e63652e 636a7356 616c7565 + 28636a73 56616c75 65293b5c 6e7d2928 295c6e22 2c225f5f 6c697665 4578706f + 72744d61 705f5f22 3a7b7d2c 225f5f72 65657870 6f72744d 61705f5f 223a7b22 + 61766572 79223a5b 5b226176 65727922 2c226176 65727922 5d5d2c22 62726f6f + 6b65223a 5b5b2262 726f6f6b 65222c22 62726f6f 6b65225d 5d2c2263 6c61726b + 65223a5b 5b22636c 61726b65 222c2263 6c61726b 65225d5d 2c226461 6e6e7922 + 3a5b5b22 64616e6e 79222c22 64616e6e 79225d5d 7d2c225f 5f666978 65644578 + 706f7274 4d61705f 5f223a7b 22726563 65697665 64476c6f 62616c50 726f7065 + 72747922 3a5b2272 65636569 76656447 6c6f6261 6c50726f 70657274 79225d2c + 22696d70 6f72744d 65746155 726c223a 5b22696d 706f7274 4d657461 55726c22 + 5d2c2263 6a735661 6c756522 3a5b2263 6a735661 6c756522 5d7d2c22 5f5f6e65 + 65647349 6d706f72 744d6574 615f5f22 3a747275 657d + ], + location: 'main.js', + parser: 'pre-mjs-json', + record: StaticModuleRecord { + __fixedExportMap__: { + cjsValue: [ + 'cjsValue', + ], + importMetaUrl: [ + 'importMetaUrl', + ], + receivedGlobalProperty: [ + 'receivedGlobalProperty', + ], + }, + __liveExportMap__: {}, + __needsImportMeta__: true, + __reexportMap__: { + avery: [ + [ + 'avery', + 'avery', + ], + ], + brooke: [ + [ + 'brooke', + 'brooke', + ], + ], + clarke: [ + [ + 'clarke', + 'clarke', + ], + ], + danny: [ + [ + 'danny', + 'danny', + ], + ], + }, + __syncModuleProgram__: `({ imports: $h‍_imports, liveVar: $h‍_live, onceVar: $h‍_once, importMeta: $h‍____meta, }) => (function () { 'use strict'; let app,appMain,cjs;$h‍_imports([["avery", []],["brooke", []],["clarke", []],["danny", []],["app", [["*", [$h‍_a => (app = $h‍_a),$h‍_a => (appMain = $h‍_a)]]]],["cjs", [["default", [$h‍_a => (cjs = $h‍_a)]]]]]); ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + const receivedGlobalProperty=globalProperty;$h‍_once.receivedGlobalProperty(receivedGlobalProperty);␊ + ␊ + const importMetaUrl=$h‍____meta.url;$h‍_once.importMetaUrl(importMetaUrl);␊ + ␊ + if(!Object.isFrozen(globalThis)){␊ + throw new Error('The global object must be frozen in all compartments');␊ + }␊ + ␊ + /* reflexivity*/␊ + ␊ + ␊ + if(app!==appMain){␊ + throw new Error(␊ + 'Import aliases for the reflexive module name should produce identical namespace objects');␊ + ␊ + }␊ + ␊ + ␊ + const cjsValue=cjs.cjs;$h‍_once.cjsValue(cjsValue);␊ + })()␊ + `, + exports: [ + 'cjsValue', + 'importMetaUrl', + 'receivedGlobalProperty', + undefined, + undefined, + undefined, + undefined, + ], + imports: [ + 'avery', + 'brooke', + 'clarke', + 'danny', + 'app', + 'cjs', + ], + reexports: [], + }, + sha512: '255f54bf3c1ce7d6b847fa61f639d2e68c4479e3902f60aabc3874f3d29c96dd52df4efb9c414427b9ad2b6323ce494d220c24b8b7e4c5d956f6a039b2f19bde', + sourceLocation: 'file:///node_modules/app/main.js', + }, + }, + 'avery-v1.0.0': { + './avery.js': { + bytes: Uint8Array [ + 7b22696d 706f7274 73223a5b 222e2e2f 65647769 6e225d2c 22657870 6f727473 + 223a5b22 61766572 79222c6e 756c6c5d 2c227265 6578706f 72747322 3a5b5d2c + 225f5f73 796e634d 6f64756c 6550726f 6772616d 5f5f223a 22287b20 2020696d + 706f7274 733a2024 68e2808d 5f696d70 6f727473 2c202020 6c697665 5661723a + 202468e2 808d5f6c 6976652c 2020206f 6e636556 61723a20 2468e280 8d5f6f6e + 63652c20 2020696d 706f7274 4d657461 3a202468 e2808d5f 5f5f5f6d 6574612c + 207d2920 3d3e2028 66756e63 74696f6e 20282920 7b202775 73652073 74726963 + 74273b20 20202468 e2808d5f 696d706f 72747328 5b5b5c22 2e2e2f65 6477696e + 5c222c20 5b5d5d5d 293b2020 20636f6e 73742020 20202020 20206176 6572793d + 27417665 7279273b 2468e280 8d5f6f6e 63652e61 76657279 28617665 7279293b + 5c6e7d29 28295c6e 222c225f 5f6c6976 65457870 6f72744d 61705f5f 223a7b7d + 2c225f5f 72656578 706f7274 4d61705f 5f223a7b 222e2e2f 65647769 6e223a5b + 5b226564 77696e22 2c226564 77696e22 5d5d7d2c 225f5f66 69786564 4578706f + 72744d61 705f5f22 3a7b2261 76657279 223a5b22 61766572 79225d7d 2c225f5f + 6e656564 73496d70 6f72744d 6574615f 5f223a66 616c7365 7d + ], + location: 'avery.js', + parser: 'pre-mjs-json', + record: StaticModuleRecord { + __fixedExportMap__: { + avery: [ + 'avery', + ], + }, + __liveExportMap__: {}, + __needsImportMeta__: false, + __reexportMap__: { + '../edwin': [ + [ + 'edwin', + 'edwin', + ], + ], + }, + __syncModuleProgram__: `({ imports: $h‍_imports, liveVar: $h‍_live, onceVar: $h‍_once, importMeta: $h‍____meta, }) => (function () { 'use strict'; $h‍_imports([["../edwin", []]]); const avery='Avery';$h‍_once.avery(avery);␊ + })()␊ + `, + exports: [ + 'avery', + undefined, + ], + imports: [ + '../edwin', + ], + reexports: [], + }, + sha512: 'c1ce0832b3169e62f6b983027d769963470dcb699e904eb6cdd8524335f45ef74e5f6661cedd34ce39cdd3a26bf9d4594cb1e1eb3b4762ebb838c1fed0f6bd63', + sourceLocation: 'file:///node_modules/avery/avery.js', + }, + }, + 'brooke-v1.0.0': { + './brooke.js': { + bytes: Uint8Array [ + 7b22696d 706f7274 73223a5b 22636c61 726b6522 5d2c2265 78706f72 7473223a + 5b226272 6f6f6b65 225d2c22 72656578 706f7274 73223a5b 5d2c225f 5f73796e + 634d6f64 756c6550 726f6772 616d5f5f 223a2228 7b202020 696d706f 7274733a + 202468e2 808d5f69 6d706f72 74732c20 20206c69 76655661 723a2024 68e2808d + 5f6c6976 652c2020 206f6e63 65566172 3a202468 e2808d5f 6f6e6365 2c202020 + 696d706f 72744d65 74613a20 2468e280 8d5f5f5f 5f6d6574 612c207d 29203d3e + 20286675 6e637469 6f6e2028 29207b20 27757365 20737472 69637427 3b202020 + 6c657420 636c6172 6b653b24 68e2808d 5f696d70 6f727473 285b5b5c 22636c61 + 726b655c 222c205b 5b5c2263 6c61726b 655c222c 205b2468 e2808d5f 61203d3e + 2028636c 61726b65 203d2024 68e2808d 5f61295d 5d5d5d5d 293b2020 205c6e5c + 6e636f6e 73742020 20202020 20206272 6f6f6b65 3d274272 6f6f6b65 273b2468 + e2808d5f 6f6e6365 2e62726f 6f6b6528 62726f6f 6b65293b 5c6e5c6e 69662821 + 4f626a65 63742e69 7346726f 7a656e28 676c6f62 616c5468 69732929 7b5c6e74 + 68726f77 206e6577 20457272 6f722827 54686520 676c6f62 616c206f 626a6563 + 74206d75 73742062 65206672 6f7a656e 20696e20 616c6c20 636f6d70 6172746d + 656e7473 27293b5c 6e207d5c 6e7d2928 295c6e22 2c225f5f 6c697665 4578706f + 72744d61 705f5f22 3a7b7d2c 225f5f72 65657870 6f72744d 61705f5f 223a7b7d + 2c225f5f 66697865 64457870 6f72744d 61705f5f 223a7b22 62726f6f 6b65223a + 5b226272 6f6f6b65 225d7d2c 225f5f6e 65656473 496d706f 72744d65 74615f5f + 223a6661 6c73657d + ], + location: 'brooke.js', + parser: 'pre-mjs-json', + record: StaticModuleRecord { + __fixedExportMap__: { + brooke: [ + 'brooke', + ], + }, + __liveExportMap__: {}, + __needsImportMeta__: false, + __reexportMap__: {}, + __syncModuleProgram__: `({ imports: $h‍_imports, liveVar: $h‍_live, onceVar: $h‍_once, importMeta: $h‍____meta, }) => (function () { 'use strict'; let clarke;$h‍_imports([["clarke", [["clarke", [$h‍_a => (clarke = $h‍_a)]]]]]); ␊ + ␊ + const brooke='Brooke';$h‍_once.brooke(brooke);␊ + ␊ + if(!Object.isFrozen(globalThis)){␊ + throw new Error('The global object must be frozen in all compartments');␊ + }␊ + })()␊ + `, + exports: [ + 'brooke', + ], + imports: [ + 'clarke', + ], + reexports: [], + }, + sha512: 'e06a978ff8b7065fe2230c52c025ca6e8e4d82c1c7c048ddaee3c4507d4c6e5cb8ded33f5e0cdee5392030aa7fa37391dbdb08cc1cc4f4b700dead5593c3b685', + sourceLocation: 'file:///node_modules/brooke/brooke.js', + }, + }, + 'cjs-v1.0.0': { + './index.js': { + bytes: Uint8Array [ + 7b22696d 706f7274 73223a5b 22636c61 726b6522 5d2c2265 78706f72 7473223a + 5b22636a 73222c22 64656661 756c7422 5d2c2272 65657870 6f727473 223a5b5d + 2c22736f 75726365 223a2228 66756e63 74696f6e 20287265 71756972 652c2065 + 78706f72 74732c20 6d6f6475 6c652c20 5f5f6669 6c656e61 6d652c20 5f5f6469 + 726e616d 6529207b 20726571 75697265 2827636c 61726b65 27293b5c 6e5c6e6d + 6f64756c 652e6578 706f7274 733d7b5c 6e636a73 3a747275 657d3b20 2f2f2a2f + 5c6e7d29 5c6e227d + ], + location: 'index.js', + parser: 'pre-cjs-json', + record: { + cjsFunctor: `(function (require, exports, module, __filename, __dirname) { require('clarke');␊ + ␊ + module.exports={␊ + cjs:true}; //*/␊ + })␊ + //# sourceURL=file:///node_modules/cjs/index.js␊ + `, + execute: Function noopExecute {}, + exports: [ + 'cjs', + 'default', + ], + imports: [ + 'clarke', + ], + reexports: [], + }, + sha512: 'f931ea7295b4d9754481dded53e2513624e1c3b2b9b4d278c9d132656a0cee6a1f304d03dfdb5901aafc0a528643ab2933530fe2eb6132e32fd487d5772dae3c', + sourceLocation: 'file:///node_modules/cjs/index.js', + }, + }, + 'clarke-v1.0.0': { + './index.js': { + bytes: Uint8Array [ + 7b22696d 706f7274 73223a5b 5d2c2265 78706f72 7473223a 5b22636c 61726b65 + 225d2c22 72656578 706f7274 73223a5b 5d2c225f 5f73796e 634d6f64 756c6550 + 726f6772 616d5f5f 223a2228 7b202020 696d706f 7274733a 202468e2 808d5f69 + 6d706f72 74732c20 20206c69 76655661 723a2024 68e2808d 5f6c6976 652c2020 + 206f6e63 65566172 3a202468 e2808d5f 6f6e6365 2c202020 696d706f 72744d65 + 74613a20 2468e280 8d5f5f5f 5f6d6574 612c207d 29203d3e 20286675 6e637469 + 6f6e2028 29207b20 27757365 20737472 69637427 3b202020 2468e280 8d5f696d + 706f7274 73285b5d 293b2020 20636f6e 73742020 20202020 2020636c 61726b65 + 3d27436c 61726b65 273b2468 e2808d5f 6f6e6365 2e636c61 726b6528 636c6172 + 6b65293b 5c6e5c6e 69662821 4f626a65 63742e69 7346726f 7a656e28 676c6f62 + 616c5468 69732929 7b5c6e74 68726f77 206e6577 20457272 6f722827 54686520 + 676c6f62 616c206f 626a6563 74206d75 73742062 65206672 6f7a656e 20696e20 + 616c6c20 636f6d70 6172746d 656e7473 27293b5c 6e207d5c 6e7d2928 295c6e22 + 2c225f5f 6c697665 4578706f 72744d61 705f5f22 3a7b7d2c 225f5f72 65657870 + 6f72744d 61705f5f 223a7b7d 2c225f5f 66697865 64457870 6f72744d 61705f5f + 223a7b22 636c6172 6b65223a 5b22636c 61726b65 225d7d2c 225f5f6e 65656473 + 496d706f 72744d65 74615f5f 223a6661 6c73657d + ], + location: 'index.js', + parser: 'pre-mjs-json', + record: StaticModuleRecord { + __fixedExportMap__: { + clarke: [ + 'clarke', + ], + }, + __liveExportMap__: {}, + __needsImportMeta__: false, + __reexportMap__: {}, + __syncModuleProgram__: `({ imports: $h‍_imports, liveVar: $h‍_live, onceVar: $h‍_once, importMeta: $h‍____meta, }) => (function () { 'use strict'; $h‍_imports([]); const clarke='Clarke';$h‍_once.clarke(clarke);␊ + ␊ + if(!Object.isFrozen(globalThis)){␊ + throw new Error('The global object must be frozen in all compartments');␊ + }␊ + })()␊ + `, + exports: [ + 'clarke', + ], + imports: [], + reexports: [], + }, + sha512: 'bba41dc9c98da34c1460123ec2a052acb4108bfd661560ebfef135c725ff49fbba93a8f28593cdc789b38b51c7f138aeef32fb267bdb87ca63c1676411ded460', + sourceLocation: 'file:///node_modules/clarke/index.js', + }, + }, + 'clarke-v1.0.0-n1': { + './index.js': { + bytes: Uint8Array [ + 7b22696d 706f7274 73223a5b 5d2c2265 78706f72 7473223a 5b22636c 61726b65 + 222c2264 65666175 6c74225d 2c227265 6578706f 72747322 3a5b5d2c 22736f75 + 72636522 3a222866 756e6374 696f6e20 28726571 75697265 2c206578 706f7274 + 732c206d 6f64756c 652c205f 5f66696c 656e616d 652c205f 5f646972 6e616d65 + 29207b20 6578706f 7274732e 636c6172 6b653d27 436c6172 6b65273b 5c6e5c6e + 69662821 4f626a65 63742e69 7346726f 7a656e28 676c6f62 616c5468 69732929 + 7b5c6e74 68726f77 206e6577 20457272 6f722827 54686520 676c6f62 616c206f + 626a6563 74206d75 73742062 65206672 6f7a656e 20696e20 616c6c20 636f6d70 + 6172746d 656e7473 27293b5c 6e7d202f 2f2a2f5c 6e7d295c 6e227d + ], + location: 'index.js', + parser: 'pre-cjs-json', + record: { + cjsFunctor: `(function (require, exports, module, __filename, __dirname) { exports.clarke='Clarke';␊ + ␊ + if(!Object.isFrozen(globalThis)){␊ + throw new Error('The global object must be frozen in all compartments');␊ + } //*/␊ + })␊ + //# sourceURL=file:///node_modules/cjs/node_modules/clarke/index.js␊ + `, + execute: Function noopExecute {}, + exports: [ + 'clarke', + 'default', + ], + imports: [], + reexports: [], + }, + sha512: '97055bde236eb9dc56c2b744a83ea6a9c1c85aef4077935b8f7c439ebf47f1c7dca0d49697844a67df18a50b6a745ef9a8bacf4c94da355575d550bfbac0fecd', + sourceLocation: 'file:///node_modules/cjs/node_modules/clarke/index.js', + }, + }, + 'danny-v1.0.0': { + './src/danny.js': { + bytes: Uint8Array [ + 7b22696d 706f7274 73223a5b 5d2c2265 78706f72 7473223a 5b226461 6e6e7922 + 5d2c2272 65657870 6f727473 223a5b5d 2c225f5f 73796e63 4d6f6475 6c655072 + 6f677261 6d5f5f22 3a22287b 20202069 6d706f72 74733a20 2468e280 8d5f696d + 706f7274 732c2020 206c6976 65566172 3a202468 e2808d5f 6c697665 2c202020 + 6f6e6365 5661723a 202468e2 808d5f6f 6e63652c 20202069 6d706f72 744d6574 + 613a2024 68e2808d 5f5f5f5f 6d657461 2c207d29 203d3e20 2866756e 6374696f + 6e202829 207b2027 75736520 73747269 6374273b 20202024 68e2808d 5f696d70 + 6f727473 285b5d29 3b202020 636f6e73 74202020 20202020 2064616e 6e793d27 + 44616e6e 79273b24 68e2808d 5f6f6e63 652e6461 6e6e7928 64616e6e 79293b5c + 6e5c6e69 6628214f 626a6563 742e6973 46726f7a 656e2867 6c6f6261 6c546869 + 7329297b 5c6e7468 726f7720 6e657720 4572726f 72282754 68652067 6c6f6261 + 6c206f62 6a656374 206d7573 74206265 2066726f 7a656e20 696e2061 6c6c2063 + 6f6d7061 72746d65 6e747327 293b5c6e 207d5c6e 7d292829 5c6e222c 225f5f6c + 69766545 78706f72 744d6170 5f5f223a 7b7d2c22 5f5f7265 6578706f 72744d61 + 705f5f22 3a7b7d2c 225f5f66 69786564 4578706f 72744d61 705f5f22 3a7b2264 + 616e6e79 223a5b22 64616e6e 79225d7d 2c225f5f 6e656564 73496d70 6f72744d + 6574615f 5f223a66 616c7365 7d + ], + location: 'src/danny.js', + parser: 'pre-mjs-json', + record: StaticModuleRecord { + __fixedExportMap__: { + danny: [ + 'danny', + ], + }, + __liveExportMap__: {}, + __needsImportMeta__: false, + __reexportMap__: {}, + __syncModuleProgram__: `({ imports: $h‍_imports, liveVar: $h‍_live, onceVar: $h‍_once, importMeta: $h‍____meta, }) => (function () { 'use strict'; $h‍_imports([]); const danny='Danny';$h‍_once.danny(danny);␊ + ␊ + if(!Object.isFrozen(globalThis)){␊ + throw new Error('The global object must be frozen in all compartments');␊ + }␊ + })()␊ + `, + exports: [ + 'danny', + ], + imports: [], + reexports: [], + }, + sha512: 'f91bd5735c1c6a3741cb159a3ce21e1ca69168c959c6788eb60813a79772ff45498ed5084e8aecd04e75ae36f1ccbf8d5ee85897c996bde952f66e7358825255', + sourceLocation: 'file:///node_modules/danny/src/danny.js', + }, + './src/index.js': { + bytes: Uint8Array [ + 7b22696d 706f7274 73223a5b 2264616e 6e792f73 72632f64 616e6e79 2e6a7322 + 5d2c2265 78706f72 7473223a 5b5d2c22 72656578 706f7274 73223a5b 2264616e + 6e792f73 72632f64 616e6e79 2e6a7322 5d2c225f 5f73796e 634d6f64 756c6550 + 726f6772 616d5f5f 223a2228 7b202020 696d706f 7274733a 202468e2 808d5f69 + 6d706f72 74732c20 20206c69 76655661 723a2024 68e2808d 5f6c6976 652c2020 + 206f6e63 65566172 3a202468 e2808d5f 6f6e6365 2c202020 696d706f 72744d65 + 74613a20 2468e280 8d5f5f5f 5f6d6574 612c207d 29203d3e 20286675 6e637469 + 6f6e2028 29207b20 27757365 20737472 69637427 3b202020 2468e280 8d5f696d + 706f7274 73285b5b 5c226461 6e6e792f 7372632f 64616e6e 792e6a73 5c222c20 + 5b5d5d5d 293b2020 205c6e5c 6e696628 214f626a 6563742e 69734672 6f7a656e + 28676c6f 62616c54 68697329 297b5c6e 7468726f 77206e65 77204572 726f7228 + 27546865 20676c6f 62616c20 6f626a65 6374206d 75737420 62652066 726f7a65 + 6e20696e 20616c6c 20636f6d 70617274 6d656e74 7327293b 5c6e207d 5c6e7d29 + 28295c6e 222c225f 5f6c6976 65457870 6f72744d 61705f5f 223a7b7d 2c225f5f + 72656578 706f7274 4d61705f 5f223a7b 7d2c225f 5f666978 65644578 706f7274 + 4d61705f 5f223a7b 7d2c225f 5f6e6565 6473496d 706f7274 4d657461 5f5f223a + 66616c73 657d + ], + location: 'src/index.js', + parser: 'pre-mjs-json', + record: StaticModuleRecord { + __fixedExportMap__: {}, + __liveExportMap__: {}, + __needsImportMeta__: false, + __reexportMap__: {}, + __syncModuleProgram__: `({ imports: $h‍_imports, liveVar: $h‍_live, onceVar: $h‍_once, importMeta: $h‍____meta, }) => (function () { 'use strict'; $h‍_imports([["danny/src/danny.js", []]]); ␊ + ␊ + if(!Object.isFrozen(globalThis)){␊ + throw new Error('The global object must be frozen in all compartments');␊ + }␊ + })()␊ + `, + exports: [], + imports: [ + 'danny/src/danny.js', + ], + reexports: [ + 'danny/src/danny.js', + ], + }, + sha512: 'db255297ee0330dc47dec12f0184bf72e38387e396fcc1cc8ec1f78cfad290f10e57f23fdfa50e8135a81b4bf36e4c7b14e696a95c83f1b593d34d9a1d857948', + sourceLocation: 'file:///node_modules/danny/src/index.js', + }, + }, + 'edwin-v1.0.0': { + './index.js': { + bytes: Uint8Array [ + 7b22696d 706f7274 73223a5b 5d2c2265 78706f72 7473223a 5b226564 77696e22 + 5d2c2272 65657870 6f727473 223a5b5d 2c225f5f 73796e63 4d6f6475 6c655072 + 6f677261 6d5f5f22 3a22287b 20202069 6d706f72 74733a20 2468e280 8d5f696d + 706f7274 732c2020 206c6976 65566172 3a202468 e2808d5f 6c697665 2c202020 + 6f6e6365 5661723a 202468e2 808d5f6f 6e63652c 20202069 6d706f72 744d6574 + 613a2024 68e2808d 5f5f5f5f 6d657461 2c207d29 203d3e20 2866756e 6374696f + 6e202829 207b2027 75736520 73747269 6374273b 20202024 68e2808d 5f696d70 + 6f727473 285b5d29 3b202020 636f6e73 74202020 20202020 20656477 696e3d5b + 27456464 6965272c 27456427 5d3b2468 e2808d5f 6f6e6365 2e656477 696e2865 + 6477696e 293b5c6e 7d292829 5c6e222c 225f5f6c 69766545 78706f72 744d6170 + 5f5f223a 7b7d2c22 5f5f7265 6578706f 72744d61 705f5f22 3a7b7d2c 225f5f66 + 69786564 4578706f 72744d61 705f5f22 3a7b2265 6477696e 223a5b22 65647769 + 6e225d7d 2c225f5f 6e656564 73496d70 6f72744d 6574615f 5f223a66 616c7365 + 7d + ], + location: 'index.js', + parser: 'pre-mjs-json', + record: StaticModuleRecord { + __fixedExportMap__: { + edwin: [ + 'edwin', + ], + }, + __liveExportMap__: {}, + __needsImportMeta__: false, + __reexportMap__: {}, + __syncModuleProgram__: `({ imports: $h‍_imports, liveVar: $h‍_live, onceVar: $h‍_once, importMeta: $h‍____meta, }) => (function () { 'use strict'; $h‍_imports([]); const edwin=['Eddie','Ed'];$h‍_once.edwin(edwin);␊ + })()␊ + `, + exports: [ + 'edwin', + ], + imports: [], + reexports: [], + }, + sha512: '1d433e7d9ea3eea7ffd01e469cc7960d917d6aeea0e70fde961fee0281a66c82e6d9b2d857a3fcbcf09eda52ee5a07daf8fe205f27a206ca47d08e4a7ed937d1', + sourceLocation: 'file:///node_modules/edwin/index.js', + }, + }, + 'ignored-v1.0.0': {}, + 'main-v1.0.0': { + './index.js': { + bytes: Uint8Array [ + 7b22696d 706f7274 73223a5b 22617070 225d2c22 6578706f 72747322 3a5b2241 + 7070222c 2268656c 6c6f225d 2c227265 6578706f 72747322 3a5b5d2c 225f5f73 + 796e634d 6f64756c 6550726f 6772616d 5f5f223a 22287b20 2020696d 706f7274 + 733a2024 68e2808d 5f696d70 6f727473 2c202020 6c697665 5661723a 202468e2 + 808d5f6c 6976652c 2020206f 6e636556 61723a20 2468e280 8d5f6f6e 63652c20 + 2020696d 706f7274 4d657461 3a202468 e2808d5f 5f5f5f6d 6574612c 207d2920 + 3d3e2028 66756e63 74696f6e 20282920 7b202775 73652073 74726963 74273b20 + 20206c65 74204170 703b2468 e2808d5f 696d706f 72747328 5b5b5c22 6170705c + 222c205b 5b5c222a 5c222c20 5b2468e2 808d5f61 203d3e20 28417070 203d2024 + 68e2808d 5f61292c 2468e280 8d5f6c69 76655b5c 22417070 5c225d5d 5d5d5d5d + 293b2020 205c6e5c 6e636f6e 73742020 20202020 20206865 6c6c6f3d 27776f72 + 6c64273b 2468e280 8d5f6f6e 63652e68 656c6c6f 2868656c 6c6f293b 5c6e7d29 + 28295c6e 222c225f 5f6c6976 65457870 6f72744d 61705f5f 223a7b22 41707022 + 3a5b2241 7070222c 66616c73 655d7d2c 225f5f72 65657870 6f72744d 61705f5f + 223a7b7d 2c225f5f 66697865 64457870 6f72744d 61705f5f 223a7b22 68656c6c + 6f223a5b 2268656c 6c6f225d 7d2c225f 5f6e6565 6473496d 706f7274 4d657461 + 5f5f223a 66616c73 657d + ], + location: 'index.js', + parser: 'pre-mjs-json', + record: StaticModuleRecord { + __fixedExportMap__: { + hello: [ + 'hello', + ], + }, + __liveExportMap__: { + App: [ + 'App', + false, + ], + }, + __needsImportMeta__: false, + __reexportMap__: {}, + __syncModuleProgram__: `({ imports: $h‍_imports, liveVar: $h‍_live, onceVar: $h‍_once, importMeta: $h‍____meta, }) => (function () { 'use strict'; let App;$h‍_imports([["app", [["*", [$h‍_a => (App = $h‍_a),$h‍_live["App"]]]]]]); ␊ + ␊ + const hello='world';$h‍_once.hello(hello);␊ + })()␊ + `, + exports: [ + 'App', + 'hello', + ], + imports: [ + 'app', + ], + reexports: [], + }, + sha512: 'ee859eb35ed3baa77ade9eb727db0f31e43628c8318f45c0f6cecd65c02298ca8791baed26aa8262c394e36ccd431f496a108e749c12c550e35d17246a8b5cd2', + sourceLocation: 'file:///index.js', + }, + }, + }, + } diff --git a/packages/endomoat/test/policy-gen/snapshots/load-compartment-map.spec.js.snap b/packages/endomoat/test/policy-gen/snapshots/load-compartment-map.spec.js.snap new file mode 100644 index 0000000000..eb7e3d0a99 Binary files /dev/null and b/packages/endomoat/test/policy-gen/snapshots/load-compartment-map.spec.js.snap differ diff --git a/packages/endomoat/test/policy-gen/snapshots/policy-generator.spec.js.md b/packages/endomoat/test/policy-gen/snapshots/policy-generator.spec.js.md new file mode 100644 index 0000000000..9b0bd0c1d9 --- /dev/null +++ b/packages/endomoat/test/policy-gen/snapshots/policy-generator.spec.js.md @@ -0,0 +1,11 @@ +# Snapshot report for `test/policy-gen/policy-generator.spec.js` + +The actual snapshot is saved in `policy-generator.spec.js.snap`. + +Generated by [AVA](https://avajs.dev). + +## buildModuleRecords() - result is deterministic + +> Snapshot 1 + + '[{"file":"/index.js","importMap":{"app":"/node_modules/app/main.js"},"packageName":"$root$","specifier":"/index.js","type":"js"},{"file":"/node_modules/app/main.js","importMap":{"app":"/node_modules/app/main.js","avery":"/node_modules/avery/avery.js","brooke":"/node_modules/brooke/brooke.js","cjs":"/node_modules/cjs/index.js","clarke":"/node_modules/clarke/index.js","danny":"/node_modules/danny/src/index.js"},"packageName":"app","specifier":"/node_modules/app/main.js","type":"js"},{"file":"/node_modules/avery/avery.js","importMap":{},"packageName":"avery","specifier":"/node_modules/avery/avery.js","type":"js"},{"file":"/node_modules/brooke/brooke.js","importMap":{"clarke":"/node_modules/clarke/index.js"},"packageName":"brooke","specifier":"/node_modules/brooke/brooke.js","type":"js"},{"file":"/node_modules/cjs/index.js","importMap":{"clarke":"/node_modules/cjs/node_modules/clarke/index.js"},"packageName":"cjs","specifier":"/node_modules/cjs/index.js","type":"js"},{"file":"/node_modules/cjs/node_modules/clarke/index.js","importMap":{},"packageName":"clarke","specifier":"/node_modules/cjs/node_modules/clarke/index.js","type":"js"},{"file":"/node_modules/clarke/index.js","importMap":{},"packageName":"clarke","specifier":"/node_modules/clarke/index.js","type":"js"},{"file":"/node_modules/danny/src/index.js","importMap":{"danny/src/danny.js":"/node_modules/danny/src/danny.js"},"packageName":"danny","specifier":"/node_modules/danny/src/index.js","type":"js"},{"file":"/node_modules/danny/src/danny.js","importMap":{},"packageName":"danny","specifier":"/node_modules/danny/src/danny.js","type":"js"},{"file":"/node_modules/edwin/index.js","importMap":{},"packageName":"edwin","specifier":"/node_modules/edwin/index.js","type":"js"}]' diff --git a/packages/endomoat/test/policy-gen/snapshots/policy-generator.spec.js.snap b/packages/endomoat/test/policy-gen/snapshots/policy-generator.spec.js.snap new file mode 100644 index 0000000000..c449e394ec Binary files /dev/null and b/packages/endomoat/test/policy-gen/snapshots/policy-generator.spec.js.snap differ