@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.
Files changed (71) hide show
  1. package/.github/workflows/guarddog.yml +31 -0
  2. package/.github/workflows/pr-checks.yml +87 -0
  3. package/.github/workflows/release.yml +33 -0
  4. package/.vscode/settings.json +12 -0
  5. package/CHANGELOG.md +6 -0
  6. package/LICENSE.txt +68 -81
  7. package/README.md +45 -35
  8. package/dist/cjs/cli.d.ts +1 -0
  9. package/dist/cjs/cli.js +38 -35
  10. package/dist/cjs/cli.js.map +1 -1
  11. package/dist/cjs/cli_utils.d.ts +3 -15
  12. package/dist/cjs/cli_utils.d.ts.map +1 -1
  13. package/dist/cjs/cli_utils.js +9 -42
  14. package/dist/cjs/cli_utils.js.map +1 -1
  15. package/dist/cjs/errors.d.ts.map +1 -1
  16. package/dist/cjs/errors.js +3 -3
  17. package/dist/cjs/errors.js.map +1 -1
  18. package/dist/cjs/index.d.ts.map +1 -1
  19. package/dist/cjs/index.js.map +1 -1
  20. package/dist/cjs/shrink.d.ts.map +1 -1
  21. package/dist/cjs/shrink.js +28 -29
  22. package/dist/cjs/shrink.js.map +1 -1
  23. package/dist/cjs/shrink_file.d.ts +1 -1
  24. package/dist/cjs/shrink_file.d.ts.map +1 -1
  25. package/dist/cjs/shrink_file.js.map +1 -1
  26. package/dist/cjs/validate.d.ts.map +1 -1
  27. package/dist/cjs/validate.js +1 -1
  28. package/dist/cjs/validate.js.map +1 -1
  29. package/dist/esm/cli.d.ts +1 -0
  30. package/dist/esm/cli.js +41 -38
  31. package/dist/esm/cli.js.map +1 -1
  32. package/dist/esm/cli_utils.d.ts +3 -15
  33. package/dist/esm/cli_utils.d.ts.map +1 -1
  34. package/dist/esm/cli_utils.js +9 -41
  35. package/dist/esm/cli_utils.js.map +1 -1
  36. package/dist/esm/errors.d.ts.map +1 -1
  37. package/dist/esm/errors.js +3 -3
  38. package/dist/esm/errors.js.map +1 -1
  39. package/dist/esm/index.d.ts.map +1 -1
  40. package/dist/esm/index.js.map +1 -1
  41. package/dist/esm/shrink.d.ts.map +1 -1
  42. package/dist/esm/shrink.js +28 -29
  43. package/dist/esm/shrink.js.map +1 -1
  44. package/dist/esm/shrink_file.d.ts +1 -1
  45. package/dist/esm/shrink_file.d.ts.map +1 -1
  46. package/dist/esm/shrink_file.js +1 -1
  47. package/dist/esm/shrink_file.js.map +1 -1
  48. package/dist/esm/validate.d.ts.map +1 -1
  49. package/dist/esm/validate.js +2 -2
  50. package/dist/esm/validate.js.map +1 -1
  51. package/package.json +72 -3
  52. package/src/cli.ts +56 -46
  53. package/src/cli_utils.test.ts +20 -58
  54. package/src/cli_utils.ts +21 -55
  55. package/src/errors.ts +14 -10
  56. package/src/index.ts +3 -4
  57. package/src/shrink.test.ts +270 -270
  58. package/src/shrink.ts +166 -134
  59. package/src/shrink_file.test.ts +4 -4
  60. package/src/shrink_file.ts +14 -10
  61. package/src/validate.test.ts +19 -21
  62. package/src/validate.ts +15 -12
  63. package/dist/cjs/stdin.d.ts +0 -7
  64. package/dist/cjs/stdin.d.ts.map +0 -1
  65. package/dist/cjs/stdin.js +0 -36
  66. package/dist/cjs/stdin.js.map +0 -1
  67. package/dist/esm/stdin.d.ts +0 -7
  68. package/dist/esm/stdin.d.ts.map +0 -1
  69. package/dist/esm/stdin.js +0 -33
  70. package/dist/esm/stdin.js.map +0 -1
  71. 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(desiredPatterns: string[], shrinkOptions?: Partial<ShrinkOptions>): Promise<string[]> {
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, {expandServiceAsterisk: true})
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}:*`, {expandServiceAsterisk: true}))
42
- const reducedServiceActions = shrinkResolvedList(desiredActions.withoutService, possibleActions, options.iterations)
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(action => `${service}:${action}`);
46
- const invalidMatch = await validateShrinkResults(desiredActions.withService, reducedServiceActionsWithService);
47
- if(invalidMatch) {
48
- throw new ShrinkValidationError(desiredPatterns, invalidMatch);
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(":")[1]);
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(actions: string[]): Map<string, {withService: string[], withoutService: string[]}> {
77
- const serviceMap = new Map<string, {withService: string[], withoutService: string[]}>();
78
- actions.forEach(actionString => {
79
- const [service, action] = actionString.split(":");
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(desiredActions: string[], possibleActions: string[], iterations: number): string[] {
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(desiredActions: string[], undesiredActions: string[], deep: boolean): string[] {
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(sequence => sequence.sequence != "*");;
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(reducedActions.map(action => reduceAction(action, sequence.sequence, undesiredActions)))
157
- );
158
- reducedActions = consolidateWildcardPatterns(reducedIteration);
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(desiredAction: string, sequence: string, undesiredActions: string[]): string {
174
- const testArray = splitActionIntoParts(desiredAction);
175
- if(testArray.length === 1) {
176
- return desiredAction;
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 = "^" + wildcardAction.replace(/\*/g, '.*?') + "$"
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(actions: string[]): { sequence: string, frequency: number, length: number }[] {
331
- const allSubstrings = new Set<string>();
332
- actions.forEach(action => {
333
- splitActionIntoParts(action).forEach(substring => allSubstrings.add(substring));
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 => matchesPattern(consolidated, pattern))
364
- if(coveredByExistingPattern) {
365
- continue;
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(consolidated => !matchesPattern(pattern, consolidated));
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("^" + general.replace(/\*/g, ".*") + "$");
384
- return regex.test(specific);
385
- }
415
+ const regex = new RegExp('^' + general.replace(/\*/g, '.*') + '$')
416
+ return regex.test(specific)
417
+ }
@@ -1,7 +1,7 @@
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";
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
 
@@ -1,4 +1,4 @@
1
- import { ShrinkOptions, shrink } from "./shrink.js";
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(options: Partial<ShrinkOptions>, document: any, key?: string): Promise<any> {
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
+ }
@@ -1,22 +1,21 @@
1
- import { expandIamActions } from '@cloud-copilot/iam-expand';
2
- import { describe, expect, it, vi } from "vitest";
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 "@cloud-copilot/iam-expand";
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(desiredActions: string[], patterns: string[]): Promise<string | undefined> {
13
- const desiredActionSet = new Set(desiredActions);
14
- const expandedAfterActions = await expandIamActions(patterns, {expandServiceAsterisk: true});
15
- const expandedAfterActionSet = new Set(expandedAfterActions);
16
- for(const afterAction of expandedAfterActions) {
17
- if(!desiredActionSet.has(afterAction)) {
18
- return `Undesired action: ${afterAction}`;
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
+ }
@@ -1,7 +0,0 @@
1
- /**
2
- * Read from stdin until the stream ends, timeout, or an error occurs
3
- *
4
- * @returns the string input from stdin
5
- */
6
- export declare function readStdin(readWait: number | undefined): Promise<string>;
7
- //# sourceMappingURL=stdin.d.ts.map