-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
File scanning connector to handle multiple AV tools #1514
File scanning connector to handle multiple AV tools #1514
Conversation
backend/src/services/file.ts
Outdated
avScan: { state: ScanState.Error }, | ||
}) | ||
if (config.ui.avScanning.enabled) { | ||
const fileScanConnectors = getFileScanningConnectors() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good if this new connector type could have the same interface as other connectors and this could just be scanners.scan(file)
. Ideally we want to encapsulate the initialisation and the calling of each individual scanner inside the connector logic, not here in the service layer every time we want to use the scanners
} | ||
|
||
async init() { | ||
if (!av) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this check is necessary because the init()
function is only called to initialise the av scanner
try { | ||
av = await new NodeClam().init({ clamdscan: config.avScanning.clamdscan }) | ||
} catch (error) { | ||
throw ConfigurationError('Cannot retrieve Clam AV config', { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you tested what happens if the Clam AV service isn't running? I assume this throw would be called. If so, the error message needs updating
} | ||
|
||
async scan(file: FileInterfaceDoc) { | ||
const avStream = av.passthrough() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit of a pain but to be safe, you should probably check that av
has been initialised at the start of functions like this when using the AV scanner and throw an error recommending that init()
should be called first if av
has not been assigned.
if (!file.avScan.find((result) => result.toolName === toolName)) { | ||
const updatedAvScanArray = [...file.avScan, { toolName: toolName, state: ScanState.InProgress }] | ||
file.avScan = updatedAvScanArray | ||
await FileModel.updateOne( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my opinion, these scanners should not be responsible for storing the scan information in the DB, they should just be an interface with a particular scanning and this method should take in a file stream and return a scan result object. Ther service layer should be responsible for getting the file stream and processing the scan results.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit tricky as it's async and the service that calls these connectors already returns the response to the user. We'd need to call the scanners, wait for the results, store them, then return a 200 response to the user from the backend.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just because we want a process to run asynchronously doesn't mean all the implementation needs to be in a single method. Use a .then()
on the asynchronous call to provide the implementation for how we will process the results once the scanners are complete.
isInfected: { type: Boolean }, | ||
viruses: [{ type: String }], | ||
}, | ||
avScan: [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't forget, this will require a migration. I don't think a migration was done when this field was originally added so this migration will have to account for when this field doesn't exist
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it does but feel free to double check. From testing it looks like it will return single results as arrays anyway
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From looking at this diff it looks like the previous type of avScan
was an object and now avScan
is an array? That's why I thought this would require a migration
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It becomes an array of the same type of object as it was before and it seems to handle it automatically thankfully it seems
…d point for checkig what filescanners are active
… check for info insead of ui config for scanner list
backend/config/default.cjs
Outdated
@@ -205,6 +204,10 @@ module.exports = { | |||
audit: { | |||
kind: 'silly', | |||
}, | |||
|
|||
fileScanners: { | |||
kinds: ['clamAV'], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By default, we shouldn't have any scanners configured. This instead should be done in the docker_compose.cjs
}) | ||
} | ||
}) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would recommend call init()
on all the connectors in fileScanConnectors
here or in each case where you add a new connector to the list. This way, you can pick up on configuration errors early on
} | ||
}) | ||
|
||
const wrapper = new FileScanningWrapper(fileScanConnectors) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the other index.ts
files for the other connectors we also have a caching mechanism we'll probably also want here. Something that checks if wrapper
has already been assigned and returns it if it has
} | ||
|
||
async scan(file: FileInterfaceDoc): Promise<FileScanResult[]> { | ||
await this.init() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment on index.ts
about moving this to there
backend/src/services/file.ts
Outdated
avScan: { state: ScanState.Error }, | ||
}) | ||
}) | ||
if (runFileScanners().info()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't a major thing but could we get this to look more like the other connectors eg: const auth = await authorisation.file(user, model, file, FileAction.Upload)
so this would become if (scanners.info()) {
export const getFilescanningInfo = [ | ||
bodyParser.json(), | ||
async (req: Request, res: Response<GetFileScanningInfoResponse>) => { | ||
return res.json({ scanners: runFileScanners().info() }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment on file.ts
to make this be return res.json({ scanners: scanners.info() })
} | ||
|
||
return file | ||
} | ||
|
||
export async function updateFileWithResults(file: FileInterface, results: FileScanResult[]) { | ||
for (const result of results) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be interested to look to see if Mongo can handle this logic for you and add new elements to the array if a an element with a tool doesn't exist and update the existing element if a element with that tool does exist
backend/src/services/file.ts
Outdated
await FileModel.updateOne( | ||
{ _id: file._id }, | ||
{ | ||
$set: { avScan: updatedAvScanArray }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here you should use $push
and then you won't need to have updatedAvScanArray
} else if ( | ||
file.avScan.some( | ||
(scanResult) => | ||
scanResult.state === ScanState.NotScanned || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this not be scanResult.state !== ScanState.Complete
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When setting the scanners connectors to an empty array, the information still appears in the UI?
@@ -27,16 +34,16 @@ vi.mock('../../src/connectors/authorisation/index.js', async () => ({ | |||
default: authMock, | |||
})) | |||
|
|||
// const fileScanConnectors: BaseFileScanningConnector[] = [new ClamAvFileScanningConnector()] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?
@@ -193,6 +192,10 @@ data: | |||
audit: { | |||
kind: '{{ .Values.connectors.audit.kind }}', | |||
}, | |||
|
|||
fileScanners: { | |||
kinds: '{{ .Values.connectors.fileScanners.kinds }}', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be an array?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just a reference though isn't it? I don't think we define if it's an array or not here
No description provided.