@borgresearch/odin-mcp 0.1.0 → 0.2.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/README.md +82 -66
- package/dist/index.js +343 -101
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
# @borgresearch/odin-mcp
|
|
2
2
|
|
|
3
|
-
[Model Context Protocol](https://modelcontextprotocol.io) server for the [Odin](https://odin.borghq.io) security platform by Borg.
|
|
3
|
+
[Model Context Protocol](https://modelcontextprotocol.io) server for the [Odin](https://odin.borghq.io) security platform by Borg. It gives coding agents organisation-scoped finding context and a safe GitHub-first remediation loop.
|
|
4
|
+
|
|
5
|
+
The MCP can inspect a finding such as `ODI-12`, show the remediation and suggested branch, link an existing accessible GitHub pull request, and report automatic retest history. It never creates commits, pushes branches, or creates pull requests.
|
|
4
6
|
|
|
5
7
|
## Prerequisites
|
|
6
8
|
|
|
7
|
-
- Node.js
|
|
9
|
+
- Node.js 24 or newer.
|
|
8
10
|
- An Odin account with at least one organisation.
|
|
9
|
-
- An Odin API key
|
|
11
|
+
- An Odin API key. **Read only** is enough for discovery, remediation context, and retest reads. **Read & Write** is required for status changes and linking an existing pull request.
|
|
10
12
|
|
|
11
13
|
## Quick start
|
|
12
14
|
|
|
13
|
-
1.
|
|
14
|
-
2.
|
|
15
|
-
3.
|
|
15
|
+
1. Sign in to <https://odin.borghq.io>, open **Management > API Keys**, and create a key. The dialog defaults to **Read & Write** for the remediation workflow; choose **Read only** when the agent should only inspect data.
|
|
16
|
+
2. Copy the full key — it is shown once.
|
|
17
|
+
3. Add the server to your client:
|
|
16
18
|
|
|
17
19
|
```sh
|
|
18
20
|
ODIN_API_KEY=odin_... npx @borgresearch/odin-mcp
|
|
@@ -25,75 +27,85 @@ npm install -g @borgresearch/odin-mcp
|
|
|
25
27
|
ODIN_API_KEY=odin_... odin-mcp
|
|
26
28
|
```
|
|
27
29
|
|
|
30
|
+
Restart your MCP client after changing its configuration. Nine tools should be available after startup.
|
|
31
|
+
|
|
28
32
|
## Configuration
|
|
29
33
|
|
|
30
34
|
| Variable | Required | Notes |
|
|
31
35
|
|----------|----------|-------|
|
|
32
|
-
| `ODIN_API_KEY` | yes |
|
|
36
|
+
| `ODIN_API_KEY` | yes | An organisation-scoped key with `read`; `write` is required for mutations. |
|
|
37
|
+
| `ODIN_BASE_URL` | no | Odin API base URL. Defaults to `https://odin.borghq.io`. |
|
|
33
38
|
|
|
34
|
-
Each call sends `Authorization: Bearer ${ODIN_API_KEY}`. Keys
|
|
39
|
+
Each call sends `Authorization: Bearer ${ODIN_API_KEY}`. Keys belong to one organisation. If you belong to multiple organisations, create one key per organisation and run a separate MCP server entry for each.
|
|
35
40
|
|
|
36
41
|
## Tools
|
|
37
42
|
|
|
38
|
-
All tools accept
|
|
43
|
+
All tools accept one JSON object and return both a Markdown summary and a structured payload.
|
|
39
44
|
|
|
40
45
|
| Name | Permission | Description |
|
|
41
46
|
|------|------------|-------------|
|
|
42
|
-
| `list_findings` |
|
|
43
|
-
| `get_finding` |
|
|
44
|
-
| `update_finding_status` |
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
47
|
+
| `list_findings` | Read | List findings with pagination, severity, repository, current status, and title/identifier search. |
|
|
48
|
+
| `get_finding` | Read | Get bounded remediation context, revision content, suggested branch, linked PRs, workflow readiness, and latest retest state. |
|
|
49
|
+
| `update_finding_status` | Read & Write | Move a finding to `MITIGATING`, `OPEN_FOR_RETEST`, or `ACKNOWLEDGED`. Cookie-authenticated callers also need member role. |
|
|
50
|
+
| `link_finding_pull_request` | Read & Write | Link an existing accessible GitHub PR. It does not create, modify, merge, or push a PR. |
|
|
51
|
+
| `get_finding_retests` | Read | Read retest history; it never triggers a retest or spends credits. |
|
|
52
|
+
| `list_pentests` | Read | List pentests for the organisation. |
|
|
53
|
+
| `get_pentest` | Read | Get a pentest's status, schedule, and scope. |
|
|
54
|
+
| `list_assets` | Read | List attack-surface assets with filters and pagination. |
|
|
55
|
+
| `get_asset` | Read | Get an asset and the findings that reference it. |
|
|
56
|
+
|
|
57
|
+
### Finding references
|
|
49
58
|
|
|
50
|
-
|
|
59
|
+
Finding references are organisation-local. Prefer a human identifier such as `ODI-12`; an internal `findingId` is retained for chaining tool calls. Odin looks up the numeric suffix only within the organisation belonging to the API key and returns the canonical stored identifier. It never searches across organisations or reveals a cross-organisation match through an error.
|
|
51
60
|
|
|
52
|
-
|
|
61
|
+
### `list_findings`
|
|
53
62
|
|
|
54
|
-
`pentestId
|
|
63
|
+
Optional parameters include `pentestId`, `assetId`, `repository`, `severity` (`CRITICAL` | `HIGH` | `MEDIUM` | `LOW` | `INFORMATIONAL`), `status` (`REPORTED` | `ACKNOWLEDGED` | `MITIGATING` | `OPEN_FOR_RETEST` | `NEEDS_REVISION` | `FIXED_AND_RETESTED`), `search`, `sortBy` (`severity` | `createdAt`), `sortOrder`, `limit` (1–200, default 50), and `offset`.
|
|
55
64
|
|
|
56
|
-
|
|
57
|
-
> _"Search my findings for anything mentioning `S3`."_
|
|
65
|
+
### `get_finding`
|
|
58
66
|
|
|
59
|
-
|
|
67
|
+
Provide `identifier` or `findingId` (at least one is required). The response includes the current status and severity, revision summary/description/impact/details/remediation, affected assets, code reference where available, repository, linked GitHub PR metadata, `suggestedBranch`, retest state, and workflow blockers.
|
|
60
68
|
|
|
61
|
-
|
|
69
|
+
Example prompt:
|
|
62
70
|
|
|
63
|
-
>
|
|
71
|
+
> Explain `ODI-12`, show me the suggested branch, and tell me what must be ready before I open a PR.
|
|
64
72
|
|
|
65
|
-
|
|
73
|
+
### GitHub remediation loop
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
1. Ask `get_finding` about the human identifier.
|
|
76
|
+
2. Create the returned branch, make and test the fix, commit it, push it, and open the GitHub PR yourself in Cursor or your normal development environment.
|
|
77
|
+
3. Explicitly ask Odin to link that existing PR with `link_finding_pull_request` and its URL. The operation is idempotent and verifies that the organisation's GitHub App installation can access the repository.
|
|
78
|
+
4. When the linked PR merges, automatic webhook processing may queue a retest. Use `get_finding_retests` to read `QUEUED`, `RUNNING`, `COMPLETED`, `SKIPPED`, or `FAILED` entries and their sanitised verdicts.
|
|
68
79
|
|
|
69
|
-
|
|
70
|
-
> _"I've fixed `f_abc123`, mark it ready for retest."_
|
|
80
|
+
Odin does not infer permission from a matching PR. Ask before linking a PR or changing status. The MCP does not create commits, push branches, create PRs, merge PRs, or manually trigger paid retests.
|
|
71
81
|
|
|
72
|
-
|
|
82
|
+
### `update_finding_status`
|
|
73
83
|
|
|
74
|
-
|
|
84
|
+
Provide `identifier` or `findingId` and one of:
|
|
75
85
|
|
|
76
|
-
|
|
86
|
+
| Status | Use it when |
|
|
87
|
+
|--------|-------------|
|
|
88
|
+
| `MITIGATING` | You have started work on the fix. |
|
|
89
|
+
| `OPEN_FOR_RETEST` | The fix is ready for verification. |
|
|
90
|
+
| `ACKNOWLEDGED` | You accept the risk for now. |
|
|
77
91
|
|
|
78
|
-
|
|
79
|
-
> _"List all my pentests scheduled this quarter."_
|
|
92
|
+
A Read-only key receives an actionable 403. Create a new Read & Write key for mutations; existing key permissions are not silently upgraded. `NEEDS_REVISION` and `FIXED_AND_RETESTED` are controlled by Borg's retest process.
|
|
80
93
|
|
|
81
|
-
|
|
94
|
+
### `link_finding_pull_request`
|
|
82
95
|
|
|
83
|
-
|
|
96
|
+
Provide `identifier` or `findingId` and an existing GitHub pull request `url`. The GitHub integration must be active and able to access the repository. The tool returns the canonical finding reference, verified PR metadata, resulting status, and workflow readiness. It does not modify GitHub.
|
|
84
97
|
|
|
85
|
-
|
|
86
|
-
> _"Which manually added domains are currently down?"_
|
|
98
|
+
### `get_finding_retests`
|
|
87
99
|
|
|
88
|
-
|
|
100
|
+
Provide `identifier` or `findingId`. The tool is read-only and distinguishes no history from a history containing queued/running, completed, skipped, or failed runs. It does not call the manual retest endpoint and never spends credits.
|
|
89
101
|
|
|
90
|
-
|
|
102
|
+
### Other tools
|
|
91
103
|
|
|
92
|
-
|
|
104
|
+
`list_pentests` takes no parameters and `get_pentest` requires `pentestId`. `list_assets` and `get_asset` retain their existing type, source, status, search, sorting, and pagination parameters.
|
|
93
105
|
|
|
94
106
|
## Use with Claude Desktop
|
|
95
107
|
|
|
96
|
-
Add to
|
|
108
|
+
Add to `claude_desktop_config.json`:
|
|
97
109
|
|
|
98
110
|
```json
|
|
99
111
|
{
|
|
@@ -112,12 +124,23 @@ Add to your `claude_desktop_config.json`:
|
|
|
112
124
|
## Use with Claude Code
|
|
113
125
|
|
|
114
126
|
```sh
|
|
115
|
-
claude mcp add
|
|
127
|
+
claude mcp add --transport stdio --env ODIN_API_KEY=odin_... odin -- npx -y @borgresearch/odin-mcp
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Use with Codex
|
|
131
|
+
|
|
132
|
+
Add to `~/.codex/config.toml`, then restart Codex:
|
|
133
|
+
|
|
134
|
+
```toml
|
|
135
|
+
[mcp_servers.odin]
|
|
136
|
+
command = "npx"
|
|
137
|
+
args = ["-y", "@borgresearch/odin-mcp"]
|
|
138
|
+
env = { ODIN_API_KEY = "odin_..." }
|
|
116
139
|
```
|
|
117
140
|
|
|
118
141
|
## Use with Cursor
|
|
119
142
|
|
|
120
|
-
Open **Settings > MCP
|
|
143
|
+
Open **Settings > MCP**, or add this to `.cursor/mcp.json`:
|
|
121
144
|
|
|
122
145
|
```json
|
|
123
146
|
{
|
|
@@ -135,7 +158,7 @@ Open **Settings > MCP** and add a new server, or add the following to your proje
|
|
|
135
158
|
|
|
136
159
|
## Use with Windsurf
|
|
137
160
|
|
|
138
|
-
Add
|
|
161
|
+
Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
139
162
|
|
|
140
163
|
```json
|
|
141
164
|
{
|
|
@@ -153,7 +176,7 @@ Add the following to `~/.codeium/windsurf/mcp_config.json`:
|
|
|
153
176
|
|
|
154
177
|
## Use with VS Code (GitHub Copilot)
|
|
155
178
|
|
|
156
|
-
Add
|
|
179
|
+
Add to `.vscode/mcp.json`:
|
|
157
180
|
|
|
158
181
|
```json
|
|
159
182
|
{
|
|
@@ -171,35 +194,28 @@ Add the following to your project's `.vscode/mcp.json`:
|
|
|
171
194
|
|
|
172
195
|
## Use with Cline
|
|
173
196
|
|
|
174
|
-
Open **Settings > MCP Servers > Edit MCP Settings** and add
|
|
175
|
-
|
|
176
|
-
```json
|
|
177
|
-
{
|
|
178
|
-
"mcpServers": {
|
|
179
|
-
"odin": {
|
|
180
|
-
"command": "npx",
|
|
181
|
-
"args": ["-y", "@borgresearch/odin-mcp"],
|
|
182
|
-
"env": {
|
|
183
|
-
"ODIN_API_KEY": "odin_..."
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
```
|
|
197
|
+
Open **Settings > MCP Servers > Edit MCP Settings** and add the same `mcpServers.odin` JSON shape shown for Cursor. Pass `ODIN_API_KEY` in the server environment and restart the client after editing its MCP configuration.
|
|
189
198
|
|
|
190
199
|
## Troubleshooting
|
|
191
200
|
|
|
192
|
-
**`ODIN_API_KEY is not set or invalid`** —
|
|
201
|
+
**`ODIN_API_KEY is not set or invalid`** — check the key under **Management > API Keys**, confirm the client config sets `env.ODIN_API_KEY`, and restart the client.
|
|
193
202
|
|
|
194
|
-
**`API key does not have write permission`** — the
|
|
203
|
+
**`API key does not have write permission`** — the agent attempted a status change or PR link with a Read-only key. Create a new key with **Read & Write** permissions.
|
|
195
204
|
|
|
196
|
-
**`Finding not found`** — the
|
|
205
|
+
**`Finding not found`** — check the `ODI-12` reference and the organisation associated with the key. Keys and human identifiers are organisation-scoped by design.
|
|
197
206
|
|
|
198
|
-
|
|
207
|
+
**Retest history is unavailable** — the organisation's retest automation capability is gated or not configured. This does not enable manual retests; check the GitHub and automatic-retest settings.
|
|
199
208
|
|
|
200
|
-
**The server starts but no tools appear
|
|
209
|
+
**The server starts but no tools appear** — most clients require a full restart after changing configuration. Confirm Node.js 24 or newer is on the client's `PATH` and inspect its MCP logs.
|
|
201
210
|
|
|
202
|
-
|
|
211
|
+
## Development
|
|
212
|
+
|
|
213
|
+
```sh
|
|
214
|
+
pnpm test:run
|
|
215
|
+
pnpm typecheck
|
|
216
|
+
pnpm lint
|
|
217
|
+
pnpm build
|
|
218
|
+
```
|
|
203
219
|
|
|
204
220
|
## License
|
|
205
221
|
|
package/dist/index.js
CHANGED
|
@@ -27,6 +27,7 @@ var OdinClient = class {
|
|
|
27
27
|
baseUrl;
|
|
28
28
|
apiKey;
|
|
29
29
|
fetchImpl;
|
|
30
|
+
vercelProtectionBypassSecret;
|
|
30
31
|
constructor(options) {
|
|
31
32
|
if (!options.apiKey) {
|
|
32
33
|
throw new Error("OdinClient requires an apiKey");
|
|
@@ -37,6 +38,7 @@ var OdinClient = class {
|
|
|
37
38
|
""
|
|
38
39
|
);
|
|
39
40
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
41
|
+
this.vercelProtectionBypassSecret = options.vercelProtectionBypassSecret?.trim() || void 0;
|
|
40
42
|
}
|
|
41
43
|
async get(path, searchParams) {
|
|
42
44
|
const url = this.buildUrl(path, searchParams);
|
|
@@ -46,6 +48,8 @@ var OdinClient = class {
|
|
|
46
48
|
const url = this.buildUrl(path);
|
|
47
49
|
return this.request("PATCH", url, path, body);
|
|
48
50
|
}
|
|
51
|
+
// Public HTTP method API — kept alongside get/patch even when unused internally
|
|
52
|
+
// fallow-ignore-next-line unused-class-member
|
|
49
53
|
async post(path, body) {
|
|
50
54
|
const url = this.buildUrl(path);
|
|
51
55
|
return this.request("POST", url, path, body);
|
|
@@ -61,13 +65,17 @@ var OdinClient = class {
|
|
|
61
65
|
return url.toString();
|
|
62
66
|
}
|
|
63
67
|
async request(method, url, path, body) {
|
|
68
|
+
const headers = {
|
|
69
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
Accept: "application/json"
|
|
72
|
+
};
|
|
73
|
+
if (this.vercelProtectionBypassSecret) {
|
|
74
|
+
headers["x-vercel-protection-bypass"] = this.vercelProtectionBypassSecret;
|
|
75
|
+
}
|
|
64
76
|
const response = await this.fetchImpl(url, {
|
|
65
77
|
method,
|
|
66
|
-
headers
|
|
67
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
68
|
-
"Content-Type": "application/json",
|
|
69
|
-
Accept: "application/json"
|
|
70
|
-
},
|
|
78
|
+
headers,
|
|
71
79
|
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
72
80
|
});
|
|
73
81
|
const rawText = await response.text();
|
|
@@ -157,6 +165,21 @@ function extractServerMessage(parsed) {
|
|
|
157
165
|
import { z } from "zod";
|
|
158
166
|
|
|
159
167
|
// src/formatters/markdown.ts
|
|
168
|
+
var STATUS_LABELS = {
|
|
169
|
+
REPORTED: "Reported",
|
|
170
|
+
ACKNOWLEDGED: "Acknowledged",
|
|
171
|
+
MITIGATING: "Mitigating",
|
|
172
|
+
OPEN_FOR_RETEST: "Open for Retest",
|
|
173
|
+
NEEDS_REVISION: "Needs Revision",
|
|
174
|
+
FIXED_AND_RETESTED: "Fixed & Retested"
|
|
175
|
+
};
|
|
176
|
+
var RETEST_STATUS_LABELS = {
|
|
177
|
+
QUEUED: "Queued",
|
|
178
|
+
RUNNING: "Running",
|
|
179
|
+
COMPLETED: "Completed",
|
|
180
|
+
FAILED: "Failed",
|
|
181
|
+
SKIPPED: "Skipped"
|
|
182
|
+
};
|
|
160
183
|
function formatFindingsList(data) {
|
|
161
184
|
const { findings, stats, pagination } = data;
|
|
162
185
|
if (findings.length === 0) {
|
|
@@ -164,60 +187,159 @@ function formatFindingsList(data) {
|
|
|
164
187
|
}
|
|
165
188
|
const rows = findings.map((f) => {
|
|
166
189
|
const severity = `${f.severity.label} (CVSS ${f.severity.cvss.toFixed(1)})`;
|
|
167
|
-
return `| ${f.findingId} | ${escapePipes(f.title)} | ${severity} | ${f.status} | ${formatDate(f.createdAt)} |`;
|
|
190
|
+
return `| ${f.identifier ?? "\u2014"} | ${f.findingId} | ${escapePipes(f.title)} | ${severity} | ${formatStatus(f.status)} | ${formatDate(f.createdAt)} |`;
|
|
168
191
|
});
|
|
169
192
|
return [
|
|
170
193
|
`Showing ${findings.length} of ${stats.total} finding(s). Offset ${pagination.offset}, limit ${pagination.limit}. hasMore=${pagination.hasMore}.`,
|
|
171
194
|
"",
|
|
172
|
-
"|
|
|
173
|
-
"| --- | --- | --- | --- | --- |",
|
|
195
|
+
"| Identifier | Internal ID | Title | Severity | Status | Created |",
|
|
196
|
+
"| --- | --- | --- | --- | --- | --- |",
|
|
174
197
|
...rows
|
|
175
198
|
].join("\n");
|
|
176
199
|
}
|
|
177
200
|
function formatFindingDetail(data) {
|
|
178
201
|
const { finding, revision } = data;
|
|
202
|
+
const context = data.remediationContext;
|
|
203
|
+
const identifier = context?.identifier ?? finding.identifier ?? null;
|
|
204
|
+
const displayIdentifier = identifier ?? finding.findingId;
|
|
179
205
|
const severity = `${finding.severity.label} (CVSS ${finding.severity.cvss.toFixed(1)})`;
|
|
180
206
|
const header = [
|
|
181
|
-
`# ${finding.title}`,
|
|
207
|
+
`# ${escapePipes(displayIdentifier)} \u2014 ${escapePipes(finding.title)}`,
|
|
182
208
|
"",
|
|
183
|
-
`- **
|
|
209
|
+
`- **Finding:** ${displayIdentifier}`,
|
|
210
|
+
`- **Internal ID:** ${finding.findingId}`,
|
|
184
211
|
`- **Severity:** ${severity}`,
|
|
185
|
-
`- **Status:** ${finding.status}`,
|
|
212
|
+
`- **Status:** ${formatStatus(finding.status)}`,
|
|
213
|
+
`- **Repository:** ${context?.repository ?? finding.repository ?? "Not recorded"}`,
|
|
186
214
|
`- **Pentest:** ${finding.context?.pentestId ?? "standalone"}`,
|
|
187
215
|
`- **Created:** ${formatDate(finding.createdAt)}`,
|
|
188
216
|
`- **Updated:** ${formatDate(finding.updatedAt)}`
|
|
189
217
|
];
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
218
|
+
const content = context ? {
|
|
219
|
+
revisionNumber: context.revision.revisionNumber,
|
|
220
|
+
summary: context.revision.summary,
|
|
221
|
+
description: context.revision.description,
|
|
222
|
+
impact: context.revision.impact,
|
|
223
|
+
details: context.revision.details,
|
|
224
|
+
remediation: context.revision.remediation,
|
|
225
|
+
affectedAssets: context.revision.affectedAssets,
|
|
226
|
+
codeReference: context.revision.codeReference
|
|
227
|
+
} : revision ? {
|
|
228
|
+
revisionNumber: revision.revisionNumber,
|
|
229
|
+
summary: revision.content.summaryMd,
|
|
230
|
+
description: revision.content.descriptionMd,
|
|
231
|
+
impact: revision.content.businessImpactMd,
|
|
232
|
+
details: revision.content.detailsMd,
|
|
233
|
+
remediation: revision.content.remediationMd,
|
|
234
|
+
affectedAssets: revision.content.affectedAssets ?? [],
|
|
235
|
+
codeReference: null
|
|
236
|
+
} : null;
|
|
194
237
|
const sections = [];
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
sections.push(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
238
|
+
if (!content) {
|
|
239
|
+
sections.push("_No revision content available._");
|
|
240
|
+
} else {
|
|
241
|
+
sections.push(`_Revision ${content.revisionNumber ?? "unknown"}_`);
|
|
242
|
+
if (content.summary) sections.push("## Summary", content.summary);
|
|
243
|
+
if (content.description)
|
|
244
|
+
sections.push("## Description", content.description);
|
|
245
|
+
if (content.impact) sections.push("## Business Impact", content.impact);
|
|
246
|
+
if (content.details) sections.push("## Details", content.details);
|
|
247
|
+
if (content.remediation)
|
|
248
|
+
sections.push("## Remediation", content.remediation);
|
|
249
|
+
if (content.codeReference) {
|
|
250
|
+
sections.push(
|
|
251
|
+
"## Code reference",
|
|
252
|
+
`- ${content.codeReference.file}:${content.codeReference.line}`
|
|
253
|
+
);
|
|
254
|
+
if (content.codeReference.snippet) {
|
|
255
|
+
sections.push("```", content.codeReference.snippet, "```");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (content.affectedAssets?.length) {
|
|
259
|
+
sections.push(
|
|
260
|
+
"## Affected Assets",
|
|
261
|
+
content.affectedAssets.map((asset) => `- ${asset.display} (${asset.assetId})`).join("\n")
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (context) {
|
|
202
266
|
sections.push(
|
|
203
|
-
"##
|
|
204
|
-
|
|
267
|
+
"## Remediation workflow",
|
|
268
|
+
`- **Suggested branch:** ${context.suggestedBranch ?? "Not available"}`,
|
|
269
|
+
`- **Ready for a user-created PR:** ${yesNo(context.workflow.readyForUserCreatedPullRequest)}`,
|
|
270
|
+
`- **GitHub connected:** ${yesNo(context.workflow.githubConnected)}`,
|
|
271
|
+
`- **Automatic retests:** ${context.workflow.automaticRetestsReady ? "Ready after merge" : "Not ready"}`
|
|
205
272
|
);
|
|
273
|
+
if (context.workflow.blockers.length) {
|
|
274
|
+
sections.push(
|
|
275
|
+
"### Blockers",
|
|
276
|
+
context.workflow.blockers.map((blocker) => `- ${blocker}`).join("\n")
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
if (context.workflow.nextSteps.length) {
|
|
280
|
+
sections.push(
|
|
281
|
+
"### Next steps",
|
|
282
|
+
context.workflow.nextSteps.map((step) => `- ${step}`).join("\n")
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
if (context.linkedPullRequests.length) {
|
|
286
|
+
sections.push(
|
|
287
|
+
"## Linked GitHub pull requests",
|
|
288
|
+
context.linkedPullRequests.map(
|
|
289
|
+
(pullRequest) => `- [#${pullRequest.number}](${pullRequest.url}) ${escapePipes(pullRequest.title)} \u2014 ${pullRequest.state}, linked via ${pullRequest.linkedVia}`
|
|
290
|
+
).join("\n")
|
|
291
|
+
);
|
|
292
|
+
} else {
|
|
293
|
+
sections.push("## Linked GitHub pull requests", "_None linked._");
|
|
294
|
+
}
|
|
295
|
+
sections.push("## Latest retest");
|
|
296
|
+
if (context.latestRetest.state === "unavailable") {
|
|
297
|
+
sections.push(`_${context.latestRetest.reason}_`);
|
|
298
|
+
} else if (context.latestRetest.state === "none") {
|
|
299
|
+
sections.push("_No retest history is available yet._");
|
|
300
|
+
} else {
|
|
301
|
+
sections.push(formatRetestSummary(context.latestRetest.retest));
|
|
302
|
+
}
|
|
206
303
|
}
|
|
207
|
-
return [
|
|
208
|
-
...header,
|
|
209
|
-
"",
|
|
210
|
-
`_Revision ${revision.revisionNumber}_`,
|
|
211
|
-
"",
|
|
212
|
-
...sections
|
|
213
|
-
].join("\n");
|
|
304
|
+
return [...header, "", ...sections].join("\n");
|
|
214
305
|
}
|
|
215
306
|
function formatFindingStatusUpdate(finding) {
|
|
307
|
+
const identifier = finding.identifier ?? finding.findingId;
|
|
216
308
|
return [
|
|
217
|
-
`Updated finding **${finding.findingId}
|
|
309
|
+
`Updated finding **${identifier}** (internal ID ${finding.findingId}) status to **${formatStatus(finding.status)}**.`,
|
|
218
310
|
`Title: ${finding.title}`
|
|
219
311
|
].join("\n");
|
|
220
312
|
}
|
|
313
|
+
function formatFindingPullRequestLink(data) {
|
|
314
|
+
const identifier = data.finding.identifier ?? data.finding.findingId;
|
|
315
|
+
const pullRequest = data.pullRequest;
|
|
316
|
+
const readiness = data.workflow.automaticRetestsReady ? "Automatic retesting is ready after this PR merges." : "Automatic retesting is not currently ready; review the blockers below.";
|
|
317
|
+
const blockers = data.workflow.blockers.length ? `
|
|
318
|
+
${data.workflow.blockers.map((blocker) => `- ${blocker}`).join("\n")}` : "";
|
|
319
|
+
return [
|
|
320
|
+
`Linked existing GitHub pull request [#${pullRequest.number}](${pullRequest.url}) to **${identifier}**.`,
|
|
321
|
+
`Repository: ${pullRequest.repo}; state: ${pullRequest.state}; finding status: ${formatStatus(data.finding.status)}.`,
|
|
322
|
+
"Odin did not create, modify, merge, or push this pull request.",
|
|
323
|
+
readiness,
|
|
324
|
+
blockers ? `Workflow blockers:${blockers}` : ""
|
|
325
|
+
].filter(Boolean).join("\n");
|
|
326
|
+
}
|
|
327
|
+
function formatFindingRetests(data) {
|
|
328
|
+
if (data.state === "none" || data.retests.length === 0) {
|
|
329
|
+
return "No retest history is available for this finding.";
|
|
330
|
+
}
|
|
331
|
+
return [
|
|
332
|
+
`Retest history (${data.retests.length}):`,
|
|
333
|
+
"",
|
|
334
|
+
"| Retest | Status | Trigger | Verdict | Created | Completed |",
|
|
335
|
+
"| --- | --- | --- | --- | --- | --- |",
|
|
336
|
+
...data.retests.map(
|
|
337
|
+
(retest) => `| ${retest.retestId} | ${RETEST_STATUS_LABELS[retest.status]} | ${retest.trigger} | ${retest.combinedVerdict ?? "\u2014"} | ${formatDate(retest.createdAt)} | ${formatDate(retest.completedAt ?? 0)} |`
|
|
338
|
+
),
|
|
339
|
+
"",
|
|
340
|
+
"Queued and running entries are still in progress; completed, skipped, and failed entries are final for that run."
|
|
341
|
+
].join("\n");
|
|
342
|
+
}
|
|
221
343
|
function formatPentestsList(data) {
|
|
222
344
|
if (data.pentests.length === 0) return "No pentests found.";
|
|
223
345
|
const rows = data.pentests.map(formatPentestRow);
|
|
@@ -245,10 +367,15 @@ function formatPentestDetail(data) {
|
|
|
245
367
|
scope
|
|
246
368
|
].join("\n");
|
|
247
369
|
}
|
|
248
|
-
function
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
370
|
+
function formatFindingsForAsset(findings) {
|
|
371
|
+
if (findings.length === 0) return "_No findings reference this asset._";
|
|
372
|
+
return [
|
|
373
|
+
`## Findings (${findings.length})`,
|
|
374
|
+
"",
|
|
375
|
+
"| Identifier | Internal ID | Title | Severity | Status | Created |",
|
|
376
|
+
"| --- | --- | --- | --- | --- | --- |",
|
|
377
|
+
...findings.map(formatFindingRow)
|
|
378
|
+
].join("\n");
|
|
252
379
|
}
|
|
253
380
|
function formatAssetsList(data) {
|
|
254
381
|
const { assets, stats, pagination } = data;
|
|
@@ -274,26 +401,36 @@ function formatAssetDetail(data) {
|
|
|
274
401
|
`- **Last seen:** ${formatDate(asset.lastSeenAt)}`,
|
|
275
402
|
`- **Created:** ${formatDate(asset.createdAt)}`
|
|
276
403
|
];
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
`## Findings (${findings.length})`,
|
|
288
|
-
"",
|
|
289
|
-
"| Finding ID | Title | Severity | Status | Created |",
|
|
290
|
-
"| --- | --- | --- | --- | --- |",
|
|
291
|
-
...rows
|
|
292
|
-
].join("\n");
|
|
404
|
+
return [...header, "", formatFindingsForAsset(findings)].join("\n");
|
|
405
|
+
}
|
|
406
|
+
function formatFindingRow(finding) {
|
|
407
|
+
const severity = `${finding.severity.label} (CVSS ${finding.severity.cvss.toFixed(1)})`;
|
|
408
|
+
return `| ${finding.identifier ?? "\u2014"} | ${finding.findingId} | ${escapePipes(finding.title)} | ${severity} | ${formatStatus(finding.status)} | ${formatDate(finding.createdAt)} |`;
|
|
409
|
+
}
|
|
410
|
+
function formatPentestRow(p) {
|
|
411
|
+
const end = p.endAt ? formatDate(p.endAt) : "open";
|
|
412
|
+
const scope = p.scope.slice(0, 3).join(", ") + (p.scope.length > 3 ? ", \u2026" : "");
|
|
413
|
+
return `| ${p.pentestId} | ${escapePipes(p.name)} | ${p.status} | ${formatDate(p.startAt)} | ${end} | ${escapePipes(scope)} |`;
|
|
293
414
|
}
|
|
294
415
|
function formatAssetRow(a) {
|
|
295
416
|
return `| ${a.assetId} | ${a.type} | ${escapePipes(a.value)} | ${a.source} | ${a.status} | ${formatDate(a.lastSeenAt)} |`;
|
|
296
417
|
}
|
|
418
|
+
function formatRetestSummary(retest) {
|
|
419
|
+
return [
|
|
420
|
+
`- **Status:** ${RETEST_STATUS_LABELS[retest.status]}`,
|
|
421
|
+
`- **Trigger:** ${retest.trigger}`,
|
|
422
|
+
`- **Verdict:** ${retest.combinedVerdict ?? "Not available yet"}`,
|
|
423
|
+
`- **Finding action:** ${retest.statusAction ?? "None"}`,
|
|
424
|
+
`- **Created:** ${formatDate(retest.createdAt)}`,
|
|
425
|
+
`- **Completed:** ${formatDate(retest.completedAt ?? 0)}`
|
|
426
|
+
].join("\n");
|
|
427
|
+
}
|
|
428
|
+
function formatStatus(status) {
|
|
429
|
+
return STATUS_LABELS[status] ?? status;
|
|
430
|
+
}
|
|
431
|
+
function yesNo(value) {
|
|
432
|
+
return value ? "Yes" : "No";
|
|
433
|
+
}
|
|
297
434
|
function escapePipes(value) {
|
|
298
435
|
return value.replace(/\|/g, "\\|");
|
|
299
436
|
}
|
|
@@ -323,34 +460,101 @@ var getAssetTool = {
|
|
|
323
460
|
}
|
|
324
461
|
};
|
|
325
462
|
|
|
326
|
-
// src/tools/
|
|
463
|
+
// src/tools/finding-reference.ts
|
|
327
464
|
import { z as z2 } from "zod";
|
|
328
|
-
var
|
|
329
|
-
|
|
465
|
+
var findingReferenceFields = {
|
|
466
|
+
identifier: z2.string().trim().min(1).optional().describe(
|
|
467
|
+
'Human-readable finding identifier such as "ODI-12". Preferred input.'
|
|
468
|
+
),
|
|
469
|
+
findingId: z2.string().trim().min(1).optional().describe(
|
|
470
|
+
"Raw internal finding ID, useful when chaining from another tool."
|
|
471
|
+
)
|
|
472
|
+
};
|
|
473
|
+
var findingReferenceInput = z2.object(findingReferenceFields).refine((value) => Boolean(value.identifier || value.findingId), {
|
|
474
|
+
message: "Provide either identifier or findingId."
|
|
330
475
|
});
|
|
476
|
+
function findingReferenceValue(reference) {
|
|
477
|
+
const value = reference.identifier ?? reference.findingId;
|
|
478
|
+
if (!value) {
|
|
479
|
+
throw new Error("A finding identifier or findingId is required.");
|
|
480
|
+
}
|
|
481
|
+
return value;
|
|
482
|
+
}
|
|
483
|
+
function findingReferencePath(reference, suffix = "") {
|
|
484
|
+
return `/api/findings/${encodeURIComponent(findingReferenceValue(reference))}${suffix}`;
|
|
485
|
+
}
|
|
486
|
+
async function resolveFindingId(client, reference) {
|
|
487
|
+
if (reference.findingId) return reference.findingId;
|
|
488
|
+
const data = await client.get(
|
|
489
|
+
findingReferencePath(reference)
|
|
490
|
+
);
|
|
491
|
+
return data.remediationContext?.findingId ?? data.finding.findingId;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/tools/get-finding.ts
|
|
495
|
+
var getFindingInput = findingReferenceInput;
|
|
331
496
|
var getFindingTool = {
|
|
332
497
|
name: "get_finding",
|
|
333
|
-
title: "Get finding",
|
|
334
|
-
description: "Fetch a
|
|
498
|
+
title: "Get finding remediation context",
|
|
499
|
+
description: "Fetch a finding by human identifier such as ODI-12 (preferred) or by internal findingId. Returns bounded remediation context, the suggested branch name, linked GitHub pull requests, workflow readiness, and retest state. Odin only provides context and can link an existing PR; it never creates commits, pushes branches, or creates pull requests.",
|
|
335
500
|
inputSchema: getFindingInput,
|
|
336
501
|
handler: (client) => async (params) => {
|
|
337
502
|
const data = await client.get(
|
|
338
|
-
|
|
503
|
+
findingReferencePath(params)
|
|
339
504
|
);
|
|
340
505
|
return {
|
|
341
506
|
markdown: formatFindingDetail(data),
|
|
342
507
|
structured: {
|
|
343
|
-
finding: data.finding,
|
|
344
|
-
revision: data.revision
|
|
508
|
+
finding: data.remediationContext ?? data.finding,
|
|
509
|
+
revision: data.remediationContext?.revision ?? data.revision,
|
|
510
|
+
remediationContext: data.remediationContext ?? null
|
|
345
511
|
}
|
|
346
512
|
};
|
|
347
513
|
}
|
|
348
514
|
};
|
|
349
515
|
|
|
350
|
-
// src/tools/get-
|
|
516
|
+
// src/tools/get-finding-retests.ts
|
|
351
517
|
import { z as z3 } from "zod";
|
|
352
|
-
var
|
|
353
|
-
|
|
518
|
+
var getFindingRetestsInput = z3.object(findingReferenceFields).refine((value) => Boolean(value.identifier || value.findingId), {
|
|
519
|
+
message: "Provide either identifier or findingId."
|
|
520
|
+
});
|
|
521
|
+
var getFindingRetestsTool = {
|
|
522
|
+
name: "get_finding_retests",
|
|
523
|
+
title: "Get finding retest history",
|
|
524
|
+
description: "Read the retest history for a finding by human identifier such as ODI-12 or internal findingId. Distinguishes no history, queued or running, completed, skipped, failed, and gated/unavailable results. This tool never triggers a retest or spends credits.",
|
|
525
|
+
inputSchema: getFindingRetestsInput,
|
|
526
|
+
handler: (client) => async (params) => {
|
|
527
|
+
try {
|
|
528
|
+
const data = await client.get(
|
|
529
|
+
findingReferencePath(params, "/retests")
|
|
530
|
+
);
|
|
531
|
+
return {
|
|
532
|
+
markdown: formatFindingRetests(data),
|
|
533
|
+
structured: {
|
|
534
|
+
state: data.state,
|
|
535
|
+
retests: data.retests
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
} catch (error) {
|
|
539
|
+
if (error instanceof OdinApiError && error.status === 404 && isRetestHistoryUnavailable(error.body)) {
|
|
540
|
+
const reason = "Retest history is gated or unavailable for this organisation.";
|
|
541
|
+
return {
|
|
542
|
+
markdown: reason,
|
|
543
|
+
structured: { state: "unavailable", reason, retests: [] }
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
function isRetestHistoryUnavailable(body) {
|
|
551
|
+
return typeof body === "object" && body !== null && "code" in body && body.code === "RETEST_HISTORY_UNAVAILABLE";
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/tools/get-pentest.ts
|
|
555
|
+
import { z as z4 } from "zod";
|
|
556
|
+
var getPentestInput = z4.object({
|
|
557
|
+
pentestId: z4.string().min(1)
|
|
354
558
|
});
|
|
355
559
|
var getPentestTool = {
|
|
356
560
|
name: "get_pentest",
|
|
@@ -368,19 +572,49 @@ var getPentestTool = {
|
|
|
368
572
|
}
|
|
369
573
|
};
|
|
370
574
|
|
|
575
|
+
// src/tools/link-finding-pull-request.ts
|
|
576
|
+
import { z as z5 } from "zod";
|
|
577
|
+
var linkFindingPullRequestInput = z5.object({
|
|
578
|
+
...findingReferenceFields,
|
|
579
|
+
url: z5.string().url().describe("Existing GitHub pull request URL to link to the finding.")
|
|
580
|
+
}).refine((value) => Boolean(value.identifier || value.findingId), {
|
|
581
|
+
message: "Provide either identifier or findingId."
|
|
582
|
+
});
|
|
583
|
+
var linkFindingPullRequestTool = {
|
|
584
|
+
name: "link_finding_pull_request",
|
|
585
|
+
title: "Link an existing GitHub pull request",
|
|
586
|
+
description: "Link an existing accessible GitHub pull request to a finding identified by ODI-12 or findingId. This operation is idempotent. It does not create, modify, merge, or push a pull request, commit, or branch, and it requires an API key with Read & Write permission; cookie-authenticated callers also need member role. Ask the user before linking.",
|
|
587
|
+
inputSchema: linkFindingPullRequestInput,
|
|
588
|
+
handler: (client) => async (params) => {
|
|
589
|
+
const data = await client.post(
|
|
590
|
+
findingReferencePath(params, "/link-pr"),
|
|
591
|
+
{ url: params.url }
|
|
592
|
+
);
|
|
593
|
+
return {
|
|
594
|
+
markdown: formatFindingPullRequestLink(data),
|
|
595
|
+
structured: {
|
|
596
|
+
finding: data.finding,
|
|
597
|
+
pullRequest: data.pullRequest,
|
|
598
|
+
linkSource: data.linkSource,
|
|
599
|
+
workflow: data.workflow
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
|
|
371
605
|
// src/tools/list-assets.ts
|
|
372
|
-
import { z as
|
|
373
|
-
var listAssetsInput =
|
|
374
|
-
type:
|
|
375
|
-
source:
|
|
376
|
-
status:
|
|
606
|
+
import { z as z6 } from "zod";
|
|
607
|
+
var listAssetsInput = z6.object({
|
|
608
|
+
type: z6.enum(["DOMAIN", "SUBDOMAIN", "IP"]).optional(),
|
|
609
|
+
source: z6.enum(["MANUAL", "RECON", "IMPORT", "AGENT"]).optional(),
|
|
610
|
+
status: z6.string().optional().describe(
|
|
377
611
|
"Dynamic asset status set by recon (e.g. ACTIVE, INACTIVE, TLS_ERROR)."
|
|
378
612
|
),
|
|
379
|
-
search:
|
|
380
|
-
sortBy:
|
|
381
|
-
sortOrder:
|
|
382
|
-
limit:
|
|
383
|
-
offset:
|
|
613
|
+
search: z6.string().optional(),
|
|
614
|
+
sortBy: z6.enum(["lastSeenAt", "createdAt", "value"]).optional(),
|
|
615
|
+
sortOrder: z6.enum(["asc", "desc"]).optional(),
|
|
616
|
+
limit: z6.number().int().min(1).max(100).optional().default(50),
|
|
617
|
+
offset: z6.number().int().min(0).optional().default(0)
|
|
384
618
|
});
|
|
385
619
|
var listAssetsTool = {
|
|
386
620
|
name: "list_assets",
|
|
@@ -401,33 +635,32 @@ var listAssetsTool = {
|
|
|
401
635
|
};
|
|
402
636
|
|
|
403
637
|
// src/tools/list-findings.ts
|
|
404
|
-
import { z as
|
|
405
|
-
var listFindingsInput =
|
|
406
|
-
pentestId:
|
|
407
|
-
assetId:
|
|
638
|
+
import { z as z7 } from "zod";
|
|
639
|
+
var listFindingsInput = z7.object({
|
|
640
|
+
pentestId: z7.string().optional().describe("Filter to a single pentest."),
|
|
641
|
+
assetId: z7.string().optional().describe(
|
|
408
642
|
"Filter to findings whose latest revision references this asset."
|
|
409
643
|
),
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
"IN_REVIEW",
|
|
644
|
+
repository: z7.string().optional().describe("Filter to a source repository such as owner/name."),
|
|
645
|
+
severity: z7.enum(["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFORMATIONAL"]).optional(),
|
|
646
|
+
status: z7.enum([
|
|
414
647
|
"REPORTED",
|
|
648
|
+
"ACKNOWLEDGED",
|
|
415
649
|
"MITIGATING",
|
|
416
650
|
"OPEN_FOR_RETEST",
|
|
417
|
-
"
|
|
418
|
-
"
|
|
419
|
-
"ACCEPTED_RISK"
|
|
651
|
+
"NEEDS_REVISION",
|
|
652
|
+
"FIXED_AND_RETESTED"
|
|
420
653
|
]).optional(),
|
|
421
|
-
search:
|
|
422
|
-
sortBy:
|
|
423
|
-
sortOrder:
|
|
424
|
-
limit:
|
|
425
|
-
offset:
|
|
654
|
+
search: z7.string().optional().describe("Free-text search on finding titles and human identifiers."),
|
|
655
|
+
sortBy: z7.enum(["severity", "createdAt"]).optional(),
|
|
656
|
+
sortOrder: z7.enum(["asc", "desc"]).optional(),
|
|
657
|
+
limit: z7.number().int().min(1).max(200).optional().default(50),
|
|
658
|
+
offset: z7.number().int().min(0).optional().default(0)
|
|
426
659
|
});
|
|
427
660
|
var listFindingsTool = {
|
|
428
661
|
name: "list_findings",
|
|
429
662
|
title: "List findings",
|
|
430
|
-
description: "List security findings for the organisation. Supports filtering by pentest, severity, status, and
|
|
663
|
+
description: "List security findings for the organisation. Supports filtering by pentest, asset, repository, severity, current client-visible status, and searching finding titles or human identifiers such as ODI-12.",
|
|
431
664
|
inputSchema: listFindingsInput,
|
|
432
665
|
handler: (client) => async (params) => {
|
|
433
666
|
const data = await client.get(
|
|
@@ -446,8 +679,8 @@ var listFindingsTool = {
|
|
|
446
679
|
};
|
|
447
680
|
|
|
448
681
|
// src/tools/list-pentests.ts
|
|
449
|
-
import { z as
|
|
450
|
-
var listPentestsInput =
|
|
682
|
+
import { z as z8 } from "zod";
|
|
683
|
+
var listPentestsInput = z8.object({});
|
|
451
684
|
var listPentestsTool = {
|
|
452
685
|
name: "list_pentests",
|
|
453
686
|
title: "List pentests",
|
|
@@ -463,21 +696,24 @@ var listPentestsTool = {
|
|
|
463
696
|
};
|
|
464
697
|
|
|
465
698
|
// src/tools/update-finding-status.ts
|
|
466
|
-
import { z as
|
|
467
|
-
var updateFindingStatusInput =
|
|
468
|
-
|
|
469
|
-
status:
|
|
470
|
-
"Target status.
|
|
699
|
+
import { z as z9 } from "zod";
|
|
700
|
+
var updateFindingStatusInput = z9.object({
|
|
701
|
+
...findingReferenceFields,
|
|
702
|
+
status: z9.enum(["MITIGATING", "OPEN_FOR_RETEST", "ACKNOWLEDGED"]).describe(
|
|
703
|
+
"Target status. MITIGATING means work has started, OPEN_FOR_RETEST means the fix is ready for verification, and ACKNOWLEDGED means the organisation accepts the risk for now."
|
|
471
704
|
)
|
|
705
|
+
}).refine((value) => Boolean(value.identifier || value.findingId), {
|
|
706
|
+
message: "Provide either identifier or findingId."
|
|
472
707
|
});
|
|
473
708
|
var updateFindingStatusTool = {
|
|
474
709
|
name: "update_finding_status",
|
|
475
710
|
title: "Update finding status",
|
|
476
|
-
description: "Change a finding's status. Requires an API key with
|
|
711
|
+
description: "Change a finding's status by human identifier such as ODI-12 or internal findingId. Requires an API key with Read & Write permission; cookie-authenticated callers also need member role. Only use this after the user explicitly asks for the status change; a matching pull request is not permission to mutate a finding.",
|
|
477
712
|
inputSchema: updateFindingStatusInput,
|
|
478
713
|
handler: (client) => async (params) => {
|
|
714
|
+
const findingId = await resolveFindingId(client, params);
|
|
479
715
|
const data = await client.patch(
|
|
480
|
-
`/api/findings/${encodeURIComponent(
|
|
716
|
+
`/api/findings/${encodeURIComponent(findingId)}/status`,
|
|
481
717
|
{ status: params.status }
|
|
482
718
|
);
|
|
483
719
|
return {
|
|
@@ -488,7 +724,7 @@ var updateFindingStatusTool = {
|
|
|
488
724
|
};
|
|
489
725
|
|
|
490
726
|
// src/index.ts
|
|
491
|
-
var PACKAGE_VERSION = "0.
|
|
727
|
+
var PACKAGE_VERSION = "0.2.0";
|
|
492
728
|
async function main() {
|
|
493
729
|
const apiKey = process.env.ODIN_API_KEY;
|
|
494
730
|
if (!apiKey) {
|
|
@@ -498,7 +734,11 @@ async function main() {
|
|
|
498
734
|
process.exit(1);
|
|
499
735
|
}
|
|
500
736
|
const baseUrl = process.env.ODIN_BASE_URL ?? DEFAULT_ODIN_BASE_URL;
|
|
501
|
-
const client = new OdinClient({
|
|
737
|
+
const client = new OdinClient({
|
|
738
|
+
apiKey,
|
|
739
|
+
baseUrl,
|
|
740
|
+
vercelProtectionBypassSecret: process.env.ODIN_VERCEL_PROTECTION_BYPASS_SECRET
|
|
741
|
+
});
|
|
502
742
|
const server = new McpServer({
|
|
503
743
|
name: "@borgresearch/odin-mcp",
|
|
504
744
|
version: PACKAGE_VERSION
|
|
@@ -507,6 +747,8 @@ async function main() {
|
|
|
507
747
|
listFindingsTool,
|
|
508
748
|
getFindingTool,
|
|
509
749
|
updateFindingStatusTool,
|
|
750
|
+
linkFindingPullRequestTool,
|
|
751
|
+
getFindingRetestsTool,
|
|
510
752
|
listPentestsTool,
|
|
511
753
|
getPentestTool,
|
|
512
754
|
listAssetsTool,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/tools/get-asset.ts","../src/formatters/markdown.ts","../src/tools/get-finding.ts","../src/tools/get-pentest.ts","../src/tools/list-assets.ts","../src/tools/list-findings.ts","../src/tools/list-pentests.ts","../src/tools/update-finding-status.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { DEFAULT_ODIN_BASE_URL, OdinApiError, OdinClient } from \"./client.js\";\nimport { getAssetTool } from \"./tools/get-asset.js\";\nimport { getFindingTool } from \"./tools/get-finding.js\";\nimport { getPentestTool } from \"./tools/get-pentest.js\";\nimport { listAssetsTool } from \"./tools/list-assets.js\";\nimport { listFindingsTool } from \"./tools/list-findings.js\";\nimport { listPentestsTool } from \"./tools/list-pentests.js\";\nimport { updateFindingStatusTool } from \"./tools/update-finding-status.js\";\n\nconst PACKAGE_VERSION = \"0.1.0\";\n\ninterface ToolResult {\n markdown: string;\n structured: unknown;\n}\n\nasync function main() {\n const apiKey = process.env.ODIN_API_KEY;\n if (!apiKey) {\n console.error(\n \"ODIN_API_KEY is not set or invalid. Create one at Management > API Keys in the Odin dashboard.\",\n );\n process.exit(1);\n }\n\n const baseUrl = process.env.ODIN_BASE_URL ?? DEFAULT_ODIN_BASE_URL;\n const client = new OdinClient({ apiKey, baseUrl });\n\n const server = new McpServer({\n name: \"@borgresearch/odin-mcp\",\n version: PACKAGE_VERSION,\n });\n\n const tools = [\n listFindingsTool,\n getFindingTool,\n updateFindingStatusTool,\n listPentestsTool,\n getPentestTool,\n listAssetsTool,\n getAssetTool,\n ];\n\n for (const tool of tools) {\n const run = tool.handler(client) as (\n params: unknown,\n ) => Promise<ToolResult>;\n server.registerTool(\n tool.name,\n {\n title: tool.title,\n description: tool.description,\n inputSchema: tool.inputSchema.shape,\n },\n async (params: unknown) => {\n try {\n const { markdown, structured } = await run(params);\n return {\n content: [{ type: \"text\" as const, text: markdown }],\n structuredContent: { markdown, ...toObject(structured) },\n };\n } catch (error) {\n return {\n isError: true,\n content: [{ type: \"text\" as const, text: toErrorMessage(error) }],\n };\n }\n },\n );\n }\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n console.error(\n `odin-mcp ${PACKAGE_VERSION} connected (baseUrl=${baseUrl}, tools=${tools.length}).`,\n );\n}\n\nfunction toObject(value: unknown): Record<string, unknown> {\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n return value as Record<string, unknown>;\n }\n return { data: value };\n}\n\nfunction toErrorMessage(error: unknown): string {\n if (error instanceof OdinApiError) return error.message;\n if (error instanceof Error) return `Unexpected error: ${error.message}`;\n return `Unexpected error: ${String(error)}`;\n}\n\nmain().catch((error) => {\n console.error(\"odin-mcp failed to start:\", error);\n process.exit(1);\n});\n","export class OdinApiError extends Error {\n readonly status: number;\n readonly path: string;\n readonly method: string;\n readonly retryAfterSeconds: number | null;\n readonly body: unknown;\n\n constructor(params: {\n status: number;\n path: string;\n method: string;\n retryAfterSeconds: number | null;\n body: unknown;\n message: string;\n }) {\n super(params.message);\n this.name = \"OdinApiError\";\n this.status = params.status;\n this.path = params.path;\n this.method = params.method;\n this.retryAfterSeconds = params.retryAfterSeconds;\n this.body = params.body;\n }\n}\n\nexport interface OdinClientOptions {\n apiKey: string;\n baseUrl?: string;\n fetchImpl?: typeof fetch;\n}\n\nexport const DEFAULT_ODIN_BASE_URL = \"https://odin.borghq.io\";\n\nconst WRITE_METHODS = new Set([\"PATCH\", \"POST\", \"PUT\", \"DELETE\"]);\n\nexport class OdinClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n private readonly fetchImpl: typeof fetch;\n\n constructor(options: OdinClientOptions) {\n if (!options.apiKey) {\n throw new Error(\"OdinClient requires an apiKey\");\n }\n this.apiKey = options.apiKey;\n this.baseUrl = (options.baseUrl ?? DEFAULT_ODIN_BASE_URL).replace(\n /\\/+$/,\n \"\",\n );\n this.fetchImpl = options.fetchImpl ?? fetch;\n }\n\n async get<T>(\n path: string,\n searchParams?: Record<string, string | number | undefined>,\n ): Promise<T> {\n const url = this.buildUrl(path, searchParams);\n return this.request<T>(\"GET\", url, path);\n }\n\n async patch<T>(path: string, body: unknown): Promise<T> {\n const url = this.buildUrl(path);\n return this.request<T>(\"PATCH\", url, path, body);\n }\n\n async post<T>(path: string, body: unknown): Promise<T> {\n const url = this.buildUrl(path);\n return this.request<T>(\"POST\", url, path, body);\n }\n\n private buildUrl(\n path: string,\n searchParams?: Record<string, string | number | undefined>,\n ): string {\n const url = new URL(path.startsWith(\"/\") ? path : `/${path}`, this.baseUrl);\n if (searchParams) {\n for (const [key, value] of Object.entries(searchParams)) {\n if (value === undefined || value === \"\") continue;\n url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n }\n\n private async request<T>(\n method: string,\n url: string,\n path: string,\n body?: unknown,\n ): Promise<T> {\n const response = await this.fetchImpl(url, {\n method,\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n\n const rawText = await response.text();\n const parsed = rawText ? safeJsonParse(rawText) : null;\n\n if (!response.ok) {\n const retryAfterSeconds = parseRetryAfter(\n response.headers.get(\"Retry-After\"),\n );\n throw new OdinApiError({\n status: response.status,\n path,\n method,\n retryAfterSeconds,\n body: parsed ?? rawText,\n message: formatErrorMessage({\n status: response.status,\n method,\n path,\n retryAfterSeconds,\n parsed,\n rawText,\n }),\n });\n }\n\n return parsed as T;\n }\n}\n\nfunction safeJsonParse(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nfunction parseRetryAfter(header: string | null): number | null {\n if (!header) return null;\n const trimmed = header.trim();\n if (/^\\d+$/.test(trimmed)) return Number.parseInt(trimmed, 10);\n const dateMs = Date.parse(trimmed);\n if (Number.isNaN(dateMs)) return null;\n const seconds = Math.ceil((dateMs - Date.now()) / 1000);\n return seconds > 0 ? seconds : null;\n}\n\ninterface FormatErrorInput {\n status: number;\n method: string;\n path: string;\n retryAfterSeconds: number | null;\n parsed: unknown;\n rawText: string;\n}\n\nfunction formatErrorMessage(input: FormatErrorInput): string {\n const serverMessage = extractServerMessage(input.parsed);\n const actionable = actionableMessage(input);\n if (actionable) {\n return serverMessage\n ? `${actionable} (server: ${serverMessage})`\n : actionable;\n }\n const suffix = serverMessage\n ? ` — ${serverMessage}`\n : input.rawText\n ? ` — ${input.rawText.slice(0, 200)}`\n : \"\";\n return `Odin API ${input.status} on ${input.path}${suffix}.`;\n}\n\nfunction actionableMessage(input: FormatErrorInput): string | null {\n switch (input.status) {\n case 401:\n return \"ODIN_API_KEY is not set or invalid. Create one at Management > API Keys in the Odin dashboard.\";\n case 403:\n if (WRITE_METHODS.has(input.method)) {\n return \"API key does not have write permission. Create a new key with 'Read & Write' permissions.\";\n }\n return null;\n case 404:\n if (isFindingDetailPath(input.path)) {\n return \"Finding not found. Check the finding ID is correct.\";\n }\n return null;\n case 429: {\n const n = input.retryAfterSeconds;\n return n\n ? `Rate limit exceeded. Try again in ${n} seconds.`\n : \"Rate limit exceeded. Try again shortly.\";\n }\n default:\n return null;\n }\n}\n\nfunction isFindingDetailPath(path: string): boolean {\n // /api/findings/{id} or /api/findings/{id}/status\n return /^\\/api\\/findings\\/[^/]+(\\/.*)?$/.test(path);\n}\n\nfunction extractServerMessage(parsed: unknown): string | null {\n if (parsed && typeof parsed === \"object\" && \"error\" in parsed) {\n const err = (parsed as { error: unknown }).error;\n if (typeof err === \"string\") return err;\n }\n return null;\n}\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatAssetDetail } from \"../formatters/markdown.js\";\nimport type { AssetDetailResponse } from \"../types.js\";\n\nexport const getAssetInput = z.object({\n assetId: z.string().min(1),\n});\n\nexport const getAssetTool = {\n name: \"get_asset\",\n title: \"Get asset\",\n description:\n \"Fetch a single attack-surface asset by ID, including the findings that reference it in their latest revision.\",\n inputSchema: getAssetInput,\n handler:\n (client: OdinClient) => async (params: z.infer<typeof getAssetInput>) => {\n const data = await client.get<AssetDetailResponse>(\n `/api/assets/${encodeURIComponent(params.assetId)}`,\n );\n return {\n markdown: formatAssetDetail(data),\n structured: { asset: data.asset, findings: data.findings },\n };\n },\n};\n","import type {\n Asset,\n AssetDetailResponse,\n AssetsListResponse,\n FindingDetailResponse,\n FindingSummary,\n FindingsListResponse,\n Pentest,\n PentestDetailResponse,\n PentestListResponse,\n} from \"../types.js\";\n\nexport function formatFindingsList(data: FindingsListResponse): string {\n const { findings, stats, pagination } = data;\n if (findings.length === 0) {\n return \"No findings match the supplied filters.\";\n }\n\n const rows = findings.map((f) => {\n const severity = `${f.severity.label} (CVSS ${f.severity.cvss.toFixed(1)})`;\n return `| ${f.findingId} | ${escapePipes(f.title)} | ${severity} | ${f.status} | ${formatDate(f.createdAt)} |`;\n });\n\n return [\n `Showing ${findings.length} of ${stats.total} finding(s). Offset ${pagination.offset}, limit ${pagination.limit}. hasMore=${pagination.hasMore}.`,\n \"\",\n \"| Finding ID | Title | Severity | Status | Created |\",\n \"| --- | --- | --- | --- | --- |\",\n ...rows,\n ].join(\"\\n\");\n}\n\nexport function formatFindingDetail(data: FindingDetailResponse): string {\n const { finding, revision } = data;\n const severity = `${finding.severity.label} (CVSS ${finding.severity.cvss.toFixed(1)})`;\n const header = [\n `# ${finding.title}`,\n \"\",\n `- **ID:** ${finding.findingId}`,\n `- **Severity:** ${severity}`,\n `- **Status:** ${finding.status}`,\n `- **Pentest:** ${finding.context?.pentestId ?? \"standalone\"}`,\n `- **Created:** ${formatDate(finding.createdAt)}`,\n `- **Updated:** ${formatDate(finding.updatedAt)}`,\n ];\n\n if (!revision) {\n return [...header, \"\", \"_No revision content available._\"].join(\"\\n\");\n }\n\n const c = revision.content;\n const sections: string[] = [];\n if (c.summaryMd) sections.push(\"## Summary\", c.summaryMd);\n if (c.descriptionMd) sections.push(\"## Description\", c.descriptionMd);\n if (c.businessImpactMd)\n sections.push(\"## Business Impact\", c.businessImpactMd);\n if (c.detailsMd) sections.push(\"## Details\", c.detailsMd);\n if (c.remediationMd) sections.push(\"## Remediation\", c.remediationMd);\n if (c.affectedAssets?.length) {\n sections.push(\n \"## Affected Assets\",\n c.affectedAssets.map((a) => `- ${a.display} (${a.assetId})`).join(\"\\n\"),\n );\n }\n\n return [\n ...header,\n \"\",\n `_Revision ${revision.revisionNumber}_`,\n \"\",\n ...sections,\n ].join(\"\\n\");\n}\n\nexport function formatFindingStatusUpdate(finding: FindingSummary): string {\n return [\n `Updated finding **${finding.findingId}** status to **${finding.status}**.`,\n `Title: ${finding.title}`,\n ].join(\"\\n\");\n}\n\nexport function formatPentestsList(data: PentestListResponse): string {\n if (data.pentests.length === 0) return \"No pentests found.\";\n const rows = data.pentests.map(formatPentestRow);\n return [\n \"| Pentest ID | Name | Status | Start | End | Scope |\",\n \"| --- | --- | --- | --- | --- | --- |\",\n ...rows,\n ].join(\"\\n\");\n}\n\nexport function formatPentestDetail(data: PentestDetailResponse): string {\n const p = data.pentest;\n const end = p.endAt ? formatDate(p.endAt) : \"open\";\n const scope = p.scope.length\n ? p.scope.map((s) => `- ${s}`).join(\"\\n\")\n : \"_No scope defined._\";\n return [\n `# ${p.name}`,\n \"\",\n `- **ID:** ${p.pentestId}`,\n `- **Status:** ${p.status}`,\n `- **Start:** ${formatDate(p.startAt)}`,\n `- **End:** ${end}`,\n `- **Created:** ${formatDate(p.createdAt)}`,\n \"\",\n \"## Scope\",\n \"\",\n scope,\n ].join(\"\\n\");\n}\n\nfunction formatPentestRow(p: Pentest): string {\n const end = p.endAt ? formatDate(p.endAt) : \"open\";\n const scope =\n p.scope.slice(0, 3).join(\", \") + (p.scope.length > 3 ? \", …\" : \"\");\n return `| ${p.pentestId} | ${escapePipes(p.name)} | ${p.status} | ${formatDate(p.startAt)} | ${end} | ${escapePipes(scope)} |`;\n}\n\nexport function formatAssetsList(data: AssetsListResponse): string {\n const { assets, stats, pagination } = data;\n if (assets.length === 0) return \"No assets match the supplied filters.\";\n const rows = assets.map(formatAssetRow);\n return [\n `Showing ${assets.length} of ${stats.total} asset(s). Offset ${pagination.offset}, limit ${pagination.limit}. hasMore=${pagination.hasMore}.`,\n \"\",\n \"| Asset ID | Type | Value | Source | Status | Last seen |\",\n \"| --- | --- | --- | --- | --- | --- |\",\n ...rows,\n ].join(\"\\n\");\n}\n\nexport function formatAssetDetail(data: AssetDetailResponse): string {\n const { asset, findings } = data;\n const header = [\n `# ${asset.value}`,\n \"\",\n `- **ID:** ${asset.assetId}`,\n `- **Type:** ${asset.type}`,\n `- **Source:** ${asset.source}`,\n `- **Status:** ${asset.status}`,\n `- **Last seen:** ${formatDate(asset.lastSeenAt)}`,\n `- **Created:** ${formatDate(asset.createdAt)}`,\n ];\n\n if (findings.length === 0) {\n return [...header, \"\", \"_No findings reference this asset._\"].join(\"\\n\");\n }\n\n const rows = findings.map((f) => {\n const severity = `${f.severity.label} (CVSS ${f.severity.cvss.toFixed(1)})`;\n return `| ${f.findingId} | ${escapePipes(f.title)} | ${severity} | ${f.status} | ${formatDate(f.createdAt)} |`;\n });\n\n return [\n ...header,\n \"\",\n `## Findings (${findings.length})`,\n \"\",\n \"| Finding ID | Title | Severity | Status | Created |\",\n \"| --- | --- | --- | --- | --- |\",\n ...rows,\n ].join(\"\\n\");\n}\n\nfunction formatAssetRow(a: Asset): string {\n return `| ${a.assetId} | ${a.type} | ${escapePipes(a.value)} | ${a.source} | ${a.status} | ${formatDate(a.lastSeenAt)} |`;\n}\n\nfunction escapePipes(value: string): string {\n return value.replace(/\\|/g, \"\\\\|\");\n}\n\nfunction formatDate(epoch: number): string {\n if (!epoch) return \"—\";\n // Odin stores timestamps as milliseconds for documents and seconds for some fields\n // (pentest startAt/endAt are seconds). Heuristic: values < 10^12 are seconds.\n const ms = epoch < 1e12 ? epoch * 1000 : epoch;\n return new Date(ms).toISOString().slice(0, 10);\n}\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatFindingDetail } from \"../formatters/markdown.js\";\nimport type { FindingDetailResponse } from \"../types.js\";\n\nexport const getFindingInput = z.object({\n findingId: z.string().min(1),\n});\n\nexport const getFindingTool = {\n name: \"get_finding\",\n title: \"Get finding\",\n description:\n \"Fetch a single finding by ID, including the latest revision content (description, impact, details, remediation).\",\n inputSchema: getFindingInput,\n handler:\n (client: OdinClient) => async (params: z.infer<typeof getFindingInput>) => {\n const data = await client.get<FindingDetailResponse>(\n `/api/findings/${encodeURIComponent(params.findingId)}`,\n );\n return {\n markdown: formatFindingDetail(data),\n structured: {\n finding: data.finding,\n revision: data.revision,\n },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatPentestDetail } from \"../formatters/markdown.js\";\nimport type { PentestDetailResponse } from \"../types.js\";\n\nexport const getPentestInput = z.object({\n pentestId: z.string().min(1),\n});\n\nexport const getPentestTool = {\n name: \"get_pentest\",\n title: \"Get pentest\",\n description:\n \"Fetch a single pentest by ID, including status, scheduled window, and in-scope targets.\",\n inputSchema: getPentestInput,\n handler:\n (client: OdinClient) => async (params: z.infer<typeof getPentestInput>) => {\n const data = await client.get<PentestDetailResponse>(\n `/api/pentests/${encodeURIComponent(params.pentestId)}`,\n );\n return {\n markdown: formatPentestDetail(data),\n structured: { pentest: data.pentest },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatAssetsList } from \"../formatters/markdown.js\";\nimport type { AssetsListResponse } from \"../types.js\";\n\nexport const listAssetsInput = z.object({\n type: z.enum([\"DOMAIN\", \"SUBDOMAIN\", \"IP\", \"URL\", \"WILDCARD\"]).optional(),\n source: z.enum([\"MANUAL\", \"RECON\", \"IMPORT\", \"AGENT\"]).optional(),\n status: z\n .string()\n .optional()\n .describe(\n \"Dynamic asset status set by recon (e.g. ACTIVE, INACTIVE, TLS_ERROR).\",\n ),\n search: z.string().optional(),\n sortBy: z.enum([\"lastSeenAt\", \"createdAt\", \"value\"]).optional(),\n sortOrder: z.enum([\"asc\", \"desc\"]).optional(),\n limit: z.number().int().min(1).max(100).optional().default(50),\n offset: z.number().int().min(0).optional().default(0),\n});\n\nexport const listAssetsTool = {\n name: \"list_assets\",\n title: \"List assets\",\n description:\n \"List attack-surface assets for the organisation. Supports filtering by type, source, status, and free-text search.\",\n inputSchema: listAssetsInput,\n handler:\n (client: OdinClient) => async (params: z.infer<typeof listAssetsInput>) => {\n const data = await client.get<AssetsListResponse>(\"/api/assets\", params);\n return {\n markdown: formatAssetsList(data),\n structured: {\n assets: data.assets,\n stats: data.stats,\n pagination: data.pagination,\n },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatFindingsList } from \"../formatters/markdown.js\";\nimport type { FindingsListResponse } from \"../types.js\";\n\nexport const listFindingsInput = z.object({\n pentestId: z.string().optional().describe(\"Filter to a single pentest.\"),\n assetId: z\n .string()\n .optional()\n .describe(\n \"Filter to findings whose latest revision references this asset.\",\n ),\n severity: z\n .enum([\"CRITICAL\", \"HIGH\", \"MEDIUM\", \"LOW\", \"INFORMATIONAL\"])\n .optional(),\n status: z\n .enum([\n \"DRAFT\",\n \"IN_REVIEW\",\n \"REPORTED\",\n \"MITIGATING\",\n \"OPEN_FOR_RETEST\",\n \"RESOLVED\",\n \"NOT_REPRODUCIBLE\",\n \"ACCEPTED_RISK\",\n ])\n .optional(),\n search: z.string().optional().describe(\"Free-text search on finding titles.\"),\n sortBy: z.enum([\"severity\", \"createdAt\"]).optional(),\n sortOrder: z.enum([\"asc\", \"desc\"]).optional(),\n limit: z.number().int().min(1).max(200).optional().default(50),\n offset: z.number().int().min(0).optional().default(0),\n});\n\nexport const listFindingsTool = {\n name: \"list_findings\",\n title: \"List findings\",\n description:\n \"List security findings for the organisation. Supports filtering by pentest, severity, status, and free-text search.\",\n inputSchema: listFindingsInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof listFindingsInput>) => {\n const data = await client.get<FindingsListResponse>(\n \"/api/findings\",\n params,\n );\n return {\n markdown: formatFindingsList(data),\n structured: {\n findings: data.findings,\n stats: data.stats,\n pagination: data.pagination,\n },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatPentestsList } from \"../formatters/markdown.js\";\nimport type { PentestListResponse } from \"../types.js\";\n\nexport const listPentestsInput = z.object({});\n\nexport const listPentestsTool = {\n name: \"list_pentests\",\n title: \"List pentests\",\n description:\n \"List all pentests for the organisation, including status, scheduled window, and in-scope targets.\",\n inputSchema: listPentestsInput,\n handler: (client: OdinClient) => async () => {\n const data = await client.get<PentestListResponse>(\"/api/pentests\");\n return {\n markdown: formatPentestsList(data),\n structured: { pentests: data.pentests },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatFindingStatusUpdate } from \"../formatters/markdown.js\";\nimport type { FindingStatusUpdateResponse } from \"../types.js\";\n\nexport const updateFindingStatusInput = z.object({\n findingId: z.string().min(1),\n status: z\n .enum([\"MITIGATING\", \"OPEN_FOR_RETEST\", \"ACKNOWLEDGED\"])\n .describe(\n \"Target status. Only MITIGATING, OPEN_FOR_RETEST, and ACKNOWLEDGED are client-writable; transitions are only allowed from REPORTED, MITIGATING, or OPEN_FOR_RETEST. ACKNOWLEDGED marks the finding as accepted-risk and is terminal.\",\n ),\n});\n\nexport const updateFindingStatusTool = {\n name: \"update_finding_status\",\n title: \"Update finding status\",\n description:\n \"Change a finding's status. Requires an API key with write permission and member role. Only MITIGATING, OPEN_FOR_RETEST, and ACKNOWLEDGED are accepted.\",\n inputSchema: updateFindingStatusInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof updateFindingStatusInput>) => {\n const data = await client.patch<FindingStatusUpdateResponse>(\n `/api/findings/${encodeURIComponent(params.findingId)}/status`,\n { status: params.status },\n );\n return {\n markdown: formatFindingStatusUpdate(data.finding),\n structured: { finding: data.finding },\n };\n },\n};\n"],"mappings":"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","names":["z","z","z","z","z","z","z","z","z","z","z","z"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/tools/get-asset.ts","../src/formatters/markdown.ts","../src/tools/finding-reference.ts","../src/tools/get-finding.ts","../src/tools/get-finding-retests.ts","../src/tools/get-pentest.ts","../src/tools/link-finding-pull-request.ts","../src/tools/list-assets.ts","../src/tools/list-findings.ts","../src/tools/list-pentests.ts","../src/tools/update-finding-status.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { DEFAULT_ODIN_BASE_URL, OdinApiError, OdinClient } from \"./client.js\";\nimport { getAssetTool } from \"./tools/get-asset.js\";\nimport { getFindingTool } from \"./tools/get-finding.js\";\nimport { getFindingRetestsTool } from \"./tools/get-finding-retests.js\";\nimport { getPentestTool } from \"./tools/get-pentest.js\";\nimport { linkFindingPullRequestTool } from \"./tools/link-finding-pull-request.js\";\nimport { listAssetsTool } from \"./tools/list-assets.js\";\nimport { listFindingsTool } from \"./tools/list-findings.js\";\nimport { listPentestsTool } from \"./tools/list-pentests.js\";\nimport { updateFindingStatusTool } from \"./tools/update-finding-status.js\";\n\nconst PACKAGE_VERSION = \"0.2.0\";\n\ninterface ToolResult {\n markdown: string;\n structured: unknown;\n}\n\nasync function main() {\n const apiKey = process.env.ODIN_API_KEY;\n if (!apiKey) {\n console.error(\n \"ODIN_API_KEY is not set or invalid. Create one at Management > API Keys in the Odin dashboard.\",\n );\n process.exit(1);\n }\n\n const baseUrl = process.env.ODIN_BASE_URL ?? DEFAULT_ODIN_BASE_URL;\n const client = new OdinClient({\n apiKey,\n baseUrl,\n vercelProtectionBypassSecret:\n process.env.ODIN_VERCEL_PROTECTION_BYPASS_SECRET,\n });\n\n const server = new McpServer({\n name: \"@borgresearch/odin-mcp\",\n version: PACKAGE_VERSION,\n });\n\n const tools = [\n listFindingsTool,\n getFindingTool,\n updateFindingStatusTool,\n linkFindingPullRequestTool,\n getFindingRetestsTool,\n listPentestsTool,\n getPentestTool,\n listAssetsTool,\n getAssetTool,\n ];\n\n for (const tool of tools) {\n const run = tool.handler(client) as (\n params: unknown,\n ) => Promise<ToolResult>;\n server.registerTool(\n tool.name,\n {\n title: tool.title,\n description: tool.description,\n inputSchema: tool.inputSchema.shape,\n },\n async (params: unknown) => {\n try {\n const { markdown, structured } = await run(params);\n return {\n content: [{ type: \"text\" as const, text: markdown }],\n structuredContent: { markdown, ...toObject(structured) },\n };\n } catch (error) {\n return {\n isError: true,\n content: [{ type: \"text\" as const, text: toErrorMessage(error) }],\n };\n }\n },\n );\n }\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n console.error(\n `odin-mcp ${PACKAGE_VERSION} connected (baseUrl=${baseUrl}, tools=${tools.length}).`,\n );\n}\n\nfunction toObject(value: unknown): Record<string, unknown> {\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n return value as Record<string, unknown>;\n }\n return { data: value };\n}\n\nfunction toErrorMessage(error: unknown): string {\n if (error instanceof OdinApiError) return error.message;\n if (error instanceof Error) return `Unexpected error: ${error.message}`;\n return `Unexpected error: ${String(error)}`;\n}\n\nmain().catch((error) => {\n console.error(\"odin-mcp failed to start:\", error);\n process.exit(1);\n});\n","export class OdinApiError extends Error {\n readonly status: number;\n readonly path: string;\n readonly method: string;\n readonly retryAfterSeconds: number | null;\n readonly body: unknown;\n\n constructor(params: {\n status: number;\n path: string;\n method: string;\n retryAfterSeconds: number | null;\n body: unknown;\n message: string;\n }) {\n super(params.message);\n this.name = \"OdinApiError\";\n this.status = params.status;\n this.path = params.path;\n this.method = params.method;\n this.retryAfterSeconds = params.retryAfterSeconds;\n this.body = params.body;\n }\n}\n\nexport interface OdinClientOptions {\n apiKey: string;\n baseUrl?: string;\n fetchImpl?: typeof fetch;\n vercelProtectionBypassSecret?: string;\n}\n\nexport const DEFAULT_ODIN_BASE_URL = \"https://odin.borghq.io\";\n\nconst WRITE_METHODS = new Set([\"PATCH\", \"POST\", \"PUT\", \"DELETE\"]);\n\nexport class OdinClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n private readonly fetchImpl: typeof fetch;\n private readonly vercelProtectionBypassSecret: string | undefined;\n\n constructor(options: OdinClientOptions) {\n if (!options.apiKey) {\n throw new Error(\"OdinClient requires an apiKey\");\n }\n this.apiKey = options.apiKey;\n this.baseUrl = (options.baseUrl ?? DEFAULT_ODIN_BASE_URL).replace(\n /\\/+$/,\n \"\",\n );\n this.fetchImpl = options.fetchImpl ?? fetch;\n this.vercelProtectionBypassSecret =\n options.vercelProtectionBypassSecret?.trim() || undefined;\n }\n\n async get<T>(\n path: string,\n searchParams?: Record<string, string | number | undefined>,\n ): Promise<T> {\n const url = this.buildUrl(path, searchParams);\n return this.request<T>(\"GET\", url, path);\n }\n\n async patch<T>(path: string, body: unknown): Promise<T> {\n const url = this.buildUrl(path);\n return this.request<T>(\"PATCH\", url, path, body);\n }\n\n // Public HTTP method API — kept alongside get/patch even when unused internally\n // fallow-ignore-next-line unused-class-member\n async post<T>(path: string, body: unknown): Promise<T> {\n const url = this.buildUrl(path);\n return this.request<T>(\"POST\", url, path, body);\n }\n\n private buildUrl(\n path: string,\n searchParams?: Record<string, string | number | undefined>,\n ): string {\n const url = new URL(path.startsWith(\"/\") ? path : `/${path}`, this.baseUrl);\n if (searchParams) {\n for (const [key, value] of Object.entries(searchParams)) {\n if (value === undefined || value === \"\") continue;\n url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n }\n\n private async request<T>(\n method: string,\n url: string,\n path: string,\n body?: unknown,\n ): Promise<T> {\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.apiKey}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n };\n if (this.vercelProtectionBypassSecret) {\n headers[\"x-vercel-protection-bypass\"] = this.vercelProtectionBypassSecret;\n }\n\n const response = await this.fetchImpl(url, {\n method,\n headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n\n const rawText = await response.text();\n const parsed = rawText ? safeJsonParse(rawText) : null;\n\n if (!response.ok) {\n const retryAfterSeconds = parseRetryAfter(\n response.headers.get(\"Retry-After\"),\n );\n throw new OdinApiError({\n status: response.status,\n path,\n method,\n retryAfterSeconds,\n body: parsed ?? rawText,\n message: formatErrorMessage({\n status: response.status,\n method,\n path,\n retryAfterSeconds,\n parsed,\n rawText,\n }),\n });\n }\n\n return parsed as T;\n }\n}\n\nfunction safeJsonParse(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nfunction parseRetryAfter(header: string | null): number | null {\n if (!header) return null;\n const trimmed = header.trim();\n if (/^\\d+$/.test(trimmed)) return Number.parseInt(trimmed, 10);\n const dateMs = Date.parse(trimmed);\n if (Number.isNaN(dateMs)) return null;\n const seconds = Math.ceil((dateMs - Date.now()) / 1000);\n return seconds > 0 ? seconds : null;\n}\n\ninterface FormatErrorInput {\n status: number;\n method: string;\n path: string;\n retryAfterSeconds: number | null;\n parsed: unknown;\n rawText: string;\n}\n\nfunction formatErrorMessage(input: FormatErrorInput): string {\n const serverMessage = extractServerMessage(input.parsed);\n const actionable = actionableMessage(input);\n if (actionable) {\n return serverMessage\n ? `${actionable} (server: ${serverMessage})`\n : actionable;\n }\n const suffix = serverMessage\n ? ` — ${serverMessage}`\n : input.rawText\n ? ` — ${input.rawText.slice(0, 200)}`\n : \"\";\n return `Odin API ${input.status} on ${input.path}${suffix}.`;\n}\n\nfunction actionableMessage(input: FormatErrorInput): string | null {\n switch (input.status) {\n case 401:\n return \"ODIN_API_KEY is not set or invalid. Create one at Management > API Keys in the Odin dashboard.\";\n case 403:\n if (WRITE_METHODS.has(input.method)) {\n return \"API key does not have write permission. Create a new key with 'Read & Write' permissions.\";\n }\n return null;\n case 404:\n if (isFindingDetailPath(input.path)) {\n return \"Finding not found. Check the finding ID is correct.\";\n }\n return null;\n case 429: {\n const n = input.retryAfterSeconds;\n return n\n ? `Rate limit exceeded. Try again in ${n} seconds.`\n : \"Rate limit exceeded. Try again shortly.\";\n }\n default:\n return null;\n }\n}\n\nfunction isFindingDetailPath(path: string): boolean {\n // /api/findings/{id} or /api/findings/{id}/status\n return /^\\/api\\/findings\\/[^/]+(\\/.*)?$/.test(path);\n}\n\nfunction extractServerMessage(parsed: unknown): string | null {\n if (parsed && typeof parsed === \"object\" && \"error\" in parsed) {\n const err = (parsed as { error: unknown }).error;\n if (typeof err === \"string\") return err;\n }\n return null;\n}\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatAssetDetail } from \"../formatters/markdown.js\";\nimport type { AssetDetailResponse } from \"../types.js\";\n\nexport const getAssetInput = z.object({\n assetId: z.string().min(1),\n});\n\nexport const getAssetTool = {\n name: \"get_asset\",\n title: \"Get asset\",\n description:\n \"Fetch a single attack-surface asset by ID, including the findings that reference it in their latest revision.\",\n inputSchema: getAssetInput,\n handler:\n (client: OdinClient) => async (params: z.infer<typeof getAssetInput>) => {\n const data = await client.get<AssetDetailResponse>(\n `/api/assets/${encodeURIComponent(params.assetId)}`,\n );\n return {\n markdown: formatAssetDetail(data),\n structured: { asset: data.asset, findings: data.findings },\n };\n },\n};\n","import type {\n Asset,\n AssetDetailResponse,\n AssetsListResponse,\n FindingDetailResponse,\n FindingStatus,\n FindingSummary,\n FindingsListResponse,\n LinkFindingPullRequestResponse,\n Pentest,\n PentestDetailResponse,\n PentestListResponse,\n Retest,\n RetestHistoryResponse,\n} from \"../types.js\";\n\nconst STATUS_LABELS: Record<FindingStatus, string> = {\n REPORTED: \"Reported\",\n ACKNOWLEDGED: \"Acknowledged\",\n MITIGATING: \"Mitigating\",\n OPEN_FOR_RETEST: \"Open for Retest\",\n NEEDS_REVISION: \"Needs Revision\",\n FIXED_AND_RETESTED: \"Fixed & Retested\",\n};\n\nconst RETEST_STATUS_LABELS: Record<Retest[\"status\"], string> = {\n QUEUED: \"Queued\",\n RUNNING: \"Running\",\n COMPLETED: \"Completed\",\n FAILED: \"Failed\",\n SKIPPED: \"Skipped\",\n};\n\nexport function formatFindingsList(data: FindingsListResponse): string {\n const { findings, stats, pagination } = data;\n if (findings.length === 0) {\n return \"No findings match the supplied filters.\";\n }\n\n const rows = findings.map((f) => {\n const severity = `${f.severity.label} (CVSS ${f.severity.cvss.toFixed(1)})`;\n return `| ${f.identifier ?? \"—\"} | ${f.findingId} | ${escapePipes(f.title)} | ${severity} | ${formatStatus(f.status)} | ${formatDate(f.createdAt)} |`;\n });\n\n return [\n `Showing ${findings.length} of ${stats.total} finding(s). Offset ${pagination.offset}, limit ${pagination.limit}. hasMore=${pagination.hasMore}.`,\n \"\",\n \"| Identifier | Internal ID | Title | Severity | Status | Created |\",\n \"| --- | --- | --- | --- | --- | --- |\",\n ...rows,\n ].join(\"\\n\");\n}\n\nexport function formatFindingDetail(data: FindingDetailResponse): string {\n const { finding, revision } = data;\n const context = data.remediationContext;\n const identifier = context?.identifier ?? finding.identifier ?? null;\n const displayIdentifier = identifier ?? finding.findingId;\n const severity = `${finding.severity.label} (CVSS ${finding.severity.cvss.toFixed(1)})`;\n const header = [\n `# ${escapePipes(displayIdentifier)} — ${escapePipes(finding.title)}`,\n \"\",\n `- **Finding:** ${displayIdentifier}`,\n `- **Internal ID:** ${finding.findingId}`,\n `- **Severity:** ${severity}`,\n `- **Status:** ${formatStatus(finding.status)}`,\n `- **Repository:** ${context?.repository ?? finding.repository ?? \"Not recorded\"}`,\n `- **Pentest:** ${finding.context?.pentestId ?? \"standalone\"}`,\n `- **Created:** ${formatDate(finding.createdAt)}`,\n `- **Updated:** ${formatDate(finding.updatedAt)}`,\n ];\n\n const content = context\n ? {\n revisionNumber: context.revision.revisionNumber,\n summary: context.revision.summary,\n description: context.revision.description,\n impact: context.revision.impact,\n details: context.revision.details,\n remediation: context.revision.remediation,\n affectedAssets: context.revision.affectedAssets,\n codeReference: context.revision.codeReference,\n }\n : revision\n ? {\n revisionNumber: revision.revisionNumber,\n summary: revision.content.summaryMd,\n description: revision.content.descriptionMd,\n impact: revision.content.businessImpactMd,\n details: revision.content.detailsMd,\n remediation: revision.content.remediationMd,\n affectedAssets: revision.content.affectedAssets ?? [],\n codeReference: null,\n }\n : null;\n\n const sections: string[] = [];\n if (!content) {\n sections.push(\"_No revision content available._\");\n } else {\n sections.push(`_Revision ${content.revisionNumber ?? \"unknown\"}_`);\n if (content.summary) sections.push(\"## Summary\", content.summary);\n if (content.description)\n sections.push(\"## Description\", content.description);\n if (content.impact) sections.push(\"## Business Impact\", content.impact);\n if (content.details) sections.push(\"## Details\", content.details);\n if (content.remediation)\n sections.push(\"## Remediation\", content.remediation);\n if (content.codeReference) {\n sections.push(\n \"## Code reference\",\n `- ${content.codeReference.file}:${content.codeReference.line}`,\n );\n if (content.codeReference.snippet) {\n sections.push(\"```\", content.codeReference.snippet, \"```\");\n }\n }\n if (content.affectedAssets?.length) {\n sections.push(\n \"## Affected Assets\",\n content.affectedAssets\n .map((asset) => `- ${asset.display} (${asset.assetId})`)\n .join(\"\\n\"),\n );\n }\n }\n\n if (context) {\n sections.push(\n \"## Remediation workflow\",\n `- **Suggested branch:** ${context.suggestedBranch ?? \"Not available\"}`,\n `- **Ready for a user-created PR:** ${yesNo(context.workflow.readyForUserCreatedPullRequest)}`,\n `- **GitHub connected:** ${yesNo(context.workflow.githubConnected)}`,\n `- **Automatic retests:** ${context.workflow.automaticRetestsReady ? \"Ready after merge\" : \"Not ready\"}`,\n );\n if (context.workflow.blockers.length) {\n sections.push(\n \"### Blockers\",\n context.workflow.blockers.map((blocker) => `- ${blocker}`).join(\"\\n\"),\n );\n }\n if (context.workflow.nextSteps.length) {\n sections.push(\n \"### Next steps\",\n context.workflow.nextSteps.map((step) => `- ${step}`).join(\"\\n\"),\n );\n }\n\n if (context.linkedPullRequests.length) {\n sections.push(\n \"## Linked GitHub pull requests\",\n context.linkedPullRequests\n .map(\n (pullRequest) =>\n `- [#${pullRequest.number}](${pullRequest.url}) ${escapePipes(pullRequest.title)} — ${pullRequest.state}, linked via ${pullRequest.linkedVia}`,\n )\n .join(\"\\n\"),\n );\n } else {\n sections.push(\"## Linked GitHub pull requests\", \"_None linked._\");\n }\n\n sections.push(\"## Latest retest\");\n if (context.latestRetest.state === \"unavailable\") {\n sections.push(`_${context.latestRetest.reason}_`);\n } else if (context.latestRetest.state === \"none\") {\n sections.push(\"_No retest history is available yet._\");\n } else {\n sections.push(formatRetestSummary(context.latestRetest.retest));\n }\n }\n\n return [...header, \"\", ...sections].join(\"\\n\");\n}\n\nexport function formatFindingStatusUpdate(finding: FindingSummary): string {\n const identifier = finding.identifier ?? finding.findingId;\n return [\n `Updated finding **${identifier}** (internal ID ${finding.findingId}) status to **${formatStatus(finding.status)}**.`,\n `Title: ${finding.title}`,\n ].join(\"\\n\");\n}\n\nexport function formatFindingPullRequestLink(\n data: LinkFindingPullRequestResponse,\n): string {\n const identifier = data.finding.identifier ?? data.finding.findingId;\n const pullRequest = data.pullRequest;\n const readiness = data.workflow.automaticRetestsReady\n ? \"Automatic retesting is ready after this PR merges.\"\n : \"Automatic retesting is not currently ready; review the blockers below.\";\n const blockers = data.workflow.blockers.length\n ? `\\n${data.workflow.blockers.map((blocker) => `- ${blocker}`).join(\"\\n\")}`\n : \"\";\n\n return [\n `Linked existing GitHub pull request [#${pullRequest.number}](${pullRequest.url}) to **${identifier}**.`,\n `Repository: ${pullRequest.repo}; state: ${pullRequest.state}; finding status: ${formatStatus(data.finding.status)}.`,\n \"Odin did not create, modify, merge, or push this pull request.\",\n readiness,\n blockers ? `Workflow blockers:${blockers}` : \"\",\n ]\n .filter(Boolean)\n .join(\"\\n\");\n}\n\nexport function formatFindingRetests(data: RetestHistoryResponse): string {\n if (data.state === \"none\" || data.retests.length === 0) {\n return \"No retest history is available for this finding.\";\n }\n\n return [\n `Retest history (${data.retests.length}):`,\n \"\",\n \"| Retest | Status | Trigger | Verdict | Created | Completed |\",\n \"| --- | --- | --- | --- | --- | --- |\",\n ...data.retests.map(\n (retest) =>\n `| ${retest.retestId} | ${RETEST_STATUS_LABELS[retest.status]} | ${retest.trigger} | ${retest.combinedVerdict ?? \"—\"} | ${formatDate(retest.createdAt)} | ${formatDate(retest.completedAt ?? 0)} |`,\n ),\n \"\",\n \"Queued and running entries are still in progress; completed, skipped, and failed entries are final for that run.\",\n ].join(\"\\n\");\n}\n\nexport function formatPentestsList(data: PentestListResponse): string {\n if (data.pentests.length === 0) return \"No pentests found.\";\n const rows = data.pentests.map(formatPentestRow);\n return [\n \"| Pentest ID | Name | Status | Start | End | Scope |\",\n \"| --- | --- | --- | --- | --- | --- |\",\n ...rows,\n ].join(\"\\n\");\n}\n\nexport function formatPentestDetail(data: PentestDetailResponse): string {\n const p = data.pentest;\n const end = p.endAt ? formatDate(p.endAt) : \"open\";\n const scope = p.scope.length\n ? p.scope.map((s) => `- ${s}`).join(\"\\n\")\n : \"_No scope defined._\";\n return [\n `# ${p.name}`,\n \"\",\n `- **ID:** ${p.pentestId}`,\n `- **Status:** ${p.status}`,\n `- **Start:** ${formatDate(p.startAt)}`,\n `- **End:** ${end}`,\n `- **Created:** ${formatDate(p.createdAt)}`,\n \"\",\n \"## Scope\",\n \"\",\n scope,\n ].join(\"\\n\");\n}\n\nexport function formatFindingsForAsset(findings: FindingSummary[]): string {\n if (findings.length === 0) return \"_No findings reference this asset._\";\n return [\n `## Findings (${findings.length})`,\n \"\",\n \"| Identifier | Internal ID | Title | Severity | Status | Created |\",\n \"| --- | --- | --- | --- | --- | --- |\",\n ...findings.map(formatFindingRow),\n ].join(\"\\n\");\n}\n\nexport function formatAssetsList(data: AssetsListResponse): string {\n const { assets, stats, pagination } = data;\n if (assets.length === 0) return \"No assets match the supplied filters.\";\n const rows = assets.map(formatAssetRow);\n return [\n `Showing ${assets.length} of ${stats.total} asset(s). Offset ${pagination.offset}, limit ${pagination.limit}. hasMore=${pagination.hasMore}.`,\n \"\",\n \"| Asset ID | Type | Value | Source | Status | Last seen |\",\n \"| --- | --- | --- | --- | --- | --- |\",\n ...rows,\n ].join(\"\\n\");\n}\n\nexport function formatAssetDetail(data: AssetDetailResponse): string {\n const { asset, findings } = data;\n const header = [\n `# ${asset.value}`,\n \"\",\n `- **ID:** ${asset.assetId}`,\n `- **Type:** ${asset.type}`,\n `- **Source:** ${asset.source}`,\n `- **Status:** ${asset.status}`,\n `- **Last seen:** ${formatDate(asset.lastSeenAt)}`,\n `- **Created:** ${formatDate(asset.createdAt)}`,\n ];\n\n return [...header, \"\", formatFindingsForAsset(findings)].join(\"\\n\");\n}\n\nfunction formatFindingRow(finding: FindingSummary): string {\n const severity = `${finding.severity.label} (CVSS ${finding.severity.cvss.toFixed(1)})`;\n return `| ${finding.identifier ?? \"—\"} | ${finding.findingId} | ${escapePipes(finding.title)} | ${severity} | ${formatStatus(finding.status)} | ${formatDate(finding.createdAt)} |`;\n}\n\nfunction formatPentestRow(p: Pentest): string {\n const end = p.endAt ? formatDate(p.endAt) : \"open\";\n const scope =\n p.scope.slice(0, 3).join(\", \") + (p.scope.length > 3 ? \", …\" : \"\");\n return `| ${p.pentestId} | ${escapePipes(p.name)} | ${p.status} | ${formatDate(p.startAt)} | ${end} | ${escapePipes(scope)} |`;\n}\n\nfunction formatAssetRow(a: Asset): string {\n return `| ${a.assetId} | ${a.type} | ${escapePipes(a.value)} | ${a.source} | ${a.status} | ${formatDate(a.lastSeenAt)} |`;\n}\n\nfunction formatRetestSummary(retest: {\n status: Retest[\"status\"];\n combinedVerdict: Retest[\"combinedVerdict\"];\n statusAction: Retest[\"statusAction\"];\n trigger: Retest[\"trigger\"];\n createdAt: number;\n completedAt: number | null;\n}): string {\n return [\n `- **Status:** ${RETEST_STATUS_LABELS[retest.status]}`,\n `- **Trigger:** ${retest.trigger}`,\n `- **Verdict:** ${retest.combinedVerdict ?? \"Not available yet\"}`,\n `- **Finding action:** ${retest.statusAction ?? \"None\"}`,\n `- **Created:** ${formatDate(retest.createdAt)}`,\n `- **Completed:** ${formatDate(retest.completedAt ?? 0)}`,\n ].join(\"\\n\");\n}\n\nfunction formatStatus(status: FindingStatus): string {\n return STATUS_LABELS[status] ?? status;\n}\n\nfunction yesNo(value: boolean): string {\n return value ? \"Yes\" : \"No\";\n}\n\nfunction escapePipes(value: string): string {\n return value.replace(/\\|/g, \"\\\\|\");\n}\n\nfunction formatDate(epoch: number): string {\n if (!epoch) return \"—\";\n // Odin stores timestamps as milliseconds for documents and seconds for some fields\n // (pentest startAt/endAt are seconds). Heuristic: values < 10^12 are seconds.\n const ms = epoch < 1e12 ? epoch * 1000 : epoch;\n return new Date(ms).toISOString().slice(0, 10);\n}\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport type { FindingDetailResponse } from \"../types.js\";\n\nexport const findingReferenceFields = {\n identifier: z\n .string()\n .trim()\n .min(1)\n .optional()\n .describe(\n 'Human-readable finding identifier such as \"ODI-12\". Preferred input.',\n ),\n findingId: z\n .string()\n .trim()\n .min(1)\n .optional()\n .describe(\n \"Raw internal finding ID, useful when chaining from another tool.\",\n ),\n};\n\nexport const findingReferenceInput = z\n .object(findingReferenceFields)\n .refine((value) => Boolean(value.identifier || value.findingId), {\n message: \"Provide either identifier or findingId.\",\n });\n\nexport type FindingReferenceInput = z.infer<typeof findingReferenceInput>;\n\n/** Prefer the human reference when both values are supplied. */\nexport function findingReferenceValue(\n reference: FindingReferenceInput,\n): string {\n const value = reference.identifier ?? reference.findingId;\n if (!value) {\n throw new Error(\"A finding identifier or findingId is required.\");\n }\n return value;\n}\n\nexport function findingReferencePath(\n reference: FindingReferenceInput,\n suffix = \"\",\n): string {\n return `/api/findings/${encodeURIComponent(findingReferenceValue(reference))}${suffix}`;\n}\n\n/**\n * Resolves only when a caller supplied an identifier. A supplied internal ID\n * is already suitable for mutation chaining and avoids an unnecessary read.\n */\nexport async function resolveFindingId(\n client: OdinClient,\n reference: FindingReferenceInput,\n): Promise<string> {\n if (reference.findingId) return reference.findingId;\n\n const data = await client.get<FindingDetailResponse>(\n findingReferencePath(reference),\n );\n return data.remediationContext?.findingId ?? data.finding.findingId;\n}\n","import type { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatFindingDetail } from \"../formatters/markdown.js\";\nimport type { FindingDetailResponse } from \"../types.js\";\nimport {\n findingReferenceInput,\n findingReferencePath,\n} from \"./finding-reference.js\";\n\nexport const getFindingInput = findingReferenceInput;\n\nexport const getFindingTool = {\n name: \"get_finding\",\n title: \"Get finding remediation context\",\n description:\n \"Fetch a finding by human identifier such as ODI-12 (preferred) or by internal findingId. Returns bounded remediation context, the suggested branch name, linked GitHub pull requests, workflow readiness, and retest state. Odin only provides context and can link an existing PR; it never creates commits, pushes branches, or creates pull requests.\",\n inputSchema: getFindingInput,\n handler:\n (client: OdinClient) => async (params: z.infer<typeof getFindingInput>) => {\n const data = await client.get<FindingDetailResponse>(\n findingReferencePath(params),\n );\n return {\n markdown: formatFindingDetail(data),\n structured: {\n finding: data.remediationContext ?? data.finding,\n revision: data.remediationContext?.revision ?? data.revision,\n remediationContext: data.remediationContext ?? null,\n },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { OdinApiError } from \"../client.js\";\nimport { formatFindingRetests } from \"../formatters/markdown.js\";\nimport type { RetestHistoryResponse } from \"../types.js\";\nimport {\n findingReferenceFields,\n findingReferencePath,\n} from \"./finding-reference.js\";\n\nexport const getFindingRetestsInput = z\n .object(findingReferenceFields)\n .refine((value) => Boolean(value.identifier || value.findingId), {\n message: \"Provide either identifier or findingId.\",\n });\n\nexport const getFindingRetestsTool = {\n name: \"get_finding_retests\",\n title: \"Get finding retest history\",\n description:\n \"Read the retest history for a finding by human identifier such as ODI-12 or internal findingId. Distinguishes no history, queued or running, completed, skipped, failed, and gated/unavailable results. This tool never triggers a retest or spends credits.\",\n inputSchema: getFindingRetestsInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof getFindingRetestsInput>) => {\n try {\n const data = await client.get<RetestHistoryResponse>(\n findingReferencePath(params, \"/retests\"),\n );\n return {\n markdown: formatFindingRetests(data),\n structured: {\n state: data.state,\n retests: data.retests,\n },\n };\n } catch (error) {\n if (\n error instanceof OdinApiError &&\n error.status === 404 &&\n isRetestHistoryUnavailable(error.body)\n ) {\n const reason =\n \"Retest history is gated or unavailable for this organisation.\";\n return {\n markdown: reason,\n structured: { state: \"unavailable\", reason, retests: [] },\n };\n }\n throw error;\n }\n },\n};\n\nfunction isRetestHistoryUnavailable(body: unknown): boolean {\n return (\n typeof body === \"object\" &&\n body !== null &&\n \"code\" in body &&\n (body as { code?: unknown }).code === \"RETEST_HISTORY_UNAVAILABLE\"\n );\n}\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatPentestDetail } from \"../formatters/markdown.js\";\nimport type { PentestDetailResponse } from \"../types.js\";\n\nexport const getPentestInput = z.object({\n pentestId: z.string().min(1),\n});\n\nexport const getPentestTool = {\n name: \"get_pentest\",\n title: \"Get pentest\",\n description:\n \"Fetch a single pentest by ID, including status, scheduled window, and in-scope targets.\",\n inputSchema: getPentestInput,\n handler:\n (client: OdinClient) => async (params: z.infer<typeof getPentestInput>) => {\n const data = await client.get<PentestDetailResponse>(\n `/api/pentests/${encodeURIComponent(params.pentestId)}`,\n );\n return {\n markdown: formatPentestDetail(data),\n structured: { pentest: data.pentest },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatFindingPullRequestLink } from \"../formatters/markdown.js\";\nimport type { LinkFindingPullRequestResponse } from \"../types.js\";\nimport {\n findingReferenceFields,\n findingReferencePath,\n} from \"./finding-reference.js\";\n\nexport const linkFindingPullRequestInput = z\n .object({\n ...findingReferenceFields,\n url: z\n .string()\n .url()\n .describe(\"Existing GitHub pull request URL to link to the finding.\"),\n })\n .refine((value) => Boolean(value.identifier || value.findingId), {\n message: \"Provide either identifier or findingId.\",\n });\n\nexport const linkFindingPullRequestTool = {\n name: \"link_finding_pull_request\",\n title: \"Link an existing GitHub pull request\",\n description:\n \"Link an existing accessible GitHub pull request to a finding identified by ODI-12 or findingId. This operation is idempotent. It does not create, modify, merge, or push a pull request, commit, or branch, and it requires an API key with Read & Write permission; cookie-authenticated callers also need member role. Ask the user before linking.\",\n inputSchema: linkFindingPullRequestInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof linkFindingPullRequestInput>) => {\n const data = await client.post<LinkFindingPullRequestResponse>(\n findingReferencePath(params, \"/link-pr\"),\n { url: params.url },\n );\n return {\n markdown: formatFindingPullRequestLink(data),\n structured: {\n finding: data.finding,\n pullRequest: data.pullRequest,\n linkSource: data.linkSource,\n workflow: data.workflow,\n },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatAssetsList } from \"../formatters/markdown.js\";\nimport type { AssetsListResponse } from \"../types.js\";\n\nexport const listAssetsInput = z.object({\n type: z.enum([\"DOMAIN\", \"SUBDOMAIN\", \"IP\"]).optional(),\n source: z.enum([\"MANUAL\", \"RECON\", \"IMPORT\", \"AGENT\"]).optional(),\n status: z\n .string()\n .optional()\n .describe(\n \"Dynamic asset status set by recon (e.g. ACTIVE, INACTIVE, TLS_ERROR).\",\n ),\n search: z.string().optional(),\n sortBy: z.enum([\"lastSeenAt\", \"createdAt\", \"value\"]).optional(),\n sortOrder: z.enum([\"asc\", \"desc\"]).optional(),\n limit: z.number().int().min(1).max(100).optional().default(50),\n offset: z.number().int().min(0).optional().default(0),\n});\n\nexport const listAssetsTool = {\n name: \"list_assets\",\n title: \"List assets\",\n description:\n \"List attack-surface assets for the organisation. Supports filtering by type, source, status, and free-text search.\",\n inputSchema: listAssetsInput,\n handler:\n (client: OdinClient) => async (params: z.infer<typeof listAssetsInput>) => {\n const data = await client.get<AssetsListResponse>(\"/api/assets\", params);\n return {\n markdown: formatAssetsList(data),\n structured: {\n assets: data.assets,\n stats: data.stats,\n pagination: data.pagination,\n },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatFindingsList } from \"../formatters/markdown.js\";\nimport type { FindingsListResponse } from \"../types.js\";\n\nexport const listFindingsInput = z.object({\n pentestId: z.string().optional().describe(\"Filter to a single pentest.\"),\n assetId: z\n .string()\n .optional()\n .describe(\n \"Filter to findings whose latest revision references this asset.\",\n ),\n repository: z\n .string()\n .optional()\n .describe(\"Filter to a source repository such as owner/name.\"),\n severity: z\n .enum([\"CRITICAL\", \"HIGH\", \"MEDIUM\", \"LOW\", \"INFORMATIONAL\"])\n .optional(),\n status: z\n .enum([\n \"REPORTED\",\n \"ACKNOWLEDGED\",\n \"MITIGATING\",\n \"OPEN_FOR_RETEST\",\n \"NEEDS_REVISION\",\n \"FIXED_AND_RETESTED\",\n ])\n .optional(),\n search: z\n .string()\n .optional()\n .describe(\"Free-text search on finding titles and human identifiers.\"),\n sortBy: z.enum([\"severity\", \"createdAt\"]).optional(),\n sortOrder: z.enum([\"asc\", \"desc\"]).optional(),\n limit: z.number().int().min(1).max(200).optional().default(50),\n offset: z.number().int().min(0).optional().default(0),\n});\n\nexport const listFindingsTool = {\n name: \"list_findings\",\n title: \"List findings\",\n description:\n \"List security findings for the organisation. Supports filtering by pentest, asset, repository, severity, current client-visible status, and searching finding titles or human identifiers such as ODI-12.\",\n inputSchema: listFindingsInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof listFindingsInput>) => {\n const data = await client.get<FindingsListResponse>(\n \"/api/findings\",\n params,\n );\n return {\n markdown: formatFindingsList(data),\n structured: {\n findings: data.findings,\n stats: data.stats,\n pagination: data.pagination,\n },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatPentestsList } from \"../formatters/markdown.js\";\nimport type { PentestListResponse } from \"../types.js\";\n\nexport const listPentestsInput = z.object({});\n\nexport const listPentestsTool = {\n name: \"list_pentests\",\n title: \"List pentests\",\n description:\n \"List all pentests for the organisation, including status, scheduled window, and in-scope targets.\",\n inputSchema: listPentestsInput,\n handler: (client: OdinClient) => async () => {\n const data = await client.get<PentestListResponse>(\"/api/pentests\");\n return {\n markdown: formatPentestsList(data),\n structured: { pentests: data.pentests },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatFindingStatusUpdate } from \"../formatters/markdown.js\";\nimport type { FindingStatusUpdateResponse } from \"../types.js\";\nimport {\n findingReferenceFields,\n resolveFindingId,\n} from \"./finding-reference.js\";\n\nexport const updateFindingStatusInput = z\n .object({\n ...findingReferenceFields,\n status: z\n .enum([\"MITIGATING\", \"OPEN_FOR_RETEST\", \"ACKNOWLEDGED\"])\n .describe(\n \"Target status. MITIGATING means work has started, OPEN_FOR_RETEST means the fix is ready for verification, and ACKNOWLEDGED means the organisation accepts the risk for now.\",\n ),\n })\n .refine((value) => Boolean(value.identifier || value.findingId), {\n message: \"Provide either identifier or findingId.\",\n });\n\nexport const updateFindingStatusTool = {\n name: \"update_finding_status\",\n title: \"Update finding status\",\n description:\n \"Change a finding's status by human identifier such as ODI-12 or internal findingId. Requires an API key with Read & Write permission; cookie-authenticated callers also need member role. Only use this after the user explicitly asks for the status change; a matching pull request is not permission to mutate a finding.\",\n inputSchema: updateFindingStatusInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof updateFindingStatusInput>) => {\n const findingId = await resolveFindingId(client, params);\n const data = await client.patch<FindingStatusUpdateResponse>(\n `/api/findings/${encodeURIComponent(findingId)}/status`,\n { status: params.status },\n );\n return {\n markdown: formatFindingStatusUpdate(data.finding),\n structured: { finding: data.finding },\n };\n },\n};\n"],"mappings":"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","names":["z","z","z","z","z","z","z","z","z","z","z","z","z","z","z"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@borgresearch/odin-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "MCP server for the Odin security API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"registry": "https://registry.npmjs.org/"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
|
-
"node": ">=
|
|
45
|
+
"node": ">=24.0.0"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "tsup",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@biomejs/biome": "2.3.14",
|
|
64
64
|
"@modelcontextprotocol/inspector": "^0.21.2",
|
|
65
|
-
"@types/node": "^
|
|
65
|
+
"@types/node": "^24.10.1",
|
|
66
66
|
"tsup": "^8.3.5",
|
|
67
67
|
"typescript": "^5.9.3",
|
|
68
68
|
"vitest": "^4.1.0"
|