Files
nuclei/internal/runner/runner_test.go
2026-11-08 15:12:03 +01:00

295 lines
8.9 KiB
Go

package runner
import (
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strconv"
"sync/atomic"
"testing"
"time"
"github.com/miekg/dns"
"github.com/projectdiscovery/gologger"
"github.com/stretchr/testify/require"
"github.com/projectdiscovery/nuclei/v3/pkg/input"
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
)
func TestCreateReportingOptions(t *testing.T) {
var options types.Options
options.ReportingConfig = filepath.Join("testdata", "test-issue-tracker-config1.yaml")
resultOptions, err := createReportingOptions(&options)
require.Nil(t, err)
require.Equal(t, resultOptions.AllowList.Severities, severity.Severities{severity.High, severity.Critical})
require.Equal(t, resultOptions.DenyList.Severities, severity.Severities{severity.Low})
options.ReportingConfig = filepath.Join("testdata", "test-issue-tracker-config2.yaml")
resultOptions2, err := createReportingOptions(&options)
require.Nil(t, err)
require.Equal(t, resultOptions2.AllowList.Severities, resultOptions.AllowList.Severities)
require.Equal(t, resultOptions2.DenyList.Severities, resultOptions.DenyList.Severities)
}
func TestCreateReportingOptionsCSVExport(t *testing.T) {
t.Run("flag only", func(t *testing.T) {
resultOptions, err := createReportingOptions(&types.Options{CSVExport: "out.csv", OmitRawRequests: true})
require.NoError(t, err)
require.NotNil(t, resultOptions.CSVExporter)
require.Equal(t, "out.csv", resultOptions.CSVExporter.File)
require.True(t, resultOptions.CSVExporter.OmitRaw)
})
t.Run("flag is not set", func(t *testing.T) {
resultOptions, err := createReportingOptions(&types.Options{})
require.NoError(t, err)
require.Nil(t, resultOptions.CSVExporter)
})
t.Run("flag takes precedence over config file", func(t *testing.T) {
config := filepath.Join(t.TempDir(), "report.yaml")
require.NoError(t, os.WriteFile(config, []byte("csv:\n file: from-config.csv\n"), 0o600))
resultOptions, err := createReportingOptions(&types.Options{ReportingConfig: config})
require.NoError(t, err)
require.Equal(t, "from-config.csv", resultOptions.CSVExporter.File)
resultOptions, err = createReportingOptions(&types.Options{ReportingConfig: config, CSVExport: "from-flag.csv"})
require.NoError(t, err)
require.Equal(t, "from-flag.csv", resultOptions.CSVExporter.File)
})
}
type TestStruct1 struct {
A string `yaml:"a"`
Struct *TestStruct2 `yaml:"b"`
}
type TestStruct2 struct {
B string `yaml:"b"`
}
type TestStruct3 struct {
A string `yaml:"a"`
B string `yaml:"b"`
C string `yaml:"c"`
}
type TestStruct4 struct {
A string `yaml:"a"`
Struct *TestStruct3 `yaml:"b"`
}
type TestStruct5 struct {
A []string `yaml:"a"`
B [2]string `yaml:"b"`
}
type TestStruct6 struct {
A string `yaml:"a"`
B *TestStruct2 `yaml:"b"`
C string
}
func TestWalkReflectStructAssignsEnvVars(t *testing.T) {
testStruct := &TestStruct1{
A: "$VAR_EXAMPLE",
Struct: &TestStruct2{
B: "$VAR_TWO",
},
}
_ = os.Setenv("VAR_EXAMPLE", "value")
_ = os.Setenv("VAR_TWO", "value2")
Walk(testStruct, expandEndVars)
require.Equal(t, "value", testStruct.A)
require.Equal(t, "value2", testStruct.Struct.B)
}
func TestWalkReflectStructHandlesDifferentTypes(t *testing.T) {
testStruct := &TestStruct3{
A: "$VAR_EXAMPLE",
B: "$VAR_TWO",
C: "$VAR_THREE",
}
_ = os.Setenv("VAR_EXAMPLE", "value")
_ = os.Setenv("VAR_TWO", "2")
_ = os.Setenv("VAR_THREE", "true")
Walk(testStruct, expandEndVars)
require.Equal(t, "value", testStruct.A)
require.Equal(t, "2", testStruct.B)
require.Equal(t, "true", testStruct.C)
}
func TestWalkReflectStructEmpty(t *testing.T) {
testStruct := &TestStruct3{
A: "$VAR_EXAMPLE",
B: "",
C: "$VAR_THREE",
}
_ = os.Setenv("VAR_EXAMPLE", "value")
_ = os.Setenv("VAR_TWO", "2")
_ = os.Setenv("VAR_THREE", "true")
Walk(testStruct, expandEndVars)
require.Equal(t, "value", testStruct.A)
require.Equal(t, "", testStruct.B)
require.Equal(t, "true", testStruct.C)
}
func TestWalkReflectStructWithNoYamlTag(t *testing.T) {
test := &TestStruct6{
A: "$GITHUB_USER",
B: &TestStruct2{
B: "$GITHUB_USER",
},
C: "$GITHUB_USER",
}
_ = os.Setenv("GITHUB_USER", "testuser")
Walk(test, expandEndVars)
require.Equal(t, "testuser", test.A)
require.Equal(t, "testuser", test.B.B, test.B)
require.Equal(t, "$GITHUB_USER", test.C)
}
func TestWalkReflectStructHandlesNestedStructs(t *testing.T) {
testStruct := &TestStruct4{
A: "$VAR_EXAMPLE",
Struct: &TestStruct3{
B: "$VAR_TWO",
C: "$VAR_THREE",
},
}
_ = os.Setenv("VAR_EXAMPLE", "value")
_ = os.Setenv("VAR_TWO", "2")
_ = os.Setenv("VAR_THREE", "true")
Walk(testStruct, expandEndVars)
require.Equal(t, "value", testStruct.A)
require.Equal(t, "2", testStruct.Struct.B)
require.Equal(t, "true", testStruct.Struct.C)
}
func TestNewInteractshHTTPClient(t *testing.T) {
// ProxyFromEnvironment reads the environment once per process. Set it
// before the first request. NO_PROXY keeps the local dial direct.
for _, key := range []string{"HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy", "https_proxy", "no_proxy"} {
t.Setenv(key, "")
}
t.Setenv("HTTPS_PROXY", "http://127.0.0.1:9")
t.Setenv("NO_PROXY", "127.0.0.1,localhost")
options := &types.Options{
ExecutionId: "test-interactsh-client-dialer",
Timeout: 35,
}
require.NoError(t, protocolstate.Init(options))
t.Cleanup(func() { protocolstate.Close(options.ExecutionId) })
client := newInteractshHTTPClient(options)
require.NotNil(t, client)
require.Equal(t, 35*time.Second, client.HTTPClient.Timeout)
transport, ok := client.HTTPClient.Transport.(*http.Transport)
require.True(t, ok)
require.NotNil(t, transport.Proxy)
require.NotNil(t, transport.DialContext)
require.NotNil(t, transport.DialTLSContext)
require.NotNil(t, transport.TLSClientConfig, "INTERACTSH_TLS_VERIFY edits this config")
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
t.Cleanup(srv.Close)
resp, err := client.HTTPClient.Get(srv.URL)
require.NoError(t, err)
require.Equal(t, http.StatusNoContent, resp.StatusCode)
require.NoError(t, resp.Body.Close())
host := srv.Listener.Addr().(*net.TCPAddr).IP.String()
dialers := protocolstate.GetDialersWithId(options.ExecutionId)
require.Equal(t, host, dialers.Fastdialer.GetDialedIP(host))
req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
require.NoError(t, err)
proxyURL, err := transport.Proxy(req)
require.NoError(t, err)
require.NotNil(t, proxyURL)
require.Equal(t, "http://127.0.0.1:9", proxyURL.String())
}
func TestInitializeTemplatesHTTPInputUsesResolvers(t *testing.T) {
for _, key := range []string{"HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy"} {
t.Setenv(key, "")
}
listener, err := net.ListenPacket("udp", "127.0.0.1:0")
require.NoError(t, err)
queries := new(atomic.Int32)
server := &dns.Server{PacketConn: listener, Handler: dns.HandlerFunc(func(writer dns.ResponseWriter, request *dns.Msg) {
queries.Add(1)
response := new(dns.Msg).SetReply(request)
for _, question := range request.Question {
if question.Qtype == dns.TypeA {
response.Answer = append(response.Answer, &dns.A{Hdr: dns.RR_Header{Name: question.Name, Rrtype: dns.TypeA, Class: dns.ClassINET}, A: net.ParseIP("127.0.0.1")})
}
}
_ = writer.WriteMsg(response)
})}
started := make(chan struct{})
server.NotifyStartedFunc = func() { close(started) }
done := make(chan error, 1)
go func() { done <- server.ActivateAndServe() }()
<-started
t.Cleanup(func() { require.NoError(t, server.Shutdown()); require.NoError(t, <-done) })
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
t.Cleanup(srv.Close)
resolverFile := filepath.Join(t.TempDir(), "resolv.conf")
require.NoError(t, os.WriteFile(resolverFile, nil, 0o600))
t.Setenv("RESOLVERS_PATH", resolverFile)
options := &types.Options{
ExecutionId: t.Name(),
Timeout: 5,
BulkSize: 1,
InternalResolversList: []string{listener.LocalAddr().String()},
}
require.NoError(t, protocolstate.Init(options))
t.Cleanup(func() { protocolstate.Close(options.ExecutionId) })
target := "probe.nuclei.test:" + strconv.Itoa(srv.Listener.Addr().(*net.TCPAddr).Port)
r := &Runner{
options: options,
inputProvider: provider.NewSimpleInputProviderWithUrls(options.ExecutionId, target),
Logger: &gologger.Logger{},
}
hm, err := r.initializeTemplatesHTTPInput()
require.NoError(t, err)
t.Cleanup(func() { _ = hm.Close() })
probed, ok := hm.Get(input.ProbeKey(target, ""))
require.True(t, ok, "the probe should resolve the target through the -r resolver")
require.Equal(t, "http://"+target, string(probed))
require.Positive(t, queries.Load())
}