diff --git a/marketplace/plugins/lc-essentials/agents/billing-collector.md b/marketplace/plugins/lc-essentials/agents/billing-collector.md new file mode 100644 index 00000000..7e126446 --- /dev/null +++ b/marketplace/plugins/lc-essentials/agents/billing-collector.md @@ -0,0 +1,153 @@ +--- +name: billing-collector +description: Collect billing data for a SINGLE LimaCharlie organization. Designed to be spawned in parallel (one instance per org) by the reporting skill. Returns structured JSON matching the billing-report template schema. +model: haiku +allowed-tools: + - mcp__plugin_lc-essentials_limacharlie__lc_call_tool + - Bash +--- + +# Billing Collector Agent + +You collect billing/invoice data for a SINGLE LimaCharlie organization and return structured JSON. + +## Input Format + +You will receive a prompt containing: +- **Organization name**: Human-readable name +- **OID**: Organization UUID +- **Year**: Invoice year (integer) +- **Month**: Invoice month (1-12) + +Example prompt: +``` +Collect billing data for org 'Acme Corp' (OID: c7e8f940-1234-5678-abcd-1234567890ab) +Year: 2025, Month: 11 +Return JSON matching billing-report template schema +``` + +## Workflow + +### Step 1: Extract Parameters + +Parse the prompt to extract: +- `org_name`: Organization name +- `oid`: Organization UUID +- `year`: Invoice year +- `month`: Invoice month + +### Step 2: Fetch Invoice Data + +Call the MCP tool directly: + +``` +tool: get_org_invoice_url +parameters: + oid: [extracted-oid] + year: [extracted-year] + month: [extracted-month] + format: simple_json +``` + +### Step 3: Handle Response + +**On Success:** +The API returns: +```json +{ + "lines": [ + { "description": "...", "amount": 400000, "quantity": 1 }, + { "description": "...", "amount": 150000, "quantity": 2500 } + ], + "total": 550000 +} +``` + +Transform to output schema: +1. Convert `total` from cents to dollars: `total / 100` +2. Convert each line item `amount` from cents to dollars +3. Map `lines` to `skus` array + +**On Error:** +- **403 Forbidden**: Return status="error", error_message="Insufficient permissions - requires admin/owner role" +- **404 Not Found**: Return status="no_invoice", error_message="No invoice exists for this period" +- **Other errors**: Return status="error" with error details + +### Step 4: Optionally Fetch Invoice URL + +If data retrieval succeeded, also get the PDF URL: + +``` +tool: get_org_invoice_url +parameters: + oid: [extracted-oid] + year: [extracted-year] + month: [extracted-month] +``` + +(No format parameter = returns PDF URL) + +## Output Schema + +Return ONLY valid JSON matching this structure: + +```json +{ + "name": "Acme Corp", + "oid": "c7e8f940-1234-5678-abcd-1234567890ab", + "status": "success", + "cost": 5500.00, + "currency": "usd", + "skus": [ + { + "description": "Enterprise Plan - Base", + "amount": 4000.00, + "quantity": 1 + }, + { + "description": "Additional Sensors", + "amount": 1500.00, + "quantity": 2500 + } + ], + "invoice_url": "https://pay.stripe.com/invoice/..." +} +``` + +**On Error:** +```json +{ + "name": "Acme Corp", + "oid": "c7e8f940-1234-5678-abcd-1234567890ab", + "status": "error", + "error_message": "403 Forbidden - Insufficient permissions" +} +``` + +**No Invoice:** +```json +{ + "name": "Acme Corp", + "oid": "c7e8f940-1234-5678-abcd-1234567890ab", + "status": "no_invoice", + "error_message": "No invoice exists for November 2025" +} +``` + +## Critical Rules + +### NEVER Fabricate Data +- Only return data from the API response +- If a field is missing, omit it or return null +- NEVER estimate, guess, or calculate values not in the response + +### Currency Conversion +- API returns amounts in **cents** +- ALWAYS divide by 100 for dollars +- Example: `400000` cents = `4000.00` dollars + +### Return Format +- Return ONLY the JSON object +- No markdown formatting +- No explanatory text before or after +- Must be valid, parseable JSON diff --git a/marketplace/plugins/lc-essentials/commands/reporting-templates.md b/marketplace/plugins/lc-essentials/commands/reporting-templates.md deleted file mode 100644 index b28c2f4a..00000000 --- a/marketplace/plugins/lc-essentials/commands/reporting-templates.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -description: Pre-defined report templates combining data collection and visualization. Usage: /lc-essentials:reporting-templates ---- - -# Reporting Templates - -Present the user with a menu of pre-defined report templates. Each template combines the `reporting` skill (data collection) with the `graphic-output` skill (HTML visualization). - -## Instructions - -Use the `AskUserQuestion` tool to present the template menu: - -``` -AskUserQuestion( - questions=[{ - "question": "Which report template would you like to generate?", - "header": "Template", - "options": [ - {"label": "MSSP Executive Summary", "description": "High-level metrics across all customers: sensor counts, detection volumes, SLA status"}, - {"label": "Customer Health Dashboard", "description": "Per-org health scores, offline sensors, detection trends with drill-down"}, - {"label": "Monthly Billing Report", "description": "Usage statistics and billing data for customer invoicing"}, - {"label": "Detection Analytics", "description": "Security activity breakdown: top categories, trends, alert volumes by org"} - ], - "multiSelect": false - }] -) -``` - -## Template Definitions - -### Template 1: MSSP Executive Summary - -**Purpose**: High-level overview for MSSP leadership - quick health check across all customers. - -**Data Collection** (reporting skill): -- List all organizations -- Per-org: sensor count (online/offline), detection count (7 days), SLA status -- Aggregate totals across fleet - -**Visualization** (graphic-output skill): -- Summary cards: Total sensors, Total detections, Fleet coverage %, Orgs passing SLA -- Pie chart: Platform distribution (Windows/Linux/macOS) -- Bar chart: Top 10 orgs by detection volume -- Table: All orgs with health indicators (green/yellow/red) - -**Time Range**: Default last 7 days, prompt user to confirm. - -**Output**: `/tmp/mssp-executive-summary-{date}.html` - ---- - -### Template 2: Customer Health Dashboard - -**Purpose**: Operational dashboard for SOC teams - identify customers needing attention. - -**Data Collection** (reporting skill): -- List all organizations -- Per-org: sensor inventory with online/offline status, detection counts by category -- Identify: offline sensors >24h, orgs below SLA, detection spikes - -**Visualization** (graphic-output skill): -- Health gauge: Fleet-wide coverage percentage -- Heatmap: Org health matrix (rows=orgs, columns=metrics, color=status) -- Bar chart: Offline sensors by org -- Line chart: Detection trend (daily, last 7 days) if data available -- Alert list: Orgs requiring immediate attention - -**Time Range**: Default last 7 days, prompt user to confirm. - -**Output**: `/tmp/customer-health-dashboard-{date}.html` - ---- - -### Template 3: Monthly Billing Report - -**Purpose**: Usage data for customer invoicing and capacity planning. - -**Data Collection** (reporting skill): -- List all organizations -- Per-org: billing details, usage stats (events, outputs, storage) -- Aggregate: total usage, month-over-month comparison if available - -**Visualization** (graphic-output skill): -- Summary cards: Total events, Total output bytes, Active sensors -- Bar chart: Usage by organization (top consumers) -- Table: Full org breakdown with usage columns -- Pie chart: Usage distribution by org size tier - -**Time Range**: Prompt user for billing period (default: previous calendar month). - -**Output**: `/tmp/billing-report-{month}-{year}.html` - ---- - -### Template 4: Detection Analytics - -**Purpose**: Security activity analysis for threat intelligence and tuning. - -**Data Collection** (reporting skill): -- List all organizations -- Per-org: detections by category, top detection rules triggered -- Aggregate: fleet-wide detection categories, rule effectiveness - -**Visualization** (graphic-output skill): -- Summary cards: Total detections, Unique categories, Orgs with alerts -- Pie chart: Detection categories (top 10) -- Bar chart: Detections by organization -- Table: Top triggered rules with counts -- Warning banner: Detection limits reached (if applicable) - -**Time Range**: Default last 30 days, prompt user to confirm. - -**Output**: `/tmp/detection-analytics-{date}.html` - ---- - -## Execution Flow - -Once the user selects a template: - -1. **Confirm Time Range**: Use `AskUserQuestion` to confirm or customize the time period -2. **Confirm Scope**: Ask if they want all orgs or a specific subset -3. **Collect Data**: Spawn `org-reporter` agents in parallel to collect data from each organization -4. **Generate HTML**: Spawn `html-renderer` agent to create the visualization dashboard -5. **Open in Browser**: Automatically open the generated HTML file using `xdg-open` or serve via local HTTP server - -**Browser Launch Command:** -```bash -# Option 1: Direct file open -xdg-open /tmp/{report-file}.html - -# Option 2: HTTP server (if direct open fails) -cd /tmp && python3 -m http.server 8765 & -xdg-open http://localhost:8765/{report-file}.html -``` - -## Example Conversation Flow - -``` -User: /lc-essentials:reporting-templates - -Assistant: [Presents template menu] - -User: MSSP Executive Summary - -Assistant: [Confirms time range, collects data via reporting skill, generates HTML via graphic-output skill] - -Assistant: Your MSSP Executive Summary is ready! Opening in browser... - -[Browser opens with the report at http://localhost:8765/mssp-executive-summary-2025-12-06.html] -``` diff --git a/marketplace/plugins/lc-essentials/commands/reporting.md b/marketplace/plugins/lc-essentials/commands/reporting.md new file mode 100644 index 00000000..50e702cb --- /dev/null +++ b/marketplace/plugins/lc-essentials/commands/reporting.md @@ -0,0 +1,139 @@ +--- +description: Generate reports from LimaCharlie data with visualization. Usage: /lc-essentials:reporting +--- + +# Report Generation Wizard + +You MUST complete all 3 steps in order before generating any report: +1. **Report Type** - Ask which report to generate +2. **Time Period** - Ask which period (NEVER assume a default) +3. **Scope** - Ask which organizations to include + +Do NOT skip any step. Do NOT assume defaults for time period. + +--- + +## Step 1: Report Type (MANDATORY) + +IMMEDIATELY call AskUserQuestion - do NOT output any text first: + +questions=[{"question": "Which report would you like to generate?", "header": "Report", "options": [{"label": "Billing Summary", "description": "Monthly billing with roll-up totals and per-tenant SKU breakdown (Recommended)"}, {"label": "Fleet Health", "description": "Sensor status, platform distribution, and detection activity"}, {"label": "Detection Analytics", "description": "Detection volume, severity breakdown, and category analysis"}, {"label": "Customer Health", "description": "Health scores with sensor coverage and attention items"}], "multiSelect": false}] + +## Step 2: Time Period (MANDATORY - DO NOT SKIP) + +After Step 1 response, you MUST call AskUserQuestion for time period. Never assume a default period: + +questions=[{"question": "What time period should the report cover?", "header": "Period", "options": [{"label": "Previous month", "description": "Most recent completed month (Recommended for billing)"}, {"label": "Current month", "description": "Current month to date (may be incomplete)"}, {"label": "Custom date range", "description": "I'll specify the year and month"}], "multiSelect": false}] + +IMPORTANT: You must wait for the user to select a time period before proceeding. Do NOT assume "current billing period" or any default. + +If "Custom date range" is selected, call AskUserQuestion with year and month questions: + +questions=[{"question": "Which year?", "header": "Year", "options": [{"label": "2025", "description": "Current year"}, {"label": "2024", "description": "Previous year"}], "multiSelect": false}, {"question": "Which month?", "header": "Month", "options": [{"label": "January", "description": "Month 1"}, {"label": "February", "description": "Month 2"}, {"label": "March", "description": "Month 3"}, {"label": "April", "description": "Month 4"}, {"label": "May", "description": "Month 5"}, {"label": "June", "description": "Month 6"}, {"label": "July", "description": "Month 7"}, {"label": "August", "description": "Month 8"}, {"label": "September", "description": "Month 9"}, {"label": "October", "description": "Month 10"}, {"label": "November", "description": "Month 11"}, {"label": "December", "description": "Month 12"}], "multiSelect": false}] + +## Step 3: Scope (MANDATORY) + +After Step 2 response (and custom date if applicable), call AskUserQuestion for scope: + +questions=[{"question": "Which organizations should be included?", "header": "Scope", "options": [{"label": "All organizations", "description": "Generate report for all accessible orgs (Recommended)"}, {"label": "Specific organizations", "description": "I'll select which orgs to include"}], "multiSelect": false}] + +If "Specific organizations" is selected: +1. Load the lc-essentials:limacharlie-call skill +2. Call list_user_orgs to get available organizations +3. Present a multi-select AskUserQuestion with the org names and OIDs + +After all selections are complete, display this summary: + +## Report Configuration + +| Setting | Value | +|---------|-------| +| **Report** | {selected_report} | +| **Period** | {selected_period} | +| **Scope** | {scope_description} | + +Then execute the report based on the selected type: + +## Billing Summary Report Execution + +1. Calculate billing period using bash: + - Previous month: YEAR=$(date -d "last month" +%Y) MONTH=$(date -d "last month" +%-m) + - Current month: YEAR=$(date +%Y) MONTH=$(date +%-m) + +2. Load the lc-essentials:limacharlie-call skill + +3. Call list_user_orgs to get all organizations + +4. For each organization, call get_org_invoice_url with parameters: + - oid: the organization's OID + - year: the billing year + - month: the billing month + - format: "simple_json" + +5. Aggregate results into billing-summary.json schema: + - report_metadata: title, generated_at, period (month_name, year, display) + - summary: total_billing, total_sensors, avg_cost_per_sensor, active_tenants + - tenants: array with name, oid, region, sensors, monthly_cost, cost_per_sensor, percentage_of_total, status, line_items + +6. Load the lc-essentials:graphic-output skill and render using template "billing-summary" + +7. Open the generated HTML file with: xdg-open /tmp/billing-report-{month}-{year}.html + +## Fleet Health Report Execution + +1. Calculate timestamps using bash: + - END_TS=$(date +%s) + - START_TS based on selected period + +2. Load the lc-essentials:limacharlie-call skill + +3. Call list_user_orgs + +4. For each organization: + - Call list_sensors to get sensor inventory + - Call get_historic_detections with start/end timestamps + - Calculate online/offline from last_seen timestamps + +5. Aggregate into fleet-health.json schema + +6. Load lc-essentials:graphic-output skill and render using template "fleet-health" + +7. Open: xdg-open /tmp/fleet-health-{days}d.html + +## Detection Analytics Report Execution + +1. Calculate timestamps using bash + +2. Load lc-essentials:limacharlie-call skill + +3. Call list_user_orgs + +4. For each organization: + - Call get_historic_detections + - Extract categories and severity from detection metadata + +5. Aggregate into detection-analytics.json schema + +6. Load lc-essentials:graphic-output skill and render using template "detection-analytics" + +7. Open: xdg-open /tmp/detection-analytics-{days}d.html + +## Customer Health Report Execution + +1. Calculate timestamps using bash + +2. Load lc-essentials:limacharlie-call skill + +3. Call list_user_orgs + +4. For each organization: + - Call list_sensors + - Call get_historic_detections + - Calculate health score (online/total * 100) + - Classify: healthy >= 90%, warning 70-89%, critical < 70% + +5. Aggregate into customer-health.json schema + +6. Load lc-essentials:graphic-output skill and render using template "customer-health" + +7. Open: xdg-open /tmp/customer-health-{days}d.html diff --git a/marketplace/plugins/lc-essentials/scripts/render-html.py b/marketplace/plugins/lc-essentials/scripts/render-html.py index c20efa17..3c220096 100644 --- a/marketplace/plugins/lc-essentials/scripts/render-html.py +++ b/marketplace/plugins/lc-essentials/scripts/render-html.py @@ -73,7 +73,22 @@ def validate_no_fabrication(data: dict, template: str) -> list: ], 'billing-summary': [ 'metadata.generated_at', - 'data.usage', + 'data.tenants', + 'data.rollup', + ], + 'fleet-health': [ + 'metadata.generated_at', + 'data.rollup', + 'data.organizations', + ], + 'detection-analytics': [ + 'metadata.generated_at', + 'data.tenants', + ], + 'customer-health': [ + 'metadata.generated_at', + 'data.rollup', + 'data.customers', ], } @@ -385,6 +400,9 @@ def determine_title(template: str, metadata: dict) -> str: 'sensor-health': 'Sensor Health Report', 'detection-summary': 'Detection Summary', 'billing-summary': 'Billing Summary', + 'fleet-health': 'Fleet Health Dashboard', + 'detection-analytics': 'Detection Analytics', + 'customer-health': 'Customer Health Report', } base_title = titles.get(template, 'LimaCharlie Report') @@ -435,7 +453,7 @@ def main(): parser.add_argument( '--template', '-t', required=True, - choices=['mssp-dashboard', 'org-detail', 'sensor-health', 'detection-summary', 'billing-summary'], + choices=['mssp-dashboard', 'org-detail', 'sensor-health', 'detection-summary', 'billing-summary', 'fleet-health', 'detection-analytics', 'customer-health'], help='Template to render' ) diff --git a/marketplace/plugins/lc-essentials/skills/graphic-output/SKILL.md b/marketplace/plugins/lc-essentials/skills/graphic-output/SKILL.md index 20bf65a3..b4574354 100644 --- a/marketplace/plugins/lc-essentials/skills/graphic-output/SKILL.md +++ b/marketplace/plugins/lc-essentials/skills/graphic-output/SKILL.md @@ -192,9 +192,12 @@ This skill: | Template | Description | Best For | |----------|-------------|----------| +| `billing-summary` | Multi-tenant billing roll-up with per-tenant breakdown, SKU details, cost distribution charts | Billing/cost analysis | +| `fleet-health` | Sensor health, platform distribution, detection activity across organizations | MSSP fleet monitoring | +| `detection-analytics` | Detection volume, categories, severity breakdown, and trends across tenants | Detection analysis | +| `customer-health` | Comprehensive customer health combining sensor coverage, detection activity, and health metrics | Customer success tracking | | `mssp-dashboard` | Multi-tenant overview with aggregate metrics, org table, charts | MSSP/multi-org reports | | `security-overview` | Single-org comprehensive security report with platform breakdown, MITRE coverage, detection timeline, rules inventory | Individual org deep-dive | -| `billing-summary` | Multi-tenant billing roll-up with per-tenant breakdown, SKU details, cost distribution charts | Billing/cost analysis | | `custom-report` | **Component-based flexible reports** - build any report by specifying which components to include | Ad-hoc/custom reports | **Note:** Use `custom-report` when none of the predefined templates fit your needs. It supports composable components that can be arranged in any order. @@ -281,11 +284,13 @@ If integrating with the reporting skill, the data structure should include: Choose the appropriate template based on data type: -- **Multi-org data** → `mssp-dashboard` -- **Single org data** → `org-detail` -- **Health-focused** → `sensor-health` -- **Detection-focused** → `detection-summary` - **Billing/usage** → `billing-summary` +- **Fleet health (sensors/detections)** → `fleet-health` +- **Detection analysis** → `detection-analytics` +- **Customer success** → `customer-health` +- **Multi-org data** → `mssp-dashboard` +- **Single org data** → `security-overview` +- **Custom/flexible** → `custom-report` ### Step 3: Spawn HTML Renderer @@ -319,31 +324,9 @@ Task( ) ``` -### Step 4: Launch in Browser (Default Completion) +### Step 4: Return Results to User -**IMPORTANT:** When generating any HTML report, always open it in the user's browser as the default completion action. - -```bash -# On ChromeOS/Linux with garcon: -garcon-url-handler "http://localhost:8080/report.html" - -# Alternative for other systems: -xdg-open "/tmp/report.html" -# or -open "/tmp/report.html" # macOS -``` - -Before opening, ensure an HTTP server is running to serve the file: -```bash -# Start server if not already running -cd /tmp && python3 -m http.server 8080 --bind 0.0.0.0 & -``` - -This ensures the user immediately sees their report without manual steps. - -### Step 5: Return Results to User - -After the renderer completes and browser is opened, inform the user: +After the renderer completes, inform the user: ``` Interactive HTML dashboard generated successfully! @@ -480,6 +463,184 @@ errors # Array of error objects - All `warnings` displayed prominently - No data fabrication under any circumstances +### Fleet Health (`fleet-health`) + +**Purpose:** Sensor health, platform distribution, and detection activity across multiple organizations for MSSP fleet monitoring. + +**Visualizations:** +- Summary cards (total sensors, online sensors, fleet health %, total detections) +- Platform distribution donut chart +- Organization health horizontal bar chart +- Top detection categories bar chart +- Top organizations by detection volume +- Per-organization sortable table with health status +- Detection limit warnings prominently displayed +- Warnings and errors section + +**Required Data Fields:** +``` +metadata.generated_at # When data was collected +metadata.time_window.start_display # Time range start +metadata.time_window.end_display # Time range end +metadata.time_window.days # Number of days in window + +data.rollup.total_sensors # Total sensor count +data.rollup.online_sensors # Online sensor count +``` + +**Optional Data Fields (shown if present, "N/A" if missing):** +``` +metadata.organizations.total # Total org count +metadata.organizations.successful # Successful org count +metadata.organizations.success_rate # Success percentage + +data.rollup.offline_sensors # Offline sensor count +data.rollup.health_percent # Fleet health percentage +data.rollup.total_detections # Total detections +data.rollup.detection_limit_reached # Boolean limit flag +data.rollup.orgs_at_limit # Count of orgs at limit + +data.platforms[] # Platform breakdown array +data.organizations[] # Per-org details array +data.top_categories[] # Top detection categories +data.top_orgs_by_detections[] # Top orgs by detection volume + +warnings # Array of warning strings +errors # Array of error objects +``` + +**Guardrail Behavior:** +- If `health_percent` missing but online/total provided: Calculates as online/total * 100 +- If `platforms` empty: Shows "Platform data not available" +- Detection limit warning banner shown if `detection_limit_reached` is true +- All orgs at limit marked with "LIMIT" badge in table + +### Detection Analytics (`detection-analytics`) + +**Purpose:** Detection volume, categories, severity breakdown, and trends across multiple tenants for security analysis. + +**Visualizations:** +- Summary cards (total detections, tenants with detections, tenants at limit, critical severity count) +- Aggregate severity distribution bar (critical/high/medium/low/info) +- Top detection categories bar chart +- Top tenants by detection volume +- Per-tenant table with severity breakdown and categories +- Detection limit warnings prominently displayed + +**Required Data Fields:** +``` +metadata.generated_at # When data was collected +metadata.time_window.start_display # Time range start +metadata.time_window.end_display # Time range end +metadata.time_window.days # Number of days in window + +data.tenants[] # Per-tenant detection data +data.tenants[].name # Tenant name +data.tenants[].oid # Tenant OID +data.tenants[].status # success/error/no_detections +``` + +**Optional Data Fields (shown if present, "N/A" if missing):** +``` +metadata.scope # single/all +metadata.tenant_count # Total tenant count + +data.tenants[].detection_count # Detection count +data.tenants[].limit_reached # Boolean limit flag +data.tenants[].categories[] # Category breakdown +data.tenants[].severities # Severity breakdown object +data.tenants[].top_hosts[] # Top hosts by detections + +data.rollup.total_detections # Aggregate detection count +data.rollup.total_tenants # Total tenants +data.rollup.tenants_with_detections # Tenants with detections +data.rollup.tenants_at_limit # Tenants at limit +data.rollup.severities # Aggregate severity breakdown + +data.top_categories[] # Top categories across all +data.top_tenants[] # Top tenants by volume + +warnings # Array of warning strings +errors # Array of error objects +``` + +**Guardrail Behavior:** +- Detection limit warning banner shown if any tenant at limit +- Tenants at limit marked with "LIMIT" badge +- Severity bars only shown if severity data available +- Critical severity count highlighted in red + +### Customer Health (`customer-health`) + +**Purpose:** Comprehensive customer health combining sensor coverage, detection activity, and health metrics for customer success tracking. + +**Visualizations:** +- Attention required banner (critical/warning items summary) +- Summary cards (customers, sensors, fleet health, detections) +- Customer health distribution grid (healthy/warning/critical/inactive counts) +- Health distribution donut chart +- Platform distribution donut chart +- Top detection categories bar chart +- Per-customer table with health score, sensors, detections, attention items +- Customers needing attention section with expanded cards +- Warnings and errors section + +**Required Data Fields:** +``` +metadata.generated_at # When data was collected +metadata.time_window.start_display # Time range start +metadata.time_window.end_display # Time range end +metadata.time_window.days # Number of days in window + +data.rollup.total_customers # Total customer count +data.rollup.total_sensors # Total sensor count + +data.customers[] # Per-customer details +data.customers[].name # Customer name +data.customers[].oid # Customer OID +data.customers[].health_status # healthy/warning/critical/inactive/error +``` + +**Optional Data Fields (shown if present, "N/A" if missing):** +``` +metadata.organizations.total # Total org count +metadata.organizations.successful # Successful org count +metadata.organizations.success_rate # Success percentage + +data.rollup.online_sensors # Online sensors +data.rollup.offline_sensors # Offline sensors +data.rollup.fleet_health_percent # Fleet health % +data.rollup.total_detections # Total detections +data.rollup.customers_healthy # Healthy customer count +data.rollup.customers_warning # Warning customer count +data.rollup.customers_critical # Critical customer count +data.rollup.customers_inactive # Inactive customer count + +data.health_distribution # Health distribution percentages +data.platforms[] # Platform breakdown +data.customers[].health_score # Numeric health score +data.customers[].sensors # Sensor metrics object +data.customers[].detections # Detection metrics object +data.customers[].attention_items[] # Attention items array + +data.attention_summary # Overall attention summary +data.attention_summary.critical_items # Critical item count +data.attention_summary.warning_items # Warning item count +data.attention_summary.customers_needing_attention[] + +data.top_detection_categories[] # Top categories across all + +warnings # Array of warning strings +errors # Array of error objects +``` + +**Guardrail Behavior:** +- Attention banner shown if any critical or warning items exist +- Customers needing attention highlighted with colored border +- Health scores color-coded (green ≥90%, amber 70-89%, red <70%) +- Rows with attention items have amber background +- All attention badges shown per customer + ## Chart Behavior with Missing Data ### Pie Chart @@ -628,16 +789,22 @@ skills/graphic-output/ ├── templates/ │ ├── base.html.j2 # Base template with CSS, Chart.js utilities │ └── reports/ +│ ├── billing-summary.html.j2 # Multi-tenant billing report +│ ├── fleet-health.html.j2 # Fleet health dashboard +│ ├── detection-analytics.html.j2 # Detection analytics dashboard +│ ├── customer-health.html.j2 # Customer health dashboard │ ├── mssp-dashboard.html.j2 # Multi-tenant MSSP dashboard │ ├── security-overview.html.j2 # Single-org security report -│ ├── billing-summary.html.j2 # Multi-tenant billing report │ └── custom-report.html.j2 # Component-based flexible reports ├── static/ │ └── js/ │ └── lc-charts.js # Chart.js utility functions └── schemas/ + ├── billing-summary.json # Schema for billing-summary data + ├── fleet-health.json # Schema for fleet-health data + ├── detection-analytics.json # Schema for detection-analytics data + ├── customer-health.json # Schema for customer-health data ├── mssp-report.json # Schema for mssp-dashboard data ├── security-overview.json # Schema for security-overview data - ├── billing-summary.json # Schema for billing-summary data └── custom-report.json # Schema for custom-report components ``` diff --git a/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/billing-summary.json b/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/billing-summary.json index 929f7a5b..f1d9f485 100644 --- a/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/billing-summary.json +++ b/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/billing-summary.json @@ -1,92 +1,139 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Multi-Tenant Billing Summary Data", - "description": "Schema for data input to the billing-summary template. All values must come from actual LimaCharlie billing API responses - no fabrication allowed.", + "title": "Billing Summary Visualization Data", + "description": "Schema for data input to the billing-summary template. Accepts output from the reporting skill's billing-report template. All values must come from actual LimaCharlie billing API responses - no fabrication allowed.", "type": "object", "required": ["metadata", "data"], "properties": { "metadata": { "type": "object", + "description": "Report metadata - when and how data was collected", "required": ["generated_at", "period"], "properties": { "generated_at": { "type": "string", - "description": "ISO 8601 timestamp when data was collected" + "format": "date-time", + "description": "ISO 8601 timestamp when data was collected", + "examples": ["2025-12-10T14:32:45Z"] }, "period": { "type": "string", - "description": "Billing period (e.g., 'November 2025')" + "description": "Billing period in human-readable format", + "examples": ["November 2025", "December 2025"] + }, + "scope": { + "type": "string", + "enum": ["single", "all"], + "description": "Report scope" }, "tenant_count": { "type": "integer", - "description": "Number of tenants with billing data" + "description": "Number of tenants with billing data retrieved", + "examples": [24, 50, 100] } } }, "data": { "type": "object", - "required": ["rollup", "tenants"], + "description": "Report data containing per-tenant details and optional rollup", + "required": ["tenants"], "properties": { - "rollup": { - "type": "object", - "description": "Aggregate totals across all tenants", - "required": ["total_cost", "total_sensors"], - "properties": { - "total_cost": { - "type": "number", - "description": "Total monthly cost across all tenants" - }, - "total_sensors": { - "type": "integer", - "description": "Total sensors across all tenants" - }, - "avg_cost_per_sensor": { - "type": "number", - "description": "Average cost per sensor (blended rate)" - } - } - }, "tenants": { "type": "array", - "description": "Per-tenant billing details", + "description": "Per-tenant billing details - each item represents one organization", "items": { "type": "object", - "required": ["name", "oid"], + "required": ["name", "oid", "status"], "properties": { - "name": { "type": "string" }, - "oid": { "type": "string", "format": "uuid" }, - "region": { "type": "string" }, - "sensors": { "type": "integer" }, - "cost": { "type": "number" }, + "name": { + "type": "string", + "description": "Organization display name", + "examples": ["Acme Corp", "TPS Reporting Solutions"] + }, + "oid": { + "type": "string", + "format": "uuid", + "description": "Organization ID (UUID)", + "examples": ["cdadbaa4-265d-4549-9686-990731ee4091"] + }, "status": { "type": "string", - "enum": ["active", "draft", "no_usage", "error"] + "description": "Data collection status", + "enum": ["success", "error", "no_invoice"], + "examples": ["success"] + }, + "cost": { + "type": "number", + "description": "Total invoice amount in dollars (converted from cents)", + "examples": [1250.00, 5000.00, 25000.00] + }, + "currency": { + "type": "string", + "default": "usd", + "description": "Currency code" }, "skus": { "type": "array", - "description": "SKU line items", + "description": "Invoice line items", "items": { "type": "object", - "required": ["name", "amount"], + "required": ["description", "amount"], "properties": { - "name": { "type": "string" }, - "amount": { "type": "number" }, - "quantity": { "type": ["string", "number"] } + "description": { + "type": "string", + "description": "Line item description (SKU name)", + "examples": [ + "Sensor License - Windows", + "Sensor License - Linux", + "Data Output (GB)" + ] + }, + "amount": { + "type": "number", + "description": "Amount in dollars", + "examples": [1000.00, 250.00, 500.00] + }, + "quantity": { + "type": ["string", "number"], + "description": "Quantity or usage amount for this SKU", + "examples": [21, "50 GB", "1,000,000 evals", 100] + } } } + }, + "invoice_url": { + "type": "string", + "format": "uri", + "description": "PDF download URL (expires)" + }, + "error_message": { + "type": "string", + "description": "Error details if status=error" } } } }, - "categories": { - "type": "array", - "description": "Cost breakdown by category (optional)", - "items": { - "type": "object", - "required": ["name", "amount"], - "properties": { - "name": { "type": "string" }, - "amount": { "type": "number" } + "rollup": { + "type": "object", + "description": "Aggregate totals across all tenants (optional, only when scope=all)", + "properties": { + "total_cost": { + "type": "number", + "description": "Sum of all tenant costs in dollars", + "examples": [12500.00, 45000.00] + }, + "total_tenants": { + "type": "integer", + "description": "Total number of tenants processed", + "examples": [24, 50] + }, + "successful_count": { + "type": "integer", + "description": "Tenants with successful data retrieval" + }, + "failed_count": { + "type": "integer", + "description": "Tenants that failed or had no invoice" } } } @@ -94,17 +141,94 @@ }, "warnings": { "type": "array", - "items": { "type": "string" } + "description": "Data collection warnings - displayed in yellow alert box", + "items": { + "type": "string" + }, + "examples": [ + ["12 organizations missing billing.ctrl permission", "Usage stats unavailable for 5 orgs"] + ] }, "errors": { "type": "array", + "description": "Per-organization errors - displayed in collapsible error section", "items": { "type": "object", "properties": { - "org_name": { "type": "string" }, - "error_message": { "type": "string" } + "org_name": { + "type": "string", + "description": "Name of the organization that failed", + "examples": ["Client XYZ", "Test Org"] + }, + "oid": { + "type": "string", + "description": "Organization ID (UUID)", + "examples": ["abc123-def456-..."] + }, + "error_message": { + "type": "string", + "description": "Error description", + "examples": ["Permission denied: missing billing.ctrl", "API timeout"] + } } } } - } + }, + "examples": [ + { + "metadata": { + "generated_at": "2025-12-10T14:32:45Z", + "period": "November 2025", + "scope": "all", + "tenant_count": 24 + }, + "data": { + "tenants": [ + { + "name": "TPS Reporting Solutions", + "oid": "aac9c41d-e0a3-4e7e-88b8-33936ab93238", + "status": "success", + "cost": 1250.00, + "currency": "usd", + "skus": [ + {"description": "Sensor License - Windows", "amount": 800.00, "quantity": 16}, + {"description": "Sensor License - Linux", "amount": 250.00, "quantity": 5}, + {"description": "Data Output (GB)", "amount": 200.00, "quantity": "40 GB"} + ] + }, + { + "name": "lc-infrastructure", + "oid": "efedcf87-318c-4c15-9f52-482606491223", + "status": "success", + "cost": 850.00, + "skus": [ + {"description": "Sensor License - Linux", "amount": 850.00, "quantity": 17} + ] + }, + { + "name": "Test Org", + "oid": "ddcc6630-20d1-4fd6-b16c-245c58a6d58e", + "status": "error", + "error_message": "Permission denied: missing billing.ctrl" + } + ], + "rollup": { + "total_cost": 2100.00, + "total_tenants": 3, + "successful_count": 2, + "failed_count": 1 + } + }, + "warnings": [ + "1 organization returned permission denied for billing data" + ], + "errors": [ + { + "org_name": "Test Org", + "oid": "ddcc6630-20d1-4fd6-b16c-245c58a6d58e", + "error_message": "Permission denied: missing billing.ctrl permission" + } + ] + } + ] } diff --git a/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/custom-report.json b/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/custom-report.json deleted file mode 100644 index 12f41bfa..00000000 --- a/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/custom-report.json +++ /dev/null @@ -1,256 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Custom Report Data", - "description": "Schema for component-based custom reports. Build flexible reports by specifying which components to include.", - "type": "object", - "required": ["metadata", "components"], - "properties": { - "metadata": { - "type": "object", - "properties": { - "title": { "type": "string", "description": "Report title" }, - "subtitle": { "type": "string", "description": "Report subtitle" }, - "generated_at": { "type": "string", "description": "Generation timestamp" } - } - }, - "components": { - "type": "array", - "description": "Array of components to render in order", - "items": { - "oneOf": [ - { "$ref": "#/$defs/summary_cards" }, - { "$ref": "#/$defs/chart" }, - { "$ref": "#/$defs/table" }, - { "$ref": "#/$defs/alert_banner" }, - { "$ref": "#/$defs/text_section" }, - { "$ref": "#/$defs/two_column" }, - { "$ref": "#/$defs/metric_grid" }, - { "$ref": "#/$defs/platform_table" }, - { "$ref": "#/$defs/divider" } - ] - } - }, - "warnings": { - "type": "array", - "items": { "type": "string" } - } - }, - "$defs": { - "summary_cards": { - "type": "object", - "required": ["type", "cards"], - "properties": { - "type": { "const": "summary_cards" }, - "title": { "type": "string" }, - "cards": { - "type": "array", - "items": { - "type": "object", - "required": ["label"], - "properties": { - "label": { "type": "string" }, - "value": { "type": ["string", "number"] }, - "subvalue": { "type": "string" }, - "warning": { "type": "string" }, - "icon": { - "type": "string", - "enum": ["sensors", "alerts", "rules", "users", "money", "check", "shield", "activity"] - }, - "icon_color": { - "type": "string", - "enum": ["blue", "green", "amber", "purple", "red"] - }, - "value_color": { "type": "string" }, - "format": { - "type": "string", - "enum": ["currency", "percent", "number"] - } - } - } - } - } - }, - "chart": { - "type": "object", - "required": ["type", "chart_type", "data"], - "properties": { - "type": { "const": "chart" }, - "title": { "type": "string" }, - "subtitle": { "type": "string" }, - "chart_type": { - "type": "string", - "enum": ["bar", "pie", "doughnut", "line"] - }, - "data": { - "type": "array", - "items": { - "type": "object", - "required": ["label", "value"], - "properties": { - "label": { "type": "string" }, - "value": { "type": "number" } - } - } - }, - "colors": { "type": "array", "items": { "type": "string" } }, - "horizontal": { "type": "boolean", "default": false }, - "show_legend": { "type": "boolean", "default": true }, - "legend_position": { "type": "string", "enum": ["top", "bottom", "left", "right"] }, - "height": { "type": "integer" }, - "dataset_label": { "type": "string" }, - "line_color": { "type": "string" }, - "fill": { "type": "boolean" }, - "empty_message": { "type": "string" } - } - }, - "table": { - "type": "object", - "required": ["type", "rows"], - "properties": { - "type": { "const": "table" }, - "title": { "type": "string" }, - "columns": { - "type": "array", - "items": { - "type": "object", - "required": ["label"], - "properties": { - "label": { "type": "string" }, - "align": { "type": "string", "enum": ["left", "right", "center"] } - } - } - }, - "rows": { - "type": "array", - "items": { - "type": "array", - "items": { - "oneOf": [ - { "type": "string" }, - { "type": "number" }, - { - "type": "object", - "properties": { - "value": { "type": ["string", "number"] }, - "align": { "type": "string" }, - "color": { "type": "string" }, - "badge": { "type": "string", "enum": ["success", "partial", "failed"] }, - "type": { "type": "string" } - } - } - ] - } - } - }, - "footer": { "type": "array" }, - "sortable": { "type": "boolean", "default": false }, - "empty_message": { "type": "string" } - } - }, - "alert_banner": { - "type": "object", - "required": ["type", "message"], - "properties": { - "type": { "const": "alert_banner" }, - "title": { "type": "string" }, - "message": { "type": "string" }, - "severity": { - "type": "string", - "enum": ["info", "warning", "danger", "success"], - "default": "info" - } - } - }, - "text_section": { - "type": "object", - "required": ["type", "content"], - "properties": { - "type": { "const": "text_section" }, - "title": { "type": "string" }, - "content": { "type": "string", "description": "HTML content" } - } - }, - "two_column": { - "type": "object", - "required": ["type", "columns"], - "properties": { - "type": { "const": "two_column" }, - "title": { "type": "string" }, - "columns": { - "type": "array", - "minItems": 2, - "maxItems": 2, - "items": { - "type": "object", - "properties": { - "title": { "type": "string" }, - "chart_type": { "type": "string", "enum": ["pie", "doughnut", "bar"] }, - "data": { - "type": "array", - "items": { - "type": "object", - "required": ["label", "value"], - "properties": { - "label": { "type": "string" }, - "value": { "type": "number" } - } - } - }, - "colors": { "type": "array", "items": { "type": "string" } } - } - } - } - } - }, - "metric_grid": { - "type": "object", - "required": ["type", "metrics"], - "properties": { - "type": { "const": "metric_grid" }, - "title": { "type": "string" }, - "metrics": { - "type": "array", - "items": { - "type": "object", - "required": ["label", "value"], - "properties": { - "label": { "type": "string" }, - "value": { "type": ["string", "number"] }, - "color": { "type": "string" } - } - } - } - } - }, - "platform_table": { - "type": "object", - "required": ["type", "platforms"], - "properties": { - "type": { "const": "platform_table" }, - "title": { "type": "string" }, - "show_detections": { "type": "boolean", "default": false }, - "platforms": { - "type": "array", - "items": { - "type": "object", - "required": ["name", "sensors"], - "properties": { - "name": { "type": "string" }, - "sensors": { "type": "integer" }, - "sensors_percent": { "type": "number" }, - "detections": { "type": "integer" }, - "detections_percent": { "type": "number" } - } - } - } - } - }, - "divider": { - "type": "object", - "required": ["type"], - "properties": { - "type": { "const": "divider" } - } - } - } -} diff --git a/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/customer-health.json b/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/customer-health.json new file mode 100644 index 00000000..2a6dbe0c --- /dev/null +++ b/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/customer-health.json @@ -0,0 +1,545 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Customer Health Dashboard Data", + "description": "Schema for data input to the customer-health template. Visualizes comprehensive customer health combining sensor coverage, detection activity, and health metrics for customer success tracking. All values must come from actual LimaCharlie API responses - no fabrication allowed.", + "type": "object", + "required": ["metadata", "data"], + "properties": { + "metadata": { + "type": "object", + "description": "Report metadata - when and how data was collected", + "required": ["generated_at", "time_window"], + "properties": { + "generated_at": { + "type": "string", + "description": "ISO 8601 timestamp when data was collected", + "examples": ["2025-12-10T14:32:45Z"] + }, + "time_window": { + "type": "object", + "description": "Time range for detection and activity data", + "required": ["start_display", "end_display", "days"], + "properties": { + "start": { + "type": "integer", + "description": "Start timestamp in Unix epoch seconds" + }, + "end": { + "type": "integer", + "description": "End timestamp in Unix epoch seconds" + }, + "start_display": { + "type": "string", + "description": "Human-readable start time", + "examples": ["2025-11-10 00:00:00 UTC"] + }, + "end_display": { + "type": "string", + "description": "Human-readable end time", + "examples": ["2025-12-10 23:59:59 UTC"] + }, + "days": { + "type": "integer", + "description": "Number of days in the time window", + "examples": [7, 14, 30] + } + } + }, + "organizations": { + "type": "object", + "description": "Organization processing summary", + "properties": { + "total": { + "type": "integer", + "description": "Total number of organizations attempted" + }, + "successful": { + "type": "integer", + "description": "Number of organizations successfully processed" + }, + "failed": { + "type": "integer", + "description": "Number of organizations that failed data collection" + }, + "success_rate": { + "type": "number", + "description": "Percentage of organizations successfully processed" + } + } + } + } + }, + "data": { + "type": "object", + "description": "Report data containing customer health metrics", + "required": ["rollup", "customers"], + "properties": { + "rollup": { + "type": "object", + "description": "Fleet-wide summary metrics - displayed in summary cards", + "required": ["total_customers", "total_sensors"], + "properties": { + "total_customers": { + "type": "integer", + "description": "Total number of customer organizations" + }, + "total_sensors": { + "type": "integer", + "description": "Total sensor count across all customers" + }, + "online_sensors": { + "type": "integer", + "description": "Number of sensors currently online" + }, + "offline_sensors": { + "type": "integer", + "description": "Number of sensors currently offline" + }, + "fleet_health_percent": { + "type": "number", + "description": "Fleet-wide health percentage (online/total * 100)" + }, + "total_detections": { + "type": "integer", + "description": "Total detections across all customers in time window" + }, + "customers_healthy": { + "type": "integer", + "description": "Number of customers with healthy status (>=90% sensors online)" + }, + "customers_warning": { + "type": "integer", + "description": "Number of customers with warning status (70-89% online)" + }, + "customers_critical": { + "type": "integer", + "description": "Number of customers with critical status (<70% online)" + }, + "customers_inactive": { + "type": "integer", + "description": "Number of customers with no sensors or no activity" + } + } + }, + "health_distribution": { + "type": "object", + "description": "Distribution of customer health statuses - displayed as donut chart", + "properties": { + "healthy": { + "type": "number", + "description": "Percentage of customers with healthy status" + }, + "warning": { + "type": "number", + "description": "Percentage of customers with warning status" + }, + "critical": { + "type": "number", + "description": "Percentage of customers with critical status" + }, + "inactive": { + "type": "number", + "description": "Percentage of customers with inactive status" + } + } + }, + "platforms": { + "type": "array", + "description": "Sensor count breakdown by platform across all customers", + "items": { + "type": "object", + "required": ["name", "count"], + "properties": { + "name": { + "type": "string", + "description": "Platform name (Windows, Linux, macOS, Chrome, USP Adapter, etc.)" + }, + "count": { + "type": "integer", + "description": "Number of sensors on this platform" + }, + "percent": { + "type": "number", + "description": "Percentage of total sensors" + } + } + } + }, + "customers": { + "type": "array", + "description": "Per-customer health details - displayed in sortable table with expandable details", + "items": { + "type": "object", + "required": ["name", "oid", "health_status"], + "properties": { + "name": { + "type": "string", + "description": "Customer organization name" + }, + "oid": { + "type": "string", + "format": "uuid", + "description": "Organization ID" + }, + "health_status": { + "type": "string", + "enum": ["healthy", "warning", "critical", "inactive", "error"], + "description": "Overall health status" + }, + "health_score": { + "type": "number", + "description": "Numeric health score (0-100) based on sensor uptime" + }, + "sensors": { + "type": "object", + "description": "Sensor metrics for this customer", + "properties": { + "total": { + "type": "integer", + "description": "Total sensor count" + }, + "online": { + "type": "integer", + "description": "Online sensor count" + }, + "offline": { + "type": "integer", + "description": "Offline sensor count" + }, + "offline_24h": { + "type": "integer", + "description": "Sensors offline for less than 24 hours" + }, + "offline_7d": { + "type": "integer", + "description": "Sensors offline for 1-7 days" + }, + "offline_30d": { + "type": "integer", + "description": "Sensors offline for more than 7 days" + } + } + }, + "detections": { + "type": "object", + "description": "Detection metrics for this customer", + "properties": { + "total": { + "type": ["integer", "null"], + "description": "Total detections in time window (null if unavailable)" + }, + "limit_reached": { + "type": "boolean", + "description": "Whether the 5000 detection limit was hit" + }, + "severities": { + "type": "object", + "description": "Breakdown by severity level", + "properties": { + "critical": {"type": "integer"}, + "high": {"type": "integer"}, + "medium": {"type": "integer"}, + "low": {"type": "integer"}, + "info": {"type": "integer"} + } + }, + "top_categories": { + "type": "array", + "description": "Top 5 detection categories for this customer", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "count": {"type": "integer"} + } + } + } + } + }, + "platforms": { + "type": "array", + "description": "Sensor breakdown by platform for this customer", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "count": {"type": "integer"} + } + } + }, + "last_activity": { + "type": "string", + "format": "date-time", + "description": "Timestamp of most recent sensor activity" + }, + "attention_items": { + "type": "array", + "description": "Items requiring attention for this customer - displayed as badges/alerts", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["offline_sensors", "high_severity_detections", "stale_sensors", "no_recent_detections", "detection_limit"], + "description": "Type of attention item" + }, + "severity": { + "type": "string", + "enum": ["info", "warning", "critical"], + "description": "Severity of the attention item" + }, + "message": { + "type": "string", + "description": "Human-readable description" + }, + "count": { + "type": "integer", + "description": "Count associated with this item" + } + } + } + } + } + } + }, + "attention_summary": { + "type": "object", + "description": "Summary of items requiring attention across all customers - displayed as alert section", + "properties": { + "critical_items": { + "type": "integer", + "description": "Total critical attention items" + }, + "warning_items": { + "type": "integer", + "description": "Total warning attention items" + }, + "customers_needing_attention": { + "type": "array", + "description": "List of customers with critical issues", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "oid": {"type": "string"}, + "issues": { + "type": "array", + "items": {"type": "string"} + } + } + } + } + } + }, + "top_detection_categories": { + "type": "array", + "description": "Top detection categories across all customers - displayed as bar chart", + "items": { + "type": "object", + "required": ["name", "count"], + "properties": { + "name": { + "type": "string", + "description": "Detection category name" + }, + "count": { + "type": "integer", + "description": "Total count across all customers" + }, + "customer_count": { + "type": "integer", + "description": "Number of customers with this detection type" + } + } + } + } + } + }, + "warnings": { + "type": "array", + "description": "Data collection warnings - displayed prominently in alert box", + "items": { + "type": "string" + }, + "examples": [ + ["Detection data limited to 5000 per organization", "3 customers have sensors offline > 30 days"] + ] + }, + "errors": { + "type": "array", + "description": "Per-customer errors - displayed in collapsible error section", + "items": { + "type": "object", + "properties": { + "org_name": { + "type": "string", + "description": "Name of the organization that failed" + }, + "oid": { + "type": "string", + "description": "Organization ID (UUID)" + }, + "error_message": { + "type": "string", + "description": "Error description" + } + } + } + } + }, + "examples": [ + { + "metadata": { + "generated_at": "2025-12-10T14:32:45Z", + "time_window": { + "start": 1731196800, + "end": 1733875199, + "start_display": "2025-11-10 00:00:00 UTC", + "end_display": "2025-12-10 23:59:59 UTC", + "days": 30 + }, + "organizations": { + "total": 24, + "successful": 22, + "failed": 2, + "success_rate": 91.7 + } + }, + "data": { + "rollup": { + "total_customers": 24, + "total_sensors": 1500, + "online_sensors": 1350, + "offline_sensors": 150, + "fleet_health_percent": 90.0, + "total_detections": 45000, + "customers_healthy": 18, + "customers_warning": 4, + "customers_critical": 1, + "customers_inactive": 1 + }, + "health_distribution": { + "healthy": 75.0, + "warning": 16.7, + "critical": 4.2, + "inactive": 4.2 + }, + "platforms": [ + {"name": "Windows", "count": 850, "percent": 56.7}, + {"name": "Linux", "count": 400, "percent": 26.7}, + {"name": "macOS", "count": 150, "percent": 10.0}, + {"name": "USP Adapter", "count": 100, "percent": 6.6} + ], + "customers": [ + { + "name": "TPS Reporting Solutions", + "oid": "aac9c41d-e0a3-4e7e-88b8-33936ab93238", + "health_status": "healthy", + "health_score": 94.7, + "sensors": { + "total": 150, + "online": 142, + "offline": 8, + "offline_24h": 5, + "offline_7d": 2, + "offline_30d": 1 + }, + "detections": { + "total": 1250, + "limit_reached": false, + "severities": { + "critical": 5, + "high": 45, + "medium": 200, + "low": 500, + "info": 500 + }, + "top_categories": [ + {"name": "SENSITIVE_PROCESS_ACCESS", "count": 500}, + {"name": "NEW_PROCESS", "count": 350} + ] + }, + "platforms": [ + {"name": "Windows", "count": 100}, + {"name": "Linux", "count": 50} + ], + "last_activity": "2025-12-10T14:30:00Z", + "attention_items": [] + }, + { + "name": "Acme Corp", + "oid": "cdadbaa4-265d-4549-9686-990731ee4091", + "health_status": "warning", + "health_score": 75.0, + "sensors": { + "total": 200, + "online": 150, + "offline": 50, + "offline_24h": 10, + "offline_7d": 25, + "offline_30d": 15 + }, + "detections": { + "total": 5000, + "limit_reached": true, + "severities": { + "critical": 25, + "high": 175, + "medium": 800, + "low": 2000, + "info": 2000 + } + }, + "attention_items": [ + { + "type": "offline_sensors", + "severity": "warning", + "message": "50 sensors offline", + "count": 50 + }, + { + "type": "stale_sensors", + "severity": "critical", + "message": "15 sensors offline > 30 days", + "count": 15 + }, + { + "type": "detection_limit", + "severity": "warning", + "message": "Hit 5000 detection limit", + "count": 5000 + } + ] + } + ], + "attention_summary": { + "critical_items": 3, + "warning_items": 8, + "customers_needing_attention": [ + { + "name": "Acme Corp", + "oid": "cdadbaa4-265d-4549-9686-990731ee4091", + "issues": ["50 sensors offline", "15 stale sensors", "Detection limit reached"] + } + ] + }, + "top_detection_categories": [ + {"name": "NEW_PROCESS", "count": 15000, "customer_count": 22}, + {"name": "SENSITIVE_PROCESS_ACCESS", "count": 8500, "customer_count": 18}, + {"name": "DNS_REQUEST", "count": 6000, "customer_count": 15} + ] + }, + "warnings": [ + "Detection data limited to 5000 per organization", + "3 customers have sensors offline > 30 days" + ], + "errors": [ + { + "org_name": "Test Org", + "oid": "ddcc6630-20d1-4fd6-b16c-245c58a6d58e", + "error_message": "Permission denied: missing sensor.list permission" + } + ] + } + ] +} diff --git a/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/detection-analytics.json b/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/detection-analytics.json new file mode 100644 index 00000000..d63d1b6a --- /dev/null +++ b/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/detection-analytics.json @@ -0,0 +1,392 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Detection Analytics Dashboard Data", + "description": "Schema for data input to the detection-analytics template. Visualizes detection volume, categories, severity breakdown, and trends across tenants. All values must come from actual LimaCharlie API responses - no fabrication allowed.", + "type": "object", + "required": ["metadata", "data"], + "properties": { + "metadata": { + "type": "object", + "description": "Report metadata - when and how data was collected", + "required": ["generated_at", "time_window"], + "properties": { + "generated_at": { + "type": "string", + "description": "ISO 8601 timestamp when data was collected", + "examples": ["2025-12-10T14:32:45Z"] + }, + "time_window": { + "type": "object", + "description": "Time range for detection data", + "required": ["start_display", "end_display", "days"], + "properties": { + "start": { + "type": "integer", + "description": "Start timestamp in Unix epoch seconds" + }, + "end": { + "type": "integer", + "description": "End timestamp in Unix epoch seconds" + }, + "start_display": { + "type": "string", + "description": "Human-readable start time", + "examples": ["2025-12-03 00:00:00 UTC"] + }, + "end_display": { + "type": "string", + "description": "Human-readable end time", + "examples": ["2025-12-10 23:59:59 UTC"] + }, + "days": { + "type": "integer", + "description": "Number of days in the time window", + "examples": [7, 14, 30] + } + } + }, + "scope": { + "type": "string", + "enum": ["single", "all"], + "description": "Report scope" + }, + "tenant_count": { + "type": "integer", + "description": "Number of tenants in report (when scope=all)" + } + } + }, + "data": { + "type": "object", + "description": "Report data containing detection analytics", + "required": ["tenants"], + "properties": { + "tenants": { + "type": "array", + "description": "Per-tenant detection data - displayed in sortable table with expandable details", + "items": { + "type": "object", + "required": ["name", "oid", "status"], + "properties": { + "name": { + "type": "string", + "description": "Organization name" + }, + "oid": { + "type": "string", + "format": "uuid", + "description": "Organization ID" + }, + "status": { + "type": "string", + "enum": ["success", "error", "no_detections"], + "description": "Data collection status" + }, + "detection_count": { + "type": "integer", + "description": "Total detections in time window" + }, + "limit_reached": { + "type": "boolean", + "description": "Whether the 5000 detection limit was hit" + }, + "categories": { + "type": "array", + "description": "Detection breakdown by category/rule", + "items": { + "type": "object", + "required": ["name", "count"], + "properties": { + "name": { + "type": "string", + "description": "Category or rule name" + }, + "count": { + "type": "integer", + "description": "Number of detections in this category" + }, + "percent": { + "type": "number", + "description": "Percentage of tenant's total detections" + } + } + } + }, + "severities": { + "type": "object", + "description": "Detection breakdown by severity - displayed as stacked bar", + "properties": { + "critical": { + "type": "integer", + "description": "Critical severity count" + }, + "high": { + "type": "integer", + "description": "High severity count" + }, + "medium": { + "type": "integer", + "description": "Medium severity count" + }, + "low": { + "type": "integer", + "description": "Low severity count" + }, + "info": { + "type": "integer", + "description": "Informational severity count" + } + } + }, + "top_hosts": { + "type": "array", + "description": "Hosts with most detections", + "items": { + "type": "object", + "required": ["hostname", "count"], + "properties": { + "hostname": { + "type": "string", + "description": "Host name" + }, + "sid": { + "type": "string", + "description": "Sensor ID" + }, + "count": { + "type": "integer", + "description": "Detection count for this host" + } + } + } + }, + "error_message": { + "type": "string", + "description": "Error details if status=error" + } + } + } + }, + "rollup": { + "type": "object", + "description": "Aggregate totals across all tenants - displayed in summary cards", + "properties": { + "total_detections": { + "type": "integer", + "description": "Sum of all detections across tenants" + }, + "total_tenants": { + "type": "integer", + "description": "Total number of tenants processed" + }, + "tenants_with_detections": { + "type": "integer", + "description": "Tenants that had at least one detection" + }, + "tenants_at_limit": { + "type": "integer", + "description": "Tenants that hit the 5000 detection limit" + }, + "successful_count": { + "type": "integer", + "description": "Tenants with successful data retrieval" + }, + "failed_count": { + "type": "integer", + "description": "Tenants that failed data retrieval" + }, + "severities": { + "type": "object", + "description": "Aggregate severity breakdown across all tenants", + "properties": { + "critical": {"type": "integer"}, + "high": {"type": "integer"}, + "medium": {"type": "integer"}, + "low": {"type": "integer"}, + "info": {"type": "integer"} + } + } + } + }, + "top_categories": { + "type": "array", + "description": "Top detection categories across all tenants - displayed as bar chart", + "items": { + "type": "object", + "required": ["name", "count"], + "properties": { + "name": { + "type": "string", + "description": "Category or rule name" + }, + "count": { + "type": "integer", + "description": "Total count across all tenants" + }, + "tenant_count": { + "type": "integer", + "description": "Number of tenants with this detection type" + } + } + } + }, + "top_tenants": { + "type": "array", + "description": "Tenants ranked by detection volume - displayed as horizontal bar chart", + "items": { + "type": "object", + "required": ["name", "count"], + "properties": { + "name": { + "type": "string", + "description": "Tenant name" + }, + "oid": { + "type": "string", + "description": "Organization ID" + }, + "count": { + "type": "integer", + "description": "Detection count" + }, + "limit_reached": { + "type": "boolean", + "description": "Whether limit was hit" + } + } + } + } + } + }, + "warnings": { + "type": "array", + "description": "Data collection warnings - displayed prominently in alert box", + "items": { + "type": "string" + }, + "examples": [ + ["5 tenants hit the 5000 detection limit", "Severity data unavailable for 3 tenants"] + ] + }, + "errors": { + "type": "array", + "description": "Per-tenant errors - displayed in collapsible error section", + "items": { + "type": "object", + "properties": { + "org_name": { + "type": "string", + "description": "Name of the organization that failed" + }, + "oid": { + "type": "string", + "description": "Organization ID (UUID)" + }, + "error_message": { + "type": "string", + "description": "Error description" + } + } + } + } + }, + "examples": [ + { + "metadata": { + "generated_at": "2025-12-10T14:32:45Z", + "time_window": { + "start": 1733270400, + "end": 1733875199, + "start_display": "2025-12-03 00:00:00 UTC", + "end_display": "2025-12-10 23:59:59 UTC", + "days": 7 + }, + "scope": "all", + "tenant_count": 24 + }, + "data": { + "tenants": [ + { + "name": "TPS Reporting Solutions", + "oid": "aac9c41d-e0a3-4e7e-88b8-33936ab93238", + "status": "success", + "detection_count": 1250, + "limit_reached": false, + "categories": [ + {"name": "SENSITIVE_PROCESS_ACCESS", "count": 500, "percent": 40.0}, + {"name": "NEW_PROCESS", "count": 350, "percent": 28.0}, + {"name": "NETWORK_CONNECTIONS", "count": 200, "percent": 16.0} + ], + "severities": { + "critical": 5, + "high": 45, + "medium": 200, + "low": 500, + "info": 500 + }, + "top_hosts": [ + {"hostname": "WORKSTATION-01", "sid": "abc123", "count": 150}, + {"hostname": "SERVER-DC01", "sid": "def456", "count": 120} + ] + }, + { + "name": "Acme Corp", + "oid": "cdadbaa4-265d-4549-9686-990731ee4091", + "status": "success", + "detection_count": 5000, + "limit_reached": true, + "categories": [ + {"name": "NEW_PROCESS", "count": 2500, "percent": 50.0}, + {"name": "DNS_REQUEST", "count": 1500, "percent": 30.0} + ], + "severities": { + "critical": 25, + "high": 175, + "medium": 800, + "low": 2000, + "info": 2000 + } + } + ], + "rollup": { + "total_detections": 45000, + "total_tenants": 24, + "tenants_with_detections": 20, + "tenants_at_limit": 5, + "successful_count": 22, + "failed_count": 2, + "severities": { + "critical": 150, + "high": 1200, + "medium": 8000, + "low": 18000, + "info": 17650 + } + }, + "top_categories": [ + {"name": "NEW_PROCESS", "count": 15000, "tenant_count": 22}, + {"name": "SENSITIVE_PROCESS_ACCESS", "count": 8500, "tenant_count": 18}, + {"name": "DNS_REQUEST", "count": 6000, "tenant_count": 15}, + {"name": "NETWORK_CONNECTIONS", "count": 5500, "tenant_count": 20}, + {"name": "FILE_CREATE", "count": 4000, "tenant_count": 19} + ], + "top_tenants": [ + {"name": "GlobalTech Industries", "oid": "abc123", "count": 5000, "limit_reached": true}, + {"name": "Acme Corp", "oid": "def456", "count": 5000, "limit_reached": true}, + {"name": "Nexus Systems", "oid": "ghi789", "count": 4200, "limit_reached": false} + ] + }, + "warnings": [ + "5 tenants hit the 5000 detection limit - actual counts may be higher", + "Severity data based on detection metadata where available" + ], + "errors": [ + { + "org_name": "Test Org", + "oid": "ddcc6630-20d1-4fd6-b16c-245c58a6d58e", + "error_message": "Permission denied: missing insight.det.get permission" + } + ] + } + ] +} diff --git a/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/fleet-health.json b/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/fleet-health.json new file mode 100644 index 00000000..6aa0bc08 --- /dev/null +++ b/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/fleet-health.json @@ -0,0 +1,492 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Fleet Health Dashboard Data", + "description": "Schema for data input to the fleet-health template. Visualizes sensor health, platform distribution, and detection activity across multiple organizations. All values must come from actual LimaCharlie API responses - no fabrication allowed.", + "type": "object", + "required": ["metadata", "data"], + "properties": { + "metadata": { + "type": "object", + "description": "Report metadata - when and how data was collected", + "required": ["generated_at", "time_window"], + "properties": { + "generated_at": { + "type": "string", + "description": "ISO 8601 timestamp when data was collected", + "examples": ["2025-12-10T14:32:45Z"] + }, + "time_window": { + "type": "object", + "description": "Time range for detection data", + "required": ["start_display", "end_display", "days"], + "properties": { + "start": { + "type": "integer", + "description": "Start timestamp in Unix epoch seconds" + }, + "end": { + "type": "integer", + "description": "End timestamp in Unix epoch seconds" + }, + "start_display": { + "type": "string", + "description": "Human-readable start time", + "examples": ["2025-12-03 00:00:00 UTC"] + }, + "end_display": { + "type": "string", + "description": "Human-readable end time", + "examples": ["2025-12-10 23:59:59 UTC"] + }, + "days": { + "type": "integer", + "description": "Number of days in the time window", + "examples": [7, 14, 30] + } + } + }, + "organizations": { + "type": "object", + "description": "Organization processing summary", + "properties": { + "total": { + "type": "integer", + "description": "Total number of organizations attempted" + }, + "successful": { + "type": "integer", + "description": "Number of organizations with full data access" + }, + "limited_access": { + "type": "integer", + "description": "Number of organizations with limited permissions (partial data)" + }, + "failed": { + "type": "integer", + "description": "Number of organizations that failed data collection completely" + }, + "success_rate": { + "type": "number", + "description": "Percentage of organizations successfully processed" + } + } + } + } + }, + "data": { + "type": "object", + "description": "Report data containing rollup totals and per-organization details", + "required": ["rollup"], + "properties": { + "rollup": { + "type": "object", + "description": "Aggregate totals across all organizations - displayed in summary cards", + "required": ["total_sensors", "online_sensors"], + "properties": { + "total_sensors": { + "type": "integer", + "description": "Total sensor count across all organizations", + "examples": [1500, 5000] + }, + "online_sensors": { + "type": "integer", + "description": "Number of sensors currently online", + "examples": [1350, 4500] + }, + "offline_sensors": { + "type": "integer", + "description": "Number of sensors currently offline (total - online)", + "examples": [150, 500] + }, + "health_percent": { + "type": "number", + "description": "Fleet-wide health percentage (online/total * 100)", + "examples": [90.0, 95.5] + }, + "total_detections": { + "type": "integer", + "description": "Total detections across all organizations in time window", + "examples": [12500, 50000] + }, + "detection_limit_reached": { + "type": "boolean", + "description": "Whether any organization hit the 5000 detection limit" + }, + "orgs_at_limit": { + "type": "integer", + "description": "Number of organizations that hit the detection limit" + } + } + }, + "platforms": { + "type": "array", + "description": "Sensor count breakdown by platform - displayed as donut chart", + "items": { + "type": "object", + "required": ["name", "count"], + "properties": { + "name": { + "type": "string", + "description": "Platform name (Windows, Linux, macOS, Chrome, USP Adapter, etc.)", + "examples": ["Windows", "Linux", "macOS"] + }, + "count": { + "type": "integer", + "description": "Number of sensors on this platform" + }, + "percent": { + "type": "number", + "description": "Percentage of total sensors" + } + } + } + }, + "organizations": { + "type": "array", + "description": "Per-organization health details - displayed in sortable table", + "items": { + "type": "object", + "required": ["name", "oid"], + "properties": { + "name": { + "type": "string", + "description": "Organization display name", + "examples": ["Acme Corp", "TPS Reporting Solutions"] + }, + "oid": { + "type": "string", + "format": "uuid", + "description": "Organization ID" + }, + "sensors_total": { + "type": "integer", + "description": "Total sensor count for this organization" + }, + "sensors_online": { + "type": "integer", + "description": "Online sensor count" + }, + "sensors_offline": { + "type": "integer", + "description": "Offline sensor count" + }, + "health": { + "type": "number", + "description": "Health percentage (0-100)" + }, + "detections": { + "type": ["integer", "null"], + "description": "Detection count in time window (null if data unavailable)" + }, + "detection_limit_reached": { + "type": "boolean", + "description": "Whether this org hit the 5000 detection limit" + }, + "status": { + "type": "string", + "enum": ["healthy", "warning", "critical", "limited_access", "error"], + "description": "Health status: healthy (>=90%), warning (70-89%), critical (<70%), limited_access (missing permissions), error (no data)" + }, + "status_reason": { + "type": "string", + "description": "Explanation for limited_access or error status" + }, + "platforms": { + "type": "array", + "description": "Per-organization platform breakdown with online/offline counts", + "items": { + "type": "object", + "required": ["name", "count"], + "properties": { + "name": { "type": "string", "description": "Platform name" }, + "count": { "type": "integer", "description": "Total sensors on this platform" }, + "online": { "type": "integer", "description": "Online sensors (optional if limited_access)" }, + "offline": { "type": "integer", "description": "Offline sensors (optional if limited_access)" } + } + } + }, + "top_detections": { + "type": "array", + "description": "Top detections for this organization", + "items": { + "type": "object", + "required": ["name", "count"], + "properties": { + "name": { "type": "string", "description": "Detection rule/category name" }, + "count": { "type": "integer", "description": "Number of times detected" }, + "severity": { "type": "string", "enum": ["critical", "high", "medium", "low", "info"] }, + "host": { "type": "string", "description": "Primary affected host" } + } + } + }, + "top_offline_sensors": { + "type": "array", + "description": "Top offline sensors for this organization", + "items": { + "type": "object", + "required": ["hostname", "platform"], + "properties": { + "hostname": { "type": "string", "description": "Sensor hostname" }, + "platform": { "type": "string", "description": "Sensor platform" }, + "last_seen": { "type": "string", "description": "Last seen timestamp (display format)" }, + "days_offline": { "type": "integer", "description": "Days since last seen" } + } + } + } + } + } + }, + "top_categories": { + "type": "array", + "description": "Top detection categories across all organizations - displayed as list with severity", + "items": { + "type": "object", + "required": ["label", "value"], + "properties": { + "label": { + "type": "string", + "description": "Detection category name (from source_rule or cat field)" + }, + "value": { + "type": "integer", + "description": "Total detection count for this category" + }, + "severity": { + "type": "string", + "enum": ["critical", "high", "medium", "low", "info"], + "description": "Severity level of this detection category" + }, + "affected_orgs": { + "type": "integer", + "description": "Number of organizations with this detection" + }, + "affected_hosts": { + "type": "integer", + "description": "Number of unique hosts with this detection" + } + } + } + }, + "top_orgs_by_detections": { + "type": "array", + "description": "Top organizations by detection volume - displayed as list", + "items": { + "type": "object", + "required": ["name", "value"], + "properties": { + "name": { + "type": "string", + "description": "Organization name" + }, + "value": { + "type": "integer", + "description": "Detection count" + }, + "limit_reached": { + "type": "boolean", + "description": "Whether this org hit the detection limit" + }, + "top_category": { + "type": ["string", "null"], + "description": "Most common detection category for this org" + }, + "sensors": { + "type": "integer", + "description": "Total sensor count for this org" + } + } + } + }, + "offline_summary": { + "type": "object", + "description": "Summary of offline sensors across all organizations", + "properties": { + "total_offline": { + "type": "integer", + "description": "Total number of offline sensors" + }, + "offline_7_plus_days": { + "type": "integer", + "description": "Sensors offline for 7 or more days" + }, + "offline_30_plus_days": { + "type": "integer", + "description": "Sensors offline for 30 or more days" + }, + "by_platform": { + "type": "array", + "description": "Offline sensor counts by platform", + "items": { + "type": "object", + "required": ["platform", "count"], + "properties": { + "platform": { "type": "string" }, + "count": { "type": "integer" } + } + } + } + } + } + } + }, + "warnings": { + "type": "array", + "description": "Data collection warnings - displayed in yellow alert box", + "items": { + "type": "string" + }, + "examples": [ + ["4 organizations hit the 5000 detection limit", "Detection counts are approximate for 2 orgs"] + ] + }, + "errors": { + "type": "array", + "description": "Per-organization errors - displayed in collapsible error section", + "items": { + "type": "object", + "properties": { + "org_name": { + "type": "string", + "description": "Name of the organization that failed" + }, + "oid": { + "type": "string", + "description": "Organization ID (UUID)" + }, + "error_message": { + "type": "string", + "description": "Error description" + } + } + } + } + }, + "examples": [ + { + "metadata": { + "generated_at": "2025-12-10T14:32:45Z", + "time_window": { + "start": 1733270400, + "end": 1733875199, + "start_display": "2025-12-03 00:00:00 UTC", + "end_display": "2025-12-10 23:59:59 UTC", + "days": 7 + }, + "organizations": { + "total": 24, + "successful": 20, + "limited_access": 2, + "failed": 2, + "success_rate": 91.7 + } + }, + "data": { + "rollup": { + "total_sensors": 1500, + "online_sensors": 1350, + "offline_sensors": 150, + "health_percent": 90.0, + "total_detections": 12500, + "detection_limit_reached": true, + "orgs_at_limit": 3 + }, + "platforms": [ + {"name": "Windows", "count": 850, "percent": 56.7}, + {"name": "Linux", "count": 400, "percent": 26.7}, + {"name": "macOS", "count": 150, "percent": 10.0}, + {"name": "USP Adapter", "count": 100, "percent": 6.6} + ], + "organizations": [ + { + "name": "TPS Reporting Solutions", + "oid": "aac9c41d-e0a3-4e7e-88b8-33936ab93238", + "sensors_total": 150, + "sensors_online": 142, + "sensors_offline": 8, + "health": 94.7, + "detections": 1250, + "detection_limit_reached": false, + "status": "healthy", + "platforms": [ + {"name": "Windows", "count": 100, "online": 95, "offline": 5}, + {"name": "Linux", "count": 50, "online": 47, "offline": 3} + ], + "top_detections": [ + {"name": "Suspicious Process", "count": 5, "severity": "medium", "host": "workstation-01"} + ], + "top_offline_sensors": [ + {"hostname": "laptop-042", "platform": "windows", "last_seen": "2025-12-08 10:00 UTC", "days_offline": 2} + ] + }, + { + "name": "Acme Corp", + "oid": "cdadbaa4-265d-4549-9686-990731ee4091", + "sensors_total": 200, + "sensors_online": 150, + "sensors_offline": 50, + "health": 75.0, + "detections": 5000, + "detection_limit_reached": true, + "status": "warning", + "platforms": [ + {"name": "Windows", "count": 180, "online": 140, "offline": 40}, + {"name": "macOS", "count": 20, "online": 10, "offline": 10} + ], + "top_detections": [ + {"name": "Network Scan Detected", "count": 150, "severity": "high", "host": "server-db-01"} + ], + "top_offline_sensors": [ + {"hostname": "remote-laptop-001", "platform": "windows", "last_seen": "2025-12-01 08:00 UTC", "days_offline": 9} + ] + }, + { + "name": "Limited Corp", + "oid": "eecc6630-20d1-4fd6-b16c-245c58a6d58e", + "sensors_total": 500, + "sensors_online": null, + "sensors_offline": null, + "health": null, + "detections": null, + "detection_limit_reached": false, + "status": "limited_access", + "status_reason": "Sensor online/offline status not available - may require sensor.list.* permission", + "platforms": [ + {"name": "Windows", "count": 400}, + {"name": "Linux", "count": 100} + ] + } + ], + "top_categories": [ + {"label": "SENSITIVE_PROCESS_ACCESS", "value": 3500, "severity": "high", "affected_orgs": 8, "affected_hosts": 45}, + {"label": "NEW_PROCESS", "value": 2800, "severity": "info", "affected_orgs": 15, "affected_hosts": 120}, + {"label": "NETWORK_CONNECTIONS", "value": 1500, "severity": "medium", "affected_orgs": 5, "affected_hosts": 22} + ], + "top_orgs_by_detections": [ + {"name": "Acme Corp", "value": 5000, "limit_reached": true, "top_category": "Network Scan Detected", "sensors": 200}, + {"name": "GlobalTech", "value": 3200, "limit_reached": false, "top_category": "NEW_PROCESS", "sensors": 150} + ], + "offline_summary": { + "total_offline": 150, + "offline_7_plus_days": 25, + "offline_30_plus_days": 5, + "by_platform": [ + {"platform": "Windows", "count": 100}, + {"platform": "Linux", "count": 30}, + {"platform": "macOS", "count": 20} + ] + } + }, + "warnings": [ + "3 organizations hit the 5000 detection limit - actual counts may be higher", + "2 organizations have limited access permissions - sensor health data unavailable" + ], + "errors": [ + { + "org_name": "Test Org", + "oid": "ddcc6630-20d1-4fd6-b16c-245c58a6d58e", + "error_message": "Permission denied: missing sensor.list permission" + } + ] + } + ] +} diff --git a/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/mssp-report.json b/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/mssp-report.json deleted file mode 100644 index 18f2c83f..00000000 --- a/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/mssp-report.json +++ /dev/null @@ -1,311 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MSSP Dashboard Report Data", - "description": "Schema for data input to the mssp-dashboard template. All values must come from actual API responses - no fabrication allowed.", - "type": "object", - "required": ["metadata", "data"], - "properties": { - "metadata": { - "type": "object", - "description": "Report metadata - when and how data was collected", - "required": ["generated_at", "time_window", "organizations"], - "properties": { - "generated_at": { - "type": "string", - "description": "ISO 8601 timestamp when data was collected", - "format": "date-time", - "examples": ["2025-11-27T14:32:45Z"] - }, - "time_window": { - "type": "object", - "description": "Time range covered by the report", - "required": ["start", "end", "start_display", "end_display"], - "properties": { - "start": { - "type": "integer", - "description": "Start timestamp in Unix epoch seconds" - }, - "end": { - "type": "integer", - "description": "End timestamp in Unix epoch seconds" - }, - "start_display": { - "type": "string", - "description": "Human-readable start time", - "examples": ["2025-11-01 00:00:00 UTC"] - }, - "end_display": { - "type": "string", - "description": "Human-readable end time", - "examples": ["2025-11-30 23:59:59 UTC"] - }, - "days": { - "type": "integer", - "description": "Number of days in the time window" - } - } - }, - "organizations": { - "type": "object", - "description": "Summary of organizations processed", - "required": ["total", "successful"], - "properties": { - "total": { - "type": "integer", - "description": "Total number of organizations attempted" - }, - "successful": { - "type": "integer", - "description": "Number of organizations successfully processed" - }, - "failed": { - "type": "integer", - "description": "Number of organizations that failed" - }, - "success_rate": { - "type": "number", - "description": "Percentage of successful organizations", - "minimum": 0, - "maximum": 100 - } - } - } - } - }, - "data": { - "type": "object", - "description": "Report data - all values from actual API responses", - "required": ["aggregate", "organizations"], - "properties": { - "aggregate": { - "type": "object", - "description": "Aggregated metrics across all successful organizations", - "required": ["sensors", "detections", "rules"], - "properties": { - "sensors": { - "type": "object", - "description": "Sensor metrics", - "required": ["total", "online"], - "properties": { - "total": { - "type": "integer", - "description": "Total number of sensors" - }, - "online": { - "type": "integer", - "description": "Number of online sensors" - }, - "offline": { - "type": "integer", - "description": "Number of offline sensors" - }, - "health_percent": { - "type": "number", - "description": "Percentage of sensors online" - }, - "platforms": { - "type": "object", - "description": "Sensor count by platform", - "additionalProperties": { - "type": "integer" - }, - "examples": [{"windows": 1823, "linux": 712, "macos": 247}] - } - } - }, - "detections": { - "type": "object", - "description": "Detection metrics", - "required": ["retrieved", "top_categories"], - "properties": { - "retrieved": { - "type": "integer", - "description": "Total detections retrieved (may be limited)" - }, - "limit_reached": { - "type": "boolean", - "description": "Whether any organization hit the detection limit" - }, - "orgs_at_limit": { - "type": "integer", - "description": "Number of organizations that hit detection limit" - }, - "top_categories": { - "type": "array", - "description": "Top detection categories with counts", - "items": { - "type": "object", - "required": ["label", "value"], - "properties": { - "label": { - "type": "string", - "description": "Category name" - }, - "value": { - "type": "integer", - "description": "Detection count" - } - } - } - }, - "daily_trend": { - "type": "array", - "description": "Daily detection counts (optional)", - "items": { - "type": "object", - "required": ["date", "value"], - "properties": { - "date": { - "type": "string", - "description": "Date in YYYY-MM-DD format" - }, - "value": { - "type": "integer", - "description": "Detection count for that day" - } - } - } - } - } - }, - "rules": { - "type": "object", - "description": "D&R rule metrics", - "required": ["total", "enabled"], - "properties": { - "total": { - "type": "integer", - "description": "Total number of D&R rules" - }, - "enabled": { - "type": "integer", - "description": "Number of enabled rules" - } - } - }, - "usage": { - "type": "object", - "description": "Usage metrics (optional)", - "properties": { - "total_events": { - "type": "integer", - "description": "Total sensor events" - }, - "total_output_bytes": { - "type": "integer", - "description": "Total bytes output" - }, - "total_output_gb": { - "type": "number", - "description": "Total GB output" - }, - "total_evaluations": { - "type": "integer", - "description": "Total D&R evaluations" - } - } - } - } - }, - "organizations": { - "type": "array", - "description": "Per-organization data", - "items": { - "type": "object", - "required": ["name", "oid", "status"], - "properties": { - "name": { - "type": "string", - "description": "Organization name" - }, - "oid": { - "type": "string", - "description": "Organization ID (UUID)", - "format": "uuid" - }, - "status": { - "type": "string", - "description": "Data collection status", - "enum": ["success", "partial", "failed"] - }, - "sensors_total": { - "type": "integer", - "description": "Total sensors in this org" - }, - "sensors_online": { - "type": "integer", - "description": "Online sensors in this org" - }, - "health": { - "type": "number", - "description": "Health percentage (0-100)" - }, - "detections": { - "type": "integer", - "description": "Detection count for this org" - }, - "detection_limit_reached": { - "type": "boolean", - "description": "Whether this org hit the detection limit" - }, - "rules_total": { - "type": "integer", - "description": "Total rules in this org" - }, - "rules_enabled": { - "type": "integer", - "description": "Enabled rules in this org" - } - } - } - } - } - }, - "warnings": { - "type": "array", - "description": "Warnings from data collection - ALL must be displayed", - "items": { - "type": "string" - } - }, - "errors": { - "type": "array", - "description": "Errors from data collection - ALL must be displayed", - "items": { - "type": "object", - "properties": { - "org_name": { - "type": "string", - "description": "Organization name" - }, - "oid": { - "type": "string", - "description": "Organization ID" - }, - "error_code": { - "type": ["integer", "string"], - "description": "Error code" - }, - "error_message": { - "type": "string", - "description": "Error message" - }, - "remediation": { - "type": "string", - "description": "Suggested remediation action" - } - } - } - }, - "source_skill": { - "type": "string", - "description": "Skill that generated the data", - "default": "reporting" - } - }, - "$defs": { - "data_accuracy_note": { - "description": "DATA ACCURACY GUARDRAILS: All values in this schema must come from actual API responses. The graphic-output skill will NEVER fabricate, estimate, or interpolate data. Missing fields will show as 'N/A' in the output." - } - } -} diff --git a/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/security-overview.json b/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/security-overview.json deleted file mode 100644 index f44e941c..00000000 --- a/marketplace/plugins/lc-essentials/skills/graphic-output/schemas/security-overview.json +++ /dev/null @@ -1,179 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Security Overview Report Data", - "description": "Schema for data input to the security-overview template. Single-tenant comprehensive security report. All values must come from actual API responses - no fabrication allowed.", - "type": "object", - "required": ["metadata", "data"], - "properties": { - "metadata": { - "type": "object", - "description": "Report metadata - when and how data was collected", - "required": ["generated_at", "time_window", "org_name", "oid"], - "properties": { - "generated_at": { - "type": "string", - "description": "ISO 8601 timestamp when data was collected", - "format": "date-time" - }, - "time_window": { - "type": "object", - "required": ["start", "end", "start_display", "end_display"], - "properties": { - "start": { "type": "integer" }, - "end": { "type": "integer" }, - "start_display": { "type": "string" }, - "end_display": { "type": "string" }, - "days": { "type": "integer" } - } - }, - "org_name": { - "type": "string", - "description": "Organization name" - }, - "oid": { - "type": "string", - "description": "Organization ID (UUID)", - "format": "uuid" - } - } - }, - "data": { - "type": "object", - "description": "Report data - all values from actual API responses", - "required": ["summary"], - "properties": { - "summary": { - "type": "object", - "description": "Executive summary metrics", - "required": ["sensors_total", "detections_total", "rules_total"], - "properties": { - "sensors_total": { "type": "integer" }, - "sensors_online": { "type": "integer" }, - "detections_total": { "type": "integer" }, - "detections_24h": { "type": "integer" }, - "rules_total": { "type": "integer" }, - "rules_enabled": { "type": "integer" }, - "mitre_coverage": { "type": "number", "minimum": 0, "maximum": 100 } - } - }, - "active_threats": { - "type": "array", - "description": "Active threat alerts (optional)", - "items": { - "type": "object", - "properties": { - "title": { "type": "string" }, - "description": { "type": "string" }, - "attacker_ip": { "type": "string" }, - "start_time": { "type": "string" }, - "severity": { "type": "string", "enum": ["critical", "high", "medium", "low"] } - } - } - }, - "platforms": { - "type": "array", - "description": "Platform breakdown with sensors and detections", - "items": { - "type": "object", - "required": ["name", "sensors"], - "properties": { - "name": { "type": "string" }, - "sensors": { "type": "integer" }, - "sensors_percent": { "type": "number" }, - "detections": { "type": "integer" }, - "detections_percent": { "type": "number" }, - "badge_class": { "type": "string" } - } - } - }, - "hourly_detections": { - "type": "array", - "description": "Detection timeline data (optional)", - "items": { - "type": "object", - "required": ["hour", "count"], - "properties": { - "hour": { "type": "string" }, - "count": { "type": "integer" } - } - } - }, - "top_detections": { - "type": "array", - "description": "Top detection categories", - "items": { - "type": "object", - "required": ["category", "count"], - "properties": { - "category": { "type": "string" }, - "count": { "type": "integer" }, - "trend": { "type": "string", "enum": ["up", "down", "stable"] } - } - } - }, - "mitre_techniques": { - "type": "array", - "description": "MITRE ATT&CK technique coverage (optional)", - "items": { - "type": "object", - "required": ["id", "name"], - "properties": { - "id": { "type": "string", "pattern": "^T[0-9]{4}(\\.[0-9]{3})?$" }, - "name": { "type": "string" }, - "tactic": { "type": "string" }, - "detection_count": { "type": "integer" } - } - } - }, - "rules": { - "type": "object", - "description": "D&R rule inventory", - "properties": { - "general": { "type": "integer" }, - "managed": { "type": "integer" }, - "service": { "type": "integer" }, - "enabled": { "type": "integer" }, - "disabled": { "type": "integer" } - } - }, - "attack_analysis": { - "type": "object", - "description": "Attack pattern analysis (optional)", - "properties": { - "primary_vector": { "type": "string" }, - "top_attacker_ips": { - "type": "array", - "items": { "type": "string" } - }, - "targeted_assets": { - "type": "array", - "items": { "type": "string" } - } - } - } - } - }, - "warnings": { - "type": "array", - "description": "Warnings from data collection - ALL must be displayed", - "items": { "type": "string" } - }, - "errors": { - "type": "array", - "description": "Errors from data collection", - "items": { - "type": "object", - "properties": { - "error_code": { "type": ["integer", "string"] }, - "error_message": { "type": "string" }, - "remediation": { "type": "string" } - } - } - } - }, - "$defs": { - "data_accuracy_note": { - "description": "DATA ACCURACY GUARDRAILS: All values must come from actual API responses. Missing fields show as 'N/A'." - } - } -} diff --git a/marketplace/plugins/lc-essentials/skills/graphic-output/templates/reports/billing-summary.html.j2 b/marketplace/plugins/lc-essentials/skills/graphic-output/templates/reports/billing-summary.html.j2 index 834b095f..ea90897a 100644 --- a/marketplace/plugins/lc-essentials/skills/graphic-output/templates/reports/billing-summary.html.j2 +++ b/marketplace/plugins/lc-essentials/skills/graphic-output/templates/reports/billing-summary.html.j2 @@ -36,43 +36,75 @@ .meta dd { font-weight: 600; margin-top: 0.25rem; } .cost-highlight { font-size: 1.125rem; font-weight: 700; color: var(--color-success); } + + /* Expandable tenant cards */ .tenant-card { - padding: 1.25rem; background: var(--color-card); border: 1px solid var(--color-border); border-radius: var(--radius-lg); - margin-bottom: 1rem; + margin-bottom: 0.75rem; + overflow: hidden; } .tenant-header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 1rem; - padding-bottom: 0.75rem; - border-bottom: 1px solid var(--color-border); + padding: 1rem 1.25rem; + cursor: pointer; + user-select: none; + transition: background 0.15s ease; + } + .tenant-header:hover { + background: var(--color-bg); + } + .tenant-header-left { + display: flex; + align-items: center; + gap: 0.75rem; } - .tenant-name { font-size: 1.125rem; font-weight: 600; } - .tenant-cost { font-size: 1.5rem; font-weight: 700; color: var(--color-success); } + .tenant-expand-icon { + width: 20px; + height: 20px; + color: var(--color-text-muted); + transition: transform 0.2s ease; + } + .tenant-card.expanded .tenant-expand-icon { + transform: rotate(90deg); + } + .tenant-name { font-size: 1rem; font-weight: 600; } + .tenant-meta { font-size: 0.8rem; color: var(--color-text-muted); } + .tenant-cost { font-size: 1.25rem; font-weight: 700; color: var(--color-success); } + .tenant-cost.zero { color: var(--color-text-muted); } + + .tenant-details { + display: none; + padding: 0 1.25rem 1.25rem 1.25rem; + border-top: 1px solid var(--color-border); + } + .tenant-card.expanded .tenant-details { + display: block; + } + .sku-row { display: flex; justify-content: space-between; padding: 0.5rem 0; - border-bottom: 1px solid #f8fafc; + border-bottom: 1px solid #f1f5f9; font-size: 0.875rem; } .sku-row:last-child { border-bottom: none; } .sku-name { color: var(--color-text-muted); } .sku-amount { font-weight: 500; } .progress-bar { - height: 8px; + height: 6px; background: var(--color-border); - border-radius: 4px; + border-radius: 3px; overflow: hidden; - margin-top: 0.5rem; + margin-top: 0.75rem; } .progress-fill { height: 100%; - border-radius: 4px; + border-radius: 3px; transition: width 0.5s ease; } .badge-region { @@ -82,6 +114,26 @@ font-size: 0.75rem; font-weight: 600; } + + /* Expand all button */ + .expand-controls { + display: flex; + justify-content: flex-end; + margin-bottom: 1rem; + gap: 0.5rem; + } + .expand-btn { + padding: 0.5rem 1rem; + font-size: 0.8rem; + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + cursor: pointer; + transition: all 0.15s ease; + } + .expand-btn:hover { + background: var(--color-border); + } {% endblock %} @@ -107,11 +159,17 @@ {# ================================================================ - EXECUTIVE SUMMARY ROLL-UP - GUARDRAIL: All values come directly from data.rollup + EXECUTIVE SUMMARY - MOVED TO TOP ================================================================ #} + {% set success_count = data.tenants | selectattr('status', 'equalto', 'success') | list | length %} + {% set error_count = data.tenants | selectattr('status', 'equalto', 'error') | list | length %} + {% set no_invoice_count = data.tenants | selectattr('status', 'equalto', 'no_invoice') | list | length %} + {% set total_tenants = data.tenants | length %} + {% set failed_count = error_count + no_invoice_count %} + {% set completeness_pct = ((success_count / total_tenants) * 100) | round(0) | int if total_tenants > 0 else 0 %} +
-

