Skip to content

refactor(installer): preserve local templates during updates - #7584

Open
dwisiswant0 wants to merge 2 commits into
devfrom
dwisiswant0/refactor/installer/preserve-local-templates-during-updates
Open

refactor(installer): preserve local templates during updates#7584
dwisiswant0 wants to merge 2 commits into
devfrom
dwisiswant0/refactor/installer/preserve-local-templates-during-updates

Conversation

@dwisiswant0

@dwisiswant0 dwisiswant0 commented Jul 24, 2026

Copy link
Copy Markdown
Member

Proposed changes

Track files installed by each official release and
remove only retired templates whose contents still
match the recorded digest. Bootstrap ownership for
existing installations so upgrades keep unknown
and locally modified templates intact.

Write templates, ownership state, and config thru
synced temporary files. Recover interrupted
cleanup before another update and record the new
version only after metadata finalization succeeds.

Proof

Checklist

  • Pull request is created against the dev branch
  • All checks passed (lint, unit/integration/regression tests etc.) with my changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation (if appropriate)

Summary by CodeRabbit

  • New Features

    • Improved template installation and updates with stronger ownership recovery and reconciliation during finalization.
    • Added safer, atomic template config and output handling with improved metadata/index/checksum regeneration.
  • Bug Fixes

    • Hardened filesystem writes against symlink traversal and special files (e.g., FIFOs), including correct behavior for relative/dangling symlinks.
    • Ensured permissions are honored correctly (including umask) and replacements are handled reliably across platforms.
  • Tests

    • Expanded cross-platform test coverage for atomic replacement, umask/permission behavior, containment rules, and recovery paths.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 20d43571-2102-4bfe-bc0a-656368ba5f84

📥 Commits

Reviewing files that changed from the base of the PR and between c7b025e and 7d5af36.

📒 Files selected for processing (4)
  • pkg/installer/template_ownership.go
  • pkg/installer/template_ownership_publish_darwin.go
  • pkg/installer/template_ownership_publish_linux.go
  • pkg/installer/template_ownership_test.go

Walkthrough

The PR adds atomic config and template writes, symlink-safe path resolution, ownership recovery, platform-specific filesystem helpers, metadata and checksum regeneration, and expanded filesystem and path-safety tests.

Changes

Template integrity

Layer / File(s) Summary
Atomic configuration writes
go.mod, pkg/catalog/config/nucleiconfig*
Configuration writes use temporary files, permission preservation, syncing, symlink resolution, and platform-specific replacement.
Atomic installation and finalization
pkg/installer/template.go, pkg/installer/template_test.go, pkg/installer/zipslip_unix_test.go, pkg/installer/template_output_unix_test.go
Installation and updates write outputs atomically, synchronize touched directories, regenerate metadata and checksums, and commit versions after finalization.
Ownership manifests and recovery
pkg/installer/template_ownership.go, pkg/installer/template_ownership_test.go, pkg/installer/template_ownership_publish_*, pkg/installer/template_ownership_state_unix_test.go
Restore operations classify unsupported renames, use exclusive publication fallback, preserve modes, and clean up interrupted restore state.
Platform filesystem support
pkg/installer/template_ownership_publish_*, pkg/installer/template_ownership_sync*
Platform-specific rename and synchronization behavior is added for Windows, Unix, Darwin, and unsupported platforms, with targeted tests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: mzack9999

Sequence Diagram(s)

sequenceDiagram
  participant TemplateManager
  participant OwnershipRecovery
  participant TemplateWriter
  participant MetadataRegenerator
  participant Filesystem
  TemplateManager->>OwnershipRecovery: recover or bootstrap ownership
  TemplateManager->>TemplateWriter: write template outputs
  TemplateWriter->>Filesystem: atomically replace files and sync directories
  TemplateManager->>MetadataRegenerator: finalize ownership and metadata
  MetadataRegenerator->>Filesystem: write index and checksum maps
  MetadataRegenerator-->>TemplateManager: finalized checksums and version
Loading

Poem

A rabbit hops through files tonight,
Atomic writes land just right.
Old templates rest, ownership stays,
Symlinks guide the safest ways.
Checksums sparkle, tests applaud—
Clean config earns a carrot nod!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: preserving local templates during installer updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dwisiswant0/refactor/installer/preserve-local-templates-during-updates

Comment @coderabbitai help to get the list of available commands.

Track files installed by each official release and
remove only retired templates whose contents still
match the recorded digest. Bootstrap ownership for
existing installations so upgrades keep unknown
and locally modified templates intact.

Write templates, ownership state, and config thru
synced temporary files. Recover interrupted
cleanup before another update and record the new
version only after metadata finalization succeeds.

