Skip to content

Commit ac76ec2

Browse files
authored
Feat/allow switching between windows defender and clamav (#927)
1 parent 06bf015 commit ac76ec2

14 files changed

Lines changed: 131 additions & 112 deletions

File tree

src/apps/main/antivirus/ManualSystemScan.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { ScannedItem } from '../database/entities/ScannedItem';
33
import { getUserSystemPath } from '../device/service';
44
import { queue, QueueObject } from 'async';
55
import eventBus from '../event-bus';
6-
import { AntivirusClamAV } from './antivirus-clam-av';
6+
import { AntivirusManager } from './antivirus-manager/antivirus-manager';
7+
import { AntivirusEngine } from './antivirus-manager/types';
78
import { transformItem } from './utils/transformItem';
89
import { isPermissionError } from './utils/isPermissionError';
910
import { DBScannerConnection } from './utils/dbConections';
@@ -39,8 +40,7 @@ class ManualSystemScan {
3940
private cancelled = false;
4041
private scanSessionId = 0;
4142

42-
private antivirus: AntivirusClamAV | null;
43-
43+
private antivirus: AntivirusEngine | null;
4444
constructor() {
4545
this.progressEvents = [];
4646
this.manualQueue = null;
@@ -131,7 +131,8 @@ class ManualSystemScan {
131131
const currentSession = this.scanSessionId;
132132

133133
if (!this.antivirus) {
134-
this.antivirus = await AntivirusClamAV.createInstance();
134+
const antivirusManager = await AntivirusManager.getInstance();
135+
this.antivirus = await antivirusManager.getActiveEngine();
135136
}
136137
const antivirus = this.antivirus;
137138

@@ -155,7 +156,7 @@ class ManualSystemScan {
155156
return this.handlePreviousScannedItem(currentSession, scannedItem, previousScannedItem);
156157
}
157158

158-
const currentScannedFile = await antivirus.scanFile(scannedItem.pathName);
159+
const currentScannedFile = await antivirus?.scanFile({ filePath: scannedItem.pathName });
159160

160161
if (currentScannedFile) {
161162
await this.dbConnection.addItemToDatabase({

src/apps/main/antivirus/antivirus-clam-av.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import NodeClam from '@internxt/scan';
33
import * as clamAVServer from './ClamAVDaemon';
44
import { app } from 'electron';
55
import { cwd } from 'process';
6+
import { logger } from '@/apps/shared/logger/logger';
67

7-
export interface SelectedItemToScanProps {
8+
export type SelectedItemToScanProps = {
89
path: string;
910
itemName: string;
1011
isDirectory: boolean;
11-
}
12+
};
1213

1314
const RESOURCES_PATH = app.isPackaged ? path.join(process.resourcesPath, 'clamAV') : path.join(cwd(), 'clamAV');
1415

@@ -45,12 +46,15 @@ export class AntivirusClamAV {
4546

4647
this.isInitialized = true;
4748
} catch (error) {
48-
console.error('Error Initializing ClamAV:', error);
49-
throw error;
49+
throw logger.error({
50+
tag: 'ANTIVIRUS',
51+
msg: 'Error initializing ClamAV',
52+
exc: error,
53+
});
5054
}
5155
}
5256

53-
async scanFile(filePath: string) {
57+
async scanFile({ filePath }: { filePath: string }) {
5458
if (!this.clamAv || !this.isInitialized) {
5559
throw new Error('ClamAV is not initialized');
5660
}

src/apps/main/antivirus/antivirus-manager/select-antivirus-engine.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { selectAntivirusEngine } from './select-antivirus-engine';
22
import { partialSpyOn } from 'tests/vitest/utils.helper.test';
33
import { loggerMock } from '@/tests/vitest/mocks.helper.test';
4-
import * as ipcMainAntivirus from '@/apps/main/ipcs/ipcMainAntivirus';
4+
import * as isDefenderAvailable from '../windows-defender/is-windows-defender-available';
55
import * as initializeAntivirusModule from '../utils/initializeAntivirus';
66
import * as clamAVDaemon from '../ClamAVDaemon';
77

88
describe('selectAntivirusEngine', () => {
9-
const isWindowsDefenderActiveMock = partialSpyOn(ipcMainAntivirus, 'isWindowsDefenderRealTimeProtectionActive');
9+
const isWindowsDefenderActiveMock = partialSpyOn(isDefenderAvailable, 'isWindowsDefenderAvailable');
1010
const checkClamdAvailabilityMock = partialSpyOn(clamAVDaemon, 'checkClamdAvailability');
1111
const initializeClamAVMock = partialSpyOn(initializeAntivirusModule, 'initializeClamAV');
1212

src/apps/main/antivirus/antivirus-manager/select-antivirus-engine.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isWindowsDefenderRealTimeProtectionActive } from '../../ipcs/ipcMainAntivirus';
1+
import { isWindowsDefenderAvailable } from '../windows-defender/is-windows-defender-available';
22
import { initializeClamAV } from '../utils/initializeAntivirus';
33
import { checkClamdAvailability } from '../ClamAVDaemon';
44
import { logger } from '@/apps/shared/logger/logger';
@@ -9,7 +9,7 @@ export async function selectAntivirusEngine() {
99
msg: 'Selecting antivirus engine...',
1010
});
1111

12-
if (await isWindowsDefenderRealTimeProtectionActive()) {
12+
if (await isWindowsDefenderAvailable()) {
1313
logger.debug({
1414
tag: 'ANTIVIRUS',
1515
msg: 'Default antivirus selected as engine',
@@ -18,15 +18,21 @@ export async function selectAntivirusEngine() {
1818
}
1919

2020
const clamavAvailable = await checkClamdAvailability();
21-
if (!clamavAvailable) {
22-
const { antivirusEnabled } = await initializeClamAV();
23-
if (antivirusEnabled) {
24-
logger.debug({
25-
tag: 'ANTIVIRUS',
26-
msg: 'ClamAV selected as fallback antivirus',
27-
});
28-
return 'clamav';
29-
}
21+
if (clamavAvailable) {
22+
logger.debug({
23+
tag: 'ANTIVIRUS',
24+
msg: 'ClamAV is already available, selected as engine',
25+
});
26+
return 'clamav';
27+
}
28+
29+
const { antivirusEnabled } = await initializeClamAV();
30+
if (antivirusEnabled) {
31+
logger.debug({
32+
tag: 'ANTIVIRUS',
33+
msg: 'ClamAV initialized, selected as engine',
34+
});
35+
return 'clamav';
3036
}
3137

3238
logger.warn({

src/apps/main/antivirus/scan-file.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { logger } from '@/apps/shared/logger/logger';
2-
import { AntivirusClamAV } from './antivirus-clam-av';
32
import { DBScannerConnection } from './utils/dbConections';
43
import { transformItem } from './utils/transformItem';
4+
import { AntivirusEngine } from './antivirus-manager/types';
55

66
type TProps = {
77
filePath: string;
88
database: DBScannerConnection;
9-
antivirus: AntivirusClamAV;
9+
antivirus: AntivirusEngine;
1010
};
1111

1212
export const scanFile = async ({ filePath, database, antivirus }: TProps) => {
@@ -18,7 +18,7 @@ export const scanFile = async ({ filePath, database, antivirus }: TProps) => {
1818
if (scannedItem.updatedAtW === previousScannedItem.updatedAtW) return;
1919
if (scannedItem.hash === previousScannedItem.hash) return;
2020

21-
const currentScannedFile = await antivirus.scanFile(scannedItem.pathName);
21+
const currentScannedFile = await antivirus.scanFile({ filePath: scannedItem.pathName });
2222
if (currentScannedFile) {
2323
await database.updateItemToDatabase(previousScannedItem.id, {
2424
...scannedItem,
@@ -28,7 +28,7 @@ export const scanFile = async ({ filePath, database, antivirus }: TProps) => {
2828
return;
2929
}
3030

31-
const currentScannedFile = await antivirus.scanFile(scannedItem.pathName);
31+
const currentScannedFile = await antivirus.scanFile({ filePath: scannedItem.pathName });
3232
if (currentScannedFile) {
3333
await database.addItemToDatabase({
3434
...scannedItem,

src/apps/main/antivirus/scanCronJob.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getUserSystemPath } from '../device/service';
2-
import { AntivirusClamAV } from './antivirus-clam-av';
2+
import { AntivirusManager } from './antivirus-manager/antivirus-manager';
33
import { queue } from 'async';
44
import { DBScannerConnection } from './utils/dbConections';
55
import { ScannedItemCollection } from '../database/collections/ScannedItemCollection';
@@ -15,17 +15,25 @@ let dailyScanInterval: NodeJS.Timeout | null = null;
1515

1616
export function scheduleDailyScan() {
1717
async function startBackgroundScan() {
18-
console.log('Starting user system scan (BACKGROUND)...');
18+
logger.debug({ tag: 'ANTIVIRUS', msg: 'Starting user system scan (BACKGROUND)' });
1919
await scanInBackground();
2020
}
2121

2222
startBackgroundScan().catch((err) => {
23-
console.error('Error in initial background scan:', err);
23+
logger.error({
24+
tag: 'ANTIVIRUS',
25+
msg: 'Error in initial background scan',
26+
exc: err,
27+
});
2428
});
2529

2630
dailyScanInterval = setInterval(() => {
2731
startBackgroundScan().catch((err) => {
28-
console.error('Error in scheduled background scan:', err);
32+
logger.error({
33+
tag: 'ANTIVIRUS',
34+
msg: 'Error in scheduled background scan',
35+
exc: err,
36+
});
2937
});
3038
}, ONE_DAY_MS);
3139
}
@@ -40,7 +48,13 @@ export function clearDailyScan() {
4048
const scanInBackground = async (): Promise<void> => {
4149
const hashedFilesAdapter = new ScannedItemCollection();
4250
const database = new DBScannerConnection(hashedFilesAdapter);
43-
const antivirus = await AntivirusClamAV.createInstance();
51+
const antivirusManager = AntivirusManager.getInstance();
52+
const antivirus = await antivirusManager.getActiveEngine();
53+
54+
if (!antivirus) {
55+
logger.error({ tag: 'ANTIVIRUS', msg: 'No active antivirus engine found' });
56+
return;
57+
}
4458

4559
const userSystemPath = await getUserSystemPath();
4660
if (!userSystemPath) return;

src/apps/main/antivirus/utils/initializeAntivirus.ts

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { buildPaymentsService } from '../../payments/builder';
22
import { PaymentsService } from '../../payments/service';
33
import * as clamAVServer from '../ClamAVDaemon';
4+
import { isWindowsDefenderAvailable } from '../windows-defender/is-windows-defender-available';
45
import { clearDailyScan, scheduleDailyScan } from '../scanCronJob';
56
import { logger } from '@/apps/shared/logger/logger';
67

@@ -9,54 +10,43 @@ let isClamAVRunning = false;
910
let clamAVInitializationPromise: Promise<{ antivirusEnabled: boolean }> | null = null;
1011

1112
export async function initializeAntivirusIfAvailable() {
12-
isClamAVRunning = await clamAVServer.checkClamdAvailability();
13-
14-
if (isClamAVRunning) {
15-
return { antivirusEnabled: true };
16-
}
17-
18-
if (clamAVInitializationPromise) {
19-
return clamAVInitializationPromise;
20-
}
21-
22-
clamAVInitializationPromise = initializeClamAV();
23-
return clamAVInitializationPromise;
24-
}
25-
26-
export async function initializeClamAV() {
2713
paymentService = buildPaymentsService();
2814

2915
try {
3016
const availableProducts = await paymentService.getAvailableProducts();
3117
const isAntivirusEnabled = availableProducts.antivirus;
3218

33-
if (isAntivirusEnabled) {
34-
await clamAVServer.startClamdServer();
35-
await clamAVServer.waitForClamd();
36-
19+
if (!isAntivirusEnabled) {
3720
logger.debug({
3821
tag: 'ANTIVIRUS',
39-
msg: 'ClamAV is running. Scheduling daily scan.',
22+
msg: 'Antivirus not enabled for this user. Clearing any running ClamAV instance.',
4023
});
4124

42-
scheduleDailyScan();
25+
clearAntivirus();
4326

44-
isClamAVRunning = true;
45-
clamAVInitializationPromise = null;
27+
return { antivirusEnabled: false };
28+
}
4629

47-
return { antivirusEnabled: true };
48-
} else {
30+
const isWindowsDefenderActive = await isWindowsDefenderAvailable();
31+
if (isWindowsDefenderActive) {
4932
logger.debug({
5033
tag: 'ANTIVIRUS',
51-
msg: 'Antivirus not enabled for this user. Clearing any running ClamAV instance.',
34+
msg: 'Using default antivirus.',
5235
});
36+
return { antivirusEnabled: true };
37+
}
5338

54-
clamAVInitializationPromise = null;
55-
56-
clearAntivirus();
39+
isClamAVRunning = await clamAVServer.checkClamdAvailability();
40+
if (isClamAVRunning) {
41+
return { antivirusEnabled: true };
42+
}
5743

58-
return { antivirusEnabled: false };
44+
if (clamAVInitializationPromise) {
45+
return clamAVInitializationPromise;
5946
}
47+
48+
clamAVInitializationPromise = initializeClamAV();
49+
return clamAVInitializationPromise;
6050
} catch (error) {
6151
logger.warn({
6252
tag: 'ANTIVIRUS',
@@ -71,6 +61,36 @@ export async function initializeClamAV() {
7161
}
7262
}
7363

64+
export async function initializeClamAV() {
65+
try {
66+
await clamAVServer.startClamdServer();
67+
await clamAVServer.waitForClamd();
68+
69+
logger.debug({
70+
tag: 'ANTIVIRUS',
71+
msg: 'ClamAV is running. Scheduling daily scan.',
72+
});
73+
74+
scheduleDailyScan();
75+
76+
isClamAVRunning = true;
77+
clamAVInitializationPromise = null;
78+
79+
return { antivirusEnabled: true };
80+
} catch (error) {
81+
logger.warn({
82+
tag: 'ANTIVIRUS',
83+
msg: 'Error initializing ClamAV.',
84+
exc: error,
85+
});
86+
87+
clamAVInitializationPromise = null;
88+
clearAntivirus();
89+
90+
return { antivirusEnabled: false };
91+
}
92+
}
93+
7494
export function clearAntivirus() {
7595
if (isClamAVRunning) {
7696
clearDailyScan();
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { logger } from '@/apps/shared/logger/logger';
2+
import { exec } from 'child_process';
3+
import { promisify } from 'util';
4+
5+
const execPromise = promisify(exec);
6+
export async function isWindowsDefenderAvailable(): Promise<boolean> {
7+
try {
8+
const { stdout } = await execPromise('powershell "Get-MpComputerStatus | Select-Object -Property AMServiceEnabled"');
9+
return stdout.includes('True');
10+
} catch (error) {
11+
logger.error({
12+
tag: 'ANTIVIRUS',
13+
msg: 'Error checking Windows Defender status.',
14+
exc: error,
15+
});
16+
return false;
17+
}
18+
}

src/apps/main/main.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import configStore from './config';
4949
import { getTray, setTrayStatus, setupTrayIcon } from './tray/tray';
5050
import { openOnboardingWindow } from './windows/onboarding';
5151
import { Theme } from '../shared/types/Theme';
52-
import { clearAntivirus, initializeAntivirusIfAvailable } from './antivirus/utils/initializeAntivirus';
52+
import { clearAntivirus } from './antivirus/utils/initializeAntivirus';
5353
import { registerUsageHandlers } from './usage/handlers';
5454
import { setupQuitHandlers } from './quit';
5555
import { clearConfig, setDefaultConfig } from '../sync-engine/config';
@@ -156,8 +156,6 @@ eventBus.on('USER_LOGGED_IN', async () => {
156156
} else if (widget) {
157157
widget.show();
158158
}
159-
160-
await initializeAntivirusIfAvailable();
161159
} catch (error) {
162160
Logger.error(error);
163161
reportError(error as Error);

0 commit comments

Comments
 (0)