Using Policies
This guide will help you understand what a LavaMoat Policy is and how to use it.
What’s a Policy?
Section titled “What’s a Policy?”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 treepolicy-override.json- manually created by you, to expand or limit access to resources
Policy, by Example
Section titled “Policy, by Example”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.jsWhen generating a Policy from this application, LavaMoat will write the following to ./lavamoat/node/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 } } }}Trusted vs. Untrusted Applications
Section titled “Trusted vs. Untrusted Applications”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.
Fields of policy.json
Section titled “Fields of policy.json”This section is an overview of the fields of policy.json.
resources
Section titled “resources”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 and builtin
Section titled “packages and builtin”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
Section titled “globals”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.
native
Section titled “native”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!
root.usePolicy
Section titled “root.usePolicy”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:
{ "resources": { "webpack": { "globals": { "console": true } } }, "root": { "usePolicy": "webpack" }}In this situation, webpack’s executable is the entry point.
include
Section titled “include”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.
Using include with @lavamoat/node
Section titled “Using include with @lavamoat/node”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/nodeemits a warning during which references packageyargs. After this, you run your app via@lavamoat/node, and it fails with an exception explaining thatyargsattempted to importcliui, which was disallowed.To solve this, create an
includeentry:policy-override.json {"include": ["yargs>cliui"],"resources": {"yargs": {"packages": {"yargs>cliui": true}}}}This uses the
stringform ofinclude; it assumes a dynamic import ofcliui’s “main” (or.) export.It’s not enough to only provide the
includefield! The corresponding package Policy granting access toyargs>cliuifromyargsis also needed, since static analysis cannot determine the relationship betweenyargsandcliui;includeonly tells LavaMoat it needs to analyzecliui. -
Subpath Module Imports
-
If a non-“main” module is imported—such as
./builds/lib/string-utils.js—theincludeentry will have anamefield corresponding to the resource2, and amodulesarray 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,
webpackwill want to load yourwebpack.config.jsfile, but your application surely isn’t a dependency ofwebpack! Becausewebpackwill 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
includeentry can instead provide alocationfield (instead ofname), which is a relative path from the project root to apackage.json-containing directory. Be sure to allow access tomy-app4 viawebpack’spackagesproperty:policy-override.json {"include": [{"location": ".","modules": ["./webpack.config.js"]}],"resources": {"webpack": {"packages": {"my-app": true}}}}Here, we’re running the untrusted
webpackvia@lavamoat/node’s--binflag.my-appis the name of your application (as per itspackage.json), and thelocationfield in theincludeentry references the directory containing your application’spackage.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.jswill be.Only those modules modules appearing in the
modulesarray will be accessible bywebpack.webpackwon’t have access to any other file inmy-app(such asapp.js, or whatever your “main” module is) unless it appears in themodulesarray5.This form of
includeis 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.
Overriding Auto-Generated Policies
Section titled “Overriding Auto-Generated Policies”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.
Example: Granting Access to Resources
Section titled “Example: Granting Access to Resources”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).
{ "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.
Example: Revoking Access to Resources
Section titled “Example: Revoking Access to Resources”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
assertbyentropoetry - Deny access to
consolewith the exception ofconsole.log
{ "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:
- Revoke access to
consoleusingconsole: false - Explicitly grant access to the
console.logfunction
Policies Are Not Deny-Lists
Section titled “Policies Are Not Deny-Lists”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:
"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:
"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:
"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.
Footnotes
Section titled “Footnotes”-
It’s worth noting that with
@lavamoat/webpack, lazily-loaded chunks are supported but are always known at compile-time; thus,@lavamoat/webpackhas no need forinclude. ↩ -
If an
includeentry has anamefield butmodulesis omitted or empty, LavaMoat will assume the “main” module is being imported. Since this is the most common use-case, thestringform is allowed for convenience. ↩ -
Relative paths are computed relative to the project root; absolute paths should never appear in Policy. ↩
-
Your application—e.g.,
my-appperpackage.json—is not treated as a trustedapplication; it’s a resource like any other. Whilewebpack.config.jsmay be “your code”, it is not considered the entry point (webpackis) and thus cannot be considered trusted. ↩ -
…or is otherwise directly depended upon by a resource, which is highly unlikely unless you’ve created such a relationship via Policy Overrides. ↩