muaddib-scanner 2.4.4 → 2.4.5
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/LICENSE +20 -20
- package/iocs/builtin.yaml +131 -131
- package/iocs/hashes.yaml +214 -214
- package/iocs/packages.yaml +276 -276
- package/package.json +2 -3
- package/src/canary-tokens.js +184 -184
- package/src/ioc/bootstrap.js +181 -181
- package/src/ioc/yaml-loader.js +223 -223
- package/src/maintainer-change.js +224 -224
- package/src/output-formatter.js +192 -192
- package/src/publish-anomaly.js +206 -206
- package/src/report.js +230 -230
- package/src/sarif.js +96 -96
- package/src/scanner/ai-config.js +183 -183
- package/src/scanner/ast-detectors.js +40 -17
- package/src/scanner/ast.js +1 -0
- package/src/scanner/dataflow.js +14 -2
- package/src/scanner/dependencies.js +223 -223
- package/src/scanner/entropy.js +7 -0
- package/src/scanner/hash.js +118 -118
- package/src/scanner/npm-registry.js +128 -128
- package/src/scanner/python.js +442 -442
- package/src/scoring.js +3 -1
- package/src/shared/analyze-helper.js +49 -49
- package/src/temporal-analysis.js +260 -260
- package/src/temporal-runner.js +139 -139
- package/src/utils.js +327 -327
- package/src/watch.js +55 -55
package/src/output-formatter.js
CHANGED
|
@@ -1,192 +1,192 @@
|
|
|
1
|
-
const { saveReport } = require('./report.js');
|
|
2
|
-
const { saveSARIF } = require('./sarif.js');
|
|
3
|
-
const { getPlaybook } = require('./response/playbooks.js');
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Format and print scan output in the requested format.
|
|
7
|
-
* Handles JSON, HTML, SARIF, explain, and normal (default) modes.
|
|
8
|
-
* @param {Object} result - scan result object
|
|
9
|
-
* @param {Object} options - scan options (json, html, sarif, explain, breakdown)
|
|
10
|
-
* @param {Object} ctx - context object with shared state
|
|
11
|
-
* @param {Object|null} ctx.spinner - TTY spinner instance
|
|
12
|
-
* @param {Object|null} ctx.sandboxData - sandbox analysis results
|
|
13
|
-
* @param {string|null} ctx.mostSuspiciousFile - file with highest score
|
|
14
|
-
* @param {number} ctx.maxFileScore - highest per-file score
|
|
15
|
-
* @param {number} ctx.packageScore - package-level score
|
|
16
|
-
* @param {number} ctx.globalRiskScore - global sum score
|
|
17
|
-
* @param {Array} ctx.deduped - deduplicated threats
|
|
18
|
-
* @param {Array} ctx.enrichedThreats - enriched threats with rules/playbooks
|
|
19
|
-
* @param {Object|null} ctx.pythonInfo - Python scan metadata
|
|
20
|
-
* @param {Array} ctx.breakdown - score breakdown sorted by impact
|
|
21
|
-
* @param {string} ctx.targetPath - scan target path
|
|
22
|
-
*/
|
|
23
|
-
function formatOutput(result, options, ctx) {
|
|
24
|
-
const {
|
|
25
|
-
spinner, sandboxData, mostSuspiciousFile, maxFileScore,
|
|
26
|
-
packageScore, globalRiskScore, deduped, enrichedThreats,
|
|
27
|
-
pythonInfo, breakdown, targetPath
|
|
28
|
-
} = ctx;
|
|
29
|
-
|
|
30
|
-
// JSON output
|
|
31
|
-
if (options.json) {
|
|
32
|
-
console.log(JSON.stringify(result, null, 2));
|
|
33
|
-
}
|
|
34
|
-
// HTML output
|
|
35
|
-
else if (options.html) {
|
|
36
|
-
saveReport(result, options.html);
|
|
37
|
-
console.log(`[OK] HTML report generated: ${options.html}`);
|
|
38
|
-
}
|
|
39
|
-
// SARIF output
|
|
40
|
-
else if (options.sarif) {
|
|
41
|
-
saveSARIF(result, options.sarif);
|
|
42
|
-
console.log(`[OK] SARIF report generated: ${options.sarif}`);
|
|
43
|
-
}
|
|
44
|
-
// Explain output
|
|
45
|
-
else if (options.explain) {
|
|
46
|
-
if (!spinner) console.log(`\n[MUADDIB] Scanning ${targetPath}\n`);
|
|
47
|
-
else console.log('');
|
|
48
|
-
|
|
49
|
-
const explainScoreBar = '█'.repeat(Math.floor(result.summary.riskScore / 5)) + '░'.repeat(20 - Math.floor(result.summary.riskScore / 5));
|
|
50
|
-
console.log(`[SCORE] ${result.summary.riskScore}/100 [${explainScoreBar}] ${result.summary.riskLevel}`);
|
|
51
|
-
if (mostSuspiciousFile) {
|
|
52
|
-
console.log(` Max file: ${mostSuspiciousFile} (${maxFileScore} pts)`);
|
|
53
|
-
if (packageScore > 0) {
|
|
54
|
-
console.log(` Package-level: +${packageScore} pts`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
console.log('');
|
|
58
|
-
|
|
59
|
-
if (options.breakdown && breakdown.length > 0) {
|
|
60
|
-
console.log('[BREAKDOWN] Score contributors:');
|
|
61
|
-
for (const entry of breakdown) {
|
|
62
|
-
const pts = String(entry.points).padStart(2);
|
|
63
|
-
console.log(` +${pts} ${entry.reason} (${entry.rule})`);
|
|
64
|
-
}
|
|
65
|
-
if (globalRiskScore !== result.summary.riskScore) {
|
|
66
|
-
console.log(' ----');
|
|
67
|
-
console.log(` Global sum: ${globalRiskScore}, Per-file max: ${result.summary.riskScore}`);
|
|
68
|
-
}
|
|
69
|
-
console.log('');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (pythonInfo) {
|
|
73
|
-
console.log(`[PYTHON] ${pythonInfo.dependencies} dependencies detected (${pythonInfo.files.join(', ')})`);
|
|
74
|
-
if (pythonInfo.threats > 0) {
|
|
75
|
-
console.log(`[PYTHON] ${pythonInfo.threats} malicious PyPI package(s) found!\n`);
|
|
76
|
-
} else {
|
|
77
|
-
console.log(`[PYTHON] No known malicious PyPI packages.\n`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (enrichedThreats.length === 0) {
|
|
82
|
-
console.log('[OK] No threats detected.\n');
|
|
83
|
-
} else {
|
|
84
|
-
console.log(`[ALERT] ${enrichedThreats.length} threat(s) detected:\n`);
|
|
85
|
-
enrichedThreats.forEach((t, i) => {
|
|
86
|
-
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
87
|
-
const countStr = t.count > 1 ? ` (x${t.count})` : '';
|
|
88
|
-
console.log(` ${i + 1}. [${t.severity}] ${t.rule_name}${countStr}`);
|
|
89
|
-
console.log(` Rule ID: ${t.rule_id}`);
|
|
90
|
-
console.log(` File: ${t.file}`);
|
|
91
|
-
if (t.line) console.log(` Line: ${t.line}`);
|
|
92
|
-
console.log(` Confidence: ${t.confidence}`);
|
|
93
|
-
console.log(` Message: ${t.message}`);
|
|
94
|
-
if (t.mitre) console.log(` MITRE: ${t.mitre} (https://attack.mitre.org/techniques/${t.mitre.replace('.', '/')})`);
|
|
95
|
-
if (t.references && t.references.length > 0) {
|
|
96
|
-
console.log(` References:`);
|
|
97
|
-
t.references.forEach(ref => console.log(` - ${ref}`));
|
|
98
|
-
}
|
|
99
|
-
console.log(` Playbook: ${t.playbook}`);
|
|
100
|
-
console.log('');
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Sandbox section (explain)
|
|
105
|
-
if (sandboxData) {
|
|
106
|
-
console.log(`\n[SANDBOX] Dynamic analysis — ${sandboxData.package}`);
|
|
107
|
-
console.log(` Score: ${sandboxData.score}/100`);
|
|
108
|
-
console.log(` Severity: ${sandboxData.severity}`);
|
|
109
|
-
if (sandboxData.findings.length === 0) {
|
|
110
|
-
console.log(' No suspicious behavior detected.\n');
|
|
111
|
-
} else {
|
|
112
|
-
console.log(` ${sandboxData.findings.length} finding(s):`);
|
|
113
|
-
sandboxData.findings.forEach(f => {
|
|
114
|
-
console.log(` [${f.severity}] ${f.type}: ${f.detail}`);
|
|
115
|
-
});
|
|
116
|
-
console.log('');
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
// Normal output
|
|
121
|
-
else {
|
|
122
|
-
if (!spinner) console.log(`\n[MUADDIB] Scanning ${targetPath}\n`);
|
|
123
|
-
else console.log('');
|
|
124
|
-
|
|
125
|
-
const scoreBar = '█'.repeat(Math.floor(result.summary.riskScore / 5)) + '░'.repeat(20 - Math.floor(result.summary.riskScore / 5));
|
|
126
|
-
console.log(`[SCORE] ${result.summary.riskScore}/100 [${scoreBar}] ${result.summary.riskLevel}`);
|
|
127
|
-
if (mostSuspiciousFile) {
|
|
128
|
-
console.log(` Max file: ${mostSuspiciousFile} (${maxFileScore} pts)`);
|
|
129
|
-
if (packageScore > 0) {
|
|
130
|
-
console.log(` Package-level: +${packageScore} pts`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
console.log('');
|
|
134
|
-
|
|
135
|
-
if (options.breakdown && breakdown.length > 0) {
|
|
136
|
-
console.log('[BREAKDOWN] Score contributors:');
|
|
137
|
-
for (const entry of breakdown) {
|
|
138
|
-
const pts = String(entry.points).padStart(2);
|
|
139
|
-
console.log(` +${pts} ${entry.reason} (${entry.rule})`);
|
|
140
|
-
}
|
|
141
|
-
if (globalRiskScore !== result.summary.riskScore) {
|
|
142
|
-
console.log(' ----');
|
|
143
|
-
console.log(` Global sum: ${globalRiskScore}, Per-file max: ${result.summary.riskScore}`);
|
|
144
|
-
}
|
|
145
|
-
console.log('');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (pythonInfo) {
|
|
149
|
-
console.log(`[PYTHON] ${pythonInfo.dependencies} dependencies detected (${pythonInfo.files.join(', ')})`);
|
|
150
|
-
if (pythonInfo.threats > 0) {
|
|
151
|
-
console.log(`[PYTHON] ${pythonInfo.threats} malicious PyPI package(s) found!\n`);
|
|
152
|
-
} else {
|
|
153
|
-
console.log(`[PYTHON] No known malicious PyPI packages.\n`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (deduped.length === 0) {
|
|
158
|
-
console.log('[OK] No threats detected.\n');
|
|
159
|
-
} else {
|
|
160
|
-
console.log(`[ALERT] ${deduped.length} threat(s) detected:\n`);
|
|
161
|
-
deduped.forEach((t, i) => {
|
|
162
|
-
const countStr = t.count > 1 ? ` (x${t.count})` : '';
|
|
163
|
-
console.log(` ${i + 1}. [${t.severity}] ${t.type}${countStr}`);
|
|
164
|
-
console.log(` ${t.message}`);
|
|
165
|
-
console.log(` File: ${t.file}`);
|
|
166
|
-
const playbook = getPlaybook(t.type);
|
|
167
|
-
if (playbook) {
|
|
168
|
-
console.log(` \u2192 ${playbook}`);
|
|
169
|
-
}
|
|
170
|
-
console.log('');
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Sandbox section (normal)
|
|
175
|
-
if (sandboxData) {
|
|
176
|
-
console.log(`[SANDBOX] Dynamic analysis — ${sandboxData.package}`);
|
|
177
|
-
console.log(` Score: ${sandboxData.score}/100`);
|
|
178
|
-
console.log(` Severity: ${sandboxData.severity}`);
|
|
179
|
-
if (sandboxData.findings.length === 0) {
|
|
180
|
-
console.log(' No suspicious behavior detected.\n');
|
|
181
|
-
} else {
|
|
182
|
-
console.log(` ${sandboxData.findings.length} finding(s):`);
|
|
183
|
-
sandboxData.findings.forEach(f => {
|
|
184
|
-
console.log(` [${f.severity}] ${f.type}: ${f.detail}`);
|
|
185
|
-
});
|
|
186
|
-
console.log('');
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
module.exports = { formatOutput };
|
|
1
|
+
const { saveReport } = require('./report.js');
|
|
2
|
+
const { saveSARIF } = require('./sarif.js');
|
|
3
|
+
const { getPlaybook } = require('./response/playbooks.js');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format and print scan output in the requested format.
|
|
7
|
+
* Handles JSON, HTML, SARIF, explain, and normal (default) modes.
|
|
8
|
+
* @param {Object} result - scan result object
|
|
9
|
+
* @param {Object} options - scan options (json, html, sarif, explain, breakdown)
|
|
10
|
+
* @param {Object} ctx - context object with shared state
|
|
11
|
+
* @param {Object|null} ctx.spinner - TTY spinner instance
|
|
12
|
+
* @param {Object|null} ctx.sandboxData - sandbox analysis results
|
|
13
|
+
* @param {string|null} ctx.mostSuspiciousFile - file with highest score
|
|
14
|
+
* @param {number} ctx.maxFileScore - highest per-file score
|
|
15
|
+
* @param {number} ctx.packageScore - package-level score
|
|
16
|
+
* @param {number} ctx.globalRiskScore - global sum score
|
|
17
|
+
* @param {Array} ctx.deduped - deduplicated threats
|
|
18
|
+
* @param {Array} ctx.enrichedThreats - enriched threats with rules/playbooks
|
|
19
|
+
* @param {Object|null} ctx.pythonInfo - Python scan metadata
|
|
20
|
+
* @param {Array} ctx.breakdown - score breakdown sorted by impact
|
|
21
|
+
* @param {string} ctx.targetPath - scan target path
|
|
22
|
+
*/
|
|
23
|
+
function formatOutput(result, options, ctx) {
|
|
24
|
+
const {
|
|
25
|
+
spinner, sandboxData, mostSuspiciousFile, maxFileScore,
|
|
26
|
+
packageScore, globalRiskScore, deduped, enrichedThreats,
|
|
27
|
+
pythonInfo, breakdown, targetPath
|
|
28
|
+
} = ctx;
|
|
29
|
+
|
|
30
|
+
// JSON output
|
|
31
|
+
if (options.json) {
|
|
32
|
+
console.log(JSON.stringify(result, null, 2));
|
|
33
|
+
}
|
|
34
|
+
// HTML output
|
|
35
|
+
else if (options.html) {
|
|
36
|
+
saveReport(result, options.html);
|
|
37
|
+
console.log(`[OK] HTML report generated: ${options.html}`);
|
|
38
|
+
}
|
|
39
|
+
// SARIF output
|
|
40
|
+
else if (options.sarif) {
|
|
41
|
+
saveSARIF(result, options.sarif);
|
|
42
|
+
console.log(`[OK] SARIF report generated: ${options.sarif}`);
|
|
43
|
+
}
|
|
44
|
+
// Explain output
|
|
45
|
+
else if (options.explain) {
|
|
46
|
+
if (!spinner) console.log(`\n[MUADDIB] Scanning ${targetPath}\n`);
|
|
47
|
+
else console.log('');
|
|
48
|
+
|
|
49
|
+
const explainScoreBar = '█'.repeat(Math.floor(result.summary.riskScore / 5)) + '░'.repeat(20 - Math.floor(result.summary.riskScore / 5));
|
|
50
|
+
console.log(`[SCORE] ${result.summary.riskScore}/100 [${explainScoreBar}] ${result.summary.riskLevel}`);
|
|
51
|
+
if (mostSuspiciousFile) {
|
|
52
|
+
console.log(` Max file: ${mostSuspiciousFile} (${maxFileScore} pts)`);
|
|
53
|
+
if (packageScore > 0) {
|
|
54
|
+
console.log(` Package-level: +${packageScore} pts`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
console.log('');
|
|
58
|
+
|
|
59
|
+
if (options.breakdown && breakdown.length > 0) {
|
|
60
|
+
console.log('[BREAKDOWN] Score contributors:');
|
|
61
|
+
for (const entry of breakdown) {
|
|
62
|
+
const pts = String(entry.points).padStart(2);
|
|
63
|
+
console.log(` +${pts} ${entry.reason} (${entry.rule})`);
|
|
64
|
+
}
|
|
65
|
+
if (globalRiskScore !== result.summary.riskScore) {
|
|
66
|
+
console.log(' ----');
|
|
67
|
+
console.log(` Global sum: ${globalRiskScore}, Per-file max: ${result.summary.riskScore}`);
|
|
68
|
+
}
|
|
69
|
+
console.log('');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (pythonInfo) {
|
|
73
|
+
console.log(`[PYTHON] ${pythonInfo.dependencies} dependencies detected (${pythonInfo.files.join(', ')})`);
|
|
74
|
+
if (pythonInfo.threats > 0) {
|
|
75
|
+
console.log(`[PYTHON] ${pythonInfo.threats} malicious PyPI package(s) found!\n`);
|
|
76
|
+
} else {
|
|
77
|
+
console.log(`[PYTHON] No known malicious PyPI packages.\n`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (enrichedThreats.length === 0) {
|
|
82
|
+
console.log('[OK] No threats detected.\n');
|
|
83
|
+
} else {
|
|
84
|
+
console.log(`[ALERT] ${enrichedThreats.length} threat(s) detected:\n`);
|
|
85
|
+
enrichedThreats.forEach((t, i) => {
|
|
86
|
+
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
87
|
+
const countStr = t.count > 1 ? ` (x${t.count})` : '';
|
|
88
|
+
console.log(` ${i + 1}. [${t.severity}] ${t.rule_name}${countStr}`);
|
|
89
|
+
console.log(` Rule ID: ${t.rule_id}`);
|
|
90
|
+
console.log(` File: ${t.file}`);
|
|
91
|
+
if (t.line) console.log(` Line: ${t.line}`);
|
|
92
|
+
console.log(` Confidence: ${t.confidence}`);
|
|
93
|
+
console.log(` Message: ${t.message}`);
|
|
94
|
+
if (t.mitre) console.log(` MITRE: ${t.mitre} (https://attack.mitre.org/techniques/${t.mitre.replace('.', '/')})`);
|
|
95
|
+
if (t.references && t.references.length > 0) {
|
|
96
|
+
console.log(` References:`);
|
|
97
|
+
t.references.forEach(ref => console.log(` - ${ref}`));
|
|
98
|
+
}
|
|
99
|
+
console.log(` Playbook: ${t.playbook}`);
|
|
100
|
+
console.log('');
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Sandbox section (explain)
|
|
105
|
+
if (sandboxData) {
|
|
106
|
+
console.log(`\n[SANDBOX] Dynamic analysis — ${sandboxData.package}`);
|
|
107
|
+
console.log(` Score: ${sandboxData.score}/100`);
|
|
108
|
+
console.log(` Severity: ${sandboxData.severity}`);
|
|
109
|
+
if (sandboxData.findings.length === 0) {
|
|
110
|
+
console.log(' No suspicious behavior detected.\n');
|
|
111
|
+
} else {
|
|
112
|
+
console.log(` ${sandboxData.findings.length} finding(s):`);
|
|
113
|
+
sandboxData.findings.forEach(f => {
|
|
114
|
+
console.log(` [${f.severity}] ${f.type}: ${f.detail}`);
|
|
115
|
+
});
|
|
116
|
+
console.log('');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Normal output
|
|
121
|
+
else {
|
|
122
|
+
if (!spinner) console.log(`\n[MUADDIB] Scanning ${targetPath}\n`);
|
|
123
|
+
else console.log('');
|
|
124
|
+
|
|
125
|
+
const scoreBar = '█'.repeat(Math.floor(result.summary.riskScore / 5)) + '░'.repeat(20 - Math.floor(result.summary.riskScore / 5));
|
|
126
|
+
console.log(`[SCORE] ${result.summary.riskScore}/100 [${scoreBar}] ${result.summary.riskLevel}`);
|
|
127
|
+
if (mostSuspiciousFile) {
|
|
128
|
+
console.log(` Max file: ${mostSuspiciousFile} (${maxFileScore} pts)`);
|
|
129
|
+
if (packageScore > 0) {
|
|
130
|
+
console.log(` Package-level: +${packageScore} pts`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
console.log('');
|
|
134
|
+
|
|
135
|
+
if (options.breakdown && breakdown.length > 0) {
|
|
136
|
+
console.log('[BREAKDOWN] Score contributors:');
|
|
137
|
+
for (const entry of breakdown) {
|
|
138
|
+
const pts = String(entry.points).padStart(2);
|
|
139
|
+
console.log(` +${pts} ${entry.reason} (${entry.rule})`);
|
|
140
|
+
}
|
|
141
|
+
if (globalRiskScore !== result.summary.riskScore) {
|
|
142
|
+
console.log(' ----');
|
|
143
|
+
console.log(` Global sum: ${globalRiskScore}, Per-file max: ${result.summary.riskScore}`);
|
|
144
|
+
}
|
|
145
|
+
console.log('');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (pythonInfo) {
|
|
149
|
+
console.log(`[PYTHON] ${pythonInfo.dependencies} dependencies detected (${pythonInfo.files.join(', ')})`);
|
|
150
|
+
if (pythonInfo.threats > 0) {
|
|
151
|
+
console.log(`[PYTHON] ${pythonInfo.threats} malicious PyPI package(s) found!\n`);
|
|
152
|
+
} else {
|
|
153
|
+
console.log(`[PYTHON] No known malicious PyPI packages.\n`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (deduped.length === 0) {
|
|
158
|
+
console.log('[OK] No threats detected.\n');
|
|
159
|
+
} else {
|
|
160
|
+
console.log(`[ALERT] ${deduped.length} threat(s) detected:\n`);
|
|
161
|
+
deduped.forEach((t, i) => {
|
|
162
|
+
const countStr = t.count > 1 ? ` (x${t.count})` : '';
|
|
163
|
+
console.log(` ${i + 1}. [${t.severity}] ${t.type}${countStr}`);
|
|
164
|
+
console.log(` ${t.message}`);
|
|
165
|
+
console.log(` File: ${t.file}`);
|
|
166
|
+
const playbook = getPlaybook(t.type);
|
|
167
|
+
if (playbook) {
|
|
168
|
+
console.log(` \u2192 ${playbook}`);
|
|
169
|
+
}
|
|
170
|
+
console.log('');
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Sandbox section (normal)
|
|
175
|
+
if (sandboxData) {
|
|
176
|
+
console.log(`[SANDBOX] Dynamic analysis — ${sandboxData.package}`);
|
|
177
|
+
console.log(` Score: ${sandboxData.score}/100`);
|
|
178
|
+
console.log(` Severity: ${sandboxData.severity}`);
|
|
179
|
+
if (sandboxData.findings.length === 0) {
|
|
180
|
+
console.log(' No suspicious behavior detected.\n');
|
|
181
|
+
} else {
|
|
182
|
+
console.log(` ${sandboxData.findings.length} finding(s):`);
|
|
183
|
+
sandboxData.findings.forEach(f => {
|
|
184
|
+
console.log(` [${f.severity}] ${f.type}: ${f.detail}`);
|
|
185
|
+
});
|
|
186
|
+
console.log('');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = { formatOutput };
|