np-audit 1.5.1 → 2.1.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 +117 -182
- package/package.json +1 -1
- package/src/cli.js +18 -3
- package/src/commands/alias.js +30 -11
- package/src/commands/ci.js +5 -2
- package/src/commands/install.js +5 -2
- package/src/commands/scan.js +5 -2
- package/src/core/detector.js +8 -28
- package/src/core/scanner.js +173 -42
- package/src/marshallers/base.js +18 -0
- package/src/marshallers/base64Exec.js +23 -0
- package/src/marshallers/childProcess.js +32 -0
- package/src/marshallers/cve.js +116 -0
- package/src/marshallers/eval.js +30 -0
- package/src/marshallers/filesystemManipulation.js +47 -0
- package/src/marshallers/fromCharCode.js +40 -0
- package/src/marshallers/hexArray.js +21 -0
- package/src/marshallers/hexEscapes.js +27 -0
- package/src/marshallers/highEntropy.js +70 -0
- package/src/marshallers/index.js +25 -0
- package/src/marshallers/networkCalls.js +30 -0
- package/src/marshallers/obfuscatorIo.js +22 -0
- package/src/marshallers/processEnv.js +17 -0
- package/src/marshallers/runtimeDownload.js +73 -0
- package/src/marshallers/vscodeTasks.js +29 -0
- package/src/utils/config.js +2 -0
- package/src/utils/entropy.js +15 -0
- package/src/utils/fetcher.js +5 -4
- package/src/utils/output.js +39 -9
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Marshaller } = require('./base');
|
|
4
|
+
|
|
5
|
+
class HexEscapesMarshaller extends Marshaller {
|
|
6
|
+
constructor() {
|
|
7
|
+
super('hex-escape-density', 'Dense hex/unicode escape detection');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
check(code) {
|
|
11
|
+
const hexMatches = (code.match(/\\x[0-9a-fA-F]{2}/g) || []).length;
|
|
12
|
+
const unicodeMatches = (code.match(/\\u[0-9a-fA-F]{4}/g) || []).length
|
|
13
|
+
+ (code.match(/\\u\{[0-9a-fA-F]+\}/g) || []).length;
|
|
14
|
+
const total = hexMatches + unicodeMatches;
|
|
15
|
+
if (total < 10) return null;
|
|
16
|
+
let score = 5;
|
|
17
|
+
if (total > 1000) score = 50;
|
|
18
|
+
else if (total > 200) score = 30;
|
|
19
|
+
else if (total > 50) score = 15;
|
|
20
|
+
const detail = unicodeMatches > 0
|
|
21
|
+
? `${hexMatches} \\xNN + ${unicodeMatches} \\uXXXX escapes found`
|
|
22
|
+
: `${hexMatches} \\xNN hex escapes found`;
|
|
23
|
+
return { name: this.name, score, detail };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = new HexEscapesMarshaller();
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Marshaller } = require('./base');
|
|
4
|
+
const { shannonEntropy } = require('../utils/entropy');
|
|
5
|
+
|
|
6
|
+
class HighEntropyMarshaller extends Marshaller {
|
|
7
|
+
constructor() {
|
|
8
|
+
super('high-entropy-string', 'High-entropy string detection');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
check(code) {
|
|
12
|
+
let maxEntropy = 0;
|
|
13
|
+
let worst = '';
|
|
14
|
+
const minLen = 50;
|
|
15
|
+
|
|
16
|
+
for (const quote of ['"', "'", '`']) {
|
|
17
|
+
let pos = 0;
|
|
18
|
+
while (pos < code.length) {
|
|
19
|
+
const start = code.indexOf(quote, pos);
|
|
20
|
+
if (start === -1) break;
|
|
21
|
+
|
|
22
|
+
let end = start + 1;
|
|
23
|
+
while (end < code.length) {
|
|
24
|
+
if (code[end] === '\\') { end += 2; continue; }
|
|
25
|
+
if (code[end] === quote) break;
|
|
26
|
+
end++;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (end < code.length && end - start - 1 >= minLen) {
|
|
30
|
+
const s = code.slice(start + 1, end);
|
|
31
|
+
const e = shannonEntropy(s);
|
|
32
|
+
if (e > maxEntropy) { maxEntropy = e; worst = s.slice(0, 40); }
|
|
33
|
+
}
|
|
34
|
+
pos = end + 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (maxEntropy >= 4.5) {
|
|
39
|
+
return {
|
|
40
|
+
name: this.name,
|
|
41
|
+
score: 6,
|
|
42
|
+
detail: `Entropy ${maxEntropy.toFixed(2)} in string "${worst}…"`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const concatChainRe = /(?:['"`][^'"`\n]{0,40}['"`]\s*\+\s*){5,}['"`][^'"`\n]{0,40}['"`]/g;
|
|
47
|
+
let m;
|
|
48
|
+
let bestChain = '';
|
|
49
|
+
while ((m = concatChainRe.exec(code)) !== null) {
|
|
50
|
+
const literals = m[0].match(/['"`]([^'"`\n]{0,40})['"`]/g) || [];
|
|
51
|
+
const joined = literals.map(l => l.slice(1, -1)).join('');
|
|
52
|
+
if (joined.length >= 40) {
|
|
53
|
+
const e = shannonEntropy(joined);
|
|
54
|
+
if (e > maxEntropy) { maxEntropy = e; bestChain = joined.slice(0, 40); }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (maxEntropy >= 4.5) {
|
|
59
|
+
return {
|
|
60
|
+
name: this.name,
|
|
61
|
+
score: 6,
|
|
62
|
+
detail: `Entropy ${maxEntropy.toFixed(2)} in concatenated literals "${bestChain}…"`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = new HighEntropyMarshaller();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const MARSHALLERS_DIR = __dirname;
|
|
7
|
+
const EXCLUDE = new Set(['index.js', 'base.js', 'cve.js']);
|
|
8
|
+
|
|
9
|
+
let _staticMarshallers = null;
|
|
10
|
+
|
|
11
|
+
function getStaticMarshallers() {
|
|
12
|
+
if (_staticMarshallers) return _staticMarshallers;
|
|
13
|
+
|
|
14
|
+
_staticMarshallers = fs.readdirSync(MARSHALLERS_DIR)
|
|
15
|
+
.filter(f => f.endsWith('.js') && !EXCLUDE.has(f))
|
|
16
|
+
.map(f => require(path.join(MARSHALLERS_DIR, f)));
|
|
17
|
+
|
|
18
|
+
return _staticMarshallers;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getPackageMarshallers() {
|
|
22
|
+
return [require('./cve')];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { getStaticMarshallers, getPackageMarshallers };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Marshaller } = require('./base');
|
|
4
|
+
|
|
5
|
+
class NetworkCallsMarshaller extends Marshaller {
|
|
6
|
+
constructor() {
|
|
7
|
+
super('network-call', 'Suspicious network call detection');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
check(code) {
|
|
11
|
+
const patterns = [
|
|
12
|
+
/require\s*\(\s*['"]https?['"]\s*\)/,
|
|
13
|
+
/require\s*\(\s*['"]net['"]\s*\)/,
|
|
14
|
+
/require\s*\(\s*['"]dns['"]\s*\)/,
|
|
15
|
+
/require\s*\(\s*['"]tls['"]\s*\)/,
|
|
16
|
+
/require\s*\(\s*['"]dgram['"]\s*\)/,
|
|
17
|
+
/require\s*\(\s*['"]http2['"]\s*\)/,
|
|
18
|
+
/\bfetch\s*\(/,
|
|
19
|
+
/XMLHttpRequest/,
|
|
20
|
+
/\.request\s*\(/,
|
|
21
|
+
/require\s*\(\s*['"]node:(?:https?|net|dns|tls|dgram|http2)['"]\s*\)/,
|
|
22
|
+
/import\s*\(\s*['"](?:node:)?(?:https?|net|dns|tls|dgram|http2)['"]\s*\)/,
|
|
23
|
+
];
|
|
24
|
+
const matched = patterns.filter(p => p.test(code));
|
|
25
|
+
if (matched.length === 0) return null;
|
|
26
|
+
return { name: this.name, score: 4, detail: `Network call found (${matched.length} pattern(s))` };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = new NetworkCallsMarshaller();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Marshaller } = require('./base');
|
|
4
|
+
|
|
5
|
+
class ObfuscatorIoMarshaller extends Marshaller {
|
|
6
|
+
constructor() {
|
|
7
|
+
super('obfuscator.io', 'Obfuscator.io signature detection');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
check(code) {
|
|
11
|
+
const matches = code.match(/_0x[0-9a-fA-F]+/g) || [];
|
|
12
|
+
if (matches.length < 3) return null;
|
|
13
|
+
let score = 9;
|
|
14
|
+
if (matches.length > 1000) score = 80;
|
|
15
|
+
else if (matches.length > 200) score = 50;
|
|
16
|
+
else if (matches.length > 50) score = 30;
|
|
17
|
+
else if (matches.length > 10) score = 15;
|
|
18
|
+
return { name: this.name, score, detail: `${matches.length} _0x identifiers found` };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = new ObfuscatorIoMarshaller();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Marshaller } = require('./base');
|
|
4
|
+
|
|
5
|
+
class ProcessEnvMarshaller extends Marshaller {
|
|
6
|
+
constructor() {
|
|
7
|
+
super('process-env', 'Process environment access detection');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
check(code) {
|
|
11
|
+
const matches = (code.match(/process\.env\b/g) || []).length;
|
|
12
|
+
if (matches === 0) return null;
|
|
13
|
+
return { name: this.name, score: 3, detail: `${matches} process.env access(es)` };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = new ProcessEnvMarshaller();
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Marshaller } = require('./base');
|
|
4
|
+
|
|
5
|
+
class RuntimeDownloadMarshaller extends Marshaller {
|
|
6
|
+
constructor() {
|
|
7
|
+
super('runtime-download', 'External runtime download and execution');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
check(code) {
|
|
11
|
+
// Detect downloading external runtimes (common evasion: use Bun/Deno to bypass Node-based tools)
|
|
12
|
+
const runtimePatterns = [
|
|
13
|
+
/bun-(?:linux|darwin|windows)/i,
|
|
14
|
+
/oven-sh\/bun/i,
|
|
15
|
+
/deno\.land/i,
|
|
16
|
+
/denoland\/deno/i,
|
|
17
|
+
/BUN_VERSION/,
|
|
18
|
+
/DENO_VERSION/,
|
|
19
|
+
/bun\.exe/,
|
|
20
|
+
/deno\.exe/,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// Detect execution of downloaded binaries
|
|
24
|
+
const execPatterns = [
|
|
25
|
+
/execFileSync\s*\(\s*\w*[Bb]un/,
|
|
26
|
+
/execFileSync\s*\(\s*\w*[Dd]eno/,
|
|
27
|
+
/execFile\s*\(\s*\w*[Bb]un/,
|
|
28
|
+
/execFile\s*\(\s*\w*[Dd]eno/,
|
|
29
|
+
/spawn\s*\(\s*\w*[Bb]un/,
|
|
30
|
+
/spawn\s*\(\s*\w*[Dd]eno/,
|
|
31
|
+
// Generic: download + chmod + exec pattern
|
|
32
|
+
/chmodSync\s*\([^)]*0o?755\)[\s\S]{0,500}execFileSync/,
|
|
33
|
+
/chmod[\s\S]{0,200}exec(?:File)?(?:Sync)?\s*\(/,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Detect download URLs for binaries combined with execution
|
|
37
|
+
const downloadExecPatterns = [
|
|
38
|
+
/https:\/\/github\.com\/[^/]+\/[^/]+\/releases\/download[\s\S]{0,1000}exec(?:File)?(?:Sync)?\s*\(/,
|
|
39
|
+
/downloadToFile[\s\S]{0,2000}exec(?:File)?(?:Sync)?\s*\(/,
|
|
40
|
+
/createWriteStream[\s\S]{0,2000}exec(?:File)?(?:Sync)?\s*\(/,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const runtimeMatches = runtimePatterns.filter(p => p.test(code));
|
|
44
|
+
const execMatches = execPatterns.filter(p => p.test(code));
|
|
45
|
+
const downloadExecMatches = downloadExecPatterns.filter(p => p.test(code));
|
|
46
|
+
|
|
47
|
+
if (runtimeMatches.length === 0 && downloadExecMatches.length === 0) return null;
|
|
48
|
+
|
|
49
|
+
const details = [];
|
|
50
|
+
|
|
51
|
+
if (runtimeMatches.length > 0) {
|
|
52
|
+
details.push(`external runtime reference (${runtimeMatches.length} pattern(s))`);
|
|
53
|
+
}
|
|
54
|
+
if (execMatches.length > 0) {
|
|
55
|
+
details.push(`executes downloaded binary (${execMatches.length} pattern(s))`);
|
|
56
|
+
}
|
|
57
|
+
if (downloadExecMatches.length > 0) {
|
|
58
|
+
details.push('downloads and executes remote binary');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// High score: downloading a runtime to execute code is a major evasion technique
|
|
62
|
+
let score = 9;
|
|
63
|
+
if (runtimeMatches.length > 0 && (execMatches.length > 0 || downloadExecMatches.length > 0)) {
|
|
64
|
+
score = 50;
|
|
65
|
+
} else if (downloadExecMatches.length > 0) {
|
|
66
|
+
score = 30;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { name: this.name, score, detail: details.join('; ') };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = new RuntimeDownloadMarshaller();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Marshaller } = require('./base');
|
|
4
|
+
|
|
5
|
+
class VscodeTasksMarshaller extends Marshaller {
|
|
6
|
+
constructor() {
|
|
7
|
+
super('vscode-autorun', 'VS Code tasks with automatic execution');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
check(code) {
|
|
11
|
+
// Detect tasks.json content with runOn: folderOpen (auto-executes on project open)
|
|
12
|
+
if (!code.includes('runOn') && !code.includes('folderOpen')) return null;
|
|
13
|
+
|
|
14
|
+
const hasFolderOpen = /["']runOn["']\s*:\s*["']folderOpen["']/.test(code);
|
|
15
|
+
if (!hasFolderOpen) return null;
|
|
16
|
+
|
|
17
|
+
// Extract the command being auto-run
|
|
18
|
+
const commandMatch = code.match(/["']command["']\s*:\s*["']([^"']+)["']/);
|
|
19
|
+
const command = commandMatch ? commandMatch[1] : 'unknown';
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
name: this.name,
|
|
23
|
+
score: 30,
|
|
24
|
+
detail: `VS Code task auto-executes on folder open: "${command}"`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = new VscodeTasksMarshaller();
|
package/src/utils/config.js
CHANGED
|
@@ -17,6 +17,8 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
17
17
|
silent: false,
|
|
18
18
|
scanSelf: true,
|
|
19
19
|
maxTarballSize: '50MB', // Max unpacked tarball size (e.g. '5MB', '1GB', or bytes as number)
|
|
20
|
+
checkVulnerabilities: true,
|
|
21
|
+
deepResolve: false,
|
|
20
22
|
});
|
|
21
23
|
|
|
22
24
|
const VALID_KEYS = new Set(Object.keys(DEFAULT_CONFIG));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function shannonEntropy(str) {
|
|
4
|
+
if (!str || str.length === 0) return 0;
|
|
5
|
+
const freq = {};
|
|
6
|
+
for (const ch of str) freq[ch] = (freq[ch] || 0) + 1;
|
|
7
|
+
let entropy = 0;
|
|
8
|
+
for (const count of Object.values(freq)) {
|
|
9
|
+
const p = count / str.length;
|
|
10
|
+
entropy -= p * Math.log2(p);
|
|
11
|
+
}
|
|
12
|
+
return entropy;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { shannonEntropy };
|
package/src/utils/fetcher.js
CHANGED
|
@@ -9,10 +9,10 @@ const MAX_REDIRECTS = 5;
|
|
|
9
9
|
const DEFAULT_TIMEOUT = 30000;
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Perform an HTTP/HTTPS
|
|
12
|
+
* Perform an HTTP/HTTPS request and collect the response body as a Buffer.
|
|
13
13
|
* Follows redirects up to MAX_REDIRECTS.
|
|
14
14
|
* @param {string} rawUrl
|
|
15
|
-
* @param {object} opts { timeout?, headers? }
|
|
15
|
+
* @param {object} opts { timeout?, headers?, method?, body? }
|
|
16
16
|
* @returns {Promise<Buffer>}
|
|
17
17
|
*/
|
|
18
18
|
function fetch(rawUrl, opts = {}) {
|
|
@@ -28,7 +28,7 @@ function fetch(rawUrl, opts = {}) {
|
|
|
28
28
|
hostname: parsed.hostname,
|
|
29
29
|
port: parsed.port || (isHttps ? 443 : 80),
|
|
30
30
|
path: parsed.pathname + parsed.search,
|
|
31
|
-
method: 'GET',
|
|
31
|
+
method: opts.method || 'GET',
|
|
32
32
|
headers: Object.assign({
|
|
33
33
|
'User-Agent': 'npa/1.0.0 (npm-auditor)',
|
|
34
34
|
'Accept': '*/*',
|
|
@@ -61,6 +61,7 @@ function fetch(rawUrl, opts = {}) {
|
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
req.on('error', reject);
|
|
64
|
+
if (opts.body) req.write(opts.body);
|
|
64
65
|
req.end();
|
|
65
66
|
}
|
|
66
67
|
|
|
@@ -87,7 +88,7 @@ function fetchTarball(tarballUrl, opts = {}) {
|
|
|
87
88
|
async function fetchJSON(jsonUrl, opts = {}) {
|
|
88
89
|
const buf = await fetch(jsonUrl, {
|
|
89
90
|
...opts,
|
|
90
|
-
headers: { 'Accept': 'application/json' },
|
|
91
|
+
headers: { 'Accept': 'application/json', ...opts.headers },
|
|
91
92
|
});
|
|
92
93
|
return JSON.parse(buf.toString('utf8'));
|
|
93
94
|
}
|
package/src/utils/output.js
CHANGED
|
@@ -29,10 +29,12 @@ function cyan(text) { return c(CYAN, text); }
|
|
|
29
29
|
function white(text) { return c(WHITE, text); }
|
|
30
30
|
|
|
31
31
|
function error(msg) {
|
|
32
|
+
if (process.stderr.isTTY) process.stderr.write('\r\x1b[2K');
|
|
32
33
|
process.stderr.write(red(`✖ ${msg}`) + '\n');
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
function warn(msg) {
|
|
37
|
+
if (process.stderr.isTTY) process.stderr.write('\r\x1b[2K');
|
|
36
38
|
process.stderr.write(yellow(`⚠ ${msg}`) + '\n');
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -67,12 +69,17 @@ const ASCII_LOGO = `
|
|
|
67
69
|
|
|
68
70
|
function printScanHeader(silent = false) {
|
|
69
71
|
if (silent) return;
|
|
70
|
-
log(
|
|
71
|
-
log(dim(' npm package auditor — static obfuscation detection'));
|
|
72
|
+
log('');
|
|
72
73
|
log(dim('─'.repeat(60)));
|
|
73
74
|
log('');
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
function printLogo(version) {
|
|
78
|
+
log(blue(ASCII_LOGO));
|
|
79
|
+
log(dim(` npm package auditor v${version}`));
|
|
80
|
+
log('');
|
|
81
|
+
}
|
|
82
|
+
|
|
76
83
|
function printPackageResult(pkg, result) {
|
|
77
84
|
const badge = verdictBadge(result.verdict);
|
|
78
85
|
const name = bold(`${pkg.name}@${pkg.version}`);
|
|
@@ -86,22 +93,45 @@ function printPackageResult(pkg, result) {
|
|
|
86
93
|
function printSummary(results) {
|
|
87
94
|
const blocked = results.filter(r => r.verdict === 'BLOCK').length;
|
|
88
95
|
const warned = results.filter(r => r.verdict === 'WARN').length;
|
|
89
|
-
const
|
|
90
|
-
const
|
|
96
|
+
const total = results.totalPackages || results.length;
|
|
97
|
+
const ok = total - blocked - warned;
|
|
91
98
|
|
|
92
99
|
log('');
|
|
93
100
|
log(dim('─'.repeat(60)));
|
|
94
|
-
|
|
95
|
-
if (skipped > 0) {
|
|
96
|
-
summary += ` ${dim(String(skipped) + ' skipped (no install scripts)')}`;
|
|
97
|
-
}
|
|
98
|
-
log(summary);
|
|
101
|
+
log(` ${green(String(ok))} clean ${yellow(String(warned))} warnings ${red(String(blocked))} blocked`);
|
|
99
102
|
log('');
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
const SPINNER_FRAMES = ['▉', '▊', '▋', '▌', '▍', '▎', '▏', '▎', '▍', '▌', '▋', '▊', '▉'];
|
|
106
|
+
|
|
107
|
+
function createSpinner(message) {
|
|
108
|
+
if (NO_COLOR || !process.stderr.isTTY) {
|
|
109
|
+
return { start() {}, stop() {} };
|
|
110
|
+
}
|
|
111
|
+
let i = 0;
|
|
112
|
+
let timer = null;
|
|
113
|
+
const clear = () => process.stderr.write('\r\x1b[2K');
|
|
114
|
+
return {
|
|
115
|
+
start() {
|
|
116
|
+
process.stderr.write(`\x1b[?25l`);
|
|
117
|
+
process.stderr.write(` ${cyan(SPINNER_FRAMES[0])} ${white(message)}`);
|
|
118
|
+
timer = setInterval(() => {
|
|
119
|
+
i = (i + 1) % SPINNER_FRAMES.length;
|
|
120
|
+
process.stderr.write(`\r\x1b[2K ${cyan(SPINNER_FRAMES[i])} ${white(message)}`);
|
|
121
|
+
}, 60);
|
|
122
|
+
},
|
|
123
|
+
stop() {
|
|
124
|
+
if (timer) clearInterval(timer);
|
|
125
|
+
clear();
|
|
126
|
+
process.stderr.write(`\x1b[?25h`);
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
102
131
|
module.exports = {
|
|
103
132
|
bold, dim, red, green, yellow, blue, cyan, white,
|
|
104
133
|
error, warn, info, success, log,
|
|
105
134
|
verdictBadge, printScanHeader, printPackageResult, printSummary,
|
|
135
|
+
printLogo, createSpinner,
|
|
106
136
|
RESET, BOLD, DIM, RED, GREEN, YELLOW, BLUE, CYAN,
|
|
107
137
|
};
|