Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug] Version bump with previous #1870

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 52 additions & 15 deletions detection_rules/version_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,59 +89,94 @@ def manage_versions(self, rules: RuleCollection,

verbose_echo('Rule changes detected!')

route = None
existing_rule_lock = {}
original_hash = None
changes = []

def add_changes(r, *msg):
if not original_hash or original_hash != current_rule_lock['sha256']:
new = [f' {route}: {r.id}, new version: {existing_rule_lock["version"]}']
new.extend([f' - {m}' for m in msg if m])
changes.extend(new)

for rule in rules:
if rule.contents.metadata.maturity == "production" or rule.id in newly_deprecated:
# assume that older stacks are always locked first
min_stack = _convert_lock_version(rule.contents.metadata.min_stack_version)

lock_info = rule.contents.lock_info(bump=not exclude_version_update)
current_rule_lock: dict = current_version_lock.setdefault(rule.id, {})
current_rule_lock = rule.contents.lock_info(bump=not exclude_version_update)
existing_rule_lock: dict = current_version_lock.setdefault(rule.id, {})
original_hash = existing_rule_lock.get('sha256')

# scenarios to handle, assuming older stacks are always locked first:
# 1) no breaking changes ever made or the first time a rule is created
# 2) on the latest, after a breaking change has been locked
# 3) on the latest stack, locking in a breaking change
# 4) on an old stack, after a breaking change has been made
latest_locked_stack_version = _convert_lock_version(current_rule_lock.get("min_stack_version"))
latest_locked_stack_version = _convert_lock_version(existing_rule_lock.get("min_stack_version"))

if not current_rule_lock or min_stack == latest_locked_stack_version:
if not existing_rule_lock or min_stack == latest_locked_stack_version:
route = 'A'
# 1) no breaking changes ever made or the first time a rule is created
# 2) on the latest, after a breaking change has been locked
current_rule_lock.update(lock_info)
existing_rule_lock.update(current_rule_lock)

# add the min_stack_version to the lock if it's explicitly set
log_msg = None
if rule.contents.metadata.min_stack_version is not None:
current_rule_lock["min_stack_version"] = str(min_stack)
existing_rule_lock["min_stack_version"] = str(min_stack)
log_msg = f'min_stack_version added: {min_stack}'

add_changes(rule, log_msg)

elif min_stack > latest_locked_stack_version:
route = 'B'
# 3) on the latest stack, locking in a breaking change
previous_lock_info = {
"rule_name": current_rule_lock["rule_name"],
"sha256": current_rule_lock["sha256"],
"version": current_rule_lock["version"],
"rule_name": existing_rule_lock["rule_name"],
"sha256": existing_rule_lock["sha256"],
"version": existing_rule_lock["version"],
}
current_rule_lock.setdefault("previous", {})
existing_rule_lock.setdefault("previous", {})

# move the current locked info into the previous section
current_rule_lock["previous"][str(latest_locked_stack_version)] = previous_lock_info
existing_rule_lock["previous"][str(latest_locked_stack_version)] = previous_lock_info

# overwrite the "latest" part of the lock at the top level
current_rule_lock.update(lock_info, min_stack_version=str(min_stack))
# TODO: would need to preserve space here as well if supporting forked version spacing
existing_rule_lock.update(current_rule_lock, min_stack_version=str(min_stack))
add_changes(
rule,
f'previous {latest_locked_stack_version} saved as version: {previous_lock_info["version"]}',
f'current min_stack updated to {min_stack}'
)

elif min_stack < latest_locked_stack_version:
# 4) on an old stack, after a breaking change has been made
assert str(min_stack) in current_rule_lock.get("previous", {}), \
route = 'C'
# 4) on an old stack, after a breaking change has been made (updated fork)
assert str(min_stack) in existing_rule_lock.get("previous", {}), \
f"Expected {rule.id} @ v{min_stack} in the rule lock"

# TODO: Figure out whether we support locking old versions and if we want to
# "leave room" by skipping versions when breaking changes are made.
# We can still inspect the version lock manually after locks are made,
# since it's a good summary of everything that happens
current_rule_lock["previous"][str(min_stack)] = lock_info
existing_rule_lock["previous"][str(min_stack)] = current_rule_lock
existing_rule_lock.update(current_rule_lock)
add_changes(rule, f'previous version {min_stack} updated version to {current_rule_lock["version"]}')
continue
else:
raise RuntimeError("Unreachable code")

if 'previous' in existing_rule_lock:
current_rule_version = rule.contents.lock_info()['version']
for min_stack_version, versioned_lock in existing_rule_lock['previous'].items():
existing_lock_version = versioned_lock['version']
if current_rule_version < existing_lock_version:
raise ValueError(f'{rule.id} - previous {min_stack_version=} {existing_lock_version=} '
f'has a higher version than {current_rule_version=}')

for rule in rules.deprecated:
if rule.id in newly_deprecated:
current_deprecated_lock[rule.id] = {
Expand All @@ -154,6 +189,8 @@ def manage_versions(self, rules: RuleCollection,
click.echo(f' - {len(changed_rules)} changed rules')
click.echo(f' - {len(new_rules)} new rules')
click.echo(f' - {len(newly_deprecated)} newly deprecated rules')
if changes:
click.echo('Detailed changes: \n' + '\n'.join(changes))

if not save_changes:
verbose_echo(
Expand Down
22 changes: 11 additions & 11 deletions etc/version.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -1393,7 +1393,7 @@
},
"rule_name": "Google Workspace Admin Role Assigned to a User",
"sha256": "afd34ab4f1d7e038c874333fd83de248c0b54d625f489e74359f3ce4ec9ac71b",
"version": 6
"version": 8
},
"689b9d57-e4d5-4357-ad17-9c334609d79a": {
"rule_name": "Scheduled Task Created by a Windows Script",
Expand Down Expand Up @@ -1497,7 +1497,7 @@
},
"rule_name": "Google Workspace Role Modified",
"sha256": "33a6f2e64d79ebfed4fe0f1b4e5c4a7968b9b4941e11fa0cf720ef3810e38a15",
"version": 6
"version": 8
},
"7024e2a0-315d-4334-bb1a-441c593e16ab": {
"rule_name": "AWS CloudTrail Log Deleted",
Expand Down Expand Up @@ -1611,7 +1611,7 @@
},
"rule_name": "Application Added to Google Workspace Domain",
"sha256": "ab5ac05b1f57b0e9a197d51506441eee921132528fde66e99b64021454556e71",
"version": 6
"version": 8
},
"7882cebf-6cf1-4de3-9662-213aa13e8b80": {
"rule_name": "Azure Privilege Identity Management Role Modified",
Expand Down Expand Up @@ -1941,7 +1941,7 @@
},
"rule_name": "Google Workspace Admin Role Deletion",
"sha256": "7f3e1672e2c15b1f4386242655493bbd483c0c30d377b65c94cadf17d5dbb100",
"version": 6
"version": 8
},
"93f47b6f-5728-4004-ba00-625083b3dcb0": {
"rule_name": "Modification of Standard Authentication Module or Configuration",
Expand Down Expand Up @@ -2235,7 +2235,7 @@
},
"rule_name": "Google Workspace Password Policy Modified",
"sha256": "7741aa9c38ba126329fbb075496847374a2dd8d65aadd49aa25b7f0f00e6aeb5",
"version": 7
"version": 9
},
"a9b05c3b-b304-4bf9-970d-acdfaef2944c": {
"rule_name": "Persistence via Hidden Run Key Detected",
Expand Down Expand Up @@ -2298,7 +2298,7 @@
},
"rule_name": "Google Workspace API Access Granted via Domain-Wide Delegation of Authority",
"sha256": "3d8eab60bf795ae6756c1c6058a7c1be2eb14e1c1777a7b4bda27e1906206c95",
"version": 6
"version": 8
},
"acd611f3-2b93-47b3-a0a3-7723bcc46f6d": {
"rule_name": "Potential Command and Control via Internet Explorer",
Expand Down Expand Up @@ -2331,7 +2331,7 @@
},
"rule_name": "Google Workspace Custom Admin Role Created",
"sha256": "72ff218857ba09e7c08970ebc6cdfcba3cd1dd4f0711dbd403b074fee911011c",
"version": 6
"version": 8
},
"ad84d445-b1ce-4377-82d9-7c633f28bf9a": {
"rule_name": "Suspicious Portable Executable Encoded in Powershell Script",
Expand Down Expand Up @@ -2756,7 +2756,7 @@
},
"rule_name": "Google Workspace MFA Enforcement Disabled",
"sha256": "de718fed93c2314061daddd300ddb5e01064210ddc42d687fcdd988aa2595d5a",
"version": 7
"version": 9
},
"cb71aa62-55c8-42f0-b0dd-afb0bb0b1f51": {
"rule_name": "Suspicious Calendar File Modification",
Expand Down Expand Up @@ -2834,7 +2834,7 @@
},
"rule_name": "Domain Added to Google Workspace Trusted Domains",
"sha256": "734ba85eb72a8c8167a1247c75d48bbd9abb0a9954f8a357a20017258da978de",
"version": 6
"version": 8
},
"cff92c41-2225-4763-b4ce-6f71e5bda5e6": {
"rule_name": "Execution from Unusual Directory - Command Line",
Expand Down Expand Up @@ -3064,7 +3064,7 @@
},
"rule_name": "Whitespace Padding in Process Command Line",
"sha256": "f182f841954adaa9009a1b62d0b98506f864adc4d7ab93e8467f26ada0f518d0",
"version": 4
"version": 6
},
"e0f36de1-0342-453d-95a9-a068b257b053": {
"rule_name": "Azure Event Hub Deletion",
Expand Down Expand Up @@ -3158,7 +3158,7 @@
},
"rule_name": "MFA Disabled for Google Workspace Organization",
"sha256": "aea30c3bf1eb96e0c6f0c64da484ca2310b1ae26e8679030c0a30a8058982a77",
"version": 7
"version": 9
},
"e56993d2-759c-4120-984c-9ec9bb940fd5": {
"rule_name": "RDP (Remote Desktop Protocol) to the Internet",
Expand Down