diff --git a/.kiro/specs/compliance-metric-grouping/.config.kiro b/.kiro/specs/compliance-metric-grouping/.config.kiro new file mode 100644 index 0000000..cb1651d --- /dev/null +++ b/.kiro/specs/compliance-metric-grouping/.config.kiro @@ -0,0 +1 @@ +{"specId": "9ecf72f0-b470-4877-b244-899e583007f7", "workflowType": "requirements-first", "specType": "feature"} diff --git a/.kiro/specs/compliance-metric-grouping/design.md b/.kiro/specs/compliance-metric-grouping/design.md new file mode 100644 index 0000000..e0a24f5 --- /dev/null +++ b/.kiro/specs/compliance-metric-grouping/design.md @@ -0,0 +1,516 @@ +# Design Document: Compliance Metric Grouping + +## Overview + +The Compliance Metric Grouping feature consolidates the AEO Compliance page's metric health cards from one-per-summary-entry to one-per-metric-family. A metric family is the set of summary entries that share the same base `metric_id` (e.g., `5.2.5`). The same base ID can appear multiple times in the summary data because the backend parser produces one entry per team/variant row in the xlsx Summary sheet. + +The feature adds three new capabilities on top of the grouping: + +1. **Variant pills** inside each grouped card showing per-variant compliance percentages and status indicators +2. **Hover tooltip** (300ms delay) displaying metric title, business justification, and data sources from a static definitions file +3. **Info panel** opened via an info icon on each card, showing the full metric definition (scope, filters, exclusions, notes) + +A static JSON file (`metricDefinitions.json`) ships with the frontend containing structured metric definition data for all tracked metrics. No new backend endpoints are needed. + +### Design Decisions + +- **Grouping is frontend-only.** The backend summary endpoint returns flat entries. The frontend groups them by `metric_id` at render time. This avoids backend changes and keeps the grouping logic testable as a pure function. +- **`metricFilter` changes from a single string to an array.** Currently `metricFilter` is a single `metric_id` string or `null`. With grouping, clicking a card sets the filter to the array of all `metric_id` values in that family. For single-entry families this is a one-element array. The device table filter checks `metricFilter.includes(m.metric_id)` instead of `m.metric_id === metricFilter`. +- **Worst-status drives card color.** Each grouped card computes the most severe status across its variants using a defined severity ordering. This gives engineers an at-a-glance signal when any variant is failing. +- **Definitions file is static, not an API.** The metric definitions table has ~130 rows that change infrequently. A static JSON import avoids API round-trips and keeps the tooltip/panel responsive. The file can be regenerated from the source xlsx definitions table when metrics change. +- **MetricInfoPanel is a new component.** The detail panel is complex enough (12+ fields, dark-themed sections) to warrant its own component file rather than inlining it in CompliancePage.js. The hover tooltip, being lightweight, stays inline. +- **Info icon click uses `stopPropagation`.** The info icon sits inside the card button. Clicking it opens the detail panel without triggering the card's metric filter toggle. + +## Architecture + +```mermaid +graph TD + A[CompliancePage.js] -->|imports| B[metricDefinitions.json] + A -->|renders| C[MetricHealthCard - grouped] + A -->|renders| D[MetricInfoPanel] + A -->|renders| E[HoverTooltip - inline] + A -->|fetches| F[GET /api/compliance/summary?team=X] + A -->|fetches| G[GET /api/compliance/items?team=X&status=Y] + + F -->|returns| H[summary.entries array] + H -->|groupByMetricFamily| I[Map of metricId → entries array] + I -->|one card per group| C + + C -->|click outside info icon| J[setMetricFilter - array of IDs] + C -->|click info icon| K[setInfoMetric - opens MetricInfoPanel] + C -->|hover 300ms| E + + J -->|filters| G2[filteredDevices] + B -->|lookup by metric_id| E + B -->|lookup by metric_id| D +``` + +### Component Hierarchy + +``` +CompliancePage +├── PageHeader (unchanged) +├── TeamTabs (unchanged) +├── MetricHealthSection +│ ├── SectionHeader ("Metric Health — click to filter" + clear button) +│ └── MetricHealthCard (one per metric family) +│ ├── CardTitle (base metric_id + category) +│ ├── VariantPill[] (one per summary entry in family) +│ ├── WorstStatusPill (computed from all variants) +│ ├── TargetDisplay (shared target %) +│ ├── InfoIcon (lucide-react Info, top-right) +│ └── HoverTooltip (inline, 300ms delay) +├── MetricInfoPanel (slide-out/overlay, opened by info icon click) +├── ComplianceChartsPanel (unchanged) +├── DeviceTable (unchanged, filter logic updated) +├── ComplianceDetailPanel (unchanged) +├── ComplianceUploadModal (unchanged) +└── RollbackModal (unchanged) +``` + +### Data Flow + +1. `CompliancePage` fetches summary entries from `/api/compliance/summary?team=X` on mount and team change. +2. The `groupByMetricFamily(entries)` helper groups the flat entries array into a `Map` keyed by base `metric_id`. +3. One `MetricHealthCard` renders per map entry. Each card receives the full array of entries for that family. +4. The card computes `worstStatus` from the entries' status fields and uses it for border/pill coloring. +5. On card click (outside info icon), `metricFilter` is set to the array of `metric_id` values in that family (or cleared if already active). +6. The device table filter changes from `d.failing_metrics.some(m => m.metric_id === metricFilter)` to `d.failing_metrics.some(m => metricFilter.includes(m.metric_id))`. +7. On hover (300ms), a tooltip renders using data from the `metricDefinitions.json` lookup map, falling back to the summary entry description. +8. On info icon click, `infoMetric` state is set, opening `MetricInfoPanel` with the full definition. + +## Components and Interfaces + +### groupByMetricFamily (pure helper function) + +```javascript +// In CompliancePage.js — replaces the current teamMetrics() helper +// Input: entries (array of SummaryEntry from the summary endpoint), team (string) +// Output: array of { metricId, entries, category, target, worstStatus } + +function groupByMetricFamily(allEntries, team) { + const teamEntries = allEntries.filter(e => e.team === team); + const familyMap = {}; + + for (const entry of teamEntries) { + const baseId = entry.metric_id; // already the base ID from the parser + if (!familyMap[baseId]) { + familyMap[baseId] = []; + } + familyMap[baseId].push(entry); + } + + return Object.entries(familyMap).map(([metricId, entries]) => ({ + metricId, + entries, + category: entries[0].category, + target: entries[0].target, + worstStatus: computeWorstStatus(entries.map(e => e.status)), + })); +} +``` + +### computeWorstStatus (pure helper function) + +```javascript +// Input: array of status strings +// Output: the most severe status string +// Severity order: "Below 15% of Target" > "Within 15% of Target" > "Meets/Exceeds Target" + +const STATUS_SEVERITY = { + 'Below 15% of Target': 0, + 'Within 15% of Target': 1, + 'Meets/Exceeds Target': 2, +}; + +function computeWorstStatus(statuses) { + let worst = 'Meets/Exceeds Target'; + let worstSev = 2; + for (const s of statuses) { + const sev = STATUS_SEVERITY[s] ?? 0; + if (sev < worstSev) { + worstSev = sev; + worst = s; + } + } + return worst; +} +``` + +### MetricHealthCard (redesigned) + +```javascript +// Props: +// family: { metricId, entries, category, target, worstStatus } +// active: boolean (is this family's filter currently active) +// onClick: () => void (toggle metric filter) +// onInfoClick: (metricId) => void (open detail panel) +// definitionLookup: Map (for tooltip) +// +// Renders: +// - Base metric ID as title +// - Category label +// - One VariantPill per entry in family.entries +// - Shared target percentage +// - Worst-status pill with border color +// - Info icon (top-right, stopPropagation on click) +// - HoverTooltip (300ms delay, positioned near card) +``` + +### VariantPill (new inline sub-component) + +```javascript +// Props: +// entry: SummaryEntry (single variant) +// +// Renders: +// - Label: entry.team or entry.description (distinguishing text) +// - Compliance percentage in monospace +// - Background tint from entry's status color at ~12% opacity +// - Glow dot if status !== "Meets/Exceeds Target" +// +// Layout: inline-flex, wraps via parent flexWrap +``` + +### HoverTooltip (inline in CompliancePage.js) + +```javascript +// State managed in CompliancePage: +// hoveredMetric: string | null +// hoverTimeout: ref (setTimeout ID) +// tooltipPosition: { top, left } (computed from card bounding rect) +// +// On mouseEnter on MetricHealthCard: +// Set timeout for 300ms → set hoveredMetric to family.metricId +// On mouseLeave: +// Clear timeout, set hoveredMetric to null +// +// Renders (when hoveredMetric matches): +// - Fixed-position div near the card +// - Metric title (from definitionLookup or entry.description) +// - Business justification (from definitionLookup) +// - Data sources required (from definitionLookup) +// - Dark card background, subtle border, shadow per DESIGN_SYSTEM.md +// - Falls back to summary entry description if no definition found +``` + +### MetricInfoPanel (new component file) + +```javascript +// frontend/src/components/pages/MetricInfoPanel.js +// Props: +// metricId: string (base metric ID) +// definition: MetricDefinition | null (from lookup) +// summaryEntries: SummaryEntry[] (the family's entries, for fallback) +// onClose: () => void +// +// Renders: +// - Overlay/slide-out panel with dark theme +// - Close button (X icon, top-right) +// - Metric title (h3, monospace) +// - Sections with monospace uppercase labels: +// - Asset Types / Asset Types In Scope +// - Application Types In Scope +// - Environment In Scope +// - Status In Scope +// - Instance Types In Scope +// - Criticality Levels In Scope +// - Exclusions +// - Special Conditions +// - Data Sources Required +// - Business Justification +// - Notes +// - If definition is null: "No detailed definition available" + summary description fallback +// - Click outside or close button → onClose() +``` + +### Integration with CompliancePage.js + +```javascript +// New imports: +import { Info } from 'lucide-react'; +import MetricInfoPanel from './MetricInfoPanel'; +import metricDefinitionsRaw from '../../data/metricDefinitions.json'; + +// Build lookup map once at module level: +const METRIC_DEFINITIONS = {}; +for (const def of metricDefinitionsRaw) { + METRIC_DEFINITIONS[def.metric_id] = def; +} + +// State changes in CompliancePage: +// - metricFilter: null → null | string[] (array of metric IDs) +// - New state: infoMetric (string | null) — which metric's info panel is open +// - New state: hoveredMetric (string | null) — which metric is being hovered +// - New ref: hoverTimeoutRef — for 300ms delay + +// Filter logic change: +// Old: .filter(d => !metricFilter || d.failing_metrics.some(m => m.metric_id === metricFilter)) +// New: .filter(d => !metricFilter || d.failing_metrics.some(m => metricFilter.includes(m.metric_id))) + +// Card rendering change: +// Old: metrics.map(entry => ) +// New: families.map(family => ) +``` + +## Data Models + +### SummaryEntry (from backend, unchanged) + +```javascript +{ + metric_id: string, // e.g. "5.2.5" — base ID, no suffix + team: string, // e.g. "STEAM", "ACCESS-ENG" + priority: string, + non_compliant: number, + compliant: number, + total: number, + compliance_pct: number, // 0.0–1.0 + target: number, // 0.0–1.0 + status: string, // "Meets/Exceeds Target" | "Within 15% of Target" | "Below 15% of Target" + description: string, + category: string // from compliance_config.json metric_categories +} +``` + +### MetricFamily (computed client-side) + +```javascript +{ + metricId: string, // base metric ID (e.g. "5.2.5") + entries: SummaryEntry[], // all summary entries for this base ID + category: string, // from first entry + target: number, // from first entry (shared across variants) + worstStatus: string // computed worst status across all entries +} +``` + +### MetricDefinition (from metricDefinitions.json) + +```javascript +{ + metric_id: string, // e.g. "5.2.5" + metric_title: string, // e.g. "MFA for Privileged Access" + asset_types: string, // e.g. "Servers, Network Devices" + asset_types_in_scope: string, + application_types_in_scope: string, + environment_in_scope: string, + status_in_scope: string, + instance_types_in_scope: string, + criticality_levels_in_scope: string, + exclusions: string, // empty string if none + special_conditions: string, // empty string if none + data_sources_required: string, + business_justification: string, + notes: string // empty string if none +} +``` + +### metricDefinitions.json (file structure) + +```json +[ + { + "metric_id": "1.1.1", + "metric_title": "...", + "asset_types": "...", + "asset_types_in_scope": "...", + "application_types_in_scope": "...", + "environment_in_scope": "...", + "status_in_scope": "...", + "instance_types_in_scope": "...", + "criticality_levels_in_scope": "...", + "exclusions": "", + "special_conditions": "", + "data_sources_required": "...", + "business_justification": "...", + "notes": "" + } +] +``` + +All entries use the same set of keys. Optional fields with no value use empty strings, never `null` or omitted keys. + +### Status Severity Map + +```javascript +const STATUS_SEVERITY = { + 'Below 15% of Target': 0, // worst + 'Within 15% of Target': 1, + 'Meets/Exceeds Target': 2, // best +}; +``` + +### Updated metricFilter State + +```javascript +// Old: metricFilter: string | null +// New: metricFilter: string[] | null +// +// null = no filter (show all devices) +// string[] = show devices with failing_metrics matching any ID in the array +``` + +## 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: Grouping invariant — no entries lost or misplaced + +*For any* array of summary entries and any team string, grouping by metric family SHALL produce groups where (a) every entry appears in exactly one group, (b) all entries within a group share the same `metric_id`, and (c) the total number of entries across all groups equals the number of entries for that team in the input. + +**Validates: Requirements 1.1, 1.2** + +### Property 2: Worst-status computation follows severity ordering + +*For any* non-empty array of status strings drawn from `{"Below 15% of Target", "Within 15% of Target", "Meets/Exceeds Target"}`, the computed worst status SHALL be the status with the lowest severity rank present in the array. If the array contains "Below 15% of Target", the result SHALL be "Below 15% of Target" regardless of other values. + +**Validates: Requirements 1.6, 3.1** + +### Property 3: Device filtering with metric family includes all matching devices + +*For any* array of device objects (each with a `failing_metrics` array of `{metric_id}` objects) and *for any* non-empty array of filter metric IDs, the filtered result SHALL contain exactly those devices that have at least one `failing_metrics` entry whose `metric_id` is included in the filter array. No matching device is excluded and no non-matching device is included. + +**Validates: Requirements 1.8, 7.1, 7.2** + +### Property 4: Definition lookup returns correct entry or null + +*For any* array of metric definition objects with unique `metric_id` values, building a lookup map and querying it with a `metric_id` that exists in the array SHALL return the corresponding definition object. Querying with a `metric_id` not in the array SHALL return `undefined`. + +**Validates: Requirements 4.2, 4.6** + +### Property 5: Detail panel renders all required definition fields + +*For any* valid metric definition object (with all 14 fields present), the set of field keys rendered by the detail panel SHALL include: `metric_title`, `asset_types`, `asset_types_in_scope`, `application_types_in_scope`, `environment_in_scope`, `status_in_scope`, `instance_types_in_scope`, `criticality_levels_in_scope`, `exclusions`, `special_conditions`, `data_sources_required`, `business_justification`, and `notes`. + +**Validates: Requirements 5.3** + +### Property 6: Definitions schema validation — all entries have required fields + +*For any* entry in the metric definitions array, the entry SHALL have all 14 required keys present, and the `metric_id` field SHALL be a non-empty string. Optional fields (`exclusions`, `special_conditions`, `notes`) SHALL be present as strings (empty string if no value), never omitted or null. + +**Validates: Requirements 6.2, 8.3, 8.4** + +### Property 7: Lookup map construction preserves all definitions + +*For any* array of metric definition objects with unique `metric_id` values, building a lookup map keyed by `metric_id` SHALL produce a map with exactly as many entries as the input array, and every input definition SHALL be retrievable by its `metric_id`. + +**Validates: Requirements 6.4** + +### Property 8: JSON round-trip preserves metric definition data + +*For any* valid metric definition object, `JSON.parse(JSON.stringify(definition))` SHALL produce an object deeply equal to the original. + +**Validates: Requirements 8.1, 8.2** + +## Error Handling + +### Metric Definitions File + +| Error Scenario | Handling | +|---|---| +| `metricDefinitions.json` fails to import (malformed JSON) | Build-time error caught by Create React App. The file is validated at development time. | +| Metric ID not found in definitions lookup | Tooltip falls back to `entry.description` from summary data. Info panel shows "No detailed definition available" with summary description. | +| Definition entry has empty optional fields | Rendered sections show "—" placeholder for empty strings. No error thrown. | + +### Grouping Logic + +| Error Scenario | Handling | +|---|---| +| Summary entries array is empty | `groupByMetricFamily` returns empty array. No cards rendered. Existing "No compliance data" empty state shown. | +| Summary entry has missing or empty `metric_id` | Entry is skipped during grouping (filtered out). | +| All entries for a team have the same `metric_id` | Single family group with multiple variant pills. Works correctly. | + +### Hover Tooltip + +| Error Scenario | Handling | +|---|---| +| User moves mouse away before 300ms | Timeout cleared, tooltip never shown. No side effects. | +| Tooltip would render off-screen | Position clamped to viewport bounds using `getBoundingClientRect()`. | +| Rapid hover/unhover across multiple cards | Previous timeout cleared on each `mouseLeave`. Only the currently hovered card's tooltip can appear. | + +### Info Panel + +| Error Scenario | Handling | +|---|---| +| Info icon click while tooltip is visible | Tooltip dismissed (mouseLeave fires). Panel opens. No conflict. | +| Multiple rapid info icon clicks | `infoMetric` state is set to the latest clicked metric. Only one panel open at a time. | +| Click outside panel while scrolled | Overlay backdrop captures click, closes panel. Scroll position preserved. | + +### Device Table Filtering + +| Error Scenario | Handling | +|---|---| +| `metricFilter` is set to an array but no devices match | Empty state message shown: "No non-compliant devices". | +| Device has `failing_metrics` with IDs not in any family | Device only shown when no filter is active or when its metric IDs match the active filter. | + +## Testing Strategy + +### Unit Tests (Example-Based) + +Unit tests cover specific rendering, interaction, and integration scenarios: + +**Grouping and display:** +- Groups entries with the same `metric_id` into one card +- Single-entry families render one variant pill +- Multi-entry families render one pill per entry +- Card title shows base metric ID and category from first entry +- Card shows shared target percentage + +**Worst-status computation:** +- All "Meets/Exceeds Target" → card shows "OK" with success color +- Mix of statuses → card uses the worst status color +- Single "Below 15% of Target" among passing variants → card shows danger color + +**Variant pills:** +- Each pill shows the entry's team label and compliance percentage +- Pill background tint matches the entry's individual status color +- Non-passing variants show a glow dot + +**Hover tooltip:** +- Tooltip appears after 300ms hover delay +- Tooltip shows metric title, business justification, data sources from definitions +- Tooltip disappears on mouse leave +- Tooltip falls back to summary description when no definition exists +- Tooltip does not interfere with card click + +**Info panel:** +- Info icon click opens MetricInfoPanel with correct metric definition +- Info icon click does not trigger card's metric filter toggle (stopPropagation) +- Panel displays all 12+ definition fields with section labels +- Panel shows fallback message when no definition exists +- Panel closes on outside click or close button + +**Device table filtering:** +- Clicking a grouped card sets filter to all metric IDs in that family +- Filtered device table shows devices matching any ID in the family +- Clicking the same card again clears the filter +- Clear filter button resets to show all devices +- Active card shows highlighted styling + +**Definitions file:** +- File imports without error +- Lookup map contains all metric IDs from the file +- All entries have the required 14 fields + +### Property-Based Tests + +Property-based tests use [fast-check](https://github.com/dubzzz/fast-check) to verify universal properties across generated inputs. Each test runs a minimum of 100 iterations. + +| Property | Test Description | Tag | +|---|---|---| +| Property 1 | Generate random arrays of summary entries with varying metric_id values and team strings. Group them and verify: all entries accounted for, entries within each group share the same metric_id, group count equals unique metric_id count. | Feature: compliance-metric-grouping, Property 1: Grouping invariant — no entries lost or misplaced | +| Property 2 | Generate random non-empty arrays of status strings from the valid set. Compute worst status and verify it matches the minimum severity rank in the array. | Feature: compliance-metric-grouping, Property 2: Worst-status computation follows severity ordering | +| Property 3 | Generate random device arrays (each with random failing_metrics) and random filter ID arrays. Filter and verify the result contains exactly the devices with at least one matching metric. | Feature: compliance-metric-grouping, Property 3: Device filtering with metric family includes all matching devices | +| Property 4 | Generate random arrays of metric definitions with unique IDs. Build lookup map, query with IDs from the array (expect hit) and IDs not in the array (expect miss). | Feature: compliance-metric-grouping, Property 4: Definition lookup returns correct entry or null | +| Property 5 | Generate random metric definition objects with all 14 fields. Extract the rendered field keys and verify all required keys are present. | Feature: compliance-metric-grouping, Property 5: Detail panel renders all required definition fields | +| Property 6 | Generate random arrays of metric definition objects. Verify every entry has all 14 keys present, metric_id is a non-empty string, and optional fields are strings (not null/undefined). | Feature: compliance-metric-grouping, Property 6: Definitions schema validation — all entries have required fields | +| Property 7 | Generate random definition arrays with unique IDs. Build lookup map and verify map size equals array length, and every definition is retrievable by its metric_id. | Feature: compliance-metric-grouping, Property 7: Lookup map construction preserves all definitions | +| Property 8 | Generate random metric definition objects with string values. Round-trip through JSON.stringify then JSON.parse and verify deep equality. | Feature: compliance-metric-grouping, Property 8: JSON round-trip preserves metric definition data | + +### Test Configuration + +- **Library:** fast-check (JavaScript property-based testing) +- **Runner:** Jest (via react-scripts test) +- **Iterations:** Minimum 100 per property test (`fc.assert(property, { numRuns: 100 })`) +- **Tag format:** Comment at top of each property test referencing the design property diff --git a/.kiro/specs/compliance-metric-grouping/requirements.md b/.kiro/specs/compliance-metric-grouping/requirements.md new file mode 100644 index 0000000..6f3f325 --- /dev/null +++ b/.kiro/specs/compliance-metric-grouping/requirements.md @@ -0,0 +1,121 @@ +# Requirements Document + +## Introduction + +The AEO Compliance page currently renders one metric health card per `metric_id` returned from the summary endpoint. Many metrics share the same base ID but differ by network variant suffix (e.g., `-Corp`, `-Cust`, `-SpecBus`) in the definitions reference table. This feature groups those variant entries into a single card per metric family, adds hover tooltips with metric descriptions for quick context, provides an info panel for full metric definitions, and ships a static JSON reference file containing the complete metric definitions data. The goal is to reduce card clutter, surface metric context to engineers unfamiliar with the metrics, and preserve the existing card-click filtering behavior. + +## Glossary + +- **Compliance_Page**: The `CompliancePage.js` React component that renders metric health cards, team tabs, and the device violation table +- **Metric_Family**: A group of summary entries that share the same base metric ID (e.g., `5.2.5`), regardless of network variant suffix +- **Network_Variant**: A suffix classification from the metric definitions table indicating which network a metric applies to — Corp, Cust, or SpecBus +- **Variant_Pill**: A small inline badge within a grouped metric card that displays a single network variant's suffix label and its compliance percentage +- **Metric_Health_Card**: The existing `MetricHealthCard` button component that displays a metric's compliance status, now extended to support grouped variants +- **Worst_Status**: The most severe compliance status among all variants in a Metric_Family, used to determine the card's overall border and status color +- **Hover_Tooltip**: A floating overlay that appears on mouse hover over a Metric_Health_Card, showing the metric title, business justification, and data sources +- **Info_Icon**: A small `Info` icon from lucide-react placed in the corner of each Metric_Health_Card that opens the Detail_Panel on click +- **Detail_Panel**: A slide-out or inline expandable section that displays the full metric definition including scope, filters, exclusions, and per-variant notes +- **Metric_Definitions_File**: A static JSON file shipped with the frontend containing structured metric definition data for all tracked metrics +- **Design_System**: The color palette, typography, component specs, and interaction patterns defined in `DESIGN_SYSTEM.md` +- **Summary_Entry**: A single row from the backend's `/api/compliance/summary` response, containing `metric_id`, `team`, `compliance_pct`, `target`, `status`, `description`, and `category` +- **Device_Table**: The lower section of the Compliance_Page that lists non-compliant devices, filterable by metric + +## Requirements + +### Requirement 1: Metric Family Grouping + +**User Story:** As an engineer, I want metrics that share the same base ID to be consolidated into a single card, so that the compliance page is less cluttered and I can see the full picture for each metric family at a glance. + +#### Acceptance Criteria + +1. WHEN the Compliance_Page receives Summary_Entry data, THE Compliance_Page SHALL group entries by their base metric ID to form Metric_Family groups +2. THE Compliance_Page SHALL render one Metric_Health_Card per Metric_Family instead of one card per Summary_Entry +3. THE Metric_Health_Card SHALL display the base metric ID (e.g., `5.2.5`) as the card title and the category name from the first entry in the group +4. THE Metric_Health_Card SHALL display one Variant_Pill for each Summary_Entry in the Metric_Family, showing the variant's team label and compliance percentage +5. WHEN a Metric_Family contains only one Summary_Entry, THE Metric_Health_Card SHALL display a single Variant_Pill — the layout scales naturally without special-casing +6. THE Metric_Health_Card SHALL determine its overall border color and status indicator using the Worst_Status among all variants in the Metric_Family +7. THE Metric_Health_Card SHALL display the shared target percentage from the Metric_Family entries +8. WHEN a user clicks a grouped Metric_Health_Card, THE Compliance_Page SHALL filter the Device_Table to show violations across all metric IDs belonging to that Metric_Family + +### Requirement 2: Variant Pill Display + +**User Story:** As an engineer, I want to see each network variant's compliance percentage inside the grouped card, so that I can quickly identify which variant is underperforming. + +#### Acceptance Criteria + +1. THE Variant_Pill SHALL display the variant's distinguishing label (team name or suffix) and its compliance percentage in monospace font +2. THE Variant_Pill SHALL use a background tint derived from the variant's individual status color at low opacity, consistent with the Design_System badge pattern +3. WHEN a variant's status is not "Meets/Exceeds Target", THE Variant_Pill SHALL display a subtle glow dot matching the variant's status color to draw attention +4. THE Variant_Pill layout SHALL wrap to multiple rows when the Metric_Family contains more variants than fit on a single line + +### Requirement 3: Worst-Status Card Coloring + +**User Story:** As an engineer, I want the grouped card to immediately show me if any variant is failing, so that I do not have to inspect each variant individually to find problems. + +#### Acceptance Criteria + +1. THE Metric_Health_Card SHALL compute the Worst_Status by selecting the most severe status from all Summary_Entry items in the Metric_Family, using the severity order: "Below 15% of Target" (worst) > "Within 15% of Target" > "Meets/Exceeds Target" (best) +2. THE Metric_Health_Card SHALL apply the Worst_Status color to its border, status pill text, and status dot +3. WHEN all variants in a Metric_Family meet or exceed the target, THE Metric_Health_Card SHALL display the "OK" status indicator with the success color + +### Requirement 4: Hover Tooltip for Quick Context + +**User Story:** As an engineer unfamiliar with the metrics, I want to hover over a metric card and see a brief description, so that I can understand what the metric measures without disrupting my workflow. + +#### Acceptance Criteria + +1. WHEN a user hovers over a Metric_Health_Card for more than 300 milliseconds, THE Compliance_Page SHALL display a Hover_Tooltip positioned near the card +2. THE Hover_Tooltip SHALL display the metric title, a one-liner business justification, and the data sources required, sourced from the Metric_Definitions_File +3. THE Hover_Tooltip SHALL use the Design_System dark card background with a subtle border and shadow for readability +4. WHEN the user moves the cursor away from the Metric_Health_Card, THE Hover_Tooltip SHALL disappear +5. THE Hover_Tooltip SHALL NOT interfere with the card's click behavior for filtering the Device_Table +6. IF no definition exists in the Metric_Definitions_File for a given metric, THEN THE Hover_Tooltip SHALL display the metric description from the Summary_Entry data as a fallback + +### Requirement 5: Info Icon and Detail Panel + +**User Story:** As an engineer, I want to click an info icon on a metric card to see the full metric definition, so that I can understand the exact scope, filters, and exclusions without leaving the compliance page. + +#### Acceptance Criteria + +1. THE Metric_Health_Card SHALL display an Info_Icon (lucide-react `Info`) in the top-right corner of the card +2. WHEN a user clicks the Info_Icon, THE Compliance_Page SHALL open a Detail_Panel displaying the full metric definition from the Metric_Definitions_File +3. THE Detail_Panel SHALL display: metric title, asset types in scope, application types in scope, environment in scope, status in scope, instance types in scope, criticality levels in scope, exclusions, special conditions, data sources required, business justification, and per-variant notes +4. THE Detail_Panel SHALL use the Design_System dark theme with section labels in monospace uppercase and content in the standard text colors +5. WHEN a user clicks the Info_Icon, THE click event SHALL NOT propagate to the Metric_Health_Card's onClick handler that filters the Device_Table +6. WHEN a user clicks outside the Detail_Panel or clicks a close button, THE Detail_Panel SHALL close +7. IF no definition exists in the Metric_Definitions_File for a given metric, THEN THE Detail_Panel SHALL display a "No detailed definition available" message with the Summary_Entry description as fallback content + +### Requirement 6: Metric Definitions Data File + +**User Story:** As a developer, I want metric definitions stored as a static JSON file in the frontend, so that the tooltip and detail panel can render metric context without additional API calls. + +#### Acceptance Criteria + +1. THE Metric_Definitions_File SHALL be a JSON file located in the frontend source directory (e.g., `frontend/src/data/metricDefinitions.json`) +2. THE Metric_Definitions_File SHALL contain an entry for each metric ID with the following fields: metric_id, metric_title, asset_types, asset_types_in_scope, application_types_in_scope, environment_in_scope, status_in_scope, instance_types_in_scope, criticality_levels_in_scope, exclusions, special_conditions, data_sources_required, business_justification, and notes +3. THE Metric_Definitions_File SHALL be importable as a standard JavaScript module using a static import statement +4. WHEN the Metric_Definitions_File is loaded, THE Compliance_Page SHALL build a lookup map keyed by metric_id for efficient access +5. THE Metric_Definitions_File SHALL use a flat array structure where each entry represents one metric row from the definitions table + +### Requirement 7: Preserved Card-Click Filtering Behavior + +**User Story:** As an engineer, I want clicking a grouped metric card to still filter the device table, so that the existing workflow for investigating violations is not disrupted. + +#### Acceptance Criteria + +1. WHEN a user clicks a grouped Metric_Health_Card (outside the Info_Icon), THE Compliance_Page SHALL set the metric filter to include all metric IDs in that Metric_Family +2. WHEN a metric filter is active for a Metric_Family, THE Device_Table SHALL display devices that have violations for any metric ID within that family +3. WHEN a user clicks the same grouped Metric_Health_Card again, THE Compliance_Page SHALL clear the metric filter +4. THE "clear filter" button in the metric health section header SHALL continue to reset the filter to show all devices +5. THE Metric_Health_Card SHALL visually indicate the active/selected state using the existing highlight pattern (tinted background with the status color) + +### Requirement 8: Metric Definitions File Structure and Round-Trip Integrity + +**User Story:** As a developer, I want the metric definitions JSON to be parseable and printable without data loss, so that the file can be maintained and validated reliably. + +#### Acceptance Criteria + +1. THE Metric_Definitions_File SHALL be valid JSON that parses without error using `JSON.parse()` +2. FOR ALL entries in the Metric_Definitions_File, parsing the JSON then stringifying it then parsing it again SHALL produce an equivalent object (round-trip property) +3. THE Metric_Definitions_File SHALL contain a `metric_id` field in every entry that is a non-empty string +4. IF an optional field (exclusions, special_conditions, notes) has no value for a metric, THEN THE Metric_Definitions_File SHALL represent it as an empty string rather than omitting the key diff --git a/.kiro/specs/compliance-metric-grouping/tasks.md b/.kiro/specs/compliance-metric-grouping/tasks.md new file mode 100644 index 0000000..1e2a447 --- /dev/null +++ b/.kiro/specs/compliance-metric-grouping/tasks.md @@ -0,0 +1,178 @@ +# Implementation Plan: Compliance Metric Grouping + +## Overview + +Consolidate the AEO Compliance page's metric health cards from one-per-summary-entry to one-per-metric-family. Add variant pills inside each grouped card, a hover tooltip with metric context (300ms delay), an info panel for full metric definitions, and a static `metricDefinitions.json` data file. All work is frontend-only — no backend changes needed. The `metricFilter` state changes from `string|null` to `string[]|null` to support filtering by all metric IDs in a family. + +## Tasks + +- [x] 1. Create metric definitions data file and install test dependencies + - [x] 1.1 Create `frontend/src/data/metricDefinitions.json` + - Create the `frontend/src/data/` directory + - Build the JSON array from the metric definitions table provided by the user (130+ rows, 14 fields each) + - Each entry must have all 14 keys: `metric_id`, `metric_title`, `asset_types`, `asset_types_in_scope`, `application_types_in_scope`, `environment_in_scope`, `status_in_scope`, `instance_types_in_scope`, `criticality_levels_in_scope`, `exclusions`, `special_conditions`, `data_sources_required`, `business_justification`, `notes` + - Use empty strings for optional fields with no value — never `null` or omitted keys + - Verify the file imports without error via a quick `JSON.parse` check + - _Requirements: 6.1, 6.2, 6.3, 6.5, 8.3, 8.4_ + + - [x] 1.2 Install `fast-check` as a dev dependency + - Run `npm install --save-dev fast-check` in `frontend/` + - Verify it appears in `package.json` devDependencies + - _Requirements: (testing infrastructure)_ + +- [x] 2. Implement pure helper functions and their tests + - [x] 2.1 Add `computeWorstStatus` and `groupByMetricFamily` helpers to CompliancePage.js + - Add `STATUS_SEVERITY` map: `{ 'Below 15% of Target': 0, 'Within 15% of Target': 1, 'Meets/Exceeds Target': 2 }` + - Implement `computeWorstStatus(statuses)` — returns the status with the lowest severity rank from a non-empty array + - Implement `groupByMetricFamily(allEntries, team)` — filters entries by team, groups by `metric_id`, returns array of `{ metricId, entries, category, target, worstStatus }` objects + - Export both functions for testing (named exports alongside the default CompliancePage export) + - Remove the existing `teamMetrics()` helper (replaced by `groupByMetricFamily`) + - _Requirements: 1.1, 1.2, 1.6, 3.1_ + + - [x] 2.2 Write property test: Grouping invariant — no entries lost or misplaced + - **Property 1: Grouping invariant — no entries lost or misplaced** + - Create test file `frontend/src/components/pages/__tests__/complianceGrouping.property.test.js` + - Generate random arrays of summary entry objects with varying `metric_id` and `team` values + - Verify: (a) every entry appears in exactly one group, (b) all entries within a group share the same `metric_id`, (c) total entries across groups equals team-filtered input count + - Use `fc.assert(property, { numRuns: 100 })` + - **Validates: Requirements 1.1, 1.2** + + - [x] 2.3 Write property test: Worst-status computation follows severity ordering + - **Property 2: Worst-status computation follows severity ordering** + - Generate random non-empty arrays of status strings from `{"Below 15% of Target", "Within 15% of Target", "Meets/Exceeds Target"}` + - Verify the result is the status with the lowest severity rank present in the array + - If the array contains "Below 15% of Target", the result must be "Below 15% of Target" + - Use `fc.assert(property, { numRuns: 100 })` + - **Validates: Requirements 1.6, 3.1** + + - [ ]* 2.4 Write property test: Device filtering with metric family includes all matching devices + - **Property 3: Device filtering with metric family includes all matching devices** + - Generate random device arrays (each with a `failing_metrics` array of `{ metric_id }` objects) and random filter ID arrays + - Verify the filtered result contains exactly those devices with at least one matching `metric_id` in the filter array + - Use `fc.assert(property, { numRuns: 100 })` + - **Validates: Requirements 1.8, 7.1, 7.2** + +- [x] 3. Checkpoint — Verify helpers and property tests + - Ensure all tests pass, ask the user if questions arise. + +- [x] 4. Redesign MetricHealthCard with variant pills and worst-status coloring + - [x] 4.1 Redesign `MetricHealthCard` to accept a family group + - Change props from `{ entry, active, onClick }` to `{ family, active, onClick, onInfoClick, definitionLookup }` + - `family` is `{ metricId, entries, category, target, worstStatus }` + - Display base `metricId` as card title and `category` from the family + - Display shared `target` percentage + - Use `worstStatus` color for card border, status pill text, and status dot + - When all variants meet/exceed target, show "OK" status indicator with success color + - Add `Info` icon (lucide-react) in the top-right corner with `stopPropagation` on click to call `onInfoClick(family.metricId)` + - _Requirements: 1.2, 1.3, 1.6, 1.7, 3.1, 3.2, 3.3, 5.1, 5.5_ + + - [x] 4.2 Implement `VariantPill` inline sub-component + - Render one pill per `entry` in `family.entries` + - Each pill shows the entry's `description` or `team` label and compliance percentage in monospace + - Background tint from the entry's individual status color at ~12% opacity + - Show a glow dot when the variant's status is not "Meets/Exceeds Target" + - Layout: `inline-flex` with `flexWrap: 'wrap'` on the parent container + - _Requirements: 1.4, 1.5, 2.1, 2.2, 2.3, 2.4_ + +- [x] 5. Update CompliancePage state and rendering to use grouped families + - [x] 5.1 Add new imports and build the definitions lookup map + - Import `{ Info }` from `lucide-react` + - Import `MetricInfoPanel` from `./MetricInfoPanel` (created in task 6) + - Import `metricDefinitionsRaw` from `../../data/metricDefinitions.json` + - Build `METRIC_DEFINITIONS` lookup object at module level keyed by `metric_id` + - _Requirements: 6.3, 6.4_ + + - [x] 5.2 Update state management and filter logic + - Change `metricFilter` from `string|null` to `string[]|null` + - Add new state: `infoMetric` (`string|null`) — which metric's info panel is open + - Add new state: `hoveredMetric` (`string|null`) — which metric is being hovered + - Add new ref: `hoverTimeoutRef` — for 300ms delay management + - Update `filteredDevices` filter from `m.metric_id === metricFilter` to `metricFilter.includes(m.metric_id)` + - _Requirements: 7.1, 7.2_ + + - [x] 5.3 Update card rendering to use `groupByMetricFamily` + - Replace `const metrics = teamMetrics(summary.entries, activeTeam)` with `const families = groupByMetricFamily(summary.entries, activeTeam)` + - Replace `metrics.map(entry => )` with `families.map(family => )` + - On card click: set `metricFilter` to `family.entries.map(e => e.metric_id)` (array of all IDs in the family), or clear if already active + - Active state check: compare `metricFilter` array contents against the family's metric IDs + - Pass `onInfoClick` handler that sets `infoMetric` state + - Pass `definitionLookup` as `METRIC_DEFINITIONS` + - _Requirements: 1.2, 1.8, 7.1, 7.3, 7.4, 7.5_ + +- [x] 6. Implement MetricInfoPanel component + - [x] 6.1 Create `frontend/src/components/pages/MetricInfoPanel.js` + - Props: `metricId`, `definition` (from lookup or null), `summaryEntries` (family entries for fallback), `onClose` + - Render overlay/slide-out panel with dark theme matching DESIGN_SYSTEM.md + - Close button (X icon, top-right) + - Metric title in h3 monospace + - Sections with monospace uppercase labels: Asset Types, Asset Types In Scope, Application Types In Scope, Environment In Scope, Status In Scope, Instance Types In Scope, Criticality Levels In Scope, Exclusions, Special Conditions, Data Sources Required, Business Justification, Notes + - Show "—" placeholder for empty string fields + - If `definition` is null: show "No detailed definition available" with summary description fallback + - Click outside or close button calls `onClose()` + - _Requirements: 5.2, 5.3, 5.4, 5.6, 5.7_ + +- [x] 7. Implement HoverTooltip inline in CompliancePage + - [x] 7.1 Add hover tooltip logic and rendering + - On `mouseEnter` on MetricHealthCard: set 300ms timeout, then set `hoveredMetric` to `family.metricId` + - On `mouseLeave`: clear timeout, set `hoveredMetric` to null + - Render tooltip when `hoveredMetric` matches a family — positioned near the card using `getBoundingClientRect()` + - Tooltip content: metric title, business justification, data sources required (from `METRIC_DEFINITIONS` lookup) + - Fall back to summary entry `description` when no definition exists + - Dark card background with subtle border and shadow per DESIGN_SYSTEM.md + - Tooltip must not interfere with card click behavior + - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6_ + +- [x] 8. Checkpoint — Verify full UI integration + - Ensure all tests pass, ask the user if questions arise. + +- [x] 9. Property tests for definitions data and lookup + - [x] 9.1 Write property test: Definition lookup returns correct entry or null + - **Property 4: Definition lookup returns correct entry or null** + - Create test file `frontend/src/components/pages/__tests__/metricDefinitions.property.test.js` + - Generate random arrays of metric definition objects with unique `metric_id` values + - Build lookup map, query with IDs from the array (expect hit) and IDs not in the array (expect miss) + - Use `fc.assert(property, { numRuns: 100 })` + - **Validates: Requirements 4.2, 4.6** + + - [x] 9.2 Write property test: Detail panel renders all required definition fields + - **Property 5: Detail panel renders all required definition fields** + - Generate random metric definition objects with all 14 fields + - Extract the set of field keys that the MetricInfoPanel renders and verify all required keys are present + - Use `fc.assert(property, { numRuns: 100 })` + - **Validates: Requirements 5.3** + + - [x] 9.3 Write property test: Definitions schema validation — all entries have required fields + - **Property 6: Definitions schema validation — all entries have required fields** + - Generate random arrays of metric definition objects + - Verify every entry has all 14 keys present, `metric_id` is a non-empty string, and optional fields (`exclusions`, `special_conditions`, `notes`) are strings (not null/undefined) + - Use `fc.assert(property, { numRuns: 100 })` + - **Validates: Requirements 6.2, 8.3, 8.4** + + - [x] 9.4 Write property test: Lookup map construction preserves all definitions + - **Property 7: Lookup map construction preserves all definitions** + - Generate random definition arrays with unique `metric_id` values + - Build lookup map and verify map size equals array length, and every definition is retrievable by its `metric_id` + - Use `fc.assert(property, { numRuns: 100 })` + - **Validates: Requirements 6.4** + + - [x] 9.5 Write property test: JSON round-trip preserves metric definition data + - **Property 8: JSON round-trip preserves metric definition data** + - Generate random metric definition objects with string values for all fields + - Round-trip through `JSON.stringify` then `JSON.parse` and verify deep equality + - Use `fc.assert(property, { numRuns: 100 })` + - **Validates: Requirements 8.1, 8.2** + +- [x] 10. Final checkpoint — Ensure all tests pass + - Ensure all tests pass, ask the user if questions arise. + +## Notes + +- Tasks marked with `*` are optional and can be skipped for faster MVP +- Each task references specific requirements for traceability +- Checkpoints ensure incremental validation +- Property tests validate universal correctness properties from the design document using fast-check +- Unit tests validate specific examples and edge cases +- All styling follows the project convention of inline styles (no CSS modules or Tailwind) +- The `fast-check` library must be installed as a dev dependency before running property tests +- The `metricDefinitions.json` file contains 130+ rows — the user will provide the metric definitions table data for conversion +- `computeWorstStatus` and `groupByMetricFamily` are exported as named exports from CompliancePage.js for testability diff --git a/frontend/package.json b/frontend/package.json index 299b6ae..d606676 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,5 +41,16 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "jest": { + "transformIgnorePatterns": [ + "node_modules/(?!(fast-check)/)" + ], + "moduleNameMapper": { + "^pure-rand/(.*)$": "/node_modules/pure-rand/lib/$1.js" + } + }, + "devDependencies": { + "fast-check": "^4.7.0" } } diff --git a/frontend/src/components/pages/CompliancePage.js b/frontend/src/components/pages/CompliancePage.js index 7ec2d1d..9964100 100644 --- a/frontend/src/components/pages/CompliancePage.js +++ b/frontend/src/components/pages/CompliancePage.js @@ -1,14 +1,22 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { Upload, MessageSquare, RefreshCw, AlertCircle, Loader, RotateCcw } from 'lucide-react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import { Upload, MessageSquare, RefreshCw, AlertCircle, Loader, RotateCcw, Info } from 'lucide-react'; import { useAuth } from '../../contexts/AuthContext'; import ComplianceUploadModal from './ComplianceUploadModal'; import ComplianceDetailPanel from './ComplianceDetailPanel'; import ComplianceChartsPanel from './ComplianceChartsPanel'; +import MetricInfoPanel from './MetricInfoPanel'; +import metricDefinitionsRaw from '../../data/metricDefinitions.json'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; const TEAL = '#14B8A6'; const TEAMS = ['STEAM', 'ACCESS-ENG']; +// Build definitions lookup map once at module level +const METRIC_DEFINITIONS = {}; +for (const def of metricDefinitionsRaw) { + METRIC_DEFINITIONS[def.metric_id] = def; +} + // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- @@ -38,18 +46,83 @@ function pctDisplay(pct) { return `${Math.round(pct * 100)}%`; } -// Deduplicate summary entries — one per metric_id for the selected team -// (exclude aggregate "ALL: NTS-AEO" rows) -function teamMetrics(entries, team) { - return entries.filter(e => e.team === team); +const STATUS_SEVERITY = { + 'Below 15% of Target': 0, + 'Within 15% of Target': 1, + 'Meets/Exceeds Target': 2, +}; + +function computeWorstStatus(statuses) { + let worst = 'Meets/Exceeds Target'; + let worstSev = 2; + for (const s of statuses) { + const sev = STATUS_SEVERITY[s] ?? 0; + if (sev < worstSev) { + worstSev = sev; + worst = s; + } + } + return worst; +} + +function groupByMetricFamily(allEntries, team) { + const teamEntries = allEntries.filter(e => e.team === team); + const familyMap = {}; + + for (const entry of teamEntries) { + const baseId = entry.metric_id; + if (!baseId) continue; + if (!familyMap[baseId]) { + familyMap[baseId] = []; + } + familyMap[baseId].push(entry); + } + + return Object.entries(familyMap).map(([metricId, entries]) => ({ + metricId, + entries, + category: entries[0].category, + target: entries[0].target, + worstStatus: computeWorstStatus(entries.map(e => e.status)), + })); } // --------------------------------------------------------------------------- // Sub-components // --------------------------------------------------------------------------- -function MetricHealthCard({ entry, active, onClick }) { +function VariantPill({ entry }) { const color = statusColor(entry.status); - const isOk = entry.status === 'Meets/Exceeds Target'; + const isOk = entry.status === 'Meets/Exceeds Target'; + return ( + + {!isOk && ( + + )} + {entry.description || entry.team} + {pctDisplay(entry.compliance_pct)} + + ); +} + +function MetricHealthCard({ family, active, onClick, onInfoClick, definitionLookup }) { + const color = statusColor(family.worstStatus); + const isOk = family.worstStatus === 'Meets/Exceeds Target'; return ( ); @@ -158,6 +256,10 @@ export default function CompliancePage({ onNavigate }) { const [rollbackConfirm, setRollbackConfirm] = useState(false); const [rollbackLoading, setRollbackLoading] = useState(false); const [rollbackResult, setRollbackResult] = useState(null); + const [infoMetric, setInfoMetric] = useState(null); + const [hoveredMetric, setHoveredMetric] = useState(null); + const hoverTimeoutRef = useRef(null); + const hoveredCardRef = useRef(null); const fetchSummary = useCallback(async (team) => { try { @@ -225,10 +327,10 @@ export default function CompliancePage({ onNavigate }) { // In-memory filters const filteredDevices = devices - .filter(d => !metricFilter || d.failing_metrics.some(m => m.metric_id === metricFilter)) + .filter(d => !metricFilter || d.failing_metrics.some(m => metricFilter.includes(m.metric_id))) .filter(d => !hostSearch || d.hostname.toLowerCase().includes(hostSearch.toLowerCase())); - const metrics = teamMetrics(summary.entries, activeTeam); + const families = groupByMetricFamily(summary.entries, activeTeam); const lastUpload = summary.upload; return ( @@ -336,7 +438,7 @@ export default function CompliancePage({ onNavigate }) { {/* ── Metric health cards ──────────────────────────────────── */} - {metrics.length > 0 ? ( + {families.length > 0 ? (
Metric Health — click to filter @@ -348,15 +450,81 @@ export default function CompliancePage({ onNavigate }) { )}
- {metrics.map(entry => ( - setMetricFilter(metricFilter === entry.metric_id ? null : entry.metric_id)} - /> - ))} + {families.map(family => { + const familyIds = family.entries.map(e => e.metric_id); + const isActive = metricFilter !== null && metricFilter.length === familyIds.length && familyIds.every(id => metricFilter.includes(id)); + return ( +
{ + hoveredCardRef.current = e.currentTarget; + if (hoverTimeoutRef.current) clearTimeout(hoverTimeoutRef.current); + hoverTimeoutRef.current = setTimeout(() => setHoveredMetric(family.metricId), 300); + }} + onMouseLeave={() => { + if (hoverTimeoutRef.current) clearTimeout(hoverTimeoutRef.current); + hoverTimeoutRef.current = null; + hoveredCardRef.current = null; + setHoveredMetric(null); + }} + style={{ display: 'flex', flex: '1 1 0', minWidth: '160px' }} + > + setMetricFilter(isActive ? null : familyIds)} + onInfoClick={(metricId) => setInfoMetric(metricId)} + definitionLookup={METRIC_DEFINITIONS} + /> +
+ ); + })}
+ + {/* Hover tooltip */} + {hoveredMetric && (() => { + const family = families.find(f => f.metricId === hoveredMetric); + if (!family) return null; + const def = METRIC_DEFINITIONS[hoveredMetric]; + const rect = hoveredCardRef.current ? hoveredCardRef.current.getBoundingClientRect() : null; + if (!rect) return null; + const tooltipTop = Math.min(rect.bottom + 8, window.innerHeight - 180); + const tooltipLeft = Math.max(8, Math.min(rect.left, window.innerWidth - 320)); + return ( +
+
+ {def ? def.metric_title : (family.entries[0]?.description || hoveredMetric)} +
+ {def && def.business_justification && ( +
+ {def.business_justification} +
+ )} + {def && def.data_sources_required && ( +
+ Sources: {def.data_sources_required} +
+ )} + {!def && family.entries[0]?.description && ( +
+ {family.entries[0].description} +
+ )} +
+ ); + })()}
) : lastUpload === null ? (
)} + {/* ── Metric info panel ───────────────────────────────────── */} + {infoMetric && ( + f.metricId === infoMetric) || {}).entries || []} + onClose={() => setInfoMetric(null)} + /> + )} + {/* ── Rollback confirmation modal ──────────────────────────── */} {rollbackConfirm && lastUpload && (
); } + +// Named exports for testing +export { computeWorstStatus, groupByMetricFamily }; diff --git a/frontend/src/components/pages/MetricInfoPanel.js b/frontend/src/components/pages/MetricInfoPanel.js new file mode 100644 index 0000000..2f56e8f --- /dev/null +++ b/frontend/src/components/pages/MetricInfoPanel.js @@ -0,0 +1,161 @@ +import React from 'react'; +import { X } from 'lucide-react'; + +const TEAL = '#14B8A6'; + +const SECTION_FIELDS = [ + { key: 'asset_types', label: 'Asset Types' }, + { key: 'asset_types_in_scope', label: 'Asset Types In Scope' }, + { key: 'application_types_in_scope', label: 'Application Types In Scope' }, + { key: 'environment_in_scope', label: 'Environment In Scope' }, + { key: 'status_in_scope', label: 'Status In Scope' }, + { key: 'instance_types_in_scope', label: 'Instance Types In Scope' }, + { key: 'criticality_levels_in_scope', label: 'Criticality Levels In Scope' }, + { key: 'exclusions', label: 'Exclusions' }, + { key: 'special_conditions', label: 'Special Conditions' }, + { key: 'data_sources_required', label: 'Data Sources Required' }, + { key: 'business_justification', label: 'Business Justification' }, + { key: 'notes', label: 'Notes' }, +]; + +export default function MetricInfoPanel({ metricId, definition, summaryEntries, onClose }) { + const handleBackdropClick = (e) => { + if (e.target === e.currentTarget) { + onClose(); + } + }; + + const title = definition + ? definition.metric_title + : (summaryEntries && summaryEntries.length > 0 ? summaryEntries[0].description : metricId); + + return ( +
+
+ {/* Close button */} + + + {/* Metric ID */} +
+ Metric {metricId} +
+ + {/* Title */} +

