-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMainWindow.Quarantine.cs
More file actions
362 lines (326 loc) · 15.8 KB
/
Copy pathMainWindow.Quarantine.cs
File metadata and controls
362 lines (326 loc) · 15.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
using System.Windows;
using ClamHub.Core;
using ClamHub.Models;
namespace ClamHub;
/// <summary>
/// Quarantine tab logic: lists isolated files and restores or deletes them.
/// Also provides the post-scan quarantine routine used by the scan flow.
/// Partial class companion to MainWindow.xaml.cs, initialized from InitializeAsync.
/// </summary>
public partial class MainWindow
{
/// <summary>
/// Loads quarantine.json and binds the list, wiring the fill-width column.
/// Called from: MainWindow.InitializeAsync.
/// </summary>
private void InitializeQuarantine()
{
QuarantineManager.Load();
BindQuarantine();
QuarantineList.SizeChanged += (_, _) => ScheduleFill(QuarantineList, QuarantineGridView);
QuarantineList.IsVisibleChanged += (_, _) => ScheduleFill(QuarantineList, QuarantineGridView);
}
/// <summary>Rebinds the quarantine list. Called from: init and every action.</summary>
private void BindQuarantine()
{
QuarantineList.ItemsSource = QuarantineManager.Entries;
// Force the (sorted) default view to re-read the change-less list so every
// restore/delete refreshes the table immediately, including the first one.
System.Windows.Data.CollectionViewSource.GetDefaultView(QuarantineManager.Entries)?.Refresh();
QuarantineStatus.Text = $"{QuarantineManager.Entries.Count} file(s) in quarantine.";
ScheduleFill(QuarantineList, QuarantineGridView);
}
/// <summary>
/// Rebinds the quarantine list for callers outside this class (the Detections
/// window quarantines files too). Called from: DetectionsWindow.Quarantine_Click.
/// </summary>
internal void RefreshQuarantineView() => BindQuarantine();
/// <summary>
/// Moves the scan's infected files into quarantine (GUI-managed move that
/// keeps the original path for an exact restore). The per-file XOR copy runs
/// on a worker thread (QuarantineManager.QuarantineAsync), so a multi-GB
/// infected file no longer freezes the UI. Returns how many were moved
/// successfully. Called from: RunScanGuarded after a Quarantine scan.
/// </summary>
private async Task<int> QuarantineInfectedFilesAsync(IReadOnlyList<string> infectedLines)
{
int moved = 0, failed = 0;
foreach (var line in infectedLines)
{
if (!ScanEngine.TryParseFoundLine(line, out var path, out var threat))
continue;
var (ok, error) = await QuarantineManager.QuarantineAsync(path, threat);
if (ok)
moved++;
else
{
failed++;
AppendLine($"Quarantine failed for {path}: {error}");
}
}
if (moved > 0 || failed > 0)
AppendLine($"Quarantine: {moved} file(s) moved" +
(failed > 0 ? $", {failed} failed (see above)." : "."));
BindQuarantine();
return moved;
}
/// <summary>Reloads the quarantine index. Called from: XAML Click binding.</summary>
private void RefreshQuarantine_Click(object sender, RoutedEventArgs e)
{
QuarantineManager.Load();
BindQuarantine();
}
/// <summary>
/// Returns a snapshot of the selected quarantine entries (multi-selection with
/// Ctrl/Shift, v1.0.3.5), with a status hint when nothing is selected.
/// Called from: the quarantine action handlers.
/// </summary>
private List<QuarantineEntry> SelectedQuarantineEntries(string emptyHint)
{
var list = QuarantineList.SelectedItems.Cast<QuarantineEntry>().ToList();
if (list.Count == 0) QuarantineStatus.Text = emptyHint;
return list;
}
/// <summary>Preview text for batch confirmations (up to 5 names). Called from: the quarantine handlers.</summary>
private static string QuarantinePreview(IReadOnlyList<QuarantineEntry> entries)
{
string preview = string.Join("\n", entries.Take(5).Select(x => x.OriginalName));
if (entries.Count > 5) preview += $"\n...and {entries.Count - 5} more";
return preview;
}
/// <summary>
/// Restores ONE quarantined file, asking per file before overwriting an
/// existing target, and writes the console line + History record on success.
/// The XOR-reversing copy runs on a worker thread (RestoreAsync) so a large
/// file does not freeze the UI. Returns true when the file is back at its
/// original path. Called from: RestoreQuarantine_Click and
/// RestoreWhitelistQuarantine_Click (batch loops).
/// </summary>
private async Task<bool> RestoreOneQuarantineAsync(QuarantineEntry entry)
{
bool overwritten = false;
var (ok, error) = await QuarantineManager.RestoreAsync(entry, overwrite: false);
if (!ok)
{
// Offer overwrite when the only problem is an existing target file.
if (error == null || !error.Contains("already exists"))
{
AppendLine($"{entry.OriginalName}: restore failed: {error ?? "unknown error"}");
return false;
}
if (!Confirm("Overwrite",
$"A file already exists at:\n{entry.OriginalPath}\n\nOverwrite it?",
"Overwrite", "Cancel"))
{
AppendLine($"{entry.OriginalName}: restore skipped (a file exists at the target).");
return false;
}
var (okOverwrite, overwriteError) = await QuarantineManager.RestoreAsync(entry, overwrite: true);
if (!okOverwrite)
{
AppendLine($"{entry.OriginalName}: restore failed: {overwriteError ?? "unknown error"}");
return false;
}
overwritten = true;
}
AppendLine($"{entry.OriginalName} restored{(overwritten ? " (overwritten)" : "")} to {entry.OriginalPath}");
AddHistory("Quarantine action", entry.OriginalPath, "", "Restored",
$"Restored from quarantine.{Environment.NewLine}" +
$"File: {entry.OriginalName}{Environment.NewLine}" +
$"To: {entry.OriginalPath}" +
(overwritten ? $"{Environment.NewLine}(an existing file was overwritten)" : ""));
return true;
}
/// <summary>
/// Restores the selected file(s) to their original paths (one combined
/// confirmation; overwrite conflicts are asked per file). Called from: XAML
/// Click binding.
/// </summary>
private async void RestoreQuarantine_Click(object sender, RoutedEventArgs e)
{
var entries = SelectedQuarantineEntries("Select one or more files to restore.");
if (entries.Count == 0) return;
if (!Confirm("Restore from quarantine",
$"Restore {entries.Count} file(s) to their original locations?\n\n{QuarantinePreview(entries)}\n\n" +
"These files were flagged as infected. Restore anyway?",
"Restore", "Cancel"))
return;
AppendSection("QUARANTINE");
int done = 0;
foreach (var entry in entries)
if (await RestoreOneQuarantineAsync(entry)) done++;
BindQuarantine();
QuarantineStatus.Text = done == entries.Count
? $"{done} file(s) restored."
: $"{done} of {entries.Count} restored (see console).";
}
/// <summary>
/// Permanently deletes the selected quarantined file(s) after one combined
/// confirmation. Called from: XAML Click binding.
/// </summary>
private async void DeleteQuarantine_Click(object sender, RoutedEventArgs e)
{
var entries = SelectedQuarantineEntries("Select one or more files to delete.");
if (entries.Count == 0) return;
if (!Confirm("Delete from quarantine",
$"Permanently delete {entries.Count} file(s) from quarantine?\n\n{QuarantinePreview(entries)}\n\n" +
"This cannot be undone.",
"Delete", "Cancel"))
return;
AppendSection("QUARANTINE");
int done = 0, failed = 0;
foreach (var entry in entries)
{
// Worker thread: deleting a huge stored file can take a moment.
var (ok, error) = await QuarantineManager.DeleteAsync(entry);
if (ok)
{
done++;
AppendLine($"{entry.OriginalName} permanently deleted.");
AddHistory("Quarantine action", entry.OriginalPath, "", "Removed",
$"Permanently removed from quarantine.{Environment.NewLine}" +
$"File: {entry.OriginalName}{Environment.NewLine}" +
$"Original path: {entry.OriginalPath}");
}
else
{
failed++;
AppendLine($"{entry.OriginalName}: delete failed: {error ?? "unknown error"}");
}
}
BindQuarantine();
QuarantineStatus.Text = failed == 0
? $"{done} file(s) permanently deleted."
: $"{done} deleted, {failed} failed (see console).";
}
/// <summary>
/// Looks up the selected quarantined file on VirusTotal via the ORIGINAL file's
/// SHA256. The stored copy is XOR-obfuscated, so the hash is recomputed from the
/// de-obfuscated bytes (in memory only) to match what VT knows. Output goes to
/// the console. Called from: XAML Click binding (Quarantine VirusTotal).
/// </summary>
private async void QuarantineVirusTotal_Click(object sender, RoutedEventArgs e)
{
var entries = SelectedQuarantineEntries("Select one or more files to check on VirusTotal.");
if (entries.Count == 0) return;
if (entries.Count > 4)
{
AppendSection("VIRUSTOTAL");
AppendLine($"{entries.Count} lookups queued; the free VirusTotal tier allows 4 per minute, so this takes a while.");
}
foreach (var entry in entries)
{
// Worker thread: a full read pass over the stored file to hash it.
var (sha256, error) = await QuarantineManager.ComputeOriginalSha256Async(entry);
if (sha256 == null)
{
AppendSection("VIRUSTOTAL");
AppendLine($"{entry.OriginalName}: could not read the quarantined file to hash it: {error}");
continue;
}
var stored = System.IO.Path.Combine(AppPaths.QuarantineDir, entry.Id);
await RunVirusTotalLookup(stored, entry.OriginalName, sha256);
}
}
/// <summary>
/// Opens the signature search window pre-seeded with the selected entry's threat name
/// and immediately searches every database for it, so the user can see which databases
/// carry that detection (and under which names). Read-only: it does not touch the
/// quarantined file. Called from: the "Compare with databanks" button in the Quarantine
/// tab (MainWindow.xaml).
/// </summary>
private void CompareWithDatabanks_Click(object sender, RoutedEventArgs e)
{
// Acts on the FIRST selected entry (one search window per signature).
if (QuarantineList.SelectedItem is not QuarantineEntry entry)
{
QuarantineStatus.Text = "Select a file to compare with the databases.";
return;
}
string threat = (entry.Threat ?? "").Trim();
if (threat.Length == 0)
{
QuarantineStatus.Text = "This entry has no recorded threat name to search for.";
return;
}
// libclamav appends ".UNOFFICIAL" to detections from UNSIGNED databases (plain
// text .ndb/.hdb etc., i.e. anything that is not a signed .cvd/.cld container).
// The suffix exists only in scan REPORTS, never inside the database files, so it
// must be stripped or the search would find nothing.
const string unofficial = ".UNOFFICIAL";
if (threat.EndsWith(unofficial, StringComparison.OrdinalIgnoreCase))
threat = threat[..^unofficial.Length];
// Non-modal and ownerless (v1.0.3.6), like the Signatures tab's search
// opener. The constructor runs the search.
ToolWindows.Show(new SignatureSearchWindow(threat), this);
}
/// <summary>Opens the quarantine folder in Explorer. Called from: XAML Click binding.</summary>
private void OpenQuarantineFolder_Click(object sender, RoutedEventArgs e)
{
try
{
System.Diagnostics.Process.Start("explorer.exe", AppPaths.QuarantineDir);
}
catch (Exception ex)
{
QuarantineStatus.Text = $"Could not open folder: {ex.Message}";
}
}
/// <summary>
/// Restores the selected file(s) AND adds them to the whitelist so future scans
/// ignore them (for confirmed false positives). One combined confirmation;
/// restores first (per-file overwrite prompt as in a normal restore), then
/// whitelists all restored files in ONE pass through the shared flow
/// (ApplySignatureAddAsync: mutual exclusion with the blacklist + a History
/// entry). No automatic daemon reload, only the DaemonReloadNote hint.
/// Called from: XAML Click binding (Restore + whitelist).
/// </summary>
private async void RestoreWhitelistQuarantine_Click(object sender, RoutedEventArgs e)
{
var entries = SelectedQuarantineEntries("Select one or more files to restore and whitelist.");
if (entries.Count == 0) return;
if (!Confirm("Restore and whitelist",
$"Restore {entries.Count} file(s) to their original locations\n\n{QuarantinePreview(entries)}\n\n" +
"and add them to the whitelist so future scans ignore these exact files. " +
"Only do this if you are sure the detections are false positives.",
"Restore + whitelist", "Cancel"))
return;
// Step 1 - restore each file, reusing the same per-file overwrite prompt as
// a normal restore. Each success writes its own console line + History record.
AppendSection("QUARANTINE");
var restored = new List<QuarantineEntry>();
foreach (var entry in entries)
if (await RestoreOneQuarantineAsync(entry)) restored.Add(entry);
BindQuarantine();
// Step 2 - whitelist every restored file in ONE pass through the shared flow
// (mutual exclusion with the blacklist + one "whitelist modified" History
// entry + count refresh). The commit reports file NAMES, so results are
// matched back by name.
var paths = restored
.Where(x => System.IO.File.Exists(x.OriginalPath))
.Select(x => x.OriginalPath)
.ToArray();
foreach (var entry in restored.Where(x => !System.IO.File.Exists(x.OriginalPath)))
AppendLine($"{entry.OriginalName}: restored, but the file was not found afterwards, so it was not whitelisted.");
if (paths.Length == 0)
{
QuarantineStatus.Text = restored.Count == 0
? "Nothing was restored (see console)."
: $"{restored.Count} restored, nothing whitelisted (see console).";
return;
}
var result = await ApplySignatureAddAsync(
CustomSignatureManager.ListKind.Whitelist, paths, this);
var whitelistedNames = new HashSet<string>(
result.Added.Concat(result.Moved).Concat(result.SkippedSameList),
StringComparer.OrdinalIgnoreCase);
int whitelisted = restored.Count(x => whitelistedNames.Contains(x.OriginalName));
foreach (var name in result.SkippedConflict)
AppendLine($"{name}: kept on the blacklist, not whitelisted.");
foreach (var (path, reason) in result.Failed)
AppendLine($"Whitelist failed for {path}: {reason}");
if (result.Added.Count > 0 || result.Moved.Count > 0)
AppendLine(DaemonReloadNote);
QuarantineStatus.Text = $"{restored.Count} restored, {whitelisted} whitelisted (see console).";
}
}