diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..830ce5d2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Report an issue +about: Create a bug report to fix an existing issue. +title: '' +labels: 'bug' +assignees: '' + +--- + +### Description + +> Provide a description of the issue + +### Expected Behavior + +### Actual Behavior + +### Reproduction + +> Detail the steps taken to reproduce the issue +> +> Where applicable, please include (exclude sensitive information): +> +> - Code of Files to reproduce the issue +> - Log files +> - Application settings +> - Screenshots + +### Environment Details + +> Provide any information relating to the environment the issue was identified in - include applicable version and additional runtime information (include OS or other underlying infrastructure) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..99f9dee4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest new functionality for this project. +title: '' +labels: 'enhancement' +assignees: '' + +--- + +### Describe the problem + +> A clear description of what the problem is. + +### Proposed solution + +> A clear description of what you want to happen. + +### Additional details + +> Add any other details / contexts / screenshots about the feature request. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..14f3411d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,21 @@ +By submitting a PR to this repository, you agree to the terms within the [Checkmarx Code of Conduct](https://github.com/checkmarx-ltd/open-source-template/blob/master/CODE-OF-CONDUCT.md). Please see the [contributing guidelines](https://github.com/checkmarx-ltd/open-source-template/blob/master/CONTRIBUTING.md) for how to create and submit a high-quality PR for this repo. + +### Description + +> Describe the purpose of this PR along with any background information and the impacts of the proposed change. + +### References + +> Include supporting link to GitHub Issue/PR number + +### Testing + +> Describe how this change was tested. Be specific about anything not tested and reasons why. If this solution has unit and/or integration testing, tests should be added for new functionality and existing tests should complete without errors. +> +> Please include any manual steps for testing end-to-end or functionality not covered by unit/integration tests. + +### Checklist + +- [ ] I have added documentation for new/changed functionality in this PR (if applicable). *If documentaiton is a Wiki Update, please indicate desired changes within PR MD Comment* +- [ ] All active GitHub checks for tests, formatting, and security are passing +- [ ] The correct base branch is being used diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000..acf275b6 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,32 @@ +name-template: 'v$RESOLVED_VERSION 🌈' +tag-template: 'v$RESOLVED_VERSION' +categories: + - title: '🚀 Features' + labels: + - 'feature' + - 'enhancement' + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - title: '🧰 Maintenance' + label: 'chore' +exclude-labels: + - 'skip-release-notes' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +version-resolver: + major: + labels: + - 'major' + minor: + labels: + - 'minor' + patch: + labels: + - 'patch' + default: patch +template: | + ## Changes + + $CHANGES diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 00000000..8ea20312 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,19 @@ +name: Release Drafter + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - master + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v5 + with: + # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml + config-name: release-drafter.yml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 6a755137..7a885cc1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ work/ out/ +*-secrets.properties #Gradle .gradle/ @@ -19,3 +20,4 @@ bin/ #Jenkins specific out *.hpi classes/ +*.dummy diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..780d9703 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,77 @@ +# Contributing to Checkmarx projects + +Welcome and thank you for considering contributing to a Checkmarx open source project. + +Reading and following these guidelines will help us make the contribution process easy and effective for everyone involved. It also communicates that you agree to respect the time of the developers managing and developing these open source projects. In return, we will reciprocate that respect by addressing your issue, assessing changes, and helping you finalize your pull requests. + + +## Quicklinks + +- [Contributing to Checkmarx projects](#contributing-to-checkmarx-projects) + - [Quicklinks](#quicklinks) + - [Code of Conduct](#code-of-conduct) + - [Getting Started](#getting-started) + - [Issues](#issues) + - [Pull Requests](#pull-requests) + - [Resources](#resources) + +## Code of Conduct + +By participating and contributing to any Checkmarx projects, you agree to uphold our [Code of Conduct](https://github.com/checkmarx-ltd/open-source-template/blob/master/CODE-OF-CONDUCT.md). + +## Getting Started + +If you have suggestions for how this project could be improved, or want to report a bug, open an issue. We appreciate all contributions. If you have questions, we'd love to hear them. + +We also appreciate PRs. If you're thinking of submitting any PR, pleae open an issue first to spark a discussion around it. + +Contributions are made to this repo via Issues and Pull Requests (PRs). A few general guidelines that cover both: + +- Search for existing Issues and PRs before creating your own to avoid duplicates. +- PRs will only be accepted if associated with an issue (enhancement or bug) that has been submitted and reviewed/labeld as *accepted* by a Checkmarx team member. +- We will work hard to makes sure issues that are raised are handled in a timely manner. + +## Issues + +Issues should be used to report problems with the solution / source code, request a new feature, or to discuss potential changes before a PR is created. When you create a new Issue, a template will be loaded that will guide you through collecting and providing the information we need to investigate. + +If you find an Issue that addresses the problem you're having, please add your own reproduction information to the existing issue rather than creating a new one. Adding a [reaction](https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) can also help by indicating to our maintainers that a particular problem is affecting more than just the reporter. + +### Templates + +The following templates will be used within Checkmarx github repositories +- [Enhancement/Feature Request Template](.github/ISSUE_TEMPLATE/feature_request.md) +- [Bug Report Template](.github/ISSUE_TEMPLATE/bug_report.md) + +## Pull Requests + +PRs to our source are always welcome and can be a quick way to get your fix or improvement slated for the next release. In general, PRs should: + +- Only fix/add the functionality in question **or** address code style issues, not both. +- Ensure all necessary details are provided and adhered to +- Add unit or integration tests for fixed or changed functionality (if a test suite already exists) or specify steps taken to ensure changes were tested and functionality works as expected. +- Address a single concern in the least number of changed lines as possible. +- Include documentation in the repo or Provide additional comments in Markdown comments that should be pulled/reflected in GitHub Wiki for the given project. +- Be accompanied by a complete Pull Request template (loaded automatically when a PR is created). + +For changes that address core functionality or would require breaking changes (e.g. a major release), please open an Issue to discuss your proposal first. + +In general, we follow the _fork-and-pull_ Git workflow + +1. Fork the repository to your own Github account +2. Clone the project to your machine +3. Create a branch locally with a succinct but descriptive name (prefix with feature/-descriptive-name> or hotfix/-descriptive-name) +4. Commit changes to the branch +5. Push changes to your fork +6. Open a PR in our repository and follow the PR template so that we can efficiently review and asses the changes. *Ensure an associated Issue has been accepted by the Checkmarx team.* + +### Templates +The following template will be used within Checkmarx github repositories + +[Pull Request Template](.github/PULL_REQUEST_TEMPLATE.md) + +## Resources + +- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) +- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) +- [GitHub Help](https://help.github.com) diff --git a/Jenkinsfile-CI-CD b/Jenkinsfile-CI-CD new file mode 100644 index 00000000..437945b7 --- /dev/null +++ b/Jenkinsfile-CI-CD @@ -0,0 +1,486 @@ +#!/groovy +@Library('cx-jenkins-pipeline-kit') _ +import java.time.* + +def workspace +def vmName = "${BUILD_TAG}-CxSAST" +def vmTemplate95 = "CxSDLC-Template-CxSAST-9-5" +def ipAddress95 +def vmTemplate96 = "CxSDLC-Template-CxSAST-9-6-1" +def ipAddress96 +def vmTemplate97 = "CxSDLC-Template-CxSAST-9-7" +def ipAddress97 +def ram = "12000" +def cpu = "8" +def provider = "VMWARE" +def decommissionPeriod = "3 hour" +def vmwareNetwork = "Lab" +def automationBranch = "9.0.0" + + +pipeline { + parameters { + string(name: "vmTemplate95",defaultValue: "${vmTemplate95}", description: "Template for 9.5 VM") + string(name: "vmTemplate96",defaultValue: "${vmTemplate96}", description: "Template for 9.6 VM") + string(name: "vmTemplate97",defaultValue: "${vmTemplate97}", description: "Template for 9.7 VM") + string(name: "ram",defaultValue: "${ram}", description: "Server memory") + string(name: "cpu",defaultValue: "${cpu}", description: "") + string(name: "provider",defaultValue: "${provider}", description: "IAAS platform to be used") + string(name: "decommissionPeriod",defaultValue: "${decommissionPeriod}", description: "Decommission period") + string(name: "vmwareNetwork",defaultValue: "${vmwareNetwork}", description: "vmware network for new VMs") + string(name: "automationBranch", defaultValue: "${automationBranch}", description: "automation branch") + } + agent { node { label 'CxSDLC-Slave' } } + options { + timestamps() + timeout(time: 2, unit: 'HOURS') + //skipDefaultCheckout() + } + stages { + + stage('Pipeline Info') { + steps { + script { + env.PIPELINE_STATUS = "Success" + env.STAGE_NAME_FAILED = "None" + if (BRANCH_NAME == 'master') { + Calendar cal = Calendar.getInstance(Locale.US) + int quarter = (cal.get(Calendar.MONTH) / 3) + 1 + int year = cal.get(Calendar.YEAR) + env.cxCommonVersion = "${year}.${quarter}.${BUILD_NUMBER}" + sh "sed -e 's/\${cxcommon.version}/${env.cxCommonVersion}/g' -i ./pom.xml" + // get version from POM + /*sh "mvn resources:resources" + def commonPropertiesContent = readFile "./target/classes/common.properties" + env.cxCommonVersion = commonPropertiesContent.substring(10)*/ + } else { + env.cxCommonVersion = "${BUILD_TAG}" + } + workspace = pwd() + if (env.automationBranch == null) { + env.automationBranch = automationBranch + } + sh 'printenv' + } + } + } + + stage('Build CxCommon') { + steps { + script { + sh "docker run --rm --name build-${BUILD_TAG} -v /root/.m2:/root/.m2 -v ${workspace}:/usr/src/cx-common -w /usr/src/cx-common maven:3.6.1-jdk-8-alpine mvn clean install -DskipTests -Dcxcommon.version=${env.cxCommonVersion}" + } + } + post { + failure { + script { + env.PIPELINE_STATUS = "Failure" + env.STAGE_NAME_FAILED="${STAGE_NAME}" + } + } + } + } + + stage('UT-IT & Sonar') { + parallel { + + stage('Run Unit & Integration Tests') { + steps { + script { + sh "docker run --rm --name test-${BUILD_TAG} -v /root/.m2:/root/.m2 -v ${workspace}:/usr/src/cx-common -w /usr/src/cx-common maven:3.6.1-jdk-8-alpine mvn test -Dcxcommon.version=${env.cxCommonVersion}" + } + } + post { + failure { + script { + env.PIPELINE_STATUS = "Failure" + env.STAGE_NAME_FAILED="${STAGE_NAME}" + } + } + } + } + + stage('Run Code Quality') { + when { + expression { false } + } + environment { + SONAR_CLOUD_TOKEN = credentials('sonarcloud') + } + steps { + script { + sh "docker run --rm --name sonar-${BUILD_TAG} -v /root/.m2:/root/.m2 -v ${workspace}:/usr/src/cx-common -w /usr/src/cx-common maven:3.6.3-jdk-11-slim mvn sonar:sonar -Dcxcommon.version=${env.cxCommonVersion} -Dsonar.login=${SONAR_CLOUD_TOKEN}" + def sonarTaskUrl = sh (returnStdout: true, script: "awk '/./{line=\$0} END{print line}' ./target/sonar/report-task.txt | cut -d '=' -f '2,3'").trim() + def sonarTaskStatus = sh (returnStdout: true, script: "curl -s -u ${SONAR_CLOUD_TOKEN}: ${sonarTaskUrl} | jq -r '.task.status'").trim() + while (sonarTaskStatus == 'PENDING' || sonarTaskStatus == 'IN_PROGRESS') { + echo "Sonar task status is: ${sonarTaskStatus}. Waiting for 5 seconds..." + sleep(5) + sonarTaskStatus = sh (returnStdout: true, script: "curl -s -u ${SONAR_CLOUD_TOKEN}: ${sonarTaskUrl} | jq -r '.task.status'").trim() + } + if (sonarTaskStatus == "FAILED" || sonarTaskStatus == "CANCELED") { + kit.Error_Msg("Sonar scan ${sonarTaskStatus}. You can find more details at https://sonarcloud.io/dashboard?id=checkmarx-ltd_Cx-Client-Common") + error "Sonar scan ${sonarTaskStatus}. You can find more details at https://sonarcloud.io/dashboard?id=checkmarx-ltd_Cx-Client-Common" + } + def sonarAnalysisId = sh (returnStdout: true, script: "curl -s -u ${SONAR_CLOUD_TOKEN}: ${sonarTaskUrl} | jq -r '.task.analysisId'").trim() + def sonarResultsStatus = sh (returnStdout: true, script: "curl -s -u ${SONAR_CLOUD_TOKEN}: https://sonarcloud.io/api/qualitygates/project_status?analysisId=${sonarAnalysisId} | jq -r '.projectStatus.status'").trim() + if (sonarResultsStatus == "ERROR") { + kit.Error_Msg("Sonar scan FAILED. You can find more details at https://sonarcloud.io/dashboard?id=checkmarx-ltd_Cx-Client-Common") + error "Sonar scan FAILED. You can find more details at https://sonarcloud.io/dashboard?id=checkmarx-ltd_Cx-Client-Common" + } + } + } + post { + failure { + script { + env.PIPELINE_STATUS = "Failure" + env.STAGE_NAME_FAILED="${STAGE_NAME}" + } + } + } + } + } + } + + stage('Install CxCommon in Local Repository') { + when { + expression { + BRANCH_NAME == 'master' || BRANCH_NAME.startsWith("PR-") && CHANGE_TARGET == 'master' + } + } + steps { + sh "docker run --rm --memory 2gb --name install-cx-common-${BUILD_TAG} -v maven-repo:/root/.m2 -v ${workspace}/target/cx-client-common-${env.cxCommonVersion}.jar:/usr/src/artifact.jar maven:3.6.1-jdk-8-alpine \ + mvn -q install:install-file -Dfile=/usr/src/artifact.jar -DgroupId=com.checkmarx -DartifactId=cx-client-common -Dversion=${env.cxCommonVersion} -Dpackaging=jar" + sh "docker run --rm --memory 2gb --name install-cx-common-${BUILD_TAG} -v maven-repo:/root/.m2 -v ${workspace}/pom.xml:/usr/src/pom.xml maven:3.6.1-jdk-8-alpine \ + mvn -q install:install-file -Dfile=/usr/src/pom.xml -DgroupId=com.checkmarx -DartifactId=cx-client-common -Dversion=${env.cxCommonVersion} -Dpackaging=pom" + } + } + + stage('System Tests') { + when { + expression { + BRANCH_NAME == 'master' || BRANCH_NAME.startsWith("PR-") && CHANGE_TARGET == 'master' + } + } + parallel { + + stage('9.5') { + stages { + stage('Create VM') { + steps { + script { + kit.Create_Vm_Terraform(vmName + "-9.5", vmTemplate95, ram, cpu, provider, decommissionPeriod, "Auto", "Plugins-CI", vmwareNetwork) + ipAddress95 = kit.getIpAddress(vmName + "-9.5", provider) + node('install01') { + kit.Create_Jenkins_Slave_On_Master_cx_operation(vmName + "-9.5") + kit.Start_Jenkins_Slave_On_Windows_Pstools_cx_operation(ipAddress95, vmName + "-9.5") + } + } + } + } + stage('Pull Automation Code') { + steps { + dir("${workspace}/9.5/Checkmarx-System-Test") { + git branch: "${env.automationBranch}", credentialsId: 'TFS-Test', poll: false, url: 'http://tfs2013:8080/tfs/DefaultCollection/Automation/_git/Checkmarx-System-Test' + sh "cp -r ../../../env ." + sh "sed -e 's//${ipAddress95}/g' -i ./env/topology.xml" + } + } + } + stage('Plugins API Sanity Test') { + steps { + dir("${workspace}/9.5/Checkmarx-System-Test") { + sh "docker run --rm --memory 2gb --name plugins-api-sanity-test-9.5-${BUILD_TAG} -v maven-repo:/root/.m2 -v ${workspace}/9.5/Checkmarx-System-Test:/usr/src/automation -w /usr/src/automation --add-host WIN2012-ENV9-B:${ipAddress95} maven:3.6.1-jdk-8-alpine \ + mvn -q clean test -Dcxcommon.version=${env.cxCommonVersion} -Dsast.version=9.5 -Dtest=com.cx.automation.plugin.test.cxcommonclient.sanity.* -Dtopology.xml.ref=/usr/src/automation/env/topology.xml -Denv=hardening_env -DfailIfNoTests=false -Dmaven.test.failure.ignore=false -DskipTests=false" + } + } + } + stage('Plugins API Smoke Tests') { + steps { + dir("${workspace}/9.5/Checkmarx-System-Test") { + sh "docker run --rm --memory 2gb --name plugins-api-smoke-tests-9.5-${BUILD_TAG} -v maven-repo:/root/.m2 -v ${workspace}/9.5/Checkmarx-System-Test:/usr/src/automation -w /usr/src/automation --add-host WIN2012-ENV9-B:${ipAddress95} maven:3.6.1-jdk-8-alpine \ + mvn -q test -Dcxcommon.version=${env.cxCommonVersion} -Dsast.version=9.5 -Dtest=com.cx.automation.plugin.test.cxcommonclient.PluginsCxSASTSmokeTests,com.cx.automation.plugin.test.cxcommonclient.PluginsCxMandOAndOSASmokeTests \ + -Dtopology.xml.ref=/usr/src/automation/env/topology.xml -Denv=hardening_env -DfailIfNoTests=false -Dmaven.test.failure.ignore=false -DskipTests=false" + } + } + } + stage('Plugins CxCommonClient All Tests') { + steps { + dir("${workspace}/9.5/Checkmarx-System-Test") { + sh "docker run --rm --memory 2gb --name plugins-cxcommonclient-all-tests-9.5-${BUILD_TAG} -v maven-repo:/root/.m2 -v ${workspace}/9.5/Checkmarx-System-Test:/usr/src/automation -w /usr/src/automation --add-host WIN2012-ENV9-B:${ipAddress95} maven:3.6.1-jdk-8-alpine \ + mvn -q test -Dcxcommon.version=${env.cxCommonVersion} -Dsast.version=9.5 -Dtest=com.cx.automation.plugin.test.cxcommonclient.scan.*,com.cx.automation.plugin.test.cxcommonclient.osa.* \ + -Dtopology.xml.ref=/usr/src/automation/env/topology.xml -Denv=hardening_env -DfailIfNoTests=false -Dmaven.test.failure.ignore=false -DskipTests=false" + } + } + } + } + post { + always { + script { + dir("${workspace}/9.5/Checkmarx-System-Test") { + junit '**/PluginsCommonClient/target/surefire-reports/**/*.xml' + } + if (ipAddress95 != null) { + try { + node(vmName + "-9.5") { + kit.zipStashInstallationLogs("CxSAST-9.5-Logs") + } + unstash "CxSAST-9.5-Logs" + } catch (Exception e) { + kit.Warning_Msg("Could not stash CxSAST-9.5-Logs, error:\n" + e.toString()) + } + deleteVm(provider, ipAddress95, vmName + "-9.5") + } + } + } + failure { + script { + env.PIPELINE_STATUS = "Failure" + env.STAGE_NAME_FAILED = "${STAGE_NAME}" + } + } + } + } + + stage('9.6') { + stages { + stage('Create VM') { + steps { + script { + kit.Create_Vm_Terraform(vmName + "-9.6", vmTemplate96, ram, cpu, provider, decommissionPeriod, "Auto", "Plugins-CI", vmwareNetwork) + ipAddress96 = kit.getIpAddress(vmName + "-9.6", provider) + node('install01') { + kit.Create_Jenkins_Slave_On_Master_cx_operation(vmName + "-9.6") + kit.Start_Jenkins_Slave_On_Windows_Pstools_cx_operation(ipAddress96, vmName + "-9.6") + } + } + } + } + stage('Pull Automation Code') { + steps { + dir("${workspace}/9.6/Checkmarx-System-Test") { + git branch: "${env.automationBranch}", credentialsId: 'TFS-Test', poll: false, url: 'http://tfs2013:8080/tfs/DefaultCollection/Automation/_git/Checkmarx-System-Test' + sh "cp -r ../../../env ." + sh "sed -e 's//${ipAddress96}/g' -i ./env/topology.xml" + } + } + } + stage('Plugins API Sanity Test') { + steps { + dir("${workspace}/9.6/Checkmarx-System-Test") { + sh "docker run --rm --memory 2gb --name plugins-api-sanity-test-9.6-${BUILD_TAG} -v maven-repo:/root/.m2 -v ${workspace}/9.6/Checkmarx-System-Test:/usr/src/automation -w /usr/src/automation --add-host WIN2012-ENV9-B:${ipAddress96} maven:3.6.1-jdk-8-alpine \ + mvn -q clean test -Dcxcommon.version=${env.cxCommonVersion} -Dsast.version=9.6 -Dtest=com.cx.automation.plugin.test.cxcommonclient.sanity.* -Dtopology.xml.ref=/usr/src/automation/env/topology.xml -Denv=hardening_env -DfailIfNoTests=false -Dmaven.test.failure.ignore=false -DskipTests=false" + } + } + } + stage('Plugins API Smoke Tests') { + steps { + dir("${workspace}/9.6/Checkmarx-System-Test") { + sh "docker run --rm --memory 2gb --name plugins-api-smoke-tests-9.6-${BUILD_TAG} -v maven-repo:/root/.m2 -v ${workspace}/9.6/Checkmarx-System-Test:/usr/src/automation -w /usr/src/automation --add-host WIN2012-ENV9-B:${ipAddress96} maven:3.6.1-jdk-8-alpine \ + mvn -q test -Dcxcommon.version=${env.cxCommonVersion} -Dsast.version=9.6 -Dtest=com.cx.automation.plugin.test.cxcommonclient.PluginsCxSASTSmokeTests,com.cx.automation.plugin.test.cxcommonclient.PluginsCxMandOAndOSASmokeTests \ + -Dtopology.xml.ref=/usr/src/automation/env/topology.xml -Denv=hardening_env -DfailIfNoTests=false -Dmaven.test.failure.ignore=false -DskipTests=false" + } + } + } + stage('Plugins CxCommonClient All Tests') { + steps { + dir("${workspace}/9.6/Checkmarx-System-Test") { + sh "docker run --rm --memory 2gb --name plugins-cxcommonclient-all-tests-9.6-${BUILD_TAG} -v maven-repo:/root/.m2 -v ${workspace}/9.6/Checkmarx-System-Test:/usr/src/automation -w /usr/src/automation --add-host WIN2012-ENV9-B:${ipAddress96} maven:3.6.1-jdk-8-alpine \ + mvn -q test -Dcxcommon.version=${env.cxCommonVersion} -Dsast.version=9.6 -Dtest=com.cx.automation.plugin.test.cxcommonclient.scan.*,com.cx.automation.plugin.test.cxcommonclient.osa.* \ + -Dtopology.xml.ref=/usr/src/automation/env/topology.xml -Denv=hardening_env -DfailIfNoTests=false -Dmaven.test.failure.ignore=false -DskipTests=false" + } + } + } + } + post { + always { + script { + dir("${workspace}/9.6/Checkmarx-System-Test") { + junit '**/PluginsCommonClient/target/surefire-reports/**/*.xml' + } + if (ipAddress96 != null) { + try { + node(vmName + "-9.6") { + kit.zipStashInstallationLogs("CxSAST-9.6-Logs") + } + unstash "CxSAST-9.6-Logs" + } catch (Exception e) { + kit.Warning_Msg("Could not stash CxSAST-9.6-Logs, error:\n" + e.toString()) + } + deleteVm(provider, ipAddress96, vmName + "-9.6") + } + } + } + failure { + script { + env.PIPELINE_STATUS = "Failure" + env.STAGE_NAME_FAILED = "${STAGE_NAME}" + } + } + } + } + + stage('9.7') { + stages { + stage('Create VM') { + steps { + script { + kit.Create_Vm_Terraform(vmName + "-9.7", vmTemplate97, ram, cpu, provider, decommissionPeriod, "Auto", "Plugins-CI", vmwareNetwork) + ipAddress97 = kit.getIpAddress(vmName + "-9.7", provider) + node('install01') { + kit.Create_Jenkins_Slave_On_Master_cx_operation(vmName + "-9.7") + kit.Start_Jenkins_Slave_On_Windows_Pstools_cx_operation(ipAddress97, vmName + "-9.7") + } + } + } + } + stage('Pull Automation Code') { + steps { + dir("${workspace}/9.7/Checkmarx-System-Test") { + git branch: "${env.automationBranch}", credentialsId: 'TFS-Test', poll: false, url: 'http://tfs2013:8080/tfs/DefaultCollection/Automation/_git/Checkmarx-System-Test' + sh "cp -r ../../../env ." + sh "sed -e 's//${ipAddress97}/g' -i ./env/topology.xml" + } + } + } + stage('Plugins API Sanity Test') { + steps { + dir("${workspace}/9.7/Checkmarx-System-Test") { + sh "docker run --rm --memory 2gb --name plugins-api-sanity-test-9.7-${BUILD_TAG} -v maven-repo:/root/.m2 -v ${workspace}/9.7/Checkmarx-System-Test:/usr/src/automation -w /usr/src/automation --add-host WIN2012-ENV9-B:${ipAddress97} maven:3.6.1-jdk-8-alpine \ + mvn -q clean test -Dcxcommon.version=${env.cxCommonVersion} -Dsast.version=9.7 -Dtest=com.cx.automation.plugin.test.cxcommonclient.sanity.* -Dtopology.xml.ref=/usr/src/automation/env/topology.xml -Denv=hardening_env -DfailIfNoTests=false -Dmaven.test.failure.ignore=false -DskipTests=false" + } + } + } + stage('Plugins API Smoke Tests') { + steps { + dir("${workspace}/9.7/Checkmarx-System-Test") { + sh "docker run --rm --memory 2gb --name plugins-api-smoke-tests-9.7-${BUILD_TAG} -v maven-repo:/root/.m2 -v ${workspace}/9.7/Checkmarx-System-Test:/usr/src/automation -w /usr/src/automation --add-host WIN2012-ENV9-B:${ipAddress97} maven:3.6.1-jdk-8-alpine \ + mvn -q test -Dcxcommon.version=${env.cxCommonVersion} -Dsast.version=9.7 -Dtest=com.cx.automation.plugin.test.cxcommonclient.PluginsCxSASTSmokeTests,com.cx.automation.plugin.test.cxcommonclient.PluginsCxMandOAndOSASmokeTests \ + -Dtopology.xml.ref=/usr/src/automation/env/topology.xml -Denv=hardening_env -DfailIfNoTests=false -Dmaven.test.failure.ignore=false -DskipTests=false" + } + } + } + stage('Plugins CxCommonClient All Tests') { + steps { + dir("${workspace}/9.7/Checkmarx-System-Test") { + sh "docker run --rm --memory 2gb --name plugins-cxcommonclient-all-tests-9.7-${BUILD_TAG} -v maven-repo:/root/.m2 -v ${workspace}/9.7/Checkmarx-System-Test:/usr/src/automation -w /usr/src/automation --add-host WIN2012-ENV9-B:${ipAddress97} maven:3.6.1-jdk-8-alpine \ + mvn -q test -Dcxcommon.version=${env.cxCommonVersion} -Dsast.version=9.7 -Dtest=com.cx.automation.plugin.test.cxcommonclient.scan.*,com.cx.automation.plugin.test.cxcommonclient.osa.* \ + -Dtopology.xml.ref=/usr/src/automation/env/topology.xml -Denv=hardening_env -DfailIfNoTests=false -Dmaven.test.failure.ignore=false -DskipTests=false" + } + } + } + } + post { + always { + script { + dir("${workspace}/9.7/Checkmarx-System-Test") { + junit '**/PluginsCommonClient/target/surefire-reports/**/*.xml' + } + if (ipAddress97 != null) { + try { + node(vmName + "-9.7") { + kit.zipStashInstallationLogs("CxSAST-9.7-Logs") + } + unstash "CxSAST-9.7-Logs" + } catch (Exception e) { + kit.Warning_Msg("Could not stash CxSAST-9.7-Logs, error:\n" + e.toString()) + } + deleteVm(provider, ipAddress97, vmName + "-9.7") + } + } + } + failure { + script { + env.PIPELINE_STATUS = "Failure" + env.STAGE_NAME_FAILED = "${STAGE_NAME}" + } + } + } + + + } + } + } + + stage('Publish') { + when { + expression { + BRANCH_NAME == 'master' + } + } + parallel { + + stage('GitHub Release') { + environment { + GITHUB_TOKEN = credentials('github-cxflowtestuser') + } + steps { + script { + sh "ghr -t ${GITHUB_TOKEN} -u checkmarx-ltd -n ${env.cxCommonVersion} -r Cx-Client-Common -c ${GIT_COMMIT} -delete ${env.cxCommonVersion} ./target/cx-client-common-${env.cxCommonVersion}.jar" + } + } + post { + failure { + script { + env.PIPELINE_STATUS = "Failure" + env.STAGE_NAME_FAILED="${STAGE_NAME}" + } + } + } + } + + stage('Maven Central') { + environment { + GNUPG_TOKEN = credentials('gnupg-credentials') + } + steps { + script { + //sh "docker run --rm --name publish-${BUILD_TAG} -v /root/.m2:/root/.m2 -v ${workspace}:/usr/src/cx-common -v /root/.gnupg:/root/.gnupg -w /usr/src/cx-common maven:3.6.1-jdk-8 mvn deploy -P release -DskipTests -Dcxcommon.version=${env.cxCommonVersion} -Dgpg.passphrase=${GNUPG_TOKEN}" + sh "mvn deploy -P release -DskipTests -Dcxcommon.version=${env.cxCommonVersion} -Dgpg.passphrase=${GNUPG_TOKEN}" + } + } + post { + failure { + script { + env.PIPELINE_STATUS = "Failure" + env.STAGE_NAME_FAILED="${STAGE_NAME}" + } + } + } + } + + stage('cx-artifactory') { + steps { + script { + kit.Upload_To_Artifactory("./target/cx-client-common-${env.cxCommonVersion}.jar", "libs-release-local/com/checkmarx/cx-client-common/${env.cxCommonVersion}/cx-client-common-${env.cxCommonVersion}.jar") + } + } + post { + failure { + script { + env.PIPELINE_STATUS = "Failure" + env.STAGE_NAME_FAILED="${STAGE_NAME}" + } + } + } + } + } + } + } + + post { + always { + archiveArtifacts "*.zip, target/cx-client-common-${env.cxCommonVersion}.jar" + script { + try { + kit.Command_Execution_Sh("jq -n env > env.json") + kit.Command_Execution_Sh("curl -sb -k -v -H \"Content-type: application/json\" -XPOST http://cx-elastic01:9200/cx-client-common/_doc -d @env.json") + } catch (Exception e) { + kit.Warning_Msg("The message couldn't be pushed to elastic, error:\n" + e.toString()) + } + } + } + cleanup { + cleanWs() + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 00000000..bfcdda52 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# cx-client-common +Client library that allows Java applications to interact with Checkmarx products. + +## Release Notes +Please read latest features and fixes from the [Releases](https://github.com/checkmarx-ltd/Cx-Client-Common/releases/latest). + +## Build +mvn clean install + +## Contributing +Please read through our [contributing guidelines](CONTRIBUTING.md). \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 00000000..7ad3bb1f --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,40 @@ +# Maven +# Build your Java project and run tests with Apache Maven. +# Add steps that analyze code, save build artifacts, deploy, and more: +# https://docs.microsoft.com/azure/devops/pipelines/languages/java + +trigger: +- Version_9.00 + +pool: + vmImage: 'ubuntu-latest' + +steps: +- task: Maven@3 + inputs: + mavenPomFile: 'pom.xml' + mavenOptions: '-Xmx3072m' + javaHomeOption: 'JDKVersion' + jdkVersionOption: '1.8' + jdkArchitectureOption: 'x64' + publishJUnitResults: false + goals: 'package' +- task: S3Upload@1 + inputs: + awsCredentials: 'CxSLDC bucket' + regionName: 'eu-west-1' + bucketName: 'cxsdlc' + sourceFolder: 'target' + globExpressions: 'cx-client-common-*.jar' + targetFolder: '$(Build.BuildID)' + filesAcl: 'bucket-owner-full-control' + createBucket: true +- task: S3Upload@1 + inputs: + awsCredentials: 'CxSLDC bucket' + regionName: 'eu-west-1' + bucketName: 'cxsdlc' + sourceFolder: 'target' + globExpressions: 'cx-client-common-*.jar' + filesAcl: 'bucket-owner-full-control' + createBucket: true diff --git a/log4j.xml b/log4j.xml new file mode 100644 index 00000000..daa3ddfa --- /dev/null +++ b/log4j.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 86282af6..16d71e29 100644 --- a/pom.xml +++ b/pom.xml @@ -1,30 +1,40 @@ - 4.0.0 com.checkmarx cx-client-common - 8.90.0-SNAPSHOT + ${cxcommon.version} jar - Checkmarx Client - Enables web services access Checkmarx SAST and OSA scan. + Web client for interaction with Checkmarx SAST, SCA and OSA products https://www.checkmarx.com - scm:git:git://github.com/CxRepositories/Cx-Client-Common.git - scm:git:ssh://github.com/CxRepositories/Cx-Client-Common.git - https://github.com/CxRepositories/Cx-Client-Common/tree/master + scm:git:git://github.com/checkmarx-ltd/Cx-Client-Common.git + scm:git:ssh://github.com/checkmarx-ltd/Cx-Client-Common.git + https://github.com/checkmarx-ltd/Cx-Client-Common/tree/master + MIT License http://www.opensource.org/licenses/mit-license.php - + UTF-8 + 1.7.31 + 1.2.0 + 2.3.0 + 1.18.6 + 24.2.2 + + + checkmarx-ltd_Cx-Client-Common + checkmarx-ltd + https://sonarcloud.io + Cx-Client-Common Checkmarx @@ -59,71 +69,134 @@ maven-compiler-plugin 3.7.0 - 1.7 - 1.7 + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + + src/main/resources + true + + - + + org.slf4j + slf4j-api + ${slf4j.version} + provided + + + junit + junit + 4.13.1 + test + org.freemarker freemarker - 2.3.23 + 2.3.31 org.apache.httpcomponents httpmime - 4.4.1 + 4.5.4 + + + httpclient + org.apache.httpcomponents + + + + + org.apache.httpcomponents + httpclient + 4.5.13 org.apache.httpcomponents httpcore - 4.4 + 4.4.12 + + + org.apache.httpcomponents + httpclient-win + 4.5.10 - com.fasterxml.jackson.core - jackson-databind - 2.8.9 + com.google.guava + guava + 27.0-jre - org.whitesource - whitesource-fs-agent - 18.7.1 + com.checkmarx + cx-ws-fs-agent + ${cx.ws.fs.agent.version} javax.xml.bind jaxb-api - ch.qos.logback - logback-classic - - - org.slf4j - slf4j-api + commons-collections + commons-collections - + - org.slf4j - slf4j-api - 1.7.5 + org.projectlombok + lombok + ${lombok.version} provided - + + org.awaitility + awaitility + 4.0.2 + + + org.apache.commons + commons-lang3 + 3.10 + + + + + jakarta.xml.bind + jakarta.xml.bind-api + 3.0.1 + - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - + + + org.glassfish.jaxb + jaxb-runtime + 3.0.2 + + @@ -134,7 +207,6 @@ - release @@ -167,14 +239,13 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.7 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 true - ossrh - https://oss.sonatype.org/ - true + central + true @@ -182,8 +253,7 @@ maven-gpg-plugin 1.5 - C:\Program Files (x86)\GNU\GnuPG\gpg2.exe - Checkmarx123456 + /usr/bin/gpg @@ -199,71 +269,12 @@ - - Gal Or Nussbaum - gal.nussbaum@checkmarx.com - Checkmarx - https://www.checkmarx.com/ - - Architect - Developer - - - https://www.linkedin.com/in/gal-nussbaum-68b3a76a/de - - - - Dor Golan - dor.golan@checkmarx.com - Checkmarx - https://www.checkmarx.com/ - - Architect - Developer - - - http://i.imgur.com/44Iil53.png - - - - Shaul Valero - shaul.valero@checkmarx.com - Checkmarx - https://www.checkmarx.com/ - - Dev TL - Developer - - - http://i.imgur.com/44Iil53.png - - - - Eyal Amor - eyal.amor@checkmarx.com - Checkmarx - https://www.checkmarx.com/ - - Architect - Developer - - - http://i.imgur.com/44Iil53.png - - - - Yair David - Yair.David@checkmarx.com - Checkmarx + Kedar Bhujade + kedar.bhujade@checkmarx.com + Checkmarx Ltd https://www.checkmarx.com/ - - QA Manager - - - http://i.imgur.com/EVIS8LO.jpg - - \ No newline at end of file + diff --git a/src/main/java/com/cx/restclient/CxClientDelegator.java b/src/main/java/com/cx/restclient/CxClientDelegator.java new file mode 100644 index 00000000..2966a037 --- /dev/null +++ b/src/main/java/com/cx/restclient/CxClientDelegator.java @@ -0,0 +1,215 @@ +package com.cx.restclient; + +import com.cx.restclient.ast.AstSastClient; +import com.cx.restclient.ast.AstScaClient; +import com.cx.restclient.ast.dto.sca.AstScaResults; +import com.cx.restclient.common.Scanner; +import com.cx.restclient.common.summary.SummaryUtils; +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.configuration.PropertyFileLoader; +import com.cx.restclient.dto.Results; +import com.cx.restclient.dto.ScanResults; +import com.cx.restclient.dto.ScannerType; +import com.cx.restclient.osa.dto.OSAResults; +import com.cx.restclient.sast.dto.SASTResults; +import com.cx.restclient.sast.utils.State; +import org.slf4j.Logger; + +import java.net.MalformedURLException; +import java.util.EnumMap; +import java.util.Map; + +import static com.cx.restclient.common.CxPARAM.*; +import static com.cx.restclient.cxArm.utils.CxARMUtils.getPoliciesNames; + +/** + * Created by Galn on 05/02/2018. + */ + +public class CxClientDelegator implements Scanner { + private static final PropertyFileLoader properties = PropertyFileLoader.getDefaultInstance(); + + private static final String PRINT_LINE = "-----------------------------------------------------------------------------------------"; + + private Logger log; + private CxScanConfig config; + + Map scannersMap = new EnumMap<>(ScannerType.class); + + public CxClientDelegator(CxScanConfig config, Logger log) throws MalformedURLException { + + this.config = config; + this.log = log; + if (config.isAstSastEnabled()) { + scannersMap.put(ScannerType.AST_SAST, new AstSastClient(config, log)); + } + + if (config.isSastEnabled()) { + scannersMap.put(ScannerType.SAST, new CxSASTClient(config, log)); + } + + if (config.isOsaEnabled()) { + scannersMap.put(ScannerType.OSA, new CxOSAClient(config, log)); + } + + if (config.isAstScaEnabled()) { + scannersMap.put(ScannerType.AST_SCA, new AstScaClient(config, log)); + } + } + + + public CxClientDelegator(String serverUrl, String username, String password, String origin, boolean disableCertificateValidation, Logger log) throws MalformedURLException { + this(new CxScanConfig(serverUrl, username, password, origin, disableCertificateValidation), log); + } + + @Override + public ScanResults init() { + log.info("Initializing Cx client [{}]", properties.get("version")); + ScanResults scanResultsCombined = new ScanResults(); + + scannersMap.forEach((key, scanner) -> { + Results scanResults = scanner.init(); + scanResultsCombined.put(key, scanResults); + }); + + return scanResultsCombined; + } + + + @Override + public ScanResults initiateScan() { + + ScanResults scanResultsCombined = new ScanResults(); + + scannersMap.forEach((key, scanner) -> { + if (scanner.getState() == State.SUCCESS) { + Results scanResults = scanner.initiateScan(); + scanResultsCombined.put(key, scanResults); + } + }); + + return scanResultsCombined; + } + + + @Override + public ScanResults waitForScanResults() { + + ScanResults scanResultsCombined = new ScanResults(); + + scannersMap.forEach((key, scanner) -> { + if (scanner.getState() == State.SUCCESS) { + Results scanResults = scanner.waitForScanResults(); + scanResultsCombined.put(key, scanResults); + } + }); + + return scanResultsCombined; + } + + @Override + public ScanResults getLatestScanResults() { + + ScanResults scanResultsCombined = new ScanResults(); + + scannersMap.forEach((key, scanner) -> { + if (scanner.getState() == State.SUCCESS) { + Results scanResults = scanner.getLatestScanResults(); + scanResultsCombined.put(key, scanResults); + } + }); + + return scanResultsCombined; + + } + + public void printIsProjectViolated(ScanResults scanResults) { + if (config.getEnablePolicyViolations()) { + log.info(PRINT_LINE); + log.info("Policy Management: SAST and OSA "); + log.info("--------------------"); + + OSAResults osaResults = (OSAResults) scanResults.get(ScannerType.OSA); + SASTResults sastResults = (SASTResults) scanResults.get(ScannerType.SAST); + + boolean hasOsaViolations = + osaResults != null && + osaResults.getOsaPolicies() != null && + !osaResults.getOsaPolicies().isEmpty(); + + boolean hasSastPolicies = false; + + if (sastResults != null && sastResults.getSastPolicies() != null && !sastResults.getSastPolicies().isEmpty()) { + hasSastPolicies = true; + } + + if (!hasSastPolicies && !hasOsaViolations) { + log.info(PROJECT_POLICY_COMPLIANT_STATUS_SAST); + log.info(PRINT_LINE); + } else { + log.info(PROJECT_POLICY_VIOLATED_STATUS_SAST); + if (hasSastPolicies) { + log.info("SAST violated policies names: {}", getPoliciesNames(sastResults.getSastPolicies())); + } + if (hasOsaViolations) { + log.info("OSA violated policies names: {}", getPoliciesNames(osaResults.getOsaPolicies())); + } + log.info(PRINT_LINE); + } + } + if (config.getEnablePolicyViolationsSCA()) { + log.info(PRINT_LINE); + log.info("Policy Management: SCA "); + log.info("--------------------"); + + AstScaResults scaResults = (AstScaResults) scanResults.get(ScannerType.AST_SCA); + + boolean hasScaViolations = false; + if (scaResults != null && scaResults.getPolicyEvaluations() != null && !scaResults.getPolicyEvaluations().isEmpty()) { + hasScaViolations = true; + } + + if (!hasScaViolations) { + log.info(PROJECT_POLICY_COMPLIANT_STATUS_SCA); + log.info(PRINT_LINE); + } else { + log.info(PROJECT_POLICY_VIOLATED_STATUS_SCA); + if (hasScaViolations) { + log.info("SCA policies are violated."); + } + log.info(PRINT_LINE); + } + } + } + + + public String generateHTMLSummary(ScanResults combinedResults) throws Exception { + + return SummaryUtils.generateSummary( + (SASTResults) combinedResults.get(ScannerType.SAST), + (OSAResults) combinedResults.get(ScannerType.OSA), + (AstScaResults) combinedResults.get(ScannerType.AST_SCA), config); + } + + public String generateHTMLSummary(SASTResults sastResults, OSAResults osaResults, AstScaResults scaResults) throws Exception { + return SummaryUtils.generateSummary(sastResults, osaResults, scaResults, config); + } + + public CxSASTClient getSastClient() { + return (CxSASTClient) scannersMap.get(ScannerType.SAST); + } + + public CxOSAClient getOsaClient() { + return (CxOSAClient) scannersMap.get(ScannerType.OSA); + } + + public AstScaClient getScaClient() { + return (AstScaClient) scannersMap.get(ScannerType.AST_SCA); + } + + public void close() { + scannersMap.values().forEach(Scanner::close); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/cx/restclient/CxOSAClient.java b/src/main/java/com/cx/restclient/CxOSAClient.java index 8366665e..54a82c2e 100644 --- a/src/main/java/com/cx/restclient/CxOSAClient.java +++ b/src/main/java/com/cx/restclient/CxOSAClient.java @@ -1,23 +1,34 @@ package com.cx.restclient; +import com.cx.restclient.common.Scanner; +import com.cx.restclient.common.ShragaUtils; import com.cx.restclient.common.Waiter; import com.cx.restclient.configuration.CxScanConfig; -import com.cx.restclient.cxArm.dto.Policy; +import com.cx.restclient.dto.Results; import com.cx.restclient.dto.Status; import com.cx.restclient.exception.CxClientException; +import com.cx.restclient.exception.CxOSAException; import com.cx.restclient.httpClient.CxHttpClient; import com.cx.restclient.osa.dto.*; import com.cx.restclient.osa.utils.OSAUtils; +import com.cx.restclient.sast.utils.LegacyClient; +import com.cx.restclient.sast.utils.State; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; import org.apache.http.entity.StringEntity; import org.slf4j.Logger; +import org.whitesource.agent.utils.CommandLineErrors; import org.whitesource.fs.ComponentScan; import java.io.IOException; +import java.net.MalformedURLException; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Properties; import static com.cx.restclient.cxArm.dto.CxProviders.OPEN_SOURCE; -import static com.cx.restclient.cxArm.utils.CxARMUtils.getProjectViolations; +import static com.cx.restclient.cxArm.utils.CxARMUtils.getProjectViolatedPolicies; import static com.cx.restclient.httpClient.utils.ContentType.CONTENT_TYPE_APPLICATION_JSON_V1; import static com.cx.restclient.httpClient.utils.HttpClientHelper.convertToJson; import static com.cx.restclient.osa.utils.OSAParam.*; @@ -26,36 +37,65 @@ /** * Created by Galn on 05/02/2018. */ -class CxOSAClient { - - private CxHttpClient httpClient; - private Logger log; - private CxScanConfig config; - private Waiter osaWaiter = new Waiter("CxOSA scan", 20) { - @Override - public OSAScanStatus getStatus(String id) throws CxClientException, IOException { - return getOSAScanStatus(id); - } +public class CxOSAClient extends LegacyClient implements Scanner { - @Override - public void printProgress(OSAScanStatus scanStatus) { - printOSAProgress(scanStatus, getStartTimeSec()); - } + private Waiter osaWaiter; - @Override - public OSAScanStatus resolveStatus(OSAScanStatus scanStatus) throws CxClientException { - return resolveOSAStatus(scanStatus); - } - }; + private String scanId; + private OSAResults osaResults = new OSAResults(); + + + public OSAScanStatus getStatus(String id) throws IOException { + return getOSAScanStatus(id); + } + + public CxOSAClient(CxScanConfig config, Logger log) throws MalformedURLException { + super(config, log); + int interval = config.getOsaProgressInterval() != null ? config.getOsaProgressInterval() : 20; + int retry = config.getConnectionRetries() != null ? config.getConnectionRetries() : 3; + osaWaiter = new Waiter("CxOSA scan", interval, retry) { + @Override + public OSAScanStatus getStatus(String id) throws IOException { + return getOSAScanStatus(id); + } + + @Override + public void printProgress(OSAScanStatus scanStatus) { + printOSAProgress(scanStatus, getStartTimeSec()); + } - public CxOSAClient(CxHttpClient client, Logger log, CxScanConfig config) { - this.log = log; - this.httpClient = client; - this.config = config; + @Override + public OSAScanStatus resolveStatus(OSAScanStatus scanStatus) { + return resolveOSAStatus(scanStatus); + } + }; } - //API - public String createOSAScan(long projectId) throws IOException, CxClientException { + @Override + public Results init() { + OSAResults initOsaResults = new OSAResults(); + try { + initiate(); + } catch (CxClientException e) { + log.error(e.getMessage()); + setState(State.FAILED); + initOsaResults.setException(e); + } + return initOsaResults; + } + + @Override + public Results initiateScan() { + osaResults = new OSAResults(); + try { + ensureProjectIdSpecified(); + } catch (CxClientException e) { + log.error(e.getMessage()); + setState(State.FAILED); + osaResults.setException(e); + return osaResults; + } + log.info("----------------------------------- Create CxOSA Scan:------------------------------------"); log.info("Creating OSA scan"); String osaDependenciesJson = config.getOsaDependenciesJson(); @@ -63,13 +103,57 @@ public String createOSAScan(long projectId) throws IOException, CxClientExceptio try { osaDependenciesJson = resolveOSADependencies(); } catch (Exception e) { - throw new CxClientException("Failed to resolve dependencies for OSA scan: " + e.getMessage(), e); + log.error(e.getMessage()); + setState(State.FAILED); + osaResults.setException(new CxClientException("Failed to resolve dependencies for OSA scan: " + e.getMessage(), e)); + return osaResults; } } - return sendOSAScan(osaDependenciesJson, projectId); + if ((!validateOsaDependencies(osaDependenciesJson) && dependenciesErrorsExist()) || + (config.isOsaFailOnError() && dependenciesErrorsExist())) { + log.error("Fail to resolve dependencies: {}", osaDependenciesJson); + List failedCommands = CommandLineErrors.getFailedCommands(); + for (String fc : failedCommands) { + log.error("Failed command: {}", fc); + } + setState(State.FAILED); + osaResults.setException(new CxOSAException("Failed to resolve dependencies for OSA scan: \n" + osaDependenciesJson)); + return osaResults; + } else if ((!validateOsaDependencies(osaDependenciesJson) && !dependenciesErrorsExist())) { + log.warn("No dependencies found for OSA scan"); + setState(State.FAILED); + osaResults.setException(new CxOSAException("No dependencies found for OSA scan")); + return osaResults; + } + + try { + scanId = sendOSAScan(osaDependenciesJson, projectId); + } catch (IOException e) { + scanId = null; + log.error(e.getMessage()); + setState(State.FAILED); + osaResults.setException(new CxClientException("Error sending OSA scan request.", e)); + return osaResults; + } + + osaResults.setOsaProjectSummaryLink(config.getUrl(), projectId); + osaResults.setOsaScanId(scanId); + return osaResults; + } + + private boolean dependenciesErrorsExist() { + return CommandLineErrors.getFailedCommands() != null && !CommandLineErrors.getFailedCommands().isEmpty(); } - private String resolveOSADependencies() { + private boolean validateOsaDependencies(String osaDependenciesJson) { + return StringUtils.isNotEmpty(osaDependenciesJson) && !osaDependenciesJson.equalsIgnoreCase("[{\"dependencies\":[],\"projectToken\":\" \"}]"); + } + + public void setOsaFSAProperties(Properties fsaConfig) { //For CxMaven plugin + config.setOsaFsaConfig(fsaConfig); + } + + private String resolveOSADependencies() throws JsonProcessingException { log.info("Scanning for CxOSA compatible files"); Properties scannerProperties = config.getOsaFsaConfig(); if (scannerProperties == null) { @@ -77,42 +161,64 @@ private String resolveOSADependencies() { config.getOsaFolderExclusions(), config.getOsaFilterPattern(), config.getOsaArchiveIncludePatterns(), - config.getSourceDir(), + config.getEffectiveSourceDirForDependencyScan(), config.getOsaRunInstall(), + config.getOsaScanDepth(), log); } + ObjectMapper mapper = new ObjectMapper(); + log.info("Scanner properties: " + mapper.writerWithDefaultPrettyPrinter().writeValueAsString(scannerProperties.toString())); ComponentScan componentScan = new ComponentScan(scannerProperties); String osaDependenciesJson = componentScan.scan(); - OSAUtils.writeToOsaListToFile(config.getReportsDir(), osaDependenciesJson, log); - + OSAUtils.writeToOsaListToFile(OSAUtils.getWorkDirectory(config.getReportsDir(), config.getOsaGenerateJsonReport()), osaDependenciesJson, log); return osaDependenciesJson; } - public OSAResults getOSAResults(String scanId, long projectId) throws CxClientException, InterruptedException, IOException { - log.info("-------------------------------------Get CxOSA Results:-----------------------------------"); - log.info("Waiting for OSA scan to finish"); - OSAScanStatus osaScanStatus = osaWaiter.waitForTaskToFinish(scanId, -1, log); - log.info("OSA scan finished successfully. Retrieving OSA scan results"); + @Override + public CxHttpClient getHttpClient() { + return httpClient; + } + + @Override + public Results waitForScanResults() { + try { + ensureProjectIdSpecified(); + + if (scanId == null) { + throw new CxClientException("Scan was not created."); + } - log.info("Creating OSA reports"); - OSAResults osaResults = retrieveOSAResults(scanId, osaScanStatus, projectId); + log.info("-------------------------------------Get CxOSA Results:-----------------------------------"); + log.info("Waiting for OSA scan to finish"); - if (config.getEnablePolicyViolations()) { - resolveOSAViolation(osaResults, projectId); - } + OSAScanStatus osaScanStatus; + osaScanStatus = osaWaiter.waitForTaskToFinish(scanId, this.config.getOsaScanTimeoutInMinutes(), log); + log.info("OSA scan finished successfully. Retrieving OSA scan results"); - OSAUtils.printOSAResultsToConsole(osaResults, config.getEnablePolicyViolations(), log); + log.info("Creating OSA reports"); + + osaResults = retrieveOSAResults(scanId, osaScanStatus, projectId); + + if (config.getEnablePolicyViolations()) { + resolveOSAViolation(osaResults, projectId); + } - if (config.getReportsDir() != null) { - writeJsonToFile(OSA_SUMMARY_NAME, osaResults.getResults(), config.getReportsDir(), log); - writeJsonToFile(OSA_LIBRARIES_NAME, osaResults.getOsaLibraries(), config.getReportsDir(), log); - writeJsonToFile(OSA_VULNERABILITIES_NAME, osaResults.getOsaVulnerabilities(), config.getReportsDir(), log); + OSAUtils.printOSAResultsToConsole(osaResults, config.getEnablePolicyViolations(), log); + + if (config.getReportsDir() != null) { + writeJsonToFile(OSA_SUMMARY_NAME, osaResults.getResults(), config.getReportsDir(), config.getOsaGenerateJsonReport(), log); + writeJsonToFile(OSA_LIBRARIES_NAME, osaResults.getOsaLibraries(), config.getReportsDir(), config.getOsaGenerateJsonReport(), log); + writeJsonToFile(OSA_VULNERABILITIES_NAME, osaResults.getOsaVulnerabilities(), config.getReportsDir(), config.getOsaGenerateJsonReport(), log); + } + } catch (Exception e) { + log.error(e.getMessage()); + osaResults.setException(new CxClientException("Failed to retrieve OSA results.", e)); } return osaResults; } - private OSAResults retrieveOSAResults(String scanId, OSAScanStatus osaScanStatus, long projectId) throws CxClientException, IOException { + private OSAResults retrieveOSAResults(String scanId, OSAScanStatus osaScanStatus, long projectId) throws IOException { OSASummaryResults osaSummaryResults = getOSAScanSummaryResults(scanId); List osaLibraries = getOSALibraries(scanId); List osaVulnerabilities = getOSAVulnerabilities(scanId); @@ -122,66 +228,75 @@ private OSAResults retrieveOSAResults(String scanId, OSAScanStatus osaScanStatus return results; } - private void resolveOSAViolation(OSAResults osaResults, long projectId){ + private void resolveOSAViolation(OSAResults osaResults, long projectId) { try { - for (Policy policy : getProjectViolations(httpClient, config.getCxARMUrl(), projectId, OPEN_SOURCE.value())) { - osaResults.getOsaPolicies().add(policy.getPolicyName()); - osaResults.addAllViolations(policy.getViolations()); - } - }catch (Exception ex) { - log.error("CxARM is not available. Policy violations for OSA cannot be calculated: " + ex.getMessage()); + getProjectViolatedPolicies(httpClient, config.getCxARMUrl(), projectId, OPEN_SOURCE.value()) + .forEach(osaResults::addPolicy); + } catch (Exception ex) { + throw new CxClientException("CxARM is not available. Policy violations for OSA cannot be calculated: " + ex.getMessage()); } } + @Override + public Results getLatestScanResults() { + osaResults = new OSAResults(); + try { + ensureProjectIdSpecified(); - public OSAResults getLatestOSAResults(long projectId) throws CxClientException, IOException, InterruptedException { - log.info("----------------------------------Get CxOSA Last Results:--------------------------------"); - List scanList = getOSALastOSAStatus(projectId); - for (OSAScanStatus s : scanList) { - if (Status.SUCCEEDED.value().equals(s.getState().getName())) { - return retrieveOSAResults(s.getId(), s, projectId); + log.info("----------------------------------Get CxOSA Last Results:--------------------------------"); + + List scanList = getOSALastOSAStatus(projectId); + for (OSAScanStatus s : scanList) { + if (Status.SUCCEEDED.value().equals(s.getState().getName())) { + osaResults = retrieveOSAResults(s.getId(), s, projectId); + break; + } } + } catch (Exception e) { + log.error(e.getMessage()); + osaResults.setException(new CxClientException("Error getting last scan results.", e)); } - return new OSAResults(); + + return osaResults; } //Private Methods - private String sendOSAScan(String osaDependenciesJson, long projectId) throws CxClientException, IOException { + private String sendOSAScan(String osaDependenciesJson, long projectId) throws IOException { log.info("Sending OSA scan request"); CreateOSAScanResponse osaScan = sendOSARequest(projectId, osaDependenciesJson); - String summaryLink = OSAUtils.composeProjectOSASummaryLink(config.getUrl(), projectId); - log.info("OSA scan created successfully. Link to project state: " + summaryLink); + //String summaryLink = OSAUtils.composeProjectOSASummaryLink(config.getUrl(), projectId); + log.info("OSA scan created successfully. Link to project state: " + config.getUrl() + LINK_FORMAT + projectId + LINK_FORMAL_SUMMARY); return osaScan.getScanId(); } - private CreateOSAScanResponse sendOSARequest(long projectId, String osaDependenciesJson) throws IOException, CxClientException { + private CreateOSAScanResponse sendOSARequest(long projectId, String osaDependenciesJson) throws IOException { CreateOSAScanRequest req = new CreateOSAScanRequest(projectId, osaDependenciesJson); - StringEntity entity = new StringEntity(convertToJson(req)); + StringEntity entity = new StringEntity(convertToJson(req), StandardCharsets.UTF_8); return httpClient.postRequest(OSA_SCAN_PROJECT, CONTENT_TYPE_APPLICATION_JSON_V1, entity, CreateOSAScanResponse.class, 201, "create OSA scan"); } - private OSASummaryResults getOSAScanSummaryResults(String scanId) throws IOException, CxClientException { + private OSASummaryResults getOSAScanSummaryResults(String scanId) throws IOException { String relativePath = OSA_SCAN_SUMMARY + SCAN_ID_QUERY_PARAM + scanId; return httpClient.getRequest(relativePath, CONTENT_TYPE_APPLICATION_JSON_V1, OSASummaryResults.class, 200, "OSA scan summary results", false); } - private List getOSALastOSAStatus(long projectId) throws IOException, CxClientException { + private List getOSALastOSAStatus(long projectId) throws IOException { return (List) httpClient.getRequest(OSA_SCANS + PROJECT_ID_QUERY_PARAM + projectId, CONTENT_TYPE_APPLICATION_JSON_V1, OSAScanStatus.class, 200, " last OSA scan ID", true); } - private List getOSALibraries(String scanId) throws IOException, CxClientException { + private List getOSALibraries(String scanId) throws IOException { String relPath = OSA_SCAN_LIBRARIES + SCAN_ID_QUERY_PARAM + scanId + ITEM_PER_PAGE_QUERY_PARAM + MAX_ITEMS; return (List) httpClient.getRequest(relPath, CONTENT_TYPE_APPLICATION_JSON_V1, Library.class, 200, "OSA libraries", true); } - private List getOSAVulnerabilities(String scanId) throws CxClientException, IOException { + private List getOSAVulnerabilities(String scanId) throws IOException { String relPath = OSA_SCAN_VULNERABILITIES + SCAN_ID_QUERY_PARAM + scanId + ITEM_PER_PAGE_QUERY_PARAM + MAX_ITEMS; return (List) httpClient.getRequest(relPath, CONTENT_TYPE_APPLICATION_JSON_V1, CVE.class, 200, "OSA vulnerabilities", true); } //Waiter - overload methods - private OSAScanStatus getOSAScanStatus(String scanId) throws CxClientException, IOException { + private OSAScanStatus getOSAScanStatus(String scanId) throws IOException { String relPath = OSA_SCAN_STATUS.replace("{scanId}", scanId); OSAScanStatus scanStatus = httpClient.getRequest(relPath, CONTENT_TYPE_APPLICATION_JSON_V1, OSAScanStatus.class, 200, "OSA scan status", false); int stateId = scanStatus.getState().getId(); @@ -197,27 +312,31 @@ private OSAScanStatus getOSAScanStatus(String scanId) throws CxClientException, } private void printOSAProgress(OSAScanStatus scanStatus, long startTime) { - long elapsedSec = System.currentTimeMillis() / 1000 - startTime; - long hours = elapsedSec / 3600; - long minutes = elapsedSec % 3600 / 60; - long seconds = elapsedSec % 60; - String hoursStr = (hours < 10) ? ("0" + Long.toString(hours)) : (Long.toString(hours)); - String minutesStr = (minutes < 10) ? ("0" + Long.toString(minutes)) : (Long.toString(minutes)); - String secondsStr = (seconds < 10) ? ("0" + Long.toString(seconds)) : (Long.toString(seconds)); - log.info("Waiting for OSA scan results. Elapsed time: " + hoursStr + ":" + minutesStr + ":" + secondsStr + ". " + + String timestamp = ShragaUtils.getTimestampSince(startTime); + + log.info("Waiting for OSA scan results. Elapsed time: " + timestamp + ". " + "Status: " + scanStatus.getState().getName()); } - private OSAScanStatus resolveOSAStatus(OSAScanStatus scanStatus) throws CxClientException { - if (Status.FAILED == scanStatus.getBaseStatus()) { + private OSAScanStatus resolveOSAStatus(OSAScanStatus scanStatus) { + if (scanStatus == null) { + throw new CxClientException("OSA scan cannot be completed."); + } else if (Status.FAILED == scanStatus.getBaseStatus()) { String failedMsg = scanStatus.getState() == null ? "" : "status [" + scanStatus.getState().getName() + "]. Reason: " + scanStatus.getState().getFailureReason(); throw new CxClientException("OSA scan cannot be completed. " + failedMsg); } if (Status.SUCCEEDED == scanStatus.getBaseStatus()) { log.info("OSA scan finished."); - return scanStatus; } return scanStatus; } + + private void ensureProjectIdSpecified() { + if (projectId == 0) { + throw new CxClientException("projectId must be set before executing this method."); + } + } + + } diff --git a/src/main/java/com/cx/restclient/CxSASTClient.java b/src/main/java/com/cx/restclient/CxSASTClient.java index 9cc88e00..b0384755 100644 --- a/src/main/java/com/cx/restclient/CxSASTClient.java +++ b/src/main/java/com/cx/restclient/CxSASTClient.java @@ -1,29 +1,42 @@ package com.cx.restclient; +import com.cx.restclient.common.Scanner; +import com.cx.restclient.common.ShragaUtils; import com.cx.restclient.common.Waiter; import com.cx.restclient.configuration.CxScanConfig; -import com.cx.restclient.cxArm.dto.Policy; +import com.cx.restclient.dto.*; import com.cx.restclient.dto.Status; import com.cx.restclient.exception.CxClientException; -import com.cx.restclient.httpClient.CxHttpClient; +import com.cx.restclient.exception.CxHTTPClientException; import com.cx.restclient.sast.dto.*; +import com.cx.restclient.sast.utils.LegacyClient; import com.cx.restclient.sast.utils.SASTUtils; +import com.cx.restclient.sast.utils.State; import com.cx.restclient.sast.utils.zip.CxZipUtils; +import com.google.gson.Gson; import org.apache.http.HttpEntity; +import org.apache.http.entity.BufferedHttpEntity; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.entity.mime.HttpMultipartMode; import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.entity.mime.content.InputStreamBody; +import org.awaitility.core.ConditionTimeoutException; +import org.json.JSONObject; import org.slf4j.Logger; +import java.io.ByteArrayInputStream; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.util.List; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; import static com.cx.restclient.cxArm.dto.CxProviders.SAST; -import static com.cx.restclient.cxArm.utils.CxARMUtils.getProjectViolations; +import static com.cx.restclient.cxArm.utils.CxARMUtils.getProjectViolatedPolicies; import static com.cx.restclient.httpClient.utils.ContentType.*; import static com.cx.restclient.httpClient.utils.HttpClientHelper.convertToJson; import static com.cx.restclient.sast.utils.SASTParam.*; @@ -33,61 +46,357 @@ /** * Created by Galn on 05/02/2018. */ +public class CxSASTClient extends LegacyClient implements Scanner { -class CxSASTClient { - private Logger log; - private CxHttpClient httpClient; - private CxScanConfig config; - private int reportTimeoutSec = 500; - private Waiter sastWaiter = new Waiter("CxSAST scan", 20) { + public static final String JENKINS = "jenkins"; + + private int reportTimeoutSec = 5000; + private int cxARMTimeoutSec = 1000; + private Waiter sastWaiter; + private static final String SCAN_ID_PATH_PARAM = "{scanId}"; + private static final String PROJECT_ID_PATH_PARAM = "{projectId}"; + + private static final String SCAN_WITH_SETTINGS_URL = "sast/scanWithSettings"; + private static final String ENGINE_CONFIGURATION_ID_DEFAULT = "0"; + private long scanId; + private SASTResults sastResults = new SASTResults(); + private static final String SWAGGER_LOCATION = "help/swagger/docs/v1.1"; + private static final String ZIPPED_SOURCE = "zippedSource"; + private static final String SAST_SCAN = "SAST scan status"; + private static final String MSG_AVOID_DUPLICATE_PROJECT_SCANS = "\nAvoid duplicate project scans in queue\n"; + + private String language = "en-US"; + + private Waiter reportWaiter = new Waiter("Scan report", 10, 3) { @Override - public ResponseQueueScanStatus getStatus(String id) throws CxClientException, IOException { - return getSASTScanStatus(id); + public ReportStatus getStatus(String id) throws IOException { + return getReportStatus(id); } @Override - public void printProgress(ResponseQueueScanStatus scanStatus) { - printSASTProgress(scanStatus, getStartTimeSec()); + public void printProgress(ReportStatus reportStatus) { + printReportProgress(reportStatus, getStartTimeSec()); } @Override - public ResponseQueueScanStatus resolveStatus(ResponseQueueScanStatus scanStatus) throws CxClientException { - return resolveSASTStatus(scanStatus); + public ReportStatus resolveStatus(ReportStatus reportStatus) { + return resolveReportStatus(reportStatus); + } + + //Report Waiter - overload methods + private ReportStatus getReportStatus(String reportId) throws CxClientException, IOException { + ReportStatus reportStatus = httpClient.getRequest(SAST_GET_REPORT_STATUS.replace("{reportId}", reportId), CONTENT_TYPE_APPLICATION_JSON_V1, ReportStatus.class, 200, " report status", false); + reportStatus.setBaseId(reportId); + String currentStatus = reportStatus.getStatus().getValue(); + if (currentStatus.equals(ReportStatusEnum.INPROCESS.value())) { + reportStatus.setBaseStatus(Status.IN_PROGRESS); + } else if (currentStatus.equals(ReportStatusEnum.FAILED.value())) { + reportStatus.setBaseStatus(Status.FAILED); + } else { + reportStatus.setBaseStatus(Status.SUCCEEDED); //todo fix it!! + } + + return reportStatus; + } + + private ReportStatus resolveReportStatus(ReportStatus reportStatus) throws CxClientException { + if (reportStatus != null) { + if (Status.SUCCEEDED == reportStatus.getBaseStatus()) { + return reportStatus; + } else { + throw new CxClientException("Generation of scan report [id=" + reportStatus.getBaseId() + "] failed."); + } + } else { + throw new CxClientException("Generation of scan report failed."); + } + } + + private void printReportProgress(ReportStatus reportStatus, long startTime) { + String reportType = reportStatus.getContentType().replace("application/", ""); + log.info("Waiting for server to generate " + reportType + " report. " + (startTime + reportTimeoutSec - (System.currentTimeMillis() / 1000)) + " seconds left to timeout"); } + }; - private Waiter reportWaiter = new Waiter("Scan report", 5) { + private Waiter cxARMWaiter = new Waiter("CxARM policy violations", 20, 3) { @Override - public ReportStatus getStatus(String id) throws CxClientException, IOException { - return getReportStatus(id); + public CxARMStatus getStatus(String id) throws IOException { + return getCxARMStatus(id); } @Override - public void printProgress(ReportStatus reportStatus) { - printReportProgress(reportStatus, getStartTimeSec()); + public void printProgress(CxARMStatus cxARMStatus) { + printCxARMProgress(getStartTimeSec()); } @Override - public ReportStatus resolveStatus(ReportStatus reportStatus) throws CxClientException { - return resolveReportStatus(reportStatus); + public CxARMStatus resolveStatus(CxARMStatus cxARMStatus) { + return resolveCxARMStatus(cxARMStatus); + } + + + //CxARM Waiter - overload methods + private CxARMStatus getCxARMStatus(String projectId) throws CxClientException, IOException { + CxARMStatus cxARMStatus = httpClient.getRequest(SAST_GET_CXARM_STATUS.replace(PROJECT_ID_PATH_PARAM, projectId), CONTENT_TYPE_APPLICATION_JSON_V1, CxARMStatus.class, 200, " cxARM status", false); + cxARMStatus.setBaseId(projectId); + + String currentStatus = cxARMStatus.getStatus(); + if (currentStatus.equals(CxARMStatusEnum.IN_PROGRESS.value())) { + cxARMStatus.setBaseStatus(Status.IN_PROGRESS); + } else if (currentStatus.equals(CxARMStatusEnum.FAILED.value())) { + cxARMStatus.setBaseStatus(Status.FAILED); + } else if (currentStatus.equals(CxARMStatusEnum.FINISHED.value())) { + cxARMStatus.setBaseStatus(Status.SUCCEEDED); + } else { + cxARMStatus.setBaseStatus(Status.FAILED); + } + + return cxARMStatus; + } + + private void printCxARMProgress(long startTime) { + log.info("Waiting for server to retrieve policy violations. " + (startTime + cxARMTimeoutSec - (System.currentTimeMillis() / 1000)) + " seconds left to timeout"); + } + + private CxARMStatus resolveCxARMStatus(CxARMStatus cxARMStatus) throws CxClientException { + if (cxARMStatus != null) { + if (Status.SUCCEEDED == cxARMStatus.getBaseStatus()) { + return cxARMStatus; + } else { + throw new CxClientException("Getting policy violations of project [id=" + cxARMStatus.getBaseId() + "] failed."); + } + } else { + throw new CxClientException("Getting policy violations of project failed."); + } } }; - CxSASTClient(CxHttpClient client, Logger log, CxScanConfig config) { - this.log = log; - this.httpClient = client; - this.config = config; + + public CxSASTClient(CxScanConfig config, Logger log) throws MalformedURLException { + super(config, log); + + + int interval = config.getProgressInterval() != null ? config.getProgressInterval() : 20; + int retry = config.getConnectionRetries() != null ? config.getConnectionRetries() : 3; + sastWaiter = new Waiter("CxSAST scan", interval, retry) { + @Override + public ResponseQueueScanStatus getStatus(String id) throws IOException { + ResponseQueueScanStatus statusResponse = null; + try { + statusResponse = getSASTScanStatus(id); + } catch (CxHTTPClientException e) { + try { + ResponseSastScanStatus statusResponseTemp = getSASTScanOutOfQueueStatus(id); + statusResponse = statusResponseTemp.convertResponseSastScanStatusToResponseQueueScanStatus(statusResponseTemp); + } catch (MalformedURLException exception) { + throw new MalformedURLException("Failed with next error: " + exception); + } + } + return statusResponse; + } + + @Override + public void printProgress(ResponseQueueScanStatus scanStatus) { + printSASTProgress(scanStatus, getStartTimeSec()); + } + + @Override + public ResponseQueueScanStatus resolveStatus(ResponseQueueScanStatus scanStatus) { + return resolveSASTStatus(scanStatus); + } + }; + } + + @Override + public Results init() { + SASTResults initSastResults = new SASTResults(); + try { + initiate(); + language = httpClient.getLanguageFromAccessToken(); + initSastResults.setSastLanguage(language); + } catch (CxClientException e) { + log.error(e.getMessage()); + setState(State.FAILED); + initSastResults.setException(e); + } + return initSastResults; } //**------ API ------**// //CREATE SAST scan - long createSASTScan(long projectId) throws IOException, CxClientException { - log.info("-----------------------------------Create CxSAST Scan:------------------------------------"); - ScanSettingResponse scanSettingResponse = getScanSetting(projectId); + private void createSASTScan(long projectId) { + boolean dupScanFound = false; + try { + log.info("-----------------------------------Create CxSAST Scan:------------------------------------"); + if (config.isAvoidDuplicateProjectScans() != null && config.isAvoidDuplicateProjectScans() && projectHasQueuedScans(projectId)) { + throw new CxClientException(MSG_AVOID_DUPLICATE_PROJECT_SCANS); + } + if (config.getRemoteType() == null) { //scan is local + scanId = createLocalSASTScan(projectId); + } else { + scanId = createRemoteSourceScan(projectId); + } + if (config.getProjectLevelCustomFields() != null) { + updateProjectCustomFields(); + } + sastResults.setSastLanguage(language); + sastResults.setScanId(scanId); + log.info("SAST scan created successfully: Scan ID is {}", scanId); + sastResults.setSastScanLink(config.getUrl(), scanId, projectId); + } catch (Exception e) { + setState(State.FAILED); + if (!errorToBeSuppressed(e)) { + sastResults.setException(new CxClientException(e)); + } + } + } + + private long createLocalSASTScan(long projectId) throws IOException { + if (isScanWithSettingsSupported()) { + log.info("Uploading the zipped source code."); + PathFilter filter = new PathFilter(config.getSastFolderExclusions(), config.getSastFilterPattern(), log); + byte[] zipFile = CxZipUtils.getZippedSources(config, filter, config.getSourceDir(), log); + ScanWithSettingsResponse response = scanWithSettings(zipFile, projectId, false); + return response.getId(); + } else { + configureScanSettings(projectId); + //prepare sources for scan + PathFilter filter = new PathFilter(config.getSastFolderExclusions(), config.getSastFilterPattern(), log); + byte[] zipFile = CxZipUtils.getZippedSources(config, filter, config.getSourceDir(), log); + uploadZipFile(zipFile, projectId); + + return createScan(projectId); + } + } + + private long createRemoteSourceScan(long projectId) throws IOException { + HttpEntity entity; + excludeProjectSettings(projectId); + RemoteSourceRequest req = new RemoteSourceRequest(config); + RemoteSourceTypes type = req.getType(); + boolean isSSH = false; + String apiVersion = getContentTypeAndApiVersion(config, SAST_CREATE_REMOTE_SOURCE_SCAN); + + switch (type) { + case SVN: + if (req.getPrivateKey() != null && req.getPrivateKey().length > 1) { + isSSH = true; + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.addBinaryBody("privateKey", req.getPrivateKey(), ContentType.APPLICATION_JSON, null) + .addTextBody("absoluteUrl", req.getUri().getAbsoluteUrl()) + .addTextBody("port", String.valueOf(req.getUri().getPort())) + .addTextBody("paths", config.getSourceDir()); //todo add paths to req OR using without + entity = builder.build(); + } else { + entity = new StringEntity(convertToJson(req), ContentType.APPLICATION_JSON); + } + break; + case TFS: + entity = new StringEntity(convertToJson(req), ContentType.APPLICATION_JSON); + break; + case PERFORCE: + if (config.getPerforceMode() != null) { + req.setBrowseMode("Workspace"); + } else { + req.setBrowseMode("Depot"); + } + entity = new StringEntity(convertToJson(req), StandardCharsets.UTF_8); + break; + case SHARED: + entity = new StringEntity(new Gson().toJson(req), StandardCharsets.UTF_8); + break; + case GIT: + if (req.getPrivateKey() == null || req.getPrivateKey().length < 1) { + Map content = new HashMap<>(); + content.put("url", req.getUri().getAbsoluteUrl()); + content.put("branch", config.getRemoteSrcBranch()); + entity = new StringEntity(new JSONObject(content).toString(), StandardCharsets.UTF_8); + } else { + isSSH = true; + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.addTextBody("url", req.getUri().getAbsoluteUrl(), ContentType.APPLICATION_JSON); + builder.addTextBody("branch", config.getRemoteSrcBranch(), ContentType.APPLICATION_JSON); //todo add branch to req OR using without this else?? + builder.addBinaryBody("privateKey", req.getPrivateKey(), ContentType.MULTIPART_FORM_DATA, null); + entity = builder.build(); + } + break; + default: + log.error("todo"); + entity = new StringEntity("", StandardCharsets.UTF_8); + + } + if (isScanWithSettingsSupported()) { + createRemoteSourceRequest(projectId, apiVersion, entity, type.value(), isSSH); + ScanWithSettingsResponse response = scanWithSettings(null, projectId, true); + return response.getId(); + } else { + configureScanSettings(projectId); + createRemoteSourceRequest(projectId, apiVersion, entity, type.value(), isSSH); + return createScan(projectId); + } + } + + /** + * Determine the appropriate content type and API version based on the provided configuration and API name. + *

+ * scanWithSettings api and some other api have got different versions in different sast server because those implement different capabilities. + * The cx-client-common as and when gets enhanced to support those new capabilities and it should also take care of sending required version. + * This logic takes care of search api's that may have different versions of different capabilities accross different sast servers. + * This logic is to identify what should be content type for actual version name and api version name that should be used while calling api + *

+ * If the apiName is equal to SAST_RETENTION_RATE and data retention is enabled in the configuration, + * it sets the apiVersion to "application/json;v=1.1". + *

+ * If the apiName is equal to SCAN_WITH_SETTINGS_URL, it checks if custom fields are defined in the configuration. + * If custom fields exist, it sets the apiVersion to "application/json;v=1.2". + * If not, it checks if a post-scan action ID is defined and sets the apiVersion to "application/json;v=1.2". + * If none of these conditions are met, it sets the apiVersion to the default "application/json;v=1.0". + *

+ * If the current version is between 9.2 and 9.3 (inclusive), it sets the apiVersion to "application/json;v=1.0". + */ + public String getContentTypeAndApiVersion(CxScanConfig config, String apiName) { + CxVersion cxVersion = config.getCxVersion(); + String sastVersion = cxVersion.getVersion(); + String apiVersion = CONTENT_TYPE_APPLICATION_JSON_V1; + if (sastVersion != null && !sastVersion.isEmpty()) { + String[] versionComponents = sastVersion.split("\\."); + if (versionComponents.length >= 2) { + String currentVersion = versionComponents[0] + "." + versionComponents[1]; + float currentVersionFloat = Float.parseFloat(currentVersion); + if (currentVersionFloat >= Float.parseFloat("9.7")) { + if (apiName.equalsIgnoreCase(SAST_SCAN_RESULTS_STATISTICS)) { + apiVersion = CONTENT_TYPE_APPLICATION_XML_V6; + } + } else if (currentVersionFloat >= Float.parseFloat("9.4")) { + if (SAST_RETENTION_RATE.equalsIgnoreCase(apiName) && config.isEnableDataRetention()) { + apiVersion = CONTENT_TYPE_API_VERSION_1_1; + } else if (SCAN_WITH_SETTINGS_URL.equalsIgnoreCase(apiName)) { + String customFields = config.getCustomFields(); + if (customFields != null && !customFields.isEmpty()) { + apiVersion = CONTENT_TYPE_API_VERSION_1_2; + } else if (config.getPostScanActionId() != null) { + apiVersion = CONTENT_TYPE_API_VERSION_1_2; + } else { + apiVersion = CONTENT_TYPE_APPLICATION_JSON_V1; + } + } + else { + apiVersion = CONTENT_TYPE_APPLICATION_JSON_V1; + } + } else if (currentVersionFloat >= 9.2 && currentVersionFloat <= 9.3) { + apiVersion = CONTENT_TYPE_APPLICATION_JSON_V1; + } + } + } + return apiVersion; + } + + private void configureScanSettings(long projectId) throws IOException { + ScanSettingResponse scanSettingResponse = getScanSetting(projectId); ScanSettingRequest scanSettingRequest = new ScanSettingRequest(); - scanSettingRequest.setEngineConfigurationId(scanSettingResponse.getEngineConfiguration().getId());//todo check for null + scanSettingRequest.setEngineConfigurationId(scanSettingResponse.getEngineConfiguration().getId()); scanSettingRequest.setProjectId(projectId); scanSettingRequest.setPresetId(config.getPresetId()); if (config.getEngineConfigurationId() != null) { @@ -95,143 +404,281 @@ long createSASTScan(long projectId) throws IOException, CxClientException { } //Define createSASTScan settings defineScanSetting(scanSettingRequest); + } - //prepare sources for scan - if (config.getZipFile() == null) { - log.info("Zipping sources"); - File zipTempFile = CxZipUtils.zipWorkspaceFolder(config, MAX_ZIP_SIZE_BYTES, log); - //Upload zipped source file - uploadZipFile(zipTempFile, projectId); - deleteTempZipFile(zipTempFile, log); - } else { - uploadZipFile(config.getZipFile(), projectId); + + /* + * Suppress only those conditions for which it is generally acceptable + * to have plugin not error out so that rest of the pipeline can continue. + */ + private boolean errorToBeSuppressed(Exception error) { + + final String additionalMessage = "Build status will be marked successfull as this error is benign. Results from last scan will be displayed, if available."; + boolean suppressed = false; + + //log actual error as it is first. + log.error(error.getMessage()); + + if (error instanceof ConditionTimeoutException && config.getContinueBuild()) { + suppressed = true; } + //Plugins will control if errors handled here will be ignored. + else if (config.isIgnoreBenignErrors()) { - //Start a new createSASTScan - log.info("Uploading zip file"); - CreateScanRequest scanRequest = new CreateScanRequest(projectId, config.getIncremental(), config.getPublic(), config.getForceScan(), config.getScanComment() == null ? "" : config.getScanComment()); - log.info("Sending SAST scan request"); - CxID createScanResponse = createScan(scanRequest); - log.info(String.format("SAST Scan created successfully. Link to project state: " + config.getUrl() + LINK_FORMAT, projectId)); + if (error.getMessage().contains("source folder is empty,") || (sastResults.getException() != null + && sastResults.getException().getMessage().contains("No files to zip"))) { - return createScanResponse.getId(); + suppressed = true; + } else if (error.getMessage().contains("No files to zip")) { + suppressed = true; + } else if (error.getMessage().equalsIgnoreCase(MSG_AVOID_DUPLICATE_PROJECT_SCANS)) { + suppressed = true; + } + } + + if (suppressed) { + log.info(additionalMessage); + try { + sastResults = getLatestScanResults(); + if (super.isIsNewProject() && sastResults.getSastScanLink() == null) { + String message = String + .format("The project %s is a new project. Hence there is no last scan report to be shown.", config.getProjectName()); + log.info(message); + } + } catch (Exception okayToNotHaveResults) { + sastResults = null; + } + + if (sastResults == null) + sastResults = new SASTResults(); + + sastResults.setException(null); + setState(State.SKIPPED); + + } + return suppressed; } + //GET SAST results + reports - public SASTResults waitForSASTResults(long scanId, long projectId) throws InterruptedException, IOException, CxClientException { - SASTResults sastResults; - - log.info("------------------------------------Get CxSAST Results:-----------------------------------"); - //wait for SAST scan to finish - log.info("Waiting for CxSAST scan to finish."); - sastWaiter.waitForTaskToFinish(Long.toString(scanId), config.getSastScanTimeoutInMinutes() * 60, log); - log.info("Retrieving SAST scan results"); - - //retrieve SAST scan results - sastResults = retrieveSASTResults(scanId, projectId); - /* if (config.getEnablePolicyViolations()) { - resolveSASTViolation(sastResults, projectId); - }*/ - SASTUtils.printSASTResultsToConsole(sastResults, config.getEnablePolicyViolations(), log); - - //PDF report - if (config.getGeneratePDFReport()) { - log.info("Generating PDF report"); - byte[] pdfReport = getScanReport(sastResults.getScanId(), ReportType.PDF, CONTENT_TYPE_APPLICATION_PDF_V1); - sastResults.setPDFReport(pdfReport); - if (config.getReportsDir() != null) { - String pdfFileName = writePDFReport(pdfReport, config.getReportsDir(), log); + @Override + public Results waitForScanResults() { + try { + log.info("------------------------------------Get CxSAST Results:-----------------------------------"); + //wait for SAST scan to finish + log.info("Waiting for CxSAST scan to finish."); + try { + + sastWaiter.waitForTaskToFinish(Long.toString(scanId), config.getSastScanTimeoutInMinutes() * 60, log); + log.info("Retrieving SAST scan results"); + //retrieve SAST scan results + sastResults = retrieveSASTResults(scanId, projectId); + } catch (ConditionTimeoutException e) { + + if (!errorToBeSuppressed(e)) { + // throw the exception so that caught by outer catch + throw new Exception(e.getMessage()); + } + } catch (CxClientException | IOException e) { + if (!errorToBeSuppressed(e)) { + // throw the exception so that caught by outer catch + throw new Exception(e.getMessage()); + } + } + if (config.getEnablePolicyViolations()) { + resolveSASTViolation(sastResults, projectId); + } + if (sastResults.getSastScanLink() != null) { + SASTUtils.printSASTResultsToConsole(config, sastResults, config.getEnablePolicyViolations(), log); + } + + if (!config.getReports().isEmpty()) { + for (Map.Entry report : config.getReports().entrySet()) { + if (report != null) { + log.info("Generating " + report.getKey().value() + " report"); + byte[] scanReport = getScanReport(sastResults.getScanId(), report.getKey(), CONTENT_TYPE_APPLICATION_PDF_V1); + writeReport(scanReport, report.getValue(), log); + if (report.getKey().value().equals("PDF")) { + sastResults.setPDFReport(scanReport); + sastResults.setPdfFileName(report.getValue()); + } + } + } + } else if (config.getGeneratePDFReport()) { + log.info("Generating PDF report"); + byte[] pdfReport = getScanReport(sastResults.getScanId(), ReportType.PDF, CONTENT_TYPE_APPLICATION_PDF_V1); + sastResults.setPDFReport(pdfReport); + if (config.getReportsDir() == null) { + config.setReportsDir(new File(System.getProperty("user.dir"))); + } + String now = new SimpleDateFormat("dd_MM_yyyy-HH_mm_ss").format(new Date()); + String pdfFileName = PDF_REPORT_NAME + "_" + now + ".pdf"; + String pdfLink = writePDFReport(pdfReport, config.getReportsDir(), pdfFileName, log, "PDF"); + sastResults.setSastPDFLink(pdfLink); sastResults.setPdfFileName(pdfFileName); } + } catch (Exception e) { + if (!errorToBeSuppressed(e)) + sastResults.setException(new CxClientException(e)); } + return sastResults; } private void resolveSASTViolation(SASTResults sastResults, long projectId) { try { - for (Policy policy : getProjectViolations(httpClient, config.getCxARMUrl(), projectId, SAST.value())) { - sastResults.getSastPolicies().add(policy.getPolicyName()); - sastResults.addAllViolations(policy.getViolations()); - } - }catch (Exception ex) { - log.error("CxARM is not available. Policy violations for SAST cannot be calculated: " + ex.getMessage()); + cxARMWaiter.waitForTaskToFinish(Long.toString(projectId), cxARMTimeoutSec, log); + getProjectViolatedPolicies(httpClient, config.getCxARMUrl(), projectId, SAST.value()) + .forEach(sastResults::addPolicy); + } catch (Exception ex) { + throw new CxClientException("CxARM is not available. Policy violations for SAST cannot be calculated: " + ex.getMessage()); } } - private SASTResults retrieveSASTResults(long scanId, long projectId) throws CxClientException, IOException, InterruptedException { - SASTResults sastResults = new SASTResults(); + private SASTResults retrieveSASTResults(long scanId, long projectId) throws IOException { + + SASTStatisticsResponse statisticsResults = getScanStatistics(scanId); + sastResults.setResults(scanId, statisticsResults, config.getUrl(), projectId); //SAST detailed report - byte[] cxReport = getScanReport(sastResults.getScanId(), ReportType.XML, CONTENT_TYPE_APPLICATION_XML_V1); - CxXMLResults reportObj = convertToXMLResult(cxReport); - sastResults.setScanDetailedReport(reportObj); - sastResults.setRawXMLReport(cxReport); + if (config.getGenerateXmlReport() == null || config.getGenerateXmlReport()) { + byte[] cxReport = getScanReport(sastResults.getScanId(), ReportType.XML, CONTENT_TYPE_APPLICATION_XML_V1); + CxXMLResults reportObj = convertToXMLResult(cxReport); + sastResults.setScanDetailedReport(reportObj, config); + sastResults.setRawXMLReport(cxReport); + } sastResults.setSastResultsReady(true); return sastResults; } - SASTResults getLatestSASTResults(long projectId) throws IOException, CxClientException, InterruptedException { - log.info("---------------------------------Get Last CxSAST Results:--------------------------------"); - List scanList = getLatestSASTStatus(projectId); - for (LastScanResponse s : scanList) { - if (CurrentStatus.FINISHED.value().equals(s.getStatus().getName())) { - return retrieveSASTResults(s.getId(), projectId); + @Override + public SASTResults getLatestScanResults() { + sastResults = new SASTResults(); + sastResults.setSastLanguage(language); + try { + log.info("---------------------------------Get Last CxSAST Results:--------------------------------"); + List scanList = getLatestSASTStatus(projectId); + for (LastScanResponse s : scanList) { + if (CurrentStatus.FINISHED.value().equals(s.getStatus().getName())) { + return retrieveSASTResults(s.getId(), projectId); + } } + } catch (Exception e) { + log.error(e.getMessage()); + sastResults.setException(new CxClientException(e)); } - return new SASTResults(); + return sastResults; } //Cancel SAST Scan - public void cancelSASTScan(long scanId) throws IOException, CxClientException { + public void cancelSASTScan() throws IOException { UpdateScanStatusRequest request = new UpdateScanStatusRequest(CurrentStatus.CANCELED); String json = convertToJson(request); - StringEntity entity = new StringEntity(json); - httpClient.patchRequest(SAST_QUEUE_SCAN_STATUS.replace("{scanId}", Long.toString(scanId)), CONTENT_TYPE_APPLICATION_JSON_V1, entity, 200, "cancel SAST scan"); + StringEntity entity = new StringEntity(json, StandardCharsets.UTF_8); + httpClient.patchRequest(SAST_QUEUE_SCAN_STATUS.replace(SCAN_ID_PATH_PARAM, Long.toString(scanId)), CONTENT_TYPE_APPLICATION_JSON_V1, entity, 200, "cancel SAST scan"); log.info("SAST Scan canceled. (scanId: " + scanId + ")"); } - //**------ Private Methods ------**// + private boolean projectHasQueuedScans(long projectId) throws IOException { + List queuedScans = getQueueScans(projectId); + for (ResponseQueueScanStatus scan : queuedScans) { + if (isStatusToAvoid(scan.getStage().getValue()) && scan.getProject().getId() == projectId) { + return true; + } + } + return false; + } + + private boolean isStatusToAvoid(String status) { + QueueStatus qStatus = QueueStatus.valueOf(status); + + switch (qStatus) { + case New: + case PreScan: + case SourcePullingAndDeployment: + case Queued: + case Scanning: + case PostScan: + return true; + default: + return false; + } + } - private ScanSettingResponse getScanSetting(long projectId) throws IOException, CxClientException { - return httpClient.getRequest(SAST_GET_SCAN_SETTINGS.replace("{projectId}", Long.toString(projectId)), CONTENT_TYPE_APPLICATION_JSON_V1, ScanSettingResponse.class, 200, "Scan setting", false); + public ScanSettingResponse getScanSetting(long projectId) throws IOException { + return httpClient.getRequest(SAST_GET_SCAN_SETTINGS.replace(PROJECT_ID_PATH_PARAM, Long.toString(projectId)), CONTENT_TYPE_APPLICATION_JSON_V1, ScanSettingResponse.class, 200, "Scan setting", false); } - private void defineScanSetting(ScanSettingRequest scanSetting) throws IOException, CxClientException { - StringEntity entity = new StringEntity(convertToJson(scanSetting)); + private void defineScanSetting(ScanSettingRequest scanSetting) throws IOException { + StringEntity entity = new StringEntity(convertToJson(scanSetting), StandardCharsets.UTF_8); httpClient.putRequest(SAST_UPDATE_SCAN_SETTINGS, CONTENT_TYPE_APPLICATION_JSON_V1, entity, CxID.class, 200, "define scan setting"); } - private void uploadZipFile(File zipFile, long projectId) throws CxClientException, IOException { - InputStreamBody streamBody = new InputStreamBody(new FileInputStream(zipFile.getAbsoluteFile()), ContentType.APPLICATION_OCTET_STREAM, "zippedSource"); - MultipartEntityBuilder builder = MultipartEntityBuilder.create(); - builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); - builder.addPart("zippedSource", streamBody); - HttpEntity entity = builder.build(); - httpClient.postRequest(SAST_ZIP_ATTACHMENTS.replace("{projectId}", Long.toString(projectId)), null, entity, null, 204, "upload ZIP file"); + private void excludeProjectSettings(long projectId) throws IOException { + String excludeFoldersPattern = Arrays.stream(config.getSastFolderExclusions().split(",")).map(String::trim).collect(Collectors.joining(",")); + String excludeFilesPattern = Arrays.stream(config.getSastFilterPattern().split(",")).map(String::trim).map(file -> file.replace("!**/", "")).collect(Collectors.joining(",")); + ExcludeSettingsRequest excludeSettingsRequest = new ExcludeSettingsRequest(excludeFoldersPattern, excludeFilesPattern); + StringEntity entity = new StringEntity(convertToJson(excludeSettingsRequest), StandardCharsets.UTF_8); + log.info("Exclude folders pattern: " + excludeFoldersPattern); + log.info("Exclude files pattern: " + excludeFilesPattern); + httpClient.putRequest(String.format(SAST_EXCLUDE_FOLDERS_FILES_PATTERNS, projectId), CONTENT_TYPE_APPLICATION_JSON_V1, entity, null, 200, "exclude project's settings"); } - private CxID createScan(CreateScanRequest request) throws CxClientException, IOException { - StringEntity entity = new StringEntity(convertToJson(request)); - return httpClient.postRequest(SAST_CREATE_SCAN, CONTENT_TYPE_APPLICATION_JSON_V1, entity, CxID.class, 201, "create new SAST Scan"); + private void uploadZipFile(byte[] zipFile, long projectId) throws CxClientException, IOException { + log.info("Uploading zip file"); + + try (InputStream is = new ByteArrayInputStream(zipFile)) { + InputStreamBody streamBody = new InputStreamBody(is, ContentType.APPLICATION_OCTET_STREAM, ZIPPED_SOURCE); + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); + builder.addPart(ZIPPED_SOURCE, streamBody); + String apiVersion = getContentTypeAndApiVersion(config, SAST_ZIP_ATTACHMENTS); + HttpEntity entity = builder.build(); + httpClient.postRequest(SAST_ZIP_ATTACHMENTS.replace(PROJECT_ID_PATH_PARAM, Long.toString(projectId)), null, apiVersion, new BufferedHttpEntity(entity), null, 204, "upload ZIP file"); + } } - private SASTStatisticsResponse getScanStatistics(long scanId) throws CxClientException, IOException { - return httpClient.getRequest(SAST_SCAN_RESULTS_STATISTICS.replace("{scanId}", Long.toString(scanId)), CONTENT_TYPE_APPLICATION_JSON_V1, SASTStatisticsResponse.class, 200, "SAST scan statistics", false); + private long createScan(long projectId) throws IOException { + CreateScanRequest scanRequest = new CreateScanRequest(projectId, config.getIncremental(), config.getPublic(), config.getForceScan(), config.getScanComment() == null ? "" : config.getScanComment()); + + log.info("Sending SAST scan request"); + StringEntity entity = new StringEntity(convertToJson(scanRequest), StandardCharsets.UTF_8); + CxID createScanResponse = httpClient.postRequest(SAST_CREATE_SCAN, CONTENT_TYPE_APPLICATION_JSON_V1, entity, CxID.class, 201, "create new SAST Scan"); + log.info(String.format("SAST Scan created successfully. Link to project state: " + config.getUrl() + LINK_FORMAT + projectId + LINK_FORMAL_SUMMARY)); + + return createScanResponse.getId(); } - private List getLatestSASTStatus(long projectId) throws CxClientException, IOException { - return (List) httpClient.getRequest(SAST_GET_PROJECT_SCANS.replace("{projectId}", Long.toString(projectId)), CONTENT_TYPE_APPLICATION_JSON_V1, LastScanResponse.class, 200, "last SAST scan ID", true); + + private CxID createRemoteSourceRequest(long projectId, String apiVersion, HttpEntity entity, String sourceType, boolean isSSH) throws IOException { + + return httpClient.postRequest(String.format(SAST_CREATE_REMOTE_SOURCE_SCAN, projectId, sourceType, isSSH ? "ssh" : ""), isSSH ? null : CONTENT_TYPE_APPLICATION_JSON_V1, apiVersion, + entity, CxID.class, 204, "create " + sourceType + " remote source scan setting"); + + } + + private SASTStatisticsResponse getScanStatistics(long scanId) throws IOException { + String apiVersion = getContentTypeAndApiVersion(config, SAST_SCAN_RESULTS_STATISTICS); + return httpClient.getRequest(SAST_SCAN_RESULTS_STATISTICS.replace(SCAN_ID_PATH_PARAM, Long.toString(scanId)), apiVersion, SASTStatisticsResponse.class, 200, "SAST scan statistics", false); + } + + public List getLatestSASTStatus(long projectId) throws IOException { + return (List) httpClient.getRequest(SAST_GET_PROJECT_SCANS.replace(PROJECT_ID_PATH_PARAM, Long.toString(projectId)), CONTENT_TYPE_APPLICATION_JSON_V1, LastScanResponse.class, 200, "last SAST scan ID", true); } - private CreateReportResponse createScanReport(CreateReportRequest reportRequest) throws CxClientException, IOException { - StringEntity entity = new StringEntity(convertToJson(reportRequest)); + private List getQueueScans(long projectId) throws IOException { + return (List) httpClient.getRequest(SAST_GET_QUEUED_SCANS.replace(PROJECT_ID_PATH_PARAM, Long.toString(projectId)), CONTENT_TYPE_APPLICATION_JSON_V1, ResponseQueueScanStatus.class, 200, "scans in the queue. (projectId: )" + projectId, true); + } + + private CreateReportResponse createScanReport(CreateReportRequest reportRequest) throws IOException { + StringEntity entity = new StringEntity(convertToJson(reportRequest), StandardCharsets.UTF_8); return httpClient.postRequest(SAST_CREATE_REPORT, CONTENT_TYPE_APPLICATION_JSON_V1, entity, CreateReportResponse.class, 202, "to create " + reportRequest.getReportType() + " scan report"); } - private byte[] getScanReport(long scanId, ReportType reportType, String contentType) throws CxClientException, IOException, InterruptedException { + private byte[] getScanReport(long scanId, ReportType reportType, String contentType) throws IOException { CreateReportRequest reportRequest = new CreateReportRequest(scanId, reportType.name()); CreateReportResponse createReportResponse = createScanReport(reportRequest); int reportId = createReportResponse.getReportId(); @@ -240,13 +687,14 @@ private byte[] getScanReport(long scanId, ReportType reportType, String contentT return getReport(reportId, contentType); } - private byte[] getReport(long reportId, String contentType) throws CxClientException, IOException { + private byte[] getReport(long reportId, String contentType) throws IOException { return httpClient.getRequest(SAST_GET_REPORT.replace("{reportId}", Long.toString(reportId)), contentType, byte[].class, 200, " scan report: " + reportId, false); } //SCAN Waiter - overload methods - private ResponseQueueScanStatus getSASTScanStatus(String scanId) throws CxClientException, IOException { - ResponseQueueScanStatus scanStatus = httpClient.getRequest(SAST_QUEUE_SCAN_STATUS.replace("{scanId}", scanId), CONTENT_TYPE_APPLICATION_JSON_V1, ResponseQueueScanStatus.class, 200, "SAST scan status", false); + public ResponseQueueScanStatus getSASTScanStatus(String scanId) throws IOException { + + ResponseQueueScanStatus scanStatus = httpClient.getRequest(SAST_QUEUE_SCAN_STATUS.replace(SCAN_ID_PATH_PARAM, scanId), CONTENT_TYPE_APPLICATION_JSON_V1, ResponseQueueScanStatus.class, 200, SAST_SCAN, false); String currentStatus = scanStatus.getStage().getValue(); if (CurrentStatus.FAILED.value().equals(currentStatus) || CurrentStatus.CANCELED.value().equals(currentStatus) || @@ -261,55 +709,169 @@ private ResponseQueueScanStatus getSASTScanStatus(String scanId) throws CxClient return scanStatus; } + //Check SAST scan status via sast/scans/{scanId} API + public ResponseSastScanStatus getSASTScanOutOfQueueStatus(String scanId) throws IOException { + ResponseSastScanStatus scanStatus = httpClient.getRequest(SAST_SCAN_STATUS.replace(SCAN_ID_PATH_PARAM, scanId), CONTENT_TYPE_APPLICATION_JSON_V1, ResponseSastScanStatus.class, 200, SAST_SCAN, false); + String currentStatus = scanStatus.getStatus().getName(); + + if (CurrentStatus.FAILED.value().equals(currentStatus) || CurrentStatus.CANCELED.value().equals(currentStatus) || + CurrentStatus.DELETED.value().equals(currentStatus) || CurrentStatus.UNKNOWN.value().equals(currentStatus)) { + scanStatus.setBaseStatus(Status.FAILED); + } else if (CurrentStatus.FINISHED.value().equals(currentStatus)) { + scanStatus.setBaseStatus(Status.SUCCEEDED); + } else { + scanStatus.setBaseStatus(Status.IN_PROGRESS); + } + + return scanStatus; + } + private void printSASTProgress(ResponseQueueScanStatus scanStatus, long startTime) { - long elapsedSec = System.currentTimeMillis() / 1000 - startTime; - long hours = elapsedSec / 3600; - long minutes = elapsedSec % 3600 / 60; - long seconds = elapsedSec % 60; - String hoursStr = (hours < 10) ? ("0" + Long.toString(hours)) : (Long.toString(hours)); - String minutesStr = (minutes < 10) ? ("0" + Long.toString(minutes)) : (Long.toString(minutes)); - String secondsStr = (seconds < 10) ? ("0" + Long.toString(seconds)) : (Long.toString(seconds)); + String timestamp = ShragaUtils.getTimestampSince(startTime); String prefix = (scanStatus.getTotalPercent() < 10) ? " " : ""; - log.info("Waiting for SAST scan results. Elapsed time: " + hoursStr + ":" + minutesStr + ":" + secondsStr + ". " + prefix + + log.info("Waiting for SAST scan results. Elapsed time: " + timestamp + ". " + prefix + scanStatus.getTotalPercent() + "% processed. Status: " + scanStatus.getStage().getValue() + "."); } - private ResponseQueueScanStatus resolveSASTStatus(ResponseQueueScanStatus scanStatus) throws CxClientException { - if (Status.SUCCEEDED == scanStatus.getBaseStatus()) { - log.info("SAST scan finished successfully."); - return scanStatus; + private ResponseQueueScanStatus resolveSASTStatus(ResponseQueueScanStatus scanStatus) { + if (scanStatus != null) { + if (Status.SUCCEEDED == scanStatus.getBaseStatus()) { + log.info("SAST scan finished successfully."); + return scanStatus; + } else { + throw new CxClientException("SAST scan cannot be completed. status [" + scanStatus.getStage().getValue() + "]: " + scanStatus.getStageDetails()); + } } else { - throw new CxClientException("SAST scan cannot be completed. status [" + scanStatus.getStage().getValue() + "]: " + scanStatus.getStageDetails()); + throw new CxClientException("SAST scan cannot be completed."); } } - //Report Waiter - overload methods - private ReportStatus getReportStatus(String reportId) throws CxClientException, IOException { - ReportStatus reportStatus = httpClient.getRequest(SAST_GET_REPORT_STATUS.replace("{reportId}", reportId), CONTENT_TYPE_APPLICATION_JSON_V1, ReportStatus.class, 200, " report status", false); + @Override + public Results initiateScan() { + sastResults = new SASTResults(); + sastResults.setSastLanguage(language); + createSASTScan(projectId); + return sastResults; + } - String currentStatus = reportStatus.getStatus().getValue(); - if (currentStatus.equals(ReportStatusEnum.INPROCESS.value())) { - reportStatus.setBaseStatus(Status.IN_PROGRESS); - } else if (currentStatus.equals(ReportStatusEnum.FAILED.value())) { - reportStatus.setBaseStatus(Status.FAILED); - } else { - reportStatus.setBaseStatus(Status.SUCCEEDED); + private boolean isScanWithSettingsSupported() { + try { + HashMap swaggerResponse = this.httpClient.getRequest(SWAGGER_LOCATION, CONTENT_TYPE_APPLICATION_JSON, HashMap.class, 200, SAST_SCAN, false); + return swaggerResponse.toString().contains("/sast/scanWithSettings"); + } catch (Exception e) { + // Assuming something went wrong but SAST version is greater than 9.x + return true; } + } - return reportStatus; + public void updateProjectCustomFields() { + try { + log.info("Updating Project Custom Fields."); + if (config != null) { + String projectId = String.valueOf(this.projectId); + String apiVersion = getContentTypeAndApiVersion(config, PROJECT_PATH); + String apiVersionCustomField = getContentTypeAndApiVersion(config, CUSTOM_FIELD_PATH); + String projectCustomFieldsString = config.getProjectLevelCustomFields(); + if (projectCustomFieldsString != null && !projectCustomFieldsString.isEmpty()) { + List fetchSASTProjectCustomFields = (List) httpClient.getRequest( + CUSTOM_FIELD_PATH, apiVersionCustomField, ProjectLevelCustomFields.class, 200, SAST_SCAN, true + ); + ArrayList custObj = new ArrayList<>(); + Map projectCustomFieldMap = customFieldMap(projectCustomFieldsString); + Project getProjectRequest = httpClient.getRequest(PROJECT_PATH + projectId, CONTENT_TYPE_APPLICATION_JSON_V2, Project.class, 200, SAST_SCAN, false); + ProjectPutRequest projectPutRequest = new ProjectPutRequest(); + projectPutRequest.setName(getProjectRequest.getName()); + Integer team = Integer.parseInt(getProjectRequest.getTeamId()); + List tempCustomFields = getProjectRequest.getCustomFields(); + Boolean validCustomFields = false; + for (int i = 0; i < fetchSASTProjectCustomFields.size(); i++) { + if (projectCustomFieldMap.containsKey(fetchSASTProjectCustomFields.get(i).getName())) { + validCustomFields = true; + ProjectLevelCustomFields customProjectField = new ProjectLevelCustomFields( + fetchSASTProjectCustomFields.get(i).getId(), + projectCustomFieldMap.get(fetchSASTProjectCustomFields.get(i).getName()), + fetchSASTProjectCustomFields.get(i).getName() + ); + custObj.add(customProjectField); + } + } + if (!validCustomFields) { + log.error("Project level custom fields not configured in SAST"); + } + List additionalCustomFields = new ArrayList<>(); + for (ProjectLevelCustomFields existingCustomField : tempCustomFields) { + String existingCustomFieldName = existingCustomField.getName(); + boolean isIdExists = projectCustomFieldMap.containsKey(existingCustomFieldName); + if (!isIdExists) { + additionalCustomFields.add(existingCustomField); + } + } + custObj.addAll(additionalCustomFields); + projectPutRequest.setOwningTeam(team); + if (!custObj.isEmpty()) { + projectPutRequest.setCustomFields(custObj); + String json = convertToJson(projectPutRequest); + StringEntity entity = new StringEntity(json); + try { + httpClient.putRequest(PROJECT_PATH + projectId, apiVersion, entity, null, 204, "define project level custom field"); + log.info("Project Level-Custom Fields updated successfully."); + } catch (CxHTTPClientException e) { + log.error("Error updating Project Level-Custom Fields: {}", e.getMessage()); + } + } + } + } + } catch (Exception ex) { + throw new CxClientException("Failed to Update Project Level-Custom Fields: " + ex.getMessage()); + } } - private void printReportProgress(ReportStatus reportStatus, long startTime) { - String reportType = reportStatus.getContentType().replace("application/", ""); - log.info("Waiting for server to generate " + reportType + " report. " + (startTime + reportTimeoutSec - (System.currentTimeMillis() / 1000)) + " seconds left to timeout"); + private Map customFieldMap(String projectCustomField) { + Map customFieldMap = new HashMap(); + StringTokenizer tokenizer = new StringTokenizer(projectCustomField, ","); + log.info("Project custom field: {}", projectCustomField); + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + String[] keyValue = token.split(":"); + customFieldMap.put(keyValue[0], keyValue[1]); + } + return customFieldMap; } - private ReportStatus resolveReportStatus(ReportStatus reportStatus) throws CxClientException { - if (Status.SUCCEEDED == reportStatus.getBaseStatus()) { - return reportStatus; + private ScanWithSettingsResponse scanWithSettings(byte[] zipFile, long projectId, boolean isRemote) throws IOException { + log.info("Uploading zip file"); + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); + if (!isRemote) { + try (InputStream is = new ByteArrayInputStream(zipFile)) { + InputStreamBody streamBody = new InputStreamBody(is, ContentType.APPLICATION_OCTET_STREAM, ZIPPED_SOURCE); + builder.addPart(ZIPPED_SOURCE, streamBody); + } + } + builder.addTextBody("projectId", Long.toString(projectId), ContentType.APPLICATION_JSON); + if (config.getIsOverrideProjectSetting()) { + builder.addTextBody("overrideProjectSetting", config.getIsOverrideProjectSetting() + "", ContentType.APPLICATION_JSON); } else { - throw new CxClientException("Generation of scan report [id=" + reportStatus.getBaseId() + "] failed."); + builder.addTextBody("overrideProjectSetting", super.isIsNewProject() ? "true" : "false", ContentType.APPLICATION_JSON); } + builder.addTextBody("isIncremental", config.getIncremental().toString(), ContentType.APPLICATION_JSON); + builder.addTextBody("isPublic", config.getPublic().toString(), ContentType.APPLICATION_JSON); + builder.addTextBody("forceScan", config.getForceScan().toString(), ContentType.APPLICATION_JSON); + builder.addTextBody("presetId", config.getPresetId().toString(), ContentType.APPLICATION_JSON); + builder.addTextBody("comment", config.getScanComment() == null ? "" : config.getScanComment(), ContentType.APPLICATION_JSON); + builder.addTextBody("engineConfigurationId", config.getEngineConfigurationId() != null ? config.getEngineConfigurationId().toString() : ENGINE_CONFIGURATION_ID_DEFAULT, ContentType.APPLICATION_JSON); + + builder.addTextBody("postScanActionId", + config.getPostScanActionId() != null && config.getPostScanActionId() != 0 ? + config.getPostScanActionId().toString() : "", + ContentType.APPLICATION_JSON); + + builder.addTextBody("customFields", config.getCustomFields() != null ? + config.getCustomFields() : "", ContentType.APPLICATION_JSON); + String apiVersion = getContentTypeAndApiVersion(config, SCAN_WITH_SETTINGS_URL); + + HttpEntity entity = builder.build(); + return httpClient.postRequest(SCAN_WITH_SETTINGS_URL, null, apiVersion, new BufferedHttpEntity(entity), ScanWithSettingsResponse.class, 201, "upload ZIP file"); } } diff --git a/src/main/java/com/cx/restclient/CxShragaClient.java b/src/main/java/com/cx/restclient/CxShragaClient.java deleted file mode 100644 index 29eecff5..00000000 --- a/src/main/java/com/cx/restclient/CxShragaClient.java +++ /dev/null @@ -1,299 +0,0 @@ -package com.cx.restclient; - -import com.cx.restclient.common.summary.SummaryUtils; -import com.cx.restclient.configuration.CxScanConfig; -import com.cx.restclient.cxArm.dto.CxArmConfig; -import com.cx.restclient.dto.Team; -import com.cx.restclient.dto.ThresholdResult; -import com.cx.restclient.exception.CxClientException; -import com.cx.restclient.exception.CxHTTPClientException; -import com.cx.restclient.httpClient.CxHttpClient; -import com.cx.restclient.osa.dto.OSAResults; -import com.cx.restclient.sast.dto.*; -import org.apache.http.client.HttpResponseException; -import org.apache.http.entity.StringEntity; -import org.slf4j.Logger; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URLEncoder; -import java.util.List; -import java.util.Properties; - -import static com.cx.restclient.common.CxPARAM.*; -import static com.cx.restclient.common.ShragaUtils.isThresholdExceeded; -import static com.cx.restclient.httpClient.utils.ContentType.CONTENT_TYPE_APPLICATION_JSON_V1; -import static com.cx.restclient.httpClient.utils.HttpClientHelper.convertToJson; -import static com.cx.restclient.sast.utils.SASTParam.*; - -/** - * Created by Galn on 05/02/2018. - */ -//SHRAGA -//System Holistic Rest Api Generic Application -public class CxShragaClient { - - private CxHttpClient httpClient; - private Logger log; - private CxScanConfig config; - private long projectId; - - private CxSASTClient sastClient; - private CxOSAClient osaClient; - private long sastScanId; - private String osaScanId; - private SASTResults sastResults = new SASTResults(); - private OSAResults osaResults = new OSAResults(); - - - public CxShragaClient(CxScanConfig config, Logger log) throws MalformedURLException { - this.config = config; - this.log = log; - this.httpClient = new CxHttpClient( - config.getUrl(), - config.getUsername(), - config.getPassword(), - config.getCxOrigin(), - config.isDisableCertificateValidation(), log); - sastClient = new CxSASTClient(httpClient, log, config); - osaClient = new CxOSAClient(httpClient, log, config); - } - - public CxShragaClient(String serverUrl, String username, String password, String origin, boolean disableCertificateValidation, Logger log) throws MalformedURLException { - this(new CxScanConfig(serverUrl, username, password, origin, disableCertificateValidation), log); - } - - //API Scans methods - public void init() throws CxClientException, IOException { - log.info("Initializing Cx client"); - login(); - if (config.getSastEnabled()) { - resolvePreset(); - } - if (config.getEnablePolicyViolations()){ - resolveCxARMUrl(); - } - resolveTeam(); - resolveProject(); - } - - public long createSASTScan() throws IOException, CxClientException { - sastScanId = sastClient.createSASTScan(projectId); - sastResults.setSastScanLink(config.getUrl(), sastScanId, projectId); - return sastScanId; - } - - public String createOSAScan() throws IOException, CxClientException { - osaScanId = osaClient.createOSAScan(projectId); - osaResults.setOsaProjectSummaryLink(config.getUrl(), projectId); - return osaScanId; - } - - public void cancelSASTScan() throws IOException, CxClientException { - sastClient.cancelSASTScan(sastScanId); - } - - public SASTResults waitForSASTResults() throws InterruptedException, CxClientException, IOException { - sastResults = sastClient.waitForSASTResults(sastScanId, projectId); - return sastResults; - } - - public SASTResults getLatestSASTResults() throws InterruptedException, CxClientException, IOException { - sastResults = sastClient.getLatestSASTResults(projectId); - return sastResults; - } - - public OSAResults waitForOSAResults() throws InterruptedException, CxClientException, IOException { - osaResults = osaClient.getOSAResults(osaScanId, projectId); - return osaResults; - } - - public OSAResults getLatestOSAResults() throws InterruptedException, CxClientException, IOException { - osaResults = osaClient.getLatestOSAResults(projectId); - return osaResults; - } - - public ThresholdResult getThresholdResult() { - StringBuilder res = new StringBuilder(""); - boolean isFail = isThresholdExceeded(config, sastResults, osaResults, res); - return new ThresholdResult(isFail, res.toString()); - } - - public boolean isPolicyViolated(StringBuilder failDescription) { - boolean isPolicyViolated = config.getEnablePolicyViolations() && osaResults.getOsaViolations().size() > 0; - if(isPolicyViolated) { - failDescription.append("Project policy status: violated").append("\n");; - } - return isPolicyViolated; - } - - - private CxArmConfig getCxARMConfig() throws IOException, CxClientException { - return httpClient.getRequest(CX_ARM_URL, CONTENT_TYPE_APPLICATION_JSON_V1, CxArmConfig.class, 200, "CxARM URL", false); - } - - public String generateHTMLSummary() throws Exception { - return SummaryUtils.generateSummary(sastResults, osaResults, config); - } - - public String generateHTMLSummary(SASTResults sastResults, OSAResults osaResults) throws Exception { - return SummaryUtils.generateSummary(sastResults, osaResults, config); - } - - public List getAllProjects() throws IOException, CxClientException { - List projects = null; - try { - projects = (List) httpClient.getRequest(SAST_GET_All_PROJECTS, CONTENT_TYPE_APPLICATION_JSON_V1, Project.class, 200, "all projects", true); - } catch (HttpResponseException ex) { - if (ex.getStatusCode() != 404) { - throw ex; - } - } - return projects; - } - - public void close() { - httpClient.close(); - } - - //HELP config Methods - public void login() throws IOException, CxClientException { - // perform login to server - log.info("Logging into the Checkmarx service."); - httpClient.login(); - } - - public String getTeamIdByName(String teamName) throws CxClientException, IOException { - List allTeams = getTeamList(); - for (Team team : allTeams) { - if (team.getFullName().equalsIgnoreCase(teamName)) { //TODO caseSenesitive - return team.getId(); - } - } - throw new CxClientException("Could not resolve team ID from team name: " + teamName); - } - - public String getTeamNameById(String teamId) throws CxClientException, IOException { - List allTeams = getTeamList(); - for (Team team : allTeams) { - if (teamId.equals(team.getId())) { - return team.getFullName(); - } - } - throw new CxClientException("Could not resolve team name from id: " + teamId); - } - - public int getPresetIdByName(String presetName) throws CxClientException, IOException { - List allPresets = getPresetList(); - for (Preset preset : allPresets) { - if (preset.getName().equalsIgnoreCase(presetName)) { //TODO caseSenesitive- checkkk - return preset.getId(); - } - } - - throw new CxClientException("Could not resolve preset ID from preset name: " + presetName); - } - - public List getTeamList() throws IOException, CxClientException { - return (List) httpClient.getRequest(CXTEAMS, CONTENT_TYPE_APPLICATION_JSON_V1, Team.class, 200, "team list", true); - } - - public Preset getPresetById(int presetId) throws IOException, CxClientException { - return httpClient.getRequest(CXPRESETS + "/" + presetId, CONTENT_TYPE_APPLICATION_JSON_V1, Preset.class, 200, "preset by id", false); - } - - public List getPresetList() throws IOException, CxClientException { - return (List) httpClient.getRequest(CXPRESETS, CONTENT_TYPE_APPLICATION_JSON_V1, Preset.class, 200, "preset list", true); - } - - public List getConfigurationSetList() throws IOException, CxClientException { - return (List) httpClient.getRequest(SAST_ENGINE_CONFIG, CONTENT_TYPE_APPLICATION_JSON_V1, CxNameObj.class, 200, "engine configurations", true); - } - - public void setOsaFSAProperties(Properties fsaConfig) { //For CxMaven plugin - config.setOsaFsaConfig(fsaConfig); - } - - //Private methods - private void resolveTeam() throws CxClientException, IOException { - if (config.getTeamId() == null) { - config.setTeamId(getTeamIdByName(config.getTeamPath())); - } - printTeamPath(); - } - - private void resolveCxARMUrl() { - try { - this.config.setCxARMUrl(getCxARMConfig().getCxARMPolicyURL()); - } catch (Exception ex) { - log.error("CxARM is not available. Policy violations cannot be calculated: " + ex.getMessage()); - } - } - - private void resolvePreset() throws CxClientException, IOException { - if (config.getPresetId() == null) { - config.setPresetId(getPresetIdByName(config.getPresetName())); - } - printPresetName(); - } - - private void printPresetName() { - try { - String presetName = config.getPresetName(); - if (presetName == null) { - presetName = getPresetById(config.getPresetId()).getName(); - } - log.info("preset name: " + presetName); - } catch (Exception e) { - } - } - - private void printTeamPath() { - try { - String teamPath = config.getTeamPath(); - if (teamPath == null) { - teamPath = getTeamNameById(config.getTeamId()); - } - log.info("full team path: " + teamPath); - } catch (Exception e) { - } - } - - private void resolveProject() throws IOException, CxClientException { - List projects = getProjectByName(config.getProjectName(), config.getTeamId()); - if (projects == null || projects.isEmpty()) { // Project is new - if (config.getDenyProject()) { - String errMsg = "Creation of the new project [" + config.getProjectName() + "] is not authorized. " + - "Please use an existing project. \nYou can enable the creation of new projects by disabling" + "" + - " the Deny new Checkmarx projects creation checkbox in the Checkmarx plugin global settings.\n"; - throw new CxClientException(errMsg); - } - //Create newProject - CreateProjectRequest request = new CreateProjectRequest(config.getProjectName(), config.getTeamId(), config.getPublic()); - projectId = createNewProject(request).getId(); - - } else { - projectId = projects.get(0).getId(); - } - } - - private List getProjectByName(String projectName, String teamId) throws IOException, CxClientException { - projectName = URLEncoder.encode(projectName, "UTF-8"); - String projectNamePath = SAST_GET_PROJECT.replace("{name}", projectName).replace("{teamId}", teamId); - List projects = null; - try { - projects = (List) httpClient.getRequest(projectNamePath, CONTENT_TYPE_APPLICATION_JSON_V1, Project.class, 200, "project by name: " + projectName, true); - } catch (CxHTTPClientException ex) { - if (ex.getStatusCode() != 404) { - throw ex; - } - } - return projects; - } - - private Project createNewProject(CreateProjectRequest request) throws CxClientException, IOException { - String json = convertToJson(request); - StringEntity entity = new StringEntity(json); - return httpClient.postRequest(CREATE_PROJECT, CONTENT_TYPE_APPLICATION_JSON_V1, entity, Project.class, 201, "create new project: " + request.getName()); - } -} \ No newline at end of file diff --git a/src/main/java/com/cx/restclient/ast/AstClient.java b/src/main/java/com/cx/restclient/ast/AstClient.java new file mode 100644 index 00000000..b6a5b075 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/AstClient.java @@ -0,0 +1,322 @@ +package com.cx.restclient.ast; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.*; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.entity.StringEntity; +import org.slf4j.Logger; + +import com.cx.restclient.ast.dto.common.ASTConfig; +import com.cx.restclient.ast.dto.common.GitCredentials; +import com.cx.restclient.ast.dto.common.HandlerRef; +import com.cx.restclient.ast.dto.common.ProjectToScan; +import com.cx.restclient.ast.dto.common.RemoteRepositoryInfo; +import com.cx.restclient.ast.dto.common.ScanConfig; +import com.cx.restclient.ast.dto.common.ScanStartHandler; +import com.cx.restclient.ast.dto.common.StartScanRequest; +import com.cx.restclient.ast.dto.sca.AstScaConfig; +import com.cx.restclient.common.UrlUtils; +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.configuration.PropertyFileLoader; +import com.cx.restclient.dto.Results; +import com.cx.restclient.dto.SourceLocationType; +import com.cx.restclient.exception.CxClientException; +import com.cx.restclient.httpClient.CxHttpClient; +import com.cx.restclient.httpClient.utils.ContentType; +import com.cx.restclient.httpClient.utils.HttpClientHelper; +import com.cx.restclient.sast.utils.State; +import com.cx.restclient.sca.dto.CxSCAScanAPIConfig; +import com.cx.restclient.sca.dto.CxSCAScanApiConfigEntry; +import com.cx.restclient.sca.dto.GetUploadUrlRequest; +import com.cx.restclient.sca.dto.ScanAPIConfigEntry; +import com.fasterxml.jackson.databind.JsonNode; + +public abstract class AstClient { + + private static final String LOCATION_HEADER = "Location"; + private static final String CREDENTIAL_TYPE_PASSWORD = "password"; + protected static final String ENCODING = StandardCharsets.UTF_8.name(); + + protected final CxScanConfig config; + protected final Logger log; + + protected CxHttpClient httpClient; + + private State state = State.SUCCESS; + + protected static final PropertyFileLoader properties = PropertyFileLoader.getDefaultInstance(); + public static final String GET_SCAN = properties.get("ast.getScan"); + public static final String CREATE_SCAN = properties.get("ast.createScan"); + public static final String GET_UPLOAD_URL = properties.get("ast.getUploadUrl"); + + public AstClient(CxScanConfig config, Logger log) { + validate(config, log); + this.config = config; + this.log = log; + } + + protected abstract String getScannerDisplayName(); + + protected abstract ScanConfig getScanConfig(); + + protected abstract HandlerRef getBranchToScan(RemoteRepositoryInfo repoInfo); + + protected abstract HttpResponse submitAllSourcesFromLocalDir(String projectId, String zipFilePath) throws IOException; + + protected abstract String getWebReportPath() throws UnsupportedEncodingException; + + protected CxHttpClient createHttpClient(String baseUrl) { + log.debug("Creating HTTP client."); + CxHttpClient client = new CxHttpClient(baseUrl, + config.getCxOrigin(), + config.getCxOriginUrl(), + config.isDisableCertificateValidation(), + false, // AST clients don't support SSO. + null, + config.isScaProxy(), + config.getScaProxyConfig(), + log, + config.getNTLM(), + config.getPluginVersion()); + //initializing Team Path to prevent null pointer in login when called from automation + client.setTeamPathHeader(""); + + return client; + } + + private void validate(CxScanConfig config, Logger log) { + if (config == null && log == null) { + throw new CxClientException("Both scan config and log must be provided."); + } + } + + protected HttpResponse sendStartScanRequest(RemoteRepositoryInfo repoInfo, + SourceLocationType sourceLocation, + String projectId, String scanCustomTags) throws IOException { + log.debug("Constructing the 'start scan' request"); + + ScanStartHandler handler = getScanStartHandler(repoInfo); + Map scanCustomMap = customFiledMap(scanCustomTags); + ProjectToScan project = ProjectToScan.builder() + .id(projectId) + .type(sourceLocation.getApiValue()) + .handler(handler) + .build(); + + List apiScanConfig = Collections.singletonList(getScanConfig()); + + StartScanRequest request = StartScanRequest.builder() + .project(project) + .config(apiScanConfig) + .tags(scanCustomMap) + .build(); + + StringEntity entity = HttpClientHelper.convertToStringEntity(request); + + log.info("Sending the 'start scan' request."); + return httpClient.postRequest(CREATE_SCAN, ContentType.CONTENT_TYPE_APPLICATION_JSON, entity, + HttpResponse.class, HttpStatus.SC_CREATED, "start the scan"); + } + private Map customFiledMap(String scanCustomField){ + Map customFieldMap = new HashMap(); + if(!StringUtils.isEmpty(scanCustomField)) { + StringTokenizer tokenizer = new StringTokenizer(scanCustomField, ","); + log.info("scan custom Tags: {}", scanCustomField); + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + String[] keyValue = token.split(":"); + customFieldMap.put(keyValue[0], keyValue[1]); + } + } + return customFieldMap; + } + protected HttpResponse submitSourcesFromRemoteRepo(ASTConfig config, String projectId,String customTags) throws IOException { + log.info("Using remote repository flow."); + RemoteRepositoryInfo repoInfo = config.getRemoteRepositoryInfo(); + validateRepoInfo(repoInfo); + + URL sanitizedUrl = sanitize(repoInfo.getUrl()); + log.info("Repository URL: {}", sanitizedUrl); + return sendStartScanRequest(repoInfo, SourceLocationType.REMOTE_REPOSITORY, projectId,customTags); + } + + protected void waitForScanToFinish(String scanId) { + + log.info("------------------------------------Get {} Results:-----------------------------------", getScannerDisplayName()); + log.info("Waiting for {} scan to finish", getScannerDisplayName()); + + AstWaiter waiter = new AstWaiter(httpClient, config, getScannerDisplayName(), log); + waiter.waitForScanToFinish(scanId); + log.info("{} scan finished successfully. Retrieving {} scan results.", getScannerDisplayName(), getScannerDisplayName()); + } + + /** + * @param repoInfo may represent an actual git repo or a presigned URL of an uploaded archive. + */ + private ScanStartHandler getScanStartHandler(RemoteRepositoryInfo repoInfo) { + log.debug("Creating the handler object."); + + HandlerRef ref = getBranchToScan(repoInfo); + + // AST-SAST doesn't allow nulls here. + String password = StringUtils.defaultString(repoInfo.getPassword()); + String username = StringUtils.defaultString(repoInfo.getUsername()); + + GitCredentials credentials = GitCredentials.builder() + .type(CREDENTIAL_TYPE_PASSWORD) + .value(password) + .build(); + + URL effectiveRepoUrl = getEffectiveRepoUrl(repoInfo); + + // The ref/username/credentials properties are mandatory even if not specified in repoInfo. + return ScanStartHandler.builder() + .ref(ref) + .username(username) + .credentials(credentials) + .url(effectiveRepoUrl.toString()) + .build(); + } + + protected URL getEffectiveRepoUrl(RemoteRepositoryInfo repoInfo) { + return repoInfo.getUrl(); + } + + protected String getWebReportLink(String baseUrl) { + String result = null; + String warning = null; + try { + if (StringUtils.isNotEmpty(baseUrl)) { + String path = getWebReportPath(); + result = UrlUtils.parseURLToString(baseUrl, path); + } else { + warning = "Web app URL is not specified."; + } + } catch (MalformedURLException e) { + warning = "Invalid web app URL."; + } catch (Exception e) { + warning = "General error."; + } + + Optional.ofNullable(warning) + .ifPresent(warn -> log.warn("Unable to generate web report link. {}", warn)); + + return result; + } + + /** + * Removes the userinfo part of the input URL (if present), so that the URL may be logged safely. + * The URL may contain userinfo when a private repo is scanned. + */ + private static URL sanitize(URL url) throws MalformedURLException { + return new URL(url.getProtocol(), url.getHost(), url.getFile()); + } + + private void validateRepoInfo(RemoteRepositoryInfo repoInfo) { + log.debug("Validating remote repository info."); + if (repoInfo == null) { + String message = String.format( + "%s must be provided in %s configuration when using source location of type %s.", + RemoteRepositoryInfo.class.getName(), + getScannerDisplayName(), + SourceLocationType.REMOTE_REPOSITORY.name()); + + throw new CxClientException(message); + } + } + + protected String extractScanIdFrom(HttpResponse response) { + String result = null; + + log.debug("Extracting scan ID from the '{}' response header.", LOCATION_HEADER); + if (response != null && response.getLastHeader(LOCATION_HEADER) != null) { + // Expecting values like + // /api/scans/1ecffa00-0e42-49b2-8755-388b9f6a9293 + // /07e5b4b0-184a-458e-9d82-7f3da407f940 + String urlPathWithScanId = response.getLastHeader(LOCATION_HEADER).getValue(); + result = FilenameUtils.getName(urlPathWithScanId); + } + + if (StringUtils.isNotEmpty(result)) { + + log.info("Scan started successfully. Scan ID: {}", result); + } else { + throw new CxClientException("Unable to get scan ID."); + } + return result; + } + + protected void handleInitError(Exception e, Results results) { + String message = String.format("Failed to init %s client. %s", getScannerDisplayName(), e.getMessage()); + log.error(message); + setState(State.FAILED); + results.setException(new CxClientException(message, e)); + } + + protected HttpResponse initiateScanForUpload(String projectId, byte[] zipFile, ASTConfig scanConfig,String scanCustomTag) throws IOException { + String uploadedArchiveUrl = getSourcesUploadUrl(scanConfig); + String cleanPath = uploadedArchiveUrl.split("\\?")[0]; + log.info("Uploading to: {}", cleanPath); + uploadArchive(zipFile, uploadedArchiveUrl); + + //delete only if path not specified in the config + //If zipFilePath is specified in config, it means that the user has prepared the zip file themselves. The user obviously doesn't want this file to be deleted. + //If zipFilePath is NOT specified, Common Client will create the zip itself. After uploading the zip, Common Client should clean after itself (delete the zip file that it created). + + RemoteRepositoryInfo uploadedFileInfo = new RemoteRepositoryInfo(); + uploadedFileInfo.setUrl(new URL(uploadedArchiveUrl)); + + return sendStartScanRequest(uploadedFileInfo, SourceLocationType.LOCAL_DIRECTORY, projectId,scanCustomTag); + } + + private String getSourcesUploadUrl(ASTConfig scanConfig) throws IOException { + JsonNode response; + if (scanConfig instanceof AstScaConfig) { + AstScaConfig scaConfig = (AstScaConfig) scanConfig; + boolean includeSources = scaConfig.isIncludeSources(); + CxSCAScanAPIConfig scaApiConfig = CxSCAScanAPIConfig.builder() + .includeSourceCode(includeSources ? "true" : "false").build(); + CxSCAScanApiConfigEntry configentry = CxSCAScanApiConfigEntry.builder().type("sca").value(scaApiConfig) + .build(); + List scanconfigEntry = Collections.singletonList(configentry); + + GetUploadUrlRequest request = GetUploadUrlRequest.builder(). + config(scanconfigEntry). + build(); + + StringEntity entity = HttpClientHelper.convertToStringEntity(request); + response = httpClient.postRequest(GET_UPLOAD_URL, null, entity, JsonNode.class, HttpStatus.SC_OK, + "get upload URL for sources"); + } + else { + response = httpClient.postRequest(GET_UPLOAD_URL, null, null, JsonNode.class, + HttpStatus.SC_OK, "get upload URL for sources"); + } + + if (response == null || response.get("url") == null) { + throw new CxClientException("Unable to get the upload URL."); + } + + return response.get("url").asText(); + } + + protected abstract void uploadArchive(byte[] source, String uploadUrl) throws IOException; + + + + public State getState() { + return state; + } + + public void setState(State state) { + this.state = state; + } +} diff --git a/src/main/java/com/cx/restclient/ast/AstSastClient.java b/src/main/java/com/cx/restclient/ast/AstSastClient.java new file mode 100644 index 00000000..1ed93144 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/AstSastClient.java @@ -0,0 +1,514 @@ +package com.cx.restclient.ast; + +import com.cx.restclient.ast.dto.common.*; +import com.cx.restclient.ast.dto.sast.AstSastConfig; +import com.cx.restclient.ast.dto.sast.AstSastResults; +import com.cx.restclient.ast.dto.sast.SastScanConfigValue; +import com.cx.restclient.ast.dto.sast.report.*; +import com.cx.restclient.common.Scanner; +import com.cx.restclient.common.UrlUtils; +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.dto.*; +import com.cx.restclient.dto.scansummary.Severity; +import com.cx.restclient.exception.CxClientException; +import com.cx.restclient.exception.CxHTTPClientException; +import com.cx.restclient.httpClient.CxHttpClient; +import com.cx.restclient.httpClient.utils.ContentType; +import com.cx.restclient.osa.dto.ClientType; +import com.cx.restclient.sast.utils.State; +import com.cx.restclient.sast.utils.zip.CxZipUtils; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.EnumUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.*; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.message.BasicNameValuePair; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.util.*; +import java.util.stream.Collectors; + +public class AstSastClient extends AstClient implements Scanner { + private static final String ENGINE_TYPE_FOR_API = "sast"; + private static final String REF_TYPE_BRANCH = "branch"; + private static final String SUMMARY_PATH = properties.get("astSast.scanSummary"); + private static final String SCAN_RESULTS_PATH = properties.get("astSast.scanResults"); + private static final String AUTH_PATH = properties.get("astSast.authentication"); + private static final String WEB_PROJECT_PATH = properties.get("astSast.webProject"); + private static final String URL_PARSING_EXCEPTION = "URL parsing exception."; + private static final String DESCRIPTIONS_PATH = properties.get("astSast.descriptionPath"); + + private static final int DEFAULT_PAGE_SIZE = 1000; + private static final int NO_FINDINGS_CODE = 4004; + + private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final String API_VERSION = "*/*; version=0.1"; + private static final String SCAN_ID_PARAM_NAME = "scan-id"; + private static final String OFFSET_PARAM_NAME = "offset"; + private static final String LIMIT_PARAM_NAME = "limit"; + private static final String ID_PARAM_NAME = "ids"; + private static final int URL_MAX_CHAR_SIZE = 1490; + + private String scanId; + + public AstSastClient(CxScanConfig config, Logger log) { + super(config, log); + + AstSastConfig astConfig = this.config.getAstSastConfig(); + validate(astConfig); + + // Make sure we won't get URLs like "http://example.com//api/scans". + String normalizedUrl = StringUtils.stripEnd(astConfig.getApiUrl(), "/"); + + httpClient = createHttpClient(normalizedUrl); + httpClient.addCustomHeader(HttpHeaders.ACCEPT, API_VERSION); + } + + @Override + public Results init() { + log.debug("Initializing {} client.", getScannerDisplayName()); + AstSastResults astResults = new AstSastResults(); + try { + ClientType clientType = getClientType(); + LoginSettings settings = getLoginSettings(clientType); + httpClient.login(settings); + } catch (Exception e) { + super.handleInitError(e, astResults); + } + return astResults; + } + + private LoginSettings getLoginSettings(ClientType clientType) throws MalformedURLException { + String authUrl = UrlUtils.parseURLToString(config.getAstSastConfig().getApiUrl(), AUTH_PATH); + return LoginSettings.builder() + .accessControlBaseUrl(authUrl) + .clientTypeForPasswordAuth(clientType) + .build(); + } + + private ClientType getClientType() { + AstSastConfig astConfig = config.getAstSastConfig(); + return ClientType.builder() + .clientId(astConfig.getClientId()) + .clientSecret(astConfig.getClientSecret()) + .scopes("ast-api") + .grantType("client_credentials") + .build(); + } + + @Override + protected String getScannerDisplayName() { + return ScannerType.AST_SAST.getDisplayName(); + } + + @Override + protected void uploadArchive(byte[] source, String uploadUrl) throws IOException { + log.info("Uploading the zipped data."); + + HttpEntity request = new ByteArrayEntity(source); + String baseAstUri = httpClient.getRootUri(); + httpClient.setRootUri(uploadUrl); + + try { + // Relative path is empty, because we use the whole upload URL as the base URL for the HTTP client. + // Content type is empty, because the server at uploadUrl throws an error if Content-Type is non-empty. + httpClient.putRequest("", "", request, JsonNode.class, HttpStatus.SC_OK, "upload ZIP file"); + } + finally { + httpClient.setRootUri(baseAstUri); + } + } + + @Override + public Results initiateScan() { + log.info("----------------------------------- Initiating {} Scan:------------------------------------", + getScannerDisplayName()); + + AstSastResults astResults = new AstSastResults(); + scanId = null; + + AstSastConfig astConfig = config.getAstSastConfig(); + try { + SourceLocationType locationType = astConfig.getSourceLocationType(); + HttpResponse response; + if (locationType == SourceLocationType.REMOTE_REPOSITORY) { + response = submitSourcesFromRemoteRepo(astConfig, config.getProjectName(),config.getAstScaConfig().getScaScanCustomTags()); + } else { + + response = submitAllSourcesFromLocalDir(config.getProjectName(), astConfig.getZipFilePath()); + } + scanId = extractScanIdFrom(response); + astResults.setScanId(scanId); + } catch (Exception e) { + log.error(e.getMessage()); + setState(State.FAILED); + astResults.setException(new CxClientException("Error creating scan.", e)); + } + return astResults; + } + + protected HttpResponse submitAllSourcesFromLocalDir(String projectId, String zipFilePath) throws IOException { + log.info("Using local directory flow."); + + PathFilter filter = new PathFilter("", "", log); + String sourceDir = config.getSourceDir(); + byte[] zipFile = CxZipUtils.getZippedSources(config, filter, sourceDir, log); + + return initiateScanForUpload(projectId, zipFile, config.getAstSastConfig(),config.getAstScaConfig().getScaScanCustomTags()); + } + + @Override + protected ScanConfig getScanConfig() { + String presetName = config.getAstSastConfig().getPresetName(); + if (StringUtils.isEmpty(presetName)) { + throw new CxClientException("Scan preset must be specified."); + } + + String isIncremental = Boolean.toString(config.getAstSastConfig().isIncremental()); + ScanConfigValue configValue = SastScanConfigValue.builder() + .incremental(isIncremental) + .presetName(presetName) + .build(); + + return ScanConfig.builder() + .type(ENGINE_TYPE_FOR_API) + .value(configValue) + .build(); + } + + @Override + protected HandlerRef getBranchToScan(RemoteRepositoryInfo repoInfo) { + // We need to return this object even if no branch is specified in repoInfo. + return HandlerRef.builder() + .type(REF_TYPE_BRANCH) + .value(repoInfo.getBranch()) + .build(); + } + + @Override + public Results waitForScanResults() { + AstSastResults result; + try { + waitForScanToFinish(scanId); + result = retrieveScanResults(); + } catch (CxClientException e) { + log.error(e.getMessage()); + result = new AstSastResults(); + result.setException(e); + } + return result; + } + + private AstSastResults retrieveScanResults() { + try { + AstSastResults result = new AstSastResults(); + result.setScanId(scanId); + + AstSastSummaryResults scanSummary = getSummary(); + result.setSummary(scanSummary); + + List findings = getFindings(); + result.setFindings(findings); + + String projectLink = getWebReportLink(config.getAstSastConfig().getWebAppUrl()); + result.setWebReportLink(projectLink); + + return result; + } catch (IOException e) { + String message = String.format("Error getting %s scan results.", getScannerDisplayName()); + throw new CxClientException(message, e); + } + } + + @Override + protected String getWebReportPath() throws UnsupportedEncodingException { + return String.format(WEB_PROJECT_PATH, + URLEncoder.encode(config.getProjectName(), ENCODING)); + } + + private AstSastSummaryResults getSummary() { + AstSastSummaryResults result = new AstSastSummaryResults(); + + String summaryUrl = getRelativeSummaryUrl(); + SummaryResponse summaryResponse = getSummaryResponse(summaryUrl); + + SingleScanSummary nativeSummary = getNativeSummary(summaryResponse); + setFindingCountsPerSeverity(nativeSummary.getSeverityCounters(), result); + + result.setStatusCounters(nativeSummary.getStatusCounters()); + result.setTotalCounter(nativeSummary.getTotalCounter()); + + return result; + } + + private List getFindings() throws IOException { + int offset = 0; + int limit = config.getAstSastConfig().getResultsPageSize(); + if (limit <= 0) { + limit = DEFAULT_PAGE_SIZE; + } + + List allFindings = new ArrayList<>(); + while (true) { + String relativeUrl = getRelativeResultsUrl(offset, limit); + ScanResultsResponse response = getScanResultsResponse(relativeUrl); + List findingsFromResponse = response.getResults(); + allFindings.addAll(findingsFromResponse); + offset += findingsFromResponse.size(); + if (offset >= response.getTotalCount()) { + break; + } + } + + log.info(String.format("Total findings: %d", allFindings.size())); + + + try { + populateAdditionalFields(allFindings); + } catch (CxClientException e) { + log.error(e.getMessage()); + } + + return allFindings; + } + + private void populateAdditionalFields(List allFindings) throws IOException { + + final Map allQueryDescriptionMap = new HashMap<>(); + + Set queryIDs = allFindings.stream().map(finding -> finding.getQueryID()).collect(Collectors.toSet()); + + while (queryIDs.size() > 0) { + Set processedQueryIds = new HashSet(); + List queryDescriptionList = processQueryIDs(queryIDs, processedQueryIds); + + allQueryDescriptionMap.putAll( + queryDescriptionList.stream().collect(Collectors.toMap(QueryDescription::getQueryId, queryDescription -> queryDescription))); + + queryIDs.removeAll(processedQueryIds); + } + + log.info(String.format("QueryIds with descriptions size: {} ", allQueryDescriptionMap.size())); + + allFindings.stream().forEach(finding -> { + String queryId = finding.getQueryID(); + QueryDescription query = allQueryDescriptionMap.get(queryId); + finding.setDescription(query.getResultDescription()); + }); + + + } + + private String prepareURL(Set ids, Set processedIds) { + try { + int lengthOtherParams = new URIBuilder().setPath(DESCRIPTIONS_PATH).setParameter(SCAN_ID_PARAM_NAME, scanId) + .build() + .toString().length(); + + URIBuilder uriBuilder = new URIBuilder(); + uriBuilder.setPath(DESCRIPTIONS_PATH); + + int idsAllowedLength = URL_MAX_CHAR_SIZE - lengthOtherParams; + + List nameValues = new LinkedList<>(); + + for (String id : ids) { + idsAllowedLength = idsAllowedLength - ID_PARAM_NAME.length() - 2 - id.length(); + if (idsAllowedLength > 0) { + processedIds.add(id); + nameValues.add(new BasicNameValuePair(ID_PARAM_NAME, id)); + } + } + + uriBuilder.setParameters(nameValues); + String result = uriBuilder.setParameter(SCAN_ID_PARAM_NAME, scanId) + .build() + .toString(); + + + log.debug(String.format("Getting descriptions from %s", result)); + + return result; + } catch (URISyntaxException e) { + throw new CxClientException(URL_PARSING_EXCEPTION, e); + } + } + + private String getRelativeResultsUrl(int offset, int limit) { + try { + String result = new URIBuilder() + .setPath(SCAN_RESULTS_PATH) + .setParameter(SCAN_ID_PARAM_NAME, scanId) + .setParameter(OFFSET_PARAM_NAME, Integer.toString(offset)) + .setParameter(LIMIT_PARAM_NAME, Integer.toString(limit)) + .build() + .toString(); + + if (log.isDebugEnabled()) { + log.debug(String.format("Getting findings from %s", result)); + } + + return result; + } catch (URISyntaxException e) { + throw new CxClientException(URL_PARSING_EXCEPTION, e); + } + } + + private List processQueryIDs(Set ids, Set processedIds) throws IOException { + + String relativeUrl = prepareURL(ids, processedIds); + + List result = (List) httpClient.getRequest(relativeUrl, + ContentType.CONTENT_TYPE_APPLICATION_JSON, + QueryDescription.class, + HttpStatus.SC_OK, + "retrieving queries description", + true); + + return result; + } + + private ScanResultsResponse getScanResultsResponse(String relativeUrl) throws IOException { + return httpClient.getRequest(relativeUrl, + ContentType.CONTENT_TYPE_APPLICATION_JSON, + ScanResultsResponse.class, + HttpStatus.SC_OK, + "retrieving scan results", + false); + } + + private SummaryResponse getSummaryResponse(String relativeUrl) { + SummaryResponse result; + try { + result = httpClient.getRequest(relativeUrl, + ContentType.CONTENT_TYPE_APPLICATION_JSON, + SummaryResponse.class, + HttpStatus.SC_OK, + "retrieving scan summary", + false); + } catch (Exception e) { + result = getEmptySummaryIfApplicable(e); + } + return result; + } + + private SummaryResponse getEmptySummaryIfApplicable(Exception e) { + SummaryResponse result; + if (noFindingsWereDetected(e)) { + result = new SummaryResponse(); + result.getScansSummaries().add(new SingleScanSummary()); + } else { + throw new CxClientException("Error getting scan summary.", e); + } + return result; + } + + /** + * When no findings are detected, AST-SAST API returns the 404 status with a specific + * error code, which is quite awkward. + * Response example: {"code":4004,"message":"can't find all the provided scan ids","data":null} + * + * @return true: scan completed successfully and the result contains no findings (normal flow). + * false: some other error has occurred (error flow). + */ + private boolean noFindingsWereDetected(Exception e) { + boolean result = false; + if (e instanceof CxHTTPClientException) { + CxHTTPClientException httpException = (CxHTTPClientException) e; + if (httpException.getStatusCode() == HttpStatus.SC_NOT_FOUND && + StringUtils.isNotEmpty(httpException.getResponseBody())) { + try { + JsonNode body = objectMapper.readTree(httpException.getResponseBody()); + result = (body.get("code").asInt() == NO_FINDINGS_CODE); + } catch (Exception parsingException) { + log.warn("Error parsing the 'Not found' response.", parsingException); + } + } + } + return result; + } + + + private String getRelativeSummaryUrl() { + try { + String result = new URIBuilder() + .setPath(SUMMARY_PATH) + .setParameter("scan-ids", scanId) + .build() + .toString(); + + if (log.isDebugEnabled()) { + log.debug(String.format("Getting summary from %s", result)); + } + + return result; + } catch (URISyntaxException e) { + throw new CxClientException(URL_PARSING_EXCEPTION, e); + } + } + + private static void setFindingCountsPerSeverity(List nativeCounters, AstSastSummaryResults target) { + if (nativeCounters == null) { + return; + } + + for (SeverityCounter counter : nativeCounters) { + Severity parsedSeverity = EnumUtils.getEnum(Severity.class, counter.getSeverity()); + int value = counter.getCounter(); + if (parsedSeverity != null) { + if (parsedSeverity == Severity.CRITICAL) { + target.setCriticalVulnerabilityCount(value); + } + else if (parsedSeverity == Severity.HIGH) { + target.setHighVulnerabilityCount(value); + } else if (parsedSeverity == Severity.MEDIUM) { + target.setMediumVulnerabilityCount(value); + } else if (parsedSeverity == Severity.LOW) { + target.setLowVulnerabilityCount(value); + } + } + } + } + + private static SingleScanSummary getNativeSummary(SummaryResponse summaryResponse) { + return Optional.ofNullable(summaryResponse).map(SummaryResponse::getScansSummaries) + // We are sending a single scan ID in the request and therefore expect exactly 1 scan summary. + .filter(scanSummaries -> scanSummaries.size() == 1) + .map(scanSummaries -> scanSummaries.get(0)) + .orElseThrow(() -> new CxClientException("Invalid summary response.")); + } + + @Override + public Results getLatestScanResults() { + log.error("Unsupported Operation."); + AstSastResults result = new AstSastResults(); + result.setException(new CxClientException(new UnsupportedOperationException())); + return result; + } + + @Override + public void close() { + Optional.ofNullable(httpClient).ifPresent(CxHttpClient::close); + } + + private void validate(ASTConfig astSastConfig) { + log.debug("Validating config."); + String error = null; + if (astSastConfig == null) { + error = "%s config must be provided."; + } else if (StringUtils.isBlank(astSastConfig.getApiUrl())) { + error = "%s API URL must be provided."; + } + + if (error != null) { + throw new IllegalArgumentException(String.format(error, getScannerDisplayName())); + } + } +} diff --git a/src/main/java/com/cx/restclient/ast/AstScaClient.java b/src/main/java/com/cx/restclient/ast/AstScaClient.java new file mode 100644 index 00000000..fe14b77a --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/AstScaClient.java @@ -0,0 +1,1365 @@ +package com.cx.restclient.ast; +import static com.cx.restclient.sast.utils.SASTParam.MAX_ZIP_SIZE_BYTES; +import static com.cx.restclient.sast.utils.SASTParam.SAST_CREATE_REPORT; +import static com.cx.restclient.sast.utils.SASTParam.SCA_RESOLVER_RESULT_FILE_NAME; +import static com.cx.restclient.sast.utils.SASTParam.TEMP_FILE_NAME_TO_SCA_RESOLVER_RESULTS_ZIP; +import static com.cx.restclient.sast.utils.SASTParam.TEMP_FILE_NAME_TO_ZIP; +import static com.cx.restclient.common.CxPARAM.CX_REPORT_LOCATION; +import static com.cx.restclient.httpClient.utils.ContentType.*; +import static com.cx.restclient.httpClient.utils.HttpClientHelper.convertToJson; + +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileAttribute; +import java.sql.Timestamp; +import java.util.*; +import java.text.SimpleDateFormat; +import java.time.Duration; +import java.text.ParseException; +import java.util.Date; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import com.cx.restclient.ast.dto.sca.*; +import com.cx.restclient.sca.dto.Tags; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.entity.StringEntity; +import org.apache.velocity.runtime.parser.node.SetExecutor; +import org.awaitility.Awaitility; +import org.awaitility.core.ConditionTimeoutException; +import org.json.JSONObject; +import org.slf4j.Logger; + +import com.cx.restclient.ast.dto.common.HandlerRef; +import com.cx.restclient.ast.dto.common.RemoteRepositoryInfo; +import com.cx.restclient.ast.dto.common.ScanConfig; +import com.cx.restclient.ast.dto.common.ScanConfigValue; +import com.cx.restclient.ast.dto.sca.AstScaConfig; +import com.cx.restclient.ast.dto.sca.AstScaResults; +import com.cx.restclient.ast.dto.sca.CreateProjectRequest; +import com.cx.restclient.ast.dto.sca.Project; +import com.cx.restclient.ast.dto.sca.ScaScanConfigValue; +import com.cx.restclient.ast.dto.sca.Team; +import com.cx.restclient.ast.dto.sca.report.AstScaSummaryResults; +import com.cx.restclient.ast.dto.sca.report.Finding; +import com.cx.restclient.ast.dto.sca.report.Package; +import com.cx.restclient.ast.dto.sca.report.PolicyEvaluation; +import com.cx.restclient.common.CxPARAM; +import com.cx.restclient.common.Scanner; +import com.cx.restclient.common.UrlUtils; +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.dto.LoginSettings; +import com.cx.restclient.dto.PathFilter; +import com.cx.restclient.dto.Results; +import com.cx.restclient.dto.ScannerType; +import com.cx.restclient.dto.SourceLocationType; +import com.cx.restclient.exception.CxClientException; +import com.cx.restclient.exception.CxHTTPClientException; +import com.cx.restclient.httpClient.CxHttpClient; +import com.cx.restclient.httpClient.utils.ContentType; +import com.cx.restclient.httpClient.utils.HttpClientHelper; +import com.cx.restclient.osa.dto.ClientType; +import com.cx.restclient.osa.utils.OSAUtils; +import com.cx.restclient.sast.utils.SASTParam; +import com.cx.restclient.sast.utils.SASTUtils; +import com.cx.restclient.sast.utils.State; +import com.cx.restclient.sast.utils.zip.CxZipUtils; +import com.cx.restclient.sast.utils.zip.NewCxZipFile; +import com.cx.restclient.sast.utils.zip.Zipper; + +import com.cx.restclient.sca.dto.CxSCAResolvingConfiguration; +import com.cx.restclient.sca.dto.ScanReportExportIdRequester; +import com.cx.restclient.sca.dto.SbomReportResponse; +import com.cx.restclient.sca.utils.CxSCAFileSystemUtils; +import com.cx.restclient.sca.utils.fingerprints.CxSCAScanFingerprints; +import com.cx.restclient.sca.utils.fingerprints.FingerprintCollector; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; + +/** + * SCA - Software Composition Analysis - is the successor of OSA. + */ +public class AstScaClient extends AstClient implements Scanner { + private static final String RISK_MANAGEMENT_API = properties.get("astSca.riskManagementApi"); + private static final String PROJECTS = RISK_MANAGEMENT_API + properties.get("astSca.projects"); + private static final String PROJECTID = PROJECTS + properties.get("astSca.projectId"); + private static final String SUMMARY_REPORT = RISK_MANAGEMENT_API + properties.get("astSca.summaryReport"); + private static final String FINDINGS = RISK_MANAGEMENT_API + properties.get("astSca.findings"); + private static final String PACKAGES = RISK_MANAGEMENT_API + properties.get("astSca.packages"); + private static final String LATEST_SCAN = RISK_MANAGEMENT_API + properties.get("astSca.latestScan"); + private static final String WEB_REPORT = properties.get("astSca.webReport"); + private static final String RESOLVING_CONFIGURATION_API = properties.get("astSca.resolvingConfigurationApi"); + private static final String REPORTID_API = RISK_MANAGEMENT_API + properties.get("astSca.reportId"); + private static final String POLICY_MANAGEMENT_API = properties.get("astSca.policyManagementApi"); + private static final String POLICY_MANAGEMENT_EVALUATION_API = POLICY_MANAGEMENT_API + properties.get("astSca.policyManagementEvaliation"); + private static final String TEAMBYID = properties.get("astSca.teamById"); + private static final String REPORT_SCA_PACKAGES = "cxSCAPackages"; + private static final String REPORT_SCA_FINDINGS = "cxSCAVulnerabilities"; + private static final String REPORT_SCA_SUMMARY = "cxSCASummary"; + private static final String JSON_EXTENSION = ".json"; + + private static final String ENGINE_TYPE_FOR_API = "sca"; + + private static final String TENANT_HEADER_NAME = "Account-Name"; + + private static final ObjectMapper caseInsensitiveObjectMapper = new ObjectMapper() + // Ignore any fields that can be added to SCA API in the future. + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + // We need this feature to properly deserialize finding severity, + // e.g. "High" (in JSON) -> Severity.HIGH (in Java). + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS); + private final AstScaConfig astScaConfig; + + + private String projectId; + private String scanId; + private String reportId; + private File tempUploadFile; + private final FingerprintCollector fingerprintCollector; + private CxSCAResolvingConfiguration resolvingConfiguration; + private static final String FINGERPRINT_FILE_NAME = ".cxsca.sig"; + private static final String SCA_CONFIG_FOLDER_NAME = ".cxsca.configurations"; + + public AstScaClient(CxScanConfig config, Logger log) { + super(config, log); + + this.astScaConfig = config.getAstScaConfig(); + validate(astScaConfig); + + + httpClient = createHttpClient(astScaConfig.getApiUrl()); + this.resolvingConfiguration = null; + fingerprintCollector = new FingerprintCollector(log); + // Pass tenant name in a custom header. This will allow to get token from on-premise access control server + // and then use this token for SCA authentication in cloud. + httpClient.addCustomHeader(TENANT_HEADER_NAME, config.getAstScaConfig().getTenant()); + } + + @Override + protected String getScannerDisplayName() { + return ScannerType.AST_SCA.getDisplayName(); + } + + @Override + protected ScanConfig getScanConfig() { + + String sastProjectId = config.getAstScaConfig().getSastProjectId(); + String sastServerUrl = config.getAstScaConfig().getSastServerUrl(); + String sastUsername = config.getAstScaConfig().getSastUsername(); + String sastPassword = config.getAstScaConfig().getSastPassword(); + String sastProjectName = config.getAstScaConfig().getSastProjectName(); + + Map envVariables = config.getAstScaConfig().getEnvVariables(); + JSONObject envJsonString = new JSONObject(envVariables); + + ScanConfigValue configValue = ScaScanConfigValue.builder() + .environmentVariables(envJsonString.toString()) + .sastProjectId(sastProjectId) + .sastServerUrl(sastServerUrl) + .sastUsername(sastUsername) + .sastPassword(sastPassword) + .sastProjectName(sastProjectName) + .build(); + + return ScanConfig.builder() + .type(ENGINE_TYPE_FOR_API) + .value(configValue) + .build(); + + } + + @Override + protected HandlerRef getBranchToScan(RemoteRepositoryInfo repoInfo) { + if (StringUtils.isNotEmpty(repoInfo.getBranch())) { + // If we pass the branch to start scan API, the API will return an error: + // "Git references (branch, commit ID, etc.) are not yet supported." + // + // We can't just ignore the branch, because it will lead to confusion. + String message = String.format("Branch specification is not yet supported by %s.", getScannerDisplayName()); + throw new CxClientException(message); + } + return null; + } + + /** + * Transforms the repo URL if credentials are specified in repoInfo. + */ + @Override + protected URL getEffectiveRepoUrl(RemoteRepositoryInfo repoInfo) { + + URL result; + URL initialUrl = repoInfo.getUrl(); + + + // Otherwise we may get something like "https://mytoken:null@github.com". + String username = StringUtils.defaultString(repoInfo.getUsername()); + String password = StringUtils.defaultString(repoInfo.getPassword()); + + try { + if (StringUtils.isNotEmpty(username) || StringUtils.isNotEmpty(password)) { + log.info("Adding credentials as the userinfo part of the URL, because {} only supports this kind of authentication.", + getScannerDisplayName()); + + result = new URIBuilder(initialUrl.toURI()) + .setUserInfo(username, password) + .build() + .toURL(); + } else { + result = repoInfo.getUrl(); + } + } catch (Exception e) { + throw new CxClientException("Error getting effective repo URL."); + } + return result; + } + + @Override + public Results init() { + log.debug("Initializing {} client.", getScannerDisplayName()); + AstScaResults scaResults = new AstScaResults(); + + try { + login(); + } catch (Exception e) { + super.handleInitError(e, scaResults); + } + return scaResults; + } + + public CxSCAResolvingConfiguration getCxSCAResolvingConfigurationForProject(String projectId) throws IOException { + log.info("Resolving configuration for project: {}", projectId); + String path = String.format(RESOLVING_CONFIGURATION_API, URLEncoder.encode(projectId, ENCODING)); + + return httpClient.getRequest(path, + ContentType.CONTENT_TYPE_APPLICATION_JSON, + CxSCAResolvingConfiguration.class, + HttpStatus.SC_OK, + "get CxSCA resolving configuration", + false); + } + + private byte[] getExportIdForReport(String scanId, String contentType) throws IOException { + + try { + ScanReportExportIdRequester scanReportExportIdRequester = new ScanReportExportIdRequester(scanId, contentType) ; + StringEntity entity = new StringEntity(convertToJson(scanReportExportIdRequester), StandardCharsets.UTF_8); + + String jsonResponse = httpClient.postRequest(CxPARAM.SCA_GET_EXPORT_ID, CONTENT_TYPE_APPLICATION_JSON, entity, + String.class, HttpStatus.SC_ACCEPTED, "failed to fetch export id" ); + + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(jsonResponse); + String exportId = root.get("exportId").asText(); + + log.info("Export Id generated for "+ contentType + " :: "+exportId) ; + + // getting Cyclonex Report by export Id + SbomReportResponse sbomReportResponse = getReportByExportId(exportId, contentType); + return HttpClientHelper.getSBOMReport(sbomReportResponse.getFileUrl()); + + }catch(Exception e ) { + log.error("Failed to getExportIdForReport :: ", e); + } + return null; + } + + private SbomReportResponse getReportByExportId(String exportId, String contentType) throws IOException { + + return Awaitility.await() + .atMost(Duration.ofMinutes(2)) + .pollInterval(Duration.ofSeconds(3)) + .pollDelay(Duration.ZERO) + .until(() -> { + try { + SbomReportResponse response = httpClient.getRequest( + CxPARAM.SCA_GET_SBOM_REPORT.replace("{export_id}", exportId), + contentType, + SbomReportResponse.class, + 200, + " failed to fetch scan report for exportId :: " + exportId, + false + ); + + if (response != null && response.getFileUrl() != null && !response.getFileUrl().isEmpty()) { + return response; + } + + } catch (Exception e) { + log.warn("Still waiting for report (exportId={})... {}", exportId, e.getMessage()); + } + + return null; + }, Objects::nonNull); + } + + + /* The getReport Method is called by waitForScanResults in ASTSCAClient for getting the report such as PDF,CSV, + * XML, JSON, CyclonedxJson, CyclonedxXml, SPdxjson. + * It will first fetch the exportId for the report with the help of scanId and contentType and then will fetch the report. + */ + private byte[] getReport(String scanId, String contentType) throws IOException { + contentType = getContentType(contentType); + return getExportIdForReport(scanId, contentType) ; + } + + private String getContentType(String contentType) { + if (contentType == null) return null; + + switch (contentType.toUpperCase()) { + case "PDF": + return "ScanReportPdf"; + case "XML": + return "ScanReportXml"; + case "CSV": + return "ScanReportCsv"; + case "JSON": + return "ScanReportJson"; + default: + return contentType; + } + } + + + /* +// Depricated Method + private byte[] getReport(String scanId, String contentType) throws IOException { + String SCA_GET_REPORT = "/risk-management/risk-reports/{scan_id}/export?format={file_type}"; + + return httpClient.getRequest(SCA_GET_REPORT.replace("{scan_id}", scanId).replace("{file_type}", contentType), + contentType, byte[].class, 200, " scan report: " + reportId, false); + }*/ + + //cli reports + public static void writeReport(byte[] scanReport, String reportName, Logger log) { + try { + File reportFile = new File(reportName); + if (!reportFile.isAbsolute()) { + reportFile = new File(System.getProperty("user.dir") + CX_REPORT_LOCATION + File.separator + reportFile); + } + + if (!reportFile.getParentFile().exists()) { + reportFile.getParentFile().mkdirs(); + } + + FileUtils.writeByteArrayToFile(reportFile, scanReport); + log.info("Report location: " + reportFile.getAbsolutePath()); + } catch (Exception e) { + log.error("Failed to write report: ", e.getMessage()); + } + } + /** + * Waits for SCA scan to finish, then gets scan results. + * + * @throws CxClientException in case of a network error, scan failure or when scan is aborted by timeout. + */ + @Override + public Results waitForScanResults() { + AstScaResults scaResults; + try { + waitForScanToFinish(scanId); + scaResults = tryGetScanResults().orElseThrow(() -> new CxClientException("Unable to get scan results: scan not found.")); + if (config.getScaJsonReport() != null) { + OSAUtils.writeJsonToFile(REPORT_SCA_FINDINGS + JSON_EXTENSION, scaResults.getFindings(), config.getReportsDir(), config.getOsaGenerateJsonReport(), log); + OSAUtils.writeJsonToFile(REPORT_SCA_PACKAGES + JSON_EXTENSION, scaResults.getPackages(), config.getReportsDir(), config.getOsaGenerateJsonReport(), log); + OSAUtils.writeJsonToFile(REPORT_SCA_SUMMARY + JSON_EXTENSION, scaResults.getSummary(), config.getReportsDir(), config.getOsaGenerateJsonReport(), log); + } + + if (config.isGenerateScaReport()) { + String reportFormat = config.getScaReportFormat(); + log.info("Generating SCA report. Report type: " + reportFormat); + byte[] scanReport = getReport(scaResults.getScanId(), reportFormat); + scaResults.setPDFReport(scanReport); + String now = new SimpleDateFormat("dd_MM_yyyy-HH_mm_ss").format(new Date()); + String PDF_REPORT_NAME = "AstScaReport"; + String fileName = ""; + if (reportFormat.equalsIgnoreCase("CSV")) { + reportFormat = "zip"; + } + + fileName = PDF_REPORT_NAME + "_" + now + "." + reportFormat.toLowerCase(); + String pdfLink = SASTUtils.writePDFReport(scanReport, config.getReportsDir(), fileName, log, reportFormat); + if (reportFormat.toLowerCase().equals("pdf")) { + scaResults.setScaPDFLink(pdfLink); + scaResults.setPdfFileName(fileName); + } + + } + return scaResults; + } catch (CxClientException e) { + log.error(e.getMessage()); + scaResults = new AstScaResults(); + scaResults.setException(e); + } + catch(IOException e) { + log.error(e.getMessage()); + } catch(ConditionTimeoutException e) { + log.error(e.getMessage()); + scaResults = new AstScaResults(); + scaResults.setException(new CxClientException(e)); + return scaResults; + } + return new AstScaResults(); + // return scaResults; + } + + @Override + protected void uploadArchive(byte[] source, String uploadUrl) throws IOException { + log.info("Uploading the zipped data."); + CxHttpClient uploader = null; + HttpEntity request = new ByteArrayEntity(source); + + try { + uploader = createHttpClient(uploadUrl); + + // Relative path is empty, because we use the whole upload URL as the base URL for the HTTP client. + // Content type is empty, because the server at uploadUrl throws an error if Content-Type is non-empty. + uploader.putRequest("", "", request, JsonNode.class, HttpStatus.SC_OK, "upload ZIP file"); + }finally { + Optional.ofNullable(uploader).ifPresent(CxHttpClient::close); + } + + } + + @Override + public Results initiateScan() { + log.info("----------------------------------- Initiating {} Scan:------------------------------------", + getScannerDisplayName()); + AstScaResults scaResults = new AstScaResults(); + scanId = null; + projectId = null; + try { + AstScaConfig scaConfig = config.getAstScaConfig(); + SourceLocationType locationType = scaConfig.getSourceLocationType(); + HttpResponse response; + + projectId = resolveRiskManagementProject(); + boolean isManifestAndFingerprintsOnly = !config.getAstScaConfig().isIncludeSources(); + if (isManifestAndFingerprintsOnly) { + this.resolvingConfiguration = getCxSCAResolvingConfigurationForProject(this.projectId); + log.info("Got the following manifest patterns {}", this.resolvingConfiguration.getManifests()); + log.info("Got the following fingerprint patterns {}", this.resolvingConfiguration.getFingerprints()); + } + + if (locationType == SourceLocationType.REMOTE_REPOSITORY) { + response = submitSourcesFromRemoteRepo(scaConfig, projectId,scaConfig.getScaScanCustomTags()); + } else { + if (scaConfig.isIncludeSources()) { + response = submitAllSourcesFromLocalDir(projectId, astScaConfig.getZipFilePath()); + } else if(scaConfig.isEnableScaResolver()) { + response = submitScaResolverEvidenceFile(scaConfig); + }else { + response = submitManifestsAndFingerprintsFromLocalDir(projectId); + } + } + this.scanId = extractScanIdFrom(response); + scaResults.setScanId(scanId); + if(scaConfig.isEnableScaResolver() && tempUploadFile != null){ + log.info("Deleting uploaded file for scan {}", tempUploadFile.getAbsolutePath()); + if(!tempUploadFile.delete()) + { + log.error("Error while deleting uploaded file for scan {}", tempUploadFile.getAbsolutePath()); + } + } + } catch (Exception e) { + log.error("Error occurred while initiating scan.", e); + setState(State.FAILED); + scaResults.setException(new CxClientException("Error creating scan.", e)); + } + return scaResults; + } + + protected HttpResponse submitAllSourcesFromLocalDir(String projectId, String zipFilePath) throws IOException { + log.info("Using local directory flow."); + + PathFilter filter = new PathFilter(config.getOsaFolderExclusions(), config.getOsaFilterPattern(), log); + String sourceDir = config.getEffectiveSourceDirForDependencyScan(); + + Path configFileDestination = copyConfigFileToSourceDir(sourceDir); + + byte[] zipFile = CxZipUtils.getZippedSources(config, filter, sourceDir, log); + + + FileUtils.deleteDirectory(configFileDestination.toFile()); + + return initiateScanForUpload(projectId, zipFile, config.getAstScaConfig(),config.getAstScaConfig().getScaScanCustomTags()); + } + + /** + * This method + * 1) executes sca resolver to generate result json file. + * 2) create ScaResolverResultsxxxx.zip file with sca resolver result json file to be uploaded for scan + * 3) Execute initiateScan method to generate SCA scan. + * @param scaConfig - AST Sca config object + * @return - Returns the response + * @throws IOException + */ + private HttpResponse submitScaResolverEvidenceFile(AstScaConfig scaConfig) throws IOException,CxClientException { + log.info("Executing SCA Resolver flow."); + log.info("Path to Sca Resolver: {}", scaConfig.getPathToScaResolver()); + File zipFile; + String pathToResultJSONFile = ""; + String pathToSASTResultJSONFile = ""; + String pathToResultJSONFileNew= ""; + String pathToSASTResultJSONFileNew= ""; + + String scaResultPathArgName = getScaResultPathArgumentName(scaConfig); + if(!scaResultPathArgName.equals("")) { + try { + pathToResultJSONFile = getScaResolverResultFilePathFromAdditionalParams(scaConfig.getScaResolverAddParameters(), scaResultPathArgName); + } catch (ParseException e) { + throw new CxClientException(e.getMessage()); + } + } + + log.info("SCA resolver result path configured: " + pathToResultJSONFile); + + String timeStamp = getTimestampFolder(); + pathToResultJSONFileNew = createTimestampBasedPath(pathToResultJSONFile, timeStamp, SASTParam.SCA_RESOLVER_RESULT_FILE_NAME); + if (checkSastResultPath(scaConfig)) { + try { + pathToSASTResultJSONFile = getScaResolverResultFilePathFromAdditionalParams(scaConfig.getScaResolverAddParameters(), "--sast-result-path"); + + } catch (ParseException e) { + throw new CxClientException(e.getMessage()); + } + log.info("SAST result path location configured: " + pathToSASTResultJSONFile); + pathToSASTResultJSONFileNew = createTimestampBasedPath(pathToSASTResultJSONFile, timeStamp, SASTParam.SAST_RESOLVER_RESULT_FILE_NAME); + } + log.info("Launching dependency resolution by ScaResolver. ScaResolver logs can be viewed in debug level logs of the pipeline."); + int exitCode = SpawnScaResolver.runScaResolver(scaConfig.getPathToScaResolver(), scaConfig.getScaResolverAddParameters(),pathToResultJSONFileNew,pathToSASTResultJSONFileNew, log); + if (exitCode == 0) { + log.info("Dependency resolution completed."); + String parentDir = pathToResultJSONFileNew.substring(0, pathToResultJSONFileNew.lastIndexOf(File.separator)); + String parentDirSast = ""; + File destPartentSastDir = new File("/"); + if (!StringUtils.isEmpty(pathToSASTResultJSONFileNew)) { + parentDirSast = pathToSASTResultJSONFileNew.substring(0, + pathToSASTResultJSONFileNew.lastIndexOf(File.separator)); + destPartentSastDir = new File(parentDirSast); + } + + String tempDirectory = parentDir + File.separator + "tmp"; + String tempResultFile = tempDirectory + File.separator + SASTParam.SCA_RESOLVER_RESULT_FILE_NAME; + String tempSASTResultFile = tempDirectory + File.separator + SASTParam.SAST_RESOLVER_RESULT_FILE_NAME; + log.debug("Copying ScaResolver result files to temporary location."); + File destTempDir = new File(tempDirectory); + File destParentDir = new File(parentDir); + if (!destTempDir.exists()) { + Files.createDirectory(destTempDir.toPath()); + } + + Files.copy(new File(pathToResultJSONFileNew).toPath(), new File(tempResultFile).toPath(), + StandardCopyOption.REPLACE_EXISTING); + if(!StringUtils.isEmpty(pathToSASTResultJSONFileNew)) + Files.copy(new File(pathToSASTResultJSONFileNew).toPath(), new File(tempSASTResultFile).toPath(), StandardCopyOption.REPLACE_EXISTING); + + log.info("Completed File copy to "+tempDirectory); + zipFile = zipEvidenceFile(destTempDir); + + if (!pathToResultJSONFileNew.equals(pathToResultJSONFile)) { + log.info("Deleting directory of result file {}", destParentDir.getAbsolutePath()); + FileUtils.deleteDirectory(destParentDir); + log.info("Deleted directory of result file " + destParentDir.getAbsolutePath()); + } else { + + log.info("Deleting temporary uploaded file for scan {}", destTempDir.getAbsolutePath()); + FileUtils.deleteDirectory(destTempDir); + log.info("Deleted temp directory " + destTempDir.getAbsolutePath()); + } + if (!StringUtils.isEmpty(pathToSASTResultJSONFileNew) && !pathToSASTResultJSONFileNew.equals(pathToSASTResultJSONFile)) { + + log.info("Deleting directory of result file {}", destPartentSastDir.getAbsolutePath()); + FileUtils.deleteDirectory(destPartentSastDir); + log.info("Deleted directory of result file " + destPartentSastDir.getAbsolutePath()); + } + + }else{ + throw new CxClientException("Error while running sca resolver executable. Exit code: "+exitCode); + } + return initiateScanForUpload(projectId, FileUtils.readFileToByteArray(zipFile), config.getAstScaConfig(),config.getAstScaConfig().getScaScanCustomTags()); + } + + public boolean checkSastResultPath(AstScaConfig scaConfig) { + if (scaConfig.getScaResolverAddParameters().contains("--sast-result-path")) { + return true; + } + return false; + } + private String createTimestampBasedPath(String inputResultFilePath, String timeStamp, + String targetFileName) { + if(inputResultFilePath.isEmpty()) + return inputResultFilePath; + + String lastPathComponent = ""; + if (!inputResultFilePath.endsWith(File.separator)) { + lastPathComponent = inputResultFilePath.substring(inputResultFilePath.lastIndexOf(File.separator)+1, inputResultFilePath.length()); + } + if (inputResultFilePath.endsWith(File.separator) || lastPathComponent.indexOf(".") == -1) { + //if so, it's a directory + if(!inputResultFilePath.endsWith(File.separator)) + inputResultFilePath = inputResultFilePath + File.separator ; + inputResultFilePath = inputResultFilePath + timeStamp + File.separator + targetFileName; + }else { + //Honor user's choice to create file at given absolute path + return inputResultFilePath; + } + return inputResultFilePath; + } + private String getTimestampFolder() { + SimpleDateFormat sd = new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss"); + Date date = new Date(); + Timestamp tt = new Timestamp(System.currentTimeMillis()); + + + String ss =(sd.format(tt)); + String res = ss.replace(".", ""); + return res; + } + + private String getScaResultPathArgumentName(AstScaConfig scaConfig) { + String scaResolverResultPathArgName = ""; + + + if (scaConfig.getScaResolverAddParameters().contains("--resolver-result-path")) { + scaResolverResultPathArgName = "--resolver-result-path"; + }else if (scaConfig.getScaResolverAddParameters().contains("-r")) { + scaResolverResultPathArgName = "-r"; + } + return scaResolverResultPathArgName; + } + + /** + * This method returns SCA Resolver execution result file path. SCA Resolver additional + * parameter has result file location. Appending result directory path with file name + * .cxsca-results.json + * @param scaResolverAddParams - SCA resolver additional parameters + * @return - SCA resolver execution result file path. + */ + public String getScaResolverResultFilePathFromAdditionalParams(String scaResolverAddParams,String arg)throws ParseException { + String[] argument; + String resolverResultPath = ""; + argument = scaResolverAddParams.split(" "); + for (int i = 0; i < argument.length; i++) { + if (arg.equals(argument[i])) { + if (argument.length - 1 == i) { + resolverResultPath = argument[i]; + } + else { + resolverResultPath = argument[i + 1] ; + } + break; + } + } + return resolverResultPath; + } + + private HttpResponse submitManifestsAndFingerprintsFromLocalDir(String projectId) throws IOException { + log.info("Using manifest only and fingerprint flow"); + String sourceDir = config.getEffectiveSourceDirForDependencyScan(); + Path configFileDestination = copyConfigFileToSourceDir(sourceDir); + String additinalFilters = getAdditionalManifestFilters(); + String finalFilters = additinalFilters + getManifestsIncludePattern(); + PathFilter userFilter = new PathFilter(config.getOsaFolderExclusions(), config.getOsaFilterPattern(), log); + if (ArrayUtils.isNotEmpty(userFilter.getIncludes()) && !ArrayUtils.contains(userFilter.getIncludes(), "**")) { + userFilter.addToIncludes("**"); + } + Set scannedFileSet = new HashSet<>(Arrays.asList(CxSCAFileSystemUtils.scanAndGetIncludedFiles(sourceDir, userFilter))); + + PathFilter manifestIncludeFilter = new PathFilter(null,finalFilters , log); + if (manifestIncludeFilter.getIncludes().length == 0) { + throw new CxClientException(String.format("Using manifest only mode requires include filter. Resolving config does not have include patterns defined: %s", getManifestsIncludePattern())); + } + + List filesToZip = + Arrays.stream(CxSCAFileSystemUtils.scanAndGetIncludedFiles(sourceDir, manifestIncludeFilter)) + .filter(scannedFileSet::contains). + collect(Collectors.toList()); + + List filesToFingerprint = + Arrays.stream(CxSCAFileSystemUtils.scanAndGetIncludedFiles(sourceDir, + new PathFilter(null, getFingerprintsIncludePattern(), log))) + .filter(scannedFileSet::contains). + collect(Collectors.toList()); + + + CxSCAScanFingerprints fingerprints = fingerprintCollector.collectFingerprints(sourceDir, filesToFingerprint); + + File zipFile = zipDirectoryAndFingerprints(sourceDir, filesToZip, fingerprints); + + optionallyWriteFingerprintsToFile(fingerprints); + + + FileUtils.deleteDirectory(configFileDestination.toFile()); + + return initiateScanForUpload(projectId, FileUtils.readFileToByteArray(zipFile), astScaConfig,config.getAstScaConfig().getScaScanCustomTags()); + } + + /** + * This file create zip file to + * @param filePath - SCA Resolver evidence/result file path + * @return - Zipped file + * @throws IOException + */ + private File zipEvidenceFile(File filePath) throws IOException { + + tempUploadFile = File.createTempFile(TEMP_FILE_NAME_TO_SCA_RESOLVER_RESULTS_ZIP, ".zip"); + String sourceDir = filePath.getAbsolutePath(); + + log.info("Collecting files to zip archive: {}", tempUploadFile.getAbsolutePath()); + + long maxZipSizeBytes = config.getMaxZipSize() != null ? config.getMaxZipSize() * 1024 * 1024 : MAX_ZIP_SIZE_BYTES; + + List paths = Arrays.asList(filePath.list()); + try (NewCxZipFile zipper = new NewCxZipFile(tempUploadFile, maxZipSizeBytes, log)) { + zipper.addMultipleFilesToArchive(new File(sourceDir), paths); + log.info("Added {} files to zip.", zipper.getFileCount()); + log.info("The sources were zipped to {}", tempUploadFile.getAbsolutePath()); + return tempUploadFile; + } catch (Zipper.MaxZipSizeReached e) { + throw handleFileDeletion(filePath, new IOException("Reached maximum upload size limit of " + FileUtils.byteCountToDisplaySize(maxZipSizeBytes))); + } catch (IOException ioException) { + throw handleFileDeletion(filePath, ioException); + } + } + + /** + * + * This method gets the additional config file(from different package manager) manifest filters + * e.g. returns "settings.xml,npmrc"/" + **/ + + + + private String getAdditionalManifestFilters() { + List configFilePaths = config.getAstScaConfig().getConfigFilePaths(); + String additionalFilters = ""; + if (configFilePaths != null) { + for (String configFileString : configFilePaths) { + if (StringUtils.isNotEmpty(configFileString)) { + if (configFileString.lastIndexOf("\\") != -1) + configFileString = configFileString.substring(configFileString.lastIndexOf("\\") + 1); + additionalFilters = additionalFilters.concat("**/" + configFileString + ","); + } + } + } + return additionalFilters; + } + + private Path copyConfigFileToSourceDir(String sourceDir) throws IOException { + + Path configFileDestination = Paths.get(""); + log.info("Source Directory : {}", sourceDir); + List configFilePaths = config.getAstScaConfig().getConfigFilePaths(); + + if(configFilePaths != null) { + for(String configFileString : configFilePaths) { + + if (StringUtils.isNotEmpty(configFileString)) { + String fileSystemSeparator = FileSystems.getDefault().getSeparator(); + Path configFilePath = CxSCAFileSystemUtils.checkIfFileExists(sourceDir, configFileString, fileSystemSeparator, log); + + if (configFilePath != null) { + configFileDestination = Paths.get(sourceDir, fileSystemSeparator, SCA_CONFIG_FOLDER_NAME); + + if (Files.notExists(configFileDestination)) { + Path destDir = Files.createDirectory(configFileDestination); + Files.copy(configFilePath, destDir.resolve(configFilePath.getFileName()), StandardCopyOption.REPLACE_EXISTING); + + } else { + Path r = configFileDestination.resolve(configFilePath.getFileName()); + Files.copy(configFilePath,r , StandardCopyOption.REPLACE_EXISTING); + + } + log.info("Config file ({}) copied to directory: {}", configFilePath, configFileDestination); + } + } + } + } + return configFileDestination; + } + + + + private File zipDirectoryAndFingerprints(String sourceDir, List paths, CxSCAScanFingerprints fingerprints) throws IOException { + File result = config.getZipFile(); + if (result != null) { + return result; + + } + File tempFile = getZipFile(); + log.debug("Collecting files to zip archive: {}", tempFile.getAbsolutePath()); + long maxZipSizeBytes = config.getMaxZipSize() != null ? config.getMaxZipSize() * 1024 * 1024 : MAX_ZIP_SIZE_BYTES; + + + try (NewCxZipFile zipper = new NewCxZipFile(tempFile, maxZipSizeBytes, log)) { + zipper.addMultipleFilesToArchive(new File(sourceDir), paths); + if (zipper.getFileCount() == 0 && fingerprints.getFingerprints().isEmpty()) { + throw handleFileDeletion(tempFile); + } + if (!fingerprints.getFingerprints().isEmpty()) { + zipper.zipContentAsFile(FINGERPRINT_FILE_NAME, FingerprintCollector.getFingerprintsAsJsonString(fingerprints).getBytes()); + } else { + log.debug("No supported fingerprints found to zip"); + } + + log.debug("The sources were zipped to {}", tempFile.getAbsolutePath()); + return tempFile; + } catch (Zipper.MaxZipSizeReached e) { + throw handleFileDeletion(tempFile, new IOException("Reached maximum upload size limit of " + FileUtils.byteCountToDisplaySize(maxZipSizeBytes))); + } catch (IOException ioException) { + throw handleFileDeletion(tempFile, ioException); + } + } + + private CxClientException handleFileDeletion(File file, IOException ioException) { + try { + Files.delete(file.toPath()); + } catch (IOException e) { + return new CxClientException(e); + } + + return new CxClientException(ioException); + + } + + private CxClientException handleFileDeletion(File file) { + try { + Files.delete(file.toPath()); + } catch (IOException e) { + return new CxClientException(e); + } + + return new CxClientException("No files found to zip and no supported fingerprints found"); + } + + private String getFingerprintsIncludePattern() { + if (StringUtils.isNotEmpty(astScaConfig.getFingerprintsIncludePattern())) { + return astScaConfig.getFingerprintsIncludePattern(); + } + + return resolvingConfiguration.getFingerprintsIncludePattern(); + } + + private String getManifestsIncludePattern() { + if (StringUtils.isNotEmpty(astScaConfig.getManifestsIncludePattern())) { + return astScaConfig.getManifestsIncludePattern(); + } + + return resolvingConfiguration.getManifestsIncludePattern(); + } + + private File getZipFile() throws IOException { + if (StringUtils.isNotEmpty(astScaConfig.getZipFilePath())) { + return new File(astScaConfig.getZipFilePath()); + } + return File.createTempFile(TEMP_FILE_NAME_TO_ZIP, ".bin"); + } + + private void optionallyWriteFingerprintsToFile(CxSCAScanFingerprints fingerprints) { + if (StringUtils.isNotEmpty(astScaConfig.getFingerprintFilePath())) { + try { + fingerprintCollector.writeScanFingerprintsFile(fingerprints, astScaConfig.getFingerprintFilePath()); + } catch (IOException ioException) { + log.error(String.format("Failed writing fingerprint file to %s", astScaConfig.getFingerprintFilePath()), ioException); + } + } + } + + /** + * Gets latest scan results using {@link CxScanConfig#getProjectName()} for the current config. + * + * @return results of the latest successful scan for a project, if present; null - otherwise. + */ + @Override + public Results getLatestScanResults() { + AstScaResults result = new AstScaResults(); + try { + log.info("Getting latest scan results."); + projectId = getRiskManagementProjectId(config.getProjectName()); + scanId = getLatestScanId(projectId); + result = tryGetScanResults().orElse(null); + } catch (Exception e) { + log.error(e.getMessage()); + result.setException(new CxClientException("Error getting latest scan results.", e)); + } + return result; + } + + private Optional tryGetScanResults() { + AstScaResults result = null; + if (StringUtils.isNotEmpty(scanId)) { + result = getScanResults(); + } else { + log.info("Unable to get scan results"); + } + return Optional.ofNullable(result); + } + + private String getLatestScanId(String projectId) throws IOException { + String result = null; + if (StringUtils.isNotEmpty(projectId)) { + log.debug("Getting latest scan ID for project ID: {}", projectId); + String path = String.format(LATEST_SCAN, URLEncoder.encode(projectId, ENCODING)); + JsonNode response = httpClient.getRequest(path, + ContentType.CONTENT_TYPE_APPLICATION_JSON, + ArrayNode.class, + HttpStatus.SC_OK, + "scan ID by project ID", + false); + + result = Optional.ofNullable(response) + // 'riskReportId' is in fact scanId, but the name is kept for backward compatibility. + .map(resp -> resp.at("/0/riskReportId").textValue()) + .orElse(null); + } + String message = (result == null ? "Scan not found" : String.format("Scan ID: %s", result)); + log.info(message); + return result; + } + + + private void printWebReportLink(AstScaResults scaResult) { + if (!StringUtils.isEmpty(scaResult.getWebReportLink())) { + log.info("{} scan results location: {}", getScannerDisplayName(), scaResult.getWebReportLink()); + } + } + + void testConnection() throws IOException { + // The calls below allow to check both access control and API connectivity. + login(); + getRiskManagementProjects(); + } + + public void login() throws IOException { + log.info("Logging into {}", getScannerDisplayName()); + AstScaConfig scaConfig = config.getAstScaConfig(); + + String acUrl = scaConfig.getAccessControlUrl(); + LoginSettings settings = LoginSettings.builder() + .accessControlBaseUrl(UrlUtils.parseURLToString(acUrl, CxPARAM.AUTHENTICATION)) + .username(scaConfig.getUsername()) + .password(scaConfig.getPassword()) + .tenant(scaConfig.getTenant()) + .build(); + + ClientTypeResolver resolver = new ClientTypeResolver(config); + ClientType clientType = resolver.determineClientType(acUrl); + settings.setClientTypeForPasswordAuth(clientType); + + httpClient.login(settings); + } + + public void close() { + if (httpClient != null) { + httpClient.close(); + } + } + + /** + * The following config properties are used: + * astScaConfig + * proxyConfig + * cxOrigin + * disableCertificateValidation + */ + public void testScaConnection() { + try { + testConnection(); + } catch (IOException e) { + throw new CxClientException(e); + } + } + + private String resolveRiskManagementProject() throws IOException { + String projectName = config.getProjectName(); + String assignedTeam = config.getAstScaConfig().getTeamPath(); + String assignedTeamId = config.getAstScaConfig().getTeamId(); + String projectCustomTag = config.getAstScaConfig().getScaProjectCustomTags(); + if (!StringUtils.isEmpty(assignedTeamId)) { + assignedTeam = getTeamById(assignedTeamId); + + } else if(StringUtils.isEmpty(assignedTeam)){ + + assignedTeam = config.getTeamPath(); + } + + log.info("Getting project by name: '{}'", projectName); + String resolvedProjectId = getRiskManagementProjectId(projectName); + if (resolvedProjectId == null) { + log.info("Project not found, creating a new one."); + resolvedProjectId = createRiskManagementProject(projectName, assignedTeam,projectCustomTag); + log.info("Created a project with ID {}", resolvedProjectId); + } else { + log.info("Project already exists with ID {}", resolvedProjectId); + UpdateRiskManagementProject(resolvedProjectId,projectCustomTag,assignedTeam); + } + return resolvedProjectId; + } + + private String getRiskManagementProjectId(String projectName) throws IOException { + log.info("Getting project ID by name: '{}'", projectName); + + if (StringUtils.isEmpty(projectName)) { + throw new CxClientException("Non-empty project name must be provided."); + } + + Project project = sendGetProjectRequest(projectName); + + String result = Optional.ofNullable(project) + .map(Project::getId) + .orElse(null); + String message = (result == null ? "Project not found" : String.format("Project ID: %s", result)); + log.info(message); + + return result; + } + + private String getTeamById(String teamId) throws IOException { + log.info("Getting Team name by ID : '{}'", teamId); + + if (StringUtils.isEmpty(teamId)) { + throw new CxClientException("Team Id provided is empty."); + } + + Team team = sendGetTeamById(teamId); + + String result = Optional.ofNullable(team) + .map(Team::getFullName) + .orElse(null); + + String message = (result == null ? "Team not found" : String.format("Team name: %s", result)); + log.info(message); + + return result; + } + + private Project sendGetProjectRequest(String projectName) throws IOException { + Project result; + try { + String getProjectByName = String.format("%s?name=%s", PROJECTS, URLEncoder.encode(projectName, ENCODING)); + result = httpClient.getRequest(getProjectByName, + ContentType.CONTENT_TYPE_APPLICATION_JSON, + Project.class, + HttpStatus.SC_OK, + "CxSCA project ID by name", + false); + } catch (CxHTTPClientException e) { + if (e.getStatusCode() == HttpStatus.SC_NOT_FOUND) { + result = null; + } else { + throw e; + } + } + return result; + } + + private Team sendGetTeamById(String teamId) throws IOException { + Team result; + try { + String teamNameAPI = String.format("%s/%s", TEAMBYID, teamId); + result = httpClient.getRequest(this.astScaConfig.getAccessControlUrl()+"/",teamNameAPI,ContentType.CONTENT_TYPE_APPLICATION_JSON, + ContentType.CONTENT_TYPE_APPLICATION_JSON, + Team.class, + HttpStatus.SC_OK, + "CxSCA team ID by name", + false); + } catch (CxHTTPClientException e) { + if (e.getStatusCode() == HttpStatus.SC_NOT_FOUND) { + result = null; + } else { + throw e; + } + } + return result; + } + + private void getRiskManagementProjects() throws IOException { + httpClient.getRequest(PROJECTS, + ContentType.CONTENT_TYPE_APPLICATION_JSON, + Project.class, + HttpStatus.SC_OK, + "CxSCA projects", + true); + } + + private String createRiskManagementProject(String name, String assignedTeam, String projectCustomTag) throws IOException { + CreateProjectRequest request = new CreateProjectRequest(); + request.setName(name); + if(!StringUtils.isEmpty(assignedTeam)) { + request.addAssignedTeams(assignedTeam); + log.info("Team name: {}", assignedTeam); + } + + log.info("Project level custom tag name: {}",projectCustomTag); + if(!StringUtils.isEmpty(projectCustomTag)) { + Map tagMaps = customFieldMap(projectCustomTag); + log.debug("Project level custom tags: {}",tagMaps); + request.setTags(tagMaps); + } + + StringEntity entity = HttpClientHelper.convertToStringEntity(request); + Project newProject = httpClient.postRequest(PROJECTS, + ContentType.CONTENT_TYPE_APPLICATION_JSON, + entity, + Project.class, + HttpStatus.SC_CREATED, + "create a project"); + return newProject.getId(); + } + + private void UpdateRiskManagementProject(String projectId, String customTags, String assignedTeam) throws IOException { + Project existingProject = httpClient.getRequest(PROJECTID.replace("id",projectId),ContentType.CONTENT_TYPE_APPLICATION_JSON,Project.class, + HttpStatus.SC_OK,"got project details",false); + + UpdateProjectRequest request = new UpdateProjectRequest(); + request.setName(existingProject.getName()); + if (!StringUtils.isEmpty(assignedTeam)) { + request.addAssignedTeams(assignedTeam); + log.info("Team name: {}", assignedTeam); + } + + log.info("Project level custom tag name: {}",customTags); + if(existingProject.getTags()!=null){ + Map tagMaps = (Map) existingProject.getTags(); + if(!StringUtils.isEmpty(customTags)) { + StringTokenizer tokenizer = new StringTokenizer(customTags, ","); + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + String[] keyValue = token.split(":"); + tagMaps.put(keyValue[0], keyValue[1]); + } + } + request.setTags(tagMaps); + }else{ + if(!StringUtils.isEmpty(customTags)) { + Map tagMaps = customFieldMap(customTags); + + request.setTags(tagMaps); + } + } + StringEntity entity = HttpClientHelper.convertToStringEntity(request); + httpClient.putRequest(PROJECTID.replace("id",projectId),ContentType.CONTENT_TYPE_APPLICATION_JSON,entity,Project.class, + HttpStatus.SC_NO_CONTENT,"Updated project successfully"); + } + + + private Map customFieldMap(String projectCustomField){ + Map customFieldMap = new LinkedHashMap(); + if(!StringUtils.isEmpty(projectCustomField)){ + StringTokenizer tokenizer = new StringTokenizer(projectCustomField, ","); + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + String[] keyValue = token.split(":"); + customFieldMap.put(keyValue[0], keyValue[1]); + } + } + return customFieldMap; + + } + private AstScaResults getScanResults() { + AstScaResults result; + log.debug("Getting results for scan ID {}", scanId); + try { + result = new AstScaResults(); + result.setScanId(this.scanId); + + reportId = getReportId(scanId); + result.setReportId(reportId); + + AstScaSummaryResults scanSummary = getSummaryReport(scanId); + result.setSummary(scanSummary); + printSummary(scanSummary, this.scanId); + + List findings = getFindings(scanId); + result.setFindings(findings); + + List packages = getPackages(scanId); + result.setPackages(packages); + + + if(config.isEnablePolicyViolationsSCA()) { + List policyEvaluations = getPolicyEvaluation(reportId); + result.setPolicyEvaluations(policyEvaluations); + printPolicyEvaluations(policyEvaluations); + determinePolicyViolations(result); + } + + String reportLink = getWebReportLink(config.getAstScaConfig().getWebAppUrl()); + result.setWebReportLink(reportLink); + printWebReportLink(result); + result.setScaResultReady(true); + log.info("Retrieved SCA results successfully."); + } catch (IOException e) { + throw new CxClientException("Error retrieving CxSCA scan results.", e); + } + return result; + } + + @Override + protected String getWebReportPath() throws UnsupportedEncodingException { + return String.format(WEB_REPORT, + URLEncoder.encode(projectId, ENCODING), + URLEncoder.encode(scanId, ENCODING)); + } + + private AstScaSummaryResults getSummaryReport(String scanId) throws IOException { + log.debug("Getting summary report."); + + String path = String.format(SUMMARY_REPORT, URLEncoder.encode(scanId, ENCODING)); + + return httpClient.getRequest(path, + ContentType.CONTENT_TYPE_APPLICATION_JSON, + AstScaSummaryResults.class, + HttpStatus.SC_OK, + "CxSCA report summary", + false); + } + + private List getFindings(String scanId) throws IOException { + log.debug("Getting findings."); + + String path = String.format(FINDINGS, URLEncoder.encode(scanId, ENCODING)); + + ArrayNode responseJson = httpClient.getRequest(path, + ContentType.CONTENT_TYPE_APPLICATION_JSON, + ArrayNode.class, + HttpStatus.SC_OK, + "CxSCA findings", + false); + + Finding[] findings = caseInsensitiveObjectMapper.treeToValue(responseJson, Finding[].class); + + return Arrays.asList(findings); + } + + private List getPackages(String scanId) throws IOException { + log.debug("Getting packages."); + + String path = String.format(PACKAGES, URLEncoder.encode(scanId, ENCODING)); + + return (List) httpClient.getRequest(path, + ContentType.CONTENT_TYPE_APPLICATION_JSON, + Package.class, + HttpStatus.SC_OK, + "CxSCA findings", + true); + } + + public String getReportId(String scanId) throws IOException { + log.debug("Getting report id."); + + String path = String.format(REPORTID_API, URLEncoder.encode(scanId, ENCODING)); + + String resultReportId = (String) httpClient.getRequest(path, + ContentType.CONTENT_TYPE_APPLICATION_JSON, + String.class, + HttpStatus.SC_OK, + "CxSCA Risk ReportId", + false); + return StringUtils.strip(resultReportId, "\""); + } + + public List getPolicyEvaluation(String reportId) throws IOException { + log.debug("Getting policy evaluation for the scan report id {}.", reportId); + + String path = String.format(POLICY_MANAGEMENT_EVALUATION_API, URLEncoder.encode(reportId, ENCODING)); + + return (List) httpClient.getRequest(path, + ContentType.CONTENT_TYPE_APPLICATION_JSON, + PolicyEvaluation.class, + HttpStatus.SC_OK, + "CxSCA policy evaulation", + true); + } + + private void determinePolicyViolations(AstScaResults result) { + + result.getPolicyEvaluations().forEach(p-> { + if(p.getIsViolated()) { + //its enough even one policy is violated + result.setPolicyViolated(true); + if(p.getActions().isBreakBuild()) + result.setBreakTheBuild(true); + } + } + ); + } + + private void printSummary(AstScaSummaryResults summary, String scanId) { + if (log.isInfoEnabled()) { + log.info("----CxSCA risk report summary----"); + log.info("Created on: {}", summary.getCreatedOn()); + log.info("Direct packages: {}", summary.getDirectPackages()); + log.info("Critical vulnerabilities: {}", summary.getCriticalVulnerabilityCount()); + log.info("High vulnerabilities: {}", summary.getHighVulnerabilityCount()); + log.info("Medium vulnerabilities: {}", summary.getMediumVulnerabilityCount()); + log.info("Low vulnerabilities: {}", summary.getLowVulnerabilityCount()); + log.info("Scan ID: {}", scanId); + log.info(String.format("Risk score: %.2f", summary.getRiskScore())); + log.info("Total packages: {}", summary.getTotalPackages()); + log.info("Total outdated packages: {}", summary.getTotalOutdatedPackages()); + } + } + + private void printPolicyEvaluations(List policyEvaulations) { + if (log.isInfoEnabled()) { + log.info("----CxSCA Policy Evaluation Results----"); + policyEvaulations.forEach(p-> printPolicyEvaluation(p)); + log.info("---------------------------------------"); + } + } + + private void printPolicyEvaluation(PolicyEvaluation p) { + if (log.isInfoEnabled()) { + log.info(" Policy name: {} | Violated:{} | Policy Description: {}", p.getName(), p.getIsViolated(), p.getDescription()); + + p.getRules().forEach(r-> + log.info(" Rule name: {} | Violated: {}", r.getName(), r.getIsViolated()) + ); + + } + } + + private void validate(AstScaConfig config) { + String error = null; + if (config == null) { + error = "%s config must be provided."; + } else if (StringUtils.isEmpty(config.getApiUrl())) { + error = "%s API URL must be provided."; + } else if (StringUtils.isEmpty(config.getAccessControlUrl())) { + error = "%s access control URL must be provided."; + } else { + RemoteRepositoryInfo repoInfo = config.getRemoteRepositoryInfo(); + if (repoInfo == null && config.getSourceLocationType() == SourceLocationType.REMOTE_REPOSITORY) { + error = "%s remote repository info must be provided."; + } else if (repoInfo != null && StringUtils.isNotEmpty(repoInfo.getBranch())) { + error = "%s doesn't support specifying custom branches. It currently uses the default branch of a repo."; + } + } + + if (error != null) { + throw new IllegalArgumentException(String.format(error, getScannerDisplayName())); + } + } +} diff --git a/src/main/java/com/cx/restclient/ast/AstWaiter.java b/src/main/java/com/cx/restclient/ast/AstWaiter.java new file mode 100644 index 00000000..51068100 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/AstWaiter.java @@ -0,0 +1,142 @@ +package com.cx.restclient.ast; + +import com.cx.restclient.common.ShragaUtils; +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.exception.CxClientException; +import com.cx.restclient.httpClient.CxHttpClient; +import com.cx.restclient.httpClient.utils.ContentType; +import com.cx.restclient.ast.dto.common.ScanInfoResponse; +import com.cx.restclient.ast.dto.common.ScanStatus; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.apache.commons.lang3.EnumUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.http.HttpStatus; +import org.awaitility.Awaitility; +import org.awaitility.core.ConditionTimeoutException; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.cx.restclient.ast.AstClient.ENCODING; + +@RequiredArgsConstructor +public class AstWaiter { + private final CxHttpClient httpClient; + private final CxScanConfig config; + private final String scannerDisplayName; + private long startTimestampSec; + private final Logger log; + + public void waitForScanToFinish(String scanId) { + startTimestampSec = System.currentTimeMillis() / 1000; + Duration timeout = getTimeout(config); + Duration pollInterval = getPollInterval(config); + + int maxErrorCount = getMaxErrorCount(config); + AtomicInteger errorCounter = new AtomicInteger(); + + try { + String urlPath = String.format(AstClient.GET_SCAN, URLEncoder.encode(scanId, ENCODING)); + + Awaitility.await() + .atMost(timeout) + .pollDelay(Duration.ZERO) + .pollInterval(pollInterval) + .until(() -> scanIsCompleted(urlPath, errorCounter, maxErrorCount)); + + } catch (ConditionTimeoutException e) { + String message = String.format( + "Failed to perform %s scan. The scan has been automatically aborted: " + + "reached the user-specified timeout (%d minutes).", + scannerDisplayName, + timeout.toMinutes()); + throw new ConditionTimeoutException(message); + } catch (UnsupportedEncodingException e) { + log.error("Unexpected error.", e); + } + } + + private static Duration getTimeout(CxScanConfig config) { + Integer rawTimeout = config.getOsaScanTimeoutInMinutes(); + // For SCA scan get SCA scan time out as the timeout + if(config.isAstScaEnabled()) { + rawTimeout = config.getSCAScanTimeoutInMinutes(); + } + final int DEFAULT_TIMEOUT = 60; + rawTimeout = rawTimeout != null && rawTimeout > 0 ? rawTimeout : DEFAULT_TIMEOUT; + return Duration.ofMinutes(rawTimeout); + } + + private static Duration getPollInterval(CxScanConfig config) { + int rawPollInterval = ObjectUtils.defaultIfNull(config.getOsaProgressInterval(), 20); + return Duration.ofSeconds(rawPollInterval); + } + + private static int getMaxErrorCount(CxScanConfig config) { + return ObjectUtils.defaultIfNull(config.getConnectionRetries(), 3); + } + + private boolean scanIsCompleted(String path, AtomicInteger errorCounter, int maxErrorCount) { + ScanInfoResponse response = null; + String errorMessage = null; + try { + String failedMessage = scannerDisplayName + " scan"; + response = httpClient.getRequest(path, ContentType.CONTENT_TYPE_APPLICATION_JSON, + ScanInfoResponse.class, HttpStatus.SC_OK, failedMessage, false); + } catch (Exception e) { + errorMessage = e.getMessage(); + } + + boolean completedSuccessfully = false; + if (response == null) { + // A network error is likely to have occurred -> retry. + countError(errorCounter, maxErrorCount, errorMessage); + } else { + ScanStatus status = extractScanStatusFrom(response); + completedSuccessfully = handleScanStatus(status); + } + + return completedSuccessfully; + } + + private boolean handleScanStatus(ScanStatus status) { + boolean completedSuccessfully = false; + if (status == ScanStatus.COMPLETED) { + completedSuccessfully = true; + } else if (status == ScanStatus.FAILED) { + // Scan has failed on the back end, no need to retry. + throw new CxClientException(String.format("Scan status is %s, aborting.", status)); + } else if (status == null) { + log.warn("Unknown status."); + } + return completedSuccessfully; + } + + private void countError(AtomicInteger errorCounter, int maxErrorCount, String message) { + int currentErrorCount = errorCounter.incrementAndGet(); + int triesLeft = maxErrorCount - currentErrorCount; + if (triesLeft < 0) { + String fullMessage = String.format("Maximum number of errors was reached (%d), aborting.", maxErrorCount); + throw new CxClientException(fullMessage); + } else { + String note = (triesLeft == 0 ? "last attempt" : String.format("tries left: %d", triesLeft)); + log.info(String.format("Failed to get status from %s with the message: %s. Retrying (%s)", + scannerDisplayName, + message, + note)); + } + } + + private ScanStatus extractScanStatusFrom(ScanInfoResponse response) { + String rawStatus = response.getStatus(); + String elapsedTimestamp = ShragaUtils.getTimestampSince(startTimestampSec); + log.info(String.format("Waiting for %s scan results. Elapsed time: %s. Status: %s.", + scannerDisplayName, + elapsedTimestamp, + rawStatus)); + return EnumUtils.getEnumIgnoreCase(ScanStatus.class, rawStatus); + } +} diff --git a/src/main/java/com/cx/restclient/ast/ClientTypeResolver.java b/src/main/java/com/cx/restclient/ast/ClientTypeResolver.java new file mode 100644 index 00000000..c9217e7c --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/ClientTypeResolver.java @@ -0,0 +1,116 @@ +package com.cx.restclient.ast; + +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.exception.CxClientException; +import com.cx.restclient.httpClient.CxHttpClient; +import com.cx.restclient.osa.dto.ClientType; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import static com.cx.restclient.httpClient.utils.ContentType.CONTENT_TYPE_APPLICATION_JSON_V1; + +@Slf4j +public class ClientTypeResolver { + private static final String WELL_KNOWN_CONFIG_PATH = "identity/.well-known/openid-configuration"; + private static final String SCOPES_JSON_PROP = "scopes_supported"; + + private static final Set scopesForCloudAuth = new HashSet<>(Arrays.asList("sca_api", "offline_access")); + private static final Set scopesForOnPremAuth = new HashSet<>(Arrays.asList("sast_rest_api", "cxarm_api")); + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private CxHttpClient httpClient; + + private CxScanConfig config; + + public ClientTypeResolver(CxScanConfig config) { + this.config = config; + } + + /** + * Determines which scopes and client secret must be used for SCA login. + * + * @param accessControlServerBaseUrl used to determine scopes supported by this server. + * @return client settings for the provided AC server. + */ + public ClientType determineClientType(String accessControlServerBaseUrl) { + JsonNode response = getConfigResponse(accessControlServerBaseUrl); + Set supportedScopes = getSupportedScopes(response); + Set scopesToUse = getScopesForAuth(supportedScopes); + + String clientSecret = scopesToUse.equals(scopesForOnPremAuth) ? ClientType.RESOURCE_OWNER.getClientSecret() : ""; + + String scopesForRequest = String.join(" ", scopesToUse); + + return ClientType.builder().clientId(ClientType.RESOURCE_OWNER.getClientId()) + .scopes(scopesForRequest) + .clientSecret(clientSecret) + .build(); + } + + private Set getScopesForAuth(Set supportedScopes) { + Set result; + if (supportedScopes.containsAll(scopesForCloudAuth)) { + result = scopesForCloudAuth; + } else if (supportedScopes.containsAll(scopesForOnPremAuth)) { + result = scopesForOnPremAuth; + } else { + String message = String.format("Access control server doesn't support the necessary scopes (either %s or %s)." + + " It only supports the following scopes: %s.", + scopesForCloudAuth, + scopesForOnPremAuth, + supportedScopes); + + throw new CxClientException(message); + } + log.debug(String.format("Using scopes: %s", result)); + return result; + } + + private JsonNode getConfigResponse(String accessControlServerBaseUrl) { + try { + String res = getHttpClient(accessControlServerBaseUrl).getRequest(WELL_KNOWN_CONFIG_PATH, CONTENT_TYPE_APPLICATION_JSON_V1, String.class, 200, "Get openId configuration", false); + return objectMapper.readTree(res); + } catch (Exception e) { + log.error(e.getMessage()); + throw new CxClientException("Error getting OpenID config response.", e); + } + } + + private CxHttpClient getHttpClient(String acBaseUrl) { + if (httpClient == null) { + httpClient = new CxHttpClient( + StringUtils.appendIfMissing(acBaseUrl, "/"), + config.getCxOrigin(), + config.getCxOriginUrl(), + config.isDisableCertificateValidation(), + config.isUseSSOLogin(), + config.getRefreshToken(), + config.isScaProxy(), + config.getScaProxyConfig(), + log, + config.getNTLM(), + config.getPluginVersion()); + } + return httpClient; + } + + private static Set getSupportedScopes(JsonNode response) { + Set result = null; + if (response != null) { + TypeReference> typeRef = new TypeReference>() { + }; + result = objectMapper.convertValue(response.get(SCOPES_JSON_PROP), typeRef); + } + return Optional.ofNullable(result).orElse(new HashSet<>()); + } + +} diff --git a/src/main/java/com/cx/restclient/ast/SpawnScaResolver.java b/src/main/java/com/cx/restclient/ast/SpawnScaResolver.java new file mode 100644 index 00000000..987170cc --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/SpawnScaResolver.java @@ -0,0 +1,169 @@ +package com.cx.restclient.ast; + +import com.cx.restclient.exception.CxClientException; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.io.FileWriter; + +import org.apache.commons.lang3.SystemUtils; +import org.slf4j.Logger; + +/** + * This class executes sca resolver executable to generate evidence/result file. + */ + +public class SpawnScaResolver { + + public static final String SCA_RESOLVER_EXE = "\\" + "ScaResolver" + ".exe"; + public static final String SCA_RESOLVER_FOR_LINUX = "/" + "ScaResolver"; + public static final String OFFLINE = "offline"; + + /** + * This method executes + * + * @param pathToScaResolver - Path to SCA Resolver executable + * @param scaResolverAddParams - Additional parameters for SCA resolver + * @param pathToSASTResultJSONFile - Additional parameters for SCA resolver + * @return + */ + protected static int runScaResolver(String pathToScaResolver, String scaResolverAddParams, String pathToResultJSONFile,String pathToSASTResultJSONFile, Logger log) + throws CxClientException { + int exitCode = -100; + String[] scaResolverCommand; + + List arguments = new ArrayList(); + Matcher m = Pattern.compile("([^\"]\\S*|\".+?\")\\s*").matcher(scaResolverAddParams); + while (m.find()) + arguments.add(m.group(1)); + /* + Convert path and additional parameters into a single CMD command + */ + scaResolverCommand = new String[arguments.size() + 2]; + + if (!SystemUtils.IS_OS_UNIX) { + //Add "ScaResolver.exe" to cmd command on Windows + pathToScaResolver = pathToScaResolver + SCA_RESOLVER_EXE; + } else { + //Add "/ScaResolver" command on Linux machines + pathToScaResolver = pathToScaResolver + SCA_RESOLVER_FOR_LINUX; + } + + log.debug("Starting build CMD command"); + scaResolverCommand[0] = pathToScaResolver; + scaResolverCommand[1] = OFFLINE; + boolean configGiven = false; + for (int i = 0; i < arguments.size(); i++) { + + String arg = arguments.get(i); + scaResolverCommand[i + 2] = arg; + if (arg.equals("-r") || "--resolver-result-path".equals(arg)) { + while (pathToResultJSONFile.contains("\"")) + pathToResultJSONFile = pathToResultJSONFile.replace("\"", ""); + scaResolverCommand[i + 3] = pathToResultJSONFile; + i++; + } else if (arg.equals("-c") || arg.equals("--config-path")) { + configGiven = true; + } + + else if ("--sast-result-path".equals(arg)) { + while (pathToSASTResultJSONFile.contains("\"")) + pathToSASTResultJSONFile = pathToSASTResultJSONFile.replace("\"", ""); + scaResolverCommand[i + 3] = pathToSASTResultJSONFile; + i++; + } + } + + if (!configGiven) { + if (pathToResultJSONFile.equals("")) { + Path parent = Paths.get(pathToResultJSONFile).getParent(); + if (parent != null) { + Path logDir = Paths.get(parent.toString(), "log"); + Path configPath = Paths.get(parent.toString(), "Configuration.ini"); + + try { + Files.createDirectories(logDir); + } catch (IOException e) { + log.error("Could not create log directory: " + e.getMessage(), e.getStackTrace()); + throw new CxClientException(e); + } + try (FileWriter config = new FileWriter(configPath.toString())) { + config.write("LogsDirectory=" + logDir); + } catch (IOException e) { + log.error("Could not create configuration file: " + e.getMessage(), e.getStackTrace()); + } + + log.debug(" --config-path " + configPath); + scaResolverCommand[arguments.size()] = "--config-path"; + scaResolverCommand[arguments.size() + 1] = configPath.toString(); + } + } + } + + log.debug("Finished created CMD command"); + try { + Process process; + String[] command = scaResolverCommand; + if (SystemUtils.IS_OS_UNIX) { + String tempPermissionValidation = "ls " + pathToScaResolver + " -ltr"; + printExecCommandOutput(tempPermissionValidation, log); + } + + log.debug("Executing ScaResolver command."); + process = Runtime.getRuntime().exec(command); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line = null; + while ((line = reader.readLine()) != null) { + log.info(line); + } + } catch (IOException e) { + log.error("Error while reading standard output: " + e.getMessage(), e.getStackTrace()); + throw new CxClientException(e); + } + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { + String line; + while ((line = reader.readLine()) != null) { + log.debug(line); + } + } catch (IOException e) { + log.error("Error while reading error output: " + e.getMessage(), e.getStackTrace()); + throw new CxClientException(e); + } + exitCode = process.waitFor(); + + } catch (IOException | InterruptedException e) { + log.error("Failed to execute next command : " + scaResolverCommand, e.getMessage(), e.getStackTrace()); + Thread.currentThread().interrupt(); + if (Thread.interrupted()) { + throw new CxClientException(e); + } + } + return exitCode; + + } + + private static void printExecCommandOutput(String execCommand, Logger log) { + try { + log.debug("Checking that next file has -rwxrwxrwx permissions " + execCommand); + Runtime r = Runtime.getRuntime(); + Process p = r.exec(execCommand); + BufferedReader is = new BufferedReader(new InputStreamReader(p.getInputStream())); + String line; + while ((line = is.readLine()) != null) { + log.debug(line); + } + } catch (Exception ex) { + log.debug("Failed to run execute [%s] command "); + } + } +} diff --git a/src/main/java/com/cx/restclient/ast/dto/common/ASTConfig.java b/src/main/java/com/cx/restclient/ast/dto/common/ASTConfig.java new file mode 100644 index 00000000..5ec1dac5 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/common/ASTConfig.java @@ -0,0 +1,25 @@ +package com.cx.restclient.ast.dto.common; + +import com.cx.restclient.dto.SourceLocationType; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; + +@Getter +@Setter +@NoArgsConstructor +@SuperBuilder +public abstract class ASTConfig implements Serializable { + private String apiUrl; + private String webAppUrl; + private SourceLocationType sourceLocationType; + private String zipFilePath; + + /** + * Must be specified if sourceLocationType is {@link SourceLocationType#REMOTE_REPOSITORY} + */ + private RemoteRepositoryInfo remoteRepositoryInfo; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/common/Credentials.java b/src/main/java/com/cx/restclient/ast/dto/common/Credentials.java new file mode 100644 index 00000000..776e4bf9 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/common/Credentials.java @@ -0,0 +1,8 @@ +package com.cx.restclient.ast.dto.common; + +public class Credentials { + + public String type; + public String value; + +} diff --git a/src/main/java/com/cx/restclient/ast/dto/common/GitCredentials.java b/src/main/java/com/cx/restclient/ast/dto/common/GitCredentials.java new file mode 100644 index 00000000..b1d3a003 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/common/GitCredentials.java @@ -0,0 +1,11 @@ +package com.cx.restclient.ast.dto.common; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class GitCredentials { + private String type; + private String value; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/common/HandlerRef.java b/src/main/java/com/cx/restclient/ast/dto/common/HandlerRef.java new file mode 100644 index 00000000..c33675c2 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/common/HandlerRef.java @@ -0,0 +1,11 @@ +package com.cx.restclient.ast.dto.common; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class HandlerRef { + private String type; + private String value; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/common/ProjectToScan.java b/src/main/java/com/cx/restclient/ast/dto/common/ProjectToScan.java new file mode 100644 index 00000000..547e576e --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/common/ProjectToScan.java @@ -0,0 +1,12 @@ +package com.cx.restclient.ast.dto.common; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class ProjectToScan { + private String id; + private String type; + private ScanStartHandler handler; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/common/RemoteRepositoryInfo.java b/src/main/java/com/cx/restclient/ast/dto/common/RemoteRepositoryInfo.java new file mode 100644 index 00000000..8b7ef4e2 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/common/RemoteRepositoryInfo.java @@ -0,0 +1,29 @@ +package com.cx.restclient.ast.dto.common; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.net.URL; + +/** + * Instructs AST scanners which repository should be scanned. + */ +@Getter +@Setter +public class RemoteRepositoryInfo implements Serializable { + /** + * A URL for which 'git clone' is possible. + */ + private URL url; + + private String branch; + + /** + * If access token is used instead of username/password, pass the token into this field. + * TODO: add a dedicated field for token. + */ + private String username; + + private String password; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/common/ScanConfig.java b/src/main/java/com/cx/restclient/ast/dto/common/ScanConfig.java new file mode 100644 index 00000000..6995e4fa --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/common/ScanConfig.java @@ -0,0 +1,25 @@ +package com.cx.restclient.ast.dto.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * Defines a single scan engine that will be used during scan. + */ +@Builder +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class ScanConfig { + /** + * Scan engine type. + */ + private String type; + + /** + * Engine-specific config. + */ + private ScanConfigValue value; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/common/ScanConfigValue.java b/src/main/java/com/cx/restclient/ast/dto/common/ScanConfigValue.java new file mode 100644 index 00000000..88a0a543 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/common/ScanConfigValue.java @@ -0,0 +1,8 @@ +package com.cx.restclient.ast.dto.common; + +/** + * Marker interface for scanner-specific scan configurations. + */ +public interface ScanConfigValue { + +} diff --git a/src/main/java/com/cx/restclient/ast/dto/common/ScanInfoResponse.java b/src/main/java/com/cx/restclient/ast/dto/common/ScanInfoResponse.java new file mode 100644 index 00000000..74a18f4a --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/common/ScanInfoResponse.java @@ -0,0 +1,9 @@ +package com.cx.restclient.ast.dto.common; + +import lombok.Getter; + +@Getter +public class ScanInfoResponse { + private String id; + private String status; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/common/ScanStartHandler.java b/src/main/java/com/cx/restclient/ast/dto/common/ScanStartHandler.java new file mode 100644 index 00000000..2f282e2c --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/common/ScanStartHandler.java @@ -0,0 +1,23 @@ +package com.cx.restclient.ast.dto.common; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class ScanStartHandler { + /** + * For local directory scan - the URL where the zipped directory has been uploaded. + * For remote repo scan - a URL for which 'git clone' is possible. + */ + private String url; + + /** + * For remote repo scan, contains a reference to a specific commit. + */ + private HandlerRef ref; + + private String username; + + private GitCredentials credentials; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/common/ScanStatus.java b/src/main/java/com/cx/restclient/ast/dto/common/ScanStatus.java new file mode 100644 index 00000000..99b7544b --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/common/ScanStatus.java @@ -0,0 +1,10 @@ +package com.cx.restclient.ast.dto.common; + +public enum ScanStatus { + // Some of the statuses are not used in code, but they help to prevent the "unknown status" warnings. + CANCELED, + QUEUED, + RUNNING, + COMPLETED, + FAILED +} diff --git a/src/main/java/com/cx/restclient/ast/dto/common/StartScanRequest.java b/src/main/java/com/cx/restclient/ast/dto/common/StartScanRequest.java new file mode 100644 index 00000000..c8a00382 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/common/StartScanRequest.java @@ -0,0 +1,21 @@ +package com.cx.restclient.ast.dto.common; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Builder +@Getter +public class StartScanRequest { + /** + * What to scan. + */ + private ProjectToScan project; + + /** + * How to scan. + */ + private List config; + private Object tags; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/common/SummaryResults.java b/src/main/java/com/cx/restclient/ast/dto/common/SummaryResults.java new file mode 100644 index 00000000..cf9a6bfc --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/common/SummaryResults.java @@ -0,0 +1,13 @@ +package com.cx.restclient.ast.dto.common; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class SummaryResults { + private int criticalVulnerabilityCount = 0; + private int highVulnerabilityCount = 0; + private int mediumVulnerabilityCount = 0; + private int lowVulnerabilityCount = 0; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/common/tags.java b/src/main/java/com/cx/restclient/ast/dto/common/tags.java new file mode 100644 index 00000000..481e802f --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/common/tags.java @@ -0,0 +1,11 @@ +package com.cx.restclient.ast.dto.common; + + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class tags { + +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sast/AstSastConfig.java b/src/main/java/com/cx/restclient/ast/dto/sast/AstSastConfig.java new file mode 100644 index 00000000..d018ffe2 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sast/AstSastConfig.java @@ -0,0 +1,25 @@ +package com.cx.restclient.ast.dto.sast; + +import com.cx.restclient.ast.dto.common.ASTConfig; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; + +@SuperBuilder +@Getter +@Setter +@NoArgsConstructor +public class AstSastConfig extends ASTConfig implements Serializable { + private String clientSecret; + private String clientId; + private String presetName; + private boolean incremental; + + /** + * Used as a paging parameter in scan result requests. + */ + private int resultsPageSize; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sast/AstSastResults.java b/src/main/java/com/cx/restclient/ast/dto/sast/AstSastResults.java new file mode 100644 index 00000000..eed37969 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sast/AstSastResults.java @@ -0,0 +1,19 @@ +package com.cx.restclient.ast.dto.sast; + +import com.cx.restclient.ast.dto.sast.report.AstSastSummaryResults; +import com.cx.restclient.ast.dto.sast.report.Finding; +import com.cx.restclient.dto.Results; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.List; + +@Getter +@Setter +public class AstSastResults extends Results implements Serializable { + private String scanId; + private AstSastSummaryResults summary; + private String webReportLink; + private List findings; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sast/SastScanConfigValue.java b/src/main/java/com/cx/restclient/ast/dto/sast/SastScanConfigValue.java new file mode 100644 index 00000000..3a75535c --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sast/SastScanConfigValue.java @@ -0,0 +1,23 @@ +package com.cx.restclient.ast.dto.sast; + +import com.cx.restclient.ast.dto.common.ScanConfigValue; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * AST-SAST-specific config parameters. Should be expanded with other supported properties. + */ +@Builder +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class SastScanConfigValue implements ScanConfigValue { + private String presetName; + + /** + * Must be a string ("true" or "false"). + */ + private String incremental; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sast/report/AstSastSummaryResults.java b/src/main/java/com/cx/restclient/ast/dto/sast/report/AstSastSummaryResults.java new file mode 100644 index 00000000..13122e2d --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sast/report/AstSastSummaryResults.java @@ -0,0 +1,18 @@ +package com.cx.restclient.ast.dto.sast.report; + +import com.cx.restclient.ast.dto.common.SummaryResults; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.List; + +/** + * Summary for external use as a part of general scan results. + */ +@Getter +@Setter +public class AstSastSummaryResults extends SummaryResults implements Serializable { + private List statusCounters; + private int totalCounter; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sast/report/Finding.java b/src/main/java/com/cx/restclient/ast/dto/sast/report/Finding.java new file mode 100644 index 00000000..e5212fd1 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sast/report/Finding.java @@ -0,0 +1,27 @@ +package com.cx.restclient.ast.dto.sast.report; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +public class Finding implements Serializable { + private String queryID; + private String queryName; + private String severity; + private int cweID; + private int similarityID; + private int uniqueID; + private List nodes = new ArrayList<>(); + private String pathSystemID; + private String firstScanID; + private String firstFoundAt; + private String foundAt; + private String status; + private String description; + private String state; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sast/report/FindingNode.java b/src/main/java/com/cx/restclient/ast/dto/sast/report/FindingNode.java new file mode 100644 index 00000000..890092fa --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sast/report/FindingNode.java @@ -0,0 +1,18 @@ +package com.cx.restclient.ast.dto.sast.report; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; + +@Getter +@Setter +public class FindingNode implements Serializable { + private int column; + private String fileName; + private String fullName; + private int length; + private int line; + private int methodLine; + private String name; + private String nodeSystemID; +} \ No newline at end of file diff --git a/src/main/java/com/cx/restclient/ast/dto/sast/report/QueryDescription.java b/src/main/java/com/cx/restclient/ast/dto/sast/report/QueryDescription.java new file mode 100644 index 00000000..257d613e --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sast/report/QueryDescription.java @@ -0,0 +1,18 @@ +package com.cx.restclient.ast.dto.sast.report; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; + +@Getter +@Setter +public class QueryDescription implements Serializable { + + private String queryId; + private String queryDescriptionId; + private String resultDescription; + private String risk; + private String cause; + private String generalRecommendations; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sast/report/ScanResultsResponse.java b/src/main/java/com/cx/restclient/ast/dto/sast/report/ScanResultsResponse.java new file mode 100644 index 00000000..d6b895ba --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sast/report/ScanResultsResponse.java @@ -0,0 +1,15 @@ +package com.cx.restclient.ast.dto.sast.report; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +public class ScanResultsResponse implements Serializable { + private List results = new ArrayList<>(); + private int totalCount; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sast/report/SeverityCounter.java b/src/main/java/com/cx/restclient/ast/dto/sast/report/SeverityCounter.java new file mode 100644 index 00000000..dac426ea --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sast/report/SeverityCounter.java @@ -0,0 +1,13 @@ +package com.cx.restclient.ast.dto.sast.report; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; + +@Getter +@Setter +public class SeverityCounter implements Serializable { + private String severity; + private int counter; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sast/report/SingleScanSummary.java b/src/main/java/com/cx/restclient/ast/dto/sast/report/SingleScanSummary.java new file mode 100644 index 00000000..c5bfa82e --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sast/report/SingleScanSummary.java @@ -0,0 +1,18 @@ +package com.cx.restclient.ast.dto.sast.report; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +public +class SingleScanSummary implements Serializable { + private String scanId; + private List severityCounters = new ArrayList<>(); + private List statusCounters = new ArrayList<>(); + private int totalCounter; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sast/report/StatusCounter.java b/src/main/java/com/cx/restclient/ast/dto/sast/report/StatusCounter.java new file mode 100644 index 00000000..ea5bd8e2 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sast/report/StatusCounter.java @@ -0,0 +1,13 @@ +package com.cx.restclient.ast.dto.sast.report; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; + +@Getter +@Setter +public class StatusCounter implements Serializable { + private String status; + private int counter; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sast/report/SummaryResponse.java b/src/main/java/com/cx/restclient/ast/dto/sast/report/SummaryResponse.java new file mode 100644 index 00000000..c042cdb6 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sast/report/SummaryResponse.java @@ -0,0 +1,15 @@ +package com.cx.restclient.ast.dto.sast.report; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +public class SummaryResponse implements Serializable { + private List scansSummaries = new ArrayList<>(); + private int totalCount; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sca/AstScaConfig.java b/src/main/java/com/cx/restclient/ast/dto/sca/AstScaConfig.java new file mode 100644 index 00000000..98558128 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sca/AstScaConfig.java @@ -0,0 +1,95 @@ +package com.cx.restclient.ast.dto.sca; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; + +import com.cx.restclient.ast.dto.common.ASTConfig; + +import lombok.Getter; +import lombok.Setter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +public class AstScaConfig extends ASTConfig implements Serializable { + + private String accessControlUrl; + private String username; + private String password; + private String tenant; + private String teamPath; + private String teamId; + + /** + * true: upload all sources for scan + *
+ * false: only upload manifest and fingerprints for scan. Useful for customers that don't want their proprietary + * code to be uploaded into the cloud. + */ + private boolean includeSources; + private boolean enableScaResolver; + + private String fingerprintsIncludePattern; + private String manifestsIncludePattern; + private String fingerprintFilePath; + private String sastProjectId; + private String sastProjectName; + private String sastServerUrl; + private String sastUsername; + private String sastPassword; + private Boolean isScaProxy; + + private String pathToScaResolver; + private String scaProjectCustomTags; + private String scaScanCustomTags; + private String scaResolverAddParameters; + + public String getPathToScaResolver() { + return pathToScaResolver; + } + public void setPathToScaResolver(String pathToScaResolver) { + this.pathToScaResolver = pathToScaResolver; + } + public String getScaResolverAddParameters() { + return scaResolverAddParameters; + } + public void setScaResolverAddParameters(String scaResolverAddParameters) { + this.scaResolverAddParameters = scaResolverAddParameters; + } + + public String getScaProjectCustomTags() { + return scaProjectCustomTags; + } + + public void setScaProjectCustomTags(String scaProjectCustomTags) { + this.scaProjectCustomTags = scaProjectCustomTags; + } + + public String getScaScanCustomTags() { + return scaScanCustomTags; + } + + public void setScaScanCustomTags(String scaScanCustomTags) { + this.scaScanCustomTags = scaScanCustomTags; + } + private Map envVariables; + private List configFilePaths; + + public void setTeamPath(String teamPath) { + //Make teampath always in the form /CxServer/Team1. User might have used '\' in the path. + if (!StringUtils.isEmpty(teamPath) && !teamPath.startsWith("\\") && !teamPath.startsWith(("/"))) { + teamPath = "/" + teamPath; + } + if (!StringUtils.isEmpty(teamPath) && teamPath != null) { + teamPath = teamPath.replace("\\", "/"); + } + this.teamPath = teamPath; + } + public Boolean getScaProxy() {return isScaProxy;} + + public void isScaProxy(Boolean scaProxy) {isScaProxy = scaProxy;} +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sca/AstScaResults.java b/src/main/java/com/cx/restclient/ast/dto/sca/AstScaResults.java new file mode 100644 index 00000000..c3d51848 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sca/AstScaResults.java @@ -0,0 +1,183 @@ +package com.cx.restclient.ast.dto.sca; + +import com.cx.restclient.ast.dto.sca.report.AstScaSummaryResults; +import com.cx.restclient.ast.dto.sca.report.Finding; +import com.cx.restclient.ast.dto.sca.report.Package; +import com.cx.restclient.ast.dto.sca.report.PolicyEvaluation; +import com.cx.restclient.dto.Results; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.Serializable; +import java.util.List; + +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class AstScaResults extends Results implements Serializable { + private String scanId; + private String reportId; + private byte[] rawXMLReport; + private String scaPDFLink; + public String getScaPDFLink() { + return scaPDFLink; + } + + public void setScaPDFLink(String scaPDFLink) { + this.scaPDFLink = scaPDFLink; + } + + public byte[] getRawXMLReport() { + return rawXMLReport; + } + + public void setRawXMLReport(byte[] rawXMLReport) { + this.rawXMLReport = rawXMLReport; + } + + public byte[] getPDFReport() { + return PDFReport; + } + + public void setPDFReport(byte[] pDFReport) { + PDFReport = pDFReport; + } + + public String getPdfFileName() { + return pdfFileName; + } + + public void setPdfFileName(String pdfFileName) { + this.pdfFileName = pdfFileName; + } + + private byte[] PDFReport; + private String pdfFileName; + + public String getScanId() { + return scanId; + } + + public void setScanId(String scanId) { + this.scanId = scanId; + } + + public String getReportId() { + return reportId; + } + + public void setReportId(String reportId) { + this.reportId = reportId; + } + + public AstScaSummaryResults getSummary() { + return summary; + } + + public void setSummary(AstScaSummaryResults summary) { + this.summary = summary; + } + + public String getWebReportLink() { + return webReportLink; + } + + public void setWebReportLink(String webReportLink) { + this.webReportLink = webReportLink; + } + + public List getFindings() { + return findings; + } + + public void setFindings(List findings) { + this.findings = findings; + } + + public List getPackages() { + return packages; + } + + public void setPackages(List packages) { + this.packages = packages; + } + + public boolean isScaResultReady() { + return scaResultReady; + } + + public void setScaResultReady(boolean scaResultReady) { + this.scaResultReady = scaResultReady; + } + + public int getNonVulnerableLibraries() { + return nonVulnerableLibraries; + } + + public void setNonVulnerableLibraries(int nonVulnerableLibraries) { + this.nonVulnerableLibraries = nonVulnerableLibraries; + } + + public int getVulnerableAndOutdated() { + return vulnerableAndOutdated; + } + + public void setVulnerableAndOutdated(int vulnerableAndOutdated) { + this.vulnerableAndOutdated = vulnerableAndOutdated; + } + + public List getPolicyEvaluations() { + return policyEvaluations; + } + + public void setPolicyEvaluations(List policyEvaluations) { + this.policyEvaluations = policyEvaluations; + } + + public boolean isPolicyViolated() { + return policyViolated; + } + + public void setPolicyViolated(boolean policyViolated) { + this.policyViolated = policyViolated; + } + + public boolean isBreakTheBuild() { + return breakTheBuild; + } + + public void setBreakTheBuild(boolean breakTheBuild) { + this.breakTheBuild = breakTheBuild; + } + + private AstScaSummaryResults summary; + private String webReportLink; + private List findings; + private List packages; + private boolean scaResultReady; + private int nonVulnerableLibraries; + private int vulnerableAndOutdated; + private List policyEvaluations; + private boolean policyViolated; + private boolean breakTheBuild; + + public void calculateVulnerableAndOutdatedPackages() { + int sum; + if (this.packages != null) { + for (Package pckg : this.packages) { + sum = pckg.getCriticalVulnerabilityCount() + pckg.getHighVulnerabilityCount() + pckg.getMediumVulnerabilityCount() + pckg.getLowVulnerabilityCount(); + if (sum == 0) { + this.nonVulnerableLibraries++; + } else if (sum > 0 && pckg.isOutdated()) { + this.vulnerableAndOutdated++; + } + } + } + } + +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sca/CreateProjectRequest.java b/src/main/java/com/cx/restclient/ast/dto/sca/CreateProjectRequest.java new file mode 100644 index 00000000..40825995 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sca/CreateProjectRequest.java @@ -0,0 +1,38 @@ +package com.cx.restclient.ast.dto.sca; + +import com.cx.restclient.sca.dto.Tags; + +import java.util.ArrayList; +import java.util.List; + +public class CreateProjectRequest { + private String name; + private List assignedTeams = new ArrayList<>(); + private Object Tags; + + public Object getTags() { + return Tags; + } + + public void setTags(Object tags) { + this.Tags = tags; + } + + public List getAssignedTeams() { + return assignedTeams; + } + + public void addAssignedTeams(String assignedTeam) { + this.assignedTeams.add(assignedTeam); + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sca/Project.java b/src/main/java/com/cx/restclient/ast/dto/sca/Project.java new file mode 100644 index 00000000..d4284598 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sca/Project.java @@ -0,0 +1,12 @@ +package com.cx.restclient.ast.dto.sca; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class Project { + private String name; + private String id; + private Object Tags; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sca/ScaScanConfigValue.java b/src/main/java/com/cx/restclient/ast/dto/sca/ScaScanConfigValue.java new file mode 100644 index 00000000..83b53de4 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sca/ScaScanConfigValue.java @@ -0,0 +1,27 @@ +package com.cx.restclient.ast.dto.sca; + +import com.cx.restclient.ast.dto.common.ScanConfigValue; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * AST-SCA-specific config parameters. Should be expanded with other supported properties. + */ +@Builder +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class ScaScanConfigValue implements ScanConfigValue { + + private String environmentVariables; + private String sastProjectId; + private String sastServerUrl; + private String sastUsername; + private String sastPassword; + private String sastProjectName; + +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sca/Team.java b/src/main/java/com/cx/restclient/ast/dto/sca/Team.java new file mode 100644 index 00000000..3c5c24aa --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sca/Team.java @@ -0,0 +1,115 @@ +package com.cx.restclient.ast.dto.sca; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class Team { + public String id; + public String name; + public String fullName; + public String parentId; + public String creationDate; + + public Team() {} + + public Team(String id, String name, String fullName, String parentId, String creationDate) { + super(); + this.id = id; + this.name = name; + this.fullName = fullName; + this.parentId = parentId; + this.creationDate = creationDate; + } + + public final String getId() { + return id; + } + + public final void setId(String id) { + this.id = id; + } + + public final String getName() { + return name; + } + + public final void setName(String name) { + this.name = name; + } + + public final String getFullName() { + return fullName; + } + + public final void setFullName(String fullName) { + this.fullName = fullName; + } + + public final String getParentId() { + return parentId; + } + + public final void setParentId(String parentId) { + this.parentId = parentId; + } + + public final String getCreationDate() { + return creationDate; + } + + public final void setCreationDate(String creationDate) { + this.creationDate = creationDate; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((creationDate == null) ? 0 : creationDate.hashCode()); + result = prime * result + ((fullName == null) ? 0 : fullName.hashCode()); + result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((parentId == null) ? 0 : parentId.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Team other = (Team) obj; + if (creationDate == null) { + if (other.creationDate != null) + return false; + } else if (!creationDate.equals(other.creationDate)) + return false; + if (fullName == null) { + if (other.fullName != null) + return false; + } else if (!fullName.equals(other.fullName)) + return false; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (parentId == null) { + if (other.parentId != null) + return false; + } else if (!parentId.equals(other.parentId)) + return false; + return true; + } + + + +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sca/UpdateProjectRequest.java b/src/main/java/com/cx/restclient/ast/dto/sca/UpdateProjectRequest.java new file mode 100644 index 00000000..1aa1c5a5 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sca/UpdateProjectRequest.java @@ -0,0 +1,39 @@ +package com.cx.restclient.ast.dto.sca; + +import java.util.ArrayList; +import java.util.List; + +public class UpdateProjectRequest { + private String name; + + public void setAssignedTeams(List assignedTeams) { + this.assignedTeams = assignedTeams; + } + + private List assignedTeams = new ArrayList<>(); + private Object Tags; + + public Object getTags() { + return Tags; + } + + public void setTags(Object tags) { + this.Tags = tags; + } + + public List getAssignedTeams() { + return assignedTeams; + } + + public void addAssignedTeams(String assignedTeam) { + this.assignedTeams.add(assignedTeam); + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sca/report/AstScaSummaryResults.java b/src/main/java/com/cx/restclient/ast/dto/sca/report/AstScaSummaryResults.java new file mode 100644 index 00000000..e2275954 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sca/report/AstScaSummaryResults.java @@ -0,0 +1,109 @@ +package com.cx.restclient.ast.dto.sca.report; + + +import java.io.Serializable; + +public class AstScaSummaryResults implements Serializable { + private int totalPackages; + private int directPackages; + private String createdOn; + private double riskScore; + private int totalOutdatedPackages; + private int criticalVulnerabilityCount = 0; + private int highVulnerabilityCount = 0; + private int mediumVulnerabilityCount = 0; + private int lowVulnerabilityCount = 0; + + public AstScaSummaryResults() { + } + + public AstScaSummaryResults(int totalPackages, int directPackages, String createdOn, double riskScore, int totalOutdatedPackages, int criticalVulnerabilityCount, int highVulnerabilityCount, int mediumVulnerabilityCount, int lowVulnerabilityCount) { + this.totalPackages = totalPackages; + this.directPackages = directPackages; + this.createdOn = createdOn; + this.riskScore = riskScore; + this.totalOutdatedPackages = totalOutdatedPackages; + this.criticalVulnerabilityCount = criticalVulnerabilityCount; + this.highVulnerabilityCount = highVulnerabilityCount; + this.mediumVulnerabilityCount = mediumVulnerabilityCount; + this.lowVulnerabilityCount = lowVulnerabilityCount; + } + + public int getTotalOkLibraries() { + int totalOk = (totalPackages - (criticalVulnerabilityCount +highVulnerabilityCount + mediumVulnerabilityCount + lowVulnerabilityCount)); + totalOk = Math.max(totalOk, 0); + return totalOk; + } + + public int getTotalPackages() { + return totalPackages; + } + + public void setTotalPackages(int totalPackages) { + this.totalPackages = totalPackages; + } + + public int getDirectPackages() { + return directPackages; + } + + public void setDirectPackages(int directPackages) { + this.directPackages = directPackages; + } + + public String getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(String createdOn) { + this.createdOn = createdOn; + } + + public double getRiskScore() { + return riskScore; + } + + public void setRiskScore(double riskScore) { + this.riskScore = riskScore; + } + + public int getTotalOutdatedPackages() { + return totalOutdatedPackages; + } + + public void setTotalOutdatedPackages(int totalOutdatedPackages) { + this.totalOutdatedPackages = totalOutdatedPackages; + } + + public int getCriticalVulnerabilityCount() { + return criticalVulnerabilityCount; + } + + public void setCriticalVulnerabilityCount(int criticalVulnerabilityCount) { + this.criticalVulnerabilityCount = criticalVulnerabilityCount; + } + + public int getHighVulnerabilityCount() { + return highVulnerabilityCount; + } + + public void setHighVulnerabilityCount(int highVulnerabilityCount) { + this.highVulnerabilityCount = highVulnerabilityCount; + } + + public int getMediumVulnerabilityCount() { + return mediumVulnerabilityCount; + } + + public void setMediumVulnerabilityCount(int mediumVulnerabilityCount) { + this.mediumVulnerabilityCount = mediumVulnerabilityCount; + } + + public int getLowVulnerabilityCount() { + return lowVulnerabilityCount; + } + + public void setLowVulnerabilityCount(int lowVulnerabilityCount) { + this.lowVulnerabilityCount = lowVulnerabilityCount; + } +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sca/report/DependencyPath.java b/src/main/java/com/cx/restclient/ast/dto/sca/report/DependencyPath.java new file mode 100644 index 00000000..fe30a269 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sca/report/DependencyPath.java @@ -0,0 +1,15 @@ +package com.cx.restclient.ast.dto.sca.report; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.ArrayList; + +/** + * Added for readability. + */ +@Getter +@Setter +public class DependencyPath extends ArrayList implements Serializable { +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sca/report/DependencyPathSegment.java b/src/main/java/com/cx/restclient/ast/dto/sca/report/DependencyPathSegment.java new file mode 100644 index 00000000..edbe02ef --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sca/report/DependencyPathSegment.java @@ -0,0 +1,16 @@ +package com.cx.restclient.ast.dto.sca.report; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; + +@Getter +@Setter +public class DependencyPathSegment implements Serializable { + private String id; + private String name; + private String version; + private boolean isResolved; + private boolean isDevelopment; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sca/report/Finding.java b/src/main/java/com/cx/restclient/ast/dto/sca/report/Finding.java new file mode 100644 index 00000000..4e2dad9d --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sca/report/Finding.java @@ -0,0 +1,30 @@ +package com.cx.restclient.ast.dto.sca.report; + +import com.cx.restclient.dto.scansummary.Severity; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * This entity is called vulnerability in SCA API, but here it is called Finding for consistency. + * Indicates a specific type of vulnerability detected in a specific package. + */ +@Getter +@Setter +public class Finding implements Serializable { + private String id; + private String cveName; + private double score; + private Severity severity; + private String publishDate; + private List references = new ArrayList<>(); + private String description; + private String recommendations; + private String packageId; + private String similarityId; + private String fixResolutionText; + private boolean isIgnored; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sca/report/Package.java b/src/main/java/com/cx/restclient/ast/dto/sca/report/Package.java new file mode 100644 index 00000000..8066126f --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sca/report/Package.java @@ -0,0 +1,45 @@ +package com.cx.restclient.ast.dto.sca.report; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * Info about a package that SCA retrieves by analyzing project dependencies. + */ +@Getter +@Setter +public class Package implements Serializable { + private String id; + private String name; + private String version; + private List licenses = new ArrayList<>(); + + /** + * The current values are [Filename, Sha1]. Not considered an enum in SCA API. + */ + private String matchType; + + private int criticalVulnerabilityCount; + private int highVulnerabilityCount; + private int mediumVulnerabilityCount; + private int lowVulnerabilityCount; + private int ignoredVulnerabilityCount; + private int numberOfVersionsSinceLastUpdate; + private String newestVersionReleaseDate; + private String newestVersion; + private boolean outdated; + private String releaseDate; + private String confidenceLevel; + private double riskScore; + private PackageSeverity severity; + private List locations = new ArrayList<>(); + private List dependencyPaths = new ArrayList<>(); + private String packageRepository; + private boolean isDirectDependency; + private boolean isDevelopment; + private PackageUsage packageUsage; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sca/report/PackageSeverity.java b/src/main/java/com/cx/restclient/ast/dto/sca/report/PackageSeverity.java new file mode 100644 index 00000000..44ef22b0 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sca/report/PackageSeverity.java @@ -0,0 +1,13 @@ +package com.cx.restclient.ast.dto.sca.report; + +public enum PackageSeverity { + /** + * Package was scanned but no vulnerabilities were detected. + */ + NONE, + + LOW, + MEDIUM, + HIGH, + CRITICAL +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sca/report/PackageUsage.java b/src/main/java/com/cx/restclient/ast/dto/sca/report/PackageUsage.java new file mode 100644 index 00000000..4a9c90f6 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sca/report/PackageUsage.java @@ -0,0 +1,13 @@ +package com.cx.restclient.ast.dto.sca.report; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; + +@Getter +@Setter +public class PackageUsage implements Serializable { + private String usageType; + private String packageId; +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sca/report/PolicyAction.java b/src/main/java/com/cx/restclient/ast/dto/sca/report/PolicyAction.java new file mode 100644 index 00000000..16c6f5c0 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sca/report/PolicyAction.java @@ -0,0 +1,18 @@ +package com.cx.restclient.ast.dto.sca.report; + +import java.io.Serializable; + + +public class PolicyAction implements Serializable { + private boolean breakBuild; + + public final boolean isBreakBuild() { + return breakBuild; + } + + public final void setBreakBuild(boolean breakBuild) { + this.breakBuild = breakBuild; + } + + +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sca/report/PolicyEvaluation.java b/src/main/java/com/cx/restclient/ast/dto/sca/report/PolicyEvaluation.java new file mode 100644 index 00000000..46e1a8ad --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sca/report/PolicyEvaluation.java @@ -0,0 +1,58 @@ +package com.cx.restclient.ast.dto.sca.report; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +//cannot use lombok because the way boolean members in the APi payload are named +//Jackson ObjectMapper looks for setter/getter with literally same. + +public class PolicyEvaluation implements Serializable { + private String id; + private String description; + private String name; + private boolean isViolated; + private PolicyAction actions; + private List rules = new ArrayList<>(); + + public final String getId() { + return id; + } + public final void setId(String id) { + this.id = id; + } + public final String getDescription() { + return description; + } + public final void setDescription(String description) { + this.description = description; + } + public final String getName() { + return name; + } + public final void setName(String name) { + this.name = name; + } + public final boolean getIsViolated() { + return isViolated; + } + + public final void setIsViolated(boolean isViolated) { + this.isViolated = isViolated; + } + + public final PolicyAction getActions() { + return actions; + } + public final void setActions(PolicyAction actions) { + this.actions = actions; + } + public final List getRules() { + return rules; + } + public final void setRules(List rules) { + this.rules = rules; + } + + +} diff --git a/src/main/java/com/cx/restclient/ast/dto/sca/report/PolicyRule.java b/src/main/java/com/cx/restclient/ast/dto/sca/report/PolicyRule.java new file mode 100644 index 00000000..cf768d24 --- /dev/null +++ b/src/main/java/com/cx/restclient/ast/dto/sca/report/PolicyRule.java @@ -0,0 +1,30 @@ +package com.cx.restclient.ast.dto.sca.report; + +import java.io.Serializable; + +public class PolicyRule implements Serializable { + private String id; + private boolean isViolated; + private String name; + + public final String getId() { + return id; + } + public final void setId(String id) { + this.id = id; + } + public final boolean getIsViolated() { + return isViolated; + } + public final void setIsViolated(boolean isViolated) { + this.isViolated = isViolated; + } + public final String getName() { + return name; + } + public final void setName(String name) { + this.name = name; + } + + +} diff --git a/src/main/java/com/cx/restclient/common/CxPARAM.java b/src/main/java/com/cx/restclient/common/CxPARAM.java index 7536a0f8..031b01d8 100644 --- a/src/main/java/com/cx/restclient/common/CxPARAM.java +++ b/src/main/java/com/cx/restclient/common/CxPARAM.java @@ -6,14 +6,37 @@ * Created by Galn on 14/02/2018. */ public abstract class CxPARAM { - public static final String AUTHENTICATION = "auth/identity/connect/token"; - public static final String ORIGIN_HEADER = "cxOrigin"; + public static final String AUTHENTICATION = "identity/connect/token"; + public static final String REVOCATION = "auth/identity/connect/revocation"; + public static final String SSO_AUTHENTICATION = "auth/identity/externalLogin"; public static final String CXPRESETS = "sast/presets"; public static final String CXTEAMS = "auth/teams"; public static final String CREATE_PROJECT = "projects";//Create new project (default preset and configuration) + public static final String CX_VERSION = "system/version"; + + public static final String CX_ARM_URL = "/Configurations/Portal"; + public static final String CX_ARM_VIOLATION = "/cxarm/policymanager/projects/{projectId}/violations?provider={provider}"; + + public static final String BROWSER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"; + + public static final String CX_REPORT_LOCATION = File.separator + "Checkmarx" + File.separator + "Reports"; - public static final String CX_ARM_URL ="/Configurations/Portal"; - public static final String CX_ARM_VIOLATION ="/cxarm/policymanager/projects/{projectId}/violations?provider={provider}"; + public static final String ORIGIN_HEADER = "cxOrigin"; + public static final String ORIGIN_URL_HEADER = "cxOriginUrl"; + public static final String CSRF_TOKEN_HEADER = "CXCSRFToken"; + public static final String PROJECT_POLICY_VIOLATED_STATUS_SAST = "Project policy status for SAST and OSA : violated"; + + public static final String PROJECT_POLICY_VIOLATED_STATUS ="Project policy status : violated"; + public static final String PROJECT_POLICY_VIOLATED_STATUS_SCA = "Project policy status for SCA : violated"; + public static final String PROJECT_POLICY_COMPLIANT_STATUS_SAST = "Project policy status for SAST and OSA : compliant"; + public static final String PROJECT_POLICY_COMPLIANT_STATUS_SCA = "Project policy status for SCA : compliant"; + public static final String DENY_NEW_PROJECT_ERROR = "Creation of the new project [{projectName}] is not authorized. " + + "Please use an existing project. \nYou can enable the creation of new projects by disabling" + "" + + " the Deny new Checkmarx projects creation checkbox in the Checkmarx plugin global settings.\n"; + public static final String TEAM_PATH = "cxTeamPath"; + public static final String SCA_GET_EXPORT_ID = "/export/requests"; + public static final String SCA_GET_SBOM_REPORT = "/export/requests?exportId={export_id}"; + } diff --git a/src/main/java/com/cx/restclient/common/ErrorUtil.java b/src/main/java/com/cx/restclient/common/ErrorUtil.java deleted file mode 100644 index 208b69e8..00000000 --- a/src/main/java/com/cx/restclient/common/ErrorUtil.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.cx.restclient.common; - -import org.apache.http.HttpStatus; - -import java.util.HashSet; -import java.util.Set; - -/** - * Created by shaulv on 6/20/2018. - */ -public class ErrorUtil { - - private ErrorUtil() { - - } - - private static Set serverErrorCodes = new HashSet<>(); - - static { - serverErrorCodes.add(HttpStatus.SC_INTERNAL_SERVER_ERROR); - serverErrorCodes.add(HttpStatus.SC_NOT_IMPLEMENTED); - serverErrorCodes.add(HttpStatus.SC_BAD_GATEWAY); - serverErrorCodes.add(HttpStatus.SC_SERVICE_UNAVAILABLE); - serverErrorCodes.add(HttpStatus.SC_GATEWAY_TIMEOUT); - serverErrorCodes.add(HttpStatus.SC_HTTP_VERSION_NOT_SUPPORTED); - serverErrorCodes.add(HttpStatus.SC_INSUFFICIENT_STORAGE); - } - - public static boolean isServerErrorCodes(int errorCodes) { - if(serverErrorCodes.contains(errorCodes)) { - return true; - } - return false; - } -} diff --git a/src/main/java/com/cx/restclient/common/Scanner.java b/src/main/java/com/cx/restclient/common/Scanner.java new file mode 100644 index 00000000..4515b06f --- /dev/null +++ b/src/main/java/com/cx/restclient/common/Scanner.java @@ -0,0 +1,23 @@ +package com.cx.restclient.common; + +import com.cx.restclient.dto.Results; +import com.cx.restclient.sast.utils.State; + +/** + * Common functionality for vulnerability scanners. + */ +public interface Scanner { + Results init(); + + Results initiateScan(); + + Results waitForScanResults(); + + Results getLatestScanResults(); + + void close(); + + default State getState() { + return State.SUCCESS; + } +} diff --git a/src/main/java/com/cx/restclient/common/ShragaUtils.java b/src/main/java/com/cx/restclient/common/ShragaUtils.java index a3a37e9c..0d80517d 100644 --- a/src/main/java/com/cx/restclient/common/ShragaUtils.java +++ b/src/main/java/com/cx/restclient/common/ShragaUtils.java @@ -1,8 +1,5 @@ package com.cx.restclient.common; -import com.cx.restclient.configuration.CxScanConfig; -import com.cx.restclient.osa.dto.OSAResults; -import com.cx.restclient.sast.dto.SASTResults; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -12,70 +9,11 @@ import java.util.List; import java.util.Map; -/** - * Created by: dorg. - * Date: 4/12/2018. - */ public abstract class ShragaUtils { - //Util methods - public static boolean isThresholdExceeded(CxScanConfig config, SASTResults sastResults, OSAResults osaResults, StringBuilder res) { - boolean thresholdExceeded = false; - if (config.isSASTThresholdEffectivelyEnabled() && sastResults != null && sastResults.isSastResultsReady()) { - thresholdExceeded = isSeverityExceeded(sastResults.getHigh(), config.getSastHighThreshold(), res, "high", "CxSAST "); - thresholdExceeded |= isSeverityExceeded(sastResults.getMedium(), config.getSastMediumThreshold(), res, "medium", "CxSAST "); - thresholdExceeded |= isSeverityExceeded(sastResults.getLow(), config.getSastLowThreshold(), res, "low", "CxSAST "); - } - if (config.isOSAThresholdEffectivelyEnabled() && osaResults != null && osaResults.isOsaResultsReady()) { - thresholdExceeded |= isSeverityExceeded(osaResults.getResults().getTotalHighVulnerabilities(), config.getOsaHighThreshold(), res, "high", "CxOSA "); - thresholdExceeded |= isSeverityExceeded(osaResults.getResults().getTotalMediumVulnerabilities(), config.getOsaMediumThreshold(), res, "medium", "CxOSA "); - thresholdExceeded |= isSeverityExceeded(osaResults.getResults().getTotalLowVulnerabilities(), config.getOsaLowThreshold(), res, "low", "CxOSA "); - } - return thresholdExceeded; - } - - public static boolean isThresholdForNewResultExceeded(CxScanConfig config, SASTResults sastResults, StringBuilder res) { - boolean exceeded = false; - - if (sastResults != null && sastResults.isSastResultsReady() && config.getSastNewResultsThresholdEnabled()) { - String severity = config.getSastNewResultsThresholdSeverity(); - - if ("LOW".equals(severity)) { - if (sastResults.getNewLow() > 0) { - res.append("One or more new results of low severity\n"); - exceeded = true; - } - severity = "MEDIUM"; - } - - if ("MEDIUM".equals(severity)) { - if (sastResults.getNewMedium() > 0) { - res.append("One or more new results of medium severity\n"); - exceeded = true; - } - severity = "HIGH"; - } - - if ("HIGH".equals(severity)) { - if (sastResults.getNewHigh() > 0) { - res.append("One or more New results of high severity\n"); - exceeded = true; - } - } - } - - return exceeded; - } - - private static boolean isSeverityExceeded(int result, Integer threshold, StringBuilder res, String severity, String severityType) { - boolean fail = false; - if (threshold != null && result > threshold) { - res.append(severityType).append(severity).append(" severity results are above threshold. Results: ").append(result).append(". Threshold: ").append(threshold).append("\n"); - fail = true; - } - return fail; - } public static Map> generateIncludesExcludesPatternLists(String folderExclusions, String filterPattern, Logger log) { + folderExclusions = removeSpaceAndNewLine(folderExclusions); + filterPattern = removeSpaceAndNewLine(filterPattern); String excludeFoldersPattern = processExcludeFolders(folderExclusions, log); String combinedPatterns = ""; @@ -93,6 +31,22 @@ public static Map> generateIncludesExcludesPatternLists(Str return convertPatternsToLists(combinedPatterns); } + public static String removeSpaceAndNewLine(String str) { + if (str != null) { + str = str.replace("\\s", "").replace("\n", "").replace("\r", "").replace("\t", ""); + StringBuilder sb = new StringBuilder(); + String[] strings = str.split(","); + for (String s : strings) { + sb.append(s.trim()); + sb.append(","); + } + + str = sb.toString(); + str = StringUtils.removeEnd(str, ","); + } + return str; + } + public static String processExcludeFolders(String folderExclusions, Logger log) { if (StringUtils.isEmpty(folderExclusions)) { return ""; @@ -112,8 +66,10 @@ public static String processExcludeFolders(String folderExclusions, Logger log) log.info("Exclude folders converted to: '" + result.toString() + "'"); return result.toString(); } + public static final String INCLUDES_LIST = "includes"; public static final String EXCLUDES_LIST = "excludes"; + public static Map> convertPatternsToLists(String filterPatterns) { filterPatterns = StringUtils.defaultString(filterPatterns); List inclusions = new ArrayList(); @@ -122,10 +78,10 @@ public static Map> convertPatternsToLists(String filterPatt for (String filter : filters) { if (StringUtils.isNotEmpty(filter)) { if (!filter.startsWith("!")) { - inclusions.add(filter); + inclusions.add(filter.trim()); } else if (filter.length() > 1) { filter = filter.substring(1); // Trim the "!" - exclusions.add(filter); + exclusions.add(filter.trim()); } } } @@ -148,4 +104,16 @@ public static String formatDate(String date, String fromFormat, String toFormat) } return ret; } -} + + public static String getTimestampSince(long startTimeSec) { + long elapsedSec = System.currentTimeMillis() / 1000 - startTimeSec; + long hours = elapsedSec / 3600; + long minutes = elapsedSec % 3600 / 60; + long seconds = elapsedSec % 60; + String hoursStr = (hours < 10) ? ("0" + hours) : (Long.toString(hours)); + String minutesStr = (minutes < 10) ? ("0" + minutes) : (Long.toString(minutes)); + String secondsStr = (seconds < 10) ? ("0" + seconds) : (Long.toString(seconds)); + return String.format("%s:%s:%s", hoursStr, minutesStr, secondsStr); + } + +} \ No newline at end of file diff --git a/src/main/java/com/cx/restclient/common/Waiter.java b/src/main/java/com/cx/restclient/common/Waiter.java index e852695f..f20a59a5 100644 --- a/src/main/java/com/cx/restclient/common/Waiter.java +++ b/src/main/java/com/cx/restclient/common/Waiter.java @@ -3,56 +3,70 @@ import com.cx.restclient.dto.BaseStatus; import com.cx.restclient.dto.Status; import com.cx.restclient.exception.CxClientException; +import org.awaitility.core.ConditionTimeoutException; import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Date; -/** - * Created by Galn on 13/02/2018. - */ -public abstract class Waiter { +public abstract class Waiter { - private int retry = 5; + public static final Logger log = LoggerFactory.getLogger(Waiter.class); + + private static final String FAILED_MSG = "Failed to get status from "; + + private int retry; private String scanType; private int sleepIntervalSec; - public Waiter(String scanType, int interval) { + public Waiter(String scanType, int interval, int retry) { this.scanType = scanType; this.sleepIntervalSec = interval; + this.retry = retry; } private long startTimeSec; - protected Status status = null; - - public T waitForTaskToFinish(String taskId, Integer scanTimeoutSec, Logger log) throws CxClientException, InterruptedException { + public T waitForTaskToFinish(String taskId, Integer scanTimeoutSec, Logger log) throws CxClientException { startTimeSec = System.currentTimeMillis() / 1000; long elapsedTimeSec = 0L; - status = Status.IN_PROGRESS; - T obj = null; - - while (status.equals(Status.IN_PROGRESS) && (scanTimeoutSec <= 0 || elapsedTimeSec < scanTimeoutSec)) { - Thread.sleep(sleepIntervalSec * 1000); - try { - obj = getStatus(taskId); - status = ((BaseStatus) obj).getBaseStatus(); - } catch (Exception e) { - log.debug("Failed to get status from " + scanType + ". retrying (" + (retry - 1) + " tries left). Error message: " + e.getMessage()); - if (retry <= 0) { - throw new CxClientException("Failed to get status from " + scanType + ". Error message: " + e.getMessage(), e); + T statusResponse = null; + final int initialReset = this.retry; + try { + do { + try { + Thread.sleep((long) sleepIntervalSec * 1000); + statusResponse = getStatus(taskId); + retry = initialReset; + } catch (IOException e) { + + log.debug(FAILED_MSG + scanType + ". retrying (" + (retry - 1) + " tries left). Error message: " + e.getMessage()); + retry--; + if (retry <= 0) { + throw new CxClientException(FAILED_MSG + scanType + ". Error message: " + e.getMessage(), e); + } + if (statusResponse == null || (statusResponse.getBaseStatus() == null)) { + statusResponse = (T) new BaseStatus(Status.SOURCE_PULLING_AND_DEPLOYMENT); + } + continue; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + if (Thread.interrupted()) { + throw new CxClientException(e.getMessage()); + } } - retry--; - continue; - } - elapsedTimeSec = (new Date()).getTime() / 1000 - startTimeSec; - printProgress(obj); + elapsedTimeSec = (new Date()).getTime() / 1000 - startTimeSec; + printProgress(statusResponse); + } while (isTaskInProgress(statusResponse) && (scanTimeoutSec <= 0 || elapsedTimeSec < scanTimeoutSec)); + if (scanTimeoutSec > 0 && scanTimeoutSec <= elapsedTimeSec) { + throw new ConditionTimeoutException("Failed to perform " + scanType + ": " + scanType + " has been automatically aborted: reached the user-specified timeout (" + scanTimeoutSec / 60 + " minutes)"); + } + } catch (CxClientException e) { + throw new CxClientException(FAILED_MSG + scanType + ". Error message: " + e.getMessage(), e); } - if (scanTimeoutSec > 0 && scanTimeoutSec <= elapsedTimeSec) { - throw new CxClientException( "Failed to perform " + scanType + ": " + scanType + " has been automatically aborted: reached the user-specified timeout (" + scanTimeoutSec / 60 + " minutes)"); - } - return resolveStatus(obj); + return resolveStatus(statusResponse); } public abstract T getStatus(String id) throws CxClientException, IOException; @@ -61,15 +75,13 @@ public T waitForTaskToFinish(String taskId, Integer scanTimeoutSec, Logger log) public abstract T resolveStatus(T status) throws CxClientException; - public Status getStatus() { - return status; - } - - public void setStatus(Status status) { - this.status = status; + public boolean isTaskInProgress(T statusResponse) { + Status status = statusResponse.getBaseStatus(); + return status.equals(Status.IN_PROGRESS); } public long getStartTimeSec() { return startTimeSec; } + } diff --git a/src/main/java/com/cx/restclient/common/summary/DependencyScanResult.java b/src/main/java/com/cx/restclient/common/summary/DependencyScanResult.java new file mode 100644 index 00000000..8727d5f0 --- /dev/null +++ b/src/main/java/com/cx/restclient/common/summary/DependencyScanResult.java @@ -0,0 +1,234 @@ +package com.cx.restclient.common.summary; + +import com.cx.restclient.dto.Results; +import com.cx.restclient.dto.ScannerType; +import com.cx.restclient.dto.scansummary.Severity; +import com.cx.restclient.osa.dto.CVEReportTableRow; +import com.cx.restclient.osa.dto.OSAResults; +import com.cx.restclient.ast.dto.sca.AstScaResults; +import com.cx.restclient.ast.dto.sca.report.Finding; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import static com.cx.restclient.common.ShragaUtils.formatDate; + +public class DependencyScanResult extends Results implements Serializable { + private ScannerType scannerType; + private boolean resultReady; + private int criticalVulnerability; + private int highVulnerability; + private int mediumVulnerability; + private int lowVulnerability; + private String summaryLink; + private int vulnerableAndOutdated; + private int nonVulnerableLibraries; + private String scanStartTime; + private String scanEndTime; + private List dependencyCriticalCVEReportTable = new ArrayList<>(); + private List dependencyHighCVEReportTable = new ArrayList<>(); + private List dependencyMediumCVEReportTable = new ArrayList<>(); + private List dependencyLowCVEReportTable = new ArrayList<>(); + private int totalLibraries; + + DependencyScanResult(){} + + DependencyScanResult(AstScaResults scaResults){ + scaResults.calculateVulnerableAndOutdatedPackages(); + this.scannerType = ScannerType.AST_SCA; + this.criticalVulnerability = scaResults.getSummary().getCriticalVulnerabilityCount(); + this.highVulnerability = scaResults.getSummary().getHighVulnerabilityCount(); + this.mediumVulnerability = scaResults.getSummary().getMediumVulnerabilityCount(); + this.lowVulnerability = scaResults.getSummary().getLowVulnerabilityCount(); + this.resultReady = scaResults.isScaResultReady(); + this.summaryLink = scaResults.getWebReportLink(); + this.vulnerableAndOutdated = scaResults.getVulnerableAndOutdated(); + this.nonVulnerableLibraries = scaResults.getNonVulnerableLibraries(); + this.scanStartTime = formatDate(scaResults.getSummary().getCreatedOn(), "yyyy-MM-dd'T'HH:mm:ss.SSSSSSS", "dd/MM/yy HH:mm"); + this.scanEndTime =""; + this.setDependencyCVEReportTableSCA(scaResults.getFindings()); + this.setTotalLibraries(scaResults.getSummary().getTotalPackages()); + } + + + DependencyScanResult(OSAResults osaResults){ + this.scannerType = ScannerType.OSA; + this.highVulnerability = osaResults.getResults().getTotalHighVulnerabilities(); + this.mediumVulnerability = osaResults.getResults().getTotalMediumVulnerabilities(); + this.lowVulnerability = osaResults.getResults().getTotalLowVulnerabilities(); + this.resultReady = osaResults.isOsaResultsReady(); + this.summaryLink = osaResults.getOsaProjectSummaryLink(); + this.vulnerableAndOutdated = osaResults.getResults().getVulnerableAndOutdated(); + this.nonVulnerableLibraries = osaResults.getResults().getNonVulnerableLibraries(); + this.scanStartTime =osaResults.getScanStartTime(); + this.scanEndTime = osaResults.getScanEndTime(); + this.setDependencyCVEReportTableOsa(osaResults.getOsaLowCVEReportTable(),osaResults.getOsaMediumCVEReportTable(),osaResults.getOsaHighCVEReportTable(),osaResults.getOsaCriticalCVEReportTable()); + this.setTotalLibraries(osaResults.getResults().getTotalLibraries()); + } + + public void setDependencyCVEReportTableOsa(List osaCVEResultsLow,List osaCVEResultsMedium,List osaCVEResultsHigh, List osaCVEResultsCritical){ + CVEReportTableRow row; + for(CVEReportTableRow lowCVE :osaCVEResultsLow ){ + row = lowCVE; + this.dependencyLowCVEReportTable.add(row); + } + for(CVEReportTableRow mediumCVE :osaCVEResultsMedium ){ + row = mediumCVE; + this.dependencyMediumCVEReportTable.add(row); + } + for(CVEReportTableRow highCVE :osaCVEResultsHigh ){ + row = highCVE; + this.dependencyHighCVEReportTable.add(row); + } + for(CVEReportTableRow criticalCVE :osaCVEResultsCritical ){ + row = criticalCVE; + this.dependencyCriticalCVEReportTable.add(row); + } + } + + public void setDependencyCVEReportTableSCA(List scaFindings){ + CVEReportTableRow row; + for(Finding scaFinding :scaFindings ){ + row =new CVEReportTableRow(scaFinding); + if(scaFinding.getSeverity() == Severity.LOW){ + this.dependencyLowCVEReportTable.add(row); + }else if(scaFinding.getSeverity() == Severity.MEDIUM){ + this.dependencyMediumCVEReportTable.add(row); + }else if(scaFinding.getSeverity() == Severity.HIGH){ + this.dependencyHighCVEReportTable.add(row); + }else if(scaFinding.getSeverity() == Severity.CRITICAL){ + this.dependencyCriticalCVEReportTable.add(row); + } + } + } + + public ScannerType getScannerType() { + return scannerType; + } + + public void setScannerType(ScannerType scannerType) { + this.scannerType = scannerType; + } + + public boolean isResultReady() { + return resultReady; + } + + public void setResultReady(boolean resultReady) { + this.resultReady = resultReady; + } + + public int getCriticalVulnerability() { + return criticalVulnerability; + } + + public void setCriticalVulnerability(int criticalVulnerability) { + this.criticalVulnerability = criticalVulnerability; + } + + public int getHighVulnerability() { + return highVulnerability; + } + + public void setHighVulnerability(int highVulnerability) { + this.highVulnerability = highVulnerability; + } + + public int getMediumVulnerability() { + return mediumVulnerability; + } + + public void setMediumVulnerability(int mediumVulnerability) { + this.mediumVulnerability = mediumVulnerability; + } + + public int getLowVulnerability() { + return lowVulnerability; + } + + public void setLowVulnerability(int lowVulnerability) { + this.lowVulnerability = lowVulnerability; + } + + public String getSummaryLink() { + return summaryLink; + } + + public void setSummaryLink(String summaryLink) { + this.summaryLink = summaryLink; + } + + public int getVulnerableAndOutdated() { + return vulnerableAndOutdated; + } + + public void setVulnerableAndOutdated(int vulnerableAndOutdated) { + this.vulnerableAndOutdated = vulnerableAndOutdated; + } + + public int getNonVulnerableLibraries() { + return nonVulnerableLibraries; + } + + public void setNonVulnerableLibraries(int nonVulnerableLibraries) { + this.nonVulnerableLibraries = nonVulnerableLibraries; + } + + public String getScanStartTime() { + return scanStartTime; + } + + public void setScanStartTime(String scanStartTime) { + this.scanStartTime = scanStartTime; + } + + public String getScanEndTime() { + return scanEndTime; + } + + public void setScanEndTime(String scanEndTime) { + this.scanEndTime = scanEndTime; + } + + public List getDependencyCriticalCVEReportTable() { + return dependencyCriticalCVEReportTable; + } + + public void setDependencyCriticalCVEReportTable(List dependencyCriticalCVEReportTable) { + this.dependencyCriticalCVEReportTable = dependencyCriticalCVEReportTable; + } + + public List getDependencyHighCVEReportTable() { + return dependencyHighCVEReportTable; + } + + public void setDependencyHighCVEReportTable(List dependencyHighCVEReportTable) { + this.dependencyHighCVEReportTable = dependencyHighCVEReportTable; + } + + public List getDependencyMediumCVEReportTable() { + return dependencyMediumCVEReportTable; + } + + public void setDependencyMediumCVEReportTable(List dependencyMediumCVEReportTable) { + this.dependencyMediumCVEReportTable = dependencyMediumCVEReportTable; + } + + public List getDependencyLowCVEReportTable() { + return dependencyLowCVEReportTable; + } + + public void setDependencyLowCVEReportTable(List dependencyLowCVEReportTable) { + this.dependencyLowCVEReportTable = dependencyLowCVEReportTable; + } + + public int getTotalLibraries() { + return totalLibraries; + } + + public void setTotalLibraries(int totalLibraries) { + this.totalLibraries = totalLibraries; + } + +} diff --git a/src/main/java/com/cx/restclient/common/summary/SummaryUtils.java b/src/main/java/com/cx/restclient/common/summary/SummaryUtils.java index e7fd16c2..dfa84844 100644 --- a/src/main/java/com/cx/restclient/common/summary/SummaryUtils.java +++ b/src/main/java/com/cx/restclient/common/summary/SummaryUtils.java @@ -1,10 +1,14 @@ package com.cx.restclient.common.summary; -import com.cx.restclient.common.ShragaUtils; import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.cxArm.dto.Policy; +import com.cx.restclient.dto.ScannerType; +import com.cx.restclient.dto.scansummary.ScanSummary; import com.cx.restclient.osa.dto.OSAResults; -import com.cx.restclient.osa.dto.OSASummaryResults; import com.cx.restclient.sast.dto.SASTResults; +import com.cx.restclient.ast.dto.sca.AstScaResults; +import com.cx.restclient.ast.dto.sca.report.PolicyEvaluation; + import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; @@ -14,93 +18,248 @@ import java.io.StringWriter; import java.util.HashMap; import java.util.Map; +import java.util.stream.Collectors; public abstract class SummaryUtils { + private SummaryUtils() { + } - public static String generateSummary(SASTResults sastResults, OSAResults osaResults, CxScanConfig config) throws IOException, TemplateException { + public static String generateSummary(SASTResults sastResults, OSAResults osaResults, AstScaResults scaResults, CxScanConfig config) throws IOException, TemplateException { Configuration cfg = new Configuration(new Version("2.3.23")); - cfg.setClassForTemplateLoading(SummaryUtils.class, "/com/cx/report/"); + cfg.setClassForTemplateLoading(SummaryUtils.class, "/com/cx/report"); Template template = cfg.getTemplate("report.ftl"); - Map templateData = new HashMap(); + Map templateData = new HashMap<>(); templateData.put("config", config); - templateData.put("sast", sastResults); - templateData.put("osa", osaResults); + templateData.put("sast", sastResults != null ? sastResults : new SASTResults()); + + // TODO: null value for "osa" should be handled inside the template. + templateData.put("osa", osaResults != null ? osaResults : new OSAResults()); + templateData.put("sca", scaResults != null ? scaResults : new AstScaResults()); + + DependencyScanResult dependencyScanResult = resolveDependencyResult(osaResults, scaResults); + + if(dependencyScanResult == null) { + dependencyScanResult = new DependencyScanResult(); + if(config.isAstScaEnabled()) + dependencyScanResult.setScannerType(ScannerType.AST_SCA); + else if(config.isOsaEnabled()) + dependencyScanResult.setScannerType(ScannerType.OSA); + } + + templateData.put("dependencyResult", dependencyScanResult); + + + ScanSummary scanSummary = new ScanSummary(config, sastResults, osaResults, scaResults); //calculated params: boolean buildFailed = false; boolean policyViolated = false; + int policyViolatedCount=0; //sast: - if (config.getSastEnabled() && sastResults.isSastResultsReady()) { - boolean sastThresholdExceeded = ShragaUtils.isThresholdExceeded(config, sastResults, null, new StringBuilder()); - boolean sastNewResultsExceeded = ShragaUtils.isThresholdForNewResultExceeded(config, sastResults, new StringBuilder()); - templateData.put("sastThresholdExceeded", sastThresholdExceeded); - templateData.put("sastNewResultsExceeded", sastNewResultsExceeded); - buildFailed = sastThresholdExceeded || sastNewResultsExceeded; - //calculate sast bars: - float maxCount = Math.max(sastResults.getHigh(), Math.max(sastResults.getMedium(), sastResults.getLow())); - float sastBarNorm = maxCount * 10f / 9f; - - //sast high bars - float sastHighTotalHeight = (float) sastResults.getHigh() / sastBarNorm * 238f; - float sastHighNewHeight = calculateNewBarHeight(sastResults.getNewHigh(), sastResults.getHigh(), sastHighTotalHeight); - float sastHighRecurrentHeight = sastHighTotalHeight - sastHighNewHeight; - templateData.put("sastHighTotalHeight", sastHighTotalHeight); - templateData.put("sastHighNewHeight", sastHighNewHeight); - templateData.put("sastHighRecurrentHeight", sastHighRecurrentHeight); - /*if (config.getEnablePolicyViolations() && !sastResults.getSastViolations().isEmpty()){ - policyViolated = true; - }*/ - - //sast medium bars - float sastMediumTotalHeight = (float) sastResults.getMedium() / sastBarNorm * 238f; - float sastMediumNewHeight = calculateNewBarHeight(sastResults.getNewMedium(), sastResults.getMedium(), sastMediumTotalHeight); - float sastMediumRecurrentHeight = sastMediumTotalHeight - sastMediumNewHeight; - templateData.put("sastMediumTotalHeight", sastMediumTotalHeight); - templateData.put("sastMediumNewHeight", sastMediumNewHeight); - templateData.put("sastMediumRecurrentHeight", sastMediumRecurrentHeight); - - //sast low bars - float sastLowTotalHeight = (float) sastResults.getLow() / sastBarNorm * 238f; - float sastLowNewHeight = calculateNewBarHeight(sastResults.getNewLow(), sastResults.getLow(), sastLowTotalHeight); - float sastLowRecurrentHeight = sastLowTotalHeight - sastLowNewHeight; - templateData.put("sastLowTotalHeight", sastLowTotalHeight); - templateData.put("sastLowNewHeight", sastLowNewHeight); - templateData.put("sastLowRecurrentHeight", sastLowRecurrentHeight); + if (config.isSastEnabled()) { + if (sastResults != null && sastResults.isSastResultsReady()) { + boolean sastThresholdExceeded = scanSummary.isSastThresholdExceeded(); + boolean sastNewResultsExceeded = scanSummary.isSastThresholdForNewResultsExceeded(); + templateData.put("sastThresholdExceeded", sastThresholdExceeded); + templateData.put("sastNewResultsExceeded", sastNewResultsExceeded); + buildFailed = sastThresholdExceeded || sastNewResultsExceeded; + //calculate sast bars: + float maxCount = Math.max(sastResults.getHigh(), Math.max(sastResults.getMedium(), sastResults.getLow())); + float sastBarNorm = maxCount * 10f / 9f; + + //sast critical bars + float sastCriticalTotalHeight = (float) sastResults.getCritical() / sastBarNorm * 238f; + float sastCriticalNewHeight = calculateNewBarHeight(sastResults.getNewCritical(), sastResults.getCritical(), sastCriticalTotalHeight); + float sastCriticalRecurrentHeight = sastCriticalTotalHeight - sastCriticalNewHeight; + templateData.put("sastCriticalTotalHeight", sastCriticalTotalHeight); + templateData.put("sastCriticalNewHeight", sastCriticalNewHeight); + templateData.put("sastCriticalRecurrentHeight", sastCriticalRecurrentHeight); + + //sast high bars + float sastHighTotalHeight = (float) sastResults.getHigh() / sastBarNorm * 238f; + float sastHighNewHeight = calculateNewBarHeight(sastResults.getNewHigh(), sastResults.getHigh(), sastHighTotalHeight); + float sastHighRecurrentHeight = sastHighTotalHeight - sastHighNewHeight; + templateData.put("sastHighTotalHeight", sastHighTotalHeight); + templateData.put("sastHighNewHeight", sastHighNewHeight); + templateData.put("sastHighRecurrentHeight", sastHighRecurrentHeight); + + //sast medium bars + float sastMediumTotalHeight = (float) sastResults.getMedium() / sastBarNorm * 238f; + float sastMediumNewHeight = calculateNewBarHeight(sastResults.getNewMedium(), sastResults.getMedium(), sastMediumTotalHeight); + float sastMediumRecurrentHeight = sastMediumTotalHeight - sastMediumNewHeight; + templateData.put("sastMediumTotalHeight", sastMediumTotalHeight); + templateData.put("sastMediumNewHeight", sastMediumNewHeight); + templateData.put("sastMediumRecurrentHeight", sastMediumRecurrentHeight); + + //sast low bars + float sastLowTotalHeight = (float) sastResults.getLow() / sastBarNorm * 238f; + float sastLowNewHeight = calculateNewBarHeight(sastResults.getNewLow(), sastResults.getLow(), sastLowTotalHeight); + float sastLowRecurrentHeight = sastLowTotalHeight - sastLowNewHeight; + templateData.put("sastLowTotalHeight", sastLowTotalHeight); + templateData.put("sastLowNewHeight", sastLowNewHeight); + templateData.put("sastLowRecurrentHeight", sastLowRecurrentHeight); + + } else { + buildFailed = true; + } } +/* //osa: - if (config.getOsaEnabled() && osaResults.isOsaResultsReady()) { - boolean osaThresholdExceeded = ShragaUtils.isThresholdExceeded(config, null, osaResults, new StringBuilder()); - templateData.put("osaThresholdExceeded", osaThresholdExceeded); - buildFailed |= osaThresholdExceeded; - if (config.getEnablePolicyViolations() && !osaResults.getOsaViolations().isEmpty()){ + if (config.getDependencyScannerType() == DependencyScannerType.OSA) { + if (osaResults!=null && osaResults.isOsaResultsReady()) { + boolean thresholdExceeded = scanSummary.isOsaThresholdExceeded(); + templateData.put("osaThresholdExceeded", thresholdExceeded); + buildFailed |= thresholdExceeded; + + //calculate osa bars: + OSASummaryResults osaSummaryResults = osaResults.getResults(); + int osaHigh = osaSummaryResults.getTotalHighVulnerabilities(); + int osaMedium = osaSummaryResults.getTotalMediumVulnerabilities(); + int osaLow = osaSummaryResults.getTotalLowVulnerabilities(); + float osaMaxCount = Math.max(osaHigh, Math.max(osaMedium, osaLow)); + float osaBarNorm = osaMaxCount * 10f / 9f; + + float osaHighTotalHeight = (float) osaHigh / osaBarNorm * 238f; + float osaMediumTotalHeight = (float) osaMedium / osaBarNorm * 238f; + float osaLowTotalHeight = (float) osaLow / osaBarNorm * 238f; + + templateData.put("osaHighTotalHeight", osaHighTotalHeight); + templateData.put("osaMediumTotalHeight", osaMediumTotalHeight); + templateData.put("osaLowTotalHeight", osaLowTotalHeight); + } else { + buildFailed = true; + } + } else if (config.getDependencyScannerType() == DependencyScannerType.SCA){ + boolean thresholdExceeded = scanSummary.isOsaThresholdExceeded(); + templateData.put("scaThresholdExceeded", thresholdExceeded); + buildFailed |= thresholdExceeded; + + //calculate sca bars: + AstScaSummaryResults scaSummaryResults = scaResults.getSummary(); + int scaHigh = scaSummaryResults.getHighVulnerabilityCount(); + int scaMedium = scaSummaryResults.getMediumVulnerabilityCount(); + int scaLow = scaSummaryResults.getLowVulnerabilityCount(); + float scaMaxCount = Math.max(scaHigh, Math.max(scaMedium, scaLow)); + float scaBarNorm = scaMaxCount * 10f / 9f; + + float scaHighTotalHeight = (float) scaHigh / scaBarNorm * 238f; + float scaMediumTotalHeight = (float) scaMedium / scaBarNorm * 238f; + float scaLowTotalHeight = (float) scaLow / scaBarNorm * 238f; + + templateData.put("scaHighTotalHeight", scaHighTotalHeight); + templateData.put("scaMediumTotalHeight", scaMediumTotalHeight); + templateData.put("scaLowTotalHeight", scaLowTotalHeight); + }else{ + buildFailed = true; + } +*/ + + if (config.isOsaEnabled() || config.isAstScaEnabled()) { + if (dependencyScanResult != null && dependencyScanResult.isResultReady()) { + boolean thresholdExceeded = scanSummary.isOsaThresholdExceeded(); + templateData.put("dependencyThresholdExceeded", thresholdExceeded); + if (config.isSastEnabled()) { + buildFailed |= thresholdExceeded || buildFailed; + } else { + buildFailed |= thresholdExceeded; + } + + //calculate dependency results bars: + int dependencyCritical = dependencyScanResult.getCriticalVulnerability(); + int dependencyHigh = dependencyScanResult.getHighVulnerability(); + int dependencyMedium = dependencyScanResult.getMediumVulnerability(); + int dependencyLow = dependencyScanResult.getLowVulnerability(); + float dependencyMaxCount = Math.max(dependencyHigh, Math.max(dependencyMedium, dependencyLow)); + float dependencyBarNorm = dependencyMaxCount * 10f / 9f; + + + float dependencyCriticalTotalHeight = (float) dependencyCritical / dependencyBarNorm * 238f; + float dependencyHighTotalHeight = (float) dependencyHigh / dependencyBarNorm * 238f; + float dependencyMediumTotalHeight = (float) dependencyMedium / dependencyBarNorm * 238f; + float dependencyLowTotalHeight = (float) dependencyLow / dependencyBarNorm * 238f; + + templateData.put("dependencyCriticalTotalHeight", dependencyCriticalTotalHeight); + templateData.put("dependencyHighTotalHeight", dependencyHighTotalHeight); + templateData.put("dependencyMediumTotalHeight", dependencyMediumTotalHeight); + templateData.put("dependencyLowTotalHeight", dependencyLowTotalHeight); + } else { + buildFailed = true; + } + } + + + if ((config.isSastEnabled()|| config.isOsaEnabled() )&& config.getEnablePolicyViolations()) { + Map policies = new HashMap<>(); + + + if (Boolean.TRUE.equals(config.isSastEnabled()) + && sastResults != null + && sastResults.getSastPolicies() != null + && !sastResults.getSastPolicies().isEmpty()) { + policyViolated = true; + policies.putAll(sastResults.getSastPolicies().stream().collect( + Collectors.toMap(Policy::getPolicyName, Policy::getRuleName, + (left, right) -> left))); + } + + if (Boolean.TRUE.equals(config.isOsaEnabled()) + && osaResults != null + && osaResults.getOsaPolicies() != null + && !osaResults.getOsaPolicies().isEmpty()) { policyViolated = true; + policies.putAll(osaResults.getOsaPolicies().stream().collect( + Collectors.toMap(Policy::getPolicyName, Policy::getRuleName, + (left, right) -> left))); + } + + if(scanSummary.isPolicyViolated()) { + buildFailed = true; + policyViolated = true; } - //calculate osa bars: - OSASummaryResults osaSummaryResults = osaResults.getResults(); - int osaHigh = osaSummaryResults.getTotalHighVulnerabilities(); - int osaMedium = osaSummaryResults.getTotalMediumVulnerabilities(); - int osaLow = osaSummaryResults.getTotalLowVulnerabilities(); - float osaMaxCount = Math.max(osaHigh, Math.max(osaMedium, osaLow)); - float osaBarNorm = osaMaxCount * 10f / 9f; - - float osaHighTotalHeight = (float) osaHigh / osaBarNorm * 238f; - float osaMediumTotalHeight = (float) osaMedium / osaBarNorm * 238f; - float osaLowTotalHeight = (float) osaLow / osaBarNorm * 238f; - - templateData.put("osaHighTotalHeight", osaHighTotalHeight); - templateData.put("osaMediumTotalHeight", osaMediumTotalHeight); - templateData.put("osaLowTotalHeight", osaLowTotalHeight); + policyViolatedCount = policies.size(); + String policyLabel = policyViolatedCount == 1 ? "Policy" : "Policies"; + templateData.put("policyLabel", policyLabel); + + templateData.put("policyViolatedCount", policyViolatedCount); } - String policyLabel = osaResults.getOsaPolicies().size() == 1? "Policy": "Policies"; - templateData.put("policyLabel", policyLabel); + if (config.isAstScaEnabled() && config.getEnablePolicyViolationsSCA()) { + Map policies = new HashMap<>(); + if(Boolean.TRUE.equals(config.isAstScaEnabled()) + && scaResults != null && scaResults.getPolicyEvaluations() != null + && !scaResults.getPolicyEvaluations().isEmpty()) + { + policies.putAll(scaResults.getPolicyEvaluations().stream().filter(policy -> policy.getIsViolated()).collect( + Collectors.toMap(PolicyEvaluation::getName, PolicyEvaluation::getId, + (left, right) -> left))); + if(!policyViolated && policies.size()==0) + { + policyViolated = false; + } + else + { + policyViolated = true; + } + } + + + if(scanSummary.isPolicyViolated()) { + buildFailed = true; + policyViolated = true; + } + policyViolatedCount = policyViolatedCount+policies.size(); + String policyLabel = policyViolatedCount == 1 ? "Policy" : "Policies"; + templateData.put("policyLabel", policyLabel); + + templateData.put("policyViolatedCount", policyViolatedCount); + } templateData.put("policyViolated", policyViolated); - buildFailed |= policyViolated; + buildFailed = buildFailed || policyViolated; templateData.put("buildFailed", buildFailed); //generate the report: @@ -109,6 +268,18 @@ public static String generateSummary(SASTResults sastResults, OSAResults osaResu return writer.toString(); } + private static DependencyScanResult resolveDependencyResult(OSAResults osaResults, AstScaResults scaResults) { + DependencyScanResult dependencyScanResult; + if (osaResults != null) { + dependencyScanResult = new DependencyScanResult(osaResults); + } else if (scaResults != null) { + dependencyScanResult = new DependencyScanResult(scaResults); + } else { + dependencyScanResult = null; + } + return dependencyScanResult; + } + private static float calculateNewBarHeight(int newCount, int count, float totalHeight) { int minimalVisibilityHeight = 5; //new high diff --git a/src/main/java/com/cx/restclient/configuration/CxScanConfig.java b/src/main/java/com/cx/restclient/configuration/CxScanConfig.java index 7e70e169..c9858d24 100644 --- a/src/main/java/com/cx/restclient/configuration/CxScanConfig.java +++ b/src/main/java/com/cx/restclient/configuration/CxScanConfig.java @@ -1,65 +1,187 @@ package com.cx.restclient.configuration; +import com.cx.restclient.ast.dto.sast.AstSastConfig; +import com.cx.restclient.ast.dto.sca.AstScaConfig; +import com.cx.restclient.dto.*; +import com.cx.restclient.sast.dto.ReportType; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.cookie.Cookie; import java.io.File; import java.io.Serializable; -import java.util.Properties; +import java.util.*; /** * Created by galn on 21/12/2016. */ public class CxScanConfig implements Serializable { - private Boolean sastEnabled = false; - private Boolean osaEnabled = false; - private String cxOrigin; + private String cxOriginUrl; + private CxVersion cxVersion; + private boolean showCriticalLabel = false; + + + public boolean isShowCriticalLabel() { + return showCriticalLabel; + } + + public void setShowCriticalLabel(boolean showCriticalLabel) { + this.showCriticalLabel = showCriticalLabel; + } + + private Integer projectRetentionRate; + private boolean enableDataRetention; private boolean disableCertificateValidation = false; + private boolean useSSOLogin = false; + private String sourceDir; + private String osaLocationPath; private File reportsDir; + // Map / (e.g. PDF to its file path) + private Map reports = new HashMap<>(); private String username; private String password; + private String refreshToken; private String url; private String projectName; private String teamPath; + private String mvnPath; private String teamId; private Boolean denyProject = false; + private Boolean hideResults = false; + private Boolean continueBuild = false; private Boolean isPublic = true; private Boolean forceScan = false; private String presetName; private Integer presetId; + private String postScanName; private String sastFolderExclusions; private String sastFilterPattern; private Integer sastScanTimeoutInMinutes; + private Integer scaScanTimeoutInMinutes; + private Integer osaScanTimeoutInMinutes; private String scanComment; private Boolean isIncremental = false; private Boolean isSynchronous = false; private Boolean sastThresholdsEnabled = false; + private Boolean sastEnableCriticalSeverity = false; + private Integer sastCriticalThreshold; private Integer sastHighThreshold; private Integer sastMediumThreshold; private Integer sastLowThreshold; private Boolean sastNewResultsThresholdEnabled = false; private String sastNewResultsThresholdSeverity; - + private TokenLoginResponse token; private Boolean generatePDFReport = false; private File zipFile; - private Integer engineConfigurationId = 1; + private Integer engineConfigurationId; + private String engineConfigurationName; + private String projectCustomFields; + private boolean ignoreBenignErrors = false; + private String pluginVersion; + + public String getPluginVersion() { + return pluginVersion; + } + + public void setPluginVersion(String pluginVersion) { + this.pluginVersion = pluginVersion; + } + + private String osaFolderExclusions; + public String getEngineConfigurationName() { + return engineConfigurationName; + } + + public void setEngineConfigurationName(String engineConfigurationName) { + this.engineConfigurationName = engineConfigurationName; + } + + public String getprojectCustomFields() { + return projectCustomFields; + } + + public void setprojectCustomFields(String projectCustomFields) { + this.projectCustomFields = projectCustomFields; + } - private String osaFolderExclusions; private String osaFilterPattern; private String osaArchiveIncludePatterns; private Boolean osaGenerateJsonReport = true; private Boolean osaRunInstall = false; private Boolean osaThresholdsEnabled = false; - private Integer osaHighThreshold; + private Boolean osaFailOnError = false; + private Integer osaCriticalThreshold; + private Integer osaHighThreshold; private Integer osaMediumThreshold; private Integer osaLowThreshold; private Properties osaFsaConfig; //for MAVEN private String osaDependenciesJson; - + private Boolean avoidDuplicateProjectScans = false; private boolean enablePolicyViolations = false; + private boolean enablePolicyViolationsSCA = false; + private Boolean generateXmlReport = true; private String cxARMUrl; + private String[] paths; + //remote source control + private RemoteSourceTypes remoteType = null; + private String remoteSrcUser; + private String remoteSrcPass; + private String remoteSrcUrl; + private int remoteSrcPort; + private byte[] remoteSrcKeyFile; + private String remoteSrcBranch; + private String perforceMode; + + // CLI config properties + private Integer progressInterval; + private Integer osaProgressInterval; + private Integer connectionRetries; + private String osaScanDepth; + private Integer maxZipSize; + private String defaultProjectName; + + private String scaJsonReport; + + private AstScaConfig astScaConfig; + private AstSastConfig astSastConfig; + + private final Set scannerTypes = new HashSet<>(); + private final List sessionCookies = new ArrayList<>(); + private Boolean isProxy = true; + private Boolean isScaProxy = false; + private ProxyConfig proxyConfig; + private ProxyConfig proxyScaConfig; + private Boolean useNTLM = false; + private boolean generateScaReport = false; + private boolean hasScaReportFormat = false; + private String scaReportFormat; + + private Integer postScanActionId; + + private String customFields; + + private String projectLevelCustomFields; + + private boolean isOverrideProjectSetting = false; + + public boolean isOverrideRetentionRate() { + return isOverrideRetentionRate; + } + + public void setOverrideRetentionRate(boolean overrideRetentionRate) { + isOverrideRetentionRate = overrideRetentionRate; + } + + private boolean isOverrideRetentionRate = false; + + private Boolean enableSastBranching = false; + + private String masterBranchProjName; + + private Integer copyBranchTimeOutInSeconds; public CxScanConfig() { } @@ -72,30 +194,63 @@ public CxScanConfig(String url, String username, String password, String cxOrigi this.disableCertificateValidation = disableCertificateValidation; } - public Boolean getSastEnabled() { - return sastEnabled; + public CxScanConfig(String url, String username, String password, String cxOrigin, String cxOriginUrl, boolean disableCertificateValidation) { + this.url = url; + this.username = username; + this.password = password; + this.cxOrigin = cxOrigin; + this.cxOriginUrl = cxOriginUrl; + this.disableCertificateValidation = disableCertificateValidation; + } + + + public CxScanConfig(String url, String refreshToken, String cxOrigin, boolean disableCertificateValidation) { + this.url = url; + this.refreshToken = refreshToken; + this.cxOrigin = cxOrigin; + this.disableCertificateValidation = disableCertificateValidation; + } + + public boolean isSastEnabled() { + return scannerTypes.contains(ScannerType.SAST); + } + + public boolean isOsaEnabled() { + return scannerTypes.contains(ScannerType.OSA); } - public void setSastEnabled(Boolean sastEnabled) { - this.sastEnabled = sastEnabled; + public boolean isAstScaEnabled() { + return scannerTypes.contains(ScannerType.AST_SCA); } - public Boolean getOsaEnabled() { - return osaEnabled; + public boolean isAstSastEnabled() { + return scannerTypes.contains(ScannerType.AST_SAST); } - public void setOsaEnabled(Boolean osaEnabled) { - this.osaEnabled = osaEnabled; + public void setSastEnabled(boolean sastEnabled) { + if (sastEnabled) { + scannerTypes.add(ScannerType.SAST); + } else { + scannerTypes.remove(ScannerType.SAST); + } } public String getCxOrigin() { return cxOrigin; } + public String getCxOriginUrl() { + return cxOriginUrl; + } + public void setCxOrigin(String cxOrigin) { this.cxOrigin = cxOrigin; } + public void setCxOriginUrl(String cxOriginUrl) { + this.cxOriginUrl = cxOriginUrl; + } + public boolean isDisableCertificateValidation() { return disableCertificateValidation; } @@ -104,6 +259,25 @@ public void setDisableCertificateValidation(boolean disableCertificateValidation this.disableCertificateValidation = disableCertificateValidation; } + public boolean isUseSSOLogin() { + return useSSOLogin; + } + + public void setUseSSOLogin(boolean useSSOLogin) { + this.useSSOLogin = useSSOLogin; + } + + public Boolean getAvoidDuplicateProjectScans() { + return avoidDuplicateProjectScans; + } + public boolean isEnableDataRetention() { + return enableDataRetention; + } + + public void setEnableDataRetention(boolean enableDataRetention) { + this.enableDataRetention = enableDataRetention; + } + public String getSourceDir() { return sourceDir; } @@ -112,6 +286,34 @@ public void setSourceDir(String sourceDir) { this.sourceDir = sourceDir; } + public String getCustomFields() { + return customFields; + } + + public void setCustomFields(String customFields) { + this.customFields = customFields; + } + + public String getProjectLevelCustomFields() { + return projectLevelCustomFields; + } + + public void setProjectLevelCustomFields(String projectLevelCustomFields) { + this.projectLevelCustomFields = projectLevelCustomFields; + } + + public String getOsaLocationPath() { + return osaLocationPath; + } + + public void setOsaLocationPath(String osaLocationPath) { + this.osaLocationPath = osaLocationPath; + } + + public String getEffectiveSourceDirForDependencyScan() { + return osaLocationPath != null ? osaLocationPath : sourceDir; + } + public File getReportsDir() { return reportsDir; } @@ -128,6 +330,14 @@ public void setUsername(String username) { this.username = username; } + public void setRefreshToken(String token) { + this.refreshToken = token; + } + + public String getRefreshToken() { + return refreshToken; + } + public String getPassword() { return password; } @@ -140,6 +350,14 @@ public String getUrl() { return url; } + public String getScaJsonReport() { + return scaJsonReport; + } + + public void setScaJsonReport(String scaJsonReport) { + this.scaJsonReport = scaJsonReport; + } + public void setUrl(String url) { this.url = url; } @@ -157,6 +375,13 @@ public String getTeamPath() { } public void setTeamPath(String teamPath) { + //Make teampath always in the form /CxServer/Team1. User might have used '\' in the path. + if (!StringUtils.isEmpty(teamPath) && !teamPath.startsWith("\\") && !teamPath.startsWith(("/"))) { + teamPath = "/" + teamPath; + } + if (!StringUtils.isEmpty(teamPath) && teamPath != null) { + teamPath = teamPath.replace("\\", "/"); + } this.teamPath = teamPath; } @@ -211,10 +436,24 @@ public void setPresetId(Integer presetId) { public String getSastFolderExclusions() { return sastFolderExclusions; } + + public String getPostScanName() { + return postScanName; + } + public void setPostScanName(String postScanName) { + this.postScanName = postScanName; + } public void setSastFolderExclusions(String sastFolderExclusions) { this.sastFolderExclusions = sastFolderExclusions; } + public Integer getProjectRetentionRate() { + return projectRetentionRate; + } + + public void setProjectRetentionRate(Integer projectRetentionRate) { + this.projectRetentionRate = projectRetentionRate; + } public String getSastFilterPattern() { return sastFilterPattern; @@ -227,11 +466,28 @@ public void setSastFilterPattern(String sastFilterPattern) { public Integer getSastScanTimeoutInMinutes() { return sastScanTimeoutInMinutes == null ? -1 : sastScanTimeoutInMinutes; } + + public Integer getSCAScanTimeoutInMinutes() { + return scaScanTimeoutInMinutes == null ? -1 : scaScanTimeoutInMinutes; + } + + public void setSCAScanTimeoutInMinutes(Integer scaScanTimeoutInMinutes) { + this.scaScanTimeoutInMinutes = scaScanTimeoutInMinutes; + } + public void setSastScanTimeoutInMinutes(Integer sastScanTimeoutInMinutes) { this.sastScanTimeoutInMinutes = sastScanTimeoutInMinutes; } + public Integer getOsaScanTimeoutInMinutes() { + return osaScanTimeoutInMinutes == null ? -1 : osaScanTimeoutInMinutes; + } + + public void setOsaScanTimeoutInMinutes(Integer sastOsaScanTimeoutInMinutes) { + this.osaScanTimeoutInMinutes = sastOsaScanTimeoutInMinutes; + } + public String getScanComment() { return scanComment; } @@ -255,6 +511,15 @@ public Boolean getSynchronous() { public void setSynchronous(Boolean synchronous) { this.isSynchronous = synchronous; } + + public Boolean getSastEnableCriticalSeverity() { + return sastEnableCriticalSeverity; + } + + public void setSastEnableCriticalSeverity(Boolean sastEnableCriticalSeverity) { + this.sastEnableCriticalSeverity = sastEnableCriticalSeverity; + } + public Boolean getSastThresholdsEnabled() { return sastThresholdsEnabled; @@ -263,6 +528,14 @@ public Boolean getSastThresholdsEnabled() { public void setSastThresholdsEnabled(Boolean sastThresholdsEnabled) { this.sastThresholdsEnabled = sastThresholdsEnabled; } + + public Integer getSastCriticalThreshold() { + return sastCriticalThreshold; + } + + public void setSastCriticalThreshold(Integer sastCriticalThreshold) { + this.sastCriticalThreshold = sastCriticalThreshold; + } public Integer getSastHighThreshold() { return sastHighThreshold; @@ -368,6 +641,22 @@ public void setOsaThresholdsEnabled(Boolean osaThresholdsEnabled) { this.osaThresholdsEnabled = osaThresholdsEnabled; } + public Boolean isOsaFailOnError() { + return osaFailOnError; + } + + public void setOsaFailOnError(Boolean osaFailOnError) { + this.osaFailOnError = osaFailOnError; + } + + public Integer getOsaCriticalThreshold() { + return osaCriticalThreshold; + } + + public void setOsaCriticalThreshold(Integer osaCriticalThreshold) { + this.osaCriticalThreshold = osaCriticalThreshold; + } + public Integer getOsaHighThreshold() { return osaHighThreshold; } @@ -405,11 +694,15 @@ public String getOsaDependenciesJson() { } public boolean isSASTThresholdEffectivelyEnabled() { - return getSastEnabled() && getSastThresholdsEnabled() && (getSastHighThreshold() != null || getSastMediumThreshold() != null || getSastLowThreshold() != null); + + return isSastEnabled() && getSastThresholdsEnabled() && (getSastCriticalThreshold() != null || getSastHighThreshold() != null || getSastMediumThreshold() != null || getSastLowThreshold() != null ); + } public boolean isOSAThresholdEffectivelyEnabled() { - return getOsaEnabled() && getOsaThresholdsEnabled() && (getOsaHighThreshold() != null || getOsaMediumThreshold() != null || getOsaLowThreshold() != null); + return (isOsaEnabled() || isAstScaEnabled()) && + getOsaThresholdsEnabled() && + (getOsaCriticalThreshold() != null || getOsaHighThreshold() != null || getOsaMediumThreshold() != null || getOsaLowThreshold() != null); } public void setOsaDependenciesJson(String osaDependenciesJson) { @@ -428,14 +721,34 @@ public boolean getEnablePolicyViolations() { return enablePolicyViolations; } + public boolean getEnablePolicyViolationsSCA() { + return enablePolicyViolationsSCA; + } + public void setEnablePolicyViolations(boolean enablePolicyViolations) { this.enablePolicyViolations = enablePolicyViolations; } + public void setEnablePolicyViolationsSCA(boolean enablePolicyViolationsSCA) { + this.enablePolicyViolationsSCA = enablePolicyViolationsSCA; + } + public boolean isEnablePolicyViolations() { return enablePolicyViolations; } + public boolean isEnablePolicyViolationsSCA() { + return enablePolicyViolationsSCA; + } + + public Boolean isSASTversionCompitable(){ + + if(Float.parseFloat(cxVersion.getVersion())>=9.6){ + return false; + } + return true; + } + public String getCxARMUrl() { return cxARMUrl; } @@ -443,4 +756,343 @@ public String getCxARMUrl() { public void setCxARMUrl(String cxARMUrl) { this.cxARMUrl = cxARMUrl; } + + public Boolean getHideResults() { + return hideResults; + } + + public void setHideResults(Boolean hideResults) { + this.hideResults = hideResults; + } + + public Boolean getContinueBuild() { + return continueBuild; + } + + public void setContinueBuild(Boolean continueBuild) { + this.continueBuild = continueBuild; + } + + + public Boolean isAvoidDuplicateProjectScans() { + return avoidDuplicateProjectScans; + } + + public void setAvoidDuplicateProjectScans(Boolean avoidDuplicateProjectScans) { + this.avoidDuplicateProjectScans = avoidDuplicateProjectScans; + } + + public String getRemoteSrcUser() { + return remoteSrcUser; + } + + public void setRemoteSrcUser(String remoteSrcUser) { + this.remoteSrcUser = remoteSrcUser; + } + + public String getRemoteSrcPass() { + return remoteSrcPass; + } + + public void setRemoteSrcPass(String remoteSrcPass) { + this.remoteSrcPass = remoteSrcPass; + } + + public String getRemoteSrcUrl() { + return remoteSrcUrl; + } + + public void setRemoteSrcUrl(String remoteSrcUrl) { + this.remoteSrcUrl = remoteSrcUrl; + } + + public int getRemoteSrcPort() { + return remoteSrcPort; + } + + public void setRemoteSrcPort(int remoteSrcPort) { + this.remoteSrcPort = remoteSrcPort; + } + + public byte[] getRemoteSrcKeyFile() { + return remoteSrcKeyFile; + } + + public void setRemoteSrcKeyFile(byte[] remoteSrcKeyFile) { + this.remoteSrcKeyFile = remoteSrcKeyFile; + } + + public RemoteSourceTypes getRemoteType() { + return remoteType; + } + + public void setRemoteType(RemoteSourceTypes remoteType) { + this.remoteType = remoteType; + } + + public String[] getPaths() { + return paths; + } + + public void setPaths(String[] paths) { + this.paths = paths; + } + + public String getRemoteSrcBranch() { + return remoteSrcBranch; + } + + public void setRemoteSrcBranch(String remoteSrcBranch) { + this.remoteSrcBranch = remoteSrcBranch; + } + + public String getPerforceMode() { + return perforceMode; + } + + public void setPerforceMode(String perforceMode) { + this.perforceMode = perforceMode; + } + + public Boolean getGenerateXmlReport() { + return generateXmlReport; + } + + public void setGenerateXmlReport(Boolean generateXmlReport) { + this.generateXmlReport = generateXmlReport; + } + + public CxVersion getCxVersion() { + return cxVersion; + } + + public void setCxVersion(CxVersion cxVersion) { + this.cxVersion = cxVersion; + } + + public Integer getProgressInterval() { + return progressInterval; + } + + public void setProgressInterval(Integer progressInterval) { + this.progressInterval = progressInterval; + } + + public Integer getOsaProgressInterval() { + return osaProgressInterval; + } + + public void setOsaProgressInterval(Integer osaProgressInterval) { + this.osaProgressInterval = osaProgressInterval; + } + + public Integer getConnectionRetries() { + return connectionRetries; + } + + public void setConnectionRetries(Integer connectionRetries) { + this.connectionRetries = connectionRetries; + } + + public String getMvnPath() { + return mvnPath; + } + + public void setMvnPath(String mvnPath) { + this.mvnPath = mvnPath; + } + + public String getOsaScanDepth() { + return osaScanDepth; + } + + public void setOsaScanDepth(String osaScanDepth) { + this.osaScanDepth = osaScanDepth; + } + + public Integer getMaxZipSize() { + return maxZipSize; + } + + public void setMaxZipSize(Integer maxZipSize) { + this.maxZipSize = maxZipSize; + } + + public String getDefaultProjectName() { + return defaultProjectName; + } + + public void setDefaultProjectName(String defaultProjectName) { + this.defaultProjectName = defaultProjectName; + } + + public Map getReports() { + return reports; + } + + public void addPDFReport(String pdfReportPath) { + reports.put(ReportType.PDF, pdfReportPath); + } + + public void addXMLReport(String xmlReportPath) { + reports.put(ReportType.XML, xmlReportPath); + } + + public void addCSVReport(String csvReportPath) { + reports.put(ReportType.CSV, csvReportPath); + } + + public void addRTFReport(String rtfReportPath) { + reports.put(ReportType.RTF, rtfReportPath); + } + + public AstScaConfig getAstScaConfig() { + return astScaConfig; + } + + public void setAstScaConfig(AstScaConfig astScaConfig) { + this.astScaConfig = astScaConfig; + } + + public AstSastConfig getAstSastConfig() { + return astSastConfig; + } + + public void setAstSastConfig(AstSastConfig astConfig) { + this.astSastConfig = astConfig; + } + + public Set getScannerTypes() { + return scannerTypes; + } + + public void addScannerType(ScannerType scannerType) { + this.scannerTypes.add(scannerType); + } + + /** + * SAST and OSA are currently deployed on-premises, whereas AST-SCA is deployed in a cloud. + * If SAST or OSA are enabled, some of the config properties are mandatory (url, username, password etc). + * Otherwise, these properties are optional. + */ + public boolean isSastOrOSAEnabled() { + return isSastEnabled() || isOsaEnabled(); + } + + public Boolean isProxy() { + return isProxy; + } + + public void setProxy(Boolean proxy) { + isProxy = proxy; + } + + public Boolean isScaProxy() {return isScaProxy;} + + public void setScaProxy(Boolean scaProxy) {isScaProxy = scaProxy;} + + public ProxyConfig getProxyConfig() { + return proxyConfig; + } + + public void setProxyConfig(ProxyConfig proxyConfig) { + this.proxyConfig = proxyConfig; + } + + public ProxyConfig getScaProxyConfig() { + return proxyScaConfig; + } + + public void setScaProxyConfig(ProxyConfig proxyScaConfig) { + this.proxyScaConfig = proxyScaConfig; + } + + public void addCookie(Cookie cookie) { + this.sessionCookies.add(cookie); + } + + public List getSessionCookie() { + return this.sessionCookies; + } + + public TokenLoginResponse getToken() { + return token; + } + + public void setToken(TokenLoginResponse token) { + this.token = token; + } + + public Boolean getNTLM() { + return useNTLM; + } + + public void setNTLM(Boolean ntlm) { + useNTLM = ntlm; + } + + public boolean getIsOverrideProjectSetting() { + return isOverrideProjectSetting; + } + + public void setIsOverrideProjectSetting(boolean isOverrideProjectSetting) { + this.isOverrideProjectSetting = isOverrideProjectSetting; + } + + public Integer getPostScanActionId() { + return postScanActionId; + } + + public void setPostScanActionId(Integer postScanActionId) { + this.postScanActionId = postScanActionId; + } + + public final boolean isIgnoreBenignErrors() { + return ignoreBenignErrors; + } + + public final void setIgnoreBenignErrors(boolean ignoreBenignErrors) { + this.ignoreBenignErrors = ignoreBenignErrors; + } + + public Boolean isEnableSASTBranching() { + return enableSastBranching; + } + + public void setEnableSASTBranching(Boolean enableSASTBranching) { + this.enableSastBranching = enableSASTBranching; + } + + public String getMasterBranchProjName() { + return masterBranchProjName; + } + + public void setMasterBranchProjName(String masterBranchProjName) { + this.masterBranchProjName = masterBranchProjName; + } + + public boolean isGenerateScaReport() { + return generateScaReport; + } + + public void setGenerateScaReport(boolean generateScaReport) { + this.generateScaReport = generateScaReport; + } + + public String getScaReportFormat() { + return scaReportFormat; + } + + public void setScaReportFormat(String scaReportFormat) { + this.scaReportFormat = scaReportFormat; + } + + public Integer getcopyBranchTimeOutInSeconds() { + return copyBranchTimeOutInSeconds; + } + + public void setcopyBranchTimeOutInSeconds(Integer copyBranchTimeOutInSeconds) { + this.copyBranchTimeOutInSeconds = copyBranchTimeOutInSeconds; + } + } diff --git a/src/main/java/com/cx/restclient/configuration/PropertyFileLoader.java b/src/main/java/com/cx/restclient/configuration/PropertyFileLoader.java new file mode 100644 index 00000000..b357eb35 --- /dev/null +++ b/src/main/java/com/cx/restclient/configuration/PropertyFileLoader.java @@ -0,0 +1,58 @@ +package com.cx.restclient.configuration; + +import com.cx.restclient.exception.CxClientException; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +@Slf4j +public class PropertyFileLoader { + private static final String DEFAULT_FILENAME = "common.properties"; + + @Getter(lazy = true) + private static final PropertyFileLoader defaultInstance = new PropertyFileLoader(DEFAULT_FILENAME); + + private final Properties properties; + + /** + * Loads properties from resources. + * + * @param filenames list of resource filenames to load properties from. + * If the same property appears several times in the files, the property value from a file will be overridden with the value from the next file. + * @throws CxClientException if no filenames is provided, or if an error occurred while loading a file. + */ + public PropertyFileLoader(String... filenames) { + if (filenames.length == 0) { + throw new CxClientException("Please provide at least one filename."); + } + + properties = new Properties(); + for (String filename : filenames) { + Properties singleFileProperties = getPropertiesFromResource(filename); + properties.putAll(singleFileProperties); + } + } + + private Properties getPropertiesFromResource(String resourceName) { + Properties result = new Properties(); + + log.debug("Loading properties from resource: {}", resourceName); + try (InputStream input = getClass().getClassLoader().getResourceAsStream(resourceName)) { + if (input != null) { + result.load(input); + } else { + log.warn("Unable to find resource: {}, skipping.", resourceName); + } + } catch (IOException e) { + throw new CxClientException(String.format("Error loading the '%s' resource.", resourceName), e); + } + return result; + } + + public String get(String propertyName) { + return properties.getProperty(propertyName); + } +} diff --git a/src/main/java/com/cx/restclient/cxArm/dto/Policy.java b/src/main/java/com/cx/restclient/cxArm/dto/Policy.java index 3b73a1f0..63b118db 100644 --- a/src/main/java/com/cx/restclient/cxArm/dto/Policy.java +++ b/src/main/java/com/cx/restclient/cxArm/dto/Policy.java @@ -13,7 +13,21 @@ public class Policy implements Serializable { long policyId; String policyName; - List violations = new ArrayList(); + String ruleName; + String firstDetectionDate; + private List violations = new ArrayList(); + + public Policy() { + } + + + public Policy(long policyId, String policyName, String ruleName, List violations, String firstDate) { + this.policyId = policyId; + this.policyName = policyName; + this.ruleName = ruleName; + this.violations = violations; + this.firstDetectionDate = firstDate; + } public long getPolicyId() { return policyId; @@ -36,11 +50,22 @@ public List getViolations() { } public void setViolations(List violations) { - //Add the policy name to the violations for the report - for(Violation violation: violations){ - violation.setPolicyName(policyName); - } - this.violations = violations; } + + public String getRuleName() { + return ruleName; + } + + public void setRuleName(String ruleName) { + this.ruleName = ruleName; + } + + public String getFirstDetectionDate() { + return firstDetectionDate; + } + + public void setFirstDetectionDate(String firstDetectionDate) { + this.firstDetectionDate = firstDetectionDate; + } } diff --git a/src/main/java/com/cx/restclient/cxArm/dto/Rule.java b/src/main/java/com/cx/restclient/cxArm/dto/Rule.java new file mode 100644 index 00000000..d0028a8c --- /dev/null +++ b/src/main/java/com/cx/restclient/cxArm/dto/Rule.java @@ -0,0 +1,19 @@ +package com.cx.restclient.cxArm.dto; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Galn on 11/11/2018. + */ +public class Rule { + List violations = new ArrayList(); + + public List getViolations() { + return violations; + } + + public void setViolations(List violations) { + this.violations = violations; + } +} diff --git a/src/main/java/com/cx/restclient/cxArm/dto/Violation.java b/src/main/java/com/cx/restclient/cxArm/dto/Violation.java index ced4e617..4e18a82a 100644 --- a/src/main/java/com/cx/restclient/cxArm/dto/Violation.java +++ b/src/main/java/com/cx/restclient/cxArm/dto/Violation.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.io.Serializable; import java.util.Date; import static com.cx.restclient.common.ShragaUtils.formatDate; @@ -10,7 +11,7 @@ * Created by Galn on 7/5/2018. */ @JsonIgnoreProperties(ignoreUnknown = true) -public class Violation { +public class Violation implements Serializable { private String ruleId; private String ruleName; @@ -47,9 +48,6 @@ public class Violation { private String policyName; - private String detectionDate; - - public String getRuleId() { return ruleId; } @@ -193,16 +191,4 @@ public String getPolicyName() { public void setPolicyName(String policyName) { this.policyName = policyName; } - - public String getDetectionDate() { - if (detectionDate == null){ - String date = new Date(firstDetectionDateByArm).toString(); - detectionDate = formatDate(date, "E MMM dd hh:mm:ss Z yyyy", "dd/MM/yy"); - } - return detectionDate; - } - - public void setDetectionDate(String detectionDate) { - this.detectionDate = detectionDate; - } } diff --git a/src/main/java/com/cx/restclient/cxArm/utils/CxARMUtils.java b/src/main/java/com/cx/restclient/cxArm/utils/CxARMUtils.java index 569814f4..fa416ba4 100644 --- a/src/main/java/com/cx/restclient/cxArm/utils/CxARMUtils.java +++ b/src/main/java/com/cx/restclient/cxArm/utils/CxARMUtils.java @@ -1,21 +1,75 @@ package com.cx.restclient.cxArm.utils; import com.cx.restclient.cxArm.dto.Policy; +import com.cx.restclient.cxArm.dto.Violation; import com.cx.restclient.exception.CxClientException; import com.cx.restclient.httpClient.CxHttpClient; import java.io.IOException; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; import static com.cx.restclient.common.CxPARAM.CX_ARM_VIOLATION; +import static com.cx.restclient.common.ShragaUtils.formatDate; import static com.cx.restclient.httpClient.utils.ContentType.CONTENT_TYPE_APPLICATION_JSON_V1; /** * Created by Galn on 7/30/2018. */ public abstract class CxARMUtils { - public static List getProjectViolations(CxHttpClient httpClient, String cxARMUrl, long projectId, String provider) throws IOException, CxClientException { + public static List getProjectViolatedPolicies(CxHttpClient httpClient, String cxARMUrl, long projectId, String provider) throws IOException, CxClientException { String relativePath = CX_ARM_VIOLATION.replace("{projectId}", Long.toString(projectId)).replace("{provider}", provider); return (List) httpClient.getRequest(cxARMUrl, relativePath, CONTENT_TYPE_APPLICATION_JSON_V1, null, Policy.class, 200, "CxARM violations", true); } + + public static List getPolicyList(Policy policy) { + List policies = new ArrayList(); + Map> rules = resolveRules(policy.getViolations()); + Iterator it = rules.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry pair = (Map.Entry) it.next(); + List violations = (List) pair.getValue(); + String firstDate = resolveFirstDate(violations); + policies.add(new Policy(policy.getPolicyId(), policy.getPolicyName(), pair.getKey().toString(), violations, firstDate)); + it.remove(); // avoids a ConcurrentModificationException + } + + return policies; + } + + private static Map> resolveRules(List violations) { + Map> rules = violations.stream().collect( + Collectors.toMap(Violation::getRuleName, e -> { + List ary = new ArrayList(); + ary.add(e); + return ary; + }, + (left, right) -> { + left.addAll(right); + return left; + })); + + return rules; + } + + private static String resolveFirstDate(List violations) { + Date firstDetectionDate = new Date(violations.get(0).getFirstDetectionDateByArm()); + for (Violation violation : violations) { + Date date = new Date(violation.getFirstDetectionDateByArm()); + if (date.before(firstDetectionDate)) { + firstDetectionDate = date; + } + } + String firstDate = formatDate(firstDetectionDate.toString(), "E MMM dd hh:mm:ss Z yyyy", "dd/MM/yy"); + return firstDate; + } + + public static String getPoliciesNames(List policies) { + String str =""; + for (Policy policy : policies){ + str += ", " + policy.getPolicyName(); + } + str = str.substring(1, str.length()); + return str; + } } diff --git a/src/main/java/com/cx/restclient/dto/BaseStatus.java b/src/main/java/com/cx/restclient/dto/BaseStatus.java index b9376e68..8d853702 100644 --- a/src/main/java/com/cx/restclient/dto/BaseStatus.java +++ b/src/main/java/com/cx/restclient/dto/BaseStatus.java @@ -1,8 +1,11 @@ package com.cx.restclient.dto; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + /** * Created by Galn on 13/02/2018. */ +@JsonIgnoreProperties(ignoreUnknown = true) public class BaseStatus { private String baseId; private Status baseStatus; diff --git a/src/main/java/com/cx/restclient/dto/CxProxy.java b/src/main/java/com/cx/restclient/dto/CxProxy.java new file mode 100644 index 00000000..59084b84 --- /dev/null +++ b/src/main/java/com/cx/restclient/dto/CxProxy.java @@ -0,0 +1,60 @@ +package com.cx.restclient.dto; + +/** + * Created by Galn on 4/2/2019. + */ +public class CxProxy { + private Boolean useProxy; + private String proxyHost; + private Integer proxyPort; + private String proxyScheme; + + + public CxProxy(Boolean useProxy, String proxyHost, Integer proxyPort, String scheme) { + this.useProxy = useProxy; + this.proxyHost = proxyHost; + this.proxyPort = proxyPort; + this.proxyScheme = scheme; + } + + public CxProxy() { + } + + public Boolean getUseProxy() { + return useProxy; + } + + public void setUseProxy(Boolean useProxy) { + this.useProxy = useProxy; + } + + public String getProxyHost() { + return proxyHost; + } + + public void setProxyHost(String proxyHost) { + this.proxyHost = proxyHost; + } + + public Integer getProxyPort() { + return proxyPort; + } + + public void setProxyPort(Integer proxyPort) { + this.proxyPort = proxyPort; + } + + public void setProxyPort(String proxyPort) { + try { + this.proxyPort = Integer.parseInt(proxyPort); + }catch (Exception ex){} + } + + public String getProxyScheme() { + return proxyScheme; + } + + public void setProxyScheme(String proxyScheme) { + this.proxyScheme = proxyScheme; + } +} diff --git a/src/main/java/com/cx/restclient/dto/CxVersion.java b/src/main/java/com/cx/restclient/dto/CxVersion.java new file mode 100644 index 00000000..722cbe3e --- /dev/null +++ b/src/main/java/com/cx/restclient/dto/CxVersion.java @@ -0,0 +1,37 @@ +package com.cx.restclient.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Created by Galn on 4/1/2019. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class CxVersion { + private String version; + private String hotFix; + private String enginePackVersion; + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getHotFix() { + return hotFix; + } + + public void setHotFix(String hotFix) { + this.hotFix = hotFix; + } + + public String getEnginePackVersion() { + return enginePackVersion; + } + + public void setEnginePackVersion(String enginePackVersion) { + this.enginePackVersion = enginePackVersion; + } +} diff --git a/src/main/java/com/cx/restclient/dto/EngineConfiguration.java b/src/main/java/com/cx/restclient/dto/EngineConfiguration.java new file mode 100644 index 00000000..e3f9d1eb --- /dev/null +++ b/src/main/java/com/cx/restclient/dto/EngineConfiguration.java @@ -0,0 +1,31 @@ +package com.cx.restclient.dto; + +public class EngineConfiguration { + + private int id; + + private String name; + + public EngineConfiguration() { + } + + public EngineConfiguration(String name) { + this.name = name; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/cx/restclient/dto/LoginRequest.java b/src/main/java/com/cx/restclient/dto/LoginRequest.java index cbf9e413..abfa1cc5 100644 --- a/src/main/java/com/cx/restclient/dto/LoginRequest.java +++ b/src/main/java/com/cx/restclient/dto/LoginRequest.java @@ -1,9 +1,12 @@ package com.cx.restclient.dto; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + /** * Created by: Dorg. * Date: 08/09/2016. */ +@JsonIgnoreProperties(ignoreUnknown = true) public class LoginRequest { private String username; diff --git a/src/main/java/com/cx/restclient/dto/LoginSettings.java b/src/main/java/com/cx/restclient/dto/LoginSettings.java new file mode 100644 index 00000000..e134c448 --- /dev/null +++ b/src/main/java/com/cx/restclient/dto/LoginSettings.java @@ -0,0 +1,96 @@ +package com.cx.restclient.dto; + +import com.cx.restclient.osa.dto.ClientType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.NoArgsConstructor; +import org.apache.http.cookie.Cookie; + +import java.util.ArrayList; +import java.util.List; + +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class LoginSettings { + private String accessControlBaseUrl; + private String username; + private String password; + private CharSequence tenant; + private String refreshToken; + private final List sessionCookies = new ArrayList<>(); + private String version; + + // TODO: find a way to use a single client type here. + private ClientType clientTypeForRefreshToken; + private ClientType clientTypeForPasswordAuth; + + public void setAccessControlBaseUrl(String accessControlBaseUrl) { + this.accessControlBaseUrl = accessControlBaseUrl; + } + + public String getAccessControlBaseUrl() { + return accessControlBaseUrl; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getUsername() { + return username; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getPassword() { + return password; + } + + public void setClientTypeForRefreshToken(ClientType clientType) { + this.clientTypeForRefreshToken = clientType; + } + + public ClientType getClientTypeForRefreshToken() { + return clientTypeForRefreshToken; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + public String getRefreshToken() { + return refreshToken; + } + + public void setClientTypeForPasswordAuth(ClientType clientType) { + this.clientTypeForPasswordAuth = clientType; + } + + public ClientType getClientTypeForPasswordAuth() { + return clientTypeForPasswordAuth; + } + + public CharSequence getTenant() { + return tenant; + } + + public void setTenant(CharSequence tenant) { + this.tenant = tenant; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public List getSessionCookies() { + return sessionCookies; + } + +} diff --git a/src/main/java/com/cx/restclient/dto/PathFilter.java b/src/main/java/com/cx/restclient/dto/PathFilter.java new file mode 100644 index 00000000..b982ccc7 --- /dev/null +++ b/src/main/java/com/cx/restclient/dto/PathFilter.java @@ -0,0 +1,35 @@ +package com.cx.restclient.dto; + +import com.cx.restclient.common.ShragaUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.slf4j.Logger; + +import java.util.List; +import java.util.Map; + +public class PathFilter { + private String[] includes; + private String[] excludes; + + public PathFilter(String folderExclusions, String filterPattern, Logger log) { + Map> stringListMap = ShragaUtils.generateIncludesExcludesPatternLists(folderExclusions, filterPattern, log); + includes = getArray(stringListMap, ShragaUtils.INCLUDES_LIST); + excludes = getArray(stringListMap, ShragaUtils.EXCLUDES_LIST); + } + + public String[] getIncludes() { + return includes; + } + + public String[] getExcludes() { + return excludes; + } + + private static String[] getArray(Map> map, String key){ + return map.get(key).toArray(new String[0]); + } + + public void addToIncludes(String element) { + includes = ArrayUtils.add(includes, element); + } +} diff --git a/src/main/java/com/cx/restclient/dto/ProxyConfig.java b/src/main/java/com/cx/restclient/dto/ProxyConfig.java new file mode 100644 index 00000000..39a1d9f4 --- /dev/null +++ b/src/main/java/com/cx/restclient/dto/ProxyConfig.java @@ -0,0 +1,87 @@ +package com.cx.restclient.dto; + +import java.io.Serializable; + +public class ProxyConfig implements Serializable { + + private String host; + private int port; + private String username; + private String password; + private boolean useHttps; + private String noproxyHosts; + + public String getNoproxyHosts() { + return noproxyHosts; + } + + public void setNoproxyHosts(String noproxyHosts) { + this.noproxyHosts = noproxyHosts; + } + + public ProxyConfig() { + } + + public ProxyConfig(String host, int port, String username, String password, boolean useHttps) { + this.host = host; + this.port = port; + this.username = username; + this.password = password; + this.useHttps = useHttps; + this.noproxyHosts = ""; + } + /** + * TO be used in Bamboo plugin for the time being + * @param host + * @param port + * @param username + * @param password + * @param useHttps + */ + public ProxyConfig(String host, int port, String username, String password, boolean useHttps, String noProxyHosts){ + this.port = port; + this.username = username; + this.password = password; + this.useHttps = useHttps; + this.noproxyHosts = noProxyHosts; + } + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public boolean isUseHttps() { + return useHttps; + } + + public void setUseHttps(boolean useHttps) { + this.useHttps = useHttps; + } +} diff --git a/src/main/java/com/cx/restclient/dto/RemoteSourceRequest.java b/src/main/java/com/cx/restclient/dto/RemoteSourceRequest.java new file mode 100644 index 00000000..23f755b9 --- /dev/null +++ b/src/main/java/com/cx/restclient/dto/RemoteSourceRequest.java @@ -0,0 +1,131 @@ +package com.cx.restclient.dto; + +import com.cx.restclient.configuration.CxScanConfig; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Created by Galn on 11/25/2018. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties({ "type" }) +public class RemoteSourceRequest { + + public class Credentials { + private String userName; + private String password; + + public Credentials(String userName, String password) { + this.userName = userName; + this.password = password; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + } + + public class Uri { + private String absoluteUrl; + private int port; + + public Uri(String absoluteUrl, int port) { + this.absoluteUrl = absoluteUrl; + this.port = port; + } + + public String getAbsoluteUrl() { + return absoluteUrl; + } + + public void setAbsoluteUrl(String absoluteUrl) { + this.absoluteUrl = absoluteUrl; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + } + + private Credentials credentials; + private Uri uri; + private byte[] privateKey; + private String[] paths; + private RemoteSourceTypes type; + private String browseMode; + + public RemoteSourceRequest() { + } + + public RemoteSourceRequest(CxScanConfig config) { + credentials = new Credentials(config.getRemoteSrcUser(), config.getRemoteSrcPass()); + uri = new Uri(config.getRemoteSrcUrl(), config.getRemoteSrcPort()); + privateKey = config.getRemoteSrcKeyFile() == null ? new byte[0] : config.getRemoteSrcKeyFile(); + paths = config.getPaths(); + type = config.getRemoteType(); + } + + public Credentials getCredentials() { + return credentials; + } + + public void setCredentials(Credentials credentials) { + this.credentials = credentials; + } + + public Uri getUri() { + return uri; + } + + public void setUri(Uri uri) { + this.uri = uri; + } + + public byte[] getPrivateKey() { + return privateKey; + } + + public void setPrivateKey(byte[] privateKey) { + this.privateKey = privateKey; + } + + public String[] getPaths() { + return paths; + } + + public void setPaths(String[] paths) { + this.paths = paths; + } + + public RemoteSourceTypes getType() { + return type; + } + + public void setType(RemoteSourceTypes type) { + this.type = type; + } + + public String getBrowseMode() { + return browseMode; + } + + public void setBrowseMode(String browseMode) { + this.browseMode = browseMode; + } +} diff --git a/src/main/java/com/cx/restclient/dto/RemoteSourceTypes.java b/src/main/java/com/cx/restclient/dto/RemoteSourceTypes.java new file mode 100644 index 00000000..02f07277 --- /dev/null +++ b/src/main/java/com/cx/restclient/dto/RemoteSourceTypes.java @@ -0,0 +1,25 @@ +package com.cx.restclient.dto; + +/** + * Created by: Galn. + * Date: 25/11/2016. + */ +public enum RemoteSourceTypes { + + SHARED("shared"), + SVN("svn"), + GIT("git"), + TFS("tfs"), + PERFORCE("perforce"); + + private String value; + + RemoteSourceTypes(String value) { + this.value = value; + } + + public String value() { + return value; + } + +} \ No newline at end of file diff --git a/src/main/java/com/cx/restclient/dto/Results.java b/src/main/java/com/cx/restclient/dto/Results.java new file mode 100644 index 00000000..6b88e4e9 --- /dev/null +++ b/src/main/java/com/cx/restclient/dto/Results.java @@ -0,0 +1,13 @@ +package com.cx.restclient.dto; + +import java.io.Serializable; + +import com.cx.restclient.exception.CxClientException; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public abstract class Results implements Serializable{ + private CxClientException exception; +} diff --git a/src/main/java/com/cx/restclient/dto/ScanResults.java b/src/main/java/com/cx/restclient/dto/ScanResults.java index c4d67649..98d49480 100644 --- a/src/main/java/com/cx/restclient/dto/ScanResults.java +++ b/src/main/java/com/cx/restclient/dto/ScanResults.java @@ -1,69 +1,65 @@ package com.cx.restclient.dto; +import com.cx.restclient.ast.dto.sast.AstSastResults; +import com.cx.restclient.ast.dto.sca.AstScaResults; +import com.cx.restclient.exception.CxClientException; import com.cx.restclient.osa.dto.OSAResults; import com.cx.restclient.sast.dto.SASTResults; import java.io.Serializable; +import java.util.EnumMap; +import java.util.Map; -public class ScanResults implements Serializable { +public class ScanResults extends Results implements Serializable { + private final Map resultsMap = new EnumMap<>(ScannerType.class); - private SASTResults sastResults; - private OSAResults osaResults; + private Exception generalException = null; - private Exception sastCreateException = null; - private Exception sastWaitException = null; - private Exception osaCreateException = null; - private Exception osaWaitException = null; - - public ScanResults() { + public Map getResults(){ + return resultsMap; } - - public SASTResults getSastResults() { - return sastResults; + + public void put(ScannerType type, Results results) { + if(resultsMap.containsKey(type)){ + throw new CxClientException("Results already contain type " + type); + } + resultsMap.put(type, results); } - public void setSastResults(SASTResults sastResults) { - this.sastResults = sastResults; + public Map getResultsMap() { + return resultsMap; } - public OSAResults getOsaResults() { - return osaResults; + public Results get(ScannerType type) { + return resultsMap.get(type); } - public void setOsaResults(OSAResults osaResults) { - this.osaResults = osaResults; - } - public Exception getSastCreateException() { - return sastCreateException; + public OSAResults getOsaResults() { + return (OSAResults)resultsMap.get(ScannerType.OSA); } - public void setSastCreateException(Exception sastCreateException) { - this.sastCreateException = sastCreateException; + public AstSastResults getAstResults() { + return (AstSastResults)resultsMap.get(ScannerType.AST_SAST); } - public Exception getSastWaitException() { - return sastWaitException; + public AstScaResults getScaResults() { + return (AstScaResults)resultsMap.get(ScannerType.AST_SCA); } - public void setSastWaitException(Exception sastWaitException) { - this.sastWaitException = sastWaitException; - } - public Exception getOsaCreateException() { - return osaCreateException; - } + public SASTResults getSastResults() { + return (SASTResults)resultsMap.get(ScannerType.SAST); - public void setOsaCreateException(Exception osaCreateException) { - this.osaCreateException = osaCreateException; } - public Exception getOsaWaitException() { - return osaWaitException; + public Exception getGeneralException() { + return generalException; } - public void setOsaWaitException(Exception osaWaitException) { - this.osaWaitException = osaWaitException; + public void setGeneralException(Exception generalException) { + this.generalException = generalException; } + } diff --git a/src/main/java/com/cx/restclient/dto/ScannerType.java b/src/main/java/com/cx/restclient/dto/ScannerType.java new file mode 100644 index 00000000..f69a19e1 --- /dev/null +++ b/src/main/java/com/cx/restclient/dto/ScannerType.java @@ -0,0 +1,21 @@ +package com.cx.restclient.dto; + +public enum ScannerType { + // Legacy scanners. + SAST("CxSAST"), + OSA("CxOSA"), + + // Scan engines of the new CxAST platform. + AST_SCA("CxAST-SCA"), + AST_SAST("CxAST-SAST"); + + private final String displayName; + + ScannerType(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } +} diff --git a/src/main/java/com/cx/restclient/dto/SourceLocationType.java b/src/main/java/com/cx/restclient/dto/SourceLocationType.java new file mode 100644 index 00000000..2e2e94e4 --- /dev/null +++ b/src/main/java/com/cx/restclient/dto/SourceLocationType.java @@ -0,0 +1,16 @@ +package com.cx.restclient.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum SourceLocationType { + LOCAL_DIRECTORY("upload"), + REMOTE_REPOSITORY("git"); + + /** + * Value used in API calls. Currently all the clients using this enum use the same API values. + */ + private final String apiValue; +} diff --git a/src/main/java/com/cx/restclient/dto/Status.java b/src/main/java/com/cx/restclient/dto/Status.java index 3494ad53..390420ec 100644 --- a/src/main/java/com/cx/restclient/dto/Status.java +++ b/src/main/java/com/cx/restclient/dto/Status.java @@ -8,8 +8,8 @@ public enum Status { IN_PROGRESS("In progress"), SUCCEEDED("Succeeded"), - FAILED("Failed"); - + FAILED("Failed"), + SOURCE_PULLING_AND_DEPLOYMENT ("SourcePullingAndDeployment"); private String value; diff --git a/src/main/java/com/cx/restclient/dto/Team.java b/src/main/java/com/cx/restclient/dto/Team.java index 8af500c4..2f389cdd 100644 --- a/src/main/java/com/cx/restclient/dto/Team.java +++ b/src/main/java/com/cx/restclient/dto/Team.java @@ -1,8 +1,12 @@ package com.cx.restclient.dto; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + /** * Created by Galn on 14/02/2018. */ + +@JsonIgnoreProperties(ignoreUnknown = true) public class Team { public String id; public String fullName; diff --git a/src/main/java/com/cx/restclient/dto/ThresholdResult.java b/src/main/java/com/cx/restclient/dto/ThresholdResult.java deleted file mode 100644 index 35671989..00000000 --- a/src/main/java/com/cx/restclient/dto/ThresholdResult.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.cx.restclient.dto; - -/** - * Created by Galn on 4/10/2018. - */ -public class ThresholdResult { - private boolean isFail; - private String failDescription; - - public ThresholdResult(boolean isFail, String failDescription) { - this.isFail = isFail; - this.failDescription = failDescription; - } - - public boolean isFail() { - return isFail; - } - - public void setFail(boolean fail) { - isFail = fail; - } - - public String getFailDescription() { - return failDescription; - } - - public void setFailDescription(String failDescription) { - this.failDescription = failDescription; - } -} diff --git a/src/main/java/com/cx/restclient/dto/TokenLoginResponse.java b/src/main/java/com/cx/restclient/dto/TokenLoginResponse.java index ffec9c1b..948aa620 100644 --- a/src/main/java/com/cx/restclient/dto/TokenLoginResponse.java +++ b/src/main/java/com/cx/restclient/dto/TokenLoginResponse.java @@ -1,12 +1,16 @@ package com.cx.restclient.dto; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + /** * Created by Galn on 19/03/2018. */ +@JsonIgnoreProperties(ignoreUnknown = true) public class TokenLoginResponse { private String access_token; private long expires_in; private String token_type; + private String refresh_token; public String getAccess_token() { return access_token; @@ -31,4 +35,12 @@ public String getToken_type() { public void setToken_type(String token_type) { this.token_type = token_type; } + + public String getRefresh_token() { + return refresh_token; + } + + public void setRefresh_token(String refresh_token) { + this.refresh_token = refresh_token; + } } diff --git a/src/main/java/com/cx/restclient/dto/scansummary/ErrorSource.java b/src/main/java/com/cx/restclient/dto/scansummary/ErrorSource.java new file mode 100644 index 00000000..501e5dfc --- /dev/null +++ b/src/main/java/com/cx/restclient/dto/scansummary/ErrorSource.java @@ -0,0 +1,7 @@ +package com.cx.restclient.dto.scansummary; + +public enum ErrorSource { + SAST, + OSA, + SCA +} diff --git a/src/main/java/com/cx/restclient/dto/scansummary/ScanSummary.java b/src/main/java/com/cx/restclient/dto/scansummary/ScanSummary.java new file mode 100644 index 00000000..d2a1010d --- /dev/null +++ b/src/main/java/com/cx/restclient/dto/scansummary/ScanSummary.java @@ -0,0 +1,195 @@ +package com.cx.restclient.dto.scansummary; + +import com.cx.restclient.ast.dto.sca.AstScaResults; +import com.cx.restclient.ast.dto.sca.report.AstScaSummaryResults; +import com.cx.restclient.common.CxPARAM; +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.osa.dto.OSAResults; +import com.cx.restclient.osa.dto.OSASummaryResults; +import com.cx.restclient.sast.dto.SASTResults; + +import java.util.ArrayList; +import java.util.List; + +/** + * Collects errors from provided scan results, based on scan config. + */ +public class ScanSummary { + private final List thresholdErrors = new ArrayList<>(); + private final List newResultThresholdErrors = new ArrayList<>(); + private final boolean policyViolated; + + private boolean policyVioletedSAST; + + private boolean policyVioletedSCA; + + public ScanSummary(CxScanConfig config, SASTResults sastResults, OSAResults osaResults, AstScaResults scaResults) { + + addSastThresholdErrors(config, sastResults); + addDependencyScanThresholdErrors(config, osaResults, scaResults); + + addNewResultThresholdErrors(config, sastResults); + + policyVioletedSAST = determinePolicyViolation(config, sastResults, osaResults); + policyVioletedSCA = determinePolicyViolationSCA(config,scaResults); + if(policyVioletedSAST || policyVioletedSCA){ + policyViolated=true; + }else{ + policyViolated=false; + } + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + + for (ThresholdError error : thresholdErrors) { + result.append(String.format("%s %s severity results are above threshold. Results: %d. Threshold: %d.%n", + error.getSource().toString(), + error.getSeverity().toString().toLowerCase(), + error.getValue(), + error.getThreshold())); + } + + for (Severity severity : newResultThresholdErrors) { + result.append(String.format("One or more new results of %s severity%n", severity.toString().toLowerCase())); + } + + if (policyViolated) { + result.append(CxPARAM.PROJECT_POLICY_VIOLATED_STATUS).append("\n"); + } + + return result.toString(); + } + + public List getThresholdErrors() { + return thresholdErrors; + } + + public boolean hasErrors() { + return !thresholdErrors.isEmpty() || !newResultThresholdErrors.isEmpty() || policyViolated; + } + + public boolean isPolicyViolated() { + return policyViolated; + } + + public boolean isSastThresholdExceeded() { + return thresholdErrors.stream().anyMatch(error -> error.getSource() == ErrorSource.SAST); + } + + public boolean isOsaThresholdExceeded() { + return thresholdErrors.stream().anyMatch(error -> error.getSource() == ErrorSource.OSA || error.getSource() == ErrorSource.SCA); + } + + public boolean isSastThresholdForNewResultsExceeded() { + return !newResultThresholdErrors.isEmpty(); + } + + private void addSastThresholdErrors(CxScanConfig config, SASTResults sastResults) { + if (config.isSASTThresholdEffectivelyEnabled() && + sastResults != null && + sastResults.isSastResultsReady()) { + checkForThresholdError(sastResults.getCritical(), config.getSastCriticalThreshold(), ErrorSource.SAST, Severity.CRITICAL); + checkForThresholdError(sastResults.getHigh(), config.getSastHighThreshold(), ErrorSource.SAST, Severity.HIGH); + checkForThresholdError(sastResults.getMedium(), config.getSastMediumThreshold(), ErrorSource.SAST, Severity.MEDIUM); + checkForThresholdError(sastResults.getLow(), config.getSastLowThreshold(), ErrorSource.SAST, Severity.LOW); + } + } + + private void addDependencyScanThresholdErrors(CxScanConfig config, OSAResults osaResults, AstScaResults scaResults) { + if (config.isOSAThresholdEffectivelyEnabled() && (scaResults != null || osaResults != null)) { + + ErrorSource errorSource = osaResults != null ? ErrorSource.OSA : ErrorSource.SCA; + int totalCritical = 0; + int totalHigh = 0; + int totalMedium = 0; + int totalLow = 0; + boolean hasSummary = false; + + if (scaResults != null) { + AstScaSummaryResults summary = scaResults.getSummary(); + if (summary != null) { + hasSummary = true; + totalCritical = summary.getCriticalVulnerabilityCount(); + totalHigh = summary.getHighVulnerabilityCount(); + totalMedium = summary.getMediumVulnerabilityCount(); + totalLow = summary.getLowVulnerabilityCount(); + } + } else if (osaResults.isOsaResultsReady()) { + OSASummaryResults summary = osaResults.getResults(); + if (summary != null) { + hasSummary = true; + totalHigh = summary.getTotalHighVulnerabilities(); + totalMedium = summary.getTotalMediumVulnerabilities(); + totalLow = summary.getTotalLowVulnerabilities(); + } + } + + if (hasSummary) { + if (scaResults != null) { + checkForThresholdError(totalCritical, config.getOsaCriticalThreshold(), errorSource, Severity.CRITICAL); + } + checkForThresholdError(totalHigh, config.getOsaHighThreshold(), errorSource, Severity.HIGH); + checkForThresholdError(totalMedium, config.getOsaMediumThreshold(), errorSource, Severity.MEDIUM); + checkForThresholdError(totalLow, config.getOsaLowThreshold(), errorSource, Severity.LOW); + } + } + } + + private void addNewResultThresholdErrors(CxScanConfig config, SASTResults sastResults) { + if (sastResults != null && sastResults.isSastResultsReady() && config.getSastNewResultsThresholdEnabled()) { + String severity = config.getSastNewResultsThresholdSeverity(); + + if ("LOW".equals(severity)) { + if (sastResults.getNewLow() > 0) { + newResultThresholdErrors.add(Severity.LOW); + } + severity = "MEDIUM"; + } + + if ("CRITICAL".equals(severity)) { + if (sastResults.getNewCritical() > 0) { + newResultThresholdErrors.add(Severity.CRITICAL); + } + severity = "LOW"; + } + + if ("MEDIUM".equals(severity)) { + if (sastResults.getNewMedium() > 0) { + newResultThresholdErrors.add(Severity.MEDIUM); + } + severity = "HIGH"; + } + + if ("HIGH".equals(severity)) { + if (sastResults.getNewHigh() > 0) { + newResultThresholdErrors.add(Severity.HIGH); + } + severity = "CRITICAL"; + } + + if ("CRITICAL".equals(severity) && sastResults.getNewCritical() > 0) { + newResultThresholdErrors.add(Severity.CRITICAL); + } + } + } + + private static boolean determinePolicyViolation(CxScanConfig config, SASTResults sastResults, OSAResults osaResults) { + return config.getEnablePolicyViolations() && + ((osaResults != null && + !osaResults.getOsaPolicies().isEmpty()) || + (sastResults != null && !sastResults.getSastPolicies().isEmpty())); + } + + private static boolean determinePolicyViolationSCA(CxScanConfig config,AstScaResults scaResults) { + return config.getEnablePolicyViolationsSCA() && (scaResults != null && scaResults.isBreakTheBuild() && scaResults.isPolicyViolated()); + } + + private void checkForThresholdError(int value, Integer threshold, ErrorSource source, Severity severity) { + if (threshold != null && value > threshold) { + ThresholdError error = new ThresholdError(source, severity, value, threshold); + thresholdErrors.add(error); + } + } +} diff --git a/src/main/java/com/cx/restclient/dto/scansummary/Severity.java b/src/main/java/com/cx/restclient/dto/scansummary/Severity.java new file mode 100644 index 00000000..caa9e6d2 --- /dev/null +++ b/src/main/java/com/cx/restclient/dto/scansummary/Severity.java @@ -0,0 +1,9 @@ +package com.cx.restclient.dto.scansummary; + +public enum Severity { + LOW, + MEDIUM, + HIGH, + CRITICAL, + NONE +} diff --git a/src/main/java/com/cx/restclient/dto/scansummary/ThresholdError.java b/src/main/java/com/cx/restclient/dto/scansummary/ThresholdError.java new file mode 100644 index 00000000..3b002ed8 --- /dev/null +++ b/src/main/java/com/cx/restclient/dto/scansummary/ThresholdError.java @@ -0,0 +1,31 @@ +package com.cx.restclient.dto.scansummary; + +public class ThresholdError { + private ErrorSource source; + private Severity severity; + private final int value; + private final Integer threshold; + + public ThresholdError(ErrorSource source, Severity severity, int value, Integer threshold) { + this.source = source; + this.severity = severity; + this.value = value; + this.threshold = threshold; + } + + public ErrorSource getSource() { + return source; + } + + public Severity getSeverity() { + return severity; + } + + public int getValue() { + return value; + } + + public Integer getThreshold() { + return threshold; + } +} diff --git a/src/main/java/com/cx/restclient/exception/CxClientException.java b/src/main/java/com/cx/restclient/exception/CxClientException.java index 6284f00e..df27ac27 100644 --- a/src/main/java/com/cx/restclient/exception/CxClientException.java +++ b/src/main/java/com/cx/restclient/exception/CxClientException.java @@ -3,7 +3,7 @@ /** * Created by Galn on 05/02/2018. */ -public class CxClientException extends Exception { +public class CxClientException extends RuntimeException { public CxClientException() { super(); } diff --git a/src/main/java/com/cx/restclient/exception/CxHTTPClientException.java b/src/main/java/com/cx/restclient/exception/CxHTTPClientException.java index 8103a5e4..f33ed730 100644 --- a/src/main/java/com/cx/restclient/exception/CxHTTPClientException.java +++ b/src/main/java/com/cx/restclient/exception/CxHTTPClientException.java @@ -4,12 +4,13 @@ * Created by Galn on 05/02/2018. */ public class CxHTTPClientException extends CxClientException { - private int statusCode = 0; + private String responseBody; - public CxHTTPClientException(int statusCode, String s) { - super(s); + public CxHTTPClientException(int statusCode, String message, String responseBody) { + super(message); this.statusCode = statusCode; + this.responseBody = responseBody; } public CxHTTPClientException() { @@ -32,5 +33,7 @@ public int getStatusCode() { return this.statusCode; } - + public String getResponseBody() { + return responseBody; + } } diff --git a/src/main/java/com/cx/restclient/exception/CxOSAException.java b/src/main/java/com/cx/restclient/exception/CxOSAException.java new file mode 100644 index 00000000..bc87928b --- /dev/null +++ b/src/main/java/com/cx/restclient/exception/CxOSAException.java @@ -0,0 +1,24 @@ +package com.cx.restclient.exception; + +/** + * Created by Ilan Dayan + */ +public class CxOSAException extends CxClientException { + + public CxOSAException() { + super(); + } + + public CxOSAException(String message) { + super(message); + } + + public CxOSAException(String message, Throwable cause) { + super(message, cause); + } + + public CxOSAException(Throwable cause) { + super(cause); + } + +} \ No newline at end of file diff --git a/src/main/java/com/cx/restclient/httpClient/CxHttpClient.java b/src/main/java/com/cx/restclient/httpClient/CxHttpClient.java index 4c6cc71c..cfaa24b6 100644 --- a/src/main/java/com/cx/restclient/httpClient/CxHttpClient.java +++ b/src/main/java/com/cx/restclient/httpClient/CxHttpClient.java @@ -1,153 +1,725 @@ package com.cx.restclient.httpClient; import com.cx.restclient.common.ErrorMessage; -import com.cx.restclient.common.UrlUtils; +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.dto.CxVersion; +import com.cx.restclient.dto.LoginSettings; +import com.cx.restclient.dto.ProxyConfig; import com.cx.restclient.dto.TokenLoginResponse; import com.cx.restclient.exception.CxClientException; import com.cx.restclient.exception.CxHTTPClientException; import com.cx.restclient.exception.CxTokenExpiredException; +import com.cx.restclient.osa.dto.ClientType; +import com.google.gson.Gson; +import org.apache.commons.lang3.StringUtils; import org.apache.http.*; +import org.apache.commons.codec.binary.Base64; +import org.apache.http.auth.AuthSchemeProvider; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.NTCredentials; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CookieStore; +import org.apache.http.client.CredentialsProvider; import org.apache.http.client.HttpClient; +import org.apache.http.client.config.AuthSchemes; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.*; +import org.apache.http.client.params.AuthPolicy; import org.apache.http.client.utils.HttpClientUtils; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.routing.HttpRoute; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.TrustAllStrategy; +import org.apache.http.conn.ssl.TrustSelfSignedStrategy; +import org.apache.http.cookie.Cookie; import org.apache.http.entity.ContentType; -import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.NoConnectionReuseStrategy; +import org.apache.http.impl.auth.BasicSchemeFactory; +import org.apache.http.impl.auth.DigestSchemeFactory; +import org.apache.http.impl.auth.win.WindowsCredentialsProvider; +import org.apache.http.impl.auth.win.WindowsNTLMSchemeFactory; +import org.apache.http.impl.auth.win.WindowsNegotiateSchemeFactory; +import org.apache.http.impl.client.*; +import org.apache.http.impl.conn.DefaultProxyRoutePlanner; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicNameValuePair; import org.apache.http.protocol.HttpContext; +import org.apache.http.ssl.SSLContextBuilder; import org.apache.http.ssl.SSLContexts; import org.apache.http.ssl.TrustStrategy; +import org.json.JSONException; +import org.json.JSONObject; import org.slf4j.Logger; +import javax.annotation.Nullable; +import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; +import java.io.Closeable; import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; +import java.net.IDN; +import java.net.URI; +import java.net.URISyntaxException; import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.List; +import java.text.MessageFormat; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; -import static com.cx.restclient.common.CxPARAM.AUTHENTICATION; -import static com.cx.restclient.common.CxPARAM.ORIGIN_HEADER; +import static com.cx.restclient.common.CxPARAM.*; import static com.cx.restclient.httpClient.utils.ContentType.CONTENT_TYPE_APPLICATION_JSON; import static com.cx.restclient.httpClient.utils.HttpClientHelper.*; + /** * Created by Galn on 05/02/2018. */ -public class CxHttpClient { +public class CxHttpClient implements Closeable { + + private static String HTTP_NO_HOST = System.getProperty("http.nonProxyHosts"); + private static String HTTPS_NO_HOST = System.getProperty("https.nonProxyHosts"); + + private static final String HTTPS = "https"; + + private static final String LOGIN_FAILED_MSG = "Fail to login with windows authentication: "; + + private static final String DEFAULT_GRANT_TYPE = "password"; + private static final String LOCATION_HEADER = "Location"; + private static final String AUTH_MESSAGE = "authenticate"; + private static final String CLIENT_SECRET_PROP = "client_secret"; + public static final String REFRESH_TOKEN_PROP = "refresh_token"; + private static final String PASSWORD_PROP = "password"; + public static final String CLIENT_ID_PROP = "client_id"; + private static final String KEY_USER = "user"; + private static final String KEY_DOMAIN = "domain"; - private Logger logi; private HttpClient apacheClient; + + private Logger log; private TokenLoginResponse token; private String rootUri; - private final String username; - private final String password; + private final String refreshToken; private String cxOrigin; + private String cxOriginUrl; + private Boolean useSSo; + private Boolean useNTLM; + private LoginSettings lastLoginSettings; + private String teamPath; + private CookieStore cookieStore = new BasicCookieStore(); + private HttpClientBuilder cb = HttpClients.custom(); + private final Map customHeaders = new HashMap<>(); + private CxVersion cxVersion; + private String pluginVersion; - private final HttpRequestInterceptor requestFilter = new HttpRequestInterceptor() { - public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException { - httpRequest.addHeader(ORIGIN_HEADER, cxOrigin); - if (token != null) { - httpRequest.addHeader(HttpHeaders.AUTHORIZATION, token.getToken_type() + " " + token.getAccess_token()); - } - } - }; - public CxHttpClient(String hostname, String username, String password, String origin, boolean disableSSLValidation, Logger logi) throws MalformedURLException { - this.logi = logi; - this.username = username; - this.password = password; - this.rootUri = UrlUtils.parseURLToString(hostname, "CxRestAPI/"); + public CxHttpClient(String rootUri, String origin, boolean disableSSLValidation, boolean isSSO, String refreshToken, + boolean isProxy, @Nullable ProxyConfig proxyConfig, Logger log, Boolean useNTLM, String pluginVersion) throws CxClientException { + + this.log = log; + this.rootUri = rootUri; + this.refreshToken = refreshToken; this.cxOrigin = origin; + this.useSSo = isSSO; + this.useNTLM = useNTLM; + this.pluginVersion = pluginVersion; //create httpclient - HttpClientBuilder builder = HttpClientBuilder.create().addInterceptorFirst(requestFilter); + cb.setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build()); + setSSLTls("TLSv1.2", log); + SSLContextBuilder builder = new SSLContextBuilder(); + SSLConnectionSocketFactory sslConnectionSocketFactory = null; + Registry registry; + PoolingHttpClientConnectionManager cm = null; + if (disableSSLValidation) { + try { + builder.loadTrustMaterial(null, new TrustSelfSignedStrategy()); + sslConnectionSocketFactory = new SSLConnectionSocketFactory(builder.build(), NoopHostnameVerifier.INSTANCE); + registry = RegistryBuilder.create() + .register("http", new PlainConnectionSocketFactory()) + .register(HTTPS, sslConnectionSocketFactory) + .build(); + cm = new PoolingHttpClientConnectionManager(registry); + cm.setMaxTotal(100); + } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { + log.error(e.getMessage()); + } + cb.setSSLSocketFactory(sslConnectionSocketFactory); + cb.setConnectionManager(cm); + } else { + String customTrustStore = System.getProperty("javax.net.ssl.trustStore"); + if(!StringUtils.isEmpty(customTrustStore)) + this.log.info("Custom truststore is configured. Ensure that trusted certificate for all CxSAST/CxSCA endpoints are imported. Custom store path: " + customTrustStore ); + cb.setConnectionManager(getHttpConnectionManager(false)); + } + cb.setConnectionManagerShared(true); + + if (isProxy) { + if (!setCustomProxy(cb, proxyConfig, log)) { + cb.useSystemProperties(); + } + } + + if (Boolean.TRUE.equals(useSSo)) { + cb.setDefaultCredentialsProvider(new WindowsCredentialsProvider(new SystemDefaultCredentialsProvider())); + cb.setDefaultCookieStore(cookieStore); + } else { + cb.setConnectionReuseStrategy(new NoConnectionReuseStrategy()); + } + cb.setDefaultAuthSchemeRegistry(getAuthSchemeProviderRegistry()); + + if (useNTLM) { + setNTLMProxy(proxyConfig, cb, log); + } else apacheClient = cb.build(); + } + + public CxHttpClient(String rootUri, String origin, String originUrl, boolean disableSSLValidation, boolean isSSO, String refreshToken, + boolean isProxy, @Nullable ProxyConfig proxyConfig, Logger log, Boolean useNTLM, String pluginVersion) throws CxClientException { + this(rootUri, origin, disableSSLValidation, isSSO, refreshToken, isProxy, proxyConfig, log, useNTLM, pluginVersion); + this.cxOriginUrl = originUrl; + } + + public void setRootUri(String rootUri) { + this.rootUri = rootUri; + } + + public String getRootUri() { + return rootUri; + } + + private void setNTLMProxy(ProxyConfig proxyConfig, HttpClientBuilder cb, Logger log) { + + if (proxyConfig == null || + StringUtils.isEmpty(proxyConfig.getHost()) || + proxyConfig.getPort() == 0) { + log.info("Proxy configuration not provided."); + apacheClient = cb.build(); + return; + } + + log.info("Setting NTLM proxy for Checkmarx http client"); + HttpHost proxy = new HttpHost(proxyConfig.getHost(), proxyConfig.getPort(), "http"); + + HashMap userDomainMap = splitDomainAndTheUserName(proxyConfig.getUsername()); + String user = userDomainMap.get(KEY_USER); + String domain = userDomainMap.get(KEY_DOMAIN); + + CredentialsProvider credsProvider = new BasicCredentialsProvider(); + final NTCredentials credentials = new NTCredentials(user, proxyConfig.getPassword(), null, domain); + credsProvider.setCredentials(AuthScope.ANY, credentials); + + apacheClient = HttpClientBuilder.create() + .setDefaultCredentialsProvider(credsProvider) + .setProxy(proxy) + .setProxyAuthenticationStrategy(ProxyAuthenticationStrategy.INSTANCE) + .setDefaultRequestConfig(RequestConfig.custom() + .setAuthenticationEnabled(true) + .setProxyPreferredAuthSchemes(Arrays.asList(AuthPolicy.NTLM)) + .build() + ) + .build(); + } + + private static HashMap splitDomainAndTheUserName(String userName) { + String domain = ""; + String user = ""; + // If the username has a backslash, then the domain is the first part and the username is the second part + if (userName.contains("\\")) { + String[] parts = userName.split("[\\\\]"); + if (parts.length == 2) { + domain = parts[0]; + user = parts[1]; + } + } else if (userName.contains("/")) { + // If the username has a slash, then the domain is the first part and the username is the second part + String[] parts = userName.split("[/]"); + if (parts.length == 2) { + domain = parts[0]; + user = parts[1]; + } + } else if (userName.contains("@")) { + // If the username has an asterisk, then the domain is the second part and the username is the first part + String[] parts = userName.split("[@]"); + if (parts.length == 2) { + user = parts[0]; + domain = parts[1]; + } + } + + HashMap userDomain = new HashMap(); + userDomain.put(KEY_USER, user); + userDomain.put(KEY_DOMAIN, domain); + return userDomain; + } + + private static boolean setCustomProxy(HttpClientBuilder cb, ProxyConfig proxyConfig, Logger logi) { + if (proxyConfig == null || + StringUtils.isEmpty(proxyConfig.getHost()) || + proxyConfig.getPort() == 0) { + return false; + } + + String scheme = proxyConfig.isUseHttps() ? HTTPS : "http"; + HttpHost proxy = new HttpHost(proxyConfig.getHost(), proxyConfig.getPort(), scheme); + if (StringUtils.isNotEmpty(proxyConfig.getUsername()) && + StringUtils.isNotEmpty(proxyConfig.getPassword())) { + UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(proxyConfig.getUsername(), proxyConfig.getPassword()); + CredentialsProvider credsProvider = new BasicCredentialsProvider(); + credsProvider.setCredentials(new AuthScope(proxy), credentials); + cb.setDefaultCredentialsProvider(credsProvider); + } + + logi.info("Setting proxy for Checkmarx http client"); + cb.setProxy(proxy); + cb.setRoutePlanner(getRoutePlanner(proxyConfig, proxy, logi)); + cb.setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy()); + return true; + } + + private static DefaultProxyRoutePlanner getRoutePlanner(ProxyConfig proxyConfig, HttpHost proxyHost, Logger logi) { + return new DefaultProxyRoutePlanner(proxyHost) { + public HttpRoute determineRoute( + final HttpHost host, + final HttpRequest request, + final HttpContext context) throws HttpException { + String hostname = host.getHostName(); + String noHost = proxyConfig.getNoproxyHosts(); // StringUtils.isNotEmpty(HTTP_NO_HOST) ? HTTP_NO_HOST : HTTPS_NO_HOST; + if (StringUtils.isNotEmpty(noHost)) { + String[] hosts = noHost.split("\\|"); + for (String nonHost : hosts) { + try { + if (matchNonProxyHostWildCard(hostname, noHost)) { + logi.debug("Bypassing proxy as host " + hostname + " is found in the nonProxyHosts"); + return new HttpRoute(host); + } + } catch (PatternSyntaxException e) { + logi.warn("Wrong nonProxyHost param: " + nonHost); + } + } + } + return super.determineRoute(host, request, context); + } + }; + } + + /* + * '*' is the only wildcard support in nonProxyHosts JVM argument. + * * in Java regex has different meaning than required here. + * Hence the custom logic + */ + private static boolean matchNonProxyHostWildCard(String sourceHost, String nonProxyHost) { + if (nonProxyHost.indexOf("*") > -1) + nonProxyHost = nonProxyHost.replaceAll("\\.", "\\\\."); + + nonProxyHost = nonProxyHost.replaceAll("\\*", "\\.\\*"); + + Pattern p = Pattern.compile(nonProxyHost);//. represents single character + Matcher m = p.matcher(sourceHost); + return m.matches(); + } + + + private static SSLConnectionSocketFactory getTrustAllSSLSocketFactory() { + TrustStrategy acceptingTrustStrategy = new TrustAllStrategy(); + SSLContext sslContext; + try { + sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build(); + } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { + throw new CxClientException("Fail to set trust all certificate, 'SSLConnectionSocketFactory'", e); + } + return new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); + } + + private static PoolingHttpClientConnectionManager getHttpConnectionManager(boolean disableSSLValidation) { + ConnectionSocketFactory factory; if (disableSSLValidation) { - builder = disableCertificateValidation(builder, logi); + factory = getTrustAllSSLSocketFactory(); + } else { + factory = new SSLConnectionSocketFactory(SSLContexts.createDefault(), NoopHostnameVerifier.INSTANCE); + } + Registry socketFactoryRegistry = RegistryBuilder.create() + .register(HTTPS, factory) + .register("http", new PlainConnectionSocketFactory()) + .build(); + PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); + connManager.setMaxTotal(50); + connManager.setDefaultMaxPerRoute(5); + return connManager; + } + + private static Registry getAuthSchemeProviderRegistry() { + return RegistryBuilder.create() + .register(AuthSchemes.DIGEST, new DigestSchemeFactory()) + .register(AuthSchemes.BASIC, new BasicSchemeFactory()) + .register(AuthSchemes.NTLM, new WindowsNTLMSchemeFactory(null)) + .register(AuthSchemes.SPNEGO, new WindowsNegotiateSchemeFactory(null)) + .build(); + } + + public void login(LoginSettings settings) throws IOException { + lastLoginSettings = settings; + + if (!settings.getSessionCookies().isEmpty()) { + setSessionCookies(settings.getSessionCookies()); + return; + } + + if (settings.getRefreshToken() != null) { + token = getAccessTokenFromRefreshToken(settings); + } else if (Boolean.TRUE.equals(useSSo)) { + if (settings.getVersion().equals("lower than 9.0")) { + ssoLegacyLogin(); + } else { + token = ssoLogin(); + // Don't delete this print. VS Code plugin relies on CxCLI output to work properly. + // Also we don't want the token to appear in regular logs. + System.out.printf("Access Token: %s%n", token.getAccess_token()); // NOSONAR: we need standard output here. + } + } else { + token = generateToken(settings); + } + } + + public ArrayList ssoLegacyLogin() { + HttpUriRequest request; + HttpResponse loginResponse = null; + + try { + request = RequestBuilder.post() + .setUri(rootUri + "auth/ssologin") + .setConfig(RequestConfig.DEFAULT) + .setEntity(new StringEntity("", StandardCharsets.UTF_8)) + .build(); + + loginResponse = apacheClient.execute(request); + + } catch (IOException e) { + String message = LOGIN_FAILED_MSG + e.getMessage(); + log.error(message); + throw new CxClientException(message); + } finally { + HttpClientUtils.closeQuietly(loginResponse); + } + setSessionCookies(cookieStore.getCookies()); + + //return cookies clone - for IDE's usage + return new ArrayList<>(cookieStore.getCookies()); + } + + private void setSessionCookies(List cookies) { + String cxCookie = null; + String csrfToken = null; + + for (Cookie cookie : cookies) { + if (cookie.getName().equals(CSRF_TOKEN_HEADER)) { + csrfToken = cookie.getValue(); + } + if (cookie.getName().equals("cxCookie")) { + cxCookie = cookie.getValue(); + } + } + + List

headers = new ArrayList<>(); + headers.add(new BasicHeader(CSRF_TOKEN_HEADER, csrfToken)); + headers.add(new BasicHeader("cookie", String.format("CXCSRFToken=%s; cxCookie=%s", csrfToken, cxCookie))); + + // Don't delete these prints, they are being used on VS Code plugin + System.out.println(CSRF_TOKEN_HEADER + ": " + csrfToken); + System.out.printf("cookie: CXCSRFToken=%s; cxCookie=%s%n", csrfToken, cxCookie); + + apacheClient = cb.setDefaultHeaders(headers).build(); + } + + private TokenLoginResponse ssoLogin() { + HttpUriRequest request; + HttpResponse response; + final String BASE_URL = "/auth/identity/"; + + RequestConfig requestConfig = RequestConfig.custom() + .setRedirectsEnabled(false) + .setAuthenticationEnabled(true) + .setCookieSpec(CookieSpecs.STANDARD) + .build(); + try { + //Request1 + request = RequestBuilder.post() + .setUri(rootUri + SSO_AUTHENTICATION) + .setConfig(requestConfig) + .setHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.toString()) + .setEntity(generateSSOEntity()) + .build(); + + response = apacheClient.execute(request); + + //Request2 + String cookies = retrieveCookies(); + String redirectURL = response.getHeaders(LOCATION_HEADER)[0].getValue(); + request = RequestBuilder.get() + .setUri(rootUri + BASE_URL + redirectURL) + .setConfig(requestConfig) + .setHeader("Cookie", cookies) + .setHeader("Upgrade-Insecure-Requests", "1") + .build(); + response = apacheClient.execute(request); + + //Request3 + cookies = retrieveCookies(); + redirectURL = response.getHeaders(LOCATION_HEADER)[0].getValue(); + redirectURL = rootUri + redirectURL.replace("/CxRestAPI/", ""); + request = RequestBuilder.get() + .setUri(redirectURL) + .setConfig(requestConfig) + .setHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.toString()) + .setHeader("Cookie", cookies) + .build(); + response = apacheClient.execute(request); + return extractToken(response); + } catch (IOException e) { + throw new CxClientException(LOGIN_FAILED_MSG + e.getMessage()); + } + } + + private TokenLoginResponse extractToken(HttpResponse response) { + String redirectURL = response.getHeaders(LOCATION_HEADER)[0].getValue(); + if (!redirectURL.contains("access_token")) { + throw new CxClientException("Failed retrieving access token from server"); } - apacheClient = builder.build(); + return new Gson().fromJson(urlToJson(redirectURL), TokenLoginResponse.class); + } + + private String urlToJson(String url) { + url = url.replace("=", "\":\""); + url = url.replace("&", "\",\""); + return "{\"" + url + "\"}"; + } + + private String retrieveCookies() { + List cookieList = cookieStore.getCookies(); + final StringBuilder builder = new StringBuilder(); + cookieList.forEach(cookie -> + builder.append(cookie.getName()).append("=").append(cookie.getValue()).append(";")); + return builder.toString(); + } + + public TokenLoginResponse generateToken(LoginSettings settings) throws IOException { + UrlEncodedFormEntity requestEntity = getAuthRequest(settings); + HttpPost post = new HttpPost(settings.getAccessControlBaseUrl()); + try { + return request(post, ContentType.APPLICATION_FORM_URLENCODED.toString(), requestEntity, + TokenLoginResponse.class, HttpStatus.SC_OK, AUTH_MESSAGE, false, false); + } catch (CxClientException e) { + if (!e.getMessage().contains("invalid_scope")) { + throw new CxClientException(String.format("Failed to generate access token, failure error was: %s", e.getMessage()), e); + } + ClientType.RESOURCE_OWNER.setScopes("sast_rest_api"); + settings.setClientTypeForPasswordAuth(ClientType.RESOURCE_OWNER); + UrlEncodedFormEntity requestEntityForSecondLoginRetry = getAuthRequest(settings); + HttpPost post_1 = new HttpPost(settings.getAccessControlBaseUrl()); + return request(post_1, ContentType.APPLICATION_FORM_URLENCODED.toString(), requestEntityForSecondLoginRetry, + TokenLoginResponse.class, HttpStatus.SC_OK, AUTH_MESSAGE, false, false); + } + } + + private TokenLoginResponse getAccessTokenFromRefreshToken(LoginSettings settings) throws IOException { + UrlEncodedFormEntity requestEntity = getTokenRefreshingRequest(settings); + HttpPost post = new HttpPost(settings.getAccessControlBaseUrl()); + try { + return request(post, ContentType.APPLICATION_FORM_URLENCODED.toString(), requestEntity, + TokenLoginResponse.class, HttpStatus.SC_OK, AUTH_MESSAGE, false, false); + } catch (CxClientException e) { + throw new CxClientException(String.format("Failed to generate access token from refresh token. The error was: %s", e.getMessage()), e); + } + } + + public void revokeToken(String token) throws IOException { + UrlEncodedFormEntity requestEntity = getRevocationRequest(ClientType.CLI, token); + HttpPost post = new HttpPost(rootUri + REVOCATION); + try { + request(post, ContentType.APPLICATION_FORM_URLENCODED.toString(), requestEntity, + String.class, HttpStatus.SC_OK, "revocation", false, false); + } catch (CxClientException e) { + throw new CxClientException(String.format("Token revocation failure error was: %s", e.getMessage()), e); + } + } + + private static UrlEncodedFormEntity getRevocationRequest(ClientType clientType, String token) { + List parameters = new ArrayList<>(); + parameters.add(new BasicNameValuePair("token_type_hint", REFRESH_TOKEN_PROP)); + parameters.add(new BasicNameValuePair("token", token)); + parameters.add(new BasicNameValuePair(CLIENT_ID_PROP, clientType.getClientId())); + parameters.add(new BasicNameValuePair(CLIENT_SECRET_PROP, clientType.getClientSecret())); + + return new UrlEncodedFormEntity(parameters, StandardCharsets.UTF_8); } - public void login() throws IOException, CxClientException { - UrlEncodedFormEntity requestEntity = generateUrlEncodedFormEntity(); - HttpPost post = new HttpPost(rootUri + AUTHENTICATION); - token = request(post, ContentType.APPLICATION_FORM_URLENCODED.toString(), requestEntity, TokenLoginResponse.class, HttpStatus.SC_OK, "authenticate", false, false); + private static UrlEncodedFormEntity getAuthRequest(LoginSettings settings) { + ClientType clientType = settings.getClientTypeForPasswordAuth(); + String grantType = StringUtils.defaultString(clientType.getGrantType(), DEFAULT_GRANT_TYPE); + List parameters = new ArrayList<>(); + parameters.add(new BasicNameValuePair("username", settings.getUsername())); + parameters.add(new BasicNameValuePair(PASSWORD_PROP, settings.getPassword())); + parameters.add(new BasicNameValuePair("grant_type", grantType)); + parameters.add(new BasicNameValuePair("scope", clientType.getScopes())); + parameters.add(new BasicNameValuePair(CLIENT_ID_PROP, clientType.getClientId())); + parameters.add(new BasicNameValuePair(CLIENT_SECRET_PROP, clientType.getClientSecret())); + + if (!StringUtils.isEmpty(settings.getTenant())) { + String authContext = String.format("Tenant:%s", settings.getTenant()); + parameters.add(new BasicNameValuePair("acr_values", authContext)); + } + + return new UrlEncodedFormEntity(parameters, StandardCharsets.UTF_8); } - private UrlEncodedFormEntity generateUrlEncodedFormEntity() throws UnsupportedEncodingException { - List parameters = new ArrayList(); - parameters.add(new BasicNameValuePair("username", username)); - parameters.add(new BasicNameValuePair("password", password)); - parameters.add(new BasicNameValuePair("grant_type", "password")); - parameters.add(new BasicNameValuePair("scope", "sast_rest_api cxarm_api")); - parameters.add(new BasicNameValuePair("client_id", "resource_owner_client")); - parameters.add(new BasicNameValuePair("client_secret", "014DF517-39D1-4453-B7B3-9930C563627C")); + private static UrlEncodedFormEntity getTokenRefreshingRequest(LoginSettings settings) throws UnsupportedEncodingException { + ClientType clientType = settings.getClientTypeForRefreshToken(); + List parameters = new ArrayList<>(); + parameters.add(new BasicNameValuePair("grant_type", REFRESH_TOKEN_PROP)); + parameters.add(new BasicNameValuePair(CLIENT_ID_PROP, clientType.getClientId())); + parameters.add(new BasicNameValuePair(CLIENT_SECRET_PROP, clientType.getClientSecret())); + parameters.add(new BasicNameValuePair(REFRESH_TOKEN_PROP, settings.getRefreshToken())); - return new UrlEncodedFormEntity(parameters, "utf-8"); + return new UrlEncodedFormEntity(parameters, StandardCharsets.UTF_8.name()); } //GET REQUEST - public T getRequest(String relPath, String contentType, Class responseType, int expectStatus, String failedMsg, boolean isCollection) throws IOException, CxClientException { - return getRequest(rootUri, relPath, CONTENT_TYPE_APPLICATION_JSON, contentType, responseType, expectStatus, failedMsg, isCollection); + public T getRequest(String relPath, String contentType, Class responseType, int expectStatus, String failedMsg, boolean isCollection) throws IOException { + return getRequest(rootUri, relPath, CONTENT_TYPE_APPLICATION_JSON, contentType, responseType, expectStatus, failedMsg, isCollection); } - public T getRequest(String rootURL, String relPath, String acceptHeader, String contentType, Class responseType, int expectStatus, String failedMsg, boolean isCollection) throws IOException, CxClientException { - HttpGet get = new HttpGet(rootURL + relPath); + public T getRequest(String rootURL, String relPath, String acceptHeader, String contentType, Class responseType, int expectStatus, String failedMsg, boolean isCollection) throws IOException { + HttpGet get = new HttpGet(rootURL + relPath); get.addHeader(HttpHeaders.ACCEPT, acceptHeader); return request(get, contentType, null, responseType, expectStatus, "get " + failedMsg, isCollection, true); } //POST REQUEST - public T postRequest(String relPath, String contentType, HttpEntity entity, Class responseType, int expectStatus, String failedMsg) throws IOException, CxClientException { + public T postRequest(String relPath, String contentType, HttpEntity entity, Class responseType, int expectStatus, String failedMsg) throws IOException { HttpPost post = new HttpPost(rootUri + relPath); - return request(post, contentType, entity, responseType, expectStatus, failedMsg, false, true); - } + return request(post, contentType, entity, responseType, expectStatus, failedMsg, false, true); + } + + // POST REQUEST + public T postRequest(String relPath, String contentType, String acceptHeader, HttpEntity entity, + Class responseType, int expectStatus, String failedMsg) throws IOException { + HttpPost post = new HttpPost(rootUri + relPath); + post.addHeader("Accept", acceptHeader); + return request(post, contentType, entity, responseType, expectStatus, failedMsg, false, true); + } //PUT REQUEST - public T putRequest(String relPath, String contentType, HttpEntity entity, Class responseType, int expectStatus, String failedMsg) throws IOException, CxClientException { + public T putRequest(String relPath, String contentType, HttpEntity entity, Class responseType, int expectStatus, String failedMsg) throws IOException { HttpPut put = new HttpPut(rootUri + relPath); return request(put, contentType, entity, responseType, expectStatus, failedMsg, false, true); } //PATCH REQUEST - public void patchRequest(String relPath, String contentType, HttpEntity entity, int expectStatus, String failedMsg) throws IOException, CxClientException { + public void patchRequest(String relPath, String contentType, HttpEntity entity, int expectStatus, String failedMsg) throws IOException { HttpPatch patch = new HttpPatch(rootUri + relPath); request(patch, contentType, entity, null, expectStatus, failedMsg, false, true); } - private T request(HttpRequestBase httpMethod, String contentType, HttpEntity entity, Class responseType, int expectStatus, String failedMsg, boolean isCollection, boolean retry) throws IOException, CxClientException { - if (contentType != null) { + public void setTeamPathHeader(String teamPath) { + this.teamPath = teamPath; + } + + public void addCustomHeader(String name, String value) { + log.debug(String.format("Adding a custom header: %s: %s", name, value)); + customHeaders.put(name, value); + } + + private String getUserAgentValue() { + if (cxOrigin == null) { + log.warn("cxOrigin is null"); + cxOrigin = "unknown"; // Or handle as appropriate + } + + String version = (pluginVersion != null ) ? pluginVersion : "unknown"; // Ensure cxVersion is not null + + return "plugin_name=" + cxOrigin + ";plugin_version=" + version; + } + + private T request(HttpRequestBase httpMethod, String contentType, HttpEntity entity, Class responseType, int expectStatus, String failedMsg, boolean isCollection, boolean retry) throws IOException { + //Support unicode characters + if (httpMethod.getURI() != null && (StringUtils.isNotEmpty(httpMethod.getURI().getHost()) || + StringUtils.isNotEmpty(httpMethod.getURI().getAuthority()))) { + URI tmpUri = httpMethod.getURI(); + String host = StringUtils.isNotEmpty(tmpUri.getAuthority()) ? tmpUri.getAuthority() : tmpUri.getHost(); + host = IDN.toASCII(host, IDN.ALLOW_UNASSIGNED); + String hostname = host; + String portNumber = "" + tmpUri.getPort(); + String[] arr = host.split(":"); + if(arr != null && arr.length>1) { + hostname = arr[0]; + portNumber = arr[1]; + } + try { + URIBuilder uriBuilder = new URIBuilder(tmpUri).setHost(hostname).setPort(Integer.parseInt(portNumber)); + URI uri = uriBuilder.build(); + httpMethod.setURI(uri); + } catch (URISyntaxException e) { + log.error("Fail to convert URI: " + httpMethod.getURI().toString()); + } + } + if (contentType != null) { httpMethod.addHeader("Content-type", contentType); } + if (getUserAgentValue() != null) { + httpMethod.addHeader("User-Agent", getUserAgentValue()); + } if (entity != null && httpMethod instanceof HttpEntityEnclosingRequestBase) { //Entity for Post methods ((HttpEntityEnclosingRequestBase) httpMethod).setEntity(entity); } + HttpResponse response = null; + int statusCode = 0; try { + httpMethod.addHeader(ORIGIN_HEADER, cxOrigin); + httpMethod.addHeader(ORIGIN_URL_HEADER, cxOriginUrl); + httpMethod.addHeader(TEAM_PATH, this.teamPath); + if (token != null) { + httpMethod.addHeader(HttpHeaders.AUTHORIZATION, token.getToken_type() + " " + token.getAccess_token()); + } + + for (Map.Entry entry : customHeaders.entrySet()) { + httpMethod.addHeader(entry.getKey(), entry.getValue()); + } response = apacheClient.execute(httpMethod); + statusCode = response.getStatusLine().getStatusCode(); - if (response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) { //Token expired + if (statusCode == HttpStatus.SC_UNAUTHORIZED) { // Token has probably expired throw new CxTokenExpiredException(extractResponseBody(response)); } + validateResponse(response, expectStatus, "Failed to " + failedMsg); //extract response as object and return the link return convertToObject(response, responseType, isCollection); - } catch (UnknownHostException e){ - throw new CxHTTPClientException(ErrorMessage.CHECKMARX_SERVER_CONNECTION_FAILED.getErrorMessage()); + } catch (UnknownHostException e) { + log.error(e.getMessage()); + throw new CxHTTPClientException(ErrorMessage.CHECKMARX_SERVER_CONNECTION_FAILED.getErrorMessage(), e); } catch (CxTokenExpiredException ex) { if (retry) { - logi.warn("Access token expired, requesting a new token"); - login(); - return request(httpMethod, contentType, entity, responseType, expectStatus, failedMsg, isCollection, false); + logTokenError(httpMethod, statusCode, ex); + if (lastLoginSettings != null) { + login(lastLoginSettings); + removeHeaders(httpMethod); + return request(httpMethod, contentType, entity, responseType, expectStatus, failedMsg, isCollection, false); + } } throw ex; } finally { @@ -156,24 +728,97 @@ private T request(HttpRequestBase httpMethod, String contentType, HttpEntity } } + private void removeHeaders(HttpRequestBase httpMethod) { + httpMethod.removeHeaders("Content-type"); + httpMethod.removeHeaders(ORIGIN_HEADER); + httpMethod.removeHeaders(ORIGIN_URL_HEADER); + httpMethod.removeHeaders(TEAM_PATH); + httpMethod.removeHeaders(HttpHeaders.AUTHORIZATION); + } + public void close() { HttpClientUtils.closeQuietly(apacheClient); } - private HttpClientBuilder disableCertificateValidation(HttpClientBuilder builder, Logger logi) { + private void setSSLTls(String protocol, Logger log) { try { - SSLContext disabledSSLContext = SSLContexts.custom().loadTrustMaterial(new TrustStrategy() { - public boolean isTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { - return true; - } - }).build(); - builder.setSslcontext(disabledSSLContext); - builder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE); - } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) { - logi.warn("Failed to disable certificate verification: " + e.getMessage()); + final SSLContext sslContext = SSLContext.getInstance(protocol); + sslContext.init(null, null, null); + HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory()); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + log.warn(String.format("Failed to set SSL TLS : %s", e.getMessage())); + } + } + + //TODO handle missing scope issue with management_and_orchestration_api + private StringEntity generateSSOEntity() { + final String clientId = "cxsast_client"; + final String redirectUri = "%2Fcxwebclient%2FauthCallback.html%3F"; + final String responseType = "id_token%20token"; + final String nonce = "9313f0902ba64e50bc564f5137f35a52"; + final String isPrompt = "true"; + final String scopes = "sast_api openid sast-permissions access-control-permissions access_control_api management_and_orchestration_api".replace(" ", "%20"); + final String providerId = "2"; //windows provider id + + String redirectUrl = MessageFormat.format("/CxRestAPI/auth/identity/connect/authorize/callback" + + "?client_id={0}" + + "&redirect_uri={1}" + redirectUri + + "&response_type={2}" + + "&scope={3}" + + "&nonce={4}" + + "&prompt={5}" + , clientId, rootUri, responseType, scopes, nonce, isPrompt); + try { + List urlParameters = new ArrayList<>(); + urlParameters.add(new BasicNameValuePair("redirectUrl", redirectUrl)); + urlParameters.add(new BasicNameValuePair("providerid", providerId)); + return new UrlEncodedFormEntity(urlParameters, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new CxClientException(e.getMessage()); } + } + + public void setToken(TokenLoginResponse token) { + this.token = token; + } - return builder; + private void logTokenError(HttpRequestBase httpMethod, int statusCode, CxTokenExpiredException ex) { + String message = String.format("Received status code %d for URL: %s with the message: %s", + statusCode, + httpMethod.getURI(), + ex.getMessage()); + + log.warn(message); + + log.info("Possible reason: access token has expired. Trying to request a new token..."); + } + + /* + * This will return string from encoded access token + * which will use to identify which language is used in SAST + * + * */ + public String getLanguageFromAccessToken() { + String languageForSAST = "en-US"; + try { + + String actToken = token.getAccess_token(); + String[] split_string = actToken.split("\\."); + if (split_string != null && split_string.length > 0) { + String base64EncodedBody = split_string[1]; + Base64 base64Url = new Base64(true); + String body = new String(base64Url.decode(base64EncodedBody)); + String tokenToParse = body.replace("\"", "'"); + JSONObject json = new JSONObject(tokenToParse); + languageForSAST = json.getString("locale"); + log.info("Locale used in CxSAST is " + languageForSAST); + + } + } catch (Exception ex) { + // In case the SAST used will not have token, set to default English language + languageForSAST = "en-US"; + } + return languageForSAST; } } diff --git a/src/main/java/com/cx/restclient/httpClient/utils/ContentType.java b/src/main/java/com/cx/restclient/httpClient/utils/ContentType.java index de4a4616..7dfdd1e1 100644 --- a/src/main/java/com/cx/restclient/httpClient/utils/ContentType.java +++ b/src/main/java/com/cx/restclient/httpClient/utils/ContentType.java @@ -5,9 +5,15 @@ */ public class ContentType { public static final String CONTENT_TYPE_APPLICATION_JSON = "application/json"; + public static final String CONTENT_TYPE_API_VERSION_1_2 = "application/json;v=1.2"; + public static final String CONTENT_TYPE_API_VERSION_1_1 = "application/json;v=1.1"; + public static final String CONTENT_TYPE_APPLICATION_JSON_V4 = "application/json;v=4.0"; + public static final String CONTENT_TYPE_APPLICATION_JSON_V21 = "application/json;v=2.1"; + public static final String CONTENT_TYPE_APPLICATION_JSON_V2 = "application/json;v=2.0"; public static final String CONTENT_TYPE_APPLICATION_JSON_V1 = "application/json;v=1.0"; public static final String CONTENT_TYPE_APPLICATION_XML_V1 = "application/xml;v=1.0"; public static final String CONTENT_TYPE_APPLICATION_PDF_V1 = "application/pdf;v=1.0"; public static final String CONTENT_TYPE_APPLICATION_RTF_V1 = "application/rtf;v=1.0"; public static final String CONTENT_TYPE_APPLICATION_CSV_V1 = "application/csv;v=1.0"; + public static final String CONTENT_TYPE_APPLICATION_XML_V6 = "application/json;v=6.0"; } diff --git a/src/main/java/com/cx/restclient/httpClient/utils/HttpClientHelper.java b/src/main/java/com/cx/restclient/httpClient/utils/HttpClientHelper.java index dcda4b49..56d066e4 100644 --- a/src/main/java/com/cx/restclient/httpClient/utils/HttpClientHelper.java +++ b/src/main/java/com/cx/restclient/httpClient/utils/HttpClientHelper.java @@ -1,17 +1,23 @@ package com.cx.restclient.httpClient.utils; -import com.cx.restclient.common.ErrorMessage; -import com.cx.restclient.common.ErrorUtil; import com.cx.restclient.exception.CxClientException; import com.cx.restclient.exception.CxHTTPClientException; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.TypeFactory; import org.apache.commons.io.IOUtils; import org.apache.http.HttpResponse; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.List; @@ -19,7 +25,21 @@ * Created by Galn on 06/02/2018. */ public abstract class HttpClientHelper { + private HttpClientHelper() { + } + public static T convertToObject(HttpResponse response, Class responseType, boolean isCollection) throws IOException, CxClientException { + + if (responseType != null && responseType.isInstance(response)) { + return (T) response; + } + + // If the caller is asking for the whole response, return the response (instead of just its entity), + // no matter if the entity is empty. + if (responseType != null && responseType.isAssignableFrom(response.getClass())) { + return (T) response; + } + //No content if (responseType == null || response.getEntity() == null || response.getEntity().getContentLength() == 0) { return null; @@ -32,17 +52,21 @@ public static T convertToObject(HttpResponse response, Class responseType if (isCollection) { return convertToCollectionObject(response, TypeFactory.defaultInstance().constructCollectionType(List.class, responseType)); } + //convert to T return convertToStrObject(response, responseType); } private static T convertToStrObject(HttpResponse response, Class valueType) throws CxClientException { - ObjectMapper mapper = new ObjectMapper(); + ObjectMapper mapper = getObjectMapper(); try { if (response.getEntity() == null) { return null; } String json = IOUtils.toString(response.getEntity().getContent(), Charset.defaultCharset()); + if (valueType.equals(String.class)) { + return (T) json; + } return mapper.readValue(json, valueType); } catch (IOException e) { @@ -51,7 +75,7 @@ private static T convertToStrObject(HttpResponse response, Class valueTyp } public static String convertToJson(Object o) throws CxClientException { - ObjectMapper mapper = new ObjectMapper(); + ObjectMapper mapper = getObjectMapper(); try { return mapper.writeValueAsString(o); } catch (Exception e) { @@ -59,8 +83,12 @@ public static String convertToJson(Object o) throws CxClientException { } } + public static StringEntity convertToStringEntity(Object o) throws CxClientException, UnsupportedEncodingException { + return new StringEntity(convertToJson(o)); + } + private static T convertToCollectionObject(HttpResponse response, JavaType javaType) throws CxClientException { - ObjectMapper mapper = new ObjectMapper(); + ObjectMapper mapper = getObjectMapper(); try { String json = IOUtils.toString(response.getEntity().getContent(), Charset.defaultCharset()); return mapper.readValue(json, javaType); @@ -69,23 +97,50 @@ private static T convertToCollectionObject(HttpResponse response, JavaType j } } - public static void validateResponse(HttpResponse response, int status, String message) throws CxClientException { - if (response.getStatusLine().getStatusCode() != status) { - if (ErrorUtil.isServerErrorCodes(response.getStatusLine().getStatusCode())) { - throw new CxClientException(ErrorMessage.SERVICE_UNAVAILABLE.getErrorMessage()); - } + private static ObjectMapper getObjectMapper() { + ObjectMapper result = new ObjectMapper(); + + // Prevent UnrecognizedPropertyException if additional fields are added to API responses. + result.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + return result; + } + + public static void validateResponse(HttpResponse response, int expectedStatus, String message) throws CxClientException { + int actualStatusCode = response.getStatusLine().getStatusCode(); + if (actualStatusCode != expectedStatus) { String responseBody = extractResponseBody(response); - responseBody = responseBody.replace("{", "").replace("}", "").replace(System.getProperty("line.separator"), " ").replace(" ", ""); - throw new CxHTTPClientException(response.getStatusLine().getStatusCode(), message + ": " + responseBody); - } + String readableBody = responseBody.replace("{", "") + .replace("}", "") + .replace(System.getProperty("line.separator"), " ") + .replace(" ", ""); + + String exceptionMessage = String.format("Status code: %d, message: '%s', response body: %s", + actualStatusCode, message, readableBody); + throw new CxHTTPClientException(actualStatusCode, exceptionMessage, responseBody); + } } public static String extractResponseBody(HttpResponse response) { try { - return IOUtils.toString(response.getEntity().getContent(), Charset.defaultCharset()); + return IOUtils.toString(response.getEntity().getContent()); } catch (Exception e) { return ""; } } + + public static byte[] getSBOMReport(String fileUrl) throws IOException { + HttpGet get = new HttpGet(fileUrl); + try (CloseableHttpClient client = HttpClients.createDefault(); + CloseableHttpResponse response = client.execute(get)) { + + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == 200) { + return EntityUtils.toByteArray(response.getEntity()); + } else { + throw new IOException("Failed to fetch file. Status code: " + statusCode); + } + } + } + } diff --git a/src/main/java/com/cx/restclient/osa/dto/CVEReportTableRow.java b/src/main/java/com/cx/restclient/osa/dto/CVEReportTableRow.java index 603c05ad..fb1081a0 100644 --- a/src/main/java/com/cx/restclient/osa/dto/CVEReportTableRow.java +++ b/src/main/java/com/cx/restclient/osa/dto/CVEReportTableRow.java @@ -1,7 +1,13 @@ package com.cx.restclient.osa.dto; +import com.cx.restclient.ast.dto.sca.report.Finding; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + import java.io.Serializable; +import static com.cx.restclient.common.ShragaUtils.formatDate; + +@JsonIgnoreProperties(ignoreUnknown = true) public class CVEReportTableRow implements Serializable { private String name; @@ -18,6 +24,21 @@ public CVEReportTableRow(String name, String severity, String publishDate, Strin this.state = state; } + public CVEReportTableRow(CVE cve) { + this.state = cve.getState().getName(); + this.name = cve.getCveName(); + this.publishDate = cve.getPublishDate(); + this.libraryName = cve.getLibraryId(); + + } + + public CVEReportTableRow(Finding finding){ + this.state = finding.isIgnored()?"NOT_EXPLOITABLE":"EXPLOITABLE"; + this.name = finding.getId(); + this.publishDate = formatDate(finding.getPublishDate(), "yyyy-MM-dd'T'HH:mm:ss", "dd/MM/yy"); + this.libraryName = finding.getPackageId(); + } + public String getName() { return name; } diff --git a/src/main/java/com/cx/restclient/osa/dto/ClientType.java b/src/main/java/com/cx/restclient/osa/dto/ClientType.java new file mode 100644 index 00000000..5a0d8797 --- /dev/null +++ b/src/main/java/com/cx/restclient/osa/dto/ClientType.java @@ -0,0 +1,26 @@ +package com.cx.restclient.osa.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Builder +@Getter +@Setter +public class ClientType { + + public static final ClientType RESOURCE_OWNER = new ClientType("resource_owner_client", + "sast_rest_api cxarm_api", + "014DF517-39D1-4453-B7B3-9930C563627C", + null); + + public static final ClientType CLI = new ClientType("cli_client", + "sast_rest_api offline_access", + "B9D84EA8-E476-4E83-A628-8A342D74D3BD", + null); + + private String clientId; + private String scopes; + private String clientSecret; + private String grantType; +} diff --git a/src/main/java/com/cx/restclient/osa/dto/Content.java b/src/main/java/com/cx/restclient/osa/dto/Content.java index 1dc12eda..c50b4219 100644 --- a/src/main/java/com/cx/restclient/osa/dto/Content.java +++ b/src/main/java/com/cx/restclient/osa/dto/Content.java @@ -1,8 +1,10 @@ package com.cx.restclient.osa.dto; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonRawValue; +@JsonIgnoreProperties(ignoreUnknown = true) public class Content { @JsonRawValue diff --git a/src/main/java/com/cx/restclient/osa/dto/CreateOSAScanRequest.java b/src/main/java/com/cx/restclient/osa/dto/CreateOSAScanRequest.java index c4a1b0a3..f603667b 100644 --- a/src/main/java/com/cx/restclient/osa/dto/CreateOSAScanRequest.java +++ b/src/main/java/com/cx/restclient/osa/dto/CreateOSAScanRequest.java @@ -1,7 +1,9 @@ package com.cx.restclient.osa.dto; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +@JsonIgnoreProperties(ignoreUnknown = true) public class CreateOSAScanRequest { @JsonProperty("ProjectId") diff --git a/src/main/java/com/cx/restclient/osa/dto/Library.java b/src/main/java/com/cx/restclient/osa/dto/Library.java index 53790e15..cd3a6f83 100644 --- a/src/main/java/com/cx/restclient/osa/dto/Library.java +++ b/src/main/java/com/cx/restclient/osa/dto/Library.java @@ -1,20 +1,23 @@ package com.cx.restclient.osa.dto; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; import java.io.Serializable; -/** - * Created by zoharby on 09/01/2017. - */ @JsonIgnoreProperties(ignoreUnknown = true) public class Library implements Serializable { private String id;//:"36b32b00-9ee6-4e2f-85c9-3f03f26519a9", private String name;//:"lib-name", private String version;//:"lib-version", + @JsonProperty("criticalUniqueVulnerabilityCount") + private int criticalVulnerabilityCount;//:1, + @JsonProperty("highUniqueVulnerabilityCount") private int highVulnerabilityCount;//:1, + @JsonProperty("mediumUniqueVulnerabilityCount") private int mediumVulnerabilityCount;//:1, + @JsonProperty("lowUniqueVulnerabilityCount") private int lowVulnerabilityCount;//:1, private String newestVersion;//:"1.0.0", private String newestVersionReleaseDate;//:"2016-12-19T10:16:19.1206743Z", @@ -23,7 +26,7 @@ public class Library implements Serializable { public String getId() { - return id; + return this.id; } public void setId(String id) { @@ -31,7 +34,7 @@ public void setId(String id) { } public String getName() { - return name; + return this.name; } public void setName(String name) { @@ -39,15 +42,23 @@ public void setName(String name) { } public String getVersion() { - return version; + return this.version; } public void setVersion(String version) { this.version = version; } + + public int getCriticalVulnerabilityCount() { + return this.criticalVulnerabilityCount; + } + + public void setCriticalVulnerabilityCount(int criticalVulnerabilityCount) { + this.criticalVulnerabilityCount = criticalVulnerabilityCount; + } public int getHighVulnerabilityCount() { - return highVulnerabilityCount; + return this.highVulnerabilityCount; } public void setHighVulnerabilityCount(int highVulnerabilityCount) { @@ -55,7 +66,7 @@ public void setHighVulnerabilityCount(int highVulnerabilityCount) { } public int getMediumVulnerabilityCount() { - return mediumVulnerabilityCount; + return this.mediumVulnerabilityCount; } public void setMediumVulnerabilityCount(int mediumVulnerabilityCount) { @@ -63,7 +74,7 @@ public void setMediumVulnerabilityCount(int mediumVulnerabilityCount) { } public int getLowVulnerabilityCount() { - return lowVulnerabilityCount; + return this.lowVulnerabilityCount; } public void setLowVulnerabilityCount(int lowVulnerabilityCount) { @@ -71,7 +82,7 @@ public void setLowVulnerabilityCount(int lowVulnerabilityCount) { } public String getNewestVersion() { - return newestVersion; + return this.newestVersion; } public void setNewestVersion(String newestVersion) { @@ -79,7 +90,7 @@ public void setNewestVersion(String newestVersion) { } public String getNewestVersionReleaseDate() { - return newestVersionReleaseDate; + return this.newestVersionReleaseDate; } public void setNewestVersionReleaseDate(String newestVersionReleaseDate) { @@ -87,7 +98,7 @@ public void setNewestVersionReleaseDate(String newestVersionReleaseDate) { } public int getNumberOfVersionsSinceLastUpdate() { - return numberOfVersionsSinceLastUpdate; + return this.numberOfVersionsSinceLastUpdate; } public void setNumberOfVersionsSinceLastUpdate(int numberOfVersionsSinceLastUpdate) { @@ -95,11 +106,11 @@ public void setNumberOfVersionsSinceLastUpdate(int numberOfVersionsSinceLastUpda } public int getConfidenceLevel() { - return confidenceLevel; + return this.confidenceLevel; } public void setConfidenceLevel(int confidenceLevel) { this.confidenceLevel = confidenceLevel; } -} +} \ No newline at end of file diff --git a/src/main/java/com/cx/restclient/osa/dto/LoginRequest.java b/src/main/java/com/cx/restclient/osa/dto/LoginRequest.java index e0df293b..1807469c 100644 --- a/src/main/java/com/cx/restclient/osa/dto/LoginRequest.java +++ b/src/main/java/com/cx/restclient/osa/dto/LoginRequest.java @@ -1,9 +1,12 @@ package com.cx.restclient.osa.dto; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + /** * Created by: Dorg. * Date: 08/09/2016. */ +@JsonIgnoreProperties(ignoreUnknown = true) public class LoginRequest { private String username; diff --git a/src/main/java/com/cx/restclient/osa/dto/OSAResults.java b/src/main/java/com/cx/restclient/osa/dto/OSAResults.java index 05466be2..14cd92f4 100644 --- a/src/main/java/com/cx/restclient/osa/dto/OSAResults.java +++ b/src/main/java/com/cx/restclient/osa/dto/OSAResults.java @@ -1,6 +1,8 @@ package com.cx.restclient.osa.dto; -import com.cx.restclient.cxArm.dto.Violation; +import com.cx.restclient.cxArm.dto.Policy; +import com.cx.restclient.dto.Results; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import java.io.Serializable; import java.util.ArrayList; @@ -9,11 +11,13 @@ import java.util.Map; import static com.cx.restclient.common.ShragaUtils.formatDate; +import static com.cx.restclient.cxArm.utils.CxARMUtils.getPolicyList; /** * Created by Galn on 07/02/2018. */ -public class OSAResults implements Serializable { +@JsonIgnoreProperties(ignoreUnknown = true) +public class OSAResults extends Results implements Serializable { private String osaScanId; private OSASummaryResults results; private List osaLibraries; @@ -21,6 +25,7 @@ public class OSAResults implements Serializable { private OSAScanStatus osaScanStatus; private String osaProjectSummaryLink; private boolean osaResultsReady = false; + private List osaCriticalCVEReportTable = new ArrayList(); private List osaHighCVEReportTable = new ArrayList(); private List osaMediumCVEReportTable = new ArrayList(); private List osaLowCVEReportTable = new ArrayList(); @@ -28,9 +33,7 @@ public class OSAResults implements Serializable { private String scanStartTime; private String scanEndTime; - private List osaPolicies = new ArrayList<>(); - private List osaViolations = new ArrayList<>(); - + private List osaPolicies = new ArrayList<>(); public OSAResults() { } @@ -109,6 +112,10 @@ public String getOsaScanId() { public void setOsaScanId(String osaScanId) { this.osaScanId = osaScanId; } + + public List getOsaCriticalCVEReportTable() { + return osaCriticalCVEReportTable; + } public List getOsaHighCVEReportTable() { return osaHighCVEReportTable; @@ -146,7 +153,10 @@ private void setOsaCVEReportTable(List osaVulnerabilities, List os } for (CVEReportTableRow row : cveMap.values()) { - if ("High".equals(row.getSeverity())) { + if ("Critical".equals(row.getSeverity())) { + osaCriticalCVEReportTable.add(row); + } + else if ("High".equals(row.getSeverity())) { osaHighCVEReportTable.add(row); } else if ("Medium".equals(row.getSeverity())) { osaMediumCVEReportTable.add(row); @@ -161,6 +171,10 @@ public void setDates(OSAScanStatus status) { this.scanStartTime = formatDate(status.getStartAnalyzeTime(), "yyyy-MM-dd'T'HH:mm:ss.SSSSSSS", "dd/MM/yy HH:mm"); this.scanEndTime = formatDate(status.getEndAnalyzeTime(), "yyyy-MM-dd'T'HH:mm:ss.SSSSSSS", "dd/MM/yy HH:mm"); } + + public void setOsaCriticalCVEReportTable(List osaCriticalCVEReportTable) { + this.osaCriticalCVEReportTable = osaCriticalCVEReportTable; + } public void setOsaHighCVEReportTable(List osaHighCVEReportTable) { this.osaHighCVEReportTable = osaHighCVEReportTable; @@ -182,23 +196,15 @@ public void setScanEndTime(String scanEndTime) { this.scanEndTime = scanEndTime; } - public List getOsaViolations() { - return osaViolations; - } - - public void setOsaViolations(List osaViolations) { - this.osaViolations = osaViolations; - } - - public void addAllViolations(List violations) { - this.osaViolations.addAll(violations); + public void addPolicy(Policy policy) { + this.osaPolicies.addAll(getPolicyList(policy)); } - public List getOsaPolicies() { + public List getOsaPolicies() { return osaPolicies; } - public void setOsaPolicies(List osaPolicies) { + public void setOsaPolicies(List osaPolicies) { this.osaPolicies = osaPolicies; } } diff --git a/src/main/java/com/cx/restclient/osa/dto/ScanConfiguration.java b/src/main/java/com/cx/restclient/osa/dto/ScanConfiguration.java index 2970b248..d0f55fb0 100644 --- a/src/main/java/com/cx/restclient/osa/dto/ScanConfiguration.java +++ b/src/main/java/com/cx/restclient/osa/dto/ScanConfiguration.java @@ -2,18 +2,28 @@ import com.cx.restclient.sast.dto.Project; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import java.io.File; /** * Created by galn on 21/12/2016. */ +@JsonIgnoreProperties(ignoreUnknown = true) public class ScanConfiguration { private boolean SASTEnabled; - private String OSAEnabled; private String cxOrigin; - private String sourceDir; + private String pluginVersion; + public String getPluginVersion() { + return pluginVersion; + } + + public void setPluginVersion(String pluginVersion) { + this.pluginVersion = pluginVersion; + } + + private String sourceDir; private String tempDir; private String reportsDir; private String username; @@ -31,6 +41,7 @@ public class ScanConfiguration { private boolean isIncremental = false; private boolean isSynchronous = false; private boolean thresholdsEnabled = false; + private Integer criticalThreshold; private Integer highThreshold; private Integer mediumThreshold; private Integer lowThreshold; @@ -40,6 +51,7 @@ public class ScanConfiguration { private String osaArchiveIncludePatterns; private boolean osaInstallBeforeScan; private boolean osaThresholdsEnabled = false; + private Integer osaCriticalThreshold; private Integer osaHighThreshold; private Integer osaMediumThreshold; private Integer osaLowThreshold; @@ -60,14 +72,6 @@ public void setSASTEnabled(boolean SASTEnabled) { this.SASTEnabled = SASTEnabled; } - public String getOSAEnabled() { - return OSAEnabled; - } - - public void setOSAEnabled(String OSAEnabled) { - this.OSAEnabled = OSAEnabled; - } - public String getCxOrigin() { return cxOrigin; } @@ -198,6 +202,18 @@ public boolean isThresholdsEnabled() { public void setThresholdsEnabled(boolean thresholdsEnabled) { this.thresholdsEnabled = thresholdsEnabled; } + + public Integer getCriticalThreshold() { + return criticalThreshold; + } + + public void setCriticalThreshold(Integer criticalThreshold) { + this.criticalThreshold = criticalThreshold; + } + + private void setCriticalThreshold(String criticalSeveritiesThreshold) { + this.criticalThreshold = getAsInteger(criticalSeveritiesThreshold); + } public Integer getHighThreshold() { return highThreshold; @@ -298,6 +314,18 @@ public boolean isOsaThresholdsEnabled() { public void setOsaThresholdsEnabled(boolean osaThresholdsEnabled) { this.osaThresholdsEnabled = osaThresholdsEnabled; } + + public Integer getOsaCriticalThreshold() { + return osaCriticalThreshold; + } + + public void setOsaCriticalThreshold(Integer osaCriticalThreshold) { + this.osaCriticalThreshold = osaCriticalThreshold; + } + + private void setOsaCriticalSeveritiesThreshold(String osaCriticalSeveritiesThreshold) { + this.osaCriticalThreshold = getAsInteger(osaCriticalSeveritiesThreshold); + } public Integer getOsaHighThreshold() { return osaHighThreshold; @@ -348,11 +376,11 @@ private Integer getAsInteger(String number) { } public boolean isSASTThresholdEffectivelyEnabled() { - return isThresholdsEnabled() && (getLowThreshold() != null || getMediumThreshold() != null || getHighThreshold() != null); + return isThresholdsEnabled() && (getLowThreshold() != null || getMediumThreshold() != null || getHighThreshold() != null || getCriticalThreshold() != null); } public boolean isOSAThresholdEffectivelyEnabled() { - return isOsaEnabled() && isOsaThresholdsEnabled() && (getOsaHighThreshold() != null || getOsaMediumThreshold() != null || getOsaLowThreshold() != null); + return isOsaEnabled() && isOsaThresholdsEnabled() && (getOsaCriticalThreshold() != null || getOsaHighThreshold() != null || getOsaMediumThreshold() != null || getOsaLowThreshold() != null); } public String getReportsDir() { diff --git a/src/main/java/com/cx/restclient/osa/utils/OSAParam.java b/src/main/java/com/cx/restclient/osa/utils/OSAParam.java index 24710de4..c5ee9f43 100644 --- a/src/main/java/com/cx/restclient/osa/utils/OSAParam.java +++ b/src/main/java/com/cx/restclient/osa/utils/OSAParam.java @@ -20,4 +20,6 @@ public class OSAParam { public static final String OSA_LIBRARIES_NAME = "CxOSALibraries"; public static final String OSA_VULNERABILITIES_NAME = "CxOSAVulnerabilities"; public static final String OSA_SUMMARY_NAME = "CxOSASummary"; + public static final String LINK_FORMAT = "/CxWebClient/portal#/projectState/"; + public static final String LINK_FORMAL_SUMMARY = "/Summary"; } diff --git a/src/main/java/com/cx/restclient/osa/utils/OSAUtils.java b/src/main/java/com/cx/restclient/osa/utils/OSAUtils.java index 2e0395b2..40803362 100644 --- a/src/main/java/com/cx/restclient/osa/utils/OSAUtils.java +++ b/src/main/java/com/cx/restclient/osa/utils/OSAUtils.java @@ -11,26 +11,19 @@ import java.io.File; import java.nio.charset.Charset; import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Properties; +import java.util.*; import static com.cx.restclient.common.CxPARAM.CX_REPORT_LOCATION; -/** - * Created by Galn on 07/02/2018. - */ public abstract class OSAUtils { private static final String[] SUPPORTED_EXTENSIONS = {"jar", "war", "ear", "aar", "dll", "exe", "msi", "nupkg", "egg", "whl", "tar.gz", "gem", "deb", "udeb", - "dmg", "drpm", "rpm", "pkg.tar.xz", "swf", "swc", "air", "apk", "zip", "gzip", "tar.bz2", "tgz", "c", "cc", "cp", "cpp", "css", "c++", "h", "hh", "hpp", - "hxx", "h++", "m", "mm", "pch", "java", "c#", "cs", "csharp", "go", "goc", "js", "plx", "pm", "ph", "cgi", "fcgi", "psgi", "al", "perl", "t", "p6m", "p6l", "nqp,6pl", "6pm", - "p6", "php", "py", "rb", "swift", "clj", "cljx", "cljs", "cljc"}; + "dmg", "drpm", "rpm", "pkg.tar.xz", "swf", "swc", "air", "apk", "zip", "gzip", "tar.bz2", "tgz", "js"}; private static final String INCLUDE_ALL_EXTENSIONS = "**/**"; + private static final String JSON_EXTENSION = ".json"; - public static final String DEFAULT_ARCHIVE_INCLUDES = "**/.*jar,**/*.war,**/*.ear,**/*.sca,**/*.gem,**/*.whl,**/*.egg,**/*.tar,**/*.tar.gz,**/*.tgz,**/*.zip,**/*.rar"; + public static final String DEFAULT_ARCHIVE_INCLUDES = "**/*.jar,**/*.war,**/*.ear,**/*.sca,**/*.gem,**/*.whl,**/*.egg,**/*.tar,**/*.tar.gz,**/*.tgz,**/*.zip,**/*.rar"; public static void writeToOsaListToFile(File dir, String osaDependenciesJson, Logger log) { @@ -48,7 +41,7 @@ public static String composeProjectOSASummaryLink(String url, long projectId) { return String.format(url + "/CxWebClient/SPA/#/viewer/project/%s", projectId); } - public static Properties generateOSAScanConfiguration(String folderExclusions, String filterPatterns, String archiveIncludes, String scanFolder, boolean installBeforeScan, Logger log) { + public static Properties generateOSAScanConfiguration(String folderExclusions, String filterPatterns, String archiveIncludes, String sourceDir, boolean installBeforeScan, String osaScanDepth, Logger log) { Properties ret = new Properties(); filterPatterns = StringUtils.defaultString(filterPatterns); archiveIncludes = StringUtils.defaultString(archiveIncludes); @@ -86,24 +79,55 @@ public static Properties generateOSAScanConfiguration(String folderExclusions, S ret.put("archiveIncludes", DEFAULT_ARCHIVE_INCLUDES); } - ret.put("archiveExtractionDepth", "4"); + ret.put("archiveExtractionDepth", StringUtils.isNotEmpty(osaScanDepth) ? osaScanDepth : "4"); + + ret.put("npm.ignoreNpmLsErrors", "true"); if (installBeforeScan) { - ret.put("npm.runPreStep", "true"); - ret.put("bower.runPreStep", "false"); + setPreInstallDependencies(ret, "true"); ret.put("npm.ignoreScripts", "true"); - - ret.put("nuget.resolveDependencies", "true"); - ret.put("nuget.restoreDependencies", "true"); - ret.put("python.resolveDependencies", "true"); - ret.put("python.ignorePipInstallErrors", "true"); + ret.put("sbt.targetFolder", getSbtTargetFolder(sourceDir)); + } else { + setPreInstallDependencies(ret, "false"); } - ret.put("d", scanFolder); - + ret.put("d", sourceDir); return ret; } + private static void setPreInstallDependencies(Properties ret, String resolveDependencies) { + ret.put("npm.runPreStep", resolveDependencies); + ret.put("php.runPreStep", resolveDependencies); + ret.put("sbt.runPreStep", resolveDependencies); + ret.put("gradle.runAssembleCommand", resolveDependencies); + ret.put("nuget.restoreDependencies", resolveDependencies); + ret.put("python.ignorePipInstallErrors", resolveDependencies); + } + + private static String getSbtTargetFolder(String sourceFolder) { + List files = new ArrayList(); + files = getBuildSbtFiles(sourceFolder, files); + if (!files.isEmpty()) { + return files.get(0).getAbsolutePath().replace("build.sbt", "target"); + } + return "target"; + } + + private static List getBuildSbtFiles(String path, List inputFiles) { + File folder = new File(path); + List files = Arrays.asList(folder.listFiles()); + for (File file : files) { + if (file.isFile()) { + if (file.getName().endsWith("build.sbt")) { + inputFiles.add(file); + } + } else if (file.isDirectory()) { + inputFiles = getBuildSbtFiles(file.getAbsolutePath(), inputFiles); + } + } + return inputFiles; + } + public static void printOSAResultsToConsole(OSAResults osaResults, boolean enableViolations, Logger log) { OSASummaryResults osaSummaryResults = osaResults.getResults(); log.info("----------------------------Checkmarx Scan Results(CxOSA):-------------------------------"); @@ -124,29 +148,59 @@ public static void printOSAResultsToConsole(OSAResults osaResults, boolean enabl log.info("Vulnerable and updated: " + osaSummaryResults.getVulnerableAndUpdated()); log.info("Non-vulnerable libraries: " + osaSummaryResults.getNonVulnerableLibraries()); log.info(""); - if (enableViolations) { - if (osaResults.getOsaPolicies().isEmpty()){ - log.info("Project policy status: compliant"); - }else{ - log.info("Project policy status: violated"); - log.info("OSA violated policies names: " + StringUtils.join(osaResults.getOsaPolicies(), ',')); - } - } log.info("OSA scan results location: " + osaResults.getOsaProjectSummaryLink()); log.info("-----------------------------------------------------------------------------------------"); } - public static void writeJsonToFile(String name, Object jsonObj, File workDirectory, Logger log) { + public static File getWorkDirectory(File filePath, Boolean osaGenerateJsonReport) { + if (filePath == null) { + return null; + } + + if (!osaGenerateJsonReport) { + return filePath; + } + + File workDirectory; + if (!filePath.isAbsolute()) { + workDirectory = new File(System.getProperty("user.dir") + CX_REPORT_LOCATION); + } else { + workDirectory = filePath.getParentFile(); + } + if (!workDirectory.exists()) { + workDirectory.mkdirs(); + } + + return workDirectory; + } + + public static void writeJsonToFile(String name, Object jsonObj, File workDirectory, Boolean cliOsaGenerateJsonReport, Logger log) { try { ObjectMapper objectMapper = new ObjectMapper(); - String now = new SimpleDateFormat("dd_MM_yyyy-HH_mm_ss").format(new Date()); - String fileName = name + "_" + now + ".json"; - File jsonFile = new File(workDirectory + CX_REPORT_LOCATION, fileName); String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonObj); - FileUtils.writeStringToFile(jsonFile, json); - log.info(name + " json location: " + workDirectory + CX_REPORT_LOCATION + File.separator + fileName); + + if (cliOsaGenerateJsonReport) { + //workDirectory = new File(workDirectory.getPath().replace(".json", "_" + name + ".json")); + if (!workDirectory.isAbsolute()) { + workDirectory = new File(System.getProperty("user.dir") + CX_REPORT_LOCATION + File.separator + workDirectory); + } + if (!workDirectory.getParentFile().exists()) { + workDirectory.getParentFile().mkdirs(); + } + name = name.endsWith(JSON_EXTENSION) ? name : name + JSON_EXTENSION; + File jsonFile = new File(workDirectory + File.separator + name); + FileUtils.writeStringToFile(jsonFile, json); + log.info(" Report " + name + " saved under location: " + jsonFile); + } else { + String now = new SimpleDateFormat("dd_MM_yyyy-HH_mm_ss").format(new Date()); + String fileName = name + "_" + now + JSON_EXTENSION; + File jsonFile = new File(workDirectory + CX_REPORT_LOCATION, fileName); + FileUtils.writeStringToFile(jsonFile, json); + log.info("Report " + name + " saved under location: " + workDirectory + CX_REPORT_LOCATION + File.separator + fileName); + } } catch (Exception ex) { log.warn("Failed to write OSA JSON report (" + name + ") to file: " + ex.getMessage()); } } + } diff --git a/src/main/java/com/cx/restclient/sast/dto/CreateBranchStatus.java b/src/main/java/com/cx/restclient/sast/dto/CreateBranchStatus.java new file mode 100644 index 00000000..3358a5bd --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/dto/CreateBranchStatus.java @@ -0,0 +1,91 @@ +package com.cx.restclient.sast.dto; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class CreateBranchStatus { + + private long id; + private long originalProjectId; + private String originalProjectName; + private long branchedOnScanId; + private long branchedProjectId; + private String timestamp; + private String comment; + private Status status; + private String errorMessage; + public CreateBranchStatus(long id, long originalProjectId, String originalProjectName, long branchedOnScanId, + long branchedProjectId, String timestamp, String comment, Status status, String errorMessage) { + this.id = id; + this.originalProjectId = originalProjectId; + this.originalProjectName = originalProjectName; + this.branchedOnScanId = branchedOnScanId; + this.branchedProjectId = branchedProjectId; + this.timestamp = timestamp; + this.comment = comment; + this.status = status; + this.errorMessage = errorMessage; + } + + public CreateBranchStatus() { + } + + public long getId() { + return id; + } + public void setId(long id) { + this.id = id; + } + public long getOriginalProjectId() { + return originalProjectId; + } + public void setOriginalProjectId(Integer originalProjectId) { + this.originalProjectId = originalProjectId; + } + public String getOriginalProjectName() { + return originalProjectName; + } + public void setOriginalProjectName(String originalProjectName) { + this.originalProjectName = originalProjectName; + } + public long getBranchedOnScanId() { + return branchedOnScanId; + } + public void setBranchedOnScanId(long branchedOnScanId) { + this.branchedOnScanId = branchedOnScanId; + } + public long getBranchedProjectId() { + return branchedProjectId; + } + public void setBranchedProjectId(long branchedProjectId) { + this.branchedProjectId = branchedProjectId; + } + public String getTimestamp() { + return timestamp; + } + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } + public String getComment() { + return comment; + } + public void setComment(String comment) { + this.comment = comment; + } + public Status getStatus() { + return status; + } + public void setStatus(Status status) { + this.status = status; + } + public String getErrorMessage() { + return errorMessage; + } + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + +} \ No newline at end of file diff --git a/src/main/java/com/cx/restclient/sast/dto/CreateProjectRequest.java b/src/main/java/com/cx/restclient/sast/dto/CreateProjectRequest.java index 1e611112..c132fc8c 100644 --- a/src/main/java/com/cx/restclient/sast/dto/CreateProjectRequest.java +++ b/src/main/java/com/cx/restclient/sast/dto/CreateProjectRequest.java @@ -16,7 +16,10 @@ public CreateProjectRequest(String name, String owningTeam, boolean isPublic) { this.owningTeam = owningTeam; this.isPublic = isPublic; } - + public CreateProjectRequest(String name) { + this.name = name; + } + public String getName() { return name; } @@ -38,6 +41,6 @@ public boolean getIsPublic() { } public void setIsPublic(boolean isPublic) { - isPublic = isPublic; + this.isPublic = isPublic; } } diff --git a/src/main/java/com/cx/restclient/sast/dto/CxARMStatus.java b/src/main/java/com/cx/restclient/sast/dto/CxARMStatus.java new file mode 100644 index 00000000..691ee0d3 --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/dto/CxARMStatus.java @@ -0,0 +1,48 @@ +package com.cx.restclient.sast.dto; + + +import com.cx.restclient.dto.BaseStatus; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Created by Galn on 07/03/2018. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class CxARMStatus extends BaseStatus { + private CxID project; + private CxID scan; + String status; + String lastSync; + + public CxID getProject() { + return project; + } + + public void setProject(CxID project) { + this.project = project; + } + + public CxID getScan() { + return scan; + } + + public void setScan(CxID scan) { + this.scan = scan; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getLastSync() { + return lastSync; + } + + public void setLastSync(String lastSync) { + this.lastSync = lastSync; + } +} diff --git a/src/main/java/com/cx/restclient/sast/dto/CxARMStatusEnum.java b/src/main/java/com/cx/restclient/sast/dto/CxARMStatusEnum.java new file mode 100644 index 00000000..caf256aa --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/dto/CxARMStatusEnum.java @@ -0,0 +1,25 @@ +package com.cx.restclient.sast.dto; + +/** + * Created by Galn on 07/03/2018. + */ +public enum CxARMStatusEnum { + + IN_PROGRESS("InProgress"), + FINISHED("Finished"), + FAILED("Failed"), + NONE("None"); + + private final String value; + + CxARMStatusEnum(String value) { + this.value = value; + } + + public String value() { + return this.value; + } + + +} + diff --git a/src/main/java/com/cx/restclient/sast/dto/CxDateAndTimeObj.java b/src/main/java/com/cx/restclient/sast/dto/CxDateAndTimeObj.java new file mode 100644 index 00000000..29937b2d --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/dto/CxDateAndTimeObj.java @@ -0,0 +1,45 @@ +package com.cx.restclient.sast.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class CxDateAndTimeObj { + private String startedOn; + + public String getStartedOn() { + return startedOn; + } + + public void setStartedOn(String startedOn) { + this.startedOn = startedOn; + } + + public String getFinishedOn() { + return finishedOn; + } + + public void setFinishedOn(String finishedOn) { + this.finishedOn = finishedOn; + } + + public String getEngineStartedOn() { + return engineStartedOn; + } + + public void setEngineStartedOn(String engineStartedOn) { + this.engineStartedOn = engineStartedOn; + } + + public String getEngineFinishedOn() { + return engineFinishedOn; + } + + public void setEngineFinishedOn(String engineFinishedOn) { + this.engineFinishedOn = engineFinishedOn; + } + + private String finishedOn; + private String engineStartedOn; + private String engineFinishedOn; + +} diff --git a/src/main/java/com/cx/restclient/sast/dto/CxLanguageObj.java b/src/main/java/com/cx/restclient/sast/dto/CxLanguageObj.java new file mode 100644 index 00000000..231d8c5c --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/dto/CxLanguageObj.java @@ -0,0 +1,29 @@ +package com.cx.restclient.sast.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Created by Galn on 05/03/2018. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class CxLanguageObj { + private long languageID; + private String languageName; + + public long getLanguageID() { + return languageID; + } + + public void setLanguageID(long languageID) { + this.languageID = languageID; + } + + public String getLanguageName() { + return languageName; + } + + public void setLanguageName(String languageName) { + this.languageName = languageName; + } + +} diff --git a/src/main/java/com/cx/restclient/sast/dto/CxLinkObj.java b/src/main/java/com/cx/restclient/sast/dto/CxLinkObj.java new file mode 100644 index 00000000..d38a97f8 --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/dto/CxLinkObj.java @@ -0,0 +1,27 @@ +package com.cx.restclient.sast.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class CxLinkObj { + private String rel; + + public String getRel() { + return rel; + } + + public void setRel(String rel) { + this.rel = rel; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + private String uri; + +} diff --git a/src/main/java/com/cx/restclient/sast/dto/CxNameObj.java b/src/main/java/com/cx/restclient/sast/dto/CxNameObj.java index 3242a6f9..713f49d8 100644 --- a/src/main/java/com/cx/restclient/sast/dto/CxNameObj.java +++ b/src/main/java/com/cx/restclient/sast/dto/CxNameObj.java @@ -7,14 +7,14 @@ */ @JsonIgnoreProperties(ignoreUnknown = true) public class CxNameObj { - private long id; + private int id; private String name; - public long getId() { + public int getId() { return id; } - public void setId(long id) { + public void setId(int id) { this.id = id; } diff --git a/src/main/java/com/cx/restclient/sast/dto/CxScanStateObj.java b/src/main/java/com/cx/restclient/sast/dto/CxScanStateObj.java new file mode 100644 index 00000000..e04ecb64 --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/dto/CxScanStateObj.java @@ -0,0 +1,62 @@ +package com.cx.restclient.sast.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class CxScanStateObj { + private String path; + private String sourceId; + private long filesCount; + private long linesOfCode; + private long failedLinesOfCode; + private String cxVersion; + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getSourceId() { + return sourceId; + } + + public void setSourceId(String sourceId) { + this.sourceId = sourceId; + } + + public long getFilesCount() { + return filesCount; + } + + public void setFilesCount(long filesCount) { + this.filesCount = filesCount; + } + + public long getLinesOfCode() { + return linesOfCode; + } + + public void setLinesOfCode(long linesOfCode) { + this.linesOfCode = linesOfCode; + } + + public long getFailedLinesOfCode() { + return failedLinesOfCode; + } + + public void setFailedLinesOfCode(long failedLinesOfCode) { + this.failedLinesOfCode = failedLinesOfCode; + } + + public String getCxVersion() { + return cxVersion; + } + + public void setCxVersion(String cxVersion) { + this.cxVersion = cxVersion; + } + +} diff --git a/src/main/java/com/cx/restclient/sast/dto/CxXMLResults.java b/src/main/java/com/cx/restclient/sast/dto/CxXMLResults.java index 2a037413..a1f837f9 100644 --- a/src/main/java/com/cx/restclient/sast/dto/CxXMLResults.java +++ b/src/main/java/com/cx/restclient/sast/dto/CxXMLResults.java @@ -1,6 +1,6 @@ package com.cx.restclient.sast.dto; -import javax.xml.bind.annotation.*; +import jakarta.xml.bind.annotation.*; import java.io.Serializable; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/com/cx/restclient/sast/dto/DataRetentionSettingsDto.java b/src/main/java/com/cx/restclient/sast/dto/DataRetentionSettingsDto.java new file mode 100644 index 00000000..3a8a8e42 --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/dto/DataRetentionSettingsDto.java @@ -0,0 +1,21 @@ +package com.cx.restclient.sast.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class DataRetentionSettingsDto { + private int scansToKeep; + public int getScansToKeep() { + return scansToKeep; + } + + public void setScansToKeep(int scansToKeep) { + this.scansToKeep = scansToKeep; + } + + + public DataRetentionSettingsDto(int scansToKeep ) { + this.scansToKeep = scansToKeep; + } + +} diff --git a/src/main/java/com/cx/restclient/sast/dto/DateAndTime.java b/src/main/java/com/cx/restclient/sast/dto/DateAndTime.java new file mode 100644 index 00000000..5c90cd7a --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/dto/DateAndTime.java @@ -0,0 +1,47 @@ +package com.cx.restclient.sast.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.Date; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class DateAndTime { + private Date startedOn; + private Date finishedOn; + private Date engineStartedOn; + private Date engineFinishedOn; + + public DateAndTime() { + } + + public Date getStartedOn() { + return startedOn; + } + + public void setStartedOn(Date startedOn) { + this.startedOn = startedOn; + } + + public Date getFinishedOn() { + return finishedOn; + } + + public void setFinishedOn(Date finishedOn) { + this.finishedOn = finishedOn; + } + + public Date getEngineStartedOn() { + return engineStartedOn; + } + + public void setEngineStartedOn(Date engineStartedOn) { + this.engineStartedOn = engineStartedOn; + } + + public Date getEngineFinishedOn() { + return engineFinishedOn; + } + + public void setEngineFinishedOn(Date engineFinishedOn) { + this.engineFinishedOn = engineFinishedOn; + } +} diff --git a/src/main/java/com/cx/restclient/sast/dto/ExcludeSettingsRequest.java b/src/main/java/com/cx/restclient/sast/dto/ExcludeSettingsRequest.java new file mode 100644 index 00000000..1e195a8c --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/dto/ExcludeSettingsRequest.java @@ -0,0 +1,33 @@ +package com.cx.restclient.sast.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ExcludeSettingsRequest { + private String excludeFoldersPattern; + private String excludeFilesPattern; + + public ExcludeSettingsRequest() { + } + + public ExcludeSettingsRequest(String excludeFoldersPattern, String excludeFilesPattern) { + this.excludeFoldersPattern = excludeFoldersPattern; + this.excludeFilesPattern = excludeFilesPattern; + } + + public String getExcludeFoldersPattern() { + return excludeFoldersPattern; + } + + public void setExcludeFoldersPattern(String excludeFoldersPattern) { + this.excludeFoldersPattern = excludeFoldersPattern; + } + + public String getExcludeFilesPattern() { + return excludeFilesPattern; + } + + public void setExcludeFilesPattern(String excludeFilesPattern) { + this.excludeFilesPattern = excludeFilesPattern; + } +} diff --git a/src/main/java/com/cx/restclient/sast/dto/LastScanResponse.java b/src/main/java/com/cx/restclient/sast/dto/LastScanResponse.java index 6da1944c..eb675f43 100644 --- a/src/main/java/com/cx/restclient/sast/dto/LastScanResponse.java +++ b/src/main/java/com/cx/restclient/sast/dto/LastScanResponse.java @@ -9,6 +9,7 @@ public class LastScanResponse { private long id; private CxNameObj status; + private DateAndTime dateAndTime; public long getId() { return id; @@ -25,4 +26,12 @@ public CxNameObj getStatus() { public void setStatus(CxNameObj status) { this.status = status; } + + public DateAndTime getDateAndTime() { + return dateAndTime; + } + + public void setDateAndTime(DateAndTime dateAndTime) { + this.dateAndTime = dateAndTime; + } } diff --git a/src/main/java/com/cx/restclient/sast/dto/PostAction.java b/src/main/java/com/cx/restclient/sast/dto/PostAction.java new file mode 100644 index 00000000..78753952 --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/dto/PostAction.java @@ -0,0 +1,72 @@ +package com.cx.restclient.sast.dto; + +public class PostAction { + + private int id; + private String name; + private String type; + private String data; + + public PostAction() { + } + + public PostAction(int id, String name, String type, String data) { + super(); + this.id = id; + this.name = name; + this.type = type; + this.data = data; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostAction)) return false; + + PostAction postAction = (PostAction) o; + + if (getId() != postAction.getId()) return false; + return getName().equals(postAction.getName()); + + } + + @Override + public int hashCode() { + int result = getId(); + result = 31 * result + getName().hashCode(); + return result; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + +} diff --git a/src/main/java/com/cx/restclient/sast/dto/Project.java b/src/main/java/com/cx/restclient/sast/dto/Project.java index 4ba6c835..f128c253 100644 --- a/src/main/java/com/cx/restclient/sast/dto/Project.java +++ b/src/main/java/com/cx/restclient/sast/dto/Project.java @@ -1,5 +1,8 @@ package com.cx.restclient.sast.dto; +import java.util.ArrayList; +import java.util.List; + import com.fasterxml.jackson.annotation.JsonIgnoreProperties; /** @@ -8,10 +11,33 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class Project { private long id; + private String owner; private String name; private String teamId; private boolean isPublic; + private List customFields; + + public boolean isPublic() { + return isPublic; + } + + public void setPublic(boolean aPublic) { + isPublic = aPublic; + } + public ArrayList getCustomFields() { + if (customFields instanceof ArrayList) { + return (ArrayList) customFields; + } else if (customFields != null) { + return new ArrayList<>(customFields); + } else { + return new ArrayList<>(); + } + } + public void setCustomFields(ArrayList customFields) { + this.customFields = customFields; + } + public long getId() { return id; } @@ -43,4 +69,13 @@ public boolean getIsPublic() { public void setIsPublic(boolean isPublic) { this.isPublic = isPublic; } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + } diff --git a/src/main/java/com/cx/restclient/sast/dto/ProjectLevelCustomFields.java b/src/main/java/com/cx/restclient/sast/dto/ProjectLevelCustomFields.java new file mode 100644 index 00000000..28bbba9e --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/dto/ProjectLevelCustomFields.java @@ -0,0 +1,40 @@ +package com.cx.restclient.sast.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Created by Galn on 4/11/2018. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ProjectLevelCustomFields { + @Override + public String toString() { + return "ProjectLevelCustomFields [id=" + id + ", value=" + value + ", name=" + name + "]"; + } + + private long id; + private String value; + private String name; + + public ProjectLevelCustomFields() { + } + + public ProjectLevelCustomFields(long id, String value, String name) { + this.id = id; + this.value = value; + this.name = name; + } + + public long getId() { + return id; + } + + public String getValue() { + return value; + } + + public String getName() { + return name; + } + +} diff --git a/src/main/java/com/cx/restclient/sast/dto/ProjectPutRequest.java b/src/main/java/com/cx/restclient/sast/dto/ProjectPutRequest.java new file mode 100644 index 00000000..ffac0599 --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/dto/ProjectPutRequest.java @@ -0,0 +1,42 @@ +package com.cx.restclient.sast.dto; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Created by Galn on 13/02/2018. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ProjectPutRequest { + + private String name; + private Integer owningTeam; + private List customFields; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getOwningTeam() { + return owningTeam; + } + + public void setOwningTeam(Integer owningTeam) { + this.owningTeam = owningTeam; + } + + public List getCustomFields() { + return customFields; + } + + public void setCustomFields(ArrayList custObj) { + this.customFields = custObj; + } + +} diff --git a/src/main/java/com/cx/restclient/sast/dto/QueueStatus.java b/src/main/java/com/cx/restclient/sast/dto/QueueStatus.java new file mode 100644 index 00000000..e7eaf3b2 --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/dto/QueueStatus.java @@ -0,0 +1,18 @@ +package com.cx.restclient.sast.dto; + +/** + * Created by Galn on 05/02/2018. + */ + +public enum QueueStatus { + New, + PreScan, + SourcePullingAndDeployment, + Queued, + Scanning, + PostScan, + Finished, + Canceled, + Failed; +} + diff --git a/src/main/java/com/cx/restclient/sast/dto/ResponseSastScanStatus.java b/src/main/java/com/cx/restclient/sast/dto/ResponseSastScanStatus.java new file mode 100644 index 00000000..690cb05d --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/dto/ResponseSastScanStatus.java @@ -0,0 +1,196 @@ +package com.cx.restclient.sast.dto; + +import com.cx.restclient.dto.BaseStatus; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ResponseSastScanStatus extends BaseStatus { + + private long id; + private Project project; + private CxNameObj status; + private CxValueObj scanType; + private String comment; + private CxDateAndTimeObj dateAndTime; + private CxLinkObj resultsStatistics; + private CxScanStateObj scanState; + private String owner; + private String origin; + private String initiatorName; + private long owningTeamId; + private boolean isPublic; + private boolean isLocked; + private boolean isIncremental; + private long scanRisk; + private long scanRiskSeverity; + private CxNameObj engineServer; + private CxValueObj finishedScanStatus; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Project getProject() { + return project; + } + + public void setProject(Project project) { + this.project = project; + } + + public CxNameObj getStatus() { + return status; + } + + public void setStatus(CxNameObj status) { + this.status = status; + } + + public CxValueObj getScanType() { + return scanType; + } + + public void setScanType(CxValueObj scanType) { + this.scanType = scanType; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public CxDateAndTimeObj getDateAndTime() { + return dateAndTime; + } + + public void setDateAndTime(CxDateAndTimeObj dateAndTime) { + this.dateAndTime = dateAndTime; + } + + public CxLinkObj getResultsStatistics() { + return resultsStatistics; + } + + public void setResultsStatistics(CxLinkObj resultsStatistics) { + this.resultsStatistics = resultsStatistics; + } + + public CxScanStateObj getScanState() { + return scanState; + } + + public void setScanState(CxScanStateObj scanState) { + this.scanState = scanState; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public String getOrigin() { + return origin; + } + + public void setOrigin(String origin) { + this.origin = origin; + } + + public String getInitiatorName() { + return initiatorName; + } + + public void setInitiatorName(String initiatorName) { + this.initiatorName = initiatorName; + } + + public long getOwningTeamId() { + return owningTeamId; + } + + public void setOwningTeamId(long owningTeamId) { + this.owningTeamId = owningTeamId; + } + + public boolean isPublic() { + return isPublic; + } + + public void setPublic(boolean aPublic) { + isPublic = aPublic; + } + + public boolean isLocked() { + return isLocked; + } + + public void setLocked(boolean locked) { + isLocked = locked; + } + + public boolean isIncremental() { + return isIncremental; + } + + public void setIncremental(boolean incremental) { + isIncremental = incremental; + } + + public long getScanRisk() { + return scanRisk; + } + + public void setScanRisk(long scanRisk) { + this.scanRisk = scanRisk; + } + + public long getScanRiskSeverity() { + return scanRiskSeverity; + } + + public void setScanRiskSeverity(long scanRiskSeverity) { + this.scanRiskSeverity = scanRiskSeverity; + } + + public CxNameObj getEngineServer() { + return engineServer; + } + + public void setEngineServer(CxNameObj engineServer) { + this.engineServer = engineServer; + } + + public CxValueObj getFinishedScanStatus() { + return finishedScanStatus; + } + + public void setFinishedScanStatus(CxValueObj finishedScanStatus) { + this.finishedScanStatus = finishedScanStatus; + } + public ResponseQueueScanStatus convertResponseSastScanStatusToResponseQueueScanStatus(ResponseSastScanStatus responseSastScanStatus){ + ResponseQueueScanStatus tempResponseQueueScanStatus = new ResponseQueueScanStatus(); + + tempResponseQueueScanStatus.setId(this.id); + CxValueObj stage = new CxValueObj(); + stage.setId(responseSastScanStatus.status.getId()); + stage.setValue(responseSastScanStatus.status.getName()); + tempResponseQueueScanStatus.setStage(stage); + tempResponseQueueScanStatus.setStageDetails(""); + tempResponseQueueScanStatus.setProject(responseSastScanStatus.project); + tempResponseQueueScanStatus.setTotalPercent(100); + tempResponseQueueScanStatus.setStagePercent(100); + tempResponseQueueScanStatus.setBaseStatus(responseSastScanStatus.getBaseStatus()); + + return tempResponseQueueScanStatus; + } + } \ No newline at end of file diff --git a/src/main/java/com/cx/restclient/sast/dto/SASTResults.java b/src/main/java/com/cx/restclient/sast/dto/SASTResults.java index 8b9fbdd1..25213e65 100644 --- a/src/main/java/com/cx/restclient/sast/dto/SASTResults.java +++ b/src/main/java/com/cx/restclient/sast/dto/SASTResults.java @@ -1,14 +1,40 @@ package com.cx.restclient.sast.dto; +import com.cx.restclient.ast.dto.common.ScanConfig; +import com.cx.restclient.common.UrlUtils; +import com.cx.restclient.configuration.CxScanConfig; import com.cx.restclient.cxArm.dto.Policy; -import com.cx.restclient.cxArm.dto.Violation; +import com.cx.restclient.dto.LoginSettings; +import com.cx.restclient.dto.Results; +import com.cx.restclient.dto.TokenLoginResponse; +import com.cx.restclient.exception.CxClientException; +import com.cx.restclient.httpClient.CxHttpClient; +import com.cx.restclient.osa.dto.ClientType; +import com.cx.restclient.sast.dto.CxXMLResults.Query; +import com.cx.restclient.sast.utils.LegacyClient; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringUtils; +import org.json.JSONObject; + + +import java.io.IOException; import java.io.Serializable; +import java.net.MalformedURLException; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; import java.util.*; + +import static com.cx.restclient.common.CxPARAM.AUTHENTICATION; +import static com.cx.restclient.cxArm.utils.CxARMUtils.getPolicyList; import static com.cx.restclient.sast.utils.SASTParam.PROJECT_LINK_FORMAT; import static com.cx.restclient.sast.utils.SASTParam.SCAN_LINK_FORMAT; @@ -16,16 +42,18 @@ /** * Created by Galn on 05/02/2018. */ -public class SASTResults implements Serializable { +public class SASTResults extends Results implements Serializable { private long scanId; - + private static final String DEFAULT_AUTH_API_PATH = "CxRestApi/auth/" + AUTHENTICATION; private boolean sastResultsReady = false; private int high = 0; + private int critical = 0; private int medium = 0; private int low = 0; private int information = 0; + private int newCritical = 0; private int newHigh = 0; private int newMedium = 0; private int newLow = 0; @@ -35,12 +63,31 @@ public class SASTResults implements Serializable { private String sastProjectLink; private String sastPDFLink; - private String scanStart; - private String scanTime; - private String scanStartTime; - private String scanEndTime; - - private String filesScanned; + private String scanStart = ""; + private String scanTime = ""; + private String scanStartTime = ""; + private String scanEndTime = ""; + private String sastLanguage="en-US"; + + private Map languageMap; + + public Map getLanguageMap() { + return languageMap; + } + + public void setLanguageMap(Map languageMap) { + this.languageMap = languageMap; + } + + public String getSastLanguage() { + return sastLanguage; + } + + public void setSastLanguage(String sastLanguage) { + this.sastLanguage = sastLanguage; + } + + private String filesScanned; private String LOC; private List queryList; @@ -48,21 +95,26 @@ public class SASTResults implements Serializable { private byte[] PDFReport; private String pdfFileName; - private List sastPolicies = new ArrayList<>(); - private List sastViolations = new ArrayList<>(); - + private List sastPolicies = new ArrayList<>(); public enum Severity { - High, Medium, Low, Information; + Critical, High, Medium, Low, Information; + + } + - public void setScanDetailedReport(CxXMLResults reportObj) { - this.scanStart = reportObj.getScanStart(); + public void setScanDetailedReport(CxXMLResults reportObj,CxScanConfig config) throws IOException { + + setLanguageEquivalent(sastLanguage); + + this.scanStart = reportObj.getScanStart(); this.scanTime = reportObj.getScanTime(); - setScanStartEndDates(this.scanStart, this.scanTime); + setScanStartEndDates(this.scanStart, this.scanTime,sastLanguage); this.LOC = reportObj.getLinesOfCodeScanned(); this.filesScanned = reportObj.getFilesScanned(); - + + for (CxXMLResults.Query q : reportObj.getQuery()) { List qResult = q.getResult(); for (int i = 0; i < qResult.size(); i++) { @@ -72,6 +124,10 @@ public void setScanDetailedReport(CxXMLResults reportObj) { } else if ("New".equals(result.getStatus())) { Severity sev = Severity.valueOf(result.getSeverity()); switch (sev) { + + case Critical: + newCritical++; + break; case High: newHigh++; break; @@ -90,17 +146,49 @@ public void setScanDetailedReport(CxXMLResults reportObj) { } this.queryList = reportObj.getQuery(); } - + + /* + *It will create a map for lanaguage specific severity + * */ + private void setLanguageEquivalent(String sastLanguage) { + //Setting sast language equivalent for HTML Report + if(sastLanguage!=null){ + Locale l = Locale.forLanguageTag(sastLanguage); + final String languageTag = StringUtils.upperCase(l.getLanguage()+ l.getCountry()); + + languageMap = new HashMap(); + SupportedLanguage lang = SupportedLanguage.valueOf(languageTag); + languageMap.put("Critical", lang.getCritical()); + languageMap.put("High", lang.getHigh()); + languageMap.put("Medium", lang.getMedium()); + languageMap.put("Low", lang.getLow()); + } + } + + public String encodeXSS(String injection) { + String lt="<"; + String gt=">"; + String ap="\'"; + String ic="\""; + injection=injection.replace(lt, "<").replace(gt, ">").replace(ap, "'").replace(ic,"""); + return injection; + } public void setResults(long scanId, SASTStatisticsResponse statisticsResults, String url, long projectId) { setScanId(scanId); + setCritical(statisticsResults.getCriticalSeverity()); setHigh(statisticsResults.getHighSeverity()); setMedium(statisticsResults.getMediumSeverity()); setLow(statisticsResults.getLowSeverity()); + setCritical(statisticsResults.getCriticalSeverity()); setInformation(statisticsResults.getInfoSeverity()); setSastScanLink(url, scanId, projectId); setSastProjectLink(url, projectId); } + public void addPolicy(Policy policy) { + this.sastPolicies.addAll(getPolicyList(policy)); + } + public long getScanId() { return scanId; } @@ -108,6 +196,14 @@ public long getScanId() { public void setScanId(long scanId) { this.scanId = scanId; } + + public int getCritical() { + return critical; + } + + public void setCritical(int critical) { + this.critical = critical; + } public int getHigh() { return high; @@ -140,6 +236,14 @@ public int getInformation() { public void setInformation(int information) { this.information = information; } + + public int getNewCritical() { + return newCritical; + } + + public void setNewCritical(int newCritical) { + this.newCritical = newCritical; + } public int getNewHigh() { return newHigh; @@ -294,17 +398,16 @@ public void setPDFReport(byte[] PDFReport) { } public boolean hasNewResults() { - return newHigh + newMedium + newLow > 0; + return newCritical + newHigh + newMedium + newLow > 0; } - private void setScanStartEndDates(String scanStart, String scanTime) { + private void setScanStartEndDates(String scanStart, String scanTime, String lang) { try { //turn strings to date objects - Date scanStartDate = createStartDate(scanStart); - Date scanTimeDate = createTimeDate(scanTime); - Date scanEndDate = createEndDate(scanStartDate, scanTimeDate); - + LocalDateTime scanStartDate = createStartDate(scanStart, lang); + LocalTime scanTimeDate = createTimeDate(scanTime); + LocalDateTime scanEndDate = createEndDate(scanStartDate, scanTimeDate); //turn dates back to strings String scanStartDateFormatted = formatToDisplayDate(scanStartDate); String scanEndDateFormatted = formatToDisplayDate(scanEndDate); @@ -312,63 +415,57 @@ private void setScanStartEndDates(String scanStart, String scanTime) { //set sast scan result object with formatted strings this.scanStartTime = scanStartDateFormatted; this.scanEndTime = scanEndDateFormatted; - } catch (Exception ignored) { //ignored + ignored.printStackTrace(); } } - private String formatToDisplayDate(Date date) { - //"26/2/17 12:17" - String displayDatePattern = "dd/MM/yy HH:mm"; - Locale locale = Locale.ENGLISH; - - return new SimpleDateFormat(displayDatePattern, locale).format(date); - } - - private Date createStartDate(String scanStart) throws ParseException { - //"Sunday, February 26, 2017 12:17:09 PM" - String oldPattern = "EEEE, MMMM dd, yyyy hh:mm:ss a"; - Locale locale = Locale.ENGLISH; - - DateFormat oldDateFormat = new SimpleDateFormat(oldPattern, locale); - - return oldDateFormat.parse(scanStart); - } - - private Date createTimeDate(String scanTime) throws ParseException { - //"00h:00m:30s" - String oldPattern = "HH'h':mm'm':ss's'"; - - DateFormat oldTimeFormat = new SimpleDateFormat(oldPattern); - oldTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - - return oldTimeFormat.parse(scanTime); + private String formatToDisplayDate(LocalDateTime date) throws ParseException{ + String displayDatePattern = "dd/MM/yy HH:mm:ss"; + return date.format(DateTimeFormatter.ofPattern(displayDatePattern)); } - private Date createEndDate(Date scanStartDate, Date scanTimeDate) { - long time /*no c*/ = scanStartDate.getTime() + scanTimeDate.getTime(); - return new Date(time); - } + + /* + * Convert localized date to english date + */ + private LocalDateTime createStartDate(String scanStart, String langTag) throws Exception { + LocalDateTime startDate = LocalDateTime.now(); + + Locale l = Locale.forLanguageTag(langTag); + try { + final String languageTag = StringUtils.upperCase(l.getLanguage() + l.getCountry()); + final DateTimeFormatter formatter = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .appendPattern(SupportedLanguage.valueOf(languageTag).getDatePattern()) + .toFormatter(l); + + + startDate = LocalDateTime.parse(scanStart, formatter); + } catch (Exception ignored) { - public List getSastViolations() { - return sastViolations; + } + + return startDate; } - public void setSastViolations(List sastViolations) { - this.sastViolations = sastViolations; + private LocalTime createTimeDate(String hhmmss) throws ParseException { + LocalTime scanTime = LocalTime.parse(hhmmss,DateTimeFormatter.ofPattern("HH'h':mm'm':ss's'")); + return scanTime; } - public void addAllViolations(List violations) { - this.sastViolations.addAll(violations); + private LocalDateTime createEndDate(LocalDateTime scanStartDate, LocalTime scanTime) { + return scanStartDate.plusHours(scanTime.getHour()).plusMinutes(scanTime.getMinute()).plusSeconds(scanTime.getSecond()); } - public List getSastPolicies() { + public List getSastPolicies() { return sastPolicies; } - public void setSastPolicies(List sastPolicies) { + public void setSastPolicies(List sastPolicies) { this.sastPolicies = sastPolicies; } + } diff --git a/src/main/java/com/cx/restclient/sast/dto/SASTStatisticsResponse.java b/src/main/java/com/cx/restclient/sast/dto/SASTStatisticsResponse.java index 803fcca0..a4579578 100644 --- a/src/main/java/com/cx/restclient/sast/dto/SASTStatisticsResponse.java +++ b/src/main/java/com/cx/restclient/sast/dto/SASTStatisticsResponse.java @@ -10,6 +10,7 @@ public class SASTStatisticsResponse { private int highSeverity; private int mediumSeverity; private int lowSeverity; + private int criticalSeverity; private int infoSeverity; public int getHighSeverity() { @@ -35,6 +36,14 @@ public int getLowSeverity() { public void setLowSeverity(int lowSeverity) { this.lowSeverity = lowSeverity; } + + public int getCriticalSeverity() { + return criticalSeverity; + } + + public void setCriticalSeverity(int criticalSeverity) { + this.criticalSeverity = criticalSeverity; + } public int getInfoSeverity() { return infoSeverity; diff --git a/src/main/java/com/cx/restclient/sast/dto/ScanWithSettingsResponse.java b/src/main/java/com/cx/restclient/sast/dto/ScanWithSettingsResponse.java new file mode 100644 index 00000000..a4babfbb --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/dto/ScanWithSettingsResponse.java @@ -0,0 +1,20 @@ +package com.cx.restclient.sast.dto; + +public class ScanWithSettingsResponse { + private int id; + + public ScanWithSettingsResponse(int id) { + this.id = id; + } + + public ScanWithSettingsResponse() { + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } +} diff --git a/src/main/java/com/cx/restclient/sast/dto/Status.java b/src/main/java/com/cx/restclient/sast/dto/Status.java new file mode 100644 index 00000000..39639432 --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/dto/Status.java @@ -0,0 +1,23 @@ +package com.cx.restclient.sast.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class Status { + + private long id; + private String value; + + public long getId() { + return id; + } + public void setId(long id) { + this.id = id; + } + public String getValue() { + return value; + } + public void setValue(String value) { + this.value = value; + } +} \ No newline at end of file diff --git a/src/main/java/com/cx/restclient/sast/dto/SupportedLanguage.java b/src/main/java/com/cx/restclient/sast/dto/SupportedLanguage.java new file mode 100644 index 00000000..26d95d16 --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/dto/SupportedLanguage.java @@ -0,0 +1,63 @@ +package com.cx.restclient.sast.dto; + +import java.util.Locale; + +public enum SupportedLanguage { + + ENUS(new Locale("en-US"),"Critical","High","Medium","Low","Information", "EEEE, MMMM dd, yyyy hh:mm:ss a"), + JAJP(new Locale("ja-JP"),"危うい","高","中","低","情報","yyyy年M月d日 H:mm:ss"), + FRFR(new Locale("fr-FR"),"critique","Haute","Moyenne","Basse","Informations","EEEE dd MMMM yyyy HH:mm:ss"), + PTBR(new Locale("pt-BR"),"crítico","Alto","Médio","Baixo","Em formação", "EEEE, d 'de' MMMM 'de' yyyy HH:mm:ss"), + ESES(new Locale("es-ES"),"Crítico","Altas","Medias","Bajas","Información","EEEE, d 'de' MMMM 'de' yyyy HH:mm:ss"), + KOKR(new Locale("ko-KR"),"비판적인","높음","중간","낮음","정보", "yyyy년 M월 d일 EEEE a h:mm:ss"), + ZHCN(new Locale("zh-CN"),"危急","高危","中危","低危","信息", "yyyy年M月d日 HH:mm:ss"), + ZHTW(new Locale("zh-TW"),"危急","高","中","低","信息", "yyyy年M月d日 a hh:mm:ss"), + RURU(new Locale("ru-RU"),"критический","Высокое","Среднее","Низкое","Информация","d MMMM yyyy 'г'. H:mm:ss"); + + private final Locale locale; + private final String High; + private final String Medium; + private final String Low; + private final String Critical; + private final String Information; + private final String datePattern; + + private SupportedLanguage(Locale locale, String critical, String high, String medium, String low, String information, String datePattern) { + this.locale = locale; + this.Critical = critical; + this.High = high; + this.Medium = medium; + this.Low = low; + this.Information = information; + this.datePattern = datePattern; + } + + public Locale getLocale() { + return locale; + } + + public String getCritical() { + return Critical; + } + + public String getHigh() { + return High; + } + + public String getMedium() { + return Medium; + } + + public String getLow() { + return Low; + } + + public String getInformation() { + return Information; + } + + public String getDatePattern() { + return datePattern; + } + +} diff --git a/src/main/java/com/cx/restclient/sast/dto/UpdateScanStatusRequest.java b/src/main/java/com/cx/restclient/sast/dto/UpdateScanStatusRequest.java index 3f27b915..df54beef 100644 --- a/src/main/java/com/cx/restclient/sast/dto/UpdateScanStatusRequest.java +++ b/src/main/java/com/cx/restclient/sast/dto/UpdateScanStatusRequest.java @@ -1,8 +1,11 @@ package com.cx.restclient.sast.dto; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + /** * Created by Galn on 18/03/2018. */ +@JsonIgnoreProperties(ignoreUnknown = true) public class UpdateScanStatusRequest { private String status; diff --git a/src/main/java/com/cx/restclient/sast/utils/LegacyClient.java b/src/main/java/com/cx/restclient/sast/utils/LegacyClient.java new file mode 100644 index 00000000..5f89b610 --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/utils/LegacyClient.java @@ -0,0 +1,622 @@ +package com.cx.restclient.sast.utils; + + +import com.cx.restclient.common.UrlUtils; +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.cxArm.dto.CxArmConfig; +import com.cx.restclient.dto.*; +import com.cx.restclient.exception.CxClientException; +import com.cx.restclient.exception.CxHTTPClientException; +import com.cx.restclient.httpClient.CxHttpClient; +import com.cx.restclient.osa.dto.ClientType; +import com.cx.restclient.sast.dto.*; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.exception.ExceptionUtils; +import org.apache.http.HttpEntity; +import org.apache.http.client.HttpResponseException; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.json.JSONException; +import org.slf4j.Logger; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static com.cx.restclient.common.CxPARAM.*; +import static com.cx.restclient.httpClient.utils.ContentType.CONTENT_TYPE_API_VERSION_1_1; +import static com.cx.restclient.httpClient.utils.ContentType.CONTENT_TYPE_APPLICATION_JSON_V1; +import static com.cx.restclient.httpClient.utils.ContentType.CONTENT_TYPE_APPLICATION_JSON_V4; +import static com.cx.restclient.httpClient.utils.HttpClientHelper.convertToJson; +import static com.cx.restclient.sast.utils.SASTParam.*; + +/** + * Common parent for SAST and OSA clients. + * Extracted from {@link com.cx.restclient.CxClientDelegator} for better maintainability. + */ +public abstract class LegacyClient { + + private static final String DEFAULT_AUTH_API_PATH = "CxRestApi/auth/" + AUTHENTICATION; + public static final String PRESETNAME_PROJET_SETTING_DEFAULT = "Project Default"; + public static final String PRESETID_PROJET_SETTING_DEFAULT = "0"; + private static final Integer UNKNOWN_INT = -1; + + private static final String ID_PATH_PARAM = "{id}"; + + protected CxHttpClient httpClient; + protected CxScanConfig config; + protected Logger log; + private String teamPath; + protected long projectId; + private State state = State.SUCCESS; + private boolean isNewProject = false; + + public LegacyClient(CxScanConfig config, Logger log) throws MalformedURLException { + this.config = config; + this.log = log; + initHttpClient(config, log); + validateConfig(config); + } + + public void setConfig(CxScanConfig config) { + this.config = config; + } + + public void close() { + if (httpClient != null) { + httpClient.close(); + } + } + + public boolean isIsNewProject() { + return isNewProject; + } + + public void setIsNewProject(boolean isNewProject) { + this.isNewProject = isNewProject; + } + + public long resolveProjectId() throws IOException { + List projects = getProjectByName(config.getProjectName(), config.getTeamId(), teamPath); + + if (projects == null || projects.isEmpty()) { // Project is new + if (config.getDenyProject()) { + throw new CxClientException(DENY_NEW_PROJECT_ERROR.replace("{projectName}", config.getProjectName())); + } + //Create newProject and checking if EnableSASTBranching is enabled then creating branch project + if(config.isEnableSASTBranching()) { + if (StringUtils.isEmpty(config.getMasterBranchProjName())) { + throw new CxClientException("Master branch project name is must to create branched project."); + } else { + Long masterProjectId; + List masterProject = getProjectByName(config.getMasterBranchProjName(), config.getTeamId(), + teamPath); + if (masterProject != null && !masterProject.isEmpty()) { + masterProjectId = masterProject.get(0).getId(); + } else { + throw new CxClientException( + "Master branch project does not exist:" + config.getMasterBranchProjName()); + } + log.info("Project not found, creating a new one.: '{}' with Team '{}'", config.getProjectName(), + teamPath); + projectId = createChildProject(masterProjectId, config.getProjectName()); + if (projectId == UNKNOWN_INT) { + throw new CxClientException( + "Branched project could not be created: " + config.getProjectName()); + }else { + checkCreateBranchProjectStatus(projectId); + log.info("Created a project with ID {}", projectId); + if(config.isEnableDataRetention()){ + setRetentionRate(projectId); + } + setIsNewProject(true); + } + } + } + else { + CreateProjectRequest request = new CreateProjectRequest(config.getProjectName(), config.getTeamId(), + config.getPublic()); + log.info("Project not found, creating a new one.: '{}' with Team '{}'", config.getProjectName(), + teamPath); + projectId = createNewProject(request, teamPath).getId(); + log.info("Created a project with ID {}", projectId); + if(config.isEnableDataRetention()) { + setRetentionRate(projectId); + } + setIsNewProject(true); + } + } else { + projectId = projects.get(0).getId(); + if(config.isEnableDataRetention()) { + setRetentionRate(projectId); + } + setIsNewProject(false); + log.info("Project already exists with ID {}", projectId); + } + + return projectId; + } + + private void checkCreateBranchProjectStatus(long branchprojectId) throws IOException { + // TODO Auto-generated method stub + String Status = ""; + CreateBranchStatus getBranchRequest = null; + int timeout = checkTimeOut(); + for (int i = 0; i < 3; i++) { + try { + getBranchRequest = populateBranchStatusList(branchprojectId); + } catch (Exception e) { + log.info("Version is less than SAST 9.5 V4"); + break; + } + if (getBranchRequest != null) { + Status = getBranchRequest.getStatus().getValue(); + log.info("Interval =" + i + " BranchStatus=" + Status); + if (Status.equals("Completed")) { + break; + } else { + waitTime(timeout); + } + } + } + } + + private void waitTime(int timeout) { + try { + log.info("timeout =" + timeout +" Seconds"); + Thread.sleep(timeout*1000); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + + private int checkTimeOut() { + int timeout = 10; + if((config.getcopyBranchTimeOutInSeconds())!=null) { + timeout = config.getcopyBranchTimeOutInSeconds(); + log.info("copybranchtimeoutinseconds =" + timeout +" Seconds"); + } + if (timeout > 0 && timeout < 60 ) { + log.info("copybranchtimeoutinseconds is "+ timeout +"seconds"); + } else { + timeout = 10; + log.warn("copybranchtimeoutinseconds is not between the range of 0 to 60 seconds, using default timeout i.e. 10 seconds"); + } + return timeout; + } + + private CreateBranchStatus populateBranchStatusList(long branchprojectId) throws IOException, CxClientException { + return httpClient.getRequest(PROJECT_BRANCH_ID.replace("{id}", Long.toString(branchprojectId)), CONTENT_TYPE_APPLICATION_JSON_V4, CreateBranchStatus.class, 200, "branch status", false); + } + + + public String configureTeamPath() throws IOException, CxClientException { + if (StringUtils.isEmpty(config.getTeamPath())) { + List teamList = populateTeamList(); + // If there is no chosen teamPath, just add first one from the teams + // list as default + if (StringUtils.isEmpty(teamPath) && teamList != null && !teamList.isEmpty()) { + teamPath = teamList.get(0).getFullName(); + } + } else { + teamPath = config.getTeamPath(); + } + httpClient.setTeamPathHeader(teamPath); + log.debug(String.format("setTeamPathHeader %s", teamPath)); + return teamPath; + } + + public List getTeamList() throws IOException, CxClientException { + + return populateTeamList(); + } + + private List populateTeamList() throws IOException { + return (List) httpClient.getRequest(CXTEAMS, CONTENT_TYPE_APPLICATION_JSON_V1, Team.class, 200, "team list", true); + } + + + public String getToken() throws IOException, CxClientException { + LoginSettings settings = getDefaultLoginSettings(); + settings.setClientTypeForPasswordAuth(ClientType.CLI); + final TokenLoginResponse tokenLoginResponse = getHttpClient().generateToken(settings); + return tokenLoginResponse.getRefresh_token(); + } + + public void revokeToken(String token) throws IOException, CxClientException { + getHttpClient().revokeToken(token); + } + + + private Project createNewProject(CreateProjectRequest request, String teamPath) throws IOException { + String json = convertToJson(request); + httpClient.setTeamPathHeader(teamPath); + StringEntity entity = new StringEntity(json, StandardCharsets.UTF_8); + return httpClient.postRequest(CREATE_PROJECT, CONTENT_TYPE_APPLICATION_JSON_V1, entity, Project.class, 201, "create new project: " + request.getName()); + } + + private List getProjectByName(String projectName, String teamId, String teamPath) throws IOException, CxClientException { + projectName = URLEncoder.encode(projectName, "UTF-8"); + String projectNamePath = SAST_GET_PROJECT.replace("{name}", projectName).replace("{teamId}", teamId); + List projects = null; + try { + httpClient.setTeamPathHeader(teamPath); + projects = (List) httpClient.getRequest(projectNamePath, CONTENT_TYPE_APPLICATION_JSON_V1, Project.class, 200, "project by name: " + projectName, true); + } catch (CxHTTPClientException ex) { + if (ex.getStatusCode() != 404) { + throw ex; + } + } + return projects; + } + + private void initHttpClient(CxScanConfig config, Logger log) throws MalformedURLException { + if (!org.apache.commons.lang3.StringUtils.isEmpty(config.getUrl())) { + httpClient = new CxHttpClient( + + UrlUtils.parseURLToString(config.getUrl(), "CxRestAPI/"), + config.getCxOrigin(), + config.getCxOriginUrl(), + config.isDisableCertificateValidation(), + config.isUseSSOLogin(), + config.getRefreshToken(), + config.isProxy(), + config.getProxyConfig(), + log, + config.getNTLM(), + config.getPluginVersion()); + } + } + + public void initiate() throws CxClientException { + try { + if (config.isSastOrOSAEnabled()) { + String version = getCxVersion(); + login(version); + resolveTeam(); + //httpClient.setTeamPathHeader(this.teamPath); + if (config.isSastEnabled()) { + resolvePreset(); + } + if (config.getEnablePolicyViolations() || config.getEnablePolicyViolationsSCA()) { + resolveCxARMUrl(); + } + resolveEngineConfiguration(); + resolveProjectId(); + resolvePostScanAction(); + } + } catch (Exception e) { + throw new CxClientException(e); + } + } + + public String getCxVersion() throws IOException, CxClientException { + String version; + try { + config.setCxVersion(httpClient.getRequest(CX_VERSION, CONTENT_TYPE_API_VERSION_1_1, CxVersion.class, 200, "cx Version", false)); + String hotfix = ""; + try { + if (config.getCxVersion().getHotFix() != null && Integer.parseInt(config.getCxVersion().getHotFix()) > 0) { + hotfix = " Hotfix [" + config.getCxVersion().getHotFix() + "]."; + } + } catch (Exception ex) { + } + + version = config.getCxVersion().getVersion(); + log.info("Checkmarx server version [" + config.getCxVersion().getVersion() + "]." + hotfix); + log.info("Checkmarx Engine Pack Version [" + config.getCxVersion().getEnginePackVersion() + "]."); + + } catch (Exception ex) { + version = "lower than 9.0"; + log.debug("Checkmarx server version [lower than 9.0]"); + } + return version; + } + + public String login(Boolean isVersionRequired) throws IOException { + String cxVersion = getCxVersion(); + login(cxVersion); + return cxVersion; + } + + public void login() throws IOException { + String version = getCxVersion(); + login(version); + } + + public void login(String version) throws IOException, CxClientException { + // perform login to server + log.info("Logging into the Checkmarx service."); + + if (config.getToken() != null) { + httpClient.setToken(config.getToken()); + return; + } + LoginSettings settings = getDefaultLoginSettings(); + settings.setRefreshToken(config.getRefreshToken()); + settings.setVersion(version); + httpClient.login(settings); + } + + public LoginSettings getDefaultLoginSettings() throws MalformedURLException { + String baseUrl = UrlUtils.parseURLToString(config.getUrl(), DEFAULT_AUTH_API_PATH); + LoginSettings result = LoginSettings.builder() + .accessControlBaseUrl(baseUrl) + .username(config.getUsername()) + .password(config.getPassword()) + .clientTypeForPasswordAuth(ClientType.RESOURCE_OWNER) + .clientTypeForRefreshToken(ClientType.CLI) + .build(); + + result.getSessionCookies().addAll(config.getSessionCookie()); + + return result; + } + + + public CxHttpClient getHttpClient() { + return httpClient; + } + + private void resolveEngineConfiguration() throws IOException { + if (config.getEngineConfigurationId() == null && config.getEngineConfigurationName() == null) { + config.setEngineConfigurationId(1); + } else if (config.getEngineConfigurationName() != null) { + final List engineConfigurations = getEngineConfiguration(); + for (EngineConfiguration engineConfiguration : engineConfigurations) { + if (engineConfiguration.getName().equalsIgnoreCase(config.getEngineConfigurationName())) { + config.setEngineConfigurationId(engineConfiguration.getId()); + log.info(String.format("Engine configuration: \"%s\" was validated in server", config.getEngineConfigurationName())); + }else{ + if ("Improved Scan Flow".equalsIgnoreCase(config.getEngineConfigurationName())){ + config.setEngineConfigurationId(1); + } + } + } + if (config.getEngineConfigurationId() == null) { + throw new CxClientException("Engine configuration: \"" + config.getEngineConfigurationName() + "\" was not found in server"); + } + } + } + + public List getEngineConfiguration() throws IOException { + this.teamPath = configureTeamPath(); + httpClient.setTeamPathHeader(this.teamPath); + return (List) httpClient.getRequest(SAST_ENGINE_CONFIG, CONTENT_TYPE_APPLICATION_JSON_V1, EngineConfiguration.class, 200, "engine configurations", true); + } + + + public void validateConfig(CxScanConfig config) throws CxClientException { + String message = null; + if (config == null) { + message = "Non-null config must be provided."; + } else if (org.apache.commons.lang3.StringUtils.isEmpty(config.getUrl()) && config.isSastOrOSAEnabled()) { + message = "Server URL is required when SAST or OSA is enabled."; + } + if (message != null) { + throw new CxClientException(message); + } + } + + private void resolveTeam() throws CxClientException, IOException { + + config.setTeamPath(configureTeamPath()); + + if (config.getTeamId() == null) { + config.setTeamId(getTeamIdByName(config.getTeamPath())); + } + + printTeamPath(); + } + + public String getTeamIdByName(String teamName) throws CxClientException, IOException { + teamName = replaceDelimiters(teamName); + List allTeams = getTeamList(); + for (Team team : allTeams) { + String fullName = replaceDelimiters(team.getFullName()); + if (fullName.equalsIgnoreCase(teamName)) { //TODO caseSenesitive + return team.getId(); + } + } + throw new CxClientException("Could not resolve team ID from team name: " + teamName); + } + + private String replaceDelimiters(String teamName) { + while (teamName.contains("\\") || teamName.contains("//")) { + teamName = teamName.replace("\\", "/"); + teamName = teamName.replace("//", "/"); + } + return teamName; + } + + private CxArmConfig getCxARMConfig() throws IOException, CxClientException { + httpClient.setTeamPathHeader(this.teamPath); + return httpClient.getRequest(CX_ARM_URL, CONTENT_TYPE_APPLICATION_JSON_V1, CxArmConfig.class, 200, "CxARM URL", false); + } + + private void resolveCxARMUrl() throws CxClientException { + try { + this.config.setCxARMUrl(getCxARMConfig().getCxARMPolicyURL()); + } catch (Exception ex) { + throw new CxClientException("CxARM is not available. Policy violations cannot be calculated: " + ex.getMessage()); + } + } + + //Some plugins share preset name while others share presetId in CxScanConfig + //If is given preference. + //presetId=0 is a special case, which does not exist in SAST but SAST has a special meaning for it, + // which is to use whatever preset available on the project settings. + private void resolvePreset() throws CxClientException, IOException { + if (config.getPresetId() == null && !StringUtils.isEmpty(config.getPresetName())) { + if(PRESETNAME_PROJET_SETTING_DEFAULT.equalsIgnoreCase(config.getPresetName())) + config.setPresetId(Integer.parseInt(PRESETID_PROJET_SETTING_DEFAULT)); + else + config.setPresetId(getPresetIdByName(config.getPresetName())); + + }else if(config.getPresetId() == Integer.parseInt(PRESETID_PROJET_SETTING_DEFAULT)) { + config.setPresetName(PRESETNAME_PROJET_SETTING_DEFAULT); + }else { + config.setPresetName(getPresetById(config.getPresetId()).getName()); + } + log.info(String.format("preset name: %s preset id: %s", config.getPresetName(), config.getPresetId())); + } + + private void resolvePostScanAction() throws CxClientException, IOException { + if (config.getPostScanActionId() == null && !StringUtils.isEmpty(config.getPostScanName())) { + config.setPostScanActionId(getPostScanActionIdByName(config.getPostScanName())); + log.info(String.format("post scan action name: %s post scan action id: %s", config.getPostScanName(), + config.getPostScanActionId())); + } + } + public int getPresetIdByName(String presetName) throws CxClientException, IOException { + List allPresets = getPresetList(); + for (Preset preset : allPresets) { + if (preset.getName().equalsIgnoreCase(presetName)) { //TODO caseSenesitive- checkkk + return preset.getId(); + } + } + + throw new CxClientException("Could not resolve preset ID from preset name: " + presetName); + } + + public List getPresetList() throws IOException, CxClientException { + configureTeamPath(); + return (List) httpClient.getRequest(CXPRESETS, CONTENT_TYPE_APPLICATION_JSON_V1, Preset.class, 200, "preset list", true); + } + + public Preset getPresetById(int presetId) throws IOException, CxClientException { + httpClient.setTeamPathHeader(this.teamPath); + return httpClient.getRequest(CXPRESETS + "/" + presetId, CONTENT_TYPE_APPLICATION_JSON_V1, Preset.class, 200, "preset by id", false); + } + + public List getPostScanActionList() throws IOException, CxClientException { + configureTeamPath(); + return (List) httpClient.getRequest(SAST_CUSTOM_TASKS, CONTENT_TYPE_APPLICATION_JSON_V1, PostAction.class, 200, "post scan action list", true); + } + + public int getPostScanActionIdByName(String name) throws CxClientException, IOException { + List allPostActionItems = getPostScanActionList(); + for (PostAction postAction : allPostActionItems) { + if (postAction.getName().equalsIgnoreCase(name)) { // TODO caseSenesitive- checkkk + return postAction.getId(); + } + } + throw new CxClientException("Could not resolve post scan item ID from post scan action list: " + name); + } + private void printTeamPath() { + try { + this.teamPath = config.getTeamPath(); + if (this.teamPath == null) { + this.teamPath = getTeamNameById(config.getTeamId()); + } + log.info(String.format("full team path: %s", this.teamPath)); + } catch (Exception e) { + log.warn("Error getting team path."); + } + } + + private void setRetentionRate(long projectId) throws IOException { + DataRetentionSettingsDto retentionRequest = new DataRetentionSettingsDto(config.getProjectRetentionRate()); + log.info("Sending request to set retentionRate for project with id {}",projectId); + String json = convertToJson(retentionRequest); + httpClient.setTeamPathHeader(teamPath); + HttpEntity entity = new StringEntity(json); + try{ + httpClient.postRequest(SAST_RETENTION_RATE.replace(ID_PATH_PARAM, Long.toString(projectId)), CONTENT_TYPE_APPLICATION_JSON_V1, entity, CxID.class, 204, "Set retention Rate for project"); + log.info("Set '{}' Retention Rate for project with project ID : '{}' ",config.getProjectRetentionRate(), projectId); + }catch (CxHTTPClientException exception){ + log.info(exception.getMessage()); + log.info("Fail to set Retention Rate for project with project ID {}", projectId); + } + } + public String getTeamNameById(String teamId) throws CxClientException, IOException { + List allTeams = getTeamList(); + for (Team team : allTeams) { + if (teamId.equals(team.getId())) { + return team.getFullName(); + } + } + throw new CxClientException("Could not resolve team name from id: " + teamId); + } + + + public List getAllProjects() throws IOException, CxClientException { + List projects = null; + configureTeamPath(); + + try { + projects = (List) httpClient.getRequest(SAST_GET_ALL_PROJECTS, CONTENT_TYPE_APPLICATION_JSON_V1, Project.class, 200, "all projects", true); + } catch (HttpResponseException ex) { + if (ex.getStatusCode() != 404) { + throw ex; + } + } + return projects; + } + + public Project getProjectById(String projectId, String contentType) throws IOException, CxClientException { + String projectNamePath = SAST_GET_PROJECT_BY_ID.replace("{projectId}", projectId); + Project projects = null; + try { + httpClient.setTeamPathHeader(this.teamPath); + projects = httpClient.getRequest(projectNamePath, contentType, Project.class, 200, "project by id: " + projectId, false); + } catch (CxHTTPClientException ex) { + if (ex.getStatusCode() != 404) { + throw ex; + } + } + return projects; + } + + + public List getConfigurationSetList() throws IOException, CxClientException { + configureTeamPath(); + return (List) httpClient.getRequest(SAST_ENGINE_CONFIG, CONTENT_TYPE_APPLICATION_JSON_V1, CxNameObj.class, 200, "engine configurations", true); + } + + public String getTeamPath() { + return teamPath; + } + + public void setTeamPath(String teamPath) { + this.teamPath = teamPath; + } + + public State getState() { + return state; + } + + public void setState(State state) { + this.state = state; + } + ///function to create child project from master branch + public long createChildProject(long projectId, String childProjectName)throws IOException, CxClientException { + long childProjectId = UNKNOWN_INT; + CreateProjectRequest request = new CreateProjectRequest(childProjectName); + String json = convertToJson(request); + httpClient.setTeamPathHeader(teamPath); + StringEntity entity = new StringEntity(json, StandardCharsets.UTF_8); + log.info("Creating branched project with name '{}' from existing project with ID {}", childProjectName, projectId); + try { + Project obj = httpClient.postRequest(PROJECT_BRANCH.replace("{id}", Long.toString(projectId)), CONTENT_TYPE_APPLICATION_JSON_V1, entity, Project.class, 201, "branch project"); + if (obj != null) { + childProjectId = obj.getId(); + return childProjectId; + + } else { + log.error("CX Response for branch project request with name '{}' from existing project with ID {} was null", childProjectName, projectId); + } + } + catch (CxHTTPClientException e) { + log.error(e.getMessage()); + log.error("Error occured while creating branched project with name '{}' from existing project with ID {}", childProjectName, projectId); + } + catch (JSONException e) { + log.error("Error processing JSON Response while creating branched project with name '{}' from existing project with ID {}", childProjectName, projectId); + log.error(ExceptionUtils.getStackTrace(e)); + } + return childProjectId; + } +} diff --git a/src/main/java/com/cx/restclient/sast/utils/SASTParam.java b/src/main/java/com/cx/restclient/sast/utils/SASTParam.java index 0ca54d5d..5877240e 100644 --- a/src/main/java/com/cx/restclient/sast/utils/SASTParam.java +++ b/src/main/java/com/cx/restclient/sast/utils/SASTParam.java @@ -9,30 +9,52 @@ public class SASTParam { public static final String SAST_UPDATE_SCAN_SETTINGS = "sast/pluginsScanSettings"; //Update preset and configuration public static final String SAST_GET_SCAN_SETTINGS = "/sast/scanSettings/{projectId}"; //Update preset and configuration public static final String SAST_CREATE_SCAN = "sast/scans"; //Run a new Scan - public static final String SAST_SCAN = "sast/scans/{scanId}"; //Get Scan status (by scan ID) + public static final String SAST_SCAN_STATUS = "sast/scans/{scanId}"; //Get Scan status (by scan ID) public static final String SAST_QUEUE_SCAN_STATUS = "sast/scansQueue/{scanId}"; + public static final String SAST_GET_PROJECT_BY_ID = "projects/{projectId}"; public static final String SAST_GET_PROJECT = "projects?projectname={name}&teamid={teamId}";// Get project) - public static final String SAST_GET_All_PROJECTS = "projects";// Get project) + public static final String SAST_GET_ALL_PROJECTS = "projects";// Get project) public static final String SAST_ZIP_ATTACHMENTS = "projects/{projectId}/sourceCode/attachments";//Attach ZIP file public static final String SAST_GET_PROJECT_SCANS = "sast/scans?projectId={projectId}"; + public static final String SAST_GET_QUEUED_SCANS = "sast/scansQueue?projectId={projectId}"; + public static final String SAST_CUSTOM_TASKS = "customTasks"; + public static final String SAST_CREATE_REMOTE_SOURCE_SCAN = "projects/%s/sourceCode/remoteSettings/%s/%s"; + public static final String SAST_EXCLUDE_FOLDERS_FILES_PATTERNS = "projects/%s/sourceCode/excludeSettings"; + + public static final String SAST_RETENTION_RATE ="projects/{id}/dataRetentionSettings"; + public static final String PROJECT_PATH = "projects/"; + public static final String CUSTOM_FIELD_PATH = "customFields"; //Once it has results public static final String SAST_SCAN_RESULTS_STATISTICS = "sast/scans/{scanId}/resultsStatistics"; public static final String SAST_CREATE_REPORT = "reports/sastScan/"; //Create new report (get ID) public static final String SAST_GET_REPORT_STATUS = "reports/sastScan/{reportId}/status"; //Get report status public static final String SAST_GET_REPORT = "reports/sastScan/{reportId}"; //Get report status + public static final String SAST_GET_CXARM_STATUS = "sast/projects/{projectId}/publisher/policyFindings/status"; //Get report status + - //ZIP PARAMS + //ZIP PARAM/ public static final long MAX_ZIP_SIZE_BYTES = 2147483648L; public static final String TEMP_FILE_NAME_TO_ZIP = "zippedSource"; + public static final String TEMP_FILE_NAME_TO_SCA_RESOLVER_RESULTS_ZIP = "ScaResolverResults"; + public static final String SCA_RESOLVER_RESULT_FILE_NAME = ".cxsca-results.json"; + + public static final String SAST_RESOLVER_RESULT_FILE_NAME=".cxsca-sast-results.json"; + public static final String TEMP_FOLDER_NAME_TO_SCA_RESOLVER_RESULTS = "ScaResolverResultsTemp"; //Links formats - public static final String LINK_FORMAT = "/CxWebClient/portal#/projectState/%d/Summary"; + public static final String LINK_FORMAT = "/CxWebClient/portal#/projectState/"; + public static final String LINK_FORMAL_SUMMARY = "/Summary"; + public static final String SCAN_LINK_FORMAT = "/CxWebClient/ViewerMain.aspx?scanId=%s&ProjectID=%s"; public static final String PROJECT_LINK_FORMAT = "/CxWebClient/portal#/projectState/%d/Summary"; //REPORT PARAMS public static final String PDF_REPORT_NAME = "CxSASTReport"; + + //PROJECT BRANCHING + public static final String PROJECT_BRANCH = "projects/{id}/branch"; + public static final String PROJECT_BRANCH_ID = "projects/branch/{id}"; } diff --git a/src/main/java/com/cx/restclient/sast/utils/SASTUtils.java b/src/main/java/com/cx/restclient/sast/utils/SASTUtils.java index 403541d8..c9d6629f 100644 --- a/src/main/java/com/cx/restclient/sast/utils/SASTUtils.java +++ b/src/main/java/com/cx/restclient/sast/utils/SASTUtils.java @@ -1,84 +1,120 @@ package com.cx.restclient.sast.utils; -import com.cx.restclient.exception.CxClientException; -import com.cx.restclient.sast.dto.CxXMLResults; -import com.cx.restclient.sast.dto.SASTResults; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; +import static com.cx.restclient.common.CxPARAM.CX_REPORT_LOCATION; -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBException; -import javax.xml.bind.Unmarshaller; import java.io.ByteArrayInputStream; import java.io.File; -import java.text.SimpleDateFormat; -import java.util.Date; +import java.io.IOException; -import static com.cx.restclient.common.CxPARAM.CX_REPORT_LOCATION; -import static com.cx.restclient.sast.utils.SASTParam.PDF_REPORT_NAME; +import org.apache.commons.io.FileUtils; +import org.glassfish.jaxb.runtime.v2.JAXBContextFactory; +import org.slf4j.Logger; + +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.dto.CxVersion; +import com.cx.restclient.exception.CxClientException; +import com.cx.restclient.sast.dto.CxXMLResults; +import com.cx.restclient.sast.dto.SASTResults; + +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Unmarshaller; /** * Created by Galn on 07/02/2018. */ public abstract class SASTUtils { - public static void deleteTempZipFile(File zipTempFile, Logger log) { - if (zipTempFile.exists() && !zipTempFile.delete()) { - log.warn("Failed to delete temporary zip file: " + zipTempFile.getAbsolutePath()); - } else { - log.info("Temporary file deleted"); - } - } - public static CxXMLResults convertToXMLResult(byte[] cxReport) throws CxClientException { CxXMLResults reportObj = null; - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cxReport); - try { - JAXBContext jaxbContext = JAXBContext.newInstance(CxXMLResults.class); + try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cxReport)) { + JAXBContextFactory jaxbContextFactory = new JAXBContextFactory(); + JAXBContext jaxbContext = jaxbContextFactory.createContext(new Class[]{CxXMLResults.class}, null); Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); reportObj = (CxXMLResults) unmarshaller.unmarshal(byteArrayInputStream); - } catch (JAXBException e) { + } catch (JAXBException | IOException e) { throw new CxClientException("Failed to parse xml report: " + e.getMessage(), e); - } finally { - IOUtils.closeQuietly(byteArrayInputStream); } return reportObj; } - public static void printSASTResultsToConsole(SASTResults sastResults,boolean enableViolations, Logger log) { + public static void printSASTResultsToConsole(CxScanConfig config, SASTResults sastResults, boolean enableViolations, Logger log) { String highNew = sastResults.getNewHigh() > 0 ? " (" + sastResults.getNewHigh() + " new)" : ""; String mediumNew = sastResults.getNewMedium() > 0 ? " (" + sastResults.getNewMedium() + " new)" : ""; String lowNew = sastResults.getNewLow() > 0 ? " (" + sastResults.getNewLow() + " new)" : ""; + String criticalNew = sastResults.getNewCritical() > 0 ? " (" + sastResults.getNewCritical() + " new)" : ""; String infoNew = sastResults.getNewInfo() > 0 ? " (" + sastResults.getNewInfo() + " new)" : ""; - - log.info("----------------------------Checkmarx Scan Results(CxSAST):-------------------------------"); + + CxVersion cxVersion = config.getCxVersion(); + String sastVersion = cxVersion != null ? cxVersion.getVersion() : null; + + if (sastVersion != null && !sastVersion.isEmpty()) { + + String[] versionComponents = sastVersion.split("\\."); + + if (versionComponents.length >= 2) { + + String currentVersion = versionComponents[0] + "." + versionComponents[1]; + float currentVersionFloat = Float.parseFloat(currentVersion); + + String cxOrigin = config.getCxOrigin(); + + if(currentVersionFloat < Float.parseFloat("9.7")){ + if(config.getSastCriticalThreshold() != null && config.getSastCriticalThreshold() != 0) { + log.warn("SAST Critical Threshold is not supported for SAST versions prior to 9.7"); + } + } + + log.info("----------------------------Checkmarx Scan Results(CxSAST):-------------------------------"); + + + if (currentVersionFloat >= Float.parseFloat("9.7")) { + log.info("Critical severity results: " + sastResults.getCritical() + criticalNew); + } + } + } log.info("High severity results: " + sastResults.getHigh() + highNew); log.info("Medium severity results: " + sastResults.getMedium() + mediumNew); log.info("Low severity results: " + sastResults.getLow() + lowNew); log.info("Information severity results: " + sastResults.getInformation() + infoNew); log.info(""); - /* if (enableViolations && !sastResults.getSastPolicies().isEmpty()) { - log.info("SAST violated policies names: " + StringUtils.join(sastResults.getSastPolicies(), ',')); - }*/ - log.info("Scan results location: " + sastResults.getSastScanLink()); + if (sastResults.getSastScanLink() != null) + log.info("Scan results location: " + sastResults.getSastScanLink()); log.info("------------------------------------------------------------------------------------------\n"); } //PDF Report - public static String writePDFReport(byte[] scanReport, File workspace, Logger log) { - String now = new SimpleDateFormat("dd_MM_yyyy-HH_mm_ss").format(new Date()); - String pdfFileName = PDF_REPORT_NAME + "_" + now + ".pdf"; + //This method is used for generate report for other file formats(CSV , XML, JSON etc) as well not only PDF file format. + public static String writePDFReport(byte[] scanReport, File workspace, String pdfFileName, Logger log, String reportFormat) { try { - FileUtils.writeByteArrayToFile(new File(workspace + CX_REPORT_LOCATION, pdfFileName), scanReport); - log.info("PDF report location: " + workspace + CX_REPORT_LOCATION + File.separator + pdfFileName); + FileUtils.writeByteArrayToFile(new File(workspace + CX_REPORT_LOCATION, pdfFileName), scanReport); + log.info("" +reportFormat + " Report Location: " + workspace + CX_REPORT_LOCATION+ File.separator+ pdfFileName); } catch (Exception e) { - log.error("Failed to write PDF report to workspace: ", e.getMessage()); + log.error("Failed to write "+reportFormat+" report to workspace: ", e.getMessage()); + pdfFileName = ""; } return pdfFileName; } + + // CLI Report/s + public static void writeReport(byte[] scanReport, String reportName, Logger log) { + try { + File reportFile = new File(reportName); + if (!reportFile.isAbsolute()) { + reportFile = new File(System.getProperty("user.dir") + CX_REPORT_LOCATION + File.separator + reportFile); + } + + if (!reportFile.getParentFile().exists()) { + reportFile.getParentFile().mkdirs(); + } + + FileUtils.writeByteArrayToFile(reportFile, scanReport); + log.info("report location: " + reportFile.getAbsolutePath()); + } catch (Exception e) { + log.error("Failed to write report: ", e.getMessage()); + } + } } diff --git a/src/main/java/com/cx/restclient/sast/utils/State.java b/src/main/java/com/cx/restclient/sast/utils/State.java new file mode 100644 index 00000000..3abf930a --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/utils/State.java @@ -0,0 +1,7 @@ +package com.cx.restclient.sast.utils; + +public enum State { + SUCCESS, + FAILED, + SKIPPED +} diff --git a/src/main/java/com/cx/restclient/sast/utils/zip/CxZip.java b/src/main/java/com/cx/restclient/sast/utils/zip/CxZip.java index aed2a75e..d8a051ac 100644 --- a/src/main/java/com/cx/restclient/sast/utils/zip/CxZip.java +++ b/src/main/java/com/cx/restclient/sast/utils/zip/CxZip.java @@ -1,13 +1,13 @@ package com.cx.restclient.sast.utils.zip; +import com.cx.restclient.dto.PathFilter; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; +import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.OutputStream; public class CxZip { @@ -23,34 +23,32 @@ public CxZip(String tempFileName, long maxZipSizeInBytes, Logger log) { this.maxZipSizeInBytes = maxZipSizeInBytes; } - public File zipWorkspaceFolder(File baseDir, String[] includes, String[] excludes) - throws IOException { - log.info("Zipping workspace: '" + baseDir + "'"); + public byte[] zipWorkspaceFolder(File baseDir, PathFilter filter) throws IOException { + log.debug("Zipping workspace: '" + baseDir + "'"); ZipListener zipListener = new ZipListener() { public void updateProgress(String fileName, long size) { numOfZippedFiles++; - log.info("Zipping (" + FileUtils.byteCountToDisplaySize(size) + "): " + fileName); + log.debug("Zipping (" + FileUtils.byteCountToDisplaySize(size) + "): " + fileName); } }; - File tempFile = File.createTempFile(tempFileName, ".bin"); - OutputStream fileOutputStream = new FileOutputStream(tempFile); - - try { - new Zipper(log).zip(baseDir, includes, excludes, fileOutputStream, maxZipSizeInBytes, zipListener); - } catch (Zipper.MaxZipSizeReached e) { - tempFile.delete(); - throw new IOException("Reached maximum upload size limit of " + FileUtils.byteCountToDisplaySize(maxZipSizeInBytes)); - } catch (Zipper.NoFilesToZip e) { - throw new IOException("No files to zip"); - } + byte[] zipFileBA; + try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { + try { + new Zipper(log).zip(baseDir, filter.getIncludes(), filter.getExcludes(), byteArrayOutputStream, maxZipSizeInBytes, zipListener); + } catch (Zipper.MaxZipSizeReached e) { + throw new IOException("Reached maximum upload size limit of " + FileUtils.byteCountToDisplaySize(maxZipSizeInBytes)); + } catch (Zipper.NoFilesToZip e) { + throw new IOException("No files to zip"); + } - log.info("Zipping complete with " + numOfZippedFiles + " files, total compressed size: " + - FileUtils.byteCountToDisplaySize(tempFile.length())); - log.info("Temporary file with zipped sources was created at: '" + tempFile.getAbsolutePath() + "'"); + log.debug("Zipping complete with " + numOfZippedFiles + " files, total compressed size: " + + FileUtils.byteCountToDisplaySize(byteArrayOutputStream.size())); - return tempFile; + zipFileBA = byteArrayOutputStream.toByteArray(); + } + return zipFileBA; } public CxZip setMaxZipSizeInBytes(long maxZipSizeInBytes) { diff --git a/src/main/java/com/cx/restclient/sast/utils/zip/CxZipUtils.java b/src/main/java/com/cx/restclient/sast/utils/zip/CxZipUtils.java index d47d9e9e..9e1a2c54 100644 --- a/src/main/java/com/cx/restclient/sast/utils/zip/CxZipUtils.java +++ b/src/main/java/com/cx/restclient/sast/utils/zip/CxZipUtils.java @@ -1,34 +1,35 @@ package com.cx.restclient.sast.utils.zip; -import com.cx.restclient.common.ShragaUtils; import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.dto.PathFilter; +import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import java.io.File; import java.io.IOException; -import java.util.List; -import java.util.Map; +import static com.cx.restclient.sast.utils.SASTParam.MAX_ZIP_SIZE_BYTES; import static com.cx.restclient.sast.utils.SASTParam.TEMP_FILE_NAME_TO_ZIP; /** * CxZipUtils generates the patterns used for zipping the workspace folder */ - - public abstract class CxZipUtils { - public static File zipWorkspaceFolder(CxScanConfig config, long maxZipBytes, Logger log) throws IOException { - Map> stringListMap = ShragaUtils.generateIncludesExcludesPatternLists(config.getSastFolderExclusions(), config.getSastFilterPattern(), log); - List includes = stringListMap.get(ShragaUtils.INCLUDES_LIST); - List excludes = stringListMap.get(ShragaUtils.EXCLUDES_LIST); - - CxZip cxZip = new CxZip(TEMP_FILE_NAME_TO_ZIP, maxZipBytes, log); - - return cxZip.zipWorkspaceFolder(new File(config.getSourceDir()), includes.toArray(new String[includes.size()]), excludes.toArray(new String[excludes.size()])); - + public synchronized static byte[] getZippedSources(CxScanConfig config, PathFilter filter, String sourceDir, Logger log) throws IOException { + byte[] zipFile = config.getZipFile() != null ? FileUtils.readFileToByteArray(config.getZipFile()) : null; + if (zipFile == null) { + log.debug("----------------------------------- Start zipping files :------------------------------------"); + Long maxZipSize = config.getMaxZipSize() != null ? config.getMaxZipSize() * 1024 * 1024 : MAX_ZIP_SIZE_BYTES; + + CxZip cxZip = new CxZip(TEMP_FILE_NAME_TO_ZIP, maxZipSize, log); + zipFile = cxZip.zipWorkspaceFolder(new File(sourceDir), filter); + log.debug("----------------------------------- Finish zipping files :------------------------------------"); + } + return zipFile; } + } diff --git a/src/main/java/com/cx/restclient/sast/utils/zip/NewCxZipFile.java b/src/main/java/com/cx/restclient/sast/utils/zip/NewCxZipFile.java new file mode 100644 index 00000000..2d5fa5d1 --- /dev/null +++ b/src/main/java/com/cx/restclient/sast/utils/zip/NewCxZipFile.java @@ -0,0 +1,142 @@ +package com.cx.restclient.sast.utils.zip; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.tools.ant.DirectoryScanner; +import org.apache.tools.zip.ZipEntry; +import org.apache.tools.zip.ZipOutputStream; +import org.slf4j.Logger; + +import java.io.*; +import java.util.List; + + +public class NewCxZipFile implements Closeable { + + private static final double AVERAGE_ZIP_COMPRESSION_RATIO = 4.0D; + + private final Logger log; + private final long maxSize; + private final ZipListener listener; + private final OutputStream outputStream; + private final ZipOutputStream zipOutputStream; + private long fileCount; + private long compressedSize; + + public NewCxZipFile(File zipFile, long maxZipSizeInBytes, Logger log) throws IOException { + this.log = log; + this.maxSize = maxZipSizeInBytes; + this.fileCount = 0; + this.compressedSize = 0; + this.listener = (fileName, size) -> { + fileCount++; + if (log.isInfoEnabled()) + log.info("Zipping ( {} ): {}", FileUtils.byteCountToDisplaySize(size), fileName); + }; + outputStream = new FileOutputStream(zipFile); + zipOutputStream = new ZipOutputStream(outputStream); + zipOutputStream.setEncoding("UTF8"); + } + + + public void zipContentAsFile(String pathInZip, byte[] content) throws IOException { + + validateNextFileWillNotReachMaxCompressedSize((double) content.length); + + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(content)) { + compressedSize += InsertZipEntry(zipOutputStream, pathInZip, inputStream); + if (listener != null) { + listener.updateProgress(pathInZip, compressedSize); + } + } catch (IOException ioException) { + log.warn(String.format("Failed to add file to archive: %s", pathInZip), ioException); + } + } + + public void close() { + IOUtils.closeQuietly(zipOutputStream); + IOUtils.closeQuietly(outputStream); + } + + public void addMultipleFilesToArchive(File baseDir, List relativePaths) throws IOException { + assert baseDir != null : "baseDir must not be null"; + assert outputStream != null : "outputStream must not be null"; + + int len$ = relativePaths.size(); + + for (int i$ = 0; i$ < len$; ++i$) { + String fileName = relativePaths.get(i$); + + File file = new File(baseDir, fileName); + if (!file.canRead()) { + log.warn("Skipping unreadable file: {}", file); + continue; + } + validateNextFileWillNotReachMaxCompressedSize((double) file.length()); + + try (FileInputStream fileInputStream = new FileInputStream(file)) { + compressedSize += InsertZipEntry(zipOutputStream, fileName, fileInputStream); + if (listener != null) { + listener.updateProgress(fileName, compressedSize); + } + } catch (IOException ioException) { + log.warn(String.format("Failed to add file to archive: %s", fileName), ioException); + } + } + zipOutputStream.flush(); + } + + private void validateNextFileWillNotReachMaxCompressedSize(double uncompressedSize) throws IOException { + if (maxSize > 0L && (double) compressedSize + uncompressedSize / AVERAGE_ZIP_COMPRESSION_RATIO > (double) maxSize) { + log.info("Maximum zip file size reached. Zip size: {} bytes Limit: {} bytes", compressedSize, maxSize); + throw new MaxZipSizeReached(compressedSize, maxSize); + } + } + + private long InsertZipEntry(ZipOutputStream zipOutputStream, String fileName, InputStream inputStream) throws IOException { + ZipEntry zipEntry = new ZipEntry(fileName); + zipOutputStream.putNextEntry(zipEntry); + IOUtils.copy(inputStream, zipOutputStream); + zipOutputStream.closeEntry(); + return zipEntry.getCompressedSize(); + } + + private DirectoryScanner createDirectoryScanner(File baseDir, String[] filterIncludePatterns, String[] filterExcludePatterns) { + DirectoryScanner ds = new DirectoryScanner(); + ds.setBasedir(baseDir); + ds.setCaseSensitive(false); + ds.setFollowSymlinks(true); + ds.setErrorOnMissingDir(false); + if (filterIncludePatterns != null && filterIncludePatterns.length > 0) { + ds.setIncludes(filterIncludePatterns); + } + + if (filterExcludePatterns != null && filterExcludePatterns.length > 0) { + ds.setExcludes(filterExcludePatterns); + } + + return ds; + } + + public long getFileCount() { + return fileCount; + } + + public static class MaxZipSizeReached extends IOException { + private long compressedSize; + private long maxZipSize; + + public MaxZipSizeReached(long compressedSize, long maxZipSize) { + super("Zip compressed size reached a limit of " + maxZipSize + " bytes"); + } + + public long getCompressedSize() { + return this.compressedSize; + } + + public long getMaxZipSize() { + return this.maxZipSize; + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/cx/restclient/sast/utils/zip/Zipper.java b/src/main/java/com/cx/restclient/sast/utils/zip/Zipper.java index 02fcee48..f9c55c06 100644 --- a/src/main/java/com/cx/restclient/sast/utils/zip/Zipper.java +++ b/src/main/java/com/cx/restclient/sast/utils/zip/Zipper.java @@ -7,6 +7,7 @@ import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ArrayUtils; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.zip.ZipEntry; import org.apache.tools.zip.ZipOutputStream; @@ -26,41 +27,39 @@ public Zipper(Logger log) { public void zip(File baseDir, String[] filterIncludePatterns, String[] filterExcludePatterns, OutputStream outputStream, long maxZipSize, ZipListener listener) throws IOException { assert baseDir != null : "baseDir must not be null"; - assert outputStream != null : "outputStream must not be null"; - DirectoryScanner ds = this.createDirectoryScanner(baseDir, filterIncludePatterns, filterExcludePatterns); - ds.setFollowSymlinks(true); + DirectoryScanner ds = createDirectoryScanner(baseDir, filterIncludePatterns, filterExcludePatterns); ds.scan(); - // this.printDebug(ds); + printDebug(ds); if (ds.getIncludedFiles().length == 0) { outputStream.close(); - log.info("No files to zip"); - throw new Zipper.NoFilesToZip(); - } else { - this.zipFile(baseDir, ds.getIncludedFiles(), outputStream, maxZipSize, listener); + log.debug("No files to zip"); + throw new NoFilesToZip(); } + + zipFile(baseDir, ds.getIncludedFiles(), outputStream, maxZipSize, listener); } private void zipFile(File baseDir, String[] files, OutputStream outputStream, long maxZipSize, ZipListener listener) throws IOException { - ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream); - zipOutputStream.setEncoding("UTF8"); - long compressedSize = 0L; - double AVERAGE_ZIP_COMPRESSION_RATIO = 4.0D; - String[] arr$ = files; - int len$ = files.length; - - for (int i$ = 0; i$ < len$; ++i$) { - String fileName = arr$[i$]; - // log.debug("Adding file to zip: " + fileName); - File file = new File(baseDir, fileName); - if (!file.canRead()) { - log.warn("Skipping unreadable file: " + file); - } else { - if (maxZipSize > 0L && (double) compressedSize + (double) file.length() / 4.0D > (double) maxZipSize) { - log.info("Maximum zip file size reached. Zip size: " + compressedSize + " bytes Limit: " + maxZipSize + " bytes"); + try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { + zipOutputStream.setEncoding("UTF8"); + long compressedSize = 0; + final double AVERAGE_ZIP_COMPRESSION_RATIO = 4.0; + + for (String fileName : files) { + log.debug("Adding file to zip: " + fileName); + + File file = new File(baseDir, fileName); + if (!file.canRead()) { + log.debug("Skipping unreadable file: " + file); + continue; + } + + if (maxZipSize > 0 && compressedSize + (file.length() / AVERAGE_ZIP_COMPRESSION_RATIO) > maxZipSize) { + log.debug("Maximum zip file size reached. Zip size: " + compressedSize + " bytes Limit: " + maxZipSize + " bytes"); zipOutputStream.close(); - throw new Zipper.MaxZipSizeReached(compressedSize, maxZipSize); + throw new MaxZipSizeReached(compressedSize, maxZipSize); } if (listener != null) { @@ -69,22 +68,20 @@ private void zipFile(File baseDir, String[] files, OutputStream outputStream, lo ZipEntry zipEntry = new ZipEntry(fileName); zipOutputStream.putNextEntry(zipEntry); - FileInputStream fileInputStream = new FileInputStream(file); - IOUtils.copy(fileInputStream, zipOutputStream); - fileInputStream.close(); + try (FileInputStream fileInputStream = new FileInputStream(file)) { + IOUtils.copy(fileInputStream, zipOutputStream); + } zipOutputStream.closeEntry(); compressedSize += zipEntry.getCompressedSize(); } } - - zipOutputStream.close(); } private DirectoryScanner createDirectoryScanner(File baseDir, String[] filterIncludePatterns, String[] filterExcludePatterns) { DirectoryScanner ds = new DirectoryScanner(); ds.setBasedir(baseDir); ds.setCaseSensitive(false); - ds.setFollowSymlinks(false); + ds.setFollowSymlinks(true); ds.setErrorOnMissingDir(false); if (filterIncludePatterns != null && filterIncludePatterns.length > 0) { ds.setIncludes(filterIncludePatterns); diff --git a/src/main/java/com/cx/restclient/sca/dto/CreateProjectRequest.java b/src/main/java/com/cx/restclient/sca/dto/CreateProjectRequest.java new file mode 100644 index 00000000..30e8c6bc --- /dev/null +++ b/src/main/java/com/cx/restclient/sca/dto/CreateProjectRequest.java @@ -0,0 +1,13 @@ +package com.cx.restclient.sca.dto; + +public class CreateProjectRequest { + private String name; + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/com/cx/restclient/sca/dto/CxSCAResolvingConfiguration.java b/src/main/java/com/cx/restclient/sca/dto/CxSCAResolvingConfiguration.java new file mode 100644 index 00000000..3606e581 --- /dev/null +++ b/src/main/java/com/cx/restclient/sca/dto/CxSCAResolvingConfiguration.java @@ -0,0 +1,23 @@ +package com.cx.restclient.sca.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +public class CxSCAResolvingConfiguration implements Serializable { + private List Manifests = new ArrayList<>(); + private List Fingerprints = new ArrayList<>(); + + public String getManifestsIncludePattern(){ + return String.join(",", Manifests); + } + + public String getFingerprintsIncludePattern(){ + return String.join(",", Fingerprints); + } +} diff --git a/src/main/java/com/cx/restclient/sca/dto/CxSCAScanAPIConfig.java b/src/main/java/com/cx/restclient/sca/dto/CxSCAScanAPIConfig.java new file mode 100644 index 00000000..95762901 --- /dev/null +++ b/src/main/java/com/cx/restclient/sca/dto/CxSCAScanAPIConfig.java @@ -0,0 +1,11 @@ +package com.cx.restclient.sca.dto; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class CxSCAScanAPIConfig { + private boolean isZeroCodeZip; + private String includeSourceCode; +} diff --git a/src/main/java/com/cx/restclient/sca/dto/CxSCAScanApiConfigEntry.java b/src/main/java/com/cx/restclient/sca/dto/CxSCAScanApiConfigEntry.java new file mode 100644 index 00000000..abb5dad9 --- /dev/null +++ b/src/main/java/com/cx/restclient/sca/dto/CxSCAScanApiConfigEntry.java @@ -0,0 +1,10 @@ +package com.cx.restclient.sca.dto; +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class CxSCAScanApiConfigEntry implements ScanAPIConfigEntry { + private String type; + private CxSCAScanAPIConfig value; +} diff --git a/src/main/java/com/cx/restclient/sca/dto/GetUploadUrlRequest.java b/src/main/java/com/cx/restclient/sca/dto/GetUploadUrlRequest.java new file mode 100644 index 00000000..8e1f75e4 --- /dev/null +++ b/src/main/java/com/cx/restclient/sca/dto/GetUploadUrlRequest.java @@ -0,0 +1,12 @@ +package com.cx.restclient.sca.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Builder +@Getter +public class GetUploadUrlRequest { + private List config; +} diff --git a/src/main/java/com/cx/restclient/sca/dto/RemoteRepositoryInfo.java b/src/main/java/com/cx/restclient/sca/dto/RemoteRepositoryInfo.java new file mode 100644 index 00000000..a28211d2 --- /dev/null +++ b/src/main/java/com/cx/restclient/sca/dto/RemoteRepositoryInfo.java @@ -0,0 +1,20 @@ +package com.cx.restclient.sca.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.net.URL; + +/** + * Instructs SCA which repository should be scanned. + * In the future this class may be expanded to include repo credentials and commit/branch/tag reference. + */ +@Getter +@Setter +public class RemoteRepositoryInfo implements Serializable { + /** + * A URL for which 'git clone' is possible. + */ + private URL url; +} diff --git a/src/main/java/com/cx/restclient/sca/dto/SbomReportResponse.java b/src/main/java/com/cx/restclient/sca/dto/SbomReportResponse.java new file mode 100644 index 00000000..cc4adfef --- /dev/null +++ b/src/main/java/com/cx/restclient/sca/dto/SbomReportResponse.java @@ -0,0 +1,56 @@ +package com.cx.restclient.sca.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class SbomReportResponse { + + private String exportId; + private String exportStatus; + private String fileUrl; + + public SbomReportResponse() { + // default constructor + } + + @JsonCreator + public SbomReportResponse( + @JsonProperty("exportId") String exportId, + @JsonProperty("exportStatus") String exportStatus, + @JsonProperty("fileUrl") String fileUrl) { + this.exportId = exportId; + this.exportStatus = exportStatus; + this.fileUrl = fileUrl; + } + + public String getExportId() { + return exportId; + } + + public void setExportId(String exportId) { + this.exportId = exportId; + } + + public String getExportStatus() { + return exportStatus; + } + + public void setExportStatus(String exportStatus) { + this.exportStatus = exportStatus; + } + + public String getFileUrl() { + return fileUrl; + } + + public void setFileUrl(String fileUrl) { + this.fileUrl = fileUrl; + } + + @Override + public String toString() { + return "SbomReportResponse [exportId=" + exportId + + ", exportStatus=" + exportStatus + + ", fileUrl=" + fileUrl + "]"; + } +} diff --git a/src/main/java/com/cx/restclient/sca/dto/ScanAPIConfigEntry.java b/src/main/java/com/cx/restclient/sca/dto/ScanAPIConfigEntry.java new file mode 100644 index 00000000..d19e80b0 --- /dev/null +++ b/src/main/java/com/cx/restclient/sca/dto/ScanAPIConfigEntry.java @@ -0,0 +1,4 @@ +package com.cx.restclient.sca.dto; + +public interface ScanAPIConfigEntry { +} diff --git a/src/main/java/com/cx/restclient/sca/dto/ScanReportExportIdRequester.java b/src/main/java/com/cx/restclient/sca/dto/ScanReportExportIdRequester.java new file mode 100644 index 00000000..0dfdf6d5 --- /dev/null +++ b/src/main/java/com/cx/restclient/sca/dto/ScanReportExportIdRequester.java @@ -0,0 +1,41 @@ +package com.cx.restclient.sca.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ScanReportExportIdRequester { + + @JsonProperty("ScanId") + private String scanId; + + @JsonProperty("FileFormat") + private String fileFormat; + + public ScanReportExportIdRequester(String scanId, String fileFormat) { + this.scanId = scanId; + this.fileFormat = fileFormat; + } + + public String getScanId() { + return scanId; + } + + public void setScanId(String scanId) { + this.scanId = scanId; + } + + public String getFileFormat() { + return fileFormat; + } + + public void setFileFormat(String fileFormat) { + this.fileFormat = fileFormat; + } + + @Override + public String toString() { + return "ScanReportExporter [scanId=" + scanId + ", fileFormat=" + fileFormat + "]"; + } + +} + + diff --git a/src/main/java/com/cx/restclient/sca/dto/Tags.java b/src/main/java/com/cx/restclient/sca/dto/Tags.java new file mode 100644 index 00000000..49a2112f --- /dev/null +++ b/src/main/java/com/cx/restclient/sca/dto/Tags.java @@ -0,0 +1,5 @@ +package com.cx.restclient.sca.dto; + +public class Tags { + +} diff --git a/src/main/java/com/cx/restclient/sca/utils/CxSCAFileSystemUtils.java b/src/main/java/com/cx/restclient/sca/utils/CxSCAFileSystemUtils.java new file mode 100644 index 00000000..8de35e57 --- /dev/null +++ b/src/main/java/com/cx/restclient/sca/utils/CxSCAFileSystemUtils.java @@ -0,0 +1,90 @@ +package com.cx.restclient.sca.utils; + +import com.cx.restclient.dto.PathFilter; +import org.apache.tools.ant.DirectoryScanner; +import org.slf4j.Logger; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +public class CxSCAFileSystemUtils { + + public static String[] scanAndGetIncludedFiles(String baseDir, PathFilter filter) { + DirectoryScanner ds = createDirectoryScanner(new File(baseDir), filter.getIncludes(), filter.getExcludes()); + ds.setFollowSymlinks(true); + ds.scan(); + return ds.getIncludedFiles(); + } + + private static DirectoryScanner createDirectoryScanner(File baseDir, String[] filterIncludePatterns, String[] filterExcludePatterns) { + DirectoryScanner ds = new DirectoryScanner(); + ds.setBasedir(baseDir); + ds.setCaseSensitive(false); + ds.setFollowSymlinks(false); + ds.setErrorOnMissingDir(false); + + if (filterIncludePatterns != null && filterIncludePatterns.length > 0) { + ds.setIncludes(filterIncludePatterns); + } + + if (filterExcludePatterns != null && filterExcludePatterns.length > 0) { + ds.setExcludes(filterExcludePatterns); + } + + return ds; + } + + public static HashMap convertStringToKeyValueMap(String envString) { + + HashMap envMap = new HashMap<>(); + //"Key1:Val1,Key2:Val2" + String trimmedString = envString.replace("\"",""); + List envlist = Arrays.asList(trimmedString.split(",")); + + for( String pair : envlist) + { + String[] splitFromColon = pair.split(":",2); + String key = (splitFromColon[0]).trim(); + String value = (splitFromColon[1]).trim(); + envMap.put(key, value); + } + return envMap; + + } + + public static Path checkIfFileExists(String sourceDir, String fileString, String fileSystemSeparator, Logger log) + { + Path filePath = null; + try { + filePath = Paths.get(fileString); + if (Files.notExists(filePath) && !filePath.isAbsolute()) { + filePath = Paths.get(sourceDir, fileSystemSeparator, fileString); + if (Files.notExists(filePath)) { + log.info("File doesnt exist at the given location."); + filePath = null; + } + } + + } + catch (InvalidPathException e) + { + log.error("Invalid file path. Error Message :" + e.getMessage()); + } + catch (SecurityException e) + { + log.error("Unable to access the file. Error Message :" + e.getMessage()); + } + catch (Exception e) + { + log.error("Error while determing the existence of file. Error Message :" + e.getMessage()); + } + return filePath; + } + +} diff --git a/src/main/java/com/cx/restclient/sca/utils/CxSCAResolverUtils.java b/src/main/java/com/cx/restclient/sca/utils/CxSCAResolverUtils.java new file mode 100644 index 00000000..6cc0ebad --- /dev/null +++ b/src/main/java/com/cx/restclient/sca/utils/CxSCAResolverUtils.java @@ -0,0 +1,88 @@ +package com.cx.restclient.sca.utils; + +import java.util.*; +import java.text.ParseException; + +import org.apache.commons.lang3.StringUtils; + + +public class CxSCAResolverUtils { + + public static Map shortArgumentsMap() { + Map args = new HashMap<>(); + args.put("-a", "--account"); + args.put("-c", "--config-path"); + args.put("-e", "--excludes"); + args.put("-n", "--project-name"); + args.put("-p", "--password"); + args.put("-r", "--resolver-result-path"); + args.put("-s", "--scan-path"); + args.put("-t", "--project-teams"); + args.put("-u", "--username"); + args.put("-v", "--version"); + + return Collections.unmodifiableMap(args); + } + + public static Map parseArguments(String text) throws ParseException { + // Split the provided arguments text on spaces. + // NOTE: We loose multiple spaces information but that should not be of an issue. + Map parsed = new HashMap<>(); + + text = text.trim(); + if (StringUtils.isEmpty(text)) { + return parsed; + } + + String[] arguments = text.split("\\s+"); + Map shortArgs = shortArgumentsMap(); + + int parsePos = 0; // Keep track of our position in the text for error reporting. + for (int i = 0; i < arguments.length;) { + String arg = arguments[i]; + // The argument value may have spaces. This buffer is used to reconstruct the original value. + List valueBuffer = new ArrayList<>(); + + // Check that we caught a short (`-x`) or long (`--xxxx`) argument. + if (!(arg.startsWith("-") && arg.length() == 2) && !arg.startsWith("--")) { + throw new ParseException("Could not parse provided arguments: " + text, parsePos); + } + + // Do we have a long argument in the form `--xxxx=XXXXX`? + if (arg.contains("=")) { + String[] parts = arg.split("=", 2); + arg = parts[0]; + valueBuffer.add(parts[1]); + } + + parsePos += arg.length() + 1; + if (shortArgs.containsKey(arg)) { + arg = shortArgs.get(arg); + } + + // Complete the value until we reach a new argument. + for (i++; i < arguments.length; i++) { + if ((arguments[i].startsWith("-") && arguments[i].length() == 2) || arguments[i].startsWith("--")) { + break; + } + valueBuffer.add(arguments[i]); + } + + // Reconstruct and unescape the value. + String value = null; + if (!valueBuffer.isEmpty()) { + value = String.join(" ", valueBuffer); + parsePos += value.length() + 1; + + // Remove potential enclosing quotes. + if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) { + value = value.substring(1, value.length() - 1); + } + } + + parsed.put(arg, value); + } + + return parsed; + } +} diff --git a/src/main/java/com/cx/restclient/sca/utils/fingerprints/CxSCAFileFingerprints.java b/src/main/java/com/cx/restclient/sca/utils/fingerprints/CxSCAFileFingerprints.java new file mode 100644 index 00000000..37ef82e0 --- /dev/null +++ b/src/main/java/com/cx/restclient/sca/utils/fingerprints/CxSCAFileFingerprints.java @@ -0,0 +1,51 @@ +package com.cx.restclient.sca.utils.fingerprints; + +import java.util.ArrayList; +import java.util.List; + +public class CxSCAFileFingerprints { + private String path; + private long size; + private List signatures = new ArrayList<>(); + + + public CxSCAFileFingerprints(String path, long size, List sig) { + this.path = path; + this.size = size; + this.signatures = sig; + } + + public CxSCAFileFingerprints(String path, long size) { + this.path = path; + this.size = size; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + + public List getSignatures() { + return signatures; + } + + public void setSignatures(List signatures) { + this.signatures = signatures; + } + + public void addFileSignature(CxSCAFileSignature signature){ + this.signatures.add(signature); + } +} diff --git a/src/main/java/com/cx/restclient/sca/utils/fingerprints/CxSCAFileSignature.java b/src/main/java/com/cx/restclient/sca/utils/fingerprints/CxSCAFileSignature.java new file mode 100644 index 00000000..41ce1800 --- /dev/null +++ b/src/main/java/com/cx/restclient/sca/utils/fingerprints/CxSCAFileSignature.java @@ -0,0 +1,27 @@ +package com.cx.restclient.sca.utils.fingerprints; + +public class CxSCAFileSignature { + private String type; + private String value; + + public CxSCAFileSignature(String type, String value) { + this.type = type; + this.value = value; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/src/main/java/com/cx/restclient/sca/utils/fingerprints/CxSCAScanFingerprints.java b/src/main/java/com/cx/restclient/sca/utils/fingerprints/CxSCAScanFingerprints.java new file mode 100644 index 00000000..27eafee8 --- /dev/null +++ b/src/main/java/com/cx/restclient/sca/utils/fingerprints/CxSCAScanFingerprints.java @@ -0,0 +1,40 @@ +package com.cx.restclient.sca.utils.fingerprints; + +import java.util.ArrayList; +import java.util.List; + +public class CxSCAScanFingerprints { + + private String version; + private String time; + private List fingerprints = new ArrayList<>(); + + + public CxSCAScanFingerprints(){ + version = "1.0.0"; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public List getFingerprints() { + return fingerprints; + } + + public void addFileFingerprints(CxSCAFileFingerprints fileFingerprints){ + fingerprints.add(fileFingerprints); + } + + public String getTime() { + return time; + } + + public void setTime(String time) { + this.time = time; + } +} diff --git a/src/main/java/com/cx/restclient/sca/utils/fingerprints/FingerprintCollector.java b/src/main/java/com/cx/restclient/sca/utils/fingerprints/FingerprintCollector.java new file mode 100644 index 00000000..858d8ac4 --- /dev/null +++ b/src/main/java/com/cx/restclient/sca/utils/fingerprints/FingerprintCollector.java @@ -0,0 +1,81 @@ +package com.cx.restclient.sca.utils.fingerprints; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + + +public class FingerprintCollector { + + private static final String DEFAULT_FINGERPRINT_FILENAME = "CxSCAFingerprints.json"; + private final SignatureCalculator sha1SignatureCalculator; + private final Logger log; + + public FingerprintCollector(Logger log){ + this.log = log; + sha1SignatureCalculator = new Sha1SignatureCalculator(); + } + + public CxSCAScanFingerprints collectFingerprints(String baseDir, + List files) { + log.info(String.format("Started fingerprint collection on %s", baseDir)); + + CxSCAScanFingerprints scanFingerprints = new CxSCAScanFingerprints(); + + + for (String filePath : files) { + + Path fullFilePath = Paths.get(baseDir, filePath); + try { + log.debug(String.format("Calculating signatures for file %s", fullFilePath)); + byte[] fileContent = Files.readAllBytes(fullFilePath); + CxSCAFileFingerprints fingerprints = new CxSCAFileFingerprints(filePath, Files.size(fullFilePath)); + + fingerprints.addFileSignature(sha1SignatureCalculator.calculateSignature(fileContent)); + + scanFingerprints.addFileFingerprints(fingerprints); + } catch (IOException e) { + log.error(String.format("Failed calculating file signature: %s",fullFilePath.toString() ), e); + } + } + log.info(String.format("Calculated fingerprints for %d files", scanFingerprints.getFingerprints().size())); + scanFingerprints.setTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + return scanFingerprints; + } + + public void writeScanFingerprintsFile(CxSCAScanFingerprints scanFingerprints, String path) throws IOException { + + long fingerprintCount = scanFingerprints.getFingerprints().size(); + if (fingerprintCount == 0){ + log.info("No supported files for fingerprinting found in this scan"); + } + String fingerprintFilePath = path; + File targetLocation = new File(path); + if (targetLocation.isDirectory()){ + fingerprintFilePath = Paths.get(path, DEFAULT_FINGERPRINT_FILENAME).toString(); + } + + log.info(String.format("Writing %d file signatures to fingerprint file: %s", fingerprintCount, fingerprintFilePath)); + ObjectMapper objectMapper = new ObjectMapper(); + File fingerprintFile = new File(fingerprintFilePath); + objectMapper.writeValue(fingerprintFile, scanFingerprints); + + } + + + public static String getFingerprintsAsJsonString(CxSCAScanFingerprints scanFingerprints) throws JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.writeValueAsString(scanFingerprints); + } + + +} diff --git a/src/main/java/com/cx/restclient/sca/utils/fingerprints/Sha1SignatureCalculator.java b/src/main/java/com/cx/restclient/sca/utils/fingerprints/Sha1SignatureCalculator.java new file mode 100644 index 00000000..ae743515 --- /dev/null +++ b/src/main/java/com/cx/restclient/sca/utils/fingerprints/Sha1SignatureCalculator.java @@ -0,0 +1,28 @@ +package com.cx.restclient.sca.utils.fingerprints; + +import com.cx.restclient.exception.CxClientException; +import org.apache.commons.codec.binary.Hex; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class Sha1SignatureCalculator implements SignatureCalculator { + + + private static final String SHA1_SIGNATURE_TYPE_NAME = "SHA1"; + + @Override + public CxSCAFileSignature calculateSignature(byte[] content) throws CxClientException { + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + throw new CxClientException("Unable to use SHA-1 algorithm", e); + } + + digest.update(content); + + return new CxSCAFileSignature(SHA1_SIGNATURE_TYPE_NAME, Hex.encodeHexString(digest.digest())); + } + +} diff --git a/src/main/java/com/cx/restclient/sca/utils/fingerprints/SignatureCalculator.java b/src/main/java/com/cx/restclient/sca/utils/fingerprints/SignatureCalculator.java new file mode 100644 index 00000000..66b8766e --- /dev/null +++ b/src/main/java/com/cx/restclient/sca/utils/fingerprints/SignatureCalculator.java @@ -0,0 +1,11 @@ +package com.cx.restclient.sca.utils.fingerprints; + +import com.cx.restclient.exception.CxClientException; + +import java.io.IOException; + + +public interface SignatureCalculator { + CxSCAFileSignature calculateSignature(byte[] content) throws IOException, CxClientException; + +} diff --git a/src/main/resources/com/cx/report/report.ftl b/src/main/resources/com/cx/report/report.ftl index afc4f8cb..de64ac75 100644 --- a/src/main/resources/com/cx/report/report.ftl +++ b/src/main/resources/com/cx/report/report.ftl @@ -34,7 +34,7 @@ background-color: #372F51; } - .cx-report .legend-color-box.new-legend-color { + .cx-report .legend-color-box.new-legend-color { background: linear-gradient(45deg, white 25%, #373050 25%, #373050 50%, white 50%, white 75%, #373050 75%); background-size: 4px 4px; } @@ -371,10 +371,9 @@ background-color: #373050; } - - .threshold-exceeded, - .threshold-compliance, - .policy-compliance{ + .threshold-exceeded, + .threshold-compliance, + .policy-compliance { min-width: 100%; display: inline-flex; font-size: 14px; @@ -383,21 +382,21 @@ padding: 4px 9px; } - .threshold-exceeded { + .threshold-exceeded { background-color: #DA2945; color: white; border-radius: 2px; font-weight: bold; } - .policy-compliance{ + + .policy-compliance { border-radius: 2px; font-weight: bold; } - .threshold-exceeded-icon, .threshold-compliance-icon, - .policy-compliance{ + .policy-compliance { display: inline-flex; padding-right: 6px; margin: auto 0; @@ -426,11 +425,10 @@ overflow: hidden; } - .cx-report .results-report .libraries-vulnerable-number{ + .cx-report .results-report .libraries-vulnerable-number { font-size: 18px; } - .cx-report .results-report .libraries-vulnerable-text { font-size: 12px; line-height: 15px; @@ -627,9 +625,10 @@ .cx-report .html-report.download-icon { margin-right: 6px; } - .cx-report .pdf-report.download-icon, { + + .cx-report .pdf-report.download-icon { margin-right: 6px; - border-left: solid 1px #d5d5d + border-left: 1px solid #d5d5d5; } .cx-report .summary-section .html-report { @@ -740,9 +739,11 @@ padding: 11px; } + .cx-report table.cve-table td.sast-cve-table-critical .cx-report table.cve-table td.sast-cve-table-high, .cx-report table.cve-table td.sast-cve-table-medium, - .cx-report table.cve-table td.sast-cve-table-low { + .cx-report table.cve-table td.sast-cve-table-low + .cx-report table.cve-table td.sast-cve-table-critical { max-width: 19px; } @@ -808,6 +809,7 @@ margin-top: -3px; margin-left: -7px; } + .scan-status .content-scan-status li { margin-top: 6px; margin-bottom: 6px; @@ -826,7 +828,6 @@ padding-top: 10px; } - .scan-status .indicator-scan-status.success { background-color: #38d87d; } @@ -839,7 +840,7 @@ margin-top: 5px; text-align: center; border: 1px solid #ffffff; - padding-left: 5px ; + padding-left: 5px; } .scan-status .indicator-scan-status.success .icon-scan-status { @@ -850,23 +851,20 @@ border: 1px solid #DD3D56; } - - - .scan-status .title-scan-status { font-size: 12px; font-weight: bold; padding-left: 16px; + padding-top: 10px; } - .scan-status .title-scan-status.failure { + .scan-status .content-scan-status .title-scan-status.failure { padding-top: 7px; color: #DD3D56; padding-bottom: 3px; } - } - .scan-status .title-scan-status.success { + .scan-status .content-scan-status .title-scan-status.success { color: #38d87d; } @@ -909,91 +907,109 @@ -
Checkmarx Report
- <#if buildFailed> -
-
-
- - - error - Created with Sketch. - - - - - - - - - - - - - - - -
-
-
-

- Checkmarx Scan Failed -

-
    - <#if policyViolated> -
  • ${osa.osaPolicies?size} ${policyLabel} Violated
  • - - <#if config.sastEnabled && sast.sastResultsReady && (sastThresholdExceeded || sastNewResultsExceeded) && config.osaEnabled && osa.osaResultsReady && osaThresholdExceeded> -
  • CxSAST and CxOSA Vulnerability Thresholds Exceeded
  • - <#elseif config.sastEnabled && sast.sastResultsReady && (sastThresholdExceeded || sastNewResultsExceeded)> -
  • CxSAST Vulnerability Threshold Exceeded
  • - <#elseif config.osaEnabled && osa.osaResultsReady && osaThresholdExceeded> -
  • CxOSA Vulnerability Threshold Exceeded
  • - -
-
-
- <#else> -
-
-
- - - OK - Created with Sketch. - - - - - - - - - - - - - - - - - - -
-
-
-

- Checkmarx Scan Passed -

-
-
- - + <#if buildFailed> +
+
+
+ + + error + Created with Sketch. + + + + + + + + + + + + + + + +
+
+
+

+ Checkmarx scan found the following issues: +

+
    + <#if config.isSastEnabled() && !sast.sastResultsReady> +
  • SAST Scan Failed
  • + + <#if config.isOsaEnabled() && !dependencyResult.resultReady> +
  • OSA Scan Failed
  • + + <#if config.isAstScaEnabled() && !dependencyResult.resultReady> +
  • SCA Scan Failed
  • + + <#if policyViolated> +
  • ${policyViolatedCount} ${sast.encodeXSS(policyLabel)} Violated
  • + + <#if config.isSastEnabled() && sast.sastResultsReady && (sastThresholdExceeded || sastNewResultsExceeded) && (config.isOsaEnabled() || config.isAstScaEnabled()) && dependencyResult.resultReady && dependencyThresholdExceeded> +
  • Exceeded CxSAST and CxOSA/CxSCA Vulnerability Thresholds
  • + <#elseif config.isSastEnabled() && sast.sastResultsReady && (sastThresholdExceeded || sastNewResultsExceeded)> +
  • Exceeded CxSAST Vulnerability Threshold
  • + <#elseif config.isOsaEnabled() && dependencyResult.resultReady && dependencyThresholdExceeded> +
  • Exceeded CxOSA Vulnerability Threshold
  • + <#elseif config.isAstScaEnabled() && dependencyResult.resultReady && dependencyThresholdExceeded> +
  • Exceeded CxSCA Vulnerability Threshold
  • + <#else> +
  • CxScan Failed
  • + +
+
+
+ <#else> +
+
+
+ + + OK + Created with Sketch. + + + + + + + + + + + + + + + + + + +
+
+
+

+ Checkmarx Scan Passed +

+
+
+ +
@@ -1002,8 +1018,8 @@
- <#if config.sastEnabled> -
+ <#if config.isSastEnabled()> +
CxSAST Vulnerabilities Status
<#if sast.sastResultsReady> @@ -1013,7 +1029,7 @@
    + + + <#if config.cxVersion.version?has_content> + <#assign versionComponents = config.cxVersion.version?split(".")> + <#assign currentVersion = versionComponents[0] + "." + versionComponents[1]> + <#assign currentVersionFloat = currentVersion?number> + <#if (currentVersionFloat?exists) && (currentVersionFloat >= 9.7)> +
  • + +
    + <#if config.sastThresholdsEnabled && config.sastCriticalThreshold??> + <@thresholdTooltip threshold=config.sastCriticalThreshold count=sast.critical/> + +
    +
    +
    +
    + +
    +
    +
    + + + Med + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    Critical -
    +
    ${sast.critical}
    +
    + <#if sast.hasNewResults()> +
    ${sast.newCritical} + New +
    + +
    +
  • + + + +
  • @@ -1293,295 +1388,729 @@
+
+ + + <#if config.isOsaEnabled()|| config.isAstScaEnabled() > +
+
+ <#if dependencyResult.scannerType=="AST_SCA"> +
CxSCA Vulnerabilities & Libraries
+ <#else> +
Cx${sast.encodeXSS(dependencyResult.scannerType)} Vulnerabilities & Libraries
+ + <#if dependencyResult.resultReady> + + <#else> +
+
+
+ + + */ + scan_failed + Created with Sketch. + + + + + + + + + + + + + + + + + +
+
OSA scan failed
+
+
+ +
+ <#if dependencyResult.resultReady> +
+ +
+
Libraries:
+ +
+
+
${dependencyResult.vulnerableAndOutdated}
+
+
+ Vulnerable and Outdated Libraries +
+
+ +
+
+
${dependencyResult.nonVulnerableLibraries}
+
+
+ No Known Vulnerability Libraries +
+
+
+ +
+
    + + <#if sca.scaResultReady> +
  • + +
    + <#if config.osaThresholdsEnabled && config.osaCriticalThreshold??> + <@thresholdTooltip threshold=config.osaCriticalThreshold count=dependencyResult.criticalVulnerability!0/> - <#if config.osaEnabled> -
    -
    -
    CxOSA Vulnerabilities & Libraries
    - <#if osa.osaResultsReady> -
  • + + + +
  • + +
    + <#if config.osaThresholdsEnabled && config.osaHighThreshold??> + <@thresholdTooltip threshold=config.osaHighThreshold count=dependencyResult.highVulnerability/> + +
    +
    + +
    +
    +
    + + + Med + Created with Sketch. + + + + + + + + + + + + + + + + + + + + - - + +
    +
    High -
    +
    ${dependencyResult.highVulnerability}
    -
    OSA scan failed
    -
- -
- <#if osa.osaResultsReady> -
- -
-
Libraries:
- -
-
-
${osa.results.vulnerableAndOutdated}
-
-
- Vulnerable and Outdated Libraries + + + +
  • + +
    + <#if config.osaThresholdsEnabled && config.osaMediumThreshold??> + <@thresholdTooltip threshold=config.osaMediumThreshold count=dependencyResult.mediumVulnerability/> + +
    +
    +
    +
    +
    + + Med + + + + + + + + + + + + + + + + + + +
    +
    Medium -
    +
    ${dependencyResult.mediumVulnerability}
    - -
    -
    -
    ${osa.results.nonVulnerableLibraries}
    -
    -
    - No Known Vulnerability Libraries +
  • + + +
  • + +
    + <#if config.osaThresholdsEnabled && config.osaLowThreshold??> + <@thresholdTooltip threshold=config.osaLowThreshold count=dependencyResult.lowVulnerability/> + +
    +
    +
    +
    +
    + + Low + + + + + + + + + + + + + + + + + + + +
    +
    Low -
    +
    ${dependencyResult.lowVulnerability}
    - -
    -
    -
    ${osa.osaViolations?size }
    -
    -
    - Policy Violated Libraries +
  • + +
    +
    + +
    + +
    +
    + + <#--<#if config.dependencyScannerType == "SCA"> +
    +
    +
    SCA Vulnerabilities & Libraries
    + <#if sca.scaResultReady> + + <#else> +
    +
    +
    + + -
    -
      - -
    • +
    + - Med - Created with Sketch. - - - - - - - - - - - - - - - - - - - - +
    +
    + + -
  • + -
  • - + + + <#if config.isSastEnabled() && config.generateXmlReport &&sast.sastResultsReady> + + <#if sast.critical gt 0 || sast.high gt 0 || sast.medium gt 0 || sast.low gt 0> - <#if config.sastEnabled && sast.sastResultsReady> - <#if sast.high gt 0 || sast.medium gt 0 || sast.low gt 0>
    @@ -1661,7 +2190,7 @@ Start:
    ${sast.scanStartTime}
    + id="sast-full-start-date">${sast.encodeXSS(sast.scanStartTime)}
  • @@ -1709,7 +2238,7 @@
    End:
    -
    ${sast.scanEndTime}
    +
    ${sast.encodeXSS(sast.scanEndTime)}
    @@ -1755,7 +2284,7 @@
    Files:
    -
    ${sast.filesScanned}
    +
    ${sast.encodeXSS(sast.filesScanned)}
    @@ -1795,10 +2324,56 @@
    Code Lines:
    -
    ${sast.LOC}
    +
    ${sast.encodeXSS(sast.LOC)}
    + + <#if sast.critical gt 0> +
    +
    +
    + + Critical + + + + + + + + + + + + + + + + + + +
    +
    Critical
    +
    ${sast.critical}
    +
    + + + + + + <#list sast.queryList as query> + <#if query.severity == sast.languageMap["Critical"]> + + + + + + +
    VulnerabilityIssues Found
    ${sast.encodeXSS(query.name)}${query.result?size}
    +
    + + <#if sast.high gt 0>
    @@ -1833,17 +2408,17 @@
    High
    -
    ${sast.high}
    +
    ${sast.high}
    - + <#list sast.queryList as query> - <#if query.severity == "High"> + <#if query.severity == sast.languageMap["High"]> - + @@ -1891,12 +2466,12 @@ class="cve-table sast-cve-table sast-cve-table-medium"> - + <#list sast.queryList as query> - <#if query.severity == "Medium"> + <#if query.severity == sast.languageMap["Medium"]> - + @@ -1945,12 +2520,12 @@
    Vulnerability##Issues Found
    ${query.name}${sast.encodeXSS(query.name)} ${query.result?size}
    Vulnerability##Issues Found
    ${query.name}${sast.encodeXSS(query.name)} ${query.result?size}
    - + <#list sast.queryList as query> - <#if query.severity == "Low"> + <#if query.severity == sast.languageMap["Low"]> - + @@ -1958,85 +2533,30 @@
    Vulnerability##Issues Found
    ${query.name}${sast.encodeXSS(query.name)} ${query.result?size}
    - - <#if sast.sastViolations?size gt 0> -
    -
    -
    - - - Policy violation - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    Policy Violations
    -
    ${sast.sastViolations?size}
    -
    - -
    - + - <#if config.osaEnabled && osa.osaResultsReady> - <#if osa.osaHighCVEReportTable?size gt 0 || osa.osaMediumCVEReportTable?size gt 0 || osa.osaLowCVEReportTable?size gt 0 || osa.osaViolations?size gt 0> + <#if ( config.isOsaEnabled() || config.isAstScaEnabled()) && dependencyResult.resultReady> + <#if dependencyResult.dependencyCriticalCVEReportTable?size gt 0 ||dependencyResult.dependencyHighCVEReportTable?size gt 0 || dependencyResult.dependencyMediumCVEReportTable?size gt 0 || dependencyResult.dependencyLowCVEReportTable?size gt 0> - <#if osa.osaHighCVEReportTable?size gt 0> + + <#if sca.scaResultReady> + <#if dependencyResult.dependencyCriticalCVEReportTable?size gt 0> +
    +
    +
    + Critical + + + + + + + + + + + + + + + + + + +
    +
    Critical
    +
    ${dependencyResult.criticalVulnerability}
    +
    + + + + + + + <#list dependencyResult.dependencyCriticalCVEReportTable as cve> + <#if cve.state =="NOT_EXPLOITABLE"> + + <#else> + + + + + + + +
    VulnerabilityPublish DateLibrary
    ${sast.encodeXSS(cve.name)}${sast.encodeXSS(cve.publishDate)}${sast.encodeXSS(cve.libraryName)}
    +
    + + + + <#if dependencyResult.dependencyHighCVEReportTable?size gt 0>
    @@ -2221,7 +2801,7 @@
    High
    -
    ${osa.results.totalHighVulnerabilities}
    +
    ${dependencyResult.highVulnerability}
    @@ -2229,22 +2809,22 @@ - <#list osa.osaHighCVEReportTable as cve> + <#list dependencyResult.dependencyHighCVEReportTable as cve> <#if cve.state =="NOT_EXPLOITABLE"> <#else> - - - + + +
    Publish Date Library
    ${cve.name}${cve.publishDate}${cve.libraryName}${sast.encodeXSS(cve.name)}${sast.encodeXSS(cve.publishDate)}${sast.encodeXSS(cve.libraryName)}
    - <#if osa.osaMediumCVEReportTable?size gt 0> + <#if dependencyResult.dependencyMediumCVEReportTable?size gt 0>
    @@ -2277,7 +2857,7 @@
    Medium
    -
    ${osa.results.totalMediumVulnerabilities}
    +
    ${dependencyResult.mediumVulnerability}
    @@ -2285,22 +2865,22 @@ - <#list osa.osaMediumCVEReportTable as cve> + <#list dependencyResult.dependencyMediumCVEReportTable as cve> <#if cve.state =="NOT_EXPLOITABLE"> <#else> - - - + + +
    Publish Date Library
    ${cve.name}${cve.publishDate}${cve.libraryName}${sast.encodeXSS(cve.name)}${sast.encodeXSS(cve.publishDate)}${sast.encodeXSS(cve.libraryName)}
    - <#if osa.osaLowCVEReportTable?size gt 0> + <#if dependencyResult.dependencyLowCVEReportTable?size gt 0>
    @@ -2335,7 +2915,7 @@
    Low
    -
    ${osa.results.totalLowVulnerabilities}
    +
    ${dependencyResult.lowVulnerability}
    @@ -2343,87 +2923,176 @@ - <#list osa.osaLowCVEReportTable as cve> + <#list dependencyResult.dependencyLowCVEReportTable as cve> <#if cve.state =="NOT_EXPLOITABLE"> <#else> - - - + + +
    Publish Date Library
    ${cve.name}${cve.publishDate}${cve.libraryName}${sast.encodeXSS(cve.name)}${sast.encodeXSS(cve.publishDate)}${sast.encodeXSS(cve.libraryName)}
    + + + + + - <#if osa.osaViolations?size gt 0> -
    -
    -
    - - - Policy violation - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - + <#if (config.isOsaEnabled() || config.isAstScaEnabled()|| config.isSastEnabled()) && policyViolated> + + <#if policyViolatedCount gt 0> + +
    + + + + +
    diff --git a/src/main/resources/com/cx/restclient/sast/dto/jaxb.index b/src/main/resources/com/cx/restclient/sast/dto/jaxb.index new file mode 100644 index 00000000..7436e11f --- /dev/null +++ b/src/main/resources/com/cx/restclient/sast/dto/jaxb.index @@ -0,0 +1 @@ +CxXMLResults \ No newline at end of file diff --git a/src/main/resources/com/cx/restclient/sast/dto/jaxb.properties b/src/main/resources/com/cx/restclient/sast/dto/jaxb.properties new file mode 100644 index 00000000..47e77158 --- /dev/null +++ b/src/main/resources/com/cx/restclient/sast/dto/jaxb.properties @@ -0,0 +1,2 @@ +javax.xml.bind.JAXBContextFactory=com.sun.xml.bind.v2.JAXBContextFactory +javax.xml.bind.context.factory=com.sun.xml.bind.v2.JAXBContextFactory \ No newline at end of file diff --git a/src/main/resources/common.properties b/src/main/resources/common.properties new file mode 100644 index 00000000..b1bcc688 --- /dev/null +++ b/src/main/resources/common.properties @@ -0,0 +1,25 @@ +version = ${project.version} + +ast.getScan = /api/scans/%s +ast.createScan = /api/scans +ast.getUploadUrl = /api/uploads + +astSast.webProject = /#/projects/%s/overview +astSast.authentication = /auth/realms/organization/protocol/openid-connect/token +astSast.scanResults = /api/results +astSast.scanSummary = /api/scan-summary +astSast.descriptionPath = /api/queries/descriptions + +astSca.riskManagementApi = /risk-management/ +astSca.projects = projects +astSca.projectId = /id +astSca.summaryReport = riskReports/%s/summary +astSca.findings = riskReports/%s/vulnerabilities +astSca.packages = riskReports/%s/packages +astSca.latestScan = riskReports?size=1&projectId=%s +astSca.webReport = /#/projects/%s/reports/%s +astSca.resolvingConfigurationApi = /settings/projects/%s/resolving-configuration +astSca.policyManagementApi= /policy-management/ +astSca.policyManagementEvaliation= policy-evaluation?reportId=%s +astSca.reportId = scans/%s/riskReportId +astSca.teamById = teams \ No newline at end of file diff --git a/src/test/java/com/cx/restclient/CxSASTClientTest.java b/src/test/java/com/cx/restclient/CxSASTClientTest.java new file mode 100644 index 00000000..ffb533c3 --- /dev/null +++ b/src/test/java/com/cx/restclient/CxSASTClientTest.java @@ -0,0 +1,202 @@ +package com.cx.restclient; + +import java.net.MalformedURLException; +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.dto.CxVersion; +import junit.framework.TestCase; + +public class CxSASTClientTest extends TestCase { + public void testGetContentTypeAndApiVersion_SastRetentionRate_Sast96() throws MalformedURLException { + CxScanConfig config = new CxScanConfig(); + CxVersion cxVersion = new CxVersion(); + cxVersion.setVersion("9.6"); + config.setCxVersion(cxVersion); + String apiName = "projects/{id}/dataRetentionSettings"; + config.setEnableDataRetention(true); + + CxSASTClient cxSASTClient = new CxSASTClient(config, null); + String apiVersion = cxSASTClient.getContentTypeAndApiVersion(config, apiName); + assertEquals("Expected API version for valid SAST version and SAST_RETENTION_RATE", "application/json;v=1.1", + apiVersion); + } + + public void testGetContentTypeAndApiVersion_ScanWithSettings_Sast96() throws MalformedURLException { + CxScanConfig config = new CxScanConfig(); + CxVersion cxVersion = new CxVersion(); + cxVersion.setVersion("9.6"); + config.setCxVersion(cxVersion); + String apiName = "sast/scanWithSettings"; + String customFields = "{\"custom1\":\"value1\"}"; + config.setCustomFields(customFields); + config.setPostScanActionId(1); + CxSASTClient cxSASTClient = new CxSASTClient(config, null); + String apiVersion = cxSASTClient.getContentTypeAndApiVersion(config, apiName); + assertEquals( + "Expected API version for valid SAST version and scanWithSettings with custom fields and PostScanActionId", + "application/json;v=1.2", apiVersion); + } + + public void testGetContentTypeAndApiVersion_ScanWithSettings_CustomFields_Sast96() + throws MalformedURLException { + CxScanConfig config = new CxScanConfig(); + CxVersion cxVersion = new CxVersion(); + cxVersion.setVersion("9.6"); + config.setCxVersion(cxVersion); + String apiName = "sast/scanWithSettings"; + String customFields = "{\"custom1\":\"value1\"}"; + config.setCustomFields(customFields); + config.setPostScanActionId(null); + CxSASTClient cxSASTClient = new CxSASTClient(config, null); + String apiVersion = cxSASTClient.getContentTypeAndApiVersion(config, apiName); + assertEquals("Expected API version for valid SAST version and scanWithSettings with custom fields only", + "application/json;v=1.2", apiVersion); + } + + public void testGetContentTypeAndApiVersion_ScanWithSettings_PostScanActionId_Sast96() + throws MalformedURLException { + CxScanConfig config = new CxScanConfig(); + CxVersion cxVersion = new CxVersion(); + cxVersion.setVersion("9.6"); + config.setCxVersion(cxVersion); + String apiName = "sast/scanWithSettings"; + config.setCustomFields(null); + config.setPostScanActionId(1); + CxSASTClient cxSASTClient = new CxSASTClient(config, null); + String apiVersion = cxSASTClient.getContentTypeAndApiVersion(config, apiName); + assertEquals("Expected API version for valid SAST version and scanWithSettings with PostScanActionId only", + "application/json;v=1.2", apiVersion); + } + + public void testGetContentTypeAndApiVersion_SastRetentionRate_Sast93() + throws MalformedURLException { + CxScanConfig config = new CxScanConfig(); + CxVersion cxVersion = new CxVersion(); + cxVersion.setVersion("9.3.5"); + config.setCxVersion(cxVersion); + String apiName = "projects/{id}/dataRetentionSettings"; + CxSASTClient cxSASTClient = new CxSASTClient(config, null); + String apiVersion = cxSASTClient.getContentTypeAndApiVersion(config, apiName); + assertEquals("Expected API version for SAST version in the 9.2 to 9.3 range and SAST_RETENTION_RATE", + "application/json;v=1.0", apiVersion); + } + + public void testGetContentTypeAndApiVersion_NullSastVersion() throws MalformedURLException { + CxScanConfig config = new CxScanConfig(); + CxVersion cxVersion = new CxVersion(); + cxVersion.setVersion(null); + config.setCxVersion(cxVersion); + String apiName = "projects/{id}/dataRetentionSettings"; + CxSASTClient cxSASTClient = new CxSASTClient(config, null); + String apiVersion = cxSASTClient.getContentTypeAndApiVersion(config, apiName); + assertEquals("Expected API version when SAST version is null", "application/json;v=1.0", apiVersion); + } + + public void testGetContentTypeAndApiVersion_InvalidSastVersion() throws MalformedURLException { + CxScanConfig config = new CxScanConfig(); + CxVersion cxVersion = new CxVersion(); + cxVersion.setVersion("9.1.5"); + config.setCxVersion(cxVersion); + String apiName = "projects/{id}/dataRetentionSettings"; + CxSASTClient cxSASTClient = new CxSASTClient(config, null); + String apiVersion = cxSASTClient.getContentTypeAndApiVersion(config, apiName); + assertEquals("Expected default API version for an invalid SAST version", "application/json;v=1.0", apiVersion); + } + + public void testGetContentTypeAndApiVersion_UnknownApiName() throws MalformedURLException { + CxScanConfig config = new CxScanConfig(); + CxVersion cxVersion = new CxVersion(); + cxVersion.setVersion("9.4.1"); + config.setCxVersion(cxVersion); + String apiName = "createScanReport"; + CxSASTClient cxSASTClient = new CxSASTClient(config, null); + String apiVersion = cxSASTClient.getContentTypeAndApiVersion(config, apiName); + assertEquals("Expected default API version for an unknown API name", "application/json;v=1.0", apiVersion); + } + + public void testGetContentTypeAndApiVersion__SastRetentionRate_Sast94() throws MalformedURLException { + CxScanConfig config = new CxScanConfig(); + CxVersion cxVersion = new CxVersion(); + cxVersion.setVersion("9.4"); + config.setCxVersion(cxVersion); + String apiName = "projects/{id}/dataRetentionSettings"; + CxSASTClient cxSASTClient = new CxSASTClient(config, null); + String apiVersion = cxSASTClient.getContentTypeAndApiVersion(config, apiName); + assertEquals("Expected API version for SAST version 9.4", "application/json;v=1.0", apiVersion); + } + + public void testGetContentTypeAndApiVersion_SastRetentionRate_Sast95() throws MalformedURLException { + CxScanConfig config = new CxScanConfig(); + CxVersion cxVersion = new CxVersion(); + cxVersion.setVersion("9.5"); + config.setCxVersion(cxVersion); + String apiName = "projects/{id}/dataRetentionSettings"; + CxSASTClient cxSASTClient = new CxSASTClient(config, null); + String apiVersion = cxSASTClient.getContentTypeAndApiVersion(config, apiName); + assertEquals("Expected API version for SAST version 9.5", "application/json;v=1.0", apiVersion); + } + + public void testGetContentTypeAndApiVersion_ScanWithSettings_Sast94() throws MalformedURLException { + CxScanConfig config = new CxScanConfig(); + CxVersion cxVersion = new CxVersion(); + cxVersion.setVersion("9.4"); + config.setCxVersion(cxVersion); + String apiName = "sast/scanWithSettings"; + CxSASTClient cxSASTClient = new CxSASTClient(config, null); + String apiVersion = cxSASTClient.getContentTypeAndApiVersion(config, apiName); + assertEquals("Expected API version for SAST version 9.4", "application/json;v=1.0", apiVersion); + } + + public void testGetContentTypeAndApiVersion_ScanWithSettings_Sast95() throws MalformedURLException { + CxScanConfig config = new CxScanConfig(); + CxVersion cxVersion = new CxVersion(); + cxVersion.setVersion("9.5"); + config.setCxVersion(cxVersion); + String apiName = "sast/scanWithSettings"; + CxSASTClient cxSASTClient = new CxSASTClient(config, null); + String apiVersion = cxSASTClient.getContentTypeAndApiVersion(config, apiName); + assertEquals("Expected API version for SAST version 9.5", "application/json;v=1.0", apiVersion); + } + + public void testGetContentTypeAndApiVersion_DataRetentionEnabled_Sast94() + throws MalformedURLException { + CxScanConfig config = new CxScanConfig(); + CxVersion cxVersion = new CxVersion(); + cxVersion.setVersion("9.4"); + config.setCxVersion(cxVersion); + String apiName = "projects/{id}/dataRetentionSettings"; + config.setEnableDataRetention(true); + CxSASTClient cxSASTClient = new CxSASTClient(config, null); + String apiVersion = cxSASTClient.getContentTypeAndApiVersion(config, apiName); + assertEquals("Expected API version for SAST version 9.4 with data retention enabled", "application/json;v=1.1", + apiVersion); + } + + public void testGetContentTypeAndApiVersion_ScanWithSettings_CustomFields_Sast94() + throws MalformedURLException { + CxScanConfig config = new CxScanConfig(); + CxVersion cxVersion = new CxVersion(); + cxVersion.setVersion("9.4"); + config.setCxVersion(cxVersion); + String apiName = "sast/scanWithSettings"; + String customFields = "{\"custom1\":\"value1\"}"; + config.setCustomFields(customFields); + CxSASTClient cxSASTClient = new CxSASTClient(config, null); + String apiVersion = cxSASTClient.getContentTypeAndApiVersion(config, apiName); + assertEquals("Expected API version for SAST version 9.4 with scanWithSettings and custom fields", + "application/json;v=1.2", apiVersion); + } + + public void testGetContentTypeAndApiVersion_ScanWithSettings_PostScanActionId_Sast94() + throws MalformedURLException { + CxScanConfig config = new CxScanConfig(); + CxVersion cxVersion = new CxVersion(); + cxVersion.setVersion("9.4"); + config.setCxVersion(cxVersion); + String apiName = "sast/scanWithSettings"; + config.setPostScanActionId(1); + CxSASTClient cxSASTClient = new CxSASTClient(config, null); + String apiVersion = cxSASTClient.getContentTypeAndApiVersion(config, apiName); + assertEquals("Expected API version for SAST version 9.4 with scanWithSettings and PostScanActionId", + "application/json;v=1.2", apiVersion); + } +} \ No newline at end of file diff --git a/src/test/java/com/cx/restclient/ast/ClientTypeResolverTest.java b/src/test/java/com/cx/restclient/ast/ClientTypeResolverTest.java new file mode 100644 index 00000000..3969fbab --- /dev/null +++ b/src/test/java/com/cx/restclient/ast/ClientTypeResolverTest.java @@ -0,0 +1,57 @@ +package com.cx.restclient.ast; + +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.exception.CxClientException; +import com.cx.restclient.general.CommonClientTest; +import com.cx.restclient.osa.dto.ClientType; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + + +@Slf4j +public class ClientTypeResolverTest extends CommonClientTest { + @Test + public void determineClientType_cloudAccessControl() { + testDetermineClientType("astSca.cloud.accessControlUrl"); + } + + //TODO : fix this test + @Test + @Ignore("this test fails and needs to be fixed") + public void determineClientType_onPremAccessControl() { + testDetermineClientType("astSca.onPremise.accessControlUrl"); + } + + @Test + public void determineClientType_invalidServer() { + checkThatExceptionIsThrown("https://example.com"); + } + + @Test + public void determineClientType_invalidUrlFormat() { + checkThatExceptionIsThrown("incorrect!url?format"); + } + + private static void checkThatExceptionIsThrown(String url) { + ClientTypeResolver resolver = new ClientTypeResolver(new CxScanConfig()); + try { + resolver.determineClientType(url); + Assert.fail("Expected exception, but didn't get any."); + } catch (Exception e) { + log.info("Got an exception", e); + Assert.assertTrue("Unexpected exception type.", e instanceof CxClientException); + Assert.assertTrue("Exception message is empty.", StringUtils.isNotEmpty(e.getMessage())); + } + } + + private void testDetermineClientType(String urlPropName) { + ClientTypeResolver resolver = new ClientTypeResolver(new CxScanConfig()); + ClientType clientType = resolver.determineClientType(prop(urlPropName)); + Assert.assertNotNull("Client type is null.", clientType); + Assert.assertTrue("Client ID is empty.", StringUtils.isNotEmpty(clientType.getClientId())); + Assert.assertTrue("Scopes are empty.", StringUtils.isNotEmpty(clientType.getScopes())); + } +} \ No newline at end of file diff --git a/src/test/java/com/cx/restclient/configuration/CxScanConfigTest.java b/src/test/java/com/cx/restclient/configuration/CxScanConfigTest.java new file mode 100644 index 00000000..a2aa6394 --- /dev/null +++ b/src/test/java/com/cx/restclient/configuration/CxScanConfigTest.java @@ -0,0 +1,84 @@ +package com.cx.restclient.configuration; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.junit.Assert.*; + +public class CxScanConfigTest { + private Logger logUnitTests = LoggerFactory.getLogger("CxCommonClient Unit tests "); + private CxScanConfig cxScanConfig = new CxScanConfig(); + + String url; + String username; + String password; + String cxOrigin; + boolean disableCertificateValidation = false; + private CxScanConfig cxScanConfigWithParameters = new CxScanConfig(url, username, password, cxOrigin, disableCertificateValidation); + + @Test + public void CxScanConfig() { + String url = "http://XX.XX.XX.XX"; + String username = "userName"; + String password = "password"; + String cxOrigin = "cxOrigin"; + boolean disableCertificateValidation = false; + CxScanConfig cxScanConfigWithParameters = new CxScanConfig(url, username, password, cxOrigin,"", disableCertificateValidation); + + assertEquals("Incorrect URL", url, cxScanConfigWithParameters.getUrl()); + assertEquals("Incorrect userName", username, cxScanConfigWithParameters.getUsername()); + assertEquals("Incorrect password", password, cxScanConfigWithParameters.getPassword()); + assertEquals("Incorrect CxOrigin", cxOrigin, cxScanConfigWithParameters.getCxOrigin()); + assertEquals("Incorrect disableCertificateValidation", disableCertificateValidation, cxScanConfigWithParameters.isDisableCertificateValidation()); + } + + + + @Test + public void getSetSastEnabled() { + + logUnitTests.info("Current test validate that we get correct values from getSastEnabled method\n"); + + logUnitTests.info("1 test --> Set 'setSastEnabled' to value 'false' and validate that get 'false' value\n"); + cxScanConfig.setSastEnabled(false); + assertEquals("I expected to get 'false' value but got different value", cxScanConfig.isSastEnabled(), false); + + + logUnitTests.info("2 test --> Set 'setSastEnabled' to value 'true' and validate that get 'true' value\n"); + cxScanConfig.setSastEnabled(true); + assertEquals("I expected to get 'true' value but got different value", cxScanConfig.isSastEnabled(), true); + + + logUnitTests.info("3 test --> Negative test - Set 'setSastEnabled' to value 'true' and validate that get 'false' value\n"); + cxScanConfig.setSastEnabled(true); + assertEquals("Negative test - expected to see different values", !(cxScanConfig.isSastEnabled()), false); + + + logUnitTests.info("4 test --> Negative test - Set 'setSastEnabled' to value 'false' and validate that get 'true' value\n"); + cxScanConfig.setSastEnabled(false); + assertEquals("Negative test - expected to see different values", !(cxScanConfig.isSastEnabled()), true); + + } + + @Test + public void getSetCxOrigin() { + + logUnitTests.info("Current test validate that we get correct values from getCxOrigin method\n"); + + logUnitTests.info("1 test --> Set 'setCxOrigin' to value ANY String value and validate that getCxOrigin method will return correct value\n"); + String cxOriginValue = "SetCxOriginValueUnitTest"; + cxScanConfig.setCxOrigin(cxOriginValue); + assertEquals("I expected to get" + cxOriginValue + " value but got different value", cxScanConfig.getCxOrigin(), cxOriginValue); + + //TODO current test failed because we allow to set empty string + logUnitTests.info("2 test --> Set 'setCxOrigin' to value to EMPTY String and validate that getCxOrigin method will return correct value\n"); + cxScanConfig.setCxOrigin(" "); + assertEquals("I expected to get ' ' value but got different value", cxScanConfig.getCxOrigin().trim().isEmpty(), true); + + logUnitTests.info("3 test --> Set 'setCxOrigin' to value to NOT STRING and validate that getCxOrigin method throw error\n"); + cxScanConfig.setCxOrigin(null); + assertNull("Did not Get NULL value", cxScanConfig.getCxOrigin()); + + } +} \ No newline at end of file diff --git a/src/test/java/com/cx/restclient/general/AstSastTest.java b/src/test/java/com/cx/restclient/general/AstSastTest.java new file mode 100644 index 00000000..99af1ce5 --- /dev/null +++ b/src/test/java/com/cx/restclient/general/AstSastTest.java @@ -0,0 +1,178 @@ +package com.cx.restclient.general; + +import com.cx.restclient.CxClientDelegator; +import com.cx.restclient.ast.dto.common.RemoteRepositoryInfo; +import com.cx.restclient.ast.dto.sast.AstSastConfig; +import com.cx.restclient.ast.dto.sast.AstSastResults; +import com.cx.restclient.ast.dto.sast.report.AstSastSummaryResults; +import com.cx.restclient.ast.dto.sast.report.Finding; +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.dto.ScanResults; +import com.cx.restclient.dto.ScannerType; +import com.cx.restclient.dto.SourceLocationType; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.*; + +@Slf4j +public class AstSastTest extends CommonClientTest { + private static final ObjectMapper objectMapper = new ObjectMapper(); + + //TODO : Fix this test + @Test + @Ignore("this test fails and needs to be fixed") + public void scan_remotePublicRepo() throws MalformedURLException { + CxScanConfig config = getScanConfig(); + + CxClientDelegator client = new CxClientDelegator(config, log); + try { + client.init(); + ScanResults initialResults = client.initiateScan(); + validateInitialResults(initialResults); + + ScanResults finalResults = client.waitForScanResults(); + validateFinalResults(finalResults); + } catch (Exception e) { + failOnException(e); + } + } + + private void validateFinalResults(ScanResults finalResults) { + Assert.assertNotNull("Final scan results are null.", finalResults); + + AstSastResults astSastResults = finalResults.getAstResults(); + Assert.assertNotNull("AST-SAST results are null.", astSastResults); + Assert.assertTrue("Scan ID is missing.", StringUtils.isNotEmpty(astSastResults.getScanId())); + Assert.assertTrue("Web report link is missing.", StringUtils.isNotEmpty(astSastResults.getWebReportLink())); + + validateFindings(astSastResults); + validateSummary(astSastResults); + } + + private void validateSummary(AstSastResults astSastResults) { + AstSastSummaryResults summary = astSastResults.getSummary(); + Assert.assertNotNull("Summary is null.", summary); + Assert.assertTrue("No medium-severity vulnerabilities.", + summary.getMediumVulnerabilityCount() > 0); + + Assert.assertNotNull("Status counter list is null.", summary.getStatusCounters()); + Assert.assertFalse("No status counters.", summary.getStatusCounters().isEmpty()); + + Assert.assertTrue("Expected total counter to be a positive value.", summary.getTotalCounter() > 0); + + int actualFindingCount = astSastResults.getFindings().size(); + Assert.assertEquals("Total finding count from summary doesn't correspond to the actual count.", + actualFindingCount, + summary.getTotalCounter()); + + long actualFindingCountExceptInfo = astSastResults.getFindings() + .stream() + .filter(finding -> !StringUtils.equalsIgnoreCase(finding.getSeverity(), "info")) + .count(); + + int countFromSummaryExceptInfo = summary.getHighVulnerabilityCount() + + summary.getMediumVulnerabilityCount() + + summary.getLowVulnerabilityCount(); + + Assert.assertEquals("Finding count from summary (excluding 'info') doesn't correspond to the actual count.", + actualFindingCountExceptInfo, + countFromSummaryExceptInfo); + } + + private void validateFindings(AstSastResults astSastResults) { + List findings = astSastResults.getFindings(); + Assert.assertNotNull("Finding list is null.", findings); + Assert.assertFalse("Finding list is empty.", findings.isEmpty()); + + boolean someNodeListsAreEmpty = findings.stream().anyMatch(finding -> finding.getNodes().isEmpty()); + Assert.assertFalse("Some of the finding node lists are empty.", someNodeListsAreEmpty); + + + log.info("Validating each finding."); + + findings.forEach(this::validateFinding); + + validateDescriptions(findings); + } + + private void validateDescriptions(List findings) { + + Map> mapDescriptions = new HashMap<>(); + + findings.forEach(finding -> { + Set listDescriptions = mapDescriptions.get(finding.getQueryID()); + if(listDescriptions == null){ + listDescriptions = new HashSet<>(); + } + listDescriptions.add(finding.getDescription()); + mapDescriptions.put(finding.getQueryID(), listDescriptions); + }); + + Set uniqueDescriptions = new HashSet<>(); + + //validate for each queryId there is exactly one corresponding description + for( Map.Entry> entry :mapDescriptions.entrySet()){ + Assert.assertEquals( 1, entry.getValue().size()); + uniqueDescriptions.add((String)entry.getValue().toArray()[0]); + } + + //validate all descriptions are unique + Assert.assertEquals(uniqueDescriptions.size(),mapDescriptions.size() ); + } + + private void validateFinding(Finding finding) { + logFinding(finding); + Assert.assertTrue("State is missing.", StringUtils.isNotEmpty(finding.getState())); + Assert.assertTrue("Status is missing.", StringUtils.isNotEmpty(finding.getStatus())); + Assert.assertTrue("Severity is missing.", StringUtils.isNotEmpty(finding.getSeverity())); + Assert.assertTrue("Query name is missing.", StringUtils.isNotEmpty(finding.getQueryName())); + Assert.assertTrue("Description is missing. ", StringUtils.isNotEmpty(finding.getDescription())); + } + + private void logFinding(Finding finding) { + try { + log.info("Validating finding: {}", objectMapper.writeValueAsString(finding)); + } catch (JsonProcessingException e) { + Assert.fail("Error serializing finding to JSON."); + } + } + + + private void validateInitialResults(ScanResults initialResults) { + Assert.assertNotNull("Initial scan results are null.", initialResults); + Assert.assertNotNull("AST-SAST results are null.", initialResults.getAstResults()); + Assert.assertTrue("Scan ID is missing.", StringUtils.isNotEmpty(initialResults.getAstResults().getScanId())); + } + + private CxScanConfig getScanConfig() throws MalformedURLException { + AstSastConfig astConfig = AstSastConfig.builder() + .apiUrl(prop("astSast.apiUrl")) + .webAppUrl(prop("astSast.webAppUrl")) + .clientSecret(prop("astSast.clientSecret")) + .clientId("CxFlow") + .sourceLocationType(SourceLocationType.REMOTE_REPOSITORY) + .build(); + + RemoteRepositoryInfo repoInfo = new RemoteRepositoryInfo(); + URL repoUrl = new URL(prop("astSast.remoteRepoUrl.public")); + repoInfo.setUrl(repoUrl); + astConfig.setRemoteRepositoryInfo(repoInfo); + astConfig.setResultsPageSize(10); + astConfig.setPresetName("Checkmarx Default"); + + CxScanConfig config = new CxScanConfig(); + config.setAstSastConfig(astConfig); + config.setProjectName(prop("astSast.projectName")); + config.addScannerType(ScannerType.AST_SAST); + config.setOsaProgressInterval(5); + return config; + } +} diff --git a/src/test/java/com/cx/restclient/general/AstScaTests.java b/src/test/java/com/cx/restclient/general/AstScaTests.java new file mode 100644 index 00000000..7eab933b --- /dev/null +++ b/src/test/java/com/cx/restclient/general/AstScaTests.java @@ -0,0 +1,234 @@ +package com.cx.restclient.general; + +import com.cx.restclient.CxClientDelegator; +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.dto.ScanResults; +import com.cx.restclient.dto.SourceLocationType; +import com.cx.restclient.exception.CxClientException; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.compress.archivers.*; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.junit.*; + +import java.io.*; +import java.net.MalformedURLException; +import java.nio.file.*; +import java.util.UUID; + +import static org.junit.Assert.fail; + +@Slf4j +public class AstScaTests extends ScaTestsBase { + @Test + public void scan_localDirUpload() throws IOException, CxClientException { + CxScanConfig config = initScaConfig(false); + config.setOsaThresholdsEnabled(true); + config.getAstScaConfig().setSourceLocationType(SourceLocationType.LOCAL_DIRECTORY); + + Path sourcesDir = null; + try { + sourcesDir = extractTestProjectFromResources(); + config.setSourceDir(sourcesDir.toString()); + + ScanResults scanResults = runScan(config); + verifyScanResults(scanResults); + } finally { + deleteDir(sourcesDir); + } + } + + @Test + public void scan_remotePublicRepo() throws MalformedURLException { + scanRemoteRepo(PUBLIC_REPO_PROP, false); + } + + @Test + public void scan_remotePrivateRepo() throws MalformedURLException { + scanRemoteRepo(PRIVATE_REPO_PROP, false); + } + + @Test + public void getLatestScanResults_existingResults() { + CxScanConfig config = initScaConfig(false); + ScanResults latestResults = getLatestResults(config); + verifyScanResults(latestResults); + } + + /** + * Getting latest results for a project that doesn't exist. + */ + @Test + public void getLatestScanResults_nonexistentProject() { + testMissingResultsCase("nonexistent-project-name"); + } + + /** + * Existing project without any scans. + */ + @Test + public void getLatestScanResults_projectWithoutScans() { + testMissingResultsCase("common-client-test-02-no-scans"); + } + + /** + * Project with all scans failed (e.g. invalid git repo). + */ + @Test + public void getLatestScanResults_projectWithAllScansFailed() { + testMissingResultsCase("common-client-test-03-all-scans-failed"); + } + + /** + * Make sure that SCA results are null in different expected cases. + */ + private void testMissingResultsCase(String projectName) { + log.info("Checking that scaResults are null for the {} project", projectName); + CxScanConfig config = initScaConfig(false); + config.setProjectName(projectName); + ScanResults latestResults = getLatestResults(config); + Assert.assertNotNull("scanResults must not be null.", latestResults); + Assert.assertNull("scaResults must be null.", latestResults.getScaResults()); + } + + @Test + @Ignore("There is no stable on-prem environment.") + public void scan_onPremiseAuthentication() throws MalformedURLException { + scanRemoteRepo(PUBLIC_REPO_PROP, true); + } + + @Test + @Ignore("Needs specific network configuration with a proxy.") + public void runScaScanWithProxy() throws MalformedURLException, CxClientException { + CxScanConfig config = initScaConfig(false); + setScaProxy(config); + ScanResults scanResults = runScan(config); + verifyScanResults(scanResults); + } + + private ScanResults getLatestResults(CxScanConfig config) { + CxClientDelegator client = null; + try { + client = new CxClientDelegator(config, log); + } catch (MalformedURLException e) { + failOnException(e); + } + Assert.assertNotNull(client); + client.init(); + + return client.getLatestScanResults(); + } + + @Test + public void scan_localDirUploadIncludeSources() throws IOException, CxClientException { + CxScanConfig config = initScaConfig(false); + localDirScan(config); + } + + @Test + public void scan_localDirZeroCodeScan() throws IOException, CxClientException { + CxScanConfig config = initScaConfig(false); + localDirScan(config); + } + + private void localDirScan(CxScanConfig config) throws MalformedURLException { + config.setOsaThresholdsEnabled(true); + config.getAstScaConfig().setSourceLocationType(SourceLocationType.LOCAL_DIRECTORY); + + Path sourcesDir = null; + try { + sourcesDir = extractTestProjectFromResources(); + config.setSourceDir(sourcesDir.toString()); + + ScanResults scanResults = runScan(config); + verifyScanResults(scanResults); + } finally { + deleteDir(sourcesDir); + } + } + + private void scanRemoteRepo(String repoUrlProp, boolean useOnPremAuthentication) throws MalformedURLException { + CxScanConfig config = initScaConfig(repoUrlProp, useOnPremAuthentication); + ScanResults scanResults = runScan(config); + verifyScanResults(scanResults); + } + + private Path extractTestProjectFromResources() { + InputStream testProjectStream = getTestProjectStream(); + Path tempDirectory = createTempDirectory(); + extractResourceToDir(testProjectStream, tempDirectory); + return tempDirectory; + } + + private void extractResourceToDir(InputStream source, Path targetDir) { + log.info("Unpacking sources into the temp dir."); + int fileCount = 0; + try (ArchiveInputStream inputStream = new ArchiveStreamFactory().createArchiveInputStream(source)) { + ArchiveEntry entry; + while ((entry = inputStream.getNextEntry()) != null) { + if (!inputStream.canReadEntryData(entry)) { + throw new IOException(String.format("Unable to read entry: %s", entry)); + } + Path fullTargetPath = targetDir.resolve(entry.getName()); + File targetFile = fullTargetPath.toFile(); + if (entry.isDirectory()) { + extractDirectory(targetFile); + } else { + extractFile(inputStream, targetFile); + fileCount++; + } + } + } catch (IOException | ArchiveException e) { + failOnException(e); + } + log.info("Files extracted: {}", fileCount); + } + + private static void extractFile(ArchiveInputStream inputStream, File targetFile) throws IOException { + File parent = targetFile.getParentFile(); + extractDirectory(parent); + try (OutputStream outputStream = Files.newOutputStream(targetFile.toPath())) { + IOUtils.copy(inputStream, outputStream); + } + } + + private static void extractDirectory(File targetFile) throws IOException { + if (!targetFile.isDirectory() && !targetFile.mkdirs()) { + throw new IOException(String.format("Failed to create directory %s", targetFile)); + } + } + + private static Path createTempDirectory() { + String systemTempDir = FileUtils.getTempDirectoryPath(); + String subdir = String.format("common-client-tests-%s", UUID.randomUUID()); + Path result = Paths.get(systemTempDir, subdir); + + log.info("Creating a temp dir: {}", result); + boolean success = result.toFile().mkdir(); + if (!success) { + fail("Failed to create temp dir."); + } + return result; + } + + private static void deleteDir(Path directory) { + if (directory == null) { + return; + } + + log.info("Deleting '{}'", directory); + try { + FileUtils.deleteDirectory(directory.toFile()); + } catch (IOException e) { + log.warn("Failed to delete temp dir.", e); + } + } + + private static InputStream getTestProjectStream() { + String srcResourceName = AstScaTests.PACKED_SOURCES_TO_SCAN; + log.info("Getting resource stream from '{}'", srcResourceName); + return Thread.currentThread() + .getContextClassLoader() + .getResourceAsStream(srcResourceName); + } +} diff --git a/src/test/java/com/cx/restclient/general/CommonClientTest.java b/src/test/java/com/cx/restclient/general/CommonClientTest.java new file mode 100644 index 00000000..1bd591be --- /dev/null +++ b/src/test/java/com/cx/restclient/general/CommonClientTest.java @@ -0,0 +1,130 @@ +package com.cx.restclient.general; + +import com.cx.restclient.CxClientDelegator; +import com.cx.restclient.ast.dto.sca.AstScaConfig; +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.configuration.PropertyFileLoader; +import com.cx.restclient.dto.ProxyConfig; +import com.cx.restclient.dto.ScanResults; +import com.cx.restclient.dto.ScannerType; +import com.cx.restclient.exception.CxClientException; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.BeforeClass; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; + +@Slf4j +public abstract class CommonClientTest { + private static final String MAIN_PROPERTIES_FILE = "config.properties"; + public static final String OVERRIDE_FILE = "config-secrets.properties"; + + private PropertyFileLoader props = new PropertyFileLoader(MAIN_PROPERTIES_FILE, OVERRIDE_FILE); + + protected String prop(String key) { + return props.get(key); + } + + protected void setProxy(CxScanConfig config) { + ProxyConfig proxyConfig = new ProxyConfig(); + proxyConfig.setHost(prop("proxy.host")); + proxyConfig.setPort(Integer.parseInt(prop("proxy.port"))); + config.setProxyConfig(proxyConfig); + } + + protected void setScaProxy(CxScanConfig config) { + ProxyConfig proxyConfig = new ProxyConfig(); + proxyConfig.setHost(prop("proxy.host")); + proxyConfig.setPort(Integer.parseInt(prop("proxy.port"))); + config.setScaProxyConfig(proxyConfig); + } + + void failOnException(Exception e) { + log.error("Unexpected exception during test.", e); + Assert.fail(e.getMessage()); + } + + protected CxScanConfig initSastConfig(CxScanConfig config, String projectName) { + config.setReportsDir(new File("C:\\report")); + config.setSourceDir(prop("sastSource")); + config.setUsername(prop("username")); + config.setPassword(prop("password")); + config.setUrl(prop("serverUrl")); + config.setCxOrigin("common"); + config.setProjectName(projectName); + config.setPresetName("Default"); + config.setTeamPath("\\CxServer"); + config.setSynchronous(true); + config.setGeneratePDFReport(true); + config.addScannerType(ScannerType.SAST); + config.setPresetName("Default"); +// config.setPresetId(7); + + return config; + } + + protected AstScaConfig getScaConfig(boolean useOnPremiseAuthentication) { + String accessControlProp, usernameProp, passwordProp; + if (useOnPremiseAuthentication) { + accessControlProp = "astSca.onPremise.accessControlUrl"; + usernameProp = "astSca.onPremise.username"; + passwordProp = "astSca.onPremise.password"; + } else { + accessControlProp = "astSca.cloud.accessControlUrl"; + usernameProp = "astSca.cloud.username"; + passwordProp = "astSca.cloud.password"; + } + + AstScaConfig result = new AstScaConfig(); + result.setApiUrl(prop("astSca.apiUrl")); + result.setWebAppUrl(prop("astSca.webAppUrl")); + result.setTenant(prop("astSca.tenant")); + result.setAccessControlUrl(prop(accessControlProp)); + result.setUsername(prop(usernameProp)); + result.setPassword(prop(passwordProp)); + result.setIncludeSources(false); + return result; + } + + protected ScanResults runScan(CxScanConfig config) throws MalformedURLException, CxClientException { + CxClientDelegator client = new CxClientDelegator(config, log); + try { + client.init(); + log.info("Initiate scan for the following scanners: " + config.getScannerTypes()); + client.initiateScan(); + log.info("Waiting for results of " + config.getScannerTypes()); + ScanResults results = client.waitForScanResults(); + Assert.assertNotNull(results); + log.info("Results retrieved" ); + return results; + } catch (Exception e) { + failOnException(e); + throw new CxClientException(e); + } + } + + protected CxScanConfig initOsaConfig(CxScanConfig config, String projectName) { + log.info("Scan ProjectName " + projectName); + config.addScannerType(ScannerType.OSA); + config.setSourceDir(prop("dependencyScanSourceDir")); + config.setReportsDir(new File("C:\\report")); + config.setUrl(prop("serverUrl")); + config.setUsername(prop("username")); + config.setPassword(prop("password")); + + config.setCxOrigin("common"); + config.setProjectName(projectName); + config.setPresetName("Default"); + config.setTeamPath("\\CxServer"); + config.setSynchronous(true); + config.setGeneratePDFReport(true); + + config.setOsaRunInstall(true); + config.setOsaThresholdsEnabled(true); + config.setPublic(true); + + return config; + } +} diff --git a/src/test/java/com/cx/restclient/general/ConnectionTest.java b/src/test/java/com/cx/restclient/general/ConnectionTest.java new file mode 100644 index 00000000..51b44783 --- /dev/null +++ b/src/test/java/com/cx/restclient/general/ConnectionTest.java @@ -0,0 +1,63 @@ +package com.cx.restclient.general; + +import com.cx.restclient.CxClientDelegator; +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.dto.ScannerType; +import com.cx.restclient.exception.CxClientException; +import com.cx.restclient.ast.dto.sca.AstScaConfig; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.IOException; +import java.net.MalformedURLException; + +@Slf4j +public class ConnectionTest extends CommonClientTest { + + //TODO : fix this test + @Test + @Ignore("this test fails and needs to be fixed") + public void ssoConnectionTest() { + CxScanConfig config = initConfig(); + try { + CxClientDelegator client = new CxClientDelegator(config, log); + client.init(); + } catch (IOException | CxClientException e) { + e.printStackTrace(); + log.error("Error running osa scan: " + e.getMessage()); + Assert.fail(e.getMessage()); + } + } +//TODO : Fix this test + @Ignore("this test fails and needs to be fixed") + @Test + public void scaConnectionTest() { + CxScanConfig config = new CxScanConfig(); + config.setCxOrigin("common"); + AstScaConfig scaConfig = getScaConfig(false); + config.setAstScaConfig(scaConfig); + config.addScannerType(ScannerType.AST_SCA); + try { + CxClientDelegator delegator = new CxClientDelegator(config, log); + delegator.getScaClient().testScaConnection(); + } catch (CxClientException | MalformedURLException e) { + failOnException(e); + } + } + + private CxScanConfig initConfig() { + CxScanConfig config = new CxScanConfig(); + config.setSastEnabled(true); + config.setUseSSOLogin(true); + config.setUsername(prop("username")); + config.setPassword(prop("password")); + config.setUrl(prop("serverUrl")); + config.setCxOrigin("common"); + config.setTeamPath("\\CxServer"); + config.setPresetName("Checkmarx Default"); + config.setProjectName("testSSO"); + return config; + } +} diff --git a/src/test/java/com/cx/restclient/general/GetTeamListTest.java b/src/test/java/com/cx/restclient/general/GetTeamListTest.java new file mode 100644 index 00000000..b41b3fd7 --- /dev/null +++ b/src/test/java/com/cx/restclient/general/GetTeamListTest.java @@ -0,0 +1,43 @@ +package com.cx.restclient.general; + +import com.cx.restclient.CxClientDelegator; +import com.cx.restclient.CxSASTClient; +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.dto.Team; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.List; + +@Slf4j +public class GetTeamListTest extends CommonClientTest { + + //TODO : Fix this tes + @Ignore("this test fails and needs to be fixed") + @Test + public void getTeamListTest() { + CxScanConfig config = initConfig(); + config.setSastEnabled(true); + try { + CxClientDelegator client = new CxClientDelegator(config, log); + CxSASTClient sastClient = client.getSastClient(); + sastClient.login("9.0"); + List teams = sastClient.getTeamList(); + Assert.assertNotNull(teams); + Assert.assertFalse(teams.isEmpty()); + } catch (Exception e) { + failOnException(e); + } + } + + private CxScanConfig initConfig() { + CxScanConfig config = new CxScanConfig(); + config.setUsername(prop("username")); + config.setPassword(prop("password")); + config.setUrl(prop("serverUrl")); + config.setCxOrigin("common"); + return config; + } +} diff --git a/src/test/java/com/cx/restclient/general/OsaScanTests.java b/src/test/java/com/cx/restclient/general/OsaScanTests.java new file mode 100644 index 00000000..4191d11e --- /dev/null +++ b/src/test/java/com/cx/restclient/general/OsaScanTests.java @@ -0,0 +1,22 @@ +package com.cx.restclient.general; + +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.dto.ScanResults; +import com.cx.restclient.exception.CxClientException; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.Test; + +import java.net.MalformedURLException; + +@Slf4j +public class OsaScanTests extends CommonClientTest { + @Test + public void runOsaScan() throws MalformedURLException, CxClientException { + CxScanConfig config = initOsaConfig(new CxScanConfig(), "osaOnlyScan"); + ScanResults results = runScan(config); + Assert.assertNull(results.getScaResults()); + Assert.assertNotNull(results.getOsaResults()); + Assert.assertNotNull("Expected valid osa scan id", results.getOsaResults().getOsaScanId()); + } +} diff --git a/src/test/java/com/cx/restclient/general/SastAndOsaScanTest.java b/src/test/java/com/cx/restclient/general/SastAndOsaScanTest.java new file mode 100644 index 00000000..78fc91ed --- /dev/null +++ b/src/test/java/com/cx/restclient/general/SastAndOsaScanTest.java @@ -0,0 +1,43 @@ +package com.cx.restclient.general; + +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.dto.ScanResults; +import com.cx.restclient.exception.CxClientException; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import java.net.MalformedURLException; + +@Ignore +@Slf4j +public class SastAndOsaScanTest extends CommonClientTest { + @Test + public void runSastAndOsaScan() throws MalformedURLException, CxClientException { + String projectName = "SastAndOsa"; + CxScanConfig config = initSastConfig(new CxScanConfig(), projectName); + config = initOsaConfig(config, projectName); + + ScanResults results = runScan(config); + + Assert.assertNull(results.getScaResults()); + Assert.assertNotNull(results.getOsaResults()); + log.info("Osa scan ID: " + results.getOsaResults().getOsaScanId()); + log.info("Osa Vulnerabilities: " + results.getOsaResults().getOsaVulnerabilities()); + log.info("Osa Libraries:" + results.getOsaResults().getOsaLibraries()); + Assert.assertNotNull("Expected valid osa scan id", results.getOsaResults().getOsaScanId()); + + Assert.assertNotNull(results.getSastResults()); + log.info("Sast scan ID: " + results.getSastResults().getScanId()); + log.info("Sast High: " + results.getSastResults().getHigh() + "Sast Medium: " + results.getSastResults().getMedium()); + + Assert.assertNotNull("Expected valid osa scan id", results.getOsaResults().getOsaScanId()); + } + + protected ScanResults runScan(CxScanConfig config) throws MalformedURLException, CxClientException { + ScanResults results = super.runScan(config); + Assert.assertNotEquals("Expected valid SAST scan id", 0, results.getSastResults().getScanId()); + return results; + } +} diff --git a/src/test/java/com/cx/restclient/general/SastAndScaScanTest.java b/src/test/java/com/cx/restclient/general/SastAndScaScanTest.java new file mode 100644 index 00000000..3cb48c8a --- /dev/null +++ b/src/test/java/com/cx/restclient/general/SastAndScaScanTest.java @@ -0,0 +1,34 @@ +package com.cx.restclient.general; + +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.dto.ScanResults; +import lombok.extern.slf4j.Slf4j; +import org.junit.Ignore; +import org.junit.Test; + +import java.net.MalformedURLException; + +@Slf4j +public class SastAndScaScanTest extends ScaTestsBase { + //TODO : fix this test + @Test + @Ignore("this test fails and needs to be fixed") + public void scan_remotePublicRepo() throws MalformedURLException { + scanRemoteRepo(PUBLIC_REPO_PROP, false); + } + + //TODO : fix this test + @Test + @Ignore("this test fails and needs to be fixed") + public void scan_remotePrivateRepo() throws MalformedURLException { + scanRemoteRepo(PRIVATE_REPO_PROP, false); + } + + private void scanRemoteRepo(String repoUrlProp, boolean useOnPremAuthentication) throws MalformedURLException { + CxScanConfig config = initScaConfig(repoUrlProp, useOnPremAuthentication); + config = initSastConfig(config, "SastAndSca"); + + ScanResults scanResults = runScan(config); + verifyScanResults(scanResults); + } +} diff --git a/src/test/java/com/cx/restclient/general/SastScanTests.java b/src/test/java/com/cx/restclient/general/SastScanTests.java new file mode 100644 index 00000000..e059af12 --- /dev/null +++ b/src/test/java/com/cx/restclient/general/SastScanTests.java @@ -0,0 +1,35 @@ +package com.cx.restclient.general; + +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.dto.ScanResults; +import com.cx.restclient.exception.CxClientException; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import java.net.MalformedURLException; + +@Slf4j +public class SastScanTests extends CommonClientTest { + private static final String PROJECT_NAME = "sastOnlyScan"; + + @Test + public void runSastScan() throws MalformedURLException, CxClientException { + CxScanConfig config = initSastConfig(new CxScanConfig(), PROJECT_NAME); + runSastScan(config); + } + + @Test + @Ignore("There is no stable environment for this test") + public void runSastScanWithProxy() throws MalformedURLException, CxClientException { + CxScanConfig config = initSastConfig(new CxScanConfig(), PROJECT_NAME); + setProxy(config); + runSastScan(config); + } + + private void runSastScan(CxScanConfig config) throws MalformedURLException, CxClientException { + ScanResults results = runScan(config); + Assert.assertNotEquals("Expected valid SAST scan id", 0, results.getSastResults().getScanId()); + } +} diff --git a/src/test/java/com/cx/restclient/general/ScaTestsBase.java b/src/test/java/com/cx/restclient/general/ScaTestsBase.java new file mode 100644 index 00000000..0bb8eee3 --- /dev/null +++ b/src/test/java/com/cx/restclient/general/ScaTestsBase.java @@ -0,0 +1,105 @@ +package com.cx.restclient.general; + +import com.cx.restclient.ast.dto.common.RemoteRepositoryInfo; +import com.cx.restclient.ast.dto.sca.AstScaConfig; +import com.cx.restclient.ast.dto.sca.AstScaResults; +import com.cx.restclient.ast.dto.sca.report.AstScaSummaryResults; +import com.cx.restclient.ast.dto.sca.report.Finding; +import com.cx.restclient.ast.dto.sca.report.Package; +import com.cx.restclient.configuration.CxScanConfig; +import com.cx.restclient.dto.ScanResults; +import com.cx.restclient.dto.ScannerType; +import com.cx.restclient.dto.SourceLocationType; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + +import static org.junit.Assert.*; + +@Slf4j +public abstract class ScaTestsBase extends CommonClientTest { + // Storing the test project as an archive to avoid cluttering the current project + // and also to prevent false positives during a vulnerability scan of the current project. + protected static final String PACKED_SOURCES_TO_SCAN = "sources-to-scan.zip"; + protected static final String PUBLIC_REPO_PROP = "astSca.remoteRepoUrl.public"; + protected static final String PRIVATE_REPO_PROP = "astSca.remoteRepoUrl.private"; + + protected CxScanConfig initScaConfig(String repoUrlProp, boolean useOnPremAuthentication) throws MalformedURLException { + CxScanConfig config = initScaConfig(useOnPremAuthentication); + config.getAstScaConfig().setSourceLocationType(SourceLocationType.REMOTE_REPOSITORY); + RemoteRepositoryInfo repoInfo = new RemoteRepositoryInfo(); + + URL repoUrl = new URL(prop(repoUrlProp)); + repoInfo.setUrl(repoUrl); + repoInfo.setUsername(prop("astSca.remoteRepo.private.token")); + + config.getAstScaConfig().setRemoteRepositoryInfo(repoInfo); + return config; + } + + protected CxScanConfig initScaConfig(boolean useOnPremAuthentication){ + CxScanConfig config = new CxScanConfig(); + config.addScannerType(ScannerType.AST_SCA); + config.setSastEnabled(false); + config.setProjectName(prop("astSca.projectName")); + config.setOsaProgressInterval(5); + AstScaConfig sca = getScaConfig(useOnPremAuthentication); + config.setAstScaConfig(sca); + + return config; + } + + protected void verifyScanResults(ScanResults results) { + assertNotNull("Scan results are null.", results); + assertNull("OSA results are not null.", results.getOsaResults()); + + AstScaResults scaResults = results.getScaResults(); + assertNotNull("SCA results are null", scaResults); + + log.info("scanID " + scaResults.getScanId()); + assertTrue("Scan ID is empty", StringUtils.isNotEmpty(scaResults.getScanId())); + assertTrue("Web report link is empty", StringUtils.isNotEmpty(scaResults.getWebReportLink())); + + verifySummary(scaResults.getSummary()); + verifyPackages(scaResults); + verifyFindings(scaResults); + } + + private void verifySummary(AstScaSummaryResults summary) { + + assertNotNull("SCA summary is null", summary); + assertTrue("SCA hasn't found any packages.", summary.getTotalPackages() > 0); + + boolean anyVulnerabilitiesDetected = summary.getCriticalVulnerabilityCount() > 0 || + summary.getHighVulnerabilityCount() > 0 || + summary.getMediumVulnerabilityCount() > 0 || + summary.getLowVulnerabilityCount() > 0; + assertTrue("Expected that at least one vulnerability would be detected.", anyVulnerabilitiesDetected); + } + + private void verifyPackages(AstScaResults scaResults) { + List packages = scaResults.getPackages(); + + assertNotNull("Packages are null.", packages); + assertFalse("Response contains no packages.", packages.isEmpty()); + + assertEquals("Actual package count differs from package count in summary.", + scaResults.getSummary().getTotalPackages(), + packages.size()); + } + + private void verifyFindings(AstScaResults scaResults) { + List findings = scaResults.getFindings(); + assertNotNull("Findings are null", findings); + assertFalse("Response contains no findings.", findings.isEmpty()); + + // Special check due to a case-sensitivity issue. + boolean allSeveritiesAreSpecified = findings.stream() + .allMatch(finding -> finding.getSeverity() != null); + + assertTrue("Some of the findings have severity set to null.", allSeveritiesAreSpecified); + } +} diff --git a/src/test/resources/config.properties b/src/test/resources/config.properties new file mode 100644 index 00000000..9bdb19d0 --- /dev/null +++ b/src/test/resources/config.properties @@ -0,0 +1,30 @@ +serverUrl=http:// +username=myuser +password=mypassword +sastSource=C:\\projectsToScan\\project1 +dependencyScanSourceDir=C:\\projectsToScan\\project2 + +astSca.apiUrl=https://api-sca.checkmarx.net +astSca.webAppUrl=https://sca.checkmarx.net +astSca.tenant=mytenant +astSca.remoteRepoUrl.public=http://example.com/my/public-repo.git +astSca.remoteRepoUrl.private=http://example.com/my/private-repo.git +astSca.remoteRepo.private.token=myrepotoken +astSca.projectName=common-client-test-01 + +proxy.host=1.2.3.4 +proxy.port=12345 + +astSca.cloud.accessControlUrl=https://platform.checkmarx.net +astSca.cloud.username=myuser +astSca.cloud.password=mypassword + +astSca.onPremise.accessControlUrl=http://my-onprem-server/CxRestAPI/auth/ +astSca.onPremise.username=myuser +astSca.onPremise.password=mypassword + +astSast.apiUrl=http://example1.com +astSast.webAppUrl=http://example2.com +astSast.remoteRepoUrl.public=http://example.com/my/public-repo.git +astSast.projectName=ast-sast-test-01 +astSast.clientSecret=mysecret \ No newline at end of file diff --git a/src/test/resources/gson-2.2.2.jar b/src/test/resources/gson-2.2.2.jar new file mode 100644 index 00000000..9adc66fd Binary files /dev/null and b/src/test/resources/gson-2.2.2.jar differ diff --git a/src/test/resources/sources-to-scan.zip b/src/test/resources/sources-to-scan.zip new file mode 100644 index 00000000..10d7b893 Binary files /dev/null and b/src/test/resources/sources-to-scan.zip differ