Claude
Skills
Sign in
Back

init-cases-to-slack

Included with Lifetime
$97 forever

Set up automated Slack notifications for LimaCharlie case events. Creates a Python playbook, D&R rules, API key, and secrets so that case creations, resolutions, severity upgrades, and closures post rich messages to a Slack channel. Usage - /init-cases-to-slack <org_name>

Backend & APIs

What this skill does


# Initialize Cases-to-Slack Integration

Set up automated Slack notifications for LimaCharlie case management events. This skill creates all required components in a single org: a Python playbook, D&R trigger rules, an API key, and secrets for the Slack token and channel.

---

## LimaCharlie Integration

> **Prerequisites**: Run `/init-lc` to initialize LimaCharlie context.

### LimaCharlie CLI Access

All LimaCharlie operations use the `limacharlie` CLI directly:

```bash
limacharlie <noun> <verb> --oid <oid> --output yaml [flags]
```

For command help and discovery: `limacharlie <command> --ai-help`

### Critical Rules

| Rule | Wrong | Right |
|------|-------|-------|
| **CLI Access** | Call MCP tools or spawn api-executor | Use `Bash("limacharlie ...")` directly |
| **Output Format** | `--output json` | `--output yaml` (more token-efficient) |
| **Filter Output** | Pipe to jq/yq | Use `--filter JMESPATH` to select fields |
| **OID** | Use org name | Use UUID (call `limacharlie org list` if needed) |

---

## What Gets Created

| Component | Hive | Key | Purpose |
|-----------|------|-----|---------|
| Playbook | `playbook` | `cases-to-slack` | Python code that posts rich Slack messages |
| D&R Rule | `dr-general` | `cases-to-slack-on-created` | Triggers playbook on case creation |
| D&R Rule | `dr-general` | `cases-to-slack-on-resolved` | Triggers playbook on case resolution |
| D&R Rule | `dr-general` | `cases-to-slack-on-severity-upgraded` | Triggers playbook on severity upgrade |
| D&R Rule | `dr-general` | `cases-to-slack-on-closed` | Triggers playbook on case closure |
| Secret | `secret` | `slack-api-token` | Slack Bot OAuth token |
| Secret | `secret` | `slack-notification-channel` | Slack channel ID for notifications |
| Secret | `secret` | `cases-to-slack-api-key` | LC API key for playbook SDK auth |
| API Key | — | `cases-to-slack` | Scoped key with `secret.get` permission |

## Procedure

### Step 1: Resolve the Organization

Parse the org name from the skill argument. Resolve it to an OID:

```bash
limacharlie org list --output yaml
```

Find the org matching the name provided by the user and extract its `oid` UUID. If not found, ask the user which org to use.

### Step 2: Verify Prerequisites

Check that the required extensions are subscribed:

```bash
limacharlie extension list --oid <oid> --output yaml
```

Look for `ext-cases` and `ext-playbook` in the output. If either is missing, subscribe:

```bash
limacharlie extension subscribe --name ext-cases --oid <oid>
limacharlie extension subscribe --name ext-playbook --oid <oid>
```

### Step 3: Collect Slack Configuration from User

Ask the user for TWO values:

1. **Slack Bot Token** — starts with `xoxb-`. The bot must have the `chat:write` scope and be invited to the target channel.
2. **Slack Channel ID** — the channel ID (e.g., `C0123456789`), NOT the channel name. The user can find this by right-clicking the channel in Slack → "View channel details" → the ID is at the bottom.

### Step 4: Store Slack Secrets

Store the Slack bot token:

```bash
echo '{"data": {"secret": "<SLACK_BOT_TOKEN>"}, "usr_mtd": {"enabled": true}}' \
  | limacharlie hive set --hive-name secret --key slack-api-token --oid <oid>
```

Store the Slack channel ID:

```bash
echo '{"data": {"secret": "<SLACK_CHANNEL_ID>"}, "usr_mtd": {"enabled": true}}' \
  | limacharlie hive set --hive-name secret --key slack-notification-channel --oid <oid>
```

### Step 5: Create the Playbook API Key

Create an LC API key with minimal permissions for the playbook to read secrets, and atomically store its value as a secret with `--store-secret`. The secret name must be `cases-to-slack-api-key` (what the playbook reads); `--store-secret` creates it **enabled**:

