diff --git a/core/src/main/java/hudson/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java index 01bf9c6ddb7976c41092b9e43ad75ddfa205db31..3a234941a423a3bb4d149daadc68882f9af4d965 100644 --- a/core/src/main/java/hudson/model/UpdateSite.java +++ b/core/src/main/java/hudson/model/UpdateSite.java @@ -26,6 +26,7 @@ package hudson.model; import hudson.ClassicPluginStrategy; +import hudson.ExtensionList; import hudson.PluginManager; import hudson.PluginWrapper; import hudson.Util; @@ -46,7 +47,9 @@ import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeMap; @@ -55,17 +58,24 @@ import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; +import javax.annotation.Nullable; + import jenkins.model.Jenkins; import jenkins.model.DownloadSettings; +import jenkins.security.UpdateSiteWarningsConfiguration; import jenkins.util.JSONSignatureValidator; import jenkins.util.SystemProperties; +import net.sf.json.JSONArray; import net.sf.json.JSONException; import net.sf.json.JSONObject; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.HttpResponse; @@ -513,6 +523,12 @@ public class UpdateSite { * Plugins in the repository, keyed by their artifact IDs. */ public final Map plugins = new TreeMap(String.CASE_INSENSITIVE_ORDER); + /** + * List of warnings (mostly security) published with the update site. + * + * @since TODO + */ + private final Set warnings = new HashSet(); /** * If this is non-null, Jenkins is going to check the connectivity to this URL to make sure @@ -528,6 +544,18 @@ public class UpdateSite { } else { core = null; } + + JSONArray w = o.optJSONArray("warnings"); + if (w != null) { + for (int i = 0; i < w.size(); i++) { + try { + warnings.add(new Warning(w.getJSONObject(i))); + } catch (JSONException ex) { + LOGGER.log(Level.WARNING, "Failed to parse JSON for warning", ex); + } + } + } + for(Map.Entry e : (Set>)o.getJSONObject("plugins").entrySet()) { Plugin p = new Plugin(sourceId, e.getValue()); // JENKINS-33308 - include implied dependencies for older plugins that may need them @@ -545,6 +573,16 @@ public class UpdateSite { connectionCheckUrl = (String)o.get("connectionCheckUrl"); } + /** + * Returns the set of warnings + * @return the set of warnings + * @since TODO + */ + @Restricted(NoExternalUse.class) + public Set getWarnings() { + return this.warnings; + } + /** * Is there a new version of the core? */ @@ -646,6 +684,232 @@ public class UpdateSite { } + /** + * A version range for {@code Warning}s indicates which versions of a given plugin are affected + * by it. + * + * {@link #name}, {@link #firstVersion} and {@link #lastVersion} fields are only used for administrator notices. + * + * The {@link #pattern} is used to determine whether a given warning applies to the current installation. + * + * @since TODO + */ + @Restricted(NoExternalUse.class) + public static final class WarningVersionRange { + /** + * Human-readable English name for this version range, e.g. 'regular', 'LTS', '2.6 line'. + */ + @Nullable + public final String name; + + /** + * First version in this version range to be subject to the warning. + */ + @Nullable + public final String firstVersion; + + /** + * Last version in this version range to be subject to the warning. + */ + @Nullable + public final String lastVersion; + + /** + * Regular expression pattern for this version range that matches all included version numbers. + */ + @Nonnull + private final Pattern pattern; + + public WarningVersionRange(JSONObject o) { + this.name = Util.fixEmpty(o.optString("name")); + this.firstVersion = Util.fixEmpty(o.optString("firstVersion")); + this.lastVersion = Util.fixEmpty(o.optString("lastVersion")); + Pattern p; + try { + p = Pattern.compile(o.getString("pattern")); + } catch (PatternSyntaxException ex) { + LOGGER.log(Level.WARNING, "Failed to compile pattern '" + o.getString("pattern") + "', using '.*' instead", ex); + p = Pattern.compile(".*"); + } + this.pattern = p; + } + + public boolean includes(VersionNumber number) { + return pattern.matcher(number.toString()).matches(); + } + } + + /** + * Represents a warning about a certain component, mostly related to known security issues. + * + * @see UpdateSiteWarningsConfiguration + * @see jenkins.security.UpdateSiteWarningsMonitor + * + * @since TODO + */ + @Restricted(NoExternalUse.class) + public static final class Warning { + + public enum Type { + CORE, + PLUGIN, + UNKNOWN + } + + /** + * The type classifier for this warning. + */ + @Nonnull + public /* final */ Type type; + + /** + * The globally unique ID of this warning. + * + *

This is typically the CVE identifier or SECURITY issue (Jenkins project); + * possibly with a unique suffix (e.g. artifactId) if either applies to multiple components.

+ */ + @Exported + @Nonnull + public final String id; + + /** + * The name of the affected component. + *
    + *
  • If type is 'core', this is 'core' by convention. + *
  • If type is 'plugin', this is the artifactId of the affected plugin + *
+ */ + @Exported + @Nonnull + public final String component; + + /** + * A short, English language explanation for this warning. + */ + @Exported + @Nonnull + public final String message; + + /** + * A URL with more information about this, typically a security advisory. For use in administrator notices + * only, so + */ + @Exported + @Nonnull + public final String url; + + /** + * A list of named version ranges specifying which versions of the named component this warning applies to. + * + * If this list is empty, all versions of the component are considered to be affected by this warning. + */ + @Exported + @Nonnull + public final List versionRanges; + + /** + * + * @param o the {@link JSONObject} representing the warning + * @throws JSONException if the argument does not match the expected format + */ + @Restricted(NoExternalUse.class) + public Warning(JSONObject o) { + try { + this.type = Type.valueOf(o.getString("type").toUpperCase(Locale.US)); + } catch (IllegalArgumentException ex) { + this.type = Type.UNKNOWN; + } + this.id = o.getString("id"); + this.component = o.getString("name"); + this.message = o.getString("message"); + this.url = o.getString("url"); + + if (o.has("versions")) { + List ranges = new ArrayList<>(); + JSONArray versions = o.getJSONArray("versions"); + for (int i = 0; i < versions.size(); i++) { + WarningVersionRange range = new WarningVersionRange(versions.getJSONObject(i)); + ranges.add(range); + } + this.versionRanges = Collections.unmodifiableList(ranges); + } else { + this.versionRanges = Collections.emptyList(); + } + } + + /** + * Two objects are considered equal if they are the same type and have the same ID. + * + * @param o the other object + * @return true iff this object and the argument are considered equal + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Warning)) return false; + + Warning warning = (Warning) o; + + return id.equals(warning.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + public boolean isPluginWarning(@Nonnull String pluginName) { + return type == Type.PLUGIN && pluginName.equals(this.component); + } + + /** + * Returns true if this warning is relevant to the current configuration + * @return true if this warning is relevant to the current configuration + */ + public boolean isRelevant() { + switch (this.type) { + case CORE: + VersionNumber current = Jenkins.getVersion(); + + if (!isRelevantToVersion(current)) { + return false; + } + return true; + case PLUGIN: + + // check whether plugin is installed + PluginWrapper plugin = Jenkins.getInstance().getPluginManager().getPlugin(this.component); + if (plugin == null) { + return false; + } + + // check whether warning is relevant to installed version + VersionNumber currentCore = plugin.getVersionNumber(); + if (!isRelevantToVersion(currentCore)) { + return false; + } + return true; + case UNKNOWN: + default: + return false; + } + } + + public boolean isRelevantToVersion(@Nonnull VersionNumber version) { + if (this.versionRanges.isEmpty()) { + // no version ranges specified, so all versions are affected + return true; + } + + for (UpdateSite.WarningVersionRange range : this.versionRanges) { + if (range.includes(version)) { + return true; + } + } + return false; + } + } + public final class Plugin extends Entry { /** * Optional URL to the Wiki page that discusses this plugin. @@ -863,6 +1127,49 @@ public class UpdateSite { return true; } + /** + * @since TODO + */ + @CheckForNull + @Restricted(NoExternalUse.class) + public Set getWarnings() { + ExtensionList list = ExtensionList.lookup(UpdateSiteWarningsConfiguration.class); + if (list.size() == 0) { + return Collections.emptySet(); + } + + Set warnings = new HashSet<>(); + + UpdateSiteWarningsConfiguration configuration = list.get(0); + + for (Warning warning: configuration.getAllWarnings()) { + if (configuration.isIgnored(warning)) { + // warning is currently being ignored + continue; + } + if (!warning.isPluginWarning(this.name)) { + // warning is not about this plugin + continue; + } + + if (!warning.isRelevantToVersion(new VersionNumber(this.version))) { + // warning is not relevant to this version + continue; + } + warnings.add(warning); + } + + return warnings; + } + + /** + * @since TODO + */ + @Restricted(DoNotUse.class) + public boolean hasWarnings() { + return getWarnings().size() > 0; + } + /** * @deprecated as of 1.326 * Use {@link #deploy()}. diff --git a/core/src/main/java/jenkins/security/UpdateSiteWarningsConfiguration.java b/core/src/main/java/jenkins/security/UpdateSiteWarningsConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..08bce4881bf79169ba05d4db4f724cfab1ab9246 --- /dev/null +++ b/core/src/main/java/jenkins/security/UpdateSiteWarningsConfiguration.java @@ -0,0 +1,124 @@ +/* + * The MIT License + * + * Copyright (c) 2016, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.security; + +import hudson.Extension; +import hudson.PluginWrapper; +import hudson.model.UpdateSite; +import jenkins.model.GlobalConfiguration; +import jenkins.model.GlobalConfigurationCategory; +import jenkins.model.Jenkins; +import net.sf.json.JSONObject; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.StaplerRequest; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Configuration for update site-provided warnings. + * + * @see UpdateSiteWarningsMonitor + * + * @since TODO + */ +@Extension +@Restricted(NoExternalUse.class) +public class UpdateSiteWarningsConfiguration extends GlobalConfiguration { + + private HashSet ignoredWarnings = new HashSet<>(); + + @Override + public GlobalConfigurationCategory getCategory() { + return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class); + } + + public UpdateSiteWarningsConfiguration() { + load(); + } + + @Nonnull + public Set getIgnoredWarnings() { + return Collections.unmodifiableSet(ignoredWarnings); + } + + public boolean isIgnored(@Nonnull UpdateSite.Warning warning) { + return ignoredWarnings.contains(warning.id); + } + + @CheckForNull + public PluginWrapper getPlugin(@Nonnull UpdateSite.Warning warning) { + if (warning.type != UpdateSite.Warning.Type.PLUGIN) { + return null; + } + return Jenkins.getInstance().getPluginManager().getPlugin(warning.component); + } + + @Nonnull + public Set getAllWarnings() { + HashSet allWarnings = new HashSet<>(); + + for (UpdateSite site : Jenkins.getInstance().getUpdateCenter().getSites()) { + UpdateSite.Data data = site.getData(); + if (data != null) { + allWarnings.addAll(data.getWarnings()); + } + } + return allWarnings; + } + + @Nonnull + public Set getApplicableWarnings() { + Set allWarnings = getAllWarnings(); + + HashSet applicableWarnings = new HashSet<>(); + for (UpdateSite.Warning warning: allWarnings) { + if (warning.isRelevant()) { + applicableWarnings.add(warning); + } + } + + return Collections.unmodifiableSet(applicableWarnings); + } + + + @Override + public boolean configure(StaplerRequest req, JSONObject json) throws FormException { + HashSet newIgnoredWarnings = new HashSet<>(); + for (Object key : json.keySet()) { + String warningKey = key.toString(); + if (!json.getBoolean(warningKey)) { + newIgnoredWarnings.add(warningKey); + } + } + this.ignoredWarnings = newIgnoredWarnings; + this.save(); + return true; + } +} diff --git a/core/src/main/java/jenkins/security/UpdateSiteWarningsMonitor.java b/core/src/main/java/jenkins/security/UpdateSiteWarningsMonitor.java new file mode 100644 index 0000000000000000000000000000000000000000..378717448dec4ea6929746ade5722d84f24584c7 --- /dev/null +++ b/core/src/main/java/jenkins/security/UpdateSiteWarningsMonitor.java @@ -0,0 +1,177 @@ +/* + * The MIT License + * + * Copyright (c) 2016, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.security; + +import hudson.Extension; +import hudson.ExtensionList; +import hudson.PluginWrapper; +import hudson.model.AdministrativeMonitor; +import hudson.model.UpdateSite; +import hudson.util.HttpResponses; +import jenkins.model.Jenkins; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.interceptor.RequirePOST; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +/** + * Administrative monitor showing plugin/core warnings published by the configured update site to the user. + * + *

Terminology overview:

+ * + *
    + *
  • Applicable warnings are those relevant to currently installed components + *
  • Active warnings are those actually shown to users. + *
  • Hidden warnings are those _not_ shown to users due to them being configured to be hidden. + *
  • Inapplicable warnings are those that are not applicable. + *
+ * + *

The following sets may be non-empty:

+ * + *
    + *
  • Intersection of applicable and active + *
  • Intersection of applicable and hidden + *
  • Intersection of hidden and inapplicable (although not really relevant) + *
  • Intersection of inapplicable and neither hidden nor active + *
+ * + *

The following sets must necessarily be empty:

+ * + *
    + *
  • Intersection of applicable and inapplicable + *
  • Intersection of active and hidden + *
  • Intersection of active and inapplicable + *
+ * + * @since TODO + */ +@Extension +@Restricted(NoExternalUse.class) +public class UpdateSiteWarningsMonitor extends AdministrativeMonitor { + @Override + public boolean isActivated() { + return !getActiveCoreWarnings().isEmpty() || !getActivePluginWarningsByPlugin().isEmpty(); + } + + public List getActiveCoreWarnings() { + List CoreWarnings = new ArrayList<>(); + + for (UpdateSite.Warning warning : getActiveWarnings()) { + if (warning.type != UpdateSite.Warning.Type.CORE) { + // this is not a core warning + continue; + } + CoreWarnings.add(warning); + } + return CoreWarnings; + } + + public Map> getActivePluginWarningsByPlugin() { + Map> activePluginWarningsByPlugin = new HashMap<>(); + + for (UpdateSite.Warning warning : getActiveWarnings()) { + if (warning.type != UpdateSite.Warning.Type.PLUGIN) { + // this is not a plugin warning + continue; + } + + String pluginName = warning.component; + + PluginWrapper plugin = Jenkins.getInstance().getPluginManager().getPlugin(pluginName); + + if (!activePluginWarningsByPlugin.containsKey(plugin)) { + activePluginWarningsByPlugin.put(plugin, new ArrayList()); + } + activePluginWarningsByPlugin.get(plugin).add(warning); + } + return activePluginWarningsByPlugin; + + } + + private Set getActiveWarnings() { + ExtensionList configurations = ExtensionList.lookup(UpdateSiteWarningsConfiguration.class); + if (configurations.isEmpty()) { + return Collections.emptySet(); + } + UpdateSiteWarningsConfiguration configuration = configurations.get(0); + + HashSet activeWarnings = new HashSet<>(); + + for (UpdateSite.Warning warning : configuration.getApplicableWarnings()) { + if (!configuration.getIgnoredWarnings().contains(warning.id)) { + activeWarnings.add(warning); + } + } + + return Collections.unmodifiableSet(activeWarnings); + } + + /** + * Redirects the user to the plugin manager or security configuration + */ + @RequirePOST + public HttpResponse doForward(@QueryParameter String fix, @QueryParameter String configure) { + if (fix != null) { + return HttpResponses.redirectViaContextPath("pluginManager"); + } + if (configure != null) { + return HttpResponses.redirectViaContextPath("configureSecurity"); + } + + // shouldn't happen + return HttpResponses.redirectViaContextPath("/"); + } + + /** + * Returns true iff there are applicable but ignored (i.e. hidden) warnings. + * + * @return true iff there are applicable but ignored (i.e. hidden) warnings. + */ + public boolean hasApplicableHiddenWarnings() { + ExtensionList configurations = ExtensionList.lookup(UpdateSiteWarningsConfiguration.class); + if (configurations.isEmpty()) { + return false; + } + + UpdateSiteWarningsConfiguration configuration = configurations.get(0); + + return getActiveWarnings().size() < configuration.getApplicableWarnings().size(); + } + + @Override + public String getDisplayName() { + return Messages.UpdateSiteWarningsMonitor_DisplayName(); + } +} diff --git a/core/src/main/resources/hudson/PluginManager/table.jelly b/core/src/main/resources/hudson/PluginManager/table.jelly index 7c289a7c82775f297151270b776dc19c907bc530..7511c82584d95ee677399e5c67aafd3b67ba9162 100644 --- a/core/src/main/resources/hudson/PluginManager/table.jelly +++ b/core/src/main/resources/hudson/PluginManager/table.jelly @@ -110,6 +110,15 @@ THE SOFTWARE.
${%depCoreWarning(p.getNeededDependenciesRequiredCore().toString())}
+ +
${%securityWarning} + +
+
diff --git a/core/src/main/resources/hudson/PluginManager/table.properties b/core/src/main/resources/hudson/PluginManager/table.properties index e7cd25852c8301c07874ae00edfff1db0fb23c67..63c161534cefcfc384899a32d6c4174d3094c1d3 100644 --- a/core/src/main/resources/hudson/PluginManager/table.properties +++ b/core/src/main/resources/hudson/PluginManager/table.properties @@ -34,3 +34,6 @@ depCoreWarning=\ Warning: This plugin requires dependent plugins that require Jenkins {0} or newer. \ Jenkins will refuse to load the dependent plugins requiring a newer version of Jenkins, \ and in turn loading this plugin will fail. +securityWarning=\ + Warning: This plugin version may not be safe to use. Please review the following security notices: + diff --git a/core/src/main/resources/jenkins/security/Messages.properties b/core/src/main/resources/jenkins/security/Messages.properties index e09dca3e79758277e4fc1be3e1a943366d9053c2..f0a98fb4a2978195bde2b4aad095bb2d06f36cc8 100644 --- a/core/src/main/resources/jenkins/security/Messages.properties +++ b/core/src/main/resources/jenkins/security/Messages.properties @@ -24,4 +24,5 @@ ApiTokenProperty.DisplayName=API Token ApiTokenProperty.ChangeToken.TokenIsHidden=Token is hidden ApiTokenProperty.ChangeToken.Success=
Updated. See the new token in the field above
ApiTokenProperty.ChangeToken.SuccessHidden=
Updated. You need to login as the user to see the token
-RekeySecretAdminMonitor.DisplayName=Re-keying \ No newline at end of file +RekeySecretAdminMonitor.DisplayName=Re-keying +UpdateSiteWarningsMonitor.DisplayName=Update Site Warnings diff --git a/core/src/main/resources/jenkins/security/UpdateSiteWarningsConfiguration/config.groovy b/core/src/main/resources/jenkins/security/UpdateSiteWarningsConfiguration/config.groovy new file mode 100644 index 0000000000000000000000000000000000000000..ac82bf1891838da370792124a6c966aa0969c3fb --- /dev/null +++ b/core/src/main/resources/jenkins/security/UpdateSiteWarningsConfiguration/config.groovy @@ -0,0 +1,70 @@ +/* + * The MIT License + * + * Copyright (c) 2016, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.security.UpdateSiteWarningsConfiguration + +f = namespace(lib.FormTagLib) +st = namespace("jelly:stapler") + +st.adjunct(includes: "jenkins.security.UpdateSiteWarningsConfiguration.style") + +def printEntry(warning, title, checked) { + f.block { + f.checkbox(name: warning.id, + title: title, + checked: checked, + class: 'hideWarnings'); + div(class: "setting-description") { + a(warning.url, href: warning.url) + } + } +} + +f.section(title:_("Hidden security warnings")) { + + f.advanced(title: _("Security warnings"), align:"left") { + f.block { + text(_("blurb")) + } + f.entry(title: _("Security warnings"), + help: '/descriptorByName/UpdateSiteWarningsConfiguration/help') { + table(width:"100%") { + + descriptor.applicableWarnings.each { warning -> + if (warning.type == hudson.model.UpdateSite.Warning.Type.CORE) { + printEntry(warning, + _("warning.core", warning.message), + !descriptor.isIgnored(warning)) + } + else if (warning.type == hudson.model.UpdateSite.Warning.Type.PLUGIN) { + def plugin = descriptor.getPlugin(warning) + printEntry(warning, + _("warning.plugin", plugin.displayName, warning.message), + !descriptor.isIgnored(warning)) + } + } + } + } + } +} diff --git a/core/src/main/resources/jenkins/security/UpdateSiteWarningsConfiguration/config.properties b/core/src/main/resources/jenkins/security/UpdateSiteWarningsConfiguration/config.properties new file mode 100644 index 0000000000000000000000000000000000000000..333445cd3f55d04fdad68413e2b9d5c64a64ee88 --- /dev/null +++ b/core/src/main/resources/jenkins/security/UpdateSiteWarningsConfiguration/config.properties @@ -0,0 +1,27 @@ +# The MIT License +# +# Copyright (c) 2016, CloudBees, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +warning.core = Jenkins core: {0} +warning.plugin = {0}: {1} + +blurb = This section allows you to disable warnings published on the update site. \ + Checked warnings are visible (the default), unchecked warnings are hidden. diff --git a/core/src/main/resources/jenkins/security/UpdateSiteWarningsConfiguration/help.html b/core/src/main/resources/jenkins/security/UpdateSiteWarningsConfiguration/help.html new file mode 100644 index 0000000000000000000000000000000000000000..1cb7b1df5d426ff875b5b05e071973536503885e --- /dev/null +++ b/core/src/main/resources/jenkins/security/UpdateSiteWarningsConfiguration/help.html @@ -0,0 +1,14 @@ +

+ This list contains all warnings relevant to currently installed components published by the configured update sites. + These are typically security-related. + Warnings that have been published but are not relevant to currently installed components (either because the affected component isn't installed, or an unaffected version is installed) are not shown here. +

+

+ Checked entries (the default) are active, i.e. they're shown to administrators in an administrative monitor. + Entries can be unchecked to hide them. + This can be useful if you've evaluated a specific warning and are confident it does not apply to your environment or configuration, and continued use of the specified component does not constitute a security problem. +

+

+ Please note that only specific warnings can be disabled; it is not possible to disable all warnings about a certain component. + If you wish to disable the display of warnings entirely, then you can disable the administrative monitor in Configure System. +

diff --git a/core/src/main/resources/jenkins/security/UpdateSiteWarningsConfiguration/style.css b/core/src/main/resources/jenkins/security/UpdateSiteWarningsConfiguration/style.css new file mode 100644 index 0000000000000000000000000000000000000000..ff1dd9bb5ec4b03dd528550df62ff71dd1f7f089 --- /dev/null +++ b/core/src/main/resources/jenkins/security/UpdateSiteWarningsConfiguration/style.css @@ -0,0 +1,4 @@ +.hideWarnings:not(:checked) + label { + color: grey; + text-decoration: line-through; +} diff --git a/core/src/main/resources/jenkins/security/UpdateSiteWarningsMonitor/message.groovy b/core/src/main/resources/jenkins/security/UpdateSiteWarningsMonitor/message.groovy new file mode 100644 index 0000000000000000000000000000000000000000..09e9bfe161a335af50f14cb51f31c2372cf0c2dd --- /dev/null +++ b/core/src/main/resources/jenkins/security/UpdateSiteWarningsMonitor/message.groovy @@ -0,0 +1,77 @@ +/* + * The MIT License + * + * Copyright (c) 2016, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.security.UpdateSiteWarningsMonitor + +def f = namespace(lib.FormTagLib) + +def listWarnings(warnings) { + warnings.each { warning -> + li { + a(warning.message, href: warning.url) + } + } +} + +def coreWarnings = my.activeCoreWarnings +def pluginWarnings = my.activePluginWarningsByPlugin + +div(class: "error") { + text(_("blurb")) + ul { + if (!coreWarnings.isEmpty()) { + li { + text(_("coreTitle", jenkins.model.Jenkins.version)) + ul { + listWarnings(coreWarnings) + } + } + } + + if (!pluginWarnings.isEmpty()) { + li { + pluginWarnings.each { plugin, warnings -> + a(_("pluginTitle", plugin.displayName, plugin.version), href: plugin.url) + + ul { + listWarnings(warnings) + } + } + } + } + } + + if (my.hasApplicableHiddenWarnings()) { + text(_("more")) + } +} + +form(method: "post", action: "${rootURL}/${it.url}/forward") { + div { + if (!pluginWarnings.isEmpty()) { + f.submit(name: 'fix', value: _("pluginManager.link")) + } + f.submit(name: 'configure', value: _("configureSecurity.link")) + } +} diff --git a/core/src/main/resources/jenkins/security/UpdateSiteWarningsMonitor/message.properties b/core/src/main/resources/jenkins/security/UpdateSiteWarningsMonitor/message.properties new file mode 100644 index 0000000000000000000000000000000000000000..35d33182f298088612131b234614789d69aa30bf --- /dev/null +++ b/core/src/main/resources/jenkins/security/UpdateSiteWarningsMonitor/message.properties @@ -0,0 +1,31 @@ +# The MIT License +# +# Copyright (c) 2016, CloudBees, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +pluginTitle = {0} {1}: +coreTitle = Jenkins {0} core and libraries: + +blurb = Warnings have been published for the following currently installed components: +more = Additional warnings are hidden due to the current security configuration. + + +pluginManager.link = Go to plugin manager +configureSecurity.link = Configure which of these warnings are shown diff --git a/test/src/test/java/hudson/model/UpdateSiteTest.java b/test/src/test/java/hudson/model/UpdateSiteTest.java index 8e22d43f769a20cfb238b655314dcbef76b4d7fd..8e49e20f2de295d2c3168b6500d6e08078ce8e18 100644 --- a/test/src/test/java/hudson/model/UpdateSiteTest.java +++ b/test/src/test/java/hudson/model/UpdateSiteTest.java @@ -33,6 +33,7 @@ import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import javax.servlet.ServletException; @@ -41,6 +42,8 @@ import javax.servlet.http.HttpServletResponse; import static org.junit.Assert.*; +import jenkins.security.UpdateSiteWarningsConfiguration; +import jenkins.security.UpdateSiteWarningsMonitor; import org.apache.commons.io.FileUtils; import org.eclipse.jetty.server.HttpConnection; import org.eclipse.jetty.server.Request; @@ -52,6 +55,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.recipes.LocalData; public class UpdateSiteTest { @@ -127,5 +131,24 @@ public class UpdateSiteTest { assertEquals(FormValidation.ok(), us.updateDirectly(/* TODO the certificate is now expired, and downloading a fresh copy did not seem to help */false).get()); assertNotNull(us.getPlugin("AdaptivePlugin")); } - + + @Test public void lackOfDataDoesNotFailWarningsCode() throws Exception { + assertNull("plugin data is not present", j.jenkins.getUpdateCenter().getSite("default").getData()); + + // nothing breaking? + j.jenkins.getExtensionList(UpdateSiteWarningsMonitor.class).get(0).getActivePluginWarningsByPlugin(); + j.jenkins.getExtensionList(UpdateSiteWarningsMonitor.class).get(0).getActiveCoreWarnings(); + j.jenkins.getExtensionList(UpdateSiteWarningsConfiguration.class).get(0).getAllWarnings(); + } + + @Test public void incompleteWarningsJson() throws Exception { + PersistedList sites = j.jenkins.getUpdateCenter().getSites(); + sites.clear(); + URL url = new URL(baseUrl, "/plugins/warnings-update-center-malformed.json"); + UpdateSite site = new UpdateSite(UpdateCenter.ID_DEFAULT, url.toString()); + sites.add(site); + assertEquals(FormValidation.ok(), site.updateDirectly(false).get()); + assertEquals("number of warnings", 7, site.getData().getWarnings().size()); + assertNotEquals("plugin data is present", Collections.emptyMap(), site.getData().plugins); + } } diff --git a/test/src/test/resources/plugins/warnings-update-center-malformed.json b/test/src/test/resources/plugins/warnings-update-center-malformed.json new file mode 100644 index 0000000000000000000000000000000000000000..ba99e7aa55d760d677947773404c0af0ed05cff6 --- /dev/null +++ b/test/src/test/resources/plugins/warnings-update-center-malformed.json @@ -0,0 +1,264 @@ +{ + "connectionCheckUrl": "http://www.google.com/", + "core": { + "buildDate": "Dec 18, 2016", + "name": "core", + "sha1": "x4rDgtNYm9l7zJ16RVuxMRgGoQE=", + "url": "http://updates.jenkins-ci.org/download/war/2.37/jenkins.war", + "version": "2.37" + }, + "id": "jenkins40494", + "plugins": { + "display-url-api": { + "buildDate": "Sep 22, 2016", + "dependencies": [ + { + "name": "junit", + "optional": false, + "version": "1.3" + } + ], + "developers": [ + { + "developerId": "jdumay", + "email": "jdumay@cloudbees.com", + "name": "James Dumay" + } + ], + "excerpt": "\\\\ Provides the DisplayURLProvider extension point to provide alternate URLs for use in notifications. URLs can be requested/extended for these UI locations: * Root page. * Job. * Run. * Run changes. * Test result. ", + "gav": "org.jenkins-ci.plugins:display-url-api:0.5", + "labels": [], + "name": "display-url-api", + "previousTimestamp": "2016-09-22T09:35:08.00Z", + "previousVersion": "0.4", + "releaseTimestamp": "2016-09-22T10:42:00.00Z", + "requiredCore": "1.625.3", + "scm": "github.com", + "sha1": "QEykyLSFuZTnFyHrzZEOgzSsYcU=", + "title": "Display URL API", + "url": "http://updates.jenkins-ci.org/download/plugins/display-url-api/0.5/display-url-api.hpi", + "version": "0.5", + "wiki": "https://wiki.jenkins-ci.org/display/JENKINS/Display+URL+API+Plugin" + }, + "junit": { + "buildDate": "Oct 17, 2016", + "dependencies": [ + { + "name": "structs", + "optional": false, + "version": "1.2" + } + ], + "developers": [ + { + "developerId": "ogondza" + } + ], + "excerpt": "Allows JUnit-format test results to be published.", + "gav": "org.jenkins-ci.plugins:junit:1.19", + "labels": [ + "report" + ], + "name": "junit", + "previousTimestamp": "2016-08-08T15:10:34.00Z", + "previousVersion": "1.18", + "releaseTimestamp": "2016-10-17T12:15:20.00Z", + "requiredCore": "1.580.1", + "scm": "github.com", + "sha1": "f3jcYlxz6/8PK43W3KL5LFtz7ro=", + "title": "JUnit Plugin", + "url": "http://updates.jenkins-ci.org/download/plugins/junit/1.19/junit.hpi", + "version": "1.19", + "wiki": "https://wiki.jenkins-ci.org/display/JENKINS/JUnit+Plugin" + }, + "mailer": { + "buildDate": "Sep 04, 2016", + "dependencies": [ + { + "name": "display-url-api", + "optional": false, + "version": "0.2" + } + ], + "developers": [ + { + "developerId": "andresrc" + } + ], + "excerpt": "This plugin allows you to configure email notifications for build results. This is a break-out of the original core based email component. ", + "gav": "org.jenkins-ci.plugins:mailer:1.18", + "labels": [], + "name": "mailer", + "previousTimestamp": "2016-04-20T08:46:22.00Z", + "previousVersion": "1.17", + "releaseTimestamp": "2016-09-04T09:14:16.00Z", + "requiredCore": "1.625.3", + "scm": "github.com", + "sha1": "poG1EauZFM5lZE5hCBx5mqr/mMA=", + "title": "Jenkins Mailer Plugin", + "url": "http://updates.jenkins-ci.org/download/plugins/mailer/1.18/mailer.hpi", + "version": "1.18", + "wiki": "https://wiki.jenkins-ci.org/display/JENKINS/Mailer" + }, + "extra-columns": { + "buildDate": "Apr 11, 2016", + "dependencies": [ + { + "name": "junit", + "optional": false, + "version": "1.11" + } + ], + "developers": [ + { + "developerId": "fredg", + "name": "Fred G." + } + ], + "excerpt": "This is a general listview-column plugin that currently contains the following columns: Test Result, Configure Project button, Disable/Enable Project button, Project Description, Build Description, SCM Type, Last/Current Build Console output, Job Type, Build Duration, Build Parameters.", + "gav": "org.jenkins-ci.plugins:extra-columns:1.17", + "labels": [ + "listview-column" + ], + "name": "extra-columns", + "previousTimestamp": "2015-12-11T01:18:48.00Z", + "previousVersion": "1.16", + "releaseTimestamp": "2016-04-11T22:36:22.00Z", + "requiredCore": "1.475", + "scm": "github.com", + "sha1": "8y9Y91n7/Aw47G3pCxzJzTd94J0=", + "title": "Extra Columns Plugin", + "url": "http://updates.jenkins-ci.org/download/plugins/extra-columns/1.17/extra-columns.hpi", + "version": "1.17", + "wiki": "https://wiki.jenkins-ci.org/display/JENKINS/Extra+Columns+Plugin" + }, + "structs": { + "buildDate": "Aug 30, 2016", + "dependencies": [], + "developers": [ + { + "developerId": "jglick" + } + ], + "excerpt": "Library plugin for DSL plugins that need concise names for Jenkins extensions", + "gav": "org.jenkins-ci.plugins:structs:1.5", + "labels": [], + "name": "structs", + "previousTimestamp": "2016-08-26T14:11:44.00Z", + "previousVersion": "1.4", + "releaseTimestamp": "2016-08-30T14:10:10.00Z", + "requiredCore": "1.580.1", + "scm": "github.com", + "sha1": "fK+F0PEfSS//DOqmNJvX2rQYabo=", + "title": "Structs Plugin", + "url": "http://updates.jenkins-ci.org/download/plugins/structs/1.5/structs.hpi", + "version": "1.5", + "wiki": "https://wiki.jenkins-ci.org/display/JENKINS/Structs+plugin" + } + }, + "warnings": [ + { + "id": "SECURITY-208", + "type": "plugin", + "name": "google-login", + "message": "Authentication bypass vulnerability", + "url": "https://wiki.jenkins-ci.org/display/SECURITY/Jenkins+Security+Advisory+2015-10-12", + "versions": [ + { + "lastVersion": "1.1", + "pattern": "1[.][01](|[.-].*)" + } + ] + }, + { + "id": "SECURITY-136", + "type": "plugin", + "name": "extra-columns", + "message": "Stored XSS vulnerability", + "url": "https://wiki.jenkins-ci.org/display/SECURITY/Jenkins+Security+Advisory+2016-04-11", + "versions": [ + { + "lastVersion": "1.16", + "pattern": "1[.](\\d|1[0123456])(|[.-].*)" + } + ] + }, + { + "id": "SECURITY-258", + "type": "plugin", + "name": "extra-columns", + "message": "Groovy sandbox protection incomplete", + "url": "https://wiki.jenkins-ci.org/display/SECURITY/Jenkins+Security+Advisory+2016-04-11", + "versions": [ + { + "lastVersion": "1.18", + "pattern": "1[.](\\d|1[012345678])(|[.-].*)" + } + ] + }, + { + "id": "SECURITY-85", + "type": "plugin", + "name": "tap", + "message": "Path traversal vulnerability", + "url": "https://wiki.jenkins-ci.org/display/SECURITY/Jenkins+Security+Advisory+2016-06-20", + "versions": [ + { + "lastVersion": "1.24", + "pattern": "1[.](\\d|1\\d|2[01234])(|[.-].*)" + } + ] + }, + { + "id": "SECURITY-278", + "type": "plugin", + "name": "image-gallery", + "message": "Path traversal vulnerability", + "url": "https://wiki.jenkins-ci.org/display/SECURITY/Jenkins+Security+Advisory+2016-06-20", + "versions": [ + { + "lastVersion": "1.3", + "pattern": "(0[.].*|1[.][0123])(|[.-].*)" + } + ] + }, + { + "id": "SECURITY-290", + "type": "plugin", + "name": "build-failure-analyzer", + "message": "Cross-site scripting vulnerability", + "url": "https://wiki.jenkins-ci.org/display/SECURITY/Jenkins+Security+Advisory+2016-06-20", + "versions": [ + { + "lastVersion": "1.15.0", + "pattern": "1[.](\\d|1[012345])[.]\\d+(|[.-].*)" + } + ] + }, + { + "id": "SECURITY-305", + "type": "plugin", + "versions": [ + { + "lastVersion": "1.7.24", + "pattern": "1[.]7[.](\\d(|[.-].*)|24)" + } + ] + }, + { + "id": "SECURITY-309", + "type": "plugin", + "name": "cucumber-reports", + "message": "Plugin disables Content-Security-Policy for files served by Jenkins", + "url": "https://wiki.jenkins-ci.org/display/SECURITY/Jenkins+Security+Advisory+2016-07-27", + "versions": [ + { + "firstVersion": "1.3.0", + "lastVersion": "2.5.1", + "pattern": "(1[.][34]|2[.][012345])(|[.-].*)" + } + ] + } + ], + "updateCenterVersion": "1" +} \ No newline at end of file diff --git a/war/src/main/webapp/css/style.css b/war/src/main/webapp/css/style.css index 9491f28afb05baa282fc15dcdce5ab63e0c0ab9e..3f2812f2cb71621513315eace4cd38fc51b0da19 100644 --- a/war/src/main/webapp/css/style.css +++ b/war/src/main/webapp/css/style.css @@ -1580,6 +1580,13 @@ TEXTAREA.rich-editor { color: #FF0000; } +#plugins .securityWarning { + white-space: normal; + margin-top: 0.5em; + padding-left: 2em; + color: #FF0000; +} + /* ========================= progress bar ========================= */ table.progress-bar {