@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.
- package/LICENSE.txt +674 -0
- package/README.md +187 -0
- package/dist/cjs/cli.d.ts +2 -0
- package/dist/cjs/cli.d.ts.map +1 -0
- package/dist/cjs/cli.js +78 -0
- package/dist/cjs/cli.js.map +1 -0
- package/dist/cjs/cli_utils.d.ts +30 -0
- package/dist/cjs/cli_utils.d.ts.map +1 -0
- package/dist/cjs/cli_utils.js +75 -0
- package/dist/cjs/cli_utils.js.map +1 -0
- package/dist/cjs/errors.d.ts +13 -0
- package/dist/cjs/errors.d.ts.map +1 -0
- package/dist/cjs/errors.js +56 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +8 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/shrink.d.ts +131 -0
- package/dist/cjs/shrink.d.ts.map +1 -0
- package/dist/cjs/shrink.js +358 -0
- package/dist/cjs/shrink.js.map +1 -0
- package/dist/cjs/shrink_file.d.ts +12 -0
- package/dist/cjs/shrink_file.d.ts.map +1 -0
- package/dist/cjs/shrink_file.js +38 -0
- package/dist/cjs/shrink_file.js.map +1 -0
- package/dist/cjs/stdin.d.ts +7 -0
- package/dist/cjs/stdin.d.ts.map +1 -0
- package/dist/cjs/stdin.js +34 -0
- package/dist/cjs/stdin.js.map +1 -0
- package/dist/cjs/validate.d.ts +11 -0
- package/dist/cjs/validate.d.ts.map +1 -0
- package/dist/cjs/validate.js +30 -0
- package/dist/cjs/validate.js.map +1 -0
- package/dist/esm/cli.d.ts +2 -0
- package/dist/esm/cli.d.ts.map +1 -0
- package/dist/esm/cli.js +76 -0
- package/dist/esm/cli.js.map +1 -0
- package/dist/esm/cli_utils.d.ts +30 -0
- package/dist/esm/cli_utils.d.ts.map +1 -0
- package/dist/esm/cli_utils.js +69 -0
- package/dist/esm/cli_utils.js.map +1 -0
- package/dist/esm/errors.d.ts +13 -0
- package/dist/esm/errors.d.ts.map +1 -0
- package/dist/esm/errors.js +50 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/shrink.d.ts +131 -0
- package/dist/esm/shrink.d.ts.map +1 -0
- package/dist/esm/shrink.js +343 -0
- package/dist/esm/shrink.js.map +1 -0
- package/dist/esm/shrink_file.d.ts +12 -0
- package/dist/esm/shrink_file.d.ts.map +1 -0
- package/dist/esm/shrink_file.js +35 -0
- package/dist/esm/shrink_file.js.map +1 -0
- package/dist/esm/stdin.d.ts +7 -0
- package/dist/esm/stdin.d.ts.map +1 -0
- package/dist/esm/stdin.js +31 -0
- package/dist/esm/stdin.js.map +1 -0
- package/dist/esm/validate.d.ts +11 -0
- package/dist/esm/validate.d.ts.map +1 -0
- package/dist/esm/validate.js +27 -0
- package/dist/esm/validate.js.map +1 -0
- package/package.json +43 -0
- package/postbuild.sh +13 -0
- package/src/cli.ts +83 -0
- package/src/cli_utils.test.ts +117 -0
- package/src/cli_utils.ts +82 -0
- package/src/errors.ts +52 -0
- package/src/index.ts +3 -0
- package/src/shrink.test.ts +594 -0
- package/src/shrink.ts +385 -0
- package/src/shrink_file.test.ts +72 -0
- package/src/shrink_file.ts +38 -0
- package/src/stdin.ts +34 -0
- package/src/validate.test.ts +55 -0
- package/src/validate.ts +29 -0
- package/tsconfig.cjs.json +12 -0
- package/tsconfig.esm.json +15 -0
- 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
|
+
|
package/src/cli_utils.ts
ADDED
|
@@ -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