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.
182 lines
6.0 KiB
Go
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
|
|
}
|