ConnectWise PSA skills and tools under ops/connectwise (hub-sanitized 2026-11-01: client intel, internal workflow/credential/member/company ids, archived-page apiToken/sessionId stripped to placeholders)

This commit is contained in:
Hermes Agent committed 2026-11-01 13:34:58 -08:00
commit 9c96092034
36 files changed
+310875

No files matched your search

@@ -0,0 +1,107 @@
# 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.**
@@ -0,0 +1,12 @@
# ConnectWise PSA Ticket Report Logic
Reference for the `cw_open_tickets.py` script logic.
## Filtering Logic
- **URL Condition**: `closedFlag = false` (Must be URL-encoded).
- **Post-Processing**:
- `owner.identifier == 'SBrown'`
- `board.name == 'VistaNet Help Desk'`
- (Optional) `status.name != '>Completed'`
## Production Notes
- Historically, `id >= 110000` was required to avoid the 2011 legacy mirror. Current implementation prioritizes `closedFlag=false`.