From 436a583468febdc6ea4d85df6f4bd3673649ecc3 Mon Sep 17 00:00:00 2001 From: Manuel Leduc <manuel.leduc@xwiki.com> Date: Wed, 19 Jul 2023 14:15:30 +0200 Subject: [PATCH] XWIKI-21030: List core security issues on the security vulnerability list --- .../ExtensionIndexSolrCoreInitializer.java | 33 ++++- .../index/internal/ExtensionIndexStore.java | 24 ++- .../ExtensionSecurityAnalysisResult.java | 56 +++++++ .../SecurityVulnerabilityDescriptor.java | 63 +++++++- .../index/security/review/Review.java | 134 +++++++++++++++++ .../index/security/review/ReviewResult.java | 42 ++++++ .../index/security/review/ReviewsMap.java | 102 +++++++++++++ .../internal/ExtensionSecuritySolrClient.java | 16 +- .../internal/SolrToLiveDataEntryMapper.java | 98 ++++++++++-- .../resources/ApplicationResources.properties | 2 + .../extension/security/liveData/cveID.vm | 65 ++++++++ .../ExtensionSecuritySolrClientTest.java | 57 ++++++- .../SolrToLiveDataEntryMapperTest.java | 23 ++- .../internal/ExtensionSecurityJob.java | 104 ++++++++++--- .../internal/ExtensionSecurityScheduler.java | 4 +- .../analyzer/VulnerabilityIndexer.java | 139 +++++++++++++++++- .../analyzer/osv/OsvResponseAnalyzer.java | 14 +- .../osv/model/response/VulnObject.java | 3 + .../resources/ApplicationResources.properties | 4 +- .../ExtensionSecuritySchedulerTest.java | 104 +++++++++++++ .../analyzer/VulnerabilityIndexerTest.java | 132 +++++++++++++++++ .../analyzer/osv/OsvResponseAnalyzerTest.java | 2 - .../security/test/ui/ExtensionSecurityIT.java | 6 +- .../XWiki/Extension/Security/Code/Admin.xml | 49 +++++- 24 files changed, 1195 insertions(+), 81 deletions(-) create mode 100644 xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/security/review/Review.java create mode 100644 xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/security/review/ReviewResult.java create mode 100644 xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/security/review/ReviewsMap.java create mode 100644 xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/main/resources/templates/extension/security/liveData/cveID.vm create mode 100644 xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/test/java/org/xwiki/extension/security/internal/ExtensionSecuritySchedulerTest.java create mode 100644 xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/test/java/org/xwiki/extension/security/internal/analyzer/VulnerabilityIndexerTest.java diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/internal/ExtensionIndexSolrCoreInitializer.java b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/internal/ExtensionIndexSolrCoreInitializer.java index 4a1a0733d2b..571a5dbea0f 100644 --- a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/internal/ExtensionIndexSolrCoreInitializer.java +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/internal/ExtensionIndexSolrCoreInitializer.java @@ -140,6 +140,28 @@ public class ExtensionIndexSolrCoreInitializer extends AbstractSolrCoreInitializ */ public static final String SECURITY_ADVICE = "security_advice"; + /** + * When {@code true} the extension is provided by the environemnt (e.g., from the servlet engine), {@code false} + * otherwise. + */ + public static final String IS_FROM_ENVIRONMENT = "is_from_environment"; + + /** + * When {@code true} the extension is installed, otherwise the extension is provided by the core. + */ + public static final String IS_INSTALLED_EXTENSION = "is_installed"; + + /** + * When {@code true} the extension has been reviewed and is not is considered as safe, {@code false} otherwise. + */ + public static final String IS_REVIEWED_SAFE = "security_is_reviewed_safe"; + + /** + * Contains the explanations regarding why a given vulnerability can be considered as safe. This field contains an + * array of html contents. + */ + public static final String IS_SAFE_EXPLANATIONS = "security_is_safe_explanations"; + private static final Pattern COMPONENT_SPECIAL_CHARS = Pattern.compile("[<>,]+"); private static final long SCHEMA_VERSION_12_9 = 120900000; @@ -150,10 +172,12 @@ public class ExtensionIndexSolrCoreInitializer extends AbstractSolrCoreInitializ private static final long SCHEMA_VERSION_15_5 = 150500000; + private static final long SCHEMA_VERSION_15_6 = 150600000; + @Override protected long getVersion() { - return SCHEMA_VERSION_15_5; + return SCHEMA_VERSION_15_6; } @Override @@ -227,6 +251,13 @@ protected void migrateSchema(long cversion) throws SolrException setStringField(SECURITY_FIX_VERSION, false, false); setStringField(SECURITY_ADVICE, false, false); } + + if (cversion < SCHEMA_VERSION_15_6) { + setBooleanField(IS_FROM_ENVIRONMENT, false, false); + setBooleanField(IS_INSTALLED_EXTENSION, false, false); + setBooleanField(IS_REVIEWED_SAFE, true, false); + setStringField(IS_SAFE_EXPLANATIONS, true, false); + } } /** diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/internal/ExtensionIndexStore.java b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/internal/ExtensionIndexStore.java index c8fcb57d5c3..c6d816bc255 100644 --- a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/internal/ExtensionIndexStore.java +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/internal/ExtensionIndexStore.java @@ -28,6 +28,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -82,6 +83,10 @@ import org.xwiki.search.solr.SolrException; import org.xwiki.search.solr.SolrUtils; +import static org.xwiki.extension.index.internal.ExtensionIndexSolrCoreInitializer.IS_INSTALLED_EXTENSION; +import static org.xwiki.extension.index.internal.ExtensionIndexSolrCoreInitializer.IS_SAFE_EXPLANATIONS; +import static org.xwiki.extension.index.internal.ExtensionIndexSolrCoreInitializer.IS_FROM_ENVIRONMENT; +import static org.xwiki.extension.index.internal.ExtensionIndexSolrCoreInitializer.IS_REVIEWED_SAFE; import static org.xwiki.extension.index.internal.ExtensionIndexSolrCoreInitializer.SECURITY_ADVICE; import static org.xwiki.extension.index.internal.ExtensionIndexSolrCoreInitializer.SECURITY_CVE_COUNT; import static org.xwiki.extension.index.internal.ExtensionIndexSolrCoreInitializer.SECURITY_CVE_CVSS; @@ -311,7 +316,7 @@ public void update(ExtensionId extensionId, ExtensionSecurityAnalysisResult resu result.getMaxCVSS(), doc); } else { // Remove the CVSS score if the new list of security vulnerabilities becomes empty. - this.utils.setAtomic(SolrUtils.ATOMIC_UPDATE_MODIFIER_REMOVE, SECURITY_MAX_CVSS, 0.0, Double.class, doc); + this.utils.setAtomic(SolrUtils.ATOMIC_UPDATE_MODIFIER_SET, SECURITY_MAX_CVSS, null, doc); } Stream<String> cveIds = result.getSecurityVulnerabilities().stream().map(SecurityVulnerabilityDescriptor::getId); @@ -325,6 +330,7 @@ public void update(ExtensionId extensionId, ExtensionSecurityAnalysisResult resu .map(SecurityVulnerabilityDescriptor::getScore).collect(Collectors.toList()), doc); String fixVersion = result.getSecurityVulnerabilities().stream() .map(SecurityVulnerabilityDescriptor::getFixVersion) + .filter(Objects::nonNull) .max(Comparator.naturalOrder()) .map(Version::getValue) .orElse(null); @@ -332,6 +338,20 @@ public void update(ExtensionId extensionId, ExtensionSecurityAnalysisResult resu this.utils.setAtomic(SolrUtils.ATOMIC_UPDATE_MODIFIER_SET, SECURITY_ADVICE, result.getAdvice(), doc); this.utils.setAtomic(SolrUtils.ATOMIC_UPDATE_MODIFIER_SET, SECURITY_CVE_COUNT, result.getSecurityVulnerabilities().size(), doc); + this.utils.setAtomic(SolrUtils.ATOMIC_UPDATE_MODIFIER_SET, IS_FROM_ENVIRONMENT, result.isFromEnvironment(), + doc); + this.utils.setAtomic(SolrUtils.ATOMIC_UPDATE_MODIFIER_SET, IS_INSTALLED_EXTENSION, + result.isInstalledExtension(), doc); + List<Boolean> safeMapping = result.getSecurityVulnerabilities() + .stream() + .map(SecurityVulnerabilityDescriptor::isSafe) + .collect(Collectors.toList()); + this.utils.setAtomic(SolrUtils.ATOMIC_UPDATE_MODIFIER_SET, IS_REVIEWED_SAFE, safeMapping, doc); + List<String> reviewExplanations = result.getSecurityVulnerabilities() + .stream() + .map(SecurityVulnerabilityDescriptor::getReviews) + .collect(Collectors.toList()); + this.utils.setAtomic(SolrUtils.ATOMIC_UPDATE_MODIFIER_SET, IS_SAFE_EXPLANATIONS, reviewExplanations, doc); add(doc); commit(); @@ -450,7 +470,7 @@ private void updateCompatible(SolrInputDocument document, String namespace, Bool } if (incompatible != null) { - this.utils.setAtomic(incompatible.booleanValue() ? SolrUtils.ATOMIC_UPDATE_MODIFIER_ADD_DISTINCT + this.utils.setAtomic(incompatible.booleanValue() ? SolrUtils.ATOMIC_UPDATE_MODIFIER_ADD_DISTINCT : SolrUtils.ATOMIC_UPDATE_MODIFIER_REMOVE, ExtensionIndexSolrCoreInitializer.SOLR_FIELD_INCOMPATIBLE_NAMESPACES, this.extensionIndexSolrUtil.toStoredNamespace(namespace), document); diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/security/ExtensionSecurityAnalysisResult.java b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/security/ExtensionSecurityAnalysisResult.java index 48ceaa3eddb..644b20199bf 100644 --- a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/security/ExtensionSecurityAnalysisResult.java +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/security/ExtensionSecurityAnalysisResult.java @@ -41,6 +41,10 @@ public class ExtensionSecurityAnalysisResult private String advice; + private boolean fromEnvironment; + + private boolean isInstalledExtension; + /** * @param securityVulnerabilities the security vulnerabilities associated with the analyzed extension * @return the current object @@ -68,6 +72,32 @@ public String getAdvice() return this.advice; } + /** + * @return {@code true} when the extension is provided by the environment (e.g., from a servlet engine), + * {@code false} when the extension is from xwiki core, an installable extension, or their transitive + * dependencies + * @since 15.6RC1 + */ + @Unstable + public boolean isFromEnvironment() + { + return this.fromEnvironment; + } + + /** + * @param fromEnvironment {@code true} when the extension is provided by the environment (e.g., from a servlet + * engine), {@code false} when the extension is from xwiki core, an installable extension, or their transitive + * dependencies + * @return the current object + * @since 15.6RC1 + */ + @Unstable + public ExtensionSecurityAnalysisResult setFromEnvironment(boolean fromEnvironment) + { + this.fromEnvironment = fromEnvironment; + return this; + } + /** * @param advice the translation key of the advice applicable on the security analysis (e.g., how to upgrade the * extension to fix the identified security vulnerabilities) @@ -95,6 +125,26 @@ public Double getMaxCVSS() } } + /** + * @param isInstalledExtension {@code true} when the extension is installed, {@code false} otherwise + * @since 15.6RC1 + */ + @Unstable + public void setInstalledExtension(boolean isInstalledExtension) + { + this.isInstalledExtension = isInstalledExtension; + } + + /** + * @return {@code true} when the extension is installed, {@code false} otherwise + * @since 15.6RC1 + */ + @Unstable + public boolean isInstalledExtension() + { + return this.isInstalledExtension; + } + @Override public boolean equals(Object o) { @@ -111,6 +161,8 @@ public boolean equals(Object o) return new EqualsBuilder() .append(this.securityVulnerabilities, that.securityVulnerabilities) .append(this.advice, that.advice) + .append(this.fromEnvironment, that.fromEnvironment) + .append(this.isInstalledExtension, that.isInstalledExtension) .isEquals(); } @@ -120,6 +172,8 @@ public int hashCode() return new HashCodeBuilder(17, 37) .append(this.securityVulnerabilities) .append(this.advice) + .append(this.fromEnvironment) + .append(this.isInstalledExtension) .toHashCode(); } @@ -129,6 +183,8 @@ public String toString() return new XWikiToStringBuilder(this) .append("securityVulnerabilities", this.securityVulnerabilities) .append("advice", this.advice) + .append("fromEnvironment", this.fromEnvironment) + .append("isInstalledExtension", this.isInstalledExtension) .toString(); } } diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/security/SecurityVulnerabilityDescriptor.java b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/security/SecurityVulnerabilityDescriptor.java index d0ffe3d8d07..6a584ed6b8e 100644 --- a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/security/SecurityVulnerabilityDescriptor.java +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/security/SecurityVulnerabilityDescriptor.java @@ -19,6 +19,7 @@ */ package org.xwiki.extension.index.security; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.xwiki.extension.version.Version; @@ -44,6 +45,10 @@ public class SecurityVulnerabilityDescriptor private Version fixVersion; + private boolean safe; + + private String reviews; + /** * @param id the security vulnerability id * @return the current object @@ -89,7 +94,9 @@ public String getURL() */ public SecurityVulnerabilityDescriptor setSeverityScore(String vector) { - this.score = Cvss.fromVector(vector).calculateScore().getBaseScore(); + if (StringUtils.isNotEmpty(vector)) { + this.score = Cvss.fromVector(vector).calculateScore().getBaseScore(); + } return this; } @@ -129,6 +136,54 @@ public SecurityVulnerabilityDescriptor setFixVersion(Version fixVersion) return this; } + /** + * @return {@code true} when the extension has some known vulnerabilities, but all are reviewed as safe, + * {@code false} otherwise + * @since 15.6RC1 + */ + @Unstable + public boolean isSafe() + { + return this.safe; + } + + /** + * @param safe {@code true} when the extension has some known vulnerabilities, but all are reviewed as safe, + * {@code false} otherwise + * @return the current object + * @since 15.6RC1 + */ + @Unstable + public SecurityVulnerabilityDescriptor setSafe(boolean safe) + { + this.safe = safe; + return this; + } + + /** + * @return the ignored vulnerabilities description, this is an html content containing all the false-positive + * analysis + * @since 15.6RC1 + */ + @Unstable + public String getReviews() + { + return this.reviews; + } + + /** + * @param reviews the ignored vulnerabilities description, this is an html content containing all the + * false-positive analysis + * @return the current object + * @since 15.6RC1 + */ + @Unstable + public SecurityVulnerabilityDescriptor setReviews(String reviews) + { + this.reviews = reviews; + return this; + } + @Override public boolean equals(Object o) { @@ -147,6 +202,8 @@ public boolean equals(Object o) .append(this.id, that.id) .append(this.url, that.url) .append(this.fixVersion, that.fixVersion) + .append(this.safe, that.safe) + .append(this.reviews, that.reviews) .isEquals(); } @@ -158,6 +215,8 @@ public int hashCode() .append(this.url) .append(this.score) .append(this.fixVersion) + .append(this.safe) + .append(this.reviews) .toHashCode(); } @@ -169,6 +228,8 @@ public String toString() .append("url", this.url) .append("score", this.score) .append("fixVersion", this.fixVersion) + .append("safe", this.safe) + .append("reviews", this.reviews) .toString(); } } diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/security/review/Review.java b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/security/review/Review.java new file mode 100644 index 00000000000..e85c0d5a0bb --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/security/review/Review.java @@ -0,0 +1,134 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.extension.index.security.review; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.xwiki.stability.Unstable; +import org.xwiki.text.XWikiToStringBuilder; + +/** + * Contains the metadata relative to the review of a vulnerability. + * + * @version $Id$ + * @since 15.6RC1 + */ +@Unstable +public class Review +{ + private String emitter; + + private String explanation; + + private ReviewResult result; + + /** + * @return the {@code source} of the analysis (e.g., {@code xwiki-platform}, or the name of the extension for which + * the analysis has been done) + */ + public String getEmitter() + { + return this.emitter; + } + + /** + * @param emitter the {@code source} of the analysis (e.g., {@code xwiki-platform}, or the name of the extension + * for which the analysis has been done) + */ + public void setEmitter(String emitter) + { + this.emitter = emitter; + } + + /** + * @return the textual explanation, detailing why a given CVE should not be considered as a security vulnerability + * in the context of the {@code source} + */ + public String getExplanation() + { + return this.explanation; + } + + /** + * @param explanation the textual explanation, detailing why a given CVE should not be considered as a security + * vulnerability in the context of the {@code source} + */ + public void setExplanation(String explanation) + { + this.explanation = explanation; + } + + /** + * @return the result of the vulnerability review + */ + public ReviewResult getResult() + { + return this.result; + } + + /** + * @param result the result of the vulnerability review + */ + public void setResult(ReviewResult result) + { + this.result = result; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + Review review = (Review) o; + + return new EqualsBuilder() + .append(this.emitter, review.emitter) + .append(this.explanation, review.explanation) + .append(this.result, review.result) + .isEquals(); + } + + @Override + public int hashCode() + { + return new HashCodeBuilder(17, 37) + .append(this.emitter) + .append(this.explanation) + .append(this.result) + .toHashCode(); + } + + @Override + public String toString() + { + return new XWikiToStringBuilder(this) + .append("emitter", this.emitter) + .append("explanation", this.explanation) + .append("result", this.result) + .toString(); + } + +} diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/security/review/ReviewResult.java b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/security/review/ReviewResult.java new file mode 100644 index 00000000000..3391980c22d --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/security/review/ReviewResult.java @@ -0,0 +1,42 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.extension.index.security.review; + +import org.xwiki.stability.Unstable; + +/** + * The review result of a vulnerability. + * + * @version $Id$ + * @since 15.6RC1 + */ +@Unstable +public enum ReviewResult +{ + /** + * When the conclusion of the analysis is that the vulnerability is safe. + */ + SAFE, + + /** + * When the conclusion of the analysis is that the vulnerability can be unsafe. + */ + UNSAFE +} diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/security/review/ReviewsMap.java b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/security/review/ReviewsMap.java new file mode 100644 index 00000000000..ea7de7ed746 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-index/src/main/java/org/xwiki/extension/index/security/review/ReviewsMap.java @@ -0,0 +1,102 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.extension.index.security.review; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.xwiki.stability.Unstable; +import org.xwiki.text.XWikiToStringBuilder; + +/** + * Contains the maps of all the CVEs with available reviews. + * + * @version $Id$ + * @since 15.6RC1 + */ +@Unstable +public class ReviewsMap +{ + private final Map<String, List<Review>> reviewsMap = new HashMap<>(); + + /** + * @return the map of CVEs and their associated reviews. + */ + public Map<String, List<Review>> getReviewsMap() + { + return this.reviewsMap; + } + + /** + * @param id a CVE id + * @return {@code true} if at least a review is available for a given id + */ + public boolean contains(String id) + { + return this.reviewsMap.containsKey(id); + } + + /** + * @param id a CVE id + * @return the list of reviews if found, {@link Optional#empty()} otherwise + */ + public Optional<List<Review>> getById(String id) + { + return Optional.ofNullable(this.reviewsMap.get(id)); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + ReviewsMap that = (ReviewsMap) o; + + return new EqualsBuilder() + .append(this.reviewsMap, that.reviewsMap) + .isEquals(); + } + + @Override + public int hashCode() + { + return new HashCodeBuilder(17, 37) + .append(this.reviewsMap) + .toHashCode(); + } + + @Override + public String toString() + { + return new XWikiToStringBuilder(this) + .append("reviewsMap", this.reviewsMap) + .toString(); + } +} diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/main/java/org/xwiki/extension/security/internal/ExtensionSecuritySolrClient.java b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/main/java/org/xwiki/extension/security/internal/ExtensionSecuritySolrClient.java index f4161b46e29..973ce8024c7 100644 --- a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/main/java/org/xwiki/extension/security/internal/ExtensionSecuritySolrClient.java +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/main/java/org/xwiki/extension/security/internal/ExtensionSecuritySolrClient.java @@ -37,6 +37,7 @@ import org.xwiki.search.solr.SolrUtils; import static org.xwiki.extension.InstalledExtension.FIELD_INSTALLED_NAMESPACES; +import static org.xwiki.extension.index.internal.ExtensionIndexSolrCoreInitializer.IS_FROM_ENVIRONMENT; import static org.xwiki.extension.index.internal.ExtensionIndexSolrCoreInitializer.SECURITY_FIX_VERSION; import static org.xwiki.extension.index.internal.ExtensionIndexSolrCoreInitializer.SECURITY_MAX_CVSS; import static org.xwiki.extension.index.internal.ExtensionIndexSolrCoreInitializer.SOLR_FIELD_EXTENSIONID; @@ -67,6 +68,8 @@ public class ExtensionSecuritySolrClient WIKIS, FIELD_INSTALLED_NAMESPACES ); + private static final String EXACT_MATCH_PATTERN = "%s:%s"; + @Inject private ExtensionIndexStore extensionIndexStore; @@ -87,6 +90,8 @@ public long getVulnerableExtensionsCount() throws SolrServerException, IOExcepti this.extensionIndexStore.createSolrQuery(new ExtensionQuery(), solrQuery); initFilter(solrQuery); + // Exclude the extensions with only safe vulnerabilities. + solrQuery.addFilterQuery("security_is_reviewed_safe:false"); QueryResponse search = this.extensionIndexStore.search(solrQuery); return search.getResults().getNumFound(); } @@ -137,6 +142,13 @@ private void initFilter(LiveDataQuery liveDataQuery, SolrQuery solrQuery) } } } + + Map<String, Object> parametersMap = liveDataQuery.getSource().getParameters(); + String isFromEnvironment = Boolean.TRUE.toString(); + if (!Objects.equals(parametersMap.get("isFromEnvironment"), Boolean.TRUE.toString())) { + isFromEnvironment = Boolean.FALSE.toString(); + } + solrQuery.addFilterQuery(String.format(EXACT_MATCH_PATTERN, IS_FROM_ENVIRONMENT, isFromEnvironment)); } private static void initFilter(SolrQuery solrQuery) @@ -146,8 +158,8 @@ private static void initFilter(SolrQuery solrQuery) } // Only include extensions with a computed CVSS score, meaning that they have at least one known security // vulnerability. - solrQuery.addFilterQuery(String.format("%s:[0 TO 10]", SECURITY_MAX_CVSS)); - solrQuery.addFilterQuery(FIELD_INSTALLED_NAMESPACES + ":[* TO *]"); + solrQuery.addFilterQuery(String.format("%s:{0 TO 10]", SECURITY_MAX_CVSS)); + solrQuery.addFilterQuery(String.format("(%s:[* TO *] OR is_installed:false)", FIELD_INSTALLED_NAMESPACES)); } private static void initSort(LiveDataQuery liveDataQuery, SolrQuery solrQuery) diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/main/java/org/xwiki/extension/security/internal/SolrToLiveDataEntryMapper.java b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/main/java/org/xwiki/extension/security/internal/SolrToLiveDataEntryMapper.java index 85c1ce6fb77..9fe531cfcb5 100644 --- a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/main/java/org/xwiki/extension/security/internal/SolrToLiveDataEntryMapper.java +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/main/java/org/xwiki/extension/security/internal/SolrToLiveDataEntryMapper.java @@ -20,14 +20,18 @@ package org.xwiki.extension.security.internal; import java.nio.charset.Charset; -import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; +import java.util.function.IntPredicate; +import java.util.stream.Collectors; import java.util.stream.IntStream; import javax.inject.Inject; import javax.inject.Singleton; +import javax.script.ScriptContext; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.message.BasicNameValuePair; @@ -39,14 +43,19 @@ import org.xwiki.extension.index.internal.ExtensionIndexStore; import org.xwiki.localization.ContextualLocalizationManager; import org.xwiki.model.reference.LocalDocumentReference; +import org.xwiki.script.ScriptContextManager; import org.xwiki.search.solr.SolrUtils; import org.xwiki.search.solr.internal.api.FieldUtils; +import org.xwiki.template.TemplateManager; import static com.xpn.xwiki.web.ViewAction.VIEW_ACTION; import static java.util.Map.entry; import static java.util.Map.ofEntries; import static java.util.stream.Collectors.joining; +import static javax.script.ScriptContext.ENGINE_SCOPE; import static org.apache.commons.lang.StringEscapeUtils.escapeXml; +import static org.xwiki.extension.index.internal.ExtensionIndexSolrCoreInitializer.IS_REVIEWED_SAFE; +import static org.xwiki.extension.index.internal.ExtensionIndexSolrCoreInitializer.IS_SAFE_EXPLANATIONS; import static org.xwiki.extension.index.internal.ExtensionIndexSolrCoreInitializer.SECURITY_ADVICE; import static org.xwiki.extension.index.internal.ExtensionIndexSolrCoreInitializer.SECURITY_CVE_CVSS; import static org.xwiki.extension.index.internal.ExtensionIndexSolrCoreInitializer.SECURITY_CVE_ID; @@ -71,6 +80,8 @@ @Singleton public class SolrToLiveDataEntryMapper { + private static final String EXTENSION_ID = "extensionId"; + @Inject private SolrUtils solrUtils; @@ -83,6 +94,12 @@ public class SolrToLiveDataEntryMapper @Inject private ExtensionIndexStore extensionIndexStore; + @Inject + private ScriptContextManager scriptContextManager; + + @Inject + private TemplateManager templateManager; + /** * @param doc the document to convert to Live Data entries. * @return Converts a {@link SolrDocument} to a {@link Map} of Live Data entries. @@ -99,28 +116,79 @@ public Map<String, Object> mapDocToEntries(SolrDocument doc) ); } - private static String buildCVEList(SolrDocument doc) + private String buildCVEList(SolrDocument doc) + { + // The CVEs of the current extension vulnerabilities. + ScriptContext currentScriptContext = this.scriptContextManager.getCurrentScriptContext(); + currentScriptContext.setAttribute("cveIds", mapToStrings(doc, SECURITY_CVE_ID), ENGINE_SCOPE); + // The CVE links of the current extension vulnerabilities. + currentScriptContext.setAttribute("cveLinks", mapToStrings(doc, SECURITY_CVE_LINK), ENGINE_SCOPE); + + // The CVSS of the current extension vulnerabilities. + currentScriptContext.setAttribute("cveCVSS", mapToStrings(doc, SECURITY_CVE_CVSS), ENGINE_SCOPE); + + List<Boolean> safe = getSafe(doc); + currentScriptContext.setAttribute("notSafeCVEsIndex", getNotSafeCVEsIndex(doc, safe), ENGINE_SCOPE); + // The index of safe CVEs. + currentScriptContext.setAttribute("safeCVEsIndex", getSafeCVEsIndex(doc, safe), ENGINE_SCOPE); + currentScriptContext.setAttribute(EXTENSION_ID, buildExtensionId(doc), ENGINE_SCOPE); + currentScriptContext.setAttribute("messages", mapToStrings(doc, IS_SAFE_EXPLANATIONS), ENGINE_SCOPE); + + return this.templateManager.renderNoException("extension/security/liveData/cveID.vm"); + } + + private static List<Boolean> getSafe(SolrDocument doc) + { + // The list of safe CVEs. + return Optional.ofNullable(doc.getFieldValues(IS_REVIEWED_SAFE)) + .map(values -> values.stream() + .map(it -> (boolean) it) + .collect(Collectors.toList())) + .orElse(List.of()); + } + + private static List<Integer> getNotSafeCVEsIndex(SolrDocument doc, List<Boolean> safe) { - List<Object> cveIds = new ArrayList<>(doc.getFieldValues(SECURITY_CVE_ID)); - List<Object> cveLinks = new ArrayList<>(doc.getFieldValues(SECURITY_CVE_LINK)); - List<Object> cveCVSS = new ArrayList<>(doc.getFieldValues(SECURITY_CVE_CVSS)); - - return IntStream.range(0, cveIds.size()) - .mapToObj(value -> String.format("<a href='%s'>%s</a> (%s)", - escapeXml(String.valueOf(cveLinks.get(value))), - escapeXml(String.valueOf(cveIds.get(value))), - escapeXml(String.valueOf(cveCVSS.get(value))))) - .collect(joining("<br/>")); + // The index of non-safe CVEs. + return IntStream.range(0, mapToStrings(doc, SECURITY_CVE_ID).size()) + .filter(((IntPredicate) safe::get).negate()) + .boxed() + .collect(Collectors.toList()); + } + + private static List<Integer> getSafeCVEsIndex(SolrDocument doc, List<Boolean> safe) + { + return IntStream.range(0, mapToStrings(doc, SECURITY_CVE_ID).size()) + .filter(safe::get) + .boxed() + .collect(Collectors.toList()); + } + + private static List<String> mapToStrings(SolrDocument doc, String name) + { + Collection<Object> fieldValues = doc.getFieldValues(name); + if (fieldValues == null) { + return List.of(); + } + return fieldValues.stream().map(String::valueOf).collect(Collectors.toList()); } private String buildAdvice(SolrDocument doc) { - return this.l10n.getTranslationPlain(this.solrUtils.get(SECURITY_ADVICE, doc)); + String advice = this.l10n.getTranslationPlain(this.solrUtils.get(SECURITY_ADVICE, doc)); + if (advice == null) { + return ""; + } + return advice; } private String buildFixVersion(SolrDocument doc) { - return this.solrUtils.get(SECURITY_FIX_VERSION, doc); + Object fixVersion = doc.get(SECURITY_FIX_VERSION); + if (fixVersion == null) { + return ""; + } + return String.valueOf(fixVersion); } private Double buildMaxCVSS(SolrDocument doc) @@ -166,7 +234,7 @@ private static List<BasicNameValuePair> buildExtensionURLParameters(String exten { return List.of( new BasicNameValuePair("section", "XWiki.Extensions"), - new BasicNameValuePair("extensionId", extensionId), + new BasicNameValuePair(EXTENSION_ID, extensionId), new BasicNameValuePair("extensionVersion", extensionVersion) ); } diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/main/resources/ApplicationResources.properties b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/main/resources/ApplicationResources.properties index f7a7cadb374..e71d015a7a0 100644 --- a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/main/resources/ApplicationResources.properties +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/main/resources/ApplicationResources.properties @@ -21,6 +21,8 @@ extension.security.liveData.name=Name extension.security.liveData.extensionId=Extension Id extension.security.liveData.maxCVSS=Max CVSS extension.security.liveData.cveID=CVE IDs +extension.security.liveData.cveID.modal.openButton=Display reviews for {0} +extension.security.liveData.cveID.modal.title=Vulnerability {0} of {1} extension.security.liveData.fixVersion=Latest Fix Version extension.security.liveData.advice=Advice extension.security.liveData.wikis=Wikis \ No newline at end of file diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/main/resources/templates/extension/security/liveData/cveID.vm b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/main/resources/templates/extension/security/liveData/cveID.vm new file mode 100644 index 00000000000..8af889067e0 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/main/resources/templates/extension/security/liveData/cveID.vm @@ -0,0 +1,65 @@ +## --------------------------------------------------------------------------- +## See the NOTICE file distributed with this work for additional +## information regarding copyright ownership. +## +## This is free software; you can redistribute it and/or modify it +## under the terms of the GNU Lesser General Public License as +## published by the Free Software Foundation; either version 2.1 of +## the License, or (at your option) any later version. +## +## This software is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +## Lesser General Public License for more details. +## +## You should have received a copy of the GNU Lesser General Public +## License along with this software; if not, write to the Free +## Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +## 02110-1301 USA, or see the FSF site: http://www.fsf.org. +## --------------------------------------------------------------------------- +## Since 15.6RC1 +## This template renders the content of the CVEs list of an extension. +## The CVEs list is enriched with links to the CVEs description page, their corresponding CVSS scores. +## Additionally, reviewed CVSS have a popup with providing more details explanations. +## --------------------------------------------------------------------------- +#macro (showCVEs $indexes $isSafe) + #foreach($index in $indexes) + #set ($cveId = $cveIds.get($index)) + #set ($message = $messages[$index]) + <a href="$cveLinks[$index]" class="#if ($isSafe)xHint#end">$cveId</a> + <span class="#if ($isSafe)xHint#end">($cveCVSS[$index])</span> + #if ("$!message" != '') + <button type="button" class="btn btn-default btn-xs #if ($isSafe)xHint#end" data-toggle="modal" + data-target="#$escapetool.xml($cveId)" + aria-controls="$escapetool.xml($cveId)"> + <span class="sr-only"> + $escapetool.xml($services.localization.render("extension.security.liveData.cveID.modal.openButton", [$cveId])) + </span> + $services.icon.renderHTML('file-text') + </button> + <div class="modal fade security-vulnerability-detail-modal" tabindex="-1" role="dialog" + id='$escapetool.xml($cveId)'> + <div class="modal-dialog modal-lg" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + <h2 class="modal-title"> + $escapetool.xml($services.localization.render( + "extension.security.liveData.cveID.modal.title", [$cveId, $extensionId])) + </h2> + </div> + <div class="modal-body"> + $message + </div> + </div> + </div> + </div> + #end + <br> + #end +#end +## +#showCVEs($notSafeCVEsIndex false) +#showCVEs($safeCVEsIndex true) diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/test/java/org/xwiki/extension/security/internal/ExtensionSecuritySolrClientTest.java b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/test/java/org/xwiki/extension/security/internal/ExtensionSecuritySolrClientTest.java index f5d7ca40626..4eb90e0c854 100644 --- a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/test/java/org/xwiki/extension/security/internal/ExtensionSecuritySolrClientTest.java +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/test/java/org/xwiki/extension/security/internal/ExtensionSecuritySolrClientTest.java @@ -22,6 +22,7 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.response.QueryResponse; @@ -37,13 +38,16 @@ import org.xwiki.test.junit5.mockito.MockComponent; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.xwiki.extension.InstalledExtension.FIELD_INSTALLED_NAMESPACES; +import static org.xwiki.extension.index.internal.ExtensionIndexSolrCoreInitializer.IS_FROM_ENVIRONMENT; +import static org.xwiki.extension.index.internal.ExtensionIndexSolrCoreInitializer.IS_INSTALLED_EXTENSION; +import static org.xwiki.extension.index.internal.ExtensionIndexSolrCoreInitializer.SECURITY_MAX_CVSS; import static org.xwiki.extension.security.internal.livedata.ExtensionSecurityLiveDataConfigurationProvider.FIX_VERSION; /** @@ -79,8 +83,9 @@ void getExtensionsCount() throws Exception assertEquals(42, this.solrClient.getVulnerableExtensionsCount()); SolrQuery params = new SolrQuery(); - params.addFilterQuery("security_maxCVSS:[0 TO 10]"); - params.addFilterQuery(FIELD_INSTALLED_NAMESPACES + ":[* TO *]"); + params.addFilterQuery(String.format("%s:{0 TO 10]", SECURITY_MAX_CVSS)); + params.addFilterQuery(String.format("(%s:[* TO *] OR %s:false)", FIELD_INSTALLED_NAMESPACES, + IS_INSTALLED_EXTENSION)); verify(this.extensionIndexStore) .search(ArgumentMatchers.<SolrQuery>argThat( t -> Arrays.equals(t.getFilterQueries(), params.getFilterQueries()))); @@ -101,15 +106,55 @@ void solrQuery() throws Exception liveDataQuery.setOffset(0L); liveDataQuery.setSort(List.of()); liveDataQuery.setFilters(List.of(new LiveDataQuery.Filter(FIX_VERSION, "match", "15.5"))); + liveDataQuery.setSource(new LiveDataQuery.Source()); this.solrClient.solrQuery(liveDataQuery); SolrQuery params = new SolrQuery(); - params.addFilterQuery("security_maxCVSS:[0 TO 10]"); - params.addFilterQuery(FIELD_INSTALLED_NAMESPACES + ":[* TO *]"); + params.addFilterQuery(SECURITY_MAX_CVSS + ":{0 TO 10]"); + params.addFilterQuery(IS_FROM_ENVIRONMENT + ":false"); + params.addFilterQuery(String.format("(%s:[* TO *] OR %s:false)", FIELD_INSTALLED_NAMESPACES, + IS_INSTALLED_EXTENSION)); verify(this.extensionIndexStore) .search(AdditionalMatchers.<SolrQuery>and( - argThat(t -> Arrays.equals(t.getFilterQueries(), params.getFilterQueries())), + argThat(t -> Arrays.stream(t.getFilterQueries()).sorted().collect(Collectors.toList()) + .equals(Arrays.stream(params.getFilterQueries()).sorted().collect( + Collectors.toList()))), + argThat(t -> Objects.equals(t.getSorts(), params.getSorts()))) + ); + } + + @Test + void solrQueryIsFromEnvironment() throws Exception + { + doAnswer(invocationOnMock -> { + SolrQuery solrQuery = invocationOnMock.getArgument(1); + solrQuery.setFilterQueries(""); + solrQuery.setSort("fake", SolrQuery.ORDER.asc); + return null; + }).when(this.extensionIndexStore).createSolrQuery(any(), any()); + + LiveDataQuery liveDataQuery = new LiveDataQuery(); + liveDataQuery.setLimit(10); + liveDataQuery.setOffset(0L); + liveDataQuery.setSort(List.of()); + liveDataQuery.setFilters(List.of(new LiveDataQuery.Filter(FIX_VERSION, "match", "15.5"))); + LiveDataQuery.Source source = new LiveDataQuery.Source(); + source.getParameters().put("isFromEnvironment", "true"); + liveDataQuery.setSource(source); + this.solrClient.solrQuery(liveDataQuery); + + SolrQuery params = new SolrQuery(); + params.addFilterQuery(SECURITY_MAX_CVSS + ":{0 TO 10]"); + params.addFilterQuery(IS_FROM_ENVIRONMENT + ":true"); + params.addFilterQuery(String.format("(%s:[* TO *] OR %s:false)", FIELD_INSTALLED_NAMESPACES, + IS_INSTALLED_EXTENSION)); + + verify(this.extensionIndexStore) + .search(AdditionalMatchers.<SolrQuery>and( + argThat(t -> Arrays.stream(t.getFilterQueries()).sorted().collect(Collectors.toList()) + .equals(Arrays.stream(params.getFilterQueries()).sorted().collect( + Collectors.toList()))), argThat(t -> Objects.equals(t.getSorts(), params.getSorts()))) ); } diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/test/java/org/xwiki/extension/security/internal/SolrToLiveDataEntryMapperTest.java b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/test/java/org/xwiki/extension/security/internal/SolrToLiveDataEntryMapperTest.java index c09a0b03895..349d9ddc2dd 100644 --- a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/test/java/org/xwiki/extension/security/internal/SolrToLiveDataEntryMapperTest.java +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-api/src/test/java/org/xwiki/extension/security/internal/SolrToLiveDataEntryMapperTest.java @@ -22,6 +22,9 @@ import java.util.List; import java.util.Map; +import javax.inject.Inject; +import javax.script.ScriptContext; + import org.apache.solr.common.SolrDocument; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -29,8 +32,10 @@ import org.xwiki.extension.ExtensionId; import org.xwiki.extension.index.internal.ExtensionIndexStore; import org.xwiki.localization.ContextualLocalizationManager; +import org.xwiki.script.ScriptContextManager; import org.xwiki.search.solr.SolrUtils; import org.xwiki.search.solr.internal.api.FieldUtils; +import org.xwiki.template.TemplateManager; import org.xwiki.test.junit5.mockito.ComponentTest; import org.xwiki.test.junit5.mockito.InjectMockComponents; import org.xwiki.test.junit5.mockito.MockComponent; @@ -66,12 +71,26 @@ class SolrToLiveDataEntryMapperTest @MockComponent private ExtensionIndexStore extensionIndexStore; + @MockComponent + private ScriptContextManager scriptContextManager; + + @MockComponent + private TemplateManager templateManager; + + @Mock private SolrDocument doc; + @Mock + private ScriptContext scriptContext; + @Test void mapDocToEntries() { + when(this.scriptContextManager.getCurrentScriptContext()).thenReturn(this.scriptContext); + when(this.templateManager.renderNoException("extension/security/liveData/cveID.vm")) + .thenReturn("template content"); + when(this.extensionIndexStore.getExtensionId(this.doc)).thenReturn(new ExtensionId("org.test:ext", "7.5")); when(this.doc.get(FieldUtils.NAME)).thenReturn("Ext Name"); when(this.solrUtils.get(FieldUtils.NAME, this.doc)).thenReturn("Ext Name"); @@ -85,8 +104,8 @@ void mapDocToEntries() "wiki:s1")); when(this.l10n.getTranslationPlain("translation.key")).thenReturn("Translation Value"); assertEquals(Map.of( - "cveID", "", - "fixVersion", "8.4", + "cveID", "template content", + "fixVersion", "", "maxCVSS", 5.0, "advice", "Translation Value", "name", diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/java/org/xwiki/extension/security/internal/ExtensionSecurityJob.java b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/java/org/xwiki/extension/security/internal/ExtensionSecurityJob.java index caf71bdaf97..f2ef3dba562 100644 --- a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/java/org/xwiki/extension/security/internal/ExtensionSecurityJob.java +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/java/org/xwiki/extension/security/internal/ExtensionSecurityJob.java @@ -19,7 +19,14 @@ */ package org.xwiki.extension.security.internal; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import javax.inject.Inject; import javax.inject.Named; @@ -27,8 +34,15 @@ import org.xwiki.component.annotation.Component; import org.xwiki.component.annotation.InstantiationStrategy; import org.xwiki.component.descriptor.ComponentInstantiationStrategy; +import org.xwiki.context.ExecutionContext; +import org.xwiki.context.ExecutionContextException; +import org.xwiki.context.ExecutionContextManager; +import org.xwiki.extension.CoreExtension; +import org.xwiki.extension.Extension; import org.xwiki.extension.InstalledExtension; import org.xwiki.extension.index.security.ExtensionSecurityAnalysisResult; +import org.xwiki.extension.index.security.review.ReviewsMap; +import org.xwiki.extension.repository.CoreExtensionRepository; import org.xwiki.extension.repository.InstalledExtensionRepository; import org.xwiki.extension.security.ExtensionSecurityIndexationEndEvent; import org.xwiki.extension.security.analyzer.ExtensionSecurityAnalyzer; @@ -59,6 +73,9 @@ public class ExtensionSecurityJob @Inject private InstalledExtensionRepository installedExtensionRepository; + @Inject + private CoreExtensionRepository coreExtensionRepository; + @Inject @Named(OsvExtensionSecurityAnalyzer.ID) private ExtensionSecurityAnalyzer extensionSecurityAnalyzer; @@ -66,6 +83,9 @@ public class ExtensionSecurityJob @Inject private VulnerabilityIndexer vulnerabilityIndexer; + @Inject + private ExecutionContextManager executionContextManager; + @Override public String getType() { @@ -75,36 +95,74 @@ public String getType() @Override protected void runInternal() { - Collection<InstalledExtension> installedExtensions = - this.installedExtensionRepository.getInstalledExtensions(); - this.progressManager.pushLevelProgress(installedExtensions.size(), this); + Collection<InstalledExtension> installedExtensions = this.installedExtensionRepository.getInstalledExtensions(); + Collection<CoreExtension> coreExtensions = this.coreExtensionRepository.getCoreExtensions(); + this.progressManager.pushLevelProgress(installedExtensions.size() + coreExtensions.size(), this); + + // TODO: Replace with an actual remote reviews fetch. + ReviewsMap reviewsMap = new ReviewsMap(); try { - // Note: for now, this step is sequential and each extension is analyzed after the previous one. - long newVulnerabilityCount = 0; + ExecutorService executorService = Executors.newFixedThreadPool(10); + + List<Future<Boolean>> tasks = new ArrayList<>(); for (InstalledExtension extension : installedExtensions) { - this.progressManager.startStep(this); - try { - ExtensionSecurityAnalysisResult analysis = this.extensionSecurityAnalyzer.analyze(extension); - if (analysis != null) { - boolean update = this.vulnerabilityIndexer.update(extension, analysis); - if (update) { - newVulnerabilityCount++; - } - } - } catch (ExtensionSecurityException e) { - this.logger.warn("Failed to analyse [{}]. Cause: [{}]", extension.getId().toString(), - getRootCauseMessage(e)); - } catch (Exception e) { - this.logger.warn("Unexpected error [{}]", getRootCauseMessage(e)); - } - this.progressManager.endStep(this); + tasks.add(executorService.submit(() -> handleExtension(extension, reviewsMap))); + } + + for (CoreExtension extension : coreExtensions) { + tasks.add(executorService.submit(() -> handleExtension(extension, reviewsMap))); } - + + long newVulnerabilityCount = consumeTasks(tasks); this.observationManager.notify(new ExtensionSecurityIndexationEndEvent(), null, newVulnerabilityCount); - + } catch (InterruptedException e) { + this.logger.warn("The job has been interrupted. Cause: [{}]", getRootCauseMessage(e)); + Thread.currentThread().interrupt(); } finally { this.progressManager.popLevelProgress(this); } } + + private long consumeTasks(List<Future<Boolean>> tasks) throws InterruptedException + { + long newVulnerabilityCount = 0; + for (Future<Boolean> future : tasks) { + try { + Boolean updated = future.get(); + this.progressManager.startStep(this); + if (Objects.equals(Boolean.TRUE, updated)) { + newVulnerabilityCount++; + } + } catch (ExecutionException e) { + this.logger.error("Failed to execute an extension analysis.", e); + } finally { + this.progressManager.endStep(this); + } + } + return newVulnerabilityCount; + } + + private boolean handleExtension(Extension extension, ReviewsMap reviewsMap) + { + boolean hasNew = false; + try { + this.executionContextManager.initialize(new ExecutionContext()); + ExtensionSecurityAnalysisResult analysis = this.extensionSecurityAnalyzer.analyze(extension); + if (analysis != null) { + boolean update = this.vulnerabilityIndexer.update(extension, analysis, reviewsMap); + if (update) { + hasNew = true; + } + } + } catch (ExtensionSecurityException e) { + this.logger.warn("Failed to analyse [{}]. Cause: [{}]", extension.getId(), getRootCauseMessage(e)); + } catch (ExecutionContextException e) { + this.logger.warn("Failed to initialize the execution context for [{}]. Cause: [{}]", extension, + getRootCauseMessage(e)); + } catch (Exception e) { + this.logger.warn("Unexpected error [{}]", getRootCauseMessage(e)); + } + return hasNew; + } } diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/java/org/xwiki/extension/security/internal/ExtensionSecurityScheduler.java b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/java/org/xwiki/extension/security/internal/ExtensionSecurityScheduler.java index 20c4c79391f..e15076c1f29 100644 --- a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/java/org/xwiki/extension/security/internal/ExtensionSecurityScheduler.java +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/java/org/xwiki/extension/security/internal/ExtensionSecurityScheduler.java @@ -118,6 +118,8 @@ public void run() @Override public void dispose() { - this.executor.shutdownNow(); + if (this.executor != null) { + this.executor.shutdownNow(); + } } } diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/java/org/xwiki/extension/security/internal/analyzer/VulnerabilityIndexer.java b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/java/org/xwiki/extension/security/internal/analyzer/VulnerabilityIndexer.java index 34b3356126b..f504987eb42 100644 --- a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/java/org/xwiki/extension/security/internal/analyzer/VulnerabilityIndexer.java +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/java/org/xwiki/extension/security/internal/analyzer/VulnerabilityIndexer.java @@ -20,19 +20,36 @@ package org.xwiki.extension.security.internal.analyzer; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Path; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.solr.client.solrj.SolrServerException; import org.slf4j.Logger; import org.xwiki.component.annotation.Component; +import org.xwiki.extension.CoreExtension; +import org.xwiki.extension.CoreExtensionFile; import org.xwiki.extension.Extension; +import org.xwiki.extension.ExtensionFile; import org.xwiki.extension.index.internal.ExtensionIndexStore; import org.xwiki.extension.index.security.ExtensionSecurityAnalysisResult; +import org.xwiki.extension.index.security.SecurityVulnerabilityDescriptor; +import org.xwiki.extension.index.security.review.Review; +import org.xwiki.extension.index.security.review.ReviewResult; +import org.xwiki.extension.index.security.review.ReviewsMap; +import static org.apache.commons.lang.StringEscapeUtils.escapeXml; import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage; +import static org.xwiki.extension.index.security.review.ReviewResult.UNSAFE; /** * Update an {@link Extension} with the results of its latest {@link ExtensionSecurityAnalysisResult}. @@ -44,6 +61,21 @@ @Singleton public class VulnerabilityIndexer { + /** + * Shared constant when the suggestion is to upgrade the extension from the Extension Manager. + */ + private static final String UPGRADE_FROM_EM_ADVICE = "extension.security.analysis.advice.upgradeFromEM"; + + /** + * Shared constant when the suggestion is to upgrade the extension XWiki itself. + */ + private static final String UPGRADE_XWIKI_ADVICE = "extension.security.analysis.advice.upgradeXWiki"; + + /** + * Shared constant when the suggestion is to upgrade something else from the environment. + */ + private static final String UPGRADE_ENVIRONMENT_ADVICE = "extension.security.analysis.advice.upgradeEnvironment"; + @Inject private ExtensionIndexStore extensionIndexStore; @@ -55,18 +87,47 @@ public class VulnerabilityIndexer * * @param extension the extension to update * @param analysis the security analysis to update the extension + * @param reviewsMap the map of reviewed CVEs * @return {@code true} if some new security vulnerabilities are inserted, {@code false} otherwise */ - public boolean update(Extension extension, ExtensionSecurityAnalysisResult analysis) + public boolean update(Extension extension, ExtensionSecurityAnalysisResult analysis, ReviewsMap reviewsMap) { try { List<String> cveiDs = this.extensionIndexStore.getCVEIDs(extension.getId()); - boolean hasNew = - analysis.getSecurityVulnerabilities().stream() - .anyMatch(vulnerability -> !cveiDs.contains(vulnerability.getId())); + List<SecurityVulnerabilityDescriptor> securityVulnerabilities = analysis.getSecurityVulnerabilities(); + boolean hasNew = securityVulnerabilities.stream() + .anyMatch(vulnerability -> { + String vulnerabilityId = vulnerability.getId(); + return !cveiDs.contains(vulnerabilityId) && isNotSafe(reviewsMap, vulnerabilityId); + }); + + boolean fromEnvironment = isFromEnvironment(extension); + if (!securityVulnerabilities.isEmpty()) { + if (fromEnvironment) { + analysis.setAdvice(UPGRADE_ENVIRONMENT_ADVICE); + analysis.setInstalledExtension(false); + } else if (extension instanceof CoreExtension) { + analysis.setAdvice(UPGRADE_XWIKI_ADVICE); + analysis.setInstalledExtension(false); + } else { + analysis.setAdvice(UPGRADE_FROM_EM_ADVICE); + analysis.setInstalledExtension(true); + } + } - this.extensionIndexStore.update(extension.getId(), analysis); + securityVulnerabilities.forEach(securityVulnerabilityDescriptor -> { + Optional<List<Review>> byId = reviewsMap.getById(securityVulnerabilityDescriptor.getId()); + if (byId.isPresent()) { + securityVulnerabilityDescriptor.setReviews(formatReviews(byId.get())); + } else { + securityVulnerabilityDescriptor.setReviews(""); + } + securityVulnerabilityDescriptor + .setSafe(!isNotSafe(reviewsMap, securityVulnerabilityDescriptor.getId())); + }); + + this.extensionIndexStore.update(extension.getId(), analysis.setFromEnvironment(fromEnvironment)); return hasNew; } catch (SolrServerException | IOException e) { @@ -76,4 +137,72 @@ public boolean update(Extension extension, ExtensionSecurityAnalysisResult analy return false; } } + + private static String formatReviews(List<Review> reviews) + { + String reviewsFormatted = reviews.stream() + .filter(it -> it.getResult() == ReviewResult.SAFE) + .map(VulnerabilityIndexer::formatReview).collect(Collectors.joining()); + boolean hasUnsafe = reviews.stream().anyMatch(review -> review.getResult() == UNSAFE); + if (hasUnsafe) { + String unsafeReviews = reviews + .stream() + .filter(review -> review.getResult() == UNSAFE) + .map(VulnerabilityIndexer::formatReview) + .collect(Collectors.joining()); + reviewsFormatted = + String.format("<div class='box errormessage'>%s</div>%s", unsafeReviews, reviewsFormatted); + } + return reviewsFormatted; + } + + private static boolean isNotSafe(ReviewsMap reviewsMap, String vulnerabilityId) + { + boolean isNotSafe; + Map<String, List<Review>> map = reviewsMap.getReviewsMap(); + if (map.containsKey(vulnerabilityId)) { + isNotSafe = map.get(vulnerabilityId) + .stream() + .anyMatch(review -> review.getResult() == UNSAFE); + } else { + isNotSafe = true; + } + return isNotSafe; + } + + private boolean isFromEnvironment(Extension extension) + { + ExtensionFile file = extension.getFile(); + boolean isFromEnvironment; + if (file instanceof CoreExtensionFile) { + Path currentPathAbsolute = Path.of(".").toAbsolutePath(); + CoreExtensionFile coreExtensionFile = (CoreExtensionFile) file; + if (coreExtensionFile.getURL() == null) { + return false; + } + String withoutInternalReference = StringUtils.substringBefore(coreExtensionFile.getURL().toString(), "!") + .replaceFirst("^jar:", ""); + try { + Path jarPathAbsolute = FileUtils.toFile(new URL(withoutInternalReference)).toPath(); + isFromEnvironment = !currentPathAbsolute.relativize(jarPathAbsolute).startsWith(Path.of("webapps")); + } catch (MalformedURLException e) { + this.logger.warn("Failed to resolve the URL for [{}]. Cause: [{}]", coreExtensionFile, + getRootCauseMessage(e)); + isFromEnvironment = false; + } + } else { + isFromEnvironment = false; + } + return isFromEnvironment; + } + + private static String formatReview(Review review) + { + return String.format("<dl>\n" + + " <dt>%s</dt>\n" + + " <dd>%s</dd>\n" + + "</dl>", + escapeXml(review.getEmitter()), + escapeXml(review.getExplanation())); + } } diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/java/org/xwiki/extension/security/internal/analyzer/osv/OsvResponseAnalyzer.java b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/java/org/xwiki/extension/security/internal/analyzer/osv/OsvResponseAnalyzer.java index e43a3c7f2cb..2a754f06297 100644 --- a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/java/org/xwiki/extension/security/internal/analyzer/osv/OsvResponseAnalyzer.java +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/java/org/xwiki/extension/security/internal/analyzer/osv/OsvResponseAnalyzer.java @@ -55,10 +55,6 @@ @Singleton public class OsvResponseAnalyzer { - /** - * Shared constant when the suggestion is to upgrade the extension from the Extension Manager. - */ - private static final String UPGRADE_FROM_EM_ADVICE = "extension.security.analysis.advice.upgradeFromEM"; @Inject private Logger logger; @@ -81,14 +77,10 @@ public ExtensionSecurityAnalysisResult analyzeOsvResponse(String extensionId, St vulnerability) .ifPresent(matchingVulns::add)); } - Version currentVersion = new DefaultVersion(version); - ExtensionSecurityAnalysisResult extensionSecurityAnalysisResult = new ExtensionSecurityAnalysisResult() - .setResults(matchingVulns.stream().map(vulnObject -> convert(vulnObject, currentVersion)) + + return new ExtensionSecurityAnalysisResult() + .setResults(matchingVulns.stream().map(vulnObject -> convert(vulnObject, new DefaultVersion(version))) .collect(Collectors.toList())); - if (!extensionSecurityAnalysisResult.getSecurityVulnerabilities().isEmpty()) { - extensionSecurityAnalysisResult.setAdvice(UPGRADE_FROM_EM_ADVICE); - } - return extensionSecurityAnalysisResult; } private SecurityVulnerabilityDescriptor convert(VulnObject vulnObject, Version currentVersion) diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/java/org/xwiki/extension/security/internal/analyzer/osv/model/response/VulnObject.java b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/java/org/xwiki/extension/security/internal/analyzer/osv/model/response/VulnObject.java index 306a00fcad1..e0a12d8d2fe 100644 --- a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/java/org/xwiki/extension/security/internal/analyzer/osv/model/response/VulnObject.java +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/java/org/xwiki/extension/security/internal/analyzer/osv/model/response/VulnObject.java @@ -128,6 +128,9 @@ public String getMainURL() */ public String getSeverityCCSV3() { + if (this.severity == null) { + return ""; + } return this.severity.stream() .filter(s -> Objects.equals("CVSS_V3", s.getType())) .map(SeverityObject::getScore) diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/resources/ApplicationResources.properties b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/resources/ApplicationResources.properties index f739a57b251..f182b5a4b9b 100644 --- a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/resources/ApplicationResources.properties +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/main/resources/ApplicationResources.properties @@ -18,4 +18,6 @@ # 02110-1301 USA, or see the FSF site: http://www.fsf.org. # --------------------------------------------------------------------------- -extension.security.analysis.advice.upgradeFromEM=This extension can be upgraded from the extension manager. \ No newline at end of file +extension.security.analysis.advice.upgradeFromEM=This extension can be upgraded from the extension manager. +extension.security.analysis.advice.upgradeXWiki=Please upgrade XWiki. +extension.security.analysis.advice.upgradeEnvironment=Please upgrade your servlet engine. \ No newline at end of file diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/test/java/org/xwiki/extension/security/internal/ExtensionSecuritySchedulerTest.java b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/test/java/org/xwiki/extension/security/internal/ExtensionSecuritySchedulerTest.java new file mode 100644 index 00000000000..275df2a0bc6 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/test/java/org/xwiki/extension/security/internal/ExtensionSecuritySchedulerTest.java @@ -0,0 +1,104 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.extension.security.internal; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mock; +import org.xwiki.extension.security.ExtensionSecurityConfiguration; +import org.xwiki.job.Job; +import org.xwiki.job.JobExecutor; +import org.xwiki.test.LogLevel; +import org.xwiki.test.junit5.LogCaptureExtension; +import org.xwiki.test.junit5.mockito.ComponentTest; +import org.xwiki.test.junit5.mockito.InjectMockComponents; +import org.xwiki.test.junit5.mockito.MockComponent; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Test of {@link ExtensionSecurityScheduler}. + * + * @version $Id$ + */ +@ComponentTest +class ExtensionSecuritySchedulerTest +{ + @InjectMockComponents + private ExtensionSecurityScheduler scheduler; + + @MockComponent + private JobExecutor jobExecutor; + + @MockComponent + private ExtensionSecurityConfiguration extensionSecurityConfiguration; + + @RegisterExtension + private LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.INFO); + + @Mock + private Job mock; + + @BeforeEach + void setUp() throws Exception + { + when(this.extensionSecurityConfiguration.getScanDelay()).thenReturn(1); + when(this.jobExecutor.execute(anyString(), any())).thenReturn(this.mock); + } + + @Test + void start() + { + this.scheduler.start(); + verify(this.extensionSecurityConfiguration, timeout(1000)).isSecurityScanEnabled(); + this.scheduler.start(); + verify(this.extensionSecurityConfiguration).isSecurityScanEnabled(); + assertEquals("Extension security scan disabled.", this.logCapture.getMessage(0)); + } + + @Test + void startEnabled() throws Exception + { + when(this.extensionSecurityConfiguration.isSecurityScanEnabled()).thenReturn(true); + this.scheduler.start(); + verify(this.extensionSecurityConfiguration, timeout(1000)).isSecurityScanEnabled(); + verify(this.jobExecutor, timeout(1000)).execute(ExtensionSecurityJob.JOBTYPE, new ExtensionSecurityRequest()); + this.scheduler.start(); + verify(this.extensionSecurityConfiguration).isSecurityScanEnabled(); + verify(this.jobExecutor).execute(ExtensionSecurityJob.JOBTYPE, new ExtensionSecurityRequest()); + } + + @Test + void restart() + { + this.scheduler.start(); + verify(this.extensionSecurityConfiguration, timeout(1000)).isSecurityScanEnabled(); + this.scheduler.restart(); + verify(this.extensionSecurityConfiguration, timeout(1000).times(2)).isSecurityScanEnabled(); + assertEquals("Extension security scan disabled.", this.logCapture.getMessage(0)); + assertEquals("Extension security scan disabled.", this.logCapture.getMessage(1)); + } +} diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/test/java/org/xwiki/extension/security/internal/analyzer/VulnerabilityIndexerTest.java b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/test/java/org/xwiki/extension/security/internal/analyzer/VulnerabilityIndexerTest.java new file mode 100644 index 00000000000..b7156633ac6 --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/test/java/org/xwiki/extension/security/internal/analyzer/VulnerabilityIndexerTest.java @@ -0,0 +1,132 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.extension.security.internal.analyzer; + +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.xwiki.extension.ExtensionId; +import org.xwiki.extension.index.internal.ExtensionIndexStore; +import org.xwiki.extension.index.security.review.ReviewsMap; +import org.xwiki.extension.index.security.ExtensionSecurityAnalysisResult; +import org.xwiki.extension.index.security.SecurityVulnerabilityDescriptor; +import org.xwiki.extension.repository.internal.core.DefaultCoreExtension; +import org.xwiki.extension.repository.internal.core.DefaultCoreExtensionRepository; +import org.xwiki.extension.version.internal.DefaultVersion; +import org.xwiki.test.LogLevel; +import org.xwiki.test.junit5.LogCaptureExtension; +import org.xwiki.test.junit5.mockito.ComponentTest; +import org.xwiki.test.junit5.mockito.InjectMockComponents; +import org.xwiki.test.junit5.mockito.MockComponent; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Test of {@link VulnerabilityIndexer}. + * + * @version $Id$ + */ +@ComponentTest +class VulnerabilityIndexerTest +{ + @InjectMockComponents + private VulnerabilityIndexer indexer; + + @MockComponent + private ExtensionIndexStore extensionIndexStore; + + @RegisterExtension + private LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN); + + @Test + void update() throws Exception + { + ExtensionId extensionId = new ExtensionId("org.xwiki:ext", "12.10"); + DefaultCoreExtension extension = + new DefaultCoreExtension(mock(DefaultCoreExtensionRepository.class), new URL("file:///path/ext.jar"), + extensionId, "exttype"); + ExtensionSecurityAnalysisResult inputAnalysis = new ExtensionSecurityAnalysisResult() + .setResults(List.of()); + boolean update = this.indexer.update(extension, inputAnalysis, new ReviewsMap()); + + assertFalse(update); + + verify(this.extensionIndexStore).update(extensionId, new ExtensionSecurityAnalysisResult() + .setFromEnvironment(true) + .setResults(List.of())); + } + + @Test + void updateWithVulnerabilities() throws Exception + { + ExtensionId extensionId = new ExtensionId("org.xwiki:ext", "12.10"); + DefaultCoreExtension extension = + new DefaultCoreExtension(mock(DefaultCoreExtensionRepository.class), new URL("file:///path/ext.jar"), + extensionId, "exttype"); + SecurityVulnerabilityDescriptor securityVulnerabilityDescriptor = new SecurityVulnerabilityDescriptor() + .setScore(1.3) + .setId("org.xwiki.ext/12.10") + .setURL("https://ext.dev/path") + .setFixVersion(new DefaultVersion("15.2")); + ExtensionSecurityAnalysisResult inputAnalysis = new ExtensionSecurityAnalysisResult() + .setResults(List.of(securityVulnerabilityDescriptor)); + + boolean update = this.indexer.update(extension, inputAnalysis, new ReviewsMap()); + + assertTrue(update); + + verify(this.extensionIndexStore).update(extensionId, new ExtensionSecurityAnalysisResult() + .setFromEnvironment(true) + .setAdvice("extension.security.analysis.advice.upgradeEnvironment") + .setResults(List.of(securityVulnerabilityDescriptor))); + } + + @Test + void updateIsNotServlet() throws Exception + { + ExtensionId extensionId = new ExtensionId("org.xwiki:ext", "12.10"); + + DefaultCoreExtension extension = + new DefaultCoreExtension(mock(DefaultCoreExtensionRepository.class), getWebjarExtensionURL(), + extensionId, "exttype"); + ExtensionSecurityAnalysisResult inputAnalysis = new ExtensionSecurityAnalysisResult() + .setResults(List.of()); + boolean update = this.indexer.update(extension, inputAnalysis, new ReviewsMap()); + + assertFalse(update); + + verify(this.extensionIndexStore).update(extensionId, new ExtensionSecurityAnalysisResult() + .setFromEnvironment(false) + .setResults(List.of())); + } + + private static URL getWebjarExtensionURL() throws MalformedURLException + { + Path root = Path.of(".").toAbsolutePath(); + return root.resolve(Path.of("webapps", "ext.jar")).toUri().toURL(); + } +} diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/test/java/org/xwiki/extension/security/internal/analyzer/osv/OsvResponseAnalyzerTest.java b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/test/java/org/xwiki/extension/security/internal/analyzer/osv/OsvResponseAnalyzerTest.java index 97ac857cb2c..de6a8892f6c 100644 --- a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/test/java/org/xwiki/extension/security/internal/analyzer/osv/OsvResponseAnalyzerTest.java +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-index/src/test/java/org/xwiki/extension/security/internal/analyzer/osv/OsvResponseAnalyzerTest.java @@ -86,7 +86,6 @@ void analyzeOsvResponseOrgXWikiPlatform() securityVulnerabilityDescriptor1, securityVulnerabilityDescriptor2 )); - expected.setAdvice("extension.security.analysis.advice.upgradeFromEM"); assertEquals(expected, this.analyzer.analyzeOsvResponse("org.xwiki.platform:xwiki-platform-administration-ui", "13.10", @@ -105,7 +104,6 @@ void analyzeOsvResponse() securityVulnerabilityDescriptor.setScore(7.5); securityVulnerabilityDescriptor.setFixVersion(new DefaultVersion("15.7")); expected.setResults(List.of(securityVulnerabilityDescriptor)); - expected.setAdvice("extension.security.analysis.advice.upgradeFromEM"); assertEquals(expected, this.analyzer.analyzeOsvResponse("org.test:my-ext", "7.5", osvResponse)); diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-test/xwiki-platform-extension-security-test-docker/src/test/it/org/xwiki/extension/security/test/ui/ExtensionSecurityIT.java b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-test/xwiki-platform-extension-security-test-docker/src/test/it/org/xwiki/extension/security/test/ui/ExtensionSecurityIT.java index 411c2bd3d49..3b86542c696 100644 --- a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-test/xwiki-platform-extension-security-test-docker/src/test/it/org/xwiki/extension/security/test/ui/ExtensionSecurityIT.java +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-test/xwiki-platform-extension-security-test-docker/src/test/it/org/xwiki/extension/security/test/ui/ExtensionSecurityIT.java @@ -75,9 +75,9 @@ void extensionVulnerabilitiesAdmin(TestUtils setup, TestReference testReference) + "org.xwiki.platform:xwiki-platform-administration-ui/"))); tableLayout.assertRow("Wikis", "xwiki"); tableLayout.assertRow("Max CVSS", "9.9"); - tableLayout.assertRow("CVE IDs", "GHSA-4v38-964c-xjmw (9.9)\n" - + "GHSA-9j36-3cp4-rh4j (9.9)\n" - + "GHSA-mgjw-2wrp-r535 (8.8)"); + tableLayout.assertRow("CVE IDs", "GHSA-4v38-964c-xjmw (9.9) \n" + + "GHSA-9j36-3cp4-rh4j (9.9) \n" + + "GHSA-mgjw-2wrp-r535 (8.8) "); tableLayout.assertRow("Latest Fix Version", "140.10.2"); } diff --git a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-ui/src/main/resources/XWiki/Extension/Security/Code/Admin.xml b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-ui/src/main/resources/XWiki/Extension/Security/Code/Admin.xml index 773bf731fda..d93182bf2eb 100644 --- a/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-ui/src/main/resources/XWiki/Extension/Security/Code/Admin.xml +++ b/xwiki-platform-core/xwiki-platform-extension/xwiki-platform-extension-security/xwiki-platform-extension-security-ui/src/main/resources/XWiki/Extension/Security/Code/Admin.xml @@ -41,18 +41,48 @@ {{velocity}} #if($services.security.authorization.hasAccess('admin') && $services.extension.index.security.isSecurityScanEnabled()) #set($discard = $xwiki.ssx.use("XWiki.Extension.Security.Code.Admin")) -{{warning}} -{{translation key="extension.security.admin.generalWarning"/}} -{{/warning}} + +{{html clean='false' wiki='true'}} +<ul class="nav nav-tabs" role="tablist"> + <li role="presentation" class="active"><a href="#extension-vulnerabilities" aria-controls="extension-vulnerabilities" role="tab" data-toggle="tab">Extension Vulnerabilities</a></li> + <li role="presentation"><a href="#environment-vulnerabilities" aria-controls="environment-vulnerabilities" + role="tab" data-toggle="tab">Environment Vulnerabilities</a></li> +</ul> + + +<div class="tab-content"> + <div role="tabpanel" class="tab-pane active" id="extension-vulnerabilities"> {{liveData id="extension-vulnerabilities-list" source="extensionSecurity" + sourceParameters="isFromEnvironment=false" properties="name,wikis,maxCVSS,cveID,fixVersion,advice" showPageSizeDropdown="false"}} {{/liveData}} -{{html}} +</div> + +<div role="tabpanel" class="tab-pane" id="environment-vulnerabilities"> + +(((&nbsp;))) +{{warning}} +Extensions listed in this section have known vulnerabilities, but are outside of the scope of XWiki. +This means that the extensions are provided by the environment running XWiki (e.g., its servlet engine), and can't be + fixed by upgrading XWiki or one of its extension. +{{/warning}} + +{{liveData + id="environment-vulnerabilities-list" + source="extensionSecurity" + sourceParameters="isFromEnvironment=true" + properties="name,wikis,maxCVSS,cveID,fixVersion" + showPageSizeDropdown="false"}} +{{/liveData}} + + </div> +</div> + #set ($indexJobStatus = $services.job.getJobStatus(['extension_security'])) #if ($indexJobStatus) <div class="box infomessage"> @@ -73,7 +103,8 @@ #else <div class="box warningmessage">$escapetool.xml($services.localization.render('extension.security.indexed.nojob'))</div> #end -{{/html}} + </div> + {{/html}} #end {{/velocity}} @@ -443,7 +474,8 @@ <cache>long</cache> </property> <property> - <code>#extension-vulnerabilities-list { + <code>#extension-vulnerabilities-list, +#environment-vulnerabilities-list { td, th { inline-size: min-content; @@ -464,6 +496,11 @@ word-wrap: break-word; white-space: break-spaces; } + + .security-vulnerability-detail-modal { + white-space: normal; + word-wrap: normal; + } } </code> </property> -- GitLab