Add AD detection on desktop app #264, #304

This commit addresses issues #264 and #304, where users were not
receiving error messages when script execution failed due to
antivirus intervention, particularly with Microsoft Defender.
Now, desktop app users will see a detailed error message with
guidance on next steps if script saving or execution fails due
to antivirus removal.

Key changes:

- Implement a check to detect failure in file writing,
  including reading the written file back. This method effectively
  detects antivirus interventions, as the read operation triggers
  an antivirus scan, leading to file deletion by the antivirus.
- Introduce a specific error message for scenarios where an
  antivirus intervention is detected.
This commit is contained in:
undergroundwires
2024-01-16 22:26:28 +01:00
parent 756c736e21
commit f03fc24098
23 changed files with 841 additions and 142 deletions

View File

@@ -0,0 +1,120 @@
import { writeFile, access, readFile } from 'node:fs/promises';
import { constants } from 'node:fs';
import { Logger } from '@/application/Common/Log/Logger';
import { ElectronLogger } from '../Log/ElectronLogger';
import {
FailedFileWrite, ReadbackFileWriter, FileWriteErrorType,
FileWriteOutcome, SuccessfulFileWrite,
} from './ReadbackFileWriter';
const FILE_ENCODING: NodeJS.BufferEncoding = 'utf-8';
export class NodeReadbackFileWriter implements ReadbackFileWriter {
constructor(
private readonly logger: Logger = ElectronLogger,
private readonly fileSystem: FileReadWriteOperations = {
writeFile,
readFile: (path, encoding) => readFile(path, encoding),
access,
},
) { }
public async writeAndVerifyFile(
filePath: string,
fileContents: string,
): Promise<FileWriteOutcome> {
this.logger.info(`Starting file write and verification process for: ${filePath}`);
const fileWritePipelineActions: ReadonlyArray<() => Promise<FileWriteOutcome>> = [
() => this.createOrOverwriteFile(filePath, fileContents),
() => this.verifyFileExistsWithoutReading(filePath),
/*
Reading the file contents back, we can detect if the file has been altered or
removed post-creation. Removal of scripts when reading back is seen by some antivirus
software when it falsely identifies a script as harmful.
*/
() => this.verifyFileContentsByReading(filePath, fileContents),
];
for (const action of fileWritePipelineActions) {
const actionOutcome = await action(); // eslint-disable-line no-await-in-loop
if (!actionOutcome.success) {
return actionOutcome;
}
}
return this.reportSuccess(`File successfully written and verified: ${filePath}`);
}
private async createOrOverwriteFile(
filePath: string,
fileContents: string,
): Promise<FileWriteOutcome> {
try {
this.logger.info(`Creating file at ${filePath}, size: ${fileContents.length} characters`);
await this.fileSystem.writeFile(filePath, fileContents, FILE_ENCODING);
return this.reportSuccess('Created file.');
} catch (error) {
return this.reportFailure('WriteOperationFailed', error);
}
}
private async verifyFileExistsWithoutReading(
filePath: string,
): Promise<FileWriteOutcome> {
try {
await this.fileSystem.access(filePath, constants.F_OK);
return this.reportSuccess('Verified file existence without reading.');
} catch (error) {
return this.reportFailure('FileExistenceVerificationFailed', error);
}
}
private async verifyFileContentsByReading(
filePath: string,
expectedFileContents: string,
): Promise<FileWriteOutcome> {
try {
const actualFileContents = await this.fileSystem.readFile(filePath, FILE_ENCODING);
if (actualFileContents !== expectedFileContents) {
return this.reportFailure(
'ContentVerificationFailed',
[
'The contents of the written file do not match the expected contents.',
'Written file contents do not match the expected file contents',
`File path: ${filePath}`,
`Expected total characters: ${actualFileContents.length}`,
`Actual total characters: ${expectedFileContents.length}`,
].join('\n'),
);
}
return this.reportSuccess('Verified file content by reading.');
} catch (error) {
return this.reportFailure('ReadVerificationFailed', error);
}
}
private reportFailure(
errorType: FileWriteErrorType,
error: Error | string,
): FailedFileWrite {
this.logger.error('Error saving file', errorType, error);
return {
success: false,
error: {
type: errorType,
message: typeof error === 'string' ? error : error.message,
},
};
}
private reportSuccess(successAction: string): SuccessfulFileWrite {
this.logger.info(`Successful file save: ${successAction}`);
return {
success: true,
};
}
}
export interface FileReadWriteOperations {
readonly writeFile: typeof writeFile;
readonly access: typeof access;
readFile: (filePath: string, encoding: NodeJS.BufferEncoding) => Promise<string>;
}