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.
Files changed (2) hide show
  1. package/bin/index.js +540 -0
  2. 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
+ }