Skip to content

feat(malware): allowlist false positives so trusted files stop being flagged#193

Merged
dbfx merged 6 commits into
mainfrom
feat/malware-allowlist
May 29, 2026
Merged

feat(malware): allowlist false positives so trusted files stop being flagged#193
dbfx merged 6 commits into
mainfrom
feat/malware-allowlist

Conversation

@dbfx
Copy link
Copy Markdown
Contributor

@dbfx dbfx commented May 29, 2026

What & why

Users had no way to tell Kudu that a malware detection is a false positive. The scanner would re-flag a known-good file (the reporting user's Guild Wars 1 .exe) on every scan, with no override. This adds an allowlist so a trusted file can be marked safe and stops being reported.

How it works

  • Match by content hash (SHA-256). A whitelisted file stays trusted even if moved; if the binary is later changed (e.g. a game patch) it re-flags once and can be re-ignored; malware that replaces the file at the same path is not silently trusted.
  • Single suppression point. All detection engines (YARA signature, heuristics, scripts, LNK, persistence, native AV) funnel into one threats array; the filter runs there, so it covers every source. Only flagged files are hashed (not every scanned file). The scan summary reports how many detections were suppressed — no silent drops.
  • Non-file detections (registry, hosts file, ADS) can't be hashed and are intentionally excluded from the allowlist flow.
  • Persistence mirrors the existing updateRegistryIgnoredTweaks lock-safe pattern: de-dupe by hash, capped at 500 entries.

UI

  • Scan results: a "Not a virus — ignore" action in the expanded detail of each file-based detection.
  • Quarantine: a "Restore & always ignore" bulk action (restore + allowlist in one step).
  • New Allowlist tab: lists entries (file, what it was flagged as, date) with a Remove button to undo.

Tests

  • New persistence tests for the allowlist mutators (add / de-dupe by hash / remove / concurrent-add merge).
  • New replica tests for the suppression decision (file-hash match vs non-file vs unreadable vs empty list) and the renderer path-classification helper.
  • Full suite green (2113 tests); production build clean.

Verify manually

  1. Scan, expand a benign flagged file, click Not a virus — ignore → it disappears.
  2. Re-scan → not reported (suppressed by hash). Allowlist tab → entry listed; Remove → re-scan flags it again.
  3. Modify the file's bytes, re-scan → re-flags (hash changed), proving same-path malware swaps aren't trusted.
  4. Quarantine → Restore & always ignore → file returns and is allowlisted.

dbfx and others added 3 commits May 25, 2026 04:25
After a clean, "x threats quarantined" was plain text and the quarantine
list could not show why a file was flagged (the manifest only stored the
original path and timestamp).

- Dashboard result links straight to the quarantine list and opens the
  Quarantine tab directly via router state.
- Detection metadata (name, severity, source, details) is now threaded
  through quarantine and persisted in the manifest; the list shows a
  severity badge and detection name, with details on hover.

Backward compatible: meta is optional, so the CLI/cloud-agent callers and
pre-existing quarantined entries continue to work (older entries simply
omit the reason).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Detection enums are read back from a JSON manifest on disk and arrive over
IPC, so an unexpected value (corrupt/edited manifest) could be passed to
the renderer and index severityConfig to undefined, crashing the
quarantine view.

- Normalize severity/source to known values at the IPC boundary, both when
  writing the manifest and when listing quarantined files.
- Defensively guard the renderer so an unknown severity omits the badge
  instead of throwing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…flagged

Adds a "Not a virus / ignore" action on scan-result detections and a
"Restore & always ignore" action on quarantined items, plus an Allowlist
tab to view and undo those decisions.

Detections are matched against the allowlist by file content SHA-256, so a
known-good file (e.g. a game .exe) stays trusted even if moved, while a
different binary at the same path is still scanned. Suppression happens at
the single point where all detection sources are aggregated, so it covers
every engine (signature, heuristic, scripts, persistence, native AV). Only
real files can be allowlisted; registry/hosts/ADS detections are excluded.

The allowlist persists in settings via lock-safe add/remove mutators
mirroring updateRegistryIgnoredTweaks (de-dupe by hash, capped at 500).
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6e439a1933

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/main/services/settings-store.ts Outdated
const prev = writeLock
let unlock: () => void
writeLock = new Promise<void>((r) => { unlock = r })
prev.then(() => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Await allowlist writes before reporting success

When a user marks a detection safe (or removes an allowlist entry), this queues the settings write in prev.then(...) but returns before the write has run. The IPC handlers therefore resolve successfully while getMalwareAllowlist() may still read the old config; an immediate rescan after “Not a virus” can flag the same file again, and the Remove button can reload the stale entry right after reporting success. Make these helpers return the queued promise and await it from the IPC handlers before returning.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0a946219be

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1063 to +1065
if (allowed.has(await hashFileSha256(threat.path))) {
suppressed++
continue
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Do not allowlist ADS detections by base-file hash

On Windows, ADS detections are created with path: ads.path for the base file (Heuristic.HiddenADS), so stat(threat.path).isFile() is true and this hashes only the unnamed file stream. If a user has allowlisted that base file (or clicks “Not a virus” on an ADS row), future hidden alternate streams on the same file are suppressed even though the stream contents were never hashed or trusted. Exclude ADS detections from hash suppression or include the stream identity/content in the allowlist key.

Useful? React with 👍 / 👎.

dbfx and others added 2 commits May 29, 2026 17:24
# Conflicts:
#	src/main/ipc/malware-scanner.ipc.ts
#	src/renderer/src/pages/MalwareScannerPage.tsx
#	src/shared/types.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 88443a97f3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (!ok) { failed.push(item.originalFileName); continue }
restored++
// Allowlist the restored file by content hash so it isn't re-flagged.
await window.kudu.malwareIgnore(item.originalPath, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Check whether restore-and-ignore actually allowlisted

In the restore-and-ignore flow, malwareIgnore can return null when the restored target is not allowlistable (for example ADS metadata, a path outside the current scan dirs, or a stat/hash failure), but this return value is ignored and the code still increments restored and shows “restored and added to the allowlist.” In those cases the file is restored but not trusted, so the next scan can flag it again while the UI told the user it was permanently ignored; treat a null result as a partial failure or at least avoid the allowlist success message for that item.

Useful? React with 👍 / 👎.

@dbfx dbfx merged commit c752129 into main May 29, 2026
4 checks passed
@dbfx dbfx deleted the feat/malware-allowlist branch May 29, 2026 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant