[platform] changing the way the Windows Defender checker works (IDEA-298058)

GitOrigin-RevId: 64a290b7e8d7d56854bc7ae9f6d7f665be057285
This commit is contained in:
Roman Shevchenko
2022-12-28 12:16:20 +01:00
committed by intellij-monorepo-bot
parent ce42d3729c
commit 5c2eec522d
10 changed files with 377 additions and 416 deletions

View File

@@ -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
}

View File

@@ -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:<br>{0}<br>Alternatively, add the IDE process as an exclusion.
virus.scanning.fix.action=Exclude directories...
virus.scanning.fix.explanation=<html>To improve performance, {0} can update your Windows Defender configuration to exclude your IDE and project directories from real-time scanning.<br>\
This will require running a command with administrator privileges.<br>\
Alternatively, you can configure Windows Defender manually, according to the <a href="{1}">instructions</a>.
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=<html>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:<br>{0}<br><br> \
Choose "{1}" to run a script that excludes these paths (<b>note:</b> Windows will ask for administrative privileges). \
Choose "{2}" to see Defender configuration instructions.</html>
defender.config.prompt.no.script=<html>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:<br>{0}<br><br></html>
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

View File

@@ -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));
}
}
}

View File

@@ -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:
* <a href="https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/configure-extension-file-exclusions-microsoft-defender-antivirus">Defender Settings</a>,
* <a href="https://learn.microsoft.com/en-us/powershell/module/defender/">Defender PowerShell Module</a>.
*/
@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<Extension> EP_NAME = ExtensionPointName.create("com.intellij.defender.config");
public enum RealtimeScanningStatus {
SCANNING_DISABLED,
SCANNING_ENABLED,
ERROR
public interface Extension {
@NotNull Collection<Path> 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<Path, Boolean> pathStatus;
public CheckResult(RealtimeScanningStatus status, Map<Path, Boolean> pathStatus) {
this.status = status;
this.pathStatus = pathStatus;
private final NullableLazyValue<Path> 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<String> excludedProcesses = getExcludedProcesses();
List<Path> 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<Pattern> excludedPatterns = getExcludedPatterns();
if (excludedPatterns != null) {
Map<Path, Boolean> 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<Path> getProcessesToCheck() {
List<Path> 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<Path> getImportantPaths(@NotNull Project project) {
var paths = new TreeSet<Path>();
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<Path> 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<Pattern> getExcludedPatterns() {
final Collection<String> 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<String> getExcludedProcesses() {
final Collection<String> 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<String> 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<String> 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<Path> getImportantPaths(@NotNull Project project) {
String homeDir = System.getProperty("user.home");
String gradleUserHome = System.getenv("GRADLE_USER_HOME");
String projectDir = project.getBasePath();
List<Path> 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<Path, Boolean> checkPathsExcluded(@NotNull List<? extends Path> paths, @NotNull List<Pattern> excludedPatterns) {
Map<Path, Boolean> 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<? extends Path> nonExcludedPaths) {
return DiagnosticBundle.message("virus.scanning.warn.message", StringUtil.join(nonExcludedPaths, "<br/>"));
}
public String getConfigurationInstructionsUrl() {
return "https://intellij-support.jetbrains.com/hc/en-us/articles/360006298560";
}
public boolean runExcludePathsCommand(Project project, Collection<Path> 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";
}
}

View File

@@ -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<WindowsDefenderCheckerActivity>()
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 = "<br>&nbsp;&nbsp;", prefix = "<br>&nbsp;&nbsp;") { 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<Path>) {
@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<Path>) :
Notification(NotificationGroup.createIdWithTitle("System Health", IdeBundle.message("notification.group.system.health")), title, text, NotificationType.WARNING), NotificationFullContent
internal class WindowsDefenderFixAction(val paths: Collection<Path>) : 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)
}

View File

@@ -499,5 +499,7 @@
<extensionPoint name="internal.ml.featureProvider" beanClass="com.intellij.lang.LanguageExtensionPoint" dynamic="true">
<with attribute="implementationClass" implements="com.intellij.internal.ml.MLFeatureProvider"/>
</extensionPoint>
<extensionPoint name="defender.config" interface="com.intellij.diagnostic.WindowsDefenderChecker$Extension" dynamic="true" />
</extensionPoints>
</idea-plugin>

View File

@@ -1316,6 +1316,7 @@
<notificationGroup id="Test Results" displayType="TOOL_WINDOW" toolWindowId="Run" isLogByDefault="false" hideFromSettings="true"/>
<notificationGroup id="feedback.form" displayType="BALLOON" bundle="messages.ApplicationBundle" key="feedback.form.notification.group"/>
<notificationGroup id="PerformanceWatcher" displayType="STICKY_BALLOON" bundle="messages.DiagnosticBundle" key="notification.group.performance.watcher"/>
<notificationGroup id="WindowsDefender" displayType="BALLOON" bundle="messages.DiagnosticBundle" key="notification.group.defender.config"/>
<defaultHighlightingSettingProvider implementation="com.intellij.codeInsight.actions.ReaderModeHighlightingSettingsProvider"/>
<registryKey key="html.editor.timeout" defaultValue="15000" description="HTML editor content loading timeout, ms"/>
@@ -1425,13 +1426,16 @@
</extensions>
<applicationListeners>
<listener class="com.intellij.ide.plugins.DynamicPluginsFrameStateListener" topic="com.intellij.openapi.application.ApplicationActivationListener"/>
<listener class="com.intellij.openapi.updateSettings.impl.UpdateCheckerService$MyAppLifecycleListener" topic="com.intellij.ide.AppLifecycleListener"/>
<listener class="com.intellij.openapi.updateSettings.impl.UpdateSettingsEntryPointActionProvider$LifecycleListener" topic="com.intellij.ide.AppLifecycleListener"/>
<listener class="com.intellij.ui.mac.MergeAllWindowsAction$RecentProjectsFullScreenTabSupport" topic="com.intellij.ide.AppLifecycleListener"/>
<listener class="com.intellij.openapi.vcs.FileStatusFactoryImpl$PluginListener" topic="com.intellij.ide.plugins.DynamicPluginListener"/>
<listener class="com.intellij.ide.plugins.DynamicPluginsFrameStateListener"
topic="com.intellij.openapi.application.ApplicationActivationListener"/>
<listener class="com.intellij.openapi.updateSettings.impl.UpdateCheckerService$MyAppLifecycleListener"
topic="com.intellij.ide.AppLifecycleListener"/>
<listener class="com.intellij.openapi.updateSettings.impl.UpdateSettingsEntryPointActionProvider$LifecycleListener"
topic="com.intellij.ide.AppLifecycleListener"/>
<listener class="com.intellij.ui.mac.MergeAllWindowsAction$RecentProjectsFullScreenTabSupport"
topic="com.intellij.ide.AppLifecycleListener"/>
<listener class="com.intellij.openapi.vcs.FileStatusFactoryImpl$PluginListener"
topic="com.intellij.ide.plugins.DynamicPluginListener"/>
<listener class="com.intellij.notification.impl.widget.NotificationWidgetListener" activeInHeadlessMode="false" activeInTestMode="false"
topic="com.intellij.ide.ui.UISettingsListener"/>
<listener class="com.intellij.notification.impl.widget.NotificationWidgetListener" activeInHeadlessMode="false" activeInTestMode="false"
@@ -1443,47 +1447,37 @@
<listener class="com.intellij.openapi.fileTypes.StdFileTypes$StdFileTypesUpdater" activeInHeadlessMode="true" activeInTestMode="false"
topic="com.intellij.openapi.fileTypes.FileTypeListener"/>
<listener class="com.intellij.internal.statistic.collectors.fus.TypingEventsLogger$TypingEventsListener" activeInHeadlessMode="true"
activeInTestMode="false"
topic="com.intellij.openapi.actionSystem.ex.AnActionListener"/>
topic="com.intellij.openapi.actionSystem.ex.AnActionListener" activeInTestMode="false"/>
<listener class="com.intellij.internal.statistic.collectors.fus.TypingEventsLogger$TypingLatencyReporter"
topic="com.intellij.openapi.fileEditor.FileEditorManagerListener"
activeInTestMode="false"/>
topic="com.intellij.openapi.fileEditor.FileEditorManagerListener" activeInTestMode="false"/>
<listener class="com.intellij.featureStatistics.StatisticsStateCollectorsTrigger" activeInTestMode="false" activeInHeadlessMode="false"
topic="com.intellij.ide.AppLifecycleListener"/>
<listener class="com.intellij.ide.plugins.CreateAllServicesAndExtensionsActivity" topic="com.intellij.ide.AppLifecycleListener"
activeInHeadlessMode="false" activeInTestMode="false"/>
<listener class="com.intellij.ide.plugins.CreateAllServicesAndExtensionsActivity"
topic="com.intellij.ide.AppLifecycleListener" activeInHeadlessMode="false" activeInTestMode="false"/>
<listener class="com.intellij.ide.actionsOnSave.impl.ActionsOnSaveFileDocumentManagerListener"
topic="com.intellij.openapi.fileEditor.FileDocumentManagerListener"/>
<listener class="com.intellij.ide.actionsOnSave.impl.ActionsOnSaveFileDocumentManagerListener$CurrentActionListener"
topic="com.intellij.openapi.actionSystem.ex.AnActionListener"/>
<listener class="com.intellij.ide.impl.UntrustedProjectNotificationProvider$TrustedListener"
topic="com.intellij.ide.impl.trustedProjects.TrustedProjectsListener"
activeInHeadlessMode="false" activeInTestMode="false"/>
topic="com.intellij.ide.impl.trustedProjects.TrustedProjectsListener" activeInHeadlessMode="false" activeInTestMode="false"/>
<listener class="com.intellij.ide.impl.TrustStateListener$Bridge"
topic="com.intellij.ide.impl.trustedProjects.TrustedProjectsListener"
activeInHeadlessMode="false" activeInTestMode="false"/>
topic="com.intellij.ide.impl.trustedProjects.TrustedProjectsListener" activeInHeadlessMode="false" activeInTestMode="false"/>
<listener class="com.intellij.openapi.util.registry.EarlyAccessRegistryManager$MyListener"
topic="com.intellij.openapi.util.registry.RegistryValueListener"/>
<listener class="com.intellij.ide.ui.experimental.toolbar.ExperimentalToolbarSettings$ToolbarRegistryListener"
topic="com.intellij.openapi.util.registry.RegistryValueListener"/>
<listener class="com.intellij.ui.ExperimentalUI$NewUiRegistryListener"
topic="com.intellij.openapi.util.registry.RegistryValueListener"/>
<listener class="com.intellij.ide.FrameStateManagerAppListener" topic="com.intellij.openapi.application.ApplicationActivationListener"/>
<listener class="com.intellij.diagnostic.FusFreezeReporter" topic="com.intellij.diagnostic.IdePerformanceListener"/>
<listener class="com.intellij.ide.FrameStateManagerAppListener"
topic="com.intellij.openapi.application.ApplicationActivationListener"/>
<listener class="com.intellij.diagnostic.FusFreezeReporter"
topic="com.intellij.diagnostic.IdePerformanceListener"/>
<listener class="com.intellij.openapi.keymap.impl.KeymapFlagsStorageListener"
topic="com.intellij.openapi.keymap.KeymapManagerListener"/>
<listener class="com.intellij.ide.plugins.advertiser.OnDemandDependencyFeatureCollector"
topic="com.intellij.ide.AppLifecycleListener"
activeInHeadlessMode="false"
activeInTestMode="false"/>
topic="com.intellij.ide.AppLifecycleListener" activeInHeadlessMode="false" activeInTestMode="false"/>
</applicationListeners>
<projectListeners>
<listener class="com.intellij.notification.impl.widget.NotificationWidgetListener"
activeInHeadlessMode="false"

View File

@@ -0,0 +1,16 @@
// 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.testFramework.fixtures.BareTestFixtureTestCase;
import org.junit.Test;
import static com.intellij.openapi.util.io.IoTestUtil.assumeWindows;
import static org.junit.Assert.assertNotNull;
public class WindowsDefenderCheckerTest extends BareTestFixtureTestCase {
@Test
public void defenderStatusDetection() {
assumeWindows();
assertNotNull(WindowsDefenderChecker.getInstance().isRealTimeProtectionEnabled());
}
}

View File

@@ -75,6 +75,8 @@
<moduleBuilder builderClass="org.jetbrains.plugins.gradle.service.project.wizard.InternalGradleModuleBuilder"/>
<projectImportProvider implementation="org.jetbrains.plugins.gradle.service.project.wizard.JavaGradleProjectImportProvider"/>
<defender.config implementation="org.jetbrains.plugins.gradle.util.GradleWindowsDefenderCheckerExt"/>
<!--Gradle Test Runner-->
<testActionProvider implementation="org.jetbrains.plugins.gradle.execution.test.runner.OpenGradleTestResultActionProvider"/>
@@ -113,7 +115,6 @@
enabledByDefault="true" level="WARNING"
implementationClass="org.jetbrains.plugins.gradle.codeInspection.GradleConfigurationAvoidanceInspection"/>
<localInspection language="" groupPath="Gradle" shortName="DependencyNotationArgument"
bundle="messages.GradleInspectionBundle"
key="inspection.display.name.unrecognized.dependency.notation" groupKey="group.names.probable.bugs" groupBundle="messages.InspectionsBundle"
@@ -137,9 +138,5 @@
key="inspection.display.name.deprecated.configurations" groupKey="inspection.validity" groupBundle="messages.GradleInspectionBundle"
enabledByDefault="true" level="WARNING"
implementationClass="org.jetbrains.plugins.gradle.codeInspection.GradleDeprecatedConfigurationInspection"/>
</extensions>
</idea-plugin>
</idea-plugin>

View File

@@ -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<Path> 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();
}
}