@cloud-copilot/iam-shrink 0.1.0

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 (85) hide show
  1. package/LICENSE.txt +674 -0
  2. package/README.md +187 -0
  3. package/dist/cjs/cli.d.ts +2 -0
  4. package/dist/cjs/cli.d.ts.map +1 -0
  5. package/dist/cjs/cli.js +78 -0
  6. package/dist/cjs/cli.js.map +1 -0
  7. package/dist/cjs/cli_utils.d.ts +30 -0
  8. package/dist/cjs/cli_utils.d.ts.map +1 -0
  9. package/dist/cjs/cli_utils.js +75 -0
  10. package/dist/cjs/cli_utils.js.map +1 -0
  11. package/dist/cjs/errors.d.ts +13 -0
  12. package/dist/cjs/errors.d.ts.map +1 -0
  13. package/dist/cjs/errors.js +56 -0
  14. package/dist/cjs/errors.js.map +1 -0
  15. package/dist/cjs/index.d.ts +3 -0
  16. package/dist/cjs/index.d.ts.map +1 -0
  17. package/dist/cjs/index.js +8 -0
  18. package/dist/cjs/index.js.map +1 -0
  19. package/dist/cjs/package.json +3 -0
  20. package/dist/cjs/shrink.d.ts +131 -0
  21. package/dist/cjs/shrink.d.ts.map +1 -0
  22. package/dist/cjs/shrink.js +358 -0
  23. package/dist/cjs/shrink.js.map +1 -0
  24. package/dist/cjs/shrink_file.d.ts +12 -0
  25. package/dist/cjs/shrink_file.d.ts.map +1 -0
  26. package/dist/cjs/shrink_file.js +38 -0
  27. package/dist/cjs/shrink_file.js.map +1 -0
  28. package/dist/cjs/stdin.d.ts +7 -0
  29. package/dist/cjs/stdin.d.ts.map +1 -0
  30. package/dist/cjs/stdin.js +34 -0
  31. package/dist/cjs/stdin.js.map +1 -0
  32. package/dist/cjs/validate.d.ts +11 -0
  33. package/dist/cjs/validate.d.ts.map +1 -0
  34. package/dist/cjs/validate.js +30 -0
  35. package/dist/cjs/validate.js.map +1 -0
  36. package/dist/esm/cli.d.ts +2 -0
  37. package/dist/esm/cli.d.ts.map +1 -0
  38. package/dist/esm/cli.js +76 -0
  39. package/dist/esm/cli.js.map +1 -0
  40. package/dist/esm/cli_utils.d.ts +30 -0
  41. package/dist/esm/cli_utils.d.ts.map +1 -0
  42. package/dist/esm/cli_utils.js +69 -0
  43. package/dist/esm/cli_utils.js.map +1 -0
  44. package/dist/esm/errors.d.ts +13 -0
  45. package/dist/esm/errors.d.ts.map +1 -0
  46. package/dist/esm/errors.js +50 -0
  47. package/dist/esm/errors.js.map +1 -0
  48. package/dist/esm/index.d.ts +3 -0
  49. package/dist/esm/index.d.ts.map +1 -0
  50. package/dist/esm/index.js +3 -0
  51. package/dist/esm/index.js.map +1 -0
  52. package/dist/esm/package.json +3 -0
  53. package/dist/esm/shrink.d.ts +131 -0
  54. package/dist/esm/shrink.d.ts.map +1 -0
  55. package/dist/esm/shrink.js +343 -0
  56. package/dist/esm/shrink.js.map +1 -0
  57. package/dist/esm/shrink_file.d.ts +12 -0
  58. package/dist/esm/shrink_file.d.ts.map +1 -0
  59. package/dist/esm/shrink_file.js +35 -0
  60. package/dist/esm/shrink_file.js.map +1 -0
  61. package/dist/esm/stdin.d.ts +7 -0
  62. package/dist/esm/stdin.d.ts.map +1 -0
  63. package/dist/esm/stdin.js +31 -0
  64. package/dist/esm/stdin.js.map +1 -0
  65. package/dist/esm/validate.d.ts +11 -0
  66. package/dist/esm/validate.d.ts.map +1 -0
  67. package/dist/esm/validate.js +27 -0
  68. package/dist/esm/validate.js.map +1 -0
  69. package/package.json +43 -0
  70. package/postbuild.sh +13 -0
  71. package/src/cli.ts +83 -0
  72. package/src/cli_utils.test.ts +117 -0
  73. package/src/cli_utils.ts +82 -0
  74. package/src/errors.ts +52 -0
  75. package/src/index.ts +3 -0
  76. package/src/shrink.test.ts +594 -0
  77. package/src/shrink.ts +385 -0
  78. package/src/shrink_file.test.ts +72 -0
  79. package/src/shrink_file.ts +38 -0
  80. package/src/stdin.ts +34 -0
  81. package/src/validate.test.ts +55 -0
  82. package/src/validate.ts +29 -0
  83. package/tsconfig.cjs.json +12 -0
  84. package/tsconfig.esm.json +15 -0
  85. package/tsconfig.json +23 -0
