def trim_version_lock()

in detection_rules/devtools.py [0:0]


def trim_version_lock(ctx: click.Context, stack_version: str, skip_rule_updates: bool, dry_run: bool):
    """Trim all previous entries within the version lock file which are lower than the min_version."""
    stack_versions = get_stack_versions()
    assert stack_version in stack_versions, \
        f'Unknown min_version ({stack_version}), expected: {", ".join(stack_versions)}'

    min_version = Version.parse(stack_version)

    if RULES_CONFIG.bypass_version_lock:
        click.echo('WARNING: Cannot trim the version lock when the versioning strategy is configured to bypass the '
                   'version lock. Set `bypass_version_lock` to `false` in the rules config to use the version lock.')
        ctx.exit()
    version_lock_dict = loaded_version_lock.version_lock.to_dict()
    removed = defaultdict(list)
    rule_msv_drops = []

    today = time.strftime('%Y/%m/%d')
    rc: RuleCollection | None = None
    if dry_run:
        rc = RuleCollection()
    else:
        if not skip_rule_updates:
            click.echo('Loading rules ...')
            rc = RuleCollection.default()

    for rule_id, lock in version_lock_dict.items():
        file_min_stack: Version | None = None
        if 'min_stack_version' in lock:
            file_min_stack = Version.parse((lock['min_stack_version']), optional_minor_and_patch=True)
            if file_min_stack <= min_version:
                removed[rule_id].append(
                    f'locked min_stack_version <= {min_version} - {"will remove" if dry_run else "removing"}!'
                )
                rule_msv_drops.append(rule_id)
                file_min_stack = None

                if not dry_run:
                    lock.pop('min_stack_version')
                    if not skip_rule_updates:
                        # remove the min_stack_version and min_stack_comments from rules as well (and update date)
                        rule = rc.id_map.get(rule_id)
                        if rule:
                            new_meta = dataclasses.replace(
                                rule.contents.metadata,
                                updated_date=today,
                                min_stack_version=None,
                                min_stack_comments=None
                            )
                            contents = dataclasses.replace(rule.contents, metadata=new_meta)
                            new_rule = TOMLRule(contents=contents, path=rule.path)
                            new_rule.save_toml()
                            removed[rule_id].append('rule min_stack_version dropped')
                        else:
                            removed[rule_id].append('rule not found to update!')

        if 'previous' in lock:
            prev_vers = [Version.parse(v, optional_minor_and_patch=True) for v in list(lock['previous'])]
            outdated_vers = [v for v in prev_vers if v < min_version]

            if not outdated_vers:
                continue

            # we want to remove all "old" versions, but save the latest that is >= the min version supplied as the new
            # stack_version.
            latest_version = max(outdated_vers)

            for outdated in outdated_vers:
                short_outdated = f"{outdated.major}.{outdated.minor}"
                popped = lock['previous'].pop(str(short_outdated))
                # the core of the update - we only need to keep previous entries that are newer than the min supported
                # version (from stack-schema-map and stack-version parameter) and older than the locked
                # min_stack_version for a given rule, if one exists
                if file_min_stack and outdated == latest_version and outdated < file_min_stack:
                    lock['previous'][f'{min_version.major}.{min_version.minor}'] = popped
                    removed[rule_id].append(f'{short_outdated} updated to: {min_version.major}.{min_version.minor}')
                else:
                    removed[rule_id].append(f'{outdated} dropped')

            # remove the whole previous entry if it is now blank
            if not lock['previous']:
                lock.pop('previous')

    click.echo(f'Changes {"that will be " if dry_run else ""} applied:' if removed else 'No changes')
    click.echo('\n'.join(f'{k}: {", ".join(v)}' for k, v in removed.items()))
    if not dry_run:
        new_lock = VersionLockFile.from_dict(dict(data=version_lock_dict))
        new_lock.save_to_file()