mcp-security-framework 2.0.1__tar.gz → 2.2.0__tar.gz

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.
Files changed (57) hide show
  1. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/PKG-INFO +1 -1
  2. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/__init__.py +1 -1
  3. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/cli/cert_cli.py +69 -52
  4. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/cli/security_cli.py +31 -23
  5. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/core/auth_manager.py +10 -17
  6. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/core/cert_manager.py +139 -23
  7. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/core/rate_limiter.py +3 -3
  8. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/core/security_manager.py +16 -6
  9. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/core/ssl_manager.py +305 -73
  10. mcp_security_framework-2.2.0/mcp_security_framework/examples/cert_inspection_example.py +391 -0
  11. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/examples/comprehensive_example.py +71 -38
  12. mcp_security_framework-2.2.0/mcp_security_framework/examples/crl_retrieval_example.py +385 -0
  13. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/examples/fastapi_example.py +260 -18
  14. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/examples/flask_example.py +20 -3
  15. mcp_security_framework-2.2.0/mcp_security_framework/examples/protected_keys_example.py +297 -0
  16. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/examples/standalone_example.py +42 -22
  17. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/middleware/auth_middleware.py +13 -3
  18. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/middleware/fastapi_auth_middleware.py +4 -27
  19. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/middleware/flask_auth_middleware.py +4 -27
  20. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/schemas/config.py +124 -28
  21. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/schemas/models.py +31 -16
  22. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/schemas/responses.py +54 -13
  23. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/utils/__init__.py +27 -0
  24. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/utils/cert_utils.py +286 -56
  25. mcp_security_framework-2.2.0/mcp_security_framework/utils/crl_client.py +408 -0
  26. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/utils/crypto_utils.py +65 -21
  27. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/utils/datetime_compat.py +15 -10
  28. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/utils/validation_utils.py +34 -8
  29. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework.egg-info/PKG-INFO +1 -1
  30. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework.egg-info/SOURCES.txt +4 -0
  31. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/pyproject.toml +1 -1
  32. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/LICENSE +0 -0
  33. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/README.md +0 -0
  34. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/cli/__init__.py +0 -0
  35. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/constants.py +0 -0
  36. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/core/__init__.py +0 -0
  37. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/core/adapter_wrapper.py +0 -0
  38. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/core/audit_logger.py +0 -0
  39. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/core/permission_manager.py +0 -0
  40. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/core/security_adapter.py +0 -0
  41. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/examples/__init__.py +0 -0
  42. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/examples/adapter_example.py +0 -0
  43. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/examples/gateway_example.py +0 -0
  44. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/examples/microservice_example.py +0 -0
  45. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/middleware/__init__.py +0 -0
  46. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/middleware/fastapi_middleware.py +0 -0
  47. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/middleware/flask_middleware.py +0 -0
  48. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/middleware/mtls_middleware.py +0 -0
  49. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/middleware/rate_limit_middleware.py +0 -0
  50. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/middleware/security_middleware.py +0 -0
  51. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/schemas/__init__.py +0 -0
  52. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework/schemas/operation_context.py +0 -0
  53. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework.egg-info/dependency_links.txt +0 -0
  54. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework.egg-info/entry_points.txt +0 -0
  55. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework.egg-info/requires.txt +0 -0
  56. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/mcp_security_framework.egg-info/top_level.txt +0 -0
  57. {mcp_security_framework-2.0.1 → mcp_security_framework-2.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-security-framework
3
- Version: 2.0.1
3
+ Version: 2.2.0
4
4
  Summary: Universal security framework for microservices with SSL/TLS, authentication, authorization, and rate limiting. Added extensible security adapter system for custom operation validation, structured audit logging, and operation context support. Comprehensive test coverage with 1104+ tests. Fully backward compatible. Requires cryptography>=42.0.0 for certificate operations.
5
5
  Author-email: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
6
6
  Maintainer-email: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
@@ -79,7 +79,7 @@ from mcp_security_framework.schemas.responses import (
79
79
  )
80
80
 
81
81
  # Version information
82
- __version__ = "2.0.1"
82
+ __version__ = "2.2.0"
83
83
  __author__ = "Vasiliy Zdanovskiy"
84
84
  __email__ = "vasilyvz@gmail.com"
85
85
  __license__ = "MIT"
@@ -27,7 +27,7 @@ License: MIT
27
27
 
28
28
  import json
29
29
  import os
30
- from typing import Optional
30
+ from typing import List, Optional, Tuple
31
31
 
32
32
  import click
33
33
 
@@ -47,7 +47,7 @@ from ..schemas.config import (
47
47
  )
48
48
  @click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
49
49
  @click.pass_context
50
- def cert_cli(ctx, config_path: Optional[str], verbose: bool):
50
+ def cert_cli(ctx: click.Context, config_path: Optional[str], verbose: bool) -> None:
51
51
  """
52
52
  Certificate Management CLI
53
53
 
@@ -88,7 +88,7 @@ def cert_cli(ctx, config_path: Optional[str], verbose: bool):
88
88
  # _get_cert_manager(ctx), only once its own body actually runs.
89
89
 
90
90
 
91
- def _get_cert_manager(ctx) -> CertificateManager:
91
+ def _get_cert_manager(ctx: click.Context) -> CertificateManager:
92
92
  """
93
93
  Lazily construct (and cache) the CertificateManager for this invocation.
94
94
 
@@ -110,10 +110,11 @@ def _get_cert_manager(ctx) -> CertificateManager:
110
110
  """
111
111
  if "cert_manager" not in ctx.obj:
112
112
  ctx.obj["cert_manager"] = CertificateManager(ctx.obj["config"])
113
- return ctx.obj["cert_manager"]
113
+ cert_manager: CertificateManager = ctx.obj["cert_manager"]
114
+ return cert_manager
114
115
 
115
116
 
116
- def _validate_permissions(permissions: tuple) -> list:
117
+ def _validate_permissions(permissions: Tuple[str, ...]) -> List[str]:
117
118
  """
118
119
  Validate permission strings supplied via ``--permissions``.
119
120
 
@@ -179,7 +180,7 @@ def _validate_permissions(permissions: tuple) -> list:
179
180
  @click.option("--key-size", "-k", default=2048, help="RSA key size")
180
181
  @click.pass_context
181
182
  def create_ca(
182
- ctx,
183
+ ctx: click.Context,
183
184
  common_name: str,
184
185
  organization: str,
185
186
  country: str,
@@ -188,7 +189,7 @@ def create_ca(
188
189
  email: Optional[str],
189
190
  validity_years: int,
190
191
  key_size: int,
191
- ):
192
+ ) -> None:
192
193
  """
193
194
  Create a root CA certificate.
194
195
 
@@ -263,7 +264,7 @@ def create_ca(
263
264
  @click.option("--ca-key", required=True, help="Path to the signing CA private key")
264
265
  @click.pass_context
265
266
  def create_server(
266
- ctx,
267
+ ctx: click.Context,
267
268
  common_name: str,
268
269
  organization: str,
269
270
  country: str,
@@ -272,12 +273,12 @@ def create_server(
272
273
  email: Optional[str],
273
274
  validity_days: int,
274
275
  key_size: int,
275
- san: tuple,
276
- key_usage: tuple,
277
- extended_key_usage: tuple,
276
+ san: Tuple[str, ...],
277
+ key_usage: Tuple[str, ...],
278
+ extended_key_usage: Tuple[str, ...],
278
279
  ca_cert: str,
279
280
  ca_key: str,
280
- ):
281
+ ) -> None:
281
282
  """
282
283
  Create a server certificate.
283
284
 
@@ -287,30 +288,40 @@ def create_server(
287
288
  cert_manager = _get_cert_manager(ctx)
288
289
  verbose = ctx.obj["verbose"]
289
290
 
290
- # Create server configuration
291
- server_config_kwargs = {
292
- "common_name": common_name,
293
- "organization": organization,
294
- "country": country,
295
- "state": state,
296
- "locality": locality,
297
- "email": email,
298
- "validity_days": validity_days,
299
- "key_size": key_size,
300
- "subject_alt_names": list(san) if san else [],
301
- "ca_cert_path": ca_cert,
302
- "ca_key_path": ca_key,
303
- }
304
-
305
- # Only include key_usage if provided (to allow pydantic defaults)
306
- if key_usage:
307
- server_config_kwargs["key_usage"] = list(key_usage)
308
-
309
- # Only include extended_key_usage if provided (to allow pydantic defaults)
310
- if extended_key_usage:
311
- server_config_kwargs["extended_key_usage"] = list(extended_key_usage)
312
-
313
- server_config = ServerCertConfig(**server_config_kwargs)
291
+ # Create server configuration.
292
+ #
293
+ # key_usage/extended_key_usage fall back to ServerCertConfig's own
294
+ # field defaults (read from the model rather than duplicated here)
295
+ # when the flag is not given, instead of an empty list -- an empty
296
+ # list would silently strip the default key usage extensions from
297
+ # every certificate that does not pass --key-usage explicitly.
298
+ # Reading the defaults from the model, rather than a **kwargs dict
299
+ # assembled conditionally, also keeps every field's real type
300
+ # (str, Optional[str], int, List[str]) visible to the type checker
301
+ # instead of collapsing them into one imprecise union.
302
+ key_usage_default = ServerCertConfig.model_fields["key_usage"].default
303
+ extended_key_usage_default = ServerCertConfig.model_fields[
304
+ "extended_key_usage"
305
+ ].default
306
+ server_config = ServerCertConfig(
307
+ common_name=common_name,
308
+ organization=organization,
309
+ country=country,
310
+ state=state,
311
+ locality=locality,
312
+ email=email,
313
+ validity_days=validity_days,
314
+ key_size=key_size,
315
+ subject_alt_names=list(san) if san else [],
316
+ key_usage=list(key_usage) if key_usage else list(key_usage_default),
317
+ extended_key_usage=(
318
+ list(extended_key_usage)
319
+ if extended_key_usage
320
+ else list(extended_key_usage_default)
321
+ ),
322
+ ca_cert_path=ca_cert,
323
+ ca_key_path=ca_key,
324
+ )
314
325
 
315
326
  if verbose:
316
327
  click.echo("Creating server certificate with configuration:")
@@ -365,7 +376,7 @@ def create_server(
365
376
  @click.option("--ca-key", required=True, help="Path to the signing CA private key")
366
377
  @click.pass_context
367
378
  def create_client(
368
- ctx,
379
+ ctx: click.Context,
369
380
  common_name: str,
370
381
  organization: str,
371
382
  country: str,
@@ -374,11 +385,11 @@ def create_client(
374
385
  email: Optional[str],
375
386
  validity_days: int,
376
387
  key_size: int,
377
- roles: tuple,
378
- permissions: tuple,
388
+ roles: Tuple[str, ...],
389
+ permissions: Tuple[str, ...],
379
390
  ca_cert: str,
380
391
  ca_key: str,
381
- ):
392
+ ) -> None:
382
393
  """
383
394
  Create a client certificate.
384
395
 
@@ -443,7 +454,9 @@ def create_client(
443
454
  help="Path to CRL file for revocation check",
444
455
  )
445
456
  @click.pass_context
446
- def validate(ctx, cert_path: str, ca_cert: Optional[str], crl: Optional[str]):
457
+ def validate(
458
+ ctx: click.Context, cert_path: str, ca_cert: Optional[str], crl: Optional[str]
459
+ ) -> None:
447
460
  """
448
461
  Validate a certificate.
449
462
 
@@ -480,7 +493,7 @@ def validate(ctx, cert_path: str, ca_cert: Optional[str], crl: Optional[str]):
480
493
  @cert_cli.command()
481
494
  @click.argument("cert_path", type=click.Path(exists=True))
482
495
  @click.pass_context
483
- def info(ctx, cert_path: str):
496
+ def info(ctx: click.Context, cert_path: str) -> None:
484
497
  """
485
498
  Display certificate information.
486
499
 
@@ -543,7 +556,7 @@ def info(ctx, cert_path: str):
543
556
  )
544
557
  @click.pass_context
545
558
  def create_intermediate_ca(
546
- ctx,
559
+ ctx: click.Context,
547
560
  common_name: str,
548
561
  organization: str,
549
562
  country: str,
@@ -554,7 +567,7 @@ def create_intermediate_ca(
554
567
  key_size: int,
555
568
  parent_ca_cert: str,
556
569
  parent_ca_key: str,
557
- ):
570
+ ) -> None:
558
571
  """
559
572
  Create an intermediate CA certificate.
560
573
 
@@ -612,8 +625,12 @@ def create_intermediate_ca(
612
625
  @click.option("--validity-days", "-d", default=30, help="CRL validity in days")
613
626
  @click.pass_context
614
627
  def create_crl(
615
- ctx, ca_cert: str, ca_key: str, output: Optional[str], validity_days: int
616
- ):
628
+ ctx: click.Context,
629
+ ca_cert: str,
630
+ ca_key: str,
631
+ output: Optional[str],
632
+ validity_days: int,
633
+ ) -> None:
617
634
  """
618
635
  Create a Certificate Revocation List (CRL).
619
636
 
@@ -653,7 +670,7 @@ def create_crl(
653
670
  @click.argument("serial_number")
654
671
  @click.option("--reason", "-r", default="unspecified", help="Reason for revocation")
655
672
  @click.pass_context
656
- def revoke(ctx, serial_number: str, reason: str):
673
+ def revoke(ctx: click.Context, serial_number: str, reason: str) -> None:
657
674
  """
658
675
  Revoke a certificate.
659
676
 
@@ -690,7 +707,7 @@ def revoke(ctx, serial_number: str, reason: str):
690
707
  help="Path to CRL file for revocation check",
691
708
  )
692
709
  @click.pass_context
693
- def check_revocation(ctx, cert_path: str, crl: Optional[str]):
710
+ def check_revocation(ctx: click.Context, cert_path: str, crl: Optional[str]) -> None:
694
711
  """
695
712
  Check if certificate is revoked according to CRL.
696
713
 
@@ -726,7 +743,7 @@ def check_revocation(ctx, cert_path: str, crl: Optional[str]):
726
743
  help="Path to CRL file for detailed revocation check",
727
744
  )
728
745
  @click.pass_context
729
- def revocation_info(ctx, cert_path: str, crl: Optional[str]):
746
+ def revocation_info(ctx: click.Context, cert_path: str, crl: Optional[str]) -> None:
730
747
  """
731
748
  Get detailed revocation information for certificate.
732
749
 
@@ -742,7 +759,7 @@ def revocation_info(ctx, cert_path: str, crl: Optional[str]):
742
759
  if crl:
743
760
  click.echo(f"Using CRL file: {crl}")
744
761
 
745
- # Get detailed revocation information
762
+ # Get detailed revocation information.
746
763
  revocation_info = cert_manager.validate_certificate_against_crl(cert_path, crl)
747
764
 
748
765
  click.echo(f"Certificate Serial Number: {revocation_info['serial_number']}")
@@ -765,7 +782,7 @@ def revocation_info(ctx, cert_path: str, crl: Optional[str]):
765
782
  @cert_cli.command()
766
783
  @click.argument("crl_path", type=click.Path(exists=True))
767
784
  @click.pass_context
768
- def crl_info(ctx, crl_path: str):
785
+ def crl_info(ctx: click.Context, crl_path: str) -> None:
769
786
  """
770
787
  Display CRL information.
771
788
 
@@ -807,7 +824,7 @@ def crl_info(ctx, crl_path: str):
807
824
  @cert_cli.command()
808
825
  @click.argument("crl_path", type=click.Path(exists=True))
809
826
  @click.pass_context
810
- def validate_crl(ctx, crl_path: str):
827
+ def validate_crl(ctx: click.Context, crl_path: str) -> None:
811
828
  """
812
829
  Validate CRL file.
813
830
 
@@ -27,7 +27,7 @@ License: MIT
27
27
  import json
28
28
  import os
29
29
  from datetime import datetime
30
- from typing import Optional
30
+ from typing import Any, Dict, Optional, Tuple
31
31
 
32
32
  import click
33
33
 
@@ -47,7 +47,7 @@ from ..schemas.config import (
47
47
  )
48
48
  @click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
49
49
  @click.pass_context
50
- def security_cli(ctx, config_path: Optional[str], verbose: bool):
50
+ def security_cli(ctx: click.Context, config_path: Optional[str], verbose: bool) -> None:
51
51
  """
52
52
  Security Management CLI
53
53
 
@@ -90,7 +90,7 @@ def security_cli(ctx, config_path: Optional[str], verbose: bool):
90
90
  # _get_security_manager(ctx), only once its own body actually runs.
91
91
 
92
92
 
93
- def _get_security_manager(ctx) -> SecurityManager:
93
+ def _get_security_manager(ctx: click.Context) -> SecurityManager:
94
94
  """
95
95
  Lazily construct (and cache) the SecurityManager for this invocation.
96
96
 
@@ -113,12 +113,13 @@ def _get_security_manager(ctx) -> SecurityManager:
113
113
  """
114
114
  if "security_manager" not in ctx.obj:
115
115
  ctx.obj["security_manager"] = SecurityManager(ctx.obj["config"])
116
- return ctx.obj["security_manager"]
116
+ security_manager: SecurityManager = ctx.obj["security_manager"]
117
+ return security_manager
117
118
 
118
119
 
119
120
  @security_cli.group()
120
121
  @click.pass_context
121
- def auth(ctx):
122
+ def auth(ctx: click.Context) -> None:
122
123
  """
123
124
  Authentication operations.
124
125
 
@@ -132,7 +133,7 @@ def auth(ctx):
132
133
  @click.option("--username", "-u", required=True, help="Username for the API key")
133
134
  @click.option("--api-key", "-k", required=True, help="API key value")
134
135
  @click.pass_context
135
- def add_api_key(ctx, username: str, api_key: str):
136
+ def add_api_key(ctx: click.Context, username: str, api_key: str) -> None:
136
137
  """
137
138
  Add an API key for a user.
138
139
 
@@ -163,7 +164,7 @@ def add_api_key(ctx, username: str, api_key: str):
163
164
  @auth.command()
164
165
  @click.option("--username", "-u", required=True, help="Username to remove API key for")
165
166
  @click.pass_context
166
- def remove_api_key(ctx, username: str):
167
+ def remove_api_key(ctx: click.Context, username: str) -> None:
167
168
  """
168
169
  Remove an API key for a user.
169
170
 
@@ -194,7 +195,7 @@ def remove_api_key(ctx, username: str):
194
195
  @auth.command()
195
196
  @click.option("--api-key", "-k", required=True, help="API key to test")
196
197
  @click.pass_context
197
- def test_api_key(ctx, api_key: str):
198
+ def test_api_key(ctx: click.Context, api_key: str) -> None:
198
199
  """
199
200
  Test API key authentication.
200
201
 
@@ -229,7 +230,7 @@ def test_api_key(ctx, api_key: str):
229
230
  @auth.command()
230
231
  @click.option("--token", "-t", required=True, help="JWT token to test")
231
232
  @click.pass_context
232
- def test_jwt(ctx, token: str):
233
+ def test_jwt(ctx: click.Context, token: str) -> None:
233
234
  """
234
235
  Test JWT token authentication.
235
236
 
@@ -263,7 +264,7 @@ def test_jwt(ctx, token: str):
263
264
 
264
265
  @security_cli.group()
265
266
  @click.pass_context
266
- def permissions(ctx):
267
+ def permissions(ctx: click.Context) -> None:
267
268
  """
268
269
  Permission management operations.
269
270
 
@@ -285,7 +286,7 @@ def permissions(ctx):
285
286
  help="Permissions to check (can be specified multiple times)",
286
287
  )
287
288
  @click.pass_context
288
- def check(ctx, username: str, permissions: tuple):
289
+ def check(ctx: click.Context, username: str, permissions: Tuple[str, ...]) -> None:
289
290
  """
290
291
  Check user permissions.
291
292
 
@@ -334,7 +335,7 @@ def check(ctx, username: str, permissions: tuple):
334
335
  @permissions.command()
335
336
  @click.option("--role", "-r", required=True, help="Role name")
336
337
  @click.pass_context
337
- def list_role_permissions(ctx, role: str):
338
+ def list_role_permissions(ctx: click.Context, role: str) -> None:
338
339
  """
339
340
  List permissions for a role.
340
341
 
@@ -364,7 +365,7 @@ def list_role_permissions(ctx, role: str):
364
365
 
365
366
  @security_cli.group()
366
367
  @click.pass_context
367
- def rate_limit(ctx):
368
+ def rate_limit(ctx: click.Context) -> None:
368
369
  """
369
370
  Rate limiting operations.
370
371
 
@@ -382,7 +383,7 @@ def rate_limit(ctx):
382
383
  help="Rate limit identifier (IP, user ID, etc.)",
383
384
  )
384
385
  @click.pass_context
385
- def rate_limit_check(ctx, identifier: str):
386
+ def rate_limit_check(ctx: click.Context, identifier: str) -> None:
386
387
  """
387
388
  Check rate limit status.
388
389
 
@@ -414,7 +415,7 @@ def rate_limit_check(ctx, identifier: str):
414
415
  "--identifier", "-i", required=True, help="Rate limit identifier to reset"
415
416
  )
416
417
  @click.pass_context
417
- def reset(ctx, identifier: str):
418
+ def reset(ctx: click.Context, identifier: str) -> None:
418
419
  """
419
420
  Reset rate limit for an identifier.
420
421
 
@@ -442,7 +443,7 @@ def reset(ctx, identifier: str):
442
443
  "--identifier", "-i", required=True, help="Rate limit identifier to get status for"
443
444
  )
