@cap-js/audit-logging 1.1.1 → 1.2.1
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/README.md +7 -10
- package/cds-plugin.js +56 -44
- package/lib/_relation.js +103 -100
- package/lib/access.js +38 -37
- package/lib/modification.js +117 -108
- package/lib/utils.js +200 -157
- package/package.json +14 -11
- package/srv/log2als.js +5 -3
- package/srv/log2alsng.js +111 -105
- package/srv/log2console.js +7 -7
- package/srv/log2restv2.js +136 -109
- package/srv/service.js +17 -16
package/srv/log2alsng.js
CHANGED
|
@@ -1,135 +1,135 @@
|
|
|
1
|
-
const cds = require(
|
|
2
|
-
const LOG = cds.log(
|
|
1
|
+
const cds = require("@sap/cds");
|
|
2
|
+
const LOG = cds.log("audit-log");
|
|
3
|
+
const { appMetadata } = require("../lib/utils");
|
|
4
|
+
const https = require("https");
|
|
3
5
|
|
|
4
|
-
const
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
if (!this._userProvided.credentials) throw new Error('No credentials found for SAP Audit Log Service NG')
|
|
14
|
-
this._vcapApplication = JSON.parse(process.env.VCAP_APPLICATION || '{}')
|
|
10
|
+
super();
|
|
11
|
+
if (!cds.env.requires["audit-log"]?.credentials)
|
|
12
|
+
throw new Error("No credentials found for SAP Audit Log Service NG");
|
|
15
13
|
}
|
|
16
14
|
|
|
17
15
|
async init() {
|
|
18
|
-
this.on(
|
|
19
|
-
const { event, data } = req
|
|
20
|
-
return this.eventMapper(event, data)
|
|
21
|
-
})
|
|
22
|
-
await super.init()
|
|
16
|
+
this.on("*", function (req) {
|
|
17
|
+
const { event, data } = req;
|
|
18
|
+
return this.eventMapper(event, data);
|
|
19
|
+
});
|
|
20
|
+
await super.init();
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
eventMapper(event, data) {
|
|
26
24
|
const known = {
|
|
27
|
-
PersonalDataModified: () => this.logEvent(
|
|
28
|
-
SensitiveDataRead: () => this.logEvent(
|
|
29
|
-
ConfigurationModified: () => this.logEvent(
|
|
30
|
-
SecurityEvent: () => this.logEvent(
|
|
31
|
-
}
|
|
32
|
-
const dfault = () => this.logEvent(event, data)
|
|
33
|
-
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)();
|
|
34
32
|
}
|
|
35
33
|
|
|
36
34
|
flattenAndSortIdObject(id) {
|
|
37
|
-
if (!id || !Object.keys(id).length) return
|
|
35
|
+
if (!id || !Object.keys(id).length) return "not provided";
|
|
38
36
|
|
|
39
|
-
let s =
|
|
40
|
-
for (const k of Object.keys(id).sort()) s += `${k}:${id[k]}
|
|
41
|
-
return s.trim()
|
|
37
|
+
let s = "";
|
|
38
|
+
for (const k of Object.keys(id).sort()) s += `${k}:${id[k]} `;
|
|
39
|
+
return s.trim();
|
|
42
40
|
}
|
|
43
41
|
|
|
44
42
|
eventDataPayload(event, data) {
|
|
45
|
-
const object = data[
|
|
46
|
-
const channel = data[
|
|
47
|
-
const subject = data[
|
|
48
|
-
const attributes = data[
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
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"]);
|
|
53
53
|
|
|
54
54
|
const known = {
|
|
55
55
|
dppDataModification: {
|
|
56
|
-
objectType: object[
|
|
56
|
+
objectType: object["type"],
|
|
57
57
|
objectId: objectId,
|
|
58
|
-
attribute: attributes[0][
|
|
58
|
+
attribute: attributes[0]["name"],
|
|
59
59
|
oldValue: oldValue,
|
|
60
60
|
newValue: newValue,
|
|
61
|
-
dataSubjectType: subject[
|
|
61
|
+
dataSubjectType: subject["type"],
|
|
62
62
|
dataSubjectId: dataSubjectId
|
|
63
63
|
},
|
|
64
64
|
dppDataAccess: {
|
|
65
|
-
channelType: channel[
|
|
66
|
-
channelId: channel[
|
|
67
|
-
dataSubjectType: subject[
|
|
65
|
+
channelType: channel["type"],
|
|
66
|
+
channelId: channel["id"],
|
|
67
|
+
dataSubjectType: subject["type"],
|
|
68
68
|
dataSubjectId: dataSubjectId,
|
|
69
|
-
objectType: object[
|
|
69
|
+
objectType: object["type"],
|
|
70
70
|
objectId: objectId,
|
|
71
|
-
attribute: attributes[0][
|
|
71
|
+
attribute: attributes[0]["name"]
|
|
72
72
|
},
|
|
73
73
|
configurationChange: {
|
|
74
|
-
propertyName: attributes[0][
|
|
74
|
+
propertyName: attributes[0]["name"],
|
|
75
75
|
oldValue: oldValue,
|
|
76
76
|
newValue: newValue,
|
|
77
|
-
objectType: object[
|
|
77
|
+
objectType: object["type"],
|
|
78
78
|
objectId: objectId
|
|
79
79
|
},
|
|
80
80
|
legacySecurityWrapper: {
|
|
81
81
|
origEvent: JSON.stringify({
|
|
82
82
|
...data,
|
|
83
83
|
data:
|
|
84
|
-
typeof data.data ===
|
|
84
|
+
typeof data.data === "object" && data.data !== null && !Array.isArray(data.data)
|
|
85
85
|
? JSON.stringify(data.data)
|
|
86
86
|
: data.data
|
|
87
87
|
})
|
|
88
88
|
}
|
|
89
|
-
}
|
|
90
|
-
if (event in known) return known[event]
|
|
89
|
+
};
|
|
90
|
+
if (event in known) return known[event];
|
|
91
91
|
|
|
92
92
|
// For unknown events, remove common audit log entry fields from the event payload
|
|
93
|
-
if (typeof data ===
|
|
94
|
-
const rest = this.removeCommonAuditLogFields(data)
|
|
95
|
-
return rest
|
|
93
|
+
if (typeof data === "object" && data !== null) {
|
|
94
|
+
const rest = this.removeCommonAuditLogFields(data);
|
|
95
|
+
return rest;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
return data
|
|
98
|
+
return data;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
removeCommonAuditLogFields(obj) {
|
|
102
|
-
if (typeof obj !==
|
|
103
|
-
const { ...rest } = obj
|
|
104
|
-
delete rest.uuid
|
|
105
|
-
delete rest.user
|
|
106
|
-
delete rest.time
|
|
107
|
-
delete rest.tenant
|
|
108
|
-
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;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
eventPayload(event, data) {
|
|
112
|
-
const tenant = cds.context?.tenant || null
|
|
113
|
-
const timestamp = new Date().toISOString()
|
|
112
|
+
const tenant = cds.context?.tenant || null;
|
|
113
|
+
const timestamp = new Date().toISOString();
|
|
114
114
|
|
|
115
115
|
const eventData = {
|
|
116
116
|
id: cds.utils.uuid(),
|
|
117
117
|
specversion: 1,
|
|
118
|
-
source: `/${
|
|
118
|
+
source: `/${cds.env.requires["audit-log"].credentials?.region}/${cds.env.requires["audit-log"].credentials?.namespace}/${tenant}`,
|
|
119
119
|
type: event,
|
|
120
120
|
time: timestamp,
|
|
121
121
|
data: {
|
|
122
122
|
metadata: {
|
|
123
123
|
ts: timestamp,
|
|
124
|
-
appId:
|
|
124
|
+
appId: appMetadata.appID || "default app",
|
|
125
125
|
infrastructure: {
|
|
126
126
|
other: {
|
|
127
|
-
runtimeType:
|
|
127
|
+
runtimeType: "Node.js"
|
|
128
128
|
}
|
|
129
129
|
},
|
|
130
130
|
platform: {
|
|
131
131
|
other: {
|
|
132
|
-
platformName:
|
|
132
|
+
platformName: "CAP"
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
},
|
|
@@ -137,78 +137,84 @@ module.exports = class AuditLog2ALSNG extends AuditLogService {
|
|
|
137
137
|
[event]: this.eventDataPayload(event, data)
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
-
}
|
|
140
|
+
};
|
|
141
141
|
|
|
142
|
-
return eventData
|
|
142
|
+
return eventData;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
formatEventData(event, data) {
|
|
146
|
-
if (event ===
|
|
147
|
-
return JSON.stringify([this.eventPayload(event, data)])
|
|
146
|
+
if (event === "legacySecurityWrapper") {
|
|
147
|
+
return JSON.stringify([this.eventPayload(event, data)]);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
if (event in { dppDataModification: 1, dppDataAccess: 1, configurationChange: 1 }) {
|
|
151
|
-
const eventData = data[
|
|
151
|
+
const eventData = data["attributes"]?.map((attr) => {
|
|
152
152
|
return this.eventPayload(event, {
|
|
153
153
|
...data,
|
|
154
154
|
attributes: [attr]
|
|
155
|
-
})
|
|
156
|
-
})
|
|
157
|
-
return JSON.stringify(eventData || [])
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
return JSON.stringify(eventData || []);
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
// Always wrap event in an envelope for custom events
|
|
161
|
-
return JSON.stringify([this.eventPayload(event, data)])
|
|
161
|
+
return JSON.stringify([this.eventPayload(event, data)]);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
logEvent(event, data) {
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
const credentials = cds.env.requires["audit-log"].credentials;
|
|
166
|
+
if (!credentials) {
|
|
167
|
+
throw new Error("No credentials found for SAP Audit Log Service NG");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const passphrase = credentials.keyPassphrase;
|
|
171
|
+
const url = new URL(`${credentials.url}/ingestion/v1/events`);
|
|
172
|
+
const eventData = this.formatEventData(event, data);
|
|
168
173
|
|
|
169
174
|
const options = {
|
|
170
|
-
method:
|
|
175
|
+
method: "POST",
|
|
171
176
|
headers: {
|
|
172
|
-
|
|
173
|
-
|
|
177
|
+
"Content-Type": "application/json",
|
|
178
|
+
"Content-Length": Buffer.byteLength(eventData)
|
|
174
179
|
},
|
|
175
|
-
key:
|
|
176
|
-
cert:
|
|
180
|
+
key: credentials.key,
|
|
181
|
+
cert: credentials.cert,
|
|
177
182
|
...(passphrase !== undefined && { passphrase })
|
|
178
|
-
}
|
|
183
|
+
};
|
|
179
184
|
|
|
180
185
|
return new Promise((resolve, reject) => {
|
|
181
|
-
const req = https.request(url, options, res => {
|
|
182
|
-
LOG.trace(
|
|
186
|
+
const req = https.request(url, options, (res) => {
|
|
187
|
+
LOG.trace("🛰️ Status Code:", res.statusCode);
|
|
183
188
|
|
|
184
|
-
const chunks = []
|
|
185
|
-
res.on(
|
|
189
|
+
const chunks = [];
|
|
190
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
186
191
|
|
|
187
|
-
res.on(
|
|
188
|
-
const { statusCode, statusMessage } = res
|
|
189
|
-
let body = Buffer.concat(chunks).toString()
|
|
190
|
-
if (res.headers[
|
|
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);
|
|
191
196
|
if (res.statusCode >= 400) {
|
|
192
197
|
// prettier-ignore
|
|
193
198
|
const err = new Error(`Request failed with${statusMessage ? `: ${statusCode} - ${statusMessage}` : ` status ${statusCode}`}`)
|
|
194
|
-
err.request = { method: options.method, url, headers: options.headers, body: data }
|
|
199
|
+
err.request = { method: options.method, url, headers: options.headers, body: data };
|
|
195
200
|
if (err.request.headers.authorization)
|
|
196
|
-
err.request.headers.authorization =
|
|
197
|
-
|
|
198
|
-
|
|
201
|
+
err.request.headers.authorization =
|
|
202
|
+
err.request.headers.authorization.split(" ")[0] + " ***";
|
|
203
|
+
err.response = { statusCode, statusMessage, headers: res.headers, body };
|
|
204
|
+
reject(err);
|
|
199
205
|
} else {
|
|
200
|
-
resolve(body)
|
|
206
|
+
resolve(body);
|
|
201
207
|
}
|
|
202
|
-
})
|
|
203
|
-
})
|
|
208
|
+
});
|
|
209
|
+
});
|
|
204
210
|
|
|
205
|
-
req.on(
|
|
206
|
-
reject(e.message)
|
|
207
|
-
LOG.trace(`Problem with request: ${e.message}`)
|
|
208
|
-
})
|
|
211
|
+
req.on("error", (e) => {
|
|
212
|
+
reject(e.message);
|
|
213
|
+
LOG.trace(`Problem with request: ${e.message}`);
|
|
214
|
+
});
|
|
209
215
|
|
|
210
|
-
req.write(eventData)
|
|
211
|
-
req.end()
|
|
212
|
-
})
|
|
216
|
+
req.write(eventData);
|
|
217
|
+
req.end();
|
|
218
|
+
});
|
|
213
219
|
}
|
|
214
|
-
}
|
|
220
|
+
};
|
package/srv/log2console.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
const AuditLogService = require(
|
|
1
|
+
const AuditLogService = require("./service");
|
|
2
2
|
|
|
3
3
|
module.exports = class AuditLog2Console extends AuditLogService {
|
|
4
4
|
async init() {
|
|
5
|
-
this.on(
|
|
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
|
+
};
|