ctcloud: CertSpotter-backed CT cloud-resource monitor
Single stateless Go process that consumes CT logs via embedded (patched) CertSpotter internals, classifies cloud/database/storage DNS identifiers in-process with a zero-allocation classifier, enriches via round-robin DNS resolvers with CNAME following, and batch-writes useful findings to PostgreSQL. - third_party/certspotter: 2-hunk IdentifierSink patch, no stdout/JSON/files - internal/classify: AWS/Azure/GCP/Firebase/Cloudflare R2/DigitalOcean/Supabase/OCI - internal/dns: sharded bounded TTL cache, multi-resolver load balancing + failover - internal/pipeline: bounded queues, dedup, direct-priority load shedding - internal/store: pgx batch writer, Postgres CT checkpoints (no cert contents) - internal/probe: optional non-destructive reachability checks (off by default) - Docker: distroless non-root, read-only rootfs, no app volume, resource limits - tests enforce 0 allocs/op on the classifier and sink hot path
This commit is contained in:
1 parent
9e78a114ac
commit
e8f7d6bfdf
118 files changed
+16473
No files matched your search
@@ -0,0 +1,219 @@
|
||||
// Package model holds the compact enums and hot-path structs shared by the
|
||||
// classifier, pipeline and store.
|
||||
package model
|
||||
|
||||
// Provider identifies a cloud/database/storage provider.
|
||||
type Provider uint8
|
||||
|
||||
const (
|
||||
ProviderUnknown Provider = iota
|
||||
ProviderAWS
|
||||
ProviderAzure
|
||||
ProviderGCP
|
||||
ProviderCloudflare
|
||||
ProviderDigitalOcean
|
||||
ProviderSupabase
|
||||
ProviderOracle
|
||||
)
|
||||
|
||||
func (p Provider) String() string {
|
||||
switch p {
|
||||
case ProviderAWS:
|
||||
return "aws"
|
||||
case ProviderAzure:
|
||||
return "azure"
|
||||
case ProviderGCP:
|
||||
return "gcp"
|
||||
case ProviderCloudflare:
|
||||
return "cloudflare"
|
||||
case ProviderDigitalOcean:
|
||||
return "digitalocean"
|
||||
case ProviderSupabase:
|
||||
return "supabase"
|
||||
case ProviderOracle:
|
||||
return "oracle"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Service identifies the provider service the resource belongs to.
|
||||
type Service uint8
|
||||
|
||||
const (
|
||||
ServiceUnknown Service = iota
|
||||
|
||||
ServiceObjectStorage
|
||||
ServiceS3
|
||||
ServiceBlobStorage
|
||||
ServiceDatabase
|
||||
ServicePostgres
|
||||
ServiceMySQL
|
||||
ServiceRDS
|
||||
ServiceDocumentDB
|
||||
ServiceRedshift
|
||||
ServiceOpenSearch
|
||||
ServiceNeptune
|
||||
ServiceRedis
|
||||
ServiceMemoryDB
|
||||
ServiceCosmosDB
|
||||
ServiceFirebaseDB
|
||||
ServiceElastiCache
|
||||
ServiceSearch
|
||||
ServiceQueue
|
||||
ServiceFileStorage
|
||||
ServiceTableStorage
|
||||
ServiceWebApp
|
||||
)
|
||||
|
||||
func (s Service) String() string {
|
||||
switch s {
|
||||
case ServiceObjectStorage:
|
||||
return "object_storage"
|
||||
case ServiceS3:
|
||||
return "s3"
|
||||
case ServiceBlobStorage:
|
||||
return "blob_storage"
|
||||
case ServiceDatabase:
|
||||
return "database"
|
||||
case ServicePostgres:
|
||||
return "postgres"
|
||||
case ServiceMySQL:
|
||||
return "mysql"
|
||||
case ServiceRDS:
|
||||
return "rds"
|
||||
case ServiceDocumentDB:
|
||||
return "documentdb"
|
||||
case ServiceRedshift:
|
||||
return "redshift"
|
||||
case ServiceOpenSearch:
|
||||
return "opensearch"
|
||||
case ServiceNeptune:
|
||||
return "neptune"
|
||||
case ServiceRedis:
|
||||
return "redis"
|
||||
case ServiceMemoryDB:
|
||||
return "memorydb"
|
||||
case ServiceCosmosDB:
|
||||
return "cosmosdb"
|
||||
case ServiceFirebaseDB:
|
||||
return "firebase_db"
|
||||
case ServiceElastiCache:
|
||||
return "elasticache"
|
||||
case ServiceSearch:
|
||||
return "search"
|
||||
case ServiceQueue:
|
||||
return "queue"
|
||||
case ServiceFileStorage:
|
||||
return "file_storage"
|
||||
case ServiceTableStorage:
|
||||
return "table_storage"
|
||||
case ServiceWebApp:
|
||||
return "web_app"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Source is where a candidate was discovered.
|
||||
type Source uint8
|
||||
|
||||
const (
|
||||
SourceUnknown Source = iota
|
||||
SourceDirectCT
|
||||
SourceCNAME
|
||||
SourceDNSAlias
|
||||
)
|
||||
|
||||
func (s Source) String() string {
|
||||
switch s {
|
||||
case SourceDirectCT:
|
||||
return "direct_ct"
|
||||
case SourceCNAME:
|
||||
return "cname"
|
||||
case SourceDNSAlias:
|
||||
return "dns_alias"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// PublicSemantic captures when the hostname format itself carries public
|
||||
// accessibility meaning. It never asserts that data is actually exposed.
|
||||
type PublicSemantic uint8
|
||||
|
||||
const (
|
||||
PublicSemanticNone PublicSemantic = iota
|
||||
PublicSemanticPublicEndpoint
|
||||
PublicSemanticVPCEndpoint
|
||||
PublicSemanticPublicURL
|
||||
)
|
||||
|
||||
func (p PublicSemantic) String() string {
|
||||
switch p {
|
||||
case PublicSemanticPublicEndpoint:
|
||||
return "public_endpoint"
|
||||
case PublicSemanticVPCEndpoint:
|
||||
return "vpc_endpoint"
|
||||
case PublicSemanticPublicURL:
|
||||
return "public_url"
|
||||
default:
|
||||
return "none"
|
||||
}
|
||||
}
|
||||
|
||||
// ExposureState is the outcome of an optional safe reachability check.
|
||||
type ExposureState uint8
|
||||
|
||||
const (
|
||||
ExposureUnknown ExposureState = iota
|
||||
ExposurePublicDNS
|
||||
ExposureReachable
|
||||
ExposureAuthRequired
|
||||
ExposurePublicRead
|
||||
ExposurePrivate
|
||||
ExposureNotFound
|
||||
)
|
||||
|
||||
func (e ExposureState) String() string {
|
||||
switch e {
|
||||
case ExposurePublicDNS:
|
||||
return "public_dns"
|
||||
case ExposureReachable:
|
||||
return "reachable"
|
||||
case ExposureAuthRequired:
|
||||
return "auth_required"
|
||||
case ExposurePublicRead:
|
||||
return "public_read"
|
||||
case ExposurePrivate:
|
||||
return "private"
|
||||
case ExposureNotFound:
|
||||
return "not_found"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Match is the compact result of classifying a single hostname. The string
|
||||
// fields are substrings of the input hostname (zero-copy).
|
||||
type Match struct {
|
||||
Provider Provider
|
||||
Service Service
|
||||
PublicSemantic PublicSemantic
|
||||
ResourceSpecific bool
|
||||
Region string
|
||||
Resource string
|
||||
}
|
||||
|
||||
// Candidate is a provider resource discovered directly or through DNS.
|
||||
type Candidate struct {
|
||||
Hostname string // hostname that matched (provider hostname)
|
||||
Original string // original CT hostname (equal to Hostname for direct matches)
|
||||
Region string
|
||||
Resource string
|
||||
Provider Provider
|
||||
Service Service
|
||||
Source Source
|
||||
PublicSemantic PublicSemantic
|
||||
ResourceSpecific bool
|
||||
}
|
||||
Reference in new issue
Block a user