Executive Summary (Roll-Up)

+

Executive Summary

@@ -122,226 +180,221 @@
- {% if data.rollup.total_cost is defined %} + {% if data.rollup and data.rollup.total_cost is defined %} ${{ data.rollup.total_cost | format_number }} {% else %} N/A {% endif %} - Total Monthly Billing - All tenants combined + Total Billed Amount + {{ success_count }} organization{{ 's' if success_count != 1 else '' }} with successful invoices
- - + +
- - {% if data.rollup.total_sensors is defined %} - {{ data.rollup.total_sensors | format_number }} - {% else %} - N/A - {% endif %} - - Total Sensors - Across {{ data.tenants | length }} organizations + {{ success_count }} + Organizations with Invoices + {{ completeness_pct }}% of total organizations
-
+
- - - - + + +
- - {% if data.rollup.avg_cost_per_sensor is defined %} - ${{ data.rollup.avg_cost_per_sensor | round(2) }} - {% elif data.rollup.total_cost is defined and data.rollup.total_sensors is defined and data.rollup.total_sensors > 0 %} - ${{ (data.rollup.total_cost / data.rollup.total_sensors) | round(2) }} - {% else %} - N/A - {% endif %} - - Avg Cost/Sensor - Blended rate + {{ failed_count }} + Organizations without Invoices + {{ no_invoice_count }} no invoice, {{ error_count }} permission denied
- - - +
- {{ data.tenants | length }} - Active Tenants - With billing data + {{ completeness_pct }}% + Data Completeness + {{ success_count }} of {{ total_tenants }} organizations successfully retrieved
{# ================================================================ - DISTRIBUTION CHARTS + DISTRIBUTION CHARTS - IMMEDIATELY AFTER SUMMARY ================================================================ #} {% if data.tenants and data.tenants | length > 0 %}
-

Cost & Sensor Distribution

+

Distribution Overview

-

Cost Distribution by Tenant

+

Cost Distribution by Organization

-

Sensor Distribution by Tenant

- +

Data Retrieval Status

+
{% endif %} {# ================================================================ - PER-TENANT BREAKDOWN TABLE + WARNINGS AND ERRORS - SHOW EARLY IF PRESENT ================================================================ #} -
-

Per-Tenant Breakdown

- - {% if data.tenants and data.tenants | length > 0 %} -
- - - - - - - - - - - - - - {% for tenant in data.tenants %} - - - - - - - - - - {% endfor %} - - - - - - - - - - - - -
OrganizationRegionSensorsMonthly CostCost/Sensor% of TotalStatus
{{ tenant.name | default('N/A') }} - {% if tenant.region %} - {{ tenant.region }} - {% else %} - N/A - {% endif %} - {{ tenant.sensors | default(0) | format_number }}${{ tenant.cost | default(0) | format_number }} - {% if tenant.sensors and tenant.sensors > 0 and tenant.cost %} - ${{ (tenant.cost / tenant.sensors) | round(2) }} - {% else %} - - - {% endif %} - - {% if data.rollup.total_cost and data.rollup.total_cost > 0 and tenant.cost %} - {{ ((tenant.cost / data.rollup.total_cost) * 100) | round(1) }}% - {% else %} - 0% - {% endif %} - - {% if tenant.status == 'active' %} - Active - {% elif tenant.status == 'draft' %} - Draft - {% elif tenant.cost == 0 or tenant.sensors == 0 %} - No Usage - {% else %} - {{ tenant.status | default('N/A') }} - {% endif %} -
Total ({{ data.tenants | length }} tenants){{ data.rollup.total_sensors | default(0) | format_number }}${{ data.rollup.total_cost | default(0) | format_number }} - {% if data.rollup.avg_cost_per_sensor is defined %} - ${{ data.rollup.avg_cost_per_sensor | round(2) }} - {% endif %} - 100%
-
- {% else %} -
- 📋 -

No tenant billing data available

+ {% if warnings or errors %} +
+

Data Limitations & Warnings

+ + {% if warnings %} +
+ {% for warning in warnings %} +
+ ⚠️ + {{ warning }} +
+ {% endfor %}
{% endif %} + + {% if errors %} +
+ + {{ errors | length }} organization{{ 's' if errors | length != 1 else '' }} without billing access (click to expand) + +
+ {% for error in errors %} +
+ {{ error.org_name | default('Unknown') }} +

{{ error.error_message | default('Permission denied') }}

+
+ {% endfor %} +
+
+ {% endif %}
+ {% endif %} {# ================================================================ - DETAILED SKU BREAKDOWN BY TENANT + PER-TENANT BREAKDOWN - UNIFIED EXPANDABLE VIEW ================================================================ #} {% if data.tenants and data.tenants | length > 0 %}
-

Detailed SKU Breakdown by Tenant

- - {% for tenant in data.tenants %} -
-
-
-
{{ tenant.name }}
-
- {{ tenant.sensors | default(0) }} sensors | {{ tenant.region | default('Unknown') }} region +

Per-Organization Breakdown

+ +
+ + +
+ + {# Sort tenants: successful with costs first, then successful with $0, then errors #} + {% set sorted_tenants = data.tenants | sort(attribute='cost', reverse=true) %} + + {% for tenant in sorted_tenants %} +
+
+
+ + + +
+
+ {{ tenant.name }} + {% if tenant.status == 'error' %} + Permission Denied + {% elif tenant.status == 'no_invoice' %} + No Invoice + {% endif %} +
+
+ {% if tenant.status == 'success' %} + {{ tenant.skus | length if tenant.skus else 0 }} line items + {% if data.rollup and data.rollup.total_cost and data.rollup.total_cost > 0 and tenant.cost %} + | {{ ((tenant.cost / data.rollup.total_cost) * 100) | round(1) }}% of total + {% endif %} + {% elif tenant.status == 'error' %} + Missing billing.ctrl permission + {% else %} + No active billing this period + {% endif %} +
-
- ${{ tenant.cost | default(0) | format_number }} +
+ {% if tenant.status == 'success' %} + ${{ tenant.cost | default(0) | format_number }} + {% else %} + — + {% endif %}
- {% if tenant.skus and tenant.skus | length > 0 %} - {% for sku in tenant.skus %} -
- {{ sku.name }}{% if sku.quantity %} ({{ sku.quantity }}){% endif %} - ${{ sku.amount | format_number }} +
+ {% if tenant.status == 'error' %} +
+ ⚠️ Cannot retrieve billing data - missing billing.ctrl permission
- {% endfor %} - {% elif tenant.cost == 0 %} -
- No active usage - subscription active but 0 sensors deployed -
- {% else %} -
- SKU details not available -
- {% endif %} + {% elif tenant.status == 'no_invoice' %} +
+ No invoice found for this billing period +
+ {% elif tenant.skus and tenant.skus | length > 0 %} + {% for sku in tenant.skus %} +
+ {{ sku.description }}{% if sku.quantity %} ({{ sku.quantity }}){% endif %} + ${{ sku.amount | format_number }} +
+ {% endfor %} -
-
+
+
+
+ {% elif tenant.cost == 0 %} +
+ No billable usage this period +
+ {% else %} +
+ SKU details not available +
+ {% endif %}
{% endfor %} + + {# Summary footer #} +
+ {{ data.tenants | length }} organizations total + + Total: ${{ (data.rollup.total_cost if data.rollup and data.rollup.total_cost else 0) | format_number }} + +
+
+ {% else %} +
+

Per-Organization Breakdown

+
+ 📋 +

No tenant billing data available

+
{% endif %} @@ -357,38 +410,6 @@
{% endif %} - {# ================================================================ - WARNINGS AND ERRORS - ================================================================ #} - {% if warnings or errors %} -
-

Data Limitations & Warnings

- - {% if warnings %} -
- {% for warning in warnings %} -
- ⚠️ - {{ warning }} -
- {% endfor %} -
- {% endif %} - - {% if errors %} -
-

Organizations Without Billing Access

- {% for error in errors %} -
- {{ error.org_name | default('Unknown') }} -

{{ error.error_message | default('Permission denied') }}

-
- {% endfor %} -
- {% endif %} -
- {% endif %} -