arnmatch 2026.1.0__tar.gz → 2026.1.2__tar.gz

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.
@@ -32,28 +32,30 @@ uv run arnmatch <arn>
32
32
  ### Core Library (`src/arnmatch/`)
33
33
 
34
34
  - `__init__.py` - Main module with `arnmatch(arn)` function and `ARN` dataclass
35
- - `arn_patterns.py` - Generated file containing compiled regex patterns indexed by service
35
+ - `arn_patterns.py` - Generated file containing compiled regex patterns indexed by service and AWS SDK services mapping
36
36
 
37
- The `arnmatch()` function splits the ARN, looks up patterns by service, and returns an `ARN` with partition, service, region, account, resource_type, and captured groups. The `resource_id` and `resource_name` properties use heuristics (prefer groups ending in "Id" or "Name").
37
+ The `arnmatch()` function splits the ARN, looks up patterns by service, and returns an `ARN` with partition, service, region, account, resource_type, and captured groups. The `resource_id` and `resource_name` properties use heuristics (prefer groups ending in "Id" or "Name"). The `aws_sdk_services` property returns boto3 client names for the service.
38
38
 
39
39
  ### Code Generation (`codegen/`)
40
40
 
41
41
  - `scraper.py` - Scrapes AWS service authorization reference pages, caches results with joblib
42
42
  - `codegen.py` - Processes resources and generates `arn_patterns.py`
43
+ - `index_sdk.py` - Maps ARN service names to AWS SDK (boto3) client names
43
44
 
44
- Data flow: AWS docs → `scraper.py` → raw resources → `codegen.py` → `codegen/build/arn_patterns.py` → (copied by `make build`) → `src/arnmatch/arn_patterns.py`
45
+ Data flow: AWS docs → `scraper.py` → raw resources → `codegen.py` + `index_sdk.py` → `codegen/build/arn_patterns.py` → (copied by `make build`) → `src/arnmatch/arn_patterns.py`
45
46
 
46
47
  ### Key Design Decisions
47
48
 
48
49
  1. **Pattern ordering**: Patterns sorted by specificity (more literal segments first) for correct matching
49
50
  2. **Service index**: O(1) lookup by service before pattern matching
50
51
  3. **Overrides in codegen.py**: `PATTERN_OVERRIDES` fixes AWS docs that use wildcards instead of capture groups; `PATTERN_INCLUDES` adds patterns not in docs (EKS k8s resources, Inspector legacy)
51
- 4. **Zero runtime dependencies**: Only codegen has external deps (requests, beautifulsoup4, joblib)
52
+ 4. **SDK service mapping**: `index_sdk.py` maps ARN service names to boto3 client names using botocore metadata (signingName/endpointPrefix), with manual overrides for edge cases and excludes for discontinued/console-only services
53
+ 5. **Zero runtime dependencies**: Only codegen has external deps (requests, beautifulsoup4, joblib, boto3)
52
54
 
53
55
  ## Build Notes
54
56
 
55
57
  - Uses `uv` for package management (not pip)
56
58
  - Build system is hatchling with dynamic version from `src/arnmatch/__init__.py`
57
- - **Versioning**: CalVer format `YYYY.MM.MICRO` (e.g., `2026.01.0`)
59
+ - **Versioning**: CalVer format `YYYY.0M.MICRO` (e.g., `2026.01.0`)
58
60
  - Always run `make build` before publishing to ensure patterns are current
59
61
  - Scraper cache lives in `.cache/` - delete if AWS docs change significantly
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arnmatch
3
- Version: 2026.1.0
3
+ Version: 2026.1.2
4
4
  Summary: Parse AWS ARNs into structured data (2000+ resource types)
5
5
  Author-email: Andrey Gubarev <andrey@andreygubarev.com>
6
6
  Requires-Python: >=3.10
@@ -37,6 +37,7 @@ pip install arnmatch
37
37
  ```bash
38
38
  $ uvx arnmatch "arn:aws:lambda:us-east-1:123456789012:function:my-function"
39
39
  aws_service: lambda
40
+ aws_sdk_services: lambda
40
41
  aws_region: us-east-1
41
42
  aws_account: 123456789012
42
43
  resource_type: function
@@ -52,13 +53,14 @@ from arnmatch import arnmatch
52
53
  arn = "arn:aws:lambda:us-east-1:123456789012:function:my-function"
53
54
  result = arnmatch(arn)
54
55
 
55
- print(result.aws_service) # lambda
56
- print(result.aws_region) # us-east-1
57
- print(result.aws_account) # 123456789012
58
- print(result.resource_type) # function
59
- print(result.resource_id) # my-function
60
- print(result.resource_name) # my-function
61
- print(result.attributes) # {'Partition': 'aws', 'Region': 'us-east-1', ...}
56
+ print(result.aws_service) # lambda
57
+ print(result.aws_sdk_services) # ['lambda']
58
+ print(result.aws_region) # us-east-1
59
+ print(result.aws_account) # 123456789012
60
+ print(result.resource_type) # function
61
+ print(result.resource_id) # my-function
62
+ print(result.resource_name) # my-function
63
+ print(result.attributes) # {'Partition': 'aws', 'Region': 'us-east-1', ...}
62
64
  ```
63
65
 
64
66
  ## API Reference
@@ -89,6 +91,7 @@ Properties:
89
91
  |----------|-------------|
90
92
  | `resource_id` | Resource identifier (prefers groups ending in `Id`, falls back to `Name`, then last group) |
91
93
  | `resource_name` | Resource name (prefers groups ending in `Name`, falls back to `resource_id`) |
94
+ | `aws_sdk_services` | List of boto3 client names for this service (e.g., `['elb', 'elbv2']` for elasticloadbalancing) |
92
95
 
93
96
  ### `ARNError`
94
97
 
@@ -96,7 +99,7 @@ Exception raised when ARN parsing fails. Inherits from `ValueError`.
96
99
 
97
100
  ## Versioning
98
101
 