```bash
limacharlie api-key create \
  --name "cases-to-slack" \
  --permissions "secret.get,investigation.get" \
  --store-secret cases-to-slack-api-key \
  --oid <oid> \
  --output yaml
```

`--store-secret` writes the key value to `hive://secret/cases-to-slack-api-key` created enabled (updated via etag if it already exists), so the value never has to be captured manually.

**Fallback (manual two-step)**: if `--store-secret` is unavailable, capture the `api_key` value from the output and store it as a secret with `enabled: true`:

```bash
echo '{"data": {"secret": "<API_KEY_VALUE>"}, "usr_mtd": {"enabled": true}}' \
  | limacharlie hive set --hive-name secret --key cases-to-slack-api-key --oid <oid>
```

### Step 6: Create the Playbook

Write the playbook Python code to a temp file, then push it as a hive record.

The playbook code:

```bash
cat > /tmp/cases-to-slack.py << 'PYEOF'
import json
import urllib.request
from limacharlie import Hive


_CASES_API = "https://cases.limacharlie.io"
_WEB_UI = "https://app.limacharlie.io"

_SEVERITY_COLORS = {
    "critical": "#E01E5A",
    "high": "#E87722",
    "medium": "#ECB22E",
    "low": "#2EB67D",
    "info": "#36C5F0",
}

_SEVERITY_EMOJIS = {
    "critical": ":rotating_light:",
    "high": ":fire:",
    "medium": ":warning:",
    "low": ":large_blue_circle:",
    "info": ":information_source:",
}


def _api_get(sdk, path):
    """GET request to the ext-cases REST API using the SDK auth."""
    try:
        oid = sdk._oid
        return sdk._apiCall(
            path.lstrip('/'),
            'GET',
            altRoot=_CASES_API,
            queryParams={"oid": oid}
        )
    except Exception:
        return None


def _fetch_case(sdk, case_number):
    """Fetch full case details from ext-cases API."""
    data = _api_get(sdk, f"/api/v1/cases/{case_number}")
    return data.get("case") if data else None


def _fetch_detections(sdk, case_number):
    """Fetch detections linked to the case."""
    data = _api_get(sdk, f"/api/v1/cases/{case_number}/detections")
    return data.get("detections", []) if data else []


def _format_duration(seconds):
    """Format seconds into a human-readable duration."""
    if not seconds:
        return None
    if seconds < 60:
        return f"{seconds}s"
    if seconds < 3600:
        return f"{seconds // 60}m"
    hours = seconds // 3600
    mins = (seconds % 3600) // 60
    return f"{hours}h {mins}m" if mins else f"{hours}h"


def playbook(sdk, data):
    slack_token = Hive(sdk, "secret").get("slack-api-token").data["secret"]
    slack_channel = Hive(sdk, "secret").get("slack-notification-channel").data["secret"]

    # ext-playbook nests the D&R respond data under a "data" key.
    data = data.get("data", data)

    action = data.get("action", "unknown")
    case_number = data.get("case_number", "?")
    case_id = data.get("case_id", "")
    by = data.get("by", "system")
    oid = sdk._oid

    # Fetch full case for richer context (may fail; event data is primary).
    case = _fetch_case(sdk, case_number)

    # Determine effective severity.
    severity = data.get("severity") or (case or {}).get("severity") or "info"
    if action == "case_severity_upgraded":
        severity = data.get("to_severity") or severity

    sev_color = _SEVERITY_COLORS.get(severity, "#808080")
    sev_emoji = _SEVERITY_EMOJIS.get(severity, "")
    case_link = f"{_WEB_UI}/cases/{oid}/{case_number}"

    # Action-specific configuration.
    configs = {
        "case_created": {
            "color": sev_color,
            "title": f":new: New Case #{case_number}",
        },
        "case_resolved": {
            "color": "#2EB67D",
            "title": f":white_check_mark: Case #{case_number} Resolved",
        },
        "case_severity_upgraded": {
            "color": sev_color,
            "title": f":chart_with_upwards_trend: Case #{case_number} Escalated",
        },
        "case_closed": {
            "color": "#808080",
            "title": f":lock: Case #{case_number} Closed",
        },
    }
    cfg = configs.get(action, {
        "color": "#808080",
        "title": f"Case #{case_number} Updated",
    })

    blocks = []

    # Title.
    blocks.append({
        "type": "section",
        "t

Related in Backend & APIs