iam-policy-validator 1.7.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.7.0.dist-info/METADATA +1057 -0
- iam_policy_validator-1.7.0.dist-info/RECORD +83 -0
- iam_policy_validator-1.7.0.dist-info/WHEEL +4 -0
- iam_policy_validator-1.7.0.dist-info/entry_points.txt +2 -0
- iam_policy_validator-1.7.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 +43 -0
- iam_validator/checks/action_condition_enforcement.py +884 -0
- iam_validator/checks/action_resource_matching.py +441 -0
- iam_validator/checks/action_validation.py +72 -0
- iam_validator/checks/condition_key_validation.py +92 -0
- iam_validator/checks/condition_type_mismatch.py +259 -0
- iam_validator/checks/full_wildcard.py +71 -0
- iam_validator/checks/mfa_condition_check.py +112 -0
- iam_validator/checks/policy_size.py +147 -0
- iam_validator/checks/policy_type_validation.py +305 -0
- iam_validator/checks/principal_validation.py +776 -0
- iam_validator/checks/resource_validation.py +138 -0
- iam_validator/checks/sensitive_action.py +254 -0
- iam_validator/checks/service_wildcard.py +107 -0
- iam_validator/checks/set_operator_validation.py +157 -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 +294 -0
- iam_validator/checks/utils/wildcard_expansion.py +87 -0
- iam_validator/checks/wildcard_action.py +67 -0
- iam_validator/checks/wildcard_resource.py +135 -0
- iam_validator/commands/__init__.py +25 -0
- iam_validator/commands/analyze.py +531 -0
- iam_validator/commands/base.py +48 -0
- iam_validator/commands/cache.py +392 -0
- iam_validator/commands/download_services.py +255 -0
- iam_validator/commands/post_to_pr.py +86 -0
- iam_validator/commands/validate.py +600 -0
- iam_validator/core/__init__.py +14 -0
- iam_validator/core/access_analyzer.py +671 -0
- iam_validator/core/access_analyzer_report.py +640 -0
- iam_validator/core/aws_fetcher.py +940 -0
- iam_validator/core/check_registry.py +607 -0
- iam_validator/core/cli.py +134 -0
- iam_validator/core/condition_validators.py +626 -0
- iam_validator/core/config/__init__.py +81 -0
- iam_validator/core/config/aws_api.py +35 -0
- iam_validator/core/config/aws_global_conditions.py +160 -0
- iam_validator/core/config/category_suggestions.py +104 -0
- iam_validator/core/config/condition_requirements.py +155 -0
- iam_validator/core/config/config_loader.py +472 -0
- iam_validator/core/config/defaults.py +523 -0
- iam_validator/core/config/principal_requirements.py +421 -0
- iam_validator/core/config/sensitive_actions.py +672 -0
- iam_validator/core/config/service_principals.py +95 -0
- iam_validator/core/config/wildcards.py +124 -0
- iam_validator/core/constants.py +74 -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 +440 -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 +251 -0
- iam_validator/core/models.py +327 -0
- iam_validator/core/policy_checks.py +656 -0
- iam_validator/core/policy_loader.py +396 -0
- iam_validator/core/pr_commenter.py +424 -0
- iam_validator/core/report.py +872 -0
- iam_validator/integrations/__init__.py +28 -0
- iam_validator/integrations/github_integration.py +815 -0
- iam_validator/integrations/ms_teams.py +442 -0
- iam_validator/sdk/__init__.py +187 -0
- iam_validator/sdk/arn_matching.py +382 -0
- iam_validator/sdk/context.py +222 -0
- iam_validator/sdk/exceptions.py +48 -0
- iam_validator/sdk/helpers.py +177 -0
- iam_validator/sdk/policy_utils.py +425 -0
- iam_validator/sdk/shortcuts.py +283 -0
- iam_validator/utils/__init__.py +31 -0
- iam_validator/utils/cache.py +105 -0
- iam_validator/utils/regex.py +206 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Default service principals for resource policy validation.
|
|
3
|
+
|
|
4
|
+
These are AWS service principals that are commonly used and considered safe
|
|
5
|
+
in resource-based policies (S3 bucket policies, SNS topic policies, etc.).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Final
|
|
9
|
+
|
|
10
|
+
# ============================================================================
|
|
11
|
+
# Allowed Service Principals
|
|
12
|
+
# ============================================================================
|
|
13
|
+
# These AWS service principals are commonly used in resource policies
|
|
14
|
+
# and are generally considered safe to allow
|
|
15
|
+
|
|
16
|
+
DEFAULT_SERVICE_PRINCIPALS: Final[tuple[str, ...]] = (
|
|
17
|
+
"cloudfront.amazonaws.com",
|
|
18
|
+
"s3.amazonaws.com",
|
|
19
|
+
"sns.amazonaws.com",
|
|
20
|
+
"lambda.amazonaws.com",
|
|
21
|
+
"logs.amazonaws.com",
|
|
22
|
+
"events.amazonaws.com",
|
|
23
|
+
"elasticloadbalancing.amazonaws.com",
|
|
24
|
+
"cloudtrail.amazonaws.com",
|
|
25
|
+
"config.amazonaws.com",
|
|
26
|
+
"backup.amazonaws.com",
|
|
27
|
+
"cloudwatch.amazonaws.com",
|
|
28
|
+
"monitoring.amazonaws.com",
|
|
29
|
+
"ec2.amazonaws.com",
|
|
30
|
+
"ecs-tasks.amazonaws.com",
|
|
31
|
+
"eks.amazonaws.com",
|
|
32
|
+
"apigateway.amazonaws.com",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_service_principals() -> tuple[str, ...]:
|
|
37
|
+
"""
|
|
38
|
+
Get tuple of allowed service principals.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Tuple of AWS service principal names
|
|
42
|
+
"""
|
|
43
|
+
return DEFAULT_SERVICE_PRINCIPALS
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_allowed_service_principal(principal: str) -> bool:
|
|
47
|
+
"""
|
|
48
|
+
Check if a principal is an allowed service principal.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
principal: Principal to check (e.g., "lambda.amazonaws.com")
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
True if principal is in allowed list
|
|
55
|
+
|
|
56
|
+
Performance: O(n) but small list (~16 items)
|
|
57
|
+
"""
|
|
58
|
+
return principal in DEFAULT_SERVICE_PRINCIPALS
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_service_principals_by_category() -> dict[str, tuple[str, ...]]:
|
|
62
|
+
"""
|
|
63
|
+
Get service principals organized by service category.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Dictionary mapping categories to service principal tuples
|
|
67
|
+
"""
|
|
68
|
+
return {
|
|
69
|
+
"storage": (
|
|
70
|
+
"s3.amazonaws.com",
|
|
71
|
+
"backup.amazonaws.com",
|
|
72
|
+
),
|
|
73
|
+
"compute": (
|
|
74
|
+
"lambda.amazonaws.com",
|
|
75
|
+
"ec2.amazonaws.com",
|
|
76
|
+
"ecs-tasks.amazonaws.com",
|
|
77
|
+
"eks.amazonaws.com",
|
|
78
|
+
),
|
|
79
|
+
"networking": (
|
|
80
|
+
"cloudfront.amazonaws.com",
|
|
81
|
+
"elasticloadbalancing.amazonaws.com",
|
|
82
|
+
"apigateway.amazonaws.com",
|
|
83
|
+
),
|
|
84
|
+
"monitoring": (
|
|
85
|
+
"logs.amazonaws.com",
|
|
86
|
+
"cloudwatch.amazonaws.com",
|
|
87
|
+
"monitoring.amazonaws.com",
|
|
88
|
+
"cloudtrail.amazonaws.com",
|
|
89
|
+
),
|
|
90
|
+
"messaging": (
|
|
91
|
+
"sns.amazonaws.com",
|
|
92
|
+
"events.amazonaws.com",
|
|
93
|
+
),
|
|
94
|
+
"management": ("config.amazonaws.com",),
|
|
95
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Default wildcard configurations for security best practices checks.
|
|
3
|
+
|
|
4
|
+
These wildcards define which actions are considered "safe" to use with
|
|
5
|
+
Resource: "*" (e.g., read-only describe operations).
|
|
6
|
+
|
|
7
|
+
Using Python tuples instead of YAML lists provides:
|
|
8
|
+
- Zero parsing overhead
|
|
9
|
+
- Immutable by default (tuples)
|
|
10
|
+
- Better performance
|
|
11
|
+
- Easy PyPI packaging
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Final
|
|
15
|
+
|
|
16
|
+
# ============================================================================
|
|
17
|
+
# Allowed Wildcards for Resource: "*"
|
|
18
|
+
# ============================================================================
|
|
19
|
+
# These action patterns are considered safe to use with wildcard resources
|
|
20
|
+
# They are typically read-only operations that need broad resource access
|
|
21
|
+
|
|
22
|
+
DEFAULT_ALLOWED_WILDCARDS: Final[tuple[str, ...]] = (
|
|
23
|
+
# Auto Scaling
|
|
24
|
+
"autoscaling:Describe*",
|
|
25
|
+
# CloudWatch
|
|
26
|
+
"cloudwatch:Describe*",
|
|
27
|
+
"cloudwatch:Get*",
|
|
28
|
+
"cloudwatch:List*",
|
|
29
|
+
# DynamoDB
|
|
30
|
+
"dynamodb:Describe*",
|
|
31
|
+
# EC2
|
|
32
|
+
"ec2:Describe*",
|
|
33
|
+
# Elastic Load Balancing
|
|
34
|
+
"elasticloadbalancing:Describe*",
|
|
35
|
+
# IAM (non-sensitive read operations)
|
|
36
|
+
"iam:Get*",
|
|
37
|
+
"iam:List*",
|
|
38
|
+
# KMS
|
|
39
|
+
"kms:Describe*",
|
|
40
|
+
# Lambda
|
|
41
|
+
"lambda:Get*",
|
|
42
|
+
"lambda:List*",
|
|
43
|
+
# CloudWatch Logs
|
|
44
|
+
"logs:Describe*",
|
|
45
|
+
"logs:Filter*",
|
|
46
|
+
"logs:Get*",
|
|
47
|
+
# RDS
|
|
48
|
+
"rds:Describe*",
|
|
49
|
+
# Route53
|
|
50
|
+
"route53:Get*",
|
|
51
|
+
"route53:List*",
|
|
52
|
+
# S3 (safe read operations only)
|
|
53
|
+
"s3:Describe*",
|
|
54
|
+
"s3:GetBucket*",
|
|
55
|
+
"s3:GetM*",
|
|
56
|
+
"s3:List*",
|
|
57
|
+
# SQS
|
|
58
|
+
"sqs:Get*",
|
|
59
|
+
"sqs:List*",
|
|
60
|
+
# API Gateway
|
|
61
|
+
"apigateway:GET",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# ============================================================================
|
|
65
|
+
# Service-Level Wildcards (Allowed Services)
|
|
66
|
+
# ============================================================================
|
|
67
|
+
# Services that are allowed to use service-level wildcards like "logs:*"
|
|
68
|
+
# These are typically low-risk monitoring/logging services
|
|
69
|
+
|
|
70
|
+
DEFAULT_SERVICE_WILDCARDS: Final[tuple[str, ...]] = (
|
|
71
|
+
"logs",
|
|
72
|
+
"cloudwatch",
|
|
73
|
+
"xray",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_allowed_wildcards() -> tuple[str, ...]:
|
|
78
|
+
"""
|
|
79
|
+
Get tuple of allowed wildcard action patterns.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Tuple of action patterns that are safe to use with Resource: "*"
|
|
83
|
+
"""
|
|
84
|
+
return DEFAULT_ALLOWED_WILDCARDS
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_allowed_service_wildcards() -> tuple[str, ...]:
|
|
88
|
+
"""
|
|
89
|
+
Get tuple of services allowed to use service-level wildcards.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Tuple of service names (e.g., "logs", "cloudwatch")
|
|
93
|
+
"""
|
|
94
|
+
return DEFAULT_SERVICE_WILDCARDS
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def is_allowed_wildcard(pattern: str) -> bool:
|
|
98
|
+
"""
|
|
99
|
+
Check if a wildcard pattern is in the allowed list.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
pattern: Action pattern to check (e.g., "s3:List*")
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
True if pattern is in allowed wildcards
|
|
106
|
+
|
|
107
|
+
Performance: O(n) but typically small list (~25 items)
|
|
108
|
+
"""
|
|
109
|
+
return pattern in DEFAULT_ALLOWED_WILDCARDS
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def is_allowed_service_wildcard(service: str) -> bool:
|
|
113
|
+
"""
|
|
114
|
+
Check if a service is allowed to use service-level wildcards.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
service: Service name (e.g., "logs", "s3")
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
True if service is in allowed list
|
|
121
|
+
|
|
122
|
+
Performance: O(n) but very small list (~3 items)
|
|
123
|
+
"""
|
|
124
|
+
return service in DEFAULT_SERVICE_WILDCARDS
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core constants for IAM Policy Validator.
|
|
3
|
+
|
|
4
|
+
This module defines constants used across the validator to ensure consistency
|
|
5
|
+
and provide a single source of truth for shared values. These constants are
|
|
6
|
+
based on AWS service limits and documentation.
|
|
7
|
+
|
|
8
|
+
References:
|
|
9
|
+
- AWS IAM Policy Size Limits: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html
|
|
10
|
+
- AWS ARN Format: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# ============================================================================
|
|
14
|
+
# ARN Validation
|
|
15
|
+
# ============================================================================
|
|
16
|
+
|
|
17
|
+
# ARN Validation Pattern
|
|
18
|
+
# This pattern is specifically designed for validation and allows wildcards (*) in region and account fields
|
|
19
|
+
# Unlike the parsing pattern in CompiledPatterns, this is more lenient for validation purposes
|
|
20
|
+
# Supports all AWS partitions: aws, aws-cn, aws-us-gov, aws-eusc, aws-iso*
|
|
21
|
+
DEFAULT_ARN_VALIDATION_PATTERN = r"^arn:(aws|aws-cn|aws-us-gov|aws-eusc|aws-iso|aws-iso-b|aws-iso-e|aws-iso-f):[a-z0-9\-]+:[a-z0-9\-*]*:[0-9*]*:.+$"
|
|
22
|
+
|
|
23
|
+
# Maximum allowed ARN length to prevent ReDoS attacks
|
|
24
|
+
# AWS maximum ARN length is approximately 2048 characters
|
|
25
|
+
MAX_ARN_LENGTH = 2048
|
|
26
|
+
|
|
27
|
+
# ============================================================================
|
|
28
|
+
# AWS IAM Policy Size Limits
|
|
29
|
+
# ============================================================================
|
|
30
|
+
# These limits are enforced by AWS and policies exceeding them will be rejected
|
|
31
|
+
# Note: AWS does not count whitespace when calculating policy size
|
|
32
|
+
|
|
33
|
+
# Managed policy maximum size (characters, excluding whitespace)
|
|
34
|
+
MAX_MANAGED_POLICY_SIZE = 6144
|
|
35
|
+
|
|
36
|
+
# Inline policy maximum size for IAM users (characters, excluding whitespace)
|
|
37
|
+
MAX_INLINE_USER_POLICY_SIZE = 2048
|
|
38
|
+
|
|
39
|
+
# Inline policy maximum size for IAM groups (characters, excluding whitespace)
|
|
40
|
+
MAX_INLINE_GROUP_POLICY_SIZE = 5120
|
|
41
|
+
|
|
42
|
+
# Inline policy maximum size for IAM roles (characters, excluding whitespace)
|
|
43
|
+
MAX_INLINE_ROLE_POLICY_SIZE = 10240
|
|
44
|
+
|
|
45
|
+
# Policy size limits dictionary (for backward compatibility and easy lookup)
|
|
46
|
+
AWS_POLICY_SIZE_LIMITS = {
|
|
47
|
+
"managed": MAX_MANAGED_POLICY_SIZE,
|
|
48
|
+
"inline_user": MAX_INLINE_USER_POLICY_SIZE,
|
|
49
|
+
"inline_group": MAX_INLINE_GROUP_POLICY_SIZE,
|
|
50
|
+
"inline_role": MAX_INLINE_ROLE_POLICY_SIZE,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# ============================================================================
|
|
54
|
+
# Configuration Defaults
|
|
55
|
+
# ============================================================================
|
|
56
|
+
|
|
57
|
+
# Default configuration file names (searched in order)
|
|
58
|
+
DEFAULT_CONFIG_FILENAMES = [
|
|
59
|
+
"iam-validator.yaml",
|
|
60
|
+
"iam-validator.yml",
|
|
61
|
+
".iam-validator.yaml",
|
|
62
|
+
".iam-validator.yml",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
# ============================================================================
|
|
66
|
+
# GitHub Integration
|
|
67
|
+
# ============================================================================
|
|
68
|
+
|
|
69
|
+
# Bot identifier for GitHub comments and reviews
|
|
70
|
+
BOT_IDENTIFIER = "🤖 IAM Policy Validator"
|
|
71
|
+
|
|
72
|
+
# HTML comment markers for identifying bot-generated content (for cleanup/updates)
|
|
73
|
+
SUMMARY_IDENTIFIER = "<!-- iam-policy-validator-summary -->"
|
|
74
|
+
REVIEW_IDENTIFIER = "<!-- iam-policy-validator-review -->"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Output formatters for IAM Policy Validator."""
|
|
2
|
+
|
|
3
|
+
from iam_validator.core.formatters.base import (
|
|
4
|
+
FormatterRegistry,
|
|
5
|
+
OutputFormatter,
|
|
6
|
+
get_global_registry,
|
|
7
|
+
)
|
|
8
|
+
from iam_validator.core.formatters.console import ConsoleFormatter
|
|
9
|
+
from iam_validator.core.formatters.csv import CSVFormatter
|
|
10
|
+
from iam_validator.core.formatters.enhanced import EnhancedFormatter
|
|
11
|
+
from iam_validator.core.formatters.html import HTMLFormatter
|
|
12
|
+
from iam_validator.core.formatters.json import JSONFormatter
|
|
13
|
+
from iam_validator.core.formatters.markdown import MarkdownFormatter
|
|
14
|
+
from iam_validator.core.formatters.sarif import SARIFFormatter
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"OutputFormatter",
|
|
18
|
+
"FormatterRegistry",
|
|
19
|
+
"get_global_registry",
|
|
20
|
+
"ConsoleFormatter",
|
|
21
|
+
"EnhancedFormatter",
|
|
22
|
+
"JSONFormatter",
|
|
23
|
+
"MarkdownFormatter",
|
|
24
|
+
"SARIFFormatter",
|
|
25
|
+
"CSVFormatter",
|
|
26
|
+
"HTMLFormatter",
|
|
27
|
+
]
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Base formatter and registry for output formatters."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from iam_validator.core.models import ValidationReport
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OutputFormatter(ABC):
|
|
13
|
+
"""Base class for all output formatters."""
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def format_id(self) -> str:
|
|
18
|
+
"""Unique identifier for this formatter (e.g., 'json', 'markdown')."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def description(self) -> str:
|
|
24
|
+
"""Human-readable description of the formatter."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def file_extension(self) -> str:
|
|
29
|
+
"""Default file extension for this format."""
|
|
30
|
+
return "txt"
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def content_type(self) -> str:
|
|
34
|
+
"""MIME content type for this format."""
|
|
35
|
+
return "text/plain"
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def format(self, report: ValidationReport, **kwargs: Any) -> str:
|
|
39
|
+
"""Format the validation report.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
report: The validation report to format
|
|
43
|
+
**kwargs: Additional formatter-specific options
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Formatted string representation of the report
|
|
47
|
+
"""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
def format_to_file(self, report: ValidationReport, filepath: str, **kwargs: Any) -> None:
|
|
51
|
+
"""Format report and write to file.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
report: The validation report to format
|
|
55
|
+
filepath: Path to output file
|
|
56
|
+
**kwargs: Additional formatter-specific options
|
|
57
|
+
"""
|
|
58
|
+
output = self.format(report, **kwargs)
|
|
59
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
60
|
+
f.write(output)
|
|
61
|
+
logger.info(f"Report written to {filepath} using {self.format_id} formatter")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class FormatterRegistry:
|
|
65
|
+
"""Registry for managing output formatters."""
|
|
66
|
+
|
|
67
|
+
def __init__(self) -> None:
|
|
68
|
+
"""Initialize the formatter registry."""
|
|
69
|
+
self._formatters: dict[str, OutputFormatter] = {}
|
|
70
|
+
|
|
71
|
+
def register(self, formatter: OutputFormatter) -> None:
|
|
72
|
+
"""Register a new formatter.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
formatter: OutputFormatter instance to register
|
|
76
|
+
"""
|
|
77
|
+
self._formatters[formatter.format_id] = formatter
|
|
78
|
+
logger.debug(f"Registered formatter: {formatter.format_id}")
|
|
79
|
+
|
|
80
|
+
def unregister(self, format_id: str) -> None:
|
|
81
|
+
"""Unregister a formatter.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
format_id: ID of the formatter to unregister
|
|
85
|
+
"""
|
|
86
|
+
if format_id in self._formatters:
|
|
87
|
+
del self._formatters[format_id]
|
|
88
|
+
logger.debug(f"Unregistered formatter: {format_id}")
|
|
89
|
+
|
|
90
|
+
def get_formatter(self, format_id: str) -> OutputFormatter | None:
|
|
91
|
+
"""Get a formatter by ID.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
format_id: ID of the formatter to retrieve
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
OutputFormatter instance or None if not found
|
|
98
|
+
"""
|
|
99
|
+
return self._formatters.get(format_id)
|
|
100
|
+
|
|
101
|
+
def list_formatters(self) -> dict[str, str]:
|
|
102
|
+
"""List all registered formatters.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Dictionary of format_id -> description
|
|
106
|
+
"""
|
|
107
|
+
return {fmt_id: formatter.description for fmt_id, formatter in self._formatters.items()}
|
|
108
|
+
|
|
109
|
+
def format_report(self, report: ValidationReport, format_id: str, **kwargs: Any) -> str:
|
|
110
|
+
"""Format a report using the specified formatter.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
report: The validation report to format
|
|
114
|
+
format_id: ID of the formatter to use
|
|
115
|
+
**kwargs: Additional formatter-specific options
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Formatted string representation
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
ValueError: If formatter not found
|
|
122
|
+
"""
|
|
123
|
+
formatter = self.get_formatter(format_id)
|
|
124
|
+
if not formatter:
|
|
125
|
+
available = ", ".join(self._formatters.keys())
|
|
126
|
+
raise ValueError(f"Formatter '{format_id}' not found. Available: {available}")
|
|
127
|
+
|
|
128
|
+
return formatter.format(report, **kwargs)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# Global formatter registry
|
|
132
|
+
_global_registry = FormatterRegistry()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_global_registry() -> FormatterRegistry:
|
|
136
|
+
"""Get the global formatter registry."""
|
|
137
|
+
return _global_registry
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def register_formatter(formatter: OutputFormatter) -> None:
|
|
141
|
+
"""Register a formatter in the global registry."""
|
|
142
|
+
_global_registry.register(formatter)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_formatter(format_id: str) -> OutputFormatter | None:
|
|
146
|
+
"""Get a formatter from the global registry."""
|
|
147
|
+
return _global_registry.get_formatter(format_id)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Console formatter - Classic console output using report.py."""
|
|
2
|
+
|
|
3
|
+
from io import StringIO
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
from iam_validator.core.formatters.base import OutputFormatter
|
|
8
|
+
from iam_validator.core.models import ValidationReport
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConsoleFormatter(OutputFormatter):
|
|
12
|
+
"""Classic console formatter - uses the standard report.py print_console_report output."""
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def format_id(self) -> str:
|
|
16
|
+
return "console"
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def description(self) -> str:
|
|
20
|
+
return "Classic console output with colors and tables"
|
|
21
|
+
|
|
22
|
+
def format(self, report: ValidationReport, **kwargs) -> str:
|
|
23
|
+
"""Format validation report using the classic console output from report.py.
|
|
24
|
+
|
|
25
|
+
This delegates to the ReportGenerator.print_console_report method,
|
|
26
|
+
capturing its output to return as a string.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
report: Validation report to format
|
|
30
|
+
**kwargs: Additional options (color: bool = True)
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Formatted string with classic console output
|
|
34
|
+
"""
|
|
35
|
+
# Import here to avoid circular dependency
|
|
36
|
+
from iam_validator.core.report import ReportGenerator
|
|
37
|
+
|
|
38
|
+
# Allow disabling color for plain text output
|
|
39
|
+
color = kwargs.get("color", True)
|
|
40
|
+
|
|
41
|
+
# Capture the output from print_console_report
|
|
42
|
+
string_buffer = StringIO()
|
|
43
|
+
console = Console(file=string_buffer, force_terminal=color, width=120)
|
|
44
|
+
|
|
45
|
+
# Create a generator instance with our custom console
|
|
46
|
+
generator = ReportGenerator()
|
|
47
|
+
|
|
48
|
+
# Replace the console temporarily to capture output
|
|
49
|
+
original_console = generator.console
|
|
50
|
+
generator.console = console
|
|
51
|
+
|
|
52
|
+
# Call the actual print_console_report method
|
|
53
|
+
generator.print_console_report(report)
|
|
54
|
+
|
|
55
|
+
# Restore original console
|
|
56
|
+
generator.console = original_console
|
|
57
|
+
|
|
58
|
+
# Return captured output
|
|
59
|
+
return string_buffer.getvalue()
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""CSV formatter for IAM Policy Validator."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import io
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from iam_validator.core.formatters.base import OutputFormatter
|
|
8
|
+
from iam_validator.core.models import ValidationReport
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CSVFormatter(OutputFormatter):
|
|
12
|
+
"""Formats validation results as CSV for spreadsheet analysis."""
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def format_id(self) -> str:
|
|
16
|
+
return "csv"
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def description(self) -> str:
|
|
20
|
+
return "CSV format for spreadsheet import and analysis"
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def file_extension(self) -> str:
|
|
24
|
+
return "csv"
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def content_type(self) -> str:
|
|
28
|
+
return "text/csv"
|
|
29
|
+
|
|
30
|
+
def format(self, report: ValidationReport, **kwargs) -> str:
|
|
31
|
+
"""Format report as CSV.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
report: The validation report
|
|
35
|
+
**kwargs: Additional options like 'include_summary'
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
CSV string
|
|
39
|
+
"""
|
|
40
|
+
include_summary = kwargs.get("include_summary", True)
|
|
41
|
+
include_header = kwargs.get("include_header", True)
|
|
42
|
+
|
|
43
|
+
output = io.StringIO()
|
|
44
|
+
writer = csv.writer(output, quoting=csv.QUOTE_MINIMAL)
|
|
45
|
+
|
|
46
|
+
if include_summary:
|
|
47
|
+
self._write_summary_section(writer, report)
|
|
48
|
+
writer.writerow([]) # Empty row separator
|
|
49
|
+
|
|
50
|
+
self._write_issues_section(writer, report, include_header)
|
|
51
|
+
|
|
52
|
+
return output.getvalue()
|
|
53
|
+
|
|
54
|
+
def _write_summary_section(self, writer: Any, report: ValidationReport) -> None:
|
|
55
|
+
"""Write summary statistics to CSV."""
|
|
56
|
+
# Count issues by severity - support both IAM validity and security severities
|
|
57
|
+
errors = sum(
|
|
58
|
+
1
|
|
59
|
+
for r in report.results
|
|
60
|
+
for i in r.issues
|
|
61
|
+
if i.severity in ("error", "critical", "high")
|
|
62
|
+
)
|
|
63
|
+
warnings = sum(
|
|
64
|
+
1 for r in report.results for i in r.issues if i.severity in ("warning", "medium")
|
|
65
|
+
)
|
|
66
|
+
infos = sum(1 for r in report.results for i in r.issues if i.severity in ("info", "low"))
|
|
67
|
+
|
|
68
|
+
writer.writerow(["Summary Statistics"])
|
|
69
|
+
writer.writerow(["Metric", "Value"])
|
|
70
|
+
writer.writerow(["Total Policies", report.total_policies])
|
|
71
|
+
writer.writerow(["Valid Policies (IAM)", report.valid_policies])
|
|
72
|
+
writer.writerow(["Invalid Policies (IAM)", report.invalid_policies])
|
|
73
|
+
writer.writerow(["Policies with Security Findings", report.policies_with_security_issues])
|
|
74
|
+
writer.writerow(["Total Issues", report.total_issues])
|
|
75
|
+
writer.writerow(["Validity Issues", report.validity_issues])
|
|
76
|
+
writer.writerow(["Security Issues", report.security_issues])
|
|
77
|
+
writer.writerow([""])
|
|
78
|
+
writer.writerow(["Legacy Severity Breakdown"])
|
|
79
|
+
writer.writerow(["Errors", errors])
|
|
80
|
+
writer.writerow(["Warnings", warnings])
|
|
81
|
+
writer.writerow(["Info", infos])
|
|
82
|
+
|
|
83
|
+
def _write_issues_section(
|
|
84
|
+
self, writer: Any, report: ValidationReport, include_header: bool
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Write detailed issues to CSV."""
|
|
87
|
+
if include_header:
|
|
88
|
+
writer.writerow(
|
|
89
|
+
[
|
|
90
|
+
"Policy File",
|
|
91
|
+
"Statement Index",
|
|
92
|
+
"Statement SID",
|
|
93
|
+
"Line Number",
|
|
94
|
+
"Severity",
|
|
95
|
+
"Issue Type",
|
|
96
|
+
"Action",
|
|
97
|
+
"Resource",
|
|
98
|
+
"Condition Key",
|
|
99
|
+
"Message",
|
|
100
|
+
"Suggestion",
|
|
101
|
+
]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
for policy_result in report.results:
|
|
105
|
+
for issue in policy_result.issues:
|
|
106
|
+
writer.writerow(
|
|
107
|
+
[
|
|
108
|
+
policy_result.policy_file,
|
|
109
|
+
(issue.statement_index + 1 if issue.statement_index is not None else ""),
|
|
110
|
+
issue.statement_sid or "",
|
|
111
|
+
issue.line_number or "",
|
|
112
|
+
issue.severity,
|
|
113
|
+
issue.issue_type or "",
|
|
114
|
+
issue.action or "",
|
|
115
|
+
issue.resource or "",
|
|
116
|
+
issue.condition_key or "",
|
|
117
|
+
issue.message,
|
|
118
|
+
issue.suggestion or "",
|
|
119
|
+
]
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def format_pivot_table(self, report: ValidationReport) -> str:
|
|
123
|
+
"""Format report as a pivot table CSV for analysis.
|
|
124
|
+
|
|
125
|
+
Groups issues by check_id and severity for easy analysis.
|
|
126
|
+
"""
|
|
127
|
+
output = io.StringIO()
|
|
128
|
+
writer = csv.writer(output)
|
|
129
|
+
|
|
130
|
+
# Create pivot data
|
|
131
|
+
pivot_data = self._create_pivot_data(report)
|
|
132
|
+
|
|
133
|
+
# Write header
|
|
134
|
+
writer.writerow(["Issue Type", "Severity", "Count", "Policy Files"])
|
|
135
|
+
|
|
136
|
+
# Write pivot rows
|
|
137
|
+
for (issue_type, severity), data in sorted(pivot_data.items()):
|
|
138
|
+
writer.writerow(
|
|
139
|
+
[
|
|
140
|
+
issue_type or "unknown",
|
|
141
|
+
severity,
|
|
142
|
+
data["count"],
|
|
143
|
+
"; ".join(data["files"][:5]) + ("..." if len(data["files"]) > 5 else ""),
|
|
144
|
+
]
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return output.getvalue()
|
|
148
|
+
|
|
149
|
+
def _create_pivot_data(self, report: ValidationReport) -> dict[tuple, dict[str, Any]]:
|
|
150
|
+
"""Create pivot table data structure."""
|
|
151
|
+
pivot_data = {}
|
|
152
|
+
|
|
153
|
+
for policy_result in report.results:
|
|
154
|
+
for issue in policy_result.issues:
|
|
155
|
+
key = (issue.issue_type or "unknown", issue.severity)
|
|
156
|
+
|
|
157
|
+
if key not in pivot_data:
|
|
158
|
+
pivot_data[key] = {
|
|
159
|
+
"count": 0,
|
|
160
|
+
"files": set(),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
pivot_data[key]["count"] += 1
|
|
164
|
+
pivot_data[key]["files"].add(policy_result.policy_file)
|
|
165
|
+
|
|
166
|
+
# Convert sets to lists
|
|
167
|
+
for key in pivot_data:
|
|
168
|
+
pivot_data[key]["files"] = sorted(list(pivot_data[key]["files"]))
|
|
169
|
+
|
|
170
|
+
return pivot_data
|