@cap-js/audit-logging 1.2.0 → 1.2.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/srv/log2alsng.js CHANGED
@@ -1,114 +1,116 @@
1
- const cds = require('@sap/cds')
2
- const LOG = cds.log('audit-log')
1
+ const cds = require("@sap/cds");
2
+ const LOG = cds.log("audit-log");
3
3
  const { appMetadata } = require("../lib/utils");
4
- const https = require('https')
4
+ const https = require("https");
5
5
 
6
- const AuditLogService = require('./service')
6
+ const AuditLogService = require("./service");
7
7
 
8
8
  module.exports = class AuditLog2ALSNG extends AuditLogService {
9
9
  constructor() {
10
- super()
11
- if (!cds.env.requires['audit-log']?.credentials)
12
- throw new Error('No credentials found for SAP Audit Log Service NG')
10
+ super();
11
+ if (!cds.env.requires["audit-log"]?.credentials)
12
+ throw new Error("No credentials found for SAP Audit Log Service NG");
13
13
  }
14
14
 
15
15
  async init() {
16
- this.on('*', function (req) {
17
- const { event, data } = req
18
- return this.eventMapper(event, data)
19
- })
20
- await super.init()
16
+ this.on("*", function (req) {
17
+ const { event, data } = req;
18
+ return this.eventMapper(event, data);
19
+ });
20
+ await super.init();
21
21
  }
22
22
 
23
23
  eventMapper(event, data) {
24
24
  const known = {
25
- PersonalDataModified: () => this.logEvent('dppDataModification', data),
26
- SensitiveDataRead: () => this.logEvent('dppDataAccess', data),
27
- ConfigurationModified: () => this.logEvent('configurationChange', data),
28
- SecurityEvent: () => this.logEvent('legacySecurityWrapper', data)
29
- }
30
- const dfault = () => this.logEvent(event, data)
31
- return (known[event] ?? dfault)()
25
+ PersonalDataModified: () => this.logEvent("dppDataModification", data),
26
+ SensitiveDataRead: () => this.logEvent("dppDataAccess", data),
27
+ ConfigurationModified: () => this.logEvent("configurationChange", data),
28
+ SecurityEvent: () => this.logEvent("legacySecurityWrapper", data)
29
+ };
30
+ const dfault = () => this.logEvent(event, data);
31
+ return (known[event] ?? dfault)();
32
32
  }
33
33
 
34
34
  flattenAndSortIdObject(id) {
35
- if (!id || !Object.keys(id).length) return 'not provided'
35
+ if (!id || !Object.keys(id).length) return "not provided";
36
36
 
37
- let s = ''
38
- for (const k of Object.keys(id).sort()) s += `${k}:${id[k]} `
39
- return s.trim()
37
+ let s = "";
38
+ for (const k of Object.keys(id).sort()) s += `${k}:${id[k]} `;
39
+ return s.trim();
40
40
  }
41
41
 
