Files
cve-dashboard/.kiro/specs/atlas-metrics-report/design.md

18 KiB
Raw Blame History

Design Document: Atlas Metrics Report

Overview

This feature adds a tab system to the existing Metric Graphs panel on the ReportingPage, separating Ivanti donut charts from new Atlas coverage charts. A new GET /api/atlas/metrics endpoint aggregates cached Atlas action plan data into chart-ready metrics. The frontend renders three new donut charts — coverage, plan type distribution, and plan status distribution — under an "Atlas Coverage" tab, while the existing four Ivanti donuts move under an "Ivanti Findings" tab.

Key Design Decisions

  • Server-side aggregation: The metrics endpoint computes counts and distributions on the backend rather than shipping raw plans_json to the client. This keeps the frontend simple and avoids parsing potentially large JSON arrays in the browser.
  • Reuse existing donut helpers: The new Atlas donut charts reuse the polarToCartesian and donutArcPath SVG helper functions already defined in ReportingPage.js, along with the same dimensions (180px size, 72px outer radius, 48px inner radius). This keeps the visual language consistent.
  • Fetch once, refresh on sync: Atlas metrics are fetched once on page mount and re-fetched only after a successful Atlas sync. Tab switches do not trigger new API calls.
  • No server.js changes: The new endpoint is added inside the existing createAtlasRouter(db, requireAuth) factory function in backend/routes/atlas.js. The router is already mounted at /api/atlas in server.js.
  • Tab state is local: The active tab is stored in React component state — no URL params, no localStorage. The default is "Ivanti Findings" to preserve the existing experience.
  • IvantiCountsChart conditional visibility: The trend chart below the Metric Graphs panel is only shown when the Ivanti tab is active, since it has no relevance to Atlas data.

Architecture

graph TD
    subgraph Frontend - ReportingPage
        TS[Tab System<br/>Ivanti Findings | Atlas Coverage]
        ID[Ivanti Donuts<br/>StatusDonut, ActionCoverageDonut,<br/>FPWorkflowDonut x2]
        AD[Atlas Donuts<br/>CoverageDonut, PlanTypeDonut,<br/>PlanStatusDonut]
        IC[IvantiCountsChart]
    end

    subgraph Backend - Atlas Router
        ME[GET /api/atlas/metrics]
        AC[(atlas_action_plans_cache<br/>SQLite)]
    end

    TS -->|tab = ivanti| ID
    TS -->|tab = ivanti| IC
    TS -->|tab = atlas| AD
    AD -->|fetch on mount + after sync| ME
    ME -->|SELECT + aggregate| AC

Data Flow: Metrics Fetch

  1. ReportingPage mounts → calls GET /api/atlas/metrics
  2. Backend queries all rows from atlas_action_plans_cache, including plans_json
  3. Backend iterates rows, parses each plans_json, counts plans by type and status
  4. Backend returns aggregated JSON: { totalHosts, hostsWithPlans, hostsWithoutPlans, plansByType, plansByStatus, totalPlans }
  5. Frontend stores result in atlasMetrics state
  6. When "Atlas Coverage" tab is active, three donut components render from this state

Data Flow: Refresh After Sync

  1. User clicks Atlas sync button → POST /api/atlas/sync (existing)
  2. On success, frontend calls GET /api/atlas/metrics again
  3. Atlas donut charts re-render with updated data

Components and Interfaces

Backend: GET /api/atlas/metrics Endpoint

Added inside the existing createAtlasRouter(db, requireAuth) factory function in backend/routes/atlas.js.

Method Path Auth Group Description
GET /metrics requireAuth any Return aggregated Atlas metrics for chart rendering

Response shape:

{
    "totalHosts": 42,
    "hostsWithPlans": 28,
    "hostsWithoutPlans": 14,
    "plansByType": {
        "decommission": 5,
        "remediation": 18,
        "false_positive": 3,
        "risk_acceptance": 8,
        "scan_exclusion": 2
    },
    "plansByStatus": {
        "active": 25,
        "expired": 7,
        "completed": 4
    },
    "totalPlans": 36
}

