@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.
- package/Models/DatabaseModels/DetectionRule.ts +81 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788600000000-AddDetectionRuleIncidentColumns.ts +39 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/BillingService.ts +8 -0
- package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +303 -46
- package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +17 -0
- package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +250 -0
- package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +106 -0
- package/Tests/App/Dashboard/SecurityEventsMonitorsPage.test.tsx +185 -0
- package/Tests/App/Dashboard/SecurityEventsSetupGuide.test.tsx +200 -0
- package/Tests/Models/DetectionRuleCreateContract.test.ts +35 -0
- package/Tests/Server/Services/BillingService.test.ts +20 -0
- package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +438 -0
- package/Tests/Types/Kubernetes/KubernetesObjectParsers.test.ts +794 -0
- package/Tests/Types/Kubernetes/KubernetesRightSizing.test.ts +53 -0
- package/Tests/Types/Measurement/MeasurementAggregationType.test.ts +100 -0
- package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +114 -0
- package/Tests/Types/Workspace/WorkspaceType.test.ts +53 -0
- package/Tests/Utils/Dashboard/Components/DashboardComponentDefaults.test.ts +257 -0
- package/Types/Billing/PayAsYouGoPricing.ts +1 -1
- package/Types/SecurityEvent/DetectionFindingConstants.ts +24 -0
- package/build/dist/Models/DatabaseModels/DetectionRule.js +81 -0
- package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788600000000-AddDetectionRuleIncidentColumns.js +24 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788600000000-AddDetectionRuleIncidentColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/BillingService.js +6 -0
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +220 -24
- package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
- package/build/dist/Types/Billing/PayAsYouGoPricing.js +1 -1
- package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -1
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +18 -0
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -0
- 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({
|