mirror of
https://github.com/Z4nzu/hackingtool.git
synced 2026-11-04 05:02:29 +00:00
Interactive terminal UI (Textual, default on a TTY) - Category browser with live tool counts; type to search EVERY tool across all categories; up/down move, Enter open, left/Esc back; `/` opens a command palette. - Tool screen gates Run/Install/Uninstall behind a confirm, dropping to a real TTY so a tool can't crash the UI; Open-repo is http(s)-guarded. - The engine is unchanged; `--classic` keeps the full prompt_toolkit REPL and the headless `--engagement` orchestrator is untouched. Update awareness + self-update helper - Cached-daily GitHub Releases check (offline-safe, opt-out via HACKINGTOOL_NO_UPDATE_CHECK / update_check config); shows an "update available" line on the main screen via a background worker (never blocks first render). - `/update` detects the install method (pipx/pip/apt/docker/source) and shows the exact upgrade command to copy - it never runs it. Native settings + config hygiene - `/update`/`/config` run as in-TUI screens instead of suspending to prompt_toolkit (which raised "event loop already running" nested under Textual). - Sponsor feed URL moved from user config to a fixed code constant (SPONSORS_URL); update_check is now a proper bool toggle. Docker / README - Docker image references aligned to hardikzinzu/hackingtool. - README refreshed for the interactive UI; first screenshot replaced with the new UI. Community-reported bug fixes (verified still present in current code) - Data safety: atomic JSON/YAML writes (#773, #765); malformed config.json no longer crashes load/import (#775); UTF-8 for catalog/settings IO (#766); findings parsers skip non-object/typed records instead of aborting (#768); pipeline decodes invalid bytes and salvages timed-out evidence (#771, #770). - AI layer: reject null/non-text provider responses (#778); auto-mode connection test honors the Ollama fallback (#779); settings stay responsive during the AI probe (#780); one findings snapshot per AI report (#772). - CLI/REPL: `@nmap`/`@sqlmap` resolve canonically instead of fuzzy mis-hits (#767); bare `/` no longer crashes the REPL (#761); bad/incomplete args exit non-zero instead of opening the menu (#763); search/tag result #99 is reachable (#762); background pane creation failures are reported, not shown as "started" (#777); config/API-key save failures surface without closing settings (#776). - Misc: .dockerignore keeps README.md for the build (#781); PortScan prompt markup fixed (#698); uninstall command lines use [warning] not [error] (#666, #663); untrusted-output fence can't be broken out of + script import fix (#757).
83 lines
2.9 KiB
Python
83 lines
2.9 KiB
Python
"""sponsors: trust-filtered committed loader, tier-weighted pick, zero network by default. Never raises."""
|
|
import hackingtool.sponsors as sp
|
|
from hackingtool.sponsors import Sponsor
|
|
|
|
|
|
def _reset():
|
|
sp._featured_memo = None
|
|
|
|
|
|
def test_load_shipped_empty_returns_empty(monkeypatch):
|
|
_reset()
|
|
monkeypatch.setattr(sp, "_sponsors_url", lambda: "") # no remote, exercise shipped file
|
|
assert sp.load() == []
|
|
|
|
|
|
def test_load_filters_untrusted_and_incomplete(monkeypatch, tmp_path):
|
|
y = tmp_path / "sponsors.yaml"
|
|
y.write_text(
|
|
'sponsors:\n'
|
|
' - {name: "Good", url: "https://good.example", tier: "gold"}\n'
|
|
' - {name: "AlsoGood", url: "http://ok.example"}\n'
|
|
' - {name: "JS", url: "javascript:alert(1)"}\n'
|
|
' - {name: "File", url: "file:///etc/passwd"}\n'
|
|
' - {url: "https://noname.example"}\n'
|
|
' - {name: "NoUrl"}\n'
|
|
' - {name: "BadTier", url: "https://bt.example", tier: "platinum"}\n'
|
|
)
|
|
monkeypatch.setattr(sp, "_YAML", y)
|
|
monkeypatch.setattr(sp, "_sponsors_url", lambda: "")
|
|
out = sp.load()
|
|
assert [s.url for s in out] == ["https://good.example", "http://ok.example", "https://bt.example"]
|
|
assert out[0].tier == "gold"
|
|
assert out[2].tier == "bronze" # unknown tier -> default
|
|
|
|
|
|
def test_featured_deterministic_and_from_list(monkeypatch):
|
|
_reset()
|
|
pool = [Sponsor("A", "https://a.example"), Sponsor("B", "https://b.example")]
|
|
monkeypatch.setattr(sp, "load", lambda: pool)
|
|
r1 = sp.featured(1, seed=0)
|
|
_reset()
|
|
r2 = sp.featured(1, seed=0)
|
|
assert len(r1) == 1 and r1[0] in pool
|
|
assert r1[0].url == r2[0].url # deterministic for a fixed seed
|
|
|
|
|
|
def test_featured_empty_list(monkeypatch):
|
|
_reset()
|
|
monkeypatch.setattr(sp, "load", lambda: [])
|
|
assert sp.featured(1, seed=0) == []
|
|
|
|
|
|
def test_tier_weighting_favors_gold(monkeypatch):
|
|
pool = [Sponsor("Gold", "https://g.example", "", "gold")] + [
|
|
Sponsor(f"B{i}", f"https://b{i}.example", "", "bronze") for i in range(9)
|
|
]
|
|
monkeypatch.setattr(sp, "load", lambda: pool)
|
|
n = 300
|
|
gold = 0
|
|
for seed in range(n):
|
|
_reset()
|
|
picked = sp.featured(1, seed=seed)
|
|
if picked and picked[0].name == "Gold":
|
|
gold += 1
|
|
# P(gold) = 3/(3+9) = 0.25 weighted vs 0.10 uniform -> weighting is clearly applied.
|
|
assert gold > n * 0.15
|
|
|
|
|
|
def test_no_network_by_default(monkeypatch):
|
|
_reset()
|
|
monkeypatch.setattr(sp, "SPONSORS_URL", "") # fixed-in-code feed is blank by default
|
|
monkeypatch.delenv("HACKINGTOOL_NO_SPONSOR_FETCH", raising=False)
|
|
|
|
def boom(*a, **k):
|
|
raise AssertionError("network must not be touched when SPONSORS_URL is blank")
|
|
|
|
monkeypatch.setattr(sp.urllib.request, "urlopen", boom)
|
|
assert isinstance(sp.load(), list) # no exception, no network
|
|
|
|
|
|
def test_sponsor_cta():
|
|
assert sp.sponsor_cta() == "https://github.com/sponsors/Z4nzu"
|