diff --git a/detection_rules/packaging.py b/detection_rules/packaging.py index e563d9ee1..a3bef733f 100644 --- a/detection_rules/packaging.py +++ b/detection_rules/packaging.py @@ -465,7 +465,7 @@ class Package(object): for rule in self.rules: summary_doc['rule_ids'].append(rule.id) summary_doc['rule_names'].append(rule.name) - summary_doc['rule_hashes'].append(rule.contents.sha256()) + summary_doc['rule_hashes'].append(rule.contents.get_hash()) if rule.id in self.new_ids: status = 'new' @@ -481,7 +481,7 @@ class Package(object): if relative_path is None: raise ValueError(f"Could not find a valid relative path for the rule: {rule.id}") - rule_doc = dict(hash=rule.contents.sha256(), + rule_doc = dict(hash=rule.contents.get_hash(), source='repo', datetime_uploaded=now, status=status, diff --git a/detection_rules/rule.py b/detection_rules/rule.py index 767b1872c..06c645d81 100644 --- a/detection_rules/rule.py +++ b/detection_rules/rule.py @@ -1010,18 +1010,25 @@ class BaseRuleContents(ABC): def lock_info(self, bump=True) -> dict: version = self.autobumped_version if bump else (self.saved_version or 1) - contents = {"rule_name": self.name, "sha256": self.sha256(), "version": version, "type": self.type} + contents = {"rule_name": self.name, "sha256": self.get_hash(), "version": version, "type": self.type} return contents @property - def is_dirty(self) -> Optional[bool]: + def is_dirty(self) -> bool: """Determine if the rule has changed since its version was locked.""" min_stack = Version.parse(self.get_supported_version(), optional_minor_and_patch=True) existing_sha256 = self.version_lock.get_locked_hash(self.id, f"{min_stack.major}.{min_stack.minor}") - if existing_sha256 is not None: - return existing_sha256 != self.sha256() + if not existing_sha256: + return False + + rule_hash = self.get_hash() + rule_hash_with_integrations = self.get_hash(include_integrations=True) + + # Checking against current and previous version of the hash to avoid mass version bump + is_dirty = existing_sha256 not in (rule_hash, rule_hash_with_integrations) + return is_dirty @property def lock_entry(self) -> Optional[dict]: @@ -1123,10 +1130,25 @@ class BaseRuleContents(ABC): def to_api_format(self, include_version: bool = True) -> dict: """Convert the rule to the API format.""" + def get_hashable_content(self, include_version: bool = False, include_integrations: bool = False) -> dict: + """Returns the rule content to be used for calculating the hash value for the rule""" + + # get the API dict without the version by default, otherwise it'll always be dirty. + hashable_dict = self.to_api_format(include_version=include_version) + + # drop related integrations if present + if not include_integrations: + hashable_dict.pop("related_integrations", None) + + return hashable_dict + @cached - def sha256(self, include_version=False) -> str: - # get the hash of the API dict without the version by default, otherwise it'll always be dirty. - hashable_contents = self.to_api_format(include_version=include_version) + def get_hash(self, include_version: bool = False, include_integrations: bool = False) -> str: + """Returns a sha256 hash of the rule contents""" + hashable_contents = self.get_hashable_content( + include_version=include_version, + include_integrations=include_integrations, + ) return utils.dict_hash(hashable_contents) diff --git a/pyproject.toml b/pyproject.toml index 6ecf29ec8..25bd340c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "detection_rules" -version = "1.1.2" +version = "1.1.3" description = "Detection Rules is the home for rules used by Elastic Security. This repository is used for the development, maintenance, testing, validation, and release of rules for Elastic Security’s Detection Engine." readme = "README.md" requires-python = ">=3.12" diff --git a/tests/test_packages.py b/tests/test_packages.py index 319b78881..e0adab2fa 100644 --- a/tests/test_packages.py +++ b/tests/test_packages.py @@ -43,7 +43,7 @@ class TestPackages(BaseRuleTest): version_info = { rule.id: { 'rule_name': rule.name, - 'sha256': rule.contents.sha256(), + 'sha256': rule.contents.get_hash(), 'version': version } for rule in rules } @@ -76,7 +76,7 @@ class TestPackages(BaseRuleTest): # test that no rules have versions defined for rule in rules: self.assertGreaterEqual(rule.contents.autobumped_version, 1, '{} - {}: version is not being set in package') - original_hashes.append(rule.contents.sha256()) + original_hashes.append(rule.contents.get_hash()) package = Package(rules, 'test-package') @@ -87,7 +87,7 @@ class TestPackages(BaseRuleTest): # test that rules validate with version for rule in package.rules: - post_bump_hashes.append(rule.contents.sha256()) + post_bump_hashes.append(rule.contents.get_hash()) # test that no hashes changed as a result of the version bumps self.assertListEqual(original_hashes, post_bump_hashes, 'Version bumping modified the hash of a rule')