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.
Files changed (40) hide show
  1. orithos_cli/__init__.py +6 -0
  2. orithos_cli/agent.py +78 -0
  3. orithos_cli/agent_wizard.py +255 -0
  4. orithos_cli/auth.py +21 -0
  5. orithos_cli/cli.py +82 -0
  6. orithos_cli/compliance.py +133 -0
  7. orithos_cli/config.py +114 -0
  8. orithos_cli/configure.py +103 -0
  9. orithos_cli/connection.py +69 -0
  10. orithos_cli/connection_wizard.py +129 -0
  11. orithos_cli/discovery.py +78 -0
  12. orithos_cli/graph.py +102 -0
  13. orithos_cli/guardrail.py +131 -0
  14. orithos_cli/mcp.py +170 -0
  15. orithos_cli/output.py +178 -0
  16. orithos_cli/probes.py +50 -0
  17. orithos_cli/remediation.py +111 -0
  18. orithos_cli/runtime.py +100 -0
  19. orithos_cli/scan.py +758 -0
  20. orithos_cli/skill.py +64 -0
  21. orithos_cli/skillscan/__init__.py +37 -0
  22. orithos_cli/skillscan/checks/__init__.py +28 -0
  23. orithos_cli/skillscan/checks/credentials.py +112 -0
  24. orithos_cli/skillscan/checks/iocs.py +95 -0
  25. orithos_cli/skillscan/checks/manifest.py +156 -0
  26. orithos_cli/skillscan/checks/network.py +117 -0
  27. orithos_cli/skillscan/checks/obfuscation.py +130 -0
  28. orithos_cli/skillscan/checks/permissions.py +108 -0
  29. orithos_cli/skillscan/checks/shell.py +140 -0
  30. orithos_cli/skillscan/collect.py +205 -0
  31. orithos_cli/skillscan/model.py +99 -0
  32. orithos_cli/skillscan/report.py +130 -0
  33. orithos_cli/template.py +60 -0
  34. orithos_cli/verify.py +85 -0
  35. orithos_cli/wizard.py +314 -0
  36. orithos_cli-0.1.0.dist-info/METADATA +99 -0
  37. orithos_cli-0.1.0.dist-info/RECORD +40 -0
  38. orithos_cli-0.1.0.dist-info/WHEEL +5 -0
  39. orithos_cli-0.1.0.dist-info/entry_points.txt +2 -0
  40. orithos_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,130 @@
