openhack-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.
- openhack_cli/__init__.py +3 -0
- openhack_cli/cli.py +88 -0
- openhack_cli/client.py +124 -0
- openhack_cli/commands/__init__.py +1 -0
- openhack_cli/commands/auth.py +185 -0
- openhack_cli/commands/config_cmd.py +56 -0
- openhack_cli/commands/orgs.py +71 -0
- openhack_cli/commands/pentest.py +603 -0
- openhack_cli/commands/projects.py +130 -0
- openhack_cli/commands/scans.py +154 -0
- openhack_cli/commands/vulns.py +341 -0
- openhack_cli/config.py +137 -0
- openhack_cli/context.py +76 -0
- openhack_cli/output.py +104 -0
- openhack_cli-0.1.0.dist-info/METADATA +164 -0
- openhack_cli-0.1.0.dist-info/RECORD +20 -0
- openhack_cli-0.1.0.dist-info/WHEEL +5 -0
- openhack_cli-0.1.0.dist-info/entry_points.txt +2 -0
- openhack_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- openhack_cli-0.1.0.dist-info/top_level.txt +1 -0
openhack_cli/config.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Persistent CLI configuration: app URL, auth token, and active org/project context.
|
|
2
|
+
|
|
3
|
+
Stored as JSON at ``$XDG_CONFIG_HOME/openhack/config.json`` (falling back to
|
|
4
|
+
``~/.config/openhack/config.json``). The file is written with ``0600`` perms
|
|
5
|
+
since it holds a long-lived API token.
|
|
6
|
+
|
|
7
|
+
Every value can be overridden at runtime by an environment variable so the CLI
|
|
8
|
+
stays scriptable for agents and CI:
|
|
9
|
+
|
|
10
|
+
OPENHACK_APP_URL -> app_url
|
|
11
|
+
OPENHACK_TOKEN -> token
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Optional
|
|
20
|
+
|
|
21
|
+
DEFAULT_APP_URL = "https://app.openhack.com"
|
|
22
|
+
LOCAL_APP_URL = "http://localhost:9080"
|
|
23
|
+
|
|
24
|
+
# Environment overrides take precedence over the on-disk config.
|
|
25
|
+
ENV_APP_URL = "OPENHACK_APP_URL"
|
|
26
|
+
ENV_TOKEN = "OPENHACK_TOKEN"
|
|
27
|
+
# Export OPENHACK_DEV=1 to target the local dev server without passing --local.
|
|
28
|
+
ENV_DEV = "OPENHACK_DEV"
|
|
29
|
+
|
|
30
|
+
_TRUTHY = {"1", "true", "yes", "on"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _truthy(value: Optional[str]) -> bool:
|
|
34
|
+
return bool(value) and value.strip().lower() in _TRUTHY
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def config_dir() -> Path:
|
|
38
|
+
base = os.environ.get("XDG_CONFIG_HOME") or os.path.join(
|
|
39
|
+
os.path.expanduser("~"), ".config"
|
|
40
|
+
)
|
|
41
|
+
return Path(base) / "openhack"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def config_path() -> Path:
|
|
45
|
+
return config_dir() / "config.json"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Config:
|
|
49
|
+
"""Thin wrapper over the JSON config file with env-var overrides."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, data: Optional[dict[str, Any]] = None):
|
|
52
|
+
self._data: dict[str, Any] = data or {}
|
|
53
|
+
# Set by CLI flags (--app-url/--local); wins over env and saved config.
|
|
54
|
+
self._override_app_url: Optional[str] = None
|
|
55
|
+
|
|
56
|
+
# ----- loading / saving -------------------------------------------------
|
|
57
|
+
@classmethod
|
|
58
|
+
def load(cls) -> "Config":
|
|
59
|
+
path = config_path()
|
|
60
|
+
if path.exists():
|
|
61
|
+
try:
|
|
62
|
+
data = json.loads(path.read_text())
|
|
63
|
+
except (json.JSONDecodeError, OSError):
|
|
64
|
+
data = {}
|
|
65
|
+
else:
|
|
66
|
+
data = {}
|
|
67
|
+
return cls(data)
|
|
68
|
+
|
|
69
|
+
def save(self) -> None:
|
|
70
|
+
path = config_path()
|
|
71
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
# Write atomically-ish, then lock down perms (token lives here).
|
|
73
|
+
tmp = path.with_suffix(".json.tmp")
|
|
74
|
+
tmp.write_text(json.dumps(self._data, indent=2) + "\n")
|
|
75
|
+
os.chmod(tmp, 0o600)
|
|
76
|
+
tmp.replace(path)
|
|
77
|
+
os.chmod(path, 0o600)
|
|
78
|
+
|
|
79
|
+
# ----- resolved accessors (env wins) -----------------------------------
|
|
80
|
+
def set_override_app_url(self, url: str) -> None:
|
|
81
|
+
"""Force the app URL for this invocation (from a CLI flag)."""
|
|
82
|
+
self._override_app_url = url.rstrip("/")
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def app_url(self) -> str:
|
|
86
|
+
# Precedence (highest first):
|
|
87
|
+
# 1. CLI flag (--app-url / --local)
|
|
88
|
+
# 2. OPENHACK_APP_URL env
|
|
89
|
+
# 3. OPENHACK_DEV=1 -> local dev server
|
|
90
|
+
# 4. saved config (last login / `config set`)
|
|
91
|
+
# 5. built-in production default
|
|
92
|
+
if self._override_app_url:
|
|
93
|
+
return self._override_app_url
|
|
94
|
+
explicit = os.environ.get(ENV_APP_URL)
|
|
95
|
+
if explicit:
|
|
96
|
+
return explicit.rstrip("/")
|
|
97
|
+
if _truthy(os.environ.get(ENV_DEV)):
|
|
98
|
+
return LOCAL_APP_URL
|
|
99
|
+
return (self._data.get("app_url") or DEFAULT_APP_URL).rstrip("/")
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def token(self) -> Optional[str]:
|
|
103
|
+
return os.environ.get(ENV_TOKEN) or self._data.get("token")
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def user(self) -> Optional[dict[str, Any]]:
|
|
107
|
+
return self._data.get("user")
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def org(self) -> Optional[dict[str, Any]]:
|
|
111
|
+
return self._data.get("org")
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def project(self) -> Optional[dict[str, Any]]:
|
|
115
|
+
return self._data.get("project")
|
|
116
|
+
|
|
117
|
+
# ----- mutators ---------------------------------------------------------
|
|
118
|
+
def set(self, key: str, value: Any) -> None:
|
|
119
|
+
if value is None:
|
|
120
|
+
self._data.pop(key, None)
|
|
121
|
+
else:
|
|
122
|
+
self._data[key] = value
|
|
123
|
+
|
|
124
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
125
|
+
return self._data.get(key, default)
|
|
126
|
+
|
|
127
|
+
def as_dict(self) -> dict[str, Any]:
|
|
128
|
+
return dict(self._data)
|
|
129
|
+
|
|
130
|
+
def clear_auth(self) -> None:
|
|
131
|
+
"""Remove credentials and bound context (logout)."""
|
|
132
|
+
for key in ("token", "user", "org", "project"):
|
|
133
|
+
self._data.pop(key, None)
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def is_authenticated(self) -> bool:
|
|
137
|
+
return bool(self.token)
|
openhack_cli/context.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Helpers that bridge Click context, config, and the API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from .client import Client
|
|
10
|
+
from .config import Config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_config(ctx: click.Context) -> Config:
|
|
14
|
+
return ctx.obj["config"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_client(ctx: click.Context, require_auth: bool = True) -> Client:
|
|
18
|
+
"""Return a configured API client, optionally requiring a stored token."""
|
|
19
|
+
cfg: Config = ctx.obj["config"]
|
|
20
|
+
if require_auth and not cfg.token:
|
|
21
|
+
raise click.ClickException(
|
|
22
|
+
"Not logged in. Run `openhack-cli auth login` first."
|
|
23
|
+
)
|
|
24
|
+
if "client" not in ctx.obj:
|
|
25
|
+
ctx.obj["client"] = Client(cfg.app_url, token=cfg.token)
|
|
26
|
+
return ctx.obj["client"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_json(ctx: click.Context) -> bool:
|
|
30
|
+
return bool(ctx.obj.get("json"))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resolve_org_id(ctx: click.Context, org: Optional[str]) -> str:
|
|
34
|
+
"""Resolve an org id from an explicit arg or the active context.
|
|
35
|
+
|
|
36
|
+
``org`` may be an org id (passed through as-is). When omitted we fall back
|
|
37
|
+
to the org bound to the active context (set via `orgs use`, or the org the
|
|
38
|
+
login token is bound to).
|
|
39
|
+
"""
|
|
40
|
+
cfg: Config = ctx.obj["config"]
|
|
41
|
+
if org:
|
|
42
|
+
return org
|
|
43
|
+
active = cfg.org
|
|
44
|
+
if active and active.get("id"):
|
|
45
|
+
return active["id"]
|
|
46
|
+
raise click.ClickException(
|
|
47
|
+
"No organization selected. Pass --org <id> or run `openhack-cli orgs use <id|slug>`."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def resolve_project_id(ctx: click.Context, project: Optional[str]) -> str:
|
|
52
|
+
"""Resolve a project id from an explicit arg or the active context."""
|
|
53
|
+
cfg: Config = ctx.obj["config"]
|
|
54
|
+
if project:
|
|
55
|
+
return project
|
|
56
|
+
active = cfg.project
|
|
57
|
+
if active and active.get("id"):
|
|
58
|
+
return active["id"]
|
|
59
|
+
raise click.ClickException(
|
|
60
|
+
"No project selected. Pass a project id or run `openhack-cli projects use <id|slug>`."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def match_resource(items: list[dict], needle: str,
|
|
65
|
+
keys: tuple[str, ...] = ("id", "slug", "name")) -> Optional[dict]:
|
|
66
|
+
"""Find a resource by id/slug/name (case-insensitive for slug/name)."""
|
|
67
|
+
for item in items:
|
|
68
|
+
if item.get("id") == needle:
|
|
69
|
+
return item
|
|
70
|
+
low = needle.lower()
|
|
71
|
+
for item in items:
|
|
72
|
+
for key in keys:
|
|
73
|
+
val = item.get(key)
|
|
74
|
+
if val and str(val).lower() == low:
|
|
75
|
+
return item
|
|
76
|
+
return None
|
openhack_cli/output.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Rendering helpers shared by all commands.
|
|
2
|
+
|
|
3
|
+
Every command supports two modes:
|
|
4
|
+
- human: Rich tables / panels (default, when stdout is a TTY or not --json)
|
|
5
|
+
- json: raw machine-readable JSON for agents and scripts (--json)
|
|
6
|
+
|
|
7
|
+
The active mode is carried on the Click context object (``ctx.obj``).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json as _json
|
|
13
|
+
import sys
|
|
14
|
+
from typing import Any, Optional, Sequence
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
err_console = Console(stderr=True)
|
|
22
|
+
|
|
23
|
+
# Severity ordering + colors reused across scans / findings / pentest output.
|
|
24
|
+
SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
|
|
25
|
+
SEVERITY_COLORS = {
|
|
26
|
+
"critical": "bright_red",
|
|
27
|
+
"high": "red",
|
|
28
|
+
"medium": "yellow",
|
|
29
|
+
"low": "cyan",
|
|
30
|
+
"info": "blue",
|
|
31
|
+
}
|
|
32
|
+
STATUS_COLORS = {
|
|
33
|
+
"completed": "green",
|
|
34
|
+
"approved": "green",
|
|
35
|
+
"in_progress": "yellow",
|
|
36
|
+
"pending": "yellow",
|
|
37
|
+
"draft": "dim",
|
|
38
|
+
"failed": "red",
|
|
39
|
+
"cancelled": "red",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def severity_label(sev: Optional[str]) -> str:
|
|
44
|
+
if not sev:
|
|
45
|
+
return "[dim]-[/dim]"
|
|
46
|
+
color = SEVERITY_COLORS.get(sev.lower(), "white")
|
|
47
|
+
return f"[{color}]{sev.upper()}[/{color}]"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def status_label(status: Optional[str]) -> str:
|
|
51
|
+
if not status:
|
|
52
|
+
return "[dim]-[/dim]"
|
|
53
|
+
color = STATUS_COLORS.get(status.lower(), "white")
|
|
54
|
+
return f"[{color}]{status}[/{color}]"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def print_json(data: Any) -> None:
|
|
58
|
+
"""Emit raw JSON to stdout (machine mode)."""
|
|
59
|
+
click.echo(_json.dumps(data, indent=2, default=str))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def emit(ctx_obj: dict, data: Any, render) -> None:
|
|
63
|
+
"""Print JSON when --json is set, otherwise call ``render(data)``."""
|
|
64
|
+
if ctx_obj.get("json"):
|
|
65
|
+
print_json(data)
|
|
66
|
+
else:
|
|
67
|
+
render(data)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def make_table(title: Optional[str], columns: Sequence[str]) -> Table:
|
|
71
|
+
table = Table(title=title, title_justify="left", header_style="bold",
|
|
72
|
+
expand=False, pad_edge=False)
|
|
73
|
+
for col in columns:
|
|
74
|
+
table.add_column(col, overflow="fold")
|
|
75
|
+
return table
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def info(message: str) -> None:
|
|
79
|
+
err_console.print(message)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def success(message: str) -> None:
|
|
83
|
+
err_console.print(f"[green]✓[/green] {message}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def warn(message: str) -> None:
|
|
87
|
+
err_console.print(f"[yellow]![/yellow] {message}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def error(message: str) -> None:
|
|
91
|
+
err_console.print(f"[red]✗[/red] {message}")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def fail(message: str, code: int = 1) -> "click.ClickException":
|
|
95
|
+
"""Print an error and exit with a non-zero code."""
|
|
96
|
+
error(message)
|
|
97
|
+
sys.exit(code)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def short(value: Optional[str], length: int = 36) -> str:
|
|
101
|
+
if not value:
|
|
102
|
+
return "-"
|
|
103
|
+
value = str(value)
|
|
104
|
+
return value if len(value) <= length else value[: length - 1] + "…"
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openhack-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command-line interface for the OpenHack security platform.
|
|
5
|
+
Author: OpenHack
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://openhack.com
|
|
8
|
+
Project-URL: Repository, https://github.com/openhackai/cli
|
|
9
|
+
Project-URL: Issues, https://github.com/openhackai/cli/issues
|
|
10
|
+
Keywords: openhack,security,pentest,cli,sast,vulnerability
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
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.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Security
|
|
24
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: click>=8.1
|
|
29
|
+
Requires-Dist: requests>=2.31
|
|
30
|
+
Requires-Dist: rich>=13.7
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# OpenHack CLI
|
|
34
|
+
|
|
35
|
+
`openhack-cli` is the command-line interface for the [OpenHack](https://openhack.com)
|
|
36
|
+
security platform. It does one thing: talk to the OpenHack app in an
|
|
37
|
+
authenticated way, so that **humans and agents** can drive the platform from a
|
|
38
|
+
terminal or a script. No scanning happens in the CLI itself — it's a thin,
|
|
39
|
+
authenticated client over the OpenHack API.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pipx install openhack-cli # recommended (isolated)
|
|
45
|
+
# or
|
|
46
|
+
pip install openhack-cli
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
From source:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git clone <this repo> && cd openhack-cli
|
|
53
|
+
pip install -e .
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Requires Python 3.9+.
|
|
57
|
+
|
|
58
|
+
> **Driving the CLI from an automated agent?** See [`AGENT.md`](./AGENT.md) — an
|
|
59
|
+
> exhaustive, copy-pasteable command + field reference covering every command
|
|
60
|
+
> group, with allowed enum values and `--json` output for automation.
|
|
61
|
+
|
|
62
|
+
## Quick start
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
openhack-cli auth login # device-code login in your browser
|
|
66
|
+
openhack-cli orgs list # list your organizations
|
|
67
|
+
openhack-cli orgs use acme # pick the active org
|
|
68
|
+
openhack-cli projects list # list projects
|
|
69
|
+
openhack-cli projects use web # pick the active project
|
|
70
|
+
openhack-cli scans list # scans for the active project
|
|
71
|
+
openhack-cli vulns list # vulnerabilities across recent scans
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Authentication
|
|
75
|
+
|
|
76
|
+
Login uses the same **device-code flow** as the OpenHack web app:
|
|
77
|
+
|
|
78
|
+
1. `auth login` calls `POST /api/cli/auth` and prints a short code + URL.
|
|
79
|
+
2. Your browser opens; you sign in, choose an organization, and approve.
|
|
80
|
+
3. The CLI polls `POST /api/cli/auth/poll` until approval, then stores the
|
|
81
|
+
issued token.
|
|
82
|
+
|
|
83
|
+
The token is a long-lived, **org-scoped** API key sent as
|
|
84
|
+
`Authorization: Bearer openhack_…` on every request. It is stored at
|
|
85
|
+
`$XDG_CONFIG_HOME/openhack/config.json` (default `~/.config/openhack/config.json`)
|
|
86
|
+
with `0600` permissions.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
openhack-cli auth status # who am I / active context
|
|
90
|
+
openhack-cli auth token # print the raw token (scripting)
|
|
91
|
+
openhack-cli auth logout # remove local credentials
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Commands
|
|
95
|
+
|
|
96
|
+
| Group | Command | Description |
|
|
97
|
+
|-------|---------|-------------|
|
|
98
|
+
| `auth` | `login`, `logout`, `status`/`whoami`, `token` | Manage credentials |
|
|
99
|
+
| `orgs` | `list`, `use <id\|slug>` | List / select organizations |
|
|
100
|
+
| `projects` | `list`, `get`, `use`, `create` | Manage projects |
|
|
101
|
+
| `scans` | `list`, `get <scan_id>`, `trigger-full` | View / trigger scans |
|
|
102
|
+
| `vulns` | `list`, `groups`, `report`, `get`, `edit` | View, report, and edit project vulnerabilities |
|
|
103
|
+
| `pentest` | `list`, `get`, `create` | Pentesting engagements |
|
|
104
|
+
| `pentest findings` | `[engagement]` | Findings in an engagement (defaults to latest) |
|
|
105
|
+
| `pentest finding` | `get`, `create`, `update`, `delete`, `link`, `unlink` | Single finding: view / create / patch / delete / cross-reference |
|
|
106
|
+
| `config` | `show`, `set`, `path` | CLI configuration |
|
|
107
|
+
|
|
108
|
+
For an exhaustive, example-driven command reference (aimed at automation and
|
|
109
|
+
agents), see [`AGENT.md`](./AGENT.md).
|
|
110
|
+
|
|
111
|
+
## Scripting & agents
|
|
112
|
+
|
|
113
|
+
Pass `--json` (global flag) to get machine-readable output from any command:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
openhack-cli --json scans list
|
|
117
|
+
openhack-cli --json vulns list --severity critical
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Configuration can be driven entirely by environment variables (handy in CI):
|
|
121
|
+
|
|
122
|
+
| Variable | Purpose |
|
|
123
|
+
|----------|---------|
|
|
124
|
+
| `OPENHACK_TOKEN` | API token (overrides stored credentials) |
|
|
125
|
+
| `OPENHACK_APP_URL` | App base URL (default `https://app.openhack.com`) |
|
|
126
|
+
| `OPENHACK_DEV` | Set to `1` to target the local dev server (`http://localhost:9080`) |
|
|
127
|
+
| `XDG_CONFIG_HOME` | Where the config file lives |
|
|
128
|
+
|
|
129
|
+
### Targeting an environment
|
|
130
|
+
|
|
131
|
+
Production (`https://app.openhack.com`) is the default. For local dev work
|
|
132
|
+
(`http://localhost:9080`), the easiest option is to export `OPENHACK_DEV` once —
|
|
133
|
+
then every command targets localhost with no flags:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
export OPENHACK_DEV=1 # add to your ~/.zshrc for permanent dev mode
|
|
137
|
+
openhack-cli auth login # now logs in against localhost:9080
|
|
138
|
+
openhack-cli scans list
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Or use the `--local` flag per-command (shorthand for `--app-url
|
|
142
|
+
http://localhost:9080`):
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
openhack-cli --local auth login
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The app URL is resolved with this precedence (highest first):
|
|
149
|
+
|
|
150
|
+
1. `--app-url` / `--local` flag
|
|
151
|
+
2. `OPENHACK_APP_URL` env var
|
|
152
|
+
3. `OPENHACK_DEV=1` → `http://localhost:9080`
|
|
153
|
+
4. saved config (last login, or `openhack-cli config set app_url <url>`)
|
|
154
|
+
5. built-in default `https://app.openhack.com`
|
|
155
|
+
|
|
156
|
+
Exit codes: `0` success, `1` API/usage error, `2` auth error, `130` interrupted.
|
|
157
|
+
|
|
158
|
+
## Configuration
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
openhack-cli config show
|
|
162
|
+
openhack-cli config set app_url https://your-openhack-host
|
|
163
|
+
openhack-cli config path
|
|
164
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
openhack_cli/__init__.py,sha256=6nfu9_udx4lsS141GD_R5QAtLAYXHcFhoWDWw63odBQ,107
|
|
2
|
+
openhack_cli/cli.py,sha256=BDl-T3qbilOtPAxUT-8ohnA64EVewPqTs5FzZNRRpDU,2901
|
|
3
|
+
openhack_cli/client.py,sha256=sJB_2IyI1Pym7c4vbC3Ka3wCa6PuEKFeZDeS9pLbG8Y,3980
|
|
4
|
+
openhack_cli/config.py,sha256=nXHcsAlCFcE_WG9mDkop26Gxt9bnGvCqeUZXebEXRy0,4421
|
|
5
|
+
openhack_cli/context.py,sha256=JcGn3riPW3I1Y9wNllihxNOJLLj0A5iYsxfjSSyaSk0,2396
|
|
6
|
+
openhack_cli/output.py,sha256=99Rt9e9kE_2nW33Qob_Cy237NeorDJ0XJwEuDoR99M8,2745
|
|
7
|
+
openhack_cli/commands/__init__.py,sha256=9NftJ5ZmxEB-ahGxqevwt2mWSvoIhYqWN1qNiafgl2Q,43
|
|
8
|
+
openhack_cli/commands/auth.py,sha256=4XUOOLZexNjJkWwCbz-DoYGCbtU7nJRH9iP0Dw4Nu9k,6115
|
|
9
|
+
openhack_cli/commands/config_cmd.py,sha256=EZLtRJXYLQkW-foGDgeBLTDQU-sYj7gfcZQ9b-WnzbM,1577
|
|
10
|
+
openhack_cli/commands/orgs.py,sha256=Jt2EJA8dXdPNnZBdVwNpGRhenV-bn_RPiZAMU4k-hqE,2158
|
|
11
|
+
openhack_cli/commands/pentest.py,sha256=5WeNRDvYzqWtgkb8B9o_cBEC0-IhbCfixGeiJ9F1-dM,24970
|
|
12
|
+
openhack_cli/commands/projects.py,sha256=1N-EBtqA9dwzlwurkuC-FvuquSkLwyqpVGe8inPoPkk,4504
|
|
13
|
+
openhack_cli/commands/scans.py,sha256=ExnYWOHzuIfGuBtSMtzNET1nCOWC-D7IZ0630Xibpe4,5993
|
|
14
|
+
openhack_cli/commands/vulns.py,sha256=-6sx0Y8i5o_JBwrQatESua25T5UACY5Yf10bRtsg53o,13934
|
|
15
|
+
openhack_cli-0.1.0.dist-info/licenses/LICENSE,sha256=0tVdhGTtdQjkkFnwg6Lpg6OBdMhd8wA-9Ml_lZrIAt0,1065
|
|
16
|
+
openhack_cli-0.1.0.dist-info/METADATA,sha256=zHhMd_Vs5zYp-R6_m7JxWd3rhI0SVADaTkoAVcsblx8,5905
|
|
17
|
+
openhack_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
18
|
+
openhack_cli-0.1.0.dist-info/entry_points.txt,sha256=Whtj5Zn_-rVj3hbrBRDWvqGEM0I2d9-xX2Zg29FTdpA,55
|
|
19
|
+
openhack_cli-0.1.0.dist-info/top_level.txt,sha256=vV5GLcxelf8nE1-fcKcAawtKzfILLvrfmfr-bSt6Gc8,13
|
|
20
|
+
openhack_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OpenHack
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
openhack_cli
|