// ----------------------------------------------------------------------------- // NodeMethodKeyBuilder.cs // Sprint: SPRINT_3700_0002_0001_vuln_surfaces_core (SURF-012) // Description: Method key builder for Node.js/npm packages. // ----------------------------------------------------------------------------- using System.Text; using System.Text.RegularExpressions; namespace StellaOps.Scanner.VulnSurfaces.MethodKeys; /// /// Builds normalized method keys for JavaScript/Node.js modules. /// Format: module.path::functionName(param1,param2) or module.path.ClassName::methodName(params) /// public sealed partial class NodeMethodKeyBuilder : IMethodKeyBuilder { // Pattern: module.path[.ClassName]::methodName(params) [GeneratedRegex(@"^([^:]+)::([^(]+)\(([^)]*)\)$", RegexOptions.Compiled)] private static partial Regex MethodKeyPattern(); /// public string Ecosystem => "npm"; /// public string BuildKey(MethodKeyRequest request) { ArgumentNullException.ThrowIfNull(request); var sb = new StringBuilder(); // Module path if (!string.IsNullOrEmpty(request.Namespace)) { sb.Append(NormalizeModulePath(request.Namespace)); } // Class name (if any) if (!string.IsNullOrEmpty(request.TypeName)) { if (sb.Length > 0) { sb.Append('.'); } sb.Append(request.TypeName); } // ::functionName sb.Append("::"); sb.Append(request.MethodName); // (params) sb.Append('('); if (request.ParameterTypes is { Count: > 0 }) { sb.Append(string.Join(",", request.ParameterTypes)); } sb.Append(')'); return sb.ToString(); } /// public MethodKeyComponents? ParseKey(string methodKey) { if (string.IsNullOrEmpty(methodKey)) return null; var match = MethodKeyPattern().Match(methodKey); if (!match.Success) return null; var modulePath = match.Groups[1].Value; var methodName = match.Groups[2].Value; var parameters = match.Groups[3].Value; // Try to extract class name from module path string? typeName = null; var lastDot = modulePath.LastIndexOf('.'); if (lastDot > 0) { var lastPart = modulePath[(lastDot + 1)..]; // Check if it looks like a class name (starts with uppercase) if (char.IsUpper(lastPart[0])) { typeName = lastPart; modulePath = modulePath[..lastDot]; } } var paramTypes = string.IsNullOrEmpty(parameters) ? [] : parameters.Split(',').Select(p => p.Trim()).ToList(); return new MethodKeyComponents { Namespace = modulePath, TypeName = typeName, MethodName = methodName, ParameterTypes = paramTypes }; } /// public string NormalizeKey(string methodKey) { var components = ParseKey(methodKey); if (components is null) return methodKey; return BuildKey(new MethodKeyRequest { Namespace = components.Namespace, TypeName = components.TypeName, MethodName = components.MethodName, ParameterTypes = components.ParameterTypes?.ToList() }); } private static string NormalizeModulePath(string path) { // Normalize path separators and common patterns var normalized = path .Replace('/', '.') .Replace('\\', '.') .Replace("..", "."); // Remove leading/trailing dots normalized = normalized.Trim('.'); // Remove 'index' from module paths if (normalized.EndsWith(".index", StringComparison.OrdinalIgnoreCase)) { normalized = normalized[..^6]; } // Remove common prefixes like 'src.' or 'lib.' foreach (var prefix in new[] { "src.", "lib.", "dist." }) { if (normalized.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { normalized = normalized[prefix.Length..]; break; } } return normalized; } }