|
25 | 25 | import pytoml # type: ignore[reportMissingTypeStubs] |
26 | 26 | import requests.exceptions |
27 | 27 | import yaml |
28 | | -from elasticsearch import Elasticsearch |
| 28 | +from elasticsearch import BadRequestError, Elasticsearch |
| 29 | +from elasticsearch import ConnectionError as ESConnectionError |
29 | 30 | from eql.table import Table # type: ignore[reportMissingTypeStubs] |
30 | 31 | from eql.utils import load_dump # type: ignore[reportMissingTypeStubs, reportUnknownVariableType] |
31 | 32 | from kibana.connector import Kibana # type: ignore[reportMissingTypeStubs] |
|
39 | 40 | from .docs import REPO_DOCS_DIR, IntegrationSecurityDocs, IntegrationSecurityDocsMDX |
40 | 41 | from .ecs import download_endpoint_schemas, download_schemas |
41 | 42 | from .endgame import EndgameSchemaManager |
| 43 | +from .esql_errors import ( |
| 44 | + ESQL_EXCEPTION_TYPES, |
| 45 | +) |
42 | 46 | from .eswrap import CollectEvents, add_range_to_dsl |
43 | 47 | from .ghwrap import GithubClient, update_gist |
44 | 48 | from .integrations import ( |
|
50 | 54 | load_integrations_manifests, |
51 | 55 | ) |
52 | 56 | from .main import root |
53 | | -from .misc import PYTHON_LICENSE, add_client, raise_client_error |
| 57 | +from .misc import ( |
| 58 | + PYTHON_LICENSE, |
| 59 | + add_client, |
| 60 | + get_default_elasticsearch_client, |
| 61 | + get_default_kibana_client, |
| 62 | + raise_client_error, |
| 63 | +) |
54 | 64 | from .packaging import CURRENT_RELEASE_PATH, PACKAGE_FILE, RELEASE_DIR, Package |
55 | 65 | from .rule import ( |
56 | 66 | AnyRuleData, |
|
63 | 73 | TOMLRuleContents, |
64 | 74 | ) |
65 | 75 | from .rule_loader import RuleCollection, production_filter |
| 76 | +from .rule_validators import ESQLValidator |
66 | 77 | from .schemas import definitions, get_stack_versions |
67 | 78 | from .utils import check_version_lock_double_bumps, dict_hash, get_etc_path, get_path |
68 | 79 | from .version_lock import VersionLockFile, loaded_version_lock |
@@ -1403,6 +1414,72 @@ def rule_event_search( # noqa: PLR0913 |
1403 | 1414 | raise_client_error("Rule is not a query rule!") |
1404 | 1415 |
|
1405 | 1416 |
|
| 1417 | +@test_group.command("esql-remote-validation") |
| 1418 | +@click.option( |
| 1419 | + "--verbosity", |
| 1420 | + type=click.IntRange(0, 1), |
| 1421 | + default=0, |
| 1422 | + help="Set verbosity level: 0 for minimal output, 1 for detailed output.", |
| 1423 | +) |
| 1424 | +def esql_remote_validation( |
| 1425 | + verbosity: int, |
| 1426 | +) -> None: |
| 1427 | + """Search using a rule file against an Elasticsearch instance.""" |
| 1428 | + |
| 1429 | + rule_collection: RuleCollection = RuleCollection.default().filter(production_filter) |
| 1430 | + esql_rules = [r for r in rule_collection if r.contents.data.type == "esql"] |
| 1431 | + |
| 1432 | + click.echo(f"ESQL rules loaded: {len(esql_rules)}") |
| 1433 | + |
| 1434 | + if not esql_rules: |
| 1435 | + return |
| 1436 | + # TODO(eric-forte-elastic): @add_client https://github.com/elastic/detection-rules/issues/5156 # noqa: FIX002 |
| 1437 | + with get_default_kibana_client() as kibana_client, get_default_elasticsearch_client() as elastic_client: |
| 1438 | + if not kibana_client or not elastic_client: |
| 1439 | + raise_client_error("Skipping remote validation due to missing client") |
| 1440 | + |
| 1441 | + failed_count = 0 |
| 1442 | + fail_list: list[str] = [] |
| 1443 | + max_retries = 3 |
| 1444 | + for r in esql_rules: |
| 1445 | + retry_count = 0 |
| 1446 | + while retry_count < max_retries: |
| 1447 | + try: |
| 1448 | + validator = ESQLValidator(r.contents.data.query) # type: ignore[reportIncompatibleMethodOverride] |
| 1449 | + _ = validator.remote_validate_rule_contents(kibana_client, elastic_client, r.contents, verbosity) |
| 1450 | + break |
| 1451 | + except (ValueError, BadRequestError, *ESQL_EXCEPTION_TYPES) as e: # type: ignore[reportUnknownMemberType] |
| 1452 | + e_type = type(e) # type: ignore[reportUnknownMemberType] |
| 1453 | + if isinstance(e, ESQL_EXCEPTION_TYPES): |
| 1454 | + click.echo(click.style(f"{r.contents.data.rule_id} ", fg="red", bold=True), nl=False) |
| 1455 | + _ = e.show() # type: ignore[reportUnknownMemberType] |
| 1456 | + else: |
| 1457 | + click.echo(f"FAILURE: {e_type}: {e}") # type: ignore[reportUnknownMemberType] |
| 1458 | + fail_list.append(f"{r.contents.data.rule_id} FAILURE: {e_type}: {e}") # type: ignore[reportUnknownMemberType] |
| 1459 | + failed_count += 1 |
| 1460 | + break |
| 1461 | + except ESConnectionError as e: |
| 1462 | + retry_count += 1 |
| 1463 | + click.echo(f"Connection error: {e}. Retrying {retry_count}/{max_retries}...") |
| 1464 | + time.sleep(30) |
| 1465 | + if retry_count == max_retries: |
| 1466 | + click.echo(f"FAILURE: {e} after {max_retries} retries") |
| 1467 | + fail_list.append(f"FAILURE: {e} after {max_retries} retries") |
| 1468 | + failed_count += 1 |
| 1469 | + |
| 1470 | + click.echo(f"Total rules: {len(esql_rules)}") |
| 1471 | + click.echo(f"Failed rules: {failed_count}") |
| 1472 | + |
| 1473 | + _ = Path("failed_rules.log").write_text("\n".join(fail_list), encoding="utf-8") |
| 1474 | + click.echo("Failed rules written to failed_rules.log") |
| 1475 | + if failed_count > 0: |
| 1476 | + click.echo("Failed rule IDs:") |
| 1477 | + uuids = {line.split()[0] for line in fail_list} |
| 1478 | + click.echo("\n".join(uuids)) |
| 1479 | + ctx = click.get_current_context() |
| 1480 | + ctx.exit(1) |
| 1481 | + |
| 1482 | + |
1406 | 1483 | @test_group.command("rule-survey") |
1407 | 1484 | @click.argument("query", required=False) |
1408 | 1485 | @click.option("--date-range", "-d", type=(str, str), default=("now-7d", "now"), help="Date range to scope search") |
|
0 commit comments