Skip to content

Using Policies

This guide will help you understand what a LavaMoat Policy is and how to use it.

A Policy is an object that describes which resources any given dependency can access. These “resources” include globals, builtins (e.g., node:fs), native modules, and other packages. All direct and transitive dependencies in your application’s dependency tree are subject to a Policy.

The behavior of your application is not restricted by the Policy. A Policy only applies to direct and transitive dependencies of your application.

Policies are fundamental to LavaMoat’s operation.

Policy Files: policy.json and policy-override.json

Section titled “Policy Files: policy.json and policy-override.json”

When getting stared with LavaMoat, you’ll first generate a Policy file using the LavaMoat CLI. To do this, LavaMoat crawls your application’s dependency tree and determines which resources each dependency is currently using. In other words, auto-generating a Policy takes a snapshot of the permissions at a point in time. LavaMoat writes this policy to policy.json.

At some point, you’ll want to add, update, or remove dependencies. When you do, you’ll want to have LavaMoat re-generate policy.json—another snapshot.

You should manually review policy.json at time of creation and whenever changed; it may surface dubious permissions. For example, the policy may show that package purporting to format strings is sneakily using fs.writeFile() for evil!

Do not edit policy.json by hand. Instead, you can revoke permission to fs.writeFile() by creating a policy-override.json file (details below).

Due to the dynamic nature of JavaScript, automatic Policy generation is imperfect. If a package lacks permission to a resource that it should have, you may manually grant it the necessary permissions by editing policy-override.json.

In summary, the Policy files are:

  • policy.json - automatically generated by LavaMoat, reflecting the resources accessed by packages in your dependency tree
  • policy-override.json - manually created by you, to expand or limit access to resources

Say we generate a Policy file for an application with the following dependency tree, where app.js is the entry point:

app.js
└─┬ some-package
└─┬ entropoetry
└── bn.js

When generating a Policy from this application, LavaMoat will write the following to ./lavamoat/node/policy.json:

policy.json
{
"resources": {
"some-package": {
"globals": {
"Buffer.from": true
},
"packages": {
"some-package>entropoetry": true
}
},
"some-package>entropoetry": {
"builtin": {
"assert": true,
"buffer.Buffer": true,
"zlib": true
},
"globals": {
"console": true,
"process.exitCode": "write"
},
"packages": {
"some-package>entropoetry>bn.js": true
}
},
"some-package>entropoetry>bn.js": {
"builtin": {
"buffer.Buffer": true
},
"globals": {
"Buffer": true
}
}
}
}

When running your own application under LavaMoat, it is given free access to all resources (globals, built-ins, packages, etc.). This is a trusted application; a Policy for such an app will not contain an entry in resources for your application.

However, when running a third-party application under @lavamoat/node—such as a linting or build tool—the app is considered untrusted. A generated Policy will contain a root field which declares a named resource as the “root” of the application, and the resource is treated as any other dependency, with an explicit Policy limiting access by the principle of least privilege.

An app running under @lavamoat/node will always have either zero (0) or one (1) trusted package, which can only correspond to the entry point. All other packages are considered untrusted and must explicitly be granted access to resources via Policy.

This section is an overview of the fields of policy.json.

resources contains all importable packages in your dependency graph.

The keys of resources are the canonical names of packages as determined by LavaMoat. In order to identify a package, LavaMoat cannot not use the package name as published, but instead generates a unique name by tracing the package’s shortest route through its dependents within the dependency graph. This prevents a malicious package which shares the same name as a published package from hijacking permissions.

packages contains all packages accessible by the dependency. In this example, some-package has access to entropoetry. This means that some-package can, for example, require('entropoetry').

Likewise, entropoetry has assert in its builtin property. This means entropoetry can require('assert') - a built-in module of Node.js.

globals contains all platform APIs and global variables accessible by the dependency. In this example, entropoetry can access to the global console, but it can write to process.exitCode. A value of true grants read-only access to a global; write access can be granted by setting the value to write.

Since granting write access to globals is potentially dangerous, write access is represented by a string to make it stand out when inspecting the policy file.

