rhombus-node-mcp 0.1.51 → 0.1.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/elements-tool-api.js +6 -9
- package/dist/api/netbox-access-anomaly-tool-api.js +105 -0
- package/dist/api/netbox-badge-timeline-tool-api.js +56 -0
- package/dist/api/netbox-lost-badge-tool-api.js +86 -0
- package/dist/api/netbox-tool-api.js +61 -0
- package/dist/api/onguard-tool-api.js +6 -6
- package/dist/tools-console/netbox-access-anomaly-tool.js +53 -0
- package/dist/tools-console/netbox-badge-timeline-tool.js +50 -0
- package/dist/tools-console/netbox-lost-badge-tool.js +47 -0
- package/dist/tools-console/netbox-tool.js +55 -0
- package/dist/types/netbox-access-anomaly-tool-types.js +3 -0
- package/dist/types/netbox-badge-timeline-tool-types.js +3 -0
- package/dist/types/netbox-lost-badge-tool-types.js +3 -0
- package/dist/types/netbox-tool-types.js +4 -0
- package/dist/types/schema.js +9 -0
- package/dist/types/zod-schemas.js +22 -0
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { postApi } from "../network/network.js";
|
|
2
|
+
import { ActivityEnum } from "../types/schema.js";
|
|
2
3
|
import { formatTimestamp } from "../util.js";
|
|
3
4
|
/**
|
|
4
5
|
* Honeywell Elements (LenelS2 Elements) activity enum values. Passing these as `activityTypes` to the
|
|
@@ -6,19 +7,15 @@ import { formatTimestamp } from "../util.js";
|
|
|
6
7
|
* OnGuard search, which is implicitly scoped to ONGUARD_* types server-side.
|
|
7
8
|
*/
|
|
8
9
|
export const ELEMENTS_ACTIVITY_TYPES = [
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
ActivityEnum.ELEMENTS_BADGE_AUTHORIZED,
|
|
11
|
+
ActivityEnum.ELEMENTS_BADGE_ANOMALY,
|
|
12
|
+
ActivityEnum.ELEMENTS_NO_ENTRY_MADE,
|
|
12
13
|
];
|
|
13
14
|
/**
|
|
14
15
|
* Calls the generalized webservice access-control event search
|
|
15
16
|
* (POST /eventSearchV2/searchIntegrationAccessEvents) scoped to Honeywell Elements via `activityTypes`, and
|
|
16
|
-
* maps the raw seekpoints to the same agent-friendly shape as searchOnGuardEvents.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* The generalized endpoint, its `activityTypes` field, and the ELEMENTS_* enum values predate the generated
|
|
20
|
-
* public OpenAPI schema, so the body is built off the OnGuard request DTO and the extra field is added via a
|
|
21
|
-
* cast until `assets/openapi.json` is regenerated.
|
|
17
|
+
* maps the raw seekpoints to the same agent-friendly shape as searchOnGuardEvents. Typed against the
|
|
18
|
+
* generated public OpenAPI schema (Eventsearch_SearchIntegrationAccessEventsWSRequest/Response).
|
|
22
19
|
*/
|
|
23
20
|
export async function searchElementsEvents(args, timeZone, requestModifiers, sessionId) {
|
|
24
21
|
const body = {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { buildMediaHints } from "./badge-correlation.js";
|
|
2
|
+
import { searchNetboxEvents } from "./netbox-tool-api.js";
|
|
3
|
+
const ALL_RULES = [
|
|
4
|
+
"lost_or_inactive_badge",
|
|
5
|
+
"entry_not_made",
|
|
6
|
+
"off_hours",
|
|
7
|
+
"impossible_travel",
|
|
8
|
+
"area_novelty",
|
|
9
|
+
];
|
|
10
|
+
const DAY_MS = 86_400_000;
|
|
11
|
+
/**
|
|
12
|
+
* Flags anomalous Lenel S2 NetBox access events over a window: deterministic rules (lost/inactive badge,
|
|
13
|
+
* no-entry, off-hours, impossible travel, first-time-in-area vs a baseline) computed over searchNetboxEvents,
|
|
14
|
+
* each finding carrying the still/clip hints to confirm it. The agent narrates/triages from here.
|
|
15
|
+
*/
|
|
16
|
+
export async function getNetboxAccessAnomalies(args, timeZone, requestModifiers, sessionId) {
|
|
17
|
+
const rules = new Set(args.rules && args.rules.length ? args.rules : ALL_RULES);
|
|
18
|
+
const offStart = args.offHoursStartHour ?? 7;
|
|
19
|
+
const offEnd = args.offHoursEndHour ?? 19;
|
|
20
|
+
const maxTravelSec = args.impossibleTravelMaxSeconds ?? 30;
|
|
21
|
+
const baselineDays = args.baselineDays ?? 30;
|
|
22
|
+
const scope = { area: args.area, locationUuids: args.locationUuids, deviceUuids: args.deviceUuids };
|
|
23
|
+
const { events } = await searchNetboxEvents({ ...scope, afterMs: args.afterMs, beforeMs: args.beforeMs, limit: args.limit ?? 500 }, timeZone, requestModifiers, sessionId);
|
|
24
|
+
// Baseline area-set per cardholder (for area_novelty).
|
|
25
|
+
const baselineAreas = new Map();
|
|
26
|
+
if (rules.has("area_novelty") && baselineDays > 0 && args.afterMs != null) {
|
|
27
|
+
const { events: baseEvents } = await searchNetboxEvents({ ...scope, afterMs: args.afterMs - baselineDays * DAY_MS, beforeMs: args.afterMs, limit: 1000 }, timeZone, requestModifiers, sessionId);
|
|
28
|
+
for (const e of baseEvents) {
|
|
29
|
+
if (!e.cardholderName || !e.areaEntering)
|
|
30
|
+
continue;
|
|
31
|
+
const set = baselineAreas.get(e.cardholderName) ?? new Set();
|
|
32
|
+
set.add(e.areaEntering);
|
|
33
|
+
baselineAreas.set(e.cardholderName, set);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const hourFmt = new Intl.DateTimeFormat("en-US", { timeZone, hour: "2-digit", hourCycle: "h23" });
|
|
37
|
+
const localHour = (ms) => parseInt(hourFmt.format(new Date(ms)), 10);
|
|
38
|
+
const findings = [];
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
const add = (e, rule, severity, rationale) => {
|
|
41
|
+
const key = `${e.cardholderName ?? ""}|${rule}|${e.timestampMs ?? ""}|${e.areaEntering ?? ""}`;
|
|
42
|
+
if (seen.has(key))
|
|
43
|
+
return;
|
|
44
|
+
seen.add(key);
|
|
45
|
+
const { clipHint, stillHint } = buildMediaHints({ deviceUuid: e.deviceUuid, timestampMs: e.timestampMs });
|
|
46
|
+
findings.push({
|
|
47
|
+
cardholderName: e.cardholderName,
|
|
48
|
+
rule,
|
|
49
|
+
severity,
|
|
50
|
+
datetime: e.datetime,
|
|
51
|
+
timestampMs: e.timestampMs,
|
|
52
|
+
deviceUuid: e.deviceUuid,
|
|
53
|
+
area: e.areaEntering ?? e.areaExiting,
|
|
54
|
+
rationale,
|
|
55
|
+
clipHint,
|
|
56
|
+
stillHint,
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
for (const e of events) {
|
|
60
|
+
const at = e.areaEntering ? ` at "${e.areaEntering}"` : "";
|
|
61
|
+
if (rules.has("lost_or_inactive_badge") && e.badgeStatus && e.badgeStatus.toLowerCase() !== "active") {
|
|
62
|
+
add(e, "lost_or_inactive_badge", "high", `Badge status "${e.badgeStatus}" used${at}.`);
|
|
63
|
+
}
|
|
64
|
+
if (rules.has("entry_not_made") && e.entryMade === false) {
|
|
65
|
+
add(e, "entry_not_made", "medium", `Access granted but no entry made${at} — possible tailgating or aborted entry.`);
|
|
66
|
+
}
|
|
67
|
+
if (rules.has("off_hours") && e.timestampMs != null) {
|
|
68
|
+
const h = localHour(e.timestampMs);
|
|
69
|
+
if (h < offStart || h >= offEnd) {
|
|
70
|
+
add(e, "off_hours", "medium", `Entry ${e.datetime ?? `${h}:00`} is outside business hours (${offStart}:00–${offEnd}:00)${at}.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (rules.has("area_novelty") && baselineAreas.size > 0 && e.cardholderName && e.areaEntering) {
|
|
74
|
+
const base = baselineAreas.get(e.cardholderName);
|
|
75
|
+
if (base && !base.has(e.areaEntering)) {
|
|
76
|
+
add(e, "area_novelty", "high", `First entry to "${e.areaEntering}" — not in ${e.cardholderName}'s prior ${baselineDays}-day pattern.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Impossible travel: per cardholder, consecutive taps in different areas within the window.
|
|
81
|
+
if (rules.has("impossible_travel")) {
|
|
82
|
+
const byPerson = new Map();
|
|
83
|
+
for (const e of events) {
|
|
84
|
+
if (!e.cardholderName || e.timestampMs == null)
|
|
85
|
+
continue;
|
|
86
|
+
const arr = byPerson.get(e.cardholderName) ?? [];
|
|
87
|
+
arr.push(e);
|
|
88
|
+
byPerson.set(e.cardholderName, arr);
|
|
89
|
+
}
|
|
90
|
+
for (const [name, evs] of byPerson) {
|
|
91
|
+
const sorted = evs.slice().sort((a, b) => (a.timestampMs ?? 0) - (b.timestampMs ?? 0));
|
|
92
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
93
|
+
const a = sorted[i - 1];
|
|
94
|
+
const b = sorted[i];
|
|
95
|
+
const gapSec = ((b.timestampMs ?? 0) - (a.timestampMs ?? 0)) / 1000;
|
|
96
|
+
if (a.areaEntering && b.areaEntering && a.areaEntering !== b.areaEntering && gapSec <= maxTravelSec) {
|
|
97
|
+
add(b, "impossible_travel", "high", `${name} at "${a.areaEntering}" then "${b.areaEntering}" ${Math.round(gapSec)}s apart — physically implausible for one person.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const sev = (s) => (s === "high" ? 0 : 1);
|
|
103
|
+
findings.sort((a, b) => sev(a.severity) - sev(b.severity) || (b.timestampMs ?? 0) - (a.timestampMs ?? 0));
|
|
104
|
+
return { findings, eventsAnalyzed: events.length };
|
|
105
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { buildMediaHints } from "./badge-correlation.js";
|
|
2
|
+
import { searchNetboxEvents } from "./netbox-tool-api.js";
|
|
3
|
+
/**
|
|
4
|
+
* Reconstructs a single cardholder's movements as a chronological timeline of Lenel S2 NetBox badge taps,
|
|
5
|
+
* each with the still/clip hints needed to show what happened. Pure orchestration over
|
|
6
|
+
* searchNetboxEvents + the shared correlation helper.
|
|
7
|
+
*/
|
|
8
|
+
export async function getNetboxBadgeTimeline(args, timeZone, requestModifiers, sessionId) {
|
|
9
|
+
const { events } = await searchNetboxEvents({
|
|
10
|
+
cardholderQuery: args.cardholderQuery,
|
|
11
|
+
locationUuids: args.locationUuids,
|
|
12
|
+
afterMs: args.afterMs,
|
|
13
|
+
beforeMs: args.beforeMs,
|
|
14
|
+
limit: args.limit ?? 200,
|
|
15
|
+
}, timeZone, requestModifiers, sessionId);
|
|
16
|
+
// searchNetboxEvents returns newest-first; a timeline reads chronologically.
|
|
17
|
+
const ordered = events
|
|
18
|
+
.filter((e) => e.timestampMs != null)
|
|
19
|
+
.sort((a, b) => (a.timestampMs ?? 0) - (b.timestampMs ?? 0));
|
|
20
|
+
const stops = ordered.map((e, i) => {
|
|
21
|
+
const next = ordered[i + 1];
|
|
22
|
+
const gapToNextSeconds = next?.timestampMs != null && e.timestampMs != null
|
|
23
|
+
? Math.round((next.timestampMs - e.timestampMs) / 1000)
|
|
24
|
+
: undefined;
|
|
25
|
+
const { clipHint, stillHint } = buildMediaHints({ deviceUuid: e.deviceUuid, timestampMs: e.timestampMs }, args.clipPaddingSeconds);
|
|
26
|
+
return {
|
|
27
|
+
timestampMs: e.timestampMs,
|
|
28
|
+
datetime: e.datetime,
|
|
29
|
+
deviceUuid: e.deviceUuid,
|
|
30
|
+
area: e.areaEntering ?? e.areaExiting,
|
|
31
|
+
label: e.label,
|
|
32
|
+
isAnomaly: e.isAnomaly,
|
|
33
|
+
clipHint,
|
|
34
|
+
stillHint,
|
|
35
|
+
gapToNextSeconds,
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
// The sequence of areas traversed, collapsing consecutive repeats.
|
|
39
|
+
const path = [];
|
|
40
|
+
for (const stop of stops) {
|
|
41
|
+
if (stop.area && stop.area !== path[path.length - 1]) {
|
|
42
|
+
path.push(stop.area);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// cardholderQuery is full-text, so it can match more than one person — surface that so the agent
|
|
46
|
+
// can disambiguate rather than silently merge two people's movements.
|
|
47
|
+
const distinctCardholders = [
|
|
48
|
+
...new Set(ordered.map((e) => e.cardholderName).filter((n) => !!n)),
|
|
49
|
+
];
|
|
50
|
+
return {
|
|
51
|
+
cardholderName: distinctCardholders[0],
|
|
52
|
+
ambiguousCardholders: distinctCardholders.length > 1 ? distinctCardholders : undefined,
|
|
53
|
+
stops,
|
|
54
|
+
path,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { buildMediaHints } from "./badge-correlation.js";
|
|
2
|
+
import { searchNetboxEvents } from "./netbox-tool-api.js";
|
|
3
|
+
import { getFaceEvents, searchSimilarFaces } from "./faces-tool-api.js";
|
|
4
|
+
/**
|
|
5
|
+
* Lost / stolen-badge live response for Lenel S2 NetBox. Finds recent lost/inactive-badge use, and for
|
|
6
|
+
* each: the door clip/still, the face captured at the door (identified or UNIDENTIFIED), and a cross-camera
|
|
7
|
+
* track of that same face (via similar-face search) ordered to a last-known location. Detection + door
|
|
8
|
+
* evidence are exact; the track is best-effort (depends on face capture + recognition coverage).
|
|
9
|
+
*/
|
|
10
|
+
export async function getNetboxLostBadgeResponse(args, timeZone, requestModifiers, sessionId) {
|
|
11
|
+
const scope = { area: args.area, locationUuids: args.locationUuids, deviceUuids: args.deviceUuids };
|
|
12
|
+
const { events } = await searchNetboxEvents({ ...scope, anomalyOnly: true, afterMs: args.afterMs, beforeMs: args.beforeMs, limit: args.limit ?? 50 }, timeZone, requestModifiers, sessionId);
|
|
13
|
+
// anomalyOnly returns lost-badge AND no-entry anomalies; keep the lost/inactive-badge ones. NetBox
|
|
14
|
+
// credentialStatus uses values like Lost/Stolen/Terminated — anything that isn't "active".
|
|
15
|
+
const lostEvents = events.filter((e) => e.badgeStatus && e.badgeStatus.toLowerCase() !== "active");
|
|
16
|
+
const winMs = (args.faceWindowSeconds ?? 30) * 1000;
|
|
17
|
+
const incidents = [];
|
|
18
|
+
for (const e of lostEvents) {
|
|
19
|
+
const { clipHint, stillHint } = buildMediaHints({ deviceUuid: e.deviceUuid, timestampMs: e.timestampMs });
|
|
20
|
+
let facesAtDoor = [];
|
|
21
|
+
let sightings = [];
|
|
22
|
+
if (e.deviceUuid && e.timestampMs != null) {
|
|
23
|
+
try {
|
|
24
|
+
// Face(s) at the door: query by time window (the faces tool can't filter by device), then match
|
|
25
|
+
// the door camera client-side.
|
|
26
|
+
const { faceEvents } = await getFaceEvents({
|
|
27
|
+
pageRequest: { lastEvaluatedKey: null, maxPageSize: 75 },
|
|
28
|
+
searchFilter: {
|
|
29
|
+
faceNameContains: null,
|
|
30
|
+
faceNames: [],
|
|
31
|
+
hasEmbedding: null,
|
|
32
|
+
hasName: null,
|
|
33
|
+
labels: [],
|
|
34
|
+
locationUuids: [],
|
|
35
|
+
personUuids: [],
|
|
36
|
+
timestampFilter: {
|
|
37
|
+
rangeStart: new Date(e.timestampMs - winMs).toISOString(),
|
|
38
|
+
rangeEnd: new Date(e.timestampMs + winMs).toISOString(),
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}, timeZone, requestModifiers, sessionId);
|
|
42
|
+
facesAtDoor = faceEvents
|
|
43
|
+
.filter((f) => f.deviceUuid === e.deviceUuid)
|
|
44
|
+
.map((f) => ({
|
|
45
|
+
faceName: f.faceName ?? undefined,
|
|
46
|
+
personUuid: f.personUuid ?? undefined,
|
|
47
|
+
thumbnailS3Key: f.thumbnailS3Key ?? undefined,
|
|
48
|
+
faceEventUuid: f.uuid ?? undefined,
|
|
49
|
+
eventTimestamp: f.eventTimestamp,
|
|
50
|
+
}));
|
|
51
|
+
// Track that face across cameras, ordered in time.
|
|
52
|
+
const seed = facesAtDoor.find((f) => f.faceEventUuid);
|
|
53
|
+
if (seed?.faceEventUuid) {
|
|
54
|
+
const similar = await searchSimilarFaces(seed.faceEventUuid, timeZone, requestModifiers, sessionId);
|
|
55
|
+
sightings = similar
|
|
56
|
+
.map((s) => ({
|
|
57
|
+
deviceUuid: s.deviceUuid,
|
|
58
|
+
datetime: s.eventTimestamp,
|
|
59
|
+
timestampMs: s.eventTimestampMs,
|
|
60
|
+
similarity: s.similarity,
|
|
61
|
+
personUuid: s.personUuid,
|
|
62
|
+
}))
|
|
63
|
+
.sort((a, b) => (a.timestampMs ?? 0) - (b.timestampMs ?? 0));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Face enrichment is best-effort — never fail the whole response over it.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const last = sightings.length ? sightings[sightings.length - 1] : undefined;
|
|
71
|
+
incidents.push({
|
|
72
|
+
cardholderOfRecord: e.cardholderName,
|
|
73
|
+
badgeStatus: e.badgeStatus,
|
|
74
|
+
datetime: e.datetime,
|
|
75
|
+
timestampMs: e.timestampMs,
|
|
76
|
+
deviceUuid: e.deviceUuid,
|
|
77
|
+
area: e.areaEntering ?? e.areaExiting,
|
|
78
|
+
clipHint,
|
|
79
|
+
stillHint,
|
|
80
|
+
facesAtDoor,
|
|
81
|
+
sightings,
|
|
82
|
+
lastKnownSighting: last ? { deviceUuid: last.deviceUuid, datetime: last.datetime } : undefined,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return { incidents, count: incidents.length };
|
|
86
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { postApi } from "../network/network.js";
|
|
2
|
+
import { ActivityEnum } from "../types/schema.js";
|
|
3
|
+
import { formatTimestamp } from "../util.js";
|
|
4
|
+
/**
|
|
5
|
+
* Lenel S2 NetBox (Honeywell NetBox) activity enum values. Passing these as `activityTypes` to the
|
|
6
|
+
* generalized access-control event search scopes results to NetBox events only — exactly mirroring the
|
|
7
|
+
* OnGuard search, which is implicitly scoped to ONGUARD_* types server-side.
|
|
8
|
+
*/
|
|
9
|
+
export const NETBOX_ACTIVITY_TYPES = [
|
|
10
|
+
ActivityEnum.NETBOX_BADGE_AUTHORIZED,
|
|
11
|
+
ActivityEnum.NETBOX_BADGE_ANOMALY,
|
|
12
|
+
ActivityEnum.NETBOX_NO_ENTRY_MADE,
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Calls the generalized webservice access-control event search
|
|
16
|
+
* (POST /eventSearchV2/searchIntegrationAccessEvents) scoped to Lenel S2 NetBox via `activityTypes`, and
|
|
17
|
+
* maps the raw seekpoints to the same agent-friendly shape as searchOnGuardEvents. Typed against the
|
|
18
|
+
* generated public OpenAPI schema (Eventsearch_SearchIntegrationAccessEventsWSRequest/Response).
|
|
19
|
+
*/
|
|
20
|
+
export async function searchNetboxEvents(args, timeZone, requestModifiers, sessionId) {
|
|
21
|
+
const body = {
|
|
22
|
+
deviceUuids: args.deviceUuids,
|
|
23
|
+
locationUuids: args.locationUuids,
|
|
24
|
+
afterMs: args.afterMs,
|
|
25
|
+
beforeMs: args.beforeMs,
|
|
26
|
+
cardholderQuery: args.cardholderQuery,
|
|
27
|
+
badgeStatus: args.badgeStatus,
|
|
28
|
+
badgeType: args.badgeType,
|
|
29
|
+
area: args.area,
|
|
30
|
+
anomalyOnly: args.anomalyOnly,
|
|
31
|
+
entryMade: args.entryMade,
|
|
32
|
+
limit: args.limit ?? 200,
|
|
33
|
+
activityTypes: [...NETBOX_ACTIVITY_TYPES],
|
|
34
|
+
};
|
|
35
|
+
const res = await postApi({
|
|
36
|
+
route: "/eventSearchV2/searchIntegrationAccessEvents",
|
|
37
|
+
body,
|
|
38
|
+
modifiers: requestModifiers,
|
|
39
|
+
sessionId,
|
|
40
|
+
});
|
|
41
|
+
if (res.error) {
|
|
42
|
+
throw new Error(res.status ?? res.errorMsg ?? "NetBox event search failed");
|
|
43
|
+
}
|
|
44
|
+
const events = (res.events ?? []).map((e) => ({
|
|
45
|
+
timestampMs: e.timestampMs ?? undefined,
|
|
46
|
+
datetime: e.timestampMs != null ? formatTimestamp(e.timestampMs, timeZone) : undefined,
|
|
47
|
+
deviceUuid: e.deviceUuid ?? undefined,
|
|
48
|
+
label: e.customDisplayName ?? e.objectType ?? undefined,
|
|
49
|
+
// Dedicated-event-types put the cardholder in its own `cardholderName` field (customDescription
|
|
50
|
+
// is null on those docs); keep customDescription as a legacy fallback. The generated schema
|
|
51
|
+
// predates the field, so read it through a cast until `assets/openapi.json` is regenerated.
|
|
52
|
+
cardholderName: e.cardholderName ?? e.customDescription ?? undefined,
|
|
53
|
+
badgeStatus: e.badgeStatus ?? undefined,
|
|
54
|
+
badgeType: e.badgeType ?? undefined,
|
|
55
|
+
areaEntering: e.areaEntering ?? undefined,
|
|
56
|
+
areaExiting: e.areaExiting ?? undefined,
|
|
57
|
+
entryMade: e.entryMade ?? undefined,
|
|
58
|
+
isAnomaly: e.alert ?? undefined,
|
|
59
|
+
}));
|
|
60
|
+
return { events };
|
|
61
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { postApi } from "../network/network.js";
|
|
2
|
+
import { ActivityEnum } from "../types/schema.js";
|
|
2
3
|
import { formatTimestamp } from "../util.js";
|
|
3
4
|
/**
|
|
4
5
|
* Calls the unified integration access-event search (POST /eventSearchV2/searchIntegrationAccessEvents),
|
|
@@ -10,14 +11,13 @@ import { formatTimestamp } from "../util.js";
|
|
|
10
11
|
* integration access-event search scopes results to OnGuard events only.
|
|
11
12
|
*/
|
|
12
13
|
export const ONGUARD_ACTIVITY_TYPES = [
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
ActivityEnum.ONGUARD_BADGE_AUTHORIZED,
|
|
15
|
+
ActivityEnum.ONGUARD_BADGE_ANOMALY,
|
|
16
|
+
ActivityEnum.ONGUARD_NO_ENTRY_MADE,
|
|
16
17
|
];
|
|
17
18
|
export async function searchOnGuardEvents(args, timeZone, requestModifiers, sessionId) {
|
|
18
|
-
// Unified third-party integration access-event search, scoped to OnGuard via `activityTypes`.
|
|
19
|
-
//
|
|
20
|
-
// a cast off the OnGuard request DTO until `assets/openapi.json` is regenerated.
|
|
19
|
+
// Unified third-party integration access-event search, scoped to OnGuard via `activityTypes`. Typed
|
|
20
|
+
// against the generated public OpenAPI schema (Eventsearch_SearchIntegrationAccessEventsWSRequest/Response).
|
|
21
21
|
const body = {
|
|
22
22
|
deviceUuids: args.deviceUuids,
|
|
23
23
|
locationUuids: args.locationUuids,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { getNetboxAccessAnomalies } from "../api/netbox-access-anomaly-tool-api.js";
|
|
2
|
+
import { OUTPUT_SCHEMA, TOOL_ARGS } from "../types/netbox-access-anomaly-tool-types.js";
|
|
3
|
+
import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
|
|
4
|
+
const TOOL_NAME = "netbox-access-anomaly-tool";
|
|
5
|
+
const TOOL_DESCRIPTION = `
|
|
6
|
+
Scans Lenel S2 NetBox (Honeywell NetBox) access events over a time window and flags anomalous badge activity. Use for
|
|
7
|
+
"find unusual badge activity", "anything suspicious in access this week", or proactive access review.
|
|
8
|
+
|
|
9
|
+
Runs deterministic rules and returns ranked findings (high severity first):
|
|
10
|
+
- lost_or_inactive_badge: a non-active badge (lost/suspended) was used
|
|
11
|
+
- entry_not_made: access granted but no entry made (possible tailgating)
|
|
12
|
+
- off_hours: entry outside business hours (configurable)
|
|
13
|
+
- impossible_travel: one cardholder at two different areas seconds apart
|
|
14
|
+
- area_novelty: a cardholder's first-ever entry to an area vs their prior history (needs a baseline window)
|
|
15
|
+
|
|
16
|
+
Each finding includes cardholderName, the rule, severity, datetime, area, the camera deviceUuid, a plain-language
|
|
17
|
+
rationale, and clip/still hints. Resolve relative times (e.g. "this week") to ISO 8601 first via the timestamp tool.
|
|
18
|
+
|
|
19
|
+
This is a triage aid: present findings grouped by severity, and for the notable ones call the camera-tool
|
|
20
|
+
(requestType "image", cameraUuid = finding.deviceUuid, timestamp = finding.timestampMs) and/or clips-tool
|
|
21
|
+
("createClip" using finding.clipHint) — in PARALLEL — so a human can confirm. Don't assert wrongdoing; surface the
|
|
22
|
+
evidence.
|
|
23
|
+
`;
|
|
24
|
+
const TOOL_HANDLER = async (args, _extra) => {
|
|
25
|
+
const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
|
|
26
|
+
try {
|
|
27
|
+
const result = await getNetboxAccessAnomalies({
|
|
28
|
+
area: args.area ?? undefined,
|
|
29
|
+
locationUuids: args.locationUuids ?? undefined,
|
|
30
|
+
deviceUuids: args.deviceUuids ?? undefined,
|
|
31
|
+
afterMs: args.startTime ? new Date(args.startTime).getTime() : undefined,
|
|
32
|
+
beforeMs: args.endTime ? new Date(args.endTime).getTime() : undefined,
|
|
33
|
+
rules: args.rules ?? undefined,
|
|
34
|
+
baselineDays: args.baselineDays ?? undefined,
|
|
35
|
+
offHoursStartHour: args.offHoursStartHour ?? undefined,
|
|
36
|
+
offHoursEndHour: args.offHoursEndHour ?? undefined,
|
|
37
|
+
impossibleTravelMaxSeconds: args.impossibleTravelMaxSeconds ?? undefined,
|
|
38
|
+
limit: args.limit ?? undefined,
|
|
39
|
+
}, args.timeZone ?? "UTC", requestModifiers, sessionId);
|
|
40
|
+
return createToolStructuredContent(result);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
44
|
+
return createToolStructuredContent({ error: message });
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
export function createTool(server) {
|
|
48
|
+
server.registerTool(TOOL_NAME, {
|
|
49
|
+
description: TOOL_DESCRIPTION,
|
|
50
|
+
inputSchema: TOOL_ARGS,
|
|
51
|
+
outputSchema: OUTPUT_SCHEMA.shape,
|
|
52
|
+
}, TOOL_HANDLER);
|
|
53
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getNetboxBadgeTimeline } from "../api/netbox-badge-timeline-tool-api.js";
|
|
2
|
+
import { OUTPUT_SCHEMA, TOOL_ARGS } from "../types/netbox-badge-timeline-tool-types.js";
|
|
3
|
+
import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
|
|
4
|
+
const TOOL_NAME = "netbox-badge-timeline-tool";
|
|
5
|
+
const TOOL_DESCRIPTION = `
|
|
6
|
+
Reconstructs one person's movements through a building from their Lenel S2 NetBox (Honeywell NetBox) badge taps.
|
|
7
|
+
Use this for incident reconstruction / "follow the badge" requests, e.g. "reconstruct Eve's movements
|
|
8
|
+
yesterday" or "where did this cardholder go".
|
|
9
|
+
|
|
10
|
+
Returns the cardholder's badge taps in CHRONOLOGICAL order (oldest first), each with:
|
|
11
|
+
- datetime / timestampMs and the area entered
|
|
12
|
+
- deviceUuid: the camera at that door
|
|
13
|
+
- clipHint (camera + start/end window) and stillHint (camera + timestamp)
|
|
14
|
+
- gapToNextSeconds: time until the next tap (a large gap = unobserved movement between doors)
|
|
15
|
+
plus a "path" array summarizing the areas traversed in order.
|
|
16
|
+
|
|
17
|
+
Resolve relative times like "yesterday" to ISO 8601 first (use the timestamp tool), then pass
|
|
18
|
+
startTime/endTime. cardholderQuery is a full-text name match; if "ambiguousCardholders" is returned the
|
|
19
|
+
query matched more than one person — ask the user which one before trusting the timeline.
|
|
20
|
+
|
|
21
|
+
IMPORTANT — to show the movement visually: for each stop (or the key transitions), call the camera-tool
|
|
22
|
+
(requestType "image", cameraUuid = stop.deviceUuid, timestamp = stop.timestampMs) for a still you can see,
|
|
23
|
+
and/or the clips-tool (requestType "createClip", using stop.clipHint) for video. Issue those per-stop
|
|
24
|
+
media calls in PARALLEL, then present the timeline as a chronological narrative.
|
|
25
|
+
`;
|
|
26
|
+
const TOOL_HANDLER = async (args, _extra) => {
|
|
27
|
+
const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
|
|
28
|
+
try {
|
|
29
|
+
const result = await getNetboxBadgeTimeline({
|
|
30
|
+
cardholderQuery: args.cardholderQuery,
|
|
31
|
+
locationUuids: args.locationUuids ?? undefined,
|
|
32
|
+
afterMs: args.startTime ? new Date(args.startTime).getTime() : undefined,
|
|
33
|
+
beforeMs: args.endTime ? new Date(args.endTime).getTime() : undefined,
|
|
34
|
+
clipPaddingSeconds: args.clipPaddingSeconds ?? undefined,
|
|
35
|
+
limit: args.limit ?? undefined,
|
|
36
|
+
}, args.timeZone ?? "UTC", requestModifiers, sessionId);
|
|
37
|
+
return createToolStructuredContent(result);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
41
|
+
return createToolStructuredContent({ error: message });
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
export function createTool(server) {
|
|
45
|
+
server.registerTool(TOOL_NAME, {
|
|
46
|
+
description: TOOL_DESCRIPTION,
|
|
47
|
+
inputSchema: TOOL_ARGS,
|
|
48
|
+
outputSchema: OUTPUT_SCHEMA.shape,
|
|
49
|
+
}, TOOL_HANDLER);
|
|
50
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { getNetboxLostBadgeResponse } from "../api/netbox-lost-badge-tool-api.js";
|
|
2
|
+
import { OUTPUT_SCHEMA, TOOL_ARGS } from "../types/netbox-lost-badge-tool-types.js";
|
|
3
|
+
import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
|
|
4
|
+
const TOOL_NAME = "netbox-lost-badge-tool";
|
|
5
|
+
const TOOL_DESCRIPTION = `
|
|
6
|
+
Lost / stolen-badge live response for Lenel S2 NetBox (Honeywell NetBox) access. Use for "a lost badge was just used —
|
|
7
|
+
who is it and where did they go?", or to review lost/inactive-badge use over a window.
|
|
8
|
+
|
|
9
|
+
For each lost/inactive-badge use it returns:
|
|
10
|
+
- cardholderOfRecord (who the badge belongs to — may NOT be who used it) and badgeStatus
|
|
11
|
+
- the door deviceUuid + time, plus clipHint / stillHint for the door footage
|
|
12
|
+
- facesAtDoor: the face(s) captured at the door at that moment — a recognized name, or UNIDENTIFIED (an unknown
|
|
13
|
+
face on a valid badge is the strongest stolen/shared-badge signal), with a thumbnail
|
|
14
|
+
- sightings: that same face tracked across cameras, ordered in time, ending in lastKnownSighting (last-known location)
|
|
15
|
+
|
|
16
|
+
Resolve relative times (e.g. "in the last hour") to ISO 8601 first via the timestamp tool.
|
|
17
|
+
|
|
18
|
+
To present: show the door still/clip (camera-tool image / clips-tool createClip with the hints), the face at the
|
|
19
|
+
door, and the cross-camera track to last-known location. Detection + door evidence are reliable; the face track
|
|
20
|
+
depends on face-recognition coverage, so treat it as investigative, not proof.
|
|
21
|
+
`;
|
|
22
|
+
const TOOL_HANDLER = async (args, _extra) => {
|
|
23
|
+
const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
|
|
24
|
+
try {
|
|
25
|
+
const result = await getNetboxLostBadgeResponse({
|
|
26
|
+
area: args.area ?? undefined,
|
|
27
|
+
locationUuids: args.locationUuids ?? undefined,
|
|
28
|
+
deviceUuids: args.deviceUuids ?? undefined,
|
|
29
|
+
afterMs: args.startTime ? new Date(args.startTime).getTime() : undefined,
|
|
30
|
+
beforeMs: args.endTime ? new Date(args.endTime).getTime() : undefined,
|
|
31
|
+
faceWindowSeconds: args.faceWindowSeconds ?? undefined,
|
|
32
|
+
limit: args.limit ?? undefined,
|
|
33
|
+
}, args.timeZone ?? "UTC", requestModifiers, sessionId);
|
|
34
|
+
return createToolStructuredContent(result);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
38
|
+
return createToolStructuredContent({ error: message });
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
export function createTool(server) {
|
|
42
|
+
server.registerTool(TOOL_NAME, {
|
|
43
|
+
description: TOOL_DESCRIPTION,
|
|
44
|
+
inputSchema: TOOL_ARGS,
|
|
45
|
+
outputSchema: OUTPUT_SCHEMA.shape,
|
|
46
|
+
}, TOOL_HANDLER);
|
|
47
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { searchNetboxEvents } from "../api/netbox-tool-api.js";
|
|
2
|
+
import { OUTPUT_SCHEMA, TOOL_ARGS } from "../types/netbox-tool-types.js";
|
|
3
|
+
import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
|
|
4
|
+
const TOOL_NAME = "netbox-events-tool";
|
|
5
|
+
const TOOL_DESCRIPTION = `
|
|
6
|
+
Searches Lenel S2 NetBox (Honeywell NetBox) badge / access-control events for the organization. Use this to answer
|
|
7
|
+
"who entered WHERE and WHEN" questions, e.g. "who entered the back office yesterday".
|
|
8
|
+
|
|
9
|
+
Each returned event includes:
|
|
10
|
+
- cardholderName: the person's name
|
|
11
|
+
- deviceUuid: the camera that saw the event
|
|
12
|
+
- timestampMs / datetime: when it happened
|
|
13
|
+
- label: e.g. "NetBox: Badge Authorized" (a grant) or an anomaly label
|
|
14
|
+
- badgeStatus, badgeType, areaEntering, areaExiting, entryMade, isAnomaly
|
|
15
|
+
|
|
16
|
+
Filters (all optional): area, locationUuids, deviceUuids, cardholderQuery, badgeStatus, badgeType,
|
|
17
|
+
anomalyOnly, entryMade, startTime, endTime, limit. Resolve relative times like "yesterday" to ISO 8601
|
|
18
|
+
first (use the timestamp tool), then pass startTime/endTime.
|
|
19
|
+
|
|
20
|
+
IMPORTANT — to show pictures and video of each person so the user can visually identify them: after this
|
|
21
|
+
returns, for each event (or the most relevant ones) call the camera-tool (requestType "image",
|
|
22
|
+
cameraUuid = the event's deviceUuid, timestamp = the event's time) to get a still you can see, and/or the
|
|
23
|
+
clips-tool (requestType "createClip") with a short window around the timestamp for video. Issue those
|
|
24
|
+
per-event media calls in PARALLEL.
|
|
25
|
+
`;
|
|
26
|
+
const TOOL_HANDLER = async (args, _extra) => {
|
|
27
|
+
const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
|
|
28
|
+
try {
|
|
29
|
+
const result = await searchNetboxEvents({
|
|
30
|
+
area: args.area ?? undefined,
|
|
31
|
+
locationUuids: args.locationUuids ?? undefined,
|
|
32
|
+
deviceUuids: args.deviceUuids ?? undefined,
|
|
33
|
+
cardholderQuery: args.cardholderQuery ?? undefined,
|
|
34
|
+
badgeStatus: args.badgeStatus ?? undefined,
|
|
35
|
+
badgeType: args.badgeType ?? undefined,
|
|
36
|
+
anomalyOnly: args.anomalyOnly ?? undefined,
|
|
37
|
+
entryMade: args.entryMade ?? undefined,
|
|
38
|
+
afterMs: args.startTime ? new Date(args.startTime).getTime() : undefined,
|
|
39
|
+
beforeMs: args.endTime ? new Date(args.endTime).getTime() : undefined,
|
|
40
|
+
limit: args.limit ?? undefined,
|
|
41
|
+
}, args.timeZone ?? "UTC", requestModifiers, sessionId);
|
|
42
|
+
return createToolStructuredContent(result);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
46
|
+
return createToolStructuredContent({ error: message });
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
export function createTool(server) {
|
|
50
|
+
server.registerTool(TOOL_NAME, {
|
|
51
|
+
description: TOOL_DESCRIPTION,
|
|
52
|
+
inputSchema: TOOL_ARGS,
|
|
53
|
+
outputSchema: OUTPUT_SCHEMA.shape,
|
|
54
|
+
}, TOOL_HANDLER);
|
|
55
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Identical arg/output shapes to the OnGuard access-anomaly tool (same generalized backend), so reuse
|
|
2
|
+
// the OnGuard Zod schemas verbatim. Re-exported to keep the netbox-* module naming convention parallel.
|
|
3
|
+
export { ANOMALY_RULES, TOOL_ARGS, AnomalyFindingSchema, OUTPUT_SCHEMA, } from "./access-anomaly-tool-types.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Identical arg/output shapes to the OnGuard badge-timeline tool (same generalized backend), so reuse
|
|
2
|
+
// the OnGuard Zod schemas verbatim. Re-exported to keep the netbox-* module naming convention parallel.
|
|
3
|
+
export { TOOL_ARGS, BadgeTimelineStopSchema, OUTPUT_SCHEMA, } from "./badge-timeline-tool-types.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Identical arg/output shapes to the OnGuard lost-badge tool (same generalized backend), so reuse the
|
|
2
|
+
// OnGuard Zod schemas verbatim. Re-exported to keep the netbox-* module naming convention parallel.
|
|
3
|
+
export { TOOL_ARGS, LostBadgeIncidentSchema, OUTPUT_SCHEMA, } from "./lost-badge-tool-types.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// The Lenel S2 NetBox event search exposes the exact same filters and result shape as OnGuard
|
|
2
|
+
// (the backend uses one generalized DTO), so reuse the OnGuard Zod schemas verbatim rather than
|
|
3
|
+
// duplicating them. Re-exported here to keep the netbox-* module naming convention parallel.
|
|
4
|
+
export { TOOL_ARGS, OnGuardEventSchema as NetboxEventSchema, OUTPUT_SCHEMA, } from "./onguard-tool-types.js";
|
package/dist/types/schema.js
CHANGED
|
@@ -287,6 +287,15 @@ export var ActivityEnum;
|
|
|
287
287
|
ActivityEnum["ALM_THREAT_DETECTED"] = "ALM_THREAT_DETECTED";
|
|
288
288
|
ActivityEnum["ALM_MONITORING_EVENT_DETECTED"] = "ALM_MONITORING_EVENT_DETECTED";
|
|
289
289
|
ActivityEnum["ROBOT_DOG_DETECTED"] = "ROBOT_DOG_DETECTED";
|
|
290
|
+
ActivityEnum["ONGUARD_BADGE_AUTHORIZED"] = "ONGUARD_BADGE_AUTHORIZED";
|
|
291
|
+
ActivityEnum["ONGUARD_BADGE_ANOMALY"] = "ONGUARD_BADGE_ANOMALY";
|
|
292
|
+
ActivityEnum["ONGUARD_NO_ENTRY_MADE"] = "ONGUARD_NO_ENTRY_MADE";
|
|
293
|
+
ActivityEnum["ELEMENTS_BADGE_AUTHORIZED"] = "ELEMENTS_BADGE_AUTHORIZED";
|
|
294
|
+
ActivityEnum["ELEMENTS_BADGE_ANOMALY"] = "ELEMENTS_BADGE_ANOMALY";
|
|
295
|
+
ActivityEnum["ELEMENTS_NO_ENTRY_MADE"] = "ELEMENTS_NO_ENTRY_MADE";
|
|
296
|
+
ActivityEnum["NETBOX_BADGE_AUTHORIZED"] = "NETBOX_BADGE_AUTHORIZED";
|
|
297
|
+
ActivityEnum["NETBOX_BADGE_ANOMALY"] = "NETBOX_BADGE_ANOMALY";
|
|
298
|
+
ActivityEnum["NETBOX_NO_ENTRY_MADE"] = "NETBOX_NO_ENTRY_MADE";
|
|
290
299
|
ActivityEnum["UNKNOWN"] = "UNKNOWN";
|
|
291
300
|
})(ActivityEnum || (ActivityEnum = {}));
|
|
292
301
|
export var AddOnLicenseEnum;
|
|
@@ -10843,6 +10843,26 @@ const Eventsearch_SearchOnGuardEventsWSResponse = z.object({
|
|
|
10843
10843
|
events: z.array(SeekpointIndexType).optional(),
|
|
10844
10844
|
warningMsg: z.string().optional()
|
|
10845
10845
|
});
|
|
10846
|
+
const Eventsearch_SearchIntegrationAccessEventsWSRequest = z.object({
|
|
10847
|
+
activityTypes: z.array(ActivityEnum).optional(),
|
|
10848
|
+
afterMs: z.number().int().optional(),
|
|
10849
|
+
anomalyOnly: z.boolean().optional(),
|
|
10850
|
+
area: z.string().optional(),
|
|
10851
|
+
badgeStatus: z.string().optional(),
|
|
10852
|
+
badgeType: z.string().optional(),
|
|
10853
|
+
beforeMs: z.number().int().optional(),
|
|
10854
|
+
cardholderQuery: z.string().optional(),
|
|
10855
|
+
deviceUuids: z.array(z.string()).optional(),
|
|
10856
|
+
entryMade: z.boolean().optional(),
|
|
10857
|
+
limit: z.number().int().optional(),
|
|
10858
|
+
locationUuids: z.array(z.string()).optional()
|
|
10859
|
+
});
|
|
10860
|
+
const Eventsearch_SearchIntegrationAccessEventsWSResponse = z.object({
|
|
10861
|
+
error: z.boolean().optional(),
|
|
10862
|
+
errorMsg: z.string().optional(),
|
|
10863
|
+
events: z.array(SeekpointIndexType).optional(),
|
|
10864
|
+
warningMsg: z.string().optional()
|
|
10865
|
+
});
|
|
10846
10866
|
const Export_ExportAuditEventsWSRequest = z.object({
|
|
10847
10867
|
endInterval: z.number().int().optional(),
|
|
10848
10868
|
excludeActions: z.array(z.string()).optional(),
|
|
@@ -22012,6 +22032,8 @@ export const schemas = {
|
|
|
22012
22032
|
Eventsearch_GetEventSeekpointsWSResponse,
|
|
22013
22033
|
Eventsearch_SearchOnGuardEventsWSRequest,
|
|
22014
22034
|
Eventsearch_SearchOnGuardEventsWSResponse,
|
|
22035
|
+
Eventsearch_SearchIntegrationAccessEventsWSRequest,
|
|
22036
|
+
Eventsearch_SearchIntegrationAccessEventsWSResponse,
|
|
22015
22037
|
Export_ExportAuditEventsWSRequest,
|
|
22016
22038
|
Export_ExportClimateEventsWSRequest,
|
|
22017
22039
|
Export_ExportCountReportsWSRequest,
|