+ {title} +

+ + {!definition ? ( +
+ No detailed definition available. + {summaryEntries && summaryEntries.length > 0 && ( +
+ {summaryEntries[0].description} +
+ )} +
+ ) : ( +
+ {SECTION_FIELDS.map(({ key, label }) => ( +
+
+ {label} +
+
+ {definition[key] || '—'} +
+
+ ))} +
+ )} +
+
+ ); +} + +// Exported for testing — the list of field keys rendered by the panel +MetricInfoPanel.RENDERED_FIELD_KEYS = SECTION_FIELDS.map(f => f.key); diff --git a/frontend/src/components/pages/__tests__/complianceGrouping.property.test.js b/frontend/src/components/pages/__tests__/complianceGrouping.property.test.js new file mode 100644 index 0000000..279df8a --- /dev/null +++ b/frontend/src/components/pages/__tests__/complianceGrouping.property.test.js @@ -0,0 +1,118 @@ +import fc from 'fast-check'; +import { computeWorstStatus, groupByMetricFamily } from '../CompliancePage'; + +// --------------------------------------------------------------------------- +// Generators +// --------------------------------------------------------------------------- + +const VALID_STATUSES = [ + 'Below 15% of Target', + 'Within 15% of Target', + 'Meets/Exceeds Target', +]; + +const statusArb = fc.constantFrom(...VALID_STATUSES); + +const summaryEntryArb = fc.record({ + metric_id: fc.stringMatching(/^[1-9]\d{0,2}\.\d{1,2}\.\d{1,2}$/), + team: fc.constantFrom('STEAM', 'ACCESS-ENG'), + priority: fc.constantFrom('High', 'Medium', 'Low'), + non_compliant: fc.nat({ max: 500 }), + compliant: fc.nat({ max: 500 }), + total: fc.nat({ max: 1000 }), + compliance_pct: fc.double({ min: 0, max: 1, noNaN: true }), + target: fc.double({ min: 0, max: 1, noNaN: true }), + status: statusArb, + description: fc.string({ minLength: 1, maxLength: 50 }), + category: fc.constantFrom( + 'Vulnerability Management', + 'Access & MFA', + 'Logging & Monitoring', + 'End-of-Life OS', + ), +}); + +// --------------------------------------------------------------------------- +// Property 1: Grouping invariant — no entries lost or misplaced +// Validates: Requirements 1.1, 1.2 +// --------------------------------------------------------------------------- + +describe('Property 1: Grouping invariant — no entries lost or misplaced', () => { + test('every entry appears in exactly one group, groups share metric_id, totals match', () => { + fc.assert( + fc.property( + fc.array(summaryEntryArb, { minLength: 0, maxLength: 30 }), + fc.constantFrom('STEAM', 'ACCESS-ENG'), + (entries, team) => { + const groups = groupByMetricFamily(entries, team); + const teamEntries = entries.filter( + (e) => e.team === team && e.metric_id, + ); + + // (c) total entries across groups equals team-filtered input count + const totalGrouped = groups.reduce( + (sum, g) => sum + g.entries.length, + 0, + ); + expect(totalGrouped).toBe(teamEntries.length); + + // (b) all entries within a group share the same metric_id + for (const group of groups) { + for (const entry of group.entries) { + expect(entry.metric_id).toBe(group.metricId); + } + } + + // (a) every team entry appears in exactly one group + const allGroupedEntries = groups.flatMap((g) => g.entries); + for (const entry of teamEntries) { + const occurrences = allGroupedEntries.filter( + (e) => e === entry, + ).length; + expect(occurrences).toBe(1); + } + }, + ), + { numRuns: 100 }, + ); + }); +}); + +// --------------------------------------------------------------------------- +// Property 2: Worst-status computation follows severity ordering +// Validates: Requirements 1.6, 3.1 +// --------------------------------------------------------------------------- + +const STATUS_SEVERITY = { + 'Below 15% of Target': 0, + 'Within 15% of Target': 1, + 'Meets/Exceeds Target': 2, +}; + +describe('Property 2: Worst-status computation follows severity ordering', () => { + test('result is the status with the lowest severity rank present', () => { + fc.assert( + fc.property( + fc.array(statusArb, { minLength: 1, maxLength: 20 }), + (statuses) => { + const result = computeWorstStatus(statuses); + + // Result must be a valid status + expect(VALID_STATUSES).toContain(result); + + // Result must be the minimum severity present + const minSeverity = Math.min( + ...statuses.map((s) => STATUS_SEVERITY[s]), + ); + expect(STATUS_SEVERITY[result]).toBe(minSeverity); + + // If array contains "Below 15% of Target", result must be that + if (statuses.includes('Below 15% of Target')) { + expect(result).toBe('Below 15% of Target'); + } + }, + ), + { numRuns: 100 }, + ); + }); +}); diff --git a/frontend/src/components/pages/__tests__/metricDefinitions.property.test.js b/frontend/src/components/pages/__tests__/metricDefinitions.property.test.js new file mode 100644 index 0000000..748557b --- /dev/null +++ b/frontend/src/components/pages/__tests__/metricDefinitions.property.test.js @@ -0,0 +1,211 @@ +import fc from 'fast-check'; +import MetricInfoPanel from '../MetricInfoPanel'; + +// --------------------------------------------------------------------------- +// Generators +// --------------------------------------------------------------------------- + +const DEFINITION_KEYS = [ + 'metric_id', + 'metric_title', + 'asset_types', + 'asset_types_in_scope', + 'application_types_in_scope', + 'environment_in_scope', + 'status_in_scope', + 'instance_types_in_scope', + 'criticality_levels_in_scope', + 'exclusions', + 'special_conditions', + 'data_sources_required', + 'business_justification', + 'notes', +]; + +const metricDefinitionArb = fc.record({ + metric_id: fc.stringMatching(/^[1-9]\d{0,2}\.\d{1,2}\.\d{1,2}$/), + metric_title: fc.string({ minLength: 1, maxLength: 80 }), + asset_types: fc.string({ minLength: 0, maxLength: 60 }), + asset_types_in_scope: fc.string({ minLength: 0, maxLength: 60 }), + application_types_in_scope: fc.string({ minLength: 0, maxLength: 60 }), + environment_in_scope: fc.string({ minLength: 0, maxLength: 60 }), + status_in_scope: fc.string({ minLength: 0, maxLength: 60 }), + instance_types_in_scope: fc.string({ minLength: 0, maxLength: 60 }), + criticality_levels_in_scope: fc.string({ minLength: 0, maxLength: 60 }), + exclusions: fc.string({ minLength: 0, maxLength: 60 }), + special_conditions: fc.string({ minLength: 0, maxLength: 60 }), + data_sources_required: fc.string({ minLength: 0, maxLength: 60 }), + business_justification: fc.string({ minLength: 0, maxLength: 60 }), + notes: fc.string({ minLength: 0, maxLength: 60 }), +}); + +/** + * Generate an array of metric definitions with unique metric_id values. + */ +const uniqueDefinitionsArb = fc + .array(metricDefinitionArb, { minLength: 1, maxLength: 20 }) + .map((defs) => { + const seen = new Set(); + return defs.filter((d) => { + if (seen.has(d.metric_id)) return false; + seen.add(d.metric_id); + return true; + }); + }) + .filter((arr) => arr.length > 0); + +// --------------------------------------------------------------------------- +// Property 4: Definition lookup returns correct entry or null +// Validates: Requirements 4.2, 4.6 +// --------------------------------------------------------------------------- + +describe('Property 4: Definition lookup returns correct entry or null', () => { + test('lookup hits for IDs in the array and misses for IDs not in the array', () => { + fc.assert( + fc.property( + uniqueDefinitionsArb, + fc.stringMatching(/^[1-9]\d{0,2}\.\d{1,2}\.\d{1,2}$/), + (definitions, queryId) => { + // Build lookup map + const lookup = {}; + for (const def of definitions) { + lookup[def.metric_id] = def; + } + + // Query with IDs from the array — expect hit + for (const def of definitions) { + expect(lookup[def.metric_id]).toBe(def); + } + + // Query with a random ID — expect hit if present, miss if not + const existsInArray = definitions.some( + (d) => d.metric_id === queryId, + ); + if (existsInArray) { + expect(lookup[queryId]).toBeDefined(); + } else { + expect(lookup[queryId]).toBeUndefined(); + } + }, + ), + { numRuns: 100 }, + ); + }); +}); + +// --------------------------------------------------------------------------- +// Property 5: Detail panel renders all required definition fields +// Validates: Requirements 5.3 +// --------------------------------------------------------------------------- + +describe('Property 5: Detail panel renders all required definition fields', () => { + test('RENDERED_FIELD_KEYS includes all required definition keys (excluding metric_id and metric_title)', () => { + const renderedKeys = MetricInfoPanel.RENDERED_FIELD_KEYS; + + // Keys that are rendered separately (as title/header), not in the section list + const separatelyRendered = ['metric_id', 'metric_title']; + const requiredSectionKeys = DEFINITION_KEYS.filter( + (k) => !separatelyRendered.includes(k), + ); + + fc.assert( + fc.property(metricDefinitionArb, (definition) => { + // Verify every required section key is in the rendered set + for (const key of requiredSectionKeys) { + expect(renderedKeys).toContain(key); + } + + // Verify the definition object has all keys that will be rendered + for (const key of renderedKeys) { + expect(definition).toHaveProperty(key); + } + }), + { numRuns: 100 }, + ); + }); +}); + +// --------------------------------------------------------------------------- +// Property 6: Definitions schema validation — all entries have required fields +// Validates: Requirements 6.2, 8.3, 8.4 +// --------------------------------------------------------------------------- + +describe('Property 6: Definitions schema validation — all entries have required fields', () => { + test('every entry has all 14 keys, metric_id is non-empty string, optional fields are strings', () => { + fc.assert( + fc.property( + fc.array(metricDefinitionArb, { minLength: 1, maxLength: 15 }), + (definitions) => { + for (const def of definitions) { + // All 14 keys present + for (const key of DEFINITION_KEYS) { + expect(def).toHaveProperty(key); + } + + // metric_id is a non-empty string + expect(typeof def.metric_id).toBe('string'); + expect(def.metric_id.length).toBeGreaterThan(0); + + // Optional fields are strings (not null/undefined) + const optionalFields = [ + 'exclusions', + 'special_conditions', + 'notes', + ]; + for (const field of optionalFields) { + expect(typeof def[field]).toBe('string'); + expect(def[field]).not.toBeNull(); + expect(def[field]).not.toBeUndefined(); + } + } + }, + ), + { numRuns: 100 }, + ); + }); +}); + +// --------------------------------------------------------------------------- +// Property 7: Lookup map construction preserves all definitions +// Validates: Requirements 6.4 +// --------------------------------------------------------------------------- + +describe('Property 7: Lookup map construction preserves all definitions', () => { + test('map size equals array length and every definition is retrievable', () => { + fc.assert( + fc.property(uniqueDefinitionsArb, (definitions) => { + // Build lookup map + const lookup = {}; + for (const def of definitions) { + lookup[def.metric_id] = def; + } + + // Map size equals array length + expect(Object.keys(lookup).length).toBe(definitions.length); + + // Every definition is retrievable by its metric_id + for (const def of definitions) { + expect(lookup[def.metric_id]).toBe(def); + } + }), + { numRuns: 100 }, + ); + }); +}); + +// --------------------------------------------------------------------------- +// Property 8: JSON round-trip preserves metric definition data +// Validates: Requirements 8.1, 8.2 +// --------------------------------------------------------------------------- + +describe('Property 8: JSON round-trip preserves metric definition data', () => { + test('JSON.parse(JSON.stringify(definition)) produces a deeply equal object', () => { + fc.assert( + fc.property(metricDefinitionArb, (definition) => { + const roundTripped = JSON.parse(JSON.stringify(definition)); + expect(roundTripped).toEqual(definition); + }), + { numRuns: 100 }, + ); + }); +}); diff --git a/frontend/src/data/metricDefinitions.json b/frontend/src/data/metricDefinitions.json new file mode 100644 index 0000000..32eae4f --- /dev/null +++ b/frontend/src/data/metricDefinitions.json @@ -0,0 +1,1394 @@ +[ + { + "metric_id": "1.1.1", + "metric_title": "% of identified Red Criticality application(s) with a defined owner", + "asset_types": "Applications", + "asset_types_in_scope": "Red Critical Applications", + "application_types_in_scope": "", + "environment_in_scope": "PROD", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "Critical", + "exclusions": "", + "special_conditions": "Business owner field cannot be null or empty", + "data_sources_required": "Cherwell CMDB", + "business_justification": "Critical apps need ownership for incident response", + "notes": "Variants: Corp (no exclusions), Cust (exemption 1.1.1-Cust), SpecBus (WIP trend metric)" + }, + { + "metric_id": "1.1.1A", + "metric_title": "% of identified risk Tier 1 application(s) with a defined owner", + "asset_types": "Applications", + "asset_types_in_scope": "Tier 1 Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Business owner documented, Tier 1 flag must be True", + "data_sources_required": "Cherwell CMDB", + "business_justification": "Tier 1 apps need ownership for risk management", + "notes": "" + }, + { + "metric_id": "1.1.2", + "metric_title": "% of production applications assets that have been classified", + "asset_types": "Assets, Servers", + "asset_types_in_scope": "Production Applications", + "application_types_in_scope": "", + "environment_in_scope": "PROD", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Criticality rating defined (not Undefined or No Criticality)", + "data_sources_required": "Cherwell CMDB", + "business_justification": "Asset classification drives prioritization", + "notes": "Variants: Corp (count assets not applications), Cust (currently not reporting)" + }, + { + "metric_id": "1.1.3", + "metric_title": "% of Red Criticality applications compliant with disaster recovery exercise requirements", + "asset_types": "Applications", + "asset_types_in_scope": "Red Critical Applications", + "application_types_in_scope": "", + "environment_in_scope": "PROD", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "Charter On-Prem/Charter Managed, Charter Private Cloud/Charter Managed, Hybrid/Charter Managed", + "criticality_levels_in_scope": "Critical", + "exclusions": "Admin instances excluded", + "special_conditions": "DR exercise within 365 days", + "data_sources_required": "Cherwell CMDB", + "business_justification": "DR testing ensures business continuity", + "notes": "9box requirements implemented" + }, + { + "metric_id": "1.1.3A", + "metric_title": "% of risk Tier 1 applications compliant with disaster recovery exercise requirements", + "asset_types": "Applications", + "asset_types_in_scope": "Tier 1 Applications", + "application_types_in_scope": "", + "environment_in_scope": "PROD", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "Charter On-Prem/Charter Managed, Charter Private Cloud/Charter Managed, Hybrid/Charter Managed", + "criticality_levels_in_scope": "Critical, High, Medium", + "exclusions": "Admin instances excluded", + "special_conditions": "DR exercise based on criticality thresholds", + "data_sources_required": "Cherwell CMDB", + "business_justification": "DR testing for high-risk applications", + "notes": "" + }, + { + "metric_id": "1.2.2", + "metric_title": "% of servers associated with Red Criticality applications generating actionable logs", + "asset_types": "Servers", + "asset_types_in_scope": "Red Critical Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "Critical", + "exclusions": "Appliances excluded", + "special_conditions": "Logs seen in last 7 days", + "data_sources_required": "Splunk, Cherwell CMDB", + "business_justification": "Log visibility for critical systems", + "notes": "OS or APP logs ingested by SIEM" + }, + { + "metric_id": "1.2.2A", + "metric_title": "% of servers associated with risk Tier 1 applications generating actionable logs", + "asset_types": "Servers", + "asset_types_in_scope": "Tier 1 Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Appliances excluded", + "special_conditions": "Logs ingested by SIEM with actionable alerting", + "data_sources_required": "Cherwell CMDB, Splunk", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "1.2.2All", + "metric_title": "% of servers associated with applications generating actionable security logs", + "asset_types": "Servers", + "asset_types_in_scope": "All Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Appliances excluded", + "special_conditions": "Security logs with actionable alerting", + "data_sources_required": "Cherwell CMDB, Splunk", + "business_justification": "Comprehensive log monitoring", + "notes": "" + }, + { + "metric_id": "1.2.2B", + "metric_title": "% of servers associated w/ Red Criticality applications generating actionable OS logs", + "asset_types": "Servers", + "asset_types_in_scope": "Red Critical Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "Critical", + "exclusions": "Appliances excluded", + "special_conditions": "OS logs in Splunk indices containing 'nix' or 'win'", + "data_sources_required": "Cherwell CMDB, Splunk", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "1.2.2C", + "metric_title": "% of servers associated w/ Red Criticality applications generating actionable APP logs", + "asset_types": "Servers", + "asset_types_in_scope": "Red Critical Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "Critical", + "exclusions": "Appliances excluded", + "special_conditions": "APP logs in Splunk indices NOT containing 'nix' or 'win'", + "data_sources_required": "Cherwell CMDB, Splunk", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "1.2.3", + "metric_title": "% of servers associated with Red Criticality applications monitored for compliance with a defined configuration baseline", + "asset_types": "Servers", + "asset_types_in_scope": "Red Critical Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "Critical", + "exclusions": "Appliances excluded", + "special_conditions": "Tanium deployed and monitoring", + "data_sources_required": "Cherwell CMDB, Tanium", + "business_justification": "Configuration drift detection", + "notes": "" + }, + { + "metric_id": "1.2.3A", + "metric_title": "% of servers passing configuration compliance", + "asset_types": "Servers", + "asset_types_in_scope": "All Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Appliances excluded", + "special_conditions": "Tanium compliance percentage >= 0.9", + "data_sources_required": "Tanium", + "business_justification": "90% compliance threshold", + "notes": "" + }, + { + "metric_id": "1.2.3All", + "metric_title": "% of servers monitored for compliance with a defined configuration baseline", + "asset_types": "Servers", + "asset_types_in_scope": "All Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Appliances excluded", + "special_conditions": "Tanium deployed and monitoring", + "data_sources_required": "Cherwell CMDB, Tanium", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "1.2.4", + "metric_title": "% Red critical servers with confirmed supported operating systems", + "asset_types": "Servers", + "asset_types_in_scope": "Red Critical Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "Critical", + "exclusions": "Appliances excluded", + "special_conditions": "OS not past end of life and EOL date known", + "data_sources_required": "Cherwell CMDB, ESD", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "1.2.4A", + "metric_title": "% of risk Tier 1 applications without end of support operating system", + "asset_types": "Applications", + "asset_types_in_scope": "Tier 1 Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Appliances excluded", + "special_conditions": "Applications not utilizing EOL systems", + "data_sources_required": "Cherwell CMDB, ESD", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "1.2.5", + "metric_title": "% of servers associated with Red Criticality Applications with installed and functioning endpoint security agents", + "asset_types": "Servers", + "asset_types_in_scope": "Red Critical Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "Critical", + "exclusions": "Appliances excluded", + "special_conditions": "CrowdStrike agent active within 7 days", + "data_sources_required": "Cherwell CMDB, CrowdStrike", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "1.2.5A", + "metric_title": "% of servers associated with risk Tier 1 Applications with installed and functioning endpoint security agents", + "asset_types": "Servers", + "asset_types_in_scope": "Tier 1 Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Appliances excluded", + "special_conditions": "CrowdStrike agent active within 7 days", + "data_sources_required": "Cherwell CMDB, CrowdStrike", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "1.2.5All", + "metric_title": "% of servers with installed and functioning endpoint security agents", + "asset_types": "Servers", + "asset_types_in_scope": "All Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Appliances excluded", + "special_conditions": "CrowdStrike agent active within 7 days", + "data_sources_required": "Cherwell CMDB, CrowdStrike", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "1.3.1A", + "metric_title": "% of vulnerabilities (critical and high) associated with Tier 1 Applications detected within SLA / Policy", + "asset_types": "Assets", + "asset_types_in_scope": "Tier 1 Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Critical: 15 days, High: 60 days from first found", + "data_sources_required": "Cherwell CMDB, Kenna", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "1.4.1", + "metric_title": "% of Red Criticality applications compliant with Business Impact Analysis review requirements", + "asset_types": "Applications", + "asset_types_in_scope": "Red Critical Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All except Admin", + "criticality_levels_in_scope": "Critical", + "exclusions": "Admin instances excluded", + "special_conditions": "BIA completed within 365 days", + "data_sources_required": "Cherwell CMDB", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "1.4.1A", + "metric_title": "% of Tier 1 applications compliant with Business Impact Analysis review requirements", + "asset_types": "Applications", + "asset_types_in_scope": "Tier 1 Applications", + "application_types_in_scope": "", + "environment_in_scope": "PROD", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All except Admin", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Admin instances excluded", + "special_conditions": "BIA based on criticality: Low=731 days, others=366 days", + "data_sources_required": "Cherwell CMDB", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "1.4.1All", + "metric_title": "% of applications compliant with Business Impact Analysis review requirements", + "asset_types": "Applications", + "asset_types_in_scope": "All Applications", + "application_types_in_scope": "", + "environment_in_scope": "PROD", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All except Admin", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Admin instances excluded", + "special_conditions": "BIA based on criticality: Low=731 days, others=366 days", + "data_sources_required": "Cherwell CMDB", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "1.4.2", + "metric_title": "% of Red Criticality applications with a defined and operational backup process", + "asset_types": "Applications", + "asset_types_in_scope": "Red Critical Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All except Public Cloud/3rd Party Managed, Public Cloud/Charter Managed", + "criticality_levels_in_scope": "Critical", + "exclusions": "Appliances, Public cloud managed excluded", + "special_conditions": "NetBackup or application method defined", + "data_sources_required": "Cherwell CMDB, NetBackup", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "1.4.2A", + "metric_title": "% of Tier 1 application environments with a defined and operational backup process", + "asset_types": "Applications", + "asset_types_in_scope": "Tier 1 Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All except Public Cloud/3rd Party Managed, Public Cloud/Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Appliances, Public cloud managed excluded", + "special_conditions": "NetBackup or specific application IDs", + "data_sources_required": "Cherwell CMDB, NetBackup", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "1.4.2All", + "metric_title": "% of application environments with a defined and operational backup process", + "asset_types": "Applications", + "asset_types_in_scope": "All Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All except Public Cloud/3rd Party Managed, Public Cloud/Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Appliances, Public cloud managed excluded", + "special_conditions": "NetBackup or specific application IDs", + "data_sources_required": "Cherwell CMDB, NetBackup", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "1.5.1A", + "metric_title": "% of Red Criticality servers with software components inventoried and cataloged in the system of record", + "asset_types": "Servers", + "asset_types_in_scope": "Red Critical Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "Critical", + "exclusions": "Appliances excluded", + "special_conditions": "Flexera deployed", + "data_sources_required": "Flexera, CMDB", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "1.5.1B", + "metric_title": "% of Red Criticality applications with associated software bill of materials (SBOM) defined maintained and cataloged", + "asset_types": "Applications", + "asset_types_in_scope": "Red Critical Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "Critical", + "exclusions": "", + "special_conditions": "SBOM field = Yes", + "data_sources_required": "Cherwell CMDB", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "1.5.2", + "metric_title": "% of Red Criticality applications subject to code security testing (e.g. SAST DAST)", + "asset_types": "Applications", + "asset_types_in_scope": "Red Critical Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "Critical", + "exclusions": "", + "special_conditions": "Contrast, Veracode, or SpecFlow deployed", + "data_sources_required": "Cherwell CMDB, Contrast", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "2.3.3", + "metric_title": "% of vulnerabilities (critical/high) on red critical servers that were closed or risk accepted within due date in the last 30 days", + "asset_types": "Servers", + "asset_types_in_scope": "Red Critical Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "Critical", + "exclusions": "", + "special_conditions": "Closed by due date or risk accepted by due date, due date in last 30 days", + "data_sources_required": "Kenna, Cherwell CMDB", + "business_justification": "Risk meter 67-100", + "notes": "" + }, + { + "metric_id": "2.3.4", + "metric_title": "% of vulnerabilities (critical/high) on servers that were closed/risk accepted within due date in the last 30 days", + "asset_types": "Servers", + "asset_types_in_scope": "All Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Closed by due date or risk accepted by due date, due date in last 30 days", + "data_sources_required": "Cherwell CMDB, Kenna", + "business_justification": "Risk meter 67-100", + "notes": "" + }, + { + "metric_id": "2.3.5", + "metric_title": "% of red critical servers without active critical/high-severity vulnerability that are overdue", + "asset_types": "Servers", + "asset_types_in_scope": "Red Critical Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "Critical", + "exclusions": "", + "special_conditions": "No open overdue or risk accepted vulnerabilities", + "data_sources_required": "Cherwell CMDB, Kenna", + "business_justification": "Risk meter 67-100", + "notes": "" + }, + { + "metric_id": "2.3.6", + "metric_title": "% of servers without active critical/high-severity vulnerability that are overdue", + "asset_types": "Servers", + "asset_types_in_scope": "All Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "No open overdue or risk accepted vulnerabilities", + "data_sources_required": "Cherwell CMDB, Kenna", + "business_justification": "Risk meter 67-100", + "notes": "" + }, + { + "metric_id": "2.3.7", + "metric_title": "% of red critical servers with no open past due vulnerabilities (critical/high)", + "asset_types": "Servers", + "asset_types_in_scope": "Red Critical Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "Critical", + "exclusions": "", + "special_conditions": "No open past due vulnerabilities", + "data_sources_required": "Cherwell CMDB, Kenna", + "business_justification": "Risk meter 67-100, past due only", + "notes": "" + }, + { + "metric_id": "2.3.8", + "metric_title": "% of servers with no open past due vulnerabilities (critical/high)", + "asset_types": "Servers", + "asset_types_in_scope": "All Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "No open past due vulnerabilities", + "data_sources_required": "Cherwell CMDB, Kenna", + "business_justification": "Risk meter 67-100, past due only", + "notes": "" + }, + { + "metric_id": "2.3.9", + "metric_title": "% of network devices with no open past due vulnerabilities (critical/high)", + "asset_types": "Network Devices", + "asset_types_in_scope": "All Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Qualys exclusion list", + "special_conditions": "No open past due vulnerabilities", + "data_sources_required": "Cherwell CMDB, Kenna", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.2.3", + "metric_title": "% of storage components protected by MFA", + "asset_types": "Storage Components", + "asset_types_in_scope": "All Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "FOS operating systems excluded", + "special_conditions": "MFA method configured", + "data_sources_required": "Cherwell CMDB, ESD", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.2.4", + "metric_title": "% of network components protected by MFA", + "asset_types": "Network Components", + "asset_types_in_scope": "Jump Host Application (APP2394)", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "MFA = 1", + "data_sources_required": "Cherwell CMDB, Centrify", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.2.5", + "metric_title": "% of servers protected by MFA", + "asset_types": "Servers", + "asset_types_in_scope": "All Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Incompatible OS excluded", + "special_conditions": "MFA = 1 or ESD MFA Method defined", + "data_sources_required": "Cherwell CMDB, Centrify", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.2.6", + "metric_title": "% of database servers protected by MFA", + "asset_types": "Database Servers", + "asset_types_in_scope": "All Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "All statuses", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "MFA method configured for database access", + "data_sources_required": "Database security tools", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.2.7", + "metric_title": "% of externally accessible enterprise applications protected by MFA", + "asset_types": "Applications", + "asset_types_in_scope": "Corporate Applications", + "application_types_in_scope": "", + "environment_in_scope": "PROD", + "status_in_scope": "Installed", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Blue Enterprise and Blue Red Network excluded", + "special_conditions": "Network: Corp", + "data_sources_required": "Cherwell CMDB, JIRA (ESSO)", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.2.8", + "metric_title": "% of customer facing applications protected by MFA", + "asset_types": "Applications", + "asset_types_in_scope": "Customer-Facing Applications", + "application_types_in_scope": "", + "environment_in_scope": "PROD", + "status_in_scope": "Installed", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Applications with exemption 5.2.8-Cust excluded", + "special_conditions": "End User Type: Customer", + "data_sources_required": "Cherwell CMDB, CyberArk, Cisco ISE, Centrify", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.3.4", + "metric_title": "% of database servers with data integrity controls and monitoring", + "asset_types": "Servers", + "asset_types_in_scope": "Database Servers", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Legacy Enterprise systems excluded", + "special_conditions": "Server Type: Database", + "data_sources_required": "Cherwell CMDB, Imperva Apex", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.4.2", + "metric_title": "% of workstations with endpoint security agents installed and functioning", + "asset_types": "Workstations", + "asset_types_in_scope": "All Workstations", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Last seen within 30 days", + "data_sources_required": "Cherwell CMDB, CrowdStrike", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.4.3", + "metric_title": "% of workstations with endpoint DLP agents installed and functioning", + "asset_types": "Workstations", + "asset_types_in_scope": "All Workstations", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Last seen within 60 days", + "data_sources_required": "Cherwell CMDB, JAMF, ADDM, MS Defender", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.4.4", + "metric_title": "% of workstations utilizing whole device encryption", + "asset_types": "Workstations", + "asset_types_in_scope": "All Workstations", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Mobile devices excluded", + "special_conditions": "Device encryption enabled", + "data_sources_required": "Cherwell CMDB, MaaS360, JamF", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.4.5", + "metric_title": "% of workstations with internet security agent installed and functioning", + "asset_types": "Workstations", + "asset_types_in_scope": "All Workstations", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "NetSkope client last seen within 30 days", + "data_sources_required": "Cherwell CMDB, JAMF, ADDM, NetSkope", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.4.6", + "metric_title": "% of workstations without overdue critical/high vulnerabilities", + "asset_types": "Workstations", + "asset_types_in_scope": "All Workstations", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Workstations not in Kenna excluded", + "special_conditions": "SCCM or JAMF managed workstations", + "data_sources_required": "Kenna, SCCM, JAMF", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.5.2", + "metric_title": "% of servers with confirmed supported operating systems", + "asset_types": "Servers", + "asset_types_in_scope": "All Servers", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "EOS data available", + "data_sources_required": "Cherwell CMDB, ESD", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.5.4", + "metric_title": "% of infrastructure without overdue critical/high vulnerabilities", + "asset_types": "Servers", + "asset_types_in_scope": "All Infrastructure", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Kenna vulnerability data available", + "data_sources_required": "Kenna", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.5.5", + "metric_title": "% of servers which have been decommissioned and are no longer connected to the network", + "asset_types": "Servers", + "asset_types_in_scope": "All Servers", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Retired", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "No recent activity in security tools", + "data_sources_required": "Cherwell CMDB, CrowdStrike, Kenna, Splunk", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.6.1", + "metric_title": "% of network monitored or scanned for connection of unknown devices", + "asset_types": "Network", + "asset_types_in_scope": "All Devices", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Customer-owned ranges excluded", + "special_conditions": "Charter-known IP ranges", + "data_sources_required": "Forescout, Cherwell CMDB", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.6.2", + "metric_title": "% of IP addresses active on network covered by vulnerability scans", + "asset_types": "Network", + "asset_types_in_scope": "All IP Addresses", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Exception ranges excluded", + "special_conditions": "Charter-known IP ranges", + "data_sources_required": "Cherwell CMDB, ESD, Qualys", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.6.2A", + "metric_title": "% of Active Workstations and Servers covered by vulnerability scans", + "asset_types": "Workstations and Servers", + "asset_types_in_scope": "All", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Qualys scan within 60 days", + "data_sources_required": "Cherwell CMDB, Qualys", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.6.3", + "metric_title": "% of devices identified that are in the centralized asset inventory", + "asset_types": "Network Devices", + "asset_types_in_scope": "All Devices", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Device discovery and inventory correlation", + "data_sources_required": "Forescout, Resolve, Charter Asset Discovery, Cherwell CMDB", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.6.3B", + "metric_title": "% of devices Managed Enforced over all devices permitted on network by NAC", + "asset_types": "Network Devices", + "asset_types_in_scope": "All Devices", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Blocked and Uncategorized devices excluded", + "special_conditions": "NAC policy enforcement", + "data_sources_required": "Forescout", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.6.4", + "metric_title": "% of unique undocumented devices detected and remediated within 30 days", + "asset_types": "Network Devices", + "asset_types_in_scope": "Undocumented Devices", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active", + "instance_types_in_scope": "Unknown", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Device remediation within 30 days", + "data_sources_required": "Forescout, Resolve, Charter Asset Discovery, Cherwell CMDB", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.7.1", + "metric_title": "% of AWS accounts sending logs to SIEM for monitoring", + "asset_types": "Cloud Accounts", + "asset_types_in_scope": "AWS Accounts", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "CloudTrail and GuardDuty enabled", + "data_sources_required": "AWS CloudTrail, AWS GuardDuty", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.7.2", + "metric_title": "% of external data connections encrypted in transit accessible to public cloud services", + "asset_types": "Cloud Connections", + "asset_types_in_scope": "Data Connections", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "External cloud connections", + "data_sources_required": "AWS S3 Bucket", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.7.3", + "metric_title": "% of data encrypted at rest stored in and accessible via public cloud", + "asset_types": "Cloud Data", + "asset_types_in_scope": "Data Objects", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Public cloud storage", + "data_sources_required": "AWS S3 Bucket", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "5.8.1", + "metric_title": "% of applications subject to code security testing within the past year", + "asset_types": "Applications", + "asset_types_in_scope": "Charter Developed Applications", + "application_types_in_scope": "", + "environment_in_scope": "PROD", + "status_in_scope": "Installed", + "instance_types_in_scope": "Charter In-house/Third Party Custom", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Development type filtering", + "data_sources_required": "Cherwell CMDB, Veracode, SpecFlow", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "7.1.1", + "metric_title": "% of servers generating actionable logs ingested into enterprise monitoring solution", + "asset_types": "Servers", + "asset_types_in_scope": "All Servers", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Appliances excluded", + "special_conditions": "Splunk log ingestion", + "data_sources_required": "Cherwell CMDB, Splunk", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "7.1.4", + "metric_title": "% of assets discovered during last quarter that are managed by Charter and documented", + "asset_types": "Assets", + "asset_types_in_scope": "All Assets", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Quarterly discovery tracking", + "data_sources_required": "Cherwell CMDB, Forescout, Resolve, Charter Asset Discovery", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "7.2.1", + "metric_title": "% of cases that met Time to Detect objective within the last month", + "asset_types": "Security Cases", + "asset_types_in_scope": "All Cases", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "All Severities", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Manual/Phishing cases excluded", + "special_conditions": "TTD within 10 minutes", + "data_sources_required": "Swimlane", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "7.2.2", + "metric_title": "% of cases that met Time to Acknowledge objective within the last month", + "asset_types": "Security Cases", + "asset_types_in_scope": "All Cases", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "All Severities", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "TTA within 15 minutes", + "data_sources_required": "Swimlane", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "7.2.3", + "metric_title": "% of cases that met Time to Close objective within the last month", + "asset_types": "Security Cases", + "asset_types_in_scope": "Closed Cases", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "All Severities", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "TTC within 120 hours", + "data_sources_required": "Swimlane", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "7.3.1", + "metric_title": "% of incidents that met Time to Detect objective within the last month", + "asset_types": "Security Incidents", + "asset_types_in_scope": "All Incidents", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "All Severities", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "TTD varies by severity", + "data_sources_required": "Swimlane", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "7.3.2", + "metric_title": "% of incidents that met Time to Acknowledge objective within the last month", + "asset_types": "Security Incidents", + "asset_types_in_scope": "All Incidents", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "All Severities", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "TTA varies by severity", + "data_sources_required": "Swimlane", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "7.3.3", + "metric_title": "% of incidents that met Time to Contain objective within the last month", + "asset_types": "Security Incidents", + "asset_types_in_scope": "All Incidents", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "All Severities", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "TTC varies by severity", + "data_sources_required": "Swimlane", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "7.3.4", + "metric_title": "% of incidents that met Time to Close objective within the last month", + "asset_types": "Security Incidents", + "asset_types_in_scope": "Closed Incidents", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "All Severities", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Resolution within target time", + "data_sources_required": "Swimlane", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "7.4.6", + "metric_title": "% of incidents closed within defined target resolution time/SLA within last quarter", + "asset_types": "Security Incidents", + "asset_types_in_scope": "All Incidents", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Closed", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Quarterly SLA measurement", + "data_sources_required": "Swimlane", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "7.6.13", + "metric_title": "% of applications compliant with disaster recovery exercises requirements", + "asset_types": "Applications", + "asset_types_in_scope": "All Applications", + "application_types_in_scope": "", + "environment_in_scope": "PROD", + "status_in_scope": "Installed", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "Critical, High, Medium", + "exclusions": "Admin applications excluded", + "special_conditions": "DR exercise completion tracking", + "data_sources_required": "Cherwell CMDB", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "7.6.15", + "metric_title": "% of critical outages not resulting from cyber causes during the past month", + "asset_types": "Outages", + "asset_types_in_scope": "Critical Outages", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "All", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "Critical", + "exclusions": "", + "special_conditions": "Monthly outage tracking", + "data_sources_required": "Swimlane, Remedy Report", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "7.6.16", + "metric_title": "% of applications compliant with disaster recovery plan review requirements", + "asset_types": "Applications", + "asset_types_in_scope": "All Applications", + "application_types_in_scope": "", + "environment_in_scope": "PROD", + "status_in_scope": "Installed", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "Admin applications excluded", + "special_conditions": "DR plan review tracking", + "data_sources_required": "Cherwell CMDB", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "8.0.1", + "metric_title": "% of Resources/Accounts compliant with Cloud Configuration Standards", + "asset_types": "Cloud Resources", + "asset_types_in_scope": "All Cloud Resources", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "All", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Cloud configuration compliance", + "data_sources_required": "CrowdStrike CSPM", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "8.0.2", + "metric_title": "% of Accounts using AMIs and ECRs with supported OS", + "asset_types": "Cloud Accounts", + "asset_types_in_scope": "AMI/ECR Images", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "All", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Supported OS validation", + "data_sources_required": "Cloud Image Management", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "8.0.3", + "metric_title": "% of cloud accounts configured for MFA requirements", + "asset_types": "Cloud Accounts", + "asset_types_in_scope": "All Cloud Accounts", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "All", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "MFA configuration validation", + "data_sources_required": "Cloud Account Management", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "8.0.4", + "metric_title": "% of cloud accounts configured for WAF requirements", + "asset_types": "Cloud Accounts", + "asset_types_in_scope": "All Cloud Accounts", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "All", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "WAF configuration validation", + "data_sources_required": "Cloud Security Management", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "8.0.5", + "metric_title": "% of cloud accounts logging", + "asset_types": "Cloud Accounts", + "asset_types_in_scope": "All Cloud Accounts", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "All", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Logging configuration validation", + "data_sources_required": "Cloud Logging Management", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "8.0.6", + "metric_title": "% of cloud accounts configured for vulnerability scanning on compute resources", + "asset_types": "Cloud Accounts", + "asset_types_in_scope": "Compute Resources", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "All", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Inspector service enabled", + "data_sources_required": "AWS Inspector", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "8.0.7", + "metric_title": "% of cloud compute resources covered by vulnerability scans", + "asset_types": "Cloud Resources", + "asset_types_in_scope": "Compute Resources", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "AWS Account resources excluded", + "special_conditions": "Active scan status", + "data_sources_required": "AWS Inspector", + "business_justification": "", + "notes": "" + }, + { + "metric_id": "2.3.4i", + "metric_title": "% of vulnerabilities (critical/high) on servers that were closed/risk accepted within due date in the last 30 days (infrastructure)", + "asset_types": "Servers", + "asset_types_in_scope": "All Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Closed by due date or risk accepted by due date, due date in last 30 days", + "data_sources_required": "Cherwell CMDB, Kenna", + "business_justification": "Vulnerability Management", + "notes": "Infrastructure variant of 2.3.4" + }, + { + "metric_id": "2.3.6i", + "metric_title": "% of servers without active critical/high-severity vulnerability that are overdue (infrastructure)", + "asset_types": "Servers", + "asset_types_in_scope": "All Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "No open overdue or risk accepted vulnerabilities", + "data_sources_required": "Cherwell CMDB, Kenna", + "business_justification": "Vulnerability Management", + "notes": "Infrastructure variant of 2.3.6" + }, + { + "metric_id": "2.3.8i", + "metric_title": "% of servers with no open past due vulnerabilities (critical/high) (infrastructure)", + "asset_types": "Servers", + "asset_types_in_scope": "All Applications", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "No open past due vulnerabilities", + "data_sources_required": "Cherwell CMDB, Kenna", + "business_justification": "Vulnerability Management", + "notes": "Infrastructure variant of 2.3.8" + }, + { + "metric_id": "5.5.4i", + "metric_title": "% of infrastructure without overdue critical/high vulnerabilities (infrastructure)", + "asset_types": "Servers", + "asset_types_in_scope": "All Infrastructure", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "Charter Managed", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Kenna vulnerability data available", + "data_sources_required": "Kenna", + "business_justification": "Vulnerability Management", + "notes": "Infrastructure variant of 5.5.4" + }, + { + "metric_id": "Missing_AppID", + "metric_title": "Assets missing Application ID assignment", + "asset_types": "Assets", + "asset_types_in_scope": "All Assets", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Application ID field is empty or null", + "data_sources_required": "Cherwell CMDB", + "business_justification": "Asset Data Quality", + "notes": "Data quality metric for CMDB hygiene" + }, + { + "metric_id": "Missing_DF", + "metric_title": "Assets missing Data Function assignment", + "asset_types": "Assets", + "asset_types_in_scope": "All Assets", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Data Function field is empty or null", + "data_sources_required": "Cherwell CMDB", + "business_justification": "Asset Data Quality", + "notes": "Data quality metric for CMDB hygiene" + }, + { + "metric_id": "Missing_OS", + "metric_title": "Assets missing Operating System assignment", + "asset_types": "Assets", + "asset_types_in_scope": "All Assets", + "application_types_in_scope": "", + "environment_in_scope": "All environments", + "status_in_scope": "Active, Installed", + "instance_types_in_scope": "All instance types", + "criticality_levels_in_scope": "All criticality levels", + "exclusions": "", + "special_conditions": "Operating System field is empty or null", + "data_sources_required": "Cherwell CMDB", + "business_justification": "Asset Data Quality", + "notes": "Data quality metric for CMDB hygiene" + } +]