diff --git a/.gitignore b/.gitignore index bc78471..1699c0f 100644 --- a/.gitignore +++ b/.gitignore @@ -482,3 +482,105 @@ $RECYCLE.BIN/ # Vim temporary swap files *.swp + +## ---------------------------------------------------------------------- +## .NET / Visual Studio / Rider +## ---------------------------------------------------------------------- + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# IDEs & Tooling +.vs/ +.vscode/ +.idea/ +*.sln.iml +*.suo +*.user +*.userosscache +*.sln.docstates +.dockerignore +.env + +# Testing +[Tt]est[Rr]esult*/ +BenchmarkDotNet.Artifacts/ +coverage*.json +coverage*.xml +*.coverage +*.coveragexml + +## ---------------------------------------------------------------------- +## Python (Red Team Tools) +## ---------------------------------------------------------------------- + +# Byte-compiled / Optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Virtual Environments +.venv/ +venv/ +ENV/ +env/ + +# Distribution / Packaging +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +## ---------------------------------------------------------------------- +## OS Specific +## ---------------------------------------------------------------------- +.DS_Store +Thumbs.db + +# ---------------------------------------------------------------------- +# Generated gRPC Python Files (Do Not Commit) +# ---------------------------------------------------------------------- +attack/*_pb2.py +attack/*_pb2_grpc.py +attack/*.pyi \ No newline at end of file diff --git a/Rasp.Core.Tests/Rasp.Core.Tests.csproj b/Rasp.Core.Tests/Rasp.Core.Tests.csproj new file mode 100644 index 0000000..34acbcb --- /dev/null +++ b/Rasp.Core.Tests/Rasp.Core.Tests.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Rasp.Instrumentation.Grpc.Tests/Rasp.Instrumentation.Grpc.Tests.csproj b/Rasp.Instrumentation.Grpc.Tests/Rasp.Instrumentation.Grpc.Tests.csproj new file mode 100644 index 0000000..1976419 --- /dev/null +++ b/Rasp.Instrumentation.Grpc.Tests/Rasp.Instrumentation.Grpc.Tests.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Rasp.sln b/Rasp.sln index 0e552e8..dfe45ce 100644 --- a/Rasp.sln +++ b/Rasp.sln @@ -27,6 +27,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibrarySystem.Domain", "mod EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibrarySystem.Persistence", "modules\dotnet-grpc-library-api\LibrarySystem.Persistence\LibrarySystem.Persistence.csproj", "{689C3555-DC1F-4A87-B349-500DF85B9F39}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{7BF13981-E617-4FF6-9463-B251628BAF1E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rasp.Core.Tests", "Rasp.Core.Tests\Rasp.Core.Tests.csproj", "{9FF0212B-D43D-46D7-AAD4-3793958CA197}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rasp.Instrumentation.Grpc.Tests", "Rasp.Instrumentation.Grpc.Tests\Rasp.Instrumentation.Grpc.Tests.csproj", "{6F314CAD-E989-41CA-8C9C-9FD721F5DA50}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -145,6 +151,30 @@ Global {689C3555-DC1F-4A87-B349-500DF85B9F39}.Release|x64.Build.0 = Release|Any CPU {689C3555-DC1F-4A87-B349-500DF85B9F39}.Release|x86.ActiveCfg = Release|Any CPU {689C3555-DC1F-4A87-B349-500DF85B9F39}.Release|x86.Build.0 = Release|Any CPU + {9FF0212B-D43D-46D7-AAD4-3793958CA197}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9FF0212B-D43D-46D7-AAD4-3793958CA197}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9FF0212B-D43D-46D7-AAD4-3793958CA197}.Debug|x64.ActiveCfg = Debug|Any CPU + {9FF0212B-D43D-46D7-AAD4-3793958CA197}.Debug|x64.Build.0 = Debug|Any CPU + {9FF0212B-D43D-46D7-AAD4-3793958CA197}.Debug|x86.ActiveCfg = Debug|Any CPU + {9FF0212B-D43D-46D7-AAD4-3793958CA197}.Debug|x86.Build.0 = Debug|Any CPU + {9FF0212B-D43D-46D7-AAD4-3793958CA197}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9FF0212B-D43D-46D7-AAD4-3793958CA197}.Release|Any CPU.Build.0 = Release|Any CPU + {9FF0212B-D43D-46D7-AAD4-3793958CA197}.Release|x64.ActiveCfg = Release|Any CPU + {9FF0212B-D43D-46D7-AAD4-3793958CA197}.Release|x64.Build.0 = Release|Any CPU + {9FF0212B-D43D-46D7-AAD4-3793958CA197}.Release|x86.ActiveCfg = Release|Any CPU + {9FF0212B-D43D-46D7-AAD4-3793958CA197}.Release|x86.Build.0 = Release|Any CPU + {6F314CAD-E989-41CA-8C9C-9FD721F5DA50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F314CAD-E989-41CA-8C9C-9FD721F5DA50}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F314CAD-E989-41CA-8C9C-9FD721F5DA50}.Debug|x64.ActiveCfg = Debug|Any CPU + {6F314CAD-E989-41CA-8C9C-9FD721F5DA50}.Debug|x64.Build.0 = Debug|Any CPU + {6F314CAD-E989-41CA-8C9C-9FD721F5DA50}.Debug|x86.ActiveCfg = Debug|Any CPU + {6F314CAD-E989-41CA-8C9C-9FD721F5DA50}.Debug|x86.Build.0 = Debug|Any CPU + {6F314CAD-E989-41CA-8C9C-9FD721F5DA50}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F314CAD-E989-41CA-8C9C-9FD721F5DA50}.Release|Any CPU.Build.0 = Release|Any CPU + {6F314CAD-E989-41CA-8C9C-9FD721F5DA50}.Release|x64.ActiveCfg = Release|Any CPU + {6F314CAD-E989-41CA-8C9C-9FD721F5DA50}.Release|x64.Build.0 = Release|Any CPU + {6F314CAD-E989-41CA-8C9C-9FD721F5DA50}.Release|x86.ActiveCfg = Release|Any CPU + {6F314CAD-E989-41CA-8C9C-9FD721F5DA50}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -160,5 +190,7 @@ Global {D7B61D10-C142-47E1-99C6-31C582E330E9} = {87C876C9-7554-CEDF-203F-AF43B1B74702} {BDCCB396-349D-42AF-990E-769813A6AC13} = {87C876C9-7554-CEDF-203F-AF43B1B74702} {689C3555-DC1F-4A87-B349-500DF85B9F39} = {87C876C9-7554-CEDF-203F-AF43B1B74702} + {9FF0212B-D43D-46D7-AAD4-3793958CA197} = {7BF13981-E617-4FF6-9463-B251628BAF1E} + {6F314CAD-E989-41CA-8C9C-9FD721F5DA50} = {7BF13981-E617-4FF6-9463-B251628BAF1E} EndGlobalSection EndGlobal diff --git a/attack/exploit_grpc.py b/attack/exploit_grpc.py new file mode 100644 index 0000000..a7f5d28 --- /dev/null +++ b/attack/exploit_grpc.py @@ -0,0 +1,69 @@ +print("DEBUG: Script iniciando...") # <--- Adicione isso na linha 1 +import grpc +import sys +import os + +# Add current directory to path to import generated stubs +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +# Import generated modules (created by the build script) +# Note: Names depend on how protoc generates files. +# Assuming library.proto generates library_pb2.py and library_pb2_grpc.py +try: + import library_pb2 + import library_pb2_grpc +except ImportError: + print("❌ Error: Compiled proto files not found.") + print("Run: bash generate_protos.sh") + sys.exit(1) + +def run_exploit(target): + print(f"[*] Connecting to target: {target}") + + # Create insecure channel (for local PoC only) + with grpc.insecure_channel(target) as channel: + stub = library_pb2_grpc.LibraryStub(channel) + + # --- SCENARIO 1: SQL Injection Payload --- + # This payload matches the regex defined in RegexDetectionEngine + payload = "' OR '1'='1" + print(f"[*] Sending malicious payload: \"{payload}\"") + + try: + response = stub.CreateBook(library_pb2.CreateBookRequest( + title=payload, # <--- INJECTION POINT + author="Hacker", + publication_year=2025, + pages=100, + total_copies=10 + )) + print("❌ FAILURE: Server accepted the payload! RASP did not block.") + print(f" Response: ID={response.id}, Title={response.title}") + return False + + except grpc.RpcError as e: + # Check if blocked by RASP (PermissionDenied) + if e.code() == grpc.StatusCode.PERMISSION_DENIED: + print("✅ SUCCESS: RASP blocked the attack!") + print(f" Status: {e.code()}") + print(f" Details: {e.details()}") + return True + else: + print(f"⚠️ UNEXPECTED ERROR: Server returned an error, but not the expected block.") + print(f" Status: {e.code()}") + print(f" Details: {e.details()}") + return False + +if __name__ == '__main__': + # Default target: localhost:5001 (gRPC backend) + # Adjust if running via Docker or another port + target_url = 'localhost:5001' + if len(sys.argv) > 1: + target_url = sys.argv[1] + + success = run_exploit(target_url) + + if success: + sys.exit(0) + else: + sys.exit(1) \ No newline at end of file diff --git a/attack/generate_protos.sh b/attack/generate_protos.sh new file mode 100644 index 0000000..e69de29 diff --git a/attack/requirements.txt b/attack/requirements.txt new file mode 100644 index 0000000..2a8f38c --- /dev/null +++ b/attack/requirements.txt @@ -0,0 +1,3 @@ +grpcio==1.60.0 +grpcio-tools==1.60.0 +protobuf==4.25.1 \ No newline at end of file diff --git a/src/Rasp.Bootstrapper/Class1.cs b/src/Rasp.Bootstrapper/Class1.cs deleted file mode 100644 index 45fb368..0000000 --- a/src/Rasp.Bootstrapper/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Rasp.Bootstrapper; - -public class Class1 -{ - -} diff --git a/src/Rasp.Bootstrapper/Configuration/RaspOptions.cs b/src/Rasp.Bootstrapper/Configuration/RaspOptions.cs new file mode 100644 index 0000000..b78fa36 --- /dev/null +++ b/src/Rasp.Bootstrapper/Configuration/RaspOptions.cs @@ -0,0 +1,20 @@ +namespace Rasp.Bootstrapper.Configuration; + +/// +/// Global configuration options for the RASP. +/// Allows adjusting security behavior without recompilation. +/// +public class RaspOptions +{ + /// + /// If true, blocks detected attacks and throws an exception. + /// If false, only logs/monitors ("Audit" Mode). + /// Default: true (Secure by default). + /// + public bool BlockOnDetection { get; set; } = true; + + /// + /// If true, enables detailed telemetry (may have performance impact). + /// + public bool EnableMetrics { get; set; } = true; +} \ No newline at end of file diff --git a/src/Rasp.Bootstrapper/Rasp.Bootstrapper.csproj b/src/Rasp.Bootstrapper/Rasp.Bootstrapper.csproj index b760144..c00d00f 100644 --- a/src/Rasp.Bootstrapper/Rasp.Bootstrapper.csproj +++ b/src/Rasp.Bootstrapper/Rasp.Bootstrapper.csproj @@ -6,4 +6,14 @@ enable + + + + + + + + + + diff --git a/src/Rasp.Bootstrapper/RaspDependencyInjection.cs b/src/Rasp.Bootstrapper/RaspDependencyInjection.cs new file mode 100644 index 0000000..fd424c4 --- /dev/null +++ b/src/Rasp.Bootstrapper/RaspDependencyInjection.cs @@ -0,0 +1,41 @@ +using Grpc.AspNetCore.Server; +using Microsoft.Extensions.DependencyInjection; +using Rasp.Bootstrapper.Configuration; +using Rasp.Core; +using Rasp.Instrumentation.Grpc.Interceptors; + +namespace Rasp.Bootstrapper; + +/// +/// Provides extension methods to easily register RASP services. +/// +public static class RaspDependencyInjection +{ + /// + /// Adds the RASP (Runtime Application Self-Protection) services to the DI container. + /// This method registers detection engines, telemetry, and gRPC interceptors. + /// + /// The application service collection. + /// Optional delegate to configure RASP behavior. + /// The service collection for chaining. + public static IServiceCollection AddRasp( + this IServiceCollection services, + Action? configureOptions = null) + { + if (configureOptions != null) + { + services.Configure(configureOptions); + } + + services.AddRaspCore(); + + services.AddSingleton(); + + services.PostConfigure(options => + { + options.Interceptors.Add(); + }); + + return services; + } +} \ No newline at end of file diff --git a/src/Rasp.Core/Abstractions/IDetectionEngine.cs b/src/Rasp.Core/Abstractions/IDetectionEngine.cs new file mode 100644 index 0000000..a96f133 --- /dev/null +++ b/src/Rasp.Core/Abstractions/IDetectionEngine.cs @@ -0,0 +1,20 @@ +using Rasp.Core.Models; + +namespace Rasp.Core.Abstractions; + +/// +/// Defines the contract for the core detection logic. +/// Implementations of this interface are responsible for analyzing payloads +/// and determining if they contain malicious patterns (e.g., SQLi, XSS). +/// +public interface IDetectionEngine +{ + /// + /// Analyzes a generic text payload for potential threats. + /// This is the primary entry point for string-based inspections (gRPC fields, SQL queries). + /// + /// The content to inspect (e.g., user input, SQL command text). + /// Optional context about the source (e.g., "gRPC.BookService/CreateBook"). + /// A indicating the verdict. + DetectionResult Inspect(string? payload, string context = "Unknown"); +} \ No newline at end of file diff --git a/src/Rasp.Core/DependencyInjection.cs b/src/Rasp.Core/DependencyInjection.cs index 7d9bf29..c244a62 100644 --- a/src/Rasp.Core/DependencyInjection.cs +++ b/src/Rasp.Core/DependencyInjection.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Rasp.Core.Abstractions; +using Rasp.Core.Engine; using Rasp.Core.Telemetry; namespace Rasp.Core; @@ -14,7 +15,8 @@ public static class DependencyInjection public static IServiceCollection AddRaspCore(this IServiceCollection services) { services.TryAddSingleton(); - + services.TryAddSingleton(); + return services; } } \ No newline at end of file diff --git a/src/Rasp.Core/Engine/RegexDetectionEngine.cs b/src/Rasp.Core/Engine/RegexDetectionEngine.cs new file mode 100644 index 0000000..12bfa36 --- /dev/null +++ b/src/Rasp.Core/Engine/RegexDetectionEngine.cs @@ -0,0 +1,43 @@ +using System.Text.RegularExpressions; +using Rasp.Core.Abstractions; +using Rasp.Core.Enums; +using Rasp.Core.Models; + +namespace Rasp.Core.Engine; + +/// +/// A basic detection engine based on Regular Expressions. +/// NOTE: In a real-world scenario, this would be much more sophisticated. +/// For the PoC, we focus on blocking the most obvious SQLi patterns. +/// +public partial class RegexDetectionEngine : IDetectionEngine +{ + // Otimização: Source Generator para Regex (Zero-Allocation na inicialização) + // Bloqueia: ' OR '1'='1 (e variações simples) + [GeneratedRegex(@"(?i)'\s*OR\s*'\d+'\s*=\s*'\d+", RegexOptions.Compiled | RegexOptions.CultureInvariant)] + private static partial Regex BasicSqlInjectionPattern(); + + public DetectionResult Inspect(string? payload, string context = "Unknown") + { + if (string.IsNullOrEmpty(payload)) + { + return DetectionResult.Safe(); + } + + // 1. Check for Basic SQL Injection + if (BasicSqlInjectionPattern().IsMatch(payload)) + { + return DetectionResult.Threat( + threatType: "SQL Injection", + description: "Detected basic SQLi tautology pattern.", + severity: ThreatSeverity.High, + confidence: 1.0, + matchedPattern: "BasicSqlInjectionPattern" + ); + } + + // 2. Future: Add more patterns here (XSS, RCE) + + return DetectionResult.Safe(); + } +} \ No newline at end of file diff --git a/src/Rasp.Core/Enums/ThreatSeverity.cs b/src/Rasp.Core/Enums/ThreatSeverity.cs new file mode 100644 index 0000000..2ead483 --- /dev/null +++ b/src/Rasp.Core/Enums/ThreatSeverity.cs @@ -0,0 +1,32 @@ +namespace Rasp.Core.Enums; + +/// +/// Defines the severity levels for detected threats. +/// +public enum ThreatSeverity +{ + /// + /// Informational - suspicious but not necessarily malicious. + /// + Info = 0, + + /// + /// Low severity - minor security concern. + /// + Low = 1, + + /// + /// Medium severity - potential security issue. + /// + Medium = 2, + + /// + /// High severity - likely attack attempt. + /// + High = 3, + + /// + /// Critical severity - confirmed attack with high confidence. + /// + Critical = 4 +} \ No newline at end of file diff --git a/src/Rasp.Core/Models/DetectionResult.cs b/src/Rasp.Core/Models/DetectionResult.cs new file mode 100644 index 0000000..2961311 --- /dev/null +++ b/src/Rasp.Core/Models/DetectionResult.cs @@ -0,0 +1,65 @@ +using Rasp.Core.Enums; + +namespace Rasp.Core.Models; + +public sealed record DetectionResult +{ + // OTIMIZAÇÃO: Cache da instância "Safe" para evitar alocação no hot path (99% das requisições). + private static readonly DetectionResult _safeInstance = new() { IsThreat = false }; + + /// + /// Indicates whether a threat was detected. + /// + public required bool IsThreat { get; init; } + + /// + /// The type of threat detected (e.g., "SQL Injection", "XSS"). + /// Null if no threat was detected. + /// + public string? ThreatType { get; init; } + + /// + /// A detailed description of the detected threat. + /// + public string? Description { get; init; } + + /// + /// The confidence level of the detection (0.0 to 1.0). + /// + public double Confidence { get; init; } = 1.0; + + /// + /// The specific pattern or rule that triggered the detection. + /// + public string? MatchedPattern { get; init; } + + /// + /// The severity level of the threat. + /// + public ThreatSeverity Severity { get; init; } = ThreatSeverity.Medium; + + /// + /// Returns a cached result indicating no threat was detected. + /// ZERO ALLOCATION call. + /// + public static DetectionResult Safe() => _safeInstance; + + /// + /// Creates a result indicating a threat was detected. + /// Allocations here are acceptable as we are likely about to block the request anyway. + /// + public static DetectionResult Threat( + string threatType, + string description, + ThreatSeverity severity = ThreatSeverity.High, + double confidence = 1.0, + string? matchedPattern = null) => new() + { + IsThreat = true, + ThreatType = threatType, + Description = description, + Severity = severity, + Confidence = confidence, + MatchedPattern = matchedPattern + }; +} \ No newline at end of file diff --git a/src/Rasp.Instrumentation.Grpc/Class1.cs b/src/Rasp.Instrumentation.Grpc/Class1.cs deleted file mode 100644 index 51c2e64..0000000 --- a/src/Rasp.Instrumentation.Grpc/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Rasp.Instrumentation.Grpc; - -public class Class1 -{ - -} diff --git a/src/Rasp.Instrumentation.Grpc/Interceptors/SecurityInterceptor.cs b/src/Rasp.Instrumentation.Grpc/Interceptors/SecurityInterceptor.cs new file mode 100644 index 0000000..94af490 --- /dev/null +++ b/src/Rasp.Instrumentation.Grpc/Interceptors/SecurityInterceptor.cs @@ -0,0 +1,61 @@ +using System.Diagnostics; +using Grpc.Core; +using Grpc.Core.Interceptors; +using Rasp.Core.Abstractions; + +namespace Rasp.Instrumentation.Grpc.Interceptors; + +/// +/// The main RASP barrier for gRPC services. +/// Intercepts every unary call, inspects the payload, and decides whether to proceed. +/// +public class SecurityInterceptor : Interceptor +{ + private readonly IDetectionEngine _detectionEngine; + private readonly IRaspMetrics _metrics; + + public SecurityInterceptor(IDetectionEngine detectionEngine, IRaspMetrics metrics) + { + _detectionEngine = detectionEngine; + _metrics = metrics; + } + + public override async Task UnaryServerHandler( + TRequest request, + ServerCallContext context, + UnaryServerMethod continuation) + { + var sw = Stopwatch.StartNew(); + var method = context.Method; // e.g., "/Library.Library/GetBookById" + + try + { + // --- 1. INSPECTION PHASE --- + // For MVP: Convert request to string (Naive approach - Phase 2 optimization target) + // Warning: request.ToString() in Protobuf usually returns the JSON representation. + // This allocates memory! We will optimize this with Source Generators later. + string payload = request?.ToString() ?? string.Empty; + + var result = _detectionEngine.Inspect(payload, method); + + if (result.IsThreat) + { + // --- 2. BLOCKING PHASE --- + _metrics.ReportThreat("gRPC", result.ThreatType!, blocked: true); + + // Fail Fast with PermissionDenied (or InvalidArgument) + throw new RpcException(new Status( + StatusCode.PermissionDenied, + $"RASP Security Alert: {result.Description}")); + } + + // --- 3. EXECUTION PHASE --- + return await continuation(request, context); + } + finally + { + sw.Stop(); + _metrics.RecordInspection("gRPC", sw.Elapsed.TotalMilliseconds); + } + } +} \ No newline at end of file diff --git a/src/Rasp.Instrumentation.Grpc/Rasp.Instrumentation.Grpc.csproj b/src/Rasp.Instrumentation.Grpc/Rasp.Instrumentation.Grpc.csproj index b760144..f49a197 100644 --- a/src/Rasp.Instrumentation.Grpc/Rasp.Instrumentation.Grpc.csproj +++ b/src/Rasp.Instrumentation.Grpc/Rasp.Instrumentation.Grpc.csproj @@ -6,4 +6,12 @@ enable + + + + + + + +