Skip to content

Commit 872716f

Browse files
author
Tyson Kunovsky
committed
Merge branch 'feature/CG-891' into 'master'
feat(rulesEngine): Composite Rules Closes CG-891 See merge request auto-cloud/cloudgraph/sdk!55
2 parents 5fbb911 + e061c80 commit 872716f

File tree

14 files changed

+570
-286
lines changed

14 files changed

+570
-286
lines changed

src/plugins/policyPack/index.ts

Lines changed: 85 additions & 22 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,
@@ -130,6 +136,73 @@ export default class PolicyPackPlugin extends Plugin {
130136
}
131137
}
132138

139+
private async executeRule({
140+
rules,
141+
policyPack,
142+
storageEngine,
143+
}: {
144+
rules: Rule[]
145+
policyPack: string
146+
storageEngine: StorageEngine
147+
}): Promise<RuleFinding[]> {
148+
const findings: RuleFinding[] = []
149+
150+
// Run rules:
151+
for (const rule of rules) {
152+
try {
153+
if (rule.queries?.length > 0) {
154+
const { queries, ...ruleMetadata } = rule
155+
const subRules = queries.map(q => ({
156+
...q,
157+
...ruleMetadata,
158+
}))
159+
160+
findings.push(
161+
...(await this.executeRule({
162+
rules: subRules,
163+
policyPack,
164+
storageEngine,
165+
}))
166+
)
167+
} else {
168+
const { data } = rule.gql
169+
? await storageEngine.query(rule.gql)
170+
: { data: undefined }
171+
const results = (await this.policyPacksPlugins[
172+
policyPack
173+
]?.engine?.processRule(rule, data)) as RuleFinding[]
174+
175+
findings.push(...results)
176+
}
177+
} catch (error) {
178+
this.logger.error(
179+
`Error processing rule ${rule.id} for ${policyPack} policy pack`
180+
)
181+
this.logger.debug(error)
182+
}
183+
}
184+
185+
return findings
186+
}
187+
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+
133206
async configure(
134207
pluginManager: PluginManager,
135208
plugins: ConfiguredPlugin[]
@@ -239,29 +312,20 @@ export default class PolicyPackPlugin extends Plugin {
239312
mergeSchemas(currentSchema, findingsSchema),
240313
])
241314

242-
const findings: RuleFinding[] = []
243-
244-
// Run rules:
245-
for (const rule of rules) {
246-
try {
247-
const { data } = rule.gql
248-
? await storageEngine.query(rule.gql)
249-
: { data: undefined }
250-
const results = (await this.policyPacksPlugins[
251-
policyPack
252-
]?.engine?.processRule(rule, data)) as RuleFinding[]
253-
254-
findings.push(...results)
255-
} catch (error) {
256-
this.logger.error(
257-
`Error processing rule ${rule.id} for ${policyPack} policy pack`
258-
)
259-
this.logger.debug(error)
260-
}
261-
}
315+
const findings = await this.executeRule({
316+
rules,
317+
policyPack,
318+
storageEngine,
319+
})
320+
321+
// Data Processor
322+
const dataProcessor = this.getDataProcessor({
323+
entity,
324+
provider: this.provider.name,
325+
})
262326

263327
// Prepare mutations
264-
const mutations = engine?.prepareMutations(findings)
328+
const mutations = dataProcessor.prepareMutations(findings)
265329

266330
// Save connections
267331
processConnectionsBetweenEntities({
@@ -279,7 +343,6 @@ export default class PolicyPackPlugin extends Plugin {
279343
)
280344
this.logger.successSpinner('success')
281345

282-
// TODO: Use table to display results
283346
const results = findings.filter(
284347
finding => finding.result === Result.FAIL
285348
)
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/js-evaluator.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ export default class JsEvaluator implements RuleEvaluator<JsRule> {
2929
typename: data.resource?.__typename, // eslint-disable-line no-underscore-dangle
3030
rule: ruleMetadata,
3131
} as RuleFinding
32-
3332
return finding
3433
}
3534
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import {
1010
RuleResult,
1111
_ResourceData,
1212
} from '../types'
13-
import { RuleEvaluator } from './rule-evaluator'
1413
import AdditionalOperators from '../operators'
1514
import ComparisonOperators from '../operators/comparison'
15+
import { RuleEvaluator } from './rule-evaluator'
1616

1717
export default class JsonEvaluator implements RuleEvaluator<JsonRule> {
1818
canEvaluate(rule: JsonRule): boolean {
@@ -35,7 +35,6 @@ export default class JsonEvaluator implements RuleEvaluator<JsonRule> {
3535
typename: data.resource?.__typename, // eslint-disable-line no-underscore-dangle
3636
rule: ruleMetadata,
3737
} as RuleFinding
38-
3938
return finding
4039
}
4140

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,4 @@ import { ResourceData, Rule, RuleFinding } from '../types'
33
export interface RuleEvaluator<K extends Rule> {
44
canEvaluate: (rule: K) => boolean
55
evaluateSingleResource: (rule: K, data?: ResourceData) => Promise<RuleFinding>
6-
// @TODO complex rules can take a query and return an array of resourceId + Result
76
}

0 commit comments

Comments
 (0)