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 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.1.0",
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
  }
@@ -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');
@@ -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');
@@ -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);
@@ -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 versionData = meta.versions && meta.versions[version];
691
- if (!versionData) continue;
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 exactVersion = extractSemver(range);
756
- if (!exactVersion) return null;
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, exactVersion, config.registry);
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 depData = depMeta.versions && depMeta.versions[exactVersion];
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 || depTarball;
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: exactVersion,
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 };
@@ -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