Skip to content

[FR] Adapt PyPi semver Library and Remove Custom #2503

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
53023fd
removed custom semver and replaced with pypi
terrancedejesus Jan 28, 2023
b715e48
updated beats.py version references
terrancedejesus Jan 28, 2023
273a6ab
updated bump-versions CLI command to use semver and change logic
terrancedejesus Jan 28, 2023
3b79faa
updated schemas __init__, test_version_lock and unstage incompatible …
terrancedejesus Jan 28, 2023
ed82c8c
updated test_stack_schema_map in TestVersions unittest
terrancedejesus Jan 29, 2023
5239ff7
updated test_all_rules unit testing Version() references
terrancedejesus Jan 29, 2023
22c6cd1
updated stack_compat.py for get_restricted_field references)
terrancedejesus Jan 29, 2023
9391f64
updated version_lock.py Version() references
terrancedejesus Jan 29, 2023
0ba644f
updated docs.py Version() reference for parse_registry
terrancedejesus Jan 29, 2023
532c156
updated devtools.py Version() reference for trim-version-lock
terrancedejesus Jan 29, 2023
0888b65
updated mixins.py Version() reference in validate_field_compatibility
terrancedejesus Jan 29, 2023
77960b1
adjusted schemas.__init__ Version() reference in get_stack_schemas
terrancedejesus Jan 29, 2023
352dee0
adjusted ecs.py Version() references
terrancedejesus Jan 29, 2023
81dfe38
adjusted integrations.py Version() references
terrancedejesus Jan 29, 2023
397c76e
adjusted rule.py Version() references
terrancedejesus Jan 29, 2023
72d88b9
sorted imports
terrancedejesus Jan 29, 2023
386a913
replaced custom semver with pypi semver in unit test files
terrancedejesus Jan 29, 2023
0d09298
addressed unit test and flake errors
terrancedejesus Jan 30, 2023
774e258
Merge branch 'main' into 2502-fr-adapt-pypi-semver-library-and-remove…
terrancedejesus Jan 30, 2023
2b2fbe5
changed semver strings casted to version_lock.py
terrancedejesus Jan 31, 2023
9454d0d
fixed sorting in integrations.py
terrancedejesus Feb 1, 2023
22aed7d
Merge branch 'main' into 2502-fr-adapt-pypi-semver-library-and-remove…
terrancedejesus Feb 1, 2023
90898eb
Merge branch 'main' into 2502-fr-adapt-pypi-semver-library-and-remove…
terrancedejesus Feb 2, 2023
7606932
updated bump-pkgs-versions CLI command
terrancedejesus Feb 2, 2023
11207a6
merging in changes from main and resolving conflicts
terrancedejesus Feb 6, 2023
d140209
adjusted semantic version in unstage-incompatible-rules command
terrancedejesus Feb 6, 2023
880130a
adjusted semver import to VersionInfo
terrancedejesus Feb 6, 2023
09eb6cd
added semver 3 and adjusted import names
terrancedejesus Feb 6, 2023
6e351e8
added option_minor_and_patch parameter where version is major.minor
terrancedejesus Feb 6, 2023
346156e
updated bump-pkg-versions to always save to packages.yml
terrancedejesus Feb 6, 2023
f11c663
removed leftover split call & updated find latest compatible version …
terrancedejesus Feb 6, 2023
e0ef91e
updated integrations.py, version_lock.py and schemas.__init__.py
terrancedejesus Feb 6, 2023
06a450a
changed fstring reference in downgrade function
terrancedejesus Feb 6, 2023
f578bb9
Merge branch 'main' into 2502-fr-adapt-pypi-semver-library-and-remove…
terrancedejesus Feb 7, 2023
7f1afb7
reverted formatting changes for detection_rules __init__.py
terrancedejesus Feb 7, 2023
0d91e6f
added newline to detection_rules __init__.py
terrancedejesus Feb 7, 2023
8a4eb0a
adjusted finding latest_release for attack package logic
terrancedejesus Feb 7, 2023
4aa700e
adjusted unstage-incompatible-rules command logic comparing versions
terrancedejesus Feb 7, 2023
d63abe6
Merge branch 'main' into 2502-fr-adapt-pypi-semver-library-and-remove…
terrancedejesus Feb 7, 2023
3ee7fab
removing changes from misc.py related to auto-formatting
terrancedejesus Feb 7, 2023
0a8222d
adding newline to misc.py
terrancedejesus Feb 7, 2023
a694ceb
fixed bug in downgrade function calling decorators
terrancedejesus Feb 7, 2023
82ec97c
added semantic version validation on migrate decorator function
terrancedejesus Feb 7, 2023
49ae652
added expected type returned from find_latest_integration_version in …
terrancedejesus Feb 7, 2023
b9bba39
add comment about stripped versions for version lock file
terrancedejesus Feb 7, 2023
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
11 changes: 6 additions & 5 deletions detection_rules/attack.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import requests
from collections import OrderedDict