Signed-off-by: Dwi Siswanto <git@dw1.io>
@dwisiswant0
dwisiswant0 force-pushed the dwisiswant0/refactor/installer/preserve-local-templates-during-updates branch from ab55db7 to c7b025e Compare July 30, 2026 16:03
@dwisiswant0
dwisiswant0 marked this pull request as ready for review July 30, 2026 16:04
@dwisiswant0
dwisiswant0 requested a review from Mzack9999 July 30, 2026 16:04

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
pkg/catalog/config/nucleiconfig.go (1)

409-419: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

WriteTemplatesIndex still writes non-atomically.

The config write now goes through the temp-file + replace path, but the index (Line 419) remains a direct os.WriteFile, so an interrupted update can still leave a truncated/partial index next to a fully-written config. Consider routing it through writeTemplatesConfigFile-style atomic replacement for consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/catalog/config/nucleiconfig.go` around lines 409 - 419, Update
Config.WriteTemplatesIndex to write the generated index through the existing
writeTemplatesConfigFile-style temporary-file-and-replace mechanism instead of
calling os.WriteFile directly, preserving the current index contents and file
path while ensuring interrupted updates cannot leave partial data.
pkg/catalog/config/nucleiconfig_unix_test.go (1)

12-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Narrow the build tag to unix !windows still includes targets where these syscalls aren’t available, so this test file can be selected on unsupported platforms. //go:build unix matches the intended scope better.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/catalog/config/nucleiconfig_unix_test.go` around lines 12 - 27, Change
the build constraint for the tests containing
TestWriteTemplatesConfigFileHonorsUmaskForNewFile from !windows to unix so the
file is selected only on platforms supporting syscall.Umask; keep the test
implementation unchanged.
pkg/installer/template_ownership.go (1)

525-528: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Commented-out warnings leave every preservation decision silent.

Lines 526, 540, 623, 634, and 652 are the only places where the installer would tell a user that a retired template was preserved (symlink, non-regular, locally modified) and that ownership was relinquished. All five are commented out, so the reconciliation outcome is now unobservable. Either restore them (gologger.Verbose() if Warning is too loud for thousands of files) or delete the dead comments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/installer/template_ownership.go` around lines 525 - 528, The
retired-template preservation decisions in the reconciliation logic are
currently silent because the logging calls at the symlink, non-regular, locally
modified, and ownership-relinquished branches are commented out. Restore these
messages using gologger.Verbose() (or Warning() if appropriate) at the five
referenced branches, or remove the dead comments if logging is intentionally not
retained.
pkg/installer/template_ownership_sync_windows_test.go (1)

37-62: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the cross-platform tests out of pkg/installer/template_ownership_sync_windows_test.go. TestSyncTemplateOwnershipDirectory and TestRenameTemplateRestoreNoReplaceUsesOpenedRoot should live in a non-Windows test file so Linux/macOS exercise the opened-root path too; keep only TestSyncTemplateOwnershipFileRestoresModeAfterOpenFailure here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/installer/template_ownership_sync_windows_test.go` around lines 37 - 62,
Move TestSyncTemplateOwnershipDirectory and
TestRenameTemplateRestoreNoReplaceUsesOpenedRoot into a non-Windows test file so
they run on Linux and macOS as well as Windows. Remove those tests from
template_ownership_sync_windows_test.go, leaving only
TestSyncTemplateOwnershipFileRestoresModeAfterOpenFailure there and preserving
all test logic unchanged.
pkg/installer/template.go (1)

269-299: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Stream the prior-release archive instead of materializing it all in memory. fetchTemplateReleaseArchive reads the whole zipball into a bytes.Buffer before returning it, so larger releases can spike RSS; a plain http.Client plus a streaming or size-bounded reader would keep this path lighter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/installer/template.go` around lines 269 - 299, Update
fetchTemplateReleaseArchive to return a streaming or size-bounded reader backed
by the HTTP response instead of copying the entire archive into bytes.Buffer and
bytes.Reader. Preserve the existing request, status validation, and error
context, and ensure the response body is properly closed when the returned
reader no longer needs it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/installer/template_ownership_publish_linux.go`:
- Around line 12-26: Classify unsupported flag errors in
renameTemplateRestoreNoReplace in
pkg/installer/template_ownership_publish_linux.go lines 12-26 by wrapping
ENOSYS, EINVAL, and EOPNOTSUPP from unix.Renameat2 as errors.ErrUnsupported;
apply the equivalent ENOTSUP classification in
pkg/installer/template_ownership_publish_darwin.go lines 12-26 for
unix.RenameatxNp, allowing callers to select the existing
copyQuarantinedTemplateWithLink fallback while preserving no-overwrite behavior.

In `@pkg/installer/template_ownership_publish_other.go`:
- Around line 10-12: Update renameTemplateRestoreNoReplace so unsupported
platforms return the non-fatal fallback signal expected by the generic restore
flow instead of propagating errors.ErrUnsupported. Ensure template
restore/update continues through the move/copy fallback on non-linux, darwin,
and windows targets while preserving exclusive rename behavior where supported.

