Skip to content

add new field related_integrations to the post build #2060

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
Show file tree
Hide file tree
Changes from 62 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
2811249
add new field `related_integrations` to the post build
Mikaayenson Jun 27, 2022
1381e11
add exception for endpoint `integration`
Mikaayenson Jun 27, 2022
9d43b5b
Skip rules without related integrations
Mikaayenson Jun 27, 2022
21b1a8e
lint
Mikaayenson Jun 27, 2022
0f4f88e
refactor related_integrations to TOMLRuleContents class
Mikaayenson Jun 29, 2022
9982dc2
Merge branch 'main' into 2057-add-related_integrationsto-post-transfo…
Mikaayenson Jun 29, 2022
62c8aa2
Merge branch 'main' into 2057-add-related_integrationsto-post-transfo…
Mikaayenson Jun 30, 2022
6ae5318
update to reflect required_fields updates
Mikaayenson Jun 30, 2022
2dbff89
Merge branch '2057-add-related_integrationsto-post-transform-build-pr…
Mikaayenson Jun 30, 2022
f4e39f6
add todo
Mikaayenson Jun 30, 2022
7d25987
add new line for linting
Mikaayenson Jun 30, 2022
fa8fc84
Merge branch 'main' into 2057-add-related_integrationsto-post-transfo…
brokensound77 Jul 6, 2022
f53f9d7
added changes from required_fields PR
terrancedejesus Jul 6, 2022
d818c57
related_integrations updates, get_packaged_integrations returns list …
terrancedejesus Jul 6, 2022
6dfde72
build_integrations_manifest command completed
terrancedejesus Jul 7, 2022
0170361
initial test completed for post-building related_integrations
terrancedejesus Jul 8, 2022
3ccc408
removed get_integration_manifest method from rule, removed global int…
terrancedejesus Jul 8, 2022
9685bbe
moved integration related methods to integrations.py and fixed flake …
terrancedejesus Jul 8, 2022
ddcbbde
adjustments for PipedQuery from eql sequence rules and packages with …
terrancedejesus Jul 8, 2022
aff4d08
adjusted github client import for integrations.py
terrancedejesus Jul 8, 2022
5dbab29
Update detection_rules/devtools.py
terrancedejesus Jul 12, 2022
50c8664
Update detection_rules/devtools.py
terrancedejesus Jul 12, 2022
acc408c
added integration manifest schema, made adjustments
terrancedejesus Jul 12, 2022
e031f2c
Update detection_rules/integrations.py
terrancedejesus Jul 12, 2022
40c4eb9
Merge branch 'main' into 2057-add-related_integrationsto-post-transfo…
terrancedejesus Jul 13, 2022
76cb759
Merge branch 'main' into 2057-add-related_integrationsto-post-transfo…
terrancedejesus Jul 13, 2022
866e577
Update detection_rules/rule.py
terrancedejesus Jul 18, 2022
8457a3c
Update detection_rules/rule.py
terrancedejesus Jul 18, 2022
7d68f51
Update detection_rules/integrations.py
terrancedejesus Jul 18, 2022
5fdba7e
Update detection_rules/integrations.py
terrancedejesus Jul 18, 2022
07c331b
Update detection_rules/integrations.py
terrancedejesus Jul 18, 2022
357662b
Update detection_rules/rule.py
terrancedejesus Jul 18, 2022
c981d9f
removed get_integrations_package to consolidate code
terrancedejesus Jul 18, 2022
03fcef5
removed type list return
terrancedejesus Jul 18, 2022
b244b18
updating rule.py with new changes
terrancedejesus Jul 18, 2022
3b2961e
adjusted import flake errors
terrancedejesus Jul 18, 2022
db5ee18
Update detection_rules/integrations.py
terrancedejesus Jul 19, 2022
30d0c82
Update detection_rules/integrations.py
terrancedejesus Jul 19, 2022
71ffa74
adjusted indentation error
terrancedejesus Jul 19, 2022
47f67cf
Merge branch 'main' into 2057-add-related_integrationsto-post-transfo…
terrancedejesus Jul 19, 2022
92e3c1c
Merge branch 'main' into 2057-add-related_integrationsto-post-transfo…
terrancedejesus Jul 20, 2022
561987e
Merge branch 'main' into 2057-add-related_integrationsto-post-transfo…
Mikaayenson Aug 2, 2022
15fc949
Merge branch 'main' into 2057-add-related_integrationsto-post-transfo…
Mikaayenson Aug 2, 2022
a88f784
Merge branch 'main' into 2057-add-related_integrationsto-post-transfo…
terrancedejesus Aug 3, 2022
1f76bbd
adjusted rule.get_packaged_integrations to account for kql.ast.OrExpr…
terrancedejesus Aug 3, 2022
f5e243e
Update detection_rules/devtools.py
terrancedejesus Aug 4, 2022
553e47b
Update detection_rules/devtools.py
terrancedejesus Aug 4, 2022
165f383
Update detection_rules/integrations.py
terrancedejesus Aug 4, 2022
d57f5dc
Update detection_rules/integrations.py
terrancedejesus Aug 4, 2022
684292d
adjusted find_least_compatible_version in integrations.py
terrancedejesus Aug 4, 2022
ef58a09
Update detection_rules/integrations.py
terrancedejesus Aug 4, 2022
1e6efa1
fixed flake issues
terrancedejesus Aug 4, 2022
ca3a082
Merge branch 'main' into 2057-add-related_integrationsto-post-transfo…
terrancedejesus Aug 4, 2022
ee8ac61
adjusted get_packaged_integrations
terrancedejesus Aug 4, 2022
986f19b
iterate the ast for literal event.dataset values
brokensound77 Aug 4, 2022
1dcbff8
Update detection_rules/integrations.py
terrancedejesus Aug 4, 2022
fd26283
Update detection_rules/integrations.py
terrancedejesus Aug 4, 2022
e6230cb
Update detection_rules/integrations.py
terrancedejesus Aug 4, 2022
481d233
Update detection_rules/integrations.py
terrancedejesus Aug 4, 2022
0de0c45
made small adjustments to address errors during build manifests command
terrancedejesus Aug 4, 2022
825132a
Merge branch 'main' into 2057-add-related_integrationsto-post-transfo…
terrancedejesus Aug 5, 2022
565b45b
Merge branch 'main' into 2057-add-related_integrationsto-post-transfo…
Mikaayenson Aug 8, 2022
abeccd0
addressing integrations.find_least_compatible method to return None i…
terrancedejesus Aug 8, 2022
2537d07
Update detection_rules/integrations.py
terrancedejesus Aug 8, 2022
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
15 changes: 15 additions & 0 deletions detection_rules/devtools.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from .schemas import definitions, get_stack_versions
from .semver import Version
from .utils import dict_hash, get_path, get_etc_path, load_dump
from .integrations import build_integrations_manifest

