@pnpm/deps.compliance.audit 1002.0.14 → 1101.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +5 -4
- package/lib/index.js +155 -19
- package/lib/lockfileToAuditIndex.d.ts +27 -0
- package/lib/lockfileToAuditIndex.js +281 -0
- package/lib/types.d.ts +8 -48
- package/package.json +21 -21
- package/lib/lockfileToAuditTree.d.ts +0 -24
- package/lib/lockfileToAuditTree.js +0 -93
package/lib/index.d.ts
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { PnpmError } from '@pnpm/error';
|
|
2
2
|
import type { GetAuthHeader } from '@pnpm/fetching.types';
|
|
3
3
|
import type { EnvLockfile, LockfileObject } from '@pnpm/lockfile.types';
|
|
4
|
-
import { type
|
|
4
|
+
import { type DispatcherOptions, type RetryTimeoutOptions } from '@pnpm/network.fetch';
|
|
5
5
|
import type { DependenciesField } from '@pnpm/types';
|
|
6
6
|
import type { AuditReport } from './types.js';
|
|
7
|
+
export type { AuditIndexRequest, AuditPathIndex, PathInfo } from './lockfileToAuditIndex.js';
|
|
8
|
+
export { buildAuditPathIndex, lockfileToAuditRequest } from './lockfileToAuditIndex.js';
|
|
7
9
|
export * from './types.js';
|
|
8
10
|
export declare function audit(lockfile: LockfileObject, getAuthHeader: GetAuthHeader, opts: {
|
|
9
|
-
|
|
11
|
+
dispatcherOptions?: DispatcherOptions;
|
|
10
12
|
envLockfile?: EnvLockfile | null;
|
|
11
13
|
include?: {
|
|
12
14
|
[dependenciesField in DependenciesField]: boolean;
|
|
13
15
|
};
|
|
14
|
-
lockfileDir: string;
|
|
15
16
|
registry: string;
|
|
16
17
|
retry?: RetryTimeoutOptions;
|
|
17
18
|
timeout?: number;
|
|
18
|
-
virtualStoreDirMaxLength: number;
|
|
19
19
|
}): Promise<AuditReport>;
|
|
20
|
+
export declare function normalizeGhsaId(ghsaId: string): string;
|
|
20
21
|
export declare class AuditEndpointNotExistsError extends PnpmError {
|
|
21
22
|
constructor(endpoint: string);
|
|
22
23
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,38 +1,174 @@
|
|
|
1
1
|
import { PnpmError } from '@pnpm/error';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { detectDepTypes } from '@pnpm/lockfile.detect-dep-types';
|
|
3
|
+
import { fetchWithDispatcher } from '@pnpm/network.fetch';
|
|
4
|
+
import semver from 'semver';
|
|
5
|
+
import { buildAuditPathIndex, collectOptionalOnlyDepPaths, lockfileToAuditRequest, } from './lockfileToAuditIndex.js';
|
|
6
|
+
export { buildAuditPathIndex, lockfileToAuditRequest } from './lockfileToAuditIndex.js';
|
|
4
7
|
export * from './types.js';
|
|
5
8
|
export async function audit(lockfile, getAuthHeader, opts) {
|
|
6
|
-
const
|
|
9
|
+
const depTypes = detectDepTypes(lockfile);
|
|
10
|
+
const optionalOnly = collectOptionalOnlyDepPaths(lockfile, opts.include);
|
|
11
|
+
const auditRequest = lockfileToAuditRequest(lockfile, { envLockfile: opts.envLockfile, include: opts.include, depTypes, optionalOnly });
|
|
7
12
|
const registry = opts.registry.endsWith('/') ? opts.registry : `${opts.registry}/`;
|
|
8
|
-
const auditUrl = `${registry}-/npm/v1/security/
|
|
9
|
-
const quickAuditUrl = `${registry}-/npm/v1/security/audits/quick`;
|
|
13
|
+
const auditUrl = `${registry}-/npm/v1/security/advisories/bulk`;
|
|
10
14
|
const authHeaderValue = getAuthHeader(registry);
|
|
11
|
-
const requestBody = JSON.stringify(auditTree);
|
|
12
15
|
const requestHeaders = {
|
|
13
16
|
'Content-Type': 'application/json',
|
|
14
17
|
...getAuthHeaders(authHeaderValue),
|
|
15
18
|
};
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
body:
|
|
19
|
+
const res = await fetchWithDispatcher(auditUrl, {
|
|
20
|
+
dispatcherOptions: opts.dispatcherOptions ?? {},
|
|
21
|
+
body: JSON.stringify(auditRequest.request),
|
|
19
22
|
headers: requestHeaders,
|
|
20
23
|
method: 'POST',
|
|
21
24
|
retry: opts.retry,
|
|
22
25
|
timeout: opts.timeout,
|
|
26
|
+
});
|
|
27
|
+
if (res.status === 200) {
|
|
28
|
+
const rawBody = await res.text();
|
|
29
|
+
let body;
|
|
30
|
+
try {
|
|
31
|
+
body = JSON.parse(rawBody);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
35
|
+
throw new PnpmError('AUDIT_BAD_RESPONSE', `The audit endpoint (at ${auditUrl}) returned invalid JSON: ${reason}. Response body: ${rawBody.slice(0, 500)}`);
|
|
36
|
+
}
|
|
37
|
+
if (!isBulkResponseShape(body)) {
|
|
38
|
+
throw new PnpmError('AUDIT_BAD_RESPONSE', `The audit endpoint (at ${auditUrl}) returned an unexpected body. Expected an object keyed by package name; got: ${JSON.stringify(body)?.slice(0, 500) ?? String(body)}`);
|
|
39
|
+
}
|
|
40
|
+
const vulnerableNames = new Set(Object.keys(body));
|
|
41
|
+
let auditPathIndex = {};
|
|
42
|
+
if (vulnerableNames.size > 0) {
|
|
43
|
+
auditPathIndex = buildAuditPathIndex(lockfile, vulnerableNames, { envLockfile: opts.envLockfile, include: opts.include, depTypes, optionalOnly });
|
|
44
|
+
}
|
|
45
|
+
return bulkResponseToAuditReport(body, auditRequest, auditPathIndex);
|
|
46
|
+
}
|
|
47
|
+
if (res.status === 404) {
|
|
48
|
+
throw new AuditEndpointNotExistsError(auditUrl);
|
|
49
|
+
}
|
|
50
|
+
throw new PnpmError('AUDIT_BAD_RESPONSE', `The audit endpoint (at ${auditUrl}) responded with ${res.status}: ${await res.text()}`);
|
|
51
|
+
}
|
|
52
|
+
function bulkResponseToAuditReport(bulk, auditRequest, auditPathIndex) {
|
|
53
|
+
// Null-prototype map — the id comes from the registry and could be anything.
|
|
54
|
+
const advisories = Object.create(null);
|
|
55
|
+
const vulnerabilities = { info: 0, low: 0, moderate: 0, high: 0, critical: 0 };
|
|
56
|
+
for (const [moduleName, packageAdvisories] of Object.entries(bulk)) {
|
|
57
|
+
const byVersion = auditPathIndex[moduleName];
|
|
58
|
+
for (const adv of packageAdvisories) {
|
|
59
|
+
// Guard against registry-supplied values that could corrupt the report:
|
|
60
|
+
// only accept finite numeric ids and severities from the known set.
|
|
61
|
+
if (typeof adv.id !== 'number' || !Number.isFinite(adv.id))
|
|
62
|
+
continue;
|
|
63
|
+
if (!isKnownSeverity(adv.severity))
|
|
64
|
+
continue;
|
|
65
|
+
const findings = buildFindings(adv, byVersion);
|
|
66
|
+
// If no installed version is vulnerable, skip the advisory entirely so
|
|
67
|
+
// we don't report false positives for packages the lockfile doesn't use.
|
|
68
|
+
if (findings.length === 0)
|
|
69
|
+
continue;
|
|
70
|
+
advisories[String(adv.id)] = normalizeAdvisory(adv, moduleName, findings);
|
|
71
|
+
// npm's audit report counts one vulnerability per advisory in the metadata summary
|
|
72
|
+
// when using the bulk endpoint format pnpm expects.
|
|
73
|
+
vulnerabilities[adv.severity] += 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
advisories,
|
|
78
|
+
metadata: {
|
|
79
|
+
vulnerabilities,
|
|
80
|
+
dependencies: auditRequest.dependencies,
|
|
81
|
+
devDependencies: auditRequest.devDependencies,
|
|
82
|
+
optionalDependencies: auditRequest.optionalDependencies,
|
|
83
|
+
totalDependencies: auditRequest.totalDependencies,
|
|
84
|
+
},
|
|
23
85
|
};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
86
|
+
}
|
|
87
|
+
function buildFindings(adv, byVersion) {
|
|
88
|
+
if (byVersion == null)
|
|
89
|
+
return [];
|
|
90
|
+
const findings = [];
|
|
91
|
+
for (const [version, info] of byVersion) {
|
|
92
|
+
if (satisfiesSafe(version, adv.vulnerable_versions)) {
|
|
93
|
+
findings.push({
|
|
94
|
+
version,
|
|
95
|
+
paths: info.paths,
|
|
96
|
+
dev: info.dev,
|
|
97
|
+
optional: info.optional,
|
|
98
|
+
bundled: false,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
27
101
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
102
|
+
return findings;
|
|
103
|
+
}
|
|
104
|
+
const KNOWN_SEVERITIES = new Set(['info', 'low', 'moderate', 'high', 'critical']);
|
|
105
|
+
function isKnownSeverity(severity) {
|
|
106
|
+
return typeof severity === 'string' && KNOWN_SEVERITIES.has(severity);
|
|
107
|
+
}
|
|
108
|
+
function isBulkResponseShape(body) {
|
|
109
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body))
|
|
110
|
+
return false;
|
|
111
|
+
// Every value must be an array of advisory objects; a null or scalar value
|
|
112
|
+
// would crash `for (const adv of packageAdvisories)` downstream.
|
|
113
|
+
return Object.values(body).every((packageAdvisories) => Array.isArray(packageAdvisories) && packageAdvisories.every((advisory) => typeof advisory === 'object' && advisory !== null && !Array.isArray(advisory) &&
|
|
114
|
+
typeof advisory.vulnerable_versions === 'string'));
|
|
115
|
+
}
|
|
116
|
+
function satisfiesSafe(version, range) {
|
|
117
|
+
try {
|
|
118
|
+
return semver.satisfies(version, range, { includePrerelease: true, loose: true });
|
|
31
119
|
}
|
|
32
|
-
|
|
33
|
-
|
|
120
|
+
catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function normalizeAdvisory(adv, moduleName, findings) {
|
|
125
|
+
const cwe = Array.isArray(adv.cwe) ? adv.cwe.join(', ') : adv.cwe;
|
|
126
|
+
return {
|
|
127
|
+
findings,
|
|
128
|
+
id: adv.id,
|
|
129
|
+
title: adv.title ?? '',
|
|
130
|
+
module_name: moduleName,
|
|
131
|
+
vulnerable_versions: adv.vulnerable_versions,
|
|
132
|
+
patched_versions: inferPatchedVersions(adv.vulnerable_versions),
|
|
133
|
+
severity: adv.severity,
|
|
134
|
+
cwe: cwe ?? '',
|
|
135
|
+
github_advisory_id: deriveGithubAdvisoryId(adv.url),
|
|
136
|
+
url: adv.url ?? '',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function inferPatchedVersions(vulnerableRange) {
|
|
140
|
+
// Matches `<X.Y.Z` or `<= X.Y.Z` (with optional whitespace after the operator)
|
|
141
|
+
// at the end of the range, optionally preceded by other comparators like
|
|
142
|
+
// `>=0.8.1 <0.28.0`. Returns undefined if the range doesn't have a
|
|
143
|
+
// recognizable upper bound — callers must not confuse that with "no fix".
|
|
144
|
+
const trimmed = vulnerableRange.trim();
|
|
145
|
+
const ltMatch = trimmed.match(/(?:^|\s)<\s*(\d+\.\d+\.\d[\w\-.+]*)\s*$/);
|
|
146
|
+
if (ltMatch)
|
|
147
|
+
return `>=${ltMatch[1]}`;
|
|
148
|
+
const lteMatch = trimmed.match(/(?:^|\s)<=\s*(\d+\.\d+\.\d[\w\-.+]*)\s*$/);
|
|
149
|
+
if (lteMatch) {
|
|
150
|
+
const next = semver.inc(lteMatch[1], 'patch');
|
|
151
|
+
if (next)
|
|
152
|
+
return `>=${next}`;
|
|
34
153
|
}
|
|
35
|
-
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
function deriveGithubAdvisoryId(url) {
|
|
157
|
+
if (!url)
|
|
158
|
+
return '';
|
|
159
|
+
const match = url.match(/\/(GHSA-[\w-]+)/i);
|
|
160
|
+
return match ? normalizeGhsaId(match[1]) : '';
|
|
161
|
+
}
|
|
162
|
+
// GHSA identifiers are canonically written with an uppercase `GHSA-` prefix
|
|
163
|
+
// and a lowercase hexadecimal-style suffix (e.g. `GHSA-cph5-m8f7-6c5x`).
|
|
164
|
+
// Normalize both halves so ignore-list comparisons don't depend on how the
|
|
165
|
+
// user (or the advisory url) happens to case the id.
|
|
166
|
+
export function normalizeGhsaId(ghsaId) {
|
|
167
|
+
const trimmed = ghsaId.trim();
|
|
168
|
+
const dash = trimmed.indexOf('-');
|
|
169
|
+
if (dash < 0)
|
|
170
|
+
return trimmed.toUpperCase();
|
|
171
|
+
return trimmed.slice(0, dash).toUpperCase() + trimmed.slice(dash).toLowerCase();
|
|
36
172
|
}
|
|
37
173
|
function getAuthHeaders(authHeaderValue) {
|
|
38
174
|
const headers = {};
|
|
@@ -43,7 +179,7 @@ function getAuthHeaders(authHeaderValue) {
|
|
|
43
179
|
}
|
|
44
180
|
export class AuditEndpointNotExistsError extends PnpmError {
|
|
45
181
|
constructor(endpoint) {
|
|
46
|
-
const message = `The audit endpoint (at ${endpoint})
|
|
182
|
+
const message = `The audit endpoint (at ${endpoint}) doesn't exist.`;
|
|
47
183
|
super('AUDIT_ENDPOINT_NOT_EXISTS', message, {
|
|
48
184
|
hint: 'This issue is probably because you are using a private npm registry and that endpoint doesn\'t have an implementation of audit.',
|
|
49
185
|
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type DepTypes } from '@pnpm/lockfile.detect-dep-types';
|
|
2
|
+
import type { EnvLockfile, LockfileObject } from '@pnpm/lockfile.types';
|
|
3
|
+
import type { DependenciesField, DepPath } from '@pnpm/types';
|
|
4
|
+
export interface PathInfo {
|
|
5
|
+
paths: string[];
|
|
6
|
+
dev: boolean;
|
|
7
|
+
optional: boolean;
|
|
8
|
+
}
|
|
9
|
+
export type AuditPathIndex = Record<string, Map<string, PathInfo>>;
|
|
10
|
+
export interface AuditIndexRequest {
|
|
11
|
+
request: Record<string, string[]>;
|
|
12
|
+
totalDependencies: number;
|
|
13
|
+
dependencies: number;
|
|
14
|
+
devDependencies: number;
|
|
15
|
+
optionalDependencies: number;
|
|
16
|
+
}
|
|
17
|
+
export interface AuditIndexOptions {
|
|
18
|
+
envLockfile?: EnvLockfile | null;
|
|
19
|
+
include?: {
|
|
20
|
+
[dependenciesField in DependenciesField]: boolean;
|
|
21
|
+
};
|
|
22
|
+
depTypes?: DepTypes;
|
|
23
|
+
optionalOnly?: Set<DepPath>;
|
|
24
|
+
}
|
|
25
|
+
export declare function lockfileToAuditRequest(lockfile: LockfileObject, opts: AuditIndexOptions): AuditIndexRequest;
|
|
26
|
+
export declare function buildAuditPathIndex(lockfile: LockfileObject, vulnerableNames: Set<string>, opts: AuditIndexOptions): AuditPathIndex;
|
|
27
|
+
export declare function collectOptionalOnlyDepPaths(lockfile: LockfileObject, include?: AuditIndexOptions['include']): Set<DepPath>;
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import * as dp from '@pnpm/deps.path';
|
|
2
|
+
import { DepType, detectDepTypes } from '@pnpm/lockfile.detect-dep-types';
|
|
3
|
+
import { convertToLockfileObject } from '@pnpm/lockfile.fs';
|
|
4
|
+
import { nameVerFromPkgSnapshot } from '@pnpm/lockfile.utils';
|
|
5
|
+
import { lockfileWalkerGroupImporterSteps } from '@pnpm/lockfile.walker';
|
|
6
|
+
export function lockfileToAuditRequest(lockfile, opts) {
|
|
7
|
+
const importerIds = Object.keys(lockfile.importers);
|
|
8
|
+
const importerWalkers = lockfileWalkerGroupImporterSteps(lockfile, importerIds, { include: opts.include });
|
|
9
|
+
const depTypes = opts.depTypes ?? detectDepTypes(lockfile);
|
|
10
|
+
const optionalOnly = opts.optionalOnly ?? collectOptionalOnlyDepPaths(lockfile, opts.include);
|
|
11
|
+
// Use null-prototype objects for records keyed by package names so a
|
|
12
|
+
// hostile or unusual package name (e.g. "__proto__") cannot pollute the
|
|
13
|
+
// prototype or overwrite inherited properties.
|
|
14
|
+
const request = Object.create(null);
|
|
15
|
+
// Per (name, version) classification. Counted as dev/optional only while
|
|
16
|
+
// every observed occurrence is dev-only / optional-only; once a non-dev or
|
|
17
|
+
// non-optional occurrence is seen, the flag is cleared and the counter
|
|
18
|
+
// decremented.
|
|
19
|
+
const versionStatesByName = Object.create(null);
|
|
20
|
+
let totalDependencies = 0;
|
|
21
|
+
let dependencies = 0;
|
|
22
|
+
let devDependencies = 0;
|
|
23
|
+
let optionalDependencies = 0;
|
|
24
|
+
const registerOccurrence = (o) => {
|
|
25
|
+
let versionStates = versionStatesByName[o.name];
|
|
26
|
+
if (!versionStates) {
|
|
27
|
+
versionStates = new Map();
|
|
28
|
+
versionStatesByName[o.name] = versionStates;
|
|
29
|
+
request[o.name] = [];
|
|
30
|
+
}
|
|
31
|
+
const state = versionStates.get(o.version);
|
|
32
|
+
if (!state) {
|
|
33
|
+
versionStates.set(o.version, { devOnly: o.devOnly, optionalOnly: o.optionalOnly });
|
|
34
|
+
request[o.name].push(o.version);
|
|
35
|
+
totalDependencies++;
|
|
36
|
+
if (o.devOnly)
|
|
37
|
+
devDependencies++;
|
|
38
|
+
if (o.optionalOnly)
|
|
39
|
+
optionalDependencies++;
|
|
40
|
+
if (!o.devOnly && !o.optionalOnly)
|
|
41
|
+
dependencies++;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const wasProduction = !state.devOnly && !state.optionalOnly;
|
|
45
|
+
if (state.devOnly && !o.devOnly) {
|
|
46
|
+
state.devOnly = false;
|
|
47
|
+
devDependencies--;
|
|
48
|
+
}
|
|
49
|
+
if (state.optionalOnly && !o.optionalOnly) {
|
|
50
|
+
state.optionalOnly = false;
|
|
51
|
+
optionalDependencies--;
|
|
52
|
+
}
|
|
53
|
+
if (!wasProduction && !state.devOnly && !state.optionalOnly) {
|
|
54
|
+
dependencies++;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
// Build a visitor for one lockfile graph. The walker already de-duplicates
|
|
58
|
+
// by depPath internally, so we don't need a second visited set here.
|
|
59
|
+
const makeVisitor = (graphDepTypes, graphOptionalOnly) => {
|
|
60
|
+
const visit = (step) => {
|
|
61
|
+
for (const { depPath, pkgSnapshot, next } of step.dependencies) {
|
|
62
|
+
const { name, version } = nameVerFromPkgSnapshot(depPath, pkgSnapshot);
|
|
63
|
+
if (version) {
|
|
64
|
+
registerOccurrence({
|
|
65
|
+
name,
|
|
66
|
+
version,
|
|
67
|
+
devOnly: graphDepTypes[depPath] === DepType.DevOnly,
|
|
68
|
+
optionalOnly: graphOptionalOnly.has(depPath),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
visit(next());
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
return visit;
|
|
75
|
+
};
|
|
76
|
+
const visitMain = makeVisitor(depTypes, optionalOnly);
|
|
77
|
+
for (const importerWalker of importerWalkers) {
|
|
78
|
+
visitMain(importerWalker.step);
|
|
79
|
+
}
|
|
80
|
+
if (opts.envLockfile) {
|
|
81
|
+
const envLockfileObject = envLockfileToLockfileObject(opts.envLockfile);
|
|
82
|
+
const envDepTypes = detectDepTypes(envLockfileObject);
|
|
83
|
+
const envOptionalOnly = collectOptionalOnlyDepPaths(envLockfileObject, opts.include);
|
|
84
|
+
const visitEnv = makeVisitor(envDepTypes, envOptionalOnly);
|
|
85
|
+
for (const { step } of lockfileWalkerGroupImporterSteps(envLockfileObject, Object.keys(envLockfileObject.importers), { include: opts.include })) {
|
|
86
|
+
visitEnv(step);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { request, totalDependencies, dependencies, devDependencies, optionalDependencies };
|
|
90
|
+
}
|
|
91
|
+
export function buildAuditPathIndex(lockfile, vulnerableNames, opts) {
|
|
92
|
+
// Null-prototype record keyed by package name to avoid prototype pollution
|
|
93
|
+
// from registry-supplied or lockfile-supplied names.
|
|
94
|
+
const paths = Object.create(null);
|
|
95
|
+
const depTypes = opts.depTypes ?? detectDepTypes(lockfile);
|
|
96
|
+
const optionalOnly = opts.optionalOnly ?? collectOptionalOnlyDepPaths(lockfile, opts.include);
|
|
97
|
+
walkForPaths({
|
|
98
|
+
lockfile,
|
|
99
|
+
vulnerableNames,
|
|
100
|
+
paths,
|
|
101
|
+
depTypes,
|
|
102
|
+
optionalOnly,
|
|
103
|
+
include: opts.include,
|
|
104
|
+
importerSegmentOf: (importerId) => importerId.replace(/\//g, '__'),
|
|
105
|
+
});
|
|
106
|
+
if (opts.envLockfile) {
|
|
107
|
+
const envLockfileObject = envLockfileToLockfileObject(opts.envLockfile);
|
|
108
|
+
walkForPaths({
|
|
109
|
+
lockfile: envLockfileObject,
|
|
110
|
+
vulnerableNames,
|
|
111
|
+
paths,
|
|
112
|
+
depTypes: detectDepTypes(envLockfileObject),
|
|
113
|
+
optionalOnly: collectOptionalOnlyDepPaths(envLockfileObject, opts.include),
|
|
114
|
+
include: opts.include,
|
|
115
|
+
importerSegmentOf: (importerId) => importerId,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return paths;
|
|
119
|
+
}
|
|
120
|
+
function walkForPaths(ctx) {
|
|
121
|
+
const { lockfile, vulnerableNames, paths, depTypes, optionalOnly, include, importerSegmentOf } = ctx;
|
|
122
|
+
const includeDeps = include?.dependencies !== false;
|
|
123
|
+
const includeDevDeps = include?.devDependencies !== false;
|
|
124
|
+
const includeOptDeps = include?.optionalDependencies !== false;
|
|
125
|
+
const packages = lockfile.packages ?? {};
|
|
126
|
+
// Reused across every root to avoid per-node Set cloning. visit adds the
|
|
127
|
+
// current depPath before recursing and removes it on the way back, so the
|
|
128
|
+
// set always reflects the current trail.
|
|
129
|
+
const inTrail = new Set();
|
|
130
|
+
const visit = (edge, trail) => {
|
|
131
|
+
if (inTrail.has(edge.depPath))
|
|
132
|
+
return;
|
|
133
|
+
const pkgSnapshot = packages[edge.depPath];
|
|
134
|
+
if (pkgSnapshot == null)
|
|
135
|
+
return;
|
|
136
|
+
const { name, version } = nameVerFromPkgSnapshot(edge.depPath, pkgSnapshot);
|
|
137
|
+
const resolvedName = name ?? edge.name;
|
|
138
|
+
const fullPath = [...trail, resolvedName];
|
|
139
|
+
if (version && vulnerableNames.has(resolvedName)) {
|
|
140
|
+
recordPath(paths, resolvedName, version, fullPath.join('>'), depTypes[edge.depPath] === DepType.DevOnly, optionalOnly.has(edge.depPath));
|
|
141
|
+
}
|
|
142
|
+
inTrail.add(edge.depPath);
|
|
143
|
+
try {
|
|
144
|
+
for (const child of resolvedDepsToNamedDepPaths(pkgSnapshot.dependencies ?? {})) {
|
|
145
|
+
visit(child, fullPath);
|
|
146
|
+
}
|
|
147
|
+
if (includeOptDeps) {
|
|
148
|
+
for (const child of resolvedDepsToNamedDepPaths(pkgSnapshot.optionalDependencies ?? {})) {
|
|
149
|
+
visit(child, fullPath);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
inTrail.delete(edge.depPath);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
for (const [importerId, importer] of Object.entries(lockfile.importers)) {
|
|
158
|
+
const trail = [importerSegmentOf(importerId)];
|
|
159
|
+
const roots = [];
|
|
160
|
+
if (includeDeps)
|
|
161
|
+
roots.push(...resolvedDepsToNamedDepPaths(importer.dependencies ?? {}));
|
|
162
|
+
if (includeDevDeps)
|
|
163
|
+
roots.push(...resolvedDepsToNamedDepPaths(importer.devDependencies ?? {}));
|
|
164
|
+
if (includeOptDeps)
|
|
165
|
+
roots.push(...resolvedDepsToNamedDepPaths(importer.optionalDependencies ?? {}));
|
|
166
|
+
for (const root of roots) {
|
|
167
|
+
visit(root, trail);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Per-(name, version) cap on recorded paths. The CLI only ever displays the
|
|
172
|
+
// first few and follows with a "run pnpm why" hint, so keeping tens of
|
|
173
|
+
// thousands of equivalent chains is wasted memory/CPU for projects with
|
|
174
|
+
// heavy sharing (e.g. diamond dependencies deep in the graph).
|
|
175
|
+
const MAX_PATHS_PER_FINDING = 100;
|
|
176
|
+
function recordPath(paths, name, version, joined, isDev, isOptional) {
|
|
177
|
+
let byVersion = paths[name];
|
|
178
|
+
if (!byVersion) {
|
|
179
|
+
byVersion = new Map();
|
|
180
|
+
paths[name] = byVersion;
|
|
181
|
+
}
|
|
182
|
+
const info = byVersion.get(version);
|
|
183
|
+
if (!info) {
|
|
184
|
+
byVersion.set(version, { paths: [joined], dev: isDev, optional: isOptional });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (!isDev)
|
|
188
|
+
info.dev = false;
|
|
189
|
+
if (!isOptional)
|
|
190
|
+
info.optional = false;
|
|
191
|
+
if (info.paths.length >= MAX_PATHS_PER_FINDING)
|
|
192
|
+
return;
|
|
193
|
+
// Dedupe — the same joined trail can be produced when a package appears in
|
|
194
|
+
// both `dependencies` and `optionalDependencies` of the same parent, or via
|
|
195
|
+
// equivalent peer-suffix variants.
|
|
196
|
+
if (info.paths.includes(joined))
|
|
197
|
+
return;
|
|
198
|
+
info.paths.push(joined);
|
|
199
|
+
}
|
|
200
|
+
function resolvedDepsToNamedDepPaths(deps) {
|
|
201
|
+
const result = [];
|
|
202
|
+
for (const [alias, ref] of Object.entries(deps)) {
|
|
203
|
+
const depPath = dp.refToRelative(ref, alias);
|
|
204
|
+
if (depPath != null)
|
|
205
|
+
result.push({ name: alias, depPath });
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
// Returns the set of depPaths that are reachable only through optional edges
|
|
210
|
+
// (i.e. they would be absent from the install set if optionalDependencies were
|
|
211
|
+
// not included). Matches the AuditMetadata.optionalDependencies semantic.
|
|
212
|
+
//
|
|
213
|
+
// Implemented as (reachableWithOptional − reachableWithoutOptional) so that
|
|
214
|
+
// optionalDependencies nested inside a required chain are also accounted for,
|
|
215
|
+
// not just the ones declared directly on importer.optionalDependencies.
|
|
216
|
+
//
|
|
217
|
+
// Root selection honours the caller's `include` flags, so running
|
|
218
|
+
// `pnpm audit --prod` doesn't let dev-only subgraphs flip a package out of
|
|
219
|
+
// "optional-only" classification.
|
|
220
|
+
export function collectOptionalOnlyDepPaths(lockfile, include) {
|
|
221
|
+
const includeDeps = include?.dependencies !== false;
|
|
222
|
+
const includeDevDeps = include?.devDependencies !== false;
|
|
223
|
+
const includeOptDeps = include?.optionalDependencies !== false;
|
|
224
|
+
const withoutOptional = new Set();
|
|
225
|
+
const withOptional = new Set();
|
|
226
|
+
for (const importer of Object.values(lockfile.importers)) {
|
|
227
|
+
const nonOptionalRoots = [
|
|
228
|
+
...(includeDeps ? resolvedDepsToDepPaths(importer.dependencies ?? {}) : []),
|
|
229
|
+
...(includeDevDeps ? resolvedDepsToDepPaths(importer.devDependencies ?? {}) : []),
|
|
230
|
+
];
|
|
231
|
+
const allRoots = [
|
|
232
|
+
...nonOptionalRoots,
|
|
233
|
+
...(includeOptDeps ? resolvedDepsToDepPaths(importer.optionalDependencies ?? {}) : []),
|
|
234
|
+
];
|
|
235
|
+
walkReachable(lockfile, nonOptionalRoots, withoutOptional, false);
|
|
236
|
+
walkReachable(lockfile, allRoots, withOptional, includeOptDeps);
|
|
237
|
+
}
|
|
238
|
+
const result = new Set();
|
|
239
|
+
for (const depPath of withOptional) {
|
|
240
|
+
if (!withoutOptional.has(depPath))
|
|
241
|
+
result.add(depPath);
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
function walkReachable(lockfile, depPaths, seen, includeOptionalEdges) {
|
|
246
|
+
const packages = lockfile.packages ?? {};
|
|
247
|
+
for (const depPath of depPaths) {
|
|
248
|
+
if (seen.has(depPath))
|
|
249
|
+
continue;
|
|
250
|
+
seen.add(depPath);
|
|
251
|
+
const snapshot = packages[depPath];
|
|
252
|
+
if (!snapshot)
|
|
253
|
+
continue;
|
|
254
|
+
walkReachable(lockfile, resolvedDepsToDepPaths(snapshot.dependencies ?? {}), seen, includeOptionalEdges);
|
|
255
|
+
if (includeOptionalEdges) {
|
|
256
|
+
walkReachable(lockfile, resolvedDepsToDepPaths(snapshot.optionalDependencies ?? {}), seen, includeOptionalEdges);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function resolvedDepsToDepPaths(deps) {
|
|
261
|
+
return Object.entries(deps)
|
|
262
|
+
.map(([alias, ref]) => dp.refToRelative(ref, alias))
|
|
263
|
+
.filter((depPath) => depPath !== null);
|
|
264
|
+
}
|
|
265
|
+
function envLockfileToLockfileObject(envLockfile) {
|
|
266
|
+
const envImporter = envLockfile.importers['.'];
|
|
267
|
+
const importers = {};
|
|
268
|
+
if (Object.keys(envImporter.configDependencies).length > 0) {
|
|
269
|
+
importers['configDependencies'] = { dependencies: envImporter.configDependencies };
|
|
270
|
+
}
|
|
271
|
+
if (envImporter.packageManagerDependencies) {
|
|
272
|
+
importers['packageManagerDependencies'] = { dependencies: envImporter.packageManagerDependencies };
|
|
273
|
+
}
|
|
274
|
+
return convertToLockfileObject({
|
|
275
|
+
lockfileVersion: envLockfile.lockfileVersion,
|
|
276
|
+
importers,
|
|
277
|
+
packages: envLockfile.packages,
|
|
278
|
+
snapshots: envLockfile.snapshots,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
//# sourceMappingURL=lockfileToAuditIndex.js.map
|
package/lib/types.d.ts
CHANGED
|
@@ -6,64 +6,31 @@ export interface AuditVulnerabilityCounts {
|
|
|
6
6
|
critical: number;
|
|
7
7
|
}
|
|
8
8
|
export interface IgnoredAuditVulnerabilityCounts {
|
|
9
|
+
info: number;
|
|
9
10
|
low: number;
|
|
10
11
|
moderate: number;
|
|
11
12
|
high: number;
|
|
12
13
|
critical: number;
|
|
13
14
|
}
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
export type AuditLevelString = 'info' | 'low' | 'moderate' | 'high' | 'critical';
|
|
16
|
+
export type AuditLevelNumber = 0 | 1 | 2 | 3 | 4;
|
|
17
|
+
export interface AuditFinding {
|
|
18
|
+
version: string;
|
|
19
|
+
paths: string[];
|
|
17
20
|
dev: boolean;
|
|
18
21
|
optional: boolean;
|
|
19
22
|
bundled: boolean;
|
|
20
23
|
}
|
|
21
|
-
export interface AuditAction {
|
|
22
|
-
action: string;
|
|
23
|
-
module: string;
|
|
24
|
-
target: string;
|
|
25
|
-
isMajor: boolean;
|
|
26
|
-
resolves: AuditResolution[];
|
|
27
|
-
}
|
|
28
|
-
export type AuditLevelString = 'low' | 'moderate' | 'high' | 'critical';
|
|
29
|
-
export type AuditLevelNumber = 0 | 1 | 2 | 3;
|
|
30
24
|
export interface AuditAdvisory {
|
|
31
|
-
findings: [
|
|
32
|
-
{
|
|
33
|
-
version: string;
|
|
34
|
-
paths: string[];
|
|
35
|
-
dev: boolean;
|
|
36
|
-
optional: boolean;
|
|
37
|
-
bundled: boolean;
|
|
38
|
-
}
|
|
39
|
-
];
|
|
25
|
+
findings: AuditFinding[];
|
|
40
26
|
id: number;
|
|
41
|
-
created: string;
|
|
42
|
-
updated: string;
|
|
43
|
-
deleted?: boolean;
|
|
44
27
|
title: string;
|
|
45
|
-
found_by: {
|
|
46
|
-
name: string;
|
|
47
|
-
};
|
|
48
|
-
reported_by: {
|
|
49
|
-
name: string;
|
|
50
|
-
};
|
|
51
28
|
module_name: string;
|
|
52
|
-
cves: string[];
|
|
53
29
|
vulnerable_versions: string;
|
|
54
|
-
patched_versions
|
|
55
|
-
overview: string;
|
|
56
|
-
recommendation: string;
|
|
57
|
-
references: string;
|
|
58
|
-
access: string;
|
|
30
|
+
patched_versions?: string;
|
|
59
31
|
severity: AuditLevelString;
|
|
60
32
|
cwe: string;
|
|
61
33
|
github_advisory_id: string;
|
|
62
|
-
metadata: {
|
|
63
|
-
module_type: string;
|
|
64
|
-
exploitability: number;
|
|
65
|
-
affected_components: string;
|
|
66
|
-
};
|
|
67
34
|
url: string;
|
|
68
35
|
}
|
|
69
36
|
export interface AuditMetadata {
|
|
@@ -74,15 +41,8 @@ export interface AuditMetadata {
|
|
|
74
41
|
totalDependencies: number;
|
|
75
42
|
}
|
|
76
43
|
export interface AuditReport {
|
|
77
|
-
actions: AuditAction[];
|
|
78
44
|
advisories: {
|
|
79
45
|
[id: string]: AuditAdvisory;
|
|
80
46
|
};
|
|
81
|
-
muted: unknown[];
|
|
82
47
|
metadata: AuditMetadata;
|
|
83
48
|
}
|
|
84
|
-
export interface AuditActionRecommendation {
|
|
85
|
-
cmd: string;
|
|
86
|
-
isBreaking: boolean;
|
|
87
|
-
action: AuditAction;
|
|
88
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/deps.compliance.audit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1101.0.0",
|
|
4
4
|
"description": "Audit a lockfile",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -25,28 +25,28 @@
|
|
|
25
25
|
"!*.map"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"
|
|
29
|
-
"@pnpm/error": "
|
|
30
|
-
"@pnpm/fetching.types": "
|
|
31
|
-
"@pnpm/
|
|
32
|
-
"@pnpm/lockfile.detect-dep-types": "
|
|
33
|
-
"@pnpm/lockfile.
|
|
34
|
-
"@pnpm/lockfile.
|
|
35
|
-
"@pnpm/
|
|
36
|
-
"@pnpm/
|
|
37
|
-
"@pnpm/
|
|
38
|
-
"@pnpm/
|
|
28
|
+
"semver": "^7.7.2",
|
|
29
|
+
"@pnpm/error": "1100.0.0",
|
|
30
|
+
"@pnpm/fetching.types": "1100.0.0",
|
|
31
|
+
"@pnpm/deps.path": "1100.0.1",
|
|
32
|
+
"@pnpm/lockfile.detect-dep-types": "1100.0.1",
|
|
33
|
+
"@pnpm/lockfile.fs": "1100.0.1",
|
|
34
|
+
"@pnpm/lockfile.utils": "1100.0.1",
|
|
35
|
+
"@pnpm/lockfile.types": "1100.0.1",
|
|
36
|
+
"@pnpm/lockfile.walker": "1100.0.1",
|
|
37
|
+
"@pnpm/network.fetch": "1100.0.1",
|
|
38
|
+
"@pnpm/types": "1101.0.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@pnpm/logger": ">=1001.0.0 <1002.0.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@types/
|
|
45
|
-
"
|
|
46
|
-
"@pnpm/
|
|
47
|
-
"@pnpm/
|
|
48
|
-
"@pnpm/
|
|
49
|
-
"@pnpm/
|
|
44
|
+
"@types/semver": "7.7.1",
|
|
45
|
+
"@pnpm/deps.compliance.audit": "1101.0.0",
|
|
46
|
+
"@pnpm/constants": "1100.0.0",
|
|
47
|
+
"@pnpm/logger": "1100.0.0",
|
|
48
|
+
"@pnpm/test-fixtures": "1100.0.0",
|
|
49
|
+
"@pnpm/testing.mock-agent": "1100.0.1"
|
|
50
50
|
},
|
|
51
51
|
"engines": {
|
|
52
52
|
"node": ">=22.13"
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
59
|
+
"test": "pn compile && pn .test",
|
|
60
|
+
"compile": "tsgo --build && pn lint --fix",
|
|
61
|
+
".test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest"
|
|
62
62
|
}
|
|
63
63
|
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { EnvLockfile, LockfileObject } from '@pnpm/lockfile.types';
|
|
2
|
-
import type { DependenciesField } from '@pnpm/types';
|
|
3
|
-
export interface AuditNode {
|
|
4
|
-
version?: string;
|
|
5
|
-
integrity?: string;
|
|
6
|
-
requires?: Record<string, string>;
|
|
7
|
-
dependencies?: {
|
|
8
|
-
[name: string]: AuditNode;
|
|
9
|
-
};
|
|
10
|
-
dev: boolean;
|
|
11
|
-
}
|
|
12
|
-
export interface AuditTree extends AuditNode {
|
|
13
|
-
name?: string;
|
|
14
|
-
install: string[];
|
|
15
|
-
remove: string[];
|
|
16
|
-
metadata: unknown;
|
|
17
|
-
}
|
|
18
|
-
export declare function lockfileToAuditTree(lockfile: LockfileObject, opts: {
|
|
19
|
-
envLockfile?: EnvLockfile | null;
|
|
20
|
-
include?: {
|
|
21
|
-
[dependenciesField in DependenciesField]: boolean;
|
|
22
|
-
};
|
|
23
|
-
lockfileDir: string;
|
|
24
|
-
}): Promise<AuditTree>;
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { DepType, detectDepTypes } from '@pnpm/lockfile.detect-dep-types';
|
|
3
|
-
import { convertToLockfileObject } from '@pnpm/lockfile.fs';
|
|
4
|
-
import { nameVerFromPkgSnapshot } from '@pnpm/lockfile.utils';
|
|
5
|
-
import { lockfileWalkerGroupImporterSteps } from '@pnpm/lockfile.walker';
|
|
6
|
-
import { safeReadProjectManifestOnly } from '@pnpm/workspace.project-manifest-reader';
|
|
7
|
-
import { map as mapValues } from 'ramda';
|
|
8
|
-
export async function lockfileToAuditTree(lockfile, opts) {
|
|
9
|
-
const importerWalkers = lockfileWalkerGroupImporterSteps(lockfile, Object.keys(lockfile.importers), { include: opts?.include });
|
|
10
|
-
const dependencies = {};
|
|
11
|
-
const depTypes = detectDepTypes(lockfile);
|
|
12
|
-
await Promise.all(importerWalkers.map(async (importerWalker) => {
|
|
13
|
-
const importerDeps = lockfileToAuditNode(depTypes, importerWalker.step);
|
|
14
|
-
// For some reason the registry responds with 500 if the keys in dependencies have slashes
|
|
15
|
-
// see issue: https://github.com/pnpm/pnpm/issues/2848
|
|
16
|
-
const depName = importerWalker.importerId.replace(/\//g, '__');
|
|
17
|
-
const manifest = await safeReadProjectManifestOnly(path.join(opts.lockfileDir, importerWalker.importerId));
|
|
18
|
-
dependencies[depName] = {
|
|
19
|
-
dependencies: importerDeps,
|
|
20
|
-
dev: false,
|
|
21
|
-
requires: toRequires(importerDeps),
|
|
22
|
-
version: manifest?.version ?? '0.0.0',
|
|
23
|
-
};
|
|
24
|
-
}));
|
|
25
|
-
if (opts.envLockfile) {
|
|
26
|
-
const envLockfileObject = envLockfileToLockfileObject(opts.envLockfile);
|
|
27
|
-
const envDepTypes = detectDepTypes(envLockfileObject);
|
|
28
|
-
for (const { importerId, step } of lockfileWalkerGroupImporterSteps(envLockfileObject, Object.keys(envLockfileObject.importers), { include: opts.include })) {
|
|
29
|
-
const deps = lockfileToAuditNode(envDepTypes, step);
|
|
30
|
-
if (Object.keys(deps).length > 0) {
|
|
31
|
-
dependencies[importerId] = wrapDepsGroup(deps);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
const auditTree = {
|
|
36
|
-
name: undefined,
|
|
37
|
-
version: undefined,
|
|
38
|
-
dependencies,
|
|
39
|
-
dev: false,
|
|
40
|
-
install: [],
|
|
41
|
-
integrity: undefined,
|
|
42
|
-
metadata: {},
|
|
43
|
-
remove: [],
|
|
44
|
-
requires: toRequires(dependencies),
|
|
45
|
-
};
|
|
46
|
-
return auditTree;
|
|
47
|
-
}
|
|
48
|
-
function lockfileToAuditNode(depTypes, step) {
|
|
49
|
-
const dependencies = {};
|
|
50
|
-
for (const { depPath, pkgSnapshot, next } of step.dependencies) {
|
|
51
|
-
const { name, version } = nameVerFromPkgSnapshot(depPath, pkgSnapshot);
|
|
52
|
-
const subdeps = lockfileToAuditNode(depTypes, next());
|
|
53
|
-
const dep = {
|
|
54
|
-
dev: depTypes[depPath] === DepType.DevOnly,
|
|
55
|
-
integrity: pkgSnapshot.resolution.integrity,
|
|
56
|
-
version,
|
|
57
|
-
};
|
|
58
|
-
if (Object.keys(subdeps).length > 0) {
|
|
59
|
-
dep.dependencies = subdeps;
|
|
60
|
-
dep.requires = toRequires(subdeps);
|
|
61
|
-
}
|
|
62
|
-
dependencies[name] = dep;
|
|
63
|
-
}
|
|
64
|
-
return dependencies;
|
|
65
|
-
}
|
|
66
|
-
function toRequires(auditNodesByDepName) {
|
|
67
|
-
return mapValues((auditNode) => auditNode.version, auditNodesByDepName);
|
|
68
|
-
}
|
|
69
|
-
function wrapDepsGroup(deps) {
|
|
70
|
-
return {
|
|
71
|
-
dependencies: deps,
|
|
72
|
-
dev: false,
|
|
73
|
-
requires: toRequires(deps),
|
|
74
|
-
version: '0.0.0',
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
function envLockfileToLockfileObject(envLockfile) {
|
|
78
|
-
const envImporter = envLockfile.importers['.'];
|
|
79
|
-
const importers = {};
|
|
80
|
-
if (Object.keys(envImporter.configDependencies).length > 0) {
|
|
81
|
-
importers['configDependencies'] = { dependencies: envImporter.configDependencies };
|
|
82
|
-
}
|
|
83
|
-
if (envImporter.packageManagerDependencies) {
|
|
84
|
-
importers['packageManagerDependencies'] = { dependencies: envImporter.packageManagerDependencies };
|
|
85
|
-
}
|
|
86
|
-
return convertToLockfileObject({
|
|
87
|
-
lockfileVersion: envLockfile.lockfileVersion,
|
|
88
|
-
importers,
|
|
89
|
-
packages: envLockfile.packages,
|
|
90
|
-
snapshots: envLockfile.snapshots,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
//# sourceMappingURL=lockfileToAuditTree.js.map
|