gitwebhooks 2.1.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.
Files changed (47) hide show
  1. gitwebhooks/__init__.py +18 -0
  2. gitwebhooks/__main__.py +10 -0
  3. gitwebhooks/auth/__init__.py +20 -0
  4. gitwebhooks/auth/custom.py +40 -0
  5. gitwebhooks/auth/factory.py +73 -0
  6. gitwebhooks/auth/gitee.py +86 -0
  7. gitwebhooks/auth/github.py +52 -0
  8. gitwebhooks/auth/gitlab.py +29 -0
  9. gitwebhooks/auth/verifier.py +34 -0
  10. gitwebhooks/cli/__init__.py +107 -0
  11. gitwebhooks/cli/config.py +216 -0
  12. gitwebhooks/cli/init_wizard.py +701 -0
  13. gitwebhooks/cli/prompts.py +275 -0
  14. gitwebhooks/cli/service.py +374 -0
  15. gitwebhooks/config/__init__.py +17 -0
  16. gitwebhooks/config/loader.py +170 -0
  17. gitwebhooks/config/models.py +145 -0
  18. gitwebhooks/config/registry.py +104 -0
  19. gitwebhooks/config/server.py +85 -0
  20. gitwebhooks/handlers/__init__.py +22 -0
  21. gitwebhooks/handlers/base.py +107 -0
  22. gitwebhooks/handlers/custom.py +73 -0
  23. gitwebhooks/handlers/factory.py +83 -0
  24. gitwebhooks/handlers/gitee.py +113 -0
  25. gitwebhooks/handlers/github.py +43 -0
  26. gitwebhooks/handlers/gitlab.py +43 -0
  27. gitwebhooks/handlers/request.py +248 -0
  28. gitwebhooks/logging/__init__.py +10 -0
  29. gitwebhooks/logging/setup.py +57 -0
  30. gitwebhooks/main/__init__.py +117 -0
  31. gitwebhooks/main/__main__.py +12 -0
  32. gitwebhooks/models/__init__.py +14 -0
  33. gitwebhooks/models/provider.py +22 -0
  34. gitwebhooks/models/request.py +61 -0
  35. gitwebhooks/models/result.py +40 -0
  36. gitwebhooks/server.py +128 -0
  37. gitwebhooks/utils/__init__.py +14 -0
  38. gitwebhooks/utils/constants.py +55 -0
  39. gitwebhooks/utils/exceptions.py +34 -0
  40. gitwebhooks/utils/executor.py +34 -0
  41. gitwebhooks/utils/systemd.py +274 -0
  42. gitwebhooks-2.1.0.dist-info/METADATA +494 -0
  43. gitwebhooks-2.1.0.dist-info/RECORD +47 -0
  44. gitwebhooks-2.1.0.dist-info/WHEEL +5 -0
  45. gitwebhooks-2.1.0.dist-info/entry_points.txt +2 -0
  46. gitwebhooks-2.1.0.dist-info/licenses/LICENSE +21 -0
  47. gitwebhooks-2.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,18 @@
