|
4 | 4 | "net" |
5 | 5 | "net/url" |
6 | 6 | "regexp" |
| 7 | + "strings" |
7 | 8 |
|
8 | 9 | "google.golang.org/protobuf/types/known/structpb" |
9 | 10 | ) |
@@ -78,8 +79,9 @@ func extractLinksFromListValue(list *structpb.ListValue) []*LinkedItemQuery { |
78 | 79 | } |
79 | 80 |
|
80 | 81 | // A regex that matches the ARN format and extracts the service, region, account |
81 | | -// id and resource |
82 | | -var awsARNRegex = regexp.MustCompile(`^arn:[\w-]+:([\w-]+):([\w-]*):([\w-]+):([\w-]+)`) |
| 82 | +// id and resource. Uses a capture group for the full resource portion after |
| 83 | +// the account-id (which may include slashes for resource types). |
| 84 | +var awsARNRegex = regexp.MustCompile(`^arn:[\w-]+:([\w-]+):([\w-]*):([\w-]*):(.+)`) |
83 | 85 |
|
84 | 86 | // This function does all the heavy lifting for extracting linked item queries |
85 | 87 | // from strings. It will be called once for every string value in the item so |
@@ -157,23 +159,52 @@ func extractLinksFromStringValue(val string) []*LinkedItemQuery { |
157 | 159 | // If it looks like an ARN then we can construct a SEARCH query to try |
158 | 160 | // and find it. We can rely on the conventions in the AWS source here |
159 | 161 |
|
160 | | - // Validate that we have enough data to construct a query |
161 | | - if len(matches) != 5 || matches[1] == "" || matches[3] == "" || matches[4] == "" { |
| 162 | + // Basic validation |
| 163 | + if len(matches) != 5 || matches[1] == "" { |
162 | 164 | return nil |
163 | 165 | } |
164 | 166 |
|
165 | | - // By convention the scope is {accountID}.{region} unless region is |
166 | | - // blank in which case it's just {accountID} |
| 167 | + // Parsed ARN parts |
| 168 | + service := matches[1] // e.g. "ec2", "iam", "s3" |
| 169 | + region := matches[2] // may be empty for global services (iam, cloudfront) |
| 170 | + accountID := matches[3] // may be empty (e.g. s3, route53) |
| 171 | + resource := matches[4] // full resource segment (may contain ":" or "/") |
| 172 | + |
| 173 | + // Extract resource type from the resource field (everything before first "/" or ":" if present) |
| 174 | + resourceType := resource |
| 175 | + if idx := strings.IndexAny(resource, "/:"); idx != -1 { |
| 176 | + resourceType = resource[:idx] |
| 177 | + } |
| 178 | + |
| 179 | + // Determine scope using a simple rule: |
| 180 | + // - No account → wildcard scope |
| 181 | + // - Account, no region → account-only |
| 182 | + // - Account and region → account.region |
167 | 183 | var scope string |
168 | | - if matches[2] == "" { |
169 | | - scope = matches[3] |
| 184 | + if accountID == "" { |
| 185 | + scope = WILDCARD |
| 186 | + } else if region == "" { |
| 187 | + scope = accountID |
170 | 188 | } else { |
171 | | - scope = matches[3] + "." + matches[2] |
| 189 | + scope = accountID + "." + region |
172 | 190 | } |
173 | 191 |
|
174 | | - // By convention the type is the service name, plus the resource name, |
175 | | - // we can extract this from the ARN also |
176 | | - queryType := matches[1] + "-" + matches[4] |
| 192 | + // Determine type using a consistent rule. Default to service-resourceType if available. |
| 193 | + queryType := service |
| 194 | + if resourceType != "" { |
| 195 | + queryType = service + "-" + resourceType |
| 196 | + } |
| 197 | + // Special-case S3 ARNs that omit account and region → treat as bucket references |
| 198 | + if service == "s3" && accountID == "" && region == "" { |
| 199 | + queryType = "s3-bucket" |
| 200 | + |
| 201 | + // If this is an S3 object ARN (contains /), extract just the bucket |
| 202 | + if strings.Contains(resource, "/") { |
| 203 | + bucketName := strings.SplitN(resource, "/", 2)[0] |
| 204 | + // Construct a bucket-only ARN for the query |
| 205 | + val = "arn:aws:s3:::" + bucketName |
| 206 | + } |
| 207 | + } |
177 | 208 |
|
178 | 209 | return []*LinkedItemQuery{ |
179 | 210 | { |
|
0 commit comments