From 5c2eec522d59a58e6a4beac1d81ed83ed691a226 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Wed, 28 Dec 2022 12:16:20 +0100 Subject: [PATCH] [platform] changing the way the Windows Defender checker works (IDEA-298058) GitOrigin-RevId: 64a290b7e8d7d56854bc7ae9f6d7f665be057285 --- bin/win/defender-exclusions.ps1 | 48 ++ .../messages/DiagnosticBundle.properties | 30 +- .../ResetWindowsDefenderNotification.java | 26 +- .../diagnostic/WindowsDefenderChecker.java | 450 ++++++------------ .../WindowsDefenderCheckerActivity.kt | 133 +++--- .../src/META-INF/PlatformExtensionPoints.xml | 2 + .../src/META-INF/PlatformExtensions.xml | 52 +- .../WindowsDefenderCheckerTest.java | 16 + .../gradle/java/resources/META-INF/plugin.xml | 9 +- .../util/GradleWindowsDefenderCheckerExt.java | 27 ++ 10 files changed, 377 insertions(+), 416 deletions(-) create mode 100644 bin/win/defender-exclusions.ps1 create mode 100644 platform/platform-tests/testSrc/com/intellij/diagnostic/WindowsDefenderCheckerTest.java create mode 100644 plugins/gradle/java/src/util/GradleWindowsDefenderCheckerExt.java diff --git a/bin/win/defender-exclusions.ps1 b/bin/win/defender-exclusions.ps1 new file mode 100644 index 000000000000..f77807fe2f23 --- /dev/null +++ b/bin/win/defender-exclusions.ps1 @@ -0,0 +1,48 @@ +<# + The script adds paths, given as parameters, to the Windows Defender folder exclusion list, + unless they are already excluded. +#> + +#Requires -RunAsAdministrator + +if ($args.Count -eq 0) { + Write-Host "usage: $PSCommandPath path [path ...]" + exit 1 +} + +try { + Import-Module Defender + + # returns `$true` when a path is already covered by the exclusion list + function Test-Excluded ([string] $path, [string[]] $exclusions) { + foreach ($exclusion in $exclusions) { + $expanded = [System.Environment]::ExpandEnvironmentVariables($exclusion) + $resolvedPaths = Resolve-Path -Path $expanded + foreach ($resolved in $resolvedPaths) { + $resolvedStr = $resolved.ProviderPath.ToString() + if ([cultureinfo]::InvariantCulture.CompareInfo.IsPrefix($path, $resolvedStr, @("IgnoreCase"))) { + return $true + } + } + } + + return $false + } + + $exclusions = (Get-MpPreference).ExclusionPath + + foreach ($path in $args) { + if (-not (Test-Excluded $path $exclusions)) { + $exclusions += $path + Write-Host "added: $path" + } else { + Write-Host "skipped: $path" + } + } + + Set-MpPreference -ExclusionPath $exclusions +} catch { + Write-Host $_.Exception.Message + Write-Host $_.ScriptStackTrace + exit 1 +} diff --git a/platform/platform-impl/resources/messages/DiagnosticBundle.properties b/platform/platform-impl/resources/messages/DiagnosticBundle.properties index a76e715545b0..7ceadd63451d 100644 --- a/platform/platform-impl/resources/messages/DiagnosticBundle.properties +++ b/platform/platform-impl/resources/messages/DiagnosticBundle.properties @@ -159,18 +159,24 @@ change.memory.apply=Save and Restart change.memory.exit=Save and Exit change.memory.low=The value should be greater than {0} -virus.scanning.warn.title=Windows Defender might impact performance -virus.scanning.warn.message=Exclude IDE and project directories from antivirus scans:
{0}
Alternatively, add the IDE process as an exclusion. -virus.scanning.fix.action=Exclude directories... -virus.scanning.fix.explanation=To improve performance, {0} can update your Windows Defender configuration to exclude your IDE and project directories from real-time scanning.
\ - This will require running a command with administrator privileges.
\ - Alternatively, you can configure Windows Defender manually, according to the instructions. -virus.scanning.fix.title=Configure Windows Defender -virus.scanning.fix.automatically=Configure Automatically -virus.scanning.fix.manually=Configure Manually -virus.scanning.fix.success.notification=Windows Defender configuration updated -virus.scanning.fix.failed=Failed to update Windows Defender configuration: {0} -virus.scanning.dont.show.again=Don't show again +notification.group.defender.config=Windows Defender configuration +defender.config.prompt=The IDE has detected Windows Defender with Real-Time Protection enabled. \ + It might severely degrade IDE performance. \ + It is recommended to add following paths to the Defender folder exclusion list:
{0}

\ + Choose "{1}" to run a script that excludes these paths (note: Windows will ask for administrative privileges). \ + Choose "{2}" to see Defender configuration instructions. +defender.config.prompt.no.script=The IDE has detected Windows Defender with Real-Time Protection enabled. \ + It might severely degrade IDE performance. \ + It is recommended to add following paths to the Defender folder exclusion list:
{0}

