pawpy-cli 1.0.0b0__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 +145 -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 +135 -0
- pawpy_cli-1.0.0b0.dist-info/METADATA +721 -0
- pawpy_cli-1.0.0b0.dist-info/RECORD +37 -0
- pawpy_cli-1.0.0b0.dist-info/WHEEL +5 -0
- pawpy_cli-1.0.0b0.dist-info/entry_points.txt +2 -0
- pawpy_cli-1.0.0b0.dist-info/top_level.txt +1 -0
pawpy/__init__.py
ADDED
pawpy/__main__.py
ADDED
pawpy/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""API and web dashboard subsystem."""
|
pawpy/api/dashboard.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Web dashboard with WebSocket real-time logging.
|
|
2
|
+
|
|
3
|
+
Provides a simple browser-based interface to trigger wordlist generation
|
|
4
|
+
and watch progress in real time.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Set
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("pawpy.dashboard")
|
|
14
|
+
|
|
15
|
+
_connections: Set = set()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Minimal HTML for the dashboard
|
|
19
|
+
_DASHBOARD_HTML = """<!DOCTYPE html>
|
|
20
|
+
<html lang="en">
|
|
21
|
+
<head>
|
|
22
|
+
<meta charset="UTF-8">
|
|
23
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
24
|
+
<title>Pawpy Dashboard</title>
|
|
25
|
+
<style>
|
|
26
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
27
|
+
body {
|
|
28
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
29
|
+
background: #0d1117; color: #c9d1d9;
|
|
30
|
+
display: flex; min-height: 100vh;
|
|
31
|
+
}
|
|
32
|
+
.sidebar {
|
|
33
|
+
width: 280px; background: #161b22; padding: 20px;
|
|
34
|
+
border-right: 1px solid #30363d;
|
|
35
|
+
}
|
|
36
|
+
.sidebar h1 { color: #58a6ff; font-size: 1.5rem; margin-bottom: 20px; }
|
|
37
|
+
.sidebar label { display: block; color: #8b949e; margin: 12px 0 4px; font-size: 0.85rem; }
|
|
38
|
+
.sidebar input[type="file"], .sidebar input[type="text"],
|
|
39
|
+
.sidebar select { width: 100%; padding: 8px; background: #0d1117;
|
|
40
|
+
border: 1px solid #30363d; border-radius: 6px; color: #c9d1d9; }
|
|
41
|
+
.sidebar button { width: 100%; padding: 10px; margin-top: 16px;
|
|
42
|
+
background: #238636; color: #fff; border: none; border-radius: 6px;
|
|
43
|
+
cursor: pointer; font-size: 1rem; font-weight: bold; }
|
|
44
|
+
.sidebar button:hover { background: #2ea043; }
|
|
45
|
+
.sidebar .checkbox { display: flex; align-items: center; gap: 8px; margin: 6px 0; }
|
|
46
|
+
.sidebar .checkbox input { width: auto; }
|
|
47
|
+
.main { flex: 1; display: flex; flex-direction: column; }
|
|
48
|
+
.header { padding: 16px 20px; background: #161b22;
|
|
49
|
+
border-bottom: 1px solid #30363d; font-size: 1.1rem; }
|
|
50
|
+
.log-area { flex: 1; padding: 16px; overflow-y: auto; font-family: 'Fira Code', monospace;
|
|
51
|
+
font-size: 0.85rem; line-height: 1.6; }
|
|
52
|
+
.log-line { padding: 2px 0; }
|
|
53
|
+
.log-line.info { color: #58a6ff; }
|
|
54
|
+
.log-line.success { color: #3fb950; }
|
|
55
|
+
.log-line.warning { color: #d29922; }
|
|
56
|
+
.log-line.error { color: #f85149; }
|
|
57
|
+
.stats { padding: 12px 20px; background: #161b22;
|
|
58
|
+
border-top: 1px solid #30363d; display: flex; gap: 24px; }
|
|
59
|
+
.stat-item span.label { color: #8b949e; font-size: 0.8rem; }
|
|
60
|
+
.stat-item span.value { color: #58a6ff; font-size: 1.2rem; font-weight: bold; }
|
|
61
|
+
</style>
|
|
62
|
+
</head>
|
|
63
|
+
<body>
|
|
64
|
+
<div class="sidebar">
|
|
65
|
+
<h1>Pawpy</h1>
|
|
66
|
+
<label>Profile JSON</label>
|
|
67
|
+
<input type="file" id="profileFile" accept=".json">
|
|
68
|
+
<label>Output File</label>
|
|
69
|
+
<input type="text" id="outputFile" placeholder="pawpy_wordlist.txt">
|
|
70
|
+
<label>Mode</label>
|
|
71
|
+
<select id="mode">
|
|
72
|
+
<option value="normal">Normal</option>
|
|
73
|
+
<option value="lite">Lite (fast)</option>
|
|
74
|
+
<option value="extreme">Extreme (all mutations)</option>
|
|
75
|
+
</select>
|
|
76
|
+
<div class="checkbox"><input type="checkbox" id="markov"> <label for="markov" style="margin:0">Markov Blending</label></div>
|
|
77
|
+
<div class="checkbox"><input type="checkbox" id="scoring"> <label for="scoring" style="margin:0">zxcvbn Scoring</label></div>
|
|
78
|
+
<button onclick="startGeneration()">Generate Wordlist</button>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="main">
|
|
81
|
+
<div class="header">Generation Log</div>
|
|
82
|
+
<div class="log-area" id="logArea"></div>
|
|
83
|
+
<div class="stats">
|
|
84
|
+
<div class="stat-item"><span class="label">Candidates</span><br><span class="value" id="statCandidates">0</span></div>
|
|
85
|
+
<div class="stat-item"><span class="label">Output Size</span><br><span class="value" id="statSize">0 B</span></div>
|
|
86
|
+
<div class="stat-item"><span class="label">Status</span><br><span class="value" id="statStatus">Idle</span></div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
<script>
|
|
90
|
+
const ws = new WebSocket(`ws://${window.location.host}/ws`);
|
|
91
|
+
ws.onmessage = (e) => {
|
|
92
|
+
const msg = JSON.parse(e.data);
|
|
93
|
+
const logArea = document.getElementById('logArea');
|
|
94
|
+
const line = document.createElement('div');
|
|
95
|
+
line.className = `log-line ${msg.level || 'info'}`;
|
|
96
|
+
line.textContent = msg.text;
|
|
97
|
+
logArea.appendChild(line);
|
|
98
|
+
logArea.scrollTop = logArea.scrollHeight;
|
|
99
|
+
if (msg.candidates) document.getElementById('statCandidates').textContent = Number(msg.candidates).toLocaleString();
|
|
100
|
+
if (msg.size) document.getElementById('statSize').textContent = (msg.size / (1024*1024)).toFixed(2) + ' MB';
|
|
101
|
+
if (msg.status) document.getElementById('statStatus').textContent = msg.status;
|
|
102
|
+
};
|
|
103
|
+
ws.onclose = () => { addLog('WebSocket disconnected', 'error'); };
|
|
104
|
+
function addLog(text, level) {
|
|
105
|
+
const logArea = document.getElementById('logArea');
|
|
106
|
+
const line = document.createElement('div');
|
|
107
|
+
line.className = `log-line ${level}`;
|
|
108
|
+
line.textContent = text;
|
|
109
|
+
logArea.appendChild(line);
|
|
110
|
+
}
|
|
111
|
+
async function startGeneration() {
|
|
112
|
+
const fileInput = document.getElementById('profileFile');
|
|
113
|
+
if (!fileInput.files.length) { addLog('Please select a profile JSON file.', 'warning'); return; }
|
|
114
|
+
const formData = new FormData();
|
|
115
|
+
formData.append('profile', fileInput.files[0]);
|
|
116
|
+
formData.append('output', document.getElementById('outputFile').value || 'pawpy_wordlist.txt');
|
|
117
|
+
const mode = document.getElementById('mode').value;
|
|
118
|
+
formData.append('lite', mode === 'lite');
|
|
119
|
+
formData.append('extreme', mode === 'extreme');
|
|
120
|
+
formData.append('markov', document.getElementById('markov').checked);
|
|
121
|
+
formData.append('min_strength', document.getElementById('scoring').checked ? '2' : '');
|
|
122
|
+
addLog('Starting generation...', 'info');
|
|
123
|
+
try {
|
|
124
|
+
const resp = await fetch('/generate', { method: 'POST', body: formData });
|
|
125
|
+
const data = await resp.json();
|
|
126
|
+
if (data.download_url) {
|
|
127
|
+
addLog('Done! Download: ' + data.download_url, 'success');
|
|
128
|
+
} else {
|
|
129
|
+
addLog('Error: ' + (data.message || 'Unknown'), 'error');
|
|
130
|
+
}
|
|
131
|
+
} catch (err) { addLog('Fetch error: ' + err.message, 'error'); }
|
|
132
|
+
}
|
|
133
|
+
</script>
|
|
134
|
+
</body>
|
|
135
|
+
</html>
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def _websocket_handler(websocket):
|
|
140
|
+
"""Handle a single WebSocket connection for real-time logging."""
|
|
141
|
+
_connections.add(websocket)
|
|
142
|
+
try:
|
|
143
|
+
async for message in websocket:
|
|
144
|
+
pass # Dashboard only receives; no client commands over WS
|
|
145
|
+
finally:
|
|
146
|
+
_connections.discard(websocket)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def broadcast_log(text: str, level: str = "info", **extra):
|
|
150
|
+
"""Broadcast a log message to all connected WebSocket clients."""
|
|
151
|
+
msg = json.dumps({"text": text, "level": level, **extra})
|
|
152
|
+
for ws in list(_connections):
|
|
153
|
+
try:
|
|
154
|
+
await ws.send_text(msg)
|
|
155
|
+
except Exception:
|
|
156
|
+
_connections.discard(ws)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def run_dashboard(host: str = "127.0.0.1", port: int = 8080):
|
|
160
|
+
"""Launch the web dashboard with both HTTP and WebSocket support."""
|
|
161
|
+
try:
|
|
162
|
+
import uvicorn
|
|
163
|
+
from fastapi import FastAPI, WebSocket
|
|
164
|
+
from fastapi.responses import HTMLResponse
|
|
165
|
+
except ImportError:
|
|
166
|
+
raise ImportError(
|
|
167
|
+
"FastAPI and uvicorn are required for dashboard mode. "
|
|
168
|
+
"Install them with: pip install fastapi uvicorn websockets"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
app = FastAPI(title="Pawpy Dashboard")
|
|
172
|
+
|
|
173
|
+
@app.get("/", response_class=HTMLResponse)
|
|
174
|
+
async def dashboard_page():
|
|
175
|
+
return _DASHBOARD_HTML
|
|
176
|
+
|
|
177
|
+
@app.websocket("/ws")
|
|
178
|
+
async def websocket_endpoint(websocket: WebSocket):
|
|
179
|
+
await websocket.accept()
|
|
180
|
+
await _websocket_handler(websocket)
|
|
181
|
+
|
|
182
|
+
logger.info("Starting Pawpy Dashboard on http://%s:%d", host, port)
|
|
183
|
+
uvicorn.run(app, host=host, port=port)
|
pawpy/api/rest.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""FastAPI REST endpoints for Pawpy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
import uuid
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("pawpy.api")
|
|
12
|
+
|
|
13
|
+
_app = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_app():
|
|
17
|
+
"""Lazily create and return the FastAPI app."""
|
|
18
|
+
global _app
|
|
19
|
+
if _app is not None:
|
|
20
|
+
return _app
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from fastapi import FastAPI, File, Form, UploadFile
|
|
24
|
+
from fastapi.responses import FileResponse, JSONResponse
|
|
25
|
+
except ImportError:
|
|
26
|
+
raise ImportError(
|
|
27
|
+
"FastAPI is required for API mode. "
|
|
28
|
+
"Install it with: pip install fastapi uvicorn"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
app = FastAPI(
|
|
32
|
+
title="Pawpy API",
|
|
33
|
+
description="Educational Wordlist Generator – REST API",
|
|
34
|
+
version="1.0.0",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# In-memory job store (simple; production would use a database)
|
|
38
|
+
_jobs: Dict[str, Dict[str, Any]] = {}
|
|
39
|
+
|
|
40
|
+
@app.get("/")
|
|
41
|
+
async def root():
|
|
42
|
+
return {"name": "Pawpy API", "version": "1.0.0", "status": "running"}
|
|
43
|
+
|
|
44
|
+
@app.post("/generate")
|
|
45
|
+
async def generate_wordlist(
|
|
46
|
+
profile: UploadFile = File(...),
|
|
47
|
+
output: Optional[str] = Form(None),
|
|
48
|
+
lite: bool = Form(False),
|
|
49
|
+
extreme: bool = Form(False),
|
|
50
|
+
min_length: Optional[int] = Form(None),
|
|
51
|
+
min_strength: Optional[int] = Form(None),
|
|
52
|
+
markov: bool = Form(False),
|
|
53
|
+
):
|
|
54
|
+
"""Generate a wordlist from an uploaded profile JSON file."""
|
|
55
|
+
job_id = str(uuid.uuid4())[:8]
|
|
56
|
+
out_file = output or f"pawpy_{job_id}.txt"
|
|
57
|
+
|
|
58
|
+
# Save uploaded profile to temp file
|
|
59
|
+
tmp_profile = tempfile.NamedTemporaryFile(
|
|
60
|
+
mode="w", suffix=".json", delete=False, prefix="pawpy_profile_"
|
|
61
|
+
)
|
|
62
|
+
content = await profile.read()
|
|
63
|
+
tmp_profile.write(content.decode("utf-8", errors="ignore"))
|
|
64
|
+
tmp_profile.close()
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
from pawpy.config import PawpyConfig
|
|
68
|
+
from pawpy.generator.core import PipelineOrchestrator
|
|
69
|
+
from pawpy.profile.base import ProfileCollector
|
|
70
|
+
|
|
71
|
+
config = PawpyConfig(
|
|
72
|
+
output_file=out_file,
|
|
73
|
+
profile_json=tmp_profile.name,
|
|
74
|
+
lite=lite,
|
|
75
|
+
extreme=extreme,
|
|
76
|
+
min_length=min_length,
|
|
77
|
+
min_strength=min_strength,
|
|
78
|
+
markov=markov,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
collector = ProfileCollector()
|
|
82
|
+
prof = collector.run(config)
|
|
83
|
+
orchestrator = PipelineOrchestrator(config, prof)
|
|
84
|
+
result_path = orchestrator.run()
|
|
85
|
+
|
|
86
|
+
_jobs[job_id] = {
|
|
87
|
+
"status": "completed",
|
|
88
|
+
"output": result_path,
|
|
89
|
+
"size": os.path.getsize(result_path),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return JSONResponse(
|
|
93
|
+
content={
|
|
94
|
+
"job_id": job_id,
|
|
95
|
+
"status": "completed",
|
|
96
|
+
"download_url": f"/download/{job_id}",
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.exception("Generation failed")
|
|
102
|
+
return JSONResponse(
|
|
103
|
+
status_code=500,
|
|
104
|
+
content={"job_id": job_id, "status": "error", "message": str(e)},
|
|
105
|
+
)
|
|
106
|
+
finally:
|
|
107
|
+
try:
|
|
108
|
+
os.unlink(tmp_profile.name)
|
|
109
|
+
except Exception:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
@app.get("/download/{job_id}")
|
|
113
|
+
async def download_wordlist(job_id: str):
|
|
114
|
+
"""Download a generated wordlist by job ID."""
|
|
115
|
+
job = _jobs.get(job_id)
|
|
116
|
+
if not job or job["status"] != "completed":
|
|
117
|
+
return JSONResponse(
|
|
118
|
+
status_code=404, content={"error": "Job not found or not completed"}
|
|
119
|
+
)
|
|
120
|
+
return FileResponse(
|
|
121
|
+
job["output"],
|
|
122
|
+
media_type="text/plain",
|
|
123
|
+
filename=os.path.basename(job["output"]),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
@app.get("/jobs")
|
|
127
|
+
async def list_jobs():
|
|
128
|
+
"""List all generation jobs."""
|
|
129
|
+
return {"jobs": _jobs}
|
|
130
|
+
|
|
131
|
+
_app = app
|
|
132
|
+
return app
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def run_api(host: str = "127.0.0.1", port: int = 8000):
|
|
136
|
+
"""Start the API server using uvicorn."""
|
|
137
|
+
app = get_app()
|
|
138
|
+
try:
|
|
139
|
+
import uvicorn
|
|
140
|
+
except ImportError:
|
|
141
|
+
raise ImportError(
|
|
142
|
+
"uvicorn is required for API mode. " "Install it with: pip install uvicorn"
|
|
143
|
+
)
|
|
144
|
+
logger.info("Starting Pawpy API on %s:%d", host, port)
|
|
145
|
+
uvicorn.run(app, host=host, port=port)
|
pawpy/cli.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Pawpy command-line interface.
|
|
2
|
+
|
|
3
|
+
Parses all CLI options, displays the banner, orchestrates profile collection
|
|
4
|
+
and the generation pipeline, and handles sub-commands (update-passwords,
|
|
5
|
+
api, dashboard).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import logging
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from pawpy import __version__
|
|
15
|
+
from pawpy.config import PawpyConfig
|
|
16
|
+
from pawpy.utils import confirm_ethical_use, console, print_banner, setup_logging
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("pawpy")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
22
|
+
"""Build and return the argument parser with all options."""
|
|
23
|
+
parser = argparse.ArgumentParser(
|
|
24
|
+
prog="pawpy",
|
|
25
|
+
description="Pawpy – The Most Powerful Educational Wordlist Generator",
|
|
26
|
+
epilog=(
|
|
27
|
+
"Examples:\n"
|
|
28
|
+
" pawpy # Interactive mode\n"
|
|
29
|
+
" pawpy -j profile.json -o wordlist.txt # From JSON profile\n"
|
|
30
|
+
" pawpy --multi targets.json --extreme # Multi-target extreme mode\n"
|
|
31
|
+
" pawpy --rules best64.rule --markov # Rules + Markov blending\n"
|
|
32
|
+
" pawpy --hybrid-left ?l?d -o hybrid.txt # Hybrid mask attack\n"
|
|
33
|
+
" pawpy --min-length 8 --require-upper # Policy-filtered output\n"
|
|
34
|
+
" pawpy update-passwords # Download latest SecLists\n"
|
|
35
|
+
" pawpy api # Start REST API\n"
|
|
36
|
+
" pawpy dashboard # Launch web dashboard\n"
|
|
37
|
+
),
|
|
38
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# --- I/O ---
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"-o",
|
|
44
|
+
"--output",
|
|
45
|
+
default="pawpy_wordlist.txt",
|
|
46
|
+
help="Output file path (default: pawpy_wordlist.txt)",
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"-j",
|
|
50
|
+
"--import-json",
|
|
51
|
+
dest="profile_json",
|
|
52
|
+
help="Import a single target profile from a JSON file",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--multi",
|
|
56
|
+
help="Import multiple profiles from a JSON array file",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--rules",
|
|
60
|
+
help="Load a hashcat/John-style .rule file",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--template",
|
|
64
|
+
action="append",
|
|
65
|
+
default=[],
|
|
66
|
+
dest="templates",
|
|
67
|
+
help="Custom pattern template, e.g. [FirstName][Year][!] (repeatable)",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# --- Hybrid masks ---
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--hybrid-left",
|
|
73
|
+
help="Left mask for hybrid attack (hashcat -a 7), e.g. ?l?d",
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"--hybrid-right",
|
|
77
|
+
help="Right mask for hybrid attack (hashcat -a 6), e.g. ?d?d?d",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# --- Markov ---
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--markov",
|
|
83
|
+
action="store_true",
|
|
84
|
+
help="Enable Markov chain blending",
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--markov-order",
|
|
88
|
+
type=int,
|
|
89
|
+
default=2,
|
|
90
|
+
help="Markov chain order (default: 2)",
|
|
91
|
+
)
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"--markov-count",
|
|
94
|
+
type=int,
|
|
95
|
+
default=5000,
|
|
96
|
+
help="Number of Markov-generated words (default: 5000)",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# --- Scoring / filtering ---
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"--min-strength",
|
|
102
|
+
type=int,
|
|
103
|
+
choices=[0, 1, 2, 3, 4],
|
|
104
|
+
help="Minimum zxcvbn strength score (0-4). Requires zxcvbn installed.",
|
|
105
|
+
)
|
|
106
|
+
parser.add_argument(
|
|
107
|
+
"--min-length",
|
|
108
|
+
type=int,
|
|
109
|
+
help="Minimum password length",
|
|
110
|
+
)
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
"--require-upper",
|
|
113
|
+
action="store_true",
|
|
114
|
+
help="Policy: require at least one uppercase letter",
|
|
115
|
+
)
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
"--require-lower",
|
|
118
|
+
action="store_true",
|
|
119
|
+
help="Policy: require at least one lowercase letter",
|
|
120
|
+
)
|
|
121
|
+
parser.add_argument(
|
|
122
|
+
"--require-digit",
|
|
123
|
+
action="store_true",
|
|
124
|
+
help="Policy: require at least one digit",
|
|
125
|
+
)
|
|
126
|
+
parser.add_argument(
|
|
127
|
+
"--require-special",
|
|
128
|
+
action="store_true",
|
|
129
|
+
help="Policy: require at least one special character",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# --- Modes ---
|
|
133
|
+
mode_group = parser.add_mutually_exclusive_group()
|
|
134
|
+
mode_group.add_argument(
|
|
135
|
+
"--lite",
|
|
136
|
+
action="store_true",
|
|
137
|
+
help="Fast mode: skip heavy combinations (two-word, Markov, large hybrid)",
|
|
138
|
+
)
|
|
139
|
+
mode_group.add_argument(
|
|
140
|
+
"--extreme",
|
|
141
|
+
action="store_true",
|
|
142
|
+
help="Enable all heavy mutations (year blends, Markov, dynamic keyboard walks)",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# --- Performance ---
|
|
146
|
+
parser.add_argument(
|
|
147
|
+
"--gpu",
|
|
148
|
+
action="store_true",
|
|
149
|
+
help="Use GPU acceleration if CuPy is available",
|
|
150
|
+
)
|
|
151
|
+
parser.add_argument(
|
|
152
|
+
"-t",
|
|
153
|
+
"--threads",
|
|
154
|
+
type=int,
|
|
155
|
+
default=None,
|
|
156
|
+
help="Number of worker processes (default: CPU count)",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# --- Meta ---
|
|
160
|
+
parser.add_argument(
|
|
161
|
+
"-v",
|
|
162
|
+
"--version",
|
|
163
|
+
action="version",
|
|
164
|
+
version=f"%(prog)s {__version__}",
|
|
165
|
+
)
|
|
166
|
+
parser.add_argument(
|
|
167
|
+
"--verbose",
|
|
168
|
+
action="store_true",
|
|
169
|
+
help="Enable verbose (debug) logging",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return parser
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def build_config(args: argparse.Namespace) -> PawpyConfig:
|
|
176
|
+
"""Convert parsed arguments into a PawpyConfig object."""
|
|
177
|
+
import multiprocessing
|
|
178
|
+
|
|
179
|
+
config = PawpyConfig(
|
|
180
|
+
output_file=args.output,
|
|
181
|
+
profile_json=args.profile_json,
|
|
182
|
+
multi_json=args.multi,
|
|
183
|
+
rule_file=args.rules,
|
|
184
|
+
templates=args.templates,
|
|
185
|
+
hybrid_left=args.hybrid_left,
|
|
186
|
+
hybrid_right=args.hybrid_right,
|
|
187
|
+
markov=args.markov,
|
|
188
|
+
markov_order=args.markov_order,
|
|
189
|
+
markov_count=args.markov_count,
|
|
190
|
+
min_strength=args.min_strength,
|
|
191
|
+
min_length=args.min_length,
|
|
192
|
+
require_upper=args.require_upper,
|
|
193
|
+
require_lower=args.require_lower,
|
|
194
|
+
require_digit=args.require_digit,
|
|
195
|
+
require_special=args.require_special,
|
|
196
|
+
lite=args.lite,
|
|
197
|
+
extreme=args.extreme,
|
|
198
|
+
gpu=args.gpu,
|
|
199
|
+
threads=args.threads if args.threads else multiprocessing.cpu_count() or 4,
|
|
200
|
+
)
|
|
201
|
+
return config
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def handle_update_passwords() -> None:
|
|
205
|
+
"""Handle the 'update-passwords' sub-command."""
|
|
206
|
+
from pawpy.data.updater import update_common_passwords
|
|
207
|
+
|
|
208
|
+
console.print("[cyan]Downloading latest common passwords from SecLists...[/cyan]")
|
|
209
|
+
try:
|
|
210
|
+
path = update_common_passwords()
|
|
211
|
+
console.print(f"[green]Updated common passwords saved to:[/green] {path}")
|
|
212
|
+
except Exception as e:
|
|
213
|
+
console.print(f"[red]Failed to update:[/red] {e}")
|
|
214
|
+
sys.exit(1)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def handle_api() -> None:
|
|
218
|
+
"""Handle the 'api' sub-command."""
|
|
219
|
+
from pawpy.api.rest import run_api
|
|
220
|
+
|
|
221
|
+
console.print("[cyan]Starting Pawpy REST API...[/cyan]")
|
|
222
|
+
try:
|
|
223
|
+
run_api()
|
|
224
|
+
except ImportError as e:
|
|
225
|
+
console.print(f"[red]Missing dependency:[/red] {e}")
|
|
226
|
+
sys.exit(1)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def handle_dashboard() -> None:
|
|
230
|
+
"""Handle the 'dashboard' sub-command."""
|
|
231
|
+
from pawpy.api.dashboard import run_dashboard
|
|
232
|
+
|
|
233
|
+
console.print("[cyan]Starting Pawpy Web Dashboard...[/cyan]")
|
|
234
|
+
try:
|
|
235
|
+
run_dashboard()
|
|
236
|
+
except ImportError as e:
|
|
237
|
+
console.print(f"[red]Missing dependency:[/red] {e}")
|
|
238
|
+
sys.exit(1)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def main(argv: list = None) -> None:
|
|
242
|
+
"""Main entry point for the Pawpy CLI."""
|
|
243
|
+
# Handle sub-commands first (they don't need the full parser)
|
|
244
|
+
if argv is None:
|
|
245
|
+
argv = sys.argv[1:]
|
|
246
|
+
|
|
247
|
+
if not argv:
|
|
248
|
+
# No args → show banner, then interactive mode
|
|
249
|
+
pass
|
|
250
|
+
elif argv[0] == "update-passwords":
|
|
251
|
+
handle_update_passwords()
|
|
252
|
+
return
|
|
253
|
+
elif argv[0] == "api":
|
|
254
|
+
handle_api()
|
|
255
|
+
return
|
|
256
|
+
elif argv[0] == "dashboard":
|
|
257
|
+
handle_dashboard()
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
# Parse arguments
|
|
261
|
+
parser = build_parser()
|
|
262
|
+
args = parser.parse_args(argv)
|
|
263
|
+
|
|
264
|
+
setup_logging()
|
|
265
|
+
|
|
266
|
+
# Print banner and ethical warning
|
|
267
|
+
print_banner()
|
|
268
|
+
|
|
269
|
+
# For interactive mode, confirm ethical use
|
|
270
|
+
if not args.profile_json and not args.multi:
|
|
271
|
+
if not confirm_ethical_use():
|
|
272
|
+
console.print("[yellow]Authorisation not confirmed. Exiting.[/yellow]")
|
|
273
|
+
sys.exit(0)
|
|
274
|
+
else:
|
|
275
|
+
console.print(
|
|
276
|
+
"[dim]Note: Ensure you have explicit authorisation to test "
|
|
277
|
+
"target accounts.[/dim]\n"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Build configuration
|
|
281
|
+
config = build_config(args)
|
|
282
|
+
|
|
283
|
+
# Collect profile
|
|
284
|
+
from pawpy.generator.core import PipelineOrchestrator
|
|
285
|
+
from pawpy.profile.base import ProfileCollector
|
|
286
|
+
from pawpy.profile.multi import (
|
|
287
|
+
extract_merged_base_words,
|
|
288
|
+
load_multi_profiles,
|
|
289
|
+
merge_profiles,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
if config.multi_json:
|
|
293
|
+
console.print(
|
|
294
|
+
f"[cyan]Loading multi-target profiles from:[/cyan] {config.multi_json}"
|
|
295
|
+
)
|
|
296
|
+
profiles = load_multi_profiles(config.multi_json)
|
|
297
|
+
merged = merge_profiles(profiles)
|
|
298
|
+
profile = merged
|
|
299
|
+
base_words = extract_merged_base_words(merged)
|
|
300
|
+
console.print(
|
|
301
|
+
f"[green]Extracted {len(base_words)} unique base words from {len(profiles)} profiles.[/green]\n"
|
|
302
|
+
)
|
|
303
|
+
else:
|
|
304
|
+
collector = ProfileCollector()
|
|
305
|
+
profile = collector.run(config)
|
|
306
|
+
base_words = ProfileCollector.extract_base_words(profile)
|
|
307
|
+
if base_words:
|
|
308
|
+
console.print(
|
|
309
|
+
f"[green]Extracted {len(base_words)} base words from profile.[/green]\n"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# GPU availability check
|
|
313
|
+
if config.gpu:
|
|
314
|
+
from pawpy.generator.gpu import is_gpu_available
|
|
315
|
+
|
|
316
|
+
if is_gpu_available():
|
|
317
|
+
console.print("[green]GPU acceleration enabled (CuPy detected).[/green]\n")
|
|
318
|
+
else:
|
|
319
|
+
console.print(
|
|
320
|
+
"[yellow]CuPy not installed. GPU mode requested but falling back to CPU.[/yellow]\n"
|
|
321
|
+
)
|
|
322
|
+
console.print(
|
|
323
|
+
"[dim]Install CuPy for GPU support: pip install cupy-cuda11x (or cupy-cuda12x)[/dim]\n"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# Run the generation pipeline
|
|
327
|
+
try:
|
|
328
|
+
orchestrator = PipelineOrchestrator(config, profile)
|
|
329
|
+
output_path = orchestrator.run()
|
|
330
|
+
console.print(f"\n[bold green]Wordlist saved to: {output_path}[/bold green]")
|
|
331
|
+
except KeyboardInterrupt:
|
|
332
|
+
console.print("\n[yellow]Generation interrupted by user.[/yellow]")
|
|
333
|
+
sys.exit(130)
|
|
334
|
+
except Exception as e:
|
|
335
|
+
logger.exception("Generation failed")
|
|
336
|
+
console.print(f"\n[bold red]Error:[/bold red] {e}")
|
|
337
|
+
sys.exit(1)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
if __name__ == "__main__":
|
|
341
|
+
main()
|