All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.liferay.source.formatter.check.LibraryVulnerabilitiesCheck Maven / Gradle / Ivy

There is a newer version: 1.0.1437
Show newest version
/**
 * SPDX-FileCopyrightText: (c) 2000 Liferay, Inc. https://liferay.com
 * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06
 */

package com.liferay.source.formatter.check;

import com.liferay.petra.string.StringBundler;
import com.liferay.petra.string.StringPool;
import com.liferay.portal.json.JSONObjectImpl;
import com.liferay.portal.kernel.json.JSONArray;
import com.liferay.portal.kernel.json.JSONException;
import com.liferay.portal.kernel.json.JSONObject;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.util.StringUtil;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.source.formatter.SourceFormatterArgs;
import com.liferay.source.formatter.check.util.SourceUtil;
import com.liferay.source.formatter.processor.SourceProcessor;
import com.liferay.source.formatter.util.FileUtil;
import com.liferay.source.formatter.util.GradleBuildFile;
import com.liferay.source.formatter.util.GradleDependency;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;

import java.net.URL;
import java.net.URLConnection;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.VersionRange;

import org.dom4j.Document;
import org.dom4j.Element;

/**
 * @author Qi Zhang
 */
public class LibraryVulnerabilitiesCheck extends BaseFileCheck {

	@Override
	public boolean isLiferaySourceCheck() {
		return true;
	}

	@Override
	protected String doProcess(
			String fileName, String absolutePath, String content)
		throws Exception {

		SourceProcessor sourceProcessor = getSourceProcessor();

		SourceFormatterArgs sourceFormatterArgs =
			sourceProcessor.getSourceFormatterArgs();

		if (!sourceFormatterArgs.isCheckVulnerabilities()) {
			return content;
		}

		_githubAccessToken = _getGithubAccessToken(sourceFormatterArgs);

		if (fileName.endsWith(".gradle")) {
			_checkGradleLibraryVulnerabilities(fileName, absolutePath, content);
		}
		else if (fileName.endsWith(".json")) {
			_checkJsonLibraryVulnerabilities(fileName, absolutePath, content);
		}
		else if (fileName.endsWith(".properties")) {
			_checkPropertiesLibraryVulnerabilities(
				fileName, absolutePath, content);
		}
		else if (fileName.endsWith("ivy.xml")) {
			_checkIvyXmlLibraryVulnerabilities(fileName, absolutePath, content);
		}
		else if (fileName.endsWith("pom.xml")) {
			_checkPomXmlLibraryVulnerabilities(fileName, absolutePath, content);
		}

		return content;
	}

	private static boolean _isGenerateVulnerableLibrariesCacheFile() {
		if (Validator.isNull(System.getenv("JENKINS_HOME"))) {
			return false;
		}

		String jobName = System.getenv("JOB_NAME");

		if (Validator.isNull(jobName)) {
			return false;
		}

		return jobName.contains("liferay-binaries-cache-upstream");
	}

	private void _checkGradleLibraryVulnerabilities(
			String fileName, String absolutePath, String content)
		throws Exception {

		GradleBuildFile gradleBuildFile = new GradleBuildFile(content);

		List gradleDependencies =
			gradleBuildFile.getGradleDependencies();

		gradleDependencies.addAll(gradleBuildFile.getBuildScriptDependencies());

		for (GradleDependency gradleDependency : gradleDependencies) {
			String gradleDependencyGroup = gradleDependency.getGroup();
			String gradleDependencyName = gradleDependency.getName();
			String gradleDependencyVersion = gradleDependency.getVersion();

			if (Validator.isNull(gradleDependencyGroup) ||
				Validator.isNull(gradleDependencyName) ||
				Validator.isNull(gradleDependencyVersion)) {

				continue;
			}

			_checkVulnerabilities(
				fileName, absolutePath,
				gradleDependencyGroup + StringPool.COLON + gradleDependencyName,
				gradleDependencyVersion, SecurityAdvisoryEcosystemEnum.MAVEN);
		}
	}

	private void _checkIvyXmlLibraryVulnerabilities(
			String fileName, String absolutePath, String content)
		throws Exception {

		if (Validator.isNull(content)) {
			return;
		}

		Document document = SourceUtil.readXML(content);

		if (document == null) {
			return;
		}

		Element rootElement = document.getRootElement();

		for (Element dependenciesElement :
				(List)rootElement.elements("dependencies")) {

			for (Element dependencyElement :
					(List)dependenciesElement.elements("dependency")) {

				String name = dependencyElement.attributeValue("name");
				String org = dependencyElement.attributeValue("org");
				String rev = dependencyElement.attributeValue("rev");

				if (Validator.isNull(name) || Validator.isNull(org) ||
					Validator.isNull(rev)) {

					continue;
				}

				_checkVulnerabilities(
					fileName, absolutePath, org + StringPool.COLON + name, rev,
					SecurityAdvisoryEcosystemEnum.MAVEN);
			}
		}
	}

