dependency-radar 0.7.0 → 0.8.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 +108 -7
- package/dist/aggregator.js +35 -9
- package/dist/cli.js +347 -39
- package/dist/compare.js +79 -0
- package/dist/failOn.js +16 -2
- package/dist/findings.js +166 -0
- package/dist/generated/spdx.js +3 -0
- package/dist/nodeEngine.js +181 -0
- package/dist/outputFormats.js +185 -0
- package/dist/report-assets.js +2 -2
- package/dist/report.js +137 -71
- package/dist/runners/importGraphRunner.js +9 -5
- package/dist/runners/lockfileGraph.js +144 -1
- package/dist/runners/lockfileSignals.js +303 -0
- package/dist/runners/npmLs.js +15 -0
- package/dist/schema.js +107 -0
- package/dist/utils.js +62 -3
- package/dist/why.js +69 -0
- package/dist/workspaceFilter.js +25 -0
- package/package.json +5 -4
- package/dist/runners/depcheckRunner.js +0 -23
- package/dist/runners/licenseChecker.js +0 -33
- package/dist/runners/madgeRunner.js +0 -29
package/dist/cli.js
CHANGED
|
@@ -13,10 +13,19 @@ 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 lockfileSignals_1 = require("./runners/lockfileSignals");
|
|
16
17
|
const report_1 = require("./report");
|
|
18
|
+
const compare_1 = require("./compare");
|
|
19
|
+
const outputFormats_1 = require("./outputFormats");
|
|
20
|
+
const why_1 = require("./why");
|
|
21
|
+
const schema_1 = require("./schema");
|
|
17
22
|
const failOn_1 = require("./failOn");
|
|
18
23
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
19
24
|
const utils_1 = require("./utils");
|
|
25
|
+
const skippedToolResult = {
|
|
26
|
+
ok: true,
|
|
27
|
+
status: "skipped",
|
|
28
|
+
};
|
|
20
29
|
function normalizeSlashes(p) {
|
|
21
30
|
return p.split(path_1.default.sep).join("/");
|
|
22
31
|
}
|
|
@@ -425,6 +434,8 @@ function inferPackageManager(rootPkg) {
|
|
|
425
434
|
return "pnpm";
|
|
426
435
|
if (raw.startsWith("yarn@") || raw === "yarn")
|
|
427
436
|
return "yarn";
|
|
437
|
+
if (raw.startsWith("bun@") || raw === "bun")
|
|
438
|
+
return "bun";
|
|
428
439
|
if (raw.startsWith("npm@") || raw === "npm")
|
|
429
440
|
return "npm";
|
|
430
441
|
return undefined;
|
|
@@ -437,6 +448,8 @@ async function detectPackageManager(projectPath, rootPkg, workspaceType) {
|
|
|
437
448
|
return workspaceType;
|
|
438
449
|
if (await detectYarnPnP(projectPath))
|
|
439
450
|
return "yarn";
|
|
451
|
+
if ((await (0, utils_1.pathExists)(path_1.default.join(projectPath, "bun.lock"))) || (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "bun.lockb"))))
|
|
452
|
+
return "bun";
|
|
440
453
|
if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules", ".pnpm")))
|
|
441
454
|
return "pnpm";
|
|
442
455
|
if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules", ".yarn-state.yml")))
|
|
@@ -444,10 +457,14 @@ async function detectPackageManager(projectPath, rootPkg, workspaceType) {
|
|
|
444
457
|
return "npm";
|
|
445
458
|
}
|
|
446
459
|
async function detectScanManager(projectPath, fallback) {
|
|
460
|
+
if (fallback === "bun" && ((await (0, utils_1.pathExists)(path_1.default.join(projectPath, "bun.lock"))) || (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "bun.lockb")))))
|
|
461
|
+
return "bun";
|
|
447
462
|
if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "pnpm-lock.yaml")))
|
|
448
463
|
return "pnpm";
|
|
449
464
|
if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "yarn.lock")))
|
|
450
465
|
return "yarn";
|
|
466
|
+
if ((await (0, utils_1.pathExists)(path_1.default.join(projectPath, "bun.lock"))) || (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "bun.lockb"))))
|
|
467
|
+
return "bun";
|
|
451
468
|
if ((await (0, utils_1.pathExists)(path_1.default.join(projectPath, "package-lock.json"))) ||
|
|
452
469
|
(await (0, utils_1.pathExists)(path_1.default.join(projectPath, "npm-shrinkwrap.json")))) {
|
|
453
470
|
return "npm";
|
|
@@ -911,11 +928,15 @@ function buildCombinedDependencyGraph(rootPath, packageMetas, dependencyGraphs)
|
|
|
911
928
|
return { name: "dependency-radar-workspace", version: "0.0.0", dependencies };
|
|
912
929
|
}
|
|
913
930
|
/**
|
|
914
|
-
* Parse
|
|
931
|
+
* Parse CLI tokens and return a configured CliOptions object reflecting the requested command and flags.
|
|
932
|
+
*
|
|
933
|
+
* Recognizes an optional leading command (scan, explain, compare, why, schema), positional operands for
|
|
934
|
+
* commands that require them (package name for explain/why, compare path for compare), and these flags:
|
|
935
|
+
* --project, --quiet, --out, --keep-temp, --offline, --json, --format, --sbom, --target-node,
|
|
936
|
+
* --audit-signatures, --schema, --timestamp, --open, --no-report, --fail-on, --help / -h.
|
|
915
937
|
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
* The --offline flag disables both audit and outdated checks.
|
|
938
|
+
* The --offline flag disables both audit and outdated checks. Unknown options or unexpected positional
|
|
939
|
+
* arguments cause the process to exit with an error.
|
|
919
940
|
*
|
|
920
941
|
* @param argv - Array of CLI tokens (typically process.argv.slice(2))
|
|
921
942
|
* @returns The resolved CliOptions with defaults applied and values overridden by argv
|
|
@@ -923,6 +944,7 @@ function buildCombinedDependencyGraph(rootPath, packageMetas, dependencyGraphs)
|
|
|
923
944
|
function parseArgs(argv) {
|
|
924
945
|
const opts = {
|
|
925
946
|
command: "scan",
|
|
947
|
+
commandProvided: false,
|
|
926
948
|
project: process.cwd(),
|
|
927
949
|
quiet: false,
|
|
928
950
|
out: "dependency-radar.html",
|
|
@@ -933,12 +955,18 @@ function parseArgs(argv) {
|
|
|
933
955
|
open: false,
|
|
934
956
|
noReport: false,
|
|
935
957
|
failOn: new Set(),
|
|
958
|
+
format: "html",
|
|
959
|
+
auditSignatures: false,
|
|
960
|
+
schema: false,
|
|
961
|
+
outProvided: false,
|
|
962
|
+
timestamp: false,
|
|
936
963
|
};
|
|
937
964
|
const args = [...argv];
|
|
938
965
|
if (args[0] && !args[0].startsWith("-")) {
|
|
939
966
|
const command = args.shift();
|
|
940
|
-
if (command === "scan" || command === "explain") {
|
|
967
|
+
if (command === "scan" || command === "explain" || command === "compare" || command === "why" || command === "schema") {
|
|
941
968
|
opts.command = command;
|
|
969
|
+
opts.commandProvided = true;
|
|
942
970
|
}
|
|
943
971
|
else {
|
|
944
972
|
opts.invalidCommand = command;
|
|
@@ -949,23 +977,65 @@ function parseArgs(argv) {
|
|
|
949
977
|
const arg = args.shift();
|
|
950
978
|
if (!arg)
|
|
951
979
|
break;
|
|
952
|
-
if (!arg.startsWith("-") && opts.command === "explain" && !opts.packageName) {
|
|
980
|
+
if (!arg.startsWith("-") && (opts.command === "explain" || opts.command === "why") && !opts.packageName) {
|
|
953
981
|
opts.packageName = arg;
|
|
954
982
|
}
|
|
955
|
-
else if (arg === "
|
|
956
|
-
opts.
|
|
983
|
+
else if (!arg.startsWith("-") && opts.command === "compare" && !opts.comparePath) {
|
|
984
|
+
opts.comparePath = arg;
|
|
985
|
+
}
|
|
986
|
+
else if (!arg.startsWith("-")) {
|
|
987
|
+
console.error(`Unexpected argument: "${arg}".`);
|
|
988
|
+
process.exit(1);
|
|
989
|
+
}
|
|
990
|
+
else if (arg === "--project")
|
|
991
|
+
opts.project = takeOptionValue(args, arg, true);
|
|
957
992
|
else if (arg === "--quiet")
|
|
958
993
|
opts.quiet = true;
|
|
959
|
-
else if (arg === "--out"
|
|
960
|
-
opts.out = args
|
|
994
|
+
else if (arg === "--out") {
|
|
995
|
+
opts.out = takeOptionValue(args, arg, true);
|
|
996
|
+
opts.outProvided = true;
|
|
997
|
+
}
|
|
961
998
|
else if (arg === "--keep-temp")
|
|
962
999
|
opts.keepTemp = true;
|
|
963
1000
|
else if (arg === "--offline") {
|
|
964
1001
|
opts.audit = false;
|
|
965
1002
|
opts.outdated = false;
|
|
966
1003
|
}
|
|
967
|
-
else if (arg === "--json")
|
|
1004
|
+
else if (arg === "--json") {
|
|
968
1005
|
opts.json = true;
|
|
1006
|
+
opts.format = "json";
|
|
1007
|
+
}
|
|
1008
|
+
else if (arg === "--format") {
|
|
1009
|
+
const format = takeOptionValue(args, arg);
|
|
1010
|
+
if (!isReportFormat(format)) {
|
|
1011
|
+
console.error(`Unknown --format: "${format}". Supported formats: html, json, sarif, cyclonedx, spdx.`);
|
|
1012
|
+
process.exit(1);
|
|
1013
|
+
}
|
|
1014
|
+
opts.format = format;
|
|
1015
|
+
opts.json = format === "json";
|
|
1016
|
+
}
|
|
1017
|
+
else if (arg === "--sbom") {
|
|
1018
|
+
const format = takeOptionValue(args, arg);
|
|
1019
|
+
if (format !== "cyclonedx" && format !== "spdx") {
|
|
1020
|
+
console.error('Unknown --sbom format. Supported formats: cyclonedx, spdx.');
|
|
1021
|
+
process.exit(1);
|
|
1022
|
+
}
|
|
1023
|
+
opts.format = format;
|
|
1024
|
+
}
|
|
1025
|
+
else if (arg === "--target-node") {
|
|
1026
|
+
const value = Number.parseInt(takeOptionValue(args, arg), 10);
|
|
1027
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1028
|
+
console.error("--target-node must be a positive Node.js major version.");
|
|
1029
|
+
process.exit(1);
|
|
1030
|
+
}
|
|
1031
|
+
opts.targetNodeMajor = value;
|
|
1032
|
+
}
|
|
1033
|
+
else if (arg === "--audit-signatures")
|
|
1034
|
+
opts.auditSignatures = true;
|
|
1035
|
+
else if (arg === "--schema")
|
|
1036
|
+
opts.schema = true;
|
|
1037
|
+
else if (arg === "--timestamp")
|
|
1038
|
+
opts.timestamp = true;
|
|
969
1039
|
else if (arg === "--open")
|
|
970
1040
|
opts.open = true;
|
|
971
1041
|
else if (arg === "--no-report")
|
|
@@ -993,35 +1063,128 @@ function parseArgs(argv) {
|
|
|
993
1063
|
printHelp();
|
|
994
1064
|
process.exit(0);
|
|
995
1065
|
}
|
|
1066
|
+
else {
|
|
1067
|
+
console.error(`Unknown option: "${arg}".`);
|
|
1068
|
+
process.exit(1);
|
|
1069
|
+
}
|
|
996
1070
|
}
|
|
997
1071
|
return opts;
|
|
998
1072
|
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Extracts and returns the next CLI token as the value for a given option, consuming it from `args`.
|
|
1075
|
+
*
|
|
1076
|
+
* @param args - Remaining argv tokens; the first element will be removed and returned.
|
|
1077
|
+
* @param option - The option name shown in the error message when a value is missing.
|
|
1078
|
+
* @param allowLeadingDash - When `true`, permit values that begin with `-`; otherwise treat such tokens as missing values.
|
|
1079
|
+
* @returns The consumed option value.
|
|
1080
|
+
* @remarks Exits the process with code 1 and prints an error if no valid value is present.
|
|
1081
|
+
*/
|
|
1082
|
+
function takeOptionValue(args, option, allowLeadingDash = false) {
|
|
1083
|
+
const value = args[0];
|
|
1084
|
+
if (!value || (!allowLeadingDash && value.startsWith("-"))) {
|
|
1085
|
+
console.error(`Missing value for ${option}.`);
|
|
1086
|
+
process.exit(1);
|
|
1087
|
+
}
|
|
1088
|
+
return args.shift();
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Checks whether a string is a supported report format.
|
|
1092
|
+
*
|
|
1093
|
+
* @param value - The string to test
|
|
1094
|
+
* @returns `true` if `value` is one of `html`, `json`, `sarif`, `cyclonedx`, or `spdx`, `false` otherwise.
|
|
1095
|
+
*/
|
|
1096
|
+
function isReportFormat(value) {
|
|
1097
|
+
return value === "html" || value === "json" || value === "sarif" || value === "cyclonedx" || value === "spdx";
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Formats a numeric date/time component as a two-digit string.
|
|
1101
|
+
*
|
|
1102
|
+
* @param value - The numeric part (e.g., day, month, hour, or minute) to format
|
|
1103
|
+
* @returns The value as a two-character string, padded with a leading zero when needed
|
|
1104
|
+
*/
|
|
1105
|
+
function padDatePart(value) {
|
|
1106
|
+
return String(value).padStart(2, "0");
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Formats a Date into a filesystem-safe timestamp string suitable for filenames.
|
|
1110
|
+
*
|
|
1111
|
+
* @param date - The date to format (local time is used).
|
|
1112
|
+
* @returns A string in the form `YYYY-MM-DD_HH-MM-SS` (zero-padded).
|
|
1113
|
+
*/
|
|
1114
|
+
function formatFilenameTimestamp(date) {
|
|
1115
|
+
return [
|
|
1116
|
+
date.getFullYear(),
|
|
1117
|
+
padDatePart(date.getMonth() + 1),
|
|
1118
|
+
padDatePart(date.getDate()),
|
|
1119
|
+
].join("-") + "_" + [
|
|
1120
|
+
padDatePart(date.getHours()),
|
|
1121
|
+
padDatePart(date.getMinutes()),
|
|
1122
|
+
padDatePart(date.getSeconds()),
|
|
1123
|
+
].join("-");
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Insert a local timestamp into the filename portion of an output path.
|
|
1127
|
+
*
|
|
1128
|
+
* If the path has a file extension, the timestamp is inserted before the extension;
|
|
1129
|
+
* otherwise the timestamp is appended to the filename. The directory portion is preserved.
|
|
1130
|
+
*
|
|
1131
|
+
* @param outputPath - The file path whose filename will receive the timestamp
|
|
1132
|
+
* @param date - Date to derive the timestamp from; defaults to the current date/time
|
|
1133
|
+
* @returns The input path with a timestamp embedded into the filename, preserving directory and extension
|
|
1134
|
+
*/
|
|
1135
|
+
function addTimestampToOutputPath(outputPath, date = new Date()) {
|
|
1136
|
+
const parsed = path_1.default.parse(outputPath);
|
|
1137
|
+
const timestamp = formatFilenameTimestamp(date);
|
|
1138
|
+
const basename = parsed.ext
|
|
1139
|
+
? `${parsed.name}.${timestamp}${parsed.ext}`
|
|
1140
|
+
: `${parsed.name}.${timestamp}`;
|
|
1141
|
+
return path_1.default.join(parsed.dir, basename);
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Determines whether the given package manager uses registry-based collectors.
|
|
1145
|
+
*
|
|
1146
|
+
* @returns `true` if the manager is `npm`, `pnpm`, or `yarn`, `false` otherwise.
|
|
1147
|
+
*/
|
|
1148
|
+
function supportsRegistryCollectors(manager) {
|
|
1149
|
+
return manager === "npm" || manager === "pnpm" || manager === "yarn";
|
|
1150
|
+
}
|
|
999
1151
|
/**
|
|
1000
1152
|
* Print the CLI usage and available options to the console.
|
|
1001
1153
|
*
|
|
1002
1154
|
* Displays the command synopsis and descriptions for supported flags including
|
|
1003
|
-
* --project, --out, --json, --no-report, --keep-temp, --offline, --open, and --fail-on.
|
|
1155
|
+
* --project, --out, --json, --timestamp, --no-report, --keep-temp, --offline, --open, and --fail-on.
|
|
1004
1156
|
*/
|
|
1005
1157
|
function printHelp() {
|
|
1006
1158
|
console.log(`dependency-radar [scan] [options]
|
|
1007
1159
|
dependency-radar explain <package-name> [options]
|
|
1160
|
+
dependency-radar why <package-name> [options]
|
|
1161
|
+
dependency-radar compare <previous dependency-radar.json> [options]
|
|
1008
1162
|
|
|
1009
1163
|
If no command is provided, \`scan\` is run by default.
|
|
1010
1164
|
|
|
1011
1165
|
Options:
|
|
1012
1166
|
--project <path> Project folder (default: cwd)
|
|
1013
1167
|
--quiet Suppress progress/info logs but keep summary and failures
|
|
1014
|
-
--out <path> Output
|
|
1168
|
+
--out <path> Output file (default depends on format)
|
|
1169
|
+
--format <format> Output format: html, json, sarif, cyclonedx, spdx
|
|
1170
|
+
--sbom <format> Write an SBOM: cyclonedx or spdx
|
|
1171
|
+
--target-node <n> Add Node major compatibility findings
|
|
1172
|
+
--audit-signatures Verify npm registry signatures/provenance (opt-in, online only)
|
|
1173
|
+
--schema Print JSON schema, or write it when --out is provided
|
|
1015
1174
|
--json Write aggregated data to JSON (default filename: dependency-radar.json)
|
|
1175
|
+
--timestamp Add a local timestamp to generated report filenames
|
|
1016
1176
|
--no-report Do not write HTML/JSON report files or temp artifacts to disk
|
|
1017
1177
|
--keep-temp Keep .dependency-radar folder
|
|
1018
1178
|
--offline Skip npm audit and npm outdated (useful for offline scans)
|
|
1019
1179
|
--open Open the generated report using the system default application
|
|
1020
1180
|
--fail-on <rules> Fail with exit code 1 when selected rules are violated
|
|
1021
1181
|
Supported: reachable-vuln, production-vuln, high-severity-vuln,
|
|
1022
|
-
licence-mismatch, copyleft-detected, unknown-licence
|
|
1182
|
+
licence-mismatch, copyleft-detected, unknown-licence,
|
|
1183
|
+
supply-chain-source
|
|
1023
1184
|
|
|
1024
1185
|
\`explain\` reuses the same local scan model and prints a terminal view for one package.
|
|
1186
|
+
\`why\` prints shortest dependency paths for one package.
|
|
1187
|
+
\`compare\` scans the current project and compares it with a previous JSON report.
|
|
1025
1188
|
`);
|
|
1026
1189
|
}
|
|
1027
1190
|
/**
|
|
@@ -1329,20 +1492,44 @@ function printCliSummary(summary) {
|
|
|
1329
1492
|
}
|
|
1330
1493
|
console.log("");
|
|
1331
1494
|
}
|
|
1495
|
+
function formatLabel(format) {
|
|
1496
|
+
if (format === "html")
|
|
1497
|
+
return "Report";
|
|
1498
|
+
if (format === "json")
|
|
1499
|
+
return "JSON";
|
|
1500
|
+
if (format === "sarif")
|
|
1501
|
+
return "SARIF";
|
|
1502
|
+
if (format === "cyclonedx")
|
|
1503
|
+
return "CycloneDX SBOM";
|
|
1504
|
+
return "SPDX SBOM";
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Run a full dependency analysis for a project/workspace and return the aggregated results.
|
|
1508
|
+
*
|
|
1509
|
+
* Performs workspace detection, per-package collection (audit, dependency trees, import graphs, outdated), merges collected data, runs aggregation and policy evaluation, and optionally writes report artifacts to disk. May create a temporary directory under the project (projectPath/.dependency-radar) and will remove it unless `opts.keepTemp` is set.
|
|
1510
|
+
*
|
|
1511
|
+
* @param opts - CLI-resolved options controlling project path, enabled collectors (audit/outdated), artifact format/output, reporting flags, and policy rules.
|
|
1512
|
+
* @param options.shouldWriteArtifacts - If true, write report artifacts (JSON/HTML/SBOM) to the resolved output path.
|
|
1513
|
+
* @param options.emitArtifactSummary - If true, print a summary line about artifact creation to stdout.
|
|
1514
|
+
* @param options.emitWorkspacePackageSummary - If true, print a brief workspace package summary when a workspace is detected.
|
|
1515
|
+
* @returns An AnalysisExecutionResult containing the aggregated report, CLI summary, policy violations, dependency counts, timing, output path/creation status, collector availability, and workspace metadata.
|
|
1516
|
+
*/
|
|
1332
1517
|
async function executeAnalysis(opts, options) {
|
|
1333
|
-
var _a;
|
|
1518
|
+
var _a, _b;
|
|
1334
1519
|
const shouldWriteArtifacts = options.shouldWriteArtifacts;
|
|
1335
1520
|
const projectPath = path_1.default.resolve(opts.project);
|
|
1336
|
-
let outputPath =
|
|
1521
|
+
let outputPath = opts.outProvided
|
|
1522
|
+
? path_1.default.resolve(opts.out)
|
|
1523
|
+
: path_1.default.resolve(projectPath, opts.out);
|
|
1337
1524
|
const startTime = Date.now();
|
|
1338
1525
|
let dependencyCount = 0;
|
|
1339
1526
|
let outputCreated = false;
|
|
1340
1527
|
if (opts.command === "scan" && opts.noReport && opts.keepTemp && !opts.quiet) {
|
|
1341
1528
|
console.log(statusLine("⚠", "--keep-temp is ignored when --no-report is enabled."));
|
|
1342
1529
|
}
|
|
1343
|
-
if (opts.
|
|
1344
|
-
opts.out =
|
|
1345
|
-
outputPath = path_1.default.resolve(opts.out);
|
|
1530
|
+
if (!opts.outProvided && opts.format !== "html") {
|
|
1531
|
+
opts.out = (0, outputFormats_1.defaultOutputName)(opts.format);
|
|
1532
|
+
outputPath = path_1.default.resolve(projectPath, opts.out);
|
|
1346
1533
|
}
|
|
1347
1534
|
if (shouldWriteArtifacts) {
|
|
1348
1535
|
try {
|
|
@@ -1352,20 +1539,24 @@ async function executeAnalysis(opts, options) {
|
|
|
1352
1539
|
if ((stat && stat.isDirectory()) ||
|
|
1353
1540
|
endsWithSeparator ||
|
|
1354
1541
|
(!stat && !hasExtension)) {
|
|
1355
|
-
outputPath = path_1.default.join(outputPath,
|
|
1542
|
+
outputPath = path_1.default.join(outputPath, (0, outputFormats_1.defaultOutputName)(opts.format));
|
|
1356
1543
|
}
|
|
1357
1544
|
}
|
|
1358
1545
|
catch {
|
|
1359
1546
|
// ignore, best-effort path normalization
|
|
1360
1547
|
}
|
|
1548
|
+
if (opts.timestamp) {
|
|
1549
|
+
outputPath = addTimestampToOutputPath(outputPath);
|
|
1550
|
+
}
|
|
1361
1551
|
}
|
|
1362
1552
|
const tempDir = path_1.default.join(projectPath, ".dependency-radar");
|
|
1363
1553
|
const workspace = await detectWorkspace(projectPath);
|
|
1364
1554
|
const yarnPnP = await detectYarnPnP(projectPath);
|
|
1365
1555
|
if (workspace.type === "yarn" && workspace.packagePaths.length === 0) {
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1556
|
+
if (!opts.quiet) {
|
|
1557
|
+
console.log(statusLine("⚠", "Yarn Plug'n'Play detected; using the root package and yarn.lock where possible."));
|
|
1558
|
+
}
|
|
1559
|
+
workspace.packagePaths = [projectPath];
|
|
1369
1560
|
}
|
|
1370
1561
|
const hasProjectNodeModules = await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules"));
|
|
1371
1562
|
if (!hasProjectNodeModules) {
|
|
@@ -1373,9 +1564,9 @@ async function executeAnalysis(opts, options) {
|
|
|
1373
1564
|
? "single project"
|
|
1374
1565
|
: `${workspace.type.toUpperCase()} workspace`;
|
|
1375
1566
|
const yarnHint = yarnPnP
|
|
1376
|
-
? " Yarn Plug'n'Play appears enabled;
|
|
1567
|
+
? " Yarn Plug'n'Play appears enabled; lockfile graph data will be used where possible, but package metadata from zip/cache files is not crawled in this release."
|
|
1377
1568
|
: "";
|
|
1378
|
-
console.warn(colorLeadingSymbol(`⚠ node_modules was not found at ${projectPath}. Scan completeness may be reduced for this ${workspaceHint}. Run your package manager install
|
|
1569
|
+
console.warn(colorLeadingSymbol(`⚠ node_modules was not found at ${projectPath}. Scan completeness may be reduced for this ${workspaceHint}. Run your package manager install before scanning when local package metadata is required.${yarnHint}`));
|
|
1379
1570
|
}
|
|
1380
1571
|
const rootPkg = await readJsonFile(path_1.default.join(projectPath, "package.json"));
|
|
1381
1572
|
const projectDependencyPolicy = workspace.pnpmWorkspaceOverrides
|
|
@@ -1394,20 +1585,22 @@ async function executeAnalysis(opts, options) {
|
|
|
1394
1585
|
getToolVersion("pnpm", projectPath),
|
|
1395
1586
|
getToolVersion("yarn", projectPath),
|
|
1396
1587
|
]);
|
|
1588
|
+
const bunVersion = await getToolVersion("bun", projectPath);
|
|
1397
1589
|
const toolVersions = compactToolVersions({
|
|
1398
1590
|
npm: npmVersion,
|
|
1399
1591
|
pnpm: pnpmVersion,
|
|
1400
1592
|
yarn: yarnVersion,
|
|
1593
|
+
bun: bunVersion,
|
|
1401
1594
|
});
|
|
1402
1595
|
const packageManagerVersion = scanManager === "npm"
|
|
1403
1596
|
? npmVersion
|
|
1404
1597
|
: scanManager === "pnpm"
|
|
1405
1598
|
? pnpmVersion
|
|
1406
|
-
:
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1599
|
+
: scanManager === "yarn"
|
|
1600
|
+
? yarnVersion
|
|
1601
|
+
: bunVersion;
|
|
1602
|
+
if (!opts.quiet && packageManager === "yarn" && yarnPnP) {
|
|
1603
|
+
console.log(statusLine("⚠", "Yarn Plug'n'Play detected; using lockfile-derived graph data where available."));
|
|
1411
1604
|
}
|
|
1412
1605
|
const packagePaths = workspace.packagePaths;
|
|
1413
1606
|
const workspaceLabel = workspace.type === "none"
|
|
@@ -1430,6 +1623,11 @@ async function executeAnalysis(opts, options) {
|
|
|
1430
1623
|
const perPackageLs = [];
|
|
1431
1624
|
const perPackageImportGraph = [];
|
|
1432
1625
|
const perPackageOutdated = [];
|
|
1626
|
+
const supplyChainResult = await (0, lockfileSignals_1.runLockfileSupplyChainSignals)(projectPath, tempDir, {
|
|
1627
|
+
persistToDisk: shouldWriteArtifacts,
|
|
1628
|
+
auditSignatures: opts.auditSignatures,
|
|
1629
|
+
offline: !opts.audit
|
|
1630
|
+
}).catch((err) => ({ ok: false, error: String(err) }));
|
|
1433
1631
|
for (const meta of packageMetas) {
|
|
1434
1632
|
spinner.update(`Scanning ${workspaceLabel} (${perPackageLs.length + 1}/${packageMetas.length}) at ${projectPath}`);
|
|
1435
1633
|
const pkgTempDir = path_1.default.join(tempDir, meta.name.replace(/[^a-zA-Z0-9._-]/g, "_"));
|
|
@@ -1438,8 +1636,9 @@ async function executeAnalysis(opts, options) {
|
|
|
1438
1636
|
}
|
|
1439
1637
|
const [a, l, ig, o] = await Promise.all([
|
|
1440
1638
|
opts.audit
|
|
1639
|
+
&& supportsRegistryCollectors(scanManager)
|
|
1441
1640
|
? (0, npmAudit_1.runPackageAudit)(meta.path, pkgTempDir, scanManager, yarnVersion, { persistToDisk: shouldWriteArtifacts }).catch((err) => ({ ok: false, error: String(err) }))
|
|
1442
|
-
: Promise.resolve(undefined),
|
|
1641
|
+
: Promise.resolve(opts.audit ? skippedToolResult : undefined),
|
|
1443
1642
|
(0, npmLs_1.runNpmLs)(meta.path, pkgTempDir, scanManager, {
|
|
1444
1643
|
contextLabel: meta.name,
|
|
1445
1644
|
lockfileSearchRoot: projectPath,
|
|
@@ -1450,8 +1649,9 @@ async function executeAnalysis(opts, options) {
|
|
|
1450
1649
|
persistToDisk: shouldWriteArtifacts,
|
|
1451
1650
|
}).catch((err) => ({ ok: false, error: String(err) })),
|
|
1452
1651
|
opts.outdated
|
|
1652
|
+
&& supportsRegistryCollectors(scanManager)
|
|
1453
1653
|
? (0, npmOutdated_1.runPackageOutdated)(meta.path, pkgTempDir, scanManager, { persistToDisk: shouldWriteArtifacts }).catch((err) => ({ ok: false, error: String(err) }))
|
|
1454
|
-
: Promise.resolve(undefined),
|
|
1654
|
+
: Promise.resolve(opts.outdated ? skippedToolResult : undefined),
|
|
1455
1655
|
]);
|
|
1456
1656
|
perPackageAudit.push(a);
|
|
1457
1657
|
perPackageLs.push(l);
|
|
@@ -1460,14 +1660,25 @@ async function executeAnalysis(opts, options) {
|
|
|
1460
1660
|
}
|
|
1461
1661
|
if (opts.audit) {
|
|
1462
1662
|
const auditOk = perPackageAudit.every((r) => r && r.ok);
|
|
1663
|
+
const auditSkipped = perPackageAudit.every((r) => (r === null || r === void 0 ? void 0 : r.status) === "skipped");
|
|
1463
1664
|
if (!opts.quiet || !auditOk) {
|
|
1464
|
-
spinner.log(statusLine(auditOk ? "✔" : "✖", `${scanManager.toUpperCase()} audit data ${auditOk ? "collected" : "unavailable"}`));
|
|
1665
|
+
spinner.log(statusLine(auditSkipped ? "ℹ" : auditOk ? "✔" : "✖", `${scanManager.toUpperCase()} audit data ${auditSkipped ? "skipped" : auditOk ? "collected" : "unavailable"}`));
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
if (opts.auditSignatures && !opts.quiet) {
|
|
1669
|
+
const audit = supplyChainResult.ok ? (_a = supplyChainResult.data) === null || _a === void 0 ? void 0 : _a.signatureAudit : undefined;
|
|
1670
|
+
if ((audit === null || audit === void 0 ? void 0 : audit.status) === "skipped") {
|
|
1671
|
+
spinner.log(statusLine("⚠", "npm audit signatures skipped (--offline)"));
|
|
1672
|
+
}
|
|
1673
|
+
else {
|
|
1674
|
+
spinner.log(statusLine((audit === null || audit === void 0 ? void 0 : audit.ok) ? "✔" : "✖", `npm audit signatures ${(audit === null || audit === void 0 ? void 0 : audit.ok) ? "verified" : "unavailable"}`));
|
|
1465
1675
|
}
|
|
1466
1676
|
}
|
|
1467
1677
|
if (opts.outdated) {
|
|
1468
1678
|
const outdatedOk = perPackageOutdated.every((r) => r.result && r.result.ok);
|
|
1679
|
+
const outdatedSkipped = perPackageOutdated.every((r) => { var _a; return ((_a = r.result) === null || _a === void 0 ? void 0 : _a.status) === "skipped"; });
|
|
1469
1680
|
if (!opts.quiet || !outdatedOk) {
|
|
1470
|
-
spinner.log(statusLine(outdatedOk ? "✔" : "✖", `${scanManager.toUpperCase()} outdated data ${outdatedOk ? "collected" : "unavailable"}`));
|
|
1681
|
+
spinner.log(statusLine(outdatedSkipped ? "ℹ" : outdatedOk ? "✔" : "✖", `${scanManager.toUpperCase()} outdated data ${outdatedSkipped ? "skipped" : outdatedOk ? "collected" : "unavailable"}`));
|
|
1471
1682
|
}
|
|
1472
1683
|
}
|
|
1473
1684
|
const mergedAuditData = mergeAuditResults(perPackageAudit.map((r) => (r && r.ok ? r.data : undefined)));
|
|
@@ -1498,7 +1709,7 @@ async function executeAnalysis(opts, options) {
|
|
|
1498
1709
|
if (lsFailures.length > 0) {
|
|
1499
1710
|
const packageList = lsFailures.map((entry) => { var _a; return (_a = entry.meta) === null || _a === void 0 ? void 0 : _a.name; }).filter(Boolean);
|
|
1500
1711
|
spinner.log(`Dependency tree warning: ${lsFailures.length} package${lsFailures.length === 1 ? "" : "s"} failed (${packageList.join(", ")}).`);
|
|
1501
|
-
spinner.log(`First dependency tree error: ${((
|
|
1712
|
+
spinner.log(`First dependency tree error: ${((_b = lsFailures[0].result) === null || _b === void 0 ? void 0 : _b.error) || "pnpm ls failed"}`);
|
|
1502
1713
|
}
|
|
1503
1714
|
if (importFailures.length > 0) {
|
|
1504
1715
|
spinner.log(`Import graph warning: ${importFailures.length} package${importFailures.length === 1 ? "" : "s"} failed (${importFailures[0].error || "import graph failed"})`);
|
|
@@ -1509,6 +1720,7 @@ async function executeAnalysis(opts, options) {
|
|
|
1509
1720
|
npmLsResult,
|
|
1510
1721
|
importGraphResult,
|
|
1511
1722
|
outdatedResult,
|
|
1723
|
+
supplyChainResult,
|
|
1512
1724
|
pkgOverride: mergedPkgForAggregator,
|
|
1513
1725
|
projectPackageJson: rootPkg,
|
|
1514
1726
|
...(projectDependencyPolicy ? { projectDependencyPolicy } : {}),
|
|
@@ -1536,6 +1748,7 @@ async function executeAnalysis(opts, options) {
|
|
|
1536
1748
|
arch: process.arch,
|
|
1537
1749
|
ci: isCI(),
|
|
1538
1750
|
...(toolVersions ? { toolVersions } : {}),
|
|
1751
|
+
...(typeof opts.targetNodeMajor === "number" ? { targetNodeMajor: opts.targetNodeMajor } : {}),
|
|
1539
1752
|
});
|
|
1540
1753
|
dependencyCount = Object.keys(aggregated.dependencies).length;
|
|
1541
1754
|
const importGraphComplete = perPackageImportGraph.every((result) => result.ok);
|
|
@@ -1547,10 +1760,22 @@ async function executeAnalysis(opts, options) {
|
|
|
1547
1760
|
console.log(`Detected ${workspace.type.toUpperCase()} workspace with ${packagePaths.length} package${packagePaths.length === 1 ? "" : "s"}.`);
|
|
1548
1761
|
}
|
|
1549
1762
|
if (dependencyCount > 0 && shouldWriteArtifacts) {
|
|
1550
|
-
if (opts.json) {
|
|
1763
|
+
if (opts.format === "json") {
|
|
1551
1764
|
await promises_1.default.mkdir(path_1.default.dirname(outputPath), { recursive: true });
|
|
1552
1765
|
await promises_1.default.writeFile(outputPath, JSON.stringify(aggregated, null, 2), "utf8");
|
|
1553
1766
|
}
|
|
1767
|
+
else if (opts.format === "sarif") {
|
|
1768
|
+
await promises_1.default.mkdir(path_1.default.dirname(outputPath), { recursive: true });
|
|
1769
|
+
await promises_1.default.writeFile(outputPath, (0, outputFormats_1.renderSarif)(aggregated), "utf8");
|
|
1770
|
+
}
|
|
1771
|
+
else if (opts.format === "cyclonedx") {
|
|
1772
|
+
await promises_1.default.mkdir(path_1.default.dirname(outputPath), { recursive: true });
|
|
1773
|
+
await promises_1.default.writeFile(outputPath, (0, outputFormats_1.renderCycloneDx)(aggregated), "utf8");
|
|
1774
|
+
}
|
|
1775
|
+
else if (opts.format === "spdx") {
|
|
1776
|
+
await promises_1.default.mkdir(path_1.default.dirname(outputPath), { recursive: true });
|
|
1777
|
+
await promises_1.default.writeFile(outputPath, (0, outputFormats_1.renderSpdx)(aggregated), "utf8");
|
|
1778
|
+
}
|
|
1554
1779
|
else {
|
|
1555
1780
|
await (0, report_1.renderReport)(aggregated, outputPath);
|
|
1556
1781
|
}
|
|
@@ -1566,10 +1791,10 @@ async function executeAnalysis(opts, options) {
|
|
|
1566
1791
|
console.log(statusLine("ℹ", "Report output disabled (--no-report); no report artifacts written."));
|
|
1567
1792
|
}
|
|
1568
1793
|
else if (outputCreated) {
|
|
1569
|
-
console.log(statusLine("✔", `${opts.
|
|
1794
|
+
console.log(statusLine("✔", `${formatLabel(opts.format)} written to ${outputPath}`));
|
|
1570
1795
|
}
|
|
1571
1796
|
else {
|
|
1572
|
-
console.log(statusLine("✖", `No dependencies were found - ${opts.
|
|
1797
|
+
console.log(statusLine("✖", `No dependencies were found - ${formatLabel(opts.format)} not created`));
|
|
1573
1798
|
}
|
|
1574
1799
|
}
|
|
1575
1800
|
return {
|
|
@@ -1619,7 +1844,7 @@ async function runScanCommand(opts) {
|
|
|
1619
1844
|
console.log(statusLine("✖", "Skipping auto-open because --no-report is enabled."));
|
|
1620
1845
|
}
|
|
1621
1846
|
else if (opts.open && result.outputCreated && !isCI()) {
|
|
1622
|
-
console.log(statusLine("↗", `Opening ${path_1.default.basename(result.outputPath)} using system default ${opts.
|
|
1847
|
+
console.log(statusLine("↗", `Opening ${path_1.default.basename(result.outputPath)} using system default ${opts.format === "html" ? "browser" : "application"}.`));
|
|
1623
1848
|
openInBrowser(result.outputPath);
|
|
1624
1849
|
}
|
|
1625
1850
|
else if (opts.open && result.outputCreated && isCI()) {
|
|
@@ -1658,6 +1883,64 @@ async function runExplainCommand(opts) {
|
|
|
1658
1883
|
process.exit(1);
|
|
1659
1884
|
}
|
|
1660
1885
|
}
|
|
1886
|
+
async function runWhyCommand(opts) {
|
|
1887
|
+
var _a;
|
|
1888
|
+
const packageName = (_a = opts.packageName) === null || _a === void 0 ? void 0 : _a.trim();
|
|
1889
|
+
if (!packageName) {
|
|
1890
|
+
console.error("Missing package name for why. Usage: dependency-radar why <package-name>");
|
|
1891
|
+
process.exit(1);
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
const result = await executeAnalysis(opts, {
|
|
1895
|
+
shouldWriteArtifacts: false,
|
|
1896
|
+
emitArtifactSummary: false,
|
|
1897
|
+
emitWorkspacePackageSummary: false,
|
|
1898
|
+
});
|
|
1899
|
+
console.log("");
|
|
1900
|
+
const output = (0, why_1.formatWhyOutput)(result.aggregated, packageName);
|
|
1901
|
+
console.log(output);
|
|
1902
|
+
if (output.startsWith("Package not found")) {
|
|
1903
|
+
process.exit(1);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
async function runCompareCommand(opts) {
|
|
1907
|
+
var _a;
|
|
1908
|
+
const previousPath = (_a = opts.comparePath) === null || _a === void 0 ? void 0 : _a.trim();
|
|
1909
|
+
if (!previousPath) {
|
|
1910
|
+
console.error("Missing previous report path. Usage: dependency-radar compare <previous dependency-radar.json>");
|
|
1911
|
+
process.exit(1);
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
let previous;
|
|
1915
|
+
try {
|
|
1916
|
+
const parsed = JSON.parse(await promises_1.default.readFile(path_1.default.resolve(previousPath), "utf8"));
|
|
1917
|
+
const schemaVersion = parsed && typeof parsed === "object" ? parsed.schemaVersion : undefined;
|
|
1918
|
+
if (!parsed ||
|
|
1919
|
+
typeof parsed !== "object" ||
|
|
1920
|
+
schemaVersion !== schema_1.REPORT_SCHEMA_VERSION ||
|
|
1921
|
+
!parsed.project ||
|
|
1922
|
+
!parsed.summary ||
|
|
1923
|
+
!parsed.dependencies ||
|
|
1924
|
+
typeof parsed.dependencies !== "object") {
|
|
1925
|
+
console.error(`Previous report schema mismatch: expected schemaVersion ${schema_1.REPORT_SCHEMA_VERSION}, found ${schemaVersion !== null && schemaVersion !== void 0 ? schemaVersion : "missing"}.`);
|
|
1926
|
+
process.exit(1);
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
previous = parsed;
|
|
1930
|
+
}
|
|
1931
|
+
catch (err) {
|
|
1932
|
+
console.error(`Could not read previous report at ${previousPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1933
|
+
process.exit(1);
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
const result = await executeAnalysis(opts, {
|
|
1937
|
+
shouldWriteArtifacts: false,
|
|
1938
|
+
emitArtifactSummary: false,
|
|
1939
|
+
emitWorkspacePackageSummary: false,
|
|
1940
|
+
});
|
|
1941
|
+
console.log("");
|
|
1942
|
+
console.log((0, compare_1.formatCompareOutput)((0, compare_1.compareReports)(previous, result.aggregated)));
|
|
1943
|
+
}
|
|
1661
1944
|
/**
|
|
1662
1945
|
* Run the CLI entrypoint and dispatch to the selected command.
|
|
1663
1946
|
*/
|
|
@@ -1669,10 +1952,22 @@ async function run() {
|
|
|
1669
1952
|
return;
|
|
1670
1953
|
}
|
|
1671
1954
|
try {
|
|
1955
|
+
if ((opts.schema && !opts.commandProvided) || opts.command === "schema") {
|
|
1956
|
+
await runSchemaCommand(opts);
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1672
1959
|
if (opts.command === "explain") {
|
|
1673
1960
|
await runExplainCommand(opts);
|
|
1674
1961
|
return;
|
|
1675
1962
|
}
|
|
1963
|
+
if (opts.command === "why") {
|
|
1964
|
+
await runWhyCommand(opts);
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
if (opts.command === "compare") {
|
|
1968
|
+
await runCompareCommand(opts);
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1676
1971
|
await runScanCommand(opts);
|
|
1677
1972
|
}
|
|
1678
1973
|
catch (err) {
|
|
@@ -1680,6 +1975,19 @@ async function run() {
|
|
|
1680
1975
|
process.exit(1);
|
|
1681
1976
|
}
|
|
1682
1977
|
}
|
|
1978
|
+
async function runSchemaCommand(opts) {
|
|
1979
|
+
const schema = (0, schema_1.renderReportJsonSchema)();
|
|
1980
|
+
if (opts.outProvided) {
|
|
1981
|
+
const outputPath = path_1.default.resolve(opts.out);
|
|
1982
|
+
await promises_1.default.mkdir(path_1.default.dirname(outputPath), { recursive: true });
|
|
1983
|
+
await promises_1.default.writeFile(outputPath, schema, "utf8");
|
|
1984
|
+
if (!opts.quiet) {
|
|
1985
|
+
console.log(statusLine("✔", `JSON schema written to ${outputPath}`));
|
|
1986
|
+
}
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
console.log(schema);
|
|
1990
|
+
}
|
|
1683
1991
|
run();
|
|
1684
1992
|
function createProgressReporter(text, quiet) {
|
|
1685
1993
|
if (!quiet)
|