backup_test.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. package backup
  2. import (
  3. "archive/zip"
  4. "bytes"
  5. "os"
  6. "path/filepath"
  7. "strings"
  8. "testing"
  9. )
  10. func TestCreateAndInspectBackup(t *testing.T) {
  11. tempDir := t.TempDir()
  12. debtikDir := filepath.Join(tempDir, "debtik")
  13. netDir := filepath.Join(tempDir, "network")
  14. storeDir := filepath.Join(tempDir, "backups")
  15. // Create test structure
  16. if err := os.MkdirAll(filepath.Join(debtikDir, "certs"), 0755); err != nil {
  17. t.Fatal(err)
  18. }
  19. if err := os.MkdirAll(netDir, 0755); err != nil {
  20. t.Fatal(err)
  21. }
  22. metadataContent := []byte(`{"version": 1, "description": "test"}`)
  23. keyContent := []byte(`super-secret-ssh-key`)
  24. certContent := []byte(`fake-cert-data`)
  25. ifacesContent := []byte(`auto eth0` + "\n" + `iface eth0 inet dhcp`)
  26. if err := os.WriteFile(filepath.Join(debtikDir, "metadata.json"), metadataContent, 0644); err != nil {
  27. t.Fatal(err)
  28. }
  29. if err := os.WriteFile(filepath.Join(debtikDir, "ssh_host_ed25519_key"), keyContent, 0600); err != nil {
  30. t.Fatal(err)
  31. }
  32. if err := os.WriteFile(filepath.Join(debtikDir, "certs", "server.crt"), certContent, 0644); err != nil {
  33. t.Fatal(err)
  34. }
  35. if err := os.WriteFile(filepath.Join(netDir, "interfaces.cfg"), ifacesContent, 0644); err != nil {
  36. t.Fatal(err)
  37. }
  38. mgr := NewManager(debtikDir, netDir, storeDir, nil)
  39. // Create backup
  40. zipBytes, manifest, err := mgr.CreateBackup("Unit test backup")
  41. if err != nil {
  42. t.Fatalf("CreateBackup failed: %v", err)
  43. }
  44. if len(zipBytes) == 0 {
  45. t.Fatal("Expected non-empty zip bytes")
  46. }
  47. if manifest.Comment != "Unit test backup" {
  48. t.Errorf("Unexpected manifest comment: %s", manifest.Comment)
  49. }
  50. if len(manifest.Files) != 4 {
  51. t.Fatalf("Expected 4 files in manifest, got %d", len(manifest.Files))
  52. }
  53. // Inspect backup
  54. r := bytes.NewReader(zipBytes)
  55. inspected, err := mgr.InspectBackup(r, int64(len(zipBytes)))
  56. if err != nil {
  57. t.Fatalf("InspectBackup failed: %v", err)
  58. }
  59. if inspected.Hostname != manifest.Hostname {
  60. t.Errorf("Hostname mismatch: %s != %s", inspected.Hostname, manifest.Hostname)
  61. }
  62. if len(inspected.Files) != 4 {
  63. t.Errorf("Expected 4 files in inspected manifest, got %d", len(inspected.Files))
  64. }
  65. }
  66. func TestRestoreBackup(t *testing.T) {
  67. tempDir := t.TempDir()
  68. debtikDir := filepath.Join(tempDir, "debtik")
  69. netDir := filepath.Join(tempDir, "network")
  70. storeDir := filepath.Join(tempDir, "backups")
  71. _ = os.MkdirAll(debtikDir, 0755)
  72. _ = os.MkdirAll(netDir, 0755)
  73. origMetadata := []byte(`{"version": "orig"}`)
  74. origKey := []byte(`private-key-123`)
  75. origIfaces := []byte(`auto eth0`)
  76. _ = os.WriteFile(filepath.Join(debtikDir, "metadata.json"), origMetadata, 0644)
  77. _ = os.WriteFile(filepath.Join(debtikDir, "ssh_host_ed25519_key"), origKey, 0600)
  78. _ = os.WriteFile(filepath.Join(netDir, "interfaces.cfg"), origIfaces, 0644)
  79. restarted := false
  80. restartHook := func() error {
  81. restarted = true
  82. return nil
  83. }
  84. mgr := NewManager(debtikDir, netDir, storeDir, restartHook)
  85. _ = restarted
  86. // 1. Create backup
  87. zipBytes, _, err := mgr.CreateBackup("Pre-modify backup")
  88. if err != nil {
  89. t.Fatalf("CreateBackup failed: %v", err)
  90. }
  91. // 2. Modify live files
  92. _ = os.WriteFile(filepath.Join(debtikDir, "metadata.json"), []byte(`{"version": "modified"}`), 0644)
  93. _ = os.WriteFile(filepath.Join(debtikDir, "rogue.json"), []byte(`rogue data`), 0644)
  94. _ = os.WriteFile(filepath.Join(netDir, "interfaces.cfg"), []byte(`modified eth0`), 0644)
  95. // 3. Restore backup
  96. r := bytes.NewReader(zipBytes)
  97. res, err := mgr.RestoreBackup(r, int64(len(zipBytes)))
  98. if err != nil {
  99. t.Fatalf("RestoreBackup failed: %v", err)
  100. }
  101. if !res.Success {
  102. t.Fatal("Expected restore success true")
  103. }
  104. // 4. Verify original metadata was restored
  105. restoredMeta, err := os.ReadFile(filepath.Join(debtikDir, "metadata.json"))
  106. if err != nil {
  107. t.Fatalf("Failed reading restored metadata: %v", err)
  108. }
  109. if string(restoredMeta) != string(origMetadata) {
  110. t.Errorf("Metadata mismatch: got %q, want %q", string(restoredMeta), string(origMetadata))
  111. }
  112. // 5. Verify rogue file was removed during sync
  113. if _, err := os.Stat(filepath.Join(debtikDir, "rogue.json")); !os.IsNotExist(err) {
  114. t.Error("Expected rogue.json to be cleaned up after restore")
  115. }
  116. // 6. Verify key permissions are 0600
  117. keyInfo, err := os.Stat(filepath.Join(debtikDir, "ssh_host_ed25519_key"))
  118. if err != nil {
  119. t.Fatalf("Failed reading restored key: %v", err)
  120. }
  121. if keyInfo.Mode().Perm() != 0600 {
  122. t.Errorf("Expected 0600 perm on private key, got %v", keyInfo.Mode().Perm())
  123. }
  124. // 7. Verify pre-restore fallback backup exists in storeDir
  125. if res.SafetyBackup == "" {
  126. t.Error("Expected non-empty SafetyBackup path")
  127. }
  128. if _, err := os.Stat(res.SafetyBackup); err != nil {
  129. t.Errorf("Safety backup file does not exist: %v", err)
  130. }
  131. }
  132. func TestZipSlipPrevention(t *testing.T) {
  133. tempDir := t.TempDir()
  134. debtikDir := filepath.Join(tempDir, "debtik")
  135. netDir := filepath.Join(tempDir, "network")
  136. storeDir := filepath.Join(tempDir, "backups")
  137. mgr := NewManager(debtikDir, netDir, storeDir, nil)
  138. // Create malicious zip with traversal path
  139. buf := new(bytes.Buffer)
  140. zw := zip.NewWriter(buf)
  141. // Valid manifest
  142. mf, err := zw.Create("manifest.json")
  143. if err != nil {
  144. t.Fatal(err)
  145. }
  146. _, _ = mf.Write([]byte(`{"version": "1.0", "files": []}`))
  147. // Malicious traversal file
  148. malicious, err := zw.Create("../../../etc/evil.conf")
  149. if err != nil {
  150. t.Fatal(err)
  151. }
  152. _, _ = malicious.Write([]byte(`malicious payload`))
  153. _ = zw.Close()
  154. r := bytes.NewReader(buf.Bytes())
  155. _, err = mgr.InspectBackup(r, int64(buf.Len()))
  156. if err == nil {
  157. t.Fatal("Expected InspectBackup to fail on Zip Slip traversal entry")
  158. }
  159. if !strings.Contains(err.Error(), "illegal entry path") {
  160. t.Errorf("Expected illegal entry path error, got %v", err)
  161. }
  162. // Attempt restore
  163. _, err = mgr.RestoreBackup(r, int64(buf.Len()))
  164. if err == nil {
  165. t.Fatal("Expected RestoreBackup to fail on Zip Slip entry")
  166. }
  167. }
  168. func TestLocalBackupsAndPruning(t *testing.T) {
  169. tempDir := t.TempDir()
  170. debtikDir := filepath.Join(tempDir, "debtik")
  171. netDir := filepath.Join(tempDir, "network")
  172. storeDir := filepath.Join(tempDir, "backups")
  173. _ = os.MkdirAll(debtikDir, 0755)
  174. _ = os.WriteFile(filepath.Join(debtikDir, "dummy.json"), []byte("{}"), 0644)
  175. mgr := NewManager(debtikDir, netDir, storeDir, nil)
  176. // Create 7 pre-restore backups
  177. for i := 0; i < 7; i++ {
  178. _, err := mgr.CreatePreRestoreBackup()
  179. if err != nil {
  180. t.Fatalf("Failed creating pre-restore backup %d: %v", i, err)
  181. }
  182. }
  183. // List backups
  184. list, err := mgr.ListLocalBackups()
  185. if err != nil {
  186. t.Fatalf("ListLocalBackups failed: %v", err)
  187. }
  188. // Pre-restore backups should be pruned to keep 5
  189. if len(list) != 5 {
  190. t.Errorf("Expected 5 backups after pruning, got %d", len(list))
  191. }
  192. // Test GetLocalBackup
  193. savedFile := list[0].Filename
  194. path, err := mgr.GetLocalBackup(savedFile)
  195. if err != nil {
  196. t.Fatalf("GetLocalBackup failed: %v", err)
  197. }
  198. if _, err := os.Stat(path); err != nil {
  199. t.Fatalf("File does not exist: %v", err)
  200. }
  201. // Test GetLocalBackup traversal reject
  202. _, err = mgr.GetLocalBackup("../../evil.zip")
  203. if err == nil {
  204. t.Error("Expected error on traversal filename")
  205. }
  206. }