.github/skills/svg-dashboard/SKILL.md
Use this skill when asked to generate SVG data visualization dashboards from investigation data or skill reports. Triggers on keywords like "generate SVG dashboard", "create a visual dashboard", "visualize this report", "SVG from the report", "visualize results", "create SVG chart", "SVG from this data". Supports two modes: manifest-driven structured dashboards (from skill reports with svg-widgets.yaml) and freeform adaptive visualizations from ad-hoc investigation data. Component library includes KPI cards, score cards, bar charts, line charts, donut charts, waterfall charts, tables, recommendation cards, assessment banners. SharePoint Dark Theme default palette.
npx skillsauth add scstelz/security-investigator svg-dashboardInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Renders SVG data visualization dashboards — either from a skill's
svg-widgets.yamlmanifest (structured dashboards) or freeform from ad-hoc investigation data in context.
Before rendering, determine which mode applies:
| Condition | Mode | Behavior |
|-----------|------|----------|
| User asks for a dashboard after a skill report AND the calling skill has an svg-widgets.yaml | Manifest Mode | Read the YAML manifest → follow its layout exactly → deterministic dashboard |
| User asks to "visualize", "chart", or "create an SVG" from ad-hoc data in context (query results, investigation findings, inline tables) | Freeform Mode | Select widget types from the Component Library below based on data shape → creative layout |
| No svg-widgets.yaml exists for the current workflow | Freeform Mode | Same as above |
Decision flow:
1. Is there an svg-widgets.yaml for the current skill?
→ YES + user said "dashboard" or "SVG from the report" → Manifest Mode
→ NO → Freeform Mode
2. Does the user have structured data in context (query results, tables, metrics)?
→ YES → Freeform Mode (use data shape to pick widgets)
→ NO → Ask user what data to visualize
Used when a skill provides an svg-widgets.yaml manifest (e.g., mcp-usage-monitoring, sentinel-ingestion-report).
Step 1: Read the calling skill's svg-widgets.yaml (widget manifest)
Step 2: Read this file's Rendering Rules below (component library + quality standards)
Step 3: Read the completed report file (data source)
— If same chat: report data is already in context
— If new chat: read the file path provided by user or find latest in the skill's reports/ subfolder
Step 4: Map manifest fields → report data using data_sources.field_mapping_notes
Step 5: Render SVG → save to the same directory as the report: {report_basename}_dashboard.svg
data_sources.field_mapping_notes to locate values.max_items.unit.Used when no manifest exists or the user wants an ad-hoc visualization from investigation data already in context.
Step 1: Identify the data in context (query results, investigation findings, report sections, inline tables)
Step 2: Analyze data shape — what dimensions, metrics, categories, and time series are present?
Step 3: Read this file's Rendering Rules below (component library + quality standards)
Step 4: Select appropriate widget types from the Component Library (see Data Shape Guide below)
Step 5: Design a layout: title banner → KPI summary → detail charts/tables → optional assessment
Step 6: Render SVG → save to temp/{descriptive_name}_dashboard.svg or user-specified path
| Data Shape | Best Widget | Example |
|------------|-------------|---------|
| Single metrics / counts | kpi-card | Total failed logins: 47, Unique IPs: 12 |
| Metric with period-over-period change | delta-kpi-card | Incidents: 47 (↑23% vs last period) |
| Scored assessment (0-100) | score-card | Risk Score: 73/100 |
| Categorical counts (top-N) | horizontal-bar-chart | Top 10 source IPs by attempt count |
| Composition within categories | stacked-bar-chart | Alert severity breakdown per week |
| Time series (values over dates) | line-chart | Daily sign-in volume over 30 days |
| Proportional breakdown | donut-chart | Auth methods: 60% password, 30% MFA, 10% token |
| Additive/subtractive flow | waterfall-chart | Ingestion costs with license benefits |
| Completion / target tracking | progress-bar | 72% of critical CVEs patched |
| Inline trend in KPI or table cell | sparkline | 7-day mini trend beneath a KPI value |
| Tabular detail rows | table-widget | IP enrichment results, alert details |
| Prioritized action items | recommendation-cards | High/Medium/Low priority findings |
| Executive summary | assessment-banner | Overall risk assessment with key risks/strengths |
| 2D framework coverage (categories × items) | coverage-matrix | MITRE ATT&CK tactic × technique map, permission grids |
| Report header | title-banner | Investigation title, date, scope |
title-banner (data source, date range, scope)Why this matters: SVG is verbose — every
<rect>,<text>, and<path>consumes output tokens. Without limits, freeform dashboards with rich investigation data routinely exceed the model's output token budget, producing truncated/broken SVGs. Manifest-mode dashboards avoid this because the YAMLmax_itemsand fixed row count act as natural constraints.
Hard Limits — Always Enforced:
| Constraint | Limit | Rationale | |-----------|-------|-----------| | Max rows | 6 (including title banner) | Each row adds ~100-200 SVG elements | | Max widgets total | 12 | Beyond this, SVG size balloons past safe output limits | | Max KPI cards per row | 5 | More than 5 become unreadable at standard canvas width | | Max canvas height | 1200px | Forces prioritization; prevents unbounded vertical growth |
Per-Widget Data Limits:
| Widget Type | Max Data Points | What to Do with Excess |
|-------------|----------------|----------------------|
| horizontal-bar-chart | 10 bars | Show top 10, add a summary "Other (N remaining)" bar |
| stacked-bar-chart | 8 bars × 6 segments | Aggregate smaller segments into "Other" |
| line-chart | 30 data points | Resample to weekly if daily exceeds 30; show date range in subtitle |
| donut-chart | 7 segments | Merge smallest into "Other" |
| waterfall-chart | 8 segments | Combine minor items |
| table-widget | 8 rows | Show top 8, add footer "Showing 8 of N" |
| recommendation-cards | 4 cards | Prioritize highest-impact recommendations |
| sparkline | 14 data points | Resample to fit (e.g., daily → every-other-day) |
Data Triage Strategy:
When the data in context exceeds these limits, apply this priority filter:
If the data is too rich for 6 rows / 12 widgets: Tell the user what was included vs omitted, and suggest they request a second dashboard for the remaining data or provide an svg-widgets.yaml manifest for full control.
In freeform mode, you have latitude to:
You are still bound by the Quality Standards and Color & Typography rules below — these ensure visual consistency regardless of mode.
<svg> element with xmlns="http://www.w3.org/2000/svg" and the width/height from the manifest (or chosen dimensions in freeform mode).canvas.background (manifest) or #1b1a19 (freeform default).canvas.padding (manifest) or 40px (freeform default) on all sides. Usable width = width - 2 * padding.canvas.row_gap (manifest) or 24px (freeform default) spacing between rows.width_pct, it gets that percentage of usable width. Otherwise, widgets share remaining space equally.canvas.col_gap (manifest) or 20px (freeform default) for spacing between widgets in the same row.palette.* values from the manifest. In freeform mode, use the default palette below.fill to black — which is invisible on dark backgrounds. Every <text> element MUST have an explicit fill attribute. Set fill="{palette.text_primary}" on the root <svg> or a top-level <g> so all text inherits white by default. Never rely on SVG's implicit black fill.canvas.font_family (manifest) or Segoe UI, sans-serif (freeform default).palette.primary or widget's highlight_color.palette.text_secondary.palette.text_primary.palette.text_secondary.palette.text_primary. Never place value labels inside bars — always position them after/outside the bar.palette in svg-widgets.yaml.palette:
background: "#0d1117"
card_bg: "#161b22"
primary: "#409AE1" # Blue — KPI highlights
secondary: "#b4a0ff" # Purple — secondary charts
success: "#40C5AF" # Teal-green — healthy metrics
warning: "#ff8c00" # Orange — moderate risk
danger: "#EF6950" # Red — critical findings
text_primary: "#e6edf3"
text_secondary: "#b2b2b2"
accent: "#FFC83D" # Yellow — warnings, anomalies
grid_line: "#30363d"
title-bannerFull-width banner. Render the title large and centered horizontally on the canvas, subtitle fields centered below on the same line separated by " · ". Optional accent underline. Use text-anchor="middle" with x at canvas midpoint. If the manifest specifies title_align: left, left-align instead — but the default is always center.
kpi-cardRounded rectangle (rx="12"). Show the value large and centered, label below in small text, optional unit suffix. Color the value with highlight_color if specified, otherwise palette.primary. No actual icon rendering needed — use a colored dot or small indicator instead.
delta-kpi-cardExtends kpi-card with a period-over-period change indicator. Render the primary value the same as kpi-card. Below (or beside) the value, show a delta line: an arrow (▲ or ▼) followed by the percentage or absolute change. Color the delta with palette.success for favorable changes and palette.danger for unfavorable changes. If invert_color is true, reverse the color logic (e.g., for metrics where "down" is good, like error rate). Show the comparison period label in palette.text_secondary at 10px (e.g., "vs prior 7d"). If no delta data is available, render as a standard kpi-card with no delta line.
score-cardRounded rectangle card (rx="12") with card_bg background. Render the numeric score value large and centered (bold, 42-48px), colored by whichever range it falls into (from the widget's ranges array). Below the number, show the rating label (e.g., "CONCERNING") in 14px bold, same color as the number. Above both, render the widget title in 14-16px bold, palette.text_primary. Add a subtle /100 suffix after the score in smaller muted text (18px, palette.text_secondary). Keep it visually clean — no gauge arcs, needles, or scale markers.
stacked-bar-chartVertical or horizontal bars where each bar is subdivided into colored segments representing categories (e.g., severity levels, sources, status). Include a legend mapping segment colors to category names. If orientation: horizontal, render left-to-right stacked rows with labels on the left. If orientation: vertical (default), render bottom-to-top stacked columns with labels on the x-axis. Show segment values on hover via <title> elements. If show_totals is true, display the total above each bar. Use segment_colors from the manifest or assign from palette automatically.
horizontal-bar-chartHorizontal bars sorted by value descending. Layout per row (left to right): label → optional inline badges → bar (proportional to max value) → value label → optional extra column (rightmost). Value labels MUST be positioned outside (after) the bar, never inside it — use fill="{palette.text_primary}" (white on dark themes). Append value_suffix if specified. If show_rule_count: right, render the rule count as the rightmost column, right-aligned. If a value is 0, render it in palette.danger. If show_tier_badge is true, render a small colored badge after each label using colors from the YAML segments or badge_colors definitions. If bar_color_by: severity is set, color bars by severity level. If show_error_overlay is true, render a red overlay segment proportional to failure count. If highlight_sensitive is true, mark flagged items with a warning indicator.
line-chartSVG <polyline> or <path> for the trend line with optional area fill (fill_opacity). X-axis = dates, Y-axis = values. Render annotations as labeled markers: peak (triangle up), low (triangle down), average (dashed horizontal line). Grid lines at sensible intervals. If show_weekday_pattern is true, add subtle mini-bars along the bottom showing day-of-week averages.
donut-chartRender using SVG <circle> elements with stroke-dasharray/stroke-dashoffset. Use this exact formula — do not iterate or try alternative approaches:
circumference = 2 * π * radius (e.g., radius=70 → C ≈ 439.82)
For each segment i (ordered by value descending):
arc_len_i = (value_i / total) * circumference
start_i = sum of all previous arc_lens (0 for first segment)
dasharray = "arc_len_i, (circumference - arc_len_i)"
dashoffset = circumference - start_i
transform = "rotate(-90, cx, cy)" ← starts at 12 o'clock
Each segment is a <circle cx cy r> with fill="none", stroke="{segment_color}", stroke-width="20". Stack all circles at the same position — the dasharray/dashoffset combination makes each one draw only its arc portion. Add <title> tooltips.
Legend to the right or below. If show_center_total is true, display the total count in the donut center. If compact is true, reduce the donut radius and legend font size to fit alongside a stacked widget below.
waterfall-chartStacked/cascading vertical bars: each segment starts where the previous ended. Negative segments (benefits) flow downward. Show values on each bar. Final bar shows net total.
progress-barHorizontal bar showing completion percentage against a target. Render a rounded track (rx="6") in palette.grid_line (or card_bg), filled proportionally with palette.primary (or bar_color if specified). Show the percentage value (bold, 18-22px) to the right of the bar or centered inside the filled portion. Label text above or to the left in palette.text_primary at 12-14px. If target_label is provided, show it at the 100% mark in palette.text_secondary. If thresholds are defined (e.g., [{"at": 90, "color": "success"}, {"at": 50, "color": "warning"}, {"at": 0, "color": "danger"}]), color the fill bar according to which threshold the value meets. If show_remaining is true, display the remaining percentage in muted text after the bar.
sparklineMiniature trend line — a compact <polyline> rendered inline within a kpi-card, delta-kpi-card, or table-widget cell. Dimensions: typically 60-100px wide × 16-24px tall. No axes, labels, or grid lines — just the trend shape. Stroke width 1.5-2px in palette.primary (or line_color if specified). Optional: fill the area below with the same color at 10-15% opacity. If show_endpoints is true, render small circles (r=2) at the first and last data points. If the last value is higher than the first, color the line palette.success; if lower, palette.danger; if auto_color: false, use the specified line_color instead.
table-widgetRows of data with alternating row backgrounds (card_bg and slightly lighter). Column headers in text_secondary. If color_scale is true for a column, color positive values red and negative green (or vice versa for cost savings). If badge is true, render small severity badges. If highlight_zero is true for a column, render zero values in palette.danger color. If summary_row is specified, add a totals/summary row at the bottom with a top border separator. If stack_below is specified, this widget shares the same column as the named widget above it — render it directly below that widget rather than side-by-side.
recommendation-cardsSide-by-side rounded cards. Left border colored by priority (card_colors). Title bold, description in text_secondary. If show_impact_estimate, add a small impact line.
assessment-bannerLarge panel with a colored left border. Title + main assessment text. Sub-fields rendered as bullet lists (key_risks in palette.danger, strengths in palette.success).
coverage-matrixCompact grid visualization for displaying coverage status across a two-dimensional framework (e.g., MITRE ATT&CK tactics × techniques, permission matrices, data readiness grids). Renders as a grid of small colored <rect> cells organized into columns, where each column represents a category (e.g., tactic) and each cell represents an item (e.g., technique) within that category.
Layout: Columns are arranged left-to-right. Each column has a rotated header label at the top (45° angle, 10-11px text) and a vertical stack of cells below. Columns are variable-height — each has as many cells as items in that category. A legend bar is rendered below the grid mapping colors to status labels.
Cell rendering: Each cell is a small <rect> (default cell_size: 12 × 12px, cell_gap: 2px between cells). Cells are colored according to their status field using the status_colors map from the manifest. Cells within each column are sorted by status priority (covered items at top, uncovered at bottom) to create a visible "waterline" effect. Each cell has a <title> element containing the item name and status for hover tooltips — this is essential since cell text is not rendered at this scale.
Column rendering: Each column is cell_size + cell_gap wide. Columns are separated by col_gap (default 6px). Column header text is right-rotated and positioned above the first cell. An optional column footer shows the count or percentage (e.g., "5/11" or "45%") in 9px text below the last cell.
Legend: Horizontal bar below the grid with colored squares and labels for each status. Rendered in a single row, 10px text, using the status_colors map.
Manifest fields:
| Field | Required | Description |
|-------|----------|-------------|
| field | ✅ | Data source — array of {column, items: [{name, status}]} objects |
| status_colors | ✅ | Map of status label → hex color (e.g., custom_rule: "#409AE1", tier_1: "#40C5AF", uncovered: "#21262d") |
| cell_size | ❌ | Cell width and height in px (default: 12) |
| cell_gap | ❌ | Gap between cells in px (default: 2) |
| col_gap | ❌ | Gap between columns in px (default: 6) |
| show_col_footer | ❌ | Show count/percentage below each column (default: true) |
| sort_order | ❌ | Array of status labels defining top-to-bottom cell sort order (covered statuses first) |
| max_rows | ❌ | Cap the tallest column at this many cells; excess items are collapsed into a single "+" cell with count in tooltip |
Token budget: This widget is compact by design — 250 cells ≈ 250 <rect> elements (~15KB SVG). No text per cell keeps it efficient. The primary token cost is the <title> tooltip content. For grids exceeding 300 items, set max_rows to cap column height and keep SVG size manageable.
Example use cases: MITRE ATT&CK tactic × technique coverage map, data source × table readiness grid, permission scope × application access matrix, compliance framework × control status.
rx="8" to rx="12") on all cards and panels.<title> elements on interactive-looking elements for accessibility.&, <, etc.).<!-- Generated by Copilot SVG Dashboard Generator --> comment at the top.{report_basename}_dashboard.svgtemp/{descriptive_name}_dashboard.svg or a user-specified pathdevelopment
Use this skill when asked to investigate a computer, device, endpoint, or machine for security issues, suspicious activity, malware, or compliance review. Triggers on keywords like "investigate computer", "investigate device", "investigate endpoint", "check machine", "device security", "endpoint investigation", or when a device name/hostname is mentioned with investigation context. This skill provides comprehensive device security analysis including Defender alerts, sign-in patterns, logged-on users, vulnerabilities, software inventory, compliance status, network activity, and automated investigation tracking for Entra Joined, Hybrid Joined, and Entra Registered devices.
development
Recommended starting point for new users and daily SOC operations. Quick 15-minute security posture scan across 7 domains: active incidents, identity (human + NonHuman), endpoint, email threats, admin & cloud ops, and exposure. 12 queries executed in parallel batches, producing a prioritized Threat Pulse Dashboard with color-coded verdicts (🔴 Escalate / 🟠 Investigate / 🟡 Monitor / ✅ Clear) and drill-down recommendations pointing to specialized skills. Trigger on getting-started questions like "what can you do", "where do I start", "help me investigate". Supports inline chat and markdown file output
development
Use this skill when asked to investigate a user account for security issues, suspicious activity, or compliance review. Triggers on keywords like "investigate user", "security investigation", "user investigation", "check user activity", "analyze sign-ins", or when a UPN/email is mentioned with investigation context. This skill provides comprehensive Entra ID user security analysis including sign-in anomalies, MFA status, device compliance, audit logs, security incidents, Identity Protection risk, and automated reports (HTML, markdown file, or inline chat).
development
Sentinel Ingestion Report — YAML-driven PowerShell pipeline gathers all data via az monitor/az rest/Graph API, writes a deterministic scratchpad, LLM renders the report. Covers table-level volume breakdown, tier classification (Analytics/Basic/Data Lake), SecurityEvent/Syslog/CommonSecurityLog deep dives, ingestion anomaly detection (24h and WoW), analytic rule inventory via REST API, rule health via SentinelHealth, detection coverage cross-reference, tier migration candidates with DL-eligibility lookup, license benefit analysis (DfS P2 500MB/server/day, M365 E5 data grant). Inline chat and markdown file output.