from .semver import Version
from semver import Version
from .utils import cached, clear_caches, get_etc_path, get_etc_glob_path, read_gzip, gzip_compress

PLATFORMS = ['Windows', 'macOS', 'Linux']
Expand Down Expand Up @@ -105,16 +105,17 @@ def get_version_from_tag(name, pattern='att&ck-v'):
_, version = name.lower().split(pattern, 1)
return version

current_version = get_version_from_tag(filename, 'attack-v')
current_version = Version.parse(get_version_from_tag(filename, 'attack-v'), optional_minor_and_patch=True)

r = requests.get('https://api.github.com/repos/mitre/cti/tags')
r.raise_for_status()
releases = [t for t in r.json() if t['name'].startswith('ATT&CK-v')]
latest_release = max(releases, key=lambda release: Version(get_version_from_tag(release['name'])))
latest_release = max(releases, key=lambda release: Version.parse(get_version_from_tag(release['name']),
optional_minor_and_patch=True))
release_name = latest_release['name']
latest_version = get_version_from_tag(release_name)
latest_version = Version.parse(get_version_from_tag(release_name), optional_minor_and_patch=True)

if Version(current_version) >= Version(latest_version):
if current_version >= latest_version:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this code change tested?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

detection-rules on  2502-fr-adapt-pypi-semver-library-and-remove-custom [✘!?] is 📦 v0.1.0 via 🐍 v3.9.15 (detection-rules-dev) on ☁️  [email protected] 
❯ python -m detection_rules dev attack refresh-data                                                                   

█▀▀▄ ▄▄▄ ▄▄▄ ▄▄▄ ▄▄▄ ▄▄▄ ▄▄▄ ▄▄▄ ▄   ▄      █▀▀▄ ▄  ▄ ▄   ▄▄▄ ▄▄▄
█  █ █▄▄  █  █▄▄ █    █   █  █ █ █▀▄ █      █▄▄▀ █  █ █   █▄▄ █▄▄
█▄▄▀ █▄▄  █  █▄▄ █▄▄  █  ▄█▄ █▄█ █ ▀▄█      █ ▀▄ █▄▄█ █▄▄ █▄▄ ▄▄█

Replaced file: /Users/tdejesus/code/src/detection-rules/detection_rules/etc/attack-v11.1.json.gz with /Users/tdejesus/code/src/detection-rules/detection_rules/etc/attack-v12.1.0.json.gz

detection-rules on  2502-fr-adapt-pypi-semver-library-and-remove-custom [✘!?] is 📦 v0.1.0 via 🐍 v3.9.15 (detection-rules-dev) on ☁️  [email protected] took 5s 
❯ python -m detection_rules dev attack refresh-data

█▀▀▄ ▄▄▄ ▄▄▄ ▄▄▄ ▄▄▄ ▄▄▄ ▄▄▄ ▄▄▄ ▄   ▄      █▀▀▄ ▄  ▄ ▄   ▄▄▄ ▄▄▄
█  █ █▄▄  █  █▄▄ █    █   █  █ █ █▀▄ █      █▄▄▀ █  █ █   █▄▄ █▄▄
█▄▄▀ █▄▄  █  █▄▄ █▄▄  █  ▄█▄ █▄█ █ ▀▄█      █ ▀▄ █▄▄█ █▄▄ █▄▄ ▄▄█

No versions newer than the current detected: 12.1.0

Tested after renaming file to a lower version and running the refresh-data command.

print(f'No versions newer than the current detected: {current_version}')
return None, None

Expand Down
16 changes: 9 additions & 7 deletions detection_rules/beats.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@
# 2.0.

"""ECS Schemas management."""
import json
import os
import re
from typing import List, Optional

import kql
import eql
import json
import requests
from semver import Version
import yaml

