cybersentinel-cli 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/bin/index.js +540 -0
- package/package.json +22 -0
package/bin/index.js
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const https = require('https');
|
|
7
|
+
const axios = require('axios');
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
const { program } = require('commander');
|
|
10
|
+
const inquirer = require('inquirer');
|
|
11
|
+
const ora = require('ora');
|
|
12
|
+
const forge = require('node-forge');
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
|
|
15
|
+
const CONFIG_DIR = path.join(os.homedir(), '.cybersentinel');
|
|
16
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
17
|
+
const CLIENT_KEY = path.join(CONFIG_DIR, 'client.key');
|
|
18
|
+
const CLIENT_CRT = path.join(CONFIG_DIR, 'client.crt');
|
|
19
|
+
const CA_CRT = path.join(CONFIG_DIR, 'ca.crt');
|
|
20
|
+
const CLIENT_P12 = path.join(CONFIG_DIR, 'client.p12');
|
|
21
|
+
|
|
22
|
+
function decryptResponse(data) {
|
|
23
|
+
if (data && data.encrypted === true) {
|
|
24
|
+
try {
|
|
25
|
+
const keyString = 'CyberSentinelSuperSecureAESKey2026';
|
|
26
|
+
const key = crypto.createHash('sha256').update(keyString).digest();
|
|
27
|
+
|
|
28
|
+
const iv = Buffer.from(data.iv, 'hex');
|
|
29
|
+
const ciphertext = Buffer.from(data.content, 'hex');
|
|
30
|
+
const tag = Buffer.from(data.tag, 'hex');
|
|
31
|
+
|
|
32
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
33
|
+
decipher.setAuthTag(tag);
|
|
34
|
+
|
|
35
|
+
let decrypted = decipher.update(ciphertext, undefined, 'utf8');
|
|
36
|
+
decrypted += decipher.final('utf8');
|
|
37
|
+
return JSON.parse(decrypted);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error(chalk.red('\n[Crypto Error] Failed to decrypt server response payload.'));
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return data;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
const VIPER_BANNER = `
|
|
48
|
+
${chalk.green(` c. _.._`)}
|
|
49
|
+
${chalk.green(` :: .-' \`-.`)}
|
|
50
|
+
${chalk.green(` \`:. .' .---. \\`)}
|
|
51
|
+
${chalk.green(` \`::. \\ / \\ |`)} ${chalk.green.bold('CYBERSENTINEL')}
|
|
52
|
+
${chalk.green(` \`:::..| | |`)} ${chalk.green('Audit Platform CLI v1.0')}
|
|
53
|
+
${chalk.green(` \`'' \\ / /`)}
|
|
54
|
+
${chalk.green(` |\`-.-'-'|`)} ${chalk.gray("Type: 'cybersentinel --help'")}
|
|
55
|
+
${chalk.green(` | | |`)}
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const SNAKE_SPINNER = {
|
|
59
|
+
interval: 100,
|
|
60
|
+
frames: ['⠏', '⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇'].map(f => chalk.green(f))
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Ensure configuration directory exists
|
|
64
|
+
function ensureConfigDir() {
|
|
65
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
66
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check if credentials/certificate exist
|
|
71
|
+
function hasCertificates() {
|
|
72
|
+
return (
|
|
73
|
+
fs.existsSync(CLIENT_KEY) &&
|
|
74
|
+
fs.existsSync(CLIENT_CRT) &&
|
|
75
|
+
fs.existsSync(CA_CRT) &&
|
|
76
|
+
fs.existsSync(CONFIG_FILE)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Load CLI Config
|
|
81
|
+
function loadConfig() {
|
|
82
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
83
|
+
throw new Error('Configuration file not found. Please register first.');
|
|
84
|
+
}
|
|
85
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Create Axios client with mTLS certificates
|
|
89
|
+
function getMtlsClient(config) {
|
|
90
|
+
const agent = new https.Agent({
|
|
91
|
+
cert: fs.readFileSync(CLIENT_CRT),
|
|
92
|
+
key: fs.readFileSync(CLIENT_KEY),
|
|
93
|
+
ca: fs.readFileSync(CA_CRT),
|
|
94
|
+
rejectUnauthorized: false // since we are using localhost self-signed certs
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return axios.create({
|
|
98
|
+
baseURL: config.apiUrl,
|
|
99
|
+
httpsAgent: agent,
|
|
100
|
+
headers: {
|
|
101
|
+
'x-api-key': config.apiKey || 'CyberSentinelSecretAPIKey2026!'
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Print startup banner
|
|
107
|
+
console.log(VIPER_BANNER);
|
|
108
|
+
|
|
109
|
+
program
|
|
110
|
+
.name('cybersentinel')
|
|
111
|
+
.description('Interactive audit checklist and device certification tool')
|
|
112
|
+
.version('1.0.0');
|
|
113
|
+
|
|
114
|
+
// Command: register
|
|
115
|
+
program
|
|
116
|
+
.command('register')
|
|
117
|
+
.description('Register this device and request signed client certificate')
|
|
118
|
+
.option('-d, --device-name <name>', 'Custom device name for certificate common name')
|
|
119
|
+
.action(async (options) => {
|
|
120
|
+
ensureConfigDir();
|
|
121
|
+
|
|
122
|
+
const questions = [
|
|
123
|
+
{
|
|
124
|
+
type: 'input',
|
|
125
|
+
name: 'apiUrl',
|
|
126
|
+
message: 'Enter CyberSentinel Base API URL:',
|
|
127
|
+
default: 'https://localhost:3002/api'
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
type: 'input',
|
|
131
|
+
name: 'username',
|
|
132
|
+
message: 'Enter Username:',
|
|
133
|
+
default: 'cybersentinel-admin'
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
type: 'password',
|
|
137
|
+
name: 'password',
|
|
138
|
+
message: 'Enter Password:',
|
|
139
|
+
mask: '*'
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
type: 'input',
|
|
143
|
+
name: 'deviceName',
|
|
144
|
+
message: 'Enter Device Name (Common Name):',
|
|
145
|
+
default: options.deviceName || os.hostname() || 'Auditor-Terminal',
|
|
146
|
+
when: !options.deviceName
|
|
147
|
+
}
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
const answers = await inquirer.prompt(questions);
|
|
151
|
+
const deviceName = options.deviceName || answers.deviceName;
|
|
152
|
+
const apiUrl = answers.apiUrl.replace(/\/$/, ''); // strip trailing slash
|
|
153
|
+
|
|
154
|
+
const spinner = ora({
|
|
155
|
+
text: 'Generating 2048-bit RSA keypair locally...',
|
|
156
|
+
spinner: SNAKE_SPINNER
|
|
157
|
+
}).start();
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
// 1. Generate local keypair
|
|
161
|
+
const pki = forge.pki;
|
|
162
|
+
const keys = pki.rsa.generateKeyPair(2048);
|
|
163
|
+
const privateKeyPem = pki.privateKeyToPem(keys.privateKey);
|
|
164
|
+
|
|
165
|
+
// 2. Generate local CSR
|
|
166
|
+
spinner.text = 'Generating Certification Request (CSR)...';
|
|
167
|
+
const csr = pki.createCertificationRequest();
|
|
168
|
+
csr.publicKey = keys.publicKey;
|
|
169
|
+
csr.setSubject([
|
|
170
|
+
{ name: 'commonName', value: deviceName },
|
|
171
|
+
{ name: 'organizationName', value: 'CyberSentinel' }
|
|
172
|
+
]);
|
|
173
|
+
csr.sign(keys.privateKey, forge.md.sha256.create());
|
|
174
|
+
const csrPem = pki.certificationRequestToPem(csr);
|
|
175
|
+
|
|
176
|
+
// 3. Register to backend
|
|
177
|
+
spinner.text = 'Submitting CSR to CyberSentinel Portal...';
|
|
178
|
+
const nonMtlsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
179
|
+
|
|
180
|
+
const response = await axios.post(`${apiUrl}/devices/register`, {
|
|
181
|
+
username: answers.username,
|
|
182
|
+
password: answers.password,
|
|
183
|
+
deviceName: deviceName,
|
|
184
|
+
csr: csrPem
|
|
185
|
+
}, {
|
|
186
|
+
httpsAgent: nonMtlsAgent,
|
|
187
|
+
headers: {
|
|
188
|
+
'x-api-key': 'CyberSentinelSecretAPIKey2026!'
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
spinner.succeed(chalk.green('Server signature obtained successfully!'));
|
|
193
|
+
|
|
194
|
+
const decryptedData = decryptResponse(response.data);
|
|
195
|
+
const { certificate, caCertificate, serial, fingerprint } = decryptedData;
|
|
196
|
+
|
|
197
|
+
// 4. Save certs locally
|
|
198
|
+
fs.writeFileSync(CLIENT_KEY, privateKeyPem, 'utf8');
|
|
199
|
+
fs.writeFileSync(CLIENT_CRT, certificate, 'utf8');
|
|
200
|
+
fs.writeFileSync(CA_CRT, caCertificate, 'utf8');
|
|
201
|
+
|
|
202
|
+
const configData = {
|
|
203
|
+
apiUrl,
|
|
204
|
+
deviceName,
|
|
205
|
+
serial,
|
|
206
|
+
fingerprint,
|
|
207
|
+
apiKey: 'CyberSentinelSecretAPIKey2026!'
|
|
208
|
+
};
|
|
209
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(configData, null, 2), 'utf8');
|
|
210
|
+
|
|
211
|
+
// 5. Package into PKCS12 .p12 file
|
|
212
|
+
const p12Answers = await inquirer.prompt([
|
|
213
|
+
{
|
|
214
|
+
type: 'password',
|
|
215
|
+
name: 'p12Password',
|
|
216
|
+
message: 'Set a password to protect your client.p12 bundle:',
|
|
217
|
+
mask: '*'
|
|
218
|
+
}
|
|
219
|
+
]);
|
|
220
|
+
|
|
221
|
+
const clientCertObj = pki.certificateFromPem(certificate);
|
|
222
|
+
const p12Asn1 = forge.pkcs12.toPkcs12Asn1(
|
|
223
|
+
keys.privateKey,
|
|
224
|
+
[clientCertObj],
|
|
225
|
+
p12Answers.p12Password,
|
|
226
|
+
{ algorithm: '3des' }
|
|
227
|
+
);
|
|
228
|
+
const p12Der = forge.asn1.toDer(p12Asn1).getBytes();
|
|
229
|
+
fs.writeFileSync(CLIENT_P12, p12Der, 'binary');
|
|
230
|
+
|
|
231
|
+
console.log('\n' + chalk.green.bold('✔ Registration Complete!'));
|
|
232
|
+
console.log(chalk.green(`Device Serial: ${serial}`));
|
|
233
|
+
console.log(chalk.green(`Fingerprint: ${fingerprint}`));
|
|
234
|
+
console.log(chalk.green(`Files saved to: ${CONFIG_DIR}`));
|
|
235
|
+
console.log(chalk.green(`PKCS12 Container: client.p12 (encrypted)\n`));
|
|
236
|
+
|
|
237
|
+
} catch (error) {
|
|
238
|
+
spinner.fail(chalk.red('Registration failed!'));
|
|
239
|
+
if (error.response) {
|
|
240
|
+
console.error(chalk.red(`Error: ${error.response.status} - ${error.response.data.message || error.response.data.error}`));
|
|
241
|
+
} else {
|
|
242
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Command: init
|
|
248
|
+
program
|
|
249
|
+
.command('init')
|
|
250
|
+
.description('Verify credential certificate files status')
|
|
251
|
+
.action(() => {
|
|
252
|
+
if (hasCertificates()) {
|
|
253
|
+
try {
|
|
254
|
+
const config = loadConfig();
|
|
255
|
+
console.log(chalk.green('✔ System keys and certificate verification: OK'));
|
|
256
|
+
console.log(chalk.green(`Registered Server: ${config.apiUrl}`));
|
|
257
|
+
console.log(chalk.green(`Registered Device: ${config.deviceName}`));
|
|
258
|
+
console.log(chalk.green(`Certificate Serial: ${config.serial}`));
|
|
259
|
+
} catch (err) {
|
|
260
|
+
console.error(chalk.red(`Error loading configuration: ${err.message}`));
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
console.log(chalk.yellow('⚠ No active client certificate found. Please run:'));
|
|
264
|
+
console.log(chalk.cyan(' cybersentinel register'));
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Command: audit
|
|
269
|
+
program
|
|
270
|
+
.command('audit')
|
|
271
|
+
.description('Perform security audit on a website and sync scores')
|
|
272
|
+
.requiredOption('-i, --id <assessmentId>', 'Prisma Database Assessment ID')
|
|
273
|
+
.requiredOption('-t, --target <url>', 'Website target URL for automated scan')
|
|
274
|
+
.action(async (options) => {
|
|
275
|
+
if (!hasCertificates()) {
|
|
276
|
+
console.error(chalk.red('Error: Local certificate files not found. Run "cybersentinel register" first.'));
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let config;
|
|
281
|
+
try {
|
|
282
|
+
config = loadConfig();
|
|
283
|
+
} catch (err) {
|
|
284
|
+
console.error(chalk.red(err.message));
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const client = getMtlsClient(config);
|
|
289
|
+
const assessmentId = parseInt(options.id, 10);
|
|
290
|
+
const targetUrl = options.target;
|
|
291
|
+
|
|
292
|
+
const fetchSpinner = ora({
|
|
293
|
+
text: `Connecting to ${config.apiUrl} via mTLS...`,
|
|
294
|
+
spinner: SNAKE_SPINNER
|
|
295
|
+
}).start();
|
|
296
|
+
|
|
297
|
+
let assessment;
|
|
298
|
+
try {
|
|
299
|
+
const response = await client.get(`/assessments/${assessmentId}`);
|
|
300
|
+
assessment = decryptResponse(response.data);
|
|
301
|
+
fetchSpinner.succeed(chalk.green(`mTLS connection verified. Active assessment: "${assessment.projectName}"`));
|
|
302
|
+
} catch (err) {
|
|
303
|
+
fetchSpinner.fail(chalk.red('mTLS authentication or fetch failed!'));
|
|
304
|
+
if (err.response) {
|
|
305
|
+
console.error(chalk.red(`Server error: ${err.response.status} - ${err.response.data.message || err.response.data.error}`));
|
|
306
|
+
} else {
|
|
307
|
+
console.error(chalk.red(`Connection error: ${err.message}`));
|
|
308
|
+
}
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// STEP 1: AUTOMATED SCAN
|
|
313
|
+
console.log(chalk.green.bold('\n--- Phase 1: Automated Security Vulnerability Scan ---'));
|
|
314
|
+
const scanSpinner = ora({
|
|
315
|
+
text: `Auditing target: ${targetUrl}...`,
|
|
316
|
+
spinner: SNAKE_SPINNER
|
|
317
|
+
}).start();
|
|
318
|
+
|
|
319
|
+
const results = {
|
|
320
|
+
csp: { passed: false, value: 'None' },
|
|
321
|
+
hsts: { passed: false, value: 'None' },
|
|
322
|
+
xfo: { passed: false, value: 'None' },
|
|
323
|
+
cors: { passed: false, value: 'None', details: 'None' }
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const targetAgent = new https.Agent({ rejectUnauthorized: false });
|
|
328
|
+
const targetResponse = await axios.get(targetUrl, {
|
|
329
|
+
httpsAgent: targetAgent,
|
|
330
|
+
timeout: 10000,
|
|
331
|
+
headers: { 'User-Agent': 'CyberSentinel-Auditor-CLI/1.0' }
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const headers = targetResponse.headers;
|
|
335
|
+
|
|
336
|
+
// HSTS Check
|
|
337
|
+
if (headers['strict-transport-security']) {
|
|
338
|
+
results.hsts.passed = true;
|
|
339
|
+
results.hsts.value = headers['strict-transport-security'];
|
|
340
|
+
}
|
|
341
|
+
// CSP Check
|
|
342
|
+
if (headers['content-security-policy']) {
|
|
343
|
+
results.csp.passed = true;
|
|
344
|
+
results.csp.value = headers['content-security-policy'].substring(0, 50) + '...';
|
|
345
|
+
}
|
|
346
|
+
// XFO Check
|
|
347
|
+
if (headers['x-frame-options']) {
|
|
348
|
+
results.xfo.passed = true;
|
|
349
|
+
results.xfo.value = headers['x-frame-options'];
|
|
350
|
+
}
|
|
351
|
+
// CORS Check
|
|
352
|
+
if (headers['access-control-allow-origin']) {
|
|
353
|
+
results.cors.value = headers['access-control-allow-origin'];
|
|
354
|
+
if (results.cors.value === '*') {
|
|
355
|
+
results.cors.passed = false;
|
|
356
|
+
results.cors.details = 'Wildcard "*" origin allows global access';
|
|
357
|
+
} else {
|
|
358
|
+
results.cors.passed = true;
|
|
359
|
+
results.cors.details = 'Safe origin control configured';
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
results.cors.passed = true;
|
|
363
|
+
results.cors.details = 'No cross-origin sharing headers exposed';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
scanSpinner.succeed(chalk.green('Automated scan completed!'));
|
|
367
|
+
} catch (err) {
|
|
368
|
+
scanSpinner.warn(chalk.yellow(`Automated scan incomplete (Target request failed: ${err.message}). Defaulting results.`));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
console.log(chalk.green('\nAutomated Headers Check Report:'));
|
|
372
|
+
console.log(`- Strict-Transport-Security (HSTS): ${results.hsts.passed ? chalk.green('FOUND') : chalk.red('MISSING')} [${results.hsts.value}]`);
|
|
373
|
+
console.log(`- Content-Security-Policy (CSP): ${results.csp.passed ? chalk.green('FOUND') : chalk.red('MISSING')} [${results.csp.value}]`);
|
|
374
|
+
console.log(`- X-Frame-Options (Clickjacking): ${results.xfo.passed ? chalk.green('FOUND') : chalk.red('MISSING')} [${results.xfo.value}]`);
|
|
375
|
+
console.log(`- Access-Control-Allow-Origin: ${results.cors.value === '*' ? chalk.red('WILDCARD DETECTED') : chalk.green('SAFE/NONE')} [${results.cors.value}]`);
|
|
376
|
+
|
|
377
|
+
// Sync automated findings
|
|
378
|
+
const scores = JSON.parse(assessment.scores || '{}');
|
|
379
|
+
|
|
380
|
+
// Map CSP to item 7.1
|
|
381
|
+
scores['7.1'] = {
|
|
382
|
+
rating: results.csp.passed ? '5' : '1',
|
|
383
|
+
notes: `[Automated CLI Scan] Content-Security-Policy header is ${results.csp.passed ? 'configured' : 'missing'}.`
|
|
384
|
+
};
|
|
385
|
+
// Map HSTS to item 7.2
|
|
386
|
+
scores['7.2'] = {
|
|
387
|
+
rating: results.hsts.passed ? '5' : '1',
|
|
388
|
+
notes: `[Automated CLI Scan] Strict-Transport-Security is ${results.hsts.passed ? 'configured' : 'missing'}.`
|
|
389
|
+
};
|
|
390
|
+
// Map XFO to item 8.1
|
|
391
|
+
scores['8.1'] = {
|
|
392
|
+
rating: results.xfo.passed ? '5' : '1',
|
|
393
|
+
notes: `[Automated CLI Scan] Clickjacking protection via X-Frame-Options is ${results.xfo.passed ? 'configured' : 'missing'}.`
|
|
394
|
+
};
|
|
395
|
+
// Map CORS to item 10.1
|
|
396
|
+
scores['10.1'] = {
|
|
397
|
+
rating: results.cors.value === '*' ? '2' : '5',
|
|
398
|
+
notes: `[Automated CLI Scan] CORS verification results: ${results.cors.details}.`
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const syncSpinner = ora({
|
|
402
|
+
text: 'Synchronizing automated scores to CyberSentinel platform...',
|
|
403
|
+
spinner: SNAKE_SPINNER
|
|
404
|
+
}).start();
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
await client.patch(`/assessments/${assessmentId}`, {
|
|
408
|
+
scores: JSON.stringify(scores)
|
|
409
|
+
});
|
|
410
|
+
syncSpinner.succeed(chalk.green('Automated scan ratings synchronized!'));
|
|
411
|
+
} catch (err) {
|
|
412
|
+
syncSpinner.fail(chalk.red('Failed to synchronize automated ratings.'));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// STEP 2: MANUAL AUDIT
|
|
416
|
+
console.log(chalk.green.bold('\n--- Phase 2: Manual Audit Control Walkthrough ---'));
|
|
417
|
+
let template;
|
|
418
|
+
try {
|
|
419
|
+
const response = await client.get('/assessments/template');
|
|
420
|
+
template = decryptResponse(response.data);
|
|
421
|
+
} catch (err) {
|
|
422
|
+
console.error(chalk.red(`Failed to fetch checklist template: ${err.message}`));
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Let user choose a category to audit
|
|
427
|
+
const catChoice = await inquirer.prompt([
|
|
428
|
+
{
|
|
429
|
+
type: 'list',
|
|
430
|
+
name: 'categoryId',
|
|
431
|
+
message: 'Select checklist category to audit:',
|
|
432
|
+
choices: [
|
|
433
|
+
...template.map(c => ({ name: `${c.id}. ${c.name}`, value: c.id })),
|
|
434
|
+
{ name: chalk.yellow('Exit Audit Session'), value: -1 }
|
|
435
|
+
]
|
|
436
|
+
}
|
|
437
|
+
]);
|
|
438
|
+
|
|
439
|
+
if (catChoice.categoryId === -1) {
|
|
440
|
+
console.log(chalk.yellow('Audit session exited.'));
|
|
441
|
+
process.exit(0);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const selectedCategory = template.find(c => c.id === catChoice.categoryId);
|
|
445
|
+
console.log(chalk.green.bold(`\nAuditing Category: ${selectedCategory.id}. ${selectedCategory.name}\n`));
|
|
446
|
+
|
|
447
|
+
for (const item of selectedCategory.items) {
|
|
448
|
+
console.log(chalk.white(`--------------------------------------------------`));
|
|
449
|
+
console.log(chalk.green.bold(`Item ID: ${item.id}`));
|
|
450
|
+
console.log(chalk.green(`Criteria: ${item.criteria}`));
|
|
451
|
+
|
|
452
|
+
const currentVal = scores[item.id] || { rating: '', notes: '' };
|
|
453
|
+
if (currentVal.rating) {
|
|
454
|
+
console.log(chalk.gray(`Current: Rating: ${currentVal.rating} | Notes: ${currentVal.notes}`));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const itemAnswers = await inquirer.prompt([
|
|
458
|
+
{
|
|
459
|
+
type: 'list',
|
|
460
|
+
name: 'rating',
|
|
461
|
+
message: 'Rate implementing control quality:',
|
|
462
|
+
choices: ['5', '4', '3', '2', '1', 'N/A'],
|
|
463
|
+
default: currentVal.rating || '5'
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
type: 'input',
|
|
467
|
+
name: 'notes',
|
|
468
|
+
message: 'Add auditor notes / findings / evidence:',
|
|
469
|
+
default: currentVal.notes || ''
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
type: 'confirm',
|
|
473
|
+
name: 'hasEvidence',
|
|
474
|
+
message: 'Do you want to upload a local evidence file (image/pdf)?',
|
|
475
|
+
default: false
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
type: 'input',
|
|
479
|
+
name: 'evidencePath',
|
|
480
|
+
message: 'Enter absolute path to the evidence file:',
|
|
481
|
+
validate: (val) => fs.existsSync(val) ? true : 'File does not exist at path!',
|
|
482
|
+
when: (ans) => ans.hasEvidence
|
|
483
|
+
}
|
|
484
|
+
]);
|
|
485
|
+
|
|
486
|
+
let finalNotes = itemAnswers.notes;
|
|
487
|
+
|
|
488
|
+
// Handle Evidence Upload
|
|
489
|
+
if (itemAnswers.hasEvidence && itemAnswers.evidencePath) {
|
|
490
|
+
const uploadSpinner = ora({
|
|
491
|
+
text: 'Uploading evidence file to Supabase storage...',
|
|
492
|
+
spinner: SNAKE_SPINNER
|
|
493
|
+
}).start();
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
const FormData = require('form-data');
|
|
497
|
+
const form = new FormData();
|
|
498
|
+
form.append('file', fs.createReadStream(itemAnswers.evidencePath));
|
|
499
|
+
|
|
500
|
+
const uploadResponse = await client.post(`/assessments/${assessmentId}/evidence`, form, {
|
|
501
|
+
headers: {
|
|
502
|
+
...form.getHeaders()
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const decryptedUpload = decryptResponse(uploadResponse.data);
|
|
507
|
+
uploadSpinner.succeed(chalk.green('Evidence file uploaded successfully!'));
|
|
508
|
+
const fileRef = `[Evidence Reference: ${decryptedUpload.filePath}]`;
|
|
509
|
+
finalNotes = finalNotes ? `${finalNotes} ${fileRef}` : fileRef;
|
|
510
|
+
} catch (uploadErr) {
|
|
511
|
+
uploadSpinner.fail(chalk.red('Failed to upload evidence file.'));
|
|
512
|
+
console.error(chalk.red(`Upload error: ${uploadErr.message}`));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Update and Sync
|
|
517
|
+
scores[item.id] = {
|
|
518
|
+
rating: itemAnswers.rating,
|
|
519
|
+
notes: finalNotes
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const itemSyncSpinner = ora({
|
|
523
|
+
text: 'Saving item state...',
|
|
524
|
+
spinner: SNAKE_SPINNER
|
|
525
|
+
}).start();
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
await client.patch(`/assessments/${assessmentId}`, {
|
|
529
|
+
scores: JSON.stringify(scores)
|
|
530
|
+
});
|
|
531
|
+
itemSyncSpinner.succeed(chalk.green('Item state synchronized.'));
|
|
532
|
+
} catch (err) {
|
|
533
|
+
itemSyncSpinner.fail(chalk.red('Failed to save item.'));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
console.log(chalk.green.bold(`\n✔ Completed Category ${selectedCategory.id} Audit!\n`));
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cybersentinel-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CyberSentinel Cybersecurity Auditor CLI Tool",
|
|
5
|
+
"main": "bin/index.js",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"cybersentinel": "bin/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/index.js"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"axios": "^1.6.8",
|
|
15
|
+
"chalk": "^4.1.2",
|
|
16
|
+
"commander": "^12.0.0",
|
|
17
|
+
"form-data": "^4.0.0",
|
|
18
|
+
"inquirer": "^8.2.6",
|
|
19
|
+
"node-forge": "^1.3.1",
|
|
20
|
+
"ora": "^5.4.1"
|
|
21
|
+
}
|
|
22
|
+
}
|