Not shown in the example above, native is a boolean flag that indicates a package is allowed to load a native Node.js module. Any auto-generated occurrence of native should be reviewed carefully—a native module is a powerful resource!

The root.usePolicy field is used to declare a named resource as the untrusted entry point of the application. An example Policy may look like this:

policy.json
{
"resources": {
"webpack": {
"globals": {
"console": true
}
}
},
"root": {
"usePolicy": "webpack"
}
}

In this situation, webpack’s executable is the entry point.

Sometimes a dependency may be missed by policy generation, typically due to it being dynamically imported. If a package is missed in this way, none of its dependencies will be crawled, and thus omitted from policy entirely. Your app will most definitely break at runtime!

include offers an escape hatch to “force” a package or specific module to be included during Policy generation1.

In @lavamoat/node, when dynamic imports (or requires) are encountered, @lavamoat/node emits a warning with a link to the specific line of code causing the issue. At runtime, @lavamoat/node will fail with an exception which mentions the name of the disallowed, dynamically-imported package. In tandem, this information can help you create include entries to solve the problem.

include can be specified using three (3) different forms, which are described below.

Basic Dynamic Imports & Requires

Consider this situation:

You’re generating policy for your app and @lavamoat/node emits a warning during which references package yargs. After this, you run your app via @lavamoat/node, and it fails with an exception explaining that yargs attempted to import cliui, which was disallowed.

To solve this, create an include entry:

policy-override.json
{
"include": ["yargs>cliui"],
"resources": {
"yargs": {
"packages": {
"yargs>cliui": true
}
}
}
}

This uses the string form of include; it assumes a dynamic import of cliui’s “main” (or .) export.

It’s not enough to only provide the include field! The corresponding package Policy granting access to yargs>cliui from yargs is also needed, since static analysis cannot determine the relationship between yargs and cliui; include only tells LavaMoat it needs to analyze cliui.

Subpath Module Imports

If a non-“main” module is imported—such as ./builds/lib/string-utils.js—the include entry will have a name field corresponding to the resource2, and a modules array containing relative path(s)3 to the imported file(s):

policy-override.json
{
"include": [
{
"name": "yargs>cliui",
"modules": ["./builds/lib/string-utils.js"]
}
],
"resources": {
"yargs": {
"packages": {
"yargs>cliui": true
}
}
}
}

Dynamic Imports of Arbitrary Modules

In certain cases, a package will dynamically import a module which isn’t present in its own dependencies, such as a configuration file.

For example, webpack will want to load your webpack.config.js file, but your application surely isn’t a dependency of webpack! Because webpack will be treated as an untrusted application under @lavamoat/node, it only has access to resources to which it’s explicitly been granted—and “your code” isn’t among them.

For this situation, an include entry can instead provide a location field (instead of name), which is a relative path from the project root to a package.json-containing directory. Be sure to allow access to my-app4 via webpack’s packages property:

policy-override.json
{
"include": [
{
"location": ".",
"modules": ["./webpack.config.js"]
}
],
"resources": {
"webpack": {
"packages": {
"my-app": true
}
}
}
}

Here, we’re running the untrusted webpack via @lavamoat/node’s --bin flag. my-app is the name of your application (as per its package.json), and the location field in the include entry references the directory containing your application’s package.json, relative to the project root (which happens to be the same directory).

Thus, your application’s entry point will not be crawled during Policy generation, but webpack.config.js will be.

Only those modules modules appearing in the modules array will be accessible by webpack. webpack won’t have access to any other file in my-app (such as app.js, or whatever your “main” module is) unless it appears in the modules array5.

This form of include is useful for tools which load configuration files (e.g., ESLint, Webpack, TypeScript and the usual suspects).

Regardless of which form an include entry takes, any package and/or module referenced therein will will cause @lavamoat/node’s Policy generation to analyze it and any modules it can reach. This will be reflected in the resulting Policy file.

Don’t forget to grant access to the include’d package via the requesting resource’s packages property!

Limiting Property Access in Powerful Objects

Section titled “Limiting Property Access in Powerful Objects”

