Thanks to visit codestin.com
Credit goes to Github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
5ddd3f6
Some unfinished changes to basic typing
IR96334 Jan 16, 2026
e205d23
More scanner generalisation and some small changes/fixes to fileScanning
IR96334 Jan 20, 2026
d67ed54
More file scanning alterations
IR96334 Jan 22, 2026
f9a7edf
Mass renaing in file scanning files, fixed tests, edited config, edit…
IR96334 Jan 22, 2026
5a93031
More extensive config alterations
IR96334 Jan 22, 2026
0d21bba
Fixed old property names and renamed directory from fileScanning to a…
IR96334 Jan 22, 2026
c8d2004
fixed more tests
IR96334 Jan 22, 2026
9c292ca
edited doc file
IR96334 Jan 22, 2026
7cf5b04
Fixed mocking
IR96334 Jan 22, 2026
0104eb0
temporary fix to wrapper.startScans
IR96334 Jan 23, 2026
ae92311
Added vulnerability table functionality for generalisation
IR96334 Jan 26, 2026
0f95d09
removed commented out code
IR96334 Jan 27, 2026
bf9b353
fixed filescanobject bug
IR96334 Jan 27, 2026
1bee2a7
straggling av and virus references
IR96334 Jan 27, 2026
36f59bc
fixed tests
IR96334 Jan 27, 2026
8ab65d1
Removed the now unnecessary isVulnerable
IR96334 Jan 27, 2026
fe8c0b9
Fixed scan chips to update properly, removed repeated functionality, …
IR96334 Jan 29, 2026
5deaccb
Migration script, scan result type changes, and coincidental frontend…
IR96334 Feb 3, 2026
7b8d166
Added reminder comment
IR96334 Feb 3, 2026
c15db7c
more name refactoring
IR96334 Feb 4, 2026
c5d306d
Test fixes
IR96334 Feb 5, 2026
2359eed
Addressed PR comments
IR96334 Feb 5, 2026
a58f7da
solved merge conflicts
IR96334 Feb 5, 2026
41c3007
Changes as per PR
IR96334 Feb 6, 2026
ff7165f
merge conflict
IR96334 Feb 6, 2026
126f925
fixed test
IR96334 Feb 9, 2026
3234b7e
Fixed more tests
IR96334 Feb 10, 2026
52da255
Addressed PR comments, mostly
IR96334 Feb 12, 2026
9b50d51
frontend changes, now included
IR96334 Feb 12, 2026
02a5421
Fixed bugs
IR96334 Feb 12, 2026
04152a4
revert scope creep
IR96334 Feb 17, 2026
7862e53
small change
IR96334 Feb 17, 2026
df1d4db
Merge branch 'main' into BAI-2223-generalise-scanners-away-from-security
IR96334 Feb 17, 2026
56bf632
fix
IR96334 Feb 17, 2026
1e7d2a5
Fix ui issue
IR96334 Feb 17, 2026
5076a7e
fixes as pr
IR96334 Feb 17, 2026
0c3eb63
test fixes
IR96334 Feb 18, 2026
aa6b3cc
fix tests
IR96334 Feb 19, 2026
9a76042
Merge branch 'main' into BAI-2223-generalise-scanners-away-from-security
IR96334 Feb 19, 2026
31eb60d
Small changes as per PR comments
IR96334 Feb 19, 2026
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
4 changes: 2 additions & 2 deletions backend/config/default.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ module.exports = {
},
},

