Add Atlas metrics reporting, security audit tracker, and spec documents
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
// Feature: atlas-metrics-report, Property 1: Metrics aggregation correctness
|
||||
|
||||
import fc from 'fast-check';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock backend dependencies so we can import the pure function
|
||||
// without pulling in Express, SQLite, etc.
|
||||
// ---------------------------------------------------------------------------
|
||||
jest.mock('express', () => ({ Router: jest.fn(() => ({ get: jest.fn(), post: jest.fn(), put: jest.fn(), patch: jest.fn() })) }));
|
||||
jest.mock('../../../../../backend/middleware/auth', () => ({ requireGroup: jest.fn() }), { virtual: true });
|
||||
jest.mock('../../../../../backend/helpers/auditLog', () => jest.fn(), { virtual: true });
|
||||
jest.mock('../../../../../backend/helpers/atlasApi', () => ({
|
||||
isConfigured: false,
|
||||
atlasGet: jest.fn(),
|
||||
atlasPut: jest.fn(),
|
||||
atlasPatch: jest.fn(),
|
||||
atlasPost: jest.fn(),
|
||||
}), { virtual: true });
|
||||
|
||||
// Now import the pure function
|
||||
const { aggregateAtlasMetrics } = require('../../../../../backend/routes/atlas');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Generators
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const PLAN_TYPES = ['decommission', 'remediation', 'false_positive', 'risk_acceptance', 'scan_exclusion'];
|
||||
const PLAN_STATUSES = ['active', 'expired', 'completed'];
|
||||
|
||||
/** Generate a single plan object with plan_type and status */
|
||||
const planArb = fc.record({
|
||||
plan_type: fc.constantFrom(...PLAN_TYPES),
|
||||
status: fc.constantFrom(...PLAN_STATUSES),
|
||||
});
|
||||
|
||||
/** Generate a valid plans_json string (JSON array of plan objects) */
|
||||
const validPlansJsonArb = fc
|
||||
.array(planArb, { minLength: 0, maxLength: 10 })
|
||||
.map((plans) => JSON.stringify(plans));
|
||||
|
||||
/** Generate an invalid JSON string that will fail JSON.parse */
|
||||
const invalidPlansJsonArb = fc.constantFrom(
|
||||
'{bad json',
|
||||
'not json at all',
|
||||
'{{[',
|
||||
'',
|
||||
'undefined',
|
||||
);
|
||||
|
||||
/** Generate a plans_json value — either valid JSON or invalid */
|
||||
const plansJsonArb = fc.oneof(
|
||||
{ weight: 3, arbitrary: validPlansJsonArb },
|
||||
{ weight: 1, arbitrary: invalidPlansJsonArb },
|
||||
);
|
||||
|
||||
/** Generate a single cache row */
|
||||
const cacheRowArb = fc.record({
|
||||
has_action_plan: fc.constantFrom(0, 1),
|
||||
plans_json: plansJsonArb,
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: manually compute expected metrics for comparison
|
||||
// ---------------------------------------------------------------------------
|
||||
function computeExpected(rows) {
|
||||
const expected = {
|
||||
totalHosts: rows.length,
|
||||
hostsWithPlans: 0,
|
||||
hostsWithoutPlans: 0,
|
||||
plansByType: {},
|
||||
plansByStatus: {},
|
||||
totalPlans: 0,
|
||||
};
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.has_action_plan === 1) {
|
||||
expected.hostsWithPlans++;
|
||||
} else {
|
||||
expected.hostsWithoutPlans++;
|
||||
}
|
||||
|
||||
let plans;
|
||||
try {
|
||||
plans = JSON.parse(row.plans_json);
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Array.isArray(plans)) continue;
|
||||
|
||||
for (const plan of plans) {
|
||||
expected.totalPlans++;
|
||||
if (plan.plan_type) {
|
||||
expected.plansByType[plan.plan_type] = (expected.plansByType[plan.plan_type] || 0) + 1;
|
||||
}
|
||||
if (plan.status) {
|
||||
expected.plansByStatus[plan.status] = (expected.plansByStatus[plan.status] || 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return expected;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Property 1: Metrics aggregation correctness
|
||||
// Validates: Requirements 1.3, 1.4, 1.5
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Property 1: Metrics aggregation correctness', () => {
|
||||
test('totalHosts equals rows.length', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(cacheRowArb, { minLength: 0, maxLength: 30 }),
|
||||
(rows) => {
|
||||
const result = aggregateAtlasMetrics(rows);
|
||||
expect(result.totalHosts).toBe(rows.length);
|
||||
},
|
||||
),
|
||||
{ numRuns: 100 },
|
||||
);
|
||||
});
|
||||
|
||||
test('hostsWithPlans + hostsWithoutPlans equals totalHosts', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(cacheRowArb, { minLength: 0, maxLength: 30 }),
|
||||
(rows) => {
|
||||
const result = aggregateAtlasMetrics(rows);
|
||||
expect(result.hostsWithPlans + result.hostsWithoutPlans).toBe(result.totalHosts);
|
||||
},
|
||||
),
|
||||
{ numRuns: 100 },
|
||||
);
|
||||
});
|
||||
|
||||
test('hostsWithPlans equals count of rows where has_action_plan === 1', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(cacheRowArb, { minLength: 0, maxLength: 30 }),
|
||||
(rows) => {
|
||||
const result = aggregateAtlasMetrics(rows);
|
||||
const expectedWithPlans = rows.filter((r) => r.has_action_plan === 1).length;
|
||||
expect(result.hostsWithPlans).toBe(expectedWithPlans);
|
||||
},
|
||||
),
|
||||
{ numRuns: 100 },
|
||||
);
|
||||
});
|
||||
|
||||
test('totalPlans equals sum of valid plan array lengths', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(cacheRowArb, { minLength: 0, maxLength: 30 }),
|
||||
(rows) => {
|
||||
const result = aggregateAtlasMetrics(rows);
|
||||
const expected = computeExpected(rows);
|
||||
expect(result.totalPlans).toBe(expected.totalPlans);
|
||||
},
|
||||
),
|
||||
{ numRuns: 100 },
|
||||
);
|
||||
});
|
||||
|
||||
test('plansByType and plansByStatus counts match individual plan fields', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(cacheRowArb, { minLength: 0, maxLength: 30 }),
|
||||
(rows) => {
|
||||
const result = aggregateAtlasMetrics(rows);
|
||||
const expected = computeExpected(rows);
|
||||
expect(result.plansByType).toEqual(expected.plansByType);
|
||||
expect(result.plansByStatus).toEqual(expected.plansByStatus);
|
||||
},
|
||||
),
|
||||
{ numRuns: 100 },
|
||||
);
|
||||
});
|
||||
|
||||
test('rows with invalid JSON are counted in host totals but excluded from plan counts', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(
|
||||
fc.record({
|
||||
has_action_plan: fc.constantFrom(0, 1),
|
||||
plans_json: invalidPlansJsonArb,
|
||||
}),
|
||||
{ minLength: 1, maxLength: 20 },
|
||||
),
|
||||
(rows) => {
|
||||
const result = aggregateAtlasMetrics(rows);
|
||||
|
||||
// Host totals should still be correct
|
||||
expect(result.totalHosts).toBe(rows.length);
|
||||
expect(result.hostsWithPlans + result.hostsWithoutPlans).toBe(rows.length);
|
||||
|
||||
// No plans should be counted since all JSON is invalid
|
||||
expect(result.totalPlans).toBe(0);
|
||||
expect(result.plansByType).toEqual({});
|
||||
expect(result.plansByStatus).toEqual({});
|
||||
},
|
||||
),
|
||||
{ numRuns: 100 },
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user