asp-cli 0.1.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.
@@ -0,0 +1,40 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ *.egg
8
+
9
+ # Virtual environment
10
+ .venv/
11
+
12
+ # Django
13
+ *.sqlite3
14
+ media/
15
+ staticfiles/
16
+
17
+ # Environment
18
+ .env
19
+ .env.local
20
+
21
+ # IDE
22
+ .idea/
23
+ .vscode/
24
+ *.swp
25
+ *.swo
26
+
27
+ # Node
28
+ node_modules/
29
+
30
+ # Local git worktrees
31
+ .worktrees/
32
+ /asf-doc/
33
+ /asp-marketplace/
34
+
35
+ # OS
36
+ .DS_Store
37
+ Thumbs.db
38
+
39
+ .claude/*
40
+ .asp/
asp_cli-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Agentic SOC Platform contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
asp_cli-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.4
2
+ Name: asp-cli
3
+ Version: 0.1.0
4
+ Summary: Command line client for Agentic SOC Platform
5
+ Project-URL: Homepage, https://github.com/FunnyWolf/agentic-soc-platform
6
+ Project-URL: Repository, https://github.com/FunnyWolf/agentic-soc-platform
7
+ Project-URL: Issues, https://github.com/FunnyWolf/agentic-soc-platform/issues
8
+ Author: Agentic SOC Platform contributors
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agentic-soc,cli,security,soc
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Security
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: httpx>=0.28.1
24
+ Requires-Dist: jmespath>=1.0.1
25
+ Requires-Dist: pydantic>=2.13.4
26
+ Requires-Dist: rich>=14.2.0
27
+ Requires-Dist: typer>=0.20.0
28
+ Description-Content-Type: text/markdown
29
+
30
+ # ASP CLI
31
+
32
+ Command line client for Agentic SOC Platform.
33
+
34
+ `asp-cli` provides the `asp` command for SOC analysts and automation agents to authenticate with an ASP server, inspect cases and alerts, add comments, upload files, run playbooks, and query investigation integrations.
35
+
36
+ ## Install
37
+
38
+ ```powershell
39
+ pipx install asp-cli
40
+ ```
41
+
42
+ ## Quick start
43
+
44
+ ```powershell
45
+ asp auth login --api-url https://asp.example.com --api-key asp_xxx
46
+ asp doctor
47
+ asp case list
48
+ ```
49
+
50
+ For automation and agent skills, prefer stable JSON output:
51
+
52
+ ```powershell
53
+ asp case list --output json
54
+ ```
@@ -0,0 +1,25 @@
1
+ # ASP CLI
2
+
3
+ Command line client for Agentic SOC Platform.
4
+
5
+ `asp-cli` provides the `asp` command for SOC analysts and automation agents to authenticate with an ASP server, inspect cases and alerts, add comments, upload files, run playbooks, and query investigation integrations.
6
+
7
+ ## Install
8
+
9
+ ```powershell
10
+ pipx install asp-cli
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```powershell
16
+ asp auth login --api-url https://asp.example.com --api-key asp_xxx
17
+ asp doctor
18
+ asp case list
19
+ ```
20
+
21
+ For automation and agent skills, prefer stable JSON output:
22
+
23
+ ```powershell
24
+ asp case list --output json
25
+ ```
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "asp-cli"
3
+ version = "0.1.0"
4
+ description = "Command line client for Agentic SOC Platform"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "MIT"
8
+ authors = [
9
+ { name = "Agentic SOC Platform contributors" },
10
+ ]
11
+ keywords = ["agentic-soc", "soc", "security", "cli"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Information Technology",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Security",
23
+ ]
24
+ dependencies = [
25
+ "httpx>=0.28.1",
26
+ "jmespath>=1.0.1",
27
+ "pydantic>=2.13.4",
28
+ "rich>=14.2.0",
29
+ "typer>=0.20.0",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/FunnyWolf/agentic-soc-platform"
34
+ Repository = "https://github.com/FunnyWolf/agentic-soc-platform"
35
+ Issues = "https://github.com/FunnyWolf/agentic-soc-platform/issues"
36
+
37
+ [project.scripts]
38
+ asp = "asp_cli.main:run"
39
+
40
+ [build-system]
41
+ requires = ["hatchling>=1.28"]
42
+ build-backend = "hatchling.build"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/asp_cli"]
46
+
47
+ [tool.hatch.build.targets.wheel.force-include]
48
+ "src/asp_cli/spec/operations.json" = "asp_cli/spec/operations.json"
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Any
5
+
6
+ import httpx
7
+ from rich.console import Console
8
+
9
+ from . import __version__
10
+ from .config import redact_secret
11
+ from .errors import (
12
+ CliError,
13
+ EXIT_AUTH,
14
+ EXIT_NETWORK,
15
+ EXIT_NOT_FOUND,
16
+ EXIT_PERMISSION,
17
+ EXIT_SERVER,
18
+ EXIT_USAGE,
19
+ )
20
+
21
+
22
+ class AspClient:
23
+ def __init__(
24
+ self,
25
+ *,
26
+ api_url: str,
27
+ api_key: str | None = None,
28
+ verbose: bool = False,
29
+ console: Console | None = None,
30
+ timeout: float = 20.0,
31
+ ) -> None:
32
+ self.base_url = _normalize_base_url(api_url)
33
+ self.api_key = api_key
34
+ self.verbose = verbose
35
+ self.console = console or Console(stderr=True)
36
+ self.timeout = timeout
37
+
38
+ def health(self) -> dict[str, Any]:
39
+ return self.request("GET", "/api/health/", authenticated=False)
40
+
41
+ def version(self) -> dict[str, Any]:
42
+ return self.request("GET", "/api/agent/v1/version/")
43
+
44
+ def request(self, method: str, path: str, *, authenticated: bool = True, json: Any = None, files: Any = None) -> dict[str, Any]:
45
+ if authenticated and not self.api_key:
46
+ raise CliError("missing_api_key", "API key is required", {}, EXIT_AUTH)
47
+
48
+ headers = {
49
+ "Accept": "application/json",
50
+ "User-Agent": f"asp-cli/{__version__}",
51
+ }
52
+ if authenticated and self.api_key:
53
+ headers["Authorization"] = f"Api-Key {self.api_key}"
54
+
55
+ url = f"{self.base_url}{path}"
56
+ started = time.perf_counter()
57
+ try:
58
+ response = httpx.request(method, url, headers=headers, json=json, files=files, timeout=self.timeout)
59
+ except httpx.HTTPError as exc:
60
+ raise CliError("network_error", f"Unable to reach ASP server: {exc}", {"url": _redact_url(url)}, EXIT_NETWORK) from exc
61
+
62
+ elapsed_ms = int((time.perf_counter() - started) * 1000)
63
+ if self.verbose:
64
+ self.console.print(f"{method} {path} -> {response.status_code} ({elapsed_ms}ms)", style="dim")
65
+
66
+ if response.status_code >= 400:
67
+ self._raise_http_error(response, path)
68
+
69
+ if not response.content:
70
+ return {}
71
+ try:
72
+ payload = response.json()
73
+ except ValueError as exc:
74
+ raise CliError("invalid_response", "Server returned non-JSON response", {"status_code": response.status_code}, EXIT_SERVER) from exc
75
+ if not isinstance(payload, dict):
76
+ raise CliError("invalid_response", "Server response must be a JSON object", {"status_code": response.status_code}, EXIT_SERVER)
77
+ return payload
78
+
79
+ def _raise_http_error(self, response: httpx.Response, path: str) -> None:
80
+ message = _response_message(response)
81
+ details = {"status_code": response.status_code, "path": path}
82
+ if response.status_code == 400:
83
+ raise CliError("bad_request", message, details, EXIT_USAGE)
84
+ if response.status_code == 401:
85
+ raise CliError("authentication_failed", message, details, EXIT_AUTH)
86
+ if response.status_code == 403:
87
+ raise CliError("permission_denied", message, details, EXIT_PERMISSION)
88
+ if response.status_code == 404:
89
+ raise CliError("not_found", message, details, EXIT_NOT_FOUND)
90
+ raise CliError("server_error", message, details, EXIT_SERVER)
91
+
92
+
93
+ def _normalize_base_url(api_url: str) -> str:
94
+ base = api_url.strip().rstrip("/")
95
+ if base.endswith("/api"):
96
+ base = base[:-4]
97
+ if not base:
98
+ raise CliError("missing_api_url", "ASP API URL is required", {}, EXIT_USAGE)
99
+ return base
100
+
101
+
102
+ def _response_message(response: httpx.Response) -> str:
103
+ try:
104
+ payload = response.json()
105
+ except ValueError:
106
+ return response.text.strip() or f"HTTP {response.status_code}"
107
+ if isinstance(payload, dict):
108
+ detail = payload.get("detail")
109
+ if isinstance(detail, str):
110
+ return detail
111
+ error = payload.get("error")
112
+ if isinstance(error, dict) and isinstance(error.get("message"), str):
113
+ return error["message"]
114
+ return f"HTTP {response.status_code}"
115
+
116
+
117
+ def _redact_url(url: str) -> str:
118
+ return url.replace(redact_secret(url), "****") if "asp_" in url else url
@@ -0,0 +1,176 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .errors import CliError, EXIT_CONFIG
10
+
11
+ GLOBAL_SETTINGS_PATH = Path.home() / ".asp" / "settings.json"
12
+ LOCAL_SETTINGS_DIR = ".asp"
13
+ SETTINGS_FILENAME = "settings.json"
14
+ SUPPORTED_KEYS = {"api_url", "api_key"}
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class ResolvedConfig:
19
+ api_url: str | None
20
+ api_key: str | None
21
+ sources: dict[str, str]
22
+ global_path: Path
23
+ local_path: Path | None
24
+
25
+ @property
26
+ def has_auth(self) -> bool:
27
+ return bool(self.api_url and self.api_key)
28
+
29
+
30
+ def resolve_config(*, cwd: Path | None = None, api_url: str | None = None, api_key: str | None = None) -> ResolvedConfig:
31
+ cwd = (cwd or Path.cwd()).resolve()
32
+ global_settings = read_settings(GLOBAL_SETTINGS_PATH)
33
+ local_path = find_local_settings(cwd)
34
+ local_settings = read_settings(local_path) if local_path else {}
35
+ values: dict[str, Any] = {}
36
+ sources: dict[str, str] = {}
37
+
38
+ _merge(values, sources, global_settings, "global")
39
+ if local_path:
40
+ _merge(values, sources, local_settings, "local")
41
+ _merge(
42
+ values,
43
+ sources,
44
+ {
45
+ "api_url": os.environ.get("ASP_API_URL"),
46
+ "api_key": os.environ.get("ASP_API_KEY"),
47
+ },
48
+ "env",
49
+ )
50
+ _merge(values, sources, {"api_url": api_url, "api_key": api_key}, "flags")
51
+
52
+ return ResolvedConfig(
53
+ api_url=_clean(values.get("api_url")),
54
+ api_key=_clean(values.get("api_key")),
55
+ sources=sources,
56
+ global_path=GLOBAL_SETTINGS_PATH,
57
+ local_path=local_path,
58
+ )
59
+
60
+
61
+ def auth_settings_path(*, local: bool, cwd: Path | None = None) -> Path:
62
+ if local:
63
+ return (cwd or Path.cwd()).resolve() / LOCAL_SETTINGS_DIR / SETTINGS_FILENAME
64
+ return GLOBAL_SETTINGS_PATH
65
+
66
+
67
+ def save_auth(*, api_url: str, api_key: str, local: bool = False, cwd: Path | None = None) -> Path:
68
+ path = auth_settings_path(local=local, cwd=cwd)
69
+ settings = read_settings(path)
70
+ settings["api_url"] = api_url.rstrip("/")
71
+ settings["api_key"] = api_key
72
+ write_settings(path, settings)
73
+ return path
74
+
75
+
76
+ def clear_auth(*, local: bool = False, cwd: Path | None = None) -> Path:
77
+ path = auth_settings_path(local=local, cwd=cwd)
78
+ settings = read_settings(path)
79
+ settings.pop("api_url", None)
80
+ settings.pop("api_key", None)
81
+ write_settings(path, settings)
82
+ return path
83
+
84
+
85
+ def set_config_value(key: str, value: str, *, local: bool = False, cwd: Path | None = None) -> Path:
86
+ if key not in SUPPORTED_KEYS:
87
+ raise CliError("invalid_config_key", f"Unsupported config key: {key}", {"supported": sorted(SUPPORTED_KEYS)}, EXIT_CONFIG)
88
+ path = auth_settings_path(local=local, cwd=cwd)
89
+ settings = read_settings(path)
90
+ settings[key] = value.rstrip("/") if key == "api_url" else value
91
+ write_settings(path, settings)
92
+ return path
93
+
94
+
95
+ def get_config_value(key: str, *, cwd: Path | None = None, api_url: str | None = None, api_key: str | None = None) -> tuple[str | None, str | None]:
96
+ if key not in SUPPORTED_KEYS:
97
+ raise CliError("invalid_config_key", f"Unsupported config key: {key}", {"supported": sorted(SUPPORTED_KEYS)}, EXIT_CONFIG)
98
+ config = resolve_config(cwd=cwd, api_url=api_url, api_key=api_key)
99
+ return getattr(config, key), config.sources.get(key)
100
+
101
+
102
+ def read_settings(path: Path | None) -> dict[str, Any]:
103
+ if path is None or not path.exists():
104
+ return {}
105
+ try:
106
+ with path.open("r", encoding="utf-8") as handle:
107
+ payload = json.load(handle)
108
+ except json.JSONDecodeError as exc:
109
+ raise CliError("invalid_config", f"Invalid JSON in settings file: {path}", {"path": str(path)}, EXIT_CONFIG) from exc
110
+ if not isinstance(payload, dict):
111
+ raise CliError("invalid_config", f"Settings file must contain a JSON object: {path}", {"path": str(path)}, EXIT_CONFIG)
112
+ return payload
113
+
114
+
115
+ def write_settings(path: Path, settings: dict[str, Any]) -> None:
116
+ path.parent.mkdir(parents=True, exist_ok=True)
117
+ with path.open("w", encoding="utf-8") as handle:
118
+ json.dump(settings, handle, indent=2, sort_keys=True)
119
+ handle.write("\n")
120
+ _restrict_permissions(path)
121
+
122
+
123
+ def find_local_settings(cwd: Path) -> Path | None:
124
+ git_root = find_git_root(cwd)
125
+ if git_root is None:
126
+ candidate = cwd / LOCAL_SETTINGS_DIR / SETTINGS_FILENAME
127
+ return candidate if candidate.exists() else None
128
+
129
+ current = cwd
130
+ while True:
131
+ candidate = current / LOCAL_SETTINGS_DIR / SETTINGS_FILENAME
132
+ if candidate.exists():
133
+ return candidate
134
+ if current == git_root:
135
+ return None
136
+ current = current.parent
137
+
138
+
139
+ def find_git_root(cwd: Path) -> Path | None:
140
+ current = cwd
141
+ while True:
142
+ if (current / ".git").exists():
143
+ return current
144
+ if current == current.parent:
145
+ return None
146
+ current = current.parent
147
+
148
+
149
+ def redact_secret(value: str | None) -> str:
150
+ if not value:
151
+ return ""
152
+ if len(value) <= 8:
153
+ return "****"
154
+ return f"{value[:4]}...{value[-4:]}"
155
+
156
+
157
+ def _merge(values: dict[str, Any], sources: dict[str, str], incoming: dict[str, Any], source: str) -> None:
158
+ for key in SUPPORTED_KEYS:
159
+ value = _clean(incoming.get(key))
160
+ if value:
161
+ values[key] = value
162
+ sources[key] = source
163
+
164
+
165
+ def _clean(value: Any) -> str | None:
166
+ if value is None:
167
+ return None
168
+ text = str(value).strip()
169
+ return text or None
170
+
171
+
172
+ def _restrict_permissions(path: Path) -> None:
173
+ try:
174
+ os.chmod(path, 0o600)
175
+ except OSError:
176
+ return
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+
7
+ EXIT_USAGE = 2
8
+ EXIT_CONFIG = 3
9
+ EXIT_AUTH = 4
10
+ EXIT_PERMISSION = 5
11
+ EXIT_NOT_FOUND = 6
12
+ EXIT_CONFLICT = 7
13
+ EXIT_VERSION = 8
14
+ EXIT_NETWORK = 70
15
+ EXIT_SERVER = 75
16
+
17
+
18
+ @dataclass
19
+ class CliError(Exception):
20
+ code: str
21
+ message: str
22
+ details: dict[str, Any] = field(default_factory=dict)
23
+ exit_code: int = EXIT_USAGE
24
+
25
+ def __str__(self) -> str:
26
+ return self.message