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.
Files changed (54) hide show
  1. package/README.md +9 -3
  2. package/build/client/harness-client.d.ts +1 -0
  3. package/build/client/harness-client.d.ts.map +1 -1
  4. package/build/client/harness-client.js +29 -7
  5. package/build/client/harness-client.js.map +1 -1
  6. package/build/client/types.d.ts +2 -2
  7. package/build/client/types.d.ts.map +1 -1
  8. package/build/config.d.ts +25 -1
  9. package/build/config.d.ts.map +1 -1
  10. package/build/config.js +47 -0
  11. package/build/config.js.map +1 -1
  12. package/build/index.js +1 -1
  13. package/build/index.js.map +1 -1
  14. package/build/registry/appsec/builders.d.ts +33 -0
  15. package/build/registry/appsec/builders.d.ts.map +1 -0
  16. package/build/registry/appsec/builders.js +1403 -0
  17. package/build/registry/appsec/builders.js.map +1 -0
  18. package/build/registry/appsec/extractors.d.ts +46 -0
  19. package/build/registry/appsec/extractors.d.ts.map +1 -0
  20. package/build/registry/appsec/extractors.js +699 -0
  21. package/build/registry/appsec/extractors.js.map +1 -0
  22. package/build/registry/appsec/queries.d.ts +22 -0
  23. package/build/registry/appsec/queries.d.ts.map +1 -0
  24. package/build/registry/appsec/queries.js +322 -0
  25. package/build/registry/appsec/queries.js.map +1 -0
  26. package/build/registry/extractors.d.ts +2 -1
  27. package/build/registry/extractors.d.ts.map +1 -1
  28. package/build/registry/extractors.js +2 -1
  29. package/build/registry/extractors.js.map +1 -1
  30. package/build/registry/index.d.ts.map +1 -1
  31. package/build/registry/index.js +3 -1
  32. package/build/registry/index.js.map +1 -1
  33. package/build/registry/toolsets/appsec.d.ts +5 -0
  34. package/build/registry/toolsets/appsec.d.ts.map +1 -0
  35. package/build/registry/toolsets/appsec.js +624 -0
  36. package/build/registry/toolsets/appsec.js.map +1 -0
  37. package/build/registry/toolsets/chaos-descriptions.d.ts +12 -11
  38. package/build/registry/toolsets/chaos-descriptions.d.ts.map +1 -1
  39. package/build/registry/toolsets/chaos-descriptions.js +15 -13
  40. package/build/registry/toolsets/chaos-descriptions.js.map +1 -1
  41. package/build/registry/toolsets/chaos.d.ts.map +1 -1
  42. package/build/registry/toolsets/chaos.js +90 -32
  43. package/build/registry/toolsets/chaos.js.map +1 -1
  44. package/build/registry/toolsets/runbooks.d.ts.map +1 -1
  45. package/build/registry/toolsets/runbooks.js +6 -2
  46. package/build/registry/toolsets/runbooks.js.map +1 -1
  47. package/build/registry/types.d.ts +10 -2
  48. package/build/registry/types.d.ts.map +1 -1
  49. package/build/utils/session-headers.d.ts +1 -0
  50. package/build/utils/session-headers.d.ts.map +1 -1
  51. package/build/utils/session-headers.js +6 -1
  52. package/build/utils/session-headers.js.map +1 -1
  53. package/npm-shrinkwrap.json +2 -2
  54. 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