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 .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: ...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: .memorydb..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 ...amazonaws.com // and rejects the bare ..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: .s3-accesspoint[-fips][.dualstack]..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 (.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: .s3express-..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: .s3-website..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): .s3-website-.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: .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: .s3..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: .s3.dualstack..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: .s3-.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 }