In `@pkg/installer/template_ownership.go`:
- Around line 202-205: Update the recovery error handling around
loadTemplateOwnership in updateIfOutdatedLocked to include the pending
quarantine or restore-state artifact paths alongside the manifest-load failure.
Preserve error wrapping while naming the paths needed for users to identify and
remove the blocking recovery state.

In `@pkg/installer/template_test.go`:
- Around line 264-278: Initialize a logger before configuring the test config in
TestGetTemplateOutputLocationRoutesIgnoreFile, following the existing
gologger.DefaultLogger setup used by
TestFinalizeTemplateReleaseCommitsVersionLast. Ensure config.Config has a
non-nil Logger before calling SetConfigDir so its error paths remain safe.

---

Nitpick comments:
In `@pkg/catalog/config/nucleiconfig_unix_test.go`:
- Around line 12-27: Change the build constraint for the tests containing
TestWriteTemplatesConfigFileHonorsUmaskForNewFile from !windows to unix so the
file is selected only on platforms supporting syscall.Umask; keep the test
implementation unchanged.

In `@pkg/catalog/config/nucleiconfig.go`:
- Around line 409-419: Update Config.WriteTemplatesIndex to write the generated
index through the existing writeTemplatesConfigFile-style
temporary-file-and-replace mechanism instead of calling os.WriteFile directly,
preserving the current index contents and file path while ensuring interrupted
updates cannot leave partial data.

In `@pkg/installer/template_ownership_sync_windows_test.go`:
- Around line 37-62: Move TestSyncTemplateOwnershipDirectory and
TestRenameTemplateRestoreNoReplaceUsesOpenedRoot into a non-Windows test file so
they run on Linux and macOS as well as Windows. Remove those tests from
template_ownership_sync_windows_test.go, leaving only
TestSyncTemplateOwnershipFileRestoresModeAfterOpenFailure there and preserving
all test logic unchanged.

In `@pkg/installer/template_ownership.go`:
- Around line 525-528: The retired-template preservation decisions in the
reconciliation logic are currently silent because the logging calls at the
symlink, non-regular, locally modified, and ownership-relinquished branches are
commented out. Restore these messages using gologger.Verbose() (or Warning() if
appropriate) at the five referenced branches, or remove the dead comments if
logging is intentionally not retained.

In `@pkg/installer/template.go`:
- Around line 269-299: Update fetchTemplateReleaseArchive to return a streaming
or size-bounded reader backed by the HTTP response instead of copying the entire
archive into bytes.Buffer and bytes.Reader. Preserve the existing request,
status validation, and error context, and ensure the response body is properly
closed when the returned reader no longer needs it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c6d25dde-98b7-4381-90b5-2ca7477a1e4f

📥 Commits

Reviewing files that changed from the base of the PR and between ba05210 and c7b025e.

📒 Files selected for processing (22)
  • go.mod
  • pkg/catalog/config/nucleiconfig.go
  • pkg/catalog/config/nucleiconfig_replace.go
  • pkg/catalog/config/nucleiconfig_replace_windows.go
  • pkg/catalog/config/nucleiconfig_replace_windows_test.go
  • pkg/catalog/config/nucleiconfig_test.go
  • pkg/catalog/config/nucleiconfig_unix_test.go
  • pkg/installer/template.go
  • pkg/installer/template_output_unix_test.go
  • pkg/installer/template_ownership.go
  • pkg/installer/template_ownership_publish_darwin.go
  • pkg/installer/template_ownership_publish_linux.go
  • pkg/installer/template_ownership_publish_linux_test.go
  • pkg/installer/template_ownership_publish_other.go
  • pkg/installer/template_ownership_publish_windows.go
  • pkg/installer/template_ownership_state_unix_test.go
  • pkg/installer/template_ownership_sync.go
  • pkg/installer/template_ownership_sync_windows.go
  • pkg/installer/template_ownership_sync_windows_test.go
  • pkg/installer/template_ownership_test.go
  • pkg/installer/template_test.go
  • pkg/installer/zipslip_unix_test.go

