-
Notifications
You must be signed in to change notification settings - Fork 646
Generate an integrations package from a release #983
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
Changes from all commits
52db8e2
b3676ba
3f58332
61ec159
0c541fc
4117d74
4546bfc
1756c94
9766145
49b5c7e
4ee618d
c2e22d1
cbc569a
d2c90f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,9 +12,10 @@ | |
| import shutil | ||
| from collections import defaultdict, OrderedDict | ||
| from pathlib import Path | ||
| from typing import List, Tuple | ||
| from typing import List, Optional, Tuple | ||
|
|
||
| import click | ||
| import yaml | ||
|
|
||
| from . import rule_loader | ||
| from .misc import JS_LICENSE, cached | ||
|
|
@@ -24,6 +25,7 @@ | |
| RELEASE_DIR = get_path("releases") | ||
| PACKAGE_FILE = get_etc_path('packages.yml') | ||
| NOTICE_FILE = get_path('NOTICE.txt') | ||
| # CHANGELOG_FILE = Path(get_etc_path('rules-changelog.json')) | ||
|
|
||
|
|
||
| def filter_rule(rule: Rule, config_filter: dict, exclude_fields: dict) -> bool: | ||
|
|
@@ -150,13 +152,17 @@ def manage_versions(rules: list, deprecated_rules: list = None, current_versions | |
| class Package(object): | ||
| """Packaging object for siem rules and releases.""" | ||
|
|
||
| def __init__(self, rules, name, deprecated_rules=None, release=False, current_versions=None, min_version=None, | ||
| max_version=None, update_version_lock=False, verbose=True): | ||
| def __init__(self, rules: List[Rule], name: str, deprecated_rules: Optional[List[Rule]] = None, | ||
| release: Optional[bool] = False, current_versions: Optional[dict] = None, | ||
| min_version: Optional[int] = None, max_version: Optional[int] = None, | ||
| update_version_lock: Optional[bool] = False, registry_data: Optional[dict] = None, | ||
| verbose: Optional[bool] = True): | ||
| """Initialize a package.""" | ||
| self.rules: List[Rule] = [r.copy() for r in rules] | ||
| self.rules = [r.copy() for r in rules] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like type inspection has figured it out. I think we could annotate the |
||
| self.name = name | ||
| self.deprecated_rules: List[Rule] = [r.copy() for r in deprecated_rules or []] | ||
| self.deprecated_rules = [r.copy() for r in deprecated_rules or []] | ||
| self.release = release | ||
| self.registry_data = registry_data or {} | ||
|
|
||
| self.changed_rule_ids, self.new_rules_ids, self.removed_rule_ids = self._add_versions(current_versions, | ||
| update_version_lock, | ||
|
|
@@ -171,6 +177,11 @@ def _add_versions(self, current_versions, update_versions_lock=False, verbose=Tr | |
| return manage_versions(self.rules, deprecated_rules=self.deprecated_rules, current_versions=current_versions, | ||
| save_changes=update_versions_lock, verbose=verbose) | ||
|
|
||
| @classmethod | ||
| def load_configs(cls): | ||
| """Load configs from packages.yml.""" | ||
| return load_etc_dump(PACKAGE_FILE)['package'] | ||
|
|
||
| @staticmethod | ||
| def _package_kibana_notice_file(save_dir): | ||
| """Convert and save notice file with package.""" | ||
|
|
@@ -257,6 +268,7 @@ def save(self, verbose=True): | |
| self._package_kibana_index_file(rules_dir) | ||
|
|
||
| if self.release: | ||
| self._generate_registry_package(save_dir) | ||
| self.save_release_files(extras_dir, self.changed_rule_ids, self.new_rules_ids, self.removed_rule_ids) | ||
|
|
||
| # zip all rules only and place in extras | ||
|
|
@@ -461,6 +473,34 @@ def generate_xslx(self, path): | |
| doc.populate() | ||
| doc.close() | ||
|
|
||
| def _generate_registry_package(self, save_dir): | ||
| """Generate the artifact for the oob package-storage.""" | ||
| from .schemas.registry_package import RegistryPackageManifest | ||
|
|
||
| manifest = RegistryPackageManifest.from_dict(self.registry_data) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would make more sense to give this class an already assembled RegistryPackageManifest, but we can do this later with a restructur |
||
|
|
||
| package_dir = Path(save_dir).joinpath(manifest.version) | ||
| docs_dir = package_dir / 'docs' | ||
| rules_dir = package_dir / 'kibana' / 'rules' | ||
|
|
||
| docs_dir.mkdir(parents=True) | ||
| rules_dir.mkdir(parents=True) | ||
|
brokensound77 marked this conversation as resolved.
|
||
|
|
||
| manifest_file = package_dir.joinpath('manifest.yml') | ||
| readme_file = docs_dir.joinpath('README.md') | ||
|
|
||
| manifest_file.write_text(yaml.safe_dump(manifest.asdict())) | ||
| # shutil.copyfile(CHANGELOG_FILE, str(rules_dir.joinpath('CHANGELOG.json'))) | ||
|
|
||
| for rule in self.rules: | ||
| rule.save(new_path=str(rules_dir.joinpath(f'rule-{rule.id}.json'))) | ||
|
|
||
| readme_text = ('# Detection rules\n\n' | ||
| 'The detection rules package stores all the security rules ' | ||
| 'for the detection engine within the Elastic Security application.\n\n') | ||
|
|
||
| readme_file.write_text(readme_text) | ||
|
|
||
| def bump_versions(self, save_changes=False, current_versions=None): | ||
| """Bump the versions of all production rules included in a release and optionally save changes.""" | ||
| return manage_versions(self.rules, current_versions=current_versions, save_changes=save_changes) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| # or more contributor license agreements. Licensed under the Elastic License | ||
| # 2.0; you may not use this file except in compliance with the Elastic License | ||
| # 2.0. | ||
|
|
||
| """Custom shared definitions for schemas.""" | ||
|
brokensound77 marked this conversation as resolved.
|
||
|
|
||
| from typing import ClassVar, Type | ||
|
|
||
| import marshmallow | ||
| import marshmallow_dataclass | ||
| from marshmallow_dataclass import NewType | ||
| from marshmallow import validate | ||
|
|
||
|
|
||
| DATE_PATTERN = r'\d{4}/\d{2}/\d{2}' | ||
| MATURITY_LEVELS = ['development', 'experimental', 'beta', 'production', 'deprecated'] | ||
| OS_OPTIONS = ['windows', 'linux', 'macos', 'solaris'] | ||
| PR_PATTERN = r'^$|\d+' | ||
| SHA256_PATTERN = r'[a-fA-F0-9]{64}' | ||
| UUID_PATTERN = r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | ||
|
|
||
| _version = r'\d+\.\d+(\.\d+[\w-]*)*' | ||
| CONDITION_VERSION_PATTERN = rf'^\^{_version}$' | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think we'll loosen this up as we go. might move from
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good to know - can always expand it here |
||
| VERSION_PATTERN = f'^{_version}$' | ||
| BRANCH_PATTERN = f'{VERSION_PATTERN}|^master$' | ||
|
|
||
| ConditionSemVer = NewType('ConditionSemVer', str, validate=validate.Regexp(CONDITION_VERSION_PATTERN)) | ||
| Date = NewType('Date', str, validate=validate.Regexp(DATE_PATTERN)) | ||
|
rw-access marked this conversation as resolved.
|
||
| SemVer = NewType('SemVer', str, validate=validate.Regexp(VERSION_PATTERN)) | ||
| Sha256 = NewType('Sha256', str, validate=validate.Regexp(SHA256_PATTERN)) | ||
| UUIDString = NewType('UUIDString', str, validate=validate.Regexp(UUID_PATTERN)) | ||
|
|
||
|
|
||
| @marshmallow_dataclass.dataclass | ||
| class BaseMarshmallowDataclass: | ||
| """Base marshmallow dataclass configs.""" | ||
|
|
||
| class Meta: | ||
| ordered = True | ||
|
|
||
| Schema: ClassVar[Type[marshmallow.Schema]] = marshmallow.Schema | ||
|
|
||
| def dump(self) -> dict: | ||
| return self.Schema().dump(self) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| # or more contributor license agreements. Licensed under the Elastic License | ||
| # 2.0; you may not use this file except in compliance with the Elastic License | ||
| # 2.0. | ||
|
|
||
| """Definitions for packages destined for the registry.""" | ||
|
|
||
| from dataclasses import dataclass, field | ||
| from typing import Dict, List, Type | ||
|
|
||
| from marshmallow import Schema, validate | ||
| from marshmallow_dataclass import class_schema | ||
|
|
||
| from .definitions import ConditionSemVer, SemVer | ||
|
|
||
|
|
||
| @dataclass | ||
| class RegistryPackageManifest: | ||
| """Base class for registry packages.""" | ||
|
|
||
| conditions: Dict[str, ConditionSemVer] | ||
| version: SemVer | ||
|
|
||
| categories: List[str] = field(default_factory=lambda: ['security']) | ||
| description: str = 'Rules for the detection engine in the Security application.' | ||
| format_version: SemVer = field(metadata=dict(validate=validate.Equal('1.0.0')), default='1.0.0') | ||
| icons: list = field(default_factory=list) | ||
| internal: bool = True | ||
| license: str = 'basic' | ||
| name: str = 'detection_rules' | ||
| owner: Dict[str, str] = field(default_factory=lambda: dict(github='elastic/protections')) | ||
| policy_templates: list = field(default_factory=list) | ||
| release: str = 'experimental' | ||
| screenshots: list = field(default_factory=list) | ||
| title: str = 'Detection rules' | ||
| type: str = 'integration' | ||
|
|
||
| @classmethod | ||
| def get_schema(cls) -> Type[Schema]: | ||
| return class_schema(cls) | ||
|
|
||
| @classmethod | ||
| def from_dict(cls, obj: dict) -> 'RegistryPackageManifest': | ||
| return cls.get_schema()().load(obj) | ||
|
|
||
| def asdict(self) -> dict: | ||
| return self.get_schema()().dump(self) | ||
|
rw-access marked this conversation as resolved.
|
||
Uh oh!
There was an error while loading. Please reload this page.