from .semver import Version
from .utils import DateTimeEncoder, unzip, get_etc_path, gzip_compress, read_gzip, cached
import kql

from .utils import (DateTimeEncoder, cached, get_etc_path, gzip_compress,
read_gzip, unzip)


def _decompress_and_save_schema(url, release_name):
Expand Down Expand Up @@ -91,7 +93,7 @@ def download_latest_beats_schema():
url = 'https://api.github.com/repos/elastic/beats/releases'
releases = requests.get(url)

latest_release = max(releases.json(), key=lambda release: Version(release["tag_name"].lstrip("v")))
latest_release = max(releases.json(), key=lambda release: Version.parse(release["tag_name"].lstrip("v")))
download_beats_schema(latest_release["tag_name"])


Expand Down Expand Up @@ -198,7 +200,7 @@ def get_versions() -> List[Version]:
for filename in os.listdir(get_etc_path("beats_schemas")):
version_match = re.match(r'v(.+)\.json\.gz', filename)
if version_match:
versions.append(Version(version_match.groups()[0]))
versions.append(Version.parse(version_match.groups()[0]))

return versions

Expand All @@ -213,7 +215,7 @@ def read_beats_schema(version: str = None):
if version and version.lower() == 'main':
return json.loads(read_gzip(get_etc_path('beats_schemas', 'main.json.gz')))

version = Version(version) if version else None
version = Version.parse(version) if version else None
beats_schemas = get_versions()

if version and version not in beats_schemas:
Expand Down
92 changes: 50 additions & 42 deletions detection_rules/devtools.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import click
import requests.exceptions
from semver import Version
import yaml
from elasticsearch import Elasticsearch
from eql.table import Table
Expand All @@ -33,7 +34,10 @@
from .endgame import EndgameSchemaManager
from .eswrap import CollectEvents, add_range_to_dsl
from .ghwrap import GithubClient, update_gist
from .integrations import (build_integrations_manifest, build_integrations_schemas, find_latest_compatible_version,
from .integrations import (build_integrations_manifest,
build_integrations_schemas,
find_latest_compatible_version,
find_latest_integration_version,
load_integrations_manifests)
from .main import root
from .misc import PYTHON_LICENSE, add_client, client_error
Expand All @@ -43,9 +47,8 @@
ThreatMapping, TOMLRule)
from .rule_loader import RuleCollection, production_filter
from .schemas import definitions, get_stack_versions
from .semver import Version
from .utils import (dict_hash, get_etc_path, get_path, load_dump, save_etc_dump,
load_etc_dump)
from .utils import (dict_hash, get_etc_path, get_path, load_dump,
load_etc_dump, save_etc_dump)
from .version_lock import VersionLockFile, default_version_lock

