package crtsh import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "sort" "strings" "github.com/anotherhadi/iknowyou/internal/tools" ) const ( name = "crt.sh" description = "SSL/TLS certificate transparency log search via crt.sh — enumerates subdomains and certificates issued for a domain." link = "https://crt.sh" icon = "" ) type Runner struct{} func New() tools.ToolRunner { return &Runner{} } func (r *Runner) Name() string { return name } func (r *Runner) Description() string { return description } func (r *Runner) Link() string { return link } func (r *Runner) Icon() string { return icon } func (r *Runner) InputTypes() []tools.InputType { return []tools.InputType{tools.InputTypeDomain} } type crtEntry struct { IssuerName string `json:"issuer_name"` CommonName string `json:"common_name"` NameValue string `json:"name_value"` NotBefore string `json:"not_before"` NotAfter string `json:"not_after"` EntryTimestamp string `json:"entry_timestamp"` } func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out chan<- tools.Event) error { defer close(out) params := url.Values{} params.Set("q", "%."+target) params.Set("output", "json") apiURL := "https://crt.sh/?" + params.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil) if err != nil { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; crtsh-scanner/1.0)") resp, err := http.DefaultClient.Do(req) if err != nil { if ctx.Err() != nil { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"} } else { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()} } out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "failed to read response"} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } if resp.StatusCode != http.StatusOK { msg := fmt.Sprintf("API error %d", resp.StatusCode) if resp.StatusCode == http.StatusBadGateway || resp.StatusCode == http.StatusServiceUnavailable || resp.StatusCode == http.StatusGatewayTimeout { msg = fmt.Sprintf("crt.sh is temporarily unavailable (%d), try again later", resp.StatusCode) } out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: msg} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } var entries []crtEntry if err := json.Unmarshal(body, &entries); err != nil { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "failed to parse response"} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } if len(entries) == 0 { out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } // Deduplicate subdomains from name_value fields seen := make(map[string]struct{}) for _, e := range entries { for _, line := range strings.Split(e.NameValue, "\n") { line = strings.TrimSpace(line) if line != "" && !strings.HasPrefix(line, "*") { seen[line] = struct{}{} } } } subdomains := make([]string, 0, len(seen)) for s := range seen { subdomains = append(subdomains, s) } sort.Strings(subdomains) var sb strings.Builder sb.WriteString(fmt.Sprintf("Found %d unique subdomains across %d certificate entries\n\n", len(subdomains), len(entries))) for _, s := range subdomains { sb.WriteString(s + "\n") } out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: strings.TrimSpace(sb.String())} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: len(subdomains)} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil }