@oneuptime/common 12.0.18 → 12.0.19
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/DatabaseModels/EnterpriseLicense.ts +23 -0
- package/Models/DatabaseModels/Index.ts +4 -2
- package/Models/DatabaseModels/Monitor.ts +39 -9
- package/Models/DatabaseModels/UserMicrosoftTeams.ts +344 -0
- package/Models/DatabaseModels/UserNotificationRule.ts +144 -8
- package/Models/DatabaseModels/UserNotificationSetting.ts +34 -0
- package/Models/DatabaseModels/UserOnCallLogTimeline.ts +95 -0
- package/Models/DatabaseModels/UserSlack.ts +341 -0
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +9 -0
- package/Server/API/UserMicrosoftTeamsAPI.ts +127 -0
- package/Server/API/UserSlackAPI.ts +125 -0
- package/Server/EnvironmentConfig.ts +12 -107
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.ts +51 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +117 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +197 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Infrastructure/Queue.ts +7 -0
- package/Server/Services/EnterpriseLicenseService.ts +60 -0
- package/Server/Services/Index.ts +6 -2
- package/Server/Services/LlmLogService.ts +25 -9
- package/Server/Services/OnCallReadinessService.ts +86 -2
- package/Server/Services/ProjectService.ts +67 -0
- package/Server/Services/UserMicrosoftTeamsService.ts +208 -0
- package/Server/Services/UserNotificationMethodAdminService.ts +167 -5
- package/Server/Services/UserNotificationRuleAdminService.ts +40 -6
- package/Server/Services/UserNotificationRuleService.ts +589 -11
- package/Server/Services/UserNotificationSettingService.ts +138 -0
- package/Server/Services/UserService.ts +28 -0
- package/Server/Services/UserSlackService.ts +218 -0
- package/Server/Services/WorkspaceProjectAuthTokenService.ts +67 -1
- package/Server/Services/WorkspaceUserAuthTokenService.ts +73 -0
- package/Server/Services/WorkspaceUserNotificationService.ts +190 -0
- package/Server/Utils/Marketing/MarketingEventUtil.ts +145 -0
- package/Server/Utils/Marketing/MarketingEventWebhook.ts +114 -0
- package/Server/Utils/Monitor/MonitorLogUtil.ts +14 -1
- package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +321 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +191 -4
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +22 -14
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +22 -11
- package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +37 -1
- package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +18 -10
- package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +27 -1
- package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +214 -0
- package/Tests/Server/API/OnCallReadinessAPI.test.ts +145 -26
- package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +2 -2
- package/Tests/Server/Services/AIServiceDailyBudget.test.ts +11 -1
- package/Tests/Server/Services/AdminRuleEditGuards.test.ts +22 -8
- package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +18 -2
- package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +5 -1
- package/Tests/Server/Services/LlmLogServiceTokenAggregateParams.test.ts +640 -0
- package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +246 -22
- package/Tests/Server/Services/NotificationDeletionImpact.test.ts +136 -8
- package/Tests/Server/Services/OnCallNotificationFallback.test.ts +191 -8
- package/Tests/Server/Services/OnCallReadinessService.test.ts +343 -18
- package/Tests/Server/Services/SeverityRuleBackfill.test.ts +5 -1
- package/Tests/Server/Services/UserMicrosoftTeamsService.test.ts +343 -0
- package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +209 -8
- package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +22 -6
- package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +12 -6
- package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +37 -3
- package/Tests/Server/Services/UserNotificationRuleWorkspaceDelivery.test.ts +669 -0
- package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +410 -0
- package/Tests/Server/Services/UserSlackService.test.ts +407 -0
- package/Tests/Server/Services/WorkspaceProjectAuthTokenDisconnectCascade.test.ts +224 -0
- package/Tests/Server/Services/WorkspaceUserAuthTokenNotificationMethodCascade.test.ts +189 -0
- package/Tests/Server/Services/WorkspaceUserNotificationService.test.ts +496 -0
- package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +30 -17
- package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +8 -3
- package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +72 -3
- package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +22 -18
- package/Tests/Server/Types/Database/Permissions/WorkspaceMethodStampedColumnCreate.test.ts +178 -0
- package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +259 -0
- package/Tests/Server/Utils/Marketing/MarketingEventWebhook.test.ts +257 -0
- package/Tests/Server/Utils/Monitor/MonitorLogUtilRedaction.test.ts +300 -0
- package/Tests/Server/Utils/Monitor/MonitorPayloadRedaction.test.ts +381 -0
- package/Tests/Server/Utils/Runbook/RunbookExecutePermission.test.ts +228 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayErasureTombstone.test.ts +189 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +205 -0
- package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +275 -0
- package/Tests/Server/Utils/Telemetry/TelemetryContext.test.ts +279 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +454 -0
- package/Tests/UI/Utils/NotificationMethodUtil.test.ts +137 -9
- package/Tests/UI/Utils/PermissionGate.test.ts +105 -0
- package/Types/Marketing/MarketingEvent.ts +97 -0
- package/UI/Utils/NotificationMethodUtil.ts +41 -1
- package/UI/Utils/PermissionGate.ts +56 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +24 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +4 -2
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Monitor.js +39 -9
- package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js +363 -0
- package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js +145 -8
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +38 -0
- package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +96 -0
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserSlack.js +361 -0
- package/build/dist/Models/DatabaseModels/UserSlack.js.map +1 -0
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +82 -0
- package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -0
- package/build/dist/Server/API/UserSlackAPI.js +81 -0
- package/build/dist/Server/API/UserSlackAPI.js.map +1 -0
- package/build/dist/Server/EnvironmentConfig.js +10 -70
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js +37 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +106 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +78 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Queue.js +7 -0
- package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
- package/build/dist/Server/Services/EnterpriseLicenseService.js +65 -0
- package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +6 -2
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/LlmLogService.js +25 -9
- package/build/dist/Server/Services/LlmLogService.js.map +1 -1
- package/build/dist/Server/Services/OnCallReadinessService.js +74 -2
- package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +56 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/UserMicrosoftTeamsService.js +183 -0
- package/build/dist/Server/Services/UserMicrosoftTeamsService.js.map +1 -0
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js +138 -2
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js +32 -4
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +499 -67
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +118 -0
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +26 -1
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/UserSlackService.js +195 -0
- package/build/dist/Server/Services/UserSlackService.js.map +1 -0
- package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +64 -1
- package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js +70 -0
- package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceUserNotificationService.js +129 -0
- package/build/dist/Server/Services/WorkspaceUserNotificationService.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +94 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +93 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -1
- package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +233 -0
- package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +141 -4
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/Marketing/MarketingEvent.js +43 -0
- package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -0
- package/build/dist/UI/Utils/NotificationMethodUtil.js +33 -1
- package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -1
- package/build/dist/UI/Utils/PermissionGate.js +32 -0
- package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
- package/package.json +1 -1
- package/Models/DatabaseModels/MarketingConversion.ts +0 -410
- package/Server/Services/MarketingConversionService.ts +0 -10
- package/Server/Utils/Marketing/ConversionUploadProvider.ts +0 -215
- package/Server/Utils/Marketing/ConversionUploadProviders.ts +0 -22
- package/Server/Utils/Marketing/Providers/GoogleAds.ts +0 -333
- package/Server/Utils/Marketing/Providers/LinkedIn.ts +0 -185
- package/Server/Utils/Marketing/Providers/Meta.ts +0 -182
- package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +0 -223
- package/Server/Utils/Marketing/Providers/Reddit.ts +0 -208
- package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +0 -315
- package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +0 -300
- package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +0 -592
- package/Tests/Server/Utils/Marketing/LinkedIn.test.ts +0 -282
- package/Tests/Server/Utils/Marketing/Meta.test.ts +0 -304
- package/Tests/Server/Utils/Marketing/MicrosoftAds.test.ts +0 -263
- package/Tests/Server/Utils/Marketing/Reddit.test.ts +0 -259
- package/Types/Marketing/MarketingConversion.ts +0 -53
- package/build/dist/Models/DatabaseModels/MarketingConversion.js +0 -445
- package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +0 -1
- package/build/dist/Server/Services/MarketingConversionService.js +0 -9
- package/build/dist/Server/Services/MarketingConversionService.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +0 -93
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js +0 -20
- package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +0 -221
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +0 -137
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js +0 -133
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +0 -143
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +0 -145
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +0 -1
- package/build/dist/Types/Marketing/MarketingConversion.js +0 -52
- package/build/dist/Types/Marketing/MarketingConversion.js.map +0 -1
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* https://github.com/OneUptime/oneuptime/issues/3360
|
|
3
|
+
*
|
|
4
|
+
* MonitorLogUtil is the widest-reach persistence sink in the monitoring path:
|
|
5
|
+
* every monitor type, every evaluation, straight into `MonitorLog.logBody` --
|
|
6
|
+
* a column whose read ACL includes `Permission.Viewer`. Whatever
|
|
7
|
+
* `saveMonitorLog` is handed is what a read-only API key can select back out.
|
|
8
|
+
*
|
|
9
|
+
* The unit-level behaviour of the redactor is pinned in
|
|
10
|
+
* MonitorPayloadRedaction.test.ts. What THIS suite pins is the wiring: that
|
|
11
|
+
* the row actually enqueued for ClickHouse is the redacted one, that the
|
|
12
|
+
* caller's live `dataToProcess` is not collaterally damaged on the way, and
|
|
13
|
+
* that the rest of the row (ids, timestamps, retention) is unchanged -- so the
|
|
14
|
+
* fix cannot be quietly reverted by someone reinstating the raw
|
|
15
|
+
* `JSON.parse(JSON.stringify(...))`.
|
|
16
|
+
*
|
|
17
|
+
* Flushing: the buffer drains on a 5s timer or at 10,000 rows, neither of
|
|
18
|
+
* which a test should wait for. Rather than reach into a private, the suite
|
|
19
|
+
* drives the flush through MonitorLogUtil's own PUBLIC contract -- the
|
|
20
|
+
* GracefulShutdown handler it registers, which awaits `flushAndWait()`. The
|
|
21
|
+
* mock below captures that callback so a test can invoke it directly.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const mockShutdown: { callback: (() => Promise<void> | void) | null } = {
|
|
25
|
+
callback: null,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
jest.mock("../../../../Server/Utils/GracefulShutdown", () => {
|
|
29
|
+
return {
|
|
30
|
+
__esModule: true,
|
|
31
|
+
default: {
|
|
32
|
+
registerHandler: (
|
|
33
|
+
_name: string,
|
|
34
|
+
_priority: number,
|
|
35
|
+
callback: () => Promise<void> | void,
|
|
36
|
+
): void => {
|
|
37
|
+
mockShutdown.callback = callback;
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
ShutdownPriority: {
|
|
41
|
+
HttpServer: 10,
|
|
42
|
+
Workers: 20,
|
|
43
|
+
Buffers: 30,
|
|
44
|
+
DataStores: 40,
|
|
45
|
+
Telemetry: 50,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
jest.mock("../../../../Server/Services/MonitorLogService", () => {
|
|
51
|
+
return {
|
|
52
|
+
__esModule: true,
|
|
53
|
+
default: {
|
|
54
|
+
insertJsonRows: jest.fn(() => {
|
|
55
|
+
return Promise.resolve();
|
|
56
|
+
}),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
jest.mock("../../../../Server/Services/GlobalConfigService", () => {
|
|
62
|
+
return {
|
|
63
|
+
__esModule: true,
|
|
64
|
+
default: {
|
|
65
|
+
findOneBy: jest.fn(() => {
|
|
66
|
+
return Promise.resolve({ monitorLogRetentionInDays: 7 });
|
|
67
|
+
}),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
jest.mock("../../../../Server/Utils/Logger", () => {
|
|
73
|
+
return {
|
|
74
|
+
__esModule: true,
|
|
75
|
+
default: {
|
|
76
|
+
debug: jest.fn(),
|
|
77
|
+
info: jest.fn(),
|
|
78
|
+
warn: jest.fn(),
|
|
79
|
+
error: jest.fn(),
|
|
80
|
+
trace: jest.fn(),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
import MonitorLogUtil from "../../../../Server/Utils/Monitor/MonitorLogUtil";
|
|
86
|
+
import MonitorLogService from "../../../../Server/Services/MonitorLogService";
|
|
87
|
+
import DataToProcess from "../../../../Server/Utils/Monitor/DataToProcess";
|
|
88
|
+
import { REDACTED } from "../../../../Server/Utils/LogRedaction";
|
|
89
|
+
import { JSONObject } from "../../../../Types/JSON";
|
|
90
|
+
import ObjectID from "../../../../Types/ObjectID";
|
|
91
|
+
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
92
|
+
|
|
93
|
+
const SECRET: string = "b1946ac9-2492-4b0f-9b2f-ee9b6cbe36ba";
|
|
94
|
+
|
|
95
|
+
const MONITOR_ID: ObjectID = new ObjectID(
|
|
96
|
+
"8f14e45f-ceea-467a-9575-1b0d0d3e7a9c",
|
|
97
|
+
);
|
|
98
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
99
|
+
"3c6e0b8a-9c15-4f8b-a1d2-7e5f4c3b2a19",
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
/*
|
|
103
|
+
* Loosely typed on purpose: the module factory above returns an untyped
|
|
104
|
+
* jest.fn(), and pinning a signature here only fights ts-jest without making
|
|
105
|
+
* the assertions any stronger.
|
|
106
|
+
*/
|
|
107
|
+
const insertJsonRows: jest.Mock =
|
|
108
|
+
MonitorLogService.insertJsonRows as unknown as jest.Mock;
|
|
109
|
+
|
|
110
|
+
type SaveAndFlushFunction = (
|
|
111
|
+
dataToProcess: DataToProcess,
|
|
112
|
+
) => Promise<JSONObject>;
|
|
113
|
+
|
|
114
|
+
/*
|
|
115
|
+
* Run one payload all the way through to the row handed to ClickHouse.
|
|
116
|
+
*
|
|
117
|
+
* `saveMonitorLog` is deliberately fire-and-forget (the monitor hot path must
|
|
118
|
+
* not block on a retention lookup), so the promise chain has to be given a
|
|
119
|
+
* turn of the event loop before the row exists in the buffer at all.
|
|
120
|
+
*/
|
|
121
|
+
const saveAndFlush: SaveAndFlushFunction = async (
|
|
122
|
+
dataToProcess: DataToProcess,
|
|
123
|
+
): Promise<JSONObject> => {
|
|
124
|
+
MonitorLogUtil.saveMonitorLog({
|
|
125
|
+
monitorId: MONITOR_ID,
|
|
126
|
+
projectId: PROJECT_ID,
|
|
127
|
+
dataToProcess: dataToProcess,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await new Promise<void>((resolve: () => void) => {
|
|
131
|
+
setTimeout(resolve, 0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (!mockShutdown.callback) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
"MonitorLogUtil did not register its shutdown flush handler",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
await mockShutdown.callback();
|
|
141
|
+
|
|
142
|
+
const calls: Array<Array<unknown>> = insertJsonRows.mock
|
|
143
|
+
.calls as unknown as Array<Array<unknown>>;
|
|
144
|
+
|
|
145
|
+
expect(calls.length).toBeGreaterThan(0);
|
|
146
|
+
|
|
147
|
+
const rows: Array<JSONObject> = calls[
|
|
148
|
+
calls.length - 1
|
|
149
|
+
]![0] as Array<JSONObject>;
|
|
150
|
+
|
|
151
|
+
expect(rows).toHaveLength(1);
|
|
152
|
+
|
|
153
|
+
return rows[0]!;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
type ServerBeatFunction = () => JSONObject;
|
|
157
|
+
|
|
158
|
+
// The payload the Go agent sends, with the two fields MonitorResource stamps on.
|
|
159
|
+
const serverBeat: ServerBeatFunction = (): JSONObject => {
|
|
160
|
+
return {
|
|
161
|
+
secretKey: SECRET,
|
|
162
|
+
hostname: "web-01.internal",
|
|
163
|
+
monitorId: MONITOR_ID,
|
|
164
|
+
projectId: PROJECT_ID,
|
|
165
|
+
requestReceivedAt: new Date("2026-08-23T10:00:00.000Z"),
|
|
166
|
+
onlyCheckRequestReceivedAt: false,
|
|
167
|
+
basicInfrastructureMetrics: {
|
|
168
|
+
cpuMetrics: { percentUsed: 12.5, cores: 8 },
|
|
169
|
+
memoryMetrics: { total: 16_777_216, percentUsed: 75 },
|
|
170
|
+
},
|
|
171
|
+
processes: [{ pid: 42, name: "node", command: "node index.js" }],
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
describe("MonitorLogUtil.saveMonitorLog", () => {
|
|
176
|
+
beforeEach(() => {
|
|
177
|
+
insertJsonRows.mockClear();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("does not write the server-agent secret into logBody", async () => {
|
|
181
|
+
const row: JSONObject = await saveAndFlush(
|
|
182
|
+
serverBeat() as unknown as DataToProcess,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const logBody: JSONObject = row["logBody"] as JSONObject;
|
|
186
|
+
|
|
187
|
+
expect(logBody["secretKey"]).toBe(REDACTED);
|
|
188
|
+
expect(JSON.stringify(row)).not.toContain(SECRET);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("still writes everything the monitor is actually for", async () => {
|
|
192
|
+
const row: JSONObject = await saveAndFlush(
|
|
193
|
+
serverBeat() as unknown as DataToProcess,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const logBody: JSONObject = row["logBody"] as JSONObject;
|
|
197
|
+
|
|
198
|
+
expect(logBody["hostname"]).toBe("web-01.internal");
|
|
199
|
+
expect(logBody["onlyCheckRequestReceivedAt"]).toBe(false);
|
|
200
|
+
expect(
|
|
201
|
+
(logBody["basicInfrastructureMetrics"] as JSONObject)["cpuMetrics"],
|
|
202
|
+
).toEqual({ percentUsed: 12.5, cores: 8 });
|
|
203
|
+
expect(logBody["processes"]).toEqual([
|
|
204
|
+
{ pid: 42, name: "node", command: "node index.js" },
|
|
205
|
+
]);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("leaves the caller's live payload untouched", async () => {
|
|
209
|
+
/*
|
|
210
|
+
* saveMonitorLog returns immediately and MonitorResource keeps evaluating
|
|
211
|
+
* criteria against the same object afterwards. Redacting in place would
|
|
212
|
+
* change the monitor's verdict, not just its stored log -- so the clone,
|
|
213
|
+
* not the original, must be what gets masked.
|
|
214
|
+
*/
|
|
215
|
+
const live: JSONObject = serverBeat();
|
|
216
|
+
|
|
217
|
+
await saveAndFlush(live as unknown as DataToProcess);
|
|
218
|
+
|
|
219
|
+
expect(live["secretKey"]).toBe(SECRET);
|
|
220
|
+
expect(live["hostname"]).toBe("web-01.internal");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("keeps the rest of the row intact", async () => {
|
|
224
|
+
/*
|
|
225
|
+
* Redaction must not disturb the columns ClickHouse partitions, sorts and
|
|
226
|
+
* expires on. A row that loses its retentionDate never gets TTL'd.
|
|
227
|
+
*/
|
|
228
|
+
const row: JSONObject = await saveAndFlush(
|
|
229
|
+
serverBeat() as unknown as DataToProcess,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
expect(row["monitorId"]).toBe(MONITOR_ID.toString());
|
|
233
|
+
expect(row["projectId"]).toBe(PROJECT_ID.toString());
|
|
234
|
+
expect(typeof row["_id"]).toBe("string");
|
|
235
|
+
expect(typeof row["time"]).toBe("string");
|
|
236
|
+
expect(typeof row["createdAt"]).toBe("string");
|
|
237
|
+
expect(typeof row["retentionDate"]).toBe("string");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("masks caller-supplied auth headers on an incoming-request monitor", async () => {
|
|
241
|
+
/*
|
|
242
|
+
* The other half of the issue: an incoming-request monitor stores the
|
|
243
|
+
* headers and body of whatever called it, so a caller's bearer token was
|
|
244
|
+
* landing in Viewer-readable logBody as well.
|
|
245
|
+
*/
|
|
246
|
+
const row: JSONObject = await saveAndFlush({
|
|
247
|
+
monitorId: MONITOR_ID,
|
|
248
|
+
projectId: PROJECT_ID,
|
|
249
|
+
incomingRequestReceivedAt: new Date("2026-08-23T10:00:00.000Z"),
|
|
250
|
+
checkedAt: new Date("2026-08-23T10:00:01.000Z"),
|
|
251
|
+
requestHeaders: {
|
|
252
|
+
authorization: `Bearer ${SECRET}`,
|
|
253
|
+
"content-type": "application/json",
|
|
254
|
+
},
|
|
255
|
+
requestBody: { status: "ok" },
|
|
256
|
+
} as unknown as DataToProcess);
|
|
257
|
+
|
|
258
|
+
const logBody: JSONObject = row["logBody"] as JSONObject;
|
|
259
|
+
const headers: JSONObject = logBody["requestHeaders"] as JSONObject;
|
|
260
|
+
|
|
261
|
+
expect(headers["authorization"]).toBe(REDACTED);
|
|
262
|
+
expect(headers["content-type"]).toBe("application/json");
|
|
263
|
+
expect((logBody["requestBody"] as JSONObject)["status"]).toBe("ok");
|
|
264
|
+
expect(JSON.stringify(row)).not.toContain(SECRET);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("writes nothing at all when required ids are missing", async () => {
|
|
268
|
+
/*
|
|
269
|
+
* Pins the existing guards, which the redaction change sits directly on
|
|
270
|
+
* top of: a row with no monitorId cannot be scoped or TTL'd.
|
|
271
|
+
*/
|
|
272
|
+
MonitorLogUtil.saveMonitorLog({
|
|
273
|
+
monitorId: undefined as unknown as ObjectID,
|
|
274
|
+
projectId: PROJECT_ID,
|
|
275
|
+
dataToProcess: serverBeat() as unknown as DataToProcess,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
MonitorLogUtil.saveMonitorLog({
|
|
279
|
+
monitorId: MONITOR_ID,
|
|
280
|
+
projectId: undefined as unknown as ObjectID,
|
|
281
|
+
dataToProcess: serverBeat() as unknown as DataToProcess,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
MonitorLogUtil.saveMonitorLog({
|
|
285
|
+
monitorId: MONITOR_ID,
|
|
286
|
+
projectId: PROJECT_ID,
|
|
287
|
+
dataToProcess: undefined as unknown as DataToProcess,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
await new Promise<void>((resolve: () => void) => {
|
|
291
|
+
setTimeout(resolve, 0);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
if (mockShutdown.callback) {
|
|
295
|
+
await mockShutdown.callback();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
expect(insertJsonRows).not.toHaveBeenCalled();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import {
|
|
2
|
+
redactForPersistence,
|
|
3
|
+
stripAgentCredentials,
|
|
4
|
+
} from "../../../../Server/Utils/Monitor/MonitorPayloadRedaction";
|
|
5
|
+
import { REDACTED } from "../../../../Server/Utils/LogRedaction";
|
|
6
|
+
import { JSONObject } from "../../../../Types/JSON";
|
|
7
|
+
import { describe, expect, it } from "@jest/globals";
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* Regression tests for https://github.com/OneUptime/oneuptime/issues/3360.
|
|
11
|
+
*
|
|
12
|
+
* The infrastructure agent authenticates with the monitor's
|
|
13
|
+
* `serverMonitorSecretKey` and sends that key in the request BODY as well as
|
|
14
|
+
* the URL (InfrastructureAgent/model/server_monitor_report.go:
|
|
15
|
+
* `SecretKey string \`json:"secretKey"\``). The body became `dataToProcess`,
|
|
16
|
+
* and `dataToProcess` was written verbatim into three columns that
|
|
17
|
+
* `Permission.Viewer` -- the least privilege OneUptime grants -- can select:
|
|
18
|
+
* `MonitorLog.logBody`, `Monitor.serverMonitorResponse` and
|
|
19
|
+
* `MonitorProbe.lastMonitoringLog`.
|
|
20
|
+
*
|
|
21
|
+
* The property under test is therefore not "the redactor works" in the
|
|
22
|
+
* abstract. It is: given the payload the real agent actually sends, the secret
|
|
23
|
+
* does not appear ANYWHERE in the output, in any spelling, at any depth --
|
|
24
|
+
* while every legitimate monitor observation survives untouched, because a
|
|
25
|
+
* redactor that eats the metrics is a redactor somebody will turn off.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// A value distinctive enough that a substring search for it is meaningful.
|
|
29
|
+
const SECRET: string = "b1946ac9-2492-4b0f-9b2f-ee9b6cbe36ba";
|
|
30
|
+
|
|
31
|
+
type AgentPayloadFunction = () => JSONObject;
|
|
32
|
+
|
|
33
|
+
/*
|
|
34
|
+
* The real shape, key-for-key, that the Go agent marshals and
|
|
35
|
+
* ProcessServerMonitorIngest deserializes -- plus the two fields
|
|
36
|
+
* MonitorResource stamps on before anything is persisted (`projectId`,
|
|
37
|
+
* `evaluationSummary`). Reproduced faithfully because a redactor tested only
|
|
38
|
+
* against a toy `{secretKey: "x"}` proves nothing about the payload that
|
|
39
|
+
* actually leaked.
|
|
40
|
+
*/
|
|
41
|
+
const agentPayload: AgentPayloadFunction = (): JSONObject => {
|
|
42
|
+
return {
|
|
43
|
+
secretKey: SECRET,
|
|
44
|
+
basicInfrastructureMetrics: {
|
|
45
|
+
memoryMetrics: {
|
|
46
|
+
total: 16_777_216,
|
|
47
|
+
free: 4_194_304,
|
|
48
|
+
used: 12_582_912,
|
|
49
|
+
percentUsed: 75,
|
|
50
|
+
percentFree: 25,
|
|
51
|
+
cached: 1_048_576,
|
|
52
|
+
swapTotal: 0,
|
|
53
|
+
},
|
|
54
|
+
cpuMetrics: {
|
|
55
|
+
percentUsed: 12.5,
|
|
56
|
+
cores: 8,
|
|
57
|
+
perCorePercent: [10, 15, 12, 11, 14, 13, 12, 13],
|
|
58
|
+
},
|
|
59
|
+
diskMetrics: [{ diskPath: "/", total: 500, free: 200, percentUsed: 60 }],
|
|
60
|
+
},
|
|
61
|
+
requestReceivedAt: "2026-08-23T10:00:00.000Z",
|
|
62
|
+
onlyCheckRequestReceivedAt: false,
|
|
63
|
+
processes: [
|
|
64
|
+
{ pid: 1, name: "systemd", command: "/sbin/init", cpuPercent: 0.1 },
|
|
65
|
+
{ pid: 42, name: "node", command: "node index.js", cpuPercent: 3.2 },
|
|
66
|
+
],
|
|
67
|
+
hostname: "web-01.internal",
|
|
68
|
+
monitorId: "8f14e45f-ceea-467a-9575-1b0d0d3e7a9c",
|
|
69
|
+
timeNow: "2026-08-23T10:00:01.000Z",
|
|
70
|
+
evaluationSummary: {
|
|
71
|
+
evaluatedAt: "2026-08-23T10:00:01.000Z",
|
|
72
|
+
criteriaResults: [],
|
|
73
|
+
events: [],
|
|
74
|
+
},
|
|
75
|
+
projectId: "3c6e0b8a-9c15-4f8b-a1d2-7e5f4c3b2a19",
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type ContainsSecretFunction = (value: unknown) => boolean;
|
|
80
|
+
|
|
81
|
+
/*
|
|
82
|
+
* The assertion that actually matters. Checking `output.secretKey` alone would
|
|
83
|
+
* pass for an implementation that merely moved the value one level down, so
|
|
84
|
+
* every test asserts on the serialized whole.
|
|
85
|
+
*/
|
|
86
|
+
const containsSecret: ContainsSecretFunction = (value: unknown): boolean => {
|
|
87
|
+
return JSON.stringify(value)?.includes(SECRET) ?? false;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
describe("stripAgentCredentials - the ingest boundary", () => {
|
|
91
|
+
it("removes the secret the agent puts in the request body", () => {
|
|
92
|
+
const output: JSONObject = stripAgentCredentials(agentPayload());
|
|
93
|
+
|
|
94
|
+
expect(output["secretKey"]).toBeUndefined();
|
|
95
|
+
expect("secretKey" in output).toBe(false);
|
|
96
|
+
expect(containsSecret(output)).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("keeps every legitimate observation in the beat", () => {
|
|
100
|
+
const input: JSONObject = agentPayload();
|
|
101
|
+
const output: JSONObject = stripAgentCredentials(input);
|
|
102
|
+
|
|
103
|
+
/*
|
|
104
|
+
* The whole point of a server monitor. If redaction ever starts eating
|
|
105
|
+
* these, criteria stop evaluating and the feature is dead -- so pin the
|
|
106
|
+
* metrics block as deep-equal rather than spot-checking one field.
|
|
107
|
+
*/
|
|
108
|
+
expect(output["basicInfrastructureMetrics"]).toEqual(
|
|
109
|
+
input["basicInfrastructureMetrics"],
|
|
110
|
+
);
|
|
111
|
+
expect(output["processes"]).toEqual(input["processes"]);
|
|
112
|
+
expect(output["hostname"]).toBe("web-01.internal");
|
|
113
|
+
expect(output["monitorId"]).toBe("8f14e45f-ceea-467a-9575-1b0d0d3e7a9c");
|
|
114
|
+
expect(output["projectId"]).toBe("3c6e0b8a-9c15-4f8b-a1d2-7e5f4c3b2a19");
|
|
115
|
+
expect(output["onlyCheckRequestReceivedAt"]).toBe(false);
|
|
116
|
+
expect(output["requestReceivedAt"]).toBe("2026-08-23T10:00:00.000Z");
|
|
117
|
+
expect(output["evaluationSummary"]).toEqual(input["evaluationSummary"]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("drops the key rather than masking it, so the typed column stays faithful", () => {
|
|
121
|
+
/*
|
|
122
|
+
* `Monitor.serverMonitorResponse` is typed as ServerMonitorResponse, which
|
|
123
|
+
* declares no `secretKey`. Masking would persist a phantom field on a
|
|
124
|
+
* typed jsonb column; removing keeps the stored object a valid instance of
|
|
125
|
+
* its own interface.
|
|
126
|
+
*/
|
|
127
|
+
const output: JSONObject = stripAgentCredentials(agentPayload());
|
|
128
|
+
|
|
129
|
+
expect(Object.keys(output)).not.toContain("secretKey");
|
|
130
|
+
expect(JSON.stringify(output)).not.toContain(REDACTED);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("does not mutate the payload it was handed", () => {
|
|
134
|
+
/*
|
|
135
|
+
* The caller keeps using the input object. An in-place strip would be a
|
|
136
|
+
* different bug wearing the same fix: it would change what the monitor
|
|
137
|
+
* evaluates, not just what gets stored.
|
|
138
|
+
*/
|
|
139
|
+
const input: JSONObject = agentPayload();
|
|
140
|
+
|
|
141
|
+
stripAgentCredentials(input);
|
|
142
|
+
|
|
143
|
+
expect(input["secretKey"]).toBe(SECRET);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("returns a defensive copy, not the same object", () => {
|
|
147
|
+
const input: JSONObject = agentPayload();
|
|
148
|
+
const output: JSONObject = stripAgentCredentials(input);
|
|
149
|
+
|
|
150
|
+
expect(output).not.toBe(input);
|
|
151
|
+
expect(output["processes"]).not.toBe(input["processes"]);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("stripAgentCredentials - spellings and nesting", () => {
|
|
156
|
+
it("catches the secret under every spelling of the key", () => {
|
|
157
|
+
/*
|
|
158
|
+
* The Go struct tag is `secretKey` today. A future agent, a proxy that
|
|
159
|
+
* renames fields, or a different ingest path may spell it otherwise, and
|
|
160
|
+
* the classifier normalizes case and separators for exactly that reason.
|
|
161
|
+
*/
|
|
162
|
+
for (const key of [
|
|
163
|
+
"secretKey",
|
|
164
|
+
"secret_key",
|
|
165
|
+
"SecretKey",
|
|
166
|
+
"SECRET_KEY",
|
|
167
|
+
"secret-key",
|
|
168
|
+
"monitorSecretKey",
|
|
169
|
+
"serverMonitorSecretKey",
|
|
170
|
+
]) {
|
|
171
|
+
const output: JSONObject = stripAgentCredentials({
|
|
172
|
+
[key]: SECRET,
|
|
173
|
+
hostname: "web-01",
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(containsSecret(output)).toBe(false);
|
|
177
|
+
expect(output["hostname"]).toBe("web-01");
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("finds a credential buried below the top level", () => {
|
|
182
|
+
const output: JSONObject = stripAgentCredentials({
|
|
183
|
+
hostname: "web-01",
|
|
184
|
+
nested: { deeper: { secretKey: SECRET, keep: "yes" } },
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(containsSecret(output)).toBe(false);
|
|
188
|
+
expect(
|
|
189
|
+
((output["nested"] as JSONObject)["deeper"] as JSONObject)["keep"],
|
|
190
|
+
).toBe("yes");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("finds a credential inside an array element", () => {
|
|
194
|
+
const output: JSONObject = stripAgentCredentials({
|
|
195
|
+
beats: [{ secretKey: SECRET }, { hostname: "web-02" }],
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(containsSecret(output)).toBe(false);
|
|
199
|
+
expect(output["beats"] as Array<JSONObject>).toHaveLength(2);
|
|
200
|
+
expect((output["beats"] as Array<JSONObject>)[1]!["hostname"]).toBe(
|
|
201
|
+
"web-02",
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("handles null, undefined and empty payloads without throwing", () => {
|
|
206
|
+
expect(stripAgentCredentials(null)).toBeNull();
|
|
207
|
+
expect(stripAgentCredentials(undefined)).toBeUndefined();
|
|
208
|
+
expect(stripAgentCredentials({})).toEqual({});
|
|
209
|
+
expect(stripAgentCredentials({ a: null, b: undefined })).toEqual({
|
|
210
|
+
a: null,
|
|
211
|
+
b: undefined,
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("passes class instances through as leaves", () => {
|
|
216
|
+
/*
|
|
217
|
+
* The ingest boundary runs on JSONFunctions.deserialize output, which
|
|
218
|
+
* rehydrates Dates and ObjectIDs into live instances. Walking into them
|
|
219
|
+
* would rebuild them as plain objects and destroy the type.
|
|
220
|
+
*/
|
|
221
|
+
const date: Date = new Date("2026-08-23T10:00:00.000Z");
|
|
222
|
+
|
|
223
|
+
const output: JSONObject = stripAgentCredentials({
|
|
224
|
+
requestReceivedAt: date,
|
|
225
|
+
secretKey: SECRET,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(output["requestReceivedAt"]).toBe(date);
|
|
229
|
+
expect(output["requestReceivedAt"] instanceof Date).toBe(true);
|
|
230
|
+
expect(containsSecret(output)).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe("redactForPersistence - the logBody sink", () => {
|
|
235
|
+
it("masks the secret instead of dropping it", () => {
|
|
236
|
+
/*
|
|
237
|
+
* logBody is a diagnostic record, so the useful answer is "a credential
|
|
238
|
+
* was here" rather than silence. The value still must not survive.
|
|
239
|
+
*/
|
|
240
|
+
const output: JSONObject = redactForPersistence(
|
|
241
|
+
agentPayload(),
|
|
242
|
+
) as JSONObject;
|
|
243
|
+
|
|
244
|
+
expect(output["secretKey"]).toBe(REDACTED);
|
|
245
|
+
expect(containsSecret(output)).toBe(false);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("masks the auth headers an incoming-request monitor records", () => {
|
|
249
|
+
/*
|
|
250
|
+
* The second half of the issue: IncomingMonitorRequest carries
|
|
251
|
+
* requestHeaders and requestBody straight from the caller, so whatever
|
|
252
|
+
* token the caller sent was landing in Viewer-readable logBody too.
|
|
253
|
+
*/
|
|
254
|
+
const output: JSONObject = redactForPersistence({
|
|
255
|
+
requestHeaders: {
|
|
256
|
+
Authorization: `Bearer ${SECRET}`,
|
|
257
|
+
Cookie: `session=${SECRET}`,
|
|
258
|
+
"x-api-key": SECRET,
|
|
259
|
+
"content-type": "application/json",
|
|
260
|
+
"user-agent": "curl/8.0",
|
|
261
|
+
},
|
|
262
|
+
requestBody: { password: SECRET, orderId: "A-1001" },
|
|
263
|
+
incomingRequestReceivedAt: "2026-08-23T10:00:00.000Z",
|
|
264
|
+
}) as JSONObject;
|
|
265
|
+
|
|
266
|
+
const headers: JSONObject = output["requestHeaders"] as JSONObject;
|
|
267
|
+
|
|
268
|
+
expect(headers["Authorization"]).toBe(REDACTED);
|
|
269
|
+
expect(headers["Cookie"]).toBe(REDACTED);
|
|
270
|
+
expect(headers["x-api-key"]).toBe(REDACTED);
|
|
271
|
+
|
|
272
|
+
// Non-credential headers are exactly why masking beats dropping the block.
|
|
273
|
+
expect(headers["content-type"]).toBe("application/json");
|
|
274
|
+
expect(headers["user-agent"]).toBe("curl/8.0");
|
|
275
|
+
|
|
276
|
+
expect((output["requestBody"] as JSONObject)["password"]).toBe(REDACTED);
|
|
277
|
+
expect((output["requestBody"] as JSONObject)["orderId"]).toBe("A-1001");
|
|
278
|
+
expect(containsSecret(output)).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("leaves probe observations that only look credential-shaped alone", () => {
|
|
282
|
+
/*
|
|
283
|
+
* Over-redaction is a real cost here: these are the fields monitor
|
|
284
|
+
* criteria are written against, and `code` in particular is classified by
|
|
285
|
+
* VALUE, so an error code must survive while an OAuth code must not.
|
|
286
|
+
*/
|
|
287
|
+
const output: JSONObject = redactForPersistence({
|
|
288
|
+
responseCode: 200,
|
|
289
|
+
responseTimeInMs: 143,
|
|
290
|
+
requestFailedDetails: {
|
|
291
|
+
failedPhase: "TCP Connection",
|
|
292
|
+
errorCode: "ECONNREFUSED",
|
|
293
|
+
errorDescription: "Connection refused",
|
|
294
|
+
},
|
|
295
|
+
sslResponse: {
|
|
296
|
+
certificateValidationErrorCode: "CERT_HAS_EXPIRED",
|
|
297
|
+
fingerprint: "AA:BB:CC",
|
|
298
|
+
serialNumber: "0123456789",
|
|
299
|
+
commonName: "example.com",
|
|
300
|
+
},
|
|
301
|
+
customCodeMonitorResponse: {
|
|
302
|
+
executionTimeInMS: 12,
|
|
303
|
+
scriptError: undefined,
|
|
304
|
+
},
|
|
305
|
+
code: "ECONNREFUSED",
|
|
306
|
+
}) as JSONObject;
|
|
307
|
+
|
|
308
|
+
expect(output["responseCode"]).toBe(200);
|
|
309
|
+
expect(output["responseTimeInMs"]).toBe(143);
|
|
310
|
+
expect((output["requestFailedDetails"] as JSONObject)["errorCode"]).toBe(
|
|
311
|
+
"ECONNREFUSED",
|
|
312
|
+
);
|
|
313
|
+
expect(
|
|
314
|
+
(output["sslResponse"] as JSONObject)["certificateValidationErrorCode"],
|
|
315
|
+
).toBe("CERT_HAS_EXPIRED");
|
|
316
|
+
expect((output["sslResponse"] as JSONObject)["commonName"]).toBe(
|
|
317
|
+
"example.com",
|
|
318
|
+
);
|
|
319
|
+
expect(
|
|
320
|
+
(output["customCodeMonitorResponse"] as JSONObject)["executionTimeInMS"],
|
|
321
|
+
).toBe(12);
|
|
322
|
+
expect(output["code"]).toBe("ECONNREFUSED");
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("still redacts a code that is actually a credential", () => {
|
|
326
|
+
const output: JSONObject = redactForPersistence({
|
|
327
|
+
code: SECRET,
|
|
328
|
+
}) as JSONObject;
|
|
329
|
+
|
|
330
|
+
expect(output["code"]).toBe(REDACTED);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("does not mutate the payload it was handed", () => {
|
|
334
|
+
const input: JSONObject = agentPayload();
|
|
335
|
+
|
|
336
|
+
redactForPersistence(input);
|
|
337
|
+
|
|
338
|
+
expect(input["secretKey"]).toBe(SECRET);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("handles null and undefined without throwing", () => {
|
|
342
|
+
expect(redactForPersistence(null)).toBeNull();
|
|
343
|
+
expect(redactForPersistence(undefined as never)).toBeUndefined();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("drops rather than passes through a subtree past the depth ceiling", () => {
|
|
347
|
+
/*
|
|
348
|
+
* A payload nested deeper than the walk will go is not something a monitor
|
|
349
|
+
* legitimately reports, and passing the tail through unredacted would be
|
|
350
|
+
* an trivially exploitable way to smuggle a secret past the walk.
|
|
351
|
+
*/
|
|
352
|
+
let deep: JSONObject = { secretKey: SECRET };
|
|
353
|
+
|
|
354
|
+
for (let i: number = 0; i < 40; i++) {
|
|
355
|
+
deep = { nested: deep };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
expect(containsSecret(redactForPersistence(deep))).toBe(false);
|
|
359
|
+
expect(containsSecret(stripAgentCredentials(deep))).toBe(false);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe("the reproduction in the issue", () => {
|
|
364
|
+
it("no longer yields the monitor secret from a stored beat", () => {
|
|
365
|
+
/*
|
|
366
|
+
* Issue #3360, steps 1-4: register an agent, select logBody with a
|
|
367
|
+
* Viewer-only key, read logBody.secretKey, compare to
|
|
368
|
+
* Monitor.serverMonitorSecretKey. The comparison is what must now fail.
|
|
369
|
+
*/
|
|
370
|
+
const stored: JSONObject = redactForPersistence(
|
|
371
|
+
stripAgentCredentials(agentPayload()),
|
|
372
|
+
) as JSONObject;
|
|
373
|
+
|
|
374
|
+
expect(stored["secretKey"]).toBeUndefined();
|
|
375
|
+
expect(JSON.stringify(stored)).not.toContain(SECRET);
|
|
376
|
+
|
|
377
|
+
// And the beat is still a usable monitor observation.
|
|
378
|
+
expect(stored["hostname"]).toBe("web-01.internal");
|
|
379
|
+
expect(stored["basicInfrastructureMetrics"]).toBeDefined();
|
|
380
|
+
});
|
|
381
|
+
});
|