	private void _checkJsonLibraryVulnerabilities(
			String fileName, String absolutePath, String content)
		throws Exception {

		if (Validator.isNull(content)) {
			return;
		}

		try {
			JSONObject contentJSONObject = new JSONObjectImpl(content);

			_checkVersionInJsonFile(
				fileName, absolutePath,
				contentJSONObject.getJSONObject("dependencies"));
			_checkVersionInJsonFile(
				fileName, absolutePath,
				contentJSONObject.getJSONObject("devDependencies"));
		}
		catch (JSONException jsonException) {
			if (_log.isDebugEnabled()) {
				_log.debug(jsonException);
			}
		}
	}

	private void _checkPomXmlLibraryVulnerabilities(
			String fileName, String absolutePath, String content)
		throws Exception {

		if (Validator.isNull(content)) {
			return;
		}

		Document document = SourceUtil.readXML(content);

		if (document == null) {
			return;
		}

		Element rootElement = document.getRootElement();

		for (Element dependenciesElement :
				(List)rootElement.elements("dependencies")) {

			for (Element dependencyElement :
					(List)dependenciesElement.elements("dependency")) {

				Element artifactIdElement = dependencyElement.element(
					"artifactId");
				Element groupIdElement = dependencyElement.element("groupId");
				Element versionElement = dependencyElement.element("version");

				if ((artifactIdElement == null) || (groupIdElement == null) ||
					(versionElement == null)) {

					continue;
				}

				String version = versionElement.getText();

				if (version.startsWith(StringPool.DOLLAR)) {
					continue;
				}

				_checkVulnerabilities(
					fileName, absolutePath,
					groupIdElement.getText() + StringPool.COLON +
						artifactIdElement.getText(),
					version, SecurityAdvisoryEcosystemEnum.MAVEN);
			}
		}

		for (Element buildsElement :
				(List)rootElement.elements("build")) {

			for (Element pluginsElement :
					(List)buildsElement.elements("plugins")) {

				for (Element pluginElement :
						(List)pluginsElement.elements("plugin")) {

					Element artifactIdElement = pluginElement.element(
						"artifactId");
					Element groupIdElement = pluginElement.element("groupId");
					Element versionElement = pluginElement.element("version");

					if ((artifactIdElement == null) ||
						(groupIdElement == null) || (versionElement == null)) {

						continue;
					}

					String version = versionElement.getText();

					if (version.startsWith(StringPool.DOLLAR)) {
						continue;
					}

					_checkVulnerabilities(
						fileName, absolutePath,
						groupIdElement.getText() + StringPool.COLON +
							artifactIdElement.getText(),
						version, SecurityAdvisoryEcosystemEnum.MAVEN);
				}
			}
		}
	}

	private void _checkPropertiesLibraryVulnerabilities(
			String fileName, String absolutePath, String content)
		throws Exception {

		Properties properties = new Properties();

		properties.load(new StringReader(content));

		Enumeration enumeration =
			(Enumeration)properties.propertyNames();

		while (enumeration.hasMoreElements()) {
			String key = enumeration.nextElement();

			String value = properties.getProperty(key);

			if (Validator.isNull(value)) {
				continue;
			}

			String[] dependency = value.split(StringPool.COLON);

			if (dependency.length < 3) {
				continue;
			}

			_checkVulnerabilities(
				fileName, absolutePath,
				dependency[0] + StringPool.COLON + dependency[1], dependency[2],
				SecurityAdvisoryEcosystemEnum.MAVEN);
		}
	}

	private void _checkVersionInJsonFile(
			String fileName, String absolutePath, JSONObject jsonObject)
		throws Exception {

		if (jsonObject == null) {
			return;
		}

		for (String dependencyName : jsonObject.keySet()) {
			String version = jsonObject.getString(dependencyName);

			if (version.startsWith("^") || version.startsWith("~") ||
				version.startsWith("*")) {

				continue;
			}

			_checkVulnerabilities(
				fileName, absolutePath, dependencyName, version,
				SecurityAdvisoryEcosystemEnum.NPM);
		}
	}

