dependency-radar 0.8.1 → 0.9.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/README.md +176 -15
- package/dist/aggregator.js +458 -15
- package/dist/cli.js +60 -6
- package/dist/explain.js +83 -1
- package/dist/failOn.js +370 -1
- package/dist/findings.js +81 -3
- package/dist/report-assets.js +3 -4
- package/dist/reportDetailRules.js +162 -0
- package/dist/runners/npmRegistryMetadata.js +390 -0
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -13,9 +13,11 @@ const importGraphRunner_1 = require("./runners/importGraphRunner");
|
|
|
13
13
|
const npmAudit_1 = require("./runners/npmAudit");
|
|
14
14
|
const npmLs_1 = require("./runners/npmLs");
|
|
15
15
|
const npmOutdated_1 = require("./runners/npmOutdated");
|
|
16
|
+
const npmRegistryMetadata_1 = require("./runners/npmRegistryMetadata");
|
|
16
17
|
const lockfileSignals_1 = require("./runners/lockfileSignals");
|
|
17
18
|
const report_1 = require("./report");
|
|
18
19
|
const compare_1 = require("./compare");
|
|
20
|
+
const findings_1 = require("./findings");
|
|
19
21
|
const outputFormats_1 = require("./outputFormats");
|
|
20
22
|
const why_1 = require("./why");
|
|
21
23
|
const schema_1 = require("./schema");
|
|
@@ -935,7 +937,7 @@ function buildCombinedDependencyGraph(rootPath, packageMetas, dependencyGraphs)
|
|
|
935
937
|
* --project, --quiet, --out, --keep-temp, --offline, --json, --format, --sbom, --target-node,
|
|
936
938
|
* --audit-signatures, --schema, --timestamp, --open, --no-report, --fail-on, --help / -h.
|
|
937
939
|
*
|
|
938
|
-
* The --offline flag disables
|
|
940
|
+
* The --offline flag disables registry-backed checks. Unknown options or unexpected positional
|
|
939
941
|
* arguments cause the process to exit with an error.
|
|
940
942
|
*
|
|
941
943
|
* @param argv - Array of CLI tokens (typically process.argv.slice(2))
|
|
@@ -1175,12 +1177,20 @@ Options:
|
|
|
1175
1177
|
--timestamp Add a local timestamp to generated report filenames
|
|
1176
1178
|
--no-report Do not write HTML/JSON report files or temp artifacts to disk
|
|
1177
1179
|
--keep-temp Keep .dependency-radar folder
|
|
1178
|
-
--offline Skip
|
|
1180
|
+
--offline Skip registry-backed checks (audit, outdated, signatures, targeted registry enrichment)
|
|
1179
1181
|
--open Open the generated report using the system default application
|
|
1180
1182
|
--fail-on <rules> Fail with exit code 1 when selected rules are violated
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1183
|
+
Scan rules: reachable-vuln, production-vuln, high-severity-vuln,
|
|
1184
|
+
licence-mismatch, copyleft-detected, unknown-licence,
|
|
1185
|
+
supply-chain-source
|
|
1186
|
+
Compare rules: new-supply-chain-signal, new-install-script,
|
|
1187
|
+
new-native-binding, new-bin, new-direct-dependency,
|
|
1188
|
+
new-child-process, new-network-access, new-env-access,
|
|
1189
|
+
new-home-access, new-ssh-usage, new-obfuscation-signal,
|
|
1190
|
+
new-bundled-dependencies, new-shrinkwrap,
|
|
1191
|
+
new-recent-package, new-recent-version,
|
|
1192
|
+
new-low-release-history, new-reactivated-package,
|
|
1193
|
+
new-old-major-patch
|
|
1184
1194
|
|
|
1185
1195
|
\`explain\` reuses the same local scan model and prints a terminal view for one package.
|
|
1186
1196
|
\`why\` prints shortest dependency paths for one package.
|
|
@@ -1359,6 +1369,9 @@ function printPolicyViolations(violations) {
|
|
|
1359
1369
|
console.log(colorLeadingSymbol("✖ Policy violations detected:"));
|
|
1360
1370
|
for (const violation of violations) {
|
|
1361
1371
|
console.log(`- ${violation.message}`);
|
|
1372
|
+
for (const detail of violation.details || []) {
|
|
1373
|
+
console.log(` - ${detail}`);
|
|
1374
|
+
}
|
|
1362
1375
|
}
|
|
1363
1376
|
}
|
|
1364
1377
|
/**
|
|
@@ -1750,6 +1763,28 @@ async function executeAnalysis(opts, options) {
|
|
|
1750
1763
|
...(toolVersions ? { toolVersions } : {}),
|
|
1751
1764
|
...(typeof opts.targetNodeMajor === "number" ? { targetNodeMajor: opts.targetNodeMajor } : {}),
|
|
1752
1765
|
});
|
|
1766
|
+
if (opts.outdated) {
|
|
1767
|
+
let registryEnrichment = { attempted: 0, succeeded: 0 };
|
|
1768
|
+
try {
|
|
1769
|
+
registryEnrichment = await (0, npmRegistryMetadata_1.enrichAggregatedWithRegistryMetadata)(aggregated, {
|
|
1770
|
+
offline: false,
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
catch (err) {
|
|
1774
|
+
if (!opts.quiet) {
|
|
1775
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1776
|
+
spinner.log(statusLine("⚠", `Targeted registry metadata unavailable (${message})`));
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
if (!opts.quiet && registryEnrichment.succeeded > 0) {
|
|
1780
|
+
spinner.log(statusLine("✔", `Targeted registry metadata collected for ${registryEnrichment.succeeded} suspicious package${registryEnrichment.succeeded === 1 ? "" : "s"}`));
|
|
1781
|
+
}
|
|
1782
|
+
if (registryEnrichment.attempted > 0) {
|
|
1783
|
+
const findings = (0, findings_1.buildDependencyFindings)(aggregated, { targetNodeMajor: opts.targetNodeMajor });
|
|
1784
|
+
aggregated.findings = findings;
|
|
1785
|
+
aggregated.summary.findingCount = findings.length;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1753
1788
|
dependencyCount = Object.keys(aggregated.dependencies).length;
|
|
1754
1789
|
const importGraphComplete = perPackageImportGraph.every((result) => result.ok);
|
|
1755
1790
|
const summary = buildCliSummary(aggregated, {
|
|
@@ -1903,6 +1938,16 @@ async function runWhyCommand(opts) {
|
|
|
1903
1938
|
process.exit(1);
|
|
1904
1939
|
}
|
|
1905
1940
|
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Compare the current analysis against a previous report and print the comparison and policy violations.
|
|
1943
|
+
*
|
|
1944
|
+
* Validates the `opts.comparePath` report file against the expected schema, runs a new analysis without
|
|
1945
|
+
* writing artifacts, computes a diff between the previous and current aggregated results, prints the
|
|
1946
|
+
* formatted comparison, prints any policy violations (including compare-specific violations), and exits
|
|
1947
|
+
* with code 1 when validation fails or any policy violations are present.
|
|
1948
|
+
*
|
|
1949
|
+
* @param opts - CLI options controlling the comparison run (must include `comparePath` and may include `failOn`)
|
|
1950
|
+
*/
|
|
1906
1951
|
async function runCompareCommand(opts) {
|
|
1907
1952
|
var _a;
|
|
1908
1953
|
const previousPath = (_a = opts.comparePath) === null || _a === void 0 ? void 0 : _a.trim();
|
|
@@ -1938,8 +1983,17 @@ async function runCompareCommand(opts) {
|
|
|
1938
1983
|
emitArtifactSummary: false,
|
|
1939
1984
|
emitWorkspacePackageSummary: false,
|
|
1940
1985
|
});
|
|
1986
|
+
const comparison = (0, compare_1.compareReports)(previous, result.aggregated);
|
|
1987
|
+
const policyViolations = [
|
|
1988
|
+
...result.policyViolations,
|
|
1989
|
+
...(0, failOn_1.evaluateComparePolicyViolations)(previous, result.aggregated, opts.failOn),
|
|
1990
|
+
];
|
|
1941
1991
|
console.log("");
|
|
1942
|
-
console.log((0, compare_1.formatCompareOutput)(
|
|
1992
|
+
console.log((0, compare_1.formatCompareOutput)(comparison));
|
|
1993
|
+
printPolicyViolations(policyViolations);
|
|
1994
|
+
if (policyViolations.length > 0) {
|
|
1995
|
+
process.exit(1);
|
|
1996
|
+
}
|
|
1943
1997
|
}
|
|
1944
1998
|
/**
|
|
1945
1999
|
* Run the CLI entrypoint and dispatch to the selected command.
|
package/dist/explain.js
CHANGED
|
@@ -9,6 +9,34 @@ const BLOCKER_LABELS = {
|
|
|
9
9
|
installScripts: 'Install lifecycle scripts',
|
|
10
10
|
deprecated: 'Deprecated by author',
|
|
11
11
|
};
|
|
12
|
+
const EXECUTION_SIGNAL_LABELS = {
|
|
13
|
+
'network-access': 'network access',
|
|
14
|
+
'dynamic-exec': 'dynamic execution',
|
|
15
|
+
'child-process': 'child process APIs',
|
|
16
|
+
encoding: 'encoding/decoding logic',
|
|
17
|
+
obfuscated: 'obfuscation-like code shape',
|
|
18
|
+
'reads-env': 'environment access',
|
|
19
|
+
'reads-home': 'home directory access',
|
|
20
|
+
'uses-ssh': 'SSH-related references'
|
|
21
|
+
};
|
|
22
|
+
const PACKAGING_SIGNAL_LABELS = {
|
|
23
|
+
'bundled-dependencies': 'bundled dependencies',
|
|
24
|
+
'embedded-shrinkwrap': 'embedded npm-shrinkwrap.json'
|
|
25
|
+
};
|
|
26
|
+
const REGISTRY_SIGNAL_LABELS = {
|
|
27
|
+
'recent-package': 'recently created package',
|
|
28
|
+
'recent-version': 'recently published installed version',
|
|
29
|
+
'low-release-history': 'low release history',
|
|
30
|
+
'reactivated-package': 'reactivated after dormancy',
|
|
31
|
+
'old-major-new-patch': 'recent patch on older major line'
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Locate dependency records for a given package name and return them in prioritized order.
|
|
35
|
+
*
|
|
36
|
+
* @param aggregated - The aggregated dataset containing dependency records
|
|
37
|
+
* @param packageName - The package name to search for
|
|
38
|
+
* @returns An array of matching `DependencyRecord` objects sorted with direct dependencies first, then by descending package version; empty if none found
|
|
39
|
+
*/
|
|
12
40
|
function findDependenciesByPackageName(aggregated, packageName) {
|
|
13
41
|
return Object.values(aggregated.dependencies || {})
|
|
14
42
|
.filter((dep) => dep.package.name === packageName)
|
|
@@ -19,8 +47,17 @@ function findDependenciesByPackageName(aggregated, packageName) {
|
|
|
19
47
|
return compareVersionStrings(b.package.version, a.package.version);
|
|
20
48
|
});
|
|
21
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Generate a human-readable, line-oriented explanation report for a package
|
|
52
|
+
* across the provided dependency records.
|
|
53
|
+
*
|
|
54
|
+
* @param packageName - The package name shown in the report header
|
|
55
|
+
* @param matches - DependencyRecord entries for the package to render as individual sections
|
|
56
|
+
* @param context - Rendering context controlling audit availability and import-graph completeness
|
|
57
|
+
* @returns The formatted multi-section report as a string. If `matches` is empty the string is `✖ Package not found: ${packageName}`.
|
|
58
|
+
*/
|
|
22
59
|
function formatExplainOutput(packageName, matches, context) {
|
|
23
|
-
var _a, _b, _c, _d;
|
|
60
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
24
61
|
if (matches.length === 0) {
|
|
25
62
|
return `✖ Package not found: ${packageName}`;
|
|
26
63
|
}
|
|
@@ -89,6 +126,51 @@ function formatExplainOutput(packageName, matches, context) {
|
|
|
89
126
|
else {
|
|
90
127
|
lines.push(' none');
|
|
91
128
|
}
|
|
129
|
+
lines.push('');
|
|
130
|
+
lines.push('Local execution signals:');
|
|
131
|
+
const scriptSignals = new Set(((_f = (_e = dep.execution) === null || _e === void 0 ? void 0 : _e.scripts) === null || _f === void 0 ? void 0 : _f.signals) || []);
|
|
132
|
+
const localSignals = (((_g = dep.execution) === null || _g === void 0 ? void 0 : _g.signals) || []).filter((signal) => !scriptSignals.has(signal));
|
|
133
|
+
if (localSignals.length) {
|
|
134
|
+
for (const signal of localSignals) {
|
|
135
|
+
lines.push(` - ${signal} (${EXECUTION_SIGNAL_LABELS[signal] || 'review signal'})`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
lines.push(' none');
|
|
140
|
+
}
|
|
141
|
+
lines.push('');
|
|
142
|
+
lines.push('Packaging signals:');
|
|
143
|
+
if ((_j = (_h = dep.packaging) === null || _h === void 0 ? void 0 : _h.signals) === null || _j === void 0 ? void 0 : _j.length) {
|
|
144
|
+
for (const signal of dep.packaging.signals) {
|
|
145
|
+
lines.push(` - ${signal} (${PACKAGING_SIGNAL_LABELS[signal] || 'review signal'})`);
|
|
146
|
+
}
|
|
147
|
+
if ((_k = dep.packaging.bundledDependencies) === null || _k === void 0 ? void 0 : _k.length) {
|
|
148
|
+
lines.push(` bundled dependencies: ${dep.packaging.bundledDependencies.join(', ')}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
lines.push(' none');
|
|
153
|
+
}
|
|
154
|
+
lines.push('');
|
|
155
|
+
lines.push('Registry metadata signals:');
|
|
156
|
+
const registry = (_l = dep.supplyChain) === null || _l === void 0 ? void 0 : _l.registry;
|
|
157
|
+
if ((_m = registry === null || registry === void 0 ? void 0 : registry.signals) === null || _m === void 0 ? void 0 : _m.length) {
|
|
158
|
+
for (const signal of registry.signals) {
|
|
159
|
+
lines.push(` - ${signal} (${REGISTRY_SIGNAL_LABELS[signal] || 'review signal'})`);
|
|
160
|
+
}
|
|
161
|
+
if (registry.installedVersionPublishedAt) {
|
|
162
|
+
lines.push(` installed version published: ${registry.installedVersionPublishedAt}`);
|
|
163
|
+
}
|
|
164
|
+
if (typeof registry.versionCount === 'number') {
|
|
165
|
+
lines.push(` version count: ${registry.versionCount}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if ((registry === null || registry === void 0 ? void 0 : registry.attempted) && !registry.ok) {
|
|
169
|
+
lines.push(` unavailable (${registry.error || 'lookup failed'})`);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
lines.push(' none');
|
|
173
|
+
}
|
|
92
174
|
if (otherVersions.length > 0) {
|
|
93
175
|
lines.push('');
|
|
94
176
|
lines.push('Other detected versions:');
|
package/dist/failOn.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.SUPPORTED_FAIL_ON_RULES = void 0;
|
|
4
4
|
exports.parseFailOnRules = parseFailOnRules;
|
|
5
5
|
exports.evaluatePolicyViolations = evaluatePolicyViolations;
|
|
6
|
+
exports.evaluateComparePolicyViolations = evaluateComparePolicyViolations;
|
|
6
7
|
const license_1 = require("./license");
|
|
7
8
|
exports.SUPPORTED_FAIL_ON_RULES = [
|
|
8
9
|
'reachable-vuln',
|
|
@@ -11,7 +12,25 @@ exports.SUPPORTED_FAIL_ON_RULES = [
|
|
|
11
12
|
'licence-mismatch',
|
|
12
13
|
'copyleft-detected',
|
|
13
14
|
'unknown-licence',
|
|
14
|
-
'supply-chain-source'
|
|
15
|
+
'supply-chain-source',
|
|
16
|
+
'new-supply-chain-signal',
|
|
17
|
+
'new-install-script',
|
|
18
|
+
'new-native-binding',
|
|
19
|
+
'new-bin',
|
|
20
|
+
'new-direct-dependency',
|
|
21
|
+
'new-child-process',
|
|
22
|
+
'new-network-access',
|
|
23
|
+
'new-env-access',
|
|
24
|
+
'new-home-access',
|
|
25
|
+
'new-ssh-usage',
|
|
26
|
+
'new-obfuscation-signal',
|
|
27
|
+
'new-bundled-dependencies',
|
|
28
|
+
'new-shrinkwrap',
|
|
29
|
+
'new-recent-package',
|
|
30
|
+
'new-recent-version',
|
|
31
|
+
'new-low-release-history',
|
|
32
|
+
'new-reactivated-package',
|
|
33
|
+
'new-old-major-patch'
|
|
15
34
|
];
|
|
16
35
|
const SUPPORTED_FAIL_ON_RULE_SET = new Set(exports.SUPPORTED_FAIL_ON_RULES);
|
|
17
36
|
/**
|
|
@@ -37,6 +56,230 @@ function vulnerabilityCount(dep) {
|
|
|
37
56
|
(dep.security.summary.moderate || 0) +
|
|
38
57
|
(dep.security.summary.low || 0));
|
|
39
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Group dependency records by their package name.
|
|
61
|
+
*
|
|
62
|
+
* @param deps - Optional record of dependency entries keyed by dependency id; entries missing a package name are ignored
|
|
63
|
+
* @returns A Map whose keys are package names and whose values are arrays of `DependencyRecord` objects for that package
|
|
64
|
+
*/
|
|
65
|
+
function byPackageName(deps) {
|
|
66
|
+
var _a;
|
|
67
|
+
const map = new Map();
|
|
68
|
+
for (const dep of Object.values(deps || {})) {
|
|
69
|
+
const name = (_a = dep.package) === null || _a === void 0 ? void 0 : _a.name;
|
|
70
|
+
if (!name)
|
|
71
|
+
continue;
|
|
72
|
+
const entries = map.get(name) || [];
|
|
73
|
+
entries.push(dep);
|
|
74
|
+
map.set(name, entries);
|
|
75
|
+
}
|
|
76
|
+
return map;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Produce a package label for a dependency using its package id when available, otherwise `name@version`.
|
|
80
|
+
*
|
|
81
|
+
* @param dep - The dependency record whose package will be formatted
|
|
82
|
+
* @returns The package `id` if present; otherwise the package `name` and `version` joined with `@`
|
|
83
|
+
*/
|
|
84
|
+
function formatPackage(dep) {
|
|
85
|
+
return dep.package.id || `${dep.package.name}@${dep.package.version}`;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Retrieve the package's execution script hook names sorted lexicographically.
|
|
89
|
+
*
|
|
90
|
+
* @param dep - Dependency record to read hooks from
|
|
91
|
+
* @returns The hook names from `dep.execution?.scripts?.hooks` sorted lexicographically; an empty array if no hooks are present
|
|
92
|
+
*/
|
|
93
|
+
function sortedHooks(dep) {
|
|
94
|
+
var _a, _b;
|
|
95
|
+
return [...(((_b = (_a = dep.execution) === null || _a === void 0 ? void 0 : _a.scripts) === null || _b === void 0 ? void 0 : _b.hooks) || [])].sort();
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Collect execution-related signals from a dependency record.
|
|
99
|
+
*
|
|
100
|
+
* @param dep - The dependency record to inspect for execution signals
|
|
101
|
+
* @returns A Set of unique `ExecutionSignal` values found on the dependency
|
|
102
|
+
*/
|
|
103
|
+
function executionSignals(dep) {
|
|
104
|
+
var _a, _b, _c;
|
|
105
|
+
return new Set([
|
|
106
|
+
...(((_a = dep.execution) === null || _a === void 0 ? void 0 : _a.signals) || []),
|
|
107
|
+
...(((_c = (_b = dep.execution) === null || _b === void 0 ? void 0 : _b.scripts) === null || _c === void 0 ? void 0 : _c.signals) || [])
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Collects packaging signals present on a dependency.
|
|
112
|
+
*
|
|
113
|
+
* @param dep - The dependency record to inspect
|
|
114
|
+
* @returns A Set of `PackagingSignal` values found on `dep`; empty if the dependency has no packaging signals
|
|
115
|
+
*/
|
|
116
|
+
function packagingSignals(dep) {
|
|
117
|
+
var _a;
|
|
118
|
+
return new Set(((_a = dep.packaging) === null || _a === void 0 ? void 0 : _a.signals) || []);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Get the registry risk signals associated with a dependency.
|
|
122
|
+
*
|
|
123
|
+
* @param dep - The dependency record to inspect
|
|
124
|
+
* @returns A Set of `RegistryRiskSignal` values found on the dependency's `supplyChain.registry.signals`; an empty `Set` if none are present
|
|
125
|
+
*/
|
|
126
|
+
function registrySignals(dep) {
|
|
127
|
+
var _a, _b;
|
|
128
|
+
return new Set(((_b = (_a = dep.supplyChain) === null || _a === void 0 ? void 0 : _a.registry) === null || _b === void 0 ? void 0 : _b.signals) || []);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Extracts the package name associated with a supply-chain signal.
|
|
132
|
+
*
|
|
133
|
+
* Prefers `signal.packageName` when present; otherwise derives the name from
|
|
134
|
+
* `signal.packageId` by taking the substring before the last `@`. Returns
|
|
135
|
+
* `undefined` if neither value yields a usable package name.
|
|
136
|
+
*
|
|
137
|
+
* @param signal - The supply-chain signal to inspect
|
|
138
|
+
* @returns The package name if present or derivable, `undefined` otherwise
|
|
139
|
+
*/
|
|
140
|
+
function signalPackageName(signal) {
|
|
141
|
+
if (signal.packageName)
|
|
142
|
+
return signal.packageName;
|
|
143
|
+
if (!signal.packageId)
|
|
144
|
+
return undefined;
|
|
145
|
+
const at = signal.packageId.lastIndexOf('@');
|
|
146
|
+
if (at <= 0)
|
|
147
|
+
return undefined;
|
|
148
|
+
return signal.packageId.slice(0, at);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Produce a human-readable package label for a supply-chain signal.
|
|
152
|
+
*
|
|
153
|
+
* @param signal - The supply-chain signal object used to derive the label.
|
|
154
|
+
* @returns The label chosen in this priority: `signal.packageId`, `packageName@packageVersion`, `packageName`, or `'lockfile'` when no package information is available.
|
|
155
|
+
*/
|
|
156
|
+
function signalPackageLabel(signal) {
|
|
157
|
+
if (signal.packageId)
|
|
158
|
+
return signal.packageId;
|
|
159
|
+
if (signal.packageName && signal.packageVersion)
|
|
160
|
+
return `${signal.packageName}@${signal.packageVersion}`;
|
|
161
|
+
if (signal.packageName)
|
|
162
|
+
return signal.packageName;
|
|
163
|
+
return 'lockfile';
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Produce sets of observed supply-chain signal types, both per-package and globally.
|
|
167
|
+
*
|
|
168
|
+
* @param signals - Array of supply-chain signals to analyze; may be `undefined` or empty.
|
|
169
|
+
* @returns An object with:
|
|
170
|
+
* - `byPackage`: map from package name to a `Set` of signal `type` strings observed for that package.
|
|
171
|
+
* - `global`: `Set` of all signal `type` strings observed (including those not tied to a package).
|
|
172
|
+
*/
|
|
173
|
+
function supplyChainSignalKeys(signals) {
|
|
174
|
+
const byPackage = new Map();
|
|
175
|
+
const global = new Set();
|
|
176
|
+
for (const signal of signals || []) {
|
|
177
|
+
global.add(signal.type);
|
|
178
|
+
const name = signalPackageName(signal);
|
|
179
|
+
if (!name)
|
|
180
|
+
continue;
|
|
181
|
+
const types = byPackage.get(name) || new Set();
|
|
182
|
+
types.add(signal.type);
|
|
183
|
+
byPackage.set(name, types);
|
|
184
|
+
}
|
|
185
|
+
return { byPackage, global };
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Appends a PolicyViolation for dependencies that newly exhibit the specified execution signal.
|
|
189
|
+
*
|
|
190
|
+
* If the provided `rule` is present in `rules` and one or more entries in `currentDeps`
|
|
191
|
+
* have the `signal` while no previous dependency with the same package name had it,
|
|
192
|
+
* a `PolicyViolation` describing the new execution signal(s) is pushed onto `violations`.
|
|
193
|
+
*
|
|
194
|
+
* @param violations - Mutable array to receive the generated PolicyViolation when matches are found
|
|
195
|
+
* @param previousByName - Map of package name to previous DependencyRecord[] used as the baseline for comparison
|
|
196
|
+
* @param currentDeps - Current dependency records to evaluate for newly introduced signals
|
|
197
|
+
* @param rules - Set of enabled fail-on rules; the function is a no-op if `rule` is not in this set
|
|
198
|
+
* @param rule - The specific FailOnRule to produce a violation for when triggered
|
|
199
|
+
* @param signal - The ExecutionSignal to detect as newly introduced
|
|
200
|
+
*/
|
|
201
|
+
function pushNewExecutionSignalViolation(violations, previousByName, currentDeps, rules, rule, signal) {
|
|
202
|
+
if (!rules.has(rule))
|
|
203
|
+
return;
|
|
204
|
+
const details = currentDeps
|
|
205
|
+
.filter((dep) => {
|
|
206
|
+
if (!executionSignals(dep).has(signal))
|
|
207
|
+
return false;
|
|
208
|
+
return !(previousByName.get(dep.package.name) || []).some((previousDep) => executionSignals(previousDep).has(signal));
|
|
209
|
+
})
|
|
210
|
+
.map((dep) => `${formatPackage(dep)} introduced execution signal: ${signal}`)
|
|
211
|
+
.sort();
|
|
212
|
+
if (details.length === 0)
|
|
213
|
+
return;
|
|
214
|
+
violations.push({
|
|
215
|
+
rule,
|
|
216
|
+
count: details.length,
|
|
217
|
+
message: `${details.length} new execution ${pluralize(details.length, 'signal', 'signals')}: ${signal}`,
|
|
218
|
+
details
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Add a policy violation to `violations` when `signal` appears in current dependencies but was not present for the same package name in `previousByName`, and the given `rule` is enabled.
|
|
223
|
+
*
|
|
224
|
+
* @param violations - Accumulates discovered PolicyViolation objects; this function may push a new violation into it.
|
|
225
|
+
* @param previousByName - Map from package name to previous scan DependencyRecord[] used to determine whether `signal` was already present for a package.
|
|
226
|
+
* @param currentDeps - Current scan dependencies to inspect for newly introduced packaging signals.
|
|
227
|
+
* @param rules - Selected fail-on rules; the function no-ops if `rule` is not in this set.
|
|
228
|
+
* @param rule - The specific packaging-related compare-mode rule to enforce (e.g., `new-shrinkwrap` or `new-bundled-dependencies`).
|
|
229
|
+
* @param signal - The PackagingSignal type to detect as newly introduced.
|
|
230
|
+
*/
|
|
231
|
+
function pushNewPackagingSignalViolation(violations, previousByName, currentDeps, rules, rule, signal) {
|
|
232
|
+
if (!rules.has(rule))
|
|
233
|
+
return;
|
|
234
|
+
const details = currentDeps
|
|
235
|
+
.filter((dep) => {
|
|
236
|
+
if (!packagingSignals(dep).has(signal))
|
|
237
|
+
return false;
|
|
238
|
+
return !(previousByName.get(dep.package.name) || []).some((previousDep) => packagingSignals(previousDep).has(signal));
|
|
239
|
+
})
|
|
240
|
+
.map((dep) => `${formatPackage(dep)} introduced packaging signal: ${signal}`)
|
|
241
|
+
.sort();
|
|
242
|
+
if (details.length === 0)
|
|
243
|
+
return;
|
|
244
|
+
violations.push({
|
|
245
|
+
rule,
|
|
246
|
+
count: details.length,
|
|
247
|
+
message: `${details.length} new packaging ${pluralize(details.length, 'signal', 'signals')}: ${signal}`,
|
|
248
|
+
details
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Append a PolicyViolation for registry risk signals that appear in the current scan but were not present previously.
|
|
253
|
+
*
|
|
254
|
+
* If `rule` is not enabled in `rules`, this function does nothing. When enabled, it identifies current dependencies whose registry signals include `signal` and for which no previous dependency with the same package name had that signal, then pushes a violation with the total `count`, a summary `message`, and `details` listing affected packages.
|
|
255
|
+
*
|
|
256
|
+
* @param violations - Array to which the resulting PolicyViolation will be pushed
|
|
257
|
+
* @param previousByName - Map of package name to list of previous DependencyRecord entries
|
|
258
|
+
* @param currentDeps - List of current DependencyRecord entries to inspect
|
|
259
|
+
* @param rules - Set of enabled fail-on rules
|
|
260
|
+
* @param rule - The specific fail-on rule to check (must be present in `rules` to produce a violation)
|
|
261
|
+
* @param signal - The registry risk signal to detect as newly introduced
|
|
262
|
+
*/
|
|
263
|
+
function pushNewRegistrySignalViolation(violations, previousByName, currentDeps, rules, rule, signal) {
|
|
264
|
+
if (!rules.has(rule))
|
|
265
|
+
return;
|
|
266
|
+
const details = currentDeps
|
|
267
|
+
.filter((dep) => {
|
|
268
|
+
if (!registrySignals(dep).has(signal))
|
|
269
|
+
return false;
|
|
270
|
+
return !(previousByName.get(dep.package.name) || []).some((previousDep) => registrySignals(previousDep).has(signal));
|
|
271
|
+
})
|
|
272
|
+
.map((dep) => `${formatPackage(dep)} introduced registry risk signal: ${signal}`)
|
|
273
|
+
.sort();
|
|
274
|
+
if (details.length === 0)
|
|
275
|
+
return;
|
|
276
|
+
violations.push({
|
|
277
|
+
rule,
|
|
278
|
+
count: details.length,
|
|
279
|
+
message: `${details.length} new registry ${pluralize(details.length, 'risk signal', 'risk signals')}: ${signal}`,
|
|
280
|
+
details
|
|
281
|
+
});
|
|
282
|
+
}
|
|
40
283
|
/**
|
|
41
284
|
* Detects whether a dependency has a strong copyleft license.
|
|
42
285
|
*
|
|
@@ -189,3 +432,129 @@ function evaluatePolicyViolations(aggregated, rules) {
|
|
|
189
432
|
}
|
|
190
433
|
return violations;
|
|
191
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* Compute compare-mode policy violations that focus on newly introduced risky traits.
|
|
437
|
+
*
|
|
438
|
+
* Delta rules compare the current scan against a previous Dependency Radar JSON report. They only fire
|
|
439
|
+
* when a targeted trait appears in the current report and the baseline did not show that trait for the
|
|
440
|
+
* same package name, or did not show that supply-chain signal type at all when a signal cannot be tied to
|
|
441
|
+
* a package.
|
|
442
|
+
*/
|
|
443
|
+
function evaluateComparePolicyViolations(previous, current, rules) {
|
|
444
|
+
var _a, _b;
|
|
445
|
+
if (rules.size === 0)
|
|
446
|
+
return [];
|
|
447
|
+
const previousByName = byPackageName(previous.dependencies);
|
|
448
|
+
const currentDeps = Object.values(current.dependencies || {});
|
|
449
|
+
const violations = [];
|
|
450
|
+
if (rules.has('new-supply-chain-signal')) {
|
|
451
|
+
const previousSignals = supplyChainSignalKeys((_a = previous.supplyChain) === null || _a === void 0 ? void 0 : _a.signals);
|
|
452
|
+
const details = (((_b = current.supplyChain) === null || _b === void 0 ? void 0 : _b.signals) || [])
|
|
453
|
+
.filter((signal) => {
|
|
454
|
+
var _a;
|
|
455
|
+
const name = signalPackageName(signal);
|
|
456
|
+
if (name)
|
|
457
|
+
return !((_a = previousSignals.byPackage.get(name)) === null || _a === void 0 ? void 0 : _a.has(signal.type));
|
|
458
|
+
return !previousSignals.global.has(signal.type);
|
|
459
|
+
})
|
|
460
|
+
.map((signal) => `${signalPackageLabel(signal)} introduced supply-chain signal: ${signal.type}`)
|
|
461
|
+
.sort();
|
|
462
|
+
if (details.length > 0) {
|
|
463
|
+
violations.push({
|
|
464
|
+
rule: 'new-supply-chain-signal',
|
|
465
|
+
count: details.length,
|
|
466
|
+
message: `${details.length} new supply-chain ${pluralize(details.length, 'signal', 'signals')}`,
|
|
467
|
+
details
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (rules.has('new-install-script')) {
|
|
472
|
+
const details = currentDeps
|
|
473
|
+
.filter((dep) => {
|
|
474
|
+
if (sortedHooks(dep).length === 0)
|
|
475
|
+
return false;
|
|
476
|
+
return !(previousByName.get(dep.package.name) || []).some((previousDep) => sortedHooks(previousDep).length > 0);
|
|
477
|
+
})
|
|
478
|
+
.map((dep) => `${formatPackage(dep)} introduced install hooks: ${sortedHooks(dep).join(', ')}`)
|
|
479
|
+
.sort();
|
|
480
|
+
if (details.length > 0) {
|
|
481
|
+
violations.push({
|
|
482
|
+
rule: 'new-install-script',
|
|
483
|
+
count: details.length,
|
|
484
|
+
message: `${details.length} new ${pluralize(details.length, 'install script surface', 'install script surfaces')}`,
|
|
485
|
+
details
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (rules.has('new-native-binding')) {
|
|
490
|
+
const details = currentDeps
|
|
491
|
+
.filter((dep) => {
|
|
492
|
+
var _a;
|
|
493
|
+
if (!((_a = dep.execution) === null || _a === void 0 ? void 0 : _a.native))
|
|
494
|
+
return false;
|
|
495
|
+
return !(previousByName.get(dep.package.name) || []).some((previousDep) => { var _a; return Boolean((_a = previousDep.execution) === null || _a === void 0 ? void 0 : _a.native); });
|
|
496
|
+
})
|
|
497
|
+
.map((dep) => `${formatPackage(dep)} introduced native build/binary surface`)
|
|
498
|
+
.sort();
|
|
499
|
+
if (details.length > 0) {
|
|
500
|
+
violations.push({
|
|
501
|
+
rule: 'new-native-binding',
|
|
502
|
+
count: details.length,
|
|
503
|
+
message: `${details.length} new native ${pluralize(details.length, 'binding surface', 'binding surfaces')}`,
|
|
504
|
+
details
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (rules.has('new-bin')) {
|
|
509
|
+
const details = currentDeps
|
|
510
|
+
.filter((dep) => {
|
|
511
|
+
var _a;
|
|
512
|
+
if (!((_a = dep.package) === null || _a === void 0 ? void 0 : _a.hasBin))
|
|
513
|
+
return false;
|
|
514
|
+
return !(previousByName.get(dep.package.name) || []).some((previousDep) => { var _a; return Boolean((_a = previousDep.package) === null || _a === void 0 ? void 0 : _a.hasBin); });
|
|
515
|
+
})
|
|
516
|
+
.map((dep) => `${formatPackage(dep)} introduced a package bin`)
|
|
517
|
+
.sort();
|
|
518
|
+
if (details.length > 0) {
|
|
519
|
+
violations.push({
|
|
520
|
+
rule: 'new-bin',
|
|
521
|
+
count: details.length,
|
|
522
|
+
message: `${details.length} new package ${pluralize(details.length, 'bin', 'bins')}`,
|
|
523
|
+
details
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (rules.has('new-direct-dependency')) {
|
|
528
|
+
const details = currentDeps
|
|
529
|
+
.filter((dep) => {
|
|
530
|
+
var _a;
|
|
531
|
+
if (!((_a = dep.usage) === null || _a === void 0 ? void 0 : _a.direct))
|
|
532
|
+
return false;
|
|
533
|
+
return !(previousByName.get(dep.package.name) || []).some((previousDep) => { var _a; return Boolean((_a = previousDep.usage) === null || _a === void 0 ? void 0 : _a.direct); });
|
|
534
|
+
})
|
|
535
|
+
.map((dep) => `${formatPackage(dep)} is now a direct dependency`)
|
|
536
|
+
.sort();
|
|
537
|
+
if (details.length > 0) {
|
|
538
|
+
violations.push({
|
|
539
|
+
rule: 'new-direct-dependency',
|
|
540
|
+
count: details.length,
|
|
541
|
+
message: `${details.length} new direct ${pluralize(details.length, 'dependency', 'dependencies')}`,
|
|
542
|
+
details
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
pushNewExecutionSignalViolation(violations, previousByName, currentDeps, rules, 'new-child-process', 'child-process');
|
|
547
|
+
pushNewExecutionSignalViolation(violations, previousByName, currentDeps, rules, 'new-network-access', 'network-access');
|
|
548
|
+
pushNewExecutionSignalViolation(violations, previousByName, currentDeps, rules, 'new-env-access', 'reads-env');
|
|
549
|
+
pushNewExecutionSignalViolation(violations, previousByName, currentDeps, rules, 'new-home-access', 'reads-home');
|
|
550
|
+
pushNewExecutionSignalViolation(violations, previousByName, currentDeps, rules, 'new-ssh-usage', 'uses-ssh');
|
|
551
|
+
pushNewExecutionSignalViolation(violations, previousByName, currentDeps, rules, 'new-obfuscation-signal', 'obfuscated');
|
|
552
|
+
pushNewPackagingSignalViolation(violations, previousByName, currentDeps, rules, 'new-bundled-dependencies', 'bundled-dependencies');
|
|
553
|
+
pushNewPackagingSignalViolation(violations, previousByName, currentDeps, rules, 'new-shrinkwrap', 'embedded-shrinkwrap');
|
|
554
|
+
pushNewRegistrySignalViolation(violations, previousByName, currentDeps, rules, 'new-recent-package', 'recent-package');
|
|
555
|
+
pushNewRegistrySignalViolation(violations, previousByName, currentDeps, rules, 'new-recent-version', 'recent-version');
|
|
556
|
+
pushNewRegistrySignalViolation(violations, previousByName, currentDeps, rules, 'new-low-release-history', 'low-release-history');
|
|
557
|
+
pushNewRegistrySignalViolation(violations, previousByName, currentDeps, rules, 'new-reactivated-package', 'reactivated-package');
|
|
558
|
+
pushNewRegistrySignalViolation(violations, previousByName, currentDeps, rules, 'new-old-major-patch', 'old-major-new-patch');
|
|
559
|
+
return violations;
|
|
560
|
+
}
|