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.
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/dist/auth/token-manager.d.ts +19 -0
- package/dist/auth/token-manager.d.ts.map +1 -0
- package/dist/auth/token-manager.js +64 -0
- package/dist/auth/token-manager.js.map +1 -0
- package/dist/client/sophos-client.d.ts +33 -0
- package/dist/client/sophos-client.d.ts.map +1 -0
- package/dist/client/sophos-client.js +126 -0
- package/dist/client/sophos-client.js.map +1 -0
- package/dist/client/tenant-resolver.d.ts +58 -0
- package/dist/client/tenant-resolver.d.ts.map +1 -0
- package/dist/client/tenant-resolver.js +158 -0
- package/dist/client/tenant-resolver.js.map +1 -0
- package/dist/config/config.d.ts +18 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +26 -0
- package/dist/config/config.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +100 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/alerts.d.ts +9 -0
- package/dist/tools/alerts.d.ts.map +1 -0
- package/dist/tools/alerts.js +178 -0
- package/dist/tools/alerts.js.map +1 -0
- package/dist/tools/directory.d.ts +9 -0
- package/dist/tools/directory.d.ts.map +1 -0
- package/dist/tools/directory.js +194 -0
- package/dist/tools/directory.js.map +1 -0
- package/dist/tools/endpoints.d.ts +10 -0
- package/dist/tools/endpoints.d.ts.map +1 -0
- package/dist/tools/endpoints.js +295 -0
- package/dist/tools/endpoints.js.map +1 -0
- package/dist/tools/exclusions.d.ts +11 -0
- package/dist/tools/exclusions.d.ts.map +1 -0
- package/dist/tools/exclusions.js +455 -0
- package/dist/tools/exclusions.js.map +1 -0
- package/dist/tools/groups.d.ts +12 -0
- package/dist/tools/groups.d.ts.map +1 -0
- package/dist/tools/groups.js +328 -0
- package/dist/tools/groups.js.map +1 -0
- package/dist/tools/health.d.ts +9 -0
- package/dist/tools/health.d.ts.map +1 -0
- package/dist/tools/health.js +40 -0
- package/dist/tools/health.js.map +1 -0
- package/dist/tools/helpers.d.ts +24 -0
- package/dist/tools/helpers.d.ts.map +1 -0
- package/dist/tools/helpers.js +46 -0
- package/dist/tools/helpers.js.map +1 -0
- package/dist/tools/policies.d.ts +9 -0
- package/dist/tools/policies.d.ts.map +1 -0
- package/dist/tools/policies.js +250 -0
- package/dist/tools/policies.js.map +1 -0
- package/dist/tools/tenants.d.ts +8 -0
- package/dist/tools/tenants.d.ts.map +1 -0
- package/dist/tools/tenants.js +55 -0
- package/dist/tools/tenants.js.map +1 -0
- package/dist/types/sophos.d.ts +179 -0
- package/dist/types/sophos.d.ts.map +1 -0
- package/dist/types/sophos.js +5 -0
- package/dist/types/sophos.js.map +1 -0
- 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
|