sophos-central-mcp-server 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -0
- package/README.md +99 -10
- package/dist/auth/token-manager.d.ts +0 -0
- package/dist/auth/token-manager.d.ts.map +0 -0
- package/dist/auth/token-manager.js +0 -0
- package/dist/auth/token-manager.js.map +0 -0
- package/dist/client/fusion-client.d.ts +132 -0
- package/dist/client/fusion-client.d.ts.map +1 -0
- package/dist/client/fusion-client.js +258 -0
- package/dist/client/fusion-client.js.map +1 -0
- package/dist/client/sophos-client.d.ts +0 -0
- package/dist/client/sophos-client.d.ts.map +0 -0
- package/dist/client/sophos-client.js +0 -0
- package/dist/client/sophos-client.js.map +0 -0
- package/dist/client/tenant-resolver.d.ts +0 -0
- package/dist/client/tenant-resolver.d.ts.map +0 -0
- package/dist/client/tenant-resolver.js +0 -0
- package/dist/client/tenant-resolver.js.map +0 -0
- package/dist/config/config.d.ts +2 -0
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +10 -0
- package/dist/config/config.js.map +1 -1
- package/dist/fusion/case-reference-data.d.ts +34 -0
- package/dist/fusion/case-reference-data.d.ts.map +1 -0
- package/dist/fusion/case-reference-data.js +97 -0
- package/dist/fusion/case-reference-data.js.map +1 -0
- package/dist/fusion/cases-ql.d.ts +28 -0
- package/dist/fusion/cases-ql.d.ts.map +1 -0
- package/dist/fusion/cases-ql.js +77 -0
- package/dist/fusion/cases-ql.js.map +1 -0
- package/dist/fusion/format.d.ts +95 -0
- package/dist/fusion/format.d.ts.map +1 -0
- package/dist/fusion/format.js +142 -0
- package/dist/fusion/format.js.map +1 -0
- package/dist/fusion/migration.d.ts +40 -0
- package/dist/fusion/migration.d.ts.map +1 -0
- package/dist/fusion/migration.js +76 -0
- package/dist/fusion/migration.js.map +1 -0
- package/dist/fusion/queries/cases.d.ts +40 -0
- package/dist/fusion/queries/cases.d.ts.map +1 -0
- package/dist/fusion/queries/cases.js +243 -0
- package/dist/fusion/queries/cases.js.map +1 -0
- package/dist/fusion/queries/detections.d.ts +21 -0
- package/dist/fusion/queries/detections.d.ts.map +1 -0
- package/dist/fusion/queries/detections.js +64 -0
- package/dist/fusion/queries/detections.js.map +1 -0
- package/dist/fusion/types.d.ts +322 -0
- package/dist/fusion/types.d.ts.map +1 -0
- package/dist/fusion/types.js +7 -0
- package/dist/fusion/types.js.map +1 -0
- package/dist/index.d.ts +0 -0
- package/dist/index.d.ts.map +0 -0
- package/dist/index.js +16 -4
- package/dist/index.js.map +1 -1
- package/dist/tools/accounts.d.ts +0 -0
- package/dist/tools/accounts.d.ts.map +0 -0
- package/dist/tools/accounts.js +0 -0
- package/dist/tools/accounts.js.map +0 -0
- package/dist/tools/admin-management.d.ts +0 -0
- package/dist/tools/admin-management.d.ts.map +0 -0
- package/dist/tools/admin-management.js +0 -0
- package/dist/tools/admin-management.js.map +0 -0
- package/dist/tools/alerts.d.ts +0 -0
- package/dist/tools/alerts.d.ts.map +0 -0
- package/dist/tools/alerts.js +0 -0
- package/dist/tools/alerts.js.map +0 -0
- package/dist/tools/audit-events.d.ts +0 -0
- package/dist/tools/audit-events.d.ts.map +0 -0
- package/dist/tools/audit-events.js +0 -0
- package/dist/tools/audit-events.js.map +0 -0
- package/dist/tools/business-automation.d.ts +0 -0
- package/dist/tools/business-automation.d.ts.map +0 -0
- package/dist/tools/business-automation.js +0 -0
- package/dist/tools/business-automation.js.map +0 -0
- package/dist/tools/cases.d.ts +2 -1
- package/dist/tools/cases.d.ts.map +1 -1
- package/dist/tools/cases.js +46 -19
- package/dist/tools/cases.js.map +1 -1
- package/dist/tools/cloud-security.d.ts +0 -0
- package/dist/tools/cloud-security.d.ts.map +0 -0
- package/dist/tools/cloud-security.js +0 -0
- package/dist/tools/cloud-security.js.map +0 -0
- package/dist/tools/detections.d.ts +2 -1
- package/dist/tools/detections.d.ts.map +1 -1
- package/dist/tools/detections.js +32 -14
- package/dist/tools/detections.js.map +1 -1
- package/dist/tools/directory.d.ts +0 -0
- package/dist/tools/directory.d.ts.map +0 -0
- package/dist/tools/directory.js +0 -0
- package/dist/tools/directory.js.map +0 -0
- package/dist/tools/dns-protection.d.ts +0 -0
- package/dist/tools/dns-protection.d.ts.map +0 -0
- package/dist/tools/dns-protection.js +0 -0
- package/dist/tools/dns-protection.js.map +0 -0
- package/dist/tools/email.d.ts +0 -0
- package/dist/tools/email.d.ts.map +0 -0
- package/dist/tools/email.js +0 -0
- package/dist/tools/email.js.map +0 -0
- package/dist/tools/endpoint-migrations.d.ts +0 -0
- package/dist/tools/endpoint-migrations.d.ts.map +0 -0
- package/dist/tools/endpoint-migrations.js +0 -0
- package/dist/tools/endpoint-migrations.js.map +0 -0
- package/dist/tools/endpoint-settings.d.ts +0 -0
- package/dist/tools/endpoint-settings.d.ts.map +0 -0
- package/dist/tools/endpoint-settings.js +0 -0
- package/dist/tools/endpoint-settings.js.map +0 -0
- package/dist/tools/endpoints.d.ts +0 -0
- package/dist/tools/endpoints.d.ts.map +0 -0
- package/dist/tools/endpoints.js +0 -0
- package/dist/tools/endpoints.js.map +0 -0
- package/dist/tools/exclusions.d.ts +0 -0
- package/dist/tools/exclusions.d.ts.map +0 -0
- package/dist/tools/exclusions.js +0 -0
- package/dist/tools/exclusions.js.map +0 -0
- package/dist/tools/firewall.d.ts +0 -0
- package/dist/tools/firewall.d.ts.map +0 -0
- package/dist/tools/firewall.js +0 -0
- package/dist/tools/firewall.js.map +0 -0
- package/dist/tools/fusion-cases.d.ts +50 -0
- package/dist/tools/fusion-cases.d.ts.map +1 -0
- package/dist/tools/fusion-cases.js +1809 -0
- package/dist/tools/fusion-cases.js.map +1 -0
- package/dist/tools/fusion-detections.d.ts +14 -0
- package/dist/tools/fusion-detections.d.ts.map +1 -0
- package/dist/tools/fusion-detections.js +75 -0
- package/dist/tools/fusion-detections.js.map +1 -0
- package/dist/tools/groups.d.ts +0 -0
- package/dist/tools/groups.d.ts.map +0 -0
- package/dist/tools/groups.js +0 -0
- package/dist/tools/groups.js.map +0 -0
- package/dist/tools/health.d.ts +0 -0
- package/dist/tools/health.d.ts.map +0 -0
- package/dist/tools/health.js +0 -0
- package/dist/tools/health.js.map +0 -0
- package/dist/tools/helpers.d.ts +7 -0
- package/dist/tools/helpers.d.ts.map +1 -1
- package/dist/tools/helpers.js +7 -0
- package/dist/tools/helpers.js.map +1 -1
- package/dist/tools/licensing.d.ts +0 -0
- package/dist/tools/licensing.d.ts.map +0 -0
- package/dist/tools/licensing.js +0 -0
- package/dist/tools/licensing.js.map +0 -0
- package/dist/tools/live-discover.d.ts +0 -0
- package/dist/tools/live-discover.d.ts.map +0 -0
- package/dist/tools/live-discover.js +0 -0
- package/dist/tools/live-discover.js.map +0 -0
- package/dist/tools/mobile.d.ts +0 -0
- package/dist/tools/mobile.d.ts.map +0 -0
- package/dist/tools/mobile.js +0 -0
- package/dist/tools/mobile.js.map +0 -0
- package/dist/tools/partner.d.ts +0 -0
- package/dist/tools/partner.d.ts.map +0 -0
- package/dist/tools/partner.js +0 -0
- package/dist/tools/partner.js.map +0 -0
- package/dist/tools/policies.d.ts +0 -0
- package/dist/tools/policies.d.ts.map +0 -0
- package/dist/tools/policies.js +0 -0
- package/dist/tools/policies.js.map +0 -0
- package/dist/tools/siem.d.ts +0 -0
- package/dist/tools/siem.d.ts.map +0 -0
- package/dist/tools/siem.js +0 -0
- package/dist/tools/siem.js.map +0 -0
- package/dist/tools/switch.d.ts +0 -0
- package/dist/tools/switch.d.ts.map +0 -0
- package/dist/tools/switch.js +0 -0
- package/dist/tools/switch.js.map +0 -0
- package/dist/tools/tenants.d.ts +0 -0
- package/dist/tools/tenants.d.ts.map +0 -0
- package/dist/tools/tenants.js +0 -0
- package/dist/tools/tenants.js.map +0 -0
- package/dist/tools/user-activity.d.ts +0 -0
- package/dist/tools/user-activity.d.ts.map +0 -0
- package/dist/tools/user-activity.js +0 -0
- package/dist/tools/user-activity.js.map +0 -0
- package/dist/tools/web-filtering.d.ts +0 -0
- package/dist/tools/web-filtering.d.ts.map +0 -0
- package/dist/tools/web-filtering.js +0 -0
- package/dist/tools/web-filtering.js.map +0 -0
- package/dist/tools/wifi.d.ts +0 -0
- package/dist/tools/wifi.d.ts.map +0 -0
- package/dist/tools/wifi.js +0 -0
- package/dist/tools/wifi.js.map +0 -0
- package/dist/tools/xdr.d.ts +0 -0
- package/dist/tools/xdr.d.ts.map +0 -0
- package/dist/tools/xdr.js +0 -0
- package/dist/tools/xdr.js.map +0 -0
- package/dist/types/sophos.d.ts +0 -0
- package/dist/types/sophos.d.ts.map +0 -0
- package/dist/types/sophos.js +0 -0
- package/dist/types/sophos.js.map +0 -0
- package/docs/screenshots/tenant-health-detail.png +0 -0
- package/docs/screenshots/tenant-health-overview.png +0 -0
- package/docs/screenshots/tenant-listing.png +0 -0
- package/package.json +16 -4
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
# Sophos
|
|
1
|
+
# Sophos Fusion MCP Server (formerly Sophos Central)
|
|
2
2
|
|
|
3
|
-
MCP (Model Context Protocol) server for
|
|
3
|
+
MCP (Model Context Protocol) server for the Sophos Fusion and Sophos Central APIs. Supports partner, organisation, and single-tenant credential types with automatic region routing. **310 tools** covering 22 Sophos API namespaces: the Sophos Central REST APIs plus the Sophos Fusion GraphQL APIs (`sophos_fusion_*`). Install it as a Claude Desktop extension (`.mcpb`), run it with npx, or host it yourself over streamable HTTP.
|
|
4
|
+
|
|
5
|
+
The npm package, the `.mcpb` bundle and the binaries keep the `sophos-central-mcp-server` name so existing installs update in place.
|
|
4
6
|
|
|
5
7
|
## Prerequisites
|
|
6
8
|
|
|
@@ -8,7 +10,7 @@ You need these before any of the install options below.
|
|
|
8
10
|
|
|
9
11
|
### Sophos Central API credentials
|
|
10
12
|
|
|
11
|
-
Every install method needs a Client ID and Client Secret. The credential type decides what the server can see:
|
|
13
|
+
Every install method needs a Client ID and Client Secret. The same credential authorises both the Sophos Central REST tools and the Sophos Fusion GraphQL tools; there is no second credential. The credential type decides what the server can see:
|
|
12
14
|
|
|
13
15
|
- **Tenant-level**: In Sophos Central, go to **Settings > API Credentials Management** and create a new credential. The server operates on that one tenant.
|
|
14
16
|
- **Partner-level**: In the Sophos Partner Dashboard, create API credentials under **Settings > API Credentials**. The server can query every tenant the partner manages.
|
|
@@ -129,6 +131,26 @@ The script:
|
|
|
129
131
|
|
|
130
132
|
`build/` and `release/` are git-ignored. To inspect a bundle without installing it, `npx mcpb info release/<file>.mcpb` prints its size and signature state, and `npx mcpb unpack release/<file>.mcpb <dir>` extracts it. Signing is optional; `npx mcpb sign --self-signed release/<file>.mcpb` adds a self-signed signature if you want one.
|
|
131
133
|
|
|
134
|
+
### Tests (maintainers)
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npm test
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Builds, then runs the `node:test` suites in `test/` with `fetch` stubbed: the Fusion GraphQL transport (including the HTTP 200 with `errors` case), the QL builder, the reference-data cache, and the path-keyed retry for the intermittent case-write fault, the migration guard, and a registration check that lists all 310 tools over an in-memory transport and checks the descriptions for the measured warnings. Nothing in `npm test` reaches Sophos.
|
|
141
|
+
|
|
142
|
+
To exercise the Fusion tools against a live tenant, put credentials in `.env` (or export them) and run:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
node scripts/fusion-smoke.mjs # read-only, tenant credential
|
|
146
|
+
node scripts/fusion-smoke.mjs <tenant-id> # read-only, partner or organisation credential
|
|
147
|
+
node scripts/fusion-smoke.mjs --write # plus the Fusion write tools on one new case
|
|
148
|
+
node scripts/fusion-smoke.mjs --write-classic # plus the Classic case tools on one new case
|
|
149
|
+
node scripts/fusion-smoke.mjs --all # everything
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
It spawns the built server over stdio and calls the tools through an MCP client, so what runs is what a host runs. Read-only mode covers the reference data, the case list with filter and cursor variants, the newest case by short ID, its evidence, summary, comments and files, the detection search, the not-found and legacy-ID refusals, and the Classic side: on a migrated tenant the migration refusal, otherwise the six Classic read tools. `--write` creates one case titled `MCP smoke test <timestamp>` (managed_by CUSTOMER), exercises comment add, edit and delete with the mention read-back, link create, update and delete, tag merge and replace, evidence add and remove_all, file upload, list and soft delete, the verdict clear on reopen, the one way door on new and the closed-case freeze, then closes it with a verdict and archives it (Fusion has no delete). `--write-classic` creates, updates and deletes one Classic case; the Cases API requires an assignee and a detection that still exists, so set `SMOKE_CLASSIC_ASSIGNEE` (a tenant admin email) and, if needed, `SMOKE_CLASSIC_DETECTION_ID` (a recent detection ID). Every write is listed at the end. Existing cases are never modified.
|
|
153
|
+
|
|
132
154
|
**Cutting a release:** bump `version` in `package.json`, run `npm run build:mcpb`, commit `package.json`, `package-lock.json`, and `manifest.json`, tag, and attach the `.mcpb` from `release/` to the GitHub release. Publish to npm as before so the Claude Code and self-hosted options pick up the same version.
|
|
133
155
|
|
|
134
156
|
## Features
|
|
@@ -141,7 +163,9 @@ The script:
|
|
|
141
163
|
- **Rate limit handling**: Retry with backoff on 429 responses
|
|
142
164
|
- **Dual transport**: stdio (Claude Desktop, Claude Code, and the `.mcpb` bundle) or streamable HTTP (self-hosted)
|
|
143
165
|
- **One-click install**: Ships as a Claude Desktop extension (`.mcpb`) with credentials held in the OS secure store
|
|
144
|
-
- **
|
|
166
|
+
- **Two API generations**: the Sophos Central REST APIs and the Sophos Fusion GraphQL APIs on one credential, with `sophos_fusion_*` tools for the GraphQL side
|
|
167
|
+
- **Migration aware**: the Classic case and detection tools refuse a tenant that has moved to Sophos Fusion and name the `sophos_fusion_*` tool to use instead
|
|
168
|
+
- **Full API coverage**: 310 tools across endpoints, alerts, policies, firewalls, web filtering, licensing, audit events, email, mobile, XDR, cases, SIEM, and more
|
|
145
169
|
|
|
146
170
|
## Screenshots
|
|
147
171
|
|
|
@@ -175,9 +199,22 @@ TRANSPORT=http
|
|
|
175
199
|
| `PORT` | No | 3100 | HTTP server port |
|
|
176
200
|
| `TRANSPORT` | No | http | `http` for streamable HTTP, `stdio` for subprocess mode |
|
|
177
201
|
| `CHARACTER_LIMIT` | No | 50000 | Maximum characters per tool response before truncation (minimum 10000) |
|
|
202
|
+
| `SOPHOS_FUSION_GRAPHQL_URL` | No | `https://api.taegis.sophos.com/graphql` | Sophos Fusion GraphQL endpoint. Override when the Fusion branded hostnames ship |
|
|
203
|
+
| `SOPHOS_CLASSIC_MIGRATION_CHECK` | No | on | `off` skips the migration check on the Classic case and detection tools (they then run for every tenant) |
|
|
178
204
|
|
|
179
205
|
## Tools
|
|
180
206
|
|
|
207
|
+
### Two API generations
|
|
208
|
+
|
|
209
|
+
The server speaks to two Sophos API generations on one credential:
|
|
210
|
+
|
|
211
|
+
- **Sophos Central REST APIs** (retained). Every tool without the `fusion` prefix. Regional hosts, discovered from `/whoami/v1`.
|
|
212
|
+
- **Sophos Fusion GraphQL APIs** (new, 18/09/2026). The `sophos_fusion_*` tools. One endpoint, `https://api.taegis.sophos.com/graphql`, same token, same `X-Tenant-ID` header. Filters are written in Fusion Query Language (QL). Case types, statuses and verdicts are tenant reference data resolved to IDs at runtime, case severity is an integer (2 to 10), and assignees are Subject IDs, not email addresses. A GraphQL failure arrives as HTTP 200 with an `errors` array; the client treats that as an error, and a partial response (data plus errors) is returned with a `warnings` list rather than as a clean result.
|
|
213
|
+
|
|
214
|
+
Sophos is moving tenants to Fusion over the coming months (the [upgrade centre](https://community.sophos.com/sophos-xdr/sophos-xdr-mdr-expansion/upgrade-center) announces each account's slot). This is a point in time change, not a fallback: once a tenant has moved, the Classic Cases and Detections REST APIs reference the pre-migration Sophos Central objects, so their answers are wrong rather than stale. The Classic tools therefore check which world a tenant is in before every call, using the Fusion case reference data (a tenant that has not moved gets an empty case type list), and refuse a migrated tenant with a message naming the `sophos_fusion_*` tool to use. For a tenant that has not moved they stay correct and carry no deprecation label. Fusion holds a separate case set (a UUID plus a `CSE#####` short ID) and neither ID form resolves in the other API. Live Discover and XDR Query are unaffected. Data Lake search over GraphQL has not shipped (Sophos says October 2026), so `sophos_run_xdr_query` stays on the SQL XDR Query API. Fusion tools for events, threat timeline and live endpoint search are planned.
|
|
215
|
+
|
|
216
|
+
Two Fusion case writes, `createCase` and `addEvidenceToCase`, fail 74% of the time with `not allowed` on `partnerPreferences` from `investigations-v2`. It is an intermittent downstream fault, not authorisation: the identical call succeeds on retry, and `updateCase` does not carry the fault at all. Measured on a live tenant 22/09/2026: 111 faults in 150 calls, drifting from 60% to 85% across four samples six minutes apart. The client retries a call whose first error path is `partnerPreferences`, and only that, up to 20 times with a short backoff, which leaves about 1 call in 410 failing at the measured rate and about 1 in 26 at the worst rate observed; an error naming the operation in its path is a real input or permission error and surfaces at once.
|
|
217
|
+
|
|
181
218
|
### Partner & Organisation (18 tools)
|
|
182
219
|
|
|
183
220
|
> These tools are only available with **partner or organisation-level** credentials. They operate across all managed tenants.
|
|
@@ -369,11 +406,13 @@ TRANSPORT=http
|
|
|
369
406
|
|
|
370
407
|
### Cases (9 tools)
|
|
371
408
|
|
|
409
|
+
Sophos Central Cases REST API. Correct for a tenant that has not moved to Fusion, and the only way to read its legacy cases (IDs like `1-598868`). Refused for a tenant that has moved, with a pointer to the `sophos_fusion_*` tool to use (see "Two API generations").
|
|
410
|
+
|
|
372
411
|
| Tool | Description |
|
|
373
412
|
|------|-------------|
|
|
374
413
|
| `sophos_list_cases` | List investigation cases |
|
|
375
414
|
| `sophos_get_case` | Get full case details |
|
|
376
|
-
| `sophos_create_case` | Create a new investigation case |
|
|
415
|
+
| `sophos_create_case` | Create a new investigation case (the API requires an assignee and a detection that still exists) |
|
|
377
416
|
| `sophos_update_case` | Update case status, severity, assignee |
|
|
378
417
|
| `sophos_delete_case` | Delete a case |
|
|
379
418
|
| `sophos_list_case_detections` | List detections linked to a case |
|
|
@@ -381,9 +420,47 @@ TRANSPORT=http
|
|
|
381
420
|
| `sophos_list_case_impacted_entities` | List impacted entities for a case |
|
|
382
421
|
| `sophos_get_case_mitre_summary` | Get MITRE ATT&CK breakdown for a case |
|
|
383
422
|
|
|
423
|
+
### Fusion Cases (21 tools)
|
|
424
|
+
|
|
425
|
+
Sophos Fusion Cases GraphQL API v2. Case IDs are UUIDs; short IDs (`CSE00001`) are accepted and resolved. Severity is 2 informational, 4 low, 6 medium, 8 high, 10 critical. Detection severity inside the case summary is a 0 to 1 float, a different scale from both the case severity and the Classic REST 0 to 10 detection severity; none of them convert. There is no delete: close the case (with a verdict when its type needs one), then archive it.
|
|
426
|
+
|
|
427
|
+
`managed_by` is required on create and decides who works the case: `PROVIDER` hands it to Sophos MDR, `CUSTOMER` keeps it self managed. It cannot be changed afterwards, and a case created without it is unclaimed, so the tool never omits it. Detection and event IDs on the evidence tools are six section resource names (`alert://priv:event-filter:123456:1789526908712:<uuid>`), which `sophos_fusion_search_detections` returns. Evidence writes are queued jobs that land category by category (an add showed after about 5 seconds, a three category removal took about 25 seconds), so read the evidence back with a poll; adding a detection also attaches its linked asset and events, and removing it does not retract them (`remove_all` does). Comment @mentions (`@authorized_contacts`, `@customer`, `@sophos`) fire wherever they appear, including in prose, and an unrecognised token is dropped silently, so the comment tools read the stored mentions back. Tags are merged on update unless `replace_tags` is set. File deletion is soft. Split and merge are irreversible and need a confirmation argument.
|
|
428
|
+
|
|
429
|
+
| Tool | Description |
|
|
430
|
+
|------|-------------|
|
|
431
|
+
| `sophos_fusion_list_cases` | List cases with QL filters (type, status, verdict resolved to IDs), offset or cursor pagination |
|
|
432
|
+
| `sophos_fusion_get_case` | Full case detail including key findings, verdict, links and processing status |
|
|
433
|
+
| `sophos_fusion_get_case_evidence` | Detection, event, asset and saved-search source IDs attached to a case |
|
|
434
|
+
| `sophos_fusion_get_case_summary` | Case plus its detections resolved in one batched call and a MITRE ATT&CK roll-up |
|
|
435
|
+
| `sophos_fusion_list_case_reference_data` | The tenant's case types, primary statuses and verdicts (15 minute cache) |
|
|
436
|
+
| `sophos_fusion_create_case` | Create a case: required managed_by, type and status by name or ID, integer severity, Markdown key findings, genesis evidence |
|
|
437
|
+
| `sophos_fusion_update_case` | Update fields, merge or replace tags, close with a verdict, reopen (verdict cleared), archive or unarchive; refuses frozen fields on a closed case rather than reopening it |
|
|
438
|
+
| `sophos_fusion_split_case` | Move named evidence into a new case (irreversible, confirm_split) |
|
|
439
|
+
| `sophos_fusion_merge_cases` | Merge source cases into a target and close them (asynchronous, irreversible, confirm_merge) |
|
|
440
|
+
| `sophos_fusion_list_case_comments` | List comments (raw author IDs, resolved mentions, read state) |
|
|
441
|
+
| `sophos_fusion_add_case_comment` | Add a comment and report which @mentions actually fired |
|
|
442
|
+
| `sophos_fusion_update_case_comment` | Edit a comment or mark it read |
|
|
443
|
+
| `sophos_fusion_delete_case_comment` | Delete a comment |
|
|
444
|
+
| `sophos_fusion_add_case_evidence` | Attach detections, events, hosts or saved searches (asynchronous, RNs checked) |
|
|
445
|
+
| `sophos_fusion_remove_case_evidence` | Detach evidence by source ID, or everything with remove_all (asynchronous) |
|
|
446
|
+
| `sophos_fusion_list_case_files` | List a case's files (deleted hidden by default, download URLs on request) |
|
|
447
|
+
| `sophos_fusion_upload_case_file` | Attach a file: register, PUT to the presigned URL, poll to UPLOADED |
|
|
448
|
+
| `sophos_fusion_delete_case_file` | Soft delete a file |
|
|
449
|
+
| `sophos_fusion_create_case_link` | Attach an external link (ServiceNow ticket, report) |
|
|
450
|
+
| `sophos_fusion_update_case_link` | Change a link's URL, title, type or reference |
|
|
451
|
+
| `sophos_fusion_delete_case_link` | Remove a link |
|
|
452
|
+
|
|
453
|
+
### Fusion Detections (1 tool)
|
|
454
|
+
|
|
455
|
+
Sophos Fusion Detections GraphQL API v2. One QL search replaces the Classic run, poll, results triple. Source keyword `alert`; working example `from alert severity >= 0.1 EARLIEST=-90d`. Severity is a 0 to 1 float.
|
|
456
|
+
|
|
457
|
+
| Tool | Description |
|
|
458
|
+
|------|-------------|
|
|
459
|
+
| `sophos_fusion_search_detections` | Search detections with QL; returns the resource-name IDs the case evidence tools take |
|
|
460
|
+
|
|
384
461
|
### Detections (6 tools)
|
|
385
462
|
|
|
386
|
-
|
|
463
|
+
Sophos Central Detections REST API, async: start a query, poll for completion, then fetch results. Refused for a tenant that has moved to Fusion, with a pointer to `sophos_fusion_search_detections`.
|
|
387
464
|
|
|
388
465
|
| Tool | Description |
|
|
389
466
|
|------|-------------|
|
|
@@ -403,7 +480,7 @@ Async API — start a query, poll for completion, then fetch results.
|
|
|
403
480
|
|
|
404
481
|
### XDR Data Lake (9 tools)
|
|
405
482
|
|
|
406
|
-
Async API
|
|
483
|
+
Async API: submit SQL queries against historical telemetry.
|
|
407
484
|
|
|
408
485
|
| Tool | Description |
|
|
409
486
|
|------|-------------|
|
|
@@ -419,7 +496,7 @@ Async API — submit SQL queries against historical telemetry.
|
|
|
419
496
|
|
|
420
497
|
### Live Discover (4 tools)
|
|
421
498
|
|
|
422
|
-
Async API
|
|
499
|
+
Async API: run OSquery SQL on live endpoints. Rate limited to 10 runs/minute, 500/day.
|
|
423
500
|
|
|
424
501
|
| Tool | Description |
|
|
425
502
|
|------|-------------|
|
|
@@ -652,6 +729,7 @@ Register tools based on identity type
|
|
|
652
729
|
|
|
|
653
730
|
v
|
|
654
731
|
Per tool call: resolve tenant -> regional API host -> execute request
|
|
732
|
+
Per sophos_fusion_* call: resolve tenant -> POST api.taegis.sophos.com/graphql -> inspect data and errors
|
|
655
733
|
```
|
|
656
734
|
|
|
657
735
|
### Key decisions
|
|
@@ -669,10 +747,21 @@ src/
|
|
|
669
747
|
├── config/config.ts # Environment config
|
|
670
748
|
├── auth/token-manager.ts # OAuth2 token lifecycle
|
|
671
749
|
├── client/
|
|
672
|
-
│ ├── sophos-client.ts #
|
|
750
|
+
│ ├── sophos-client.ts # REST client with region routing (Sophos Central)
|
|
751
|
+
│ ├── fusion-client.ts # GraphQL client (Sophos Fusion), 200-with-errors handling, path-keyed retry
|
|
673
752
|
│ └── tenant-resolver.ts # Whoami + tenant cache
|
|
753
|
+
├── fusion/
|
|
754
|
+
│ ├── queries/cases.ts # Cases v2 GraphQL documents
|
|
755
|
+
│ ├── queries/detections.ts # Detections v2 documents: the case summary lookup and the QL search
|
|
756
|
+
│ ├── case-reference-data.ts # Per-tenant cache of case types, statuses, verdicts
|
|
757
|
+
│ ├── cases-ql.ts # QL builder for the cases search
|
|
758
|
+
│ ├── format.ts # Severity scales, timestamps, ID checks, mentions, tags, detection rows
|
|
759
|
+
│ ├── migration.ts # Has this tenant moved to Fusion? Gates the Classic case and detection tools
|
|
760
|
+
│ └── types.ts # Fusion response types
|
|
674
761
|
├── tools/
|
|
675
762
|
│ ├── helpers.ts # Shared response formatting
|
|
763
|
+
│ ├── fusion-cases.ts # Sophos Fusion cases (GraphQL)
|
|
764
|
+
│ ├── fusion-detections.ts # Sophos Fusion detection search (GraphQL)
|
|
676
765
|
│ ├── tenants.ts # Tenant listing (partner/org only)
|
|
677
766
|
│ ├── partner.ts # Partner admin, roles, billing (partner/org only)
|
|
678
767
|
│ ├── alerts.ts # Alert list, get, acknowledge, search
|
|
@@ -700,7 +789,7 @@ src/
|
|
|
700
789
|
└── types/sophos.ts # Sophos API response types
|
|
701
790
|
```
|
|
702
791
|
|
|
703
|
-
Packaging files at the repo root: `manifest.json` (MCPB manifest, regenerated by the build), `scripts/build-mcpb.mjs` (bundle builder), and `.mcpbignore` (extra exclusions applied when packing).
|
|
792
|
+
Packaging files at the repo root: `manifest.json` (MCPB manifest, regenerated by the build), `scripts/build-mcpb.mjs` (bundle builder), and `.mcpbignore` (extra exclusions applied when packing). `schemas/fusion/` holds the five Fusion GraphQL schemas as downloaded from `https://developer.sophos.com/assets/graphql/<api>.graphql` on 21/09/2026, unmodified, for reference and for diffing when Sophos changes them. `test/` holds the `node:test` suites and `scripts/fusion-smoke.mjs` the live check.
|
|
704
793
|
|
|
705
794
|
## Security
|
|
706
795
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL client for the Sophos Fusion APIs (Cases v2, Detections v2, Threat
|
|
3
|
+
* Timeline v2, Events v1, Live Endpoint Search v1). Sits beside SophosClient,
|
|
4
|
+
* which keeps serving the Sophos Central REST APIs.
|
|
5
|
+
*
|
|
6
|
+
* One endpoint for every tenant, no regional host lookup. Auth is unchanged:
|
|
7
|
+
* the same bearer token from TokenManager and the same X-Tenant-ID header the
|
|
8
|
+
* REST client sends.
|
|
9
|
+
*
|
|
10
|
+
* The rule that shapes this file: a GraphQL-layer failure comes back as HTTP
|
|
11
|
+
* 200 with an `errors` array beside `data`. A 200 is not success until the
|
|
12
|
+
* body has been read. Transport failures (expired token, 429, 5xx) use the
|
|
13
|
+
* usual non-2xx status and the standard Sophos error object, and those are
|
|
14
|
+
* retried the same way SophosClient retries them. GraphQL-layer errors are
|
|
15
|
+
* not retried, with one measured exception: the intermittent
|
|
16
|
+
* partnerPreferences fault on the case write path, which is keyed on the
|
|
17
|
+
* error path and bounded (see FusionGraphQLError.isTransientPartnerPreferences).
|
|
18
|
+
*/
|
|
19
|
+
import type { TokenManager } from "../auth/token-manager.js";
|
|
20
|
+
/** One entry of a GraphQL response `errors` array. */
|
|
21
|
+
export interface GraphQLErrorEntry {
|
|
22
|
+
message: string;
|
|
23
|
+
path?: Array<string | number>;
|
|
24
|
+
locations?: Array<{
|
|
25
|
+
line: number;
|
|
26
|
+
column: number;
|
|
27
|
+
}>;
|
|
28
|
+
extensions?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
interface GraphQLResponseBody<T> {
|
|
31
|
+
data?: T | null;
|
|
32
|
+
errors?: GraphQLErrorEntry[];
|
|
33
|
+
}
|
|
34
|
+
export interface FusionQueryResult<T> {
|
|
35
|
+
/** The response `data` object. Never null: a response with no usable data throws. */
|
|
36
|
+
data: T;
|
|
37
|
+
/**
|
|
38
|
+
* GraphQL errors that arrived beside usable data (a partial response, for
|
|
39
|
+
* example a federated field that could not be resolved). Empty on a clean
|
|
40
|
+
* response. Tools must surface these; they are not silently dropped.
|
|
41
|
+
*/
|
|
42
|
+
warnings: string[];
|
|
43
|
+
}
|
|
44
|
+
export interface FusionClientOptions {
|
|
45
|
+
/** GraphQL endpoint. Defaults to SOPHOS_FUSION_GRAPHQL_URL. */
|
|
46
|
+
url?: string;
|
|
47
|
+
/** Per-attempt timeout. Default 30 s, matching SophosClient. */
|
|
48
|
+
timeoutMs?: number;
|
|
49
|
+
/** Retries after the first attempt for 429, 5xx and network errors. Default 2. */
|
|
50
|
+
retries?: number;
|
|
51
|
+
/** Base for the full-jitter exponential backoff. Default 1000 ms. */
|
|
52
|
+
backoffBaseMs?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Total attempts for a call that hits the transient partnerPreferences
|
|
55
|
+
* fault, which affects createCase and addEvidenceToCase. Measured on a live
|
|
56
|
+
* tenant 22/09/2026: 111 faults in 150 calls, so 74%, and it drifts, with
|
|
57
|
+
* four samples over six minutes running 60%, 85%, 72.5% and 80%. Default 20:
|
|
58
|
+
* about 1 call in 410 fails at the measured 74%, and about 1 in 26 at the
|
|
59
|
+
* worst rate observed. updateCase does not carry the fault at all, 0 in 30
|
|
60
|
+
* in the same minute addEvidenceToCase was 24 in 30, so the defect is in the
|
|
61
|
+
* two resolvers that read partner preferences.
|
|
62
|
+
*/
|
|
63
|
+
transientAttempts?: number;
|
|
64
|
+
/** Base for the short full-jitter backoff between those attempts. Default 300 ms, capped at 2 s. */
|
|
65
|
+
transientBackoffMs?: number;
|
|
66
|
+
}
|
|
67
|
+
/** Thrown when the GraphQL layer returns errors and no usable data. */
|
|
68
|
+
export declare class FusionGraphQLError extends Error {
|
|
69
|
+
readonly errors: GraphQLErrorEntry[];
|
|
70
|
+
constructor(message: string, errors: GraphQLErrorEntry[]);
|
|
71
|
+
/**
|
|
72
|
+
* True when every error says the record does not exist. Fusion reports an
|
|
73
|
+
* unknown case ID this way, as HTTP 200 with "record not found" beside a
|
|
74
|
+
* null root field, rather than as a bare null (live tenant, 21/09/2026).
|
|
75
|
+
*/
|
|
76
|
+
get notFound(): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* True for the intermittent investigations-v2 fault on the case write
|
|
79
|
+
* path: `errors[0].path[0]` is "partnerPreferences" ("not allowed",
|
|
80
|
+
* DOWNSTREAM_SERVICE_ERROR). It is not an authorisation failure: the
|
|
81
|
+
* identical call with identical input succeeds on retry. Keyed on the path
|
|
82
|
+
* and nothing else, because `extensions.code` is DOWNSTREAM_SERVICE_ERROR
|
|
83
|
+
* for transient faults, not found and malformed queries alike, while a
|
|
84
|
+
* genuine input or permission error carries the operation name in the path
|
|
85
|
+
* (createCase, addEvidenceToCase, tdrusers) with a specific message.
|
|
86
|
+
*/
|
|
87
|
+
get isTransientPartnerPreferences(): boolean;
|
|
88
|
+
}
|
|
89
|
+
export declare class FusionClient {
|
|
90
|
+
private tokenManager;
|
|
91
|
+
private readonly url;
|
|
92
|
+
private readonly timeoutMs;
|
|
93
|
+
private readonly retries;
|
|
94
|
+
private readonly backoffBaseMs;
|
|
95
|
+
private readonly transientAttempts;
|
|
96
|
+
private readonly transientBackoffMs;
|
|
97
|
+
constructor(tokenManager: TokenManager, options?: FusionClientOptions);
|
|
98
|
+
/** The endpoint this client posts to. */
|
|
99
|
+
get endpoint(): string;
|
|
100
|
+
/**
|
|
101
|
+
* Run a GraphQL query or mutation for a tenant.
|
|
102
|
+
*
|
|
103
|
+
* Returns the `data` object plus any partial-response warnings. Throws
|
|
104
|
+
* FusionGraphQLError when the response carries errors and no usable data,
|
|
105
|
+
* and a plain Error for transport failures. The transient partnerPreferences
|
|
106
|
+
* fault is retried up to transientAttempts times; every other GraphQL-layer
|
|
107
|
+
* error surfaces at once.
|
|
108
|
+
*/
|
|
109
|
+
query<T extends object>(tenantId: string, document: string, variables?: Record<string, unknown>): Promise<FusionQueryResult<T>>;
|
|
110
|
+
/**
|
|
111
|
+
* PUT raw bytes to a presigned upload URL (case files). The signature lives
|
|
112
|
+
* in the URL, so no bearer token is sent, and the Content-Type must match
|
|
113
|
+
* what startCaseFileUpload was told. One attempt, no retry.
|
|
114
|
+
*/
|
|
115
|
+
putPresigned(url: string, body: Uint8Array, contentType: string): Promise<void>;
|
|
116
|
+
private executeWithRetry;
|
|
117
|
+
private sleep;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Decide what a 200 response actually means.
|
|
121
|
+
*
|
|
122
|
+
* - errors present, no usable data: throw with every message joined.
|
|
123
|
+
* - errors present beside usable data: return the data with the errors as warnings.
|
|
124
|
+
* - no errors, data present: clean result. A root field that is null with no
|
|
125
|
+
* error (for example `case` for an unknown ID) is legitimate and passes
|
|
126
|
+
* through for the tool to report.
|
|
127
|
+
* - no errors, no data: malformed, throw.
|
|
128
|
+
*/
|
|
129
|
+
export declare function interpretGraphQLBody<T extends object>(body: GraphQLResponseBody<T> | null | undefined): FusionQueryResult<T>;
|
|
130
|
+
export declare function formatGraphQLError(error: GraphQLErrorEntry): string;
|
|
131
|
+
export {};
|
|
132
|
+
//# sourceMappingURL=fusion-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fusion-client.d.ts","sourceRoot":"","sources":["../../src/client/fusion-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG7D,sDAAsD;AACtD,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;IAC9B,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,UAAU,mBAAmB,CAAC,CAAC;IAC7B,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;IAChB,MAAM,CAAC,EAAE,iBAAiB,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB,CAAC,CAAC;IAClC,qFAAqF;IACrF,IAAI,EAAE,CAAC,CAAC;IACR;;;;OAIG;IACH,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;;OASG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,oGAAoG;IACpG,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAID,uEAAuE;AACvE,qBAAa,kBAAmB,SAAQ,KAAK;IAGzC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,EAAE;gBADpC,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,iBAAiB,EAAE;IAMtC;;;;OAIG;IACH,IAAI,QAAQ,IAAI,OAAO,CAKtB;IAED;;;;;;;;;OASG;IACH,IAAI,6BAA6B,IAAI,OAAO,CAE3C;CACF;AAED,qBAAa,YAAY;IASrB,OAAO,CAAC,YAAY;IARtB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;gBAGlC,YAAY,EAAE,YAAY,EAClC,OAAO,GAAE,mBAAwB;IAUnC,yCAAyC;IACzC,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED;;;;;;;;OAQG;IACG,KAAK,CAAC,CAAC,SAAS,MAAM,EAC1B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACtC,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC;IAsChC;;;;OAIG;IACG,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YA2BvE,gBAAgB;IAqF9B,OAAO,CAAC,KAAK;CAGd;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,MAAM,EACnD,IAAI,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,GAC9C,iBAAiB,CAAC,CAAC,CAAC,CAqBtB;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAUnE"}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL client for the Sophos Fusion APIs (Cases v2, Detections v2, Threat
|
|
3
|
+
* Timeline v2, Events v1, Live Endpoint Search v1). Sits beside SophosClient,
|
|
4
|
+
* which keeps serving the Sophos Central REST APIs.
|
|
5
|
+
*
|
|
6
|
+
* One endpoint for every tenant, no regional host lookup. Auth is unchanged:
|
|
7
|
+
* the same bearer token from TokenManager and the same X-Tenant-ID header the
|
|
8
|
+
* REST client sends.
|
|
9
|
+
*
|
|
10
|
+
* The rule that shapes this file: a GraphQL-layer failure comes back as HTTP
|
|
11
|
+
* 200 with an `errors` array beside `data`. A 200 is not success until the
|
|
12
|
+
* body has been read. Transport failures (expired token, 429, 5xx) use the
|
|
13
|
+
* usual non-2xx status and the standard Sophos error object, and those are
|
|
14
|
+
* retried the same way SophosClient retries them. GraphQL-layer errors are
|
|
15
|
+
* not retried, with one measured exception: the intermittent
|
|
16
|
+
* partnerPreferences fault on the case write path, which is keyed on the
|
|
17
|
+
* error path and bounded (see FusionGraphQLError.isTransientPartnerPreferences).
|
|
18
|
+
*/
|
|
19
|
+
import { SOPHOS_FUSION_GRAPHQL_URL } from "../config/config.js";
|
|
20
|
+
const TRANSIENT_BACKOFF_CAP_MS = 2000;
|
|
21
|
+
/** Thrown when the GraphQL layer returns errors and no usable data. */
|
|
22
|
+
export class FusionGraphQLError extends Error {
|
|
23
|
+
errors;
|
|
24
|
+
constructor(message, errors) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.errors = errors;
|
|
27
|
+
this.name = "FusionGraphQLError";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* True when every error says the record does not exist. Fusion reports an
|
|
31
|
+
* unknown case ID this way, as HTTP 200 with "record not found" beside a
|
|
32
|
+
* null root field, rather than as a bare null (live tenant, 21/09/2026).
|
|
33
|
+
*/
|
|
34
|
+
get notFound() {
|
|
35
|
+
return (this.errors.length > 0 &&
|
|
36
|
+
this.errors.every((error) => /record not found/i.test(error.message ?? "")));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* True for the intermittent investigations-v2 fault on the case write
|
|
40
|
+
* path: `errors[0].path[0]` is "partnerPreferences" ("not allowed",
|
|
41
|
+
* DOWNSTREAM_SERVICE_ERROR). It is not an authorisation failure: the
|
|
42
|
+
* identical call with identical input succeeds on retry. Keyed on the path
|
|
43
|
+
* and nothing else, because `extensions.code` is DOWNSTREAM_SERVICE_ERROR
|
|
44
|
+
* for transient faults, not found and malformed queries alike, while a
|
|
45
|
+
* genuine input or permission error carries the operation name in the path
|
|
46
|
+
* (createCase, addEvidenceToCase, tdrusers) with a specific message.
|
|
47
|
+
*/
|
|
48
|
+
get isTransientPartnerPreferences() {
|
|
49
|
+
return this.errors[0]?.path?.[0] === "partnerPreferences";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export class FusionClient {
|
|
53
|
+
tokenManager;
|
|
54
|
+
url;
|
|
55
|
+
timeoutMs;
|
|
56
|
+
retries;
|
|
57
|
+
backoffBaseMs;
|
|
58
|
+
transientAttempts;
|
|
59
|
+
transientBackoffMs;
|
|
60
|
+
constructor(tokenManager, options = {}) {
|
|
61
|
+
this.tokenManager = tokenManager;
|
|
62
|
+
this.url = options.url ?? SOPHOS_FUSION_GRAPHQL_URL;
|
|
63
|
+
this.timeoutMs = options.timeoutMs ?? 30_000;
|
|
64
|
+
this.retries = options.retries ?? 2;
|
|
65
|
+
this.backoffBaseMs = options.backoffBaseMs ?? 1000;
|
|
66
|
+
this.transientAttempts = Math.max(1, options.transientAttempts ?? 20);
|
|
67
|
+
this.transientBackoffMs = options.transientBackoffMs ?? 300;
|
|
68
|
+
}
|
|
69
|
+
/** The endpoint this client posts to. */
|
|
70
|
+
get endpoint() {
|
|
71
|
+
return this.url;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Run a GraphQL query or mutation for a tenant.
|
|
75
|
+
*
|
|
76
|
+
* Returns the `data` object plus any partial-response warnings. Throws
|
|
77
|
+
* FusionGraphQLError when the response carries errors and no usable data,
|
|
78
|
+
* and a plain Error for transport failures. The transient partnerPreferences
|
|
79
|
+
* fault is retried up to transientAttempts times; every other GraphQL-layer
|
|
80
|
+
* error surfaces at once.
|
|
81
|
+
*/
|
|
82
|
+
async query(tenantId, document, variables = {}) {
|
|
83
|
+
const token = await this.tokenManager.getToken();
|
|
84
|
+
const fetchOptions = {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: {
|
|
87
|
+
Authorization: `Bearer ${token}`,
|
|
88
|
+
"X-Tenant-ID": tenantId,
|
|
89
|
+
"Content-Type": "application/json",
|
|
90
|
+
Accept: "application/json",
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify({ query: document, variables }),
|
|
93
|
+
};
|
|
94
|
+
for (let attempt = 1;; attempt++) {
|
|
95
|
+
const body = await this.executeWithRetry(fetchOptions);
|
|
96
|
+
try {
|
|
97
|
+
return interpretGraphQLBody(body);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (!(error instanceof FusionGraphQLError) || !error.isTransientPartnerPreferences) {
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
if (attempt >= this.transientAttempts) {
|
|
104
|
+
throw new FusionGraphQLError(`${error.message} (transient investigations-v2 fault persisted across ${attempt} attempts; the same call normally succeeds on retry, so try again)`, error.errors);
|
|
105
|
+
}
|
|
106
|
+
const cap = Math.min(this.transientBackoffMs * Math.pow(2, attempt - 1), TRANSIENT_BACKOFF_CAP_MS);
|
|
107
|
+
const backoff = Math.round(Math.random() * cap);
|
|
108
|
+
console.error(`[fusion-client] Transient partnerPreferences fault, retrying in ${backoff}ms (attempt ${attempt} of ${this.transientAttempts})`);
|
|
109
|
+
await this.sleep(backoff);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* PUT raw bytes to a presigned upload URL (case files). The signature lives
|
|
115
|
+
* in the URL, so no bearer token is sent, and the Content-Type must match
|
|
116
|
+
* what startCaseFileUpload was told. One attempt, no retry.
|
|
117
|
+
*/
|
|
118
|
+
async putPresigned(url, body, contentType) {
|
|
119
|
+
const controller = new AbortController();
|
|
120
|
+
const timeoutMs = Math.max(this.timeoutMs, 120_000);
|
|
121
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
122
|
+
let response;
|
|
123
|
+
try {
|
|
124
|
+
response = await fetch(url, {
|
|
125
|
+
method: "PUT",
|
|
126
|
+
headers: { "Content-Type": contentType },
|
|
127
|
+
// A fresh copy sits on a plain ArrayBuffer, which is what fetch's body type accepts.
|
|
128
|
+
body: new Uint8Array(body),
|
|
129
|
+
signal: controller.signal,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
134
|
+
throw new Error(`Case file upload timed out after ${timeoutMs}ms`);
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
clearTimeout(timeout);
|
|
140
|
+
}
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
const text = await response.text();
|
|
143
|
+
throw new Error(`Case file upload failed with HTTP ${response.status}: ${text.slice(0, 300)}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async executeWithRetry(options) {
|
|
147
|
+
let lastError = null;
|
|
148
|
+
for (let attempt = 0; attempt <= this.retries; attempt++) {
|
|
149
|
+
try {
|
|
150
|
+
const controller = new AbortController();
|
|
151
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
152
|
+
let response;
|
|
153
|
+
try {
|
|
154
|
+
response = await fetch(this.url, { ...options, signal: controller.signal });
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
clearTimeout(timeout);
|
|
158
|
+
}
|
|
159
|
+
if (response.status === 429) {
|
|
160
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
161
|
+
const waitMs = retryAfter
|
|
162
|
+
? parseInt(retryAfter, 10) * 1000
|
|
163
|
+
: this.backoffBaseMs * 5;
|
|
164
|
+
console.error(`[fusion-client] Rate limited, waiting ${waitMs}ms (attempt ${attempt + 1})`);
|
|
165
|
+
await this.sleep(waitMs);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
const errorBody = await response.text();
|
|
170
|
+
let parsed = null;
|
|
171
|
+
try {
|
|
172
|
+
parsed = JSON.parse(errorBody);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Not JSON
|
|
176
|
+
}
|
|
177
|
+
// A query the schema rejects comes back as HTTP 400 with a GraphQL
|
|
178
|
+
// errors array and no data (live tenant, 21/09/2026); keep its message.
|
|
179
|
+
const graphqlErrors = Array.isArray(parsed?.errors) ? parsed.errors : [];
|
|
180
|
+
const detail = graphqlErrors.length > 0
|
|
181
|
+
? graphqlErrors.map(formatGraphQLError).join("; ")
|
|
182
|
+
: parsed
|
|
183
|
+
? [parsed.error, parsed.message].filter(Boolean).join(" - ") || errorBody.slice(0, 500)
|
|
184
|
+
: errorBody.slice(0, 500);
|
|
185
|
+
const correlation = parsed?.correlationId ? ` (correlationId: ${parsed.correlationId})` : "";
|
|
186
|
+
throw new Error(`Sophos Fusion API error ${response.status}: ${detail}${correlation}`);
|
|
187
|
+
}
|
|
188
|
+
const text = await response.text();
|
|
189
|
+
try {
|
|
190
|
+
return JSON.parse(text);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
throw new Error(`Sophos Fusion API returned a non-JSON body with status ${response.status}: ${text.slice(0, 200)}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
lastError =
|
|
198
|
+
error instanceof Error && error.name === "AbortError"
|
|
199
|
+
? new Error(`Sophos Fusion API request timed out after ${this.timeoutMs}ms`)
|
|
200
|
+
: error instanceof Error
|
|
201
|
+
? error
|
|
202
|
+
: new Error(String(error));
|
|
203
|
+
// Don't retry on 4xx client errors
|
|
204
|
+
if (/Sophos Fusion API error 4\d\d:/.test(lastError.message)) {
|
|
205
|
+
throw lastError;
|
|
206
|
+
}
|
|
207
|
+
if (attempt < this.retries) {
|
|
208
|
+
// Full-jitter exponential backoff, as the Sophos rate-limit guidance recommends
|
|
209
|
+
const cap = this.backoffBaseMs * Math.pow(2, attempt);
|
|
210
|
+
const backoff = Math.round(Math.random() * cap);
|
|
211
|
+
console.error(`[fusion-client] Request failed, retrying in ${backoff}ms: ${lastError.message}`);
|
|
212
|
+
await this.sleep(backoff);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
throw lastError || new Error("Request failed after retries");
|
|
217
|
+
}
|
|
218
|
+
sleep(ms) {
|
|
219
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Decide what a 200 response actually means.
|
|
224
|
+
*
|
|
225
|
+
* - errors present, no usable data: throw with every message joined.
|
|
226
|
+
* - errors present beside usable data: return the data with the errors as warnings.
|
|
227
|
+
* - no errors, data present: clean result. A root field that is null with no
|
|
228
|
+
* error (for example `case` for an unknown ID) is legitimate and passes
|
|
229
|
+
* through for the tool to report.
|
|
230
|
+
* - no errors, no data: malformed, throw.
|
|
231
|
+
*/
|
|
232
|
+
export function interpretGraphQLBody(body) {
|
|
233
|
+
const errors = Array.isArray(body?.errors) ? body.errors : [];
|
|
234
|
+
const data = body?.data;
|
|
235
|
+
const hasUsableData = data !== null &&
|
|
236
|
+
data !== undefined &&
|
|
237
|
+
typeof data === "object" &&
|
|
238
|
+
Object.values(data).some((value) => value !== null && value !== undefined);
|
|
239
|
+
if (errors.length > 0 && !hasUsableData) {
|
|
240
|
+
throw new FusionGraphQLError(`Sophos Fusion GraphQL error: ${errors.map(formatGraphQLError).join("; ")}`, errors);
|
|
241
|
+
}
|
|
242
|
+
if (data === null || data === undefined || typeof data !== "object") {
|
|
243
|
+
throw new Error("Sophos Fusion API returned neither data nor errors");
|
|
244
|
+
}
|
|
245
|
+
return { data, warnings: errors.map(formatGraphQLError) };
|
|
246
|
+
}
|
|
247
|
+
export function formatGraphQLError(error) {
|
|
248
|
+
const parts = [error.message || "Unknown GraphQL error"];
|
|
249
|
+
if (error.path && error.path.length > 0) {
|
|
250
|
+
parts.push(`(path: ${error.path.join(".")})`);
|
|
251
|
+
}
|
|
252
|
+
const code = error.extensions?.code;
|
|
253
|
+
if (typeof code === "string" && code) {
|
|
254
|
+
parts.push(`[${code}]`);
|
|
255
|
+
}
|
|
256
|
+
return parts.join(" ");
|
|
257
|
+
}
|
|
258
|
+
//# sourceMappingURL=fusion-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fusion-client.js","sourceRoot":"","sources":["../../src/client/fusion-client.ts"],"names":[],"mappings":"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"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|