Skip to content

Commit 62317fc

Browse files
kalevSAPmtsvetanov071sjvans
authored
feat: Beta support for next generation SAP Audit Log Service (#162)
Co-authored-by: Miroslav Tsvetanov <Miroslav.Tsvetanov@sap.com> Co-authored-by: sjvans <30337871+sjvans@users.noreply.github.com> Co-authored-by: D050513 <sebastian.van.syckel@sap.com>
1 parent 8944529 commit 62317fc

File tree

7 files changed

+221
-1
lines changed

7 files changed

+221
-1
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ jobs:
3939
ALS_CREDS_OAUTH2: ${{ secrets.ALS_CREDS_OAUTH2 }}
4040
ALS_CREDS_STANDARD: ${{ secrets.ALS_CREDS_STANDARD }}
4141
ALS_CREDS_PREMIUM: ${{ secrets.ALS_CREDS_PREMIUM }}
42+
ALS_CREDS_NG: ${{ secrets.ALS_CREDS_NG }}

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,5 @@ package-lock.json
137137
.npmrc
138138
.babelrc
139139
.prettierrc.js
140+
.vscode/
141+
.vscode-test/

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
44
This project adheres to [Semantic Versioning](http://semver.org/).
55
The format is based on [Keep a Changelog](http://keepachangelog.com/).
66

7+
## Version 1.0.0 - 2025-07-11
8+
9+
### Added
10+
11+
- Beta support for next generation SAP Audit Log Service
12+
- Use explicit kind `audit-log-to-alsng` or alpha auto-detect kind `audit-log-to-als`
13+
714
## Version 0.9.0 - 2025-06-05
815

916
### Added

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cap-js/audit-logging",
3-
"version": "0.9.0",
3+
"version": "1.0.0",
44
"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.",
55
"repository": "cap-js/audit-logging",
66
"author": "SAP SE (https://www.sap.com)",
@@ -53,11 +53,17 @@
5353
"audit-log-to-console": {
5454
"impl": "@cap-js/audit-logging/srv/log2console"
5555
},
56+
"audit-log-to-als": {
57+
"impl": "@cap-js/audit-logging/srv/log2als"
58+
},
5659
"audit-log-to-restv2": {
5760
"impl": "@cap-js/audit-logging/srv/log2restv2",
5861
"vcap": {
5962
"label": "auditlog"
6063
}
64+
},
65+
"audit-log-to-alsng": {
66+
"impl": "@cap-js/audit-logging/srv/log2alsng"
6167
}
6268
}
6369
}

srv/log2als.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const credentials = JSON.parse(process.env.VCAP_SERVICES) || {}
2+
const isV3 = credentials['user-provided']?.some(obj => obj.tags.includes('auditlog-ng'))
3+
4+
module.exports = isV3 ? require('./log2alsng') : require('./log2restv2')

srv/log2alsng.js

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
const cds = require('@sap/cds')
2+
3+
const LOG = cds.log('audit-log')
4+
5+
const https = require('https')
6+
7+
const AuditLogService = require('./service')
8+
9+
module.exports = class AuditLog2ALSNG extends AuditLogService {
10+
constructor() {
11+
super()
12+
this._vcap = JSON.parse(process.env.VCAP_SERVICES || '{}')
13+
this._userProvided = this._vcap['user-provided']?.find(obj => obj.tags.includes('auditlog-ng')) || {}
14+
if (!this._userProvided.credentials) throw new Error('No credentials found for SAP Audit Log Service NG')
15+
this._vcapApplication = this._vcap['VCAP_APPLICATION'] || {}
16+
}
17+
18+
async init() {
19+
this.on('*', function (req) {
20+
const { event, data } = req
21+
return this.eventMapper(event, data)
22+
})
23+
await super.init()
24+
}
25+
26+
eventMapper(event, data) {
27+
return {
28+
PersonalDataModified: () => this.logEvent('dppDataModification', data),
29+
SensitiveDataRead: () => this.logEvent('dppDataAccess', data),
30+
ConfigurationModified: () => this.logEvent('configurationChange', data),
31+
SecurityEvent: () => this.logEvent('legacySecurityWrapper', data)
32+
}[event]()
33+
}
34+
35+
eventDataPayload(event, data) {
36+
const object = data['object'] || { type: 'not provided', id: { ID: 'not provided' } }
37+
const channel = data['channel'] || { type: 'not specified', id: 'not specified' }
38+
const subject = data['data_subject'] || { type: 'not provided', id: { ID: 'not provided' } }
39+
const attributes = data['attributes'] || [{ name: 'not provided', old: 'not provided', new: 'not provided' }]
40+
const objectId = object['id']?.['ID'] || 'not provided'
41+
const oldValue = attributes[0]['old'] ?? ''
42+
const newValue = attributes[0]['new'] ?? ''
43+
const dataSubjectId = subject['id']?.['ID'] || 'not provided'
44+
return {
45+
dppDataModification: {
46+
objectType: object['type'],
47+
objectId: objectId,
48+
attribute: attributes[0]['name'],
49+
oldValue: oldValue,
50+
newValue: newValue,
51+
dataSubjectType: subject['type'],
52+
dataSubjectId: dataSubjectId
53+
},
54+
dppDataAccess: {
55+
channelType: channel['type'],
56+
channelId: channel['id'],
57+
dataSubjectType: subject['type'],
58+
dataSubjectId: dataSubjectId,
59+
objectType: object['type'],
60+
objectId: objectId,
61+
attribute: attributes[0]['name']
62+
},
63+
configurationChange: {
64+
propertyName: attributes[0]['name'],
65+
oldValue: oldValue,
66+
newValue: newValue,
67+
objectType: object['type'],
68+
objectId: objectId
69+
},
70+
legacySecurityWrapper: {
71+
origEvent: JSON.stringify({
72+
...data,
73+
data:
74+
typeof data.data === 'object' && data.data !== null && !Array.isArray(data.data)
75+
? JSON.stringify(data.data)
76+
: data.data
77+
})
78+
}
79+
}[event]
80+
}
81+
82+
eventPayload(event, data) {
83+
const tenant = cds.context?.tenant || null
84+
const timestamp = new Date().toISOString()
85+
86+
const eventData = {
87+
id: cds.utils.uuid(),
88+
specversion: 1,
89+
source: `/${this._userProvided.credentials?.region}/${this._userProvided.credentials?.namespace}/${tenant}`,
90+
type: event,
91+
time: timestamp,
92+
data: {
93+
metadata: {
94+
ts: timestamp,
95+
appId: this._vcapApplication.application_id || 'default app',
96+
infrastructure: {
97+
other: {
98+
runtimeType: 'Node.js'
99+
}
100+
},
101+
platform: {
102+
other: {
103+
platformName: 'CAP'
104+
}
105+
}
106+
},
107+
data: {
108+
[event]: this.eventDataPayload(event, data)
109+
}
110+
}
111+
}
112+
113+
return eventData
114+
}
115+
116+
logEvent(event, data) {
117+
const passphrase = this._userProvided.credentials?.keyPassphrase
118+
const url = new URL(`${this._userProvided.credentials?.url}/ingestion/v1/events`)
119+
let eventData = []
120+
121+
if (event === 'legacySecurityWrapper') {
122+
eventData = JSON.stringify([this.eventPayload(event, data)])
123+
} else {
124+
eventData = data['attributes'].map(attr => {
125+
return this.eventPayload(event, {
126+
...data,
127+
attributes: [attr]
128+
})
129+
})
130+
eventData = JSON.stringify(eventData)
131+
}
132+
133+
const options = {
134+
method: 'POST',
135+
headers: {
136+
'Content-Type': 'application/json',
137+
'Content-Length': Buffer.byteLength(eventData)
138+
},
139+
key: this._userProvided.credentials?.key,
140+
cert: this._userProvided.credentials?.cert,
141+
...(passphrase !== undefined && { passphrase })
142+
}
143+
144+
return new Promise((resolve, reject) => {
145+
const req = https.request(url, options, res => {
146+
LOG.trace('🛰️ Status Code:', res.statusCode)
147+
148+
const chunks = []
149+
res.on('data', chunk => chunks.push(chunk))
150+
151+
res.on('end', () => {
152+
const { statusCode, statusMessage } = res
153+
let body = Buffer.concat(chunks).toString()
154+
if (res.headers['content-type']?.match(/json/)) body = JSON.parse(body)
155+
if (res.statusCode >= 400) {
156+
// prettier-ignore
157+
const err = new Error(`Request failed with${statusMessage ? `: ${statusCode} - ${statusMessage}` : ` status ${statusCode}`}`)
158+
err.request = { method: options.method, url, headers: options.headers, body: data }
159+
if (err.request.headers.authorization)
160+
err.request.headers.authorization = err.request.headers.authorization.split(' ')[0] + ' ***'
161+
err.response = { statusCode, statusMessage, headers: res.headers, body }
162+
reject(err)
163+
} else {
164+
resolve(body)
165+
}
166+
})
167+
})
168+
169+
req.on('error', e => {
170+
reject(e.message)
171+
LOG.trace(`Problem with request: ${e.message}`)
172+
})
173+
174+
req.write(eventData)
175+
req.end()
176+
})
177+
}
178+
}

test/integration/ng.test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const cds = require('@sap/cds')
2+
3+
const { POST } = cds.test().in(__dirname)
4+
5+
cds.env.requires['audit-log'].kind = 'audit-log-to-alsng'
6+
cds.env.requires['audit-log'].impl = '@cap-js/audit-logging/srv/log2alsng'
7+
const VCAP_SERVICES = {
8+
'user-provided': [
9+
{
10+
tags: ['auditlog-ng'],
11+
credentials: process.env.ALS_CREDS_NG && JSON.parse(process.env.ALS_CREDS_NG)
12+
}
13+
]
14+
}
15+
process.env.VCAP_SERVICES = JSON.stringify(VCAP_SERVICES)
16+
17+
describe('Log to Audit Log Service NG ', () => {
18+
if (!VCAP_SERVICES['user-provided'][0].credentials)
19+
return test.skip('Skipping tests due to missing credentials', () => {})
20+
21+
require('./tests')(POST)
22+
})

0 commit comments

Comments
 (0)