Implementation approach:

  1. Query all rows: SELECT has_action_plan, plans_json FROM atlas_action_plans_cache
  2. Initialize counters: totalHosts = rows.length, hostsWithPlans = 0, hostsWithoutPlans = 0, plansByType = {}, plansByStatus = {}, totalPlans = 0
  3. For each row:
    • If has_action_plan === 1, increment hostsWithPlans; else increment hostsWithoutPlans
    • Try to parse plans_json; on failure, skip plan details for that row
    • For each plan in the parsed array, increment the corresponding plansByType[plan.plan_type] and plansByStatus[plan.status] counters, and increment totalPlans
  4. Return the aggregated object

Uses the existing dbAll promise wrapper already defined in atlas.js.

Frontend: Tab System

A horizontal tab bar rendered inside the Metric Graphs panel header, to the right of the "Metric Graphs" title and PieChart icon.

State: metricsTab'ivanti' (default) or 'atlas'

Tab bar structure:

┌──────────────────────────────────────────────────────────────┐
│ [PieChart icon] METRIC GRAPHS    [Ivanti Findings] [Atlas Coverage] │
└──────────────────────────────────────────────────────────────┘

Styling:

  • Tabs use role="tab" and aria-selected; the content area uses role="tabpanel"
  • Active tab: color: #F59E0B, borderBottom: 2px solid #F59E0B
  • Inactive tab: color: #64748B, no bottom border
  • Hover on inactive: background: rgba(245, 158, 11, 0.06)
  • Font: 'JetBrains Mono', monospace, 0.7rem, uppercase, letterSpacing: 0.08em
  • Tabs are keyboard navigable via Tab and Enter keys

Frontend: Atlas Metrics State

New state variables in the ReportingPage component:

const [metricsTab, setMetricsTab] = useState('ivanti');
const [atlasMetrics, setAtlasMetrics] = useState(null);
const [atlasMetricsLoading, setAtlasMetricsLoading] = useState(false);
const [atlasMetricsError, setAtlasMetricsError] = useState(null);

New fetch function:

const fetchAtlasMetrics = useCallback(async () => {
    setAtlasMetricsLoading(true);
    setAtlasMetricsError(null);
    try {
        const res = await fetch(`${API_BASE}/atlas/metrics`, { credentials: 'include' });
        if (res.ok) {
            const data = await res.json();
            setAtlasMetrics(data);
        } else {
            const err = await res.json().catch(() => ({}));
            setAtlasMetricsError(err.error || 'Failed to fetch Atlas metrics');
        }
    } catch (err) {
        setAtlasMetricsError(err.message);
    } finally {
        setAtlasMetricsLoading(false);
    }
}, []);

Called on mount (in the existing useEffect) and after a successful Atlas sync.

Frontend: Atlas Donut Charts

Three new inline components defined in ReportingPage.js, following the same pattern as StatusDonut, ActionCoverageDonut, and FPWorkflowDonut. All reuse the existing polarToCartesian and donutArcPath helper functions.