RULES_DIR = get_path('rules')
Expand Down Expand Up @@ -152,43 +155,45 @@ def build_integration_docs(ctx: click.Context, registry_version: str, pre: str,
return docs


@dev_group.command("bump-versions")
@click.option("--major", is_flag=True, help="bump the major version")
@click.option("--minor", is_flag=True, help="bump the minor version")
@click.option("--patch", is_flag=True, help="bump the patch version")
@click.option("--package", is_flag=True, help="Update the package version in the packages.yml file")
@click.option("--kibana", is_flag=True, help="Update the kibana version in the packages.yml file")
@click.option("--registry", is_flag=True, help="Update the registry version in the packages.yml file")
def bump_versions(major, minor, patch, package, kibana, registry):
@dev_group.command("bump-pkg-versions")
@click.option("--major-release", is_flag=True, help="bump the major version")
@click.option("--minor-release", is_flag=True, help="bump the minor version")
@click.option("--patch-release", is_flag=True, help="bump the patch version")
@click.option("--maturity", type=click.Choice(['beta', 'ga'], case_sensitive=False),
required=True, help="beta or production versions")
def bump_versions(major_release: bool, minor_release: bool, patch_release: bool, maturity: str):
"""Bump the versions"""

package_data = load_etc_dump('packages.yml')['package']
ver = package_data["name"]
new_version = Version(ver).bump(major, minor, patch)

kibana_version = f"^{new_version}.0" if not patch else f"^{new_version}"
registry_version = f"{new_version}.0-dev.0" if not patch else f"{new_version}-dev.0"

# print the new versions
click.echo(f"New package version: {new_version}")
click.echo(f"New registry data version: {registry_version}")
click.echo(f"New Kibana version: {kibana_version}")

if package:
# update package version
package_data["name"] = str(new_version)

if kibana:
# update kibana version
package_data["registry_data"]["conditions"]["kibana.version"] = kibana_version
pkg_data = load_etc_dump('packages.yml')['package']
kibana_ver = Version.parse(pkg_data["name"], optional_minor_and_patch=True)
pkg_ver = Version.parse(pkg_data["registry_data"]["version"])
pkg_kibana_ver = Version.parse(pkg_data["registry_data"]["conditions"]["kibana.version"].lstrip("^"))
if major_release:
pkg_data["name"] = str(kibana_ver.bump_major()).rstrip(".0")
pkg_data["registry_data"]["conditions"]["kibana.version"] = f"^{pkg_kibana_ver.bump_major()}"
pkg_data["registry_data"]["version"] = str(pkg_ver.bump_major().bump_prerelease("beta"))
if minor_release:
pkg_data["name"] = str(kibana_ver.bump_minor()).rstrip(".0")
pkg_data["registry_data"]["conditions"]["kibana.version"] = f"^{pkg_kibana_ver.bump_minor()}"
pkg_data["registry_data"]["version"] = str(pkg_ver.bump_minor().bump_prerelease("beta"))
pkg_data["registry_data"]["release"] = maturity
if patch_release:
latest_patch_release_ver = find_latest_integration_version("security_detection_engine",
maturity, pkg_data["name"])
if maturity == "ga":
pkg_data["registry_data"]["version"] = str(latest_patch_release_ver.bump_patch())
pkg_data["registry_data"]["release"] = maturity
else:
pkg_data["registry_data"]["version"] = str(latest_patch_release_ver.bump_prerelease("beta"))
pkg_data["registry_data"]["release"] = maturity

if registry:
# update registry version
package_data["registry_data"]["version"] = registry_version
# update packages.yml
click.echo(f"Kibana version: {pkg_data['name']}")
click.echo(f"Package Kibana version: {pkg_data['registry_data']['conditions']['kibana.version']}")
click.echo(f"Package version: {pkg_data['registry_data']['version']}")

if package or kibana or registry:
save_etc_dump({"package": package_data}, "packages.yml")
# we only save major and minor version bumps
# patch version bumps are OOB packages and thus we keep the base versioning
save_etc_dump({"package": pkg_data}, "packages.yml")


@dataclasses.dataclass
Expand Down Expand Up @@ -249,7 +254,7 @@ def prune_staging_area(target_stack_version: str, dry_run: bool, exception_list:
}
exceptions.update(exception_list.split(","))

target_stack_version = Version(target_stack_version)[:2]
target_stack_version = Version.parse(target_stack_version, optional_minor_and_patch=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these ignored patch - how does this change handle that?

Copy link
Contributor Author

@terrancedejesus terrancedejesus Feb 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great question as this command is important to the backporting workflow.

The optional_minor_and_patch flag is only to give it a default patch of 0. target_stack_version from the backport workflow command relies on dynamically generated target-branches.yml file which ultimately loads from stack-schema-map.yaml where the supported stack versions are full semantic versioned. Along the way, these versions are spliced to remove patch, but ultimately the comparison will be full semantic version < full semantic version where patch will always be 0, unless we manually run this command and provide a different target-stack-version value. So it should be fine for backporting purposes.


# load a structured summary of the diff from git
git_output = subprocess.check_output(["git", "diff", "--name-status", "HEAD"])
Expand All @@ -270,7 +275,8 @@ def prune_staging_area(target_stack_version: str, dry_run: bool, exception_list:
dict_contents = RuleCollection.deserialize_toml_string(change.read())
min_stack_version: Optional[str] = dict_contents.get("metadata", {}).get("min_stack_version")

if min_stack_version is not None and target_stack_version < Version(min_stack_version)[:2]:
if min_stack_version is not None and \
(target_stack_version < Version.parse(min_stack_version, optional_minor_and_patch=True)):
# rule is incompatible, add to the list of reversions to make later
reversions.append(change)

Expand Down Expand Up @@ -896,13 +902,13 @@ def trim_version_lock(min_version: str, dry_run: bool):
stack_versions = get_stack_versions()
assert min_version in stack_versions, f'Unknown min_version ({min_version}), expected: {", ".join(stack_versions)}'

min_version = Version(min_version)
min_version = Version.parse(min_version)
version_lock_dict = default_version_lock.version_lock.to_dict()
removed = {}

for rule_id, lock in version_lock_dict.items():
if 'previous' in lock:
prev_vers = [Version(v) for v in list(lock['previous'])]
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:
Expand Down Expand Up @@ -1212,7 +1218,9 @@ def show_latest_compatible_version(package: str, stack_version: str) -> None:
return

try:
version = find_latest_compatible_version(package, "", stack_version, packages_manifest)
version = find_latest_compatible_version(package, "",
Version.parse(stack_version, optional_minor_and_patch=True),
packages_manifest)
click.echo(f"Compatible integration {version=}")
except Exception as e:
click.echo(f"Error finding compatible version: {str(e)}")
Expand Down
8 changes: 4 additions & 4 deletions detection_rules/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

"""Create summary documents for a rule package."""
import itertools
import json
import re
import shutil
import textwrap
Expand All @@ -13,14 +14,13 @@
from pathlib import Path
from typing import Dict, Iterable, Optional, Union

import json
from semver import Version
import xlsxwriter

from .attack import attack_tm, matrix, tactics, technique_lookup
from .packaging import Package
from .rule_loader import DeprecatedCollection, RuleCollection
from .rule import ThreatMapping, TOMLRule
from .semver import Version
from .rule_loader import DeprecatedCollection, RuleCollection


class PackageDocument(xlsxwriter.Workbook):
Expand Down Expand Up @@ -304,7 +304,7 @@ def __init__(self, registry_version: str, directory: Path, overwrite=False,

@staticmethod
def parse_registry(registry_version: str) -> (str, str, str):
registry_version = Version(registry_version)
registry_version = Version.parse(registry_version)
short_registry_version = [str(n) for n in registry_version[:3]]
registry_version_str = '.'.join(short_registry_version)
base_name = "-".join(short_registry_version)
Expand Down
17 changes: 9 additions & 8 deletions detection_rules/ecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@
"""ECS Schemas management."""
import copy
import glob
import json
import os
import shutil
import json
from pathlib import Path

import requests
import eql
import eql.types
import requests
from semver import Version
import yaml

from .semver import Version
from .utils import DateTimeEncoder, cached, load_etc_dump, get_etc_path, gzip_compress, read_gzip, unzip
from .utils import (DateTimeEncoder, cached, get_etc_path, gzip_compress,
load_etc_dump, read_gzip, unzip)

ETC_NAME = "ecs_schemas"
ECS_SCHEMAS_DIR = get_etc_path(ETC_NAME)
Expand Down Expand Up @@ -87,7 +88,7 @@ def get_max_version(include_master=False):
if include_master and any([v.startswith('master') for v in versions]):
return list(Path(ECS_SCHEMAS_DIR).glob('master*'))[0].name

return str(max([Version(v) for v in versions if not v.startswith('master')]))
return str(max([Version.parse(v) for v in versions if not v.startswith('master')]))


@cached
Expand Down Expand Up @@ -205,12 +206,12 @@ def get_kql_schema(version=None, indexes=None, beat_schema=None) -> dict:

def download_schemas(refresh_master=True, refresh_all=False, verbose=True):
"""Download additional schemas from ecs releases."""
existing = [Version(v) for v in get_schema_map()] if not refresh_all else []
existing = [Version.parse(v) for v in get_schema_map()] if not refresh_all else []
url = 'https://api.github.com/repos/elastic/ecs/releases'
releases = requests.get(url)

for release in releases.json():
version = Version(release.get('tag_name', '').lstrip('v'))
version = Version.parse(release.get('tag_name', '').lstrip('v'))

# we don't ever want beta
if not version or version < (1, 0, 1) or version in existing:
Expand Down Expand Up @@ -247,7 +248,7 @@ def download_schemas(refresh_master=True, refresh_all=False, verbose=True):
# handle working master separately
if refresh_master:
master_ver = requests.get('https://raw.githubusercontent.com/elastic/ecs/master/version')
master_ver = Version(master_ver.text.strip())
master_ver = Version.parse(master_ver.text.strip())
master_schema = requests.get('https://raw.githubusercontent.com/elastic/ecs/master/generated/ecs/ecs_flat.yml')
master_schema = yaml.safe_load(master_schema.text)

Expand Down
Loading