Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,6 @@ jobs:
creds: ${{ secrets.AZURE_CREDENTIALS }}
allow-no-subscriptions: true

- name: Fetch latest YARA rules
run: node scripts/fetch-yara-rules.js
env:
KUDU_RULES_URL: ${{ secrets.KUDU_RULES_URL || 'https://cloud.usekudu.com/api/yara-rules' }}

- name: Build, package, and upload
shell: bash
env:
Expand Down
4 changes: 0 additions & 4 deletions electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ extraResources:
to: icons
filter:
- "**/*"
- from: resources/yara-rules
to: yara-rules
filter:
- "**/*.yar"

win:
target:
Expand Down
11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@
"dev": "electron-vite dev",
"build": "electron-vite build",
"preview": "electron-vite preview",
"prefetch:rules": "node scripts/fetch-yara-rules.js",
"package": "npm run prefetch:rules && electron-vite build && electron-builder",
"package:win": "npm run prefetch:rules && electron-vite build && electron-builder --win",
"package:mac": "npm run prefetch:rules && electron-vite build && electron-builder --mac",
"package:linux": "npm run prefetch:rules && electron-vite build && electron-builder --linux",
"package:all": "npm run prefetch:rules && electron-vite build && electron-builder --win --mac --linux",
"package": "electron-vite build && electron-builder",
"package:win": "electron-vite build && electron-builder --win",
"package:mac": "electron-vite build && electron-builder --mac",
"package:linux": "electron-vite build && electron-builder --linux",
"package:all": "electron-vite build && electron-builder --win --mac --linux",
"test": "vitest run",
"test:watch": "vitest",
"validate:rules": "npx tsx src/main/rules/validate.ts",
Expand Down
4 changes: 2 additions & 2 deletions scripts/fetch-yara-rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
* KUDU_RULES_URL=https://... node scripts/fetch-yara-rules.js
*
* If the API is unreachable, the script exits with code 0 and a warning —
* the build will succeed without bundled rules (the app still works via
* the regex fallback and will fetch rules from the cloud at runtime).
* the build will succeed without bundled rules (the app fetches rules
* from the cloud at runtime).
*/

const { writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync } = require('fs')
Expand Down
258 changes: 36 additions & 222 deletions src/main/ipc/malware-scanner.ipc.ts

Large diffs are not rendered by default.

52 changes: 0 additions & 52 deletions src/main/ipc/malware-scanner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,58 +186,6 @@ describe('findSuspiciousStrings', () => {
})
})

// ─── KNOWN_MALWARE_PATTERNS (replica of patterns) ────────────────

const KNOWN_MALWARE_PATTERNS: { pattern: RegExp; name: string; severity: string }[] = [
{ pattern: /xmrig/i, name: 'CoinMiner.XMRig', severity: 'critical' },
{ pattern: /cpuminer/i, name: 'CoinMiner.CPUMiner', severity: 'critical' },
{ pattern: /bonzi\s*buddy/i, name: 'Adware.BonziBuddy', severity: 'medium' },
{ pattern: /darkcomet/i, name: 'RAT.DarkComet', severity: 'critical' },
{ pattern: /njrat/i, name: 'RAT.njRAT', severity: 'critical' },
{ pattern: /emotet/i, name: 'Trojan.Emotet', severity: 'critical' },
{ pattern: /wannacry/i, name: 'Ransom.WannaCry', severity: 'critical' },
{ pattern: /superfish/i, name: 'Adware.Superfish', severity: 'critical' },
{ pattern: /cobalt[\s_-]?strike/i, name: 'HackTool.CobaltStrike', severity: 'critical' },
{ pattern: /shlayer/i, name: 'OSX.Shlayer', severity: 'critical' },
{ pattern: /^svchost\.exe$/i, name: 'Suspicious.FakeSvchost', severity: 'high' },
]

describe('malware pattern matching', () => {
it('detects XMRig miner', () => {
const match = KNOWN_MALWARE_PATTERNS.find((p) => p.pattern.test('xmrig-6.21.0.exe'))
expect(match?.name).toBe('CoinMiner.XMRig')
expect(match?.severity).toBe('critical')
})

it('detects Cobalt Strike beacon', () => {
const match = KNOWN_MALWARE_PATTERNS.find((p) => p.pattern.test('cobalt_strike_beacon.dll'))
expect(match?.name).toBe('HackTool.CobaltStrike')
})

it('detects WannaCry', () => {
const match = KNOWN_MALWARE_PATTERNS.find((p) => p.pattern.test('WannaCry.exe'))
expect(match?.name).toBe('Ransom.WannaCry')
})

it('detects macOS Shlayer', () => {
const match = KNOWN_MALWARE_PATTERNS.find((p) => p.pattern.test('shlayer_install'))
expect(match?.name).toBe('OSX.Shlayer')
})

it('is case-insensitive for pattern matching', () => {
const match = KNOWN_MALWARE_PATTERNS.find((p) => p.pattern.test('XMRIG'))
expect(match?.name).toBe('CoinMiner.XMRig')
})

it('does not match benign filenames', () => {
const benign = ['notepad.exe', 'chrome.dll', 'setup.msi', 'README.md']
for (const name of benign) {
const match = KNOWN_MALWARE_PATTERNS.find((p) => p.pattern.test(name))
expect(match).toBeUndefined()
}
})
})

