├── bin └── contrast.js ├── .gitignore ├── src ├── audit │ ├── phpAnalysisEngine │ │ ├── sanitizer.js │ │ ├── readLockFileContents.js │ │ ├── readProjectFileContents.js │ │ ├── index.js │ │ └── parseLockFileContents.js │ ├── report │ │ ├── models │ │ │ ├── reportGuidanceModel.ts │ │ │ ├── reportSeverityModel.ts │ │ │ ├── severityCountModel.ts │ │ │ ├── reportLibraryModel.ts │ │ │ ├── reportOutputModel.ts │ │ │ └── reportListModel.ts │ │ └── reportingFeature.ts │ ├── goAnalysisEngine │ │ ├── sanitizer.js │ │ ├── index.js │ │ └── readProjectFileContents.js │ ├── javaAnalysisEngine │ │ ├── sanitizer.js │ │ ├── index.js │ │ └── readProjectFileContents.js │ ├── languageAnalysisEngine │ │ ├── util │ │ │ ├── capabilities.js │ │ │ ├── requestUtils.js │ │ │ └── generalAPI.js │ │ ├── commonApi.js │ │ ├── filterProjectPath.js │ │ ├── constants.js │ │ ├── getProjectRootFilenames.js │ │ ├── report │ │ │ ├── checkIgnoreDevDep.js │ │ │ └── newReportingFeature.js │ │ ├── checkForMultipleIdentifiedLanguages.js │ │ ├── getIdentifiedLanguageInfo.js │ │ ├── checkIdentifiedLanguageHasProjectFile.js │ │ ├── checkForMultipleIdentifiedProjectFiles.js │ │ ├── checkIdentifiedLanguageHasLockFile.js │ │ ├── index.js │ │ └── sendSnapshot.js │ ├── rubyAnalysisEngine │ │ ├── sanitizer.js │ │ ├── readGemfileLockContents.js │ │ ├── readGemfileContents.js │ │ ├── parsedGemfile.js │ │ └── index.js │ ├── pythonAnalysisEngine │ │ ├── sanitizer.js │ │ ├── readPipfileLockFileContents.js │ │ ├── readPythonProjectFileContents.js │ │ ├── parsePipfileLockContents.js │ │ ├── parseProjectFileContents.js │ │ └── index.js │ ├── nodeAnalysisEngine │ │ ├── sanitizer.js │ │ ├── parseNPMLockFileContents.js │ │ ├── readNPMLockFileContents.js │ │ ├── parseYarnLockFileContents.js │ │ ├── readProjectFileContents.js │ │ ├── readYarnLockFileContents.js │ │ ├── index.js │ │ ├── handleNPMLockFileV2.js │ │ └── parseYarn2LockFileContents.js │ ├── dotnetAnalysisEngine │ │ ├── sanitizer.js │ │ ├── readProjectFileContents.js │ │ ├── readLockFileContents.js │ │ ├── index.js │ │ ├── parseProjectFileContents.js │ │ └── parseLockFileContents.js │ ├── catalogueApplication │ │ └── catalogueApplication.js │ ├── save.js │ └── AnalysisEngine.js ├── utils │ ├── capabilities.js │ ├── paramsUtil │ │ ├── envVariableParams.js │ │ ├── commandlineParams.js │ │ ├── paramHandler.js │ │ └── configStoreParams.js │ ├── oraWrapper.js │ ├── requestUtils.js │ ├── settingsHelper.js │ ├── filterProjectPath.js │ ├── saveFile.js │ ├── parsedCLIOptions.js │ ├── validationCheck.js │ ├── getConfig.ts │ ├── generalAPI.js │ └── commonApi.js ├── commands │ ├── audit │ │ ├── saveFile.js │ │ ├── saveFile.ts │ │ ├── auditConfig.js │ │ ├── auditConfig.ts │ │ ├── processAudit.js │ │ ├── processAudit.ts │ │ ├── auditController.js │ │ ├── auditController.ts │ │ ├── help.ts │ │ └── help.js │ ├── config │ │ └── config.js │ ├── scan │ │ └── processScan.js │ └── auth │ │ └── auth.js ├── scaAnalysis │ ├── dotnet │ │ ├── index.js │ │ └── analysis.js │ ├── ruby │ │ └── index.js │ ├── python │ │ ├── index.js │ │ └── analysis.js │ ├── repoMode │ │ ├── index.js │ │ ├── gradleParser.js │ │ └── mavenParser.js │ ├── go │ │ ├── goAnalysis.js │ │ └── goReadDepFile.js │ ├── php │ │ ├── index.js │ │ ├── phpNewServicesMapper.js │ │ └── analysis.js │ ├── common │ │ ├── treeUpload.js │ │ ├── scaParserForGoAndJava.js │ │ ├── formatMessage.js │ │ ├── scaServicesUpload.js │ │ └── auditReport.js │ ├── java │ │ └── index.js │ └── javascript │ │ ├── index.js │ │ └── analysis.js ├── lambda │ ├── constants.ts │ ├── analytics.ts │ ├── scanResults.ts │ ├── types.ts │ ├── __mocks__ │ │ ├── aws.ts │ │ └── lambdaConfig.json │ ├── arn.ts │ ├── logUtils.ts │ ├── cliError.ts │ ├── scanDetailCompletion.ts │ ├── help.ts │ ├── lambdaUtils.ts │ └── scanRequest.ts ├── scan │ ├── saveResults.js │ ├── models │ │ ├── groupedResultsModel.ts │ │ ├── scanResultsModel.ts │ │ └── resultContentModel.ts │ ├── help.js │ ├── scanConfig.js │ ├── populateProjectIdAndProjectName.js │ ├── scan.ts │ ├── scanController.js │ └── autoDetection.js ├── common │ ├── commonHelp.ts │ ├── commonHelp.js │ ├── fail.js │ ├── versionChecker.ts │ ├── versionChecker.js │ ├── errorHandling.js │ └── errorHandling.ts ├── sbom │ └── generateSbom.ts ├── constants │ ├── constants.js │ └── lambda.js └── index.ts ├── tsconfig.json └── LICENSE.md /bin/contrast.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../dist/index.js') 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.DS_Store 3 | binaries 4 | package-lock.json 5 | .idea 6 | -------------------------------------------------------------------------------- /src/audit/phpAnalysisEngine/sanitizer.js: -------------------------------------------------------------------------------- 1 | module.exports = exports = ({ php }, next) => { 2 | delete php.rawLockFileContents 3 | next() 4 | } 5 | -------------------------------------------------------------------------------- /src/audit/report/models/reportGuidanceModel.ts: -------------------------------------------------------------------------------- 1 | export class ReportGuidanceModel { 2 | minimum?: string 3 | maximum?: string 4 | latest?: string 5 | } 6 | -------------------------------------------------------------------------------- /src/audit/goAnalysisEngine/sanitizer.js: -------------------------------------------------------------------------------- 1 | module.exports = exports = ({ go }, next) => { 2 | // Remove anything sensitive or unnecessary from being sent to the backend as 3 | // a result of our Go project analysis 4 | delete go.modGraphOutput 5 | 6 | next() 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/capabilities.js: -------------------------------------------------------------------------------- 1 | const CLI_IGNORE_DEV_DEPS = 'CLI_IGNORE_DEV_DEPS' 2 | 3 | const featuresTeamServer = [ 4 | { 5 | CLI_IGNORE_DEV_DEPS: '3.9.0' 6 | } 7 | ] 8 | 9 | module.exports = { 10 | featuresTeamServer, 11 | CLI_IGNORE_DEV_DEPS 12 | } 13 | -------------------------------------------------------------------------------- /src/audit/javaAnalysisEngine/sanitizer.js: -------------------------------------------------------------------------------- 1 | module.exports = exports = ({ java }, next) => { 2 | // Remove anything sensitive or unnecessary from being sent to the backend as 3 | // a result of our Java project analysis 4 | delete java.mvnDependancyTreeOutput 5 | next() 6 | } 7 | -------------------------------------------------------------------------------- /src/audit/languageAnalysisEngine/util/capabilities.js: -------------------------------------------------------------------------------- 1 | const CLI_IGNORE_DEV_DEPS = 'CLI_IGNORE_DEV_DEPS' 2 | 3 | const featuresTeamServer = [ 4 | { 5 | CLI_IGNORE_DEV_DEPS: '3.9.0' 6 | } 7 | ] 8 | 9 | module.exports = { 10 | featuresTeamServer, 11 | CLI_IGNORE_DEV_DEPS 12 | } 13 | -------------------------------------------------------------------------------- /src/commands/audit/saveFile.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | const saveFile = (config, type, rawResults) => { 4 | const fileName = `${config.applicationId}-sbom-${type}.json` 5 | fs.writeFileSync(fileName, JSON.stringify(rawResults)) 6 | } 7 | 8 | module.exports = { 9 | saveFile 10 | } 11 | -------------------------------------------------------------------------------- /src/audit/rubyAnalysisEngine/sanitizer.js: -------------------------------------------------------------------------------- 1 | module.exports = exports = ({ ruby }, next) => { 2 | // Remove anything sensitive or unnecessary from being sent to the backend as 3 | // a result of our Ruby project analysis 4 | delete ruby.rawProjectFileContents 5 | delete ruby.rawLockFileContents 6 | 7 | next() 8 | } 9 | -------------------------------------------------------------------------------- /src/commands/audit/saveFile.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | 3 | export const saveFile = (config: any, type: string, rawResults: any) => { 4 | const fileName = `${config.applicationId}-sbom-${type}.json` 5 | fs.writeFileSync(fileName, JSON.stringify(rawResults)) 6 | } 7 | 8 | module.exports = { 9 | saveFile 10 | } 11 | -------------------------------------------------------------------------------- /src/audit/pythonAnalysisEngine/sanitizer.js: -------------------------------------------------------------------------------- 1 | module.exports = exports = ({ python }, next) => { 2 | // Remove anything sensitive or unnecessary from being sent to the backend as 3 | // a result of our Python project analysis 4 | delete python.rawProjectFileContents 5 | delete python.rawLockFileContents 6 | delete python.pipfileLock.default 7 | 8 | next() 9 | } 10 | -------------------------------------------------------------------------------- /src/scaAnalysis/dotnet/index.js: -------------------------------------------------------------------------------- 1 | const { getDotNetDeps } = require('./analysis') 2 | const { createDotNetTSMessage } = require('../common/formatMessage') 3 | 4 | const dotNetAnalysis = (config, languageFiles) => { 5 | const dotNetDeps = getDotNetDeps(config.file, languageFiles.DOTNET) 6 | return createDotNetTSMessage(dotNetDeps) 7 | } 8 | 9 | module.exports = { 10 | dotNetAnalysis 11 | } 12 | -------------------------------------------------------------------------------- /src/lambda/constants.ts: -------------------------------------------------------------------------------- 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 } 12 | -------------------------------------------------------------------------------- /src/lambda/analytics.ts: -------------------------------------------------------------------------------- 1 | import { getHttpClient } from '../utils/commonApi' 2 | import { getAuth } from '../utils/paramsUtil/paramHandler' 3 | import { AnalyticsOption } from './types' 4 | 5 | export const postAnalytics = (data: AnalyticsOption, provider = 'aws') => { 6 | const config = getAuth() 7 | const client = getHttpClient(config) 8 | return client.postAnalyticsFunction(config, provider, data) 9 | } 10 | -------------------------------------------------------------------------------- /src/scan/saveResults.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | const writeResultsToFile = async (responseBody, name = 'results.sarif') => { 4 | try { 5 | fs.writeFileSync(name, JSON.stringify(responseBody, null, 2)) 6 | return name 7 | } catch (err) { 8 | console.log('Error writing Scan Results to file') 9 | } 10 | } 11 | 12 | module.exports = { 13 | writeResultsToFile: writeResultsToFile 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/paramsUtil/envVariableParams.js: -------------------------------------------------------------------------------- 1 | const getAuth = () => { 2 | let params = {} 3 | params.apiKey = process.env.CONTRAST__API__API_KEY 4 | params.authorization = process.env.CONTRAST__API__AUTHORIZATION 5 | params.host = process.env.CONTRAST__API__URL 6 | params.organizationId = process.env.CONTRAST__API__ORGANIZATION_ID 7 | return params 8 | } 9 | 10 | module.exports = { getAuth: getAuth } 11 | -------------------------------------------------------------------------------- /src/utils/paramsUtil/commandlineParams.js: -------------------------------------------------------------------------------- 1 | const getAuth = (parsedCLIOptions = {}) => { 2 | let params = {} 3 | params.apiKey = parsedCLIOptions['apiKey'] 4 | params.authorization = parsedCLIOptions['authorization'] 5 | params.host = parsedCLIOptions['host'] 6 | params.organizationId = parsedCLIOptions['organizationId'] 7 | return params 8 | } 9 | 10 | module.exports = { 11 | getAuth: getAuth 12 | } 13 | -------------------------------------------------------------------------------- /src/audit/nodeAnalysisEngine/sanitizer.js: -------------------------------------------------------------------------------- 1 | module.exports = exports = ({ node }, next) => { 2 | // Remove anything sensitive or unnecessary from being sent to the backend as 3 | // a result of our NODE project analysis 4 | delete node.rawProjectFileContents 5 | delete node.projectFileJSON 6 | delete node.projectLockFileJSON 7 | delete node.rawLockFileContents 8 | delete node.rawYarnLockFileContents 9 | 10 | next() 11 | } 12 | -------------------------------------------------------------------------------- /src/audit/phpAnalysisEngine/readLockFileContents.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const i18n = require('i18n') 3 | 4 | module.exports = exports = ({ language: { lockFilePath }, php }, next) => { 5 | try { 6 | php.rawLockFileContents = JSON.parse(fs.readFileSync(lockFilePath)) 7 | } catch (err) { 8 | next(new Error(i18n.__('phpReadError', lockFilePath) + `${err.message}`)) 9 | 10 | return 11 | } 12 | 13 | next() 14 | } 15 | -------------------------------------------------------------------------------- /src/audit/report/models/reportSeverityModel.ts: -------------------------------------------------------------------------------- 1 | export class ReportSeverityModel { 2 | severity: string 3 | priority: number 4 | colour: string 5 | name: string 6 | 7 | constructor( 8 | severity: string, 9 | priority: number, 10 | colour: string, 11 | name: string 12 | ) { 13 | this.severity = severity 14 | this.priority = priority 15 | this.colour = colour 16 | this.name = name 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/audit/dotnetAnalysisEngine/sanitizer.js: -------------------------------------------------------------------------------- 1 | module.exports = exports = ({ dotnet }, next) => { 2 | // Remove anything sensitive or unnecessary from being sent to the backend as 3 | // a result of our .NET project analysis 4 | delete dotnet.rawProjectFileContents 5 | delete dotnet.parsedProjectFileContents 6 | delete dotnet.projectFileXML 7 | delete dotnet.packageReferences 8 | delete dotnet.rawLockFileContents 9 | 10 | next() 11 | } 12 | -------------------------------------------------------------------------------- /src/scaAnalysis/ruby/index.js: -------------------------------------------------------------------------------- 1 | const analysis = require('./analysis') 2 | const { createRubyTSMessage } = require('../common/formatMessage') 3 | 4 | const rubyAnalysis = (config, languageFiles) => { 5 | const rubyDeps = analysis.getRubyDeps(config, languageFiles.RUBY) 6 | 7 | if (config.experimental) { 8 | return rubyDeps 9 | } else { 10 | return createRubyTSMessage(rubyDeps) 11 | } 12 | } 13 | 14 | module.exports = { 15 | rubyAnalysis 16 | } 17 | -------------------------------------------------------------------------------- /src/common/commonHelp.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18n' 2 | 3 | export function commonHelpLinks() { 4 | return { 5 | header: i18n.__('commonHelpHeader'), 6 | content: [ 7 | i18n.__('commonHelpCheckOutHeader') + i18n.__('commonHelpCheckOutText'), 8 | i18n.__('commonHelpLearnMoreHeader') + i18n.__('commonHelpLearnMoreText'), 9 | i18n.__('commonHelpJoinDiscussionHeader') + 10 | i18n.__('commonHelpJoinDiscussionText') 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16/tsconfig.json", 3 | "compilerOptions": { 4 | "resolveJsonModule": true, 5 | "rootDir": "./src", 6 | "outDir": "./dist", 7 | "pretty": true, 8 | "module": "commonjs", 9 | "allowJs": true, 10 | "checkJs": false, 11 | "removeComments": true, 12 | "allowSyntheticDefaultImports": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules", "test", "test-integration"] 16 | } 17 | -------------------------------------------------------------------------------- /src/audit/languageAnalysisEngine/util/requestUtils.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | const Promise = require('bluebird') 3 | 4 | Promise.promisifyAll(request) 5 | 6 | function sendRequest({ options, method = 'put' }) { 7 | return request[`${method}Async`](options.url, options) 8 | } 9 | 10 | const sleep = ms => { 11 | return new Promise(resolve => setTimeout(resolve, ms)) 12 | } 13 | 14 | module.exports = { 15 | sendRequest: sendRequest, 16 | sleep: sleep 17 | } 18 | -------------------------------------------------------------------------------- /src/audit/pythonAnalysisEngine/readPipfileLockFileContents.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const i18n = require('i18n') 3 | 4 | module.exports = exports = ({ language: { lockFilePath }, python }, next) => { 5 | try { 6 | python.rawLockFileContents = fs.readFileSync(lockFilePath) 7 | } catch (err) { 8 | next( 9 | new Error( 10 | i18n.__('pythonAnalysisReadPipFileError', lockFilePath) + 11 | `${err.message}` 12 | ) 13 | ) 14 | } 15 | next() 16 | } 17 | -------------------------------------------------------------------------------- /src/scaAnalysis/python/index.js: -------------------------------------------------------------------------------- 1 | const { createPythonTSMessage } = require('../common/formatMessage') 2 | const { getPythonDeps, secondaryParser } = require('./analysis') 3 | 4 | const pythonAnalysis = (config, languageFiles) => { 5 | const pythonDeps = getPythonDeps(config, languageFiles.PYTHON) 6 | 7 | if (config.experimental) { 8 | return pythonDeps 9 | } else { 10 | return createPythonTSMessage(pythonDeps) 11 | } 12 | } 13 | 14 | module.exports = { 15 | pythonAnalysis 16 | } 17 | -------------------------------------------------------------------------------- /src/audit/rubyAnalysisEngine/readGemfileLockContents.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const i18n = require('i18n') 3 | 4 | module.exports = exports = ({ language: { lockFilePath }, ruby }, next) => { 5 | try { 6 | ruby.rawLockFileContents = fs.readFileSync(lockFilePath, 'utf8') 7 | next() 8 | } catch (err) { 9 | next( 10 | new Error( 11 | i18n.__('rubyAnalysisEngineReadGemLockFileError', lockFilePath) + 12 | `${err.message}` 13 | ) 14 | ) 15 | return 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/audit/rubyAnalysisEngine/readGemfileContents.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const i18n = require('i18n') 3 | 4 | module.exports = exports = ({ language: { projectFilePath }, ruby }, next) => { 5 | try { 6 | ruby.rawProjectFileContents = fs.readFileSync(projectFilePath, 'utf8') 7 | 8 | next() 9 | } catch (err) { 10 | next( 11 | new Error( 12 | i18n.__('rubyAnalysisEngineReadGemFileError', projectFilePath) + 13 | `${err.message}` 14 | ) 15 | ) 16 | return 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/scan/models/groupedResultsModel.ts: -------------------------------------------------------------------------------- 1 | export class GroupedResultsModel { 2 | ruleId: string 3 | codePathSet: Set 4 | cwe?: string[] 5 | reference?: string[] 6 | severity?: string 7 | advice?: string 8 | learn?: string[] 9 | issue?: string 10 | priority?: number 11 | message?: string | undefined 12 | colour: string 13 | codePath?: string 14 | 15 | constructor(ruleId: string) { 16 | this.ruleId = ruleId 17 | this.colour = '#999999' 18 | this.codePathSet = new Set() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/audit/languageAnalysisEngine/commonApi.js: -------------------------------------------------------------------------------- 1 | const { getHttpClient } = require('../../utils/commonApi') 2 | 3 | const returnAppId = async config => { 4 | const client = getHttpClient(config) 5 | let appId 6 | 7 | await client.getAppId(config).then(res => { 8 | if (res.body) { 9 | let obj = res.body['applications'] 10 | if (obj) { 11 | appId = obj.length === 0 ? '' : obj[0].app_id 12 | } 13 | } 14 | }) 15 | return appId 16 | } 17 | 18 | module.exports = { 19 | returnAppId: returnAppId 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/oraWrapper.js: -------------------------------------------------------------------------------- 1 | const ora = require('ora') 2 | 3 | const returnOra = text => { 4 | return ora(text) 5 | } 6 | 7 | const startSpinner = spinner => { 8 | spinner.start() 9 | } 10 | 11 | const stopSpinner = spinner => { 12 | spinner.stop() 13 | } 14 | 15 | const succeedSpinner = (spinner, text) => { 16 | spinner.succeed(text) 17 | } 18 | 19 | const failSpinner = (spinner, text) => { 20 | spinner.fail(text) 21 | } 22 | 23 | module.exports = { 24 | returnOra, 25 | startSpinner, 26 | succeedSpinner, 27 | failSpinner, 28 | stopSpinner 29 | } 30 | -------------------------------------------------------------------------------- /src/audit/report/models/severityCountModel.ts: -------------------------------------------------------------------------------- 1 | export class SeverityCountModel { 2 | critical!: number 3 | high!: number 4 | medium!: number 5 | low!: number 6 | note!: number 7 | total!: number 8 | 9 | //needed as default to stop NaN when new object constructed 10 | constructor() { 11 | this.critical = 0 12 | this.high = 0 13 | this.medium = 0 14 | this.low = 0 15 | this.note = 0 16 | this.total = 0 17 | } 18 | 19 | get getTotal(): number { 20 | return this.critical + this.high + this.medium + this.low + this.note 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/audit/languageAnalysisEngine/filterProjectPath.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | function resolveFilePath(filepath) { 4 | if (filepath[0] === '~') { 5 | return path.join(process.env.HOME, filepath.slice(1)) 6 | } 7 | return filepath 8 | } 9 | 10 | const returnProjectPath = () => { 11 | if (process.env.PWD !== (undefined || null || 'undefined')) { 12 | return process.env.PWD 13 | } else { 14 | return process.argv[process.argv.indexOf('--project_path') + 1] 15 | } 16 | } 17 | 18 | module.exports = { 19 | returnProjectPath: returnProjectPath, 20 | resolveFilePath: resolveFilePath 21 | } 22 | -------------------------------------------------------------------------------- /src/audit/pythonAnalysisEngine/readPythonProjectFileContents.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const i18n = require('i18n') 3 | 4 | module.exports = exports = ( 5 | { language: { projectFilePath }, python }, 6 | next 7 | ) => { 8 | try { 9 | //project file Contents points to requirements.txt for python 10 | python.rawProjectFileContents = fs.readFileSync(projectFilePath, 'utf8') 11 | 12 | next() 13 | } catch (err) { 14 | next( 15 | new Error( 16 | i18n.__('pythonAnalysisReadPythonProjectFileError', projectFilePath) + 17 | `${err.message}` 18 | ) 19 | ) 20 | return 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/audit/nodeAnalysisEngine/parseNPMLockFileContents.js: -------------------------------------------------------------------------------- 1 | const i18n = require('i18n') 2 | module.exports = exports = ({ language: { lockFilePath }, node }, next) => { 3 | // If we never read the package-lock file then pass priority 4 | if (node.rawLockFileContents === undefined) { 5 | next() 6 | } else { 7 | try { 8 | node.npmLockFile = JSON.parse(node.rawLockFileContents) 9 | } catch (err) { 10 | next( 11 | new Error( 12 | i18n.__('NodeParseNPM', lockFilePath ? lockFilePath : 'undefined') + 13 | `${err.message}` 14 | ) 15 | ) 16 | return 17 | } 18 | next() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/audit/languageAnalysisEngine/constants.js: -------------------------------------------------------------------------------- 1 | // Language identifiers 2 | const NODE = 'NODE' 3 | const JAVASCRIPT = 'JAVASCRIPT' 4 | const DOTNET = 'DOTNET' 5 | const JAVA = 'JAVA' 6 | const RUBY = 'RUBY' 7 | const PYTHON = 'PYTHON' 8 | const GO = 'GO' 9 | // we set the langauge as Node instead of PHP since we're using the Node engine in TS 10 | const PHP = 'PHP' 11 | 12 | const LOW = 'LOW' 13 | const MEDIUM = 'MEDIUM' 14 | const HIGH = 'HIGH' 15 | const CRITICAL = 'CRITICAL' 16 | 17 | module.exports = { 18 | supportedLanguages: { NODE, DOTNET, JAVA, RUBY, PYTHON, GO, PHP, JAVASCRIPT }, 19 | LOW: LOW, 20 | MEDIUM: MEDIUM, 21 | HIGH: HIGH, 22 | CRITICAL: CRITICAL 23 | } 24 | -------------------------------------------------------------------------------- /src/audit/nodeAnalysisEngine/readNPMLockFileContents.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const i18n = require('i18n') 3 | 4 | module.exports = exports = ({ language: { lockFilePath }, node }, next) => { 5 | // check if the lockFilename is populated and if it is check to 6 | // see if it has the package-lock if not then go on to next handler 7 | if (!lockFilePath || !lockFilePath.includes('package-lock.json')) { 8 | next() 9 | return 10 | } 11 | 12 | try { 13 | node.rawLockFileContents = fs.readFileSync(lockFilePath) 14 | } catch (err) { 15 | next( 16 | new Error(i18n.__('NodeReadNpmError', lockFilePath) + `${err.message}`) 17 | ) 18 | 19 | return 20 | } 21 | 22 | next() 23 | } 24 | -------------------------------------------------------------------------------- /src/audit/pythonAnalysisEngine/parsePipfileLockContents.js: -------------------------------------------------------------------------------- 1 | const i18n = require('i18n') 2 | 3 | module.exports = exports = ({ language: { lockFilePath }, python }, next) => { 4 | if (python.rawLockFileContents === undefined) { 5 | return next() 6 | } 7 | try { 8 | let parsedPipLock = JSON.parse(python.rawLockFileContents) 9 | parsedPipLock['defaults'] = parsedPipLock['default'] 10 | python.pipfileLock = parsedPipLock 11 | } catch (err) { 12 | next( 13 | new Error( 14 | i18n.__( 15 | 'pythonAnalysisEnginePipError', 16 | lockFilePath ? lockFilePath : 'undefined' 17 | ) + `${err.message}` 18 | ) 19 | ) 20 | return 21 | } 22 | next() 23 | } 24 | -------------------------------------------------------------------------------- /src/audit/goAnalysisEngine/index.js: -------------------------------------------------------------------------------- 1 | const AnalysisEngine = require('../AnalysisEngine') 2 | const readProjectFileContents = require('./readProjectFileContents') 3 | const parseProjectFileContents = require('./parseProjectFileContents') 4 | const sanitizer = require('./sanitizer') 5 | const i18n = require('i18n') 6 | 7 | module.exports = exports = (language, config, callback) => { 8 | const ae = new AnalysisEngine({ language, config, go: {} }) 9 | ae.use([readProjectFileContents, parseProjectFileContents, sanitizer]) 10 | 11 | ae.analyze((err, analysis) => { 12 | if (err) { 13 | callback(new Error(i18n.__('goAnalysisError') + `${err.message}`)) 14 | return 15 | } 16 | callback(null, analysis) 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/audit/auditConfig.js: -------------------------------------------------------------------------------- 1 | const { getCommandLineArgsCustom } = require('../../utils/parsedCLIOptions') 2 | const constants = require('../../cliConstants') 3 | const paramHandler = require('../../utils/paramsUtil/paramHandler') 4 | 5 | const getAuditConfig = async (contrastConf, command, argv) => { 6 | const auditParameters = await getCommandLineArgsCustom( 7 | contrastConf, 8 | command, 9 | argv, 10 | constants.commandLineDefinitions.auditOptionDefinitions 11 | ) 12 | const paramsAuth = paramHandler.getAuth(auditParameters) 13 | const javaAgreement = paramHandler.getAgreement() 14 | return { ...paramsAuth, ...auditParameters, ...javaAgreement } 15 | } 16 | 17 | module.exports = { 18 | getAuditConfig 19 | } 20 | -------------------------------------------------------------------------------- /src/audit/dotnetAnalysisEngine/readProjectFileContents.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const i18n = require('i18n') 3 | 4 | module.exports = exports = (analysis, next) => { 5 | const { 6 | language: { projectFilePath }, 7 | dotnet 8 | } = analysis 9 | 10 | // Read the .NET project file contents. We are reading into memory presuming 11 | // that the contents of the file aren't large which may be bad... Could look 12 | // into streaming in the future 13 | try { 14 | dotnet.rawProjectFileContents = fs.readFileSync(projectFilePath) 15 | } catch (err) { 16 | next( 17 | new Error( 18 | i18n.__('dotnetReadProjectFile', projectFilePath) + `${err.message}` 19 | ) 20 | ) 21 | 22 | return 23 | } 24 | 25 | next() 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/audit/auditConfig.ts: -------------------------------------------------------------------------------- 1 | import paramHandler from '../../utils/paramsUtil/paramHandler' 2 | import constants from '../../constants' 3 | import { getCommandLineArgsCustom } from '../../utils/parsedCLIOptions' 4 | import { ContrastConf } from '../../utils/getConfig' 5 | 6 | export const getAuditConfig = async ( 7 | contrastConf: ContrastConf, 8 | command: string, 9 | argv: string[] 10 | ): Promise<{ [key: string]: string }> => { 11 | const auditParameters = await getCommandLineArgsCustom( 12 | contrastConf, 13 | command, 14 | argv, 15 | constants.commandLineDefinitions.auditOptionDefinitions 16 | ) 17 | const paramsAuth = paramHandler.getAuth(auditParameters) 18 | 19 | // @ts-ignore 20 | return { ...paramsAuth, ...auditParameters } 21 | } 22 | -------------------------------------------------------------------------------- /src/lambda/scanResults.ts: -------------------------------------------------------------------------------- 1 | import { getHttpClient } from '../utils/commonApi' 2 | import { CliError } from './cliError' 3 | import { ERRORS } from './constants' 4 | import { ApiParams } from './lambda' 5 | 6 | const getScanResults = async ( 7 | config: any, 8 | params: ApiParams, 9 | scanId: string, 10 | functionArn: string 11 | ) => { 12 | const client = getHttpClient(config) 13 | 14 | const { statusCode, body } = await client.getFunctionScanResults( 15 | config, 16 | params, 17 | scanId, 18 | functionArn 19 | ) 20 | 21 | if (statusCode === 200) { 22 | return body 23 | } 24 | 25 | const { errorCode } = body || {} 26 | throw new CliError(ERRORS.FAILED_TO_GET_RESULTS, { statusCode, errorCode }) 27 | } 28 | 29 | export { getScanResults } 30 | -------------------------------------------------------------------------------- /src/utils/requestUtils.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | const Promise = require('bluebird') 3 | 4 | Promise.promisifyAll(request) 5 | 6 | function sendRequest({ options, method = 'put' }) { 7 | return request[`${method}Async`](options.url, options) 8 | } 9 | 10 | const millisToSeconds = millis => { 11 | return (millis / 1000).toFixed(0) 12 | } 13 | 14 | const sleep = ms => { 15 | return new Promise(resolve => setTimeout(resolve, ms)) 16 | } 17 | 18 | const timeOutError = (ms, reject) => { 19 | return setTimeout(() => { 20 | reject(new Error(`No input detected after 30s`)) 21 | }, ms) 22 | } 23 | 24 | module.exports = { 25 | sendRequest: sendRequest, 26 | sleep: sleep, 27 | millisToSeconds: millisToSeconds, 28 | timeOutError: timeOutError 29 | } 30 | -------------------------------------------------------------------------------- /src/audit/report/models/reportLibraryModel.ts: -------------------------------------------------------------------------------- 1 | export class ReportLibraryModel { 2 | name: string 3 | cveArray: ReportCVEModel[] 4 | 5 | constructor(name: string, cveArray: ReportCVEModel[]) { 6 | this.name = name 7 | this.cveArray = cveArray 8 | } 9 | } 10 | 11 | export class ReportCVEModel { 12 | name?: string 13 | description?: string 14 | authentication?: string 15 | references?: [] 16 | severityCode?: string 17 | cvss3SeverityCode?: string 18 | 19 | constructor( 20 | name: string, 21 | description: string, 22 | severityCode: string, 23 | cvss3SeverityCode: string 24 | ) { 25 | this.name = name 26 | this.description = description 27 | this.severityCode = severityCode 28 | this.cvss3SeverityCode = cvss3SeverityCode 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/settingsHelper.js: -------------------------------------------------------------------------------- 1 | const commonApi = require('./commonApi') 2 | const { getMode } = require('./generalAPI') 3 | const { SAAS, MODE_BUILD } = require('../constants/constants') 4 | 5 | const getSettings = async config => { 6 | config.isEOP = (await getMode(config)).toUpperCase() === SAAS ? false : true 7 | config.mode = MODE_BUILD 8 | config.scaServices = await isSCAServicesAvailable(config) 9 | return config 10 | } 11 | 12 | const isSCAServicesAvailable = async config => { 13 | const client = commonApi.getHttpClient(config) 14 | return client 15 | .scaServiceIngests(config) 16 | .then(res => { 17 | return res.statusCode !== 403 18 | }) 19 | .catch(err => { 20 | console.log(err) 21 | }) 22 | } 23 | 24 | module.exports = { 25 | getSettings 26 | } 27 | -------------------------------------------------------------------------------- /src/scaAnalysis/repoMode/index.js: -------------------------------------------------------------------------------- 1 | const mavenParser = require('./mavenParser') 2 | const gradleParser = require('./gradleParser') 3 | const { determineProjectTypeAndCwd } = require('../java/analysis') 4 | 5 | const buildRepo = async (config, languageFiles) => { 6 | const project = determineProjectTypeAndCwd(languageFiles.JAVA, config) 7 | 8 | if (project.projectType === 'maven') { 9 | let jsonPomFile = mavenParser.readPomFile(project) 10 | mavenParser.parsePomFile(jsonPomFile) 11 | } else if (project.projectType === 'gradle') { 12 | const gradleJson = gradleParser.readBuildGradleFile(project) 13 | gradleParser.parseGradleJson(await gradleJson) 14 | } else { 15 | console.log('Unable to read project files.') 16 | } 17 | } 18 | 19 | module.exports = { 20 | buildRepo 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/filterProjectPath.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const child_process = require('child_process') 3 | 4 | function resolveFilePath(filepath) { 5 | if (filepath[0] === '~') { 6 | return path.join(process.env.HOME, filepath.slice(1)) 7 | } 8 | return filepath 9 | } 10 | 11 | const returnProjectPath = () => { 12 | if (process.platform == 'win32') { 13 | let winPath = child_process.execSync('cd').toString() 14 | return winPath.replace(/\//g, '\\').trim() 15 | } else if (process.env.PWD !== (undefined || null || 'undefined')) { 16 | return process.env.PWD 17 | } else { 18 | return process.argv[process.argv.indexOf('--file') + 1] 19 | } 20 | } 21 | 22 | module.exports = { 23 | returnProjectPath: returnProjectPath, 24 | resolveFilePath: resolveFilePath 25 | } 26 | -------------------------------------------------------------------------------- /src/audit/nodeAnalysisEngine/parseYarnLockFileContents.js: -------------------------------------------------------------------------------- 1 | const yarnParser = require('@yarnpkg/lockfile') 2 | const i18n = require('i18n') 3 | 4 | module.exports = exports = ({ language: { lockFilename }, node }, next) => { 5 | // If we never read the lock file then pass priority 6 | if (node.rawYarnLockFileContents === undefined || node.yarnVersion === 2) { 7 | next() 8 | } else { 9 | try { 10 | node.yarnLockFile = yarnParser.parse(node.rawYarnLockFileContents) 11 | } catch (err) { 12 | next( 13 | new Error( 14 | i18n.__( 15 | 'NodeParseYarn', 16 | lockFilename.lockFilePath ? lockFilename.lockFilePath : 'undefined' 17 | ) + `${err.message}` 18 | ) 19 | ) 20 | 21 | return 22 | } 23 | 24 | next() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/saveFile.js: -------------------------------------------------------------------------------- 1 | const { SARIF_FILE } = require('../constants/constants') 2 | const commonApi = require('./commonApi') 3 | const saveResults = require('../scan/saveResults') 4 | const i18n = require('i18n') 5 | 6 | const saveScanFile = async (config, scanResults) => { 7 | if (config.save === null || config.save.toUpperCase() === SARIF_FILE) { 8 | const scanId = scanResults.scanDetail.id 9 | const client = commonApi.getHttpClient(config) 10 | const rawResults = await client.getSpecificScanResultSarif(config, scanId) 11 | const name = await saveResults.writeResultsToFile(rawResults?.body) 12 | console.log(`Scan Results saved to ${name}`) 13 | } else { 14 | console.log(i18n.__('scanNoFiletypeSpecifiedForSave')) 15 | } 16 | } 17 | 18 | module.exports = { 19 | saveScanFile: saveScanFile 20 | } 21 | -------------------------------------------------------------------------------- /src/audit/dotnetAnalysisEngine/readLockFileContents.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const i18n = require('i18n') 3 | 4 | module.exports = exports = (analysis, next) => { 5 | const { 6 | language: { lockFilePath }, 7 | dotnet 8 | } = analysis 9 | 10 | // Make sure to check to see if there was a lock file detected as its not 11 | // required 12 | if (!lockFilePath) { 13 | next() 14 | return 15 | } 16 | 17 | // we're working on the assumtion that a dotNet project will only ever have one lock file 18 | //while other language may have more 19 | try { 20 | dotnet.rawLockFileContents = fs.readFileSync(lockFilePath) 21 | } catch (err) { 22 | next( 23 | new Error(i18n.__('dotnetReadLockfile', lockFilePath) + `${err.message}`) 24 | ) 25 | 26 | return 27 | } 28 | 29 | next() 30 | } 31 | -------------------------------------------------------------------------------- /src/audit/phpAnalysisEngine/readProjectFileContents.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const i18n = require('i18n') 3 | // all node references are because we're using that engine in the backend 4 | // so complying with the already existing node format 5 | module.exports = exports = (analysis, next) => { 6 | const { 7 | language: { projectFilePath }, 8 | php 9 | } = analysis 10 | 11 | try { 12 | php.composerJSON = JSON.parse(fs.readFileSync(projectFilePath, 'utf8')) 13 | php.composerJSON.dependencies = php.composerJSON.require 14 | php.composerJSON.devDependencies = php.composerJSON['require-dev'] 15 | } catch (err) { 16 | next( 17 | new Error( 18 | i18n.__('phpReadProjectFileError', projectFilePath) + `${err.message}` 19 | ) 20 | ) 21 | return 22 | } 23 | 24 | next() 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/parsedCLIOptions.js: -------------------------------------------------------------------------------- 1 | const commandLineArgs = require('command-line-args') 2 | const { sendTelemetryConfigAsConfObj } = require('../telemetry/telemetry') 3 | 4 | const getCommandLineArgsCustom = async ( 5 | contrastConf, 6 | command, 7 | parameterList, 8 | optionDefinitions 9 | ) => { 10 | try { 11 | return commandLineArgs(optionDefinitions, { 12 | argv: parameterList, 13 | partial: false, 14 | camelCase: true, 15 | caseInsensitive: true 16 | }) 17 | } catch (e) { 18 | await sendTelemetryConfigAsConfObj( 19 | contrastConf, 20 | command, 21 | parameterList, 22 | 'FAILURE', 23 | 'undefined' 24 | ) 25 | console.log(e.message.toString()) 26 | process.exit(1) 27 | } 28 | } 29 | 30 | module.exports = { 31 | getCommandLineArgsCustom 32 | } 33 | -------------------------------------------------------------------------------- /src/lambda/types.ts: -------------------------------------------------------------------------------- 1 | export enum StatusType { 2 | FAILED = 'failed', 3 | SUCCESS = 'success' 4 | } 5 | 6 | export enum EventType { 7 | START = 'start_command_session', 8 | END = 'end_command_session' 9 | } 10 | 11 | export type LambdaOptions = { 12 | functionName?: string 13 | listFunctions?: boolean 14 | region?: string 15 | endpointUrl?: string 16 | profile?: string 17 | help?: boolean 18 | verbose?: boolean 19 | jsonOutput?: boolean 20 | _unknown?: string[] 21 | } 22 | 23 | type ScanFunctionData = { 24 | functionArn: string 25 | scanId: string 26 | } 27 | 28 | export type AnalyticsOption = { 29 | sessionId: string 30 | eventType: EventType 31 | packageVersion: string 32 | arguments?: LambdaOptions 33 | scanFunctionData?: ScanFunctionData 34 | status?: StatusType 35 | errorMsg?: string 36 | } 37 | -------------------------------------------------------------------------------- /src/scaAnalysis/go/goAnalysis.js: -------------------------------------------------------------------------------- 1 | const { createGoTSMessage } = require('../common/formatMessage') 2 | const { 3 | parseDependenciesForSCAServices 4 | } = require('../common/scaParserForGoAndJava') 5 | const goReadDepFile = require('./goReadDepFile') 6 | const goParseDeps = require('./goParseDeps') 7 | 8 | const goAnalysis = config => { 9 | try { 10 | const rawGoDependencies = goReadDepFile.getGoDependencies(config) 11 | const parsedGoDependencies = 12 | goParseDeps.parseGoDependencies(rawGoDependencies) 13 | 14 | if (config.experimental) { 15 | return parseDependenciesForSCAServices(parsedGoDependencies) 16 | } else { 17 | return createGoTSMessage(parsedGoDependencies) 18 | } 19 | } catch (e) { 20 | console.log(e.message.toString()) 21 | } 22 | } 23 | 24 | module.exports = { 25 | goAnalysis 26 | } 27 | -------------------------------------------------------------------------------- /src/audit/report/models/reportOutputModel.ts: -------------------------------------------------------------------------------- 1 | export class ReportOutputModel { 2 | header: ReportOutputHeaderModel 3 | body: ReportOutputBodyModel 4 | 5 | constructor(header: ReportOutputHeaderModel, body: ReportOutputBodyModel) { 6 | this.header = header 7 | this.body = body 8 | } 9 | } 10 | 11 | export class ReportOutputHeaderModel { 12 | vulnMessage: string 13 | introducesMessage: string 14 | 15 | constructor(vulnMessage: string, introducesMessage: string) { 16 | this.vulnMessage = vulnMessage 17 | this.introducesMessage = introducesMessage 18 | } 19 | } 20 | 21 | export class ReportOutputBodyModel { 22 | issueMessage: string[] 23 | adviceMessage: string[] 24 | 25 | constructor(issueMessage: string[], adviceMessage: string[]) { 26 | this.issueMessage = issueMessage 27 | this.adviceMessage = adviceMessage 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/scaAnalysis/php/index.js: -------------------------------------------------------------------------------- 1 | const { readFile, parseProjectFiles } = require('./analysis') 2 | const { createPhpTSMessage } = require('../common/formatMessage') 3 | const { parsePHPLockFileForScaServices } = require('./phpNewServicesMapper') 4 | 5 | const phpAnalysis = config => { 6 | let analysis = readFiles(config) 7 | 8 | if (config.experimental) { 9 | return parsePHPLockFileForScaServices(analysis.rawLockFileContents) 10 | } else { 11 | const phpDep = parseProjectFiles(analysis) 12 | return createPhpTSMessage(phpDep) 13 | } 14 | } 15 | 16 | const readFiles = config => { 17 | let php = {} 18 | 19 | php.composerJSON = JSON.parse(readFile(config, 'composer.json')) 20 | 21 | php.rawLockFileContents = JSON.parse(readFile(config, 'composer.lock')) 22 | 23 | return php 24 | } 25 | 26 | module.exports = { 27 | phpAnalysis: phpAnalysis 28 | } 29 | -------------------------------------------------------------------------------- /src/audit/phpAnalysisEngine/index.js: -------------------------------------------------------------------------------- 1 | const AnalysisEngine = require('../AnalysisEngine') 2 | 3 | const readProjectFileContents = require('./readProjectFileContents') 4 | const readLockFileContents = require('./readLockFileContents') 5 | const parseLockFileContents = require('./parseLockFileContents') 6 | const sanitizer = require('./sanitizer') 7 | const i18n = require('i18n') 8 | 9 | module.exports = exports = (language, config, callback) => { 10 | const ae = new AnalysisEngine({ language, config, php: {} }) 11 | 12 | ae.use([ 13 | readProjectFileContents, 14 | readLockFileContents, 15 | parseLockFileContents, 16 | sanitizer 17 | ]) 18 | 19 | ae.analyze((err, analysis) => { 20 | if (err) { 21 | callback(new Error(i18n.__('phpAnalysisFailure') + `${err.message}`)) 22 | return 23 | } 24 | 25 | callback(null, analysis) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /src/lambda/__mocks__/aws.ts: -------------------------------------------------------------------------------- 1 | import { Lambda, FunctionConfiguration } from '@aws-sdk/client-lambda' 2 | import { LambdaOptions } from '../lambda' 3 | import lambdaConfig from './lambdaConfig.json' 4 | 5 | const getLambdaClient = (lambdaOptions: LambdaOptions) => { 6 | return {} 7 | } 8 | 9 | const getLambdaFunctionConfiguration = async ( 10 | client: Lambda, 11 | lambdaOptions: LambdaOptions 12 | ) => { 13 | return Promise.resolve(lambdaConfig) 14 | } 15 | 16 | const getLayersLinks = async ( 17 | client: Lambda, 18 | functionConfiguration: FunctionConfiguration 19 | ) => { 20 | return [] 21 | } 22 | const getLambdaPolicies = async ( 23 | functionConfiguration: FunctionConfiguration, 24 | lambdaOptions: LambdaOptions 25 | ) => [] 26 | 27 | export { 28 | getLambdaClient, 29 | getLambdaFunctionConfiguration, 30 | getLayersLinks, 31 | getLambdaPolicies 32 | } 33 | -------------------------------------------------------------------------------- /src/audit/nodeAnalysisEngine/readProjectFileContents.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const i18n = require('i18n') 3 | 4 | module.exports = exports = (analysis, next) => { 5 | const { 6 | language: { projectFilePath }, 7 | node 8 | } = analysis 9 | 10 | // Read the NODE project file contents. We are reading into memory presuming 11 | // that the contents of the file aren't large which may be bad... Could look 12 | // into streaming in the future 13 | 14 | try { 15 | // package.json is stored in the projectFilePath other files have the word lock so are stored in lockFilename arr 16 | node.packageJSON = JSON.parse(fs.readFileSync(projectFilePath, 'utf8')) 17 | } catch (err) { 18 | next( 19 | new Error( 20 | i18n.__('nodeReadProjectFileError', projectFilePath) + `${err.message}` 21 | ) 22 | ) 23 | return 24 | } 25 | 26 | next() 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/validationCheck.js: -------------------------------------------------------------------------------- 1 | const checkConfigHasRequiredValues = store => { 2 | return ( 3 | store.has('apiKey') && 4 | store.has('organizationId') && 5 | store.has('host') && 6 | store.has('authorization') && 7 | store.has('version') 8 | ) 9 | } 10 | 11 | const validateRequiredScanParams = params => { 12 | return ( 13 | params.apiKey && 14 | params.organizationId && 15 | params.host && 16 | params.authorization && 17 | params.version 18 | ) 19 | } 20 | 21 | const validateAuthParams = params => { 22 | return !!( 23 | params.apiKey && 24 | params.organizationId && 25 | params.host && 26 | params.authorization 27 | ) 28 | } 29 | 30 | module.exports = { 31 | checkConfigHasRequiredValues: checkConfigHasRequiredValues, 32 | validateAuthParams: validateAuthParams, 33 | validateRequiredScanParams: validateRequiredScanParams 34 | } 35 | -------------------------------------------------------------------------------- /src/lambda/arn.ts: -------------------------------------------------------------------------------- 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 = 14 | /arn:(?[^:\n]*):(?[^:\n]*):(?[^:\n]*):(?[^:\n]*):(?(?[^:/\n]*)[:/])?(?.*)/ 15 | 16 | const parseARN = (arn: string | undefined) => { 17 | if (!arn) { 18 | throw new CliError(ERRORS.FAILED_TO_START_SCAN, { 19 | errorCode: 'failedToParseArn' 20 | }) 21 | } 22 | 23 | const arnMatch = arn.match(ARN_REGEX) 24 | if (!arnMatch) { 25 | throw new CliError(ERRORS.FAILED_TO_START_SCAN, { 26 | errorCode: 'failedToParseArn' 27 | }) 28 | } 29 | 30 | return arnMatch.groups as ARN 31 | } 32 | 33 | export { parseARN, ARN } 34 | -------------------------------------------------------------------------------- /src/audit/pythonAnalysisEngine/parseProjectFileContents.js: -------------------------------------------------------------------------------- 1 | const multiReplace = require('string-multiple-replace') 2 | const i18n = require('i18n') 3 | 4 | module.exports = exports = ({ python }, next) => { 5 | const { rawProjectFileContents } = python 6 | 7 | try { 8 | const matcherObj = { '"': '' } 9 | const sequencer = ['"'] 10 | const parsedPipfile = multiReplace( 11 | rawProjectFileContents, 12 | matcherObj, 13 | sequencer 14 | ) 15 | 16 | const pythonArray = parsedPipfile.split('\n') 17 | 18 | python.pipfilDependanceies = pythonArray.filter(element => { 19 | return element != '' && !element.includes('#') 20 | }) 21 | 22 | next() 23 | } catch (err) { 24 | next( 25 | new Error( 26 | i18n.__('pythonAnalysisParseProjectFileError', rawProjectFileContents) + 27 | `${err.message}` 28 | ) 29 | ) 30 | 31 | return 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/audit/languageAnalysisEngine/getProjectRootFilenames.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const i18n = require('i18n') 4 | 5 | const getDirectoryFromPathGiven = file => { 6 | let projectStats = getProjectStats(file) 7 | 8 | if (projectStats.isFile()) { 9 | let newPath = path.resolve(file) 10 | return path.dirname(newPath) 11 | } 12 | 13 | if (projectStats.isDirectory()) { 14 | return file 15 | } 16 | } 17 | 18 | const getProjectStats = file => { 19 | try { 20 | //might not need this 21 | if (file.endsWith('/')) { 22 | file = file.slice(0, -1) 23 | } 24 | return fs.statSync(file) 25 | } catch (err) { 26 | throw new Error( 27 | i18n.__('languageAnalysisProjectRootFileNameFailure', file) + 28 | `${err.message}` 29 | ) 30 | } 31 | } 32 | 33 | module.exports = { 34 | getProjectStats, 35 | getDirectoryFromPathGiven: getDirectoryFromPathGiven 36 | } 37 | -------------------------------------------------------------------------------- /src/audit/dotnetAnalysisEngine/index.js: -------------------------------------------------------------------------------- 1 | const AnalysisEngine = require('../AnalysisEngine') 2 | const readProjectFileContents = require('./readProjectFileContents') 3 | const parseProjectFileContents = require('./parseProjectFileContents') 4 | const readLockFileContents = require('./readLockFileContents') 5 | const parseLockFileContents = require('./parseLockFileContents') 6 | const sanitizer = require('./sanitizer') 7 | const i18n = require('i18n') 8 | 9 | module.exports = exports = (language, config, callback) => { 10 | const ae = new AnalysisEngine({ language, config, dotnet: {} }) 11 | ae.use([ 12 | readProjectFileContents, 13 | parseProjectFileContents, 14 | readLockFileContents, 15 | parseLockFileContents, 16 | sanitizer 17 | ]) 18 | 19 | ae.analyze((err, analysis) => { 20 | if (err) { 21 | callback(new Error(i18n.__('dotnetAnalysisFailure') + err.message)) 22 | return 23 | } 24 | callback(null, analysis) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /src/audit/languageAnalysisEngine/report/checkIgnoreDevDep.js: -------------------------------------------------------------------------------- 1 | const { 2 | getGlobalProperties, 3 | getFeatures, 4 | isFeatureEnabled 5 | } = require('../util/generalAPI') 6 | const { CLI_IGNORE_DEV_DEPS } = require('../util/capabilities') 7 | 8 | const checkDevDeps = async config => { 9 | const shouldIgnoreDev = config.ignoreDev 10 | const globalProperties = await getGlobalProperties() 11 | 12 | // returns [ 'CLI_IGNORE_DEV_DEPS' ] if teamserver version is above 3.8.1 13 | const features = getFeatures(globalProperties.internal_version) 14 | 15 | // providing user is on version >= 3.8.1, isfeatureEnabled will always return true, 16 | // therefore shouldIgnoreDev flag (from params) is needed to disable ignore dev deps 17 | const isfeatureEnabled = isFeatureEnabled(features, CLI_IGNORE_DEV_DEPS) 18 | let ignoreDevUrl = false 19 | if (shouldIgnoreDev) { 20 | ignoreDevUrl = isfeatureEnabled 21 | } 22 | return ignoreDevUrl 23 | } 24 | 25 | module.exports = { 26 | checkDevDeps 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/audit/processAudit.js: -------------------------------------------------------------------------------- 1 | const auditConfig = require('./auditConfig') 2 | const { auditUsageGuide } = require('./help') 3 | const scaController = require('../scan/sca/scaAnalysis') 4 | const { sendTelemetryConfigAsObject } = require('../../telemetry/telemetry') 5 | const { postRunMessage } = require('../../common/commonHelp') 6 | 7 | const processAudit = async (contrastConf, argvMain) => { 8 | if (argvMain.indexOf('--help') !== -1) { 9 | printHelpMessage() 10 | process.exit(0) 11 | } 12 | 13 | const config = await auditConfig.getAuditConfig( 14 | contrastConf, 15 | 'audit', 16 | argvMain 17 | ) 18 | await scaController.processSca(config) 19 | if (!config.fingerprint) { 20 | postRunMessage('audit') 21 | await sendTelemetryConfigAsObject( 22 | config, 23 | 'audit', 24 | argvMain, 25 | 'SUCCESS', 26 | config.language 27 | ) 28 | } 29 | } 30 | 31 | const printHelpMessage = () => { 32 | console.log(auditUsageGuide) 33 | } 34 | 35 | module.exports = { 36 | processAudit 37 | } 38 | -------------------------------------------------------------------------------- /src/audit/rubyAnalysisEngine/parsedGemfile.js: -------------------------------------------------------------------------------- 1 | const i18n = require('i18n') 2 | 3 | module.exports = exports = ({ ruby }, next) => { 4 | const { rawProjectFileContents } = ruby 5 | 6 | // Read the ruby requirements file contents. 7 | 8 | try { 9 | const rubyArray = rawProjectFileContents.split('\n') 10 | 11 | //give me the gemfiles 12 | let filteredRubyDep = rubyArray.filter(element => { 13 | return ( 14 | !element.includes('#') && 15 | element.includes('gem') && 16 | !element.includes('source') 17 | ) 18 | }) 19 | 20 | //trim off whitespace 21 | for (let i = 0; i < filteredRubyDep.length; i++) { 22 | filteredRubyDep[i] = filteredRubyDep[i].trim() 23 | } 24 | 25 | ruby.gemfilesDependanceies = filteredRubyDep 26 | 27 | next() 28 | } catch (err) { 29 | next( 30 | new Error( 31 | i18n.__( 32 | 'rubyAnalysisEngineParsedGemFileError', 33 | rawProjectFileContents 34 | ) + `${err.message}` 35 | ) 36 | ) 37 | return 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/getConfig.ts: -------------------------------------------------------------------------------- 1 | import Conf from 'conf' 2 | import { CE_URL } from '../constants/constants' 3 | 4 | type ContrastConfOptions = Partial<{ 5 | version: string 6 | host: string 7 | apiKey: string 8 | orgId: string 9 | authHeader: string 10 | numOfRuns: number 11 | javaAgreement: boolean 12 | }> 13 | 14 | type ContrastConf = Conf 15 | 16 | const localConfig = (name: string, version: string) => { 17 | const config: ContrastConf = new Conf({ 18 | configName: name 19 | }) 20 | config.set('version', version) 21 | 22 | if (!config.has('host')) { 23 | config.set('host', CE_URL) 24 | } 25 | return config 26 | } 27 | 28 | const setConfigValues = (config: ContrastConf, values: ContrastConfOptions) => { 29 | config.set('apiKey', values.apiKey) 30 | config.set('organizationId', values.orgId) 31 | config.set('authorization', values.authHeader) 32 | values.host ? config.set('host', values.host) : null 33 | } 34 | 35 | export { localConfig, setConfigValues, ContrastConf, ContrastConfOptions } 36 | -------------------------------------------------------------------------------- /src/audit/goAnalysisEngine/readProjectFileContents.js: -------------------------------------------------------------------------------- 1 | const child_process = require('child_process') 2 | const i18n = require('i18n') 3 | 4 | module.exports = exports = async ( 5 | { language: { projectFilePath }, go }, 6 | next 7 | ) => { 8 | let cmdStdout 9 | let cwd 10 | try { 11 | cwd = projectFilePath.replace('go.mod', '') 12 | // A sample of this output can be found 13 | // in the go test folder data/goModGraphResults.text 14 | cmdStdout = child_process.execSync('go mod graph', { cwd }) 15 | 16 | go.modGraphOutput = cmdStdout.toString() 17 | 18 | next() 19 | } catch (err) { 20 | if (err.message === 'spawnSync /bin/sh ENOENT') { 21 | err.message = 22 | '\n\n*************** No transitive dependencies ***************\n\nWe are unable to build a dependency tree view from your repository as there were no transitive dependencies found.' 23 | } 24 | next( 25 | new Error( 26 | i18n.__('goReadProjectFile', cwd, `${err.message ? err.message : ''}`) 27 | ) 28 | ) 29 | return 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/audit/dotnetAnalysisEngine/parseProjectFileContents.js: -------------------------------------------------------------------------------- 1 | const xml2js = require('xml2js') 2 | const i18n = require('i18n') 3 | 4 | module.exports = exports = ( 5 | { language: { projectFilePath }, dotnet }, 6 | next 7 | ) => { 8 | const { rawProjectFileContents } = dotnet 9 | 10 | // Read the .NET project file contents. We are reading into memory presuming 11 | // that the contents of the file aren't large which may be bad... Could look 12 | // into streaming in the future 13 | // explicitArray: false - to not abuse of arrays, with this option we are able to read JSON properties in an easier way 14 | // mergeAttrs: true - to merge attributes and child elements as properties of the parent 15 | const parser = new xml2js.Parser({ explicitArray: false, mergeAttrs: true }) 16 | parser.parseString(rawProjectFileContents, (err, projectFileXML) => { 17 | if (err) { 18 | next( 19 | new Error(i18n.__('dotnetParseProjectFile', projectFilePath) + `${err}`) 20 | ) 21 | 22 | return 23 | } 24 | 25 | dotnet.projectFile = projectFileXML 26 | 27 | next() 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/sbom/generateSbom.ts: -------------------------------------------------------------------------------- 1 | import { getHttpClient } from '../utils/commonApi' 2 | 3 | export const generateSbom = (config: any, type: string) => { 4 | const client = getHttpClient(config) 5 | return client 6 | .getSbom(config, type) 7 | .then((res: { statusCode: number; body: any }) => { 8 | if (res.statusCode === 200) { 9 | return res.body 10 | } else { 11 | console.log('Unable to retrieve Software Bill of Materials (SBOM)') 12 | } 13 | }) 14 | .catch((err: any) => { 15 | console.log(err) 16 | }) 17 | } 18 | 19 | export const generateSCASbom = ( 20 | config: any, 21 | type: string, 22 | reportId: string 23 | ) => { 24 | const client = getHttpClient(config) 25 | return client 26 | .getSCASbom(config, type, reportId) 27 | .then((res: { statusCode: number; body: any }) => { 28 | if (res.statusCode === 200) { 29 | return res.body 30 | } else { 31 | console.log('Unable to retrieve Software Bill of Materials (SBOM)') 32 | } 33 | }) 34 | .catch((err: any) => { 35 | console.log(err) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /src/scaAnalysis/go/goReadDepFile.js: -------------------------------------------------------------------------------- 1 | const child_process = require('child_process') 2 | const i18n = require('i18n') 3 | 4 | const getGoDependencies = config => { 5 | let cmdStdout 6 | let cwd = config.file ? config.file.replace('go.mod', '') : process.cwd() 7 | 8 | try { 9 | // A sample of this output can be found 10 | // in the go test folder data/goModGraphResults.text 11 | cmdStdout = child_process.execSync('go mod graph', { cwd }) 12 | 13 | return cmdStdout.toString() 14 | } catch (err) { 15 | if (err.message === 'spawnSync /bin/sh ENOENT') { 16 | err.message = 17 | '\n\n*************** No transitive dependencies ***************\n\nWe are unable to build a dependency tree view from your repository as there were no transitive dependencies found.' 18 | } 19 | console.log( 20 | i18n.__('goReadProjectFile', cwd, `${err.message ? err.message : ''}`) 21 | ) 22 | // throw new Error( 23 | // i18n.__('goReadProjectFile', cwd, `${err.message ? err.message : ''}`) 24 | // ) 25 | } 26 | } 27 | 28 | module.exports = { 29 | getGoDependencies 30 | } 31 | -------------------------------------------------------------------------------- /src/audit/rubyAnalysisEngine/index.js: -------------------------------------------------------------------------------- 1 | const AnalysisEngine = require('./../AnalysisEngine') 2 | 3 | const readGemfileContents = require('./readGemfileContents') 4 | const readGemfileLockContents = require('./readGemfileLockContents') 5 | const parsedGemfile = require('./parsedGemfile') 6 | const parseGemfileLockFileContents = require('./parseGemfileLockContents') 7 | const sanitizer = require('./sanitizer') 8 | const i18n = require('i18n') 9 | 10 | module.exports = exports = (language, config, callback) => { 11 | const ae = new AnalysisEngine({ language, config, ruby: {} }) 12 | 13 | // Ruby dependency management is mostly handled by bundler which creates 14 | // Gemfile and Gemfile.lock. This project will only pick up on those 15 | ae.use([ 16 | readGemfileContents, 17 | parsedGemfile, 18 | readGemfileLockContents, 19 | parseGemfileLockFileContents, 20 | sanitizer 21 | ]) 22 | 23 | ae.analyze((err, analysis) => { 24 | if (err) { 25 | callback(new Error(i18n.__('rubyAnalysisEngineError') + `${err.message}`)) 26 | return 27 | } 28 | callback(null, analysis) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /src/audit/nodeAnalysisEngine/readYarnLockFileContents.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const yaml = require('js-yaml') 3 | const i18n = require('i18n') 4 | 5 | module.exports = exports = ({ language: { lockFilePath }, node }, next) => { 6 | // check if the lockFilePath is populated and if it is check to 7 | // see if it has the package-lock if not then go on to next handler 8 | if (!lockFilePath || !lockFilePath.includes('yarn.lock')) { 9 | next() 10 | return 11 | } 12 | 13 | try { 14 | node.rawYarnLockFileContents = fs.readFileSync(lockFilePath, 'utf8') 15 | node.yarnVersion = 1 16 | 17 | if ( 18 | !node.rawYarnLockFileContents.includes('lockfile v1') || 19 | node.rawYarnLockFileContents.includes('__metadata') 20 | ) { 21 | node.rawYarnLockFileContents = yaml.load( 22 | fs.readFileSync(lockFilePath, 'utf8') 23 | ) 24 | node.yarnVersion = 2 25 | } 26 | } catch (err) { 27 | next( 28 | new Error( 29 | i18n.__('nodeReadYarnLockFileError', lockFilePath) + `${err.message}` 30 | ) 31 | ) 32 | 33 | return 34 | } 35 | next() 36 | } 37 | -------------------------------------------------------------------------------- /src/audit/report/models/reportListModel.ts: -------------------------------------------------------------------------------- 1 | import { ReportSeverityModel } from './reportSeverityModel' 2 | import { ReportCVEModel } from './reportLibraryModel' 3 | 4 | export class ReportList { 5 | reportOutputList: ReportModelStructure[] 6 | 7 | constructor() { 8 | this.reportOutputList = [] 9 | } 10 | } 11 | 12 | export class ReportModelStructure { 13 | compositeKey: ReportCompositeKey 14 | cveArray: ReportCVEModel[] 15 | 16 | constructor(compositeKey: ReportCompositeKey, cveArray: ReportCVEModel[]) { 17 | this.compositeKey = compositeKey 18 | this.cveArray = cveArray 19 | } 20 | } 21 | 22 | export class ReportCompositeKey { 23 | libraryName!: string 24 | libraryVersion!: string 25 | highestSeverity!: ReportSeverityModel 26 | numberOfSeverities!: number 27 | 28 | constructor( 29 | libraryName: string, 30 | libraryVersion: string, 31 | highestSeverity: ReportSeverityModel, 32 | numberOfSeverities: number 33 | ) { 34 | this.libraryName = libraryName 35 | this.libraryVersion = libraryVersion 36 | this.highestSeverity = highestSeverity 37 | this.numberOfSeverities = numberOfSeverities 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | =========== 3 | 4 | Copyright (c) 2022 Contrast Security, Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/utils/paramsUtil/paramHandler.js: -------------------------------------------------------------------------------- 1 | const commandlineAuth = require('./commandlineParams') 2 | const configStoreParams = require('./configStoreParams') 3 | const envVariableParams = require('./envVariableParams') 4 | const { validateAuthParams } = require('../validationCheck') 5 | const i18n = require('i18n') 6 | 7 | const getAuth = params => { 8 | let commandLineAuthParamsAuth = commandlineAuth.getAuth(params) 9 | let envVariableParamsAuth = envVariableParams.getAuth() 10 | let configStoreParamsAuth = configStoreParams.getAuth() 11 | 12 | if (validateAuthParams(commandLineAuthParamsAuth)) { 13 | return commandLineAuthParamsAuth 14 | } else if (validateAuthParams(envVariableParamsAuth)) { 15 | return envVariableParamsAuth 16 | } else if (validateAuthParams(configStoreParamsAuth)) { 17 | return configStoreParamsAuth 18 | } else { 19 | console.log(i18n.__('configNotFound')) 20 | process.exit(1) 21 | } 22 | } 23 | 24 | const getAgreement = () => { 25 | return configStoreParams.getAgreement() 26 | } 27 | 28 | const setAgreement = answer => { 29 | return configStoreParams.setAgreement(answer) 30 | } 31 | 32 | module.exports = { getAuth, getAgreement, setAgreement } 33 | -------------------------------------------------------------------------------- /src/commands/config/config.js: -------------------------------------------------------------------------------- 1 | const parsedCLIOptions = require('../../utils/parsedCLIOptions') 2 | const constants = require('../../cliConstants') 3 | const commandLineUsage = require('command-line-usage') 4 | const i18n = require('i18n') 5 | 6 | const processConfig = async (argv, config) => { 7 | try { 8 | let configParams = await parsedCLIOptions.getCommandLineArgsCustom( 9 | config, 10 | 'config', 11 | argv, 12 | constants.commandLineDefinitions.configOptionDefinitions 13 | ) 14 | if (configParams.help) { 15 | console.log(configUsageGuide) 16 | process.exit(0) 17 | } 18 | if (configParams.clear) { 19 | config.clear() 20 | } else { 21 | console.log(JSON.parse(JSON.stringify(config.store))) 22 | } 23 | } catch (e) { 24 | //handle unknown command inputs 25 | console.log(e.message.toString()) 26 | } 27 | } 28 | 29 | const configUsageGuide = commandLineUsage([ 30 | { 31 | header: i18n.__('configHeader') 32 | }, 33 | { 34 | content: [i18n.__('constantsConfigUsageContents')], 35 | optionList: constants.commandLineDefinitions.configOptionDefinitions 36 | } 37 | ]) 38 | 39 | module.exports = { 40 | processConfig: processConfig 41 | } 42 | -------------------------------------------------------------------------------- /src/audit/languageAnalysisEngine/checkForMultipleIdentifiedLanguages.js: -------------------------------------------------------------------------------- 1 | const i18n = require('i18n') 2 | /** 3 | * Checks that the list of languages and files that has been reduced doesn't 4 | * contain more than one identified language. 5 | */ 6 | module.exports = exports = (analysis, next) => { 7 | const { languageAnalysis } = analysis 8 | try { 9 | checkForMultipleIdentifiedLanguages(languageAnalysis.identifiedLanguages) 10 | } catch (err) { 11 | next(err) 12 | return 13 | } 14 | next() 15 | } 16 | 17 | const checkForMultipleIdentifiedLanguages = identifiedLanguages => { 18 | if (Object.keys(identifiedLanguages).length > 1) { 19 | // Handle the error case where multiple languages have been identified 20 | let errMsg = i18n.__('languageAnalysisMultipleLanguages1') 21 | 22 | for (const [language, { projectFilenames }] of Object.entries( 23 | identifiedLanguages 24 | )) { 25 | errMsg += `\t${language}: ${projectFilenames.join(', ')}\n` 26 | } 27 | 28 | errMsg += i18n.__('languageAnalysisMultipleLanguages2', "'project_path'") 29 | 30 | throw new Error(errMsg) 31 | } 32 | } 33 | 34 | //For testing purposes 35 | exports.checkForMultipleIdentifiedLanguages = checkForMultipleIdentifiedLanguages 36 | -------------------------------------------------------------------------------- /src/scan/help.js: -------------------------------------------------------------------------------- 1 | const commandLineUsage = require('command-line-usage') 2 | const i18n = require('i18n') 3 | const constants = require('../cliConstants') 4 | const { commonHelpLinks } = require('../common/commonHelp') 5 | 6 | const scanUsageGuide = commandLineUsage([ 7 | { 8 | header: i18n.__('scanHeader') 9 | }, 10 | { 11 | header: i18n.__('constantsPrerequisitesHeader'), 12 | content: [ 13 | '{bold ' + i18n.__('constantsPrerequisitesContentScanLanguages') + '}', 14 | i18n.__('constantsPrerequisitesContent'), 15 | '', 16 | i18n.__('constantsUsageCommandInfo'), 17 | i18n.__('constantsUsageCommandInfo24Hours') 18 | ] 19 | }, 20 | { 21 | header: i18n.__('constantsScanOptions'), 22 | optionList: constants.commandLineDefinitions.scanOptionDefinitions, 23 | hide: [ 24 | 'project-id', 25 | 'organization-id', 26 | 'api-key', 27 | 'authorization', 28 | 'host', 29 | 'proxy', 30 | 'help', 31 | 'ff', 32 | 'ignore-cert-errors', 33 | 'verbose', 34 | 'debug', 35 | 'experimental', 36 | 'application-name' 37 | ] 38 | }, 39 | commonHelpLinks()[0], 40 | commonHelpLinks()[1] 41 | ]) 42 | 43 | module.exports = { 44 | scanUsageGuide 45 | } 46 | -------------------------------------------------------------------------------- /src/audit/languageAnalysisEngine/util/generalAPI.js: -------------------------------------------------------------------------------- 1 | const { featuresTeamServer } = require('./capabilities') 2 | const semver = require('semver') 3 | const { handleResponseErrors } = require('../../../common/errorHandling') 4 | const { getHttpClient } = require('../../../utils/commonApi') 5 | 6 | const getGlobalProperties = async config => { 7 | const client = getHttpClient(config) 8 | 9 | return client 10 | .getGlobalProperties(config) 11 | .then(res => { 12 | if (res.statusCode === 200) { 13 | return res.body 14 | } else { 15 | handleResponseErrors(res, 'globalProperties') 16 | } 17 | }) 18 | .catch(err => { 19 | console.log(err) 20 | }) 21 | } 22 | 23 | const getFeatures = version => { 24 | const featuresEnabled = [] 25 | 26 | featuresTeamServer.forEach(feature => { 27 | const versionFrom = Object.values(feature)[0] 28 | return semver.gte(version, versionFrom) 29 | ? featuresEnabled.push(Object.keys(feature)[0]) 30 | : null 31 | }) 32 | return featuresEnabled 33 | } 34 | 35 | const isFeatureEnabled = (features, featureName) => { 36 | return features.includes(featureName) 37 | } 38 | 39 | module.exports = { 40 | getGlobalProperties, 41 | getFeatures, 42 | isFeatureEnabled 43 | } 44 | -------------------------------------------------------------------------------- /src/commands/audit/processAudit.ts: -------------------------------------------------------------------------------- 1 | import { getAuditConfig } from './auditConfig' 2 | import { auditUsageGuide } from './help' 3 | import { processSca } from '../scan/sca/scaAnalysis' 4 | import { sendTelemetryConfigAsObject } from '../../telemetry/telemetry' 5 | import { ContrastConf } from '../../utils/getConfig' 6 | import chalk from 'chalk' 7 | 8 | export type parameterInput = string[] 9 | 10 | export const processAudit = async ( 11 | contrastConf: ContrastConf, 12 | argv: parameterInput 13 | ) => { 14 | if (argv.indexOf('--help') != -1) { 15 | printHelpMessage() 16 | process.exit(0) 17 | } 18 | 19 | const config = await getAuditConfig(contrastConf, 'audit', argv) 20 | await processSca(config) 21 | postRunMessage() 22 | await sendTelemetryConfigAsObject( 23 | config, 24 | 'audit', 25 | argv, 26 | 'SUCCESS', 27 | // @ts-ignore 28 | config.language 29 | ) 30 | } 31 | 32 | const printHelpMessage = () => { 33 | console.log(auditUsageGuide) 34 | } 35 | 36 | const postRunMessage = () => { 37 | console.log('\n' + chalk.underline.bold('Other Codesec Features:')) 38 | console.log("'contrast scan' to run CodeSec’s industry leading SAST scanner") 39 | console.log("'contrast lambda' to secure your AWS serverless functions\n") 40 | } 41 | -------------------------------------------------------------------------------- /src/scaAnalysis/common/treeUpload.js: -------------------------------------------------------------------------------- 1 | const commonApi = require('../../utils/commonApi') 2 | const { APP_VERSION } = require('../../constants/constants') 3 | 4 | const commonSendSnapShot = async (analysis, config) => { 5 | let requestBody = {} 6 | config.experimental === true 7 | ? (requestBody = sendToSCAServices(config, analysis)) 8 | : (requestBody = { 9 | appID: config.applicationId, 10 | cliVersion: APP_VERSION, 11 | snapshot: analysis 12 | }) 13 | 14 | const client = commonApi.getHttpClient(config) 15 | return client 16 | .sendSnapshot(requestBody, config) 17 | .then(res => { 18 | if (res.statusCode === 201) { 19 | return res.body 20 | } else { 21 | throw new Error(res.statusCode + ` error processing dependencies`) 22 | } 23 | }) 24 | .catch(err => { 25 | throw err 26 | }) 27 | } 28 | 29 | const sendToSCAServices = (config, analysis) => { 30 | return { 31 | applicationId: config.applicationId, 32 | dependencyTree: analysis, 33 | organizationId: config.organizationId, 34 | language: config.language, 35 | tool: { 36 | name: 'Contrast Codesec', 37 | version: APP_VERSION 38 | } 39 | } 40 | } 41 | 42 | module.exports = { 43 | commonSendSnapShot 44 | } 45 | -------------------------------------------------------------------------------- /src/audit/languageAnalysisEngine/getIdentifiedLanguageInfo.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | /** 4 | * Assemble analysis results into a common object to provide 5 | * language, project file name and paths 6 | */ 7 | module.exports = exports = (analysis, next) => { 8 | const { projectPath, languageAnalysis } = analysis 9 | languageAnalysis.identifiedLanguageInfo = getIdentifiedLanguageInfo( 10 | projectPath, 11 | languageAnalysis.identifiedLanguages 12 | ) 13 | next() 14 | } 15 | 16 | const getIdentifiedLanguageInfo = (projectPath, identifiedLanguages) => { 17 | const [language] = Object.keys(identifiedLanguages) 18 | const { 19 | projectFilenames: [projectFilename], 20 | lockFilenames: [lockFilename] 21 | } = Object.values(identifiedLanguages)[0] 22 | 23 | let identifiedLanguageInfo = { 24 | language, 25 | projectFilename, 26 | projectFilePath: path.join(projectPath, projectFilename) 27 | } 28 | 29 | if (lockFilename) { 30 | identifiedLanguageInfo = { 31 | ...identifiedLanguageInfo, 32 | lockFilename, 33 | lockFilePath: path.join(projectPath, lockFilename) 34 | } 35 | } 36 | 37 | return identifiedLanguageInfo 38 | } 39 | 40 | //For testing purposes 41 | exports.getIdentifiedLanguageInfo = getIdentifiedLanguageInfo 42 | -------------------------------------------------------------------------------- /src/audit/languageAnalysisEngine/checkIdentifiedLanguageHasProjectFile.js: -------------------------------------------------------------------------------- 1 | const i18n = require('i18n') 2 | /** 3 | * Checks that a single identified language in the list of languages and files 4 | * that has been reduced has a single project file. This is important in the 5 | * (uncommon) case that a project has a lock file without a project file. 6 | */ 7 | module.exports = exports = (analysis, next) => { 8 | const { languageAnalysis } = analysis 9 | try { 10 | checkIdentifiedLanguageHasProjectFile(languageAnalysis.identifiedLanguages) 11 | } catch (err) { 12 | next(err) 13 | return 14 | } 15 | next() 16 | } 17 | 18 | const checkIdentifiedLanguageHasProjectFile = identifiedLanguages => { 19 | // Handle the error case where only a single language has been identified... 20 | if (Object.keys(identifiedLanguages).length == 1) { 21 | let { projectFilenames } = Object.values(identifiedLanguages)[0] 22 | 23 | // ...but no project files for that language have been found 24 | if (projectFilenames.length == 0) { 25 | const [language] = Object.keys(identifiedLanguages) 26 | throw new Error(i18n.__('languageAnalysisProjectFileError', language)) 27 | } 28 | } 29 | } 30 | 31 | //For testing purposes 32 | exports.checkIdentifiedLanguageHasProjectFile = checkIdentifiedLanguageHasProjectFile 33 | -------------------------------------------------------------------------------- /src/audit/nodeAnalysisEngine/index.js: -------------------------------------------------------------------------------- 1 | const AnalysisEngine = require('../AnalysisEngine') 2 | 3 | const readProjectFileContents = require('./readProjectFileContents') 4 | const readNPMLockFileContents = require('./readNPMLockFileContents') 5 | const parseNPMLockFileContents = require('./parseNPMLockFileContents') 6 | const readYarnLockFileContents = require('./readYarnLockFileContents') 7 | const parseYarnLockFileContents = require('./parseYarnLockFileContents') 8 | const parseYarn2LockFileContents = require('./parseYarn2LockFileContents') 9 | const handleNPMLockFileV2 = require('./handleNPMLockFileV2') 10 | const sanitizer = require('./sanitizer') 11 | const i18n = require('i18n') 12 | 13 | module.exports = exports = (language, config, callback) => { 14 | const ae = new AnalysisEngine({ language, config, node: {} }) 15 | 16 | ae.use([ 17 | readProjectFileContents, 18 | readNPMLockFileContents, 19 | parseNPMLockFileContents, 20 | readYarnLockFileContents, 21 | parseYarnLockFileContents, 22 | parseYarn2LockFileContents, 23 | handleNPMLockFileV2, 24 | sanitizer 25 | ]) 26 | 27 | ae.analyze((err, analysis) => { 28 | if (err) { 29 | callback(new Error(i18n.__('NodeAnalysisFailure') + `${err.message}`)) 30 | return 31 | } 32 | 33 | callback(null, analysis) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/paramsUtil/configStoreParams.js: -------------------------------------------------------------------------------- 1 | const validationCheck = require('../validationCheck') 2 | const commonApi = require('../commonApi') 3 | const config = require('../getConfig') 4 | const { APP_NAME, APP_VERSION } = require('../../constants/constants') 5 | 6 | const getAuth = () => { 7 | const ContrastConf = config.localConfig(APP_NAME, APP_VERSION) 8 | let ContrastConfToUse = {} 9 | if (validationCheck.checkConfigHasRequiredValues(ContrastConf)) { 10 | ContrastConfToUse.apiKey = ContrastConf.get('apiKey') 11 | ContrastConfToUse.organizationId = ContrastConf.get('organizationId') 12 | ContrastConfToUse.host = commonApi.getValidHost(ContrastConf.get('host')) 13 | ContrastConfToUse.authorization = ContrastConf.get('authorization') 14 | ContrastConfToUse.version = ContrastConf.get('version') 15 | } 16 | return ContrastConfToUse 17 | } 18 | 19 | const getAgreement = () => { 20 | const ContrastConf = config.localConfig(APP_NAME, APP_VERSION) 21 | let ContrastConfToUse = {} 22 | ContrastConfToUse.javaAgreement = ContrastConf.get('javaAgreement') 23 | return ContrastConfToUse 24 | } 25 | 26 | const setAgreement = agreement => { 27 | const ContrastConf = config.localConfig(APP_NAME, APP_VERSION) 28 | ContrastConf.set('javaAgreement', agreement) 29 | return agreement 30 | } 31 | 32 | module.exports = { getAuth, getAgreement, setAgreement } 33 | -------------------------------------------------------------------------------- /src/common/commonHelp.js: -------------------------------------------------------------------------------- 1 | const i18n = require('i18n') 2 | const chalk = require('chalk') 3 | 4 | const commonHelpLinks = () => { 5 | return [ 6 | { 7 | header: i18n.__('commonHelpHeader'), 8 | content: [ 9 | i18n.__('commonHelpCheckOutHeader') + i18n.__('commonHelpCheckOutText'), 10 | i18n.__('commonHelpLearnMoreHeader') + 11 | i18n.__('commonHelpLearnMoreText'), 12 | i18n.__('commonHelpJoinDiscussionHeader') + 13 | i18n.__('commonHelpJoinDiscussionText') 14 | ] 15 | }, 16 | { 17 | header: i18n.__('commonHelpEnterpriseHeader'), 18 | content: [ 19 | i18n.__('commonHelpLearnMoreEnterpriseHeader') + 20 | i18n.__('commonHelpLearnMoreEnterpriseText') 21 | ] 22 | } 23 | ] 24 | } 25 | 26 | const postRunMessage = commandName => { 27 | console.log('\n' + chalk.underline.bold('Other Features:')) 28 | if (commandName !== 'scan') 29 | console.log( 30 | "'contrast scan' to run Contrasts’ industry leading SAST scanner" 31 | ) 32 | if (commandName !== 'audit') 33 | console.log( 34 | "'contrast audit' to find vulnerabilities in your open source dependencies" 35 | ) 36 | if (commandName !== 'lambda') 37 | console.log("'contrast lambda' to secure your AWS serverless functions") 38 | } 39 | 40 | module.exports = { 41 | commonHelpLinks, 42 | postRunMessage 43 | } 44 | -------------------------------------------------------------------------------- /src/scaAnalysis/common/scaParserForGoAndJava.js: -------------------------------------------------------------------------------- 1 | const parseDependenciesForSCAServices = dependencyTreeObject => { 2 | let parsedDependencyTree = {} 3 | let subDeps 4 | 5 | for (let tree in dependencyTreeObject) { 6 | let unParsedDependencyTree = dependencyTreeObject[tree] 7 | for (let dependency in unParsedDependencyTree) { 8 | subDeps = parseSubDependencies(unParsedDependencyTree[dependency].edges) 9 | 10 | let parsedDependency = { 11 | name: unParsedDependencyTree[dependency].artifactID, 12 | group: unParsedDependencyTree[dependency].group, 13 | version: unParsedDependencyTree[dependency].version, 14 | directDependency: unParsedDependencyTree[dependency].type === 'direct', 15 | productionDependency: true, 16 | dependencies: subDeps 17 | } 18 | parsedDependencyTree[dependency] = parsedDependency 19 | } 20 | } 21 | return parsedDependencyTree 22 | } 23 | 24 | const parseSubDependencies = dependencies => { 25 | // converting: 26 | // dependencies: { 27 | // 'gopkg.in/check.v1@v0.0.0-2': 'gopkg.in/check.v1@v0.0.0-2' 28 | // } 29 | // to: 30 | // dependencies: [ 'gopkg.in/check.v1@v0.0.0-2' ] 31 | let subDeps = [] 32 | for (let x in dependencies) { 33 | subDeps.push(dependencies[x]) 34 | } 35 | return subDeps 36 | } 37 | 38 | module.exports = { 39 | parseDependenciesForSCAServices, 40 | parseSubDependencies 41 | } 42 | -------------------------------------------------------------------------------- /src/utils/generalAPI.js: -------------------------------------------------------------------------------- 1 | const { featuresTeamServer } = require('./capabilities') 2 | const semver = require('semver') 3 | const commonApi = require('./commonApi') 4 | const { isNil } = require('lodash') 5 | 6 | const getGlobalProperties = async config => { 7 | const client = commonApi.getHttpClient(config) 8 | return client 9 | .getGlobalProperties(config.host) 10 | .then(res => { 11 | if (res.statusCode === 200) { 12 | return res.body 13 | } else { 14 | commonApi.handleResponseErrors(res, 'globalProperties') 15 | } 16 | }) 17 | .catch(err => { 18 | console.log(err) 19 | }) 20 | } 21 | 22 | const getMode = async config => { 23 | const features = await getGlobalProperties(config) 24 | 25 | if (!isNil(features?.mode)) { 26 | return features.mode 27 | } 28 | return '' 29 | } 30 | 31 | const getFeatures = version => { 32 | const featuresEnabled = [] 33 | 34 | featuresTeamServer.forEach(feature => { 35 | const versionFrom = Object.values(feature)[0] 36 | return semver.gte(version, versionFrom) 37 | ? featuresEnabled.push(Object.keys(feature)[0]) 38 | : null 39 | }) 40 | return featuresEnabled 41 | } 42 | 43 | const isFeatureEnabled = (features, featureName) => { 44 | return features.includes(featureName) 45 | } 46 | 47 | module.exports = { 48 | getGlobalProperties, 49 | getFeatures, 50 | isFeatureEnabled, 51 | getMode 52 | } 53 | -------------------------------------------------------------------------------- /src/commands/scan/processScan.js: -------------------------------------------------------------------------------- 1 | const scanConfig = require('../../scan/scanConfig') 2 | const { startScan } = require('../../scan/scanController') 3 | const { saveScanFile } = require('../../utils/saveFile') 4 | const { ScanResultsModel } = require('../../scan/models/scanResultsModel') 5 | const { formatScanOutput } = require('../../scan/formatScanOutput') 6 | const common = require('../../common/fail') 7 | const { sendTelemetryConfigAsObject } = require('../../telemetry/telemetry') 8 | const { postRunMessage } = require('../../common/commonHelp') 9 | 10 | const processScan = async (contrastConf, argv) => { 11 | let config = await scanConfig.getScanConfig(contrastConf, 'scan', argv) 12 | let output = undefined 13 | 14 | let scanResults = new ScanResultsModel(await startScan(config)) 15 | await sendTelemetryConfigAsObject( 16 | config, 17 | 'scan', 18 | argv, 19 | 'SUCCESS', 20 | scanResults.scanDetail.language 21 | ) 22 | 23 | if (scanResults.scanResultsInstances !== undefined) { 24 | output = formatScanOutput(scanResults) 25 | } 26 | 27 | if (config.save !== undefined) { 28 | await saveScanFile(config, scanResults) 29 | } else { 30 | console.log('\nUse contrast scan --save to save results as a SARIF') 31 | } 32 | 33 | if (config.fail) { 34 | common.processFail(config, output) 35 | } 36 | 37 | postRunMessage('scan') 38 | } 39 | 40 | module.exports = { 41 | processScan 42 | } 43 | -------------------------------------------------------------------------------- /src/scan/models/scanResultsModel.ts: -------------------------------------------------------------------------------- 1 | import { ResultContent } from './resultContentModel' 2 | 3 | export class ScanResultsModel { 4 | projectOverview: ProjectOverview 5 | scanDetail: ScanDetail 6 | scanResultsInstances: ScanResultsInstances 7 | newProject: boolean 8 | 9 | constructor(scan: any) { 10 | this.projectOverview = scan.projectOverview as ProjectOverview 11 | this.scanDetail = scan.scanDetail as ScanDetail 12 | this.scanResultsInstances = 13 | scan.scanResultsInstances as ScanResultsInstances 14 | this.newProject = scan.newProject 15 | } 16 | } 17 | 18 | export interface ProjectOverview { 19 | id: string 20 | organizationId: string 21 | name: string 22 | archived: boolean 23 | language: string 24 | critical: number 25 | high: number 26 | medium: number 27 | low: number 28 | note: number 29 | lastScanTime: string 30 | completedScans: number 31 | lastScanId: string 32 | } 33 | 34 | export interface ScanDetail { 35 | critical: number 36 | high: number 37 | medium: number 38 | low: number 39 | note: number 40 | id: string 41 | organizationId: string 42 | projectId: string 43 | codeArtifactId: string 44 | status: string 45 | createdTime: string 46 | startedTime: string 47 | completedTime: string 48 | language: string 49 | label: string 50 | errorMessage: string 51 | } 52 | 53 | export interface ScanResultsInstances { 54 | content: ResultContent[] 55 | } 56 | -------------------------------------------------------------------------------- /src/audit/catalogueApplication/catalogueApplication.js: -------------------------------------------------------------------------------- 1 | const { getHttpClient, handleResponseErrors } = require('../../utils/commonApi') 2 | 3 | const catalogueApplication = async config => { 4 | const client = getHttpClient(config) 5 | let appId 6 | await client 7 | .catalogueCommand(config) 8 | .then(res => { 9 | if (res.statusCode === 201) { 10 | //displaySuccessMessage(config, res.body.application.app_id) 11 | appId = res.body.application.app_id 12 | } else if (doesMessagesContainAppId(res)) { 13 | appId = tryRetrieveAppIdFromMessages(res.body.messages) 14 | } else { 15 | handleResponseErrors(res, 'catalogue') 16 | } 17 | }) 18 | .catch(err => { 19 | console.log(err) 20 | }) 21 | return appId 22 | } 23 | 24 | const doesMessagesContainAppId = res => { 25 | const regex = /(Application ID =)/ 26 | if ( 27 | res.statusCode === 400 && 28 | res.body.messages.filter(message => regex.exec(message))[0] 29 | ) { 30 | return true 31 | } 32 | 33 | return false 34 | } 35 | 36 | const tryRetrieveAppIdFromMessages = messages => { 37 | let appId 38 | messages.forEach(message => { 39 | if (message.includes('Application ID')) { 40 | appId = message.split('=')[1].replace(/\s+/g, '') 41 | } 42 | }) 43 | 44 | return appId 45 | } 46 | 47 | module.exports = { 48 | catalogueApplication, 49 | doesMessagesContainAppId, 50 | tryRetrieveAppIdFromMessages 51 | } 52 | -------------------------------------------------------------------------------- /src/scaAnalysis/common/formatMessage.js: -------------------------------------------------------------------------------- 1 | const createJavaTSMessage = javaTree => { 2 | return { 3 | java: { 4 | mavenDependencyTrees: javaTree 5 | } 6 | } 7 | } 8 | 9 | const createJavaScriptTSMessage = js => { 10 | let message = { 11 | node: { 12 | packageJSON: js.packageJSON 13 | } 14 | } 15 | if (js.yarn !== undefined) { 16 | message.node.yarnLockFile = js.yarn.yarnLockFile 17 | message.node.yarnVersion = js.yarn.yarnVersion 18 | } else { 19 | message.node.npmLockFile = js.npmLockFile 20 | } 21 | return message 22 | } 23 | 24 | const createGoTSMessage = goTree => { 25 | return { 26 | go: { 27 | goDependencyTrees: goTree 28 | } 29 | } 30 | } 31 | 32 | const createRubyTSMessage = rubyTree => { 33 | return { 34 | ruby: rubyTree 35 | } 36 | } 37 | 38 | const createPythonTSMessage = pythonTree => { 39 | return { 40 | python: pythonTree 41 | } 42 | } 43 | 44 | const createPhpTSMessage = phpTree => { 45 | return { 46 | php: { 47 | composerJSON: phpTree.composerJSON, 48 | lockFile: phpTree.lockFile 49 | } 50 | } 51 | } 52 | 53 | const createDotNetTSMessage = dotnetTree => { 54 | return { 55 | dotnet: dotnetTree 56 | } 57 | } 58 | 59 | module.exports = { 60 | createJavaScriptTSMessage, 61 | createJavaTSMessage, 62 | createGoTSMessage, 63 | createPhpTSMessage, 64 | createRubyTSMessage, 65 | createPythonTSMessage, 66 | createDotNetTSMessage 67 | } 68 | -------------------------------------------------------------------------------- /src/commands/audit/auditController.js: -------------------------------------------------------------------------------- 1 | const catalogue = require('../../audit/catalogueApplication/catalogueApplication') 2 | const commonApi = require('../../audit/languageAnalysisEngine/commonApi') 3 | 4 | const dealWithNoAppId = async config => { 5 | let appID 6 | try { 7 | appID = await commonApi.returnAppId(config) 8 | 9 | if (!appID && config.applicationName) { 10 | return await catalogue.catalogueApplication(config) 11 | } 12 | 13 | if (!appID && !config.applicationName) { 14 | config.applicationName = getAppName(config.file) 15 | appID = await commonApi.returnAppId(config) 16 | 17 | if (!appID) { 18 | return await catalogue.catalogueApplication(config) 19 | } 20 | } 21 | } catch (e) { 22 | if (e.toString().includes('tunneling socket could not be established')) { 23 | console.log(e.message.toString()) 24 | console.log( 25 | 'There seems to be an issue with your proxy, please check and try again' 26 | ) 27 | } 28 | process.exit(1) 29 | } 30 | return appID 31 | } 32 | 33 | const getAppName = file => { 34 | const last = file.charAt(file.length - 1) 35 | if (last !== '/') { 36 | return file.split('/').pop() 37 | } else { 38 | const str = removeLastChar(file) 39 | return str.split('/').pop() 40 | } 41 | } 42 | 43 | const removeLastChar = str => { 44 | return str.substring(0, str.length - 1) 45 | } 46 | 47 | module.exports = { 48 | dealWithNoAppId 49 | } 50 | -------------------------------------------------------------------------------- /src/commands/audit/auditController.ts: -------------------------------------------------------------------------------- 1 | import { catalogueApplication } from '../../audit/catalogueApplication/catalogueApplication' 2 | import commonApi from '../../audit/languageAnalysisEngine/commonApi' 3 | 4 | export const dealWithNoAppId = async (config: { [x: string]: string }) => { 5 | let appID: string 6 | try { 7 | // @ts-ignore 8 | appID = await commonApi.returnAppId(config) 9 | if (!appID && config.applicationName) { 10 | return await catalogueApplication(config) 11 | } 12 | if (!appID && !config.applicationName) { 13 | config.applicationName = getAppName(config.file) as string 14 | // @ts-ignore 15 | appID = await commonApi.returnAppId(config) 16 | if (!appID) { 17 | return await catalogueApplication(config) 18 | } 19 | } 20 | } catch (e: any) { 21 | if (e.toString().includes('tunneling socket could not be established')) { 22 | console.log(e.message.toString()) 23 | console.log( 24 | 'There seems to be an issue with your proxy, please check and try again' 25 | ) 26 | } 27 | process.exit(1) 28 | } 29 | return appID 30 | } 31 | 32 | export const getAppName = (file: string) => { 33 | const last = file.charAt(file.length - 1) 34 | if (last !== '/') { 35 | return file.split('/').pop() 36 | } else { 37 | const str = removeLastChar(file) 38 | return str.split('/').pop() 39 | } 40 | } 41 | 42 | const removeLastChar = (str: string) => { 43 | return str.substring(0, str.length - 1) 44 | } 45 | -------------------------------------------------------------------------------- /src/audit/languageAnalysisEngine/checkForMultipleIdentifiedProjectFiles.js: -------------------------------------------------------------------------------- 1 | const i18n = require('i18n') 2 | /** 3 | * Checks that the list of languages and files that has been reduced doesn't 4 | * contain more than one project file for any identified language. 5 | */ 6 | module.exports = exports = (analysis, next) => { 7 | const { languageAnalysis } = analysis 8 | try { 9 | checkForMultipleIdentifiedProjectFiles(languageAnalysis.identifiedLanguages) 10 | } catch (err) { 11 | next(err) 12 | return 13 | } 14 | next() 15 | } 16 | 17 | const checkForMultipleIdentifiedProjectFiles = identifiedLanguages => { 18 | // Handle the error case where only a single language has been identified... 19 | if (Object.keys(identifiedLanguages).length == 1) { 20 | let { projectFilenames } = Object.values(identifiedLanguages)[0] 21 | 22 | // ...but multiple project files for that language have been found 23 | if (projectFilenames.length > 1) { 24 | const [language] = Object.keys(identifiedLanguages) 25 | projectFilenames = projectFilenames.join(', ') 26 | 27 | // NOTE : Quotation marks for language needs to be added back in (this includes tests) 28 | throw new Error( 29 | i18n.__( 30 | 'languageAnalysisProjectFiles', 31 | language, 32 | projectFilenames, 33 | "'project_path'" 34 | ) 35 | ) 36 | } 37 | } 38 | } 39 | 40 | //For testing purposes 41 | exports.checkForMultipleIdentifiedProjectFiles = checkForMultipleIdentifiedProjectFiles 42 | -------------------------------------------------------------------------------- /src/scaAnalysis/java/index.js: -------------------------------------------------------------------------------- 1 | const analysis = require('./analysis') 2 | const { parseBuildDeps } = require('./javaBuildDepsParser') 3 | const { createJavaTSMessage } = require('../common/formatMessage') 4 | const { 5 | parseDependenciesForSCAServices 6 | } = require('../common/scaParserForGoAndJava') 7 | const chalk = require('chalk') 8 | const _ = require('lodash') 9 | 10 | const javaAnalysis = async (config, languageFiles) => { 11 | languageFiles.JAVA.forEach(file => { 12 | file.replace('build.gradle.kts', 'build.gradle') 13 | }) 14 | 15 | await getAgreement(config) 16 | 17 | const javaDeps = buildJavaTree(config, languageFiles.JAVA) 18 | 19 | if (config.experimental) { 20 | return parseDependenciesForSCAServices(javaDeps) 21 | } else { 22 | return createJavaTSMessage(javaDeps) 23 | } 24 | } 25 | 26 | const getAgreement = async config => { 27 | console.log(chalk.bold('Java project detected')) 28 | console.log( 29 | 'Java analysis uses maven / gradle which are potentially susceptible to command injection. Be sure that the code you are running Contrast CLI on is trusted before continuing.' 30 | ) 31 | 32 | if (!process.env.CI && !config?.javaAgreement) { 33 | return await analysis.agreementPrompt(config) 34 | } 35 | return config 36 | } 37 | 38 | const buildJavaTree = (config, files) => { 39 | const javaBuildDeps = analysis.getJavaBuildDeps(config, files) 40 | return parseBuildDeps(config, javaBuildDeps) 41 | } 42 | 43 | module.exports = { 44 | javaAnalysis, 45 | getAgreement 46 | } 47 | -------------------------------------------------------------------------------- /src/audit/dotnetAnalysisEngine/parseLockFileContents.js: -------------------------------------------------------------------------------- 1 | const i18n = require('i18n') 2 | 3 | module.exports = exports = ({ language: { lockFilePath }, dotnet }, next) => { 4 | const { rawLockFileContents } = dotnet 5 | 6 | // If we never read the lock file then pass priority 7 | if (!rawLockFileContents) { 8 | next() 9 | 10 | return 11 | } 12 | 13 | try { 14 | let count = 0 // Used to test if some nodes are deleted 15 | dotnet.lockFile = JSON.parse(rawLockFileContents) 16 | 17 | for (const dependenciesNode in dotnet.lockFile.dependencies) { 18 | for (const innerNode in dotnet.lockFile.dependencies[dependenciesNode]) { 19 | const nodeValidation = JSON.stringify( 20 | dotnet.lockFile.dependencies[dependenciesNode][innerNode] 21 | ) 22 | if (nodeValidation.includes('"type":"Project"')) { 23 | count += 1 24 | delete dotnet.lockFile.dependencies[dependenciesNode][innerNode] 25 | dotnet.additionalInfo = 'dependenciesNote' 26 | } 27 | } 28 | } 29 | 30 | // If dependencies removed wait for json to be displayed and flag warning 31 | if (count > 0) { 32 | const multiLevelProjectWarning = () => { 33 | console.log('') 34 | console.log(i18n.__('dependenciesNote')) 35 | } 36 | setTimeout(multiLevelProjectWarning, 7000) 37 | } 38 | } catch (err) { 39 | next( 40 | new Error(i18n.__('dotnetParseLockfile', lockFilePath) + `${err.message}`) 41 | ) 42 | 43 | return 44 | } 45 | 46 | next() 47 | } 48 | -------------------------------------------------------------------------------- /src/audit/javaAnalysisEngine/index.js: -------------------------------------------------------------------------------- 1 | const AnalysisEngine = require('../AnalysisEngine') 2 | 3 | const readProjectFileContents = require('./readProjectFileContents') 4 | const parseMavenProjectFileContents = require('./parseMavenProjectFileContents') 5 | const parseProjectFileContents = require('./parseProjectFileContents') 6 | const sanitizer = require('./sanitizer') 7 | const i18n = require('i18n') 8 | 9 | module.exports = exports = (language, config, callback) => { 10 | const ae = new AnalysisEngine({ language, config, java: {} }) 11 | 12 | // Remove ".kts" from filename to look the same as a Gradle projectFileName so we can support Kotlin 13 | language.projectFilePath = language.projectFilePath.replace( 14 | 'build.gradle.kts', 15 | 'build.gradle' 16 | ) 17 | 18 | if (config['beta_unified_java_parser']) { 19 | console.log('Using new parser...') 20 | ae.use([readProjectFileContents, parseProjectFileContents, sanitizer]) 21 | } else if ( 22 | language.projectFilePath.endsWith('pom.xml') && 23 | !config['beta_unified_java_parser'] 24 | ) { 25 | ae.use([readProjectFileContents, parseMavenProjectFileContents, sanitizer]) 26 | } else { 27 | ae.use([ 28 | readProjectFileContents, 29 | parseMavenProjectFileContents, 30 | parseProjectFileContents, 31 | sanitizer 32 | ]) 33 | } 34 | ae.analyze((err, analysis) => { 35 | if (err) { 36 | console.log(i18n.__('javaAnalysisError'), err.message) 37 | return 38 | } 39 | callback(null, analysis, config) 40 | }, config) 41 | } 42 | -------------------------------------------------------------------------------- /src/audit/save.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const i18n = require('i18n') 3 | const chalk = require('chalk') 4 | const save = require('../commands/audit/saveFile') 5 | const sbom = require('../sbom/generateSbom') 6 | const { 7 | SBOM_CYCLONE_DX_FILE, 8 | SBOM_SPDX_FILE 9 | } = require('../constants/constants') 10 | 11 | async function auditSave(config, reportId) { 12 | let fileFormat 13 | switch (config.save) { 14 | case null: 15 | case SBOM_CYCLONE_DX_FILE: 16 | fileFormat = SBOM_CYCLONE_DX_FILE 17 | break 18 | case SBOM_SPDX_FILE: 19 | fileFormat = SBOM_SPDX_FILE 20 | break 21 | default: 22 | break 23 | } 24 | 25 | if (fileFormat) { 26 | if (config.experimental) { 27 | save.saveFile( 28 | config, 29 | fileFormat, 30 | await sbom.generateSCASbom(config, fileFormat, reportId) 31 | ) 32 | } else { 33 | save.saveFile( 34 | config, 35 | fileFormat, 36 | await sbom.generateSbom(config, fileFormat) 37 | ) 38 | } 39 | const filename = `${config.applicationId}-sbom-${fileFormat}.json` 40 | if (fs.existsSync(filename)) { 41 | console.log(i18n.__('auditSBOMSaveSuccess') + ` - ${filename}`) 42 | } else { 43 | console.log( 44 | chalk.yellow.bold( 45 | `\n Unable to save ${filename} Software Bill of Materials (SBOM)` 46 | ) 47 | ) 48 | } 49 | } else { 50 | console.log(i18n.__('auditBadFiletypeSpecifiedForSave')) 51 | } 52 | } 53 | 54 | module.exports = { 55 | auditSave 56 | } 57 | -------------------------------------------------------------------------------- /src/audit/nodeAnalysisEngine/handleNPMLockFileV2.js: -------------------------------------------------------------------------------- 1 | const i18n = require('i18n') 2 | module.exports = exports = (analysis, next) => { 3 | const { 4 | language: { lockFilePath }, 5 | node 6 | } = analysis 7 | 8 | try { 9 | if (node.npmLockFile && node.npmLockFile.lockfileVersion > 1) { 10 | const listOfTopDep = Object.keys(node.npmLockFile.dependencies) 11 | Object.entries(node.npmLockFile.dependencies).forEach(([key, value]) => { 12 | if (value.requires) { 13 | const listOfRequiresDep = Object.keys(value.requires) 14 | listOfRequiresDep.forEach(dep => { 15 | if (!listOfTopDep.includes(dep)) { 16 | addDepToLockFile(value['requires'], dep) 17 | } 18 | }) 19 | } 20 | 21 | if (value.dependencies) { 22 | Object.entries(value.dependencies).forEach( 23 | ([childKey, childValue]) => { 24 | if (childValue.requires) { 25 | const listOfRequiresDep = Object.keys(childValue.requires) 26 | listOfRequiresDep.forEach(dep => { 27 | if (!listOfTopDep.includes(dep)) { 28 | addDepToLockFile(childValue['requires'], dep) 29 | } 30 | }) 31 | } 32 | } 33 | ) 34 | } 35 | }) 36 | } 37 | } catch (err) { 38 | next( 39 | next(new Error(i18n.__('NodeParseNPM', lockFilePath) + `${err.message}`)) 40 | ) 41 | return 42 | } 43 | 44 | function addDepToLockFile(depObj, key) { 45 | node.npmLockFile.dependencies[key] = { version: depObj[key] } 46 | } 47 | 48 | next() 49 | } 50 | -------------------------------------------------------------------------------- /src/lambda/logUtils.ts: -------------------------------------------------------------------------------- 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 | /** 47 | * 48 | * @param fileSizeInBytes 49 | * 50 | * @returns human readable format 51 | */ 52 | const getReadableFileSize = (fileSizeInBytes: number) => { 53 | let i = -1 54 | const byteUnits = [' kB', ' MB', ' GB', ' TB', 'PB', 'EB', 'ZB', 'YB'] 55 | 56 | do { 57 | fileSizeInBytes = fileSizeInBytes / 1024 58 | i++ 59 | } while (fileSizeInBytes > 1024) 60 | 61 | return Math.max(fileSizeInBytes, 0.1).toFixed(1) + byteUnits[i] 62 | } 63 | 64 | export { log, prettyPrintJson, getReadableFileSize } 65 | -------------------------------------------------------------------------------- /src/audit/languageAnalysisEngine/checkIdentifiedLanguageHasLockFile.js: -------------------------------------------------------------------------------- 1 | const i18n = require('i18n') 2 | 3 | /** 4 | * Checks that a project has a lock file 5 | */ 6 | module.exports = exports = (analysis, next) => { 7 | try { 8 | const { languageAnalysis } = analysis 9 | //.NET and NODE both need lock files. currently JAVA and GO do not 10 | // need a lock file so if lang is JAVA / GO just go to next 11 | if ( 12 | Object.getOwnPropertyNames(languageAnalysis.identifiedLanguages)[0] === 13 | 'JAVA' || 14 | Object.getOwnPropertyNames(languageAnalysis.identifiedLanguages)[0] === 15 | 'GO' 16 | ) { 17 | next() 18 | return 19 | } 20 | checkForLockFile(languageAnalysis.identifiedLanguages) 21 | } catch (err) { 22 | next(err) 23 | return 24 | } 25 | next() 26 | return 27 | } 28 | 29 | const checkForLockFile = identifiedLanguages => { 30 | // Handle the error case where only a single language has been identified... 31 | if (Object.keys(identifiedLanguages).length == 1) { 32 | let { lockFilenames } = Object.values(identifiedLanguages)[0] 33 | 34 | // ...but no lock files for that language have been found 35 | if (lockFilenames.length == 0) { 36 | const [language] = Object.keys(identifiedLanguages) 37 | throw new Error(i18n.__('languageAnalysisHasNoLockFile', language)) 38 | } 39 | 40 | if (lockFilenames.length > 1) { 41 | const [language] = Object.keys(identifiedLanguages) 42 | throw new Error( 43 | i18n.__( 44 | 'languageAnalysisHasMultipleLockFiles', 45 | language, 46 | String(lockFilenames) 47 | ) 48 | ) 49 | } 50 | } 51 | } 52 | 53 | //For testing purposes 54 | exports.checkForLockFile = checkForLockFile 55 | -------------------------------------------------------------------------------- /src/commands/audit/help.ts: -------------------------------------------------------------------------------- 1 | import commandLineUsage from 'command-line-usage' 2 | import i18n from 'i18n' 3 | import constants from '../../constants' 4 | import { commonHelpLinks } from '../../common/commonHelp' 5 | 6 | const auditUsageGuide = commandLineUsage([ 7 | { 8 | header: i18n.__('auditHeader'), 9 | content: [i18n.__('auditHeaderMessage')] 10 | }, 11 | { 12 | header: i18n.__('constantsPrerequisitesHeader'), 13 | content: [ 14 | '{bold ' + 15 | i18n.__('constantsAuditPrerequisitesContentSupportedLanguages') + 16 | '}', 17 | i18n.__('constantsAuditPrerequisitesJavaContentMessage'), 18 | i18n.__('constantsAuditPrerequisitesContentDotNetMessage'), 19 | i18n.__('constantsAuditPrerequisitesContentNodeMessage'), 20 | i18n.__('constantsAuditPrerequisitesContentRubyMessage'), 21 | i18n.__('constantsAuditPrerequisitesContentPythonMessage'), 22 | i18n.__('constantsAuditPrerequisitesContentGoMessage'), 23 | i18n.__('constantsAuditPrerequisitesContentPHPMessage') 24 | ] 25 | }, 26 | { 27 | header: i18n.__('constantsAuditOptions'), 28 | optionList: constants.commandLineDefinitions.auditOptionDefinitions, 29 | hide: [ 30 | 'application-id', 31 | 'application-name', 32 | 'organization-id', 33 | 'api-key', 34 | 'authorization', 35 | 'host', 36 | 'proxy', 37 | 'help', 38 | 'ff', 39 | 'ignore-cert-errors', 40 | 'verbose', 41 | 'debug', 42 | 'experimental', 43 | 'tags', 44 | 'sub-project', 45 | 'code', 46 | 'maven-settings-path', 47 | 'language', 48 | 'experimental', 49 | 'app-groups', 50 | 'metadata' 51 | ] 52 | }, 53 | commonHelpLinks() 54 | ]) 55 | 56 | export { auditUsageGuide } 57 | -------------------------------------------------------------------------------- /src/audit/languageAnalysisEngine/index.js: -------------------------------------------------------------------------------- 1 | const AnalysisEngine = require('./../AnalysisEngine') 2 | const i18n = require('i18n') 3 | 4 | const getProjectRootFilenames = require('./getProjectRootFilenames') 5 | const reduceIdentifiedLanguages = require('./reduceIdentifiedLanguages') 6 | const checkForMultipleIdentifiedLanguages = require('./checkForMultipleIdentifiedLanguages') 7 | const checkForMultipleIdentifiedProjectFiles = require('./checkForMultipleIdentifiedProjectFiles') 8 | const checkIdentifiedLanguageHasProjectFile = require('./checkIdentifiedLanguageHasProjectFile') 9 | const checkIdentifiedLanguageHasLockFile = require('./checkIdentifiedLanguageHasLockFile') 10 | const getIdentifiedLanguageInfo = require('./getIdentifiedLanguageInfo') 11 | const { libraryAnalysisError } = require('../../common/errorHandling') 12 | 13 | module.exports = exports = (projectPath, callback, appId, config) => { 14 | // Create an analysis engine to identify the project language 15 | const ae = new AnalysisEngine({ 16 | projectPath, 17 | appId, 18 | languageAnalysis: { appId: appId }, 19 | config 20 | }) 21 | 22 | ae.use([ 23 | getProjectRootFilenames, 24 | reduceIdentifiedLanguages, 25 | checkForMultipleIdentifiedLanguages, 26 | checkForMultipleIdentifiedProjectFiles, 27 | checkIdentifiedLanguageHasProjectFile, 28 | checkIdentifiedLanguageHasLockFile, 29 | getIdentifiedLanguageInfo 30 | ]) 31 | 32 | ae.analyze((err, analysis) => { 33 | if (err) { 34 | console.log( 35 | '*******************' + 36 | i18n.__('languageAnalysisFailureMessage') + 37 | '****************' 38 | ) 39 | console.error(`${err.message}`) 40 | libraryAnalysisError() 41 | process.exit(1) 42 | } 43 | callback(null, analysis) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /src/scan/scanConfig.js: -------------------------------------------------------------------------------- 1 | const paramHandler = require('../utils/paramsUtil/paramHandler') 2 | const constants = require('../cliConstants.js') 3 | const path = require('path') 4 | const { supportedLanguagesScan } = require('../constants/constants') 5 | const i18n = require('i18n') 6 | const { scanUsageGuide } = require('./help') 7 | const parsedCLIOptions = require('../utils/parsedCLIOptions') 8 | 9 | const getScanConfig = async (contrastConf, command, argv) => { 10 | let scanParams = await parsedCLIOptions.getCommandLineArgsCustom( 11 | contrastConf, 12 | command, 13 | argv, 14 | constants.commandLineDefinitions.scanOptionDefinitions 15 | ) 16 | 17 | if (scanParams.help) { 18 | printHelpMessage() 19 | process.exit(0) 20 | } 21 | 22 | const paramsAuth = paramHandler.getAuth(scanParams) 23 | 24 | if (scanParams.language) { 25 | scanParams.language = scanParams.language.toUpperCase() 26 | if (!Object.values(supportedLanguagesScan).includes(scanParams.language)) { 27 | console.log(`Did not recognise --language ${scanParams.language}`) 28 | console.log(i18n.__('constantsHowToRunDev3')) 29 | process.exit(1) 30 | } 31 | } 32 | 33 | // if no name, take the full file path and use it as the project name 34 | let projectNameSource 35 | if (!scanParams.name && scanParams.file) { 36 | scanParams.name = getFileName(scanParams.file) 37 | projectNameSource = 'AUTO' 38 | } else { 39 | projectNameSource = 'USER' 40 | } 41 | 42 | return { ...paramsAuth, ...scanParams, projectNameSource } 43 | } 44 | 45 | const getFileName = file => { 46 | // from '/Users/x/y/spring-async.war' to 'spring-async.war' 47 | return file.split(path.sep).pop() 48 | } 49 | 50 | const printHelpMessage = () => { 51 | console.log(scanUsageGuide) 52 | } 53 | 54 | module.exports = { 55 | getScanConfig, 56 | getFileName, 57 | printHelpMessage 58 | } 59 | -------------------------------------------------------------------------------- /src/scaAnalysis/common/scaServicesUpload.js: -------------------------------------------------------------------------------- 1 | const commonApi = require('../../utils/commonApi') 2 | const { APP_VERSION } = require('../../constants/constants') 3 | const requestUtils = require('../../utils/requestUtils') 4 | 5 | const scaTreeUpload = async (analysis, config) => { 6 | const requestBody = { 7 | applicationId: config.applicationId, 8 | dependencyTree: analysis, 9 | organizationId: config.organizationId, 10 | language: config.language, 11 | tool: { 12 | name: 'Contrast Codesec', 13 | version: APP_VERSION 14 | } 15 | } 16 | 17 | if (config.branch) { 18 | requestBody.branchName = config.branch 19 | } 20 | 21 | const client = commonApi.getHttpClient(config) 22 | const reportID = await client 23 | .scaServiceIngest(requestBody, config) 24 | .then(res => { 25 | if (res.statusCode === 201) { 26 | return res.body.libraryIngestJobId 27 | } else { 28 | throw new Error(res.statusCode + ` error ingesting dependencies`) 29 | } 30 | }) 31 | .catch(err => { 32 | throw err 33 | }) 34 | if (config.debug) { 35 | console.log(' polling report', reportID) 36 | } 37 | 38 | let keepChecking = true 39 | let res 40 | while (keepChecking) { 41 | res = await client.scaServiceReportStatus(config, reportID).then(res => { 42 | if (config.debug) { 43 | console.log(res.statusCode) 44 | console.log(res.body) 45 | } 46 | if (res.body.status === 'COMPLETED') { 47 | keepChecking = false 48 | return client.scaServiceReport(config, reportID).then(res => { 49 | return [res.body, reportID] 50 | }) 51 | } 52 | }) 53 | 54 | if (!keepChecking) { 55 | return [res, reportID] 56 | } 57 | await requestUtils.sleep(5000) 58 | } 59 | return [res, reportID] 60 | } 61 | 62 | module.exports = { 63 | scaTreeUpload 64 | } 65 | -------------------------------------------------------------------------------- /src/audit/nodeAnalysisEngine/parseYarn2LockFileContents.js: -------------------------------------------------------------------------------- 1 | const i18n = require('i18n') 2 | module.exports = exports = ({ language: { lockFilename }, node }, next) => { 3 | // If we never read the lock file or its an earlier version then pass priority 4 | if (node.rawYarnLockFileContents == undefined || node.yarnVersion == 1) { 5 | next() 6 | } else { 7 | try { 8 | node.yarnLockFile = {} 9 | node.yarnLockFile['object'] = node.rawYarnLockFileContents 10 | delete node.yarnLockFile['object'].__metadata 11 | node.yarnLockFile['type'] = 'success' 12 | 13 | Object.entries(node.rawYarnLockFileContents).forEach(([key, value]) => { 14 | const rawKeyNames = key.split(',') 15 | const keyNames = formatKey(rawKeyNames) 16 | 17 | keyNames.forEach(name => { 18 | node.yarnLockFile.object[name] = value 19 | }) 20 | }) 21 | } catch (err) { 22 | next( 23 | new Error( 24 | i18n.__('NodeParseYarn2', lockFilename.lockFilePath) + 25 | `${err.message}` 26 | ) 27 | ) 28 | 29 | return 30 | } 31 | 32 | next() 33 | } 34 | } 35 | 36 | function formatKey(keyNames) { 37 | let name = '' 38 | let formattedNames = [] 39 | keyNames.forEach(dummyString => { 40 | let nameArr = dummyString.split('@') 41 | if (nameArr.length > 1) { 42 | if (nameArr.length == 2) { 43 | name = nameArr[0] 44 | } 45 | 46 | if (nameArr.length == 3) { 47 | name = '@' + nameArr[1] 48 | } 49 | 50 | let version = dummyString.split(':').pop('') 51 | 52 | if (version.length == 1 && version != '*') { 53 | version = version + '.0' 54 | } 55 | let reformattedKey = name.trim() + '@' + version 56 | 57 | formattedNames.push(reformattedKey) 58 | } 59 | }) 60 | return formattedNames 61 | } 62 | 63 | exports.formatKey = formatKey 64 | -------------------------------------------------------------------------------- /src/commands/audit/help.js: -------------------------------------------------------------------------------- 1 | const commandLineUsage = require('command-line-usage') 2 | const i18n = require('i18n') 3 | const constants = require('../../cliConstants') 4 | const { commonHelpLinks } = require('../../common/commonHelp') 5 | 6 | const auditUsageGuide = commandLineUsage([ 7 | { 8 | header: i18n.__('auditHeader'), 9 | content: [i18n.__('auditHeaderMessage')] 10 | }, 11 | { 12 | header: i18n.__('constantsPrerequisitesHeader'), 13 | content: [ 14 | '{bold ' + 15 | i18n.__('constantsAuditPrerequisitesContentSupportedLanguages') + 16 | '}', 17 | i18n.__('constantsAuditPrerequisitesJavaContentMessage'), 18 | i18n.__('constantsAuditPrerequisitesContentDotNetMessage'), 19 | i18n.__('constantsAuditPrerequisitesContentNodeMessage'), 20 | i18n.__('constantsAuditPrerequisitesContentRubyMessage'), 21 | i18n.__('constantsAuditPrerequisitesContentPythonMessage'), 22 | i18n.__('constantsAuditPrerequisitesContentGoMessage'), 23 | i18n.__('constantsAuditPrerequisitesContentPHPMessage') 24 | ] 25 | }, 26 | { 27 | header: i18n.__('constantsAuditOptions'), 28 | optionList: constants.commandLineDefinitions.auditOptionDefinitions, 29 | hide: [ 30 | 'application-id', 31 | 'application-name', 32 | 'organization-id', 33 | 'api-key', 34 | 'authorization', 35 | 'host', 36 | 'proxy', 37 | 'help', 38 | 'ff', 39 | 'ignore-cert-errors', 40 | 'verbose', 41 | 'debug', 42 | 'experimental', 43 | 'tags', 44 | 'sub-project', 45 | 'code', 46 | 'maven-settings-path', 47 | 'language', 48 | 'experimental', 49 | 'app-groups', 50 | 'metadata', 51 | 'track', 52 | 'fingerprint' 53 | ] 54 | }, 55 | commonHelpLinks()[0], 56 | commonHelpLinks()[1] 57 | ]) 58 | 59 | module.exports = { 60 | auditUsageGuide 61 | } 62 | -------------------------------------------------------------------------------- /src/constants/constants.js: -------------------------------------------------------------------------------- 1 | // Language identifiers 2 | const NODE = 'NODE' 3 | const DOTNET = 'DOTNET' 4 | const JAVA = 'JAVA' 5 | const RUBY = 'RUBY' 6 | const PYTHON = 'PYTHON' 7 | const GO = 'GO' 8 | const PHP = 'PHP' 9 | const JAVASCRIPT = 'JAVASCRIPT' 10 | // Severity 11 | const LOW = 'LOW' 12 | const MEDIUM = 'MEDIUM' 13 | const HIGH = 'HIGH' 14 | const CRITICAL = 'CRITICAL' 15 | // App 16 | const APP_NAME = 'contrast' 17 | const APP_VERSION = '1.0.17' 18 | const TIMEOUT = 120000 19 | const HIGH_COLOUR = '#ff9900' 20 | const CRITICAL_COLOUR = '#e35858' 21 | const MEDIUM_COLOUR = '#f1c232' 22 | const LOW_COLOUR = '#b7b7b7' 23 | const NOTE_COLOUR = '#999999' 24 | const CRITICAL_PRIORITY = 1 25 | const HIGH_PRIORITY = 2 26 | const MEDIUM_PRIORITY = 3 27 | const LOW_PRIORITY = 4 28 | const NOTE_PRIORITY = 5 29 | 30 | const AUTH_UI_URL = 'https://cli-auth.contrastsecurity.com' 31 | const AUTH_CALLBACK_URL = 'https://cli-auth-api.contrastsecurity.com' 32 | const SARIF_FILE = 'SARIF' 33 | const SBOM_CYCLONE_DX_FILE = 'cyclonedx' 34 | const SBOM_SPDX_FILE = 'spdx' 35 | const CE_URL = 'https://ce.contrastsecurity.com' 36 | 37 | //configuration 38 | const SAAS = 'SAAS' 39 | const EOP = 'EOP' 40 | const MODE_BUILD = 'BUILD' 41 | const MODE_REPO = 'REPO' 42 | 43 | module.exports = { 44 | supportedLanguages: { NODE, DOTNET, JAVA, RUBY, PYTHON, GO, PHP, JAVASCRIPT }, 45 | supportedLanguagesScan: { JAVASCRIPT, DOTNET, JAVA }, 46 | LOW, 47 | MEDIUM, 48 | HIGH, 49 | CRITICAL, 50 | APP_VERSION, 51 | APP_NAME, 52 | TIMEOUT, 53 | AUTH_UI_URL, 54 | AUTH_CALLBACK_URL, 55 | SARIF_FILE, 56 | HIGH_COLOUR, 57 | CRITICAL_COLOUR, 58 | MEDIUM_COLOUR, 59 | LOW_COLOUR, 60 | NOTE_COLOUR, 61 | CE_URL, 62 | CRITICAL_PRIORITY, 63 | HIGH_PRIORITY, 64 | MEDIUM_PRIORITY, 65 | LOW_PRIORITY, 66 | NOTE_PRIORITY, 67 | SBOM_CYCLONE_DX_FILE, 68 | SBOM_SPDX_FILE, 69 | SAAS, 70 | EOP, 71 | MODE_BUILD, 72 | MODE_REPO 73 | } 74 | -------------------------------------------------------------------------------- /src/scan/models/resultContentModel.ts: -------------------------------------------------------------------------------- 1 | type Importance = 'important' | 'essential' 2 | 3 | interface ArtifactLocation { 4 | uri: string 5 | } 6 | 7 | interface Region { 8 | startLine: string 9 | snippet: Snippet 10 | } 11 | 12 | interface Snippet { 13 | text: string 14 | rendered: Rendered 15 | } 16 | 17 | interface Rendered { 18 | text: string 19 | } 20 | 21 | interface PhysicalLocation { 22 | artifactLocation: ArtifactLocation 23 | region: Region 24 | } 25 | 26 | interface LogicalLocation { 27 | fullyQualifiedName: string 28 | name: string 29 | } 30 | 31 | export interface Location { 32 | physicalLocation: PhysicalLocation 33 | logicalLocations?: LogicalLocation[] 34 | } 35 | 36 | export interface ThreadFlowLocation { 37 | importance: Importance 38 | location: Location 39 | } 40 | 41 | interface ThreadFlow { 42 | locations: ThreadFlowLocation[] 43 | } 44 | 45 | interface Message { 46 | text: string 47 | } 48 | 49 | export interface CodeFlow { 50 | message: Message 51 | threadFlows: ThreadFlow[] 52 | } 53 | 54 | export interface ResultContent { 55 | message?: { text: string } 56 | id: string 57 | organizationId: string 58 | projectId: string 59 | firstCreatedTime: string 60 | ruleId: string 61 | codeFlows: CodeFlow[] 62 | lastSeenTime: string 63 | locations: Location[] 64 | name: string 65 | description: string 66 | recommendation: string | null 67 | risk: string | null 68 | category: string 69 | confidence: string 70 | standards: { [key: string]: string[] } 71 | cwe: string[] 72 | owasp: string[] 73 | reference: string[] 74 | sink: string 75 | detailsTrigger: string 76 | type: RuleType 77 | source: string 78 | severity: Severity 79 | advice: string 80 | learn: string[] 81 | issue: string 82 | } 83 | 84 | export type Severity = 'critical' | 'high' | 'medium' | 'low' | 'note' 85 | 86 | export type RuleType = 'DATA_FLOW' | 'CRYPTO' | 'CONFIG' | 'DEFAULT' 87 | -------------------------------------------------------------------------------- /src/utils/commonApi.js: -------------------------------------------------------------------------------- 1 | const HttpClient = require('./../common/HTTPClient') 2 | const { 3 | badRequestError, 4 | unauthenticatedError, 5 | forbiddenError, 6 | proxyError, 7 | genericError, 8 | maxAppError, 9 | snapshotFailureError, 10 | vulnerabilitiesFailureError, 11 | reportFailureError 12 | } = require('../common/errorHandling') 13 | 14 | const handleResponseErrors = (res, api) => { 15 | if (res.statusCode === 400) { 16 | api === 'catalogue' ? badRequestError(true) : badRequestError(false) 17 | } else if (res.statusCode === 401) { 18 | unauthenticatedError() 19 | } else if (res.statusCode === 403) { 20 | forbiddenError() 21 | } else if (res.statusCode === 407) { 22 | proxyError() 23 | } else if (res.statusCode === 412) { 24 | maxAppError() 25 | } else { 26 | if (api === 'snapshot' || api === 'catalogue') { 27 | snapshotFailureError() 28 | } 29 | if (api === 'vulnerabilities') { 30 | vulnerabilitiesFailureError() 31 | } 32 | if (api === 'report') { 33 | reportFailureError() 34 | } 35 | console.log(res.statusCode) 36 | genericError(res) 37 | } 38 | } 39 | 40 | const getProtocol = host => { 41 | const hasProtocol = 42 | host.toLowerCase().includes('https://') || 43 | host.toLowerCase().includes('http://') 44 | return hasProtocol ? host : 'https://' + host 45 | } 46 | 47 | const getPath = host => { 48 | const hasContrastPath = host.toLowerCase().endsWith('/contrast') 49 | return hasContrastPath 50 | ? host.toLowerCase().substring(0, host.length - 9) 51 | : host.replace(/\/*$/, '') 52 | } 53 | 54 | const getValidHost = host => { 55 | const correctProtocol = getProtocol(host) 56 | return getPath(correctProtocol) 57 | } 58 | 59 | const getHttpClient = config => { 60 | return new HttpClient(config) 61 | } 62 | 63 | module.exports = { 64 | getPath: getPath, 65 | getValidHost: getValidHost, 66 | getProtocol: getProtocol, 67 | handleResponseErrors: handleResponseErrors, 68 | getHttpClient: getHttpClient 69 | } 70 | -------------------------------------------------------------------------------- /src/lambda/cliError.ts: -------------------------------------------------------------------------------- 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 } 73 | -------------------------------------------------------------------------------- /src/scan/populateProjectIdAndProjectName.js: -------------------------------------------------------------------------------- 1 | const commonApi = require('../utils/commonApi.js') 2 | const i18n = require('i18n') 3 | 4 | const populateProjectId = async config => { 5 | const client = commonApi.getHttpClient(config) 6 | let proj = await createProjectId(config, client) 7 | if (proj === undefined) { 8 | proj = await getExistingProjectIdByName(config, client).then(res => { 9 | return res 10 | }) 11 | 12 | return { projectId: proj, isNewProject: false } 13 | } 14 | 15 | return { projectId: proj, isNewProject: true } 16 | } 17 | 18 | const createProjectId = async (config, client) => { 19 | return client 20 | .createProjectId(config) 21 | .then(res => { 22 | if (res.statusCode === 409) { 23 | console.log(i18n.__('foundExistingProjectScan')) 24 | return 25 | } 26 | if (res.statusCode === 403) { 27 | console.log(i18n.__('permissionsError')) 28 | process.exit(1) 29 | return 30 | } 31 | if (res.statusCode === 429) { 32 | console.log(i18n.__('exceededFreeTier')) 33 | process.exit(1) 34 | return 35 | } 36 | if (res.statusCode === 201) { 37 | console.log(i18n.__('projectCreatedScan')) 38 | if (config.verbose) { 39 | console.log(i18n.__('populateProjectIdMessage', res.body.id)) 40 | } 41 | return res.body.id 42 | } 43 | }) 44 | .catch(err => { 45 | if (config.verbose) { 46 | console.log(err) 47 | } 48 | console.log(i18n.__('connectionError')) 49 | process.exit(0) 50 | }) 51 | } 52 | 53 | const getExistingProjectIdByName = async (config, client) => { 54 | return client 55 | .getProjectIdByName(config) 56 | .then(res => { 57 | if (res.statusCode === 200) { 58 | if (config.verbose) { 59 | console.log( 60 | i18n.__('populateProjectIdMessage', res.body.content[0].id) 61 | ) 62 | } 63 | return res.body.content[0].id 64 | } 65 | }) 66 | .catch(err => { 67 | console.log(err) 68 | }) 69 | } 70 | 71 | module.exports = { 72 | populateProjectId: populateProjectId 73 | } 74 | -------------------------------------------------------------------------------- /src/audit/phpAnalysisEngine/parseLockFileContents.js: -------------------------------------------------------------------------------- 1 | const i18n = require('i18n') 2 | const _ = require('lodash') 3 | module.exports = exports = ({ language: { lockFilePath }, php }, next) => { 4 | try { 5 | php.lockFile = php.rawLockFileContents 6 | let packages = _.keyBy(php.lockFile.packages, 'name') 7 | let packagesDev = _.keyBy(php.lockFile['packages-dev'], 'name') 8 | php.lockFile.dependencies = _.merge(packages, packagesDev) 9 | 10 | const listOfTopDep = Object.keys(php.lockFile.dependencies) 11 | 12 | Object.entries(php.lockFile.dependencies).forEach(([key, value]) => { 13 | if (value.require) { 14 | const listOfRequiresDep = Object.keys(value.require) 15 | listOfRequiresDep.forEach(dep => { 16 | if (!listOfTopDep.includes(dep)) { 17 | addChildDepToLockFileAsOwnObj(value['require'], dep) 18 | } 19 | }) 20 | } 21 | 22 | if (value['require-dev']) { 23 | const listOfRequiresDep = Object.keys(value['require-dev']) 24 | listOfRequiresDep.forEach(dep => { 25 | if (!listOfTopDep.includes(dep)) { 26 | addChildDepToLockFileAsOwnObj(value['require-dev'], dep) 27 | } 28 | }) 29 | } 30 | }) 31 | 32 | formatParentDepToLockFile() 33 | } catch (err) { 34 | next( 35 | new Error( 36 | i18n.__('phpParseComposerLock', lockFilePath) + `${err.message}` 37 | ) 38 | ) 39 | return 40 | } 41 | next() 42 | 43 | function addChildDepToLockFileAsOwnObj(depObj, key) { 44 | php.lockFile.dependencies[key] = { version: depObj[key] } 45 | } 46 | 47 | function formatParentDepToLockFile() { 48 | for (const [key, value] of Object.entries(php.lockFile.dependencies)) { 49 | let requires = {} 50 | for (const [childKey, childValue] of Object.entries(value)) { 51 | if (childKey === 'require' || childKey === 'require-dev') { 52 | requires = _.merge(requires, childValue) 53 | php.lockFile.dependencies[key].requires = requires 54 | delete php.lockFile.dependencies[key].require 55 | delete php.lockFile.dependencies[key]['require-dev'] 56 | } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/common/fail.js: -------------------------------------------------------------------------------- 1 | const i18n = require('i18n') 2 | 3 | const processFail = (config, reportResults) => { 4 | if (config.severity !== undefined) { 5 | if ( 6 | reportResults[config.severity] !== undefined && 7 | isSeverityViolation(config.severity, reportResults) 8 | ) { 9 | failPipeline('failSeverityOptionErrorMessage') 10 | } 11 | } 12 | 13 | if (config.severity === undefined && reportResults.total > 0) { 14 | failPipeline('failThresholdOptionErrorMessage') 15 | } 16 | } 17 | 18 | const isSeverityViolation = (severity, reportResults) => { 19 | let count = 0 20 | switch (severity) { 21 | case 'critical': 22 | count += reportResults.critical 23 | break 24 | case 'high': 25 | count += reportResults.high + reportResults.critical 26 | break 27 | case 'medium': 28 | count += 29 | reportResults.medium + reportResults.high + reportResults.critical 30 | break 31 | case 'low': 32 | count += 33 | reportResults.high + 34 | reportResults.critical + 35 | reportResults.medium + 36 | reportResults.low 37 | break 38 | case 'note': 39 | if (reportResults.note == reportResults.total) { 40 | count = 0 41 | } else { 42 | count = reportResults.total 43 | } 44 | break 45 | default: 46 | count = 0 47 | } 48 | return count > 0 49 | } 50 | 51 | const failPipeline = (message = '') => { 52 | console.log( 53 | '\n ******************************** ' + 54 | i18n.__('snapshotFailureHeader') + 55 | ' *********************************\n' + 56 | i18n.__(message) 57 | ) 58 | process.exit(2) 59 | } 60 | 61 | const parseSeverity = severity => { 62 | const severities = ['NOTE', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] 63 | if (severities.includes(severity.toUpperCase())) { 64 | return severity.toLowerCase() 65 | } else { 66 | console.log( 67 | severity + 68 | ' Not recognised as a severity type please use LOW, MEDIUM, HIGH, CRITICAL, NOTE' 69 | ) 70 | return undefined 71 | } 72 | } 73 | 74 | module.exports = { 75 | failPipeline, 76 | processFail, 77 | isSeverityViolation, 78 | parseSeverity 79 | } 80 | -------------------------------------------------------------------------------- /src/scan/scan.ts: -------------------------------------------------------------------------------- 1 | import commonApi from '../utils/commonApi.js' 2 | import fileUtils from '../scan/fileUtils' 3 | import i18n from 'i18n' 4 | import oraWrapper from '../utils/oraWrapper' 5 | 6 | export const allowedFileTypes = ['.jar', '.war', '.js', '.zip', '.exe'] 7 | 8 | export const isFileAllowed = (scanOption: string) => { 9 | let valid = false 10 | allowedFileTypes.forEach(fileType => { 11 | if (scanOption.endsWith(fileType)) { 12 | valid = true 13 | } 14 | }) 15 | return valid 16 | } 17 | 18 | export const sendScan = async (config: any) => { 19 | if (!isFileAllowed(config.file)) { 20 | console.log(i18n.__('scanErrorFileMessage')) 21 | process.exit(9) 22 | } else { 23 | fileUtils.checkFilePermissions(config.file) 24 | const client = commonApi.getHttpClient(config) 25 | 26 | const startUploadSpinner = oraWrapper.returnOra(i18n.__('uploadingScan')) 27 | oraWrapper.startSpinner(startUploadSpinner) 28 | 29 | return await client 30 | .sendArtifact(config) 31 | .then(res => { 32 | if (res.statusCode === 201) { 33 | oraWrapper.succeedSpinner( 34 | startUploadSpinner, 35 | i18n.__('uploadingScanSuccessful') 36 | ) 37 | if (config.verbose) { 38 | console.log(i18n.__('responseMessage', res.body)) 39 | } 40 | return res.body.id 41 | } else { 42 | if (config.debug) { 43 | console.log(config) 44 | oraWrapper.failSpinner( 45 | startUploadSpinner, 46 | i18n.__('uploadingScanFail') 47 | ) 48 | console.log(i18n.__('genericServiceError', res.statusCode)) 49 | } 50 | if (res.statusCode === 429) { 51 | console.log(i18n.__('exceededFreeTier')) 52 | process.exit(1) 53 | } 54 | if (res.statusCode === 403) { 55 | console.log(i18n.__('permissionsError')) 56 | process.exit(1) 57 | } 58 | oraWrapper.stopSpinner(startUploadSpinner) 59 | console.log('Contrast Scan Finished') 60 | process.exit(1) 61 | } 62 | }) 63 | .catch(err => { 64 | oraWrapper.stopSpinner(startUploadSpinner) 65 | console.log(err) 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/common/versionChecker.ts: -------------------------------------------------------------------------------- 1 | import { APP_VERSION } from '../constants/constants' 2 | import boxen from 'boxen' 3 | import chalk from 'chalk' 4 | import semver from 'semver' 5 | import commonApi from '../utils/commonApi' 6 | import { constants } from 'http2' 7 | import { ContrastConf } from '../utils/getConfig' 8 | 9 | export const getLatestVersion = async (config: ContrastConf) => { 10 | const client = commonApi.getHttpClient(config) 11 | try { 12 | const res = await client.getLatestVersion() 13 | if (res.statusCode === constants.HTTP_STATUS_OK) { 14 | return res.body 15 | } 16 | } catch (e) { 17 | return undefined 18 | } 19 | } 20 | 21 | export async function findLatestCLIVersion(config: ContrastConf) { 22 | const isCI = process.env.CONTRAST_CODESEC_CI 23 | ? JSON.parse(process.env.CONTRAST_CODESEC_CI.toLowerCase()) 24 | : false 25 | 26 | if (!isCI) { 27 | let latestCLIVersion = await getLatestVersion(config) 28 | 29 | if (latestCLIVersion === undefined) { 30 | config.set('numOfRuns', 0) 31 | console.log( 32 | 'Failed to retrieve latest version info. Continuing execution.' 33 | ) 34 | return 35 | } 36 | 37 | //strip key and remove new lines 38 | latestCLIVersion = latestCLIVersion.substring(8).replace('\n', '') 39 | 40 | if (semver.lt(APP_VERSION, latestCLIVersion)) { 41 | const updateAvailableMessage = `Update available ${chalk.yellow( 42 | APP_VERSION 43 | )} → ${chalk.green(latestCLIVersion)}` 44 | 45 | const npmUpdateAvailableCommand = `Run ${chalk.cyan( 46 | 'npm i @contrast/contrast -g' 47 | )} to update via npm` 48 | 49 | const homebrewUpdateAvailableCommand = `Run ${chalk.cyan( 50 | 'brew install contrastsecurity/tap/contrast' 51 | )} to update via brew` 52 | 53 | console.log( 54 | boxen( 55 | `${updateAvailableMessage}\n${npmUpdateAvailableCommand}\n\n${homebrewUpdateAvailableCommand}`, 56 | { 57 | titleAlignment: 'center', 58 | margin: 1, 59 | padding: 1, 60 | align: 'center' 61 | } 62 | ) 63 | ) 64 | } 65 | } 66 | } 67 | 68 | export async function isCorrectNodeVersion(currentVersion: string) { 69 | return semver.satisfies(currentVersion, '>=16') 70 | } 71 | -------------------------------------------------------------------------------- /src/common/versionChecker.js: -------------------------------------------------------------------------------- 1 | const { APP_VERSION } = require('../constants/constants') 2 | const boxen = require('boxen') 3 | const chalk = require('chalk') 4 | const semver = require('semver') 5 | const commonApi = require('../utils/commonApi') 6 | const { constants } = require('http2') 7 | 8 | const getLatestVersion = async config => { 9 | const client = commonApi.getHttpClient(config) 10 | try { 11 | const res = await client.getLatestVersion() 12 | if (res.statusCode === constants.HTTP_STATUS_OK) { 13 | return res.body 14 | } 15 | } catch (e) { 16 | return undefined 17 | } 18 | } 19 | 20 | const findLatestCLIVersion = async config => { 21 | const isCI = process.env.CONTRAST_CODESEC_CI 22 | ? JSON.parse(process.env.CONTRAST_CODESEC_CI.toLowerCase()) 23 | : false 24 | 25 | if (!isCI) { 26 | let latestCLIVersion = await getLatestVersion(config) 27 | 28 | if (latestCLIVersion === undefined) { 29 | config.set('numOfRuns', 0) 30 | console.log( 31 | 'Failed to retrieve latest version info. Continuing execution.' 32 | ) 33 | return 34 | } 35 | 36 | //strip key and remove new lines 37 | latestCLIVersion = latestCLIVersion.substring(8).replace('\n', '') 38 | 39 | if (semver.lt(APP_VERSION, latestCLIVersion)) { 40 | const updateAvailableMessage = `Update available ${chalk.yellow( 41 | APP_VERSION 42 | )} → ${chalk.green(latestCLIVersion)}` 43 | 44 | const npmUpdateAvailableCommand = `Run ${chalk.cyan( 45 | 'npm i @contrast/contrast -g' 46 | )} to update via npm` 47 | 48 | const homebrewUpdateAvailableCommand = `Run ${chalk.cyan( 49 | 'brew install contrastsecurity/tap/contrast' 50 | )} to update via brew` 51 | 52 | console.log( 53 | boxen( 54 | `${updateAvailableMessage}\n${npmUpdateAvailableCommand}\n\n${homebrewUpdateAvailableCommand}`, 55 | { 56 | titleAlignment: 'center', 57 | margin: 1, 58 | padding: 1, 59 | align: 'center' 60 | } 61 | ) 62 | ) 63 | } 64 | } 65 | } 66 | 67 | const isCorrectNodeVersion = async currentVersion => { 68 | return semver.satisfies(currentVersion, '>=16') 69 | } 70 | 71 | module.exports = { 72 | getLatestVersion, 73 | findLatestCLIVersion, 74 | isCorrectNodeVersion 75 | } 76 | -------------------------------------------------------------------------------- /src/scaAnalysis/dotnet/analysis.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const xml2js = require('xml2js') 3 | const i18n = require('i18n') 4 | 5 | const readAndParseProjectFile = projectFilePath => { 6 | const projectFile = fs.readFileSync(projectFilePath) 7 | 8 | return new xml2js.Parser({ 9 | explicitArray: false, 10 | mergeAttrs: true 11 | }).parseString(projectFile) 12 | } 13 | 14 | const readAndParseLockFile = lockFilePath => { 15 | const lockFile = JSON.parse(fs.readFileSync(lockFilePath).toString()) 16 | 17 | let count = 0 // Used to test if some nodes are deleted 18 | 19 | for (const dependenciesNode in lockFile.dependencies) { 20 | for (const innerNode in lockFile.dependencies[dependenciesNode]) { 21 | const nodeValidation = JSON.stringify( 22 | lockFile.dependencies[dependenciesNode][innerNode] 23 | ) 24 | if (nodeValidation.includes('"type":"Project"')) { 25 | count += 1 26 | delete lockFile.dependencies[dependenciesNode][innerNode] 27 | lockFile.additionalInfo = 'dependenciesNote' 28 | } 29 | } 30 | } 31 | 32 | if (count > 0) { 33 | const multiLevelProjectWarning = () => { 34 | console.log('') 35 | console.log(i18n.__('dependenciesNote')) 36 | } 37 | setTimeout(multiLevelProjectWarning, 7000) 38 | } 39 | 40 | return lockFile 41 | } 42 | 43 | const checkForCorrectFiles = languageFiles => { 44 | if (!languageFiles.includes('packages.lock.json')) { 45 | throw new Error(i18n.__('languageAnalysisHasNoLockFile', '.NET')) 46 | } 47 | 48 | if (!languageFiles.some(i => i.includes('.csproj'))) { 49 | throw new Error(i18n.__('languageAnalysisProjectFileError', '.NET')) 50 | } 51 | } 52 | 53 | const getDotNetDeps = (filePath, languageFiles) => { 54 | checkForCorrectFiles(languageFiles) 55 | const projectFileName = languageFiles.find(fileName => 56 | fileName.includes('.csproj') 57 | ) 58 | const lockFileName = languageFiles.find(fileName => 59 | fileName.includes('.json') 60 | ) 61 | const projectFile = readAndParseProjectFile(filePath + `/${projectFileName}`) 62 | const lockFile = readAndParseLockFile(filePath + `/${lockFileName}`) 63 | 64 | return { projectFile, lockFile } 65 | } 66 | 67 | module.exports = { 68 | getDotNetDeps, 69 | readAndParseProjectFile, 70 | readAndParseLockFile, 71 | checkForCorrectFiles 72 | } 73 | -------------------------------------------------------------------------------- /src/scaAnalysis/javascript/index.js: -------------------------------------------------------------------------------- 1 | const analysis = require('./analysis') 2 | const i18n = require('i18n') 3 | const formatMessage = require('../common/formatMessage') 4 | const scaServiceParser = require('./scaServiceParser') 5 | 6 | const jsAnalysis = async (config, languageFiles) => { 7 | checkForCorrectFiles(languageFiles) 8 | 9 | if (!config.file.endsWith('/')) { 10 | config.file = config.file.concat('/') 11 | } 12 | return buildNodeTree(config, languageFiles.JAVASCRIPT) 13 | } 14 | const buildNodeTree = async (config, files) => { 15 | let analysis = await readFiles(config, files) 16 | const rawNode = await parseFiles(config, files, analysis) 17 | if (config.experimental) { 18 | return scaServiceParser.parseJS(rawNode) 19 | } 20 | return formatMessage.createJavaScriptTSMessage(rawNode) 21 | } 22 | 23 | const readFiles = async (config, files) => { 24 | let js = {} 25 | 26 | js.packageJSON = JSON.parse( 27 | await analysis.readFile(config, files, 'package.json') 28 | ) 29 | 30 | if (files.includes('package-lock.json')) { 31 | js.rawLockFileContents = await analysis.readFile( 32 | config, 33 | files, 34 | 'package-lock.json' 35 | ) 36 | } 37 | if (files.includes('yarn.lock')) { 38 | js.yarn = {} 39 | js.yarn = await analysis.readYarn(config, files, 'yarn.lock') 40 | } 41 | 42 | return js 43 | } 44 | 45 | const parseFiles = async (config, files, js) => { 46 | if (files.includes('package-lock.json')) { 47 | js.npmLockFile = await analysis.parseNpmLockFile(js) 48 | } 49 | if (files.includes('yarn.lock')) { 50 | js = await analysis.parseYarnLockFile(js) 51 | } 52 | 53 | return js 54 | } 55 | 56 | const checkForCorrectFiles = languageFiles => { 57 | if ( 58 | languageFiles.JAVASCRIPT.includes('package-lock.json') && 59 | languageFiles.JAVASCRIPT.includes('yarn.lock') 60 | ) { 61 | throw new Error( 62 | i18n.__('languageAnalysisHasMultipleLockFiles', 'javascript') 63 | ) 64 | } 65 | 66 | if ( 67 | !languageFiles.JAVASCRIPT.includes('package-lock.json') && 68 | !languageFiles.JAVASCRIPT.includes('yarn.lock') 69 | ) { 70 | throw new Error(i18n.__('languageAnalysisHasNoLockFile', 'javascript')) 71 | } 72 | 73 | if (!languageFiles.JAVASCRIPT.includes('package.json')) { 74 | throw new Error(i18n.__('languageAnalysisHasNoPackageJsonFile')) 75 | } 76 | } 77 | module.exports = { 78 | jsAnalysis 79 | } 80 | -------------------------------------------------------------------------------- /src/audit/languageAnalysisEngine/sendSnapshot.js: -------------------------------------------------------------------------------- 1 | const commonApi = require('../../utils/commonApi') 2 | const _ = require('lodash') 3 | const oraFunctions = require('../../utils/oraWrapper') 4 | const i18n = require('i18n') 5 | const oraWrapper = require('../../utils/oraWrapper') 6 | const requestUtils = require('../../utils/requestUtils') 7 | const { performance } = require('perf_hooks') 8 | 9 | const pollSnapshotResults = async (config, snapshotId, client) => { 10 | await requestUtils.sleep(5000) 11 | return client 12 | .getReportStatusById(config, snapshotId) 13 | .then(res => { 14 | return res 15 | }) 16 | .catch(err => { 17 | console.log(err) 18 | }) 19 | } 20 | 21 | const getTimeout = config => { 22 | if (config.timeout) { 23 | return config.timeout 24 | } else { 25 | if (config.verbose) { 26 | console.log('Timeout set to 5 minutes') 27 | } 28 | return 300 29 | } 30 | } 31 | 32 | const pollForSnapshotCompletion = async (config, snapshotId, reportSpinner) => { 33 | const client = commonApi.getHttpClient(config) 34 | const startTime = performance.now() 35 | const timeout = getTimeout(config) 36 | 37 | let complete = false 38 | if (!_.isNil(snapshotId)) { 39 | while (!complete) { 40 | let result = await pollSnapshotResults(config, snapshotId, client) 41 | if (result.statusCode === 200) { 42 | if (result.body.status === 'PROCESSED') { 43 | complete = true 44 | return result.body 45 | } 46 | if (result.body.status === 'FAILED') { 47 | complete = true 48 | if (config.debug) { 49 | oraFunctions.failSpinner( 50 | reportSpinner, 51 | i18n.__('auditNotCompleted') 52 | ) 53 | } 54 | console.log(result.body.errorMessage) 55 | oraWrapper.stopSpinner(reportSpinner) 56 | console.log('Contrast audit finished') 57 | process.exit(1) 58 | } 59 | } 60 | const endTime = performance.now() - startTime 61 | if (requestUtils.millisToSeconds(endTime) > timeout) { 62 | oraFunctions.failSpinner( 63 | reportSpinner, 64 | 'Contrast audit timed out at the specified timeout of ' + 65 | timeout + 66 | ' seconds.' 67 | ) 68 | throw new Error('You can update the timeout using --timeout') 69 | } 70 | } 71 | } 72 | } 73 | 74 | module.exports = { 75 | pollForSnapshotCompletion 76 | } 77 | -------------------------------------------------------------------------------- /src/lambda/scanDetailCompletion.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18n' 2 | import { sleep } from '../utils/requestUtils' 3 | import { getHttpClient } from '../utils/commonApi' 4 | import { ApiParams } from './lambda' 5 | import HTTPClient from '../common/HTTPClient' 6 | import ora from '../utils/oraWrapper' 7 | import { CliError } from './cliError' 8 | import { ERRORS } from './constants' 9 | import { ContrastConf } from '../utils/getConfig' 10 | 11 | const MS_IN_MINUTE = 1000 * 60 12 | 13 | const getScanResources = async ( 14 | config: ContrastConf, 15 | params: ApiParams, 16 | scanId: string, 17 | httpClient: HTTPClient 18 | ) => { 19 | const res = await httpClient.getScanResources(config, params, scanId) 20 | const { statusCode, body } = res 21 | 22 | if (statusCode === 200) { 23 | return res 24 | } 25 | 26 | const { errorCode } = body || {} 27 | throw new CliError(ERRORS.FAILED_TO_GET_SCAN, { statusCode, errorCode }) 28 | } 29 | 30 | const pollScanUntilCompletion = async ( 31 | config: any, 32 | timeoutInMinutes: number, 33 | params: ApiParams, 34 | scanId: string 35 | ) => { 36 | const client = getHttpClient(config) 37 | 38 | const activeStatuses = ['PENDING', 'SCANNING', 'QUEUED'] 39 | const maxEndTime = new Date().getTime() + timeoutInMinutes * MS_IN_MINUTE 40 | const startScanSpinner = ora.returnOra(i18n.__('scanStarted')) 41 | ora.startSpinner(startScanSpinner) 42 | 43 | await sleep(5000) // wait 5 sec before first polling 44 | 45 | let complete = false 46 | while (!complete) { 47 | try { 48 | const result = await exports.getScanResources( 49 | config, 50 | params, 51 | scanId, 52 | client 53 | ) 54 | const { resources: scans } = result.body.data 55 | const staticScans = scans?.filter((s: any) => s.scanType === 2) 56 | complete = staticScans.some((s: any) => !activeStatuses.includes(s.state)) 57 | 58 | if (complete) { 59 | ora.succeedSpinner(startScanSpinner, 'Scan Finished') 60 | return scans 61 | } 62 | 63 | await sleep(2 * 1000) 64 | } catch (error) { 65 | ora.failSpinner(startScanSpinner, i18n.__('scanFailed')) 66 | throw error 67 | } 68 | 69 | if (Date.now() >= maxEndTime) { 70 | ora.failSpinner(startScanSpinner, i18n.__('scanTimedOut')) 71 | throw new CliError(ERRORS.FAILED_TO_GET_SCAN, { 72 | errorCode: 'waitingTimedOut' 73 | }) 74 | } 75 | } 76 | } 77 | 78 | export { pollScanUntilCompletion, getScanResources } 79 | -------------------------------------------------------------------------------- /src/scaAnalysis/php/phpNewServicesMapper.js: -------------------------------------------------------------------------------- 1 | const { keyBy, merge } = require('lodash') 2 | 3 | const parsePHPLockFileForScaServices = phpLockFile => { 4 | const packages = keyBy(phpLockFile.packages, 'name') 5 | const packagesDev = keyBy(phpLockFile['packages-dev'], 'name') 6 | 7 | return merge(buildDepTree(packages, true), buildDepTree(packagesDev, false)) 8 | } 9 | 10 | const buildDepTree = (packages, productionDependency) => { 11 | //builds deps into flat structure 12 | const dependencyTree = {} 13 | 14 | for (const packagesKey in packages) { 15 | const currentObj = packages[packagesKey] 16 | const { group, name } = findGroupAndName(currentObj.name) 17 | 18 | const key = `${group}/${name}@${currentObj.version}` 19 | dependencyTree[key] = { 20 | group: group, 21 | name: name, 22 | version: currentObj.version, 23 | directDependency: true, 24 | productionDependency: productionDependency, 25 | dependencies: [] 26 | } 27 | 28 | const mergedChildDeps = merge( 29 | buildSubDepsIntoFlatStructure(currentObj.require), 30 | buildSubDepsIntoFlatStructure(currentObj['require-dev']) 31 | ) 32 | 33 | for (const childKey in mergedChildDeps) { 34 | const { group, name } = findGroupAndName(childKey) 35 | const builtKey = `${group}/${name}` 36 | dependencyTree[builtKey] = mergedChildDeps[childKey] 37 | } 38 | } 39 | return dependencyTree 40 | } 41 | 42 | // currently sub deps will be built into a flat structure 43 | // but not ingested via the new services as they do not have concrete versions 44 | const buildSubDepsIntoFlatStructure = childDeps => { 45 | const dependencyTree = {} 46 | 47 | for (const dep in childDeps) { 48 | const version = childDeps[dep] 49 | const { group, name } = findGroupAndName(dep) 50 | const key = `${group}/${name}` 51 | dependencyTree[key] = { 52 | group: group, 53 | name: name, 54 | version: version, 55 | directDependency: false, 56 | productionDependency: false, 57 | dependencies: [] 58 | } 59 | } 60 | return dependencyTree 61 | } 62 | 63 | const findGroupAndName = groupAndName => { 64 | if (groupAndName.includes('/')) { 65 | const groupName = groupAndName.split('/') 66 | return { group: groupName[0], name: groupName[1] } 67 | } else { 68 | return { group: groupAndName, name: groupAndName } 69 | } 70 | } 71 | 72 | module.exports = { 73 | parsePHPLockFileForScaServices, 74 | buildDepTree, 75 | buildSubDepsIntoFlatStructure, 76 | findGroupAndName 77 | } 78 | -------------------------------------------------------------------------------- /src/audit/pythonAnalysisEngine/index.js: -------------------------------------------------------------------------------- 1 | const AnalysisEngine = require('./../AnalysisEngine') 2 | 3 | const readPythonProjectFileContents = require('./readPythonProjectFileContents') 4 | const readPipfileLockFileContents = require('./readPipfileLockFileContents') 5 | const parseProjectFileContents = require('./parseProjectFileContents') 6 | const parsePipfileLockContents = require('./parsePipfileLockContents') 7 | const sanitizer = require('./sanitizer') 8 | const i18n = require('i18n') 9 | 10 | module.exports = exports = (language, config, callback) => { 11 | const ae = new AnalysisEngine({ language, config, python: {} }) 12 | 13 | // python's dependancy management is a bit of a wild west. 14 | 15 | // in general there is a requirements.txt but there can also be a test-requirements.txt 16 | // and/or dev-requirements.txt. plus the versions in all of those files do not need 17 | // to be specified meaning that when the file is run it can have different dependancies 18 | // per environment. If the user runs pip freeze it updates the requirements to the exact 19 | // versions running in their local solo environment. We might need to do this or get the 20 | // customer to do this for more accurate results. 21 | 22 | // pip has a ultility module that can be run pipdeptree that will give a dependancy tree 23 | // but this is a package that needs to be installed. 24 | 25 | // there is also pipenv that produces pipfile and pipfile.lock that helps managed dependancies 26 | // to be the same across all the environements. if pipfile.lock is found as well as a requirements.txt 27 | // then pipfile.lock superceeds the requirements. 28 | 29 | // pipenv graph can create a dependancy tree but to run that we have to 30 | // 1) know if pipenv is used to manage python env 31 | // 2) what the name of their pipenv is called 32 | // 3) run the command 33 | // 4)scrape and parse the console output 34 | 35 | // For a breakdown of more python packaging https://realpython.com/pipenv-guide/ is a good guide 36 | // and https://medium.com/python-pandemonium/better-python-dependency-and-package-management-b5d8ea29dff1 37 | 38 | ae.use([ 39 | readPythonProjectFileContents, 40 | parseProjectFileContents, 41 | readPipfileLockFileContents, 42 | parsePipfileLockContents, 43 | sanitizer 44 | ]) 45 | 46 | ae.analyze((err, analysis) => { 47 | if (err) { 48 | callback( 49 | new Error(i18n.__('pythonAnalysisEngineError') + `${err.message}`) 50 | ) 51 | return 52 | } 53 | callback(null, analysis) 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /src/scaAnalysis/repoMode/gradleParser.js: -------------------------------------------------------------------------------- 1 | const g2js = require('gradle-to-js/lib/parser') 2 | 3 | const readBuildGradleFile = async project => { 4 | const gradleFilePath = project.cwd + '/build.gradle' 5 | return await g2js.parseFile(gradleFilePath) 6 | } 7 | 8 | const filterGav = (groupId, artifactId, version, gradleJson) => { 9 | if (groupId === '') { 10 | if (artifactId.includes(':')) { 11 | groupId = artifactId.split(':')[0].replace("'", '') 12 | } 13 | } 14 | 15 | if (version === '') { 16 | if (artifactId.includes(':')) { 17 | artifactId.split(':').length > 2 18 | ? (version = artifactId.split(':')[2].replace("'", '')) 19 | : (version = null) 20 | } 21 | } 22 | 23 | if (artifactId.split(':').length > 1) { 24 | artifactId = artifactId.split(':')[1].replace("'", '') 25 | } 26 | 27 | if (version === null) { 28 | version = getVersion(gradleJson, groupId) 29 | } 30 | return { groupId, artifactId, version } 31 | } 32 | 33 | const parseGradleJson = gradleJson => { 34 | let deps = gradleJson.dependencies 35 | let dependencyTree = {} 36 | 37 | if (deps === undefined) { 38 | console.log('Unable to find any dependencies in your project file.') 39 | process.exit(0) 40 | } 41 | 42 | for (let a in deps) { 43 | let dependencyType = deps[a].type 44 | 45 | if (dependencyType === 'implementation') { 46 | let groupId = deps[a].group 47 | let artifactId = deps[a].name 48 | let version = deps[a].version 49 | 50 | let filteredGav = filterGav(groupId, artifactId, version, gradleJson) 51 | 52 | let depName = 53 | filteredGav.groupId + 54 | '/' + 55 | filteredGav.artifactId + 56 | '@' + 57 | filteredGav.version 58 | 59 | let parsedDependency = { 60 | name: filteredGav.artifactId, 61 | group: filteredGav.groupId, 62 | version: filteredGav.version, 63 | directDependency: true, 64 | isProduction: true, 65 | dependencies: [] 66 | } 67 | dependencyTree[depName] = parsedDependency 68 | } 69 | } 70 | return dependencyTree 71 | } 72 | 73 | const getVersion = (gradleJson, dependencyWithoutVersion) => { 74 | let parentVersion = gradleJson.plugins[0].version 75 | let parentGroupName = gradleJson.plugins[0].id 76 | if (parentGroupName === dependencyWithoutVersion) { 77 | return parentVersion 78 | } else { 79 | return null 80 | } 81 | } 82 | 83 | module.exports = { 84 | readBuildGradleFile, 85 | parseGradleJson, 86 | getVersion, 87 | filterGav 88 | } 89 | -------------------------------------------------------------------------------- /src/lambda/help.ts: -------------------------------------------------------------------------------- 1 | import commandLineUsage from 'command-line-usage' 2 | import i18n from 'i18n' 3 | import { commonHelpLinks } from '../common/commonHelp' 4 | 5 | const lambdaUsageGuide = commandLineUsage([ 6 | { 7 | header: i18n.__('lambdaHeader'), 8 | content: [i18n.__('lambdaSummary')] 9 | }, 10 | { 11 | header: i18n.__('constantsPrerequisitesHeader'), 12 | content: [ 13 | '{bold ' + 14 | i18n.__('lambdaPrerequisitesContentLambdaLanguages') + 15 | '}\n\n' + 16 | '{bold ' + 17 | i18n.__('lambdaPrerequisitesContentLambdaDescriptionTitle') + 18 | '}' + 19 | i18n.__('lambdaPrerequisitesContentLambdaDescription') 20 | ] 21 | }, 22 | { 23 | header: i18n.__('constantsUsage'), 24 | content: [i18n.__('lambdaUsage')] 25 | }, 26 | { 27 | header: i18n.__('constantsOptions'), 28 | content: [ 29 | { 30 | name: '{bold ' + i18n.__('lambdaFunctionNameOption') + '}', 31 | summary: i18n.__('lambdaFunctionNameSummery') 32 | }, 33 | { 34 | name: '{bold ' + i18n.__('lambdaListFunctionsOption') + '}', 35 | summary: i18n.__('lambdaListFunctionsSummery') 36 | }, 37 | { 38 | name: '{bold ' + i18n.__('lambdaEndpointOption') + '}', 39 | summary: 40 | '{bold ' + 41 | i18n.__('constantsOptional') + 42 | '}: ' + 43 | i18n.__('lambdaEndpointSummery') 44 | }, 45 | { 46 | name: '{bold ' + i18n.__('lambdaRegionOption') + '}', 47 | summary: 48 | '{bold ' + 49 | i18n.__('constantsOptional') + 50 | '}: ' + 51 | i18n.__('lambdaRegionSummery') 52 | }, 53 | { 54 | name: '{bold ' + i18n.__('lambdaProfileOption') + '}', 55 | summary: 56 | '{bold ' + 57 | i18n.__('constantsOptional') + 58 | '}: ' + 59 | i18n.__('lambdaProfileSummery') 60 | }, 61 | { 62 | name: '{bold ' + i18n.__('lambdaJsonOption') + '}', 63 | summary: 64 | '{bold ' + 65 | i18n.__('constantsOptional') + 66 | '}: ' + 67 | i18n.__('lambdaJsonSummery') 68 | }, 69 | { 70 | name: '{bold ' + i18n.__('lambdaVerboseOption') + '}', 71 | summary: 72 | '{bold ' + 73 | i18n.__('constantsOptional') + 74 | '}: ' + 75 | i18n.__('lambdaVerbosSummery') 76 | } 77 | ] 78 | }, 79 | { 80 | content: [ 81 | { name: i18n.__('lambdaHelpOption'), summary: i18n.__('helpSummary') } 82 | ] 83 | }, 84 | commonHelpLinks()[0], 85 | commonHelpLinks()[1] 86 | ]) 87 | 88 | export { lambdaUsageGuide } 89 | -------------------------------------------------------------------------------- /src/scaAnalysis/php/analysis.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const i18n = require('i18n') 3 | const _ = require('lodash') 4 | 5 | const readFile = (config, nameOfFile) => { 6 | if (config.file) { 7 | try { 8 | return fs.readFileSync(config.file + '/' + nameOfFile, 'utf8') 9 | } catch (error) { 10 | console.log('Unable to find file') 11 | console.log(error) 12 | } 13 | } 14 | } 15 | 16 | const parseProjectFiles = php => { 17 | try { 18 | // composer.json 19 | php.composerJSON.dependencies = php.composerJSON.require 20 | php.composerJSON.devDependencies = php.composerJSON['require-dev'] 21 | 22 | // composer.lock 23 | php.lockFile = php.rawLockFileContents 24 | let packages = _.keyBy(php.lockFile.packages, 'name') 25 | let packagesDev = _.keyBy(php.lockFile['packages-dev'], 'name') 26 | php.lockFile.dependencies = _.merge(packages, packagesDev) 27 | 28 | const listOfTopDep = Object.keys(php.lockFile.dependencies) 29 | 30 | Object.entries(php.lockFile.dependencies).forEach(([key, value]) => { 31 | if (value.require) { 32 | const listOfRequiresDep = Object.keys(value.require) 33 | listOfRequiresDep.forEach(dep => { 34 | if (!listOfTopDep.includes(dep)) { 35 | addChildDepToLockFileAsOwnObj(php, value['require'], dep) 36 | } 37 | }) 38 | } 39 | 40 | if (value['require-dev']) { 41 | const listOfRequiresDep = Object.keys(value['require-dev']) 42 | listOfRequiresDep.forEach(dep => { 43 | if (!listOfTopDep.includes(dep)) { 44 | addChildDepToLockFileAsOwnObj(php, value['require-dev'], dep) 45 | } 46 | }) 47 | } 48 | }) 49 | formatParentDepToLockFile(php) 50 | delete php.rawLockFileContents 51 | return php 52 | } catch (err) { 53 | return console.log(i18n.__('phpParseComposerLock', php) + `${err.message}`) // not sure on this 54 | } 55 | } 56 | 57 | function addChildDepToLockFileAsOwnObj(php, depObj, key) { 58 | php.lockFile.dependencies[key] = { version: depObj[key] } 59 | } 60 | 61 | function formatParentDepToLockFile(php) { 62 | for (const [key, value] of Object.entries(php.lockFile.dependencies)) { 63 | let requires = {} 64 | for (const [childKey, childValue] of Object.entries(value)) { 65 | if (childKey === 'require' || childKey === 'require-dev') { 66 | requires = _.merge(requires, childValue) 67 | php.lockFile.dependencies[key].requires = requires 68 | delete php.lockFile.dependencies[key].require 69 | delete php.lockFile.dependencies[key]['require-dev'] 70 | } 71 | } 72 | } 73 | } 74 | 75 | module.exports = { 76 | parseProjectFiles, 77 | readFile 78 | } 79 | -------------------------------------------------------------------------------- /src/scaAnalysis/repoMode/mavenParser.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const xml2js = require('xml2js') 3 | 4 | const readPomFile = project => { 5 | const mavenFilePath = project.cwd + '/pom.xml' 6 | const projectFile = fs.readFileSync(mavenFilePath) 7 | let jsonPomFile 8 | xml2js.parseString(projectFile, (err, result) => { 9 | if (err) { 10 | throw err 11 | } 12 | const json = JSON.stringify(result, null) 13 | jsonPomFile = JSON.parse(json) 14 | }) 15 | return jsonPomFile 16 | } 17 | 18 | const getFromVersionsTag = (dependencyName, versionIdentifier, jsonPomFile) => { 19 | // reading: 20 | // 21 | // 1.16 22 | let formattedVersion = versionIdentifier.replace(/[{}]/g, '').replace('$', '') 23 | 24 | if (jsonPomFile.project.properties[0].hasOwnProperty([formattedVersion])) { 25 | return jsonPomFile.project.properties[0][formattedVersion][0] 26 | } else { 27 | return null 28 | } 29 | } 30 | 31 | const parsePomFile = jsonPomFile => { 32 | let dependencyTree = {} 33 | let parsedVersion 34 | let dependencies 35 | jsonPomFile.project.hasOwnProperty('dependencies') 36 | ? (dependencies = jsonPomFile.project.dependencies[0].dependency) 37 | : (dependencies = 38 | jsonPomFile.project.dependencyManagement[0].dependencies[0].dependency) 39 | 40 | for (let x in dependencies) { 41 | let dependencyObject = dependencies[x] 42 | if (!dependencyObject.hasOwnProperty('version')) { 43 | parsedVersion = getVersion(jsonPomFile, dependencyObject) 44 | } else { 45 | dependencyObject.version[0].includes('${versions.') 46 | ? (parsedVersion = getFromVersionsTag( 47 | dependencyObject.artifactId[0], 48 | dependencyObject.version[0], 49 | jsonPomFile 50 | )) 51 | : (parsedVersion = dependencyObject.version[0]) 52 | } 53 | 54 | let depName = 55 | dependencyObject.groupId + 56 | '/' + 57 | dependencyObject.artifactId + 58 | '@' + 59 | parsedVersion 60 | 61 | let parsedDependency = { 62 | name: dependencyObject.artifactId[0], 63 | group: dependencyObject.groupId[0], 64 | version: parsedVersion, 65 | directDependency: true, 66 | productionDependency: true, 67 | dependencies: [] 68 | } 69 | dependencyTree[depName] = parsedDependency 70 | } 71 | return dependencyTree 72 | } 73 | 74 | const getVersion = (pomFile, dependencyWithoutVersion) => { 75 | let parentVersion = pomFile.project.parent[0].version[0] 76 | let parentGroupName = pomFile.project.parent[0].groupId[0] 77 | if (parentGroupName === dependencyWithoutVersion.groupId[0]) { 78 | return parentVersion 79 | } else { 80 | return null 81 | } 82 | } 83 | 84 | module.exports = { 85 | readPomFile, 86 | getVersion, 87 | parsePomFile, 88 | getFromVersionsTag 89 | } 90 | -------------------------------------------------------------------------------- /src/commands/auth/auth.js: -------------------------------------------------------------------------------- 1 | const { v4: uuidv4 } = require('uuid') 2 | const { setConfigValues } = require('../../utils/getConfig') 3 | const open = require('open') 4 | const commonApi = require('../../utils/commonApi') 5 | const { sleep } = require('../../utils/requestUtils') 6 | const i18n = require('i18n') 7 | const { 8 | returnOra, 9 | startSpinner, 10 | failSpinner, 11 | succeedSpinner 12 | } = require('../../utils/oraWrapper') 13 | const { TIMEOUT, AUTH_UI_URL } = require('../../constants/constants') 14 | const parsedCLIOptions = require('../../utils/parsedCLIOptions') 15 | const constants = require('../../cliConstants') 16 | const commandLineUsage = require('command-line-usage') 17 | 18 | const processAuth = async (argv, config) => { 19 | let authParams = await parsedCLIOptions.getCommandLineArgsCustom( 20 | config, 21 | 'auth', 22 | argv, 23 | constants.commandLineDefinitions.authOptionDefinitions 24 | ) 25 | 26 | if (authParams.help) { 27 | console.log(authUsageGuide) 28 | process.exit(0) 29 | } 30 | 31 | const token = uuidv4() 32 | const url = `${AUTH_UI_URL}/?token=${token}` 33 | 34 | console.log(i18n.__('redirectAuth', url)) 35 | 36 | try { 37 | //start a spinner / progress 38 | await setTimeout(() => { 39 | open(url) 40 | }, 0) 41 | 42 | const result = await isAuthComplete(token, TIMEOUT, config) 43 | if (result) { 44 | setConfigValues(config, result) 45 | } 46 | return 47 | } finally { 48 | //spinner stop 49 | } 50 | } 51 | 52 | const isAuthComplete = async (token, timeout, config) => { 53 | const authSpinner = returnOra(i18n.__('authWaitingMessage')) 54 | startSpinner(authSpinner) 55 | const client = commonApi.getHttpClient(config) 56 | let startTime = new Date() 57 | let complete = false 58 | while (!complete) { 59 | let result = await pollAuthResult(token, client) 60 | if (result.statusCode === 200) { 61 | succeedSpinner(authSpinner, i18n.__('authSuccessMessage')) 62 | console.log(i18n.__('runAuthSuccessMessage')) 63 | return result.body 64 | } 65 | let endTime = new Date() - startTime 66 | if (endTime > timeout) { 67 | failSpinner(authSpinner, i18n.__('authTimedOutMessage')) 68 | process.exit(1) 69 | return 70 | } 71 | } 72 | } 73 | 74 | const pollAuthResult = async (token, client) => { 75 | await sleep(5000) 76 | return client 77 | .pollForAuth(token) 78 | .then(res => { 79 | return res 80 | }) 81 | .catch(err => { 82 | console.log(err) 83 | }) 84 | } 85 | 86 | const authUsageGuide = commandLineUsage([ 87 | { 88 | header: i18n.__('authHeader'), 89 | content: [i18n.__('constantsAuthHeaderContents')] 90 | }, 91 | { 92 | header: i18n.__('constantsAuthUsageHeader'), 93 | content: [i18n.__('constantsAuthUsageContents')] 94 | } 95 | ]) 96 | 97 | module.exports = { 98 | processAuth: processAuth 99 | } 100 | -------------------------------------------------------------------------------- /src/scan/scanController.js: -------------------------------------------------------------------------------- 1 | const i18n = require('i18n') 2 | const { 3 | returnOra, 4 | startSpinner, 5 | succeedSpinner, 6 | stopSpinner 7 | } = require('../utils/oraWrapper') 8 | const populateProjectIdAndProjectName = require('./populateProjectIdAndProjectName') 9 | const scan = require('./scan') 10 | const scanResults = require('./scanResults') 11 | const autoDetection = require('./autoDetection') 12 | const fileFunctions = require('./fileUtils') 13 | const { performance } = require('perf_hooks') 14 | 15 | const getTimeout = config => { 16 | if (config.timeout) { 17 | return config.timeout 18 | } else { 19 | if (config.verbose) { 20 | console.log('Timeout set to 5 minutes') 21 | } 22 | return 300 23 | } 24 | } 25 | 26 | const fileAndLanguageLogic = async configToUse => { 27 | if (configToUse.file) { 28 | if (!fileFunctions.fileExists(configToUse.file)) { 29 | console.log(i18n.__('fileNotExist')) 30 | process.exit(1) 31 | } 32 | 33 | if (fileFunctions.fileIsEmpty(configToUse.file)) { 34 | console.log(i18n.__('scanFileIsEmpty')) 35 | process.exit(1) 36 | } 37 | return configToUse 38 | } else { 39 | if (configToUse.file === undefined || configToUse.file === null) { 40 | await autoDetection.autoDetectFileAndLanguage(configToUse) 41 | } 42 | } 43 | } 44 | 45 | const startScan = async configToUse => { 46 | const startTime = performance.now() 47 | await fileAndLanguageLogic(configToUse) 48 | 49 | let newProject 50 | 51 | if (!configToUse.projectId) { 52 | const { projectId, isNewProject } = 53 | await populateProjectIdAndProjectName.populateProjectId(configToUse) 54 | configToUse.projectId = projectId 55 | newProject = isNewProject 56 | } else { 57 | newProject = false 58 | } 59 | const codeArtifactId = await scan.sendScan(configToUse) 60 | 61 | if (!configToUse.ff) { 62 | const startScanSpinner = returnOra('🚀 Contrast Scan started') 63 | startSpinner(startScanSpinner) 64 | const scanDetail = await scanResults.returnScanResults( 65 | configToUse, 66 | codeArtifactId, 67 | newProject, 68 | getTimeout(configToUse), 69 | startScanSpinner 70 | ) 71 | 72 | const scanResultsInstances = await scanResults.returnScanResultsInstances( 73 | configToUse, 74 | scanDetail.id 75 | ) 76 | 77 | const endTime = performance.now() 78 | const scanDurationMs = endTime - startTime 79 | if (scanResultsInstances.statusCode !== 200) { 80 | stopSpinner(startScanSpinner) 81 | console.log('Result Service is unavailable, please try again later') 82 | process.exit(1) 83 | } else { 84 | succeedSpinner(startScanSpinner, 'Contrast Scan complete') 85 | console.log( 86 | `----- Scan completed in ${(scanDurationMs / 1000).toFixed(2)}s -----` 87 | ) 88 | return { 89 | scanDetail, 90 | scanResultsInstances: scanResultsInstances.body 91 | } 92 | } 93 | } 94 | } 95 | 96 | module.exports = { 97 | startScan: startScan 98 | } 99 | -------------------------------------------------------------------------------- /src/scaAnalysis/python/analysis.js: -------------------------------------------------------------------------------- 1 | const multiReplace = require('string-multiple-replace') 2 | const fs = require('fs') 3 | const i18n = require('i18n') 4 | 5 | const readAndParseProjectFile = file => { 6 | const filePath = filePathForWindows(file + '/Pipfile') 7 | const pipFile = fs.readFileSync(filePath, 'utf8') 8 | 9 | const matcherObj = { '"': '' } 10 | const sequencer = ['"'] 11 | const parsedPipfile = multiReplace(pipFile, matcherObj, sequencer) 12 | 13 | const pythonArray = parsedPipfile.split('\n') 14 | 15 | return pythonArray.filter(element => element !== '' && !element.includes('#')) 16 | } 17 | 18 | const readAndParseLockFile = file => { 19 | const filePath = filePathForWindows(file + '/Pipfile.lock') 20 | const lockFile = fs.readFileSync(filePath, 'utf8') 21 | let parsedPipLock = JSON.parse(lockFile) 22 | parsedPipLock['defaults'] = parsedPipLock['default'] 23 | delete parsedPipLock['default'] 24 | return parsedPipLock 25 | } 26 | 27 | const readLockFile = file => { 28 | const filePath = filePathForWindows(file + '/Pipfile.lock') 29 | const lockFile = fs.readFileSync(filePath, 'utf8') 30 | let parsedPipLock = JSON.parse(lockFile) 31 | return parsedPipLock['default'] 32 | } 33 | 34 | const scaPythonParser = pythonDependencies => { 35 | let pythonParsedDeps = {} 36 | for (let key in pythonDependencies) { 37 | pythonParsedDeps[key] = {} 38 | pythonParsedDeps[key].version = pythonDependencies[key].version.replace( 39 | '==', 40 | '' 41 | ) 42 | pythonParsedDeps[key].group = null 43 | pythonParsedDeps[key].name = key 44 | pythonParsedDeps[key].productionDependency = true 45 | pythonParsedDeps[key].dependencies = [] 46 | pythonParsedDeps[key].directDependency = true 47 | } 48 | return pythonParsedDeps 49 | } 50 | 51 | const checkForCorrectFiles = languageFiles => { 52 | if (!languageFiles.includes('Pipfile.lock')) { 53 | throw new Error(i18n.__('languageAnalysisHasNoLockFile', 'python')) 54 | } 55 | 56 | if (!languageFiles.includes('Pipfile')) { 57 | throw new Error(i18n.__('languageAnalysisProjectFileError', 'python')) 58 | } 59 | } 60 | 61 | const getPythonDeps = (config, languageFiles) => { 62 | try { 63 | if (config.experimental) { 64 | let pythonLockFileContents = readLockFile(config.file) 65 | return scaPythonParser(pythonLockFileContents) 66 | } else { 67 | checkForCorrectFiles(languageFiles) 68 | const parseProject = readAndParseProjectFile(config.file) 69 | const parsePip = readAndParseLockFile(config.file) 70 | 71 | return { pipfileLock: parsePip, pipfilDependanceies: parseProject } 72 | } 73 | } catch (err) { 74 | console.log(err.message.toString()) 75 | process.exit(1) 76 | } 77 | } 78 | 79 | const filePathForWindows = path => { 80 | if (process.platform === 'win32') { 81 | path = path.replace(/\//g, '\\') 82 | } 83 | return path 84 | } 85 | 86 | module.exports = { 87 | getPythonDeps, 88 | scaPythonParser, 89 | readAndParseLockFile, 90 | readAndParseProjectFile, 91 | checkForCorrectFiles, 92 | readLockFile 93 | } 94 | -------------------------------------------------------------------------------- /src/lambda/__mocks__/lambdaConfig.json: -------------------------------------------------------------------------------- 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 | } 43 | -------------------------------------------------------------------------------- /src/scan/autoDetection.js: -------------------------------------------------------------------------------- 1 | const i18n = require('i18n') 2 | const fileFinder = require('./fileUtils') 3 | 4 | const autoDetectFingerprintInfo = async filePath => { 5 | let complexObj = await fileFinder.findAllFiles(filePath) 6 | let result = [] 7 | let count = 0 8 | complexObj.forEach(i => { 9 | count++ 10 | result.push({ filePath: i, id: count.toString() }) 11 | }) 12 | 13 | return result 14 | } 15 | 16 | const autoDetectFileAndLanguage = async configToUse => { 17 | const entries = await fileFinder.findFile() 18 | 19 | if (entries.length === 1) { 20 | console.log(i18n.__('foundScanFile', entries[0])) 21 | 22 | if (hasWhiteSpace(entries[0])) { 23 | console.log(i18n.__('fileHasWhiteSpacesError')) 24 | process.exit(1) 25 | } 26 | 27 | if (fileFinder.fileIsEmpty(entries[0])) { 28 | console.log(i18n.__('scanFileIsEmpty')) 29 | process.exit(1) 30 | } 31 | 32 | configToUse.file = entries[0] 33 | if (configToUse.name === undefined) { 34 | configToUse.name = entries[0] 35 | } 36 | } else { 37 | errorOnFileDetection(entries) 38 | } 39 | } 40 | 41 | const autoDetectAuditFilesAndLanguages = async filePath => { 42 | let languagesFound = [] 43 | 44 | console.log(i18n.__('searchingAuditFileDirectory', filePath)) 45 | 46 | await fileFinder.findFilesJava(languagesFound, filePath) 47 | await fileFinder.findFilesJavascript(languagesFound, filePath) 48 | await fileFinder.findFilesPython(languagesFound, filePath) 49 | await fileFinder.findFilesGo(languagesFound, filePath) 50 | await fileFinder.findFilesPhp(languagesFound, filePath) 51 | await fileFinder.findFilesRuby(languagesFound, filePath) 52 | await fileFinder.findFilesDotNet(languagesFound, filePath) 53 | 54 | if (languagesFound) { 55 | return languagesFound 56 | } 57 | 58 | return [] 59 | } 60 | 61 | const hasWhiteSpace = s => { 62 | const filename = s.split('/').pop() 63 | return filename.indexOf(' ') >= 0 64 | } 65 | 66 | const errorOnFileDetection = entries => { 67 | if (entries.length > 1) { 68 | console.log(i18n.__('searchingDirectoryScan')) 69 | for (let file in entries) { 70 | console.log('-', entries[file]) 71 | } 72 | console.log('') 73 | console.log(i18n.__('specifyFileScanError')) 74 | } else { 75 | console.log(i18n.__('noFileFoundScan')) 76 | console.log('') 77 | console.log(i18n.__('specifyFileScanError')) 78 | } 79 | process.exit(1) 80 | } 81 | 82 | const errorOnAuditFileDetection = entries => { 83 | if (entries.length > 1) { 84 | console.log(i18n.__('searchingDirectoryScan')) 85 | for (let file in entries) { 86 | console.log('-', entries[file]) 87 | } 88 | console.log('') 89 | console.log(i18n.__('specifyFileAuditNotFound')) 90 | } else { 91 | console.log(i18n.__('noFileFoundScan')) 92 | console.log('') 93 | console.log(i18n.__('specifyFileAuditNotFound')) 94 | } 95 | } 96 | 97 | module.exports = { 98 | autoDetectFileAndLanguage, 99 | errorOnFileDetection, 100 | autoDetectAuditFilesAndLanguages, 101 | errorOnAuditFileDetection, 102 | autoDetectFingerprintInfo 103 | } 104 | -------------------------------------------------------------------------------- /src/audit/report/reportingFeature.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getReport, 3 | printNoVulnFoundMsg, 4 | printVulnerabilityResponse 5 | } from './commonReportingFunctions' 6 | import { 7 | convertGenericToTypedLibraryVulns, 8 | severityCountAllLibraries 9 | } from './utils/reportUtils' 10 | import i18n from 'i18n' 11 | import chalk from 'chalk' 12 | import * as constants from '../../constants/constants' 13 | import { SeverityCountModel } from './models/severityCountModel' 14 | import * as common from '../../common/fail' 15 | 16 | export function convertKeysToStandardFormat(config: any, guidance: any) { 17 | let convertedGuidance = guidance 18 | 19 | switch (config.language) { 20 | case constants.supportedLanguages.JAVA: 21 | case constants.supportedLanguages.GO: 22 | case constants.supportedLanguages.PHP: 23 | break 24 | case constants.supportedLanguages.NODE: 25 | case constants.supportedLanguages.DOTNET: 26 | case constants.supportedLanguages.PYTHON: 27 | case constants.supportedLanguages.RUBY: 28 | convertedGuidance = convertJSDotNetPython(guidance) 29 | break 30 | } 31 | return convertedGuidance 32 | } 33 | 34 | export function convertJSDotNetPython(guidance: any) { 35 | const returnObject = {} 36 | 37 | Object.entries(guidance).forEach(([key, value]) => { 38 | const splitKey = key.split('/') 39 | if (splitKey.length === 2) { 40 | // @ts-ignore 41 | returnObject[splitKey[1]] = value 42 | } 43 | }) 44 | return returnObject 45 | } 46 | 47 | export function formatVulnerabilityOutput( 48 | libraryVulnerabilityResponse: any, 49 | id: string, 50 | config: any, 51 | remediationGuidance: any 52 | ) { 53 | const vulnerableLibraries = convertGenericToTypedLibraryVulns( 54 | libraryVulnerabilityResponse 55 | ) 56 | 57 | const guidance = convertKeysToStandardFormat(config, remediationGuidance) 58 | 59 | const numberOfVulnerableLibraries = vulnerableLibraries.length 60 | 61 | if (numberOfVulnerableLibraries === 0) { 62 | printNoVulnFoundMsg() 63 | return [false, 0, [new SeverityCountModel()]] 64 | } else { 65 | let numberOfCves = 0 66 | vulnerableLibraries.forEach(lib => (numberOfCves += lib.cveArray.length)) 67 | 68 | const hasSomeVulnerabilitiesReported = printVulnerabilityResponse( 69 | config, 70 | vulnerableLibraries, 71 | numberOfVulnerableLibraries, 72 | numberOfCves, 73 | guidance 74 | ) 75 | let severityCount = new SeverityCountModel() 76 | severityCount = severityCountAllLibraries( 77 | vulnerableLibraries, 78 | severityCount 79 | ) 80 | severityCount.total = severityCount.getTotal 81 | return [hasSomeVulnerabilitiesReported, numberOfCves, severityCount] 82 | } 83 | } 84 | 85 | export async function vulnerabilityReportV2(config: any, reportId: string) { 86 | console.log() 87 | const reportResponse = await getReport(config, reportId) 88 | 89 | if (reportResponse !== undefined) { 90 | let output = formatVulnerabilityOutput( 91 | reportResponse.vulnerabilities, 92 | config.applicationId, 93 | config, 94 | reportResponse.remediationGuidance 95 | ? reportResponse.remediationGuidance 96 | : {} 97 | ) 98 | 99 | if (config.fail) { 100 | common.processFail(config, output[2]) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/constants/lambda.js: -------------------------------------------------------------------------------- 1 | const lambda = { 2 | failedToStartScan: 'Failed to start scan', 3 | failedToParseArn: 'Failed to parse ARN', 4 | failedToGetScan: 'Failed to get scan', 5 | missingLambdaConfig: 'Missing Lambda Configuration', 6 | missingLambdaArn: 'Missing Lambda ARN', 7 | validationFailed: 'Request validation failed', 8 | missingFunctionName: 9 | 'Required parameter --function-name is missing.\nRun command with --help to see usage', 10 | failedToGetResults: 'Failed to get results', 11 | missingResults: 'Missing vulnerabilities', 12 | awsError: 'AWS error', 13 | missingFlagArguments: 14 | 'The following flags are missing an arguments:\n{{flags}}', 15 | notSupportedFlags: 16 | 'The following flags are not supported:\n{{flags}}\nRun command with --help to see usage', 17 | layerNotFound: 18 | 'The layer {{layerArn}} could not be found. The scan will continue without it', 19 | 20 | // ====== general ===== // 21 | noVulnerabilitiesFound: '👏 No vulnerabilities found', 22 | scanCompleted: '----- Scan completed {{time}}s -----', 23 | sendingScanRequest: 24 | '{{icon}} Sending Lambda Function scan request to Contrast', 25 | scanRequestedSuccessfully: '{{icon}} Scan requested successfully', 26 | fetchingConfiguration: 27 | '{{icon}} Fetching configuration and policies for Lambda Function {{functionName}}', 28 | fetchedConfiguration: '{{icon}} Fetched configuration from AWS', 29 | 30 | // ====== scan polling ===== // 31 | scanStarted: 'Scan Started', 32 | scanFailed: 'Scan Failed', 33 | scanTimedOut: 'Scan timed out', 34 | 35 | // ====== lambda utils ===== // 36 | loadingFunctionList: 'Loading lambda function list', 37 | functionsFound: '{{count}} functions found', 38 | noFunctionsFound: 'No functions found', 39 | failedToLoadFunctions: 'Failed to load lambda functions', 40 | availableForScan: '{{icon}} {{count}} available for scan', 41 | runtimeCount: '----- {{runtime}} ({{count}}) -----', 42 | 43 | // ====== print vulnerabilities ===== // 44 | gatherResults: 'Gathering results...', 45 | doneGatherResults: 'Done gathering results', 46 | whatHappenedTitle: 'What happened:', 47 | whatHappenedItem: '{{policy}} have:\n{{comments}}\n', 48 | recommendation: 'Recommendation:', 49 | vulnerableDependency: 'Vulnerable dependency', 50 | dependenciesCount: { 51 | one: '1 Dependency', 52 | other: '%s Dependencies' 53 | }, 54 | foundVulnerabilities: { 55 | one: 'Found 1 vulnerability', 56 | other: 'Found %s vulnerabilities' 57 | }, 58 | vulnerableDependencyDescriptions: 59 | '{packageName} (v{version}) has {NUM} known {NUM, plural,one{CVE}other{CVEs}}\n {cves}', 60 | 61 | // ====== errorCodes ===== // 62 | something_went_wrong: 'Something went wrong', 63 | not_found_404: '404 error - Not found', 64 | internal_error: 'Internal error', 65 | inactive_account: 66 | 'Scanning a function of an inactive account is not supported', 67 | not_supported_runtime: 68 | 'Scanning resource of runtime "{{runtime}}" is not supported.\nSupported runtimes: {{supportedRuntimes}}', 69 | not_supported_onboard_account: 70 | 'Scanning a function of onboard account is not supported', 71 | scan_lock: 72 | 'Other scan is still running. Please wait until the previous scan finishes', 73 | 74 | // ====== statuses ===== // 75 | unsupported: 'unsupported', 76 | excluded: 'excluded', 77 | canceled: 'canceled', 78 | failed: 'failed', 79 | dismissed: 'dismissed' 80 | } 81 | 82 | module.exports = { 83 | lambda 84 | } 85 | -------------------------------------------------------------------------------- /src/lambda/lambdaUtils.ts: -------------------------------------------------------------------------------- 1 | import logSymbols from 'log-symbols' 2 | import chalk from 'chalk' 3 | import i18n from 'i18n' 4 | import { 5 | FunctionConfiguration, 6 | ListFunctionsCommand 7 | } from '@aws-sdk/client-lambda' 8 | import { groupBy, sortBy } from 'lodash' 9 | import { getLambdaClient } from './aws' 10 | import ora from '../utils/oraWrapper' 11 | import { LambdaOptions } from './lambda' 12 | import { log, getReadableFileSize } from './logUtils' 13 | 14 | type RuntimeLanguage = 'java' | 'python' | 'node' 15 | 16 | type FilterLambdas = { 17 | runtimes: RuntimeLanguage[] 18 | filterText?: string 19 | } 20 | 21 | /** 22 | * 23 | * @param fucntions all user lambdas 24 | * @param options filter values: runtime / free text 25 | * @returns 26 | */ 27 | const printAvailableLambdas = ( 28 | fucntions: FunctionConfiguration[] = [], 29 | options: FilterLambdas 30 | ) => { 31 | const { runtimes, filterText = '' } = options 32 | const searchValue = filterText?.trim().toLowerCase() 33 | 34 | const filteredFunctions = fucntions 35 | .filter(f => runtimes.some(r => f.Runtime?.includes(r))) 36 | .filter(f => f.FunctionName?.toLowerCase().includes(searchValue)) 37 | log( 38 | i18n.__('availableForScan', { 39 | icon: logSymbols.success, 40 | count: `${filteredFunctions.length}` 41 | }) 42 | ) 43 | const groupByRuntime = groupBy(filteredFunctions, 'Runtime') 44 | 45 | Object.entries(groupByRuntime).forEach(([runtime, arr]) => { 46 | const sorted = sortBy(arr, 'FunctionName') 47 | const count = `${arr.filter(a => a.Runtime === runtime).length}` 48 | 49 | log(chalk.gray(i18n.__('runtimeCount', { runtime, count }))) 50 | sorted.forEach(f => { 51 | const size = f.CodeSize ? getReadableFileSize(f.CodeSize) : '' 52 | log(`${f.FunctionName} ${chalk.gray(`(${size})`)}`) 53 | }) 54 | }) 55 | } 56 | 57 | /** 58 | * 59 | * @param lambdaOptions to create lambdaClient 60 | * @returns list of all user lambdas that availbale to scan 61 | */ 62 | const getAllLambdas = async (lambdaOptions: LambdaOptions) => { 63 | const functions: FunctionConfiguration[] = [] 64 | const spinner = ora.returnOra(i18n.__('loadingFunctionList')) 65 | 66 | try { 67 | const client = getLambdaClient(lambdaOptions) 68 | const command = new ListFunctionsCommand({}) 69 | 70 | ora.startSpinner(spinner) 71 | 72 | const data = await client.send(command) 73 | const { Functions } = data 74 | let { NextMarker } = data 75 | 76 | if (!Functions?.length) { 77 | ora.failSpinner(spinner, i18n.__('noFunctionsFound')) 78 | return 79 | } 80 | 81 | functions.push(...Functions) 82 | spinner.text = i18n.__('functionsFound', { count: `${functions.length}` }) 83 | 84 | // pagination on functions 85 | while (NextMarker) { 86 | command.input.Marker = NextMarker 87 | const chank = await client.send(command) 88 | 89 | if (chank.Functions?.length) { 90 | functions.push(...chank.Functions) 91 | spinner.text = i18n.__('functionsFound', { 92 | count: `${functions.length}` 93 | }) 94 | } 95 | 96 | NextMarker = chank.NextMarker 97 | } 98 | 99 | ora.succeedSpinner( 100 | spinner, 101 | i18n.__('functionsFound', { count: `${functions.length}` }) 102 | ) 103 | } catch (error) { 104 | ora.failSpinner(spinner, i18n.__('failedToLoadFunctions')) 105 | throw error 106 | } 107 | 108 | return functions 109 | } 110 | 111 | export { getAllLambdas, printAvailableLambdas } 112 | -------------------------------------------------------------------------------- /src/common/errorHandling.js: -------------------------------------------------------------------------------- 1 | const i18n = require('i18n') 2 | 3 | const libraryAnalysisError = () => { 4 | console.log(i18n.__('libraryAnalysisError')) 5 | } 6 | 7 | const snapshotFailureError = () => { 8 | console.log(i18n.__('snapshotFailureMessage')) 9 | } 10 | 11 | const vulnerabilitiesFailureError = () => { 12 | console.log(i18n.__('vulnerabilitiesFailureMessage')) 13 | } 14 | 15 | const reportFailureError = () => { 16 | console.log(i18n.__('auditReportFailureMessage')) 17 | } 18 | 19 | const genericError = () => { 20 | console.error(i18n.__('genericErrorMessage')) 21 | process.exit(1) 22 | } 23 | 24 | const unauthenticatedError = () => { 25 | generalError('unauthenticatedErrorHeader', 'unauthenticatedErrorMessage') 26 | } 27 | 28 | const badRequestError = catalogue => { 29 | catalogue === true 30 | ? generalError('badRequestErrorHeader', 'badRequestCatalogueErrorMessage') 31 | : generalError('badRequestErrorHeader', 'badRequestErrorMessage') 32 | } 33 | 34 | const forbiddenError = () => { 35 | generalError('forbiddenRequestErrorHeader', 'forbiddenRequestErrorMessage') 36 | process.exit(1) 37 | } 38 | 39 | const proxyError = () => { 40 | generalError('proxyErrorHeader', 'proxyErrorMessage') 41 | } 42 | 43 | const maxAppError = () => { 44 | generalError( 45 | 'No applications remaining', 46 | 'You have reached the maximum number of application you can create.' 47 | ) 48 | process.exit(1) 49 | } 50 | 51 | const failOptionError = () => { 52 | console.log( 53 | '\n ******************************** ' + 54 | i18n.__('snapshotFailureHeader') + 55 | ' ********************************\n' + 56 | i18n.__('failOptionErrorMessage') 57 | ) 58 | } 59 | 60 | /** 61 | * You don't have to pass `i18n` translation. 62 | * String that didn't exists on translations will pass as regular string 63 | * @param header title for the error 64 | * @param message message for the error 65 | * @returns error in general format 66 | */ 67 | const getErrorMessage = (header, message) => { 68 | // prettier-ignore 69 | const title = `******************************** ${i18n.__(header)} ********************************` 70 | const multiLine = message?.includes('\n') 71 | let finalMessage = '' 72 | 73 | // i18n split the line if it includes '\n' 74 | if (multiLine) { 75 | finalMessage = `\n${message}` 76 | } else if (message) { 77 | finalMessage = `\n${i18n.__(message)}` 78 | } 79 | 80 | return `${title}${finalMessage}` 81 | } 82 | 83 | const generalError = (header, message) => { 84 | const finalMessage = getErrorMessage(header, message) 85 | console.log(finalMessage) 86 | } 87 | 88 | const findCommandOnError = unknownOptions => { 89 | const commandKeywords = { 90 | auth: 'auth', 91 | audit: 'audit', 92 | scan: 'scan', 93 | lambda: 'lambda', 94 | config: 'config' 95 | } 96 | 97 | const containsCommandKeyword = unknownOptions.some( 98 | command => commandKeywords[command] 99 | ) 100 | 101 | if (containsCommandKeyword) { 102 | const foundCommands = unknownOptions.filter( 103 | command => commandKeywords[command] 104 | ) 105 | 106 | //return the first command found 107 | return foundCommands[0] 108 | } 109 | } 110 | 111 | module.exports = { 112 | genericError, 113 | unauthenticatedError, 114 | badRequestError, 115 | forbiddenError, 116 | proxyError, 117 | failOptionError, 118 | generalError, 119 | getErrorMessage, 120 | libraryAnalysisError, 121 | findCommandOnError, 122 | snapshotFailureError, 123 | vulnerabilitiesFailureError, 124 | reportFailureError, 125 | maxAppError 126 | } 127 | -------------------------------------------------------------------------------- /src/scaAnalysis/common/auditReport.js: -------------------------------------------------------------------------------- 1 | const { 2 | getSeverityCounts, 3 | createSummaryMessageTop, 4 | printVulnInfo, 5 | getReportTable, 6 | getIssueRow, 7 | printNoVulnFoundMsg 8 | } = require('../../audit/report/commonReportingFunctions') 9 | const { orderBy } = require('lodash') 10 | const { assignBySeverity } = require('../../scan/formatScanOutput') 11 | const chalk = require('chalk') 12 | const { CE_URL } = require('../../constants/constants') 13 | const common = require('../../common/fail') 14 | const i18n = require('i18n') 15 | 16 | const processAuditReport = (config, results) => { 17 | let severityCounts = {} 18 | if (results !== undefined) { 19 | severityCounts = formatScaServicesReport(config, results) 20 | } 21 | 22 | if (config.fail) { 23 | common.processFail(config, severityCounts) 24 | } 25 | } 26 | const formatScaServicesReport = (config, results) => { 27 | const projectOverviewCount = getSeverityCounts(results) 28 | 29 | if (projectOverviewCount.total === 0) { 30 | printNoVulnFoundMsg() 31 | return projectOverviewCount 32 | } else { 33 | let total = 0 34 | const numberOfCves = results.length 35 | const table = getReportTable() 36 | let contrastHeaderNumCounter = 0 37 | let assignPriorityToResults = results.map(result => 38 | assignBySeverity(result, result) 39 | ) 40 | const numberOfVulns = results 41 | .map(result => result.vulnerabilities) 42 | .reduce((a, b) => { 43 | return (total += b.length) 44 | }, 0) 45 | const outputOrderedByLowestSeverityAndLowestNumOfCvesFirst = orderBy( 46 | assignPriorityToResults, 47 | [ 48 | reportListItem => { 49 | return reportListItem.priority 50 | }, 51 | reportListItem => { 52 | return reportListItem.vulnerabilities.length 53 | } 54 | ], 55 | ['asc', 'desc'] 56 | ) 57 | 58 | for (const result of outputOrderedByLowestSeverityAndLowestNumOfCvesFirst) { 59 | contrastHeaderNumCounter++ 60 | const cvesNum = result.vulnerabilities.length 61 | const grammaticallyCorrectVul = 62 | result.vulnerabilities.length > 1 ? 'vulnerabilities' : 'vulnerability' 63 | 64 | const headerColour = chalk.hex(result.colour) 65 | const headerRow = [ 66 | headerColour( 67 | `CONTRAST-${contrastHeaderNumCounter.toString().padStart(3, '0')}` 68 | ), 69 | headerColour(`-`), 70 | headerColour(`[${result.severity}] `) + 71 | headerColour.bold(`${result.artifactName}`) + 72 | ` introduces ${cvesNum} ${grammaticallyCorrectVul}` 73 | ] 74 | 75 | const adviceRow = [ 76 | chalk.bold(`Advice`), 77 | chalk.bold(`:`), 78 | `Change to version ${result.remediationAdvice.latestStableVersion}` 79 | ] 80 | 81 | let assignPriorityToVulns = result.vulnerabilities.map(result => 82 | assignBySeverity(result, result) 83 | ) 84 | const issueRow = getIssueRow(assignPriorityToVulns) 85 | 86 | table.push(headerRow, issueRow, adviceRow) 87 | console.log() 88 | } 89 | 90 | console.log() 91 | createSummaryMessageTop(numberOfCves, numberOfVulns) 92 | console.log(table.toString() + '\n') 93 | printVulnInfo(projectOverviewCount) 94 | 95 | if (config.host !== CE_URL) { 96 | console.log('\n' + chalk.bold(i18n.__('auditServicesMessageForTS'))) 97 | console.log( 98 | `${config.host}/Contrast/static/ng/index.html#/${config.organizationId}/applications/${config.applicationId}/libs` 99 | ) 100 | } 101 | return projectOverviewCount 102 | } 103 | } 104 | module.exports = { 105 | formatScaServicesReport, 106 | processAuditReport 107 | } 108 | -------------------------------------------------------------------------------- /src/audit/AnalysisEngine.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The 'AnalysisEngine' type represents a simple state machine that can be used 3 | * to move through a list of steps sequentially to analyze a project. Consumers 4 | * construct their own steps and add them to the state machine in their desired 5 | * order. Upon completion the state machine can callback to the consumer that 6 | * originally invoked them with the results of the analysis. 7 | */ 8 | class AnalysisEngine { 9 | /** 10 | * Constructor that creates a new state machine instance. Accepts an optional 11 | * argument that initializes the internal state. 12 | * 13 | * @param {Object} initAnalysis - state used to initialize internal state 14 | * 15 | * @example 16 | * const ae = new AnalysisEngine() 17 | * const ae = new AnalysisEngine({ someInfo: [1, 2, 3] }) 18 | */ 19 | constructor(initAnalysis = {}) { 20 | this.analyzers = [] 21 | this.analysis = { ...initAnalysis } 22 | } 23 | 24 | /** 25 | * Takes either a function or a list of functions and adds them in sequential 26 | * order to a list. The list will be executed at a later time as the steps of 27 | * the state machine. 28 | * 29 | * Functions must follow the signature (analysis, next) where: 30 | * 'analysis' is an object that represents the current internal state 31 | * 'next' is a function to be invoked when the step is complete 32 | * 33 | * The function signature of 'next' is (err) where: 34 | * 'err' is an Error that occurred during the previous step invoked 35 | * 36 | * @param {function(analysis: object, next: function)|function[]} analyzer - 37 | * the analyzer(s) to be added to the list of steps in sequential order 38 | * 39 | * @example 40 | * const myAnalyzer = (analysis, next) => { 41 | * // Perform business logic 42 | * // Add results to 'analysis' 43 | * analysis.result = ... 44 | * 45 | * // Signal the next analyzer/step to be invoked 46 | * next() 47 | * } 48 | * 49 | * ae.use(myAnalyzer) 50 | */ 51 | use(analyzer) { 52 | if (Array.isArray(analyzer)) { 53 | this.analyzers = [...this.analyzers, ...analyzer] 54 | return 55 | } 56 | 57 | this.analyzers.push(analyzer) 58 | } 59 | 60 | /** 61 | * Starts the execution of the state machine given the steps it is to use. 62 | * When complete it callbacks back to the consumer that invoked it. The 63 | * callbacks signature is (err, analysis) where: 64 | * 'err' is an Error from one of the steps that prevented completion 65 | * 'analysis' is the final internal state 66 | * 67 | * @param {function(err: Error, analysis: object)} callback - callback to be 68 | * invoked when state machine complete or fails prematurely 69 | * @param config:object containing config - needed for Java analysis - optional for other languages 70 | */ 71 | analyze(callback, config) { 72 | let i = 0 73 | 74 | const next = err => { 75 | // If one of the analyzers encountered an error then callback 76 | if (err) { 77 | return setImmediate(() => callback(err, this.analysis)) 78 | } 79 | 80 | // If there are no more analyzers to invoke then callback 81 | if (i >= this.analyzers.length) { 82 | return setImmediate(() => callback(null, this.analysis)) 83 | } 84 | 85 | // Invoke the next analyzer 86 | const analyzer = this.analyzers[i] 87 | i++ 88 | 89 | setImmediate(() => { 90 | // Protect ourselves from any uncaught errors thrown by analyzers 91 | try { 92 | analyzer(this.analysis, next, config) 93 | } catch (uncaughtErr) { 94 | next(uncaughtErr) 95 | } 96 | }) 97 | } 98 | 99 | next() 100 | } 101 | } 102 | 103 | module.exports = exports = AnalysisEngine 104 | -------------------------------------------------------------------------------- /src/audit/languageAnalysisEngine/report/newReportingFeature.js: -------------------------------------------------------------------------------- 1 | const commonReport = require('./commonReportingFunctions') 2 | const { handleResponseErrors } = require('../commonApi') 3 | const { getHttpClient } = require('../../../utils/commonApi') 4 | 5 | const vulnReportWithoutDevDep = async ( 6 | analysis, 7 | applicationId, 8 | snapshotId, 9 | config 10 | ) => { 11 | if (config.report) { 12 | const reportResponse = await getSpecReport(snapshotId, config) 13 | if (reportResponse !== undefined) { 14 | const severity = config.cveSeverity 15 | const id = applicationId 16 | const name = config.applicationName 17 | const hasSomeVulnerabilitiesReported = formatVulnerabilityOutput( 18 | reportResponse.vulnerabilities, 19 | severity, 20 | id, 21 | name, 22 | config 23 | ) 24 | commonReport.analyseReportOptions(hasSomeVulnerabilitiesReported) 25 | } 26 | } 27 | } 28 | 29 | const getSpecReport = async (reportId, config) => { 30 | const client = getHttpClient(config) 31 | 32 | return client 33 | .getSpecificReport(config, reportId) 34 | .then(res => { 35 | if (res.statusCode === 200) { 36 | commonReport.displaySuccessMessageReport() 37 | return res.body 38 | } else { 39 | handleResponseErrors(res, 'report') 40 | } 41 | }) 42 | .catch(err => { 43 | console.log(err) 44 | }) 45 | } 46 | 47 | const countSeverity = vulnerabilities => { 48 | const severityCount = { 49 | critical: 0, 50 | high: 0, 51 | medium: 0, 52 | low: 0 53 | } 54 | 55 | // eslint-disable-next-line 56 | for (const key of Object.keys(vulnerabilities)) { 57 | vulnerabilities[key].forEach(vuln => { 58 | if (vuln.severityCode === 'HIGH') { 59 | severityCount['high'] += 1 60 | } else if (vuln.severityCode === 'MEDIUM') { 61 | severityCount['medium'] += 1 62 | } else if (vuln.severityCode === 'LOW') { 63 | severityCount['low'] += 1 64 | } else if (vuln.severityCode === 'CRITICAL') { 65 | severityCount['critical'] += 1 66 | } 67 | }) 68 | } 69 | return severityCount 70 | } 71 | 72 | const formatVulnerabilityOutput = ( 73 | vulnerabilities, 74 | severity, 75 | id, 76 | name, 77 | config 78 | ) => { 79 | const numberOfVulnerableLibraries = Object.keys(vulnerabilities).length 80 | let numberOfCves = 0 81 | 82 | // eslint-disable-next-line 83 | for (const key of Object.keys(vulnerabilities)) { 84 | numberOfCves += vulnerabilities[key].length 85 | } 86 | 87 | commonReport.createLibraryHeader( 88 | id, 89 | numberOfVulnerableLibraries, 90 | numberOfCves, 91 | name 92 | ) 93 | 94 | const severityCount = countSeverity(vulnerabilities) 95 | const filteredVulns = commonReport.filterVulnerabilitiesBySeverity( 96 | severity, 97 | vulnerabilities 98 | ) 99 | 100 | let hasSomeVulnerabilitiesReported 101 | hasSomeVulnerabilitiesReported = commonReport.printVulnerabilityResponse( 102 | severity, 103 | filteredVulns, 104 | vulnerabilities 105 | ) 106 | 107 | console.log( 108 | '\n **************************' + 109 | ` Found ${numberOfVulnerableLibraries} vulnerable libraries containing ${numberOfCves} CVE's ` + 110 | '************************** ' 111 | ) 112 | 113 | console.log( 114 | ' \n Please go to the Contrast UI to view your dependency tree: \n' + 115 | ` \n ${config.host}/Contrast/static/ng/index.html#/${config.organizationId}/applications/${config.applicationId}/libs/dependency-tree` 116 | ) 117 | return [hasSomeVulnerabilitiesReported, numberOfCves, severityCount] 118 | } 119 | 120 | module.exports = { 121 | vulnReportWithoutDevDep: vulnReportWithoutDevDep, 122 | formatVulnerabilityOutput: formatVulnerabilityOutput, 123 | getSpecReport: getSpecReport 124 | } 125 | -------------------------------------------------------------------------------- /src/scaAnalysis/javascript/analysis.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const yarnParser = require('@yarnpkg/lockfile') 3 | const yaml = require('js-yaml') 4 | const i18n = require('i18n') 5 | const { 6 | formatKey 7 | } = require('../../audit/nodeAnalysisEngine/parseYarn2LockFileContents') 8 | 9 | const readFile = async (config, languageFiles, nameOfFile) => { 10 | const index = languageFiles.findIndex(v => v.includes(nameOfFile)) 11 | 12 | if (config.file) { 13 | return fs.readFileSync(config.file.concat(languageFiles[index]), 'utf8') 14 | } else { 15 | throw new Error('could not find file') 16 | } 17 | } 18 | 19 | const readYarn = async (config, languageFiles, nameOfFile) => { 20 | let yarn = { 21 | yarnVersion: 1, 22 | rawYarnLockFileContents: '' 23 | } 24 | 25 | try { 26 | let rawYarnLockFileContents = await readFile( 27 | config, 28 | languageFiles, 29 | nameOfFile 30 | ) 31 | yarn.rawYarnLockFileContents = rawYarnLockFileContents 32 | 33 | if ( 34 | !yarn.rawYarnLockFileContents.includes('lockfile v1') || 35 | yarn.rawYarnLockFileContents.includes('__metadata') 36 | ) { 37 | yarn.rawYarnLockFileContents = yaml.load(rawYarnLockFileContents) 38 | yarn.yarnVersion = 2 39 | } 40 | 41 | return yarn 42 | } catch (err) { 43 | throw new Error(i18n.__('nodeReadYarnLockFileError') + `${err.message}`) 44 | } 45 | } 46 | 47 | const parseNpmLockFile = async js => { 48 | try { 49 | js.npmLockFile = JSON.parse(js.rawLockFileContents) 50 | if (js.npmLockFile && js.npmLockFile.lockfileVersion > 1) { 51 | const listOfTopDep = Object.keys(js.npmLockFile.dependencies) 52 | Object.entries(js.npmLockFile.dependencies).forEach(([objKey, value]) => { 53 | if (value.requires) { 54 | const listOfRequiresDep = Object.keys(value.requires) 55 | listOfRequiresDep.forEach(dep => { 56 | if (!listOfTopDep.includes(dep)) { 57 | addDepToLockFile(js, value['requires'], dep) 58 | } 59 | }) 60 | } 61 | 62 | if (value.dependencies) { 63 | Object.entries(value.dependencies).forEach( 64 | ([objChildKey, childValue]) => { 65 | if (childValue.requires) { 66 | const listOfRequiresDep = Object.keys(childValue.requires) 67 | listOfRequiresDep.forEach(dep => { 68 | if (!listOfTopDep.includes(dep)) { 69 | addDepToLockFile(js, childValue['requires'], dep) 70 | } 71 | }) 72 | } 73 | } 74 | ) 75 | } 76 | }) 77 | return js.npmLockFile 78 | } else { 79 | return js.npmLockFile 80 | } 81 | } catch (err) { 82 | throw new Error(i18n.__('NodeParseNPM') + `${err.message}`) 83 | } 84 | } 85 | 86 | const addDepToLockFile = (js, depObj, key) => { 87 | return (js.npmLockFile.dependencies[key] = { version: depObj[key] }) 88 | } 89 | const parseYarnLockFile = async js => { 90 | try { 91 | js.yarn.yarnLockFile = {} 92 | if (js.yarn.yarnVersion === 1) { 93 | js.yarn.yarnLockFile = yarnParser.parse(js.yarn.rawYarnLockFileContents) 94 | delete js.yarn.rawYarnLockFileContents 95 | return js 96 | } else { 97 | js.yarn.yarnLockFile['object'] = js.yarn.rawYarnLockFileContents 98 | delete js.yarn.yarnLockFile['object'].__metadata 99 | js.yarn.yarnLockFile['type'] = 'success' 100 | 101 | Object.entries(js.yarn.rawYarnLockFileContents).forEach( 102 | ([key, value]) => { 103 | const rawKeyNames = key.split(',') 104 | const keyNames = formatKey(rawKeyNames) 105 | 106 | keyNames.forEach(name => { 107 | js.yarn.yarnLockFile.object[name] = value 108 | }) 109 | } 110 | ) 111 | return js 112 | } 113 | } catch (err) { 114 | throw new Error( 115 | i18n.__('NodeParseYarn', js.yarn.yarnVersion) + `${err.message}` 116 | ) 117 | } 118 | } 119 | 120 | module.exports = { 121 | readYarn, 122 | parseYarnLockFile, 123 | parseNpmLockFile, 124 | readFile, 125 | formatKey 126 | } 127 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import commandLineArgs from 'command-line-args' 4 | import { processAudit } from './commands/audit/processAudit' 5 | import { processAuth } from './commands/auth/auth' 6 | import { processConfig } from './commands/config/config' 7 | import { processScan } from './commands/scan/processScan' 8 | import constants from './cliConstants' 9 | import { APP_NAME, APP_VERSION } from './constants/constants' 10 | import { processLambda } from './lambda/lambda' 11 | import { localConfig } from './utils/getConfig' 12 | import { 13 | findLatestCLIVersion, 14 | isCorrectNodeVersion 15 | } from './common/versionChecker' 16 | import { findCommandOnError } from './common/errorHandling' 17 | import { sendTelemetryConfigAsConfObj } from './telemetry/telemetry' 18 | const { 19 | commandLineDefinitions: { mainUsageGuide, mainDefinition } 20 | } = constants 21 | 22 | const config = localConfig(APP_NAME, APP_VERSION) 23 | 24 | const getMainOption = () => { 25 | const mainOptions = commandLineArgs(mainDefinition, { 26 | stopAtFirstUnknown: true, 27 | camelCase: true, 28 | caseInsensitive: true 29 | }) 30 | const argv = mainOptions._unknown || [] 31 | 32 | return { 33 | mainOptions, 34 | argv 35 | } 36 | } 37 | 38 | const start = async () => { 39 | try { 40 | if (await isCorrectNodeVersion(process.version)) { 41 | const { mainOptions, argv: argvMain } = getMainOption() 42 | const command = 43 | mainOptions.command != undefined 44 | ? mainOptions.command.toLowerCase() 45 | : '' 46 | if ( 47 | command === 'version' || 48 | argvMain.includes('--v') || 49 | argvMain.includes('--version') 50 | ) { 51 | console.log(APP_VERSION) 52 | await findLatestCLIVersion(config) 53 | return 54 | } 55 | 56 | // @ts-ignore 57 | config.set('numOfRuns', config.get('numOfRuns') + 1) 58 | 59 | // @ts-ignore 60 | if (config.get('numOfRuns') >= 10) { 61 | await findLatestCLIVersion(config) 62 | config.set('numOfRuns', 0) 63 | } 64 | 65 | if (command === 'config') { 66 | return processConfig(argvMain, config) 67 | } 68 | 69 | if (command === 'auth') { 70 | return await processAuth(argvMain, config) 71 | } 72 | 73 | if (command === 'lambda') { 74 | return await processLambda(argvMain) 75 | } 76 | 77 | if (command === 'scan') { 78 | return await processScan(config, argvMain) 79 | } 80 | 81 | if (command === 'audit') { 82 | return await processAudit(config, argvMain) 83 | } 84 | 85 | if ( 86 | command === 'help' || 87 | argvMain.includes('--help') || 88 | Object.keys(mainOptions).length === 0 89 | ) { 90 | console.log(mainUsageGuide) 91 | } else if (mainOptions._unknown !== undefined) { 92 | const foundCommand = findCommandOnError(mainOptions._unknown) 93 | 94 | foundCommand 95 | ? console.log( 96 | `Unknown Command: Did you mean "${foundCommand}"? \nUse "${foundCommand} --help" for the full list of options` 97 | ) 98 | : console.log(`\nUnknown Command: ${command} \n`) 99 | console.log(mainUsageGuide) 100 | await sendTelemetryConfigAsConfObj( 101 | config, 102 | command, 103 | argvMain, 104 | 'FAILURE', 105 | 'undefined' 106 | ) 107 | } else { 108 | console.log(`\nUnknown Command: ${command}\n`) 109 | console.log(mainUsageGuide) 110 | await sendTelemetryConfigAsConfObj( 111 | config, 112 | command, 113 | argvMain, 114 | 'FAILURE', 115 | 'undefined' 116 | ) 117 | } 118 | process.exit(9) 119 | } else { 120 | console.log( 121 | 'Contrast supports Node versions >=16.13.2 <17. Please use one of those versions.' 122 | ) 123 | process.exit(9) 124 | } 125 | } catch (err: any) { 126 | console.log() 127 | console.log(err.message.toString()) 128 | process.exit(1) 129 | } 130 | } 131 | 132 | start() 133 | -------------------------------------------------------------------------------- /src/lambda/scanRequest.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18n' 2 | import logSymbols from 'log-symbols' 3 | import chalk from 'chalk' 4 | import { parseARN } from './arn' 5 | import { 6 | getLambdaClient, 7 | getLambdaFunctionConfiguration, 8 | getLambdaPolicies, 9 | getLayersLinks 10 | } from './aws' 11 | import { toLowerKeys } from './utils' 12 | import { getHttpClient } from '../utils/commonApi' 13 | import { ApiParams, LambdaOptions } from './lambda' 14 | import { log, prettyPrintJson } from './logUtils' 15 | import { CliError } from './cliError' 16 | import { ERRORS } from './constants' 17 | 18 | const sendScanPostRequest = async ( 19 | config: any, 20 | params: ApiParams, 21 | functionsEvent: unknown, 22 | showProgress = false 23 | ) => { 24 | const client = getHttpClient(config) 25 | 26 | if (showProgress) { 27 | log(i18n.__('sendingScanRequest', { icon: logSymbols.success })) 28 | } 29 | 30 | const res = await client.postFunctionScan(config, params, functionsEvent) 31 | const { statusCode, body } = res 32 | 33 | if (statusCode === 201) { 34 | if (showProgress) { 35 | log(i18n.__('scanRequestedSuccessfully', { icon: logSymbols.success })) 36 | } 37 | 38 | return body?.data?.scanId 39 | } 40 | 41 | let { errorCode } = body?.data || {} 42 | const { data } = body?.data || {} 43 | 44 | let description = '' 45 | switch (errorCode) { 46 | case 'not_supported_runtime': 47 | description = i18n.__(errorCode, { 48 | runtime: data?.runtime, 49 | supportedRuntimes: data?.supportedRuntimes.sort().join(' | ') 50 | }) 51 | errorCode = false 52 | break 53 | } 54 | 55 | throw new CliError(ERRORS.FAILED_TO_START_SCAN, { 56 | statusCode, 57 | errorCode, 58 | data, 59 | description 60 | }) 61 | } 62 | 63 | const createFunctionEvent = ( 64 | lambdaConfig: any, 65 | layersLinks: any, 66 | lambdaPolicies: any 67 | ) => { 68 | delete lambdaConfig.$metadata 69 | 70 | const functionEvent = toLowerKeys(lambdaConfig.Configuration) 71 | functionEvent['code'] = lambdaConfig.Code 72 | functionEvent['rolePolicies'] = lambdaPolicies 73 | 74 | if (layersLinks) { 75 | functionEvent['layers'] = layersLinks 76 | } 77 | 78 | return { function: functionEvent } 79 | } 80 | 81 | const requestScanFunctionPost = async ( 82 | config: any, 83 | lambdaOptions: LambdaOptions 84 | ) => { 85 | const { verbose, jsonOutput, functionName } = lambdaOptions 86 | const lambdaClient = getLambdaClient(lambdaOptions) 87 | 88 | if (!jsonOutput) { 89 | log( 90 | i18n.__('fetchingConfiguration', { 91 | icon: logSymbols.success, 92 | functionName: chalk.bold(functionName) 93 | }) 94 | ) 95 | } 96 | 97 | const lambdaConfig = await getLambdaFunctionConfiguration( 98 | lambdaClient, 99 | lambdaOptions 100 | ) 101 | if (!lambdaConfig?.Configuration) { 102 | throw new CliError(ERRORS.FAILED_TO_START_SCAN, { 103 | errorCode: 'missingLambdaConfig' 104 | }) 105 | } 106 | const { Configuration } = lambdaConfig 107 | const layersLinks = await getLayersLinks(lambdaClient, Configuration) 108 | const lambdaPolicies = await getLambdaPolicies(Configuration, lambdaOptions) 109 | 110 | const functionEvent = createFunctionEvent( 111 | lambdaConfig, 112 | layersLinks, 113 | lambdaPolicies 114 | ) 115 | const { FunctionArn: functionArn } = Configuration 116 | if (!functionArn) { 117 | throw new CliError(ERRORS.FAILED_TO_START_SCAN, { 118 | errorCode: 'missingLambdaArn' 119 | }) 120 | } 121 | 122 | const parsedARN = parseARN(functionArn) 123 | const params: ApiParams = { 124 | organizationId: config.organizationId, 125 | provider: 'aws', 126 | accountId: parsedARN.accountId 127 | } 128 | 129 | if (verbose) { 130 | log(i18n.__('fetchedConfiguration', { icon: logSymbols.success })) 131 | prettyPrintJson(functionEvent) 132 | } 133 | 134 | const scanId = await sendScanPostRequest( 135 | config, 136 | params, 137 | functionEvent, 138 | !jsonOutput 139 | ) 140 | 141 | return { scanId, params, functionArn } 142 | } 143 | 144 | export { sendScanPostRequest, requestScanFunctionPost, createFunctionEvent } 145 | -------------------------------------------------------------------------------- /src/common/errorHandling.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18n' 2 | 3 | const handleResponseErrors = (res: any, api: string) => { 4 | if (res.statusCode === 400) { 5 | api === 'catalogue' ? badRequestError(true) : badRequestError(false) 6 | } else if (res.statusCode === 401) { 7 | unauthenticatedError() 8 | } else if (res.statusCode === 403) { 9 | forbiddenError() 10 | } else if (res.statusCode === 407) { 11 | proxyError() 12 | } else { 13 | if (api === 'snapshot' || api === 'catalogue') { 14 | snapshotFailureError() 15 | } 16 | if (api === 'vulnerabilities') { 17 | vulnerabilitiesFailureError() 18 | } 19 | if (api === 'report') { 20 | reportFailureError() 21 | } 22 | } 23 | } 24 | 25 | const libraryAnalysisError = () => { 26 | console.log(i18n.__('libraryAnalysisError')) 27 | } 28 | 29 | const snapshotFailureError = () => { 30 | console.log(i18n.__('snapshotFailureMessage')) 31 | } 32 | 33 | const vulnerabilitiesFailureError = () => { 34 | console.log(i18n.__('vulnerabilitiesFailureMessage')) 35 | } 36 | 37 | const reportFailureError = () => { 38 | console.log(i18n.__('auditReportFailureMessage')) 39 | } 40 | 41 | const genericError = () => { 42 | console.error(i18n.__('genericErrorMessage')) 43 | process.exit(1) 44 | } 45 | 46 | const unauthenticatedError = () => { 47 | generalError('unauthenticatedErrorHeader', 'unauthenticatedErrorMessage') 48 | } 49 | 50 | const badRequestError = (catalogue: boolean) => { 51 | catalogue === true 52 | ? generalError('badRequestErrorHeader', 'badRequestCatalogueErrorMessage') 53 | : generalError('badRequestErrorHeader', 'badRequestErrorMessage') 54 | } 55 | 56 | const forbiddenError = () => { 57 | generalError('forbiddenRequestErrorHeader', 'forbiddenRequestErrorMessage') 58 | process.exit(1) 59 | } 60 | 61 | const proxyError = () => { 62 | generalError('proxyErrorHeader', 'proxyErrorMessage') 63 | } 64 | 65 | const maxAppError = () => { 66 | generalError( 67 | 'No applications remaining', 68 | 'You have reached the maximum number of application you can create.' 69 | ) 70 | process.exit(1) 71 | } 72 | 73 | const failOptionError = () => { 74 | console.log( 75 | '\n ******************************** ' + 76 | i18n.__('snapshotFailureHeader') + 77 | ' ********************************\n' + 78 | i18n.__('failOptionErrorMessage') 79 | ) 80 | } 81 | 82 | /** 83 | * You don't have to pass `i18n` translation. 84 | * String that didn't exists on translations will pass as regular string 85 | * @param header title for the error 86 | * @param message message for the error 87 | * @returns error in general format 88 | */ 89 | const getErrorMessage = (header: string, message?: string) => { 90 | // prettier-ignore 91 | const title = `******************************** ${i18n.__(header)} ********************************` 92 | const multiLine = message?.includes('\n') 93 | let finalMessage = '' 94 | 95 | // i18n split the line if it includes '\n' 96 | if (multiLine) { 97 | finalMessage = `\n${message}` 98 | } else if (message) { 99 | finalMessage = `\n${i18n.__(message)}` 100 | } 101 | 102 | return `${title}${finalMessage}` 103 | } 104 | 105 | const generalError = (header: string, message?: string) => { 106 | const finalMessage = getErrorMessage(header, message) 107 | console.log(finalMessage) 108 | } 109 | 110 | const findCommandOnError = (unknownOptions: string[]) => { 111 | const commandKeywords = { 112 | auth: 'auth', 113 | audit: 'audit', 114 | scan: 'scan', 115 | lambda: 'lambda', 116 | config: 'config' 117 | } 118 | 119 | const containsCommandKeyword = unknownOptions.some( 120 | // @ts-ignore 121 | command => commandKeywords[command] 122 | ) 123 | 124 | if (containsCommandKeyword) { 125 | const foundCommands = unknownOptions.filter( 126 | // @ts-ignore 127 | command => commandKeywords[command] 128 | ) 129 | 130 | //return the first command found 131 | return foundCommands[0] 132 | } 133 | } 134 | 135 | export { 136 | genericError, 137 | unauthenticatedError, 138 | badRequestError, 139 | forbiddenError, 140 | proxyError, 141 | failOptionError, 142 | generalError, 143 | getErrorMessage, 144 | handleResponseErrors, 145 | libraryAnalysisError, 146 | findCommandOnError, 147 | snapshotFailureError, 148 | vulnerabilitiesFailureError, 149 | reportFailureError, 150 | maxAppError 151 | } 152 | -------------------------------------------------------------------------------- /src/audit/javaAnalysisEngine/readProjectFileContents.js: -------------------------------------------------------------------------------- 1 | const child_process = require('child_process') 2 | const fs = require('fs') 3 | const i18n = require('i18n') 4 | const path = require('path') 5 | 6 | module.exports = exports = ( 7 | { language: { projectFilePath }, java }, 8 | next, 9 | config 10 | ) => { 11 | let cmdStdout 12 | let cwd 13 | let timeout 14 | let javaProject = '' 15 | let mvn_settings = '' 16 | const maven = 'Maven' 17 | const gradle = 'Gradle' 18 | 19 | try { 20 | if (projectFilePath.includes('pom.xml')) { 21 | javaProject = maven 22 | cwd = projectFilePath.replace('pom.xml', '') 23 | } else if (projectFilePath.includes('build.gradle')) { 24 | javaProject = gradle 25 | cwd = projectFilePath.replace('build.gradle', '') 26 | } 27 | 28 | // timeout is in milliseconds and 2.30 mintues was choses as when tested against 29 | // Spring-boot (https://github.com/spring-projects/spring-boot) a complex project that was the 30 | // average time for a first run when it had to download projects then build tree 31 | timeout = 960000 32 | 33 | // A sample of this output can be found 34 | // in the java test data/mvnCmdResults.text 35 | if (javaProject === maven) { 36 | // Allow users to provide a custom location for their settings.xml 37 | if (config.mavenSettingsPath) { 38 | mvn_settings = ' -s ' + config.mavenSettingsPath 39 | } 40 | 41 | if (config.betaUnifiedJavaParser) { 42 | cmdStdout = child_process.execSync( 43 | 'mvn dependency:tree -B' + mvn_settings, 44 | { 45 | cwd, 46 | timeout 47 | } 48 | ) 49 | } else { 50 | cmdStdout = child_process.execSync( 51 | 'mvn dependency:tree -DoutputType=dot -B' + mvn_settings, 52 | { 53 | cwd, 54 | timeout 55 | } 56 | ) 57 | } 58 | java.mvnDependancyTreeOutput = cmdStdout.toString() 59 | } else if (javaProject === gradle) { 60 | // path.sep is user here to either execute as "./gradlew" for UNIX/Linux/MacOS 61 | // & ".\gradlew" for Windows 62 | // Check if the user has specified a sub-project 63 | if (config.subProject) { 64 | cmdStdout = child_process.execSync( 65 | '.' + 66 | path.sep + 67 | 'gradlew :' + 68 | config.subProject + 69 | ':dependencies --configuration runtimeClasspath', 70 | { 71 | cwd, 72 | timeout 73 | } 74 | ) 75 | } else { 76 | cmdStdout = child_process.execSync( 77 | '.' + 78 | path.sep + 79 | 'gradlew dependencies --configuration runtimeClasspath', 80 | { 81 | cwd, 82 | timeout 83 | } 84 | ) 85 | } 86 | if ( 87 | cmdStdout 88 | .toString() 89 | .includes( 90 | "runtimeClasspath - Runtime classpath of source set 'main'.\n" + 91 | 'No dependencies' 92 | ) 93 | ) { 94 | cmdStdout = child_process.execSync( 95 | '.' + path.sep + 'gradlew dependencies', 96 | { 97 | cwd, 98 | timeout 99 | } 100 | ) 101 | } 102 | java.mvnDependancyTreeOutput = cmdStdout.toString() 103 | } 104 | next() 105 | } catch (err) { 106 | if (javaProject === maven) { 107 | try { 108 | child_process.execSync('mvn --version', { 109 | cwd, 110 | timeout 111 | }) 112 | 113 | next( 114 | new Error( 115 | i18n.__('mavenDependencyTreeNonZero', cwd, `${err.message}`) 116 | ) 117 | ) 118 | } catch (mvnErr) { 119 | next( 120 | new Error(i18n.__('mavenNotInstalledError', cwd, `${mvnErr.message}`)) 121 | ) 122 | } 123 | } else if (javaProject === gradle) { 124 | if ( 125 | fs.existsSync(cwd + 'gradlew') || 126 | fs.existsSync(cwd + 'gradlew.bat') 127 | ) { 128 | next( 129 | new Error( 130 | i18n.__('gradleDependencyTreeNonZero', cwd, `${err.message}`) 131 | ) 132 | ) 133 | } else { 134 | next( 135 | new Error(i18n.__('gradleWrapperUnavailable', cwd, `${err.message}`)) 136 | ) 137 | } 138 | } 139 | return 140 | } 141 | } 142 | --------------------------------------------------------------------------------