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.
Files changed (134) hide show
  1. openhack-0.1.2/LICENSE +21 -0
  2. {openhack-0.1.0b1 → openhack-0.1.2}/PKG-INFO +37 -10
  3. {openhack-0.1.0b1 → openhack-0.1.2}/README.md +35 -8
  4. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/__init__.py +1 -1
  5. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/__main__.py +2 -1
  6. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/llm.py +25 -2
  7. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/config.py +9 -0
  8. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/setup.py +41 -32
  9. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/tui.py +40 -9
  10. {openhack-0.1.0b1 → openhack-0.1.2}/pyproject.toml +3 -2
  11. {openhack-0.1.0b1 → openhack-0.1.2}/uv.lock +452 -1
  12. openhack-0.1.0b1/LICENSE +0 -661
  13. {openhack-0.1.0b1 → openhack-0.1.2}/.env.example +0 -0
  14. {openhack-0.1.0b1 → openhack-0.1.2}/.github/workflows/tests.yml +0 -0
  15. {openhack-0.1.0b1 → openhack-0.1.2}/.gitignore +0 -0
  16. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/__init__.py +0 -0
  17. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/base.py +0 -0
  18. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/browser_verifier.py +0 -0
  19. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/browser_verifier_swarm.py +0 -0
  20. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/checkpoint.py +0 -0
  21. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/context_manager.py +0 -0
  22. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/coordinator.py +0 -0
  23. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/endpoint_analyst.py +0 -0
  24. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/feature_hunter.py +0 -0
  25. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/hunter.py +0 -0
  26. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/hunter_swarm.py +0 -0
  27. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/recon.py +0 -0
  28. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/sandbox_verifier.py +0 -0
  29. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/sandbox_verifier_swarm.py +0 -0
  30. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/session.py +0 -0
  31. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/validator.py +0 -0
  32. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/agents/validator_swarm.py +0 -0
  33. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/auth.py +0 -0
  34. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/browser/__init__.py +0 -0
  35. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/browser/runner.py +0 -0
  36. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/categories.py +0 -0
  37. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/deterministic_recon.py +0 -0
  38. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/entry_points.py +0 -0
  39. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/framework_classifier.py +0 -0
  40. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/framework_detection.py +0 -0
  41. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/headless_scan.py +0 -0
  42. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/__init__.py +0 -0
  43. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/browser_verifier.py +0 -0
  44. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/coordinator.py +0 -0
  45. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/django/__init__.py +0 -0
  46. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/django/auth_bypass.py +0 -0
  47. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/django/csrf.py +0 -0
  48. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/django/data_exposure.py +0 -0
  49. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/django/idor.py +0 -0
  50. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/django/injection.py +0 -0
  51. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/django/misconfiguration.py +0 -0
  52. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/django/ssrf.py +0 -0
  53. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/endpoint_analyst.py +0 -0
  54. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/express/__init__.py +0 -0
  55. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/express/auth_bypass.py +0 -0
  56. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/express/data_exposure.py +0 -0
  57. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/express/idor.py +0 -0
  58. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/express/injection.py +0 -0
  59. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/express/misconfiguration.py +0 -0
  60. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/express/ssrf.py +0 -0
  61. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/feature_hunter.py +0 -0
  62. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/flask/__init__.py +0 -0
  63. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/flask/auth_bypass.py +0 -0
  64. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/flask/data_exposure.py +0 -0
  65. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/flask/idor.py +0 -0
  66. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/flask/injection.py +0 -0
  67. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/flask/misconfiguration.py +0 -0
  68. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/flask/ssrf.py +0 -0
  69. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/hunter.py +0 -0
  70. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/hunter_continuation_loop.py +0 -0
  71. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/hunter_continuation_no_findings.py +0 -0
  72. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/hunter_continuation_no_progress.py +0 -0
  73. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/hunter_tool_instructions.py +0 -0
  74. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/__init__.py +0 -0
  75. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/auth_bypass.py +0 -0
  76. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/csrf.py +0 -0
  77. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/data_exposure.py +0 -0
  78. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/idor.py +0 -0
  79. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/injection.py +0 -0
  80. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/middleware_bypass.py +0 -0
  81. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/misconfiguration.py +0 -0
  82. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/server_actions.py +0 -0
  83. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/ssrf.py +0 -0
  84. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/nextjs/xss.py +0 -0
  85. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/pr_analysis_system.py +0 -0
  86. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/pr_analysis_user.py +0 -0
  87. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/project_context.py +0 -0
  88. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/recon.py +0 -0
  89. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/reporter.py +0 -0
  90. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/researchers.py +0 -0
  91. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/sandbox_verifier.py +0 -0
  92. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/supabase/__init__.py +0 -0
  93. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/supabase/auth_tokens.py +0 -0
  94. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/supabase/edge_functions.py +0 -0
  95. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/supabase/graphql.py +0 -0
  96. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/supabase/postgrest.py +0 -0
  97. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/supabase/realtime.py +0 -0
  98. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/supabase/rls.py +0 -0
  99. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/supabase/rpc_functions.py +0 -0
  100. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/supabase/storage.py +0 -0
  101. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/supabase/tenant_isolation.py +0 -0
  102. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/validator.py +0 -0
  103. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/validator_continuation_incomplete.py +0 -0
  104. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/prompts/validator_tool_instructions.py +0 -0
  105. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/quality.py +0 -0
  106. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/sandbox/__init__.py +0 -0
  107. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/sandbox/orchestrator.py +0 -0
  108. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/sandbox/runner.py +0 -0
  109. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/scan_session.py +0 -0
  110. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/static_validator.py +0 -0
  111. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/tools/__init__.py +0 -0
  112. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/tools/ast_tools.py +0 -0
  113. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/tools/coverage.py +0 -0
  114. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/tools/filesystem.py +0 -0
  115. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/tools/nextjs.py +0 -0
  116. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/tools/registry.py +0 -0
  117. {openhack-0.1.0b1 → openhack-0.1.2}/openhack/updates.py +0 -0
  118. {openhack-0.1.0b1 → openhack-0.1.2}/scripts/run_browser_verify.py +0 -0
  119. {openhack-0.1.0b1 → openhack-0.1.2}/scripts/run_browser_verify_live.py +0 -0
  120. {openhack-0.1.0b1 → openhack-0.1.2}/scripts/run_feature_hunt.py +0 -0
  121. {openhack-0.1.0b1 → openhack-0.1.2}/scripts/run_sandbox_verify.py +0 -0
  122. {openhack-0.1.0b1 → openhack-0.1.2}/scripts/run_sandbox_verify_live.py +0 -0
  123. {openhack-0.1.0b1 → openhack-0.1.2}/tests/__init__.py +0 -0
  124. {openhack-0.1.0b1 → openhack-0.1.2}/tests/conftest.py +0 -0
  125. {openhack-0.1.0b1 → openhack-0.1.2}/tests/test_categories.py +0 -0
  126. {openhack-0.1.0b1 → openhack-0.1.2}/tests/test_checkpoint.py +0 -0
  127. {openhack-0.1.0b1 → openhack-0.1.2}/tests/test_config.py +0 -0
  128. {openhack-0.1.0b1 → openhack-0.1.2}/tests/test_coverage.py +0 -0
  129. {openhack-0.1.0b1 → openhack-0.1.2}/tests/test_deterministic_recon.py +0 -0
  130. {openhack-0.1.0b1 → openhack-0.1.2}/tests/test_entry_points.py +0 -0
  131. {openhack-0.1.0b1 → openhack-0.1.2}/tests/test_filesystem_tools.py +0 -0
  132. {openhack-0.1.0b1 → openhack-0.1.2}/tests/test_framework_classifier.py +0 -0
  133. {openhack-0.1.0b1 → openhack-0.1.2}/tests/test_quality.py +0 -0
  134. {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.0b1
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: AGPL-3.0-only
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. OpenHack does recon -> hunting -> validation -> verification all in one pipeline to find high quality verified vulnerabilities. OpenHack exclusively uses open source models and specializes in web app vulnerabilities.
46
+ Like Claude Code Security / Codex Security but open source and **exclusively uses open source models**.
47
47
 
48
- ## Install
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
+ &nbsp;
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
+ &nbsp;
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
+ &nbsp;
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 | Effect |
174
- | ------------------ | ------------------------------------------------------------------------------------------------ |
175
- | `OPENHACK_API_KEY` | Bearer token for the OpenHack inference API |
176
- | `OPENHACK_DEV=1` | Point the CLI at local dev servers (app on `:9080`, inference on `:8787`) for self-hosted setups |
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
- AGPL-3.0 — see [LICENSE](LICENSE). Free for personal, educational, and open-source use. For commercial licensing without AGPL obligations, contact [team@openhack.com](mailto:team@openhack.com).
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. OpenHack does recon -> hunting -> validation -> verification all in one pipeline to find high quality verified vulnerabilities. OpenHack exclusively uses open source models and specializes in web app vulnerabilities.
5
+ Like Claude Code Security / Codex Security but open source and **exclusively uses open source models**.
6
6
 
7
- ## Install
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
+ &nbsp;
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
+ &nbsp;
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
+ &nbsp;
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 | Effect |
133
- | ------------------ | ------------------------------------------------------------------------------------------------ |
134
- | `OPENHACK_API_KEY` | Bearer token for the OpenHack inference API |
135
- | `OPENHACK_DEV=1` | Point the CLI at local dev servers (app on `:9080`, inference on `:8787`) for self-hosted setups |
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
- AGPL-3.0 — see [LICENSE](LICENSE). Free for personal, educational, and open-source use. For commercial licensing without AGPL obligations, contact [team@openhack.com](mailto:team@openhack.com).
175
+ MIT — see [LICENSE](LICENSE).
@@ -1,2 +1,2 @@
1
1
  """OpenHack Agent - Interactive TUI for vulnerability scanning."""
2
- __version__ = "0.1.0-beta"
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 or use /setup inside the TUI.\n")
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
- return await self._chat(messages, tools, system, tool_choice=tool_choice, on_chunk=on_chunk)
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
- if self.prompt_cache_key:
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 = getpass.getpass(" API Key: ").strip()
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 = input(" Base URL: ").strip()
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 = input(" Save this configuration? [Y/n] ").strip().lower()
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: pick model, base URL, paste key.
351
- model_items = [
352
- (m[0], m[1], m[2])
353
- for m in provider["models"]
354
- ]
355
- current_model = cfg.get("model") or cfg.get("openhack_model_id")
356
- default_model_idx = 0
357
- for i, (mid, _, _) in enumerate(provider["models"]):
358
- if mid == current_model:
359
- default_model_idx = i
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
- model_id = provider["models"][model_idx][0]
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
- @kb.add("escape", eager=True)
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
- # Dismiss the completion dropdown if it's open. Without `eager`
905
- # prompt_toolkit waits ~half a second to see if this is an alt-key
906
- # sequence, which feels laggy.
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
- if buf.complete_state:
909
- buf.cancel_completion()
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.0b1"
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 = "AGPL-3.0-only"
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