depbrief 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 +61 -0
- package/bin/depbrief.js +78 -0
- package/lib/discover.js +54 -0
- package/lib/index.js +64 -0
- package/lib/npmRegistry.js +31 -0
- package/lib/osv.js +33 -0
- package/lib/report.js +48 -0
- package/lib/semver.js +36 -0
- package/lib/verdict.js +50 -0
- package/package.json +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Liam McCall
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# depbrief
|
|
2
|
+
|
|
3
|
+
One verdict per dependency upgrade — semver bump class, known vulnerabilities, deprecation status. No GitHub App to install, no account, no config.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
$ npx depbrief
|
|
7
|
+
|
|
8
|
+
[AVOID] lodash 4.17.20 -> 4.17.21 (patch)
|
|
9
|
+
- the version you would upgrade to has a known unpatched vulnerability
|
|
10
|
+
[REVIEW] express 4.18.2 -> 5.0.0 (major)
|
|
11
|
+
- major version bump — breaking changes are likely, check the changelog
|
|
12
|
+
[URGENT] axios 0.21.1 -> 0.21.4 (patch)
|
|
13
|
+
- current version has a known vulnerability — upgrading resolves it
|
|
14
|
+
[SAFE] chalk 5.2.0 -> 5.3.0 (minor)
|
|
15
|
+
- minor bump, no known vulnerabilities, not deprecated
|
|
16
|
+
|
|
17
|
+
4 of 41 dependencies have updates available (37 already up to date).
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Why
|
|
21
|
+
|
|
22
|
+
`npm outdated` tells you what's outdated. `npm audit` tells you what's *currently* vulnerable. Neither tells you, in one line, whether upgrading from A to B is the kind of thing you can merge without looking, or the kind of thing that needs a human. Renovate and Dependabot solve this well but require installing a GitHub App and living in PRs — `depbrief` is the same idea as a 2-second local command, for the moment before you even open one.
|
|
23
|
+
|
|
24
|
+
## What it checks, per dependency
|
|
25
|
+
|
|
26
|
+
- **Semver bump class** — patch / minor / major (0.x minor bumps are treated as major, since semver itself makes no compatibility promise below 1.0).
|
|
27
|
+
- **Known vulnerabilities** — queries [OSV.dev](https://osv.dev) (free, no API key) for both the current and target version.
|
|
28
|
+
- **Deprecation** — checks whether the target version is marked deprecated on the npm registry.
|
|
29
|
+
|
|
30
|
+
These combine into one of five verdicts, worst first: `AVOID` (target itself has a known vuln) > `URGENT` (current version has a known vuln) > `REVIEW` (major bump) > `CAUTION` (deprecated target, or a downgrade) > `SAFE`.
|
|
31
|
+
|
|
32
|
+
## Install / usage
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# scan the current project's package.json + lockfile
|
|
36
|
+
npx depbrief
|
|
37
|
+
|
|
38
|
+
# check one upgrade directly, no project needed
|
|
39
|
+
npx depbrief express 4.18.2 5.0.0
|
|
40
|
+
|
|
41
|
+
# machine-readable output
|
|
42
|
+
npx depbrief --json
|
|
43
|
+
|
|
44
|
+
# CI gate: fail the build on anything AVOID or URGENT
|
|
45
|
+
npx depbrief --fail-on AVOID,URGENT
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Needs network access (npm registry + OSV.dev, both public and free). Zero runtime dependencies — uses Node's built-in `fetch`.
|
|
49
|
+
|
|
50
|
+
## Limitations (v1)
|
|
51
|
+
|
|
52
|
+
- Semver parsing is deliberately minimal (no build-metadata, no range satisfaction) — it classifies two concrete versions, it doesn't resolve ranges.
|
|
53
|
+
- No changelog summarization yet — verdicts are based on structured signals (bump class, OSV, deprecation), not parsed release notes. That's a reasonable v2 direction if there's demand.
|
|
54
|
+
|
|
55
|
+
## Contributing
|
|
56
|
+
|
|
57
|
+
Issues and PRs welcome. Decision logic lives in `lib/verdict.js` and `lib/semver.js`, both pure/offline and unit tested — network I/O is isolated in `lib/npmRegistry.js` and `lib/osv.js`.
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
package/bin/depbrief.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { analyzeProject, analyzePair } = require('../lib/index');
|
|
4
|
+
const { printReport, printJson } = require('../lib/report');
|
|
5
|
+
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
const args = { positional: [], json: false, failOn: null, help: false };
|
|
8
|
+
for (let i = 0; i < argv.length; i++) {
|
|
9
|
+
const a = argv[i];
|
|
10
|
+
if (a === '--json') args.json = true;
|
|
11
|
+
else if (a === '--fail-on') args.failOn = argv[++i];
|
|
12
|
+
else if (a === '-h' || a === '--help') args.help = true;
|
|
13
|
+
else args.positional.push(a);
|
|
14
|
+
}
|
|
15
|
+
return args;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function printHelp() {
|
|
19
|
+
console.log(`depbrief - one verdict per dependency upgrade (semver bump, known vulns, deprecation)
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
depbrief Scan package.json + lockfile in the current directory
|
|
23
|
+
depbrief <pkg> <from> <to> Check a single upgrade directly, no project needed
|
|
24
|
+
depbrief --json Machine-readable output
|
|
25
|
+
depbrief --fail-on AVOID,URGENT Exit non-zero if any result matches these verdicts (for CI)
|
|
26
|
+
|
|
27
|
+
Verdicts: AVOID > URGENT > REVIEW > CAUTION > SAFE
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const VERDICT_RANK = { AVOID: 0, URGENT: 1, REVIEW: 2, CAUTION: 3, SAFE: 4 };
|
|
32
|
+
|
|
33
|
+
async function main() {
|
|
34
|
+
const args = parseArgs(process.argv.slice(2));
|
|
35
|
+
if (args.help) {
|
|
36
|
+
printHelp();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let results;
|
|
41
|
+
let meta = {};
|
|
42
|
+
|
|
43
|
+
if (args.positional.length === 3) {
|
|
44
|
+
const [name, from, to] = args.positional;
|
|
45
|
+
results = await analyzePair(name, from, to);
|
|
46
|
+
} else if (args.positional.length === 0) {
|
|
47
|
+
const projectResult = await analyzeProject(process.cwd());
|
|
48
|
+
if (!projectResult) {
|
|
49
|
+
console.log('No package.json found in the current directory.');
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
results = projectResult.results;
|
|
54
|
+
meta = { upToDateCount: projectResult.upToDateCount, totalCount: projectResult.totalCount };
|
|
55
|
+
} else {
|
|
56
|
+
console.error('Usage: depbrief | depbrief <pkg> <from> <to>. Run depbrief --help for details.');
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (args.json) {
|
|
62
|
+
printJson(results);
|
|
63
|
+
} else {
|
|
64
|
+
printReport(results, { useColor: process.stdout.isTTY, ...meta });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (args.failOn) {
|
|
68
|
+
const failVerdicts = new Set(args.failOn.split(',').map((s) => s.trim().toUpperCase()));
|
|
69
|
+
if (results.some((r) => failVerdicts.has(r.verdict))) {
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
main().catch((err) => {
|
|
76
|
+
console.error(`depbrief error: ${err.message}`);
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
});
|
package/lib/discover.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function readJson(p) {
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
7
|
+
} catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function depNamesFromPackageJson(pkg) {
|
|
13
|
+
return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Returns Map<name, installedVersion> using whatever source is available,
|
|
17
|
+
// preferring the lockfile (exact resolved versions) over node_modules.
|
|
18
|
+
function resolveInstalledVersions(dir, names) {
|
|
19
|
+
const resolved = new Map();
|
|
20
|
+
|
|
21
|
+
const lock = readJson(path.join(dir, 'package-lock.json'));
|
|
22
|
+
if (lock && lock.packages) {
|
|
23
|
+
// npm lockfileVersion 2/3
|
|
24
|
+
for (const name of names) {
|
|
25
|
+
const entry = lock.packages[`node_modules/${name}`];
|
|
26
|
+
if (entry && entry.version) resolved.set(name, entry.version);
|
|
27
|
+
}
|
|
28
|
+
} else if (lock && lock.dependencies) {
|
|
29
|
+
// npm lockfileVersion 1
|
|
30
|
+
for (const name of names) {
|
|
31
|
+
const entry = lock.dependencies[name];
|
|
32
|
+
if (entry && entry.version) resolved.set(name, entry.version);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const name of names) {
|
|
37
|
+
if (resolved.has(name)) continue;
|
|
38
|
+
const pkgJsonPath = path.join(dir, 'node_modules', name, 'package.json');
|
|
39
|
+
const pkg = readJson(pkgJsonPath);
|
|
40
|
+
if (pkg && pkg.version) resolved.set(name, pkg.version);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return resolved;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function loadProject(dir) {
|
|
47
|
+
const pkg = readJson(path.join(dir, 'package.json'));
|
|
48
|
+
if (!pkg) return null;
|
|
49
|
+
const names = depNamesFromPackageJson(pkg);
|
|
50
|
+
const installed = resolveInstalledVersions(dir, names);
|
|
51
|
+
return { names, installed };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { loadProject };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const { loadProject } = require('./discover');
|
|
2
|
+
const { fetchPackageMetadata, getLatestVersion, isVersionDeprecated } = require('./npmRegistry');
|
|
3
|
+
const { queryVulnerabilities } = require('./osv');
|
|
4
|
+
const { computeVerdict } = require('./verdict');
|
|
5
|
+
|
|
6
|
+
const CONCURRENCY = 8;
|
|
7
|
+
|
|
8
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
9
|
+
const results = new Array(items.length);
|
|
10
|
+
let next = 0;
|
|
11
|
+
async function worker() {
|
|
12
|
+
while (next < items.length) {
|
|
13
|
+
const i = next++;
|
|
14
|
+
results[i] = await fn(items[i], i);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
18
|
+
return results;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function analyzeDependency(name, from, to) {
|
|
22
|
+
const meta = await fetchPackageMetadata(name);
|
|
23
|
+
const targetDeprecated = meta ? isVersionDeprecated(meta, to) : false;
|
|
24
|
+
|
|
25
|
+
const vulnMap = await queryVulnerabilities([
|
|
26
|
+
{ name, version: from },
|
|
27
|
+
{ name, version: to },
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
return computeVerdict({
|
|
31
|
+
name,
|
|
32
|
+
from,
|
|
33
|
+
to,
|
|
34
|
+
currentHasVuln: vulnMap.get(`${name}@${from}`) || false,
|
|
35
|
+
targetHasVuln: vulnMap.get(`${name}@${to}`) || false,
|
|
36
|
+
targetDeprecated,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function analyzePair(name, from, to) {
|
|
41
|
+
return [await analyzeDependency(name, from, to)];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function analyzeProject(dir) {
|
|
45
|
+
const project = loadProject(dir);
|
|
46
|
+
if (!project) return null;
|
|
47
|
+
|
|
48
|
+
const entries = [...project.installed.entries()];
|
|
49
|
+
const withLatest = await mapWithConcurrency(entries, CONCURRENCY, async ([name, current]) => {
|
|
50
|
+
const meta = await fetchPackageMetadata(name);
|
|
51
|
+
const latest = meta ? getLatestVersion(meta) : null;
|
|
52
|
+
return { name, current, latest };
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const toCheck = withLatest.filter((d) => d.latest && d.latest !== d.current);
|
|
56
|
+
|
|
57
|
+
const results = await mapWithConcurrency(toCheck, CONCURRENCY, ({ name, current, latest }) =>
|
|
58
|
+
analyzeDependency(name, current, latest)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return { results, upToDateCount: withLatest.length - toCheck.length, totalCount: withLatest.length };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { analyzeProject, analyzePair };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const REGISTRY_BASE = 'https://registry.npmjs.org';
|
|
2
|
+
|
|
3
|
+
const cache = new Map();
|
|
4
|
+
|
|
5
|
+
async function fetchPackageMetadata(name) {
|
|
6
|
+
if (cache.has(name)) return cache.get(name);
|
|
7
|
+
const url = `${REGISTRY_BASE}/${encodeURIComponent(name).replace('%40', '@')}`;
|
|
8
|
+
const res = await fetch(url);
|
|
9
|
+
if (!res.ok) {
|
|
10
|
+
if (res.status === 404) {
|
|
11
|
+
const empty = null;
|
|
12
|
+
cache.set(name, empty);
|
|
13
|
+
return empty;
|
|
14
|
+
}
|
|
15
|
+
throw new Error(`npm registry returned ${res.status} for ${name}`);
|
|
16
|
+
}
|
|
17
|
+
const json = await res.json();
|
|
18
|
+
cache.set(name, json);
|
|
19
|
+
return json;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getLatestVersion(meta) {
|
|
23
|
+
return meta && meta['dist-tags'] ? meta['dist-tags'].latest : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isVersionDeprecated(meta, version) {
|
|
27
|
+
if (!meta || !meta.versions || !meta.versions[version]) return false;
|
|
28
|
+
return Boolean(meta.versions[version].deprecated);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { fetchPackageMetadata, getLatestVersion, isVersionDeprecated };
|
package/lib/osv.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const OSV_BATCH_URL = 'https://api.osv.dev/v1/querybatch';
|
|
2
|
+
|
|
3
|
+
// pairs: [{ name, version }]. Returns Map key `${name}@${version}` -> boolean (has known vuln).
|
|
4
|
+
async function queryVulnerabilities(pairs) {
|
|
5
|
+
const result = new Map();
|
|
6
|
+
if (pairs.length === 0) return result;
|
|
7
|
+
|
|
8
|
+
const queries = pairs.map((p) => ({
|
|
9
|
+
package: { name: p.name, ecosystem: 'npm' },
|
|
10
|
+
version: p.version,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const res = await fetch(OSV_BATCH_URL, {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16
|
+
body: JSON.stringify({ queries }),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
throw new Error(`OSV.dev returned ${res.status}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const json = await res.json();
|
|
24
|
+
const results = json.results || [];
|
|
25
|
+
pairs.forEach((p, i) => {
|
|
26
|
+
const entry = results[i];
|
|
27
|
+
const hasVuln = Boolean(entry && Array.isArray(entry.vulns) && entry.vulns.length > 0);
|
|
28
|
+
result.set(`${p.name}@${p.version}`, hasVuln);
|
|
29
|
+
});
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { queryVulnerabilities };
|
package/lib/report.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const COLORS = {
|
|
2
|
+
reset: '\x1b[0m',
|
|
3
|
+
bold: '\x1b[1m',
|
|
4
|
+
red: '\x1b[31m',
|
|
5
|
+
yellow: '\x1b[33m',
|
|
6
|
+
green: '\x1b[32m',
|
|
7
|
+
gray: '\x1b[90m',
|
|
8
|
+
magenta: '\x1b[35m',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const VERDICT_COLOR = {
|
|
12
|
+
AVOID: COLORS.magenta,
|
|
13
|
+
URGENT: COLORS.red,
|
|
14
|
+
REVIEW: COLORS.yellow,
|
|
15
|
+
CAUTION: COLORS.yellow,
|
|
16
|
+
SAFE: COLORS.green,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function printReport(results, { useColor = true, upToDateCount, totalCount } = {}) {
|
|
20
|
+
const c = (code, s) => (useColor ? `${code}${s}${COLORS.reset}` : s);
|
|
21
|
+
|
|
22
|
+
if (results.length === 0) {
|
|
23
|
+
console.log('All dependencies are up to date.');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const order = ['AVOID', 'URGENT', 'REVIEW', 'CAUTION', 'SAFE'];
|
|
28
|
+
const sorted = [...results].sort((a, b) => order.indexOf(a.verdict) - order.indexOf(b.verdict));
|
|
29
|
+
|
|
30
|
+
console.log('');
|
|
31
|
+
for (const r of sorted) {
|
|
32
|
+
const tag = c(VERDICT_COLOR[r.verdict], `[${r.verdict}]`.padEnd(10));
|
|
33
|
+
console.log(`${tag} ${c(COLORS.bold, r.name)} ${r.from} -> ${r.to} (${r.bump})`);
|
|
34
|
+
for (const reason of r.reasons) {
|
|
35
|
+
console.log(` ${c(COLORS.gray, '-')} ${reason}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
console.log('');
|
|
39
|
+
if (typeof totalCount === 'number') {
|
|
40
|
+
console.log(`${results.length} of ${totalCount} dependencies have updates available (${upToDateCount} already up to date).`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function printJson(results) {
|
|
45
|
+
console.log(JSON.stringify(results, null, 2));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { printReport, printJson };
|
package/lib/semver.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Minimal semver handling — deliberately not the full spec (no build metadata,
|
|
2
|
+
// no range satisfaction). Good enough for "what kind of bump is this" between
|
|
3
|
+
// two concrete version strings, which is all depbrief needs.
|
|
4
|
+
|
|
5
|
+
const CORE_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/;
|
|
6
|
+
|
|
7
|
+
function parse(version) {
|
|
8
|
+
const cleaned = String(version).replace(/^[\^~>=<v]+/, '').trim();
|
|
9
|
+
const m = CORE_RE.exec(cleaned);
|
|
10
|
+
if (!m) return null;
|
|
11
|
+
return {
|
|
12
|
+
major: Number(m[1]),
|
|
13
|
+
minor: Number(m[2]),
|
|
14
|
+
patch: Number(m[3]),
|
|
15
|
+
prerelease: m[4] || null,
|
|
16
|
+
raw: cleaned,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Returns 'major' | 'minor' | 'patch' | 'none' | 'downgrade' | 'unknown'
|
|
21
|
+
function classifyBump(fromVersion, toVersion) {
|
|
22
|
+
const from = parse(fromVersion);
|
|
23
|
+
const to = parse(toVersion);
|
|
24
|
+
if (!from || !to) return 'unknown';
|
|
25
|
+
|
|
26
|
+
if (to.major > from.major) return 'major';
|
|
27
|
+
if (to.major < from.major) return 'downgrade';
|
|
28
|
+
if (to.minor > from.minor) return from.major === 0 ? 'major' : 'minor';
|
|
29
|
+
if (to.minor < from.minor) return 'downgrade';
|
|
30
|
+
if (to.patch > from.patch) return 'patch';
|
|
31
|
+
if (to.patch < from.patch) return 'downgrade';
|
|
32
|
+
if (from.prerelease && !to.prerelease) return 'patch';
|
|
33
|
+
return 'none';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { parse, classifyBump };
|
package/lib/verdict.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const { classifyBump } = require('./semver');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure decision logic — no network I/O — so it's unit-testable with
|
|
5
|
+
* plain fixture objects.
|
|
6
|
+
*
|
|
7
|
+
* input: {
|
|
8
|
+
* name, from, to,
|
|
9
|
+
* currentHasVuln: boolean, // known OSV vuln affects the *current* version
|
|
10
|
+
* targetHasVuln: boolean, // known OSV vuln affects the *target* version
|
|
11
|
+
* targetDeprecated: boolean, // npm registry marks target version deprecated
|
|
12
|
+
* }
|
|
13
|
+
*/
|
|
14
|
+
function computeVerdict(input) {
|
|
15
|
+
const bump = classifyBump(input.from, input.to);
|
|
16
|
+
const reasons = [];
|
|
17
|
+
|
|
18
|
+
if (input.targetHasVuln) {
|
|
19
|
+
reasons.push('the version you would upgrade to has a known unpatched vulnerability');
|
|
20
|
+
return { name: input.name, from: input.from, to: input.to, bump, verdict: 'AVOID', reasons };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (input.currentHasVuln) {
|
|
24
|
+
reasons.push('current version has a known vulnerability — upgrading resolves it');
|
|
25
|
+
if (bump === 'major') reasons.push('this is also a major version bump — review the changelog');
|
|
26
|
+
return { name: input.name, from: input.from, to: input.to, bump, verdict: 'URGENT', reasons };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (bump === 'major') {
|
|
30
|
+
reasons.push('major version bump — breaking changes are likely, check the changelog');
|
|
31
|
+
return { name: input.name, from: input.from, to: input.to, bump, verdict: 'REVIEW', reasons };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (input.targetDeprecated) {
|
|
35
|
+
reasons.push('target version is marked deprecated on the npm registry');
|
|
36
|
+
return { name: input.name, from: input.from, to: input.to, bump, verdict: 'CAUTION', reasons };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (bump === 'downgrade' || bump === 'unknown') {
|
|
40
|
+
reasons.push(bump === 'downgrade' ? 'this would downgrade the package' : "couldn't parse one of the versions");
|
|
41
|
+
return { name: input.name, from: input.from, to: input.to, bump, verdict: 'CAUTION', reasons };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
reasons.push(`${bump} bump, no known vulnerabilities, not deprecated`);
|
|
45
|
+
return { name: input.name, from: input.from, to: input.to, bump, verdict: 'SAFE', reasons };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const VERDICT_ORDER = ['AVOID', 'URGENT', 'REVIEW', 'CAUTION', 'SAFE'];
|
|
49
|
+
|
|
50
|
+
module.exports = { computeVerdict, VERDICT_ORDER };
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "depbrief",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "One verdict per dependency upgrade: semver bump class, known vulnerabilities, deprecation status. No accounts, no GitHub App, no dependencies.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"npm",
|
|
7
|
+
"dependencies",
|
|
8
|
+
"upgrade",
|
|
9
|
+
"semver",
|
|
10
|
+
"vulnerability",
|
|
11
|
+
"security",
|
|
12
|
+
"cli",
|
|
13
|
+
"developer-tools"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"bin": {
|
|
17
|
+
"depbrief": "bin/depbrief.js"
|
|
18
|
+
},
|
|
19
|
+
"main": "lib/index.js",
|
|
20
|
+
"files": [
|
|
21
|
+
"bin",
|
|
22
|
+
"lib"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "node test/run.js"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/greatfighter891-gif/depbrief.git"
|
|
33
|
+
}
|
|
34
|
+
}
|