gulp-cli 1.1.4__tar.gz → 1.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. {gulp_cli-1.1.4/src/gulp_cli.egg-info → gulp_cli-1.1.5}/PKG-INFO +1 -1
  2. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/docs/command-reference.md +5 -2
  3. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/docs/examples.md +11 -0
  4. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/_version.py +3 -3
  5. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/cli.py +3 -0
  6. gulp_cli-1.1.5/src/gulp_cli/commands/enrich.py +187 -0
  7. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/ingest.py +4 -1
  8. {gulp_cli-1.1.4 → gulp_cli-1.1.5/src/gulp_cli.egg-info}/PKG-INFO +1 -1
  9. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli.egg-info/SOURCES.txt +3 -1
  10. gulp_cli-1.1.5/tests/test_enrich_live.py +302 -0
  11. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/.github/workflows/portable-bundles.yml +0 -0
  12. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/.github/workflows/python-package.yml +0 -0
  13. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/.gitignore +0 -0
  14. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/GULP_DUAL_LICENSE.txt +0 -0
  15. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/LICENSE.AGPL-3.0.txt +0 -0
  16. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/LICENSE.md +0 -0
  17. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/README.md +0 -0
  18. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/docs/extensions.md +0 -0
  19. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/docs/getting-started.md +0 -0
  20. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/docs/portable.md +0 -0
  21. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/docs/resource-management.md +0 -0
  22. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/docs/troubleshooting-cli.md +0 -0
  23. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/gulp-cli.spec +0 -0
  24. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/portable/launch-linux.sh +0 -0
  25. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/portable/launch-macos.sh +0 -0
  26. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/portable/launch-windows.bat +0 -0
  27. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/pyproject.toml +0 -0
  28. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/setup.cfg +0 -0
  29. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/__init__.py +0 -0
  30. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/__main__.py +0 -0
  31. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/client.py +0 -0
  32. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/__init__.py +0 -0
  33. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/acl.py +0 -0
  34. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/auth.py +0 -0
  35. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/collab.py +0 -0
  36. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/context.py +0 -0
  37. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/db.py +0 -0
  38. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/enhance_map.py +0 -0
  39. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/glyph.py +0 -0
  40. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/mapping.py +0 -0
  41. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/operations.py +0 -0
  42. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/plugin.py +0 -0
  43. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/query.py +0 -0
  44. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/source.py +0 -0
  45. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/stats.py +0 -0
  46. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/storage.py +0 -0
  47. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/user_group.py +0 -0
  48. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/users.py +0 -0
  49. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/config.py +0 -0
  50. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/extension/__init__.py +0 -0
  51. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/extension/query_sigma_zip.py +0 -0
  52. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/extension/story.py +0 -0
  53. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/extension_helpers.py +0 -0
  54. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/extensions.py +0 -0
  55. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/output.py +0 -0
  56. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/utils.py +0 -0
  57. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli.egg-info/dependency_links.txt +0 -0
  58. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli.egg-info/entry_points.txt +0 -0
  59. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli.egg-info/requires.txt +0 -0
  60. {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gulp-cli
3
- Version: 1.1.4
3
+ Version: 1.1.5
4
4
  Summary: Command-line client for gULP
5
5
  Author-email: Mentat <info@mentat.is>
6
6
  Requires-Python: >=3.12
@@ -1298,6 +1298,7 @@ gulp-cli enrich documents OPERATION_ID [OPTIONS]
1298
1298
  - `--plugin-params TEXT` — Plugin parameters JSON
1299
1299
  - `--flt TEXT` — Filter JSON
1300
1300
  - `--wait` — Wait for completion
1301
+ - `--fields TEXT` — { "field": "value" } pairs to set on enriched documents (JSON string): if "value" is `null` the enrichment is made on field's value in the document. either, enriched "value" is used.
1301
1302
 
1302
1303
  **Examples:**
1303
1304
 
@@ -1305,7 +1306,8 @@ gulp-cli enrich documents OPERATION_ID [OPTIONS]
1305
1306
  gulp-cli enrich documents my_op \
1306
1307
  --plugin my_enricher \
1307
1308
  --plugin-params '{"api_key":"..."}' \
1308
- --flt '{"enriched":false}'
1309
+ --flt '{"operation_ids":["my_op"],"source_ids":["security"]}' \
1310
+ --fields '{"ip.destination": null}'
1309
1311
  ```
1310
1312
 
1311
1313
  ---
@@ -1327,12 +1329,13 @@ gulp-cli enrich single-id OPERATION_ID DOCUMENT_ID [OPTIONS]
1327
1329
 
1328
1330
  - `--plugin TEXT` — Plugin name (required)
1329
1331
  - `--plugin-params TEXT` — Plugin parameters JSON
1332
+ - `--fields TEXT` — { "field": "value" } pairs to set on enriched documents (JSON string): if "value" is `null` the enrichment is made on field's value in the document. either, enriched "value" is used.
1330
1333
  - `--wait` — Wait for completion
1331
1334
 
1332
1335
  **Examples:**
1333
1336
 
1334
1337
  ```bash
1335
- gulp-cli enrich single-id my_op doc123 --plugin my_enricher
1338
+ gulp-cli enrich single-id my_op doc123 --plugin my_enricher --fields '{"host.os": null}' --wait
1336
1339
  ```
1337
1340
 
1338
1341
  ---
@@ -45,6 +45,7 @@
45
45
  - [Run Rule on Specific Source](#run-rule-on-specific-source)
46
46
  - [Multiple Sigma Rules in Batch](#multiple-sigma-rules-in-batch)
47
47
  - [Enrichment \& Tagging Workflows](#enrichment--tagging-workflows)
48
+ - [use whois to enrich IP addresses with domain information](#use-whois-to-enrich-ip-addresses-with-domain-information)
48
49
  - [Tag Suspicious Events](#tag-suspicious-events)
49
50
  - [Apply Threat Level Classification](#apply-threat-level-classification)
50
51
  - [Track Analysis Progress](#track-analysis-progress)
@@ -613,6 +614,16 @@ done
613
614
 
614
615
  ## Enrichment & Tagging Workflows
615
616
 
617
+ ### use whois to enrich IP addresses with domain information
618
+
619
+ ```bash
620
+ gulp-cli enrich documents incident-001 \
621
+ --plugin enrich_whois \
622
+ --flt '{"operation_ids":["network"]}' \
623
+ --fields '{"ip.destination": null}' \
624
+ --wait
625
+ ```
626
+
616
627
  ### Tag Suspicious Events
617
628
 
618
629
  ```bash
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.1.4'
22
- __version_tuple__ = version_tuple = (1, 1, 4)
21
+ __version__ = version = '1.1.5'
22
+ __version_tuple__ = version_tuple = (1, 1, 5)
23
23
 
24
- __commit_id__ = commit_id = 'g8cbdcc79a'
24
+ __commit_id__ = commit_id = 'g8a6d297b4'
@@ -7,6 +7,7 @@ from gulp_cli.commands.auth import app as auth_app
7
7
  from gulp_cli.commands.collab import app as collab_app
8
8
  from gulp_cli.commands.context import app as context_app
9
9
  from gulp_cli.commands.db import app as db_app
10
+ from gulp_cli.commands.enrich import app as enrich_app
10
11
  from gulp_cli.commands.enhance_map import app as enhance_map_app
11
12
  from gulp_cli.commands.glyph import app as glyph_app
12
13
  from gulp_cli.commands.ingest import app as ingest_app
@@ -93,6 +94,7 @@ app.add_typer(stats_app, name="stats")
93
94
  app.add_typer(storage_app, name="storage")
94
95
  app.add_typer(collab_app, name="collab")
95
96
  app.add_typer(db_app, name="db")
97
+ app.add_typer(enrich_app, name="enrich")
96
98
  app.add_typer(acl_app, name="acl")
97
99
  app.add_typer(plugin_app, name="plugin")
98
100
  app.add_typer(mapping_app, name="mapping")
@@ -114,6 +116,7 @@ load_extensions(
114
116
  "storage": storage_app,
115
117
  "collab": collab_app,
116
118
  "db": db_app,
119
+ "enrich": enrich_app,
117
120
  "acl": acl_app,
118
121
  "plugin": plugin_app,
119
122
  "mapping": mapping_app,
@@ -0,0 +1,187 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any
5
+
6
+ import typer
7
+
8
+ from gulp_cli.client import get_client
9
+ from gulp_cli.output import print_result
10
+ from gulp_cli.utils import comma_split, parse_json_option
11
+
12
+ app = typer.Typer(help="Enrichment and tagging commands")
13
+
14
+
15
+ @app.command("tag")
16
+ def enrich_tag(
17
+ operation_id: str,
18
+ tags: list[str] = typer.Option(..., "--tag", help="Tag to add (repeatable)"),
19
+ flt: str | None = typer.Option(
20
+ None, "--flt", help="JSON object for GulpQueryFilter"
21
+ ),
22
+ wait: bool = typer.Option(False, "--wait", help="Wait for the async job to finish"),
23
+ ) -> None:
24
+ """Add tags to documents matching a filter."""
25
+
26
+ async def _run() -> None:
27
+ flt_parsed = parse_json_option(flt, field_name="flt")
28
+ async with get_client() as client:
29
+ result = await client.enrich.tag_documents(
30
+ operation_id=operation_id,
31
+ tags=tags,
32
+ flt=flt_parsed,
33
+ wait=wait,
34
+ )
35
+ print_result(result)
36
+
37
+ asyncio.run(_run())
38
+
39
+
40
+ @app.command("untag")
41
+ def enrich_untag(
42
+ operation_id: str,
43
+ tags: list[str] = typer.Option(..., "--tag", help="Tag to remove (repeatable)"),
44
+ flt: str | None = typer.Option(
45
+ None, "--flt", help="JSON object for GulpQueryFilter"
46
+ ),
47
+ wait: bool = typer.Option(False, "--wait", help="Wait for the async job to finish"),
48
+ ) -> None:
49
+ """Remove tags from documents matching a filter."""
50
+
51
+ async def _run() -> None:
52
+ flt_parsed = parse_json_option(flt, field_name="flt")
53
+ async with get_client() as client:
54
+ result = await client.enrich.untag_documents(
55
+ operation_id=operation_id,
56
+ tags=tags,
57
+ flt=flt_parsed,
58
+ wait=wait,
59
+ )
60
+ print_result(result)
61
+
62
+ asyncio.run(_run())
63
+
64
+
65
+ @app.command("update")
66
+ def enrich_update(
67
+ operation_id: str,
68
+ fields: str = typer.Option(..., "--fields", help="JSON object of field updates"),
69
+ flt: str | None = typer.Option(
70
+ None, "--flt", help="JSON object for GulpQueryFilter"
71
+ ),
72
+ wait: bool = typer.Option(False, "--wait", help="Wait for the async job to finish"),
73
+ ) -> None:
74
+ """Update fields on documents matching a filter."""
75
+
76
+ async def _run() -> None:
77
+ fields_parsed = parse_json_option(fields, field_name="fields")
78
+ if fields_parsed is None:
79
+ raise typer.BadParameter("--fields is required")
80
+ flt_parsed = parse_json_option(flt, field_name="flt")
81
+ async with get_client() as client:
82
+ result = await client.enrich.update_documents(
83
+ operation_id=operation_id,
84
+ fields=fields_parsed,
85
+ flt=flt_parsed,
86
+ wait=wait,
87
+ )
88
+ print_result(result)
89
+
90
+ asyncio.run(_run())
91
+
92
+
93
+ @app.command("remove")
94
+ def enrich_remove(
95
+ operation_id: str,
96
+ fields: str = typer.Option(
97
+ ..., "--fields", help="Comma-separated field names to remove"
98
+ ),
99
+ flt: str | None = typer.Option(
100
+ None, "--flt", help="JSON object for GulpQueryFilter"
101
+ ),
102
+ wait: bool = typer.Option(False, "--wait", help="Wait for the async job to finish"),
103
+ ) -> None:
104
+ """Remove enrichment markers or specific fields from documents matching a filter."""
105
+
106
+ async def _run() -> None:
107
+ flt_parsed = parse_json_option(flt, field_name="flt")
108
+ field_names = comma_split(fields)
109
+ async with get_client() as client:
110
+ result = await client.enrich.enrich_remove(
111
+ operation_id=operation_id,
112
+ fields=field_names or None,
113
+ flt=flt_parsed,
114
+ wait=wait,
115
+ )
116
+ print_result(result)
117
+
118
+ asyncio.run(_run())
119
+
120
+
121
+ @app.command("documents")
122
+ def enrich_documents(
123
+ operation_id: str,
124
+ plugin: str = typer.Option(..., "--plugin", help="Enrichment plugin name"),
125
+ fields: str | None = typer.Option(
126
+ None, "--fields", help="JSON object of fields to enrich"
127
+ ),
128
+ flt: str | None = typer.Option(
129
+ None, "--flt", help="JSON object for GulpQueryFilter"
130
+ ),
131
+ plugin_params: str | None = typer.Option(
132
+ None, "--plugin-params", help="JSON object for GulpPluginParameters"
133
+ ),
134
+ wait: bool = typer.Option(False, "--wait", help="Wait for the async job to finish"),
135
+ ) -> None:
136
+ """Run an enrichment plugin on documents matching a filter."""
137
+
138
+ async def _run() -> None:
139
+ fields_parsed = parse_json_option(fields, field_name="fields")
140
+ flt_parsed = parse_json_option(flt, field_name="flt")
141
+ plugin_params_parsed = parse_json_option(
142
+ plugin_params, field_name="plugin-params"
143
+ )
144
+ async with get_client() as client:
145
+ result = await client.enrich.enrich_documents(
146
+ operation_id=operation_id,
147
+ plugin=plugin,
148
+ fields=fields_parsed,
149
+ flt=flt_parsed,
150
+ plugin_params=plugin_params_parsed,
151
+ wait=wait,
152
+ )
153
+ print_result(result)
154
+
155
+ asyncio.run(_run())
156
+
157
+
158
+ @app.command("single-id")
159
+ def enrich_single_id(
160
+ operation_id: str,
161
+ doc_id: str,
162
+ plugin: str = typer.Option(..., "--plugin", help="Enrichment plugin name"),
163
+ fields: str | None = typer.Option(
164
+ None, "--fields", help="JSON object of fields to enrich"
165
+ ),
166
+ plugin_params: str | None = typer.Option(
167
+ None, "--plugin-params", help="JSON object for GulpPluginParameters"
168
+ ),
169
+ ) -> None:
170
+ """Enrich a single document by OpenSearch document ID."""
171
+
172
+ async def _run() -> None:
173
+ fields_parsed = parse_json_option(fields, field_name="fields")
174
+ plugin_params_parsed = parse_json_option(
175
+ plugin_params, field_name="plugin-params"
176
+ )
177
+ async with get_client() as client:
178
+ result = await client.enrich.enrich_single_id(
179
+ operation_id=operation_id,
180
+ doc_id=doc_id,
181
+ plugin=plugin,
182
+ fields=fields_parsed,
183
+ plugin_params=plugin_params_parsed,
184
+ )
185
+ print_result(result)
186
+
187
+ asyncio.run(_run())
@@ -948,7 +948,10 @@ def ingest_file(
948
948
  return
949
949
 
950
950
  if reset_operation:
951
- await client.operations.delete(operation_id, force=True)
951
+ try:
952
+ await client.operations.delete(operation_id, force=True)
953
+ except NotFoundError:
954
+ pass
952
955
  op = await client.operations.create(name=operation_id)
953
956
  if not get_runtime_verbose():
954
957
  print_warning(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gulp-cli
3
- Version: 1.1.4
3
+ Version: 1.1.5
4
4
  Summary: Command-line client for gULP
5
5
  Author-email: Mentat <info@mentat.is>
6
6
  Requires-Python: >=3.12
@@ -40,6 +40,7 @@ src/gulp_cli/commands/collab.py
40
40
  src/gulp_cli/commands/context.py
41
41
  src/gulp_cli/commands/db.py
42
42
  src/gulp_cli/commands/enhance_map.py
43
+ src/gulp_cli/commands/enrich.py
43
44
  src/gulp_cli/commands/glyph.py
44
45
  src/gulp_cli/commands/ingest.py
45
46
  src/gulp_cli/commands/mapping.py
@@ -53,4 +54,5 @@ src/gulp_cli/commands/user_group.py
53
54
  src/gulp_cli/commands/users.py
54
55
  src/gulp_cli/extension/__init__.py
55
56
  src/gulp_cli/extension/query_sigma_zip.py
56
- src/gulp_cli/extension/story.py
57
+ src/gulp_cli/extension/story.py
58
+ tests/test_enrich_live.py
@@ -0,0 +1,302 @@
1
+ import asyncio
2
+ import json
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ import uuid
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ from gulp_sdk import GulpClient
13
+
14
+ CLI_BIN = Path("/gulp/.venv/bin/gulp-cli")
15
+ SAMPLE_EVTX = Path("/gulp/samples/win_evtx/Security_short_selected.evtx")
16
+
17
+
18
+ def _unique(prefix: str) -> str:
19
+ return f"{prefix}_{uuid.uuid4().hex[:8]}"
20
+
21
+
22
+ def _run_cli(*args: str, config_dir: Path) -> subprocess.CompletedProcess[str]:
23
+ env = os.environ.copy()
24
+ env["GULP_CLI_HOME"] = str(config_dir)
25
+ return subprocess.run(
26
+ [str(CLI_BIN), *args],
27
+ cwd="/gulp",
28
+ env=env,
29
+ capture_output=True,
30
+ text=True,
31
+ check=False,
32
+ )
33
+
34
+
35
+ async def _setup_sample_docs() -> tuple[str, str]:
36
+ async with GulpClient("http://localhost:8080") as client:
37
+ await client.auth.login("admin", "admin")
38
+ operation_id = _unique("cli_enrich_op")
39
+ operation = await client.operations.create(operation_id)
40
+ ingest = await client.ingest.file(
41
+ operation_id=operation.id,
42
+ plugin_name="win_evtx",
43
+ file_path=str(SAMPLE_EVTX),
44
+ context_name="cli_enrich_context",
45
+ wait=True,
46
+ )
47
+ assert str(getattr(ingest, "status", "")).lower() in {"done", "pending"}
48
+
49
+ preview = await client.queries.query_raw(
50
+ operation_id=operation.id,
51
+ q=[{"query": {"match_all": {}}}],
52
+ q_options={"preview_mode": True, "limit": 1, "name": "cli_enrich_preview"},
53
+ )
54
+ docs = (preview.get("data") or {}).get("docs") or []
55
+ assert docs, "Expected at least one sample document after ingestion"
56
+ doc_id = docs[0].get("_id") or docs[0].get("id")
57
+ assert doc_id, "Could not determine document id for CLI enrichment test"
58
+ return operation.id, str(doc_id)
59
+
60
+
61
+ def _load_saved_session(config_dir: Path) -> tuple[str, str]:
62
+ config = json.loads((config_dir / "config.json").read_text(encoding="utf-8"))
63
+ active_user = str(config.get("active_user") or "")
64
+ sessions = config.get("sessions") or {}
65
+ session = sessions.get(active_user) or next(iter(sessions.values()), None)
66
+ assert isinstance(session, dict), "Expected a saved CLI session for the live test"
67
+ url = str(session.get("url") or "").strip()
68
+ token = str(session.get("token") or "").strip()
69
+ assert url and token, "Saved CLI session is missing url or token"
70
+ return url, token
71
+
72
+
73
+ async def _get_first_doc_id(operation_id: str, config_dir: Path) -> str:
74
+ url, token = _load_saved_session(config_dir)
75
+ async with GulpClient(url, token=token) as client:
76
+ preview = await client.queries.query_raw(
77
+ operation_id=operation_id,
78
+ q=[{"query": {"match_all": {}}}],
79
+ q_options={"preview_mode": True, "limit": 1, "name": "cli_enrich_preview"},
80
+ )
81
+ docs = (preview.get("data") or {}).get("docs") or []
82
+ assert docs, "Expected at least one sample document after CLI ingestion"
83
+ doc_id = docs[0].get("_id") or docs[0].get("id")
84
+ assert doc_id, "Could not determine document id for CLI enrichment test"
85
+ return str(doc_id)
86
+
87
+
88
+ async def _cleanup_operation(operation_id: str) -> None:
89
+ async with GulpClient("http://localhost:8080") as client:
90
+ await client.auth.login("admin", "admin")
91
+ await client.operations.delete(operation_id)
92
+
93
+
94
+ @pytest.mark.integration
95
+ def test_enrich_cli_tag_update_remove_and_untag(tmp_path: Path) -> None:
96
+ config_dir = tmp_path / "cli-config"
97
+ config_dir.mkdir(parents=True, exist_ok=True)
98
+
99
+ login = _run_cli(
100
+ "auth",
101
+ "login",
102
+ "--url",
103
+ "http://localhost:8080",
104
+ "--username",
105
+ "admin",
106
+ "--password",
107
+ "admin",
108
+ config_dir=config_dir,
109
+ )
110
+ assert login.returncode == 0, login.stderr or login.stdout
111
+
112
+ operation_id = _unique("cli_enrich_op")
113
+
114
+ ingest_result = _run_cli(
115
+ "ingest",
116
+ "file",
117
+ operation_id,
118
+ "win_evtx",
119
+ str(SAMPLE_EVTX),
120
+ "--context-name",
121
+ "test_context",
122
+ "--reset-operation",
123
+ "--batch-size",
124
+ "128",
125
+ "--wait",
126
+ config_dir=config_dir,
127
+ )
128
+ assert ingest_result.returncode == 0, ingest_result.stderr or ingest_result.stdout
129
+
130
+ doc_id = asyncio.run(_get_first_doc_id(operation_id, config_dir))
131
+
132
+ try:
133
+ tag_result = _run_cli(
134
+ "enrich",
135
+ "tag",
136
+ operation_id,
137
+ "--tag",
138
+ "cli_enrich_tag",
139
+ "--flt",
140
+ json.dumps({"operation_ids": [operation_id]}),
141
+ "--wait",
142
+ config_dir=config_dir,
143
+ )
144
+ assert tag_result.returncode == 0, tag_result.stderr or tag_result.stdout
145
+ assert "RuntimeWarning" not in (tag_result.stderr or "")
146
+
147
+ update_result = _run_cli(
148
+ "enrich",
149
+ "update",
150
+ operation_id,
151
+ "--fields",
152
+ json.dumps({"cli_enrich_field": True}),
153
+ "--flt",
154
+ json.dumps({"operation_ids": [operation_id]}),
155
+ "--wait",
156
+ config_dir=config_dir,
157
+ )
158
+ assert update_result.returncode == 0, (
159
+ update_result.stderr or update_result.stdout
160
+ )
161
+
162
+ remove_result = _run_cli(
163
+ "enrich",
164
+ "remove",
165
+ operation_id,
166
+ "--fields",
167
+ "cli_enrich_field",
168
+ "--flt",
169
+ json.dumps({"operation_ids": [operation_id]}),
170
+ "--wait",
171
+ config_dir=config_dir,
172
+ )
173
+ assert remove_result.returncode == 0, (
174
+ remove_result.stderr or remove_result.stdout
175
+ )
176
+
177
+ untag_result = _run_cli(
178
+ "enrich",
179
+ "untag",
180
+ operation_id,
181
+ "--tag",
182
+ "cli_enrich_tag",
183
+ "--flt",
184
+ json.dumps({"operation_ids": [operation_id]}),
185
+ "--wait",
186
+ config_dir=config_dir,
187
+ )
188
+ assert untag_result.returncode == 0, untag_result.stderr or untag_result.stdout
189
+
190
+ async def _verify() -> None:
191
+ async with GulpClient("http://localhost:8080") as client:
192
+ await client.auth.login("admin", "admin")
193
+ fetched = await client.queries.query_single_id(operation_id, doc_id)
194
+ assert isinstance(fetched, dict)
195
+ tags = fetched.get("gulp.tags") or fetched.get("gulp", {}).get(
196
+ "tags", []
197
+ )
198
+ assert "cli_enrich_tag" not in tags
199
+ assert "cli_enrich_field" not in fetched
200
+
201
+ asyncio.run(_verify())
202
+ finally:
203
+ try:
204
+ asyncio.run(_cleanup_operation(operation_id))
205
+ except Exception:
206
+ pass
207
+ try:
208
+ shutil.rmtree(config_dir, ignore_errors=True)
209
+ except Exception:
210
+ pass
211
+
212
+
213
+ @pytest.mark.integration
214
+ def test_enrich_cli_documents_and_single_id(tmp_path: Path) -> None:
215
+ config_dir = tmp_path / "cli-config"
216
+ config_dir.mkdir(parents=True, exist_ok=True)
217
+
218
+ login = _run_cli(
219
+ "auth",
220
+ "login",
221
+ "--url",
222
+ "http://localhost:8080",
223
+ "--username",
224
+ "admin",
225
+ "--password",
226
+ "admin",
227
+ config_dir=config_dir,
228
+ )
229
+ assert login.returncode == 0, login.stderr or login.stdout
230
+
231
+ operation_id, doc_id = asyncio.run(_setup_sample_docs())
232
+
233
+ login = _run_cli(
234
+ "auth",
235
+ "login",
236
+ "--url",
237
+ "http://localhost:8080",
238
+ "--username",
239
+ "admin",
240
+ "--password",
241
+ "admin",
242
+ config_dir=config_dir,
243
+ )
244
+ assert login.returncode == 0, login.stderr or login.stdout
245
+
246
+ try:
247
+ documents_result = _run_cli(
248
+ "enrich",
249
+ "documents",
250
+ operation_id,
251
+ "--plugin",
252
+ "enrich_whois",
253
+ "--fields",
254
+ json.dumps({"source.ip": None}),
255
+ "--flt",
256
+ json.dumps({"operation_ids": [operation_id]}),
257
+ "--plugin-params",
258
+ json.dumps({"custom_parameters": {}}),
259
+ "--wait",
260
+ config_dir=config_dir,
261
+ )
262
+
263
+ if documents_result.returncode != 0:
264
+ pytest.skip(
265
+ f"enrich_whois unavailable for CLI documents test: {documents_result.stderr or documents_result.stdout}"
266
+ )
267
+
268
+ single_result = _run_cli(
269
+ "enrich",
270
+ "single-id",
271
+ operation_id,
272
+ doc_id,
273
+ "--plugin",
274
+ "enrich_whois",
275
+ "--fields",
276
+ json.dumps({"sdk_cli_whois": "8.8.8.8"}),
277
+ "--plugin-params",
278
+ json.dumps({"custom_parameters": {}}),
279
+ config_dir=config_dir,
280
+ )
281
+ if single_result.returncode != 0:
282
+ pytest.skip(
283
+ f"enrich_whois unavailable for CLI single-id test: {single_result.stderr or single_result.stdout}"
284
+ )
285
+
286
+ async def _verify() -> None:
287
+ async with GulpClient("http://localhost:8080") as client:
288
+ await client.auth.login("admin", "admin")
289
+ fetched = await client.queries.query_single_id(operation_id, doc_id)
290
+ assert isinstance(fetched, dict)
291
+ assert fetched.get("_id") == doc_id or fetched.get("id") == doc_id
292
+
293
+ asyncio.run(_verify())
294
+ finally:
295
+ try:
296
+ asyncio.run(_cleanup_operation(operation_id))
297
+ except Exception:
298
+ pass
299
+ try:
300
+ shutil.rmtree(config_dir, ignore_errors=True)
301
+ except Exception:
302
+ pass
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes