iam-policy-validator 1.4.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.
Potentially problematic release.
This version of iam-policy-validator might be problematic. Click here for more details.
- iam_policy_validator-1.4.0.dist-info/METADATA +1022 -0
- iam_policy_validator-1.4.0.dist-info/RECORD +56 -0
- iam_policy_validator-1.4.0.dist-info/WHEEL +4 -0
- iam_policy_validator-1.4.0.dist-info/entry_points.txt +2 -0
- iam_policy_validator-1.4.0.dist-info/licenses/LICENSE +21 -0
- iam_validator/__init__.py +27 -0
- iam_validator/__main__.py +11 -0
- iam_validator/__version__.py +7 -0
- iam_validator/checks/__init__.py +27 -0
- iam_validator/checks/action_condition_enforcement.py +727 -0
- iam_validator/checks/action_resource_constraint.py +151 -0
- iam_validator/checks/action_validation.py +72 -0
- iam_validator/checks/condition_key_validation.py +70 -0
- iam_validator/checks/policy_size.py +151 -0
- iam_validator/checks/policy_type_validation.py +299 -0
- iam_validator/checks/principal_validation.py +282 -0
- iam_validator/checks/resource_validation.py +108 -0
- iam_validator/checks/security_best_practices.py +536 -0
- iam_validator/checks/sid_uniqueness.py +170 -0
- iam_validator/checks/utils/__init__.py +1 -0
- iam_validator/checks/utils/policy_level_checks.py +143 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +252 -0
- iam_validator/checks/utils/wildcard_expansion.py +87 -0
- iam_validator/commands/__init__.py +25 -0
- iam_validator/commands/analyze.py +434 -0
- iam_validator/commands/base.py +48 -0
- iam_validator/commands/cache.py +392 -0
- iam_validator/commands/download_services.py +260 -0
- iam_validator/commands/post_to_pr.py +86 -0
- iam_validator/commands/validate.py +539 -0
- iam_validator/core/__init__.py +14 -0
- iam_validator/core/access_analyzer.py +666 -0
- iam_validator/core/access_analyzer_report.py +643 -0
- iam_validator/core/aws_fetcher.py +880 -0
- iam_validator/core/aws_global_conditions.py +137 -0
- iam_validator/core/check_registry.py +469 -0
- iam_validator/core/cli.py +134 -0
- iam_validator/core/config_loader.py +452 -0
- iam_validator/core/defaults.py +393 -0
- iam_validator/core/formatters/__init__.py +27 -0
- iam_validator/core/formatters/base.py +147 -0
- iam_validator/core/formatters/console.py +59 -0
- iam_validator/core/formatters/csv.py +170 -0
- iam_validator/core/formatters/enhanced.py +434 -0
- iam_validator/core/formatters/html.py +672 -0
- iam_validator/core/formatters/json.py +33 -0
- iam_validator/core/formatters/markdown.py +63 -0
- iam_validator/core/formatters/sarif.py +187 -0
- iam_validator/core/models.py +298 -0
- iam_validator/core/policy_checks.py +656 -0
- iam_validator/core/policy_loader.py +396 -0
- iam_validator/core/pr_commenter.py +338 -0
- iam_validator/core/report.py +859 -0
- iam_validator/integrations/__init__.py +28 -0
- iam_validator/integrations/github_integration.py +795 -0
- iam_validator/integrations/ms_teams.py +442 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"""Analyze command for IAM Policy Validator using AWS IAM Access Analyzer."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from iam_validator.commands.base import Command
|
|
7
|
+
from iam_validator.core.access_analyzer import (
|
|
8
|
+
AccessAnalyzerReport,
|
|
9
|
+
PolicyType,
|
|
10
|
+
ResourceType,
|
|
11
|
+
validate_policies_with_analyzer,
|
|
12
|
+
)
|
|
13
|
+
from iam_validator.core.access_analyzer_report import AccessAnalyzerReportFormatter
|
|
14
|
+
from iam_validator.core.policy_checks import validate_policies
|
|
15
|
+
from iam_validator.core.policy_loader import PolicyLoader
|
|
16
|
+
from iam_validator.core.report import ReportGenerator
|
|
17
|
+
from iam_validator.integrations.github_integration import GitHubIntegration
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AnalyzeCommand(Command):
|
|
21
|
+
"""Command to analyze IAM policies using AWS IAM Access Analyzer."""
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def name(self) -> str:
|
|
25
|
+
return "analyze"
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def help(self) -> str:
|
|
29
|
+
return "Analyze IAM policies using AWS IAM Access Analyzer"
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def epilog(self) -> str:
|
|
33
|
+
return """
|
|
34
|
+
Examples:
|
|
35
|
+
# Analyze identity-based policies
|
|
36
|
+
iam-validator analyze --path ./policies/ --policy-type IDENTITY_POLICY
|
|
37
|
+
|
|
38
|
+
# Analyze resource-based policies (e.g., S3 bucket policies)
|
|
39
|
+
iam-validator analyze --path ./bucket-policies/ --policy-type RESOURCE_POLICY
|
|
40
|
+
|
|
41
|
+
# Analyze multiple paths
|
|
42
|
+
iam-validator analyze --path ./iam/ --path ./s3-policies/ --path bucket-policy.json
|
|
43
|
+
|
|
44
|
+
# Use specific AWS region and profile
|
|
45
|
+
iam-validator analyze --path ./policies/ --region us-west-2 --profile prod
|
|
46
|
+
|
|
47
|
+
# Run full validation if Access Analyzer passes
|
|
48
|
+
iam-validator analyze --path ./policies/ --run-all-checks
|
|
49
|
+
|
|
50
|
+
# Custom Policy Checks:
|
|
51
|
+
|
|
52
|
+
# Check that policies do NOT grant specific dangerous actions
|
|
53
|
+
iam-validator analyze --path ./policies/ --check-access-not-granted s3:DeleteBucket s3:DeleteObject
|
|
54
|
+
|
|
55
|
+
# Check that policies do NOT grant access to specific resources
|
|
56
|
+
iam-validator analyze --path ./policies/ \\
|
|
57
|
+
--check-access-not-granted s3:PutObject \\
|
|
58
|
+
--check-access-resources "arn:aws:s3:::production-bucket/*"
|
|
59
|
+
|
|
60
|
+
# Check that updated policies don't grant new access
|
|
61
|
+
iam-validator analyze --path ./new-policy.json \\
|
|
62
|
+
--check-no-new-access ./old-policy.json
|
|
63
|
+
|
|
64
|
+
# Check that S3 bucket policies don't allow public access
|
|
65
|
+
iam-validator analyze --path ./bucket-policy.json \\
|
|
66
|
+
--policy-type RESOURCE_POLICY \\
|
|
67
|
+
--check-no-public-access \\
|
|
68
|
+
--public-access-resource-type "AWS::S3::Bucket"
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
72
|
+
"""Add analyze command arguments."""
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--path",
|
|
75
|
+
"-p",
|
|
76
|
+
required=True,
|
|
77
|
+
action="append",
|
|
78
|
+
dest="paths",
|
|
79
|
+
help="Path to IAM policy file or directory (can be specified multiple times)",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--policy-type",
|
|
84
|
+
"-t",
|
|
85
|
+
choices=["IDENTITY_POLICY", "RESOURCE_POLICY", "SERVICE_CONTROL_POLICY"],
|
|
86
|
+
default="IDENTITY_POLICY",
|
|
87
|
+
help="Type of IAM policy to validate (default: IDENTITY_POLICY)",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"--region",
|
|
92
|
+
default="us-east-1",
|
|
93
|
+
help="AWS region for Access Analyzer (default: us-east-1)",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"--profile",
|
|
98
|
+
help="AWS profile to use for Access Analyzer",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
parser.add_argument(
|
|
102
|
+
"--format",
|
|
103
|
+
"-f",
|
|
104
|
+
choices=["console", "json", "markdown"],
|
|
105
|
+
default="console",
|
|
106
|
+
help="Output format (default: console)",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
"--output",
|
|
111
|
+
"-o",
|
|
112
|
+
help="Output file path (only for json/markdown formats)",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
"--no-recursive",
|
|
117
|
+
action="store_true",
|
|
118
|
+
help="Don't recursively search directories",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
parser.add_argument(
|
|
122
|
+
"--fail-on-warnings",
|
|
123
|
+
action="store_true",
|
|
124
|
+
help="Fail validation if warnings are found (default: only fail on errors)",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
parser.add_argument(
|
|
128
|
+
"--github-comment",
|
|
129
|
+
action="store_true",
|
|
130
|
+
help="Post validation results as GitHub PR comment",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
parser.add_argument(
|
|
134
|
+
"--github-review",
|
|
135
|
+
action="store_true",
|
|
136
|
+
help="Create line-specific review comments on PR (requires --github-comment)",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
"--run-all-checks",
|
|
141
|
+
action="store_true",
|
|
142
|
+
help="Run full validation checks if Access Analyzer passes",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Custom policy check arguments
|
|
146
|
+
parser.add_argument(
|
|
147
|
+
"--check-access-not-granted",
|
|
148
|
+
nargs="+",
|
|
149
|
+
metavar="ACTION",
|
|
150
|
+
help="Check that policy does NOT grant specific actions (e.g., s3:DeleteBucket)",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
parser.add_argument(
|
|
154
|
+
"--check-access-resources",
|
|
155
|
+
nargs="+",
|
|
156
|
+
metavar="RESOURCE",
|
|
157
|
+
help="Resources to check with --check-access-not-granted (e.g., arn:aws:s3:::bucket/*)",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
parser.add_argument(
|
|
161
|
+
"--check-no-new-access",
|
|
162
|
+
metavar="EXISTING_POLICY",
|
|
163
|
+
help="Path to existing policy to compare against for new access checks",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
parser.add_argument(
|
|
167
|
+
"--check-no-public-access",
|
|
168
|
+
action="store_true",
|
|
169
|
+
help="Check that resource policy does not allow public access (for RESOURCE_POLICY type only)",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
parser.add_argument(
|
|
173
|
+
"--public-access-resource-type",
|
|
174
|
+
nargs="+",
|
|
175
|
+
choices=[
|
|
176
|
+
"all", # Special value to check all types
|
|
177
|
+
# Storage
|
|
178
|
+
"AWS::S3::Bucket",
|
|
179
|
+
"AWS::S3::AccessPoint",
|
|
180
|
+
"AWS::S3::MultiRegionAccessPoint",
|
|
181
|
+
"AWS::S3Express::DirectoryBucket",
|
|
182
|
+
"AWS::S3Express::AccessPoint",
|
|
183
|
+
"AWS::S3::Glacier",
|
|
184
|
+
"AWS::S3Outposts::Bucket",
|
|
185
|
+
"AWS::S3Outposts::AccessPoint",
|
|
186
|
+
"AWS::S3Tables::TableBucket",
|
|
187
|
+
"AWS::S3Tables::Table",
|
|
188
|
+
"AWS::EFS::FileSystem",
|
|
189
|
+
# Database
|
|
190
|
+
"AWS::DynamoDB::Table",
|
|
191
|
+
"AWS::DynamoDB::Stream",
|
|
192
|
+
"AWS::OpenSearchService::Domain",
|
|
193
|
+
# Messaging & Streaming
|
|
194
|
+
"AWS::Kinesis::Stream",
|
|
195
|
+
"AWS::Kinesis::StreamConsumer",
|
|
196
|
+
"AWS::SNS::Topic",
|
|
197
|
+
"AWS::SQS::Queue",
|
|
198
|
+
# Security & Secrets
|
|
199
|
+
"AWS::KMS::Key",
|
|
200
|
+
"AWS::SecretsManager::Secret",
|
|
201
|
+
"AWS::IAM::AssumeRolePolicyDocument",
|
|
202
|
+
# Compute
|
|
203
|
+
"AWS::Lambda::Function",
|
|
204
|
+
# API & Integration
|
|
205
|
+
"AWS::ApiGateway::RestApi",
|
|
206
|
+
# DevOps & Management
|
|
207
|
+
"AWS::CodeArtifact::Domain",
|
|
208
|
+
"AWS::Backup::BackupVault",
|
|
209
|
+
"AWS::CloudTrail::Dashboard",
|
|
210
|
+
"AWS::CloudTrail::EventDataStore",
|
|
211
|
+
],
|
|
212
|
+
default=["AWS::S3::Bucket"],
|
|
213
|
+
help="Resource type(s) for public access check. Use 'all' to check all 29 types. Example: all OR AWS::S3::Bucket AWS::Lambda::Function",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
parser.add_argument(
|
|
217
|
+
"--verbose",
|
|
218
|
+
"-v",
|
|
219
|
+
action="store_true",
|
|
220
|
+
help="Enable verbose logging",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
async def execute(self, args: argparse.Namespace) -> int:
|
|
224
|
+
"""Execute the analyze command."""
|
|
225
|
+
try:
|
|
226
|
+
# Map string to PolicyType enum
|
|
227
|
+
policy_type = PolicyType[args.policy_type]
|
|
228
|
+
|
|
229
|
+
# Build custom checks configuration
|
|
230
|
+
custom_checks = self._build_custom_checks(args)
|
|
231
|
+
|
|
232
|
+
# Validate policies with Access Analyzer
|
|
233
|
+
report = validate_policies_with_analyzer(
|
|
234
|
+
path=args.paths,
|
|
235
|
+
region=args.region,
|
|
236
|
+
policy_type=policy_type,
|
|
237
|
+
profile=args.profile if hasattr(args, "profile") else None,
|
|
238
|
+
recursive=not args.no_recursive,
|
|
239
|
+
custom_checks=custom_checks,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Generate report
|
|
243
|
+
formatter = AccessAnalyzerReportFormatter()
|
|
244
|
+
|
|
245
|
+
# Output results
|
|
246
|
+
if args.format == "console":
|
|
247
|
+
formatter.print_console_report(report)
|
|
248
|
+
elif args.format == "json":
|
|
249
|
+
if args.output:
|
|
250
|
+
formatter.save_json_report(report, args.output)
|
|
251
|
+
logging.info(f"JSON report saved to {args.output}")
|
|
252
|
+
else:
|
|
253
|
+
print(formatter.generate_json_report(report))
|
|
254
|
+
elif args.format == "markdown":
|
|
255
|
+
if args.output:
|
|
256
|
+
formatter.save_markdown_report(report, args.output)
|
|
257
|
+
logging.info(f"Markdown report saved to {args.output}")
|
|
258
|
+
else:
|
|
259
|
+
print(formatter.generate_markdown_report(report))
|
|
260
|
+
|
|
261
|
+
# Post to GitHub if configured
|
|
262
|
+
if args.github_comment:
|
|
263
|
+
async with GitHubIntegration() as github:
|
|
264
|
+
success = await self._post_to_github(github, report, formatter)
|
|
265
|
+
if not success:
|
|
266
|
+
logging.error("Failed to post Access Analyzer results to GitHub PR")
|
|
267
|
+
|
|
268
|
+
# Determine exit code based on validation results
|
|
269
|
+
if args.fail_on_warnings:
|
|
270
|
+
exit_code = 0 if report.total_findings == 0 else 1
|
|
271
|
+
else:
|
|
272
|
+
exit_code = 0 if report.total_errors == 0 else 1
|
|
273
|
+
|
|
274
|
+
# If Access Analyzer passes and --run-all-checks is set, run full validation
|
|
275
|
+
if exit_code == 0 and getattr(args, "run_all_checks", False):
|
|
276
|
+
exit_code = await self._run_full_validation(args)
|
|
277
|
+
|
|
278
|
+
return exit_code
|
|
279
|
+
|
|
280
|
+
except ValueError as e:
|
|
281
|
+
logging.error(f"Validation error: {e}")
|
|
282
|
+
return 1
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logging.error(f"Access Analyzer validation failed: {e}", exc_info=args.verbose)
|
|
285
|
+
return 1
|
|
286
|
+
|
|
287
|
+
def _build_custom_checks(self, args: argparse.Namespace) -> dict | None:
|
|
288
|
+
"""Build custom checks configuration from CLI arguments.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
args: Parsed command line arguments
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Dictionary with custom check configurations, or None if no checks specified
|
|
295
|
+
"""
|
|
296
|
+
custom_checks = {}
|
|
297
|
+
|
|
298
|
+
# Check access not granted
|
|
299
|
+
if hasattr(args, "check_access_not_granted") and args.check_access_not_granted:
|
|
300
|
+
custom_checks["access_not_granted"] = {
|
|
301
|
+
"actions": args.check_access_not_granted,
|
|
302
|
+
}
|
|
303
|
+
if hasattr(args, "check_access_resources") and args.check_access_resources:
|
|
304
|
+
custom_checks["access_not_granted"]["resources"] = args.check_access_resources
|
|
305
|
+
|
|
306
|
+
# Check no new access
|
|
307
|
+
if hasattr(args, "check_no_new_access") and args.check_no_new_access:
|
|
308
|
+
# Load existing policy
|
|
309
|
+
from iam_validator.core.policy_loader import PolicyLoader
|
|
310
|
+
|
|
311
|
+
loader = PolicyLoader()
|
|
312
|
+
existing_policies_loaded = loader.load_from_path(
|
|
313
|
+
args.check_no_new_access, recursive=False
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
if existing_policies_loaded:
|
|
317
|
+
# Build dict of existing policies
|
|
318
|
+
existing_policies_dict = {
|
|
319
|
+
file_path: policy.model_dump(by_alias=True, exclude_none=True)
|
|
320
|
+
for file_path, policy in existing_policies_loaded
|
|
321
|
+
}
|
|
322
|
+
custom_checks["no_new_access"] = {"existing_policies": existing_policies_dict}
|
|
323
|
+
else:
|
|
324
|
+
logging.warning(f"Could not load existing policy from {args.check_no_new_access}")
|
|
325
|
+
|
|
326
|
+
# Check no public access
|
|
327
|
+
if hasattr(args, "check_no_public_access") and args.check_no_public_access:
|
|
328
|
+
resource_types = getattr(args, "public_access_resource_type", ["AWS::S3::Bucket"])
|
|
329
|
+
# Support both single string and list
|
|
330
|
+
if isinstance(resource_types, str):
|
|
331
|
+
resource_types = [resource_types]
|
|
332
|
+
|
|
333
|
+
# Expand "all" to all resource types
|
|
334
|
+
if "all" in resource_types:
|
|
335
|
+
resource_types = [member.value for member in ResourceType]
|
|
336
|
+
logging.info(
|
|
337
|
+
f"Checking all {len(resource_types)} supported resource types for public access"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Convert to ResourceType enums
|
|
341
|
+
resource_type_enums = [ResourceType(rt) for rt in resource_types]
|
|
342
|
+
custom_checks["no_public_access"] = {"resource_types": resource_type_enums}
|
|
343
|
+
|
|
344
|
+
return custom_checks if custom_checks else None
|
|
345
|
+
|
|
346
|
+
async def _run_full_validation(self, args: argparse.Namespace) -> int:
|
|
347
|
+
"""Run full validation after Access Analyzer passes."""
|
|
348
|
+
logging.info("Access Analyzer validation passed. Running full validation checks...")
|
|
349
|
+
|
|
350
|
+
# Load policies again for full validation
|
|
351
|
+
loader = PolicyLoader()
|
|
352
|
+
policies = loader.load_from_paths(args.paths, recursive=not args.no_recursive)
|
|
353
|
+
|
|
354
|
+
if not policies:
|
|
355
|
+
logging.error(f"No valid IAM policies found in: {', '.join(args.paths)}")
|
|
356
|
+
return 1
|
|
357
|
+
|
|
358
|
+
# Run full validation
|
|
359
|
+
results = await validate_policies(policies)
|
|
360
|
+
|
|
361
|
+
# Generate report
|
|
362
|
+
generator = ReportGenerator()
|
|
363
|
+
validation_report = generator.generate_report(results)
|
|
364
|
+
|
|
365
|
+
# Output results
|
|
366
|
+
if args.format == "console":
|
|
367
|
+
logging.info("\n=== Full Validation Results ===")
|
|
368
|
+
generator.print_console_report(validation_report)
|
|
369
|
+
elif args.format == "json":
|
|
370
|
+
# Don't output JSON again if already saved
|
|
371
|
+
if not args.output:
|
|
372
|
+
print(generator.generate_json_report(validation_report))
|
|
373
|
+
elif args.format == "markdown":
|
|
374
|
+
# Don't output markdown again if already saved
|
|
375
|
+
if not args.output:
|
|
376
|
+
print(generator.generate_github_comment(validation_report))
|
|
377
|
+
|
|
378
|
+
# Post to GitHub if configured
|
|
379
|
+
if args.github_comment:
|
|
380
|
+
from iam_validator.core.pr_commenter import PRCommenter
|
|
381
|
+
|
|
382
|
+
async with GitHubIntegration() as github:
|
|
383
|
+
commenter = PRCommenter(github)
|
|
384
|
+
success = await commenter.post_findings_to_pr(
|
|
385
|
+
validation_report,
|
|
386
|
+
create_review=getattr(args, "github_review", False),
|
|
387
|
+
add_summary_comment=True,
|
|
388
|
+
)
|
|
389
|
+
if not success:
|
|
390
|
+
logging.error("Failed to post full validation to GitHub PR")
|
|
391
|
+
|
|
392
|
+
# Update exit code based on full validation
|
|
393
|
+
if args.fail_on_warnings:
|
|
394
|
+
return 0 if validation_report.total_issues == 0 else 1
|
|
395
|
+
else:
|
|
396
|
+
return 0 if validation_report.invalid_policies == 0 else 1
|
|
397
|
+
|
|
398
|
+
async def _post_to_github(
|
|
399
|
+
self,
|
|
400
|
+
github: GitHubIntegration,
|
|
401
|
+
report: AccessAnalyzerReport,
|
|
402
|
+
formatter: AccessAnalyzerReportFormatter,
|
|
403
|
+
) -> bool:
|
|
404
|
+
"""Post Access Analyzer results to GitHub PR."""
|
|
405
|
+
if not github.is_configured():
|
|
406
|
+
logging.error(
|
|
407
|
+
"GitHub integration not configured. "
|
|
408
|
+
"Set GITHUB_TOKEN, GITHUB_REPOSITORY, and GITHUB_PR_NUMBER"
|
|
409
|
+
)
|
|
410
|
+
return False
|
|
411
|
+
|
|
412
|
+
# Generate markdown comment (single part for now)
|
|
413
|
+
markdown_content = formatter.generate_markdown_report(report)
|
|
414
|
+
|
|
415
|
+
# Add identifier for updating existing comments
|
|
416
|
+
identifier = "<!-- iam-access-analyzer-validator -->"
|
|
417
|
+
|
|
418
|
+
# Check if content is too large for single comment
|
|
419
|
+
if len(markdown_content) > 60000:
|
|
420
|
+
# Split into multiple parts
|
|
421
|
+
# For simplicity, we use a basic split for Access Analyzer reports
|
|
422
|
+
# TODO: Implement proper multi-part splitting for Access Analyzer reports
|
|
423
|
+
logging.warning("Access Analyzer report is large, posting as single comment")
|
|
424
|
+
|
|
425
|
+
# Post or update comment
|
|
426
|
+
logging.info("Posting Access Analyzer results to PR...")
|
|
427
|
+
success = await github.update_or_create_comment(markdown_content, identifier)
|
|
428
|
+
|
|
429
|
+
if success:
|
|
430
|
+
logging.info("Successfully posted Access Analyzer results to PR")
|
|
431
|
+
else:
|
|
432
|
+
logging.error("Failed to post Access Analyzer results to PR")
|
|
433
|
+
|
|
434
|
+
return success
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Base command class for CLI commands."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Command(ABC):
|
|
8
|
+
"""Base class for all CLI commands."""
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def name(self) -> str:
|
|
13
|
+
"""Command name (e.g., 'validate', 'post-to-pr')."""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def help(self) -> str:
|
|
19
|
+
"""Short help text for the command."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def epilog(self) -> str | None:
|
|
24
|
+
"""Optional epilog with examples."""
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Add command-specific arguments to the parser.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
parser: ArgumentParser for this command
|
|
34
|
+
"""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
async def execute(self, args: argparse.Namespace) -> int:
|
|
39
|
+
"""
|
|
40
|
+
Execute the command.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
args: Parsed command-line arguments
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Exit code (0 for success, non-zero for failure)
|
|
47
|
+
"""
|
|
48
|
+
pass
|