@cap-js/audit-logging 0.3.0 → 0.3.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,69 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ This project adheres to [Semantic Versioning](http://semver.org/).
5
+ The format is based on [Keep a Changelog](http://keepachangelog.com/).
6
+
7
+ ## Version 0.3.2 - 2023-10-11
8
+
9
+ ### Fixed
10
+
11
+ - If the request has no tenant (e.g., Unauthorized), the audit log shall be sent to the provider account
12
+
13
+ ## Version 0.3.1 - 2023-09-25
14
+
15
+ ### Fixed
16
+
17
+ - Defaulting of `@PersonalData.DataSubjectRole` to entity name
18
+ - Overriding service configuration
19
+
20
+ ## Version 0.3.0 - 2023-09-05
21
+
22
+ ### Changed
23
+
24
+ - Default value for `cds.requires['audit-log'].handle` changed to `['READ', 'WRITE']`, i.e., accessing sensitive data is now logged by default.
25
+
26
+ ## Version 0.2.0 - 2023-09-01
27
+
28
+ ### Added
29
+
30
+ - Export class `AuditLogService` for extending in custom implementations as follows:
31
+ ```js
32
+ const { AuditLogService } = require('@cap-js/audit-logging')
33
+ class MyAuditLogService extends AuditLogService {
34
+ async init() {
35
+ [...]
36
+ // call AuditLogService's init
37
+ await super.init()
38
+ }
39
+ }
40
+ module.exports = MyAuditLogService
41
+ ```
42
+
43
+ ## Version 0.1.0 - 2023-08-18
44
+
45
+ ### Added
46
+
47
+ - New API:
48
+ - `await audit.log('<event>', <data>)` for asynchronous logs (cf. `emit`)
49
+ - `await audit.logSync('<event>', <data>)` for synchronous logs (cf. `send`)
50
+ - New REST API-based schema with auto-filled `LogEntry` aspect
51
+ - New events `SensitiveDataRead`, `PersonalDataModified`, `ConfigurationModified`, and `SecurityEvent`
52
+ - Full support for OAuth2 plan of SAP Audit Log Service
53
+
54
+ ### Changed
55
+
56
+ - Whether reading sensitive data and modifying personal data is logged is determined by `cds.requires['audit-log'].handle: [...]`.
57
+ Possible values in the array are `READ` and/ or `WRITE`, with `WRITE` as the sole default entry.
58
+ Hence, accessing sensitive data is not logged by default.
59
+ - Integration with SAP Audit Log Service via REST API instead of client library (`@sap/audit-logging`)
60
+
61
+ ### Fixed
62
+
63
+ - Various glitches in log calculation
64
+
65
+ ### Removed
66
+
67
+ - Old events `dataAccessLog`, `dataModificationLog`, `configChangeLog`, and `securityLog`
68
+ - `@AuditLog.Operation` annotations are ignored. Having the plugin as dependency signals the intent to audit log.
69
+ - `cds.features.audit_personal_data: true` is no longer necessary. Instead, simply add the plugin as a dependency.
package/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # Welcome to @cap-js/audit-logging
2
+ [![REUSE status](https://api.reuse.software/badge/github.com/cap-js/audit-logging)](https://api.reuse.software/info/github.com/cap-js/audit-logging)
2
3
 
3
4
  ## About this project
4
5
 
5
- `@cap-js/audit-logging` is a CDS plugin providing integration to the SAP BTP Audit Logging Service as well as out-of-the-box personal data-related audit logging based on annotations.
6
+ `@cap-js/audit-logging` is a CDS plugin providing integration to the SAP Audit Log service as well as out-of-the-box personal data-related audit logging based on annotations.
6
7
 
7
8
  Documentation can be found at [cap.cloud.sap](https://cap.cloud.sap/docs/guides/data-privacy).
8
9
 
@@ -10,7 +11,7 @@ Documentation can be found at [cap.cloud.sap](https://cap.cloud.sap/docs/guides/
10
11
 
11
12
  See [Getting Started](https://cap.cloud.sap/docs/get-started) on how to jumpstart your development and grow as you go with SAP Cloud Application Programming Model.
12
13
 
13
- The end-to-end out-of-the-box functionality provided by this plugin requires a paid-for instance of the [SAP Audit Logging Service for SAP BTP]([url](https://help.sap.com/docs/application-logging-service?locale=en-US)). However, it is possible to provide an own implementation that writes the audit logs to a custom store.
14
+ The end-to-end out-of-the-box functionality provided by this plugin requires a paid-for instance of the [SAP Audit Log service for customers](https://help.sap.com/docs/btp/sap-business-technology-platform/audit-log-write-api-for-customers?locale=en-US). However, it is possible to provide an own implementation that writes the audit logs to a custom store.
14
15
 
15
16
  ## Support, Feedback, Contributing
16
17
 
package/lib/utils.js CHANGED
@@ -1,8 +1,10 @@
1
1
  const WRITE = { CREATE: 1, UPDATE: 1, DELETE: 1 }
2
2
 
3
3
  const hasPersonalData = entity => {
4
- if (!entity['@PersonalData.DataSubjectRole']) return
5
4
  if (!entity['@PersonalData.EntitySemantics']) return
5
+ // default role to entity name
6
+ if (entity['@PersonalData.EntitySemantics'] === 'DataSubject' && !entity['@PersonalData.DataSubjectRole'])
7
+ entity['@PersonalData.DataSubjectRole'] = entity.name.match(/\w+/g).pop()
6
8
  return !!Object.values(entity.elements).some(
7
9
  element =>
8
10
  element['@PersonalData.IsPotentiallyPersonal'] ||
@@ -148,31 +150,27 @@ const _getUps = (entity, model) => {
148
150
  return entity.set('__parents', ups)
149
151
  }
150
152
 
151
- const _ifDataSubject = (entity, role) => {
152
- return entity['@PersonalData.EntitySemantics'] === 'DataSubject' && entity['@PersonalData.DataSubjectRole'] === role
153
- }
154
-
155
- const _getDataSubjectUp = (role, model, entity, prev, next, result) => {
153
+ const _getDataSubjectUp = (model, entity, prev, next, result) => {
156
154
  for (const element of _getUps(entity, model)) {
157
155
  const me = { entity, relative: element.parent, element }
158
156
  if (prev) prev.next = me
159
- if (_ifDataSubject(element.parent, role)) {
157
+ if (element.parent['@PersonalData.EntitySemantics'] === 'DataSubject') {
160
158
  if (!result) result = { dataSubjectEntity: element.parent, subs: [] }
161
159
  result.subs.push(next || me)
162
160
  return result
163
161
  } else {
164
162
  // dfs is a must here
165
- result = _getDataSubjectUp(role, model, element.parent, me, next || me, result)
163
+ result = _getDataSubjectUp(model, element.parent, me, next || me, result)
166
164
  }
167
165
  }
168
166
  return result
169
167
  }
170
168
 
171
- const _getDataSubjectDown = (role, entity, prev, next) => {
169
+ const _getDataSubjectDown = (entity, prev, next) => {
172
170
  const associations = Object.values(entity.associations || {}).filter(e => !e._isBacklink)
173
171
  for (const element of associations) {
174
172
  const me = { entity, relative: entity, element }
175
- if (_ifDataSubject(element._target, role)) {
173
+ if (element._target['@PersonalData.EntitySemantics'] === 'DataSubject') {
176
174
  if (prev) prev.next = me
177
175
  return { dataSubjectEntity: element._target, subs: [next || me] }
178
176
  }
@@ -181,24 +179,25 @@ const _getDataSubjectDown = (role, entity, prev, next) => {
181
179
  for (const element of associations) {
182
180
  const me = { entity, relative: entity, element }
183
181
  if (prev) prev.next = me
184
- const dataSubject = _getDataSubjectDown(role, element._target, me, next || me)
182
+ const dataSubject = _getDataSubjectDown(element._target, me, next || me)
185
183
  if (dataSubject) return dataSubject
186
184
  }
187
185
  }
188
186
 
189
- const getDataSubject = (entity, model, role) => {
190
- const hash = '__dataSubject4' + role
187
+ const getDataSubject = (entity, model) => {
188
+ const hash = '__dataSubject'
191
189
  if (entity.own(hash)) return entity[hash]
192
190
  // entities with EntitySemantics 'DataSubjectDetails' or 'Other' must not necessarily
193
191
  // be always below or always above 'DataSubject' entity in CSN tree
194
- let dataSubject = _getDataSubjectUp(role, model, entity)
195
- if (!dataSubject) dataSubject = _getDataSubjectDown(role, entity)
196
- return entity.set(hash, dataSubject)
192
+ let dataSubjectInfo = _getDataSubjectUp(model, entity)
193
+ if (!dataSubjectInfo) dataSubjectInfo = _getDataSubjectDown(entity)
194
+ return entity.set(hash, dataSubjectInfo)
197
195
  }
198
196
 
199
197
  const addDataSubjectForDetailsEntity = (row, log, req, entity, model) => {
200
- const role = entity['@PersonalData.DataSubjectRole']
201
- const dataSubjectInfo = getDataSubject(entity, model, role)
198
+ const dataSubjectInfo = getDataSubject(entity, model)
199
+ const role = dataSubjectInfo.dataSubjectEntity['@PersonalData.DataSubjectRole']
200
+ log.data_subject.role ??= role
202
201
  log.data_subject.type = dataSubjectInfo.dataSubjectEntity.name
203
202
  /*
204
203
  * for each req (cf. $batch with atomicity) and data subject role (e.g., customer vs supplier),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cap-js/audit-logging",
3
- "version": "0.3.0",
4
- "description": "CDS plugin providing integration to the SAP BTP Audit Logging Service as well as out-of-the-box personal data-related audit logging based on annotations.",
3
+ "version": "0.3.2",
4
+ "description": "CDS plugin providing integration to the SAP Audit Log service as well as out-of-the-box personal data-related audit logging based on annotations.",
5
5
  "repository": "cap-js/audit-logging",
6
6
  "author": "SAP SE (https://www.sap.com)",
7
7
  "homepage": "https://cap.cloud.sap/",
@@ -9,7 +9,8 @@
9
9
  "main": "cds-plugin.js",
10
10
  "files": [
11
11
  "lib",
12
- "srv"
12
+ "srv",
13
+ "CHANGELOG.md"
13
14
  ],
14
15
  "scripts": {
15
16
  "lint": "npx eslint .",
@@ -21,27 +22,25 @@
21
22
  "devDependencies": {
22
23
  "@cap-js/sqlite": "^1",
23
24
  "axios": "^1",
24
- "better-sqlite3": "8.5.2",
25
25
  "eslint": "^8",
26
26
  "express": "^4",
27
27
  "jest": "^29"
28
28
  },
29
29
  "cds": {
30
30
  "requires": {
31
- "audit-log": true,
32
- "kinds": {
33
- "audit-log": {
34
- "handle": [
35
- "READ",
36
- "WRITE"
37
- ],
38
- "[development]": {
39
- "kind": "audit-log-to-console"
40
- },
41
- "[production]": {
42
- "kind": "audit-log-to-restv2"
43
- }
31
+ "audit-log": {
32
+ "handle": [
33
+ "READ",
34
+ "WRITE"
35
+ ],
36
+ "[development]": {
37
+ "kind": "audit-log-to-console"
44
38
  },
39
+ "[production]": {
40
+ "kind": "audit-log-to-restv2"
41
+ }
42
+ },
43
+ "kinds": {
45
44
  "audit-log-to-console": {
46
45
  "impl": "@cap-js/audit-logging/srv/log2console",
47
46
  "outbox": false
package/srv/log2restv2.js CHANGED
@@ -69,6 +69,7 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
69
69
  setTimeout(() => tokens.delete(tenant), (expires_in - 60) * 1000)
70
70
  return access_token
71
71
  } catch (err) {
72
+ LOG._trace && LOG.trace('error during token fetch:', err)
72
73
  // 401 could also mean x-zid is not valid
73
74
  if (String(err.response?.statusCode).match(/^4\d\d$/)) err.unrecoverable = true
74
75
  throw err
@@ -76,25 +77,30 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
76
77
  }
77
78
 
78
79
  async _send(data, path) {
79
- let url
80
80
  const headers = { 'content-type': 'application/json' }
81
- // TODO: what are these for?
82
81
  if (this._vcap) {
83
82
  headers.XS_AUDIT_ORG = this._vcap.organization_name
84
83
  headers.XS_AUDIT_SPACE = this._vcap.space_name
85
84
  headers.XS_AUDIT_APP = this._vcap.application_name
86
85
  }
86
+ let url
87
87
  if (this._oauth2) {
88
88
  url = this.options.credentials.url + PATHS.OAUTH2[path]
89
+ data.tenant ??= this._providerTenant //> if request has no tenant, stay in provider account
89
90
  headers.authorization = 'Bearer ' + (await this._getToken(data.tenant))
90
91
  data.tenant = data.tenant === this._providerTenant ? '$PROVIDER' : '$SUBSCRIBER'
91
92
  } else {
92
93
  url = this.options.credentials.url + PATHS.STANDARD[path]
93
94
  headers.authorization = this._auth
94
95
  }
96
+ if (LOG._debug) {
97
+ const _headers = Object.assign({}, headers, { authorization: headers.authorization.split(' ')[0] + ' ***' })
98
+ LOG.debug(`sending audit log to ${url} with tenant "${data.tenant}", user "${data.user}", and headers`, _headers)
99
+ }
95
100
  try {
96
101
  await _post(url, data, headers)
97
102
  } catch (err) {
103
+ LOG._trace && LOG.trace('error during log send:', err)
98
104
  // 429 (rate limit) is not unrecoverable
99
105
  if (String(err.response?.statusCode).match(/^4\d\d$/) && err.response?.statusCode !== 429)
100
106
  err.unrecoverable = true