1
+ """Git Webhooks Server - Modular package structure.
2
+
3
+ Automated deployment server supporting GitHub, Gitee, GitLab, and custom webhooks.
4
+ """
5
+
6
+ __version__ = '2.0.0'
7
+
8
+ # Export main classes for convenient import paths
9
+ from .server import WebhookServer
10
+ from .models.provider import Provider
11
+ from .models.request import WebhookRequest
12
+
13
+ __all__ = [
14
+ '__version__',
15
+ 'WebhookServer',
16
+ 'Provider',
17
+ 'WebhookRequest',
18
+ ]
@@ -0,0 +1,10 @@
1
+ """Python module entry point
2
+
3
+ Supports running the server using `python3 -m gitwebhooks`.
4
+ """
5
+
6
+ from gitwebhooks.main import main
7
+ import sys
8
+
9
+ if __name__ == '__main__':
10
+ sys.exit(main())
@@ -0,0 +1,20 @@
1
+ """Authentication and signature verification module
2
+
3
+ Verifies signatures and tokens for webhook requests.
4
+ """
5
+
6
+ from .verifier import SignatureVerifier
7
+ from .github import GithubSignatureVerifier
8
+ from .gitee import GiteeSignatureVerifier
9
+ from .gitlab import GitlabTokenVerifier
10
+ from .custom import CustomTokenVerifier
11
+ from .factory import VerifierFactory
12
+
13
+ __all__ = [
14
+ 'SignatureVerifier',
15
+ 'GithubSignatureVerifier',
16
+ 'GiteeSignatureVerifier',
17
+ 'GitlabTokenVerifier',
18
+ 'CustomTokenVerifier',
19
+ 'VerifierFactory',
20
+ ]
@@ -0,0 +1,40 @@
1
+ """Custom token verifier
2
+
3
+ Verifies tokens from custom webhooks.
4
+ """
5
+
6
+ from gitwebhooks.auth.verifier import SignatureVerifier
7
+ from gitwebhooks.models.result import SignatureVerificationResult
8
+
9
+
10
+ class CustomTokenVerifier(SignatureVerifier):
11
+ """Custom token verifier"""
12
+
13
+ def __init__(self, verify_enabled: bool = True):
14
+ """Initialize verifier
15
+
16
+ Args:
17
+ verify_enabled: Whether verification is enabled (False = always return success)
18
+ """
19
+ self.verify_enabled = verify_enabled
20
+
21
+ def verify(self, payload: bytes, signature: str, secret: str,
22
+ **kwargs) -> SignatureVerificationResult:
23
+ """Verify custom token
24
+
25
+ Args:
26
+ payload: Raw request body bytes (unused)
27
+ signature: Token value from custom header
28
+ secret: Configured secret key
29
+ **kwargs: Unused
30
+
31
+ Returns:
32
+ SignatureVerificationResult instance
33
+ """
34
+ if not self.verify_enabled:
35
+ return SignatureVerificationResult.success()
36
+
37
+ if signature == secret:
38
+ return SignatureVerificationResult.success()
39
+ else:
40
+ return SignatureVerificationResult.failure('Invalid token')
@@ -0,0 +1,73 @@
1
+ """Verifier factory class
2
+
3
+ Creates corresponding verifier instances based on provider type.
4
+ """
5
+
6
+ from gitwebhooks.models.provider import Provider
7
+ from gitwebhooks.auth.verifier import SignatureVerifier
8
+ from gitwebhooks.auth.github import GithubSignatureVerifier
9
+ from gitwebhooks.auth.gitee import GiteeSignatureVerifier
10
+ from gitwebhooks.auth.gitlab import GitlabTokenVerifier
11
+ from gitwebhooks.auth.custom import CustomTokenVerifier
12
+ from gitwebhooks.utils.exceptions import UnsupportedProviderError
13
+
14
+
15
+ class VerifierFactory:
16
+ """Verifier factory class
17
+
18
+ Creates corresponding verifier instances based on provider type
19
+ """
20
+
21
+ _verifiers = {
22
+ Provider.GITHUB: GithubSignatureVerifier(),
23
+ Provider.GITEE: GiteeSignatureVerifier(),
24
+ Provider.GITLAB: GitlabTokenVerifier(),
25
+ Provider.CUSTOM: None, # Custom verifier created dynamically
26
+ }
27
+
28
+ @classmethod
29
+ def get_verifier(cls, provider: Provider,
30
+ verify_enabled: bool = True) -> SignatureVerifier:
31
+ """Get verifier for the specified provider
32
+
33
+ Args:
34
+ provider: Provider type
35
+ verify_enabled: Whether verification is enabled (CUSTOM only)
36
+
37
+ Returns:
38
+ SignatureVerifier instance
39
+
40
+ Raises:
41
+ UnsupportedProviderError: Unsupported provider
42
+ """
43
+ if provider == Provider.CUSTOM:
44
+ return CustomTokenVerifier(verify_enabled=verify_enabled)
45
+
46
+ verifier = cls._verifiers.get(provider)
47
+ if verifier is None:
48
+ raise UnsupportedProviderError(f'No verifier for provider: {provider}')
49
+ return verifier
50
+
51
+ @classmethod
52
+ def create_github_verifier(cls) -> GithubSignatureVerifier:
53
+ """Create GitHub verifier (convenience method)"""
54
+ return GithubSignatureVerifier()
55
+
56
+ @classmethod
57
+ def create_gitee_verifier(cls) -> GiteeSignatureVerifier:
58
+ """Create Gitee verifier (convenience method)"""
59
+ return GiteeSignatureVerifier()
60
+
61
+ @classmethod
62
+ def create_gitlab_verifier(cls) -> GitlabTokenVerifier:
63
+ """Create GitLab verifier (convenience method)"""
64
+ return GitlabTokenVerifier()
65
+
66
+ @classmethod
67
+ def create_custom_verifier(cls, verify_enabled: bool = True) -> CustomTokenVerifier:
68
+ """Create custom verifier (convenience method)
69
+
70
+ Args:
71
+ verify_enabled: Whether verification is enabled
72
+ """
73
+ return CustomTokenVerifier(verify_enabled=verify_enabled)
@@ -0,0 +1,86 @@
1
+ """Gitee HMAC-SHA256/password verifier
2
+
3
+ Verifies signatures or passwords from Gitee webhooks.
4
+
5
+ According to Gitee official documentation:
6
+ https://help.gitee.com/webhook/how-to-verify-webhook-keys
7
+
8
+ Signature generation algorithm:
9
+ 1. sign_string = timestamp + "\\n" + secret
10
+ 2. hmac_sha256 = HmacSHA256(sign_string)
11
+ 3. signature = base64(urlEncode(hmac_sha256))
12
+
13
+ Note: The Gitee signature does NOT include the request payload.
14
+ """
15
+
16
+ import base64
17
+ import hashlib
18
+ import hmac
19
+
20
+ from gitwebhooks.auth.verifier import SignatureVerifier
21
+ from gitwebhooks.models.result import SignatureVerificationResult
22
+
23
+
24
+ class GiteeSignatureVerifier(SignatureVerifier):
25
+ """Gitee HMAC-SHA256/password verifier"""
26
+
27
+ HASH_ALGORITHM = hashlib.sha256
28
+
29
+ def verify(self, payload: bytes, signature: str, secret: str,
30
+ **kwargs) -> SignatureVerificationResult:
31
+ """Verify Gitee signature or password
32
+
33
+ Gitee sends X-Gitee-Timestamp in both signature and password modes.
34
+ To distinguish between them:
35
+ 1. First try password validation (direct comparison)
36
+ 2. If password fails and timestamp exists, try signature validation
37
+
38
+ This approach works because:
39
+ - Passwords are typically short plaintext without special chars
40
+ - Signatures are Base64 encoded, containing chars like '+', '/', '='
41
+
42
+ Args:
43
+ payload: Raw request body bytes (not used in Gitee signature)
44
+ signature: X-Gitee-Token header value
45
+ secret: Webhook secret/password
46
+ **kwargs: May contain 'timestamp' key
47
+
48
+ Returns:
49
+ SignatureVerificationResult instance
50
+ """
51
+ if signature is None:
52
+ return SignatureVerificationResult.failure('Missing signature or password')
53
+
54
+ # Step 1: Try password validation first (direct comparison)
55
+ if hmac.compare_digest(signature, secret):
56
+ return SignatureVerificationResult.success()
57
+
58
+ # Step 2: If password failed, try signature validation
59
+ timestamp = kwargs.get('timestamp')
60
+ if timestamp is not None:
61
+ # Verify signature
62
+ try:
63
+ timestamp_int = int(timestamp)
64
+ except ValueError:
65
+ return SignatureVerificationResult.failure('Invalid timestamp format')
66
+
67
+ # Gitee signature: timestamp + "\n" + secret
68
+ # Reference: https://help.gitee.com/webhook/how-to-verify-webhook-keys
69
+ sign_string = f'{timestamp_int}\n{secret}'
70
+ secret_bytes = secret.encode('utf-8')
71
+
72
+ signature_bytes = hmac.new(
73
+ secret_bytes,
74
+ sign_string.encode('utf-8'),
75
+ self.HASH_ALGORITHM
76
+ ).digest()
77
+ # Gitee sends Base64 encoded signature (NOT URL encoded)
78
+ expected_signature = base64.b64encode(signature_bytes).decode('utf-8')
79
+
80
+ if hmac.compare_digest(signature, expected_signature):
81
+ return SignatureVerificationResult.success()
82
+ else:
83
+ return SignatureVerificationResult.failure('Invalid signature')
84
+
85
+ # Password validation failed and no timestamp for signature validation
86
+ return SignatureVerificationResult.failure('Invalid password')
@@ -0,0 +1,52 @@
1
+ """GitHub HMAC-SHA1 signature verifier
2
+
3
+ Verifies HMAC-SHA1 signatures from GitHub webhooks.
4
+ """
5
+
6
+ import hashlib
7
+ import hmac
8
+
9
+ from gitwebhooks.auth.verifier import SignatureVerifier
10
+ from gitwebhooks.models.result import SignatureVerificationResult
11
+
12
+
13
+ class GithubSignatureVerifier(SignatureVerifier):
14
+ """GitHub HMAC-SHA1 signature verifier"""
15
+
16
+ PREFIX = 'sha1='
17
+ HASH_ALGORITHM = hashlib.sha1
18
+
19
+ def verify(self, payload: bytes, signature: str, secret: str,
20
+ **kwargs) -> SignatureVerificationResult:
21
+ """Verify GitHub HMAC-SHA1 signature
22
+
23
+ Args:
24
+ payload: Raw request body bytes
25
+ signature: X-Hub-Signature header value
26
+ secret: Webhook secret
27
+ **kwargs: Unused
28
+
29
+ Returns:
30
+ SignatureVerificationResult instance
31
+ """
32
+ if signature is None:
33
+ return SignatureVerificationResult.failure('Missing signature')
34
+
35
+ if not secret:
36
+ return SignatureVerificationResult.failure('Secret not configured')
37
+
38
+ if not signature.startswith(self.PREFIX):
39
+ return SignatureVerificationResult.failure('Invalid signature format')
40
+
41
+ secret_bytes = secret.encode('utf-8')
42
+ expected_signature = self.PREFIX + hmac.new(
43
+ secret_bytes,
44
+ payload,
45
+ self.HASH_ALGORITHM
46
+ ).hexdigest()
47
+
48
+ # Use constant-time comparison
49
+ if hmac.compare_digest(signature, expected_signature):
50
+ return SignatureVerificationResult.success()
51
+ else:
52
+ return SignatureVerificationResult.failure('Invalid signature')
@@ -0,0 +1,29 @@
1
+ """GitLab token verifier
2
+
3
+ Verifies tokens from GitLab webhooks.
4
+ """
5
+
6
+ from gitwebhooks.auth.verifier import SignatureVerifier
7
+ from gitwebhooks.models.result import SignatureVerificationResult
8
+
9
+
10
+ class GitlabTokenVerifier(SignatureVerifier):
11
+ """GitLab token verifier"""
12
+
13
+ def verify(self, payload: bytes, signature: str, secret: str,
14
+ **kwargs) -> SignatureVerificationResult:
15
+ """Verify GitLab token
16
+
17
+ Args:
18
+ payload: Raw request body bytes (unused)
19
+ signature: X-Gitlab-Token header value
20
+ secret: Webhook token
21
+ **kwargs: Unused
22
+
23
+ Returns:
24
+ SignatureVerificationResult instance
25
+ """
26
+ if signature == secret:
27
+ return SignatureVerificationResult.success()
28
+ else:
29
+ return SignatureVerificationResult.failure('Invalid token')
@@ -0,0 +1,34 @@
1
+ """Signature verifier base class
2
+
3
+ Defines the abstract interface for signature verification.
4
+ """
5
+
6
+ from abc import ABC, abstractmethod
7
+
8
+ from gitwebhooks.models.result import SignatureVerificationResult
9
+
10
+
11
+ class SignatureVerifier(ABC):
12
+ """Signature verifier base class
13
+
14
+ All platform-specific verifiers must inherit from this class and implement the verification method.
15
+ """
16
+
17
+ @abstractmethod
18
+ def verify(self, payload: bytes, signature: str, secret: str,
19
+ **kwargs) -> SignatureVerificationResult:
20
+ """Verify signature
21
+
22
+ Args:
23
+ payload: Raw request body bytes
24
+ signature: Signature string from request
25
+ secret: Configured secret key
26
+ **kwargs: Platform-specific additional parameters (e.g., timestamp)
27
+
28
+ Returns:
29
+ SignatureVerificationResult instance
30
+
31
+ Raises:
32
+ SignatureValidationError: When verification fails (optional)
33
+ """
34
+ pass
@@ -0,0 +1,107 @@
1
+ """CLI subcommands module
2
+
3
+ Provides subcommand registration and implementation for gitwebhooks-cli.
4
+ """
5
+
6
+ from gitwebhooks.cli.service import cmd_install, cmd_uninstall
7
+ from gitwebhooks.cli.config import cmd_init, cmd_view
8
+
9
+
10
+ def register_subparsers(subparsers):
11
+ """Register all CLI subparsers
12
+
13
+ Args:
14
+ subparsers: argparse subparsers object from ArgumentParser.add_subparsers()
15
+
16
+ Returns:
17
+ None
18
+ """
19
+ # Register service subcommand
20
+ register_service_subparser(subparsers)
21
+
22
+ # Register config subcommand
23
+ register_config_subparser(subparsers)
24
+
25
+
26
+ def register_service_subparser(subparsers):
27
+ """Register service subcommand and its actions
28
+
29
+ Args:
30
+ subparsers: argparse subparsers object
31
+
32
+ Returns:
33
+ None
34
+ """
35
+ service_parser = subparsers.add_parser(
36
+ 'service',
37
+ help='Manage systemd service installation'
38
+ )
39
+ service_subparsers = service_parser.add_subparsers(dest='service_action')
40
+
41
+ # install action
42
+ install_parser = service_subparsers.add_parser('install')
43
+ install_parser.add_argument(
44
+ '--force',
45
+ action='store_true',
46
+ help='Force overwrite existing service'
47
+ )
48
+ install_parser.add_argument(
49
+ '-v', '--verbose',
50
+ action='count',
51
+ default=0,
52
+ help='Increase verbosity (can be used: -v, -vv)'
53
+ )
54
+ install_parser.add_argument(
55
+ '--dry-run',
56
+ action='store_true',
57
+ help='Preview service file without installing'
58
+ )
59
+ install_parser.set_defaults(func=cmd_install)
60
+
61
+ # uninstall action
62
+ uninstall_parser = service_subparsers.add_parser('uninstall')
63
+ uninstall_parser.add_argument(
64
+ '--purge',
65
+ action='store_true',
66
+ help='Also remove configuration file'
67
+ )
68
+ uninstall_parser.set_defaults(func=cmd_uninstall)
69
+
70
+
71
+ def register_config_subparser(subparsers):
72
+ """Register config subcommand and its actions
73
+
74
+ Args:
75
+ subparsers: argparse subparsers object
76
+
77
+ Returns:
78
+ None
79
+ """
80
+ config_parser = subparsers.add_parser(
81
+ 'config',
82
+ help='Manage gitwebhooks configuration'
83
+ )
84
+ config_subparsers = config_parser.add_subparsers(dest='config_action')
85
+
86
+ # init action
87
+ init_parser = config_subparsers.add_parser(
88
+ 'init',
89
+ help='Initialize configuration file using interactive wizard'
90
+ )
91
+ init_parser.add_argument(
92
+ 'level',
93
+ nargs='?',
94
+ choices=['system', 'local', 'user'],
95
+ help='Configuration level (system/local/user). If not specified, will be prompted.'
96
+ )
97
+ init_parser.set_defaults(func=cmd_init)
98
+
99
+ # view action
100
+ view_parser = config_subparsers.add_parser('view')
101
+ view_parser.add_argument(
102
+ '-c',
103
+ '--config',
104
+ dest='config',
105
+ help='Path to configuration file (default: auto-detect)'
106
+ )
107
+ view_parser.set_defaults(func=cmd_view)
@@ -0,0 +1,216 @@
1
+ """Configuration CLI commands
2
+
3
+ Provides configuration initialization and viewing commands.
4
+ """
5
+
6
+ import argparse
7
+ import configparser
8
+ import os
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ from gitwebhooks.cli.init_wizard import Wizard
14
+ from gitwebhooks.utils.constants import (
15
+ CONFIG_SEARCH_PATHS,
16
+ SENSITIVE_KEYWORDS,
17
+ COLOR_SENSITIVE,
18
+ COLOR_RESET
19
+ )
20
+
21
+
22
+ def cmd_init(args: argparse.Namespace) -> int:
23
+ """Initialize configuration file using interactive wizard
24
+
25
+ Args:
26
+ args: Parsed command-line arguments
27
+ - level: Configuration level (system/local/user), optional
28
+
29
+ Returns:
30
+ Exit code (0 = success, 1 = error)
31
+ """
32
+ try:
33
+ # Create and run wizard
34
+ level = getattr(args, 'level', None)
35
+ wizard = Wizard(level=level)
36
+ config_path = wizard.run()
37
+ return 0
38
+
39
+ except ValueError as e:
40
+ print(f"错误: {e}", file=sys.stderr)
41
+ return 1
42
+ except PermissionError as e:
43
+ print(f"错误: {e}", file=sys.stderr)
44
+ return 2
45
+ except FileExistsError:
46
+ print("操作已取消", file=sys.stderr)
47
+ return 1
48
+ except KeyboardInterrupt:
49
+ print("\n操作已取消")
50
+ return 0
51
+
52
+
53
+ def locate_config_file(args: argparse.Namespace) -> Optional[Path]:
54
+ """Locate configuration file based on arguments or default priority
55
+
56
+ Args:
57
+ args: Parsed command-line arguments
58
+ - config: User-specified configuration file path (optional)
59
+
60
+ Returns:
61
+ Path to configuration file, or None if not found
62
+ """
63
+ # If user specified a config file with -c, use it
64
+ if hasattr(args, 'config') and args.config:
65
+ config_path = Path(args.config).expanduser()
66
+ if config_path.exists():
67
+ return config_path
68
+ return None
69
+
70
+ # Otherwise, search in priority order
71
+ for path_str in CONFIG_SEARCH_PATHS:
72
+ config_path = Path(path_str).expanduser()
73
+ if config_path.exists():
74
+ return config_path
75
+
76
+ return None
77
+
78
+
79
+ def cmd_view(args: argparse.Namespace) -> int:
80
+ """View configuration file content
81
+
82
+ Args:
83
+ args: Parsed command-line arguments
84
+ - config: User-specified configuration file path (optional)
85
+
86
+ Returns:
87
+ Exit code (0 = success, 1 = error)
88
+ """
89
+ # Locate configuration file
90
+ config_path = locate_config_file(args)
91
+
92
+ if config_path is None:
93
+ # Handle file not found
94
+ if hasattr(args, 'config') and args.config:
95
+ print(f'Error: Configuration file not found: {args.config}', file=sys.stderr)
96
+ else:
97
+ print('Error: No configuration file found.', file=sys.stderr)
98
+ print('\nSearched paths (in priority order):', file=sys.stderr)
99
+ for i, path_str in enumerate(CONFIG_SEARCH_PATHS, 1):
100
+ expanded = Path(path_str).expanduser()
101
+ print(f' {i}. {expanded}', file=sys.stderr)
102
+ print('\nTo create a configuration file, run:', file=sys.stderr)
103
+ print(' gitwebhooks-cli config init', file=sys.stderr)
104
+ return 1
105
+
106
+ # Read and parse configuration
107
+ try:
108
+ parser = configparser.ConfigParser()
109
+ parser.read(config_path, encoding='utf-8')
110
+ except configparser.Error as e:
111
+ print(f'Error: Failed to parse configuration file: {config_path}', file=sys.stderr)
112
+ print(f'\nParsing error: {e}', file=sys.stderr)
113
+ print('\nPlease check the file format and fix any syntax errors.', file=sys.stderr)
114
+ return 1
115
+ except PermissionError:
116
+ print(f'Error: Permission denied: {config_path}', file=sys.stderr)
117
+ print('\nPlease check file permissions or run with appropriate privileges.', file=sys.stderr)
118
+ return 1
119
+
120
+ # Determine source type
121
+ source = 'user-specified' if (hasattr(args, 'config') and args.config) else 'auto-detected'
122
+
123
+ # Format and display output
124
+ print(format_config_header(config_path, source))
125
+ print()
126
+ print(format_config_content(parser))
127
+
128
+ return 0
129
+
130
+
131
+ def format_config_header(config_path: Path, source: str) -> str:
132
+ """Format configuration file header
133
+
134
+ Args:
135
+ config_path: Path to configuration file
136
+ source: Source type ('user-specified' or 'auto-detected')
137
+
138
+ Returns:
139
+ Formatted header string
140
+ """
141
+ # Handle symlinks
142
+ display_path = str(config_path)
143
+ if config_path.is_symlink():
144
+ target = config_path.resolve()
145
+ display_path = f'{config_path} -> {target}'
146
+
147
+ return f'Config File: {display_path} (source: {source})'
148
+
149
+
150
+ def format_config_content(parser: configparser.ConfigParser) -> str:
151
+ """Format configuration content by sections
152
+
153
+ Args:
154
+ parser: ConfigParser instance with loaded configuration
155
+
156
+ Returns:
157
+ Formatted configuration content
158
+ """
159
+ if not parser.sections():
160
+ return '(Configuration file is empty or has no valid sections)'
161
+
162
+ lines = []
163
+ for section in parser.sections():
164
+ lines.append(f'[{section}]')
165
+ for key, value in parser.items(section):
166
+ formatted_value = format_sensitive_field(key, value)
167
+ lines.append(f'{key} = {formatted_value}')
168
+ lines.append('') # Empty line between sections
169
+
170
+ return '\n'.join(lines)
171
+
172
+
173
+ def is_sensitive_key(key: str) -> bool:
174
+ """Check if a configuration key is sensitive
175
+
176
+ Args:
177
+ key: Configuration key name
178
+
179
+ Returns:
180
+ True if key contains sensitive keyword, False otherwise
181
+ """
182
+ key_lower = key.lower()
183
+ return any(keyword in key_lower for keyword in SENSITIVE_KEYWORDS)
184
+
185
+
186
+ def should_use_color() -> bool:
187
+ """Check if color output should be used
188
+
189
+ Returns:
190
+ True if color should be used, False otherwise
191
+ """
192
+ # Check NO_COLOR environment variable
193
+ if os.getenv('NO_COLOR'):
194
+ return False
195
+
196
+ # Check TERM environment variable
197
+ term = os.getenv('TERM', '')
198
+ if term == 'dumb':
199
+ return False
200
+
201
+ return True
202
+
203
+
204
+ def format_sensitive_field(key: str, value: str) -> str:
205
+ """Format a configuration field with sensitive field highlighting
206
+
207
+ Args:
208
+ key: Configuration key name
209
+ value: Configuration value
210
+
211
+ Returns:
212
+ Formatted value with color if sensitive
213
+ """
214
+ if is_sensitive_key(key) and should_use_color():
215
+ return f'{COLOR_SENSITIVE}{value}{COLOR_RESET}'
216
+ return value