@oneuptime/common 12.0.16 → 12.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/Models/DatabaseModels/DetectionRule.ts +81 -0
  2. package/Server/Infrastructure/Postgres/SchemaMigrations/1788600000000-AddDetectionRuleIncidentColumns.ts +39 -0
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  4. package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +303 -46
  5. package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +250 -0
  6. package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +106 -0
  7. package/Tests/App/Dashboard/SecurityEventsMonitorsPage.test.tsx +185 -0
  8. package/Tests/App/Dashboard/SecurityEventsSetupGuide.test.tsx +200 -0
  9. package/Tests/Models/DetectionRuleCreateContract.test.ts +35 -0
  10. package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +438 -0
  11. package/Types/SecurityEvent/DetectionFindingConstants.ts +24 -0
  12. package/build/dist/Models/DatabaseModels/DetectionRule.js +81 -0
  13. package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
  14. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788600000000-AddDetectionRuleIncidentColumns.js +24 -0
  15. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788600000000-AddDetectionRuleIncidentColumns.js.map +1 -0
  16. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  17. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  18. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +220 -24
  19. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
  20. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +18 -0
  21. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -0
  22. package/package.json +1 -1
@@ -0,0 +1,200 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ jest,
8
+ test,
9
+ } from "@jest/globals";
10
+ import {
11
+ cleanup,
12
+ fireEvent,
13
+ render,
14
+ screen,
15
+ waitFor,
16
+ } from "@testing-library/react";
17
+ import * as React from "react";
18
+ import { MemoryRouter } from "react-router-dom";
19
+ import SecurityEventsSetupGuide from "../../../../App/FeatureSet/Dashboard/src/Components/SecurityEvents/SecurityEventsSetupGuide";
20
+ import ModelAPI from "../../../UI/Utils/ModelAPI/ModelAPI";
21
+ import AnalyticsModelAPI from "../../../UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI";
22
+ import ProjectUtil from "../../../UI/Utils/Project";
23
+ import TelemetryIngestionKey from "../../../Models/DatabaseModels/TelemetryIngestionKey";
24
+ import ObjectID from "../../../Types/ObjectID";
25
+
26
+ /*
27
+ * The setup guide's whole job is to hand someone a command they can paste.
28
+ * Two things make that command worthless without anything failing to
29
+ * render: the wrong token (a placeholder left in when a real key was
30
+ * available, or a stale secret from a key the user has since switched
31
+ * away from), and the wrong endpoint. Both are string interpolation, so
32
+ * neither a type nor a render error would catch them.
33
+ *
34
+ * The wiring around the page — route, tab, breadcrumb — and the contract
35
+ * the snippets are written against are pinned in
36
+ * App/Tests/Dashboard/SecurityEventsSetupGuide.test.ts. What is pinned
37
+ * here is what actually reaches the screen.
38
+ */
39
+
40
+ const PROJECT_ID: ObjectID = new ObjectID("project-1");
41
+
42
+ function ingestionKey(data: {
43
+ id: string;
44
+ name: string;
45
+ secret: string;
46
+ }): TelemetryIngestionKey {
47
+ const key: TelemetryIngestionKey = new TelemetryIngestionKey();
48
+ key.id = new ObjectID(data.id);
49
+ key.name = data.name;
50
+ key.secretKey = new ObjectID(data.secret);
51
+ return key;
52
+ }
53
+
54
+ const FIRST_KEY: TelemetryIngestionKey = ingestionKey({
55
+ id: "key-1",
56
+ name: "Production Key",
57
+ secret: "secret-production",
58
+ });
59
+
60
+ /*
61
+ * The rendered text of the whole card. CodeBlock runs the sample through
62
+ * highlight.js, which shreds it into a span per token — so a query for a
63
+ * line of the snippet finds whichever fragment happens to hold the match,
64
+ * not the sample. Reading the container back is what lets an assertion be
65
+ * about the command as the user sees it.
66
+ */
67
+ type RenderGuideFunction = () => HTMLElement;
68
+
69
+ const renderGuide: RenderGuideFunction = (): HTMLElement => {
70
+ const { container } = render(
71
+ <MemoryRouter>
72
+ <SecurityEventsSetupGuide />
73
+ </MemoryRouter>,
74
+ );
75
+
76
+ return container;
77
+ };
78
+
79
+ describe("Security Events setup guide", () => {
80
+ beforeEach(() => {
81
+ jest.spyOn(ProjectUtil, "getCurrentProjectId").mockReturnValue(PROJECT_ID);
82
+ jest.spyOn(ModelAPI, "getList").mockResolvedValue({
83
+ data: [FIRST_KEY],
84
+ count: 1,
85
+ skip: 0,
86
+ limit: 50,
87
+ } as never);
88
+ // No events yet: the guide should be waiting, not congratulating.
89
+ jest.spyOn(AnalyticsModelAPI, "count").mockResolvedValue(0 as never);
90
+ });
91
+
92
+ afterEach(() => {
93
+ cleanup();
94
+ jest.restoreAllMocks();
95
+ });
96
+
97
+ test("renders the curl sample against the real ingest path", async () => {
98
+ const container: HTMLElement = renderGuide();
99
+
100
+ await waitFor(() => {
101
+ expect(container.textContent).toContain("/security-events/v1/ingest");
102
+ });
103
+
104
+ expect(container.textContent).toContain("x-oneuptime-token");
105
+ expect(container.textContent).toContain("class_uid");
106
+ });
107
+
108
+ test("interpolates the selected key's secret, not the placeholder", async () => {
109
+ const container: HTMLElement = renderGuide();
110
+
111
+ await waitFor(() => {
112
+ expect(container.textContent).toContain(
113
+ "x-oneuptime-token: secret-production",
114
+ );
115
+ });
116
+
117
+ expect(container.textContent).not.toContain("<YOUR_ONEUPTIME_TOKEN>");
118
+ });
119
+
120
+ /*
121
+ * With no key to show, the sample must still look pasteable rather than
122
+ * carry an empty header — hence the placeholder, which is only correct
123
+ * while there is genuinely nothing to interpolate.
124
+ */
125
+ test("falls back to the placeholder when the project has no keys", async () => {
126
+ jest.spyOn(ModelAPI, "getList").mockResolvedValue({
127
+ data: [],
128
+ count: 0,
129
+ skip: 0,
130
+ limit: 50,
131
+ } as never);
132
+
133
+ const container: HTMLElement = renderGuide();
134
+
135
+ await waitFor(() => {
136
+ expect(container.textContent).toContain(
137
+ "x-oneuptime-token: <YOUR_ONEUPTIME_TOKEN>",
138
+ );
139
+ });
140
+
141
+ expect(container.textContent).not.toContain("secret-production");
142
+ });
143
+
144
+ test("switching dialect switches the sample and its format", async () => {
145
+ const container: HTMLElement = renderGuide();
146
+
147
+ await waitFor(() => {
148
+ expect(container.textContent).toContain("class_uid");
149
+ });
150
+
151
+ fireEvent.click(screen.getByRole("button", { name: "Google UDM" }));
152
+
153
+ await waitFor(() => {
154
+ expect(container.textContent).toContain("format=udm");
155
+ });
156
+
157
+ expect(container.textContent).toContain("USER_LOGIN");
158
+ expect(container.textContent).not.toContain("class_uid");
159
+ });
160
+
161
+ test("switching source shows the Fluent Bit output config", async () => {
162
+ const container: HTMLElement = renderGuide();
163
+
164
+ await waitFor(() => {
165
+ expect(container.textContent).toContain("x-oneuptime-token");
166
+ });
167
+
168
+ fireEvent.click(screen.getByRole("button", { name: /Fluent Bit/ }));
169
+
170
+ await waitFor(() => {
171
+ expect(container.textContent).toContain("Name http");
172
+ });
173
+
174
+ expect(container.textContent).toContain(
175
+ "URI /security-events/v1/ingest",
176
+ );
177
+ expect(container.textContent).toContain(
178
+ "Header x-oneuptime-token secret-production",
179
+ );
180
+ });
181
+
182
+ test("waits for the first event, then confirms when one arrives", async () => {
183
+ renderGuide();
184
+
185
+ expect(
186
+ await screen.findByText(/Listening for your first security event/),
187
+ ).toBeInTheDocument();
188
+
189
+ cleanup();
190
+
191
+ jest.spyOn(AnalyticsModelAPI, "count").mockResolvedValue(7 as never);
192
+
193
+ renderGuide();
194
+
195
+ expect(
196
+ await screen.findByText(/Security events are flowing! 7 new events/),
197
+ ).toBeInTheDocument();
198
+ expect(screen.getByText("View Security Events")).toBeInTheDocument();
199
+ });
200
+ });
@@ -0,0 +1,35 @@
1
+ import DetectionRule from "../../Models/DatabaseModels/DetectionRule";
2
+ import { describe, expect, test } from "@jest/globals";
3
+
4
+ /*
5
+ * The API-compatibility contract for the incident columns added to
6
+ * DetectionRule. DatabaseService.checkRequiredFields throws a 400 for any
7
+ * required column a create payload omits — UNLESS the column is flagged
8
+ * isDefaultValueColumn, in which case the schema's DEFAULT applies.
9
+ *
10
+ * shouldCreateIncident is required-with-default-false. Without the flag,
11
+ * every API client written before the column existed — whose payloads
12
+ * necessarily omit it — starts getting 400s on deploy, and the DB default
13
+ * the model comment promises ("unset must read as off") is unreachable on
14
+ * the create path. The dashboard would mask this via createInitialValues,
15
+ * which is exactly why only a test can keep it honest.
16
+ */
17
+ describe("DetectionRule create contract", () => {
18
+ const rule: DetectionRule = new DetectionRule();
19
+
20
+ test("shouldCreateIncident may be omitted from create payloads", () => {
21
+ expect(rule.isDefaultValueColumn("shouldCreateIncident")).toBe(true);
22
+ });
23
+
24
+ test("the schema default for an omitted flag is OFF", () => {
25
+ expect(
26
+ rule.getTableColumnMetadata("shouldCreateIncident").defaultValue,
27
+ ).toBe(false);
28
+ });
29
+
30
+ test("incidentSeverityId is optional — severity falls back to the Sigma level mapping", () => {
31
+ expect(
32
+ rule.getTableColumnMetadata("incidentSeverityId").required,
33
+ ).toBeFalsy();
34
+ });
35
+ });