mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-28 14:47:22 -07:00
feat: add rule-chain builder for non-interactive mode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
9459c21b14
commit
d2e45afb4a
@@ -0,0 +1,38 @@
|
||||
"""Non-interactive (scripted) attack entry points for hate_crack.
|
||||
|
||||
These helpers translate parsed argparse namespaces into calls against the
|
||||
existing ``hcat*`` attack functions on the main module (passed in as ``ctx``,
|
||||
the same pattern ``attacks.py`` uses). See
|
||||
``docs/superpowers/specs/2026-07-24-cli-noninteractive-design.md``.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
|
||||
def build_rule_chains(ctx: Any, rule_tokens):
|
||||
"""Convert CLI ``--rules`` tokens into hashcat ``-r`` chain strings.
|
||||
|
||||
Each token becomes one attack pass. A token may chain multiple rule files
|
||||
with ``+`` (mirroring the interactive rule selector). Filenames resolve
|
||||
against ``ctx.rulesDirectory``. Returns ``[""]`` when no rules are given
|
||||
(equivalent to the interactive "run without rules" choice).
|
||||
|
||||
Raises ``FileNotFoundError`` (with the offending filename as its argument)
|
||||
if any named rule file is missing.
|
||||
"""
|
||||
if not rule_tokens:
|
||||
return [""]
|
||||
chains = []
|
||||
for token in rule_tokens:
|
||||
chain = ""
|
||||
for name in token.split("+"):
|
||||
name = name.strip()
|
||||
if not name:
|
||||
continue
|
||||
path = os.path.join(ctx.rulesDirectory, name)
|
||||
if not os.path.isfile(path):
|
||||
raise FileNotFoundError(name)
|
||||
chain = f"{chain} -r {path}".strip()
|
||||
chains.append(chain)
|
||||
return chains
|
||||
@@ -0,0 +1,47 @@
|
||||
import os
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from hate_crack import noninteractive as ni
|
||||
|
||||
|
||||
def _ctx_with_rules(tmp_path, *rule_names):
|
||||
rules_dir = tmp_path / "rules"
|
||||
rules_dir.mkdir()
|
||||
for name in rule_names:
|
||||
(rules_dir / name).write_text(":\n")
|
||||
return SimpleNamespace(rulesDirectory=str(rules_dir))
|
||||
|
||||
|
||||
def test_build_rule_chains_no_rules_returns_empty_chain(tmp_path):
|
||||
ctx = _ctx_with_rules(tmp_path)
|
||||
assert ni.build_rule_chains(ctx, []) == [""]
|
||||
assert ni.build_rule_chains(ctx, None) == [""]
|
||||
|
||||
|
||||
def test_build_rule_chains_single_rule(tmp_path):
|
||||
ctx = _ctx_with_rules(tmp_path, "best64.rule")
|
||||
chains = ni.build_rule_chains(ctx, ["best64.rule"])
|
||||
expected = os.path.join(ctx.rulesDirectory, "best64.rule")
|
||||
assert chains == [f"-r {expected}"]
|
||||
|
||||
|
||||
def test_build_rule_chains_chained_token(tmp_path):
|
||||
ctx = _ctx_with_rules(tmp_path, "best64.rule", "d3ad0ne.rule")
|
||||
chains = ni.build_rule_chains(ctx, ["best64.rule+d3ad0ne.rule"])
|
||||
a = os.path.join(ctx.rulesDirectory, "best64.rule")
|
||||
b = os.path.join(ctx.rulesDirectory, "d3ad0ne.rule")
|
||||
assert chains == [f"-r {a} -r {b}"]
|
||||
|
||||
|
||||
def test_build_rule_chains_multiple_tokens_are_separate_passes(tmp_path):
|
||||
ctx = _ctx_with_rules(tmp_path, "best64.rule", "d3ad0ne.rule")
|
||||
chains = ni.build_rule_chains(ctx, ["best64.rule", "d3ad0ne.rule"])
|
||||
assert len(chains) == 2
|
||||
|
||||
|
||||
def test_build_rule_chains_missing_file_raises(tmp_path):
|
||||
ctx = _ctx_with_rules(tmp_path)
|
||||
with pytest.raises(FileNotFoundError):
|
||||
ni.build_rule_chains(ctx, ["nope.rule"])
|
||||
Reference in New Issue
Block a user