RULES_DIR = get_path('rules')
GH_CONFIG = Path.home() / ".config" / "gh" / "hosts.yml"
Expand Down Expand Up @@ -1088,3 +1089,17 @@ def get_branches(outfile: Path):
branch_list = get_stack_versions(drop_patch=True)
target_branches = json.dumps(branch_list[:-1]) + "\n"
outfile.write_text(target_branches)


@dev_group.group('integrations')
def integrations_group():
"""Commands for dev integrations methods."""


@integrations_group.command('build-manifests')
@click.option('--overwrite', '-o', is_flag=True, help="Overwrite the existing integrations-manifest.json.gz file")
@click.option("--token", required=True, prompt=get_github_token() is None, default=get_github_token(),
help="GitHub token to use for the PR", hide_input=True)
def build_integration_manifests(overwrite: bool, token: str):
"""Builds consolidated integrations manifests file."""
build_integrations_manifest(token, overwrite)
Binary file not shown.
120 changes: 120 additions & 0 deletions detection_rules/integrations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# 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.

"""Functions to support and interact with Kibana integrations."""
import gzip
import json
import os
import re
from pathlib import Path

import yaml
from marshmallow import EXCLUDE, Schema, fields, post_load

from .ghwrap import GithubClient
from .semver import Version
from .utils import INTEGRATION_RULE_DIR, cached, get_etc_path, read_gzip

MANIFEST_FILE_PATH = Path(get_etc_path('integration-manifests.json.gz'))


@cached
def load_integrations_manifests() -> dict:
"""Load the consolidated integrations manifest."""
return json.loads(read_gzip(get_etc_path('integration-manifests.json.gz')))


class IntegrationManifestSchema(Schema):
name = fields.Str(required=True)
version = fields.Str(required=True)
release = fields.Str(required=True)
description = fields.Str(required=True)
conditions = fields.Dict(required=True)
policy_templates = fields.List(fields.Dict, required=True)
owner = fields.Dict(required=True)

@post_load
def transform_policy_template(self, data, **kwargs):
data["policy_templates"] = [policy["name"] for policy in data["policy_templates"]]
return data