	private void _checkVulnerabilities(
		String fileName, String packageName,
		SecurityAdvisoryEcosystemEnum securityAdvisoryEcosystemEnum,
		DefaultArtifactVersion version) {

		List securityVulnerabilityNodes =
			_cachedVulnerableVersionMap.get(
				securityAdvisoryEcosystemEnum + ":" + packageName);

		for (SecurityVulnerabilityNode securityVulnerabilityNode :
				securityVulnerabilityNodes) {

			VersionRange versionRange =
				securityVulnerabilityNode.getVersionRange();

			if (versionRange.containsVersion(version)) {
				addMessage(
					fileName,
					StringBundler.concat(
						"Library '", packageName, ":", version,
						"' contains known vulnerabilities(",
						securityVulnerabilityNode.getSummary(), ", ",
						securityVulnerabilityNode.getPermalink(), ")"));

				return;
			}
		}
	}

	private void _checkVulnerabilities(
			String fileName, String absolutePath, String packageName,
			String version,
			SecurityAdvisoryEcosystemEnum securityAdvisoryEcosystemEnum)
		throws Exception {

		List allowedVulnerabilities = getAttributeValues(
			_ALLOWED_VULNERABILITIES_KEY, absolutePath);

		if (allowedVulnerabilities.contains(
				packageName + StringPool.COLON + version) ||
			!version.matches("(\\d|v).+")) {

			return;
		}

		for (String line :
				StringUtil.splitLines(_getCachedKnownVulnerabilities())) {

			String[] parts = StringUtil.split(line, StringPool.SEMICOLON);

			VersionRange versionRange = VersionRange.createFromVersionSpec(
				parts[2]);

			DefaultArtifactVersion defaultArtifactVersion =
				new DefaultArtifactVersion(version);

			if ((parts.length == 5) &&
				Objects.equals(
					securityAdvisoryEcosystemEnum.name(), parts[0]) &&
				packageName.equals(parts[1]) &&
				versionRange.containsVersion(defaultArtifactVersion)) {

				addMessage(
					fileName,
					StringBundler.concat(
						"Library '", packageName, ":", version,
						"' contains known vulnerabilities(", parts[3], ", ",
						parts[4], ")"));

				return;
			}
		}

		if (Validator.isNull(_githubAccessToken)) {
			return;
		}

		if (!_cachedVulnerableVersionMap.containsKey(
				securityAdvisoryEcosystemEnum + ":" + packageName)) {

			_generateVulnerableVersionMap(
				packageName, securityAdvisoryEcosystemEnum,
				getAttributeValues(_SEVERITIES_KEY, absolutePath));
		}

		_checkVulnerabilities(
			fileName, packageName, securityAdvisoryEcosystemEnum,
			new DefaultArtifactVersion(version));
	}

	private void _generateVulnerableVersionMap(
			String packageName,
			SecurityAdvisoryEcosystemEnum securityAdvisoryEcosystemEnum,
			List severities)
		throws Exception {

		if (_cachedVulnerableVersionMap.containsKey(
				securityAdvisoryEcosystemEnum + ":" + packageName)) {

			return;
		}

		List securityVulnerabilityNodes =
			_getSecurityVulnerabilityNodes(
				packageName, null, securityAdvisoryEcosystemEnum, severities,
				_githubAccessToken);

		_cachedVulnerableVersionMap.put(
			securityAdvisoryEcosystemEnum + ":" + packageName,
			securityVulnerabilityNodes);

		if (!_isGenerateVulnerableLibrariesCacheFile()) {
			return;
		}

		for (SecurityVulnerabilityNode securityVulnerabilityNode :
				securityVulnerabilityNodes) {

			String[] items = {
				securityAdvisoryEcosystemEnum.toString(), packageName,
				String.valueOf(securityVulnerabilityNode.getVersionRange()),
				securityVulnerabilityNode.getSummary(),
				securityVulnerabilityNode.getPermalink()
			};

			String vulnerabilityContent =
				StringUtil.merge(items, StringPool.SEMICOLON) + "\n";

			synchronized (this) {
				_write(
					new File(getPortalDir(), "vulnerable_libraries.txt"),
					vulnerabilityContent);
			}
		}
	}

	private synchronized String _getCachedKnownVulnerabilities()
		throws Exception {

		if (Validator.isNotNull(_cachedKnownVulnerabilities)) {
			return _cachedKnownVulnerabilities;
		}

		_cachedKnownVulnerabilities = StringPool.BLANK;

		if (!_isGenerateVulnerableLibrariesCacheFile()) {
			File vulnerableLibrariesFile = new File(
				getPortalDir(),
				"../liferay-binaries-cache-2020/vulnerable_libraries.txt");

			if (vulnerableLibrariesFile.exists()) {
				_cachedKnownVulnerabilities = FileUtil.read(
					vulnerableLibrariesFile);
			}
		}

		return _cachedKnownVulnerabilities;
	}

