@securityreviewai/security-review-mcp 0.2.9
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 +264 -0
- package/dist/api/client.js +345 -0
- package/dist/bin/security-review-mcp.js +172 -0
- package/dist/index.js +1 -0
- package/dist/integrations/confluenceClient.js +161 -0
- package/dist/integrations/jiraClient.js +133 -0
- package/dist/prompts/analysisPrompts.js +496 -0
- package/dist/prompts/generationPrompts.js +229 -0
- package/dist/resources/reviewResources.js +101 -0
- package/dist/server.js +38 -0
- package/dist/tools/common.js +14 -0
- package/dist/tools/documentTools.js +140 -0
- package/dist/tools/integrationTools.js +100 -0
- package/dist/tools/projectTools.js +104 -0
- package/dist/tools/reviewArtifactTools.js +61 -0
- package/dist/tools/reviewTools.js +405 -0
- package/dist/tools/workflowTools.js +105 -0
- package/dist/utils/env.js +46 -0
- package/dist/utils/serialization.js +27 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Security Review MCP
|
|
2
|
+
|
|
3
|
+
TypeScript MCP server for [SecurityReview.ai](https://securityreview.ai), published as an `npx`-runnable package.
|
|
4
|
+
|
|
5
|
+
- Pure Node runtime (no Python bootstrap)
|
|
6
|
+
- Stdio MCP server compatible with Cursor, Windsurf, Claude Desktop, ChatGPT MCP, and other MCP clients
|
|
7
|
+
- 53 tools for project/document/review/workflow/integration operations
|
|
8
|
+
- 8 built-in security-analysis prompts
|
|
9
|
+
- 4 read-only MCP resources
|
|
10
|
+
|
|
11
|
+
## Install / Run
|
|
12
|
+
|
|
13
|
+
Run directly with `npx`:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx -y security-review-mcp \
|
|
17
|
+
--api-url https://api.example.com \
|
|
18
|
+
--api-token YOUR_API_TOKEN
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or provide credentials via `.env` in your current working directory:
|
|
22
|
+
|
|
23
|
+
```env
|
|
24
|
+
SECURITY_REVIEW_API_URL=https://api.example.com
|
|
25
|
+
SECURITY_REVIEW_API_TOKEN=YOUR_API_TOKEN
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then run:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx -y security-review-mcp
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## MCP Client Configuration
|
|
35
|
+
|
|
36
|
+
### Cursor / Windsurf
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"security-review-mcp": {
|
|
42
|
+
"command": "npx",
|
|
43
|
+
"args": ["-y", "security-review-mcp"],
|
|
44
|
+
"env": {
|
|
45
|
+
"SECURITY_REVIEW_API_URL": "https://api.example.com",
|
|
46
|
+
"SECURITY_REVIEW_API_TOKEN": "YOUR_API_TOKEN"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### With Jira + Confluence Integration
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"security-review-mcp": {
|
|
59
|
+
"command": "npx",
|
|
60
|
+
"args": ["-y", "security-review-mcp"],
|
|
61
|
+
"env": {
|
|
62
|
+
"SECURITY_REVIEW_API_URL": "https://api.example.com",
|
|
63
|
+
"SECURITY_REVIEW_API_TOKEN": "YOUR_API_TOKEN",
|
|
64
|
+
"JIRA_BASE_URL": "https://yourcompany.atlassian.net",
|
|
65
|
+
"JIRA_EMAIL": "you@company.com",
|
|
66
|
+
"JIRA_API_TOKEN": "YOUR_JIRA_TOKEN",
|
|
67
|
+
"CONFLUENCE_BASE_URL": "https://yourcompany.atlassian.net",
|
|
68
|
+
"CONFLUENCE_EMAIL": "you@company.com",
|
|
69
|
+
"CONFLUENCE_API_TOKEN": "YOUR_CONFLUENCE_TOKEN"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Environment Variables
|
|
77
|
+
|
|
78
|
+
| Variable | Required | Description |
|
|
79
|
+
|---|---:|---|
|
|
80
|
+
| `SECURITY_REVIEW_API_URL` | Yes* | SecurityReview API base URL |
|
|
81
|
+
| `SECURITY_REVIEW_API_TOKEN` | Yes* | SecurityReview API bearer token |
|
|
82
|
+
| `SRAI_API_URL` | Yes* | Backward-compatible API URL alias |
|
|
83
|
+
| `SRAI_API_TOKEN` | Yes* | Backward-compatible API token alias |
|
|
84
|
+
| `JIRA_BASE_URL` | No | Jira base URL |
|
|
85
|
+
| `JIRA_EMAIL` | No | Jira user email |
|
|
86
|
+
| `JIRA_API_TOKEN` | No | Jira API token |
|
|
87
|
+
| `CONFLUENCE_BASE_URL` | No | Confluence base URL |
|
|
88
|
+
| `CONFLUENCE_EMAIL` | No | Confluence user email |
|
|
89
|
+
| `CONFLUENCE_API_TOKEN` | No | Confluence API token |
|
|
90
|
+
|
|
91
|
+
\* Either `SECURITY_REVIEW_*` or `SRAI_*` must be present.
|
|
92
|
+
|
|
93
|
+
## CLI Options
|
|
94
|
+
|
|
95
|
+
- `--api-url <url>`
|
|
96
|
+
- `--api-token <token>`
|
|
97
|
+
- `--env-file <path>`
|
|
98
|
+
- `--jira-base-url <url>`
|
|
99
|
+
- `--jira-email <email>`
|
|
100
|
+
- `--jira-api-token <token>`
|
|
101
|
+
- `--confluence-base-url <url>`
|
|
102
|
+
- `--confluence-email <email>`
|
|
103
|
+
- `--confluence-api-token <token>`
|
|
104
|
+
- `--print-config`
|
|
105
|
+
- `--help`
|
|
106
|
+
|
|
107
|
+
Compatibility flags (no-op, retained for older configs):
|
|
108
|
+
- `--python <path>`
|
|
109
|
+
- `--force-install`
|
|
110
|
+
|
|
111
|
+
## Tool Catalog (53)
|
|
112
|
+
|
|
113
|
+
### Projects
|
|
114
|
+
|
|
115
|
+
| Tool | Purpose |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `list_projects` | List all projects |
|
|
118
|
+
| `get_project` | Get project details by ID |
|
|
119
|
+
| `create_project` | Create a new project |
|
|
120
|
+
| `get_project_settings` | Get project settings/configuration |
|
|
121
|
+
| `get_project_profile` | Get project profile hub summary |
|
|
122
|
+
| `get_full_project_profile` | Get full project profile graph payload |
|
|
123
|
+
| `get_project_profile_description` | Get project profile description |
|
|
124
|
+
| `list_profile_technology_categories` | List technology categories in project profile |
|
|
125
|
+
| `get_project_profile_technology_category` | Get one technology category by name |
|
|
126
|
+
| `list_project_profile_architecture_notes` | List architecture notes in project profile |
|
|
127
|
+
| `list_project_profile_user_groups` | List user groups in project profile |
|
|
128
|
+
| `list_project_profile_language_stacks` | List language stacks in project profile |
|
|
129
|
+
| `list_project_profile_security_controls` | List security controls in project profile |
|
|
130
|
+
| `get_project_profile_security_control` | Get one security control by ID |
|
|
131
|
+
| `list_profile_compliance_requirements` | List compliance requirements in project profile |
|
|
132
|
+
| `get_profile_compliance_requirement` | Get one compliance requirement by ID |
|
|
133
|
+
|
|
134
|
+
### Documents
|
|
135
|
+
|
|
136
|
+
| Tool | Purpose |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `list_documents` | List project documents |
|
|
139
|
+
| `get_document` | Get document details by ID |
|
|
140
|
+
| `get_document_evaluation` | Get processed document evaluation |
|
|
141
|
+
| `upload_component_diagram` | Upload component diagram from local file path |
|
|
142
|
+
| `upload_document` | Upload base64 file content |
|
|
143
|
+
| `create_document_from_content` | Create/upload a text document from content |
|
|
144
|
+
| `link_external_document` | Link Jira/Confluence/GitHub document references |
|
|
145
|
+
|
|
146
|
+
### Reviews
|
|
147
|
+
|
|
148
|
+
| Tool | Purpose |
|
|
149
|
+
|---|---|
|
|
150
|
+
| `find_project_by_name` | Fuzzy project lookup by name |
|
|
151
|
+
| `list_reviews` | List reviews in a project |
|
|
152
|
+
| `list_reviews_by_project_name` | Resolve project name then list reviews |
|
|
153
|
+
| `get_review` | Get review details by ID |
|
|
154
|
+
| `get_review_overview` | Build intent-focused review summary and artifacts |
|
|
155
|
+
| `create_review` | Create a review with step config |
|
|
156
|
+
| `get_review_resource_documents` | List available docs for review creation |
|
|
157
|
+
| `get_review_resource_compliance` | List available compliance frameworks |
|
|
158
|
+
|
|
159
|
+
### Workflow
|
|
160
|
+
|
|
161
|
+
| Tool | Purpose |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `list_ai_ide_workflows` | List AI IDE workflows in a project |
|
|
164
|
+
| `get_ai_ide_workflow` | Get AI IDE workflow by ID |
|
|
165
|
+
| `create_ai_ide_workflow` | Create AI IDE workflow for a project |
|
|
166
|
+
| `create_ai_ide_event` | Create AI IDE event within a workflow |
|
|
167
|
+
| `start_workflow` | Start review workflow |
|
|
168
|
+
| `get_workflow_status` | Get workflow status |
|
|
169
|
+
| `get_workflow_job_status` | Get status for a specific job |
|
|
170
|
+
| `start_next_workflow_job` | Start next pending job |
|
|
171
|
+
| `start_workflow_job` | Start specific job by ID |
|
|
172
|
+
| `retry_workflow_job` | Retry a failed job |
|
|
173
|
+
|
|
174
|
+
### Integrations
|
|
175
|
+
|
|
176
|
+
| Tool | Purpose |
|
|
177
|
+
|---|---|
|
|
178
|
+
| `fetch_jira_issue` | Fetch Jira issue content |
|
|
179
|
+
| `fetch_confluence_page` | Fetch Confluence page by ID or URL |
|
|
180
|
+
| `search_confluence_pages` | Search Confluence via CQL |
|
|
181
|
+
| `fetch_and_link_to_srai` | Link Jira/Confluence source to SRAI project |
|
|
182
|
+
|
|
183
|
+
### Review Artifacts
|
|
184
|
+
|
|
185
|
+
| Tool | Purpose |
|
|
186
|
+
|---|---|
|
|
187
|
+
| `get_security_objectives` | Fetch review security objectives |
|
|
188
|
+
| `get_threat_scenarios` | Fetch threat scenarios |
|
|
189
|
+
| `get_countermeasures` | Fetch countermeasures |
|
|
190
|
+
| `get_components` | Fetch identified system components/entities |
|
|
191
|
+
| `get_data_dictionaries` | Fetch data dictionaries |
|
|
192
|
+
| `get_questions` | Fetch generated security questions |
|
|
193
|
+
| `get_findings` | Fetch review findings |
|
|
194
|
+
| `get_security_test_cases` | Fetch security test cases |
|
|
195
|
+
|
|
196
|
+
## Prompt Catalog (8)
|
|
197
|
+
|
|
198
|
+
| Prompt | Purpose |
|
|
199
|
+
|---|---|
|
|
200
|
+
| `full_security_analysis` | End-to-end structured security review |
|
|
201
|
+
| `security_objectives_analysis` | Security objectives and requirement extraction |
|
|
202
|
+
| `component_analysis` | Architecture/component extraction |
|
|
203
|
+
| `data_dictionary_analysis` | Sensitive data and stepping stone identification |
|
|
204
|
+
| `threat_analysis` | Threat generation with CVSS-oriented guidance |
|
|
205
|
+
| `countermeasure_analysis` | Mitigation/control generation |
|
|
206
|
+
| `code_security_review` | Security-focused code review guidance |
|
|
207
|
+
| `document_generation` | Generate architecture/API/deployment documents from brief input |
|
|
208
|
+
|
|
209
|
+
## Resource Catalog (4)
|
|
210
|
+
|
|
211
|
+
| Resource URI | Description |
|
|
212
|
+
|---|---|
|
|
213
|
+
| `srai://projects` | List all projects |
|
|
214
|
+
| `srai://projects/{project_id}/reviews` | List project reviews |
|
|
215
|
+
| `srai://projects/{project_id}/reviews/{review_id}/summary` | Review summary |
|
|
216
|
+
| `srai://projects/{project_id}/documents` | List project documents |
|
|
217
|
+
|
|
218
|
+
## Typical Workflows
|
|
219
|
+
|
|
220
|
+
### 1) Review Existing Project Artifacts
|
|
221
|
+
|
|
222
|
+
1. `find_project_by_name`
|
|
223
|
+
2. `list_reviews_by_project_name`
|
|
224
|
+
3. `get_review_overview`
|
|
225
|
+
4. `get_threat_scenarios` / `get_findings`
|
|
226
|
+
|
|
227
|
+
### 2) Create a New Review from Documents
|
|
228
|
+
|
|
229
|
+
1. `create_project`
|
|
230
|
+
2. `upload_document` or `create_document_from_content`
|
|
231
|
+
3. `create_review`
|
|
232
|
+
4. `start_workflow`
|
|
233
|
+
5. `get_workflow_status`
|
|
234
|
+
|
|
235
|
+
### 3) Bring in Jira/Confluence Context
|
|
236
|
+
|
|
237
|
+
1. `fetch_jira_issue` or `fetch_confluence_page`
|
|
238
|
+
2. `link_external_document` or `fetch_and_link_to_srai`
|
|
239
|
+
3. `create_review`
|
|
240
|
+
|
|
241
|
+
## Troubleshooting
|
|
242
|
+
|
|
243
|
+
| Issue | Cause | Fix |
|
|
244
|
+
|---|---|---|
|
|
245
|
+
| `Missing API URL` | API URL not set | Set `SECURITY_REVIEW_API_URL` or `SRAI_API_URL` |
|
|
246
|
+
| `Missing API token` | API token not set | Set `SECURITY_REVIEW_API_TOKEN` or `SRAI_API_TOKEN` |
|
|
247
|
+
| Integration tool returns config error | Jira/Confluence env vars missing | Set required integration env vars |
|
|
248
|
+
| `npx` install errors | Network/auth/cache issue | Retry with a valid npm auth/network setup; clear/fix npm cache if needed |
|
|
249
|
+
|
|
250
|
+
## Development
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
npm install
|
|
254
|
+
npm run build
|
|
255
|
+
node dist/bin/security-review-mcp.js --help
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Runtime Requirements
|
|
259
|
+
|
|
260
|
+
- Node.js `>=18`
|
|
261
|
+
|
|
262
|
+
## License
|
|
263
|
+
|
|
264
|
+
UNLICENSED
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
export const DEFAULT_TIMEOUT_MS = 60_000;
|
|
2
|
+
export class SraiApiError extends Error {
|
|
3
|
+
statusCode;
|
|
4
|
+
detail;
|
|
5
|
+
url;
|
|
6
|
+
constructor(statusCode, detail, url = "") {
|
|
7
|
+
super(`SRAI API error ${statusCode} on ${url}: ${detail}`);
|
|
8
|
+
this.statusCode = statusCode;
|
|
9
|
+
this.detail = detail;
|
|
10
|
+
this.url = url;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class SraiApiClient {
|
|
14
|
+
baseUrl;
|
|
15
|
+
apiToken;
|
|
16
|
+
timeoutMs;
|
|
17
|
+
constructor(baseUrl, apiToken, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
18
|
+
let configuredBaseUrl = (baseUrl || process.env.SRAI_API_URL || "").replace(/\/+$/u, "");
|
|
19
|
+
if (configuredBaseUrl.endsWith("/api")) {
|
|
20
|
+
configuredBaseUrl = configuredBaseUrl.slice(0, -4);
|
|
21
|
+
}
|
|
22
|
+
const configuredToken = apiToken || process.env.SRAI_API_TOKEN || "";
|
|
23
|
+
if (!configuredBaseUrl) {
|
|
24
|
+
throw new Error("SRAI_API_URL is not configured. Set the environment variable.");
|
|
25
|
+
}
|
|
26
|
+
if (!configuredToken) {
|
|
27
|
+
throw new Error("SRAI_API_TOKEN is not configured. Set the environment variable.");
|
|
28
|
+
}
|
|
29
|
+
this.baseUrl = configuredBaseUrl;
|
|
30
|
+
this.apiToken = configuredToken;
|
|
31
|
+
this.timeoutMs = timeoutMs;
|
|
32
|
+
}
|
|
33
|
+
headers(options = {}) {
|
|
34
|
+
const { includeJsonAccept = true, includeJsonContentType = false } = options;
|
|
35
|
+
return {
|
|
36
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
37
|
+
...(includeJsonAccept ? { Accept: "application/json" } : {}),
|
|
38
|
+
...(includeJsonContentType ? { "Content-Type": "application/json" } : {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
buildUrl(pathname, params) {
|
|
42
|
+
const url = new URL(`${this.baseUrl}${pathname}`);
|
|
43
|
+
if (params) {
|
|
44
|
+
for (const [key, rawValue] of Object.entries(params)) {
|
|
45
|
+
if (rawValue === undefined) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
url.searchParams.set(key, String(rawValue));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return url.toString();
|
|
52
|
+
}
|
|
53
|
+
async request(method, path, options = {}) {
|
|
54
|
+
const url = this.buildUrl(path, options.params);
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
57
|
+
try {
|
|
58
|
+
const hasFormData = Boolean(options.formData);
|
|
59
|
+
const hasJsonBody = options.jsonBody !== undefined;
|
|
60
|
+
const requestInit = {
|
|
61
|
+
method,
|
|
62
|
+
headers: this.headers({
|
|
63
|
+
includeJsonAccept: !hasFormData,
|
|
64
|
+
includeJsonContentType: !hasFormData && hasJsonBody,
|
|
65
|
+
}),
|
|
66
|
+
redirect: "follow",
|
|
67
|
+
signal: controller.signal,
|
|
68
|
+
};
|
|
69
|
+
if (options.formData) {
|
|
70
|
+
requestInit.body = options.formData;
|
|
71
|
+
}
|
|
72
|
+
else if (options.jsonBody === undefined) {
|
|
73
|
+
requestInit.body = null;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
requestInit.body = toJsonRequestBody(options.jsonBody);
|
|
77
|
+
}
|
|
78
|
+
const response = await fetch(url, requestInit);
|
|
79
|
+
if (response.status >= 400) {
|
|
80
|
+
const detail = await parseErrorBody(response);
|
|
81
|
+
throw new SraiApiError(response.status, detail, url);
|
|
82
|
+
}
|
|
83
|
+
if (response.status === 204) {
|
|
84
|
+
return { status: "ok" };
|
|
85
|
+
}
|
|
86
|
+
const text = await response.text();
|
|
87
|
+
if (!text) {
|
|
88
|
+
return { status: "ok" };
|
|
89
|
+
}
|
|
90
|
+
const contentType = (response.headers.get("content-type") || "").toLowerCase();
|
|
91
|
+
if (!contentType.includes("application/json")) {
|
|
92
|
+
const preview = text.slice(0, 300);
|
|
93
|
+
throw new SraiApiError(response.status, `Expected JSON response from SRAI API but got non-JSON content. Content-Type='${contentType}'. Response starts with: ${preview}`, url);
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(text);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
throw new SraiApiError(response.status, `Failed to parse JSON response: ${String(error)}`, url);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error instanceof SraiApiError) {
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
107
|
+
throw new SraiApiError(408, `Request timed out after ${this.timeoutMs}ms`, url);
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
clearTimeout(timeout);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async listProjects() {
|
|
116
|
+
return this.request("GET", "/api/projects");
|
|
117
|
+
}
|
|
118
|
+
async getProject(projectId) {
|
|
119
|
+
return this.request("GET", `/api/projects/${projectId}`);
|
|
120
|
+
}
|
|
121
|
+
async createProject(name, description) {
|
|
122
|
+
return this.request("POST", "/api/projects", {
|
|
123
|
+
jsonBody: { name, description },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async getProjectSettings(projectId) {
|
|
127
|
+
return this.request("GET", `/api/projects/${projectId}/settings`);
|
|
128
|
+
}
|
|
129
|
+
async getLatestProfileVersion(projectId) {
|
|
130
|
+
return this.request("GET", `/api/projects/${projectId}/profile-versions/`);
|
|
131
|
+
}
|
|
132
|
+
async getProjectProfile(projectId) {
|
|
133
|
+
return this.request("GET", `/api/projects/${projectId}/profile`);
|
|
134
|
+
}
|
|
135
|
+
async getFullProjectProfile(projectId) {
|
|
136
|
+
return this.request("GET", `/api/projects/${projectId}/profile/full`);
|
|
137
|
+
}
|
|
138
|
+
async getProjectProfileDescription(projectId) {
|
|
139
|
+
return this.request("GET", `/api/projects/${projectId}/profile/description`);
|
|
140
|
+
}
|
|
141
|
+
async listProjectProfileTechnologyCategories(projectId) {
|
|
142
|
+
return this.request("GET", `/api/projects/${projectId}/profile/technology-categories`);
|
|
143
|
+
}
|
|
144
|
+
async getProjectProfileTechnologyCategory(projectId, categoryName) {
|
|
145
|
+
return this.request("GET", `/api/projects/${projectId}/profile/technology-categories/${encodeURIComponent(categoryName)}`);
|
|
146
|
+
}
|
|
147
|
+
async listProjectProfileArchitectureNotes(projectId) {
|
|
148
|
+
return this.request("GET", `/api/projects/${projectId}/profile/architecture-notes`);
|
|
149
|
+
}
|
|
150
|
+
async listProjectProfileUserGroups(projectId, groupType) {
|
|
151
|
+
return this.request("GET", `/api/projects/${projectId}/profile/user-groups`, {
|
|
152
|
+
params: {
|
|
153
|
+
group_type: groupType,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
async listProjectProfileLanguageStacks(projectId) {
|
|
158
|
+
return this.request("GET", `/api/projects/${projectId}/profile/language-stacks`);
|
|
159
|
+
}
|
|
160
|
+
async listProjectProfileSecurityControls(projectId, status) {
|
|
161
|
+
return this.request("GET", `/api/projects/${projectId}/profile/security-controls`, {
|
|
162
|
+
params: {
|
|
163
|
+
status,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
async getProjectProfileSecurityControl(projectId, controlId) {
|
|
168
|
+
return this.request("GET", `/api/projects/${projectId}/profile/security-controls/${controlId}`);
|
|
169
|
+
}
|
|
170
|
+
async listProjectProfileComplianceRequirements(projectId) {
|
|
171
|
+
return this.request("GET", `/api/projects/${projectId}/profile/compliance-requirements`);
|
|
172
|
+
}
|
|
173
|
+
async getProjectProfileComplianceRequirement(projectId, requirementId) {
|
|
174
|
+
return this.request("GET", `/api/projects/${projectId}/profile/compliance-requirements/${requirementId}`);
|
|
175
|
+
}
|
|
176
|
+
async listDocuments(projectId) {
|
|
177
|
+
return this.request("GET", `/api/projects/${projectId}/documents`);
|
|
178
|
+
}
|
|
179
|
+
async getDocument(projectId, documentId) {
|
|
180
|
+
return this.request("GET", `/api/projects/${projectId}/documents/${documentId}`);
|
|
181
|
+
}
|
|
182
|
+
async getDocumentEvaluation(projectId, documentId) {
|
|
183
|
+
return this.request("GET", `/api/projects/${projectId}/documents/${documentId}/evaluation`);
|
|
184
|
+
}
|
|
185
|
+
async uploadDocument(projectId, name, description, documentType, fileContent, fileName) {
|
|
186
|
+
const form = new FormData();
|
|
187
|
+
form.set("name", name);
|
|
188
|
+
form.set("description", description);
|
|
189
|
+
form.set("document_type", documentType);
|
|
190
|
+
form.set("file", new Blob([toArrayBuffer(fileContent)], { type: "application/octet-stream" }), fileName);
|
|
191
|
+
return this.request("POST", `/api/projects/${projectId}/documents/upload`, {
|
|
192
|
+
formData: form,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
async uploadComponentDiagram(projectId, name, description, fileContent, fileName, contentType = "application/octet-stream") {
|
|
196
|
+
const form = new FormData();
|
|
197
|
+
form.set("name", name);
|
|
198
|
+
form.set("description", description);
|
|
199
|
+
form.set("document_type", "Component Diagram");
|
|
200
|
+
form.set("file", new Blob([toArrayBuffer(fileContent)], { type: contentType }), fileName);
|
|
201
|
+
return this.request("POST", `/api/projects/${projectId}/documents/upload`, {
|
|
202
|
+
formData: form,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
async linkDocument(projectId, payload) {
|
|
206
|
+
return this.request("POST", `/api/projects/${projectId}/documents/link`, {
|
|
207
|
+
jsonBody: payload,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
async listReviews(projectId) {
|
|
211
|
+
return this.request("GET", `/api/projects/${projectId}/reviews`);
|
|
212
|
+
}
|
|
213
|
+
async getReview(projectId, reviewId) {
|
|
214
|
+
return this.request("GET", `/api/projects/${projectId}/reviews/${reviewId}`);
|
|
215
|
+
}
|
|
216
|
+
async createReview(projectId, reviewConfig) {
|
|
217
|
+
return this.request("POST", `/api/projects/${projectId}/reviews`, {
|
|
218
|
+
jsonBody: reviewConfig,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
async getReviewResourceDocuments(projectId) {
|
|
222
|
+
return this.request("GET", `/api/projects/${projectId}/reviews/resources/documents`);
|
|
223
|
+
}
|
|
224
|
+
async getReviewResourceComponentDiagrams(projectId) {
|
|
225
|
+
return this.request("GET", `/api/projects/${projectId}/reviews/resources/component-diagrams`);
|
|
226
|
+
}
|
|
227
|
+
async getReviewResourceCompliance(projectId) {
|
|
228
|
+
return this.request("GET", `/api/projects/${projectId}/reviews/resources/compliance`);
|
|
229
|
+
}
|
|
230
|
+
async startWorkflow(projectId, reviewId) {
|
|
231
|
+
return this.request("POST", `/api/projects/${projectId}/reviews/${reviewId}/workflow/start`);
|
|
232
|
+
}
|
|
233
|
+
async getWorkflowStatus(projectId, reviewId) {
|
|
234
|
+
return this.request("GET", `/api/projects/${projectId}/reviews/${reviewId}/workflow`);
|
|
235
|
+
}
|
|
236
|
+
async getWorkflowJobStatus(projectId, reviewId, jobId) {
|
|
237
|
+
return this.request("GET", `/api/projects/${projectId}/reviews/${reviewId}/workflow/status/${jobId}`);
|
|
238
|
+
}
|
|
239
|
+
async startWorkflowJob(projectId, reviewId, jobId) {
|
|
240
|
+
return this.request("POST", `/api/projects/${projectId}/reviews/${reviewId}/workflow/jobs/${jobId}/start`);
|
|
241
|
+
}
|
|
242
|
+
async retryWorkflowJob(projectId, reviewId, jobId) {
|
|
243
|
+
return this.request("POST", `/api/projects/${projectId}/reviews/${reviewId}/workflow/jobs/${jobId}/retry`);
|
|
244
|
+
}
|
|
245
|
+
async getNextWorkflowJob(projectId, reviewId) {
|
|
246
|
+
return this.request("POST", `/api/projects/${projectId}/reviews/${reviewId}/workflow/next`);
|
|
247
|
+
}
|
|
248
|
+
async createAiIdeWorkflow(projectId, name, description) {
|
|
249
|
+
return this.request("POST", `/api/projects/${projectId}/ai-ide/workflows`, {
|
|
250
|
+
jsonBody: {
|
|
251
|
+
name,
|
|
252
|
+
description,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
async listAiIdeWorkflows(projectId, page = 1, pageSize = 10) {
|
|
257
|
+
return this.request("GET", `/api/projects/${projectId}/ai-ide/workflows`, {
|
|
258
|
+
params: {
|
|
259
|
+
page,
|
|
260
|
+
page_size: pageSize,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
async getAiIdeWorkflow(projectId, workflowId) {
|
|
265
|
+
return this.request("GET", `/api/projects/${projectId}/ai-ide/workflows/${workflowId}`);
|
|
266
|
+
}
|
|
267
|
+
async createAiIdeEvent(projectId, workflowId, payload) {
|
|
268
|
+
return this.request("POST", `/api/projects/${projectId}/ai-ide/workflows/${workflowId}/events`, {
|
|
269
|
+
jsonBody: payload,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
async getSecurityObjectives(projectId, reviewId) {
|
|
273
|
+
return this.request("GET", `/api/projects/${projectId}/reviews/${reviewId}/security-objectives`);
|
|
274
|
+
}
|
|
275
|
+
async getThreatScenarios(projectId, reviewId) {
|
|
276
|
+
return this.request("GET", `/api/projects/${projectId}/reviews/${reviewId}/threat-scenarios`);
|
|
277
|
+
}
|
|
278
|
+
async getDataDictionaries(projectId, reviewId) {
|
|
279
|
+
return this.request("GET", `/api/projects/${projectId}/reviews/${reviewId}/data-dictionaries`);
|
|
280
|
+
}
|
|
281
|
+
async getCountermeasures(projectId, reviewId) {
|
|
282
|
+
return this.request("GET", `/api/projects/${projectId}/reviews/${reviewId}/countermeasures`);
|
|
283
|
+
}
|
|
284
|
+
async getComponents(projectId, reviewId) {
|
|
285
|
+
return this.request("GET", `/api/projects/${projectId}/reviews/${reviewId}/entity`);
|
|
286
|
+
}
|
|
287
|
+
async getQuestions(projectId, reviewId) {
|
|
288
|
+
return this.request("GET", `/api/projects/${projectId}/reviews/${reviewId}/questions`);
|
|
289
|
+
}
|
|
290
|
+
async getFindings(projectId, reviewId) {
|
|
291
|
+
return this.request("GET", `/api/projects/${projectId}/reviews/${reviewId}/findings`);
|
|
292
|
+
}
|
|
293
|
+
async getSecurityTestCases(projectId, reviewId) {
|
|
294
|
+
return this.request("GET", `/api/projects/${projectId}/reviews/${reviewId}/security-test-cases`);
|
|
295
|
+
}
|
|
296
|
+
async listJiraProjects(projectId) {
|
|
297
|
+
return this.request("GET", `/api/projects/${projectId}/integrations/jira/projects`);
|
|
298
|
+
}
|
|
299
|
+
async listConfluenceSpaces(projectId) {
|
|
300
|
+
return this.request("GET", `/api/projects/${projectId}/integrations/confluence/spaces`);
|
|
301
|
+
}
|
|
302
|
+
async listGithubRepos(projectId) {
|
|
303
|
+
return this.request("GET", `/api/projects/${projectId}/integrations/github/repos`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async function parseErrorBody(response) {
|
|
307
|
+
const text = await response.text();
|
|
308
|
+
if (!text) {
|
|
309
|
+
return `HTTP ${response.status}`;
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
return JSON.stringify(JSON.parse(text));
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
return text;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
let singletonClient;
|
|
319
|
+
export function getApiClient() {
|
|
320
|
+
if (!singletonClient) {
|
|
321
|
+
singletonClient = new SraiApiClient();
|
|
322
|
+
}
|
|
323
|
+
return singletonClient;
|
|
324
|
+
}
|
|
325
|
+
export function resetApiClientForTests() {
|
|
326
|
+
singletonClient = undefined;
|
|
327
|
+
}
|
|
328
|
+
function toArrayBuffer(content) {
|
|
329
|
+
const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content);
|
|
330
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
331
|
+
}
|
|
332
|
+
function toJsonRequestBody(payload) {
|
|
333
|
+
// If a JSON object was accidentally pre-stringified upstream, forward it as-is
|
|
334
|
+
// to avoid double encoding (which turns objects into JSON strings).
|
|
335
|
+
if (typeof payload === "string") {
|
|
336
|
+
try {
|
|
337
|
+
JSON.parse(payload);
|
|
338
|
+
return payload;
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return JSON.stringify(payload);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return JSON.stringify(payload);
|
|
345
|
+
}
|