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/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."""
@@ -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
+ )