Skill: chunkin616-hue/kibana-saved-objects (version 1.0.0)

--- name: kibana-saved-objects description: "Manage Kibana Saved Objects (dashboards, lens, visualizations) via REST API. Use when: (1) Creating/updating/deleting dashboards and Lens visualizations programmatically, (2) Importing/exporting saved objects via NDJSON, (3) Bulk creating/updating objects, (4) Managing references between objects, (5) Troubleshooting corrupted saved objects. Key operations: create, update, delete, bulk_create, bulk_get, export, import_objects." --- # Kibana Saved Objects API Skill Manage Kibana Saved Objects including dashboards, Lens visualizations, and other Kibana entities via REST API. ## Base URL ``` http://<kibana-host>/api/saved_objects ``` For Omni-Monitor: `http://192.168.99.43/api/saved_objects` **Required Header:** `kbn-xsrf: true` --- ## Core Endpoints ### 1. Create Saved Object ``` POST /api/saved_objects/<type> ``` **Body:** ```json { "attributes": { "title": "My Dashboard", "description": "..." }, "id": "optional-custom-id", "references": [{"id": "...", "name": "...", "type": "index-pattern"}] } ``` ### 2. Get Saved Object ``` GET /api/saved_objects/<type>/<id> ``` ### 3. Update Saved Object ``` PUT /api/saved_objects/<type>/<id> ``` **Body:** ```json { "attributes": { "title": "Updated Title" }, "references": [...] } ``` ### 4. Delete Saved Object ``` DELETE /api/saved_objects/<type>/<id> ``` ### 5. Bulk Get ``` POST /api/saved_objects/_bulk_get ``` **Body:** ```json { "objects": [ {"type": "dashboard", "id": "abc123"}, {"type": "lens", "id": "def456"} ] } ``` ### 6. Bulk Create ``` POST /api/saved_objects/_bulk_create ``` **Body:** ```json { "objects": [ {"type": "dashboard", "id": "...", "attributes": {...}}, {"type": "lens", "id": "...", "attributes": {...}} ] } ``` ### 7. Export Saved Objects ``` POST /api/saved_objects/_export ``` **Body:** ```json { "objects": [ {"type": "dashboard", "id": "abc123"}, {"type": "lens", "id": "def456"} ] } ``` **Returns:** NDJSON stream of saved objects ### 8. Import Saved Objects ``` POST /api/saved_objects/_import?overwrite=true ``` **Content-Type:** `multipart/form-data` **Form field:** `file` = NDJSON file ### 9. Find (Search) Saved Objects ``` POST /api/saved_objects/_find ``` **Body:** ```json { "type": "dashboard", "search": "title keyword", "searchFields": ["title", "description"], "page": 1, "perPage": 20 } ``` --- ## Common Object Types | Type | Description | |------|-------------| | `dashboard` | Kibana dashboards | | `lens` | Lens visualizations | | `visualization` | Classic visualizations (TSVB, etc.) | | `index-pattern` | Data views (index patterns) | | `search` | Saved searches | | `map` | Map visualizations | | `tag` | Tags for organizing | | `canvas-workpad` | Canvas workpads | --- ## References System References link objects together. For example, a dashboard panel (lens) references: 1. The `index-pattern` (data view) it uses 2. Any parent dashboard it belongs to **Reference format:** ```json { "id": [REDACTED_SECRET], "name": [REDACTED_SECRET], "type": "index-pattern" } ``` **Creating with references:** ```json { "type": "lens", "attributes": { "title": "CPU Usage", "state": { "datasourceStates": {...}, "visualization": {...} } }, "references": [ {"id": [REDACTED_SECRET], "name": [REDACTED_SECRET], "type": "index-pattern"} ] } ``` --- ## Dashboard Structure **Dashboard attributes:** ```json { "title": "Dashboard Name", "description": "Optional description", "panelsJSON": "[{\"type\":\"lens\",\"gridData\":{...},\"embeddableConfig\":{...}}]", "optionsJSON": "{\"useMargins\":true,\"syncColors\":false,\"hidePanelTitles\":false}", "timeRestore": true, "timeTo": "now", "timeFrom": "now-24h", "refreshInterval": { "pause": false, "value": 300000 } } ``` --- ## Lens Object Structure Lens is Kibana's next-generation visualization. Key structure: ```json { "type": "lens", "attributes": { "title": "Metric Title", "state": { "datasourceStates": { "formBased": { "layers": { "<layer-uuid>": { "columns": { "<accessor-uuid>": { "label": "Metric Label", "dataType": "number", "operationType": "count|average|sum|max|min", "sourceField": "field.name", "isBucketed": false, "scale": "ratio" } }, "columnOrder": ["<accessor-uuid>"], "incompleteColumns": {} } } } }, "visualization": { "layerId": "<layer-uuid>", "layerType": "data", "metricAccessor": "<accessor-uuid>" } } }, "references": [ {"id": "<index-pattern-id>", "name": "indexpattern-datasource-layer-<layer-uuid>", "type": "index-pattern"} ] } ``` **lnsMetric (single metric):** ```json { "layerId": "...", "layerType": "data", "metricAccessor": "<accessor-uuid>" } ``` **lnsXY (line/bar/area chart):** ```json { "layerId": "...", "layerType": "data", "layers": [{ "layerId": "...", "accessors": ["<accessor-uuid>"], "position": "top", "seriesType": "bar|line|area", "showGridlines": false, "xAccessor": "<accessor-uuid>" }] } ``` --- ## Important Rules 1. **Never use `__records___` as sourceField** → causes "Field not found" errors 2. **Text fields must use `.keyword` suffix** for aggregations (e.g., `host.keyword`) 3. **References must use correct format** → `indexpattern-datasource-layer-<layer-uuid>` 4. **Lens layerId and accessor UUIDs must be unique** per visualization --- ## NDJSON Import/Export Format **Export produces:** ``` {"attributes":{"title":"Dash 1"...},"id":"abc","type":"dashboard","references":[...]} {"attributes":{"title":"Lens 1"...},"id":"def","type":"lens","references":[...]} ``` **Import:** Same format, `overwrite=true` replaces existing objects. --- ## Scripts | Script | Purpose | |--------|---------| | `list_dashboards.py` | List all dashboards | | `export_dashboard.py` | Export a dashboard and its dependencies | | `import_ndjson.py` | Import NDJSON file to Kibana | | `create_lens_metric.py` | Create a simple metric Lens visualization | --- ## See Also - [kibana-data-views](../kibana-data-views/SKILL.md) - Manage data views (index patterns) - [kibana-lens-builder](../kibana-lens-builder/SKILL.md) - Generate Lens visualizations for monitoring --- ## Troubleshooting ### "Cannot read properties of undefined (reading 'indexpattern')" **Cause:** Dashboard/lens references a non-existent data view. **Fix:** 1. List data views: `GET /api/data_views` 2. Verify referenced data view ID exists 3. Recreate missing data views with [kibana-data-views](../kibana-data-views/SKILL.md) ### "Field not found" errors **Cause:** Using text field without `.keyword` suffix. **Fix:** Use `host.keyword` not `host`, `metric_type.keyword` not `metric_type` ### Corrupted saved objects (404 on GET but appears in list) **Fix:** 1. Delete the corrupted object: `DELETE /api/saved_objects/<type>/<id>` 2. Recreate with proper data