1
+ """Report renderers: text, JSON, SARIF."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from orithos_cli.skillscan.model import SEVERITY_ORDER, ScanResult
9
+
10
+ CHECK_TITLES = {
11
+ "manifest": "Manifest hygiene",
12
+ "permissions": "Permission scope",
13
+ "network": "Outbound network",
14
+ "shell": "Shell execution",
15
+ "credentials": "Credential access",
16
+ "obfuscation": "Obfuscation",
17
+ "iocs": "Known-bad indicators",
18
+ }
19
+
20
+ _SARIF_LEVEL = {
21
+ "critical": "error",
22
+ "high": "error",
23
+ "medium": "warning",
24
+ "low": "note",
25
+ "info": "note",
26
+ }
27
+
28
+
29
+ def render_text(result: ScanResult) -> str:
30
+ lines: list[str] = []
31
+ counts = result.counts()
32
+ total = len(result.findings)
33
+ lines.append("Orithos skill scan")
34
+ lines.append("=" * 60)
35
+ lines.append(f"Artifact: {result.source}")
36
+ lines.append(
37
+ f"Files: {result.files_scanned} scanned"
38
+ + (f", {len(result.skipped)} skipped" if result.skipped else "")
39
+ )
40
+ summary = " · ".join(
41
+ f"{sev} {counts[sev]}"
42
+ for sev in sorted(counts, key=lambda s: SEVERITY_ORDER.get(s, 99))
43
+ )
44
+ lines.append(f"Findings: {total}" + (f" ({summary})" if summary else ""))
45
+ if result.errors:
46
+ lines.append(f"Errors: {len(result.errors)} (scan continued)")
47
+ for e in result.errors[:5]:
48
+ lines.append(f" ! {e}")
49
+ lines.append("")
50
+
51
+ if not total:
52
+ lines.append("No findings — artifact looks clean under the v1 rule set.")
53
+ return "\n".join(lines)
54
+
55
+ for f in result.sorted_findings():
56
+ loc = f"{f.file}:{f.line}" if f.line else f.file
57
+ lines.append(f"[{f.severity.upper():8}] {f.check:12} {f.title}")
58
+ lines.append(f" at {loc}")
59
+ if f.evidence:
60
+ lines.append(f" > {f.evidence}")
61
+ lines.append(f" {f.detail}")
62
+ lines.append("")
63
+ return "\n".join(lines).rstrip() + "\n"
64
+
65
+
66
+ def render_json(result: ScanResult) -> str:
67
+ counts = result.counts()
68
+ payload: dict[str, Any] = {
69
+ "tool": "orithos-skill-scan",
70
+ "version": "0.1.0",
71
+ "artifact": result.source,
72
+ "files_scanned": result.files_scanned,
73
+ "summary": {
74
+ "total": len(result.findings),
75
+ "by_severity": counts,
76
+ },
77
+ "findings": [f.to_dict() for f in result.sorted_findings()],
78
+ }
79
+ if result.skipped:
80
+ payload["skipped"] = result.skipped
81
+ if result.errors:
82
+ payload["errors"] = result.errors
83
+ return json.dumps(payload, indent=2)
84
+
85
+
86
+ def render_sarif(result: ScanResult) -> str:
87
+ rules = [
88
+ {
89
+ "id": f"ORITHOS-SKILL-{check.upper()}",
90
+ "name": check,
91
+ "shortDescription": {"text": title},
92
+ }
93
+ for check, title in CHECK_TITLES.items()
94
+ ]
95
+ results: list[dict[str, Any]] = []
96
+ for f in result.sorted_findings():
97
+ entry: dict[str, Any] = {
98
+ "ruleId": f"ORITHOS-SKILL-{f.check.upper()}",
99
+ "level": _SARIF_LEVEL.get(f.severity, "note"),
100
+ "message": {"text": f"{f.title}: {f.detail}"},
101
+ "locations": [
102
+ {
103
+ "physicalLocation": {
104
+ "artifactLocation": {"uri": f.file},
105
+ "region": {"startLine": f.line or 1, "startColumn": 1},
106
+ }
107
+ }
108
+ ],
109
+ "properties": {"severity": f.severity, "evidence": f.evidence},
110
+ }
111
+ results.append(entry)
112
+
113
+ sarif = {
114
+ "version": "2.1.0",
115
+ "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
116
+ "runs": [
117
+ {
118
+ "tool": {
119
+ "driver": {
120
+ "name": "orithos-skill-scan",
121
+ "version": "0.1.0",
122
+ "informationUri": "https://orithos.com",
123
+ "rules": rules,
124
+ }
125
+ },
126
+ "results": results,
127
+ }
128
+ ],
129
+ }
130
+ return json.dumps(sarif, indent=2)
@@ -0,0 +1,60 @@
1
+ """Scan template subcommands for Orithos CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+ import httpx
7
+
8
+ from orithos_cli.config import get_config
9
+
10
+
11
+ @click.group(name="template")
12
+ def template_group() -> None:
13
+ """Manage scan templates."""
14
+ pass
15
+
16
+
17
+ @template_group.command("create")
18
+ @click.option("--name", required=True, help="Template name")
19
+ @click.option("--agent-id", required=True, help="Agent ID")
20
+ @click.option("--connection-id", default=None, help="Connection ID")
21
+ @click.option("--depth", default="standard", type=click.Choice(["quick", "standard", "deep", "exhaustive"]), help="Scan depth")
22
+ @click.option("--attack-suite", default="standard", type=click.Choice(["standard", "policy", "both"]), help="Attack suite")
23
+ @click.option("--remediation-mode", default="manual_only", type=click.Choice(["advisory", "auto_patch_low_medium", "manual_only"]), help="Remediation mode")
24
+ def create_template(name: str, agent_id: str, connection_id: str | None, depth: str, attack_suite: str, remediation_mode: str) -> None:
25
+ """Create a new scan template."""
26
+ cfg = get_config()
27
+ payload = {"name": name, "agent_id": agent_id, "depth": depth, "attack_suite": attack_suite, "remediation_mode": remediation_mode}
28
+ if connection_id:
29
+ payload["connection_id"] = connection_id
30
+ try:
31
+ resp = httpx.post(f"{cfg.api_url}/v1/templates", json=payload, headers=cfg.auth_headers(), timeout=cfg.timeout)
32
+ resp.raise_for_status()
33
+ except httpx.HTTPStatusError as exc:
34
+ click.echo(f"Error: {exc.response.status_code} — {exc.response.text}", err=True)
35
+ raise SystemExit(1)
36
+ data = resp.json()
37
+ click.echo(f"Template created: {data['id'][:8]}")
38
+ click.echo(f" Name: {data.get('name', '?')}")
39
+ click.echo(f" Agent: {data.get('agent_id', '?')[:8]}")
40
+ click.echo(f" Depth: {data.get('depth', '?')}")
41
+
42
+
43
+ @template_group.command("list")
44
+ def list_templates() -> None:
45
+ """List all scan templates."""
46
+ cfg = get_config()
47
+ try:
48
+ resp = httpx.get(f"{cfg.api_url}/v1/templates", headers=cfg.auth_headers(), timeout=cfg.timeout)
49
+ resp.raise_for_status()
50
+ except httpx.HTTPStatusError as exc:
51
+ click.echo(f"Error: {exc.response.status_code} — {exc.response.text}", err=True)
52
+ raise SystemExit(1)
53
+ data = resp.json()
54
+ items = data if isinstance(data, list) else data.get("templates", [])
55
+ if not items:
56
+ click.echo("No templates found.")
57
+ return
58
+ for t in items:
59
+ click.echo(f"[{t['id'][:8]}] {t.get('name', '?')} — depth={t.get('depth', '?')} suite={t.get('attack_suite', '?')}")
60
+ click.echo(f" Agent: {t.get('agent_id', '?')[:8]} Remediation: {t.get('remediation_mode', 'manual_only')}")
orithos_cli/verify.py ADDED
@@ -0,0 +1,85 @@
1
+ """Offline evidence-package verification (`orithos verify <package.json>`).
2
+
3
+ Recomputes per-artifact SHA-256 and the Merkle root from the package's own
4
+ artifact content and compares against the manifest. Independent
5
+ verifiability — no Orithos contact required, which is what makes the
6
+ package acceptable as auditor-obtained evidence (AS 1105 preference for
7
+ directly obtained evidence; FRE 902(14) hash self-authentication basis).
8
+ """
9
+
10
+ import hashlib
11
+ import json
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+
16
+ def sha256_bytes(data: bytes) -> str:
17
+ return hashlib.sha256(data).hexdigest()
18
+
19
+
20
+ def merkle_root(leaf_hashes: list[str]) -> str:
21
+ if not leaf_hashes:
22
+ return sha256_bytes(b"")
23
+ level = sorted(leaf_hashes)
24
+ while len(level) > 1:
25
+ nxt: list[str] = []
26
+ for i in range(0, len(level), 2):
27
+ left = level[i]
28
+ right = level[i + 1] if i + 1 < len(level) else left
29
+ nxt.append(sha256_bytes((left + right).encode()))
30
+ level = nxt
31
+ return level[0]
32
+
33
+
34
+ def verify_package(pkg: dict[str, Any]) -> dict[str, Any]:
35
+ """Return {ok, errors, artifacts} for a parsed package dict."""
36
+ errors: list[str] = []
37
+ manifest = pkg.get("manifest", pkg)
38
+ artifacts = pkg.get("artifacts", {})
39
+ if not artifacts:
40
+ errors.append("no artifacts section in package")
41
+ return {"ok": False, "errors": errors, "artifacts": []}
42
+
43
+ computed: dict[str, str] = {}
44
+ for name, art in artifacts.items():
45
+ content = art.get("content") if isinstance(art, dict) else art
46
+ if content is None:
47
+ errors.append(f"artifact {name}: no content to hash")
48
+ continue
49
+ computed[name] = sha256_bytes(
50
+ json.dumps(content, sort_keys=True, default=str).encode()
51
+ )
52
+ declared = art.get("sha256") if isinstance(art, dict) else None
53
+ if declared and declared != computed[name]:
54
+ errors.append(
55
+ f"artifact {name}: hash mismatch (declared {declared}, computed {computed[name]})"
56
+ )
57
+
58
+ expected_root = manifest.get("integrity", {}).get("merkle_root")
59
+ if expected_root:
60
+ actual_root = merkle_root(list(computed.values()))
61
+ if actual_root != expected_root:
62
+ errors.append(
63
+ f"merkle root mismatch (declared {expected_root}, computed {actual_root})"
64
+ )
65
+ elif "merkle_root" in manifest:
66
+ errors.append("merkle_root declared but integrity section missing")
67
+ else:
68
+ errors.append("no merkle root declared in manifest")
69
+
70
+ return {
71
+ "ok": not errors,
72
+ "errors": errors,
73
+ "artifacts": [{"name": n, "sha256": h} for n, h in sorted(computed.items())],
74
+ "merkle_root": merkle_root(list(computed.values())),
75
+ }
76
+
77
+
78
+ def verify_package_file(path: Path) -> dict[str, Any]:
79
+ data = json.loads(path.read_text())
80
+ if "manifest" in data and "artifacts" in data:
81
+ return verify_package(data)
82
+ # single-document form: {"package": {...}} or manifest-with-embedded-artifacts
83
+ if "package" in data:
84
+ return verify_package(data["package"])
85
+ return verify_package(data)
orithos_cli/wizard.py ADDED
@@ -0,0 +1,314 @@
1
+ """Interactive scan wizard for Orithos CLI — 4 steps matching dashboard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+ import click
7
+
8
+ from orithos_cli.config import get_config, fmt_http_error
9
+ from orithos_cli.scan import _run_scan
10
+
11
+
12
+ DEPTH_OPTIONS = [
13
+ ("quick", "Quick", "Light scan, fast results"),
14
+ ("standard", "Standard", "Balanced coverage (default)"),
15
+ ("deep", "Deep", "Thorough scan, longer runtime"),
16
+ ("exhaustive", "Exhaustive", "Maximum coverage"),
17
+ ]
18
+
19
+ REMEDIATION_OPTIONS = [
20
+ ("advisory", "Advisory", "Recommendations only"),
21
+ ("auto_patch_low_medium", "Auto-patch", "Automatic fixes for low/medium findings"),
22
+ ("manual_only", "Manual only", "No auto-remediation"),
23
+ ]
24
+
25
+ SCHEDULE_OPTIONS = [
26
+ ("now", "Now", "Run immediately"),
27
+ ("daily", "Daily", "Daily at 06:00 UTC"),
28
+ ("weekly", "Weekly", "Weekly on Monday"),
29
+ ("monthly", "Monthly", "Monthly on 1st"),
30
+ ]
31
+
32
+ DEFAULT_PROBES = [
33
+ "prompt_injection",
34
+ "tool_misuse",
35
+ "scope_escalation",
36
+ "exfil_attempt",
37
+ ]
38
+
39
+
40
+ def _s(
41
+ text: str = "", fg: str | None = None, dim: bool = False, bold: bool = False
42
+ ) -> str:
43
+ return click.style(text, fg=fg, dim=dim, bold=bold)
44
+
45
+
46
+ def _fetch_agents(cfg) -> list[dict]:
47
+ try:
48
+ r = httpx.get(
49
+ f"{cfg.api_url}/v1/agents", headers=cfg.auth_headers(), timeout=cfg.timeout
50
+ )
51
+ r.raise_for_status()
52
+ data = r.json()
53
+ return data if isinstance(data, list) else data.get("items", [])
54
+ except Exception:
55
+ return []
56
+
57
+
58
+ def _fetch_connections(cfg) -> list[dict]:
59
+ try:
60
+ r = httpx.get(
61
+ f"{cfg.api_url}/v1/connections",
62
+ headers=cfg.auth_headers(),
63
+ timeout=cfg.timeout,
64
+ )
65
+ r.raise_for_status()
66
+ data = r.json()
67
+ return data if isinstance(data, list) else data.get("items", [])
68
+ except Exception:
69
+ return []
70
+
71
+
72
+ def _pick(options: list[tuple[str, str, str]], prompt: str, default: int = 1) -> str:
73
+ for i, (_, label, desc) in enumerate(options, 1):
74
+ click.echo(f" [{i}] {_s(label, bold=True)} \u2014 {_s(desc, dim=True)}")
75
+ choice = click.prompt(prompt, type=int, default=default)
76
+ return options[choice - 1][0]
77
+
78
+
79
+ def _pick_from_list(
80
+ items: list[dict],
81
+ prompt: str,
82
+ name_key: str = "name",
83
+ detail_keys: list[str] | None = None,
84
+ ) -> dict | None:
85
+ if not items:
86
+ click.echo(_s(" No items available.", fg="yellow"))
87
+ return None
88
+ if len(items) == 1:
89
+ click.echo(f" Using: {_s(items[0].get(name_key, '?'), bold=True)}")
90
+ return items[0]
91
+ for i, item in enumerate(items, 1):
92
+ label = item.get(name_key, "?")
93
+ details = ""
94
+ if detail_keys:
95
+ parts = [str(item.get(k, "")) for k in detail_keys if item.get(k)]
96
+ if parts:
97
+ details = " \u2014 " + " \u00b7 ".join(parts)
98
+ click.echo(f" [{i}] {_s(label, bold=True)}{_s(details, dim=True)}")
99
+ choice = click.prompt(prompt, type=int, default=1)
100
+ if choice == 0:
101
+ return None
102
+ return items[choice - 1] if 0 < choice <= len(items) else None
103
+
104
+
105
+ def _confirm(prompt: str, default: bool = True) -> bool:
106
+ return click.confirm(_s(prompt, dim=True), default=default)
107
+
108
+
109
+ def interactive_wizard() -> None:
110
+ cfg = get_config()
111
+
112
+ click.echo()
113
+ click.echo(_s("\u2726 Orithos Scan Wizard", fg="green", bold=True))
114
+ click.echo(_s(" Configure your scan step by step.", dim=True))
115
+ click.echo()
116
+
117
+ # Step 1: Agent & Connection
118
+ click.echo(_s("Step 1/4: Agent & Connection", fg="green"))
119
+ click.echo(_s(" Fetching available agents...", dim=True))
120
+ agents = _fetch_agents(cfg)
121
+ if not agents:
122
+ click.echo(
123
+ _s(" No agents found. Create one in the dashboard first.", fg="red")
124
+ )
125
+ raise SystemExit(1)
126
+ agent = _pick_from_list(agents, "Select agent")
127
+ if not agent:
128
+ raise SystemExit(1)
129
+ agent_id = agent["id"]
130
+
131
+ click.echo(_s(" Fetching available connections...", dim=True))
132
+ connections = _fetch_connections(cfg)
133
+ connection_id = None
134
+
135
+ # Check for preferred connection on agent
136
+ preferred_id = agent.get("preferred_connection_id")
137
+ preferred_conn = (
138
+ next((c for c in connections if c.get("id") == preferred_id), None)
139
+ if preferred_id and connections
140
+ else None
141
+ )
142
+
143
+ skip_conn_picker = False
144
+ if preferred_conn:
145
+ conn_name = preferred_conn.get("name", "?")
146
+ click.echo(
147
+ f" Using preferred connection: {_s(conn_name, bold=True)} (auto-selected from agent)"
148
+ )
149
+ if not click.confirm(
150
+ _s(" Choose a different connection?", dim=True), default=False
151
+ ):
152
+ connection_id = preferred_conn["id"]
153
+ agent_name = agent.get("name", agent_id[:8])
154
+ click.echo(
155
+ f" Pairing: {_s(agent_name, bold=True)} → {_s(preferred_conn.get('base_url', '?'), dim=True)}"
156
+ )
157
+ skip_conn_picker = True
158
+
159
+ if not skip_conn_picker and connections:
160
+ conn = _pick_from_list(
161
+ connections,
162
+ "Select connection (0 to skip)",
163
+ detail_keys=["base_url", "health_status", "model_name"],
164
+ )
165
+ if conn:
166
+ connection_id = conn["id"]
167
+ agent_name = agent.get("name", agent_id[:8])
168
+ click.echo(
169
+ f" Pairing: {_s(agent_name, bold=True)} → {_s(conn.get('base_url', '?'), dim=True)}"
170
+ )
171
+ click.echo()
172
+
173
+ # Step 2: Attack Suite
174
+ click.echo(_s("Step 2/4: Attack Suite", fg="green"))
175
+ click.echo(_s(" Select scan mode:", dim=True))
176
+ MODE_OPTIONS = [
177
+ ("agent", "Agent-Targeted", "Attacks derived from this agent's exact config"),
178
+ ("standard", "Standard Library", "Standard attack vectors"),
179
+ ("full", "Full Coverage", "Both agent-targeted and standard probes"),
180
+ ]
181
+ scan_mode = _pick(MODE_OPTIONS, "Select mode")
182
+ click.echo()
183
+
184
+ # Step 3: Configuration
185
+ click.echo(_s("Step 3/4: Configuration", fg="green"))
186
+
187
+ concurrency = click.prompt(" Concurrency (probes/batch)", type=int, default=50)
188
+ timeout_ms = click.prompt(" Timeout (ms)", type=int, default=30000)
189
+ session_isolation = _confirm(" Session isolation?")
190
+ test_guardrails = _confirm(" Test through guardrail layers?")
191
+ halt_on_critical = _confirm(" Halt on CRITICAL finding?", default=False)
192
+ capture_tool_traces = _confirm(" Capture tool call traces?")
193
+ slack_notify = _confirm(" Slack notification on completion?", default=False)
194
+
195
+ click.echo(_s(" Select scan depth:", dim=True))
196
+ depth = _pick(DEPTH_OPTIONS, "Choose depth")
197
+
198
+ click.echo(_s(" Select remediation mode:", dim=True))
199
+ remediation = _pick(REMEDIATION_OPTIONS, "Choose remediation mode")
200
+
201
+ click.echo(_s(" Schedule:", dim=True))
202
+ schedule = _pick(SCHEDULE_OPTIONS, "Choose schedule")
203
+
204
+ config = {
205
+ "concurrency": concurrency,
206
+ "timeout": timeout_ms,
207
+ "session_isolation": session_isolation,
208
+ "test_guardrails": test_guardrails,
209
+ "halt_on_critical": halt_on_critical,
210
+ "capture_tool_traces": capture_tool_traces,
211
+ "slack_notify": slack_notify,
212
+ "remediation_mode": remediation,
213
+ }
214
+ click.echo()
215
+
216
+ # Step 4: Review & Run
217
+ click.echo(_s("Step 4/4: Review & Launch", fg="green"))
218
+ mode_label = {
219
+ "agent": "Agent-Targeted",
220
+ "standard": "Standard Library",
221
+ "full": "Full Coverage",
222
+ }.get(scan_mode, scan_mode)
223
+ agent_name = agent.get("name", agent_id[:8])
224
+ conn_name = (
225
+ next(
226
+ (c.get("name", "?") for c in connections if c.get("id") == connection_id),
227
+ "None",
228
+ )
229
+ if connection_id
230
+ else "None"
231
+ )
232
+
233
+ click.echo(_s("\u250c" + "\u2500" * 47 + "\u2510", dim=True))
234
+ for line in [
235
+ f" Agent: {agent_name}",
236
+ f" Connection: {conn_name}",
237
+ f" Mode: {mode_label}",
238
+ f" Depth: {depth}",
239
+ f" Remediation: {remediation}",
240
+ f" Concurrency: {concurrency}",
241
+ f" Isolation: {'On' if session_isolation else 'Off'}",
242
+ f" Slack: {'On' if slack_notify else 'Off'}",
243
+ ]:
244
+ click.echo(_s(f"\u2502 {line:<45}\u2502", dim=True))
245
+ click.echo(_s("\u2514" + "\u2500" * 47 + "\u2518", dim=True))
246
+ click.echo()
247
+
248
+ click.echo(_s("Choose action:", bold=True))
249
+ click.echo(
250
+ f" [{_s('1', fg='green', bold=True)}] {_s('Launch Scan', fg='green', bold=True)}"
251
+ )
252
+ click.echo(
253
+ f" [{_s('2', fg='yellow', bold=True)}] {_s('Save as Template', fg='yellow', bold=True)}"
254
+ )
255
+
256
+ action = click.prompt("Enter choice", type=int, default=1)
257
+ if action == 2:
258
+ _save_template(
259
+ cfg,
260
+ agent_id,
261
+ connection_id,
262
+ depth,
263
+ scan_mode,
264
+ remediation,
265
+ config,
266
+ agent_name,
267
+ )
268
+ return
269
+
270
+ click.echo()
271
+ click.echo(_s("\u2726 Starting scan...", fg="green", bold=True))
272
+ probe_str = ",".join(DEFAULT_PROBES)
273
+ _run_scan(agent_id, probe_str, "summary", 2, 600, None, connection_id, config)
274
+
275
+
276
+ def _save_template(
277
+ cfg,
278
+ agent_id: str,
279
+ connection_id: str | None,
280
+ depth: str,
281
+ attack_suite: str,
282
+ remediation: str,
283
+ config: dict,
284
+ default_name: str,
285
+ ) -> None:
286
+ name = click.prompt("Template name", default=f"Scan - {default_name}")
287
+ template_config = dict(config)
288
+ template_config["remediation_mode"] = remediation
289
+ payload = {
290
+ "name": name,
291
+ "agent_id": agent_id,
292
+ "depth": depth,
293
+ "attack_suite": attack_suite,
294
+ "config": template_config,
295
+ }
296
+ if connection_id:
297
+ payload["connection_id"] = connection_id
298
+ try:
299
+ r = httpx.post(
300
+ f"{cfg.api_url}/v1/templates",
301
+ json=payload,
302
+ headers=cfg.auth_headers(),
303
+ timeout=cfg.timeout,
304
+ )
305
+ r.raise_for_status()
306
+ except httpx.HTTPStatusError as exc:
307
+ click.echo(
308
+ _s(f"Error saving template: {fmt_http_error(exc)}", fg="red"), err=True
309
+ )
310
+ raise SystemExit(1)
311
+ data = r.json()
312
+ click.echo(
313
+ _s(f"\u2726 Template saved: {data.get('id', '?')[:8]}", fg="green", bold=True)
314
+ )
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: orithos-cli
3
+ Version: 0.1.0
4
+ Summary: Orithos CLI — AI agent security testing from the terminal: scan deployed agents, MCP servers, and skill/plugin artifacts.
5
+ Author: Orithos
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://orithos.com
8
+ Project-URL: Documentation, https://orithos.com/docs
9
+ Project-URL: Repository, https://github.com/nishkmg/TraceShield
10
+ Keywords: ai-security,agent-security,red-team,mcp,llm,security
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Security
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: click>=8.1.0
21
+ Requires-Dist: httpx>=0.27.0
22
+ Requires-Dist: pydantic>=2.0.0
23
+ Requires-Dist: pyyaml>=6.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
26
+ Requires-Dist: pytest-mock>=3.14.0; extra == "dev"
27
+ Requires-Dist: types-PyYAML>=6.0; extra == "dev"
28
+
29
+ # Orithos CLI
30
+
31
+ AI agent security testing from the terminal. Scan deployed agents and MCP
32
+ servers against the Orithos probe catalog, vet skill/plugin artifacts before
33
+ they run, and pull findings, compliance reports, and evidence packages — all
34
+ from the command line.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install orithos-cli
40
+ ```
41
+
42
+ ## Quick start
43
+
44
+ ```bash
45
+ # Point the CLI at your Orithos account (or use ORITHOS_API_KEY / ORITHOS_API_URL env vars)
46
+ orithos configure
47
+
48
+ # Vet a skill or plugin before installing it — no account needed, fully offline
49
+ orithos skill scan ./some-agent-skill
50
+
51
+ # Scan a registered agent
52
+ orithos scan run --agent-id <AGENT_ID>
53
+ orithos scan status <SCAN_ID>
54
+ orithos scan findings <SCAN_ID> --format sarif
55
+ ```
56
+
57
+ ## `orithos skill scan`
58
+
59
+ Static vetting for agent skills, plugins, and MCP packages. Runs offline with
60
+ zero outbound calls; archives (zip/tar.gz) are unpacked with traversal
61
+ protection.
62
+
63
+ ```bash
64
+ orithos skill scan <path|url> [--format text|json|sarif] [--fail-on critical|high|medium|low]
65
+ ```
66
+
67
+ Seven check classes: manifest hygiene · declared-vs-implied permissions ·
68
+ outbound destinations (exfil-prone endpoints, IP literals) · shell & install
69
+ hooks · credential access · obfuscation · known-bad signatures. Every finding
70
+ cites file, line, and evidence.
71
+
72
+ Exit codes: `0` clean · `1` findings at/above `--fail-on` (default `high`) ·
73
+ `2` error — ready for CI gates.
74
+
75
+ ## Commands
76
+
77
+ - `orithos agent ...` — register and manage agent endpoints
78
+ - `orithos scan ...` — run scans, fetch findings (`summary`/`json`/`sarif`/`junit`)
79
+ - `orithos skill scan ...` — static skill/plugin vetting
80
+ - `orithos mcp ...` — interact with the Orithos MCP policy server
81
+ - `orithos compliance ...` — compliance mappings and reports
82
+ - `orithos verify <package>` — offline evidence-package integrity check (hashes + Merkle root)
83
+ - `orithos connection / discovery / graph / guardrail / probes / remediation / runtime ...` — platform workflows
84
+
85
+ ## Environment variables
86
+
87
+ | Variable | Default | Description |
88
+ |----------|---------|-------------|
89
+ | `ORITHOS_API_KEY` | — | API key (`tsk_...`); overrides the config file |
90
+ | `ORITHOS_API_URL` | `https://api.orithos.com` | API base URL |
91
+ | `ORITHOS_ORG_ID` | — | Organisation ID (optional override) |
92
+ | `ORITHOS_TIMEOUT` | `30` | Request timeout in seconds |
93
+
94
+ ## Development
95
+
96
+ ```bash
97
+ pip install -e "apps/cli[dev]"
98
+ pytest -q
99
+ ```