gulp-cli 1.0.8__tar.gz → 1.0.9__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 (49) hide show
  1. {gulp_cli-1.0.8/src/gulp_cli.egg-info → gulp_cli-1.0.9}/PKG-INFO +1 -1
  2. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/docs/command-reference.md +44 -1
  3. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/docs/examples.md +28 -0
  4. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/_version.py +3 -3
  5. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/commands/ingest.py +187 -2
  6. {gulp_cli-1.0.8 → gulp_cli-1.0.9/src/gulp_cli.egg-info}/PKG-INFO +1 -1
  7. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/.github/workflows/python-package.yml +0 -0
  8. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/.gitignore +0 -0
  9. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/README.md +0 -0
  10. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/docs/extensions.md +0 -0
  11. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/docs/getting-started.md +0 -0
  12. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/docs/resource-management.md +0 -0
  13. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/docs/troubleshooting-cli.md +0 -0
  14. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/pyproject.toml +0 -0
  15. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/setup.cfg +0 -0
  16. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/__init__.py +0 -0
  17. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/__main__.py +0 -0
  18. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/cli.py +0 -0
  19. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/client.py +0 -0
  20. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/commands/__init__.py +0 -0
  21. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/commands/acl.py +0 -0
  22. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/commands/auth.py +0 -0
  23. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/commands/collab.py +0 -0
  24. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/commands/context.py +0 -0
  25. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/commands/db.py +0 -0
  26. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/commands/enhance_map.py +0 -0
  27. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/commands/glyph.py +0 -0
  28. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/commands/mapping.py +0 -0
  29. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/commands/operations.py +0 -0
  30. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/commands/plugin.py +0 -0
  31. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/commands/query.py +0 -0
  32. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/commands/source.py +0 -0
  33. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/commands/stats.py +0 -0
  34. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/commands/storage.py +0 -0
  35. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/commands/user_group.py +0 -0
  36. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/commands/users.py +0 -0
  37. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/config.py +0 -0
  38. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/extension/__init__.py +0 -0
  39. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/extension/query_sigma_zip.py +0 -0
  40. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/extension/story.py +0 -0
  41. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/extension_helpers.py +0 -0
  42. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/extensions.py +0 -0
  43. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/output.py +0 -0
  44. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli/utils.py +0 -0
  45. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli.egg-info/SOURCES.txt +0 -0
  46. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli.egg-info/dependency_links.txt +0 -0
  47. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli.egg-info/entry_points.txt +0 -0
  48. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/src/gulp_cli.egg-info/requires.txt +0 -0
  49. {gulp_cli-1.0.8 → gulp_cli-1.0.9}/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.0.8
3
+ Version: 1.0.9
4
4
  Summary: Command-line client for gULP
5
5
  Author-email: Mentat <info@mentat.is>
6
6
  Requires-Python: >=3.12
@@ -520,6 +520,8 @@ gulp-cli ingest file-to-source source123 '/new/*.evtx'
520
520
 
521
521
  Ingest from a ZIP archive.
522
522
 
523
+ > TO BE DEPRECATED...
524
+
523
525
  ```bash
524
526
  gulp-cli ingest zip OPERATION_ID ZIP_FILE [OPTIONS]
525
527
  ```
@@ -527,7 +529,7 @@ gulp-cli ingest zip OPERATION_ID ZIP_FILE [OPTIONS]
527
529
  **Arguments:**
528
530
 
529
531
  - `OPERATION_ID` — Target operation
530
- - `ZIP_FILE` — Path to ZIP file
532
+ - `ZIP_FILE` — Path to ZIP file containing a `metadata.json` in the root, describing the content as specified in gulp's `ingest_zip` docs.
531
533
 
532
534
  **Options:**
533
535
 
@@ -546,6 +548,47 @@ gulp-cli ingest zip my_op /data/evidence.zip --create-operation
546
548
 
547
549
  ---
548
550
 
