107 lines
3.1 KiB
Markdown
107 lines
3.1 KiB
Markdown
# ConnectWise SLA Evaluation Recipe
|
|
|
|
Reference: `sla_evaluation.md` — How to evaluate SLA state and score tickets for prioritization using the CW v3 API.
|
|
|
|
## Fields to Read
|
|
|
|
| Field | Type | Meaning |
|
|
|-------|------|---------|
|
|
| `isInSla` | bool | False = breached |
|
|
| `slaStatus` | string | e.g. "Respond by Tue 04/21 3:37 PM UTC-07" |
|
|
| `respondMinutes` | int | Minutes in "respond" phase |
|
|
| `resPlanMinutes` | int | Minutes in "resplan" phase |
|
|
| `resolveMinutes` | int | Minutes in "resolve" phase |
|
|
| `escalationLevel` | int | Tier (higher = worse) |
|
|
| `priority.level` | string | Critical / High / Medium / Low |
|
|
| `severity` | string | Critical / High / Medium / Low |
|
|
| `impact` | string | Critical / High / Medium / Low |
|
|
| `_info.dateEntered` | ISO string | Canonical creation time |
|
|
|
|
## Composite Scoring Formula
|
|
|
|
```python
|
|
score = 0
|
|
|
|
if sla_overdue:
|
|
score += 1000
|
|
elif sla_state == 'Due <24h':
|
|
score += 500
|
|
elif sla_state == 'Due <72h':
|
|
score += 200
|
|
|
|
score += (escalation or 0) * 50
|
|
|
|
prio_map = {
|
|
'Priority 1 - Critical': 400,
|
|
'Priority 2 - High': 200,
|
|
'Priority 3 - Medium': 50,
|
|
'Priority 4 - Low': 10
|
|
}
|
|
score += prio_map.get(priority_name, 0)
|
|
|
|
if severity in ('Critical', 'High'):
|
|
score += 100
|
|
if impact in ('Critical', 'High'):
|
|
score += 100
|
|
|
|
if date_entered:
|
|
age_days = (now - date_entered).total_seconds() / 86400
|
|
score += min(int(age_days) * 5, 300)
|
|
```
|
|
|
|
## SLA State Parsing
|
|
|
|
```python
|
|
import re
|
|
from datetime import datetime
|
|
|
|
def parse_sla(sla_status_text):
|
|
if not sla_status_text or sla_status_text == 'Resolved':
|
|
return sla_status_text or 'No SLA'
|
|
m = re.search(r'(Respond|Resolve)\s+by\s+(.+)', sla_status_text, re.IGNORECASE)
|
|
if m:
|
|
due_text = m.group(2).strip()
|
|
try:
|
|
dt = datetime.strptime(due_text, '%a %m/%d %I:%M %p %Z%z')
|
|
if dt < now:
|
|
return 'Overdue'
|
|
hours = (dt - now).total_seconds() / 3600
|
|
if hours < 24:
|
|
return 'Due <24h'
|
|
elif hours < 72:
|
|
return 'Due <72h'
|
|
return 'On Track'
|
|
except ValueError:
|
|
return 'Due (unparsed)'
|
|
return sla_status_text
|
|
```
|
|
|
|
## RMM Alert Categories (for filtering / triage)
|
|
|
|
Keyword-based categorization of common Sophos/RMM-generated summaries:
|
|
|
|
| Category | Keywords |
|
|
|----------|----------|
|
|
| Firewall Sophos Offline | `firewall` + `sophos` |
|
|
| Firewall Heartbeat | `not sending heartbeat` |
|
|
| Gateway Down | `gateway` + `down` |
|
|
| Malware Detection | `malicious`, `c2/` |
|
|
| Device Offline | `has not contacted` |
|
|
| Disk/Storage | `disk`, `drive`, `storage`, `c:` |
|
|
| CPU Alert | `cpu` |
|
|
| Memory Alert | `memory`, `ram` |
|
|
| Reboot Required | `reboot`, `restart` |
|
|
| AV Alert | `antivirus`, `virus` |
|
|
|
|
## Bulk Close Recipe
|
|
|
|
Update stale "new from RMM" tickets to "Self-Healed":
|
|
|
|
```python
|
|
# 1. Fetch all open tickets
|
|
# 2. Filter: status == 'new from RMM' AND _info.dateEntered > 7 days ago
|
|
# 3. PATCH each: [{"op": "replace", "path": "/status/id", "value": 425}]
|
|
# Status ID 425 = "Self-Healed" on board 6 (Managed Service Alerts)
|
|
```
|
|
|
|
**Never auto-close malware detections without human review.** |