openhack 0.1.0b1__tar.gz → 0.1.2__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-0.1.2/LICENSE +21 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/PKG-INFO +37 -10
- {openhack-0.1.0b1 → openhack-0.1.2}/README.md +35 -8
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/__init__.py +1 -1
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/__main__.py +2 -1
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/llm.py +25 -2
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/config.py +9 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/setup.py +41 -32
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/tui.py +40 -9
- {openhack-0.1.0b1 → openhack-0.1.2}/pyproject.toml +3 -2
- {openhack-0.1.0b1 → openhack-0.1.2}/uv.lock +452 -1
- openhack-0.1.0b1/LICENSE +0 -661
- {openhack-0.1.0b1 → openhack-0.1.2}/.env.example +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/.github/workflows/tests.yml +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/.gitignore +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/__init__.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/base.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/browser_verifier.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/browser_verifier_swarm.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/checkpoint.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/context_manager.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/coordinator.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/endpoint_analyst.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/feature_hunter.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/hunter.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/hunter_swarm.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/recon.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/sandbox_verifier.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/sandbox_verifier_swarm.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/session.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/validator.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/validator_swarm.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/auth.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/browser/__init__.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/browser/runner.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/categories.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/deterministic_recon.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/entry_points.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/framework_classifier.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/framework_detection.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/headless_scan.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/__init__.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/browser_verifier.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/coordinator.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/django/__init__.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/django/auth_bypass.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/django/csrf.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/django/data_exposure.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/django/idor.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/django/injection.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/django/misconfiguration.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/django/ssrf.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/endpoint_analyst.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/express/__init__.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/express/auth_bypass.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/express/data_exposure.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/express/idor.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/express/injection.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/express/misconfiguration.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/express/ssrf.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/feature_hunter.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/flask/__init__.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/flask/auth_bypass.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/flask/data_exposure.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/flask/idor.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/flask/injection.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/flask/misconfiguration.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/flask/ssrf.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/hunter.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/hunter_continuation_loop.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/hunter_continuation_no_findings.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/hunter_continuation_no_progress.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/hunter_tool_instructions.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/__init__.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/auth_bypass.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/csrf.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/data_exposure.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/idor.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/injection.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/middleware_bypass.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/misconfiguration.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/server_actions.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/ssrf.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/xss.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/pr_analysis_system.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/pr_analysis_user.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/project_context.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/recon.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/reporter.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/researchers.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/sandbox_verifier.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/supabase/__init__.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/supabase/auth_tokens.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/supabase/edge_functions.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/supabase/graphql.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/supabase/postgrest.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/supabase/realtime.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/supabase/rls.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/supabase/rpc_functions.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/supabase/storage.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/supabase/tenant_isolation.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/validator.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/validator_continuation_incomplete.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/validator_tool_instructions.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/quality.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/sandbox/__init__.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/sandbox/orchestrator.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/sandbox/runner.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/scan_session.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/static_validator.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/tools/__init__.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/tools/ast_tools.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/tools/coverage.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/tools/filesystem.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/tools/nextjs.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/tools/registry.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/openhack/updates.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/scripts/run_browser_verify.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/scripts/run_browser_verify_live.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/scripts/run_feature_hunt.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/scripts/run_sandbox_verify.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/scripts/run_sandbox_verify_live.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/tests/__init__.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/tests/conftest.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/tests/test_categories.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/tests/test_checkpoint.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/tests/test_config.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/tests/test_coverage.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/tests/test_deterministic_recon.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/tests/test_entry_points.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/tests/test_filesystem_tools.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/tests/test_framework_classifier.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/tests/test_quality.py +0 -0
- {openhack-0.1.0b1 → openhack-0.1.2}/tests/test_scan_session.py +0 -0
openhack-0.1.2/LICENSE
ADDED
|
@@ -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.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openhack
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: AI-powered security scanner for your codebase. Find SQL injection, XSS, IDOR, auth bypass, and more — straight from your terminal.
|
|
5
5
|
Project-URL: Homepage, https://openhack.com
|
|
6
6
|
Project-URL: Documentation, https://github.com/openhackai/openhack
|
|
7
7
|
Project-URL: Repository, https://github.com/openhackai/openhack
|
|
8
8
|
Project-URL: Issues, https://github.com/openhackai/openhack/issues
|
|
9
9
|
Author: OpenHack
|
|
10
|
-
License-Expression:
|
|
10
|
+
License-Expression: MIT
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Keywords: ai-security,appsec,code-review,llm,sast,security,static-analysis,vulnerability-scanner
|
|
13
13
|
Classifier: Development Status :: 4 - Beta
|
|
@@ -41,11 +41,25 @@ Description-Content-Type: text/markdown
|
|
|
41
41
|
|
|
42
42
|
# ⏚ [OpenHack](https://openhack.com)
|
|
43
43
|
|
|
44
|
-
**Open Source Agentic Security Scanner for your codebase.**
|
|
44
|
+
**Open Source Agentic Security Scanner & Verifier for your codebase.**
|
|
45
45
|
|
|
46
|
-
Like Claude Code Security / Codex Security but open source
|
|
46
|
+
Like Claude Code Security / Codex Security but open source and **exclusively uses open source models**.
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
<p align="center">
|
|
49
|
+
<a href="https://openhack.com"><img src="https://img.shields.io/badge/Website-openhack.com-0969da?style=for-the-badge" alt="Website"></a>
|
|
50
|
+
|
|
51
|
+
<a href="https://openhack.com/discord"><img src="https://img.shields.io/badge/Discord-Join_Server-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Discord"></a>
|
|
52
|
+
|
|
53
|
+
<a href="https://x.com/openhackai"><img src="https://img.shields.io/badge/X-@openhackai-000000?style=for-the-badge&logo=x&logoColor=white" alt="Follow on X"></a>
|
|
54
|
+
</p>
|
|
55
|
+
|
|
56
|
+
<p align="center">
|
|
57
|
+
<a href="https://pypi.org/project/openhack/"><img src="https://img.shields.io/pypi/v/openhack?style=for-the-badge&label=pypi&color=3775A9" alt="PyPI"></a>
|
|
58
|
+
|
|
59
|
+
<a href="https://github.com/openhackai/openhack/blob/main/LICENSE"><img src="https://img.shields.io/github/license/openhackai/openhack?style=for-the-badge" alt="License"></a>
|
|
60
|
+
</p>
|
|
61
|
+
|
|
62
|
+
## Get started
|
|
49
63
|
|
|
50
64
|
```bash
|
|
51
65
|
pipx install openhack
|
|
@@ -57,6 +71,18 @@ Or with pip:
|
|
|
57
71
|
pip install openhack
|
|
58
72
|
```
|
|
59
73
|
|
|
74
|
+
## How it works
|
|
75
|
+
OpenHack does `recon` -> `hunting` -> `validation` -> `verification` all in one pipeline to find high quality verified vulnerabilities.
|
|
76
|
+
|
|
77
|
+
**Recon**: Does a deep dive and fully understands your application along with any custom context you give it. Builds a full project model before hunting begins.
|
|
78
|
+
|
|
79
|
+
**Hunter**: Specialized category based hunters get to finding vulnerabilities initially, along with feature based hunters divind deep to find vulnerabilities in risky code areas.
|
|
80
|
+
|
|
81
|
+
**Validation**: Validation agent performs a review of the finding and it's impact and whether it's even valid.
|
|
82
|
+
|
|
83
|
+
**Verification**: Verification agent performs a full browser + sandbox based attack to find verify vulnerabilities in a real docker / DOM environment.
|
|
84
|
+
|
|
85
|
+
|
|
60
86
|
## Quick start
|
|
61
87
|
|
|
62
88
|
```bash
|
|
@@ -170,10 +196,11 @@ Configuration is stored in `~/.openhack/config` (mode `0600` since it contains a
|
|
|
170
196
|
You can override at runtime via environment variables:
|
|
171
197
|
|
|
172
198
|
|
|
173
|
-
| Variable
|
|
174
|
-
|
|
|
175
|
-
| `OPENHACK_API_KEY`
|
|
176
|
-
| `OPENHACK_DEV=1`
|
|
199
|
+
| Variable | Effect |
|
|
200
|
+
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
201
|
+
| `OPENHACK_API_KEY` | Bearer token for the OpenHack inference API |
|
|
202
|
+
| `OPENHACK_DEV=1` | Point the CLI at local dev servers (app on `:9080`, inference on `:8787`) for self-hosted setups |
|
|
203
|
+
| `PROMPT_CACHING=0` | Stop sending `prompt_cache_key` with API calls — needed for OpenAI-compatible endpoints that reject it (also: `/config prompt_caching false`) |
|
|
177
204
|
|
|
178
205
|
|
|
179
206
|
## Privacy
|
|
@@ -186,4 +213,4 @@ OpenHack is open source. Issues and PRs welcome on [GitHub](https://github.com/o
|
|
|
186
213
|
|
|
187
214
|
## License
|
|
188
215
|
|
|
189
|
-
|
|
216
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
# ⏚ [OpenHack](https://openhack.com)
|
|
2
2
|
|
|
3
|
-
**Open Source Agentic Security Scanner for your codebase.**
|
|
3
|
+
**Open Source Agentic Security Scanner & Verifier for your codebase.**
|
|
4
4
|
|
|
5
|
-
Like Claude Code Security / Codex Security but open source
|
|
5
|
+
Like Claude Code Security / Codex Security but open source and **exclusively uses open source models**.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://openhack.com"><img src="https://img.shields.io/badge/Website-openhack.com-0969da?style=for-the-badge" alt="Website"></a>
|
|
9
|
+
|
|
10
|
+
<a href="https://openhack.com/discord"><img src="https://img.shields.io/badge/Discord-Join_Server-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Discord"></a>
|
|
11
|
+
|
|
12
|
+
<a href="https://x.com/openhackai"><img src="https://img.shields.io/badge/X-@openhackai-000000?style=for-the-badge&logo=x&logoColor=white" alt="Follow on X"></a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="https://pypi.org/project/openhack/"><img src="https://img.shields.io/pypi/v/openhack?style=for-the-badge&label=pypi&color=3775A9" alt="PyPI"></a>
|
|
17
|
+
|
|
18
|
+
<a href="https://github.com/openhackai/openhack/blob/main/LICENSE"><img src="https://img.shields.io/github/license/openhackai/openhack?style=for-the-badge" alt="License"></a>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
## Get started
|
|
8
22
|
|
|
9
23
|
```bash
|
|
10
24
|
pipx install openhack
|
|
@@ -16,6 +30,18 @@ Or with pip:
|
|
|
16
30
|
pip install openhack
|
|
17
31
|
```
|
|
18
32
|
|
|
33
|
+
## How it works
|
|
34
|
+
OpenHack does `recon` -> `hunting` -> `validation` -> `verification` all in one pipeline to find high quality verified vulnerabilities.
|
|
35
|
+
|
|
36
|
+
**Recon**: Does a deep dive and fully understands your application along with any custom context you give it. Builds a full project model before hunting begins.
|
|
37
|
+
|
|
38
|
+
**Hunter**: Specialized category based hunters get to finding vulnerabilities initially, along with feature based hunters divind deep to find vulnerabilities in risky code areas.
|
|
39
|
+
|
|
40
|
+
**Validation**: Validation agent performs a review of the finding and it's impact and whether it's even valid.
|
|
41
|
+
|
|
42
|
+
**Verification**: Verification agent performs a full browser + sandbox based attack to find verify vulnerabilities in a real docker / DOM environment.
|
|
43
|
+
|
|
44
|
+
|
|
19
45
|
## Quick start
|
|
20
46
|
|
|
21
47
|
```bash
|
|
@@ -129,10 +155,11 @@ Configuration is stored in `~/.openhack/config` (mode `0600` since it contains a
|
|
|
129
155
|
You can override at runtime via environment variables:
|
|
130
156
|
|
|
131
157
|
|
|
132
|
-
| Variable
|
|
133
|
-
|
|
|
134
|
-
| `OPENHACK_API_KEY`
|
|
135
|
-
| `OPENHACK_DEV=1`
|
|
158
|
+
| Variable | Effect |
|
|
159
|
+
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
160
|
+
| `OPENHACK_API_KEY` | Bearer token for the OpenHack inference API |
|
|
161
|
+
| `OPENHACK_DEV=1` | Point the CLI at local dev servers (app on `:9080`, inference on `:8787`) for self-hosted setups |
|
|
162
|
+
| `PROMPT_CACHING=0` | Stop sending `prompt_cache_key` with API calls — needed for OpenAI-compatible endpoints that reject it (also: `/config prompt_caching false`) |
|
|
136
163
|
|
|
137
164
|
|
|
138
165
|
## Privacy
|
|
@@ -145,4 +172,4 @@ OpenHack is open source. Issues and PRs welcome on [GitHub](https://github.com/o
|
|
|
145
172
|
|
|
146
173
|
## License
|
|
147
174
|
|
|
148
|
-
|
|
175
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""OpenHack Agent - Interactive TUI for vulnerability scanning."""
|
|
2
|
-
__version__ = "0.1.
|
|
2
|
+
__version__ = "0.1.2"
|
|
@@ -213,7 +213,8 @@ def main():
|
|
|
213
213
|
if needs_first_time_setup():
|
|
214
214
|
completed = run_first_time_setup()
|
|
215
215
|
if not completed:
|
|
216
|
-
print("\nSetup skipped. Run openhack again
|
|
216
|
+
print("\nSetup skipped. Run 'openhack' again to retry.\n")
|
|
217
|
+
return
|
|
217
218
|
|
|
218
219
|
from openhack.tui import main as tui_main
|
|
219
220
|
tui_main()
|
|
@@ -71,6 +71,10 @@ class LLMClient:
|
|
|
71
71
|
"kimi-k2.5": {"input": 0.50, "output": 2.80},
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
# Set to True for the rest of the session when the endpoint rejects
|
|
75
|
+
# prompt_cache_key (e.g. Groq), so we stop sending it.
|
|
76
|
+
_cache_key_unsupported = False
|
|
77
|
+
|
|
74
78
|
def __init__(
|
|
75
79
|
self,
|
|
76
80
|
model: Optional[str] = None,
|
|
@@ -156,7 +160,20 @@ class LLMClient:
|
|
|
156
160
|
tool_choice: Optional[str] = None,
|
|
157
161
|
on_chunk: Optional[Callable] = None,
|
|
158
162
|
) -> LLMResponse:
|
|
159
|
-
|
|
163
|
+
try:
|
|
164
|
+
return await self._chat(messages, tools, system, tool_choice=tool_choice, on_chunk=on_chunk)
|
|
165
|
+
except openai.APIStatusError as e:
|
|
166
|
+
detail = str(e).lower()
|
|
167
|
+
if (
|
|
168
|
+
self.prompt_cache_key
|
|
169
|
+
and not LLMClient._cache_key_unsupported
|
|
170
|
+
and ("prompt_cache_key" in detail or "prompt cache key" in detail)
|
|
171
|
+
):
|
|
172
|
+
LLMClient._cache_key_unsupported = True
|
|
173
|
+
print(" Endpoint doesn't support prompt caching — retrying without it.")
|
|
174
|
+
print(" To disable permanently: /config prompt_caching false")
|
|
175
|
+
return await self._chat(messages, tools, system, tool_choice=tool_choice, on_chunk=on_chunk)
|
|
176
|
+
raise
|
|
160
177
|
|
|
161
178
|
async def _chat(
|
|
162
179
|
self,
|
|
@@ -179,7 +196,13 @@ class LLMClient:
|
|
|
179
196
|
if tools:
|
|
180
197
|
kwargs["tools"] = self._convert_tools_to_openai_format(tools)
|
|
181
198
|
kwargs["tool_choice"] = tool_choice or "auto"
|
|
182
|
-
|
|
199
|
+
# Re-read settings via the module so /config changes apply mid-session.
|
|
200
|
+
from openhack import config as _config
|
|
201
|
+
if (
|
|
202
|
+
self.prompt_cache_key
|
|
203
|
+
and _config.settings.prompt_caching
|
|
204
|
+
and not LLMClient._cache_key_unsupported
|
|
205
|
+
):
|
|
183
206
|
kwargs["prompt_cache_key"] = self.prompt_cache_key
|
|
184
207
|
|
|
185
208
|
max_retries = settings.openhack_max_retries
|
|
@@ -99,6 +99,10 @@ class Settings(BaseSettings):
|
|
|
99
99
|
openhack_connect_timeout: int = 30
|
|
100
100
|
openhack_max_retries: int = 5
|
|
101
101
|
|
|
102
|
+
# Send prompt_cache_key with API calls. Supported by OpenHack and OpenAI;
|
|
103
|
+
# some OpenAI-compatible endpoints (e.g. Groq) reject unknown params.
|
|
104
|
+
prompt_caching: bool = True
|
|
105
|
+
|
|
102
106
|
recon_model_id: Optional[str] = None
|
|
103
107
|
hunter_model_id: Optional[str] = None
|
|
104
108
|
validator_model_id: Optional[str] = None
|
|
@@ -154,6 +158,11 @@ class Settings(BaseSettings):
|
|
|
154
158
|
env_file=".env",
|
|
155
159
|
env_file_encoding="utf-8",
|
|
156
160
|
case_sensitive=False,
|
|
161
|
+
# Ignore unrelated keys in a CWD .env / environment. The scanner runs
|
|
162
|
+
# inside arbitrary target repos whose .env files contain keys we don't
|
|
163
|
+
# own; without this, pydantic-settings' default extra="forbid" crashes
|
|
164
|
+
# the CLI on any unknown key (e.g. a target's gemini_sandbox_proxy_command).
|
|
165
|
+
extra="ignore",
|
|
157
166
|
)
|
|
158
167
|
|
|
159
168
|
def model_post_init(self, __context) -> None:
|
|
@@ -10,11 +10,11 @@ input for API keys, and a final confirmation screen.
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import asyncio
|
|
13
|
-
import getpass
|
|
14
13
|
import os
|
|
15
14
|
from typing import Optional
|
|
16
15
|
|
|
17
16
|
from prompt_toolkit import print_formatted_text
|
|
17
|
+
from prompt_toolkit.shortcuts import PromptSession
|
|
18
18
|
from prompt_toolkit.formatted_text import HTML
|
|
19
19
|
from prompt_toolkit.key_binding import KeyBindings
|
|
20
20
|
from prompt_toolkit.application import Application
|
|
@@ -94,6 +94,12 @@ def _has_running_loop() -> bool:
|
|
|
94
94
|
return False
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
async def _input_async(message: str, is_password: bool = False) -> str:
|
|
98
|
+
"""Async text input with full editing keybindings (word jump/delete)."""
|
|
99
|
+
session: PromptSession = PromptSession()
|
|
100
|
+
return await session.prompt_async(message, is_password=is_password)
|
|
101
|
+
|
|
102
|
+
|
|
97
103
|
# ── Arrow-key selection menu ──────────────────────────────────────
|
|
98
104
|
|
|
99
105
|
async def _select_menu_async(title: str, items: list[tuple[str, str, str]], default_idx: int = 0) -> int:
|
|
@@ -173,7 +179,7 @@ def _select_menu(title: str, items: list[tuple[str, str, str]], default_idx: int
|
|
|
173
179
|
|
|
174
180
|
# ── API key input ─────────────────────────────────────────────────
|
|
175
181
|
|
|
176
|
-
def _prompt_api_key(provider: dict, existing_key: Optional[str] = None) -> Optional[str]:
|
|
182
|
+
async def _prompt_api_key(provider: dict, existing_key: Optional[str] = None) -> Optional[str]:
|
|
177
183
|
"""Prompt for an API key with masked display."""
|
|
178
184
|
_html("")
|
|
179
185
|
_html(f' {B}API Key for {_esc(provider["display"])}{EB}')
|
|
@@ -193,7 +199,7 @@ def _prompt_api_key(provider: dict, existing_key: Optional[str] = None) -> Optio
|
|
|
193
199
|
_html("")
|
|
194
200
|
|
|
195
201
|
try:
|
|
196
|
-
key =
|
|
202
|
+
key = (await _input_async(" API Key: ", is_password=True)).strip()
|
|
197
203
|
except (EOFError, KeyboardInterrupt):
|
|
198
204
|
return existing_key
|
|
199
205
|
|
|
@@ -209,7 +215,7 @@ def _prompt_api_key(provider: dict, existing_key: Optional[str] = None) -> Optio
|
|
|
209
215
|
|
|
210
216
|
# ── Base URL input (for OpenHack provider) ───────────────────────────
|
|
211
217
|
|
|
212
|
-
def _prompt_base_url(existing: Optional[str] = None) -> str:
|
|
218
|
+
async def _prompt_base_url(existing: Optional[str] = None) -> str:
|
|
213
219
|
if not existing:
|
|
214
220
|
existing = settings.openhack_base_url
|
|
215
221
|
_html("")
|
|
@@ -218,7 +224,7 @@ def _prompt_base_url(existing: Optional[str] = None) -> str:
|
|
|
218
224
|
_html(f' {DIM}Press Enter to keep default{EDIM}')
|
|
219
225
|
_html("")
|
|
220
226
|
try:
|
|
221
|
-
url =
|
|
227
|
+
url = (await _input_async(" Base URL: ")).strip()
|
|
222
228
|
except (EOFError, KeyboardInterrupt):
|
|
223
229
|
return existing
|
|
224
230
|
return url if url else existing
|
|
@@ -226,7 +232,7 @@ def _prompt_base_url(existing: Optional[str] = None) -> str:
|
|
|
226
232
|
|
|
227
233
|
# ── Summary / confirmation ────────────────────────────────────────
|
|
228
234
|
|
|
229
|
-
def _show_summary(provider: dict, model_id: str, api_key: Optional[str], base_url: Optional[str] = None, org_name: Optional[str] = None) -> bool:
|
|
235
|
+
async def _show_summary(provider: dict, model_id: str, api_key: Optional[str], base_url: Optional[str] = None, org_name: Optional[str] = None) -> bool:
|
|
230
236
|
_html("")
|
|
231
237
|
_html(f' {"━" * 50}')
|
|
232
238
|
_html(f' {B}Configuration Summary{EB}')
|
|
@@ -244,7 +250,7 @@ def _show_summary(provider: dict, model_id: str, api_key: Optional[str], base_ur
|
|
|
244
250
|
_html("")
|
|
245
251
|
|
|
246
252
|
try:
|
|
247
|
-
confirm =
|
|
253
|
+
confirm = (await _input_async(" Save this configuration? [Y/n] ")).strip().lower()
|
|
248
254
|
except (EOFError, KeyboardInterrupt):
|
|
249
255
|
return False
|
|
250
256
|
|
|
@@ -340,49 +346,52 @@ async def _run_wizard(is_first_time: bool = True) -> bool:
|
|
|
340
346
|
elif setup_choice == 1:
|
|
341
347
|
# User pastes an existing OpenHack API token from the dashboard.
|
|
342
348
|
existing_key = cfg.get(provider["key_field"])
|
|
343
|
-
api_key = _prompt_api_key(provider, existing_key)
|
|
349
|
+
api_key = await _prompt_api_key(provider, existing_key)
|
|
344
350
|
if not api_key:
|
|
345
351
|
_html("")
|
|
346
352
|
_html(f' {YELLOW}⚠{EYELLOW} An API key is required.')
|
|
347
353
|
_html(f' {DIM}Sign up at: {_esc(settings.openhack_app_url)}/signup{EDIM}')
|
|
348
354
|
_html("")
|
|
349
355
|
else:
|
|
350
|
-
# Custom:
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
break
|
|
361
|
-
|
|
362
|
-
model_idx = await _select_menu_async(
|
|
363
|
-
"Choose a model:",
|
|
364
|
-
model_items,
|
|
365
|
-
default_idx=default_model_idx,
|
|
366
|
-
)
|
|
367
|
-
if model_idx < 0:
|
|
356
|
+
# Custom: base URL, API key, model string.
|
|
357
|
+
_html("")
|
|
358
|
+
_html(f' {B}OpenAI-Compatible API Endpoint{EB}')
|
|
359
|
+
existing_base = cfg.get("openhack_base_url") or default_base_url
|
|
360
|
+
_html(f' {DIM}Current: {_esc(existing_base)}{EDIM}')
|
|
361
|
+
_html(f' {DIM}Press Enter to keep current{EDIM}')
|
|
362
|
+
_html("")
|
|
363
|
+
try:
|
|
364
|
+
url_input = (await _input_async(" Base URL: ")).strip()
|
|
365
|
+
except (EOFError, KeyboardInterrupt):
|
|
368
366
|
_html(f' {DIM}Setup cancelled.{EDIM}')
|
|
369
367
|
_html("")
|
|
370
368
|
return False
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
base_url = _prompt_base_url(default_base_url)
|
|
369
|
+
base_url = url_input if url_input else existing_base
|
|
374
370
|
|
|
375
371
|
existing_key = cfg.get(provider["key_field"])
|
|
376
|
-
api_key = _prompt_api_key(provider, existing_key)
|
|
372
|
+
api_key = await _prompt_api_key(provider, existing_key)
|
|
377
373
|
if not api_key:
|
|
378
374
|
_html("")
|
|
379
375
|
_html(f' {YELLOW}⚠{EYELLOW} An API key is required.')
|
|
380
|
-
_html(f' {DIM}Sign up at: {_esc(settings.openhack_app_url)}/signup{EDIM}')
|
|
381
376
|
_html("")
|
|
382
377
|
|
|
378
|
+
_html("")
|
|
379
|
+
_html(f' {B}Model{EB}')
|
|
380
|
+
existing_model = cfg.get("model") or cfg.get("openhack_model_id") or default_model
|
|
381
|
+
_html(f' {DIM}Current: {_esc(existing_model)}{EDIM}')
|
|
382
|
+
_html(f' {DIM}Press Enter to keep current{EDIM}')
|
|
383
|
+
_html("")
|
|
384
|
+
try:
|
|
385
|
+
model_input = (await _input_async(" Model: ")).strip()
|
|
386
|
+
except (EOFError, KeyboardInterrupt):
|
|
387
|
+
_html(f' {DIM}Setup cancelled.{EDIM}')
|
|
388
|
+
_html("")
|
|
389
|
+
return False
|
|
390
|
+
model_id = model_input if model_input else existing_model
|
|
391
|
+
|
|
383
392
|
# ── Step 3: Summary & confirm ─────────────────────────────────
|
|
384
393
|
org_name = login_result.org_name if login_result else None
|
|
385
|
-
if not _show_summary(provider, model_id, api_key, base_url, org_name):
|
|
394
|
+
if not await _show_summary(provider, model_id, api_key, base_url, org_name):
|
|
386
395
|
_html(f' {DIM}Setup cancelled. No changes saved.{EDIM}')
|
|
387
396
|
_html("")
|
|
388
397
|
return False
|
|
@@ -899,14 +899,36 @@ class OpenHackApp:
|
|
|
899
899
|
self.last_status_line = "cancelled"
|
|
900
900
|
self._invalidate()
|
|
901
901
|
|
|
902
|
-
|
|
902
|
+
def _completion_open() -> bool:
|
|
903
|
+
return self.input_buffer.complete_state is not None
|
|
904
|
+
|
|
905
|
+
@kb.add("escape", eager=True, filter=Condition(_completion_open))
|
|
906
|
+
def _escape_completion(event):
|
|
907
|
+
event.current_buffer.cancel_completion()
|
|
908
|
+
|
|
909
|
+
@kb.add("escape", eager=False, filter=~Condition(_completion_open))
|
|
903
910
|
def _escape(event):
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
911
|
+
pass
|
|
912
|
+
|
|
913
|
+
# Option+Shift+Left/Right — select word (macOS sends Escape + ShiftLeft/Right)
|
|
914
|
+
@kb.add("escape", "s-left")
|
|
915
|
+
def _select_word_left(event):
|
|
916
|
+
buf = event.current_buffer
|
|
917
|
+
pos = buf.document.find_previous_word_beginning() or 0
|
|
918
|
+
buf.cursor_position += pos
|
|
919
|
+
buf.start_selection()
|
|
920
|
+
# Already moved — selection is from new pos to old pos
|
|
921
|
+
# Re-do: move back, start selection, then move
|
|
922
|
+
buf.cursor_position -= pos
|
|
923
|
+
buf.start_selection()
|
|
924
|
+
buf.cursor_position += pos
|
|
925
|
+
|
|
926
|
+
@kb.add("escape", "s-right")
|
|
927
|
+
def _select_word_right(event):
|
|
907
928
|
buf = event.current_buffer
|
|
908
|
-
|
|
909
|
-
|
|
929
|
+
pos = buf.document.find_next_word_ending() or 0
|
|
930
|
+
buf.start_selection()
|
|
931
|
+
buf.cursor_position += pos
|
|
910
932
|
|
|
911
933
|
# Tab navigation — only in scanning/viewing modes, and only when the
|
|
912
934
|
# input is empty so they don't conflict with typing.
|
|
@@ -2587,7 +2609,7 @@ class OpenHackApp:
|
|
|
2587
2609
|
parts = arg.strip().split(None, 1)
|
|
2588
2610
|
key = parts[0].lower()
|
|
2589
2611
|
value = parts[1] if len(parts) > 1 else ""
|
|
2590
|
-
valid = {"provider", "model", "openhack_api_key", "openhack_model_id"}
|
|
2612
|
+
valid = {"provider", "model", "openhack_api_key", "openhack_model_id", "openhack_base_url", "prompt_caching"}
|
|
2591
2613
|
if key not in valid:
|
|
2592
2614
|
self.last_status_line = f"unknown config key: {key}"
|
|
2593
2615
|
return
|
|
@@ -2603,6 +2625,7 @@ class OpenHackApp:
|
|
|
2603
2625
|
save_user_config({"provider": self.provider})
|
|
2604
2626
|
elif key == "model":
|
|
2605
2627
|
self.model = value
|
|
2628
|
+
reload_settings()
|
|
2606
2629
|
self.last_status_line = f"saved {key}"
|
|
2607
2630
|
|
|
2608
2631
|
# ── Setup / login (delegate to setup.py / auth.py) ────────────
|
|
@@ -3130,14 +3153,22 @@ class OpenHackApp:
|
|
|
3130
3153
|
)
|
|
3131
3154
|
|
|
3132
3155
|
except asyncio.CancelledError:
|
|
3133
|
-
self.last_status_line = "scan cancelled"
|
|
3134
3156
|
if session is not None:
|
|
3135
3157
|
self._write_report(session, target_dir, status="cancelled")
|
|
3158
|
+
self.last_status_line = (
|
|
3159
|
+
f"scan cancelled · resume with: openhack resume {session.id}"
|
|
3160
|
+
)
|
|
3161
|
+
else:
|
|
3162
|
+
self.last_status_line = "scan cancelled"
|
|
3136
3163
|
raise
|
|
3137
3164
|
except Exception as exc:
|
|
3138
|
-
self.last_status_line = f"scan failed: {exc}"
|
|
3139
3165
|
if session is not None:
|
|
3140
3166
|
self._write_report(session, target_dir, status="failed")
|
|
3167
|
+
self.last_status_line = (
|
|
3168
|
+
f"scan failed: {exc} · retry with: openhack resume {session.id}"
|
|
3169
|
+
)
|
|
3170
|
+
else:
|
|
3171
|
+
self.last_status_line = f"scan failed: {exc}"
|
|
3141
3172
|
finally:
|
|
3142
3173
|
if self.scan is not None:
|
|
3143
3174
|
self.scan.finish()
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "openhack"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
description = "AI-powered security scanner for your codebase. Find SQL injection, XSS, IDOR, auth bypass, and more — straight from your terminal."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
7
|
-
license = "
|
|
7
|
+
license = "MIT"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name = "OpenHack" },
|
|
10
10
|
]
|
|
@@ -71,5 +71,6 @@ dev-dependencies = [
|
|
|
71
71
|
"pytest>=8.3.0",
|
|
72
72
|
"pytest-asyncio>=0.24.0",
|
|
73
73
|
"pytest-cov>=7.1.0",
|
|
74
|
+
"twine>=6.2.0",
|
|
74
75
|
]
|
|
75
76
|
|