@oneuptime/common 12.0.27 → 12.0.29
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/Models/AnalyticsModels/RumSessionChunk.ts +52 -0
- package/Models/DatabaseModels/CloudResource.ts +17 -1
- package/Models/DatabaseModels/DashboardDomain.ts +43 -0
- package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
- package/Models/DatabaseModels/RumApplication.ts +35 -2
- package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
- package/Server/API/DashboardDomainAPI.ts +74 -0
- package/Server/API/MicrosoftTeamsAPI.ts +81 -180
- package/Server/API/StatusPageDomainAPI.ts +74 -0
- package/Server/API/TelemetryAPI.ts +104 -15
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/ProjectAuthorization.ts +6 -1
- package/Server/Services/DashboardDomainService.ts +131 -0
- package/Server/Services/LogPipelineProcessorService.ts +88 -0
- package/Server/Services/MonitorTemplateService.ts +70 -11
- package/Server/Services/StatusPageDomainService.ts +163 -3
- package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
- package/Server/Types/Domain.ts +17 -0
- package/Server/Utils/APIKey/AccessPermission.ts +24 -0
- package/Server/Utils/Greenlock/Greenlock.ts +156 -58
- package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
- package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +12 -5
- package/Server/Utils/SessionReplay/SessionReplayIdentity.ts +102 -0
- package/Server/Utils/SessionReplay/SessionReplayReadService.ts +11 -2
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
- package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
- package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +81 -0
- package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
- package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
- package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
- package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
- package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
- package/Tests/Server/API/SessionReplayAPI.test.ts +232 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
- package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
- package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
- package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
- package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
- package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
- package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
- package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
- package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
- package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
- package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
- package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
- package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
- package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
- package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
- package/Tests/Types/API/HTTPResponse.test.ts +175 -0
- package/Tests/Types/API/Route.test.ts +36 -4
- package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
- package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
- package/Tests/Types/DateLocalTimeString.test.ts +259 -0
- package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
- package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
- package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
- package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
- package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
- package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
- package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
- package/Tests/UI/Utils/Navigation.test.ts +82 -0
- package/Tests/Utils/Array.test.ts +100 -0
- package/Tests/Utils/CertificateReissue.test.ts +225 -0
- package/Tests/Utils/Grok/Grok.test.ts +492 -0
- package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
- package/Types/API/Route.ts +20 -5
- package/Types/Date.ts +143 -13
- package/Types/Rum/SessionReplay.ts +76 -1
- package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
- package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
- package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
- package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
- package/UI/Utils/Navigation.ts +42 -0
- package/Utils/Array.ts +37 -0
- package/Utils/CertificateReissue.ts +128 -0
- package/Utils/Grok/Grok.ts +461 -0
- package/Utils/Grok/GrokPatterns.ts +118 -0
- package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +48 -0
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -1
- package/build/dist/Models/DatabaseModels/CloudResource.js +19 -2
- package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
- package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +37 -3
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
- package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
- package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
- package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +67 -14
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/DashboardDomainService.js +104 -0
- package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
- package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
- package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
- package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
- package/build/dist/Server/Types/Domain.js +16 -0
- package/build/dist/Server/Types/Domain.js.map +1 -1
- package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
- package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
- package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
- package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
- package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
- package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js +59 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +9 -2
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/API/Route.js +16 -5
- package/build/dist/Types/API/Route.js.map +1 -1
- package/build/dist/Types/Date.js +115 -13
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplay.js +38 -0
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
- package/build/dist/UI/Utils/Navigation.js +30 -0
- package/build/dist/UI/Utils/Navigation.js.map +1 -1
- package/build/dist/Utils/Array.js +25 -0
- package/build/dist/Utils/Array.js.map +1 -1
- package/build/dist/Utils/CertificateReissue.js +97 -0
- package/build/dist/Utils/CertificateReissue.js.map +1 -0
- package/build/dist/Utils/Grok/Grok.js +324 -0
- package/build/dist/Utils/Grok/Grok.js.map +1 -0
- package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
- package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
- package/package.json +1 -1
- package/UI/Utils/JsonWebToken.ts +0 -14
- package/build/dist/UI/Utils/JsonWebToken.js +0 -10
- package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
|
@@ -16,7 +16,7 @@ import CaptureSpan from "../../Telemetry/CaptureSpan";
|
|
|
16
16
|
import ConnectorErrorMessage from "../ConnectorErrorMessage";
|
|
17
17
|
import { buildSecurityEventDbRow } from "../SecurityEventRow";
|
|
18
18
|
import ThreatIntelEnricher from "../ThreatIntel/ThreatIntelEnricher";
|
|
19
|
-
import GoogleSecOpsClient from "./GoogleSecOpsClient";
|
|
19
|
+
import GoogleSecOpsClient, { FetchAlertsResult } from "./GoogleSecOpsClient";
|
|
20
20
|
|
|
21
21
|
const SECOPS_SERVICE_NAME: string = "Google SecOps";
|
|
22
22
|
|
|
@@ -31,6 +31,10 @@ const MAX_LOOKBACK_IN_MINUTES: number = 24 * 60;
|
|
|
31
31
|
/*
|
|
32
32
|
* Overlap between windows so boundary alerts are never missed; the
|
|
33
33
|
* eventUid content hash keeps redelivered alerts deduplicatable.
|
|
34
|
+
*
|
|
35
|
+
* Still open (out of scope here): the overlap does produce genuine
|
|
36
|
+
* duplicate rows, because SecurityEvent is a plain MergeTree with
|
|
37
|
+
* eventUid outside the sort key, so nothing collapses them at write time.
|
|
34
38
|
*/
|
|
35
39
|
const WINDOW_OVERLAP_IN_MINUTES: number = 1;
|
|
36
40
|
|
|
@@ -80,7 +84,18 @@ export default class GoogleSecOpsPoller {
|
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
try {
|
|
83
|
-
await this.pollConnection(connection);
|
|
87
|
+
const ingested: number = await this.pollConnection(connection);
|
|
88
|
+
|
|
89
|
+
/*
|
|
90
|
+
* The count used to be returned and dropped on the floor, which
|
|
91
|
+
* left "polling healthy but quiet" and "polling silently broken"
|
|
92
|
+
* looking identical from outside the process. At info rather than
|
|
93
|
+
* debug for that reason — a number nobody sees at the default log
|
|
94
|
+
* level answers the question no better than not having it.
|
|
95
|
+
*/
|
|
96
|
+
logger.info(
|
|
97
|
+
`GoogleSecOpsPoller: connection ${connection.id?.toString()} ingested ${ingested} security events.`,
|
|
98
|
+
);
|
|
84
99
|
} catch (error) {
|
|
85
100
|
logger.error(
|
|
86
101
|
`GoogleSecOpsPoller: error polling connection ${connection.id?.toString()}:`,
|
|
@@ -134,6 +149,8 @@ export default class GoogleSecOpsPoller {
|
|
|
134
149
|
);
|
|
135
150
|
}
|
|
136
151
|
|
|
152
|
+
const connectionId: string = connection.id.toString();
|
|
153
|
+
|
|
137
154
|
const client: GoogleSecOpsClient =
|
|
138
155
|
clientOverride ||
|
|
139
156
|
new GoogleSecOpsClient({
|
|
@@ -149,26 +166,96 @@ export default class GoogleSecOpsPoller {
|
|
|
149
166
|
-MAX_LOOKBACK_IN_MINUTES,
|
|
150
167
|
);
|
|
151
168
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
169
|
+
const defaultStartTime: Date = OneUptimeDate.addRemoveMinutes(
|
|
170
|
+
endTime,
|
|
171
|
+
-DEFAULT_LOOKBACK_IN_MINUTES,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
let startTime: Date = defaultStartTime;
|
|
175
|
+
|
|
176
|
+
if (connection.cursor) {
|
|
177
|
+
const cursorTime: Date = new Date(connection.cursor);
|
|
178
|
+
|
|
179
|
+
if (Number.isNaN(cursorTime.getTime())) {
|
|
180
|
+
/*
|
|
181
|
+
* An unreadable cursor used to share a branch with a stale one and
|
|
182
|
+
* open the full 24 hour window — the widest possible blast radius
|
|
183
|
+
* for the least trustworthy input. It means "no usable cursor",
|
|
184
|
+
* which is what a first poll means too.
|
|
185
|
+
*/
|
|
186
|
+
logger.warn(
|
|
187
|
+
`GoogleSecOpsPoller: connection ${connectionId} has an unreadable cursor ${JSON.stringify(
|
|
188
|
+
connection.cursor,
|
|
189
|
+
)}; polling the default ${DEFAULT_LOOKBACK_IN_MINUTES} minute window instead.`,
|
|
190
|
+
);
|
|
191
|
+
} else {
|
|
192
|
+
startTime = OneUptimeDate.addRemoveMinutes(
|
|
193
|
+
cursorTime,
|
|
155
194
|
-WINDOW_OVERLAP_IN_MINUTES,
|
|
156
|
-
)
|
|
157
|
-
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (OneUptimeDate.isBefore(startTime, earliestAllowed)) {
|
|
200
|
+
/*
|
|
201
|
+
* The alerts between the cursor and the cap are skipped and will
|
|
202
|
+
* never be fetched again, so the gap has to be stated. Silently
|
|
203
|
+
* truncating it is how a connector comes back from an outage
|
|
204
|
+
* looking healthy while a day of detections is simply missing.
|
|
205
|
+
*/
|
|
206
|
+
logger.warn(
|
|
207
|
+
`GoogleSecOpsPoller: connection ${connectionId} would poll from ${startTime.toISOString()}, ${OneUptimeDate.getMinutesBetweenTwoDates(
|
|
208
|
+
startTime,
|
|
209
|
+
endTime,
|
|
210
|
+
)} minutes back and past the ${MAX_LOOKBACK_IN_MINUTES} minute cap; alerts before ${earliestAllowed.toISOString()} are skipped and will not be fetched again.`,
|
|
211
|
+
);
|
|
158
212
|
|
|
159
|
-
if (
|
|
160
|
-
Number.isNaN(startTime.getTime()) ||
|
|
161
|
-
OneUptimeDate.isBefore(startTime, earliestAllowed)
|
|
162
|
-
) {
|
|
163
213
|
startTime = earliestAllowed;
|
|
164
214
|
}
|
|
165
215
|
|
|
166
|
-
|
|
216
|
+
if (!OneUptimeDate.isBefore(startTime, endTime)) {
|
|
217
|
+
/*
|
|
218
|
+
* A cursor in the future — clock skew on the writer, or a restored
|
|
219
|
+
* backup — used to send Chronicle an inverted timeRange, which is
|
|
220
|
+
* neither an error the operator sees nor a window that returns
|
|
221
|
+
* anything.
|
|
222
|
+
*/
|
|
223
|
+
logger.warn(
|
|
224
|
+
`GoogleSecOpsPoller: connection ${connectionId} cursor resolves to ${startTime.toISOString()}, at or after the current time; polling the default ${DEFAULT_LOOKBACK_IN_MINUTES} minute window instead.`,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
startTime = defaultStartTime;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const fetched: FetchAlertsResult = await client.fetchDetectionAlerts({
|
|
167
231
|
startTime,
|
|
168
232
|
endTime,
|
|
169
233
|
});
|
|
170
234
|
|
|
171
|
-
|
|
235
|
+
if (fetched.truncatedByCount) {
|
|
236
|
+
logger.warn(
|
|
237
|
+
`GoogleSecOpsPoller: connection ${connectionId} window ${startTime.toISOString()} to ${endTime.toISOString()} matched more alerts than Chronicle will return, and this endpoint has no pagination — alerts in this window were dropped. Lower pollIntervalInMinutes.`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (fetched.truncatedByBytes) {
|
|
242
|
+
logger.warn(
|
|
243
|
+
`GoogleSecOpsPoller: connection ${connectionId} window ${startTime.toISOString()} to ${endTime.toISOString()} was truncated by Chronicle's memory limit — alerts in this window were dropped. Lower pollIntervalInMinutes.`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const alerts: Array<JSONObject> = fetched.alerts;
|
|
248
|
+
|
|
249
|
+
const normalizedAlerts: Array<NormalizedSecurityEvent> = [];
|
|
250
|
+
|
|
251
|
+
/*
|
|
252
|
+
* Objects the alert gate refused, and objects that blew up on the way
|
|
253
|
+
* through it. They are kept apart because they mean opposite things
|
|
254
|
+
* for the cursor: a refusal is deterministic and re-fetching it would
|
|
255
|
+
* refuse it again forever, a failure may well succeed next time.
|
|
256
|
+
*/
|
|
257
|
+
let rejectedCount: number = 0;
|
|
258
|
+
let failedCount: number = 0;
|
|
172
259
|
|
|
173
260
|
if (alerts.length > 0) {
|
|
174
261
|
const serviceMetadata: TelemetryServiceMetadata =
|
|
@@ -185,17 +272,34 @@ export default class GoogleSecOpsPoller {
|
|
|
185
272
|
projectRetentionInDays: serviceMetadata.projectRetentionInDays,
|
|
186
273
|
});
|
|
187
274
|
|
|
188
|
-
const normalizedAlerts: Array<NormalizedSecurityEvent> = [];
|
|
189
|
-
|
|
190
275
|
for (const alert of alerts) {
|
|
191
276
|
try {
|
|
277
|
+
/*
|
|
278
|
+
* The normalizer is a total function whose class, category,
|
|
279
|
+
* vendor and product are constants, so anything handed to it
|
|
280
|
+
* comes back out as a plausible Detection Finding row. The gate
|
|
281
|
+
* is what keeps a stray envelope chunk from being stored as a
|
|
282
|
+
* severity-Unknown detection nobody can tell from a real one.
|
|
283
|
+
*/
|
|
284
|
+
if (!GoogleSecOpsAlertNormalizer.isGoogleSecOpsAlert(alert)) {
|
|
285
|
+
rejectedCount++;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
192
289
|
normalizedAlerts.push(GoogleSecOpsAlertNormalizer.normalize(alert));
|
|
193
290
|
} catch (normalizeError) {
|
|
291
|
+
failedCount++;
|
|
194
292
|
logger.error("GoogleSecOpsPoller: error normalizing alert");
|
|
195
293
|
logger.error(normalizeError);
|
|
196
294
|
}
|
|
197
295
|
}
|
|
198
296
|
|
|
297
|
+
if (rejectedCount > 0) {
|
|
298
|
+
logger.warn(
|
|
299
|
+
`GoogleSecOpsPoller: connection ${connectionId} discarded ${rejectedCount} of ${alerts.length} fetched objects that do not look like Google SecOps alerts.`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
199
303
|
/*
|
|
200
304
|
* Threat-intel enrichment before row building, same seam as the
|
|
201
305
|
* HTTP ingest path — polled alerts get the same threat.* stamps.
|
|
@@ -218,15 +322,43 @@ export default class GoogleSecOpsPoller {
|
|
|
218
322
|
|
|
219
323
|
if (rows.length > 0) {
|
|
220
324
|
await SecurityEventService.insertJsonRows(rows);
|
|
221
|
-
ingested = rows.length;
|
|
222
325
|
}
|
|
223
326
|
}
|
|
224
327
|
|
|
328
|
+
const ingested: number = normalizedAlerts.length;
|
|
329
|
+
|
|
330
|
+
/*
|
|
331
|
+
* Nothing storable came out of a window that did contain alerts, and
|
|
332
|
+
* the reason was a failure rather than a refusal — so the alerts still
|
|
333
|
+
* exist at Chronicle and the window has to be re-fetched. Advancing
|
|
334
|
+
* here is what turned a normalizer fault into permanent, silent data
|
|
335
|
+
* loss: the insert-failure path leaves the cursor alone by throwing,
|
|
336
|
+
* and this path did not.
|
|
337
|
+
*/
|
|
338
|
+
const advanceCursor: boolean = ingested > 0 || failedCount === 0;
|
|
339
|
+
|
|
340
|
+
if (!advanceCursor) {
|
|
341
|
+
logger.warn(
|
|
342
|
+
`GoogleSecOpsPoller: connection ${connectionId} normalized none of its ${alerts.length} fetched alerts; holding the cursor so the window is polled again rather than skipped.`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const cursorUpdate: { cursor?: string } = advanceCursor
|
|
347
|
+
? {
|
|
348
|
+
/*
|
|
349
|
+
* Still open (out of scope here): this stores the end of the
|
|
350
|
+
* window rather than the newest detection timestamp actually
|
|
351
|
+
* ingested, which is what the column documents itself as.
|
|
352
|
+
*/
|
|
353
|
+
cursor: endTime.toISOString(),
|
|
354
|
+
}
|
|
355
|
+
: {};
|
|
356
|
+
|
|
225
357
|
await GoogleSecOpsConnectionService.updateOneById({
|
|
226
358
|
id: connection.id,
|
|
227
359
|
data: {
|
|
228
360
|
lastPolledAt: endTime,
|
|
229
|
-
|
|
361
|
+
...cursorUpdate,
|
|
230
362
|
lastError: null as unknown as string,
|
|
231
363
|
},
|
|
232
364
|
props: {
|
|
@@ -234,6 +366,10 @@ export default class GoogleSecOpsPoller {
|
|
|
234
366
|
},
|
|
235
367
|
});
|
|
236
368
|
|
|
369
|
+
logger.debug(
|
|
370
|
+
`GoogleSecOpsPoller: connection ${connectionId} fetched ${alerts.length} alerts and ingested ${ingested} security events for ${startTime.toISOString()} to ${endTime.toISOString()}.`,
|
|
371
|
+
);
|
|
372
|
+
|
|
237
373
|
return ingested;
|
|
238
374
|
}
|
|
239
375
|
}
|
|
@@ -105,11 +105,18 @@ export interface SessionReplayGatePolicy {
|
|
|
105
105
|
isAppEnabled: boolean;
|
|
106
106
|
|
|
107
107
|
/*
|
|
108
|
-
* Empty means
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
108
|
+
* Empty means ANY ORIGIN, which is the shipped default (the column
|
|
109
|
+
* defaults to '[]') and what isOriginAllowed below actually implements -
|
|
110
|
+
* this doc used to claim the opposite ("empty means REFUSED"), in the one
|
|
111
|
+
* direction where being wrong matters, since a reader would conclude the
|
|
112
|
+
* feature was locked down out of the box.
|
|
113
|
+
*
|
|
114
|
+
* Filling it in is the only anti-forgery control available: a
|
|
115
|
+
* TelemetryIngestionKey has no expiry, no scope and no origin binding, and
|
|
116
|
+
* the docs tell customers to paste it into browser JavaScript, so anyone
|
|
117
|
+
* who scrapes the key can write recordings into the victim's project until
|
|
118
|
+
* this list names the customer's own domains. The installation-test panel
|
|
119
|
+
* flags an empty list as a warning for exactly that reason.
|
|
113
120
|
*/
|
|
114
121
|
allowedOrigins: Array<string>;
|
|
115
122
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import { EncryptionSecret } from "../../EnvironmentConfig";
|
|
3
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
4
|
+
import { SESSION_REPLAY_MAX_USER_REF_LENGTH } from "../../../Types/Rum/SessionReplay";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* "Whose session is this?"
|
|
8
|
+
*
|
|
9
|
+
* A page that has an end-user reference hands it to the recorder
|
|
10
|
+
* (data-oneuptime-user-ref, or the init global). When the application has
|
|
11
|
+
* identity capture switched on, the recorder puts that reference on the
|
|
12
|
+
* chunk envelope's meta, and this module turns it into the two columns the
|
|
13
|
+
* session header carries:
|
|
14
|
+
*
|
|
15
|
+
* identifiedUserKey HMAC(EncryptionSecret, "<projectId>:<userRef>")
|
|
16
|
+
* identifiedUserLabel the reference itself, behind its own column ACL
|
|
17
|
+
*
|
|
18
|
+
* The key is what makes a reference SEARCHABLE and ERASABLE without storing
|
|
19
|
+
* it in a lookup-friendly form: a support engineer filtering by user, and a
|
|
20
|
+
* right-to-erasure request naming one, both resolve to the same digest. The
|
|
21
|
+
* label is what makes a session READABLE - "jane@example.com" rather than a
|
|
22
|
+
* 64-character hash - and is why it sits behind a narrower ACL than the rest
|
|
23
|
+
* of the session metadata.
|
|
24
|
+
*
|
|
25
|
+
* Scoped by project and NOT by application, deliberately. A project-wide
|
|
26
|
+
* erasure request (ProcessSessionErasureRequests filters on projectId with
|
|
27
|
+
* the application clause optional) has to be able to reach every session for
|
|
28
|
+
* that person, including ones recorded by sibling applications. An
|
|
29
|
+
* application-scoped digest would make that request silently under-delete,
|
|
30
|
+
* which is the failure mode with legal consequences.
|
|
31
|
+
*
|
|
32
|
+
* Deliberately NOT reusing SessionReplayTargeting.buildTargetKey: that one
|
|
33
|
+
* is application-scoped and prefixed for a Redis keyspace. Both are HMACs of
|
|
34
|
+
* a user reference, and conflating them would tie an erasure lookup to the
|
|
35
|
+
* shape of a Redis key.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
export interface SessionReplayUserKeyInput {
|
|
39
|
+
projectId: ObjectID;
|
|
40
|
+
userRef: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default class SessionReplayIdentity {
|
|
44
|
+
/*
|
|
45
|
+
* Usable when non-empty and within the shared cap. The cap matters on both
|
|
46
|
+
* sides of the comparison: the recorder slices the reference to the same
|
|
47
|
+
* length before sending it, so anything longer could never match a stored
|
|
48
|
+
* key anyway, and hashing an unbounded string here would be the only
|
|
49
|
+
* unbounded work on this path.
|
|
50
|
+
*/
|
|
51
|
+
public static isUsableUserRef(userRef: unknown): userRef is string {
|
|
52
|
+
return (
|
|
53
|
+
typeof userRef === "string" &&
|
|
54
|
+
userRef.trim().length > 0 &&
|
|
55
|
+
userRef.length <= SESSION_REPLAY_MAX_USER_REF_LENGTH
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
* The project scope lives in the KEY, not in the message.
|
|
61
|
+
*
|
|
62
|
+
* HMAC takes arbitrary key material, so deriving a per-project key from
|
|
63
|
+
* the instance secret and the project id gives real domain separation:
|
|
64
|
+
* two projects cannot produce the same digest for the same person even
|
|
65
|
+
* if one of them learns the other's project id, which prefixing the
|
|
66
|
+
* message only achieves by convention. It is also what makes the
|
|
67
|
+
* "per-project" wording on the identity columns literally true.
|
|
68
|
+
*
|
|
69
|
+
* The reference is trimmed but NOT lower-cased. End-user references are
|
|
70
|
+
* opaque to us - "U-1000" and "u-1000" may well be two different
|
|
71
|
+
* customers in the host application's own database - so folding case here
|
|
72
|
+
* would merge two people's recordings under one key, and an erasure for
|
|
73
|
+
* one would delete the other's. Targeting lower-cases the APPLICATION
|
|
74
|
+
* identifier for the same reason in reverse: that one is ours and is
|
|
75
|
+
* case-insensitive.
|
|
76
|
+
*
|
|
77
|
+
* A slow KDF (bcrypt / scrypt / argon2) is deliberately NOT used and
|
|
78
|
+
* would not help. This is a keyed pseudonym, not a stored password: it
|
|
79
|
+
* has to be DETERMINISTIC so the session filter and a right-to-erasure
|
|
80
|
+
* request months later resolve the same digest, and those algorithms are
|
|
81
|
+
* salted per invocation. What defends the digest is the key - without
|
|
82
|
+
* EncryptionSecret an attacker holding the column cannot test candidate
|
|
83
|
+
* references at all, at any work factor.
|
|
84
|
+
*/
|
|
85
|
+
public static buildUserKey(data: SessionReplayUserKeyInput): string {
|
|
86
|
+
const projectKey: string = `${EncryptionSecret.toString()}:${data.projectId.toString()}`;
|
|
87
|
+
|
|
88
|
+
return crypto
|
|
89
|
+
.createHmac("sha256", projectKey)
|
|
90
|
+
.update(data.userRef.trim())
|
|
91
|
+
.digest("hex");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
* The label as stored. Trimmed and capped to the same length the recorder
|
|
96
|
+
* enforces, so the column can never hold something the wire could not have
|
|
97
|
+
* carried.
|
|
98
|
+
*/
|
|
99
|
+
public static buildUserLabel(userRef: string): string {
|
|
100
|
+
return userRef.trim().slice(0, SESSION_REPLAY_MAX_USER_REF_LENGTH);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -1341,13 +1341,22 @@ export default class SessionReplayReadService {
|
|
|
1341
1341
|
);
|
|
1342
1342
|
}
|
|
1343
1343
|
|
|
1344
|
-
if (filters.hasFrustration
|
|
1344
|
+
if (filters.hasFrustration !== undefined) {
|
|
1345
1345
|
/*
|
|
1346
1346
|
* Over the argMax aliases, like every HAVING predicate here — the
|
|
1347
1347
|
* raw columns would sum across ReplacingMergeTree versions.
|
|
1348
|
+
*
|
|
1349
|
+
* `!== undefined` rather than `=== true`, so `false` means "sessions
|
|
1350
|
+
* with NO frustration signals" instead of being silently dropped. The
|
|
1351
|
+
* route admits any boolean, and hasError / isFinalized beside it both
|
|
1352
|
+
* honour false, so accepting the value and ignoring it returned the
|
|
1353
|
+
* whole unfiltered list with a 200 and no indication why.
|
|
1348
1354
|
*/
|
|
1355
|
+
const total: string =
|
|
1356
|
+
"(aggRageClickCount + aggDeadClickCount + aggErrorClickCount + aggRefreshRageCount)";
|
|
1357
|
+
|
|
1349
1358
|
statement.append(
|
|
1350
|
-
|
|
1359
|
+
filters.hasFrustration ? ` AND ${total} > 0` : ` AND ${total} = 0`,
|
|
1351
1360
|
);
|
|
1352
1361
|
}
|
|
1353
1362
|
|
|
@@ -18,6 +18,8 @@ import AlertStateService from "../../../../Services/AlertStateService";
|
|
|
18
18
|
import UserNotificationEventType from "../../../../../Types/UserNotification/UserNotificationEventType";
|
|
19
19
|
import OnCallDutyPolicy from "../../../../../Models/DatabaseModels/OnCallDutyPolicy";
|
|
20
20
|
import AlertState from "../../../../../Models/DatabaseModels/AlertState";
|
|
21
|
+
import DatabaseCommonInteractionProps from "../../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
22
|
+
import MicrosoftTeamsActionAuthorization from "./Authorization";
|
|
21
23
|
|
|
22
24
|
export default class MicrosoftTeamsAlertActions {
|
|
23
25
|
@CaptureSpan()
|
|
@@ -255,6 +257,7 @@ export default class MicrosoftTeamsAlertActions {
|
|
|
255
257
|
value: JSONObject;
|
|
256
258
|
projectId: ObjectID;
|
|
257
259
|
oneUptimeUserId: ObjectID;
|
|
260
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
258
261
|
turnContext: TurnContext;
|
|
259
262
|
}): Promise<void> {
|
|
260
263
|
const {
|
|
@@ -263,6 +266,7 @@ export default class MicrosoftTeamsAlertActions {
|
|
|
263
266
|
value,
|
|
264
267
|
projectId,
|
|
265
268
|
oneUptimeUserId,
|
|
269
|
+
databaseProps,
|
|
266
270
|
turnContext,
|
|
267
271
|
} = data;
|
|
268
272
|
|
|
@@ -274,10 +278,15 @@ export default class MicrosoftTeamsAlertActions {
|
|
|
274
278
|
return;
|
|
275
279
|
}
|
|
276
280
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
+
const alertId: ObjectID = new ObjectID(actionValue);
|
|
282
|
+
|
|
283
|
+
await MicrosoftTeamsActionAuthorization.assertCanUpdateAlert({
|
|
284
|
+
alertId: alertId,
|
|
285
|
+
projectId: projectId,
|
|
286
|
+
props: databaseProps,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
await AlertService.acknowledgeAlert(alertId, oneUptimeUserId);
|
|
281
290
|
await turnContext.sendActivity("✅ Alert acknowledged.");
|
|
282
291
|
return;
|
|
283
292
|
}
|
|
@@ -288,10 +297,15 @@ export default class MicrosoftTeamsAlertActions {
|
|
|
288
297
|
return;
|
|
289
298
|
}
|
|
290
299
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
300
|
+
const alertId: ObjectID = new ObjectID(actionValue);
|
|
301
|
+
|
|
302
|
+
await MicrosoftTeamsActionAuthorization.assertCanUpdateAlert({
|
|
303
|
+
alertId: alertId,
|
|
304
|
+
projectId: projectId,
|
|
305
|
+
props: databaseProps,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
await AlertService.resolveAlert(alertId, oneUptimeUserId);
|
|
295
309
|
await turnContext.sendActivity("✅ Alert resolved.");
|
|
296
310
|
return;
|
|
297
311
|
}
|
|
@@ -506,9 +520,7 @@ export default class MicrosoftTeamsAlertActions {
|
|
|
506
520
|
data: {
|
|
507
521
|
currentAlertStateId: new ObjectID(alertStateId.toString()),
|
|
508
522
|
},
|
|
509
|
-
props:
|
|
510
|
-
isRoot: true,
|
|
511
|
-
},
|
|
523
|
+
props: databaseProps,
|
|
512
524
|
});
|
|
513
525
|
|
|
514
526
|
await turnContext.sendActivity("✅ Alert state changed successfully.");
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import { ExpressRequest } from "../../../Express";
|
|
2
1
|
import { JSONObject } from "../../../../../Types/JSON";
|
|
3
2
|
import ObjectID from "../../../../../Types/ObjectID";
|
|
4
|
-
import WorkspaceProjectAuthToken from "../../../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
|
|
5
|
-
import WorkspaceProjectAuthTokenService from "../../../../Services/WorkspaceProjectAuthTokenService";
|
|
6
3
|
import WorkspaceType from "../../../../../Types/Workspace/WorkspaceType";
|
|
7
4
|
import logger from "../../../Logger";
|
|
8
5
|
import CaptureSpan from "../../../Telemetry/CaptureSpan";
|
|
@@ -30,173 +27,6 @@ export interface MicrosoftTeamsRequest {
|
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
export default class MicrosoftTeamsAuthAction {
|
|
33
|
-
@CaptureSpan()
|
|
34
|
-
public static async isAuthorized(data: {
|
|
35
|
-
req: ExpressRequest;
|
|
36
|
-
}): Promise<MicrosoftTeamsRequest> {
|
|
37
|
-
const req: ExpressRequest = data.req;
|
|
38
|
-
let payload: JSONObject = {};
|
|
39
|
-
let tenantId: string = "";
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
payload = req.body as JSONObject;
|
|
43
|
-
logger.debug("Microsoft Teams payload received:");
|
|
44
|
-
logger.debug(payload);
|
|
45
|
-
} catch (error) {
|
|
46
|
-
logger.debug("Failed to parse Microsoft Teams payload:");
|
|
47
|
-
logger.debug(error);
|
|
48
|
-
return {
|
|
49
|
-
isAuthorized: false,
|
|
50
|
-
projectId: new ObjectID(""),
|
|
51
|
-
authToken: "",
|
|
52
|
-
payloadType: "unknown",
|
|
53
|
-
tenantId: tenantId,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Microsoft Teams sends different types of activities
|
|
58
|
-
const activityType: string = payload["type"] as string;
|
|
59
|
-
const from: JSONObject = payload["from"] as JSONObject;
|
|
60
|
-
const conversation: JSONObject = payload["conversation"] as JSONObject;
|
|
61
|
-
const channelData: JSONObject = payload["channelData"] as JSONObject;
|
|
62
|
-
|
|
63
|
-
let teamId: string = "";
|
|
64
|
-
let channelId: string = "";
|
|
65
|
-
let userId: string = "";
|
|
66
|
-
|
|
67
|
-
// Extract team and channel information
|
|
68
|
-
if (channelData && channelData["team"]) {
|
|
69
|
-
teamId = (channelData["team"] as JSONObject)["id"] as string;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (conversation && conversation["id"]) {
|
|
73
|
-
// For channel messages, conversation ID contains channel info
|
|
74
|
-
channelId = conversation["id"] as string;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (from && from["id"]) {
|
|
78
|
-
userId = from["id"] as string;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (channelData && channelData["tenant"]) {
|
|
82
|
-
tenantId = (channelData["tenant"] as JSONObject)["id"] as string;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Handle different activity types
|
|
86
|
-
if (activityType === "installationUpdate") {
|
|
87
|
-
// Bot was installed or uninstalled
|
|
88
|
-
const action: string = payload["action"] as string;
|
|
89
|
-
if (action === "remove") {
|
|
90
|
-
return {
|
|
91
|
-
isAuthorized: true,
|
|
92
|
-
projectId: new ObjectID(""), // We'll need to find the project
|
|
93
|
-
authToken: "",
|
|
94
|
-
payloadType: "app_uninstall",
|
|
95
|
-
teamId: teamId,
|
|
96
|
-
tenantId: tenantId,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Find the project associated with this team
|
|
102
|
-
if (!teamId) {
|
|
103
|
-
logger.debug("No team ID found in payload", {
|
|
104
|
-
tenantId: tenantId,
|
|
105
|
-
});
|
|
106
|
-
return {
|
|
107
|
-
isAuthorized: false,
|
|
108
|
-
projectId: new ObjectID(""),
|
|
109
|
-
authToken: "",
|
|
110
|
-
payloadType: activityType,
|
|
111
|
-
tenantId: tenantId,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
let projectAuthToken: WorkspaceProjectAuthToken | null = null;
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
// Find project auth token by checking if miscData contains the teamId
|
|
119
|
-
projectAuthToken = await WorkspaceProjectAuthTokenService.findOneBy({
|
|
120
|
-
query: {
|
|
121
|
-
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
122
|
-
workspaceProjectId: tenantId,
|
|
123
|
-
},
|
|
124
|
-
select: {
|
|
125
|
-
_id: true,
|
|
126
|
-
projectId: true,
|
|
127
|
-
authToken: true,
|
|
128
|
-
miscData: true,
|
|
129
|
-
},
|
|
130
|
-
props: {
|
|
131
|
-
isRoot: true,
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
} catch (error) {
|
|
135
|
-
logger.debug("Error finding project auth token:", {
|
|
136
|
-
teamId: teamId,
|
|
137
|
-
tenantId: tenantId,
|
|
138
|
-
});
|
|
139
|
-
logger.debug(error);
|
|
140
|
-
return {
|
|
141
|
-
isAuthorized: false,
|
|
142
|
-
projectId: new ObjectID(""),
|
|
143
|
-
authToken: "",
|
|
144
|
-
payloadType: activityType,
|
|
145
|
-
tenantId: tenantId,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (!projectAuthToken) {
|
|
150
|
-
logger.debug(`No project auth token found for team: ${teamId}`, {
|
|
151
|
-
teamId: teamId,
|
|
152
|
-
tenantId: tenantId,
|
|
153
|
-
});
|
|
154
|
-
return {
|
|
155
|
-
isAuthorized: false,
|
|
156
|
-
projectId: new ObjectID(""),
|
|
157
|
-
authToken: "",
|
|
158
|
-
payloadType: activityType,
|
|
159
|
-
tenantId: tenantId,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Parse actions from the payload
|
|
164
|
-
const actions: Array<MicrosoftTeamsAction> = [];
|
|
165
|
-
|
|
166
|
-
if (activityType === "invoke") {
|
|
167
|
-
// Handle adaptive card button clicks
|
|
168
|
-
const value: JSONObject = payload["value"] as JSONObject;
|
|
169
|
-
if (value && value["action"]) {
|
|
170
|
-
actions.push({
|
|
171
|
-
actionType: value["action"] as string,
|
|
172
|
-
actionValue: value["actionValue"] as string,
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
} else if (activityType === "message") {
|
|
176
|
-
// Handle text messages to the bot
|
|
177
|
-
const text: string = (payload["text"] as string) || "";
|
|
178
|
-
if (text.toLowerCase().includes("help")) {
|
|
179
|
-
actions.push({
|
|
180
|
-
actionType: "help",
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
isAuthorized: true,
|
|
187
|
-
projectId: projectAuthToken.projectId!,
|
|
188
|
-
authToken: projectAuthToken.authToken!,
|
|
189
|
-
payloadType: activityType,
|
|
190
|
-
actions: actions,
|
|
191
|
-
userId: userId,
|
|
192
|
-
teamId: teamId,
|
|
193
|
-
tenantId: tenantId,
|
|
194
|
-
channelId: channelId,
|
|
195
|
-
messageId: payload["id"] as string,
|
|
196
|
-
payload: payload,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
30
|
@CaptureSpan()
|
|
201
31
|
public static async getOneUptimeUserIdFromTeamsUserId(data: {
|
|
202
32
|
teamsUserId: string;
|