dependency-radar 0.8.0 → 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 +6 -6
package/dist/aggregator.js
CHANGED
|
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.aggregateData = aggregateData;
|
|
7
|
+
exports.detectLocalExecutionSignals = detectLocalExecutionSignals;
|
|
8
|
+
exports.collectPackageExecutionSignals = collectPackageExecutionSignals;
|
|
7
9
|
const utils_1 = require("./utils");
|
|
8
10
|
const license_1 = require("./license");
|
|
9
11
|
const findings_1 = require("./findings");
|
|
@@ -287,6 +289,17 @@ function isWorkspacePackageNode(node, input) {
|
|
|
287
289
|
}
|
|
288
290
|
return false;
|
|
289
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Builds a consolidated AggregatedData object for a dependency and audit scan.
|
|
294
|
+
*
|
|
295
|
+
* Combines package metadata, dependency graph structure, vulnerability summaries,
|
|
296
|
+
* import/usage data, outdated status, supply-chain signals, and per-dependency
|
|
297
|
+
* heuristics (license, execution/installation scripts, packaging, types support,
|
|
298
|
+
* links, peer requirements, and upgrade blockers) into a single summary object.
|
|
299
|
+
*
|
|
300
|
+
* @param input - All inputs and options required to perform aggregation (tool outputs, workspace/configuration, resolution paths, platform/runtime metadata, and optional policy overrides).
|
|
301
|
+
* @returns The assembled AggregatedData containing project metadata, environment info, per-dependency records, findings, and summary counts.
|
|
302
|
+
*/
|
|
290
303
|
async function aggregateData(input) {
|
|
291
304
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
292
305
|
const pkg = input.pkgOverride || (await (0, utils_1.readPackageJson)(input.projectPath));
|
|
@@ -351,6 +364,7 @@ async function aggregateData(input) {
|
|
|
351
364
|
const parentIds = Array.from(node.parents).sort();
|
|
352
365
|
const origins = buildOrigins(rootCauses, parentIds, (_f = input.workspaceUsage) === null || _f === void 0 ? void 0 : _f.get(node.name), input.workspaceEnabled, MAX_TOP_ROOT_PACKAGES, MAX_TOP_PARENT_PACKAGES);
|
|
353
366
|
const execution = packageInsights.execution;
|
|
367
|
+
const packaging = packageInsights.packaging;
|
|
354
368
|
const id = node.key;
|
|
355
369
|
const upgrade = buildUpgradeBlock(packageInsights);
|
|
356
370
|
const outdated = resolveOutdated(node, direct, outdatedById, outdatedUnknownNames);
|
|
@@ -414,7 +428,8 @@ async function aggregateData(input) {
|
|
|
414
428
|
fanOut: node.children.size,
|
|
415
429
|
...(subDeps ? { subDeps } : {})
|
|
416
430
|
},
|
|
417
|
-
...(execution ? { execution } : {})
|
|
431
|
+
...(execution ? { execution } : {}),
|
|
432
|
+
...(packaging ? { packaging } : {})
|
|
418
433
|
};
|
|
419
434
|
}
|
|
420
435
|
const minRequiredMajor = deriveMinRequiredMajor(nodeEngineRanges);
|
|
@@ -1243,6 +1258,16 @@ function buildUpgradeBlock(insights) {
|
|
|
1243
1258
|
...(blocksNodeMajor ? { blocksNodeMajor: true } : {})
|
|
1244
1259
|
};
|
|
1245
1260
|
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Collects package metadata and bounded filesystem heuristics to produce insights used for dependency analysis.
|
|
1263
|
+
*
|
|
1264
|
+
* Gathers package.json data (description, engines, scripts, bin, dependencies, links), computes file-based stats when the package directory is available, detects TypeScript typing availability (bundled or DefinitelyTyped), counts required peer dependencies, and derives execution and packaging signals for use in aggregation and risk heuristics.
|
|
1265
|
+
*
|
|
1266
|
+
* @param resolvePaths - Array of filesystem roots to use when resolving package metadata.
|
|
1267
|
+
* @param metaCache - Cache mapping package id (`name` or `name@version`) to previously loaded PackageMeta to avoid repeated resolution and parsing.
|
|
1268
|
+
* @param statCache - Cache mapping package directory paths to previously computed PackageStats to avoid repeated filesystem scans.
|
|
1269
|
+
* @returns A PackageInsights object containing deprecation, engine constraint, required peer count, optional fileCount, declared dependency maps, links, execution signals, optional packaging signals, and TypeScript types information.
|
|
1270
|
+
*/
|
|
1246
1271
|
async function gatherPackageInsights(name, version, resolvePaths, metaCache, statCache) {
|
|
1247
1272
|
var _a;
|
|
1248
1273
|
const meta = await loadPackageMeta(name, resolvePaths, metaCache, version);
|
|
@@ -1277,7 +1302,8 @@ async function gatherPackageInsights(name, version, resolvePaths, metaCache, sta
|
|
|
1277
1302
|
const hasDefinitelyTyped = await hasDefinitelyTypedPackage(name, resolvePaths, metaCache);
|
|
1278
1303
|
const tsTypes = determineTypes(pkg, (stats === null || stats === void 0 ? void 0 : stats.hasDts) || false, hasDefinitelyTyped);
|
|
1279
1304
|
const links = extractPackageLinks(pkg);
|
|
1280
|
-
const execution = await deriveExecutionInfo(scripts, dir, stats);
|
|
1305
|
+
const execution = await deriveExecutionInfo(pkg, scripts, dir, stats);
|
|
1306
|
+
const packaging = derivePackagingInfo(pkg, stats);
|
|
1281
1307
|
return {
|
|
1282
1308
|
deprecated,
|
|
1283
1309
|
nodeEngine,
|
|
@@ -1288,6 +1314,7 @@ async function gatherPackageInsights(name, version, resolvePaths, metaCache, sta
|
|
|
1288
1314
|
declaredDependencies,
|
|
1289
1315
|
links,
|
|
1290
1316
|
execution,
|
|
1317
|
+
...(packaging ? { packaging } : {}),
|
|
1291
1318
|
tsTypes
|
|
1292
1319
|
};
|
|
1293
1320
|
}
|
|
@@ -1321,6 +1348,12 @@ function countRequiredPeerDependencies(peerDependencies, peerDependenciesMeta) {
|
|
|
1321
1348
|
}
|
|
1322
1349
|
return count;
|
|
1323
1350
|
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Detects whether a package.json `bin` field indicates at least one executable target.
|
|
1353
|
+
*
|
|
1354
|
+
* @param binField - The `bin` value from a package.json (string or object)
|
|
1355
|
+
* @returns `true` if `binField` contains at least one non-empty string entry, `false` otherwise.
|
|
1356
|
+
*/
|
|
1324
1357
|
function hasPackageBin(binField) {
|
|
1325
1358
|
if (typeof binField === 'string')
|
|
1326
1359
|
return binField.trim().length > 0;
|
|
@@ -1328,6 +1361,58 @@ function hasPackageBin(binField) {
|
|
|
1328
1361
|
return false;
|
|
1329
1362
|
return Object.values(binField).some((value) => typeof value === 'string' && value.trim().length > 0);
|
|
1330
1363
|
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Normalize a package's `bundledDependencies` / `bundleDependencies` field into a canonical list.
|
|
1366
|
+
*
|
|
1367
|
+
* @param pkg - The package.json-like object to read bundling metadata from
|
|
1368
|
+
* @returns A sorted list of bundled package names. Returns `['*']` when bundling is declared as `true`, or an empty array when no bundled dependencies are specified.
|
|
1369
|
+
*/
|
|
1370
|
+
function normalizeBundledDependencies(pkg) {
|
|
1371
|
+
if ((pkg === null || pkg === void 0 ? void 0 : pkg.bundledDependencies) === true || (pkg === null || pkg === void 0 ? void 0 : pkg.bundleDependencies) === true) {
|
|
1372
|
+
return ['*'];
|
|
1373
|
+
}
|
|
1374
|
+
const raw = Array.isArray(pkg === null || pkg === void 0 ? void 0 : pkg.bundledDependencies)
|
|
1375
|
+
? pkg.bundledDependencies
|
|
1376
|
+
: Array.isArray(pkg === null || pkg === void 0 ? void 0 : pkg.bundleDependencies)
|
|
1377
|
+
? pkg.bundleDependencies
|
|
1378
|
+
: [];
|
|
1379
|
+
const entries = raw
|
|
1380
|
+
.map((entry) => typeof entry === 'string' ? entry.trim() : '')
|
|
1381
|
+
.filter((entry) => entry.length > 0);
|
|
1382
|
+
return Array.from(new Set(entries)).sort();
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Derives packaging-related signals from a package manifest and optional filesystem stats.
|
|
1386
|
+
*
|
|
1387
|
+
* @param pkg - The package.json object for the package being analyzed.
|
|
1388
|
+
* @param stats - Optional file-statistics for the package directory (e.g., presence of shrinkwrap).
|
|
1389
|
+
* @returns Packaging information containing `signals` (one or more of `bundled-dependencies` and `embedded-shrinkwrap`) and, when present, a `bundledDependencies` list; returns `undefined` when no packaging signals are detected.
|
|
1390
|
+
*/
|
|
1391
|
+
function derivePackagingInfo(pkg, stats) {
|
|
1392
|
+
const signals = new Set();
|
|
1393
|
+
const bundledDependencies = normalizeBundledDependencies(pkg);
|
|
1394
|
+
if (bundledDependencies.length > 0)
|
|
1395
|
+
signals.add('bundled-dependencies');
|
|
1396
|
+
if (stats === null || stats === void 0 ? void 0 : stats.hasShrinkwrap)
|
|
1397
|
+
signals.add('embedded-shrinkwrap');
|
|
1398
|
+
const signalList = ['bundled-dependencies', 'embedded-shrinkwrap']
|
|
1399
|
+
.filter((signal) => signals.has(signal));
|
|
1400
|
+
if (signalList.length === 0)
|
|
1401
|
+
return undefined;
|
|
1402
|
+
return {
|
|
1403
|
+
signals: signalList,
|
|
1404
|
+
...(bundledDependencies.length > 0 ? { bundledDependencies } : {})
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Load and cache a package's package.json and its directory metadata.
|
|
1409
|
+
*
|
|
1410
|
+
* @param name - Package name to resolve
|
|
1411
|
+
* @param resolvePaths - Ordered list of base paths to use when resolving the package.json
|
|
1412
|
+
* @param cache - Map used to store and reuse previously loaded PackageMeta entries; keyed by `name` or `name@version`
|
|
1413
|
+
* @param version - Optional version hint used when resolving a specific package variant
|
|
1414
|
+
* @returns The `PackageMeta` containing the parsed `package.json` (`pkg`) and its directory (`dir`), or `undefined` if the package.json could not be resolved or read
|
|
1415
|
+
*/
|
|
1331
1416
|
async function loadPackageMeta(name, resolvePaths, cache, version) {
|
|
1332
1417
|
const cacheKey = version ? `${name}@${version}` : name;
|
|
1333
1418
|
if (cache.has(cacheKey))
|
|
@@ -1366,12 +1451,33 @@ async function hasDefinitelyTypedPackage(name, resolvePaths, cache) {
|
|
|
1366
1451
|
const meta = await loadPackageMeta(typesName, resolvePaths, cache);
|
|
1367
1452
|
return Boolean(meta);
|
|
1368
1453
|
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Collects filesystem-derived statistics for a package directory.
|
|
1456
|
+
*
|
|
1457
|
+
* Scans the package directory (best-effort) to detect whether the package
|
|
1458
|
+
* contains TypeScript declaration files, native binary artifacts, a
|
|
1459
|
+
* binding.gyp file, or an npm shrinkwrap file, and counts files. Nested
|
|
1460
|
+
* dependency stores (node_modules) and .git directories are ignored; I/O
|
|
1461
|
+
* errors are swallowed and the function returns whatever information could
|
|
1462
|
+
* be gathered.
|
|
1463
|
+
*
|
|
1464
|
+
* @param dir - Filesystem path to the package directory to inspect.
|
|
1465
|
+
* @param cache - Memoization map keyed by directory path; cached results are
|
|
1466
|
+
* returned when available and new results are stored here.
|
|
1467
|
+
* @returns An object with:
|
|
1468
|
+
* - `hasDts`: `true` when any `.d.ts` files were found.
|
|
1469
|
+
* - `hasNativeBinary`: `true` when any `.node` binaries were found.
|
|
1470
|
+
* - `hasBindingGyp`: `true` when a `binding.gyp` file was found.
|
|
1471
|
+
* - `hasShrinkwrap`: `true` when an `npm-shrinkwrap.json` file was found.
|
|
1472
|
+
* - `fileCount`: total number of regular files encountered under the directory.
|
|
1473
|
+
*/
|
|
1369
1474
|
async function calculatePackageStats(dir, cache) {
|
|
1370
1475
|
if (cache.has(dir))
|
|
1371
1476
|
return cache.get(dir);
|
|
1372
1477
|
let hasDts = false;
|
|
1373
1478
|
let hasNativeBinary = false;
|
|
1374
1479
|
let hasBindingGyp = false;
|
|
1480
|
+
let hasShrinkwrap = false;
|
|
1375
1481
|
let fileCount = 0;
|
|
1376
1482
|
async function walk(current) {
|
|
1377
1483
|
const entries = await promises_1.default.readdir(current, { withFileTypes: true });
|
|
@@ -1393,6 +1499,8 @@ async function calculatePackageStats(dir, cache) {
|
|
|
1393
1499
|
hasNativeBinary = true;
|
|
1394
1500
|
if (entry.name === 'binding.gyp')
|
|
1395
1501
|
hasBindingGyp = true;
|
|
1502
|
+
if (entry.name === 'npm-shrinkwrap.json')
|
|
1503
|
+
hasShrinkwrap = true;
|
|
1396
1504
|
}
|
|
1397
1505
|
}
|
|
1398
1506
|
}
|
|
@@ -1402,7 +1510,7 @@ async function calculatePackageStats(dir, cache) {
|
|
|
1402
1510
|
catch (err) {
|
|
1403
1511
|
// best-effort; ignore inaccessible paths
|
|
1404
1512
|
}
|
|
1405
|
-
const result = { hasDts, hasNativeBinary, hasBindingGyp, fileCount };
|
|
1513
|
+
const result = { hasDts, hasNativeBinary, hasBindingGyp, hasShrinkwrap, fileCount };
|
|
1406
1514
|
cache.set(dir, result);
|
|
1407
1515
|
return result;
|
|
1408
1516
|
}
|
|
@@ -1493,7 +1601,16 @@ const EXECUTION_SIGNAL_ORDER = [
|
|
|
1493
1601
|
'uses-ssh'
|
|
1494
1602
|
];
|
|
1495
1603
|
const INSTALL_SCRIPT_MAX_BYTES = 200000;
|
|
1604
|
+
const LOCAL_SIGNAL_MAX_FILES_PER_PACKAGE = 24;
|
|
1605
|
+
const LOCAL_SIGNAL_MAX_BYTES_PER_FILE = 120000;
|
|
1606
|
+
const LOCAL_SIGNAL_MAX_PACKAGE_FILES = 2000;
|
|
1496
1607
|
const COMPLEXITY_THRESHOLD = 12;
|
|
1608
|
+
/**
|
|
1609
|
+
* Collects non-empty lifecycle hook commands from a package `scripts` object.
|
|
1610
|
+
*
|
|
1611
|
+
* @param scripts - The `scripts` section from a package.json (map of script names to values).
|
|
1612
|
+
* @returns An object mapping each lifecycle hook found in `scripts` to its trimmed command; only hooks defined in `LIFECYCLE_HOOKS` and with non-empty string values are included.
|
|
1613
|
+
*/
|
|
1497
1614
|
function collectLifecycleScripts(scripts) {
|
|
1498
1615
|
const lifecycle = {};
|
|
1499
1616
|
for (const hook of LIFECYCLE_HOOKS) {
|
|
@@ -1566,6 +1683,12 @@ function extractNodeScriptPath(command) {
|
|
|
1566
1683
|
}
|
|
1567
1684
|
return undefined;
|
|
1568
1685
|
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Finds the first Node script file path referenced by lifecycle hook commands, checking hooks in predefined order.
|
|
1688
|
+
*
|
|
1689
|
+
* @param lifecycleScripts - Map of lifecycle hook names to their command strings; only hooks with non-empty commands are inspected.
|
|
1690
|
+
* @returns The referenced script path (as found in a `node <script>` invocation) when present, `undefined` otherwise.
|
|
1691
|
+
*/
|
|
1569
1692
|
function findReferencedInstallScript(lifecycleScripts) {
|
|
1570
1693
|
for (const hook of LIFECYCLE_HOOKS) {
|
|
1571
1694
|
const command = lifecycleScripts[hook];
|
|
@@ -1577,6 +1700,80 @@ function findReferencedInstallScript(lifecycleScripts) {
|
|
|
1577
1700
|
}
|
|
1578
1701
|
return undefined;
|
|
1579
1702
|
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Normalize an input into a package-relative path by trimming whitespace and removing a leading `./` or `.\`.
|
|
1705
|
+
*
|
|
1706
|
+
* @param value - The input value to normalize (expected to be a path string)
|
|
1707
|
+
* @returns The cleaned package-relative path, or `undefined` if the input is not a string, is empty after trimming, or appears to be an absolute/URL (contains `://`)
|
|
1708
|
+
*/
|
|
1709
|
+
function normalizePackageRelativePath(value) {
|
|
1710
|
+
if (typeof value !== 'string')
|
|
1711
|
+
return undefined;
|
|
1712
|
+
const cleaned = value.trim().replace(/^[.][/\\]/, '');
|
|
1713
|
+
if (!cleaned || cleaned.includes('://'))
|
|
1714
|
+
return undefined;
|
|
1715
|
+
return cleaned;
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Collects normalized executable entry targets from a package `bin` field.
|
|
1719
|
+
*
|
|
1720
|
+
* @param binField - The package `bin` field; may be a string or an object mapping executable names to paths.
|
|
1721
|
+
* @returns An array of normalized package-relative executable target paths, deduplicated and sorted.
|
|
1722
|
+
*/
|
|
1723
|
+
function packageBinTargets(binField) {
|
|
1724
|
+
const targets = new Set();
|
|
1725
|
+
const add = (value) => {
|
|
1726
|
+
const normalized = normalizePackageRelativePath(value);
|
|
1727
|
+
if (normalized)
|
|
1728
|
+
targets.add(normalized);
|
|
1729
|
+
};
|
|
1730
|
+
if (typeof binField === 'string')
|
|
1731
|
+
add(binField);
|
|
1732
|
+
else if (binField && typeof binField === 'object') {
|
|
1733
|
+
for (const value of Object.values(binField))
|
|
1734
|
+
add(value);
|
|
1735
|
+
}
|
|
1736
|
+
return Array.from(targets).sort();
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Collects candidate entry file paths from a package manifest.
|
|
1740
|
+
*
|
|
1741
|
+
* Supports `main`, `module`, and `exports` fields, flattening arrays and objects and normalizing package-relative paths.
|
|
1742
|
+
*
|
|
1743
|
+
* @param pkg - The package.json object to read entry targets from
|
|
1744
|
+
* @returns An array of normalized package-relative entry paths, sorted; contains `index.js` when no entry fields are present
|
|
1745
|
+
*/
|
|
1746
|
+
function packageEntryTargets(pkg) {
|
|
1747
|
+
const targets = new Set();
|
|
1748
|
+
const add = (value) => {
|
|
1749
|
+
if (typeof value === 'string') {
|
|
1750
|
+
const normalized = normalizePackageRelativePath(value);
|
|
1751
|
+
if (normalized)
|
|
1752
|
+
targets.add(normalized);
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
if (Array.isArray(value)) {
|
|
1756
|
+
value.forEach(add);
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
if (value && typeof value === 'object') {
|
|
1760
|
+
Object.values(value).forEach(add);
|
|
1761
|
+
}
|
|
1762
|
+
};
|
|
1763
|
+
add(pkg === null || pkg === void 0 ? void 0 : pkg.main);
|
|
1764
|
+
add(pkg === null || pkg === void 0 ? void 0 : pkg.module);
|
|
1765
|
+
add(pkg === null || pkg === void 0 ? void 0 : pkg.exports);
|
|
1766
|
+
if (targets.size === 0)
|
|
1767
|
+
targets.add('index.js');
|
|
1768
|
+
return Array.from(targets).sort();
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Reads an install script file from a package directory if it is a regular file within size limits.
|
|
1772
|
+
*
|
|
1773
|
+
* @param scriptPath - The path to the script file as referenced in package scripts (resolved relative to `packageDir`)
|
|
1774
|
+
* @param packageDir - The package root directory used to resolve and bound `scriptPath`
|
|
1775
|
+
* @returns The file contents as UTF-8 text when the file exists inside `packageDir`, is a regular file, and its size is at most `INSTALL_SCRIPT_MAX_BYTES`; `undefined` otherwise.
|
|
1776
|
+
*/
|
|
1580
1777
|
async function readInstallScriptFile(scriptPath, packageDir) {
|
|
1581
1778
|
const resolvedDir = path_1.default.resolve(packageDir);
|
|
1582
1779
|
const resolvedPath = path_1.default.resolve(resolvedDir, scriptPath);
|
|
@@ -1594,14 +1791,182 @@ async function readInstallScriptFile(scriptPath, packageDir) {
|
|
|
1594
1791
|
return undefined;
|
|
1595
1792
|
}
|
|
1596
1793
|
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Checks whether a file path refers to an inspectable source file used for static analysis.
|
|
1796
|
+
*
|
|
1797
|
+
* @param filePath - The file path to test (relative or absolute)
|
|
1798
|
+
* @returns `true` if the path ends with a JS/TS-related extension (`.js`, `.cjs`, `.mjs`, `.jsx`, `.ts`, `.tsx`) or has no extension, `false` otherwise.
|
|
1799
|
+
*/
|
|
1800
|
+
function isInspectableSourcePath(filePath) {
|
|
1801
|
+
return /\.(?:js|cjs|mjs|jsx|ts|tsx)$/i.test(filePath) || path_1.default.extname(filePath) === '';
|
|
1802
|
+
}
|
|
1803
|
+
/**
|
|
1804
|
+
* Detects whether a JavaScript-like file is likely minified or otherwise obfuscated.
|
|
1805
|
+
*
|
|
1806
|
+
* Uses filename and simple content heuristics to identify minified files.
|
|
1807
|
+
*
|
|
1808
|
+
* @param fileName - The file name or path (used to detect `.min.js/.min.cjs/.min.mjs` suffixes)
|
|
1809
|
+
* @param text - The file contents to inspect
|
|
1810
|
+
* @returns `true` if the file appears minified or obfuscated, `false` otherwise.
|
|
1811
|
+
*/
|
|
1812
|
+
function looksMinified(fileName, text) {
|
|
1813
|
+
if (/\.min\.(?:js|cjs|mjs)$/i.test(fileName))
|
|
1814
|
+
return true;
|
|
1815
|
+
const lines = text.split(/\r?\n/);
|
|
1816
|
+
if (lines.length <= 3 && text.length > 20000)
|
|
1817
|
+
return true;
|
|
1818
|
+
return lines.some((line) => line.length > 10000);
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Determine whether a string appears to be readable text (not binary or overwhelmingly control-character data).
|
|
1822
|
+
*
|
|
1823
|
+
* Empty strings are considered text; presence of a NUL character marks the input as non-text. The function treats
|
|
1824
|
+
* the input as text when the proportion of suspicious control or replacement characters is less than 1%.
|
|
1825
|
+
*
|
|
1826
|
+
* @param text - The string to evaluate
|
|
1827
|
+
* @returns `true` if the input appears to be readable text, `false` otherwise.
|
|
1828
|
+
*/
|
|
1829
|
+
function looksTextLike(text) {
|
|
1830
|
+
if (text.includes('\0'))
|
|
1831
|
+
return false;
|
|
1832
|
+
if (text.length === 0)
|
|
1833
|
+
return true;
|
|
1834
|
+
const suspicious = (text.match(/[\uFFFD\x00-\x08\x0E-\x1F]/g) || []).length;
|
|
1835
|
+
return suspicious / text.length < 0.01;
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Detects whether a text blob appears to be JavaScript or Node.js source.
|
|
1839
|
+
*
|
|
1840
|
+
* @param text - The text to inspect for JavaScript/Node indicators
|
|
1841
|
+
* @returns `true` if the text contains common JavaScript or Node.js source markers (shebang with `node`, `require(`, `import`, `module.exports`, or `process.`), `false` otherwise
|
|
1842
|
+
*/
|
|
1843
|
+
function looksJavaScriptLike(text) {
|
|
1844
|
+
return (/^#!.*\bnode\b/.test(text) ||
|
|
1845
|
+
/\brequire\s*\(/.test(text) ||
|
|
1846
|
+
/\bimport\s+/.test(text) ||
|
|
1847
|
+
/\bmodule\.exports\b/.test(text) ||
|
|
1848
|
+
/\bprocess\./.test(text));
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Read and return the text of a package-relative source file when it is safe and useful to inspect.
|
|
1852
|
+
*
|
|
1853
|
+
* Attempts to resolve and read `filePath` inside `packageDir` and returns the file contents only if the file:
|
|
1854
|
+
* - resides within `packageDir`,
|
|
1855
|
+
* - matches the allowed inspectable source path patterns,
|
|
1856
|
+
* - is a regular file whose size does not exceed `maxBytes`,
|
|
1857
|
+
* - appears to be text (and, for extensionless files, looks like JavaScript),
|
|
1858
|
+
* - does not appear minified or otherwise obfuscated.
|
|
1859
|
+
*
|
|
1860
|
+
* @param filePath - Path to the candidate file relative to the package directory
|
|
1861
|
+
* @param packageDir - Absolute path of the package root to constrain reads
|
|
1862
|
+
* @param maxBytes - Maximum number of bytes to read from the file (per-file size limit)
|
|
1863
|
+
* @returns The file content when it is inspectable under the above constraints, `undefined` otherwise
|
|
1864
|
+
*/
|
|
1865
|
+
async function readInspectablePackageFile(filePath, packageDir, maxBytes = LOCAL_SIGNAL_MAX_BYTES_PER_FILE) {
|
|
1866
|
+
const resolvedDir = path_1.default.resolve(packageDir);
|
|
1867
|
+
const resolvedPath = path_1.default.resolve(resolvedDir, filePath);
|
|
1868
|
+
if (!resolvedPath.startsWith(resolvedDir + path_1.default.sep))
|
|
1869
|
+
return undefined;
|
|
1870
|
+
if (!isInspectableSourcePath(resolvedPath))
|
|
1871
|
+
return undefined;
|
|
1872
|
+
try {
|
|
1873
|
+
const stat = await promises_1.default.stat(resolvedPath);
|
|
1874
|
+
if (!stat.isFile())
|
|
1875
|
+
return undefined;
|
|
1876
|
+
if (stat.size > maxBytes)
|
|
1877
|
+
return undefined;
|
|
1878
|
+
const text = await promises_1.default.readFile(resolvedPath, 'utf8');
|
|
1879
|
+
if (!looksTextLike(text))
|
|
1880
|
+
return undefined;
|
|
1881
|
+
if (path_1.default.extname(resolvedPath) === '' && !looksJavaScriptLike(text))
|
|
1882
|
+
return undefined;
|
|
1883
|
+
if (looksMinified(resolvedPath, text))
|
|
1884
|
+
return undefined;
|
|
1885
|
+
return text;
|
|
1886
|
+
}
|
|
1887
|
+
catch {
|
|
1888
|
+
return undefined;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
/**
|
|
1892
|
+
* Collects a bounded list of inspectable source file paths inside a package directory.
|
|
1893
|
+
*
|
|
1894
|
+
* Traverses the package tree (skipping symlinks and common non-source directories) and returns
|
|
1895
|
+
* a sorted array of package-relative paths for files considered inspectable.
|
|
1896
|
+
*
|
|
1897
|
+
* @param packageDir - Absolute path to the package directory to scan
|
|
1898
|
+
* @param maxFiles - Maximum number of inspectable file paths to return
|
|
1899
|
+
* @param maxPackageFiles - Maximum number of files to examine within the package (counts all files visited)
|
|
1900
|
+
* @returns A sorted array of relative file paths (relative to `packageDir`) for inspectable source files
|
|
1901
|
+
*/
|
|
1902
|
+
async function collectBoundedInspectableFiles(packageDir, maxFiles, maxPackageFiles) {
|
|
1903
|
+
const out = [];
|
|
1904
|
+
let seen = 0;
|
|
1905
|
+
async function walk(current) {
|
|
1906
|
+
if (out.length >= maxFiles || seen >= maxPackageFiles)
|
|
1907
|
+
return;
|
|
1908
|
+
const entries = await promises_1.default.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
1909
|
+
entries.sort((a, b) => a.name.localeCompare(b.name, 'en', { numeric: true, sensitivity: 'base' }));
|
|
1910
|
+
for (const entry of entries) {
|
|
1911
|
+
if (out.length >= maxFiles || seen >= maxPackageFiles)
|
|
1912
|
+
return;
|
|
1913
|
+
const full = path_1.default.join(current, entry.name);
|
|
1914
|
+
if (entry.isSymbolicLink())
|
|
1915
|
+
continue;
|
|
1916
|
+
if (entry.isDirectory()) {
|
|
1917
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'test' || entry.name === 'tests' || entry.name === 'docs')
|
|
1918
|
+
continue;
|
|
1919
|
+
await walk(full);
|
|
1920
|
+
}
|
|
1921
|
+
else if (entry.isFile()) {
|
|
1922
|
+
seen += 1;
|
|
1923
|
+
if (isInspectableSourcePath(full)) {
|
|
1924
|
+
out.push(path_1.default.relative(packageDir, full));
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
await walk(packageDir);
|
|
1930
|
+
return out.sort();
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Identify execution-related static signals present in a block of source or script text.
|
|
1934
|
+
*
|
|
1935
|
+
* @param text - The file or lifecycle script text to analyze for execution signals
|
|
1936
|
+
* @returns An ordered array of distinct execution signals detected in `text`, prioritized according to the canonical execution-signal ordering
|
|
1937
|
+
*/
|
|
1938
|
+
function detectLocalExecutionSignals(text) {
|
|
1939
|
+
const signals = new Set();
|
|
1940
|
+
detectFileSignals(text, signals);
|
|
1941
|
+
return EXECUTION_SIGNAL_ORDER.filter((signal) => signals.has(signal));
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Checks whether the given text matches any regular expression in the provided list.
|
|
1945
|
+
*
|
|
1946
|
+
* @param text - The string to test against the patterns
|
|
1947
|
+
* @param patterns - Array of `RegExp` objects to test
|
|
1948
|
+
* @returns `true` if at least one pattern matches `text`, `false` otherwise.
|
|
1949
|
+
*/
|
|
1597
1950
|
function textHasAny(text, patterns) {
|
|
1598
1951
|
return patterns.some((pattern) => pattern.test(text));
|
|
1599
1952
|
}
|
|
1953
|
+
/**
|
|
1954
|
+
* Detects install-time script characteristics from a text blob and adds matching execution signals to the provided set.
|
|
1955
|
+
*
|
|
1956
|
+
* Examines the input text with heuristics for common patterns and adds any of the following `ExecutionSignal` values to `signals` when matched:
|
|
1957
|
+
* - `network-access` — patterns that fetch remote resources (curl, wget, http(s), fetch, axios, net.connect, dns, etc.).
|
|
1958
|
+
* - `reads-env` — access to environment variables or printenv-style usage.
|
|
1959
|
+
* - `reads-home` — references to user home paths or APIs returning the home directory.
|
|
1960
|
+
* - `uses-ssh` — references to SSH-related files, agents, or SSH/git configuration.
|
|
1961
|
+
*
|
|
1962
|
+
* @param text - The script or file text to statically inspect for indicative patterns.
|
|
1963
|
+
* @param signals - A mutable Set that will receive any detected execution signals.
|
|
1964
|
+
*/
|
|
1600
1965
|
function detectScriptSignals(text, signals) {
|
|
1601
1966
|
// Signals are derived from static text inspection only: no code execution and no import walking.
|
|
1602
1967
|
// They are NOT malware detection; they merely highlight review-worthy install-time behavior.
|
|
1603
1968
|
// "network-access" surfaces install scripts that fetch remote resources (review for expected downloads; does NOT imply exfiltration).
|
|
1604
|
-
if (textHasAny(text, [/\bcurl\b/i, /\bwget\b/i, /https?:\/\//i, /\bfetch\s*\(/i, /\baxios\b/i, /node-fetch/i])) {
|
|
1969
|
+
if (textHasAny(text, [/\bcurl\b/i, /\bwget\b/i, /https?:\/\//i, /\bfetch\s*\(/i, /\baxios\b/i, /node-fetch/i, /\bhttps?\.request\s*\(/, /\bnet\.connect\s*\(/, /\bdns\./])) {
|
|
1605
1970
|
signals.add('network-access');
|
|
1606
1971
|
}
|
|
1607
1972
|
// "reads-env" highlights environment access (does NOT imply exfiltration).
|
|
@@ -1609,11 +1974,11 @@ function detectScriptSignals(text, signals) {
|
|
|
1609
1974
|
signals.add('reads-env');
|
|
1610
1975
|
}
|
|
1611
1976
|
// "reads-home" highlights access to user home paths (does NOT imply credential theft).
|
|
1612
|
-
if (textHasAny(text, [/\$HOME\b/, /process\.env\.HOME\b/, /os\.homedir\s*\(/,
|
|
1977
|
+
if (textHasAny(text, [/\$HOME\b/, /process\.env\.HOME\b/, /\bUSERPROFILE\b/, /os\.homedir\s*\(/, /~\//, /\/Users\//, /\/home\//])) {
|
|
1613
1978
|
signals.add('reads-home');
|
|
1614
1979
|
}
|
|
1615
1980
|
// "uses-ssh" flags access to SSH-related paths (does NOT imply key exfiltration).
|
|
1616
|
-
if (textHasAny(text, [/\.ssh\b/i, /id_rsa\b/i, /known_hosts\b/i, /\.npmrc\b/i])) {
|
|
1981
|
+
if (textHasAny(text, [/\.ssh\b/i, /id_rsa\b/i, /known_hosts\b/i, /ssh-key/i, /ssh-agent/i, /GIT_SSH_COMMAND\b/, /\.npmrc\b/i])) {
|
|
1617
1982
|
signals.add('uses-ssh');
|
|
1618
1983
|
}
|
|
1619
1984
|
}
|
|
@@ -1634,6 +1999,14 @@ function isObfuscated(text) {
|
|
|
1634
1999
|
}
|
|
1635
2000
|
return /[A-Za-z0-9+/]{800,}={0,2}/.test(text);
|
|
1636
2001
|
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Detects static execution- and obfuscation-related signals from a JavaScript file's text and adds them to `signals`.
|
|
2004
|
+
*
|
|
2005
|
+
* Scans the provided source text for review cues such as script-level indicators, dynamic code execution APIs, child-process usage, explicit encoding patterns, and signs of obfuscation/minification. Matches are added to the supplied `signals` set; the function does not return a value and does not imply malicious intent by itself.
|
|
2006
|
+
*
|
|
2007
|
+
* @param text - The source text of a JavaScript file to analyze.
|
|
2008
|
+
* @param signals - A mutable set that will be populated with discovered `ExecutionSignal` values.
|
|
2009
|
+
*/
|
|
1637
2010
|
function detectFileSignals(text, signals) {
|
|
1638
2011
|
// Signals from a single directly-referenced JS file (no execution, no imports, no deep scanning).
|
|
1639
2012
|
// These are review cues only and do NOT imply malicious intent.
|
|
@@ -1643,7 +2016,7 @@ function detectFileSignals(text, signals) {
|
|
|
1643
2016
|
signals.add('dynamic-exec');
|
|
1644
2017
|
}
|
|
1645
2018
|
// "child-process" flags process spawning (does NOT imply abuse).
|
|
1646
|
-
if (textHasAny(text, [/\bchild_process
|
|
2019
|
+
if (textHasAny(text, [/\bchild_process\b/, /\bexec\s*\(/, /\bspawn\s*\(/, /\bexecFile\s*\(/, /\bexecSync\s*\(/, /\bspawnSync\s*\(/])) {
|
|
1647
2020
|
signals.add('child-process');
|
|
1648
2021
|
}
|
|
1649
2022
|
// "encoding" flags explicit encode/decode flows (does NOT imply obfuscation intent).
|
|
@@ -1655,46 +2028,116 @@ function detectFileSignals(text, signals) {
|
|
|
1655
2028
|
signals.add('obfuscated');
|
|
1656
2029
|
}
|
|
1657
2030
|
}
|
|
2031
|
+
/**
|
|
2032
|
+
* Classifies execution-related risk level for a package based on lifecycle scripts, detected signals, and script complexity.
|
|
2033
|
+
*
|
|
2034
|
+
* @param hasScripts - Whether the package defines any lifecycle scripts
|
|
2035
|
+
* @param hasSignals - Whether static analysis detected execution-related signals (network, child-process, dynamic execution, etc.)
|
|
2036
|
+
* @param highComplexity - Whether the package's lifecycle scripts exceed the complexity threshold
|
|
2037
|
+
* @param hooks - Lifecycle hooks present (note: `install` or `postinstall` count as install-time hooks)
|
|
2038
|
+
* @returns `'red'` when scripts are present and either execution signals exist or scripts are highly complex on an install-time hook; `'amber'` otherwise.
|
|
2039
|
+
*/
|
|
1658
2040
|
function determineExecutionRisk(hasScripts, hasSignals, highComplexity, hooks) {
|
|
1659
2041
|
const hasInstallHook = hooks.includes('install') || hooks.includes('postinstall');
|
|
1660
2042
|
if (hasScripts && (hasSignals || (highComplexity && hasInstallHook)))
|
|
1661
2043
|
return 'red';
|
|
1662
2044
|
return 'amber';
|
|
1663
2045
|
}
|
|
1664
|
-
|
|
2046
|
+
/**
|
|
2047
|
+
* Collects execution-related static signals from a package's likely entry and inspectable source files using bounded, best-effort inspection.
|
|
2048
|
+
*
|
|
2049
|
+
* This inspects candidate entry points (e.g., `bin`, `main`, `module`, `exports`) plus a bounded set of other inspectable files, applies static detectors, and returns the detected execution signals in the canonical ordering.
|
|
2050
|
+
*
|
|
2051
|
+
* @param pkg - The package.json object for the package being inspected.
|
|
2052
|
+
* @param packageDir - The package filesystem directory to read files from; when `undefined`, the function returns an empty array.
|
|
2053
|
+
* @param options.maxFiles - Maximum number of files to inspect (default: LOCAL_SIGNAL_MAX_FILES_PER_PACKAGE).
|
|
2054
|
+
* @param options.maxBytesPerFile - Maximum bytes to read per file (default: LOCAL_SIGNAL_MAX_BYTES_PER_FILE).
|
|
2055
|
+
* @param options.maxPackageFiles - Maximum number of candidate package files to enumerate before selecting up to `maxFiles` for inspection (default: LOCAL_SIGNAL_MAX_PACKAGE_FILES).
|
|
2056
|
+
* @returns An array of detected `ExecutionSignal` values, ordered according to the canonical `EXECUTION_SIGNAL_ORDER`. Empty if no signals are found.
|
|
2057
|
+
*/
|
|
2058
|
+
async function collectPackageExecutionSignals(pkg, packageDir, options = {}) {
|
|
2059
|
+
var _a, _b, _c;
|
|
2060
|
+
if (!packageDir)
|
|
2061
|
+
return [];
|
|
2062
|
+
const maxFiles = (_a = options.maxFiles) !== null && _a !== void 0 ? _a : LOCAL_SIGNAL_MAX_FILES_PER_PACKAGE;
|
|
2063
|
+
const maxBytesPerFile = (_b = options.maxBytesPerFile) !== null && _b !== void 0 ? _b : LOCAL_SIGNAL_MAX_BYTES_PER_FILE;
|
|
2064
|
+
const maxPackageFiles = (_c = options.maxPackageFiles) !== null && _c !== void 0 ? _c : LOCAL_SIGNAL_MAX_PACKAGE_FILES;
|
|
2065
|
+
const candidateFiles = new Set();
|
|
2066
|
+
for (const file of [...packageBinTargets(pkg === null || pkg === void 0 ? void 0 : pkg.bin), ...packageEntryTargets(pkg)]) {
|
|
2067
|
+
candidateFiles.add(file);
|
|
2068
|
+
}
|
|
2069
|
+
for (const file of await collectBoundedInspectableFiles(packageDir, maxFiles, maxPackageFiles)) {
|
|
2070
|
+
candidateFiles.add(file);
|
|
2071
|
+
if (candidateFiles.size >= maxFiles)
|
|
2072
|
+
break;
|
|
2073
|
+
}
|
|
2074
|
+
const signals = new Set();
|
|
2075
|
+
let inspected = 0;
|
|
2076
|
+
for (const file of candidateFiles) {
|
|
2077
|
+
if (inspected >= maxFiles)
|
|
2078
|
+
break;
|
|
2079
|
+
const text = await readInspectablePackageFile(file, packageDir, maxBytesPerFile);
|
|
2080
|
+
if (!text)
|
|
2081
|
+
continue;
|
|
2082
|
+
inspected += 1;
|
|
2083
|
+
detectFileSignals(text, signals);
|
|
2084
|
+
}
|
|
2085
|
+
return EXECUTION_SIGNAL_ORDER.filter((signal) => signals.has(signal));
|
|
2086
|
+
}
|
|
2087
|
+
/**
|
|
2088
|
+
* Derives install-time execution signals, script metadata, and a consolidated execution risk for a package.
|
|
2089
|
+
*
|
|
2090
|
+
* Analyzes lifecycle hooks, referenced install scripts, native-build indicators, and bounded package file inspection to produce ordered execution signals, an execution complexity score when applicable, and a risk classification. Returns undefined when no meaningful execution signals, scripts, or native indicators are present.
|
|
2091
|
+
*
|
|
2092
|
+
* @param pkg - The package.json object for the dependency; used to locate entry points and candidate files for inspection.
|
|
2093
|
+
* @param scripts - The package's lifecycle `scripts` map (from package.json).
|
|
2094
|
+
* @param packageDir - Absolute path to the package directory used for reading referenced install scripts and package files; when undefined, file-based inspection is skipped.
|
|
2095
|
+
* @param stats - Optional PackageStats containing file-based heuristics (e.g., `hasNativeBinary`, `hasBindingGyp`) used as native indicators.
|
|
2096
|
+
* @returns An execution record containing:
|
|
2097
|
+
* - `risk`: computed execution risk,
|
|
2098
|
+
* - optional `native`: `true` when native-build indicators are present,
|
|
2099
|
+
* - optional `signals`: ordered list of detected execution signals,
|
|
2100
|
+
* - optional `scripts`: metadata about lifecycle hooks including `hooks`, optional `complexity`, and optional script-only `signals`.
|
|
2101
|
+
*/
|
|
2102
|
+
async function deriveExecutionInfo(pkg, scripts, packageDir, stats) {
|
|
1665
2103
|
const lifecycleScripts = collectLifecycleScripts(scripts);
|
|
1666
2104
|
const hooks = LIFECYCLE_HOOKS.filter((hook) => Boolean(lifecycleScripts[hook]));
|
|
1667
2105
|
const hasScripts = hooks.length > 0;
|
|
1668
2106
|
const hasNative = Boolean((stats === null || stats === void 0 ? void 0 : stats.hasNativeBinary) || (stats === null || stats === void 0 ? void 0 : stats.hasBindingGyp) || scriptsContainNativeTooling(scripts));
|
|
1669
|
-
|
|
1670
|
-
return undefined;
|
|
1671
|
-
const signals = new Set();
|
|
2107
|
+
const scriptSignals = new Set();
|
|
1672
2108
|
const combinedScripts = hooks.map((hook) => lifecycleScripts[hook]).join('\n');
|
|
1673
2109
|
if (combinedScripts) {
|
|
1674
|
-
detectScriptSignals(combinedScripts,
|
|
2110
|
+
detectScriptSignals(combinedScripts, scriptSignals);
|
|
1675
2111
|
}
|
|
1676
2112
|
if (hasScripts && packageDir) {
|
|
1677
2113
|
const referencedScript = findReferencedInstallScript(lifecycleScripts);
|
|
1678
2114
|
if (referencedScript) {
|
|
1679
2115
|
const fileContent = await readInstallScriptFile(referencedScript, packageDir);
|
|
1680
2116
|
if (fileContent) {
|
|
1681
|
-
detectFileSignals(fileContent,
|
|
2117
|
+
detectFileSignals(fileContent, scriptSignals);
|
|
1682
2118
|
}
|
|
1683
2119
|
}
|
|
1684
2120
|
}
|
|
2121
|
+
const packageSignals = await collectPackageExecutionSignals(pkg, packageDir);
|
|
2122
|
+
const allSignals = new Set([...scriptSignals, ...packageSignals]);
|
|
1685
2123
|
const complexityScore = hasScripts ? scoreLifecycleScripts(lifecycleScripts) : 0;
|
|
1686
2124
|
const complexity = complexityScore >= COMPLEXITY_THRESHOLD ? complexityScore : undefined;
|
|
1687
|
-
const
|
|
2125
|
+
const scriptSignalList = EXECUTION_SIGNAL_ORDER.filter((signal) => scriptSignals.has(signal));
|
|
2126
|
+
const signalList = EXECUTION_SIGNAL_ORDER.filter((signal) => allSignals.has(signal));
|
|
2127
|
+
if (!hasScripts && !hasNative && signalList.length === 0)
|
|
2128
|
+
return undefined;
|
|
1688
2129
|
const scriptsInfo = {
|
|
1689
2130
|
hooks,
|
|
1690
2131
|
...(complexity !== undefined ? { complexity } : {}),
|
|
1691
|
-
...(
|
|
2132
|
+
...(scriptSignalList.length > 0 ? { signals: scriptSignalList } : {})
|
|
1692
2133
|
};
|
|
1693
2134
|
const risk = determineExecutionRisk(hasScripts, signalList.length > 0, complexity !== undefined, hooks);
|
|
1694
2135
|
const execution = { risk };
|
|
1695
2136
|
// Native is surface description only; not a behavioral signal.
|
|
1696
2137
|
if (hasNative)
|
|
1697
2138
|
execution.native = true;
|
|
2139
|
+
if (signalList.length > 0)
|
|
2140
|
+
execution.signals = signalList;
|
|
1698
2141
|
if (hasScripts)
|
|
1699
2142
|
execution.scripts = scriptsInfo;
|
|
1700
2143
|
return execution;
|