def build_integrations_manifest(token: str, overwrite: bool) -> None:
"""Builds a new local copy of manifest.yaml from integrations Github."""
if overwrite:
if os.path.exists(MANIFEST_FILE_PATH):
os.remove(MANIFEST_FILE_PATH)
rule_integrations = [d.name for d in Path(INTEGRATION_RULE_DIR).glob('*')]
if "endpoint" in rule_integrations:
rule_integrations.remove("endpoint")

final_integration_manifests = {integration: {} for integration in rule_integrations}

# initialize github client and point to package-storage prod
github = GithubClient(token)
client = github.authenticated_client
organization = client.get_organization("elastic")
repository = organization.get_repo("package-storage")
pkg_storage_prod_branch = repository.get_branch("production")
pkg_storage_branch_sha = pkg_storage_prod_branch.commit.sha

for integration in rule_integrations:
integration_manifests = get_integration_manifests(repository, pkg_storage_branch_sha,
pkg_path=f"packages/{integration}")
for manifest in integration_manifests:
validated_manifest = IntegrationManifestSchema(unknown=EXCLUDE).load(manifest)
package_version = validated_manifest.pop("version")
final_integration_manifests[integration][package_version] = validated_manifest

manifest_file = gzip.open(MANIFEST_FILE_PATH, "w+")
manifest_file_bytes = json.dumps(final_integration_manifests).encode("utf-8")
manifest_file.write(manifest_file_bytes)


def find_least_compatible_version(package: str, integration: str,
current_stack_version: str, packages_manifest: dict) -> str:
"""Finds least compatible version for specified integration based on stack version supplied."""
integration_manifests = {k: v for k, v in sorted(packages_manifest[package].items(), key=Version)}

def compare_versions(int_ver: str, pkg_ver: str) -> bool:
"""Compares integration and package version"""
pkg_major, pkg_minor = Version(pkg_ver)
integration_major, integration_minor = Version(int_ver)[:2]

if int(integration_major) < int(pkg_major) or int(pkg_major) > int(integration_major):
return False

compatible = Version(int_ver) <= Version(pkg_ver)
return compatible

for version, manifest in integration_manifests.items():
for kibana_compat_vers in re.sub(r"\>|\<|\=|\^", "", manifest["conditions"]["kibana.version"]).split(" || "):
if compare_versions(kibana_compat_vers, current_stack_version):
return version
raise Exception(f"no compatible version for integration {package}:{integration}")
Copy link
Contributor

Choose a reason for hiding this comment

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

going forward, we may just want to return None and not set the integration rather than raising an exception, but we can punt this for now

Copy link
Contributor

Choose a reason for hiding this comment

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

adjusted in recent commit. Method expects Union[str, None]. If no compatible version, it will print instead of raise the error and then return None. Had to adjust rule.add_related_integrations code so when policy templates are checked, it only does it if version exists or it would error.



def get_integration_manifests(repository, sha: str, pkg_path: str) -> list:
"""Iterates over specified integrations from package-storage and combines manifests per version."""
integration = pkg_path.split("/")[-1]
versioned_packages = repository.get_dir_contents(pkg_path, ref=sha)
versions = [p.path.split("/")[-1] for p in versioned_packages]

manifests = []
for version in versions:
contents = repository.get_dir_contents(f"{pkg_path}/{version}", ref=sha)
print(f"Processing {integration} - Version: {version}")

processing_version = contents[0].path.split("/")[2]
manifest_content = [c for c in contents if "manifest" in c.path]

if len(manifest_content) < 1:
raise Exception(f"manifest file does not exist for {integration}:{processing_version}")

path = manifest_content[0].path
manifest_content = yaml.safe_load(repository.get_contents(path, ref=sha).decoded_content.decode())
manifests.append(manifest_content)

return manifests
71 changes: 65 additions & 6 deletions detection_rules/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@

import eql
import kql
from kql.ast import FieldComparison
from marko.block import Document as MarkoDocument
from marko.ext.gfm import gfm
from marshmallow import ValidationError, validates_schema

from . import beats, ecs, utils
from .integrations import (find_least_compatible_version,
load_integrations_manifests)
from .misc import load_current_package_version
from .mixins import MarshmallowDataclassMixin, StackCompatMixin
from .rule_formatter import nested_normalize, toml_write
Expand Down Expand Up @@ -165,6 +168,12 @@ class RequiredFields:
type: definitions.NonEmptyStr
ecs: bool

