Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
27ea47e
feat: add try catch in stream function to catch error
viniciuslora Aug 15, 2025
f78633b
fix: remove req.target.name for in case is not existing
viniciuslora Aug 15, 2025
b307ad7
chore: remove await for scan request
viniciuslora Aug 15, 2025
b8327f1
chore: refactor malware scan code to be easier to make maintenance
viniciuslora Aug 15, 2025
7dae946
chore: add log for update
viniciuslora Aug 15, 2025
eb71d9d
chore: add req tenant into get operation
viniciuslora Aug 15, 2025
67c799e
chore: move attachment service call to methods instead of passing as …
viniciuslora Aug 18, 2025
a1ec657
chore: move attachment service outside try catch context
viniciuslora Aug 18, 2025
ace5552
chore: add more logs
viniciuslora Aug 18, 2025
59a7c74
chore: add more logs
viniciuslora Aug 18, 2025
085c921
chore: stringify attachmentsrv
viniciuslora Aug 18, 2025
cadcab9
chore: extract entity name from current entity
viniciuslora Aug 19, 2025
683b6bc
chore: add await to test async issues
viniciuslora Aug 20, 2025
1e80bbb
chore: test removing promise from remoteScan
viniciuslora Aug 27, 2025
9865389
chore: try moving to generic call from update
viniciuslora Aug 27, 2025
6d4b2d8
chore: use cds spawn to trigger request
viniciuslora Aug 27, 2025
5f0a9a1
chore: remove next
viniciuslora Aug 27, 2025
191029a
chore: add spawn for other put
viniciuslora Aug 27, 2025
b5214b9
chore: add next to do commit
viniciuslora Aug 28, 2025
346abb9
chore: add logs
viniciuslora Aug 28, 2025
d5ef872
chore: remove events handler for success/fail
viniciuslora Aug 29, 2025
c0f86e9
chore: remove unecessary zip file
viniciuslora Sep 2, 2025
4540bc6
Merge branch 'main' into fix/malware-scan-issues
eric-pSAP Sep 10, 2025
3002759
Merge branch 'main' into fix/malware-scan-issues
eric-pSAP Sep 10, 2025
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
28 changes: 18 additions & 10 deletions lib/aws-s3.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,11 @@

const stored = super.put(attachments, metadata, null, isDraftEnabled);
await Promise.all([stored, multipartUpload.done()]);
if (this.kind === 's3') scanRequest(attachments, { ID: metadata.ID }, req);
if (this.kind === 's3') {
cds.spawn({
tenant: req.tenant
}, async () => await scanRequest(req.target, keys, req));

Check failure on line 111 in lib/aws-s3.js

View workflow job for this annotation

GitHub Actions / lint

'keys' is not defined
}
DEBUG?.(`[S3 Upload] File uploaded successfully using put to ${this.bucket}`);
} catch (err) {
console.error(err); // eslint-disable-line no-console
Expand Down Expand Up @@ -192,28 +196,32 @@
await Promise.all([multipartUpload.done()]);

const keys = { ID: req.data.ID }
scanRequest(req.target, keys, req)

cds.spawn({
tenant: req.tenant
}, async () => await scanRequest(req.target, keys, req));

DEBUG?.(`[S3 Upload] Uploaded file using updateContentHandler for ${req.target.name}`);
}
} else if (req?.data?.note) {
const key = { ID: req.data.ID };
await super.update(req.target, key, { note: req.data.note });
DEBUG?.(`[S3 Upload] Updated file upload with note for ${req.target.name}`);
} else {
next();
}
next();
}

async getAttachmentsToDelete({ draftEntity, activeEntity, id }) {
const [draftAttachments, activeAttachments] = await Promise.all([
SELECT.from(draftEntity).columns("url").where(id),
SELECT.from(activeEntity).columns("url").where(id)
]);
const attachmentsStored = [];
draftEntity ? attachmentsStored.push(SELECT.from(draftEntity).where(id).columns('url')) : null;
activeEntity ? attachmentsStored.push(SELECT.from(activeEntity).where(id).columns('url')) : null;

const [draftAttachments, activeAttachments] = await Promise.all(attachmentsStored);

const activeUrls = new Set(activeAttachments.map(a => a.url));
return draftAttachments

return attachmentsToDelete = draftAttachments

Check failure on line 223 in lib/aws-s3.js

View workflow job for this annotation

GitHub Actions / lint

'attachmentsToDelete' is not defined
.filter(({ url }) => !activeUrls.has(url))
.map(({ url }) => ({ url }));
}

