@hailbytes/vulnerability-calculator 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.
@@ -0,0 +1,862 @@
1
+ /**
2
+ * HailBytes Vulnerability Scanner Infrastructure Calculator
3
+ * Zero-dependency Web Component — pure ES module, no build step required.
4
+ *
5
+ * Usage: <hailbytes-vuln-calculator></hailbytes-vuln-calculator>
6
+ *
7
+ * Attributes:
8
+ * theme="dark|light" (default: dark)
9
+ *
10
+ * Events dispatched:
11
+ * vuln-calculated — fired on every calculation, detail = full result object
12
+ *
13
+ * License: MPL-2.0
14
+ */
15
+
16
+ // ─── Constants ───────────────────────────────────────────────────────────
17
+ const ASM_BASE_CPU = 4;
18
+ const ASM_BASE_RAM = 8; // GB
19
+ const ASM_BASE_STORAGE = 50; // GB
20
+ const SECURITY_ENGINEER_HOURLY_RATE = 75;
21
+ const MANAGED_SERVICE_MONTHLY_COST = 299;
22
+
23
+ const TOOL_LICENSING_ANNUAL = {
24
+ hailbytes_asm: 0,
25
+ openvas: 0,
26
+ nessus_professional: 3990,
27
+ qualys_vmdr: 3500,
28
+ };
29
+
30
+ const TOOL_MANAGEMENT_MONTHLY = { // hours * $75/hr already multiplied
31
+ hailbytes_asm: 450,
32
+ openvas: 450,
33
+ nessus_professional: 300,
34
+ qualys_vmdr: 225,
35
+ };
36
+
37
+ const TOOL_SETUP_HOURS = {
38
+ hailbytes_asm: 8,
39
+ openvas: 10,
40
+ nessus_professional: 6,
41
+ qualys_vmdr: 4,
42
+ };
43
+
44
+ const ASM_INTENSITY_MULT = { light: 1.0, medium: 1.3, aggressive: 1.8, continuous: 2.2 };
45
+ const TRADITIONAL_INTENSITY_MULT = { light: 1.0, medium: 1.5, aggressive: 2.5, continuous: 3.0 };
46
+ const FREQUENCY_MULT = { daily: 1.5, weekly: 1.0, monthly: 0.8, quarterly: 0.6 };
47
+ const TIME_MULT = { light: 0.5, medium: 1.0, aggressive: 2.0, continuous: 0.3 };
48
+
49
+ // ─── Calculation engine ────────────────────────────────────────────────────────
50
+
51
+ function calculateResources(data) {
52
+ const hasAsm = data.scanning_tools.includes('hailbytes_asm');
53
+
54
+ const vmResources = hasAsm
55
+ ? calcAsmResources(data)
56
+ : calcTraditionalResources(data);
57
+
58
+ const timing = calcTiming(data, vmResources);
59
+ const costs = calcCosts(vmResources, data);
60
+ const recommendations = generateRecommendations(data, vmResources);
61
+
62
+ return {
63
+ vm_resources: vmResources,
64
+ timing,
65
+ costs,
66
+ recommendations,
67
+ has_asm: hasAsm,
68
+ inputs: { ...data },
69
+ timestamp: new Date().toISOString(),
70
+ };
71
+ }
72
+
73
+ function calcAsmResources(data) {
74
+ const intensityMult = ASM_INTENSITY_MULT[data.scan_intensity] ?? 1.0;
75
+ const frequencyMult = FREQUENCY_MULT[data.scan_frequency] ?? 1.0;
76
+ const complianceFactor = 1.0 + (data.compliance_needs.length * 0.1);
77
+ const totalMultiplier = intensityMult * frequencyMult * complianceFactor;
78
+
79
+ const hostFactor = Math.max(1, data.target_hosts / 1000);
80
+ const cpuCores = Math.max(2, Math.ceil(ASM_BASE_CPU * hostFactor * totalMultiplier));
81
+ const ramGb = Math.max(4, Math.ceil(ASM_BASE_RAM * hostFactor * totalMultiplier));
82
+ const ramRecommended = Math.ceil(ramGb * 1.5);
83
+ const storageGb = Math.max(20, Math.ceil(ASM_BASE_STORAGE + (data.target_hosts / 100 * 2) * complianceFactor));
84
+ const networkMbps = Math.max(10, Math.ceil(data.target_hosts / 200 * intensityMult * complianceFactor));
85
+
86
+ return {
87
+ cpu_cores: cpuCores,
88
+ ram_gb: ramGb,
89
+ ram_recommended: ramRecommended,
90
+ storage_gb: storageGb,
91
+ network_bandwidth_mbps: networkMbps,
92
+ docker_required: true,
93
+ tool_type: 'hailbytes_asm',
94
+ scaling_info: {
95
+ host_factor: +hostFactor.toFixed(2),
96
+ total_multiplier: +totalMultiplier.toFixed(2),
97
+ optimized_for: 'continuous_attack_surface_management',
98
+ },
99
+ };
100
+ }
101
+
102
+ function calcTraditionalResources(data) {
103
+ const intensityMult = TRADITIONAL_INTENSITY_MULT[data.scan_intensity] ?? 1.5;
104
+ const hostFactor = Math.max(0.001, data.target_hosts / 1000);
105
+
106
+ const cpuCores = Math.max(2, Math.ceil(4 * hostFactor * intensityMult));
107
+ const ramGb = Math.max(4, Math.ceil(8 * hostFactor * intensityMult));
108
+ const storageGb = Math.max(10, Math.ceil(0.5 * data.target_hosts / 1024));
109
+ const networkMbps = Math.max(10, Math.ceil(data.target_hosts / 100 * intensityMult));
110
+
111
+ return {
112
+ cpu_cores: cpuCores,
113
+ ram_gb: ramGb,
114
+ ram_recommended: Math.ceil(ramGb * 1.5),
115
+ storage_gb: storageGb,
116
+ network_bandwidth_mbps: networkMbps,
117
+ docker_required: false,
118
+ tool_type: 'traditional',
119
+ scaling_info: {
120
+ host_factor: +hostFactor.toFixed(3),
121
+ intensity_multiplier: intensityMult,
122
+ optimized_for: 'scheduled_scanning',
123
+ },
124
+ };
125
+ }
126
+
127
+ function calcTiming(data, vmRes) {
128
+ const baseScanTime = vmRes.tool_type === 'hailbytes_asm' ? 1.5 : 2.0; // min/host
129
+ const timeMult = TIME_MULT[data.scan_intensity] ?? 1.0;
130
+
131
+ const totalScanTime = baseScanTime * data.target_hosts * timeMult;
132
+ const parallelHosts = Math.min(data.target_hosts, vmRes.cpu_cores * 100);
133
+ const optimizedScanTime = Math.ceil(totalScanTime / Math.max(1, parallelHosts / 100));
134
+ const windowUtilization = Math.min(100, (optimizedScanTime / (data.scan_window * 60)) * 100);
135
+
136
+ const efficiencyRating = windowUtilization <= 60 ? 'excellent'
137
+ : windowUtilization <= 80 ? 'good'
138
+ : windowUtilization <= 95 ? 'acceptable'
139
+ : 'poor';
140
+
141
+ const bottlenecks = [];
142
+ if (vmRes.cpu_cores < 4 && data.target_hosts > 1000) bottlenecks.push('cpu_insufficient');
143
+ if (vmRes.ram_gb < 8 && data.scan_intensity === 'aggressive') bottlenecks.push('memory_constraint');
144
+ if (data.scan_window < 4 && data.target_hosts > 2000) bottlenecks.push('time_window_too_small');
145
+
146
+ const optimizationSuggestions = [];
147
+ if (windowUtilization > 90) {
148
+ optimizationSuggestions.push('increase_scan_window');
149
+ optimizationSuggestions.push('reduce_scan_intensity');
150
+ }
151
+ if (data.target_hosts > 5000) optimizationSuggestions.push('implement_distributed_scanning');
152
+ if (data.scan_intensity === 'aggressive' && data.compliance_needs.length > 0) {
153
+ optimizationSuggestions.push('schedule_compliance_scans_separately');
154
+ }
155
+
156
+ return {
157
+ total_scan_time_minutes: Math.ceil(totalScanTime),
158
+ optimized_scan_time_minutes: optimizedScanTime,
159
+ parallel_hosts: parallelHosts,
160
+ scan_window_utilization: +windowUtilization.toFixed(1),
161
+ performance_metrics: {
162
+ efficiency_rating: efficiencyRating,
163
+ bottleneck_analysis: bottlenecks,
164
+ optimization_suggestions: optimizationSuggestions,
165
+ },
166
+ };
167
+ }
168
+
169
+ function calcCosts(vmRes, data) {
170
+ // Infrastructure
171
+ const cpuFactor = vmRes.cpu_cores / 4.0;
172
+ const ramFactor = vmRes.ram_gb / 8.0;
173
+ const scaleFactor = Math.max(cpuFactor, ramFactor);
174
+ const hoursPerMonth = 730; // 24 * 30.44 ≈ 730
175
+
176
+ const awsCompute = 0.17 * scaleFactor * hoursPerMonth;
177
+ const azureCompute = 0.16 * scaleFactor * hoursPerMonth;
178
+ const awsStorage = vmRes.storage_gb * 0.10;
179
+ const azureStorage = vmRes.storage_gb * 0.12;
180
+
181
+ const awsMonthly = Math.ceil(awsCompute + awsStorage);
182
+ const azureMonthly = Math.ceil(azureCompute + azureStorage);
183
+
184
+ // Tool costs
185
+ let licensingAnnual = 0;
186
+ let managementMonthly = 0;
187
+ let setupCost = 0;
188
+ const toolBreakdown = {};
189
+
190
+ for (const tool of data.scanning_tools) {
191
+ const lic = TOOL_LICENSING_ANNUAL[tool] ?? 0;
192
+ const mgmt = TOOL_MANAGEMENT_MONTHLY[tool] ?? 300;
193
+ const sh = TOOL_SETUP_HOURS[tool] ?? 8;
194
+ const sc = sh * SECURITY_ENGINEER_HOURLY_RATE;
195
+
196
+ licensingAnnual += lic;
197
+ managementMonthly += mgmt;
198
+ setupCost += sc;
199
+
200
+ toolBreakdown[tool] = {
201
+ licensing_annual: lic,
202
+ licensing_monthly: +(lic / 12).toFixed(2),
203
+ management_monthly: mgmt,
204
+ management_hours_monthly: +(mgmt / SECURITY_ENGINEER_HOURLY_RATE).toFixed(1),
205
+ setup_cost: sc,
206
+ setup_hours: sh,
207
+ total_monthly: +((lic / 12) + mgmt).toFixed(2),
208
+ };
209
+ }
210
+
211
+ const licensingMonthly = +(licensingAnnual / 12).toFixed(2);
212
+ const totalMonthly = +(licensingMonthly + managementMonthly).toFixed(2);
213
+
214
+ // ROI
215
+ const hasAsm = data.scanning_tools.includes('hailbytes_asm');
216
+ const selfManagedMonthly = awsMonthly + totalMonthly;
217
+ const selfManagedAnnual = selfManagedMonthly * 12;
218
+
219
+ let roiAnalysis;
220
+ if (hasAsm) {
221
+ const monthlySavings = Math.max(0, selfManagedMonthly - MANAGED_SERVICE_MONTHLY_COST);
222
+ const annualSavings = monthlySavings * 12;
223
+ const roiPct = MANAGED_SERVICE_MONTHLY_COST * 12 > 0
224
+ ? +((annualSavings / (MANAGED_SERVICE_MONTHLY_COST * 12)) * 100).toFixed(1)
225
+ : 0;
226
+
227
+ roiAnalysis = {
228
+ self_managed_monthly: +selfManagedMonthly.toFixed(2),
229
+ self_managed_annual: +selfManagedAnnual.toFixed(2),
230
+ managed_monthly: MANAGED_SERVICE_MONTHLY_COST,
231
+ managed_annual: MANAGED_SERVICE_MONTHLY_COST * 12,
232
+ monthly_savings: +monthlySavings.toFixed(2),
233
+ annual_savings: +annualSavings.toFixed(2),
234
+ roi_percentage: roiPct,
235
+ has_managed_option: true,
236
+ setup_cost: setupCost,
237
+ };
238
+ } else {
239
+ roiAnalysis = {
240
+ self_managed_monthly: +selfManagedMonthly.toFixed(2),
241
+ self_managed_annual: +selfManagedAnnual.toFixed(2),
242
+ has_managed_option: false,
243
+ setup_cost: setupCost,
244
+ };
245
+ }
246
+
247
+ return {
248
+ infrastructure_monthly_aws: awsMonthly,
249
+ infrastructure_monthly_azure: azureMonthly,
250
+ tool_licensing_annual: licensingAnnual,
251
+ tool_licensing_monthly: licensingMonthly,
252
+ tool_management_monthly: managementMonthly,
253
+ tool_setup_cost: setupCost,
254
+ total_monthly_aws: awsMonthly + totalMonthly,
255
+ total_monthly_azure: azureMonthly + totalMonthly,
256
+ roi_analysis: roiAnalysis,
257
+ tool_breakdown: toolBreakdown,
258
+ };
259
+ }
260
+
261
+ function generateRecommendations(data, vmRes) {
262
+ const recs = [];
263
+
264
+ // Tool-specific
265
+ if (data.scanning_tools.includes('hailbytes_asm')) {
266
+ recs.push('HailBytes ASM excels at continuous attack surface management — consider daily or weekly scanning for optimal coverage');
267
+ recs.push('Ensure Docker is properly secured and regularly updated for HailBytes ASM deployment');
268
+ recs.push('Configure asset data retention policies to manage storage growth effectively');
269
+ recs.push('Consider the HailBytes ASM managed service to eliminate infrastructure setup and maintenance overhead');
270
+ }
271
+ if (data.scanning_tools.includes('openvas')) {
272
+ recs.push('OpenVAS requires regular feed updates — schedule automatic vulnerability database updates');
273
+ recs.push('Consider OpenVAS enterprise support for production environments');
274
+ }
275
+ if (data.scanning_tools.length > 1) {
276
+ recs.push('Multiple scanning tools detected — implement result correlation and deduplication');
277
+ }
278
+ if (!data.scanning_tools.includes('hailbytes_asm')) {
279
+ recs.push('Consider HailBytes ASM for continuous attack surface visibility alongside point-in-time scanning tools');
280
+ }
281
+
282
+ // Scale
283
+ if (data.target_hosts > 10000) {
284
+ recs.push('Large environment detected — consider distributed scanning architecture');
285
+ recs.push('Implement network segmentation-aware scanning strategies');
286
+ }
287
+ if (vmRes.cpu_cores > 16) recs.push('High CPU requirement — multiple smaller scanner instances may provide better performance');
288
+ if (vmRes.ram_gb > 32) recs.push('High memory requirement — consider memory optimization and result streaming');
289
+
290
+ // Performance
291
+ if (data.scan_intensity === 'aggressive') {
292
+ recs.push('Aggressive scanning — implement scan throttling during business hours');
293
+ recs.push('Monitor target system impact and adjust scan policies accordingly');
294
+ }
295
+ if (data.scan_window < 6) recs.push('Short scan window detected — consider extending maintenance windows for comprehensive scanning');
296
+ if (data.scan_frequency === 'daily') recs.push('Daily scanning requires robust infrastructure — ensure adequate monitoring and alerting');
297
+
298
+ // Compliance
299
+ if (data.compliance_needs.length > 2) {
300
+ recs.push('Multiple compliance requirements — implement centralized reporting dashboard');
301
+ recs.push('Consider compliance-specific scan scheduling to meet different requirements');
302
+ }
303
+ if (data.compliance_needs.includes('pci')) recs.push('PCI DSS requires quarterly external scans by approved scanning vendor (ASV)');
304
+ if (data.compliance_needs.includes('nist')) recs.push('NIST framework — integrate vulnerability management with risk assessment processes');
305
+
306
+ // Security baseline
307
+ recs.push('Implement network segmentation for scanner placement and access control');
308
+ recs.push('Regular scanner patching and security hardening is essential');
309
+ recs.push('Use encrypted channels for scan result transmission and storage');
310
+ if (vmRes.docker_required) recs.push('Docker security: regularly update base images and implement container security scanning');
311
+ if (data.target_hosts > 1000) recs.push('Large target environment — implement scan result encryption and secure storage');
312
+
313
+ return [...new Set(recs)]; // deduplicate
314
+ }
315
+
316
+ // ─── Styles ───────────────────────────────────────────────────────────────────
317
+
318
+ const STYLES = `
319
+ :host {
320
+ --accent: #ff6b35;
321
+ --accent-hover: #e85d2a;
322
+ --bg: #1a1a2e;
323
+ --bg-card: #16213e;
324
+ --bg-input: #0f3460;
325
+ --border: #2a2a4a;
326
+ --text: #e8e8f0;
327
+ --text-muted: #8888aa;
328
+ --success: #4caf50;
329
+ --warning: #ff9800;
330
+ --info: #2196f3;
331
+ --radius: 10px;
332
+ display: block;
333
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
334
+ color: var(--text);
335
+ background: var(--bg);
336
+ border-radius: 16px;
337
+ overflow: hidden;
338
+ }
339
+ :host([theme="light"]) {
340
+ --bg: #f5f5f5;
341
+ --bg-card: #ffffff;
342
+ --bg-input: #f0f0f8;
343
+ --border: #ddd;
344
+ --text: #1a1a2e;
345
+ --text-muted:#555570;
346
+ }
347
+ * { box-sizing: border-box; margin: 0; padding: 0; }
348
+
349
+ :host([branding="off"]) .hb-branding,
350
+ :host([branding="off"]) .hailbytes-badge { display: none; }
351
+
352
+ /* Footer */
353
+ .hb-footer {
354
+ padding: 14px 32px;
355
+ text-align: center;
356
+ font-size: 0.82rem;
357
+ color: var(--text-muted);
358
+ border-top: 1px solid var(--border);
359
+ }
360
+ .hb-footer a {
361
+ color: var(--accent);
362
+ text-decoration: none;
363
+ font-weight: 600;
364
+ }
365
+ .hb-footer a:hover { text-decoration: underline; }
366
+
367
+ /* Header */
368
+ .header {
369
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
370
+ padding: 28px 32px 20px;
371
+ border-bottom: 2px solid var(--accent);
372
+ }
373
+ .header h1 {
374
+ font-size: 1.45rem; font-weight: 700; color: #fff;
375
+ display: flex; align-items: center; gap: 10px; margin-bottom: 6px;
376
+ }
377
+ .header h1 .icon { color: var(--accent); }
378
+ .header p { color: #aab; font-size: 0.87rem; }
379
+ .hailbytes-badge {
380
+ display: inline-block; background: var(--accent); color: #fff;
381
+ font-size: 0.64rem; font-weight: 700; letter-spacing: 1px;
382
+ padding: 2px 8px; border-radius: 4px; vertical-align: middle;
383
+ margin-left: 4px; text-transform: uppercase;
384
+ }
385
+
386
+ /* Layout */
387
+ .body { display: grid; grid-template-columns: 300px 1fr; min-height: 480px; }
388
+ @media (max-width: 700px) { .body { grid-template-columns: 1fr; } }
389
+
390
+ /* Form panel */
391
+ .form-panel {
392
+ padding: 24px;
393
+ background: var(--bg-card);
394
+ border-right: 1px solid var(--border);
395
+ display: flex; flex-direction: column; gap: 16px;
396
+ }
397
+ @media (max-width: 700px) { .form-panel { border-right: none; border-bottom: 1px solid var(--border); } }
398
+
399
+ .section-label {
400
+ font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
401
+ letter-spacing: 0.8px; color: var(--accent); margin-bottom: 8px;
402
+ }
403
+ .field { display: flex; flex-direction: column; gap: 5px; }
404
+ label.field-label {
405
+ font-size: 0.79rem; font-weight: 600; color: var(--text-muted);
406
+ text-transform: uppercase; letter-spacing: 0.4px;
407
+ }
408
+
409
+ input[type="number"], select {
410
+ background: var(--bg-input); border: 1px solid var(--border);
411
+ border-radius: 8px; color: var(--text); font-size: 0.93rem;
412
+ padding: 9px 12px; width: 100%; outline: none;
413
+ transition: border-color 0.2s, box-shadow 0.2s;
414
+ appearance: none; -webkit-appearance: none;
415
+ }
416
+ input[type="number"]:focus, select:focus {
417
+ border-color: var(--accent);
418
+ box-shadow: 0 0 0 3px rgba(255,107,53,0.2);
419
+ }
420
+ select {
421
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ff6b35'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
422
+ background-repeat: no-repeat; background-position: right 8px center;
423
+ background-size: 20px; padding-right: 32px; cursor: pointer;
424
+ }
425
+ select option { background: #1a1a2e; color: #e8e8f0; }
426
+
427
+ /* Checkbox groups */
428
+ .cb-list { display: flex; flex-direction: column; gap: 4px; }
429
+ .cb-item {
430
+ display: flex; align-items: center; gap: 8px;
431
+ background: var(--bg-input); border: 1px solid var(--border);
432
+ border-radius: 6px; padding: 7px 10px; cursor: pointer;
433
+ transition: border-color 0.2s; user-select: none; font-size: 0.84rem;
434
+ }
435
+ .cb-item:hover { border-color: var(--accent); }
436
+ .cb-item.checked { border-color: var(--accent); background: rgba(255,107,53,0.07); }
437
+ .cb-item input[type="checkbox"] {
438
+ width: 15px; height: 15px; accent-color: var(--accent);
439
+ cursor: pointer; flex-shrink: 0;
440
+ }
441
+
442
+ /* Calculate button */
443
+ .calc-btn {
444
+ display: flex; align-items: center; justify-content: center; gap: 8px;
445
+ width: 100%; padding: 12px; border: none; border-radius: 8px;
446
+ background: var(--accent); color: #fff; font-size: 0.97rem;
447
+ font-weight: 700; cursor: pointer; transition: all 0.2s; margin-top: 4px;
448
+ }
449
+ .calc-btn:hover {
450
+ background: var(--accent-hover);
451
+ box-shadow: 0 4px 16px rgba(255,107,53,0.4);
452
+ transform: translateY(-1px);
453
+ }
454
+
455
+ /* Results panel */
456
+ .results-panel {
457
+ padding: 24px;
458
+ background: var(--bg);
459
+ overflow-y: auto;
460
+ }
461
+ .results-placeholder {
462
+ display: flex; flex-direction: column; align-items: center;
463
+ justify-content: center; height: 100%; gap: 12px;
464
+ color: var(--text-muted); text-align: center; padding: 40px;
465
+ }
466
+ .results-placeholder .big-icon { font-size: 3rem; opacity: 0.3; }
467
+ .results-placeholder p { font-size: 0.9rem; }
468
+
469
+ /* Cards */
470
+ .cards-grid {
471
+ display: grid;
472
+ grid-template-columns: 1fr 1fr;
473
+ gap: 12px;
474
+ }
475
+ @media (max-width: 480px) { .cards-grid { grid-template-columns: 1fr; } }
476
+
477
+ .card {
478
+ background: var(--bg-card); border: 1px solid var(--border);
479
+ border-radius: var(--radius); padding: 16px;
480
+ }
481
+ .card.full-width { grid-column: 1 / -1; }
482
+ .card.accent-border { border-color: var(--accent); }
483
+ .card.roi-card {
484
+ grid-column: 1 / -1;
485
+ background: rgba(255,107,53,0.05);
486
+ border-color: var(--accent);
487
+ }
488
+
489
+ .card-title {
490
+ font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
491
+ letter-spacing: 0.8px; color: var(--text-muted); margin-bottom: 12px;
492
+ display: flex; align-items: center; gap: 6px;
493
+ }
494
+ .card-title .icon { font-size: 1rem; }
495
+
496
+ .metric-row {
497
+ display: flex; justify-content: space-between; align-items: center;
498
+ padding: 6px 0; border-bottom: 1px solid var(--border); font-size: 0.85rem;
499
+ }
500
+ .metric-row:last-child { border-bottom: none; }
501
+ .metric-label { color: var(--text-muted); }
502
+ .metric-value { color: var(--text); font-weight: 600; }
503
+ .metric-value.accent { color: var(--accent); }
504
+ .metric-value.success { color: var(--success); }
505
+ .metric-value.warning { color: var(--warning); }
506
+
507
+ /* Efficiency badge */
508
+ .badge {
509
+ display: inline-block; padding: 2px 10px; border-radius: 12px;
510
+ font-size: 0.74rem; font-weight: 700; text-transform: uppercase;
511
+ }
512
+ .badge.excellent { background: rgba(76,175,80,0.15); color: var(--success); }
513
+ .badge.good { background: rgba(33,150,243,0.15); color: var(--info); }
514
+ .badge.acceptable{ background: rgba(255,152,0,0.15); color: var(--warning); }
515
+ .badge.poor { background: rgba(244,67,54,0.15); color: #f44336; }
516
+
517
+ /* ROI comparison */
518
+ .roi-compare {
519
+ display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 10px;
520
+ }
521
+ @media (max-width: 400px) { .roi-compare { grid-template-columns: 1fr; } }
522
+ .roi-side {
523
+ padding: 12px; border-radius: 8px; border: 1px solid var(--border);
524
+ text-align: center;
525
+ }
526
+ .roi-side.managed { border-color: var(--success); background: rgba(76,175,80,0.05); }
527
+ .roi-side h4 { font-size: 0.78rem; color: var(--text-muted); margin-bottom: 8px; }
528
+ .roi-price {
529
+ font-size: 1.5rem; font-weight: 800; color: var(--text); margin-bottom: 4px;
530
+ }
531
+ .roi-side.managed .roi-price { color: var(--success); }
532
+ .roi-savings {
533
+ margin-top: 10px; padding: 10px; background: rgba(76,175,80,0.08);
534
+ border-radius: 6px; text-align: center; font-size: 0.84rem; color: var(--success);
535
+ font-weight: 700;
536
+ }
537
+
538
+ /* Recommendations */
539
+ .rec-list {
540
+ list-style: none; display: flex; flex-direction: column; gap: 6px;
541
+ margin-top: 4px;
542
+ }
543
+ .rec-list li {
544
+ display: flex; gap: 8px; font-size: 0.83rem; color: var(--text);
545
+ background: var(--bg-input); padding: 8px 10px; border-radius: 6px;
546
+ border-left: 3px solid var(--accent);
547
+ }
548
+ .rec-list li::before { content: '→'; color: var(--accent); flex-shrink: 0; }
549
+ `;
550
+
551
+ // ─── Template ─────────────────────────────────────────────────────────────────
552
+
553
+ const TMPL = document.createElement('template');
554
+ TMPL.innerHTML = `<style>${STYLES}</style>
555
+ <div class="header">
556
+ <h1><span class="icon">🔍</span> Vulnerability Scanner Infrastructure Calculator <span class="hailbytes-badge">HailBytes</span></h1>
557
+ <p>Size your scanning infrastructure — estimate VM requirements, cloud costs, and ROI entirely in the browser</p>
558
+ </div>
559
+
560
+ <div class="body">
561
+ <!-- FORM PANEL -->
562
+ <div class="form-panel">
563
+
564
+ <div>
565
+ <div class="section-label">🎯 Target</div>
566
+ <div class="field">
567
+ <label class="field-label" for="target_hosts">Target Hosts</label>
568
+ <input type="number" id="target_hosts" min="1" max="50000" value="1000" placeholder="e.g. 1000">
569
+ </div>
570
+ </div>
571
+
572
+ <div>
573
+ <div class="section-label">⚡ Scan Config</div>
574
+ <div class="field" style="margin-bottom:8px">
575
+ <label class="field-label" for="scan_intensity">Intensity</label>
576
+ <select id="scan_intensity">
577
+ <option value="light">Light</option>
578
+ <option value="medium" selected>Medium</option>
579
+ <option value="aggressive">Aggressive</option>
580
+ <option value="continuous">Continuous</option>
581
+ </select>
582
+ </div>
583
+ <div class="field" style="margin-bottom:8px">
584
+ <label class="field-label" for="scan_frequency">Frequency</label>
585
+ <select id="scan_frequency">
586
+ <option value="daily">Daily</option>
587
+ <option value="weekly" selected>Weekly</option>
588
+ <option value="monthly">Monthly</option>
589
+ <option value="quarterly">Quarterly</option>
590
+ </select>
591
+ </div>
592
+ <div class="field">
593
+ <label class="field-label" for="scan_window">Scan Window (hours)</label>
594
+ <input type="number" id="scan_window" min="1" max="24" value="8" placeholder="1–24">
595
+ </div>
596
+ </div>
597
+
598
+ <div>
599
+ <div class="section-label">🛠️ Scanning Tools</div>
600
+ <div class="cb-list" id="tools-list">
601
+ <label class="cb-item checked">
602
+ <input type="checkbox" name="tool" value="hailbytes_asm" checked>HailBytes ASM (free, self-hosted)
603
+ </label>
604
+ <label class="cb-item">
605
+ <input type="checkbox" name="tool" value="openvas">OpenVAS (free)
606
+ </label>
607
+ <label class="cb-item">
608
+ <input type="checkbox" name="tool" value="nessus_professional">Nessus Professional
609
+ </label>
610
+ <label class="cb-item">
611
+ <input type="checkbox" name="tool" value="qualys_vmdr">Qualys VMDR
612
+ </label>
613
+ </div>
614
+ </div>
615
+
616
+ <div>
617
+ <div class="section-label">📋 Compliance</div>
618
+ <div class="cb-list" id="compliance-list">
619
+ <label class="cb-item"><input type="checkbox" name="compliance" value="pci">PCI DSS</label>
620
+ <label class="cb-item"><input type="checkbox" name="compliance" value="hipaa">HIPAA</label>
621
+ <label class="cb-item"><input type="checkbox" name="compliance" value="nist">NIST</label>
622
+ <label class="cb-item"><input type="checkbox" name="compliance" value="iso27001">ISO 27001</label>
623
+ <label class="cb-item"><input type="checkbox" name="compliance" value="soc2">SOC 2</label>
624
+ </div>
625
+ </div>
626
+
627
+ <button class="calc-btn" id="calc-btn">⚡ Calculate Infrastructure</button>
628
+ </div>
629
+
630
+ <!-- RESULTS PANEL -->
631
+ <div class="results-panel" id="results-panel">
632
+ <div class="results-placeholder" id="placeholder">
633
+ <div class="big-icon">🖥️</div>
634
+ <p>Configure your scan parameters and click <strong>Calculate Infrastructure</strong> to size your VM requirements.</p>
635
+ </div>
636
+ <div id="results-content" style="display:none">
637
+ <div class="cards-grid" id="cards"></div>
638
+ </div>
639
+ </div>
640
+ <div class="hb-footer hb-branding">
641
+ by <a href="https://hailbytes.com/asm" target="_blank" rel="noopener">HailBytes</a> — managed attack surface management
642
+ </div>
643
+ </div>`;
644
+
645
+ // ─── Web Component ────────────────────────────────────────────────────────────
646
+
647
+ class HailbytesVulnCalculator extends HTMLElement {
648
+ static get observedAttributes() { return ['theme', 'branding']; }
649
+
650
+ constructor() {
651
+ super();
652
+ this._shadow = this.attachShadow({ mode: 'open' });
653
+ this._shadow.appendChild(TMPL.content.cloneNode(true));
654
+ this._lastResult = null;
655
+ }
656
+
657
+ connectedCallback() {
658
+ this._bindEvents();
659
+ }
660
+
661
+ _bindEvents() {
662
+ const s = this._shadow;
663
+ s.getElementById('calc-btn').addEventListener('click', () => this._run());
664
+
665
+ // Checkbox visual state
666
+ s.querySelectorAll('.cb-item input[type="checkbox"]').forEach(cb => {
667
+ cb.addEventListener('change', () => {
668
+ cb.closest('.cb-item').classList.toggle('checked', cb.checked);
669
+ });
670
+ });
671
+ }
672
+
673
+ _collectInputs() {
674
+ const s = this._shadow;
675
+ return {
676
+ target_hosts: Math.min(50000, Math.max(1, parseInt(s.getElementById('target_hosts').value) || 1000)),
677
+ scan_intensity: s.getElementById('scan_intensity').value,
678
+ scan_frequency: s.getElementById('scan_frequency').value,
679
+ scan_window: Math.min(24, Math.max(1, parseInt(s.getElementById('scan_window').value) || 8)),
680
+ scanning_tools: [...s.querySelectorAll('input[name="tool"]:checked')].map(c => c.value),
681
+ compliance_needs: [...s.querySelectorAll('input[name="compliance"]:checked')].map(c => c.value),
682
+ };
683
+ }
684
+
685
+ _run() {
686
+ const data = this._collectInputs();
687
+ if (data.scanning_tools.length === 0) {
688
+ data.scanning_tools = ['openvas']; // fallback
689
+ }
690
+
691
+ const result = calculateResources(data);
692
+ this._lastResult = result;
693
+ this._render(result);
694
+
695
+ this.dispatchEvent(new CustomEvent('vuln-calculated', {
696
+ bubbles: true, composed: true, detail: result
697
+ }));
698
+ }
699
+
700
+ _render(r) {
701
+ const s = this._shadow;
702
+ s.getElementById('placeholder').style.display = 'none';
703
+ const content = s.getElementById('results-content');
704
+ content.style.display = 'block';
705
+
706
+ const vm = r.vm_resources;
707
+ const t = r.timing;
708
+ const c = r.costs;
709
+ const eff = t.performance_metrics.efficiency_rating;
710
+ const util = t.scan_window_utilization;
711
+
712
+ const fmtMin = m => m >= 60
713
+ ? `${Math.floor(m/60)}h ${m % 60}m`
714
+ : `${m}m`;
715
+
716
+ const roiCard = r.has_asm ? `
717
+ <div class="card roi-card full-width">
718
+ <div class="card-title"><span class="icon">💰</span> ROI: Self-Managed vs HailBytes ASM Managed Service</div>
719
+ <div class="roi-compare">
720
+ <div class="roi-side">
721
+ <h4>Self-Managed (Cloud + Tools)</h4>
722
+ <div class="roi-price">$${c.roi_analysis.self_managed_monthly.toLocaleString()}/mo</div>
723
+ <div style="font-size:0.78rem;color:var(--text-muted)">$${c.roi_analysis.self_managed_annual.toLocaleString()}/yr</div>
724
+ </div>
725
+ <div class="roi-side managed">
726
+ <h4>HailBytes ASM Managed Service</h4>
727
+ <div class="roi-price">$${c.roi_analysis.managed_monthly.toLocaleString()}/mo</div>
728
+ <div style="font-size:0.78rem;color:var(--text-muted)">$${c.roi_analysis.managed_annual.toLocaleString()}/yr</div>
729
+ </div>
730
+ </div>
731
+ ${c.roi_analysis.monthly_savings > 0 ? `
732
+ <div class="roi-savings">
733
+ 💚 Save $${c.roi_analysis.monthly_savings.toLocaleString()}/month ($${c.roi_analysis.annual_savings.toLocaleString()}/year) with HailBytes ASM managed service
734
+ ${c.roi_analysis.roi_percentage > 0 ? ` — ${c.roi_analysis.roi_percentage}% ROI` : ''}
735
+ </div>` : ''}
736
+ </div>` : '';
737
+
738
+ const toolCostRows = Object.entries(c.tool_breakdown).map(([tool, tb]) => `
739
+ <div class="metric-row">
740
+ <span class="metric-label">${this._toolLabel(tool)}</span>
741
+ <span class="metric-value">$${tb.licensing_annual > 0 ? tb.licensing_annual.toLocaleString() + '/yr' : 'Free'} + $${tb.management_monthly}/mo mgmt</span>
742
+ </div>`).join('');
743
+
744
+ const recItems = r.recommendations.slice(0, 8).map(rec => `<li>${rec}</li>`).join('');
745
+
746
+ const cards = s.getElementById('cards');
747
+ cards.innerHTML = `
748
+ <!-- VM Requirements -->
749
+ <div class="card">
750
+ <div class="card-title"><span class="icon">🖥️</span> VM Requirements</div>
751
+ <div class="metric-row">
752
+ <span class="metric-label">CPU Cores</span>
753
+ <span class="metric-value accent">${vm.cpu_cores} cores</span>
754
+ </div>
755
+ <div class="metric-row">
756
+ <span class="metric-label">RAM (minimum)</span>
757
+ <span class="metric-value">${vm.ram_gb} GB</span>
758
+ </div>
759
+ <div class="metric-row">
760
+ <span class="metric-label">RAM (recommended)</span>
761
+ <span class="metric-value accent">${vm.ram_recommended} GB</span>
762
+ </div>
763
+ <div class="metric-row">
764
+ <span class="metric-label">Storage</span>
765
+ <span class="metric-value">${vm.storage_gb} GB</span>
766
+ </div>
767
+ <div class="metric-row">
768
+ <span class="metric-label">Network Bandwidth</span>
769
+ <span class="metric-value">${vm.network_bandwidth_mbps} Mbps</span>
770
+ </div>
771
+ ${vm.docker_required ? '<div class="metric-row"><span class="metric-label">Docker</span><span class="metric-value accent">Required</span></div>' : ''}
772
+ </div>
773
+
774
+ <!-- Timing -->
775
+ <div class="card">
776
+ <div class="card-title"><span class="icon">⏱️</span> Scan Timing</div>
777
+ <div class="metric-row">
778
+ <span class="metric-label">Total scan time</span>
779
+ <span class="metric-value">${fmtMin(t.total_scan_time_minutes)}</span>
780
+ </div>
781
+ <div class="metric-row">
782
+ <span class="metric-label">Optimized (parallel)</span>
783
+ <span class="metric-value accent">${fmtMin(t.optimized_scan_time_minutes)}</span>
784
+ </div>
785
+ <div class="metric-row">
786
+ <span class="metric-label">Parallel hosts</span>
787
+ <span class="metric-value">${t.parallel_hosts.toLocaleString()}</span>
788
+ </div>
789
+ <div class="metric-row">
790
+ <span class="metric-label">Window utilization</span>
791
+ <span class="metric-value ${util > 95 ? 'warning' : ''}">${util}%</span>
792
+ </div>
793
+ <div class="metric-row">
794
+ <span class="metric-label">Efficiency</span>
795
+ <span class="metric-value"><span class="badge ${eff}">${eff}</span></span>
796
+ </div>
797
+ </div>
798
+
799
+ <!-- Infrastructure Cost -->
800
+ <div class="card">
801
+ <div class="card-title"><span class="icon">☁️</span> Compute Cost</div>
802
+ <div class="metric-row">
803
+ <span class="metric-label">AWS (monthly)</span>
804
+ <span class="metric-value accent">$${c.infrastructure_monthly_aws.toLocaleString()}</span>
805
+ </div>
806
+ <div class="metric-row">
807
+ <span class="metric-label">Azure (monthly)</span>
808
+ <span class="metric-value">$${c.infrastructure_monthly_azure.toLocaleString()}</span>
809
+ </div>
810
+ <div class="metric-row">
811
+ <span class="metric-label">Total (AWS + tools)</span>
812
+ <span class="metric-value">$${c.total_monthly_aws.toLocaleString()}/mo</span>
813
+ </div>
814
+ <div class="metric-row">
815
+ <span class="metric-label">Total (Azure + tools)</span>
816
+ <span class="metric-value">$${c.total_monthly_azure.toLocaleString()}/mo</span>
817
+ </div>
818
+ </div>
819
+
820
+ <!-- Tool Licensing -->
821
+ <div class="card">
822
+ <div class="card-title"><span class="icon">🔧</span> Tool Costs</div>
823
+ <div class="metric-row">
824
+ <span class="metric-label">Licensing (annual)</span>
825
+ <span class="metric-value accent">$${c.tool_licensing_annual.toLocaleString()}</span>
826
+ </div>
827
+ <div class="metric-row">
828
+ <span class="metric-label">Management (monthly)</span>
829
+ <span class="metric-value">$${c.tool_management_monthly.toLocaleString()}</span>
830
+ </div>
831
+ <div class="metric-row">
832
+ <span class="metric-label">Setup cost (one-time)</span>
833
+ <span class="metric-value">$${c.tool_setup_cost.toLocaleString()}</span>
834
+ </div>
835
+ ${toolCostRows}
836
+ </div>
837
+
838
+ ${roiCard}
839
+
840
+ <!-- Recommendations -->
841
+ <div class="card full-width">
842
+ <div class="card-title"><span class="icon">💡</span> Recommendations</div>
843
+ <ul class="rec-list">${recItems}</ul>
844
+ </div>
845
+ `;
846
+ }
847
+
848
+ _toolLabel(tool) {
849
+ const labels = {
850
+ hailbytes_asm: 'HailBytes ASM',
851
+ openvas: 'OpenVAS',
852
+ nessus_professional: 'Nessus Professional',
853
+ qualys_vmdr: 'Qualys VMDR',
854
+ };
855
+ return labels[tool] || tool;
856
+ }
857
+ }
858
+
859
+ customElements.define('hailbytes-vuln-calculator', HailbytesVulnCalculator);
860
+
861
+ export default HailbytesVulnCalculator;
862
+ export { HailbytesVulnCalculator, calculateResources as calculate };