Skip to content

Quarantine Chrome extensions and other directory-shaped findings#27

Merged
OpenSource-For-Freedom merged 1 commit into
mainfrom
patch
May 30, 2026
Merged

Quarantine Chrome extensions and other directory-shaped findings#27
OpenSource-For-Freedom merged 1 commit into
mainfrom
patch

Conversation

@OpenSource-For-Freedom
Copy link
Copy Markdown
Owner

Summary

Closes the directory-shaped-finding gap reported during real-world testing: Chrome/Edge extensions detected by scanner/browser_scanner.py (path = the extension's directory) were silently skipped by AutomatedResponseService with "Quarantine skipped (file missing)" because the gate was if (!File.Exists(containmentPath)). The malicious extension stayed installed; the scan report just had a one-liner the user had to know to look for.

What changed

QuarantineService.csQuarantineFile now detects Directory.Exists(source) and routes to a new QuarantineDirectory helper:

  • Zip the tree into the vault with ZipFile.CreateFromDirectory(..., includeBaseDirectory: true). The vault filename gets the .zip suffix so the entry type is obvious.
  • Try Directory.Delete(source, recursive: true). If that fails (Chrome holds files open), fall back to Directory.Move(source, <source>.wraith-quarantined-<8hex>) to neutralise the extension path — Chrome looks up extensions by their directory path on disk, so renaming the root is enough to break discovery on the next scan. Each remaining file inside the parked tree is scheduled for delete-on-reboot via MoveFileEx.
  • If even the rename fails, roll back the vault copy and throw a clear "close the holding process" message so the caller knows containment was partial.

QuarantineRecord gains IsDirectory (defaults false; JSON round-trips with old records as files, no migration needed).

Restore now branches on IsDirectory:

  • Directory entries are extracted with ZipFile.ExtractToDirectory(quarantinedPath, targetDir) back to the original parent. If the original path is now occupied, the restored leaf is renamed to a _restored_<timestamp> sibling. The vault zip is deleted after a successful extract.
  • File entries use the existing File.Move path unchanged.

AutomatedResponseService.cs — the gate now accepts both files and directories:

if (!File.Exists(containmentPath) && !Directory.Exists(containmentPath))
    report.Messages.Add($"Quarantine skipped (path not on disk): {containmentPath}");

Renamed the message from "file missing" → "path not on disk" so registry-shaped paths (HKLM\…, HKCU\…) skip with an accurate diagnostic. Containing registry findings is a different problem (registry value removal, not the file vault) and remains out of scope here.

QuarantineWindow.xaml.cs — manual import accepts dropped folders, not just files.

Test plan

  • On a test machine with Chrome installed and at least one high-permission extension, run a scan with AutoQuarantineFile = true. Expect the extension's directory to land in the vault as a .zip, and Chrome to no longer list the extension after restart.
  • On the same machine with Chrome running, quarantine an in-use extension. Expect the directory to be renamed (.wraith-quarantined-<hex>), Chrome to lose the extension on next launch, and the parked tree to disappear after reboot.
  • Restore the quarantined extension from the vault UI. Expect the directory to reappear at the original path (or with a _restored_<timestamp> suffix if the path is occupied).
  • Delete the quarantined extension from the vault. Expect the vault zip to disappear and the index to mark it Deleted.
  • In the Quarantine window, drag-and-drop a folder onto the drop zone. Expect a zipped record to appear.

https://claude.ai/code/session_01LJscMnf6U5HycjwB89m1zU


Generated by Claude Code

scanner/browser_scanner.py emits Chrome/Edge extension findings with
path = ext_id_dir — the extension's directory under
%LocalAppData%\Google\Chrome\User Data\<profile>\Extensions\<id>\.
AutomatedResponseService gated on File.Exists, so these findings
silently skipped with 'Quarantine skipped (file missing)' and the
malicious extension stayed installed.

QuarantineService now handles directory sources alongside files:

- QuarantineFile detects Directory.Exists and routes to a new
  QuarantineDirectory helper. The directory is zipped into the vault
  with the .zip suffix on the vault filename, then the source is
  removed via recursive delete.
- When the source can't be recursively deleted (Chrome still has the
  extension files open), fall back to Directory.Move into a parked
  name (<source>.wraith-quarantined-<8-hex>) and schedule each
  remaining file for delete-on-reboot. The rename alone is enough to
  break the extension on Chrome's next scan — Chrome looks up by
  path, and the path is gone.
- If even the rename fails, the vault copy is rolled back and a
  clear error tells the user to close the holding process.
- QuarantineRecord gains an IsDirectory flag so the JSON round-trip
  preserves the entry type.
- Restore extracts the zip back to OriginalPath (or a _restored_-
  suffixed sibling if the path is now occupied). DeleteFromVault
  is unchanged — the zip is just another file in the vault.
- AutomatedResponseService gate now allows Directory.Exists, with
  a clearer skip message for registry-path findings (HKLM\..., etc.)
  which the vault legitimately can't contain.
- QuarantineWindow.ImportFiles also accepts dropped folders.
Copilot AI review requested due to automatic review settings May 30, 2026 01:56
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR extends quarantine handling from single files to directory-shaped findings, especially browser extension directories, so automated and manual quarantine can contain them through zipped vault entries and restore them later.

Changes:

  • Adds directory quarantine support via zip archive creation, delete/rename fallback, and IsDirectory records.
  • Updates restore logic to extract directory records and keep existing file restore behavior.
  • Allows automated response and drag/drop import paths to accept directories as quarantine candidates.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
WRAITH/Services/QuarantineService.cs Adds directory vault records, directory zip quarantine, delete/rename fallback, and directory restore handling.
WRAITH/Services/AutomatedResponseService.cs Allows containment paths that are directories and updates skip diagnostics for non-disk paths.
WRAITH/QuarantineWindow.xaml.cs Allows manual import/drop handling to pass directories to quarantine.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +245 to +255
// ExtractToDirectory recreates the included base folder inside targetDir,
// so we extract into the parent and let the zip place the leaf folder.
ZipFile.ExtractToDirectory(quarantinedPath, targetDir);

// The above places the leaf at originalPath. If we need the
// _restored_ suffix because originalPath already existed,
// rename the extracted leaf into place.
if (!string.Equals(target, originalPath, StringComparison.OrdinalIgnoreCase)
&& Directory.Exists(originalPath))
{
Directory.Move(originalPath, target);
Comment on lines +180 to +201
try
{
Directory.Move(sourceDir, parked);
// Schedule the parked tree for delete-on-reboot, file by file.
// Best-effort: if any file can't be scheduled, leave it — the
// rename alone has already broken the extension's discovery path.
foreach (var f in Directory.EnumerateFiles(parked, "*", SearchOption.AllDirectories))
{
if (!TryDeleteFile(f))
MoveFileEx(f, null, MOVEFILE_DELAY_UNTIL_REBOOT);
}
}
catch
{
// Rename failed too — the vault still has the zip, but the
// original directory is intact and the extension may still load.
// Surface a clear error so the caller knows containment is partial.
TryDeleteFile(destZip);
throw new IOException(
$"Directory '{sourceDir}' is in use and could not be moved or renamed. " +
"Close the process holding it (e.g. quit Chrome) and try again.");
}
Comment on lines +98 to +100
if (isDirectory)
{
File.Move(sourcePath, dest);
QuarantineDirectory(sourcePath, dest);
@OpenSource-For-Freedom OpenSource-For-Freedom merged commit 8e09e4c into main May 30, 2026
9 checks passed
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