99
- This project uses [CalVer](https://calver.org/) with format `YYYY.MM.MICRO` (e.g., `2026.01.0`).
102
+ This project uses [CalVer](https://calver.org/) with format `YYYY.0M.MICRO` (e.g., `2026.01.0`).
100
103
 
101
104
  ## Development
102
105
 
@@ -29,6 +29,7 @@ pip install arnmatch
29
29
  ```bash
30
30
  $ uvx arnmatch "arn:aws:lambda:us-east-1:123456789012:function:my-function"
31
31
  aws_service: lambda
32
+ aws_sdk_services: lambda
32
33
  aws_region: us-east-1
33
34
  aws_account: 123456789012
34
35
  resource_type: function
@@ -44,13 +45,14 @@ from arnmatch import arnmatch
44
45
  arn = "arn:aws:lambda:us-east-1:123456789012:function:my-function"
45
46
  result = arnmatch(arn)
46
47
 
47
- print(result.aws_service) # lambda
48
- print(result.aws_region) # us-east-1
49
- print(result.aws_account) # 123456789012
50
- print(result.resource_type) # function
51
- print(result.resource_id) # my-function
52
- print(result.resource_name) # my-function
53
- print(result.attributes) # {'Partition': 'aws', 'Region': 'us-east-1', ...}
48
+ print(result.aws_service) # lambda
49
+ print(result.aws_sdk_services) # ['lambda']
50
+ print(result.aws_region) # us-east-1
51
+ print(result.aws_account) # 123456789012
52
+ print(result.resource_type) # function
53
+ print(result.resource_id) # my-function
54
+ print(result.resource_name) # my-function
55
+ print(result.attributes) # {'Partition': 'aws', 'Region': 'us-east-1', ...}
54
56
  ```
55
57
 
56
58
  ## API Reference
@@ -81,6 +83,7 @@ Properties:
81
83
  |----------|-------------|
82
84
  | `resource_id` | Resource identifier (prefers groups ending in `Id`, falls back to `Name`, then last group) |
83
85
  | `resource_name` | Resource name (prefers groups ending in `Name`, falls back to `resource_id`) |
86
+ | `aws_sdk_services` | List of boto3 client names for this service (e.g., `['elb', 'elbv2']` for elasticloadbalancing) |
84
87
 
85
88
  ### `ARNError`
86
89
 
@@ -88,7 +91,7 @@ Exception raised when ARN parsing fails. Inherits from `ValueError`.
88
91
 
89
92
  ## Versioning
90
93
 
91
- This project uses [CalVer](https://calver.org/) with format `YYYY.MM.MICRO` (e.g., `2026.01.0`).
94
+ This project uses [CalVer](https://calver.org/) with format `YYYY.0M.MICRO` (e.g., `2026.01.0`).
92
95
 
93
96
  ## Development
94
97
 
@@ -1,6 +1,6 @@
1
1
  # /// script
2
2
  # requires-python = ">=3.10"
3
- # dependencies = ["requests", "joblib", "beautifulsoup4"]
3
+ # dependencies = ["requests", "joblib", "beautifulsoup4", "boto3"]
4
4
  # ///
5
5
 
6
6
  import logging
@@ -8,6 +8,7 @@ import re
8
8
  from pathlib import Path
9
9
 
10
10
  from scraper import AWSScraper
11
+ from index_sdk import SDKServiceIndexer
11
12
 
12
13
  log = logging.getLogger(__name__)
13
14
 
@@ -233,8 +234,8 @@ class CodeGenerator:
233
234
  ("dms", "ReplicationSubnetGroup"): ["subgrp"],
234
235
  }
235
236
 
236
- def generate(self, resources, output_path):
237
- """Generate Python file with ARN patterns."""
237
+ def generate(self, resources, sdk_services_mapping, output_path):
238
+ """Generate Python file with ARN patterns and SDK services mapping."""
238
239
  # Group by service
239
240
  by_service = {}
240
241
  for r in resources:
@@ -258,9 +259,17 @@ class CodeGenerator:
258
259
  f.write(f' (re.compile(r"{regex}"), {type_names!r}),\n')
259
260
  f.write(" ],\n")
260
261
 
262
+ f.write("}\n\n")
263
+
264
+ # Write SDK services mapping
265
+ f.write("# Auto-generated mapping: ARN service -> AWS SDK client names\n")
266
+ f.write("AWS_SDK_SERVICES = {\n")
267
+ for arn_svc, clients in sorted(sdk_services_mapping.items()):
268
+ f.write(f" {arn_svc!r}: {clients!r},\n")
261
269
  f.write("}\n")
262
270
 
263
271
  log.info(f"Wrote {len(resources)} patterns for {len(by_service)} services to {output_path}")
272
+ log.info(f"Wrote SDK mapping for {len(sdk_services_mapping)} services")
264
273
 
265
274
  def pattern_to_regex(self, arn_pattern):
266
275
  """Convert ARN pattern to regex with named capture groups."""
@@ -300,14 +309,19 @@ def main():
300
309
  for svc in services:
301
310
  resources.extend(scraper.get_resources(svc["href"]))
302
311
 
303
- # Process
312
+ # Process ARN patterns
304
313
  indexer = ARNIndexer()
305
314
  resources = indexer.process(resources)
306
315
 
316
+ # Build SDK services mapping
317
+ arn_services = {r["arn_service"] for r in resources}
318
+ sdk_indexer = SDKServiceIndexer()
319
+ sdk_mapping = sdk_indexer.process(arn_services)
320
+
307
321
  # Generate
308
322
  BUILD_DIR.mkdir(exist_ok=True)
309
323
  generator = CodeGenerator()
310
- generator.generate(resources, BUILD_DIR / "arn_patterns.py")
324
+ generator.generate(resources, sdk_mapping, BUILD_DIR / "arn_patterns.py")
311
325
 
312
326
 
313
327
  if __name__ == "__main__":
@@ -0,0 +1,194 @@
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = ["boto3"]
4
+ # ///
5
+
6
+ """Maps ARN service names to AWS SDK (boto3) client names."""
7
+
8
+ import gzip
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+
13
+
14
+ class SDKServiceIndexer:
15
+ """Builds mapping from ARN service names to AWS SDK client names."""
16
+
17
+ # Phase 3: Manual overrides for services where botocore metadata doesn't match
18
+ # Format: "arn_service" -> ["sdk_client1", "sdk_client2", ...]
19
+ OVERRIDES = {
20
+ # AI DevOps uses aiops client
21
+ "aidevops": ["aiops"],
22
+ # AppMesh preview uses appmesh client
23
+ "appmesh-preview": ["appmesh"],
24
+ # Service Catalog uses 'catalog' in ARNs but 'servicecatalog' client
25
+ "catalog": ["servicecatalog"],
26
+ # CloudWatch uses 'monitoring' as endpointPrefix but 'cloudwatch' in ARNs
27
+ "cloudwatch": ["cloudwatch"],
28
+ # Partner Central has multiple sub-clients
29
+ "partnercentral": [
30
+ "partnercentral-account",
31
+ "partnercentral-benefits",
32
+ "partnercentral-channel",
33
+ "partnercentral-selling",
34
+ ],
35
+ # AWS Private 5G uses privatenetworks client
36
+ "private-networks": ["privatenetworks"],
37
+ # RDS IAM auth uses rds client
38
+ "rds-db": ["rds"],
39
+ # Route53 recovery services
40
+ "route53-recovery-control": [
41
+ "route53-recovery-cluster",
42
+ "route53-recovery-control-config",
43
+ ],
44
+ # S3 variants map to s3 client
45
+ "s3-object-lambda": ["s3"],
46
+ "s3express": ["s3"],
47
+ }
48
+
49
+ # Discontinued/EOL services
50
+ EXCLUDES_DISCONTINUED = {
51
+ "a4b", # Alexa for Business
52
+ "bugbust", # AWS BugBust
53
+ "codestar", # CodeStar
54
+ "elastic-inference", # EOL April 2024
55
+ "elastictranscoder", # Replaced by MediaConvert
56
+ "honeycode", # Honeycode
57
+ "iotfleethub", # EOL October 2025
58
+ "lookoutmetrics", # Lookout for Metrics
59
+ "lookoutvision", # Lookout for Vision
60
+ "monitron", # Monitron
61
+ "nimble", # Nimble Studio
62
+ "opsworks", # OpsWorks Stacks - EOL May 2024
63
+ "opsworks-cm", # OpsWorks Chef/Puppet - EOL 2024
64
+ "qldb", # QLDB - EOL 2025
65
+ "robomaker", # RoboMaker - EOL 2025
66
+ "worklink", # WorkLink
67
+ }
68
+
69
+ # Console-only services (no SDK)
70
+ EXCLUDES_CONSOLE = {
71
+ "appstudio", # AWS App Studio
72
+ "cloudshell", # AWS CloudShell
73
+ "consoleapp", # Console Mobile App
74
+ "elemental-appliances-software", # Physical hardware
75
+ "elemental-support-cases", # Support tickets
76
+ "identity-sync", # Identity sync
77
+ "iq", # AWS IQ
78
+ "iq-permission", # AWS IQ
79
+ "mapcredits", # AWS MAP credits
80
+ "one", # Amazon One Enterprise (palm recognition)
81
+ "payments", # Billing payments
82
+ "pricingplanmanager", # Pricing plans
83
+ "purchase-orders", # Billing purchase orders
84
+ "securityagent", # AWS Security Agent (preview)
85
+ "sqlworkbench", # Redshift Query Editor
86
+ "ts", # AWS Diagnostic Tools
87
+ "vendor-insights", # Marketplace Vendor Insights
88
+ }
89
+
90
+ # Services using non-boto3 SDK (IDE plugins, CLI, OpenAI SDK, etc.)
91
+ EXCLUDES_NOSDK = {
92
+ "apptest", # AWS Application Testing
93
+ "bedrock-mantle", # Uses OpenAI SDK
94
+ "codewhisperer", # IDE extension (now Q Developer)
95
+ "freertos", # FreeRTOS device SDK
96
+ "qdeveloper", # IDE plugins only
97
+ "transform", # CLI only
98
+ "transform-custom", # CLI only
99
+ }
100
+
101
+ def __init__(self):
102
+ import botocore
103
+ self.botocore_data = Path(botocore.__file__).parent / "data"
104
+
105
+ def process(self, arn_services):
106
+ """Build ARN service -> SDK clients mapping."""
107
+ # Get all boto3 client metadata
108
+ metadata = self.metadata_load()
109
+
110
+ result = {}
111
+
112
+ for arn_service in sorted(arn_services):
113
+ # Phase 3: Check manual overrides first
114
+ if arn_service in self.OVERRIDES:
115
+ result[arn_service] = self.OVERRIDES[arn_service]
116
+ continue
117
+
118
+ # Known no-SDK services
119
+ excludes = self.EXCLUDES_DISCONTINUED | self.EXCLUDES_CONSOLE | self.EXCLUDES_NOSDK
120
+ if arn_service in excludes:
121
+ result[arn_service] = []
122
+ continue
123
+
124
+ # Phase 1: Direct name match
125
+ if arn_service in metadata:
126
+ result[arn_service] = [arn_service]
127
+ # Also check for additional clients via metadata
128
+ additional = self.metadata_match(
129
+ arn_service, metadata, exclude={arn_service}
130
+ )
131
+ if additional:
132
+ result[arn_service].extend(sorted(additional))
133
+ continue
134
+
135
+ # Phase 2: Find via botocore metadata (signingName/endpointPrefix)
136
+ clients = self.metadata_match(arn_service, metadata)
137
+ if clients:
138
+ result[arn_service] = sorted(clients)
139
+ continue
140
+
141
+ # No mapping found
142
+ raise ValueError(f"No SDK client mapping for ARN service: {arn_service}")
143
+
144
+ return result
145
+
146
+ def metadata_load(self):
147
+ """Load metadata for all boto3 clients."""
148
+ metadata = {}
149
+
150
+ for sdk_service in os.listdir(self.botocore_data):
151
+ client_path = self.botocore_data / sdk_service
152
+ if not client_path.is_dir():
153
+ continue
154
+
155
+ # Find latest version
156
+ versions = sorted(
157
+ [d for d in os.listdir(client_path) if d[0].isdigit()],
158
+ reverse=True,
159
+ )
160
+ if not versions:
161
+ continue
162
+
163
+ # Load service metadata
164
+ service_file = client_path / versions[0] / "service-2.json.gz"
165
+ if not service_file.exists():
166
+ continue
167
+
168
+ with gzip.open(service_file) as f:
169
+ data = json.load(f)
170
+ metadata[sdk_service] = data.get("metadata", {})
171
+
172
+ return metadata
173
+
174
+ def metadata_match(self, arn_service, metadata, exclude=None):
175
+ """Find SDK clients whose signingName or endpointPrefix matches ARN service."""
176
+ exclude = exclude or set()
177
+ matches = set()
178
+
179
+ for sdk_service, meta in metadata.items():
180
+ if sdk_service in exclude:
181
+ continue
182
+
183
+ # Check signingName first (more specific)
184
+ signing_name = meta.get("signingName")
185
+ if signing_name == arn_service:
186
+ matches.add(sdk_service)
187
+ continue
188
+
189
+ # Check endpointPrefix (fallback)
190
+ endpoint_prefix = meta.get("endpointPrefix")
191
+ if endpoint_prefix == arn_service:
192
+ matches.add(sdk_service)
193
+
194
+ return matches
@@ -1,12 +1,12 @@
1
1
  """ARN pattern matching using regex patterns."""
2
2
 
3
- __version__ = "2026.01.0"
3
+ __version__ = "2026.01.2"
4
4
 
5
5
  import sys
6
6
  from dataclasses import dataclass
7
7
  from functools import cached_property
8
8
 
9
- from .arn_patterns import ARN_PATTERNS
9
+ from .arn_patterns import ARN_PATTERNS, AWS_SDK_SERVICES
10
10
 
11
11
  # Standard groups that are not resource-specific
12
12
  STANDARD_GROUPS = {"Partition", "Region", "Account"}
@@ -77,6 +77,17 @@ class ARN:
77
77
  # Fall back to resource_id
78
78
  return self.resource_id
79
79
 
80
+ @cached_property
81
+ def aws_sdk_services(self):
82
+ """Get AWS SDK (boto3) client names for this resource's service.
83
+
84
+ Returns list of client names that can interact with this resource type.
85
+ May return multiple clients for services with versioned APIs
86
+ (e.g., ['elb', 'elbv2'] for elasticloadbalancing).
87
+ Returns empty list if no SDK client exists.
88
+ """
89
+ return AWS_SDK_SERVICES.get(self.aws_service, [])
90
+
80
91
 
81
92
  def arnmatch(arn: str) -> ARN:
82
93
  """Match ARN against patterns.
@@ -121,6 +132,7 @@ def main() -> None:
121
132
  try:
122
133
  result = arnmatch(arn)
123
134
  print(f"aws_service: {result.aws_service}")
135
+ print(f"aws_sdk_services: {','.join(result.aws_sdk_services)}")
124
136
  print(f"aws_region: {result.aws_region}")
125
137
  print(f"aws_account: {result.aws_account}")
126
138
  print(f"resource_type: {result.resource_type}")
@@ -2414,15 +2414,17 @@ ARN_PATTERNS = {
2414
2414
  (re.compile(r"^arn:(?P<Partition>[\w-]+):security-ir:(?P<Region>[\w-]*):(?P<Account>\d{12}):membership/(?P<MembershipId>.+?)$"), ['membership']),
2415
2415
  ],
2416
2416
  'securityagent': [
2417
- (re.compile(r"^arn:(?P<Partition>[\w-]+):securityagent:(?P<Region>[\w-]*):(?P<Account>\d{12}):agent-instance/(?P<AgentId>.+?)/artifact/(?P<ArtifactId>.+?)$"), ['Artifact']),
2418
- (re.compile(r"^arn:(?P<Partition>[\w-]+):securityagent:(?P<Region>[\w-]*):(?P<Account>\d{12}):agent-instance/(?P<AgentId>.+?)/finding/(?P<FindingId>.+?)$"), ['Finding']),
2419
- (re.compile(r"^arn:(?P<Partition>[\w-]+):securityagent:(?P<Region>[\w-]*):(?P<Account>\d{12}):agent-instance/(?P<AgentId>.+?)/pentest/(?P<PentestId>.+?)$"), ['Pentest']),
2420
- (re.compile(r"^arn:(?P<Partition>[\w-]+):securityagent:(?P<Region>[\w-]*):(?P<Account>\d{12}):agent-instance/(?P<AgentId>.+?)/pentest-job/(?P<JobId>.+?)$"), ['PentestJob']),
2421
- (re.compile(r"^arn:(?P<Partition>[\w-]+):securityagent:(?P<Region>[\w-]*):(?P<Account>\d{12}):agent-instance/(?P<AgentId>.+?)/pentest-task/(?P<TaskId>.+?)$"), ['PentestTask']),
2417
+ (re.compile(r"^arn:(?P<Partition>[\w-]+):securityagent:(?P<Region>[\w-]*):(?P<Account>\d{12}):agent-space/(?P<AgentId>.+?)/artifact/(?P<ArtifactId>.+?)$"), ['Artifact']),
2418
+ (re.compile(r"^arn:(?P<Partition>[\w-]+):securityagent:(?P<Region>[\w-]*):(?P<Account>\d{12}):agent-space/(?P<AgentId>.+?)/finding/(?P<FindingId>.+?)$"), ['Finding']),
2419
+ (re.compile(r"^arn:(?P<Partition>[\w-]+):securityagent:(?P<Region>[\w-]*):(?P<Account>\d{12}):agent-space/(?P<AgentId>.+?)/pentest/(?P<PentestId>.+?)$"), ['Pentest']),
2420
+ (re.compile(r"^arn:(?P<Partition>[\w-]+):securityagent:(?P<Region>[\w-]*):(?P<Account>\d{12}):agent-space/(?P<AgentId>.+?)/pentest-job/(?P<JobId>.+?)$"), ['PentestJob']),
2421
+ (re.compile(r"^arn:(?P<Partition>[\w-]+):securityagent:(?P<Region>[\w-]*):(?P<Account>\d{12}):agent-space/(?P<AgentId>.+?)/pentest-task/(?P<TaskId>.+?)$"), ['PentestTask']),
2422
2422
  (re.compile(r"^arn:(?P<Partition>[\w-]+):securityagent:(?P<Region>[\w-]*):(?P<Account>\d{12}):agent-instance/(?P<AgentId>.+?)$"), ['AgentInstance']),
2423
+ (re.compile(r"^arn:(?P<Partition>[\w-]+):securityagent:(?P<Region>[\w-]*):(?P<Account>\d{12}):agent-space/(?P<AgentId>.+?)$"), ['AgentSpace']),
2423
2424
  (re.compile(r"^arn:(?P<Partition>[\w-]+):securityagent:(?P<Region>[\w-]*):(?P<Account>\d{12}):application/(?P<ApplicationId>.+?)$"), ['Application']),
2424
2425
  (re.compile(r"^arn:(?P<Partition>[\w-]+):securityagent:(?P<Region>[\w-]*):(?P<Account>\d{12}):control/(?P<ControlId>.+?)$"), ['Control']),
2425
2426
  (re.compile(r"^arn:(?P<Partition>[\w-]+):securityagent:(?P<Region>[\w-]*):(?P<Account>\d{12}):integration/(?P<IntegrationId>.+?)$"), ['Integration']),
2427
+ (re.compile(r"^arn:(?P<Partition>[\w-]+):securityagent:(?P<Region>[\w-]*):(?P<Account>\d{12}):security-requirement/(?P<SecurityRequirementId>.+?)$"), ['SecurityRequirement']),
2426
2428
  ],
2427
2429
  'securityhub': [
2428
2430
  (re.compile(r"^arn:(?P<Partition>[\w-]+):securityhub:(?P<Region>[\w-]*):(?P<Account>\d{12}):product/(?P<Company>.+?)/(?P<ProductId>.+?)$"), ['product']),
@@ -2794,3 +2796,362 @@ ARN_PATTERNS = {
2794
2796
  (re.compile(r"^arn:(?P<Partition>[\w-]+):xray:(?P<Region>[\w-]*):(?P<Account>\d{12}):sampling-rule/(?P<SamplingRuleName>.+?)$"), ['sampling-rule']),
2795
2797
  ],
2796
2798
  }
2799
+
2800
+ # Auto-generated mapping: ARN service -> AWS SDK client names
2801
+ AWS_SDK_SERVICES = {
2802
+ 'a4b': [],
2803
+ 'access-analyzer': ['accessanalyzer'],
2804
+ 'account': ['account'],
2805
+ 'acm': ['acm'],
2806
+ 'acm-pca': ['acm-pca'],
2807
+ 'aidevops': ['aiops'],
2808
+ 'aiops': ['aiops'],
2809
+ 'airflow': ['mwaa'],
2810
+ 'airflow-serverless': ['mwaa-serverless'],
2811
+ 'amplify': ['amplify'],
2812
+ 'amplifybackend': ['amplifybackend'],
2813
+ 'amplifyuibuilder': ['amplifyuibuilder'],
2814
+ 'aoss': ['opensearchserverless'],
2815
+ 'apigateway': ['apigateway', 'apigatewayv2'],
2816
+ 'app-integrations': ['appintegrations'],
2817
+ 'appconfig': ['appconfig', 'appconfigdata'],
2818
+ 'appfabric': ['appfabric'],
2819
+ 'appflow': ['appflow'],
2820
+ 'application-autoscaling': ['application-autoscaling'],
2821
+ 'application-signals': ['application-signals'],
2822
+ 'appmesh': ['appmesh'],
2823
+ 'appmesh-preview': ['appmesh'],
2824
+ 'apprunner': ['apprunner'],
2825
+ 'appstream': ['appstream'],
2826
+ 'appstudio': [],
2827
+ 'appsync': ['appsync'],
2828
+ 'apptest': [],
2829
+ 'aps': ['amp'],
2830
+ 'arc-region-switch': ['arc-region-switch'],
2831
+ 'artifact': ['artifact'],
2832
+ 'athena': ['athena'],
2833
+ 'auditmanager': ['auditmanager'],
2834
+ 'autoscaling': ['autoscaling'],
2835
+ 'aws-marketplace': ['marketplace-agreement', 'marketplace-catalog', 'marketplace-deployment', 'marketplace-entitlement', 'marketplace-reporting', 'meteringmarketplace'],
2836
+ 'b2bi': ['b2bi'],
2837
+ 'backup': ['backup'],
2838
+ 'backup-gateway': ['backup-gateway'],
2839
+ 'backup-search': ['backupsearch'],
2840
+ 'batch': ['batch'],
2841
+ 'bcm-data-exports': ['bcm-data-exports'],
2842
+ 'bcm-pricing-calculator': ['bcm-pricing-calculator'],
2843
+ 'bedrock': ['bedrock', 'bedrock-agent', 'bedrock-agent-runtime', 'bedrock-data-automation', 'bedrock-data-automation-runtime', 'bedrock-runtime'],
2844
+ 'bedrock-agentcore': ['bedrock-agentcore', 'bedrock-agentcore-control'],
2845
+ 'bedrock-mantle': [],
2846
+ 'billing': ['billing'],
2847
+ 'billingconductor': ['billingconductor'],
2848
+ 'braket': ['braket'],
2849
+ 'budgets': ['budgets'],
2850
+ 'bugbust': [],
2851
+ 'cases': ['connectcases'],
2852
+ 'cassandra': ['keyspaces', 'keyspacesstreams'],
2853
+ 'catalog': ['servicecatalog'],
2854
+ 'ce': ['ce'],
2855
+ 'chatbot': ['chatbot'],
2856
+ 'chime': ['chime', 'chime-sdk-identity', 'chime-sdk-media-pipelines', 'chime-sdk-meetings', 'chime-sdk-messaging', 'chime-sdk-voice'],
2857
+ 'cleanrooms': ['cleanrooms'],
2858
+ 'cleanrooms-ml': ['cleanroomsml'],
2859
+ 'cloud9': ['cloud9'],
2860
+ 'clouddirectory': ['clouddirectory'],
2861
+ 'cloudformation': ['cloudformation'],
2862
+ 'cloudfront': ['cloudfront'],
2863
+ 'cloudhsm': ['cloudhsm', 'cloudhsmv2'],
2864
+ 'cloudsearch': ['cloudsearch', 'cloudsearchdomain'],
2865
+ 'cloudshell': [],
2866
+ 'cloudtrail': ['cloudtrail'],
2867
+ 'cloudwatch': ['cloudwatch'],
2868
+ 'codeartifact': ['codeartifact'],
2869
+ 'codebuild': ['codebuild'],
2870
+ 'codecatalyst': ['codecatalyst'],
2871
+ 'codecommit': ['codecommit'],
2872
+ 'codeconnections': ['codeconnections'],
2873
+ 'codedeploy': ['codedeploy'],
2874
+ 'codeguru-profiler': ['codeguruprofiler'],
2875
+ 'codeguru-reviewer': ['codeguru-reviewer'],
2876
+ 'codeguru-security': ['codeguru-security'],
2877
+ 'codepipeline': ['codepipeline'],
2878
+ 'codestar': [],
2879
+ 'codestar-connections': ['codestar-connections'],
2880
+ 'codestar-notifications': ['codestar-notifications'],
2881
+ 'codewhisperer': [],
2882
+ 'cognito-identity': ['cognito-identity'],
2883
+ 'cognito-idp': ['cognito-idp'],
2884
+ 'cognito-sync': ['cognito-sync'],
2885
+ 'comprehend': ['comprehend'],
2886
+ 'compute-optimizer': ['compute-optimizer'],
2887
+ 'config': ['config'],
2888
+ 'connect': ['connect', 'connect-contact-lens'],
2889
+ 'connect-campaigns': ['connectcampaigns', 'connectcampaignsv2'],
2890
+ 'consoleapp': [],
2891
+ 'controlcatalog': ['controlcatalog'],
2892
+ 'controltower': ['controltower'],
2893
+ 'cur': ['cur'],
2894
+ 'databrew': ['databrew'],
2895
+ 'dataexchange': ['dataexchange'],
2896
+ 'datapipeline': ['datapipeline'],
2897
+ 'datasync': ['datasync'],
2898
+ 'datazone': ['datazone'],
2899
+ 'dax': ['dax'],
2900
+ 'deadline': ['deadline'],
2901
+ 'detective': ['detective'],
2902
+ 'devicefarm': ['devicefarm'],
2903
+ 'directconnect': ['directconnect'],
2904
+ 'dlm': ['dlm'],
2905
+ 'dms': ['dms'],
2906
+ 'docdb-elastic': ['docdb-elastic'],
2907
+ 'drs': ['drs'],
2908
+ 'ds': ['ds'],
2909
+ 'dsql': ['dsql'],
2910
+ 'dynamodb': ['dynamodb', 'dynamodbstreams'],
2911
+ 'ec2': ['ec2'],
2912
+ 'ecr': ['ecr'],
2913
+ 'ecr-public': ['ecr-public'],
2914
+ 'ecs': ['ecs'],
2915
+ 'eks': ['eks'],
2916
+ 'elastic-inference': [],
2917
+ 'elasticache': ['elasticache'],
2918
+ 'elasticbeanstalk': ['elasticbeanstalk'],
2919
+ 'elasticfilesystem': ['efs'],
2920
+ 'elasticloadbalancing': ['elb', 'elbv2'],
2921
+ 'elasticmapreduce': ['emr'],
2922
+ 'elastictranscoder': [],
2923
+ 'elemental-appliances-software': [],
2924
+ 'elemental-support-cases': [],
2925
+ 'emr-containers': ['emr-containers'],
2926
+ 'emr-serverless': ['emr-serverless'],
2927
+ 'entityresolution': ['entityresolution'],
2928
+ 'es': ['es', 'opensearch'],
2929
+ 'events': ['events'],
2930
+ 'evidently': ['evidently'],
2931
+ 'evs': ['evs'],
2932
+ 'execute-api': ['apigatewaymanagementapi', 'connectparticipant'],
2933
+ 'finspace': ['finspace'],
2934
+ 'finspace-api': ['finspace-data'],
2935
+ 'firehose': ['firehose'],
2936
+ 'fis': ['fis'],
2937
+ 'fms': ['fms'],
2938
+ 'forecast': ['forecast', 'forecastquery'],
2939
+ 'frauddetector': ['frauddetector'],
2940
+ 'freertos': [],
2941
+ 'fsx': ['fsx'],
2942
+ 'gamelift': ['gamelift'],
2943
+ 'gameliftstreams': ['gameliftstreams'],
2944
+ 'geo': ['location'],
2945
+ 'geo-maps': ['geo-maps'],
2946
+ 'geo-places': ['geo-places'],
2947
+ 'geo-routes': ['geo-routes'],
2948
+ 'glacier': ['glacier'],
2949
+ 'globalaccelerator': ['globalaccelerator'],
2950
+ 'glue': ['glue'],
2951
+ 'grafana': ['grafana'],
2952
+ 'greengrass': ['greengrass', 'greengrassv2'],
2953
+ 'groundstation': ['groundstation'],
2954
+ 'guardduty': ['guardduty'],
2955
+ 'health': ['health'],
2956
+ 'healthlake': ['healthlake'],
2957
+ 'honeycode': [],
2958
+ 'iam': ['iam'],
2959
+ 'identity-sync': [],
2960
+ 'identitystore': ['identitystore'],
2961
+ 'imagebuilder': ['imagebuilder'],
2962
+ 'inspector': ['inspector'],
2963
+ 'inspector2': ['inspector2'],
2964
+ 'internetmonitor': ['internetmonitor'],
2965
+ 'invoicing': ['invoicing'],
2966
+ 'iot': ['iot'],
2967
+ 'iotanalytics': ['iotanalytics'],
2968
+ 'iotdeviceadvisor': ['iotdeviceadvisor'],
2969
+ 'iotevents': ['iotevents'],
2970
+ 'iotfleethub': [],
2971
+ 'iotfleetwise': ['iotfleetwise'],
2972
+ 'iotmanagedintegrations': ['iot-managed-integrations'],
2973
+ 'iotsitewise': ['iotsitewise'],
2974
+ 'iottwinmaker': ['iottwinmaker'],
2975
+ 'iotwireless': ['iotwireless'],
2976
+ 'iq': [],
2977
+ 'iq-permission': [],
2978
+ 'ivs': ['ivs', 'ivs-realtime'],
2979
+ 'ivschat': ['ivschat'],
2980
+ 'kafka': ['kafka'],
2981
+ 'kafkaconnect': ['kafkaconnect'],
2982
+ 'kendra': ['kendra'],
2983
+ 'kendra-ranking': ['kendra-ranking'],
2984
+ 'kinesis': ['kinesis'],
2985
+ 'kinesisanalytics': ['kinesisanalytics', 'kinesisanalyticsv2'],
2986
+ 'kinesisvideo': ['kinesisvideo', 'kinesis-video-archived-media', 'kinesis-video-media', 'kinesis-video-signaling', 'kinesis-video-webrtc-storage'],
2987
+ 'kms': ['kms'],
2988
+ 'lambda': ['lambda'],
2989
+ 'launchwizard': ['launch-wizard'],
2990
+ 'lex': ['lex-models', 'lex-runtime', 'lexv2-models', 'lexv2-runtime'],
2991
+ 'license-manager': ['license-manager'],
2992
+ 'license-manager-linux-subscriptions': ['license-manager-linux-subscriptions'],
2993
+ 'license-manager-user-subscriptions': ['license-manager-user-subscriptions'],
2994
+ 'lightsail': ['lightsail'],
2995
+ 'logs': ['logs'],
2996
+ 'lookoutequipment': ['lookoutequipment'],
2997
+ 'lookoutmetrics': [],
2998
+ 'lookoutvision': [],
2999
+ 'm2': ['m2'],
3000
+ 'machinelearning': ['machinelearning'],
3001
+ 'macie2': ['macie2'],
3002
+ 'managedblockchain': ['managedblockchain'],
3003
+ 'mapcredits': [],
3004
+ 'mediaconnect': ['mediaconnect'],
3005
+ 'mediaconvert': ['mediaconvert'],
3006
+ 'medialive': ['medialive'],
3007
+ 'mediapackage': ['mediapackage'],
3008
+ 'mediapackage-vod': ['mediapackage-vod'],
3009
+ 'mediapackagev2': ['mediapackagev2'],
3010
+ 'mediastore': ['mediastore', 'mediastore-data'],
3011
+ 'mediatailor': ['mediatailor'],
3012
+ 'medical-imaging': ['medical-imaging'],
3013
+ 'memorydb': ['memorydb'],
3014
+ 'mgh': ['mgh', 'migrationhub-config'],
3015
+ 'mgn': ['mgn'],
3016
+ 'migrationhub-orchestrator': ['migrationhuborchestrator'],
3017
+ 'mobiletargeting': ['pinpoint'],
3018
+ 'monitron': [],
3019
+ 'mpa': ['mpa'],
3020
+ 'mq': ['mq'],
3021
+ 'neptune-db': ['neptunedata'],
3022
+ 'neptune-graph': ['neptune-graph'],
3023
+ 'network-firewall': ['network-firewall'],
3024
+ 'networkflowmonitor': ['networkflowmonitor'],
3025
+ 'networkmanager': ['networkmanager'],
3026
+ 'networkmonitor': ['networkmonitor'],
3027
+ 'nimble': [],
3028
+ 'notifications': ['notifications'],
3029
+ 'notifications-contacts': ['notificationscontacts'],
3030
+ 'nova-act': ['nova-act'],
3031
+ 'oam': ['oam'],
3032
+ 'observabilityadmin': ['observabilityadmin'],
3033
+ 'odb': ['odb'],
3034
+ 'omics': ['omics'],
3035
+ 'one': [],
3036
+ 'opensearch': ['opensearch'],
3037
+ 'opsworks': [],
3038
+ 'opsworks-cm': [],
3039
+ 'organizations': ['organizations'],
3040
+ 'osis': ['osis'],
3041
+ 'outposts': ['outposts'],
3042
+ 'panorama': ['panorama'],
3043
+ 'partnercentral': ['partnercentral-account', 'partnercentral-benefits', 'partnercentral-channel', 'partnercentral-selling'],
3044
+ 'payment-cryptography': ['payment-cryptography', 'payment-cryptography-data'],
3045
+ 'payments': [],
3046
+ 'pca-connector-ad': ['pca-connector-ad'],
3047
+ 'pca-connector-scep': ['pca-connector-scep'],
3048
+ 'pcs': ['pcs'],
3049
+ 'personalize': ['personalize', 'personalize-events', 'personalize-runtime'],
3050
+ 'pi': ['pi'],
3051
+ 'pipes': ['pipes'],
3052
+ 'polly': ['polly'],
3053
+ 'pricingplanmanager': [],
3054
+ 'private-networks': ['privatenetworks'],
3055
+ 'profile': ['customer-profiles'],
3056
+ 'proton': ['proton'],
3057
+ 'purchase-orders': [],
3058
+ 'qapps': ['qapps'],
3059
+ 'qbusiness': ['qbusiness'],
3060
+ 'qdeveloper': [],
3061
+ 'qldb': [],
3062
+ 'quicksight': ['quicksight'],
3063
+ 'ram': ['ram'],
3064
+ 'rbin': ['rbin'],
3065
+ 'rds': ['rds', 'docdb', 'neptune'],
3066
+ 'rds-db': ['rds'],
3067
+ 'redshift': ['redshift'],
3068
+ 'redshift-serverless': ['redshift-serverless'],
3069
+ 'refactor-spaces': ['migration-hub-refactor-spaces'],
3070
+ 'rekognition': ['rekognition'],
3071
+ 'repostspace': ['repostspace'],
3072
+ 'resiliencehub': ['resiliencehub'],
3073
+ 'resource-explorer-2': ['resource-explorer-2'],
3074
+ 'resource-groups': ['resource-groups'],
3075
+ 'robomaker': [],
3076
+ 'rolesanywhere': ['rolesanywhere'],
3077
+ 'route53': ['route53'],
3078
+ 'route53-recovery-control': ['route53-recovery-cluster', 'route53-recovery-control-config'],
3079
+ 'route53-recovery-readiness': ['route53-recovery-readiness'],
3080
+ 'route53globalresolver': ['route53globalresolver'],
3081
+ 'route53profiles': ['route53profiles'],
3082
+ 'route53resolver': ['route53resolver'],
3083
+ 'rum': ['rum'],
3084
+ 's3': ['s3', 's3control'],
3085
+ 's3-object-lambda': ['s3'],
3086
+ 's3-outposts': ['s3outposts'],
3087
+ 's3express': ['s3'],
3088
+ 's3tables': ['s3tables'],
3089
+ 's3vectors': ['s3vectors'],
3090
+ 'sagemaker': ['sagemaker', 'sagemaker-a2i-runtime', 'sagemaker-edge', 'sagemaker-featurestore-runtime', 'sagemaker-metrics', 'sagemaker-runtime'],
3091
+ 'sagemaker-geospatial': ['sagemaker-geospatial'],
3092
+ 'savingsplans': ['savingsplans'],
3093
+ 'scheduler': ['scheduler'],
3094
+ 'schemas': ['schemas'],
3095
+ 'scn': ['supplychain'],
3096
+ 'sdb': ['sdb'],
3097
+ 'secretsmanager': ['secretsmanager'],
3098
+ 'security-ir': ['security-ir'],
3099
+ 'securityagent': [],
3100
+ 'securityhub': ['securityhub'],
3101
+ 'securitylake': ['securitylake'],
3102
+ 'serverlessrepo': ['serverlessrepo'],
3103
+ 'servicecatalog': ['servicecatalog', 'servicecatalog-appregistry'],
3104
+ 'servicediscovery': ['servicediscovery'],
3105
+ 'servicequotas': ['service-quotas'],
3106
+ 'ses': ['ses', 'mailmanager', 'pinpoint-email', 'sesv2'],
3107
+ 'shield': ['shield'],
3108
+ 'signer': ['signer'],
3109
+ 'simspaceweaver': ['simspaceweaver'],
3110
+ 'sms-voice': ['sms-voice', 'pinpoint-sms-voice', 'pinpoint-sms-voice-v2'],
3111
+ 'snow-device-management': ['snow-device-management'],
3112
+ 'sns': ['sns'],
3113
+ 'social-messaging': ['socialmessaging'],
3114
+ 'sqlworkbench': [],
3115
+ 'sqs': ['sqs'],
3116
+ 'ssm': ['ssm'],
3117
+ 'ssm-contacts': ['ssm-contacts'],
3118
+ 'ssm-incidents': ['ssm-incidents'],
3119
+ 'ssm-quicksetup': ['ssm-quicksetup'],
3120
+ 'ssm-sap': ['ssm-sap'],
3121
+ 'sso': ['sso', 'sso-admin'],
3122
+ 'states': ['stepfunctions'],
3123
+ 'storagegateway': ['storagegateway'],
3124
+ 'sts': ['sts'],
3125
+ 'swf': ['swf'],
3126
+ 'synthetics': ['synthetics'],
3127
+ 'textract': ['textract'],
3128
+ 'thinclient': ['workspaces-thin-client'],
3129
+ 'timestream': ['timestream-query', 'timestream-write'],
3130
+ 'timestream-influxdb': ['timestream-influxdb'],
3131
+ 'tnb': ['tnb'],
3132
+ 'transcribe': ['transcribe'],
3133
+ 'transfer': ['transfer'],
3134
+ 'transform': [],
3135
+ 'transform-custom': [],
3136
+ 'translate': ['translate'],
3137
+ 'trustedadvisor': ['trustedadvisor'],
3138
+ 'ts': [],
3139
+ 'vendor-insights': [],
3140
+ 'verifiedpermissions': ['verifiedpermissions'],
3141
+ 'voiceid': ['voice-id'],
3142
+ 'vpc-lattice': ['vpc-lattice'],
3143
+ 'waf': ['waf'],
3144
+ 'waf-regional': ['waf-regional'],
3145
+ 'wafv2': ['wafv2'],
3146
+ 'wellarchitected': ['wellarchitected'],
3147
+ 'wickr': ['wickr'],
3148
+ 'wisdom': ['wisdom', 'qconnect'],
3149
+ 'workdocs': ['workdocs'],
3150
+ 'worklink': [],
3151
+ 'workmail': ['workmail'],
3152
+ 'workmailmessageflow': ['workmailmessageflow'],
3153
+ 'workspaces': ['workspaces'],
3154
+ 'workspaces-instances': ['workspaces-instances'],
3155
+ 'workspaces-web': ['workspaces-web'],
3156
+ 'xray': ['xray'],
3157
+ }
@@ -9,6 +9,7 @@ def test_acm():
9
9
  )
10
10
  assert result.resource_type == "certificate"
11
11
  assert result.attributes["CertificateId"] == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
12
+ assert result.aws_sdk_services == ["acm"]
12
13
 
13
14
 
14
15
  def test_apigateway():
@@ -68,6 +69,8 @@ def test_cloudwatch():
68
69
  result.attributes["AlarmName"]
69
70
  == "CPU Utilization - High Warning - service-production-1"
70
71
  )
72
+ # cloudwatch is a manual override (endpointPrefix is 'monitoring')
73
+ assert result.aws_sdk_services == ["cloudwatch"]
71
74
 
72
75
 
73
76
  def test_codebuild():
@@ -212,6 +215,8 @@ def test_elasticfilesystem():
212
215
  )
213
216
  assert result.resource_type == "file-system"
214
217
  assert result.attributes["FileSystemId"] == "fs-01234567"
218
+ # elasticfilesystem maps to efs via metadata
219
+ assert result.aws_sdk_services == ["efs"]
215
220
 
216
221
 
217
222
  def test_elasticloadbalancing():
@@ -245,6 +250,8 @@ def test_elasticloadbalancing():
245
250
  assert result.resource_type == "targetgroup"
246
251
  assert result.attributes["TargetGroupName"] == "target-grp-1"
247
252
  assert result.attributes["TargetGroupId"] == "0123456789abcdef"
253
+ # elasticloadbalancing maps to multiple SDK clients
254
+ assert result.aws_sdk_services == ["elb", "elbv2"]
248
255
 
249
256
 
250
257
  def test_es():
@@ -307,6 +314,7 @@ def test_lambda():
307
314
  )
308
315
  assert result.resource_type == "function"
309
316
  assert result.attributes["FunctionName"] == "ProcessDataHandler"
317
+ assert result.aws_sdk_services == ["lambda"]
310
318
 
311
319
 
312
320
  def test_logs():
@@ -339,6 +347,8 @@ def test_rds():
339
347
  )
340
348
  assert result.resource_type == "snapshot"
341
349
  assert result.attributes["SnapshotName"] == "final-database-backup-01234567"
350
+ # rds maps to multiple SDK clients (rds, docdb, neptune share ARN format)
351
+ assert result.aws_sdk_services == ["rds", "docdb", "neptune"]
342
352
 
343
353
 
344
354
  def test_route53():
@@ -357,6 +367,7 @@ def test_s3():
357
367
  result = arnmatch("arn:aws:s3:::example-bucket-01")
358
368
  assert result.resource_type == "bucket"
359
369
  assert result.attributes["BucketName"] == "example-bucket-01"
370
+ assert result.aws_sdk_services == ["s3", "s3control"]
360
371
 
361
372
 
362
373
  def test_secretsmanager():
File without changes
File without changes
File without changes
File without changes
File without changes