openclawsec 1.0.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/.github/ISSUE_TEMPLATE/bug-report.md +42 -0
- package/.github/ISSUE_TEMPLATE/feature-request.md +23 -0
- package/.github/workflows/ci.yml +41 -0
- package/CONTRIBUTING.md +28 -0
- package/LICENSE +21 -0
- package/README.md +175 -0
- package/clawshield-web/index.html +344 -0
- package/cli.js +184 -0
- package/package.json +33 -0
- package/src/checks/configHarden.js +210 -0
- package/src/checks/cve.js +115 -0
- package/src/checks/secretsCheck.js +192 -0
- package/src/checks/skillAudit.js +204 -0
- package/src/checks/version.js +114 -0
- package/src/commands/audit.js +59 -0
- package/src/commands/doctor.js +85 -0
- package/src/commands/monitor.js +175 -0
- package/src/commands/scan.js +144 -0
- package/src/utils/output.js +171 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
const out = require('../utils/output');
|
|
2
|
+
const ora = require('ora');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const version = require('../checks/version');
|
|
6
|
+
const cve = require('../checks/cve');
|
|
7
|
+
const configHarden = require('../checks/configHarden');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_INTERVAL = 60 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
function getLogPath() {
|
|
12
|
+
const home = process.env.USERPROFILE || process.env.HOME || process.env.HOMEPATH;
|
|
13
|
+
const logDir = path.join(home, '.clawshield', 'logs');
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(logDir)) {
|
|
16
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return path.join(logDir, 'monitor-' + new Date().toISOString().split('T')[0] + '.json');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function saveResult(result) {
|
|
23
|
+
const logPath = getLogPath();
|
|
24
|
+
let logs = [];
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
if (fs.existsSync(logPath)) {
|
|
28
|
+
logs = JSON.parse(fs.readFileSync(logPath, 'utf8'));
|
|
29
|
+
}
|
|
30
|
+
} catch (e) {}
|
|
31
|
+
|
|
32
|
+
logs.push({
|
|
33
|
+
timestamp: new Date().toISOString(),
|
|
34
|
+
...result
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
fs.writeFileSync(logPath, JSON.stringify(logs, null, 2));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function runMonitor(interval = DEFAULT_INTERVAL) {
|
|
41
|
+
const spinner = ora({ spinner: 'dots' });
|
|
42
|
+
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log(out.COLORS.title('╔══════════════════════════════════════════════════╗'));
|
|
45
|
+
console.log(out.COLORS.title('║ CLAWSHIELD MONITOR MODE ║'));
|
|
46
|
+
console.log(out.COLORS.title('╚══════════════════════════════════════════════════╝'));
|
|
47
|
+
console.log('');
|
|
48
|
+
|
|
49
|
+
console.log(`Monitoring interval: ${interval / 1000 / 60} minutes`);
|
|
50
|
+
console.log('Press Ctrl+C to stop\n');
|
|
51
|
+
|
|
52
|
+
out.info('Starting continuous monitoring...');
|
|
53
|
+
out.text('Results will be logged to: ~/.clawshield/logs/');
|
|
54
|
+
console.log('');
|
|
55
|
+
|
|
56
|
+
const runScan = async () => {
|
|
57
|
+
spinner.start('Running security scan...');
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const versionResult = await version.check();
|
|
61
|
+
const cveResult = await cve.check(versionResult.installed);
|
|
62
|
+
const configPath = configHarden.getOpenClawConfigPath();
|
|
63
|
+
const config = configHarden.getConfigFromFile(configPath);
|
|
64
|
+
const configResult = configHarden.check(config);
|
|
65
|
+
|
|
66
|
+
let score = 100;
|
|
67
|
+
if (versionResult.outdated) score -= 20;
|
|
68
|
+
if (cveResult.vulnerable) score -= cveResult.count * 15;
|
|
69
|
+
if (configResult.gateway?.exposed) score -= 25;
|
|
70
|
+
score = Math.max(0, score);
|
|
71
|
+
|
|
72
|
+
const result = {
|
|
73
|
+
score,
|
|
74
|
+
version: versionResult,
|
|
75
|
+
cve: cveResult,
|
|
76
|
+
config: configResult,
|
|
77
|
+
timestamp: new Date().toISOString()
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
saveResult(result);
|
|
81
|
+
|
|
82
|
+
if (score < 60) {
|
|
83
|
+
spinner.fail(`ALERT: Security score dropped to ${score}/100`);
|
|
84
|
+
out.critical('Security issues detected! Review logs for details.');
|
|
85
|
+
} else if (score < 80) {
|
|
86
|
+
spinner.warn(`Warning: Security score is ${score}/100`);
|
|
87
|
+
} else {
|
|
88
|
+
spinner.succeed(`All clear - Score: ${score}/100`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
} catch (error) {
|
|
92
|
+
spinner.fail('Scan failed: ' + error.message);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log('');
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
await runScan();
|
|
99
|
+
|
|
100
|
+
console.log(out.COLORS.dim(`Next scan in ${interval / 1000 / 60} minutes...`));
|
|
101
|
+
console.log('');
|
|
102
|
+
|
|
103
|
+
const intervalId = setInterval(async () => {
|
|
104
|
+
await runScan();
|
|
105
|
+
console.log(out.COLORS.dim(`Next scan in ${interval / 1000 / 60} minutes...`));
|
|
106
|
+
console.log('');
|
|
107
|
+
}, interval);
|
|
108
|
+
|
|
109
|
+
process.on('SIGINT', () => {
|
|
110
|
+
console.log('');
|
|
111
|
+
out.info('Stopping monitor...');
|
|
112
|
+
clearInterval(intervalId);
|
|
113
|
+
process.exit(0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return intervalId;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function checkStatus() {
|
|
120
|
+
const logPath = getLogPath().replace(new Date().toISOString().split('T')[0], '*');
|
|
121
|
+
const logDir = path.join(process.env.USERPROFILE || process.env.HOME || process.env.HOMEPATH, '.clawshield', 'logs');
|
|
122
|
+
|
|
123
|
+
console.log('');
|
|
124
|
+
console.log(out.COLORS.title('╔══════════════════════════════════════════════════╗'));
|
|
125
|
+
console.log(out.COLORS.title('║ CLAWSHIELD STATUS CHECK ║'));
|
|
126
|
+
console.log(out.COLORS.title('╚══════════════════════════════════════════════════╝'));
|
|
127
|
+
console.log('');
|
|
128
|
+
|
|
129
|
+
if (!fs.existsSync(logDir)) {
|
|
130
|
+
out.warning('No monitoring data found');
|
|
131
|
+
out.text('Run: clawshield monitor to start monitoring');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const files = fs.readdirSync(logDir).filter(f => f.endsWith('.json'));
|
|
136
|
+
|
|
137
|
+
if (files.length === 0) {
|
|
138
|
+
out.warning('No monitoring data found');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const latestFile = files.sort().pop();
|
|
143
|
+
const latestPath = path.join(logDir, latestFile);
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const logs = JSON.parse(fs.readFileSync(latestPath, 'utf8'));
|
|
147
|
+
const latest = logs[logs.length - 1];
|
|
148
|
+
|
|
149
|
+
out.keyValue('Last Scan', latest.timestamp);
|
|
150
|
+
out.keyValue('Security Score', `${latest.score}/100`);
|
|
151
|
+
out.keyValue('Log File', latestFile);
|
|
152
|
+
out.keyValue('Total Scans', logs.length.toString());
|
|
153
|
+
|
|
154
|
+
console.log('');
|
|
155
|
+
|
|
156
|
+
if (latest.cve?.vulnerable) {
|
|
157
|
+
out.critical(`⚠ ${latest.cve.count} CVE vulnerability(ies) detected!`);
|
|
158
|
+
} else {
|
|
159
|
+
out.passed('No CVE vulnerabilities found');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (latest.config?.gateway?.exposed) {
|
|
163
|
+
out.critical('⚠ Gateway is exposed!');
|
|
164
|
+
} else {
|
|
165
|
+
out.passed('Gateway is protected');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log('');
|
|
169
|
+
|
|
170
|
+
} catch (e) {
|
|
171
|
+
out.error('Failed to read monitoring data');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { runMonitor, checkStatus };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const out = require('../utils/output');
|
|
2
|
+
const ora = require('ora');
|
|
3
|
+
const version = require('../checks/version');
|
|
4
|
+
const cve = require('../checks/cve');
|
|
5
|
+
const configHarden = require('../checks/configHarden');
|
|
6
|
+
const skillAudit = require('../checks/skillAudit');
|
|
7
|
+
const secretsCheck = require('../checks/secretsCheck');
|
|
8
|
+
|
|
9
|
+
function calculateScore(results) {
|
|
10
|
+
let score = 100;
|
|
11
|
+
|
|
12
|
+
if (results.version.outdated) score -= 20;
|
|
13
|
+
if (results.cve.vulnerable) score -= results.cve.count * 15;
|
|
14
|
+
if (results.config.gateway?.exposed) score -= 25;
|
|
15
|
+
if (results.config.dmPolicy?.issues?.length > 0) score -= 10;
|
|
16
|
+
if (results.config.tools?.issues?.length > 0) score -= 5;
|
|
17
|
+
if (results.skills.criticalCount > 0) score -= results.skills.criticalCount * 20;
|
|
18
|
+
if (results.skills.warningCount > 0) score -= results.skills.warningCount * 5;
|
|
19
|
+
if (results.secrets.findings?.length > 0) score -= results.secrets.findings.length * 10;
|
|
20
|
+
if (results.secrets.sensitiveFiles > 0) score -= 5;
|
|
21
|
+
|
|
22
|
+
return Math.max(0, Math.min(100, score));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getSeverityCounts(results) {
|
|
26
|
+
let critical = 0;
|
|
27
|
+
let warnings = 0;
|
|
28
|
+
|
|
29
|
+
if (results.version.outdated) critical++;
|
|
30
|
+
if (results.cve.vulnerable) critical += results.cve.count || 0;
|
|
31
|
+
if (results.config.gateway?.exposed) critical++;
|
|
32
|
+
if (results.config.dmPolicy?.issues?.length > 0) warnings++;
|
|
33
|
+
if (results.skills.criticalCount > 0) critical += results.skills.criticalCount;
|
|
34
|
+
if (results.skills.warningCount > 0) warnings += results.skills.warningCount;
|
|
35
|
+
if (results.secrets.findings?.length > 0) critical += results.secrets.findings.length;
|
|
36
|
+
|
|
37
|
+
return { critical, warnings };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function scan() {
|
|
41
|
+
out.banner();
|
|
42
|
+
|
|
43
|
+
const spinner = ora({
|
|
44
|
+
text: 'Running comprehensive security scan...',
|
|
45
|
+
spinner: 'dots'
|
|
46
|
+
}).start();
|
|
47
|
+
|
|
48
|
+
const results = {
|
|
49
|
+
version: { status: 'unknown', outdated: false },
|
|
50
|
+
cve: { cves: [], vulnerable: false },
|
|
51
|
+
config: {},
|
|
52
|
+
skills: { criticalCount: 0, warningCount: 0 },
|
|
53
|
+
secrets: { findings: [], filesScanned: 0 }
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
spinner.text = 'Checking OpenClaw version...';
|
|
58
|
+
results.version = await version.check();
|
|
59
|
+
out.divider();
|
|
60
|
+
|
|
61
|
+
spinner.text = 'Checking CVE vulnerabilities...';
|
|
62
|
+
results.cve = await cve.check(results.version.installed);
|
|
63
|
+
out.divider();
|
|
64
|
+
|
|
65
|
+
spinner.text = 'Analyzing configuration...';
|
|
66
|
+
const configPath = configHarden.getOpenClawConfigPath();
|
|
67
|
+
const config = configHarden.getConfigFromFile(configPath);
|
|
68
|
+
results.config = configHarden.check(config);
|
|
69
|
+
out.divider();
|
|
70
|
+
|
|
71
|
+
spinner.text = 'Auditing installed skills...';
|
|
72
|
+
results.skills = skillAudit.check();
|
|
73
|
+
out.divider();
|
|
74
|
+
|
|
75
|
+
spinner.text = 'Scanning for exposed secrets...';
|
|
76
|
+
results.secrets = secretsCheck.check();
|
|
77
|
+
out.divider();
|
|
78
|
+
|
|
79
|
+
spinner.succeed('Scan complete!');
|
|
80
|
+
|
|
81
|
+
const score = calculateScore(results);
|
|
82
|
+
const severityCounts = getSeverityCounts(results);
|
|
83
|
+
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(out.COLORS.title('=========================================='));
|
|
86
|
+
console.log(out.COLORS.title(' SECURITY SUMMARY'));
|
|
87
|
+
console.log(out.COLORS.title('=========================================='));
|
|
88
|
+
console.log('');
|
|
89
|
+
|
|
90
|
+
console.log(' OpenClaw Version: ' + (results.version.installed || 'Unknown'));
|
|
91
|
+
console.log(' Version Status: ' + (results.version.outdated ? out.COLORS.error('OUTDATED') : out.COLORS.passed('UP TO DATE')));
|
|
92
|
+
console.log('');
|
|
93
|
+
|
|
94
|
+
const scoreColor = score >= 80 ? out.COLORS.passed : (score >= 60 ? out.COLORS.warning : out.COLORS.critical);
|
|
95
|
+
console.log(' Security Score: ' + scoreColor(score + '/100'));
|
|
96
|
+
console.log('');
|
|
97
|
+
|
|
98
|
+
console.log(out.COLORS.title('----------------------------------------'));
|
|
99
|
+
console.log(' Issues Found:');
|
|
100
|
+
console.log(' Critical: ' + out.COLORS.error(severityCounts.critical.toString()));
|
|
101
|
+
console.log(' Warnings: ' + out.COLORS.warning(severityCounts.warnings.toString()));
|
|
102
|
+
console.log(out.COLORS.title('----------------------------------------'));
|
|
103
|
+
console.log('');
|
|
104
|
+
|
|
105
|
+
console.log(' Detailed Results:');
|
|
106
|
+
console.log(' CVEs Found: ' + (results.cve.count ? out.COLORS.error(results.cve.count.toString()) : out.COLORS.passed('None')));
|
|
107
|
+
console.log(' Skills Scanned: ' + out.COLORS.info((results.skills.total || 0).toString()));
|
|
108
|
+
console.log(' Malicious Skills: ' + (results.skills.criticalCount ? out.COLORS.error(results.skills.criticalCount.toString()) : out.COLORS.passed('0')));
|
|
109
|
+
console.log(' Exposed Secrets: ' + (results.secrets.findings?.length ? out.COLORS.error(results.secrets.findings.length.toString()) : out.COLORS.passed('0')));
|
|
110
|
+
console.log(' Gateway Exposed: ' + (results.config.gateway?.exposed ? out.COLORS.error('YES') : out.COLORS.passed('No')));
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(out.COLORS.title('=========================================='));
|
|
113
|
+
console.log('');
|
|
114
|
+
|
|
115
|
+
if (severityCounts.critical > 0) {
|
|
116
|
+
console.log(out.COLORS.error('! ACTION REQUIRED: Critical security issues detected!'));
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log('Recommendations:');
|
|
119
|
+
console.log(' 1. Update OpenClaw: npm update -g openclaw');
|
|
120
|
+
console.log(' 2. Remove malicious skills: clawhub uninstall <skill-name>');
|
|
121
|
+
console.log(' 3. Revoke exposed API keys and regenerate new ones');
|
|
122
|
+
console.log(' 4. Configure gateway token if not set');
|
|
123
|
+
console.log('');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (score >= 80) {
|
|
127
|
+
console.log(out.COLORS.success('[OK] Your OpenClaw deployment is in good shape!'));
|
|
128
|
+
} else if (score >= 60) {
|
|
129
|
+
console.log(out.COLORS.warning('[WARN] Your OpenClaw deployment needs some attention.'));
|
|
130
|
+
} else {
|
|
131
|
+
console.log(out.COLORS.error('[FAIL] Your OpenClaw deployment has critical issues!'));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return results;
|
|
135
|
+
|
|
136
|
+
} catch (error) {
|
|
137
|
+
spinner.fail('Scan failed: ' + error.message);
|
|
138
|
+
out.error('An error occurred during the scan');
|
|
139
|
+
out.text('Please report this issue with your OpenClaw version and configuration');
|
|
140
|
+
return results;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = { scan };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
|
|
3
|
+
const COLORS = {
|
|
4
|
+
critical: chalk.red,
|
|
5
|
+
warning: chalk.keyword('orange'),
|
|
6
|
+
passed: chalk.green,
|
|
7
|
+
info: chalk.blue,
|
|
8
|
+
title: chalk.bold.cyan,
|
|
9
|
+
subtitle: chalk.cyan,
|
|
10
|
+
cyan: chalk.cyan,
|
|
11
|
+
highlight: chalk.bold.white,
|
|
12
|
+
dim: chalk.gray,
|
|
13
|
+
success: chalk.green.bold,
|
|
14
|
+
error: chalk.red.bold,
|
|
15
|
+
bold: chalk.bold,
|
|
16
|
+
yellow: chalk.keyword('yellow')
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function banner() {
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log(COLORS.title('╔══════════════════════════════════════════════════╗'));
|
|
22
|
+
console.log(COLORS.title('║ CLAWSHIELD SECURITY REPORT ║'));
|
|
23
|
+
console.log(COLORS.title('╠══════════════════════════════════════════════════╣'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function footer(score) {
|
|
27
|
+
let scoreColor;
|
|
28
|
+
if (score >= 80) scoreColor = COLORS.passed;
|
|
29
|
+
else if (score >= 60) scoreColor = COLORS.warning;
|
|
30
|
+
else scoreColor = COLORS.critical;
|
|
31
|
+
|
|
32
|
+
console.log(COLORS.title('╠══════════════════════════════════════════════════╣'));
|
|
33
|
+
console.log(COLORS.title('║') + ' Security Score: ' + scoreColor(`${score}/100`) + ' '.repeat(26) + COLORS.title('║'));
|
|
34
|
+
console.log(COLORS.title('╚══════════════════════════════════════════════════╝'));
|
|
35
|
+
console.log('');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function section(title) {
|
|
39
|
+
console.log('');
|
|
40
|
+
console.log(COLORS.subtitle(`━━━ ${title} ━━━`));
|
|
41
|
+
console.log('');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function critical(message) {
|
|
45
|
+
console.log(COLORS.critical('🔴 CRITICAL: ') + message);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function warning(message) {
|
|
49
|
+
console.log(COLORS.warning('🟡 WARNING: ') + message);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function passed(message) {
|
|
53
|
+
console.log(COLORS.passed('🟢 PASSED: ') + message);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function info(message) {
|
|
57
|
+
console.log(COLORS.info('🔵 INFO: ') + message);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function text(message) {
|
|
61
|
+
console.log(' ' + message);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function list(items, type = 'info') {
|
|
65
|
+
items.forEach(item => {
|
|
66
|
+
if (type === 'critical') critical(item);
|
|
67
|
+
else if (type === 'warning') warning(item);
|
|
68
|
+
else if (type === 'passed') passed(item);
|
|
69
|
+
else info(item);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function divider() {
|
|
74
|
+
console.log(COLORS.dim('─'.repeat(50)));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function label(label, value, color = COLORS.info) {
|
|
78
|
+
console.log(` ${COLORS.dim(label)}: ${color(value)}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function keyValue(key, value) {
|
|
82
|
+
const k = COLORS.highlight(key.padEnd(20));
|
|
83
|
+
const v = COLORS.dim(value);
|
|
84
|
+
console.log(` ${k}${v}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function success(message) {
|
|
88
|
+
console.log(COLORS.success('✓ ') + message);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function error(message) {
|
|
92
|
+
console.log(COLORS.error('✗ ') + message);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function heading(message) {
|
|
96
|
+
console.log(COLORS.title(`━━━ ${message} ━━━`));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function subheading(message) {
|
|
100
|
+
console.log(COLORS.subtitle(message));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function generateTable(items) {
|
|
104
|
+
const maxLen = 50;
|
|
105
|
+
items.forEach(item => {
|
|
106
|
+
const [label, value, status] = item;
|
|
107
|
+
let statusSymbol, statusColor;
|
|
108
|
+
|
|
109
|
+
if (status === 'pass') {
|
|
110
|
+
statusSymbol = '✓';
|
|
111
|
+
statusColor = COLORS.passed;
|
|
112
|
+
} else if (status === 'warn') {
|
|
113
|
+
statusSymbol = '⚠';
|
|
114
|
+
statusColor = COLORS.warning;
|
|
115
|
+
} else if (status === 'fail') {
|
|
116
|
+
statusSymbol = '✗';
|
|
117
|
+
statusColor = COLORS.critical;
|
|
118
|
+
} else {
|
|
119
|
+
statusSymbol = '○';
|
|
120
|
+
statusColor = COLORS.info;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const labelPadded = (label + ' ').padEnd(maxLen);
|
|
124
|
+
console.log(` ${statusColor(statusSymbol)} ${COLORS.dim(labelPadded)}${statusColor(value)}`);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function progressBar(current, total, width = 30) {
|
|
129
|
+
const filled = Math.round((current / total) * width);
|
|
130
|
+
const empty = width - filled;
|
|
131
|
+
const bar = '█'.repeat(filled) + '░'.repeat(empty);
|
|
132
|
+
return bar;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function scoreColor(score) {
|
|
136
|
+
if (score >= 80) return COLORS.passed;
|
|
137
|
+
if (score >= 60) return COLORS.warning;
|
|
138
|
+
return COLORS.critical;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function formatBytes(bytes) {
|
|
142
|
+
if (bytes === 0) return '0 Bytes';
|
|
143
|
+
const k = 1024;
|
|
144
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
145
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
146
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
COLORS,
|
|
151
|
+
banner,
|
|
152
|
+
footer,
|
|
153
|
+
section,
|
|
154
|
+
critical,
|
|
155
|
+
warning,
|
|
156
|
+
passed,
|
|
157
|
+
info,
|
|
158
|
+
text,
|
|
159
|
+
list,
|
|
160
|
+
divider,
|
|
161
|
+
label,
|
|
162
|
+
keyValue,
|
|
163
|
+
success,
|
|
164
|
+
error,
|
|
165
|
+
heading,
|
|
166
|
+
subheading,
|
|
167
|
+
generateTable,
|
|
168
|
+
progressBar,
|
|
169
|
+
scoreColor,
|
|
170
|
+
formatBytes
|
|
171
|
+
};
|