13
13
import subprocess
14
14
import textwrap
15
15
import time
16
+ import typing
16
17
from pathlib import Path
17
18
from typing import Optional , Tuple , List
18
19
19
20
import click
20
- import typing
21
+ import yaml
21
22
from elasticsearch import Elasticsearch
22
23
23
24
from kibana .connector import Kibana
24
- from . import rule_loader
25
+ from . import rule_loader , utils
25
26
from .cli_utils import single_collection
26
27
from .eswrap import CollectEvents , add_range_to_dsl
27
28
from .ghwrap import GithubClient
@@ -95,6 +96,7 @@ def path(self) -> Path:
95
96
96
97
def revert (self , dry_run = False ):
97
98
"""Run a git command to revert this change."""
99
+
98
100
def git (* args ):
99
101
command_line = ["git" ] + [str (arg ) for arg in args ]
100
102
click .echo (subprocess .list2cmdline (command_line ))
@@ -252,8 +254,6 @@ def decorated(*args, **kwargs):
252
254
def kibana_commit (ctx , local_repo : str , github_repo : str , ssh : bool , kibana_directory : str , base_branch : str ,
253
255
branch_name : Optional [str ], message : Optional [str ], push : bool ) -> (str , str ):
254
256
"""Prep a commit and push to Kibana."""
255
- git_exe = shutil .which ("git" )
256
-
257
257
package_name = Package .load_configs ()["name" ]
258
258
release_dir = os .path .join (RELEASE_DIR , package_name )
259
259
message = message or f"[Detection Rules] Add { package_name } rules"
@@ -263,23 +263,17 @@ def kibana_commit(ctx, local_repo: str, github_repo: str, ssh: bool, kibana_dire
263
263
click .echo (f"Run { click .style ('python -m detection_rules dev build-release' , bold = True )} to populate" , err = True )
264
264
ctx .exit (1 )
265
265
266
- if not git_exe :
267
- click .secho ("Unable to find git" , err = True , fg = "red" )
268
- ctx .exit (1 )
266
+ git = utils .make_git ("-C" , local_repo )
269
267
270
268
# Get the current hash of the repo
271
- long_commit_hash = subprocess . check_output ([ git_exe , "rev-parse" , "HEAD" ], encoding = "utf-8" ). strip ( )
272
- short_commit_hash = subprocess . check_output ([ git_exe , "rev-parse" , "--short" , "HEAD" ], encoding = "utf-8" ). strip ( )
269
+ long_commit_hash = git ( "rev-parse" , "HEAD" )
270
+ short_commit_hash = git ( "rev-parse" , "--short" , "HEAD" )
273
271
274
272
try :
275
- def git (* args , show_output = False ):
276
- method = subprocess .call if show_output else subprocess .check_output
277
- return method ([git_exe , "-C" , local_repo ] + list (args ), encoding = "utf-8" )
278
-
279
273
if not os .path .exists (local_repo ):
280
274
click .echo (f"Kibana repository doesn't exist at { local_repo } . Cloning..." )
281
275
url = f"[email protected] :{ github_repo } .git" if ssh else f"https://github.com/{ github_repo } .git"
282
- subprocess . check_call ([ git_exe , "clone" , url , local_repo , "--depth" , "1" ] )
276
+ utils . make_git ()( "clone" , url , local_repo , "--depth" , "1" )
283
277
else :
284
278
git ("checkout" , base_branch )
285
279
@@ -324,7 +318,6 @@ def git(*args, show_output=False):
324
318
# Pending an official GitHub API
325
319
# @click.option("--automerge", is_flag=True, help="Enable auto-merge on the PR")
326
320
@add_git_args
327
- @click .pass_context
328
321
def kibana_pr (ctx : click .Context , label : Tuple [str , ...], assign : Tuple [str , ...], draft : bool , token : str , ** kwargs ):
329
322
"""Create a pull request to Kibana."""
330
323
branch_name , commit_hash = ctx .invoke (kibana_commit , push = True , ** kwargs )
@@ -344,7 +337,7 @@ def kibana_pr(ctx: click.Context, label: Tuple[str, ...], assign: Tuple[str, ...
344
337
- [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing),
345
338
uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)
346
339
""" ).strip () # noqa: E501
347
- pr = repo .create_pull (title , body , kwargs ["base_branch" ], branch_name , draft = draft )
340
+ pr = repo .create_pull (title , body , kwargs ["base_branch" ], branch_name , maintainer_can_modify = True , draft = draft )
348
341
349
342
# labels could also be comma separated
350
343
label = {lbl for cs_labels in label for lbl in cs_labels .split ("," ) if lbl }
@@ -359,6 +352,167 @@ def kibana_pr(ctx: click.Context, label: Tuple[str, ...], assign: Tuple[str, ...
359
352
click .echo (pr .html_url )
360
353
361
354
355
+ @dev_group .command ("integrations-pr" )
356
+ @click .argument ("local-repo" , type = click .Path (exists = True , file_okay = False , dir_okay = True ),
357
+ default = get_path (".." , "integrations" ))
358
+ @click .option ("--token" , required = True , prompt = get_github_token () is None , default = get_github_token (),
359
+ help = "GitHub token to use for the PR" , hide_input = True )
360
+ @click .option ("--pkg-directory" , "-d" , help = "Directory to save the package in cloned repository" ,
361
+ default = os .path .join ("packages" , "security_detection_engine" ))
362
+ @click .option ("--base-branch" , "-b" , help = "Base branch in target repository" , default = "master" )
363
+ @click .option ("--branch-name" , "-n" , help = "New branch for the rules commit" )
364
+ @click .option ("--github-repo" , "-r" , help = "Repository to use for the branch" , default = "elastic/integrations" )
365
+ @click .option ("--assign" , multiple = True , help = "GitHub users to assign the PR" )
366
+ @click .option ("--label" , multiple = True , help = "GitHub labels to add to the PR" )
367
+ @click .option ("--draft" , is_flag = True , help = "Open the PR as a draft" )
368
+ @click .option ("--remote" , help = "Override the remote from 'origin'" , default = "origin" )
369
+ @click .pass_context
370
+ def integrations_pr (ctx : click .Context , local_repo : str , token : str , draft : bool ,
371
+ pkg_directory : str , base_branch : str , remote : str ,
372
+ branch_name : Optional [str ], github_repo : str , assign : Tuple [str , ...], label : Tuple [str , ...]):
373
+ """Create a pull request to publish the Fleet package to elastic/integrations."""
374
+ local_repo = os .path .abspath (local_repo )
375
+ stack_version = Package .load_configs ()["name" ]
376
+ package_version = Package .load_configs ()["registry_data" ]["version" ]
377
+
378
+ release_dir = Path (RELEASE_DIR ) / stack_version / "fleet" / package_version
379
+ message = f"[Security Rules] Update security rules package to v{ package_version } "
380
+
381
+ if not release_dir .exists ():
382
+ click .secho ("Release directory doesn't exist." , fg = "red" , err = True )
383
+ click .echo (f"Run { click .style ('python -m detection_rules dev build-release' , bold = True )} to populate" , err = True )
384
+ ctx .exit (1 )
385
+
386
+ if not Path (local_repo ).exists ():
387
+ click .secho (f"{ github_repo } is not present at { local_repo } ." , fg = "red" , err = True )
388
+ ctx .exit (1 )
389
+
390
+ # Get the most recent commit hash of detection-rules
391
+ detection_rules_git = utils .make_git ()
392
+ long_commit_hash = detection_rules_git ("rev-parse" , "HEAD" )
393
+ short_commit_hash = detection_rules_git ("rev-parse" , "--short" , "HEAD" )
394
+
395
+ # refresh the local clone of the repository
396
+ git = utils .make_git ("-C" , local_repo )
397
+ git ("checkout" , base_branch )
398
+ git ("pull" , remote , base_branch )
399
+
400
+ # Switch to a new branch in elastic/integrations
401
+ branch_name = branch_name or f"detection-rules/{ package_version } -{ short_commit_hash } "
402
+ git ("checkout" , "-b" , branch_name )
403
+
404
+ # Load the changelog in memory, before it's removed. Come back for it after the PR is created
405
+ target_directory = Path (local_repo ) / pkg_directory
406
+ changelog_path = target_directory / "changelog.yml"
407
+ changelog_entries : list = yaml .safe_load (changelog_path .read_text (encoding = "utf-8" ))
408
+
409
+ changelog_entries .insert (0 , {
410
+ "version" : package_version ,
411
+ "changes" : [
412
+ # This will be changed later
413
+ {"description" : "Release security rules update" , "type" : "enhancement" ,
414
+ "link" : "https://github.com/elastic/integrations/pulls/0000" }
415
+ ]
416
+ })
417
+
418
+ # Remove existing assets and replace everything
419
+ shutil .rmtree (target_directory )
420
+ actual_target_directory = shutil .copytree (release_dir , target_directory )
421
+ assert Path (actual_target_directory ).absolute () == Path (target_directory ).absolute (), \
422
+ f"Expected a copy to { pkg_directory } "
423
+
424
+ # Add the changelog back
425
+ def save_changelog ():
426
+ with changelog_path .open ("wt" ) as f :
427
+ # add a note for other maintainers of elastic/integrations to be careful with versions
428
+ f .write ("# newer versions go on top\n " )
429
+ f .write ("# NOTE: please use pre-release versions (e.g. -dev.0) until a package is ready for production\n " )
430
+
431
+ yaml .dump (changelog_entries , f , allow_unicode = True , default_flow_style = False , indent = 2 )
432
+
433
+ save_changelog ()
434
+
435
+ # Use elastic-package to format and lint
436
+ gopath = utils .gopath ()
437
+ assert gopath is not None , "$GOPATH isn't set"
438
+
439
+ def elastic_pkg (* args ):
440
+ """Run a command with $GOPATH/bin/elastic-package in the package directory."""
441
+ prev = os .path .abspath (os .getcwd ())
442
+ os .chdir (target_directory )
443
+
444
+ try :
445
+ return subprocess .check_call ([os .path .join (gopath , "bin" , "elastic-package" )] + list (args ))
446
+ finally :
447
+ os .chdir (prev )
448
+
449
+ elastic_pkg ("format" )
450
+ elastic_pkg ("lint" )
451
+
452
+ # Upload the files to a branch
453
+ git ("add" , pkg_directory )
454
+ git ("commit" , "-m" , message )
455
+ git ("push" , "--set-upstream" , remote , branch_name )
456
+
457
+ # Create a pull request (not done yet, but we need the PR number)
458
+ client = GithubClient (token ).authenticated_client
459
+ repo = client .get_repo (github_repo )
460
+ body = textwrap .dedent (f"""
461
+ ## What does this PR do?
462
+ Update the Security Rules package to version { package_version } .
463
+ Autogenerated from commit https://github.com/elastic/detection-rules/tree/{ long_commit_hash }
464
+
465
+ ## Checklist
466
+
467
+ - [x] I have reviewed [tips for building integrations](https://github.com/elastic/integrations/blob/master/docs/tips_for_building_integrations.md) and this pull request is aligned with them.
468
+ - [ ] ~I have verified that all data streams collect metrics or logs.~
469
+ - [x] I have added an entry to my package's `changelog.yml` file.
470
+ - [x] If I'm introducing a new feature, I have modified the Kibana version constraint in my package's `manifest.yml` file to point to the latest Elastic stack release (e.g. `^7.13.0`).
471
+
472
+ ## Author's Checklist
473
+ - Install the most recently release security rules in the Detection Engine
474
+ - Install the package
475
+ - Confirm the update is available in Kibana. Click "Update X rules" or "Install X rules"
476
+ - Look at the changes made after the install and confirm they are consistent
477
+
478
+ ## How to test this PR locally
479
+ - Perform the above checklist, and use `package-storage` to build EPR from source
480
+
481
+ ## Related issues
482
+ None
483
+
484
+ ## Screenshots
485
+ None
486
+ """ ) # noqa: E501
487
+
488
+ pr = repo .create_pull (message , body , base_branch , branch_name , maintainer_can_modify = True , draft = draft )
489
+
490
+ # labels could also be comma separated
491
+ label = {lbl for cs_labels in label for lbl in cs_labels .split ("," ) if lbl }
492
+
493
+ if label :
494
+ pr .add_to_labels (* sorted (label ))
495
+
496
+ if assign :
497
+ pr .add_to_assignees (* assign )
498
+
499
+ click .echo ("PR created:" )
500
+ click .echo (pr .html_url )
501
+
502
+ # replace the changelog entry with the actual PR link
503
+ changelog_entries [0 ]["changes" ][0 ]["link" ] = pr .html_url
504
+ save_changelog ()
505
+
506
+ # format the yml file with elastic-package
507
+ elastic_pkg ("format" )
508
+ elastic_pkg ("lint" )
509
+
510
+ # Push the updated changelog to the PR branch
511
+ git ("add" , pkg_directory )
512
+ git ("commit" , "-m" , f"Add changelog entry for { package_version } " )
513
+ git ("push" )
514
+
515
+
362
516
@dev_group .command ('license-check' )
363
517
@click .option ('--ignore-directory' , '-i' , multiple = True , help = 'Directories to skip (relative to base)' )
364
518
@click .pass_context
0 commit comments