Files
ft/Client/Packages/io.github.hatayama.uloopmcp/Editor/Compilation/SourceSecurityScanner.cs

132 lines
6.0 KiB
C#

using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace io.github.hatayama.uLoopMCP
{
/// <summary>
/// Pre-compilation source-level security scanner.
/// Blocks dangerous API patterns before code reaches the compiler,
/// preventing actually harmful code from executing (e.g. Process.Kill crashing Unity).
/// </summary>
internal static class SourceSecurityScanner
{
private static readonly Regex StripCommentsAndStringsRegex = new Regex(
@"
//[^\n]* | # line comment
/\*[\s\S]*?\*/ | # block comment
@""(?:""""|[^""])*"" | # verbatim string
""(?:\\.|[^\\""\n])*"" | # regular string
'(?:\\.|[^\\'\n])*' # char literal
",
RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
private static readonly List<DangerousPattern> Patterns = new()
{
// Process manipulation
new("Process", "Start", @"\bProcess\s*\.\s*Start\b"),
new("Process", "Kill", @"\b(?:global::\s*)?(?:System\s*\.\s*Diagnostics\s*\.\s*)?Process\s*\.\s*Kill\b"),
// File system destructive operations
new("File", "Delete", @"\bFile\s*\.\s*Delete\b"),
new("File", "WriteAllText", @"\bFile\s*\.\s*WriteAllText\b"),
new("File", "WriteAllBytes", @"\bFile\s*\.\s*WriteAllBytes\b"),
new("File", "Replace", @"\bFile\s*\.\s*Replace\b"),
new("File", "OpenWrite", @"\bFile\s*\.\s*OpenWrite\b"),
new("File", "AppendAllText", @"\bFile\s*\.\s*AppendAllText\b"),
new("Directory", "Delete", @"\bDirectory\s*\.\s*Delete\b"),
// Network
new("HttpClient", "new", @"\bnew\s+HttpClient\b"),
new("HttpClient", "usage", @"\bHttpClient\s*\("),
new("WebClient", "new", @"\bnew\s+WebClient\b"),
new("WebRequest", "Create", @"\bWebRequest\s*\.\s*Create\b"),
new("Socket", "new", @"\bnew\s+Socket\b"),
new("TcpClient", "new", @"\bnew\s+TcpClient\b|\bTcpClient\s*\("),
new("UdpClient", "new", @"\bnew\s+UdpClient\b|\bUdpClient\s*\("),
new("ClientWebSocket", "new",
@"\bnew\s+(?:global::\s*)?(?:System\s*\.\s*Net\s*\.\s*WebSockets\s*\.\s*)?ClientWebSocket\b|\bClientWebSocket\s*\("),
new("Dns", "GetHostEntry", @"\bDns\s*\.\s*GetHostEntry\b"),
new("UnityWebRequest", "new", @"\bnew\s+UnityWebRequest\b"),
// Reflection execution
new("Assembly", "Load", @"\bAssembly\s*\.\s*Load\b"),
new("Assembly", "LoadFrom", @"\bAssembly\s*\.\s*LoadFrom\b"),
new("Assembly", "GetType", @"\b(?:global::\s*)?(?:System\s*\.\s*Reflection\s*\.\s*)?Assembly\s*\.\s*GetType\s*\("),
new("Type", "GetType", @"\b(?:global::\s*)?(?:System\s*\.\s*)?Type\s*\.\s*GetType\s*\("),
new("Activator", "CreateComInstanceFrom", @"\bActivator\s*\.\s*CreateComInstanceFrom\b"),
// Environment
new("Environment", "Exit", @"\bEnvironment\s*\.\s*Exit\b"),
new("Environment", "FailFast", @"\bEnvironment\s*\.\s*FailFast\b"),
// GC manipulation
new("GCSettings", "LatencyMode", @"\bGCSettings\s*\.\s*LatencyMode\s*="),
// Thread manipulation
new("Thread", "Abort", @"\bThread\s*\.\s*Abort\s*\("),
// Asset deletion
new("AssetDatabase", "DeleteAsset", @"\bAssetDatabase\s*\.\s*DeleteAsset\b"),
new("FileUtil", "DeleteFileOrDirectory", @"\bFileUtil\s*\.\s*DeleteFileOrDirectory\b"),
// Unsafe code
new("unsafe", "block", @"\bunsafe\s*\{|\bunsafe\s+\w"),
new("DllImport", "attribute", @"\bDllImport\b"),
new("extern", "declaration", @"\bextern\s+\w"),
new("ModuleInitializer", "attribute", @"\bModuleInitializer\b"),
};
public static SecurityValidationResult Scan(string sourceCode)
{
SecurityValidationResult result = new SecurityValidationResult
{
IsValid = true,
Violations = new List<SecurityViolation>()
};
if (string.IsNullOrWhiteSpace(sourceCode)) return result;
// Strip comments and string literals to avoid false positives
string stripped = StripCommentsAndStrings(sourceCode);
foreach (DangerousPattern pattern in Patterns)
{
if (pattern.Regex.IsMatch(stripped))
{
result.Violations.Add(new SecurityViolation
{
Type = SecurityViolationType.DangerousApiCall,
ApiName = $"{pattern.TypeName}.{pattern.MemberName}",
Message = $"Dangerous API detected: {pattern.TypeName}.{pattern.MemberName}",
Description = $"Source contains blocked pattern: {pattern.TypeName}.{pattern.MemberName}"
});
}
}
result.IsValid = result.Violations.Count == 0;
return result;
}
private static string StripCommentsAndStrings(string source)
{
// Replace string literals and comments with spaces to avoid false positives
// while preserving line structure for position tracking
return StripCommentsAndStringsRegex.Replace(source, match => new string(' ', match.Length));
}
private sealed class DangerousPattern
{
public string TypeName { get; }
public string MemberName { get; }
public Regex Regex { get; }
public DangerousPattern(string typeName, string memberName, string regexPattern)
{
TypeName = typeName;
MemberName = memberName;
Regex = new Regex(regexPattern, RegexOptions.Singleline | RegexOptions.Compiled);
}
}
}
}