Files
bucketeer/internal/classify/aws.go
T
StasonJatham 816fa572df drop MRAP and CloudFront from findings
MRAP aliases (<alias>.mrap.accesspoint.s3-global.amazonaws.com) were 91,560 of
91,561 S3 findings: AWS-generated, not customer buckets, never publicly
resolvable, and they monopolised pipeline capacity. CloudFront distributions are
CDN endpoints, not buckets or databases. Both are now excluded so real buckets
and databases get the capacity.
2026-09-22 15:36:08 +02:00

182 lines
6.0 KiB
Go

package classify
import (
"strings"
"tea.karlcom.de/karl/bucketeer/internal/model"
)
func aws(p model.Provider, s model.Service, sem model.PublicSemantic, resource, region string) model.Match {
return model.Match{
Provider: p,
Service: s,
PublicSemantic: sem,
ResourceSpecific: true,
Region: region,
Resource: resource,
}
}
// classifyCloudFront matches <distribution>.cloudfront.net. Custom domains
// almost always CNAME to a CloudFront distribution, so this is how CDN-hosted
// static sites get discovered via DNS enrichment.
func classifyCloudFront(s *scanner) (model.Match, bool) {
n := s.n
if n != 3 || s.label(n-1) != "net" || s.label(n-2) != "cloudfront" {
return model.Match{}, false
}
// CloudFront distributions are CDN endpoints, not buckets or databases, so
// they are identified but not stored as findings.
return model.Match{
Provider: model.ProviderAWS,
Service: model.ServiceWebApp,
ResourceSpecific: false,
PublicSemantic: model.PublicSemanticPublicEndpoint,
Resource: s.label(0),
}, true
}
func classifyAWS(s *scanner) (model.Match, bool) {
n := s.n
if n < 4 || s.label(n-1) != "com" || s.label(n-2) != "amazonaws" {
return model.Match{}, false
}
if m, ok := classifyAWSS3(s); ok {
return m, true
}
// Service-after-region forms: <resource>.<region>.<service>.amazonaws.com
if n >= 5 {
switch s.label(n - 3) {
case "rds":
return awsResourceAfterRegion(s, model.ServiceRDS)
case "docdb":
return awsResourceAfterRegion(s, model.ServiceDocumentDB)
case "neptune":
return awsResourceAfterRegion(s, model.ServiceNeptune)
case "redshift", "redshift-serverless":
return awsResourceAfterRegion(s, model.ServiceRedshift)
case "es":
first := s.label(0)
switch {
case strings.HasPrefix(first, "search-"):
return aws(model.ProviderAWS, model.ServiceOpenSearch,
model.PublicSemanticPublicEndpoint, first, s.label(n-4)), true
case strings.HasPrefix(first, "vpc-"):
return aws(model.ProviderAWS, model.ServiceOpenSearch,
model.PublicSemanticVPCEndpoint, first, s.label(n-4)), true
}
case "cache":
return aws(model.ProviderAWS, model.ServiceElastiCache,
model.PublicSemanticNone, s.label(0), s.label(n-4)), true
}
}
// Service-before-region form: <resource>.memorydb.<region>.amazonaws.com
if n >= 5 && s.label(n-4) == "memorydb" {
return aws(model.ProviderAWS, model.ServiceMemoryDB,
model.PublicSemanticNone, s.label(0), s.label(n-3)), true
}
return model.Match{}, false
}
// awsResourceAfterRegion matches <resource>.<region>.<service>.amazonaws.com
// and rejects the bare <service>.<region>.amazonaws.com service endpoint.
func awsResourceAfterRegion(s *scanner, service model.Service) (model.Match, bool) {
n := s.n
if n < 5 {
return model.Match{}, false
}
resource := s.label(0)
if resource == s.label(n-3) {
return model.Match{}, false
}
return aws(model.ProviderAWS, service, model.PublicSemanticNone, resource, s.label(n-4)), true
}
func classifyAWSS3(s *scanner) (model.Match, bool) {
n := s.n
// Access points: <ap>.s3-accesspoint[-fips][.dualstack].<region>.amazonaws.com
if n >= 5 {
svcIdx := -1
switch {
case s.label(n-4) == "s3-accesspoint" || s.label(n-4) == "s3-accesspoint-fips":
svcIdx = n - 4
case n >= 6 && s.label(n-4) == "dualstack" && s.label(n-5) == "s3-accesspoint":
svcIdx = n - 5
}
if svcIdx > 0 {
res := s.label(0)
if res != "s3-accesspoint" && res != "s3-accesspoint-fips" {
return aws(model.ProviderAWS, model.ServiceS3,
model.PublicSemanticNone, res, s.label(n-3)), true
}
}
}
// Multi-region access point aliases (<alias>.mrap.accesspoint.s3-global...) are
// AWS-generated and appear in CT in enormous volume. They are not customer
// bucket names, do not resolve publicly, and were flooding the pipeline, so
// they are deliberately not treated as findings.
if n >= 6 && s.label(n-5) == "mrap" && s.label(n-4) == "accesspoint" &&
s.label(n-3) == "s3-global" {
return model.Match{}, false
}
// S3 Express: <bucket>.s3express-<zone>.<region>.amazonaws.com
if n >= 5 && strings.HasPrefix(s.label(n-4), "s3express-") {
return aws(model.ProviderAWS, model.ServiceS3,
model.PublicSemanticNone, s.label(0), s.label(n-3)), true
}
// S3 website: <bucket>.s3-website.<region>.amazonaws.com
if n >= 5 && s.label(n-4) == "s3-website" {
return aws(model.ProviderAWS, model.ServiceS3,
model.PublicSemanticNone, s.label(0), s.label(n-3)), true
}
// S3 website (region in the service label): <bucket>.s3-website-<region>.amazonaws.com
if n >= 4 && strings.HasPrefix(s.label(n-3), "s3-website-") {
return aws(model.ProviderAWS, model.ServiceS3,
model.PublicSemanticNone, s.label(0), ""), true
}
// Plain S3 without region: <bucket>.s3.amazonaws.com
if n == 4 && s.label(n-3) == "s3" {
res := s.label(0)
if res == "s3" {
return model.Match{}, false
}
return aws(model.ProviderAWS, model.ServiceS3, model.PublicSemanticNone, res, ""), true
}
// Plain S3 with region: <bucket>.s3.<region>.amazonaws.com
if n >= 5 && s.label(n-4) == "s3" {
res := s.label(0)
if res == "s3" {
return model.Match{}, false
}
return aws(model.ProviderAWS, model.ServiceS3, model.PublicSemanticNone, res, s.label(n-3)), true
}
// Dualstack S3: <bucket>.s3.dualstack.<region>.amazonaws.com
if n >= 6 && s.label(n-5) == "s3" && s.label(n-4) == "dualstack" {
res := s.label(0)
if res == "s3" {
return model.Match{}, false
}
return aws(model.ProviderAWS, model.ServiceS3, model.PublicSemanticNone, res, s.label(n-3)), true
}
// Legacy regional form: <bucket>.s3-<region>.amazonaws.com (e.g. s3-eu-west-1)
if n == 4 {
svc := s.label(n - 3)
if region, ok := strings.CutPrefix(svc, "s3-"); ok && strings.Contains(region, "-") &&
!strings.HasPrefix(svc, "s3-website") && !strings.HasPrefix(svc, "s3-accesspoint") &&
!strings.HasPrefix(svc, "s3express-") {
res := s.label(0)
if res != "s3" {
return aws(model.ProviderAWS, model.ServiceS3, model.PublicSemanticNone, res, region), true
}
}
}
return model.Match{}, false
}