package installer
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
fileutil "github.com/projectdiscovery/utils/file"
mapsutil "github.com/projectdiscovery/utils/maps"
"github.com/stretchr/testify/require"
)
func TestTemplateInstallation(t *testing.T) {
HideProgressBar = true
tm := &TemplateManager{}
dir, err := os.MkdirTemp("", "nuclei-templates-*")
require.Nil(t, err)
cfgdir, err := os.MkdirTemp("", "nuclei-config-*")
require.Nil(t, err)
defer func() {
_ = os.RemoveAll(dir)
_ = os.RemoveAll(cfgdir)
}()
config.DefaultConfig.SetConfigDir(cfgdir)
templatesTempDir := filepath.Join(dir, "templates")
config.DefaultConfig.SetTemplatesDir(templatesTempDir)
err = tm.FreshInstallIfNotExists()
if err != nil {
if strings.Contains(err.Error(), "rate limit") {
t.Skip("Skipping test due to github rate limit")
}
require.Nil(t, err)
}
counter := 0
err = filepath.Walk(templatesTempDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
counter++
}
return nil
})
require.Nil(t, err)
require.Greater(t, counter, 1000)
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
}{
{"v10.2.7", "", false, "Empty latest version should not trigger update"},
{"v10.2.7", "v10.2.7", false, "Same versions should not trigger update"},
{"v10.2.6", "v10.2.7", true, "Older version should trigger update"},
{"v10.2.8", "v10.2.7", false, "Newer current version should not trigger update"},
{"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"},
{"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)
})
}
}
func TestCleanupOrphanedTemplates(t *testing.T) {
HideProgressBar = true
tm := &TemplateManager{}
t.Run("removes orphaned templates", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "nuclei-cleanup-test-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(tmpDir)
}()
cfgdir, err := os.MkdirTemp("", "nuclei-config-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(cfgdir)
}()
config.DefaultConfig.SetConfigDir(cfgdir)
config.DefaultConfig.SetTemplatesDir(tmpDir)
templatesDir1 := filepath.Join(tmpDir, "cves", "2023")
templatesDir2 := filepath.Join(tmpDir, "exposures", "configs")
require.NoError(t, os.MkdirAll(templatesDir1, 0755))
require.NoError(t, os.MkdirAll(templatesDir2, 0755))
template1 := filepath.Join(templatesDir1, "CVE-2023-1234.yaml")
template2 := filepath.Join(templatesDir1, "CVE-2023-5678.yaml")
template3 := filepath.Join(templatesDir2, "git-config-exposure.yaml")
orphanedTemplate1 := filepath.Join(templatesDir1, "old-template.yaml")
orphanedTemplate2 := filepath.Join(templatesDir2, "removed-template.yaml")
templateContent := `id: test-template
info:
name: Test Template
author: test
severity: info`
require.NoError(t, os.WriteFile(template1, []byte(templateContent), 0644))
require.NoError(t, os.WriteFile(template2, []byte(templateContent), 0644))
require.NoError(t, os.WriteFile(template3, []byte(templateContent), 0644))
require.NoError(t, os.WriteFile(orphanedTemplate1, []byte(templateContent), 0644))
require.NoError(t, os.WriteFile(orphanedTemplate2, []byte(templateContent), 0644))
writtenPaths := mapsutil.NewSyncLockMap[string, struct{}]()
absTemplate1, _ := filepath.Abs(template1)
absTemplate2, _ := filepath.Abs(template2)
absTemplate3, _ := filepath.Abs(template3)
absTemplate1 = filepath.Clean(absTemplate1)
absTemplate2 = filepath.Clean(absTemplate2)
absTemplate3 = filepath.Clean(absTemplate3)
_ = writtenPaths.Set(absTemplate1, struct{}{})
_ = writtenPaths.Set(absTemplate2, struct{}{})
_ = writtenPaths.Set(absTemplate3, struct{}{})
err = tm.cleanupOrphanedTemplates(tmpDir, writtenPaths)
require.NoError(t, err)
require.NoFileExists(t, orphanedTemplate1, "orphaned template should be removed")
require.NoFileExists(t, orphanedTemplate2, "orphaned template should be removed")
require.FileExists(t, template1, "template from new release should exist")
require.FileExists(t, template2, "template from new release should exist")
require.FileExists(t, template3, "template from new release should exist")
})
t.Run("preserves custom templates", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "nuclei-cleanup-custom-test-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(tmpDir)
}()
cfgdir, err := os.MkdirTemp("", "nuclei-config-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(cfgdir)
}()
config.DefaultConfig.SetConfigDir(cfgdir)
config.DefaultConfig.SetTemplatesDir(tmpDir)
customGitHubDir := filepath.Join(tmpDir, "github", "owner", "repo")
require.NoError(t, os.MkdirAll(customGitHubDir, 0755))
customTemplate := filepath.Join(customGitHubDir, "custom-template.yaml")
templateContent := `id: custom-template
info:
name: Custom Template
author: test
severity: info`
require.NoError(t, os.WriteFile(customTemplate, []byte(templateContent), 0644))
writtenPaths := mapsutil.NewSyncLockMap[string, struct{}]()
err = tm.cleanupOrphanedTemplates(tmpDir, writtenPaths)
require.NoError(t, err)
require.FileExists(t, customTemplate, "custom template should be preserved")
})
t.Run("skips non-template files", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "nuclei-cleanup-nontemplate-test-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(tmpDir)
}()
cfgdir, err := os.MkdirTemp("", "nuclei-config-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(cfgdir)
}()
config.DefaultConfig.SetConfigDir(cfgdir)
config.DefaultConfig.SetTemplatesDir(tmpDir)
readmeFile := filepath.Join(tmpDir, "README.md")
configFile := filepath.Join(tmpDir, "cves.json")
checksumFile := filepath.Join(tmpDir, ".checksum")
require.NoError(t, os.WriteFile(readmeFile, []byte("# Templates"), 0644))
require.NoError(t, os.WriteFile(configFile, []byte("{}"), 0644))
require.NoError(t, os.WriteFile(checksumFile, []byte(""), 0644))
writtenPaths := mapsutil.NewSyncLockMap[string, struct{}]()
err = tm.cleanupOrphanedTemplates(tmpDir, writtenPaths)
require.NoError(t, err)
require.FileExists(t, readmeFile, "README.md should be preserved")
require.FileExists(t, configFile, "config file should be preserved")
require.FileExists(t, checksumFile, "checksum file should be preserved")
})
t.Run("handles empty written paths", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "nuclei-cleanup-empty-test-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(tmpDir)
}()
cfgdir, err := os.MkdirTemp("", "nuclei-config-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(cfgdir)
}()
config.DefaultConfig.SetConfigDir(cfgdir)
config.DefaultConfig.SetTemplatesDir(tmpDir)
template1 := filepath.Join(tmpDir, "template1.yaml")
templateContent := `id: test-template
info:
name: Test Template
author: test
severity: info`
require.NoError(t, os.WriteFile(template1, []byte(templateContent), 0644))
writtenPaths := mapsutil.NewSyncLockMap[string, struct{}]()
err = tm.cleanupOrphanedTemplates(tmpDir, writtenPaths)
require.NoError(t, err)
require.NoFileExists(t, template1, "template should be removed when not in written paths")
})
t.Run("handles relative and absolute paths correctly", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "nuclei-cleanup-path-test-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(tmpDir)
}()
cfgdir, err := os.MkdirTemp("", "nuclei-config-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(cfgdir)
}()
config.DefaultConfig.SetConfigDir(cfgdir)
config.DefaultConfig.SetTemplatesDir(tmpDir)
template1 := filepath.Join(tmpDir, "template1.yaml")
templateContent := `id: test-template
info:
name: Test Template
author: test
severity: info`
require.NoError(t, os.WriteFile(template1, []byte(templateContent), 0644))
writtenPaths := mapsutil.NewSyncLockMap[string, struct{}]()
_ = writtenPaths.Set(template1, struct{}{})
err = tm.cleanupOrphanedTemplates(tmpDir, writtenPaths)
require.NoError(t, err)
require.FileExists(t, template1, "template should be preserved when in written paths")
})
t.Run("handles empty templates directory", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "nuclei-cleanup-empty-dir-test-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(tmpDir)
}()
cfgdir, err := os.MkdirTemp("", "nuclei-config-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(cfgdir)
}()
config.DefaultConfig.SetConfigDir(cfgdir)
config.DefaultConfig.SetTemplatesDir(tmpDir)
require.True(t, fileutil.FolderExists(tmpDir), "templates directory should exist")
writtenPaths := mapsutil.NewSyncLockMap[string, struct{}]()
err = tm.cleanupOrphanedTemplates(tmpDir, writtenPaths)
require.NoError(t, err, "cleanup should handle empty directory without error")
require.True(t, fileutil.FolderExists(tmpDir), "templates directory should still exist")
})
t.Run("handles non-existent directory gracefully", func(t *testing.T) {
nonExistentDir := "/tmp/nuclei-test-non-existent-dir-12345"
_ = os.RemoveAll(nonExistentDir)
require.False(t, fileutil.FolderExists(nonExistentDir), "directory should not exist")
writtenPaths := mapsutil.NewSyncLockMap[string, struct{}]()
err := tm.cleanupOrphanedTemplates(nonExistentDir, writtenPaths)
require.NoError(t, err, "cleanup should handle non-existent directory without error")
})
}
func TestRegenerateTemplateMetadata(t *testing.T) {
HideProgressBar = true
tm := &TemplateManager{}
t.Run("creates index and checksum files", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "nuclei-metadata-test-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(tmpDir)
}()
cfgdir, err := os.MkdirTemp("", "nuclei-config-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(cfgdir)
}()
config.DefaultConfig.SetConfigDir(cfgdir)
config.DefaultConfig.SetTemplatesDir(tmpDir)
template1 := filepath.Join(tmpDir, "template1.yaml")
template2 := filepath.Join(tmpDir, "cves", "template2.yaml")
require.NoError(t, os.MkdirAll(filepath.Dir(template2), 0755))
template1Content := `id: template-one
info:
name: Template One
author: test
severity: info`
template2Content := `id: template-two
info:
name: Template Two
author: test
severity: high`
require.NoError(t, os.WriteFile(template1, []byte(template1Content), 0644))
require.NoError(t, os.WriteFile(template2, []byte(template2Content), 0644))
err = tm.regenerateTemplateMetadata(tmpDir)
require.NoError(t, err)
indexPath := config.DefaultConfig.GetTemplateIndexFilePath()
require.FileExists(t, indexPath, "template index file should be created")
checksumPath := config.DefaultConfig.GetChecksumFilePath()
require.FileExists(t, checksumPath, "checksum file should be created")
index, err := config.GetNucleiTemplatesIndex()
require.NoError(t, err)
require.Contains(t, index, "template-one", "index should contain template-one")
require.Contains(t, index, "template-two", "index should contain template-two")
checksums, err := tm.getChecksumFromDir(tmpDir)
require.NoError(t, err)
require.Contains(t, checksums, template1, "checksum should contain template1")
require.Contains(t, checksums, template2, "checksum should contain template2")
})
t.Run("excludes deleted templates from index after cleanup", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "nuclei-metadata-cleanup-test-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(tmpDir)
}()
cfgdir, err := os.MkdirTemp("", "nuclei-config-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(cfgdir)
}()
config.DefaultConfig.SetConfigDir(cfgdir)
config.DefaultConfig.SetTemplatesDir(tmpDir)
template1 := filepath.Join(tmpDir, "kept-template.yaml")
template2 := filepath.Join(tmpDir, "deleted-template.yaml")
orphanedTemplate := filepath.Join(tmpDir, "orphaned-template.yaml")
template1Content := `id: test-template-1
info:
name: Test Template 1
author: test
severity: info`
template2Content := `id: test-template-2
info:
name: Test Template 2
author: test
severity: info`
orphanedContent := `id: test-template-orphaned
info:
name: Test Template Orphaned
author: test
severity: info`
require.NoError(t, os.WriteFile(template1, []byte(template1Content), 0644))
require.NoError(t, os.WriteFile(template2, []byte(template2Content), 0644))
require.NoError(t, os.WriteFile(orphanedTemplate, []byte(orphanedContent), 0644))
initialIndex := map[string]string{
"test-template-1": template1,
"test-template-2": template2,
"test-template-orphaned": orphanedTemplate,
}
err = config.DefaultConfig.WriteTemplatesIndex(initialIndex)
require.NoError(t, err)
index, err := config.GetNucleiTemplatesIndex()
require.NoError(t, err)
require.Contains(t, index, "test-template-orphaned", "initial index should contain orphaned template")
writtenPaths := mapsutil.NewSyncLockMap[string, struct{}]()
absTemplate1, _ := filepath.Abs(template1)
absTemplate1 = filepath.Clean(absTemplate1)
_ = writtenPaths.Set(absTemplate1, struct{}{})
err = tm.cleanupOrphanedTemplates(tmpDir, writtenPaths)
require.NoError(t, err)
require.NoFileExists(t, orphanedTemplate, "orphaned template should be deleted")
require.NoFileExists(t, template2, "template2 should be deleted since it's not in writtenPaths")
err = tm.regenerateTemplateMetadata(tmpDir)
require.NoError(t, err)
index, err = config.GetNucleiTemplatesIndex()
require.NoError(t, err)
require.NotContains(t, index, "test-template-orphaned", "index should not contain deleted orphaned template")
require.Contains(t, index, "test-template-1", "index should still contain kept template")
require.NotContains(t, index, "test-template-2", "index should not contain template that was deleted but not cleaned")
})
t.Run("excludes deleted templates from checksum after cleanup", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "nuclei-checksum-cleanup-test-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(tmpDir)
}()
cfgdir, err := os.MkdirTemp("", "nuclei-config-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(cfgdir)
}()
config.DefaultConfig.SetConfigDir(cfgdir)
config.DefaultConfig.SetTemplatesDir(tmpDir)
keptTemplate := filepath.Join(tmpDir, "kept.yaml")
orphanedTemplate := filepath.Join(tmpDir, "orphaned.yaml")
templateContent := `id: test-template
info:
name: Test Template
author: test
severity: info`
require.NoError(t, os.WriteFile(keptTemplate, []byte(templateContent), 0644))
require.NoError(t, os.WriteFile(orphanedTemplate, []byte(templateContent), 0644))
err = tm.writeChecksumFileInDir(tmpDir)
require.NoError(t, err)
initialChecksums, err := tm.getChecksumFromDir(tmpDir)
require.NoError(t, err)
require.Contains(t, initialChecksums, orphanedTemplate, "initial checksum should contain orphaned template")
writtenPaths := mapsutil.NewSyncLockMap[string, struct{}]()
absKept, _ := filepath.Abs(keptTemplate)
absKept = filepath.Clean(absKept)
_ = writtenPaths.Set(absKept, struct{}{})
err = tm.cleanupOrphanedTemplates(tmpDir, writtenPaths)
require.NoError(t, err)
require.NoFileExists(t, orphanedTemplate, "orphaned template should be deleted")
err = tm.regenerateTemplateMetadata(tmpDir)
require.NoError(t, err)
checksums, err := tm.getChecksumFromDir(tmpDir)
require.NoError(t, err)
require.NotContains(t, checksums, orphanedTemplate, "checksum should not contain deleted orphaned template")
require.Contains(t, checksums, keptTemplate, "checksum should still contain kept template")
})
t.Run("cleanup and metadata regeneration integration", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "nuclei-integration-test-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(tmpDir)
}()
cfgdir, err := os.MkdirTemp("", "nuclei-config-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(cfgdir)
}()
config.DefaultConfig.SetConfigDir(cfgdir)
config.DefaultConfig.SetTemplatesDir(tmpDir)
template1 := filepath.Join(tmpDir, "cves", "2023", "cve1.yaml")
template2 := filepath.Join(tmpDir, "cves", "2023", "cve2.yaml")
orphaned1 := filepath.Join(tmpDir, "cves", "2022", "old-cve.yaml")
orphaned2 := filepath.Join(tmpDir, "exposures", "old-exposure.yaml")
require.NoError(t, os.MkdirAll(filepath.Dir(template1), 0755))
require.NoError(t, os.MkdirAll(filepath.Dir(orphaned1), 0755))
require.NoError(t, os.MkdirAll(filepath.Dir(orphaned2), 0755))
template1Content := `id: cve1
info:
name: CVE1
author: test
severity: info`
template2Content := `id: cve2
info:
name: CVE2
author: test
severity: info`
orphaned1Content := `id: old-cve
info:
name: Old CVE
author: test
severity: info`
orphaned2Content := `id: old-exposure
info:
name: Old Exposure
author: test
severity: info`
require.NoError(t, os.WriteFile(template1, []byte(template1Content), 0644))
require.NoError(t, os.WriteFile(template2, []byte(template2Content), 0644))
require.NoError(t, os.WriteFile(orphaned1, []byte(orphaned1Content), 0644))
require.NoError(t, os.WriteFile(orphaned2, []byte(orphaned2Content), 0644))
writtenPaths := mapsutil.NewSyncLockMap[string, struct{}]()
absTemplate1, _ := filepath.Abs(template1)
absTemplate2, _ := filepath.Abs(template2)
absTemplate1 = filepath.Clean(absTemplate1)
absTemplate2 = filepath.Clean(absTemplate2)
_ = writtenPaths.Set(absTemplate1, struct{}{})
_ = writtenPaths.Set(absTemplate2, struct{}{})
err = tm.cleanupOrphanedTemplates(tmpDir, writtenPaths)
require.NoError(t, err)
require.NoFileExists(t, orphaned1, "orphaned template 1 should be deleted")
require.NoFileExists(t, orphaned2, "orphaned template 2 should be deleted")
err = tm.regenerateTemplateMetadata(tmpDir)
require.NoError(t, err)
index, err := config.GetNucleiTemplatesIndex()
require.NoError(t, err)
require.Contains(t, index, "cve1", "index should contain kept template cve1")
require.Contains(t, index, "cve2", "index should contain kept template cve2")
require.NotContains(t, index, "old-cve", "index should not contain deleted template")
require.NotContains(t, index, "old-exposure", "index should not contain deleted template")
checksums, err := tm.getChecksumFromDir(tmpDir)
require.NoError(t, err)
require.Contains(t, checksums, template1, "checksum should contain kept template1")
require.Contains(t, checksums, template2, "checksum should contain kept template2")
require.NotContains(t, checksums, orphaned1, "checksum should not contain deleted template")
require.NotContains(t, checksums, orphaned2, "checksum should not contain deleted template")
require.False(t, fileutil.FolderExists(filepath.Dir(orphaned1)), "empty directory should be purged")
require.False(t, fileutil.FolderExists(filepath.Dir(orphaned2)), "empty directory should be purged")
})
t.Run("handles empty templates directory", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "nuclei-metadata-empty-test-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(tmpDir)
}()
cfgdir, err := os.MkdirTemp("", "nuclei-config-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(cfgdir)
}()
config.DefaultConfig.SetConfigDir(cfgdir)
config.DefaultConfig.SetTemplatesDir(tmpDir)
require.NoError(t, os.MkdirAll(tmpDir, 0755))
err = tm.regenerateTemplateMetadata(tmpDir)
require.NoError(t, err, "should handle empty directory without error")
indexPath := config.DefaultConfig.GetTemplateIndexFilePath()
if fileutil.FileExists(indexPath) {
index, err := config.GetNucleiTemplatesIndex()
require.NoError(t, err)
require.Empty(t, index, "index should be empty for empty templates directory")
}
})
t.Run("purges empty directories", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "nuclei-metadata-purge-test-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(tmpDir)
}()
cfgdir, err := os.MkdirTemp("", "nuclei-config-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(cfgdir)
}()
config.DefaultConfig.SetConfigDir(cfgdir)
config.DefaultConfig.SetTemplatesDir(tmpDir)
emptyDir1 := filepath.Join(tmpDir, "empty1", "nested", "deep")
emptyDir2 := filepath.Join(tmpDir, "empty2")
require.NoError(t, os.MkdirAll(emptyDir1, 0755))
require.NoError(t, os.MkdirAll(emptyDir2, 0755))
templateFile := filepath.Join(tmpDir, "kept", "template.yaml")
require.NoError(t, os.MkdirAll(filepath.Dir(templateFile), 0755))
require.NoError(t, os.WriteFile(templateFile, []byte(`id: kept-template
info:
name: Kept
author: test
severity: info`), 0644))
err = tm.regenerateTemplateMetadata(tmpDir)
require.NoError(t, err)
require.False(t, fileutil.FolderExists(emptyDir1), "empty nested directory should be purged")
require.False(t, fileutil.FolderExists(emptyDir2), "empty directory should be purged")
require.True(t, fileutil.FolderExists(filepath.Dir(templateFile)), "directory with template should not be purged")
})
}