selfguard 0.0.1 → 0.2.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 +133 -3
- package/dist/activity.js +12 -0
- package/dist/alerts.js +189 -0
- package/dist/announce.js +136 -0
- package/dist/api.js +494 -0
- package/dist/cli.js +591 -0
- package/dist/config.js +76 -0
- package/dist/discover.js +73 -0
- package/dist/events.js +61 -0
- package/dist/fsutil.js +66 -0
- package/dist/guide.js +61 -0
- package/dist/hook.js +114 -0
- package/dist/lockfile/bun.js +113 -0
- package/dist/lockfile/common.js +79 -0
- package/dist/lockfile/detect.js +42 -0
- package/dist/lockfile/index.js +22 -0
- package/dist/lockfile/npm.js +77 -0
- package/dist/lockfile/pnpm.js +121 -0
- package/dist/lockfile/types.js +1 -0
- package/dist/lockfile/yaml.js +140 -0
- package/dist/lockfile/yarn.js +136 -0
- package/dist/match.js +81 -0
- package/dist/mcp.js +248 -0
- package/dist/notify.js +43 -0
- package/dist/osv.js +142 -0
- package/dist/paths.js +16 -0
- package/dist/repoid.js +171 -0
- package/dist/report.js +126 -0
- package/dist/sanitize.js +12 -0
- package/dist/scan.js +197 -0
- package/dist/schedule.js +142 -0
- package/dist/semver.js +65 -0
- package/dist/setup.js +236 -0
- package/dist/state.js +19 -0
- package/dist/store.js +37 -0
- package/dist/tui.js +132 -0
- package/dist/ui/server.js +325 -0
- package/dist/ui/static/app.css +263 -0
- package/dist/ui/static/app.js +482 -0
- package/dist/ui/static/favicon.svg +1 -0
- package/dist/ui/static/index.html +47 -0
- package/dist/version.js +2 -0
- package/package.json +45 -4
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static parser for package-lock.json / npm-shrinkwrap.json (lockfileVersion 1, 2 and 3).
|
|
3
|
+
* Pure data: never executes anything from the project.
|
|
4
|
+
*/
|
|
5
|
+
import { isObject } from "../fsutil.js";
|
|
6
|
+
import { Collector, depNames, parseAlias } from "./common.js";
|
|
7
|
+
function isNonRegistry(resolved) {
|
|
8
|
+
if (typeof resolved !== "string")
|
|
9
|
+
return false;
|
|
10
|
+
return /^(git\+|git:|github:|file:|link:)/.test(resolved) || resolved.endsWith(".git");
|
|
11
|
+
}
|
|
12
|
+
const NM = "node_modules/";
|
|
13
|
+
function parseV2(packages, out) {
|
|
14
|
+
// Direct deps = deps declared by the root package or any workspace package.
|
|
15
|
+
const direct = new Set();
|
|
16
|
+
for (const [key, entry] of Object.entries(packages)) {
|
|
17
|
+
if (key === "" || !key.includes(NM))
|
|
18
|
+
for (const n of depNames(entry))
|
|
19
|
+
direct.add(n);
|
|
20
|
+
}
|
|
21
|
+
for (const [key, entry] of Object.entries(packages)) {
|
|
22
|
+
if (key === "" || !isObject(entry))
|
|
23
|
+
continue;
|
|
24
|
+
const idx = key.lastIndexOf(NM);
|
|
25
|
+
if (idx === -1)
|
|
26
|
+
continue; // workspace source folder, not an installed package
|
|
27
|
+
if (entry.link === true)
|
|
28
|
+
continue; // symlink to a workspace
|
|
29
|
+
if (isNonRegistry(entry.resolved)) {
|
|
30
|
+
out.skipped++;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const installedAs = key.slice(idx + NM.length);
|
|
34
|
+
const name = typeof entry.name === "string" ? entry.name : installedAs; // aliases carry the real name
|
|
35
|
+
const version = typeof entry.version === "string" ? entry.version : "";
|
|
36
|
+
const isTopLevel = key.indexOf(NM) === idx; // only one node_modules segment in the path
|
|
37
|
+
out.add(name, version, isTopLevel && direct.has(installedAs), entry.dev === true);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function parseV1(deps, out, direct, depth) {
|
|
41
|
+
if (depth > 200)
|
|
42
|
+
return; // defensive: malformed/cyclic input
|
|
43
|
+
for (const [installedAs, entry] of Object.entries(deps)) {
|
|
44
|
+
if (!isObject(entry))
|
|
45
|
+
continue;
|
|
46
|
+
const spec = typeof entry.version === "string" ? entry.version : "";
|
|
47
|
+
if (isNonRegistry(entry.resolved) || isNonRegistry(spec)) {
|
|
48
|
+
out.skipped++;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const alias = parseAlias(spec);
|
|
52
|
+
out.add(alias?.name ?? installedAs, alias?.version ?? spec, depth === 0 && direct.has(installedAs), entry.dev === true);
|
|
53
|
+
}
|
|
54
|
+
if (isObject(entry.dependencies))
|
|
55
|
+
parseV1(entry.dependencies, out, direct, depth + 1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* @param lockText raw contents of package-lock.json
|
|
60
|
+
* @param manifest parsed package.json (only needed for lockfileVersion 1 to know direct deps)
|
|
61
|
+
*/
|
|
62
|
+
export function parseNpmLockfile(lockText, manifest) {
|
|
63
|
+
const lock = JSON.parse(lockText);
|
|
64
|
+
if (!isObject(lock))
|
|
65
|
+
throw new Error("package-lock.json is not a JSON object");
|
|
66
|
+
const out = new Collector();
|
|
67
|
+
if (isObject(lock.packages)) {
|
|
68
|
+
parseV2(lock.packages, out);
|
|
69
|
+
}
|
|
70
|
+
else if (isObject(lock.dependencies)) {
|
|
71
|
+
parseV1(lock.dependencies, out, depNames(manifest), 0);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
throw new Error("unrecognized package-lock.json format");
|
|
75
|
+
}
|
|
76
|
+
return out.result();
|
|
77
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static parser for pnpm-lock.yaml (lockfileVersion 5.x, 6.x and 9.x).
|
|
3
|
+
* Pure data: never executes anything from the project.
|
|
4
|
+
*/
|
|
5
|
+
import { Collector, devOnlyNames, reachable, splitNameAt } from "./common.js";
|
|
6
|
+
import { isMap, parseYamlDocuments } from "./yaml.js";
|
|
7
|
+
const IMPORTER_PROD = ["dependencies", "optionalDependencies"];
|
|
8
|
+
const IMPORTER_DEV = ["devDependencies"];
|
|
9
|
+
/** Drop peer-dependency suffixes: "18.2.0(react@18.2.0)" (v6+) or "18.2.0_react@18.2.0" (v5). */
|
|
10
|
+
function stripPeers(v) {
|
|
11
|
+
const paren = v.indexOf("(");
|
|
12
|
+
const base = paren >= 0 ? v.slice(0, paren) : v;
|
|
13
|
+
const underscore = base.indexOf("_");
|
|
14
|
+
return underscore >= 0 ? base.slice(0, underscore) : base;
|
|
15
|
+
}
|
|
16
|
+
/** Package key -> name/version. v9 "name@1.0.0(peers)", v6 "/name@1.0.0(peers)", v5 "/name/1.0.0_peers". */
|
|
17
|
+
function parseKey(key, major) {
|
|
18
|
+
const k = key.startsWith("/") ? key.slice(1) : key;
|
|
19
|
+
if (major < 6) {
|
|
20
|
+
const slash = k.lastIndexOf("/");
|
|
21
|
+
if (slash <= 0)
|
|
22
|
+
return null;
|
|
23
|
+
return { name: k.slice(0, slash), version: stripPeers(k.slice(slash + 1)) };
|
|
24
|
+
}
|
|
25
|
+
const parsed = splitNameAt(k.split("(")[0]);
|
|
26
|
+
return parsed ? { name: parsed.name, version: stripPeers(parsed.version) } : null;
|
|
27
|
+
}
|
|
28
|
+
/** Importer entry "version" -> snapshot key, handling aliases ("left-pad@1.3.0", "/left-pad@1.3.0"). */
|
|
29
|
+
function importerTarget(name, ref, major) {
|
|
30
|
+
if (/^(link|file|workspace):/.test(ref))
|
|
31
|
+
return null;
|
|
32
|
+
if (ref.startsWith("/"))
|
|
33
|
+
return major < 6 ? ref : ref.slice(1);
|
|
34
|
+
// "1.2.3(...)" is a plain version; "real@1.2.3(...)" is an alias target.
|
|
35
|
+
const head = ref.split("(")[0];
|
|
36
|
+
if (/^\d/.test(head))
|
|
37
|
+
return major < 6 ? `/${name}/${ref}` : `${name}@${ref}`;
|
|
38
|
+
return major < 6 ? `/${ref}` : ref;
|
|
39
|
+
}
|
|
40
|
+
function refOf(entry) {
|
|
41
|
+
if (typeof entry === "string")
|
|
42
|
+
return entry; // v5: name: version
|
|
43
|
+
if (isMap(entry) && typeof entry.version === "string")
|
|
44
|
+
return entry.version; // v6/v9: {specifier, version}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
export function parsePnpmLockfile(text, manifest) {
|
|
48
|
+
const docs = parseYamlDocuments(text);
|
|
49
|
+
// pnpm 10 can prepend a document for config dependencies; the main one has importers/packages.
|
|
50
|
+
const doc = docs.find((d) => isMap(d.importers) || isMap(d.packages)) ?? docs.find((d) => d.lockfileVersion !== undefined);
|
|
51
|
+
if (!doc || doc.lockfileVersion === undefined)
|
|
52
|
+
throw new Error("not a pnpm lockfile");
|
|
53
|
+
const major = Math.trunc(Number.parseFloat(String(doc.lockfileVersion)));
|
|
54
|
+
if (!Number.isFinite(major) || major < 5)
|
|
55
|
+
throw new Error(`unsupported pnpm lockfileVersion ${String(doc.lockfileVersion)}`);
|
|
56
|
+
const importers = isMap(doc.importers) ? doc.importers : { ".": doc };
|
|
57
|
+
const packages = isMap(doc.packages) ? doc.packages : Object.create(null);
|
|
58
|
+
// v9 keeps the graph in "snapshots"; older versions keep it in "packages".
|
|
59
|
+
const graph = isMap(doc.snapshots) ? doc.snapshots : packages;
|
|
60
|
+
const directPkgs = new Set(); // "name@version" depended on by some importer
|
|
61
|
+
const prodRoots = [];
|
|
62
|
+
const devOnly = devOnlyNames(manifest);
|
|
63
|
+
for (const importer of Object.values(importers)) {
|
|
64
|
+
if (!isMap(importer))
|
|
65
|
+
continue;
|
|
66
|
+
for (const field of [...IMPORTER_PROD, ...IMPORTER_DEV]) {
|
|
67
|
+
const deps = importer[field];
|
|
68
|
+
if (!isMap(deps))
|
|
69
|
+
continue;
|
|
70
|
+
for (const [name, entry] of Object.entries(deps)) {
|
|
71
|
+
const ref = refOf(entry);
|
|
72
|
+
const target = ref ? importerTarget(name, ref, major) : null;
|
|
73
|
+
if (!target)
|
|
74
|
+
continue;
|
|
75
|
+
const pkg = parseKey(target, major);
|
|
76
|
+
if (pkg)
|
|
77
|
+
directPkgs.add(`${pkg.name}@${pkg.version}`);
|
|
78
|
+
if (IMPORTER_PROD.includes(field) && !devOnly.has(name))
|
|
79
|
+
prodRoots.push(target);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const edges = (key) => {
|
|
84
|
+
const node = graph[key];
|
|
85
|
+
if (!isMap(node))
|
|
86
|
+
return [];
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const field of ["dependencies", "optionalDependencies"]) {
|
|
89
|
+
const deps = node[field];
|
|
90
|
+
if (!isMap(deps))
|
|
91
|
+
continue;
|
|
92
|
+
for (const [name, ref] of Object.entries(deps)) {
|
|
93
|
+
const target = typeof ref === "string" ? importerTarget(name, ref, major) : null;
|
|
94
|
+
if (target)
|
|
95
|
+
out.push(target);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
};
|
|
100
|
+
const prodKeys = reachable(prodRoots, edges);
|
|
101
|
+
const prodPkgs = new Set([...prodKeys].map((k) => parseKey(k, major)).filter(Boolean).map((p) => `${p.name}@${p.version}`));
|
|
102
|
+
const out = new Collector();
|
|
103
|
+
// Every installed package appears in the graph (snapshots in v9, packages before).
|
|
104
|
+
for (const [key, entry] of Object.entries(graph)) {
|
|
105
|
+
const pkg = parseKey(key, major);
|
|
106
|
+
if (!pkg) {
|
|
107
|
+
out.skipped++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const meta = isMap(packages[key]) ? packages[key] : isMap(entry) ? entry : undefined;
|
|
111
|
+
const resolution = meta && isMap(meta.resolution) ? meta.resolution : meta?.resolution;
|
|
112
|
+
if (typeof resolution === "string" && /(tarball|repo|directory|commit):/.test(resolution) && !/integrity:/.test(resolution)) {
|
|
113
|
+
out.skipped++; // git / tarball / local directory deps
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const legacyDev = isMap(entry) ? entry.dev : undefined; // v5/v6 mark dev explicitly
|
|
117
|
+
const dev = major < 9 && legacyDev !== undefined ? legacyDev === "true" : !prodPkgs.has(`${pkg.name}@${pkg.version}`);
|
|
118
|
+
out.add(pkg.name, pkg.version, directPkgs.has(`${pkg.name}@${pkg.version}`), dev);
|
|
119
|
+
}
|
|
120
|
+
return out.result();
|
|
121
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal YAML reader for the subset used by pnpm-lock.yaml and yarn (berry) yarn.lock:
|
|
3
|
+
* block mappings by indentation, plain/single/double-quoted scalars, `---` documents.
|
|
4
|
+
* Flow collections ({...} / [...]) are kept as raw strings and sequence items are
|
|
5
|
+
* ignored: lockfile parsers never need them.
|
|
6
|
+
*
|
|
7
|
+
* Hardened for untrusted input: objects have no prototype (so keys like "__proto__"
|
|
8
|
+
* are plain data), nesting depth is bounded, and there is no tag/anchor/alias support
|
|
9
|
+
* (no "billion laughs").
|
|
10
|
+
*/
|
|
11
|
+
const MAX_DEPTH = 64;
|
|
12
|
+
const newMap = () => Object.create(null);
|
|
13
|
+
export function isMap(v) {
|
|
14
|
+
return typeof v === "object" && v !== null;
|
|
15
|
+
}
|
|
16
|
+
/** Parse a key and return [key, restAfterColon], or null if the line has no mapping key. */
|
|
17
|
+
function splitKey(line) {
|
|
18
|
+
let key;
|
|
19
|
+
let i;
|
|
20
|
+
if (line[0] === "'" || line[0] === '"') {
|
|
21
|
+
const q = line[0];
|
|
22
|
+
i = 1;
|
|
23
|
+
let out = "";
|
|
24
|
+
for (;;) {
|
|
25
|
+
if (i >= line.length)
|
|
26
|
+
return null;
|
|
27
|
+
const ch = line[i];
|
|
28
|
+
if (q === "'" && ch === "'" && line[i + 1] === "'") {
|
|
29
|
+
out += "'";
|
|
30
|
+
i += 2;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (q === '"' && ch === "\\" && i + 1 < line.length) {
|
|
34
|
+
out += unescapeDouble(line.slice(i, i + 2));
|
|
35
|
+
i += 2;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (ch === q)
|
|
39
|
+
break;
|
|
40
|
+
out += ch;
|
|
41
|
+
i++;
|
|
42
|
+
}
|
|
43
|
+
key = out;
|
|
44
|
+
i++; // closing quote
|
|
45
|
+
while (line[i] === " ")
|
|
46
|
+
i++;
|
|
47
|
+
if (line[i] !== ":")
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const m = /:(?:\s|$)/.exec(line);
|
|
52
|
+
if (!m)
|
|
53
|
+
return null;
|
|
54
|
+
i = m.index;
|
|
55
|
+
key = line.slice(0, i).trimEnd();
|
|
56
|
+
}
|
|
57
|
+
return [key, line.slice(i + 1).trim()];
|
|
58
|
+
}
|
|
59
|
+
function unescapeDouble(seq) {
|
|
60
|
+
switch (seq[1]) {
|
|
61
|
+
case "n":
|
|
62
|
+
return "\n";
|
|
63
|
+
case "t":
|
|
64
|
+
return "\t";
|
|
65
|
+
case '"':
|
|
66
|
+
return '"';
|
|
67
|
+
case "\\":
|
|
68
|
+
return "\\";
|
|
69
|
+
case "/":
|
|
70
|
+
return "/";
|
|
71
|
+
default:
|
|
72
|
+
return seq;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function scalar(raw) {
|
|
76
|
+
if (raw.startsWith("'")) {
|
|
77
|
+
const end = raw.lastIndexOf("'");
|
|
78
|
+
return end > 0 ? raw.slice(1, end).replace(/''/g, "'") : raw;
|
|
79
|
+
}
|
|
80
|
+
if (raw.startsWith('"')) {
|
|
81
|
+
const end = raw.lastIndexOf('"');
|
|
82
|
+
if (end <= 0)
|
|
83
|
+
return raw;
|
|
84
|
+
return raw.slice(1, end).replace(/\\./g, unescapeDouble);
|
|
85
|
+
}
|
|
86
|
+
if (raw.startsWith("{") || raw.startsWith("["))
|
|
87
|
+
return raw;
|
|
88
|
+
const comment = raw.search(/\s#/);
|
|
89
|
+
return (comment >= 0 ? raw.slice(0, comment) : raw).trim();
|
|
90
|
+
}
|
|
91
|
+
export function parseYamlDocuments(text) {
|
|
92
|
+
const docs = [];
|
|
93
|
+
let root = newMap();
|
|
94
|
+
docs.push(root);
|
|
95
|
+
let stack = [{ indent: -1, node: root }];
|
|
96
|
+
let skipAbove = -1; // skipping a block scalar's lines (indent > skipAbove)
|
|
97
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
98
|
+
const body = raw.trimStart();
|
|
99
|
+
const indent = raw.length - body.length;
|
|
100
|
+
if (skipAbove >= 0) {
|
|
101
|
+
if (body === "" || indent > skipAbove)
|
|
102
|
+
continue;
|
|
103
|
+
skipAbove = -1;
|
|
104
|
+
}
|
|
105
|
+
if (body === "" || body.startsWith("#"))
|
|
106
|
+
continue;
|
|
107
|
+
if (indent === 0 && (body.startsWith("---") || body.startsWith("..."))) {
|
|
108
|
+
if (body.startsWith("---")) {
|
|
109
|
+
root = newMap();
|
|
110
|
+
docs.push(root);
|
|
111
|
+
stack = [{ indent: -1, node: root }];
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].indent)
|
|
116
|
+
stack.pop();
|
|
117
|
+
if (body === "-" || body.startsWith("- "))
|
|
118
|
+
continue; // sequence item: not needed
|
|
119
|
+
const kv = splitKey(body);
|
|
120
|
+
if (!kv)
|
|
121
|
+
continue;
|
|
122
|
+
const [key, rest] = kv;
|
|
123
|
+
const parent = stack[stack.length - 1].node;
|
|
124
|
+
if (rest === "") {
|
|
125
|
+
if (stack.length > MAX_DEPTH)
|
|
126
|
+
throw new Error("YAML nesting too deep");
|
|
127
|
+
const child = newMap();
|
|
128
|
+
parent[key] = child;
|
|
129
|
+
stack.push({ indent, node: child });
|
|
130
|
+
}
|
|
131
|
+
else if (/^[|>][-+0-9]*$/.test(rest)) {
|
|
132
|
+
parent[key] = ""; // block scalar: content not needed
|
|
133
|
+
skipAbove = indent;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
parent[key] = scalar(rest);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return docs.filter((d) => Object.keys(d).length > 0);
|
|
140
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static parsers for yarn.lock: classic (v1, custom format) and berry (v2+, YAML).
|
|
3
|
+
* Pure data: never executes anything from the project.
|
|
4
|
+
*/
|
|
5
|
+
import { Collector, depsOf, devOnlyNames, parseAlias, PROD_FIELDS, reachable, splitNameAt, ALL_FIELDS } from "./common.js";
|
|
6
|
+
import { isMap, parseYamlDocuments } from "./yaml.js";
|
|
7
|
+
function unquote(s) {
|
|
8
|
+
const t = s.trim();
|
|
9
|
+
return t.length >= 2 && t.startsWith('"') && t.endsWith('"') ? t.slice(1, -1) : t;
|
|
10
|
+
}
|
|
11
|
+
/** Shared tail: classify direct/dev by walking from the manifest's production deps. */
|
|
12
|
+
function collect(entries, manifest, descriptor) {
|
|
13
|
+
const out = new Collector();
|
|
14
|
+
const devOnly = devOnlyNames(manifest);
|
|
15
|
+
const all = depsOf(manifest, ALL_FIELDS);
|
|
16
|
+
const prodRoots = [...depsOf(manifest, PROD_FIELDS)]
|
|
17
|
+
.filter(([name]) => !devOnly.has(name))
|
|
18
|
+
.map(([name, spec]) => descriptor(name, spec));
|
|
19
|
+
const haveManifest = all.size > 0;
|
|
20
|
+
const prod = reachable(prodRoots, (d) => (entries.get(d)?.deps ?? []).map(([n, s]) => descriptor(n, s)));
|
|
21
|
+
const prodPkgs = new Set([...prod].map((d) => entries.get(d)).filter(Boolean).map((e) => `${e.name}@${e.version}`));
|
|
22
|
+
const directPkgs = new Set([...all].map(([n, s]) => entries.get(descriptor(n, s))).filter(Boolean).map((e) => `${e.name}@${e.version}`));
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
for (const e of entries.values()) {
|
|
25
|
+
if (seen.has(e))
|
|
26
|
+
continue; // one entry is shared by several descriptors
|
|
27
|
+
seen.add(e);
|
|
28
|
+
if (!e.registry) {
|
|
29
|
+
out.skipped++;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const key = `${e.name}@${e.version}`;
|
|
33
|
+
out.add(e.name, e.version, directPkgs.has(key), haveManifest ? !prodPkgs.has(key) : false);
|
|
34
|
+
}
|
|
35
|
+
return out.result();
|
|
36
|
+
}
|
|
37
|
+
// ---------- classic (v1) ----------
|
|
38
|
+
function parseClassic(text, manifest) {
|
|
39
|
+
const entries = new Map();
|
|
40
|
+
let current = null;
|
|
41
|
+
let inDeps = false;
|
|
42
|
+
for (const line of text.split(/\r?\n/)) {
|
|
43
|
+
if (line === "" || line.startsWith("#"))
|
|
44
|
+
continue;
|
|
45
|
+
const indent = line.length - line.trimStart().length;
|
|
46
|
+
const body = line.trim();
|
|
47
|
+
if (indent === 0) {
|
|
48
|
+
inDeps = false;
|
|
49
|
+
if (!body.endsWith(":")) {
|
|
50
|
+
current = null;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const descriptors = body.slice(0, -1).split(/,\s*/).map(unquote);
|
|
54
|
+
const first = splitNameAt(descriptors[0] ?? "");
|
|
55
|
+
if (!first) {
|
|
56
|
+
current = null;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const alias = parseAlias(first.version);
|
|
60
|
+
const spec = alias ? alias.version : first.version;
|
|
61
|
+
// git/file/link/tarball specs, or GitHub "user/repo" shorthand, are not registry packages
|
|
62
|
+
const registry = !/^(file|link|git|github|https?|portal|exec):|^git\+|^[\w.-]+\/[\w.-]+(#.*)?$/.test(spec);
|
|
63
|
+
current = { name: alias?.name ?? first.name, version: "", registry, deps: [] };
|
|
64
|
+
for (const d of descriptors)
|
|
65
|
+
entries.set(d, current);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!current)
|
|
69
|
+
continue;
|
|
70
|
+
if (indent === 2) {
|
|
71
|
+
inDeps = body === "dependencies:" || body === "optionalDependencies:";
|
|
72
|
+
const m = /^(\S+)\s+(.*)$/.exec(body);
|
|
73
|
+
if (m && m[1] === "version")
|
|
74
|
+
current.version = unquote(m[2]);
|
|
75
|
+
if (m && m[1] === "resolved" && /^"?(git|file:|link:)|codeload\.github\.com|\.git(#|"?$)/.test(m[2]))
|
|
76
|
+
current.registry = false;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (indent >= 4 && inDeps) {
|
|
80
|
+
const m = /^("(?:[^"\\]|\\.)*"|\S+)\s+(.*)$/.exec(body);
|
|
81
|
+
if (m)
|
|
82
|
+
current.deps.push([unquote(m[1]), unquote(m[2])]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return collect(entries, manifest, (name, spec) => `${name}@${spec}`);
|
|
86
|
+
}
|
|
87
|
+
// ---------- berry (v2+) ----------
|
|
88
|
+
function parseBerry(text, manifest) {
|
|
89
|
+
const doc = parseYamlDocuments(text)[0];
|
|
90
|
+
if (!doc)
|
|
91
|
+
throw new Error("empty yarn.lock");
|
|
92
|
+
const entries = new Map();
|
|
93
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
94
|
+
if (key === "__metadata" || !isMap(value))
|
|
95
|
+
continue;
|
|
96
|
+
const resolution = typeof value.resolution === "string" ? value.resolution : "";
|
|
97
|
+
let resolved = splitNameAt(resolution);
|
|
98
|
+
let registry = false;
|
|
99
|
+
if (resolved?.version.startsWith("npm:")) {
|
|
100
|
+
registry = true;
|
|
101
|
+
resolved = { name: resolved.name, version: resolved.version.slice(4) };
|
|
102
|
+
}
|
|
103
|
+
else if (resolved?.version.startsWith("patch:")) {
|
|
104
|
+
// "name@patch:name@npm%3A1.2.3#..." -> the patched npm package
|
|
105
|
+
const inner = splitNameAt(decodeURIComponent(resolved.version.slice(6).split("#")[0]));
|
|
106
|
+
if (inner?.version.startsWith("npm:")) {
|
|
107
|
+
registry = true;
|
|
108
|
+
resolved = { name: inner.name, version: inner.version.slice(4) };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const deps = [];
|
|
112
|
+
for (const field of ["dependencies", "optionalDependencies"]) {
|
|
113
|
+
const d = value[field];
|
|
114
|
+
if (isMap(d))
|
|
115
|
+
for (const [n, s] of Object.entries(d))
|
|
116
|
+
if (typeof s === "string")
|
|
117
|
+
deps.push([n, s]);
|
|
118
|
+
}
|
|
119
|
+
const entry = { name: resolved?.name ?? key, version: resolved?.version ?? "", registry, deps };
|
|
120
|
+
const isWorkspace = /@workspace:/.test(resolution);
|
|
121
|
+
for (const d of key.split(/,\s*/))
|
|
122
|
+
entries.set(d.trim(), entry);
|
|
123
|
+
if (isWorkspace)
|
|
124
|
+
entries.set(`__workspace__:${key}`, { ...entry, registry: false });
|
|
125
|
+
}
|
|
126
|
+
// Berry descriptors always carry a protocol; manifest ranges without one mean npm:.
|
|
127
|
+
const descriptor = (name, spec) => `${name}@${/^[a-z]+:/.test(spec) ? spec : `npm:${spec}`}`;
|
|
128
|
+
return collect(new Map([...entries].filter(([k, e]) => !k.startsWith("__workspace__:") && !(e.version === "0.0.0-use.local"))), manifest, descriptor);
|
|
129
|
+
}
|
|
130
|
+
export function parseYarnLockfile(text, manifest) {
|
|
131
|
+
if (/^__metadata:/m.test(text))
|
|
132
|
+
return parseBerry(text, manifest);
|
|
133
|
+
if (/^# yarn lockfile v1/m.test(text) || /^\S.*:\n {2}version "/m.test(text))
|
|
134
|
+
return parseClassic(text, manifest);
|
|
135
|
+
throw new Error("unrecognized yarn.lock format");
|
|
136
|
+
}
|
package/dist/match.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { compareParsed, parse } from "./semver.js";
|
|
2
|
+
export const SEVERITY_ORDER = ["malicious", "critical", "high", "moderate", "low", "unknown"];
|
|
3
|
+
export function severityRank(s) {
|
|
4
|
+
return SEVERITY_ORDER.indexOf(s);
|
|
5
|
+
}
|
|
6
|
+
export function severityOf(vuln) {
|
|
7
|
+
if (vuln.id.startsWith("MAL-") || vuln.aliases?.some((a) => a.startsWith("MAL-")))
|
|
8
|
+
return "malicious";
|
|
9
|
+
const raw = vuln.database_specific?.severity;
|
|
10
|
+
switch (typeof raw === "string" ? raw.toUpperCase() : "") {
|
|
11
|
+
case "CRITICAL":
|
|
12
|
+
return "critical";
|
|
13
|
+
case "HIGH":
|
|
14
|
+
return "high";
|
|
15
|
+
case "MODERATE":
|
|
16
|
+
case "MEDIUM":
|
|
17
|
+
return "moderate";
|
|
18
|
+
case "LOW":
|
|
19
|
+
return "low";
|
|
20
|
+
default:
|
|
21
|
+
return "unknown";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const ZERO = { major: 0, minor: 0, patch: 0, pre: [] };
|
|
25
|
+
function eventVersion(e) {
|
|
26
|
+
const v = e.introduced ?? e.fixed ?? e.last_affected ?? e.limit;
|
|
27
|
+
if (v === undefined)
|
|
28
|
+
return null;
|
|
29
|
+
if (e.introduced === "0")
|
|
30
|
+
return { ...ZERO, pre: [0] }; // lower than anything, incl. 0.0.0-x
|
|
31
|
+
return parse(v);
|
|
32
|
+
}
|
|
33
|
+
export function evaluate(vuln, name, version) {
|
|
34
|
+
const v = parse(version);
|
|
35
|
+
if (!v)
|
|
36
|
+
return { affected: false };
|
|
37
|
+
let affected = false;
|
|
38
|
+
const fixes = [];
|
|
39
|
+
for (const aff of vuln.affected ?? []) {
|
|
40
|
+
if (aff.package?.ecosystem !== "npm" || aff.package?.name !== name)
|
|
41
|
+
continue;
|
|
42
|
+
if (Array.isArray(aff.versions) && aff.versions.includes(version))
|
|
43
|
+
affected = true;
|
|
44
|
+
for (const range of aff.ranges ?? []) {
|
|
45
|
+
if (range.type !== "SEMVER" && range.type !== "ECOSYSTEM")
|
|
46
|
+
continue;
|
|
47
|
+
const events = (range.events ?? [])
|
|
48
|
+
.map((e) => ({ e, sv: eventVersion(e) }))
|
|
49
|
+
.filter((x) => x.sv !== null)
|
|
50
|
+
.sort((a, b) => compareParsed(a.sv, b.sv));
|
|
51
|
+
let inRange = false;
|
|
52
|
+
for (const { e, sv } of events) {
|
|
53
|
+
const c = compareParsed(v, sv);
|
|
54
|
+
if (e.introduced !== undefined) {
|
|
55
|
+
if (c >= 0)
|
|
56
|
+
inRange = true;
|
|
57
|
+
}
|
|
58
|
+
else if (e.fixed !== undefined) {
|
|
59
|
+
if (c >= 0)
|
|
60
|
+
inRange = false;
|
|
61
|
+
else
|
|
62
|
+
fixes.push({ raw: e.fixed, sv });
|
|
63
|
+
}
|
|
64
|
+
else if (e.last_affected !== undefined) {
|
|
65
|
+
if (c > 0)
|
|
66
|
+
inRange = false;
|
|
67
|
+
}
|
|
68
|
+
else if (e.limit !== undefined) {
|
|
69
|
+
if (c >= 0)
|
|
70
|
+
inRange = false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (inRange)
|
|
74
|
+
affected = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!affected)
|
|
78
|
+
return { affected };
|
|
79
|
+
fixes.sort((a, b) => compareParsed(a.sv, b.sv));
|
|
80
|
+
return fixes[0] ? { affected, fixedIn: fixes[0].raw } : { affected };
|
|
81
|
+
}
|