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.
Files changed (98) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +451 -0
  3. package/_i18n/i18n.properties +6 -0
  4. package/_i18n/i18n_ar.properties +6 -0
  5. package/_i18n/i18n_bg.properties +6 -0
  6. package/_i18n/i18n_cs.properties +6 -0
  7. package/_i18n/i18n_da.properties +6 -0
  8. package/_i18n/i18n_de.properties +6 -0
  9. package/_i18n/i18n_el.properties +6 -0
  10. package/_i18n/i18n_en.properties +6 -0
  11. package/_i18n/i18n_en_US_saptrc.properties +6 -0
  12. package/_i18n/i18n_es.properties +6 -0
  13. package/_i18n/i18n_es_MX.properties +6 -0
  14. package/_i18n/i18n_fi.properties +6 -0
  15. package/_i18n/i18n_fr.properties +6 -0
  16. package/_i18n/i18n_he.properties +6 -0
  17. package/_i18n/i18n_hr.properties +6 -0
  18. package/_i18n/i18n_hu.properties +6 -0
  19. package/_i18n/i18n_it.properties +6 -0
  20. package/_i18n/i18n_ja.properties +6 -0
  21. package/_i18n/i18n_kk.properties +6 -0
  22. package/_i18n/i18n_ko.properties +6 -0
  23. package/_i18n/i18n_ms.properties +6 -0
  24. package/_i18n/i18n_nl.properties +6 -0
  25. package/_i18n/i18n_no.properties +6 -0
  26. package/_i18n/i18n_pl.properties +6 -0
  27. package/_i18n/i18n_pt.properties +6 -0
  28. package/_i18n/i18n_ro.properties +6 -0
  29. package/_i18n/i18n_ru.properties +6 -0
  30. package/_i18n/i18n_sh.properties +6 -0
  31. package/_i18n/i18n_sk.properties +6 -0
  32. package/_i18n/i18n_sl.properties +6 -0
  33. package/_i18n/i18n_sv.properties +6 -0
  34. package/_i18n/i18n_th.properties +6 -0
  35. package/_i18n/i18n_tr.properties +6 -0
  36. package/_i18n/i18n_uk.properties +6 -0
  37. package/_i18n/i18n_vi.properties +6 -0
  38. package/_i18n/i18n_zh_CN.properties +6 -0
  39. package/_i18n/i18n_zh_TW.properties +6 -0
  40. package/_i18n/messages.properties +6 -0
  41. package/_i18n/messages_en_US_saptrc.properties +4 -0
  42. package/cds-plugin.js +4 -0
  43. package/db/data/sap.attachments-ScanStates.csv +6 -0
  44. package/db/data/sap.attachments-ScanStates_texts.csv +6 -0
  45. package/db/data/sap.attachments-ScanStates_texts_ar.csv +6 -0
  46. package/db/data/sap.attachments-ScanStates_texts_bg.csv +6 -0
  47. package/db/data/sap.attachments-ScanStates_texts_cs.csv +6 -0
  48. package/db/data/sap.attachments-ScanStates_texts_da.csv +6 -0
  49. package/db/data/sap.attachments-ScanStates_texts_de.csv +6 -0
  50. package/db/data/sap.attachments-ScanStates_texts_el.csv +6 -0
  51. package/db/data/sap.attachments-ScanStates_texts_en.csv +6 -0
  52. package/db/data/sap.attachments-ScanStates_texts_en_US_saptrc.csv +6 -0
  53. package/db/data/sap.attachments-ScanStates_texts_es.csv +6 -0
  54. package/db/data/sap.attachments-ScanStates_texts_es_MX.csv +6 -0
  55. package/db/data/sap.attachments-ScanStates_texts_fi.csv +6 -0
  56. package/db/data/sap.attachments-ScanStates_texts_fr.csv +6 -0
  57. package/db/data/sap.attachments-ScanStates_texts_he.csv +6 -0
  58. package/db/data/sap.attachments-ScanStates_texts_hr.csv +6 -0
  59. package/db/data/sap.attachments-ScanStates_texts_hu.csv +6 -0
  60. package/db/data/sap.attachments-ScanStates_texts_it.csv +6 -0
  61. package/db/data/sap.attachments-ScanStates_texts_ja.csv +6 -0
  62. package/db/data/sap.attachments-ScanStates_texts_kk.csv +6 -0
  63. package/db/data/sap.attachments-ScanStates_texts_ko.csv +6 -0
  64. package/db/data/sap.attachments-ScanStates_texts_ms.csv +6 -0
  65. package/db/data/sap.attachments-ScanStates_texts_nl.csv +6 -0
  66. package/db/data/sap.attachments-ScanStates_texts_no.csv +6 -0
  67. package/db/data/sap.attachments-ScanStates_texts_pl.csv +6 -0
  68. package/db/data/sap.attachments-ScanStates_texts_pt.csv +6 -0
  69. package/db/data/sap.attachments-ScanStates_texts_ro.csv +6 -0
  70. package/db/data/sap.attachments-ScanStates_texts_ru.csv +6 -0
  71. package/db/data/sap.attachments-ScanStates_texts_sh.csv +6 -0
  72. package/db/data/sap.attachments-ScanStates_texts_sk.csv +6 -0
  73. package/db/data/sap.attachments-ScanStates_texts_sl.csv +6 -0
  74. package/db/data/sap.attachments-ScanStates_texts_sv.csv +6 -0
  75. package/db/data/sap.attachments-ScanStates_texts_th.csv +6 -0
  76. package/db/data/sap.attachments-ScanStates_texts_tr.csv +6 -0
  77. package/db/data/sap.attachments-ScanStates_texts_uk.csv +6 -0
  78. package/db/data/sap.attachments-ScanStates_texts_vi.csv +6 -0
  79. package/db/data/sap.attachments-ScanStates_texts_zh_CN.csv +6 -0
  80. package/db/data/sap.attachments-ScanStates_texts_zh_TW.csv +6 -0
  81. package/db/index.cds +85 -0
  82. package/index.cds +1 -0
  83. package/lib/csn-runtime-extension.js +31 -0
  84. package/lib/generic-handlers.js +199 -0
  85. package/lib/helper.js +407 -0
  86. package/lib/mtx/server.js +583 -0
  87. package/lib/plugin.js +112 -0
  88. package/package.json +67 -0
  89. package/srv/aws-s3.js +249 -0
  90. package/srv/azure-blob-storage.js +202 -0
  91. package/srv/basic.js +331 -0
  92. package/srv/gcp.js +226 -0
  93. package/srv/malwareScanner-mocked.cds +3 -0
  94. package/srv/malwareScanner-mocked.js +127 -0
  95. package/srv/malwareScanner.cds +23 -0
  96. package/srv/malwareScanner.js +151 -0
  97. package/srv/object-store.js +44 -0
  98. 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
+ }
@@ -0,0 +1,3 @@
1
+ const cds = require("@sap/cds")
2
+
3
+ module.exports = require('./azure-blob-storage')