Skip to content

Commit

Permalink
feat(endomoat): add policy generation [ci skip]
Browse files Browse the repository at this point in the history
This adds policy generation for Endomoat.

It does not yet have support for writable globals.
  • Loading branch information
boneskull committed Mar 6, 2024
1 parent 65f45eb commit 0b16716
Show file tree
Hide file tree
Showing 18 changed files with 1,967 additions and 3 deletions.
6 changes: 6 additions & 0 deletions packages/endomoat/package.json
Expand Up @@ -16,6 +16,10 @@
"node": "^16.20.0 || ^18.0.0 || ^20.0.0",
"npm": ">=7.0.0"
},
"imports": {
"#policy-gen": "./src/policy-gen/index.js",
"#policy-gen/*": "./src/policy-gen/*.js"
},
"exports": {
".": {
"types": "./types/index.d.ts",
Expand All @@ -39,6 +43,7 @@
"@endo/compartment-mapper": "1.1.2",
"@endo/evasive-transform": "1.0.4",
"@types/node": "18.19.20",
"json-stable-stringify": "1.1.1",
"lavamoat-core": "^15.0.0",
"ses": "1.3.0",
"type-fest": "4.10.3",
Expand All @@ -47,6 +52,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": {
Expand Down
4 changes: 3 additions & 1 deletion packages/endomoat/src/constants.js
Expand Up @@ -3,7 +3,6 @@
*
* _Type a string more than once? Make it a constant!_
*/

import path from 'node:path'

export const DEFAULT_POLICY_PATH = path.join(
Expand All @@ -27,3 +26,6 @@ export const LAVAMOAT_PKG_POLICY_ROOT = '$root$'
export const RSRC_POLICY_PKGS = 'packages'
export const RSRC_POLICY_BUILTINS = 'builtins'
export const RSRC_POLICY_GLOBALS = 'globals'

export const LMR_TYPE_BUILTIN = 'builtin'
export const LMR_TYPE_SOURCE = 'js'
1 change: 1 addition & 0 deletions packages/endomoat/src/index.js
Expand Up @@ -10,6 +10,7 @@ import { defaultReadPower } from './power.js'

export * from './constants.js'
export * from './policy-converter.js'
export { generatePolicy } from './policy-gen/index.js'

/**
* Returns `true` if `value` is a {@link RunOptionsWithEndoPolicy}
Expand Down
100 changes: 100 additions & 0 deletions packages/endomoat/src/policy-gen/index.js
@@ -0,0 +1,100 @@
/**
* Provides Lavamoat policy generation facilities via {@link generatePolicy}
*
* @packageDocumentation
*/

import {
loadCompartmentForArchive,
makeArchiveCompartmentMap,
} from '@endo/compartment-mapper'
import { pathToFileURL } from 'node:url'
import { importHook } from '../import-hook.js'
import { moduleTransforms } from '../module-transforms.js'
import { defaultReadPower } from '../power.js'
import { PolicyGenerator } from './policy-generator.js'

const { fromEntries, entries } = Object

/**
* @typedef {import('type-fest').Merge<
* Omit<import('./policy-generator.js').PolicyGeneratorOptions, 'read'>,
* {
* readPowers?:
* | import('@endo/compartment-mapper').ReadFn
* | import('@endo/compartment-mapper').ReadPowers
* }
* >} GeneratePolicyOptions
*/

/**
* Generates a LavaMoat policy from a given entry point using
* `@endo/compartment-mapper`
*
* @param {string | URL} entrypointPath
* @param {GeneratePolicyOptions} opts
* @returns {Promise<import('lavamoat-core').LavaMoatPolicy>}
*/
export async function generatePolicy(
entrypointPath,
{ readPowers = defaultReadPower, debug = false, policyOverride } = {}
) {
const { compartmentMap, sources, renames } = await loadCompartmentMap(
entrypointPath,
{
readPowers,
}
)

const read = typeof readPowers === 'function' ? readPowers : readPowers.read

return await PolicyGenerator.generatePolicy(
compartmentMap,
sources,
renames,
{ debug, read, policyOverride }
)
}

/**
* Loads compartment map and associated sources.
*
* @param {string | URL} entrypointPath
* @param {{
* readPowers?:
* | import('@endo/compartment-mapper').ReadFn
* | import('@endo/compartment-mapper').ReadPowers
* }} opts
*/
export async function loadCompartmentMap(
entrypointPath,
{ readPowers = defaultReadPower } = {}
) {
const url =
entrypointPath instanceof URL
? `${entrypointPath}`
: `${pathToFileURL(entrypointPath)}`

const { compartmentMap, sources } = await loadCompartmentForArchive({
readPowers: readPowers,
moduleLocation: url,
importHook,
moduleTransforms,
dev: true,
})

const { archiveCompartmentMap, archiveSources, compartmentRenames } =
makeArchiveCompartmentMap(compartmentMap, sources)

// `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,
}
}
57 changes: 57 additions & 0 deletions 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<string, LavamoatModuleRecord>} */
#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()
}
}

0 comments on commit 0b16716

Please sign in to comment.