openhack-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.
- openhack_cli-0.1.0/LICENSE +21 -0
- openhack_cli-0.1.0/PKG-INFO +164 -0
- openhack_cli-0.1.0/README.md +132 -0
- openhack_cli-0.1.0/openhack_cli/__init__.py +3 -0
- openhack_cli-0.1.0/openhack_cli/cli.py +88 -0
- openhack_cli-0.1.0/openhack_cli/client.py +124 -0
- openhack_cli-0.1.0/openhack_cli/commands/__init__.py +1 -0
- openhack_cli-0.1.0/openhack_cli/commands/auth.py +185 -0
- openhack_cli-0.1.0/openhack_cli/commands/config_cmd.py +56 -0
- openhack_cli-0.1.0/openhack_cli/commands/orgs.py +71 -0
- openhack_cli-0.1.0/openhack_cli/commands/pentest.py +603 -0
- openhack_cli-0.1.0/openhack_cli/commands/projects.py +130 -0
- openhack_cli-0.1.0/openhack_cli/commands/scans.py +154 -0
- openhack_cli-0.1.0/openhack_cli/commands/vulns.py +341 -0
- openhack_cli-0.1.0/openhack_cli/config.py +137 -0
- openhack_cli-0.1.0/openhack_cli/context.py +76 -0
- openhack_cli-0.1.0/openhack_cli/output.py +104 -0
- openhack_cli-0.1.0/openhack_cli.egg-info/PKG-INFO +164 -0
- openhack_cli-0.1.0/openhack_cli.egg-info/SOURCES.txt +23 -0
- openhack_cli-0.1.0/openhack_cli.egg-info/dependency_links.txt +1 -0
- openhack_cli-0.1.0/openhack_cli.egg-info/entry_points.txt +2 -0
- openhack_cli-0.1.0/openhack_cli.egg-info/requires.txt +3 -0
- openhack_cli-0.1.0/openhack_cli.egg-info/top_level.txt +1 -0
- openhack_cli-0.1.0/pyproject.toml +46 -0
- openhack_cli-0.1.0/setup.cfg +4 -0
|
@@ -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,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,132 @@
|
|
|
1
|
+
# OpenHack CLI
|
|
2
|
+
|
|
3
|
+
`openhack-cli` is the command-line interface for the [OpenHack](https://openhack.com)
|
|
4
|
+
security platform. It does one thing: talk to the OpenHack app in an
|
|
5
|
+
authenticated way, so that **humans and agents** can drive the platform from a
|
|
6
|
+
terminal or a script. No scanning happens in the CLI itself — it's a thin,
|
|
7
|
+
authenticated client over the OpenHack API.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pipx install openhack-cli # recommended (isolated)
|
|
13
|
+
# or
|
|
14
|
+
pip install openhack-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
From source:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
git clone <this repo> && cd openhack-cli
|
|
21
|
+
pip install -e .
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Requires Python 3.9+.
|
|
25
|
+
|
|
26
|
+
> **Driving the CLI from an automated agent?** See [`AGENT.md`](./AGENT.md) — an
|
|
27
|
+
> exhaustive, copy-pasteable command + field reference covering every command
|
|
28
|
+
> group, with allowed enum values and `--json` output for automation.
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
openhack-cli auth login # device-code login in your browser
|
|
34
|
+
openhack-cli orgs list # list your organizations
|
|
35
|
+
openhack-cli orgs use acme # pick the active org
|
|
36
|
+
openhack-cli projects list # list projects
|
|
37
|
+
openhack-cli projects use web # pick the active project
|
|
38
|
+
openhack-cli scans list # scans for the active project
|
|
39
|
+
openhack-cli vulns list # vulnerabilities across recent scans
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Authentication
|
|
43
|
+
|
|
44
|
+
Login uses the same **device-code flow** as the OpenHack web app:
|
|
45
|
+
|
|
46
|
+
1. `auth login` calls `POST /api/cli/auth` and prints a short code + URL.
|
|
47
|
+
2. Your browser opens; you sign in, choose an organization, and approve.
|
|
48
|
+
3. The CLI polls `POST /api/cli/auth/poll` until approval, then stores the
|
|
49
|
+
issued token.
|
|
50
|
+
|
|
51
|
+
The token is a long-lived, **org-scoped** API key sent as
|
|
52
|
+
`Authorization: Bearer openhack_…` on every request. It is stored at
|
|
53
|
+
`$XDG_CONFIG_HOME/openhack/config.json` (default `~/.config/openhack/config.json`)
|
|
54
|
+
with `0600` permissions.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
openhack-cli auth status # who am I / active context
|
|
58
|
+
openhack-cli auth token # print the raw token (scripting)
|
|
59
|
+
openhack-cli auth logout # remove local credentials
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Commands
|
|
63
|
+
|
|
64
|
+
| Group | Command | Description |
|
|
65
|
+
|-------|---------|-------------|
|
|
66
|
+
| `auth` | `login`, `logout`, `status`/`whoami`, `token` | Manage credentials |
|
|
67
|
+
| `orgs` | `list`, `use <id\|slug>` | List / select organizations |
|
|
68
|
+
| `projects` | `list`, `get`, `use`, `create` | Manage projects |
|
|
69
|
+
| `scans` | `list`, `get <scan_id>`, `trigger-full` | View / trigger scans |
|
|
70
|
+
| `vulns` | `list`, `groups`, `report`, `get`, `edit` | View, report, and edit project vulnerabilities |
|
|
71
|
+
| `pentest` | `list`, `get`, `create` | Pentesting engagements |
|
|
72
|
+
| `pentest findings` | `[engagement]` | Findings in an engagement (defaults to latest) |
|
|
73
|
+
| `pentest finding` | `get`, `create`, `update`, `delete`, `link`, `unlink` | Single finding: view / create / patch / delete / cross-reference |
|
|
74
|
+
| `config` | `show`, `set`, `path` | CLI configuration |
|
|
75
|
+
|
|
76
|
+
For an exhaustive, example-driven command reference (aimed at automation and
|
|
77
|
+
agents), see [`AGENT.md`](./AGENT.md).
|
|
78
|
+
|
|
79
|
+
## Scripting & agents
|
|
80
|
+
|
|
81
|
+
Pass `--json` (global flag) to get machine-readable output from any command:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
openhack-cli --json scans list
|
|
85
|
+
openhack-cli --json vulns list --severity critical
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Configuration can be driven entirely by environment variables (handy in CI):
|
|
89
|
+
|
|
90
|
+
| Variable | Purpose |
|
|
91
|
+
|----------|---------|
|
|
92
|
+
| `OPENHACK_TOKEN` | API token (overrides stored credentials) |
|
|
93
|
+
| `OPENHACK_APP_URL` | App base URL (default `https://app.openhack.com`) |
|
|
94
|
+
| `OPENHACK_DEV` | Set to `1` to target the local dev server (`http://localhost:9080`) |
|
|
95
|
+
| `XDG_CONFIG_HOME` | Where the config file lives |
|
|
96
|
+
|
|
97
|
+
### Targeting an environment
|
|
98
|
+
|
|
99
|
+
Production (`https://app.openhack.com`) is the default. For local dev work
|
|
100
|
+
(`http://localhost:9080`), the easiest option is to export `OPENHACK_DEV` once —
|
|
101
|
+
then every command targets localhost with no flags:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
export OPENHACK_DEV=1 # add to your ~/.zshrc for permanent dev mode
|
|
105
|
+
openhack-cli auth login # now logs in against localhost:9080
|
|
106
|
+
openhack-cli scans list
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Or use the `--local` flag per-command (shorthand for `--app-url
|
|
110
|
+
http://localhost:9080`):
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
openhack-cli --local auth login
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The app URL is resolved with this precedence (highest first):
|
|
117
|
+
|
|
118
|
+
1. `--app-url` / `--local` flag
|
|
119
|
+
2. `OPENHACK_APP_URL` env var
|
|
120
|
+
3. `OPENHACK_DEV=1` → `http://localhost:9080`
|
|
121
|
+
4. saved config (last login, or `openhack-cli config set app_url <url>`)
|
|
122
|
+
5. built-in default `https://app.openhack.com`
|
|
123
|
+
|
|
124
|
+
Exit codes: `0` success, `1` API/usage error, `2` auth error, `130` interrupted.
|
|
125
|
+
|
|
126
|
+
## Configuration
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
openhack-cli config show
|
|
130
|
+
openhack-cli config set app_url https://your-openhack-host
|
|
131
|
+
openhack-cli config path
|
|
132
|
+
```
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""OpenHack CLI entrypoint.
|
|
2
|
+
|
|
3
|
+
Wires the command groups together, sets up the shared Click context (config +
|
|
4
|
+
output mode), and provides uniform error handling for API/auth failures.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from . import __version__, output
|
|
14
|
+
from .client import APIError, AuthError
|
|
15
|
+
from .commands.auth import auth
|
|
16
|
+
from .commands.config_cmd import config
|
|
17
|
+
from .commands.orgs import orgs
|
|
18
|
+
from .commands.pentest import pentest
|
|
19
|
+
from .commands.projects import projects
|
|
20
|
+
from .commands.scans import scans
|
|
21
|
+
from .commands.vulns import vulns
|
|
22
|
+
from .config import Config, LOCAL_APP_URL
|
|
23
|
+
|
|
24
|
+
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@click.group(context_settings=CONTEXT_SETTINGS)
|
|
28
|
+
@click.version_option(__version__, "-v", "--version", prog_name="openhack-cli")
|
|
29
|
+
@click.option("--json", "json_output", is_flag=True,
|
|
30
|
+
help="Output raw JSON (for scripts and agents).")
|
|
31
|
+
@click.option("--app-url", default=None, help="Override the OpenHack app URL.")
|
|
32
|
+
@click.option("--local", "use_local", is_flag=True,
|
|
33
|
+
help=f"Target the local dev server ({LOCAL_APP_URL}).")
|
|
34
|
+
@click.option("--token", default=None,
|
|
35
|
+
help="Override the API token (else uses stored credentials).")
|
|
36
|
+
@click.pass_context
|
|
37
|
+
def cli(ctx: click.Context, json_output: bool, app_url: str | None,
|
|
38
|
+
use_local: bool, token: str | None) -> None:
|
|
39
|
+
"""OpenHack CLI — authenticated, programmatic access to OpenHack.
|
|
40
|
+
|
|
41
|
+
Start with `openhack-cli auth login`, then explore `orgs`, `projects`,
|
|
42
|
+
`scans`, `vulns`, and `pentest`.
|
|
43
|
+
"""
|
|
44
|
+
cfg = Config.load()
|
|
45
|
+
# --local is a shorthand for the dev server; --app-url wins if both given.
|
|
46
|
+
# These are runtime overrides (beat OPENHACK_DEV / OPENHACK_APP_URL / config).
|
|
47
|
+
if use_local:
|
|
48
|
+
cfg.set_override_app_url(LOCAL_APP_URL)
|
|
49
|
+
if app_url:
|
|
50
|
+
cfg.set_override_app_url(app_url.rstrip("/"))
|
|
51
|
+
if token:
|
|
52
|
+
cfg.set("token", token)
|
|
53
|
+
ctx.obj = {"config": cfg, "json": json_output}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
cli.add_command(auth)
|
|
57
|
+
cli.add_command(orgs)
|
|
58
|
+
cli.add_command(projects)
|
|
59
|
+
cli.add_command(scans)
|
|
60
|
+
cli.add_command(vulns)
|
|
61
|
+
cli.add_command(pentest)
|
|
62
|
+
cli.add_command(config)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main() -> None:
|
|
66
|
+
try:
|
|
67
|
+
cli(standalone_mode=False)
|
|
68
|
+
except AuthError as exc:
|
|
69
|
+
output.error(f"Authentication failed ({exc.status}): {exc.message}")
|
|
70
|
+
output.info("Run `openhack-cli auth login` to (re)authorize, or check "
|
|
71
|
+
"that this endpoint accepts CLI tokens.")
|
|
72
|
+
sys.exit(2)
|
|
73
|
+
except APIError as exc:
|
|
74
|
+
output.error(exc.message if exc.status else str(exc))
|
|
75
|
+
sys.exit(1)
|
|
76
|
+
except click.ClickException as exc:
|
|
77
|
+
exc.show()
|
|
78
|
+
sys.exit(exc.exit_code)
|
|
79
|
+
except click.Abort:
|
|
80
|
+
output.error("Aborted.")
|
|
81
|
+
sys.exit(130)
|
|
82
|
+
except KeyboardInterrupt:
|
|
83
|
+
output.error("Interrupted.")
|
|
84
|
+
sys.exit(130)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
main()
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""HTTP client for the OpenHack API.
|
|
2
|
+
|
|
3
|
+
Wraps ``requests`` with the CLI's Bearer-token auth and uniform error
|
|
4
|
+
handling. Every authenticated endpoint expects::
|
|
5
|
+
|
|
6
|
+
Authorization: Bearer openhack_<hex>
|
|
7
|
+
|
|
8
|
+
which is the token issued by the device-code login flow.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
import requests
|
|
16
|
+
|
|
17
|
+
from . import __version__
|
|
18
|
+
|
|
19
|
+
# Endpoints that the CLI talks to are JSON unless noted (e.g. PDF reports).
|
|
20
|
+
USER_AGENT = f"openhack-cli/{__version__}"
|
|
21
|
+
DEFAULT_TIMEOUT = 30
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class APIError(Exception):
|
|
25
|
+
"""Raised when the API returns a non-2xx response."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, status: int, message: str, body: Any = None):
|
|
28
|
+
self.status = status
|
|
29
|
+
self.message = message
|
|
30
|
+
self.body = body
|
|
31
|
+
super().__init__(f"[{status}] {message}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AuthError(APIError):
|
|
35
|
+
"""Raised on 401/403 so the CLI can prompt the user to re-login."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Client:
|
|
39
|
+
def __init__(self, app_url: str, token: Optional[str] = None,
|
|
40
|
+
timeout: int = DEFAULT_TIMEOUT):
|
|
41
|
+
self.app_url = app_url.rstrip("/")
|
|
42
|
+
self.token = token
|
|
43
|
+
self.timeout = timeout
|
|
44
|
+
self._session = requests.Session()
|
|
45
|
+
|
|
46
|
+
# ----- low-level --------------------------------------------------------
|
|
47
|
+
def _headers(self, extra: Optional[dict] = None, auth: bool = True) -> dict:
|
|
48
|
+
headers = {"User-Agent": USER_AGENT, "Accept": "application/json"}
|
|
49
|
+
if auth and self.token:
|
|
50
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
51
|
+
if extra:
|
|
52
|
+
headers.update(extra)
|
|
53
|
+
return headers
|
|
54
|
+
|
|
55
|
+
def request(
|
|
56
|
+
self,
|
|
57
|
+
method: str,
|
|
58
|
+
path: str,
|
|
59
|
+
*,
|
|
60
|
+
json: Any = None,
|
|
61
|
+
params: Optional[dict] = None,
|
|
62
|
+
auth: bool = True,
|
|
63
|
+
raw: bool = False,
|
|
64
|
+
) -> Any:
|
|
65
|
+
"""Perform a request and return parsed JSON (or the raw Response).
|
|
66
|
+
|
|
67
|
+
``raw=True`` returns the underlying ``requests.Response`` so callers can
|
|
68
|
+
stream binary payloads (e.g. PDF reports).
|
|
69
|
+
"""
|
|
70
|
+
url = path if path.startswith("http") else f"{self.app_url}{path}"
|
|
71
|
+
try:
|
|
72
|
+
resp = self._session.request(
|
|
73
|
+
method,
|
|
74
|
+
url,
|
|
75
|
+
json=json,
|
|
76
|
+
params=params,
|
|
77
|
+
headers=self._headers(auth=auth),
|
|
78
|
+
timeout=self.timeout,
|
|
79
|
+
)
|
|
80
|
+
except requests.RequestException as exc:
|
|
81
|
+
raise APIError(0, f"Network error: {exc}") from exc
|
|
82
|
+
|
|
83
|
+
if resp.status_code >= 400:
|
|
84
|
+
self._raise_for_status(resp)
|
|
85
|
+
|
|
86
|
+
if raw:
|
|
87
|
+
return resp
|
|
88
|
+
if not resp.content:
|
|
89
|
+
return None
|
|
90
|
+
ctype = resp.headers.get("Content-Type", "")
|
|
91
|
+
if "application/json" in ctype:
|
|
92
|
+
return resp.json()
|
|
93
|
+
return resp.content
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def _raise_for_status(resp: requests.Response) -> None:
|
|
97
|
+
message = resp.reason or "Request failed"
|
|
98
|
+
body: Any = None
|
|
99
|
+
try:
|
|
100
|
+
body = resp.json()
|
|
101
|
+
if isinstance(body, dict):
|
|
102
|
+
message = body.get("error") or body.get("message") or message
|
|
103
|
+
if body.get("message") and body.get("error"):
|
|
104
|
+
message = f"{body['error']}: {body['message']}"
|
|
105
|
+
except ValueError:
|
|
106
|
+
text = resp.text.strip()
|
|
107
|
+
if text:
|
|
108
|
+
message = text[:300]
|
|
109
|
+
if resp.status_code in (401, 403):
|
|
110
|
+
raise AuthError(resp.status_code, message, body)
|
|
111
|
+
raise APIError(resp.status_code, message, body)
|
|
112
|
+
|
|
113
|
+
# ----- verb helpers -----------------------------------------------------
|
|
114
|
+
def get(self, path: str, **kw) -> Any:
|
|
115
|
+
return self.request("GET", path, **kw)
|
|
116
|
+
|
|
117
|
+
def post(self, path: str, **kw) -> Any:
|
|
118
|
+
return self.request("POST", path, **kw)
|
|
119
|
+
|
|
120
|
+
def patch(self, path: str, **kw) -> Any:
|
|
121
|
+
return self.request("PATCH", path, **kw)
|
|
122
|
+
|
|
123
|
+
def delete(self, path: str, **kw) -> Any:
|
|
124
|
+
return self.request("DELETE", path, **kw)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command groups for the OpenHack CLI."""
|