- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations). - Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns. - Added `package-lock.json` for dependency management.
286 lines
8.8 KiB
C#
286 lines
8.8 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using Microsoft.Extensions.Options;
|
|
using StellaOps.Router.Common.Abstractions;
|
|
using StellaOps.Router.Common.Models;
|
|
|
|
namespace StellaOps.Router.Gateway.OpenApi;
|
|
|
|
/// <summary>
|
|
/// Generates OpenAPI 3.1.0 documents from aggregated microservice schemas.
|
|
/// </summary>
|
|
public sealed class OpenApiDocumentGenerator : IOpenApiDocumentGenerator
|
|
{
|
|
private readonly IGlobalRoutingState _routingState;
|
|
private readonly OpenApiAggregationOptions _options;
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
WriteIndented = true
|
|
};
|
|
|
|
public OpenApiDocumentGenerator(
|
|
IGlobalRoutingState routingState,
|
|
IOptions<OpenApiAggregationOptions> options)
|
|
{
|
|
_routingState = routingState;
|
|
_options = options.Value;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public string GenerateDocument()
|
|
{
|
|
var connections = _routingState.GetAllConnections();
|
|
var doc = new JsonObject
|
|
{
|
|
["openapi"] = "3.1.0",
|
|
["info"] = GenerateInfo(),
|
|
["servers"] = GenerateServers(),
|
|
["paths"] = GeneratePaths(connections),
|
|
["components"] = GenerateComponents(connections),
|
|
["tags"] = GenerateTags(connections)
|
|
};
|
|
|
|
return doc.ToJsonString(JsonOptions);
|
|
}
|
|
|
|
private JsonObject GenerateInfo()
|
|
{
|
|
var info = new JsonObject
|
|
{
|
|
["title"] = _options.Title,
|
|
["version"] = _options.Version,
|
|
["description"] = _options.Description,
|
|
["license"] = new JsonObject
|
|
{
|
|
["name"] = _options.LicenseName
|
|
}
|
|
};
|
|
|
|
if (_options.ContactName is not null || _options.ContactEmail is not null)
|
|
{
|
|
var contact = new JsonObject();
|
|
if (_options.ContactName is not null)
|
|
contact["name"] = _options.ContactName;
|
|
if (_options.ContactEmail is not null)
|
|
contact["email"] = _options.ContactEmail;
|
|
info["contact"] = contact;
|
|
}
|
|
|
|
return info;
|
|
}
|
|
|
|
private JsonArray GenerateServers()
|
|
{
|
|
return new JsonArray
|
|
{
|
|
new JsonObject
|
|
{
|
|
["url"] = _options.ServerUrl
|
|
}
|
|
};
|
|
}
|
|
|
|
private JsonObject GeneratePaths(IReadOnlyList<ConnectionState> connections)
|
|
{
|
|
var paths = new JsonObject();
|
|
|
|
// Group endpoints by path
|
|
var pathGroups = new Dictionary<string, List<(ConnectionState Conn, EndpointDescriptor Endpoint)>>();
|
|
|
|
foreach (var conn in connections)
|
|
{
|
|
foreach (var endpoint in conn.Endpoints.Values)
|
|
{
|
|
if (!pathGroups.TryGetValue(endpoint.Path, out var list))
|
|
{
|
|
list = [];
|
|
pathGroups[endpoint.Path] = list;
|
|
}
|
|
list.Add((conn, endpoint));
|
|
}
|
|
}
|
|
|
|
// Generate path items
|
|
foreach (var (path, endpoints) in pathGroups.OrderBy(p => p.Key))
|
|
{
|
|
var pathItem = new JsonObject();
|
|
|
|
foreach (var (conn, endpoint) in endpoints)
|
|
{
|
|
var operation = GenerateOperation(conn, endpoint);
|
|
var method = endpoint.Method.ToLowerInvariant();
|
|
pathItem[method] = operation;
|
|
}
|
|
|
|
paths[path] = pathItem;
|
|
}
|
|
|
|
return paths;
|
|
}
|
|
|
|
private JsonObject GenerateOperation(ConnectionState conn, EndpointDescriptor endpoint)
|
|
{
|
|
var operation = new JsonObject
|
|
{
|
|
["operationId"] = $"{conn.Instance.ServiceName}_{endpoint.Path.Replace("/", "_").Trim('_')}_{endpoint.Method}",
|
|
["tags"] = new JsonArray { conn.Instance.ServiceName }
|
|
};
|
|
|
|
// Add documentation from SchemaInfo
|
|
if (endpoint.SchemaInfo is not null)
|
|
{
|
|
if (endpoint.SchemaInfo.Summary is not null)
|
|
operation["summary"] = endpoint.SchemaInfo.Summary;
|
|
if (endpoint.SchemaInfo.Description is not null)
|
|
operation["description"] = endpoint.SchemaInfo.Description;
|
|
if (endpoint.SchemaInfo.Deprecated)
|
|
operation["deprecated"] = true;
|
|
|
|
// Override tags if specified
|
|
if (endpoint.SchemaInfo.Tags.Count > 0)
|
|
{
|
|
var tags = new JsonArray();
|
|
foreach (var tag in endpoint.SchemaInfo.Tags)
|
|
{
|
|
tags.Add(tag);
|
|
}
|
|
operation["tags"] = tags;
|
|
}
|
|
}
|
|
|
|
// Add security requirements
|
|
var security = ClaimSecurityMapper.GenerateSecurityRequirement(endpoint);
|
|
if (security.Count > 0)
|
|
{
|
|
operation["security"] = security;
|
|
}
|
|
|
|
// Add request body if schema exists
|
|
if (endpoint.SchemaInfo?.RequestSchemaId is not null)
|
|
{
|
|
var schemaRef = $"#/components/schemas/{conn.Instance.ServiceName}_{endpoint.SchemaInfo.RequestSchemaId}";
|
|
operation["requestBody"] = new JsonObject
|
|
{
|
|
["required"] = true,
|
|
["content"] = new JsonObject
|
|
{
|
|
["application/json"] = new JsonObject
|
|
{
|
|
["schema"] = new JsonObject
|
|
{
|
|
["$ref"] = schemaRef
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// Add responses
|
|
var responses = new JsonObject();
|
|
|
|
// Success response
|
|
var successResponse = new JsonObject
|
|
{
|
|
["description"] = "Success"
|
|
};
|
|
|
|
if (endpoint.SchemaInfo?.ResponseSchemaId is not null)
|
|
{
|
|
var schemaRef = $"#/components/schemas/{conn.Instance.ServiceName}_{endpoint.SchemaInfo.ResponseSchemaId}";
|
|
successResponse["content"] = new JsonObject
|
|
{
|
|
["application/json"] = new JsonObject
|
|
{
|
|
["schema"] = new JsonObject
|
|
{
|
|
["$ref"] = schemaRef
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
responses["200"] = successResponse;
|
|
|
|
// Error responses
|
|
responses["400"] = new JsonObject { ["description"] = "Bad Request" };
|
|
responses["401"] = new JsonObject { ["description"] = "Unauthorized" };
|
|
responses["404"] = new JsonObject { ["description"] = "Not Found" };
|
|
responses["422"] = new JsonObject { ["description"] = "Validation Error" };
|
|
responses["500"] = new JsonObject { ["description"] = "Internal Server Error" };
|
|
|
|
operation["responses"] = responses;
|
|
|
|
return operation;
|
|
}
|
|
|
|
private JsonObject GenerateComponents(IReadOnlyList<ConnectionState> connections)
|
|
{
|
|
var components = new JsonObject();
|
|
|
|
// Generate schemas with service prefix
|
|
var schemas = new JsonObject();
|
|
foreach (var conn in connections)
|
|
{
|
|
foreach (var (schemaId, schemaDef) in conn.Schemas)
|
|
{
|
|
var prefixedId = $"{conn.Instance.ServiceName}_{schemaId}";
|
|
try
|
|
{
|
|
var schemaNode = JsonNode.Parse(schemaDef.SchemaJson);
|
|
if (schemaNode is not null)
|
|
{
|
|
schemas[prefixedId] = schemaNode;
|
|
}
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
// Skip invalid schemas
|
|
}
|
|
}
|
|
}
|
|
|
|
if (schemas.Count > 0)
|
|
{
|
|
components["schemas"] = schemas;
|
|
}
|
|
|
|
// Generate security schemes
|
|
var allEndpoints = connections.SelectMany(c => c.Endpoints.Values);
|
|
var securitySchemes = ClaimSecurityMapper.GenerateSecuritySchemes(allEndpoints, _options.TokenUrl);
|
|
if (securitySchemes.Count > 0)
|
|
{
|
|
components["securitySchemes"] = securitySchemes;
|
|
}
|
|
|
|
return components;
|
|
}
|
|
|
|
private JsonArray GenerateTags(IReadOnlyList<ConnectionState> connections)
|
|
{
|
|
var tags = new JsonArray();
|
|
var seen = new HashSet<string>();
|
|
|
|
foreach (var conn in connections)
|
|
{
|
|
var serviceName = conn.Instance.ServiceName;
|
|
if (seen.Add(serviceName))
|
|
{
|
|
var tag = new JsonObject
|
|
{
|
|
["name"] = serviceName,
|
|
["description"] = $"{serviceName} microservice (v{conn.Instance.Version})"
|
|
};
|
|
|
|
if (conn.OpenApiInfo?.Description is not null)
|
|
{
|
|
tag["description"] = conn.OpenApiInfo.Description;
|
|
}
|
|
|
|
tags.Add(tag);
|
|
}
|
|
}
|
|
|
|
return tags;
|
|
}
|
|
}
|