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.
- iam_policy_validator-1.14.0.dist-info/METADATA +782 -0
- iam_policy_validator-1.14.0.dist-info/RECORD +106 -0
- iam_policy_validator-1.14.0.dist-info/WHEEL +4 -0
- iam_policy_validator-1.14.0.dist-info/entry_points.txt +2 -0
- iam_policy_validator-1.14.0.dist-info/licenses/LICENSE +21 -0
- iam_validator/__init__.py +27 -0
- iam_validator/__main__.py +11 -0
- iam_validator/__version__.py +9 -0
- iam_validator/checks/__init__.py +45 -0
- iam_validator/checks/action_condition_enforcement.py +1442 -0
- iam_validator/checks/action_resource_matching.py +472 -0
- iam_validator/checks/action_validation.py +67 -0
- iam_validator/checks/condition_key_validation.py +88 -0
- iam_validator/checks/condition_type_mismatch.py +257 -0
- iam_validator/checks/full_wildcard.py +62 -0
- iam_validator/checks/mfa_condition_check.py +105 -0
- iam_validator/checks/policy_size.py +114 -0
- iam_validator/checks/policy_structure.py +556 -0
- iam_validator/checks/policy_type_validation.py +331 -0
- iam_validator/checks/principal_validation.py +708 -0
- iam_validator/checks/resource_validation.py +135 -0
- iam_validator/checks/sensitive_action.py +438 -0
- iam_validator/checks/service_wildcard.py +98 -0
- iam_validator/checks/set_operator_validation.py +153 -0
- iam_validator/checks/sid_uniqueness.py +146 -0
- iam_validator/checks/trust_policy_validation.py +509 -0
- iam_validator/checks/utils/__init__.py +17 -0
- iam_validator/checks/utils/action_parser.py +149 -0
- iam_validator/checks/utils/policy_level_checks.py +190 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +293 -0
- iam_validator/checks/utils/wildcard_expansion.py +86 -0
- iam_validator/checks/wildcard_action.py +58 -0
- iam_validator/checks/wildcard_resource.py +374 -0
- iam_validator/commands/__init__.py +31 -0
- iam_validator/commands/analyze.py +549 -0
- iam_validator/commands/base.py +48 -0
- iam_validator/commands/cache.py +393 -0
- iam_validator/commands/completion.py +471 -0
- iam_validator/commands/download_services.py +255 -0
- iam_validator/commands/post_to_pr.py +86 -0
- iam_validator/commands/query.py +485 -0
- iam_validator/commands/validate.py +830 -0
- iam_validator/core/__init__.py +13 -0
- iam_validator/core/access_analyzer.py +671 -0
- iam_validator/core/access_analyzer_report.py +640 -0
- iam_validator/core/aws_fetcher.py +29 -0
- iam_validator/core/aws_service/__init__.py +21 -0
- iam_validator/core/aws_service/cache.py +108 -0
- iam_validator/core/aws_service/client.py +205 -0
- iam_validator/core/aws_service/fetcher.py +641 -0
- iam_validator/core/aws_service/parsers.py +149 -0
- iam_validator/core/aws_service/patterns.py +51 -0
- iam_validator/core/aws_service/storage.py +291 -0
- iam_validator/core/aws_service/validators.py +380 -0
- iam_validator/core/check_registry.py +679 -0
- iam_validator/core/cli.py +134 -0
- iam_validator/core/codeowners.py +245 -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 +181 -0
- iam_validator/core/config/check_documentation.py +390 -0
- iam_validator/core/config/condition_requirements.py +258 -0
- iam_validator/core/config/config_loader.py +670 -0
- iam_validator/core/config/defaults.py +739 -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 +132 -0
- iam_validator/core/config/wildcards.py +127 -0
- iam_validator/core/constants.py +149 -0
- iam_validator/core/diff_parser.py +325 -0
- iam_validator/core/finding_fingerprint.py +131 -0
- iam_validator/core/formatters/__init__.py +27 -0
- iam_validator/core/formatters/base.py +147 -0
- iam_validator/core/formatters/console.py +68 -0
- iam_validator/core/formatters/csv.py +171 -0
- iam_validator/core/formatters/enhanced.py +481 -0
- iam_validator/core/formatters/html.py +672 -0
- iam_validator/core/formatters/json.py +33 -0
- iam_validator/core/formatters/markdown.py +64 -0
- iam_validator/core/formatters/sarif.py +251 -0
- iam_validator/core/ignore_patterns.py +297 -0
- iam_validator/core/ignore_processor.py +309 -0
- iam_validator/core/ignored_findings.py +400 -0
- iam_validator/core/label_manager.py +197 -0
- iam_validator/core/models.py +404 -0
- iam_validator/core/policy_checks.py +220 -0
- iam_validator/core/policy_loader.py +785 -0
- iam_validator/core/pr_commenter.py +780 -0
- iam_validator/core/report.py +942 -0
- iam_validator/integrations/__init__.py +28 -0
- iam_validator/integrations/github_integration.py +1821 -0
- iam_validator/integrations/ms_teams.py +442 -0
- iam_validator/sdk/__init__.py +220 -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 +451 -0
- iam_validator/sdk/query_utils.py +454 -0
- iam_validator/sdk/shortcuts.py +283 -0
- iam_validator/utils/__init__.py +35 -0
- iam_validator/utils/cache.py +105 -0
- iam_validator/utils/regex.py +205 -0
- iam_validator/utils/terminal.py +22 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Parsing and pattern matching for AWS actions and resources.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to parse IAM actions, validate ARN formats,
|
|
4
|
+
and perform wildcard matching on action patterns.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from iam_validator.core.aws_service.patterns import CompiledPatterns
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ServiceParser:
|
|
13
|
+
"""Parses and matches AWS actions, ARNs, and wildcards.
|
|
14
|
+
|
|
15
|
+
This class provides methods for:
|
|
16
|
+
- Parsing IAM actions into service prefix and action name
|
|
17
|
+
- Validating ARN format
|
|
18
|
+
- Detecting and matching wildcard patterns
|
|
19
|
+
- Expanding wildcard actions to full action lists
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
"""Initialize parser with compiled patterns."""
|
|
24
|
+
self._patterns = CompiledPatterns()
|
|
25
|
+
|
|
26
|
+
def parse_action(self, action: str) -> tuple[str, str]:
|
|
27
|
+
"""Parse IAM action into service prefix and action name.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
action: Full action string (e.g., "s3:GetObject", "iam:CreateUser")
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Tuple of (service_prefix, action_name) both lowercase
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError: If action format is invalid
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
>>> parser = ServiceParser()
|
|
40
|
+
>>> parser.parse_action("s3:GetObject")
|
|
41
|
+
('s3', 'GetObject')
|
|
42
|
+
"""
|
|
43
|
+
match = self._patterns.action_pattern.match(action)
|
|
44
|
+
if not match:
|
|
45
|
+
raise ValueError(f"Invalid action format: {action}")
|
|
46
|
+
|
|
47
|
+
return match.group("service").lower(), match.group("action")
|
|
48
|
+
|
|
49
|
+
def validate_arn_format(self, arn: str) -> tuple[bool, str | None]:
|
|
50
|
+
"""Validate ARN format using compiled regex.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
arn: ARN string to validate
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Tuple of (is_valid, error_message). error_message is None if valid.
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
>>> parser = ServiceParser()
|
|
60
|
+
>>> parser.validate_arn_format("arn:aws:s3:::my-bucket/*")
|
|
61
|
+
(True, None)
|
|
62
|
+
>>> parser.validate_arn_format("invalid")
|
|
63
|
+
(False, "Invalid ARN format: invalid")
|
|
64
|
+
"""
|
|
65
|
+
if arn == "*":
|
|
66
|
+
return True, None
|
|
67
|
+
|
|
68
|
+
match = self._patterns.arn_pattern.match(arn)
|
|
69
|
+
if not match:
|
|
70
|
+
return False, f"Invalid ARN format: {arn}"
|
|
71
|
+
|
|
72
|
+
return True, None
|
|
73
|
+
|
|
74
|
+
def is_wildcard_action(self, action_name: str) -> bool:
|
|
75
|
+
"""Check if action name contains wildcards.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
action_name: Action name to check (e.g., "GetObject", "Get*", "*")
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
True if action contains wildcard characters
|
|
82
|
+
|
|
83
|
+
Example:
|
|
84
|
+
>>> parser = ServiceParser()
|
|
85
|
+
>>> parser.is_wildcard_action("GetObject")
|
|
86
|
+
False
|
|
87
|
+
>>> parser.is_wildcard_action("Get*")
|
|
88
|
+
True
|
|
89
|
+
"""
|
|
90
|
+
return bool(self._patterns.wildcard_pattern.search(action_name))
|
|
91
|
+
|
|
92
|
+
def match_wildcard_action(self, pattern: str, actions: list[str]) -> tuple[bool, list[str]]:
|
|
93
|
+
"""Match wildcard pattern against list of actions.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
pattern: Action pattern with wildcards (e.g., "Get*", "*Object", "Describe*")
|
|
97
|
+
actions: List of valid action names to match against
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Tuple of (has_matches, list_of_matched_actions)
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
>>> parser = ServiceParser()
|
|
104
|
+
>>> actions = ["GetObject", "GetBucket", "PutObject"]
|
|
105
|
+
>>> parser.match_wildcard_action("Get*", actions)
|
|
106
|
+
(True, ['GetObject', 'GetBucket'])
|
|
107
|
+
"""
|
|
108
|
+
# Convert wildcard pattern to regex
|
|
109
|
+
# Escape special regex chars except *, then replace * with .*
|
|
110
|
+
regex_pattern = "^" + re.escape(pattern).replace(r"\*", ".*") + "$"
|
|
111
|
+
compiled_pattern = re.compile(regex_pattern, re.IGNORECASE)
|
|
112
|
+
|
|
113
|
+
matched = [a for a in actions if compiled_pattern.match(a)]
|
|
114
|
+
return len(matched) > 0, matched
|
|
115
|
+
|
|
116
|
+
def expand_wildcard_to_actions(
|
|
117
|
+
self,
|
|
118
|
+
action_pattern: str,
|
|
119
|
+
available_actions: list[str],
|
|
120
|
+
service_prefix: str,
|
|
121
|
+
) -> list[str]:
|
|
122
|
+
"""Expand wildcard pattern to full list of actions.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
action_pattern: Action pattern (e.g., "s3:Get*", "iam:*")
|
|
126
|
+
available_actions: List of available action names for the service
|
|
127
|
+
service_prefix: Service prefix (e.g., "s3", "iam")
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Sorted list of fully-qualified actions matching the pattern
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
>>> parser = ServiceParser()
|
|
134
|
+
>>> actions = ["GetObject", "PutObject", "DeleteObject"]
|
|
135
|
+
>>> parser.expand_wildcard_to_actions("s3:*Object", actions, "s3")
|
|
136
|
+
['s3:DeleteObject', 's3:GetObject', 's3:PutObject']
|
|
137
|
+
"""
|
|
138
|
+
# Parse to get action name part
|
|
139
|
+
_, action_name = self.parse_action(action_pattern)
|
|
140
|
+
|
|
141
|
+
# Handle full service wildcard (e.g., "iam:*")
|
|
142
|
+
if action_name == "*":
|
|
143
|
+
return sorted([f"{service_prefix}:{action}" for action in available_actions])
|
|
144
|
+
|
|
145
|
+
# Match wildcard pattern
|
|
146
|
+
_, matched_actions = self.match_wildcard_action(action_name, available_actions)
|
|
147
|
+
|
|
148
|
+
# Return fully-qualified actions
|
|
149
|
+
return sorted([f"{service_prefix}:{action}" for action in matched_actions])
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Pre-compiled regex patterns for AWS service validation.
|
|
2
|
+
|
|
3
|
+
This module provides a singleton class containing pre-compiled regex patterns
|
|
4
|
+
used across the AWS service validation system for better performance.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CompiledPatterns:
|
|
11
|
+
"""Pre-compiled regex patterns for validation.
|
|
12
|
+
|
|
13
|
+
This class implements the Singleton pattern to ensure patterns are compiled only once
|
|
14
|
+
and reused across all instances for better performance.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
_instance: "CompiledPatterns | None" = None
|
|
18
|
+
_initialized: bool = False
|
|
19
|
+
|
|
20
|
+
def __new__(cls) -> "CompiledPatterns":
|
|
21
|
+
"""Create or return the singleton instance."""
|
|
22
|
+
if cls._instance is None:
|
|
23
|
+
cls._instance = super().__new__(cls)
|
|
24
|
+
return cls._instance
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
"""Initialize compiled patterns (only once due to Singleton pattern)."""
|
|
28
|
+
# Only initialize once, even if __init__ is called multiple times
|
|
29
|
+
if CompiledPatterns._initialized:
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
CompiledPatterns._initialized = True
|
|
33
|
+
|
|
34
|
+
# ARN validation pattern
|
|
35
|
+
self.arn_pattern = re.compile(
|
|
36
|
+
r"^arn:(?P<partition>(aws|aws-cn|aws-us-gov|aws-eusc|aws-iso|aws-iso-b|aws-iso-e|aws-iso-f)):"
|
|
37
|
+
r"(?P<service>[a-z0-9\-]+):"
|
|
38
|
+
r"(?P<region>[a-z0-9\-]*):"
|
|
39
|
+
r"(?P<account>[0-9]*):"
|
|
40
|
+
r"(?P<resource>.+)$",
|
|
41
|
+
re.IGNORECASE,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Action format pattern
|
|
45
|
+
self.action_pattern = re.compile(
|
|
46
|
+
r"^(?P<service>[a-zA-Z0-9_-]+):(?P<action>[a-zA-Z0-9*_-]+)$"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Wildcard detection patterns
|
|
50
|
+
self.wildcard_pattern = re.compile(r"\*")
|
|
51
|
+
self.partial_wildcard_pattern = re.compile(r"^[^*]+\*$")
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""File storage operations for AWS service data.
|
|
2
|
+
|
|
3
|
+
This module handles disk caching and offline file loading for AWS service definitions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from iam_validator.core.models import ServiceDetail, ServiceInfo
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ServiceFileStorage:
|
|
21
|
+
"""Handles disk cache and offline AWS service file operations.
|
|
22
|
+
|
|
23
|
+
This class manages:
|
|
24
|
+
- Disk-based caching with TTL
|
|
25
|
+
- Loading AWS service definitions from local files
|
|
26
|
+
- Platform-specific cache directory management
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
cache_dir: Path | str | None = None,
|
|
32
|
+
aws_services_dir: Path | str | None = None,
|
|
33
|
+
cache_ttl: int = 86400,
|
|
34
|
+
enable_cache: bool = True,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Initialize storage with cache and offline directories.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
cache_dir: Custom cache directory path (uses platform default if None)
|
|
40
|
+
aws_services_dir: Directory containing pre-downloaded AWS service JSON files.
|
|
41
|
+
When set, enables offline mode. Directory should contain:
|
|
42
|
+
- _services.json: List of all services
|
|
43
|
+
- {service}.json: Individual service files (e.g., s3.json)
|
|
44
|
+
cache_ttl: Cache time-to-live in seconds
|
|
45
|
+
enable_cache: Enable persistent disk caching
|
|
46
|
+
"""
|
|
47
|
+
self.cache_ttl = cache_ttl
|
|
48
|
+
self.enable_cache = enable_cache
|
|
49
|
+
self._cache_dir = self.get_cache_directory(cache_dir)
|
|
50
|
+
|
|
51
|
+
# AWS services directory for offline mode
|
|
52
|
+
self.aws_services_dir: Path | None = None
|
|
53
|
+
if aws_services_dir:
|
|
54
|
+
self.aws_services_dir = Path(aws_services_dir)
|
|
55
|
+
if not self.aws_services_dir.exists():
|
|
56
|
+
raise ValueError(f"AWS services directory does not exist: {aws_services_dir}")
|
|
57
|
+
logger.info(f"Using local AWS services from: {self.aws_services_dir}")
|
|
58
|
+
|
|
59
|
+
# Create cache directory if needed
|
|
60
|
+
if self.enable_cache:
|
|
61
|
+
self._cache_dir.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def get_cache_directory(cache_dir: Path | str | None = None) -> Path:
|
|
65
|
+
"""Get the cache directory path, using platform-appropriate defaults.
|
|
66
|
+
|
|
67
|
+
Priority:
|
|
68
|
+
1. Provided cache_dir parameter
|
|
69
|
+
2. Platform-specific user cache directory
|
|
70
|
+
- Linux/Unix: ~/.cache/iam-validator/aws_services
|
|
71
|
+
- macOS: ~/Library/Caches/iam-validator/aws_services
|
|
72
|
+
- Windows: %LOCALAPPDATA%/iam-validator/cache/aws_services
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
cache_dir: Optional custom cache directory path
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Path object for the cache directory
|
|
79
|
+
"""
|
|
80
|
+
if cache_dir is not None:
|
|
81
|
+
return Path(cache_dir)
|
|
82
|
+
|
|
83
|
+
# Determine platform-specific cache directory
|
|
84
|
+
if sys.platform == "darwin":
|
|
85
|
+
# macOS
|
|
86
|
+
base_cache = Path.home() / "Library" / "Caches"
|
|
87
|
+
elif sys.platform == "win32":
|
|
88
|
+
# Windows
|
|
89
|
+
base_cache = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
|
|
90
|
+
else:
|
|
91
|
+
# Linux and other Unix-like systems
|
|
92
|
+
base_cache = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache"))
|
|
93
|
+
|
|
94
|
+
return base_cache / "iam-validator" / "aws_services"
|
|
95
|
+
|
|
96
|
+
def set_cache_directory(self, cache_dir: Path | str) -> None:
|
|
97
|
+
"""Set a new cache directory path dynamically.
|
|
98
|
+
|
|
99
|
+
This method allows library users to change the cache location at runtime.
|
|
100
|
+
The new directory will be created if it doesn't exist and caching is enabled.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
cache_dir: New cache directory path
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
>>> storage = ServiceFileStorage()
|
|
107
|
+
>>> storage.set_cache_directory("/tmp/my-custom-cache")
|
|
108
|
+
>>> # Future cache operations will use the new directory
|
|
109
|
+
"""
|
|
110
|
+
self._cache_dir = Path(cache_dir)
|
|
111
|
+
|
|
112
|
+
# Create new cache directory if caching is enabled
|
|
113
|
+
if self.enable_cache:
|
|
114
|
+
self._cache_dir.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
logger.info(f"Cache directory updated to: {self._cache_dir}")
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def cache_directory(self) -> Path:
|
|
119
|
+
"""Get the current cache directory path.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Current cache directory as Path object
|
|
123
|
+
|
|
124
|
+
Example:
|
|
125
|
+
>>> storage = ServiceFileStorage()
|
|
126
|
+
>>> print(storage.cache_directory)
|
|
127
|
+
PosixPath('/Users/username/Library/Caches/iam-validator/aws_services')
|
|
128
|
+
"""
|
|
129
|
+
return self._cache_dir
|
|
130
|
+
|
|
131
|
+
def _get_cache_path(self, url: str, base_url: str) -> Path:
|
|
132
|
+
"""Generate cache file path for URL.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
url: URL to generate cache path for
|
|
136
|
+
base_url: Base URL for service reference API
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Path to cache file
|
|
140
|
+
"""
|
|
141
|
+
url_hash = hashlib.md5(url.encode()).hexdigest()
|
|
142
|
+
|
|
143
|
+
# Extract service name for better organization
|
|
144
|
+
filename = f"{url_hash}.json"
|
|
145
|
+
if "/v1/" in url:
|
|
146
|
+
service_name = url.split("/v1/")[1].split("/")[0]
|
|
147
|
+
filename = f"{service_name}_{url_hash[:8]}.json"
|
|
148
|
+
elif url == base_url:
|
|
149
|
+
filename = "services_list.json"
|
|
150
|
+
|
|
151
|
+
return self._cache_dir / filename
|
|
152
|
+
|
|
153
|
+
def read_from_cache(self, url: str, base_url: str) -> Any | None:
|
|
154
|
+
"""Read from disk cache with TTL checking.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
url: URL to read from cache
|
|
158
|
+
base_url: Base URL for service reference API
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Cached data if valid, None otherwise
|
|
162
|
+
"""
|
|
163
|
+
if not self.enable_cache:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
cache_path = self._get_cache_path(url, base_url)
|
|
167
|
+
|
|
168
|
+
if not cache_path.exists():
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
# Check file modification time for TTL
|
|
173
|
+
mtime = cache_path.stat().st_mtime
|
|
174
|
+
if time.time() - mtime > self.cache_ttl:
|
|
175
|
+
logger.debug(f"Cache expired for {url}")
|
|
176
|
+
cache_path.unlink() # Remove expired cache
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
with open(cache_path, encoding="utf-8") as f:
|
|
180
|
+
data = json.load(f)
|
|
181
|
+
logger.debug(f"Disk cache hit for {url}")
|
|
182
|
+
return data
|
|
183
|
+
|
|
184
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
185
|
+
logger.warning(f"Failed to read cache for {url}: {e}")
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
def write_to_cache(self, url: str, data: Any, base_url: str) -> None:
|
|
189
|
+
"""Write to disk cache.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
url: URL to cache data for
|
|
193
|
+
data: Data to cache
|
|
194
|
+
base_url: Base URL for service reference API
|
|
195
|
+
"""
|
|
196
|
+
if not self.enable_cache:
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
cache_path = self._get_cache_path(url, base_url)
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
with open(cache_path, "w", encoding="utf-8") as f:
|
|
203
|
+
json.dump(data, f, indent=2)
|
|
204
|
+
logger.debug(f"Written to disk cache: {url}")
|
|
205
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
206
|
+
logger.warning(f"Failed to write cache for {url}: {e}")
|
|
207
|
+
|
|
208
|
+
def load_services_from_file(self) -> list[ServiceInfo]:
|
|
209
|
+
"""Load services list from local _services.json file.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
List of ServiceInfo objects loaded from _services.json
|
|
213
|
+
|
|
214
|
+
Raises:
|
|
215
|
+
ValueError: If aws_services_dir is not set or _services.json is invalid
|
|
216
|
+
FileNotFoundError: If _services.json doesn't exist
|
|
217
|
+
"""
|
|
218
|
+
if not self.aws_services_dir:
|
|
219
|
+
raise ValueError("aws_services_dir is not set")
|
|
220
|
+
|
|
221
|
+
services_file = self.aws_services_dir / "_services.json"
|
|
222
|
+
if not services_file.exists():
|
|
223
|
+
raise FileNotFoundError(f"_services.json not found in {self.aws_services_dir}")
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
with open(services_file, encoding="utf-8") as f:
|
|
227
|
+
data = json.load(f)
|
|
228
|
+
|
|
229
|
+
if not isinstance(data, list):
|
|
230
|
+
raise ValueError("Expected list of services from _services.json")
|
|
231
|
+
|
|
232
|
+
services: list[ServiceInfo] = []
|
|
233
|
+
for item in data:
|
|
234
|
+
if isinstance(item, dict):
|
|
235
|
+
service = item.get("service")
|
|
236
|
+
url = item.get("url")
|
|
237
|
+
if service and url:
|
|
238
|
+
services.append(ServiceInfo(service=str(service), url=str(url)))
|
|
239
|
+
|
|
240
|
+
logger.info(f"Loaded {len(services)} services from local file: {services_file}")
|
|
241
|
+
return services
|
|
242
|
+
|
|
243
|
+
except json.JSONDecodeError as e:
|
|
244
|
+
raise ValueError(f"Invalid JSON in _services.json: {e}") from e
|
|
245
|
+
|
|
246
|
+
def load_service_from_file(self, service_name: str) -> ServiceDetail:
|
|
247
|
+
"""Load service detail from local JSON file.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
service_name: Name of the service (case-insensitive)
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
ServiceDetail object loaded from {service}.json
|
|
254
|
+
|
|
255
|
+
Raises:
|
|
256
|
+
ValueError: If aws_services_dir is not set or service JSON is invalid
|
|
257
|
+
FileNotFoundError: If service JSON file doesn't exist
|
|
258
|
+
"""
|
|
259
|
+
if not self.aws_services_dir:
|
|
260
|
+
raise ValueError("aws_services_dir is not set")
|
|
261
|
+
|
|
262
|
+
# Normalize filename (lowercase, replace spaces with underscores)
|
|
263
|
+
filename = f"{service_name.lower().replace(' ', '_')}.json"
|
|
264
|
+
service_file = self.aws_services_dir / filename
|
|
265
|
+
|
|
266
|
+
if not service_file.exists():
|
|
267
|
+
raise FileNotFoundError(f"Service file not found: {service_file}")
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
with open(service_file, encoding="utf-8") as f:
|
|
271
|
+
data = json.load(f)
|
|
272
|
+
|
|
273
|
+
service_detail = ServiceDetail.model_validate(data)
|
|
274
|
+
logger.debug(f"Loaded service {service_name} from local file: {service_file}")
|
|
275
|
+
return service_detail
|
|
276
|
+
|
|
277
|
+
except json.JSONDecodeError as e:
|
|
278
|
+
raise ValueError(f"Invalid JSON in {service_file}: {e}") from e
|
|
279
|
+
|
|
280
|
+
def clear_disk_cache(self) -> None:
|
|
281
|
+
"""Remove all cached files from disk."""
|
|
282
|
+
if not self.enable_cache or not self._cache_dir.exists():
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
for cache_file in self._cache_dir.glob("*.json"):
|
|
286
|
+
try:
|
|
287
|
+
cache_file.unlink()
|
|
288
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
289
|
+
logger.warning(f"Failed to delete cache file {cache_file}: {e}")
|
|
290
|
+
|
|
291
|
+
logger.info("Cleared disk cache")
|