AtlasCoverageDonut

  • Props: { hostsWithPlans, hostsWithoutPlans, totalHosts }
  • Segments: emerald (#10B981) for with plans, amber (#F59E0B) for without plans
  • Center text: totalHosts count, "HOSTS" label
  • Empty state: "No data — run Atlas Sync"

AtlasPlanTypeDonut

  • Props: { plansByType, totalPlans }
  • Color map: decommission: #EF4444, remediation: #0EA5E9, false_positive: #A855F7, risk_acceptance: #F59E0B, scan_exclusion: #64748B
  • Center text: totalPlans count, "PLANS" label
  • Legend: only shows types with count > 0
  • Empty state: "No plans — run Atlas Sync"

AtlasPlanStatusDonut

  • Props: { plansByStatus, totalPlans }
  • Color map: active: #10B981, expired: #EF4444, completed: #0EA5E9, fallback: #64748B
  • Center text: totalPlans count, "STATUS" label
  • Legend: only shows statuses with count > 0
  • Empty state: "No plans — run Atlas Sync"

All three follow the same SVG dimensions: 180px size, 72px outer radius, 48px inner radius.

Frontend: Metric Graphs Panel Layout

When "Ivanti Findings" tab is active:

  • Existing four donuts in horizontal flex row with dividers (unchanged)
  • IvantiCountsChart rendered below the panel (unchanged)

When "Atlas Coverage" tab is active:

  • Three Atlas donuts in horizontal flex row with dividers (same layout pattern)
  • IvantiCountsChart hidden

Data Models

Metrics Endpoint Response

Field Type Description
totalHosts integer Count of all rows in atlas_action_plans_cache
hostsWithPlans integer Count of rows where has_action_plan = 1
hostsWithoutPlans integer Count of rows where has_action_plan = 0
plansByType object Map of plan type string → integer count
plansByStatus object Map of plan status string → integer count
totalPlans integer Sum of all plans across all hosts

Existing Atlas Cache Table (no changes)

The atlas_action_plans_cache table is unchanged. The metrics endpoint reads from it:

Column Type Used by metrics endpoint
has_action_plan INTEGER Counting hosts with/without plans
plans_json TEXT Parsed to count plans by type and status

Plan Object Shape (within plans_json)

The metrics endpoint reads plan_type and status from each plan object in the JSON array. The Atlas API returns these fields as strings. Example:

{
    "action_plan_id": "ap-123",
    "plan_type": "remediation",
    "status": "active",
    "commit_date": "2026-07-01"
}

Known plan_type values: decommission, remediation, false_positive, risk_acceptance, scan_exclusion

Known status values: active, expired, completed (the frontend handles unknown values with a neutral color)

Correctness Properties

A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.

Property 1: Metrics aggregation correctness

For any array of cache rows — each with a has_action_plan flag (0 or 1) and a plans_json string that is either valid JSON containing an array of plan objects (each with plan_type and status fields) or invalid JSON — the metrics aggregation function SHALL produce:

  • totalHosts equal to the number of rows
  • hostsWithPlans + hostsWithoutPlans equal to totalHosts
  • hostsWithPlans equal to the count of rows where has_action_plan === 1
  • totalPlans equal to the sum of plan array lengths across all rows with valid JSON
  • Each key in plansByType maps to the count of plans with that plan_type across all valid rows
  • Each key in plansByStatus maps to the count of plans with that status across all valid rows
  • Rows with invalid plans_json are counted in totalHosts and hostsWithPlans/hostsWithoutPlans but their plans are excluded from plansByType, plansByStatus, and totalPlans

Validates: Requirements 1.3, 1.4, 1.5

Property 2: Coverage donut data correctness

For any non-negative integer pair (hostsWithPlans, hostsWithoutPlans) where totalHosts = hostsWithPlans + hostsWithoutPlans > 0, the Coverage Donut SHALL display totalHosts as center text, and the legend SHALL show counts matching the input values with percentages that equal (count / totalHosts) * 100 for each segment.

Validates: Requirements 3.3, 3.4

Property 3: Plan type donut data correctness

For any plansByType object mapping plan type strings to positive integer counts, and totalPlans equal to the sum of those counts, the Plan Type Donut SHALL display totalPlans as center text, the legend SHALL include only types with count greater than zero, and each legend entry's percentage SHALL equal (count / totalPlans) * 100.

Validates: Requirements 4.3, 4.4

Property 4: Plan status donut data correctness

For any plansByStatus object mapping status strings to positive integer counts, and totalPlans equal to the sum of those counts, the Plan Status Donut SHALL display totalPlans as center text, the legend SHALL include only statuses with count greater than zero, and each legend entry's percentage SHALL equal (count / totalPlans) * 100.

Validates: Requirements 5.3, 5.4

Property 5: Plan status color assignment

For any status string, the Plan Status Donut color assignment function SHALL return #10B981 if the status is "active", #EF4444 if "expired", #0EA5E9 if "completed", and #64748B for any other string value.

Validates: Requirements 5.2

Error Handling

Backend Errors

Error Scenario Handling HTTP Status Response
Atlas not configured (missing env vars) Return 503 with descriptive message 503 { error: 'Atlas API is not configured...' }
Database query failure Catch error, log, return 500 500 { error: 'Failed to fetch Atlas metrics.' }
Invalid JSON in plans_json column Skip that row's plan details, continue processing N/A (handled internally) Metrics still returned with correct counts for valid rows
Empty cache table Return metrics object with all zeros 200 { totalHosts: 0, hostsWithPlans: 0, hostsWithoutPlans: 0, plansByType: {}, plansByStatus: {}, totalPlans: 0 }
Unauthenticated request Auth middleware rejects 401 { error: 'Authentication required' }

Frontend Errors

Error Scenario Handling User Impact
Metrics fetch returns non-200 Store error message in atlasMetricsError state Error message displayed in Atlas Coverage tab content area
Metrics fetch network failure Catch error, store message Error message displayed with failure reason
Metrics fetch in progress atlasMetricsLoading = true Loading spinner shown in Atlas Coverage tab content area
Atlas metrics data is null (not yet fetched) Donut components check for null/undefined Loading state or empty state shown

Testing Strategy

Unit Tests

Unit tests cover specific examples, edge cases, and integration points:

Backend — Metrics Aggregation (GET /api/atlas/metrics):

  • Returns all-zero metrics when cache table is empty
  • Correctly counts hosts with and without plans from seeded data
  • Correctly aggregates plansByType from multiple hosts
  • Correctly aggregates plansByStatus from multiple hosts
  • Skips plan details for rows with invalid JSON but still counts the host
  • Returns 503 when Atlas is not configured
  • Requires authentication (returns 401 without session)

Frontend — Tab System:

  • Renders two tabs with correct labels
  • Defaults to "Ivanti Findings" tab on mount
  • Switches content when tab is clicked
  • Active tab has correct ARIA attributes (aria-selected="true")
  • Tab panel has role="tabpanel" attribute
  • Keyboard navigation works (Tab + Enter)

Frontend — Atlas Donut Charts:

  • Coverage donut shows "No data — run Atlas Sync" when totalHosts is 0
  • Plan type donut shows "No plans — run Atlas Sync" when totalPlans is 0
  • Plan status donut shows "No plans — run Atlas Sync" when totalPlans is 0
  • SVG dimensions are 180px with 72px outer and 48px inner radius
  • Color assignments match specification for each plan type
  • Color assignments match specification for known statuses

Frontend — Data Fetching:

  • Fetches metrics on mount
  • Re-fetches metrics after successful Atlas sync
  • Does not re-fetch on tab switch
  • Shows loading indicator while fetch is in progress
  • Shows error message when fetch fails

Frontend — IvantiCountsChart Visibility:

  • Rendered when Ivanti tab is active
  • Not rendered when Atlas tab is active

Property-Based Tests

Property-based tests verify universal properties across generated inputs. Each test runs a minimum of 100 iterations.

Library: fast-check — the standard PBT library for JavaScript/Node.js.

Configuration: Each property test runs with { numRuns: 100 } minimum.

Tag format: Each test includes a comment referencing its design property:

// Feature: atlas-metrics-report, Property 1: Metrics aggregation correctness
Property Test Description Generator Strategy
Property 1 Metrics aggregation correctness Generate arrays of objects with has_action_plan (0 or 1) and plans_json (either valid JSON array of {plan_type, status} objects or random invalid strings). Extract the aggregation logic into a pure function, call it with generated input, verify all invariants.
Property 2 Coverage donut data correctness Generate random (hostsWithPlans, hostsWithoutPlans) pairs of non-negative integers (at least one > 0). Render AtlasCoverageDonut, verify center text equals sum and legend percentages are mathematically correct.
Property 3 Plan type donut data correctness Generate random plansByType objects with 15 plan type keys mapped to positive integers. Render AtlasPlanTypeDonut, verify center text equals sum and legend entries match input.
Property 4 Plan status donut data correctness Generate random plansByStatus objects with 14 status keys mapped to positive integers. Render AtlasPlanStatusDonut, verify center text equals sum and legend entries match input.
Property 5 Plan status color assignment Generate random strings (mix of known statuses and arbitrary strings). Verify the color function returns the correct color for known statuses and the fallback for unknowns.

Integration Tests

  • Full metrics flow: seed atlas_action_plans_cache with varied data → call GET /api/atlas/metrics → verify response matches expected aggregation
  • Empty cache flow: ensure empty cache returns all-zero metrics
  • Corrupt data flow: seed cache with mix of valid and invalid plans_json → verify metrics are correct for valid rows