Skip to content

Commit d939607

Browse files
author
Marco Franceschi
committed
feat: Migrated Policy Pack engine to SDK repo
1 parent e6bbeb3 commit d939607

13 files changed

+2663
-155
lines changed

.eslintrc.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
{
22
"extends": [
33
"@autocloud"
4-
]
4+
],
5+
"rules": {
6+
"no-plusplus": "off"
7+
}
58
}

jest.config.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/** @type {import('@ts-jest/dist/types').InitialOptionsTsJest} */
2+
module.exports = {
3+
preset: 'ts-jest',
4+
testEnvironment: 'node',
5+
testMatch: ['<rootDir>/tests/**/*.test.ts'],
6+
testPathIgnorePatterns: ['<rootDir>/lib/', '<rootDir>/node_modules/'],
7+
}

package.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@
1515
"scripts": {
1616
"build": "yarn prepublish",
1717
"prepublish": "npx tsc",
18+
"test": "NODE_ENV=test jest",
1819
"lint": "eslint --config .eslintrc.json --ext .js,.ts ./",
1920
"lint:fix": "eslint --fix --config .eslintrc.json --ext .js,.ts ./"
2021
},
2122
"author": "AutoCloud",
2223
"license": "MPL-2.0",
2324
"dependencies": {
2425
"inquirer": "^8.1.2",
26+
"jsonpath": "^1.1.1",
27+
"lodash": "^4.17.21",
2528
"ora": "^5.4.1"
2629
},
2730
"devDependencies": {
@@ -30,6 +33,7 @@
3033
"@semantic-release/git": "^10.0.0",
3134
"@semantic-release/gitlab": "^7.0.3",
3235
"@semantic-release/npm": "^7.1.3",
36+
"@types/jest": "^26.0.24",
3337
"@types/node": "^16.4.12",
3438
"@typescript-eslint/eslint-plugin": "^4.28.5",
3539
"@typescript-eslint/parser": "^4.28.5",
@@ -39,9 +43,11 @@
3943
"eslint-plugin-import": "^2.22.1",
4044
"eslint-plugin-prettier": "^3.4.0",
4145
"husky": "^4.3.0",
46+
"jest": "^27.0.6",
4247
"lint-staged": "^11.1.1",
4348
"semantic-release": "^18.0.0",
44-
"typescript": "^4.3.5"
49+
"typescript": "^4.3.5",
50+
"ts-jest": "^27.0.4"
4551
},
4652
"husky": {
4753
"hooks": {

src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import logger, { Logger } from './logger'
22
import Client from './client'
3+
import RulesEngine from './rules-engine'
34
import { Opts, Service, ServiceConnection, ProviderData } from './types'
45

56
export type { Opts, Service, ServiceConnection, Logger, Client, ProviderData }
67
export default {
78
logger,
89
Client,
10+
RulesEngine,
911
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Rule, RuleResult } from '../types'
2+
import { RuleEvaluator } from './rule-evaluator'
3+
4+
export default class DefaultEvaluator implements RuleEvaluator<Rule> {
5+
canEvaluate(/* rule: Rule */): boolean {
6+
return true
7+
}
8+
9+
async evaluateSingleResource(/* rule: Rule, data: ResourceData */): Promise<RuleResult> {
10+
// any resource captured by the query is considered as a match (and shall produce a failed finding)
11+
return RuleResult.MATCHES
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { JsRule, ResourceData, Rule, RuleResult } from '../types'
2+
import { RuleEvaluator } from './rule-evaluator'
3+
4+
export default class JsEvaluator implements RuleEvaluator<JsRule> {
5+
canEvaluate(rule: Rule | JsRule): boolean {
6+
return 'check' in rule
7+
}
8+
9+
async evaluateSingleResource(
10+
rule: JsRule,
11+
data: ResourceData
12+
): Promise<RuleResult> {
13+
return rule.check!(data) ? RuleResult.MATCHES : RuleResult.DOESNT_MATCH
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import lodash from 'lodash'
2+
import {
3+
Condition,
4+
JsonRule,
5+
Operator,
6+
ResourceData,
7+
RuleResult,
8+
_ResourceData,
9+
} from '../types'
10+
import { RuleEvaluator } from './rule-evaluator'
11+
12+
export default class JsonEvaluator implements RuleEvaluator<JsonRule> {
13+
canEvaluate(rule: JsonRule): boolean {
14+
return 'conditions' in rule
15+
}
16+
17+
async evaluateSingleResource(
18+
rule: JsonRule,
19+
data: ResourceData
20+
): Promise<RuleResult> {
21+
return this.evaluateCondition(rule.conditions, data)
22+
? RuleResult.MATCHES
23+
: RuleResult.DOESNT_MATCH
24+
}
25+
26+
calculatePath = (data: _ResourceData, _path: string) => {
27+
let path = _path
28+
if (path.indexOf('@') === 0) {
29+
// @ means the curr resource, we replace by base path
30+
path = path.replace('@', data.resourcePath).substr(2) // remove `$.`
31+
}
32+
if (path.indexOf('[*]') === 0 && data.elementPath) {
33+
// @ means the curr resource, we replace by base path
34+
path = path.replace('[*]', data.elementPath)
35+
}
36+
return path
37+
}
38+
39+
resolvePath = (data: _ResourceData, path: string): any => {
40+
return lodash.get(data.data, path)
41+
}
42+
43+
operators: { [key: string]: Operator } = {
44+
// eslint-disable-next-line eqeqeq
45+
equal: (a, b) => a == b, // == is fine
46+
notEqual: (a, b) => a !== b,
47+
in: (a, b) => (b as any[]).indexOf(a) > -1,
48+
notIn: (a, b) => (b as any[]).indexOf(a) === -1,
49+
contains: (a, b) => a.indexOf(b) > -1,
50+
doesNotContain: (a, b) => a.indexOf(b) === -1,
51+
lessThan: (a, b) => a < b,
52+
lessThanInclusive: (a, b) => a <= b,
53+
greaterThan: (a, b) => a > b,
54+
greaterThanInclusive: (a, b) => a >= b,
55+
56+
daysAgo: a =>
57+
Math.trunc((Date.now() - new Date(a).getTime()) / (60 * 60 * 1000 * 24)), // @TODO use library
58+
59+
or: (_, conditions: Condition[], data) => {
60+
for (let i = 0; i < conditions.length; i++) {
61+
// if 1 is true, it's true
62+
if (this.evaluateCondition(conditions[i], data)) return true
63+
}
64+
return false
65+
},
66+
and: (_, conditions: Condition[], data) => {
67+
for (let i = 0; i < conditions.length; i++) {
68+
// if 1 is false, it's false
69+
if (!this.evaluateCondition(conditions[i], data)) return false
70+
}
71+
return true
72+
},
73+
array_all: (array, conditions: Condition, data) => {
74+
// an AND, but with every resource item
75+
const baseElementPath = data.elementPath
76+
77+
for (let i = 0; i < array.length; i++) {
78+
if (
79+
!this.evaluateCondition(conditions, {
80+
...data,
81+
elementPath: `${baseElementPath}[${i}]`,
82+
})
83+
)
84+
return false
85+
}
86+
return true
87+
},
88+
array_any: (array, conditions, data) => {
89+
// an OR, but with every resource item
90+
91+
const baseElementPath = data.elementPath
92+
for (let i = 0; i < array.length; i++) {
93+
if (
94+
this.evaluateCondition(conditions as Condition, {
95+
...data,
96+
elementPath: `${baseElementPath}[${i}]`,
97+
})
98+
)
99+
return true
100+
}
101+
return false
102+
},
103+
}
104+
105+
isCondition = (a: unknown): boolean =>
106+
!!a && (a as any).constructor === Object
107+
108+
evaluateCondition(
109+
_condition: Condition,
110+
_data: _ResourceData
111+
): boolean | number {
112+
const condition = { ..._condition }
113+
const { path, value } = condition
114+
delete condition.path
115+
delete condition.value
116+
// remaining field should be the op name
117+
const op = Object.keys(condition)[0] //
118+
const operator = this.operators[op]
119+
const otherArgs = condition[op] // {[and]: xxx }
120+
if (!op || !operator) {
121+
throw new Error(`unrecognized operation${JSON.stringify(condition)}`)
122+
}
123+
124+
const data = { ..._data }
125+
let firstArg
126+
127+
if (path) {
128+
const elementPath = this.calculatePath(data, path)
129+
data.elementPath = elementPath
130+
firstArg = this.resolvePath(data, elementPath)
131+
} else if (this.isCondition(value)) {
132+
firstArg = this.evaluateCondition(value as any, data)
133+
} else {
134+
firstArg = value
135+
}
136+
// console.log(operator, firstArg, otherArgs, data)
137+
return operator(firstArg, otherArgs, data)
138+
}
139+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { ResourceData, Rule, RuleResult } from '../types'
2+
3+
export interface RuleEvaluator<K extends Rule> {
4+
canEvaluate: (rule: K) => boolean
5+
evaluateSingleResource: (rule: K, data: ResourceData) => Promise<RuleResult>
6+
// @TODO complex rules can take a query and return an array of resourceId + Result
7+
}

0 commit comments

Comments
 (0)