Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion hailbytes-vuln-calculator.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,36 @@ const TIME_MULT = { light: 0.5, medium: 1.0, aggressive: 2.0, continuous: 0

// ─── Calculation engine ────────────────────────────────────────────────────────

function calculateResources(data) {
/**
* Normalize raw inputs so the pure calculate() API tolerates omitted optional
* fields the same way the DOM component does. `scan_window` and
* `compliance_needs` are declared optional in the public types, so callers may
* omit them; without this, calculate() throws on a missing `compliance_needs`
* and yields NaN window-utilization on a missing `scan_window`.
*/
function normalizeInputs(data) {
const raw = data ?? {};
const hosts = Number(raw.target_hosts);
// An empty tools array is a valid, zero-cost state for the pure API (the DOM
// component injects its own UI fallback); only a missing/invalid value needs
// a default to avoid a crash on `.includes`.
const tools = Array.isArray(raw.scanning_tools) ? raw.scanning_tools.filter(Boolean) : [];

const window = Number(raw.scan_window);

return {
...raw,
target_hosts: Number.isFinite(hosts) && hosts > 0 ? hosts : 1,
scan_intensity: raw.scan_intensity ?? 'medium',
scan_frequency: raw.scan_frequency ?? 'weekly',
scan_window: Number.isFinite(window) && window > 0 ? window : 8,
scanning_tools: tools,
compliance_needs: Array.isArray(raw.compliance_needs) ? raw.compliance_needs : [],
};
}

function calculateResources(rawData) {
const data = normalizeInputs(rawData);
const hasAsm = data.scanning_tools.includes('hailbytes_asm');

const vmResources = hasAsm
Expand Down
74 changes: 74 additions & 0 deletions test/calculate.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,80 @@ describe('Edge Cases', () => {
});
});

// ═════════════════════════════════════════════════════════════════════════════
// Optional input handling — fields the public types mark as optional
// (scan_window?, compliance_needs?) must not crash or produce NaN when omitted.
// ═════════════════════════════════════════════════════════════════════════════

describe('Optional input handling', () => {
test('omitting compliance_needs does not throw and defaults to no compliance', () => {
const r = calculate({
target_hosts: 1000,
scan_intensity: 'medium',
scan_frequency: 'weekly',
scan_window: 8,
scanning_tools: ['hailbytes_asm'],
// compliance_needs intentionally omitted
});
assert.ok(r.vm_resources.cpu_cores >= 2);
// complianceFactor should behave like an empty list (1.0 multiplier)
assert.deepEqual(r.inputs.compliance_needs, []);
assert.ok(!r.recommendations.some(rec => rec.includes('PCI DSS requires')));
});

test('omitting scan_window yields a finite utilization, not NaN', () => {
const r = calculate({
target_hosts: 1000,
scan_intensity: 'medium',
scan_frequency: 'weekly',
scanning_tools: ['hailbytes_asm'],
compliance_needs: [],
// scan_window intentionally omitted
});
assert.equal(typeof r.timing.scan_window_utilization, 'number');
assert.ok(Number.isFinite(r.timing.scan_window_utilization));
assert.ok(!Number.isNaN(r.timing.scan_window_utilization));
// efficiency rating should reflect the real number, not the NaN→'poor' fallthrough
assert.ok(['excellent', 'good', 'acceptable', 'poor'].includes(
r.timing.performance_metrics.efficiency_rating));
});

test('omitting scanning_tools does not throw and produces zero tool cost', () => {
const r = calculate({
target_hosts: 1000,
scan_intensity: 'medium',
scan_frequency: 'weekly',
// scanning_tools intentionally omitted
});
assert.ok(r.vm_resources);
assert.equal(r.has_asm, false);
assert.equal(r.costs.tool_licensing_annual, 0);
assert.equal(r.costs.tool_management_monthly, 0);
assert.equal(r.costs.tool_setup_cost, 0);
});

test('zero / invalid scan_window falls back to a safe default instead of dividing by zero', () => {
const r = calculate({
target_hosts: 1000,
scan_intensity: 'medium',
scan_frequency: 'weekly',
scan_window: 0,
scanning_tools: ['hailbytes_asm'],
compliance_needs: [],
});
assert.ok(Number.isFinite(r.timing.scan_window_utilization));
});

test('only the minimal required fields are needed to get a full result', () => {
const r = calculate({ target_hosts: 500 });
assert.ok(r.vm_resources);
assert.ok(r.timing);
assert.ok(r.costs);
assert.ok(Array.isArray(r.recommendations));
assert.equal(typeof r.timestamp, 'string');
});
});

// ═════════════════════════════════════════════════════════════════════════════
// Additional Accuracy Tests - Cross-validate full scenario
// ═════════════════════════════════════════════════════════════════════════════
Expand Down
13 changes: 13 additions & 0 deletions test/smoke.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ test('calculate() recognises HailBytes ASM tool', () => {
assert.equal(r.has_asm, true);
});

test('calculate() tolerates omitted optional inputs (scan_window, compliance_needs)', () => {
// index.d.ts marks scan_window and compliance_needs as optional, so omitting
// them must not throw or yield NaN.
const r = mod.calculate({
target_hosts: 1000,
scan_intensity: 'medium',
scan_frequency: 'weekly',
scanning_tools: ['hailbytes_asm'],
});
assert.ok(r.vm_resources);
assert.ok(Number.isFinite(r.timing.scan_window_utilization));
});

test('class declares observedAttributes including theme and branding', () => {
const observed = mod.default.observedAttributes;
assert.ok(Array.isArray(observed));
Expand Down
Loading