@@ -0,0 +1,117 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { convertOptions, dashToCamelCase, extractActionsFromLineOfInput, parseStdIn } from "./cli_utils";
3
+ import { readStdin } from './stdin';
4
+ vi.mock('./stdin')
5
+
6
+ beforeEach(() => {
7
+ vi.resetAllMocks()
8
+ })
9
+
10
+ const extractScenarios = [
11
+ {input: ' s3:Get* ', expected: ['s3:Get*']},
12
+ {input: ' s3:Get* s3:Put* ', expected: ['s3:Get*', 's3:Put*']},
13
+ {input: ' "s3:Get*", "s3:Put*"', expected: ['s3:Get*', 's3:Put*']},
14
+ {input: ' `s3:Get*`, `s3:Put*`', expected: ['s3:Get*', 's3:Put*']},
15
+ {input: ` 's3:Get*', 's3:Put*'`, expected: ['s3:Get*', 's3:Put*']},
16
+ {input: ` 'resource-Groups:Get*'`, expected: ['resource-Groups:Get*']},
17
+ {input: `s3:Get*, s3:Put*`, expected: ['s3:Get*', 's3:Put*']},
18
+ {input: "s3:Put*", expected: ['s3:Put*']},
19
+ {input: ":s3:Put*", expected: []},
20
+ {input: 'arn:aws:apigateway:*::/apis', expected: []},
21
+ {input: 'hamburger', expected: []},
22
+ ]
23
+
24
+ const dashToCamelCaseScenarios = [
25
+ {input: "--iterations", expected: "iterations"},
26
+ {input: "--show-data-version", expected: "showDataVersion"},
27
+ ]
28
+
29
+ describe('cli_utils', () => {
30
+ describe('extractActionsFromLineOfInput', () => {
31
+ extractScenarios.forEach((scenario, index) => {
32
+ it(`should return for scenario ${index}: ${scenario.input} `, () => {
33
+ // Given the input
34
+ const input = scenario.input
35
+
36
+ // When the actions are extracted
37
+ const result = extractActionsFromLineOfInput(input)
38
+
39
+ // Then I should get the expected result
40
+ expect(result).toEqual(scenario.expected)
41
+ })
42
+ })
43
+ })
44
+
45
+ describe('dashToCamelCase', () => {
46
+ dashToCamelCaseScenarios.forEach((scenario, index) => {
47
+ it(`should return for scenario ${index}: ${scenario.input} `, () => {
48
+ // Given the input
49
+ const input = scenario.input
50
+
51
+ // When the input is converted
52
+ const result = dashToCamelCase(input)
53
+
54
+ // Then I should get the expected result
55
+ expect(result).toEqual(scenario.expected)
56
+ })
57
+ })
58
+ })
59
+
60
+ describe('convertOptions', () => {
61
+ it('should convert the options to keys on an object', () => {
62
+ // Given options as an array of strings
63
+ const optionArgs = ['--distinct', '--sort', '--something-cool', '--key-with-value=10']
64
+
65
+ // When the options are converted
66
+ const result = convertOptions(optionArgs)
67
+
68
+ // Then each option should be a key on the object
69
+ expect(result).toEqual({
70
+ distinct: true,
71
+ sort: true,
72
+ somethingCool: true,
73
+ keyWithValue: "10",
74
+ })
75
+ })
76
+ })
77
+
78
+ describe('parseStdIn', () => {
79
+ it('should return an empty object if no data is provided', async () => {
80
+ // Given no data is provided
81
+ vi.mocked(readStdin).mockResolvedValue('')
82
+
83
+ // When the actions are parsed
84
+ const result = await parseStdIn({})
85
+
86
+ // Then I should get an empty object
87
+ expect(result).toEqual({})
88
+ })
89
+
90
+ it('should return an array of actions from the data if it cannot be parsed', async () => {
91
+ // Given there is data with actions in multiple lines
92
+ vi.mocked(readStdin).mockResolvedValue('s3:GetObject\ns3:PutObject\ns3:DeleteObject\n')
93
+
94
+ // When the actions are parsed
95
+ const result = await parseStdIn({})
96
+
97
+ // Then I should get the expected actions
98
+ expect(result).toEqual({strings: ['s3:GetObject', 's3:PutObject', 's3:DeleteObject']})
99
+ })
100
+
101
+ it('should return an object if the data can be parsed', async () => {
102
+ // Given there is data that can be parsed
103
+ const dataValue = {
104
+ Action: ["s3:GetObject"],
105
+ Version: "2012-10-17"
106
+ }
107
+ vi.mocked(readStdin).mockResolvedValue(JSON.stringify(dataValue))
108
+
109
+ // When the actions are parsed
110
+ const result = await parseStdIn({})
111
+
112
+ // Then I should get the expected object
113
+ expect(result).toEqual({object: dataValue})
114
+ })
115
+ })
116
+ })
117
+
@@ -0,0 +1,82 @@
1
+ import { ShrinkOptions } from "./shrink.js";
2
+ import { shrinkJsonDocument } from "./shrink_file.js";
3
+ import { readStdin } from "./stdin.js";
4
+
5
+ interface CliOptions extends ShrinkOptions {
6
+ showDataVersion: boolean
7
+ readWaitMs: string
8
+ }
9
+
10
+ /**
11
+ * Convert a dash-case string to camelCase
12
+ * @param str the string to convert
13
+ * @returns the camelCase string
14
+ */
15
+ export function dashToCamelCase(str: string): string {
16
+ str = str.substring(2)
17
+ return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
18
+ }
19
+
20
+ /**
21
+ * Convert an array of option strings to an object
22
+ *
23
+ * @param optionArgs the array of option strings to convert
24
+ * @returns the object representation of the options
25
+ */
26
+ export function convertOptions(optionArgs: string[]): Partial<CliOptions> {
27
+ const options: Record<string, string | boolean | number> = {} ;
28
+
29
+ for(const option of optionArgs) {
30
+ let key: string = option
31
+ let value: boolean | string = true
32
+ if(option.includes('=')) {
33
+ [key,value] = option.split('=')
34
+ }
35
+
36
+ options[dashToCamelCase(key)] = value
37
+ }
38
+ if(options.iterations) {
39
+ const iterationNumber = parseInt(options.iterations as string)
40
+ if(isNaN(iterationNumber)) {
41
+ delete options.iterations
42
+ } else if(iterationNumber <= 0) {
43
+ options.iterations = Infinity
44
+ } else {
45
+ options.iterations = iterationNumber
46
+ }
47
+ }
48
+
49
+ return options
50
+ }
51
+
52
+ const actionPattern = /\:?([a-zA-Z0-9-]+:[a-zA-Z0-9*]+)/g;
53
+ export function extractActionsFromLineOfInput(line: string): string[] {
54
+ const matches = line.matchAll(actionPattern)
55
+
56
+ return Array.from(matches)
57
+ .filter((match) => !match[0].startsWith('arn:') && !match[0].startsWith(':'))
58
+ .map((match) => match[1])
59
+ }
60
+
61
+ /**
62
+ * Parse the actions from stdin
63
+ *
64
+ * @returns an array of strings from stdin
65
+ */
66
+ export async function parseStdIn(options: Partial<CliOptions>): Promise<{strings?: string[], object?: any}> {
67
+ const delay = options.readWaitMs ? parseInt(options.readWaitMs.replaceAll(/\D/g, '')) : undefined
68
+ const data = await readStdin(delay)
69
+ if(data.length === 0) {
70
+ return {}
71
+ }
72
+
73
+ try {
74
+ const object = await shrinkJsonDocument(options, JSON.parse(data))
75
+ return {object}
76
+ } catch (err: any) {}
77
+
78
+
79
+ const lines = data.split('\n')
80
+ const actions = lines.flatMap(line => extractActionsFromLineOfInput(line))
81
+ return {strings: actions}
82
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,52 @@
1
+ const bugBaseUrl = 'https://github.com/cloud-copilot/iam-shrink/issues/new'
2
+
3
+ /**
4
+ * The title of the bug report
5
+ * @param errorMatch The undesired match
6
+ * @returns the title of the bug report
7
+ */
8
+ function bugTitle(errorMatch: string) {
9
+ return `Bug: ShrinkValidationError. ${errorMatch}`;
10
+ }
11
+
12
+ /**
13
+ * The body of the bug report
14
+ *
15
+ * @param errorMatch The undesired match
16
+ * @param desiredPatterns The desired patterns
17
+ * @param excludedPatterns The excluded patterns
18
+ * @returns the body of the bug report
19
+ */
20
+ function bugBody(errorMatch: string, desiredPatterns: string[]) {
21
+ return `${errorMatch} while shrinking patterns ${JSON.stringify(desiredPatterns)}`;
22
+ }
23
+
24
+ /**
25
+ * Get the full url of the full bug report
26
+ *
27
+ * @param desiredPatterns The desired patterns
28
+ * @param excludedPatterns The excluded patterns
29
+ * @param errorMatch The undesired match that caused the bug
30
+ * @returns the full url to create a new bug report
31
+ */
32
+ function bugUrl(desiredPatterns: string[], errorMatch: string) {
33
+ return `${bugBaseUrl}?labels=bug&title=${encodeURIComponent(bugTitle(errorMatch))}&body=${encodeURIComponent(bugBody(errorMatch, desiredPatterns))}`;
34
+ }
35
+
36
+ export class ShrinkValidationError extends Error {
37
+ /**
38
+ * Capture a validation error from a shrink operation
39
+ *
40
+ * @param desiredPatterns the patterns the user wanted to shrink
41
+ * @param excludedPatterns the patterns the user wanted to exclude
42
+ * @param errorMatch the undesired match that triggered the bug
43
+ */
44
+ constructor(public readonly desiredPatterns: string[], public readonly errorMatch: string) {
45
+ super([
46
+ `@cloud-copilot/iam-shrink has failed validation and this is a bug.`,
47
+ `Please file a bug at ${bugUrl(desiredPatterns, errorMatch)}`,
48
+ ].join("\n"));
49
+ this.name = "ShrinkValidationError"; // Set the name of the error
50
+ }
51
+ }
52
+
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { shrink } from './shrink.js';
2
+ export { shrinkJsonDocument } from './shrink_file.js';
3
+