avScanning: {
artefactScanning: {
clamdscan: {
concurrency: 2,
host: '127.0.0.1',
Expand Down Expand Up @@ -277,7 +277,7 @@ module.exports = {
kind: 'silly',
},

fileScanners: {
artefactScanners: {
kinds: [],
retryDelayInMinutes: 60,
maxInitRetries: 5,
Expand Down
4 changes: 2 additions & 2 deletions backend/config/dev_docker_compose.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ module.exports = {
region: 'ignored',
},

avScanning: {
artefactScanning: {
clamdscan: {
host: 'clamd',
},
Expand All @@ -85,7 +85,7 @@ module.exports = {
},

connectors: {
fileScanners: {
artefactScanners: {
kinds: ['clamAV', 'modelScan'],
retryDelayInMinutes: 60,
maxInitRetries: 5,
Expand Down
4 changes: 2 additions & 2 deletions backend/config/prod_docker_compose.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ module.exports = {
region: 'ignored',
},

avScanning: {
artefactScanning: {
clamdscan: {
host: 'clamd',
},
Expand All @@ -78,7 +78,7 @@ module.exports = {
},

connectors: {
fileScanners: {
artefactScanners: {
kinds: ['clamAV', 'modelScan'],
retryDelayInMinutes: 60,
maxInitRetries: 5,
Expand Down
4 changes: 2 additions & 2 deletions backend/src/clients/modelScan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ interface ModelScanResponse {
}

export async function getModelScanInfo() {
const url = `${config.avScanning.modelscan.protocol}://${config.avScanning.modelscan.host}:${config.avScanning.modelscan.port}`
const url = `${config.artefactScanning.modelscan.protocol}://${config.artefactScanning.modelscan.host}:${config.artefactScanning.modelscan.port}`
let res: FetchResponse

try {
Expand All @@ -70,7 +70,7 @@ export async function getModelScanInfo() {
}

export async function scanStream(stream: Readable, fileName: string) {
const url = `${config.avScanning.modelscan.protocol}://${config.avScanning.modelscan.host}:${config.avScanning.modelscan.port}`
const url = `${config.artefactScanning.modelscan.protocol}://${config.artefactScanning.modelscan.host}:${config.artefactScanning.modelscan.port}`
let res: FetchResponse

try {
Expand Down
64 changes: 64 additions & 0 deletions backend/src/connectors/artefactScanning/Base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import PQueue from 'p-queue'

import { FileInterface } from '../../models/File.js'
import { ImageRefInterface } from '../../models/Release.js'
import { ScanInterface } from '../../models/Scan.js'
import log from '../../services/log.js'
import { ArtefactTypeKeys } from '../../types/types.js'

export type ArtefactScanResult = Pick<
ScanInterface,
'toolName' | 'scannerVersion' | 'state' | 'summary' | 'additionalInfo' | 'lastRunAt'
>

//NOTE: This is a placeholder, in preparation towards image scanning implementation
export type ArtefactInterface = FileInterface | ImageRefInterface

export const ArtefactScanState = {
NotScanned: 'notScanned',
InProgress: 'inProgress',
Complete: 'complete',
Error: 'error',
} as const
export type ArtefactScanStateKeys = (typeof ArtefactScanState)[keyof typeof ArtefactScanState]

export type ArtefactScanningConnectorInfo = Pick<ArtefactScanResult, 'toolName' | 'scannerVersion'>

export abstract class ArtefactBaseScanningConnector {
abstract readonly toolName: string
abstract readonly version: string | undefined
abstract readonly queue: PQueue
abstract artefactType: ArtefactTypeKeys

info(): ArtefactScanningConnectorInfo {
return { toolName: this.toolName, scannerVersion: this.version }
}

abstract init()

abstract _scan(artefact: ArtefactInterface): Promise<ArtefactScanResult>

async scan(artefact: ArtefactInterface): Promise<ArtefactScanResult> {
log.debug({ artefact, ...this.info(), queueSize: this.queue.size }, 'Queueing scan.')
const scanResult = await this.queue
.add(() => this._scan(artefact))
.catch((error) => {
return this.scanError('Queued scan threw an error.', { error, artefact })
})
// return type of `queue.add()` is Promise<void | ...> so reject void responses
if (scanResult === null || typeof scanResult !== 'object') {
return this.scanError('Queued scan failed to correctly return.', { artefact })
}
return scanResult
}

async scanError(message: string, context?: object): Promise<ArtefactScanResult> {
const scannerInfo = this.info()
log.error({ ...context, ...scannerInfo }, message)
return {
...scannerInfo,
state: ArtefactScanState.Error,
lastRunAt: new Date(),
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import PQueue from 'p-queue'

import { getObjectStream } from '../../clients/s3.js'
import { FileInterfaceDoc } from '../../models/File.js'
import { ClamAVSummary } from '../../models/Scan.js'
import log from '../../services/log.js'
import { ArtefactType, ArtefactTypeKeys } from '../../types/types.js'
import config from '../../utils/config.js'
import { BaseQueueFileScanningConnector, FileScanResult, ScanState } from './Base.js'
import { ArtefactBaseScanningConnector, ArtefactScanResult, ArtefactScanState } from './Base.js'

function safeParseVersion(versionStr: string): string {
try {
Expand All @@ -19,8 +21,9 @@ function safeParseVersion(versionStr: string): string {
}
}

export class ClamAvFileScanningConnector extends BaseQueueFileScanningConnector {
queue: PQueue = new PQueue({ concurrency: config.avScanning.clamdscan.concurrency })
export class ClamAvFileScanningConnector extends ArtefactBaseScanningConnector {
queue: PQueue = new PQueue({ concurrency: config.artefactScanning.clamdscan.concurrency })
artefactType: ArtefactTypeKeys = ArtefactType.FILE
toolName = 'Clam AV'
version: string | undefined = undefined
av: NodeClam | undefined = undefined
Expand All @@ -30,14 +33,14 @@ export class ClamAvFileScanningConnector extends BaseQueueFileScanningConnector
}

async init() {
this.av = await new NodeClam().init({ clamdscan: config.avScanning.clamdscan })
this.av = await new NodeClam().init({ clamdscan: config.artefactScanning.clamdscan })
const scannerVersion = await this.av.getVersion()
this.version = safeParseVersion(scannerVersion)
log.debug({ ...this.info() }, 'Initialised Clam AV scanner')
return this
}

async _scan(file: FileInterfaceDoc): Promise<FileScanResult[]> {
async _scan(file: FileInterfaceDoc): Promise<ArtefactScanResult> {
const scannerInfo = this.info()
if (!this.av) {
return await this.scanError(`Could not use ${this.toolName} as it is not been correctly initialised.`, {
Expand All @@ -48,17 +51,21 @@ export class ClamAvFileScanningConnector extends BaseQueueFileScanningConnector
const s3Stream = await getObjectStream(file.path)

try {
const { isInfected, viruses } = await this.av.scanStream(s3Stream)
log.debug({ file, result: { isInfected, viruses }, ...scannerInfo }, 'Scan complete.')
return [
{
...scannerInfo,
state: ScanState.Complete,
isInfected,
viruses,
lastRunAt: new Date(),
},
]
const { viruses } = await this.av.scanStream(s3Stream)
log.debug({ file, result: { viruses }, ...scannerInfo }, 'Scan complete.')
const summary: ClamAVSummary[] = viruses.map(
(virus) =>
({
virus,
}) as ClamAVSummary,
)

return {
...scannerInfo,
state: ArtefactScanState.Complete,
summary,
lastRunAt: new Date(),
}
} catch (error) {
return this.scanError(`This file could not be scanned due to an error caused by ${this.toolName}`, {
error: Error.isError(error) ? { name: error.name, stack: error.stack } : error,
Expand Down
52 changes: 52 additions & 0 deletions backend/src/connectors/artefactScanning/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import config from '../../utils/config.js'
import { ConfigurationError } from '../../utils/error.js'
import { ArtefactBaseScanningConnector } from './Base.js'
import { ClamAvFileScanningConnector } from './clamAv.js'
import { ModelScanFileScanningConnector } from './modelScan.js'
import { ArtefactScanningWrapper } from './wrapper.js'

export const ArtefactScanKind = {
ClamAv: 'clamAV',
ModelScan: 'modelScan',
} as const
export type ArtefactScanKindKeys = (typeof ArtefactScanKind)[keyof typeof ArtefactScanKind]

const artefactScanConnectors: Set<ArtefactBaseScanningConnector> = new Set<ArtefactBaseScanningConnector>()
let scannerWrapper: undefined | ArtefactScanningWrapper = undefined

async function addArtefactScanners(cache = true): Promise<ArtefactScanningWrapper> {
if (scannerWrapper && cache) {
return scannerWrapper
}
artefactScanConnectors.clear()
for (const artefactScanner of config.connectors.artefactScanners.kinds) {
switch (artefactScanner) {
case ArtefactScanKind.ClamAv:
try {
const scanner = new ClamAvFileScanningConnector()
artefactScanConnectors.add(scanner)
} catch (error) {
throw ConfigurationError('Could not configure or initialise Clam AV', { error })
}
break
case ArtefactScanKind.ModelScan:
try {
const scanner = new ModelScanFileScanningConnector()
artefactScanConnectors.add(scanner)
} catch (error) {
throw ConfigurationError('Could not configure or initialise ModelScan', { error })
}
break
default:
throw ConfigurationError(`'${artefactScanner}' is not a valid file scanning kind.`, {
validKinds: Object.values(ArtefactScanKind),
})
}
}

scannerWrapper = new ArtefactScanningWrapper(artefactScanConnectors)
await scannerWrapper.initialiseScanners()
return scannerWrapper
}

export default await addArtefactScanners()
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import PQueue from 'p-queue'
import { getModelScanInfo, scanStream } from '../../clients/modelScan.js'
import { getObjectStream } from '../../clients/s3.js'
import { FileInterfaceDoc } from '../../models/File.js'
import { ModelScanSummary, SeverityLevelKeys } from '../../models/Scan.js'
import log from '../../services/log.js'
import { ArtefactType, ArtefactTypeKeys } from '../../types/types.js'
import config from '../../utils/config.js'
import { BaseQueueFileScanningConnector, FileScanResult, ScanState } from './Base.js'
import { ArtefactBaseScanningConnector, ArtefactScanResult, ArtefactScanState } from './Base.js'

export class ModelScanFileScanningConnector extends BaseQueueFileScanningConnector {
queue: PQueue = new PQueue({ concurrency: config.avScanning.modelscan.concurrency })
export class ModelScanFileScanningConnector extends ArtefactBaseScanningConnector {
queue: PQueue = new PQueue({ concurrency: config.artefactScanning.modelscan.concurrency })
artefactType: ArtefactTypeKeys = ArtefactType.FILE
toolName: string = 'ModelScan'
version: string | undefined = undefined

Expand All @@ -22,7 +25,7 @@ export class ModelScanFileScanningConnector extends BaseQueueFileScanningConnect
return this
}

async _scan(file: FileInterfaceDoc): Promise<FileScanResult[]> {
async _scan(file: FileInterfaceDoc): Promise<ArtefactScanResult> {
await this.init()
const scannerInfo = this.info()
if (!scannerInfo.scannerVersion) {
Expand All @@ -42,21 +45,21 @@ export class ModelScanFileScanningConnector extends BaseQueueFileScanningConnect
})
}

const issues = scanResults.summary.total_issues
const isInfected = issues > 0
const viruses: string[] = isInfected
? scanResults.issues.map((issue) => `${issue.severity}: ${issue.description}. ${issue.scanner}`)
: []
log.debug({ file, result: { isInfected, viruses }, ...scannerInfo }, 'Scan complete.')
return [
{
...scannerInfo,
state: ScanState.Complete,
isInfected,
viruses,
lastRunAt: new Date(),
},
]
const summary: ModelScanSummary[] = scanResults.issues.map(
(issue) =>
({
severity: issue.severity.toLowerCase() as SeverityLevelKeys,
vulnerabilityDescription: `${issue.description}. (scanner: ${issue.scanner})`,
}) as ModelScanSummary,
)

log.debug({ file, result: { summary }, ...scannerInfo }, 'Scan complete.')
return {
...scannerInfo,
state: ArtefactScanState.Complete,
summary,
lastRunAt: new Date(),
}
} catch (error) {
return this.scanError(`This file could not be scanned due to an error caused by ${this.toolName}`, {
error: Error.isError(error) ? { name: error.name, stack: error.stack } : error,
Expand Down
Loading
Loading