Comment thread pkg/installer/template_ownership_publish_linux.go
Comment thread pkg/installer/template_ownership_publish_other.go
Comment on lines +202 to +205
previous, err := loadTemplateOwnership(dir)
if err != nil {
return fmt.Errorf("load template ownership for recovery: %w", err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Recovery dead-ends without naming the artifacts that block it.

If a quarantine or restore-state file survives but the manifest is missing or corrupt, this returns an error on every subsequent run (via updateIfOutdatedLocked → line 108 of pkg/installer/template.go), and the message names only the manifest path. Quarantine names are SHA-256 digests, so a user cannot map them back to a template or know what to delete. Include the pending paths in the error so the state is actionable.

🛠️ Proposed change
 	previous, err := loadTemplateOwnership(dir)
 	if err != nil {
-		return fmt.Errorf("load template ownership for recovery: %w", err)
+		return fmt.Errorf("load template ownership for recovery (pending quarantines %v, restore states %v): %w", quarantineEntries, restoreEntries, err)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
previous, err := loadTemplateOwnership(dir)
if err != nil {
return fmt.Errorf("load template ownership for recovery: %w", err)
}
previous, err := loadTemplateOwnership(dir)
if err != nil {
return fmt.Errorf("load template ownership for recovery (pending quarantines %v, restore states %v): %w", quarantineEntries, restoreEntries, err)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/installer/template_ownership.go` around lines 202 - 205, Update the
recovery error handling around loadTemplateOwnership in updateIfOutdatedLocked
to include the pending quarantine or restore-state artifact paths alongside the
manifest-load failure. Preserve error wrapping while naming the paths needed for
users to identify and remove the blocking recovery state.

Comment on lines +264 to +278
func TestGetTemplateOutputLocationRoutesIgnoreFile(t *testing.T) {
previousConfig := config.DefaultConfig
cfg := &config.Config{}
cfg.SetConfigDir(t.TempDir())
config.DefaultConfig = cfg
t.Cleanup(func() { config.DefaultConfig = previousConfig })

ignoreEntry := filepath.Join(t.TempDir(), config.NucleiIgnoreFileName)
require.NoError(t, os.WriteFile(ignoreEntry, nil, 0o600))
info, err := os.Stat(ignoreEntry)
require.NoError(t, err)
rootDir, writePath := (&TemplateManager{}).getTemplateOutputLocation(t.TempDir(), "nuclei-templates/"+config.NucleiIgnoreFileName, info)
require.Equal(t, cfg.GetConfigDir(), rootDir)
require.Equal(t, cfg.GetIgnoreFilePath(), writePath)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

config.Config built without a Logger before SetConfigDir.

SetConfigDir calls ReadTemplatesConfig/WriteTemplatesConfig and copyIgnoreFile, all of which log via c.Logger on failure (Logger.Fatal() / Logger.Error()). With Logger nil, any of those failure paths panics instead of failing the assertion — copyIgnoreFile in particular logs when the fallback ignore file can't be copied, which is environment-dependent. TestFinalizeTemplateReleaseCommitsVersionLast already sets gologger.DefaultLogger; do the same here.

🛡️ Proposed fix
-	cfg := &config.Config{}
+	cfg := &config.Config{Logger: gologger.DefaultLogger}
 	cfg.SetConfigDir(t.TempDir())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestGetTemplateOutputLocationRoutesIgnoreFile(t *testing.T) {
previousConfig := config.DefaultConfig
cfg := &config.Config{}
cfg.SetConfigDir(t.TempDir())
config.DefaultConfig = cfg
t.Cleanup(func() { config.DefaultConfig = previousConfig })
ignoreEntry := filepath.Join(t.TempDir(), config.NucleiIgnoreFileName)
require.NoError(t, os.WriteFile(ignoreEntry, nil, 0o600))
info, err := os.Stat(ignoreEntry)
require.NoError(t, err)
rootDir, writePath := (&TemplateManager{}).getTemplateOutputLocation(t.TempDir(), "nuclei-templates/"+config.NucleiIgnoreFileName, info)
require.Equal(t, cfg.GetConfigDir(), rootDir)
require.Equal(t, cfg.GetIgnoreFilePath(), writePath)
}
func TestGetTemplateOutputLocationRoutesIgnoreFile(t *testing.T) {
previousConfig := config.DefaultConfig
cfg := &config.Config{Logger: gologger.DefaultLogger}
cfg.SetConfigDir(t.TempDir())
config.DefaultConfig = cfg
t.Cleanup(func() { config.DefaultConfig = previousConfig })
ignoreEntry := filepath.Join(t.TempDir(), config.NucleiIgnoreFileName)
require.NoError(t, os.WriteFile(ignoreEntry, nil, 0o600))
info, err := os.Stat(ignoreEntry)
require.NoError(t, err)
rootDir, writePath := (&TemplateManager{}).getTemplateOutputLocation(t.TempDir(), "nuclei-templates/"+config.NucleiIgnoreFileName, info)
require.Equal(t, cfg.GetConfigDir(), rootDir)
require.Equal(t, cfg.GetIgnoreFilePath(), writePath)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/installer/template_test.go` around lines 264 - 278, Initialize a logger
before configuring the test config in
TestGetTemplateOutputLocationRoutesIgnoreFile, following the existing
gologger.DefaultLogger setup used by
TestFinalizeTemplateReleaseCommitsVersionLast. Ensure config.Config has a
non-nil Logger before calling SetConfigDir so its error paths remain safe.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants