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 Apr 16, 2024
1 parent bba758a commit 2c98dd6
Show file tree
Hide file tree
Showing 28 changed files with 2,787 additions and 201 deletions.
450 changes: 266 additions & 184 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/endomoat/package.json
Expand Up @@ -42,6 +42,7 @@
"@endo/compartment-mapper": "1.1.4",
"@endo/evasive-transform": "1.0.4",
"@types/node": "18.19.28",
"json-stable-stringify": "1.1.1",
"lavamoat-core": "^15.3.0",
"ses": "1.4.1",
"type-fest": "4.14.0",
Expand All @@ -50,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.8.1"
},
"publishConfig": {
Expand Down
73 changes: 71 additions & 2 deletions packages/endomoat/src/cli.js
Expand Up @@ -17,9 +17,16 @@ import assert from 'node:assert'
import path from 'node:path'
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
import { constants, loadPolicies, run } from './index.js'
import {
constants,
generateAndWritePolicy,
loadPolicies,
run,
} from './index.js'
import { readJsonFile } from './util.js'

const BEHAVIOR_GROUP = 'Behavior Options:'

/**
* Main entry point to CLI
*/
Expand Down Expand Up @@ -169,8 +176,70 @@ async function main(args = hideBin(process.argv)) {
await run(argv.entrypoint, policy)
}
)
.demandCommand(1)
.command(
['gen <entrypoint>', 'generate <entrypoint>'],
'Generate policy files; overwrites existing policies',
(yargs) =>
yargs
.options({
run: {
describe: 'Run the application after policy generated',
type: 'boolean',
group: BEHAVIOR_GROUP,
},
debug: {
type: 'boolean',
describe: 'Additionally write a debug policy',
group: BEHAVIOR_GROUP,
},
})
.positional('entrypoint', {
describe: 'Path to the application entry point',
type: 'string',
normalize: true,
coerce: path.resolve,
})
.demandOption('entrypoint')
/**
* Resolve entrypoint from `cwd`
*/
.middleware((argv) => {
argv.entrypoint = path.resolve(argv.cwd, argv.entrypoint)
}, true)
/**
* This should not fail. If it does, there is a bug.
*/
.check((argv) => {
assert(
path.isAbsolute(argv.entrypoint),
'entrypoint must be an absolute path'
)
return true
}),
async ({
entrypoint,
debug,
run: shouldRun,
policy: policyPath,
'policy-debug': policyDebugPath,
}) => {
const policy = await generateAndWritePolicy(entrypoint, {
debug,
policyPath,
policyDebugPath,
})

if (debug) {
console.error(`Wrote debug policy to ${policyDebugPath}`)
}
console.error(`Wrote policy to ${policyPath}`)

if (shouldRun) {
await run(entrypoint, policy)
}
}
)
.demandCommand(1)
.parse()
}

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'
48 changes: 43 additions & 5 deletions packages/endomoat/src/index.js
Expand Up @@ -20,23 +20,61 @@ import { pathToFileURL } from 'node:url'
import { importHook } from './import-hook.js'
import { moduleTransforms } from './module-transforms.js'
import { toEndoPolicy } from './policy-converter.js'
import { defaultReadPowers } from './power.js'
import { generatePolicy } from './policy-gen/index.js'
import { isPolicy } from './policy.js'
import { makeReadPowers } from './power.js'

export * as constants from './constants.js'
export { generateAndWritePolicy, generatePolicy } from './policy-gen/index.js'
export { loadPolicies } from './policy.js'
export { toEndoPolicy }

/**
* Runs a program in Endomoat with the provided policy
* Runs Endomoat with provided policy
*
* @param {string | URL} entrypointPath
* @overload
* @param {string | URL} entryFile
* @param {import('lavamoat-core').LavaMoatPolicy} policy
* @param {import('./types.js').RunOptions} [opts]
* @returns {Promise<unknown>}
*/
export async function run(entrypointPath, policy, opts = {}) {

/**
* Runs Endomoat with an auto-generated policy, optionally writing to disk
*
* @overload
* @param {string | URL} entryFile
* @param {import('./types.js').GenerateAndRunOptions} [opts]
* @returns {Promise<unknown>}
*/

/**
* Runs a program in Endomoat
*
* @param {string | URL} entrypointPath
* @param {import('lavamoat-core').LavaMoatPolicy
* | import('./types.js').GenerateAndRunOptions} [policyOrOpts]
* @param {import('./types.js').RunOptions} [opts]
* @returns {Promise<unknown>}
*/
export async function run(entrypointPath, policyOrOpts = {}, opts = {}) {
await Promise.resolve()
/** @type {import('lavamoat-core').LavaMoatPolicy} */
let policy
/** @type {import('./types.js').RunOptions} */
let runOpts

if (isPolicy(policyOrOpts)) {
policy = policyOrOpts
runOpts = opts
} else {
const generateOpts = policyOrOpts
runOpts = { readPowers: generateOpts.readPowers }
policy = await generatePolicy(entrypointPath, generateOpts)
}

const endoPolicy = toEndoPolicy(policy)
const { readPowers = defaultReadPowers } = opts
const readPowers = makeReadPowers(runOpts.readPowers)

const url =
entrypointPath instanceof URL
Expand Down
196 changes: 196 additions & 0 deletions packages/endomoat/src/policy-gen/index.js
@@ -0,0 +1,196 @@
/**
* Provides Lavamoat policy generation facilities via {@link generatePolicy}
*
* @packageDocumentation
*/
import { loadCompartmentMapForArchive } from '@endo/compartment-mapper'
import assert from 'node:assert'
import nodeFs from 'node:fs'
import path from 'node:path'
import { fileURLToPath, 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 { 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').GenerateOptions & { debug: true }} opts
* @returns {Promise<import('lavamoat-core').LavaMoatPolicyDebug>}
*/
/**
* Generates a LavaMoat policy from a given entry point using
* `@endo/compartment-mapper`
*
* @overload
* @param {string | URL} entrypointPath
* @param {import('./types.js').GenerateOptions} [opts]
* @returns {Promise<import('lavamoat-core').LavaMoatPolicy>}
*/

/**
* Generates a LavaMoat policy or debug policy from a given entry point using
* `@endo/compartment-mapper`
*
* @param {string | URL} entrypointPath
* @param {import('./types.js').GenerateOptions} [opts]
* @returns {Promise<import('lavamoat-core').LavaMoatPolicy>}
*/
async function generate(
entrypointPath,
{ readPowers: powers, debug = false, policyOverride, ...archiveOpts } = {}
) {
const readPowers = makeReadPowers(powers)

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` and writes the result to disk
*
* @param {string | URL} entrypointPath
* @param {import('./types.js').GeneratePolicyOptions} [opts]
* @returns {Promise<import('lavamoat-core').LavaMoatPolicy>}
* @public
*/
export async function generateAndWritePolicy(entrypointPath, opts = {}) {
return generatePolicy(entrypointPath, { write: true, ...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').GeneratePolicyOptions} [opts]
* @returns {Promise<import('lavamoat-core').LavaMoatPolicy>}
* @public
*/
export async function generatePolicy(entrypointPath, opts = {}) {
const {
policyDebugPath = path.resolve(DEFAULT_POLICY_DEBUG_PATH),
policyPath = path.resolve(DEFAULT_POLICY_PATH),
fs = nodeFs,
write: shouldWrite = false,
...generateOpts
} = opts

if (entrypointPath instanceof URL) {
entrypointPath = fileURLToPath(entrypointPath)
}
assert(
path.isAbsolute(entrypointPath),
`entrypointPath must be an absolute path; got ${entrypointPath}`
)
if (shouldWrite) {
assert(
path.isAbsolute(policyPath),
`policyPath must be an absolute path; got ${policyPath}`
)
if (policyDebugPath) {
assert(
path.isAbsolute(policyDebugPath),
`policyDebugPath must be an absolute path; got ${policyDebugPath}`
)
}
}

await Promise.resolve()

/** @type {import('lavamoat-core').LavaMoatPolicy} */
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 (shouldWrite && generateOpts.debug) {
const debugPolicy = await generate(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
} else {
policy = await generate(entrypointPath, generateOpts)
}

if (shouldWrite) {
// XXX: do we need to merge policies here?
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 loadCompartmentMapForArchive({
dev: true,
...archiveOpts,
readPowers,
moduleLocation,
importHook,
moduleTransforms,
})

// `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,
}
}

0 comments on commit 2c98dd6

Please sign in to comment.