@live-change/security-service 0.2.5

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/ban.js ADDED
@@ -0,0 +1,188 @@
1
+ const app = require("@live-change/framework").app()
2
+ const definition = require('./definition.js')
3
+ const { getClientKeysStrings } = require('./utils.js')
4
+ const lcp = require('@live-change/pattern')
5
+
6
+ const banProperties = {
7
+ actions: {
8
+ type: Array,
9
+ of: {
10
+ type: String
11
+ }
12
+ },
13
+ keys: {
14
+ type: Array,
15
+ of: {
16
+ type: Object,
17
+ properties: {
18
+ key: {
19
+ type: {
20
+ String
21
+ }
22
+ },
23
+ value: {
24
+ type: {
25
+ String
26
+ }
27
+ }
28
+ }
29
+ }
30
+ },
31
+ expire: {
32
+ type: Date
33
+ },
34
+ type: {
35
+ type: String,
36
+ options: ['captcha', 'block', 'delay']
37
+ }
38
+ }
39
+
40
+ const Ban = definition.model({
41
+ name: "Ban",
42
+ properties: {
43
+ ...banProperties
44
+ },
45
+ indexes: {
46
+ bans: {
47
+ property: 'keys',
48
+ multi: true
49
+ },
50
+ actionBans: {
51
+ function: async function(input, output) {
52
+ const values = (ban) => {
53
+ const v = ban.keys.length
54
+ const w = ban.actions.length
55
+ let res = new Array(v * w)
56
+ for(let i = 0; i < v; i++) {
57
+ for(let j = 0; j < w; j++) {
58
+ const key = ban.keys[i]
59
+ res[i * v + j] = `${ban.actions[j]}:${key.key}:${key.value}`
60
+ }
61
+ }
62
+ return res
63
+ }
64
+ await input.table("securityService_Ban").onChange((obj, oldObj) => {
65
+ if(obj && oldObj) {
66
+ let pointers = obj && new Set(values(obj))
67
+ let oldPointers = oldObj && new Set(values(oldObj))
68
+ for(let pointer of pointers) {
69
+ if(!!oldPointers.has(pointer)) output.change({ id: pointer+'_'+obj.id, to: obj.id }, null)
70
+ }
71
+ for(let pointer of oldPointers) {
72
+ if(!!pointers.has(pointer)) output.change(null, { id: pointer+'_'+obj.id, to: obj.id })
73
+ }
74
+ } else if(obj) {
75
+ values(obj).forEach(v => output.change({ id: v+'_'+obj.id, to: obj.id }, null))
76
+ } else if(oldObj) {
77
+ values(oldObj).forEach(v => output.change(null, { id: v+'_'+obj.id, to: obj.id }))
78
+ }
79
+ })
80
+ }
81
+ }
82
+ }
83
+ })
84
+
85
+ definition.event({
86
+ name: "banCreated",
87
+ async execute({ ban, data }) {
88
+ Ban.create({ id: ban, ...data })
89
+ }
90
+ })
91
+
92
+ definition.event({
93
+ name: "banRemoved",
94
+ async execute({ ban }) {
95
+ Ban.delete(ban)
96
+ }
97
+ })
98
+
99
+ definition.view({
100
+ name: "myBans",
101
+ properties: {},
102
+ daoPath(params, { client, service }) {
103
+ const keys = getClientKeysStrings(client)
104
+ return multiKeyIndexQuery(keys, 'Ban_bans')
105
+ },
106
+ })
107
+
108
+ definition.view({
109
+ name: "myActionBans",
110
+ properties: {
111
+ action: {
112
+ type: String
113
+ }
114
+ },
115
+ daoPath({ action }, { client, service }) {
116
+ const keys = getClientKeysStrings(client, action + ':')
117
+ return multiKeyIndexQuery(keys, 'Ban_actionBans')
118
+ },
119
+ })
120
+
121
+ definition.trigger({
122
+ name: "securityActionBan",
123
+ properties: {
124
+ ban: {
125
+ type: Object,
126
+ properties: {
127
+ actions: banProperties.actions,
128
+ expire: {
129
+ type: String
130
+ },
131
+ type: banProperties.type
132
+ }
133
+ },
134
+ keys: {
135
+ type: Object
136
+ }
137
+ },
138
+ async execute({ keys, event, ban: { actions, expire, type } }, { service }, emit) {
139
+ const ban = app.generateUid()
140
+
141
+ console.log("SECURITY BAN!", arguments[0])
142
+
143
+ const banKeys = []
144
+ for(const key of keys) {
145
+ //keys[key] = event.keys[key]
146
+ banKeys.push({ key, value: event.keys[key] })
147
+ }
148
+ console.log("ACTION KEYS", event.keys, '=>', banKeys)
149
+
150
+ const banExpire = expire && new Date(new Date().getTime() + lcp.parseDuration(expire))
151
+
152
+ console.log("BAN KEYS", banKeys)
153
+ console.log("BAN EXPIRE", banExpire)
154
+
155
+ // service.trigger({
156
+ // type: 'createTimer',
157
+ // timer: {
158
+ // timestamp: banExpire.getTime() + 1000,
159
+ // service: 'security',
160
+ // trigger: {
161
+ // type: 'removeExpiredBan',
162
+ // ban
163
+ // }
164
+ // }
165
+ // })
166
+
167
+ // emit({
168
+ // type: "banCreated",
169
+ // ban,
170
+ // data: { actions, banKeys, expire, type }
171
+ // })
172
+ }
173
+ })
174
+
175
+ definition.trigger({
176
+ name: "removeExpiredBan",
177
+ properties: {
178
+ ...banProperties
179
+ },
180
+ async execute({ ban }, {client, service}, emit) {
181
+ emit({
182
+ type: "banRemoved",
183
+ ban
184
+ })
185
+ }
186
+ })
187
+
188
+ module.exports = { Ban }
package/definition.js ADDED
@@ -0,0 +1,8 @@
1
+ const app = require("@live-change/framework").app()
2
+
3
+ const definition = app.createServiceDefinition({
4
+ name: "securityService"
5
+ })
6
+ const config = definition.config
7
+
8
+ module.exports = definition
package/event.js ADDED
@@ -0,0 +1,254 @@
1
+ const crypto = require('crypto')
2
+ const app = require("@live-change/framework").app()
3
+ const definition = require('./definition.js')
4
+ const { getClientKeysObject } = require('./utils.js')
5
+
6
+ const lcp = require('@live-change/pattern')
7
+ const lcpDb = require('@live-change/pattern-db')
8
+ const { request } = require('http')
9
+
10
+ const securityPatterns = definition.config.patterns
11
+ const relationsStore = lcpDb.relationsStore(app.dao, app.databaseName, 'securityService_relations')
12
+ lcp.prepareModelForLive(securityPatterns)
13
+ //console.log("SECURITY PATTERNS", securityPatterns)
14
+
15
+ const securityCounters = definition.config.counters
16
+
17
+ definition.beforeStart(service => {
18
+ relationsStore.createTable()
19
+ })
20
+
21
+ const eventProperties = {
22
+ type: {
23
+ type: String
24
+ },
25
+ keys: {
26
+ type: Array,
27
+ of: {
28
+ type: Object,
29
+ properties: {
30
+ key: {
31
+ type: {
32
+ String
33
+ }
34
+ },
35
+ value: {
36
+ type: {
37
+ String
38
+ }
39
+ }
40
+ }
41
+ }
42
+ },
43
+ timestamp: {
44
+ type: Date
45
+ }
46
+ }
47
+
48
+ const Event = definition.model({
49
+ name: "Event",
50
+ properties: {
51
+ ...eventProperties
52
+ },
53
+ indexes: {
54
+ byTypeAndTimestamp: {
55
+ property: ['type', 'timestamp']
56
+ }
57
+ }
58
+ })
59
+
60
+ definition.event({
61
+ name: "securityEvent",
62
+ async execute({ event, eventType, keys, newRelations, canceledRelations }) {
63
+ await Event.create({ id: event, type: eventType, keys, timestamp: new Date() })
64
+ const promises = []
65
+ for(const relation of newRelations) {
66
+ promises.push(relationsStore.saveRelation(relation))
67
+ }
68
+ for(const relation of canceledRelations) {
69
+ promises.push(relationsStore.removeRelation(relation))
70
+ }
71
+ await Promise.all(promises)
72
+ }
73
+ })
74
+
75
+
76
+ async function processSecurityPatterns(event, time, service) {
77
+ const changes = await lcp.processEvent({...event, time}, securityPatterns, relationsStore.getRelations)
78
+ console.log("PROCESSED EVENT PATTERNS", event, '=>', changes)
79
+ const { newRelations, canceledRelations, actions } = changes
80
+
81
+ for (const relation of newRelations) {
82
+ if (relation.prev) {
83
+ for (const prev of relation.prev) {
84
+ if (prev.relation) prev.relation.prev = null
85
+ }
86
+ }
87
+ }
88
+
89
+ let promises = []
90
+ for (const relation of newRelations) {
91
+ if (relation.type == 'eq') {
92
+ // will be handled by event
93
+ } else if (relation.type == 'timeout') {
94
+ const id = crypto.createHash('md5').update(event.id + relation.relation).digest('hex')
95
+ promises.push(service.trigger({
96
+ type: "createTimer",
97
+ timer: {
98
+ id,
99
+ timestamp: relation.time,
100
+ service: 'security',
101
+ trigger: {
102
+ type: 'handleTimeout',
103
+ timeout: relation
104
+ }
105
+ }
106
+ }))
107
+ } else {
108
+ throw new Error(`Relation type ${JSON.stringify(relation.type)} not supported`)
109
+ }
110
+ }
111
+
112
+ await Promise.all(promises)
113
+ promises = []
114
+
115
+ for (const relation of canceledRelations) {
116
+ const relationModel = securityPatterns.relations[relation.relation]
117
+ if (relationModel.eq) {
118
+ // will be handled by event
119
+ } else if (relationModel.wait) {
120
+ const id = crypto.createHash('md5').update(event.id + relation.relation).digest('hex')
121
+ promises.push(service.trigger({
122
+ type: "cancelTimerIfExists",
123
+ timer: id
124
+ }))
125
+ } else {
126
+ throw new Error(`Relation ${JSON.stringify(relation.relation)} not supported`)
127
+ }
128
+ }
129
+
130
+ await Promise.all(promises)
131
+ return { newRelations, canceledRelations, actions }
132
+ }
133
+
134
+ async function processSecurityCounters(event, timestamp, service) {
135
+ const now = Date.now()
136
+ const actions = []
137
+ const counters = securityCounters.filter(counter => counter.match.includes(event.type))
138
+ if(counters.length == 0) return
139
+ const counterEventsRequests = []
140
+ for(const counter of counters) {
141
+ const duration = lcp.parseDuration(counter.duration)
142
+ for(const eventType of counter.match) {
143
+ const request = counterEventsRequests.find(req => req.type == eventType)
144
+ if(request) {
145
+ request.max = Math.max(request.max, counter.max)
146
+ request.duration = Math.max(request.duration, duration)
147
+ } else {
148
+ counterEventsRequests.push({
149
+ type: eventType,
150
+ max: counter.max,
151
+ duration
152
+ })
153
+ }
154
+ }
155
+ }
156
+ console.log("COUNTER EVENTS REQUESTS", counterEventsRequests)
157
+ const counterEvents = await Promise.all(counterEventsRequests.map(async request => ({
158
+ type: request.type,
159
+ events: await Event.indexRangeGet('byTypeAndTimestamp',{
160
+ gt: `"${request.type}":"${(new Date(now - request.duration - 1000)).toISOString()}"`,
161
+ lt: `"${request.type}":\xFF`,
162
+ reverse: true,
163
+ limit: request.max
164
+ })
165
+ })))
166
+ console.log("COUNTER EVENTS", counterEvents)
167
+ for(const counter of counters) {
168
+ const duration = lcp.parseDuration(counter.duration)
169
+ const fromTime = (new Date(now - duration)).toISOString()
170
+ let count = 0
171
+ for(const events of counterEvents) {
172
+ if(!counter.match.includes(events.type)) continue
173
+ for(const event of events.events) {
174
+ if(event.timestamp > fromTime) {
175
+ count ++
176
+ }
177
+ }
178
+ }
179
+ if(count + 1 > counter.max) { // +1 for the new event, not added yet
180
+ console.log("COUNTER FIRE", counter)
181
+ actions.push(...counter.actions)
182
+ }
183
+ }
184
+ return { actions }
185
+ }
186
+
187
+ definition.trigger({
188
+ name: "securityEvent",
189
+ properties: {
190
+ event: {
191
+ type: Object,
192
+ properties: {
193
+ type: {
194
+ type: String
195
+ },
196
+ keys: {
197
+ type: Object
198
+ },
199
+ properties: {
200
+ type: Object
201
+ }
202
+ }
203
+ },
204
+ client: {
205
+ type: Object
206
+ }
207
+ },
208
+ async execute({ event, client, timestamp }, { service }, emit) {
209
+ console.log("SECURITY EVENT TRIGGERED", arguments[0])
210
+ event.id = event.id || app.generateUid()
211
+ event.time = event.time || timestamp
212
+ const time = (typeof event.time == 'number') ? event.time : new Date(event.time).getTime()
213
+
214
+ if(client) {
215
+ event.keys = { ...getClientKeysObject(client), ...event.keys }
216
+ }
217
+
218
+ console.log("PROCESS EVENT", event)
219
+ let [
220
+ { newRelations, canceledRelations, actions: patternsActions },
221
+ { actions: countersActions }
222
+ ] = await Promise.all([
223
+ processSecurityPatterns(event, time, service),
224
+ processSecurityCounters(event, time, service)
225
+ ])
226
+
227
+ let promises = []
228
+ const actions = patternsActions.concat(countersActions)
229
+
230
+ for(const action of actions) {
231
+ const actionTypeUpperCase = action.type[0].toUpperCase() + action.type.slice(1)
232
+ console.log("ACTION", JSON.stringify(action, null, ' '))
233
+ promises.push(service.trigger({
234
+ ...action,
235
+ event,
236
+ type: 'securityAction' + actionTypeUpperCase
237
+ }))
238
+ }
239
+
240
+ await Promise.all(promises)
241
+
242
+ emit({
243
+ type: "securityEvent",
244
+ event: event.id,
245
+ eventType: event.type,
246
+ keys: event.keys,
247
+ actions,
248
+ newRelations: newRelations.filter(rel => rel.type == 'eq'),
249
+ canceledRelations: canceledRelations.filter(rel => rel.type == 'eq')
250
+ })
251
+
252
+ }
253
+ })
254
+
package/index.js ADDED
@@ -0,0 +1,9 @@
1
+ const app = require("@live-change/framework").app()
2
+
3
+ const definition = require('./definition.js')
4
+
5
+ require('./ban.js')
6
+ require('./event.js')
7
+ require('./secured.js')
8
+
9
+ module.exports = definition
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@live-change/security-service",
3
+ "version": "0.2.5",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "NODE_ENV=test tape tests/*"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/live-change/live-change-services.git"
12
+ },
13
+ "license": "MIT",
14
+ "bugs": {
15
+ "url": "https://github.com/live-change/live-change-services/issues"
16
+ },
17
+ "homepage": "https://github.com/live-change/live-change-services",
18
+ "author": {
19
+ "email": "michal@laszczewski.pl",
20
+ "name": "Michał Łaszczewski",
21
+ "url": "https://www.viamage.com/"
22
+ },
23
+ "dependencies": {
24
+ "@live-change/framework": "^0.5.7",
25
+ "@live-change/pattern": "^0.2.1",
26
+ "@live-change/pattern-db": "^0.2.2",
27
+ "nodemailer": "^6.7.2"
28
+ },
29
+ "gitHead": "7691357c7569a18373668691eb6a81a9e161194d"
30
+ }
package/secured.js ADDED
@@ -0,0 +1,50 @@
1
+ const definition = require('./definition.js')
2
+ const { getClientKeysObject, getClientKeysStrings, multiKeyIndexQuery } = require('./utils.js')
3
+
4
+ definition.processor(function(service, app) {
5
+
6
+ for(let actionName in service.actions) {
7
+ const action = service.actions[actionName]
8
+ if(!action.secured) continue
9
+ const config = action.secured
10
+
11
+ console.log("SECURED", service.name, action.name)
12
+
13
+ const oldExec = action.execute
14
+ action.execute = async (...args) => {
15
+ const [ properties, context, emit ] = args
16
+ const { client } = context
17
+ oldExec.apply(action, args)
18
+ }
19
+
20
+ /// TODO: detect bans, block actions
21
+ /// TODO: detect associated events
22
+ /// TODO: report security violation if succeded
23
+ /// TODO: report security violation if failed - another event
24
+ /// TODO: additional validation based on ban type(captcha)
25
+ /// TODO: additional delay based on ban type
26
+ }
27
+
28
+ for(let triggerName in service.actions) {
29
+ const trigger = service.actions[triggerName]
30
+ if(!trigger.secured) continue
31
+ const config = trigger.secured
32
+
33
+ console.log("SECURED TRIGGER", service.name, trigger.name)
34
+
35
+ const oldExec = trigger.execute
36
+ trigger.execute = async (...args) => {
37
+ const [ properties, context, emit ] = args
38
+ const { client, ...otherProperties } = properties
39
+ oldExec.apply(trigger, args)
40
+ }
41
+
42
+ /// TODO: detect bans, block triggers
43
+ /// TODO: detect associated events
44
+ /// TODO: report security violation if succeded
45
+ /// TODO: report security violation if failed - another event
46
+ /// TODO: additional validation based on ban type(captcha)
47
+ /// TODO: additional delay based on ban type
48
+ }
49
+
50
+ })
package/utils.js ADDED
@@ -0,0 +1,85 @@
1
+ const definition = require('./definition.js')
2
+
3
+ const clientKeys = definition.config.clientKeys
4
+
5
+ function multiKeyIndexQuery(keys, indexName) {
6
+ return ['database', 'query', database, `(${
7
+ async (input, output, { keys, indexName, tableName }) => {
8
+ const objectStates = new Map()
9
+ async function mapper(res) {
10
+ input.table(tableName).object(res.to).get()
11
+ }
12
+ async function onIndexChange(obj, oldObj) {
13
+ if(obj && !oldObj) {
14
+ const data = await mapper(obj)
15
+ if(data) output.change(data, null)
16
+ }
17
+ if(obj && obj.to) {
18
+ let objectState = objectStates.get(obj.to)
19
+ if(!objectState) {
20
+ objectState = { data: undefined, refs: 1 }
21
+ objectState.reader = input.table(tableName).object(obj.to)
22
+ const ind = obj
23
+ objectState.observer = await objectState.reader.onChange(async obj => {
24
+ const data = obj
25
+ const oldData = objectState.data
26
+ output.change(data, oldData)
27
+ if(data) {
28
+ objectState.data = obj
29
+ } else if(oldObj) {
30
+ objectState.data = null
31
+ }
32
+ })
33
+ objectStates.set(ind.to, objectState)
34
+ } else if(!oldObj || oldObj.to != obj.to) {
35
+ objectState.refs ++
36
+ }
37
+ }
38
+ if(oldObj && oldObj.to && (!obj || obj.to != oldObj.to)) {
39
+ let objectState = objectStates.get(oldObj.to)
40
+ if(objectState) {
41
+ objectState.refs --
42
+ if(objectState.refs <= 0) {
43
+ objectState.reader.unobserve(objectState.observer)
44
+ objectStates.delete(oldObj.to)
45
+ output.change(null, objectState.data)
46
+ }
47
+ }
48
+ }
49
+ }
50
+ await Promise.all(keys.map(async (encodedKey) => {
51
+ const range = {
52
+ gte: encodedKey + '_',
53
+ lte: encodedKey + "_\xFF\xFF\xFF\xFF"
54
+ }
55
+ await (await input.index(indexName)).range(range).onChange(onIndexChange)
56
+ }))
57
+ }
58
+ })`, { keys, indexName, tableName: Ban.tableName }]
59
+ }
60
+
61
+ function getClientKeysStrings(client, prefix = '') {
62
+ if(clientKeys) {
63
+ return clientKeys(client).map(k => prefix + k.key + ':' + k.value)
64
+ } else {
65
+ const keys = []
66
+ for(let key in client) {
67
+ keys.push(prefix + key + ':' + client[key])
68
+ }
69
+ return keys
70
+ }
71
+ }
72
+
73
+ function getClientKeysObject(client, prefix = '') {
74
+ if(clientKeys) {
75
+ const obj = {}
76
+ for(const { key, value } of clientKeys(client)) {
77
+ obj[key] = value
78
+ }
79
+ return obj
80
+ } else {
81
+ return client
82
+ }
83
+ }
84
+
85
+ module.exports = { multiKeyIndexQuery, getClientKeysStrings, getClientKeysObject }