@cloud-copilot/iam-shrink 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/guarddog.yml +31 -0
- package/.github/workflows/pr-checks.yml +87 -0
- package/.github/workflows/release.yml +33 -0
- package/.vscode/settings.json +12 -0
- package/CHANGELOG.md +6 -0
- package/LICENSE.txt +68 -81
- package/README.md +45 -35
- package/dist/cjs/cli.d.ts +1 -0
- package/dist/cjs/cli.js +38 -35
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/cli_utils.d.ts +3 -15
- package/dist/cjs/cli_utils.d.ts.map +1 -1
- package/dist/cjs/cli_utils.js +9 -42
- package/dist/cjs/cli_utils.js.map +1 -1
- package/dist/cjs/errors.d.ts.map +1 -1
- package/dist/cjs/errors.js +3 -3
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/shrink.d.ts.map +1 -1
- package/dist/cjs/shrink.js +28 -29
- package/dist/cjs/shrink.js.map +1 -1
- package/dist/cjs/shrink_file.d.ts +1 -1
- package/dist/cjs/shrink_file.d.ts.map +1 -1
- package/dist/cjs/shrink_file.js.map +1 -1
- package/dist/cjs/validate.d.ts.map +1 -1
- package/dist/cjs/validate.js +1 -1
- package/dist/cjs/validate.js.map +1 -1
- package/dist/esm/cli.d.ts +1 -0
- package/dist/esm/cli.js +41 -38
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/cli_utils.d.ts +3 -15
- package/dist/esm/cli_utils.d.ts.map +1 -1
- package/dist/esm/cli_utils.js +9 -41
- package/dist/esm/cli_utils.js.map +1 -1
- package/dist/esm/errors.d.ts.map +1 -1
- package/dist/esm/errors.js +3 -3
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/shrink.d.ts.map +1 -1
- package/dist/esm/shrink.js +28 -29
- package/dist/esm/shrink.js.map +1 -1
- package/dist/esm/shrink_file.d.ts +1 -1
- package/dist/esm/shrink_file.d.ts.map +1 -1
- package/dist/esm/shrink_file.js +1 -1
- package/dist/esm/shrink_file.js.map +1 -1
- package/dist/esm/validate.d.ts.map +1 -1
- package/dist/esm/validate.js +2 -2
- package/dist/esm/validate.js.map +1 -1
- package/package.json +72 -3
- package/src/cli.ts +56 -46
- package/src/cli_utils.test.ts +20 -58
- package/src/cli_utils.ts +21 -55
- package/src/errors.ts +14 -10
- package/src/index.ts +3 -4
- package/src/shrink.test.ts +270 -270
- package/src/shrink.ts +166 -134
- package/src/shrink_file.test.ts +4 -4
- package/src/shrink_file.ts +14 -10
- package/src/validate.test.ts +19 -21
- package/src/validate.ts +15 -12
- package/dist/cjs/stdin.d.ts +0 -7
- package/dist/cjs/stdin.d.ts.map +0 -1
- package/dist/cjs/stdin.js +0 -36
- package/dist/cjs/stdin.js.map +0 -1
- package/dist/esm/stdin.d.ts +0 -7
- package/dist/esm/stdin.d.ts.map +0 -1
- package/dist/esm/stdin.js +0 -33
- package/dist/esm/stdin.js.map +0 -1
- package/src/stdin.ts +0 -36
package/src/shrink.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { expandIamActions } from '@cloud-copilot/iam-expand'
|
|
2
|
-
import { ShrinkValidationError } from './errors.js'
|
|
3
|
-
import { validateShrinkResults } from './validate.js'
|
|
1
|
+
import { expandIamActions } from '@cloud-copilot/iam-expand'
|
|
2
|
+
import { ShrinkValidationError } from './errors.js'
|
|
3
|
+
import { validateShrinkResults } from './validate.js'
|
|
4
4
|
|
|
5
5
|
export interface ShrinkOptions {
|
|
6
6
|
iterations: number
|
|
@@ -23,34 +23,46 @@ const defaultOptions: ShrinkOptions = {
|
|
|
23
23
|
* @param iterations the number of iterations to run the shrink operations
|
|
24
24
|
* @returns the smallest list of patterns that will match only the actions specified by desiredPatterns and not match any of the excludedPatterns or any actions not specified by desiredPatterns.
|
|
25
25
|
*/
|
|
26
|
-
export async function shrink(
|
|
26
|
+
export async function shrink(
|
|
27
|
+
desiredPatterns: string[],
|
|
28
|
+
shrinkOptions?: Partial<ShrinkOptions>
|
|
29
|
+
): Promise<string[]> {
|
|
27
30
|
//Check for an all actions wildcard
|
|
28
|
-
const wildCard = desiredPatterns.find(pattern => collapseAsterisks(pattern) === '*')
|
|
29
|
-
if(wildCard) {
|
|
30
|
-
return [
|
|
31
|
+
const wildCard = desiredPatterns.find((pattern) => collapseAsterisks(pattern) === '*')
|
|
32
|
+
if (wildCard) {
|
|
33
|
+
return ['*']
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
const options = {...defaultOptions, ...shrinkOptions}
|
|
34
|
-
const targetActions = await expandIamActions(desiredPatterns
|
|
35
|
-
const expandedActionsByService = groupActionsByService(targetActions)
|
|
36
|
-
const services = Array.from(expandedActionsByService.keys()).sort()
|
|
36
|
+
const options = { ...defaultOptions, ...shrinkOptions }
|
|
37
|
+
const targetActions = await expandIamActions(desiredPatterns)
|
|
38
|
+
const expandedActionsByService = groupActionsByService(targetActions)
|
|
39
|
+
const services = Array.from(expandedActionsByService.keys()).sort()
|
|
37
40
|
|
|
38
|
-
const reducedActions: string[] = []
|
|
39
|
-
for(const service of services) {
|
|
41
|
+
const reducedActions: string[] = []
|
|
42
|
+
for (const service of services) {
|
|
40
43
|
const desiredActions = expandedActionsByService.get(service)!
|
|
41
|
-
const possibleActions = mapActions(await expandIamActions(`${service}
|
|
42
|
-
const reducedServiceActions = shrinkResolvedList(
|
|
44
|
+
const possibleActions = mapActions(await expandIamActions(`${service}:*`))
|
|
45
|
+
const reducedServiceActions = shrinkResolvedList(
|
|
46
|
+
desiredActions.withoutService,
|
|
47
|
+
possibleActions,
|
|
48
|
+
options.iterations
|
|
49
|
+
)
|
|
43
50
|
|
|
44
51
|
//Validation
|
|
45
|
-
const reducedServiceActionsWithService = reducedServiceActions.map(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
const reducedServiceActionsWithService = reducedServiceActions.map(
|
|
53
|
+
(action) => `${service}:${action}`
|
|
54
|
+
)
|
|
55
|
+
const invalidMatch = await validateShrinkResults(
|
|
56
|
+
desiredActions.withService,
|
|
57
|
+
reducedServiceActionsWithService
|
|
58
|
+
)
|
|
59
|
+
if (invalidMatch) {
|
|
60
|
+
throw new ShrinkValidationError(desiredPatterns, invalidMatch)
|
|
49
61
|
}
|
|
50
62
|
reducedActions.push(...reducedServiceActionsWithService)
|
|
51
63
|
}
|
|
52
64
|
|
|
53
|
-
return reducedActions
|
|
65
|
+
return reducedActions
|
|
54
66
|
}
|
|
55
67
|
|
|
56
68
|
/**
|
|
@@ -60,7 +72,7 @@ export async function shrink(desiredPatterns: string[], shrinkOptions?: Partial<
|
|
|
60
72
|
* @returns an array of just the action strings such as ['GetObject', 'DescribeInstances']
|
|
61
73
|
*/
|
|
62
74
|
export function mapActions(actions: string[]): string[] {
|
|
63
|
-
return actions.map(action => action.split(
|
|
75
|
+
return actions.map((action) => action.split(':')[1])
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
/**
|
|
@@ -73,18 +85,19 @@ export function mapActions(actions: string[]): string[] {
|
|
|
73
85
|
* @param actions the array of service:action strings such as ['s3:GetObject', 'ec2:DescribeInstances']
|
|
74
86
|
* @returns a map of service to an object with two arrays: withService and withoutService
|
|
75
87
|
*/
|
|
76
|
-
export function groupActionsByService(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
export function groupActionsByService(
|
|
89
|
+
actions: string[]
|
|
90
|
+
): Map<string, { withService: string[]; withoutService: string[] }> {
|
|
91
|
+
const serviceMap = new Map<string, { withService: string[]; withoutService: string[] }>()
|
|
92
|
+
actions.forEach((actionString) => {
|
|
93
|
+
const [service, action] = actionString.split(':')
|
|
80
94
|
if (!serviceMap.has(service)) {
|
|
81
|
-
serviceMap.set(service, {withService: [], withoutService: []})
|
|
95
|
+
serviceMap.set(service, { withService: [], withoutService: [] })
|
|
82
96
|
}
|
|
83
|
-
serviceMap.get(service)!.withService.push(actionString)
|
|
84
|
-
serviceMap.get(service)!.withoutService.push(action)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return serviceMap;
|
|
97
|
+
serviceMap.get(service)!.withService.push(actionString)
|
|
98
|
+
serviceMap.get(service)!.withoutService.push(action)
|
|
99
|
+
})
|
|
100
|
+
return serviceMap
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
/**
|
|
@@ -96,40 +109,43 @@ export function groupActionsByService(actions: string[]): Map<string, {withServi
|
|
|
96
109
|
* @param iterations the number of iterations to run the shrink operations
|
|
97
110
|
* @returns the smallest list of patterns that when compared to possibleActions will match only the desiredActions and no others
|
|
98
111
|
*/
|
|
99
|
-
export function shrinkResolvedList(
|
|
112
|
+
export function shrinkResolvedList(
|
|
113
|
+
desiredActions: string[],
|
|
114
|
+
possibleActions: string[],
|
|
115
|
+
iterations: number
|
|
116
|
+
): string[] {
|
|
100
117
|
const desiredActionSet = new Set(desiredActions)
|
|
101
|
-
const undesiredActions = possibleActions.filter(action => !desiredActionSet.has(action))
|
|
118
|
+
const undesiredActions = possibleActions.filter((action) => !desiredActionSet.has(action))
|
|
102
119
|
|
|
103
|
-
if(undesiredActions.length === 0) {
|
|
120
|
+
if (undesiredActions.length === 0) {
|
|
104
121
|
// If there are no undesired actions, that means we want all actions
|
|
105
|
-
return [
|
|
122
|
+
return ['*']
|
|
106
123
|
}
|
|
107
124
|
|
|
108
125
|
// Iteratively shrink based on the most commmon sequence until we can't shrink anymore
|
|
109
|
-
let previousActionListLength = desiredActions.length
|
|
126
|
+
let previousActionListLength = desiredActions.length
|
|
110
127
|
let actionList = desiredActions.slice()
|
|
111
128
|
|
|
112
129
|
do {
|
|
113
|
-
previousActionListLength = actionList.length
|
|
114
|
-
actionList = shrinkIteration(actionList, undesiredActions, false)
|
|
130
|
+
previousActionListLength = actionList.length
|
|
131
|
+
actionList = shrinkIteration(actionList, undesiredActions, false)
|
|
115
132
|
iterations = iterations - 1
|
|
116
|
-
if(iterations <= 0) {
|
|
133
|
+
if (iterations <= 0) {
|
|
117
134
|
return actionList
|
|
118
135
|
}
|
|
119
|
-
} while (actionList.length < previousActionListLength)
|
|
120
|
-
|
|
136
|
+
} while (actionList.length < previousActionListLength)
|
|
121
137
|
|
|
122
138
|
// Iteratively shrink based on all common sequences until we can't shrink anymore
|
|
123
139
|
do {
|
|
124
|
-
previousActionListLength = actionList.length
|
|
125
|
-
actionList = shrinkIteration(actionList, undesiredActions, true)
|
|
140
|
+
previousActionListLength = actionList.length
|
|
141
|
+
actionList = shrinkIteration(actionList, undesiredActions, true)
|
|
126
142
|
iterations = iterations - 1
|
|
127
|
-
if(iterations <= 0) {
|
|
143
|
+
if (iterations <= 0) {
|
|
128
144
|
return actionList
|
|
129
145
|
}
|
|
130
|
-
} while (actionList.length < previousActionListLength)
|
|
146
|
+
} while (actionList.length < previousActionListLength)
|
|
131
147
|
|
|
132
|
-
return actionList
|
|
148
|
+
return actionList
|
|
133
149
|
}
|
|
134
150
|
|
|
135
151
|
/**
|
|
@@ -140,25 +156,33 @@ export function shrinkResolvedList(desiredActions: string[], possibleActions: st
|
|
|
140
156
|
* @param deep if true, will shrink based on all common sequences, otherwise will only shrink based on the most common sequence
|
|
141
157
|
* @returns the smallest list of actions that will match only the desiredActions and not match any of the undesiredActions or any actions not specified by desiredActions.
|
|
142
158
|
*/
|
|
143
|
-
export function shrinkIteration(
|
|
159
|
+
export function shrinkIteration(
|
|
160
|
+
desiredActions: string[],
|
|
161
|
+
undesiredActions: string[],
|
|
162
|
+
deep: boolean
|
|
163
|
+
): string[] {
|
|
144
164
|
// Find all common words in the strings in the desiredActions array
|
|
145
|
-
const commonSequences = findCommonSequences(desiredActions).filter(
|
|
165
|
+
const commonSequences = findCommonSequences(desiredActions).filter(
|
|
166
|
+
(sequence) => sequence.sequence != '*'
|
|
167
|
+
)
|
|
146
168
|
commonSequences.sort((a, b) => {
|
|
147
|
-
return b.frequency - a.frequency
|
|
148
|
-
})
|
|
169
|
+
return b.frequency - a.frequency
|
|
170
|
+
})
|
|
149
171
|
|
|
150
|
-
const sequencesToProcess = deep ? commonSequences : commonSequences.slice(0, 1)
|
|
172
|
+
const sequencesToProcess = deep ? commonSequences : commonSequences.slice(0, 1)
|
|
151
173
|
|
|
152
174
|
// Reduce the actions based on the common sequences
|
|
153
175
|
let reducedActions = desiredActions
|
|
154
|
-
for(const sequence of sequencesToProcess) {
|
|
176
|
+
for (const sequence of sequencesToProcess) {
|
|
155
177
|
const reducedIteration = Array.from(
|
|
156
|
-
new Set(
|
|
157
|
-
|
|
158
|
-
|
|
178
|
+
new Set(
|
|
179
|
+
reducedActions.map((action) => reduceAction(action, sequence.sequence, undesiredActions))
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
reducedActions = consolidateWildcardPatterns(reducedIteration)
|
|
159
183
|
}
|
|
160
184
|
|
|
161
|
-
return reducedActions
|
|
185
|
+
return reducedActions
|
|
162
186
|
}
|
|
163
187
|
|
|
164
188
|
/**
|
|
@@ -170,75 +194,78 @@ export function shrinkIteration(desiredActions: string[], undesiredActions: stri
|
|
|
170
194
|
* @param undesiredActions the list of actions that should not match the reduced action
|
|
171
195
|
* @returns the reduced action with as many parts replaced with asterisks as possible while still matching the desired actions and not matching any of the undesired actions
|
|
172
196
|
*/
|
|
173
|
-
export function reduceAction(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
197
|
+
export function reduceAction(
|
|
198
|
+
desiredAction: string,
|
|
199
|
+
sequence: string,
|
|
200
|
+
undesiredActions: string[]
|
|
201
|
+
): string {
|
|
202
|
+
const testArray = splitActionIntoParts(desiredAction)
|
|
203
|
+
if (testArray.length === 1) {
|
|
204
|
+
return desiredAction
|
|
177
205
|
}
|
|
178
|
-
const indexOfSequence = testArray.indexOf(sequence)
|
|
179
|
-
let shorterValue = desiredAction
|
|
206
|
+
const indexOfSequence = testArray.indexOf(sequence)
|
|
207
|
+
let shorterValue = desiredAction
|
|
180
208
|
|
|
181
|
-
if(indexOfSequence === 0) {
|
|
182
|
-
const tempArray = testArray.slice()
|
|
209
|
+
if (indexOfSequence === 0) {
|
|
210
|
+
const tempArray = testArray.slice()
|
|
183
211
|
//Iterate though ever following element and see if replacing the sequence with the first common sequence results in a failure
|
|
184
|
-
for(let i = 1; i < testArray.length; i++) {
|
|
185
|
-
tempArray[i] =
|
|
186
|
-
const tempString = collapseAsterisks(tempArray.join(
|
|
187
|
-
const problemMatch = wildcardActionMatchesAnyString(tempString, undesiredActions)
|
|
188
|
-
if(problemMatch) {
|
|
212
|
+
for (let i = 1; i < testArray.length; i++) {
|
|
213
|
+
tempArray[i] = '*'
|
|
214
|
+
const tempString = collapseAsterisks(tempArray.join(''))
|
|
215
|
+
const problemMatch = wildcardActionMatchesAnyString(tempString, undesiredActions)
|
|
216
|
+
if (problemMatch) {
|
|
189
217
|
// Stopping here seems to work the best
|
|
190
|
-
break
|
|
218
|
+
break
|
|
191
219
|
}
|
|
192
|
-
shorterValue = tempString
|
|
220
|
+
shorterValue = tempString
|
|
193
221
|
}
|
|
194
222
|
|
|
195
223
|
//its at the beginning
|
|
196
|
-
} else if(indexOfSequence === testArray.length - 1) {
|
|
224
|
+
} else if (indexOfSequence === testArray.length - 1) {
|
|
197
225
|
//its at the end
|
|
198
|
-
const tempArray = testArray.slice()
|
|
226
|
+
const tempArray = testArray.slice()
|
|
199
227
|
//Iterate through the array backwards and see if replace the items with * results in a failure
|
|
200
|
-
for(let i = testArray.length - 2; i >= 0; i--) {
|
|
201
|
-
tempArray[i] =
|
|
202
|
-
const tempString = collapseAsterisks(tempArray.join(
|
|
203
|
-
const problemMatch = wildcardActionMatchesAnyString(tempString, undesiredActions)
|
|
204
|
-
if(problemMatch) {
|
|
228
|
+
for (let i = testArray.length - 2; i >= 0; i--) {
|
|
229
|
+
tempArray[i] = '*'
|
|
230
|
+
const tempString = collapseAsterisks(tempArray.join(''))
|
|
231
|
+
const problemMatch = wildcardActionMatchesAnyString(tempString, undesiredActions)
|
|
232
|
+
if (problemMatch) {
|
|
205
233
|
// Stopping here seems to work the best
|
|
206
|
-
break
|
|
234
|
+
break
|
|
207
235
|
}
|
|
208
236
|
|
|
209
|
-
shorterValue = tempString
|
|
237
|
+
shorterValue = tempString
|
|
210
238
|
}
|
|
211
|
-
} else if(indexOfSequence > 0) {
|
|
239
|
+
} else if (indexOfSequence > 0) {
|
|
212
240
|
//its in the middle
|
|
213
|
-
const tempArray = testArray.slice()
|
|
241
|
+
const tempArray = testArray.slice()
|
|
214
242
|
//Iterate forward through the array and see if replacing the items with * results in a failure
|
|
215
|
-
for(let i = indexOfSequence + 1; i < testArray.length; i++) {
|
|
216
|
-
tempArray[i] =
|
|
217
|
-
const tempString = collapseAsterisks(tempArray.join(
|
|
218
|
-
const problemMatch = wildcardActionMatchesAnyString(tempString, undesiredActions)
|
|
219
|
-
if(problemMatch) {
|
|
243
|
+
for (let i = indexOfSequence + 1; i < testArray.length; i++) {
|
|
244
|
+
tempArray[i] = '*'
|
|
245
|
+
const tempString = collapseAsterisks(tempArray.join(''))
|
|
246
|
+
const problemMatch = wildcardActionMatchesAnyString(tempString, undesiredActions)
|
|
247
|
+
if (problemMatch) {
|
|
220
248
|
//This replacement cased a prolem match, so revert it before going backwards in the strings
|
|
221
249
|
tempArray[i] = testArray[i]
|
|
222
250
|
// Stopping here seems to work the best
|
|
223
|
-
break
|
|
251
|
+
break
|
|
224
252
|
}
|
|
225
|
-
shorterValue = tempString
|
|
253
|
+
shorterValue = tempString
|
|
226
254
|
}
|
|
227
255
|
//Iterate through the array backwards and see if replace the items with * results in a failure
|
|
228
|
-
for(let i = indexOfSequence - 1; i >= 0; i--) {
|
|
229
|
-
tempArray[i] =
|
|
230
|
-
const tempString = collapseAsterisks(tempArray.join(
|
|
231
|
-
const problemMatch = wildcardActionMatchesAnyString(tempString, undesiredActions)
|
|
232
|
-
if(problemMatch) {
|
|
256
|
+
for (let i = indexOfSequence - 1; i >= 0; i--) {
|
|
257
|
+
tempArray[i] = '*'
|
|
258
|
+
const tempString = collapseAsterisks(tempArray.join(''))
|
|
259
|
+
const problemMatch = wildcardActionMatchesAnyString(tempString, undesiredActions)
|
|
260
|
+
if (problemMatch) {
|
|
233
261
|
// Stopping here seems to work the best
|
|
234
|
-
break
|
|
262
|
+
break
|
|
235
263
|
}
|
|
236
|
-
shorterValue = tempString
|
|
264
|
+
shorterValue = tempString
|
|
237
265
|
}
|
|
238
|
-
|
|
239
266
|
}
|
|
240
267
|
|
|
241
|
-
return shorterValue
|
|
268
|
+
return shorterValue
|
|
242
269
|
}
|
|
243
270
|
|
|
244
271
|
/**
|
|
@@ -258,8 +285,8 @@ export function collapseAsterisks(wildcardAction: string): string {
|
|
|
258
285
|
* @returns a regular expression that will match the wildcard action
|
|
259
286
|
*/
|
|
260
287
|
export function regexForWildcardAction(wildcardAction: string): RegExp {
|
|
261
|
-
wildcardAction = collapseAsterisks(wildcardAction)
|
|
262
|
-
const pattern =
|
|
288
|
+
wildcardAction = collapseAsterisks(wildcardAction)
|
|
289
|
+
const pattern = '^' + wildcardAction.replace(/\*/g, '.*?') + '$'
|
|
263
290
|
return new RegExp(pattern, 'i')
|
|
264
291
|
}
|
|
265
292
|
|
|
@@ -274,10 +301,10 @@ export function wildcardActionMatchesAnyString(wildcardAction: string, strings:
|
|
|
274
301
|
const regex = regexForWildcardAction(wildcardAction)
|
|
275
302
|
for (const string of strings) {
|
|
276
303
|
if (regex.test(string)) {
|
|
277
|
-
return true
|
|
304
|
+
return true
|
|
278
305
|
}
|
|
279
306
|
}
|
|
280
|
-
return false
|
|
307
|
+
return false
|
|
281
308
|
}
|
|
282
309
|
|
|
283
310
|
/**
|
|
@@ -294,10 +321,9 @@ export function splitActionIntoParts(input: string): string[] {
|
|
|
294
321
|
// Split the string using a regex that finds transitions from lower to upper case or asterisks
|
|
295
322
|
// and keeps sequences of uppercase letters together
|
|
296
323
|
// return input.split(/(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/);
|
|
297
|
-
return input.split(/(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?=[*])|(?<=[*])/)
|
|
324
|
+
return input.split(/(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?=[*])|(?<=[*])/)
|
|
298
325
|
}
|
|
299
326
|
|
|
300
|
-
|
|
301
327
|
/**
|
|
302
328
|
* Given a list of strings and a list of strings those parts are in, count the number of times each part appears in the strings
|
|
303
329
|
*
|
|
@@ -306,19 +332,19 @@ export function splitActionIntoParts(input: string): string[] {
|
|
|
306
332
|
* @returns Returns a map of the substring to the number of times it appears in the actions
|
|
307
333
|
*/
|
|
308
334
|
export function countSubstrings(substrings: string[], actions: string[]): Map<string, number> {
|
|
309
|
-
const substringCount = new Map<string, number>()
|
|
310
|
-
substrings.forEach(substring => {
|
|
311
|
-
let count = 0
|
|
312
|
-
actions.forEach(action => {
|
|
335
|
+
const substringCount = new Map<string, number>()
|
|
336
|
+
substrings.forEach((substring) => {
|
|
337
|
+
let count = 0
|
|
338
|
+
actions.forEach((action) => {
|
|
313
339
|
if (action.includes(substring)) {
|
|
314
|
-
count
|
|
340
|
+
count++
|
|
315
341
|
}
|
|
316
|
-
})
|
|
342
|
+
})
|
|
317
343
|
if (count > 0) {
|
|
318
|
-
substringCount.set(substring, count)
|
|
344
|
+
substringCount.set(substring, count)
|
|
319
345
|
}
|
|
320
|
-
})
|
|
321
|
-
return substringCount
|
|
346
|
+
})
|
|
347
|
+
return substringCount
|
|
322
348
|
}
|
|
323
349
|
|
|
324
350
|
/**
|
|
@@ -327,20 +353,22 @@ export function countSubstrings(substrings: string[], actions: string[]): Map<st
|
|
|
327
353
|
* @param actions the list of actions to find common sequences in
|
|
328
354
|
* @returns an array of objects with the sequence, frequency, and length of the common sequences
|
|
329
355
|
*/
|
|
330
|
-
export function findCommonSequences(
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
356
|
+
export function findCommonSequences(
|
|
357
|
+
actions: string[]
|
|
358
|
+
): { sequence: string; frequency: number; length: number }[] {
|
|
359
|
+
const allSubstrings = new Set<string>()
|
|
360
|
+
actions.forEach((action) => {
|
|
361
|
+
splitActionIntoParts(action).forEach((substring) => allSubstrings.add(substring))
|
|
362
|
+
})
|
|
335
363
|
|
|
336
|
-
const substringCount = countSubstrings(Array.from(allSubstrings), actions)
|
|
364
|
+
const substringCount = countSubstrings(Array.from(allSubstrings), actions)
|
|
337
365
|
|
|
338
|
-
const result: any[] = []
|
|
366
|
+
const result: any[] = []
|
|
339
367
|
substringCount.forEach((frequency, sequence) => {
|
|
340
|
-
result.push({ sequence, frequency, length: sequence.length })
|
|
341
|
-
})
|
|
368
|
+
result.push({ sequence, frequency, length: sequence.length })
|
|
369
|
+
})
|
|
342
370
|
|
|
343
|
-
return result
|
|
371
|
+
return result
|
|
344
372
|
}
|
|
345
373
|
|
|
346
374
|
/**
|
|
@@ -355,20 +383,24 @@ export function findCommonSequences(actions: string[]): { sequence: string, freq
|
|
|
355
383
|
*/
|
|
356
384
|
export function consolidateWildcardPatterns(patterns: string[]): string[] {
|
|
357
385
|
// Sort patterns to handle simpler cases first
|
|
358
|
-
patterns.sort((a, b) => b.length - a.length)
|
|
386
|
+
patterns.sort((a, b) => b.length - a.length)
|
|
359
387
|
|
|
360
|
-
let consolidatedPatterns: string[] = []
|
|
361
|
-
for(const pattern of patterns) {
|
|
388
|
+
let consolidatedPatterns: string[] = []
|
|
389
|
+
for (const pattern of patterns) {
|
|
362
390
|
//If it's already covered, skip it
|
|
363
|
-
const coveredByExistingPattern = consolidatedPatterns.some(consolidated =>
|
|
364
|
-
|
|
365
|
-
|
|
391
|
+
const coveredByExistingPattern = consolidatedPatterns.some((consolidated) =>
|
|
392
|
+
matchesPattern(consolidated, pattern)
|
|
393
|
+
)
|
|
394
|
+
if (coveredByExistingPattern) {
|
|
395
|
+
continue
|
|
366
396
|
}
|
|
367
397
|
|
|
368
398
|
//If it subsumes any existing patterns, remove them
|
|
369
|
-
consolidatedPatterns = consolidatedPatterns.filter(
|
|
399
|
+
consolidatedPatterns = consolidatedPatterns.filter(
|
|
400
|
+
(consolidated) => !matchesPattern(pattern, consolidated)
|
|
401
|
+
)
|
|
370
402
|
|
|
371
|
-
consolidatedPatterns.push(pattern)
|
|
403
|
+
consolidatedPatterns.push(pattern)
|
|
372
404
|
}
|
|
373
405
|
return consolidatedPatterns
|
|
374
406
|
}
|
|
@@ -380,6 +412,6 @@ export function consolidateWildcardPatterns(patterns: string[]): string[] {
|
|
|
380
412
|
* @returns true if the specific string matches the general pattern
|
|
381
413
|
*/
|
|
382
414
|
function matchesPattern(general: string, specific: string): boolean {
|
|
383
|
-
const regex = new RegExp(
|
|
384
|
-
return regex.test(specific)
|
|
385
|
-
}
|
|
415
|
+
const regex = new RegExp('^' + general.replace(/\*/g, '.*') + '$')
|
|
416
|
+
return regex.test(specific)
|
|
417
|
+
}
|
package/src/shrink_file.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { beforeEach } from
|
|
2
|
-
import { describe, expect, it, vi } from
|
|
3
|
-
import { shrink } from './shrink.js'
|
|
4
|
-
import { shrinkJsonDocument } from
|
|
1
|
+
import { beforeEach } from 'node:test'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { shrink } from './shrink.js'
|
|
4
|
+
import { shrinkJsonDocument } from './shrink_file.js'
|
|
5
5
|
|
|
6
6
|
vi.mock('./shrink.js')
|
|
7
7
|
|
package/src/shrink_file.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ShrinkOptions, shrink } from
|
|
1
|
+
import { ShrinkOptions, shrink } from './shrink.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Takes any JSON document and shrinks any Action or NotAction array of strings in the document.
|
|
@@ -9,30 +9,34 @@ import { ShrinkOptions, shrink } from "./shrink.js";
|
|
|
9
9
|
* @param key the key of the current node in the document
|
|
10
10
|
* @returns the original JSON document with any actions expanded in place
|
|
11
11
|
*/
|
|
12
|
-
export async function shrinkJsonDocument(
|
|
12
|
+
export async function shrinkJsonDocument(
|
|
13
|
+
options: Partial<ShrinkOptions>,
|
|
14
|
+
document: any,
|
|
15
|
+
key?: string
|
|
16
|
+
): Promise<any> {
|
|
13
17
|
if (key === 'Action' || key === 'NotAction') {
|
|
14
18
|
// if (typeof document === 'string') {
|
|
15
19
|
// // return shrink([document], options);
|
|
16
20
|
// }
|
|
17
21
|
if (Array.isArray(document) && document.length > 0 && typeof document[0] === 'string') {
|
|
18
|
-
return shrink(document, options)
|
|
22
|
+
return shrink(document, options)
|
|
19
23
|
}
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
if (Array.isArray(document)) {
|
|
23
|
-
const results = []
|
|
27
|
+
const results = []
|
|
24
28
|
for (const item of document) {
|
|
25
|
-
results.push(await shrinkJsonDocument(options, item))
|
|
29
|
+
results.push(await shrinkJsonDocument(options, item))
|
|
26
30
|
}
|
|
27
|
-
return results
|
|
31
|
+
return results
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
if (typeof document === 'object' && document !== null) {
|
|
31
35
|
for (const key of Object.keys(document)) {
|
|
32
|
-
document[key] = await shrinkJsonDocument(options, document[key], key)
|
|
36
|
+
document[key] = await shrinkJsonDocument(options, document[key], key)
|
|
33
37
|
}
|
|
34
|
-
return document
|
|
38
|
+
return document
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
return document
|
|
38
|
-
}
|
|
41
|
+
return document
|
|
42
|
+
}
|
package/src/validate.test.ts
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
|
-
import { expandIamActions } from '@cloud-copilot/iam-expand'
|
|
2
|
-
import { describe, expect, it, vi } from
|
|
3
|
-
import { validateShrinkResults } from './validate.js'
|
|
1
|
+
import { expandIamActions } from '@cloud-copilot/iam-expand'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { validateShrinkResults } from './validate.js'
|
|
4
4
|
vi.mock('@cloud-copilot/iam-expand')
|
|
5
5
|
|
|
6
|
-
const mockExpandIamActions = vi.mocked(expandIamActions)
|
|
6
|
+
const mockExpandIamActions = vi.mocked(expandIamActions)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
describe("validate", () => {
|
|
8
|
+
describe('validate', () => {
|
|
10
9
|
it('should return nothing if the actions are valid', async () => {
|
|
11
10
|
// Given a list of desired actions
|
|
12
|
-
const desiredActions = ['s3:GetObject', 's3:PutObject', 's3:DeleteObject']
|
|
11
|
+
const desiredActions = ['s3:GetObject', 's3:PutObject', 's3:DeleteObject']
|
|
13
12
|
// And a list of patterns
|
|
14
|
-
const patterns = ['s3:*Object']
|
|
13
|
+
const patterns = ['s3:*Object']
|
|
15
14
|
// And the patterns include an undesired action
|
|
16
|
-
mockExpandIamActions.mockResolvedValue(['s3:GetObject', 's3:PutObject', 's3:DeleteObject'])
|
|
15
|
+
mockExpandIamActions.mockResolvedValue(['s3:GetObject', 's3:PutObject', 's3:DeleteObject'])
|
|
17
16
|
|
|
18
17
|
// When the patterns are validated
|
|
19
|
-
const result = await validateShrinkResults(desiredActions, patterns)
|
|
18
|
+
const result = await validateShrinkResults(desiredActions, patterns)
|
|
20
19
|
|
|
21
20
|
// Then the validation should fail
|
|
22
21
|
expect(result).toBeUndefined()
|
|
@@ -24,32 +23,31 @@ describe("validate", () => {
|
|
|
24
23
|
|
|
25
24
|
it('should find an undesired action', async () => {
|
|
26
25
|
// Given a list of desired actions
|
|
27
|
-
const desiredActions = ['s3:GetObject', 's3:PutObject']
|
|
26
|
+
const desiredActions = ['s3:GetObject', 's3:PutObject']
|
|
28
27
|
// And a list of patterns
|
|
29
|
-
const patterns = ['s3:*Object']
|
|
28
|
+
const patterns = ['s3:*Object']
|
|
30
29
|
// And the patterns include an undesired action
|
|
31
|
-
mockExpandIamActions.mockResolvedValue(['s3:GetObject', 's3:PutObject', 's3:DeleteObject'])
|
|
30
|
+
mockExpandIamActions.mockResolvedValue(['s3:GetObject', 's3:PutObject', 's3:DeleteObject'])
|
|
32
31
|
|
|
33
32
|
// When the patterns are validated
|
|
34
|
-
const result = await validateShrinkResults(desiredActions, patterns)
|
|
33
|
+
const result = await validateShrinkResults(desiredActions, patterns)
|
|
35
34
|
|
|
36
35
|
// Then the validation should fail
|
|
37
|
-
expect(result).toEqual('Undesired action: s3:DeleteObject')
|
|
36
|
+
expect(result).toEqual('Undesired action: s3:DeleteObject')
|
|
38
37
|
})
|
|
39
38
|
|
|
40
39
|
it('should find a missing action', async () => {
|
|
41
40
|
// Given a list of desired actions
|
|
42
|
-
const desiredActions = ['s3:GetObject', 's3:PutObject']
|
|
41
|
+
const desiredActions = ['s3:GetObject', 's3:PutObject']
|
|
43
42
|
// And a list of patterns
|
|
44
|
-
const patterns = ['s3:Get*']
|
|
43
|
+
const patterns = ['s3:Get*']
|
|
45
44
|
// And the patterns are missing an action
|
|
46
|
-
mockExpandIamActions.mockResolvedValue(['s3:GetObject'])
|
|
45
|
+
mockExpandIamActions.mockResolvedValue(['s3:GetObject'])
|
|
47
46
|
|
|
48
47
|
// When the patterns are validated
|
|
49
|
-
const result = await validateShrinkResults(desiredActions, patterns)
|
|
48
|
+
const result = await validateShrinkResults(desiredActions, patterns)
|
|
50
49
|
|
|
51
50
|
// Then the validation should fail
|
|
52
|
-
expect(result).toEqual('Missing action s3:PutObject')
|
|
51
|
+
expect(result).toEqual('Missing action s3:PutObject')
|
|
53
52
|
})
|
|
54
|
-
|
|
55
53
|
})
|
package/src/validate.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { expandIamActions } from
|
|
1
|
+
import { expandIamActions } from '@cloud-copilot/iam-expand'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Checks a list of patterns against a list of desired actions to validate:
|
|
@@ -9,21 +9,24 @@ import { expandIamActions } from "@cloud-copilot/iam-expand";
|
|
|
9
9
|
* @param patterns The list of patterns that the algorithm has derived
|
|
10
10
|
* @returns the first match error if any, otherwise undefined
|
|
11
11
|
*/
|
|
12
|
-
export async function validateShrinkResults(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
export async function validateShrinkResults(
|
|
13
|
+
desiredActions: string[],
|
|
14
|
+
patterns: string[]
|
|
15
|
+
): Promise<string | undefined> {
|
|
16
|
+
const desiredActionSet = new Set(desiredActions)
|
|
17
|
+
const expandedAfterActions = await expandIamActions(patterns)
|
|
18
|
+
const expandedAfterActionSet = new Set(expandedAfterActions)
|
|
19
|
+
for (const afterAction of expandedAfterActions) {
|
|
20
|
+
if (!desiredActionSet.has(afterAction)) {
|
|
21
|
+
return `Undesired action: ${afterAction}`
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
for(const desiredAction of desiredActions) {
|
|
23
|
-
if(!expandedAfterActionSet.has(desiredAction)) {
|
|
25
|
+
for (const desiredAction of desiredActions) {
|
|
26
|
+
if (!expandedAfterActionSet.has(desiredAction)) {
|
|
24
27
|
return `Missing action ${desiredAction}`
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
return undefined
|
|
29
|
-
}
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
package/dist/cjs/stdin.d.ts
DELETED