@contrast/contrast 1.0.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/README.md +109 -0
- package/bin/contrast.js +2 -0
- package/dist/commands/auth/auth.js +61 -0
- package/dist/commands/config/config.js +24 -0
- package/dist/commands/scan/processScan.js +23 -0
- package/dist/common/HTTPClient.js +192 -0
- package/dist/common/errorHandling.js +60 -0
- package/dist/constants/constants.js +30 -0
- package/dist/constants/lambda.js +31 -0
- package/dist/constants/locales.js +259 -0
- package/dist/constants.js +150 -0
- package/dist/index.js +56 -0
- package/dist/lambda/__mocks__/aws.js +21 -0
- package/dist/lambda/__mocks__/lambdaConfig.json +42 -0
- package/dist/lambda/arn.js +21 -0
- package/dist/lambda/aws.js +169 -0
- package/dist/lambda/cliError.js +76 -0
- package/dist/lambda/constants.js +11 -0
- package/dist/lambda/help.js +75 -0
- package/dist/lambda/lambda.js +130 -0
- package/dist/lambda/logUtils.js +36 -0
- package/dist/lambda/scanDetail.js +30 -0
- package/dist/lambda/scanDetailCompletion.js +56 -0
- package/dist/lambda/scanRequest.js +93 -0
- package/dist/lambda/scanResults.js +16 -0
- package/dist/lambda/utils.js +90 -0
- package/dist/scan/autoDetection.js +76 -0
- package/dist/scan/fileFinder.js +15 -0
- package/dist/scan/fileUtils.js +31 -0
- package/dist/scan/help.js +68 -0
- package/dist/scan/populateProjectIdAndProjectName.js +55 -0
- package/dist/scan/scan.js +96 -0
- package/dist/scan/scanController.js +54 -0
- package/dist/scan/scanResults.js +85 -0
- package/dist/utils/commonApi.js +45 -0
- package/dist/utils/filterProjectPath.js +20 -0
- package/dist/utils/getConfig.js +30 -0
- package/dist/utils/oraWrapper.js +20 -0
- package/dist/utils/paramsUtil/commandlineParams.js +31 -0
- package/dist/utils/paramsUtil/configStoreParams.js +18 -0
- package/dist/utils/paramsUtil/envVariableParams.js +10 -0
- package/dist/utils/paramsUtil/paramHandler.js +28 -0
- package/dist/utils/paramsUtil/yamlParams.js +6 -0
- package/dist/utils/parsedCLIOptions.js +13 -0
- package/dist/utils/requestUtils.js +18 -0
- package/dist/utils/validationCheck.js +26 -0
- package/package.json +123 -0
- package/src/commands/auth/auth.js +73 -0
- package/src/commands/config/config.js +25 -0
- package/src/commands/scan/processScan.js +29 -0
- package/src/common/HTTPClient.js +269 -0
- package/src/common/errorHandling.ts +79 -0
- package/src/constants/constants.js +34 -0
- package/src/constants/lambda.js +41 -0
- package/src/constants/locales.js +381 -0
- package/src/constants.js +168 -0
- package/src/index.ts +69 -0
- package/src/lambda/__mocks__/aws.ts +32 -0
- package/src/lambda/__mocks__/lambdaConfig.json +42 -0
- package/src/lambda/arn.ts +32 -0
- package/src/lambda/aws.ts +247 -0
- package/src/lambda/cliError.ts +72 -0
- package/src/lambda/constants.ts +11 -0
- package/src/lambda/help.ts +76 -0
- package/src/lambda/lambda.ts +174 -0
- package/src/lambda/logUtils.ts +46 -0
- package/src/lambda/scanDetailCompletion.ts +78 -0
- package/src/lambda/scanRequest.ts +142 -0
- package/src/lambda/scanResults.ts +29 -0
- package/src/lambda/utils.ts +125 -0
- package/src/scan/autoDetection.js +77 -0
- package/src/scan/fileUtils.js +33 -0
- package/src/scan/help.js +74 -0
- package/src/scan/populateProjectIdAndProjectName.js +62 -0
- package/src/scan/scan.js +126 -0
- package/src/scan/scanController.js +69 -0
- package/src/scan/scanResults.js +96 -0
- package/src/utils/commonApi.js +54 -0
- package/src/utils/filterProjectPath.js +21 -0
- package/src/utils/getConfig.ts +42 -0
- package/src/utils/oraWrapper.js +24 -0
- package/src/utils/paramsUtil/commandlineParams.js +37 -0
- package/src/utils/paramsUtil/configStoreParams.js +19 -0
- package/src/utils/paramsUtil/envVariableParams.js +10 -0
- package/src/utils/paramsUtil/paramHandler.js +28 -0
- package/src/utils/parsedCLIOptions.js +17 -0
- package/src/utils/requestUtils.js +22 -0
- package/src/utils/validationCheck.js +34 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$metadata": {
|
|
3
|
+
"httpStatusCode": 200,
|
|
4
|
+
"requestId": "c1495998-4606-46ba-b4fc-f7d0d165172d",
|
|
5
|
+
"attempts": 1,
|
|
6
|
+
"totalRetryDelay": 0
|
|
7
|
+
},
|
|
8
|
+
"Code": {
|
|
9
|
+
"Location": "https://awslambda-eu-cent-1-tasks.s3.eu-central-1.amazonaws.com/snapshots/123456789012/FunctionLambda-90e7680-c598cb5f-6bc9-4107-9fe7-13c126eb3b9e?versionId=ThxUComOVL.qnyEN9Bmi3xzK1JshuKA3&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEGcaDGV1LWNlbnRyYWwtMSJIMEYCIQDAMKTGQ5WEMHM9H7cZQugjEX8QWvq5zRxtXg%2Fz6m9lmAIhAMst%2F5iQkOPJ%2BNfflPiwD4GQchajTfvbNvVeGlz%2BdFi%2FKokECPD%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQAxoMNjgwNjg2NTU5NDM0IgwPQDJYINU9W7Csw4sq3QO52WKm5PvlVEeLrwstuDqPPKxhdK57A6z2foEF0Bp%2Bz%2Fp%2Flei2DzbpqjPDluzljZmzh3kP8QKPwq25Yk59%2BfXnxlSkHYe3GXO6W6VLBkPk81T0C9keVwdMijW1ZV94KN8qBB17kERR3sFIhBWXJRLhXSAdLREAfNLaE%2FgnP1eC1b5MZCBW46lbjmYHhHshgZJEUD2fy%2BRuOB4HijldLkpHKgZfwiD0ICXkvxF5NQT6tUhlQPNN%2BCrC2RQ0NSkmjXiaL2BXaDxaQVhZwMTGzBEyLAonA9bSisObWrVEjJcC4%2Bqz9ce25l2yYf77lEXWymEp9NvFFcUNAt6tt%2FAm2qMixcLxV0Y2NuBUBjIvPnNnTvBop%2FvAC1Rh6033AmWpmkK0tD65wEsTS4XAEtnD%2B%2Bku4r6kzatRJ88Lq9YOTbjs%2BMEccy9YIFp7Rwf2%2Bdw3EKHSqz4aB0R9KYa07NooZ%2Bym1VZWfGLfhFRJwKRLk3qKkY%2Bj4laizIE%2BBgqC7f7%2FYXcFk8RGX8vlX5y%2BMKWhBXoMPqAPG7ruoG1RbjQX8TEJvuG0G8c5x76fRjB2VRlykY78wa7%2B83a8oBqMfojq7hQWByNv%2B13KHVIIqrG87tL%2BtrmS7GAoils0vXf0Mvowqt6RkgY6pAGKzTtWpXiIhFLe5n2CNNDMuT9xCSCpQIWf2M9GcNabj%2FAUHIa81gvZwuHzB6DYpEj2cZ7wO22Ve%2FRLIrzdkBROoRYnT0GFevXUJAoO2WolWU13JS7owlBPTW7aET6o7fHJUhwAxPZPCp1cSaFMO5cDN%2BTOVm7V6P4es6m87S1yozp5SzHx3KblhJJF6krPHX6YCWC%2B%2FAu9tu7PqgiZ3RQTZ26f4A%3D%3D&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20220330T153702Z&X-Amz-SignedHeaders=host&X-Amz-Expires=600&X-Amz-Credential=ASIAZ47AUUDFFU2TD45R%2F20220330%2Feu-central-1%2Fs3%2Faws4_request&X-Amz-Signature=4f9187001f127561afee310d9a127acacb5064c51688f7cd65f98ccb0a7aaa10",
|
|
10
|
+
"RepositoryType": "S3"
|
|
11
|
+
},
|
|
12
|
+
"Configuration": {
|
|
13
|
+
"Architectures": ["x86_64"],
|
|
14
|
+
"CodeSha256": "KNNSfx0YOrZ+pG3fK0qbGBJv/b8V0/gWFFkFw581i4o=",
|
|
15
|
+
"CodeSize": 39577174,
|
|
16
|
+
"DeadLetterConfig": {
|
|
17
|
+
"TargetArn": "arn:aws:sqs:eu-central-1:123456789012:devlocaleuDeadLetterQ-89016d3"
|
|
18
|
+
},
|
|
19
|
+
"Description": "",
|
|
20
|
+
"Environment": {
|
|
21
|
+
"Variables": {
|
|
22
|
+
"EVENTS_BUCKET": "devlocaleu-cn-events-4h6eig-eu-central-1-1676820",
|
|
23
|
+
"AGENTS_TABLE": "devlocaleu.agent.agents"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"EphemeralStorage": { "Size": 512 },
|
|
27
|
+
"FunctionArn": "arn:aws:lambda:eu-central-1:123456789012:function:FunctionLambda-90e7680",
|
|
28
|
+
"FunctionName": "FunctionLambda-90e7680",
|
|
29
|
+
"Handler": "com.contrastsecurity.scan.ScanHandler::handleEventDataWithContext",
|
|
30
|
+
"LastModified": "2022-03-28T11:14:56.000+0000",
|
|
31
|
+
"LastUpdateStatus": "Successful",
|
|
32
|
+
"MemorySize": 1024,
|
|
33
|
+
"PackageType": "Zip",
|
|
34
|
+
"RevisionId": "d174710a-23ff-499b-a7ba-61036beabd7a",
|
|
35
|
+
"Role": "arn:aws:iam::123456789012:role/Function_Role",
|
|
36
|
+
"Runtime": "java11",
|
|
37
|
+
"State": "Active",
|
|
38
|
+
"Timeout": 900,
|
|
39
|
+
"TracingConfig": { "Mode": "PassThrough" },
|
|
40
|
+
"Version": "$LATEST"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CliError } from './cliError'
|
|
2
|
+
import { ERRORS } from './constants'
|
|
3
|
+
|
|
4
|
+
type ARN = {
|
|
5
|
+
partition: string
|
|
6
|
+
service: string
|
|
7
|
+
region: string
|
|
8
|
+
accountId: string
|
|
9
|
+
resource: string
|
|
10
|
+
resourceId?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const ARN_REGEX = /arn:(?<partition>[^:\n]*):(?<service>[^:\n]*):(?<region>[^:\n]*):(?<accountId>[^:\n]*):(?<ignore>(?<resource>[^:/\n]*)[:/])?(?<resourceId>.*)/
|
|
14
|
+
|
|
15
|
+
const parseARN = (arn: string | undefined) => {
|
|
16
|
+
if (!arn) {
|
|
17
|
+
throw new CliError(ERRORS.FAILED_TO_START_SCAN, {
|
|
18
|
+
errorCode: 'failedToParseArn'
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const arnMatch = arn.match(ARN_REGEX)
|
|
23
|
+
if (!arnMatch) {
|
|
24
|
+
throw new CliError(ERRORS.FAILED_TO_START_SCAN, {
|
|
25
|
+
errorCode: 'failedToParseArn'
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return arnMatch.groups as ARN
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { parseARN, ARN }
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Lambda,
|
|
3
|
+
GetFunctionCommand,
|
|
4
|
+
GetLayerVersionByArnCommand,
|
|
5
|
+
FunctionConfiguration,
|
|
6
|
+
ResourceNotFoundException,
|
|
7
|
+
LambdaServiceException
|
|
8
|
+
} from '@aws-sdk/client-lambda'
|
|
9
|
+
import {
|
|
10
|
+
GetRolePolicyCommand,
|
|
11
|
+
IAMClient,
|
|
12
|
+
paginateListRolePolicies,
|
|
13
|
+
paginateListAttachedRolePolicies,
|
|
14
|
+
GetPolicyCommand,
|
|
15
|
+
GetPolicyVersionCommand
|
|
16
|
+
} from '@aws-sdk/client-iam'
|
|
17
|
+
import { fromIni } from '@aws-sdk/credential-provider-ini'
|
|
18
|
+
import { RegionInputConfig } from '@aws-sdk/config-resolver/dist-types/regionConfig/resolveRegionConfig'
|
|
19
|
+
import { EndpointsInputConfig } from '@aws-sdk/config-resolver/dist-types/endpointsConfig/resolveEndpointsConfig'
|
|
20
|
+
import { AwsAuthInputConfig } from '@aws-sdk/middleware-signing/dist-types/configurations'
|
|
21
|
+
import { CliError } from './cliError'
|
|
22
|
+
import { LambdaOptions } from './lambda'
|
|
23
|
+
import { ERRORS } from './constants'
|
|
24
|
+
|
|
25
|
+
type AWSClientConfig = RegionInputConfig &
|
|
26
|
+
EndpointsInputConfig &
|
|
27
|
+
AwsAuthInputConfig
|
|
28
|
+
|
|
29
|
+
const getAwsClientOptions = ({
|
|
30
|
+
region,
|
|
31
|
+
endpointUrl,
|
|
32
|
+
profile
|
|
33
|
+
}: LambdaOptions): AWSClientConfig => {
|
|
34
|
+
const credentials = profile ? fromIni({ profile }) : undefined
|
|
35
|
+
return {
|
|
36
|
+
region: region || process.env.AWS_DEFAULT_REGION,
|
|
37
|
+
endpoint: endpointUrl,
|
|
38
|
+
credentials
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const getLambdaClient = (lambdaOptions: LambdaOptions) => {
|
|
43
|
+
try {
|
|
44
|
+
const clientOptions = getAwsClientOptions(lambdaOptions)
|
|
45
|
+
return new Lambda(clientOptions)
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const errorObj = error as any
|
|
48
|
+
if (errorObj?.code === 'ERR_INVALID_URL') {
|
|
49
|
+
throw new CliError(ERRORS.AWS_ERROR, { description: errorObj.message })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
throw error
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const getIAMClient = (lambdaOptions: LambdaOptions) => {
|
|
57
|
+
const clientOptions = getAwsClientOptions(lambdaOptions)
|
|
58
|
+
return new IAMClient(clientOptions)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const getLambdaFunctionConfiguration = async (
|
|
62
|
+
client: Lambda,
|
|
63
|
+
lambdaOptions: LambdaOptions
|
|
64
|
+
) => {
|
|
65
|
+
const { functionName: FunctionName } = lambdaOptions
|
|
66
|
+
const getFunctionCommand = new GetFunctionCommand({ FunctionName })
|
|
67
|
+
try {
|
|
68
|
+
return await client.send(getFunctionCommand)
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throwAwsError(error)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const getLayersLinks = async (
|
|
75
|
+
client: Lambda,
|
|
76
|
+
functionConfiguration: FunctionConfiguration
|
|
77
|
+
) => {
|
|
78
|
+
const { Layers: layers = [] } = functionConfiguration
|
|
79
|
+
const resultPromises = layers.map(async layerDict => {
|
|
80
|
+
try {
|
|
81
|
+
const layerArn = layerDict.Arn
|
|
82
|
+
const getLayerVersionByArnCommand = new GetLayerVersionByArnCommand({
|
|
83
|
+
Arn: layerArn
|
|
84
|
+
})
|
|
85
|
+
const layer = await client.send(getLayerVersionByArnCommand)
|
|
86
|
+
return {
|
|
87
|
+
Arn: layerArn,
|
|
88
|
+
Location: layer.Content?.Location
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {
|
|
91
|
+
if (e instanceof ResourceNotFoundException) {
|
|
92
|
+
e.message = `The layer ${layerDict.Arn} could not be found. We will continue the scan without it.`
|
|
93
|
+
throw e
|
|
94
|
+
}
|
|
95
|
+
throw e
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
const results = await Promise.allSettled(resultPromises)
|
|
99
|
+
const validLayers = results.filter(layerResult => {
|
|
100
|
+
if (layerResult.status === 'rejected') {
|
|
101
|
+
console.warn(layerResult.reason.message)
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
return true
|
|
105
|
+
})
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
107
|
+
// @ts-ignore: value is not recognized
|
|
108
|
+
return validLayers.map(layer => layer.value)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const getLambdaPolicies = async (
|
|
112
|
+
functionConfiguration: FunctionConfiguration,
|
|
113
|
+
lambdaOptions: LambdaOptions
|
|
114
|
+
) => {
|
|
115
|
+
const { Role: roleArn } = functionConfiguration
|
|
116
|
+
const roleSplitList = roleArn?.split('/')
|
|
117
|
+
|
|
118
|
+
if (roleSplitList) {
|
|
119
|
+
const roleName = roleSplitList[roleSplitList.length - 1]
|
|
120
|
+
const client = exports.getIAMClient(lambdaOptions)
|
|
121
|
+
const rolePolicies = await getRolePolicies(roleName, client)
|
|
122
|
+
const attachedPolicies = await getAttachedPolicies(roleName, client)
|
|
123
|
+
return [...rolePolicies, ...attachedPolicies]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const getRolePolicyNames = async (roleName: string, client: IAMClient) => {
|
|
128
|
+
const listRolePolicyNames = []
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
for await (const page of paginateListRolePolicies(
|
|
132
|
+
{ client },
|
|
133
|
+
{ RoleName: roleName }
|
|
134
|
+
)) {
|
|
135
|
+
if (page.PolicyNames) {
|
|
136
|
+
listRolePolicyNames.push(...page.PolicyNames)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (error) {
|
|
140
|
+
throwAwsError(error)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return listRolePolicyNames
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const getAttachedPolicyNames = async (roleName: string, client: IAMClient) => {
|
|
147
|
+
const listAttachedPolicies = []
|
|
148
|
+
for await (const page of paginateListAttachedRolePolicies(
|
|
149
|
+
{ client },
|
|
150
|
+
{ RoleName: roleName }
|
|
151
|
+
)) {
|
|
152
|
+
if (page.AttachedPolicies) {
|
|
153
|
+
listAttachedPolicies.push(...page.AttachedPolicies)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return listAttachedPolicies
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const getRolePolicies = async (roleName: string, client: IAMClient) => {
|
|
160
|
+
const listRolePolicyNames = await exports.getRolePolicyNames(roleName, client)
|
|
161
|
+
|
|
162
|
+
if (listRolePolicyNames) {
|
|
163
|
+
const rolePoliciesPromises = listRolePolicyNames.map(
|
|
164
|
+
async (policyName: any) => {
|
|
165
|
+
const getRolePolicyCommand = new GetRolePolicyCommand({
|
|
166
|
+
PolicyName: policyName,
|
|
167
|
+
RoleName: roleName
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const rolePolicy = await client.send(getRolePolicyCommand)
|
|
171
|
+
const policyDoc = JSON.parse(
|
|
172
|
+
decodeURIComponent(rolePolicy?.PolicyDocument || '{}')
|
|
173
|
+
)
|
|
174
|
+
policyDoc.PolicyName = policyName
|
|
175
|
+
return policyDoc
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
const rolePolicies = await Promise.all(rolePoliciesPromises)
|
|
180
|
+
return rolePolicies
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return []
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const getAttachedPolicies = async (roleName: string, client: IAMClient) => {
|
|
187
|
+
const listAttachedPolicies = await exports.getAttachedPolicyNames(
|
|
188
|
+
roleName,
|
|
189
|
+
client
|
|
190
|
+
)
|
|
191
|
+
const attachedPoliciesPromises = listAttachedPolicies.map(
|
|
192
|
+
async (policyDict: { PolicyArn: any; PolicyName: any }) => {
|
|
193
|
+
const getPolicyCommand = new GetPolicyCommand({
|
|
194
|
+
PolicyArn: policyDict.PolicyArn
|
|
195
|
+
})
|
|
196
|
+
const policy = await client.send(getPolicyCommand)
|
|
197
|
+
if (policy.Policy) {
|
|
198
|
+
const getPolicyVersionCommand = new GetPolicyVersionCommand({
|
|
199
|
+
PolicyArn: policy.Policy.Arn,
|
|
200
|
+
VersionId: policy.Policy.DefaultVersionId
|
|
201
|
+
})
|
|
202
|
+
const policyVersion = await client.send(getPolicyVersionCommand)
|
|
203
|
+
const policyDoc = JSON.parse(
|
|
204
|
+
decodeURIComponent(policyVersion?.PolicyVersion?.Document || '{}')
|
|
205
|
+
)
|
|
206
|
+
policyDoc['PolicyName'] = policyDict.PolicyName
|
|
207
|
+
policyDoc['PolicyArn'] = policyDict.PolicyArn
|
|
208
|
+
return policyDoc
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
const attachedPolicies = await Promise.all(attachedPoliciesPromises)
|
|
214
|
+
return attachedPolicies
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
*
|
|
219
|
+
* @param error any error
|
|
220
|
+
* @returns throw AWS error in union format
|
|
221
|
+
*/
|
|
222
|
+
const throwAwsError = (error: any) => {
|
|
223
|
+
const serviceError = error as LambdaServiceException
|
|
224
|
+
|
|
225
|
+
if (error instanceof Error && serviceError.$metadata) {
|
|
226
|
+
const { httpStatusCode } = serviceError.$metadata
|
|
227
|
+
const { message } = error
|
|
228
|
+
|
|
229
|
+
throw new CliError(ERRORS.AWS_ERROR, {
|
|
230
|
+
statusCode: httpStatusCode,
|
|
231
|
+
description: message
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
throw error
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export {
|
|
239
|
+
getAttachedPolicyNames,
|
|
240
|
+
getRolePolicyNames,
|
|
241
|
+
getIAMClient,
|
|
242
|
+
getLambdaClient,
|
|
243
|
+
getLambdaFunctionConfiguration,
|
|
244
|
+
getLayersLinks,
|
|
245
|
+
getLambdaPolicies,
|
|
246
|
+
throwAwsError
|
|
247
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import i18n from 'i18n'
|
|
2
|
+
import * as errorHandling from '../common/errorHandling'
|
|
3
|
+
|
|
4
|
+
type ErrorDetails = {
|
|
5
|
+
statusCode?: number // API statusCode
|
|
6
|
+
errorCode?: string // internal errorCode
|
|
7
|
+
description?: string // free usage
|
|
8
|
+
data?: any //
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
class CliError extends Error {
|
|
12
|
+
statusCode?: number
|
|
13
|
+
errorCode?: string
|
|
14
|
+
description?: string
|
|
15
|
+
data?: any
|
|
16
|
+
|
|
17
|
+
statusCodeDescription?: string
|
|
18
|
+
errorCodeDescription?: string
|
|
19
|
+
|
|
20
|
+
constructor(headerMessage: string, details?: ErrorDetails) {
|
|
21
|
+
const message = i18n.__(headerMessage || '')
|
|
22
|
+
super(message)
|
|
23
|
+
|
|
24
|
+
const { statusCode, errorCode, data, description } = details || {}
|
|
25
|
+
|
|
26
|
+
this.statusCode = statusCode
|
|
27
|
+
this.errorCode = errorCode
|
|
28
|
+
this.data = data
|
|
29
|
+
this.description = description
|
|
30
|
+
|
|
31
|
+
if (errorCode) {
|
|
32
|
+
this.errorCodeDescription = i18n.__(errorCode || '')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (statusCode) {
|
|
36
|
+
this.statusCodeDescription = this.getMessageByStatusCode(statusCode)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getErrorMessage() {
|
|
41
|
+
let finalDesc =
|
|
42
|
+
this.errorCodeDescription || this.statusCodeDescription || ''
|
|
43
|
+
|
|
44
|
+
if (this.description) {
|
|
45
|
+
finalDesc += finalDesc ? `\n${this.description}` : this.description
|
|
46
|
+
}
|
|
47
|
+
return errorHandling.getErrorMessage(this.message, finalDesc)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getMessageByStatusCode(statusCode: number) {
|
|
51
|
+
switch (statusCode) {
|
|
52
|
+
case 200:
|
|
53
|
+
return ''
|
|
54
|
+
case 400:
|
|
55
|
+
return i18n.__('badRequestErrorHeader')
|
|
56
|
+
case 401:
|
|
57
|
+
return i18n.__('unauthenticatedErrorHeader')
|
|
58
|
+
case 403:
|
|
59
|
+
return i18n.__('forbiddenRequestErrorHeader')
|
|
60
|
+
case 404:
|
|
61
|
+
return i18n.__('not_found_404')
|
|
62
|
+
case 423:
|
|
63
|
+
return i18n.__('resourceLockedErrorHeader')
|
|
64
|
+
case 500:
|
|
65
|
+
return i18n.__('internalServerErrorHeader')
|
|
66
|
+
default:
|
|
67
|
+
return i18n.__('something_went_wrong')
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { CliError }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// TODO: don't forget to add translation in `src/constants/lambda.js` for each value
|
|
2
|
+
|
|
3
|
+
const ERRORS = Object.freeze({
|
|
4
|
+
AWS_ERROR: 'awsError',
|
|
5
|
+
FAILED_TO_START_SCAN: 'failedToStartScan',
|
|
6
|
+
FAILED_TO_GET_SCAN: 'failedToGetScan',
|
|
7
|
+
FAILED_TO_GET_RESULTS: 'failedToGetResults',
|
|
8
|
+
VALIDATION_FAILED: 'validationFailed'
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export { ERRORS }
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import commandLineUsage from 'command-line-usage'
|
|
2
|
+
import i18n from 'i18n'
|
|
3
|
+
|
|
4
|
+
const lambdaUsageGuide = commandLineUsage([
|
|
5
|
+
{
|
|
6
|
+
header: i18n.__('lambdaHeader'),
|
|
7
|
+
content: [i18n.__('lambdaSummary')]
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
header: i18n.__('constantsPrerequisitesHeader'),
|
|
11
|
+
content: [i18n.__('lambdaPrerequisitesContent')]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
header: i18n.__('constantsUsage'),
|
|
15
|
+
content: [i18n.__('lambdaUsage')]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
header: i18n.__('constantsOptions'),
|
|
19
|
+
content: [
|
|
20
|
+
{
|
|
21
|
+
name: i18n.__('lambdaFunctionNameOption'),
|
|
22
|
+
summary: i18n.__('lambdaFunctionNameSummery')
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: i18n.__('lambdaEndpointOption'),
|
|
26
|
+
summary:
|
|
27
|
+
'{italic ' +
|
|
28
|
+
i18n.__('constantsOptional') +
|
|
29
|
+
'}: ' +
|
|
30
|
+
i18n.__('lambdaEndpointSummery')
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: i18n.__('lambdaRegionOption'),
|
|
34
|
+
summary:
|
|
35
|
+
'{italic ' +
|
|
36
|
+
i18n.__('constantsOptional') +
|
|
37
|
+
'}: ' +
|
|
38
|
+
i18n.__('lambdaRegionSummery')
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: i18n.__('lambdaProfileOption'),
|
|
42
|
+
summary:
|
|
43
|
+
'{italic ' +
|
|
44
|
+
i18n.__('constantsOptional') +
|
|
45
|
+
'}: ' +
|
|
46
|
+
i18n.__('lambdaProfileSummery')
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: i18n.__('lambdaJsonOption'),
|
|
50
|
+
summary:
|
|
51
|
+
'{italic ' +
|
|
52
|
+
i18n.__('constantsOptional') +
|
|
53
|
+
'}: ' +
|
|
54
|
+
i18n.__('lambdaJsonSummery')
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: i18n.__('lambdaVerboseOption'),
|
|
58
|
+
summary:
|
|
59
|
+
'{italic ' +
|
|
60
|
+
i18n.__('constantsOptional') +
|
|
61
|
+
'}: ' +
|
|
62
|
+
i18n.__('lambdaVerbosSummery')
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
content: [
|
|
68
|
+
{ name: i18n.__('lambdaHelpOption'), summary: i18n.__('helpSummary') }
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
content: '{underline https://www.contrastsecurity.com}'
|
|
73
|
+
}
|
|
74
|
+
])
|
|
75
|
+
|
|
76
|
+
export { lambdaUsageGuide }
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import commandLineArgs from 'command-line-args'
|
|
2
|
+
import { performance } from 'perf_hooks'
|
|
3
|
+
import { kebabCase } from 'lodash'
|
|
4
|
+
import i18n from 'i18n'
|
|
5
|
+
import { getAuth } from '../utils/paramsUtil/paramHandler'
|
|
6
|
+
import { CliError } from './cliError'
|
|
7
|
+
import { ERRORS } from './constants'
|
|
8
|
+
import { lambdaUsageGuide } from './help'
|
|
9
|
+
import { log } from './logUtils'
|
|
10
|
+
import { pollScanUntilCompletion } from './scanDetailCompletion'
|
|
11
|
+
import { requestScanFunctionPost } from './scanRequest'
|
|
12
|
+
import { getScanResults } from './scanResults'
|
|
13
|
+
import { prettyPrintResults } from './utils'
|
|
14
|
+
|
|
15
|
+
type LambdaOptions = {
|
|
16
|
+
functionName?: string
|
|
17
|
+
region?: string
|
|
18
|
+
endpointUrl?: string
|
|
19
|
+
profile?: string
|
|
20
|
+
help?: boolean
|
|
21
|
+
verbose?: boolean
|
|
22
|
+
jsonOutput?: boolean
|
|
23
|
+
_unknown?: string[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type ApiParams = {
|
|
27
|
+
organizationId: string
|
|
28
|
+
provider: 'aws'
|
|
29
|
+
accountId: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const failedStates = [
|
|
33
|
+
'UNSUPPORTED',
|
|
34
|
+
'EXCLUDED',
|
|
35
|
+
'CANCELED',
|
|
36
|
+
'FAILED',
|
|
37
|
+
'DISMISSED'
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
const printHelpMessage = () => {
|
|
41
|
+
log(lambdaUsageGuide)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const getLambdaOptions = (argv: string[]) => {
|
|
45
|
+
const lambdaDefinitions = [
|
|
46
|
+
{ name: 'function-name', alias: 'f', type: String },
|
|
47
|
+
{ name: 'region', alias: 'r', type: String },
|
|
48
|
+
{ name: 'endpoint-url', alias: 'e', type: String },
|
|
49
|
+
{ name: 'profile', alias: 'p', type: String },
|
|
50
|
+
{ name: 'help', alias: 'h', type: Boolean },
|
|
51
|
+
{ name: 'verbose', alias: 'v', type: Boolean },
|
|
52
|
+
{ name: 'json-output', alias: 'j', type: Boolean }
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
const lambdaOptions: LambdaOptions = commandLineArgs(lambdaDefinitions, {
|
|
56
|
+
argv,
|
|
57
|
+
partial: true,
|
|
58
|
+
camelCase: true,
|
|
59
|
+
caseInsensitive: true
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return lambdaOptions
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const processLambda = async (argv: string[]) => {
|
|
66
|
+
const lambdaOptions = getLambdaOptions(argv)
|
|
67
|
+
const { help } = lambdaOptions
|
|
68
|
+
|
|
69
|
+
if (help) {
|
|
70
|
+
return handleLambdaHelp()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
validateRequiredLambdaParams(lambdaOptions)
|
|
75
|
+
|
|
76
|
+
await actualProcessLambda(lambdaOptions)
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error instanceof CliError) {
|
|
79
|
+
console.error(error.getErrorMessage())
|
|
80
|
+
} else if (error instanceof Error) {
|
|
81
|
+
console.error(error.message)
|
|
82
|
+
}
|
|
83
|
+
process.exit(1)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const actualProcessLambda = async (lambdaOptions: LambdaOptions) => {
|
|
88
|
+
const auth = getAuth()
|
|
89
|
+
const startTime = performance.now()
|
|
90
|
+
const { jsonOutput } = lambdaOptions
|
|
91
|
+
const { scanId, params, functionArn } = await requestScanFunctionPost(
|
|
92
|
+
auth,
|
|
93
|
+
lambdaOptions
|
|
94
|
+
)
|
|
95
|
+
const scans = await pollScanUntilCompletion(auth, 10, params, scanId)
|
|
96
|
+
const failedScan = scans
|
|
97
|
+
?.filter((s: any) => s.scanType === 2)
|
|
98
|
+
.find((s: any) => failedStates.includes(s.state))
|
|
99
|
+
|
|
100
|
+
if (failedScan) {
|
|
101
|
+
throw new CliError(ERRORS.FAILED_TO_GET_SCAN, {
|
|
102
|
+
statusCode: 200,
|
|
103
|
+
errorCode: failedScan.state,
|
|
104
|
+
description: failedScan.stateReasonText
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const resultsResponse = await getScanResults(
|
|
109
|
+
auth,
|
|
110
|
+
params,
|
|
111
|
+
scanId,
|
|
112
|
+
functionArn
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if (jsonOutput) {
|
|
116
|
+
console.log(JSON.stringify(resultsResponse?.data?.results, null, 2))
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const results = resultsResponse?.data?.results
|
|
121
|
+
if (!results) {
|
|
122
|
+
throw new CliError(ERRORS.FAILED_TO_GET_RESULTS, {
|
|
123
|
+
errorCode: 'missingResults'
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!results.length) {
|
|
128
|
+
log('👏 No vulnerabilities found')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const endTime = performance.now()
|
|
132
|
+
const scanDurationMs = endTime - startTime
|
|
133
|
+
|
|
134
|
+
log(`----- Scan completed ${(scanDurationMs / 1000).toFixed(2)}s -----`)
|
|
135
|
+
|
|
136
|
+
if (results?.length) {
|
|
137
|
+
prettyPrintResults(results)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const validateRequiredLambdaParams = (options: LambdaOptions) => {
|
|
142
|
+
if (options._unknown?.length) {
|
|
143
|
+
throw new CliError(ERRORS.VALIDATION_FAILED, {
|
|
144
|
+
description: i18n.__('notSupportedFlags', options._unknown.join('\n'))
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!options?.functionName) {
|
|
149
|
+
throw new CliError(ERRORS.VALIDATION_FAILED, {
|
|
150
|
+
errorCode: 'missingFunctionName'
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const flagsWithoutValues = Object.entries(options)
|
|
155
|
+
.filter(([, value]) => !value)
|
|
156
|
+
.map(([key]) => key)
|
|
157
|
+
.map(p => `--${kebabCase(p)}`)
|
|
158
|
+
|
|
159
|
+
if (flagsWithoutValues.length) {
|
|
160
|
+
throw new CliError(ERRORS.VALIDATION_FAILED, {
|
|
161
|
+
description: i18n.__(
|
|
162
|
+
'missingFlagArguments',
|
|
163
|
+
flagsWithoutValues.join('\n')
|
|
164
|
+
)
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const handleLambdaHelp = () => {
|
|
170
|
+
printHelpMessage()
|
|
171
|
+
process.exit(0)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export { processLambda, LambdaOptions, ApiParams }
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import util from 'util'
|
|
3
|
+
|
|
4
|
+
interface logStyles {
|
|
5
|
+
bold?: boolean
|
|
6
|
+
italic?: boolean
|
|
7
|
+
underline?: boolean
|
|
8
|
+
strikethrough?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const log = (message: string | number, styles?: logStyles) => {
|
|
12
|
+
let chalkFunction = chalk.reset
|
|
13
|
+
|
|
14
|
+
if (styles?.bold) {
|
|
15
|
+
chalkFunction = chalk.bold
|
|
16
|
+
} else if (styles?.italic) {
|
|
17
|
+
chalkFunction = chalk.italic
|
|
18
|
+
} else if (styles?.underline) {
|
|
19
|
+
chalkFunction = chalk.underline
|
|
20
|
+
} else if (styles?.strikethrough) {
|
|
21
|
+
chalkFunction = chalk.strikethrough
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log(styles ? chalkFunction(message) : message)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
*
|
|
29
|
+
* @param obj any json object or string
|
|
30
|
+
* @param depth determines how levels it will recurse to show the json
|
|
31
|
+
*/
|
|
32
|
+
const prettyPrintJson = (obj: string | any, depth: number | null = null) => {
|
|
33
|
+
if (!obj) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let objToPrint = obj
|
|
38
|
+
|
|
39
|
+
if (typeof obj === 'string') {
|
|
40
|
+
objToPrint = JSON.parse(obj)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(util.inspect(objToPrint, { colors: true, depth }))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { log, prettyPrintJson }
|