Quarantine Chrome extensions and other directory-shaped findings#27
Merged
Conversation
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.
There was a problem hiding this comment.
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
IsDirectoryrecords. - 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); |
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 byAutomatedResponseServicewith "Quarantine skipped (file missing)" because the gate wasif (!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.cs—QuarantineFilenow detectsDirectory.Exists(source)and routes to a newQuarantineDirectoryhelper:ZipFile.CreateFromDirectory(..., includeBaseDirectory: true). The vault filename gets the.zipsuffix so the entry type is obvious.Directory.Delete(source, recursive: true). If that fails (Chrome holds files open), fall back toDirectory.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 viaMoveFileEx.QuarantineRecordgainsIsDirectory(defaults false; JSON round-trips with old records as files, no migration needed).Restorenow branches onIsDirectory: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.Movepath unchanged.AutomatedResponseService.cs— the gate now accepts both files and directories: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
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..wraith-quarantined-<hex>), Chrome to lose the extension on next launch, and the parked tree to disappear after reboot._restored_<timestamp>suffix if the path is occupied).https://claude.ai/code/session_01LJscMnf6U5HycjwB89m1zU
Generated by Claude Code