Compare commits

..
1 Commits
Author SHA1 Message Date
dw1andMzack9999 3fdb599603 fix(js): stop tsgen from panicking on non-struct types (#8102)
* fix(js/tsgen): map non-struct external types to any

tsgen treated every type from another package as a struct and queued
it to have its fields scraped into a TypeScript interface. The kind
was checked only at scrape time, as a fatal error, so an interface,
func, or named basic type in an exported signature or field aborted
the whole run. make jsupdate-tsgen panicked on net.Conn, which the
dcerpc, gptransport, mssql, net, and oracle libraries return, and
never wrote the remaining definitions or index.ts.

Check the kind when the type is first seen instead. Structs are still
scraped; other types have no fields to describe and become any, as
external parameter types already do. Names that do not resolve are
still reported.

Fixes: #8099
Signed-off-by: Dwi Siswanto <[email protected]>

* fix(js): exclude gptransport and smbsession from codegen

gptransport and smbsession are shared Go adapters for the smb, dcerpc,
and file protocol code, not JavaScript modules: nothing registers them
with the runtime, and index.ts does not export them. Without the
.nuclei-jsgen-ignore marker that goexec already carries, tsgen and
bindgen treat them as modules, so a full make jsupdate-tsgen run
writes gptransport.ts and smbsession.ts and adds both to index.ts.

Refs: #8099
Signed-off-by: Dwi Siswanto <[email protected]>

* fix returns

---------

Signed-off-by: Dwi Siswanto <[email protected]>
Co-authored-by: Mzack9999 <[email protected]>
2026-11-10 07:01:54 +07:00
31 changed files with 127 additions and 641 deletions

No files matched your search

-2
View File
@@ -206,7 +206,6 @@ OUTPUT:
-o, -output string output file to write found issues/vulnerabilities
-sresp, -store-resp store all request/response passed through nuclei to output directory
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
-store-events string store what the matchers of every request see, for any protocol, to a jsonl file for -replay (holds raw traffic, credentials included)
-silent display findings only
-nc, -no-color disable output content coloring (ANSI escape codes)
-j, -jsonl write output in JSONL(ines) format
@@ -241,7 +240,6 @@ CONFIGURATIONS:
-sr, -system-resolvers use system DNS resolving as error fallback
-dc, -disable-clustering disable clustering of requests
-passive enable passive HTTP response processing mode
-replay string run the templates' matchers and extractors over a -store-events file and print the findings sorted, without sending requests
-fh2, -force-http2 force http2 connection on requests
-ev, -env-vars enable environment variables to be used in template
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
-2
View File
@@ -204,7 +204,6 @@ OUTPUT:
-o, -output string output file to write found issues/vulnerabilities
-sresp, -store-resp store all request/response passed through nuclei to output directory
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
-store-events string store what the matchers of every request see, for any protocol, to a jsonl file for -replay (holds raw traffic, credentials included)
-silent display findings only
-nc, -no-color disable output content coloring (ANSI escape codes)
-j, -jsonl write output in JSONL(ines) format
@@ -238,7 +237,6 @@ CONFIGURATIONS:
-sr, -system-resolvers use system DNS resolving as error fallback
-dc, -disable-clustering disable clustering of requests
-passive enable passive HTTP response processing mode
-replay string run the templates' matchers and extractors over a -store-events file and print the findings sorted, without sending requests
-fh2, -force-http2 force http2 connection on requests
-ev, -env-vars enable environment variables to be used in template
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
-2
View File
@@ -204,7 +204,6 @@ OUTPUT:
-o, -output string output file to write found issues/vulnerabilities
-sresp, -store-resp store all request/response passed through nuclei to output directory
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
-store-events string store what the matchers of every request see, for any protocol, to a jsonl file for -replay (holds raw traffic, credentials included)
-silent display findings only
-nc, -no-color disable output content coloring (ANSI escape codes)
-j, -jsonl write output in JSONL(ines) format
@@ -238,7 +237,6 @@ CONFIGURATIONS:
-sr, -system-resolvers use system DNS resolving as error fallback
-dc, -disable-clustering disable clustering of requests
-passive enable passive HTTP response processing mode
-replay string run the templates' matchers and extractors over a -store-events file and print the findings sorted, without sending requests
-fh2, -force-http2 force http2 connection on requests
-ev, -env-vars enable environment variables to be used in template
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
-2
View File
@@ -221,7 +221,6 @@ OUTPUT:
-o, -output string output file to write found issues/vulnerabilities
-sresp, -store-resp store all request/response passed through nuclei to output directory
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
-store-events string store what the matchers of every request see, for any protocol, to a jsonl file for -replay (holds raw traffic, credentials included)
-silent display findings only
-nc, -no-color disable output content coloring (ANSI escape codes)
-j, -jsonl write output in JSONL(ines) format
@@ -255,7 +254,6 @@ CONFIGURATIONS:
-sr, -system-resolvers use system DNS resolving as error fallback
-dc, -disable-clustering disable clustering of requests
-passive enable passive HTTP response processing mode
-replay string run the templates' matchers and extractors over a -store-events file and print the findings sorted, without sending requests
-fh2, -force-http2 force http2 connection on requests
-ev, -env-vars enable environment variables to be used in template
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
-2
View File
@@ -204,7 +204,6 @@ OUTPUT:
-o, -output string output file to write found issues/vulnerabilities
-sresp, -store-resp store all request/response passed through nuclei to output directory
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
-store-events string store what the matchers of every request see, for any protocol, to a jsonl file for -replay (holds raw traffic, credentials included)
-silent display findings only
-nc, -no-color disable output content coloring (ANSI escape codes)
-j, -jsonl write output in JSONL(ines) format
@@ -238,7 +237,6 @@ CONFIGURATIONS:
-sr, -system-resolvers use system DNS resolving as error fallback
-dc, -disable-clustering disable clustering of requests
-passive enable passive HTTP response processing mode
-replay string run the templates' matchers and extractors over a -store-events file and print the findings sorted, without sending requests
-fh2, -force-http2 force http2 connection on requests
-ev, -env-vars enable environment variables to be used in template
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
-2
View File
@@ -204,7 +204,6 @@ OUTPUT:
-o, -output string output file to write found issues/vulnerabilities
-sresp, -store-resp store all request/response passed through nuclei to output directory
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
-store-events string store what the matchers of every request see, for any protocol, to a jsonl file for -replay (holds raw traffic, credentials included)
-silent display findings only
-nc, -no-color disable output content coloring (ANSI escape codes)
-j, -jsonl write output in JSONL(ines) format
@@ -238,7 +237,6 @@ CONFIGURATIONS:
-sr, -system-resolvers use system DNS resolving as error fallback
-dc, -disable-clustering disable clustering of requests
-passive enable passive HTTP response processing mode
-replay string run the templates' matchers and extractors over a -store-events file and print the findings sorted, without sending requests
-fh2, -force-http2 force http2 connection on requests
-ev, -env-vars enable environment variables to be used in template
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
-2
View File
@@ -204,7 +204,6 @@ OUTPUT:
-o, -output string output file to write found issues/vulnerabilities
-sresp, -store-resp store all request/response passed through nuclei to output directory
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
-store-events string store what the matchers of every request see, for any protocol, to a jsonl file for -replay (holds raw traffic, credentials included)
-silent display findings only
-nc, -no-color disable output content coloring (ANSI escape codes)
-j, -jsonl write output in JSONL(ines) format
@@ -238,7 +237,6 @@ CONFIGURATIONS:
-sr, -system-resolvers use system DNS resolving as error fallback
-dc, -disable-clustering disable clustering of requests
-passive enable passive HTTP response processing mode
-replay string run the templates' matchers and extractors over a -store-events file and print the findings sorted, without sending requests
-fh2, -force-http2 force http2 connection on requests
-ev, -env-vars enable environment variables to be used in template
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
-2
View File
@@ -204,7 +204,6 @@ OUTPUT:
-o, -output string output file to write found issues/vulnerabilities
-sresp, -store-resp store all request/response passed through nuclei to output directory
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
-store-events string store what the matchers of every request see, for any protocol, to a jsonl file for -replay (holds raw traffic, credentials included)
-silent display findings only
-nc, -no-color disable output content coloring (ANSI escape codes)
-j, -jsonl write output in JSONL(ines) format
@@ -238,7 +237,6 @@ CONFIGURATIONS:
-sr, -system-resolvers use system DNS resolving as error fallback
-dc, -disable-clustering disable clustering of requests
-passive enable passive HTTP response processing mode
-replay string run the templates' matchers and extractors over a -store-events file and print the findings sorted, without sending requests
-fh2, -force-http2 force http2 connection on requests
-ev, -env-vars enable environment variables to be used in template
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
-2
View File
@@ -204,7 +204,6 @@ OUTPUT:
-o, -output string output file to write found issues/vulnerabilities
-sresp, -store-resp store all request/response passed through nuclei to output directory
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
-store-events string store what the matchers of every request see, for any protocol, to a jsonl file for -replay (holds raw traffic, credentials included)
-silent display findings only
-nc, -no-color disable output content coloring (ANSI escape codes)
-j, -jsonl write output in JSONL(ines) format
@@ -238,7 +237,6 @@ CONFIGURATIONS:
-sr, -system-resolvers use system DNS resolving as error fallback
-dc, -disable-clustering disable clustering of requests
-passive enable passive HTTP response processing mode
-replay string run the templates' matchers and extractors over a -store-events file and print the findings sorted, without sending requests
-fh2, -force-http2 force http2 connection on requests
-ev, -env-vars enable environment variables to be used in template
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
-2
View File
@@ -356,7 +356,6 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.StringVarP(&options.Output, "output", "o", "", "output file to write found issues/vulnerabilities"),
flagSet.BoolVarP(&options.StoreResponse, "store-resp", "sresp", false, "store all request/response passed through nuclei to output directory"),
flagSet.StringVarP(&options.StoreResponseDir, "store-resp-dir", "srd", runner.DefaultDumpTrafficOutputFolder, "store all request/response passed through nuclei to custom directory"),
flagSet.StringVar(&options.StoreEvents, "store-events", "", "store what the matchers of every request see, for any protocol, to a jsonl file for -replay (holds raw traffic, credentials included)"),
flagSet.BoolVar(&options.Silent, "silent", false, "display findings only"),
flagSet.BoolVarP(&options.NoColor, "no-color", "nc", false, "disable output content coloring (ANSI escape codes)"),
flagSet.BoolVarP(&options.JSONL, "jsonl", "j", false, "write output in JSONL(ines) format"),
@@ -392,7 +391,6 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.BoolVarP(&options.SystemResolvers, "system-resolvers", "sr", false, "use system DNS resolving as error fallback"),
flagSet.BoolVarP(&options.DisableClustering, "disable-clustering", "dc", false, "disable clustering of requests"),
flagSet.BoolVar(&options.OfflineHTTP, "passive", false, "enable passive HTTP response processing mode"),
flagSet.StringVar(&options.ReplayFile, "replay", "", "run the templates' matchers and extractors over a -store-events file and print the findings sorted, without sending requests"),
flagSet.BoolVarP(&options.ForceAttemptHTTP2, "force-http2", "fh2", false, "force http2 connection on requests"),
flagSet.BoolVarP(&options.EnvironmentVariables, "env-vars", "ev", false, "enable environment variables to be used in template"),
flagSet.StringVarP(&options.ClientCertFile, "client-cert", "cc", "", "client certificate file (PEM-encoded) used for authenticating against scanned hosts"),
+1 -2
View File
@@ -64,8 +64,7 @@ func ConfigureOptions() error {
// would hold the scan until it does.
func readsTargetsFromStdin(options *types.Options, stdinIsPipe bool) bool {
hasOtherTargets := len(options.Targets) > 0 || options.TargetsFilePath != "" || options.Uncover || len(options.UncoverQuery) > 0
// a replay runs over stored events and has no targets to wait for
return stdinIsPipe && !options.DisableStdin && !hasOtherTargets && options.ReplayFile == ""
return stdinIsPipe && !options.DisableStdin && !hasOtherTargets
}
// ParseOptions parses the command line flags provided by a user
-1
View File
@@ -84,7 +84,6 @@ func TestReadsTargetsFromStdin(t *testing.T) {
{"targets given with -l", &types.Options{TargetsFilePath: "targets.txt"}, true, false},
{"uncover query", &types.Options{UncoverQuery: goflags.StringSlice{"example"}}, true, false},
{"uncover enabled", &types.Options{Uncover: true}, true, false},
{"replay", &types.Options{ReplayFile: "events.jsonl"}, true, false},
} {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.want, readsTargetsFromStdin(tc.options, tc.stdinIsPipe))
-24
View File
@@ -1,24 +0,0 @@
package runner
import (
"slices"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
"github.com/projectdiscovery/nuclei/v3/pkg/replay"
)
// replay writes the findings of the loaded templates over a stored corpus
// through the usual output, so -o, -j and the other output options apply.
func (r *Runner) replay(store *loader.Store) error {
results, err := replay.Replay(r.options.ReplayFile, slices.Concat(store.Templates(), store.Workflows()))
if err != nil {
return err
}
for _, result := range results {
if err := r.output.Write(result); err != nil {
return err
}
}
r.Logger.Info().Msgf("Replayed %s: %d findings", r.options.ReplayFile, len(results))
return nil
}
-18
View File
@@ -57,7 +57,6 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/automaticscan"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/globalmatchers"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/eventcreator"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/honeypotdetector"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hostbackoff"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hosterrorscache"
@@ -70,7 +69,6 @@ import (
httpProtocol "github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/catchall"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httpclientpool"
"github.com/projectdiscovery/nuclei/v3/pkg/replay"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
@@ -839,22 +837,6 @@ func (r *Runner) RunEnumeration() error {
if r.options.PlanFile != "" {
return r.writePlan(store, executorEngine)
}
if r.options.ReplayFile != "" {
return r.replay(store)
}
if r.options.StoreEvents != "" {
recorder, err := replay.NewRecorder(r.options.StoreEvents, slices.Concat(store.Templates(), store.Workflows()))
if err != nil {
return errors.Wrap(err, "could not create events file")
}
previous := eventcreator.SetRecord(recorder.Record)
defer func() {
eventcreator.SetRecord(previous)
if err := recorder.Close(); err != nil {
r.Logger.Error().Msgf("Could not write %s: %s", r.options.StoreEvents, err)
}
}()
}
// TODO: remove below functions after v3 or update warning messages
templates.PrintDeprecatedProtocolNameMsgIfApplicable(r.options.Silent, r.options.Verbose)
@@ -46,35 +46,6 @@ func TestNetwork(t *testing.T) {
}
})
t.Run("ReplayStoredEvents", func(t *testing.T) {
server := testutils.NewTCPServer(nil, 0, func(conn net.Conn) {
defer func() { _ = conn.Close() }()
data, err := reader.ConnReadNWithTimeout(conn, 4, 5*time.Second)
if err == nil && string(data) == "PING" {
_, _ = conn.Write([]byte("PONG"))
}
})
events := filepath.Join(t.TempDir(), "events.jsonl")
results, err := testutils.RunNucleiTemplateAndGetResults("protocols/network/basic.yaml", server.URL, suite.debug, "-store-events", events)
server.Close()
if err != nil {
t.Fatalf("recording network events failed: %v", err)
}
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
// the server is gone, so the finding can only come from the stored event
results, err = testutils.RunNucleiBareArgsAndGetResults(suite.debug, nil, "-t", "protocols/network/basic.yaml", "-replay", events)
if err != nil {
t.Fatalf("replaying network events failed: %v", err)
}
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
})
t.Run("PinnedTargetLine", func(t *testing.T) {
server := testutils.NewTCPServer(nil, 0, func(conn net.Conn) {
defer func() { _ = conn.Close() }()
+19 -9
View File
@@ -284,8 +284,8 @@ func (p *EntityParser) extractClassProperties(node *ast.StructType) []Property {
Description: field.Doc.Text(),
}
if strings.Contains(property.Type, ".") {
// external struct found
property.Type = p.handleExternalStruct(property.Type)
// external type found
property.Type = p.handleExternalType(property.Type)
}
properties = append(properties, property)
}
@@ -349,7 +349,7 @@ func (p *EntityParser) extractReturnType(fn *ast.FuncDecl) (out string) {
for _, result := range fn.Type.Results.List {
tmp := exprToString(result.Type)
if strings.Contains(tmp, ".") && !strings.HasPrefix(tmp, "goja.") {
tmp = p.handleExternalStruct(tmp) + " | null" // external object interfaces can always return null
tmp = p.handleExternalType(tmp) + " | null" // external object interfaces can always return null
}
returns = append(returns, tmp)
}
@@ -374,8 +374,10 @@ func (p *EntityParser) extractReturnType(fn *ast.FuncDecl) (out string) {
}
}
if sliceutil.Contains(returns, "error") {
// add | null to the return type
out = out + " | null"
// external types already record that they can be null
if !strings.HasSuffix(out, "| null") {
out += " | null"
}
return
}
}
@@ -413,10 +415,18 @@ func (p *EntityParser) extractParameters(fn *ast.FuncDecl) []Parameter {
return parameters
}
// typeName is in format ssh.ClientConfig
// it first fetches all fields from the struct and creates a new object
// with that name and returns name of that object as type
func (p *EntityParser) handleExternalStruct(typeName string) string {
// handleExternalType returns the TypeScript type for typeName, a type from
// another package such as ssh.ClientConfig. A struct is queued for
// scrapeAndCreate, which describes its fields in an interface of the same name.
// Any other type, such as the net.Conn interface, has no fields to describe,
// so it becomes any. A typeName that does not resolve is queued too, so that
// scrapeAndCreate reports it.
func (p *EntityParser) handleExternalType(typeName string) string {
obj, err := p.lookupType(typeName)
if err == nil && !isStruct(obj.Type()) {
return "any"
}
baseType := typeName[strings.LastIndex(typeName, ".")+1:]
p.newObjects[typeName] = &Entity{
Name: baseType,
@@ -0,0 +1,33 @@
package tsgen_test
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"github.com/projectdiscovery/nuclei/v3/pkg/js/devtools/tsgen"
)
func TestParseExternalTypes(t *testing.T) {
t.Parallel()
ep, err := tsgen.NewEntityParser(filepath.Join("testdata", "external"))
require.NoError(t, err)
require.NoError(t, ep.Parse())
entities := make(map[string]tsgen.Entity)
for _, e := range ep.GetEntities() {
entities[e.Name] = e
}
// net.Conn is an interface, so it has no fields to describe.
require.Equal(t, "any | null", entities["Dial"].Function.Returns)
require.Equal(t, []tsgen.Property{{Name: "Conn", Type: "any", Description: ""}}, entities["Session"].Object.Properties)
require.NotContains(t, entities, "Conn")
// url.URL is a struct, so its fields become an interface.
require.Equal(t, "URL | null", entities["Parse"].Function.Returns)
require.Equal(t, "interface", entities["URL"].Type)
require.NotEmpty(t, entities["URL"].Object.Properties)
}
+40 -19
View File
@@ -1,38 +1,29 @@
package tsgen
import (
"errors"
"fmt"
"go/types"
"regexp"
"strings"
"github.com/projectdiscovery/utils/errkit"
)
// errUnresolvedType is returned by lookupType for a name it cannot resolve.
var errUnresolvedType = errors.New("unresolved type")
// scrape.go scrapes all information of exported type from different package
func (p *EntityParser) scrapeAndCreate(typeName string) error {
if p.newObjects[typeName] == nil {
return nil
}
// get package name
pkgName := strings.Split(typeName, ".")[0]
baseTypeName := strings.Split(typeName, ".")[1]
// get package
pkg, ok := p.imports[pkgName]
if !ok {
return errkit.Newf("package %v for type %v not found", pkgName, typeName)
}
// get type
obj := pkg.Types.Scope().Lookup(baseTypeName)
if obj == nil {
return errkit.Newf("type %v not found in package %+v", typeName, pkg)
}
// Ensure the object is a type name
typeNameObj, ok := obj.(*types.TypeName)
if !ok {
return errkit.Newf("%v is not a type name", typeName)
typeNameObj, err := p.lookupType(typeName)
if err != nil {
return err
}
baseTypeName := typeNameObj.Name()
// Ensure the type is a named struct type
namedStruct, ok := typeNameObj.Type().Underlying().(*types.Struct)
if !ok {
@@ -72,6 +63,36 @@ func (p *EntityParser) scrapeAndCreate(typeName string) error {
return nil
}
// lookupType resolves typeName, such as net.Conn, in the packages imported by
// the parsed files.
func (p *EntityParser) lookupType(typeName string) (*types.TypeName, error) {
pkgName, baseTypeName, _ := strings.Cut(typeName, ".")
pkg := p.imports[pkgName]
if pkg == nil {
return nil, fmt.Errorf("%w: package %v for type %v not found", errUnresolvedType, pkgName, typeName)
}
obj := pkg.Types.Scope().Lookup(baseTypeName)
if obj == nil {
return nil, fmt.Errorf("%w: type %v not found in package %+v", errUnresolvedType, typeName, pkg)
}
typeNameObj, ok := obj.(*types.TypeName)
if !ok {
return nil, fmt.Errorf("%w: %v is not a type name", errUnresolvedType, typeName)
}
return typeNameObj, nil
}
// isStruct reports whether t is a struct type.
func isStruct(t types.Type) bool {
_, ok := t.Underlying().(*types.Struct)
return ok
}
type ExtObject struct {
builtIn map[string]string
nested map[string]map[string]*ExtObject // Changed to map of field names to ExtObject
+32
View File
@@ -0,0 +1,32 @@
// Package external is a tsgen fixture that references types from other
// packages, both structs and non-structs.
package external
import (
"context"
"net"
"net/url"
)
// Dial returns an external interface type.
func Dial(network, address string) (net.Conn, error) {
return net.Dial(network, address)
}
// Parse returns an external struct type.
func Parse(raw string) (*url.URL, error) {
return url.Parse(raw)
}
// Session holds an external interface type in an exported field.
type Session struct {
Conn net.Conn
}
type dialer struct{}
// DialContext is exported, but its receiver is not.
func (dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, network, address)
}
@@ -0,0 +1 @@
shared adapter package; not a public JavaScript module
@@ -0,0 +1 @@
shared adapter package; not a public JavaScript module
@@ -1,8 +1,6 @@
package eventcreator
import (
"sync/atomic"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
@@ -12,29 +10,6 @@ import (
"golang.org/x/text/language"
)
// RecordFunc receives the event of a request before its operators run, which
// is what a replay needs to run them again.
type RecordFunc func(request protocols.Request, event output.InternalEvent)
// Every protocol builds its result here, so a recorder set here covers all
// of them. Scans read it concurrently, hence the atomic.
var record atomic.Pointer[RecordFunc]
// SetRecord installs fn, nil to stop recording, and returns the callback it
// replaced so the caller can put it back.
func SetRecord(fn RecordFunc) RecordFunc {
var previous *RecordFunc
if fn == nil {
previous = record.Swap(nil)
} else {
previous = record.Swap(&fn)
}
if previous == nil {
return nil
}
return *previous
}
// CreateEvent wraps the outputEvent with the result of the operators defined on the request
func CreateEvent(request protocols.Request, outputEvent output.InternalEvent, isResponseDebug bool) *output.InternalWrappedEvent {
return CreateEventWithAdditionalOptions(request, outputEvent, isResponseDebug, nil)
@@ -44,9 +19,6 @@ func CreateEvent(request protocols.Request, outputEvent output.InternalEvent, is
// and enables extending the resulting event with additional attributes or values.
func CreateEventWithAdditionalOptions(request protocols.Request, outputEvent output.InternalEvent, isResponseDebug bool,
addAdditionalOptions func(internalWrappedEvent *output.InternalWrappedEvent)) *output.InternalWrappedEvent {
if fn := record.Load(); fn != nil {
(*fn)(request, outputEvent)
}
event := &output.InternalWrappedEvent{InternalEvent: outputEvent}
// Dump response variables if ran in debug mode
@@ -1,26 +0,0 @@
package eventcreator
import (
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/stretchr/testify/require"
)
func TestSetRecordRestoresTheCallbackItReplaced(t *testing.T) {
var calls []string
first := RecordFunc(func(protocols.Request, output.InternalEvent) { calls = append(calls, "first") })
second := RecordFunc(func(protocols.Request, output.InternalEvent) { calls = append(calls, "second") })
require.Nil(t, SetRecord(first))
previous := SetRecord(second)
(*record.Load())(nil, nil)
// a second runner stopping puts the first one's recorder back, not nil
SetRecord(previous)
(*record.Load())(nil, nil)
require.Equal(t, []string{"second", "first"}, calls)
SetRecord(nil)
require.Nil(t, record.Load())
}
-93
View File
@@ -1,93 +0,0 @@
package replay
import (
"encoding/base64"
"unicode/utf8"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
)
// Matchers type-assert some values, such as an int status code or a []string
// of dns answers, and binary responses are not valid UTF-8, so plain JSON
// would change what they see. Those values are wrapped in a one-key object
// naming their Go type; everything else is stored as JSON.
const (
tagBytes = "$bytes"
tagBinary = "$binary"
tagInt = "$int"
tagStrings = "$strings"
)
func encodeEvent(event output.InternalEvent) map[string]any {
encoded := make(map[string]any, len(event))
for key, value := range event {
switch v := value.(type) {
case string:
if utf8.ValidString(v) {
encoded[key] = v
} else {
encoded[key] = map[string]string{tagBinary: base64.StdEncoding.EncodeToString([]byte(v))}
}
case []byte:
encoded[key] = map[string]string{tagBytes: base64.StdEncoding.EncodeToString(v)}
case int:
encoded[key] = map[string]int{tagInt: v}
case []string:
encoded[key] = map[string][]string{tagStrings: v}
default:
// values JSON cannot hold, such as funcs, are not something a
// matcher reads
if _, err := json.Marshal(v); err == nil {
encoded[key] = v
}
}
}
return encoded
}
func decodeEvent(raw map[string]json.Message) (output.InternalEvent, error) {
event := make(output.InternalEvent, len(raw))
for key, message := range raw {
value, err := decodeValue(message)
if err != nil {
return nil, err
}
event[key] = value
}
return event, nil
}
func decodeValue(message json.Message) (any, error) {
var tagged map[string]json.Message
if json.Unmarshal(message, &tagged) == nil && len(tagged) == 1 {
for tag, inner := range tagged {
switch tag {
case tagBytes, tagBinary:
var text string
if err := json.Unmarshal(inner, &text); err != nil {
return nil, err
}
data, err := base64.StdEncoding.DecodeString(text)
if err != nil {
return nil, err
}
if tag == tagBinary {
return string(data), nil
}
return data, nil
case tagInt:
var number int
err := json.Unmarshal(inner, &number)
return number, err
case tagStrings:
var list []string
err := json.Unmarshal(inner, &list)
return list, err
}
}
}
var value any
err := json.Unmarshal(message, &value)
return value, err
}
-39
View File
@@ -1,39 +0,0 @@
package replay
import (
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
"github.com/stretchr/testify/require"
)
func TestEventRoundTripKeepsWhatMatchersRead(t *testing.T) {
event := output.InternalEvent{
"body": "<title>admin</title>",
"raw": "\x00\xffBIN\xfe",
"data": []byte{0, 1, 2},
"status_code": 200,
"answer": []string{"a", "b"},
"duration": 1.5,
"matched": true,
"info": map[string]any{"name": "x"},
"callback": func() {},
}
data, err := json.Marshal(encodeEvent(event))
require.NoError(t, err)
var raw map[string]json.Message
require.NoError(t, json.Unmarshal(data, &raw))
decoded, err := decodeEvent(raw)
require.NoError(t, err)
require.Equal(t, "<title>admin</title>", decoded["body"])
require.Equal(t, "\x00\xffBIN\xfe", decoded["raw"], "binary responses keep every byte")
require.Equal(t, []byte{0, 1, 2}, decoded["data"])
require.Equal(t, 200, decoded["status_code"], "the status matcher reads an int")
require.Equal(t, []string{"a", "b"}, decoded["answer"])
require.Equal(t, 1.5, decoded["duration"])
require.Equal(t, true, decoded["matched"])
require.Equal(t, map[string]any{"name": "x"}, decoded["info"])
require.NotContains(t, decoded, "callback", "values JSON cannot hold are dropped")
}
-121
View File
@@ -1,121 +0,0 @@
// Package replay stores the event every request hands its matchers and
// extractors, for any protocol, and runs templates over a stored corpus again
// without sending anything. Comparing the findings of two template sets or
// two builds on the same corpus shows what a change moved.
package replay
import (
"os"
"strconv"
"sync"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
)
// record is one line of a corpus.
type record[E any] struct {
TemplateID string `json:"template-id,omitempty"`
// Request is the position of the request in its template.
Request int `json:"request"`
// ClusterKey identifies the request of a single-request template; every
// template sending the same request replays the event, as the scan
// clusters them onto one.
ClusterKey string `json:"cluster-key,omitempty"`
Event E `json:"event"`
}
type requestRef struct {
templateID string
index int
single bool
}
// Recorder appends the events of a scan to a corpus file.
type Recorder struct {
mu sync.Mutex
file *os.File
requests map[protocols.Request]requestRef
err error
}
// clusterKeyed is a request the scan can cluster. The key leaves out what
// makes a request unclusterable, such as a raw request or a body, so only a
// clusterable request may share events by it.
type clusterKeyed interface {
TmplClusterKey() uint64
IsClusterable() bool
}
func clusterKey(request protocols.Request) (string, bool) {
keyed, ok := request.(clusterKeyed)
if !ok || !keyed.IsClusterable() {
return "", false
}
return strconv.FormatUint(keyed.TmplClusterKey(), 10), true
}
// NewRecorder creates the corpus at path for a scan of templatesList. The
// corpus holds raw traffic, credentials included, so only its owner can read
// it.
func NewRecorder(path string, templatesList []*templates.Template) (*Recorder, error) {
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
if err != nil {
return nil, err
}
recorder := &Recorder{file: file, requests: make(map[protocols.Request]requestRef)}
for _, source := range sources(templatesList) {
single := len(source.requests) == 1 && source.clustered
for i, request := range source.requests {
recorder.requests[request] = requestRef{templateID: source.id, index: i, single: single}
}
}
return recorder, nil
}
// Record stores event, which request produced. It runs before the operators,
// which add their own values to the event.
func (r *Recorder) Record(request protocols.Request, event output.InternalEvent) {
ref, known := r.requests[request]
if !known {
// a cluster runs one request for all its members and was compiled
// apart from them; its members replay it by the cluster key
key, ok := clusterKey(request)
if !ok {
return
}
templateID, _ := event["template-id"].(string)
r.write(record[map[string]any]{TemplateID: templateID, ClusterKey: key, Event: encodeEvent(event)})
return
}
line := record[map[string]any]{TemplateID: ref.templateID, Request: ref.index, Event: encodeEvent(event)}
if key, ok := clusterKey(request); ok && ref.single {
line.ClusterKey = key
}
r.write(line)
}
func (r *Recorder) write(line record[map[string]any]) {
data, err := json.Marshal(line)
if err != nil {
return
}
r.mu.Lock()
defer r.mu.Unlock()
// written straight to the file: ctrl+c exits without running defers
if _, err := r.file.Write(append(data, '\n')); err != nil && r.err == nil {
r.err = err
}
}
// Close closes the corpus and reports the first write error.
func (r *Recorder) Close() error {
r.mu.Lock()
defer r.mu.Unlock()
if err := r.file.Close(); err != nil && r.err == nil {
r.err = err
}
return r.err
}
-106
View File
@@ -1,106 +0,0 @@
package replay
import (
"bufio"
"bytes"
"cmp"
"fmt"
"io"
"maps"
"os"
"slices"
"strconv"
"strings"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/eventcreator"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
)
type target struct {
source source
request protocols.Request
}
// Replay runs the matchers and extractors of templatesList over every event
// in the corpus at path and returns the findings sorted, so two replays diff
// line by line. An event replays on the template and request that produced
// it, and a single-request event also on every template sending the same
// request.
func Replay(path string, templatesList []*templates.Template) ([]*output.ResultEvent, error) {
byRequest := make(map[string][]target)
byCluster := make(map[string][]target)
for _, source := range sources(templatesList) {
for i, request := range source.requests {
byRequest[source.id+"\x00"+strconv.Itoa(i)] = []target{{source: source, request: request}}
}
if len(source.requests) == 1 && source.clustered {
if key, ok := clusterKey(source.requests[0]); ok {
byCluster[key] = append(byCluster[key], target{source: source, request: source.requests[0]})
}
}
}
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer func() { _ = file.Close() }()
var results []*output.ResultEvent
reader := bufio.NewReader(file)
for number := 1; ; number++ {
line, readErr := reader.ReadBytes('\n')
if line = bytes.TrimSpace(line); len(line) > 0 {
var stored record[map[string]json.Message]
if err := json.Unmarshal(line, &stored); err != nil {
return nil, fmt.Errorf("%s:%d: %w", path, number, err)
}
event, err := decodeEvent(stored.Event)
if err != nil {
return nil, fmt.Errorf("%s:%d: %w", path, number, err)
}
targets := byRequest[stored.TemplateID+"\x00"+strconv.Itoa(stored.Request)]
if stored.ClusterKey != "" {
targets = byCluster[stored.ClusterKey]
}
for _, target := range targets {
results = append(results, replayEvent(target, event)...)
}
}
if readErr == io.EOF {
break
}
if readErr != nil {
return nil, readErr
}
}
// extractors keep values in a map, so their order changes between runs
for _, result := range results {
slices.Sort(result.ExtractedResults)
}
slices.SortStableFunc(results, compareResults)
return results, nil
}
func replayEvent(target target, event output.InternalEvent) []*output.ResultEvent {
// operators add their values to the event, and a cluster event carries
// the first member's identity
event = maps.Clone(event)
event["template-id"] = target.source.id
event["template-path"] = target.source.path
event["template-info"] = target.source.info
return eventcreator.CreateEvent(target.request, event, false).Results
}
func compareResults(a, b *output.ResultEvent) int {
return cmp.Or(
strings.Compare(a.TemplateID, b.TemplateID),
strings.Compare(a.Host, b.Host),
strings.Compare(a.Matched, b.Matched),
strings.Compare(a.MatcherName, b.MatcherName),
strings.Compare(a.ExtractorName, b.ExtractorName),
strings.Compare(strings.Join(a.ExtractedResults, "\x00"), strings.Join(b.ExtractedResults, "\x00")),
)
}
-78
View File
@@ -1,78 +0,0 @@
package replay
import (
"github.com/projectdiscovery/nuclei/v3/pkg/model"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/workflows"
)
// source is a template whose requests produce events: a loaded template, or
// a step of a loaded workflow, which may not be loaded on its own.
type source struct {
id string
path string
info model.Info
requests []protocols.Request
// clustered mirrors templates.Cluster: only a template the scan can
// cluster shares events with others by its request
clustered bool
}
type requestLister interface {
ProtocolRequests() []protocols.Request
}
type templateExecuter interface {
requestLister
TemplateOptions() *protocols.ExecutorOptions
}
func sources(templatesList []*templates.Template) []source {
var list []source
seen := make(map[string]bool)
add := func(s source) {
if !seen[s.id] && len(s.requests) > 0 {
seen[s.id] = true
list = append(list, s)
}
}
addTemplate := func(template *templates.Template) {
if lister, ok := template.Executer.(requestLister); ok {
clustered := template.Flow == "" && (template.Options == nil || !template.Options.IsMultiProtocol) && !template.Info.HasPlaceholders()
add(source{id: template.ID, path: template.Path, info: template.Info, requests: lister.ProtocolRequests(), clustered: clustered})
}
}
var walk func(steps []*workflows.WorkflowTemplate)
walk = func(steps []*workflows.WorkflowTemplate) {
for _, step := range steps {
for _, pair := range step.Executers {
switch executer := pair.Executer.(type) {
case templateExecuter:
options := executer.TemplateOptions()
add(source{id: options.TemplateID, path: options.TemplatePath, info: options.TemplateInfo, requests: executer.ProtocolRequests()})
case *templates.ClusterExecuter:
// a cluster holds only the first member's request; each
// member is parsed again for its own, which the event is
// matched to by its cluster key
for _, path := range executer.TemplatePaths() {
if member, err := templates.Parse(path, nil, pair.Options); err == nil && member != nil {
addTemplate(member)
}
}
}
}
walk(step.Subtemplates)
for _, matcher := range step.Matchers {
walk(matcher.Subtemplates)
}
}
}
for _, template := range templatesList {
addTemplate(template)
if template.CompiledWorkflow != nil {
walk(template.CompiledWorkflow.Workflows)
}
}
return list
}
-9
View File
@@ -301,15 +301,6 @@ func (e *ClusterExecuter) TemplateIDs(selection protocols.TemplateSelection) []s
return ids
}
// TemplatePaths returns the paths of every member.
func (e *ClusterExecuter) TemplatePaths() []string {
paths := make([]string, 0, len(e.operators))
for _, operator := range e.operators {
paths = append(paths, operator.templatePath)
}
return paths
}
// operatorsFor returns the members selected for input by per-target profiles.
func (e *ClusterExecuter) operatorsFor(input *contextargs.MetaInput) []*clusteredOperator {
if e.options.TargetScope == nil {
-11
View File
@@ -88,17 +88,6 @@ func (e *TemplateExecuter) Compile() error {
return e.engine.Compile()
}
// TemplateOptions returns the options of the template the requests belong
// to, which name it.
func (e *TemplateExecuter) TemplateOptions() *protocols.ExecutorOptions {
return e.options
}
// ProtocolRequests returns the compiled requests in execution order.
func (e *TemplateExecuter) ProtocolRequests() []protocols.Request {
return e.requests
}
// Requests returns the total number of requests the rule will perform
func (e *TemplateExecuter) Requests() int {
var count int
-8
View File
@@ -254,12 +254,6 @@ type Options struct {
// PlanFile is where the work a scan would do is written as JSON lines;
// nuclei exits after writing it, without sending requests.
PlanFile string
// StoreEvents is where the scan writes the event of every request, as
// JSON lines, for -replay.
StoreEvents string
// ReplayFile is a corpus written by -store-events whose events the
// templates are run over, without sending requests.
ReplayFile string
// NoStrictSyntax disables strict syntax check on nuclei templates (allows custom key-value pairs).
NoStrictSyntax bool
// Verbose flag indicates whether to show verbose output or not
@@ -721,8 +715,6 @@ func (options *Options) Copy() *Options {
Silent: options.Silent,
Validate: options.Validate,
PlanFile: options.PlanFile,
StoreEvents: options.StoreEvents,
ReplayFile: options.ReplayFile,
NoStrictSyntax: options.NoStrictSyntax,
Verbose: options.Verbose,
VerboseVerbose: options.VerboseVerbose,