From 46e14a601410d23a486899e833787b001b9d16e0 Mon Sep 17 00:00:00 2001 From: Oleg Kopysov Date: Fri, 29 Jul 2022 11:57:13 +0300 Subject: [PATCH] Initial LPVS release v1.0.0 Signed-off-by: Oleg Kopysov --- .github/Docker_Usage.md | 43 ++++ .gitignore | 28 +++ Dockerfile | 36 +++ LICENSE | 19 ++ README.md | 81 ++++++- lpvslogo.png | Bin 0 -> 49404 bytes pom.xml | 61 +++++ .../com/lpvs/LicensePreValidationSystem.java | 44 ++++ .../controller/GitHubWebhooksController.java | 67 ++++++ src/main/java/com/lpvs/entity/LPVSFile.java | 92 ++++++++ .../java/com/lpvs/entity/LPVSLicense.java | 102 ++++++++ .../java/com/lpvs/entity/ResponseWrapper.java | 24 ++ .../com/lpvs/entity/config/WebhookConfig.java | 219 ++++++++++++++++++ .../lpvs/entity/enums/PullRequestAction.java | 47 ++++ .../java/com/lpvs/service/DetectService.java | 45 ++++ .../java/com/lpvs/service/GitHubService.java | 206 ++++++++++++++++ .../java/com/lpvs/service/LicenseService.java | 175 ++++++++++++++ .../lpvs/service/QueueProcessorService.java | 39 ++++ .../java/com/lpvs/service/QueueService.java | 94 ++++++++ .../scanner/scanoss/ScanossDetectService.java | 190 +++++++++++++++ src/main/java/com/lpvs/util/FileUtil.java | 119 ++++++++++ src/main/java/com/lpvs/util/WebhookUtil.java | 72 ++++++ src/main/resources/application.properties | 15 ++ src/main/resources/licenses.json | 33 +++ 24 files changed, 1850 insertions(+), 1 deletion(-) create mode 100644 .github/Docker_Usage.md create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 lpvslogo.png create mode 100644 pom.xml create mode 100644 src/main/java/com/lpvs/LicensePreValidationSystem.java create mode 100644 src/main/java/com/lpvs/controller/GitHubWebhooksController.java create mode 100644 src/main/java/com/lpvs/entity/LPVSFile.java create mode 100644 src/main/java/com/lpvs/entity/LPVSLicense.java create mode 100644 src/main/java/com/lpvs/entity/ResponseWrapper.java create mode 100644 src/main/java/com/lpvs/entity/config/WebhookConfig.java create mode 100644 src/main/java/com/lpvs/entity/enums/PullRequestAction.java create mode 100644 src/main/java/com/lpvs/service/DetectService.java create mode 100644 src/main/java/com/lpvs/service/GitHubService.java create mode 100644 src/main/java/com/lpvs/service/LicenseService.java create mode 100644 src/main/java/com/lpvs/service/QueueProcessorService.java create mode 100644 src/main/java/com/lpvs/service/QueueService.java create mode 100644 src/main/java/com/lpvs/service/scanner/scanoss/ScanossDetectService.java create mode 100644 src/main/java/com/lpvs/util/FileUtil.java create mode 100644 src/main/java/com/lpvs/util/WebhookUtil.java create mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/licenses.json diff --git a/.github/Docker_Usage.md b/.github/Docker_Usage.md new file mode 100644 index 00000000..ff6cbb54 --- /dev/null +++ b/.github/Docker_Usage.md @@ -0,0 +1,43 @@ +# Additional information and tips for Docker usage + +#### There are three ways to execute the Docker container with LPVS after the image was built by command `docker build -t lpvs .` : + +1. Terminal mode: the log is shown in the terminal in real-time, but the terminal must be open for running the application in the container: + + ```bash + docker run -p 7896:7896 --name lpvs lpvs:latest + ``` + +2. Background mode: the container is not going to be restarted after reboot, to see the log additional commands should be used: `docker logs -f lpvs`: + + ```bash + docker run -d -p 7896:7896 --name lpvs lpvs:latest + ``` + +3. Background mode with constant usage: the container is going to be restarted after the reboot (other behavior is similar to the background mode): + + ```bash + docker run -d -p 7896:7896 --restart unless-stopped --name lpvs lpvs:latest + ``` + + ***It is better (for disk space economy) to stop and start the same container which is created by the command `docker run`.*** + +#### Useful Docker commands + +To stop the running container use the following commands: + + ```bash + docker stop lpvs + ``` + +To start the stopped container use the following commands: + + ```bash + docker start lpvs + ``` + +To clean unused containers (if the command `docker run` was used a few times), _only stopped containers will be deleted_: + + ```bash + docker rm lpvs + ``` diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..12a71dac --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +.idea/* +target/ +Projects/ +*.iml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..50f5b9df --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +# Basic image +FROM openjdk:11 + +# Install dependencies and remove tmp files +RUN apt-get update && \ +apt-get upgrade -y && \ +apt-get install -y python3-pip maven && \ +apt-get clean && \ +rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# Setup env variables +ENV PRJ_DIR="LPVS" + +# Create project dir +RUN mkdir $PRJ_DIR + +# Set workdir +WORKDIR /$PRJ_DIR + +# Copy source code into container +COPY . . + +# Install SCANOSS +RUN pip3 install scanoss + +# Build LPVS-open-source application +RUN mvn clean install + +# Allow to listen port 7896 +EXPOSE 7896 + +# Set workdir for running jar +WORKDIR /$PRJ_DIR/target + +# Run application in container +CMD ["java", "-jar", "lpvs-1.0.0.jar"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..e3805bcb --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022, Samsung Research. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 216e84ce..56c1ccb1 100644 --- a/README.md +++ b/README.md @@ -1 +1,80 @@ -# LPVS + +![License Pre-Validation Service (LPVS)](lpvslogo.png) +[![Build](https://github.sec.samsung.net/SRK-PSL/LPVS-open-source/workflows/Build/badge.svg)](https://github.sec.samsung.net/SRK-PSL/LPVS-open-source/actions?query=workflow%3ABuild) + +## Introduction +OpenSource code [refers](https://en.wikipedia.org/wiki/Open-source_software) to software available for use, study, change, and distribution by anyone and for any purpose provided that the corresponding license conditions are met. License violation may end up with litigations, damage compensation, obligation to disclose intellectual property as well as reputational losses. + +In a project with many external dependencies it might be really difficult to trace license obligations. Also if many collaborators are involved, a risk of non-intentional license violation (such as via Copy-Paste) grows. There are even more tricky nuances such as double-licensed dependencies or license change (because of owner, purpose, legislation change) that may make a previously safe dependency to become an unsafe one over time. + +License Pre-Validation Service (LPVS) helps to mitigate license-related risks for OpenSource code. The tool analyzes the project, identifies its components and their respective licenses at every commit. Then it returns the list of potential issue cases as GitHub comments. LPVS provides the comprehensive description of possible license violations, including risky code location and license issue overview. + +## Features + +- available license scanners: [SCANOSS](https://www.scanoss.com) +- LPVS supports GitHub review system + +## LPVS GitHub Integration + +LPVS license scan shall be enabled on a project via GitHub Hooks: + +1. In `src/main/resources/application.properties` specify the account to be used for posting scan results as a review message. The following fields should be filled: `github.token`. + +2. Add the user specified in `github.token` as a collaborator to your GitHub project. + +3. Configure webhook in your GitHub repository settings: +- go to `Settings` -> `Hooks` +- press `Add webhook` +- fill in Payload URL with: `http://:7896/webhooks` +- specify content type: `application/json` +- fill in `Secret` field with the passphrase: `LPVS` +- select `Let me select individual events` -> `Pull requests` (make sure that only `Pull requests` is selected) +- make it `Active` +- press `Add Webhook` + +Create a new pull request and update it with commits. +LPVS will start scanning automatically, then provide comments about the licenses found in the project. + +## LPVS Backend Configuration + +1. Install SCANOSS Python package by following the [guideline](https://github.com/scanoss/scanoss.py#installation). + +2. Fill in the lines of the `src/main/resources/application.properties` file: + ```text + # Used license scanner + scanner=scanoss + # Used license conflicts source (take from 'licenses.json' ("json") + # or from scanner response("scanner")) + license_conflict=json + ``` + +3. Fill in `src/main/resources/licenses.json` file with the information about permitted, restricted, and prohibited licenses as well as their compatibility specifics. An example of the `licenses.json` file can be found in the repository. + +4. Build LPVS application with Maven, then run it: + ```bash + mvn clean install + cd target/ + java -jar lpvs-1.0.0.jar + ``` + + Or alternatively build and run the Docker container with LPVS: + ```bash + docker build -t lpvs . + docker run -p 7896:7896 --name lpvs lpvs:latest + ``` + For additional information about using Docker and tips, please check file [Docker_Usage](.github/Docker_Usage.md). + +5. Install [ngrok](https://dashboard.ngrok.com/get-started) (step 1 and 2) and run it with the following command: + ```bash + ./ngrok http 7896 + ``` +At this point LPVS is ready for work. + +## License + +The LPVS source code is distributed under the [MIT](https://opensource.org/licenses/MIT) open source license. + +## Contributing + +You are welcome to contribute to LPVS project. +Contributing is also a great way to practice social coding at Github, study new technologies and enrich your public portfolio. diff --git a/lpvslogo.png b/lpvslogo.png new file mode 100644 index 0000000000000000000000000000000000000000..f24c0621b673a499d6329fa86a33ad2fbf4d6282 GIT binary patch literal 49404 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 BQB(i` literal 0 HcmV?d00001 diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..b7f678c5 --- /dev/null +++ b/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + com.lpvs + lpvs + 1.0.0 + jar + + + org.springframework.boot + spring-boot-starter-parent + 2.2.6.RELEASE + + + + + UTF-8 + 1.8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework + spring-core + 5.2.22.RELEASE + + + com.google.code.gson + gson + [2.8.9,) + + + org.kohsuke + github-api + 1.114 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + 2.1.2.RELEASE + + + + + diff --git a/src/main/java/com/lpvs/LicensePreValidationSystem.java b/src/main/java/com/lpvs/LicensePreValidationSystem.java new file mode 100644 index 00000000..e3c9a91f --- /dev/null +++ b/src/main/java/com/lpvs/LicensePreValidationSystem.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2022, Samsung Research. All rights reserved. + * + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.lpvs; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + + +@SpringBootApplication +@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class}) +@EnableAsync +public class LicensePreValidationSystem { + + @Value("${lpvs.cores:8}") + private int corePoolSize; + + public static void main(String[] args) { + SpringApplication app = new SpringApplication(LicensePreValidationSystem.class); + app.run(args); + } + + @Bean("threadPoolTaskExecutor") + public TaskExecutor getAsyncExecutor(){ + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(corePoolSize); + executor.setThreadNamePrefix("LPVS-ASYNC::"); + return executor; + } + +} + diff --git a/src/main/java/com/lpvs/controller/GitHubWebhooksController.java b/src/main/java/com/lpvs/controller/GitHubWebhooksController.java new file mode 100644 index 00000000..a9649d48 --- /dev/null +++ b/src/main/java/com/lpvs/controller/GitHubWebhooksController.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2022, Samsung Research. All rights reserved. + * + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.lpvs.controller; + +import com.lpvs.entity.config.WebhookConfig; +import com.lpvs.service.GitHubService; +import com.lpvs.service.QueueService; +import com.lpvs.util.WebhookUtil; +import com.lpvs.entity.ResponseWrapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestBody; +import java.util.Date; + + +@RestController +public class GitHubWebhooksController { + + @Autowired + private QueueService queueService; + + @Autowired + private GitHubService gitHubService; + + private static Logger LOG = LoggerFactory.getLogger(GitHubWebhooksController.class); + + private static final String SIGNATURE = "X-Hub-Signature"; + private static final String SUCCESS = "Success"; + private static final String ERROR = "Error"; + + @RequestMapping(value = "/webhooks", method = RequestMethod.POST) + public ResponseEntity gitHubWebhooks(@RequestHeader(SIGNATURE) String signature, @RequestBody String payload) throws InterruptedException { + LOG.info("New webhook request received"); + + // if signature is empty return 401 + if (!StringUtils.hasText(signature)) { + return new ResponseEntity<>(new ResponseWrapper(ERROR), HttpStatus.FORBIDDEN); + } + + // if payload is empty, don't do anything + if (!StringUtils.hasText(payload)) { + LOG.info("Response to empty payload sent"); + return new ResponseEntity<>(new ResponseWrapper(SUCCESS), HttpStatus.OK); + } else if (WebhookUtil.checkPayload(payload)) { + WebhookConfig webhookConfig = WebhookUtil.getGitHubWebhookConfig(payload); + webhookConfig.setDate(new Date()); + LOG.info("Repository scanning is enabled: On"); + gitHubService.setPendingCheck(webhookConfig); + queueService.addFirst(webhookConfig); + } + LOG.info("Response sent"); + return new ResponseEntity<>(new ResponseWrapper(SUCCESS), HttpStatus.OK); + } +} diff --git a/src/main/java/com/lpvs/entity/LPVSFile.java b/src/main/java/com/lpvs/entity/LPVSFile.java new file mode 100644 index 00000000..ae155e37 --- /dev/null +++ b/src/main/java/com/lpvs/entity/LPVSFile.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2022, Samsung Research. All rights reserved. + * + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.lpvs.entity; + +import java.util.Set; + +public class LPVSFile { + + private Long id; + private String fileUrl; + private String filePath; + private String snippetMatch; + private String matchedLines; + private Set licenses; + private String component; + + public LPVSFile() { + } + + public Set getLicenses() { + return licenses; + } + + public void setLicenses(Set licenses) { + this.licenses = licenses; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFileUrl() { + return fileUrl; + } + + public void setFileUrl(String fileUrl) { + this.fileUrl = fileUrl; + } + + public String getFilePath() { + return filePath; + } + + public void setFilePath(String filePath) { + this.filePath = filePath; + } + + public String getSnippetMatch() { + return snippetMatch; + } + + public void setSnippetMatch(String snippetMatch) { + this.snippetMatch = snippetMatch; + } + + public String getComponent() { + return component; + } + + public void setComponent(String component) { + this.component = component; + } + + public String getMatchedLines() { + return matchedLines; + } + + public void setMatchedLines(String matchedLines) { + this.matchedLines = matchedLines; + } + + public String convertLicensesToString() { + String licenseNames = ""; + for (LPVSLicense license : this.licenses) { + licenseNames += (license.getChecklist_url() != null ? "" : "") + + license.getSpdxId() + + (license.getChecklist_url() != null ? "" : "") + + " (" + license.getAccess().toLowerCase() + "), "; + } + if (licenseNames.endsWith(", ")) licenseNames = licenseNames.substring(0, licenseNames.length() - 2); + return licenseNames; + } +} \ No newline at end of file diff --git a/src/main/java/com/lpvs/entity/LPVSLicense.java b/src/main/java/com/lpvs/entity/LPVSLicense.java new file mode 100644 index 00000000..e4472f59 --- /dev/null +++ b/src/main/java/com/lpvs/entity/LPVSLicense.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2022, Samsung Research. All rights reserved. + * + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.lpvs.entity; + +import java.util.List; + +public class LPVSLicense { + + private Long licenseId; + + private String licenseName; + + private String spdxId; + + private String access; + + private String checklistUrl; + + private List incompatibleWith; + + public LPVSLicense() { + } + + public LPVSLicense(Long licenseId, String licenseName, String spdxId, String access, String checklistUrl, List incompatibleWith) { + this.licenseId = licenseId; + this.licenseName = licenseName; + this.spdxId = spdxId; + this.access = access; + this.checklistUrl = checklistUrl; + this.incompatibleWith = incompatibleWith; + } + + public LPVSLicense(LPVSLicense license) { + this.licenseId = license.licenseId; + this.licenseName = license.licenseName; + this.spdxId = license.spdxId; + this.access = license.access; + this.checklistUrl = license.checklistUrl; + } + + public Long getLicenseId() { + return licenseId; + } + + public void setLicenseId(Long licenseId) { + this.licenseId = licenseId; + } + + public String getLicenseName() { + return licenseName; + } + + public void setLicenseName(String licenseName) { + this.licenseName = licenseName; + } + + public String getSpdxId() { + return spdxId; + } + + public void setSpdxId(String spdxId) { + this.spdxId = spdxId; + } + + public String getAccess() { + return access; + } + + public void setAccess(String access) { + this.access = access; + } + + public String getChecklist_url() { + return checklistUrl; + } + + public void setChecklist_url(String checklist_url) { + this.checklistUrl = checklist_url; + } + + public String getChecklistUrl() { + return checklistUrl; + } + + public void setChecklistUrl(String checklistUrl) { + this.checklistUrl = checklistUrl; + } + + public List getIncompatibleWith() { + return incompatibleWith; + } + + public void setIncompatibleWith(List incompatibleWith) { + this.incompatibleWith = incompatibleWith; + } + +} diff --git a/src/main/java/com/lpvs/entity/ResponseWrapper.java b/src/main/java/com/lpvs/entity/ResponseWrapper.java new file mode 100644 index 00000000..78bfbd43 --- /dev/null +++ b/src/main/java/com/lpvs/entity/ResponseWrapper.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022, Samsung Research. All rights reserved. + * + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.lpvs.entity; + +public class ResponseWrapper { + private String message; + + public ResponseWrapper(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/lpvs/entity/config/WebhookConfig.java b/src/main/java/com/lpvs/entity/config/WebhookConfig.java new file mode 100644 index 00000000..bce9b744 --- /dev/null +++ b/src/main/java/com/lpvs/entity/config/WebhookConfig.java @@ -0,0 +1,219 @@ +/** + * Copyright (c) 2022, Samsung Research. All rights reserved. + * + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.lpvs.entity.config; + +import com.lpvs.entity.enums.PullRequestAction; + +import java.util.Date; + +public class WebhookConfig { + Long webhookId; + PullRequestAction action; + Long repositoryId; + String repositoryName; + String repositoryOrganization; + String repositoryUrl; + String repositoryLicense; + String headCommitSHA; + String pullRequestUrl; + String pullRequestFilesUrl; + String pullRequestAPIUrl; + Long pullRequestId; + String pullRequestName; + String userId; + String hubLink; + String branch; + String pullRequestBranch; + int attempts; + Date date; + String reviewSystemType; + String reviewSystemName; + String statusCallbackUrl; + + public WebhookConfig() { + } + + public Long getWebhookId() { + return webhookId; + } + + public void setWebhookId(Long webhookId) { + this.webhookId = webhookId; + } + + public Long getRepositoryId() { + return repositoryId; + } + + public void setRepositoryId(Long repositoryId) { + this.repositoryId = repositoryId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public Long getPullRequestId() { + return pullRequestId; + } + + public void setPullRequestId(Long pullRequestId) { + this.pullRequestId = pullRequestId; + } + + public String getPullRequestName() { + return pullRequestName; + } + + public void setPullRequestName(String pullRequestName) { + this.pullRequestName = pullRequestName; + } + + public String getBranch() { + return branch; + } + + public void setBranch(String branch) { + this.branch = branch; + } + + public String getRepositoryLicense() { + return repositoryLicense; + } + + public void setRepositoryLicense(String repositoryLicense) { + this.repositoryLicense = repositoryLicense; + } + + public String getHubLink() { + return hubLink; + } + + public void setHubLink(String hubLink) { + this.hubLink = hubLink; + } + + public String getRepositoryOrganization() { + return repositoryOrganization; + } + + public void setRepositoryOrganization(String repositoryOrganization) { + this.repositoryOrganization = repositoryOrganization; + } + + public String getRepositoryUrl() { + return repositoryUrl; + } + + public void setRepositoryUrl(String repositoryUrl) { + this.repositoryUrl = repositoryUrl; + } + + public String getHeadCommitSHA() { return headCommitSHA; } + + public void setHeadCommitSHA(String headCommitSHA) { this.headCommitSHA = headCommitSHA; } + + public PullRequestAction getAction() { + return action; + } + + public void setAction(PullRequestAction action) { + this.action = action; + } + + public String getRepositoryName() { + return repositoryName; + } + + public void setRepositoryName(String repositoryName) { + this.repositoryName = repositoryName; + } + + public String getPullRequestUrl() { + return pullRequestUrl; + } + + public void setPullRequestUrl(String pullRequestUrl) { + this.pullRequestUrl = pullRequestUrl; + } + + public String getPullRequestFilesUrl() { + return pullRequestFilesUrl; + } + + public void setPullRequestFilesUrl(String pullRequestFilesUrl) { + this.pullRequestFilesUrl = pullRequestFilesUrl; + } + + public String getPullRequestAPIUrl() { + return pullRequestAPIUrl; + } + + public void setPullRequestAPIUrl(String pullRequestAPIUrl) { + this.pullRequestAPIUrl = pullRequestAPIUrl; + } + + public int getAttempts() { + return attempts; + } + + public void setAttempts(int attempts) { + this.attempts = attempts; + } + + public String getPullRequestBranch() { + return pullRequestBranch; + } + + public void setPullRequestBranch(String pullRequestBranch) { + this.pullRequestBranch = pullRequestBranch; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public String getReviewSystemType() { + return reviewSystemType; + } + + public void setReviewSystemType(String reviewSystemType) { + this.reviewSystemType = reviewSystemType; + } + + public String getReviewSystemName() { + return reviewSystemName; + } + + public void setReviewSystemName(String reviewSystemName) { + this.reviewSystemName = reviewSystemName; + } + + public String getStatusCallbackUrl() { + return statusCallbackUrl; + } + + public void setStatusCallbackUrl(String statusCallbackUrl) { + this.statusCallbackUrl = statusCallbackUrl; + } + + @Override + public String toString(){ + return "WebhookConfig [action = " + getAction() + "; organization name = " + getRepositoryOrganization() + + "; repository name = " + getRepositoryName() + "; PR URL = " + getPullRequestUrl() + + "; commit = " + getHeadCommitSHA() + "]"; + } +} diff --git a/src/main/java/com/lpvs/entity/enums/PullRequestAction.java b/src/main/java/com/lpvs/entity/enums/PullRequestAction.java new file mode 100644 index 00000000..d2f9bc6b --- /dev/null +++ b/src/main/java/com/lpvs/entity/enums/PullRequestAction.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2022, Samsung Research. All rights reserved. + * + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.lpvs.entity.enums; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public enum PullRequestAction { + OPEN("opened"), + REOPEN("reopened"), + CLOSE("closed"), + UPDATE("synchronize"), + RESCAN("rescan"); + + private final String type; + + PullRequestAction(final String type) { + this.type = type; + } + + public String getPullRequestAction() { + return type; + } + + private static Logger LOG = LoggerFactory.getLogger(PullRequestAction.class); + + public static PullRequestAction convertFrom(String action) { + if (action.equals(OPEN.getPullRequestAction())) { + return OPEN; + } else if (action.equals(REOPEN.getPullRequestAction())) { + return REOPEN; + } else if (action.equals(CLOSE.getPullRequestAction())) { + return CLOSE; + } else if (action.equals(UPDATE.getPullRequestAction())) { + return UPDATE; + } else if (action.equals(RESCAN.getPullRequestAction())) { + return RESCAN; + } else { + return null; + } + } +} diff --git a/src/main/java/com/lpvs/service/DetectService.java b/src/main/java/com/lpvs/service/DetectService.java new file mode 100644 index 00000000..506bf48b --- /dev/null +++ b/src/main/java/com/lpvs/service/DetectService.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2022, Samsung Research. All rights reserved. + * + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.lpvs.service; + +import com.lpvs.entity.LPVSFile; +import com.lpvs.entity.config.WebhookConfig; +import com.lpvs.service.scanner.scanoss.ScanossDetectService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.List; + +@Service +public class DetectService { + + @Value("${scanner:scanoss}") + private String scannerType; + + @Autowired + private ScanossDetectService scanossDetectService; + + private static Logger LOG = LoggerFactory.getLogger(DetectService.class); + + @PostConstruct + private void init() { + LOG.info("License detection scanner: " + scannerType); + } + + public List runScan(WebhookConfig webhookConfig, String path) throws Exception { + if (scannerType.equals("scanoss")) { + scanossDetectService.runScan(webhookConfig, path); + return scanossDetectService.checkLicenses(webhookConfig); + } + return new ArrayList<>(); + } +} diff --git a/src/main/java/com/lpvs/service/GitHubService.java b/src/main/java/com/lpvs/service/GitHubService.java new file mode 100644 index 00000000..f68703e8 --- /dev/null +++ b/src/main/java/com/lpvs/service/GitHubService.java @@ -0,0 +1,206 @@ +/** + * Copyright (c) 2022, Samsung Research. All rights reserved. + * + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.lpvs.service; + +import com.lpvs.entity.LPVSFile; +import com.lpvs.entity.LPVSLicense; +import com.lpvs.entity.config.WebhookConfig; +import com.lpvs.entity.enums.PullRequestAction; +import com.lpvs.util.FileUtil; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHPullRequest; +import org.kohsuke.github.GHLicense; +import org.kohsuke.github.GHIssueState; +import org.kohsuke.github.GHCommitState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import java.io.IOException; +import java.util.List; + +@Service +public class GitHubService { + + @Value("${github.login}") + private String GITHUB_LOGIN; + @Value("${github.token}") + private String GITHUB_AUTH_TOKEN; + @Value("${github.api.url:}") + private String GITHUB_API_URL; + + private static Logger LOG = LoggerFactory.getLogger(GitHubService.class); + + private static GitHub gitHub; + + public String getPullRequestFiles (WebhookConfig webhookConfig) { + if (webhookConfig.getAction().equals(PullRequestAction.RESCAN)) { + webhookConfig.setPullRequestAPIUrl(GITHUB_API_URL + "/repos/" + webhookConfig.getRepositoryOrganization() + "/" + + webhookConfig.getRepositoryName() + "/pulls/" + webhookConfig.getPullRequestId()); + } + try { + if (GITHUB_API_URL.isEmpty()) gitHub = GitHub.connect(GITHUB_LOGIN, GITHUB_AUTH_TOKEN); + else gitHub = GitHub.connectToEnterpriseWithOAuth(GITHUB_API_URL, GITHUB_LOGIN, GITHUB_AUTH_TOKEN); + GHRepository repository = gitHub.getRepository(webhookConfig.getRepositoryOrganization()+"/" + +webhookConfig.getRepositoryName()); + GHPullRequest pullRequest = getPullRequest(webhookConfig, repository); + if (pullRequest == null){ + LOG.error("Can't find pull request " + webhookConfig.getPullRequestAPIUrl()); + return null; + } + webhookConfig.setPullRequestName(pullRequest.getTitle()); + if (webhookConfig.getAction().equals(PullRequestAction.RESCAN)) { + webhookConfig.setHeadCommitSHA(pullRequest.getHead().getSha()); + } + return FileUtil.saveFiles(pullRequest.listFiles(),webhookConfig.getRepositoryOrganization()+"/"+webhookConfig.getRepositoryName(), + webhookConfig.getHeadCommitSHA(), pullRequest.getDeletions()); + } catch (IOException e){ + LOG.error("Can't authorize getPullRequestFiles() " + e); + } + return null; + } + + private GHPullRequest getPullRequest(WebhookConfig webhookConfig, GHRepository repository){ + try { + List pullRequests = repository.getPullRequests(GHIssueState.OPEN); + for (GHPullRequest pullRequest : pullRequests) { + if (pullRequest.getUrl().toString().equals(webhookConfig.getPullRequestAPIUrl())){ + return pullRequest; + } + } + } catch (IOException e){ + LOG.error("Can't authorize getPullRequest() " + e); + } + return null; + } + + public void setPendingCheck(WebhookConfig webhookConfig) { + try { + if (GITHUB_API_URL.isEmpty()) gitHub = GitHub.connect(GITHUB_LOGIN, GITHUB_AUTH_TOKEN); + else gitHub = GitHub.connectToEnterpriseWithOAuth(GITHUB_API_URL, GITHUB_LOGIN, GITHUB_AUTH_TOKEN); + GHRepository repository = gitHub.getRepository(webhookConfig.getRepositoryOrganization() + "/" + + webhookConfig.getRepositoryName()); + repository.createCommitStatus(webhookConfig.getHeadCommitSHA(), GHCommitState.PENDING, null, + "Scanning opensource licenses", "[Open Source License Validation]"); + } catch (IOException e) { + LOG.error("Can't authorize setPendingCheck()" + e); + } + } + + public void setErrorCheck(WebhookConfig webhookConfig) { + try { + if (GITHUB_API_URL.isEmpty()) gitHub = GitHub.connect(GITHUB_LOGIN, GITHUB_AUTH_TOKEN); + else gitHub = GitHub.connectToEnterpriseWithOAuth(GITHUB_API_URL, GITHUB_LOGIN, GITHUB_AUTH_TOKEN); + GHRepository repository = gitHub.getRepository(webhookConfig.getRepositoryOrganization() + "/" + + webhookConfig.getRepositoryName()); + repository.createCommitStatus(webhookConfig.getHeadCommitSHA(), GHCommitState.ERROR, null, + "Scanning process failed", "[Open Source License Validation]"); + } catch (IOException e) { + LOG.error("Can't authorize setErrorCheck() " + e); + } + } + + public void commentResults(WebhookConfig webhookConfig, List scanResults, List> conflicts) { + try { + GHRepository repository = gitHub.getRepository(webhookConfig.getRepositoryOrganization() + "/" + + webhookConfig.getRepositoryName()); + GHPullRequest pullRequest = getPullRequest(webhookConfig, repository); + + if (pullRequest == null){ + LOG.error("Can't find pull request " + webhookConfig.getPullRequestAPIUrl()); + return; + } + if (scanResults.isEmpty()) { + pullRequest.comment("**\\[Open Source License Validation\\]** No license issue detected \n"); + repository.createCommitStatus(webhookConfig.getHeadCommitSHA(), GHCommitState.SUCCESS, null, + "No license issue detected", "[Open Source License Validation]"); + } else { + boolean hasProhibitedOrRestricted = false; + boolean hasConflicts = false; + String commitComment = "**Detected licenses:**\n\n\n"; + for (LPVSFile file : scanResults) { + commitComment += "**File:** " + file.getFilePath() + "\n"; + commitComment += "**License(s):** " + file.convertLicensesToString() + "\n"; + commitComment += "**Component:** " + file.getComponent() + " (" + file.getFileUrl() + ")\n"; + commitComment += "**Matched Lines:** " + getMatchedLinesAsLink(webhookConfig, file) + "\n"; + commitComment += "**Snippet Match:** " + file.getSnippetMatch() + "\n\n\n\n"; + for (LPVSLicense license : file.getLicenses()) { + if (license.getAccess().equalsIgnoreCase("prohibited") + || license.getAccess().equalsIgnoreCase("restricted") + || license.getAccess().equalsIgnoreCase("unreviewed")) { + hasProhibitedOrRestricted = true; + } + } + } + + if (conflicts.size() > 0) { + hasConflicts = true; + commitComment += "**Detected license conflicts:**\n\n\n"; + commitComment += "
    "; + for (LicenseService.Conflict conflict : conflicts) { + + commitComment += "
  • " + conflict.l1 + " and " + conflict.l2 + "
  • "; + } + commitComment += "