Sometimes a package needs a very specific functionality from a globally-available reference or a builtin. Yet, there’s a meaningful difference between granting a package access to window.location.hash vs. the entire window.location object; likewise fs.readFile vs. the entire fs.

You may have noticed the “dot notation” in keys within globals and builtins—this limits access to a subset of fields within a resource. This is how a policy can effectively limit field access in powerful objects.

In the case of global resources, the "Buffer.from": true in the example Policy means the Buffer global object will exist in the package’s scope, but will only contain the from property and no other fields.

The "buffer.Buffer": true in the builtin property means that a package accessing buffer via const buffer = require('buffer') it will receive an object with only one property: the Buffer constructor.

LavaMoat can often generate precise lists of properties used by the package. If needed, you may restrict these further by creating a policy-override.json file.

The behavior of a Policy at runtime depends on the auto-generated policy.json, and an optional policy-override.json. In this section, we will guide you through working with policy-override.json.

A policy-override.json has the same structure as a policy.json. At runtime, LavaMoat will merge policy-override.json into policy.json, overwriting the latter with the former.

JavaScript is dynamic enough that certain constructs cannot be inferred statically. Because of this, sometimes permissions may be missing from the auto-generated policy.json file. If such missing permissions are identified, it’s best to double-check they are needed before adding them policy-override.json.

In the following example, we will grant entropoetry access to the global URL class and expand access to the entire buffer builtin (Node.js).

./lavamoat/node/policy-override.json
{
"resources": {
"some-package>entropoetry": {
"globals": {
"URL": true
}
},
"some-package>entropoetry>bn.js": {
"builtin": {
"buffer": true
}
}
}
}

The above policy override will be merged at runtime with the auto-generated policy.json file; in addition to the permissions granted above, entropoetry will retain read access to the the global console object and write access to the global process.exitCode.

If LavaMoat’s static analysis believes that a package refers to a certain resource, but your project does not actually use this resource, it’s reasonable to deny the dependency access to this resource. You can achieve that with the following example.

In this example, we’ll:

  • Forbid requiring of assert by entropoetry
  • Deny access to console with the exception of console.log
./lavamoat/node/policy-override.json
{
"resources": {
"some-package>entropoetry": {
"builtin": {
"assert": false
},
"globals": {
"console": false,
"console.log": true
}
}
}
}

The auto-generated policy.json grants entropoetry full access to the global console object. For this reason, we need to use a two-step process:

  1. Revoke access to console using console: false
  2. Explicitly grant access to the console.log function

In other words, explicitly allowing access to an entire item and then attempting to narrow it down by denying specific fields is unsupported. If you want to allow allow access to a.b, you might try this:

Busted Example
"globals": {
"a": true,
"a.b": false
}

It does not work. a.b will remain accessible because a was declared as accessible in its entirety.

To properly deny access to a.b—while allowing access to other fields of a—you’d need an allow-list style policy like this:

Correct Example
"globals": {
"a.c": true,
"a.d": true
}

More specifically, the deepest chain of properties needs to point to an allowed reference and skipping all-but-necessary access on higher levels is possible, so the following will work, and allow a.z and a.b.c—but not a.b.y:

Deep Property Example
"globals": {
"a": true,
"a.b": false,
"a.b.c": true
}

For more examples, see lavamoat-core’s globals.spec.js.

You shouldn’t often need this level of complexity! LavaMoat will not burden you by generating negative entries such as this.


  1. It’s worth noting that with @lavamoat/webpack, lazily-loaded chunks are supported but are always known at compile-time; thus, @lavamoat/webpack has no need for include. ↩

  2. If an include entry has a name field but modules is omitted or empty, LavaMoat will assume the “main” module is being imported. Since this is the most common use-case, the string form is allowed for convenience. ↩

  3. Relative paths are computed relative to the project root; absolute paths should never appear in Policy. ↩

  4. Your application—e.g., my-app per package.json—is not treated as a trustedapplication; it’s a resource like any other. While webpack.config.js may be “your code”, it is not considered the entry point (webpack is) and thus cannot be considered trusted. ↩

  5. …or is otherwise directly depended upon by a resource, which is highly unlikely unless you’ve created such a relationship via Policy Overrides. ↩