pawpy-cli 1.0.0__py3-none-any.whl
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.
- pawpy/__init__.py +8 -0
- pawpy/__main__.py +6 -0
- pawpy/api/__init__.py +1 -0
- pawpy/api/dashboard.py +183 -0
- pawpy/api/rest.py +143 -0
- pawpy/cli.py +341 -0
- pawpy/config.py +60 -0
- pawpy/data/__init__.py +6 -0
- pawpy/data/common_passwords.py +139 -0
- pawpy/data/updater.py +49 -0
- pawpy/filters/__init__.py +1 -0
- pawpy/filters/policy.py +59 -0
- pawpy/generator/__init__.py +5 -0
- pawpy/generator/core.py +314 -0
- pawpy/generator/gpu.py +64 -0
- pawpy/generator/hybrid.py +99 -0
- pawpy/generator/sorter.py +136 -0
- pawpy/mutations/__init__.py +20 -0
- pawpy/mutations/dates.py +72 -0
- pawpy/mutations/keyboard.py +99 -0
- pawpy/mutations/leet.py +65 -0
- pawpy/mutations/mangle.py +238 -0
- pawpy/mutations/markov.py +125 -0
- pawpy/mutations/templates.py +131 -0
- pawpy/profile/__init__.py +5 -0
- pawpy/profile/base.py +161 -0
- pawpy/profile/multi.py +93 -0
- pawpy/profile/plugins/__init__.py +55 -0
- pawpy/profile/plugins/example.py +22 -0
- pawpy/scoring/__init__.py +1 -0
- pawpy/scoring/scorer.py +66 -0
- pawpy/utils.py +136 -0
- pawpy_cli-1.0.0.dist-info/METADATA +712 -0
- pawpy_cli-1.0.0.dist-info/RECORD +38 -0
- pawpy_cli-1.0.0.dist-info/WHEEL +5 -0
- pawpy_cli-1.0.0.dist-info/entry_points.txt +2 -0
- pawpy_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- pawpy_cli-1.0.0.dist-info/top_level.txt +1 -0
pawpy/profile/multi.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Multi-target profile handling with cross-referencing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Dict, List
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_multi_profiles(path: str) -> List[Dict[str, Any]]:
|
|
14
|
+
"""Load a JSON array of profiles from *path*."""
|
|
15
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
16
|
+
data = json.load(fh)
|
|
17
|
+
if not isinstance(data, list):
|
|
18
|
+
raise ValueError(
|
|
19
|
+
f"Expected a JSON array of profiles in {path}, "
|
|
20
|
+
f"but got {type(data).__name__}. "
|
|
21
|
+
"Use -j / --import-json for a single profile."
|
|
22
|
+
)
|
|
23
|
+
return data
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def merge_profiles(profiles: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
27
|
+
"""Merge multiple profiles into a single unified profile."""
|
|
28
|
+
merged: Dict[str, Any] = {
|
|
29
|
+
"firstname": [],
|
|
30
|
+
"lastname": [],
|
|
31
|
+
"nickname": [],
|
|
32
|
+
"birthdate": [],
|
|
33
|
+
"partner": [],
|
|
34
|
+
"partner_nick": [],
|
|
35
|
+
"partner_bdate": [],
|
|
36
|
+
"pet": [],
|
|
37
|
+
"company": [],
|
|
38
|
+
"hometown": [],
|
|
39
|
+
"favourite_color": [],
|
|
40
|
+
"children": [],
|
|
41
|
+
"keywords": [],
|
|
42
|
+
}
|
|
43
|
+
list_fields = {"children", "keywords"}
|
|
44
|
+
|
|
45
|
+
for profile in profiles:
|
|
46
|
+
for key in merged:
|
|
47
|
+
value = profile.get(key, "")
|
|
48
|
+
if key in list_fields:
|
|
49
|
+
if isinstance(value, list):
|
|
50
|
+
merged[key].extend(v for v in value if v)
|
|
51
|
+
elif key in ("birthdate", "partner_bdate"):
|
|
52
|
+
if isinstance(value, str) and value.strip():
|
|
53
|
+
merged[key].append(value.strip())
|
|
54
|
+
else:
|
|
55
|
+
if isinstance(value, str) and value.strip():
|
|
56
|
+
merged[key].append(value.strip())
|
|
57
|
+
|
|
58
|
+
for key in merged:
|
|
59
|
+
seen = set()
|
|
60
|
+
unique = []
|
|
61
|
+
for item in merged[key]:
|
|
62
|
+
if item not in seen:
|
|
63
|
+
seen.add(item)
|
|
64
|
+
unique.append(item)
|
|
65
|
+
merged[key] = unique
|
|
66
|
+
|
|
67
|
+
console.print(
|
|
68
|
+
f"[green]✓[/green] Merged {len(profiles)} profiles into unified cross-referenced profile."
|
|
69
|
+
)
|
|
70
|
+
return merged
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def extract_merged_base_words(merged: Dict[str, Any]) -> List[str]:
|
|
74
|
+
"""Flatten all fields from a merged profile into a deduplicated word list."""
|
|
75
|
+
words: set = set()
|
|
76
|
+
for key, value in merged.items():
|
|
77
|
+
if isinstance(value, list):
|
|
78
|
+
for item in value:
|
|
79
|
+
if isinstance(item, str) and item.strip():
|
|
80
|
+
words.add(item.strip().lower())
|
|
81
|
+
elif isinstance(value, str) and value.strip():
|
|
82
|
+
words.add(value.strip().lower())
|
|
83
|
+
return sorted(words)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def extract_merged_dates(merged: Dict[str, Any]) -> List[str]:
|
|
87
|
+
"""Extract all date strings from a merged profile."""
|
|
88
|
+
dates: set = set()
|
|
89
|
+
for field in ("birthdate", "partner_bdate"):
|
|
90
|
+
for d in merged.get(field, []):
|
|
91
|
+
if isinstance(d, str) and d.strip():
|
|
92
|
+
dates.add(d.strip())
|
|
93
|
+
return sorted(dates)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""OSINT plugin system for Pawpy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Callable, Dict, List
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("pawpy.plugins")
|
|
11
|
+
PluginFunc = Callable[[Dict[str, Any]], Dict[str, Any]]
|
|
12
|
+
_plugins: List[PluginFunc] = []
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _discover_plugins(plugin_dir=None) -> None:
|
|
16
|
+
if plugin_dir is None:
|
|
17
|
+
plugin_dir = Path(__file__).parent
|
|
18
|
+
else:
|
|
19
|
+
plugin_dir = Path(plugin_dir)
|
|
20
|
+
if not plugin_dir.is_dir():
|
|
21
|
+
return
|
|
22
|
+
for py_file in sorted(plugin_dir.glob("*.py")):
|
|
23
|
+
if py_file.name.startswith("__"):
|
|
24
|
+
continue
|
|
25
|
+
try:
|
|
26
|
+
spec = importlib.util.spec_from_file_location(
|
|
27
|
+
f"pawpy.plugins.{py_file.stem}", py_file
|
|
28
|
+
)
|
|
29
|
+
if spec is None or spec.loader is None:
|
|
30
|
+
continue
|
|
31
|
+
module = importlib.util.module_from_spec(spec)
|
|
32
|
+
spec.loader.exec_module(module)
|
|
33
|
+
func = getattr(module, "collect", None)
|
|
34
|
+
if callable(func):
|
|
35
|
+
_plugins.append(func)
|
|
36
|
+
logger.info("Loaded OSINT plugin: %s", py_file.stem)
|
|
37
|
+
except Exception:
|
|
38
|
+
logger.exception("Failed to load plugin: %s", py_file.stem)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def run_plugins(profile: Dict[str, Any]) -> Dict[str, Any]:
|
|
42
|
+
if not _plugins:
|
|
43
|
+
_discover_plugins()
|
|
44
|
+
for plugin_func in _plugins:
|
|
45
|
+
try:
|
|
46
|
+
profile = plugin_func(profile)
|
|
47
|
+
except Exception:
|
|
48
|
+
logger.exception("Plugin %s raised an exception", plugin_func.__name__)
|
|
49
|
+
return profile
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def list_plugins() -> List[str]:
|
|
53
|
+
if not _plugins:
|
|
54
|
+
_discover_plugins()
|
|
55
|
+
return [fn.__module__ for fn in _plugins]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Example OSINT plugin – a template for plugin authors.
|
|
2
|
+
|
|
3
|
+
This plugin demonstrates the expected interface. Replace the body of
|
|
4
|
+
``collect`` with real OSINT logic. Always respect rate-limiting,
|
|
5
|
+
terms of service, and privacy laws. Only use OSINT data for authorised
|
|
6
|
+
security testing.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Dict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def collect(profile: Dict[str, Any]) -> Dict[str, Any]:
|
|
15
|
+
"""Enrich *profile* with OSINT-derived data (example no-op)."""
|
|
16
|
+
if profile.get("firstname"):
|
|
17
|
+
keywords = profile.get("keywords", [])
|
|
18
|
+
if isinstance(keywords, str):
|
|
19
|
+
keywords = [keywords]
|
|
20
|
+
keywords.append("osint_enriched")
|
|
21
|
+
profile["keywords"] = keywords
|
|
22
|
+
return profile
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Password strength scoring subsystem."""
|
pawpy/scoring/scorer.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""zxcvbn-based password strength scoring and pruning."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("pawpy.scorer")
|
|
9
|
+
|
|
10
|
+
_zxcvbn_available = False
|
|
11
|
+
try:
|
|
12
|
+
from zxcvbn import zxcvbn
|
|
13
|
+
|
|
14
|
+
_zxcvbn_available = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
zxcvbn = None # type: ignore[assignment, misc]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_zxcvbn_available() -> bool:
|
|
20
|
+
"""Check whether zxcvbn library is installed."""
|
|
21
|
+
return _zxcvbn_available
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def score_password(password: str) -> Optional[int]:
|
|
25
|
+
"""Score a password using zxcvbn.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
An integer score 0-4, or None if zxcvbn is not available.
|
|
29
|
+
0 = very weak, 4 = very strong.
|
|
30
|
+
"""
|
|
31
|
+
if not _zxcvbn_available:
|
|
32
|
+
return None
|
|
33
|
+
try:
|
|
34
|
+
result = zxcvbn(password)
|
|
35
|
+
return result.get("score", 0)
|
|
36
|
+
except Exception:
|
|
37
|
+
logger.debug("zxcvbn failed on: %s", password[:20])
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def score_and_prune(candidates: List[str], min_score: int) -> List[str]:
|
|
42
|
+
"""Filter candidates to those with zxcvbn score >= *min_score*.
|
|
43
|
+
|
|
44
|
+
If zxcvbn is not available, returns all candidates unchanged.
|
|
45
|
+
"""
|
|
46
|
+
if not _zxcvbn_available:
|
|
47
|
+
logger.warning("zxcvbn not installed – skipping strength scoring.")
|
|
48
|
+
return candidates
|
|
49
|
+
|
|
50
|
+
min_score = max(0, min(min_score, 4))
|
|
51
|
+
kept = []
|
|
52
|
+
pruned = 0
|
|
53
|
+
for candidate in candidates:
|
|
54
|
+
score = score_password(candidate)
|
|
55
|
+
if score is not None and score < min_score:
|
|
56
|
+
pruned += 1
|
|
57
|
+
continue
|
|
58
|
+
kept.append(candidate)
|
|
59
|
+
|
|
60
|
+
logger.info(
|
|
61
|
+
"zxcvbn scoring: kept %d, pruned %d (min_score=%d)",
|
|
62
|
+
len(kept),
|
|
63
|
+
pruned,
|
|
64
|
+
min_score,
|
|
65
|
+
)
|
|
66
|
+
return kept
|
pawpy/utils.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Shared utilities: banner, dedup writer, logging helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from typing import Generator, Iterable, Set
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.logging import RichHandler
|
|
11
|
+
from rich.progress import (
|
|
12
|
+
BarColumn,
|
|
13
|
+
Progress,
|
|
14
|
+
SpinnerColumn,
|
|
15
|
+
TaskProgressColumn,
|
|
16
|
+
TextColumn,
|
|
17
|
+
TimeRemainingColumn,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Console & logging
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def setup_logging(verbose: bool = False) -> logging.Logger:
|
|
28
|
+
"""Create and return the pawpy logger."""
|
|
29
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
30
|
+
logging.basicConfig(
|
|
31
|
+
level=level,
|
|
32
|
+
format="%(message)s",
|
|
33
|
+
datefmt="[%X]",
|
|
34
|
+
handlers=[RichHandler(console=console, rich_tracebacks=True)],
|
|
35
|
+
)
|
|
36
|
+
return logging.getLogger("pawpy")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# ASCII Banner
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
BANNER_TEXT = r"""
|
|
44
|
+
██████╗ █████╗ ██╗ ██╗██████╗ ██╗ ██╗
|
|
45
|
+
██╔══██╗██╔══██╗██║ ██║██╔══██╗╚██╗ ██╔╝
|
|
46
|
+
██████╔╝███████║██║ █╗ ██║██████╔╝ ╚████╔╝
|
|
47
|
+
██╔═══╝ ██╔══██║██║███╗██║██╔═══╝ ╚██╔╝
|
|
48
|
+
██║ ██║ ██║╚███╔███╔╝██║ ██║
|
|
49
|
+
╚═╝ ╚═╝ ╚═╝ ╚══╝╚══╝ ╚═╝ ╚═╝
|
|
50
|
+
The Most Powerful Wordlist Generator
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
ETHICAL_WARNING = (
|
|
54
|
+
"[bold red]⚠ ETHICAL USE ONLY[/bold red]\n"
|
|
55
|
+
"This tool is intended for [bold]authorised security testing and educational purposes only[/bold].\n"
|
|
56
|
+
"Unauthorised use against systems or accounts you do not own or have explicit\n"
|
|
57
|
+
"permission to test is [bold red]illegal and unethical[/bold red].\n"
|
|
58
|
+
"By proceeding, you confirm you have proper authorisation.\n"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def print_banner() -> None:
|
|
63
|
+
"""Display the Pawpy ASCII banner and ethical-use warning."""
|
|
64
|
+
console.print(BANNER_TEXT, style="bold cyan")
|
|
65
|
+
console.print(
|
|
66
|
+
f" v{__import__('pawpy').__version__} | The Most Powerful Educational Wordlist Generator",
|
|
67
|
+
style="dim",
|
|
68
|
+
)
|
|
69
|
+
console.print()
|
|
70
|
+
console.print(ETHICAL_WARNING)
|
|
71
|
+
console.print()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Dedup & streaming helpers
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def dedup_stream(
|
|
80
|
+
lines: Iterable[str],
|
|
81
|
+
output_path: str,
|
|
82
|
+
) -> int:
|
|
83
|
+
"""Deduplicate an iterable of strings and write sorted output to *output_path*.
|
|
84
|
+
|
|
85
|
+
Returns the number of unique lines written.
|
|
86
|
+
"""
|
|
87
|
+
seen: Set[str] = set()
|
|
88
|
+
count = 0
|
|
89
|
+
tmp = output_path + ".tmp"
|
|
90
|
+
with open(tmp, "w", encoding="utf-8", errors="ignore") as fh:
|
|
91
|
+
for line in lines:
|
|
92
|
+
line = line.rstrip("\n\r")
|
|
93
|
+
if line and line not in seen:
|
|
94
|
+
seen.add(line)
|
|
95
|
+
fh.write(line + "\n")
|
|
96
|
+
count += 1
|
|
97
|
+
os.replace(tmp, output_path)
|
|
98
|
+
return count
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def iter_file(path: str) -> Generator[str, None, None]:
|
|
102
|
+
"""Yield non-empty lines from a text file."""
|
|
103
|
+
with open(path, "r", encoding="utf-8", errors="ignore") as fh:
|
|
104
|
+
for line in fh:
|
|
105
|
+
stripped = line.rstrip("\n\r")
|
|
106
|
+
if stripped:
|
|
107
|
+
yield stripped
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def make_progress() -> Progress:
|
|
111
|
+
"""Create a standard Rich progress bar."""
|
|
112
|
+
return Progress(
|
|
113
|
+
SpinnerColumn(),
|
|
114
|
+
TextColumn("[progress.description]{task.description}"),
|
|
115
|
+
BarColumn(),
|
|
116
|
+
TaskProgressColumn(),
|
|
117
|
+
TimeRemainingColumn(),
|
|
118
|
+
console=console,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# Confirmation prompt
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def confirm_ethical_use() -> bool:
|
|
128
|
+
"""Prompt the user to confirm ethical use. Returns True on 'y'."""
|
|
129
|
+
return (
|
|
130
|
+
console.input(
|
|
131
|
+
"[bold yellow]Do you confirm you have authorisation? {y/N}: [/bold yellow]"
|
|
132
|
+
)
|
|
133
|
+
.strip()
|
|
134
|
+
.lower()
|
|
135
|
+
== "y"
|
|
136
|
+
)
|