sophos-central-mcp-server 0.1.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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/dist/auth/token-manager.d.ts +19 -0
  4. package/dist/auth/token-manager.d.ts.map +1 -0
  5. package/dist/auth/token-manager.js +64 -0
  6. package/dist/auth/token-manager.js.map +1 -0
  7. package/dist/client/sophos-client.d.ts +33 -0
  8. package/dist/client/sophos-client.d.ts.map +1 -0
  9. package/dist/client/sophos-client.js +126 -0
  10. package/dist/client/sophos-client.js.map +1 -0
  11. package/dist/client/tenant-resolver.d.ts +58 -0
  12. package/dist/client/tenant-resolver.d.ts.map +1 -0
  13. package/dist/client/tenant-resolver.js +158 -0
  14. package/dist/client/tenant-resolver.js.map +1 -0
  15. package/dist/config/config.d.ts +18 -0
  16. package/dist/config/config.d.ts.map +1 -0
  17. package/dist/config/config.js +26 -0
  18. package/dist/config/config.js.map +1 -0
  19. package/dist/index.d.ts +9 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +100 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/tools/alerts.d.ts +9 -0
  24. package/dist/tools/alerts.d.ts.map +1 -0
  25. package/dist/tools/alerts.js +178 -0
  26. package/dist/tools/alerts.js.map +1 -0
  27. package/dist/tools/directory.d.ts +9 -0
  28. package/dist/tools/directory.d.ts.map +1 -0
  29. package/dist/tools/directory.js +194 -0
  30. package/dist/tools/directory.js.map +1 -0
  31. package/dist/tools/endpoints.d.ts +10 -0
  32. package/dist/tools/endpoints.d.ts.map +1 -0
  33. package/dist/tools/endpoints.js +295 -0
  34. package/dist/tools/endpoints.js.map +1 -0
  35. package/dist/tools/exclusions.d.ts +11 -0
  36. package/dist/tools/exclusions.d.ts.map +1 -0
  37. package/dist/tools/exclusions.js +455 -0
  38. package/dist/tools/exclusions.js.map +1 -0
  39. package/dist/tools/groups.d.ts +12 -0
  40. package/dist/tools/groups.d.ts.map +1 -0
  41. package/dist/tools/groups.js +328 -0
  42. package/dist/tools/groups.js.map +1 -0
  43. package/dist/tools/health.d.ts +9 -0
  44. package/dist/tools/health.d.ts.map +1 -0
  45. package/dist/tools/health.js +40 -0
  46. package/dist/tools/health.js.map +1 -0
  47. package/dist/tools/helpers.d.ts +24 -0
  48. package/dist/tools/helpers.d.ts.map +1 -0
  49. package/dist/tools/helpers.js +46 -0
  50. package/dist/tools/helpers.js.map +1 -0
  51. package/dist/tools/policies.d.ts +9 -0
  52. package/dist/tools/policies.d.ts.map +1 -0
  53. package/dist/tools/policies.js +250 -0
  54. package/dist/tools/policies.js.map +1 -0
  55. package/dist/tools/tenants.d.ts +8 -0
  56. package/dist/tools/tenants.d.ts.map +1 -0
  57. package/dist/tools/tenants.js +55 -0
  58. package/dist/tools/tenants.js.map +1 -0
  59. package/dist/types/sophos.d.ts +179 -0
  60. package/dist/types/sophos.d.ts.map +1 -0
  61. package/dist/types/sophos.js +5 -0
  62. package/dist/types/sophos.js.map +1 -0
  63. package/package.json +40 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # Sophos Central MCP Server
