iam-policy-validator 1.14.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 (106) hide show
  1. iam_policy_validator-1.14.0.dist-info/METADATA +782 -0
  2. iam_policy_validator-1.14.0.dist-info/RECORD +106 -0
  3. iam_policy_validator-1.14.0.dist-info/WHEEL +4 -0
  4. iam_policy_validator-1.14.0.dist-info/entry_points.txt +2 -0
  5. iam_policy_validator-1.14.0.dist-info/licenses/LICENSE +21 -0
  6. iam_validator/__init__.py +27 -0
  7. iam_validator/__main__.py +11 -0
  8. iam_validator/__version__.py +9 -0
  9. iam_validator/checks/__init__.py +45 -0
  10. iam_validator/checks/action_condition_enforcement.py +1442 -0
  11. iam_validator/checks/action_resource_matching.py +472 -0
  12. iam_validator/checks/action_validation.py +67 -0
  13. iam_validator/checks/condition_key_validation.py +88 -0
  14. iam_validator/checks/condition_type_mismatch.py +257 -0
  15. iam_validator/checks/full_wildcard.py +62 -0
  16. iam_validator/checks/mfa_condition_check.py +105 -0
  17. iam_validator/checks/policy_size.py +114 -0
  18. iam_validator/checks/policy_structure.py +556 -0
  19. iam_validator/checks/policy_type_validation.py +331 -0
  20. iam_validator/checks/principal_validation.py +708 -0
  21. iam_validator/checks/resource_validation.py +135 -0
  22. iam_validator/checks/sensitive_action.py +438 -0
  23. iam_validator/checks/service_wildcard.py +98 -0
  24. iam_validator/checks/set_operator_validation.py +153 -0
  25. iam_validator/checks/sid_uniqueness.py +146 -0
  26. iam_validator/checks/trust_policy_validation.py +509 -0
  27. iam_validator/checks/utils/__init__.py +17 -0
  28. iam_validator/checks/utils/action_parser.py +149 -0
  29. iam_validator/checks/utils/policy_level_checks.py +190 -0
  30. iam_validator/checks/utils/sensitive_action_matcher.py +293 -0
  31. iam_validator/checks/utils/wildcard_expansion.py +86 -0
  32. iam_validator/checks/wildcard_action.py +58 -0
  33. iam_validator/checks/wildcard_resource.py +374 -0
  34. iam_validator/commands/__init__.py +31 -0
  35. iam_validator/commands/analyze.py +549 -0
  36. iam_validator/commands/base.py +48 -0
  37. iam_validator/commands/cache.py +393 -0
  38. iam_validator/commands/completion.py +471 -0
  39. iam_validator/commands/download_services.py +255 -0
  40. iam_validator/commands/post_to_pr.py +86 -0
  41. iam_validator/commands/query.py +485 -0
  42. iam_validator/commands/validate.py +830 -0
  43. iam_validator/core/__init__.py +13 -0
  44. iam_validator/core/access_analyzer.py +671 -0
  45. iam_validator/core/access_analyzer_report.py +640 -0
  46. iam_validator/core/aws_fetcher.py +29 -0
  47. iam_validator/core/aws_service/__init__.py +21 -0
  48. iam_validator/core/aws_service/cache.py +108 -0
  49. iam_validator/core/aws_service/client.py +205 -0
  50. iam_validator/core/aws_service/fetcher.py +641 -0
  51. iam_validator/core/aws_service/parsers.py +149 -0
  52. iam_validator/core/aws_service/patterns.py +51 -0
  53. iam_validator/core/aws_service/storage.py +291 -0
  54. iam_validator/core/aws_service/validators.py +380 -0
  55. iam_validator/core/check_registry.py +679 -0
  56. iam_validator/core/cli.py +134 -0
  57. iam_validator/core/codeowners.py +245 -0
  58. iam_validator/core/condition_validators.py +626 -0
  59. iam_validator/core/config/__init__.py +81 -0
  60. iam_validator/core/config/aws_api.py +35 -0
  61. iam_validator/core/config/aws_global_conditions.py +160 -0
  62. iam_validator/core/config/category_suggestions.py +181 -0
  63. iam_validator/core/config/check_documentation.py +390 -0
  64. iam_validator/core/config/condition_requirements.py +258 -0
  65. iam_validator/core/config/config_loader.py +670 -0
  66. iam_validator/core/config/defaults.py +739 -0
  67. iam_validator/core/config/principal_requirements.py +421 -0
  68. iam_validator/core/config/sensitive_actions.py +672 -0
  69. iam_validator/core/config/service_principals.py +132 -0
  70. iam_validator/core/config/wildcards.py +127 -0
  71. iam_validator/core/constants.py +149 -0
  72. iam_validator/core/diff_parser.py +325 -0
  73. iam_validator/core/finding_fingerprint.py +131 -0
  74. iam_validator/core/formatters/__init__.py +27 -0
  75. iam_validator/core/formatters/base.py +147 -0
  76. iam_validator/core/formatters/console.py +68 -0
  77. iam_validator/core/formatters/csv.py +171 -0
  78. iam_validator/core/formatters/enhanced.py +481 -0
  79. iam_validator/core/formatters/html.py +672 -0
  80. iam_validator/core/formatters/json.py +33 -0
  81. iam_validator/core/formatters/markdown.py +64 -0
  82. iam_validator/core/formatters/sarif.py +251 -0
  83. iam_validator/core/ignore_patterns.py +297 -0
  84. iam_validator/core/ignore_processor.py +309 -0
  85. iam_validator/core/ignored_findings.py +400 -0
  86. iam_validator/core/label_manager.py +197 -0
  87. iam_validator/core/models.py +404 -0
  88. iam_validator/core/policy_checks.py +220 -0
  89. iam_validator/core/policy_loader.py +785 -0
  90. iam_validator/core/pr_commenter.py +780 -0
  91. iam_validator/core/report.py +942 -0
  92. iam_validator/integrations/__init__.py +28 -0
  93. iam_validator/integrations/github_integration.py +1821 -0
  94. iam_validator/integrations/ms_teams.py +442 -0
  95. iam_validator/sdk/__init__.py +220 -0
  96. iam_validator/sdk/arn_matching.py +382 -0
  97. iam_validator/sdk/context.py +222 -0
  98. iam_validator/sdk/exceptions.py +48 -0
  99. iam_validator/sdk/helpers.py +177 -0
  100. iam_validator/sdk/policy_utils.py +451 -0
  101. iam_validator/sdk/query_utils.py +454 -0
  102. iam_validator/sdk/shortcuts.py +283 -0
  103. iam_validator/utils/__init__.py +35 -0
  104. iam_validator/utils/cache.py +105 -0
  105. iam_validator/utils/regex.py +205 -0
  106. iam_validator/utils/terminal.py +22 -0