444
445
  @click.pass_context
445
- def rate_limit_status(ctx, identifier: str):
446
+ def rate_limit_status(ctx: click.Context, identifier: str) -> None:
446
447
  """
447
448
  Get rate limit status for an identifier.
448
449
 
@@ -472,7 +473,7 @@ def rate_limit_status(ctx, identifier: str):
472
473
 
473
474
  @security_cli.group()
474
475
  @click.pass_context
475
- def config(ctx):
476
+ def config(ctx: click.Context) -> None:
476
477
  """
477
478
  Configuration management operations.
478
479
 
@@ -484,7 +485,7 @@ def config(ctx):
484
485
 
485
486
  @config.command()
486
487
  @click.pass_context
487
- def validate(ctx):
488
+ def validate(ctx: click.Context) -> None:
488
489
  """
489
490
  Validate security configuration.
490
491
 
@@ -536,7 +537,7 @@ def validate(ctx):
536
537
  "--output", "-o", type=click.Path(), help="Output file path (default: stdout)"
537
538
  )
538
539
  @click.pass_context
539
- def export(ctx, output: Optional[str]):
540
+ def export(ctx: click.Context, output: Optional[str]) -> None:
540
541
  """
541
542
  Export security configuration.
542
543
 
@@ -573,7 +574,7 @@ def export(ctx, output: Optional[str]):
573
574
  "--template", "-t", is_flag=True, help="Generate template roles configuration"