async attachDraftDeletionData(req) {
Expand Down
137 changes: 79 additions & 58 deletions lib/malwareScanner.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@
const DEBUG = cds.debug('attachments')
const { SELECT } = cds.ql;


const STATUS_INFECTED = "Infected";
const STATUS_CLEAN = "Clean";
const STATUS_SCANNING = "Scanning";
const STATUS_FAILED = "Failed";

async function scanRequest(Attachments, key, req) {
DEBUG?.(`[Scan] Scanning file uploaded for ${Attachments}`);
const scanEnabled = cds.env.requires?.attachments?.scan ?? true
const AttachmentsSrv = await cds.connect.to("attachments")

let draftEntity, activeEntity
if (Attachments.isDraft) {
Expand All @@ -14,74 +19,84 @@
} else {
activeEntity = Attachments
}
DEBUG?.(`[Scan] activeEntity = ${activeEntity}`);
let currEntity = draftEntity == undefined ? activeEntity : draftEntity

DEBUG?.(`[Scan] activeEntity = ${activeEntity}`);

if (!scanEnabled) {
if (cds.env.profiles.some(p => p === "development" || p === "test") && !cds.env.profiles.includes("hybrid")) {
await updateStatus(AttachmentsSrv, key, "Scanning", currEntity, draftEntity, activeEntity)
setTimeout(() => {
DEBUG?.('Malware scanning is disabled. Setting scan status to Clean in development profile.')
updateStatus(AttachmentsSrv, key, "Clean", currEntity, draftEntity, activeEntity)
.catch(e => cds.log('attachments').error(e))
}, 5000).unref()
return
} else {
return
}
await localMockedScan(key, req, currEntity, draftEntity, activeEntity);
} else {
await remoteScan(key, req, currEntity, draftEntity, activeEntity);
}
}

await updateStatus(AttachmentsSrv, key, "Scanning", currEntity, draftEntity, activeEntity)

const credentials = getCredentials()
const contentStream = await AttachmentsSrv.get(currEntity, key)
let fileContent
async function remoteScan(key, req, currEntity, draftEntity, activeEntity) {
const AttachmentsSrv = await cds.connect.to("attachments");
try {
DEBUG?.(`[Scan] streaming to string`);
fileContent = await streamToString(contentStream)

DEBUG?.(`[Scan] Executing remote scan for ${currEntity.name}`);
await updateStatus(key, STATUS_SCANNING, currEntity, draftEntity, activeEntity);

const contentStream = await AttachmentsSrv.get(currEntity, key, req.tenant);
const fileContent = await streamToString(contentStream);

const malwareScannerResponse = await sendFileToMalwareScanner(fileContent);

const responseText = await malwareScannerResponse.json();

const status = responseText.malwareDetected ? STATUS_INFECTED : STATUS_CLEAN;

if (status === STATUS_INFECTED) {
DEBUG?.("[Scan] Malware detected in the file, deleting attachment content from db", key);
await AttachmentsSrv.deleteInfectedAttachment(currEntity, key, req);
}
DEBUG?.(`[Scan] Scanned file with status: ${status}`, key);
await updateStatus(key, status, currEntity, draftEntity, activeEntity);
} catch (err) {
DEBUG?.("[Scan] Malware Scanning: Cannot read file content", err)
await updateStatus(AttachmentsSrv, key, "Failed", currEntity, draftEntity, activeEntity)
return
DEBUG?.("[Scan] Failed to execute Malware scan - ", err)
await updateStatus(key, STATUS_FAILED, currEntity, draftEntity, activeEntity);
}
}

let response;
try {
DEBUG?.("[Scan] Sending file to Malware Scanning Service");
response = await fetch(`https://${credentials.uri}/scan`, {
method: "POST",
headers: {
Authorization:
"Basic " + Buffer.from(`${credentials.username}:${credentials.password}`, "binary").toString("base64"),
},
body: fileContent,
})
} catch (error) {
DEBUG?.("Request to malware scanner failed", error)
await updateStatus(AttachmentsSrv, key, "Failed", currEntity, draftEntity, activeEntity)
async function localMockedScan(key, req, currEntity, draftEntity, activeEntity) {
const AttachmentsSrv = await cds.connect.to("attachments");

Check warning on line 62 in lib/malwareScanner.js

View workflow job for this annotation

GitHub Actions / lint

'AttachmentsSrv' is assigned a value but never used

DEBUG?.(`[Scan] Executing local mocked scan for ${currEntity.name}`);
if (cds.env.profiles.some(p => p === "development" || p === "test") && !cds.env.profiles.includes("hybrid")) {
await updateStatus(key, "Scanning", currEntity, draftEntity, activeEntity)
setTimeout(() => {
DEBUG?.('Malware scanning is disabled. Setting scan status to Clean in development profile.')
updateStatus(key, "Clean", currEntity, draftEntity, activeEntity)
.catch(e => cds.log('attachments').error(e))
}, 5000).unref()
return
} else {
return
}
}

try {
const responseText = await response.json()
const status = responseText.malwareDetected ? "Infected" : "Clean"
DEBUG?.(`[Scan] Scanned file with status: ${status}`, key);
if (status === "Infected") {
DEBUG?.("[Scan] Malware detected in the file, deleting attachment content from db", key)
await AttachmentsSrv.deleteInfectedAttachment(currEntity, key, req)
}
await updateStatus(AttachmentsSrv, key, status, currEntity, draftEntity, activeEntity)
} catch (err) {
DEBUG?.("Cannot serialize malware scanner response body", err)
await updateStatus(AttachmentsSrv, key, "Failed", currEntity, draftEntity, activeEntity)
}
async function sendFileToMalwareScanner(fileContent) {
DEBUG?.("[Scan] Sending file to Malware Scanning Service");
const credentials = getCredentials();
response = await fetch(`https://${credentials.uri}/scan`, {

Check failure on line 81 in lib/malwareScanner.js

View workflow job for this annotation

GitHub Actions / lint

'response' is not defined
method: "POST",
headers: {
Authorization:
"Basic " + Buffer.from(`${credentials.username}:${credentials.password}`, "binary").toString("base64"),
},
body: fileContent,
});
return response;

Check failure on line 89 in lib/malwareScanner.js

View workflow job for this annotation

GitHub Actions / lint

'response' is not defined
}

async function updateStatus(AttachmentsSrv, key, status, currEntity, draftEntity, activeEntity) {
async function updateStatus(key, status, currEntity, draftEntity, activeEntity) {
DEBUG?.(`[Scan] Updating status for ${currEntity} - ${status}`);
if (currEntity == draftEntity) {
currEntity = await getCurrentEntity(currEntity, activeEntity, key)
}
await AttachmentsSrv.update(currEntity, key, { status: status })
const entityName = typeof currEntity === 'string' ? currEntity : currEntity.name;
await cds.update(entityName, key, { status: status })
DEBUG?.(`[Scan] Updated status for ${entityName} - ${status}`);
}

async function getCurrentEntity(draftEntity, activeEntity, key) {
Expand All @@ -103,17 +118,23 @@
try {
return cds.env.requires.malwareScanner.credentials;
} catch {
throw new Error("SAP Malware Scanning service is not bound.");
throw new Error("[Scan] SAP Malware Scanning service is not bound.");
}
}

function streamToString(stream) {
DEBUG?.(`[Scan] Streaming to string`);
const chunks = [];
return new Promise((resolve, reject) => {
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
stream.on('error', (err) => reject(err))
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
})
try {
return new Promise((resolve, reject) => {
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
stream.on('error', (err) => reject(err))
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
});
} catch (error) {
throw new Error("[Scan] Error in streaming data from content", error)
}

}

module.exports = {
Expand Down
Loading