fix: prevent unnecessary template updates (#6379)

* test(installer): adds `TestIsOutdatedVersionFix`

Signed-off-by: Dwi Siswanto <git@dw1.io>

* fix: prevent unnecessary template updates

when version API fails.

* fix `catalog/config.IsOutdatedVersion` logic for
  empty version strings
* add GitHub API fallback when PDTM API is unavail
* only show outdated msg for actual version
  mismatches

Signed-off-by: Dwi Siswanto <git@dw1.io>

---------

Signed-off-by: Dwi Siswanto <git@dw1.io>
This commit is contained in:
Dwi Siswanto 2025-08-16 06:20:20 +07:00 committed by GitHub
parent d569cfe864
commit 70eeb6c210
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 74 additions and 8 deletions

View File

@ -46,18 +46,21 @@ const (
// if the current version is outdated // if the current version is outdated
func IsOutdatedVersion(current, latest string) bool { func IsOutdatedVersion(current, latest string) bool {
if latest == "" { if latest == "" {
// if pdtm api call failed it's assumed that the current version is outdated // NOTE(dwisiswant0): if PDTM API call failed or returned empty, we
// and it will be confirmed while updating from GitHub // cannot determine if templates are outdated w/o additional checks
// this fixes `version string empty` errors // return false to avoid unnecessary updates.
return true return false
} }
current = trimDevIfExists(current) current = trimDevIfExists(current)
currentVer, _ := semver.NewVersion(current) currentVer, _ := semver.NewVersion(current)
newVer, _ := semver.NewVersion(latest) newVer, _ := semver.NewVersion(latest)
if currentVer == nil || newVer == nil { if currentVer == nil || newVer == nil {
// fallback to naive comparison // fallback to naive comparison - return true only if they are different
return current == latest return current != latest
} }
return newVer.GreaterThan(currentVer) return newVer.GreaterThan(currentVer)
} }

View File

@ -94,7 +94,24 @@ func (t *TemplateManager) UpdateIfOutdated() error {
if !fileutil.FolderExists(config.DefaultConfig.TemplatesDirectory) { if !fileutil.FolderExists(config.DefaultConfig.TemplatesDirectory) {
return t.FreshInstallIfNotExists() 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 t.updateTemplatesAt(config.DefaultConfig.TemplatesDirectory)
} }
return nil 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) 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 // write templates to disk
if err := t.writeTemplatesToDisk(ghrd, dir); err != nil { if err := t.writeTemplatesToDisk(ghrd, dir); err != nil {

View File

@ -59,3 +59,42 @@ func TestTemplateInstallation(t *testing.T) {
require.FileExists(t, config.DefaultConfig.GetIgnoreFilePath()) require.FileExists(t, config.DefaultConfig.GetIgnoreFilePath())
t.Logf("Installed %d templates", counter) 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)
})
}
}