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 26, 2024
1 parent 03e0424 commit 1304ad7
Show file tree
Hide file tree
Showing 22 changed files with 2,302 additions and 1 deletion.
5 changes: 5 additions & 0 deletions packages/endomoat/package.json
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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": {
Expand Down
11 changes: 10 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'

/**
Expand Down Expand Up @@ -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'
1 change: 1 addition & 0 deletions packages/endomoat/src/index.js
Expand Up @@ -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 }

Expand Down
176 changes: 176 additions & 0 deletions 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<import('lavamoat-core').LavaMoatPolicyDebug>}
* @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<import('lavamoat-core').LavaMoatPolicy>}
* @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<import('lavamoat-core').LavaMoatPolicy>}
* @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<import('lavamoat-core').LavaMoatPolicy>}
* @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,
}
}
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 1304ad7

Please sign in to comment.