@loomhelix/deptrail 0.1.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/LICENSE +21 -0
- package/README.md +421 -0
- package/dist/actions/action-error.js +11 -0
- package/dist/actions/action-jobs.js +199 -0
- package/dist/actions/action-service.js +268 -0
- package/dist/actions/apply.js +120 -0
- package/dist/actions/availability.js +79 -0
- package/dist/actions/change-sets.js +117 -0
- package/dist/actions/dry-run.js +287 -0
- package/dist/actions/edge-moves.js +108 -0
- package/dist/actions/edit-plan.js +179 -0
- package/dist/actions/file-scope.js +118 -0
- package/dist/actions/fix-options.js +141 -0
- package/dist/actions/goal-check.js +80 -0
- package/dist/actions/install.js +27 -0
- package/dist/actions/job-registry.js +104 -0
- package/dist/actions/json-edit.js +189 -0
- package/dist/actions/labels.js +35 -0
- package/dist/actions/node-actions.js +102 -0
- package/dist/actions/overrides-location.js +59 -0
- package/dist/actions/parent-chain.js +210 -0
- package/dist/actions/range-style.js +38 -0
- package/dist/actions/recipes.js +31 -0
- package/dist/actions/root-files.js +30 -0
- package/dist/actions/sandbox.js +175 -0
- package/dist/actions/set-range.js +59 -0
- package/dist/actions/shared-packages.js +50 -0
- package/dist/actions/text-diff.js +203 -0
- package/dist/actions/types.js +1 -0
- package/dist/actions/update-options.js +232 -0
- package/dist/actions/workspace-edit.js +33 -0
- package/dist/adapters/command-runner.js +127 -0
- package/dist/adapters/manifest-only.js +20 -0
- package/dist/adapters/node-modules.js +148 -0
- package/dist/adapters/npm-lock.js +149 -0
- package/dist/adapters/package-manifest.js +57 -0
- package/dist/adapters/pnpm-lock.js +177 -0
- package/dist/adapters/resolve-today.js +93 -0
- package/dist/cli/cli.js +227 -0
- package/dist/cli/open-browser.js +14 -0
- package/dist/cli/package-version.js +6 -0
- package/dist/cli/usage-text.js +27 -0
- package/dist/config/glob.js +119 -0
- package/dist/config/npmrc.js +116 -0
- package/dist/config/root-config.js +103 -0
- package/dist/config/secrets.js +18 -0
- package/dist/discovery/find-roots.js +107 -0
- package/dist/enrich/credentials.js +143 -0
- package/dist/enrich/enrichment-cache.js +42 -0
- package/dist/enrich/enrichment-keys.js +71 -0
- package/dist/enrich/enrichment-service.js +399 -0
- package/dist/enrich/enrichment-status.js +86 -0
- package/dist/enrich/enrichment-store.js +143 -0
- package/dist/enrich/http.js +201 -0
- package/dist/enrich/on-demand.js +220 -0
- package/dist/enrich/osv-client.js +260 -0
- package/dist/enrich/registry-client.js +123 -0
- package/dist/enrich/types.js +1 -0
- package/dist/git/blob-reader.js +180 -0
- package/dist/git/git-commands.js +32 -0
- package/dist/git/git-process.js +71 -0
- package/dist/git/git-repository.js +136 -0
- package/dist/git/ref-list.js +78 -0
- package/dist/git/tree-index.js +87 -0
- package/dist/graph/global-index.js +19 -0
- package/dist/graph/root-graph-builder.js +114 -0
- package/dist/graph/serialize.js +19 -0
- package/dist/model.js +2 -0
- package/dist/query/children.js +38 -0
- package/dist/query/compare-shape.js +13 -0
- package/dist/query/compare.js +245 -0
- package/dist/query/decorations.js +121 -0
- package/dist/query/dependents.js +135 -0
- package/dist/query/duplicates.js +23 -0
- package/dist/query/enrichment-lookup.js +78 -0
- package/dist/query/fixes.js +60 -0
- package/dist/query/graph-diff.js +42 -0
- package/dist/query/issues.js +235 -0
- package/dist/query/lookup.js +49 -0
- package/dist/query/node-fix.js +46 -0
- package/dist/query/package-insight.js +112 -0
- package/dist/query/search.js +61 -0
- package/dist/query/subgraph.js +107 -0
- package/dist/query/versions.js +103 -0
- package/dist/scan/build-root.js +137 -0
- package/dist/scan/cache-shape.js +52 -0
- package/dist/scan/scan-cache.js +32 -0
- package/dist/scan/scan-session.js +127 -0
- package/dist/scan/source-registry.js +55 -0
- package/dist/server/auth.js +45 -0
- package/dist/server/branches.js +105 -0
- package/dist/server/error-log.js +9 -0
- package/dist/server/root-segment.js +13 -0
- package/dist/server/router.js +474 -0
- package/dist/server/server.js +166 -0
- package/dist/server/source-key.js +1 -0
- package/dist/server/static.js +59 -0
- package/dist/sources/file-reader.js +55 -0
- package/dist/sources/git-reader.js +45 -0
- package/dist/ui/assets/index-C4uyB5v3.css +1 -0
- package/dist/ui/assets/index-DYAG2LM-.js +51 -0
- package/dist/ui/index.html +15 -0
- package/package.json +72 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { IMPORTER_PREFIX } from "../model.js";
|
|
2
|
+
import { findNode, requireNode, UnknownNodeError, viewOf } from "./lookup.js";
|
|
3
|
+
const DEFAULT_NODE_LIMIT = 5000;
|
|
4
|
+
const upwardIndexes = new WeakMap();
|
|
5
|
+
function kindsKey(kinds) {
|
|
6
|
+
return [...kinds].sort().join(',');
|
|
7
|
+
}
|
|
8
|
+
function buildUpwardIndex(root, kinds) {
|
|
9
|
+
const nodes = [...root.importers, ...root.packages.values()];
|
|
10
|
+
const positionOf = new Map(nodes.map((node, position) => [node.id, position]));
|
|
11
|
+
const passingTargets = nodes.map((node) => new Set(node.deps.filter((edge) => kinds.has(edge.kind)).map((edge) => edge.to)));
|
|
12
|
+
const parentStarts = new Int32Array(nodes.length + 1);
|
|
13
|
+
const parentPositions = [];
|
|
14
|
+
nodes.forEach((node, position) => {
|
|
15
|
+
parentStarts[position] = parentPositions.length;
|
|
16
|
+
for (const parentId of node.dependents) {
|
|
17
|
+
const parent = positionOf.get(parentId);
|
|
18
|
+
if (parent !== undefined && passingTargets[parent].has(node.id))
|
|
19
|
+
parentPositions.push(parent);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
parentStarts[nodes.length] = parentPositions.length;
|
|
23
|
+
return {
|
|
24
|
+
ids: nodes.map((node) => node.id),
|
|
25
|
+
positionOf,
|
|
26
|
+
isImporter: Uint8Array.from(nodes, (node) => (node.id.startsWith(IMPORTER_PREFIX) ? 1 : 0)),
|
|
27
|
+
parentStarts,
|
|
28
|
+
parentPositions: Int32Array.from(parentPositions),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function upwardIndex(root, kinds) {
|
|
32
|
+
const key = kindsKey(kinds);
|
|
33
|
+
let byKinds = upwardIndexes.get(root);
|
|
34
|
+
if (byKinds === undefined) {
|
|
35
|
+
byKinds = new Map();
|
|
36
|
+
upwardIndexes.set(root, byKinds);
|
|
37
|
+
}
|
|
38
|
+
const known = byKinds.get(key);
|
|
39
|
+
if (known)
|
|
40
|
+
return known;
|
|
41
|
+
const built = buildUpwardIndex(root, kinds);
|
|
42
|
+
byKinds.set(key, built);
|
|
43
|
+
return built;
|
|
44
|
+
}
|
|
45
|
+
export class ParentLinkResolver {
|
|
46
|
+
root;
|
|
47
|
+
kinds;
|
|
48
|
+
firstPassingEdges = new Map();
|
|
49
|
+
constructor(root, kinds) {
|
|
50
|
+
this.root = root;
|
|
51
|
+
this.kinds = kinds;
|
|
52
|
+
}
|
|
53
|
+
of(node) {
|
|
54
|
+
const links = [];
|
|
55
|
+
for (const parentId of node.dependents) {
|
|
56
|
+
const edge = this.edgesOf(parentId).get(node.id);
|
|
57
|
+
if (edge)
|
|
58
|
+
links.push({ id: parentId, kind: edge.kind, range: edge.range });
|
|
59
|
+
}
|
|
60
|
+
return links;
|
|
61
|
+
}
|
|
62
|
+
edgesOf(parentId) {
|
|
63
|
+
const cached = this.firstPassingEdges.get(parentId);
|
|
64
|
+
if (cached)
|
|
65
|
+
return cached;
|
|
66
|
+
const byTarget = new Map();
|
|
67
|
+
for (const edge of findNode(this.root, parentId)?.deps ?? []) {
|
|
68
|
+
if (this.kinds.has(edge.kind) && !byTarget.has(edge.to))
|
|
69
|
+
byTarget.set(edge.to, edge);
|
|
70
|
+
}
|
|
71
|
+
this.firstPassingEdges.set(parentId, byTarget);
|
|
72
|
+
return byTarget;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function dependentsSummary(root, id, kinds) {
|
|
76
|
+
const { ids, positionOf, isImporter, parentStarts, parentPositions } = upwardIndex(root, kinds);
|
|
77
|
+
const focus = positionOf.get(id);
|
|
78
|
+
if (focus === undefined)
|
|
79
|
+
throw new UnknownNodeError(id);
|
|
80
|
+
const visited = new Uint8Array(ids.length);
|
|
81
|
+
const importers = new Set();
|
|
82
|
+
const importersByDirect = new Map();
|
|
83
|
+
const queue = [focus];
|
|
84
|
+
visited[focus] = 1;
|
|
85
|
+
for (let cursor = 0; cursor < queue.length; cursor++) {
|
|
86
|
+
const position = queue[cursor];
|
|
87
|
+
const importerParents = [];
|
|
88
|
+
for (let link = parentStarts[position]; link < parentStarts[position + 1]; link++) {
|
|
89
|
+
const parent = parentPositions[link];
|
|
90
|
+
if (isImporter[parent] === 1) {
|
|
91
|
+
importers.add(ids[parent]);
|
|
92
|
+
importerParents.push(ids[parent]);
|
|
93
|
+
}
|
|
94
|
+
if (visited[parent] === 1)
|
|
95
|
+
continue;
|
|
96
|
+
visited[parent] = 1;
|
|
97
|
+
queue.push(parent);
|
|
98
|
+
}
|
|
99
|
+
if (importerParents.length > 0 && isImporter[position] === 0)
|
|
100
|
+
importersByDirect.set(ids[position], importerParents.sort());
|
|
101
|
+
}
|
|
102
|
+
importers.delete(id);
|
|
103
|
+
const via = [...importersByDirect].map(([direct, directImporters]) => ({ direct, importers: directImporters }));
|
|
104
|
+
via.sort((left, right) => (left.direct < right.direct ? -1 : left.direct > right.direct ? 1 : 0));
|
|
105
|
+
return { importers: [...importers].sort(), via };
|
|
106
|
+
}
|
|
107
|
+
export function dependents(root, id, maxDepth, kinds, nodeLimit = DEFAULT_NODE_LIMIT) {
|
|
108
|
+
const focus = requireNode(root, id);
|
|
109
|
+
const links = new ParentLinkResolver(root, kinds);
|
|
110
|
+
const nodes = [];
|
|
111
|
+
const seen = new Set([id]);
|
|
112
|
+
let truncated = false;
|
|
113
|
+
let frontier = [focus];
|
|
114
|
+
for (let depth = 1; depth <= maxDepth && frontier.length > 0 && !truncated; depth++) {
|
|
115
|
+
const nextFrontier = [];
|
|
116
|
+
for (const node of frontier) {
|
|
117
|
+
for (const link of links.of(node)) {
|
|
118
|
+
if (seen.has(link.id))
|
|
119
|
+
continue;
|
|
120
|
+
if (nodes.length >= nodeLimit) {
|
|
121
|
+
truncated = true;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
seen.add(link.id);
|
|
125
|
+
const parent = requireNode(root, link.id);
|
|
126
|
+
nodes.push({ node: viewOf(parent), parents: links.of(parent), depth });
|
|
127
|
+
nextFrontier.push(parent);
|
|
128
|
+
}
|
|
129
|
+
if (truncated)
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
frontier = nextFrontier;
|
|
133
|
+
}
|
|
134
|
+
return { focus: viewOf(focus), focusParents: links.of(focus), nodes, summary: dependentsSummary(root, id, kinds), truncated };
|
|
135
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
export function duplicates(index, rootId) {
|
|
3
|
+
const found = [];
|
|
4
|
+
for (const [name, entries] of index) {
|
|
5
|
+
const rootsByVersion = new Map();
|
|
6
|
+
for (const entry of entries) {
|
|
7
|
+
if (rootId !== undefined && entry.rootId !== rootId)
|
|
8
|
+
continue;
|
|
9
|
+
if (semver.valid(entry.version) === null)
|
|
10
|
+
continue;
|
|
11
|
+
const roots = rootsByVersion.get(entry.version) ?? new Set();
|
|
12
|
+
roots.add(entry.rootId);
|
|
13
|
+
rootsByVersion.set(entry.version, roots);
|
|
14
|
+
}
|
|
15
|
+
if (rootsByVersion.size < 2)
|
|
16
|
+
continue;
|
|
17
|
+
const versions = [...rootsByVersion]
|
|
18
|
+
.sort(([left], [right]) => semver.rcompare(left, right))
|
|
19
|
+
.map(([version, roots]) => ({ version, roots: [...roots].sort() }));
|
|
20
|
+
found.push({ name, versions });
|
|
21
|
+
}
|
|
22
|
+
return found.sort((left, right) => right.versions.length - left.versions.length || (left.name < right.name ? -1 : 1));
|
|
23
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { SEVERITY_ORDER } from "../enrich/types.js";
|
|
2
|
+
import { IMPORTER_PREFIX } from "../model.js";
|
|
3
|
+
function severityRank(severity) {
|
|
4
|
+
return SEVERITY_ORDER.indexOf(severity);
|
|
5
|
+
}
|
|
6
|
+
export function worstSeverity(severities) {
|
|
7
|
+
let worst;
|
|
8
|
+
for (const severity of severities) {
|
|
9
|
+
if (worst === undefined || severityRank(severity) < severityRank(worst))
|
|
10
|
+
worst = severity;
|
|
11
|
+
}
|
|
12
|
+
return worst;
|
|
13
|
+
}
|
|
14
|
+
export function nodeMeta(view, root, node) {
|
|
15
|
+
const registry = view.registryOf(root, node);
|
|
16
|
+
return registry.state === 'ok' || registry.state === 'stale' ? registry.meta : undefined;
|
|
17
|
+
}
|
|
18
|
+
function standInAdvisory(id) {
|
|
19
|
+
return {
|
|
20
|
+
id,
|
|
21
|
+
aliases: [],
|
|
22
|
+
summary: '',
|
|
23
|
+
severity: 'unknown',
|
|
24
|
+
malicious: false,
|
|
25
|
+
url: `https://osv.dev/vulnerability/${id}`,
|
|
26
|
+
fixedIn: [],
|
|
27
|
+
modified: '',
|
|
28
|
+
affected: [],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function compareAdvisories(left, right) {
|
|
32
|
+
const bySeverity = severityRank(left.severity) - severityRank(right.severity);
|
|
33
|
+
if (bySeverity !== 0)
|
|
34
|
+
return bySeverity;
|
|
35
|
+
return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
|
|
36
|
+
}
|
|
37
|
+
function findAdvisory(advisories, id) {
|
|
38
|
+
return advisories.find((advisory) => advisory.id === id || advisory.aliases.includes(id));
|
|
39
|
+
}
|
|
40
|
+
function loadedAdvisoriesOf(view, name) {
|
|
41
|
+
const loaded = view.advisoriesOf(name);
|
|
42
|
+
return loaded.state === 'ok' || loaded.state === 'stale' ? loaded.advisories : undefined;
|
|
43
|
+
}
|
|
44
|
+
export function nodeAdvisories(view, root, node) {
|
|
45
|
+
return advisoriesOfMatch(view, node.name, view.matchOf(root, node));
|
|
46
|
+
}
|
|
47
|
+
export function completePackageAdvisories(view, root, node) {
|
|
48
|
+
const match = view.matchOf(root, node);
|
|
49
|
+
if (match.state !== 'ok' && match.state !== 'stale')
|
|
50
|
+
return undefined;
|
|
51
|
+
const known = loadedAdvisoriesOf(view, node.name);
|
|
52
|
+
if (known === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
for (const { id } of match.vulns) {
|
|
55
|
+
if (findAdvisory(known, id) === undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return known;
|
|
59
|
+
}
|
|
60
|
+
export function advisoriesOfMatch(view, name, match) {
|
|
61
|
+
if (match.state !== 'ok' && match.state !== 'stale')
|
|
62
|
+
return [];
|
|
63
|
+
const known = loadedAdvisoriesOf(view, name) ?? [];
|
|
64
|
+
const result = match.vulns.map(({ id }) => findAdvisory(known, id) ?? standInAdvisory(id));
|
|
65
|
+
const distinct = [...new Map(result.map((advisory) => [advisory.id, advisory])).values()];
|
|
66
|
+
return distinct.sort(compareAdvisories);
|
|
67
|
+
}
|
|
68
|
+
function isPackageNode(node) {
|
|
69
|
+
return !node.id.startsWith(IMPORTER_PREFIX);
|
|
70
|
+
}
|
|
71
|
+
export function requestedRange(view, root, parent, lockfileRange, child) {
|
|
72
|
+
if (isPackageNode(parent)) {
|
|
73
|
+
const published = nodeMeta(view, root, parent)?.manifests[parent.version]?.[child.name];
|
|
74
|
+
if (published !== undefined)
|
|
75
|
+
return { range: published, source: 'published-manifest' };
|
|
76
|
+
}
|
|
77
|
+
return { range: lockfileRange, source: 'lockfile' };
|
|
78
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
export const BUMP_LIST_CAP = 20;
|
|
3
|
+
function highestSafeWithinRange(safe, range) {
|
|
4
|
+
if (range === undefined || semver.validRange(range) === null)
|
|
5
|
+
return undefined;
|
|
6
|
+
return semver.maxSatisfying(safe, range) ?? undefined;
|
|
7
|
+
}
|
|
8
|
+
function lowestSafeAbove(safe, current) {
|
|
9
|
+
if (semver.valid(current) === null)
|
|
10
|
+
return undefined;
|
|
11
|
+
return safe.find((version) => semver.gt(version, current));
|
|
12
|
+
}
|
|
13
|
+
export function edgeFix({ current, range, parentIsImporter, safe }) {
|
|
14
|
+
if (safe === undefined)
|
|
15
|
+
return { kind: 'unknown' };
|
|
16
|
+
if (safe.length === 0)
|
|
17
|
+
return { kind: 'no-fix' };
|
|
18
|
+
const withinRange = highestSafeWithinRange(safe, range);
|
|
19
|
+
if (withinRange !== undefined)
|
|
20
|
+
return { kind: 'within-range', version: withinRange };
|
|
21
|
+
if (!parentIsImporter)
|
|
22
|
+
return { kind: 'bump-parent' };
|
|
23
|
+
const raised = lowestSafeAbove(safe, current);
|
|
24
|
+
return raised === undefined ? { kind: 'no-fix' } : { kind: 'bump-manifest', version: raised };
|
|
25
|
+
}
|
|
26
|
+
function sharedVersion(edges) {
|
|
27
|
+
const versions = new Set(edges.map((edge) => edge.fix.version));
|
|
28
|
+
return versions.size === 1 ? [...versions][0] : undefined;
|
|
29
|
+
}
|
|
30
|
+
function bumpTargets(edges) {
|
|
31
|
+
return edges
|
|
32
|
+
.filter((edge) => edge.fix.kind === 'bump-manifest' || edge.fix.kind === 'bump-parent')
|
|
33
|
+
.map((edge) => ({ nodeId: edge.parent, isImporter: edge.parentIsImporter }));
|
|
34
|
+
}
|
|
35
|
+
function hasBumpTarget(edges) {
|
|
36
|
+
return edges.some((edge) => edge.fix.kind === 'bump-manifest' || edge.fix.kind === 'bump-parent');
|
|
37
|
+
}
|
|
38
|
+
function nodeFixKind(edges) {
|
|
39
|
+
const withinRangeCount = edges.filter((edge) => edge.fix.kind === 'within-range').length;
|
|
40
|
+
if (withinRangeCount === edges.length)
|
|
41
|
+
return 'relock';
|
|
42
|
+
if (withinRangeCount > 0)
|
|
43
|
+
return 'partial';
|
|
44
|
+
if (edges.every((edge) => edge.fix.kind === 'no-fix'))
|
|
45
|
+
return 'no-fix';
|
|
46
|
+
return hasBumpTarget(edges) ? 'bump' : 'unknown';
|
|
47
|
+
}
|
|
48
|
+
export function combineFixes(edges) {
|
|
49
|
+
if (edges.length === 0)
|
|
50
|
+
return { kind: 'unknown', bump: [], bumpTruncated: 0 };
|
|
51
|
+
const kind = nodeFixKind(edges);
|
|
52
|
+
const targets = bumpTargets(edges);
|
|
53
|
+
const version = kind === 'relock' ? sharedVersion(edges) : undefined;
|
|
54
|
+
return {
|
|
55
|
+
kind,
|
|
56
|
+
...(version === undefined ? {} : { version }),
|
|
57
|
+
bump: targets.slice(0, BUMP_LIST_CAP),
|
|
58
|
+
bumpTruncated: Math.max(0, targets.length - BUMP_LIST_CAP)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
import { PLACEHOLDER_PREFIX } from "../model.js";
|
|
3
|
+
export function packageVersions(graph) {
|
|
4
|
+
const versions = new Map();
|
|
5
|
+
for (const node of graph.packages.values()) {
|
|
6
|
+
if (node.id.startsWith(PLACEHOLDER_PREFIX))
|
|
7
|
+
continue;
|
|
8
|
+
const known = versions.get(node.name);
|
|
9
|
+
if (known)
|
|
10
|
+
known.add(node.version);
|
|
11
|
+
else
|
|
12
|
+
versions.set(node.name, new Set([node.version]));
|
|
13
|
+
}
|
|
14
|
+
return versions;
|
|
15
|
+
}
|
|
16
|
+
export function compareVersions(left, right) {
|
|
17
|
+
const leftValid = semver.valid(left) !== null;
|
|
18
|
+
const rightValid = semver.valid(right) !== null;
|
|
19
|
+
if (leftValid && rightValid)
|
|
20
|
+
return semver.compare(left, right);
|
|
21
|
+
if (leftValid)
|
|
22
|
+
return -1;
|
|
23
|
+
if (rightValid)
|
|
24
|
+
return 1;
|
|
25
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
26
|
+
}
|
|
27
|
+
function onlyIn(source, other) {
|
|
28
|
+
return [...(source ?? [])].filter((version) => !other?.has(version)).sort(compareVersions);
|
|
29
|
+
}
|
|
30
|
+
export function graphDiff(before, after) {
|
|
31
|
+
const beforeVersions = packageVersions(before);
|
|
32
|
+
const afterVersions = packageVersions(after);
|
|
33
|
+
const names = [...new Set([...beforeVersions.keys(), ...afterVersions.keys()])].sort();
|
|
34
|
+
const changes = [];
|
|
35
|
+
for (const name of names) {
|
|
36
|
+
const removed = onlyIn(beforeVersions.get(name), afterVersions.get(name));
|
|
37
|
+
const added = onlyIn(afterVersions.get(name), beforeVersions.get(name));
|
|
38
|
+
if (removed.length > 0 || added.length > 0)
|
|
39
|
+
changes.push({ name, removed, added });
|
|
40
|
+
}
|
|
41
|
+
return changes;
|
|
42
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { IMPORTER_PREFIX } from "../model.js";
|
|
2
|
+
import { SEVERITY_ORDER } from "../enrich/types.js";
|
|
3
|
+
import { decorations, isDirectDependency } from "./decorations.js";
|
|
4
|
+
import { dependentsSummary, ParentLinkResolver } from "./dependents.js";
|
|
5
|
+
import { nodeAdvisories, nodeMeta, requestedRange, worstSeverity } from "./enrichment-lookup.js";
|
|
6
|
+
import { findNode } from "./lookup.js";
|
|
7
|
+
import { normalizedRequestedRange, nodeFix } from "./node-fix.js";
|
|
8
|
+
import { wantedVersion } from "./versions.js";
|
|
9
|
+
export const VIA_CAP = 20;
|
|
10
|
+
export const IMPORTERS_CAP = 20;
|
|
11
|
+
export const GROUP_ISSUES_CAP = 50;
|
|
12
|
+
const OUTDATED_ORDER = ['major', 'minor', 'patch', undefined];
|
|
13
|
+
function compareText(left, right) {
|
|
14
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
15
|
+
}
|
|
16
|
+
function severityPosition(severity) {
|
|
17
|
+
return severity === undefined ? SEVERITY_ORDER.length : SEVERITY_ORDER.indexOf(severity);
|
|
18
|
+
}
|
|
19
|
+
function compareSortKeys(left, right) {
|
|
20
|
+
return (severityPosition(left.severity) - severityPosition(right.severity) ||
|
|
21
|
+
Number(right.deprecated) - Number(left.deprecated) ||
|
|
22
|
+
OUTDATED_ORDER.indexOf(left.outdatedLevel) - OUTDATED_ORDER.indexOf(right.outdatedLevel) ||
|
|
23
|
+
compareText(left.name, right.name) ||
|
|
24
|
+
compareText(left.version, right.version));
|
|
25
|
+
}
|
|
26
|
+
function sortKeyOf(flagged) {
|
|
27
|
+
return {
|
|
28
|
+
name: flagged.node.name,
|
|
29
|
+
version: flagged.node.version,
|
|
30
|
+
severity: flagged.decoration.vuln?.severity,
|
|
31
|
+
deprecated: flagged.kinds.includes('deprecated'),
|
|
32
|
+
outdatedLevel: flagged.kinds.includes('outdated') ? flagged.decoration.outdated?.level : undefined,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function compareFlagged(left, right) {
|
|
36
|
+
return compareSortKeys(sortKeyOf(left), sortKeyOf(right));
|
|
37
|
+
}
|
|
38
|
+
function issueKindsOf(decoration, isDirect) {
|
|
39
|
+
const kinds = [];
|
|
40
|
+
if (decoration.vuln)
|
|
41
|
+
kinds.push('vulnerability');
|
|
42
|
+
if (decoration.deprecated)
|
|
43
|
+
kinds.push('deprecated');
|
|
44
|
+
if (decoration.outdated && isDirect)
|
|
45
|
+
kinds.push('outdated');
|
|
46
|
+
return kinds;
|
|
47
|
+
}
|
|
48
|
+
function sharedWantedVersion(view, root, node, kinds) {
|
|
49
|
+
const meta = nodeMeta(view, root, node);
|
|
50
|
+
if (!meta)
|
|
51
|
+
return undefined;
|
|
52
|
+
const wanted = new Set();
|
|
53
|
+
for (const link of new ParentLinkResolver(root, kinds).of(node)) {
|
|
54
|
+
if (!link.id.startsWith(IMPORTER_PREFIX))
|
|
55
|
+
continue;
|
|
56
|
+
const range = normalizedRequestedRange(root, node, meta, requestedRange(view, root, findNode(root, link.id) ?? node, link.range, node));
|
|
57
|
+
wanted.add(wantedVersion(meta.versions, range));
|
|
58
|
+
}
|
|
59
|
+
return wanted.size === 1 ? [...wanted][0] : undefined;
|
|
60
|
+
}
|
|
61
|
+
function lazily(compute) {
|
|
62
|
+
let computed = false;
|
|
63
|
+
let value;
|
|
64
|
+
return () => {
|
|
65
|
+
if (!computed) {
|
|
66
|
+
value = compute();
|
|
67
|
+
computed = true;
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function flaggedNodesOf(view, root, kinds) {
|
|
73
|
+
const decorated = decorations(view, root, kinds);
|
|
74
|
+
const result = [];
|
|
75
|
+
for (const node of root.packages.values()) {
|
|
76
|
+
const decoration = decorated[node.id];
|
|
77
|
+
if (!decoration)
|
|
78
|
+
continue;
|
|
79
|
+
const isDirect = isDirectDependency(root, node, kinds);
|
|
80
|
+
const flaggedKinds = issueKindsOf(decoration, isDirect);
|
|
81
|
+
if (flaggedKinds.length === 0)
|
|
82
|
+
continue;
|
|
83
|
+
const summary = dependentsSummary(root, node.id, kinds);
|
|
84
|
+
if (summary.importers.length === 0 && !isDirect)
|
|
85
|
+
continue;
|
|
86
|
+
const deprecatedMessage = decoration.deprecated ? nodeMeta(view, root, node)?.deprecated[node.version] : undefined;
|
|
87
|
+
result.push({
|
|
88
|
+
root,
|
|
89
|
+
node,
|
|
90
|
+
decoration,
|
|
91
|
+
isDirect,
|
|
92
|
+
summary,
|
|
93
|
+
kinds: flaggedKinds,
|
|
94
|
+
vulnIds: decoration.vuln ? nodeAdvisories(view, root, node).map((advisory) => advisory.id) : [],
|
|
95
|
+
...(deprecatedMessage === undefined ? {} : { deprecatedMessage }),
|
|
96
|
+
fix: lazily(() => nodeFix(view, root, node, kinds)),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
function locationOf(flagged) {
|
|
102
|
+
const location = {
|
|
103
|
+
rootId: flagged.root.id,
|
|
104
|
+
nodeId: flagged.node.id,
|
|
105
|
+
via: flagged.summary.via.slice(0, VIA_CAP),
|
|
106
|
+
viaTruncated: Math.max(0, flagged.summary.via.length - VIA_CAP),
|
|
107
|
+
};
|
|
108
|
+
const fix = flagged.fix();
|
|
109
|
+
if (fix)
|
|
110
|
+
location.fix = fix;
|
|
111
|
+
return location;
|
|
112
|
+
}
|
|
113
|
+
function mergedRow(group, wanted) {
|
|
114
|
+
const first = group[0];
|
|
115
|
+
const kinds = ['vulnerability', 'deprecated', 'outdated'].filter((kind) => group.some((flagged) => flagged.kinds.includes(kind)));
|
|
116
|
+
const row = {
|
|
117
|
+
name: first.node.name,
|
|
118
|
+
version: first.node.version,
|
|
119
|
+
kinds,
|
|
120
|
+
vulnIds: [...new Set(group.flatMap((flagged) => flagged.vulnIds))],
|
|
121
|
+
locations: group.map(locationOf),
|
|
122
|
+
};
|
|
123
|
+
const severity = worstSeverity(group.flatMap((flagged) => (flagged.decoration.vuln ? [flagged.decoration.vuln.severity] : [])));
|
|
124
|
+
if (severity)
|
|
125
|
+
row.severity = severity;
|
|
126
|
+
if (group.some((flagged) => flagged.decoration.vuln?.malicious))
|
|
127
|
+
row.malicious = true;
|
|
128
|
+
const deprecatedMessage = group.find((flagged) => flagged.deprecatedMessage !== undefined)?.deprecatedMessage;
|
|
129
|
+
if (deprecatedMessage !== undefined)
|
|
130
|
+
row.deprecatedMessage = deprecatedMessage;
|
|
131
|
+
const outdated = group.find((flagged) => flagged.kinds.includes('outdated'))?.decoration.outdated;
|
|
132
|
+
if (outdated) {
|
|
133
|
+
row.latest = outdated.latest;
|
|
134
|
+
row.outdatedLevel = outdated.level;
|
|
135
|
+
}
|
|
136
|
+
if (wanted !== undefined)
|
|
137
|
+
row.wanted = wanted;
|
|
138
|
+
return row;
|
|
139
|
+
}
|
|
140
|
+
export function issuesByPackage(view, roots, kinds) {
|
|
141
|
+
const groups = new Map();
|
|
142
|
+
for (const root of roots) {
|
|
143
|
+
for (const flagged of flaggedNodesOf(view, root, kinds)) {
|
|
144
|
+
const key = `${flagged.node.name}@${flagged.node.version}`;
|
|
145
|
+
const group = groups.get(key);
|
|
146
|
+
if (group)
|
|
147
|
+
group.push(flagged);
|
|
148
|
+
else
|
|
149
|
+
groups.set(key, [flagged]);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const rows = [];
|
|
153
|
+
for (const group of groups.values()) {
|
|
154
|
+
const wantedVersions = group.map((flagged) => sharedWantedVersion(view, flagged.root, flagged.node, kinds));
|
|
155
|
+
const wanted = new Set(wantedVersions).size === 1 ? wantedVersions[0] : undefined;
|
|
156
|
+
const row = mergedRow(group, wanted);
|
|
157
|
+
rows.push({ row, key: { name: row.name, version: row.version, severity: row.severity, deprecated: row.kinds.includes('deprecated'), outdatedLevel: row.outdatedLevel } });
|
|
158
|
+
}
|
|
159
|
+
return rows.sort((left, right) => compareSortKeys(left.key, right.key)).map((entry) => entry.row);
|
|
160
|
+
}
|
|
161
|
+
function groupFor(builders, root, direct, importers, decoration) {
|
|
162
|
+
const key = `${root.id}|${direct.id}`;
|
|
163
|
+
const existing = builders.get(key);
|
|
164
|
+
if (existing)
|
|
165
|
+
return existing;
|
|
166
|
+
const created = {
|
|
167
|
+
worstSeen: [],
|
|
168
|
+
group: {
|
|
169
|
+
rootId: root.id,
|
|
170
|
+
direct: direct.id,
|
|
171
|
+
name: direct.name,
|
|
172
|
+
version: direct.version,
|
|
173
|
+
importers: importers.slice(0, IMPORTERS_CAP),
|
|
174
|
+
importersTruncated: Math.max(0, importers.length - IMPORTERS_CAP),
|
|
175
|
+
decoration,
|
|
176
|
+
issues: [],
|
|
177
|
+
issuesTruncated: 0,
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
builders.set(key, created);
|
|
181
|
+
return created;
|
|
182
|
+
}
|
|
183
|
+
function issueOf(flagged) {
|
|
184
|
+
const issue = {
|
|
185
|
+
nodeId: flagged.node.id,
|
|
186
|
+
name: flagged.node.name,
|
|
187
|
+
version: flagged.node.version,
|
|
188
|
+
kinds: flagged.kinds,
|
|
189
|
+
vulnIds: flagged.vulnIds,
|
|
190
|
+
};
|
|
191
|
+
if (flagged.decoration.vuln)
|
|
192
|
+
issue.severity = flagged.decoration.vuln.severity;
|
|
193
|
+
const fix = flagged.fix();
|
|
194
|
+
if (fix)
|
|
195
|
+
issue.fix = fix;
|
|
196
|
+
return issue;
|
|
197
|
+
}
|
|
198
|
+
function compareGroups(left, right) {
|
|
199
|
+
return severityPosition(left.worst) - severityPosition(right.worst) || compareText(left.name, right.name) || compareText(left.rootId, right.rootId);
|
|
200
|
+
}
|
|
201
|
+
export function issuesByDirect(view, roots, kinds) {
|
|
202
|
+
const builders = new Map();
|
|
203
|
+
for (const root of roots) {
|
|
204
|
+
const decorated = decorations(view, root, kinds);
|
|
205
|
+
const flaggedNodes = flaggedNodesOf(view, root, kinds).sort(compareFlagged);
|
|
206
|
+
for (const flagged of flaggedNodes) {
|
|
207
|
+
if (flagged.isDirect) {
|
|
208
|
+
const own = flagged.summary.via.find((entry) => entry.direct === flagged.node.id);
|
|
209
|
+
groupFor(builders, root, flagged.node, own?.importers ?? [], decorated[flagged.node.id] ?? {});
|
|
210
|
+
}
|
|
211
|
+
for (const entry of flagged.summary.via) {
|
|
212
|
+
const direct = root.packages.get(entry.direct);
|
|
213
|
+
if (!direct)
|
|
214
|
+
continue;
|
|
215
|
+
const builder = groupFor(builders, root, direct, entry.importers, decorated[direct.id] ?? {});
|
|
216
|
+
if (direct.id === flagged.node.id)
|
|
217
|
+
continue;
|
|
218
|
+
if (flagged.decoration.vuln)
|
|
219
|
+
builder.worstSeen.push(flagged.decoration.vuln.severity);
|
|
220
|
+
if (builder.group.issues.length < GROUP_ISSUES_CAP)
|
|
221
|
+
builder.group.issues.push(issueOf(flagged));
|
|
222
|
+
else
|
|
223
|
+
builder.group.issuesTruncated++;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const groups = [];
|
|
228
|
+
for (const { group, worstSeen } of builders.values()) {
|
|
229
|
+
const worst = worstSeverity(worstSeen);
|
|
230
|
+
if (worst)
|
|
231
|
+
group.worst = worst;
|
|
232
|
+
groups.push(group);
|
|
233
|
+
}
|
|
234
|
+
return groups.sort(compareGroups);
|
|
235
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { IMPORTER_PREFIX, PLACEHOLDER_PREFIX } from "../model.js";
|
|
2
|
+
export const ALL_KINDS = new Set(['prod', 'dev', 'optional', 'peer']);
|
|
3
|
+
export class UnknownNodeError extends Error {
|
|
4
|
+
nodeId;
|
|
5
|
+
constructor(nodeId) {
|
|
6
|
+
super(`Unknown node ${nodeId}`);
|
|
7
|
+
this.name = 'UnknownNodeError';
|
|
8
|
+
this.nodeId = nodeId;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const importerIndexes = new WeakMap();
|
|
12
|
+
function importerIndex(root) {
|
|
13
|
+
const cached = importerIndexes.get(root);
|
|
14
|
+
if (cached)
|
|
15
|
+
return cached;
|
|
16
|
+
const index = new Map(root.importers.map((importer) => [importer.id, importer]));
|
|
17
|
+
importerIndexes.set(root, index);
|
|
18
|
+
return index;
|
|
19
|
+
}
|
|
20
|
+
export function findNode(root, id) {
|
|
21
|
+
return root.packages.get(id) ?? importerIndex(root).get(id);
|
|
22
|
+
}
|
|
23
|
+
export function requireNode(root, id) {
|
|
24
|
+
const node = findNode(root, id);
|
|
25
|
+
if (!node)
|
|
26
|
+
throw new UnknownNodeError(id);
|
|
27
|
+
return node;
|
|
28
|
+
}
|
|
29
|
+
export function viewOf(node) {
|
|
30
|
+
const isImporter = node.id.startsWith(IMPORTER_PREFIX);
|
|
31
|
+
const view = {
|
|
32
|
+
id: node.id,
|
|
33
|
+
name: node.name,
|
|
34
|
+
version: isImporter ? '' : node.version,
|
|
35
|
+
isImporter,
|
|
36
|
+
isPlaceholder: node.id.startsWith(PLACEHOLDER_PREFIX),
|
|
37
|
+
};
|
|
38
|
+
const peerSuffix = node.peerSuffix;
|
|
39
|
+
if (peerSuffix !== undefined)
|
|
40
|
+
view.peerSuffix = peerSuffix;
|
|
41
|
+
return view;
|
|
42
|
+
}
|
|
43
|
+
export function lookup(root, id) {
|
|
44
|
+
const node = findNode(root, id);
|
|
45
|
+
return node ? viewOf(node) : undefined;
|
|
46
|
+
}
|
|
47
|
+
export function passingEdges(node, kinds) {
|
|
48
|
+
return node.deps.filter((edge) => kinds.has(edge.kind));
|
|
49
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { IMPORTER_PREFIX } from "../model.js";
|
|
2
|
+
import { ParentLinkResolver } from "./dependents.js";
|
|
3
|
+
import { completePackageAdvisories, nodeAdvisories, nodeMeta, requestedRange } from "./enrichment-lookup.js";
|
|
4
|
+
import { combineFixes, edgeFix } from "./fixes.js";
|
|
5
|
+
import { findNode } from "./lookup.js";
|
|
6
|
+
import { normalizeRange, safeVersions } from "./versions.js";
|
|
7
|
+
export function normalizedRequestedRange(root, node, meta, requested) {
|
|
8
|
+
return normalizeRange(requested.range, {
|
|
9
|
+
name: node.name,
|
|
10
|
+
catalogs: root.config?.catalogs ?? {},
|
|
11
|
+
distTags: meta?.distTags,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export function safeCandidates(view, root, node, meta) {
|
|
15
|
+
if (meta === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
const advisories = completePackageAdvisories(view, root, node);
|
|
18
|
+
if (advisories === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
return safeVersions(meta, advisories).filter((version) => version !== node.version);
|
|
21
|
+
}
|
|
22
|
+
export function edgeFixes(view, root, node, kinds) {
|
|
23
|
+
const meta = nodeMeta(view, root, node);
|
|
24
|
+
const safe = safeCandidates(view, root, node, meta);
|
|
25
|
+
const result = [];
|
|
26
|
+
for (const link of new ParentLinkResolver(root, kinds).of(node)) {
|
|
27
|
+
const parent = findNode(root, link.id);
|
|
28
|
+
if (!parent)
|
|
29
|
+
continue;
|
|
30
|
+
const parentIsImporter = link.id.startsWith(IMPORTER_PREFIX);
|
|
31
|
+
const range = requestedRange(view, root, parent, link.range, node);
|
|
32
|
+
const fix = edgeFix({
|
|
33
|
+
current: node.version,
|
|
34
|
+
range: normalizedRequestedRange(root, node, meta, range),
|
|
35
|
+
parentIsImporter,
|
|
36
|
+
safe,
|
|
37
|
+
});
|
|
38
|
+
result.push({ parent: link.id, parentIsImporter, range, fix });
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
export function nodeFix(view, root, node, kinds) {
|
|
43
|
+
if (nodeAdvisories(view, root, node).length === 0)
|
|
44
|
+
return undefined;
|
|
45
|
+
return combineFixes(edgeFixes(view, root, node, kinds));
|
|
46
|
+
}
|