"; + } + + if (hasProhibitedOrRestricted || hasConflicts) { + pullRequest.comment("**\\[Open Source License Validation\\]** Potential license problem(s) detected \n\n" + + commitComment + "\n"); + repository.createCommitStatus(webhookConfig.getHeadCommitSHA(), GHCommitState.FAILURE, null, + "Potential license problem(s) detected", "[Open Source License Validation]"); + } else { + pullRequest.comment("**\\[Open Source License Validation\\]** No license issue detected \n\n" + + commitComment + "\n"); + repository.createCommitStatus(webhookConfig.getHeadCommitSHA(), GHCommitState.SUCCESS, null, + "No license issue detected", "[Open Source License Validation]"); + } + } + } catch (IOException e) { + LOG.error("Can't authorize commentResults() " + e); + } catch (NullPointerException e) { + LOG.error("There is no commit in pull request"); + } + } + + public String getRepositoryLicense(WebhookConfig webhookConfig) { + try { + String repositoryName = webhookConfig.getRepositoryName(); + String repositoryOrganization = webhookConfig.getRepositoryOrganization(); + if (GITHUB_API_URL.isEmpty()) gitHub = GitHub.connect(GITHUB_LOGIN, GITHUB_AUTH_TOKEN); + else gitHub = GitHub.connectToEnterpriseWithOAuth(GITHUB_API_URL, GITHUB_LOGIN, GITHUB_AUTH_TOKEN); + GHRepository repository = gitHub.getRepository(repositoryOrganization + "/" + repositoryName); + GHLicense license = repository.getLicense(); + if (license == null) { + return "Proprietary"; + } else { + return license.getKey(); + } + } catch (IOException e) { + LOG.error("Can't authorize getRepositoryLicense() " + e); + } + return "Proprietary"; + } + + public String getMatchedLinesAsLink(WebhookConfig webhookConfig, LPVSFile file) { + String prefix = webhookConfig.getRepositoryUrl() + "/blob/" + webhookConfig.getHeadCommitSHA() + "/" + file.getFilePath(); + String matchedLines = new String(); + if (file.getMatchedLines().equals("all")) { + return "" + file.getMatchedLines() + ""; + } + prefix = prefix.concat("#L"); + for (String lineInfo : file.getMatchedLines().split(",")){ + String link = prefix+lineInfo.replace('-','L'); + matchedLines = matchedLines.concat("" + lineInfo + " "); + } + LOG.debug("MatchedLines: " + matchedLines); + return matchedLines; + } + +} diff --git a/src/main/java/com/lpvs/service/LicenseService.java b/src/main/java/com/lpvs/service/LicenseService.java new file mode 100644 index 00000000..811b8fae --- /dev/null +++ b/src/main/java/com/lpvs/service/LicenseService.java @@ -0,0 +1,175 @@ +/** + * Copyright (c) 2022, Samsung Research. All rights reserved. + * + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.lpvs.service; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.lpvs.entity.LPVSFile; +import com.lpvs.entity.LPVSLicense; +import com.lpvs.entity.config.WebhookConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import javax.annotation.PostConstruct; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; + +@Service +public class LicenseService { + + @Value("${license_conflict:json}") + public String licenseConflictsSource; + + private static Logger LOG = LoggerFactory.getLogger(LicenseService.class); + + private List licenses; + + private List> licenseConflicts; + + @PostConstruct + private void init() { + try { + // 1. Load licenses + // create Gson instance + Gson gson = new Gson(); + // create a reader + Reader reader = Files.newBufferedReader(Paths.get("classes/licenses.json")); + // convert JSON array to list of licenses + licenses = new Gson().fromJson(reader, new TypeToken>() {}.getType()); + // print info + LOG.info("LICENSES: loaded " + licenses.size() + " licenses from JSON file."); + // close reader + reader.close(); + + // 2. Load license conflicts + licenseConflicts = new ArrayList<>(); + + if (licenseConflictsSource.equalsIgnoreCase("json")) { + for (LPVSLicense license : licenses) { + if (license.getIncompatibleWith() != null && !license.getIncompatibleWith().isEmpty()) { + for (String lic : license.getIncompatibleWith()) { + Conflict conf = new Conflict<>(license.getSpdxId(), lic); + if (!licenseConflicts.contains(conf)) { + licenseConflicts.add(conf); + } + } + } + } + LOG.info("LICENSE CONFLICTS: loaded " + licenseConflicts.size() + " license conflicts."); + } + + } catch (Exception ex) { + LOG.error(ex.toString()); + licenses = new ArrayList<>(); + licenseConflicts = new ArrayList<>(); + } + } + + public LPVSLicense findLicenseBySPDX(String name) { + for (LPVSLicense license : licenses) { + if (license.getSpdxId().equalsIgnoreCase(name)) { + return license; + } + } + return null; + } + + public LPVSLicense findLicenseByName(String name) { + for (LPVSLicense license : licenses) { + if (license.getLicenseName().equalsIgnoreCase(name)) { + return license; + } + } + return null; + } + + public void addLicenseConflict(String license1, String license2) { + Conflict conf = new Conflict<>(license1, license2); + if (!licenseConflicts.contains(conf)) { + licenseConflicts.add(conf); + } + } + + + public LPVSLicense checkLicense(String spdxId) { + LPVSLicense newLicense = findLicenseBySPDX(spdxId); + if (newLicense == null && spdxId.contains("+")) { + newLicense = findLicenseBySPDX(spdxId.replace("+","") + "-or-later"); + } + if (newLicense == null && spdxId.contains("+")) { + newLicense = findLicenseBySPDX(spdxId + "-only"); + } + return newLicense; + } + + public List> findConflicts(WebhookConfig webhookConfig, List scanResults) { + + if (scanResults.isEmpty() || licenseConflicts.isEmpty()) { + return null; + } + + // 0. Extract the set of detected licenses from scan results + List detectedLicenses = new ArrayList<>(); + for (LPVSFile result : scanResults) { + for (LPVSLicense license : result.getLicenses()) { + detectedLicenses.add(license.getSpdxId()); + } + } + // leave license SPDX IDs without repetitions + Set detectedLicensesUnique = new HashSet<>(detectedLicenses); + + // 1. Check conflict between repository license and detected licenses + List> foundConflicts = new ArrayList<>(); + String repositoryLicense = webhookConfig.getRepositoryLicense(); + if (repositoryLicense != null) { + for (String detectedLicenseUnique : detectedLicensesUnique) { + for (Conflict licenseConflict : licenseConflicts) { + Conflict possibleConflict = new Conflict(detectedLicenseUnique, repositoryLicense); + if (licenseConflict.equals(possibleConflict)) { + foundConflicts.add(possibleConflict); + } + } + } + } + + // 2. Check conflict between detected licenses + for (int i = 0; i < detectedLicensesUnique.size(); i++) { + for (int j = i + 1; j < detectedLicensesUnique.size() - 1; j++) { + for (Conflict licenseConflict : licenseConflicts) { + Conflict possibleConflict = new Conflict(detectedLicensesUnique.toArray()[i], detectedLicensesUnique.toArray()[j]); + if (licenseConflict.equals(possibleConflict)) { + foundConflicts.add(possibleConflict); + } + } + } + } + + return foundConflicts; + } + + public class Conflict { + public License1 l1; + public License2 l2; + Conflict(License1 l1, License2 l2) { + this.l1 = l1; + this.l2 = l2; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Conflict conflict = (Conflict) o; + return (l1.equals(conflict.l1) && l2.equals(conflict.l2)) || + (l1.equals(conflict.l2) && l2.equals(conflict.l1)); + } + } +} diff --git a/src/main/java/com/lpvs/service/QueueProcessorService.java b/src/main/java/com/lpvs/service/QueueProcessorService.java new file mode 100644 index 00000000..c9e682de --- /dev/null +++ b/src/main/java/com/lpvs/service/QueueProcessorService.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2022, Samsung Research. All rights reserved. + * + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.lpvs.service; + +import com.lpvs.entity.config.WebhookConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; + +import java.util.*; + + +@Service +public class QueueProcessorService { + + @Autowired + private QueueService queueService; + + private static Logger LOG = LoggerFactory.getLogger(QueueProcessorService.class); + + @EventListener(ApplicationReadyEvent.class) + private void queueProcessor() throws Exception { + while (true) { + WebhookConfig webhookConfig = queueService.getQueueFirstElement(); + LOG.info("PROCESS Webhook id = " + webhookConfig.getWebhookId()); + webhookConfig.setDate(new Date()); + queueService.processWebHook(webhookConfig); + + } + } +} diff --git a/src/main/java/com/lpvs/service/QueueService.java b/src/main/java/com/lpvs/service/QueueService.java new file mode 100644 index 00000000..03fa6756 --- /dev/null +++ b/src/main/java/com/lpvs/service/QueueService.java @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2022, Samsung Research. All rights reserved. + * + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.lpvs.service; + +import com.lpvs.entity.LPVSFile; +import com.lpvs.entity.config.WebhookConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import java.util.*; +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.LinkedBlockingDeque; + +@Service +public class QueueService { + + @Autowired + private GitHubService gitHubService; + + @Autowired + private DetectService detectService; + + @Autowired + private LicenseService licenseService; + + private static Logger LOG = LoggerFactory.getLogger(QueueService.class); + + private static final BlockingDeque QUEUE = new LinkedBlockingDeque<>(); + + public void addFirst(WebhookConfig webhookConfig) throws InterruptedException { + QUEUE.putFirst(webhookConfig); + } + + public void delete(WebhookConfig webhookConfig) { + QUEUE.remove(webhookConfig); + } + + public WebhookConfig getQueueFirstElement() throws InterruptedException { + return QUEUE.takeFirst(); + } + + @Async("threadPoolTaskExecutor") + public void processWebHook(WebhookConfig webhookConfig) throws Exception { + try { + LOG.info(webhookConfig.toString()); + + String filePath = gitHubService.getPullRequestFiles(webhookConfig); + LOG.info("Successfully downloaded files from GitHub"); + + if (filePath != null) { + if (filePath.contains(":::::")) { + filePath = filePath.split(":::::")[0]; + } + // check repository license + String repositoryLicense = gitHubService.getRepositoryLicense(webhookConfig); + if (licenseService.checkLicense(repositoryLicense) != null) { + webhookConfig.setRepositoryLicense(licenseService.checkLicense(repositoryLicense).getSpdxId()); + } else if (licenseService.findLicenseByName(repositoryLicense) != null) { + webhookConfig.setRepositoryLicense(licenseService.findLicenseByName(repositoryLicense).getSpdxId()); + } else { + webhookConfig.setRepositoryLicense(null); + } + LOG.info("Repository license: " + webhookConfig.getRepositoryLicense()); + + List files = detectService.runScan(webhookConfig, filePath); + + // check license conflicts + List> detectedConflicts = licenseService.findConflicts(webhookConfig, files); + + LOG.info("Creating comment"); + gitHubService.commentResults(webhookConfig, files, detectedConflicts); + LOG.info("Results posted on GitHub"); + } else { + LOG.info("Files are not found. Probably pull request is not exists."); + gitHubService.commentResults(webhookConfig, new ArrayList<>(), new ArrayList<>()); + delete(webhookConfig); + throw new Exception("Files are not found. Probably pull request is not exists. Terminating."); + } + delete(webhookConfig); + } catch (Exception | Error e) { + LOG.error(e.toString()); + gitHubService.setErrorCheck(webhookConfig); + delete(webhookConfig); + } + } + +} diff --git a/src/main/java/com/lpvs/service/scanner/scanoss/ScanossDetectService.java b/src/main/java/com/lpvs/service/scanner/scanoss/ScanossDetectService.java new file mode 100644 index 00000000..e02e7029 --- /dev/null +++ b/src/main/java/com/lpvs/service/scanner/scanoss/ScanossDetectService.java @@ -0,0 +1,190 @@ +/** + * Copyright (c) 2022, Samsung Research. All rights reserved. + * + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.lpvs.service.scanner.scanoss; + +import com.google.gson.*; +import com.lpvs.entity.LPVSFile; +import com.lpvs.entity.LPVSLicense; +import com.lpvs.entity.config.WebhookConfig; +import com.lpvs.service.GitHubService; +import com.lpvs.service.LicenseService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; + +@Service +public class ScanossDetectService { + + @Autowired + private LicenseService licenseService; + + @Autowired + private GitHubService gitHubService; + + private static Logger LOG = LoggerFactory.getLogger(ScanossDetectService.class); + + public void runScan(WebhookConfig webhookConfig, String path) throws Exception { + LOG.info("Starting Scanoss scanning"); + + try { + ProcessBuilder processBuilder; + if (!(new File("RESULTS").exists())) { + new File("RESULTS").mkdir(); + } + processBuilder = new ProcessBuilder( + "scanoss-py", "scan", + "-t", + "--no-wfp-output", + "-o", "RESULTS/" + webhookConfig.getRepositoryName() + "_" + webhookConfig.getHeadCommitSHA() + ".json", + path + ); + Process process = processBuilder.inheritIO().start(); + + int status = process.waitFor(); + + if (status == 1) { + LOG.error("Scanoss scanner terminated with none-zero code. Terminating."); + BufferedReader output = new BufferedReader(new InputStreamReader(process.getErrorStream())); + LOG.error(output.readLine()); + throw new Exception("Scanoss scanner terminated with none-zero code. Terminating."); + } + } catch (IOException | InterruptedException ex) { + LOG.error("Scanoss scanner terminated with none-zero code. Terminating."); + throw ex; + } + + LOG.info("Scanoss scan done"); + } + + public List checkLicenses(WebhookConfig webhookConfig) { + List detectedFiles = new ArrayList<>(); + try { + Gson gson = new Gson(); + Reader reader = Files.newBufferedReader(Paths.get("RESULTS/" + webhookConfig.getRepositoryName() + "_" + webhookConfig.getHeadCommitSHA() + ".json")); + // convert JSON file to map + Map map = gson.fromJson(reader, Map.class); + + // parse map entries + Long ind = 0L; + for (Map.Entry entry : map.entrySet()) { + LPVSFile file = new LPVSFile(); + file.setId(ind++); + file.setFilePath(entry.getKey().toString()); + + String content = entry.getValue().toString() + .replaceAll("=\\[", "\" : [") + .replaceAll("=", "\" : \"") + .replaceAll("\\}, \\{", "\"},{") + .replaceAll("\\}\\],", "\"}],") + .replaceAll("\\{", "{\"") + .replaceAll(", ", "\", \"") + .replaceAll("\\]\"","]") + .replaceAll("}\",", "\"},") + .replaceAll(": \\[", ": [\"") + .replaceAll("],", "\"],") + .replaceAll("\"\\{\"","{\"") + .replaceAll("\"}\"]", "\"}]") + .replaceAll("incompatible_with\" : (\".*?\"), \"name", "incompatible_with\" : \\[$1\\], \"name") + ; + content = content.substring(1, content.length() - 1); + if(content.endsWith("}")) + { + content = content.substring(0, content.length() - 1) + "\"}"; + } + content = content.replaceAll("}\"}", "\"}}"); + ScanossJsonStructure object = gson.fromJson(content, ScanossJsonStructure.class); + if (object.file_url != null) file.setFileUrl(object.file_url); + if (object.component != null) file.setComponent(object.component); + if (object.lines != null) file.setMatchedLines(object.lines); + if (object.matched != null) file.setSnippetMatch(object.matched); + + Set licenses = new HashSet<>(); + if (object.licenses != null) { + for (ScanossJsonStructure.ScanossLicense license : object.licenses) { + // Check detected licenses + LPVSLicense lic = licenseService.findLicenseBySPDX(license.name); + if (lic != null) { + lic.setChecklist_url(license.checklist_url); + licenses.add(lic); + } else { + licenses.add(new LPVSLicense(0L, license.name, license.name, "UNREVIEWED", license.checklist_url, new ArrayList<>())); + } + + // Check for the license conflicts if the property "license_conflict=scanner" + if (licenseService.licenseConflictsSource.equalsIgnoreCase("scanner")) { + if (license.incompatible_with != null) { + for (String incompatibleLicense : license.incompatible_with) { + licenseService.addLicenseConflict(incompatibleLicense, license.name); + } + } + } + } + } + file.setLicenses(new HashSet<>(licenses)); + if (!file.getLicenses().isEmpty()) detectedFiles.add(file); + } + + // close reader + reader.close(); + } catch (Exception ex) { + LOG.error(ex.toString()); + } + return detectedFiles; + } + + // Scanoss JSON structure + private class ScanossJsonStructure { + private String component; + private String file; + private String file_hash; + private String file_url; + private String id; + private String latest; + private ArrayList licenses; + private String lines; + private String matched; + private String oss_lines; + private ArrayList purl; + private String release_date; + private ScanossServer server; + private String source_hash; + private String status; + private String url; + private String url_hash; + private String vendor; + private String version; + + private class ScanossLicense { + private String checklist_url; + private String copyleft; + private ArrayList incompatible_with; + private String name; + private String osadl_updated; + private String patent_hints; + private String source; + private String url; + } + + private class ScanossServer { + private KbVersion kb_version; + private String version; + + private class KbVersion { + private String daily; + private String monthly; + } + } + } + + +} diff --git a/src/main/java/com/lpvs/util/FileUtil.java b/src/main/java/com/lpvs/util/FileUtil.java new file mode 100644 index 00000000..06d05ba9 --- /dev/null +++ b/src/main/java/com/lpvs/util/FileUtil.java @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2022, Samsung Research. All rights reserved. + * + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.lpvs.util; + +import org.kohsuke.github.GHPullRequestFileDetail; +import org.kohsuke.github.PagedIterable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.FileSystemUtils; + +import java.io.File; +import java.io.FileWriter; +import java.io.BufferedWriter; +import java.io.IOException; + +public class FileUtil { + private static Logger LOG = LoggerFactory.getLogger(FileUtil.class); + + public static String saveFiles(PagedIterable files, String folder, String headCommitSHA, int deletions) { + + String directoryPath = "Projects/" + folder + "/" + headCommitSHA; + String directoryDeletionPath = ""; + if (deletions > 0) { + directoryDeletionPath = directoryPath + "delete"; + } + + try { + deleteIfExists(directoryPath); + boolean result = new File(directoryPath).mkdirs(); + boolean delResult = true; + if (deletions > 0) { + deleteIfExists(directoryDeletionPath); + delResult = new File(directoryDeletionPath).mkdirs(); + } + if (result && delResult) { + for (GHPullRequestFileDetail file : files) { + String patch = file.getPatch(); + if (patch == null) { + LOG.error("NULL PATCH for file "+ file.getFilename()); + continue; + } + int cnt = 1; + StringBuilder prettyPatch = new StringBuilder(); + StringBuilder prettyPatchDeletion = new StringBuilder(); + for (String patchString : patch.split("\n")) { + // added line + if (patchString.charAt(0) == '+') { + prettyPatch.append(patchString.substring(patchString.indexOf("+") + 1)); + prettyPatch.append("\n"); + cnt++; + } + // removed line + else if (patchString.charAt(0) == '-') { + prettyPatchDeletion.append(patchString.substring(patchString.indexOf("-") + 1)); + prettyPatchDeletion.append("\n"); + } + // information(location, number of lines) about changed lines + else if (patchString.charAt(0) == '@') { + int fIndex = patchString.indexOf("+") + 1; + int lIndex = patchString.indexOf(',', fIndex); + if (lIndex == -1) lIndex = patchString.indexOf(' ', fIndex); + int startLine = Integer.parseInt(patchString.substring(fIndex, lIndex)); + LOG.debug("Line from: " + startLine + " Git string: " + patchString); + for (int i = cnt; i < startLine; i++) { + prettyPatch.append("\n"); + } + cnt=startLine; + } + // unchanged line + else if (patchString.charAt(0) == ' ') { + prettyPatch.append("\n"); + cnt++; + } + } + String filename = file.getFilename(); + if (filename.contains("/")) { + String filepath = filename.substring(0,filename.lastIndexOf("/")); + new File(directoryPath + "/" + filepath).mkdirs(); + if (prettyPatchDeletion.length() > 0) { + new File(directoryDeletionPath+ "/" + filepath).mkdirs(); + } + } + + if (prettyPatch.length() > 0) { + BufferedWriter writer = new BufferedWriter(new FileWriter(directoryPath + "/" + filename)); + writer.write(prettyPatch.toString()); + writer.close(); + } + + if (prettyPatchDeletion.length() > 0) { + BufferedWriter writer = new BufferedWriter(new FileWriter(directoryDeletionPath + "/" + filename)); + writer.write(prettyPatchDeletion.toString()); + writer.close(); + } + } + } + } catch (IOException e) { + LOG.error("Error while writing file. " + e.getMessage()); + } + + if (deletions > 0) { + return directoryPath + ":::::" + directoryDeletionPath; + } else { + return directoryPath; + } + } + + public static void deleteIfExists (String path) { + File dir = new File(path); + if (dir.exists()) { + FileSystemUtils.deleteRecursively(dir); + } + } +} diff --git a/src/main/java/com/lpvs/util/WebhookUtil.java b/src/main/java/com/lpvs/util/WebhookUtil.java new file mode 100644 index 00000000..ef297639 --- /dev/null +++ b/src/main/java/com/lpvs/util/WebhookUtil.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2022, Samsung Research. All rights reserved. + * + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.lpvs.util; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.lpvs.entity.config.WebhookConfig; +import com.lpvs.entity.enums.PullRequestAction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WebhookUtil { + + private static Logger LOG = LoggerFactory.getLogger(WebhookUtil.class); + + public static WebhookConfig getGitHubWebhookConfig(String payload) { + Gson gson = new Gson(); + WebhookConfig webhookConfig = new WebhookConfig(); + + JsonObject json = gson.fromJson(payload, JsonObject.class); + webhookConfig.setAction(PullRequestAction.convertFrom(json.get("action").getAsString())); + webhookConfig.setRepositoryName(json.getAsJsonObject("repository") + .get("name").getAsString()); + webhookConfig.setRepositoryOrganization(json.getAsJsonObject("repository") + .get("full_name").getAsString() + .split("/")[0]); + String url = json.getAsJsonObject("pull_request").get("html_url").getAsString(); + webhookConfig.setPullRequestUrl(url); + if (json.getAsJsonObject("pull_request").getAsJsonObject("head").getAsJsonObject("repo").get("fork").getAsBoolean()) { + webhookConfig.setPullRequestFilesUrl(json.getAsJsonObject("pull_request").getAsJsonObject("head").getAsJsonObject("repo").get("html_url").getAsString()); + } else { + webhookConfig.setPullRequestFilesUrl(webhookConfig.getPullRequestUrl()); + } + webhookConfig.setPullRequestAPIUrl(json.getAsJsonObject("pull_request").get("url").getAsString()); + webhookConfig.setRepositoryUrl(json.getAsJsonObject("repository").get("html_url").getAsString()); + webhookConfig.setUserId("bot"); + webhookConfig.setPullRequestId(Long.parseLong(url.split("/")[url.split("/").length -1])); + webhookConfig.setHeadCommitSHA(json.getAsJsonObject("pull_request") + .getAsJsonObject("head") + .get("sha").getAsString()); + webhookConfig.setPullRequestBranch(json.getAsJsonObject("pull_request") + .getAsJsonObject("head") + .get("ref").getAsString()); + webhookConfig.setAttempts(0); + + return webhookConfig; + } + + public static boolean checkPayload(String payload) { + if (payload.contains("\"zen\":")){ + LOG.info("Initial webhook received"); + return false; + } + + LOG.info("PAYLOAD: " + payload); + + Gson gson = new Gson(); + JsonObject json = gson.fromJson(payload, JsonObject.class); + String actionString = json.get("action").getAsString(); + LOG.info("Action " + actionString); + PullRequestAction action = PullRequestAction.convertFrom(actionString); + //ToDo: handle all action types + return (action != null) && (action.equals(PullRequestAction.UPDATE) || action.equals(PullRequestAction.OPEN) + || action.equals(PullRequestAction.REOPEN)); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 00000000..1a375e88 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,15 @@ +####################################################################################################### +### LPVS Configuration ## +####################################################################################################### +server.port=7896 +# Used scanner name +scanner=scanoss +# license_conflict: json or scanner +license_conflict=json +# GitHub settings +github.login= +github.token= +github.api.url= + +# Used Core Pool Size +lpvs.cores=8 \ No newline at end of file diff --git a/src/main/resources/licenses.json b/src/main/resources/licenses.json new file mode 100644 index 00000000..e2abcdf9 --- /dev/null +++ b/src/main/resources/licenses.json @@ -0,0 +1,33 @@ +[ + { + "id": 0, + "licenseName": "Apache License 2.0", + "spdxId": "Apache-2.0", + "access": "PERMITTED" + }, + { + "id": 1, + "licenseName": "GNU General Public License v3.0 only", + "spdxId": "GPL-3.0-only", + "access": "PROHIBITED" + }, + { + "id": 2, + "licenseName": "OpenSSL License", + "spdxId": "OpenSSL", + "access": "PERMITTED" + }, + { + "id": 3, + "licenseName": "GNU Lesser General Public License v2.1 or later", + "spdxId": "GPL-2.0-or-later", + "incompatibleWith": ["Apache-1.0", "Apache-1.1", "Apache-2.0", "BSD-4-Clause", "BSD-4-Clause-UC", "FTL", "IJG", "MIT","OpenSSL", "Python-2.0", "zlib-acknowledgement", "XFree86-1.1", "GPL-1.0-or-later"], + "access": "RESTRICTED" + }, + { + "id": 4, + "licenseName": "MIT License", + "spdxId": "MIT", + "access": "PERMITTED" + } +] \ No newline at end of file