@convisoappsec/mcp 0.3.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -18
- package/package.json +8 -4
- package/src/conviso_mcp/filters.js +54 -0
- package/src/conviso_mcp/graphql_client.js +538 -162
- package/src/conviso_mcp/mutations.js +126 -0
- package/src/conviso_mcp/mutations_catalog.json +3174 -0
- package/src/conviso_mcp/operation_allowlist.js +74 -0
- package/src/conviso_mcp/server.js +552 -326
- package/src/conviso_mcp/feed_gateway.js +0 -80
|
@@ -1,4 +1,118 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
+
import https from 'node:https';
|
|
3
|
+
import * as F from './filters.js';
|
|
4
|
+
import { buildMutationQuery } from './mutations.js';
|
|
5
|
+
|
|
6
|
+
/** Drop only undefined/null keys, keeping false/0/'' so intentional values survive. */
|
|
7
|
+
function compact(obj) {
|
|
8
|
+
const out = {};
|
|
9
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
10
|
+
if (v !== undefined && v !== null) out[k] = v;
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// --- Curated mutation input builders (pure; exported for tests) ---------------
|
|
16
|
+
// Each maps snake_case tool arguments to the GraphQL <Name>Input shape and supports
|
|
17
|
+
// an optional `extra` object spread in for any input field not in the curated signature.
|
|
18
|
+
|
|
19
|
+
export function buildChangeIssueStatusInput(a = {}) {
|
|
20
|
+
return { input: compact({ id: a.issue_id, status: a.status, reason: a.reason, ...(a.extra || {}) }) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildSourceCodeVulnerabilityInput(a = {}) {
|
|
24
|
+
return {
|
|
25
|
+
input: compact({
|
|
26
|
+
title: a.title,
|
|
27
|
+
description: a.description,
|
|
28
|
+
solution: a.solution,
|
|
29
|
+
category: a.category,
|
|
30
|
+
patterns: a.patterns,
|
|
31
|
+
reference: a.reference,
|
|
32
|
+
impactLevel: a.impact_level ?? 'MEDIUM',
|
|
33
|
+
probabilityLevel: a.probability_level ?? 'MEDIUM',
|
|
34
|
+
severity: a.severity,
|
|
35
|
+
summary: a.summary ?? '',
|
|
36
|
+
impactDescription: a.impact_description ?? '',
|
|
37
|
+
stepsToReproduce: a.steps_to_reproduce ?? '',
|
|
38
|
+
compromisedEnvironment: a.compromised_environment,
|
|
39
|
+
status: a.status ?? 'DRAFT',
|
|
40
|
+
assetId: a.asset_id,
|
|
41
|
+
projectId: a.project_id,
|
|
42
|
+
codeSnippet: a.code_snippet,
|
|
43
|
+
fileName: a.file_name,
|
|
44
|
+
firstLine: a.first_line,
|
|
45
|
+
vulnerableLine: a.vulnerable_line,
|
|
46
|
+
source: a.source,
|
|
47
|
+
sink: a.sink,
|
|
48
|
+
...(a.extra || {}),
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function buildCreateProjectInput(a = {}) {
|
|
54
|
+
return {
|
|
55
|
+
input: compact({
|
|
56
|
+
companyId: a.company_id,
|
|
57
|
+
typeId: a.type_id,
|
|
58
|
+
label: a.label,
|
|
59
|
+
goal: a.goal,
|
|
60
|
+
scope: a.scope,
|
|
61
|
+
startDate: a.start_date,
|
|
62
|
+
endDate: a.end_date,
|
|
63
|
+
...(a.extra || {}),
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildCreateAssetInput(a = {}) {
|
|
69
|
+
return {
|
|
70
|
+
input: compact({
|
|
71
|
+
companyId: a.company_id,
|
|
72
|
+
name: a.name,
|
|
73
|
+
assetType: a.asset_type,
|
|
74
|
+
url: a.url,
|
|
75
|
+
description: a.description,
|
|
76
|
+
businessImpact: a.business_impact,
|
|
77
|
+
exploitability: a.exploitability,
|
|
78
|
+
assetsTagList: a.tags,
|
|
79
|
+
...(a.extra || {}),
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function buildCreateTicketInput(a = {}) {
|
|
85
|
+
return {
|
|
86
|
+
input: compact({
|
|
87
|
+
companyId: a.company_id,
|
|
88
|
+
type: a.type,
|
|
89
|
+
title: a.title,
|
|
90
|
+
description: a.description,
|
|
91
|
+
priority: a.priority,
|
|
92
|
+
impact: a.impact,
|
|
93
|
+
...(a.extra || {}),
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildCreatePentestArtifactInput(a = {}) {
|
|
99
|
+
return {
|
|
100
|
+
input: compact({
|
|
101
|
+
companyId: a.company_id,
|
|
102
|
+
applicationId: a.application_id,
|
|
103
|
+
label: a.label,
|
|
104
|
+
pentestType: a.pentest_type,
|
|
105
|
+
description: a.description,
|
|
106
|
+
scopeText: a.scope_text,
|
|
107
|
+
assigneeEmail: a.assignee_email,
|
|
108
|
+
domains: a.domains,
|
|
109
|
+
inScope: a.in_scope,
|
|
110
|
+
outScope: a.out_scope,
|
|
111
|
+
...(a.extra || {}),
|
|
112
|
+
}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
2
116
|
|
|
3
117
|
const GraphQLFieldTemplates = {
|
|
4
118
|
complete_issue: `
|
|
@@ -162,6 +276,191 @@ GraphQLFieldTemplates.complete_issue_with_snippet = `
|
|
|
162
276
|
}
|
|
163
277
|
`;
|
|
164
278
|
|
|
279
|
+
export const ISSUES_QUERY = `
|
|
280
|
+
query GetIssues($companyId: ID!, $pagination: PaginationInput!, $filters: IssuesFiltersInput, $sortOptions: [IssueSortOptionInput!]) {
|
|
281
|
+
issues(companyId: $companyId, pagination: $pagination, filters: $filters, sortOptions: $sortOptions) {
|
|
282
|
+
collection {
|
|
283
|
+
id
|
|
284
|
+
title
|
|
285
|
+
severity
|
|
286
|
+
status
|
|
287
|
+
createdAt
|
|
288
|
+
updatedAt
|
|
289
|
+
sla { state dueAt daysRemaining }
|
|
290
|
+
assignedUsers { name email }
|
|
291
|
+
asset { id name }
|
|
292
|
+
project { id label company { id } }
|
|
293
|
+
}
|
|
294
|
+
metadata { totalCount totalPages currentPage limitValue }
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
`;
|
|
298
|
+
|
|
299
|
+
export function buildIssuesVariables(companyId, page = 1, limit = 10, opts = {}) {
|
|
300
|
+
const {
|
|
301
|
+
severities, statuses, slaStates, createdAfter, createdBefore, assigneeEmails,
|
|
302
|
+
search, projectId, assetIds, issueIds, sortBy, order, extraFilters,
|
|
303
|
+
} = opts;
|
|
304
|
+
let built = F.prune({
|
|
305
|
+
severities: F.normalizeEnumList(severities, F.SEVERITIES),
|
|
306
|
+
statuses: F.normalizeEnumList(statuses, F.ISSUE_STATUSES),
|
|
307
|
+
slaStates: F.normalizeEnumList(slaStates, F.SLA_STATES),
|
|
308
|
+
createdAtRange: F.buildDateRange(createdAfter, createdBefore),
|
|
309
|
+
assigneeEmails: assigneeEmails || [],
|
|
310
|
+
partialTitle: search,
|
|
311
|
+
projectIds: (projectId !== undefined && projectId !== null && projectId !== 0) ? [projectId] : [],
|
|
312
|
+
assetIds: assetIds || [],
|
|
313
|
+
ids: issueIds || [],
|
|
314
|
+
});
|
|
315
|
+
if (extraFilters) {
|
|
316
|
+
for (const [k, val] of Object.entries(extraFilters)) {
|
|
317
|
+
if (val !== null && val !== undefined) built[k] = val;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
companyId,
|
|
322
|
+
pagination: { page, perPage: limit },
|
|
323
|
+
filters: built,
|
|
324
|
+
sortOptions: F.buildIssueSortOptions(sortBy, order),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export const ASSETS_QUERY = `
|
|
329
|
+
query ListAssets($companyId: ID!, $page: Int, $limit: Int, $search: AssetsSearch) {
|
|
330
|
+
assets(companyId: $companyId, page: $page, limit: $limit, search: $search) {
|
|
331
|
+
collection {
|
|
332
|
+
id
|
|
333
|
+
name
|
|
334
|
+
assetType
|
|
335
|
+
environment
|
|
336
|
+
audience
|
|
337
|
+
createdAt
|
|
338
|
+
updatedAt
|
|
339
|
+
riskScore { current { value } }
|
|
340
|
+
}
|
|
341
|
+
metadata { totalCount totalPages currentPage limitValue }
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
`;
|
|
345
|
+
|
|
346
|
+
export function buildAssetsVariables(companyId, page = 1, limit = 10, opts = {}) {
|
|
347
|
+
const {
|
|
348
|
+
name, search, tags, technology, businessImpact, exploitability, assetType,
|
|
349
|
+
environmentCompromised, coveredByScan, sortBy, order, extraFilters,
|
|
350
|
+
} = opts;
|
|
351
|
+
const s = F.prune({
|
|
352
|
+
name,
|
|
353
|
+
search,
|
|
354
|
+
tags: tags || [],
|
|
355
|
+
technology: technology || [],
|
|
356
|
+
businessImpact: F.normalizeEnumList(businessImpact, F.BUSINESS_IMPACT),
|
|
357
|
+
exploitability: F.normalizeEnumList(exploitability, F.EXPLOITABILITY),
|
|
358
|
+
assetType,
|
|
359
|
+
sortBy: F.normalizeEnum(sortBy, F.ASSET_SORT_BY, false),
|
|
360
|
+
order: F.normalizeEnum(order, F.ORDER),
|
|
361
|
+
});
|
|
362
|
+
if (environmentCompromised !== null && environmentCompromised !== undefined) {
|
|
363
|
+
s.environmentCompromised = environmentCompromised;
|
|
364
|
+
}
|
|
365
|
+
if (coveredByScan !== null && coveredByScan !== undefined) {
|
|
366
|
+
s.coveredByScan = coveredByScan;
|
|
367
|
+
}
|
|
368
|
+
if (extraFilters) {
|
|
369
|
+
for (const [k, val] of Object.entries(extraFilters)) {
|
|
370
|
+
if (val !== null && val !== undefined) s[k] = val;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return { companyId, page, limit, search: s };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function buildTopVulnsVariables(companyId, opts = {}) {
|
|
377
|
+
const {
|
|
378
|
+
severities, statuses, assetIds, assetTags, createdAfter, createdBefore,
|
|
379
|
+
} = opts;
|
|
380
|
+
const fl = F.prune({
|
|
381
|
+
severities: F.normalizeEnumList(severities, F.SEVERITIES),
|
|
382
|
+
statuses: F.normalizeEnumList(statuses, F.ISSUE_STATUSES),
|
|
383
|
+
assetIds: assetIds || [],
|
|
384
|
+
assetTags: assetTags || [],
|
|
385
|
+
createdAtRange: F.buildDateRange(createdAfter, createdBefore),
|
|
386
|
+
});
|
|
387
|
+
const variables = { companyId };
|
|
388
|
+
if (Object.keys(fl).length) variables.filters = fl;
|
|
389
|
+
return variables;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export const COMPANIES_QUERY = `
|
|
393
|
+
query companies($page: Int, $limit: Int, $params: CompanySearch, $order: OrderScopesParams, $orderType: OrderParams){
|
|
394
|
+
companies(page: $page, limit: $limit, params: $params, order: $order, orderType : $orderType) {
|
|
395
|
+
collection {
|
|
396
|
+
id
|
|
397
|
+
label
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
`;
|
|
402
|
+
|
|
403
|
+
const PROJECTS_QUERY = `
|
|
404
|
+
query projects($page: Int, $limit: Int, $params: ProjectSearch, $sortBy: String, $descending: Boolean){
|
|
405
|
+
projects(page: $page, limit: $limit, params: $params, sortBy: $sortBy, descending : $descending) {
|
|
406
|
+
collection {
|
|
407
|
+
id
|
|
408
|
+
label
|
|
409
|
+
status
|
|
410
|
+
createdAt
|
|
411
|
+
startDate
|
|
412
|
+
endDate
|
|
413
|
+
allocatedAnalyst {
|
|
414
|
+
portalUser {
|
|
415
|
+
name
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
projectType {
|
|
419
|
+
label
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
company {
|
|
423
|
+
id
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
metadata {
|
|
427
|
+
totalCount
|
|
428
|
+
totalPages
|
|
429
|
+
currentPage
|
|
430
|
+
limitValue
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
`;
|
|
435
|
+
|
|
436
|
+
export function buildProjectsVariables(companyId, page = 1, limit = 1000, opts = {}) {
|
|
437
|
+
const {
|
|
438
|
+
search, statuses, projectTypes, createdAfter, createdBefore, tags, analystEmails,
|
|
439
|
+
sortBy = 'createdAt', descending = true,
|
|
440
|
+
} = opts;
|
|
441
|
+
const params = F.prune({
|
|
442
|
+
scopeIdEq: companyId,
|
|
443
|
+
labelCont: search,
|
|
444
|
+
projectStatusLabelIn: statuses || [],
|
|
445
|
+
projectTypeLabelIn: projectTypes || [],
|
|
446
|
+
createdAtGteq: createdAfter,
|
|
447
|
+
createdAtLteq: createdBefore,
|
|
448
|
+
tags: tags || [],
|
|
449
|
+
analystsEmailIn: analystEmails || [],
|
|
450
|
+
});
|
|
451
|
+
return { page, limit, params, sortBy, descending };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Shared HTTP client: keep-alive reuses the TLS connection across the many sequential
|
|
455
|
+
// calls an agent session makes; the timeout stops a hung upstream from hanging a tool
|
|
456
|
+
// call (and the MCP client) forever.
|
|
457
|
+
const httpClient = axios.create({
|
|
458
|
+
timeout: 30_000,
|
|
459
|
+
httpsAgent: new https.Agent({ keepAlive: true }),
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const RETRYABLE_STATUS = new Set([429, 502, 503]);
|
|
463
|
+
|
|
165
464
|
class GraphQLClient {
|
|
166
465
|
constructor(endpoint, apiKey) {
|
|
167
466
|
this.endpoint = endpoint;
|
|
@@ -174,16 +473,30 @@ class GraphQLClient {
|
|
|
174
473
|
}
|
|
175
474
|
|
|
176
475
|
async execute(query, variables = {}) {
|
|
177
|
-
|
|
476
|
+
// Queries are idempotent — retry a transient failure once. Mutations never retry.
|
|
477
|
+
const isRead = /^\s*query\b/i.test(query);
|
|
478
|
+
try {
|
|
479
|
+
return await this.#post(query, variables);
|
|
480
|
+
} catch (err) {
|
|
481
|
+
const transient = RETRYABLE_STATUS.has(err.status) || err.code === 'ECONNRESET';
|
|
482
|
+
if (!isRead || !transient) throw err;
|
|
483
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
484
|
+
return this.#post(query, variables);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
178
487
|
|
|
488
|
+
async #post(query, variables) {
|
|
179
489
|
let response;
|
|
180
|
-
|
|
181
490
|
try {
|
|
182
|
-
response = await
|
|
491
|
+
response = await httpClient.post(this.endpoint, { query, variables }, { headers: this.headers });
|
|
183
492
|
} catch (err) {
|
|
184
493
|
if (err.response) {
|
|
494
|
+
const status = err.response.status;
|
|
185
495
|
const e = new Error('GraphQL request failed');
|
|
186
|
-
e.status =
|
|
496
|
+
e.status = status;
|
|
497
|
+
if (status === 401 || status === 403) {
|
|
498
|
+
e.authHint = 'Authentication failed — check that CONVISO_API_KEY is set to a valid Conviso Platform API key.';
|
|
499
|
+
}
|
|
187
500
|
throw e;
|
|
188
501
|
}
|
|
189
502
|
|
|
@@ -193,7 +506,7 @@ class GraphQLClient {
|
|
|
193
506
|
throw e;
|
|
194
507
|
}
|
|
195
508
|
|
|
196
|
-
if (err.code === 'ETIMEDOUT') {
|
|
509
|
+
if (err.code === 'ETIMEDOUT' || err.code === 'ECONNABORTED') {
|
|
197
510
|
const e = new Error('Upstream request timeout');
|
|
198
511
|
e.status = 504;
|
|
199
512
|
throw e;
|
|
@@ -203,53 +516,21 @@ class GraphQLClient {
|
|
|
203
516
|
}
|
|
204
517
|
|
|
205
518
|
if (response.data?.errors) {
|
|
519
|
+
const messages = response.data.errors.map((x) => x?.message).filter(Boolean);
|
|
206
520
|
const e = new Error('GraphQL error');
|
|
207
521
|
e.status = 400;
|
|
522
|
+
// Validation/field errors about the caller's own request — safe and useful to surface
|
|
523
|
+
// so the model can correct the input (e.g. a missing required mutation field).
|
|
524
|
+
e.graphqlErrors = messages;
|
|
208
525
|
throw e;
|
|
209
526
|
}
|
|
210
527
|
|
|
211
528
|
return response.data.data;
|
|
212
529
|
}
|
|
213
530
|
|
|
214
|
-
async
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
issues(companyId: $companyId, pagination: $pagination, filters: $filters) {
|
|
218
|
-
collection {
|
|
219
|
-
id
|
|
220
|
-
title
|
|
221
|
-
severity
|
|
222
|
-
project {
|
|
223
|
-
company {
|
|
224
|
-
id
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
asset {
|
|
229
|
-
id
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
`;
|
|
235
|
-
|
|
236
|
-
const variables = {
|
|
237
|
-
companyId: company_id,
|
|
238
|
-
filters: { title: search },
|
|
239
|
-
pagination: { page: page, perPage: limit }
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
if (project_id !== null && project_id !== 0) {
|
|
243
|
-
variables.filters.projectIds = [project_id];
|
|
244
|
-
}
|
|
245
|
-
if (Array.isArray(issue_ids) && issue_ids.length > 0) {
|
|
246
|
-
variables.filters.ids = issue_ids;
|
|
247
|
-
}
|
|
248
|
-
if (Array.isArray(asset_ids) && asset_ids.length > 0) {
|
|
249
|
-
variables.filters.assetIds = asset_ids;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return this.execute(query, variables);
|
|
531
|
+
async getIssues(companyId, opts = {}) {
|
|
532
|
+
const variables = buildIssuesVariables(companyId, opts.page || 1, opts.limit || 10, opts);
|
|
533
|
+
return this.execute(ISSUES_QUERY, variables);
|
|
253
534
|
}
|
|
254
535
|
|
|
255
536
|
async get_issue_by_id(issue_id, return_snippets = false) {
|
|
@@ -265,56 +546,15 @@ class GraphQLClient {
|
|
|
265
546
|
return this.execute(query, variables);
|
|
266
547
|
}
|
|
267
548
|
|
|
268
|
-
async get_companies(page = 1, limit = 10, search = '') {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
collection {
|
|
273
|
-
id
|
|
274
|
-
label
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
`;
|
|
279
|
-
const variables = { page, limit, params: { labelCont: search } };
|
|
280
|
-
return this.execute(query, variables);
|
|
549
|
+
async get_companies(page = 1, limit = 10, search = '', label_eq = null) {
|
|
550
|
+
const params = F.prune({ labelCont: search, labelEq: label_eq });
|
|
551
|
+
const variables = { page, limit, params };
|
|
552
|
+
return this.execute(COMPANIES_QUERY, variables);
|
|
281
553
|
}
|
|
282
554
|
|
|
283
|
-
async get_projects(company_id, page = 1, limit = 1000, search = '') {
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
projects(page: $page, limit: $limit, params: $params, sortBy: $sortBy, descending : $descending) {
|
|
287
|
-
collection {
|
|
288
|
-
id
|
|
289
|
-
label
|
|
290
|
-
status
|
|
291
|
-
createdAt
|
|
292
|
-
startDate
|
|
293
|
-
endDate
|
|
294
|
-
allocatedAnalyst {
|
|
295
|
-
portalUser {
|
|
296
|
-
name
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
projectType {
|
|
300
|
-
label
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
company {
|
|
304
|
-
id
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
`;
|
|
310
|
-
const variables = {
|
|
311
|
-
page,
|
|
312
|
-
limit,
|
|
313
|
-
params: { scopeIdEq: company_id, labelCont: search },
|
|
314
|
-
sortBy: 'createdAt',
|
|
315
|
-
descending: true
|
|
316
|
-
};
|
|
317
|
-
return this.execute(query, variables);
|
|
555
|
+
async get_projects(company_id, page = 1, limit = 1000, search = '', opts = {}) {
|
|
556
|
+
const variables = buildProjectsVariables(company_id, page, limit, { search, ...opts });
|
|
557
|
+
return this.execute(PROJECTS_QUERY, variables);
|
|
318
558
|
}
|
|
319
559
|
|
|
320
560
|
async get_project_by_id(project_id) {
|
|
@@ -356,30 +596,9 @@ class GraphQLClient {
|
|
|
356
596
|
return this.execute(query, variables);
|
|
357
597
|
}
|
|
358
598
|
|
|
359
|
-
async get_assets_by_company(company_id, page = 1, limit = 10) {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
assets(companyId: $companyId, page: $page, limit: $limit) {
|
|
363
|
-
collection {
|
|
364
|
-
id
|
|
365
|
-
name
|
|
366
|
-
assetType
|
|
367
|
-
environment
|
|
368
|
-
audience
|
|
369
|
-
createdAt
|
|
370
|
-
updatedAt
|
|
371
|
-
}
|
|
372
|
-
metadata {
|
|
373
|
-
totalCount
|
|
374
|
-
totalPages
|
|
375
|
-
currentPage
|
|
376
|
-
limitValue
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
`;
|
|
381
|
-
const variables = { companyId: company_id, page, limit };
|
|
382
|
-
return this.execute(query, variables);
|
|
599
|
+
async get_assets_by_company(company_id, page = 1, limit = 10, opts = {}) {
|
|
600
|
+
const variables = buildAssetsVariables(company_id, page, limit, opts);
|
|
601
|
+
return this.execute(ASSETS_QUERY, variables);
|
|
383
602
|
}
|
|
384
603
|
|
|
385
604
|
async get_asset_by_id(asset_id) {
|
|
@@ -412,10 +631,10 @@ class GraphQLClient {
|
|
|
412
631
|
return this.execute(query, variables);
|
|
413
632
|
}
|
|
414
633
|
|
|
415
|
-
async get_top_vulnerabilities(company_id) {
|
|
634
|
+
async get_top_vulnerabilities(company_id, opts = {}) {
|
|
416
635
|
const query = `
|
|
417
|
-
query TopVulnerabilities($companyId: ID
|
|
418
|
-
topVulnerabilities(companyId: $companyId) {
|
|
636
|
+
query TopVulnerabilities($companyId: ID!, $filters: TopVulnerabilitiesFiltersInput) {
|
|
637
|
+
topVulnerabilities(companyId: $companyId, filters: $filters) {
|
|
419
638
|
affectedAssetsCount
|
|
420
639
|
criticalCount
|
|
421
640
|
highCount
|
|
@@ -426,7 +645,7 @@ class GraphQLClient {
|
|
|
426
645
|
}
|
|
427
646
|
}
|
|
428
647
|
`;
|
|
429
|
-
const variables =
|
|
648
|
+
const variables = buildTopVulnsVariables(company_id, opts);
|
|
430
649
|
return this.execute(query, variables);
|
|
431
650
|
}
|
|
432
651
|
|
|
@@ -481,54 +700,211 @@ class GraphQLClient {
|
|
|
481
700
|
return this.execute(query, variables);
|
|
482
701
|
}
|
|
483
702
|
|
|
484
|
-
|
|
703
|
+
// --- Mutations -------------------------------------------------------------
|
|
704
|
+
|
|
705
|
+
// Generic engine: run any catalogued mutation. Builds the GraphQL document from the
|
|
706
|
+
// SDL-derived catalog (whitelist) and executes it. `variables` is typically { input }.
|
|
707
|
+
async executeMutation(name, variables = {}, returnFields = null) {
|
|
708
|
+
const built = buildMutationQuery(name, variables, returnFields);
|
|
709
|
+
return this.execute(built.query, built.variables);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Curated shortcuts for the most common writes (thin wrappers over executeMutation).
|
|
713
|
+
async change_issue_status(a = {}) {
|
|
714
|
+
return this.executeMutation('changeIssueStatus', buildChangeIssueStatusInput(a));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async create_source_code_vulnerability(a = {}) {
|
|
718
|
+
return this.executeMutation('createSourceCodeVulnerability', buildSourceCodeVulnerabilityInput(a));
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async create_project(a = {}) {
|
|
722
|
+
return this.executeMutation('createProject', buildCreateProjectInput(a));
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async create_asset(a = {}) {
|
|
726
|
+
return this.executeMutation('createAsset', buildCreateAssetInput(a));
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
async create_ticket(a = {}) {
|
|
730
|
+
return this.executeMutation('createTicket', buildCreateTicketInput(a));
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// --- Curated DAST / AI-Pentest shortcuts ----------------------------------
|
|
734
|
+
async run_dast(a = {}) {
|
|
735
|
+
return this.executeMutation('startConvisoDast', { input: compact({ assetId: a.asset_id, ...(a.extra || {}) }) });
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async trigger_pentest(a = {}) {
|
|
739
|
+
return this.executeMutation('createPentestExecution', { input: compact({ artifactId: a.artifact_id, ...(a.extra || {}) }) });
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
async create_pentest_artifact(a = {}) {
|
|
743
|
+
return this.executeMutation('createPentestArtifact', buildCreatePentestArtifactInput(a));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// --- Read queries (curated) ------------------------------------------------
|
|
747
|
+
async get_tickets(company_id, { page = 1, limit = 25, search, sort_by, descending, params } = {}) {
|
|
485
748
|
const query = `
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
$vulnerabilityStatuses: [IssueStatusLabel!],
|
|
491
|
-
$requirements: Boolean!,
|
|
492
|
-
$evidences: Boolean!
|
|
493
|
-
) {
|
|
494
|
-
generateProjectReport(
|
|
495
|
-
projectId: $projectId,
|
|
496
|
-
language: $language,
|
|
497
|
-
vulnerabilityCriticity: $vulnerabilityCriticity,
|
|
498
|
-
vulnerabilityStatuses: $vulnerabilityStatuses,
|
|
499
|
-
requirements: $requirements,
|
|
500
|
-
evidences: $evidences
|
|
501
|
-
) {
|
|
502
|
-
id
|
|
503
|
-
reportUrl
|
|
504
|
-
status
|
|
505
|
-
}
|
|
749
|
+
query GetTickets($companyId: ID!, $page: Int, $limit: Int, $sortBy: String, $descending: Boolean, $params: TicketSearch) {
|
|
750
|
+
tickets(companyId: $companyId, page: $page, limit: $limit, sortBy: $sortBy, descending: $descending, params: $params) {
|
|
751
|
+
collection { id title type status priority impact createdAt updatedAt createdBy { name email } assignee { name email } }
|
|
752
|
+
metadata { totalCount totalPages currentPage limitValue }
|
|
506
753
|
}
|
|
507
|
-
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
language,
|
|
511
|
-
vulnerabilityCriticity: vulnerability_criticity || ["CRITICAL", "HIGH", "MEDIUM", "LOW", "NOTIFICATION"],
|
|
512
|
-
vulnerabilityStatuses: vulnerability_statuses || ["IDENTIFIED", "IN_PROGRESS", "AWAITING_VALIDATION", "FIX_ACCEPTED", "RISK_ACCEPTED", "FALSE_POSITIVE"],
|
|
513
|
-
requirements,
|
|
514
|
-
evidences
|
|
515
|
-
};
|
|
516
|
-
return this.execute(query, variables);
|
|
754
|
+
}`;
|
|
755
|
+
const p = compact({ search, ...(params || {}) });
|
|
756
|
+
return this.execute(query, { companyId: company_id, page, limit, sortBy: sort_by, descending, params: Object.keys(p).length ? p : undefined });
|
|
517
757
|
}
|
|
518
758
|
|
|
519
|
-
async
|
|
759
|
+
async get_ticket(company_id, ticket_id) {
|
|
520
760
|
const query = `
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
reportUrl
|
|
526
|
-
status
|
|
527
|
-
}
|
|
761
|
+
query GetTicket($companyId: ID!, $id: ID!) {
|
|
762
|
+
ticket(companyId: $companyId, id: $id) {
|
|
763
|
+
id title description type status priority impact createdAt updatedAt
|
|
764
|
+
createdBy { name email } assignee { name email }
|
|
528
765
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
766
|
+
}`;
|
|
767
|
+
return this.execute(query, { companyId: company_id, id: ticket_id });
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async get_requirements(scope_id, { page = 1, limit = 25, filters } = {}) {
|
|
771
|
+
const query = `
|
|
772
|
+
query GetRequirements($scopeId: Int!, $pagination: BasePaginationInput!, $filters: RequirementsFilterInput) {
|
|
773
|
+
requirements(scopeId: $scopeId, pagination: $pagination, filters: $filters) {
|
|
774
|
+
collection { id label description global createdAt updatedAt }
|
|
775
|
+
metadata { totalCount totalPages currentPage limitValue }
|
|
776
|
+
}
|
|
777
|
+
}`;
|
|
778
|
+
return this.execute(query, { scopeId: scope_id, pagination: { page, perPage: limit }, filters });
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async get_requirement(company_id, requirement_id) {
|
|
782
|
+
const query = `
|
|
783
|
+
query GetRequirement($companyId: ID!, $id: ID!) {
|
|
784
|
+
requirement(companyId: $companyId, id: $id) {
|
|
785
|
+
id label description global createdAt updatedAt
|
|
786
|
+
}
|
|
787
|
+
}`;
|
|
788
|
+
return this.execute(query, { companyId: company_id, id: requirement_id });
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async get_project_requirements(project_id) {
|
|
792
|
+
const query = `
|
|
793
|
+
query GetProjectRequirements($projectId: ID!) {
|
|
794
|
+
projectRequirements(projectId: $projectId) {
|
|
795
|
+
id label description createdAt updatedAt
|
|
796
|
+
}
|
|
797
|
+
}`;
|
|
798
|
+
return this.execute(query, { projectId: project_id });
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
async get_applications(company_id, search = null) {
|
|
802
|
+
const query = `
|
|
803
|
+
query GetApplications($companyId: ID!, $search: String) {
|
|
804
|
+
applications(companyId: $companyId, search: $search) {
|
|
805
|
+
id name description url riskScore assetsCount createdAt updatedAt
|
|
806
|
+
}
|
|
807
|
+
}`;
|
|
808
|
+
return this.execute(query, { companyId: company_id, search });
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async get_application(company_id, application_id) {
|
|
812
|
+
const query = `
|
|
813
|
+
query GetApplication($companyId: ID!, $id: ID!) {
|
|
814
|
+
application(id: $id, companyId: $companyId) {
|
|
815
|
+
id name description url riskScore assetsCount createdAt updatedAt
|
|
816
|
+
assets { id name }
|
|
817
|
+
}
|
|
818
|
+
}`;
|
|
819
|
+
return this.execute(query, { companyId: company_id, id: application_id });
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async get_scan_histories(company_id, { assetIds, page = 1, limit = 25, filters, sortOptions } = {}) {
|
|
823
|
+
const query = `
|
|
824
|
+
query GetScanHistories($companyId: ID!, $assetIds: [ID!], $pagination: PaginationInput!, $filters: ScansHistoriesFiltersInput, $sortOptions: [ScansHistoriesSortOptionInput!]) {
|
|
825
|
+
scanHistories(companyId: $companyId, assetIds: $assetIds, pagination: $pagination, filters: $filters, sortOptions: $sortOptions) {
|
|
826
|
+
collection { id status integration createdAt durationInSeconds createdVulnerabilityCount closedVulnerabilityCount importedVulnerabilityCount failureReason asset { id name } }
|
|
827
|
+
metadata { totalCount totalPages currentPage limitValue }
|
|
828
|
+
}
|
|
829
|
+
}`;
|
|
830
|
+
return this.execute(query, { companyId: company_id, assetIds, pagination: { page, perPage: limit }, filters, sortOptions });
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
async get_asset_scans_count(company_id) {
|
|
834
|
+
const query = `
|
|
835
|
+
query GetAssetScansCount($companyId: ID!) {
|
|
836
|
+
assetScansCount(companyId: $companyId) { assetsWithScans assetsWithoutScans consideredScans }
|
|
837
|
+
}`;
|
|
838
|
+
return this.execute(query, { companyId: company_id });
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
async get_sbom_components(company_id, { page = 1, limit = 25, search } = {}) {
|
|
842
|
+
const query = `
|
|
843
|
+
query GetSbomComponents($companyId: ID!, $page: Int, $limit: Int, $search: SbomComponentSearchInput) {
|
|
844
|
+
sbomComponents(companyId: $companyId, page: $page, limit: $limit, search: $search) {
|
|
845
|
+
collection { id name version technology packageManager license issuesBySeverity asset { id name } createdAt updatedAt }
|
|
846
|
+
metadata { totalCount totalPages currentPage limitValue }
|
|
847
|
+
}
|
|
848
|
+
}`;
|
|
849
|
+
return this.execute(query, { companyId: company_id, page, limit, search });
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
async get_pentest_artifacts(company_id, { page = 1, limit = 25, search, assigneeEmail, pentestType, applicationId } = {}) {
|
|
853
|
+
const query = `
|
|
854
|
+
query GetPentestArtifacts($companyId: ID!, $pagination: BasePaginationInput!, $search: String, $assigneeEmail: String, $pentestType: String, $applicationId: ID) {
|
|
855
|
+
pentestArtifacts(companyId: $companyId, pagination: $pagination, search: $search, assigneeEmail: $assigneeEmail, pentestType: $pentestType, applicationId: $applicationId) {
|
|
856
|
+
collection { id label description pentestType createdAt updatedAt useScheduling scheduledAt executionsCount assignee { name email } application { id name } latestExecution { id status runNumber vulnerabilitiesCount createdAt } }
|
|
857
|
+
metadata { totalCount totalPages currentPage limitValue }
|
|
858
|
+
}
|
|
859
|
+
}`;
|
|
860
|
+
return this.execute(query, { companyId: company_id, pagination: { page, perPage: limit }, search, assigneeEmail, pentestType, applicationId });
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
async get_pentest_artifact(artifact_id) {
|
|
864
|
+
const query = `
|
|
865
|
+
query GetPentestArtifact($id: ID!) {
|
|
866
|
+
pentestArtifact(id: $id) {
|
|
867
|
+
id label description pentestType scopeText inScope outScope domains createdAt updatedAt useScheduling scheduledAt
|
|
868
|
+
assignee { name email } application { id name }
|
|
869
|
+
executions { id status runNumber vulnerabilitiesCount createdAt }
|
|
870
|
+
}
|
|
871
|
+
}`;
|
|
872
|
+
return this.execute(query, { id: artifact_id });
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async get_pentest_execution(execution_id) {
|
|
876
|
+
const query = `
|
|
877
|
+
query GetPentestExecution($id: ID!) {
|
|
878
|
+
pentestExecution(id: $id) {
|
|
879
|
+
id status kind triggerKind runNumber startedAt finishedAt durationSeconds
|
|
880
|
+
vulnerabilitiesCount severityBreakdown nodeCount retestFixedCount retestTotalCount
|
|
881
|
+
project { id label } pentestArtifact { id label } triggeredBy { name email }
|
|
882
|
+
}
|
|
883
|
+
}`;
|
|
884
|
+
return this.execute(query, { id: execution_id });
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
async get_threat_model_artifacts(company_id, { page = 1, limit = 25, search, assigneeEmail, hasVersion } = {}) {
|
|
888
|
+
const query = `
|
|
889
|
+
query GetThreatModelArtifacts($companyId: ID!, $pagination: BasePaginationInput!, $search: String, $assigneeEmail: String, $hasVersion: Boolean) {
|
|
890
|
+
threatModelArtifacts(companyId: $companyId, pagination: $pagination, search: $search, assigneeEmail: $assigneeEmail, hasVersion: $hasVersion) {
|
|
891
|
+
collection { id label description scopeText createdAt updatedAt assignee { name email } latestVersion { id version createdAt } }
|
|
892
|
+
metadata { totalCount totalPages currentPage limitValue }
|
|
893
|
+
}
|
|
894
|
+
}`;
|
|
895
|
+
return this.execute(query, { companyId: company_id, pagination: { page, perPage: limit }, search, assigneeEmail, hasVersion });
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
async get_threat_model_artifact(artifact_id) {
|
|
899
|
+
const query = `
|
|
900
|
+
query GetThreatModelArtifact($id: ID!) {
|
|
901
|
+
threatModelArtifact(id: $id) {
|
|
902
|
+
id label description scopeText createdAt updatedAt
|
|
903
|
+
assignee { name email }
|
|
904
|
+
versions { id version createdAt diagramType scopeText notes }
|
|
905
|
+
}
|
|
906
|
+
}`;
|
|
907
|
+
return this.execute(query, { id: artifact_id });
|
|
532
908
|
}
|
|
533
909
|
}
|
|
534
910
|
|