42
42
  eventDataPayload(event, data) {
43
- const object = data['object'] || { type: 'not provided', id: { ID: 'not provided' } }
44
- const channel = data['channel'] || { type: 'not specified', id: 'not specified' }
45
- const subject = data['data_subject'] || { type: 'not provided', id: { ID: 'not provided' } }
46
- const attributes = data['attributes'] || [{ name: 'not provided', old: 'not provided', new: 'not provided' }]
47
- const objectId = this.flattenAndSortIdObject(object['id'])
48
- const oldValue = attributes[0]['old'] ?? ''
49
- const newValue = attributes[0]['new'] ?? ''
50
- const dataSubjectId = this.flattenAndSortIdObject(subject['id'])
43
+ const object = data["object"] || { type: "not provided", id: { ID: "not provided" } };
44
+ const channel = data["channel"] || { type: "not specified", id: "not specified" };
45
+ const subject = data["data_subject"] || { type: "not provided", id: { ID: "not provided" } };
46
+ const attributes = data["attributes"] || [
47
+ { name: "not provided", old: "not provided", new: "not provided" }
48
+ ];
49
+ const objectId = this.flattenAndSortIdObject(object["id"]);
50
+ const oldValue = attributes[0]["old"] ?? "";
51
+ const newValue = attributes[0]["new"] ?? "";
52
+ const dataSubjectId = this.flattenAndSortIdObject(subject["id"]);
51
53
 
52
54
  const known = {
53
55
  dppDataModification: {
54
- objectType: object['type'],
56
+ objectType: object["type"],
55
57
  objectId: objectId,
56
- attribute: attributes[0]['name'],
58
+ attribute: attributes[0]["name"],
57
59
  oldValue: oldValue,
58
60
  newValue: newValue,
59
- dataSubjectType: subject['type'],
61
+ dataSubjectType: subject["type"],
60
62
  dataSubjectId: dataSubjectId
61
63
  },
62
64
  dppDataAccess: {
63
- channelType: channel['type'],
64
- channelId: channel['id'],
65
- dataSubjectType: subject['type'],
65
+ channelType: channel["type"],
66
+ channelId: channel["id"],
67
+ dataSubjectType: subject["type"],
66
68
  dataSubjectId: dataSubjectId,
67
- objectType: object['type'],
69
+ objectType: object["type"],
68
70
  objectId: objectId,
69
- attribute: attributes[0]['name']
71
+ attribute: attributes[0]["name"]
70
72
  },
71
73
  configurationChange: {
72
- propertyName: attributes[0]['name'],
74
+ propertyName: attributes[0]["name"],
73
75
  oldValue: oldValue,
74
76
  newValue: newValue,
75
- objectType: object['type'],
77
+ objectType: object["type"],
76
78
  objectId: objectId
77
79
  },
78
80
  legacySecurityWrapper: {
79
81
  origEvent: JSON.stringify({
80
82
  ...data,
81
83
  data:
82
- typeof data.data === 'object' && data.data !== null && !Array.isArray(data.data)
84
+ typeof data.data === "object" && data.data !== null && !Array.isArray(data.data)
83
85
  ? JSON.stringify(data.data)
84
86
  : data.data
85
87
  })
86
88
  }
87
- }
88
- if (event in known) return known[event]
89
+ };
90
+ if (event in known) return known[event];
89
91
 
90
92
  // For unknown events, remove common audit log entry fields from the event payload
91
- if (typeof data === 'object' && data !== null) {
92
- const rest = this.removeCommonAuditLogFields(data)
93
- return rest
93
+ if (typeof data === "object" && data !== null) {
94
+ const rest = this.removeCommonAuditLogFields(data);
95
+ return rest;
94
96
  }
95
97
 
96
- return data
98
+ return data;
97
99
  }
98
100
 
99
101
  removeCommonAuditLogFields(obj) {
100
- if (typeof obj !== 'object' || obj === null) return obj
101
- const { ...rest } = obj
102
- delete rest.uuid
103
- delete rest.user
104
- delete rest.time
105
- delete rest.tenant
106
- return rest
102
+ if (typeof obj !== "object" || obj === null) return obj;
103
+ const { ...rest } = obj;
104
+ delete rest.uuid;
105
+ delete rest.user;
106
+ delete rest.time;
107
+ delete rest.tenant;
108
+ return rest;
107
109
  }
108
110
 
109
111
  eventPayload(event, data) {
110
- const tenant = cds.context?.tenant || null
111
- const timestamp = new Date().toISOString()
112
+ const tenant = cds.context?.tenant || null;
113
+ const timestamp = new Date().toISOString();
112
114
 
113
115
  const eventData = {
114
116
  id: cds.utils.uuid(),
@@ -122,12 +124,12 @@ module.exports = class AuditLog2ALSNG extends AuditLogService {
122
124
  appId: appMetadata.appID || "default app",
123
125
  infrastructure: {
124
126
  other: {
125
- runtimeType: 'Node.js'
127
+ runtimeType: "Node.js"
126
128
  }
127
129
  },
128
130
  platform: {
129
131
  other: {
130
- platformName: 'CAP'
132
+ platformName: "CAP"
131
133
  }
132
134
  }
133
135
  },
@@ -135,28 +137,28 @@ module.exports = class AuditLog2ALSNG extends AuditLogService {
135
137
  [event]: this.eventDataPayload(event, data)
136
138
  }
137
139
  }
138
- }
140
+ };
139
141
 
