Skip to content

Commit 52da1f7

Browse files
committed
feat: leftover signature database with 50 app profiles for high-accuracy remnant detection
1 parent 472115f commit 52da1f7

5 files changed

Lines changed: 201 additions & 0 deletions

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using System.Reflection;
2+
using System.Text.Json;
3+
using DeepPurge.Core.Diagnostics;
4+
5+
namespace DeepPurge.Core.Data;
6+
7+
public class LeftoverSignature
8+
{
9+
public string Name { get; set; } = "";
10+
public List<string> Aliases { get; set; } = new();
11+
public List<string> Files { get; set; } = new();
12+
public List<string> Registry { get; set; } = new();
13+
}
14+
15+
public class LeftoverMatch
16+
{
17+
public List<string> FilePaths { get; set; } = new();
18+
public List<string> RegistryPaths { get; set; } = new();
19+
}
20+
21+
public static class LeftoverSignatureDb
22+
{
23+
private static readonly Lazy<List<LeftoverSignature>> _signatures = new(Load);
24+
25+
public static LeftoverMatch? FindMatch(string displayName)
26+
{
27+
if (string.IsNullOrWhiteSpace(displayName)) return null;
28+
29+
var sig = _signatures.Value.FirstOrDefault(s =>
30+
s.Aliases.Any(a => displayName.Contains(a, StringComparison.OrdinalIgnoreCase)));
31+
if (sig == null) return null;
32+
33+
var match = new LeftoverMatch();
34+
foreach (var pattern in sig.Files)
35+
{
36+
var expanded = Environment.ExpandEnvironmentVariables(pattern);
37+
if (expanded.Contains('*'))
38+
{
39+
var dir = Path.GetDirectoryName(expanded);
40+
var glob = Path.GetFileName(expanded);
41+
if (dir != null && Directory.Exists(dir))
42+
{
43+
try
44+
{
45+
foreach (var d in Directory.GetDirectories(dir, glob))
46+
match.FilePaths.Add(d);
47+
}
48+
catch { }
49+
}
50+
}
51+
else if (Directory.Exists(expanded) || File.Exists(expanded))
52+
{
53+
match.FilePaths.Add(expanded);
54+
}
55+
}
56+
57+
foreach (var regPath in sig.Registry)
58+
match.RegistryPaths.Add(regPath);
59+
60+
return (match.FilePaths.Count > 0 || match.RegistryPaths.Count > 0) ? match : null;
61+
}
62+
63+
private static List<LeftoverSignature> Load()
64+
{
65+
try
66+
{
67+
var asm = Assembly.GetExecutingAssembly();
68+
var resourceName = asm.GetManifestResourceNames()
69+
.FirstOrDefault(n => n.EndsWith("leftover-signatures.json", StringComparison.OrdinalIgnoreCase));
70+
if (resourceName == null) return new();
71+
72+
using var stream = asm.GetManifestResourceStream(resourceName);
73+
if (stream == null) return new();
74+
75+
return JsonSerializer.Deserialize<List<LeftoverSignature>>(stream) ?? new();
76+
}
77+
catch (Exception ex)
78+
{
79+
Log.Warn($"Failed to load leftover signatures: {ex.Message}");
80+
return new();
81+
}
82+
}
83+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
[
2+
{"name":"Google Chrome","aliases":["Chrome","Google Chrome"],"files":["%LocalAppData%\\Google\\Chrome","%AppData%\\Google\\Chrome","%ProgramData%\\Google\\Chrome","%ProgramFiles%\\Google\\Chrome"],"registry":["HKCU\\Software\\Google\\Chrome","HKLM\\SOFTWARE\\Google\\Chrome","HKLM\\SOFTWARE\\Policies\\Google\\Chrome","HKCU\\Software\\Google\\Update"]},
3+
{"name":"Mozilla Firefox","aliases":["Firefox","Mozilla Firefox"],"files":["%AppData%\\Mozilla\\Firefox","%LocalAppData%\\Mozilla\\Firefox","%ProgramFiles%\\Mozilla Firefox"],"registry":["HKCU\\Software\\Mozilla\\Firefox","HKLM\\SOFTWARE\\Mozilla\\Firefox","HKLM\\SOFTWARE\\Mozilla\\Mozilla Firefox"]},
4+
{"name":"Microsoft Edge","aliases":["Edge","Microsoft Edge"],"files":["%LocalAppData%\\Microsoft\\Edge","%ProgramFiles(x86)%\\Microsoft\\Edge"],"registry":["HKCU\\Software\\Microsoft\\Edge","HKLM\\SOFTWARE\\Microsoft\\Edge"]},
5+
{"name":"Opera","aliases":["Opera","Opera Stable","Opera GX"],"files":["%AppData%\\Opera Software","%LocalAppData%\\Opera Software"],"registry":["HKCU\\Software\\Opera Software","HKLM\\SOFTWARE\\Opera Software"]},
6+
{"name":"Brave","aliases":["Brave","Brave-Browser"],"files":["%LocalAppData%\\BraveSoftware","%AppData%\\BraveSoftware"],"registry":["HKCU\\Software\\BraveSoftware","HKLM\\SOFTWARE\\BraveSoftware"]},
7+
{"name":"Vivaldi","aliases":["Vivaldi"],"files":["%LocalAppData%\\Vivaldi","%AppData%\\Vivaldi"],"registry":["HKCU\\Software\\Vivaldi","HKLM\\SOFTWARE\\Vivaldi"]},
8+
{"name":"VLC media player","aliases":["VLC","VideoLAN"],"files":["%AppData%\\vlc","%ProgramFiles%\\VideoLAN"],"registry":["HKCU\\Software\\VideoLAN","HKLM\\SOFTWARE\\VideoLAN"]},
9+
{"name":"Spotify","aliases":["Spotify"],"files":["%AppData%\\Spotify","%LocalAppData%\\Spotify"],"registry":["HKCU\\Software\\Spotify","HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Spotify"]},
10+
{"name":"Discord","aliases":["Discord"],"files":["%AppData%\\discord","%LocalAppData%\\Discord"],"registry":["HKCU\\Software\\Discord"]},
11+
{"name":"Slack","aliases":["Slack"],"files":["%AppData%\\Slack","%LocalAppData%\\slack"],"registry":["HKCU\\Software\\Slack"]},
12+
{"name":"Zoom","aliases":["Zoom","Zoom Video Communications"],"files":["%AppData%\\Zoom","%AppData%\\Zoom Video Communications","%LocalAppData%\\Zoom"],"registry":["HKCU\\Software\\Zoom","HKCU\\Software\\ZoomVideo"]},
13+
{"name":"Steam","aliases":["Steam","Valve"],"files":["%ProgramFiles(x86)%\\Steam","%LocalAppData%\\Steam"],"registry":["HKCU\\Software\\Valve\\Steam","HKLM\\SOFTWARE\\Valve\\Steam","HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam"]},
14+
{"name":"Epic Games Launcher","aliases":["EpicGamesLauncher","Epic Games"],"files":["%LocalAppData%\\EpicGamesLauncher","%ProgramData%\\Epic","%ProgramFiles%\\Epic Games"],"registry":["HKCU\\Software\\Epic Games","HKLM\\SOFTWARE\\EpicGames"]},
15+
{"name":"Adobe Acrobat","aliases":["Acrobat","Adobe Acrobat"],"files":["%AppData%\\Adobe\\Acrobat","%LocalAppData%\\Adobe\\Acrobat","%ProgramData%\\Adobe\\Acrobat"],"registry":["HKCU\\Software\\Adobe\\Acrobat","HKLM\\SOFTWARE\\Adobe\\Acrobat"]},
16+
{"name":"Adobe Creative Cloud","aliases":["Creative Cloud","Adobe Creative Cloud"],"files":["%AppData%\\Adobe","%LocalAppData%\\Adobe","%ProgramData%\\Adobe","%ProgramFiles%\\Adobe","%CommonProgramFiles%\\Adobe"],"registry":["HKCU\\Software\\Adobe","HKLM\\SOFTWARE\\Adobe"]},
17+
{"name":"7-Zip","aliases":["7-Zip","7zip"],"files":["%ProgramFiles%\\7-Zip","%AppData%\\7-Zip"],"registry":["HKCU\\Software\\7-Zip","HKLM\\SOFTWARE\\7-Zip"]},
18+
{"name":"WinRAR","aliases":["WinRAR","RARLAB"],"files":["%ProgramFiles%\\WinRAR","%AppData%\\WinRAR"],"registry":["HKCU\\Software\\WinRAR","HKLM\\SOFTWARE\\WinRAR"]},
19+
{"name":"Notepad++","aliases":["Notepad++"],"files":["%AppData%\\Notepad++","%ProgramFiles%\\Notepad++"],"registry":["HKCU\\Software\\Notepad++","HKLM\\SOFTWARE\\Notepad++"]},
20+
{"name":"Visual Studio Code","aliases":["VSCode","Code","Visual Studio Code"],"files":["%AppData%\\Code","%LocalAppData%\\Programs\\Microsoft VS Code","%UserProfile%\\.vscode"],"registry":["HKCU\\Software\\Microsoft\\VisualStudioCode","HKCU\\Software\\Classes\\vscode"]},
21+
{"name":"Git","aliases":["Git","Git for Windows"],"files":["%ProgramFiles%\\Git","%AppData%\\Git","%LocalAppData%\\GitHubDesktop"],"registry":["HKLM\\SOFTWARE\\GitForWindows","HKCU\\Software\\GitForWindows"]},
22+
{"name":"Node.js","aliases":["Node.js","nodejs"],"files":["%ProgramFiles%\\nodejs","%AppData%\\npm","%AppData%\\npm-cache","%LocalAppData%\\npm-cache"],"registry":["HKLM\\SOFTWARE\\Node.js"]},
23+
{"name":"Python","aliases":["Python","Python3"],"files":["%LocalAppData%\\Programs\\Python","%LocalAppData%\\pip","%AppData%\\Python","%ProgramFiles%\\Python*"],"registry":["HKCU\\Software\\Python","HKLM\\SOFTWARE\\Python"]},
24+
{"name":"Java","aliases":["Java","JDK","JRE","Oracle Java"],"files":["%ProgramFiles%\\Java","%ProgramFiles(x86)%\\Java","%ProgramData%\\Oracle\\Java","%AppData%\\.oracle_jre_usage"],"registry":["HKLM\\SOFTWARE\\JavaSoft","HKLM\\SOFTWARE\\WOW6432Node\\JavaSoft"]},
25+
{"name":"Skype","aliases":["Skype"],"files":["%AppData%\\Skype","%AppData%\\Microsoft\\Skype for Desktop","%LocalAppData%\\Packages\\Microsoft.SkypeApp*"],"registry":["HKCU\\Software\\Skype","HKCU\\Software\\Microsoft\\Skype"]},
26+
{"name":"TeamViewer","aliases":["TeamViewer"],"files":["%AppData%\\TeamViewer","%ProgramFiles%\\TeamViewer","%ProgramFiles(x86)%\\TeamViewer"],"registry":["HKCU\\Software\\TeamViewer","HKLM\\SOFTWARE\\TeamViewer","HKLM\\SOFTWARE\\WOW6432Node\\TeamViewer"]},
27+
{"name":"Telegram","aliases":["Telegram","Telegram Desktop"],"files":["%AppData%\\Telegram Desktop","%LocalAppData%\\Telegram Desktop"],"registry":["HKCU\\Software\\Telegram Desktop"]},
28+
{"name":"WhatsApp","aliases":["WhatsApp"],"files":["%LocalAppData%\\WhatsApp","%AppData%\\WhatsApp"],"registry":["HKCU\\Software\\WhatsApp"]},
29+
{"name":"Dropbox","aliases":["Dropbox"],"files":["%AppData%\\Dropbox","%LocalAppData%\\Dropbox","%ProgramFiles(x86)%\\Dropbox","%ProgramData%\\Dropbox"],"registry":["HKCU\\Software\\Dropbox","HKLM\\SOFTWARE\\Dropbox"]},
30+
{"name":"OneDrive","aliases":["OneDrive","Microsoft OneDrive"],"files":["%LocalAppData%\\Microsoft\\OneDrive","%ProgramData%\\Microsoft OneDrive"],"registry":["HKCU\\Software\\Microsoft\\OneDrive"]},
31+
{"name":"iTunes","aliases":["iTunes","Apple iTunes"],"files":["%AppData%\\Apple Computer\\iTunes","%LocalAppData%\\Apple Computer","%ProgramFiles%\\iTunes","%CommonProgramFiles%\\Apple"],"registry":["HKCU\\Software\\Apple Computer, Inc.\\iTunes","HKLM\\SOFTWARE\\Apple Inc.\\iTunes"]},
32+
{"name":"Spotify","aliases":["Spotify"],"files":["%AppData%\\Spotify","%LocalAppData%\\Spotify"],"registry":["HKCU\\Software\\Spotify"]},
33+
{"name":"OBS Studio","aliases":["OBS","OBS Studio","obs-studio"],"files":["%AppData%\\obs-studio","%ProgramFiles%\\obs-studio"],"registry":["HKLM\\SOFTWARE\\OBS Studio"]},
34+
{"name":"GIMP","aliases":["GIMP"],"files":["%AppData%\\GIMP","%ProgramFiles%\\GIMP*"],"registry":["HKLM\\SOFTWARE\\GIMP"]},
35+
{"name":"Audacity","aliases":["Audacity"],"files":["%AppData%\\audacity","%ProgramFiles%\\Audacity"],"registry":["HKLM\\SOFTWARE\\Audacity"]},
36+
{"name":"LibreOffice","aliases":["LibreOffice"],"files":["%AppData%\\LibreOffice","%ProgramFiles%\\LibreOffice"],"registry":["HKLM\\SOFTWARE\\LibreOffice"]},
37+
{"name":"NVIDIA GeForce Experience","aliases":["GeForce Experience","NVIDIA"],"files":["%ProgramData%\\NVIDIA Corporation","%LocalAppData%\\NVIDIA Corporation","%LocalAppData%\\NVIDIA","%ProgramFiles%\\NVIDIA Corporation"],"registry":["HKLM\\SOFTWARE\\NVIDIA Corporation"]},
38+
{"name":"AMD Software","aliases":["AMD Software","Radeon","AMD Radeon"],"files":["%LocalAppData%\\AMD","%ProgramData%\\AMD","%ProgramFiles%\\AMD"],"registry":["HKLM\\SOFTWARE\\AMD","HKCU\\Software\\AMD"]},
39+
{"name":"CCleaner","aliases":["CCleaner","Piriform"],"files":["%ProgramFiles%\\CCleaner","%ProgramData%\\Piriform\\CCleaner"],"registry":["HKCU\\Software\\Piriform\\CCleaner","HKLM\\SOFTWARE\\Piriform\\CCleaner"]},
40+
{"name":"WinSCP","aliases":["WinSCP"],"files":["%ProgramFiles(x86)%\\WinSCP","%AppData%\\WinSCP"],"registry":["HKCU\\Software\\Martin Prikryl\\WinSCP 2","HKLM\\SOFTWARE\\WinSCP"]},
41+
{"name":"PuTTY","aliases":["PuTTY","putty"],"files":["%ProgramFiles%\\PuTTY","%ProgramFiles(x86)%\\PuTTY"],"registry":["HKCU\\Software\\SimonTatham\\PuTTY","HKLM\\SOFTWARE\\PuTTY"]},
42+
{"name":"FileZilla","aliases":["FileZilla"],"files":["%AppData%\\FileZilla","%ProgramFiles%\\FileZilla FTP Client"],"registry":["HKLM\\SOFTWARE\\FileZilla"]},
43+
{"name":"qBittorrent","aliases":["qBittorrent"],"files":["%LocalAppData%\\qBittorrent","%AppData%\\qBittorrent","%ProgramFiles%\\qBittorrent"],"registry":["HKCU\\Software\\qBittorrent"]},
44+
{"name":"Wireshark","aliases":["Wireshark"],"files":["%AppData%\\Wireshark","%ProgramFiles%\\Wireshark"],"registry":["HKLM\\SOFTWARE\\Wireshark"]},
45+
{"name":"VMware Workstation","aliases":["VMware","VMware Workstation"],"files":["%AppData%\\VMware","%ProgramData%\\VMware","%ProgramFiles(x86)%\\VMware"],"registry":["HKCU\\Software\\VMware, Inc.","HKLM\\SOFTWARE\\VMware, Inc."]},
46+
{"name":"VirtualBox","aliases":["VirtualBox","Oracle VirtualBox"],"files":["%UserProfile%\\.VirtualBox","%UserProfile%\\VirtualBox VMs","%ProgramFiles%\\Oracle\\VirtualBox"],"registry":["HKLM\\SOFTWARE\\Oracle\\VirtualBox"]},
47+
{"name":"PowerToys","aliases":["PowerToys","Microsoft PowerToys"],"files":["%LocalAppData%\\Microsoft\\PowerToys","%ProgramFiles%\\PowerToys"],"registry":["HKCU\\Software\\Classes\\powertoys"]},
48+
{"name":"Paint.NET","aliases":["Paint.NET","paintdotnet"],"files":["%LocalAppData%\\paint.net","%ProgramFiles%\\paint.net"],"registry":["HKLM\\SOFTWARE\\paint.net"]},
49+
{"name":"IrfanView","aliases":["IrfanView"],"files":["%AppData%\\IrfanView","%ProgramFiles%\\IrfanView","%ProgramFiles(x86)%\\IrfanView"],"registry":["HKCU\\Software\\IrfanView","HKLM\\SOFTWARE\\IrfanView"]},
50+
{"name":"Blender","aliases":["Blender"],"files":["%AppData%\\Blender Foundation","%ProgramFiles%\\Blender Foundation"],"registry":["HKLM\\SOFTWARE\\BlenderFoundation"]},
51+
{"name":"Docker Desktop","aliases":["Docker","Docker Desktop"],"files":["%AppData%\\Docker","%AppData%\\Docker Desktop","%LocalAppData%\\Docker","%ProgramData%\\Docker","%ProgramFiles%\\Docker"],"registry":["HKCU\\Software\\Docker Inc.","HKLM\\SOFTWARE\\Docker Inc."]}
52+
]

src/DeepPurge.Core/DeepPurge.Core.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
<Using Include="System.Runtime.InteropServices" />
3131
</ItemGroup>
3232

33+
<ItemGroup>
34+
<EmbeddedResource Include="Data\leftover-signatures.json" />
35+
</ItemGroup>
36+
3337
<ItemGroup>
3438
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
3539
<PackageReference Include="System.Management" Version="8.0.0" />

src/DeepPurge.Core/FileSystem/FileLeftoverScanner.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using DeepPurge.Core.Data;
12
using DeepPurge.Core.Models;
23

34
namespace DeepPurge.Core.FileSystem;
@@ -61,6 +62,26 @@ public FileLeftoverScanner(HashSet<string>? customExclusions = null)
6162
public List<LeftoverItem> ScanForLeftovers(InstalledProgram program, ScanMode mode)
6263
{
6364
var leftovers = new List<LeftoverItem>();
65+
66+
var sigMatch = LeftoverSignatureDb.FindMatch(program.DisplayName);
67+
if (sigMatch != null)
68+
{
69+
foreach (var path in sigMatch.FilePaths)
70+
{
71+
if (!Safety.SafetyGuard.IsPathSafeToDelete(path)) continue;
72+
var isDir = Directory.Exists(path);
73+
var size = isDir ? GetDirectorySize(path) : (File.Exists(path) ? new FileInfo(path).Length : 0);
74+
if (size == 0 && !isDir) continue;
75+
leftovers.Add(new LeftoverItem
76+
{
77+
Path = path, DisplayPath = path,
78+
Type = isDir ? LeftoverType.Folder : LeftoverType.File,
79+
Confidence = LeftoverConfidence.Safe, SizeBytes = size,
80+
IsSelected = true, Details = "Known leftover (signature match)"
81+
});
82+
}
83+
}
84+
6485
BuildSearchTerms(program);
6586
BuildCrossReference(program);
6687

src/DeepPurge.Core/Registry/RegistryLeftoverScanner.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using global::Microsoft.Win32;
2+
using DeepPurge.Core.Data;
23
using DeepPurge.Core.Models;
34

45
namespace DeepPurge.Core.Registry;
@@ -52,6 +53,24 @@ public RegistryLeftoverScanner(HashSet<string>? customExclusions = null)
5253
public List<LeftoverItem> ScanForLeftovers(InstalledProgram program, ScanMode mode)
5354
{
5455
var leftovers = new List<LeftoverItem>();
56+
57+
var sigMatch = LeftoverSignatureDb.FindMatch(program.DisplayName);
58+
if (sigMatch != null)
59+
{
60+
foreach (var regPath in sigMatch.RegistryPaths)
61+
{
62+
if (!Safety.SafetyGuard.IsRegistryPathSafeToDelete(regPath)) continue;
63+
if (!RegistryKeyExists(regPath)) continue;
64+
leftovers.Add(new LeftoverItem
65+
{
66+
Path = regPath, DisplayPath = regPath,
67+
Type = LeftoverType.RegistryKey,
68+
Confidence = LeftoverConfidence.Safe,
69+
IsSelected = true, Details = "Known leftover (signature match)"
70+
});
71+
}
72+
}
73+
5574
BuildSearchTerms(program);
5675

5776
// 1. Remove the uninstall registry key itself
@@ -641,6 +660,28 @@ private void SearchKeyRecursive(RegistryKey hive, string path, string searchTerm
641660
catch { }
642661
}
643662

663+
private static bool RegistryKeyExists(string path)
664+
{
665+
try
666+
{
667+
var split = path.IndexOf('\\');
668+
if (split <= 0) return false;
669+
var hive = path[..split].ToUpperInvariant();
670+
var sub = path[(split + 1)..];
671+
RegistryKey? baseKey = hive switch
672+
{
673+
"HKLM" or "HKEY_LOCAL_MACHINE" => global::Microsoft.Win32.Registry.LocalMachine,
674+
"HKCU" or "HKEY_CURRENT_USER" => global::Microsoft.Win32.Registry.CurrentUser,
675+
"HKCR" or "HKEY_CLASSES_ROOT" => global::Microsoft.Win32.Registry.ClassesRoot,
676+
_ => null,
677+
};
678+
if (baseKey == null) return false;
679+
using var key = baseKey.OpenSubKey(sub);
680+
return key != null;
681+
}
682+
catch { return false; }
683+
}
684+
644685
private static string ExtractExeName(string uninstallString)
645686
{
646687
if (string.IsNullOrEmpty(uninstallString)) return "";

0 commit comments

Comments
 (0)