551
+ #### `zip-create`
552
+
553
+ Create a ZIP archive from one or more source path expressions.
554
+
555
+ Path expressions may be:
556
+
557
+ - a single file path
558
+ - a directory path
559
+ - a glob mask (for example `/bla/somef*.txt`, `/bla/*`, `**/*.evtx`)
560
+
561
+ Environment variables and home shortcuts are expanded in all input paths (for example `$EVIDENCE_DIR/*.evtx`, `~/cases/case-01/*`).
562
+
563
+ ```bash
564
+ gulp-cli ingest zip-create OUTPUT_ZIP [PATH_PATTERN...] [OPTIONS]
565
+ ```
566
+
567
+ **Arguments:**
568
+
569
+ - `OUTPUT_ZIP` — Destination ZIP path (supports environment variables and `~`)
570
+ - `PATH_PATTERN` — File, directory, or glob path expression (multiple allowed)
571
+
572
+ **Options:**
573
+
574
+ - `--paths-file TEXT` — Text file with one source path expression per line (supports environment variables and `~` per line)
575
+ - `--overwrite` — Overwrite output ZIP if it already exists
576
+
577
+ **Examples:**
578
+
579
+ ```bash
580
+ # Create a ZIP from mixed path expressions
581
+ gulp-cli ingest zip-create /tmp/evidence.zip /data/host1/*.evtx /data/host2 /data/readme.txt
582
+
583
+ # Use environment variables and home shortcuts
584
+ gulp-cli ingest zip-create '$CASE_DIR/evidence.zip' '$CASE_DIR/raw/*.json' '~/pcaps/*.pcap' --overwrite
585
+
586
+ # Read source path expressions from a file
587
+ gulp-cli ingest zip-create /tmp/evidence.zip --paths-file /tmp/evidence_paths.txt --overwrite
588
+ ```
589
+
590
+ ---
591
+
549
592
  #### `raw`
550
593
 
551
594
  Ingest raw payload chunks into an operation.