140
- return eventData
142
+ return eventData;
141
143
  }
142
144
 
143
145
  formatEventData(event, data) {
144
- if (event === 'legacySecurityWrapper') {
145
- return JSON.stringify([this.eventPayload(event, data)])
146
+ if (event === "legacySecurityWrapper") {
147
+ return JSON.stringify([this.eventPayload(event, data)]);
146
148
  }
147
149
 
148
150
  if (event in { dppDataModification: 1, dppDataAccess: 1, configurationChange: 1 }) {
149
- const eventData = data['attributes']?.map(attr => {
151
+ const eventData = data["attributes"]?.map((attr) => {
150
152
  return this.eventPayload(event, {
151
153
  ...data,
152
154
  attributes: [attr]
153
- })
154
- })
155
- return JSON.stringify(eventData || [])
155
+ });
156
+ });
157
+ return JSON.stringify(eventData || []);
156
158
  }
157
159
 
158
160
  // Always wrap event in an envelope for custom events
159
- return JSON.stringify([this.eventPayload(event, data)])
161
+ return JSON.stringify([this.eventPayload(event, data)]);
160
162
  }
161
163
 
162
164
  logEvent(event, data) {
@@ -165,53 +167,54 @@ module.exports = class AuditLog2ALSNG extends AuditLogService {
165
167
  throw new Error("No credentials found for SAP Audit Log Service NG");
166
168
  }
167
169
 
168
- const passphrase = credentials.keyPassphrase
169
- const url = new URL(`${credentials.url}/ingestion/v1/events`)
170
- const eventData = this.formatEventData(event, data)
170
+ const passphrase = credentials.keyPassphrase;
171
+ const url = new URL(`${credentials.url}/ingestion/v1/events`);
172
+ const eventData = this.formatEventData(event, data);
171
173
 
172
174
  const options = {
173
- method: 'POST',
175
+ method: "POST",
174
176
  headers: {
175
- 'Content-Type': 'application/json',
176
- 'Content-Length': Buffer.byteLength(eventData)
177
+ "Content-Type": "application/json",
178
+ "Content-Length": Buffer.byteLength(eventData)
177
179
  },
178
180
  key: credentials.key,
179
181
  cert: credentials.cert,
180
182
  ...(passphrase !== undefined && { passphrase })
181
- }
183
+ };
182
184
 
183
185
  return new Promise((resolve, reject) => {
184
- const req = https.request(url, options, res => {
185
- LOG.trace('🛰️ Status Code:', res.statusCode)
186
+ const req = https.request(url, options, (res) => {
187
+ LOG.trace("🛰️ Status Code:", res.statusCode);
186
188
 
187
- const chunks = []
188
- res.on('data', chunk => chunks.push(chunk))
189
+ const chunks = [];
190
+ res.on("data", (chunk) => chunks.push(chunk));
189
191
 
190
- res.on('end', () => {
191
- const { statusCode, statusMessage } = res
192
- let body = Buffer.concat(chunks).toString()
193
- if (res.headers['content-type']?.match(/json/)) body = JSON.parse(body)
192
+ res.on("end", () => {
193
+ const { statusCode, statusMessage } = res;
194
+ let body = Buffer.concat(chunks).toString();
195
+ if (res.headers["content-type"]?.match(/json/)) body = JSON.parse(body);
194
196
  if (res.statusCode >= 400) {
195
197
  // prettier-ignore
196
198
  const err = new Error(`Request failed with${statusMessage ? `: ${statusCode} - ${statusMessage}` : ` status ${statusCode}`}`)
197
- err.request = { method: options.method, url, headers: options.headers, body: data }
199
+ err.request = { method: options.method, url, headers: options.headers, body: data };
198
200
  if (err.request.headers.authorization)
199
- err.request.headers.authorization = err.request.headers.authorization.split(' ')[0] + ' ***'
200
- err.response = { statusCode, statusMessage, headers: res.headers, body }
201
- reject(err)
201
+ err.request.headers.authorization =
202
+ err.request.headers.authorization.split(" ")[0] + " ***";
203
+ err.response = { statusCode, statusMessage, headers: res.headers, body };
204
+ reject(err);
202
205
  } else {
203
- resolve(body)
206
+ resolve(body);
204
207
  }
205
- })
206
- })
208
+ });
209
+ });
207
210
 
208
- req.on('error', e => {
209
- reject(e.message)
210
- LOG.trace(`Problem with request: ${e.message}`)
211
- })
211
+ req.on("error", (e) => {
212
+ reject(e.message);
213
+ LOG.trace(`Problem with request: ${e.message}`);
214
+ });
212
215
 
213
- req.write(eventData)
214
- req.end()
215
- })
216
+ req.write(eventData);
217
+ req.end();
218
+ });
216
219
  }
217
- }
220
+ };
@@ -1,15 +1,15 @@
1
- const AuditLogService = require('./service')
1
+ const AuditLogService = require("./service");
2
2
 
3
3
  module.exports = class AuditLog2Console extends AuditLogService {
4
4
  async init() {
5
- this.on('*', function (req) {
6
- const { event, data } = req
5
+ this.on("*", function (req) {
6
+ const { event, data } = req;
7
7
 
8
8
  // eslint-disable-next-line no-console
9
- console.log(`[audit-log] - ${event}:`, data)
10
- })
9
+ console.log(`[audit-log] - ${event}:`, data);
10
+ });
11
11
 
12
12
  // call AuditLogService's init
13
- await super.init()
13
+ await super.init();
14
14
  }
15
- }
15
+ };
package/srv/log2restv2.js CHANGED
@@ -9,17 +9,13 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
9
9
  async init() {
10
10
  // credentials stuff
11
11
  const { credentials } = this.options;
12
- if (!credentials)
13
- throw new Error('No or malformed credentials for "audit-log"');
12
+ if (!credentials) throw new Error('No or malformed credentials for "audit-log"');
14
13
  if (!credentials.uaa) {
15
14
  this._plan = "standard";
16
15
  this._auth =
17
- "Basic " +
18
- Buffer.from(credentials.user + ":" + credentials.password).toString(
19
- "base64",
20
- );
16
+ "Basic " + Buffer.from(credentials.user + ":" + credentials.password).toString("base64");
21
17
  } else {
22
- this._plan = credentials.url.match(/6081/) ? "premium" : "oauth2";
18
+ this._plan = credentials.url.match(/6081|\/premium/) ? "premium" : "oauth2";
23
19
  this._tokens = new Map();
24
20
  this._provider = credentials.uaa.tenantid;
25
21
  }
@@ -31,10 +27,7 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
31
27
  if (event === "SensitiveDataRead" || event.match(/^dataAccess/i)) {
32
28
  return this._handle(data, "DATA_ACCESS");
33
29
  }
34
- if (
35
- event === "PersonalDataModified" ||
36
- event.match(/^dataModification/i)
37
- ) {
30
+ if (event === "PersonalDataModified" || event.match(/^dataModification/i)) {
38
31
  data.success = true;
39
32
  return this._handle(data, "DATA_MODIFICATION");
40
33
  }
@@ -43,8 +36,7 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
43
36
  return this._handle(data, "CONFIGURATION_CHANGE");
44
37
  }
45
38
  if (event === "SecurityEvent" || event.match(/^security/i)) {
46
- if (typeof data.data === "object")
47
- data.data = JSON.stringify(data.data);
39
+ if (typeof data.data === "object") data.data = JSON.stringify(data.data);
48
40
  return this._handle(data, "SECURITY_EVENT");
49
41
  }
50
42
 
@@ -64,10 +56,10 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
64
56
  const data = {
65
57
  grant_type: "client_credentials",
66
58
  response_type: "token",
67
- client_id: uaa.clientid,
59
+ client_id: uaa.clientid
68
60
  };
69
61
  const options = {
70
- headers: { "content-type": "application/x-www-form-urlencoded" },
62
+ headers: { "content-type": "application/x-www-form-urlencoded" }
71
63
  };
72
64
  if (tenant !== this._provider) options.headers["x-zid"] = tenant;
73
65
 
@@ -77,16 +69,9 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
77
69
  } else {
78
70
  data.client_secret = uaa.clientsecret;
79
71
  }