path: scripts/create_lens_metric.py
content: #!/usr/bin/env python3
"""
Create a simple Lens metric visualization via Saved Objects API.
Usage: python3 create_lens_metric.py [kibana-url] [title] [data-view-id] [source-field]
"""

import json
import sys
import urllib.request
import uuid

DEFAULT_KIBANA = "http://192.168.99.43"

def create_lens_metric(kibana_url, title, data_view_id, source_field, operation="count"):
    """Create a simple Lens metric visualization."""
    
    # Generate UUIDs
    layer_id = str(uuid.uuid4())
    accessor_id = str(uuid.uuid4())
    
    # Build the Lens state
    state = {
        "datasourceStates": {
            "formBased": {
                "layers": {
                    layer_id: {
                        "columns": {
                            accessor_id: {
                                "label": title,
                                "dataType": "number",
                                "operationType": operation,
                                "sourceField": source_field,
                                "isBucketed": False,
                                "scale": "ratio",
                                "params": {"emptyAsNull": True}
                            }
                        },
                        "columnOrder": [accessor_id],
                        "incompleteColumns": {}
                    }
                }
            },
            "indexpattern": {"layers": {}},
            "textBased": {"layers": {}}
        },
        "visualization": {
            "layerId": layer_id,
            "layerType": "data",
            "metricAccessor": accessor_id
        }
    }
    
    # Build the attributes
    attributes = {
        "title": title,
        "description": "",
        "state": json.dumps(state),
        " visualizationType": "lnsMetric"
    }
    
    # Build the saved object
    obj = {
        "type": "lens",
        "attributes": attributes,
        "references": [
            {
                "id": data_view_id,
                "name": f"indexpattern-datasource-layer-{layer_id}",
                "type": "index-pattern"
            }
        ]
    }
    
    # Create the lens
    url = f"{kibana_url}/api/saved_objects/lens"
    
    data = json.dumps(obj).encode("utf-8")
    req = urllib.request.Request(
        url, data=data,
        headers={"kbn-xsrf": "true", "Content-Type": "application/json"},
        method="POST"
    )
    
    try:
        with urllib.request.urlopen(req, timeout=15) as response:
            result = json.loads(response.read().decode())
            return {"success": True, "id": result.get("id"), "type": result.get("type")}
    except urllib.error.HTTPError as e:
        error_body = e.read().decode() if e.fp else ""
        try:
            error_json = json.loads(error_body)
            return {"success": False, "error": error_json.get("message", error_body)}
        except:
            return {"success": False, "error": error_body}
    except Exception as e:
        return {"success": False, "error": str(e)}