+defender.config.auto=Automatically +defender.config.manual=Manually +defender.config.instructions=See instructions +defender.config.suppress1=Ignore for this project +defender.config.suppress2=Never ask again +defender.config.progress=Updating Windows Defender configuration +defender.config.success=Project paths were successfully added to the Windows Defender exclusion list +defender.config.failed=Windows Defender configuration script failed. Please look for "WindowsDefenderChecker" records in the log. +defender.config.restore=OK. If you change your mind, please use "{0}" action. label.issue.type=Issue Type: label.the.information.may.contain.sensitive.data=The information may contain sensitive data diff --git a/platform/platform-impl/src/com/intellij/diagnostic/ResetWindowsDefenderNotification.java b/platform/platform-impl/src/com/intellij/diagnostic/ResetWindowsDefenderNotification.java index acf4317b27da..8d0526387bb2 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/ResetWindowsDefenderNotification.java +++ b/platform/platform-impl/src/com/intellij/diagnostic/ResetWindowsDefenderNotification.java @@ -1,7 +1,6 @@ -// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.diagnostic; -import com.intellij.ide.util.PropertiesComponent; import com.intellij.openapi.actionSystem.ActionUpdateThread; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; @@ -10,16 +9,10 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.util.SystemInfo; import org.jetbrains.annotations.NotNull; -public class ResetWindowsDefenderNotification extends AnAction { +final class ResetWindowsDefenderNotification extends AnAction { @Override - public void actionPerformed(@NotNull AnActionEvent e) { - PropertiesComponent.getInstance().setValue(WindowsDefenderChecker.IGNORE_VIRUS_CHECK, false); - Project project = e.getProject(); - if (project != null) { - PropertiesComponent.getInstance(project).setValue(WindowsDefenderChecker.IGNORE_VIRUS_CHECK, false); - ApplicationManager.getApplication().executeOnPooledThread( - () -> new WindowsDefenderCheckerActivity().runActivity(project)); - } + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.BGT; } @Override @@ -28,7 +21,14 @@ public class ResetWindowsDefenderNotification extends AnAction { } @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - return ActionUpdateThread.BGT; + public void actionPerformed(@NotNull AnActionEvent e) { + WindowsDefenderChecker checker = WindowsDefenderChecker.getInstance(); + checker.ignoreStatusCheck(null, false); + Project project = e.getProject(); + if (project != null) { + checker.ignoreStatusCheck(project, false); + ApplicationManager.getApplication().executeOnPooledThread( + () -> new WindowsDefenderCheckerActivity().runActivity(project)); + } } } diff --git a/platform/platform-impl/src/com/intellij/diagnostic/WindowsDefenderChecker.java b/platform/platform-impl/src/com/intellij/diagnostic/WindowsDefenderChecker.java index 1fd820fb44a8..e5876f57e228 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/WindowsDefenderChecker.java +++ b/platform/platform-impl/src/com/intellij/diagnostic/WindowsDefenderChecker.java @@ -3,339 +3,203 @@ package com.intellij.diagnostic; import com.intellij.execution.ExecutionException; import com.intellij.execution.configurations.GeneralCommandLine; +import com.intellij.execution.configurations.PathEnvironmentVariableUtil; import com.intellij.execution.process.ProcessOutput; import com.intellij.execution.util.ExecUtil; import com.intellij.ide.util.PropertiesComponent; -import com.intellij.notification.Notification; -import com.intellij.notification.NotificationAction; -import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.util.NlsContexts; -import com.intellij.openapi.util.text.StringUtil; -import com.intellij.openapi.vfs.impl.local.NativeFileWatcherImpl; -import com.intellij.util.Restarter; -import com.intellij.util.containers.ContainerUtil; -import com.intellij.util.ui.UIUtil; +import com.intellij.openapi.project.ProjectUtil; +import com.intellij.openapi.util.NullableLazyValue; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.sun.jna.platform.win32.COM.WbemcliUtil; +import com.sun.jna.platform.win32.Ole32; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.TreeSet; +import java.util.stream.Stream; +import static com.intellij.openapi.util.NullableLazyValue.volatileLazyNullable; +import static java.util.Objects.requireNonNull; + +/** + * Sources: + * Defender Settings, + * Defender PowerShell Module. + */ +@SuppressWarnings("MethodMayBeStatic") public class WindowsDefenderChecker { private static final Logger LOG = Logger.getInstance(WindowsDefenderChecker.class); - private static final Pattern WINDOWS_ENV_VAR_PATTERN = Pattern.compile("%([^%]+?)%"); - private static final Pattern WINDOWS_DEFENDER_WILDCARD_PATTERN = Pattern.compile("[?*]"); - private static final int WMIC_COMMAND_TIMEOUT_MS = 10000; - private static final int POWERSHELL_COMMAND_TIMEOUT_MS = 10000; - private static final int MAX_POWERSHELL_STDERR_LENGTH = 500; - static final String IGNORE_VIRUS_CHECK = "ignore.virus.scanning.warn.message"; + private static final String IGNORE_STATUS_CHECK = "ignore.virus.scanning.warn.message"; + private static final String HELPER_SCRIPT_NAME = "defender-exclusions.ps1"; + private static final String SIG_MARKER = "# SIG # Begin signature block"; + private static final int WMIC_COMMAND_TIMEOUT_MS = 10_000, POWERSHELL_COMMAND_TIMEOUT_MS = 30_000; + private static final ExtensionPointName EP_NAME = ExtensionPointName.create("com.intellij.defender.config"); - public enum RealtimeScanningStatus { - SCANNING_DISABLED, - SCANNING_ENABLED, - ERROR + public interface Extension { + @NotNull Collection getPaths(@NotNull Project project); } public static WindowsDefenderChecker getInstance() { return ApplicationManager.getApplication().getService(WindowsDefenderChecker.class); } - public static class CheckResult { - public final RealtimeScanningStatus status; - - // Value in the map is true if the path is excluded, false otherwise - public final Map pathStatus; - - public CheckResult(RealtimeScanningStatus status, Map pathStatus) { - this.status = status; - this.pathStatus = pathStatus; + private final NullableLazyValue myHelper = volatileLazyNullable(() -> { + var candidate = PathManager.findBinFile(HELPER_SCRIPT_NAME); + if (candidate != null) { + try { + if (Files.readString(candidate).contains(SIG_MARKER)) { + return candidate; + } + } + catch (IOException e) { + LOG.warn(e); + } } + LOG.info("'" + HELPER_SCRIPT_NAME + (candidate == null ? "' is missing" : "' is unsigned")); + return null; + }); + + public boolean isStatusCheckIgnored(@NotNull Project project) { + return PropertiesComponent.getInstance().isTrueValue(IGNORE_STATUS_CHECK) || + PropertiesComponent.getInstance(project).isTrueValue(IGNORE_STATUS_CHECK); } - public boolean isVirusCheckIgnored(Project project) { - return PropertiesComponent.getInstance().isTrueValue(IGNORE_VIRUS_CHECK) || - PropertiesComponent.getInstance(project).isTrueValue(IGNORE_VIRUS_CHECK); - } - - public CheckResult checkWindowsDefender(@NotNull Project project) { - final Boolean windowsDefenderActive = isWindowsDefenderActive(); - if (windowsDefenderActive == null || !windowsDefenderActive) { - LOG.info("Windows Defender status: not used"); - return new CheckResult(RealtimeScanningStatus.SCANNING_DISABLED, Collections.emptyMap()); - } - - RealtimeScanningStatus scanningStatus = getRealtimeScanningEnabled(); - if (scanningStatus == RealtimeScanningStatus.SCANNING_ENABLED) { - Collection excludedProcesses = getExcludedProcesses(); - List processesToCheck = getProcessesToCheck(); - if (excludedProcesses != null && - ContainerUtil.all(processesToCheck, exe -> excludedProcesses.contains(exe.getFileName().toString().toLowerCase(Locale.ENGLISH))) && - excludedProcesses.contains("java.exe")) { - LOG.info("Windows Defender status: all relevant processes excluded from real-time scanning"); - return new CheckResult(RealtimeScanningStatus.SCANNING_DISABLED, Collections.emptyMap()); - } - - List excludedPatterns = getExcludedPatterns(); - if (excludedPatterns != null) { - Map pathStatuses = checkPathsExcluded(getImportantPaths(project), excludedPatterns); - boolean anyPathNotExcluded = !ContainerUtil.all(pathStatuses.values(), Boolean::booleanValue); - if (anyPathNotExcluded) { - LOG.info("Windows Defender status: some relevant paths not excluded from real-time scanning, notifying user"); - } - else { - LOG.info("Windows Defender status: all relevant paths excluded from real-time scanning"); - } - return new CheckResult(scanningStatus, pathStatuses); - } - else { - LOG.info("Windows Defender status: Failed to get excluded patterns"); - return new CheckResult(RealtimeScanningStatus.ERROR, Collections.emptyMap()); - } - } - if (scanningStatus == RealtimeScanningStatus.ERROR) { - LOG.info("Windows Defender status: failed to detect"); + final void ignoreStatusCheck(@Nullable Project project, boolean ignore) { + var component = project == null ? PropertiesComponent.getInstance() : PropertiesComponent.getInstance(project); + if (ignore) { + component.setValue(IGNORE_STATUS_CHECK, true); } else { - LOG.info("Windows Defender status: real-time scanning disabled"); + component.unsetValue(IGNORE_STATUS_CHECK); } - return new CheckResult(scanningStatus, Collections.emptyMap()); } - protected @NotNull List getProcessesToCheck() { - List result = new ArrayList<>(); - Path ideStarter = Restarter.getIdeStarter(); - if (ideStarter != null) { - result.add(ideStarter); - } - Path fsNotifier = NativeFileWatcherImpl.getFSNotifierExecutable(); - if (fsNotifier != null) { - result.add(fsNotifier); - } - return result; - } - - private static Boolean isWindowsDefenderActive() { + /** + * {@link Boolean#TRUE} means Defender is present, active, and real-time protection check is enabled. + * {@link Boolean#FALSE} means something from the above list is not true. + * {@code null} means the IDE cannot detect the status. + */ + public @Nullable Boolean isRealTimeProtectionEnabled() { try { - ProcessOutput output = ExecUtil.execAndGetOutput(new GeneralCommandLine( - "wmic", "/Namespace:\\\\root\\SecurityCenter2", "Path", "AntivirusProduct", "Get", "displayName,productState" - ), WMIC_COMMAND_TIMEOUT_MS); - if (output.getExitCode() == 0) { - return parseWindowsDefenderProductState(output); + var comInit = Ole32.INSTANCE.CoInitializeEx(null, Ole32.COINIT_APARTMENTTHREADED); + if (LOG.isDebugEnabled()) LOG.debug("CoInitializeEx: " + comInit); + + var avQuery = new WbemcliUtil.WmiQuery<>("Root\\SecurityCenter2", "AntivirusProduct", AntivirusProduct.class); + var avResult = avQuery.execute(WMIC_COMMAND_TIMEOUT_MS); + if (LOG.isDebugEnabled()) LOG.debug("results: " + avResult.getResultCount()); + for (var i = 0; i < avResult.getResultCount(); i++) { + var name = avResult.getValue(AntivirusProduct.DisplayName, i); + if (LOG.isDebugEnabled()) LOG.debug("DisplayName[" + i + "]: " + name + " (" + name.getClass().getName() + ')'); + if (name instanceof String s && s.contains("Windows Defender")) { + var state = avResult.getValue(AntivirusProduct.ProductState, i); + if (LOG.isDebugEnabled()) LOG.debug("ProductState: " + state + " (" + state.getClass().getName() + ')'); + var enabled = state instanceof Integer intState && (intState.intValue() & 0x1000) != 0; + if (!enabled) return false; + break; + } + } + + var statusQuery = new WbemcliUtil.WmiQuery<>("Root\\Microsoft\\Windows\\Defender", "MSFT_MpComputerStatus", MpComputerStatus.class); + var statusResult = statusQuery.execute(WMIC_COMMAND_TIMEOUT_MS); + if (LOG.isDebugEnabled()) LOG.debug("results: " + statusResult.getResultCount()); + if (statusResult.getResultCount() != 1) return false; + var rtProtection = statusResult.getValue(MpComputerStatus.RealTimeProtectionEnabled, 0); + if (LOG.isDebugEnabled()) LOG.debug("RealTimeProtectionEnabled: " + rtProtection + " (" + rtProtection.getClass().getName() + ')'); + return Boolean.TRUE.equals(rtProtection); + } + catch (Exception e) { + LOG.warn("WMI Windows Defender check failed", e); + return null; + } + } + + final boolean canRunScript() { + return myHelper.getValue() != null; + } + + private enum AntivirusProduct {DisplayName, ProductState} + private enum MpComputerStatus {RealTimeProtectionEnabled} + + final @NotNull List getImportantPaths(@NotNull Project project) { + var paths = new TreeSet(); + + var projectDir = ProjectUtil.guessProjectDir(project); + if (projectDir != null && projectDir.getFileSystem() instanceof LocalFileSystem) { + paths.add(projectDir.toNioPath()); + } + + paths.add(PathManager.getSystemDir()); + + EP_NAME.forEachExtensionSafe(ext -> { + paths.addAll(ext.getPaths(project)); + }); + + return new ArrayList<>(paths); + } + + final boolean excludeProjectPaths(@NotNull List paths) { + try { + var script = requireNonNull(myHelper.getValue(), "missing/dysfunctional helper"); + + var psh = PathEnvironmentVariableUtil.findInPath("powershell.exe"); + if (psh == null) { + LOG.info("no 'powershell.exe' on " + PathEnvironmentVariableUtil.getPathVariableValue()); + return false; + } + var sane = Stream.of("SystemRoot", "ProgramFiles").map(System::getenv).anyMatch(val -> val != null && psh.toPath().startsWith(val)); + if (!sane) { + LOG.info("suspicious 'powershell.exe' location: " + psh); + return false; + } + + var command = new GeneralCommandLine(psh.getPath(), "-NonInteractive", "-Command", "(Get-AuthenticodeSignature '" + script + "').Status"); + var output = run(command); + if (output.getExitCode() != 0 || !"Valid".equals(output.getStdout().trim())) { + LOG.info("validation failed:\n[" + output.getExitCode() + "] " + command + "\noutput: " + output.getStdout().trim()); + return false; + } + + command = ExecUtil.sudoCommand( + new GeneralCommandLine(Stream.concat( + Stream.of(psh.getPath(), "-ExecutionPolicy", "Bypass", "-NonInteractive", "-File", script.toString()), + paths.stream().map(Path::toString) + ).toList()), + ""); + output = run(command); + if (output.getExitCode() != 0) { + LOG.info("script failed:\n[" + output.getExitCode() + "] " + command + "\noutput: " + output.getStdout().trim()); + return false; } else { - LOG.warn("wmic Windows Defender check exited with status " + output.getExitCode() + ": " + - StringUtil.first(output.getStderr(), MAX_POWERSHELL_STDERR_LENGTH, false)); + LOG.info("OK; script output:\n" + output.getStdout().trim()); + return true; } } - catch (ExecutionException e) { - LOG.warn("wmic Windows Defender check failed", e); + catch (Exception e) { + LOG.warn(e); + return false; } - return null; } - private static Boolean parseWindowsDefenderProductState(ProcessOutput output) { - final String[] lines = StringUtil.splitByLines(output.getStdout()); - for (String line : lines) { - if (line.startsWith("Windows Defender")) { - final String productStateString = StringUtil.substringAfterLast(line, " "); - int productState; - try { - productState = Integer.parseInt(productStateString); - return (productState & 0x1000) != 0; - } - catch (NumberFormatException e) { - LOG.info("Unexpected wmic output format: " + line); - return null; - } - } - } - return false; + private static ProcessOutput run(GeneralCommandLine command) throws ExecutionException { + return ExecUtil.execAndGetOutput( + command.withRedirectErrorStream(true).withWorkDirectory(PathManager.getTempPath()), + POWERSHELL_COMMAND_TIMEOUT_MS); } - /** Runs a powershell command to list the paths that are excluded from realtime scanning by Windows Defender. These - * - * paths can contain environment variable references, as well as wildcards ('?', which matches a single character, and - * '*', which matches any sequence of characters (but cannot match multiple nested directories; i.e., "foo\*\bar" would - * match foo\baz\bar but not foo\baz\quux\bar)). The behavior of wildcards with respect to case-sensitivity is undocumented. - * Returns a list of patterns, one for each exclusion path, that emulate how Windows Defender would interpret that path. - */ - private static @Nullable List getExcludedPatterns() { - final Collection paths = getWindowsDefenderProperty("ExclusionPath"); - if (paths == null) return null; - if (paths.size() > 0) { - String path = paths.iterator().next(); - if (path.length() > 0 && path.indexOf('\\') < 0) { - // "N/A: Must be admin to view exclusions" - return null; - } - } - return ContainerUtil.map(paths, path -> wildcardsToRegex(expandEnvVars(path))); - } - - private static @Nullable Collection getExcludedProcesses() { - final Collection processes = getWindowsDefenderProperty("ExclusionProcess"); - if (processes == null) return null; - return ContainerUtil.map(processes, process -> process.toLowerCase()); - } - - /** Runs a powershell command to determine whether realtime scanning is enabled or not. */ - private static @NotNull RealtimeScanningStatus getRealtimeScanningEnabled() { - final Collection output = getWindowsDefenderProperty("DisableRealtimeMonitoring"); - if (output == null) return RealtimeScanningStatus.ERROR; - if (output.size() > 0 && output.iterator().next().startsWith("False")) return RealtimeScanningStatus.SCANNING_ENABLED; - return RealtimeScanningStatus.SCANNING_DISABLED; - } - - private static @Nullable Collection getWindowsDefenderProperty(final String propertyName) { - try { - ProcessOutput output = ExecUtil.execAndGetOutput(new GeneralCommandLine( - "powershell", "-inputformat", "none", "-outputformat", "text", "-NonInteractive", "-Command", - "Get-MpPreference | select -ExpandProperty \"" + propertyName + "\""), POWERSHELL_COMMAND_TIMEOUT_MS); - if (output.getExitCode() == 0) { - return output.getStdoutLines(); - } else { - LOG.warn("Windows Defender " + propertyName + " check exited with status " + output.getExitCode() + ": " + - StringUtil.first(output.getStderr(), MAX_POWERSHELL_STDERR_LENGTH, false)); - } - } catch (ExecutionException e) { - LOG.warn("Windows Defender " + propertyName + " check failed", e); - } - return null; - } - - /** Returns a list of paths that might impact build performance if Windows Defender were configured to scan them. */ - protected @NotNull List getImportantPaths(@NotNull Project project) { - String homeDir = System.getProperty("user.home"); - String gradleUserHome = System.getenv("GRADLE_USER_HOME"); - String projectDir = project.getBasePath(); - - List paths = new ArrayList<>(); - if (projectDir != null) { - paths.add(Paths.get(projectDir)); - } - paths.add(Paths.get(PathManager.getSystemPath())); - if (gradleUserHome != null) { - paths.add(Paths.get(gradleUserHome)); - } else { - paths.add(Paths.get(homeDir, ".gradle")); - } - - return paths; - } - - - /** Expands references to environment variables (strings delimited by '%') in 'path' */ - private static @NotNull String expandEnvVars(@NotNull String path) { - Matcher m = WINDOWS_ENV_VAR_PATTERN.matcher(path); - StringBuilder result = new StringBuilder(); - while (m.find()) { - String value = System.getenv(m.group(1)); - if (value != null) { - m.appendReplacement(result, Matcher.quoteReplacement(value)); - } - } - m.appendTail(result); - return result.toString(); - } - - /** - * Produces a {@link Pattern} that approximates how Windows Defender interprets the exclusion path {@code path}. - * The path is split around wildcards; the non-wildcard portions are quoted, and regex equivalents of - * the wildcards are inserted between them. See - * https://docs.microsoft.com/en-us/windows/security/threat-protection/windows-defender-antivirus/configure-extension-file-exclusions-windows-defender-antivirus - * for more details. - */ - private static @NotNull Pattern wildcardsToRegex(@NotNull String path) { - Matcher m = WINDOWS_DEFENDER_WILDCARD_PATTERN.matcher(path); - StringBuilder sb = new StringBuilder(); - int previousWildcardEnd = 0; - while (m.find()) { - sb.append(Pattern.quote(path.substring(previousWildcardEnd, m.start()))); - if (m.group().equals("?")) { - sb.append("[^\\\\]"); - } else { - sb.append("[^\\\\]*"); - } - previousWildcardEnd = m.end(); - } - sb.append(Pattern.quote(path.substring(previousWildcardEnd))); - sb.append(".*"); // technically this should only be appended if the path refers to a directory, not a file. This is difficult to determine. - return Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE); // CASE_INSENSITIVE is overly permissive. Being precise with this is more work than it's worth. - } - - /** - * Checks whether each of the given paths in {@code paths} is matched by some pattern in {@code excludedPatterns}, - * returning a map of the results. - */ - private static @NotNull Map checkPathsExcluded(@NotNull List paths, @NotNull List excludedPatterns) { - Map result = new HashMap<>(); - for (Path path : paths) { - if (!path.toFile().exists()) continue; - - try { - String canonical = path.toRealPath().toString(); - boolean found = false; - for (Pattern pattern : excludedPatterns) { - if (pattern.matcher(canonical).matches() || pattern.matcher(path.toString()).matches()) { - found = true; - result.put(path, true); - break; - } - } - if (!found) { - result.put(path, false); - } - } catch (IOException e) { - LOG.warn("Windows Defender exclusion check couldn't get real path for " + path, e); - } - } - return result; - } - - public void configureActions(Project project, WindowsDefenderNotification notification) { - notification.addAction(new WindowsDefenderFixAction(notification.getPaths())); - - notification.addAction(new NotificationAction(DiagnosticBundle.message("virus.scanning.dont.show.again")) { - @Override - public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) { - notification.expire(); - PropertiesComponent.getInstance().setValue(IGNORE_VIRUS_CHECK, "true"); - } - }); - } - - public @NlsContexts.NotificationContent String getNotificationText(Set nonExcludedPaths) { - return DiagnosticBundle.message("virus.scanning.warn.message", StringUtil.join(nonExcludedPaths, "
")); - } - - public String getConfigurationInstructionsUrl() { - return "https://intellij-support.jetbrains.com/hc/en-us/articles/360006298560"; - } - - public boolean runExcludePathsCommand(Project project, Collection paths) { - try { - final ProcessOutput output = - ExecUtil.sudoAndGetOutput(new GeneralCommandLine("powershell", "-Command", "Add-MpPreference", "-ExclusionPath", - StringUtil - .join(paths, (path) -> StringUtil.wrapWithDoubleQuote(path.toString()), ",")), - ""); - return output.getExitCode() == 0; - } - catch (IOException | ExecutionException e) { - UIUtil.invokeLaterIfNeeded(() -> - Messages.showErrorDialog(project, DiagnosticBundle.message("virus.scanning.fix.failed", e.getMessage()), - DiagnosticBundle.message("virus.scanning.fix.title"))); - } - return false; + public @NotNull String getConfigurationInstructionsUrl() { + return "https://intellij.com/antivirus-impact-on-build-speed"; } } diff --git a/platform/platform-impl/src/com/intellij/diagnostic/WindowsDefenderCheckerActivity.kt b/platform/platform-impl/src/com/intellij/diagnostic/WindowsDefenderCheckerActivity.kt index 55ba8962f997..8c531ff628b0 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/WindowsDefenderCheckerActivity.kt +++ b/platform/platform-impl/src/com/intellij/diagnostic/WindowsDefenderCheckerActivity.kt @@ -1,86 +1,93 @@ // Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.diagnostic -import com.intellij.CommonBundle import com.intellij.ide.BrowserUtil -import com.intellij.ide.IdeBundle -import com.intellij.notification.* -import com.intellij.notification.impl.NotificationFullContent -import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.ide.actions.ShowLogAction +import com.intellij.idea.ActionsBundle +import com.intellij.notification.Notification +import com.intellij.notification.NotificationAction.createSimple +import com.intellij.notification.NotificationAction.createSimpleExpiring +import com.intellij.notification.NotificationType import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.ApplicationNamesInfo -import com.intellij.openapi.application.EDT +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.progress.runBackgroundableTask import com.intellij.openapi.project.Project import com.intellij.openapi.startup.ProjectPostStartupActivity -import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.NlsContexts -import com.intellij.util.ui.UIUtil -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import kotlinx.coroutines.launch import java.nio.file.Path +private val LOG = logger() + internal class WindowsDefenderCheckerActivity : ProjectPostStartupActivity { + // is called directly from `ResetWindowsDefenderNotification` + override fun runActivity(project: Project) { + @Suppress("DEPRECATION") + project.coroutineScope.launch { execute(project) } + } + override suspend fun execute(project: Project) { - val app = ApplicationManager.getApplication() - if (app.isUnitTestMode) { + if (ApplicationManager.getApplication().isUnitTestMode) return + + val checker = WindowsDefenderChecker.getInstance() + if (checker.isStatusCheckIgnored(project)) { + LOG.info("status check is disabled") return } - val windowsDefenderChecker = WindowsDefenderChecker.getInstance() - if (windowsDefenderChecker.isVirusCheckIgnored(project)) return + val protection = checker.isRealTimeProtectionEnabled + if (protection != true) { + LOG.info("real-time protection: ${protection}") + return + } - val checkResult = windowsDefenderChecker.checkWindowsDefender(project) - if (checkResult.status == WindowsDefenderChecker.RealtimeScanningStatus.SCANNING_ENABLED && - checkResult.pathStatus.any { !it.value }) { + val paths = checker.getImportantPaths(project) + val pathList = paths.joinToString(separator = "
  ", prefix = "
  ") { it.toString() } + val notification = if (checker.canRunScript()) { + val auto = DiagnosticBundle.message("defender.config.auto") + val manual = DiagnosticBundle.message("defender.config.manual") + notification(DiagnosticBundle.message("defender.config.prompt", pathList, auto, manual), NotificationType.INFORMATION) + .addAction(createSimpleExpiring(auto) { updateDefenderConfig(checker, project, paths) }) + .addAction(createSimple(manual) { BrowserUtil.browse(checker.configurationInstructionsUrl) }) + } + else { + notification(DiagnosticBundle.message("defender.config.prompt.no.script", pathList), NotificationType.INFORMATION) + .addAction(createSimple(DiagnosticBundle.message("defender.config.instructions")) { BrowserUtil.browse(checker.configurationInstructionsUrl) }) + } + notification + .also { + it.isImportant = true + it.collapseDirection = Notification.CollapseActionsDirection.KEEP_LEFTMOST + } + .addAction(createSimpleExpiring(DiagnosticBundle.message("defender.config.suppress1")) { suppressCheck(checker, project) }) + .addAction(createSimpleExpiring(DiagnosticBundle.message("defender.config.suppress2")) { suppressCheck(checker, null) }) + .notify(project) + } - val nonExcludedPaths = checkResult.pathStatus.filter { !it.value }.keys - val notification = WindowsDefenderNotification( - DiagnosticBundle.message("virus.scanning.warn.title"), - windowsDefenderChecker.getNotificationText(nonExcludedPaths), - nonExcludedPaths - ) - notification.isImportant = true - notification.collapseDirection = Notification.CollapseActionsDirection.KEEP_LEFTMOST - windowsDefenderChecker.configureActions(project, notification) - - withContext(Dispatchers.EDT) { - notification.notify(project) + private fun updateDefenderConfig(checker: WindowsDefenderChecker, project: Project, paths: List) { + @Suppress("DialogTitleCapitalization") + runBackgroundableTask(DiagnosticBundle.message("defender.config.progress"), project, false) { + val success = checker.excludeProjectPaths(paths) + if (success) { + checker.ignoreStatusCheck(project, true) + notification(DiagnosticBundle.message("defender.config.success"), NotificationType.INFORMATION) + .notify(project) + } + else { + notification(DiagnosticBundle.message("defender.config.failed"), NotificationType.WARNING) + .addAction(ShowLogAction.notificationAction()) + .notify(project) } } } -} -internal class WindowsDefenderNotification(@NlsContexts.NotificationTitle title: String, @NlsContexts.NotificationContent text: String, val paths: Collection) : - Notification(NotificationGroup.createIdWithTitle("System Health", IdeBundle.message("notification.group.system.health")), title, text, NotificationType.WARNING), NotificationFullContent - -internal class WindowsDefenderFixAction(val paths: Collection) : NotificationAction(DiagnosticBundle.message("virus.scanning.fix.action")) { - override fun actionPerformed(e: AnActionEvent, notification: Notification) { - val rc = Messages.showDialog( - e.project, - DiagnosticBundle.message("virus.scanning.fix.explanation", ApplicationNamesInfo.getInstance().fullProductName, - WindowsDefenderChecker.getInstance().configurationInstructionsUrl), - DiagnosticBundle.message("virus.scanning.fix.title"), - arrayOf( - DiagnosticBundle.message("virus.scanning.fix.automatically"), - DiagnosticBundle.message("virus.scanning.fix.manually"), - CommonBundle.getCancelButtonText() - ), - 0, - null) - when (rc) { - 0 -> { - notification.expire() - ApplicationManager.getApplication().executeOnPooledThread { - if (WindowsDefenderChecker.getInstance().runExcludePathsCommand(e.project, paths)) { - UIUtil.invokeLaterIfNeeded { - Notifications.Bus.notifyAndHide( - Notification(NotificationGroup.createIdWithTitle("System Health", IdeBundle.message("notification.group.system.health")), - "", DiagnosticBundle.message("virus.scanning.fix.success.notification"), NotificationType.INFORMATION), e.project) - } - } - } - } - 1 -> BrowserUtil.browse(WindowsDefenderChecker.getInstance().configurationInstructionsUrl) - } + private fun suppressCheck(checker: WindowsDefenderChecker, project: Project?) { + checker.ignoreStatusCheck(project, true) + val action = ActionsBundle.message("action.ResetWindowsDefenderNotification.text") + notification(DiagnosticBundle.message("defender.config.restore", action), NotificationType.INFORMATION) + .notify(project) } + + private fun notification(@NlsContexts.NotificationContent content: String, type: NotificationType): Notification = + Notification("WindowsDefender", DiagnosticBundle.message("notification.group.defender.config"), content, type) } diff --git a/platform/platform-resources/src/META-INF/PlatformExtensionPoints.xml b/platform/platform-resources/src/META-INF/PlatformExtensionPoints.xml index 1d794f50323c..601222815963 100644 --- a/platform/platform-resources/src/META-INF/PlatformExtensionPoints.xml +++ b/platform/platform-resources/src/META-INF/PlatformExtensionPoints.xml @@ -499,5 +499,7 @@ + + diff --git a/platform/platform-resources/src/META-INF/PlatformExtensions.xml b/platform/platform-resources/src/META-INF/PlatformExtensions.xml index 6aff884fa735..3553428d6c84 100644 --- a/platform/platform-resources/src/META-INF/PlatformExtensions.xml +++ b/platform/platform-resources/src/META-INF/PlatformExtensions.xml @@ -1316,6 +1316,7 @@ + @@ -1425,13 +1426,16 @@ - - - - - - - + + + + + + topic="com.intellij.openapi.actionSystem.ex.AnActionListener" activeInTestMode="false"/> + topic="com.intellij.openapi.fileEditor.FileEditorManagerListener" activeInTestMode="false"/> - - - + + topic="com.intellij.ide.impl.trustedProjects.TrustedProjectsListener" activeInHeadlessMode="false" activeInTestMode="false"/> - + topic="com.intellij.ide.impl.trustedProjects.TrustedProjectsListener" activeInHeadlessMode="false" activeInTestMode="false"/> - - - - - + + - + topic="com.intellij.ide.AppLifecycleListener" activeInHeadlessMode="false" activeInTestMode="false"/> + + + @@ -113,7 +115,6 @@ enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.plugins.gradle.codeInspection.GradleConfigurationAvoidanceInspection"/> - - - - - - \ No newline at end of file + diff --git a/plugins/gradle/java/src/util/GradleWindowsDefenderCheckerExt.java b/plugins/gradle/java/src/util/GradleWindowsDefenderCheckerExt.java new file mode 100644 index 000000000000..3be9c1f192d5 --- /dev/null +++ b/plugins/gradle/java/src/util/GradleWindowsDefenderCheckerExt.java @@ -0,0 +1,27 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.plugins.gradle.util; + +import com.intellij.diagnostic.WindowsDefenderChecker; +import com.intellij.openapi.project.Project; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.gradle.settings.GradleSettings; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; + +public class GradleWindowsDefenderCheckerExt implements WindowsDefenderChecker.Extension { + @Override + public @NotNull Collection getPaths(@NotNull Project project) { + if (!GradleSettings.getInstance(project).getLinkedProjectsSettings().isEmpty()) { + var envVar = System.getenv("GRADLE_USER_HOME"); + var gradleDir = envVar != null ? Path.of(envVar) : Path.of(System.getProperty("user.home"), ".gradle"); + if (Files.isDirectory(gradleDir)) { + return List.of(gradleDir); + } + } + + return List.of(); + } +}