// ─── SUSPICIOUS_FILENAMES outside system dirs ────────────────────

const SUSPICIOUS_FILENAMES: { pattern: RegExp; name: string }[] = [
Expand Down
48 changes: 0 additions & 48 deletions src/main/services/yara-rules-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,54 +235,6 @@ describe('bundle integrity verification', () => {
})
})

// ─── Rule file precedence (bundled vs cached) ───────────────

describe('rule file precedence', () => {
// Replicate the merging logic from getAllRulePaths
function mergeRulePaths(bundled: string[], cached: string[]): string[] {
if (cached.length === 0) return bundled
if (bundled.length === 0) return cached
const cachedNames = new Set(cached.map(p => {
const parts = p.replace(/\\/g, '/').split('/')
return parts[parts.length - 1]
}))
const merged = bundled.filter(p => {
const parts = p.replace(/\\/g, '/').split('/')
return !cachedNames.has(parts[parts.length - 1])
})
return [...merged, ...cached]
}

it('returns bundled when no cached rules exist', () => {
const bundled = ['/app/resources/miners.yar', '/app/resources/rats.yar']
expect(mergeRulePaths(bundled, [])).toEqual(bundled)
})

it('returns cached when no bundled rules exist', () => {
const cached = ['/data/miners.yar', '/data/rats.yar']
expect(mergeRulePaths([], cached)).toEqual(cached)
})

it('cached rules override bundled with same filename', () => {
const bundled = ['/app/resources/miners.yar', '/app/resources/rats.yar']
const cached = ['/data/miners.yar']
const result = mergeRulePaths(bundled, cached)
expect(result).toContain('/app/resources/rats.yar')
expect(result).toContain('/data/miners.yar')
expect(result).not.toContain('/app/resources/miners.yar')
expect(result).toHaveLength(2)
})

it('includes new cached rules not in bundled', () => {
const bundled = ['/app/resources/miners.yar']
const cached = ['/data/custom.yar']
const result = mergeRulePaths(bundled, cached)
expect(result).toContain('/app/resources/miners.yar')
expect(result).toContain('/data/custom.yar')
expect(result).toHaveLength(2)
})
})

// ─── Metadata validation ─────────────────────────────────────

describe('metadata validation', () => {
Expand Down
33 changes: 6 additions & 27 deletions src/main/services/yara-rules-store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { readFileSync, writeFileSync, renameSync, unlinkSync, rmSync, existsSync, mkdirSync, readdirSync } from 'fs'
import { join, basename } from 'path'
import { join } from 'path'
import { createHash } from 'crypto'
import { app } from 'electron'

Expand Down Expand Up @@ -54,14 +54,6 @@ function getMetadataPath(): string {
return join(getCachedRulesDir(), 'metadata.json')
}

// ─── Bundled rules (fetched at build time, shipped with the installer) ──

function getBundledRulesDir(): string {
return app.isPackaged
? join(process.resourcesPath, 'yara-rules')
: join(__dirname, '../../resources/yara-rules')
}

/** List .yar files in a directory. */
function listYarFiles(dir: string): string[] {
try {
Expand All @@ -75,11 +67,6 @@ function listYarFiles(dir: string): string[] {
}
}

/** Get paths to bundled YARA rule files (shipped with the app). */
export function getBundledRulePaths(): string[] {
return listYarFiles(getBundledRulesDir())
}

// ─── Cached rule files (downloaded from cloud, persisted to disk) ──

/** Get paths to cached YARA rule files. */
Expand All @@ -89,19 +76,10 @@ export function getCachedRulePaths(): string[] {

/**
* Get all YARA rule file paths.
* Cached (cloud-downloaded) rules override bundled ones by filename,
* so cloud updates supersede the version that shipped with the installer.
* Rules are downloaded from the cloud on first launch and cached locally.
*/
export function getAllRulePaths(): string[] {
const bundled = getBundledRulePaths()
const cached = getCachedRulePaths()

if (cached.length === 0) return bundled
if (bundled.length === 0) return cached

const cachedNames = new Set(cached.map(p => basename(p)))
const merged = bundled.filter(p => !cachedNames.has(basename(p)))
return [...merged, ...cached]
return getCachedRulePaths()
}

export function getRulesMetadata(): YaraRulesMetadata | null {
Expand Down Expand Up @@ -309,8 +287,9 @@ export function startPeriodicRuleChecks(
}
}

// Run first check after a short delay (let app finish initializing)
setTimeout(check, 30_000)
// Run first check shortly after launch so rules are available quickly.
// Rules are no longer bundled — they must be downloaded from the cloud.
setTimeout(check, 5_000)
_checkInterval = setInterval(check, intervalMs)
}

Expand Down
Loading
Loading