fortifypass-validator 0.2.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.
@@ -0,0 +1,332 @@
1
+ Metadata-Version: 2.4
2
+ Name: fortifypass-validator
3
+ Version: 0.2.0
4
+ Summary: A modern password validation library powered by zxcvbn with policy enforcement and CLI support.
5
+ Author: Botshelo Mere
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/botshelo-mere/password-validator
8
+ Project-URL: Repository, https://github.com/botshelo-mere/password-validator
9
+ Project-URL: Issues, https://github.com/botshelo-mere/password-validator/issues
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Development Status :: 4 - Beta
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE.md
21
+ Requires-Dist: zxcvbn>=4.4.28
22
+ Requires-Dist: colorama>=0.4.6
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7.0; extra == "dev"
25
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
26
+ Requires-Dist: pytest-benchmark>=4.0; extra == "dev"
27
+ Requires-Dist: jsonschema>=4.0; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # password-validator
31
+
32
+ [![Python Version](https://img.shields.io/badge/python-3.9+-blue.svg)](https://python.org)
33
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE.md)
34
+ [![Development Status](https://img.shields.io/badge/status-beta-yellow.svg)](https://github.com/botshelo-mere/password-validator)
35
+ [![zxcvbn powered](https://img.shields.io/badge/strength-zxcvbn%20powered-purple.svg)](https://github.com/dwolfhub/zxcvbn-python)
36
+
37
+ A modern, policy-driven password validation library for Python — powered by
38
+ [zxcvbn](https://github.com/dwolfhub/zxcvbn-python) for realistic strength
39
+ estimation and fully configurable rule enforcement with a built-in CLI.
40
+
41
+ ---
42
+
43
+ ## Features
44
+
45
+ - **Policy validation** — enforce length, character classes, spaces, and banned words
46
+ - **Realistic strength scoring** — zxcvbn rates passwords 0–4 with human-readable labels
47
+ - **Unified `evaluate()` API** — combines validation + strength in one call
48
+ - **CLI interface** — interactive (colored) and non-interactive (JSON pipe) modes
49
+ - **Resilient design** — defensive constructor, graceful zxcvbn exception handling
50
+ - **Thread-safe** — safe to share a single `PasswordValidator` across threads
51
+ - **Typed** — full `typing` annotations throughout
52
+
53
+ ---
54
+
55
+ ## Installation
56
+
57
+ ### Using uv (recommended)
58
+
59
+ ```bash
60
+ git clone https://github.com/botshelo-mere/password-validator.git
61
+ cd password-validator
62
+
63
+ # Create virtual environment and install dependencies
64
+ uv sync
65
+ ```
66
+
67
+ ### Using pip
68
+
69
+ ```bash
70
+ pip install password-validator
71
+ ```
72
+
73
+ ### Dev / Testing
74
+
75
+ ```bash
76
+ # Install with development extras (pytest, pytest-cov, pytest-benchmark, jsonschema)
77
+ uv sync --extra dev
78
+ # or
79
+ pip install "password-validator[dev]"
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Quick Start
85
+
86
+ ### Library Usage
87
+
88
+ ```python
89
+ from password_validator import PasswordValidator
90
+
91
+ # Default policy: 12–64 chars, upper, lower, digit, special required
92
+ validator = PasswordValidator()
93
+
94
+ # --- validate() → (bool, list[str]) ---
95
+ valid, errors = validator.validate("Str0ngP@ssw0rd!")
96
+ print(valid) # True
97
+ print(errors) # []
98
+
99
+ valid, errors = validator.validate("weak")
100
+ print(valid) # False
101
+ print(errors) # ['Password must be at least 12 characters long', ...]
102
+
103
+ # --- estimate_strength() → dict ---
104
+ strength = validator.estimate_strength("Str0ngP@ssw0rd!")
105
+ print(strength["score"]) # 3
106
+ print(strength["label"]) # 'Strong'
107
+ print(strength["feedback"]) # []
108
+
109
+ # --- evaluate() — combines both in one call ---
110
+ result = validator.evaluate("Str0ngP@ssw0rd!")
111
+ # {
112
+ # "valid": True,
113
+ # "errors": [],
114
+ # "score": 3,
115
+ # "label": "Strong",
116
+ # "feedback": []
117
+ # }
118
+ ```
119
+
120
+ ### Custom Policy
121
+
122
+ ```python
123
+ validator = PasswordValidator(
124
+ min_length=8,
125
+ max_length=32,
126
+ require_uppercase=True,
127
+ require_lowercase=True,
128
+ require_digit=True,
129
+ require_special=True,
130
+ special_chars="!@#$%",
131
+ allow_spaces=False,
132
+ banned_words=["password", "admin", "secret"],
133
+ )
134
+
135
+ valid, errors = validator.validate("Admin123!")
136
+ # valid=False → "Contains a banned word"
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Configuration Reference
142
+
143
+ | Parameter | Type | Default | Description |
144
+ |---|---|---|---|
145
+ | `min_length` | `int` | `12` | Minimum password length (must be ≥ 1) |
146
+ | `max_length` | `int` | `64` | Maximum password length (must be ≥ `min_length`) |
147
+ | `require_uppercase` | `bool` | `True` | Require at least one uppercase letter |
148
+ | `require_lowercase` | `bool` | `True` | Require at least one lowercase letter |
149
+ | `require_digit` | `bool` | `True` | Require at least one digit |
150
+ | `require_special` | `bool` | `True` | Require at least one special character |
151
+ | `special_chars` | `str` | `"!@#$%^&*"` | Set of accepted special characters |
152
+ | `allow_spaces` | `bool` | `False` | Whether whitespace is permitted |
153
+ | `banned_words` | `list[str] \| None` | `None` | Case-insensitive list of forbidden words |
154
+
155
+ > `ValueError` is raised on construction if `min_length <= 0` or `max_length < min_length`.
156
+
157
+ ---
158
+
159
+ ## API Reference
160
+
161
+ ### `PasswordValidator.validate(pwd: str) → Tuple[bool, List[str]]`
162
+
163
+ Runs all configured policy rules against the password.
164
+
165
+ - Returns `(True, [])` if all rules pass.
166
+ - Returns `(False, [<error messages>])` if any rule fails.
167
+ - Raises `ValueError` if `pwd` is not a `str`.
168
+
169
+ ### `PasswordValidator.estimate_strength(pwd: str) → Dict[str, Any]`
170
+
171
+ Uses zxcvbn to estimate password strength.
172
+
173
+ - Passwords longer than 72 characters are truncated before scoring (zxcvbn limit).
174
+ - Always returns a dictionary — never raises.
175
+
176
+ | Key | Type | Description |
177
+ |---|---|---|
178
+ | `score` | `int` | 0 (Very Weak) → 4 (Very Strong) |
179
+ | `label` | `str` | Human-readable label |
180
+ | `feedback` | `list[str]` | Warnings and suggestions from zxcvbn |
181
+
182
+ ### `PasswordValidator.evaluate(pwd: str) → Dict[str, Any]`
183
+
184
+ Combines `validate()` and `estimate_strength()` into a single result.
185
+
186
+ | Key | Type | Description |
187
+ |---|---|---|
188
+ | `valid` | `bool` | Whether all policy rules passed |
189
+ | `errors` | `list[str]` | Policy error messages |
190
+ | `score` | `int` | zxcvbn score 0–4 |
191
+ | `label` | `str` | Strength label |
192
+ | `feedback` | `list[str]` | zxcvbn feedback |
193
+
194
+ ---
195
+
196
+ ## CLI Usage
197
+
198
+ ### Interactive Mode
199
+
200
+ ```bash
201
+ uv run password-validator
202
+ ```
203
+
204
+ ```
205
+ Password Validator v0.2.0 (zxcvbn powered)
206
+ Type .exit() to quit
207
+
208
+ Enter password: ········
209
+ ✓ Valid password
210
+
211
+ Strength: Strong
212
+ • Use a longer keyboard pattern with more turns
213
+ ```
214
+
215
+ - Type `.exit()` to quit.
216
+ - Press `Ctrl+C` to interrupt.
217
+ - Password input is hidden (no echo).
218
+
219
+ ### Non-Interactive / Pipe Mode
220
+
221
+ Pipe a password directly — output is JSON, exit code is `0` for score ≥ 3, `1` otherwise:
222
+
223
+ ```bash
224
+ echo "Str0ngP@ssw0rd!" | password-validator
225
+ ```
226
+
227
+ ```json
228
+ {
229
+ "valid": true,
230
+ "errors": [],
231
+ "score": 3,
232
+ "label": "Strong",
233
+ "feedback": []
234
+ }
235
+ ```
236
+
237
+ Ideal for shell scripts and CI pipelines:
238
+
239
+ ```bash
240
+ echo "$PASSWORD" | password-validator && echo "Password accepted" || echo "Password rejected"
241
+ ```
242
+
243
+ ---
244
+
245
+ ## Project Structure
246
+
247
+ ```
248
+ password-validator/
249
+ ├── pyproject.toml # Project configuration
250
+ ├── .gitignore
251
+ ├── README.md
252
+ ├── LICENSE.md
253
+ ├── src/
254
+ │ └── password_validator/
255
+ │ ├── __init__.py # Public API (__version__, __all__)
256
+ │ ├── validator.py # Core validation + zxcvbn strength logic
257
+ │ └── cli.py # CLI interface (colorama)
258
+ └── tests/
259
+ ├── __init__.py
260
+ ├── test_validator.py # Validator unit tests
261
+ ├── test_cli.py # CLI unit tests
262
+ └── test_performance_benchmarks.py # pytest-benchmark performance tests
263
+ ```
264
+
265
+ ---
266
+
267
+ ## Development
268
+
269
+ ### Running Tests
270
+
271
+ ```bash
272
+ # All tests
273
+ uv run pytest
274
+
275
+ # With coverage report
276
+ uv run pytest --cov=password_validator --cov-report=term-missing
277
+
278
+ # Verbose output
279
+ uv run pytest -v
280
+ ```
281
+
282
+ ### Running Benchmarks
283
+
284
+ ```bash
285
+ uv run pytest tests/test_performance_benchmarks.py --benchmark-only
286
+ ```
287
+
288
+ ### Code Coverage
289
+
290
+ ```bash
291
+ uv run pytest --cov=password_validator --cov-branch --cov-report=html
292
+ # Open htmlcov/index.html in your browser
293
+ ```
294
+
295
+ ---
296
+
297
+ ## Strength Score Labels
298
+
299
+ | Score | Label | Meaning |
300
+ |---|---|---|
301
+ | 0 | Very Weak | Trivially crackable |
302
+ | 1 | Weak | Easy to crack |
303
+ | 2 | Moderate | Some resistance |
304
+ | 3 | Strong | Good security |
305
+ | 4 | Very Strong | Excellent security |
306
+
307
+ Scoring is provided by [zxcvbn](https://github.com/dwolfhub/zxcvbn-python), which
308
+ models real-world cracking strategies (dictionary attacks, keyboard patterns, etc.)
309
+ rather than simple character-class rules.
310
+
311
+ ---
312
+
313
+ ## Version History
314
+
315
+ | Version | Changes |
316
+ |---|---|
317
+ | **v0.2.0** | Added zxcvbn strength estimation, `evaluate()` API, banned words, full type annotations, colorama CLI, pipe mode, comprehensive test suite |
318
+ | **v0.1.1** | Infrastructure improvements, packaging fixes |
319
+ | **v0.1.0** | Initial public release |
320
+
321
+ ---
322
+
323
+ ## License
324
+
325
+ This project is licensed under the MIT License — see [LICENSE.md](LICENSE.md) for details.
326
+
327
+ ---
328
+
329
+ ## Author
330
+
331
+ **Botshelo Mere**
332
+ GitHub: [botshelo-mere](https://github.com/botshelo-mere)
@@ -0,0 +1,9 @@
1
+ fortifypass_validator-0.2.0.dist-info/licenses/LICENSE.md,sha256=MS2TipvClIKHVM1sW01QXRGs9N3kegc6OrLEbsnFHBI,1089
2
+ password_validator/__init__.py,sha256=I3JRtOw3YyOj0LmLkr4mVBKQ9WxrzxqXhWqXXD5jFtw,96
3
+ password_validator/cli.py,sha256=1G8uWs1IMKm2VjYhMbBUcQ6r8t0ULJFQkmyikl698bw,1911
4
+ password_validator/validator.py,sha256=GJZfMrgAxo8dlhFrjzIQ6Av5c5amAos9MoGe5imUnMo,4440
5
+ fortifypass_validator-0.2.0.dist-info/METADATA,sha256=G4TOwJk9sgXlO9tE0laLgmr5s_bb19YsLIjDdiLyQ9w,9581
6
+ fortifypass_validator-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ fortifypass_validator-0.2.0.dist-info/entry_points.txt,sha256=srK4kKaMMUZBDNZklZxiOlh30kMTHubt7SK951cBbN8,67
8
+ fortifypass_validator-0.2.0.dist-info/top_level.txt,sha256=lXxc73p4NxqiirJMlyCfZYV6_e9bW2G32Jeh0xDoc1E,19
9
+ fortifypass_validator-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ password-validator = password_validator.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Botshelo Mere
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ password_validator
@@ -0,0 +1,4 @@
1
+ from .validator import PasswordValidator
2
+
3
+ __version__ = "0.2.0"
4
+ __all__ = ["PasswordValidator"]
@@ -0,0 +1,65 @@
1
+ import sys
2
+ import json
3
+ import getpass
4
+ from colorama import init, Fore, Style
5
+ from password_validator.validator import PasswordValidator, MAX_ZXCVBN_LEN
6
+
7
+ # Initialize Colorama
8
+ init(autoreset=True)
9
+
10
+ def main():
11
+ validator = PasswordValidator()
12
+
13
+ # Handle piped input (non-interactive)
14
+ if not sys.stdin.isatty():
15
+ pwd = sys.stdin.read().strip()
16
+
17
+ result = validator.evaluate(pwd)
18
+ print(json.dumps(result, indent=2))
19
+ sys.exit(0 if result["score"] >= 3 else 1)
20
+
21
+ # Interactive mode
22
+ print(Fore.CYAN + Style.BRIGHT + "Password Validator v0.2.0 (zxcvbn powered)")
23
+ print(Fore.CYAN + "Type .exit() to quit\n")
24
+
25
+ while True:
26
+ try:
27
+ pwd = getpass.getpass("Enter password: ")
28
+ except KeyboardInterrupt:
29
+ print("\n" + Fore.YELLOW + "Program interrupted by user.")
30
+ return
31
+ except EOFError:
32
+ print("\n" + Fore.YELLOW + "Input stream closed.")
33
+ return
34
+
35
+ if pwd == ".exit()":
36
+ print(Fore.YELLOW + "Goodbye.")
37
+ return
38
+
39
+ result = validator.evaluate(pwd)
40
+
41
+ # Validation output
42
+ if result["valid"]:
43
+ print(Fore.GREEN + Style.BRIGHT + "✓ Valid password")
44
+ else:
45
+ print(Fore.RED + Style.BRIGHT + "✗ Invalid password")
46
+ for err in result["errors"]:
47
+ print(Fore.RED + Style.BRIGHT + f" • {err}")
48
+
49
+ # Strength output
50
+ score_color = {
51
+ 0: Fore.RED + Style.BRIGHT,
52
+ 1: Fore.RED + Style.BRIGHT,
53
+ 2: Fore.YELLOW + Style.BRIGHT,
54
+ 3: Fore.GREEN + Style.BRIGHT,
55
+ 4: Fore.GREEN + Style.BRIGHT,
56
+ }[result["score"]]
57
+
58
+ print(f"\n{score_color }Strength: {result['label']}")
59
+ for msg in result["feedback"]:
60
+ print(Fore.YELLOW + f" • {msg}")
61
+
62
+ print()
63
+
64
+ if __name__ == "__main__":
65
+ main()
@@ -0,0 +1,124 @@
1
+ import logging
2
+ from typing import List, Tuple, Dict, Any, Optional
3
+ from zxcvbn import zxcvbn
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ MAX_ZXCVBN_LEN = 72
8
+
9
+ class PasswordValidator:
10
+
11
+ def __init__(
12
+ self,
13
+ min_length: int = 12,
14
+ max_length: int = 64,
15
+ require_uppercase: bool = True,
16
+ require_lowercase: bool = True,
17
+ require_digit: bool = True,
18
+ require_special: bool = True,
19
+ special_chars: str = "!@#$%^&*",
20
+ allow_spaces: bool = False,
21
+ banned_words: Optional[List[str]] = None,
22
+ ):
23
+
24
+ # Defensive Config Validation
25
+ if min_length <= 0 or max_length < min_length:
26
+ raise ValueError("Invalid length constraints")
27
+
28
+ self.min_length = min_length
29
+ self.max_length = max_length
30
+ self.require_uppercase = require_uppercase
31
+ self.require_lowercase = require_lowercase
32
+ self.require_digit = require_digit
33
+ self.require_special = require_special
34
+ self.special_chars = special_chars
35
+ self.allow_spaces = allow_spaces
36
+ self.banned_words = [w.lower() for w in (banned_words or [])]
37
+
38
+ # ===== Validation =====
39
+ def validate(self, pwd: str) -> Tuple[bool, List[str]]:
40
+ errors: List[str] = []
41
+
42
+ if not isinstance(pwd, str):
43
+ raise ValueError("Password must be a string")
44
+
45
+ if not pwd:
46
+ return False, ["Password cannot be empty"]
47
+
48
+ if not self.allow_spaces and any(c.isspace() for c in pwd):
49
+ errors.append("Password must not contain spaces")
50
+
51
+ if len(pwd) < self.min_length:
52
+ errors.append(f"Password must be at least {self.min_length} characters long")
53
+
54
+ if len(pwd) > self.max_length:
55
+ errors.append(f"Password cannot exceed {self.max_length} characters")
56
+
57
+ if self.require_uppercase and not any(c.isupper() for c in pwd):
58
+ errors.append("Missing uppercase letter")
59
+
60
+ if self.require_lowercase and not any(c.islower() for c in pwd):
61
+ errors.append("Missing lowercase letter")
62
+
63
+ if self.require_digit and not any(c.isdigit() for c in pwd):
64
+ errors.append("Missing digit")
65
+
66
+ if self.require_special and not any(c in self.special_chars for c in pwd):
67
+ errors.append("Missing special character")
68
+
69
+ if self._contains_banned_word(pwd):
70
+ errors.append("Contains a banned word")
71
+
72
+ return len(errors) == 0, errors
73
+
74
+ def _contains_banned_word(self, pwd: str) -> bool:
75
+ pwd_lower = pwd.lower()
76
+ return any(word in pwd_lower for word in self.banned_words)
77
+
78
+ # ===== Strength (zxcvbn) =====
79
+ def estimate_strength(self, pwd: str) -> Dict[str, Any]:
80
+ """
81
+ Uses zxcvbn for realistic password strength estimation.
82
+ Truncates passwords >72 characters to prevent ValueError.
83
+ Always returns a dictionary, even if zxcvbn misbehaves.
84
+ """
85
+ if not isinstance(pwd, str) or not pwd:
86
+ return {"score": 0, "label": "Very Weak", "feedback": ["Empty or invalid password"]}
87
+
88
+ truncated_pwd = pwd[:MAX_ZXCVBN_LEN]
89
+
90
+ try:
91
+ result = zxcvbn(truncated_pwd)
92
+
93
+ # Defensive: ensure result is a dict
94
+ if not isinstance(result, dict):
95
+ raise ValueError(f"Unexpected zxcvbn result type: {type(result)}")
96
+ except Exception as e:
97
+ return {"score": 0, "label": "Very Weak", "feedback": [f"Strength calculation error: {str(e)}"]}
98
+
99
+ labels = ["Very Weak", "Weak", "Moderate", "Strong", "Very Strong"]
100
+ score = result.get("score", 0) if isinstance(result, dict) else 0
101
+
102
+ feedback = []
103
+ fb_dict = result.get("feedback", {}) if isinstance(result, dict) else {}
104
+ warning = fb_dict.get("warning")
105
+ suggestions = fb_dict.get("suggestions", [])
106
+
107
+ if warning:
108
+ feedback.append(warning)
109
+ feedback.extend(suggestions)
110
+
111
+ return {"score": score, "label": labels[score], "feedback": feedback}
112
+
113
+ # ===== Public API =====
114
+ def evaluate(self, pwd: str) -> Dict[str, Any]:
115
+ valid, errors = self.validate(pwd)
116
+ strength = self.estimate_strength(pwd)
117
+
118
+ return {
119
+ "valid": valid,
120
+ "errors": errors,
121
+ "score": strength["score"],
122
+ "label": strength["label"],
123
+ "feedback": strength["feedback"]
124
+ }