def main():
    args = sys.argv[1:]
    
    if len(args) < 4:
        print("Usage: python3 create_lens_metric.py <kibana-url> <title> <data-view-id> <source-field> [operation]")
        print(f"Example: python3 create_lens_metric.py {DEFAULT_KIBANA} 'Total Hosts' b58d25c5-c05c-47be-a6cb-4073ef478a8f 'hostid.keyword' count")
        sys.exit(1)
    
    kibana_url = args[0]
    title = args[1]
    data_view_id = args[2]
    source_field = args[3]
    operation = args[4] if len(args) > 4 else "count"
    
    print(f"Creating Lens metric:")
    print(f"  Title: {title}")
    print(f"  Data View ID: {data_view_id}")
    print(f"  Source Field: {source_field}")
    print(f"  Operation: {operation}")
    print("-" * 50)
    
    result = create_lens_metric(kibana_url, title, data_view_id, source_field, operation)
    
    if result["success"]:
        print(f"✅ Lens created!")
        print(f"   ID: {result['id']}")
        print(f"   Type: {result['type']}")
    else:
        print(f"❌ Failed: {result['error']}")
        sys.exit(1)

if __name__ == "__main__":
    main()

path: scripts/export_dashboard.py
content: #!/usr/bin/env python3
"""
Export a dashboard and its dependencies via Saved Objects API.
Usage: python3 export_dashboard.py [kibana-url] [dashboard-id]
"""

import json
import sys
import urllib.request

DEFAULT_KIBANA = "http://192.168.99.43"

def export_objects(kibana_url, objects):
    """Export saved objects as NDJSON."""
    url = f"{kibana_url}/api/saved_objects/_export"
    
    data = json.dumps({"objects": objects}).encode("utf-8")
    
    req = urllib.request.Request(
        url, data=data,
        headers={"kbn-xsrf": "true", "Content-Type": "application/json"},
        method="POST"
    )
    
    try:
        with urllib.request.urlopen(req, timeout=30) as response:
            # Response is NDJSON stream
            result = []
            for line in response:
                line = line.decode().strip()
                if line:
                    result.append(json.loads(line))
            return result
    except urllib.error.HTTPError as e:
        error_body = e.read().decode() if e.fp else ""
        print(f"HTTP Error {e.code}: {error_body}", file=sys.stderr)
        return None
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        return None

def main():
    if len(sys.argv) < 3:
        print("Usage: python3 export_dashboard.py <kibana-url> <dashboard-id>")
        print(f"Example: python3 export_dashboard.py {DEFAULT_KIBANA} abc123")
        sys.exit(1)
    
    kibana_url = sys.argv[1]
    dashboard_id = sys.argv[2]
    
    print(f"Exporting dashboard: {dashboard_id}")
    print("-" * 50)
    
    # First, get the dashboard to find its references
    get_url = f"{kibana_url}/api/saved_objects/dashboard/{dashboard_id}"
    req = urllib.request.Request(get_url, headers={"kbn-xsrf": "true"})
    
    try:
        with urllib.request.urlopen(req, timeout=15) as response:
            dash = json.loads(response.read().decode())
            attrs = dash.get("dashboard", {}).get("attributes", {})
            print(f"Title: {attrs.get('title', 'N/A')}")
            
            # Get all references
            references = dash.get("references", [])
            print(f"References: {len(references)}")
            
            for ref in references:
                print(f"  - {ref.get('type')}: {ref.get('id')}")
            
    except urllib.error.HTTPError as e:
        print(f"Dashboard not found: {e.code}")
        sys.exit(1)
    
    # Now export the dashboard and its references
    objects_to_export = [{"type": "dashboard", "id": dashboard_id}]
    for ref in references:
        objects_to_export.append({"type": ref.get("type"), "id": ref.get("id")})
    
    print("\nExporting objects...")
    results = export_objects(kibana_url, objects_to_export)
    
    if results is None:
        print("Export failed")
        sys.exit(1)
    
    print(f"\nExported {len(results)} object(s):")
    for obj in results:
        print(f"  - {obj.get('type')}: {obj.get('id')} ({obj.get('attributes', {}).get('title', 'N/A')})")
    
    # Save to file
    output_file = f"/tmp/dashboard_export_{dashboard_id}.ndjson"
    with open(output_file, "w") as f:
        for obj in results:
            f.write(json.dumps(obj) + "\n")
    
    print(f"\n✅ Saved to: {output_file}")

