commit e38227f6d50d0c196dd975296f466376c90b79a3 Author: retanoj <> Date: Tue Jul 28 15:39:10 2020 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88efb8c --- /dev/null +++ b/.gitignore @@ -0,0 +1,110 @@ + +# Created by https://www.gitignore.io/api/gradle,intellij+all +# Edit at https://www.gitignore.io/?templates=gradle,intellij+all + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Gradle ### +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +### Gradle Patch ### +**/build/ + +# End of https://www.gitignore.io/api/gradle,intellij+all diff --git a/CHANGES b/CHANGES new file mode 100644 index 0000000..a1c2210 --- /dev/null +++ b/CHANGES @@ -0,0 +1,34 @@ +Changelog + +========= + +Verson 1.1.3 + +- deps add gson +- feature add Renderer +- test add Unittests + +Version 1.1.2 + +- feature default BUILD FAIL when found vulnerable + +Version 1.1.1 + +- feature BUILD FAILED when found vulnerable && parameter support + +Version 1.1.0 + +- feature add projectType param, support Maven & Android project + +Version 1.0.2 + +- feature warning detail add title and cve + +Version 1.0.1 + +- feature add onlyProvenance parameter +- feature add http timeout + +Version 1.0.0 + +- Init diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1bcd69a --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2020 momosecurity. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..16bdfb7 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# MOSEC-GRADLE-PLUGIN + +用于检测gradle项目的第三方依赖组件是否存在安全漏洞。 + +该项目灵感来自 [snyk-gradle-plugin](https://github.com/snyk/snyk-gradle-plugin.git) 。 + +## 版本支持 + +Gradle >= 3.0 + +## 安装 + +向顶层build.gradle中增加如下配置 +```groovy +// file: build.gradle + +buildscript { + repositories { + maven { url "https://raw.github.com/momosecurity/mosec-gradle-plugin/master/mvn-repo/" } + } + + dependencies { + classpath 'com.immomo.momosec:mosec-gradle-plugin:1.1.3' + } +} + +allprojects { + apply plugin: 'mosec' +} +``` + +## 使用 + +首先运行 [MOSEC-X-PLUGIN Backend](https://github.com/momosecurity/mosec-x-plugin-backend.git) + +#### 命令行使用 + +```shell script +# 对于所有projects,Maven项目 +> MOSEC_ENDPOINT=http://127.0.0.1:9000/api/plugin \ + ./gradlew --no-parallel \ + mosec -PprojectType=Maven -PonlyProvenance=true + +# 对于单个project (如 demo),Maven项目 +> MOSEC_ENDPOINT=http://127.0.0.1:9000/api/plugin \ + ./gradlew --no-parallel \ + :demo:mosec -PprojectType=Maven -PonlyProvenance=true + +# 对于 Android 项目,使用 confAttr 参数 +> MOSEC_ENDPOINT=http://127.0.0.1:9000/api/plugin \ + ./gradlew --no-parallel \ + mosec -PprojectType=Android -PconfAttr=buildtype:release -PonlyProvenance=true +``` + +## 开发 + +#### Intellij 远程调试 Gradle 插件 + +1.将mosec-gradle-plugin安装至本地仓库 + +2.git clone mosec-gradle-plugin + +3.Intellij 中新建 Remote Configuration 并填入如下信息 + +![remote-configuration](https://github.com/momosecurity/mosec-gradle-plugin/blob/master/static/remote-configuration.jpg) + +4.在另一个gradle工程中执行如下命令 + +```shell script +> ./gradlew --no-parallel --no-daemon mosec -Dorg.gradle.debug=true +``` + +5.回到Intellij中,下断点,开始Debug \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..347e2ea --- /dev/null +++ b/build.gradle @@ -0,0 +1,43 @@ +plugins { + id 'groovy' + id 'maven' +} + +group 'com.immomo.momosec' +version '1.1.3' + +repositories { + mavenCentral() +} + +dependencies { + compile gradleApi() + compile localGroovy() + + compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5' + compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.10' + + testCompile group: 'junit', name: 'junit', version: '4.12' + testCompile group: 'org.mockito', name: 'mockito-core', version: '2.28.2' + testImplementation gradleTestKit() +} + +// local maven repo install +install { + repositories.mavenInstaller { + pom.groupId = "$project.group" + pom.artifactId = "$project.name" + pom.version = "$project.version" + } +} + +// remote private maven repo deploy +uploadArchives { + repositories.mavenDeployer { + repository(url: "file://mvn-repo") + + pom.groupId = "$project.group" + pom.artifactId = "$project.name" + pom.version = "$project.version" + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..cd5b3d5 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +NEXUS_USERNAME= +NEXUS_PASSWORD= \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..87b738c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..c054594 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue May 26 14:27:38 CST 2020 +distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..af6708f --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..0f8d593 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/1.1.3/mosec-gradle-plugin-1.1.3.jar b/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/1.1.3/mosec-gradle-plugin-1.1.3.jar new file mode 100644 index 0000000..ab7edd5 Binary files /dev/null and b/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/1.1.3/mosec-gradle-plugin-1.1.3.jar differ diff --git a/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/1.1.3/mosec-gradle-plugin-1.1.3.jar.md5 b/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/1.1.3/mosec-gradle-plugin-1.1.3.jar.md5 new file mode 100644 index 0000000..b5f9856 --- /dev/null +++ b/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/1.1.3/mosec-gradle-plugin-1.1.3.jar.md5 @@ -0,0 +1 @@ +63a54e2e32ae94d5efdd5d474a47fae5 \ No newline at end of file diff --git a/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/1.1.3/mosec-gradle-plugin-1.1.3.jar.sha1 b/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/1.1.3/mosec-gradle-plugin-1.1.3.jar.sha1 new file mode 100644 index 0000000..57e325a --- /dev/null +++ b/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/1.1.3/mosec-gradle-plugin-1.1.3.jar.sha1 @@ -0,0 +1 @@ +f28f856789fbe4385c027d4f26ed8ab8c7e746f0 \ No newline at end of file diff --git a/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/1.1.3/mosec-gradle-plugin-1.1.3.pom b/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/1.1.3/mosec-gradle-plugin-1.1.3.pom new file mode 100644 index 0000000..2b7474d --- /dev/null +++ b/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/1.1.3/mosec-gradle-plugin-1.1.3.pom @@ -0,0 +1,34 @@ + + + 4.0.0 + com.immomo.momosec + mosec-gradle-plugin + 1.1.3 + + + com.google.code.gson + gson + 2.8.5 + compile + + + org.apache.httpcomponents + httpclient + 4.5.10 + compile + + + junit + junit + 4.12 + test + + + org.mockito + mockito-core + 2.28.2 + test + + + diff --git a/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/1.1.3/mosec-gradle-plugin-1.1.3.pom.md5 b/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/1.1.3/mosec-gradle-plugin-1.1.3.pom.md5 new file mode 100644 index 0000000..0fc13a0 --- /dev/null +++ b/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/1.1.3/mosec-gradle-plugin-1.1.3.pom.md5 @@ -0,0 +1 @@ +e092d0c7163e87bb55fabe6ae4fe2bf0 \ No newline at end of file diff --git a/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/1.1.3/mosec-gradle-plugin-1.1.3.pom.sha1 b/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/1.1.3/mosec-gradle-plugin-1.1.3.pom.sha1 new file mode 100644 index 0000000..d6ce9d1 --- /dev/null +++ b/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/1.1.3/mosec-gradle-plugin-1.1.3.pom.sha1 @@ -0,0 +1 @@ +6fbbb8db08cec3acd0ba30c14503e01b59f184bb \ No newline at end of file diff --git a/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/maven-metadata.xml b/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/maven-metadata.xml new file mode 100644 index 0000000..0856c2c --- /dev/null +++ b/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/maven-metadata.xml @@ -0,0 +1,12 @@ + + + com.immomo.momosec + mosec-gradle-plugin + + 1.1.3 + + 1.1.3 + + 20200728045957 + + diff --git a/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/maven-metadata.xml.md5 b/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/maven-metadata.xml.md5 new file mode 100644 index 0000000..5e5a14c --- /dev/null +++ b/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/maven-metadata.xml.md5 @@ -0,0 +1 @@ +cee7d6bf5c0a3d11b4d432fcacf1b8ab \ No newline at end of file diff --git a/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/maven-metadata.xml.sha1 b/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/maven-metadata.xml.sha1 new file mode 100644 index 0000000..bf7c148 --- /dev/null +++ b/mvn-repo/com/immomo/momosec/mosec-gradle-plugin/maven-metadata.xml.sha1 @@ -0,0 +1 @@ +64fb6c894314270126bbdcc2975fcfd86e194878 \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..315af11 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'mosec-gradle-plugin' + diff --git a/src/main/groovy/com/immomo/momosec/gradle/plugins/Mosec.groovy b/src/main/groovy/com/immomo/momosec/gradle/plugins/Mosec.groovy new file mode 100644 index 0000000..39113b9 --- /dev/null +++ b/src/main/groovy/com/immomo/momosec/gradle/plugins/Mosec.groovy @@ -0,0 +1,126 @@ +package com.immomo.momosec.gradle.plugins + +import com.immomo.momosec.gradle.plugins.exceptions.NetworkErrorException +import groovy.json.JsonOutput +import org.apache.http.HttpEntity +import org.apache.http.HttpResponse +import org.apache.http.client.HttpClient +import org.apache.http.client.methods.HttpPost +import org.apache.http.entity.StringEntity +import org.gradle.api.Plugin +import org.gradle.api.Project + +class Mosec implements Plugin { + + /** + * https://developer.android.com/studio/build/dependencies#variant_aware + * -PconfAttr=buildtype:debug,usage:java-runtime + */ + private List confAttrSpec = null + + /** + * 过滤configuration + * -Pconfiguration=confNameRegex or -Pconfiguration=spcificConfName + */ + private String confNameFilter = ".*" + + /** + * 威胁等级 [High|Medium|Low] + */ + private String severityLevel = 'High' + + /** + * 仅检查直接依赖 + */ + private Boolean onlyProvenance = false + + /** + * 发现漏洞即编译失败 + */ + private Boolean failOnVuln = true + + /** + * 项目类型 [Maven|Android] + * 决定了使用的检索漏洞库 + */ + private String projectType = 'Android' + private def allowProjectType = ['Maven', 'Android'] + + /** + * 上报API + */ + private String endpoint + + + @Override + void apply(Project project) { + project.task('mosec').doLast { + + if (project.hasProperty('projectType')) { + projectType = project.property('projectType').toString() + + if (!allowProjectType.contains(projectType)) { + throw new Exception(Constants.ERROR_ON_PROJECT_TYPE) + } + } + + if (project.hasProperty('endpoint')) { + endpoint = project.property('endpoint').toString() + } + + def endpoint_env = System.getenv(Constants.MOSEC_ENDPOINT_ENV) + if (endpoint_env != null) { + endpoint = endpoint_env + } + + if (endpoint == null) { + throw new RuntimeException(Constants.ERROR_ON_NULL_ENDPOINT) + } + + if (project.hasProperty('confAttr')) { + confAttrSpec = project.property('confAttr').toString().toLowerCase().split(',').collect { it.split(':') } + } + + if (project.hasProperty('configuration')) { + confNameFilter = String.format("%s", project.property('configuration').toString().toLowerCase()) + } + + if (project.hasProperty('severityLevel')) { + severityLevel = project.property('severityLevel') + } + + if (project.hasProperty('onlyProvenance')) { + onlyProvenance = new Boolean(project.property('onlyProvenance').toString()) + } + + if (project.hasProperty('failOnVuln')) { + failOnVuln = new Boolean(project.property('failOnVuln').toString()) + } + + ProjectDependencyCollector collector = new ProjectDependencyCollector(project, confAttrSpec, confNameFilter, onlyProvenance) + Map depsTree = collector.collect() + + if (depsTree == null) { return } + + depsTree['type'] = projectType + depsTree['language'] = Constants.PROJECT_LANGUAGE + depsTree['severityLevel'] = severityLevel + + HttpPost request = new HttpPost(endpoint) + request.addHeader("Content-Type", Constants.CONTENT_TYPE_JSON) + HttpEntity entity = new StringEntity(JsonOutput.toJson(depsTree)) + request.setEntity(entity) + + HttpClientHelper httpClientHelper = new HttpClientHelper() + HttpClient client = httpClientHelper.buildHttpClient() + HttpResponse response = client.execute(request) + + if (response.getStatusLine().getStatusCode() >= 400) { + throw new NetworkErrorException(response.getStatusLine().getReasonPhrase()) + } + + Renderer renderer = new Renderer(project.logger, failOnVuln) + renderer.renderResponse(response.getEntity().getContent()) + } + } +} diff --git a/src/main/groovy/com/immomo/momosec/gradle/plugins/ProjectDependencyCollector.groovy b/src/main/groovy/com/immomo/momosec/gradle/plugins/ProjectDependencyCollector.groovy new file mode 100644 index 0000000..ff0e540 --- /dev/null +++ b/src/main/groovy/com/immomo/momosec/gradle/plugins/ProjectDependencyCollector.groovy @@ -0,0 +1,182 @@ +package com.immomo.momosec.gradle.plugins + +import groovy.json.JsonOutput +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.ResolvedDependency +import org.gradle.api.attributes.Attribute + +class ProjectDependencyCollector { + + private final Project project + private final List confAttrSpec + private final String confNameFilter + private final Boolean onlyProvenance + + private final MosecLogHelper logHelper = new MosecLogHelper() + + private final def mosecMergedDepsConf = 'mosecMergedDepsConf' + + ProjectDependencyCollector(Project project, List confAttrSpec, String confNameFilter, Boolean onlyProvenance) { + this.project = project + this.confAttrSpec = confAttrSpec + this.confNameFilter = confNameFilter + this.onlyProvenance = onlyProvenance + } + + def collect() { + def projName = project.name + def projVersion = project.version + def logger = project.logger + + logger.info logHelper.strongInfo('MOSEC: ') + 'task is executing via doLast on ' + projName + + def depsTree = [ + 'name': projName, + 'version': projVersion, + 'from': [projName + '@' + projVersion], + 'dependencies': [:], + ] + + Map, Set> allConfigurationAttributes = new HashMap<>() + Map> attributesAsStrings = new HashMap<>() + project.allprojects.each { proj -> + proj.configurations.findAll({ + it.name != mosecMergedDepsConf && + it.name =~ confNameFilter && + matchesAttributeFilter.call(it) + }).each { conf -> + if (!conf.hasProperty('attributes')) { + // Gradle before version 3 does not support attributes + return + } + def attrs = conf.attributes + attrs.keySet().each({ attr -> + def value = attrs.getAttribute(attr as Attribute) + if (!allConfigurationAttributes.containsKey(attr)) { + allConfigurationAttributes[attr] = new HashSet() + attributesAsStrings[attr.name] = new HashSet() + } + allConfigurationAttributes[attr].add(value) + attributesAsStrings[attr.name].add(value.toString()) + }) + } + } + + logger.debug 'MOSEC: JSON Attr ' + JsonOutput.toJson(attributesAsStrings) + + def mosecConf = null + def mergeableConfs = project.configurations.findAll({it.name != mosecMergedDepsConf && it.name =~ confNameFilter}) + + if (confAttrSpec != null) { + mergeableConfs = mergeableConfs.findAll(matchesAttributeFilter) + } + + if (mergeableConfs.size() == 0 && project.configurations.size() > 0) { + throw new RuntimeException('MOSEC: Matching configurations not found: ' + confNameFilter + + ', availabie configurations for project ' + project + ': ' + + project.configurations.collect({ it.name })) + } else if (mergeableConfs.size() == 1) { + mosecConf = mergeableConfs.first() + } else if (mergeableConfs.size() > 1) { + logger.info logHelper.strongInfo('MOSEC: ') + 'constructing merged configuration from ' + mergeableConfs.collect({conf -> conf.name}) + mosecConf = project.configurations.create(mosecMergedDepsConf) + + mergeableConfs.each { mosecConf.extendsFrom(it) } + + if (mosecConf.hasProperty('attributes')) { + allConfigurationAttributes.each({ attr, valueSet -> + if (valueSet.size() == 1) { + mosecConf.attributes.attribute(attr, valueSet.head()) + } + }) + } + } + + if (mosecConf != null) { + logger.info logHelper.strongInfo('MOSEC: ') + 'resolving configuration ' + mosecConf.name + + Set gradleDeps = mosecConf.resolvedConfiguration.firstLevelModuleDependencies + + logger.debug 'MOSEC: converting dependency graph to DepTree' + + ArrayList from = new ArrayList(){{ add(projName + '@' + projVersion) }} + depsTree['dependencies'] = depsToDict.call(gradleDeps, from, onlyProvenance) + simplifyDeps.call(depsTree) + + logger.debug 'MOSEC: depsTree ' + JsonOutput.prettyPrint(JsonOutput.toJson(depsTree)) + return depsTree + } else { + logger.error('MOSEC: no configuration found.') + } + } + + def depsToDict = { Set deps, ArrayList currentChain, Boolean onlyProvenance -> + def res = [:] + deps.each { d -> + def depName = d.moduleGroup + ':' + d.moduleName + def depNameVersion = depName + '@' + d.moduleVersion + + if (!currentChain.contains(depNameVersion)) { + currentChain.add(depNameVersion) + def row = ['name': depName, 'version': d.moduleVersion, 'from': currentChain.clone()] + def subDeps = [:] + if (!onlyProvenance) { + subDeps = depsToDict.call(d.children, currentChain, onlyProvenance) + } + currentChain.remove(depNameVersion) + if (subDeps.size() > 0) { + row['dependencies'] = subDeps + } else { + row['dependencies'] = [:] + } + res[depName] = row + } + } + return res + } + + def simplifyDeps = { deps -> + def q = [] as Queue + def s = new HashSet() + + q.add(deps) + + def elem = [:] + while ( (elem = q.poll()) != null) { + def removeNames = [] + elem['dependencies'].each { name, d -> + def depFull = d['name'] + '@' + d['version'] + if (s.contains(depFull)) { + removeNames.add(name) + return + } + s.add(depFull as String) + q.add(d) + } + + removeNames.each {name -> + elem['dependencies'].remove(name) + } + } + } + + def matchesAttributeFilter = {Configuration conf -> + if (!conf.hasProperty('attributes')) { + // Gradle before version 3 does not support attributes + return true + } + def matches = true + def attrs = conf.attributes + attrs.keySet().each({ attr -> + def attrValueAsString = attrs.getAttribute(attr as Attribute).toString().toLowerCase() + for(String[] keyValueFilter : confAttrSpec) { + if (attr.name.toLowerCase().contains(keyValueFilter[0]) + && attrValueAsString != keyValueFilter[1]) { + matches = false + } + } + }) + return matches + } +} diff --git a/src/main/java/com/immomo/momosec/gradle/plugins/Constants.java b/src/main/java/com/immomo/momosec/gradle/plugins/Constants.java new file mode 100644 index 0000000..51fee52 --- /dev/null +++ b/src/main/java/com/immomo/momosec/gradle/plugins/Constants.java @@ -0,0 +1,18 @@ +package com.immomo.momosec.gradle.plugins; + +public class Constants { + + public static final String ERROR_ON_PROJECT_TYPE = "Project type not allow. Use [Maven | Android]."; + + public static final String ERROR_ON_VULNERABLE = "Dependency Vulnerable Found!"; + + public static final String ERROR_ON_API = "API return data format error."; + + public static final String ERROR_ON_NULL_ENDPOINT = "API endpoint not setting. Setting by MOSEC_ENDPOINT env."; + + public static final String CONTENT_TYPE_JSON = "application/json"; + + public static final String PROJECT_LANGUAGE = "java"; + + public static final String MOSEC_ENDPOINT_ENV = "MOSEC_ENDPOINT"; +} diff --git a/src/main/java/com/immomo/momosec/gradle/plugins/HttpClientHelper.java b/src/main/java/com/immomo/momosec/gradle/plugins/HttpClientHelper.java new file mode 100644 index 0000000..e96c2b8 --- /dev/null +++ b/src/main/java/com/immomo/momosec/gradle/plugins/HttpClientHelper.java @@ -0,0 +1,26 @@ +package com.immomo.momosec.gradle.plugins; + +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.LaxRedirectStrategy; + +public class HttpClientHelper { + + private final int timeout = 15 * 1000; + + public HttpClient buildHttpClient() { + RequestConfig config = RequestConfig.custom() + .setConnectTimeout(timeout) + .setConnectionRequestTimeout(timeout) + .setSocketTimeout(timeout) + .build(); + + HttpClientBuilder httpClientBuilder = HttpClientBuilder.create() + .setDefaultRequestConfig(config) + .setRedirectStrategy(new LaxRedirectStrategy()) + .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE); + return httpClientBuilder.build(); + } +} diff --git a/src/main/java/com/immomo/momosec/gradle/plugins/MosecLogHelper.java b/src/main/java/com/immomo/momosec/gradle/plugins/MosecLogHelper.java new file mode 100644 index 0000000..886095f --- /dev/null +++ b/src/main/java/com/immomo/momosec/gradle/plugins/MosecLogHelper.java @@ -0,0 +1,21 @@ +package com.immomo.momosec.gradle.plugins; + +public class MosecLogHelper { + private static final String YELLOW = "\033[1;33m"; + private static final String LIGHT_RED = "\033[1;31m"; + private static final String LIGHT_GREEN = "\033[1;32m"; + + private static final String CANCEL_COLOR = "\033[0m"; + + public String strongWarning(String content) { + return YELLOW + content + CANCEL_COLOR; + } + + public String strongError(String content) { + return LIGHT_RED + content + CANCEL_COLOR; + } + + public String strongInfo(String content) { + return LIGHT_GREEN + content + CANCEL_COLOR; + } +} diff --git a/src/main/java/com/immomo/momosec/gradle/plugins/Renderer.java b/src/main/java/com/immomo/momosec/gradle/plugins/Renderer.java new file mode 100644 index 0000000..741afd3 --- /dev/null +++ b/src/main/java/com/immomo/momosec/gradle/plugins/Renderer.java @@ -0,0 +1,78 @@ +package com.immomo.momosec.gradle.plugins; + +import com.immomo.momosec.gradle.plugins.exceptions.FoundVulnerableException; +import com.immomo.momosec.gradle.plugins.exceptions.NetworkErrorException; +import org.gradle.api.logging.Logger; +import com.google.gson.*; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; + +public class Renderer { + + private final MosecLogHelper logHelper = new MosecLogHelper(); + + private final Logger log; + private final Boolean failOnVuln; + + public Renderer(Logger log, Boolean failOnVuln) { + this.log = log; + this.failOnVuln = failOnVuln; + } + + public void renderResponse(InputStream in) { + JsonParser parser = new JsonParser(); + JsonObject responseJson; + try { + responseJson = parser.parse(new BufferedReader(new InputStreamReader(in))).getAsJsonObject(); + } catch (Exception e) { + throw new NetworkErrorException(Constants.ERROR_ON_API); + } + + if(responseJson.get("ok") != null && responseJson.get("ok").getAsBoolean()) { + String ok = "✓ Tested %s dependencies, no vulnerable found."; + getLog().warn(logHelper.strongInfo(String.format(ok, responseJson.get("dependencyCount").getAsString()))); + } else if (responseJson.get("vulnerabilities") != null) { + JsonArray vulns = (JsonArray) responseJson.get("vulnerabilities"); + + for (JsonElement vuln : vulns) { + printSingleVuln(vuln.getAsJsonObject()); + } + + String fail = "Tested %s dependencies, found %d vulnerable pathes."; + getLog().warn(logHelper.strongWarning(String.format(fail, responseJson.get("dependencyCount").getAsString(), vulns.size()))); + if (Boolean.TRUE.equals(failOnVuln)) { + throw new FoundVulnerableException(Constants.ERROR_ON_VULNERABLE); + } + } + } + + private void printSingleVuln(JsonObject vuln) { + String vulnWarn = "✗ %s severity (%s - %s) found on %s@%s"; + getLog().warn(logHelper.strongError(String.format(vulnWarn, + vuln.get("severity").getAsString(), + vuln.get("title").getAsString(), + vuln.get("cve").getAsString(), + vuln.get("packageName").getAsString(), + vuln.get("version").getAsString() + ))); + if(vuln.get("from") != null) { + JsonArray fromArr = vuln.get("from").getAsJsonArray(); + StringBuilder fromStrb = new StringBuilder(); + for(int i = 0; i < fromArr.size(); i++) { + fromStrb.append(fromArr.get(i).getAsString()); + fromStrb.append(" > "); + } + getLog().warn(String.format("- Path: %s" ,fromStrb.substring(0, fromStrb.length() - 3))); + } + if (vuln.get("target_version").getAsJsonArray().size() >= 0) { + getLog().warn(logHelper.strongInfo(String.format("! Fix version %s", vuln.get("target_version").getAsJsonArray()))); + } + getLog().warn(""); + } + + private Logger getLog() { + return log; + } +} diff --git a/src/main/java/com/immomo/momosec/gradle/plugins/exceptions/FoundVulnerableException.java b/src/main/java/com/immomo/momosec/gradle/plugins/exceptions/FoundVulnerableException.java new file mode 100644 index 0000000..502d9ca --- /dev/null +++ b/src/main/java/com/immomo/momosec/gradle/plugins/exceptions/FoundVulnerableException.java @@ -0,0 +1,8 @@ +package com.immomo.momosec.gradle.plugins.exceptions; + +public class FoundVulnerableException extends RuntimeException { + + public FoundVulnerableException(String message) { + super(message); + } +} diff --git a/src/main/java/com/immomo/momosec/gradle/plugins/exceptions/NetworkErrorException.java b/src/main/java/com/immomo/momosec/gradle/plugins/exceptions/NetworkErrorException.java new file mode 100644 index 0000000..06b052a --- /dev/null +++ b/src/main/java/com/immomo/momosec/gradle/plugins/exceptions/NetworkErrorException.java @@ -0,0 +1,8 @@ +package com.immomo.momosec.gradle.plugins.exceptions; + +public class NetworkErrorException extends RuntimeException { + + public NetworkErrorException(String message) { + super(message); + } +} diff --git a/src/main/resources/META-INF/gradle-plugins/mosec.properties b/src/main/resources/META-INF/gradle-plugins/mosec.properties new file mode 100644 index 0000000..c2b4cc5 --- /dev/null +++ b/src/main/resources/META-INF/gradle-plugins/mosec.properties @@ -0,0 +1 @@ +implementation-class=com.immomo.momosec.gradle.plugins.Mosec \ No newline at end of file diff --git a/src/test/groovy/com/immomo/momosec/gradle/plugins/TestProjectDependencyCollector.groovy b/src/test/groovy/com/immomo/momosec/gradle/plugins/TestProjectDependencyCollector.groovy new file mode 100644 index 0000000..1af7daa --- /dev/null +++ b/src/test/groovy/com/immomo/momosec/gradle/plugins/TestProjectDependencyCollector.groovy @@ -0,0 +1,183 @@ +package com.immomo.momosec.gradle.plugins + +import com.immomo.momosec.gradle.plugins.stubs.MyConfiguration +import com.immomo.momosec.gradle.plugins.stubs.MyResolvedDependency +import org.gradle.api.Project +import org.gradle.api.artifacts.ConfigurationContainer +import org.gradle.api.artifacts.ResolvedConfiguration +import org.gradle.api.logging.Logger +import org.junit.Assert +import org.junit.Test + + +import static org.mockito.Mockito.* + +class TestProjectDependencyCollector { + + private final Project project = mock(Project.class) + + @Test + void collectTest() { + def spyLog = spy(mock(Logger.class)) + def confNameFilter = /.*/ + def spyCollector = spy(new ProjectDependencyCollector(project, null, confNameFilter, true)) + def confContainer = mock(ConfigurationContainer.class) + + // for spy log + doNothing().when(spyLog).info(anyString()) + doNothing().when(spyLog).debug(anyString()) + doNothing().when(spyLog).warn(anyString()) + doNothing().when(spyLog).error(anyString()) + + // for project.configuration.findAll + def spyConf = spy(new MyConfiguration()) + doReturn([spyConf].toSet()).when(confContainer).findAll(any(Closure.class)) + + // for mosecConf.resolvedConfiguration.firstLevelModuleDependencies + def resolvedConf = mock(ResolvedConfiguration.class) + doReturn(resolvedConf).when(spyConf).getResolvedConfiguration() + + def parent = new MyResolvedDependency('com.study.parent', 'parent', '1.0.0') + parent.setChildren(new MyResolvedDependency('com.study.child', 'child', '1.0.0')) + doReturn(parent.getChildren()).when(resolvedConf).getFirstLevelModuleDependencies() + + // for spy project + doReturn('com.study.parent:parent').when(project).getName() + doReturn('1.0.0').when(project).getVersion() + doReturn(spyLog).when(project).getLogger() + doReturn([project].toSet()).when(project).getAllprojects() + doReturn(confContainer).when(project).getConfigurations() + + def depsTree = spyCollector.collect() + + verify(spyCollector, times(1)).collect() + verify(project, times(1)).getAllprojects() + verify(project, atLeastOnce()).getConfigurations() + verify(spyConf, times(1)).getResolvedConfiguration() + verify(resolvedConf, times(1)).getFirstLevelModuleDependencies() + Assert.assertEquals('com.study.parent:parent', depsTree.get('name')) + Assert.assertEquals('1.0.0', depsTree.get('version')) + Assert.assertEquals(2, + (((depsTree.get('dependencies') as Map) + .get('com.study.child:child') as Map) + .get('from') as List) + .size()) + } + + @Test + void depsToDictTest() { + def parent = new MyResolvedDependency('com.study.parent', 'parent', '1.0.0') + def child = new MyResolvedDependency('com.study.child', 'child', '1.0.0') + def child_child = new MyResolvedDependency('com.study.child_child', 'child_child', '1.0.0') + + parent.setChildren(child) + child.setChildren(child_child) + + child_child.setParent(child) + child.setParent(parent) + + def collector = new ProjectDependencyCollector(project, null, null, true) + def chain = ['com.study.parent:parent@1.0.0'] + + def childOnlyProvenanceDepsTree = collector.depsToDict.call(parent.getChildren(), chain as ArrayList, true) + def expectChildOnlyProvenanceTree = [ + 'com.study.child:child': [ + 'name': 'com.study.child:child', + 'version': '1.0.0', + 'from': ['com.study.parent:parent@1.0.0', 'com.study.child:child@1.0.0'], + 'dependencies': [:] + ] + ] + Assert.assertEquals(expectChildOnlyProvenanceTree, childOnlyProvenanceDepsTree) + + def childNotOnlyProvenanceDepsTree = collector.depsToDict.call(parent.getChildren(), chain as ArrayList, false) + def expectChildNotOnlyProvenanceTree = [ + 'com.study.child:child': [ + 'name': 'com.study.child:child', + 'version': '1.0.0', + 'from': ['com.study.parent:parent@1.0.0', 'com.study.child:child@1.0.0'], + 'dependencies': [ + 'com.study.child_child:child_child': [ + 'name': 'com.study.child_child:child_child', + 'version': '1.0.0', + 'from': ['com.study.parent:parent@1.0.0', 'com.study.child:child@1.0.0', 'com.study.child_child:child_child@1.0.0'], + 'dependencies': [:] + ] + ] + ] + ] + Assert.assertEquals(expectChildNotOnlyProvenanceTree, childNotOnlyProvenanceDepsTree) + } + + @Test + void simplifyDepsTest() { + def depsTree = [ + 'name': 'com.study.parent:parent', + 'version': '1.0.0', + 'from': ['com.study.parent:parent@1.0.0'], + 'dependencies': [ + 'com.study.child1:child1': [ + 'name': 'com.study.child1:child1', + 'version': '1.0.0', + 'from': ['com.study.parent:parent@1.0.0', 'com.study.child1:child1@1.0.0'], + 'dependencies': [:] + ], + 'com.study.child2:child2': [ + 'name': 'com.study.child2:child2', + 'version': '1.0.0', + 'from': ['com.study.parent:parent@1.0.0', 'com.study.child2:child2@1.0.0'], + 'dependencies': [ + 'com.study.child1:child1': [ + 'name': 'com.study.child1:child1', + 'version': '1.0.0', + 'from': ['com.study.parent:parent@1.0.0', 'com.study.child2:child2@1.0.0', 'com.study.child1:child1@1.0.0'], + 'dependencies': [:] + ] + ] + ] + ] + ] + + def expectDepsTree = [ + 'name': 'com.study.parent:parent', + 'version': '1.0.0', + 'from': ['com.study.parent:parent@1.0.0'], + 'dependencies': [ + 'com.study.child1:child1': [ + 'name': 'com.study.child1:child1', + 'version': '1.0.0', + 'from': ['com.study.parent:parent@1.0.0', 'com.study.child1:child1@1.0.0'], + 'dependencies': [:] + ], + 'com.study.child2:child2': [ + 'name': 'com.study.child2:child2', + 'version': '1.0.0', + 'from': ['com.study.parent:parent@1.0.0', 'com.study.child2:child2@1.0.0'], + 'dependencies': [:] + ] + ] + ] + + def collector = new ProjectDependencyCollector(project, null, null, true) + collector.simplifyDeps.call(depsTree) + + Assert.assertEquals(expectDepsTree, depsTree) + } + + @Test + void matchesAttributeFilter() { + def match + def conf = new MyConfiguration() // has usage:java-runtime attributes + + String matchAttrSpec = "usage:java-runtime" + def collector = new ProjectDependencyCollector(project, matchAttrSpec.split(',').collect{ it.split(':') }, null, true) + match = collector.matchesAttributeFilter.call(conf) + Assert.assertTrue(match) + + String notMatchAttrSpec = "usage:imnotexist" + def collectorWithWrongAttrSpec = new ProjectDependencyCollector(project, notMatchAttrSpec.split(',').collect{ it.split(':') }, null, true) + match = collectorWithWrongAttrSpec.matchesAttributeFilter.call(conf) + Assert.assertFalse(match) + } + +} diff --git a/src/test/groovy/com/immomo/momosec/gradle/plugins/stubs/MyConfiguration.java b/src/test/groovy/com/immomo/momosec/gradle/plugins/stubs/MyConfiguration.java new file mode 100644 index 0000000..fcfae11 --- /dev/null +++ b/src/test/groovy/com/immomo/momosec/gradle/plugins/stubs/MyConfiguration.java @@ -0,0 +1,351 @@ +package com.immomo.momosec.gradle.plugins.stubs; + +import groovy.lang.Closure; +import org.gradle.api.Action; +import org.gradle.api.artifacts.*; +import org.gradle.api.attributes.Attribute; +import org.gradle.api.attributes.AttributeContainer; +import org.gradle.api.attributes.Usage; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.FileTree; +import org.gradle.api.internal.attributes.DefaultImmutableAttributesFactory; +import org.gradle.api.specs.Spec; +import org.gradle.api.tasks.TaskDependency; + +import javax.annotation.Nullable; +import java.io.File; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +public class MyConfiguration implements Configuration { + @Override + public ResolutionStrategy getResolutionStrategy() { + return null; + } + + @Override + public Configuration resolutionStrategy(Closure closure) { + return null; + } + + @Override + public Configuration resolutionStrategy(Action action) { + return null; + } + + @Override + public State getState() { + return null; + } + + @Override + public String getName() { + return null; + } + + @Override + public boolean isVisible() { + return false; + } + + @Override + public Configuration setVisible(boolean b) { + return null; + } + + @Override + public Set getExtendsFrom() { + return null; + } + + @Override + public Configuration setExtendsFrom(Iterable iterable) { + return null; + } + + @Override + public Configuration extendsFrom(Configuration... configurations) { + return null; + } + + @Override + public boolean isTransitive() { + return false; + } + + @Override + public Configuration setTransitive(boolean b) { + return null; + } + + @Nullable + @Override + public String getDescription() { + return null; + } + + @Override + public Configuration setDescription(@Nullable String s) { + return null; + } + + @Override + public Set getHierarchy() { + return null; + } + + @Override + public Set resolve() { + return null; + } + + @Override + public Set files(Closure closure) { + return null; + } + + @Override + public Set files(Spec spec) { + return null; + } + + @Override + public Set files(Dependency... dependencies) { + return null; + } + + @Override + public FileCollection fileCollection(Spec spec) { + return null; + } + + @Override + public FileCollection fileCollection(Closure closure) { + return null; + } + + @Override + public FileCollection fileCollection(Dependency... dependencies) { + return null; + } + + @Override + public ResolvedConfiguration getResolvedConfiguration() { + return null; + } + + @Override + public String getUploadTaskName() { + return null; + } + + @Override + public TaskDependency getBuildDependencies() { + return null; + } + + @Override + public TaskDependency getTaskDependencyFromProjectDependency(boolean b, String s) { + return null; + } + + @Override + public DependencySet getDependencies() { + return null; + } + + @Override + public DependencySet getAllDependencies() { + return null; + } + + @Override + public DependencyConstraintSet getDependencyConstraints() { + return null; + } + + @Override + public DependencyConstraintSet getAllDependencyConstraints() { + return null; + } + + @Override + public PublishArtifactSet getArtifacts() { + return null; + } + + @Override + public PublishArtifactSet getAllArtifacts() { + return null; + } + + @Override + public Set getExcludeRules() { + return null; + } + + @Override + public Configuration exclude(Map map) { + return null; + } + + @Override + public Configuration defaultDependencies(Action action) { + return null; + } + + @Override + public Configuration withDependencies(Action action) { + return null; + } + + @Override + public Set getAll() { + return null; + } + + @Override + public ResolvableDependencies getIncoming() { + return null; + } + + @Override + public ConfigurationPublications getOutgoing() { + return null; + } + + @Override + public void outgoing(Action action) { + + } + + @Override + public Configuration copy() { + return null; + } + + @Override + public Configuration copyRecursive() { + return null; + } + + @Override + public Configuration copy(Spec spec) { + return null; + } + + @Override + public Configuration copyRecursive(Spec spec) { + return null; + } + + @Override + public Configuration copy(Closure closure) { + return null; + } + + @Override + public Configuration copyRecursive(Closure closure) { + return null; + } + + @Override + public void setCanBeConsumed(boolean b) { + + } + + @Override + public boolean isCanBeConsumed() { + return false; + } + + @Override + public void setCanBeResolved(boolean b) { + + } + + @Override + public boolean isCanBeResolved() { + return false; + } + + @Override + public Configuration attributes(Action action) { + return null; + } + + @Override + public AttributeContainer getAttributes() { + DefaultImmutableAttributesFactory attributesFactory = new DefaultImmutableAttributesFactory(null, null); + + AttributeContainer attrContainer = attributesFactory.mutable(); + attrContainer.attribute(Attribute.of("usage", String.class), Usage.JAVA_RUNTIME); + return attrContainer; + } + + @Override + public File getSingleFile() throws IllegalStateException { + return null; + } + + @Override + public Set getFiles() { + return null; + } + + @Override + public boolean contains(File file) { + return false; + } + + @Override + public String getAsPath() { + return null; + } + + @Override + public FileCollection plus(FileCollection fileCollection) { + return null; + } + + @Override + public FileCollection minus(FileCollection fileCollection) { + return null; + } + + @Override + public FileCollection filter(Closure closure) { + return null; + } + + @Override + public FileCollection filter(Spec spec) { + return null; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public FileTree getAsFileTree() { + return null; + } + + @Override + public void addToAntBuilder(Object o, String s, AntType antType) { + + } + + @Override + public Object addToAntBuilder(Object o, String s) { + return null; + } + + @Override + public Iterator iterator() { + return null; + } +} diff --git a/src/test/groovy/com/immomo/momosec/gradle/plugins/stubs/MyResolvedDependency.java b/src/test/groovy/com/immomo/momosec/gradle/plugins/stubs/MyResolvedDependency.java new file mode 100644 index 0000000..6bad0e0 --- /dev/null +++ b/src/test/groovy/com/immomo/momosec/gradle/plugins/stubs/MyResolvedDependency.java @@ -0,0 +1,95 @@ +package com.immomo.momosec.gradle.plugins.stubs; + +import org.gradle.api.artifacts.ResolvedArtifact; +import org.gradle.api.artifacts.ResolvedDependency; +import org.gradle.api.artifacts.ResolvedModuleVersion; + +import java.util.HashSet; +import java.util.Set; + +public class MyResolvedDependency implements ResolvedDependency { + private String group; + private String name; + private String version; + private Set children = new HashSet<>(); + private Set parent = new HashSet<>(); + + public MyResolvedDependency(String group, String name, String version) { + this.group = group; + this.name = name; + this.version = version; + } + + @Override + public String getName() { + return null; + } + + @Override + public String getModuleGroup() { + return group; + } + + @Override + public String getModuleName() { + return name; + } + + @Override + public String getModuleVersion() { + return version; + } + + @Override + public String getConfiguration() { + return null; + } + + @Override + public ResolvedModuleVersion getModule() { + return null; + } + + @Override + public Set getChildren() { + return children; + } + + public void setChildren(ResolvedDependency dependency) { + this.children.add(dependency); + } + + @Override + public Set getParents() { + return parent; + } + + public void setParent(ResolvedDependency dependency) { + this.parent.add(dependency); + } + + @Override + public Set getModuleArtifacts() { + return null; + } + + @Override + public Set getAllModuleArtifacts() { + return null; + } + + @Override + public Set getParentArtifacts(ResolvedDependency resolvedDependency) { + return null; + } + + @Override + public Set getArtifacts(ResolvedDependency resolvedDependency) { + return null; + } + + @Override + public Set getAllArtifacts(ResolvedDependency resolvedDependency) { + return null; + } +} diff --git a/src/test/java/com/immomo/momosec/gradle/plugins/TestRenderer.java b/src/test/java/com/immomo/momosec/gradle/plugins/TestRenderer.java new file mode 100644 index 0000000..46aaede --- /dev/null +++ b/src/test/java/com/immomo/momosec/gradle/plugins/TestRenderer.java @@ -0,0 +1,127 @@ +package com.immomo.momosec.gradle.plugins; + +import com.immomo.momosec.gradle.plugins.exceptions.FoundVulnerableException; +import com.immomo.momosec.gradle.plugins.exceptions.NetworkErrorException; +import org.gradle.api.logging.Logger; +import org.junit.*; +import org.junit.rules.ExpectedException; +import org.mockito.Mockito; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + + +public class TestRenderer { + + @Rule + public ExpectedException exceptionRule = ExpectedException.none(); + + private final Logger log = mock(Logger.class); + private final Logger spyLog = Mockito.spy(log); + private final MosecLogHelper logHelper = new MosecLogHelper(); + + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + + private final String no_vulnerable_response = + "{" + + " \"ok\": true," + + " \"dependencyCount\": 3" + + "}"; + + private final String vulnerable_response = + "{" + + " \"ok\": false," + + " \"dependencyCount\": 3," + + " \"vulnerabilities\": [{" + + " \"severity\": \"High\"," + + " \"title\": \"Fake Vulnerable\"," + + " \"cve\": \"CVE-0001-0001\"," + + " \"packageName\": \"com.study.foo:bar\"," + + " \"version\": \"1.0.0\"," + + " \"target_version\": [\"1.1\"]" + + " }]" + + "}"; + + @Before + public void setUpStreams() { + System.setOut(new PrintStream(outContent)); + System.setErr(new PrintStream(errContent)); + } + + @Before + public void mockLog() { + doAnswer(invocation -> { + System.out.println((String)invocation.getArgument(0)); + return null; + }).when(spyLog).info(anyString()); + + doAnswer(invocation -> { + System.out.println((String)invocation.getArgument(0)); + return null; + }).when(spyLog).debug(anyString()); + + doAnswer(invocation -> { + System.out.println((String)invocation.getArgument(0)); + return null; + }).when(spyLog).warn(anyString()); + + doAnswer(invocation -> { + System.out.println((String)invocation.getArgument(0)); + return null; + }).when(spyLog).error(anyString()); + } + + @After + public void restoreStreams() { + System.setOut(originalOut); + System.setErr(originalErr); + } + + @Test + public void renderResponseTest_ErrorJson() throws Exception { + exceptionRule.expect(NetworkErrorException.class); + exceptionRule.expectMessage(Constants.ERROR_ON_API); + + Renderer renderer = new Renderer(log, true); + renderer.renderResponse(new ByteArrayInputStream("_".getBytes())); + } + + @Test + public void renderResponseTest_NotFoundVuln() throws Exception { + Renderer renderer = new Renderer(spyLog, true); + renderer.renderResponse(new ByteArrayInputStream(no_vulnerable_response.getBytes())); + + String expect = logHelper.strongInfo("✓ Tested 3 dependencies, no vulnerable found.") + "\n"; + Assert.assertEquals(expect, outContent.toString()); + } + + @Test + public void renderResponseTest_FoundVulnWithFailOnVuln() throws Exception { + exceptionRule.expect(FoundVulnerableException.class); + exceptionRule.expectMessage(Constants.ERROR_ON_VULNERABLE); + + Renderer renderer = new Renderer(log, true); + renderer.renderResponse(new ByteArrayInputStream(vulnerable_response.getBytes())); + } + + @Test + public void renderResponseTest_FoundVulnWithoutFailOnVuln() throws Exception { + Renderer renderer = new Renderer(spyLog, false); + renderer.renderResponse(new ByteArrayInputStream(vulnerable_response.getBytes())); + + String expect = + logHelper.strongError("✗ High severity (Fake Vulnerable - CVE-0001-0001) found on com.study.foo:bar@1.0.0") + "\n" + + logHelper.strongInfo("! Fix version [\"1.1\"]") + "\n" + + "\n" + + logHelper.strongWarning("Tested 3 dependencies, found 1 vulnerable pathes.") + "\n"; + Assert.assertEquals(expect, outContent.toString()); + } + +} diff --git a/static/remote-configuration.jpg b/static/remote-configuration.jpg new file mode 100644 index 0000000..9b48b0d Binary files /dev/null and b/static/remote-configuration.jpg differ