diff --git a/AppInspector.Tests/RuleProcessor/ReflectionTests.cs b/AppInspector.Tests/RuleProcessor/ReflectionTests.cs new file mode 100644 index 00000000..db259001 --- /dev/null +++ b/AppInspector.Tests/RuleProcessor/ReflectionTests.cs @@ -0,0 +1,264 @@ +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.ApplicationInspector.RulesEngine; +using Microsoft.CST.RecursiveExtractor; +using Xunit; + +namespace AppInspector.Tests.RuleProcessor; + +public class ReflectionTests +{ + private readonly Microsoft.ApplicationInspector.RulesEngine.Languages _languages = new(); + + [Fact] + public void DetectMethodInfoInvoke() + { + var testCode = @" +using System; +using System.Reflection; + +class Program +{ + static void Main() + { + Type type = typeof(MyClass); + MethodInfo method = type.GetMethod(""MyMethod""); + method.Invoke(obj, new object[] { }); + } +}"; + RuleSet rules = new(); + rules.AddDirectory("/home/runner/work/ApplicationInspector/ApplicationInspector/AppInspector/rules/default/os"); + var processor = new Microsoft.ApplicationInspector.RulesEngine.RuleProcessor(rules, new RuleProcessorOptions()); + + if (_languages.FromFileNameOut("test.cs", out var info)) + { + var matches = processor.AnalyzeFile(testCode, new FileEntry("test.cs", new MemoryStream()), info); + var methodInvokeMatches = matches.Where(m => m.Tags?.Contains("OS.Reflection.MethodInvocation") ?? false).ToList(); + Assert.NotEmpty(methodInvokeMatches); + } + else + { + Assert.Fail("Failed to get language info"); + } + } + + [Fact] + public void DetectConstructorInfoInvoke() + { + var testCode = @" +using System; +using System.Reflection; + +class Program +{ + static void Main() + { + Type type = typeof(MyClass); + ConstructorInfo constructor = type.GetConstructor(Type.EmptyTypes); + object instance = constructor.Invoke(null); + } +}"; + RuleSet rules = new(); + rules.AddDirectory("/home/runner/work/ApplicationInspector/ApplicationInspector/AppInspector/rules/default/os"); + var processor = new Microsoft.ApplicationInspector.RulesEngine.RuleProcessor(rules, new RuleProcessorOptions()); + + if (_languages.FromFileNameOut("test.cs", out var info)) + { + var matches = processor.AnalyzeFile(testCode, new FileEntry("test.cs", new MemoryStream()), info); + var constructorInvokeMatches = matches.Where(m => m.Tags?.Contains("OS.Reflection.ConstructorInvocation") ?? false).ToList(); + Assert.NotEmpty(constructorInvokeMatches); + } + else + { + Assert.Fail("Failed to get language info"); + } + } + + [Fact] + public void DetectAssemblyLoad() + { + var testCode = @" +using System; +using System.Reflection; + +class Program +{ + static void Main() + { + Assembly assembly1 = Assembly.Load(""MyAssembly""); + Assembly assembly2 = Assembly.LoadFrom(""path/to/assembly.dll""); + Assembly assembly3 = Assembly.LoadFile(""C:\\path\\to\\assembly.dll""); + } +}"; + RuleSet rules = new(); + rules.AddDirectory("/home/runner/work/ApplicationInspector/ApplicationInspector/AppInspector/rules/default/os"); + var processor = new Microsoft.ApplicationInspector.RulesEngine.RuleProcessor(rules, new RuleProcessorOptions()); + + if (_languages.FromFileNameOut("test.cs", out var info)) + { + var matches = processor.AnalyzeFile(testCode, new FileEntry("test.cs", new MemoryStream()), info); + var assemblyLoadMatches = matches.Where(m => m.Tags?.Contains("OS.Reflection.AssemblyLoading") ?? false).ToList(); + // Should detect Assembly.Load (LoadFrom and LoadFile are in load_dll.json) + Assert.NotEmpty(assemblyLoadMatches); + } + else + { + Assert.Fail("Failed to get language info"); + } + } + + [Fact] + public void DetectInvokeMember() + { + var testCode = @" +using System; +using System.Reflection; + +class Program +{ + static void Main() + { + Type type = typeof(MyClass); + object result = type.InvokeMember(""MyMethod"", + BindingFlags.InvokeMethod, null, obj, new object[] { }); + } +}"; + RuleSet rules = new(); + rules.AddDirectory("/home/runner/work/ApplicationInspector/ApplicationInspector/AppInspector/rules/default/os"); + var processor = new Microsoft.ApplicationInspector.RulesEngine.RuleProcessor(rules, new RuleProcessorOptions()); + + if (_languages.FromFileNameOut("test.cs", out var info)) + { + var matches = processor.AnalyzeFile(testCode, new FileEntry("test.cs", new MemoryStream()), info); + var invokeMemberMatches = matches.Where(m => m.Tags?.Contains("OS.Reflection.InvokeMember") ?? false).ToList(); + Assert.NotEmpty(invokeMemberMatches); + } + else + { + Assert.Fail("Failed to get language info"); + } + } + + [Fact] + public void DetectActivatorCreateInstance() + { + var testCode = @" +using System; + +class Program +{ + static void Main() + { + object instance1 = Activator.CreateInstance(typeof(MyClass)); + object instance2 = Activator.CreateInstance(""MyAssembly"", ""MyNamespace.MyClass""); + } +}"; + RuleSet rules = new(); + rules.AddDirectory("/home/runner/work/ApplicationInspector/ApplicationInspector/AppInspector/rules/default/os"); + var processor = new Microsoft.ApplicationInspector.RulesEngine.RuleProcessor(rules, new RuleProcessorOptions()); + + if (_languages.FromFileNameOut("test.cs", out var info)) + { + var matches = processor.AnalyzeFile(testCode, new FileEntry("test.cs", new MemoryStream()), info); + var createInstanceMatches = matches.Where(m => m.Tags?.Contains("OS.Reflection.CreateInstance") ?? false).ToList(); + Assert.NotEmpty(createInstanceMatches); + } + else + { + Assert.Fail("Failed to get language info"); + } + } + + [Fact] + public void DetectGetMethod() + { + var testCode = @" +using System; +using System.Reflection; + +class Program +{ + static void Main() + { + Type type = typeof(MyClass); + MethodInfo method = type.GetMethod(""MyMethod""); + } +}"; + RuleSet rules = new(); + rules.AddDirectory("/home/runner/work/ApplicationInspector/ApplicationInspector/AppInspector/rules/default/os"); + var processor = new Microsoft.ApplicationInspector.RulesEngine.RuleProcessor(rules, new RuleProcessorOptions()); + + if (_languages.FromFileNameOut("test.cs", out var info)) + { + var matches = processor.AnalyzeFile(testCode, new FileEntry("test.cs", new MemoryStream()), info); + var getMethodMatches = matches.Where(m => m.Tags?.Contains("OS.Reflection.GetMethod") ?? false).ToList(); + Assert.NotEmpty(getMethodMatches); + } + else + { + Assert.Fail("Failed to get language info"); + } + } + + [Fact] + public void DetectGetType() + { + var testCode = @" +using System; + +class Program +{ + static void Main() + { + Type type = Type.GetType(""System.String""); + } +}"; + RuleSet rules = new(); + rules.AddDirectory("/home/runner/work/ApplicationInspector/ApplicationInspector/AppInspector/rules/default/os"); + var processor = new Microsoft.ApplicationInspector.RulesEngine.RuleProcessor(rules, new RuleProcessorOptions()); + + if (_languages.FromFileNameOut("test.cs", out var info)) + { + var matches = processor.AnalyzeFile(testCode, new FileEntry("test.cs", new MemoryStream()), info); + var getTypeMatches = matches.Where(m => m.Tags?.Contains("OS.Reflection.GetType") ?? false).ToList(); + Assert.NotEmpty(getTypeMatches); + } + else + { + Assert.Fail("Failed to get language info"); + } + } + + [Fact] + public void NoFalsePositiveOnNonReflectionCode() + { + var testCode = @" +using System; + +class Program +{ + static void Main() + { + Console.WriteLine(""Hello World""); + var myClass = new MyClass(); + myClass.DoSomething(); + } +}"; + RuleSet rules = new(); + rules.AddDirectory("/home/runner/work/ApplicationInspector/ApplicationInspector/AppInspector/rules/default/os"); + var processor = new Microsoft.ApplicationInspector.RulesEngine.RuleProcessor(rules, new RuleProcessorOptions()); + + if (_languages.FromFileNameOut("test.cs", out var info)) + { + var matches = processor.AnalyzeFile(testCode, new FileEntry("test.cs", new MemoryStream()), info); + var reflectionMatches = matches.Where(m => + m.Tags?.Any(tag => tag.Contains("OS.Reflection")) ?? false).ToList(); + Assert.Empty(reflectionMatches); + } + else + { + Assert.Fail("Failed to get language info"); + } + } +} diff --git a/AppInspector/rules/default/os/reflection.json b/AppInspector/rules/default/os/reflection.json new file mode 100644 index 00000000..08bc5db6 --- /dev/null +++ b/AppInspector/rules/default/os/reflection.json @@ -0,0 +1,301 @@ +[ + { + "name": "OS: Reflection - Method Invocation", + "id": "AI036000", + "description": "Detects dynamic method invocation via reflection which can enable runtime behavior not visible at static analysis time", + "applies_to": [ + "csharp" + ], + "tags": [ + "OS.Reflection.MethodInvocation" + ], + "severity": "Moderate", + "patterns": [ + { + "pattern": "MethodInfo|MethodBase", + "type": "RegexWord", + "scopes": [ + "Code" + ], + "confidence": "Medium", + "_comment": "Detects MethodInfo and MethodBase usage for reflection" + } + ], + "conditions": [ + { + "pattern": { + "pattern": "\\.Invoke\\(", + "type": "Regex", + "scopes": [ + "Code" + ] + }, + "search_in": "same-line", + "_comment": "Ensures .Invoke( is on the same line to confirm method invocation" + } + ], + "must-match": [ + "MethodInfo method = type.GetMethod(\"Execute\"); method.Invoke(instance, null);", + "MethodBase baseMethod = typeof(MyClass).GetMethod(\"Test\"); var result = baseMethod.Invoke(obj, new object[] { });", + "MethodInfo mi = GetMethodInfo(); mi.Invoke(target, args);" + ], + "must-not-match": [ + "MethodInfo method = type.GetMethod(\"Execute\");", + "var info = typeof(String).GetMethod(\"ToUpper\");", + "public void MyMethod() { }", + "method.InvokeAsync(args);" + ] + }, + { + "name": "OS: Reflection - Constructor Invocation", + "id": "AI036100", + "description": "Detects dynamic object creation via reflection using ConstructorInfo", + "applies_to": [ + "csharp" + ], + "tags": [ + "OS.Reflection.ConstructorInvocation" + ], + "severity": "Moderate", + "patterns": [ + { + "pattern": "ConstructorInfo", + "type": "RegexWord", + "scopes": [ + "Code" + ], + "confidence": "Medium", + "_comment": "Detects ConstructorInfo usage for reflection" + } + ], + "conditions": [ + { + "pattern": { + "pattern": "\\.Invoke\\(", + "type": "Regex", + "scopes": [ + "Code" + ] + }, + "search_in": "same-line", + "_comment": "Ensures .Invoke( is on the same line to confirm constructor invocation" + } + ], + "must-match": [ + "ConstructorInfo ctor = type.GetConstructor(Type.EmptyTypes); var instance = ctor.Invoke(null);", + "var constructor = typeof(MyClass).GetConstructor(new[] { typeof(string) }); object obj = constructor.Invoke(new object[] { \"test\" });", + "ConstructorInfo ci = t.GetConstructor(paramTypes); ci.Invoke(parameters);" + ], + "must-not-match": [ + "ConstructorInfo ctor = type.GetConstructor(Type.EmptyTypes);", + "var info = typeof(MyClass).GetConstructors();", + "public MyClass() { }", + "new MyClass();" + ] + }, + { + "name": "OS: Reflection - Dynamic Assembly Loading", + "id": "AI036200", + "description": "Detects dynamic assembly loading at runtime using Assembly.Load which can introduce code not visible at static analysis time", + "applies_to": [ + "csharp" + ], + "tags": [ + "OS.Reflection.AssemblyLoading" + ], + "severity": "Moderate", + "patterns": [ + { + "pattern": "Assembly\\.Load\\(", + "type": "Regex", + "scopes": [ + "Code" + ], + "confidence": "High", + "_comment": "Detects Assembly.Load( - note LoadFile and LoadFrom are already in load_dll.json" + } + ], + "must-match": [ + "Assembly assembly = Assembly.Load(\"MyAssembly\");", + "var asm = Assembly.Load(assemblyName);", + "Assembly.Load(new AssemblyName(\"Plugin.Core\"));" + ], + "must-not-match": [ + "Assembly assembly = Assembly.GetExecutingAssembly();", + "var asm = Assembly.GetCallingAssembly();", + "Assembly.LoadFrom(\"path/to/assembly.dll\");", + "Assembly.LoadFile(@\"C:\\assemblies\\mylib.dll\");" + ] + }, + { + "name": "OS: Reflection - InvokeMember", + "id": "AI036300", + "description": "Detects Type.InvokeMember which enables dynamic member access via reflection", + "applies_to": [ + "csharp" + ], + "tags": [ + "OS.Reflection.InvokeMember" + ], + "severity": "Moderate", + "patterns": [ + { + "pattern": "\\.InvokeMember\\(", + "type": "Regex", + "scopes": [ + "Code" + ], + "confidence": "High", + "_comment": "Detects .InvokeMember( for dynamic member access" + } + ], + "must-match": [ + "type.InvokeMember(\"MyMethod\", BindingFlags.InvokeMethod, null, obj, args);", + "var result = typeof(MyClass).InvokeMember(\"Execute\", BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod, null, instance, null);", + "t.InvokeMember(methodName, flags, binder, target, parameters);" + ], + "must-not-match": [ + "var members = type.GetMembers();", + "type.GetMethod(\"MyMethod\");", + "obj.MyMethod();", + "InvokeMemberAsync(name, args);" + ] + }, + { + "name": "OS: Reflection - Activator.CreateInstance", + "id": "AI036400", + "description": "Detects dynamic object creation using Activator.CreateInstance", + "applies_to": [ + "csharp" + ], + "tags": [ + "OS.Reflection.CreateInstance" + ], + "severity": "Moderate", + "patterns": [ + { + "pattern": "Activator\\.CreateInstance", + "type": "RegexWord", + "scopes": [ + "Code" + ], + "confidence": "High", + "_comment": "Detects Activator.CreateInstance for dynamic object instantiation" + } + ], + "must-match": [ + "object instance = Activator.CreateInstance(typeof(MyClass));", + "var obj = Activator.CreateInstance(\"MyAssembly\", \"MyNamespace.MyClass\");", + "T instance = (T)Activator.CreateInstance(type);", + "Activator.CreateInstance();" + ], + "must-not-match": [ + "new MyClass();", + "var instance = new MyClass();", + "MyClass.Create();", + "CreateInstance();" + ] + }, + { + "name": "OS: Reflection - GetMethod", + "id": "AI036500", + "description": "Detects retrieval of method information via reflection", + "applies_to": [ + "csharp" + ], + "tags": [ + "OS.Reflection.GetMethod" + ], + "severity": "Moderate", + "patterns": [ + { + "pattern": "\\.GetMethod\\(", + "type": "Regex", + "scopes": [ + "Code" + ], + "confidence": "Medium", + "_comment": "Detects .GetMethod( which is often used before dynamic invocation" + } + ], + "must-match": [ + "MethodInfo method = type.GetMethod(\"Execute\");", + "var methodInfo = typeof(MyClass).GetMethod(\"Test\", BindingFlags.Public | BindingFlags.Instance);", + "Type t = typeof(string); t.GetMethod(\"ToUpper\");" + ], + "must-not-match": [ + "var methods = type.GetMethods();", + "public void MyMethod() { }", + "GetMethodAsync(name);", + "CustomGetMethod();" + ] + }, + { + "name": "OS: Reflection - GetConstructor", + "id": "AI036600", + "description": "Detects retrieval of constructor information via reflection", + "applies_to": [ + "csharp" + ], + "tags": [ + "OS.Reflection.GetConstructor" + ], + "severity": "Moderate", + "patterns": [ + { + "pattern": "\\.GetConstructor\\(", + "type": "Regex", + "scopes": [ + "Code" + ], + "confidence": "Medium", + "_comment": "Detects .GetConstructor( which is often used before dynamic object creation" + } + ], + "must-match": [ + "ConstructorInfo ctor = type.GetConstructor(Type.EmptyTypes);", + "var constructor = typeof(MyClass).GetConstructor(new[] { typeof(string), typeof(int) });", + "Type t = GetType(); t.GetConstructor(parameterTypes);" + ], + "must-not-match": [ + "var constructors = type.GetConstructors();", + "public MyClass() { }", + "GetConstructorAsync();", + "CustomGetConstructor();" + ] + }, + { + "name": "OS: Reflection - Dynamic Type Loading", + "id": "AI036700", + "description": "Detects dynamic type loading using Type.GetType", + "applies_to": [ + "csharp" + ], + "tags": [ + "OS.Reflection.GetType" + ], + "severity": "Moderate", + "patterns": [ + { + "pattern": "Type\\.GetType\\(", + "type": "Regex", + "scopes": [ + "Code" + ], + "confidence": "Medium", + "_comment": "Detects Type.GetType( for dynamic type loading from string" + } + ], + "must-match": [ + "Type type = Type.GetType(\"System.String\");", + "var t = Type.GetType(\"MyNamespace.MyClass, MyAssembly\");", + "Type.GetType(typeName, true);" + ], + "must-not-match": [ + "var type = typeof(MyClass);", + "Type t = obj.GetType();", + "GetType();", + "myObject.GetType();" + ] + } +] \ No newline at end of file