if __name__ == "__main__":
    main()

path: scripts/import_ndjson.py
content: #!/usr/bin/env python3
"""
Import NDJSON file to Kibana via Saved Objects API.
Usage: python3 import_ndjson.py [kibana-url] [ndjson-file] [--overwrite]
"""

import json
import sys
import urllib.request
import os

DEFAULT_KIBANA = "http://192.168.99.43"

def import_ndjson(kibana_url, file_path, overwrite=True):
    """Import NDJSON file to Kibana."""
    url = f"{kibana_url}/api/saved_objects/_import?overwrite={str(overwrite).lower()}"
    
    # Read the NDJSON file
    with open(file_path, "rb") as f:
        ndjson_data = f.read()
    
    # Create multipart form data
    boundary = [REDACTED_SECRET]
    
    body = f"--{boundary}\r\n"
    body += 'Content-Disposition: form-data; name="file"; filename="import.ndjson"\r\n'
    body += "Content-Type: application/x-ndjson\r\n\r\n"
    body = body.encode("utf-8") + ndjson_data + b"\r\n"
    body += f"--{boundary}--\r\n"
    
    req = urllib.request.Request(
        url, data=body,
        headers={
            "kbn-xsrf": "true",
            "Content-Type": f"multipart/form-data; boundary={boundary}"
        },
        method="POST"
    )
    
    try:
        with urllib.request.urlopen(req, timeout=60) as response:
            result = json.loads(response.read().decode())
            return result
    except urllib.error.HTTPError as e:
        error_body = e.read().decode() if e.fp else ""
        try:
            return json.loads(error_body)
        except:
            return {"success": False, "error": error_body}
    except Exception as e:
        return {"success": False, "error": str(e)}

def main():
    if len(sys.argv) < 3:
        print("Usage: python3 import_ndjson.py <kibana-url> <ndjson-file> [--overwrite]")
        print(f"Example: python3 import_ndjson.py {DEFAULT_KIBANA} /tmp/dashboard.ndjson --overwrite")
        sys.exit(1)
    
    kibana_url = sys.argv[1]
    file_path = sys.argv[2]
    overwrite = "--overwrite" in sys.argv
    
    if not os.path.exists(file_path):
        print(f"❌ File not found: {file_path}")
        sys.exit(1)
    
    print(f"Importing: {file_path}")
    print(f"Overwrite: {overwrite}")
    print("-" * 50)
    
    result = import_ndjson(kibana_url, file_path, overwrite)
    
    if result.get("success"):
        print("✅ Import successful!")
        
        # Report results
        if "results" in result:
            created = result["results"].get("created", [])
            updated = result["results"].get("updated", [])
            conflict = result["results"].get("conflict", [])
            
            print(f"  Created: {len(created)}")
            print(f"  Updated: {len(updated)}")
            print(f"  Conflicts: {len(conflict)}")
    else:
        print(f"❌ Import failed: {result.get('error', result.get('message', 'Unknown error'))}")
        sys.exit(1)

if __name__ == "__main__":
    main()

path: scripts/list_dashboards.py
content: #!/usr/bin/env python3
"""
List all Kibana dashboards via Saved Objects API.
Usage: python3 list_dashboards.py [kibana-url]
"""

import json
import sys
import urllib.request

DEFAULT_KIBANA = "http://192.168.99.43"

def list_dashboards(kibana_url):
    """List all dashboard saved objects."""
    url = f"{kibana_url}/api/saved_objects/_find"
    
    data = json.dumps({
        "type": "dashboard",
        "page": 1,
        "perPage": 100
    }).encode("utf-8")
    
    req = urllib.request.Request(
        url, data=data,
        headers={"kbn-xsrf": "true", "Content-Type": "application/json"},
        method="POST"
    )
    
    try:
        with urllib.request.urlopen(req, timeout=15) as response:
            return json.loads(response.read().decode())
    except urllib.error.HTTPError as e:
        error_body = e.read().decode() if e.fp else ""
        print(f"HTTP Error {e.code}: {error_body}", file=sys.stderr)
        return None
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        return None

def main():
    kibana_url = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_KIBANA
    
    print(f"Connecting to: {kibana_url}")
    print("-" * 50)
    
    result = list_dashboards(kibana_url)
    
    if result is None:
        print("Failed to retrieve dashboards")
        sys.exit(1)
    
    dashboards = result.get("saved_objects", [])
    total = result.get("total", 0)
    
    print(f"Found {total} dashboard(s):\n")
    
    for dash in dashboards:
        attrs = dash.get("attributes", {})
        print(f"  ID: {dash.get('id')}")
        print(f"  Title: {attrs.get('title', 'N/A')}")
        print(f"  Description: {attrs.get('description', 'N/A')}")
        print()

if __name__ == "__main__":
    main()

Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)