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)