diff --git a/src/plugins/policyPack/index.ts b/src/plugins/policyPack/index.ts index f801153..f690416 100644 --- a/src/plugins/policyPack/index.ts +++ b/src/plugins/policyPack/index.ts @@ -186,19 +186,18 @@ export default class PolicyPackPlugin extends Plugin { } // TODO: Generalize data processor moving storage module to SDK with its interfaces - private getDataProcessor({ - entity, - provider, - }: { - entity: string - provider: string + private getDataProcessor(config: { + providerName: string + entityName: string + typenameToFieldMap?: { [tn: string]: string } + extraFields?: string[] }): DataProcessor { - const dataProcessorKey = `${provider}${entity}` + const dataProcessorKey = `${config.providerName}${config.entityName}` if (this.dataProcessors[dataProcessorKey]) { return this.dataProcessors[dataProcessorKey] } - const dataProcessor = new DgraphDataProcessor(provider, entity) + const dataProcessor = new DgraphDataProcessor(config) this.dataProcessors[dataProcessorKey] = dataProcessor return dataProcessor } @@ -256,14 +255,17 @@ export default class PolicyPackPlugin extends Plugin { continue // eslint-disable-line no-continue } - // Initialize RulesEngine - const rulesEngine = new RulesEngine({ + // Initialize Data Processor + const dataProcessor = this.getDataProcessor({ providerName: this.provider.name, entityName: policyPackPlugin.entity, typenameToFieldMap: resourceTypeNamesToFieldsMap, extraFields: policyPackPlugin.extraFields, }) + // Initialize RulesEngine + const rulesEngine = new RulesEngine(dataProcessor) + this.policyPacksPlugins[policyPack] = { engine: rulesEngine, entity: policyPackPlugin.entity, @@ -318,14 +320,8 @@ export default class PolicyPackPlugin extends Plugin { storageEngine, }) - // Data Processor - const dataProcessor = this.getDataProcessor({ - entity, - provider: this.provider.name, - }) - // Prepare mutations - const mutations = dataProcessor.prepareMutations(findings) + const mutations = engine.prepareMutations(findings) // Save connections processConnectionsBetweenEntities({ diff --git a/src/rules-engine/data-processors/data-processor.ts b/src/rules-engine/data-processors/data-processor.ts index c6e1d25..31dc0c8 100644 --- a/src/rules-engine/data-processors/data-processor.ts +++ b/src/rules-engine/data-processors/data-processor.ts @@ -2,6 +2,16 @@ import { Entity } from '../../types' import { RuleFinding } from '../types' export default interface DataProcessor { + readonly typenameToFieldMap: { [typeName: string]: string } + + readonly extraFields: string[] + + /** + * Returns an GraphQL schema build dynamically based on the provider and existing resources + * @returns new schemas and extensions for existing ones + */ + getSchema: () => string[] + /** * Transforms RuleFinding array into a mutation array for GraphQL * @param findings resulted findings during rules execution diff --git a/src/rules-engine/data-processors/dgraph-data-processor.ts b/src/rules-engine/data-processors/dgraph-data-processor.ts index 813c921..0a2a223 100644 --- a/src/rules-engine/data-processors/dgraph-data-processor.ts +++ b/src/rules-engine/data-processors/dgraph-data-processor.ts @@ -9,9 +9,91 @@ export default class DgraphDataProcessor implements DataProcessor { private readonly entityName - constructor(providerName: string, entityName: string) { + readonly typenameToFieldMap: { [typeName: string]: string } + + readonly extraFields: string[] + + constructor({ + providerName, + entityName, + typenameToFieldMap, + extraFields, + }: { + providerName: string + entityName: string + typenameToFieldMap?: { [tn: string]: string } + extraFields?: string[] + }) { this.providerName = providerName this.entityName = entityName + this.extraFields = extraFields ?? [] + this.typenameToFieldMap = typenameToFieldMap ?? {} + } + + getSchema = (): string[] => { + const mainType = ` + enum FindingsResult { + PASS + FAIL + MISSING + SKIPPED + } + + type ${this.providerName}Findings @key(fields: "id") { + id: String! @id + ${this.entityName}Findings: [${this.providerName}${ + this.entityName + }Findings] + } + + type ruleMetadata @key(fields: "id") { + id: String! @id @search(by: [hash, regexp]) + severity: String! @search(by: [hash, regexp]) + description: String! @search(by: [hash, regexp]) + title: String @search(by: [hash, regexp]) + audit: String @search(by: [hash, regexp]) + rationale: String @search(by: [hash, regexp]) + remediation: String @search(by: [hash, regexp]) + references: [String] @search(by: [hash, regexp]) + findings: [baseFinding] @hasInverse(field: rule) + } + + interface baseFinding { + id: String! @id + resourceId: String @search(by: [hash, regexp]) + rule: [ruleMetadata] @hasInverse(field: findings) + result: FindingsResult @search + } + + type ${this.providerName}${ + this.entityName + }Findings implements baseFinding @key(fields: "id") { + findings: ${this.providerName}Findings @hasInverse(field: ${ + this.entityName + }Findings) + # extra fields + ${this.extraFields.map( + field => `${field}: String @search(by: [hash, regexp])` + )} + # connections + ${Object.keys(this.typenameToFieldMap) + .map( + (tn: string) => + `${tn}: [${this.typenameToFieldMap[tn]}] @hasInverse(field: ${this.entityName}Findings)` + ) + .join(' ')} + } + ` + const extensions = Object.keys(this.typenameToFieldMap) + .map( + (tn: string) => + `extend type ${this.typenameToFieldMap[tn]} { + ${this.entityName}Findings: [${this.providerName}${this.entityName}Findings] @hasInverse(field: ${tn}) +}` + ) + .join('\n') + + return [mainType, extensions] } /** @@ -61,7 +143,7 @@ export default class DgraphDataProcessor implements DataProcessor { private prepareProcessedMutations(findingsByType: { [resource: string]: RuleFinding[] - }): Entity[] { + }): RuleFinding[] { const mutations = [] for (const findingType in findingsByType) { @@ -76,28 +158,20 @@ export default class DgraphDataProcessor implements DataProcessor { if (resource) { const data = ( (findingsByResource[resource] as RuleFinding[]) || [] - ).map(({ typename, ...properties }) => properties) - - // Create dynamically update mutations by resource - const updateMutation = { - name: `${this.providerName}${this.entityName}Findings`, - mutation: `mutation update${findingType}($input: Update${findingType}Input!) { - update${findingType}(input: $input) { - numUids - } - } - `, - data: { - filter: { - id: { eq: resource }, - }, - set: { - [`${this.entityName}Findings`]: data, + ).map(finding => { + const resourceType = Object.keys(this.typenameToFieldMap).find( + key => this.typenameToFieldMap[key] === finding.typename + ) + + return { + ...finding, + [resourceType]: { + id: resource, }, - }, - } + } + }) - mutations.push(updateMutation) + mutations.push(...data) } } } @@ -106,41 +180,36 @@ export default class DgraphDataProcessor implements DataProcessor { return mutations } - private prepareManualMutations(findings: RuleFinding[] = []): Entity[] { - const manualFindings = findings.map(({ typename, ...finding }) => ({ - ...finding, - })) - return manualFindings.length > 0 - ? [ - { - name: `${this.providerName}${this.entityName}Findings`, - mutation: ` - mutation($input: [Add${this.providerName}${this.entityName}FindingsInput!]!) { - add${this.providerName}${this.entityName}Findings(input: $input, upsert: true) { - numUids - } - } - `, - data: manualFindings, - }, - ] - : [] - } - - // TODO: Optimize generated mutations number prepareMutations = (findings: RuleFinding[] = []): Entity[] => { // Group Findings by schema type - const { manual, ...processedRules } = groupBy(findings, 'typename') + const { manual: manualData = [], ...processedRules } = groupBy( + findings, + 'typename' + ) // Prepare processed rules mutations const processedRulesData = this.prepareProcessedMutations(processedRules) - // Prepare manual mutations - const manualRulesData = this.prepareManualMutations(manual) - // Prepare provider mutations const providerData = this.prepareProviderMutations(findings) - return [...manualRulesData, ...processedRulesData, ...providerData] + return [ + { + name: `${this.providerName}${this.entityName}Findings`, + mutation: ` +mutation($input: [Add${this.providerName}${this.entityName}FindingsInput!]!) { + add${this.providerName}${this.entityName}Findings(input: $input, upsert: true) { + numUids + } +} +`, + data: [...manualData, ...processedRulesData].map( + ({ typename, ...finding }) => ({ + ...finding, + }) + ), + }, + ...providerData, + ] } } diff --git a/src/rules-engine/index.ts b/src/rules-engine/index.ts index aa58ab6..f10ab10 100644 --- a/src/rules-engine/index.ts +++ b/src/rules-engine/index.ts @@ -6,33 +6,16 @@ import ManualEvaluator from './evaluators/manual-evaluator' import JsEvaluator from './evaluators/js-evaluator' import { RuleEvaluator } from './evaluators/rule-evaluator' import { Engine, ResourceData, Rule, RuleFinding } from './types' +import DataProcessor from './data-processors/data-processor' +import { Entity } from '../types' export default class RulesProvider implements Engine { evaluators: RuleEvaluator[] = [] - private readonly typenameToFieldMap: { [typeName: string]: string } - - private readonly extraFields: string[] - - private readonly providerName - - private readonly entityName - - constructor({ - providerName, - entityName, - typenameToFieldMap, - extraFields, - }: { - providerName: string - entityName: string - typenameToFieldMap?: { [tn: string]: string } - extraFields?: string[] - }) { - this.extraFields = extraFields ?? [] - this.typenameToFieldMap = typenameToFieldMap ?? {} - this.entityName = entityName - this.providerName = providerName + private readonly dataProcessor: DataProcessor + + constructor(dataProcessor: DataProcessor) { + this.dataProcessor = dataProcessor this.evaluators = [ new JsonEvaluator(), new JsEvaluator(), @@ -69,13 +52,13 @@ export default class RulesProvider implements Engine { const finding = await evaluator.evaluateSingleResource(rule, data) // Inject extra fields - for (const field of this.extraFields) { + for (const field of this.dataProcessor.extraFields) { finding[field] = data.resource[field] } const connField = data.resource.__typename && // eslint-disable-line no-underscore-dangle - this.typenameToFieldMap[data.resource.__typename] // eslint-disable-line no-underscore-dangle + this.dataProcessor.typenameToFieldMap[data.resource.__typename] // eslint-disable-line no-underscore-dangle if (connField) { finding[connField] = [{ id: data.resource.id }] @@ -100,71 +83,7 @@ export default class RulesProvider implements Engine { return data } - getSchema = (): string[] => { - const mainType = ` - enum FindingsResult { - PASS - FAIL - MISSING - SKIPPED - } - - type ${this.providerName}Findings @key(fields: "id") { - id: String! @id - ${this.entityName}Findings: [${this.providerName}${ - this.entityName - }Findings] - } - - type ruleMetadata @key(fields: "id") { - id: String! @id @search(by: [hash, regexp]) - severity: String! @search(by: [hash, regexp]) - description: String! @search(by: [hash, regexp]) - title: String @search(by: [hash, regexp]) - audit: String @search(by: [hash, regexp]) - rationale: String @search(by: [hash, regexp]) - remediation: String @search(by: [hash, regexp]) - references: [String] @search(by: [hash, regexp]) - findings: [baseFinding] @hasInverse(field: rule) - } - - interface baseFinding { - id: String! @id - resourceId: String @search(by: [hash, regexp]) - rule: [ruleMetadata] @hasInverse(field: findings) - result: FindingsResult @search - } - - type ${this.providerName}${ - this.entityName - }Findings implements baseFinding @key(fields: "id") { - findings: ${this.providerName}Findings @hasInverse(field: ${ - this.entityName - }Findings) - # extra fields - ${this.extraFields.map( - field => `${field}: String @search(by: [hash, regexp])` - )} - # connections - ${Object.keys(this.typenameToFieldMap) - .map( - (tn: string) => - `${tn}: [${this.typenameToFieldMap[tn]}] @hasInverse(field: ${this.entityName}Findings)` - ) - .join(' ')} - } - ` - const extensions = Object.keys(this.typenameToFieldMap) - .map( - (tn: string) => - `extend type ${this.typenameToFieldMap[tn]} { - ${this.entityName}Findings: [${this.providerName}${this.entityName}Findings] @hasInverse(field: ${tn}) -}` - ) - .join('\n') - - return [mainType, extensions] - } + getSchema = (): string[] => this.dataProcessor.getSchema() processRule = async (rule: Rule, data: unknown): Promise => { const res: any[] = [] @@ -215,4 +134,7 @@ export default class RulesProvider implements Engine { } return res } + + prepareMutations = (findings: RuleFinding[]): Entity[] => + this.dataProcessor.prepareMutations(findings) } diff --git a/src/rules-engine/types.ts b/src/rules-engine/types.ts index 1fc4574..9a57b7f 100644 --- a/src/rules-engine/types.ts +++ b/src/rules-engine/types.ts @@ -1,3 +1,5 @@ +import { Entity } from '../types' + export type ResourceData = { data: { [k: string]: any } resource: { id: string; [k: string]: any } @@ -83,4 +85,11 @@ export interface Engine { * @returns An array of RuleFinding */ processRule: (rule: Rule, data: any) => Promise + + /** + * Transforms RuleFinding array into a mutation array for GraphQL + * @param findings resulted findings during rules execution + * @returns {Entity[]} Array of generated mutations + */ + prepareMutations: (findings: RuleFinding[]) => Entity[] }