kalai-attach 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +451 -0
- package/_i18n/i18n.properties +6 -0
- package/_i18n/i18n_ar.properties +6 -0
- package/_i18n/i18n_bg.properties +6 -0
- package/_i18n/i18n_cs.properties +6 -0
- package/_i18n/i18n_da.properties +6 -0
- package/_i18n/i18n_de.properties +6 -0
- package/_i18n/i18n_el.properties +6 -0
- package/_i18n/i18n_en.properties +6 -0
- package/_i18n/i18n_en_US_saptrc.properties +6 -0
- package/_i18n/i18n_es.properties +6 -0
- package/_i18n/i18n_es_MX.properties +6 -0
- package/_i18n/i18n_fi.properties +6 -0
- package/_i18n/i18n_fr.properties +6 -0
- package/_i18n/i18n_he.properties +6 -0
- package/_i18n/i18n_hr.properties +6 -0
- package/_i18n/i18n_hu.properties +6 -0
- package/_i18n/i18n_it.properties +6 -0
- package/_i18n/i18n_ja.properties +6 -0
- package/_i18n/i18n_kk.properties +6 -0
- package/_i18n/i18n_ko.properties +6 -0
- package/_i18n/i18n_ms.properties +6 -0
- package/_i18n/i18n_nl.properties +6 -0
- package/_i18n/i18n_no.properties +6 -0
- package/_i18n/i18n_pl.properties +6 -0
- package/_i18n/i18n_pt.properties +6 -0
- package/_i18n/i18n_ro.properties +6 -0
- package/_i18n/i18n_ru.properties +6 -0
- package/_i18n/i18n_sh.properties +6 -0
- package/_i18n/i18n_sk.properties +6 -0
- package/_i18n/i18n_sl.properties +6 -0
- package/_i18n/i18n_sv.properties +6 -0
- package/_i18n/i18n_th.properties +6 -0
- package/_i18n/i18n_tr.properties +6 -0
- package/_i18n/i18n_uk.properties +6 -0
- package/_i18n/i18n_vi.properties +6 -0
- package/_i18n/i18n_zh_CN.properties +6 -0
- package/_i18n/i18n_zh_TW.properties +6 -0
- package/_i18n/messages.properties +6 -0
- package/_i18n/messages_en_US_saptrc.properties +4 -0
- package/cds-plugin.js +4 -0
- package/db/data/sap.attachments-ScanStates.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_ar.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_bg.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_cs.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_da.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_de.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_el.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_en.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_en_US_saptrc.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_es.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_es_MX.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_fi.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_fr.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_he.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_hr.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_hu.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_it.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_ja.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_kk.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_ko.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_ms.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_nl.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_no.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_pl.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_pt.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_ro.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_ru.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_sh.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_sk.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_sl.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_sv.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_th.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_tr.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_uk.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_vi.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_zh_CN.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_zh_TW.csv +6 -0
- package/db/index.cds +85 -0
- package/index.cds +1 -0
- package/lib/csn-runtime-extension.js +31 -0
- package/lib/generic-handlers.js +199 -0
- package/lib/helper.js +407 -0
- package/lib/mtx/server.js +583 -0
- package/lib/plugin.js +112 -0
- package/package.json +67 -0
- package/srv/aws-s3.js +249 -0
- package/srv/azure-blob-storage.js +202 -0
- package/srv/basic.js +331 -0
- package/srv/gcp.js +226 -0
- package/srv/malwareScanner-mocked.cds +3 -0
- package/srv/malwareScanner-mocked.js +127 -0
- package/srv/malwareScanner.cds +23 -0
- package/srv/malwareScanner.js +151 -0
- package/srv/object-store.js +44 -0
- package/srv/standard.js +3 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
@protocol : 'none'
|
|
2
|
+
service malwareScanner {
|
|
3
|
+
|
|
4
|
+
event ScanAttachmentsFile {
|
|
5
|
+
target: String; //CSN name of Attachments entity to scan
|
|
6
|
+
keys: Map; //Key value pairs of attachments entity to scan
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
action scan(file: LargeBinary) returns {
|
|
10
|
+
isMalware: Boolean;
|
|
11
|
+
encryptedContentDetected: Boolean;
|
|
12
|
+
scanSize: Integer;
|
|
13
|
+
finding: String;
|
|
14
|
+
/**
|
|
15
|
+
* Returns "empty" if no type could be detected
|
|
16
|
+
*/
|
|
17
|
+
mimeType: String;
|
|
18
|
+
/**
|
|
19
|
+
* SHA256 hash of file
|
|
20
|
+
*/
|
|
21
|
+
hash: String;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
|
+
const crypto = require("crypto")
|
|
3
|
+
const https = require('https')
|
|
4
|
+
const { URL } = require('url')
|
|
5
|
+
const LOG = cds.log('attachments')
|
|
6
|
+
|
|
7
|
+
class MalwareScanner extends require('./malwareScanner-mocked') {
|
|
8
|
+
|
|
9
|
+
get credentials() {
|
|
10
|
+
return getCredentials()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
init() {
|
|
14
|
+
return super.init()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Scans the passed over file
|
|
19
|
+
* @param {*} req - The request object
|
|
20
|
+
* @param {string} fileName - The name of the file being scanned
|
|
21
|
+
*/
|
|
22
|
+
async scanFile(req) {
|
|
23
|
+
const { file } = req.data;
|
|
24
|
+
let response
|
|
25
|
+
const scanStartTime = Date.now()
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Prepare request options
|
|
29
|
+
const url = new URL(`https://${this.credentials.uri}/scan`)
|
|
30
|
+
const requestOptions = {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
hostname: url.hostname,
|
|
33
|
+
port: url.port || 443,
|
|
34
|
+
path: url.pathname,
|
|
35
|
+
headers: {}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (this.credentials?.certificate && this.credentials?.key) {
|
|
39
|
+
LOG.debug('Using mTLS authentication for malware scanning')
|
|
40
|
+
|
|
41
|
+
const cert = new crypto.X509Certificate(this.credentials.certificate)
|
|
42
|
+
const expiryDate = new Date(cert.validTo)
|
|
43
|
+
const now = Date.now()
|
|
44
|
+
|
|
45
|
+
// Show warning if certificate is expired or expiring within 30 days
|
|
46
|
+
const msIn30Days = 30 * 24 * 60 * 60 * 1000
|
|
47
|
+
|
|
48
|
+
if (expiryDate.getTime() < now) {
|
|
49
|
+
LOG.error('Malware scanner certificate expired', { validTo: cert.validTo })
|
|
50
|
+
throw new Error('Malware scanner certificate expired')
|
|
51
|
+
} else if (expiryDate.getTime() - now < msIn30Days) {
|
|
52
|
+
LOG.warn('Malware scanner certificate expiring soon', { validTo: cert.validTo })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
requestOptions.cert = this.credentials.certificate
|
|
56
|
+
requestOptions.key = this.credentials.key
|
|
57
|
+
requestOptions.rejectUnauthorized = false
|
|
58
|
+
|
|
59
|
+
LOG.debug('Using mTLS authorization')
|
|
60
|
+
} else if (this.credentials?.username && this.credentials?.password) {
|
|
61
|
+
// Basic Auth: set Authorization header
|
|
62
|
+
LOG.warn(
|
|
63
|
+
'Deprecated: Basic Authentication for malware scanning is deprecated and will be removed in future releases.',
|
|
64
|
+
)
|
|
65
|
+
requestOptions.headers.Authorization =
|
|
66
|
+
"Basic " + Buffer.from(`${this.credentials.username}:${this.credentials.password}`, "binary").toString("base64")
|
|
67
|
+
LOG.debug('Using basic authorization')
|
|
68
|
+
} else {
|
|
69
|
+
throw new Error("Could not find any credentials to authenticate against malware scanning service, please make sure binding and service key exists.")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
response = await new Promise((resolve, reject) => {
|
|
73
|
+
const req = https.request(requestOptions, (res) => {
|
|
74
|
+
let data = ''
|
|
75
|
+
res.on('data', chunk => data += chunk)
|
|
76
|
+
res.on('end', () => {
|
|
77
|
+
resolve({
|
|
78
|
+
status: res.statusCode,
|
|
79
|
+
ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
80
|
+
data
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
req.on('error', reject)
|
|
85
|
+
|
|
86
|
+
file.pipe(req)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
const json = JSON.parse(response.data || '{}')
|
|
92
|
+
const errorMsg = JSON.stringify(json) || response.statusText || 'Unknown error from malware scanner'
|
|
93
|
+
return req.reject(response.status, `Scanning failed: ${errorMsg}`)
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
const scanDuration = Date.now() - scanStartTime
|
|
97
|
+
LOG.error(`Request to malware scanner failed`, error,
|
|
98
|
+
'Check malware scanner service binding and network connectivity',
|
|
99
|
+
{ scanDuration, scannerUri: this.credentials?.uri }
|
|
100
|
+
)
|
|
101
|
+
file?.destroy()
|
|
102
|
+
return req.reject(500, 'Scanning failed')
|
|
103
|
+
} finally {
|
|
104
|
+
file?.destroy()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @typedef {Object} MalwareScanResponse
|
|
109
|
+
* @property {boolean} malwareDetected - Indicates whether the scan engine detected a threat.
|
|
110
|
+
* @property {boolean} encryptedContentDetected - Indicates whether the file has encrypted parts, which could not be scanned.
|
|
111
|
+
* @property {number} scanSize - Size in bytes of the scanned file. Use the file size to validate the success of data transmission.
|
|
112
|
+
* @property {string} finding - This field may contain information about detected malware.
|
|
113
|
+
* @property {string} mimeType - Indicates the detected MIME type for the scanned file. This data may not be reliable and results may vary on different service providers.
|
|
114
|
+
* @property {string} SHA256 - SHA-256 hash of the scanned file. Use the hash to validate the success of data transmission.
|
|
115
|
+
*/
|
|
116
|
+
/** @type {MalwareScanResponse} */
|
|
117
|
+
const responseJson = JSON.parse(response.data)
|
|
118
|
+
const scanDuration = Date.now() - scanStartTime
|
|
119
|
+
LOG.debug(`Malware scan response`, { scanDuration, response: responseJson })
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
isMalware: responseJson.malwareDetected,
|
|
123
|
+
encryptedContentDetected: responseJson.encryptedContentDetected,
|
|
124
|
+
scanSize: responseJson.scanSize,
|
|
125
|
+
finding: responseJson.finding,
|
|
126
|
+
mimeType: responseJson.mimeType,
|
|
127
|
+
hash: responseJson.SHA256,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = MalwareScanner
|
|
133
|
+
|
|
134
|
+
function getCredentials() {
|
|
135
|
+
const credentials = cds.env.requires?.malwareScanner?.credentials
|
|
136
|
+
if (!credentials) {
|
|
137
|
+
throw new Error(`cds.env.requires.malwareScanner.credentials is empty! Please bind the SAP Malware Scanning service against your app!`)
|
|
138
|
+
}
|
|
139
|
+
const requiredFields = {
|
|
140
|
+
mTLS: ['uri', 'certificate', 'key'],
|
|
141
|
+
basic: ['uri', 'username', 'password']
|
|
142
|
+
}
|
|
143
|
+
const missingMTLS = requiredFields.mTLS.filter(field => !credentials[field])
|
|
144
|
+
const missingBasic = requiredFields.basic.filter(field => !credentials[field])
|
|
145
|
+
|
|
146
|
+
if (missingMTLS.length > 0 && missingBasic.length > 0) {
|
|
147
|
+
throw new Error(`Missing Malware Scanner credentials: mTLS [${missingMTLS.join(', ')}], Basic Auth [${missingBasic.join(', ')}]`)
|
|
148
|
+
}
|
|
149
|
+
LOG.debug(`Malware scanning credentials successfully retrieved.`)
|
|
150
|
+
return credentials
|
|
151
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const cds = require("@sap/cds")
|
|
2
|
+
const LOG = cds.log('attachments')
|
|
3
|
+
|
|
4
|
+
module.exports = class RemoteAttachmentsService extends require("./basic") {
|
|
5
|
+
|
|
6
|
+
clientsCache = new Map()
|
|
7
|
+
isMultiTenancyEnabled = !!cds.env.requires.multitenancy
|
|
8
|
+
objectStoreKind = cds.env.requires?.attachments?.objectStore?.kind
|
|
9
|
+
separateObjectStore = this.isMultiTenancyEnabled && this.objectStoreKind === "separate"
|
|
10
|
+
|
|
11
|
+
init() {
|
|
12
|
+
LOG.debug(`${this.constructor.name} initialization`, {
|
|
13
|
+
multiTenancy: this.isMultiTenancyEnabled,
|
|
14
|
+
objectStoreKind: this.objectStoreKind,
|
|
15
|
+
separateObjectStore: this.separateObjectStore,
|
|
16
|
+
attachmentsConfig: {
|
|
17
|
+
kind: cds.env.requires?.attachments?.kind,
|
|
18
|
+
scan: cds.env.requires?.attachments?.scan
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
return super.init()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @inheritdoc
|
|
27
|
+
*/
|
|
28
|
+
registerHandlers(srv) {
|
|
29
|
+
srv.before(
|
|
30
|
+
"DELETE",
|
|
31
|
+
(req) => {
|
|
32
|
+
if (!req.target.isDraft || !req.target._attachments.isAttachmentsEntity) return;
|
|
33
|
+
return this.attachDraftDeletionData.bind(this)(req)
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
srv.after(
|
|
37
|
+
"DELETE",
|
|
38
|
+
(res, req) => {
|
|
39
|
+
if (!req.target.isDraft || !req.target._attachments.isAttachmentsEntity) return;
|
|
40
|
+
return this.deleteAttachmentsWithKeys.bind(this)(res, req)
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
}
|
package/srv/standard.js
ADDED