2
+
3
+ MCP (Model Context Protocol) server for interacting with Sophos Central APIs. Supports partner, organisation, and single-tenant credential types with automatic region routing.
4
+
5
+ ## Quick Start
6
+
7
+ No installation needed. Add the following to your `claude_desktop_config.json` (Claude Desktop) or equivalent MCP client config:
8
+
9
+ ```json
10
+ {
11
+ "mcpServers": {
12
+ "sophos-central": {
13
+ "command": "npx",
14
+ "args": ["-y", "sophos-central-mcp-server"],
15
+ "env": {
16
+ "SOPHOS_CLIENT_ID": "your-client-id",
17
+ "SOPHOS_CLIENT_SECRET": "your-client-secret",
18
+ "TRANSPORT": "stdio"
19
+ }
20
+ }
21
+ }
22
+ }
23
+ ```
24
+
25
+ Replace `your-client-id` and `your-client-secret` with your [Sophos Central API credentials](#creating-api-credentials). Claude Desktop will download and run the server automatically on first use.
26
+
27
+ ### Claude Code
28
+
29
+ ```bash
30
+ SOPHOS_CLIENT_ID=xxx SOPHOS_CLIENT_SECRET=yyy TRANSPORT=stdio \
31
+ claude mcp add sophos-central -- npx -y sophos-central-mcp-server
32
+ ```
33
+
34
+ ## Features
35
+
36
+ - **Universal caller support**: Works with partner, organisation, and tenant-level API credentials
37
+ - **Multi-tenant**: Partner/org callers can query across all managed tenants
38
+ - **Auto region routing**: Discovers tenant data regions via `/whoami/v1` and routes requests to the correct regional API host
39
+ - **Token lifecycle**: Automatic OAuth2 token refresh before expiry
40
+ - **Rate limit handling**: Retry with backoff on 429 responses
41
+ - **Dual transport**: Streamable HTTP (for Claude Desktop / Claude Code) or stdio
42
+
43
+ ## Prerequisites
44
+
45
+ - Node.js 20 or later
46
+ - Sophos Central API credentials (Client ID + Client Secret)
47
+
48
+ ### Creating API Credentials
49
+
50
+ **Tenant-level**: In Sophos Central, go to **Settings > API Credentials Management** and create a new credential.
51
+
52
+ **Partner-level**: In the Sophos Partner Dashboard, create API credentials under **Settings > API Credentials**.
53
+
54
+ **Organisation-level**: In Sophos Central Enterprise, use **Global Settings > API Credentials Management**.
55
+
56
+ ## Configuration
57
+
58
+ Copy `.env.example` to `.env` and set your credentials:
59
+
60
+ ```
61
+ SOPHOS_CLIENT_ID=your-client-id
62
+ SOPHOS_CLIENT_SECRET=your-client-secret
63
+ PORT=3100
64
+ TRANSPORT=http
65
+ ```
66
+
67
+ | Variable | Required | Default | Description |
68
+ |----------|----------|---------|-------------|
69
+ | `SOPHOS_CLIENT_ID` | Yes | - | OAuth2 client ID |
70
+ | `SOPHOS_CLIENT_SECRET` | Yes | - | OAuth2 client secret |
71
+ | `SOPHOS_TENANT_ID` | No | - | Lock to a single tenant (useful for tenant-level creds) |
72
+ | `PORT` | No | 3100 | HTTP server port |
73
+ | `TRANSPORT` | No | http | `http` for streamable HTTP, `stdio` for subprocess mode |
74
+
75
+ ## Tools
76
+
77
+ ### SOC Monitoring
78
+
79
+ | Tool | Description |
80
+ |------|-------------|
81
+ | `sophos_list_tenants` | List managed tenants (partner/org only) |
82
+ | `sophos_list_alerts` | List alerts with severity/category/product/date filters |
83
+ | `sophos_get_alert` | Get full alert detail with allowed actions |
84
+ | `sophos_acknowledge_alert` | Mark an alert as reviewed |
85
+ | `sophos_list_endpoints` | List endpoints with health/OS/hostname/isolation filters |
86
+ | `sophos_get_endpoint` | Get full endpoint detail |
87
+ | `sophos_scan_endpoint` | Trigger an on-demand scan |
88
+ | `sophos_isolate_endpoint` | Network-isolate a compromised endpoint |
89
+ | `sophos_release_endpoint` | Release an endpoint from isolation |
90
+ | `sophos_get_account_health` | Get tenant health check scores |
91
+ | `sophos_list_users` | List directory users |
92
+ | `sophos_list_admins` | List admin accounts and roles |
93
+
94
+ ### Tenant context
95
+
96
+ For **partner/org** callers, every tenant-scoped tool requires a `tenant_id` parameter. Use `sophos_list_tenants` first to discover available tenant IDs.
97
+
98
+ For **tenant-level** callers, `tenant_id` is optional and defaults to the authenticated tenant.
99
+
100
+ ## Architecture
101
+
102
+ ```
103
+ Authenticate (OAuth2 client credentials)
104
+ |
105
+ v
106
+ /whoami/v1 -> Discover identity type (partner | organization | tenant)
107
+ |
108
+ v
109
+ If partner/org: enumerate tenants, cache {tenantId -> apiHost}
110
+ |
111
+ v
112
+ Register tools based on identity type
113
+ |
114
+ v
115
+ Per tool call: resolve tenant -> regional API host -> execute request
116
+ ```
117
+
118
+ ### Key decisions
119
+
120
+ - **Dynamic tool registration**: Only tools valid for the caller type are exposed to the LLM
121
+ - **Explicit tenant context**: Partner/org callers must specify `tenant_id` to prevent cross-tenant accidents
122
+ - **Stateless HTTP**: Each MCP request creates a fresh transport instance (no session affinity)
123
+ - **Localhost binding**: HTTP server binds to `127.0.0.1` only
124
+
125
+ ## Project Structure
126
+
127
+ ```
128
+ src/
129
+ ├── index.ts # Entry point, server bootstrap
130
+ ├── config/config.ts # Environment config
131
+ ├── auth/token-manager.ts # OAuth2 token lifecycle
132
+ ├── client/
133
+ │ ├── sophos-client.ts # HTTP client with region routing
134
+ │ └── tenant-resolver.ts # Whoami + tenant cache
135
+ ├── tools/
136
+ │ ├── helpers.ts # Shared response formatting
137
+ │ ├── tenants.ts # sophos_list_tenants
138
+ │ ├── alerts.ts # sophos_list_alerts, get, acknowledge
139
+ │ ├── endpoints.ts # sophos_list/get/scan/isolate/release
140
+ │ ├── health.ts # sophos_get_account_health
141
+ │ └── directory.ts # sophos_list_users, list_admins
142
+ └── types/sophos.ts # Sophos API response types
143
+ ```
144
+
145
+ ## Roadmap
146
+
147
+ - **Phase 2 (Admin automation)**: Policies, endpoint groups, global exclusions, admin role management
148
+ - **Phase 3 (Investigation)**: XDR Data Lake queries, Live Discover, detections, cases, SIEM events
149
+
150
+ ## Security
151
+
152
+ - Credentials are read from environment variables only, never logged
153
+ - JWT tokens are held in memory with automatic refresh
154
+ - HTTP server binds to `127.0.0.1` (localhost only)
155
+ - Write actions have `destructiveHint` annotations so clients can warn users
156
+ - Partner/org callers require explicit `tenant_id` on every call
157
+
158
+ ## License
159
+
160
+ MIT
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Manages OAuth2 client credentials authentication with Sophos Central.
3
+ * Handles token acquisition and transparent refresh before expiry.
4
+ */
5
+ export declare class TokenManager {
6
+ private clientId;
7
+ private clientSecret;
8
+ private accessToken;
9
+ private expiresAt;
10
+ private refreshPromise;
11
+ constructor(clientId: string, clientSecret: string);
12
+ /**
13
+ * Returns a valid access token, refreshing if needed.
14
+ * Deduplicates concurrent refresh requests.
15
+ */
16
+ getToken(): Promise<string>;
17
+ private fetchToken;
18
+ }
19
+ //# sourceMappingURL=token-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-manager.d.ts","sourceRoot":"","sources":["../../src/auth/token-manager.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,qBAAa,YAAY;IAMrB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,YAAY;IANtB,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,cAAc,CAAgC;gBAG5C,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM;IAG9B;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC;YAoBnB,UAAU;CAqCzB"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Manages OAuth2 client credentials authentication with Sophos Central.
3
+ * Handles token acquisition and transparent refresh before expiry.
4
+ */
5
+ import { SOPHOS_AUTH_URL } from "../config/config.js";
6
+ export class TokenManager {
7
+ clientId;
8
+ clientSecret;
9
+ accessToken = null;
10
+ expiresAt = 0;
11
+ refreshPromise = null;
12
+ constructor(clientId, clientSecret) {
13
+ this.clientId = clientId;
14
+ this.clientSecret = clientSecret;
15
+ }
16
+ /**
17
+ * Returns a valid access token, refreshing if needed.
18
+ * Deduplicates concurrent refresh requests.
19
+ */
20
+ async getToken() {
21
+ // If token is valid for at least 60 more seconds, return it
22
+ if (this.accessToken && Date.now() < this.expiresAt - 60_000) {
23
+ return this.accessToken;
24
+ }
25
+ // Deduplicate concurrent refresh calls
26
+ if (this.refreshPromise) {
27
+ return this.refreshPromise;
28
+ }
29
+ this.refreshPromise = this.fetchToken();
30
+ try {
31
+ const token = await this.refreshPromise;
32
+ return token;
33
+ }
34
+ finally {
35
+ this.refreshPromise = null;
36
+ }
37
+ }
38
+ async fetchToken() {
39
+ const body = new URLSearchParams({
40
+ grant_type: "client_credentials",
41
+ client_id: this.clientId,
42
+ client_secret: this.clientSecret,
43
+ scope: "token",
44
+ });
45
+ const response = await fetch(SOPHOS_AUTH_URL, {
46
+ method: "POST",
47
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
48
+ body: body.toString(),
49
+ });
50
+ if (!response.ok) {
51
+ const errorText = await response.text();
52
+ throw new Error(`Sophos auth failed (${response.status}): ${errorText}`);
53
+ }
54
+ const data = (await response.json());
55
+ if (data.errorCode) {
56
+ throw new Error(`Sophos auth error: ${data.errorCode} - ${data.message}`);
57
+ }
58
+ this.accessToken = data.access_token;
59
+ this.expiresAt = Date.now() + data.expires_in * 1000;
60
+ console.error(`[sophos-auth] Token acquired, expires in ${data.expires_in}s`);
61
+ return this.accessToken;
62
+ }
63
+ }
64
+ //# sourceMappingURL=token-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-manager.js","sourceRoot":"","sources":["../../src/auth/token-manager.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGtD,MAAM,OAAO,YAAY;IAMb;IACA;IANF,WAAW,GAAkB,IAAI,CAAC;IAClC,SAAS,GAAW,CAAC,CAAC;IACtB,cAAc,GAA2B,IAAI,CAAC;IAEtD,YACU,QAAgB,EAChB,YAAoB;QADpB,aAAQ,GAAR,QAAQ,CAAQ;QAChB,iBAAY,GAAZ,YAAY,CAAQ;IAC3B,CAAC;IAEJ;;;OAGG;IACH,KAAK,CAAC,QAAQ;QACZ,4DAA4D;QAC5D,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,SAAS,GAAG,MAAM,EAAE,CAAC;YAC7D,OAAO,IAAI,CAAC,WAAW,CAAC;QAC1B,CAAC;QAED,uCAAuC;QACvC,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,OAAO,IAAI,CAAC,cAAc,CAAC;QAC7B,CAAC;QAED,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;QACxC,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC;YACxC,OAAO,KAAK,CAAC;QACf,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC7B,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,UAAU;QACtB,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC;YAC/B,UAAU,EAAE,oBAAoB;YAChC,SAAS,EAAE,IAAI,CAAC,QAAQ;YACxB,aAAa,EAAE,IAAI,CAAC,YAAY;YAChC,KAAK,EAAE,OAAO;SACf,CAAC,CAAC;QAEH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,eAAe,EAAE;YAC5C,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;YAChE,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE;SACtB,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;YACxC,MAAM,IAAI,KAAK,CACb,uBAAuB,QAAQ,CAAC,MAAM,MAAM,SAAS,EAAE,CACxD,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAwB,CAAC;QAE5D,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CACb,sBAAsB,IAAI,CAAC,SAAS,MAAM,IAAI,CAAC,OAAO,EAAE,CACzD,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,YAAY,CAAC;QACrC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QAErD,OAAO,CAAC,KAAK,CACX,4CAA4C,IAAI,CAAC,UAAU,GAAG,CAC/D,CAAC;QACF,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;CACF"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * HTTP client for Sophos Central APIs.
3
+ * Handles region-aware routing, auth headers, retries, and error mapping.
4
+ */
5
+ import type { TokenManager } from "../auth/token-manager.js";
6
+ import type { TenantResolver } from "./tenant-resolver.js";
7
+ export interface RequestOptions {
8
+ /** HTTP method */
9
+ method?: "GET" | "POST" | "PATCH" | "DELETE";
10
+ /** Query parameters */
11
+ params?: Record<string, string>;
12
+ /** JSON request body */
13
+ body?: unknown;
14
+ /** Additional headers */
15
+ headers?: Record<string, string>;
16
+ }
17
+ export declare class SophosClient {
18
+ private tokenManager;
19
+ private tenantResolver;
20
+ constructor(tokenManager: TokenManager, tenantResolver: TenantResolver);
21
+ /**
22
+ * Make a request to a tenant-scoped API endpoint.
23
+ * Automatically resolves the regional API host for the tenant.
24
+ */
25
+ tenantRequest<T>(tenantId: string, path: string, options?: RequestOptions): Promise<T>;
26
+ /**
27
+ * Make a request to a global API endpoint (partner/org level).
28
+ */
29
+ globalRequest<T>(path: string, options?: RequestOptions): Promise<T>;
30
+ private executeWithRetry;
31
+ private sleep;
32
+ }
33
+ //# sourceMappingURL=sophos-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sophos-client.d.ts","sourceRoot":"","sources":["../../src/client/sophos-client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,MAAM,WAAW,cAAc;IAC7B,kBAAkB;IAClB,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IAC7C,uBAAuB;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,wBAAwB;IACxB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,yBAAyB;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,qBAAa,YAAY;IAErB,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,cAAc;gBADd,YAAY,EAAE,YAAY,EAC1B,cAAc,EAAE,cAAc;IAGxC;;;OAGG;IACG,aAAa,CAAC,CAAC,EACnB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,CAAC,CAAC;IAgCb;;OAEG;IACG,aAAa,CAAC,CAAC,EACnB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,CAAC,CAAC;YAiCC,gBAAgB;IAqE9B,OAAO,CAAC,KAAK;CAGd"}
@@ -0,0 +1,126 @@
1
+ /**
2
+ * HTTP client for Sophos Central APIs.
3
+ * Handles region-aware routing, auth headers, retries, and error mapping.
4
+ */
5
+ export class SophosClient {
6
+ tokenManager;
7
+ tenantResolver;
8
+ constructor(tokenManager, tenantResolver) {
9
+ this.tokenManager = tokenManager;
10
+ this.tenantResolver = tenantResolver;
11
+ }
12
+ /**
13
+ * Make a request to a tenant-scoped API endpoint.
14
+ * Automatically resolves the regional API host for the tenant.
15
+ */
16
+ async tenantRequest(tenantId, path, options = {}) {
17
+ const apiHost = await this.tenantResolver.resolveApiHost(tenantId);
18
+ const token = await this.tokenManager.getToken();
19
+ const url = new URL(`${apiHost}${path}`);
20
+ if (options.params) {
21
+ for (const [key, value] of Object.entries(options.params)) {
22
+ if (value !== undefined && value !== "") {
23
+ url.searchParams.set(key, value);
24
+ }
25
+ }
26
+ }
27
+ const headers = {
28
+ Authorization: `Bearer ${token}`,
29
+ "X-Tenant-ID": tenantId,
30
+ "Content-Type": "application/json",
31
+ ...options.headers,
32
+ };
33
+ const fetchOptions = {
34
+ method: options.method || "GET",
35
+ headers,
36
+ };
37
+ if (options.body) {
38
+ fetchOptions.body = JSON.stringify(options.body);
39
+ }
40
+ return this.executeWithRetry(url.toString(), fetchOptions);
41
+ }
42
+ /**
43
+ * Make a request to a global API endpoint (partner/org level).
44
+ */
45
+ async globalRequest(path, options = {}) {
46
+ const identity = this.tenantResolver.getIdentity();
47
+ const token = await this.tokenManager.getToken();
48
+ const idHeader = this.tenantResolver.getIdHeader();
49
+ const url = new URL(`${identity.apiHosts.global}${path}`);
50
+ if (options.params) {
51
+ for (const [key, value] of Object.entries(options.params)) {
52
+ if (value !== undefined && value !== "") {
53
+ url.searchParams.set(key, value);
54
+ }
55
+ }
56
+ }
57
+ const headers = {
58
+ Authorization: `Bearer ${token}`,
59
+ [idHeader.name]: idHeader.value,
60
+ "Content-Type": "application/json",
61
+ ...options.headers,
62
+ };
63
+ const fetchOptions = {
64
+ method: options.method || "GET",
65
+ headers,
66
+ };
67
+ if (options.body) {
68
+ fetchOptions.body = JSON.stringify(options.body);
69
+ }
70
+ return this.executeWithRetry(url.toString(), fetchOptions);
71
+ }
72
+ async executeWithRetry(url, options, retries = 2) {
73
+ let lastError = null;
74
+ for (let attempt = 0; attempt <= retries; attempt++) {
75
+ try {
76
+ const response = await fetch(url, options);
77
+ if (response.status === 429) {
78
+ // Rate limited: wait and retry
79
+ const retryAfter = response.headers.get("Retry-After");
80
+ const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : 5000;
81
+ console.error(`[sophos-client] Rate limited, waiting ${waitMs}ms (attempt ${attempt + 1})`);
82
+ await this.sleep(waitMs);
83
+ continue;
84
+ }
85
+ if (!response.ok) {
86
+ const errorBody = await response.text();
87
+ let parsed = null;
88
+ try {
89
+ parsed = JSON.parse(errorBody);
90
+ }
91
+ catch {
92
+ // Not JSON
93
+ }
94
+ const msg = parsed
95
+ ? `Sophos API error: ${parsed.error} - ${parsed.message}${parsed.correlationId ? ` (correlationId: ${parsed.correlationId})` : ""}`
96
+ : `Sophos API error (${response.status}): ${errorBody.slice(0, 500)}`;
97
+ throw new Error(msg);
98
+ }
99
+ // Some endpoints return 204 No Content
100
+ if (response.status === 204) {
101
+ return {};
102
+ }
103
+ return (await response.json());
104
+ }
105
+ catch (error) {
106
+ lastError = error instanceof Error ? error : new Error(String(error));
107
+ // Don't retry on auth or client errors
108
+ if (lastError.message.includes("401") ||
109
+ lastError.message.includes("403") ||
110
+ lastError.message.includes("404")) {
111
+ throw lastError;
112
+ }
113
+ if (attempt < retries) {
114
+ const backoff = Math.pow(2, attempt) * 1000;
115
+ console.error(`[sophos-client] Request failed, retrying in ${backoff}ms: ${lastError.message}`);
116
+ await this.sleep(backoff);
117
+ }
118
+ }
119
+ }
120
+ throw lastError || new Error("Request failed after retries");
121
+ }
122
+ sleep(ms) {
123
+ return new Promise((resolve) => setTimeout(resolve, ms));
124
+ }
125
+ }
126
+ //# sourceMappingURL=sophos-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sophos-client.js","sourceRoot":"","sources":["../../src/client/sophos-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Discovers the caller's identity type via /whoami/v1 and resolves
3
+ * tenant IDs to their regional API hosts. Caches the mapping.
4
+ */
5
+ import type { TokenManager } from "../auth/token-manager.js";
6
+ import type { SophosIdType } from "../types/sophos.js";
7
+ export interface TenantInfo {
8
+ id: string;
9
+ name: string;
10
+ apiHost: string;
11
+ dataRegion: string;
12
+ dataGeography: string;
13
+ }
14
+ export interface CallerIdentity {
15
+ id: string;
16
+ idType: SophosIdType;
17
+ apiHosts: {
18
+ global: string;
19
+ dataRegion?: string;
20
+ };
21
+ }
22
+ export declare class TenantResolver {
23
+ private tokenManager;
24
+ private identity;
25
+ private tenantCache;
26
+ private tenantsLoaded;
27
+ constructor(tokenManager: TokenManager);
28
+ /**
29
+ * Initialise: calls /whoami/v1 and discovers the caller identity.
30
+ */
31
+ init(): Promise<CallerIdentity>;
32
+ getIdentity(): CallerIdentity;
33
+ /**
34
+ * Returns the ID type header name needed for partner/org API calls.
35
+ */
36
+ getIdHeader(): {
37
+ name: string;
38
+ value: string;
39
+ };
40
+ /**
41
+ * Loads all tenants for partner/org callers. Paginates through all pages.
42
+ */
43
+ loadTenants(): Promise<TenantInfo[]>;
44
+ /**
45
+ * Resolves a tenant ID to its API host. Loads tenants if not cached.
46
+ */
47
+ resolveApiHost(tenantId: string): Promise<string>;
48
+ /**
49
+ * Returns the tenant ID to use. For single-tenant callers, returns the
50
+ * identity ID. For partner/org callers, requires an explicit tenant ID.
51
+ */
52
+ resolveTenantId(providedTenantId?: string): string;
53
+ /**
54
+ * Returns all cached tenants.
55
+ */
56
+ getCachedTenants(): TenantInfo[];
57
+ }
58
+ //# sourceMappingURL=tenant-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-resolver.d.ts","sourceRoot":"","sources":["../../src/client/tenant-resolver.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EACV,YAAY,EAIb,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,YAAY,CAAC;IACrB,QAAQ,EAAE;QACR,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;CACH;AAED,qBAAa,cAAc;IAKb,OAAO,CAAC,YAAY;IAJhC,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,aAAa,CAAS;gBAEV,YAAY,EAAE,YAAY;IAE9C;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,cAAc,CAAC;IAqCrC,WAAW,IAAI,cAAc;IAO7B;;OAEG;IACH,WAAW,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE;IAY9C;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAoE1C;;OAEG;IACG,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmBvD;;;OAGG;IACH,eAAe,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM;IAiBlD;;OAEG;IACH,gBAAgB,IAAI,UAAU,EAAE;CAGjC"}
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Discovers the caller's identity type via /whoami/v1 and resolves
3
+ * tenant IDs to their regional API hosts. Caches the mapping.
4
+ */
5
+ import { SOPHOS_GLOBAL_API } from "../config/config.js";
6
+ export class TenantResolver {
7
+ tokenManager;
8
+ identity = null;
9
+ tenantCache = new Map();
10
+ tenantsLoaded = false;
11
+ constructor(tokenManager) {
12
+ this.tokenManager = tokenManager;
13
+ }
14
+ /**
15
+ * Initialise: calls /whoami/v1 and discovers the caller identity.
16
+ */
17
+ async init() {
18
+ const token = await this.tokenManager.getToken();
19
+ const response = await fetch(`${SOPHOS_GLOBAL_API}/whoami/v1`, {
20
+ headers: { Authorization: `Bearer ${token}` },
21
+ });
22
+ if (!response.ok) {
23
+ const errorText = await response.text();
24
+ throw new Error(`Whoami failed (${response.status}): ${errorText}`);
25
+ }
26
+ const data = (await response.json());
27
+ this.identity = {
28
+ id: data.id,
29
+ idType: data.idType,
30
+ apiHosts: data.apiHosts,
31
+ };
32
+ console.error(`[sophos-tenant] Identity: ${data.idType} (${data.id})`);
33
+ // For tenant-level callers, the data region host is returned directly
34
+ if (data.idType === "tenant" && data.apiHosts.dataRegion) {
35
+ this.tenantCache.set(data.id, {
36
+ id: data.id,
37
+ name: "self",
38
+ apiHost: data.apiHosts.dataRegion,
39
+ dataRegion: "self",
40
+ dataGeography: "unknown",
41
+ });
42
+ this.tenantsLoaded = true;
43
+ }
44
+ return this.identity;
45
+ }
46
+ getIdentity() {
47
+ if (!this.identity) {
48
+ throw new Error("TenantResolver not initialised. Call init() first.");
49
+ }
50
+ return this.identity;
51
+ }
52
+ /**
53
+ * Returns the ID type header name needed for partner/org API calls.
54
+ */
55
+ getIdHeader() {
56
+ const identity = this.getIdentity();
57
+ switch (identity.idType) {
58
+ case "partner":
59
+ return { name: "X-Partner-ID", value: identity.id };
60
+ case "organization":
61
+ return { name: "X-Organization-ID", value: identity.id };
62
+ case "tenant":
63
+ return { name: "X-Tenant-ID", value: identity.id };
64
+ }
65
+ }
66
+ /**
67
+ * Loads all tenants for partner/org callers. Paginates through all pages.
68
+ */
69
+ async loadTenants() {
70
+ const identity = this.getIdentity();
71
+ if (identity.idType === "tenant") {
72
+ return Array.from(this.tenantCache.values());
73
+ }
74
+ if (this.tenantsLoaded) {
75
+ return Array.from(this.tenantCache.values());
76
+ }
77
+ const apiPath = identity.idType === "partner"
78
+ ? "/partner/v1/tenants"
79
+ : "/organization/v1/tenants";
80
+ const idHeader = this.getIdHeader();
81
+ let page = 1;
82
+ let totalPages = 1;
83
+ while (page <= totalPages) {
84
+ const token = await this.tokenManager.getToken();
85
+ const url = new URL(`${SOPHOS_GLOBAL_API}${apiPath}`);
86
+ url.searchParams.set("page", String(page));
87
+ if (page === 1) {
88
+ url.searchParams.set("pageTotal", "true");
89
+ }
90
+ const response = await fetch(url.toString(), {
91
+ headers: {
92
+ Authorization: `Bearer ${token}`,
93
+ [idHeader.name]: idHeader.value,
94
+ },
95
+ });
96
+ if (!response.ok) {
97
+ const errorText = await response.text();
98
+ throw new Error(`List tenants failed (${response.status}): ${errorText}`);
99
+ }
100
+ const data = (await response.json());
101
+ if (page === 1 && data.pages.total) {
102
+ totalPages = data.pages.total;
103
+ }
104
+ for (const tenant of data.items) {
105
+ this.tenantCache.set(tenant.id, {
106
+ id: tenant.id,
107
+ name: tenant.name,
108
+ apiHost: tenant.apiHost,
109
+ dataRegion: tenant.dataRegion,
110
+ dataGeography: tenant.dataGeography,
111
+ });
112
+ }
113
+ page++;
114
+ }
115
+ this.tenantsLoaded = true;
116
+ console.error(`[sophos-tenant] Loaded ${this.tenantCache.size} tenants`);
117
+ return Array.from(this.tenantCache.values());
118
+ }
119
+ /**
120
+ * Resolves a tenant ID to its API host. Loads tenants if not cached.
121
+ */
122
+ async resolveApiHost(tenantId) {
123
+ if (this.tenantCache.has(tenantId)) {
124
+ return this.tenantCache.get(tenantId).apiHost;
125
+ }
126
+ // Try loading tenants if we haven't yet
127
+ if (!this.tenantsLoaded) {
128
+ await this.loadTenants();
129
+ }
130
+ const info = this.tenantCache.get(tenantId);
131
+ if (!info) {
132
+ throw new Error(`Tenant ${tenantId} not found. Use sophos_list_tenants to see available tenants.`);
133
+ }
134
+ return info.apiHost;
135
+ }
136
+ /**
137
+ * Returns the tenant ID to use. For single-tenant callers, returns the
138
+ * identity ID. For partner/org callers, requires an explicit tenant ID.
139
+ */
140
+ resolveTenantId(providedTenantId) {
141
+ const identity = this.getIdentity();
142
+ if (identity.idType === "tenant") {
143
+ return providedTenantId || identity.id;
144
+ }
145
+ if (!providedTenantId) {
146
+ throw new Error("tenant_id is required for partner/organization callers. " +
147
+ "Use sophos_list_tenants to find available tenant IDs.");
148
+ }
149
+ return providedTenantId;
150
+ }
151
+ /**
152
+ * Returns all cached tenants.
153
+ */
154
+ getCachedTenants() {
155
+ return Array.from(this.tenantCache.values());
156
+ }
157
+ }
158
+ //# sourceMappingURL=tenant-resolver.js.map