Files
git.stella-ops.org/src/Scanner/__Libraries/StellaOps.Scanner.Contracts/SinkRegistry.cs
2026-01-12 12:24:17 +02:00

144 lines
7.6 KiB
C#

// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) StellaOps
// Registry of known dangerous sinks per language.
namespace StellaOps.Scanner.Contracts;
using System.Collections.Frozen;
using System.Collections.Immutable;
/// <summary>
/// Registry of known dangerous sinks per language.
/// </summary>
public static class SinkRegistry
{
private static readonly FrozenDictionary<string, ImmutableArray<SinkDefinition>> SinksByLanguage = BuildRegistry();
private static FrozenDictionary<string, ImmutableArray<SinkDefinition>> BuildRegistry()
{
var builder = new Dictionary<string, List<SinkDefinition>>(StringComparer.Ordinal);
// .NET sinks
AddSink(builder, "dotnet", SinkCategory.CmdExec, "System.Diagnostics.Process.Start", cweId: "CWE-78");
AddSink(builder, "dotnet", SinkCategory.CmdExec, "System.Diagnostics.ProcessStartInfo", cweId: "CWE-78");
AddSink(builder, "dotnet", SinkCategory.UnsafeDeser, "System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize", cweId: "CWE-502");
AddSink(builder, "dotnet", SinkCategory.UnsafeDeser, "Newtonsoft.Json.JsonConvert.DeserializeObject", cweId: "CWE-502", framework: "Newtonsoft.Json");
AddSink(builder, "dotnet", SinkCategory.SqlRaw, "System.Data.SqlClient.SqlCommand.ExecuteReader", cweId: "CWE-89");
AddSink(builder, "dotnet", SinkCategory.SqlRaw, "Microsoft.EntityFrameworkCore.RelationalQueryableExtensions.FromSqlRaw", cweId: "CWE-89", framework: "EFCore");
AddSink(builder, "dotnet", SinkCategory.Ssrf, "System.Net.Http.HttpClient.GetAsync", cweId: "CWE-918");
AddSink(builder, "dotnet", SinkCategory.FileWrite, "System.IO.File.WriteAllBytes", cweId: "CWE-73");
AddSink(builder, "dotnet", SinkCategory.PathTraversal, "System.IO.Path.Combine", cweId: "CWE-22");
AddSink(builder, "dotnet", SinkCategory.CryptoWeak, "System.Security.Cryptography.MD5.Create", cweId: "CWE-327");
AddSink(builder, "dotnet", SinkCategory.CryptoWeak, "System.Security.Cryptography.DES.Create", cweId: "CWE-327");
// Java sinks
AddSink(builder, "java", SinkCategory.CmdExec, "java.lang.Runtime.exec", cweId: "CWE-78");
AddSink(builder, "java", SinkCategory.CmdExec, "java.lang.ProcessBuilder.start", cweId: "CWE-78");
AddSink(builder, "java", SinkCategory.UnsafeDeser, "java.io.ObjectInputStream.readObject", cweId: "CWE-502");
AddSink(builder, "java", SinkCategory.SqlRaw, "java.sql.Statement.executeQuery", cweId: "CWE-89");
AddSink(builder, "java", SinkCategory.Ssrf, "java.net.URL.openConnection", cweId: "CWE-918");
AddSink(builder, "java", SinkCategory.TemplateInjection, "org.springframework.expression.ExpressionParser.parseExpression", cweId: "CWE-917", framework: "Spring");
// Node.js sinks
AddSink(builder, "node", SinkCategory.CmdExec, "child_process.exec", cweId: "CWE-78");
AddSink(builder, "node", SinkCategory.CmdExec, "child_process.spawn", cweId: "CWE-78");
AddSink(builder, "node", SinkCategory.UnsafeDeser, "node-serialize.unserialize", cweId: "CWE-502");
AddSink(builder, "node", SinkCategory.SqlRaw, "mysql.query", cweId: "CWE-89");
AddSink(builder, "node", SinkCategory.PathTraversal, "path.join", cweId: "CWE-22");
AddSink(builder, "node", SinkCategory.TemplateInjection, "eval", cweId: "CWE-94");
// Python sinks
AddSink(builder, "python", SinkCategory.CmdExec, "os.system", cweId: "CWE-78");
AddSink(builder, "python", SinkCategory.CmdExec, "subprocess.call", cweId: "CWE-78");
AddSink(builder, "python", SinkCategory.UnsafeDeser, "pickle.loads", cweId: "CWE-502");
AddSink(builder, "python", SinkCategory.UnsafeDeser, "yaml.load", cweId: "CWE-502");
AddSink(builder, "python", SinkCategory.SqlRaw, "sqlite3.Cursor.execute", cweId: "CWE-89");
AddSink(builder, "python", SinkCategory.TemplateInjection, "jinja2.Template.render", cweId: "CWE-1336", framework: "Jinja2");
// Go sinks
AddSink(builder, "go", SinkCategory.CmdExec, "os/exec.Command", cweId: "CWE-78");
AddSink(builder, "go", SinkCategory.CmdExec, "os/exec.CommandContext", cweId: "CWE-78");
AddSink(builder, "go", SinkCategory.SqlRaw, "database/sql.DB.Query", cweId: "CWE-89");
AddSink(builder, "go", SinkCategory.SqlRaw, "database/sql.DB.Exec", cweId: "CWE-89");
AddSink(builder, "go", SinkCategory.Ssrf, "net/http.Get", cweId: "CWE-918");
AddSink(builder, "go", SinkCategory.PathTraversal, "filepath.Join", cweId: "CWE-22");
// Ruby sinks
AddSink(builder, "ruby", SinkCategory.CmdExec, "Kernel.system", cweId: "CWE-78");
AddSink(builder, "ruby", SinkCategory.CmdExec, "Kernel.exec", cweId: "CWE-78");
AddSink(builder, "ruby", SinkCategory.UnsafeDeser, "Marshal.load", cweId: "CWE-502");
AddSink(builder, "ruby", SinkCategory.UnsafeDeser, "YAML.load", cweId: "CWE-502");
AddSink(builder, "ruby", SinkCategory.SqlRaw, "ActiveRecord::Base.connection.execute", cweId: "CWE-89", framework: "Rails");
AddSink(builder, "ruby", SinkCategory.TemplateInjection, "ERB.new", cweId: "CWE-1336");
// PHP sinks
AddSink(builder, "php", SinkCategory.CmdExec, "exec", cweId: "CWE-78");
AddSink(builder, "php", SinkCategory.CmdExec, "shell_exec", cweId: "CWE-78");
AddSink(builder, "php", SinkCategory.CmdExec, "system", cweId: "CWE-78");
AddSink(builder, "php", SinkCategory.UnsafeDeser, "unserialize", cweId: "CWE-502");
AddSink(builder, "php", SinkCategory.SqlRaw, "mysqli_query", cweId: "CWE-89");
AddSink(builder, "php", SinkCategory.SqlRaw, "PDO::query", cweId: "CWE-89");
AddSink(builder, "php", SinkCategory.FileWrite, "file_put_contents", cweId: "CWE-73");
AddSink(builder, "php", SinkCategory.CodeInjection, "eval", cweId: "CWE-94");
return builder.ToFrozenDictionary(
kvp => kvp.Key,
kvp => kvp.Value.ToImmutableArray(),
StringComparer.Ordinal);
}
private static void AddSink(
Dictionary<string, List<SinkDefinition>> builder,
string language,
SinkCategory category,
string symbolPattern,
string? cweId = null,
string? framework = null)
{
if (!builder.TryGetValue(language, out var list))
{
list = [];
builder[language] = list;
}
list.Add(new SinkDefinition(
Category: category,
SymbolPattern: symbolPattern,
Language: language,
Framework: framework,
CweId: cweId));
}
/// <summary>
/// Gets all sink definitions for a language.
/// </summary>
public static ImmutableArray<SinkDefinition> GetSinksForLanguage(string language)
{
if (string.IsNullOrWhiteSpace(language))
{
return ImmutableArray<SinkDefinition>.Empty;
}
return SinksByLanguage.GetValueOrDefault(language.Trim().ToLowerInvariant(), ImmutableArray<SinkDefinition>.Empty);
}
/// <summary>
/// Gets all registered languages.
/// </summary>
public static IEnumerable<string> GetRegisteredLanguages() => SinksByLanguage.Keys;
/// <summary>
/// Checks if a symbol matches any known sink.
/// </summary>
public static SinkDefinition? MatchSink(string language, string symbol)
{
if (string.IsNullOrWhiteSpace(language) || string.IsNullOrWhiteSpace(symbol))
{
return null;
}
var sinks = GetSinksForLanguage(language);
return sinks.FirstOrDefault(sink => symbol.Contains(sink.SymbolPattern, StringComparison.OrdinalIgnoreCase));
}
}