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.
- {gulp_cli-1.1.4/src/gulp_cli.egg-info → gulp_cli-1.1.5}/PKG-INFO +1 -1
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/docs/command-reference.md +5 -2
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/docs/examples.md +11 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/_version.py +3 -3
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/cli.py +3 -0
- gulp_cli-1.1.5/src/gulp_cli/commands/enrich.py +187 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/ingest.py +4 -1
- {gulp_cli-1.1.4 → gulp_cli-1.1.5/src/gulp_cli.egg-info}/PKG-INFO +1 -1
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli.egg-info/SOURCES.txt +3 -1
- gulp_cli-1.1.5/tests/test_enrich_live.py +302 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/.github/workflows/portable-bundles.yml +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/.github/workflows/python-package.yml +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/.gitignore +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/GULP_DUAL_LICENSE.txt +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/LICENSE.AGPL-3.0.txt +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/LICENSE.md +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/README.md +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/docs/extensions.md +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/docs/getting-started.md +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/docs/portable.md +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/docs/resource-management.md +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/docs/troubleshooting-cli.md +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/gulp-cli.spec +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/portable/launch-linux.sh +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/portable/launch-macos.sh +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/portable/launch-windows.bat +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/pyproject.toml +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/setup.cfg +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/__init__.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/__main__.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/client.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/__init__.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/acl.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/auth.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/collab.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/context.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/db.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/enhance_map.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/glyph.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/mapping.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/operations.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/plugin.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/query.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/source.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/stats.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/storage.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/user_group.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/commands/users.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/config.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/extension/__init__.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/extension/query_sigma_zip.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/extension/story.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/extension_helpers.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/extensions.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/output.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli/utils.py +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli.egg-info/dependency_links.txt +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli.egg-info/entry_points.txt +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli.egg-info/requires.txt +0 -0
- {gulp_cli-1.1.4 → gulp_cli-1.1.5}/src/gulp_cli.egg-info/top_level.txt +0 -0
|
@@ -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 '{"
|
|
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 1,
|
|
21
|
+
__version__ = version = '1.1.5'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 1, 5)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
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
|
-
|
|
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(
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|