-
Notifications
You must be signed in to change notification settings - Fork 65
/
policyGenerator.js
135 lines (129 loc) · 5.12 KB
/
policyGenerator.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
const {
createModuleInspector,
LavamoatModuleRecord,
loadPoliciesSync,
// @ts-expect-error - missing types
} = require('lavamoat-core')
const { getPackageNameForModulePath } = require('@lavamoat/aa')
const { writeFileSync, mkdirSync } = require('node:fs')
const path = require('node:path')
const stringify = require('json-stable-stringify')
const {
sources: { RawSource },
} = require('webpack')
const POLICY_SNAPSHOT_FILENAME = 'policy-snapshot.json'
module.exports = {
/**
* @param {Object} opts
* @param {import('../types.js').Policy} [opts.policyFromOptions] - The
* hardcoded policy passed in options, takes precedence over reading from
* files
* @param {import('@lavamoat/aa').CanonicalNameMap} opts.canonicalNameMap -
* Generated from aa
* @param {import('webpack').Compilation} opts.compilation - Webpack
* compilation reference (for emitting assets)
* @param {boolean} opts.enabled - Whether to generate a policy
* @param {string} opts.location - Where to read/write the policy files
* @param {boolean} [opts.emit] - Whether to emit the policy snapshot as an
* asset
* @returns
*/
createPolicyGenerator({
policyFromOptions,
canonicalNameMap,
compilation,
enabled,
location,
emit = false,
}) {
const { policy, applyOverride } = loadPoliciesSync({
policyPath: path.join(location, 'policy.json'),
policyOverridePath: path.join(location, 'policy-override.json'),
debugMode: false,
})
if (!enabled)
return {
inspectWebpackModule: () => {},
getPolicy: () => {
let final
if (policyFromOptions) {
// TODO: avoid loading the policy file if policyFromOptions is present
final = policyFromOptions
} else if (policy) {
final = applyOverride(policy)
}
if (emit) {
compilation.emitAsset(
POLICY_SNAPSHOT_FILENAME,
new RawSource(stringify(final, { space: 2 }))
)
}
return final
},
}
// load policy file
// load overrides
// generate policy if requested and write to fileWe're not aware of any
// merge result with overrides
// return that and emit snapshot
const moduleInspector = createModuleInspector({
isBuiltin: () => false,
includeDebugInfo: false,
// If the specifier is requested as a dependency in importMap but was never passed to inspectModule, its package name will be looked up here.
// This is a workaround to inconsistencies in how webpack represents connections.
// Specifically what happened to surface this issue: `connections` in webpack contain a module entry that's identified by the path to its index.mjs entrypoint while the index.js entrypoint is actually used in the bundle, so inspector doesn't have a cached entry for this one and without the fallback handler would return <unknown:/...>
// There should be no security implications of this, since the package is already resolved clearly and this is only a part of policy generation, not runtime.
moduleToPackageFallback: (specifier) =>
getPackageNameForModulePath(canonicalNameMap, specifier),
})
return {
/**
* @param {import('webpack').NormalModule} module
* @param {Iterable<import('webpack').ModuleGraphConnection>} connections
*/
inspectWebpackModule: (module, connections) => {
// process._rawDebug(module.originalSource().source())
const moduleRecord = new LavamoatModuleRecord({
// Knowing the actual specifier is not relevant here, they're used as unique identifiers that match between here and dependencies
specifier: module.userRequest,
file: module.userRequest,
type: 'js',
packageName: getPackageNameForModulePath(
canonicalNameMap,
module.userRequest
),
content: module.originalSource()?.source()?.toString(),
importMap: {
// connections are a much better source of information than module.dependencies which contain
// all imported references separately along with exports and fluff
...Array.from(connections).reduce((acc, dep) => {
// @ts-expect-error - bad types?
const depSpecifier = dep.resolvedModule.userRequest
acc[depSpecifier] = depSpecifier
return acc
}, /** @type {Record<string, string>} */ ({})),
},
//ast: module._ast, - would have to translate to babel anyway
})
moduleInspector.inspectModule(moduleRecord)
},
getPolicy: () => {
const policy = moduleInspector.generatePolicy({})
mkdirSync(location, { recursive: true })
writeFileSync(
path.join(location, 'policy.json'),
stringify(policy, { space: 2 }),
'utf8'
)
const final = applyOverride(policy)
if (emit) {
compilation.emitAsset(
POLICY_SNAPSHOT_FILENAME,
new RawSource(stringify(final, { space: 2 }))
)
}
return final
},
}
},
}