@dataclass
class RelatedIntegrations:
package: definitions.NonEmptyStr
version: definitions.NonEmptyStr
integration: Optional[definitions.NonEmptyStr]

actions: Optional[list]
author: List[str]
building_block_type: Optional[str]
Expand All @@ -186,7 +195,7 @@ class RequiredFields:
# explicitly NOT allowed!
# output_index: Optional[str]
references: Optional[List[str]]
related_integrations: Optional[List[str]] = field(metadata=dict(metadata=dict(min_compat="8.3")))
related_integrations: Optional[List[RelatedIntegrations]] = field(metadata=dict(metadata=dict(min_compat="8.3")))
required_fields: Optional[List[RequiredFields]] = field(metadata=dict(metadata=dict(min_compat="8.3")))
risk_score: definitions.RiskScore
risk_score_mapping: Optional[List[RiskScoreMapping]]
Expand Down Expand Up @@ -665,7 +674,7 @@ def _post_dict_transform(self, obj: dict) -> dict:
"""Transform the converted API in place before sending to Kibana."""
super()._post_dict_transform(obj)

self.add_related_integrations(obj)
self._add_related_integrations(obj)
self._add_required_fields(obj)
self._add_setup(obj)

Expand All @@ -675,10 +684,35 @@ def _post_dict_transform(self, obj: dict) -> dict:
subclass.from_dict(obj)
return obj

def add_related_integrations(self, obj: dict) -> None:
def _add_related_integrations(self, obj: dict) -> None:
"""Add restricted field related_integrations to the obj."""
# field_name = "related_integrations"
...
field_name = "related_integrations"
package_integrations = obj.get(field_name, [])

if not package_integrations and self.metadata.integration:
packages_manifest = load_integrations_manifests()
current_stack_version = load_current_package_version()

if self.check_restricted_field_version(field_name):
if isinstance(self.data, QueryRuleData) and self.data.language != 'lucene':
package_integrations = self._get_packaged_integrations(packages_manifest)

if not package_integrations:
return

for package in package_integrations:
package["version"] = find_least_compatible_version(
package=package["package"],
integration=package["integration"],
current_stack_version=current_stack_version,
packages_manifest=packages_manifest)

# if integration is not a policy template remove
policy_templates = packages_manifest[package["package"]][package["version"]]["policy_templates"]
if package["integration"] not in policy_templates:
del package["integration"]

obj.setdefault("related_integrations", package_integrations)

def _add_required_fields(self, obj: dict) -> None:
"""Add restricted field required_fields to the obj, derived from the query AST."""
Expand All @@ -689,7 +723,7 @@ def _add_required_fields(self, obj: dict) -> None:
required_fields = []

field_name = "required_fields"
if self.check_restricted_field_version(field_name=field_name):
if required_fields and self.check_restricted_field_version(field_name=field_name):
obj.setdefault(field_name, required_fields)

def _add_setup(self, obj: dict) -> None:
Expand Down Expand Up @@ -759,6 +793,31 @@ def compare_field_versions(min_stack: Version, max_stack: Version) -> bool:
max_stack = max_stack or current_version
return Version(min_stack) <= current_version >= Version(max_stack)

def _get_packaged_integrations(self, package_manifest: dict) -> Optional[List[dict]]:
packaged_integrations = []
datasets = set()

for node in self.data.get('ast', []):
if isinstance(node, eql.ast.Comparison) and str(node.left) == 'event.dataset':
datasets.update(set(n.value for n in node if isinstance(n, eql.ast.Literal)))
elif isinstance(node, FieldComparison) and str(node.field) == 'event.dataset':
datasets.update(set(str(n) for n in node if isinstance(n, kql.ast.Value)))

if not datasets:
return

for value in sorted(datasets):
integration = 'Unknown'
if '.' in value:
package, integration = value.split('.', 1)
else:
package = value

if package in list(package_manifest):
packaged_integrations.append({"package": package, "integration": integration})

return packaged_integrations

@validates_schema
def post_validation(self, value: dict, **kwargs):
"""Additional validations beyond base marshmallow schemas."""
Expand Down
1 change: 1 addition & 0 deletions detection_rules/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
CURR_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT_DIR = os.path.dirname(CURR_DIR)
ETC_DIR = os.path.join(ROOT_DIR, "detection_rules", "etc")
INTEGRATION_RULE_DIR = os.path.join(ROOT_DIR, "rules", "integrations")


class NonelessDict(dict):
Expand Down