From 512979f85d821b170ce6ffd61e6c4d5dbd5d5dae Mon Sep 17 00:00:00 2001 From: Adam Driscoll Date: Sat, 25 Jul 2026 08:51:20 -0500 Subject: [PATCH] Add language mode condition support --- .../Analyze/Conditions/LanguageModeTests.cs | 38 +++++++++++++++++++ PowerShellProtect/Analyze/Analyzer.cs | 1 + .../Analyze/Conditions/LanguageMode.cs | 17 +++++++++ .../Cmdlets/NewConditionCommand.cs | 2 +- PowerShellProtect/ScriptContext.cs | 7 ++++ README.md | 13 +++++++ 6 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 Engine.Tests/Analyze/Conditions/LanguageModeTests.cs create mode 100644 PowerShellProtect/Analyze/Conditions/LanguageMode.cs diff --git a/Engine.Tests/Analyze/Conditions/LanguageModeTests.cs b/Engine.Tests/Analyze/Conditions/LanguageModeTests.cs new file mode 100644 index 0000000..e876038 --- /dev/null +++ b/Engine.Tests/Analyze/Conditions/LanguageModeTests.cs @@ -0,0 +1,38 @@ +using Engine.Analyze.Conditions; +using Xunit; + +namespace Engine.Tests.Analyze.Conditions +{ + public class LanguageModeTests + { + [Fact] + public void ShouldMatchConstrainedLanguageMode() + { + var condition = new LanguageModeCondition(); + + var result = condition.Analyze(new ScriptContext { LanguageMode = "ConstrainedLanguage" }, new Configuration.Condition + { + Property = "languagemode", + Operator = "equals", + Value = "ConstrainedLanguage" + }); + + Assert.True(result); + } + + [Fact] + public void ShouldMatchAFullLanguageViolation() + { + var condition = new LanguageModeCondition(); + + var result = condition.Analyze(new ScriptContext { LanguageMode = "FullLanguage" }, new Configuration.Condition + { + Property = "languagemode", + Operator = "notequals", + Value = "ConstrainedLanguage" + }); + + Assert.True(result); + } + } +} diff --git a/PowerShellProtect/Analyze/Analyzer.cs b/PowerShellProtect/Analyze/Analyzer.cs index 40f1a18..3d9c463 100644 --- a/PowerShellProtect/Analyze/Analyzer.cs +++ b/PowerShellProtect/Analyze/Analyzer.cs @@ -42,6 +42,7 @@ public Analyzer() new DomainControllerCondition(), new ComputerNameCondition(), new DomainCondition(), + new LanguageModeCondition(), new CommandCondition(), new ScriptCondition(), new ContentPathCondition(), diff --git a/PowerShellProtect/Analyze/Conditions/LanguageMode.cs b/PowerShellProtect/Analyze/Conditions/LanguageMode.cs new file mode 100644 index 0000000..4f5068c --- /dev/null +++ b/PowerShellProtect/Analyze/Conditions/LanguageMode.cs @@ -0,0 +1,17 @@ +namespace Engine.Analyze.Conditions +{ + /// + /// Matches the language mode of the PowerShell runspace that AMSI is scanning. + /// + internal class LanguageModeCondition : StringCondition + { + public override string Name => "languagemode"; + + public override string Description => "Matches the language mode of the PowerShell runspace that submitted the script."; + + public override string GetValue(ScriptContext context) + { + return context.LanguageMode ?? string.Empty; + } + } +} diff --git a/PowerShellProtect/Cmdlets/NewConditionCommand.cs b/PowerShellProtect/Cmdlets/NewConditionCommand.cs index 1a1ae7a..5bbd745 100644 --- a/PowerShellProtect/Cmdlets/NewConditionCommand.cs +++ b/PowerShellProtect/Cmdlets/NewConditionCommand.cs @@ -10,7 +10,7 @@ namespace PowerShellProtect.Cmdlets public class NewConditionCommand : PSCmdlet { [Parameter(Mandatory = true)] - [ValidateSet("admin", "domaincontroller", "computername", "domain", "ApplicationName", "command", "script", "contentpath", "variable", "member", "string", "applicationHash", "assembly", "assemblyHash")] + [ValidateSet("admin", "domaincontroller", "computername", "domain", "languagemode", "ApplicationName", "command", "script", "contentpath", "variable", "member", "string", "applicationHash", "assembly", "assemblyHash")] public string Property { get; set; } [Parameter(Mandatory = true, ParameterSetName = "NotEquals")] diff --git a/PowerShellProtect/ScriptContext.cs b/PowerShellProtect/ScriptContext.cs index 9956c18..dafef33 100644 --- a/PowerShellProtect/ScriptContext.cs +++ b/PowerShellProtect/ScriptContext.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Management.Automation.Language; +using System.Management.Automation.Runspaces; namespace Engine { @@ -11,11 +12,17 @@ public class ScriptContext public string Script { get; set; } public string ContentName { get; set; } public string ApplicationName { get; set; } + /// + /// The language mode of the runspace that submitted the script to AMSI. + /// This is null when the script was not submitted from a PowerShell runspace. + /// + public string LanguageMode { get; set; } public Guid Id { get; set; } = Guid.NewGuid(); public static ConcurrentDictionary History { get; } = new ConcurrentDictionary(); public ScriptContext() { + LanguageMode = Runspace.DefaultRunspace?.SessionStateProxy?.LanguageMode.ToString(); History.TryAdd(DateTime.Now, this); } diff --git a/README.md b/README.md index 07ad330..ba5f606 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,19 @@ Install-Module PowerShellProtect Install-PowerShellProtect ``` +## Enforce Constrained Language Mode + +Use a `languagemode` condition with a blocking action to prevent scripts from running unless the submitting PowerShell runspace is in Constrained Language Mode: + +```powershell +$condition = New-PSPCondition -Property languagemode -NotEquals -Value ConstrainedLanguage +$action = New-PSPAction -Block +$rule = New-PSPRule -Name 'Require Constrained Language Mode' -Condition $condition -Action $action +$configuration = New-PSPConfiguration -Rule $rule -Action $action +``` + +This rule observes the mode of the runspace that AMSI is currently scanning. It is a guardrail, not a replacement for enforcing Constrained Language Mode with WDAC, AppLocker, or a locked-down session configuration: code running with Full Language capabilities can attempt to disable AMSI or change its own session state before a later scan. + ## Resources - [License](./LICENSE)