@oneuptime/common 12.0.16 → 12.0.18

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 (36) 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/Services/BillingService.ts +8 -0
  5. package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +303 -46
  6. package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +17 -0
  7. package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +250 -0
  8. package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +106 -0
  9. package/Tests/App/Dashboard/SecurityEventsMonitorsPage.test.tsx +185 -0
  10. package/Tests/App/Dashboard/SecurityEventsSetupGuide.test.tsx +200 -0
  11. package/Tests/Models/DetectionRuleCreateContract.test.ts +35 -0
  12. package/Tests/Server/Services/BillingService.test.ts +20 -0
  13. package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +438 -0
  14. package/Tests/Types/Kubernetes/KubernetesObjectParsers.test.ts +794 -0
  15. package/Tests/Types/Kubernetes/KubernetesRightSizing.test.ts +53 -0
  16. package/Tests/Types/Measurement/MeasurementAggregationType.test.ts +100 -0
  17. package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +114 -0
  18. package/Tests/Types/Workspace/WorkspaceType.test.ts +53 -0
  19. package/Tests/Utils/Dashboard/Components/DashboardComponentDefaults.test.ts +257 -0
  20. package/Types/Billing/PayAsYouGoPricing.ts +1 -1
  21. package/Types/SecurityEvent/DetectionFindingConstants.ts +24 -0
  22. package/build/dist/Models/DatabaseModels/DetectionRule.js +81 -0
  23. package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
  24. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788600000000-AddDetectionRuleIncidentColumns.js +24 -0
  25. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788600000000-AddDetectionRuleIncidentColumns.js.map +1 -0
  26. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  27. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  28. package/build/dist/Server/Services/BillingService.js +6 -0
  29. package/build/dist/Server/Services/BillingService.js.map +1 -1
  30. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +220 -24
  31. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
  32. package/build/dist/Types/Billing/PayAsYouGoPricing.js +1 -1
  33. package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -1
  34. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +18 -0
  35. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -0
  36. package/package.json +1 -1
@@ -29,6 +29,10 @@ import Alert from "../../../../Models/DatabaseModels/Alert";
29
29
  import AlertSeverity from "../../../../Models/DatabaseModels/AlertSeverity";
30
30
  import AlertService from "../../../../Server/Services/AlertService";
31
31
  import AlertSeverityService from "../../../../Server/Services/AlertSeverityService";
32
+ import Incident from "../../../../Models/DatabaseModels/Incident";
33
+ import IncidentSeverity from "../../../../Models/DatabaseModels/IncidentSeverity";
34
+ import IncidentService from "../../../../Server/Services/IncidentService";
35
+ import IncidentSeverityService from "../../../../Server/Services/IncidentSeverityService";
32
36
  import DetectionRuleService from "../../../../Server/Services/DetectionRuleService";
33
37
  import SecurityEventService, {
34
38
  DetectionMatchGroup,
@@ -42,6 +46,7 @@ import ObjectID from "../../../../Types/ObjectID";
42
46
  import OneUptimeDate from "../../../../Types/Date";
43
47
  import ServiceType from "../../../../Types/Telemetry/ServiceType";
44
48
  import { JSONObject } from "../../../../Types/JSON";
49
+ import Includes from "../../../../Types/BaseDatabase/Includes";
45
50
  import { getJestSpyOn } from "../../../Spy";
46
51
  import {
47
52
  afterEach,
@@ -96,7 +101,9 @@ function buildRule(
96
101
  sigmaRuleYaml?: string;
97
102
  shouldCreateAlert?: boolean;
98
103
  shouldWriteDetectionFinding?: boolean;
104
+ shouldCreateIncident?: boolean;
99
105
  alertSeverityId?: ObjectID;
106
+ incidentSeverityId?: ObjectID;
100
107
  lastEvaluatedAt?: Date;
101
108
  evaluationIntervalInMinutes?: number;
102
109
  id?: ObjectID;
@@ -120,10 +127,18 @@ function buildRule(
120
127
  rule.shouldWriteDetectionFinding = options.shouldWriteDetectionFinding;
121
128
  }
122
129
 
130
+ if (options.shouldCreateIncident !== undefined) {
131
+ rule.shouldCreateIncident = options.shouldCreateIncident;
132
+ }
133
+
123
134
  if (options.alertSeverityId !== undefined) {
124
135
  rule.alertSeverityId = options.alertSeverityId;
125
136
  }
126
137
 
138
+ if (options.incidentSeverityId !== undefined) {
139
+ rule.incidentSeverityId = options.incidentSeverityId;
140
+ }
141
+
127
142
  if (options.lastEvaluatedAt !== undefined) {
128
143
  rule.lastEvaluatedAt = options.lastEvaluatedAt;
129
144
  }
@@ -161,6 +176,23 @@ function buildOpenAlert(fingerprint: string): Alert {
161
176
  return alert;
162
177
  }
163
178
 
179
+ function buildIncidentSeverity(
180
+ idSuffix: string,
181
+ name: string,
182
+ ): IncidentSeverity {
183
+ const severity: IncidentSeverity = new IncidentSeverity();
184
+ severity._id = `66666666-6666-4666-8666-66666666${idSuffix}`;
185
+ severity.name = name;
186
+ return severity;
187
+ }
188
+
189
+ function buildOpenIncident(fingerprint: string): Incident {
190
+ const incident: Incident = new Incident();
191
+ incident._id = "77777777-7777-4777-8777-777777777777";
192
+ incident.seriesFingerprint = fingerprint;
193
+ return incident;
194
+ }
195
+
164
196
  function expectedFingerprint(groupValue: string): string {
165
197
  return `detection-rule:${RULE_ID.toString()}:${MetricSeriesFingerprint.computeFingerprint(
166
198
  {
@@ -175,6 +207,9 @@ interface EvaluationSpies {
175
207
  alertFindBy: Spy;
176
208
  alertCreate: Spy;
177
209
  severityFindBy: Spy;
210
+ incidentFindBy: Spy;
211
+ incidentCreate: Spy;
212
+ incidentSeverityFindBy: Spy;
178
213
  updateOneById: Spy;
179
214
  telemetryServiceFromName: Spy;
180
215
  }
@@ -184,6 +219,8 @@ function installSpies(
184
219
  groups?: Array<DetectionMatchGroup>;
185
220
  severities?: Array<AlertSeverity>;
186
221
  openAlerts?: Array<Alert>;
222
+ incidentSeverities?: Array<IncidentSeverity>;
223
+ openIncidents?: Array<Incident>;
187
224
  } = {},
188
225
  ): EvaluationSpies {
189
226
  const findDetectionMatches: Spy = getJestSpyOn(
@@ -211,6 +248,21 @@ function installSpies(
211
248
  "findBy",
212
249
  ).mockResolvedValue((options.severities || []) as never);
213
250
 
251
+ const incidentFindBy: Spy = getJestSpyOn(
252
+ IncidentService,
253
+ "findBy",
254
+ ).mockResolvedValue((options.openIncidents || []) as never);
255
+
256
+ const incidentCreate: Spy = getJestSpyOn(
257
+ IncidentService,
258
+ "create",
259
+ ).mockResolvedValue(new Incident() as never);
260
+
261
+ const incidentSeverityFindBy: Spy = getJestSpyOn(
262
+ IncidentSeverityService,
263
+ "findBy",
264
+ ).mockResolvedValue((options.incidentSeverities || []) as never);
265
+
214
266
  const updateOneById: Spy = getJestSpyOn(
215
267
  DetectionRuleService,
216
268
  "updateOneById",
@@ -227,6 +279,9 @@ function installSpies(
227
279
  alertFindBy,
228
280
  alertCreate,
229
281
  severityFindBy,
282
+ incidentFindBy,
283
+ incidentCreate,
284
+ incidentSeverityFindBy,
230
285
  updateOneById,
231
286
  telemetryServiceFromName,
232
287
  };
@@ -237,6 +292,11 @@ function createdAlert(spies: EvaluationSpies, index: number): Alert {
237
292
  return (call as { data: Alert }).data;
238
293
  }
239
294
 
295
+ function createdIncident(spies: EvaluationSpies, index: number): Incident {
296
+ const call: unknown = spies.incidentCreate.mock.calls[index]?.[0];
297
+ return (call as { data: Incident }).data;
298
+ }
299
+
240
300
  describe("DetectionRuleEvaluator", () => {
241
301
  beforeEach(() => {
242
302
  // The evaluator logs expected failures; keep test output clean.
@@ -284,6 +344,7 @@ describe("DetectionRuleEvaluator", () => {
284
344
  matchedGroups: 0,
285
345
  totalMatches: 0,
286
346
  alertsCreated: 0,
347
+ incidentsCreated: 0,
287
348
  findingsWritten: 0,
288
349
  error: null,
289
350
  };
@@ -606,6 +667,383 @@ describe("DetectionRuleEvaluator", () => {
606
667
  });
607
668
  });
608
669
 
670
+ describe("incident creation", () => {
671
+ /*
672
+ * The gate is === true, the OPPOSITE of the alert path's !== false.
673
+ * Incidents are the heavy machinery (on-call, SLAs, status pages) and
674
+ * the column defaults to false — so unset must read as off. These
675
+ * three tests pin the whole truth table.
676
+ */
677
+ test("does not open incidents when shouldCreateIncident is unset", async () => {
678
+ const rule: DetectionRule = buildRule({
679
+ shouldWriteDetectionFinding: false,
680
+ });
681
+
682
+ const spies: EvaluationSpies = installSpies({
683
+ groups: [buildGroup("alice", 3)],
684
+ severities: [buildSeverity("0001", "Default")],
685
+ incidentSeverities: [buildIncidentSeverity("0001", "Default")],
686
+ });
687
+
688
+ const result: DetectionRuleEvaluationResult =
689
+ await DetectionRuleEvaluator.evaluateRule(rule);
690
+
691
+ expect(spies.incidentCreate).not.toHaveBeenCalled();
692
+ expect(spies.incidentSeverityFindBy).not.toHaveBeenCalled();
693
+ expect(result.incidentsCreated).toBe(0);
694
+ // The alert path is untouched by the incident gate.
695
+ expect(spies.alertCreate).toHaveBeenCalledTimes(1);
696
+ });
697
+
698
+ test("does not open incidents when shouldCreateIncident is explicitly false", async () => {
699
+ const rule: DetectionRule = buildRule({
700
+ shouldCreateIncident: false,
701
+ shouldWriteDetectionFinding: false,
702
+ });
703
+
704
+ const spies: EvaluationSpies = installSpies({
705
+ groups: [buildGroup("alice", 3)],
706
+ severities: [buildSeverity("0001", "Default")],
707
+ incidentSeverities: [buildIncidentSeverity("0001", "Default")],
708
+ });
709
+
710
+ await DetectionRuleEvaluator.evaluateRule(rule);
711
+
712
+ expect(spies.incidentCreate).not.toHaveBeenCalled();
713
+ });
714
+
715
+ test("opens one incident per matched group when opted in", async () => {
716
+ const rule: DetectionRule = buildRule({
717
+ shouldCreateIncident: true,
718
+ shouldWriteDetectionFinding: false,
719
+ });
720
+
721
+ const spies: EvaluationSpies = installSpies({
722
+ groups: [buildGroup("alice", 3), buildGroup("bob", 1)],
723
+ severities: [buildSeverity("0001", "Default")],
724
+ incidentSeverities: [buildIncidentSeverity("0001", "Default")],
725
+ });
726
+
727
+ const result: DetectionRuleEvaluationResult =
728
+ await DetectionRuleEvaluator.evaluateRule(rule);
729
+
730
+ expect(result.incidentsCreated).toBe(2);
731
+ expect(spies.incidentCreate).toHaveBeenCalledTimes(2);
732
+
733
+ const first: Incident = createdIncident(spies, 0);
734
+
735
+ expect(first.projectId?.toString()).toBe(PROJECT_ID.toString());
736
+ expect(first.title).toBe("[Detection] Brute Force Watch — alice");
737
+ expect(first.description).toContain(
738
+ "Detection rule matched 3 security events",
739
+ );
740
+ expect(first.description).toContain("Watches for brute-force logins");
741
+ expect(first.seriesFingerprint).toBe(expectedFingerprint("alice"));
742
+ expect(first.isCreatedAutomatically).toBe(true);
743
+ expect(first.rootCause).toBe(
744
+ 'Sigma detection rule "Brute Force Watch" matched security events.',
745
+ );
746
+ /*
747
+ * Detection incidents carry no monitors — monitor-driven
748
+ * auto-resolve must never touch them; the fingerprint dedupe is
749
+ * what keeps a still-firing rule from stacking incidents.
750
+ */
751
+ expect(first.monitors).toBeUndefined();
752
+
753
+ const createProps: unknown = spies.incidentCreate.mock.calls[0]?.[0];
754
+ expect((createProps as { props: JSONObject }).props).toEqual({
755
+ isRoot: true,
756
+ });
757
+ });
758
+
759
+ test("incidents open even when the rule does not create alerts", async () => {
760
+ const rule: DetectionRule = buildRule({
761
+ shouldCreateAlert: false,
762
+ shouldCreateIncident: true,
763
+ shouldWriteDetectionFinding: false,
764
+ });
765
+
766
+ const spies: EvaluationSpies = installSpies({
767
+ groups: [buildGroup("alice", 3)],
768
+ incidentSeverities: [buildIncidentSeverity("0001", "Default")],
769
+ });
770
+
771
+ const result: DetectionRuleEvaluationResult =
772
+ await DetectionRuleEvaluator.evaluateRule(rule);
773
+
774
+ expect(spies.alertCreate).not.toHaveBeenCalled();
775
+ expect(result.alertsCreated).toBe(0);
776
+ expect(result.incidentsCreated).toBe(1);
777
+ });
778
+
779
+ test("alert and incident for the same group share one fingerprint and one summary", async () => {
780
+ const rule: DetectionRule = buildRule({
781
+ shouldCreateIncident: true,
782
+ shouldWriteDetectionFinding: false,
783
+ });
784
+
785
+ const spies: EvaluationSpies = installSpies({
786
+ groups: [buildGroup("alice", 3)],
787
+ severities: [buildSeverity("0001", "Default")],
788
+ incidentSeverities: [buildIncidentSeverity("0001", "Default")],
789
+ });
790
+
791
+ await DetectionRuleEvaluator.evaluateRule(rule);
792
+
793
+ const alert: Alert = createdAlert(spies, 0);
794
+ const incident: Incident = createdIncident(spies, 0);
795
+
796
+ expect(incident.seriesFingerprint).toBe(alert.seriesFingerprint);
797
+ expect(incident.title).toBe(alert.title);
798
+ expect(incident.description).toBe(alert.description);
799
+ });
800
+
801
+ test("dedupes against unresolved incidents by fingerprint", async () => {
802
+ const rule: DetectionRule = buildRule({
803
+ shouldCreateIncident: true,
804
+ shouldWriteDetectionFinding: false,
805
+ });
806
+
807
+ const spies: EvaluationSpies = installSpies({
808
+ groups: [buildGroup("alice", 3), buildGroup("bob", 1)],
809
+ severities: [buildSeverity("0001", "Default")],
810
+ incidentSeverities: [buildIncidentSeverity("0001", "Default")],
811
+ openIncidents: [buildOpenIncident(expectedFingerprint("alice"))],
812
+ });
813
+
814
+ const result: DetectionRuleEvaluationResult =
815
+ await DetectionRuleEvaluator.evaluateRule(rule);
816
+
817
+ // alice is already open; only bob gets a new incident.
818
+ expect(result.incidentsCreated).toBe(1);
819
+ expect(createdIncident(spies, 0).seriesFingerprint).toBe(
820
+ expectedFingerprint("bob"),
821
+ );
822
+
823
+ /*
824
+ * And the dedupe query is scoped to UNRESOLVED incidents carrying
825
+ * THIS rule's candidate fingerprints — resolved incidents must be
826
+ * allowed to re-open, and a project-wide scan would age still-open
827
+ * detections out of the LIMIT_PER_PROJECT window and re-fire them.
828
+ */
829
+ const findByArg: JSONObject = spies.incidentFindBy.mock
830
+ .calls[0]?.[0] as JSONObject;
831
+ expect(findByArg["query"]).toEqual({
832
+ projectId: PROJECT_ID,
833
+ seriesFingerprint: new Includes([
834
+ expectedFingerprint("alice"),
835
+ expectedFingerprint("bob"),
836
+ ]),
837
+ currentIncidentState: {
838
+ isResolvedState: false,
839
+ },
840
+ });
841
+ });
842
+
843
+ test("an incident fingerprint already open does NOT suppress the alert", async () => {
844
+ /*
845
+ * The two dedupe sets are independent: an open incident for a group
846
+ * says nothing about whether an alert is open for it.
847
+ */
848
+ const rule: DetectionRule = buildRule({
849
+ shouldCreateIncident: true,
850
+ shouldWriteDetectionFinding: false,
851
+ });
852
+
853
+ installSpies({
854
+ groups: [buildGroup("alice", 3)],
855
+ severities: [buildSeverity("0001", "Default")],
856
+ incidentSeverities: [buildIncidentSeverity("0001", "Default")],
857
+ openIncidents: [buildOpenIncident(expectedFingerprint("alice"))],
858
+ });
859
+
860
+ const result: DetectionRuleEvaluationResult =
861
+ await DetectionRuleEvaluator.evaluateRule(rule);
862
+
863
+ expect(result.incidentsCreated).toBe(0);
864
+ expect(result.alertsCreated).toBe(1);
865
+ });
866
+
867
+ test("one failing incident create logs and continues; the rest still open", async () => {
868
+ const rule: DetectionRule = buildRule({
869
+ shouldCreateIncident: true,
870
+ shouldWriteDetectionFinding: false,
871
+ });
872
+
873
+ const spies: EvaluationSpies = installSpies({
874
+ groups: [buildGroup("alice", 3), buildGroup("bob", 1)],
875
+ severities: [buildSeverity("0001", "Default")],
876
+ incidentSeverities: [buildIncidentSeverity("0001", "Default")],
877
+ });
878
+
879
+ /*
880
+ * First create throws the way IncidentService.onBeforeCreate does
881
+ * when a project has no "created" incident state — the one failure
882
+ * mode that cannot be pre-checked.
883
+ */
884
+ spies.incidentCreate
885
+ .mockRejectedValueOnce(
886
+ new Error("Created incident state not found for this project"),
887
+ )
888
+ .mockResolvedValue(new Incident() as never);
889
+
890
+ const result: DetectionRuleEvaluationResult =
891
+ await DetectionRuleEvaluator.evaluateRule(rule);
892
+
893
+ expect(result.incidentsCreated).toBe(1);
894
+ expect(result.error).toBeNull();
895
+ expect(spies.incidentCreate).toHaveBeenCalledTimes(2);
896
+ });
897
+ });
898
+
899
+ describe("incident severity resolution", () => {
900
+ test("uses the rule's explicit incident severity when it belongs to the project", async () => {
901
+ const explicit: IncidentSeverity = buildIncidentSeverity("0002", "SEV-2");
902
+
903
+ const rule: DetectionRule = buildRule({
904
+ shouldCreateIncident: true,
905
+ shouldWriteDetectionFinding: false,
906
+ incidentSeverityId: new ObjectID(explicit._id!),
907
+ });
908
+
909
+ const spies: EvaluationSpies = installSpies({
910
+ groups: [buildGroup("alice", 1)],
911
+ severities: [buildSeverity("0001", "Default")],
912
+ incidentSeverities: [
913
+ buildIncidentSeverity("0001", "SEV-1"),
914
+ explicit,
915
+ buildIncidentSeverity("0003", "SEV-3"),
916
+ ],
917
+ });
918
+
919
+ await DetectionRuleEvaluator.evaluateRule(rule);
920
+
921
+ expect(createdIncident(spies, 0).incidentSeverityId?.toString()).toBe(
922
+ explicit._id,
923
+ );
924
+ });
925
+
926
+ test("an explicit id not in the project falls through to the level name match", async () => {
927
+ const rule: DetectionRule = buildRule({
928
+ level: "high",
929
+ shouldCreateIncident: true,
930
+ shouldWriteDetectionFinding: false,
931
+ incidentSeverityId: new ObjectID(
932
+ "99999999-0000-4000-8000-000000000000",
933
+ ),
934
+ });
935
+
936
+ const named: IncidentSeverity = buildIncidentSeverity("0002", "High");
937
+
938
+ const spies: EvaluationSpies = installSpies({
939
+ groups: [buildGroup("alice", 1)],
940
+ severities: [buildSeverity("0001", "Default")],
941
+ incidentSeverities: [buildIncidentSeverity("0001", "SEV-1"), named],
942
+ });
943
+
944
+ await DetectionRuleEvaluator.evaluateRule(rule);
945
+
946
+ expect(createdIncident(spies, 0).incidentSeverityId?.toString()).toBe(
947
+ named._id,
948
+ );
949
+ });
950
+
951
+ test("critical maps to the most severe (first) incident severity by rank", async () => {
952
+ const rule: DetectionRule = buildRule({
953
+ level: "critical",
954
+ shouldCreateIncident: true,
955
+ shouldWriteDetectionFinding: false,
956
+ });
957
+
958
+ const first: IncidentSeverity = buildIncidentSeverity("0001", "SEV-1");
959
+
960
+ const spies: EvaluationSpies = installSpies({
961
+ groups: [buildGroup("alice", 1)],
962
+ severities: [buildSeverity("0001", "Default")],
963
+ incidentSeverities: [first, buildIncidentSeverity("0002", "SEV-2")],
964
+ });
965
+
966
+ await DetectionRuleEvaluator.evaluateRule(rule);
967
+
968
+ expect(createdIncident(spies, 0).incidentSeverityId?.toString()).toBe(
969
+ first._id,
970
+ );
971
+ });
972
+
973
+ test("low maps to the least severe (last) incident severity by rank", async () => {
974
+ const rule: DetectionRule = buildRule({
975
+ level: "low",
976
+ shouldCreateIncident: true,
977
+ shouldWriteDetectionFinding: false,
978
+ });
979
+
980
+ const last: IncidentSeverity = buildIncidentSeverity("0003", "SEV-3");
981
+
982
+ const spies: EvaluationSpies = installSpies({
983
+ groups: [buildGroup("alice", 1)],
984
+ severities: [buildSeverity("0001", "Default")],
985
+ incidentSeverities: [
986
+ buildIncidentSeverity("0001", "SEV-1"),
987
+ buildIncidentSeverity("0002", "SEV-2"),
988
+ last,
989
+ ],
990
+ });
991
+
992
+ await DetectionRuleEvaluator.evaluateRule(rule);
993
+
994
+ expect(createdIncident(spies, 0).incidentSeverityId?.toString()).toBe(
995
+ last._id,
996
+ );
997
+ });
998
+
999
+ test("a project with no incident severities skips incidents but keeps alerts", async () => {
1000
+ const rule: DetectionRule = buildRule({
1001
+ shouldCreateIncident: true,
1002
+ shouldWriteDetectionFinding: false,
1003
+ });
1004
+
1005
+ const spies: EvaluationSpies = installSpies({
1006
+ groups: [buildGroup("alice", 1)],
1007
+ severities: [buildSeverity("0001", "Default")],
1008
+ incidentSeverities: [],
1009
+ });
1010
+
1011
+ const result: DetectionRuleEvaluationResult =
1012
+ await DetectionRuleEvaluator.evaluateRule(rule);
1013
+
1014
+ expect(result.incidentsCreated).toBe(0);
1015
+ expect(spies.incidentCreate).not.toHaveBeenCalled();
1016
+ expect(result.alertsCreated).toBe(1);
1017
+ expect(result.error).toBeNull();
1018
+ });
1019
+ });
1020
+
1021
+ describe("evaluateAllDueRules field selection", () => {
1022
+ test("selects the incident columns so opted-in rules do not read as off", async () => {
1023
+ /*
1024
+ * The gate is === true, so a rule fetched WITHOUT
1025
+ * shouldCreateIncident in the select silently never opens
1026
+ * incidents — this is the drift that would break the feature
1027
+ * without failing anything else.
1028
+ */
1029
+ const rulesFindBy: Spy = getJestSpyOn(
1030
+ DetectionRuleService,
1031
+ "findBy",
1032
+ ).mockResolvedValue([] as never);
1033
+
1034
+ await DetectionRuleEvaluator.evaluateAllDueRules();
1035
+
1036
+ const select: JSONObject = (
1037
+ rulesFindBy.mock.calls[0]?.[0] as { select: JSONObject }
1038
+ ).select;
1039
+
1040
+ expect(select["shouldCreateIncident"]).toBe(true);
1041
+ expect(select["incidentSeverityId"]).toBe(true);
1042
+ expect(select["shouldCreateAlert"]).toBe(true);
1043
+ expect(select["alertSeverityId"]).toBe(true);
1044
+ });
1045
+ });
1046
+
609
1047
  describe("rule bookkeeping", () => {
610
1048
  test("updates lastEvaluatedAt, lastMatchAt and clears lastError on matches", async () => {
611
1049
  const rule: DetectionRule = buildRule({