Function bodies 28 total
FetchCertificates function · go · L18-L62 (45 LOC)cert.go
func FetchCertificates(records []Record) {
lookupTargets := make(map[string][]int)
for i, r := range records {
if shouldCheckCert(r.Type) {
name := r.Name
lookupTargets[name] = append(lookupTargets[name], i)
}
}
results := make(map[string]CertInfo)
var mu sync.Mutex
var wg sync.WaitGroup
sem := make(chan struct{}, 10)
for name := range lookupTargets {
wg.Add(1)
go func(name string) {
defer wg.Done()
sem <- struct{}{}
info := lookupCert(name)
<-sem
mu.Lock()
results[name] = info
mu.Unlock()
}(name)
}
wg.Wait()
for i := range records {
if !shouldCheckCert(records[i].Type) {
records[i].CertIssuer = "n/a"
records[i].CertExpires = "n/a"
records[i].TLSVersion = "n/a"
continue
}
info := results[records[i].Name]
records[i].CertIssuer = info.Issuer
records[i].CertExpires = info.Expires
records[i].TLSVersion = info.TLSVersion
records[i].CertError = info.Error
}
}shouldCheckCert function · go · L64-L71 (8 LOC)cert.go
func shouldCheckCert(recordType string) bool {
switch recordType {
case "A", "AAAA", "CNAME":
return true
default:
return false
}
}lookupCert function · go · L73-L106 (34 LOC)cert.go
func lookupCert(hostname string) CertInfo {
dialer := &net.Dialer{Timeout: 5 * time.Second}
conn, err := tls.DialWithDialer(dialer, "tcp", hostname+":443", &tls.Config{
ServerName: hostname,
InsecureSkipVerify: true,
})
if err != nil {
return CertInfo{Error: formatCertError(err)}
}
defer conn.Close()
state := conn.ConnectionState()
certs := state.PeerCertificates
if len(certs) == 0 {
return CertInfo{Error: "no certificate"}
}
cert := certs[0]
issuer := cert.Issuer.Organization
issuerStr := ""
if len(issuer) > 0 {
issuerStr = issuer[0]
} else if cert.Issuer.CommonName != "" {
issuerStr = cert.Issuer.CommonName
} else {
issuerStr = "unknown"
}
return CertInfo{
Issuer: issuerStr,
Expires: cert.NotAfter.Format("2006-01-02"),
TLSVersion: tlsVersionName(state.Version),
}
}tlsVersionName function · go · L108-L121 (14 LOC)cert.go
func tlsVersionName(version uint16) string {
switch version {
case tls.VersionTLS10:
return "TLS 1.0"
case tls.VersionTLS11:
return "TLS 1.1"
case tls.VersionTLS12:
return "TLS 1.2"
case tls.VersionTLS13:
return "TLS 1.3"
default:
return fmt.Sprintf("unknown (0x%04x)", version)
}
}formatCertError function · go · L123-L133 (11 LOC)cert.go
func formatCertError(err error) string {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
return "timeout"
}
if opErr, ok := err.(*net.OpError); ok {
if opErr.Op == "dial" {
return "connection refused"
}
}
return fmt.Sprintf("%v", err)
}NewCloudflareClient function · go · L17-L28 (12 LOC)cloudflare.go
func NewCloudflareClient(cfg CloudflareConfig) (*CloudflareClient, error) {
if cfg.APIToken == "" {
return nil, nil
}
api, err := cloudflare.NewWithAPIToken(cfg.APIToken)
if err != nil {
return nil, fmt.Errorf("failed to create cloudflare client: %w", err)
}
return &CloudflareClient{api: api}, nil
}FetchRecords method · go · L30-L51 (22 LOC)cloudflare.go
func (c *CloudflareClient) FetchRecords(ctx context.Context, types []string) ([]Record, error) {
zones, err := c.api.ListZones(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list cloudflare zones: %w", err)
}
c.zones = zones
c.zoneMap = make(map[string]cloudflare.Zone, len(zones))
for _, z := range zones {
c.zoneMap[z.Name] = z
}
var records []Record
for _, zone := range zones {
zoneRecords, err := c.fetchZoneRecords(ctx, zone, types)
if err != nil {
return nil, err
}
records = append(records, zoneRecords...)
}
return records, nil
}Same scanner, your repo: https://repobility.com — Repobility
fetchZoneRecords method · go · L53-L87 (35 LOC)cloudflare.go
func (c *CloudflareClient) fetchZoneRecords(ctx context.Context, zone cloudflare.Zone, types []string) ([]Record, error) {
var records []Record
dnsRecords, _, err := c.api.ListDNSRecords(ctx, cloudflare.ZoneIdentifier(zone.ID), cloudflare.ListDNSRecordsParams{})
if err != nil {
return nil, fmt.Errorf("failed to list dns records for zone %s: %w", zone.Name, err)
}
for _, r := range dnsRecords {
if !matchesType(r.Type, types) {
continue
}
records = append(records, Record{
Domain: zone.Name,
Name: r.Name,
Value: r.Content,
Type: r.Type,
Source: "cloudflare",
})
}
if matchesType("NS", types) {
for _, ns := range zone.NameServers {
records = append(records, Record{
Domain: zone.Name,
Name: zone.Name,
Value: ns,
Type: "NS",
Source: "cloudflare",
})
}
}
return records, nil
}IsCloudflareDNS method · go · L89-L100 (12 LOC)cloudflare.go
func (c *CloudflareClient) IsCloudflareDNS(domain string) bool {
zone, ok := c.zoneMap[domain]
if !ok {
return false
}
for _, ns := range zone.NameServers {
if strings.HasSuffix(ns, ".ns.cloudflare.com") {
return true
}
}
return false
}matchesType function · go · L102-L112 (11 LOC)cloudflare.go
func matchesType(recordType string, types []string) bool {
if len(types) == 0 {
return true
}
for _, t := range types {
if strings.EqualFold(recordType, t) {
return true
}
}
return false
}LoadConfig function · go · L19-L41 (23 LOC)config.go
func LoadConfig() (*Config, error) {
cfg := &Config{}
if apiToken := os.Getenv("CLOUDFLARE_API_TOKEN"); apiToken != "" {
cfg.Cloudflare.APIToken = apiToken
return cfg, nil
}
configPath := configFilePath()
data, err := os.ReadFile(configPath)
if err != nil {
if os.IsNotExist(err) {
return cfg, nil
}
return nil, fmt.Errorf("failed to read config file %s: %w", configPath, err)
}
if err := yaml.Unmarshal(data, cfg); err != nil {
return nil, fmt.Errorf("failed to parse config file %s: %w", configPath, err)
}
return cfg, nil
}configFilePath function · go · L43-L49 (7 LOC)config.go
func configFilePath() string {
if xdgConfig := os.Getenv("XDG_CONFIG_HOME"); xdgConfig != "" {
return filepath.Join(xdgConfig, "domains", "config.yaml")
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".config", "domains", "config.yaml")
}main function · go · L12-L32 (21 LOC)main.go
func main() {
format := flag.String("format", "tsv", "Output format: tsv or json")
flag.StringVar(format, "f", "tsv", "Output format: tsv or json (shorthand)")
cert := flag.Bool("cert", false, "Fetch TLS certificate info (issuer, expiry)")
flag.BoolVar(cert, "c", false, "Fetch TLS certificate info (shorthand)")
showVersion := flag.Bool("version", false, "Print version and exit")
flag.BoolVar(showVersion, "v", false, "Print version and exit (shorthand)")
flag.Parse()
if *showVersion {
fmt.Println(Version())
return
}
types := NormalizeTypes(flag.Args())
if err := run(context.Background(), types, *format, *cert); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}run function · go · L34-L110 (77 LOC)main.go
func run(ctx context.Context, types []string, format string, fetchCert bool) error {
cfg, err := LoadConfig()
if err != nil {
return err
}
var records []Record
var cfClient *CloudflareClient
var r53Client *Route53Client
var cfErr, r53Err error
var cfRecords, r53Records []Record
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
cfClient, cfErr = NewCloudflareClient(cfg.Cloudflare)
if cfErr != nil {
fmt.Fprintf(os.Stderr, "warning: cloudflare: %v\n", cfErr)
return
}
if cfClient == nil {
fmt.Fprintln(os.Stderr, "warning: cloudflare credentials not configured, skipping")
return
}
cfRecords, cfErr = cfClient.FetchRecords(ctx, types)
if cfErr != nil {
fmt.Fprintf(os.Stderr, "warning: cloudflare: %v\n", cfErr)
}
}()
go func() {
defer wg.Done()
r53Client, r53Err = NewRoute53Client(ctx)
if r53Err != nil {
fmt.Fprintf(os.Stderr, "warning: route53: %v\n", r53Err)
return
}
r53Records, r53Err = r53Client.FetchRecords(ctx,resolveRegistrars function · go · L112-L140 (29 LOC)main.go
func resolveRegistrars(records []Record, r53Client *Route53Client, whoisClient *WhoisClient) {
domains := make(map[string]struct{})
for _, r := range records {
domains[r.Domain] = struct{}{}
}
resolved := make(map[string]string, len(domains))
var mu sync.Mutex
var wg sync.WaitGroup
sem := make(chan struct{}, 5)
for domain := range domains {
wg.Add(1)
go func(d string) {
defer wg.Done()
sem <- struct{}{}
registrar := resolveRegistrar(d, r53Client, whoisClient)
<-sem
mu.Lock()
resolved[d] = registrar
mu.Unlock()
}(domain)
}
wg.Wait()
for i := range records {
records[i].Registrar = resolved[records[i].Domain]
}
}Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
resolveRegistrar function · go · L142-L148 (7 LOC)main.go
func resolveRegistrar(domain string, r53Client *Route53Client, whoisClient *WhoisClient) string {
if r53Client != nil && r53Client.IsRoute53Registrar(domain) {
return "route53"
}
return whoisClient.LookupRegistrar(domain)
}OutputTSV function · go · L10-L26 (17 LOC)output.go
func OutputTSV(w io.Writer, records []Record, hasCert bool) error {
if hasCert {
fmt.Fprintln(w, "domain\trecord\tvalue\ttype\tsource\tregistrar\tcert_issuer\tcert_expires\ttls_version\tcert_error")
for _, r := range records {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
r.Domain, r.Name, r.Value, r.Type, r.Source, r.Registrar, r.CertIssuer, r.CertExpires, r.TLSVersion, r.CertError)
}
} else {
fmt.Fprintln(w, "domain\trecord\tvalue\ttype\tsource\tregistrar")
for _, r := range records {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
r.Domain, r.Name, r.Value, r.Type, r.Source, r.Registrar)
}
}
return nil
}OutputJSON function · go · L28-L32 (5 LOC)output.go
func OutputJSON(w io.Writer, records []Record) error {
encoder := json.NewEncoder(w)
encoder.SetIndent("", " ")
return encoder.Encode(records)
}NormalizeTypes function · go · L34-L40 (7 LOC)output.go
func NormalizeTypes(args []string) []string {
var types []string
for _, arg := range args {
types = append(types, strings.ToUpper(arg))
}
return types
}NewRoute53Client function · go · L19-L28 (10 LOC)route53.go
func NewRoute53Client(ctx context.Context) (*Route53Client, error) {
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return nil, fmt.Errorf("failed to load aws config: %w", err)
}
return &Route53Client{
client: route53.NewFromConfig(cfg),
}, nil
}FetchRecords method · go · L30-L56 (27 LOC)route53.go
func (c *Route53Client) FetchRecords(ctx context.Context, recordTypes []string) ([]Record, error) {
paginator := route53.NewListHostedZonesPaginator(c.client, &route53.ListHostedZonesInput{})
for paginator.HasMorePages() {
page, err := paginator.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list route53 hosted zones: %w", err)
}
c.zones = append(c.zones, page.HostedZones...)
}
c.zoneMap = make(map[string]types.HostedZone, len(c.zones))
for _, zone := range c.zones {
domain := strings.TrimSuffix(*zone.Name, ".")
c.zoneMap[domain] = zone
}
var records []Record
for _, zone := range c.zones {
zoneRecords, err := c.fetchZoneRecords(ctx, zone, recordTypes)
if err != nil {
return nil, err
}
records = append(records, zoneRecords...)
}
return records, nil
}fetchZoneRecords method · go · L58-L96 (39 LOC)route53.go
func (c *Route53Client) fetchZoneRecords(ctx context.Context, zone types.HostedZone, recordTypes []string) ([]Record, error) {
var records []Record
domain := strings.TrimSuffix(*zone.Name, ".")
var allRecordSets []types.ResourceRecordSet
input := &route53.ListResourceRecordSetsInput{HostedZoneId: zone.Id}
for {
out, err := c.client.ListResourceRecordSets(ctx, input)
if err != nil {
return nil, fmt.Errorf("failed to list record sets for zone %s: %w", domain, err)
}
allRecordSets = append(allRecordSets, out.ResourceRecordSets...)
if !out.IsTruncated {
break
}
input.StartRecordName = out.NextRecordName
input.StartRecordType = out.NextRecordType
input.StartRecordIdentifier = out.NextRecordIdentifier
}
for _, rs := range allRecordSets {
if !matchesType(string(rs.Type), recordTypes) {
continue
}
values := recordValues(rs)
for _, value := range values {
records = append(records, Record{
Domain: domain,
Name: strings.TrimSuffix(*rs.NamerecordValues function · go · L98-L108 (11 LOC)route53.go
func recordValues(rs types.ResourceRecordSet) []string {
if rs.AliasTarget != nil {
return []string{strings.TrimSuffix(*rs.AliasTarget.DNSName, ".")}
}
var values []string
for _, rr := range rs.ResourceRecords {
values = append(values, strings.TrimSuffix(*rr.Value, "."))
}
return values
}Repobility (the analyzer behind this table) · https://repobility.com
IsRoute53Registrar method · go · L110-L119 (10 LOC)route53.go
func (c *Route53Client) IsRoute53Registrar(domain string) bool {
zone, ok := c.zoneMap[domain]
if !ok {
return false
}
if zone.Config != nil && zone.Config.Comment != nil {
return strings.Contains(*zone.Config.Comment, "Route53 Registrar")
}
return false
}Version function · go · L5-L7 (3 LOC)version.go
func Version() string {
return version
}NewWhoisClient function · go · L18-L25 (8 LOC)whois.go
func NewWhoisClient() *WhoisClient {
client := whois.NewClient()
client.SetTimeout(10 * time.Second)
return &WhoisClient{
client: client,
cache: make(map[string]string),
}
}LookupRegistrar method · go · L29-L44 (16 LOC)whois.go
func (c *WhoisClient) LookupRegistrar(domain string) string {
c.mu.Lock()
if registrar, ok := c.cache[domain]; ok {
c.mu.Unlock()
return registrar
}
c.mu.Unlock()
registrar := c.fetchRegistrar(domain)
c.mu.Lock()
c.cache[domain] = registrar
c.mu.Unlock()
return registrar
}fetchRegistrar method · go · L46-L59 (14 LOC)whois.go
func (c *WhoisClient) fetchRegistrar(domain string) string {
result, err := c.client.Whois(domain)
if err != nil {
return "unknown"
}
for _, line := range strings.Split(result, "\n") {
if matches := registrarPattern.FindStringSubmatch(line); len(matches) > 1 {
return strings.ToLower(strings.TrimSpace(matches[1]))
}
}
return "unknown"
}