	private String _getCiGithubAccessToken() {
		Properties properties = new Properties();

		try {
			URL url = new URL(_CI_PROPERTIES_URL);

			URLConnection urlConnection = url.openConnection();

			urlConnection.connect();

			properties.load(urlConnection.getInputStream());
		}
		catch (IOException ioException) {
			if (_log.isDebugEnabled()) {
				_log.debug(ioException);
			}
		}

		return properties.getProperty("github.access.token");
	}

	private synchronized String _getGithubAccessToken(
			SourceFormatterArgs sourceFormatterArgs)
		throws Exception {

		if (Validator.isNotNull(_githubAccessToken)) {
			return _githubAccessToken;
		}

		if (sourceFormatterArgs.isUseCiGithubAccessToken() ||
			_isGenerateVulnerableLibrariesCacheFile()) {

			_githubAccessToken = _getCiGithubAccessToken();
		}
		else {
			_githubAccessToken = _getLocalGithubAccessToken();
		}

		return _githubAccessToken;
	}

	private String _getLocalGithubAccessToken() throws Exception {
		File file = getPortalDir();

		if (file == null) {
			return StringPool.BLANK;
		}

		File buildPropertiesFile = new File(
			file.getAbsolutePath(), _BUILD_PROPERTIES_FILE_NAME);

		if (!buildPropertiesFile.exists()) {
			return null;
		}

		Properties properties = new Properties();

		properties.load(Files.newInputStream(buildPropertiesFile.toPath()));

		return properties.getProperty("github.access.token");
	}

	private List _getSecurityVulnerabilityNodes(
		String packageName, String cursor,
		SecurityAdvisoryEcosystemEnum securityAdvisoryEcosystemEnum,
		List severities, String githubToken) {

		HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();

		try (CloseableHttpClient closeableHttpClient =
				httpClientBuilder.build()) {

			String queryArguments = StringBundler.concat(
				"first: 100, package:\\\"", packageName, "\\\", ecosystem: ",
				securityAdvisoryEcosystemEnum.name(), ", severities: ",
				severities);

			if (Validator.isNotNull(cursor)) {
				queryArguments += "after: \\\"" + cursor + "\\\"";
			}

			String resultArguments =
				"{nodes { advisory {summary, permalink} package {name} " +
					"severity vulnerableVersionRange } pageInfo {endCursor " +
						"hasNextPage } totalCount }";

			HttpPost httpPost = new HttpPost("https://api.github.com/graphql");

			httpPost.setEntity(
				new StringEntity(
					StringBundler.concat(
						"{\"query\": \"{ securityVulnerabilities(",
						queryArguments, ") ", resultArguments, "}\" }"),
					ContentType.APPLICATION_JSON));
			httpPost.addHeader("Authorization", "bearer " + githubToken);
			httpPost.addHeader(
				"Content-Type", "application/json; charset=utf-8");

			CloseableHttpResponse closeableHttpResponse =
				closeableHttpClient.execute(httpPost);

			StatusLine statusLine = closeableHttpResponse.getStatusLine();

			if (statusLine.getStatusCode() != HttpStatus.SC_OK) {
				return Collections.emptyList();
			}

			JSONObject jsonObject = new JSONObjectImpl(
				EntityUtils.toString(
					closeableHttpResponse.getEntity(), "UTF-8"));

			JSONObject dataJSONObject = jsonObject.getJSONObject("data");

			if (dataJSONObject == null) {
				return Collections.emptyList();
			}

			JSONObject securityVulnerabilitiesJSONObject =
				dataJSONObject.getJSONObject("securityVulnerabilities");

			if (securityVulnerabilitiesJSONObject == null) {
				return Collections.emptyList();
			}

			int totalCount = securityVulnerabilitiesJSONObject.getInt(
				"totalCount");

			if (totalCount == 0) {
				return Collections.emptyList();
			}

			List securityVulnerabilityNodes =
				new ArrayList<>();

			JSONArray nodesJSONArray =
				securityVulnerabilitiesJSONObject.getJSONArray("nodes");

			Iterator iterator = nodesJSONArray.iterator();

			while (iterator.hasNext()) {
				JSONObject nodeJSONObject = iterator.next();

				SecurityVulnerabilityNode securityVulnerabilityNode =
					new SecurityVulnerabilityNode();

				JSONObject advisoryJSONObject = nodeJSONObject.getJSONObject(
					"advisory");

				securityVulnerabilityNode.setPermalink(
					advisoryJSONObject.getString("permalink"));
				securityVulnerabilityNode.setSummary(
					advisoryJSONObject.getString("summary"));

				securityVulnerabilityNode.setVersionRange(
					nodeJSONObject.getString("vulnerableVersionRange"));

				securityVulnerabilityNodes.add(securityVulnerabilityNode);
			}

			JSONObject pageInfoJSONObject =
				securityVulnerabilitiesJSONObject.getJSONObject("pageInfo");

			if (pageInfoJSONObject.getBoolean("hasNextPage")) {
				securityVulnerabilityNodes.addAll(
					_getSecurityVulnerabilityNodes(
						packageName, pageInfoJSONObject.getString("endCursor"),
						securityAdvisoryEcosystemEnum, severities,
						githubToken));
			}

			if (!securityVulnerabilityNodes.isEmpty()) {
				return securityVulnerabilityNodes;
			}
		}
		catch (Exception exception) {
			_log.error(exception);
		}

		return Collections.emptyList();
	}

