np-audit 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +6 -2
- package/src/commands/ci.js +3 -0
- package/src/commands/install.js +3 -0
- package/src/commands/scan.js +3 -1
- package/src/core/scanner.js +25 -10
- package/src/utils/output.js +3 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
Static security analysis for npm packages — detects obfuscated lifecycle scripts, known vulnerabilities, and malicious patterns **before** they run. Drop-in replacement for `npm install` and `npm ci`.
|
|
13
13
|
|
|
14
|
-
**Zero dependencies.** Pure Node.js built-ins only.
|
|
14
|
+
**Zero dependencies.** Pure Node.js built-ins only. **< 100 kB** on the wire.
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
npx np-audit scan express
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "np-audit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Static obfuscation detector for npm lifecycle scripts — supply chain attack prevention",
|
|
5
5
|
"bin": {
|
|
6
6
|
"npa": "bin/npa.js",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
|
-
"test": "node test/index.js"
|
|
16
|
+
"test": "node test/index.js",
|
|
17
|
+
"coverage": "npx c8@11.0.0 --reporter=lcov --reporter=text-summary node test/index.js"
|
|
17
18
|
},
|
|
18
19
|
"engines": {
|
|
19
20
|
"node": ">=18.0.0"
|
|
@@ -32,5 +33,8 @@
|
|
|
32
33
|
"repository": {
|
|
33
34
|
"type": "git",
|
|
34
35
|
"url": "git+https://github.com/KoblerS/np-audit.git"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"c8": "^11.0.0"
|
|
35
39
|
}
|
|
36
40
|
}
|
package/src/commands/ci.js
CHANGED
|
@@ -32,7 +32,9 @@ module.exports = {
|
|
|
32
32
|
async run({ flags, config, cwd }) {
|
|
33
33
|
const spinner = !flags.json && !config.silent ? output.createSpinner('Auditing packages...') : null;
|
|
34
34
|
if (spinner) spinner.start();
|
|
35
|
+
const t0 = Date.now();
|
|
35
36
|
const results = await scan({ cwd, config, noDev: flags.noDev, verbose: flags.verbose });
|
|
37
|
+
const elapsedMs = Date.now() - t0;
|
|
36
38
|
if (spinner) spinner.stop();
|
|
37
39
|
const hasIssues = results.some(r => r.verdict !== 'OK');
|
|
38
40
|
const silent = config.silent && !hasIssues;
|
|
@@ -43,6 +45,7 @@ module.exports = {
|
|
|
43
45
|
process.stdout.write(JSON.stringify(toJsonReport(results), null, 2) + '\n');
|
|
44
46
|
} else {
|
|
45
47
|
printResults(results, silent);
|
|
48
|
+
if (!silent) output.printSummary(results, elapsedMs);
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
const blocked = results.filter(r => r.verdict === 'BLOCK');
|
package/src/commands/install.js
CHANGED
|
@@ -35,6 +35,7 @@ module.exports = {
|
|
|
35
35
|
|
|
36
36
|
const spinner = !flags.json && !config.silent ? output.createSpinner('Auditing packages...') : null;
|
|
37
37
|
if (spinner) spinner.start();
|
|
38
|
+
const t0 = Date.now();
|
|
38
39
|
const results = await scan({
|
|
39
40
|
cwd,
|
|
40
41
|
config,
|
|
@@ -42,6 +43,7 @@ module.exports = {
|
|
|
42
43
|
verbose: flags.verbose,
|
|
43
44
|
packages: packages.length > 0 ? packages : null,
|
|
44
45
|
});
|
|
46
|
+
const elapsedMs = Date.now() - t0;
|
|
45
47
|
if (spinner) spinner.stop();
|
|
46
48
|
|
|
47
49
|
const hasIssues = results.some(r => r.verdict !== 'OK');
|
|
@@ -53,6 +55,7 @@ module.exports = {
|
|
|
53
55
|
process.stdout.write(JSON.stringify(toJsonReport(results), null, 2) + '\n');
|
|
54
56
|
} else {
|
|
55
57
|
printResults(results, silent);
|
|
58
|
+
if (!silent) output.printSummary(results, elapsedMs);
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
const blocked = results.filter(r => r.verdict === 'BLOCK');
|
package/src/commands/scan.js
CHANGED
|
@@ -33,7 +33,9 @@ module.exports = {
|
|
|
33
33
|
const packages = args.filter(a => !a.startsWith('-'));
|
|
34
34
|
const spinner = !flags.json && !config.silent ? output.createSpinner('Auditing packages...') : null;
|
|
35
35
|
if (spinner) spinner.start();
|
|
36
|
+
const t0 = Date.now();
|
|
36
37
|
const results = await scan({ cwd, config, noDev: flags.noDev, verbose: flags.verbose, packages: packages.length > 0 ? packages : null });
|
|
38
|
+
const elapsedMs = Date.now() - t0;
|
|
37
39
|
if (spinner) spinner.stop();
|
|
38
40
|
const hasIssues = results.some(r => r.verdict !== 'OK');
|
|
39
41
|
const silent = config.silent && !hasIssues;
|
|
@@ -47,7 +49,7 @@ module.exports = {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
printResults(results, silent);
|
|
50
|
-
if (!silent) output.printSummary(results);
|
|
52
|
+
if (!silent) output.printSummary(results, elapsedMs);
|
|
51
53
|
|
|
52
54
|
const hasBlock = results.some(r => r.verdict === 'BLOCK');
|
|
53
55
|
process.exit(hasBlock ? 1 : 0);
|
package/src/core/scanner.js
CHANGED
|
@@ -651,6 +651,17 @@ function extractSemver(range) {
|
|
|
651
651
|
return null;
|
|
652
652
|
}
|
|
653
653
|
|
|
654
|
+
/**
|
|
655
|
+
* Resolve an extracted (possibly partial) version to a full version using registry metadata.
|
|
656
|
+
* Falls back to dist-tags.latest when the partial version doesn't match any published version.
|
|
657
|
+
*/
|
|
658
|
+
function resolveVersion(extracted, meta) {
|
|
659
|
+
if (meta.versions && meta.versions[extracted]) return extracted;
|
|
660
|
+
const latest = meta['dist-tags'] && meta['dist-tags'].latest;
|
|
661
|
+
if (latest && meta.versions && meta.versions[latest]) return latest;
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
|
|
654
665
|
/**
|
|
655
666
|
* Resolve dependencies from package.json when no lockfile exists.
|
|
656
667
|
* @param {string} cwd
|
|
@@ -687,12 +698,13 @@ async function resolveFromPackageJson(cwd, config, noDev) {
|
|
|
687
698
|
try {
|
|
688
699
|
const encodedName = name.startsWith('@') ? `@${encodeURIComponent(name.slice(1))}` : encodeURIComponent(name);
|
|
689
700
|
const meta = await fetchJSON(`${config.registry}/${encodedName}`, { timeout: config.timeout });
|
|
690
|
-
const
|
|
691
|
-
if (!
|
|
701
|
+
const resolvedVersion = resolveVersion(version, meta);
|
|
702
|
+
if (!resolvedVersion) continue;
|
|
703
|
+
const versionData = meta.versions[resolvedVersion];
|
|
692
704
|
|
|
693
705
|
packages.push({
|
|
694
706
|
name,
|
|
695
|
-
version,
|
|
707
|
+
version: resolvedVersion,
|
|
696
708
|
resolved: versionData.dist && versionData.dist.tarball,
|
|
697
709
|
integrity: versionData.dist && versionData.dist.integrity || '',
|
|
698
710
|
hasInstallScript: !!(versionData.scripts &&
|
|
@@ -752,22 +764,25 @@ async function resolveSinglePackage(packageSpec, config) {
|
|
|
752
764
|
for (const [depName] of queue) seen.add(depName);
|
|
753
765
|
|
|
754
766
|
const resolutions = await mapWithConcurrency(queue, config.parallelFetches, async ([depName, range]) => {
|
|
755
|
-
const
|
|
756
|
-
if (!
|
|
767
|
+
const extractedVersion = extractSemver(range);
|
|
768
|
+
if (!extractedVersion) return null;
|
|
757
769
|
|
|
758
770
|
let depScripts = false;
|
|
759
771
|
let depDeps = null;
|
|
760
|
-
let depTarball = buildTarballUrl(depName,
|
|
772
|
+
let depTarball = buildTarballUrl(depName, extractedVersion, config.registry);
|
|
761
773
|
let depIntegrity = '';
|
|
774
|
+
let resolvedDepVersion = extractedVersion;
|
|
762
775
|
|
|
763
776
|
try {
|
|
764
777
|
const encodedDep = depName.startsWith('@') ? `@${encodeURIComponent(depName.slice(1))}` : encodeURIComponent(depName);
|
|
765
778
|
const depMeta = await fetchJSON(`${config.registry}/${encodedDep}`, { timeout: config.timeout });
|
|
766
|
-
const
|
|
779
|
+
const fullVersion = resolveVersion(extractedVersion, depMeta);
|
|
780
|
+
const depData = fullVersion && depMeta.versions && depMeta.versions[fullVersion];
|
|
767
781
|
if (depData) {
|
|
782
|
+
resolvedDepVersion = fullVersion;
|
|
768
783
|
depScripts = !!(depData.scripts &&
|
|
769
784
|
(depData.scripts.preinstall || depData.scripts.postinstall || depData.scripts.install));
|
|
770
|
-
depTarball = depData.dist && depData.dist.tarball ||
|
|
785
|
+
depTarball = depData.dist && depData.dist.tarball || buildTarballUrl(depName, fullVersion, config.registry);
|
|
771
786
|
depIntegrity = depData.dist && depData.dist.integrity || '';
|
|
772
787
|
depDeps = depData.dependencies;
|
|
773
788
|
}
|
|
@@ -778,7 +793,7 @@ async function resolveSinglePackage(packageSpec, config) {
|
|
|
778
793
|
return {
|
|
779
794
|
pkg: {
|
|
780
795
|
name: depName,
|
|
781
|
-
version:
|
|
796
|
+
version: resolvedDepVersion,
|
|
782
797
|
resolved: depTarball,
|
|
783
798
|
integrity: depIntegrity,
|
|
784
799
|
hasInstallScript: depScripts,
|
|
@@ -837,4 +852,4 @@ async function mapWithConcurrency(items, limit, fn) {
|
|
|
837
852
|
return results;
|
|
838
853
|
}
|
|
839
854
|
|
|
840
|
-
module.exports = { scan, hasInstallScripts, extractScriptFileFromCommand, verdictFromScore };
|
|
855
|
+
module.exports = { scan, hasInstallScripts, extractScriptFileFromCommand, verdictFromScore, resolveVersion, extractSemver };
|
package/src/utils/output.js
CHANGED
|
@@ -90,15 +90,16 @@ function printPackageResult(pkg, result) {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
function printSummary(results) {
|
|
93
|
+
function printSummary(results, elapsedMs) {
|
|
94
94
|
const blocked = results.filter(r => r.verdict === 'BLOCK').length;
|
|
95
95
|
const warned = results.filter(r => r.verdict === 'WARN').length;
|
|
96
96
|
const total = results.totalPackages || results.length;
|
|
97
97
|
const ok = total - blocked - warned;
|
|
98
|
+
const timing = elapsedMs != null ? dim(` (${elapsedMs}ms)`) : '';
|
|
98
99
|
|
|
99
100
|
log('');
|
|
100
101
|
log(dim('─'.repeat(60)));
|
|
101
|
-
log(` ${green(String(ok))} clean ${yellow(String(warned))} warnings ${red(String(blocked))} blocked`);
|
|
102
|
+
log(` ${green(String(ok))} clean ${yellow(String(warned))} warnings ${red(String(blocked))} blocked${timing}`);
|
|
102
103
|
log('');
|
|
103
104
|
}
|
|
104
105
|
|