Skip to content

Commit

Permalink
[Bug] Version bump with previous (#1870)
Browse files Browse the repository at this point in the history
* save changes to top level for route C; verbose prints
* update top level on forked rule without overriding min_stack_version
* add check to ensure previous version !> current

(cherry picked from commit f4c94af)
  • Loading branch information
brokensound77 authored and github-actions[bot] committed Mar 24, 2022
1 parent c8830ee commit 5fc8d10
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 26 deletions.
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

0 comments on commit 5fc8d10

Please sign in to comment.