Files
Tarun Koyalwar 3cb054d976 Add wildcard certificate detection in JSON output (#1665)
* Add wildcard certificate detection in JSON output

This commit implements wildcard certificate detection for subdomains that
are covered by wildcard certificates (e.g., *.example.com). When sources
return results containing wildcard patterns, the subdomain is now marked
with a wildcard_certificate field in JSON output mode.

Key changes:
- Added WildcardCertificate field to HostEntry and Result structs
- Detection logic: checks if result.Value contains "*.subdomain" pattern
- Propagates wildcard flag through resolution pipeline
- JSON output includes wildcard_certificate field (omitted if false)
- Updates version to v2.9.1-dev
- Fix .goreleaser.yml 386 architecture quoting
- Add /subfinder to .gitignore

Note: wildcard_certificate field is not included when using -cs flag to
avoid breaking changes to the library API.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix wildcard certificate flag propagation with -nW flag

Fixed a bug where the wildcard_certificate field was being lost when using
the -nW flag (DNS resolution with wildcard filtering). The issue occurred
because when multiple sources find the same subdomain, only the first
occurrence is sent to the resolution pool. If a later source marks the
subdomain as having a wildcard certificate, that information was stored
in uniqueMap but never propagated to foundResults.

Solution: After resolution completes, merge wildcard certificate information
from uniqueMap into foundResults. This ensures that if any source marked a
subdomain as having a wildcard certificate, that flag is preserved in the
final output.

This ensures consistent behavior - wildcard_certificate field appears in
JSON output regardless of whether -nW flag is used.

Validation:
- Without -nW: api.nuclei.sh shows wildcard_certificate:true ✓
- With -nW: api.nuclei.sh shows wildcard_certificate:true ✓

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-05 21:49:07 +05:30

126 lines
2.8 KiB
Go

package resolve
import (
"fmt"
"sync"
"github.com/rs/xid"
)
const (
maxWildcardChecks = 3
)
// ResolutionPool is a pool of resolvers created for resolving subdomains
// for a given host.
type ResolutionPool struct {
*Resolver
Tasks chan HostEntry
Results chan Result
wg *sync.WaitGroup
removeWildcard bool
wildcardIPs map[string]struct{}
}
// HostEntry defines a host with the source
type HostEntry struct {
Domain string
Host string
Source string
WildcardCertificate bool
}
// Result contains the result for a host resolution
type Result struct {
Type ResultType
Host string
IP string
Error error
Source string
WildcardCertificate bool
}
// ResultType is the type of result found
type ResultType int
// Types of data result can return
const (
Subdomain ResultType = iota
Error
)
// NewResolutionPool creates a pool of resolvers for resolving subdomains of a given domain
func (r *Resolver) NewResolutionPool(workers int, removeWildcard bool) *ResolutionPool {
resolutionPool := &ResolutionPool{
Resolver: r,
Tasks: make(chan HostEntry),
Results: make(chan Result),
wg: &sync.WaitGroup{},
removeWildcard: removeWildcard,
wildcardIPs: make(map[string]struct{}),
}
go func() {
for range workers {
resolutionPool.wg.Add(1)
go resolutionPool.resolveWorker()
}
resolutionPool.wg.Wait()
close(resolutionPool.Results)
}()
return resolutionPool
}
// InitWildcards inits the wildcard ips array
func (r *ResolutionPool) InitWildcards(domain string) error {
for range maxWildcardChecks {
uid := xid.New().String()
hosts, _ := r.DNSClient.Lookup(uid + "." + domain)
if len(hosts) == 0 {
return fmt.Errorf("%s is not a wildcard domain", domain)
}
// Append all wildcard ips found for domains
for _, host := range hosts {
r.wildcardIPs[host] = struct{}{}
}
}
return nil
}
func (r *ResolutionPool) resolveWorker() {
for task := range r.Tasks {
if !r.removeWildcard {
r.Results <- Result{Type: Subdomain, Host: task.Host, IP: "", Source: task.Source, WildcardCertificate: task.WildcardCertificate}
continue
}
hosts, err := r.DNSClient.Lookup(task.Host)
if err != nil {
r.Results <- Result{Type: Error, Host: task.Host, Source: task.Source, Error: err, WildcardCertificate: task.WildcardCertificate}
continue
}
if len(hosts) == 0 {
continue
}
var skip bool
for _, host := range hosts {
// Ignore the host if it exists in wildcard ips map
if _, ok := r.wildcardIPs[host]; ok {
skip = true
break
}
}
if !skip {
r.Results <- Result{Type: Subdomain, Host: task.Host, IP: hosts[0], Source: task.Source, WildcardCertificate: task.WildcardCertificate}
}
}
r.wg.Done()
}