harness-mcp-v2 3.2.39 → 3.2.40
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 +9 -3
- package/build/client/harness-client.d.ts +1 -0
- package/build/client/harness-client.d.ts.map +1 -1
- package/build/client/harness-client.js +29 -7
- package/build/client/harness-client.js.map +1 -1
- package/build/client/types.d.ts +2 -2
- package/build/client/types.d.ts.map +1 -1
- package/build/config.d.ts +25 -1
- package/build/config.d.ts.map +1 -1
- package/build/config.js +47 -0
- package/build/config.js.map +1 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/registry/appsec/builders.d.ts +33 -0
- package/build/registry/appsec/builders.d.ts.map +1 -0
- package/build/registry/appsec/builders.js +1403 -0
- package/build/registry/appsec/builders.js.map +1 -0
- package/build/registry/appsec/extractors.d.ts +46 -0
- package/build/registry/appsec/extractors.d.ts.map +1 -0
- package/build/registry/appsec/extractors.js +699 -0
- package/build/registry/appsec/extractors.js.map +1 -0
- package/build/registry/appsec/queries.d.ts +22 -0
- package/build/registry/appsec/queries.d.ts.map +1 -0
- package/build/registry/appsec/queries.js +322 -0
- package/build/registry/appsec/queries.js.map +1 -0
- package/build/registry/extractors.d.ts +2 -1
- package/build/registry/extractors.d.ts.map +1 -1
- package/build/registry/extractors.js +2 -1
- package/build/registry/extractors.js.map +1 -1
- package/build/registry/index.d.ts.map +1 -1
- package/build/registry/index.js +3 -1
- package/build/registry/index.js.map +1 -1
- package/build/registry/toolsets/appsec.d.ts +5 -0
- package/build/registry/toolsets/appsec.d.ts.map +1 -0
- package/build/registry/toolsets/appsec.js +624 -0
- package/build/registry/toolsets/appsec.js.map +1 -0
- package/build/registry/toolsets/chaos-descriptions.d.ts +12 -11
- package/build/registry/toolsets/chaos-descriptions.d.ts.map +1 -1
- package/build/registry/toolsets/chaos-descriptions.js +15 -13
- package/build/registry/toolsets/chaos-descriptions.js.map +1 -1
- package/build/registry/toolsets/chaos.d.ts.map +1 -1
- package/build/registry/toolsets/chaos.js +90 -32
- package/build/registry/toolsets/chaos.js.map +1 -1
- package/build/registry/toolsets/runbooks.d.ts.map +1 -1
- package/build/registry/toolsets/runbooks.js +6 -2
- package/build/registry/toolsets/runbooks.js.map +1 -1
- package/build/registry/types.d.ts +10 -2
- package/build/registry/types.d.ts.map +1 -1
- package/build/utils/session-headers.d.ts +1 -0
- package/build/utils/session-headers.d.ts.map +1 -1
- package/build/utils/session-headers.js +6 -1
- package/build/utils/session-headers.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,1403 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { isRecord } from "../../utils/type-guards.js";
|
|
3
|
+
import { ALLOWED_VULN_ENUM_KEYS, COMMON_ENTITY_ATTRIBUTES, DATATYPES_QUERY, MCP_AI_ASSET_TYPES, ONE_DAY_MS, ONE_HOUR_MS, ONE_WEEK_MS, TIME_FILTER_MS, VULNERABILITY_DEFAULT_SOURCES, VULNERABILITY_DEFAULT_STATUSES, buildAttributeSelectionBlock, buildEntitiesQuery, } from "./queries.js";
|
|
4
|
+
import { setAppSecDispatchContext } from "./extractors.js";
|
|
5
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6
|
+
/**
|
|
7
|
+
* Validate that an `api_id` used in write operations (`appsec_derivation_config`,
|
|
8
|
+
* `appsec_fraud_policy`) is a canonical UUID and contains no quotes or JEXL syntax.
|
|
9
|
+
*/
|
|
10
|
+
export function assertValidAppSecApiId(apiId, resourceType) {
|
|
11
|
+
const trimmed = apiId.trim();
|
|
12
|
+
if (!trimmed) {
|
|
13
|
+
throw new Error(`Missing required field "api_id" for ${resourceType}.`);
|
|
14
|
+
}
|
|
15
|
+
if (!UUID_PATTERN.test(trimmed)) {
|
|
16
|
+
throw new Error(`Invalid api_id "${trimmed}" for ${resourceType}: must be a valid UUID (from harness_list(resource_type="appsec_api")).`);
|
|
17
|
+
}
|
|
18
|
+
return trimmed;
|
|
19
|
+
}
|
|
20
|
+
function toIsoMillisUtc(ms) {
|
|
21
|
+
return new Date(ms).toISOString();
|
|
22
|
+
}
|
|
23
|
+
function parseTimestampMs(raw) {
|
|
24
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
25
|
+
return raw;
|
|
26
|
+
}
|
|
27
|
+
if (typeof raw === "string" && raw.trim() !== "") {
|
|
28
|
+
const trimmed = raw.trim();
|
|
29
|
+
if (/^\d+$/.test(trimmed)) {
|
|
30
|
+
return Number(trimmed);
|
|
31
|
+
}
|
|
32
|
+
const parsed = Date.parse(trimmed);
|
|
33
|
+
if (!Number.isNaN(parsed)) {
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Invalid timestamp "${trimmed}". Expected ISO-8601 string or epoch milliseconds.`);
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
export function resolveAppSecTimeWindow(input, defaultDurationMs = ONE_DAY_MS) {
|
|
41
|
+
const nowMs = Date.now();
|
|
42
|
+
const explicitStart = parseTimestampMs(input.start_time);
|
|
43
|
+
const explicitEnd = parseTimestampMs(input.end_time);
|
|
44
|
+
const timeFilterKey = typeof input.time_filter === "string" ? input.time_filter.toUpperCase() : undefined;
|
|
45
|
+
const durationMs = typeof input.duration_ms === "number" && input.duration_ms > 0
|
|
46
|
+
? input.duration_ms
|
|
47
|
+
: timeFilterKey && TIME_FILTER_MS[timeFilterKey]
|
|
48
|
+
? TIME_FILTER_MS[timeFilterKey]
|
|
49
|
+
: defaultDurationMs;
|
|
50
|
+
let startMs;
|
|
51
|
+
let endMs;
|
|
52
|
+
if (explicitStart !== undefined && explicitEnd !== undefined) {
|
|
53
|
+
startMs = explicitStart;
|
|
54
|
+
endMs = explicitEnd;
|
|
55
|
+
}
|
|
56
|
+
else if (explicitStart !== undefined) {
|
|
57
|
+
startMs = explicitStart;
|
|
58
|
+
endMs = nowMs;
|
|
59
|
+
}
|
|
60
|
+
else if (explicitEnd !== undefined) {
|
|
61
|
+
endMs = explicitEnd;
|
|
62
|
+
startMs = endMs - durationMs;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
endMs = nowMs;
|
|
66
|
+
startMs = nowMs - durationMs;
|
|
67
|
+
}
|
|
68
|
+
if (startMs > endMs) {
|
|
69
|
+
throw new Error(`start_time (${toIsoMillisUtc(startMs)}) must not be after end_time (${toIsoMillisUtc(endMs)}).`);
|
|
70
|
+
}
|
|
71
|
+
const durationMinutes = Math.max(1, Math.round((endMs - startMs) / 60_000));
|
|
72
|
+
setAppSecDispatchContext(input, { durationMinutes });
|
|
73
|
+
return {
|
|
74
|
+
startTime: toIsoMillisUtc(startMs),
|
|
75
|
+
endTime: toIsoMillisUtc(endMs),
|
|
76
|
+
startMs,
|
|
77
|
+
endMs,
|
|
78
|
+
durationMinutes,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function splitCsvOrArray(raw) {
|
|
82
|
+
if (Array.isArray(raw)) {
|
|
83
|
+
return raw.map((v) => String(v).trim()).filter(Boolean);
|
|
84
|
+
}
|
|
85
|
+
if (typeof raw === "string" && raw.trim() !== "") {
|
|
86
|
+
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
87
|
+
}
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
function attachUiConfig(input, config) {
|
|
91
|
+
if (config.HARNESS_APPSEC_UI_URL && config.HARNESS_APPSEC_UI_URL !== "https://app.traceable.ai") {
|
|
92
|
+
setAppSecDispatchContext(input, { uiUrl: config.HARNESS_APPSEC_UI_URL });
|
|
93
|
+
}
|
|
94
|
+
else if (config.HARNESS_APPSEC_BASE_URL) {
|
|
95
|
+
const derived = config.HARNESS_APPSEC_BASE_URL
|
|
96
|
+
.replace(/\/+$/, "")
|
|
97
|
+
.replace(/\/graphql$/i, "")
|
|
98
|
+
.replace("://api-", "://app-")
|
|
99
|
+
.replace("://api.", "://app.");
|
|
100
|
+
setAppSecDispatchContext(input, { uiUrl: derived });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function resolveLimitAndOffset(input, defaultLimit = 10) {
|
|
104
|
+
const rawLimit = input.limit ?? input.size;
|
|
105
|
+
const limit = typeof rawLimit === "number" && Number.isFinite(rawLimit) && Math.trunc(rawLimit) > 0
|
|
106
|
+
? Math.min(Math.trunc(rawLimit), 1000)
|
|
107
|
+
: defaultLimit;
|
|
108
|
+
const page = typeof input.page === "number" && Number.isFinite(input.page) && Math.trunc(input.page) >= 0
|
|
109
|
+
? Math.trunc(input.page)
|
|
110
|
+
: 0;
|
|
111
|
+
const offset = typeof input.offset === "number" && Number.isFinite(input.offset) && Math.trunc(input.offset) >= 0
|
|
112
|
+
? Math.trunc(input.offset)
|
|
113
|
+
: page * limit;
|
|
114
|
+
return { limit, offset };
|
|
115
|
+
}
|
|
116
|
+
function attrFilter(key, operator, value) {
|
|
117
|
+
return {
|
|
118
|
+
keyExpression: { key },
|
|
119
|
+
operator,
|
|
120
|
+
value,
|
|
121
|
+
type: "ATTRIBUTE",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function formatGraphqlLiteral(val) {
|
|
125
|
+
if (Array.isArray(val)) {
|
|
126
|
+
return `[${val.map(formatGraphqlLiteral).join(", ")}]`;
|
|
127
|
+
}
|
|
128
|
+
if (typeof val === "string") {
|
|
129
|
+
return JSON.stringify(val);
|
|
130
|
+
}
|
|
131
|
+
if (typeof val === "number" || typeof val === "boolean") {
|
|
132
|
+
return String(val);
|
|
133
|
+
}
|
|
134
|
+
return JSON.stringify(val);
|
|
135
|
+
}
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// 1. appsec_api builders
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
export function buildApiListBody(input, config) {
|
|
140
|
+
attachUiConfig(input, config);
|
|
141
|
+
setAppSecDispatchContext(input, { entityScope: "API" });
|
|
142
|
+
const { startTime, endTime } = resolveAppSecTimeWindow(input, 2 * ONE_DAY_MS);
|
|
143
|
+
const { limit, offset } = resolveLimitAndOffset(input, 10);
|
|
144
|
+
const filters = [];
|
|
145
|
+
if (input.include_unlearnt !== true) {
|
|
146
|
+
filters.push(attrFilter("isLearnt", "EQUALS", true));
|
|
147
|
+
}
|
|
148
|
+
const discoveryStates = splitCsvOrArray(input.discovery_state);
|
|
149
|
+
filters.push(attrFilter("apiDiscoveryState", "IN", discoveryStates.length > 0 ? discoveryStates : ["DISCOVERED", "UNDER_DISCOVERY"]));
|
|
150
|
+
const nameRegex = (input.name_regex ?? input.search ?? input.name);
|
|
151
|
+
if (typeof nameRegex === "string" && nameRegex.trim() !== "") {
|
|
152
|
+
filters.push(attrFilter("name", "LIKE", nameRegex.trim()));
|
|
153
|
+
}
|
|
154
|
+
if (typeof input.service_name === "string" && input.service_name.trim() !== "") {
|
|
155
|
+
filters.push(attrFilter("serviceName", "IN", splitCsvOrArray(input.service_name)));
|
|
156
|
+
}
|
|
157
|
+
if (typeof input.service_id === "string" && input.service_id.trim() !== "") {
|
|
158
|
+
filters.push(attrFilter("serviceId", "IN", splitCsvOrArray(input.service_id)));
|
|
159
|
+
}
|
|
160
|
+
if (typeof input.environment === "string" && input.environment.trim() !== "") {
|
|
161
|
+
filters.push(attrFilter("environment", "IN", splitCsvOrArray(input.environment)));
|
|
162
|
+
}
|
|
163
|
+
if (typeof input.has_pii === "boolean") {
|
|
164
|
+
filters.push(attrFilter("hasPii", "EQUALS", input.has_pii));
|
|
165
|
+
}
|
|
166
|
+
const datatypeIds = splitCsvOrArray(input.datatype_ids);
|
|
167
|
+
if (datatypeIds.length > 0) {
|
|
168
|
+
filters.push(attrFilter("dataTypeIds", "IN", datatypeIds));
|
|
169
|
+
}
|
|
170
|
+
const riskCategories = splitCsvOrArray(input.risk_score_category);
|
|
171
|
+
if (riskCategories.length > 0) {
|
|
172
|
+
filters.push(attrFilter("riskScoreCategory", "IN", riskCategories.map((c) => c.toUpperCase())));
|
|
173
|
+
}
|
|
174
|
+
if (typeof input.is_authenticated === "boolean") {
|
|
175
|
+
filters.push(attrFilter("isAuthenticated", "EQUALS", input.is_authenticated));
|
|
176
|
+
}
|
|
177
|
+
if (typeof input.is_external === "boolean") {
|
|
178
|
+
filters.push(attrFilter("isExternal", "EQUALS", input.is_external));
|
|
179
|
+
}
|
|
180
|
+
if (typeof input.is_ai_asset === "boolean") {
|
|
181
|
+
filters.push(attrFilter("isAiAsset", "EQUALS", input.is_ai_asset));
|
|
182
|
+
}
|
|
183
|
+
const appGroupIds = splitCsvOrArray(input.application_group_ids);
|
|
184
|
+
if (appGroupIds.length > 0) {
|
|
185
|
+
filters.push(attrFilter("applicationGroupIds", "IN", appGroupIds));
|
|
186
|
+
}
|
|
187
|
+
const changeLabels = splitCsvOrArray(input.change_label);
|
|
188
|
+
if (changeLabels.length > 0) {
|
|
189
|
+
filters.push(attrFilter("changeLabel", "IN", changeLabels.map((c) => c.toUpperCase())));
|
|
190
|
+
}
|
|
191
|
+
setAppSecDispatchContext(input, { appliedFilters: filters });
|
|
192
|
+
const extraAttrs = splitCsvOrArray(input.attributes);
|
|
193
|
+
const selectedAttrs = [...COMMON_ENTITY_ATTRIBUTES.API, ...extraAttrs];
|
|
194
|
+
const query = buildEntitiesQuery(buildAttributeSelectionBlock(selectedAttrs, "apiMetrics"));
|
|
195
|
+
const sortBy = typeof input.sort_by === "string" && input.sort_by.trim() !== ""
|
|
196
|
+
? input.sort_by.trim()
|
|
197
|
+
: nameRegex
|
|
198
|
+
? "numCalls"
|
|
199
|
+
: "riskScore";
|
|
200
|
+
const orderBy = sortBy === "numCalls" || sortBy === "errorCount"
|
|
201
|
+
? [{ aggregation: "SUM", direction: "DESC", keyExpression: { key: sortBy } }]
|
|
202
|
+
: [{ direction: "DESC", keyExpression: { key: sortBy } }];
|
|
203
|
+
return {
|
|
204
|
+
query,
|
|
205
|
+
variables: {
|
|
206
|
+
scope: "API",
|
|
207
|
+
between: { startTime, endTime },
|
|
208
|
+
filterBy: filters,
|
|
209
|
+
orderBy,
|
|
210
|
+
includeInactive: typeof input.include_inactive === "boolean" ? input.include_inactive : true,
|
|
211
|
+
limit,
|
|
212
|
+
offset,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
export function buildApiGetBody(input, config) {
|
|
217
|
+
attachUiConfig(input, config);
|
|
218
|
+
setAppSecDispatchContext(input, { entityScope: "API" });
|
|
219
|
+
const apiId = String(input.api_id ?? input.resource_id ?? "").trim();
|
|
220
|
+
if (!apiId) {
|
|
221
|
+
throw new Error("Missing required field \"api_id\" for appsec_api.");
|
|
222
|
+
}
|
|
223
|
+
const { startTime, endTime } = resolveAppSecTimeWindow(input, 7 * ONE_DAY_MS);
|
|
224
|
+
const attrs = [...COMMON_ENTITY_ATTRIBUTES.API, "entireUrlPaths", "entireUrlPatterns"];
|
|
225
|
+
return {
|
|
226
|
+
query: buildEntitiesQuery(buildAttributeSelectionBlock(attrs, "apiMetrics")),
|
|
227
|
+
variables: {
|
|
228
|
+
scope: "API",
|
|
229
|
+
between: { startTime, endTime },
|
|
230
|
+
filterBy: [attrFilter("id", "EQUALS", apiId)],
|
|
231
|
+
includeInactive: true,
|
|
232
|
+
limit: 1,
|
|
233
|
+
offset: 0,
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// 2. appsec_api_spec & appsec_api_span builders
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
export function buildApiSpecGetBody(input, config) {
|
|
241
|
+
attachUiConfig(input, config);
|
|
242
|
+
const apiId = String(input.api_id ?? input.resource_id ?? "").trim();
|
|
243
|
+
if (!apiId) {
|
|
244
|
+
throw new Error("Missing required field \"api_id\" for appsec_api_spec.");
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
query: `query GetApiSpec($id: String!) {
|
|
248
|
+
apiInsightByApiId(id: $id) {
|
|
249
|
+
apiDefinition(
|
|
250
|
+
apiDefinition: {
|
|
251
|
+
formats: [OPEN_API_YAML]
|
|
252
|
+
pagination: { paginationType: NONE }
|
|
253
|
+
}
|
|
254
|
+
) {
|
|
255
|
+
definition
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}`,
|
|
259
|
+
variables: { id: apiId },
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
export function buildApiSpansListBody(input, config) {
|
|
263
|
+
attachUiConfig(input, config);
|
|
264
|
+
const apiId = String(input.api_id ?? input.resource_id ?? "").trim();
|
|
265
|
+
if (!apiId) {
|
|
266
|
+
throw new Error("Missing required filter \"api_id\" for appsec_api_span.");
|
|
267
|
+
}
|
|
268
|
+
const lookbackHours = typeof input.lookback_hours === "number" && input.lookback_hours > 0
|
|
269
|
+
? input.lookback_hours
|
|
270
|
+
: 6;
|
|
271
|
+
const endMs = Date.now();
|
|
272
|
+
const startMs = endMs - lookbackHours * ONE_HOUR_MS;
|
|
273
|
+
const { limit } = resolveLimitAndOffset(input, 2);
|
|
274
|
+
return {
|
|
275
|
+
query: `mutation FetchSampleSpans($startTime: DateTime!, $endTime: DateTime!, $apiId: Unknown!, $limit: Int!) {
|
|
276
|
+
submitAnalyticsQueryBatch(
|
|
277
|
+
input: {
|
|
278
|
+
batchScope: "EXPLORER_V3"
|
|
279
|
+
executeAsync: false
|
|
280
|
+
skipView: true
|
|
281
|
+
queries: [
|
|
282
|
+
{
|
|
283
|
+
name: "query_list"
|
|
284
|
+
scope: "SPAN"
|
|
285
|
+
limit: $limit
|
|
286
|
+
between: { startTime: $startTime, endTime: $endTime }
|
|
287
|
+
offset: 0
|
|
288
|
+
filterBy: [
|
|
289
|
+
{ keyExpression: { key: "apiId" }, operator: EQUALS, value: $apiId, type: ATTRIBUTE }
|
|
290
|
+
]
|
|
291
|
+
selections: [
|
|
292
|
+
{ expression: { key: "statusCode" } }
|
|
293
|
+
{ expression: { key: "spanTags" } }
|
|
294
|
+
{ expression: { key: "traceId" } }
|
|
295
|
+
{ expression: { key: "apiId" } }
|
|
296
|
+
{ expression: { key: "apiName" } }
|
|
297
|
+
{ expression: { key: "spanResponseBody" } }
|
|
298
|
+
{ expression: { key: "spanResponseHeaders" } }
|
|
299
|
+
{ expression: { key: "spanResponseCookies" } }
|
|
300
|
+
{ expression: { key: "spanRequestBody" } }
|
|
301
|
+
{ expression: { key: "spanRequestHeaders" } }
|
|
302
|
+
{ expression: { key: "spanRequestCookies" } }
|
|
303
|
+
{ expression: { key: "dataClassificationMatches" } }
|
|
304
|
+
{ expression: { key: "id" } }
|
|
305
|
+
{ expression: { key: "serviceName" } }
|
|
306
|
+
{ expression: { key: "userIdentifier" } }
|
|
307
|
+
{ expression: { key: "sessionId" } }
|
|
308
|
+
{ expression: { key: "ipAddress" } }
|
|
309
|
+
]
|
|
310
|
+
}
|
|
311
|
+
]
|
|
312
|
+
}
|
|
313
|
+
) {
|
|
314
|
+
batchId
|
|
315
|
+
state
|
|
316
|
+
query_list: queryResult(name: "query_list", scope: "SPAN") {
|
|
317
|
+
results {
|
|
318
|
+
id: selection(expression: { key: "id" }) { value }
|
|
319
|
+
traceId: selection(expression: { key: "traceId" }) { value }
|
|
320
|
+
apiId: selection(expression: { key: "apiId" }) { value }
|
|
321
|
+
apiName: selection(expression: { key: "apiName" }) { value }
|
|
322
|
+
serviceName: selection(expression: { key: "serviceName" }) { value }
|
|
323
|
+
statusCode: selection(expression: { key: "statusCode" }) { value }
|
|
324
|
+
ipAddress: selection(expression: { key: "ipAddress" }) { value }
|
|
325
|
+
userIdentifier: selection(expression: { key: "userIdentifier" }) { value }
|
|
326
|
+
sessionId: selection(expression: { key: "sessionId" }) { value }
|
|
327
|
+
spanRequestHeaders: selection(expression: { key: "spanRequestHeaders" }) { value }
|
|
328
|
+
spanRequestBody: selection(expression: { key: "spanRequestBody" }) { value }
|
|
329
|
+
spanRequestCookies: selection(expression: { key: "spanRequestCookies" }) { value }
|
|
330
|
+
spanResponseHeaders: selection(expression: { key: "spanResponseHeaders" }) { value }
|
|
331
|
+
spanResponseBody: selection(expression: { key: "spanResponseBody" }) { value }
|
|
332
|
+
spanResponseCookies: selection(expression: { key: "spanResponseCookies" }) { value }
|
|
333
|
+
spanTags: selection(expression: { key: "spanTags" }) { value }
|
|
334
|
+
dataClassificationMatches: selection(expression: { key: "dataClassificationMatches" }) { value }
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}`,
|
|
339
|
+
variables: {
|
|
340
|
+
startTime: toIsoMillisUtc(startMs),
|
|
341
|
+
endTime: toIsoMillisUtc(endMs),
|
|
342
|
+
apiId,
|
|
343
|
+
limit: Math.min(limit, 10),
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
// 3. appsec_entity builders
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
export function buildEntityListBody(input, config) {
|
|
351
|
+
attachUiConfig(input, config);
|
|
352
|
+
const scope = String(input.scope ?? "SERVICE").trim().toUpperCase();
|
|
353
|
+
setAppSecDispatchContext(input, { entityScope: scope });
|
|
354
|
+
const { startTime, endTime } = resolveAppSecTimeWindow(input, ONE_DAY_MS);
|
|
355
|
+
const { limit, offset } = resolveLimitAndOffset(input, 10);
|
|
356
|
+
const filters = [];
|
|
357
|
+
const assetTypes = splitCsvOrArray(input.asset_type).map((t) => t.toUpperCase());
|
|
358
|
+
const isMcpAssetQuery = assetTypes.some((t) => MCP_AI_ASSET_TYPES.has(t));
|
|
359
|
+
if (scope === "SERVICE" || scope === "DOMAIN") {
|
|
360
|
+
filters.push(attrFilter("isLearnt", "EQUALS", true));
|
|
361
|
+
}
|
|
362
|
+
else if (scope === "BACKEND") {
|
|
363
|
+
filters.push(attrFilter("isThirdParty", "EQUALS", false));
|
|
364
|
+
filters.push(attrFilter("id", "NOT_EQUALS", null));
|
|
365
|
+
if (input.has_sensitive_data === true) {
|
|
366
|
+
filters.push(attrFilter("sensitivity", "GREATER_THAN", 0));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
else if (scope === "BACKEND_API") {
|
|
370
|
+
filters.push(attrFilter("id", "NOT_EQUALS", null));
|
|
371
|
+
filters.push(attrFilter("isBackendThirdParty", "EQUALS", true));
|
|
372
|
+
filters.push(attrFilter("apiDiscoveryFrom", "NOT_EQUALS", "DEFAULT_ENDPOINT"));
|
|
373
|
+
if (input.has_sensitive_data === true) {
|
|
374
|
+
filters.push(attrFilter("sensitivity", "GREATER_THAN", 0));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
else if (scope === "APPLICATION_ASSET") {
|
|
378
|
+
filters.push(attrFilter("isLearnt", "EQUALS", true));
|
|
379
|
+
if (input.is_ai_asset === true || isMcpAssetQuery || splitCsvOrArray(input.genai_types).length > 0) {
|
|
380
|
+
filters.push(attrFilter("isAiAsset", "EQUALS", true));
|
|
381
|
+
}
|
|
382
|
+
if (assetTypes.length > 0) {
|
|
383
|
+
filters.push(attrFilter("assetType", "IN", assetTypes));
|
|
384
|
+
}
|
|
385
|
+
if (typeof input.parent_id === "string" && input.parent_id.trim() !== "") {
|
|
386
|
+
filters.push(attrFilter("parentId", "EQUALS", input.parent_id.trim()));
|
|
387
|
+
}
|
|
388
|
+
const genAiTypes = splitCsvOrArray(input.genai_types);
|
|
389
|
+
if (genAiTypes.length > 0) {
|
|
390
|
+
filters.push(attrFilter("genAiTypes", "IN", genAiTypes));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
else if (scope === "ACTOR") {
|
|
394
|
+
const actorStatuses = splitCsvOrArray(input.actor_status);
|
|
395
|
+
if (actorStatuses.length > 0) {
|
|
396
|
+
filters.push(attrFilter("status", "IN", actorStatuses.map((s) => s.toUpperCase())));
|
|
397
|
+
}
|
|
398
|
+
const scoreCategories = splitCsvOrArray(input.score_category);
|
|
399
|
+
if (scoreCategories.length > 0) {
|
|
400
|
+
filters.push(attrFilter("scoreCategory", "IN", scoreCategories.map((s) => s.toUpperCase())));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const names = splitCsvOrArray(input.names ?? input.name ?? input.search);
|
|
404
|
+
if (names.length === 1) {
|
|
405
|
+
filters.push(attrFilter("name", "LIKE", names[0]));
|
|
406
|
+
}
|
|
407
|
+
else if (names.length > 1) {
|
|
408
|
+
filters.push(attrFilter("name", "IN", names));
|
|
409
|
+
}
|
|
410
|
+
const ids = splitCsvOrArray(input.ids);
|
|
411
|
+
if (ids.length > 0) {
|
|
412
|
+
filters.push(attrFilter("id", "IN", ids));
|
|
413
|
+
}
|
|
414
|
+
if (typeof input.environment === "string" && input.environment.trim() !== "") {
|
|
415
|
+
filters.push(attrFilter("environment", "IN", splitCsvOrArray(input.environment)));
|
|
416
|
+
}
|
|
417
|
+
setAppSecDispatchContext(input, { appliedFilters: filters });
|
|
418
|
+
const defaultAttrs = COMMON_ENTITY_ATTRIBUTES[scope] ?? ["id", "name"];
|
|
419
|
+
const extraAttrs = splitCsvOrArray(input.attributes);
|
|
420
|
+
const metricMode = scope === "APPLICATION_ASSET" ? "numCallsOnly" : "none";
|
|
421
|
+
const query = buildEntitiesQuery(buildAttributeSelectionBlock([...defaultAttrs, ...extraAttrs], metricMode));
|
|
422
|
+
const includeInactive = scope === "ACTOR" ? false : true;
|
|
423
|
+
const orderBy = scope === "ACTOR"
|
|
424
|
+
? [{ direction: "DESC", keyExpression: { key: "createdTime" } }]
|
|
425
|
+
: scope === "APPLICATION_ASSET"
|
|
426
|
+
? [{ direction: "DESC", keyExpression: { key: "riskScore" } }]
|
|
427
|
+
: undefined;
|
|
428
|
+
return {
|
|
429
|
+
query,
|
|
430
|
+
variables: {
|
|
431
|
+
scope,
|
|
432
|
+
between: { startTime, endTime },
|
|
433
|
+
filterBy: filters,
|
|
434
|
+
...(orderBy ? { orderBy } : {}),
|
|
435
|
+
includeInactive,
|
|
436
|
+
limit,
|
|
437
|
+
offset,
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
export function buildEntityGetBody(input, config) {
|
|
442
|
+
attachUiConfig(input, config);
|
|
443
|
+
const entityId = String(input.entity_id ?? input.resource_id ?? "").trim();
|
|
444
|
+
if (!entityId) {
|
|
445
|
+
throw new Error("Missing required field \"entity_id\" for appsec_entity.");
|
|
446
|
+
}
|
|
447
|
+
const scope = String(input.scope ?? "SERVICE").trim().toUpperCase();
|
|
448
|
+
setAppSecDispatchContext(input, { entityScope: scope });
|
|
449
|
+
const { startTime, endTime } = resolveAppSecTimeWindow(input, 7 * ONE_DAY_MS);
|
|
450
|
+
const defaultAttrs = COMMON_ENTITY_ATTRIBUTES[scope] ?? ["id", "name"];
|
|
451
|
+
const extraAttrs = splitCsvOrArray(input.attributes);
|
|
452
|
+
const idFilter = MCP_AI_ASSET_TYPES.has(scope)
|
|
453
|
+
? { operator: "EQUALS", value: entityId, type: "ID", idScope: scope }
|
|
454
|
+
: attrFilter("id", "EQUALS", entityId);
|
|
455
|
+
return {
|
|
456
|
+
query: buildEntitiesQuery(buildAttributeSelectionBlock([...defaultAttrs, ...extraAttrs], "none")),
|
|
457
|
+
variables: {
|
|
458
|
+
scope,
|
|
459
|
+
between: { startTime, endTime },
|
|
460
|
+
filterBy: [idFilter],
|
|
461
|
+
includeInactive: true,
|
|
462
|
+
limit: 1,
|
|
463
|
+
offset: 0,
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
// ---------------------------------------------------------------------------
|
|
468
|
+
// 4. appsec_vulnerability builders
|
|
469
|
+
// ---------------------------------------------------------------------------
|
|
470
|
+
function normalizeVulnSeverity(val) {
|
|
471
|
+
const upper = val.trim().toUpperCase();
|
|
472
|
+
return upper.startsWith("VULNERABILITY_SEVERITY_") ? upper : `VULNERABILITY_SEVERITY_${upper}`;
|
|
473
|
+
}
|
|
474
|
+
function normalizeVulnStatus(val) {
|
|
475
|
+
const upper = val.trim().toUpperCase();
|
|
476
|
+
return upper.startsWith("VULNERABILITY_STATUS_") ? upper : `VULNERABILITY_STATUS_${upper}`;
|
|
477
|
+
}
|
|
478
|
+
function normalizeVulnSource(val) {
|
|
479
|
+
const upper = val.trim().toUpperCase();
|
|
480
|
+
return upper.startsWith("VULNERABILITY_SOURCE_TYPE_") ? upper : `VULNERABILITY_SOURCE_TYPE_${upper}`;
|
|
481
|
+
}
|
|
482
|
+
function buildVulnerabilityRelationalFilters(input) {
|
|
483
|
+
const rawSources = splitCsvOrArray(input.sources);
|
|
484
|
+
const sources = rawSources.length > 0 ? rawSources.map(normalizeVulnSource) : [...VULNERABILITY_DEFAULT_SOURCES];
|
|
485
|
+
const rawStatuses = splitCsvOrArray(input.status);
|
|
486
|
+
const statuses = rawStatuses.length > 0 ? rawStatuses.map(normalizeVulnStatus) : [...VULNERABILITY_DEFAULT_STATUSES];
|
|
487
|
+
const relational = [
|
|
488
|
+
{ relationalFilter: { key: "SOURCES", operator: "IN", value: sources } },
|
|
489
|
+
{ relationalFilter: { key: "STATUS", operator: "IN", value: statuses } },
|
|
490
|
+
];
|
|
491
|
+
const rawSeverities = splitCsvOrArray(input.severity);
|
|
492
|
+
if (rawSeverities.length > 0) {
|
|
493
|
+
relational.push({
|
|
494
|
+
relationalFilter: { key: "SEVERITY", operator: "IN", value: rawSeverities.map(normalizeVulnSeverity) },
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
if (typeof input.category === "string" && input.category.trim() !== "") {
|
|
498
|
+
relational.push({
|
|
499
|
+
relationalFilter: { key: "VULNERABILITY_CATEGORY", operator: "IN", value: splitCsvOrArray(input.category) },
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
if (typeof input.sub_category === "string" && input.sub_category.trim() !== "") {
|
|
503
|
+
relational.push({
|
|
504
|
+
relationalFilter: {
|
|
505
|
+
key: "VULNERABILITY_SUB_CATEGORY",
|
|
506
|
+
operator: "IN",
|
|
507
|
+
value: splitCsvOrArray(input.sub_category),
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
if (typeof input.entity_id === "string" && input.entity_id.trim() !== "") {
|
|
512
|
+
relational.push({
|
|
513
|
+
relationalFilter: { key: "ENTITY_ID", operator: "IN", value: splitCsvOrArray(input.entity_id) },
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
if (typeof input.entity_name === "string" && input.entity_name.trim() !== "") {
|
|
517
|
+
relational.push({
|
|
518
|
+
relationalFilter: { key: "ENTITY_NAME", operator: "LIKE", value: input.entity_name.trim() },
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
if (typeof input.affected_entity_type === "string" && input.affected_entity_type.trim() !== "") {
|
|
522
|
+
relational.push({
|
|
523
|
+
relationalFilter: {
|
|
524
|
+
key: "AFFECTED_ENTITY_TYPE",
|
|
525
|
+
operator: "IN",
|
|
526
|
+
value: splitCsvOrArray(input.affected_entity_type),
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
if (typeof input.service_id === "string" && input.service_id.trim() !== "") {
|
|
531
|
+
relational.push({
|
|
532
|
+
relationalFilter: { key: "SERVICE_ID", operator: "IN", value: splitCsvOrArray(input.service_id) },
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
const envVal = (input.environment_id ?? input.environment);
|
|
536
|
+
if (typeof envVal === "string" && envVal.trim() !== "") {
|
|
537
|
+
relational.push({
|
|
538
|
+
relationalFilter: { key: "ENVIRONMENT_ID", operator: "IN", value: splitCsvOrArray(envVal) },
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
if (typeof input.owasp_api_top10 === "string" && input.owasp_api_top10.trim() !== "") {
|
|
542
|
+
relational.push({
|
|
543
|
+
relationalFilter: { key: "OWASP_API_TOP10", operator: "IN", value: splitCsvOrArray(input.owasp_api_top10) },
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
if (typeof input.is_ai_asset === "boolean") {
|
|
547
|
+
relational.push({
|
|
548
|
+
relationalFilter: { key: "IS_AI_ASSET", operator: "EQUALS", value: input.is_ai_asset },
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
if (input.has_jira === true) {
|
|
552
|
+
relational.push({
|
|
553
|
+
relationalFilter: { key: "JIRA_ISSUE_URL_FIELD", operator: "CONTAINS_KEY", value: "" },
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
return relational;
|
|
557
|
+
}
|
|
558
|
+
function buildVulnerabilityFilterBlock(relationalFilters) {
|
|
559
|
+
const blocks = relationalFilters.map((f) => {
|
|
560
|
+
const rf = f.relationalFilter;
|
|
561
|
+
return `{ relationalFilter: { key: ${rf.key}, operator: ${rf.operator}, value: ${formatGraphqlLiteral(rf.value)} } }`;
|
|
562
|
+
});
|
|
563
|
+
return `{ logicalFilter: { operator: AND, filters: [${blocks.join(", ")}] } }`;
|
|
564
|
+
}
|
|
565
|
+
export function buildVulnerabilityListBody(input, config) {
|
|
566
|
+
attachUiConfig(input, config);
|
|
567
|
+
const { limit, offset } = resolveLimitAndOffset(input, 10);
|
|
568
|
+
const relationalFilters = buildVulnerabilityRelationalFilters(input);
|
|
569
|
+
const filterBlock = buildVulnerabilityFilterBlock(relationalFilters);
|
|
570
|
+
const groupByKey = typeof input.group_by === "string" && /^[A-Z0-9_]+$/.test(input.group_by.trim().toUpperCase())
|
|
571
|
+
? input.group_by.trim().toUpperCase()
|
|
572
|
+
: undefined;
|
|
573
|
+
if (groupByKey) {
|
|
574
|
+
const query = `{
|
|
575
|
+
vulnerabilitiesV3(
|
|
576
|
+
filter: ${filterBlock}
|
|
577
|
+
orderBy: { key: ${groupByKey}, direction: DESC }
|
|
578
|
+
groupBy: [{ key: ${groupByKey} }]
|
|
579
|
+
isUiRequest: true
|
|
580
|
+
) {
|
|
581
|
+
results {
|
|
582
|
+
${groupByKey}: selection(key: ${groupByKey}) { value type }
|
|
583
|
+
VULNERABILITY_ID_COUNT: aggregation(key: VULNERABILITY_ID, aggregation: COUNT) { value type }
|
|
584
|
+
}
|
|
585
|
+
count
|
|
586
|
+
total
|
|
587
|
+
}
|
|
588
|
+
}`;
|
|
589
|
+
return { query, variables: {} };
|
|
590
|
+
}
|
|
591
|
+
const query = `{
|
|
592
|
+
vulnerabilitiesV3(
|
|
593
|
+
filter: ${filterBlock}
|
|
594
|
+
orderBy: { key: SEVERITY, direction: DESC }
|
|
595
|
+
limit: ${limit}
|
|
596
|
+
offset: ${offset}
|
|
597
|
+
) {
|
|
598
|
+
results {
|
|
599
|
+
VULNERABILITY_ID: selection(key: VULNERABILITY_ID) { value type }
|
|
600
|
+
DISPLAY_NAME: selection(key: DISPLAY_NAME) { value type }
|
|
601
|
+
SEVERITY: selection(key: SEVERITY) { value type }
|
|
602
|
+
STATUS: selection(key: STATUS) { value type }
|
|
603
|
+
VULNERABILITY_CATEGORY: selection(key: VULNERABILITY_CATEGORY) { value type }
|
|
604
|
+
VULNERABILITY_SUB_CATEGORY: selection(key: VULNERABILITY_SUB_CATEGORY) { value type }
|
|
605
|
+
ENTITY_ID: selection(key: ENTITY_ID) { value type }
|
|
606
|
+
ENTITY_NAME: selection(key: ENTITY_NAME) { value type }
|
|
607
|
+
AFFECTED_ENTITY_TYPE: selection(key: AFFECTED_ENTITY_TYPE) { value type }
|
|
608
|
+
ENVIRONMENT_ID: selection(key: ENVIRONMENT_ID) { value type }
|
|
609
|
+
SOURCES: selection(key: SOURCES) { value type }
|
|
610
|
+
OWASP_API_TOP10: selection(key: OWASP_API_TOP10) { value type }
|
|
611
|
+
LAST_SEEN_TIMESTAMP_MILLIS: selection(key: LAST_SEEN_TIMESTAMP_MILLIS) { value type }
|
|
612
|
+
}
|
|
613
|
+
count
|
|
614
|
+
total
|
|
615
|
+
}
|
|
616
|
+
}`;
|
|
617
|
+
return { query, variables: {} };
|
|
618
|
+
}
|
|
619
|
+
export function buildVulnerabilityGetBody(input, config) {
|
|
620
|
+
attachUiConfig(input, config);
|
|
621
|
+
const vulnId = String(input.vulnerability_id ?? input.resource_id ?? "").trim();
|
|
622
|
+
if (!vulnId) {
|
|
623
|
+
throw new Error("Missing required field \"vulnerability_id\" for appsec_vulnerability.");
|
|
624
|
+
}
|
|
625
|
+
const filterBlock = buildVulnerabilityFilterBlock([
|
|
626
|
+
{ relationalFilter: { key: "VULNERABILITY_ID", operator: "EQUALS", value: vulnId } },
|
|
627
|
+
]);
|
|
628
|
+
const query = `{
|
|
629
|
+
vulnerabilitiesV3(
|
|
630
|
+
filter: ${filterBlock}
|
|
631
|
+
limit: 1
|
|
632
|
+
offset: 0
|
|
633
|
+
) {
|
|
634
|
+
results {
|
|
635
|
+
VULNERABILITY_ID: selection(key: VULNERABILITY_ID) { value type }
|
|
636
|
+
DISPLAY_NAME: selection(key: DISPLAY_NAME) { value type }
|
|
637
|
+
SEVERITY: selection(key: SEVERITY) { value type }
|
|
638
|
+
STATUS: selection(key: STATUS) { value type }
|
|
639
|
+
VULNERABILITY_CATEGORY: selection(key: VULNERABILITY_CATEGORY) { value type }
|
|
640
|
+
VULNERABILITY_SUB_CATEGORY: selection(key: VULNERABILITY_SUB_CATEGORY) { value type }
|
|
641
|
+
ENTITY_ID: selection(key: ENTITY_ID) { value type }
|
|
642
|
+
ENTITY_NAME: selection(key: ENTITY_NAME) { value type }
|
|
643
|
+
AFFECTED_ENTITY_TYPE: selection(key: AFFECTED_ENTITY_TYPE) { value type }
|
|
644
|
+
ENVIRONMENT_ID: selection(key: ENVIRONMENT_ID) { value type }
|
|
645
|
+
SERVICE_ID: selection(key: SERVICE_ID) { value type }
|
|
646
|
+
SOURCES: selection(key: SOURCES) { value type }
|
|
647
|
+
OWASP: selection(key: OWASP) { value type }
|
|
648
|
+
OWASP_API_TOP10: selection(key: OWASP_API_TOP10) { value type }
|
|
649
|
+
API_DISCOVERY_STATE: selection(key: API_DISCOVERY_STATE) { value type }
|
|
650
|
+
LAST_SEEN_TIMESTAMP_MILLIS: selection(key: LAST_SEEN_TIMESTAMP_MILLIS) { value type }
|
|
651
|
+
CLOSED_TIMESTAMP_MILLIS: selection(key: CLOSED_TIMESTAMP_MILLIS) { value type }
|
|
652
|
+
}
|
|
653
|
+
count
|
|
654
|
+
total
|
|
655
|
+
}
|
|
656
|
+
}`;
|
|
657
|
+
return { query, variables: {} };
|
|
658
|
+
}
|
|
659
|
+
export function buildVulnEnumValuesBody(input) {
|
|
660
|
+
const body = isRecord(input.body) ? input.body : {};
|
|
661
|
+
const rawKeys = splitCsvOrArray(body.filter_enum_keys ?? input.filter_enum_keys);
|
|
662
|
+
const keys = (rawKeys.length > 0 ? rawKeys : ["STATUS", "SEVERITY", "VULNERABILITY_CATEGORY"])
|
|
663
|
+
.map((k) => k.toUpperCase())
|
|
664
|
+
.filter((k) => ALLOWED_VULN_ENUM_KEYS.has(k));
|
|
665
|
+
if (keys.length === 0) {
|
|
666
|
+
throw new Error(`filter_enum_keys must contain at least one valid enum key (${[...ALLOWED_VULN_ENUM_KEYS].join(", ")}).`);
|
|
667
|
+
}
|
|
668
|
+
setAppSecDispatchContext(input, { enumKeys: keys });
|
|
669
|
+
const filterBlock = buildVulnerabilityFilterBlock([
|
|
670
|
+
{ relationalFilter: { key: "SOURCES", operator: "IN", value: [...VULNERABILITY_DEFAULT_SOURCES] } },
|
|
671
|
+
{ relationalFilter: { key: "STATUS", operator: "IN", value: [...VULNERABILITY_DEFAULT_STATUSES] } },
|
|
672
|
+
]);
|
|
673
|
+
const subQueries = keys
|
|
674
|
+
.map((key) => `${key}: vulnerabilitiesV3(
|
|
675
|
+
filter: ${filterBlock}
|
|
676
|
+
orderBy: { key: ${key}, direction: DESC }
|
|
677
|
+
groupBy: [{ key: ${key} }]
|
|
678
|
+
isUiRequest: true
|
|
679
|
+
) {
|
|
680
|
+
results {
|
|
681
|
+
${key}: selection(key: ${key}) { value }
|
|
682
|
+
count: aggregation(key: VULNERABILITY_ID, aggregation: COUNT) { value }
|
|
683
|
+
}
|
|
684
|
+
}`)
|
|
685
|
+
.join("\n ");
|
|
686
|
+
const query = `query {
|
|
687
|
+
${subQueries}
|
|
688
|
+
}`;
|
|
689
|
+
return { query, variables: {} };
|
|
690
|
+
}
|
|
691
|
+
// ---------------------------------------------------------------------------
|
|
692
|
+
// 5. appsec_threat_activity builders
|
|
693
|
+
// ---------------------------------------------------------------------------
|
|
694
|
+
export function buildThreatActivityListBody(input, config) {
|
|
695
|
+
attachUiConfig(input, config);
|
|
696
|
+
const { startMs, endMs } = resolveAppSecTimeWindow(input, ONE_DAY_MS);
|
|
697
|
+
const { limit, offset } = resolveLimitAndOffset(input, 10);
|
|
698
|
+
const filterBy = [
|
|
699
|
+
{ key: "START_TIME_MILLIS", operator: "LESS_THAN", value: endMs },
|
|
700
|
+
{ key: "START_TIME_MILLIS", operator: "GREATER_THAN", value: startMs - ONE_WEEK_MS },
|
|
701
|
+
{ key: "LAST_SEEN_MILLIS", operator: "LESS_THAN", value: endMs + ONE_WEEK_MS },
|
|
702
|
+
{ key: "LAST_SEEN_MILLIS", operator: "GREATER_THAN", value: startMs },
|
|
703
|
+
{ key: "ACTIVITY_STATUS", operator: "NOT_IN", value: ["Internal", "Tagged", "Excluded"] },
|
|
704
|
+
];
|
|
705
|
+
const categories = splitCsvOrArray(input.event_category);
|
|
706
|
+
if (categories.length > 0) {
|
|
707
|
+
filterBy.push({ key: "EVENT_CATEGORY", operator: "IN", value: categories });
|
|
708
|
+
}
|
|
709
|
+
if (typeof input.behaviour === "string" && input.behaviour.trim() !== "") {
|
|
710
|
+
filterBy.push({ key: "BEHAVIOUR", operator: "IN", value: splitCsvOrArray(input.behaviour) });
|
|
711
|
+
}
|
|
712
|
+
const severities = splitCsvOrArray(input.severity);
|
|
713
|
+
if (severities.length > 0) {
|
|
714
|
+
filterBy.push({ key: "SEVERITY_LEVEL", operator: "IN", value: severities.map((s) => s.toUpperCase()) });
|
|
715
|
+
}
|
|
716
|
+
const apiIds = splitCsvOrArray(input.api_ids);
|
|
717
|
+
if (apiIds.length > 0) {
|
|
718
|
+
filterBy.push({ key: "API_IDS", operator: "IN", value: apiIds });
|
|
719
|
+
}
|
|
720
|
+
const serviceIds = splitCsvOrArray(input.service_ids);
|
|
721
|
+
if (serviceIds.length > 0) {
|
|
722
|
+
filterBy.push({ key: "SERVICE_IDS", operator: "IN", value: serviceIds });
|
|
723
|
+
}
|
|
724
|
+
const actorIds = splitCsvOrArray(input.actor_ids);
|
|
725
|
+
if (actorIds.length > 0) {
|
|
726
|
+
filterBy.push({ key: "ACTOR_ENTITY_IDS", operator: "IN", value: actorIds });
|
|
727
|
+
}
|
|
728
|
+
const domainIds = splitCsvOrArray(input.domain_ids);
|
|
729
|
+
if (domainIds.length > 0) {
|
|
730
|
+
filterBy.push({ key: "DOMAIN_IDS", operator: "IN", value: domainIds });
|
|
731
|
+
}
|
|
732
|
+
setAppSecDispatchContext(input, { appliedFilters: filterBy });
|
|
733
|
+
const filterBlock = filterBy
|
|
734
|
+
.map((f) => `{ key: ${f.key}, operator: ${f.operator}, value: ${formatGraphqlLiteral(f.value)} }`)
|
|
735
|
+
.join(",\n ");
|
|
736
|
+
const groupByKey = typeof input.group_by === "string" && /^[A-Z0-9_]+$/.test(input.group_by.trim().toUpperCase())
|
|
737
|
+
? input.group_by.trim().toUpperCase()
|
|
738
|
+
: undefined;
|
|
739
|
+
if (groupByKey) {
|
|
740
|
+
const query = `{
|
|
741
|
+
threatActivity(
|
|
742
|
+
filterBy: [
|
|
743
|
+
${filterBlock}
|
|
744
|
+
]
|
|
745
|
+
groupBy: [{ key: ${groupByKey} }]
|
|
746
|
+
limit: ${limit}
|
|
747
|
+
offset: ${offset}
|
|
748
|
+
) {
|
|
749
|
+
results {
|
|
750
|
+
${groupByKey}: selection(key: ${groupByKey}) { value type }
|
|
751
|
+
THREAT_COUNT: aggregation(key: ID, aggregation: DISTINCT_COUNT) { value type }
|
|
752
|
+
}
|
|
753
|
+
count
|
|
754
|
+
total
|
|
755
|
+
}
|
|
756
|
+
}`;
|
|
757
|
+
return { query, variables: {} };
|
|
758
|
+
}
|
|
759
|
+
const query = `{
|
|
760
|
+
threatActivity(
|
|
761
|
+
filterBy: [
|
|
762
|
+
${filterBlock}
|
|
763
|
+
]
|
|
764
|
+
orderBy: [{ direction: DESC, key: LAST_SEEN_MILLIS }]
|
|
765
|
+
unnest: [
|
|
766
|
+
{ key: ACTOR_ENTITY_IDS }
|
|
767
|
+
{ key: API_IDS }
|
|
768
|
+
{ key: API_NAMES }
|
|
769
|
+
{ key: SERVICE_NAMES }
|
|
770
|
+
{ key: ANOMALOUS_ATTRIBUTES }
|
|
771
|
+
]
|
|
772
|
+
limit: ${limit}
|
|
773
|
+
offset: ${offset}
|
|
774
|
+
) {
|
|
775
|
+
results {
|
|
776
|
+
ID: selection(key: ID) { value type }
|
|
777
|
+
BEHAVIOUR: selection(key: BEHAVIOUR) { value type }
|
|
778
|
+
EVENT_CATEGORY: selection(key: EVENT_CATEGORY) { value type }
|
|
779
|
+
SEVERITY_LEVEL: selection(key: SEVERITY_LEVEL) { value type }
|
|
780
|
+
ACTIVITY_STATUS: selection(key: ACTIVITY_STATUS) { value type }
|
|
781
|
+
ACTOR_ENTITY_IDS: selection(key: ACTOR_ENTITY_IDS) { value type }
|
|
782
|
+
API_IDS: selection(key: API_IDS) { value type }
|
|
783
|
+
API_NAMES: selection(key: API_NAMES) { value type }
|
|
784
|
+
SERVICE_NAMES: selection(key: SERVICE_NAMES) { value type }
|
|
785
|
+
ANOMALOUS_ATTRIBUTES: selection(key: ANOMALOUS_ATTRIBUTES) { value type }
|
|
786
|
+
START_TIME_MILLIS: selection(key: START_TIME_MILLIS) { value type }
|
|
787
|
+
LAST_SEEN_MILLIS: selection(key: LAST_SEEN_MILLIS) { value type }
|
|
788
|
+
EVENTS_COUNT: selection(key: EVENTS_COUNT) { value type }
|
|
789
|
+
}
|
|
790
|
+
count
|
|
791
|
+
total
|
|
792
|
+
}
|
|
793
|
+
}`;
|
|
794
|
+
return { query, variables: {} };
|
|
795
|
+
}
|
|
796
|
+
export function buildThreatActivityGetBody(input, config) {
|
|
797
|
+
attachUiConfig(input, config);
|
|
798
|
+
const threatId = String(input.threat_activity_id ?? input.resource_id ?? "").trim();
|
|
799
|
+
if (!threatId) {
|
|
800
|
+
throw new Error("Missing required field \"threat_activity_id\" for appsec_threat_activity.");
|
|
801
|
+
}
|
|
802
|
+
const { startMs, endMs } = resolveAppSecTimeWindow(input, 30 * ONE_DAY_MS);
|
|
803
|
+
const query = `{
|
|
804
|
+
threatActivity(
|
|
805
|
+
filterBy: [
|
|
806
|
+
{ key: ID, operator: EQUALS, value: ${formatGraphqlLiteral(threatId)} },
|
|
807
|
+
{ key: START_TIME_MILLIS, operator: LESS_THAN, value: ${endMs} },
|
|
808
|
+
{ key: START_TIME_MILLIS, operator: GREATER_THAN, value: ${startMs - ONE_WEEK_MS} },
|
|
809
|
+
{ key: LAST_SEEN_MILLIS, operator: LESS_THAN, value: ${endMs + ONE_WEEK_MS} },
|
|
810
|
+
{ key: LAST_SEEN_MILLIS, operator: GREATER_THAN, value: ${startMs} }
|
|
811
|
+
]
|
|
812
|
+
unnest: [
|
|
813
|
+
{ key: ACTOR_ENTITY_IDS }
|
|
814
|
+
{ key: API_IDS }
|
|
815
|
+
{ key: API_NAMES }
|
|
816
|
+
{ key: SERVICE_IDS }
|
|
817
|
+
{ key: SERVICE_NAMES }
|
|
818
|
+
{ key: DOMAIN_NAMES }
|
|
819
|
+
{ key: ANOMALOUS_ATTRIBUTES }
|
|
820
|
+
]
|
|
821
|
+
limit: 1
|
|
822
|
+
offset: 0
|
|
823
|
+
) {
|
|
824
|
+
results {
|
|
825
|
+
ID: selection(key: ID) { value type }
|
|
826
|
+
BEHAVIOUR: selection(key: BEHAVIOUR) { value type }
|
|
827
|
+
EVENT_CATEGORY: selection(key: EVENT_CATEGORY) { value type }
|
|
828
|
+
SEVERITY_LEVEL: selection(key: SEVERITY_LEVEL) { value type }
|
|
829
|
+
ACTIVITY_STATUS: selection(key: ACTIVITY_STATUS) { value type }
|
|
830
|
+
ACTOR_ENTITY_IDS: selection(key: ACTOR_ENTITY_IDS) { value type }
|
|
831
|
+
API_IDS: selection(key: API_IDS) { value type }
|
|
832
|
+
API_NAMES: selection(key: API_NAMES) { value type }
|
|
833
|
+
SERVICE_IDS: selection(key: SERVICE_IDS) { value type }
|
|
834
|
+
SERVICE_NAMES: selection(key: SERVICE_NAMES) { value type }
|
|
835
|
+
DOMAIN_NAMES: selection(key: DOMAIN_NAMES) { value type }
|
|
836
|
+
ANOMALOUS_ATTRIBUTES: selection(key: ANOMALOUS_ATTRIBUTES) { value type }
|
|
837
|
+
START_TIME_MILLIS: selection(key: START_TIME_MILLIS) { value type }
|
|
838
|
+
LAST_SEEN_MILLIS: selection(key: LAST_SEEN_MILLIS) { value type }
|
|
839
|
+
EVENTS_COUNT: selection(key: EVENTS_COUNT) { value type }
|
|
840
|
+
}
|
|
841
|
+
count
|
|
842
|
+
total
|
|
843
|
+
}
|
|
844
|
+
}`;
|
|
845
|
+
return { query, variables: {} };
|
|
846
|
+
}
|
|
847
|
+
// ---------------------------------------------------------------------------
|
|
848
|
+
// 6. appsec_security_event builders
|
|
849
|
+
// ---------------------------------------------------------------------------
|
|
850
|
+
export function buildExploreListBody(input, config) {
|
|
851
|
+
attachUiConfig(input, config);
|
|
852
|
+
const mode = typeof input.mode === "string" ? input.mode.toLowerCase() : "events";
|
|
853
|
+
const scope = typeof input.scope === "string" && input.scope.trim() !== ""
|
|
854
|
+
? input.scope.trim().toUpperCase()
|
|
855
|
+
: mode === "ai_assets"
|
|
856
|
+
? "APPLICATION_ASSET"
|
|
857
|
+
: mode === "ai_traffic"
|
|
858
|
+
? "SPAN"
|
|
859
|
+
: "DOMAIN_EVENT";
|
|
860
|
+
setAppSecDispatchContext(input, { exploreScope: scope });
|
|
861
|
+
const { startTime, endTime } = resolveAppSecTimeWindow(input, ONE_DAY_MS);
|
|
862
|
+
const { limit, offset } = resolveLimitAndOffset(input, 10);
|
|
863
|
+
const filters = [];
|
|
864
|
+
if (scope === "DOMAIN_EVENT") {
|
|
865
|
+
filters.push(attrFilter("eventStatus", "NOT_IN", ["TAGGED", "INTERNAL", "EXCLUDED"]));
|
|
866
|
+
}
|
|
867
|
+
if (scope === "APPLICATION_ASSET" || mode === "ai_assets") {
|
|
868
|
+
filters.push(attrFilter("isLearnt", "EQUALS", true));
|
|
869
|
+
filters.push(attrFilter("isAiAsset", "EQUALS", true));
|
|
870
|
+
}
|
|
871
|
+
if (scope === "SPAN" && (mode === "ai_traffic" || input.is_gen_ai === true)) {
|
|
872
|
+
filters.push(attrFilter("isGenAi", "EQUALS", true));
|
|
873
|
+
}
|
|
874
|
+
if (typeof input.environment === "string" && input.environment.trim() !== "") {
|
|
875
|
+
filters.push(attrFilter("environment", "IN", splitCsvOrArray(input.environment)));
|
|
876
|
+
}
|
|
877
|
+
if (typeof input.service_name === "string" && input.service_name.trim() !== "") {
|
|
878
|
+
filters.push(attrFilter("serviceName", "IN", splitCsvOrArray(input.service_name)));
|
|
879
|
+
}
|
|
880
|
+
if (typeof input.api_id === "string" && input.api_id.trim() !== "") {
|
|
881
|
+
filters.push(attrFilter("apiId", "IN", splitCsvOrArray(input.api_id)));
|
|
882
|
+
}
|
|
883
|
+
if (typeof input.actor_id === "string" && input.actor_id.trim() !== "") {
|
|
884
|
+
filters.push(attrFilter("actorId", "IN", splitCsvOrArray(input.actor_id)));
|
|
885
|
+
}
|
|
886
|
+
if (typeof input.ip_address === "string" && input.ip_address.trim() !== "") {
|
|
887
|
+
filters.push(attrFilter("ipAddress", "IN", splitCsvOrArray(input.ip_address)));
|
|
888
|
+
}
|
|
889
|
+
setAppSecDispatchContext(input, { appliedFilters: filters });
|
|
890
|
+
const explicitGroupBy = splitCsvOrArray(input.group_by).filter((f) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(f));
|
|
891
|
+
const defaultGroupBy = scope === "APPLICATION_ASSET"
|
|
892
|
+
? ["assetType"]
|
|
893
|
+
: scope === "SPAN"
|
|
894
|
+
? ["spanKind"]
|
|
895
|
+
: ["apiId", "apiName", "threatCategory", "eventStatus"];
|
|
896
|
+
const groupByFields = explicitGroupBy.length > 0 ? explicitGroupBy : defaultGroupBy;
|
|
897
|
+
const selections = [
|
|
898
|
+
...groupByFields.map((f) => `${f}: selection(expression: { key: "${f}" }) { value type }`),
|
|
899
|
+
`count_id: selection(expression: { key: "id" }, aggregation: COUNT) { value type }`,
|
|
900
|
+
].join("\n ");
|
|
901
|
+
const expressionsLiteral = groupByFields.map((key) => `{ key: ${JSON.stringify(key)} }`).join(", ");
|
|
902
|
+
const query = `query ExploreGrouped(
|
|
903
|
+
$scope: String,
|
|
904
|
+
$between: InputTimeRange!,
|
|
905
|
+
$filterBy: [InputFilter],
|
|
906
|
+
$limit: Int,
|
|
907
|
+
$offset: Int
|
|
908
|
+
) {
|
|
909
|
+
explore(
|
|
910
|
+
scope: $scope,
|
|
911
|
+
between: $between,
|
|
912
|
+
filterBy: $filterBy,
|
|
913
|
+
groupBy: { expressions: [${expressionsLiteral}], groupLimit: ${limit} },
|
|
914
|
+
limit: $limit,
|
|
915
|
+
offset: $offset
|
|
916
|
+
) {
|
|
917
|
+
results {
|
|
918
|
+
${selections}
|
|
919
|
+
}
|
|
920
|
+
count
|
|
921
|
+
total
|
|
922
|
+
}
|
|
923
|
+
}`;
|
|
924
|
+
return {
|
|
925
|
+
query,
|
|
926
|
+
variables: {
|
|
927
|
+
scope,
|
|
928
|
+
between: { startTime, endTime },
|
|
929
|
+
filterBy: filters,
|
|
930
|
+
limit,
|
|
931
|
+
offset,
|
|
932
|
+
},
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
// ---------------------------------------------------------------------------
|
|
936
|
+
// 7. appsec_ast_scan builders
|
|
937
|
+
// ---------------------------------------------------------------------------
|
|
938
|
+
export function buildAstScanListBody(input, config) {
|
|
939
|
+
attachUiConfig(input, config);
|
|
940
|
+
const dataSource = String(input.data_source ?? "SCANS").trim().toUpperCase() === "API_COVERAGE"
|
|
941
|
+
? "API_COVERAGE"
|
|
942
|
+
: "SCANS";
|
|
943
|
+
const { limit, offset } = resolveLimitAndOffset(input, 10);
|
|
944
|
+
const suiteId = typeof input.suite_id === "string" ? input.suite_id.trim() : undefined;
|
|
945
|
+
const suiteName = typeof input.suite_name === "string" ? input.suite_name.trim() : undefined;
|
|
946
|
+
const scanId = typeof input.scan_id === "string" ? input.scan_id.trim() : undefined;
|
|
947
|
+
const scanRunNumber = typeof input.scan_run_number === "number" ? input.scan_run_number : undefined;
|
|
948
|
+
if (dataSource === "API_COVERAGE") {
|
|
949
|
+
if (!suiteId && !suiteName) {
|
|
950
|
+
throw new Error("data_source=\"API_COVERAGE\" requires suite_id or suite_name filter.");
|
|
951
|
+
}
|
|
952
|
+
const filterBy = [];
|
|
953
|
+
if (suiteId)
|
|
954
|
+
filterBy.push({ key: "SCAN_SUITE_ID", operator: "EQUALS", value: suiteId });
|
|
955
|
+
if (suiteName)
|
|
956
|
+
filterBy.push({ key: "SCAN_SUITE_NAME", operator: "EQUALS", value: suiteName });
|
|
957
|
+
if (scanRunNumber !== undefined) {
|
|
958
|
+
filterBy.push({ key: "SCAN_RUN_NUMBER", operator: "EQUALS", value: scanRunNumber });
|
|
959
|
+
}
|
|
960
|
+
setAppSecDispatchContext(input, { appliedFilters: filterBy });
|
|
961
|
+
const filterBlock = filterBy
|
|
962
|
+
.map((f) => `{ key: ${f.key}, operator: ${f.operator}, value: ${formatGraphqlLiteral(f.value)} }`)
|
|
963
|
+
.join(", ");
|
|
964
|
+
const query = `{
|
|
965
|
+
searchAst(
|
|
966
|
+
astDataSource: API_COVERAGE
|
|
967
|
+
limit: ${limit}
|
|
968
|
+
offset: ${offset}
|
|
969
|
+
filterBy: [${filterBlock}]
|
|
970
|
+
orderBy: { key: EXECUTED_TESTS_COUNT, direction: DESC }
|
|
971
|
+
) {
|
|
972
|
+
count
|
|
973
|
+
total
|
|
974
|
+
results {
|
|
975
|
+
API_ID: selection(key: API_ID) { value type }
|
|
976
|
+
API_NAME: selection(key: API_NAME) { value type }
|
|
977
|
+
API_REACHABILITY: selection(key: API_REACHABILITY) { value type }
|
|
978
|
+
GENERATED_TESTS_COUNT: selection(key: GENERATED_TESTS_COUNT) { value type }
|
|
979
|
+
EXECUTED_TESTS_COUNT: selection(key: EXECUTED_TESTS_COUNT) { value type }
|
|
980
|
+
VULNERABILITY_DISCOVERED_COUNT: selection(key: VULNERABILITY_DISCOVERED_COUNT) { value type }
|
|
981
|
+
SKIPPED_API_REASON: selection(key: SKIPPED_API_REASON) { value type }
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}`;
|
|
985
|
+
return { query, variables: {} };
|
|
986
|
+
}
|
|
987
|
+
if (suiteId || suiteName || scanId) {
|
|
988
|
+
const filterBy = [
|
|
989
|
+
{ key: "IS_METADATA", operator: "EQUALS", value: false },
|
|
990
|
+
];
|
|
991
|
+
if (suiteId)
|
|
992
|
+
filterBy.push({ key: "SCAN_SUITE_ID", operator: "EQUALS", value: suiteId });
|
|
993
|
+
if (suiteName)
|
|
994
|
+
filterBy.push({ key: "SCAN_SUITE_NAME", operator: "EQUALS", value: suiteName });
|
|
995
|
+
if (scanId)
|
|
996
|
+
filterBy.push({ key: "SCAN_ID", operator: "EQUALS", value: scanId });
|
|
997
|
+
if (scanRunNumber !== undefined) {
|
|
998
|
+
filterBy.push({ key: "SCAN_RUN_NUMBER", operator: "EQUALS", value: scanRunNumber });
|
|
999
|
+
}
|
|
1000
|
+
setAppSecDispatchContext(input, { appliedFilters: filterBy });
|
|
1001
|
+
const filterBlock = filterBy
|
|
1002
|
+
.map((f) => `{ key: ${f.key}, operator: ${f.operator}, value: ${formatGraphqlLiteral(f.value)} }`)
|
|
1003
|
+
.join(", ");
|
|
1004
|
+
const query = `{
|
|
1005
|
+
searchAst(
|
|
1006
|
+
astDataSource: SCANS
|
|
1007
|
+
limit: ${limit}
|
|
1008
|
+
offset: ${offset}
|
|
1009
|
+
filterBy: [${filterBlock}]
|
|
1010
|
+
orderBy: { key: SCAN_START_TIME, direction: DESC }
|
|
1011
|
+
) {
|
|
1012
|
+
count
|
|
1013
|
+
total
|
|
1014
|
+
results {
|
|
1015
|
+
SCAN_STATUS: selection(key: SCAN_STATUS) { value type }
|
|
1016
|
+
SCAN_SUITE_ID: selection(key: SCAN_SUITE_ID) { value type }
|
|
1017
|
+
SCAN_SUITE_NAME: selection(key: SCAN_SUITE_NAME) { value type }
|
|
1018
|
+
SCAN_ID: selection(key: SCAN_ID) { value type }
|
|
1019
|
+
SCAN_RUN_NUMBER: selection(key: SCAN_RUN_NUMBER) { value type }
|
|
1020
|
+
SCAN_START_TIME: selection(key: SCAN_START_TIME) { value type }
|
|
1021
|
+
SCAN_END_TIME: selection(key: SCAN_END_TIME) { value type }
|
|
1022
|
+
GENERATED_TESTS_COUNT: selection(key: GENERATED_TESTS_COUNT) { value type }
|
|
1023
|
+
EXECUTED_TESTS_COUNT: selection(key: EXECUTED_TESTS_COUNT) { value type }
|
|
1024
|
+
SCANNED_APIS_COUNT: selection(key: SCANNED_APIS_COUNT) { value type }
|
|
1025
|
+
VULNERABILITY_DISCOVERED_COUNT: selection(key: VULNERABILITY_DISCOVERED_COUNT) { value type }
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}`;
|
|
1029
|
+
return { query, variables: {} };
|
|
1030
|
+
}
|
|
1031
|
+
const query = `{
|
|
1032
|
+
searchAst(
|
|
1033
|
+
astDataSource: SCANS
|
|
1034
|
+
limit: ${limit}
|
|
1035
|
+
offset: ${offset}
|
|
1036
|
+
filterBy: [
|
|
1037
|
+
{ key: SCAN_RUN_NUMBER, operator: GREATER_THAN_OR_EQUAL_TO, value: 0 }
|
|
1038
|
+
]
|
|
1039
|
+
orderBy: { key: SCAN_START_TIME, direction: DESC }
|
|
1040
|
+
join: {
|
|
1041
|
+
subQuery: {
|
|
1042
|
+
selections: [SCAN_SUITE_ID]
|
|
1043
|
+
aggregations: [{ key: SCAN_RUN_NUMBER, aggregation: MAX }]
|
|
1044
|
+
groupBy: [{ key: SCAN_SUITE_ID }]
|
|
1045
|
+
}
|
|
1046
|
+
joinConditions: [
|
|
1047
|
+
{ mainQueryKey: SCAN_SUITE_ID, operator: EQUALS, subQueryKey: SCAN_SUITE_ID }
|
|
1048
|
+
{ mainQueryKey: SCAN_RUN_NUMBER, operator: EQUALS, subQueryKey: SCAN_RUN_NUMBER }
|
|
1049
|
+
]
|
|
1050
|
+
}
|
|
1051
|
+
) {
|
|
1052
|
+
count
|
|
1053
|
+
total
|
|
1054
|
+
results {
|
|
1055
|
+
SCAN_STATUS: selection(key: SCAN_STATUS) { value type }
|
|
1056
|
+
SCAN_SUITE_ID: selection(key: SCAN_SUITE_ID) { value type }
|
|
1057
|
+
SCAN_SUITE_NAME: selection(key: SCAN_SUITE_NAME) { value type }
|
|
1058
|
+
ATTACK_POLICY: selection(key: ATTACK_POLICY) { value type }
|
|
1059
|
+
ENVIRONMENT: selection(key: ENVIRONMENT) { value type }
|
|
1060
|
+
SCANNED_APIS_COUNT: selection(key: SCANNED_APIS_COUNT) { value type }
|
|
1061
|
+
VULNERABILITY_DISCOVERED_COUNT: selection(key: VULNERABILITY_DISCOVERED_COUNT) { value type }
|
|
1062
|
+
SCAN_START_TIME: selection(key: SCAN_START_TIME) { value type }
|
|
1063
|
+
SCAN_RUN_NUMBER: selection(key: SCAN_RUN_NUMBER) { value type }
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}`;
|
|
1067
|
+
return { query, variables: {} };
|
|
1068
|
+
}
|
|
1069
|
+
export function buildAstScanGetBody(input, config) {
|
|
1070
|
+
attachUiConfig(input, config);
|
|
1071
|
+
const hookId = typeof input.hook_id === "string" && input.hook_id.trim() !== "" ? input.hook_id.trim() : undefined;
|
|
1072
|
+
if (hookId) {
|
|
1073
|
+
return {
|
|
1074
|
+
query: `{
|
|
1075
|
+
getAstHookTests(
|
|
1076
|
+
astHookTestFilters: [{ testIdFilter: { ids: [${formatGraphqlLiteral(hookId)}] } }]
|
|
1077
|
+
) {
|
|
1078
|
+
advancedMode {
|
|
1079
|
+
hookScript
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}`,
|
|
1083
|
+
variables: {},
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
const suiteId = String(input.suite_id ?? input.resource_id ?? "").trim();
|
|
1087
|
+
if (!suiteId) {
|
|
1088
|
+
throw new Error("Missing required field \"suite_id\" (or \"hook_id\" in params) for appsec_ast_scan.");
|
|
1089
|
+
}
|
|
1090
|
+
return buildAstScanListBody({ ...input, suite_id: suiteId, limit: 5 }, config);
|
|
1091
|
+
}
|
|
1092
|
+
// ---------------------------------------------------------------------------
|
|
1093
|
+
// 8. appsec_inventory_trend preflight + builder
|
|
1094
|
+
// ---------------------------------------------------------------------------
|
|
1095
|
+
export async function fetchTrendCurrentCountPreflight(ctx) {
|
|
1096
|
+
const trendType = String(ctx.input.trend_type ?? "apis").trim().toLowerCase();
|
|
1097
|
+
const isVuln = trendType === "vulnerabilities" || trendType === "issues";
|
|
1098
|
+
if (isVuln) {
|
|
1099
|
+
const res = await ctx.registry.dispatch(ctx.client, "appsec_vulnerability", "list", {
|
|
1100
|
+
limit: 1,
|
|
1101
|
+
...(ctx.input.severity ? { severity: ctx.input.severity } : {}),
|
|
1102
|
+
}, ctx.signal);
|
|
1103
|
+
const currentCount = isRecord(res) && typeof res.total === "number" ? res.total : 0;
|
|
1104
|
+
setAppSecDispatchContext(ctx.input, { currentCount });
|
|
1105
|
+
}
|
|
1106
|
+
else {
|
|
1107
|
+
const res = await ctx.registry.dispatch(ctx.client, "appsec_api", "list", {
|
|
1108
|
+
limit: 1,
|
|
1109
|
+
...(ctx.input.application_group_ids ? { application_group_ids: ctx.input.application_group_ids } : {}),
|
|
1110
|
+
}, ctx.signal);
|
|
1111
|
+
const currentCount = isRecord(res) && typeof res.total === "number" ? res.total : 0;
|
|
1112
|
+
setAppSecDispatchContext(ctx.input, { currentCount });
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
export function buildInventoryTrendBody(input, config) {
|
|
1116
|
+
attachUiConfig(input, config);
|
|
1117
|
+
const trendType = String(input.trend_type ?? "apis").trim().toLowerCase();
|
|
1118
|
+
const periodDays = typeof input.period_days === "number" && input.period_days > 0 ? Math.min(input.period_days, 90) : 7;
|
|
1119
|
+
setAppSecDispatchContext(input, { periodDays });
|
|
1120
|
+
const endMs = Date.now();
|
|
1121
|
+
const startMs = endMs - periodDays * ONE_DAY_MS;
|
|
1122
|
+
const startTime = toIsoMillisUtc(startMs);
|
|
1123
|
+
const endTime = toIsoMillisUtc(endMs);
|
|
1124
|
+
const intervalUnits = periodDays > 30 ? "WEEKS" : "DAYS";
|
|
1125
|
+
const isVuln = trendType === "vulnerabilities" || trendType === "issues";
|
|
1126
|
+
const severityFilter = typeof input.severity === "string" && input.severity.trim() !== ""
|
|
1127
|
+
? `{ keyExpression: { key: "vulnerabilitySeverity" }, operator: EQUALS, value: ${formatGraphqlLiteral(input.severity.trim().toUpperCase())}, type: ATTRIBUTE },`
|
|
1128
|
+
: "";
|
|
1129
|
+
const openedFilters = isVuln
|
|
1130
|
+
? `[
|
|
1131
|
+
{ keyExpression: { key: "activityEventType" }, operator: EQUALS, value: "VULNERABILITY_CHANGE", type: ATTRIBUTE },
|
|
1132
|
+
${severityFilter}
|
|
1133
|
+
{ keyExpression: { key: "vulnerabilityCurrentStatus" }, operator: IN, value: ["OPEN", "REOPENED"], type: ATTRIBUTE }
|
|
1134
|
+
]`
|
|
1135
|
+
: `[
|
|
1136
|
+
{ keyExpression: { key: "activityEventType" }, operator: EQUALS, value: "API_INTELLIGENCE_CHANGE", type: ATTRIBUTE },
|
|
1137
|
+
{ keyExpression: { key: "apiCurrentState" }, operator: EQUALS, value: "API_STATE_LEARNT", type: ATTRIBUTE },
|
|
1138
|
+
{ keyExpression: { key: "apiPreviousState" }, operator: EQUALS, value: "API_STATE_LEARNING", type: ATTRIBUTE }
|
|
1139
|
+
]`;
|
|
1140
|
+
const closedFilters = isVuln
|
|
1141
|
+
? `[
|
|
1142
|
+
{ keyExpression: { key: "activityEventType" }, operator: EQUALS, value: "VULNERABILITY_CHANGE", type: ATTRIBUTE },
|
|
1143
|
+
${severityFilter}
|
|
1144
|
+
{ keyExpression: { key: "vulnerabilityCurrentStatus" }, operator: IN, value: ["FIXED", "NOT_A_VULNERABILITY", "ACCEPTED_RISK"], type: ATTRIBUTE }
|
|
1145
|
+
]`
|
|
1146
|
+
: `[
|
|
1147
|
+
{ keyExpression: { key: "activityEventType" }, operator: EQUALS, value: "API_DEFINITION_CHANGE", type: ATTRIBUTE },
|
|
1148
|
+
{ keyExpression: { key: "apiChangeType" }, operator: EQUALS, value: "REMOVE", type: ATTRIBUTE },
|
|
1149
|
+
{ keyExpression: { key: "isLearnt" }, operator: EQUALS, value: true, type: ATTRIBUTE },
|
|
1150
|
+
{ keyExpression: { key: "apiDiscoveryState" }, operator: IN, value: ["DISCOVERED", "UNDER_DISCOVERY"], type: ATTRIBUTE }
|
|
1151
|
+
]`;
|
|
1152
|
+
const query = `mutation SubmitTrendAnalyticsBatch {
|
|
1153
|
+
submitAnalyticsQueryBatch(
|
|
1154
|
+
input: {
|
|
1155
|
+
batchScope: "ACTIVITY_EVENT"
|
|
1156
|
+
executeAsync: true
|
|
1157
|
+
skipView: true
|
|
1158
|
+
queries: [
|
|
1159
|
+
{
|
|
1160
|
+
name: "opened_events"
|
|
1161
|
+
scope: "ACTIVITY_EVENT"
|
|
1162
|
+
limit: 10000
|
|
1163
|
+
between: { startTime: "${startTime}", endTime: "${endTime}" }
|
|
1164
|
+
offset: 0
|
|
1165
|
+
filterBy: ${openedFilters}
|
|
1166
|
+
interval: { size: 1, units: ${intervalUnits} }
|
|
1167
|
+
selections: [{ expression: { key: "activityEventType" }, aggregation: COUNT }]
|
|
1168
|
+
}
|
|
1169
|
+
{
|
|
1170
|
+
name: "closed_events"
|
|
1171
|
+
scope: "ACTIVITY_EVENT"
|
|
1172
|
+
limit: 10000
|
|
1173
|
+
between: { startTime: "${startTime}", endTime: "${endTime}" }
|
|
1174
|
+
offset: 0
|
|
1175
|
+
filterBy: ${closedFilters}
|
|
1176
|
+
interval: { size: 1, units: ${intervalUnits} }
|
|
1177
|
+
selections: [{ expression: { key: "activityEventType" }, aggregation: COUNT }]
|
|
1178
|
+
}
|
|
1179
|
+
]
|
|
1180
|
+
}
|
|
1181
|
+
) {
|
|
1182
|
+
batchId
|
|
1183
|
+
state
|
|
1184
|
+
opened_events: queryResult(name: "opened_events", scope: "ACTIVITY_EVENT") {
|
|
1185
|
+
results {
|
|
1186
|
+
intervalStart
|
|
1187
|
+
count: selection(expression: { key: "activityEventType" }, aggregation: COUNT) { value }
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
closed_events: queryResult(name: "closed_events", scope: "ACTIVITY_EVENT") {
|
|
1191
|
+
results {
|
|
1192
|
+
intervalStart
|
|
1193
|
+
count: selection(expression: { key: "activityEventType" }, aggregation: COUNT) { value }
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}`;
|
|
1198
|
+
return { query, variables: {} };
|
|
1199
|
+
}
|
|
1200
|
+
// ---------------------------------------------------------------------------
|
|
1201
|
+
// 9. appsec_datatype builders
|
|
1202
|
+
// ---------------------------------------------------------------------------
|
|
1203
|
+
export function buildDatatypesBody() {
|
|
1204
|
+
return { query: DATATYPES_QUERY, variables: {} };
|
|
1205
|
+
}
|
|
1206
|
+
// ---------------------------------------------------------------------------
|
|
1207
|
+
// 10. appsec_fraud_policy & appsec_derivation_config builders
|
|
1208
|
+
// ---------------------------------------------------------------------------
|
|
1209
|
+
export function buildFraudPolicyCreateBody(input) {
|
|
1210
|
+
if (!isRecord(input.body)) {
|
|
1211
|
+
throw new Error("body must be a JSON object. Use harness_describe(resource_type=\"appsec_fraud_policy\") for required fields.");
|
|
1212
|
+
}
|
|
1213
|
+
const b = input.body;
|
|
1214
|
+
const policyName = typeof b.policy_name === "string" ? b.policy_name.trim() : "";
|
|
1215
|
+
const description = typeof b.description === "string" ? b.description.trim() : "";
|
|
1216
|
+
const sqlQuery = typeof b.sql_query === "string" ? b.sql_query.trim() : "";
|
|
1217
|
+
const rawApiId = typeof b.api_id === "string" ? b.api_id.trim() : "";
|
|
1218
|
+
const apiId = rawApiId ? assertValidAppSecApiId(rawApiId, "appsec_fraud_policy") : "";
|
|
1219
|
+
const derivationConfigId = typeof b.associated_event_derivation_config_id === "string" && b.associated_event_derivation_config_id.trim()
|
|
1220
|
+
? b.associated_event_derivation_config_id.trim()
|
|
1221
|
+
: undefined;
|
|
1222
|
+
if (sqlQuery && !/\bselect\b/i.test(sqlQuery)) {
|
|
1223
|
+
throw new Error("sql_query must be a valid SELECT query for entity graph fraud rule evaluation.");
|
|
1224
|
+
}
|
|
1225
|
+
const variables = {
|
|
1226
|
+
policy_id: randomUUID(),
|
|
1227
|
+
policy_name: policyName || undefined,
|
|
1228
|
+
description: description || undefined,
|
|
1229
|
+
sql_query: sqlQuery || undefined,
|
|
1230
|
+
api_id: apiId || undefined,
|
|
1231
|
+
...(derivationConfigId ? { associated_event_derivation_config_id: derivationConfigId } : {}),
|
|
1232
|
+
};
|
|
1233
|
+
const query = `mutation CreateFraudPolicy(
|
|
1234
|
+
$policy_name: String!
|
|
1235
|
+
$description: String!
|
|
1236
|
+
$sql_query: String!
|
|
1237
|
+
$policy_id: String!
|
|
1238
|
+
$associated_event_derivation_config_id: String
|
|
1239
|
+
$api_id: String
|
|
1240
|
+
) {
|
|
1241
|
+
CreateFraudPolicy(
|
|
1242
|
+
input: {
|
|
1243
|
+
fraud_policy: {
|
|
1244
|
+
id: $policy_id
|
|
1245
|
+
name: $policy_name
|
|
1246
|
+
fraud_policy_rule: {
|
|
1247
|
+
fraud_rule: {
|
|
1248
|
+
fraud_rule_id: "ai_rule"
|
|
1249
|
+
entity_graph_fraud_rule: {
|
|
1250
|
+
entity_graph_data_query: { raw_sql_query: $sql_query }
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
primary_entity_type: "API"
|
|
1255
|
+
disabled: true
|
|
1256
|
+
description: $description
|
|
1257
|
+
risk_spec: { score: 5, severity: FRAUD_RISK_SEVERITY_MEDIUM }
|
|
1258
|
+
policy_group: POLICY_GROUP_BOT
|
|
1259
|
+
policy_type: POLICY_TYPE_CUSTOM
|
|
1260
|
+
labels: ["api-generated"]
|
|
1261
|
+
fraud_policy_stage: FRAUD_POLICY_STAGE_PRODUCTION
|
|
1262
|
+
schedule: { interval: { value: 1, unit: TIME_UNIT_HOURS } }
|
|
1263
|
+
target: { api_endpoint: { api_id: $api_id } }
|
|
1264
|
+
backreferences_info: { derivation_config: { derivation_config_id: $associated_event_derivation_config_id } }
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
) {
|
|
1268
|
+
fraud_policy {
|
|
1269
|
+
id
|
|
1270
|
+
name
|
|
1271
|
+
primary_entity_type
|
|
1272
|
+
disabled
|
|
1273
|
+
labels
|
|
1274
|
+
description
|
|
1275
|
+
policy_group
|
|
1276
|
+
policy_type
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}`;
|
|
1280
|
+
return { query, variables };
|
|
1281
|
+
}
|
|
1282
|
+
function parseDerivationFeaturesFromXml(xml) {
|
|
1283
|
+
const features = [];
|
|
1284
|
+
const featureBlocks = [...xml.matchAll(/<Feature\s+name=["']([^"']+)["']\s*>([\s\S]*?)<\/Feature>/g)];
|
|
1285
|
+
for (const match of featureBlocks) {
|
|
1286
|
+
const name = match[1].trim();
|
|
1287
|
+
const inner = match[2];
|
|
1288
|
+
const exprMatch = /<Expression>([\s\S]*?)<\/Expression>/.exec(inner);
|
|
1289
|
+
const explMatch = /<Explanation>([\s\S]*?)<\/Explanation>/.exec(inner);
|
|
1290
|
+
const typeMatch = /<Type>([\s\S]*?)<\/Type>/.exec(inner);
|
|
1291
|
+
if (!exprMatch || !exprMatch[1]?.trim()) {
|
|
1292
|
+
throw new Error(`Feature "${name}" is missing an <Expression> element.`);
|
|
1293
|
+
}
|
|
1294
|
+
features.push({
|
|
1295
|
+
name,
|
|
1296
|
+
expression: exprMatch[1].trim(),
|
|
1297
|
+
explanation: explMatch?.[1]?.trim() ?? "",
|
|
1298
|
+
type: typeMatch?.[1]?.trim() ?? "string",
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
if (features.length === 0) {
|
|
1302
|
+
throw new Error("jexl_expressions_xml must contain at least one <Feature name=\"...\"> element.");
|
|
1303
|
+
}
|
|
1304
|
+
return features;
|
|
1305
|
+
}
|
|
1306
|
+
export function buildDerivationConfigCreateBody(input) {
|
|
1307
|
+
if (!isRecord(input.body)) {
|
|
1308
|
+
throw new Error("body must be a JSON object. Use harness_describe(resource_type=\"appsec_derivation_config\") for required fields.");
|
|
1309
|
+
}
|
|
1310
|
+
const b = input.body;
|
|
1311
|
+
const rawApiId = typeof b.api_id === "string" ? b.api_id : "";
|
|
1312
|
+
const apiId = assertValidAppSecApiId(rawApiId, "appsec_derivation_config");
|
|
1313
|
+
let features = [];
|
|
1314
|
+
if (Array.isArray(b.features) && b.features.length > 0) {
|
|
1315
|
+
features = b.features.map((item) => {
|
|
1316
|
+
if (!isRecord(item))
|
|
1317
|
+
throw new Error("Each entry in body.features must be an object.");
|
|
1318
|
+
return {
|
|
1319
|
+
name: typeof item.name === "string" ? item.name.trim() : "",
|
|
1320
|
+
expression: typeof item.expression === "string" ? item.expression.trim() : "",
|
|
1321
|
+
explanation: typeof item.explanation === "string" ? item.explanation.trim() : "",
|
|
1322
|
+
type: typeof item.type === "string" ? item.type.trim() : "string",
|
|
1323
|
+
};
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
else if (typeof b.jexl_expressions_xml === "string" && b.jexl_expressions_xml.trim() !== "") {
|
|
1327
|
+
features = parseDerivationFeaturesFromXml(b.jexl_expressions_xml);
|
|
1328
|
+
}
|
|
1329
|
+
else {
|
|
1330
|
+
throw new Error("appsec_derivation_config requires either body.features (array) or body.jexl_expressions_xml (XML string).");
|
|
1331
|
+
}
|
|
1332
|
+
for (const feat of features) {
|
|
1333
|
+
if (!/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/.test(feat.name)) {
|
|
1334
|
+
throw new Error(`Feature name "${feat.name}" must be in snake_case format.`);
|
|
1335
|
+
}
|
|
1336
|
+
if (!feat.expression) {
|
|
1337
|
+
throw new Error(`Feature "${feat.name}" is missing a non-empty JEXL expression.`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
const configUuid = randomUUID();
|
|
1341
|
+
const innerConfig = {
|
|
1342
|
+
kind: "event_derivation_config",
|
|
1343
|
+
id: `ai_gen_${configUuid}`,
|
|
1344
|
+
name: `ai-generated-derivation-config-${configUuid}`,
|
|
1345
|
+
scope: `$s.api_id.equals('${apiId}')`,
|
|
1346
|
+
targetEventType: "customer_events",
|
|
1347
|
+
variables: [
|
|
1348
|
+
{
|
|
1349
|
+
name: "response_body_json_object",
|
|
1350
|
+
type: "string",
|
|
1351
|
+
rules: [{ expression: "traceable:parseJson($s.response_body)", script: null, condition: null }],
|
|
1352
|
+
},
|
|
1353
|
+
],
|
|
1354
|
+
attributes: features.map((f) => ({
|
|
1355
|
+
name: f.name,
|
|
1356
|
+
type: f.type || "string",
|
|
1357
|
+
rules: [
|
|
1358
|
+
{
|
|
1359
|
+
expression: f.expression,
|
|
1360
|
+
script: null,
|
|
1361
|
+
condition: null,
|
|
1362
|
+
description: f.explanation ?? "",
|
|
1363
|
+
},
|
|
1364
|
+
],
|
|
1365
|
+
})),
|
|
1366
|
+
entities: [],
|
|
1367
|
+
relationships: [],
|
|
1368
|
+
};
|
|
1369
|
+
const query = `mutation CreateDerivationConfig(
|
|
1370
|
+
$name: String!
|
|
1371
|
+
$description: String!
|
|
1372
|
+
$derivation_config: String!
|
|
1373
|
+
) {
|
|
1374
|
+
CreateDerivationConfig(
|
|
1375
|
+
input: {
|
|
1376
|
+
name: $name
|
|
1377
|
+
description: $description
|
|
1378
|
+
derivation_config: $derivation_config
|
|
1379
|
+
derivation_config_type: DERIVATION_CONFIG_TYPE_EVENT
|
|
1380
|
+
disabled: true
|
|
1381
|
+
}
|
|
1382
|
+
) {
|
|
1383
|
+
derivation_config {
|
|
1384
|
+
id
|
|
1385
|
+
derivation_config_type
|
|
1386
|
+
name
|
|
1387
|
+
derivation_config
|
|
1388
|
+
disabled
|
|
1389
|
+
description
|
|
1390
|
+
template_id
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}`;
|
|
1394
|
+
return {
|
|
1395
|
+
query,
|
|
1396
|
+
variables: {
|
|
1397
|
+
name: `ai-generated-derivation-config-${configUuid}`,
|
|
1398
|
+
description: `AI Generated Derivation Config Inner ${configUuid}`,
|
|
1399
|
+
derivation_config: JSON.stringify(innerConfig),
|
|
1400
|
+
},
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
//# sourceMappingURL=builders.js.map
|