From 4557592e7af8235083fd295b37e763f09be002c4 Mon Sep 17 00:00:00 2001 From: Alex English Date: Mon, 31 Aug 2026 16:30:40 -0700 Subject: [PATCH] Fix three coverage report misclassifications Skip SSM sessions in the tagging API supplement, treat IAM Identity Center reserved roles as untaggable, and collect Lambda layers. --- scripts/README.md | 13 ++++++ scripts/aws-terraform-coverage.ps1 | 63 +++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/scripts/README.md b/scripts/README.md index 0f4da61..111470f 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -132,6 +132,19 @@ every run so the output cannot be read without them. - **ECS task definitions** are reported as the current revision per family, not as every historical revision. There were 286 active revisions at the time of writing; the family's current revision is the meaningful unit. +- **Lambda layers** are reported as the layer, not as every layer version, for + the same reason. This is not a choice about granularity so much as what the + API allows: `lambda list-tags` accepts `arn:...:layer:` and rejects + `arn:...:layer::` with a `ValidationException`, so the version + cannot carry `managed-by` at all. +- **The tagging API supplement drops ARNs that are not durable resources.** The + supplement has no type filter, so it returns things Terraform could never own. + SSM sessions are the case that prompted the skip list: every + `ecs execute-command` leaves one in session history, and each was landing in + the report as an untagged, therefore unmanaged, resource. They age out on + their own, which makes them worse than a leak — the denominator moved + depending on whether anyone had shelled into a container recently. Add to + `$script:TaggingApiSkipPatterns` if another such type turns up. ### Read-only diff --git a/scripts/aws-terraform-coverage.ps1 b/scripts/aws-terraform-coverage.ps1 index 0bd97b7..3c37305 100644 --- a/scripts/aws-terraform-coverage.ps1 +++ b/scripts/aws-terraform-coverage.ps1 @@ -323,6 +323,17 @@ function Add-IamResources { $count++ continue } + # IAM Identity Center owns everything under /aws-reserved/. These roles + # are provisioned from permission sets and re-provisioned whenever the + # permission set changes, which discards any tag written here, so they + # cannot hold managed-by even though the tagging API accepts one. + if ($role.Path -like '/aws-reserved/*') { + Add-Resource -Arn $role.Arn -Source 'iam:list-roles' -Service 'iam' ` + -Type 'role' -ResourceRegion 'global' -Untaggable ` + -Note 'IAM Identity Center reserved role' + $count++ + continue + } $managedBy = Get-ManagedByFromCall -Arguments @('iam', 'list-role-tags', '--role-name', $role.RoleName) Add-Resource -Arn $role.Arn -ManagedBy $managedBy -Source 'iam:list-roles' ` -Service 'iam' -Type 'role' -ResourceRegion 'global' @@ -506,6 +517,22 @@ function Add-LambdaResources { -Type 'function' -ResourceRegion $SweepRegion $count++ } + + # Layers are a separate resource from the functions that use them, and the + # tagging API does not return them, so without this they were invisible. + # The taggable unit is the layer, not the layer version: ListTags accepts + # arn:...:layer: and rejects arn:...:layer::. Versions + # are therefore counted through their layer, the same way task definition + # revisions are counted through their family. + foreach ($layer in @((Invoke-AwsCli -Arguments @('lambda', 'list-layers', '--region', $SweepRegion)).Layers)) { + if ($null -eq $layer) { continue } + $managedBy = Get-ManagedByFromCall -AsMap -Arguments @( + 'lambda', 'list-tags', '--resource', $layer.LayerArn, '--region', $SweepRegion) + Add-Resource -Arn $layer.LayerArn -ManagedBy $managedBy ` + -Source 'lambda:list-layers' -Service 'lambda' ` + -Type 'layer' -ResourceRegion $SweepRegion + $count++ + } return $count } @@ -829,19 +856,46 @@ function Add-CloudTrailResources { # Tagging API - supplement only, run last so native results win on overlap # --------------------------------------------------------------------------- +# The tagging API has no type filter and returns some things that are not +# durable resources. Anything matching one of these is dropped rather than +# classified, because it can never carry managed-by and is not something +# Terraform could own. +# +# SSM sessions are the case that prompted this. Every `ecs execute-command` +# leaves a session in SSM's history, the tagging API returns it, and it lands in +# the report as an untagged - therefore unmanaged - resource. They age out of +# session history on their own, so this is not a slow leak; it is something +# worse for a metric, because the denominator moves depending on whether anyone +# happened to shell into a container in the days before the run. Two runs on +# 2026-08-31 differed for exactly this reason. +$script:TaggingApiSkipPatterns = @( + 'arn:aws:ssm:*:*:session/*' # a transient action, not a resource +) + +function Test-TaggingApiSkip { + param([string]$Arn) + foreach ($pattern in $script:TaggingApiSkipPatterns) { + if ($Arn -like $pattern) { return $true } + } + return $false +} + function Add-TaggingApiResources { param([string]$SweepRegion) Write-Step "tagging API supplement ($SweepRegion)" $before = $script:Resources.Count + $skipped = 0 $result = Invoke-AwsCli -Arguments @('resourcegroupstaggingapi', 'get-resources', '--region', $SweepRegion) foreach ($item in @($result.ResourceTagMappingList)) { if ($null -eq $item) { continue } + if (Test-TaggingApiSkip -Arn $item.ResourceARN) { $skipped++; continue } Add-Resource -Arn $item.ResourceARN -ManagedBy (Get-TagValueFromPairs -Pairs $item.Tags) ` -Source 'tagging-api' } $added = $script:Resources.Count - $before - Write-Host "$added new" + if ($skipped -gt 0) { Write-Host "$added new, $skipped skipped" } + else { Write-Host "$added new" } return $added } @@ -941,7 +995,12 @@ function Write-CoverageReport { 'than a fact. Nothing here verifies that an exempt resource is genuinely', 'one Terraform should not manage.'), @('ECS task definitions are reported as the current revision per family,', - 'not as every historical revision.') + 'not as every historical revision. Lambda layers are reported as the', + 'layer, not as every layer version, for the same reason: the layer is', + 'the unit ListTags accepts.'), + @('The tagging API supplement drops ARNs that are not durable resources,', + 'currently SSM sessions. They cannot carry managed-by and would', + 'otherwise move the denominator run to run.') ) foreach ($spot in $blindSpots) { Write-Host " - $($spot[0])"