obvyr-cli 2.7.0__tar.gz → 2.9.0__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.
- {obvyr_cli-2.7.0 → obvyr_cli-2.9.0}/PKG-INFO +2 -3
- {obvyr_cli-2.7.0 → obvyr_cli-2.9.0}/README_PYPI.md +1 -1
- {obvyr_cli-2.7.0 → obvyr_cli-2.9.0}/pyproject.toml +1 -3
- {obvyr_cli-2.7.0 → obvyr_cli-2.9.0}/src/obvyr_cli/api_client.py +1 -3
- {obvyr_cli-2.7.0 → obvyr_cli-2.9.0}/src/obvyr_cli/archive_builder.py +12 -8
- {obvyr_cli-2.7.0 → obvyr_cli-2.9.0}/src/obvyr_cli/cli.py +34 -6
- {obvyr_cli-2.7.0 → obvyr_cli-2.9.0}/src/obvyr_cli/config.py +13 -0
- obvyr_cli-2.9.0/src/obvyr_cli/utils.py +6 -0
- obvyr_cli-2.7.0/src/obvyr_cli/utils.py +0 -13
- {obvyr_cli-2.7.0 → obvyr_cli-2.9.0}/src/obvyr_cli/__init__.py +0 -0
- {obvyr_cli-2.7.0 → obvyr_cli-2.9.0}/src/obvyr_cli/command_wrapper.py +0 -0
- {obvyr_cli-2.7.0 → obvyr_cli-2.9.0}/src/obvyr_cli/error_handling.py +0 -0
- {obvyr_cli-2.7.0 → obvyr_cli-2.9.0}/src/obvyr_cli/jwt_utils.py +0 -0
- {obvyr_cli-2.7.0 → obvyr_cli-2.9.0}/src/obvyr_cli/logging_config.py +0 -0
- {obvyr_cli-2.7.0 → obvyr_cli-2.9.0}/src/obvyr_cli/redaction.py +0 -0
- {obvyr_cli-2.7.0 → obvyr_cli-2.9.0}/src/obvyr_cli/schemas.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: obvyr-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9.0
|
|
4
4
|
Summary: Command-line wrapper to allow Obvyr to capture evidence from any machine.
|
|
5
5
|
License: Proprietary
|
|
6
6
|
Author: Wyrd Technology Ltd
|
|
@@ -16,7 +16,6 @@ Requires-Dist: pydantic (==2.13.5)
|
|
|
16
16
|
Requires-Dist: pydantic-settings (==2.15.0)
|
|
17
17
|
Requires-Dist: python-dotenv (==1.2.4)
|
|
18
18
|
Requires-Dist: tenacity (==9.1.4)
|
|
19
|
-
Requires-Dist: toml (==0.10.2)
|
|
20
19
|
Requires-Dist: zstandard (==0.25.0)
|
|
21
20
|
Project-URL: Homepage, https://wyrd-technology.com
|
|
22
21
|
Description-Content-Type: text/markdown
|
|
@@ -39,7 +38,7 @@ Command-line wrapper to run Obvyr agents locally or in CI.
|
|
|
39
38
|
|
|
40
39
|
## Python Support
|
|
41
40
|
|
|
42
|
-
Python 3.
|
|
41
|
+
Python 3.14+ (running through `uvx obvyr-cli` fetches it automatically)
|
|
43
42
|
|
|
44
43
|
## Licence
|
|
45
44
|
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "obvyr-cli"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.9.0"
|
|
8
8
|
description = "Command-line wrapper to allow Obvyr to capture evidence from any machine."
|
|
9
9
|
authors = ["Wyrd Technology Ltd"]
|
|
10
10
|
license = "Proprietary"
|
|
@@ -34,7 +34,6 @@ pydantic = "2.13.5"
|
|
|
34
34
|
pydantic-settings = "2.15.0"
|
|
35
35
|
httpx = "0.28.1"
|
|
36
36
|
tenacity = "9.1.4"
|
|
37
|
-
toml = "0.10.2"
|
|
38
37
|
orjson = "3.12.0"
|
|
39
38
|
zstandard = "0.25.0"
|
|
40
39
|
|
|
@@ -46,7 +45,6 @@ pytest-cov = "7.1.0"
|
|
|
46
45
|
black = "26.5.1"
|
|
47
46
|
pyproject-fmt = "2.29.4"
|
|
48
47
|
pytest-subprocess = "1.6.0"
|
|
49
|
-
types-toml = "^0.10.8.20240310"
|
|
50
48
|
poethepoet = "0.48.0"
|
|
51
49
|
|
|
52
50
|
[tool.poetry.scripts]
|
|
@@ -16,8 +16,6 @@ from obvyr_cli.logging_config import get_logger
|
|
|
16
16
|
|
|
17
17
|
logger = get_logger("api_client")
|
|
18
18
|
|
|
19
|
-
project_config = utils.get_project_config()
|
|
20
|
-
|
|
21
19
|
|
|
22
20
|
class ObvyrAPIClient:
|
|
23
21
|
"""Client for sending execution data to the Obvyr API."""
|
|
@@ -43,7 +41,7 @@ class ObvyrAPIClient:
|
|
|
43
41
|
"""Generate headers for authentication."""
|
|
44
42
|
return {
|
|
45
43
|
"Authorization": (f"Bearer {self.api_key}"),
|
|
46
|
-
"User-Agent": f"obvyr-cli/{
|
|
44
|
+
"User-Agent": f"obvyr-cli/{utils.get_version()}",
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
@retry(
|
|
@@ -9,6 +9,7 @@ import io
|
|
|
9
9
|
import json
|
|
10
10
|
import logging
|
|
11
11
|
import mimetypes
|
|
12
|
+
import os
|
|
12
13
|
import pathlib
|
|
13
14
|
import tarfile
|
|
14
15
|
import tempfile
|
|
@@ -227,9 +228,6 @@ def build_artifacts_tar_zst(
|
|
|
227
228
|
Invalid or oversized attachments are skipped with warning logs.
|
|
228
229
|
Attachments are prioritized by value (XML/JSON > YAML/CSV > TXT/LOG).
|
|
229
230
|
"""
|
|
230
|
-
if tmp_dir is None:
|
|
231
|
-
tmp_dir = pathlib.Path(tempfile.gettempdir())
|
|
232
|
-
|
|
233
231
|
# Select valid attachments with priority-based ordering
|
|
234
232
|
selected_attachments: list[pathlib.Path] = []
|
|
235
233
|
if attachment_paths:
|
|
@@ -237,9 +235,6 @@ def build_artifacts_tar_zst(
|
|
|
237
235
|
attachment_paths
|
|
238
236
|
)
|
|
239
237
|
|
|
240
|
-
# Create output file path
|
|
241
|
-
output_path = tmp_dir / "artifacts.tar.zst"
|
|
242
|
-
|
|
243
238
|
# Prepare command.json data exactly as specified in the doc
|
|
244
239
|
command_data = {
|
|
245
240
|
"command": run_command_response.command,
|
|
@@ -288,8 +283,17 @@ def build_artifacts_tar_zst(
|
|
|
288
283
|
compressor = zstd.ZstdCompressor(write_content_size=True)
|
|
289
284
|
compressed_data = compressor.compress(tar_data)
|
|
290
285
|
|
|
291
|
-
|
|
292
|
-
|
|
286
|
+
# mkstemp creates a unique file readable and writable by the owner only
|
|
287
|
+
output_fd, output_name = tempfile.mkstemp(
|
|
288
|
+
prefix="obvyr-", suffix=".tar.zst", dir=tmp_dir
|
|
289
|
+
)
|
|
290
|
+
output_path = pathlib.Path(output_name)
|
|
291
|
+
try:
|
|
292
|
+
with os.fdopen(output_fd, "wb") as output_file:
|
|
293
|
+
output_file.write(compressed_data)
|
|
294
|
+
except Exception:
|
|
295
|
+
output_path.unlink(missing_ok=True)
|
|
296
|
+
raise
|
|
293
297
|
|
|
294
298
|
return output_path
|
|
295
299
|
|
|
@@ -187,6 +187,29 @@ def fetch_active_profile(
|
|
|
187
187
|
return active_profile
|
|
188
188
|
|
|
189
189
|
|
|
190
|
+
def resolve_active_profile(
|
|
191
|
+
settings: Settings, profile_name: str | None = None
|
|
192
|
+
) -> ProfileSettings | None:
|
|
193
|
+
"""
|
|
194
|
+
Retrieve the active profile, or warn and return None when it is missing.
|
|
195
|
+
|
|
196
|
+
A missing profile must never stop the wrapped command from running, so the
|
|
197
|
+
caller carries on without reporting to Obvyr.
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
return fetch_active_profile(settings, profile_name)
|
|
201
|
+
except ValueError as e:
|
|
202
|
+
display_name = (profile_name or "DEFAULT").strip().upper()
|
|
203
|
+
log_customer_friendly_error(
|
|
204
|
+
str(e),
|
|
205
|
+
f"Profile '{display_name}' not found, so nothing is being "
|
|
206
|
+
"reported to Obvyr. Your command will run unchanged. Set "
|
|
207
|
+
f"OBVYR_PROFILES__{display_name}__API_KEY to enable reporting.",
|
|
208
|
+
level=logging.WARNING,
|
|
209
|
+
)
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
|
|
190
213
|
def check_token_expiry(active_profile: ProfileSettings) -> None:
|
|
191
214
|
"""Warn when the active profile's API token is near or past expiry."""
|
|
192
215
|
expiry = get_jwt_expiry(active_profile.API_KEY)
|
|
@@ -440,8 +463,10 @@ def cli_run_process(
|
|
|
440
463
|
):
|
|
441
464
|
return
|
|
442
465
|
|
|
443
|
-
active_profile =
|
|
444
|
-
|
|
466
|
+
active_profile = resolve_active_profile(settings, profile)
|
|
467
|
+
|
|
468
|
+
if active_profile:
|
|
469
|
+
check_token_expiry(active_profile)
|
|
445
470
|
|
|
446
471
|
# Create streaming callback for real-time output display
|
|
447
472
|
def stream_output(line: str) -> None:
|
|
@@ -454,15 +479,18 @@ def cli_run_process(
|
|
|
454
479
|
config=execution_config,
|
|
455
480
|
)
|
|
456
481
|
|
|
457
|
-
if
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
482
|
+
if active_profile:
|
|
483
|
+
if dry_run:
|
|
484
|
+
show_dry_run_payload(active_profile, response)
|
|
485
|
+
elif active_profile.API_URL and active_profile.API_KEY:
|
|
486
|
+
send_to_api(active_profile, response)
|
|
461
487
|
|
|
462
488
|
display_execution_summary(response)
|
|
463
489
|
|
|
464
490
|
if response.returncode != 0:
|
|
465
491
|
raise click.exceptions.Exit(response.returncode)
|
|
466
492
|
|
|
493
|
+
except click.exceptions.Exit:
|
|
494
|
+
raise
|
|
467
495
|
except Exception as e:
|
|
468
496
|
raise click.ClickException(str(e)) from e
|
|
@@ -9,6 +9,7 @@ import logging
|
|
|
9
9
|
import re
|
|
10
10
|
import secrets
|
|
11
11
|
from typing import Dict, Literal
|
|
12
|
+
from urllib.parse import urlparse
|
|
12
13
|
|
|
13
14
|
from dotenv import load_dotenv
|
|
14
15
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
@@ -29,6 +30,7 @@ class ProfileSettings(BaseModel):
|
|
|
29
30
|
ATTACHMENT_MAX_AGE_SECONDS: int = 10
|
|
30
31
|
TIMEOUT: float = 10.0
|
|
31
32
|
VERIFY_SSL: bool = True
|
|
33
|
+
ALLOW_INSECURE_API_URL: bool = False
|
|
32
34
|
TAGS: list[str] = Field(default_factory=list)
|
|
33
35
|
ENV_MODE: Literal["allowlist", "none"] = "allowlist"
|
|
34
36
|
ENV_INCLUDE: list[str] = Field(default_factory=list)
|
|
@@ -61,6 +63,17 @@ class ProfileSettings(BaseModel):
|
|
|
61
63
|
) from error
|
|
62
64
|
return value
|
|
63
65
|
|
|
66
|
+
@model_validator(mode="after")
|
|
67
|
+
def require_https_api_url(self) -> "ProfileSettings":
|
|
68
|
+
"""Fail fast on an API_URL that would send the token and archive in clear text."""
|
|
69
|
+
is_https = urlparse(self.API_URL).scheme.lower() == "https"
|
|
70
|
+
if self.API_URL and not is_https and not self.ALLOW_INSECURE_API_URL:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"API_URL '{self.API_URL}' must use HTTPS. "
|
|
73
|
+
"Set ALLOW_INSECURE_API_URL to true to override."
|
|
74
|
+
)
|
|
75
|
+
return self
|
|
76
|
+
|
|
64
77
|
@model_validator(mode="after")
|
|
65
78
|
def migrate_attachment_path(self) -> "ProfileSettings":
|
|
66
79
|
"""Migrate old ATTACHMENT_PATH to ATTACHMENT_PATHS."""
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
from importlib.metadata import version
|
|
2
|
-
|
|
3
|
-
import toml
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def get_project_config(pyproject_path: str = "pyproject.toml") -> dict:
|
|
7
|
-
try:
|
|
8
|
-
with open(pyproject_path, "r") as file:
|
|
9
|
-
config = toml.load(file)
|
|
10
|
-
return config["tool"]["poetry"]
|
|
11
|
-
except (KeyError, FileNotFoundError):
|
|
12
|
-
# Fall back to package metadata for installed versions
|
|
13
|
-
return {"version": version("obvyr-cli")}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|