orithos-cli 0.1.0__py3-none-any.whl
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.
- orithos_cli/__init__.py +6 -0
- orithos_cli/agent.py +78 -0
- orithos_cli/agent_wizard.py +255 -0
- orithos_cli/auth.py +21 -0
- orithos_cli/cli.py +82 -0
- orithos_cli/compliance.py +133 -0
- orithos_cli/config.py +114 -0
- orithos_cli/configure.py +103 -0
- orithos_cli/connection.py +69 -0
- orithos_cli/connection_wizard.py +129 -0
- orithos_cli/discovery.py +78 -0
- orithos_cli/graph.py +102 -0
- orithos_cli/guardrail.py +131 -0
- orithos_cli/mcp.py +170 -0
- orithos_cli/output.py +178 -0
- orithos_cli/probes.py +50 -0
- orithos_cli/remediation.py +111 -0
- orithos_cli/runtime.py +100 -0
- orithos_cli/scan.py +758 -0
- orithos_cli/skill.py +64 -0
- orithos_cli/skillscan/__init__.py +37 -0
- orithos_cli/skillscan/checks/__init__.py +28 -0
- orithos_cli/skillscan/checks/credentials.py +112 -0
- orithos_cli/skillscan/checks/iocs.py +95 -0
- orithos_cli/skillscan/checks/manifest.py +156 -0
- orithos_cli/skillscan/checks/network.py +117 -0
- orithos_cli/skillscan/checks/obfuscation.py +130 -0
- orithos_cli/skillscan/checks/permissions.py +108 -0
- orithos_cli/skillscan/checks/shell.py +140 -0
- orithos_cli/skillscan/collect.py +205 -0
- orithos_cli/skillscan/model.py +99 -0
- orithos_cli/skillscan/report.py +130 -0
- orithos_cli/template.py +60 -0
- orithos_cli/verify.py +85 -0
- orithos_cli/wizard.py +314 -0
- orithos_cli-0.1.0.dist-info/METADATA +99 -0
- orithos_cli-0.1.0.dist-info/RECORD +40 -0
- orithos_cli-0.1.0.dist-info/WHEEL +5 -0
- orithos_cli-0.1.0.dist-info/entry_points.txt +2 -0
- orithos_cli-0.1.0.dist-info/top_level.txt +1 -0
orithos_cli/__init__.py
ADDED
orithos_cli/agent.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Agent management subcommands for Orithos CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from orithos_cli.config import get_config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group(name="agent")
|
|
12
|
+
def agent_group() -> None:
|
|
13
|
+
"""Manage Orithos agents."""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@agent_group.command("new")
|
|
18
|
+
@click.option("--name", default="", help="Agent name (omit for interactive wizard)")
|
|
19
|
+
def new_agent(name: str) -> None:
|
|
20
|
+
"""Interactive agent setup — configure identity, prompt, tools, memory, guardrails."""
|
|
21
|
+
from orithos_cli.agent_wizard import interactive_agent_wizard
|
|
22
|
+
|
|
23
|
+
interactive_agent_wizard()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@agent_group.command("create")
|
|
27
|
+
@click.option("--name", default="", help="Agent name (omit for interactive wizard)")
|
|
28
|
+
def create_agent(name: str) -> None:
|
|
29
|
+
"""Create a new agent — interactive wizard by default."""
|
|
30
|
+
from orithos_cli.agent_wizard import interactive_agent_wizard
|
|
31
|
+
|
|
32
|
+
interactive_agent_wizard()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@agent_group.command("list")
|
|
36
|
+
def list_agents() -> None:
|
|
37
|
+
"""List all registered agents."""
|
|
38
|
+
cfg = get_config()
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
response = httpx.get(
|
|
42
|
+
f"{cfg.api_url}/v1/agents",
|
|
43
|
+
headers=cfg.auth_headers(),
|
|
44
|
+
timeout=cfg.timeout,
|
|
45
|
+
)
|
|
46
|
+
response.raise_for_status()
|
|
47
|
+
except httpx.HTTPStatusError as exc:
|
|
48
|
+
click.echo(f"Error: {exc.response.status_code} — {exc.response.text}", err=True)
|
|
49
|
+
raise SystemExit(1)
|
|
50
|
+
except httpx.RequestError as exc:
|
|
51
|
+
click.echo(f"Connection error: {exc}", err=True)
|
|
52
|
+
raise SystemExit(1)
|
|
53
|
+
|
|
54
|
+
agents = response.json()
|
|
55
|
+
if not agents:
|
|
56
|
+
click.echo("No agents found.")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
for agent in agents:
|
|
60
|
+
click.echo(f"[{agent['id']}] {agent['name']} — {agent['endpoint_url']}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@agent_group.command("delete")
|
|
64
|
+
@click.argument("agent-id")
|
|
65
|
+
def delete_agent(agent_id: str) -> None:
|
|
66
|
+
"""Delete an agent by ID."""
|
|
67
|
+
cfg = get_config()
|
|
68
|
+
try:
|
|
69
|
+
response = httpx.delete(
|
|
70
|
+
f"{cfg.api_url}/v1/agents/{agent_id}",
|
|
71
|
+
headers=cfg.auth_headers(),
|
|
72
|
+
timeout=cfg.timeout,
|
|
73
|
+
)
|
|
74
|
+
response.raise_for_status()
|
|
75
|
+
except httpx.HTTPStatusError as exc:
|
|
76
|
+
click.echo(f"Error: {exc.response.status_code} — {exc.response.text}", err=True)
|
|
77
|
+
raise SystemExit(1)
|
|
78
|
+
click.echo(f"Agent {agent_id[:8]} deleted.")
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Interactive agent wizard for Orithos CLI — 5 steps matching dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import httpx
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from orithos_cli.config import get_config, fmt_http_error
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _s(text: str = "", fg: str | None = None, dim: bool = False, bold: bool = False) -> str:
|
|
13
|
+
return click.style(text, fg=fg, dim=dim, bold=bold)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
PROVIDER_OPTIONS = [
|
|
17
|
+
("Pinecone", "Pinecone", "Vector database"),
|
|
18
|
+
("Weaviate", "Weaviate", "Vector database"),
|
|
19
|
+
("Chroma", "Chroma", "Vector database"),
|
|
20
|
+
("Milvus", "Milvus", "Vector database"),
|
|
21
|
+
("Qdrant", "Qdrant", "Vector database"),
|
|
22
|
+
("Pgvector", "Pgvector", "PostgreSQL vector extension"),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
RAG_TYPE_OPTIONS = [
|
|
26
|
+
("Vector Database", "Vector Database", "Embedding-based retrieval"),
|
|
27
|
+
("Document Store", "Document Store", "Raw document storage"),
|
|
28
|
+
("Knowledge Graph", "Knowledge Graph", "Graph-based retrieval"),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
GUARDRAIL_PROVIDERS = [
|
|
32
|
+
("NVIDIA NeMo Guardrails", "NeMo Guardrails", "NeMo"),
|
|
33
|
+
("Llama Guard", "Llama Guard", "Meta safety model"),
|
|
34
|
+
("Custom Classifier", "Custom Classifier", "Custom classifier endpoint"),
|
|
35
|
+
("OpenAI Moderation", "OpenAI Moderation", "OpenAI moderation API"),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
STAGE_OPTIONS = [
|
|
39
|
+
("pre", "Pre", "Before agent inference"),
|
|
40
|
+
("post", "Post", "After agent response"),
|
|
41
|
+
("both", "Both", "Pre and post"),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
TOOL_PARAM_TYPES = ["string", "number", "integer", "boolean", "object", "array"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _pick(options: list[tuple[str, str, str]], prompt: str, default: int = 1) -> str:
|
|
48
|
+
for i, (_, label, desc) in enumerate(options, 1):
|
|
49
|
+
click.echo(f" [{i}] {_s(label, bold=True)} \u2014 {_s(desc, dim=True)}")
|
|
50
|
+
choice = click.prompt(prompt, type=int, default=default)
|
|
51
|
+
if choice < 1 or choice > len(options):
|
|
52
|
+
choice = default
|
|
53
|
+
return options[choice - 1][0]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _confirm(prompt: str, default: bool = True) -> bool:
|
|
57
|
+
return click.confirm(_s(prompt, dim=True), default=default)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _risk_class(name: str, desc: str) -> str:
|
|
61
|
+
low = (name + " " + desc).lower()
|
|
62
|
+
critical_kw = ["refund", "payment", "transfer", "charge", "delete", "remove", "purge",
|
|
63
|
+
"destroy", "create_user", "add_user", "grant", "elevate", "permission"]
|
|
64
|
+
write_kw = critical_kw + ["send", "message", "email", "notify", "write", "create",
|
|
65
|
+
"update", "post", "admin", "access", "install", "deploy"]
|
|
66
|
+
if any(w in low for w in critical_kw):
|
|
67
|
+
return "write"
|
|
68
|
+
if any(w in low for w in write_kw):
|
|
69
|
+
return "write"
|
|
70
|
+
return "read"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def interactive_agent_wizard() -> None:
|
|
74
|
+
cfg = get_config()
|
|
75
|
+
|
|
76
|
+
click.echo()
|
|
77
|
+
click.echo(_s("\u2726 Orithos Agent Wizard", fg="green", bold=True))
|
|
78
|
+
click.echo(_s(" Configure your agent step by step.", dim=True))
|
|
79
|
+
click.echo()
|
|
80
|
+
|
|
81
|
+
# === Step 1: Identity ===
|
|
82
|
+
click.echo(_s("Step 1/5: Identity", fg="green"))
|
|
83
|
+
name = click.prompt(" Agent name", default="")
|
|
84
|
+
if not name:
|
|
85
|
+
click.echo(_s(" Agent name is required.", fg="red"))
|
|
86
|
+
raise SystemExit(1)
|
|
87
|
+
version = click.prompt(" Version", default="1.0")
|
|
88
|
+
description = click.prompt(" Description", default="")
|
|
89
|
+
click.echo()
|
|
90
|
+
|
|
91
|
+
# === Step 2: System Prompt ===
|
|
92
|
+
click.echo(_s("Step 2/5: System Prompt", fg="green"))
|
|
93
|
+
click.echo(_s(" Paste the system prompt. Enter '.' on a blank line to finish.", dim=True))
|
|
94
|
+
lines: list[str] = []
|
|
95
|
+
while True:
|
|
96
|
+
line = input()
|
|
97
|
+
if line == ".":
|
|
98
|
+
break
|
|
99
|
+
lines.append(line)
|
|
100
|
+
system_prompt = "\n".join(lines).strip()
|
|
101
|
+
|
|
102
|
+
if system_prompt:
|
|
103
|
+
click.echo(_s(" Analysing prompt...", dim=True), nl=False)
|
|
104
|
+
try:
|
|
105
|
+
ar = httpx.post(
|
|
106
|
+
f"{cfg.api_url}/v1/agents/analyze-prompt",
|
|
107
|
+
json={"text": system_prompt},
|
|
108
|
+
headers=cfg.auth_headers(),
|
|
109
|
+
timeout=30,
|
|
110
|
+
)
|
|
111
|
+
if ar.status_code == 200:
|
|
112
|
+
analysis = ar.json()
|
|
113
|
+
constraints = analysis.get("constraints_count", 0)
|
|
114
|
+
identifiers = analysis.get("identifiers_count", 0)
|
|
115
|
+
probes = analysis.get("summary", {}).get("estimated_total_probes", 0)
|
|
116
|
+
leakage = analysis.get("leakage_risk", "low")
|
|
117
|
+
click.echo(_s(f" \u2713", fg="green"))
|
|
118
|
+
click.echo(_s(f" \u21b3 {constraints} constraints \u00b7 {identifiers} identifiers \u00b7 ~{probes} probes \u00b7 leakage: {leakage}", dim=True))
|
|
119
|
+
else:
|
|
120
|
+
click.echo(_s(" \u2716 (analysis unavailable)", fg="yellow"))
|
|
121
|
+
except httpx.RequestError:
|
|
122
|
+
click.echo(_s(" \u2716 (analysis unavailable)", fg="yellow"))
|
|
123
|
+
click.echo()
|
|
124
|
+
|
|
125
|
+
# === Step 3: Tools ===
|
|
126
|
+
click.echo(_s("Step 3/5: Tools (optional)", fg="green"))
|
|
127
|
+
tools: list[dict] = []
|
|
128
|
+
if _confirm(" Add tools?"):
|
|
129
|
+
while True:
|
|
130
|
+
t_name = click.prompt(" Tool name", default="")
|
|
131
|
+
if not t_name:
|
|
132
|
+
break
|
|
133
|
+
t_desc = click.prompt(" Description", default="")
|
|
134
|
+
params: list[dict] = []
|
|
135
|
+
click.echo(_s(" Parameters (enter blank name to stop):", dim=True))
|
|
136
|
+
pi = 1
|
|
137
|
+
while True:
|
|
138
|
+
p_name = click.prompt(f" param {pi} name", default="")
|
|
139
|
+
if not p_name:
|
|
140
|
+
break
|
|
141
|
+
p_type = click.prompt(f" param {pi} type", type=click.Choice(TOOL_PARAM_TYPES), default="string", show_choices=False)
|
|
142
|
+
params.append({"name": p_name, "type": p_type})
|
|
143
|
+
pi += 1
|
|
144
|
+
|
|
145
|
+
access = _risk_class(t_name, t_desc)
|
|
146
|
+
tools.append({
|
|
147
|
+
"name": t_name,
|
|
148
|
+
"description": t_desc,
|
|
149
|
+
"parameters": params,
|
|
150
|
+
"access_level": access,
|
|
151
|
+
})
|
|
152
|
+
click.echo(f" Risk: {_s(access, dim=True)}")
|
|
153
|
+
if not _confirm(" Add another tool?", default=False):
|
|
154
|
+
break
|
|
155
|
+
click.echo()
|
|
156
|
+
|
|
157
|
+
# === Step 4: Memory & Guardrails ===
|
|
158
|
+
click.echo(_s("Step 4/5: Memory & Guardrails (optional)", fg="green"))
|
|
159
|
+
memory_sources: list[dict] = []
|
|
160
|
+
guardrails: list[dict] = []
|
|
161
|
+
|
|
162
|
+
if _confirm(" Add RAG source?"):
|
|
163
|
+
while True:
|
|
164
|
+
rag_name = click.prompt(" Source name", default="knowledge-base")
|
|
165
|
+
rag_provider = _pick(PROVIDER_OPTIONS, " Provider")
|
|
166
|
+
rag_type = _pick(RAG_TYPE_OPTIONS, " Type", default=1)
|
|
167
|
+
rag_docs = click.prompt(" Document count", type=int, default=0)
|
|
168
|
+
memory_sources.append({
|
|
169
|
+
"name": rag_name,
|
|
170
|
+
"type": rag_type,
|
|
171
|
+
"provider": rag_provider,
|
|
172
|
+
"doc_count": rag_docs,
|
|
173
|
+
"risk": "supply_chain",
|
|
174
|
+
})
|
|
175
|
+
if not _confirm(" Add another RAG source?", default=False):
|
|
176
|
+
break
|
|
177
|
+
|
|
178
|
+
if _confirm(" Add guardrail?"):
|
|
179
|
+
while True:
|
|
180
|
+
g_name = click.prompt(" Guardrail name", default="")
|
|
181
|
+
g_provider = _pick(GUARDRAIL_PROVIDERS, " Provider")
|
|
182
|
+
g_stage = _pick(STAGE_OPTIONS, " Stage")
|
|
183
|
+
guardrails.append({
|
|
184
|
+
"name": g_name,
|
|
185
|
+
"type": g_provider,
|
|
186
|
+
"stage": g_stage,
|
|
187
|
+
"enabled": True,
|
|
188
|
+
})
|
|
189
|
+
if not _confirm(" Add another guardrail?", default=False):
|
|
190
|
+
break
|
|
191
|
+
click.echo()
|
|
192
|
+
|
|
193
|
+
# === Step 5: Confirm & Save ===
|
|
194
|
+
click.echo(_s("Step 5/5: Confirm & Save", fg="green"))
|
|
195
|
+
tool_count = len(tools)
|
|
196
|
+
tool_names = ", ".join(t.get("name", "?") for t in tools[:3])
|
|
197
|
+
if tool_count > 3:
|
|
198
|
+
tool_names += f" +{tool_count - 3} more"
|
|
199
|
+
prompt_preview = system_prompt[:60].replace("\n", "\\n") + ("..." if len(system_prompt) > 60 else "")
|
|
200
|
+
prompt_tokens = max(1, len(system_prompt) // 4)
|
|
201
|
+
|
|
202
|
+
click.echo(_s("\u250c" + "\u2500" * 47 + "\u2510", dim=True))
|
|
203
|
+
for line in [
|
|
204
|
+
f" Name: {name}",
|
|
205
|
+
f" Version: {version}",
|
|
206
|
+
f" Description: {description[:40] or '(none)'}",
|
|
207
|
+
f" Prompt: {prompt_tokens} tokens",
|
|
208
|
+
f" Tools: {tool_names or 'none'}",
|
|
209
|
+
f" RAG sources: {len(memory_sources)}",
|
|
210
|
+
f" Guardrails: {len(guardrails)}",
|
|
211
|
+
]:
|
|
212
|
+
click.echo(_s(f"\u2502 {line:<45}\u2502", dim=True))
|
|
213
|
+
click.echo(_s("\u2514" + "\u2500" * 47 + "\u2518", dim=True))
|
|
214
|
+
click.echo()
|
|
215
|
+
|
|
216
|
+
if not _confirm(" Save agent?", default=True):
|
|
217
|
+
click.echo(_s(" Cancelled.", fg="yellow"))
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
payload: dict = {
|
|
221
|
+
"name": name,
|
|
222
|
+
"version": version,
|
|
223
|
+
"endpoint_url": "",
|
|
224
|
+
}
|
|
225
|
+
if description:
|
|
226
|
+
payload["description"] = description
|
|
227
|
+
if system_prompt:
|
|
228
|
+
payload["system_prompt"] = system_prompt
|
|
229
|
+
if tools:
|
|
230
|
+
payload["tools_config"] = tools
|
|
231
|
+
if memory_sources:
|
|
232
|
+
payload["memory_sources"] = memory_sources
|
|
233
|
+
if guardrails:
|
|
234
|
+
payload["guardrails"] = guardrails
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
r = httpx.post(
|
|
238
|
+
f"{cfg.api_url}/v1/agents",
|
|
239
|
+
json=payload,
|
|
240
|
+
headers=cfg.auth_headers(),
|
|
241
|
+
timeout=cfg.timeout,
|
|
242
|
+
)
|
|
243
|
+
r.raise_for_status()
|
|
244
|
+
except httpx.HTTPStatusError as exc:
|
|
245
|
+
click.echo(_s(f"Error creating agent: {fmt_http_error(exc)}", fg="red"), err=True)
|
|
246
|
+
raise SystemExit(1)
|
|
247
|
+
except httpx.RequestError as exc:
|
|
248
|
+
click.echo(_s(f"Connection error: {exc}", fg="red"), err=True)
|
|
249
|
+
raise SystemExit(1)
|
|
250
|
+
|
|
251
|
+
data = r.json()
|
|
252
|
+
aid = data.get("id", "?")[:8]
|
|
253
|
+
click.echo(_s(f"\u2726 Agent created: {aid}", fg="green", bold=True))
|
|
254
|
+
click.echo(_s(f" Name: {data.get('name', '?')}", dim=True))
|
|
255
|
+
click.echo(_s(f" Tools: {len(tools)} RAG: {len(memory_sources)} Guardrails: {len(guardrails)}", dim=True))
|
orithos_cli/auth.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Authentication and request signing utilities for Orithos CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import hmac
|
|
5
|
+
import hashlib
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def sign_request(method: str, path: str, body: bytes = b"") -> dict:
|
|
10
|
+
"""Add HMAC-SHA256 signature headers to a request."""
|
|
11
|
+
key = os.environ.get("TRACESHIELD_REQUEST_SIGNING_KEY", "")
|
|
12
|
+
if not key:
|
|
13
|
+
return {} # No signing in dev mode
|
|
14
|
+
|
|
15
|
+
timestamp = int(time.time())
|
|
16
|
+
payload = f"{timestamp}.{method}.{path}.{hashlib.sha256(body).hexdigest()}".encode()
|
|
17
|
+
signature = hmac.new(key.encode(), payload, hashlib.sha256).hexdigest()
|
|
18
|
+
return {
|
|
19
|
+
"X-Orithos-Timestamp": str(timestamp),
|
|
20
|
+
"X-Orithos-Signature": f"t={timestamp},v1={signature}",
|
|
21
|
+
}
|
orithos_cli/cli.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Orithos CLI — main entry point."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from orithos_cli.configure import configure
|
|
6
|
+
from orithos_cli.agent import agent_group
|
|
7
|
+
from orithos_cli.compliance import compliance_group
|
|
8
|
+
from orithos_cli.connection import connection_group
|
|
9
|
+
from orithos_cli.discovery import discovery_group
|
|
10
|
+
from orithos_cli.graph import graph_group
|
|
11
|
+
from orithos_cli.guardrail import guardrail_group
|
|
12
|
+
from orithos_cli.mcp import mcp_group
|
|
13
|
+
from orithos_cli.probes import probes_group
|
|
14
|
+
from orithos_cli.remediation import remediation_group
|
|
15
|
+
from orithos_cli.runtime import alerting_group, runtime_group
|
|
16
|
+
from orithos_cli.scan import scan_group
|
|
17
|
+
from orithos_cli.skill import skill_group
|
|
18
|
+
from orithos_cli.template import template_group
|
|
19
|
+
from orithos_cli.verify import verify_package_file
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.group()
|
|
23
|
+
@click.version_option(version="0.1.0", prog_name="orithos")
|
|
24
|
+
def main() -> None:
|
|
25
|
+
"""Orithos CLI — AI Agent Security Testing Platform.
|
|
26
|
+
|
|
27
|
+
Quick start: orithos configure
|
|
28
|
+
|
|
29
|
+
Environment variables (override config file):
|
|
30
|
+
ORITHOS_API_KEY API key (tsk_...)
|
|
31
|
+
ORITHOS_API_URL API base URL (overrides default https://api.orithos.com)
|
|
32
|
+
ORITHOS_ORG_ID Organisation ID (optional override)
|
|
33
|
+
ORITHOS_TIMEOUT Request timeout in seconds (default: 30)
|
|
34
|
+
"""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@main.command("verify")
|
|
39
|
+
@click.argument("package_file", type=click.Path(exists=True, dir_okay=False))
|
|
40
|
+
def verify_cmd(package_file: str) -> None:
|
|
41
|
+
"""Verify an evidence package offline (hashes + Merkle root).
|
|
42
|
+
|
|
43
|
+
Recomputes every artifact hash and the Merkle root from the package
|
|
44
|
+
content and compares against the declared manifest. No network access,
|
|
45
|
+
no Orithos contact — independent verifiability for auditors.
|
|
46
|
+
"""
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
|
|
49
|
+
result = verify_package_file(Path(package_file))
|
|
50
|
+
if result["ok"]:
|
|
51
|
+
click.echo("OK — package integrity verified")
|
|
52
|
+
else:
|
|
53
|
+
click.echo("FAIL — package integrity check failed")
|
|
54
|
+
for err in result["errors"]:
|
|
55
|
+
click.echo(f" - {err}")
|
|
56
|
+
for art in result.get("artifacts", []):
|
|
57
|
+
click.echo(f" {art['name']}: {art['sha256'][:16]}…")
|
|
58
|
+
if result.get("merkle_root"):
|
|
59
|
+
click.echo(f" merkle root: {result['merkle_root'][:16]}…")
|
|
60
|
+
if not result["ok"]:
|
|
61
|
+
raise SystemExit(1)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
main.add_command(configure)
|
|
65
|
+
main.add_command(agent_group)
|
|
66
|
+
main.add_command(scan_group)
|
|
67
|
+
main.add_command(remediation_group)
|
|
68
|
+
main.add_command(guardrail_group)
|
|
69
|
+
main.add_command(graph_group)
|
|
70
|
+
main.add_command(runtime_group)
|
|
71
|
+
main.add_command(alerting_group)
|
|
72
|
+
main.add_command(discovery_group)
|
|
73
|
+
main.add_command(connection_group)
|
|
74
|
+
main.add_command(template_group)
|
|
75
|
+
main.add_command(probes_group)
|
|
76
|
+
main.add_command(compliance_group)
|
|
77
|
+
main.add_command(mcp_group)
|
|
78
|
+
main.add_command(skill_group)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
main()
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Compliance mapping and reporting commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from orithos_cli.config import get_config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group(name="compliance")
|
|
14
|
+
def compliance_group() -> None:
|
|
15
|
+
"""View compliance mappings and generate compliance reports."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@compliance_group.command("mappings")
|
|
20
|
+
@click.option(
|
|
21
|
+
"--framework", help="Filter by framework (OWASP_LLM, NIST_AI_RMF, MITRE_ATLAS, CWE)"
|
|
22
|
+
)
|
|
23
|
+
@click.option(
|
|
24
|
+
"--format", "output_format", type=click.Choice(["table", "json"]), default="table"
|
|
25
|
+
)
|
|
26
|
+
def list_mappings(framework: str | None, output_format: str) -> None:
|
|
27
|
+
"""List all compliance mappings for the organisation."""
|
|
28
|
+
cfg = get_config()
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
response = httpx.get(
|
|
32
|
+
f"{cfg.api_url}/v1/compliance/mappings",
|
|
33
|
+
headers=cfg.auth_headers(),
|
|
34
|
+
timeout=cfg.timeout,
|
|
35
|
+
)
|
|
36
|
+
response.raise_for_status()
|
|
37
|
+
except httpx.HTTPStatusError as exc:
|
|
38
|
+
click.echo(f"Error: {exc.response.status_code} — {exc.response.text}", err=True)
|
|
39
|
+
raise SystemExit(1)
|
|
40
|
+
except httpx.RequestError as exc:
|
|
41
|
+
click.echo(f"Connection error: {exc}", err=True)
|
|
42
|
+
raise SystemExit(1)
|
|
43
|
+
|
|
44
|
+
data = response.json()
|
|
45
|
+
# GET /v1/compliance/mappings returns {"mappings": [...], "total": N}.
|
|
46
|
+
mappings = (data.get("mappings") or []) if isinstance(data, dict) else data
|
|
47
|
+
|
|
48
|
+
if framework:
|
|
49
|
+
mappings = [m for m in mappings if m.get("framework") == framework]
|
|
50
|
+
|
|
51
|
+
if output_format == "json":
|
|
52
|
+
click.echo(json.dumps(mappings, indent=2))
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
frameworks = sorted(set(m["framework"] for m in mappings))
|
|
56
|
+
click.echo(
|
|
57
|
+
f"Compliance Mappings ({len(mappings)} total across {len(frameworks)} frameworks)"
|
|
58
|
+
)
|
|
59
|
+
click.echo(f"{'=' * 70}")
|
|
60
|
+
|
|
61
|
+
for fw in frameworks:
|
|
62
|
+
fw_mappings = [m for m in mappings if m["framework"] == fw]
|
|
63
|
+
click.echo(f"\n{fw} ({len(fw_mappings)} controls)")
|
|
64
|
+
click.echo("-" * 50)
|
|
65
|
+
for m in fw_mappings:
|
|
66
|
+
click.echo(f" [{m['control_id']}] {m['control_name']}")
|
|
67
|
+
click.echo(f" Severity mapping: {m['severity_mapping']}")
|
|
68
|
+
click.echo(f" {m['description'][:80]}...")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@compliance_group.command("report")
|
|
72
|
+
@click.argument("scan_id")
|
|
73
|
+
@click.option(
|
|
74
|
+
"--framework", help="Filter by framework (OWASP_LLM, NIST_AI_RMF, MITRE_ATLAS, CWE)"
|
|
75
|
+
)
|
|
76
|
+
@click.option(
|
|
77
|
+
"--format",
|
|
78
|
+
"output_format",
|
|
79
|
+
type=click.Choice(["summary", "json"]),
|
|
80
|
+
default="summary",
|
|
81
|
+
)
|
|
82
|
+
def compliance_report(scan_id: str, framework: str | None, output_format: str) -> None:
|
|
83
|
+
"""Generate a compliance report for a completed scan."""
|
|
84
|
+
cfg = get_config()
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
response = httpx.get(
|
|
88
|
+
f"{cfg.api_url}/v1/compliance/report/{scan_id}",
|
|
89
|
+
headers=cfg.auth_headers(),
|
|
90
|
+
timeout=cfg.timeout,
|
|
91
|
+
)
|
|
92
|
+
response.raise_for_status()
|
|
93
|
+
except httpx.HTTPStatusError as exc:
|
|
94
|
+
click.echo(f"Error: {exc.response.status_code} — {exc.response.text}", err=True)
|
|
95
|
+
raise SystemExit(1)
|
|
96
|
+
except httpx.RequestError as exc:
|
|
97
|
+
click.echo(f"Connection error: {exc}", err=True)
|
|
98
|
+
raise SystemExit(1)
|
|
99
|
+
|
|
100
|
+
report = response.json()
|
|
101
|
+
|
|
102
|
+
if output_format == "json":
|
|
103
|
+
click.echo(json.dumps(report, indent=2))
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
summary = report.get("summary", {})
|
|
107
|
+
click.echo(f"Orithos Compliance Report — Scan {scan_id}")
|
|
108
|
+
click.echo(f"{'=' * 60}")
|
|
109
|
+
click.echo(f"Total findings: {summary.get('total_findings', 0)}")
|
|
110
|
+
click.echo(f"High-risk findings: {summary.get('high_risk_findings', 0)}")
|
|
111
|
+
click.echo(
|
|
112
|
+
f"Frameworks with findings: {', '.join(summary.get('frameworks_with_findings', [])) or 'None'}"
|
|
113
|
+
)
|
|
114
|
+
click.echo("")
|
|
115
|
+
|
|
116
|
+
by_framework = report.get("by_framework", {})
|
|
117
|
+
if framework:
|
|
118
|
+
frameworks_to_show = [framework] if framework in by_framework else []
|
|
119
|
+
else:
|
|
120
|
+
frameworks_to_show = sorted(by_framework.keys())
|
|
121
|
+
|
|
122
|
+
for fw in frameworks_to_show:
|
|
123
|
+
controls = by_framework.get(fw, [])
|
|
124
|
+
if not controls:
|
|
125
|
+
continue
|
|
126
|
+
click.echo(f"\n{fw} ({len(controls)} findings)")
|
|
127
|
+
click.echo("-" * 50)
|
|
128
|
+
for c in controls:
|
|
129
|
+
# get_compliance_report returns control_id/control_name/our_severity
|
|
130
|
+
# (no remediation_guidance — removed with the regex-mapping rewrite).
|
|
131
|
+
click.echo(
|
|
132
|
+
f" [{c['control_id']}] {c['control_name']} — severity: {c.get('our_severity', 'N/A')}"
|
|
133
|
+
)
|
orithos_cli/config.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Configuration management for Orithos CLI.
|
|
2
|
+
|
|
3
|
+
Read order: CLI flag > env var > config file > default.
|
|
4
|
+
Config file: ~/.orithos/config.json
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
CONFIG_DIR = Path.home() / ".orithos"
|
|
16
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
17
|
+
DEFAULT_API_URL = "https://api.orithos.com"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class CLIConfig:
|
|
22
|
+
api_url: str
|
|
23
|
+
org_id: str
|
|
24
|
+
timeout: float
|
|
25
|
+
api_key: str
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_env(cls) -> "CLIConfig":
|
|
29
|
+
file_cfg = _read_config_file()
|
|
30
|
+
return cls(
|
|
31
|
+
api_url=(
|
|
32
|
+
os.environ.get("ORITHOS_API_URL")
|
|
33
|
+
or os.environ.get("TRACESHIELD_API_URL")
|
|
34
|
+
or file_cfg.get("api_url", "")
|
|
35
|
+
or DEFAULT_API_URL
|
|
36
|
+
),
|
|
37
|
+
org_id=(
|
|
38
|
+
os.environ.get("ORITHOS_ORG_ID")
|
|
39
|
+
or os.environ.get("TRACESHIELD_ORG_ID")
|
|
40
|
+
or file_cfg.get("org_id", "org_demo")
|
|
41
|
+
),
|
|
42
|
+
timeout=float(
|
|
43
|
+
os.environ.get("ORITHOS_TIMEOUT")
|
|
44
|
+
or os.environ.get("TRACESHIELD_TIMEOUT")
|
|
45
|
+
or file_cfg.get("timeout", "30")
|
|
46
|
+
),
|
|
47
|
+
api_key=(
|
|
48
|
+
os.environ.get("ORITHOS_API_KEY")
|
|
49
|
+
or os.environ.get("TRACESHIELD_INTERNAL_API_KEY")
|
|
50
|
+
or file_cfg.get("api_key", "")
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def auth_headers(self) -> dict[str, str]:
|
|
55
|
+
headers = {"x-org-id": self.org_id}
|
|
56
|
+
if self.api_key:
|
|
57
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
58
|
+
return headers
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> dict[str, str]:
|
|
61
|
+
return {
|
|
62
|
+
"api_url": self.api_url,
|
|
63
|
+
"org_id": self.org_id,
|
|
64
|
+
"timeout": str(self.timeout),
|
|
65
|
+
"api_key": self.api_key,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_dict(cls, d: dict[str, str]) -> "CLIConfig":
|
|
70
|
+
return cls(
|
|
71
|
+
api_url=d.get("api_url", DEFAULT_API_URL),
|
|
72
|
+
org_id=d.get("org_id", "org_demo"),
|
|
73
|
+
timeout=float(d.get("timeout", "30")),
|
|
74
|
+
api_key=d.get("api_key", ""),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
CONFIG: CLIConfig | None = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_config() -> CLIConfig:
|
|
82
|
+
global CONFIG
|
|
83
|
+
if CONFIG is None:
|
|
84
|
+
CONFIG = CLIConfig.from_env()
|
|
85
|
+
return CONFIG
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _read_config_file() -> dict[str, str]:
|
|
89
|
+
try:
|
|
90
|
+
if CONFIG_FILE.exists():
|
|
91
|
+
with open(CONFIG_FILE) as f:
|
|
92
|
+
return json.load(f)
|
|
93
|
+
except (json.JSONDecodeError, OSError):
|
|
94
|
+
pass
|
|
95
|
+
return {}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def write_config(cfg: CLIConfig) -> Path:
|
|
99
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
with open(CONFIG_FILE, "w") as f:
|
|
101
|
+
json.dump(cfg.to_dict(), f, indent=2)
|
|
102
|
+
return CONFIG_FILE
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def fmt_http_error(exc: httpx.HTTPStatusError) -> str:
|
|
106
|
+
"""Extract a clean detail message from an HTTP error response."""
|
|
107
|
+
try:
|
|
108
|
+
body = exc.response.json()
|
|
109
|
+
msg = body.get("detail") or body.get("message") or body.get("error") or ""
|
|
110
|
+
if msg:
|
|
111
|
+
return f"{exc.response.status_code} — {msg}"
|
|
112
|
+
except (json.JSONDecodeError, AttributeError):
|
|
113
|
+
pass
|
|
114
|
+
return f"{exc.response.status_code} — {exc.response.text[:200]}"
|