Skip to content

Commit e061c80

Browse files
author
Marco Franceschi
committed
feat: Added DataProcessors to handle mutations depending on the StorageEngine
1 parent 6c7c5f5 commit e061c80

File tree

8 files changed

+212
-42
lines changed

8 files changed

+212
-42
lines changed

src/plugins/policyPack/index.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
import RulesEngine from '../../rules-engine'
1414
import { Result, Rule, Severity } from '../../rules-engine/types'
1515
import Plugin, { ConfiguredPlugin, PluginManager } from '../types'
16+
import DgraphDataProcessor from '../../rules-engine/data-processors/dgraph-data-processor'
17+
import DataProcessor from '../../rules-engine/data-processors/data-processor'
1618

1719
export default class PolicyPackPlugin extends Plugin {
1820
constructor({
@@ -61,6 +63,10 @@ export default class PolicyPackPlugin extends Plugin {
6163
}
6264
} = {}
6365

66+
private dataProcessors: {
67+
[name: string]: DataProcessor
68+
} = {}
69+
6470
private async getPolicyPackPackage({
6571
policyPack,
6672
pluginManager,
@@ -179,6 +185,24 @@ export default class PolicyPackPlugin extends Plugin {
179185
return findings
180186
}
181187

188+
// TODO: Generalize data processor moving storage module to SDK with its interfaces
189+
private getDataProcessor({
190+
entity,
191+
provider,
192+
}: {
193+
entity: string
194+
provider: string
195+
}): DataProcessor {
196+
const dataProcessorKey = `${provider}${entity}`
197+
if (this.dataProcessors[dataProcessorKey]) {
198+
return this.dataProcessors[dataProcessorKey]
199+
}
200+
201+
const dataProcessor = new DgraphDataProcessor(provider, entity)
202+
this.dataProcessors[dataProcessorKey] = dataProcessor
203+
return dataProcessor
204+
}
205+
182206
async configure(
183207
pluginManager: PluginManager,
184208
plugins: ConfiguredPlugin[]
@@ -263,8 +287,6 @@ export default class PolicyPackPlugin extends Plugin {
263287
schemaMap?: SchemaMap
264288
}) => void
265289
}): Promise<any> {
266-
console.log('storage type', typeof storageEngine, storageEngine)
267-
268290
!isEmpty(this.policyPacksPlugins) &&
269291
this.logger.info(
270292
`Beginning ${chalk.italic.green('RULES')} for ${this.provider.name}`
@@ -296,8 +318,14 @@ export default class PolicyPackPlugin extends Plugin {
296318
storageEngine,
297319
})
298320

321+
// Data Processor
322+
const dataProcessor = this.getDataProcessor({
323+
entity,
324+
provider: this.provider.name,
325+
})
326+
299327
// Prepare mutations
300-
const mutations = engine?.prepareMutations(findings)
328+
const mutations = dataProcessor.prepareMutations(findings)
301329

302330
// Save connections
303331
processConnectionsBetweenEntities({
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Entity } from '../../types'
2+
import { RuleFinding } from '../types'
3+
4+
export default interface DataProcessor {
5+
/**
6+
* Transforms RuleFinding array into a mutation array for GraphQL
7+
* @param findings resulted findings during rules execution
8+
* @returns {Entity[]} Array of generated mutations
9+
*/
10+
prepareMutations: (findings: RuleFinding[]) => Entity[]
11+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import groupBy from 'lodash/groupBy'
2+
import isEmpty from 'lodash/isEmpty'
3+
import { Entity } from '../../types'
4+
import { RuleFinding } from '../types'
5+
import DataProcessor from './data-processor'
6+
7+
export default class DgraphDataProcessor implements DataProcessor {
8+
private readonly providerName
9+
10+
private readonly entityName
11+
12+
constructor(providerName: string, entityName: string) {
13+
this.providerName = providerName
14+
this.entityName = entityName
15+
}
16+
17+
/**
18+
* Prepare the mutations for overall provider findings
19+
* @param findings RuleFinding array
20+
* @returns A formatted Entity array
21+
*/
22+
private prepareProviderMutations = (
23+
findings: RuleFinding[] = []
24+
): Entity[] => {
25+
// Prepare provider schema connections
26+
return [
27+
{
28+
name: `${this.providerName}Findings`,
29+
mutation: `
30+
mutation($input: [Add${this.providerName}FindingsInput!]!) {
31+
add${this.providerName}Findings(input: $input, upsert: true) {
32+
numUids
33+
}
34+
}
35+
`,
36+
data: {
37+
id: `${this.providerName}-provider`,
38+
},
39+
},
40+
{
41+
name: `${this.providerName}Findings`,
42+
mutation: `mutation update${this.providerName}Findings($input: Update${this.providerName}FindingsInput!) {
43+
update${this.providerName}Findings(input: $input) {
44+
numUids
45+
}
46+
}
47+
`,
48+
data: {
49+
filter: {
50+
id: { eq: `${this.providerName}-provider` },
51+
},
52+
set: {
53+
[`${this.entityName}Findings`]: findings.map(
54+
({ typename, ...rest }) => ({ ...rest })
55+
),
56+
},
57+
},
58+
},
59+
]
60+
}
61+
62+
private prepareProcessedMutations(findingsByType: {
63+
[resource: string]: RuleFinding[]
64+
}): Entity[] {
65+
const mutations = []
66+
67+
for (const findingType in findingsByType) {
68+
if (!isEmpty(findingType)) {
69+
// Group Findings by resource
70+
const findingsByResource = groupBy(
71+
findingsByType[findingType],
72+
'resourceId'
73+
)
74+
75+
for (const resource in findingsByResource) {
76+
if (resource) {
77+
const data = (
78+
(findingsByResource[resource] as RuleFinding[]) || []
79+
).map(({ typename, ...properties }) => properties)
80+
81+
// Create dynamically update mutations by resource
82+
const updateMutation = {
83+
name: `${this.providerName}${this.entityName}Findings`,
84+
mutation: `mutation update${findingType}($input: Update${findingType}Input!) {
85+
update${findingType}(input: $input) {
86+
numUids
87+
}
88+
}
89+
`,
90+
data: {
91+
filter: {
92+
id: { eq: resource },
93+
},
94+
set: {
95+
[`${this.entityName}Findings`]: data,
96+
},
97+
},
98+
}
99+
100+
mutations.push(updateMutation)
101+
}
102+
}
103+
}
104+
}
105+
106+
return mutations
107+
}
108+
109+
private prepareManualMutations(findings: RuleFinding[] = []): Entity[] {
110+
const manualFindings = findings.map(({ typename, ...finding }) => ({
111+
...finding,
112+
}))
113+
return manualFindings.length > 0
114+
? [
115+
{
116+
name: `${this.providerName}${this.entityName}Findings`,
117+
mutation: `
118+
mutation($input: [Add${this.providerName}${this.entityName}FindingsInput!]!) {
119+
add${this.providerName}${this.entityName}Findings(input: $input, upsert: true) {
120+
numUids
121+
}
122+
}
123+
`,
124+
data: manualFindings,
125+
},
126+
]
127+
: []
128+
}
129+
130+
// TODO: Optimize generated mutations number
131+
prepareMutations = (findings: RuleFinding[] = []): Entity[] => {
132+
// Group Findings by schema type
133+
const { manual, ...processedRules } = groupBy(findings, 'typename')
134+
135+
// Prepare processed rules mutations
136+
const processedRulesData = this.prepareProcessedMutations(processedRules)
137+
138+
// Prepare manual mutations
139+
const manualRulesData = this.prepareManualMutations(manual)
140+
141+
// Prepare provider mutations
142+
const providerData = this.prepareProviderMutations(findings)
143+
144+
return [...manualRulesData, ...processedRulesData, ...providerData]
145+
}
146+
}

src/rules-engine/evaluators/json-evaluator.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,6 @@ export default class JsonEvaluator implements RuleEvaluator<JsonRule> {
7878
}
7979
return true
8080
},
81-
compare: async (_, conditions: Condition[], data) => {
82-
for (let i = 0; i < conditions.length; i++) {
83-
// if 1 is false, it's false
84-
if (!(await this.evaluateCondition(conditions[i], data))) return false
85-
}
86-
return true
87-
},
8881
array_all: async (array = [], conditions: Condition, data) => {
8982
// an AND, but with every resource item
9083
const baseElementPath = data.elementPath

src/rules-engine/evaluators/manual-evaluator.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@ export default class ManualEvaluator implements RuleEvaluator<JsonRule> {
77
}
88

99
async evaluateSingleResource(rule: Rule): Promise<RuleFinding> {
10-
const finding = {
10+
return {
1111
id: `${rule.id}/manual`,
1212
result: Result.SKIPPED,
1313
typename: 'manual',
1414
rule,
1515
} as RuleFinding
16-
return finding
1716
}
1817
}

src/rules-engine/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import ManualEvaluator from './evaluators/manual-evaluator'
66
import JsEvaluator from './evaluators/js-evaluator'
77
import { RuleEvaluator } from './evaluators/rule-evaluator'
88
import { Engine, ResourceData, Rule, RuleFinding } from './types'
9-
import { Entity } from '../types'
109

1110
export default class RulesProvider implements Engine {
1211
evaluators: RuleEvaluator<any>[] = []
@@ -167,8 +166,6 @@ export default class RulesProvider implements Engine {
167166
return [mainType, extensions]
168167
}
169168

170-
prepareMutations: (findings: RuleFinding[]) => Entity[]
171-
172169
processRule = async (rule: Rule, data: unknown): Promise<RuleFinding[]> => {
173170
const res: any[] = []
174171
const dedupeIds = {}

src/rules-engine/types.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { Entity } from '..'
2-
31
export type ResourceData = {
42
data: { [k: string]: any }
53
resource: { id: string; [k: string]: any }
@@ -85,11 +83,4 @@ export interface Engine {
8583
* @returns An array of RuleFinding
8684
*/
8785
processRule: (rule: Rule, data: any) => Promise<RuleFinding[]>
88-
89-
/**
90-
* Transforms RuleFinding array into a mutation array for GraphQL
91-
* @param findings resulted findings during rules execution
92-
* @returns {Entity[]} Array of generated mutations
93-
*/
94-
prepareMutations: (findings: RuleFinding[]) => Entity[]
9586
}

tests/rules-engine.test.ts

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import RulesProvider from '../src/rules-engine'
44
import { Engine, Rule } from '../src/rules-engine/types'
55
import ManualEvaluatorMock from './evaluators/manual-evaluator.test'
66
import JSONEvaluatordMock from './evaluators/json-evaluator.test'
7+
import DgraphDataProcessor from '../src/rules-engine/data-processors/dgraph-data-processor'
8+
import DataProcessor from '../src/rules-engine/data-processors/data-processor'
79

810
const typenameToFieldMap = {
911
resourceA: 'querySchemaA',
@@ -14,12 +16,14 @@ const entityName = 'CIS'
1416

1517
describe('RulesEngine', () => {
1618
let rulesEngine: Engine
19+
let dataProcessor: DataProcessor
1720
beforeEach(() => {
1821
rulesEngine = new RulesProvider({
1922
providerName,
2023
entityName,
2124
typenameToFieldMap,
2225
})
26+
dataProcessor = new DgraphDataProcessor(providerName, entityName)
2327
})
2428
it('Should pass getting the updated schema created dynamically using schemaTypeName and typenameToFieldMap fields', () => {
2529
const schema = rulesEngine.getSchema()
@@ -34,14 +38,13 @@ describe('RulesEngine', () => {
3438
})
3539

3640
it('Should pass preparing the mutations to insert findings data given a RuleFindings array with Manual Rules', async () => {
37-
await rulesEngine.processRule(
41+
const findings = await rulesEngine.processRule(
3842
ManualEvaluatorMock.manualRule as Rule,
3943
undefined
4044
)
41-
const findings = rulesEngine.prepareMutations([])
42-
const [mutations] = findings
45+
const [mutations] = dataProcessor.prepareMutations(findings)
4346

44-
expect(findings.length).toBe(3)
47+
expect(findings.length).toBe(1)
4548
expect(mutations).toBeDefined()
4649
expect(mutations.data instanceof Array).toBeTruthy()
4750
expect(mutations.data.length).toBe(1)
@@ -54,19 +57,21 @@ describe('RulesEngine', () => {
5457
it('Should pass preparing the mutations to insert findings data given a RuleFindings array with Automated Rules', async () => {
5558
const resourceId = cuid()
5659
const resourceType = 'schemaA'
57-
await rulesEngine.processRule(JSONEvaluatordMock.jsonRule as Rule, {
58-
querySchemaA: [
59-
{
60-
id: resourceId,
61-
__typename: resourceType,
62-
value: 'automated',
63-
},
64-
],
65-
})
66-
const findings = rulesEngine.prepareMutations([])
67-
const [mutations] = findings
60+
const findings = await rulesEngine.processRule(
61+
JSONEvaluatordMock.jsonRule as Rule,
62+
{
63+
querySchemaA: [
64+
{
65+
id: resourceId,
66+
__typename: resourceType,
67+
value: 'automated',
68+
},
69+
],
70+
}
71+
)
72+
const [mutations] = dataProcessor.prepareMutations(findings)
6873

69-
expect(findings.length).toBe(4)
74+
expect(findings.length).toBe(1)
7075
expect(mutations).toBeDefined()
7176
expect(mutations.data instanceof Object).toBeTruthy()
7277
expect(mutations.data.filter.id.eq).toBe(resourceId)
@@ -77,10 +82,10 @@ describe('RulesEngine', () => {
7782
it('Should pass preparing the mutations to insert with an empty findings array', () => {
7883
const data = []
7984

80-
const entities = rulesEngine.prepareMutations(data)
85+
const entities = dataProcessor.prepareMutations(data)
8186

8287
expect(entities).toBeDefined()
83-
expect(entities.length).toBe(3)
88+
expect(entities.length).toBe(2)
8489
})
8590

8691
it('Should return an empty array processing a rule with no data', async () => {

0 commit comments

Comments
 (0)