├── .prettierrc.json ├── .eslintrc.json ├── .npmignore ├── src ├── plugins │ ├── base │ │ └── pluginMap.ts │ ├── types.ts │ └── policyPack │ │ └── index.ts ├── rules-engine │ ├── operators │ │ ├── date.ts │ │ ├── comparison.ts │ │ ├── array.ts │ │ ├── index.ts │ │ ├── common.ts │ │ └── regex.ts │ ├── evaluators │ │ ├── rule-evaluator.ts │ │ ├── manual-evaluator.ts │ │ ├── js-evaluator.ts │ │ └── json-evaluator.ts │ ├── data-processors │ │ ├── data-processor.ts │ │ └── dgraph-data-processor.ts │ ├── types.ts │ └── index.ts ├── client │ └── index.ts ├── storage │ └── index.ts ├── types │ └── index.ts ├── utils │ ├── schema.ts │ ├── data.ts │ └── index.ts ├── index.ts └── logger │ └── index.ts ├── jest.config.js ├── .github ├── pull_request_template.md ├── workflows │ ├── pr-validator.yml │ ├── notify.yml │ └── publish.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── SECURITY.md ├── .releaserc.yml ├── tests ├── operators │ ├── common.test.ts │ ├── array.test.ts │ ├── date.test.ts │ ├── regex.test.ts │ └── comparison.test.ts ├── evaluators │ ├── manual-evaluator.test.ts │ ├── js-evaluator.test.ts │ └── json-evaluator.test.ts ├── rules-engine.test.ts └── plugins │ └── policyPack.test.ts ├── package.json ├── .gitignore ├── tsconfig.json ├── LICENSE └── CHANGELOG.md /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "arrowParens": "avoid" 5 | } 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@autocloud" 4 | ], 5 | "rules": { 6 | "no-plusplus": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !dist/**/* 3 | !package.json 4 | !LICENSE 5 | !AUTHORS 6 | !CHANGELOG.md 7 | !CODE_OF_CONDUCT.md 8 | !CODEOWNERS 9 | !CONTRIBUTING.md 10 | !README.md -------------------------------------------------------------------------------- /src/plugins/base/pluginMap.ts: -------------------------------------------------------------------------------- 1 | import PolicyPackPlugin from '../policyPack' 2 | 3 | export default { 4 | policyPack: PolicyPackPlugin, 5 | } as { [plugin: string]: any } 6 | -------------------------------------------------------------------------------- /src/rules-engine/operators/date.ts: -------------------------------------------------------------------------------- 1 | import { differenceInDays } from 'date-fns' 2 | 3 | // Date Operators 4 | export default { 5 | daysAgo: a => differenceInDays(Date.now(), new Date(a)), 6 | daysDiff: a => differenceInDays(new Date(a), Date.now()), 7 | } 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | testMatch: ['/tests/**/*.test.ts'], 6 | testPathIgnorePatterns: ['/lib/', '/node_modules/'], 7 | } 8 | -------------------------------------------------------------------------------- /src/rules-engine/evaluators/rule-evaluator.ts: -------------------------------------------------------------------------------- 1 | import { ResourceData, Rule, RuleFinding } from '../types' 2 | 3 | export interface RuleEvaluator { 4 | canEvaluate: (rule: K) => boolean 5 | evaluateSingleResource: (rule: K, data?: ResourceData) => Promise 6 | } 7 | -------------------------------------------------------------------------------- /src/rules-engine/operators/comparison.ts: -------------------------------------------------------------------------------- 1 | // Comparison Operators 2 | export default { 3 | equal: (a, b) => a == b, // eslint-disable-line eqeqeq 4 | notEqual: (a, b) => a !== b, 5 | lessThan: (a, b) => a < b, 6 | lessThanInclusive: (a, b) => a <= b, 7 | greaterThan: (a, b) => a > b, 8 | greaterThanInclusive: (a, b) => a >= b, 9 | } 10 | -------------------------------------------------------------------------------- /src/rules-engine/operators/array.ts: -------------------------------------------------------------------------------- 1 | // Array Operators 2 | export default { 3 | in: (a, b) => (Array.isArray(b) ? b.indexOf(a) > -1 : false), 4 | notIn: (a, b) => (Array.isArray(b) ? b.indexOf(a) === -1 : false), 5 | contains: (a, b) => (Array.isArray(a) ? a.indexOf(b) > -1 : false), 6 | doesNotContain: (a, b) => (Array.isArray(a) ? a.indexOf(b) === -1 : false), 7 | } 8 | -------------------------------------------------------------------------------- /src/rules-engine/operators/index.ts: -------------------------------------------------------------------------------- 1 | import { Operator } from '../types' 2 | 3 | import arrayOperators from './array' 4 | import commonOperators from './common' 5 | import comparisonOperators from './comparison' 6 | import dateOperators from './date' 7 | import regexOperators from './regex' 8 | 9 | export default { 10 | ...arrayOperators, 11 | ...comparisonOperators, 12 | ...commonOperators, 13 | ...dateOperators, 14 | ...regexOperators, 15 | } as { [key: string]: Operator } 16 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Issue tracker links 2 | 3 | _Add links to any relevant tasks/stories/bugs/pagerduty/etc_ 4 | 5 | *Example - dummy TODO project* 6 | 7 | [TODO-123](https://autoclouddev.atlassian.net/browse/TODO-123) 8 | 9 | ## Changes/solution 10 | 11 | _How does this change address the problem?_ 12 | 13 | ## Testing 14 | 15 | _Describe how the testing was done, plus evidence, if not covered by automated tests_ 16 | 17 | ## Notes and considerations 18 | 19 | _Add any additional notes and/or considerations_ 20 | 21 | ## Dependencies 22 | 23 | _Add dependencies on any other PRs, if applicable 24 | -------------------------------------------------------------------------------- /src/rules-engine/operators/common.ts: -------------------------------------------------------------------------------- 1 | // Common Operators 2 | export default { 3 | isEmpty: (data, shouldBeEmpty) => { 4 | if (Array.isArray(data)) { 5 | // Verify empty/filled arrays 6 | const array = (data || []).length 7 | return shouldBeEmpty ? array === 0 : array > 0 8 | } 9 | 10 | // Verify empty/filled objects 11 | if (typeof data === 'object' && data !== null) { 12 | const hasKeys = Object.keys(data).length 13 | return shouldBeEmpty ? hasKeys === 0 : hasKeys > 0 14 | } 15 | 16 | return ( 17 | (data === null || data === undefined || data === '') === shouldBeEmpty 18 | ) 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /src/rules-engine/evaluators/manual-evaluator.ts: -------------------------------------------------------------------------------- 1 | import { JsonRule, Result, Rule, RuleFinding } from '../types' 2 | import { RuleEvaluator } from './rule-evaluator' 3 | 4 | export default class ManualEvaluator implements RuleEvaluator { 5 | canEvaluate(rule: Rule): boolean { 6 | return !('gql' in rule) && !('conditions' in rule) && !('resource' in rule) 7 | } 8 | 9 | async evaluateSingleResource({ id, severity }: Rule): Promise { 10 | return { 11 | id: `${id}/manual`, 12 | result: Result.SKIPPED, 13 | typename: 'manual', 14 | rule: { 15 | id, 16 | severity, 17 | }, 18 | } as RuleFinding 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/rules-engine/operators/regex.ts: -------------------------------------------------------------------------------- 1 | // Regex Operators 2 | export default { 3 | match: (a, b) => new RegExp(b).test(a), 4 | mismatch: (a, b) => !new RegExp(b).test(a), 5 | matchAny: (a, b) => { 6 | if (Array.isArray(a)) { 7 | const result = a 8 | .map(value => 9 | typeof value === 'string' ? new RegExp(b).test(value) : undefined 10 | ) 11 | .some(v => v) 12 | return result 13 | } 14 | 15 | return false 16 | }, 17 | matchAll: (a, b) => { 18 | if (Array.isArray(a)) { 19 | const result = a 20 | .map(value => 21 | typeof value === 'string' ? new RegExp(b).test(value) : undefined 22 | ) 23 | .every(v => v) 24 | return result 25 | } 26 | 27 | return false 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | import { DocumentNode } from 'graphql' 2 | import inquirer from 'inquirer' 3 | 4 | import { Logger } from '../logger' 5 | import { Opts, ProviderData } from '../types' 6 | 7 | export default abstract class Provider { 8 | constructor(config: any) { 9 | this.logger = config.logger 10 | this.config = config.provider ?? {} 11 | } 12 | 13 | interface = inquirer 14 | 15 | logger: Logger 16 | 17 | config: any 18 | 19 | // eslint-disable-next-line 20 | async configure(flags: any): Promise<{[key: string]: any}> { 21 | throw new Error('Function configure has not been defined') 22 | } 23 | 24 | getSchema(): DocumentNode { 25 | throw new Error('Function getSchema has not been defined') 26 | } 27 | 28 | // eslint-disable-next-line 29 | async getData({ opts }: { opts: Opts }): Promise { 30 | throw new Error('Function getData has not been defined') 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/pr-validator.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: pr-validator 3 | 4 | on: 5 | pull_request: 6 | types: [synchronize, opened, reopened, edited] 7 | branches: 8 | - main 9 | - beta 10 | 11 | jobs: 12 | pr-validation: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - run: | 17 | if [ "$TARGET_BRANCH" == "main" ] && [ "$SOURCE_BRANCH" == "beta" ]; then 18 | echo "Merge from $SOURCE_BRANCH to $TARGET_BRANCH is valid" 19 | exit 0 20 | elif [ "$TARGET_BRANCH" == "beta" ] && [ "$SOURCE_BRANCH" == "alpha" ]; then 21 | echo "Merge from $SOURCE_BRANCH to $TARGET_BRANCH is valid" 22 | exit 0 23 | else 24 | echo "You cannot merge from $SOURCE_BRANCH to $TARGET_BRANCH" 25 | exit 1 26 | fi 27 | env: 28 | SOURCE_BRANCH: ${{ github.head_ref }} 29 | TARGET_BRANCH: ${{ github.base_ref }} 30 | -------------------------------------------------------------------------------- /src/rules-engine/evaluators/js-evaluator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JsRule, 3 | ResourceData, 4 | Rule, 5 | RuleResult, 6 | RuleFinding, 7 | Result, 8 | } from '../types' 9 | import { RuleEvaluator } from './rule-evaluator' 10 | 11 | export default class JsEvaluator implements RuleEvaluator { 12 | canEvaluate(rule: Rule | JsRule): boolean { 13 | return 'check' in rule 14 | } 15 | 16 | async evaluateSingleResource( 17 | { id, check, severity }: JsRule, 18 | data: ResourceData 19 | ): Promise { 20 | const result = check!(data) ? RuleResult.MATCHES : RuleResult.DOESNT_MATCH 21 | 22 | const finding = { 23 | id: `${id}/${data.resource?.id}`, 24 | resourceId: data.resource?.id, 25 | result: result !== RuleResult.MATCHES ? Result.FAIL : Result.PASS, 26 | typename: data.resource?.__typename, // eslint-disable-line no-underscore-dangle 27 | rule: { 28 | id, 29 | severity, 30 | }, 31 | } as RuleFinding 32 | return finding 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.releaserc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | branches: 3 | - name: alpha 4 | channel: alpha 5 | prerelease: true 6 | - name: beta 7 | channel: beta 8 | prerelease: true 9 | - name: main 10 | 11 | plugins: 12 | - "@semantic-release/commit-analyzer" 13 | - "@semantic-release/release-notes-generator" 14 | - - "@semantic-release/changelog" 15 | - changelogFile: CHANGELOG.md 16 | - - "@semantic-release/git" 17 | - assets: 18 | - CHANGELOG.md 19 | - package.json 20 | - - "@semantic-release/npm" 21 | - npmPublish: true 22 | - "@semantic-release/github" 23 | verifyConditions: 24 | - "@semantic-release/changelog" 25 | - "@semantic-release/github" 26 | - "@semantic-release/npm" 27 | prepare: 28 | - "@semantic-release/changelog" 29 | - "@semantic-release/npm" 30 | - - "@semantic-release/git" 31 | - message: "chore(release): ${nextRelease.version} \n\n${nextRelease.notes}" 32 | publish: 33 | - "@semantic-release/github" 34 | - "@semantic-release/npm" 35 | success: false 36 | fail: false 37 | tagFormat: "${version}" 38 | -------------------------------------------------------------------------------- /src/rules-engine/data-processors/data-processor.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../../types' 2 | import { Rule, RuleFinding } from '../types' 3 | 4 | export default interface DataProcessor { 5 | readonly typenameToFieldMap: { [typeName: string]: string } 6 | 7 | readonly extraFields: string[] 8 | 9 | /** 10 | * Returns an GraphQL schema build dynamically based on the provider and existing resources 11 | * @returns new schemas and extensions for existing ones 12 | */ 13 | getSchema: () => string[] 14 | 15 | /** 16 | * Transforms RuleFinding array into a mutation array for GraphQL 17 | * @param findings resulted findings during rules execution 18 | * @returns {Entity[]} Array of generated mutations 19 | */ 20 | prepareFindingsMutations: (findings: RuleFinding[]) => Entity[] 21 | 22 | /** 23 | * Transforms Rules array into a mutation array for GraphQL 24 | * @param rules rules metadata 25 | * @returns {Entity[]} Array of generated mutations 26 | */ 27 | prepareRulesMetadataMutations: (rules: Rule[]) => Entity[] 28 | } 29 | -------------------------------------------------------------------------------- /src/storage/index.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '../logger' 2 | 3 | export interface GraphQLInputData { 4 | query: string 5 | input?: any 6 | patch?: any 7 | name: string 8 | } 9 | 10 | export interface GraphQLQueryData { 11 | query: string 12 | input?: any 13 | variables: { 14 | input?: any 15 | patch?: any 16 | } 17 | } 18 | 19 | export interface StorageEngineConnectionConfig { 20 | scheme: string 21 | host: string 22 | port: string 23 | } 24 | 25 | export interface StorageEngineConfig extends StorageEngineConnectionConfig { 26 | type: string 27 | logger: Logger 28 | } 29 | 30 | export interface StorageEngine { 31 | get host(): string 32 | 33 | healthCheck: (showInitialStatus?: boolean) => Promise 34 | 35 | setSchema: (schema: string[]) => Promise 36 | 37 | validateSchema: (schema: string[], versionString: string) => Promise 38 | 39 | getSchema: () => Promise 40 | 41 | query: (query: string, path?: string) => Promise 42 | 43 | push: (data: GraphQLInputData) => void 44 | 45 | run: (dropData?: boolean) => Promise 46 | } 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a feature you would like to see CloudGraph implement 4 | title: '' 5 | labels: enhancement 6 | assignees: tyler-dunkel 7 | 8 | --- 9 | 10 | Thank you for taking the time to suggest a way the CloudGraph tool could imrpove! 11 | 12 | If this is for a larger feature request, please use our [Slack channel](https://cloudgraph-workspace.slack.com) so we can discuss and avoid duplicate work (we may already be working on it!) 13 | . 14 | **Is your feature request related to a problem? Please describe.** 15 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 16 | 17 | **Describe the solution you'd like** 18 | A clear and concise description of what you want to happen. 19 | 20 | **Describe alternatives you've considered** 21 | A clear and concise description of any alternative solutions or features you've considered. 22 | 23 | **How would this be useful to you** 24 | Tell us what this feature would help you achieve in your workflow 25 | 26 | **Additional context** 27 | Add any other context or screenshots about the feature request here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve CloudGraph 4 | title: '' 5 | labels: bug 6 | assignees: tyler-dunkel 7 | 8 | --- 9 | 10 | Thank you for filling out a bug report, we really appreciate any help in improving the CloudGraph CLI and providers! 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. 14 | 15 | **To Reproduce** 16 | Steps to reproduce the behavior: 17 | 1. Run command '...' NOTE: please run the command in DEBUG mode for additional debugging info [e.g. `CG_DEBUG=5 cg scan aws`] 18 | 2. Run GraphQL query '....' 19 | 4. See error 20 | 21 | Please include the `cg-debug.log` file if applicable 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Environment (please complete the following information):** 27 | - CLI version [e.g. `0.11.7`] 28 | - Provider versions [e.g. `aws@0.30.0`, `azure@0.15.1`] 29 | - Context [e.g. Local machine, EC2 Instance, Other] 30 | 31 | **Screenshots** 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/workflows/notify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: notify 3 | 4 | on: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | notify: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 16 16 | - run: | 17 | GIT_COMMIT_TILE=$(git log -1 --pretty=format:"%s") 18 | curl -X POST --data-urlencode "payload={\"attachments\":[{\"fallback\":\"$GIT_AUTHOR_NAME released new $ORGANIZATION_NAME $REPO_NAME version of $GITHUB_REF_NAME\",\"color\":\"good\",\"title\":\"Version $GITHUB_REF_NAME of $ORGANIZATION_NAME $REPO_NAME released\",\"title_link\":\"$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$GITHUB_REF_NAME\",\"fields\":[{\"title\":\"Tag\",\"value\":\"<$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/commits/$GITHUB_REF_NAME|$GITHUB_REF_NAME>\",\"short\":true},{\"title\":\"Commit\",\"value\":\"<$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/tree/$GITHUB_REF_NAME|$GIT_COMMIT_TILE>\",\"short\":true}],\"footer\":\"$ORGANIZATION_NAME $REPO_NAME \",\"ts\":\"$( date +%s )\"}]}" $SLACK_WEBHOOK 19 | env: 20 | REPO_NAME: ${{ github.event.repository.name }} 21 | GIT_AUTHOR_NAME: "AutoCloud Deploy Bot" 22 | SLACK_WEBHOOK: ${{secrets.slack_api_endpoint}} 23 | ORGANIZATION_NAME: ${{secrets.organization_name}} 24 | -------------------------------------------------------------------------------- /src/plugins/types.ts: -------------------------------------------------------------------------------- 1 | import { ProviderData, SchemaMap, StorageEngine } from '..' 2 | 3 | export interface ConfiguredPlugin { 4 | name: string 5 | providers: string[] 6 | } 7 | 8 | export enum PluginType { 9 | Provider = 'provider', 10 | PolicyPack = 'policyPack', 11 | } 12 | 13 | export enum PluginModule { 14 | 'provider' = 'cg-provider', 15 | 'policyPack' = 'policy-pack', 16 | } 17 | 18 | export interface PluginManager { 19 | getPlugin: (plugin: string, version?: string) => Promise 20 | 21 | queryRemoteVersion: (importPath: string) => Promise 22 | 23 | getVersion: (importPath: string) => Promise 24 | 25 | checkRequiredVersion: (importPath: string) => Promise 26 | 27 | removePlugin: (plugin: string) => Promise 28 | } 29 | 30 | export default abstract class Plugin { 31 | configure( 32 | pluginManager: PluginManager, 33 | plugins: ConfiguredPlugin[] 34 | ): Promise<{ [key: string]: any }> { 35 | throw new Error( 36 | `Function configure has not been defined: ${pluginManager} ${JSON.stringify( 37 | plugins 38 | )}` 39 | ) 40 | } 41 | 42 | execute({ 43 | storageRunning, 44 | storageEngine, 45 | processConnectionsBetweenEntities, 46 | }: { 47 | storageRunning: boolean 48 | storageEngine: StorageEngine 49 | processConnectionsBetweenEntities: (props: { 50 | provider?: string 51 | providerData: ProviderData 52 | storageEngine: StorageEngine 53 | storageRunning: boolean 54 | schemaMap?: SchemaMap 55 | }) => void 56 | }): Promise { 57 | throw new Error( 58 | `Function configure has not been defined ${storageRunning} ${storageEngine} ${processConnectionsBetweenEntities}` 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '../logger' 2 | 3 | // Deprecated 4 | export interface Opts { 5 | devMode: boolean 6 | debug: boolean 7 | logger: Logger 8 | } 9 | export interface ServiceConnection { 10 | id: string 11 | resourceType: string 12 | relation: string 13 | field: string 14 | insertAfterNodeInsertion?: boolean 15 | } 16 | export interface Service { 17 | format: ({ 18 | service, 19 | region, 20 | account, 21 | }: { 22 | service: any 23 | region: string 24 | account: string 25 | }) => any 26 | getConnections?: ({ 27 | service, 28 | region, 29 | account, 30 | data, 31 | }: { 32 | service: any 33 | region: string 34 | account: string 35 | data: any 36 | }) => { [key: string]: ServiceConnection[] } 37 | mutation?: string 38 | getData: ({ 39 | regions, 40 | config, 41 | opts, 42 | account, 43 | rawData, 44 | }: { 45 | regions?: string 46 | config: any 47 | opts: Opts 48 | account?: string 49 | rawData: any 50 | }) => any 51 | } 52 | 53 | export interface EntityMutations { 54 | query?: string 55 | upsert: string 56 | delete: string 57 | } 58 | 59 | export interface Entity { 60 | className?: string 61 | name: string 62 | mutation: EntityMutations | string 63 | data: any[] | any 64 | } 65 | 66 | export interface ProviderError { 67 | service: string 68 | function: string 69 | message: string 70 | } 71 | 72 | export interface ProviderData { 73 | entities: Entity[] 74 | connections: { [key: string]: ServiceConnection[] } 75 | errors?: ProviderError[] 76 | } 77 | 78 | export type LoggerInput = string | { [key: string]: any } | unknown 79 | 80 | export type SchemaMap = { 81 | [schemaName: string]: string 82 | } 83 | -------------------------------------------------------------------------------- /src/utils/schema.ts: -------------------------------------------------------------------------------- 1 | import { mergeTypeDefs } from '@graphql-tools/merge' 2 | import { loadFilesSync } from '@graphql-tools/load-files' 3 | import { print } from 'graphql' 4 | import path from 'path' 5 | 6 | import { EntityMutations } from '../types' 7 | 8 | export const mergeSchemas = (currSchema: string, additions: string[]) => { 9 | const s = mergeTypeDefs([currSchema, ...additions]) 10 | return print(s) 11 | } 12 | 13 | export function getSchemaFromFolder( 14 | dirPath: string, 15 | provider?: string 16 | ): string { 17 | const typesArray = loadFilesSync( 18 | path.join(dirPath, provider ? `${provider}*` : ''), 19 | { 20 | extensions: ['graphql'], 21 | } 22 | ) 23 | return print(mergeTypeDefs(typesArray)) 24 | } 25 | 26 | export const generateSchemaMapDynamically = ( 27 | provider: string, 28 | resources: string[] 29 | ): { [schemaName: string]: string } => { 30 | const resourceTypeNamesToFieldsMap: { [schemaName: string]: string } = {} 31 | 32 | for (const resource of resources) { 33 | const schemaName = `${provider}${resource 34 | .charAt(0) 35 | .toUpperCase()}${resource.slice(1)}` 36 | 37 | resourceTypeNamesToFieldsMap[resource] = schemaName 38 | } 39 | return resourceTypeNamesToFieldsMap 40 | } 41 | 42 | const generateDeleteMutation = (schemaName: string): string => 43 | `mutation delete${schemaName}($input: [String!]!){delete${schemaName}(filter: { id: { in: $input }}) { numUids } }` 44 | 45 | const generateUpsertMutation = (schemaName: string): string => 46 | `mutation($input: [Add${schemaName}Input!]!) { add${schemaName}(input: $input, upsert: true) { numUids } }` 47 | 48 | export const generateEntityMutations = ( 49 | schemaName: string 50 | ): EntityMutations => { 51 | return { 52 | upsert: generateUpsertMutation(schemaName), 53 | delete: generateDeleteMutation(schemaName), 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/operators/common.test.ts: -------------------------------------------------------------------------------- 1 | import CommonOperators from '../../src/rules-engine/operators/common' 2 | 3 | describe('Common Operators', () => { 4 | describe('isEmpty Operator', () => { 5 | test('Should pass given an empty array', () => { 6 | expect(CommonOperators.isEmpty([], true)).toBeTruthy() 7 | }) 8 | test('Should fail given an empty array', () => { 9 | expect(CommonOperators.isEmpty([], false)).toBeFalsy() 10 | }) 11 | test('Should pass given a filled array', () => { 12 | expect(CommonOperators.isEmpty(['filled'], true)).toBeFalsy() 13 | }) 14 | test('Should fail given a filled array', () => { 15 | expect(CommonOperators.isEmpty(['filled'], false)).toBeTruthy() 16 | }) 17 | test('Should pass given a filled object', () => { 18 | expect(CommonOperators.isEmpty({ key: 'one' }, false)).toBeTruthy() 19 | }) 20 | test('Should fail given a empty object', () => { 21 | expect(CommonOperators.isEmpty({}, false)).toBeFalsy() 22 | }) 23 | test('Should pass given a empty object', () => { 24 | expect(CommonOperators.isEmpty({}, true)).toBeTruthy() 25 | }) 26 | test('Should fail given a filled object', () => { 27 | expect(CommonOperators.isEmpty({ key: 'one' }, true)).toBeFalsy() 28 | }) 29 | test('Should fail given a null value', () => { 30 | expect(CommonOperators.isEmpty(null, false)).toBeFalsy() 31 | }) 32 | test('Should fail given an undefined value', () => { 33 | expect(CommonOperators.isEmpty(undefined, false)).toBeFalsy() 34 | }) 35 | test('Should fail given an integer', () => { 36 | expect(CommonOperators.isEmpty(33, true)).toBeFalsy() 37 | }) 38 | test('Should fail given a string', () => { 39 | expect(CommonOperators.isEmpty('string', true)).toBeFalsy() 40 | }) 41 | test('Should fail given a boolean', () => { 42 | expect(CommonOperators.isEmpty(true, true)).toBeFalsy() 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: publish 3 | 4 | on: 5 | push: 6 | branches: 7 | - alpha 8 | - beta 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | persist-credentials: false 19 | token: ${{secrets.gh_token}} 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: 16 23 | registry-url: "https://registry.npmjs.org" 24 | 25 | - name: Get cache directory 26 | id: yarn-cache-dir 27 | run: | 28 | echo "::set-output name=dir::$(yarn cache dir)" 29 | 30 | - name: Restoring cache 31 | uses: actions/cache@v3 32 | id: yarn-cache # use this to check for `cache-hit` ==> if: steps.yarn-cache.outputs.cache-hit != 'true' 33 | with: 34 | path: ${{ steps.yarn-cache-dir.outputs.dir }} 35 | key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} 36 | restore-keys: | 37 | ${{ runner.os }}-yarn- 38 | 39 | - name: Install Packages 40 | # NOTE: The --ignore-scripts flag is required to prevent leakage of NPM_TOKEN value 41 | # See https://github.com/actions/setup-node/blob/main/docs/advanced-usage.md#use-private-packages 42 | run: yarn install --frozen-lockfile --prefer-offline --ignore-scripts 43 | 44 | - name: Build 45 | run: yarn prepack 46 | 47 | - name: Test 48 | run: yarn test 49 | 50 | - name: Publish 51 | run: npx semantic-release 52 | env: 53 | NODE_ENV: "cicd" 54 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 55 | GITHUB_TOKEN: ${{secrets.gh_token}} 56 | GIT_AUTHOR_NAME: "autocloud-deploy-bot" 57 | GIT_AUTHOR_EMAIL: "no-reply@autocloud.dev" 58 | GIT_COMMITTER_NAME: "autocloud-deploy-bot" 59 | GIT_COMMITTER_EMAIL: "no-reply@autocloud.dev" 60 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import logger, { Logger } from './logger' 2 | import Client from './client' 3 | import RulesEngine from './rules-engine' 4 | import { 5 | StorageEngineConnectionConfig, 6 | StorageEngineConfig, 7 | StorageEngine, 8 | GraphQLInputData, 9 | GraphQLQueryData, 10 | } from './storage' 11 | import { 12 | Opts, 13 | Service, 14 | ServiceConnection, 15 | ProviderData, 16 | Entity, 17 | EntityMutations, 18 | SchemaMap, 19 | } from './types' 20 | import { 21 | Rule, 22 | RuleFinding, 23 | Result, 24 | JsRule, 25 | JsonRule, 26 | Engine, 27 | } from './rules-engine/types' 28 | import Plugin, { 29 | PluginManager, 30 | PluginModule, 31 | PluginType, 32 | ConfiguredPlugin, 33 | } from './plugins/types' 34 | import pluginMap from './plugins/base/pluginMap' 35 | import { 36 | sortResourcesDependencies, 37 | intersectStringArrays, 38 | getKeyByValue, 39 | toCamel, 40 | generateUniqueId, 41 | } from './utils' 42 | import { 43 | mergeSchemas, 44 | getSchemaFromFolder, 45 | generateSchemaMapDynamically, 46 | generateEntityMutations, 47 | } from './utils/schema' 48 | 49 | // Export Utils 50 | export { 51 | sortResourcesDependencies, 52 | intersectStringArrays, 53 | getKeyByValue, 54 | toCamel, 55 | mergeSchemas, 56 | getSchemaFromFolder, 57 | generateSchemaMapDynamically, 58 | generateEntityMutations, 59 | generateUniqueId, 60 | } 61 | 62 | export { PluginModule, PluginType, Result, pluginMap } 63 | 64 | export type { 65 | Opts, 66 | Service, 67 | ServiceConnection, 68 | Logger, 69 | Client, 70 | ProviderData, 71 | Engine, 72 | Rule, 73 | RuleFinding, 74 | JsRule, 75 | JsonRule, 76 | Entity, 77 | EntityMutations, 78 | StorageEngineConnectionConfig, 79 | StorageEngineConfig, 80 | StorageEngine, 81 | GraphQLInputData, 82 | GraphQLQueryData, 83 | SchemaMap, 84 | Plugin, 85 | PluginManager, 86 | ConfiguredPlugin, 87 | } 88 | export default { 89 | logger, 90 | Client, 91 | RulesEngine, 92 | } 93 | -------------------------------------------------------------------------------- /tests/evaluators/manual-evaluator.test.ts: -------------------------------------------------------------------------------- 1 | import cuid from 'cuid' 2 | import { Result } from '../../src' 3 | import ManualEvaluator from '../../src/rules-engine/evaluators/manual-evaluator' 4 | import { Severity } from '../../src/rules-engine/types' 5 | 6 | const manualRule = { 7 | id: cuid(), 8 | description: 'none', 9 | title: 'Mocked Manual Rule', 10 | rationale: 'Ikigai', 11 | audit: 'evaluate schemaA', 12 | remediation: 'fix the schemaA', 13 | references: [], 14 | severity: Severity.HIGH, 15 | } 16 | export default { 17 | manualRule, 18 | } 19 | 20 | describe('ManualEvaluator', () => { 21 | let evaluator 22 | beforeEach(() => { 23 | evaluator = new ManualEvaluator() 24 | }) 25 | 26 | it('should pass when it does not contain gql, conditions, check, and resource fields', () => { 27 | expect(evaluator.canEvaluate(manualRule)).toBe(true) 28 | }) 29 | 30 | it('should fail when it contains gql, conditions, check, or resource fields', () => { 31 | // Fail with resource field 32 | expect(evaluator.canEvaluate({ resource: '' } as never)).toBe(false) 33 | // Fail with gql field 34 | expect(evaluator.canEvaluate({ gql: '' } as never)).toBe(false) 35 | // Fail with conditions field 36 | expect(evaluator.canEvaluate({ conditions: {} } as never)).toBe(false) 37 | }) 38 | 39 | it('should return skipped as result', async () => { 40 | const spy = jest.fn() 41 | spy.mockReturnValue(false) 42 | const finding = await evaluator.evaluateSingleResource( 43 | { check: spy } as never, 44 | { resource: { id: cuid() } } as never 45 | ) 46 | expect(finding.result).toEqual(Result.SKIPPED) 47 | }) 48 | 49 | it('should contain "manual" at the id', async () => { 50 | const spy = jest.fn() 51 | spy.mockReturnValue(false) 52 | const finding = await evaluator.evaluateSingleResource( 53 | { check: spy } as never, 54 | { resource: { id: cuid() } } as never 55 | ) 56 | expect(finding.id.includes('manual')).toEqual(true) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /tests/operators/array.test.ts: -------------------------------------------------------------------------------- 1 | import ArrayOperators from '../../src/rules-engine/operators/array' 2 | 3 | describe('Array Operators', () => { 4 | describe('In Operator', () => { 5 | test('Should pass given a filled array with the searched element', () => { 6 | expect(ArrayOperators.in('rule', ['rule'])).toBeTruthy() 7 | }) 8 | 9 | test('Should fail given a filled array without the searched element', () => { 10 | expect(ArrayOperators.in('rule', [])).toBeFalsy() 11 | }) 12 | 13 | test('Should fail given an empty array', () => { 14 | expect(ArrayOperators.in('rule', [])).toBeFalsy() 15 | }) 16 | }) 17 | 18 | describe('NotIn Operator', () => { 19 | test('Should fail given a filled array with the searched element', () => { 20 | expect(ArrayOperators.notIn('rule', ['rule'])).toBeFalsy() 21 | }) 22 | 23 | test('Should fail given a filled array without the searched element', () => { 24 | expect(ArrayOperators.notIn('rule', [])).toBeTruthy() 25 | }) 26 | 27 | test('Should fail given an empty array', () => { 28 | expect(ArrayOperators.notIn('rule', [])).toBeTruthy() 29 | }) 30 | }) 31 | 32 | describe('Contains Operator', () => { 33 | test('Should pass given a filled array with the searched element', () => { 34 | expect(ArrayOperators.contains(['*', 'rule'], '*')).toBeTruthy() 35 | }) 36 | 37 | test('Should fail given a filled array without the searched element', () => { 38 | expect(ArrayOperators.contains(['*', 'rule'], 'x')).toBeFalsy() 39 | }) 40 | 41 | test('Should fail given an empty array', () => { 42 | expect(ArrayOperators.contains([], 'x')).toBeFalsy() 43 | }) 44 | }) 45 | 46 | describe('DoesNotContain Operator', () => { 47 | test('Should fail given a filled array with the searched element', () => { 48 | expect(ArrayOperators.doesNotContain(['rule'], 'rule')).toBeFalsy() 49 | }) 50 | 51 | test('Should pass given a filled array without the searched element', () => { 52 | expect(ArrayOperators.doesNotContain(['*'], 'rule')).toBeTruthy() 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /src/utils/data.ts: -------------------------------------------------------------------------------- 1 | import { ProviderData, SchemaMap } from '../types' 2 | 3 | const getLinkedData = (providerData: ProviderData, schemasMap?: SchemaMap): any => { 4 | const linkedData = {} 5 | const allEntities = providerData?.entities || [] 6 | const allConnections = providerData?.connections || {} 7 | const entitiesById: { [key: string]: any } = {} 8 | 9 | for (const entity of allEntities) { 10 | // AddawsEc2Input! => queryawsEc2 11 | const mutationName = /(?<=\[)(.*?)(?=\])/ 12 | .exec(entity.mutation as any)[0] 13 | .replace('Add', 'query') 14 | .replace('Input!', '') 15 | 16 | linkedData[mutationName] = entity.data 17 | 18 | for (const entityData of entity.data) { 19 | entitiesById[entityData.id] = entityData 20 | // eslint-disable-next-line no-underscore-dangle 21 | entityData.__typename = mutationName.replace('query', '') // or entity.name? 22 | } 23 | } 24 | 25 | // connect data on a second pass 26 | for (const entityId of Object.keys(allConnections)) { 27 | const entityConnections = allConnections[entityId] 28 | const entity = entitiesById[entityId] 29 | if (!entity) { 30 | // eslint-disable-next-line no-continue 31 | continue 32 | } 33 | for (const conn of entityConnections) { 34 | const targetEntity = entitiesById[conn.id] 35 | if (!targetEntity) { 36 | // eslint-disable-next-line no-continue 37 | continue 38 | } 39 | if (conn.relation === 'child') { 40 | if (!entity[conn.field]) { 41 | entity[conn.field] = [] 42 | } 43 | entity[conn.field].push(targetEntity) 44 | // inverse relation 45 | // eslint-disable-next-line no-underscore-dangle 46 | const inverseConnField = schemasMap && schemasMap[entity.__typename] || 'account' // @TODO: account doesn't have a name 47 | if (!targetEntity[inverseConnField]) { 48 | targetEntity[inverseConnField] = [] 49 | } 50 | targetEntity[inverseConnField].push(entity) 51 | } // else parent relation.. is not used atm 52 | } 53 | } 54 | 55 | return linkedData 56 | } 57 | 58 | export default getLinkedData 59 | -------------------------------------------------------------------------------- /tests/evaluators/js-evaluator.test.ts: -------------------------------------------------------------------------------- 1 | import cuid from 'cuid' 2 | import { Result } from '../../src' 3 | import JsEvaluator from '../../src/rules-engine/evaluators/js-evaluator' 4 | import { Severity } from '../../src/rules-engine/types' 5 | 6 | const jsRule = { 7 | id: cuid(), 8 | description: 'none', 9 | title: 'Mocked Automated Rule', 10 | rationale: "raison d'être", 11 | audit: 'evaluate schemaA', 12 | remediation: 'fix the schemaA', 13 | references: [], 14 | gql: `{ 15 | querySchemaA { 16 | id 17 | __typename 18 | value 19 | } 20 | }`, 21 | resource: 'querySchemaA[*]', 22 | check: jest.fn(), 23 | severity: Severity.HIGH, 24 | } 25 | 26 | export default { 27 | jsRule, 28 | } 29 | 30 | describe('JsEvaluator', () => { 31 | let evaluator 32 | beforeEach(() => { 33 | evaluator = new JsEvaluator() 34 | }) 35 | 36 | it('should accept all rules that have a check field', () => { 37 | expect(evaluator.canEvaluate({} as never)).toBe(false) 38 | expect(evaluator.canEvaluate({ checks: 1 } as never)).toBe(false) 39 | 40 | // we could improve these, but following tests will pass 41 | expect(evaluator.canEvaluate({ check: 1 } as never)).toBe(true) 42 | expect(evaluator.canEvaluate({ check: 0 } as never)).toBe(true) 43 | expect(evaluator.canEvaluate({ check: () => 1 } as never)).toBe(true) 44 | }) 45 | 46 | it('should call check with data', () => { 47 | const spy = jest.fn() 48 | const data = 'asdf' 49 | evaluator.evaluateSingleResource({ check: spy } as never, data as never) 50 | 51 | expect(spy).toHaveBeenCalledWith(data) 52 | }) 53 | 54 | it('should return fail if rule is true', async () => { 55 | const spy = jest.fn() 56 | spy.mockReturnValue(false) 57 | const failedRule = await evaluator.evaluateSingleResource( 58 | { check: spy } as never, 59 | { resource: { id: cuid() } } as never 60 | ) 61 | expect(failedRule.result).toEqual(Result.FAIL) 62 | spy.mockReturnValue(true) 63 | const passedRule = await evaluator.evaluateSingleResource( 64 | { check: spy } as never, 65 | { resource: { id: cuid() } } as never 66 | ) 67 | expect(passedRule.result).toEqual(Result.PASS) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import camelCase from 'lodash/camelCase' 3 | 4 | export const toCamel = (o: any): any => { 5 | let origKey 6 | let newKey 7 | let value 8 | 9 | if (o instanceof Array) { 10 | return o.map(value => { 11 | if (typeof value === 'object') { 12 | value = toCamel(value) 13 | } 14 | return value 15 | }) 16 | } 17 | 18 | const newObject = {} 19 | for (origKey in o) { 20 | if (o.hasOwnProperty(origKey)) { 21 | newKey = camelCase(origKey) 22 | value = o[origKey] 23 | if ( 24 | value instanceof Array || 25 | (value !== null && value !== undefined && value.constructor === Object) 26 | ) { 27 | value = toCamel(value) 28 | } 29 | newObject[newKey] = value 30 | } 31 | } 32 | 33 | return newObject 34 | } 35 | 36 | export const getKeyByValue = ( 37 | object: Record, 38 | value: any 39 | ): string | undefined => { 40 | return Object.keys(object).find(key => object[key] === value) 41 | } 42 | 43 | export const intersectStringArrays = ( 44 | a: Array, 45 | b: Array 46 | ): Array => { 47 | const setA = new Set(a) 48 | const setB = new Set(b) 49 | const intersection = new Set([...setA].filter(x => setB.has(x))) 50 | return Array.from(intersection) 51 | } 52 | 53 | /** 54 | * Sorts a services list depending on his dependencies 55 | * @param relationsMap share data services map 56 | * @param resourceNames services to sort 57 | * @returns sorted list of services 58 | */ 59 | export const sortResourcesDependencies = ( 60 | relationsMap: { [parent: string]: string[] }, 61 | resourceNames: string[] 62 | ): string[] => 63 | resourceNames.sort((prevResource, nextResource) => { 64 | const dependecies = relationsMap[prevResource] 65 | 66 | if (dependecies && dependecies.includes(nextResource)) { 67 | return -1 68 | } 69 | return 0 70 | }) 71 | 72 | /** 73 | * Create an unique hash identifier 74 | * @param entity entire entity to create identifier 75 | * @returns unique identifier 76 | */ 77 | export const generateUniqueId = (entity: any): string => 78 | crypto.createHash('md5').update(JSON.stringify(entity)).digest('hex') 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudgraph/sdk", 3 | "version": "0.23.0", 4 | "description": "SDK for cloud graph providers and cli", 5 | "main": "dist/src/index.js", 6 | "types": "dist/src/index.d.ts", 7 | "publishConfig": { 8 | "registry": "https://registry.npmjs.org/", 9 | "access": "public" 10 | }, 11 | "homepage": "https://www.cloudgraph.dev/", 12 | "repository": "github:cloudgraphdev/sdk", 13 | "bugs": "https://github.com/cloudgraphdev/sdk/issues", 14 | "scripts": { 15 | "build": "yarn prepack", 16 | "prepack": "rm -rf dist && npx tsc", 17 | "test": "NODE_ENV=test jest", 18 | "lint": "eslint --config .eslintrc.json --ext .js,.ts ./", 19 | "lint:fix": "eslint --fix --config .eslintrc.json --ext .js,.ts ./" 20 | }, 21 | "author": "AutoCloud", 22 | "license": "MPL-2.0", 23 | "dependencies": { 24 | "@graphql-tools/load-files": "^6.5.3", 25 | "@graphql-tools/merge": "^8.2.1", 26 | "chalk": "^4.1.1", 27 | "date-fns": "^2.25.0", 28 | "graphql": "^16.2.0", 29 | "inquirer": "^8.1.2", 30 | "jsonpath": "^1.1.1", 31 | "lodash": "^4.17.21", 32 | "ora": "^5.4.1" 33 | }, 34 | "devDependencies": { 35 | "@autocloud/eslint-config": "^0.1.0", 36 | "@semantic-release/changelog": "^6.0.1", 37 | "@semantic-release/git": "^10.0.1", 38 | "@semantic-release/github": "^8.0.1", 39 | "@semantic-release/npm": "^9.0.1", 40 | "@types/jest": "^29.2.3", 41 | "@types/node": "^16.4.12", 42 | "@typescript-eslint/eslint-plugin": "^4.28.5", 43 | "@typescript-eslint/parser": "^4.28.5", 44 | "cuid": "^2.1.8", 45 | "eslint": "^7.2.0", 46 | "eslint-config-airbnb-base": "14.2.1", 47 | "eslint-config-prettier": "^6.11.0", 48 | "eslint-plugin-import": "^2.22.1", 49 | "eslint-plugin-prettier": "^3.4.0", 50 | "husky": "^4.3.0", 51 | "jest": "^29.3.1", 52 | "lint-staged": "^11.1.1", 53 | "semantic-release": "^19.0.2", 54 | "ts-jest": "^29.0.3", 55 | "typescript": "^4.3.5" 56 | }, 57 | "husky": { 58 | "hooks": { 59 | "pre-commit": "yarn lint-staged" 60 | } 61 | }, 62 | "lint-staged": { 63 | "*.{ts,graphql,json}": [ 64 | "yarn lint:fix", 65 | "git add --force" 66 | ] 67 | }, 68 | "resolutions": { 69 | "**/uri-js": "^3.0.1" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/operators/date.test.ts: -------------------------------------------------------------------------------- 1 | import cuid from 'cuid' 2 | import { Result } from '../../src' 3 | import JsonEvaluator from '../../src/rules-engine/evaluators/json-evaluator' 4 | 5 | describe('Date Operators', () => { 6 | let evaluator 7 | beforeEach(() => { 8 | evaluator = new JsonEvaluator() 9 | }) 10 | 11 | test('should process dates', async () => { 12 | const day = 1000 * 60 * 60 * 24 // @TODO - replace by some date library (that is not moment) 13 | const now = Date.now() 14 | const data = { 15 | data: { 16 | users: [{ passwordDate: new Date(now - 15 * day).toISOString() }], 17 | }, 18 | resource: { 19 | id: cuid(), 20 | }, 21 | } as any 22 | const rule: any = { 23 | value: { path: 'users[0].passwordDate', daysAgo: {} }, 24 | lessThan: 10, 25 | } 26 | 27 | rule.lessThan = 10 28 | let finding = await evaluator.evaluateSingleResource( 29 | { conditions: rule } as any, 30 | data 31 | ) 32 | expect(finding.result).toBe(Result.FAIL) 33 | 34 | rule.lessThan = 20 35 | finding = await evaluator.evaluateSingleResource( 36 | { conditions: rule } as any, 37 | data 38 | ) 39 | expect(finding.result).toBe(Result.PASS) 40 | }) 41 | 42 | test('should calculate days diff between two date', async () => { 43 | const day = 1000 * 60 * 60 * 24 // @TODO - replace by some date library (that is not moment) 44 | const now = Date.now() 45 | const data = { 46 | data: { 47 | cryptoKeys: [ 48 | { nextRotationTime: new Date(now + 90 * day).toISOString() }, 49 | ], 50 | }, 51 | resource: { 52 | id: cuid(), 53 | }, 54 | } as any 55 | const rule: any = { 56 | value: { path: 'cryptoKeys[0].nextRotationTime', daysDiff: {} }, 57 | lessThanInclusive: 90, 58 | } 59 | 60 | rule.lessThanInclusive = 80 61 | let finding = await evaluator.evaluateSingleResource( 62 | { conditions: rule } as any, 63 | data 64 | ) 65 | 66 | expect(finding.result).toBe(Result.FAIL) 67 | 68 | rule.lessThanInclusive = 90 69 | finding = await evaluator.evaluateSingleResource( 70 | { conditions: rule } as any, 71 | data 72 | ) 73 | 74 | expect(finding.result).toBe(Result.PASS) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /src/rules-engine/types.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../types' 2 | 3 | export type ResourceData = { 4 | data: { [k: string]: any } 5 | resource: { id: string; [k: string]: any } 6 | resourcePath: string 7 | } 8 | 9 | export type Condition = { 10 | path?: string 11 | value?: string | number | Condition | (string | number)[] 12 | [operationId: string]: any 13 | } 14 | 15 | export type Operator = ( 16 | mainArg: any, 17 | otherArgs: any[], 18 | data: ResourceData & { elementPath?: string } 19 | ) => boolean | number | Promise 20 | 21 | export type _ResourceData = ResourceData & { elementPath?: string } 22 | 23 | export enum Severity { 24 | HIGH = 'high', 25 | MEDIUM = 'medium', 26 | LOW = 'low', 27 | } 28 | 29 | export enum RuleResult { 30 | DOESNT_MATCH = 'DOESNT_MATCH', 31 | MATCHES = 'MATCHES', 32 | MISSING = 'MISSING', 33 | } 34 | 35 | export enum Result { 36 | FAIL = 'FAIL', 37 | PASS = 'PASS', 38 | MISSING = 'MISSING', 39 | SKIPPED = 'SKIPPED', 40 | } 41 | export interface Rule { 42 | id: string 43 | title: string 44 | description: string 45 | references: string[] 46 | rationale?: string 47 | audit?: string 48 | remediation?: string 49 | severity: Severity 50 | gql: string 51 | resource: string 52 | queries?: { gql: string; conditions: Condition }[] 53 | } 54 | export interface Finding { 55 | id: string 56 | resourceId?: string 57 | result: Result 58 | typename: string 59 | } 60 | 61 | export interface RuleFinding extends Finding { 62 | rule?: Rule 63 | } 64 | 65 | export interface JsonRule extends Rule { 66 | conditions: Condition 67 | exclude?: Condition 68 | } 69 | 70 | export interface JsRule extends Rule { 71 | check?: (data: any) => boolean 72 | } 73 | 74 | export interface Engine { 75 | /** 76 | * Returns an GraphQL schema build dynamically based on the provider and existing resources 77 | * @returns new schemas and extensions for existing ones 78 | */ 79 | getSchema: () => string[] 80 | 81 | /** 82 | * Process a rule for the given data 83 | * @param rule rule to apply 84 | * @param data data to evaluate 85 | * @returns An array of RuleFinding 86 | */ 87 | processRule: (rule: Rule, data: any) => Promise 88 | 89 | /** 90 | * Transforms RuleFinding array into a mutation array for GraphQL 91 | * @param findings resulted findings during rules execution 92 | * @param rules rules metadata 93 | * @returns {Entity[]} Array of generated mutations 94 | */ 95 | prepareMutations: (findings: RuleFinding[], rules: Rule[]) => Entity[] 96 | } 97 | -------------------------------------------------------------------------------- /tests/operators/regex.test.ts: -------------------------------------------------------------------------------- 1 | import RegexOperators from '../../src/rules-engine/operators/regex' 2 | 3 | describe('Regex Operators', () => { 4 | describe('match Operator', () => { 5 | test('Should pass given a valid email', () => { 6 | expect( 7 | RegexOperators.match( 8 | 'john.doe@autocloud.dev', 9 | /[\w\d.-]+@[\w\d.-]+\.[\w\d.-]+/ 10 | ) 11 | ).toBeTruthy() 12 | }) 13 | test('Should fail given an invalid email', () => { 14 | expect( 15 | RegexOperators.match('john.doe', /[\w\d.-]+@[\w\d.-]+\.[\w\d.-]+/) 16 | ).toBeFalsy() 17 | }) 18 | }) 19 | 20 | describe('mismatch Operator', () => { 21 | test('Should pass given an invalid email', () => { 22 | expect( 23 | RegexOperators.mismatch('john.doe', /[\w\d.-]+@[\w\d.-]+\.[\w\d.-]+/) 24 | ).toBeTruthy() 25 | }) 26 | 27 | test('Should fail given a valid email', () => { 28 | expect( 29 | RegexOperators.mismatch( 30 | 'john.doe@autocloud.dev', 31 | /[\w\d.-]+@[\w\d.-]+\.[\w\d.-]+/ 32 | ) 33 | ).toBeFalsy() 34 | }) 35 | }) 36 | 37 | describe('matchAny Operator', () => { 38 | test('Should fail given an invalid array', () => { 39 | expect( 40 | RegexOperators.matchAny('john.doe', /[\w\d.-]+@[\w\d.-]+\.[\w\d.-]+/) 41 | ).toBeFalsy() 42 | }) 43 | 44 | test('Should fail given an array with invalid emails', () => { 45 | expect( 46 | RegexOperators.matchAny(['john.doe'], /[\w\d.-]+@[\w\d.-]+\.[\w\d.-]+/) 47 | ).toBeFalsy() 48 | }) 49 | 50 | test('Should pass given at least one valid email', () => { 51 | expect( 52 | RegexOperators.matchAny( 53 | ['john.doe', 'matt.dAvella@autocloud.dev'], 54 | /[\w\d.-]+@[\w\d.-]+\.[\w\d.-]+/ 55 | ) 56 | ).toBeTruthy() 57 | }) 58 | }) 59 | 60 | describe('matchAll Operator', () => { 61 | test('Should fail given an invalid array', () => { 62 | expect( 63 | RegexOperators.matchAll('john.doe', /[\w\d.-]+@[\w\d.-]+\.[\w\d.-]+/) 64 | ).toBeFalsy() 65 | }) 66 | 67 | test('Should fail given an array with invalid emails', () => { 68 | expect( 69 | RegexOperators.matchAll(['john.doe'], /[\w\d.-]+@[\w\d.-]+\.[\w\d.-]+/) 70 | ).toBeFalsy() 71 | }) 72 | 73 | test('Should fail given an array of valid emails', () => { 74 | expect( 75 | RegexOperators.matchAll( 76 | ['john.doe@autocloud.dev', 'matt.dAvella@autocloud.dev'], 77 | /[\w\d.-]+@[\w\d.-]+\.[\w\d.-]+/ 78 | ) 79 | ).toBeTruthy() 80 | }) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/node 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | .pnpm-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # Snowpack dependency directory (https://snowpack.dev/) 50 | web_modules/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | .env.test 79 | .env.production 80 | 81 | # parcel-bundler cache (https://parceljs.org/) 82 | .cache 83 | .parcel-cache 84 | 85 | # Next.js build output 86 | .next 87 | out 88 | 89 | # Nuxt.js build / generate output 90 | .nuxt 91 | dist 92 | 93 | # Gatsby files 94 | .cache/ 95 | # Comment in the public line in if your project uses Gatsby and not Next.js 96 | # https://nextjs.org/blog/next-9-1#public-directory-support 97 | # public 98 | 99 | # vuepress build output 100 | .vuepress/dist 101 | 102 | # Serverless directories 103 | .serverless/ 104 | 105 | # FuseBox cache 106 | .fusebox/ 107 | 108 | # DynamoDB Local files 109 | .dynamodb/ 110 | 111 | # TernJS port file 112 | .tern-port 113 | 114 | # Stores VSCode versions used for testing VSCode extensions 115 | .vscode-test 116 | 117 | # yarn v2 118 | .yarn/cache 119 | .yarn/unplugged 120 | .yarn/build-state.yml 121 | .yarn/install-state.gz 122 | .pnp.* 123 | 124 | # End of https://www.toptal.com/developers/gitignore/api/node 125 | 126 | .vscode -------------------------------------------------------------------------------- /tests/rules-engine.test.ts: -------------------------------------------------------------------------------- 1 | import RulesProvider from '../src/rules-engine' 2 | import { Engine, Rule } from '../src/rules-engine/types' 3 | import ManualEvaluatorMock from './evaluators/manual-evaluator.test' 4 | import JSONEvaluatordMock from './evaluators/json-evaluator.test' 5 | import DgraphDataProcessor from '../src/rules-engine/data-processors/dgraph-data-processor' 6 | 7 | const typenameToFieldMap = { 8 | resourceA: 'querySchemaA', 9 | resourceB: 'querySchemaB', 10 | } 11 | const providerName = 'aws' 12 | const entityName = 'CIS' 13 | 14 | describe('RulesEngine', () => { 15 | let rulesEngine: Engine 16 | beforeEach(() => { 17 | const dataProcessor = new DgraphDataProcessor({ 18 | providerName, 19 | entityName, 20 | typenameToFieldMap, 21 | }) 22 | rulesEngine = new RulesProvider(dataProcessor) 23 | }) 24 | it('Should pass getting the updated schema created dynamically using schemaTypeName and typenameToFieldMap fields', () => { 25 | const schema = rulesEngine.getSchema() 26 | 27 | expect(schema).toBeDefined() 28 | expect(schema.length).toBe(Object.keys(typenameToFieldMap).length) 29 | 30 | schema.forEach((resourceSchema, index) => { 31 | expect(resourceSchema).toContain(Object.keys(typenameToFieldMap)[index]) 32 | expect(resourceSchema).toContain(Object.values(typenameToFieldMap)[index]) 33 | }) 34 | }) 35 | 36 | it('Should pass preparing the mutations to insert findings data given a RuleFindings array with Manual Rules', async () => { 37 | const findings = await rulesEngine.processRule( 38 | ManualEvaluatorMock.manualRule as Rule, 39 | undefined 40 | ) 41 | const [mutations] = rulesEngine.prepareMutations(findings, []) 42 | 43 | expect(findings.length).toBe(1) 44 | expect(mutations).toBeDefined() 45 | expect(mutations.data instanceof Array).toBeTruthy() 46 | expect(mutations.data.length).toBe(1) 47 | expect(mutations.name).toBe(`${providerName}${entityName}Findings`) 48 | expect(mutations.mutation).toContain( 49 | `add${providerName}${entityName}Findings` 50 | ) 51 | }) 52 | 53 | it('Should return an empty array processing a rule with no data', async () => { 54 | const data = {} 55 | 56 | const processedRule = await rulesEngine.processRule( 57 | JSONEvaluatordMock.jsonRule, 58 | data 59 | ) 60 | 61 | expect(processedRule).toBeDefined() 62 | expect(processedRule instanceof Array).toBeTruthy() 63 | expect(processedRule.length).toBe(0) 64 | }) 65 | 66 | it('Should return empty mutations array given an empty findings array', () => { 67 | const data = [] 68 | 69 | const entities = rulesEngine.prepareMutations(data, []) 70 | 71 | expect(entities).toBeDefined() 72 | expect(entities.length).toBe(0) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | 1. [Reporting security problems to CloudGraph](#reporting) 4 | 2. [Security Point of Contact](#contact) 5 | 3. [Incident Response Process](#process) 6 | 7 | 8 | ## Reporting security problems to CloudGraph 9 | 10 | **DO NOT CREATE AN ISSUE** to report a security problem. Instead, please 11 | send an email to security@autocloud.dev 12 | 13 | 14 | ## Security Point of Contact 15 | 16 | The security point of contact is Tyler Dunkel. Tyler responds to security 17 | incident reports as fast as possible, within one business day at the latest. 18 | 19 | In case Tyler does not respond within a reasonable time, the secondary point 20 | of contact is [Tyson Kunovsky](https://github.com/orgs/cloudgraphdev/people/kunovsky). 21 | 22 | If neither Tyler nor Tyson responds then please contact support@github.com 23 | who can disable any access for the CloudGraph CLI tool until the security incident is resolved. 24 | 25 | 26 | ## Incident Response Process 27 | 28 | In case an incident is discovered or reported, CloudGraph will follow the following 29 | process to contain, respond and remediate: 30 | 31 | ### 1. Containment 32 | 33 | The first step is to find out the root cause, nature and scope of the incident. 34 | 35 | - Is still ongoing? If yes, first priority is to stop it. 36 | - Is the incident outside of my influence? If yes, first priority is to contain it. 37 | - Find out knows about the incident and who is affected. 38 | - Find out what data was potentially exposed. 39 | 40 | One way to immediately remove all access for CloudGraph is to uninstall CloudGraph globally and/or locally using 41 | `npm uninstall -g @cloudgraph/cli` && `npm uninstall @cloudgraph/cli` 42 | 43 | ### 2. Response 44 | 45 | After the initial assessment and containment to out best abilities, CloudGraph will 46 | document all actions taken in a response plan. 47 | 48 | CloudGraph will create an RCA (Root Cause Analysis) document in the [CloudGraph documentation site](https://docs.cloudgraph.dev/overview) that describes what happened and what was done to resolve it. 49 | 50 | ### 3. Remediation 51 | 52 | Once the incident is confirmed to be resolved, CloudGraph will summarize the lessons 53 | learned from the incident and create a list of actions CloudGraph will take to prevent 54 | it from happening again. 55 | 56 | ### Keep permissions to a minimum 57 | 58 | The CloudGraph CLI tool uses the least amount of access to limit the impact of possible 59 | security incidents, see [README - How It Works](https://github.com/cloudgraphdev/cli#how-it-works). 60 | 61 | ### Secure accounts with access 62 | 63 | The [CloudGraph GitHub Organization](https://github.com/cloudgraphdev) requires 2FA authorization 64 | for all members. 65 | 66 | ### Critical Updates And Security Notices 67 | 68 | We learn about critical software updates and security threats from these sources 69 | 70 | 1. GitHub Security Alerts 71 | 2. [Snyk open source vulnerability dectection](https://snyk.io/product/open-source-security-management/) 72 | 3. GitHub: https://githubstatus.com/ & [@githubstatus](https://twitter.com/githubstatus) -------------------------------------------------------------------------------- /tests/plugins/policyPack.test.ts: -------------------------------------------------------------------------------- 1 | import PolicyPackPlugin from '../../src/plugins/policyPack' 2 | import Logger from '../../src/logger' 3 | import JSONRule from '../evaluators/json-evaluator.test' 4 | import ManualRule from '../evaluators/manual-evaluator.test' 5 | 6 | jest.mock('../../src/logger') 7 | 8 | const providerName = 'aws' 9 | const config = { 10 | resources: 'schemaA, schemaB', 11 | } 12 | const schemasMap = { 13 | schemaA: `${providerName}SchemaA`, 14 | schemaB: `${providerName}SchemaB`, 15 | } 16 | const policyPacks = [ 17 | { 18 | name: 'aws-cis-1.2.0', 19 | providers: ['aws'], 20 | }, 21 | ] 22 | 23 | const getPluginManager = (rules: any[]) => ({ 24 | getPlugin: jest.fn().mockImplementation(() => ({ 25 | default: { 26 | rules, 27 | entity: 'CIS', 28 | }, 29 | })), 30 | }) 31 | 32 | const storageEngine = { 33 | setSchema: jest.fn(), 34 | getSchema: jest.fn(), 35 | query: jest.fn(), 36 | run: jest.fn(), 37 | } 38 | 39 | const providerData = { 40 | entities: [], 41 | connections: {} 42 | } 43 | 44 | describe('PolicyPack Plugin', () => { 45 | let plugin 46 | beforeEach(() => { 47 | // Initialize 48 | plugin = new PolicyPackPlugin({ 49 | config, 50 | provider: { 51 | name: providerName, 52 | schemasMap, 53 | }, 54 | flags: {}, 55 | logger: Logger, 56 | }) 57 | storageEngine.setSchema.mockClear() 58 | storageEngine.getSchema.mockClear() 59 | storageEngine.query.mockClear() 60 | storageEngine.run.mockClear() 61 | }) 62 | 63 | test('Should configure and execute configured policy pack', async () => { 64 | // Get the Plugin Manager 65 | const pluginManager = getPluginManager([ 66 | JSONRule.jsonRule, 67 | ManualRule.manualRule, 68 | ]) 69 | 70 | // Configure Plugin 71 | await plugin.configure(pluginManager, policyPacks) 72 | 73 | expect(pluginManager.getPlugin).toHaveBeenCalledTimes(1) 74 | 75 | // Execute Plugin 76 | await plugin.execute({ 77 | storageRunning: true, 78 | storageEngine, 79 | providerData, 80 | processConnectionsBetweenEntities: jest.fn(), 81 | }) 82 | 83 | expect(storageEngine.getSchema).toHaveBeenCalled() 84 | expect(storageEngine.setSchema).toHaveBeenCalled() 85 | expect(storageEngine.query).toHaveBeenCalledTimes(0) 86 | expect(storageEngine.run).toHaveBeenCalledTimes(1) 87 | }) 88 | 89 | test('Should configure and execute configured policy pack with composite rule', async () => { 90 | // Get the Plugin Manager 91 | const pluginManager = getPluginManager([ 92 | JSONRule.jsonRule, 93 | JSONRule.compositeRule, 94 | ]) 95 | 96 | // Configure Plugin 97 | await plugin.configure(pluginManager, policyPacks) 98 | 99 | expect(pluginManager.getPlugin).toHaveBeenCalledTimes(1) 100 | 101 | // Execute Plugin 102 | await plugin.execute({ 103 | storageRunning: true, 104 | storageEngine, 105 | providerData, 106 | processConnectionsBetweenEntities: jest.fn(), 107 | }) 108 | 109 | expect(storageEngine.getSchema).toHaveBeenCalled() 110 | expect(storageEngine.setSchema).toHaveBeenCalled() 111 | expect(storageEngine.query).toHaveBeenCalledTimes(0) 112 | expect(storageEngine.run).toHaveBeenCalledTimes(1) 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /src/logger/index.ts: -------------------------------------------------------------------------------- 1 | import ora, { Ora } from 'ora' 2 | import fs from 'fs' 3 | 4 | import { LoggerInput } from '../types' 5 | 6 | export enum LogLevel { 7 | Silent = -1, 8 | Error = 0, 9 | Warn = 1, 10 | Log = 2, 11 | Info = 3, 12 | Success = 3, 13 | Debug = 4, 14 | Trace = 5, 15 | } 16 | 17 | export class Logger { 18 | constructor(debug: string) { 19 | const levelRange = ['-1', '0', '1', '2', '3', '4', '5'] 20 | this.level = levelRange.indexOf(debug) > -1 ? Number(debug) : 3 21 | this.errorLogFile = './cg-debug.log' 22 | this.logger = ora({ isSilent: this.level === LogLevel.Silent }) 23 | this.spinnerMap = {} 24 | this.clearLog() 25 | } 26 | 27 | level: LogLevel 28 | 29 | errorLogFile: string 30 | 31 | logger: Ora 32 | 33 | startText: string 34 | 35 | spinnerMap: { msg?: string; instance?: Ora } 36 | 37 | // Legacy log method 38 | log( 39 | msg: string | Array | any, 40 | options: { verbose?: boolean; level?: string } = {} 41 | ): void { 42 | const { verbose, level = 'info' } = options 43 | if (verbose) { 44 | // TODO: Implement verbose loggeer 45 | } else { 46 | this.logger[level](msg) 47 | } 48 | } 49 | 50 | private parseMessage(msg: LoggerInput): string { 51 | let value = msg 52 | // Handle error objects to enable them to be stringified 53 | if (msg instanceof Error) { 54 | if (this.level > 3) { 55 | value = msg.stack 56 | } else { 57 | value = msg.toString() 58 | } 59 | } 60 | if (typeof value === 'object') { 61 | return JSON.stringify(value, null, 2) 62 | } 63 | return String(value) 64 | } 65 | 66 | private clearLog(): void { 67 | if (this.level >= LogLevel.Trace) { 68 | fs.writeFileSync(this.errorLogFile, '') 69 | } 70 | } 71 | 72 | private writeLog(text: string): void { 73 | if (this.level >= LogLevel.Trace) { 74 | fs.appendFileSync(this.errorLogFile, `${text}\n`) 75 | } 76 | } 77 | 78 | startSpinner(msg: string): void { 79 | const instance = this.logger.start(msg) 80 | this.spinnerMap = { msg, instance } 81 | } 82 | 83 | successSpinner(msg: string): void { 84 | if (this.spinnerMap.instance) { 85 | const { instance } = this.spinnerMap 86 | instance.succeed(msg) 87 | this.spinnerMap = {} 88 | } 89 | } 90 | 91 | failSpinner(msg: string): void { 92 | if (this.spinnerMap.instance) { 93 | const { instance } = this.spinnerMap 94 | instance.fail(msg) 95 | this.spinnerMap = {} 96 | } 97 | } 98 | 99 | stopSpinner(): string { 100 | if (this.spinnerMap.instance) { 101 | const { instance } = this.spinnerMap 102 | instance.stop() 103 | } 104 | return this.spinnerMap.msg 105 | } 106 | 107 | /** 108 | * For the functions below, we can not log while a spinner is running. We try to stop the 109 | * spinner => log info/error/success/warn/debug => restart spinner if there is one 110 | */ 111 | info(msg: LoggerInput): void { 112 | if (this.level >= LogLevel.Info) { 113 | this.stopSpinner() 114 | this.logger.info(this.parseMessage(msg)) 115 | this.spinnerMap.instance && this.startSpinner(this.spinnerMap.msg) 116 | } 117 | } 118 | 119 | error(msg: LoggerInput): void { 120 | if (this.level >= LogLevel.Error) { 121 | const parsedMessage = this.parseMessage(msg) 122 | this.stopSpinner() 123 | this.logger.fail(parsedMessage) 124 | this.writeLog(parsedMessage) 125 | this.spinnerMap.instance && this.startSpinner(this.spinnerMap.msg) 126 | } 127 | } 128 | 129 | success(msg: LoggerInput): void { 130 | if (this.level >= LogLevel.Success) { 131 | this.stopSpinner() 132 | this.logger.succeed(this.parseMessage(msg)) 133 | this.spinnerMap.instance && this.startSpinner(this.spinnerMap.msg) 134 | } 135 | } 136 | 137 | warn(msg: LoggerInput): void { 138 | if (this.level >= LogLevel.Warn) { 139 | this.stopSpinner() 140 | this.logger.warn(this.parseMessage(msg)) 141 | this.spinnerMap.instance && this.startSpinner(this.spinnerMap.msg) 142 | } 143 | } 144 | 145 | debug(msg: LoggerInput): void { 146 | if (this.level >= LogLevel.Trace) { 147 | const parsedMessage = this.parseMessage(msg) 148 | this.stopSpinner() 149 | this.logger.info(parsedMessage) 150 | this.writeLog(parsedMessage) 151 | this.spinnerMap.instance && this.startSpinner(this.spinnerMap.msg) 152 | } 153 | } 154 | } 155 | 156 | export default new Logger(process.env.CG_DEBUG) 157 | -------------------------------------------------------------------------------- /src/rules-engine/index.ts: -------------------------------------------------------------------------------- 1 | import jsonpath, { PathComponent } from 'jsonpath' 2 | import isEmpty from 'lodash/isEmpty' 3 | 4 | import JsonEvaluator from './evaluators/json-evaluator' 5 | import ManualEvaluator from './evaluators/manual-evaluator' 6 | import JsEvaluator from './evaluators/js-evaluator' 7 | import { RuleEvaluator } from './evaluators/rule-evaluator' 8 | import { Engine, ResourceData, Rule, RuleFinding } from './types' 9 | import DataProcessor from './data-processors/data-processor' 10 | import { Entity } from '../types' 11 | 12 | export default class RulesProvider implements Engine { 13 | evaluators: RuleEvaluator[] = [] 14 | 15 | private readonly dataProcessor: DataProcessor 16 | 17 | constructor(dataProcessor: DataProcessor) { 18 | this.dataProcessor = dataProcessor 19 | this.evaluators = [ 20 | new JsonEvaluator(), 21 | new JsEvaluator(), 22 | new ManualEvaluator(), 23 | ] 24 | } 25 | 26 | /** 27 | * Evaluates which Evaluator can be used for each rule 28 | * @param rule rule to evaluate 29 | * @returns A RuleEvaluator 30 | */ 31 | private getRuleEvaluator = (rule: Rule): RuleEvaluator => { 32 | for (const evaluator of this.evaluators) { 33 | if (evaluator.canEvaluate(rule)) { 34 | return evaluator 35 | } 36 | } 37 | return undefined 38 | } 39 | 40 | /** 41 | * Process a Rule with an RuleEvaluator 42 | * @param rule Rule to Process 43 | * @param evaluator RuleEvaluator 44 | * @param data Rule Data 45 | * @returns RuleFinding result 46 | */ 47 | private processSingleResourceRule = async ( 48 | rule: Rule, 49 | evaluator: RuleEvaluator, 50 | data: ResourceData 51 | ): Promise => { 52 | const finding = await evaluator.evaluateSingleResource(rule, data) 53 | 54 | if (!finding) return 55 | 56 | // Inject extra fields 57 | for (const field of this.dataProcessor.extraFields) { 58 | finding[field] = data.resource[field] 59 | } 60 | 61 | const connField = 62 | data.resource.__typename && // eslint-disable-line no-underscore-dangle 63 | this.dataProcessor.typenameToFieldMap[data.resource.__typename] // eslint-disable-line no-underscore-dangle 64 | 65 | if (connField) { 66 | finding[connField] = [{ id: data.resource.id }] 67 | } 68 | return finding 69 | } 70 | 71 | /** 72 | * traverse the path, and 'highlight' the path towards the resource 73 | * a[0].b.c[3].id === a['@'].b.c['@'].id // so rules can use this notation to know their parents 74 | */ 75 | private highlightPath(data: any, path: PathComponent[]): any { 76 | let curr = data // we can write the data, as next time we'll set the same fields 77 | for (let j = 1; j < path.length; j++) { 78 | const segment = path[j] 79 | if (Array.isArray(curr)) { 80 | // this is an array, we store in []._ the alias of this resource position in the array 81 | ;(curr as any)['@'] = curr[segment as number] // eslint-disable-line 82 | } 83 | curr = curr[segment] 84 | } 85 | return data 86 | } 87 | 88 | getSchema = (): string[] => this.dataProcessor.getSchema() 89 | 90 | processRule = async (rule: Rule, data: unknown): Promise => { 91 | const res: any[] = [] 92 | const dedupeIds = {} 93 | const resourcePaths = data ? jsonpath.nodes(data, rule.resource) : [] 94 | const evaluator = this.getRuleEvaluator(rule) 95 | 96 | if (!evaluator) { 97 | return [] 98 | } 99 | 100 | if (isEmpty(data) && evaluator instanceof ManualEvaluator) { 101 | // Returned Manual Rule Response 102 | res.push(await evaluator.evaluateSingleResource(rule)) 103 | return res 104 | } 105 | 106 | // @NOTE: here we can evaluate things such as Data being empty (may imply rule to pass) 107 | // or if we have no resources, or none pass, we can decide on that rule (+ some rule field) 108 | for (let i = 0; i < resourcePaths.length; i++) { 109 | const { path, value: resource } = resourcePaths[i] 110 | if (!resource.id) { 111 | // @NOTE: we'll support more complex rules in the future where you dont need a resource upfront 112 | continue // eslint-disable-line no-continue 113 | } 114 | if (dedupeIds[resource.id]) { 115 | // console.warn('Resource is duplicated, skipping', resource.id) 116 | continue // eslint-disable-line no-continue 117 | } 118 | dedupeIds[resource.id] = 1 119 | 120 | if (path[0] !== '$') { 121 | // console.log( 122 | // 'Is this case possible? how do we process it?', 123 | // resourcePaths[i] 124 | // ) 125 | continue // eslint-disable-line no-continue 126 | } 127 | const processedData = this.highlightPath(data, path) 128 | const ruleResult = await this.processSingleResourceRule(rule, evaluator, { 129 | data: processedData, 130 | resource, 131 | resourcePath: jsonpath.stringify(path), 132 | }) 133 | if (ruleResult) { 134 | res.push({ ...ruleResult }) 135 | } 136 | } 137 | return res 138 | } 139 | 140 | prepareMutations = (findings: RuleFinding[], rules: Rule[]): Entity[] => [ 141 | ...this.dataProcessor.prepareRulesMetadataMutations(rules), 142 | ...this.dataProcessor.prepareFindingsMutations(findings), 143 | ] 144 | } 145 | -------------------------------------------------------------------------------- /src/rules-engine/evaluators/json-evaluator.ts: -------------------------------------------------------------------------------- 1 | import lodash from 'lodash' 2 | 3 | import { 4 | Condition, 5 | JsonRule, 6 | Operator, 7 | ResourceData, 8 | Result, 9 | RuleFinding, 10 | RuleResult, 11 | _ResourceData, 12 | } from '../types' 13 | import AdditionalOperators from '../operators' 14 | import ComparisonOperators from '../operators/comparison' 15 | import { RuleEvaluator } from './rule-evaluator' 16 | 17 | export default class JsonEvaluator implements RuleEvaluator { 18 | canEvaluate(rule: JsonRule): boolean { 19 | return 'conditions' in rule 20 | } 21 | 22 | async evaluateSingleResource( 23 | { id, conditions, severity, exclude }: JsonRule, 24 | data: ResourceData 25 | ): Promise { 26 | if (exclude && (await this.evaluateCondition(exclude, data))) { 27 | return 28 | } 29 | const result = (await this.evaluateCondition(conditions, data)) 30 | ? RuleResult.MATCHES 31 | : RuleResult.DOESNT_MATCH 32 | 33 | const finding = { 34 | id: `${id}/${data.resource?.id}`, 35 | resourceId: data.resource?.id, 36 | result: result !== RuleResult.MATCHES ? Result.FAIL : Result.PASS, 37 | typename: data.resource?.__typename, // eslint-disable-line no-underscore-dangle 38 | rule: { 39 | id, 40 | severity, 41 | }, 42 | } as RuleFinding 43 | return finding 44 | } 45 | 46 | calculatePath = (data: _ResourceData, _path: string) => { 47 | let path = _path 48 | if (path.indexOf('@') === 0) { 49 | // @ means the curr resource, we replace by base path 50 | path = path.replace('@', data.resourcePath).substr(2) // remove `$.` 51 | } 52 | if (path.indexOf('[*]') === 0 && data.elementPath) { 53 | // @ means the curr resource, we replace by base path 54 | path = path.replace('[*]', data.elementPath) 55 | } 56 | return path 57 | } 58 | 59 | resolvePath = (data: _ResourceData, path: string): any => { 60 | return lodash.get(data.data, path) 61 | } 62 | 63 | operators: { [key: string]: Operator } = { 64 | not: async (_, conditions: Condition, data) => { 65 | let notConditions = conditions 66 | if (data.elementPath) { 67 | notConditions = { ...conditions, path: data.elementPath } 68 | } 69 | 70 | return !(await this.evaluateCondition(notConditions, data)) 71 | }, 72 | or: async (_, conditions: Condition[], data) => { 73 | for (let i = 0; i < conditions.length; i++) { 74 | // if 1 is true, it's true 75 | if (await this.evaluateCondition(conditions[i], data)) return true 76 | } 77 | return false 78 | }, 79 | and: async (_, conditions: Condition[], data) => { 80 | for (let i = 0; i < conditions.length; i++) { 81 | // if 1 is false, it's false 82 | if (!(await this.evaluateCondition(conditions[i], data))) return false 83 | } 84 | return true 85 | }, 86 | array_all: async (array = [], conditions: Condition, data) => { 87 | // an AND, but with every resource item 88 | const baseElementPath = data.elementPath 89 | for (let i = 0; i < array?.length; i++) { 90 | if ( 91 | !(await this.evaluateCondition(conditions, { 92 | ...data, 93 | elementPath: `${baseElementPath}[${i}]`, 94 | })) 95 | ) 96 | return false 97 | } 98 | return true 99 | }, 100 | array_any: async (array = [], conditions, data) => { 101 | // an OR, but with every resource item 102 | const baseElementPath = data.elementPath 103 | for (let i = 0; i < array?.length; i++) { 104 | if ( 105 | await this.evaluateCondition(conditions as Condition, { 106 | ...data, 107 | elementPath: `${baseElementPath}[${i}]`, 108 | }) 109 | ) 110 | return true 111 | } 112 | return false 113 | }, 114 | ...AdditionalOperators, 115 | } 116 | 117 | isCondition = (a: unknown): boolean => 118 | !!a && (a as any).constructor === Object 119 | 120 | async evaluateCondition( 121 | _condition: Condition, 122 | _data: _ResourceData 123 | ): Promise { 124 | const condition = { ..._condition } 125 | const { path, value } = condition 126 | delete condition.path 127 | delete condition.value 128 | // remaining field should be the op name 129 | const op = Object.keys(condition)[0] // 130 | const operator = this.operators[op] 131 | let otherArgs = condition[op] // {[and]: xxx } 132 | if (!op || !operator) { 133 | throw new Error(`unrecognized operation${JSON.stringify(condition)}`) 134 | } 135 | 136 | const data = { ..._data } 137 | let firstArg 138 | 139 | if ( 140 | Object.keys(ComparisonOperators).includes(operator.name) && 141 | this.isCondition(otherArgs) && 142 | otherArgs.path 143 | ) { 144 | const resourceData = { ..._data } 145 | const elementPath = this.calculatePath(resourceData, otherArgs.path) 146 | resourceData.elementPath = elementPath 147 | otherArgs = this.resolvePath(resourceData, elementPath) 148 | } 149 | 150 | if (path) { 151 | const elementPath = this.calculatePath(data, path) 152 | data.elementPath = elementPath 153 | firstArg = this.resolvePath(data, elementPath) 154 | } else if (this.isCondition(value)) { 155 | firstArg = await this.evaluateCondition(value as any, data) 156 | } else { 157 | firstArg = value 158 | } 159 | 160 | return operator(firstArg, otherArgs, data) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | "lib": ["es2015"], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./dist/index.js", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | // "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 44 | 45 | /* Module Resolution Options */ 46 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 47 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 48 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | // "typeRoots": [], /* List of folders to include type definitions from. */ 51 | // "types": [], /* Type declaration files to be included in compilation. */ 52 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 56 | 57 | /* Source Map Options */ 58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 62 | 63 | /* Experimental Options */ 64 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 66 | 67 | /* Advanced Options */ 68 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 69 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/operators/comparison.test.ts: -------------------------------------------------------------------------------- 1 | import ComparisonOperators from '../../src/rules-engine/operators/comparison' 2 | 3 | describe('Comparison Operators', () => { 4 | describe('Equal Operator', () => { 5 | test('Should pass given two equal strings', () => { 6 | const result = ComparisonOperators.equal('Switch', 'Switch') 7 | expect(result).toBeTruthy() 8 | }) 9 | 10 | test('Should fail given two different strings', () => { 11 | const result = ComparisonOperators.equal('PS5', 'Xbox Series X') 12 | expect(result).toBeFalsy() 13 | }) 14 | test('Should pass given two different numbers', () => { 15 | const result = ComparisonOperators.equal(360, 360) 16 | expect(result).toBeTruthy() 17 | }) 18 | test('Should fail given two different numbers', () => { 19 | const result = ComparisonOperators.equal(360, 90) 20 | expect(result).toBeFalsy() 21 | }) 22 | test('Should pass given two equal values with different types', () => { 23 | const result = ComparisonOperators.equal(180, '180') 24 | expect(result).toBeTruthy() 25 | }) 26 | }) 27 | 28 | describe('NotEqual Operator', () => { 29 | test('Should fail given two equal strings', () => { 30 | const result = ComparisonOperators.notEqual('Switch', 'Switch') 31 | expect(result).toBeFalsy() 32 | }) 33 | 34 | test('Should pass given two different strings', () => { 35 | const result = ComparisonOperators.notEqual('PS5', 'Xbox Series X') 36 | expect(result).toBeTruthy() 37 | }) 38 | test('Should fail given two different numbers', () => { 39 | const result = ComparisonOperators.notEqual(360, 360) 40 | expect(result).toBeFalsy() 41 | }) 42 | test('Should pass given two different numbers', () => { 43 | const result = ComparisonOperators.notEqual(360, 90) 44 | expect(result).toBeTruthy() 45 | }) 46 | test('Should pass given two equal values with different types', () => { 47 | const result = ComparisonOperators.notEqual(180, '180') 48 | expect(result).toBeTruthy() 49 | }) 50 | }) 51 | 52 | describe('LessThan and LessThanInclusive Operator', () => { 53 | test('Should fail if the first string is less than the second one', () => { 54 | const result = ComparisonOperators.lessThan('X', 'A') 55 | expect(result).toBeFalsy() 56 | }) 57 | 58 | test('Should fail comparing the same string', () => { 59 | const result = ComparisonOperators.lessThan('A', 'A') 60 | expect(result).toBeFalsy() 61 | }) 62 | 63 | test('Should pass if the first string is greater than the second one', () => { 64 | const result = ComparisonOperators.lessThan('A', 'X') 65 | expect(result).toBeTruthy() 66 | }) 67 | 68 | test('Should pass if the first number is less than the second one', () => { 69 | const result = ComparisonOperators.lessThan(10, 100) 70 | expect(result).toBeTruthy() 71 | }) 72 | 73 | test('Should fail comparing the same number', () => { 74 | const result = ComparisonOperators.lessThan(10, 10) 75 | expect(result).toBeFalsy() 76 | }) 77 | 78 | test('Should fail if the first number is greater than the second one', () => { 79 | const result = ComparisonOperators.lessThan(100, 10) 80 | expect(result).toBeFalsy() 81 | }) 82 | 83 | test('Should pass if the first number is less or equal than the second one', () => { 84 | expect(ComparisonOperators.lessThanInclusive(1, 10)).toBeTruthy() 85 | expect(ComparisonOperators.lessThanInclusive(10, 10)).toBeTruthy() 86 | }) 87 | 88 | test('Should pass if the first string is less or equal than the second one', () => { 89 | expect(ComparisonOperators.lessThanInclusive('a', 'x')).toBeTruthy() 90 | expect(ComparisonOperators.lessThanInclusive('a', 'a')).toBeTruthy() 91 | }) 92 | 93 | test('Should pass if the first date is before than the second one', () => { 94 | const result = ComparisonOperators.lessThan( 95 | '2022-03-15T20:42:58.510Z', 96 | new Date().toISOString() 97 | ) 98 | expect(result).toBeTruthy() 99 | }) 100 | test('Should fail if the first date is after than the second one', () => { 101 | const result = ComparisonOperators.lessThan( 102 | new Date().toISOString(), 103 | '2022-03-15T20:42:58.510Z' 104 | ) 105 | expect(result).toBeFalsy() 106 | }) 107 | }) 108 | 109 | describe('GreaterThan and GreaterThanInclusive Operator', () => { 110 | test('Should fail if the first string is greater than the second one', () => { 111 | const result = ComparisonOperators.greaterThan('A', 'X') 112 | expect(result).toBeFalsy() 113 | }) 114 | 115 | test('Should fail comparing the same string', () => { 116 | const result = ComparisonOperators.greaterThan('A', 'A') 117 | expect(result).toBeFalsy() 118 | }) 119 | 120 | test('Should pass if the first string is less than the second one', () => { 121 | const result = ComparisonOperators.greaterThan('X', 'A') 122 | expect(result).toBeTruthy() 123 | }) 124 | 125 | test('Should pass if the first number is greater than the second one', () => { 126 | const result = ComparisonOperators.greaterThan(100, 10) 127 | expect(result).toBeTruthy() 128 | }) 129 | 130 | test('Should fail comparing the same number', () => { 131 | const result = ComparisonOperators.greaterThan(10, 10) 132 | expect(result).toBeFalsy() 133 | }) 134 | 135 | test('Should fail if the first number is less than the second one', () => { 136 | const result = ComparisonOperators.greaterThan(10, 100) 137 | expect(result).toBeFalsy() 138 | }) 139 | 140 | test('Should pass if the first number is greater or equal than the second one', () => { 141 | expect(ComparisonOperators.greaterThanInclusive(10, 1)).toBeTruthy() 142 | expect(ComparisonOperators.greaterThanInclusive(10, 10)).toBeTruthy() 143 | }) 144 | 145 | test('Should pass if the first string is greater or equal than the second one', () => { 146 | expect(ComparisonOperators.greaterThanInclusive('x', 'a')).toBeTruthy() 147 | expect(ComparisonOperators.greaterThanInclusive('a', 'a')).toBeTruthy() 148 | }) 149 | 150 | test('Should pass if the first date is after than the second one', () => { 151 | const result = ComparisonOperators.greaterThan( 152 | new Date().toISOString(), 153 | '2022-03-15T20:42:58.510Z' 154 | ) 155 | expect(result).toBeTruthy() 156 | }) 157 | test('Should fail if the first date is before than the second one', () => { 158 | const result = ComparisonOperators.greaterThan( 159 | '2022-03-15T20:42:58.510Z', 160 | new Date().toISOString() 161 | ) 162 | expect(result).toBeFalsy() 163 | }) 164 | }) 165 | }) 166 | -------------------------------------------------------------------------------- /src/rules-engine/data-processors/dgraph-data-processor.ts: -------------------------------------------------------------------------------- 1 | import groupBy from 'lodash/groupBy' 2 | import isEmpty from 'lodash/isEmpty' 3 | import { Entity } from '../../types' 4 | import { Rule, 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 | readonly typenameToFieldMap: { [typeName: string]: string } 13 | 14 | readonly extraFields: string[] 15 | 16 | constructor({ 17 | providerName, 18 | entityName, 19 | typenameToFieldMap, 20 | extraFields, 21 | }: { 22 | providerName: string 23 | entityName: string 24 | typenameToFieldMap?: { [tn: string]: string } 25 | extraFields?: string[] 26 | }) { 27 | this.providerName = providerName 28 | this.entityName = entityName 29 | this.extraFields = extraFields ?? [] 30 | this.typenameToFieldMap = typenameToFieldMap ?? {} 31 | } 32 | 33 | getSchema = (): string[] => { 34 | const mainType = ` 35 | enum FindingsResult { 36 | PASS 37 | FAIL 38 | MISSING 39 | SKIPPED 40 | } 41 | 42 | type ${this.providerName}Findings @key(fields: "id") { 43 | id: String! @id 44 | ${this.entityName}Findings: [${this.providerName}${ 45 | this.entityName 46 | }Findings] 47 | } 48 | 49 | type ruleMetadata @key(fields: "id") { 50 | id: String! @id @search(by: [hash, regexp]) 51 | severity: String @search(by: [hash, regexp]) 52 | description: String! @search(by: [hash, regexp]) 53 | title: String @search(by: [hash, regexp]) 54 | audit: String @search(by: [hash, regexp]) 55 | rationale: String @search(by: [hash, regexp]) 56 | remediation: String @search(by: [hash, regexp]) 57 | references: [String] @search(by: [hash, regexp]) 58 | findings: [baseFinding] @hasInverse(field: rule) 59 | } 60 | 61 | interface baseFinding { 62 | id: String! @id 63 | resourceId: String @search(by: [hash, regexp]) 64 | rule: [ruleMetadata] @hasInverse(field: findings) 65 | result: FindingsResult @search 66 | } 67 | 68 | type ${this.providerName}${ 69 | this.entityName 70 | }Findings implements baseFinding @key(fields: "id") { 71 | findings: ${this.providerName}Findings @hasInverse(field: ${ 72 | this.entityName 73 | }Findings) 74 | # extra fields 75 | ${this.extraFields.map( 76 | field => `${field}: String @search(by: [hash, regexp])` 77 | )} 78 | # connections 79 | ${Object.keys(this.typenameToFieldMap) 80 | .map( 81 | (tn: string) => 82 | `${tn}: [${this.typenameToFieldMap[tn]}] @hasInverse(field: ${this.entityName}Findings)` 83 | ) 84 | .join(' ')} 85 | } 86 | ` 87 | const extensions = Object.keys(this.typenameToFieldMap) 88 | .map( 89 | (tn: string) => 90 | `extend type ${this.typenameToFieldMap[tn]} { 91 | ${this.entityName}Findings: [${this.providerName}${this.entityName}Findings] @hasInverse(field: ${tn}) 92 | }` 93 | ) 94 | .join('\n') 95 | 96 | return [mainType, extensions] 97 | } 98 | 99 | /** 100 | * Prepare the mutations for overall provider findings 101 | * @param findings RuleFinding array 102 | * @returns A formatted Entity array 103 | */ 104 | private prepareProviderMutations = ( 105 | findings: RuleFinding[] = [] 106 | ): Entity[] => { 107 | // Prepare provider schema connections 108 | return findings?.length > 0 109 | ? [ 110 | { 111 | name: `${this.providerName}Findings`, 112 | mutation: ` 113 | mutation($input: [Add${this.providerName}FindingsInput!]!) { 114 | add${this.providerName}Findings(input: $input, upsert: true) { 115 | numUids 116 | } 117 | } 118 | `, 119 | data: { 120 | id: `${this.providerName}-provider`, 121 | }, 122 | }, 123 | { 124 | name: `${this.providerName}Findings`, 125 | mutation: `mutation update${this.providerName}Findings($input: Update${this.providerName}FindingsInput!) { 126 | update${this.providerName}Findings(input: $input) { 127 | numUids 128 | } 129 | } 130 | `, 131 | data: { 132 | filter: { 133 | id: { eq: `${this.providerName}-provider` }, 134 | }, 135 | set: { 136 | [`${this.entityName}Findings`]: findings.map( 137 | ({ typename, ...rest }) => ({ ...rest }) 138 | ), 139 | }, 140 | }, 141 | }, 142 | ] 143 | : [] 144 | } 145 | 146 | private prepareEntitiesMutations(findings: RuleFinding[]): RuleFinding[] { 147 | // Group Findings by schema type 148 | const { manual: manualData = [], ...processedRules } = groupBy( 149 | findings, 150 | 'typename' 151 | ) 152 | 153 | const mutations = [...manualData] 154 | for (const findingType in processedRules) { 155 | if (!isEmpty(findingType)) { 156 | // Group Findings by resource 157 | const findingsByResource = groupBy( 158 | processedRules[findingType], 159 | 'resourceId' 160 | ) 161 | 162 | for (const resource in findingsByResource) { 163 | if (resource) { 164 | const data = ( 165 | (findingsByResource[resource] as RuleFinding[]) || [] 166 | ).map(finding => { 167 | const resourceType = Object.keys(this.typenameToFieldMap).find( 168 | key => this.typenameToFieldMap[key] === finding.typename 169 | ) 170 | 171 | // Set required fields to allow resource insertion 172 | const requiredFields = !isEmpty(this.extraFields) 173 | ? this.extraFields.reduce( 174 | (acc, current) => ({ 175 | ...acc, 176 | [current]: finding[current], 177 | }), 178 | {} 179 | ) 180 | : {} 181 | 182 | return { 183 | ...finding, 184 | [resourceType]: { 185 | id: resource, 186 | ...requiredFields, 187 | }, 188 | } 189 | }) 190 | 191 | mutations.push(...data) 192 | } 193 | } 194 | } 195 | } 196 | 197 | return mutations 198 | } 199 | 200 | prepareRulesMetadataMutations = (rules: Rule[] = []): Entity[] => { 201 | // Return an empty array if there are no rules 202 | if (isEmpty(rules)) { 203 | return [] 204 | } 205 | 206 | return [ 207 | { 208 | name: 'ruleMetadata', 209 | mutation: ` 210 | mutation($input: [AddruleMetadataInput!]!) { 211 | addruleMetadata(input: $input, upsert: true) { 212 | numUids 213 | } 214 | } 215 | 216 | `, 217 | data: rules.map( 218 | ({ 219 | id, 220 | title, 221 | description, 222 | references, 223 | rationale, 224 | audit, 225 | remediation, 226 | severity, 227 | }) => ({ 228 | id, 229 | title, 230 | description, 231 | references, 232 | rationale, 233 | audit, 234 | remediation, 235 | severity, 236 | }) 237 | ), 238 | }, 239 | ] 240 | } 241 | 242 | prepareFindingsMutations = (findings: RuleFinding[] = []): Entity[] => { 243 | // Return an empty array if there are no findings 244 | if (isEmpty(findings)) { 245 | return [] 246 | } 247 | 248 | // Prepare entities mutations 249 | const entitiesData = this.prepareEntitiesMutations(findings).map( 250 | ({ typename, ...finding }) => ({ 251 | ...finding, 252 | }) 253 | ) 254 | 255 | // Prepare provider mutations 256 | const providerData = this.prepareProviderMutations(findings) 257 | 258 | return [ 259 | { 260 | name: `${this.providerName}${this.entityName}Findings`, 261 | mutation: ` 262 | mutation($input: [Add${this.providerName}${this.entityName}FindingsInput!]!) { 263 | add${this.providerName}${this.entityName}Findings(input: $input, upsert: true) { 264 | numUids 265 | } 266 | } 267 | `, 268 | data: entitiesData, 269 | }, 270 | ...providerData, 271 | ] 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/plugins/policyPack/index.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { groupBy, isEmpty } from 'lodash' 3 | 4 | import { generateSchemaMapDynamically, mergeSchemas } from '../../utils/schema' 5 | import { 6 | Engine, 7 | Logger, 8 | ProviderData, 9 | RuleFinding, 10 | SchemaMap, 11 | StorageEngine, 12 | } from '../..' 13 | import RulesEngine from '../../rules-engine' 14 | import { Result, Rule, Severity } from '../../rules-engine/types' 15 | 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' 18 | import getLinkedData from '../../utils/data' 19 | 20 | export default class PolicyPackPlugin extends Plugin { 21 | constructor({ 22 | config, 23 | logger, 24 | provider, 25 | flags, 26 | }: { 27 | config: { [key: string]: string } 28 | logger: Logger 29 | provider: { name: string; schemasMap?: SchemaMap; serviceKey?: string } 30 | flags: { 31 | [flag: string]: any 32 | } 33 | }) { 34 | super() 35 | this.logger = logger 36 | this.config = config 37 | this.provider = provider 38 | this.flags = flags 39 | } 40 | 41 | private config: { [key: string]: any } = {} 42 | 43 | private flags: { 44 | [flag: string]: any 45 | } 46 | 47 | private provider: { 48 | name: string 49 | schemasMap?: SchemaMap 50 | serviceKey?: string 51 | } = { 52 | name: '', 53 | schemasMap: undefined, 54 | serviceKey: undefined, 55 | } 56 | 57 | logger: Logger 58 | 59 | private policyPacksPlugins: { 60 | [policyPackName: string]: { 61 | engine: Engine 62 | entity: string 63 | rules: Rule[] 64 | } 65 | } = {} 66 | 67 | private dataProcessors: { 68 | [name: string]: DataProcessor 69 | } = {} 70 | 71 | private async getPolicyPackPackage({ 72 | policyPack, 73 | pluginManager, 74 | }: { 75 | policyPack: string 76 | pluginManager: PluginManager 77 | }): Promise { 78 | try { 79 | if (this.policyPacksPlugins[policyPack]) { 80 | return this.policyPacksPlugins[policyPack] 81 | } 82 | 83 | const { 84 | default: { rules = [], entity = 'Custom', extraFields = [] }, 85 | } = (await pluginManager.getPlugin(policyPack)) ?? {} 86 | 87 | if (!rules) { 88 | throw new Error( 89 | `The policy pack ${policyPack} did not return a valid set of rules` 90 | ) 91 | } 92 | 93 | return { rules, entity, extraFields } 94 | } catch (error: any) { 95 | this.logger.error(error) 96 | this.logger.warn( 97 | `There was an error installing or requiring a plugin for ${policyPack}, does one exist?` 98 | ) 99 | this.logger.info( 100 | 'For more information on this error, please see https://github.com/cloudgraphdev/cli#common-errors' 101 | ) 102 | return null 103 | } 104 | } 105 | 106 | private displayResults(findingsResults: { 107 | [severity: string]: RuleFinding[] 108 | }): void { 109 | // TODO: Display Results will be improved on CG-549 110 | for (const severityLevel in findingsResults) { 111 | if (severityLevel) { 112 | const count = findingsResults[severityLevel].length 113 | let message = '' 114 | if (severityLevel === Severity.HIGH) { 115 | message = `${chalk.italic.redBright.red( 116 | `[ ${count || 0} ${ 117 | count === 1 ? 'issue was' : 'issues were' 118 | } found during rules execution marked as high severity ]` 119 | )}` 120 | } else if (severityLevel === Severity.MEDIUM) { 121 | message = `${chalk.italic.yellow( 122 | `[ ${count || 0} ${ 123 | count === 1 ? 'issue was' : 'issues were' 124 | } found during rules execution marked as medium severity ]` 125 | )}` 126 | } 127 | 128 | this.logger.info( 129 | message || 130 | `${chalk.italic( 131 | `[ ${count || 0} ${ 132 | count === 1 ? 'issue was' : 'issues were' 133 | } found during rules execution marked as ${severityLevel} severity ]` 134 | )}` 135 | ) 136 | } 137 | } 138 | } 139 | 140 | private async executeRule({ 141 | data, 142 | rules, 143 | policyPack, 144 | }: { 145 | data: any, 146 | rules: Rule[] 147 | policyPack: string 148 | }): Promise { 149 | const findings: RuleFinding[] = [] 150 | 151 | // Run rules: 152 | for (const rule of rules) { 153 | try { 154 | if (rule.queries?.length > 0) { 155 | const { queries, ...ruleMetadata } = rule 156 | const subRules = queries.map(q => ({ 157 | ...q, 158 | ...ruleMetadata, 159 | })) 160 | 161 | findings.push( 162 | ...(await this.executeRule({ 163 | data, 164 | rules: subRules, 165 | policyPack, 166 | })) 167 | ) 168 | } else { 169 | const ruleData = rule.gql ? data : undefined 170 | const results = (await this.policyPacksPlugins[ 171 | policyPack 172 | ]?.engine?.processRule(rule, ruleData)) as RuleFinding[] 173 | 174 | findings.push(...results) 175 | } 176 | } catch (error) { 177 | this.logger.error( 178 | `Error processing rule ${rule.id} for ${policyPack} policy pack` 179 | ) 180 | this.logger.debug(error) 181 | } 182 | } 183 | 184 | return findings 185 | } 186 | 187 | // TODO: Generalize data processor moving storage module to SDK with its interfaces 188 | private getDataProcessor(config: { 189 | providerName: string 190 | entityName: string 191 | typenameToFieldMap?: { [tn: string]: string } 192 | extraFields?: string[] 193 | }): DataProcessor { 194 | const dataProcessorKey = `${config.providerName}${config.entityName}` 195 | if (this.dataProcessors[dataProcessorKey]) { 196 | return this.dataProcessors[dataProcessorKey] 197 | } 198 | 199 | const dataProcessor = new DgraphDataProcessor(config) 200 | this.dataProcessors[dataProcessorKey] = dataProcessor 201 | return dataProcessor 202 | } 203 | 204 | async configure( 205 | pluginManager: PluginManager, 206 | plugins: ConfiguredPlugin[] 207 | ): Promise { 208 | const { policyPacks = '' } = this.flags 209 | let allPolicyPacks = isEmpty(policyPacks) ? [] : policyPacks.split(',') 210 | if (allPolicyPacks.length >= 1) { 211 | this.logger.debug(`Executing rules for policy packs: ${allPolicyPacks}`) 212 | } else { 213 | const policies = plugins || [] 214 | allPolicyPacks = policies 215 | .filter(policy => policy.providers.includes(this.provider.name)) 216 | .map(policyFilteredByProvider => policyFilteredByProvider.name) 217 | 218 | this.logger.debug( 219 | `Executing rules for policy packs found in config: ${allPolicyPacks}` 220 | ) 221 | } 222 | if (allPolicyPacks.length === 0) { 223 | this.logger.warn( 224 | 'There are no policy packs configured and none were passed to execute' 225 | ) 226 | } 227 | const failedPolicyPackList: string[] = [] 228 | 229 | const resources = 230 | this.config[this.provider?.serviceKey ?? 'resources']?.split(',') || [] 231 | 232 | // // Generate schema mapping 233 | const resourceTypeNamesToFieldsMap = 234 | this.provider.schemasMap || 235 | generateSchemaMapDynamically(this.provider.name, resources) 236 | 237 | for (const policyPack of allPolicyPacks) { 238 | const policyPackPlugin = await this.getPolicyPackPackage({ 239 | policyPack, 240 | pluginManager, 241 | }) 242 | 243 | if (!policyPackPlugin?.rules) { 244 | failedPolicyPackList.push(policyPack) 245 | this.logger.warn(`No valid rules found for ${policyPack}, skipping...`) 246 | continue // eslint-disable-line no-continue 247 | } 248 | 249 | if (!policyPackPlugin?.entity) { 250 | failedPolicyPackList.push(policyPack) 251 | this.logger.warn( 252 | `No entity was specified for ${policyPack}, skipping...` 253 | ) 254 | continue // eslint-disable-line no-continue 255 | } 256 | 257 | // Initialize Data Processor 258 | const dataProcessor = this.getDataProcessor({ 259 | providerName: this.provider.name, 260 | entityName: policyPackPlugin.entity, 261 | typenameToFieldMap: resourceTypeNamesToFieldsMap, 262 | extraFields: policyPackPlugin.extraFields, 263 | }) 264 | 265 | // Initialize RulesEngine 266 | const rulesEngine = new RulesEngine(dataProcessor) 267 | 268 | this.policyPacksPlugins[policyPack] = { 269 | engine: rulesEngine, 270 | entity: policyPackPlugin.entity, 271 | rules: policyPackPlugin.rules, 272 | } 273 | } 274 | } 275 | 276 | async execute({ 277 | storageRunning, 278 | storageEngine, 279 | providerData, 280 | processConnectionsBetweenEntities, 281 | }: { 282 | storageRunning: boolean 283 | storageEngine: StorageEngine 284 | providerData: ProviderData 285 | processConnectionsBetweenEntities: (props: { 286 | provider?: string 287 | providerData: ProviderData 288 | storageEngine: StorageEngine 289 | storageRunning: boolean 290 | schemaMap?: SchemaMap 291 | }) => void 292 | }): Promise { 293 | !isEmpty(this.policyPacksPlugins) && 294 | this.logger.info( 295 | `Beginning ${chalk.italic.green('RULES')} for ${this.provider.name}` 296 | ) 297 | for (const policyPack in this.policyPacksPlugins) { 298 | if (policyPack && this.policyPacksPlugins[policyPack]) { 299 | const { 300 | engine, 301 | entity, 302 | rules = [], 303 | } = this.policyPacksPlugins[policyPack] 304 | 305 | this.logger.startSpinner( 306 | `${chalk.italic.green('EXECUTING')} rules for ${chalk.italic.green( 307 | policyPack 308 | )}` 309 | ) 310 | // Update Schema: 311 | const currentSchema: string = await storageEngine.getSchema() 312 | const findingsSchema: string[] = engine?.getSchema() || [] 313 | 314 | await storageEngine.setSchema([ 315 | mergeSchemas(currentSchema, findingsSchema), 316 | ]) 317 | 318 | // Format metadata and link connections 319 | const linkedData = getLinkedData(providerData, this.provider.schemasMap) 320 | 321 | const findings = await this.executeRule({ 322 | data: linkedData, 323 | rules, 324 | policyPack, 325 | }) 326 | 327 | // Prepare mutations 328 | const mutations = engine.prepareMutations(findings, rules) 329 | 330 | // Save connections 331 | processConnectionsBetweenEntities({ 332 | providerData: { 333 | entities: mutations, 334 | connections: [] as any, 335 | }, 336 | storageEngine, 337 | storageRunning, 338 | }) 339 | await storageEngine.run(false) 340 | 341 | this.logger.successSpinner( 342 | `${chalk.italic.green(policyPack)} rules excuted successfully` 343 | ) 344 | this.logger.successSpinner('success') 345 | 346 | const results = findings.filter( 347 | finding => finding.result === Result.FAIL 348 | ) 349 | 350 | if (!isEmpty(results)) { 351 | this.displayResults(groupBy(results, 'rule.severity')) 352 | 353 | this.logger.info( 354 | `For more information, you can query ${chalk.italic.green( 355 | `query${this.provider.name}${entity}Findings` 356 | )} in the GraphQL query tool` 357 | ) 358 | } 359 | } 360 | } 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /tests/evaluators/json-evaluator.test.ts: -------------------------------------------------------------------------------- 1 | import cuid from 'cuid' 2 | import { Result } from '../../src' 3 | import JsonEvaluator from '../../src/rules-engine/evaluators/json-evaluator' 4 | import { Severity } from '../../src/rules-engine/types' 5 | 6 | const ruleMetadata = { 7 | id: cuid(), 8 | description: 'none', 9 | title: 'Mocked Automated Rule', 10 | rationale: "raison d'être", 11 | audit: 'evaluate schemaA', 12 | remediation: 'fix the schemaA', 13 | references: [], 14 | severity: Severity.HIGH, 15 | } 16 | const jsonRule = { 17 | ...ruleMetadata, 18 | gql: `{ 19 | querySchemaA { 20 | id 21 | __typename 22 | value 23 | } 24 | }`, 25 | resource: 'querySchemaA[*]', 26 | conditions: { 27 | path: '@.value', 28 | equal: false, 29 | }, 30 | } 31 | 32 | const compositeRule = { 33 | ...ruleMetadata, 34 | queries: [ 35 | { 36 | gql: `{ 37 | querySchemaA { 38 | id 39 | __typename 40 | value 41 | } 42 | }`, 43 | resource: 'querySchemaA[*]', 44 | conditions: { 45 | path: '@.value', 46 | equal: false, 47 | }, 48 | }, 49 | { 50 | gql: `{ 51 | querySchemaB { 52 | id 53 | __typename 54 | value 55 | } 56 | }`, 57 | resource: 'querySchemaB[*]', 58 | conditions: { 59 | path: '@.value', 60 | equal: true, 61 | }, 62 | }, 63 | ], 64 | } 65 | 66 | export default { 67 | jsonRule, 68 | compositeRule, 69 | } 70 | 71 | describe('JsonEvaluator', () => { 72 | let evaluator 73 | beforeEach(() => { 74 | evaluator = new JsonEvaluator() 75 | }) 76 | 77 | test('should accept all rules that have a conditions field', () => { 78 | expect(evaluator.canEvaluate({} as any)).toBe(false) 79 | expect(evaluator.canEvaluate({ conditionss: 1 } as any)).toBe(false) 80 | expect(evaluator.canEvaluate({ conditions: 1 } as any)).toBe(true) 81 | }) 82 | 83 | test('should execute simple rules', async () => { 84 | const data = { 85 | a: 1, 86 | b: '1', 87 | c: { d: 'hello' }, 88 | e: ['a', 'b'], 89 | } 90 | const rules = [ 91 | { path: 'a', equal: 1, expected: true }, 92 | { path: 'a', equal: '1', expected: true }, 93 | { path: 'a', equal: '2', expected: false }, 94 | { path: 'b', equal: 1, expected: true }, 95 | { path: 'b', equal: '1', expected: true }, 96 | { path: 'b', not: { equal: 1 }, expected: false }, 97 | { path: 'b', not: { equal: '1' }, expected: false }, 98 | { path: 'b', not: { equal: 2 }, expected: true }, 99 | { path: 'b', not: { equal: '2' }, expected: true }, 100 | { path: 'b', not: { not: { equal: 1 } }, expected: true }, 101 | { path: 'b', not: { not: { equal: 2 } }, expected: false }, 102 | { path: 'c', notEqual: 'a', expected: true }, 103 | { path: 'a', notEqual: 1, expected: false }, 104 | { path: 'a', in: [1, 2, 3], expected: true }, 105 | { path: 'a', in: [9, 10], expected: false }, 106 | { path: 'a', notIn: [2, 3], expected: true }, 107 | { path: 'a', notIn: [2, 1], expected: false }, 108 | { path: 'e', doesNotContain: 'hello', expected: true }, 109 | { path: 'e', doesNotContain: 'a', expected: false }, 110 | ] 111 | const results = [] 112 | const expected = [] 113 | for (const r of rules) { 114 | const res = await evaluator.evaluateSingleResource( 115 | { conditions: r } as any, 116 | { 117 | data, 118 | resource: { 119 | id: cuid(), 120 | }, 121 | } as any 122 | ) 123 | results.push(res.result) 124 | expected.push(r.expected ? Result.PASS : Result.FAIL) 125 | } 126 | expect(results).toStrictEqual(expected) 127 | }) 128 | 129 | test('should combine simple rules [and,or]', async () => { 130 | const data = { 131 | a: 1, 132 | b: 2, 133 | } 134 | const trueRule = { path: 'a', equal: 1 } 135 | const falseRule = { path: 'a', equal: 99 } 136 | let finding = await evaluator.evaluateSingleResource( 137 | { conditions: { and: [trueRule, trueRule, trueRule] } } as any, 138 | { 139 | data, 140 | resource: { 141 | id: cuid(), 142 | }, 143 | } as any 144 | ) 145 | 146 | expect(finding.result).toBe(Result.PASS) 147 | finding = await evaluator.evaluateSingleResource( 148 | { conditions: { and: [trueRule, trueRule, falseRule] } } as any, 149 | { 150 | data, 151 | resource: { 152 | id: cuid(), 153 | }, 154 | } as any 155 | ) 156 | expect(finding.result).toBe(Result.FAIL) 157 | 158 | finding = await evaluator.evaluateSingleResource( 159 | { conditions: { or: [falseRule, falseRule, falseRule] } } as any, 160 | { 161 | data, 162 | resource: { 163 | id: cuid(), 164 | }, 165 | } as any 166 | ) 167 | expect(finding.result).toBe(Result.FAIL) 168 | finding = await evaluator.evaluateSingleResource( 169 | { conditions: { or: [falseRule, trueRule, falseRule] } } as any, 170 | { 171 | data, 172 | resource: { 173 | id: cuid(), 174 | }, 175 | } as any 176 | ) 177 | expect(finding.result).toBe(Result.PASS) 178 | 179 | // nested 180 | finding = await evaluator.evaluateSingleResource( 181 | { 182 | conditions: { 183 | or: [falseRule, falseRule, { and: [trueRule, trueRule] }], 184 | }, 185 | } as any, 186 | { 187 | data, 188 | resource: { 189 | id: cuid(), 190 | }, 191 | } as any 192 | ) 193 | expect(finding.result).toBe(Result.PASS) 194 | 195 | finding = await evaluator.evaluateSingleResource( 196 | { 197 | conditions: { 198 | or: [falseRule, falseRule, { and: [trueRule, falseRule] }], 199 | }, 200 | } as any, 201 | { 202 | data, 203 | resource: { 204 | id: cuid(), 205 | }, 206 | } as any 207 | ) 208 | expect(finding.result).toBe(Result.FAIL) 209 | }) 210 | 211 | test('should resolve paths', async () => { 212 | const data = { data: { a: { b: [0, { d: 'value' }] } } } as any 213 | const rule = { path: 'xx', equal: 'value' } 214 | 215 | rule.path = 'a.b[1].d' 216 | let finding = await evaluator.evaluateSingleResource( 217 | { 218 | conditions: rule, 219 | resource: { 220 | id: cuid(), 221 | }, 222 | } as any, 223 | data 224 | ) 225 | expect(finding.result).toBe(Result.PASS) 226 | rule.path = 'a.b[0].d' 227 | finding = await evaluator.evaluateSingleResource( 228 | { 229 | conditions: rule, 230 | resource: { 231 | id: cuid(), 232 | }, 233 | } as any, 234 | data 235 | ) 236 | expect(finding.result).toBe(Result.FAIL) 237 | 238 | // @ is replaced by the resource path 239 | data.resourcePath = '$.a.b[1]' 240 | rule.path = '@.d' 241 | finding = await evaluator.evaluateSingleResource( 242 | { 243 | conditions: rule, 244 | resource: { 245 | id: cuid(), 246 | }, 247 | } as any, 248 | data 249 | ) 250 | expect(finding.result).toBe(Result.PASS) 251 | 252 | data.resourcePath = '$.a' 253 | rule.path = '@.b[1].d' 254 | finding = await evaluator.evaluateSingleResource( 255 | { 256 | conditions: rule, 257 | resource: { 258 | id: cuid(), 259 | }, 260 | } as any, 261 | data 262 | ) 263 | expect(finding.result).toBe(Result.PASS) 264 | }) 265 | 266 | test('should support array operators', async () => { 267 | const data = { data: { a: { b: [0, 1, 2] } } } as any 268 | let rule: any = { path: 'a.b', array_any: { path: '[*]', equal: 2 } } 269 | 270 | let finding = await evaluator.evaluateSingleResource( 271 | { 272 | conditions: rule, 273 | resource: { 274 | id: cuid(), 275 | }, 276 | } as any, 277 | data 278 | ) 279 | expect(finding.result).toBe(Result.PASS) 280 | 281 | // 282 | rule = { path: 'a.b', array_all: { path: '[*]', equal: 2 } } 283 | finding = await evaluator.evaluateSingleResource( 284 | { 285 | conditions: rule, 286 | resource: { 287 | id: cuid(), 288 | }, 289 | } as any, 290 | data 291 | ) 292 | expect(finding.result).toBe(Result.FAIL) 293 | 294 | rule = { path: 'a.b', array_all: { path: '[*]', greaterThan: -1 } } 295 | finding = await evaluator.evaluateSingleResource( 296 | { 297 | conditions: rule, 298 | resource: { 299 | id: cuid(), 300 | }, 301 | } as any, 302 | data 303 | ) 304 | expect(finding.result).toBe(Result.PASS) 305 | }) 306 | 307 | test('should support negation logic array operators', async () => { 308 | const data = { data: { a: { b: [0, 1, 2] } } } as any 309 | let rule: any = { 310 | path: 'a.b', 311 | not: { array_any: { path: '[*]', equal: 2 } }, 312 | } 313 | let finding = await evaluator.evaluateSingleResource( 314 | { conditions: rule, resource: { id: cuid() } } as any, 315 | data 316 | ) 317 | expect(finding.result).toBe(Result.FAIL) 318 | 319 | rule = { path: 'a.b', not: { array_all: { path: '[*]', equal: 2 } } } 320 | finding = await evaluator.evaluateSingleResource( 321 | { conditions: rule, resource: { id: cuid() } } as any, 322 | data 323 | ) 324 | expect(finding.result).toBe(Result.PASS) 325 | 326 | rule = { path: 'a.b', not: { array_all: { path: '[*]', greaterThan: -1 } } } 327 | finding = await evaluator.evaluateSingleResource( 328 | { conditions: rule, resource: { id: cuid() } } as any, 329 | data 330 | ) 331 | expect(finding.result).toBe(Result.FAIL) 332 | }) 333 | 334 | test('should support array with nested operators', async () => { 335 | const data = { 336 | data: { 337 | a: { 338 | b: [ 339 | { c: 1, d: 2 }, 340 | { c: 3, d: 4 }, 341 | ], 342 | }, 343 | }, 344 | resource: { 345 | id: cuid(), 346 | }, 347 | } as any 348 | const rule: any = { 349 | path: 'a.b', 350 | array_any: { 351 | path: '[*]', 352 | and: [ 353 | { 354 | path: '[*].c', 355 | equal: 3, 356 | }, 357 | { 358 | path: '[*].d', 359 | equal: 4, 360 | }, 361 | ], 362 | }, 363 | } 364 | 365 | const finding = await evaluator.evaluateSingleResource( 366 | { conditions: rule } as any, 367 | data 368 | ) 369 | expect(finding.result).toBe(Result.PASS) 370 | }) 371 | 372 | test('Should pass using the same value for the equal operator with the same path', async () => { 373 | const data = { 374 | data: { a: { b: [0, { e: 'same value', d: 'same value' }] } }, 375 | } 376 | const rule = { path: 'a.b[1].d', equal: { path: 'a.b[1].e' } } 377 | 378 | const finding = await evaluator.evaluateSingleResource( 379 | { 380 | conditions: rule, 381 | resource: { 382 | id: cuid(), 383 | }, 384 | } as any, 385 | data 386 | ) 387 | 388 | expect(finding.result).toBe(Result.PASS) 389 | }) 390 | 391 | test('Should fail using two different values for the equal operator', async () => { 392 | const data = { 393 | data: { a: { b: [0, { e: 'not the same', d: 'values' }] } }, 394 | } 395 | const rule = { path: 'a.b[1].d', equal: { path: 'a.b[1].e' } } 396 | 397 | const finding = await evaluator.evaluateSingleResource( 398 | { 399 | conditions: rule, 400 | resource: { 401 | id: cuid(), 402 | }, 403 | } as any, 404 | data 405 | ) 406 | 407 | expect(finding.result).toBe(Result.FAIL) 408 | }) 409 | 410 | test('Should fail using the same value for the equal operator but using a wrong path', async () => { 411 | const data = { 412 | data: { a: { b: [0, { e: 'values', d: 'values' }] } }, 413 | } 414 | const rule = { path: 'a.b[1].d', equal: { path: 'b[0].e' } } 415 | 416 | const finding = await evaluator.evaluateSingleResource( 417 | { 418 | conditions: rule, 419 | resource: { 420 | id: cuid(), 421 | }, 422 | } as any, 423 | data 424 | ) 425 | 426 | expect(finding.result).toBe(Result.FAIL) 427 | }) 428 | 429 | test('Should pass comparing a date greater than other', async () => { 430 | const data = { 431 | data: { 432 | a: { 433 | b: [ 434 | 0, 435 | { e: '2022-03-14T20:42:58.510Z', d: '2022-03-15T20:42:58.510Z' }, 436 | ], 437 | }, 438 | }, 439 | } 440 | const rule = { path: 'a.b[1].d', greaterThan: { path: 'a.b[1].e' } } 441 | 442 | const finding = await evaluator.evaluateSingleResource( 443 | { 444 | conditions: rule, 445 | resource: { 446 | id: cuid(), 447 | }, 448 | } as any, 449 | data 450 | ) 451 | 452 | expect(finding.result).toBe(Result.PASS) 453 | }) 454 | 455 | test('Should fail comparing a date after than other', async () => { 456 | const data = { 457 | data: { 458 | a: { 459 | b: [ 460 | 0, 461 | { e: '2022-03-15T20:42:58.510Z', d: new Date().toISOString() }, 462 | ], 463 | }, 464 | }, 465 | } 466 | const rule = { path: 'a.b[1].d', lessThan: { path: 'a.b[1].e' } } 467 | 468 | const finding = await evaluator.evaluateSingleResource( 469 | { 470 | conditions: rule, 471 | resource: { 472 | id: cuid(), 473 | }, 474 | } as any, 475 | data 476 | ) 477 | 478 | expect(finding.result).toBe(Result.FAIL) 479 | }) 480 | }) 481 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License, version 2.0 2 | 3 | 1. Definitions 4 | 5 | 1.1. “Contributor” 6 | 7 | means each individual or legal entity that creates, contributes to the 8 | creation of, or owns Covered Software. 9 | 10 | 1.2. “Contributor Version” 11 | 12 | means the combination of the Contributions of others (if any) used by a 13 | Contributor and that particular Contributor’s Contribution. 14 | 15 | 1.3. “Contribution” 16 | 17 | means Covered Software of a particular Contributor. 18 | 19 | 1.4. “Covered Software” 20 | 21 | means Source Code Form to which the initial Contributor has attached the 22 | notice in Exhibit A, the Executable Form of such Source Code Form, and 23 | Modifications of such Source Code Form, in each case including portions 24 | thereof. 25 | 26 | 1.5. “Incompatible With Secondary Licenses” 27 | means 28 | 29 | a. that the initial Contributor has attached the notice described in 30 | Exhibit B to the Covered Software; or 31 | 32 | b. that the Covered Software was made available under the terms of version 33 | 1.1 or earlier of the License, but not also under the terms of a 34 | Secondary License. 35 | 36 | 1.6. “Executable Form” 37 | 38 | means any form of the work other than Source Code Form. 39 | 40 | 1.7. “Larger Work” 41 | 42 | means a work that combines Covered Software with other material, in a separate 43 | file or files, that is not Covered Software. 44 | 45 | 1.8. “License” 46 | 47 | means this document. 48 | 49 | 1.9. “Licensable” 50 | 51 | means having the right to grant, to the maximum extent possible, whether at the 52 | time of the initial grant or subsequently, any and all of the rights conveyed by 53 | this License. 54 | 55 | 1.10. “Modifications” 56 | 57 | means any of the following: 58 | 59 | a. any file in Source Code Form that results from an addition to, deletion 60 | from, or modification of the contents of Covered Software; or 61 | 62 | b. any new file in Source Code Form that contains any Covered Software. 63 | 64 | 1.11. “Patent Claims” of a Contributor 65 | 66 | means any patent claim(s), including without limitation, method, process, 67 | and apparatus claims, in any patent Licensable by such Contributor that 68 | would be infringed, but for the grant of the License, by the making, 69 | using, selling, offering for sale, having made, import, or transfer of 70 | either its Contributions or its Contributor Version. 71 | 72 | 1.12. “Secondary License” 73 | 74 | means either the GNU General Public License, Version 2.0, the GNU Lesser 75 | General Public License, Version 2.1, the GNU Affero General Public 76 | License, Version 3.0, or any later versions of those licenses. 77 | 78 | 1.13. “Source Code Form” 79 | 80 | means the form of the work preferred for making modifications. 81 | 82 | 1.14. “You” (or “Your”) 83 | 84 | means an individual or a legal entity exercising rights under this 85 | License. For legal entities, “You” includes any entity that controls, is 86 | controlled by, or is under common control with You. For purposes of this 87 | definition, “control” means (a) the power, direct or indirect, to cause 88 | the direction or management of such entity, whether by contract or 89 | otherwise, or (b) ownership of more than fifty percent (50%) of the 90 | outstanding shares or beneficial ownership of such entity. 91 | 92 | 93 | 2. License Grants and Conditions 94 | 95 | 2.1. Grants 96 | 97 | Each Contributor hereby grants You a world-wide, royalty-free, 98 | non-exclusive license: 99 | 100 | a. under intellectual property rights (other than patent or trademark) 101 | Licensable by such Contributor to use, reproduce, make available, 102 | modify, display, perform, distribute, and otherwise exploit its 103 | Contributions, either on an unmodified basis, with Modifications, or as 104 | part of a Larger Work; and 105 | 106 | b. under Patent Claims of such Contributor to make, use, sell, offer for 107 | sale, have made, import, and otherwise transfer either its Contributions 108 | or its Contributor Version. 109 | 110 | 2.2. Effective Date 111 | 112 | The licenses granted in Section 2.1 with respect to any Contribution become 113 | effective for each Contribution on the date the Contributor first distributes 114 | such Contribution. 115 | 116 | 2.3. Limitations on Grant Scope 117 | 118 | The licenses granted in this Section 2 are the only rights granted under this 119 | License. No additional rights or licenses will be implied from the distribution 120 | or licensing of Covered Software under this License. Notwithstanding Section 121 | 2.1(b) above, no patent license is granted by a Contributor: 122 | 123 | a. for any code that a Contributor has removed from Covered Software; or 124 | 125 | b. for infringements caused by: (i) Your and any other third party’s 126 | modifications of Covered Software, or (ii) the combination of its 127 | Contributions with other software (except as part of its Contributor 128 | Version); or 129 | 130 | c. under Patent Claims infringed by Covered Software in the absence of its 131 | Contributions. 132 | 133 | This License does not grant any rights in the trademarks, service marks, or 134 | logos of any Contributor (except as may be necessary to comply with the 135 | notice requirements in Section 3.4). 136 | 137 | 2.4. Subsequent Licenses 138 | 139 | No Contributor makes additional grants as a result of Your choice to 140 | distribute the Covered Software under a subsequent version of this License 141 | (see Section 10.2) or under the terms of a Secondary License (if permitted 142 | under the terms of Section 3.3). 143 | 144 | 2.5. Representation 145 | 146 | Each Contributor represents that the Contributor believes its Contributions 147 | are its original creation(s) or it has sufficient rights to grant the 148 | rights to its Contributions conveyed by this License. 149 | 150 | 2.6. Fair Use 151 | 152 | This License is not intended to limit any rights You have under applicable 153 | copyright doctrines of fair use, fair dealing, or other equivalents. 154 | 155 | 2.7. Conditions 156 | 157 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 158 | Section 2.1. 159 | 160 | 161 | 3. Responsibilities 162 | 163 | 3.1. Distribution of Source Form 164 | 165 | All distribution of Covered Software in Source Code Form, including any 166 | Modifications that You create or to which You contribute, must be under the 167 | terms of this License. You must inform recipients that the Source Code Form 168 | of the Covered Software is governed by the terms of this License, and how 169 | they can obtain a copy of this License. You may not attempt to alter or 170 | restrict the recipients’ rights in the Source Code Form. 171 | 172 | 3.2. Distribution of Executable Form 173 | 174 | If You distribute Covered Software in Executable Form then: 175 | 176 | a. such Covered Software must also be made available in Source Code Form, 177 | as described in Section 3.1, and You must inform recipients of the 178 | Executable Form how they can obtain a copy of such Source Code Form by 179 | reasonable means in a timely manner, at a charge no more than the cost 180 | of distribution to the recipient; and 181 | 182 | b. You may distribute such Executable Form under the terms of this License, 183 | or sublicense it under different terms, provided that the license for 184 | the Executable Form does not attempt to limit or alter the recipients’ 185 | rights in the Source Code Form under this License. 186 | 187 | 3.3. Distribution of a Larger Work 188 | 189 | You may create and distribute a Larger Work under terms of Your choice, 190 | provided that You also comply with the requirements of this License for the 191 | Covered Software. If the Larger Work is a combination of Covered Software 192 | with a work governed by one or more Secondary Licenses, and the Covered 193 | Software is not Incompatible With Secondary Licenses, this License permits 194 | You to additionally distribute such Covered Software under the terms of 195 | such Secondary License(s), so that the recipient of the Larger Work may, at 196 | their option, further distribute the Covered Software under the terms of 197 | either this License or such Secondary License(s). 198 | 199 | 3.4. Notices 200 | 201 | You may not remove or alter the substance of any license notices (including 202 | copyright notices, patent notices, disclaimers of warranty, or limitations 203 | of liability) contained within the Source Code Form of the Covered 204 | Software, except that You may alter any license notices to the extent 205 | required to remedy known factual inaccuracies. 206 | 207 | 3.5. Application of Additional Terms 208 | 209 | You may choose to offer, and to charge a fee for, warranty, support, 210 | indemnity or liability obligations to one or more recipients of Covered 211 | Software. However, You may do so only on Your own behalf, and not on behalf 212 | of any Contributor. You must make it absolutely clear that any such 213 | warranty, support, indemnity, or liability obligation is offered by You 214 | alone, and You hereby agree to indemnify every Contributor for any 215 | liability incurred by such Contributor as a result of warranty, support, 216 | indemnity or liability terms You offer. You may include additional 217 | disclaimers of warranty and limitations of liability specific to any 218 | jurisdiction. 219 | 220 | 4. Inability to Comply Due to Statute or Regulation 221 | 222 | If it is impossible for You to comply with any of the terms of this License 223 | with respect to some or all of the Covered Software due to statute, judicial 224 | order, or regulation then You must: (a) comply with the terms of this License 225 | to the maximum extent possible; and (b) describe the limitations and the code 226 | they affect. Such description must be placed in a text file included with all 227 | distributions of the Covered Software under this License. Except to the 228 | extent prohibited by statute or regulation, such description must be 229 | sufficiently detailed for a recipient of ordinary skill to be able to 230 | understand it. 231 | 232 | 5. Termination 233 | 234 | 5.1. The rights granted under this License will terminate automatically if You 235 | fail to comply with any of its terms. However, if You become compliant, 236 | then the rights granted under this License from a particular Contributor 237 | are reinstated (a) provisionally, unless and until such Contributor 238 | explicitly and finally terminates Your grants, and (b) on an ongoing basis, 239 | if such Contributor fails to notify You of the non-compliance by some 240 | reasonable means prior to 60 days after You have come back into compliance. 241 | Moreover, Your grants from a particular Contributor are reinstated on an 242 | ongoing basis if such Contributor notifies You of the non-compliance by 243 | some reasonable means, this is the first time You have received notice of 244 | non-compliance with this License from such Contributor, and You become 245 | compliant prior to 30 days after Your receipt of the notice. 246 | 247 | 5.2. If You initiate litigation against any entity by asserting a patent 248 | infringement claim (excluding declaratory judgment actions, counter-claims, 249 | and cross-claims) alleging that a Contributor Version directly or 250 | indirectly infringes any patent, then the rights granted to You by any and 251 | all Contributors for the Covered Software under Section 2.1 of this License 252 | shall terminate. 253 | 254 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 255 | license agreements (excluding distributors and resellers) which have been 256 | validly granted by You or Your distributors under this License prior to 257 | termination shall survive termination. 258 | 259 | 6. Disclaimer of Warranty 260 | 261 | Covered Software is provided under this License on an “as is” basis, without 262 | warranty of any kind, either expressed, implied, or statutory, including, 263 | without limitation, warranties that the Covered Software is free of defects, 264 | merchantable, fit for a particular purpose or non-infringing. The entire 265 | risk as to the quality and performance of the Covered Software is with You. 266 | Should any Covered Software prove defective in any respect, You (not any 267 | Contributor) assume the cost of any necessary servicing, repair, or 268 | correction. This disclaimer of warranty constitutes an essential part of this 269 | License. No use of any Covered Software is authorized under this License 270 | except under this disclaimer. 271 | 272 | 7. Limitation of Liability 273 | 274 | Under no circumstances and under no legal theory, whether tort (including 275 | negligence), contract, or otherwise, shall any Contributor, or anyone who 276 | distributes Covered Software as permitted above, be liable to You for any 277 | direct, indirect, special, incidental, or consequential damages of any 278 | character including, without limitation, damages for lost profits, loss of 279 | goodwill, work stoppage, computer failure or malfunction, or any and all 280 | other commercial damages or losses, even if such party shall have been 281 | informed of the possibility of such damages. This limitation of liability 282 | shall not apply to liability for death or personal injury resulting from such 283 | party’s negligence to the extent applicable law prohibits such limitation. 284 | Some jurisdictions do not allow the exclusion or limitation of incidental or 285 | consequential damages, so this exclusion and limitation may not apply to You. 286 | 287 | 8. Litigation 288 | 289 | Any litigation relating to this License may be brought only in the courts of 290 | a jurisdiction where the defendant maintains its principal place of business 291 | and such litigation shall be governed by laws of that jurisdiction, without 292 | reference to its conflict-of-law provisions. Nothing in this Section shall 293 | prevent a party’s ability to bring cross-claims or counter-claims. 294 | 295 | 9. Miscellaneous 296 | 297 | This License represents the complete agreement concerning the subject matter 298 | hereof. If any provision of this License is held to be unenforceable, such 299 | provision shall be reformed only to the extent necessary to make it 300 | enforceable. Any law or regulation which provides that the language of a 301 | contract shall be construed against the drafter shall not be used to construe 302 | this License against a Contributor. 303 | 304 | 305 | 10. Versions of the License 306 | 307 | 10.1. New Versions 308 | 309 | Mozilla Foundation is the license steward. Except as provided in Section 310 | 10.3, no one other than the license steward has the right to modify or 311 | publish new versions of this License. Each version will be given a 312 | distinguishing version number. 313 | 314 | 10.2. Effect of New Versions 315 | 316 | You may distribute the Covered Software under the terms of the version of 317 | the License under which You originally received the Covered Software, or 318 | under the terms of any subsequent version published by the license 319 | steward. 320 | 321 | 10.3. Modified Versions 322 | 323 | If you create software not governed by this License, and you want to 324 | create a new license for such software, you may create and use a modified 325 | version of this License if you rename the license and remove any 326 | references to the name of the license steward (except to note that such 327 | modified license differs from this License). 328 | 329 | 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses 330 | If You choose to distribute Source Code Form that is Incompatible With 331 | Secondary Licenses under the terms of this version of the License, the 332 | notice described in Exhibit B of this License must be attached. 333 | 334 | Exhibit A - Source Code Form License Notice 335 | 336 | This Source Code Form is subject to the 337 | terms of the Mozilla Public License, v. 338 | 2.0. If a copy of the MPL was not 339 | distributed with this file, You can 340 | obtain one at 341 | http://mozilla.org/MPL/2.0/. 342 | 343 | If it is not possible or desirable to put the notice in a particular file, then 344 | You may include the notice in a location (such as a LICENSE file in a relevant 345 | directory) where a recipient would be likely to look for such a notice. 346 | 347 | You may add additional accurate notices of copyright ownership. 348 | 349 | Exhibit B - “Incompatible With Secondary Licenses” Notice 350 | 351 | This Source Code Form is “Incompatible 352 | With Secondary Licenses”, as defined by 353 | the Mozilla Public License, v. 2.0. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [0.23.0](https://github.com/cloudgraphdev/sdk/compare/0.22.1...0.23.0) (2023-07-18) 2 | 3 | 4 | ### Features 5 | 6 | * **api:** add errors to result interface ([6b66fce](https://github.com/cloudgraphdev/sdk/commit/6b66fcefc6a6f237ea07b8e66d5a7583191e4b2e)) 7 | 8 | # [0.23.0-beta.1](https://github.com/cloudgraphdev/sdk/compare/0.22.1...0.23.0-beta.1) (2023-07-18) 9 | 10 | 11 | ### Features 12 | 13 | * **api:** add errors to result interface ([6b66fce](https://github.com/cloudgraphdev/sdk/commit/6b66fcefc6a6f237ea07b8e66d5a7583191e4b2e)) 14 | 15 | # [0.23.0-alpha.1](https://github.com/cloudgraphdev/sdk/compare/0.22.1...0.23.0-alpha.1) (2023-07-18) 16 | 17 | 18 | ### Features 19 | 20 | * **api:** add errors to result interface ([6b66fce](https://github.com/cloudgraphdev/sdk/commit/6b66fcefc6a6f237ea07b8e66d5a7583191e4b2e)) 21 | 22 | ## [0.22.1](https://github.com/cloudgraphdev/sdk/compare/0.22.0...0.22.1) (2022-11-25) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * add uri-js to resolutions to fix cve vulnerability ([5290e0a](https://github.com/cloudgraphdev/sdk/commit/5290e0a828f1302e3fc16e5dddded36fb013d08d)) 28 | * cve vulnerabilities remediations ([c0c6200](https://github.com/cloudgraphdev/sdk/commit/c0c620079958ec562797bbf76561c028754fcfa1)) 29 | 30 | ## [0.22.1-beta.1](https://github.com/cloudgraphdev/sdk/compare/0.22.0...0.22.1-beta.1) (2022-11-25) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * add uri-js to resolutions to fix cve vulnerability ([5290e0a](https://github.com/cloudgraphdev/sdk/commit/5290e0a828f1302e3fc16e5dddded36fb013d08d)) 36 | * cve vulnerabilities remediations ([c0c6200](https://github.com/cloudgraphdev/sdk/commit/c0c620079958ec562797bbf76561c028754fcfa1)) 37 | 38 | ## [0.22.1-alpha.1](https://github.com/cloudgraphdev/sdk/compare/0.22.0...0.22.1-alpha.1) (2022-11-25) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * add uri-js to resolutions to fix cve vulnerability ([5290e0a](https://github.com/cloudgraphdev/sdk/commit/5290e0a828f1302e3fc16e5dddded36fb013d08d)) 44 | * cve vulnerabilities remediations ([c0c6200](https://github.com/cloudgraphdev/sdk/commit/c0c620079958ec562797bbf76561c028754fcfa1)) 45 | 46 | # [0.22.0](https://github.com/cloudgraphdev/sdk/compare/0.21.1...0.22.0) (2022-10-04) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * **sdk:** change type any to DocumentNode ([f85bc9c](https://github.com/cloudgraphdev/sdk/commit/f85bc9c9ef00d81e99450c714fe1d8f8c5e27753)) 52 | 53 | 54 | ### Features 55 | 56 | * **sdk:** return raw schemas instead of strings ([2990e7b](https://github.com/cloudgraphdev/sdk/commit/2990e7b6ec6784b08ebb1c4ad8c00300ae98f520)) 57 | 58 | # [0.22.0-beta.1](https://github.com/cloudgraphdev/sdk/compare/0.21.1...0.22.0-beta.1) (2022-10-04) 59 | 60 | 61 | ### Bug Fixes 62 | 63 | * **sdk:** change type any to DocumentNode ([f85bc9c](https://github.com/cloudgraphdev/sdk/commit/f85bc9c9ef00d81e99450c714fe1d8f8c5e27753)) 64 | 65 | 66 | ### Features 67 | 68 | * **sdk:** return raw schemas instead of strings ([2990e7b](https://github.com/cloudgraphdev/sdk/commit/2990e7b6ec6784b08ebb1c4ad8c00300ae98f520)) 69 | 70 | # [0.22.0-alpha.1](https://github.com/cloudgraphdev/sdk/compare/0.21.1...0.22.0-alpha.1) (2022-10-04) 71 | 72 | 73 | ### Bug Fixes 74 | 75 | * **sdk:** change type any to DocumentNode ([f85bc9c](https://github.com/cloudgraphdev/sdk/commit/f85bc9c9ef00d81e99450c714fe1d8f8c5e27753)) 76 | 77 | 78 | ### Features 79 | 80 | * **sdk:** return raw schemas instead of strings ([2990e7b](https://github.com/cloudgraphdev/sdk/commit/2990e7b6ec6784b08ebb1c4ad8c00300ae98f520)) 81 | 82 | ## [0.21.1](https://github.com/cloudgraphdev/sdk/compare/0.21.0...0.21.1) (2022-07-12) 83 | 84 | 85 | ### Bug Fixes 86 | 87 | * Added validations for null arrays ([32ad697](https://github.com/cloudgraphdev/sdk/commit/32ad697c8206590e4b036d34dea678c8992610b6)) 88 | 89 | ## [0.21.1-beta.1](https://github.com/cloudgraphdev/sdk/compare/0.21.0...0.21.1-beta.1) (2022-07-12) 90 | 91 | 92 | ### Bug Fixes 93 | 94 | * Added validations for null arrays ([32ad697](https://github.com/cloudgraphdev/sdk/commit/32ad697c8206590e4b036d34dea678c8992610b6)) 95 | 96 | ## [0.21.1-alpha.1](https://github.com/cloudgraphdev/sdk/compare/0.21.0...0.21.1-alpha.1) (2022-07-12) 97 | 98 | 99 | ### Bug Fixes 100 | 101 | * Added validations for null arrays ([32ad697](https://github.com/cloudgraphdev/sdk/commit/32ad697c8206590e4b036d34dea678c8992610b6)) 102 | 103 | # [0.21.0](https://github.com/cloudgraphdev/sdk/compare/0.20.0...0.21.0) (2022-07-06) 104 | 105 | 106 | ### Bug Fixes 107 | 108 | * adapt CLI to pass data instead of queries ([dbaa7ce](https://github.com/cloudgraphdev/sdk/commit/dbaa7ced67870863792cedaaf80b965c6d11cbf0)) 109 | * remove node-jq implementation ([c614842](https://github.com/cloudgraphdev/sdk/commit/c6148429e056cd64cc3055f861b597e2dbcdbb09)) 110 | * Rollback missing resources status feature ([5eb10e9](https://github.com/cloudgraphdev/sdk/commit/5eb10e9c610e92ec608acbac1fdefb4a84ea109c)) 111 | 112 | 113 | ### Features 114 | 115 | * adapt CLI to pass data instead of queries ([1d73587](https://github.com/cloudgraphdev/sdk/commit/1d735872a6703e3880b26df8ee8d33982d56c3f1)) 116 | * Added unit tests ([0d14267](https://github.com/cloudgraphdev/sdk/commit/0d14267c357307b8812d4d35faa2a50bc73437a7)) 117 | * Extend rule engine to include missing checks when the resource does not exist ([79f8dd3](https://github.com/cloudgraphdev/sdk/commit/79f8dd3c9ffa149c431feb09f9b087a8b92e5250)) 118 | * Move getLinkedData method to utils folder ([171d937](https://github.com/cloudgraphdev/sdk/commit/171d937ba7d4f1b6b9c0f878bd58288724ba138e)) 119 | * remove unused storageEngine param ([a9a4ccb](https://github.com/cloudgraphdev/sdk/commit/a9a4ccb3613e7cf3dbc08a1e74bddef9d082fbc4)) 120 | 121 | # [0.21.0-beta.1](https://github.com/cloudgraphdev/sdk/compare/0.20.0...0.21.0-beta.1) (2022-07-06) 122 | 123 | 124 | ### Bug Fixes 125 | 126 | * adapt CLI to pass data instead of queries ([dbaa7ce](https://github.com/cloudgraphdev/sdk/commit/dbaa7ced67870863792cedaaf80b965c6d11cbf0)) 127 | * remove node-jq implementation ([c614842](https://github.com/cloudgraphdev/sdk/commit/c6148429e056cd64cc3055f861b597e2dbcdbb09)) 128 | * Rollback missing resources status feature ([5eb10e9](https://github.com/cloudgraphdev/sdk/commit/5eb10e9c610e92ec608acbac1fdefb4a84ea109c)) 129 | 130 | 131 | ### Features 132 | 133 | * adapt CLI to pass data instead of queries ([1d73587](https://github.com/cloudgraphdev/sdk/commit/1d735872a6703e3880b26df8ee8d33982d56c3f1)) 134 | * Added unit tests ([0d14267](https://github.com/cloudgraphdev/sdk/commit/0d14267c357307b8812d4d35faa2a50bc73437a7)) 135 | * Extend rule engine to include missing checks when the resource does not exist ([79f8dd3](https://github.com/cloudgraphdev/sdk/commit/79f8dd3c9ffa149c431feb09f9b087a8b92e5250)) 136 | * Move getLinkedData method to utils folder ([171d937](https://github.com/cloudgraphdev/sdk/commit/171d937ba7d4f1b6b9c0f878bd58288724ba138e)) 137 | * remove unused storageEngine param ([a9a4ccb](https://github.com/cloudgraphdev/sdk/commit/a9a4ccb3613e7cf3dbc08a1e74bddef9d082fbc4)) 138 | 139 | # [0.21.0-alpha.3](https://github.com/cloudgraphdev/sdk/compare/0.21.0-alpha.2...0.21.0-alpha.3) (2022-07-06) 140 | 141 | 142 | ### Bug Fixes 143 | 144 | * remove node-jq implementation ([c614842](https://github.com/cloudgraphdev/sdk/commit/c6148429e056cd64cc3055f861b597e2dbcdbb09)) 145 | * Rollback missing resources status feature ([5eb10e9](https://github.com/cloudgraphdev/sdk/commit/5eb10e9c610e92ec608acbac1fdefb4a84ea109c)) 146 | 147 | # [0.21.0-alpha.2](https://github.com/cloudgraphdev/sdk/compare/0.21.0-alpha.1...0.21.0-alpha.2) (2022-06-30) 148 | 149 | 150 | ### Bug Fixes 151 | 152 | * adapt CLI to pass data instead of queries ([dbaa7ce](https://github.com/cloudgraphdev/sdk/commit/dbaa7ced67870863792cedaaf80b965c6d11cbf0)) 153 | 154 | 155 | ### Features 156 | 157 | * adapt CLI to pass data instead of queries ([1d73587](https://github.com/cloudgraphdev/sdk/commit/1d735872a6703e3880b26df8ee8d33982d56c3f1)) 158 | * Move getLinkedData method to utils folder ([171d937](https://github.com/cloudgraphdev/sdk/commit/171d937ba7d4f1b6b9c0f878bd58288724ba138e)) 159 | * remove unused storageEngine param ([a9a4ccb](https://github.com/cloudgraphdev/sdk/commit/a9a4ccb3613e7cf3dbc08a1e74bddef9d082fbc4)) 160 | 161 | # [0.21.0-alpha.1](https://github.com/cloudgraphdev/sdk/compare/0.20.0...0.21.0-alpha.1) (2022-06-29) 162 | 163 | 164 | ### Features 165 | 166 | * Added unit tests ([0d14267](https://github.com/cloudgraphdev/sdk/commit/0d14267c357307b8812d4d35faa2a50bc73437a7)) 167 | * Extend rule engine to include missing checks when the resource does not exist ([79f8dd3](https://github.com/cloudgraphdev/sdk/commit/79f8dd3c9ffa149c431feb09f9b087a8b92e5250)) 168 | 169 | # [0.20.0](https://github.com/cloudgraphdev/sdk/compare/0.19.0...0.20.0) (2022-06-09) 170 | 171 | 172 | ### Features 173 | 174 | * Added util function to generate unique ids ([8d8fc11](https://github.com/cloudgraphdev/sdk/commit/8d8fc11e0a1678ce47ce0f32f296009de253b8da)) 175 | * Adding utils to generate mutations dynamically ([c1d6fa3](https://github.com/cloudgraphdev/sdk/commit/c1d6fa3d2dec99bc25d9454b0c7801340650cffe)) 176 | 177 | # [0.20.0-beta.1](https://github.com/cloudgraphdev/sdk/compare/0.19.0...0.20.0-beta.1) (2022-06-09) 178 | 179 | 180 | ### Features 181 | 182 | * Added util function to generate unique ids ([8d8fc11](https://github.com/cloudgraphdev/sdk/commit/8d8fc11e0a1678ce47ce0f32f296009de253b8da)) 183 | * Adding utils to generate mutations dynamically ([c1d6fa3](https://github.com/cloudgraphdev/sdk/commit/c1d6fa3d2dec99bc25d9454b0c7801340650cffe)) 184 | 185 | # [0.20.0-alpha.2](https://github.com/cloudgraphdev/sdk/compare/0.20.0-alpha.1...0.20.0-alpha.2) (2022-06-09) 186 | 187 | 188 | ### Features 189 | 190 | * Added util function to generate unique ids ([8d8fc11](https://github.com/cloudgraphdev/sdk/commit/8d8fc11e0a1678ce47ce0f32f296009de253b8da)) 191 | 192 | # [0.20.0-alpha.1](https://github.com/cloudgraphdev/sdk/compare/0.19.0...0.20.0-alpha.1) (2022-05-11) 193 | 194 | 195 | ### Features 196 | 197 | * Adding utils to generate mutations dynamically ([c1d6fa3](https://github.com/cloudgraphdev/sdk/commit/c1d6fa3d2dec99bc25d9454b0c7801340650cffe)) 198 | 199 | # [0.19.0](https://github.com/cloudgraphdev/sdk/compare/0.18.1...0.19.0) (2022-05-02) 200 | 201 | 202 | ### Bug Fixes 203 | 204 | * Fixed empty rules edge case ([4953a6c](https://github.com/cloudgraphdev/sdk/commit/4953a6ce10b9e41d5f569f80fcdbed6db88be0b8)) 205 | 206 | 207 | ### Features 208 | 209 | * Considered edge cases for aws provider extra fields ([ab75afa](https://github.com/cloudgraphdev/sdk/commit/ab75afa8bfeb0949e053bb38440cae5ce1628ded)) 210 | * Reduced mutations payload ([f931f91](https://github.com/cloudgraphdev/sdk/commit/f931f91c8df7a856f68d631bf801666a4f0a7ab6)) 211 | * Refactored RulesEngine to improve mutations performance ([af20dea](https://github.com/cloudgraphdev/sdk/commit/af20dea938b83e79c714114747ebd08b0284ba38)) 212 | 213 | # [0.19.0-beta.1](https://github.com/cloudgraphdev/sdk/compare/0.18.1...0.19.0-beta.1) (2022-05-02) 214 | 215 | 216 | ### Bug Fixes 217 | 218 | * Fixed empty rules edge case ([4953a6c](https://github.com/cloudgraphdev/sdk/commit/4953a6ce10b9e41d5f569f80fcdbed6db88be0b8)) 219 | 220 | 221 | ### Features 222 | 223 | * Considered edge cases for aws provider extra fields ([ab75afa](https://github.com/cloudgraphdev/sdk/commit/ab75afa8bfeb0949e053bb38440cae5ce1628ded)) 224 | * Reduced mutations payload ([f931f91](https://github.com/cloudgraphdev/sdk/commit/f931f91c8df7a856f68d631bf801666a4f0a7ab6)) 225 | * Refactored RulesEngine to improve mutations performance ([af20dea](https://github.com/cloudgraphdev/sdk/commit/af20dea938b83e79c714114747ebd08b0284ba38)) 226 | 227 | # [0.19.0-alpha.1](https://github.com/cloudgraphdev/sdk/compare/0.18.1...0.19.0-alpha.1) (2022-04-25) 228 | 229 | 230 | ### Bug Fixes 231 | 232 | * Fixed empty rules edge case ([4953a6c](https://github.com/cloudgraphdev/sdk/commit/4953a6ce10b9e41d5f569f80fcdbed6db88be0b8)) 233 | 234 | 235 | ### Features 236 | 237 | * Considered edge cases for aws provider extra fields ([ab75afa](https://github.com/cloudgraphdev/sdk/commit/ab75afa8bfeb0949e053bb38440cae5ce1628ded)) 238 | * Reduced mutations payload ([f931f91](https://github.com/cloudgraphdev/sdk/commit/f931f91c8df7a856f68d631bf801666a4f0a7ab6)) 239 | * Refactored RulesEngine to improve mutations performance ([af20dea](https://github.com/cloudgraphdev/sdk/commit/af20dea938b83e79c714114747ebd08b0284ba38)) 240 | 241 | ## [0.18.1](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.18.0...0.18.1) (2022-03-23) 242 | 243 | 244 | ### Bug Fixes 245 | 246 | * Fixed node-jq version to 2.1.0 ([23ae96f](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/23ae96f07277a0460e9aa9e6a4d7761a9a969081)) 247 | 248 | # [0.18.0](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.17.0...0.18.0) (2022-03-22) 249 | 250 | 251 | ### Features 252 | 253 | * Included matchAll for regex operators ([6ff42ca](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/6ff42cab1e511f7948648642ca4bd350c4207dde)) 254 | * Included matchAny for regex operators ([b22971c](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/b22971c1425a24d41ce15f9f8f2cf9a0fddf7726)) 255 | 256 | # [0.17.0](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.16.0...0.17.0) (2022-03-18) 257 | 258 | 259 | ### Features 260 | 261 | * Validated null cases for isEmpty operator ([d142342](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/d142342f266595bd15318e1d10f5564180d3b835)) 262 | 263 | # [0.16.0](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.15.0...0.16.0) (2022-03-17) 264 | 265 | 266 | ### Features 267 | 268 | * Added DataProcessors to handle mutations depending on the StorageEngine ([e061c80](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/e061c80cd55a75816dfff9286b5757cc70d594dd)) 269 | * Composed related rules during prepare mutations method ([6ff9f9f](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/6ff9f9fb1b5912a04d9b587f9d207be2f2297ec7)) 270 | * Created CompositeEvaluator for composite rules ([b9b51a1](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/b9b51a1872eb2b23f07d04e24117de6bbcc8d2ce)) 271 | * Moved prepareMutations logic to DgraphDataProcessor ([6c7c5f5](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/6c7c5f5ee7ea15c19208e9abda9525bea6cfaa5f)) 272 | * Refactored RulesEngine to allow composite rules ([659e5b0](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/659e5b091cebe4ca2092f44e94a17bab22a82bca)) 273 | * Reused data for Automated Rules and created policyPackPlugin unit test ([cdc6226](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/cdc6226f80a81df814d5fa7b6976a935a11ed10b)) 274 | 275 | # [0.15.0](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.14.3...0.15.0) (2022-03-16) 276 | 277 | 278 | ### Features 279 | 280 | * First approach to compare two fields in the same object ([632ba76](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/632ba76725b0f4ac703a35c07d89d5489a8edb24)) 281 | 282 | ## [0.14.3](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.14.2...0.14.3) (2022-03-10) 283 | 284 | 285 | ### Bug Fixes 286 | 287 | * Renamed array operators to common and validate empty objects as well ([064601c](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/064601c0efadfaf537bd2c14f8e8d955fd60ffb9)) 288 | 289 | ## [0.14.2](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.14.1...0.14.2) (2022-03-01) 290 | 291 | 292 | ### Bug Fixes 293 | 294 | * Added inverse relation to ruleMetadata ([345b44d](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/345b44da24120062ec0dcc6d8595df10c6bd1377)) 295 | * Avoided rule duplication using schema connections ([251a31f](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/251a31f40c6789b70d11785f43a0919c6e29b4b0)) 296 | * Display findings result properly ([d3d4eba](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/d3d4ebaf21fabdc518cf3bc14d2fe7f6fdd01c6f)) 297 | 298 | ## [0.14.1](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.14.0...0.14.1) (2022-02-22) 299 | 300 | 301 | ### Bug Fixes 302 | 303 | * Fixed exception to execute undefined arrays ([02183cc](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/02183cca465839ee1609b17c2dc7d746c03fa701)) 304 | 305 | # [0.14.0](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.13.1...0.14.0) (2022-02-10) 306 | 307 | 308 | ### Features 309 | 310 | * Included extra fields to findings result ([4510423](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/451042360db61e1105a20c1fdc403482d85a7fcf)) 311 | 312 | ## [0.13.1](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.13.0...0.13.1) (2022-02-04) 313 | 314 | 315 | ### Bug Fixes 316 | 317 | * Included title field for Rules ([717b9fb](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/717b9fbd12a76e59e3d8483564c3c9c68c2f08a8)) 318 | 319 | # [0.13.0](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.12.0...0.13.0) (2022-02-03) 320 | 321 | 322 | ### Features 323 | 324 | * Adjustments for the policyPack plugin ([c8df8d8](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/c8df8d8c87affb36fe447f103ec494f85c84a7bc)) 325 | * Removed cgPlugins key file ([6f94b6f](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/6f94b6f94f9165b3bae6914ad969666944af3f54)) 326 | * Reorder plugins files ([8845cfd](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/8845cfd7b0aff220a9ff45e9418f4f2d2422bce8)) 327 | 328 | # [0.12.0](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.11.0...0.12.0) (2022-02-01) 329 | 330 | 331 | ### Features 332 | 333 | * Created Manual Evaluator ([612809d](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/612809d9ce4e95e527efae69badc75bd58fa9d9d)) 334 | * Deprecated default evaluator ([f199f28](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/f199f28337f2b0c514ddff17149ba11779c64c7d)) 335 | * Updated rule evaluator definition ([1409105](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/1409105f2e1a9b8ccb3df60e1154a4e8f53ecbfb)) 336 | 337 | # [0.11.0](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.10.9...0.11.0) (2022-01-28) 338 | 339 | 340 | ### Features 341 | 342 | * add daysDiff operator ([cacba7a](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/cacba7a19c023fb2a70fc989343d7adf1180a7a4)) 343 | * add daysDiff operator ([e18b6ea](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/e18b6eab834f6efcadcc8ec80f44cd1aea6baea7)) 344 | 345 | ## [0.10.9](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.10.8...0.10.9) (2022-01-28) 346 | 347 | 348 | ### Bug Fixes 349 | 350 | * **rule-engine:** update rule interface to allow for more fields ([89e9959](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/89e99599a82ecd57e959fc348a9d60b71f07115f)) 351 | * **rule-engine:** update rule processor to use new fields in schema ([ebcd67e](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/ebcd67e246e3e0a7e76ed013956d5a4607a47b1b)) 352 | 353 | ## [0.10.8](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.10.7...0.10.8) (2022-01-28) 354 | 355 | 356 | ### Bug Fixes 357 | 358 | * Moved common utils to SDK ([ab96993](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/ab96993bafcc45db430d6bb7ace91a40dbdedd94)) 359 | 360 | ## [0.10.7](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.10.6...0.10.7) (2022-01-25) 361 | 362 | 363 | ### Bug Fixes 364 | 365 | * **getData:** update get data to take account param so we can include it in raw data ([7da8949](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/7da8949221ebed1ab97b1115999f54ffd80c4bd7)) 366 | 367 | ## [0.10.6](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.10.5...0.10.6) (2022-01-18) 368 | 369 | 370 | ### Bug Fixes 371 | 372 | * type cleanup, removed old prop ([d81eb1e](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/d81eb1e8212af9e0e4d62c6912e02d07af9ef011)) 373 | 374 | ## [0.10.5](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.10.4...0.10.5) (2022-01-18) 375 | 376 | 377 | ### Bug Fixes 378 | 379 | * **types:** refactor graphql types to allow patch operation, new type for input ([cbe266c](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/cbe266cc91aa299c5f698141f5d539d77a409eb7)) 380 | 381 | ## [0.10.4](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.10.3...0.10.4) (2022-01-18) 382 | 383 | 384 | ### Bug Fixes 385 | 386 | * Updated getSchemaFromFolder definition ([a5dbe1a](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/a5dbe1afeb4f73c3441ed184b0797fdb7e7eaadd)) 387 | 388 | ## [0.10.3](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.10.2...0.10.3) (2022-01-12) 389 | 390 | 391 | ### Bug Fixes 392 | 393 | * Implemented temporary solution to display findings results ([c9a7d18](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/c9a7d18160d2749003b96168accefce0c9937c88)) 394 | * Renamed Severity levels ([ad1fd6c](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/ad1fd6cfd51528d2e1f9637e0ecb1c029b66a0f4)) 395 | 396 | ## [0.10.2](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.10.1...0.10.2) (2022-01-11) 397 | 398 | 399 | ### Bug Fixes 400 | 401 | * Only shows execution message when plugins available ([ce868a6](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/ce868a640eb6a5a1c12bbe0a976be1d1f4513d0b)) 402 | 403 | ## [0.10.1](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.10.0...0.10.1) (2022-01-10) 404 | 405 | 406 | ### Bug Fixes 407 | 408 | * Removed policy pack plugin message during configuration ([eeede94](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/eeede9447d1bd23667852b656b68ccc73cad8b16)) 409 | 410 | # [0.10.0](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.9.0...0.10.0) (2022-01-07) 411 | 412 | 413 | ### Features 414 | 415 | * adding jq and not operators to policy packs json evaluator ([1912c61](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/1912c61d3a1d73e44fa24f53430d635fc07d9427)) 416 | 417 | # [0.9.0](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.8.0...0.9.0) (2022-01-05) 418 | 419 | 420 | ### Features 421 | 422 | * Included new regex operator for rules engine ([beb4bce](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/beb4bce4f0a81e08841562c30f5195d0e9e7ae76)) 423 | 424 | # [0.8.0](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.7.1...0.8.0) (2022-01-05) 425 | 426 | 427 | ### Bug Fixes 428 | 429 | * Updated default value for missing entity field ([1689810](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/168981088abff2fc25e7b8435ac279fa486259a0)) 430 | 431 | 432 | ### Features 433 | 434 | * Created overall schema for findings ([f46e567](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/f46e567459fd71efa62541bc962b2b40c8c9be3e)) 435 | * Grouped findings by entity ([19de4b7](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/19de4b7c99d06931764a55708722596ed162b9f2)) 436 | 437 | ## [0.7.1](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.7.0...0.7.1) (2021-12-29) 438 | 439 | 440 | ### Bug Fixes 441 | 442 | * Included description field to findings schema ([596b45a](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/596b45a979f442fa57b58591adfa851993ab6fc2)) 443 | 444 | # [0.7.0](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.6.1...0.7.0) (2021-12-28) 445 | 446 | 447 | ### Bug Fixes 448 | 449 | * Pass CLI flags during initialization ([f0046e7](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/f0046e7e1543b3008303fb658865daebdf829581)) 450 | * updated plugin types ([fefeeaa](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/fefeeaaa235c7156d6c5cbcd70600eb61712ff3a)) 451 | 452 | 453 | ### Features 454 | 455 | * Included new types from CLI ([fd50907](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/fd509072dd13cd18ad510dcc0f4ae6d83fa365be)) 456 | * Included plugin base class ([c7f2dc3](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/c7f2dc37d724155e838d7e4fe89c20ed7ad92e2c)) 457 | * Included StorageEngine interface to sdk ([52cb776](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/52cb77618fd6f06eb9b77152b126b97c5e2d388c)) 458 | * Included types for PluginManager ([7d8363f](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/7d8363f457fe37ced7bc4652d8adc37aa0f2aaac)) 459 | * Moved schema utils to sdk ([a04a424](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/a04a424fee99e41220995a29fcc6756f66524bff)) 460 | 461 | ## [0.6.1](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.6.0...0.6.1) (2021-11-23) 462 | 463 | 464 | ### Bug Fixes 465 | 466 | * Enabled update mutations at rules engine ([16fbfea](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/16fbfeabc5e60aaa5955146997c9a3f46aaba7b0)) 467 | * Fixed unit tests ([8d8f221](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/8d8f221f23b962a530d937708ee7b00908d34e32)) 468 | 469 | # [0.6.0](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.5.4...0.6.0) (2021-11-23) 470 | 471 | 472 | ### Features 473 | 474 | * tweaks to drop the need to maintain a mandatory mutation file ([e3e2e92](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/e3e2e92fddb00fe83a029ec7c90fb70e46be5f1d)) 475 | 476 | ## [0.5.4](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.5.3...0.5.4) (2021-11-19) 477 | 478 | 479 | ### Bug Fixes 480 | 481 | * Fixed RulesEngine interface ([8b8958a](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/8b8958a9f52a19db716b103cffbc381b94a8042b)) 482 | * Included severity field to rule engine ([4c18614](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/4c18614054d29d4f29ac8ff117f700ff6a91f35f)) 483 | * Validated empty data passed on processRule ([dfb6e1e](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/dfb6e1ebb50de683ee12c4a5e962bb027ee079d0)) 484 | 485 | ## [0.5.3](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.5.2...0.5.3) (2021-11-15) 486 | 487 | 488 | ### Bug Fixes 489 | 490 | * Added new operator to evaluate empty/filled arrays ([57cfc82](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/57cfc82880e19e823e29a2515fcb747e53cfd760)) 491 | 492 | ## [0.5.2](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.5.1...0.5.2) (2021-11-15) 493 | 494 | 495 | ### Bug Fixes 496 | 497 | * Extended logger type ([dd19b1a](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/dd19b1a23556e933fa8b8167defd91b9f067e4e2)) 498 | 499 | ## [0.5.1](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.5.0...0.5.1) (2021-11-09) 500 | 501 | 502 | ### Bug Fixes 503 | 504 | * Exported types for Rules Engine ([360a5bf](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/360a5bf70043eb642df54a90a571890fb863777d)) 505 | 506 | # [0.5.0](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.4.0...0.5.0) (2021-10-26) 507 | 508 | 509 | ### Bug Fixes 510 | 511 | * Fixed unknow field at awsFinding schema ([96bfd2c](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/96bfd2c5ccbcba4d00d6fcaf645d2e4025d6ecf1)) 512 | 513 | 514 | ### Features 515 | 516 | * Migrated Policy Pack engine to SDK repo ([d939607](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/d939607533d8c6a4c12f94f85a1648742c258a3e)) 517 | 518 | # [0.4.0](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.3.10...0.4.0) (2021-10-19) 519 | 520 | 521 | ### Bug Fixes 522 | 523 | * **debugLog:** set cg prefix on debug log ([3b1cb73](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/3b1cb73d0a502a5bb28d6b144a205a55d0e31dcf)) 524 | 525 | 526 | ### Features 527 | 528 | * **debugLog:** write error/debug logs to file in debug mode ([bb74b62](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/bb74b62967534d1dd4816462bf60f95dff220990)) 529 | 530 | ## [0.3.10](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.3.9...0.3.10) (2021-10-12) 531 | 532 | 533 | ### Bug Fixes 534 | 535 | * **service:** update service type with new params ([f990413](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/f990413155a7c5976ee682c30abe468795fb089a)) 536 | 537 | ## [0.3.9](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.3.8...0.3.9) (2021-09-28) 538 | 539 | 540 | ### Bug Fixes 541 | 542 | * **logger:** Update how logger handles instances of error objs ([5edfea0](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/5edfea0d6a1ef790ea730ff920a50bd0960d003c)) 543 | 544 | ## [0.3.8](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.3.7...0.3.8) (2021-09-24) 545 | 546 | 547 | ### Bug Fixes 548 | 549 | * Added extra attribute to getData interface ([853586f](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/853586fc1072ff9852361a7371d8a6567c29ddb3)) 550 | 551 | ## [0.3.7](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.3.6...0.3.7) (2021-09-21) 552 | 553 | 554 | ### Bug Fixes 555 | 556 | * **.gitlab-ci:** remove yarn lockfile freezing ([343d1ad](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/343d1ad442a06ba65424b66c151a3336137f560d)) 557 | * **.gitlab-ci:** set NODE_ENV to ci for build phase ([7d06713](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/7d067132e315a007e5b225382d3dc6398dfa0729)) 558 | * **.gitlab-gi:** set NODE_ENV before yarn install ([38c1335](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/38c13352a827474b085a7c893287d04dbeaf2e3c)) 559 | 560 | ## [0.3.6](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.3.5...0.3.6) (2021-09-21) 561 | 562 | 563 | ### Bug Fixes 564 | 565 | * **deps:** correct devDeps and deps list ([c9650dd](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/c9650dd2ea057593bfc4ea5f23e101803abb2cd6)) 566 | * **deps:** correct devDeps and deps list ([3f02b21](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/3f02b210440ac02e58ebb5fb2adc2cb9206ffa3a)) 567 | * **logger:** update logger so you can log during a spinner ([713805d](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/713805d1519f7a323418f9c2d7bbf962d32ee62f)) 568 | 569 | ## [0.3.5](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.3.4...0.3.5) (2021-09-20) 570 | 571 | 572 | ### Bug Fixes 573 | 574 | * **client:** Update client to assign empty obj to config if there isn't one ([3983c23](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/3983c23ade85abbe2139ea297c0c7d2edbf7ac19)) 575 | * **client:** Update client to assign empty obj to config if there isnt one ([18de389](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/18de3894e389bd8f936ed359f24b775d851fe736)) 576 | 577 | ## [0.3.4](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.3.3...0.3.4) (2021-09-08) 578 | 579 | 580 | ### Bug Fixes 581 | 582 | * Be more specific with types ([8c30b69](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/8c30b69f00932967dbdd1819cf518999144522cf)) 583 | * Rework logger parsing content ([aed8996](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/aed899658512319d7c7b37af66753495e76945a8)) 584 | 585 | ## [0.3.3](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.3.2...0.3.3) (2021-08-26) 586 | 587 | 588 | ### Bug Fixes 589 | 590 | * **spinner:** drop spinner restart on info/debug messages ([fa2383e](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/fa2383ef3c4a878e836a0295194cacb2030cb30d)) 591 | 592 | ## [0.3.2](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.3.1...0.3.2) (2021-08-25) 593 | 594 | 595 | ### Bug Fixes 596 | 597 | * **spinner:** downgrade ora to ^5.4.1 ERR_REQUIRE_ESM ([9749e37](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/9749e3770ef85a22e8cb8584a307c6fd5671d6c2)) 598 | 599 | ## [0.3.1](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.3.0...0.3.1) (2021-08-25) 600 | 601 | 602 | ### Bug Fixes 603 | 604 | * **spinner:** remove consola ([d38c4f6](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/d38c4f642608e20167f7f1bdbe148f7093ec0588)) 605 | * **spinner:** update logger debug env var ([1cd8064](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/1cd8064e84a4446d2ad5c191326d48dc22d65210)) 606 | * **spinner:** use Ora ([b429916](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/b4299161f1ae218af732faa877f75bedb12b724f)) 607 | * **spinner:** use Ora for CLI spinner ([aa19a32](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/aa19a329e4e3a95acf90669c9a2b6773c473fdc8)) 608 | * add Github repository references ([9fbf598](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/9fbf598c9907cf4b8e5c0ac4c8cf28f69c02855c)) 609 | 610 | # [0.3.0](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.2.0...0.3.0) (2021-08-23) 611 | 612 | 613 | ### Features 614 | 615 | * **interface:** create types for new provider interface ([6db4b7f](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/6db4b7f31319224322bcccbf4ae8cc9685573d6e)) 616 | * **interface:** Create types for new provider interface, update Client abstract class, update scripts with build command ([818836e](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/818836ef472b34466963fe94138e8917b7bc8b26)) 617 | 618 | # [0.2.0](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.1.1...0.2.0) (2021-08-16) 619 | 620 | 621 | ### Features 622 | 623 | * **LICENSE:** update license to MPL 2.0 ([2e534c6](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/2e534c6fbe4420de430ddf17398c471005435e81)) 624 | 625 | ## [0.1.1](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.1.0...0.1.1) (2021-08-12) 626 | 627 | 628 | ### Bug Fixes 629 | 630 | * **tsconfig:** Fixed ECMAScript target version ([0a40e62](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/0a40e62aee82b706dbddbda750829f106117a7c9)) 631 | 632 | # [0.1.0](https://gitlab.com/auto-cloud/cloudgraph/sdk/compare/0.0.1...0.1.0) (2021-08-11) 633 | 634 | 635 | ### Bug Fixes 636 | 637 | * **eslint:** Installed autocloud ESLint package ([b1c8403](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/b1c8403f9802445e62c9554fcf6bdd9ae1c86055)) 638 | * **getSchema:** update getSchema signature return string ([fc5543c](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/fc5543c8034360ade7a3165764d6d4c3955c7fa2)) 639 | * **package:** update package name with namespace, use npx to run tsc ([2bf62dd](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/2bf62dd2cfd76ec4c9ce2b32eca12eea28932e37)) 640 | 641 | 642 | ### Features 643 | 644 | * **ESLint:** rules setup ([c51662b](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/c51662bc2a0deb45421a5b60287566a318a8684c)) 645 | * **logger:** update logger class to be global singleton using DEBUG env var to deteremine log level ([80aa0d3](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/80aa0d37fd327d19b6f87b249395de71044749bd)) 646 | * **ProviderBase:** add new provider base class to be extended. Simplified integration ([aec79a1](https://gitlab.com/auto-cloud/cloudgraph/sdk/commit/aec79a1eebd110daf5527bcac35ead92ad9a35e1)) 647 | --------------------------------------------------------------------------------