574
575
  )
575
576
  @click.pass_context
576
- def generate_roles(ctx, output: Optional[str], template: bool):
577
+ def generate_roles(ctx: click.Context, output: Optional[str], template: bool) -> None:
577
578
  """
578
579
  Generate roles configuration file.
579
580
 
@@ -651,7 +652,7 @@ def generate_roles(ctx, output: Optional[str], template: bool):
651
652
  help="Output format for audit report",
652
653
  )
653
654
  @click.pass_context
654
- def security_audit(ctx, output: Optional[str], format: str):
655
+ def security_audit(ctx: click.Context, output: Optional[str], format: str) -> None:
655
656
  """
656
657
  Perform security audit.
657
658
 
@@ -667,7 +668,14 @@ def security_audit(ctx, output: Optional[str], format: str):
667
668
  click.echo("Performing security audit...")
668
669
 
669
670
  # Perform security audit
670
- audit_results = {
671
+ #
672
+ # This report is a genuinely heterogeneous JSON-shaped structure:
673
+ # values are bools, strings, lists, dicts, and Nones. It is built once
674
+ # and either serialised (json.dumps) or rendered as text below, so a
675
+ # precise per-key TypedDict would buy nothing here. Dict[str, Any] is
676
+ # the honest, minimal-loss type for exactly this shape -- not a
677
+ # stand-in for a type that could otherwise be pinned down.
678
+ audit_results: Dict[str, Any] = {
671
679
  "timestamp": datetime.now().isoformat(),
672
680
  "configuration": {
673
681
  "authentication": {
@@ -849,7 +857,7 @@ def security_audit(ctx, output: Optional[str], format: str):
849
857
 
850
858
  @security_cli.command()
851
859
  @click.pass_context
852
- def status(ctx):
860
+ def status(ctx: click.Context) -> None:
853
861
  """
854
862
  Display security status.
855
863
 
@@ -46,6 +46,7 @@ from ..utils.crypto_utils import (
46
46
  from ..utils.datetime_compat import (
47
47
  get_not_valid_after_utc,
48
48
  )
49
+ from .cert_manager import CertificateValidationError
49
50
 
50
51
 
51
52
  class AuthManager:
@@ -697,7 +698,8 @@ class AuthManager:
697
698
  },
698
699
  )
699
700
  raise CertificateValidationError(
700
- f"Certificate authentication failed: {str(e)}"
701
+ f"Certificate authentication failed: {str(e)}",
702
+ error_code=-32004,
701
703
  )
702
704
 
703
705
  def create_jwt_token(self, user_data: Dict) -> str:
@@ -1087,19 +1089,10 @@ class JWTValidationError(Exception):
1087
1089
  super().__init__(self.message)
1088
1090
 
1089
1091
 
1090
- class CertificateValidationError(Exception):
1091
- """Raised when certificate validation fails."""
1092
-
1093
- def __init__(self, message: str, error_code: int = -32004):
1094
- """
1095
- Initialize CertificateValidationError.
1096
-
1097
- Args:
1098
- message (str): Human-readable error message describing
1099
- the certificate validation failure.
1100
- error_code (int): Error code for the certificate validation
1101
- error. Defaults to -32004.
1102
- """
1103
- self.message = message
1104
- self.error_code = error_code
1105
- super().__init__(self.message)
1092
+ # ``CertificateValidationError`` is defined once, in ``cert_manager``, and
1093
+ # re-exported here so that ``from .auth_manager import CertificateValidationError``
1094
+ # keeps working. Every module in this package that raises a certificate
1095
+ # validation failure must raise (and every caller must be able to catch) the
1096
+ # SAME class -- see ``cert_manager.CertificateValidationError`` for the
1097
+ # rationale and for why the per-site error code is passed at construction
1098
+ # instead of being encoded as a second, look-alike class.