@@ -0,0 +1,134 @@
1
+ """Command-line interface for IAM Policy Validator."""
2
+
3
+ import argparse
4
+ import asyncio
5
+ import logging
6
+ import os
7
+ import sys
8
+
9
+ from iam_validator.__version__ import __version__
10
+ from iam_validator.commands import ALL_COMMANDS
11
+
12
+
13
+ def setup_logging(log_level: str | None = None, verbose: bool = False) -> None:
14
+ """Setup logging configuration.
15
+
16
+ Args:
17
+ log_level: Log level from CLI argument (debug, info, warning, error, critical)
18
+ verbose: Enable verbose logging (deprecated, use --log-level debug instead)
19
+
20
+ Environment Variables:
21
+ LOG_LEVEL: Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
22
+
23
+ Priority:
24
+ 1. --log-level CLI argument (highest priority)
25
+ 2. LOG_LEVEL environment variable
26
+ 3. --verbose flag (sets DEBUG level)
27
+ 4. Default: WARNING (lowest priority)
28
+ """
29
+ # Check for LOG_LEVEL environment variable
30
+ env_log_level = os.getenv("LOG_LEVEL", "").upper()
31
+
32
+ # Map string to logging level
33
+ level_map = {
34
+ "DEBUG": logging.DEBUG,
35
+ "INFO": logging.INFO,
36
+ "WARNING": logging.WARNING,
37
+ "ERROR": logging.ERROR,
38
+ "CRITICAL": logging.CRITICAL,
39
+ }
40
+
41
+ # Priority: CLI --log-level > LOG_LEVEL env var > --verbose flag > default (WARNING)
42
+ if log_level:
43
+ level = level_map[log_level.upper()]
44
+ elif env_log_level in level_map:
45
+ level = level_map[env_log_level]
46
+ elif verbose:
47
+ level = logging.DEBUG
48
+ else:
49
+ level = logging.WARNING
50
+
51
+ logging.basicConfig(
52
+ level=level,
53
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
54
+ datefmt="%Y-%m-%d %H:%M:%S",
55
+ )
56
+
57
+
58
+ def main() -> int:
59
+ """Main entry point for the CLI.
60
+
61
+ Returns:
62
+ Exit code
63
+ """
64
+ parser = argparse.ArgumentParser(
65
+ description="Validate AWS IAM policies for correctness and security",
66
+ formatter_class=argparse.RawDescriptionHelpFormatter,
67
+ )
68
+
69
+ # Add version argument
70
+ parser.add_argument(
71
+ "--version",
72
+ action="version",
73
+ version=f"iam-validator {__version__}",
74
+ help="Show version information and exit",
75
+ )
76
+
77
+ # Add global log level argument
78
+ parser.add_argument(
79
+ "--log-level",
80
+ choices=["debug", "info", "warning", "error", "critical"],
81
+ default=None,
82
+ help="Set logging level (default: warning)",
83
+ )
84
+
85
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
86
+
87
+ # Register all commands
88
+ command_map = {}
89
+ for command in ALL_COMMANDS:
90
+ cmd_parser = subparsers.add_parser(
91
+ command.name,
92
+ help=command.help,
93
+ formatter_class=argparse.RawDescriptionHelpFormatter,
94
+ epilog=command.epilog,
95
+ )
96
+ command.add_arguments(cmd_parser)
97
+ command_map[command.name] = command
98
+
99
+ # Parse arguments
100
+ args = parser.parse_args()
101
+
102
+ if not args.command:
103
+ parser.print_help()
104
+ return 1
105
+
106
+ # Setup logging
107
+ log_level = getattr(args, "log_level", None)
108
+ verbose = getattr(args, "verbose", False)
109
+ setup_logging(log_level, verbose)
110
+
111
+ # Execute command
112
+ try:
113
+ command = command_map[args.command]
114
+ exit_code = asyncio.run(command.execute(args))
115
+ return exit_code
116
+ except KeyboardInterrupt:
117
+ logging.warning("Interrupted by user")
118
+ return 130 # Standard exit code for SIGINT
119
+ except asyncio.CancelledError:
120
+ logging.warning("Operation cancelled")
121
+ return 130
122
+ except FileNotFoundError as e:
123
+ logging.error(f"File not found: {e}")
124
+ return 1
125
+ except PermissionError as e:
126
+ logging.error(f"Permission denied: {e}")
127
+ return 1
128
+ except Exception as e:
129
+ logging.error(f"Unexpected error: {e}", exc_info=True)
130
+ return 1
131
+
132
+
133
+ if __name__ == "__main__":
134
+ sys.exit(main())
@@ -0,0 +1,245 @@
1
+ """CODEOWNERS file parser for GitHub repositories.
2
+
3
+ This module provides functionality to parse GitHub CODEOWNERS files and
4
+ determine which users/teams own specific files. Used to authorize users
5
+ who can ignore validation findings.
6
+
7
+ Reference: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from dataclasses import dataclass, field
14
+ from functools import lru_cache
15
+ from pathlib import PurePosixPath
16
+ from typing import ClassVar
17
+
18
+
19
+ @dataclass(slots=True)
20
+ class CodeOwnerRule:
21
+ """A single rule from a CODEOWNERS file.
22
+
23
+ Attributes:
24
+ pattern: File pattern (glob-style, GitHub CODEOWNERS format)
25
+ owners: List of @users and/or @org/teams
26
+ compiled_pattern: Pre-compiled regex for fast matching
27
+ """
28
+
29
+ pattern: str
30
+ owners: list[str]
31
+ compiled_pattern: re.Pattern[str] | None = field(default=None, repr=False)
32
+
33
+ def __post_init__(self) -> None:
34
+ """Pre-compile the pattern for efficient matching."""
35
+ self.compiled_pattern = _compile_codeowners_pattern(self.pattern)
36
+
37
+
38
+ @lru_cache(maxsize=256)
39
+ def _compile_codeowners_pattern(pattern: str) -> re.Pattern[str]:
40
+ """Compile a CODEOWNERS pattern to regex with caching.
41
+
42
+ CODEOWNERS patterns follow these rules:
43
+ - Patterns starting with / are relative to repo root
44
+ - Patterns without / match any path containing that component
45
+ - * matches anything except /
46
+ - ** matches anything including /
47
+ - Trailing / matches directories
48
+
49
+ Args:
50
+ pattern: CODEOWNERS glob pattern
51
+
52
+ Returns:
53
+ Compiled regex pattern
54
+ """
55
+ # Normalize the pattern
56
+ original_pattern = pattern
57
+ pattern = pattern.strip()
58
+
59
+ # Handle leading slash (anchored to root)
60
+ anchored = pattern.startswith("/")
61
+ if anchored:
62
+ pattern = pattern[1:]
63
+
64
+ # Handle trailing slash (directory match)
65
+ is_dir = pattern.endswith("/")
66
+ if is_dir:
67
+ pattern = pattern[:-1]
68
+
69
+ # Escape special regex characters except * and ?
70
+ pattern = re.escape(pattern)
71
+
72
+ # Convert glob patterns to regex
73
+ # ** matches any number of directories
74
+ pattern = pattern.replace(r"\*\*", "<<<DOUBLE_STAR>>>")
75
+ # * matches anything except /
76
+ pattern = pattern.replace(r"\*", "[^/]*")
77
+ # ** -> match anything
78
+ pattern = pattern.replace("<<<DOUBLE_STAR>>>", ".*")
79
+ # ? matches single character except /
80
+ pattern = pattern.replace(r"\?", "[^/]")
81
+
82
+ # Build final regex
83
+ if anchored:
84
+ # Anchored patterns match from repo root
85
+ regex = f"^{pattern}"
86
+ elif "/" in original_pattern.lstrip("/"):
87
+ # Patterns with / in them are implicitly anchored
88
+ regex = f"^{pattern}"
89
+ else:
90
+ # Patterns without / can match anywhere in path
91
+ regex = f"(^|/){pattern}"
92
+
93
+ if is_dir:
94
+ # Directory patterns match the directory and anything under it
95
+ regex += "(/|$)"
96
+ else:
97
+ # File patterns match exactly or as prefix for directories
98
+ regex += "($|/)"
99
+
100
+ return re.compile(regex)
101
+
102
+
103
+ class CodeOwnersParser:
104
+ """Parser for GitHub CODEOWNERS file format.
105
+
106
+ Parses CODEOWNERS content and provides file-to-owner mapping.
107
+ Uses last-matching-pattern semantics as per GitHub's behavior.
108
+
109
+ Example:
110
+ >>> content = '''
111
+ ... # Default owners
112
+ ... * @default-team
113
+ ... # IAM policies owned by security
114
+ ... /policies/**/*.json @security-team @security-lead
115
+ ... '''
116
+ >>> parser = CodeOwnersParser(content)
117
+ >>> parser.get_owners_for_file("policies/admin/admin.json")
118
+ ['@security-team', '@security-lead']
119
+ """
120
+
121
+ CODEOWNERS_PATHS: ClassVar[list[str]] = [
122
+ "CODEOWNERS",
123
+ ".github/CODEOWNERS",
124
+ "docs/CODEOWNERS",
125
+ ]
126
+
127
+ def __init__(self, content: str) -> None:
128
+ """Initialize parser with CODEOWNERS content.
129
+
130
+ Args:
131
+ content: Raw content of CODEOWNERS file
132
+ """
133
+ self.rules: list[CodeOwnerRule] = []
134
+ self._parse(content)
135
+
136
+ def _parse(self, content: str) -> None:
137
+ """Parse CODEOWNERS file content.
138
+
139
+ Args:
140
+ content: Raw CODEOWNERS file content
141
+ """
142
+ for line in content.splitlines():
143
+ line = line.strip()
144
+
145
+ # Skip empty lines and comments
146
+ if not line or line.startswith("#"):
147
+ continue
148
+
149
+ # Split into pattern and owners
150
+ parts = line.split()
151
+ if len(parts) >= 2:
152
+ pattern = parts[0]
153
+ owners = parts[1:]
154
+ self.rules.append(CodeOwnerRule(pattern=pattern, owners=owners))
155
+ elif len(parts) == 1:
156
+ # Pattern with no owners (unsets ownership)
157
+ self.rules.append(CodeOwnerRule(pattern=parts[0], owners=[]))
158
+
159
+ def get_owners_for_file(self, file_path: str) -> list[str]:
160
+ """Get owners for a specific file path.
161
+
162
+ Uses last-matching-pattern semantics as per GitHub's behavior.
163
+ If multiple patterns match, the last one in the file wins.
164
+
165
+ Args:
166
+ file_path: Path to the file (relative to repo root)
167
+
168
+ Returns:
169
+ List of owners for the file, or empty list if no match
170
+ """
171
+ # Normalize path (remove leading ./ or /)
172
+ file_path = file_path.lstrip("./")
173
+
174
+ # Find all matching rules, last one wins
175
+ owners: list[str] = []
176
+ for rule in self.rules:
177
+ if rule.compiled_pattern and rule.compiled_pattern.search(file_path):
178
+ owners = rule.owners
179
+
180
+ return owners
181
+
182
+ def is_owner(self, username: str, file_path: str) -> bool:
183
+ """Check if a user is an owner of a file.
184
+
185
+ Note: This only checks direct username matches. For team membership,
186
+ use GitHubIntegration.is_user_codeowner() which resolves teams.
187
+
188
+ Args:
189
+ username: GitHub username (with or without @)
190
+ file_path: Path to the file
191
+
192
+ Returns:
193
+ True if user is directly listed as owner
194
+ """
195
+ # Normalize username
196
+ username = username.lstrip("@").lower()
197
+
198
+ owners = self.get_owners_for_file(file_path)
199
+ for owner in owners:
200
+ owner = owner.lstrip("@").lower()
201
+ # Direct username match (not team)
202
+ if "/" not in owner and owner == username:
203
+ return True
204
+
205
+ return False
206
+
207
+ def get_teams_for_file(self, file_path: str) -> list[tuple[str, str]]:
208
+ """Get team owners for a file as (org, team_slug) tuples.
209
+
210
+ Args:
211
+ file_path: Path to the file
212
+
213
+ Returns:
214
+ List of (org, team_slug) tuples
215
+ """
216
+ owners = self.get_owners_for_file(file_path)
217
+ teams: list[tuple[str, str]] = []
218
+
219
+ for owner in owners:
220
+ owner = owner.lstrip("@")
221
+ if "/" in owner:
222
+ parts = owner.split("/", 1)
223
+ if len(parts) == 2:
224
+ teams.append((parts[0], parts[1]))
225
+
226
+ return teams
227
+
228
+
229
+ def normalize_path(path: str) -> str:
230
+ """Normalize a file path for CODEOWNERS matching.
231
+
232
+ Args:
233
+ path: File path (may be absolute or relative)
234
+
235
+ Returns:
236
+ Normalized relative path
237
+ """
238
+ # Convert to posix-style path
239
+ path = str(PurePosixPath(path))
240
+ # Remove leading ./
241
+ if path.startswith("./"):
242
+ path = path[2:]
243
+ # Remove leading /
244
+ path = path.lstrip("/")
245
+ return path