80
- const urlencoded = Object.keys(data).reduce((acc, cur) => {
81
- acc += (acc ? "&" : "") + cur + "=" + data[cur];
82
- return acc;
83
- }, "");
72
+ const encodedParams = new URLSearchParams(data).toString();
84
73
  try {
85
- const { access_token, expires_in } = await _post(
86
- url,
87
- urlencoded,
88
- options,
89
- );
74
+ const { access_token, expires_in } = await _post(url, encodedParams, options);
90
75
  tokens.set(tenant, access_token);
91
76
  // remove token from cache 60 seconds before it expires
92
77
  setTimeout(() => tokens.delete(tenant), (expires_in - 60) * 1000);
@@ -94,8 +79,7 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
94
79
  } catch (err) {
95
80
  LOG._trace && LOG.trace("error during token fetch:", err);
96
81
  // 401 could also mean x-zid is not valid
97
- if (String(err.response?.statusCode).match(/^4\d\d$/))
98
- err.unrecoverable = true;
82
+ if (String(err.response?.statusCode).match(/^4\d\d$/)) err.unrecoverable = true;
99
83
  throw err;
100
84
  }
101
85
  }
@@ -118,16 +102,15 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
118
102
  data.tenant ??= this._provider; //> if request has no tenant, stay in provider account
119
103
  if (data.tenant === "$PROVIDER") data.tenant = this._provider;
120
104
  headers.authorization = "Bearer " + (await this._getToken(data.tenant));
121
- data.tenant =
122
- data.tenant === this._provider ? "$PROVIDER" : "$SUBSCRIBER";
105
+ data.tenant = data.tenant === this._provider ? "$PROVIDER" : "$SUBSCRIBER";
123
106
  }
124
107
  if (LOG._debug) {
125
108
  const _headers = Object.assign({}, headers, {
126
- authorization: headers.authorization.split(" ")[0] + " ***",
109
+ authorization: headers.authorization.split(" ")[0] + " ***"
127
110
  });
128
111
  LOG.debug(
129
112
  `sending audit log to ${url} with tenant "${data.tenant}", user "${data.user}", and headers`,
130
- _headers,
113
+ _headers
131
114
  );
132
115
  }
133
116
  try {
@@ -135,10 +118,7 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
135
118
  } catch (err) {
136
119
  LOG._trace && LOG.trace("error during log send:", err);
137
120
  // 429 (rate limit) is not unrecoverable
138
- if (
139
- String(err.response?.statusCode).match(/^4\d\d$/) &&
140
- err.response?.statusCode !== 429
141
- )
121
+ if (String(err.response?.statusCode).match(/^4\d\d$/) && err.response?.statusCode !== 429)
142
122
  err.unrecoverable = true;
143
123
  throw err;
144
124
  }
@@ -149,9 +129,7 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
149
129
 
150
130
  // write the logs
151
131
  const errors = [];
152
- await Promise.all(
153
- logs.map((log) => this._send(log, path).catch((err) => errors.push(err))),
154
- );
132
+ await Promise.all(logs.map((log) => this._send(log, path).catch((err) => errors.push(err))));
155
133
  if (errors.length) throw _getErrorToThrow(errors);
156
134
  }
157
135
  };
@@ -165,14 +143,14 @@ const PATHS = {
165
143
  DATA_ACCESS: "/audit-log/v2/data-accesses",
166
144
  DATA_MODIFICATION: "/audit-log/v2/data-modifications",
167
145
  CONFIGURATION_CHANGE: "/audit-log/v2/configuration-changes",
168
- SECURITY_EVENT: "/audit-log/v2/security-events",
146
+ SECURITY_EVENT: "/audit-log/v2/security-events"
169
147
  },
170
148
  OAUTH2: {
171
149
  DATA_ACCESS: "/audit-log/oauth2/v2/data-accesses",
172
150
  DATA_MODIFICATION: "/audit-log/oauth2/v2/data-modifications",
173
151
  CONFIGURATION_CHANGE: "/audit-log/oauth2/v2/configuration-changes",
174
- SECURITY_EVENT: "/audit-log/oauth2/v2/security-events",
175
- },
152
+ SECURITY_EVENT: "/audit-log/oauth2/v2/security-events"
153
+ }
176
154
  };
177
155
 
178
156
  /*
@@ -198,13 +176,13 @@ async function _post(url, data, options) {
198
176
  : "";
199
177
  LOG._trace && LOG.trace(`Request body of failed audit-log: `, data);
200
178
  const err = new Error(
201
- `Request failed with${message ? `: ${statusCode} - ${message}` : ` status ${statusCode}`}`,
179
+ `Request failed with${message ? `: ${statusCode} - ${message}` : ` status ${statusCode}`}`
202
180
  );
203
181
  err.request = {
204
182
  method: options.method,
205
183
  url,
206
184
  headers: options.headers,
207
- body: data,
185
+ body: data
208
186
  };
209
187
  if (err.request.headers.authorization)
210
188
  err.request.headers.authorization =
package/srv/service.js CHANGED
@@ -1,28 +1,28 @@
1
- const cds = require('@sap/cds')
1
+ const cds = require("@sap/cds");
2
2
 
3
3
  module.exports = class AuditLogService extends cds.Service {
4
4
  async init() {
5
5
  // add common audit log entry fields
6
- this.before('*', req => {
7
- const { tenant, user, timestamp } = cds.context
8
- req.data.uuid ??= cds.utils.uuid()
6
+ this.before("*", (req) => {
7
+ const { tenant, user, timestamp } = cds.context;
8
+ req.data.uuid ??= cds.utils.uuid();
9
9
  // allows to specify null as tenant in order to log to provider in multi-tenant scenarios
10
10
  // NOTE: tenant: null is not a public API!
11
- if (!('tenant' in req.data)) req.data.tenant = tenant
12
- req.data.user ??= user.id
13
- req.data.time ??= timestamp
14
- })
11
+ if (!("tenant" in req.data)) req.data.tenant = tenant;
12
+ req.data.user ??= user.id;
13
+ req.data.time ??= timestamp;
14
+ });
15
15
 
16
16
  // call OutboxService's init
17
- await super.init()
17
+ await super.init();
18
18
 
19
19
  // add self-explanatory api (await audit.log/logSync(event, data))
20
- this.log = this.emit
20
+ this.log = this.emit;
21
21
  // NOTE: logSync is not a public API!
22
22
  this.logSync = (...args) => {
23
- if (cds.unboxed) return cds.unboxed(this).send(...args) //> cds >= 7.5
24
- if (this.immediate instanceof cds.Service) return this.immediate.send(...args) //> cds ~ 7.4
25
- return this.send(...args) //> cds <= 7.3
26
- }
23
+ if (cds.unboxed) return cds.unboxed(this).send(...args); //> cds >= 7.5
24
+ if (this.immediate instanceof cds.Service) return this.immediate.send(...args); //> cds ~ 7.4
25
+ return this.send(...args); //> cds <= 7.3
26
+ };
27
27
  }
28
- }
28
+ };