	private void _write(File file, String content) throws Exception {
		try (OutputStream outputStream = Files.newOutputStream(
				Paths.get(file.toURI()), StandardOpenOption.CREATE,
				StandardOpenOption.APPEND)) {

			outputStream.write(content.getBytes());
		}
	}

	private static final String _ALLOWED_VULNERABILITIES_KEY =
		"allowedVulnerabilities";

	private static final String _BUILD_PROPERTIES_FILE_NAME =
		"build." + System.getProperty("user.name") + ".properties";

	private static final String _CI_PROPERTIES_URL =
		"http://mirrors.lax.liferay.com/github.com/liferay/liferay-jenkins-" +
			"ee/commands/build.properties";

	private static final String _SEVERITIES_KEY = "severities";

	private static final Log _log = LogFactoryUtil.getLog(
		LibraryVulnerabilitiesCheck.class);

	private String _cachedKnownVulnerabilities;
	private final Map>
		_cachedVulnerableVersionMap = new ConcurrentHashMap<>();
	private String _githubAccessToken;

	private static class SecurityVulnerabilityNode {

		public String getPermalink() {
			return _permalink;
		}

		public String getSummary() {
			return _summary;
		}

		public VersionRange getVersionRange() {
			return _versionRange;
		}

		public void setPermalink(String permalink) {
			_permalink = permalink;
		}

		public void setSummary(String summary) {
			_summary = summary;
		}

		public void setVersionRange(String vulnerableVersionRange)
			throws InvalidVersionSpecificationException {

			if (!vulnerableVersionRange.contains(StringPool.COMMA)) {
				String[] versionArray = vulnerableVersionRange.split(
					StringPool.SPACE, 2);

				if (versionArray[0].equals(StringPool.EQUAL)) {
					_versionRange = VersionRange.createFromVersion(
						versionArray[1]);
				}
				else if (versionArray[0].equals(StringPool.LESS_THAN)) {
					_versionRange = VersionRange.createFromVersionSpec(
						"(," + versionArray[1] + ")");
				}
				else if (versionArray[0].equals(
							StringPool.LESS_THAN_OR_EQUAL)) {

					_versionRange = VersionRange.createFromVersionSpec(
						"(," + versionArray[1] + "]");
				}
				else if (versionArray[0].equals(StringPool.GREATER_THAN)) {
					_versionRange = VersionRange.createFromVersionSpec(
						"(" + versionArray[1] + ",)");
				}
				else if (versionArray[0].equals(
							StringPool.GREATER_THAN_OR_EQUAL)) {

					_versionRange = VersionRange.createFromVersionSpec(
						"[" + versionArray[1] + ",)");
				}
			}
			else {
				vulnerableVersionRange = vulnerableVersionRange.replaceAll(
					"([=<>]+.+?, )([=<>]+)(.+)", "$1$3$2");

				vulnerableVersionRange = StringUtil.replace(
					vulnerableVersionRange,
					new String[] {
						StringPool.GREATER_THAN_OR_EQUAL,
						StringPool.GREATER_THAN, StringPool.LESS_THAN_OR_EQUAL,
						StringPool.LESS_THAN
					},
					new String[] {
						StringPool.OPEN_BRACKET, StringPool.OPEN_PARENTHESIS,
						StringPool.CLOSE_BRACKET, StringPool.CLOSE_PARENTHESIS
					});

				_versionRange = VersionRange.createFromVersionSpec(
					vulnerableVersionRange);
			}
		}

		private String _permalink;
		private String _summary;
		private VersionRange _versionRange;

	}

	private enum SecurityAdvisoryEcosystemEnum {

		MAVEN, NPM

	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy