Skip to content

Commit

Permalink
[8.x] [Security Solution] Unlock Prebuil Rules Customization workflow…
Browse files Browse the repository at this point in the history
… for rules with missing base version (#201301) (#201657)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Security Solution] Unlock Prebuil Rules Customization workflow for
rules with missing base version
(#201301)](#201301)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Maxim
Palenov","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-11-25T17:12:35Z","message":"[Security
Solution] Unlock Prebuil Rules Customization workflow for rules with
missing base version (#201301)\n\n**Resolves:
https://github.com/elastic/kibana/issues/200904**\r\n\r\n##
Summary\r\n\r\nThis PR unlocks Prebuilt Rules Customization workflow for
rules with missing base version.\r\n\r\n## Details\r\n\r\nEach Prebuilt
Rule update contains `version` diff. `version` is a special
non-customizable field we use to track prebuilt rule version. It always
gets target rule version's value after rule upgrade.\r\n\r\nA generic
`numberDiffAlgorithm` algorithm was used for `version` field. It
produces a `SOLVABLE` conflict when rule's base version is missing. It
blocked the workflow in UI. We check the number of field with conflicts
versus resolved conflicts to decide when a rule is ready for upgrade. In
case `version` field got a conflict user had no possibility to resolve
it.\r\n\r\nThe fix adds a new `forceTargetVersionDiffAlgorithm` diff
algorithm applied only for `version` field. It produces a non-conflict
diff all the time even when base version is missing. The reason behind
is that `version` always gets target rule's
version.","sha":"dea9312246554c6af0ce148e2ec967bb5f9cca8a","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","impact:high","v9.0.0","Team:Detections
and Resp","Team: SecuritySolution","Team:Detection Rule
Management","Feature:Prebuilt Detection
Rules","backport:version","v8.17.0","v8.18.0","v8.16.2"],"title":"[Security
Solution] Unlock Prebuil Rules Customization workflow for rules with
missing base
version","number":201301,"url":"https://github.com/elastic/kibana/pull/201301","mergeCommit":{"message":"[Security
Solution] Unlock Prebuil Rules Customization workflow for rules with
missing base version (#201301)\n\n**Resolves:
https://github.com/elastic/kibana/issues/200904**\r\n\r\n##
Summary\r\n\r\nThis PR unlocks Prebuilt Rules Customization workflow for
rules with missing base version.\r\n\r\n## Details\r\n\r\nEach Prebuilt
Rule update contains `version` diff. `version` is a special
non-customizable field we use to track prebuilt rule version. It always
gets target rule version's value after rule upgrade.\r\n\r\nA generic
`numberDiffAlgorithm` algorithm was used for `version` field. It
produces a `SOLVABLE` conflict when rule's base version is missing. It
blocked the workflow in UI. We check the number of field with conflicts
versus resolved conflicts to decide when a rule is ready for upgrade. In
case `version` field got a conflict user had no possibility to resolve
it.\r\n\r\nThe fix adds a new `forceTargetVersionDiffAlgorithm` diff
algorithm applied only for `version` field. It produces a non-conflict
diff all the time even when base version is missing. The reason behind
is that `version` always gets target rule's
version.","sha":"dea9312246554c6af0ce148e2ec967bb5f9cca8a"}},"sourceBranch":"main","suggestedTargetBranches":["8.17","8.x","8.16"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/201301","number":201301,"mergeCommit":{"message":"[Security
Solution] Unlock Prebuil Rules Customization workflow for rules with
missing base version (#201301)\n\n**Resolves:
https://github.com/elastic/kibana/issues/200904**\r\n\r\n##
Summary\r\n\r\nThis PR unlocks Prebuilt Rules Customization workflow for
rules with missing base version.\r\n\r\n## Details\r\n\r\nEach Prebuilt
Rule update contains `version` diff. `version` is a special
non-customizable field we use to track prebuilt rule version. It always
gets target rule version's value after rule upgrade.\r\n\r\nA generic
`numberDiffAlgorithm` algorithm was used for `version` field. It
produces a `SOLVABLE` conflict when rule's base version is missing. It
blocked the workflow in UI. We check the number of field with conflicts
versus resolved conflicts to decide when a rule is ready for upgrade. In
case `version` field got a conflict user had no possibility to resolve
it.\r\n\r\nThe fix adds a new `forceTargetVersionDiffAlgorithm` diff
algorithm applied only for `version` field. It produces a non-conflict
diff all the time even when base version is missing. The reason behind
is that `version` always gets target rule's
version.","sha":"dea9312246554c6af0ce148e2ec967bb5f9cca8a"}},{"branch":"8.17","label":"v8.17.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.16","label":"v8.16.2","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Maxim Palenov <[email protected]>
  • Loading branch information
kibanamachine and maximpn authored Nov 25, 2024
1 parent f65baca commit 106e946
Show file tree
Hide file tree
Showing 13 changed files with 156 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { ThreeVersionsOf } from '../../../../../../../../common/api/detection_engine';
import {
ThreeWayMergeOutcome,
MissingVersion,
ThreeWayDiffConflict,
} from '../../../../../../../../common/api/detection_engine';
import { forceTargetVersionDiffAlgorithm } from './force_target_version_diff_algorithm';

describe('forceTargetVersionDiffAlgorithm', () => {
describe('when base version exists', () => {
it('returns a NON conflict diff', () => {
const mockVersions: ThreeVersionsOf<number> = {
base_version: 1,
current_version: 1,
target_version: 2,
};

const result = forceTargetVersionDiffAlgorithm(mockVersions);

expect(result).toMatchObject({
conflict: ThreeWayDiffConflict.NONE,
});
});

it('return merge outcome TARGET', () => {
const mockVersions: ThreeVersionsOf<number> = {
base_version: 1,
current_version: 1,
target_version: 2,
};

const result = forceTargetVersionDiffAlgorithm(mockVersions);

expect(result).toMatchObject({
has_base_version: true,
merge_outcome: ThreeWayMergeOutcome.Target,
});
});
});

describe('when base version missing', () => {
it('returns a NON conflict diff', () => {
const mockVersions: ThreeVersionsOf<number> = {
base_version: MissingVersion,
current_version: 1,
target_version: 2,
};

const result = forceTargetVersionDiffAlgorithm(mockVersions);

expect(result).toMatchObject({
conflict: ThreeWayDiffConflict.NONE,
});
});

it('return merge outcome TARGET', () => {
const mockVersions: ThreeVersionsOf<number> = {
base_version: MissingVersion,
current_version: 1,
target_version: 2,
};

const result = forceTargetVersionDiffAlgorithm(mockVersions);

expect(result).toMatchObject({
has_base_version: false,
merge_outcome: ThreeWayMergeOutcome.Target,
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type {
ThreeVersionsOf,
ThreeWayDiff,
} from '../../../../../../../../common/api/detection_engine/prebuilt_rules';
import {
MissingVersion,
ThreeWayDiffConflict,
ThreeWayMergeOutcome,
determineDiffOutcome,
} from '../../../../../../../../common/api/detection_engine/prebuilt_rules';

/**
* Diff algorithm forcing target version. Useful for special fields like `version`.
*/
export const forceTargetVersionDiffAlgorithm = <TValue>(
versions: ThreeVersionsOf<TValue>
): ThreeWayDiff<TValue> => {
const {
base_version: baseVersion,
current_version: currentVersion,
target_version: targetVersion,
} = versions;
const hasBaseVersion = baseVersion !== MissingVersion;
const hasUpdate = targetVersion !== currentVersion;

return {
has_base_version: hasBaseVersion,
base_version: hasBaseVersion ? baseVersion : undefined,
current_version: currentVersion,
target_version: targetVersion,
merged_version: targetVersion,
merge_outcome: ThreeWayMergeOutcome.Target,

diff_outcome: determineDiffOutcome(baseVersion, currentVersion, targetVersion),
has_update: hasUpdate,
conflict: ThreeWayDiffConflict.NONE,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export { kqlQueryDiffAlgorithm } from './kql_query_diff_algorithm';
export { eqlQueryDiffAlgorithm } from './eql_query_diff_algorithm';
export { esqlQueryDiffAlgorithm } from './esql_query_diff_algorithm';
export { ruleTypeDiffAlgorithm } from './rule_type_diff_algorithm';
export { forceTargetVersionDiffAlgorithm } from './force_target_version_diff_algorithm';
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
eqlQueryDiffAlgorithm,
esqlQueryDiffAlgorithm,
ruleTypeDiffAlgorithm,
forceTargetVersionDiffAlgorithm,
} from './algorithms';

const BASE_TYPE_ERROR = `Base version can't be of different rule type`;
Expand Down Expand Up @@ -179,7 +180,11 @@ const calculateCommonFieldsDiff = (

const commonFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableCommonFields> = {
rule_id: simpleDiffAlgorithm,
version: numberDiffAlgorithm,
/**
* `version` shouldn't have a conflict. It always get target value automatically.
* Diff has informational purpose.
*/
version: forceTargetVersionDiffAlgorithm,
name: singleLineStringDiffAlgorithm,
tags: scalarArrayDiffAlgorithm,
description: multiLineStringDiffAlgorithm,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -897,11 +897,11 @@ export default ({ getService }: FtrProviderContext): void => {
expect(fieldDiffObject.data_source).toBeUndefined();

expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1);
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // version is considered conflict
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0);
expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0);

expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1);
expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1);
expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0);
expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0);
});
});
Expand Down Expand Up @@ -958,7 +958,7 @@ export default ({ getService }: FtrProviderContext): void => {
});

expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2);
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2); // version + tags
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // tags
expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0);

expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,11 +382,11 @@ export default ({ getService }: FtrProviderContext): void => {
expect(fieldDiffObject.eql_query).toBeUndefined();

expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); // `version` is considered an updated field
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // `version` is considered conflict
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0);
expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0);

expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1);
expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1);
expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0);
expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0);
});
});
Expand Down Expand Up @@ -451,7 +451,7 @@ export default ({ getService }: FtrProviderContext): void => {
});

expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); // version + query
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2); // version + query
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // query
expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0);

expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,11 +356,11 @@ export default ({ getService }: FtrProviderContext): void => {
expect(fieldDiffObject.esql_query).toBeUndefined();

expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1);
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // version is considered conflict
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0);
expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0);

expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1);
expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1);
expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0);
expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0);
});
});
Expand Down Expand Up @@ -420,7 +420,7 @@ export default ({ getService }: FtrProviderContext): void => {
});

expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2);
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2); // version + query
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // query
expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0);

expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1042,11 +1042,11 @@ export default ({ getService }: FtrProviderContext): void => {
expect(fieldDiffObject.kql_query).toBeUndefined();

expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); // `version` is considered an updated field
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // `version` is considered conflict
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0);
expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0);

expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1);
expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1);
expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0);
expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0);
});
});
Expand Down Expand Up @@ -1114,7 +1114,7 @@ export default ({ getService }: FtrProviderContext): void => {
});

expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2);
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2); // version + query
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // query
expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0);

expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ export default ({ getService }: FtrProviderContext): void => {
expect(reviewResponse.rules[0].diff.fields.description).toBeUndefined();

expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1);
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1);
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0);
expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0);
});
});
Expand Down Expand Up @@ -430,7 +430,7 @@ export default ({ getService }: FtrProviderContext): void => {
has_base_version: false,
});
expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2);
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2);
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1);
expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0);

expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,11 +274,11 @@ export default ({ getService }: FtrProviderContext): void => {
expect(reviewResponse.rules[0].diff.fields.risk_score).toBeUndefined();

expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1);
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1);
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0);
expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0);

expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1);
expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1);
expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0);
expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0);
});
});
Expand Down Expand Up @@ -322,7 +322,7 @@ export default ({ getService }: FtrProviderContext): void => {
has_base_version: false,
});
expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2);
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2);
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1);
expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0);

expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,11 +298,11 @@ export default ({ getService }: FtrProviderContext): void => {
expect(reviewResponse.rules[0].diff.fields.type).toBeUndefined();

expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1);
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // version is considered a conflict
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0);
expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0);

expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1);
expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1);
expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0);
expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0);
});
});
Expand Down Expand Up @@ -348,7 +348,7 @@ export default ({ getService }: FtrProviderContext): void => {
});

expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(3);
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(3); // type + version + query are all considered conflicts
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2); // type + query are all considered conflicts
expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(1);

expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -423,11 +423,11 @@ export default ({ getService }: FtrProviderContext): void => {
expect(reviewResponse.rules[0].diff.fields.tags).toBeUndefined();

expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1);
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // version is considered conflict
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0);
expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0);

expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1);
expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1);
expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0);
expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0);
});
});
Expand Down Expand Up @@ -472,7 +472,7 @@ export default ({ getService }: FtrProviderContext): void => {
});

expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2);
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2); // version + tags
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // tags
expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0);

expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,11 +277,11 @@ export default ({ getService }: FtrProviderContext): void => {
expect(reviewResponse.rules[0].diff.fields.name).toBeUndefined();

expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1);
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // version is considered a conflict
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0);
expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0);

expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1);
expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1);
expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0);
expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0);
});
});
Expand Down Expand Up @@ -326,7 +326,7 @@ export default ({ getService }: FtrProviderContext): void => {
});

expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2);
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2); // name + version are both considered conflicts
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // name is considered as a conflict
expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0);

expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1);
Expand Down

0 comments on commit 106e946

Please sign in to comment.