mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-28 14:47:22 -07:00
feat: add non-interactive attack dispatcher
Appends `run_noninteractive(ctx, args)` to noninteractive.py, which dispatches quick/dict/brute/topmask commands to the appropriate hcat* function. Returns 0 on success, 1 on bad inputs, 2 on unknown command. Includes 6 new unit tests (12 total in file), all passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
db374edda2
commit
27791457fb
@@ -38,3 +38,51 @@ def build_rule_chains(ctx: Any, rule_tokens: list[str] | None) -> list[str]:
|
||||
raise ValueError(f"Rule token {token!r} resolved to no rule files")
|
||||
chains.append(chain)
|
||||
return chains
|
||||
|
||||
|
||||
def run_noninteractive(ctx: Any, args: Any) -> int:
|
||||
"""Run a non-interactive attack. Returns a process exit code.
|
||||
|
||||
``ctx`` is the main module (live ``hcat*`` functions, ``rulesDirectory``,
|
||||
``resolve_path``, ``hcatHashType``/``hcatHashFile`` already set by the
|
||||
preprocessing block in ``main()``). ``args`` is the parsed subparser
|
||||
namespace whose ``command`` selects the attack.
|
||||
"""
|
||||
command = args.command
|
||||
|
||||
if command == "quick":
|
||||
wordlist = ctx.resolve_path(args.wordlist)
|
||||
if not wordlist or not os.path.isfile(wordlist):
|
||||
print(f"Error: wordlist not found: {args.wordlist}")
|
||||
return 1
|
||||
try:
|
||||
chains = build_rule_chains(ctx, args.rules)
|
||||
except (FileNotFoundError, ValueError) as exc:
|
||||
print(f"Error: invalid --rules value: {exc}")
|
||||
return 1
|
||||
for chain in chains:
|
||||
ctx.hcatQuickDictionary(
|
||||
ctx.hcatHashType,
|
||||
ctx.hcatHashFile,
|
||||
chain,
|
||||
wordlist,
|
||||
attack_name="Quick Crack",
|
||||
)
|
||||
return 0
|
||||
|
||||
if command == "dict":
|
||||
ctx.hcatDictionary(ctx.hcatHashType, ctx.hcatHashFile)
|
||||
return 0
|
||||
|
||||
if command == "brute":
|
||||
ctx.hcatBruteForce(
|
||||
ctx.hcatHashType, ctx.hcatHashFile, args.min_len, args.max_len
|
||||
)
|
||||
return 0
|
||||
|
||||
if command == "topmask":
|
||||
ctx.hcatTopMask(ctx.hcatHashType, ctx.hcatHashFile, args.target_time * 3600)
|
||||
return 0
|
||||
|
||||
print(f"Error: unknown non-interactive command: {command}")
|
||||
return 2
|
||||
|
||||
@@ -53,3 +53,84 @@ def test_build_rule_chains_empty_token_raises(tmp_path):
|
||||
ctx = _ctx_with_rules(tmp_path, "best64.rule")
|
||||
with pytest.raises(ValueError):
|
||||
ni.build_rule_chains(ctx, ["+"])
|
||||
|
||||
|
||||
def _spy_ctx(tmp_path, **overrides):
|
||||
calls = []
|
||||
|
||||
def rec(name):
|
||||
def _fn(*a, **k):
|
||||
calls.append((name, a, k))
|
||||
return _fn
|
||||
|
||||
ctx = SimpleNamespace(
|
||||
calls=calls,
|
||||
hcatHashType="1000",
|
||||
hcatHashFile=str(tmp_path / "hashes.txt"),
|
||||
rulesDirectory=str(tmp_path / "rules"),
|
||||
resolve_path=lambda p: os.path.abspath(os.path.expanduser(p)) if p else None,
|
||||
hcatQuickDictionary=rec("hcatQuickDictionary"),
|
||||
hcatDictionary=rec("hcatDictionary"),
|
||||
hcatBruteForce=rec("hcatBruteForce"),
|
||||
hcatTopMask=rec("hcatTopMask"),
|
||||
)
|
||||
for k, v in overrides.items():
|
||||
setattr(ctx, k, v)
|
||||
return ctx
|
||||
|
||||
|
||||
def test_dispatch_quick_calls_quick_dictionary(tmp_path):
|
||||
(tmp_path / "rules").mkdir()
|
||||
(tmp_path / "rules" / "best64.rule").write_text(":\n")
|
||||
wl = tmp_path / "rockyou.txt"
|
||||
wl.write_text("password\n")
|
||||
ctx = _spy_ctx(tmp_path)
|
||||
args = SimpleNamespace(command="quick", wordlist=str(wl), rules=["best64.rule"])
|
||||
code = ni.run_noninteractive(ctx, args)
|
||||
assert code == 0
|
||||
assert [c[0] for c in ctx.calls] == ["hcatQuickDictionary"]
|
||||
name, a, k = ctx.calls[0]
|
||||
assert a[0] == "1000"
|
||||
assert a[1] == ctx.hcatHashFile
|
||||
assert a[2] == f"-r {os.path.join(ctx.rulesDirectory, 'best64.rule')}"
|
||||
assert a[3] == os.path.abspath(str(wl))
|
||||
|
||||
|
||||
def test_dispatch_quick_missing_wordlist_returns_1(tmp_path):
|
||||
(tmp_path / "rules").mkdir()
|
||||
ctx = _spy_ctx(tmp_path)
|
||||
args = SimpleNamespace(command="quick", wordlist=str(tmp_path / "nope.txt"), rules=[])
|
||||
assert ni.run_noninteractive(ctx, args) == 1
|
||||
assert ctx.calls == []
|
||||
|
||||
|
||||
def test_dispatch_quick_unknown_rule_returns_1(tmp_path):
|
||||
(tmp_path / "rules").mkdir()
|
||||
wl = tmp_path / "rockyou.txt"
|
||||
wl.write_text("password\n")
|
||||
ctx = _spy_ctx(tmp_path)
|
||||
args = SimpleNamespace(command="quick", wordlist=str(wl), rules=["ghost.rule"])
|
||||
assert ni.run_noninteractive(ctx, args) == 1
|
||||
assert ctx.calls == []
|
||||
|
||||
|
||||
def test_dispatch_dict_calls_dictionary(tmp_path):
|
||||
ctx = _spy_ctx(tmp_path)
|
||||
args = SimpleNamespace(command="dict")
|
||||
assert ni.run_noninteractive(ctx, args) == 0
|
||||
assert ctx.calls[0][0] == "hcatDictionary"
|
||||
assert ctx.calls[0][1] == ("1000", ctx.hcatHashFile)
|
||||
|
||||
|
||||
def test_dispatch_brute_calls_bruteforce_with_lengths(tmp_path):
|
||||
ctx = _spy_ctx(tmp_path)
|
||||
args = SimpleNamespace(command="brute", min_len=2, max_len=6)
|
||||
assert ni.run_noninteractive(ctx, args) == 0
|
||||
assert ctx.calls[0] == ("hcatBruteForce", ("1000", ctx.hcatHashFile, 2, 6), {})
|
||||
|
||||
|
||||
def test_dispatch_topmask_converts_hours_to_seconds(tmp_path):
|
||||
ctx = _spy_ctx(tmp_path)
|
||||
args = SimpleNamespace(command="topmask", target_time=4)
|
||||
assert ni.run_noninteractive(ctx, args) == 0
|
||||
assert ctx.calls[0] == ("hcatTopMask", ("1000", ctx.hcatHashFile, 4 * 3600), {})
|
||||
|
||||
Reference in New Issue
Block a user