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,699 @@
1
+ import { encodePathSegment } from "../../utils/path.js";
2
+ import { isRecord } from "../../utils/type-guards.js";
3
+ import { HarnessApiError } from "../../utils/errors.js";
4
+ import { redactSensitiveFields } from "../../utils/redact.js";
5
+ import { APPSEC_DATATYPE_MAPPING, ONE_DAY_MS, TIME_FILTER_MS, } from "./queries.js";
6
+ const dispatchContextMap = new WeakMap();
7
+ export function setAppSecDispatchContext(input, patch) {
8
+ const existing = dispatchContextMap.get(input) ?? {};
9
+ dispatchContextMap.set(input, { ...existing, ...patch });
10
+ }
11
+ export function getAppSecDispatchContext(input) {
12
+ if (!input)
13
+ return {};
14
+ return dispatchContextMap.get(input) ?? {};
15
+ }
16
+ const DEFAULT_APPSEC_UI_URL = "https://app.traceable.ai";
17
+ const MAX_SPAN_BODY_CHARS = 2000;
18
+ const SENSITIVE_HEADER_PATTERN = /^(authorization|proxy-authorization|cookie|set-cookie|x-api-key|x-auth-token|x-access-token|api-key|x-amz-security-token|x-csrf-token|x-xsrf-token)$/i;
19
+ const SENSITIVE_HEADER_SUBSTRING_PATTERN = /auth|token|secret|key|cookie|password|credential|session|jwt|bearer/i;
20
+ const APPSEC_ENTITY_SCOPE_TO_ID_PATH = {
21
+ API: "inventory/endpoint",
22
+ SERVICE: "inventory/service",
23
+ BACKEND: "inventory/backend",
24
+ DOMAIN: "inventory/domain",
25
+ API_COVERAGE: "inventory/endpoint",
26
+ };
27
+ const APPSEC_SCOPE_TO_FILTER_PATH = {
28
+ API: "inventory/api-endpoints",
29
+ SERVICE: "inventory/services",
30
+ BACKEND: "inventory/backends",
31
+ DOMAIN: "inventory/domains",
32
+ BACKEND_API: "third-party-apis",
33
+ VULNERABILITY: "issues-v2",
34
+ ACTOR: "attackers-v1",
35
+ THREAT_ACTIVITIES: "threat-activity-v2",
36
+ DOMAIN_EVENT: "security-events-explorer",
37
+ API_UNDER_THREAT: "apis-under-threat",
38
+ };
39
+ const APPSEC_OPERATOR_MAP = {
40
+ EQUALS: "_eq_",
41
+ NOT_EQUALS: "_neq_",
42
+ LESS_THAN: "_lt_",
43
+ LESS_THAN_OR_EQUAL_TO: "_lte_",
44
+ GREATER_THAN: "_gt_",
45
+ GREATER_THAN_OR_EQUAL_TO: "_gte_",
46
+ LIKE: "_lk_",
47
+ IN: "_in_",
48
+ CONTAINS: "_c_",
49
+ NOT_IN: "_nin_",
50
+ CONTAINS_KEY: "_ck_",
51
+ NOT_CONTAINS_KEY: "_nck_",
52
+ CONTAINS_KEY_LIKE: "_ckl_",
53
+ EMPTY: "_is_empty",
54
+ NOT_EMPTY: "_is_not_empty",
55
+ BETWEEN: "_btw_",
56
+ };
57
+ function resolveAppSecUiBaseUrl(input) {
58
+ const ctx = getAppSecDispatchContext(input);
59
+ const custom = typeof ctx.uiUrl === "string" && ctx.uiUrl.trim()
60
+ ? ctx.uiUrl.trim()
61
+ : DEFAULT_APPSEC_UI_URL;
62
+ return custom.replace(/\/+$/, "");
63
+ }
64
+ function resolveDurationMinutes(input) {
65
+ const ctx = getAppSecDispatchContext(input);
66
+ if (typeof ctx.durationMinutes === "number" && ctx.durationMinutes > 0) {
67
+ return Math.round(ctx.durationMinutes);
68
+ }
69
+ const tf = typeof input?.time_filter === "string" ? TIME_FILTER_MS[input.time_filter.toUpperCase()] : undefined;
70
+ return Math.max(1, Math.round((tf ?? ONE_DAY_MS) / 60_000));
71
+ }
72
+ function encodeAppSecRhsValue(rhs) {
73
+ if (Array.isArray(rhs)) {
74
+ return rhs.map((item) => String(item).replace(/ /g, "%2520")).join("%252c");
75
+ }
76
+ return String(rhs).replace(/ /g, "%2520");
77
+ }
78
+ export function buildAppSecEntityUrl(scope, id, input) {
79
+ if (!id)
80
+ return undefined;
81
+ const baseUrl = resolveAppSecUiBaseUrl(input);
82
+ const durationMinutes = resolveDurationMinutes(input);
83
+ const idPath = APPSEC_ENTITY_SCOPE_TO_ID_PATH[scope];
84
+ if (idPath) {
85
+ return `${baseUrl}/${idPath}/${encodePathSegment(id)}`;
86
+ }
87
+ if (scope === "ACTOR") {
88
+ const encoded = Buffer.from(id, "utf8").toString("base64");
89
+ return `${baseUrl}/attackers-v1?time=${durationMinutes}m&sheet=threat-actors-v1-sidesheet~${encoded}`;
90
+ }
91
+ if (scope === "THREAT_ACTIVITIES") {
92
+ const encoded = Buffer.from(id, "utf8").toString("base64");
93
+ return `${baseUrl}/threat-activity-v2?time=${durationMinutes}m&sheet=threat-activity-v2-detail~${encoded}`;
94
+ }
95
+ return undefined;
96
+ }
97
+ export function buildAppSecFilterUrl(scope, input) {
98
+ const baseUrl = resolveAppSecUiBaseUrl(input);
99
+ if (scope === "BACKEND_API") {
100
+ return `${baseUrl}/third-party-apis`;
101
+ }
102
+ const ctx = getAppSecDispatchContext(input);
103
+ const filters = Array.isArray(ctx.appliedFilters) ? ctx.appliedFilters : [];
104
+ const durationMinutes = ctx.durationMinutes ? Math.round(ctx.durationMinutes) : undefined;
105
+ const filterParts = [];
106
+ if (durationMinutes &&
107
+ (scope === "THREAT_ACTIVITIES" || scope === "DOMAIN_EVENT" || scope === "API_UNDER_THREAT" || scope === "VULNERABILITY")) {
108
+ filterParts.push(`time=${durationMinutes}m`);
109
+ }
110
+ for (const filter of filters) {
111
+ const keyExpr = isRecord(filter.keyExpression) ? filter.keyExpression : undefined;
112
+ const rawKey = typeof keyExpr?.key === "string"
113
+ ? keyExpr.key
114
+ : typeof filter.key === "string"
115
+ ? filter.key
116
+ : undefined;
117
+ const operator = typeof filter.operator === "string" ? filter.operator : undefined;
118
+ const value = filter.value;
119
+ if (!rawKey || !operator || value === undefined || !(operator in APPSEC_OPERATOR_MAP))
120
+ continue;
121
+ if (scope === "THREAT_ACTIVITIES" &&
122
+ (rawKey === "ACTIVITY_STATUS" || rawKey === "LAST_SEEN_MILLIS" || rawKey === "START_TIME_MILLIS")) {
123
+ continue;
124
+ }
125
+ const rhs = encodeAppSecRhsValue(value);
126
+ if (rawKey === "environment" && operator === "EQUALS") {
127
+ filterParts.push(`env=${rhs}`);
128
+ }
129
+ else if (rawKey === "lastCalledMillis" || rawKey === "lastCalledTimestamp") {
130
+ filterParts.push(`filter=lastCalledTime${APPSEC_OPERATOR_MAP[operator]}${rhs}`);
131
+ }
132
+ else {
133
+ filterParts.push(`filter=${rawKey}${APPSEC_OPERATOR_MAP[operator]}${rhs}`);
134
+ }
135
+ }
136
+ if (scope === "API_COVERAGE" || scope === "AST_SCAN" || scope === "SCANS") {
137
+ const suiteId = typeof input?.suite_id === "string" ? input.suite_id : undefined;
138
+ const basePath = suiteId
139
+ ? scope === "API_COVERAGE"
140
+ ? `api-testing-scans-v2/scan-v2/${encodePathSegment(suiteId)}/api-coverage`
141
+ : `api-testing-scans-v2/scan-v2/${encodePathSegment(suiteId)}/scan-runs`
142
+ : "api-testing-scans-v2";
143
+ return filterParts.length > 0 ? `${baseUrl}/${basePath}?${filterParts.join("&")}` : `${baseUrl}/${basePath}`;
144
+ }
145
+ const path = APPSEC_SCOPE_TO_FILTER_PATH[scope];
146
+ if (!path)
147
+ return undefined;
148
+ return filterParts.length > 0 ? `${baseUrl}/${path}?${filterParts.join("&")}` : `${baseUrl}/${path}`;
149
+ }
150
+ export function unwrapAppSecGraphql(raw) {
151
+ if (!isRecord(raw)) {
152
+ throw new HarnessApiError("Unexpected AppSec GraphQL response: expected JSON object", 502);
153
+ }
154
+ if (Array.isArray(raw.errors) && raw.errors.length > 0) {
155
+ const messages = raw.errors
156
+ .map((err) => (isRecord(err) && typeof err.message === "string" ? err.message : JSON.stringify(err)))
157
+ .join("; ");
158
+ throw new HarnessApiError(`AppSec GraphQL error: ${messages}`, 400);
159
+ }
160
+ if (typeof raw.error === "string" && raw.error.length > 0) {
161
+ throw new HarnessApiError(raw.error, 400);
162
+ }
163
+ return isRecord(raw.data) ? raw.data : raw;
164
+ }
165
+ export function flattenAppSecResultRow(row) {
166
+ if (!isRecord(row))
167
+ return {};
168
+ const out = {};
169
+ for (const [key, val] of Object.entries(row)) {
170
+ if (key === "__typename")
171
+ continue;
172
+ if (isRecord(val)) {
173
+ if ("value" in val && Object.keys(val).every((k) => k === "value" || k === "type" || k === "__typename")) {
174
+ out[key] = val.value;
175
+ continue;
176
+ }
177
+ if (isRecord(val.sum) && "value" in val.sum) {
178
+ out[key] = val.sum.value;
179
+ continue;
180
+ }
181
+ if (isRecord(val.count) && "value" in val.count) {
182
+ out[key] = val.count.value;
183
+ continue;
184
+ }
185
+ if (isRecord(val.avg) && "value" in val.avg) {
186
+ out[key] = val.avg.value;
187
+ continue;
188
+ }
189
+ }
190
+ out[key] = val;
191
+ }
192
+ return out;
193
+ }
194
+ /**
195
+ * Per-resource compactItem projection for AppSec list resources.
196
+ * Keeps already-projected domain fields while dropping bulky raw schema/pattern arrays in compact list mode.
197
+ */
198
+ export function compactAppSecItem(item) {
199
+ const { entireUrlPaths: _p, entireUrlPatterns: _pt, schema: _s, ...rest } = item;
200
+ return rest;
201
+ }
202
+ export function appsecEntitiesListExtract(raw, input) {
203
+ const data = unwrapAppSecGraphql(raw);
204
+ const entities = isRecord(data.entities) ? data.entities : {};
205
+ const results = Array.isArray(entities.results) ? entities.results : [];
206
+ const ctx = getAppSecDispatchContext(input);
207
+ const scope = ctx.entityScope
208
+ ?? (typeof input?.scope === "string" && input.scope.trim() ? input.scope.trim().toUpperCase() : "API");
209
+ const items = results.map((entry) => {
210
+ const flat = flattenAppSecResultRow(entry);
211
+ const id = typeof flat.entityId === "string"
212
+ ? flat.entityId
213
+ : typeof flat.id === "string"
214
+ ? flat.id
215
+ : typeof flat.actorId === "string"
216
+ ? flat.actorId
217
+ : undefined;
218
+ if (id && !flat.id)
219
+ flat.id = id;
220
+ const url = id ? buildAppSecEntityUrl(scope, id, input) : undefined;
221
+ return url ? { ...flat, url } : flat;
222
+ });
223
+ const total = typeof entities.total === "number" ? entities.total : items.length;
224
+ const filterUrl = buildAppSecFilterUrl(scope, input);
225
+ return {
226
+ items,
227
+ total,
228
+ scope,
229
+ ...(filterUrl ? { filter_url: filterUrl } : {}),
230
+ };
231
+ }
232
+ export function appsecEntityGetExtract(raw, input) {
233
+ const list = appsecEntitiesListExtract(raw, input);
234
+ if (!list.items || list.items.length === 0) {
235
+ throw new HarnessApiError("AppSec entity not found", 404);
236
+ }
237
+ return list.items[0];
238
+ }
239
+ export function appsecApiSpecGetExtract(raw, input) {
240
+ const data = unwrapAppSecGraphql(raw);
241
+ const apiId = (input?.api_id ?? input?.resource_id ?? "");
242
+ const insight = isRecord(data.apiInsightByApiId) ? data.apiInsightByApiId : undefined;
243
+ const rawDef = insight?.apiDefinition;
244
+ const defObj = Array.isArray(rawDef)
245
+ ? (isRecord(rawDef[0]) ? rawDef[0] : undefined)
246
+ : isRecord(rawDef)
247
+ ? rawDef
248
+ : undefined;
249
+ const definition = typeof defObj?.definition === "string" ? defObj.definition : undefined;
250
+ if (!definition) {
251
+ throw new HarnessApiError(`No OpenAPI specification found for API ID "${apiId}"`, 404);
252
+ }
253
+ return {
254
+ api_id: apiId,
255
+ definition,
256
+ url: buildAppSecEntityUrl("API", apiId, input),
257
+ };
258
+ }
259
+ function parseJsonOrString(raw) {
260
+ if (typeof raw !== "string")
261
+ return raw ?? "";
262
+ const trimmed = raw.trim();
263
+ if (!trimmed)
264
+ return "";
265
+ try {
266
+ return JSON.parse(trimmed);
267
+ }
268
+ catch {
269
+ return raw;
270
+ }
271
+ }
272
+ function parseJsonRecord(raw) {
273
+ const parsed = parseJsonOrString(raw);
274
+ if (!isRecord(parsed))
275
+ return {};
276
+ const out = {};
277
+ for (const [k, v] of Object.entries(parsed)) {
278
+ out[k] = typeof v === "string" ? v : JSON.stringify(v);
279
+ }
280
+ return out;
281
+ }
282
+ function isSensitiveSpanHeader(headerName) {
283
+ const normalized = headerName.trim();
284
+ return SENSITIVE_HEADER_PATTERN.test(normalized) || SENSITIVE_HEADER_SUBSTRING_PATTERN.test(normalized);
285
+ }
286
+ export function redactSpanHeaders(headers) {
287
+ const out = {};
288
+ for (const [k, v] of Object.entries(headers)) {
289
+ out[k] = isSensitiveSpanHeader(k) ? "[REDACTED]" : v;
290
+ }
291
+ return out;
292
+ }
293
+ export function redactSpanCookies(cookies) {
294
+ const out = {};
295
+ for (const k of Object.keys(cookies)) {
296
+ out[k] = "[REDACTED]";
297
+ }
298
+ return out;
299
+ }
300
+ export function sanitizeAndBoundSpanBody(rawBody) {
301
+ const parsed = parseJsonOrString(rawBody);
302
+ if (typeof parsed === "string") {
303
+ return parsed.length > MAX_SPAN_BODY_CHARS
304
+ ? `${parsed.slice(0, MAX_SPAN_BODY_CHARS)}...[truncated]`
305
+ : parsed;
306
+ }
307
+ if (isRecord(parsed) || Array.isArray(parsed)) {
308
+ const redacted = redactSensitiveFields(parsed);
309
+ const serialized = JSON.stringify(redacted);
310
+ if (serialized.length > MAX_SPAN_BODY_CHARS) {
311
+ return `${serialized.slice(0, MAX_SPAN_BODY_CHARS)}...[truncated]`;
312
+ }
313
+ return redacted;
314
+ }
315
+ return parsed;
316
+ }
317
+ export function appsecApiSpansListExtract(raw, input) {
318
+ const data = unwrapAppSecGraphql(raw);
319
+ const apiId = (input?.api_id ?? input?.resource_id ?? "");
320
+ const batch = isRecord(data.submitAnalyticsQueryBatch) ? data.submitAnalyticsQueryBatch : {};
321
+ const queryList = isRecord(batch.query_list) ? batch.query_list : {};
322
+ const results = Array.isArray(queryList.results) ? queryList.results : [];
323
+ const items = results.map((entry) => {
324
+ const flat = flattenAppSecResultRow(entry);
325
+ const spanId = String(flat.id ?? "");
326
+ return {
327
+ span_id: spanId,
328
+ trace_id: String(flat.traceId ?? ""),
329
+ api_id: String(flat.apiId ?? apiId),
330
+ api_name: String(flat.apiName ?? ""),
331
+ service_name: String(flat.serviceName ?? ""),
332
+ status_code: flat.statusCode,
333
+ ip_address: flat.ipAddress,
334
+ user_identifier: flat.userIdentifier,
335
+ session_id: flat.sessionId ? "[REDACTED]" : flat.sessionId,
336
+ request_headers: redactSpanHeaders(parseJsonRecord(flat.spanRequestHeaders)),
337
+ request_body: sanitizeAndBoundSpanBody(flat.spanRequestBody),
338
+ request_cookies: redactSpanCookies(parseJsonRecord(flat.spanRequestCookies)),
339
+ response_headers: redactSpanHeaders(parseJsonRecord(flat.spanResponseHeaders)),
340
+ response_body: sanitizeAndBoundSpanBody(flat.spanResponseBody),
341
+ response_cookies: redactSpanCookies(parseJsonRecord(flat.spanResponseCookies)),
342
+ attributes: redactSpanHeaders(parseJsonRecord(flat.spanTags)),
343
+ data_classification_matches: parseJsonOrString(flat.dataClassificationMatches),
344
+ };
345
+ });
346
+ return {
347
+ items,
348
+ total: items.length,
349
+ api_id: apiId,
350
+ };
351
+ }
352
+ export function appsecVulnerabilitiesListExtract(raw, input) {
353
+ const data = unwrapAppSecGraphql(raw);
354
+ const vuln = isRecord(data.vulnerabilitiesV3) ? data.vulnerabilitiesV3 : {};
355
+ const results = Array.isArray(vuln.results) ? vuln.results : [];
356
+ const items = results.map((entry) => {
357
+ const flat = flattenAppSecResultRow(entry);
358
+ const id = typeof flat.VULNERABILITY_ID === "string"
359
+ ? flat.VULNERABILITY_ID
360
+ : typeof flat.id === "string"
361
+ ? flat.id
362
+ : undefined;
363
+ return id && !flat.id ? { id, ...flat } : flat;
364
+ });
365
+ const total = typeof vuln.total === "number" ? vuln.total : items.length;
366
+ const filterUrl = buildAppSecFilterUrl("VULNERABILITY", input);
367
+ return {
368
+ items,
369
+ total,
370
+ ...(filterUrl ? { filter_url: filterUrl } : {}),
371
+ };
372
+ }
373
+ export function appsecVulnerabilityGetExtract(raw, input) {
374
+ const list = appsecVulnerabilitiesListExtract(raw, input);
375
+ if (!list.items || list.items.length === 0) {
376
+ throw new HarnessApiError("Vulnerability not found", 404);
377
+ }
378
+ return {
379
+ ...list.items[0],
380
+ ...(list.filter_url ? { url: list.filter_url } : {}),
381
+ };
382
+ }
383
+ export function appsecVulnEnumValuesExtract(raw, input) {
384
+ const data = unwrapAppSecGraphql(raw);
385
+ const ctx = getAppSecDispatchContext(input);
386
+ const supportedValues = {};
387
+ // Support legacy single vulnerabilitiesV3.results[0] shape
388
+ if (isRecord(data.vulnerabilitiesV3) && Array.isArray(data.vulnerabilitiesV3.results)) {
389
+ const first = isRecord(data.vulnerabilitiesV3.results[0]) ? data.vulnerabilitiesV3.results[0] : {};
390
+ const keys = ctx.enumKeys ?? Object.keys(first);
391
+ for (const key of keys) {
392
+ const cell = first[key];
393
+ if (isRecord(cell) && Array.isArray(cell.value)) {
394
+ supportedValues[key] = { values: cell.value, valueType: "string" };
395
+ }
396
+ }
397
+ return { supported_values: supportedValues };
398
+ }
399
+ // Fast aliased groupBy shape: data[key].results = [{ [key]: { value }, count: { value } }]
400
+ const keys = ctx.enumKeys ?? Object.keys(data);
401
+ for (const key of keys) {
402
+ const bucket = data[key];
403
+ if (!isRecord(bucket) || !Array.isArray(bucket.results))
404
+ continue;
405
+ const values = [];
406
+ for (const row of bucket.results) {
407
+ if (!isRecord(row))
408
+ continue;
409
+ const cell = row[key];
410
+ if (isRecord(cell) && cell.value !== undefined && cell.value !== null) {
411
+ values.push(cell.value);
412
+ }
413
+ }
414
+ supportedValues[key] = { values, valueType: "string" };
415
+ }
416
+ return { supported_values: supportedValues };
417
+ }
418
+ export function appsecThreatActivityListExtract(raw, input) {
419
+ const data = unwrapAppSecGraphql(raw);
420
+ const threat = isRecord(data.threatActivity) ? data.threatActivity : {};
421
+ const results = Array.isArray(threat.results) ? threat.results : [];
422
+ const items = results.map((entry) => {
423
+ const flat = flattenAppSecResultRow(entry);
424
+ const id = typeof flat.ID === "string"
425
+ ? flat.ID
426
+ : typeof flat.id === "string"
427
+ ? flat.id
428
+ : undefined;
429
+ const url = id ? buildAppSecEntityUrl("THREAT_ACTIVITIES", id, input) : undefined;
430
+ return {
431
+ ...(id && !flat.id ? { id } : {}),
432
+ ...flat,
433
+ ...(url ? { url } : {}),
434
+ };
435
+ });
436
+ const total = typeof threat.total === "number" ? threat.total : items.length;
437
+ const filterUrl = buildAppSecFilterUrl("THREAT_ACTIVITIES", input);
438
+ return {
439
+ items,
440
+ total,
441
+ ...(filterUrl ? { filter_url: filterUrl } : {}),
442
+ };
443
+ }
444
+ export function appsecThreatActivityGetExtract(raw, input) {
445
+ const list = appsecThreatActivityListExtract(raw, input);
446
+ if (!list.items || list.items.length === 0) {
447
+ throw new HarnessApiError("Threat activity not found", 404);
448
+ }
449
+ return list.items[0];
450
+ }
451
+ export function appsecExploreListExtract(raw, input) {
452
+ const data = unwrapAppSecGraphql(raw);
453
+ const explore = isRecord(data.explore) ? data.explore : {};
454
+ const results = Array.isArray(explore.results) ? explore.results : [];
455
+ const items = results.map((entry) => flattenAppSecResultRow(entry));
456
+ const total = typeof explore.total === "number" ? explore.total : items.length;
457
+ const ctx = getAppSecDispatchContext(input);
458
+ const exploreScope = ctx.exploreScope ?? "DOMAIN_EVENT";
459
+ const urlScope = input?.mode === "apis_under_threat" ? "API_UNDER_THREAT" : exploreScope;
460
+ const filterUrl = buildAppSecFilterUrl(urlScope, input);
461
+ return {
462
+ items,
463
+ total,
464
+ scope: exploreScope,
465
+ ...(filterUrl ? { filter_url: filterUrl } : {}),
466
+ };
467
+ }
468
+ export function appsecAstScanListExtract(raw, input) {
469
+ const data = unwrapAppSecGraphql(raw);
470
+ const searchAst = isRecord(data.searchAst) ? data.searchAst : {};
471
+ const results = Array.isArray(searchAst.results) ? searchAst.results : [];
472
+ const dataSource = typeof input?.data_source === "string" ? input.data_source : "SCANS";
473
+ const items = results.map((entry) => {
474
+ const flat = flattenAppSecResultRow(entry);
475
+ const apiId = typeof flat.API_ID === "string" ? flat.API_ID : undefined;
476
+ const url = dataSource === "API_COVERAGE" && apiId
477
+ ? buildAppSecEntityUrl("API_COVERAGE", apiId, input)
478
+ : undefined;
479
+ return url ? { ...flat, url } : flat;
480
+ });
481
+ const total = typeof searchAst.total === "number" ? searchAst.total : items.length;
482
+ const filterUrl = buildAppSecFilterUrl(dataSource === "API_COVERAGE" ? "API_COVERAGE" : "AST_SCAN", input);
483
+ return {
484
+ items,
485
+ total,
486
+ data_source: dataSource,
487
+ ...(filterUrl ? { filter_url: filterUrl } : {}),
488
+ };
489
+ }
490
+ export function appsecAstScanGetExtract(raw, input) {
491
+ const data = unwrapAppSecGraphql(raw);
492
+ if (data.getAstHookTests !== undefined) {
493
+ return {
494
+ hook_id: input?.hook_id ?? input?.suite_id ?? input?.resource_id,
495
+ hook_tests: data.getAstHookTests,
496
+ };
497
+ }
498
+ const list = appsecAstScanListExtract(raw, input);
499
+ if (!list.items || list.items.length === 0) {
500
+ throw new HarnessApiError("AST scan not found", 404);
501
+ }
502
+ return {
503
+ suite_id: input?.suite_id ?? input?.resource_id,
504
+ latest_run: list.items[0],
505
+ runs: list.items,
506
+ total: list.total,
507
+ ...(list.filter_url ? { url: list.filter_url } : {}),
508
+ };
509
+ }
510
+ function extractBatchEventCountsByDate(queryResult) {
511
+ const qr = isRecord(queryResult) ? queryResult : {};
512
+ const results = Array.isArray(qr.results) ? qr.results : [];
513
+ const byDate = {};
514
+ let totalEvents = 0;
515
+ for (const event of results) {
516
+ if (!isRecord(event))
517
+ continue;
518
+ let count = 0;
519
+ for (const [key, val] of Object.entries(event)) {
520
+ if (key === "intervalStart" || key === "__typename")
521
+ continue;
522
+ if (typeof val === "number") {
523
+ count = val;
524
+ break;
525
+ }
526
+ if (isRecord(val) && typeof val.value === "number") {
527
+ count = val.value;
528
+ break;
529
+ }
530
+ }
531
+ totalEvents += count;
532
+ if (typeof event.intervalStart === "string" && event.intervalStart.length >= 10) {
533
+ const dateStr = event.intervalStart.slice(0, 10);
534
+ byDate[dateStr] = (byDate[dateStr] ?? 0) + count;
535
+ }
536
+ }
537
+ return { byDate, totalEvents };
538
+ }
539
+ export function appsecTrendListExtract(raw, input) {
540
+ const data = unwrapAppSecGraphql(raw);
541
+ const batch = isRecord(data.submitAnalyticsQueryBatch) ? data.submitAnalyticsQueryBatch : {};
542
+ const ctx = getAppSecDispatchContext(input);
543
+ const currentCount = typeof ctx.currentCount === "number" ? ctx.currentCount : 0;
544
+ const periodDays = typeof ctx.periodDays === "number"
545
+ ? ctx.periodDays
546
+ : typeof input?.period_days === "number" && input.period_days > 0
547
+ ? Math.min(input.period_days, 90)
548
+ : 7;
549
+ const trendType = typeof input?.trend_type === "string" ? input.trend_type : "apis";
550
+ const openedStats = extractBatchEventCountsByDate(batch.opened_events);
551
+ const closedStats = extractBatchEventCountsByDate(batch.closed_events);
552
+ const countNDaysAgo = currentCount - openedStats.totalEvents + closedStats.totalEvents;
553
+ const now = new Date();
554
+ const currentUtcMidnight = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
555
+ const series = [];
556
+ let runningValue = currentCount;
557
+ for (let day = 0; day < periodDays; day++) {
558
+ const calcDate = new Date(currentUtcMidnight.getTime() - day * 86_400_000);
559
+ const dateKey = calcDate.toISOString().slice(0, 10);
560
+ const dayOpened = openedStats.byDate[dateKey] ?? 0;
561
+ const dayClosed = closedStats.byDate[dateKey] ?? 0;
562
+ if (day === 0) {
563
+ series.push({ date: dateKey, count: runningValue, opened: dayOpened, closed: dayClosed });
564
+ }
565
+ else if (day === 1) {
566
+ const todayKey = currentUtcMidnight.toISOString().slice(0, 10);
567
+ const todayOpened = openedStats.byDate[todayKey] ?? 0;
568
+ const todayClosed = closedStats.byDate[todayKey] ?? 0;
569
+ const yesterdayCount = currentCount - todayOpened - dayOpened + todayClosed + dayClosed;
570
+ runningValue = yesterdayCount;
571
+ series.push({ date: dateKey, count: runningValue, opened: dayOpened, closed: dayClosed });
572
+ }
573
+ else {
574
+ runningValue = runningValue - dayOpened + dayClosed;
575
+ series.push({ date: dateKey, count: runningValue, opened: dayOpened, closed: dayClosed });
576
+ }
577
+ }
578
+ return {
579
+ trend_type: trendType,
580
+ period_days: periodDays,
581
+ current_count: currentCount,
582
+ count_n_days_ago: countNDaysAgo,
583
+ total_opened_or_discovered: openedStats.totalEvents,
584
+ total_closed_or_deleted: closedStats.totalEvents,
585
+ formula: `current(${currentCount}) - opened(${openedStats.totalEvents}) + closed(${closedStats.totalEvents}) = ${countNDaysAgo}`,
586
+ items: series,
587
+ total: series.length,
588
+ };
589
+ }
590
+ export function appsecDatatypesListExtract(raw, input) {
591
+ const data = unwrapAppSecGraphql(raw);
592
+ const dtRoot = isRecord(data.dataTypes) ? data.dataTypes : {};
593
+ const allEntries = (Array.isArray(dtRoot.results) ? dtRoot.results : []).filter(isRecord);
594
+ const rawNames = input?.names;
595
+ const names = Array.isArray(rawNames)
596
+ ? rawNames.map(String)
597
+ : typeof rawNames === "string" && rawNames.trim()
598
+ ? rawNames.split(",").map((s) => s.trim()).filter(Boolean)
599
+ : [];
600
+ const rawIds = input?.ids;
601
+ const ids = Array.isArray(rawIds)
602
+ ? rawIds.map(String)
603
+ : typeof rawIds === "string" && rawIds.trim()
604
+ ? rawIds.split(",").map((s) => s.trim()).filter(Boolean)
605
+ : [];
606
+ const sensitivityFilter = typeof input?.sensitivity === "string" && input.sensitivity.trim()
607
+ ? input.sensitivity.trim().toUpperCase()
608
+ : undefined;
609
+ let filtered = allEntries;
610
+ if (ids.length > 0) {
611
+ const idSet = new Set(ids);
612
+ filtered = filtered.filter((entry) => typeof entry.id === "string" && idSet.has(entry.id));
613
+ }
614
+ else if (names.length > 0) {
615
+ const matchedIds = new Set();
616
+ const matched = [];
617
+ const nameToEntries = new Map();
618
+ for (const entry of allEntries) {
619
+ const n = typeof entry.name === "string" ? entry.name : "";
620
+ if (!nameToEntries.has(n))
621
+ nameToEntries.set(n, []);
622
+ nameToEntries.get(n).push(entry);
623
+ }
624
+ const addEntry = (e) => {
625
+ const key = typeof e.id === "string" ? e.id : JSON.stringify(e);
626
+ if (!matchedIds.has(key)) {
627
+ matchedIds.add(key);
628
+ matched.push(e);
629
+ }
630
+ };
631
+ for (const name of names) {
632
+ const parts = name.toLowerCase().split(/\s+/).filter(Boolean);
633
+ for (const [categoryKey, categoryValues] of Object.entries(APPSEC_DATATYPE_MAPPING)) {
634
+ const catLower = categoryKey.toLowerCase();
635
+ if (parts.some((p) => catLower.includes(p))) {
636
+ for (const mappedName of categoryValues) {
637
+ for (const e of nameToEntries.get(mappedName) ?? [])
638
+ addEntry(e);
639
+ }
640
+ }
641
+ }
642
+ for (const [entryName, entries] of nameToEntries.entries()) {
643
+ const entryLower = entryName.toLowerCase();
644
+ if (parts.some((p) => entryLower.includes(p))) {
645
+ for (const e of entries)
646
+ addEntry(e);
647
+ }
648
+ }
649
+ }
650
+ filtered = matched;
651
+ }
652
+ if (sensitivityFilter) {
653
+ filtered = filtered.filter((entry) => typeof entry.sensitivity === "string" && entry.sensitivity.toUpperCase() === sensitivityFilter);
654
+ }
655
+ const items = filtered.map((entry) => {
656
+ const { __typename: _t, ...rest } = entry;
657
+ return {
658
+ ...rest,
659
+ dataTypeId: entry.id,
660
+ };
661
+ });
662
+ return {
663
+ items,
664
+ total: items.length,
665
+ };
666
+ }
667
+ export function appsecDatatypeGetExtract(raw, input) {
668
+ const id = (input?.datatype_id ?? input?.resource_id ?? "");
669
+ const list = appsecDatatypesListExtract(raw, { ...input, ids: [id] });
670
+ if (!list.items || list.items.length === 0) {
671
+ throw new HarnessApiError(`AppSec datatype "${id}" not found`, 404);
672
+ }
673
+ return list.items[0];
674
+ }
675
+ export function appsecFraudPolicyCreateExtract(raw) {
676
+ const data = unwrapAppSecGraphql(raw);
677
+ const createResp = isRecord(data.CreateFraudPolicy) ? data.CreateFraudPolicy : {};
678
+ const policy = isRecord(createResp.fraud_policy) ? createResp.fraud_policy : createResp;
679
+ return {
680
+ status: "SUCCESS",
681
+ id: policy.id,
682
+ name: policy.name,
683
+ disabled: policy.disabled,
684
+ fraud_policy: policy,
685
+ };
686
+ }
687
+ export function appsecDerivationConfigCreateExtract(raw) {
688
+ const data = unwrapAppSecGraphql(raw);
689
+ const createResp = isRecord(data.CreateDerivationConfig) ? data.CreateDerivationConfig : {};
690
+ const config = isRecord(createResp.derivation_config) ? createResp.derivation_config : createResp;
691
+ return {
692
+ status: "SUCCESS",
693
+ id: config.id,
694
+ name: config.name,
695
+ disabled: config.disabled,
696
+ derivation_config: config,
697
+ };
698
+ }
699
+ //# sourceMappingURL=extractors.js.map