Repository: projectdiscovery/nuclei

Issue:
[MAINTENANCE] Migrate to golangci-lint v2
Follow up on #6290 _(specifically d56e5cc8e1c0a834a736c9a4cc9a0c91cf983724)_

Hints from issue comments:

Solution patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index dce907af..f8297c10 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -24,7 +24,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
       - uses: projectdiscovery/actions/setup/go@v1
-      - uses: projectdiscovery/actions/golangci-lint@v1
+      - uses: projectdiscovery/actions/golangci-lint/v2@v1
 
   tests:
     name: "Tests"
diff --git a/Makefile b/Makefile
index 0c3ab083..1b089e76 100644
--- a/Makefile
+++ b/Makefile
@@ -146,5 +146,14 @@ dsl-docs:
 template-validate: build
 template-validate:
 	./bin/nuclei -ut
-	./bin/nuclei -validate -et http/technologies
-	./bin/nuclei -validate -w workflows -et http/technologies
\ No newline at end of file
+	./bin/nuclei -validate \
+		-et .github/ \
+		-et helpers/payloads/ \
+		-et http/technologies \
+		-ept code
+	./bin/nuclei -validate \
+		-w workflows \
+		-et .github/ \
+		-et helpers/payloads/ \
+		-et http/technologies \
+		-ept code
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 40050393..71ab9060 100644
--- a/go.mod
+++ b/go.mod
@@ -191,7 +191,7 @@ require (
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
 	github.com/dlclark/regexp2 v1.11.5 // indirect
 	github.com/docker/cli v27.4.1+incompatible // indirect
-	github.com/docker/docker v27.1.1+incompatible // indirect
+	github.com/docker/docker v28.0.0+incompatible // indirect
 	github.com/docker/go-connections v0.5.0 // indirect
 	github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
 	github.com/emirpasic/gods v1.18.1 // indirect
@@ -213,7 +213,7 @@ require (
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
-	github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
+	github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
 	github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
diff --git a/go.sum b/go.sum
index 83ee118a..a8e3dcb9 100644
--- a/go.sum
+++ b/go.sum
@@ -297,8 +297,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
 github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI=
 github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
-github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
-github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v28.0.0+incompatible h1:Olh0KS820sJ7nPsBKChVhk5pzqcwDR15fumfAd/p9hM=
+github.com/docker/docker v28.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
 github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
 github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -397,8 +397,8 @@ github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI6
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
 github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
-github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
-github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
+github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
 github.com/goburrow/cache v0.1.4 h1:As4KzO3hgmzPlnaMniZU9+VmoNYseUhuELbxy9mRBfw=
 github.com/goburrow/cache v0.1.4/go.mod h1:cDFesZDnIlrHoNlMYqqMpCRawuXulgx+y7mXU8HZ+/c=
 github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
diff --git a/pkg/catalog/config/constants.go b/pkg/catalog/config/constants.go
index 15d8e9da..a9de026d 100644
--- a/pkg/catalog/config/constants.go
+++ b/pkg/catalog/config/constants.go
@@ -31,7 +31,7 @@ const (
 	CLIConfigFileName               = "config.yaml"
 	ReportingConfigFilename         = "reporting-config.yaml"
 	// Version is the current version of nuclei
-	Version = `v3.4.7`
+	Version = `v3.4.8`
 	// Directory Names of custom templates
 	CustomS3TemplatesDirName     = "s3"
 	CustomGitHubTemplatesDirName = "github"
@@ -46,18 +46,21 @@ const (
 // if the current version is outdated
 func IsOutdatedVersion(current, latest string) bool {
 	if latest == "" {
-		// if pdtm api call failed it's assumed that the current version is outdated
-		// and it will be confirmed while updating from GitHub
-		// this fixes `version string empty` errors
-		return true
+		// NOTE(dwisiswant0): if PDTM API call failed or returned empty, we
+		// cannot determine if templates are outdated w/o additional checks
+		// return false to avoid unnecessary updates.
+		return false
 	}
+
 	current = trimDevIfExists(current)
 	currentVer, _ := semver.NewVersion(current)
 	newVer, _ := semver.NewVersion(latest)
+
 	if currentVer == nil || newVer == nil {
-		// fallback to naive comparison
-		return current == latest
+		// fallback to naive comparison - return true only if they are different
+		return current != latest
 	}
+
 	return newVer.GreaterThan(currentVer)
 }
 
diff --git a/pkg/installer/template.go b/pkg/installer/template.go
index 9e56f12a..d9f3d5ae 100644
--- a/pkg/installer/template.go
+++ b/pkg/installer/template.go
@@ -94,7 +94,24 @@ func (t *TemplateManager) UpdateIfOutdated() error {
 	if !fileutil.FolderExists(config.DefaultConfig.TemplatesDirectory) {
 		return t.FreshInstallIfNotExists()
 	}
-	if config.DefaultConfig.NeedsTemplateUpdate() {
+
+	needsUpdate := config.DefaultConfig.NeedsTemplateUpdate()
+
+	// NOTE(dwisiswant0): if PDTM API data is not available
+	// (LatestNucleiTemplatesVersion is empty) but we have a current template
+	// version, so we MUST verify against GitHub directly.
+	if !needsUpdate && config.DefaultConfig.LatestNucleiTemplatesVersion == "" && config.DefaultConfig.TemplateVersion != "" {
+		ghrd, err := updateutils.NewghReleaseDownloader(config.OfficialNucleiTemplatesRepoName)
+		if err == nil {
+			latestVersion := ghrd.Latest.GetTagName()
+			if config.IsOutdatedVersion(config.DefaultConfig.TemplateVersion, latestVersion) {
+				needsUpdate = true
+				gologger.Debug().Msgf("PDTM API unavailable, verified update needed via GitHub API: %s -> %s", config.DefaultConfig.TemplateVersion, latestVersion)
+			}
+		}
+	}
+
+	if needsUpdate {
 		return t.updateTemplatesAt(config.DefaultConfig.TemplatesDirectory)
 	}
 	return nil
@@ -142,7 +159,14 @@ func (t *TemplateManager) updateTemplatesAt(dir string) error {
 		return errorutil.NewWithErr(err).Msgf("failed to install templates at %s", dir)
 	}
 
-	gologger.Info().Msgf("Your current nuclei-templates %s are outdated. Latest is %s\n", config.DefaultConfig.TemplateVersion, ghrd.Latest.GetTagName())
+	latestVersion := ghrd.Latest.GetTagName()
+	currentVersion := config.DefaultConfig.TemplateVersion
+
+	if config.IsOutdatedVersion(currentVersion, latestVersion) {
+		gologger.Info().Msgf("Your current nuclei-templates %s are outdated. Latest is %s\n", currentVersion, latestVersion)
+	} else {
+		gologger.Debug().Msgf("Updating nuclei-templates from %s to %s (forced update)\n", currentVersion, latestVersion)
+	}
 
 	// write templates to disk
 	if err := t.writeTemplatesToDisk(ghrd, dir); err != nil {
diff --git a/pkg/protocols/headless/engine/engine.go b/pkg/protocols/headless/engine/engine.go
index 63dbb41e..c045580d 100644
--- a/pkg/protocols/headless/engine/engine.go
+++ b/pkg/protocols/headless/engine/engine.go
@@ -20,12 +20,13 @@ import (
 
 // Browser is a browser structure for nuclei headless module
 type Browser struct {
-	customAgent  string
-	tempDir      string
-	previousPIDs map[int32]struct{} // track already running PIDs
-	engine       *rod.Browser
-	options      *types.Options
-	launcher     *launcher.Launcher
+	customAgent    string
+	defaultHeaders map[string]string
+	tempDir        string
+	previousPIDs   map[int32]struct{} // track already running PIDs
+	engine         *rod.Browser
+	options        *types.Options
+	launcher       *launcher.Launcher
 
 	// use getHTTPClient to get the http client
 	httpClient     *http.Client
@@ -95,6 +96,7 @@ func New(options *types.Options) (*Browser, error) {
 	if browserErr := browser.Connect(); browserErr != nil {
 		return nil, browserErr
 	}
+	defaultHeaders := make(map[string]string)
 	customAgent := ""
 	for _, option := range options.CustomHeaders {
 		parts := strings.SplitN(option, ":", 2)
@@ -103,12 +105,20 @@ func New(options *types.Options) (*Browser, error) {
 		}
 		if strings.EqualFold(parts[0], "User-Agent") {
 			customAgent = parts[1]
+		} else {
+			k := strings.TrimSpace(parts[0])
+			v := strings.TrimSpace(parts[1])
+			if k == "" || v == "" {
+				continue
+			}
+			defaultHeaders[k] = v
 		}
 	}
 
 	engine := &Browser{
 		tempDir:        dataStore,
 		customAgent:    customAgent,
+		defaultHeaders: defaultHeaders,
 		engine:         browser,
 		options:        options,
 		httpClientOnce: &sync.Once{},
@@ -135,6 +145,30 @@ func (b *Browser) UserAgent() string {
 	return b.customAgent
 }
 
+// applyDefaultHeaders setsheaders passed via cli -H flag
+func (b *Browser) applyDefaultHeaders(p *rod.Page) error {
+	pairs := make([]string, 0, len(b.defaultHeaders)*2+2)
+
+	hasAcceptLanguage := false
+	for k := range b.defaultHeaders {
+		if strings.EqualFold(k, "Accept-Language") {
+			hasAcceptLanguage = true
+			break
+		}
+	}
+	if !hasAcceptLanguage {
+		pairs = append(pairs, "Accept-Language", "en, en-GB, en-us;")
+	}
+	for k, v := range b.defaultHeaders {
+		pairs = append(pairs, k, v)
+	}
+	if len(pairs) == 0 {
+		return nil
+	}
+	_, err := p.SetExtraHeaders(pairs)
+	return err
+}
+
 func (b *Browser) getHTTPClient() (*http.Client, error) {
 	var err error
 	b.httpClientOnce.Do(func() {
@@ -147,6 +181,6 @@ func (b *Browser) getHTTPClient() (*http.Client, error) {
 func (b *Browser) Close() {
 	_ = b.engine.Close()
 	b.launcher.Kill()
-	os.RemoveAll(b.tempDir)
+	_ = os.RemoveAll(b.tempDir)
 	processutil.CloseProcesses(processutil.IsChromeProcess, b.previousPIDs)
 }
diff --git a/pkg/protocols/headless/engine/page.go b/pkg/protocols/headless/engine/page.go
index 6986f80b..244d740c 100644
--- a/pkg/protocols/headless/engine/page.go
+++ b/pkg/protocols/headless/engine/page.go
@@ -61,6 +61,10 @@ func (i *Instance) Run(ctx *contextargs.Context, actions []*Action, payloads map
 	}
 	page = page.Timeout(options.Timeout)
 
+	if err = i.browser.applyDefaultHeaders(page); err != nil {
+		return nil, nil, err
+	}
+
 	if i.browser.customAgent != "" {
 		if userAgentErr := page.SetUserAgent(&proto.NetworkSetUserAgentOverride{UserAgent: i.browser.customAgent}); userAgentErr != nil {
 			return nil, nil, userAgentErr
@@ -130,10 +134,6 @@ func (i *Instance) Run(ctx *contextargs.Context, actions []*Action, payloads map
 		return nil, nil, err
 	}
 
-	if _, err := page.SetExtraHeaders([]string{"Accept-Language", "en, en-GB, en-us;"}); err != nil {
-		return nil, nil, err
-	}
-
 	// inject cookies
 	// each http request is performed via the native go http client
 	// we first inject the shared cookies
diff --git a/pkg/reporting/trackers/linear/linear.go b/pkg/reporting/trackers/linear/linear.go
index be872336..75619c14 100644
--- a/pkg/reporting/trackers/linear/linear.go
+++ b/pkg/reporting/trackers/linear/linear.go
@@ -54,7 +54,7 @@ type Options struct {
 
 // New creates a new issue tracker integration client based on options.
 func New(options *Options) (*Integration, error) {
-	var transport http.RoundTripper = http.DefaultTransport
+	var transport = http.DefaultTransport
 	if options.HttpClient != nil && options.HttpClient.HTTPClient.Transport != nil {
 		transport = options.HttpClient.HTTPClient.Transport
 	}



Test patch:
diff --git a/pkg/installer/template_test.go b/pkg/installer/template_test.go
index 952c4459..435797b1 100644
--- a/pkg/installer/template_test.go
+++ b/pkg/installer/template_test.go
@@ -59,3 +59,42 @@ func TestTemplateInstallation(t *testing.T) {
 	require.FileExists(t, config.DefaultConfig.GetIgnoreFilePath())
 	t.Logf("Installed %d templates", counter)
 }
+
+func TestIsOutdatedVersion(t *testing.T) {
+	testCases := []struct {
+		current  string
+		latest   string
+		expected bool
+		desc     string
+	}{
+		// Test the empty latest version case (main bug fix)
+		{"v10.2.7", "", false, "Empty latest version should not trigger update"},
+
+		// Test same versions
+		{"v10.2.7", "v10.2.7", false, "Same versions should not trigger update"},
+
+		// Test outdated version
+		{"v10.2.6", "v10.2.7", true, "Older version should trigger update"},
+
+		// Test newer current version (edge case)
+		{"v10.2.8", "v10.2.7", false, "Newer current version should not trigger update"},
+
+		// Test dev versions
+		{"v10.2.7-dev", "v10.2.7", false, "Dev version matching release should not trigger update"},
+		{"v10.2.6-dev", "v10.2.7", true, "Outdated dev version should trigger update"},
+
+		// Test invalid semver fallback
+		{"invalid-version", "v10.2.7", true, "Invalid current version should trigger update (fallback)"},
+		{"v10.2.7", "invalid-version", true, "Invalid latest version should trigger update (fallback)"},
+		{"same-invalid", "same-invalid", false, "Same invalid versions should not trigger update (fallback)"},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			result := config.IsOutdatedVersion(tc.current, tc.latest)
+			require.Equal(t, tc.expected, result,
+				"IsOutdatedVersion(%q, %q) = %t, expected %t",
+				tc.current, tc.latest, result, tc.expected)
+		})
+	}
+}