@@ -9,6 +9,7 @@
9
9
  - [JSON Logs Ingestion](#json-logs-ingestion)
10
10
  - [Add More Evidence to Existing Source](#add-more-evidence-to-existing-source)
11
11
  - [ZIP Archive Ingestion](#zip-archive-ingestion)
12
+ - [Create ZIP Archive from Paths and Masks](#create-zip-archive-from-paths-and-masks)
12
13
  - [Raw Payload Ingestion](#raw-payload-ingestion)
13
14
  - [Request Stats Monitoring Workflows](#request-stats-monitoring-workflows)
14
15
  - [Monitor Ongoing Requests (Live)](#monitor-ongoing-requests-live)
@@ -175,6 +176,10 @@ gulp-cli ingest file incident-001 csv /data/access_log.csv \
175
176
  # pass mapping directly without using a mapping file
176
177
  gulp-cli ingest file test_operation csv ./samples/mftecmd/sample_record.csv --plugin-params '{ "mapping_parameters": { "mappings": { "test
177
178
  _mapping": { "fields": { "Created0x10": { "ecs": [ "@timestamp" ] } } } } } }' --reset-operation --wait
179
+
180
+ # pass mapping using a gulp mapping file with mapping_id to specify which mapping to use in the file
181
+ gulp-cli ingest file test_operation csv ./samples/mftecmd/sample_record.csv --plugin-params '{ "mapping_parameters"
182
+ : { "mapping_file": "mftecmd_csv.json", "mapping_id": "record" } }' --wait --reset-operation
178
183
  ```
179
184
 
180
185
  ### JSON Logs Ingestion
@@ -210,6 +215,29 @@ gulp-cli ingest zip incident-001 /evidence/evidence.zip --wait
210
215
  gulp-cli ingest zip incident-001 /evidence/evidence.zip --create-operation
211
216
  ```
212
217
 
218
+ ### Create ZIP Archive from Paths and Masks
219
+
220
+ ```bash
221
+ # Build ZIP from mixed sources: file, directory, and wildcard mask
222
+ gulp-cli ingest zip-create /evidence/evidence.zip /forensic/host1/*.evtx /forensic/host2 /forensic/notes.txt --overwrite
223
+
224
+ # Use environment variables and ~ in source expressions and output path
225
+ export CASE_ROOT=~/cases/incident-001
226
+ gulp-cli ingest zip-create '$CASE_ROOT/evidence.zip' '$CASE_ROOT/windows/*.evtx' '$CASE_ROOT/network/*' --overwrite
227
+
228
+ # Build ZIP from a text file (one path expression per line)
229
+ cat > /tmp/evidence_paths.txt <<'EOF'
230
+ $CASE_ROOT/windows/*.evtx
231
+ $CASE_ROOT/linux/**/*.log
232
+ ~/captures/*.pcap
233
+ EOF
234
+
235
+ gulp-cli ingest zip-create '$CASE_ROOT/evidence.zip' --paths-file /tmp/evidence_paths.txt --overwrite
236
+
237
+ # Then ingest the generated ZIP
238
+ gulp-cli ingest zip incident-001 '$CASE_ROOT/evidence.zip' --wait
239
+ ```
240
+
213
241
  ### Raw Payload Ingestion
214
242
 
215
243
  ```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.0.8'
22
- __version_tuple__ = version_tuple = (1, 0, 8)
21
+ __version__ = version = '1.0.9'
22
+ __version_tuple__ = version_tuple = (1, 0, 9)
23
23
 
24
- __commit_id__ = commit_id = 'gda10ffa52'
24
+ __commit_id__ = commit_id = 'gb7a79dce7'
@@ -2,7 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import json
5
- from glob import glob
5
+ import os
6
+ import zipfile
7
+ from glob import glob, has_magic
6
8
  from pathlib import Path
7
9
  from typing import Any
8
10
 
@@ -64,6 +66,140 @@ def _expand_file_patterns(file_patterns: list[str]) -> list[str]:
64
66
  return unique_files
65
67
 
66
68
 
69
+ def _resolve_path(raw_path: str) -> Path:
70
+ expanded = _expand_path_expression(raw_path)
71
+ return Path(expanded).resolve()
72
+
73
+
74
+ def _expand_path_expression(raw_path: str) -> str:
75
+ """Expand shell-style path expressions (env vars and '~')."""
76
+ return os.path.expandvars(os.path.expanduser(raw_path.strip()))
77
+
78
+
79
+ def _expand_zip_source_patterns(
80
+ path_patterns: list[str],
81
+ paths_file: str | None,
82
+ ) -> list[tuple[Path, Path]]:
83
+ if path_patterns and paths_file is not None:
84
+ raise typer.BadParameter(
85
+ "Provide paths either as arguments or via --paths-file, not both"
86
+ )
87
+ if not path_patterns and paths_file is None:
88
+ raise typer.BadParameter(
89
+ "Provide at least one path argument or --paths-file"
90
+ )
91
+
92
+ raw_entries = list(path_patterns)
93
+ if paths_file is not None:
94
+ file_path = _resolve_path(paths_file)
95
+ if not file_path.is_file():
96
+ raise typer.BadParameter(f"Paths file not found: {file_path}")
97
+ raw_entries.extend(
98
+ line.strip()
99
+ for line in file_path.read_text(encoding="utf-8").splitlines()
100
+ if line.strip() and not line.lstrip().startswith("#")
101
+ )
102
+
103
+ expanded_paths: list[tuple[Path, Path]] = []
104
+ seen: set[tuple[Path, Path]] = set()
105
+ for raw_entry in raw_entries:
106
+ expanded_entry = _expand_path_expression(raw_entry)
107
+ if not expanded_entry:
108
+ continue
109
+
110
+ if has_magic(expanded_entry):
111
+ pattern_path = Path(expanded_entry)
112
+ base_dir = _resolve_glob_base(pattern_path)
113
+ matches = [Path(match).resolve() for match in sorted(glob(expanded_entry, recursive=True))]
114
+ if not matches:
115
+ raise typer.BadParameter(f"Path mask did not match any files: {expanded_entry}")
116
+
117
+ for match in matches:
118
+ item = (match, base_dir)
119
+ if item not in seen:
120
+ seen.add(item)
121
+ expanded_paths.append(item)
122
+ continue
123
+
124
+ resolved_path = _resolve_path(expanded_entry)
125
+ if not resolved_path.exists():
126
+ raise typer.BadParameter(f"Path not found: {resolved_path}")
127
+
128
+ base_dir = resolved_path.parent
129
+ item = (resolved_path, base_dir)
130
+ if item not in seen:
131
+ seen.add(item)
132
+ expanded_paths.append(item)
133
+
134
+ return expanded_paths
135
+
136
+
137
+ def _resolve_glob_base(pattern_path: Path) -> Path:
138
+ parts = pattern_path.parts
139
+ if pattern_path.is_absolute():
140
+ base_dir = Path(pattern_path.anchor)
141
+ part_index = 1
142
+ else:
143
+ base_dir = Path.cwd()
144
+ part_index = 0
145
+
146
+ for part in parts[part_index:]:
147
+ if has_magic(part):
148
+ break
149
+ base_dir /= part
150
+
151
+ return base_dir.resolve()
152
+
153
+
154
+ def _build_zip_from_sources(output_zip: Path, sources: list[tuple[Path, Path]]) -> tuple[int, list[str]]:
155
+ output_zip.parent.mkdir(parents=True, exist_ok=True)
156
+
157
+ archived_entries: list[str] = []
158
+ archived_count = 0
159
+ seen_archive_entries: set[str] = set()
160
+ seen_source_files: set[Path] = set()
161
+
162
+ def _record_directory(archive_name_str: str, archive: zipfile.ZipFile) -> None:
163
+ entry_name = f"{archive_name_str}/"
164
+ if entry_name in seen_archive_entries:
165
+ return
166
+ archive.writestr(entry_name, "")
167
+ seen_archive_entries.add(entry_name)
168
+ archived_entries.append(entry_name)
169
+
170
+ def _record_file(source_path: Path, archive_name_str: str, archive: zipfile.ZipFile) -> None:
171
+ nonlocal archived_count
172
+ if archive_name_str in seen_archive_entries or source_path in seen_source_files:
173
+ return
174
+ archive.write(source_path, arcname=archive_name_str)
175
+ seen_archive_entries.add(archive_name_str)
176
+ seen_source_files.add(source_path)
177
+ archived_entries.append(archive_name_str)
178
+ archived_count += 1
179
+
180
+ with zipfile.ZipFile(output_zip, mode="w", compression=zipfile.ZIP_DEFLATED) as archive:
181
+ for source_path, base_dir in sources:
182
+ archive_root = source_path.relative_to(base_dir)
183
+
184
+ if source_path.is_file():
185
+ _record_file(source_path, archive_root.as_posix(), archive)
186
+ continue
187
+
188
+ child_paths = sorted(source_path.rglob("*"))
189
+ if not child_paths:
190
+ _record_directory(archive_root.as_posix(), archive)
191
+ continue
192
+
193
+ for current_path in child_paths:
194
+ archive_name_str = current_path.relative_to(base_dir).as_posix()
195
+ if current_path.is_dir():
196
+ _record_directory(archive_name_str, archive)
197
+ continue
198
+ _record_file(current_path, archive_name_str, archive)
199
+
200
+ return archived_count, archived_entries
201
+
202
+
67
203
  async def _wait_for_stats_create(
68
204
  client: Any,
69
205
  req_ids: list[str],
@@ -600,7 +736,10 @@ def ingest_file_to_source(
600
736
  @app.command("zip")
601
737
  def ingest_zip(
602
738
  operation_id: str,
603
- zip_file: str,
739
+ zip_file: str = typer.Argument(
740
+ ...,
741
+ help="Path to a ZIP file which must contain a `metadata.json` in the root, describing the content as specified in gulp's `ingest_zip` docs.",
742
+ ),
604
743
  context_name: str = typer.Option("sdk_context", "--context-name"),
605
744
  flt: str | None = typer.Option(None, "--flt", help="JSON object for GulpIngestionFilter"),
606
745
  reset_operation: bool = typer.Option(
@@ -691,6 +830,52 @@ def ingest_zip(
691
830
  asyncio.run(_run())
692
831
 
693
832
 
833
+ @app.command("zip-create")
834
+ def ingest_zip_create(
835
+ output_zip: str = typer.Argument(
836
+ ...,
837
+ help="Path to the ZIP file to create (supports $VARS and ~)",
838
+ ),
839
+ path_patterns: list[str] = typer.Argument(
840
+ None,
841
+ help="One or more files, directories, or glob patterns to archive (supports $VARS and ~)",
842
+ ),
843
+ paths_file: str | None = typer.Option(
844
+ None,
845
+ "--paths-file",
846
+ help="Text file with one file, directory, or glob pattern per line (supports $VARS and ~ per line)",
847
+ ),
848
+ overwrite: bool = typer.Option(
849
+ False,
850
+ "--overwrite",
851
+ help="Overwrite the ZIP file if it already exists",
852
+ ),
853
+ ) -> None:
854
+ """Create a ZIP archive from files, directories, or glob patterns."""
855
+
856
+ output_path = _resolve_path(output_zip)
857
+ source_paths = _expand_zip_source_patterns(path_patterns or [], paths_file)
858
+
859
+ if output_path.exists() and not overwrite:
860
+ raise typer.BadParameter(
861
+ f"Output ZIP already exists: {output_path}. Use --overwrite to replace it"
862
+ )
863
+
864
+ archived_count, archived_entries = _build_zip_from_sources(output_path, source_paths)
865
+ print_result(
866
+ {
867
+ "zip_file": str(output_path),
868
+ "sources": [str(path) for path, _ in source_paths],
869
+ "files_archived": archived_count,
870
+ "entries": archived_entries,
871
+ },
872
+ formatter=lambda data: console.print(
873
+ f"Created ZIP {data['zip_file']} from {len(data['sources'])} source"
874
+ f"{'s' if len(data['sources']) != 1 else ''} with {data['files_archived']} file(s)."
875
+ ),
876
+ )
877
+
878
+
694
879
  @app.command("raw")
695
880
  def ingest_raw(
696
881
  operation_id: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gulp-cli
3
- Version: 1.0.8
3
+ Version: 1.0.9
4
4
  Summary: Command-line client for gULP
5
5
  Author-email: Mentat <info@mentat.is>
6
6
  Requires-Python: >=3.12
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes