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>
This commit is contained in:
Tarun Koyalwar
2025-11-05 21:49:07 +05:30
committed by GitHub
parent 6c1e5384f5
commit 3cb054d976
6 changed files with 56 additions and 28 deletions
+1
View File
@@ -9,3 +9,4 @@ vendor/
.devcontainer
.vscode
dist
/subfinder
+1 -1
View File
@@ -13,7 +13,7 @@ builds:
- darwin
goarch:
- amd64
- 386
- '386'
- arm
- arm64
+5 -3
View File
@@ -28,6 +28,7 @@ type HostEntry struct {
Domain string
Host string
Source string
WildcardCertificate bool
}
// Result contains the result for a host resolution
@@ -37,6 +38,7 @@ type Result struct {
IP string
Error error
Source string
WildcardCertificate bool
}
// ResultType is the type of result found
@@ -92,13 +94,13 @@ func (r *ResolutionPool) InitWildcards(domain string) error {
func (r *ResolutionPool) resolveWorker() {
for task := range r.Tasks {
if !r.removeWildcard {
r.Results <- Result{Type: Subdomain, Host: task.Host, IP: "", Source: task.Source}
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}
r.Results <- Result{Type: Error, Host: task.Host, Source: task.Source, Error: err, WildcardCertificate: task.WildcardCertificate}
continue
}
@@ -116,7 +118,7 @@ func (r *ResolutionPool) resolveWorker() {
}
if !skip {
r.Results <- Result{Type: Subdomain, Host: task.Host, IP: hosts[0], Source: task.Source}
r.Results <- Result{Type: Subdomain, Host: task.Host, IP: hosts[0], Source: task.Source, WildcardCertificate: task.WildcardCertificate}
}
}
r.wg.Done()
+1 -1
View File
@@ -17,7 +17,7 @@ const banner = `
const ToolName = `subfinder`
// Version is the current version of subfinder
const version = `v2.9.0`
const version = `v2.9.1-dev`
// showBanner is used to show the banner to the user
func showBanner() {
+23 -2
View File
@@ -67,6 +67,9 @@ func (r *Runner) EnumerateSingleDomainWithCtx(ctx context.Context, domain string
gologger.Warning().Msgf("Encountered an error with source %s: %s\n", result.Source, result.Error)
case subscraping.Subdomain:
subdomain := replacer.Replace(result.Value)
// check if this subdomain is actually a wildcard subdomain
// that may have furthur subdomains associated with it
isWildcard := strings.Contains(result.Value, "*."+subdomain)
// Validate the subdomain found and remove wildcards from
if !strings.HasSuffix(subdomain, "."+domain) {
@@ -90,10 +93,17 @@ func (r *Runner) EnumerateSingleDomainWithCtx(ctx context.Context, domain string
// send the subdomain for resolution.
if _, ok := uniqueMap[subdomain]; ok {
skippedCounts[result.Source]++
// even if it is duplicate if it was not marked as wildcard before but this source says it is wildcard
// then we should mark it as wildcard
if !uniqueMap[subdomain].WildcardCertificate && isWildcard {
val := uniqueMap[subdomain]
val.WildcardCertificate = true
uniqueMap[subdomain] = val
}
continue
}
hostEntry := resolve.HostEntry{Domain: domain, Host: subdomain, Source: result.Source}
hostEntry := resolve.HostEntry{Domain: domain, Host: subdomain, Source: result.Source, WildcardCertificate: isWildcard}
if r.options.ResultCallback != nil && !r.options.RemoveWildcard {
r.options.ResultCallback(&hostEntry)
}
@@ -112,6 +122,7 @@ func (r *Runner) EnumerateSingleDomainWithCtx(ctx context.Context, domain string
if r.options.RemoveWildcard {
close(resolutionPool.Tasks)
}
wg.Done()
}()
@@ -129,11 +140,21 @@ func (r *Runner) EnumerateSingleDomainWithCtx(ctx context.Context, domain string
if _, ok := foundResults[result.Host]; !ok {
foundResults[result.Host] = result
if r.options.ResultCallback != nil {
r.options.ResultCallback(&resolve.HostEntry{Domain: domain, Host: result.Host, Source: result.Source})
r.options.ResultCallback(&resolve.HostEntry{Domain: domain, Host: result.Host, Source: result.Source, WildcardCertificate: result.WildcardCertificate})
}
}
}
}
// Merge wildcard certificate information from uniqueMap into foundResults
// This handles cases where a later source marked a subdomain as wildcard
// after it was already sent to the resolution pool
for host, result := range foundResults {
if entry, ok := uniqueMap[host]; ok && entry.WildcardCertificate && !result.WildcardCertificate {
result.WildcardCertificate = true
foundResults[host] = result
}
}
}
wg.Wait()
outputWriter := NewOutputWriter(r.options.JSON)
+6 -2
View File
@@ -22,6 +22,7 @@ type jsonSourceResult struct {
Host string `json:"host"`
Input string `json:"input"`
Source string `json:"source"`
WildcardCertificate bool `json:"wildcard_certificate,omitempty"`
}
type jsonSourceIPResult struct {
@@ -29,12 +30,14 @@ type jsonSourceIPResult struct {
IP string `json:"ip"`
Input string `json:"input"`
Source string `json:"source"`
WildcardCertificate bool `json:"wildcard_certificate,omitempty"`
}
type jsonSourcesResult struct {
Host string `json:"host"`
Input string `json:"input"`
Sources []string `json:"sources"`
WildcardCertificate bool `json:"wildcard_certificate,omitempty"`
}
// NewOutputWriter creates a new OutputWriter
@@ -117,7 +120,7 @@ func writeJSONHostIP(input string, results map[string]resolve.Result, writer io.
data.IP = result.IP
data.Input = input
data.Source = result.Source
data.WildcardCertificate = result.WildcardCertificate
err := encoder.Encode(&data)
if err != nil {
return err
@@ -130,7 +133,7 @@ func writeJSONHostIP(input string, results map[string]resolve.Result, writer io.
func (o *OutputWriter) WriteHostNoWildcard(input string, results map[string]resolve.Result, writer io.Writer) error {
hosts := make(map[string]resolve.HostEntry)
for host, result := range results {
hosts[host] = resolve.HostEntry{Domain: host, Host: result.Host, Source: result.Source}
hosts[host] = resolve.HostEntry{Domain: host, Host: result.Host, Source: result.Source, WildcardCertificate: result.WildcardCertificate}
}
return o.WriteHost(input, hosts, writer)
@@ -175,6 +178,7 @@ func writeJSONHost(input string, results map[string]resolve.HostEntry, writer io
data.Host = result.Host
data.Input = input
data.Source = result.Source
data.WildcardCertificate = result.WildcardCertificate
err := encoder.Encode(data)
if err != nil {
return err