From 19fc8836f0dece05007ce4acd2d2a192b4d517e0 Mon Sep 17 00:00:00 2001 From: Jason Green Date: Mon, 4 May 2026 09:07:42 -0500 Subject: [PATCH 1/2] feat: Add semantic linting for Splunk .conf files --- out/codeActionProvider.js | 131 ++++ out/extension.js | 1410 ++++++++++++++++++++++--------------- out/semanticRules.js | 227 ++++++ 3 files changed, 1206 insertions(+), 562 deletions(-) create mode 100644 out/codeActionProvider.js create mode 100644 out/semanticRules.js diff --git a/out/codeActionProvider.js b/out/codeActionProvider.js new file mode 100644 index 0000000..a0aebc7 --- /dev/null +++ b/out/codeActionProvider.js @@ -0,0 +1,131 @@ +"use strict"; +const vscode = require("vscode"); +const semanticRules = require("./semanticRules.js"); + +class SplunkCodeActionProvider { + constructor(extensionPath) { + this.extensionPath = extensionPath; + } + + static get providedCodeActionKinds() { + return [vscode.CodeActionKind.QuickFix, vscode.CodeActionKind.Refactor]; + } + + provideCodeActions(document, range, context) { + const actions = []; + + for (const diagnostic of context.diagnostics) { + // Handle semantic linting diagnostics + if (diagnostic.source === "splunk-semantic") { + const ruleId = diagnostic.code; + const rule = semanticRules.getRuleById(this.extensionPath, ruleId); + + if (!rule) continue; + + if (rule.fix) { + const fixAction = this.createFixAction(document, diagnostic, rule); + if (fixAction) actions.push(fixAction); + } + + if (rule.suggestion.documentation) { + const learnMoreAction = this.createLearnMoreAction(rule); + actions.push(learnMoreAction); + } + } + } + + return actions; + } + + createFixAction(document, diagnostic, rule) { + const fix = rule.fix; + if (!fix || !fix.changes || fix.changes.length === 0) return null; + + const action = new vscode.CodeAction( + `Fix: ${rule.suggestion.title}`, + vscode.CodeActionKind.QuickFix, + ); + + action.diagnostics = [diagnostic]; + action.isPreferred = true; + + const edit = new vscode.WorkspaceEdit(); + const stanzaLine = diagnostic.range.start.line; + + const stanzaContext = semanticRules.parseStanzaContext( + document, + stanzaLine, + ); + if (!stanzaContext) return null; + + let lastSettingLine = stanzaLine; + for (const line of Object.values(stanzaContext.settingLines)) { + if (line > lastSettingLine) lastSettingLine = line; + } + + for (const change of fix.changes) { + switch (change.action) { + case "set": { + if (change.setting in stanzaContext.settingLines) { + const line = stanzaContext.settingLines[change.setting]; + const lineText = document.lineAt(line).text; + const newLine = `${change.setting} = ${change.value}`; + edit.replace( + document.uri, + new vscode.Range(line, 0, line, lineText.length), + newLine, + ); + } else { + const insertPosition = new vscode.Position(lastSettingLine + 1, 0); + edit.insert( + document.uri, + insertPosition, + `${change.setting} = ${change.value}\n`, + ); + lastSettingLine++; + } + break; + } + case "add": { + if (!(change.setting in stanzaContext.settingLines)) { + const insertPosition = new vscode.Position(lastSettingLine + 1, 0); + edit.insert( + document.uri, + insertPosition, + `${change.setting} = ${change.value}\n`, + ); + lastSettingLine++; + } + break; + } + case "remove": { + if (change.setting in stanzaContext.settingLines) { + const line = stanzaContext.settingLines[change.setting]; + edit.delete(document.uri, new vscode.Range(line, 0, line + 1, 0)); + } + break; + } + } + } + + action.edit = edit; + return action; + } + + createLearnMoreAction(rule) { + const action = new vscode.CodeAction( + `Learn more: ${rule.suggestion.title}`, + vscode.CodeActionKind.Empty, + ); + + action.command = { + command: "vscode.open", + title: "Open Documentation", + arguments: [vscode.Uri.parse(rule.suggestion.documentation)], + }; + + return action; + } +} + +exports.SplunkCodeActionProvider = SplunkCodeActionProvider; diff --git a/out/extension.js b/out/extension.js index 0ec966b..1ed9889 100644 --- a/out/extension.js +++ b/out/extension.js @@ -7,12 +7,15 @@ const fs = require("fs"); const splunkSearchProvider = require("./searchProvider.js"); const splunkEmbeddedReportProvider = require("./embeddedReportProvider"); const splunkFoldingRangeProvider = require("./foldingRangeProvider.js"); -const splunkModViz = require('./modViz.js'); -const splunkCustomCommand = require('./customCommand.js'); -const globalConfigPreview = require('./globalConfigPreview') -const splunkCustomRESTHandler = require('./customRESTHandler.js') +const splunkModViz = require("./modViz.js"); +const splunkCustomCommand = require("./customCommand.js"); +const globalConfigPreview = require("./globalConfigPreview"); +const splunkCustomRESTHandler = require("./customRESTHandler.js"); const splunkSpec = require("./spec.js"); +const semanticRules = require("./semanticRules.js"); +const { SplunkCodeActionProvider } = require("./codeActionProvider.js"); const PLACEHOLDER_REGEX = /\<([^\>]+)\>/g; +let extensionPath = null; let specConfigs = {}; let timeout; let diagnosticCollection; @@ -20,642 +23,925 @@ let specConfig; let snippets = {}; const reload = require("./commands/reload.js"); -const { SplunkNotebookSerializer } = require('./notebooks/serializers'); -const { SplunkController } = require('./notebooks/controller'); -const { Spl2NotebookSerializer } = require('./notebooks/spl2/serializer'); -const { Spl2Controller } = require('./notebooks/spl2/controller'); -const { installMissingSpl2Requirements, getLatestSpl2Release } = require('./notebooks/spl2/installer'); -const { startSpl2ClientAndServer } = require('./notebooks/spl2/initializer'); -const notebookCommands = require('./notebooks/commands'); -const { CellResultCountStatusBarProvider } = require('./notebooks/provider'); +const { SplunkNotebookSerializer } = require("./notebooks/serializers"); +const { SplunkController } = require("./notebooks/controller"); +const { Spl2NotebookSerializer } = require("./notebooks/spl2/serializer"); +const { Spl2Controller } = require("./notebooks/spl2/controller"); +const { + installMissingSpl2Requirements, + getLatestSpl2Release, +} = require("./notebooks/spl2/installer"); +const { startSpl2ClientAndServer } = require("./notebooks/spl2/initializer"); +const notebookCommands = require("./notebooks/commands"); +const { CellResultCountStatusBarProvider } = require("./notebooks/provider"); let spl2Client; let spl2PortToAttempt = 59143; // 59143 ~ SPLNK if you squint really hard :) function getParentStanza(document, line) { - // Start at the passed in line and go backwards - // up the document until we find a line that starts with - // '[' indicating a stanza. - for (var i=line; i >= 0; i--) { - if(document.lineAt(i).text.startsWith("[")) { - return document.lineAt(i).text - } + // Start at the passed in line and go backwards + // up the document until we find a line that starts with + // '[' indicating a stanza. + for (var i = line; i >= 0; i--) { + if (document.lineAt(i).text.startsWith("[")) { + return document.lineAt(i).text; } - // No parent stanza, so any settings here apply to [default] - return "[default]" + } + // No parent stanza, so any settings here apply to [default] + return "[default]"; } function getDocumentItems(document, PATTERN) { - // Given a pattern, return line numbers that match that pattern - - let items = [] - - for (var i=0; i < document.lineCount; i++) { - if(PATTERN.test(document.lineAt(i).text)) { - // If the parent line ends with a '\', this is a continuation line, - // so do not add it. - if(i>0 && PATTERN == splunkSpec.SETTING_REGEX && document.lineAt(i-1) && document.lineAt(i-1).text.trim().endsWith("\\")) { - continue - } - let item = {} - item["text"] = document.lineAt(i).text - item["line"] = i - items.push(item) - } + // Given a pattern, return line numbers that match that pattern + + let items = []; + + for (var i = 0; i < document.lineCount; i++) { + if (PATTERN.test(document.lineAt(i).text)) { + // If the parent line ends with a '\', this is a continuation line, + // so do not add it. + if ( + i > 0 && + PATTERN == splunkSpec.SETTING_REGEX && + document.lineAt(i - 1) && + document + .lineAt(i - 1) + .text.trim() + .endsWith("\\") + ) { + continue; + } + let item = {}; + item["text"] = document.lineAt(i).text; + item["line"] = i; + items.push(item); } + } - return items + return items; } async function activate(context) { - - let splunkOutputChannel = vscode.window.createOutputChannel("Splunk"); - - // Setup globalConfig.json preview - globalConfigPreview.init(context); - - // Set up Splunk report viewer - const embeddedReportProvider = new splunkEmbeddedReportProvider.SplunkReportProvider(); - vscode.window.registerTreeDataProvider('embeddedReports', embeddedReportProvider); - vscode.commands.registerCommand('splunk.embeddedReport.refresh', () => embeddedReportProvider.refresh()); - const viewRefreshInterval = vscode.workspace.getConfiguration('splunk').get('reports.viewRefreshInterval') * 1000; - vscode.commands.registerCommand('splunk.embeddedReport.show', report => { - const panel = vscode.window.createWebviewPanel( - 'splunkWebview', - 'Splunk Report', - vscode.ViewColumn.One, - { - enableScripts: true, - retainContextWhenHidden: false - } - ); - const updateWebview = async () => { - panel.webview.html = await embeddedReportProvider.getWebviewContent(report); - } - - updateWebview(); - const interval = setInterval(updateWebview, viewRefreshInterval); - panel.onDidDispose( () => { - clearInterval(interval); - }, null, context.subscriptions); - - }); - - // Set up Splunk search viewer - const savedSearchProvider = new splunkSearchProvider.SavedSearchProvider(); - vscode.window.registerTreeDataProvider('savedSearches', savedSearchProvider); - vscode.commands.registerCommand('splunk.savedSearches.refresh', () => savedSearchProvider.refresh()); - vscode.commands.registerCommand('splunk.savedSearch.run', async search => { - if(!search) { - search = await vscode.window.showQuickPick(savedSearchProvider.getSavedSearches(), {canPickMany: false, placeHolder:'Saved Search'}) - } - let searchResult = await savedSearchProvider.runSavedSearch(search); + let splunkOutputChannel = vscode.window.createOutputChannel("Splunk"); + extensionPath = context.extensionPath; + + // Register code action provider for semantic linting quick fixes + const semanticLintingEnabled = vscode.workspace + .getConfiguration("splunk") + .get("semanticLinting.enabled", true); + if (semanticLintingEnabled) { + context.subscriptions.push( + vscode.languages.registerCodeActionsProvider( + { language: "splunk" }, + new SplunkCodeActionProvider(context.extensionPath), + { + providedCodeActionKinds: + SplunkCodeActionProvider.providedCodeActionKinds, + }, + ), + ); + } + + // Setup globalConfig.json preview + globalConfigPreview.init(context); + + // Set up Splunk report viewer + const embeddedReportProvider = + new splunkEmbeddedReportProvider.SplunkReportProvider(); + vscode.window.registerTreeDataProvider( + "embeddedReports", + embeddedReportProvider, + ); + vscode.commands.registerCommand("splunk.embeddedReport.refresh", () => + embeddedReportProvider.refresh(), + ); + const viewRefreshInterval = + vscode.workspace + .getConfiguration("splunk") + .get("reports.viewRefreshInterval") * 1000; + vscode.commands.registerCommand("splunk.embeddedReport.show", (report) => { + const panel = vscode.window.createWebviewPanel( + "splunkWebview", + "Splunk Report", + vscode.ViewColumn.One, + { + enableScripts: true, + retainContextWhenHidden: false, + }, + ); + const updateWebview = async () => { + panel.webview.html = + await embeddedReportProvider.getWebviewContent(report); + }; + + updateWebview(); + const interval = setInterval(updateWebview, viewRefreshInterval); + panel.onDidDispose( + () => { + clearInterval(interval); + }, + null, + context.subscriptions, + ); + }); + + // Set up Splunk search viewer + const savedSearchProvider = new splunkSearchProvider.SavedSearchProvider(); + vscode.window.registerTreeDataProvider("savedSearches", savedSearchProvider); + vscode.commands.registerCommand("splunk.savedSearches.refresh", () => + savedSearchProvider.refresh(), + ); + vscode.commands.registerCommand("splunk.savedSearch.run", async (search) => { + if (!search) { + search = await vscode.window.showQuickPick( + savedSearchProvider.getSavedSearches(), + { canPickMany: false, placeHolder: "Saved Search" }, + ); + } + let searchResult = await savedSearchProvider.runSavedSearch(search); + splunkOutputChannel.appendLine(searchResult); + splunkOutputChannel.show(); + }); + + // Set up Splunk ad-hoc search command + const searchProvider = new splunkSearchProvider.SearchProvider(); + context.subscriptions.push( + vscode.commands.registerCommand("splunk.search.adhoc", async () => { + let search = await vscode.window.showInputBox({ + prompt: "Search SPL", + }); + if (search) { + let searchResult = await searchProvider.runSearch(search); splunkOutputChannel.appendLine(searchResult); - splunkOutputChannel.show() - }); - - // Set up Splunk ad-hoc search command - const searchProvider = new splunkSearchProvider.SearchProvider(); - context.subscriptions.push(vscode.commands.registerCommand('splunk.search.adhoc', async () => { - let search = await vscode.window.showInputBox({ - prompt:'Search SPL' - }) - if (search) { - let searchResult = await searchProvider.runSearch(search); - splunkOutputChannel.appendLine(searchResult); - splunkOutputChannel.show(); - } - })); - - // Set up Splunk modular visualization creator - context.subscriptions.push(vscode.commands.registerCommand('splunk.new.modviz', async () => { - let destFolder = await vscode.window.showOpenDialog({ - canSelectFiles: false, - canSelectFolders: true, - canSelectMany: false, - openLabel: "Select project path" - }); - - let modVizName = await vscode.window.showInputBox({ - placeHolder: "Visualization name", - prompt: "Specify a name for this custom visualization." - }) - - if((!destFolder) || (!modVizName)) { - return - } else { - splunkModViz.createModViz(modVizName, destFolder, context); - vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(path.join(destFolder[0].path, modVizName)), true); - vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(path.join(destFolder[0].path, modVizName, "appserver", "static", "visualizations", modVizName, "src", "visualization_source.js"))); - vscode.env.openExternal(vscode.Uri.parse('https://docs.splunk.com/Documentation/Splunk/latest/AdvancedDev/CustomVizTutorial#Create_the_visualization_logic')); - } - - })); - - // Set up Splunk custom search command creator - context.subscriptions.push(vscode.commands.registerCommand('splunk.new.command', async () => { - let destFolder = await vscode.window.showOpenDialog({ - canSelectFiles: false, - canSelectFolders: true, - canSelectMany: false, - openLabel: "Select project path" - }); - - let commandAppName = await vscode.window.showInputBox({ - placeHolder: "App name", - prompt: "Specify an app name for this custom command." - }) - - if((!destFolder) || (!commandAppName)) { - return - } else { - splunkCustomCommand.createCommand(commandAppName, destFolder, context); - vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(path.join(destFolder[0].path, commandAppName)), true); - vscode.env.openExternal(vscode.Uri.parse('https://dev.splunk.com/enterprise/docs/developapps/customsearchcommands/createcustomsearchcmd')); - } - - })); - - // Set up Splunk custom REST handler creator - context.subscriptions.push(vscode.commands.registerCommand('splunk.new.resthandler', async () => { - let destFolder = await vscode.window.showOpenDialog({ - canSelectFiles: false, - canSelectFolders: true, - canSelectMany: false, - openLabel: "Select project path" - }); - - let handlerAppName = await vscode.window.showInputBox({ - placeHolder: "App name", - prompt: "Specify an app name for this custom REST handler." - }) - - if((!destFolder) || (!handlerAppName)) { - return - } else { - splunkCustomRESTHandler.createRESTHandler(handlerAppName, destFolder, context); - vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(path.join(destFolder[0].path, handlerAppName)), true); - // vscode.env.openExternal(vscode.Uri.parse('https://github.com/jrervin/splunk-rest-examples')); - } - - })); - - // Setup progress bar for install - const progressBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100); - context.subscriptions.push(progressBar); - progressBar.hide(); - - // Register Utility Commands - context.subscriptions.push(vscode.commands.registerCommand('splunk.fullDebugRefresh', async () => {reload.fullDebugRefresh(splunkOutputChannel)})) - - // Set up stanza folding - context.subscriptions.push(vscode.languages.registerFoldingRangeProvider([ - { language: 'splunk', pattern: '**/*.{conf,conf.spec}' } - ], new splunkFoldingRangeProvider.confFoldingRangeProvider())); - - // If vscode was opened with an active Splunk file, handle it. - vscode.commands.registerCommand('splunk.restartSpl2LanguageServer', async () => { - try { - if (spl2Client) { - await spl2Client.deactivate(); - } - spl2Client = undefined; - await handleSpl2Document(context, progressBar); - } catch (err) { - console.warn(`Error restarting SPL2 language server, err: ${err}`); - } - }); - if(vscode.window.activeTextEditor) { - if (isSplunkDocument(vscode.window.activeTextEditor.document)) { - handleSplunkDocument(context); - } else if (isSpl2Document(vscode.window.activeTextEditor.document)) { - await handleSpl2Document(context, progressBar); + splunkOutputChannel.show(); + } + }), + ); + + // Set up Splunk modular visualization creator + context.subscriptions.push( + vscode.commands.registerCommand("splunk.new.modviz", async () => { + let destFolder = await vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: "Select project path", + }); + + let modVizName = await vscode.window.showInputBox({ + placeHolder: "Visualization name", + prompt: "Specify a name for this custom visualization.", + }); + + if (!destFolder || !modVizName) { + return; + } else { + splunkModViz.createModViz(modVizName, destFolder, context); + vscode.commands.executeCommand( + "vscode.openFolder", + vscode.Uri.file(path.join(destFolder[0].path, modVizName)), + true, + ); + vscode.commands.executeCommand( + "vscode.openFolder", + vscode.Uri.file( + path.join( + destFolder[0].path, + modVizName, + "appserver", + "static", + "visualizations", + modVizName, + "src", + "visualization_source.js", + ), + ), + ); + vscode.env.openExternal( + vscode.Uri.parse( + "https://docs.splunk.com/Documentation/Splunk/latest/AdvancedDev/CustomVizTutorial#Create_the_visualization_logic", + ), + ); + } + }), + ); + + // Set up Splunk custom search command creator + context.subscriptions.push( + vscode.commands.registerCommand("splunk.new.command", async () => { + let destFolder = await vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: "Select project path", + }); + + let commandAppName = await vscode.window.showInputBox({ + placeHolder: "App name", + prompt: "Specify an app name for this custom command.", + }); + + if (!destFolder || !commandAppName) { + return; + } else { + splunkCustomCommand.createCommand(commandAppName, destFolder, context); + vscode.commands.executeCommand( + "vscode.openFolder", + vscode.Uri.file(path.join(destFolder[0].path, commandAppName)), + true, + ); + vscode.env.openExternal( + vscode.Uri.parse( + "https://dev.splunk.com/enterprise/docs/developapps/customsearchcommands/createcustomsearchcmd", + ), + ); + } + }), + ); + + // Set up Splunk custom REST handler creator + context.subscriptions.push( + vscode.commands.registerCommand("splunk.new.resthandler", async () => { + let destFolder = await vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: "Select project path", + }); + + let handlerAppName = await vscode.window.showInputBox({ + placeHolder: "App name", + prompt: "Specify an app name for this custom REST handler.", + }); + + if (!destFolder || !handlerAppName) { + return; + } else { + splunkCustomRESTHandler.createRESTHandler( + handlerAppName, + destFolder, + context, + ); + vscode.commands.executeCommand( + "vscode.openFolder", + vscode.Uri.file(path.join(destFolder[0].path, handlerAppName)), + true, + ); + // vscode.env.openExternal(vscode.Uri.parse('https://github.com/jrervin/splunk-rest-examples')); + } + }), + ); + + // Setup progress bar for install + const progressBar = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Right, + 100, + ); + context.subscriptions.push(progressBar); + progressBar.hide(); + + // Register Utility Commands + context.subscriptions.push( + vscode.commands.registerCommand("splunk.fullDebugRefresh", async () => { + reload.fullDebugRefresh(splunkOutputChannel); + }), + ); + + // Set up stanza folding + context.subscriptions.push( + vscode.languages.registerFoldingRangeProvider( + [{ language: "splunk", pattern: "**/*.{conf,conf.spec}" }], + new splunkFoldingRangeProvider.confFoldingRangeProvider(), + ), + ); + + // If vscode was opened with an active Splunk file, handle it. + vscode.commands.registerCommand( + "splunk.restartSpl2LanguageServer", + async () => { + try { + if (spl2Client) { + await spl2Client.deactivate(); } + spl2Client = undefined; + await handleSpl2Document(context, progressBar); + } catch (err) { + console.warn(`Error restarting SPL2 language server, err: ${err}`); + } + }, + ); + if (vscode.window.activeTextEditor) { + if (isSplunkDocument(vscode.window.activeTextEditor.document)) { + handleSplunkDocument(context); + } else if (isSpl2Document(vscode.window.activeTextEditor.document)) { + await handleSpl2Document(context, progressBar); } - - // Set up listener for text document changes - context.subscriptions.push(vscode.workspace.onDidChangeTextDocument(editor => { - if(vscode.window.activeTextEditor && vscode.window.activeTextEditor.document.languageId === 'splunk') { - // Use a timer on onDidChangeTextDocument so we are not linting as often. - triggerDiagnostics(specConfig, editor.document, diagnosticCollection); - } - })); - - // Set up listener for active editor changing - context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor( async () => { - if (!vscode.window.activeTextEditor) { - return; - } - if (isSplunkDocument(vscode.window.activeTextEditor.document)) { - handleSplunkDocument(context); - } else if (isSpl2Document(vscode.window.activeTextEditor.document)) { - await handleSpl2Document(context, progressBar); - } - })); - - // Notebook - context.subscriptions.push(vscode.workspace.registerNotebookSerializer('splunk-notebook', new SplunkNotebookSerializer(), {transientCellMetadata: {inputCollapsed: true, outputCollapsed: true}, transientOutputs: false})); - context.subscriptions.push(vscode.workspace.registerNotebookSerializer('spl2-notebook', new Spl2NotebookSerializer(), {transientCellMetadata: {inputCollapsed: true, outputCollapsed: true}, transientOutputs: false})); - const controller = new SplunkController(); - context.subscriptions.push(controller); - console.log(`Setting up Spl2Controller ...`); - const spl2Controller = new Spl2Controller(); - context.subscriptions.push(spl2Controller); - context.subscriptions.push(vscode.notebooks.registerNotebookCellStatusBarItemProvider('splunk-notebook', new CellResultCountStatusBarProvider(splunkOutputChannel))); - context.subscriptions.push(vscode.notebooks.registerNotebookCellStatusBarItemProvider('spl2-notebook', new CellResultCountStatusBarProvider(splunkOutputChannel))); - notebookCommands.registerNotebookCommands([controller, spl2Controller], splunkOutputChannel, context); - - - // Set up listener for configuration setting changes - context.subscriptions.push(vscode.workspace.onDidChangeConfiguration( () => { - embeddedReportProvider.refresh(); - savedSearchProvider.refresh(); - })) + } + + // Set up listener for text document changes + context.subscriptions.push( + vscode.workspace.onDidChangeTextDocument((editor) => { + if ( + vscode.window.activeTextEditor && + vscode.window.activeTextEditor.document.languageId === "splunk" + ) { + // Use a timer on onDidChangeTextDocument so we are not linting as often. + triggerDiagnostics(specConfig, editor.document, diagnosticCollection); + } + }), + ); + + // Set up listener for active editor changing + context.subscriptions.push( + vscode.window.onDidChangeActiveTextEditor(async () => { + if (!vscode.window.activeTextEditor) { + return; + } + if (isSplunkDocument(vscode.window.activeTextEditor.document)) { + handleSplunkDocument(context); + } else if (isSpl2Document(vscode.window.activeTextEditor.document)) { + await handleSpl2Document(context, progressBar); + } + }), + ); + + // Notebook + context.subscriptions.push( + vscode.workspace.registerNotebookSerializer( + "splunk-notebook", + new SplunkNotebookSerializer(), + { + transientCellMetadata: { inputCollapsed: true, outputCollapsed: true }, + transientOutputs: false, + }, + ), + ); + context.subscriptions.push( + vscode.workspace.registerNotebookSerializer( + "spl2-notebook", + new Spl2NotebookSerializer(), + { + transientCellMetadata: { inputCollapsed: true, outputCollapsed: true }, + transientOutputs: false, + }, + ), + ); + const controller = new SplunkController(); + context.subscriptions.push(controller); + console.log(`Setting up Spl2Controller ...`); + const spl2Controller = new Spl2Controller(); + context.subscriptions.push(spl2Controller); + context.subscriptions.push( + vscode.notebooks.registerNotebookCellStatusBarItemProvider( + "splunk-notebook", + new CellResultCountStatusBarProvider(splunkOutputChannel), + ), + ); + context.subscriptions.push( + vscode.notebooks.registerNotebookCellStatusBarItemProvider( + "spl2-notebook", + new CellResultCountStatusBarProvider(splunkOutputChannel), + ), + ); + notebookCommands.registerNotebookCommands( + [controller, spl2Controller], + splunkOutputChannel, + context, + ); + + // Set up listener for configuration setting changes + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration(() => { + embeddedReportProvider.refresh(); + savedSearchProvider.refresh(); + }), + ); } exports.activate = activate; function isSplunkDocument(document) { - let splunkFileExtensions = [".conf", "default.meta", "local.meta", "globalconfig.json"]; - for (let i=0; i < splunkFileExtensions.length; i++) { - if(document.fileName.toLowerCase().endsWith(splunkFileExtensions[i])) { - return true; - } + let splunkFileExtensions = [ + ".conf", + "default.meta", + "local.meta", + "globalconfig.json", + ]; + for (let i = 0; i < splunkFileExtensions.length; i++) { + if (document.fileName.toLowerCase().endsWith(splunkFileExtensions[i])) { + return true; } - return false; + } + return false; } function handleSplunkDocument(context) { - - if(diagnosticCollection === undefined) { - diagnosticCollection = vscode.languages.createDiagnosticCollection('splunk'); - } - - let currentDocument = path.basename(vscode.window.activeTextEditor.document.uri.fsPath); - - // Any snippets for this file? - let snippetFilePath = path.join(context.extensionPath, "snippets", currentDocument); - if(fs.existsSync(snippetFilePath) && !snippets.hasOwnProperty(currentDocument)) { - context.subscriptions.push(provideSnippetCompletionItems(snippetFilePath)); - // Cache snippets for this file so we do not regenerate them. - snippets[currentDocument] = snippetFilePath; - } - - // If this file is globalConfig.json, return as there is not a spec file for it. - if(currentDocument.toLowerCase() == "globalconfig.json") { - return; - } - let specFilePath = getSpecFilePath(context.extensionPath, currentDocument); - - if(specConfigs.hasOwnProperty(currentDocument)) { - specConfig = specConfigs[currentDocument]; - } else { - specConfig = splunkSpec.getSpecConfig(context.extensionPath, specFilePath); - - // Register Stanza completion items for this spec - context.subscriptions.push(provideStanzaCompletionItems(specConfig)); - - // Register Setting completion items for this spec - let trimWhitespace = vscode.workspace.getConfiguration().get('splunk.spec.trimEqualSignWhitespace') - context.subscriptions.push(provideSettingCompletionItems(specConfig, trimWhitespace)); - - // Register Hovers - context.subscriptions.push(provideHovers(specConfig)); - } - - // Set up diagnostics (linting) - updateDiagnostics(specConfig, vscode.window.activeTextEditor.document, diagnosticCollection); - - // Cache specConfig - specConfigs[currentDocument] = specConfig + if (diagnosticCollection === undefined) { + diagnosticCollection = + vscode.languages.createDiagnosticCollection("splunk"); + } + + let currentDocument = path.basename( + vscode.window.activeTextEditor.document.uri.fsPath, + ); + + // Any snippets for this file? + let snippetFilePath = path.join( + context.extensionPath, + "snippets", + currentDocument, + ); + if ( + fs.existsSync(snippetFilePath) && + !snippets.hasOwnProperty(currentDocument) + ) { + context.subscriptions.push(provideSnippetCompletionItems(snippetFilePath)); + // Cache snippets for this file so we do not regenerate them. + snippets[currentDocument] = snippetFilePath; + } + + // If this file is globalConfig.json, return as there is not a spec file for it. + if (currentDocument.toLowerCase() == "globalconfig.json") { + return; + } + let specFilePath = getSpecFilePath(context.extensionPath, currentDocument); + + if (specConfigs.hasOwnProperty(currentDocument)) { + specConfig = specConfigs[currentDocument]; + } else { + specConfig = splunkSpec.getSpecConfig(context.extensionPath, specFilePath); + + // Register Stanza completion items for this spec + context.subscriptions.push(provideStanzaCompletionItems(specConfig)); + + // Register Setting completion items for this spec + let trimWhitespace = vscode.workspace + .getConfiguration() + .get("splunk.spec.trimEqualSignWhitespace"); + context.subscriptions.push( + provideSettingCompletionItems(specConfig, trimWhitespace), + ); + + // Register Hovers + context.subscriptions.push(provideHovers(specConfig)); + } + + // Set up diagnostics (linting) + updateDiagnostics( + specConfig, + vscode.window.activeTextEditor.document, + diagnosticCollection, + ); + + // Cache specConfig + specConfigs[currentDocument] = specConfig; } function isSpl2Document(document) { - return document.languageId == 'splunk_spl2'; + return document.languageId == "splunk_spl2"; } async function handleSpl2Document(context, progressBar) { - console.log(`handleSpl2Document`); - if (spl2Client) { - console.log(`spl2Client detected`); - // send new didOpen message to refresh language server (even if already open) - // this workaround is needed as SPL2 language server doesn't currently cache - // all open documents and will only re-parse when new documents are opened - spl2Client.client.sendRequest( - 'textDocument/didOpen', - { - textDocument: { - uri: vscode.window.activeTextEditor.document.uri.toString(), - languageId: vscode.window.activeTextEditor.document.languageId, - version: vscode.window.activeTextEditor.document.version, - text: vscode.window.activeTextEditor.document.getText(), - } - } - ); - return; - } - try { - const globalStoragePath = context.globalStorageUri.fsPath; - console.log(`no spl2Client detected, attempting install to ${globalStoragePath}`); - const installedLatestLsp = await installMissingSpl2Requirements(globalStoragePath, progressBar); - if (!installedLatestLsp) { - console.log(`installedLatestLsp=${installedLatestLsp}, calling getLatestSpl2Release`); - await getLatestSpl2Release(globalStoragePath, progressBar); - } - const onSpl2Restart = async (nextPort) => { - console.log(`onSpl2Restart called for nextPort=${nextPort}`); - await spl2Client.deactivate(); - spl2PortToAttempt = nextPort; - console.log(`re-calling startSpl2ClientAndServer for spl2PortToAttempt=${spl2PortToAttempt} ...`); - spl2Client = await startSpl2ClientAndServer(context, progressBar, spl2PortToAttempt, onSpl2Restart); - }; - console.log(`calling startSpl2ClientAndServer for spl2PortToAttempt=${spl2PortToAttempt} ...`); - spl2Client = await startSpl2ClientAndServer(context, progressBar, spl2PortToAttempt, onSpl2Restart); - } catch (err) { - console.log(`Issue setting up SPL2 environment: ${err}`); - vscode.window.showErrorMessage(`Issue setting up SPL2 environment: ${err}`); + console.log(`handleSpl2Document`); + if (spl2Client) { + console.log(`spl2Client detected`); + // send new didOpen message to refresh language server (even if already open) + // this workaround is needed as SPL2 language server doesn't currently cache + // all open documents and will only re-parse when new documents are opened + spl2Client.client.sendRequest("textDocument/didOpen", { + textDocument: { + uri: vscode.window.activeTextEditor.document.uri.toString(), + languageId: vscode.window.activeTextEditor.document.languageId, + version: vscode.window.activeTextEditor.document.version, + text: vscode.window.activeTextEditor.document.getText(), + }, + }); + return; + } + try { + const globalStoragePath = context.globalStorageUri.fsPath; + console.log( + `no spl2Client detected, attempting install to ${globalStoragePath}`, + ); + const installedLatestLsp = await installMissingSpl2Requirements( + globalStoragePath, + progressBar, + ); + if (!installedLatestLsp) { + console.log( + `installedLatestLsp=${installedLatestLsp}, calling getLatestSpl2Release`, + ); + await getLatestSpl2Release(globalStoragePath, progressBar); } + const onSpl2Restart = async (nextPort) => { + console.log(`onSpl2Restart called for nextPort=${nextPort}`); + await spl2Client.deactivate(); + spl2PortToAttempt = nextPort; + console.log( + `re-calling startSpl2ClientAndServer for spl2PortToAttempt=${spl2PortToAttempt} ...`, + ); + spl2Client = await startSpl2ClientAndServer( + context, + progressBar, + spl2PortToAttempt, + onSpl2Restart, + ); + }; + console.log( + `calling startSpl2ClientAndServer for spl2PortToAttempt=${spl2PortToAttempt} ...`, + ); + spl2Client = await startSpl2ClientAndServer( + context, + progressBar, + spl2PortToAttempt, + onSpl2Restart, + ); + } catch (err) { + console.log(`Issue setting up SPL2 environment: ${err}`); + vscode.window.showErrorMessage(`Issue setting up SPL2 environment: ${err}`); + } } function getSpecFilePath(basePath, filename) { - - // Get the custom configuration options - let settingsSpecFilePath = vscode.workspace.getConfiguration('splunk').get('spec.FilePath'); - let settingsSpecFileVersion = vscode.workspace.getConfiguration('splunk').get('spec.FileVersion'); - let specFileName = filename + ".spec"; - - // Special case spec files - let specialSpecFiles = ["eventgen.conf.spec", "default.meta.spec", "local.meta.spec"] - if(specialSpecFiles.indexOf(specFileName) > -1) { - if (specFileName == "local.meta.spec") { specFileName = "default.meta.spec" } - return checkSpecFilePath(path.join(basePath, "spec_files", specFileName)) - } - - // Create a path to the spec file for the current document - if(!settingsSpecFilePath) { - // No path was configured in settings, so create a path to the built-in spec files - return checkSpecFilePath(path.join(basePath, "spec_files", settingsSpecFileVersion, specFileName)) - } else { - return checkSpecFilePath(path.join(settingsSpecFilePath, specFileName)) + // Get the custom configuration options + let settingsSpecFilePath = vscode.workspace + .getConfiguration("splunk") + .get("spec.FilePath"); + let settingsSpecFileVersion = vscode.workspace + .getConfiguration("splunk") + .get("spec.FileVersion"); + let specFileName = filename + ".spec"; + + // Special case spec files + let specialSpecFiles = [ + "eventgen.conf.spec", + "default.meta.spec", + "local.meta.spec", + ]; + if (specialSpecFiles.indexOf(specFileName) > -1) { + if (specFileName == "local.meta.spec") { + specFileName = "default.meta.spec"; } + return checkSpecFilePath(path.join(basePath, "spec_files", specFileName)); + } + + // Create a path to the spec file for the current document + if (!settingsSpecFilePath) { + // No path was configured in settings, so create a path to the built-in spec files + return checkSpecFilePath( + path.join(basePath, "spec_files", settingsSpecFileVersion, specFileName), + ); + } else { + return checkSpecFilePath(path.join(settingsSpecFilePath, specFileName)); + } } function checkSpecFilePath(specFilePath) { - if(!fs.existsSync(specFilePath)) { - vscode.window.showErrorMessage(`Spec file path not found: ${specFilePath}`) - return null - } - return specFilePath; + if (!fs.existsSync(specFilePath)) { + vscode.window.showErrorMessage(`Spec file path not found: ${specFilePath}`); + return null; + } + return specFilePath; } function provideHovers(specConfig) { - - let enableHover = vscode.workspace.getConfiguration().get('splunk.showDocumentationOnHover'); - if(!enableHover) { - return; - } - - // Get the currently open document - let currentDocument = path.basename(vscode.window.activeTextEditor.document.uri.fsPath); - - vscode.languages.registerHoverProvider({ language: 'splunk', pattern: `**/${currentDocument}`}, { - - provideHover(document, position, token) { - - const range = document.getWordRangeAtPosition(position, /\w[-\w\.]*/g); - const word = document.getText(range); - - if((document.lineAt(position.line).text.startsWith('['))) { - // This is a stanza - - // Get stanzas for this .spec file - // Find the hovered word - // Add a hover for the value - let stanza = specConfig["stanzas"].find(item => item.stanzaName === word) - if(stanza) { - let hoverContent = new vscode.MarkdownString(stanza["docString"]) - hoverContent.isTrusted = true; - return new vscode.Hover(hoverContent); - } - } else { - // This might be a setting - let parentStanza = getParentStanza(document, position.line); - if(parentStanza) { - let stanzaSettings = splunkSpec.getStanzaSettings(specConfig, parentStanza) - let setting = stanzaSettings.find(item => item.name === word) - if(setting) { - let hoverContent = new vscode.MarkdownString(setting["docString"]) - hoverContent.isTrusted = true; - return new vscode.Hover(hoverContent); - } - } + let enableHover = vscode.workspace + .getConfiguration() + .get("splunk.showDocumentationOnHover"); + if (!enableHover) { + return; + } + + // Get the currently open document + let currentDocument = path.basename( + vscode.window.activeTextEditor.document.uri.fsPath, + ); + + vscode.languages.registerHoverProvider( + { language: "splunk", pattern: `**/${currentDocument}` }, + { + provideHover(document, position, token) { + const range = document.getWordRangeAtPosition(position, /\w[-\w\.]*/g); + const word = document.getText(range); + + if (document.lineAt(position.line).text.startsWith("[")) { + // This is a stanza + + // Get stanzas for this .spec file + // Find the hovered word + // Add a hover for the value + let stanza = specConfig["stanzas"].find( + (item) => item.stanzaName === word, + ); + if (stanza) { + let hoverContent = new vscode.MarkdownString(stanza["docString"]); + hoverContent.isTrusted = true; + return new vscode.Hover(hoverContent); + } + } else { + // This might be a setting + let parentStanza = getParentStanza(document, position.line); + if (parentStanza) { + let stanzaSettings = splunkSpec.getStanzaSettings( + specConfig, + parentStanza, + ); + let setting = stanzaSettings.find((item) => item.name === word); + if (setting) { + let hoverContent = new vscode.MarkdownString( + setting["docString"], + ); + hoverContent.isTrusted = true; + return new vscode.Hover(hoverContent); } + } } + }, + }, + ); - }) - - return null + return null; } function provideStanzaCompletionItems(specConfig) { + // Get the currently open document + let currentDocument = path.basename( + vscode.window.activeTextEditor.document.uri.fsPath, + ); + + vscode.languages.registerCompletionItemProvider( + { language: "splunk", pattern: `**/${currentDocument}` }, + { + provideCompletionItems(document, position) { + if ( + position.character != 1 || + !document.lineAt(position.line).text.startsWith("[") + ) { + // We are not typing a stanza, so return. + return; + } - // Get the currently open document - let currentDocument = path.basename(vscode.window.activeTextEditor.document.uri.fsPath); - - vscode.languages.registerCompletionItemProvider({ language: 'splunk', pattern: `**/${currentDocument}`}, { - - provideCompletionItems(document, position) { - - if((position.character != 1) || (!document.lineAt(position.line).text.startsWith('['))) { - // We are not typing a stanza, so return. - return - } - - if(!specConfig) { - // No completion for you! - return - } + if (!specConfig) { + // No completion for you! + return; + } - let completions = []; - - // Create completion items for stanzas - you can create a stanza anywhere - specConfig["stanzas"].forEach(stanza => { - let stanzaSnippet = stanza.stanzaName - let stanzaCompletionItem = new vscode.CompletionItem(stanzaSnippet); - - // Convert type things to placeholders - // ${1:} ${2:} - if(PLACEHOLDER_REGEX.test(stanzaSnippet)) { - let placeholders = stanzaSnippet.match(PLACEHOLDER_REGEX) - placeholders.forEach(function (placeholder, i) { - // vscode placeholder tab stops start at $1 since tab stop $0 is a special case - let placeholderTabStop = i + 1 - let formattedPlaceholder = `\$\{${placeholderTabStop}:${placeholder}\}` - stanzaSnippet = stanzaSnippet.replace(placeholder, formattedPlaceholder) - }) - } - stanzaCompletionItem.insertText = new vscode.SnippetString(stanzaSnippet); - stanzaCompletionItem.documentation = new vscode.MarkdownString(stanza.docString); - stanzaCompletionItem.kind = vscode.CompletionItemKind.Class; - completions.push(stanzaCompletionItem); + let completions = []; + + // Create completion items for stanzas - you can create a stanza anywhere + specConfig["stanzas"].forEach((stanza) => { + let stanzaSnippet = stanza.stanzaName; + let stanzaCompletionItem = new vscode.CompletionItem(stanzaSnippet); + + // Convert type things to placeholders + // ${1:} ${2:} + if (PLACEHOLDER_REGEX.test(stanzaSnippet)) { + let placeholders = stanzaSnippet.match(PLACEHOLDER_REGEX); + placeholders.forEach(function (placeholder, i) { + // vscode placeholder tab stops start at $1 since tab stop $0 is a special case + let placeholderTabStop = i + 1; + let formattedPlaceholder = `\$\{${placeholderTabStop}:${placeholder}\}`; + stanzaSnippet = stanzaSnippet.replace( + placeholder, + formattedPlaceholder, + ); }); + } + stanzaCompletionItem.insertText = new vscode.SnippetString( + stanzaSnippet, + ); + stanzaCompletionItem.documentation = new vscode.MarkdownString( + stanza.docString, + ); + stanzaCompletionItem.kind = vscode.CompletionItemKind.Class; + completions.push(stanzaCompletionItem); + }); + + return completions; + }, + }, + "[", + ); +} - return completions +function provideSettingCompletionItems(specConfig, trimWhitespace) { + // Get the currently open document + let currentDocument = path.basename( + vscode.window.activeTextEditor.document.uri.fsPath, + ); + vscode.languages.registerCompletionItemProvider( + { language: "splunk", pattern: `**/${currentDocument}` }, + { + provideCompletionItems(document, position) { + if (position.character > 1 || !specConfig) { + // No completion for you! + return; } - }, '[' ); + let completions = []; + let parentStanza = getParentStanza(document, position.line); + + if (parentStanza) { + // Get settings for the current stanza + let stanzaSettings = splunkSpec.getStanzaSettings( + specConfig, + parentStanza, + ); + + // Create completion items for settings + stanzaSettings.forEach((setting) => { + // a.k.a. the Sanford setting + let settingSnippet = trimWhitespace + ? `${setting.name}=${setting.value}` + : `${setting.name} = ${setting.value}`; + let settingCompletionItem = new vscode.CompletionItem( + settingSnippet, + ); + + // Convert to ${1|true,false|} + if (settingSnippet.indexOf("") > -1) { + settingSnippet = settingSnippet.replace( + "", + "${1|true,false|}", + ); + } -} + // Convert 'true | false' to ${1|true,false|} + if (settingSnippet.indexOf("true | false") > -1) { + settingSnippet = settingSnippet.replace( + "true | false", + "${1|true,false|}", + ); + } -function provideSettingCompletionItems(specConfig, trimWhitespace) { + // Convert '[|[KB|MB|GB]|auto]' to ${1|,auto|}} + if ( + settingSnippet.indexOf("[|[KB|MB|GB]|auto]") > + -1 + ) { + settingSnippet = settingSnippet.replace( + "[|[KB|MB|GB]|auto]", + "${1|integer,auto|}", + ); + } - // Get the currently open document - let currentDocument = path.basename(vscode.window.activeTextEditor.document.uri.fsPath); - vscode.languages.registerCompletionItemProvider({ language: 'splunk', pattern: `**/${currentDocument}`}, { + // Convert type things to placeholders + // ${1:} ${2:} + if (PLACEHOLDER_REGEX.test(settingSnippet)) { + let placeholders = settingSnippet.match(PLACEHOLDER_REGEX); + placeholders.forEach(function (placeholder, i) { + // vscode placeholder tab stops start at 1 since tab stop $0 is a special case + let placeholderTabStop = i + 1; + let formattedPlaceholder = `\$\{${placeholderTabStop}:${placeholder}\}`; + settingSnippet = settingSnippet.replace( + placeholder, + formattedPlaceholder, + ); + }); + } - provideCompletionItems(document, position) { - if((position.character > 1) || (!specConfig)) { - // No completion for you! - return + // Convert to ${1|enabled,disabled|} + if (settingSnippet.indexOf("${1:}") > -1) { + settingSnippet = settingSnippet.replace( + "${1:}", + "${1|enabled,disabled|}", + ); + } + if (settingSnippet.indexOf("${2:}") > -1) { + settingSnippet = settingSnippet.replace( + "${2:}", + "${2|enabled,disabled|}", + ); } - let completions = []; - let parentStanza = getParentStanza(document, position.line); - - if(parentStanza) { - // Get settings for the current stanza - let stanzaSettings = splunkSpec.getStanzaSettings(specConfig, parentStanza) - - // Create completion items for settings - stanzaSettings.forEach(setting => { - - // a.k.a. the Sanford setting - let settingSnippet = trimWhitespace ? `${setting.name}=${setting.value}` : `${setting.name} = ${setting.value}` - let settingCompletionItem = new vscode.CompletionItem(settingSnippet); - - // Convert to ${1|true,false|} - if(settingSnippet.indexOf("") > -1) { - settingSnippet = settingSnippet.replace("", "${1|true,false|}") - } - - // Convert 'true | false' to ${1|true,false|} - if(settingSnippet.indexOf("true | false") > -1) { - settingSnippet = settingSnippet.replace("true | false", "${1|true,false|}") - } - - // Convert '[|[KB|MB|GB]|auto]' to ${1|,auto|}} - if(settingSnippet.indexOf("[|[KB|MB|GB]|auto]") > -1) { - settingSnippet = settingSnippet.replace("[|[KB|MB|GB]|auto]", "${1|integer,auto|}") - } - - // Convert type things to placeholders - // ${1:} ${2:} - if(PLACEHOLDER_REGEX.test(settingSnippet)) { - let placeholders = settingSnippet.match(PLACEHOLDER_REGEX) - placeholders.forEach(function (placeholder, i) { - // vscode placeholder tab stops start at 1 since tab stop $0 is a special case - let placeholderTabStop = i + 1 - let formattedPlaceholder = `\$\{${placeholderTabStop}:${placeholder}\}` - settingSnippet = settingSnippet.replace(placeholder, formattedPlaceholder) - }) - } - - // Convert to ${1|enabled,disabled|} - if(settingSnippet.indexOf("${1:}") > -1) { - settingSnippet = settingSnippet.replace("${1:}", "${1|enabled,disabled|}") - } - if(settingSnippet.indexOf("${2:}") > -1) { - settingSnippet = settingSnippet.replace("${2:}", "${2|enabled,disabled|}") - } - - // Convert [foo|bar|baz] or {foo|bar|baz} values to a dropdown placeholder - // ${1|foo,bar,baz|} - if(splunkSpec.DROPDOWN_PLACEHOLDER_REGEX.test(settingSnippet)) { - settingSnippet = settingSnippet.replace(/\|/g, ',') - settingSnippet = settingSnippet.replace(/\[|{/, '${1|') - settingSnippet = settingSnippet.replace(/]|}/, '|}') - } - settingCompletionItem.insertText = new vscode.SnippetString(settingSnippet); - settingCompletionItem.documentation = new vscode.MarkdownString(setting.docString); - settingCompletionItem.kind = vscode.CompletionItemKind.Value; - completions.push(settingCompletionItem) - }); + // Convert [foo|bar|baz] or {foo|bar|baz} values to a dropdown placeholder + // ${1|foo,bar,baz|} + if (splunkSpec.DROPDOWN_PLACEHOLDER_REGEX.test(settingSnippet)) { + settingSnippet = settingSnippet.replace(/\|/g, ","); + settingSnippet = settingSnippet.replace(/\[|{/, "${1|"); + settingSnippet = settingSnippet.replace(/]|}/, "|}"); } - // return all completion items as array - return completions; + settingCompletionItem.insertText = new vscode.SnippetString( + settingSnippet, + ); + settingCompletionItem.documentation = new vscode.MarkdownString( + setting.docString, + ); + settingCompletionItem.kind = vscode.CompletionItemKind.Value; + completions.push(settingCompletionItem); + }); } - }); + // return all completion items as array + return completions; + }, + }, + ); } function provideSnippetCompletionItems(snippetPath) { - - // Get the currently open document - let currentDocument = path.basename(vscode.window.activeTextEditor.document.uri.fsPath); - vscode.languages.registerCompletionItemProvider({ pattern: `**/${currentDocument}`}, { - - provideCompletionItems() { - - let completions = []; - let snippets = JSON.parse(fs.readFileSync(snippetPath)); - for (let i in snippets) { - let snippet = snippets[i]; - let snippetCompletionItem = new vscode.CompletionItem(snippet.prefix); - let snippetString = snippet.body.join("\n"); - snippetCompletionItem.insertText = new vscode.SnippetString(snippetString); - snippetCompletionItem.documentation = new vscode.MarkdownString(snippet.description); - snippetCompletionItem.kind = vscode.CompletionItemKind.Snippet; - completions.push(snippetCompletionItem); - } - return completions + // Get the currently open document + let currentDocument = path.basename( + vscode.window.activeTextEditor.document.uri.fsPath, + ); + vscode.languages.registerCompletionItemProvider( + { pattern: `**/${currentDocument}` }, + { + provideCompletionItems() { + let completions = []; + let snippets = JSON.parse(fs.readFileSync(snippetPath)); + for (let i in snippets) { + let snippet = snippets[i]; + let snippetCompletionItem = new vscode.CompletionItem(snippet.prefix); + let snippetString = snippet.body.join("\n"); + snippetCompletionItem.insertText = new vscode.SnippetString( + snippetString, + ); + snippetCompletionItem.documentation = new vscode.MarkdownString( + snippet.description, + ); + snippetCompletionItem.kind = vscode.CompletionItemKind.Snippet; + completions.push(snippetCompletionItem); } - }); + return completions; + }, + }, + ); } function triggerDiagnostics(specConfig, document, diagnosticCollection) { - if(timeout) { - clearTimeout(timeout) - timeout = undefined - } - timeout = setTimeout(updateDiagnostics, 2000, specConfig, document, diagnosticCollection) + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } + timeout = setTimeout( + updateDiagnostics, + 2000, + specConfig, + document, + diagnosticCollection, + ); } function updateDiagnostics(specConfig, document, diagnosticCollection) { - let diagnostics = getDiagnostics(specConfig, document); - diagnosticCollection.set(document.uri, diagnostics); + let diagnostics = getDiagnostics(specConfig, document); + diagnosticCollection.set(document.uri, diagnostics); } function getDiagnostics(specConfig, document) { - - let diagnostics = [] - - // Make sure stanzas are valid - let docStanzas = getDocumentItems(document, splunkSpec.STANZA_REGEX) - docStanzas.forEach(stanza => { - if(!splunkSpec.isStanzaValid(specConfig, stanza.text)) { - let range = new vscode.Range(new vscode.Position(stanza.line, 0), new vscode.Position(stanza.line, stanza.text.length)) - let message = `Stanza ${stanza.text} does not seem to be a valid stanza.` - let severity = vscode.DiagnosticSeverity.Error - diagnostics.push(new vscode.Diagnostic(range, message, severity)) - } - }); - - // Make sure settings are valid - let docSettings = getDocumentItems(document, splunkSpec.SETTING_REGEX) - docSettings.forEach(setting => { - let parentStanza = getParentStanza(document, setting.line) - if(!splunkSpec.isSettingValid(specConfig, parentStanza, setting.text)) { - let range = new vscode.Range(new vscode.Position(setting.line, 0), new vscode.Position(setting.line, setting.text.length)) - let message = `Invalid key in stanza ${parentStanza} in ${path.basename(vscode.window.activeTextEditor.document.uri.fsPath)}, line ${setting.line + 1}: ${setting.text}.` - let severity = vscode.DiagnosticSeverity.Error - diagnostics.push(new vscode.Diagnostic(range, message, severity)) - } + let diagnostics = []; + + // Make sure stanzas are valid + let docStanzas = getDocumentItems(document, splunkSpec.STANZA_REGEX); + docStanzas.forEach((stanza) => { + if (!splunkSpec.isStanzaValid(specConfig, stanza.text)) { + let range = new vscode.Range( + new vscode.Position(stanza.line, 0), + new vscode.Position(stanza.line, stanza.text.length), + ); + let message = `Stanza ${stanza.text} does not seem to be a valid stanza.`; + let severity = vscode.DiagnosticSeverity.Error; + diagnostics.push(new vscode.Diagnostic(range, message, severity)); + } + }); + + // Make sure settings are valid + let docSettings = getDocumentItems(document, splunkSpec.SETTING_REGEX); + docSettings.forEach((setting) => { + let parentStanza = getParentStanza(document, setting.line); + if (!splunkSpec.isSettingValid(specConfig, parentStanza, setting.text)) { + let range = new vscode.Range( + new vscode.Position(setting.line, 0), + new vscode.Position(setting.line, setting.text.length), + ); + let message = `Invalid key in stanza ${parentStanza} in ${path.basename(vscode.window.activeTextEditor.document.uri.fsPath)}, line ${setting.line + 1}: ${setting.text}.`; + let severity = vscode.DiagnosticSeverity.Error; + diagnostics.push(new vscode.Diagnostic(range, message, severity)); + } + }); + + // Semantic linting suggestions + const semanticLintingEnabled = vscode.workspace + .getConfiguration("splunk") + .get("semanticLinting.enabled", true); + if (semanticLintingEnabled && extensionPath) { + const suggestions = semanticRules.evaluateDocument(extensionPath, document); + suggestions.forEach((suggestion) => { + let diagnostic = new vscode.Diagnostic( + suggestion.range, + suggestion.suggestion.message, + semanticRules.getSeverity(suggestion.suggestion.severity), + ); + diagnostic.code = suggestion.ruleId; + diagnostic.source = "splunk-semantic"; + diagnostics.push(diagnostic); }); + } - return diagnostics; + return diagnostics; } async function deactivate() { - if (spl2Client) { - return await spl2Client.deactivate(); - } + if (spl2Client) { + return await spl2Client.deactivate(); + } } -exports.deactivate = deactivate; \ No newline at end of file +exports.deactivate = deactivate; diff --git a/out/semanticRules.js b/out/semanticRules.js new file mode 100644 index 0000000..8262bcc --- /dev/null +++ b/out/semanticRules.js @@ -0,0 +1,227 @@ +"use strict"; +const path = require("path"); +const fs = require("fs"); +const vscode = require("vscode"); + +let semanticRulesCache = null; +const SETTING_REGEX = /^(?\w[\w\-\.]*)\s*=\s*(?.*)$/; +const STANZA_REGEX = /^\[(?[^\]]+)\]/; + +function loadSemanticRules(extensionPath) { + if (semanticRulesCache) { + return semanticRulesCache; + } + + const rulesPath = path.join(extensionPath, "resources", "semantic_rules.json"); + if (!fs.existsSync(rulesPath)) { + console.log("Semantic rules file not found:", rulesPath); + return null; + } + + try { + const content = fs.readFileSync(rulesPath, "utf-8"); + semanticRulesCache = JSON.parse(content); + return semanticRulesCache; + } catch (err) { + console.error("Error loading semantic rules:", err); + return null; + } +} + +function getConfFileName(document) { + return path.basename(document.uri.fsPath); +} + +function parseStanzaContext(document, stanzaLine) { + const context = { + stanzaName: "", + stanzaRange: null, + settings: {}, + settingLines: {} + }; + + const stanzaMatch = document.lineAt(stanzaLine).text.match(STANZA_REGEX); + if (!stanzaMatch) return null; + + context.stanzaName = stanzaMatch.groups.stanza; + context.stanzaRange = new vscode.Range(stanzaLine, 0, stanzaLine, document.lineAt(stanzaLine).text.length); + + for (let i = stanzaLine + 1; i < document.lineCount; i++) { + const lineText = document.lineAt(i).text.trim(); + + if (lineText.startsWith("[")) break; + if (lineText === "" || lineText.startsWith("#")) continue; + + const settingMatch = lineText.match(SETTING_REGEX); + if (settingMatch) { + const name = settingMatch.groups.setting; + const value = settingMatch.groups.value.trim(); + context.settings[name] = value; + context.settingLines[name] = i; + } + } + + return context; +} + +function getAllStanzaContexts(document) { + const contexts = []; + for (let i = 0; i < document.lineCount; i++) { + if (document.lineAt(i).text.trim().startsWith("[")) { + const ctx = parseStanzaContext(document, i); + if (ctx) contexts.push(ctx); + } + } + return contexts; +} + +function matchesTrigger(trigger, stanzaContext) { + if (trigger.stanza_pattern) { + const regex = new RegExp(trigger.stanza_pattern); + if (!regex.test("[" + stanzaContext.stanzaName + "]")) { + return false; + } + } + + if (trigger.settings) { + for (const [key, expectedValue] of Object.entries(trigger.settings)) { + const actualValue = stanzaContext.settings[key]; + if (actualValue === undefined) return false; + if (String(actualValue).toLowerCase() !== String(expectedValue).toLowerCase()) { + return false; + } + } + } + + if (trigger.settings_regex) { + for (const [key, pattern] of Object.entries(trigger.settings_regex)) { + const actualValue = stanzaContext.settings[key]; + if (actualValue === undefined) return false; + let regex; + try { + regex = new RegExp(pattern); + } catch (err) { + console.error(`Invalid settings_regex pattern for ${key}: ${pattern}`, err); + return false; + } + if (!regex.test(String(actualValue))) { + return false; + } + } + } + + if (trigger.setting_exists) { + if (!(trigger.setting_exists in stanzaContext.settings)) { + return false; + } + } + + if (trigger.setting_missing) { + if (trigger.setting_missing in stanzaContext.settings) { + return false; + } + } + + if (trigger.with_any) { + const hasAny = trigger.with_any.some(s => s in stanzaContext.settings); + if (!hasAny) return false; + } + + if (trigger.with_all) { + const hasAll = trigger.with_all.every(s => s in stanzaContext.settings); + if (!hasAll) return false; + } + + if (trigger.without) { + const hasNone = trigger.without.every(s => !(s in stanzaContext.settings)); + if (!hasNone) return false; + } + + if (trigger.value_less_than) { + for (const [key, threshold] of Object.entries(trigger.value_less_than)) { + const val = parseFloat(stanzaContext.settings[key]); + if (isNaN(val) || val >= threshold) return false; + } + } + + if (trigger.value_greater_than) { + for (const [key, threshold] of Object.entries(trigger.value_greater_than)) { + const val = parseFloat(stanzaContext.settings[key]); + if (isNaN(val) || val <= threshold) return false; + } + } + + return true; +} + +function getSeverity(severityStr) { + switch (severityStr) { + case "error": return vscode.DiagnosticSeverity.Error; + case "warning": return vscode.DiagnosticSeverity.Warning; + case "information": return vscode.DiagnosticSeverity.Information; + case "hint": return vscode.DiagnosticSeverity.Hint; + default: return vscode.DiagnosticSeverity.Information; + } +} + +function evaluateDocument(extensionPath, document) { + const suggestions = []; + const rules = loadSemanticRules(extensionPath); + if (!rules) return suggestions; + + const confFile = getConfFileName(document); + const confRules = rules.rules[confFile]; + if (!confRules) return suggestions; + + const stanzaContexts = getAllStanzaContexts(document); + + for (const ctx of stanzaContexts) { + for (const [categoryName, category] of Object.entries(confRules)) { + if (!category.patterns) continue; + + for (const pattern of category.patterns) { + if (pattern.enabled === false) continue; + + if (matchesTrigger(pattern.trigger, ctx)) { + suggestions.push({ + ruleId: pattern.id, + range: ctx.stanzaRange, + stanzaContext: ctx, + suggestion: pattern.suggestion, + fix: pattern.fix + }); + } + } + } + } + + return suggestions; +} + +function getRuleById(extensionPath, ruleId) { + const rules = loadSemanticRules(extensionPath); + if (!rules) return null; + + for (const [confFile, confRules] of Object.entries(rules.rules)) { + for (const [categoryName, category] of Object.entries(confRules)) { + if (!category.patterns) continue; + for (const pattern of category.patterns) { + if (pattern.id === ruleId) { + return pattern; + } + } + } + } + return null; +} + +function clearCache() { + semanticRulesCache = null; +} + +exports.loadSemanticRules = loadSemanticRules; +exports.evaluateDocument = evaluateDocument; +exports.getRuleById = getRuleById; +exports.getSeverity = getSeverity; +exports.clearCache = clearCache; +exports.parseStanzaContext = parseStanzaContext; From b99817c9440d5492124fdf1d412c2f0893cb734f Mon Sep 17 00:00:00 2001 From: Jason Green Date: Wed, 20 May 2026 14:46:43 -0500 Subject: [PATCH 2/2] feat: Add crcSalt hint rule for wildcard monitor inputs --- resources/semantic_rules.json | 459 ++++++++++++++++++++++++++++++++++ 1 file changed, 459 insertions(+) create mode 100644 resources/semantic_rules.json diff --git a/resources/semantic_rules.json b/resources/semantic_rules.json new file mode 100644 index 0000000..4ed647a --- /dev/null +++ b/resources/semantic_rules.json @@ -0,0 +1,459 @@ +{ + "$schema": "./semantic_rules.schema.json", + "version": "1.0.0", + "rules": { + "props.conf": { + "line_breaking": { + "description": "Rules for optimal event line breaking configuration", + "patterns": [ + { + "id": "linemerge_performance", + "trigger": { + "settings": { "SHOULD_LINEMERGE": "true" }, + "with_any": ["BREAK_ONLY_BEFORE", "MUST_BREAK_AFTER"], + "without": ["LINE_BREAKER_LOOKBEHIND"] + }, + "suggestion": { + "severity": "information", + "title": "Consider LINE_BREAKER for better performance", + "message": "Using LINE_BREAKER instead of SHOULD_LINEMERGE provides a significant boost to processing speed. SHOULD_LINEMERGE reassembles individual lines into events, while LINE_BREAKER directly delimits events.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Data/Configureeventlinebreaking" + }, + "fix": { + "type": "multi", + "changes": [ + { "action": "set", "setting": "SHOULD_LINEMERGE", "value": "false" }, + { "action": "add", "setting": "LINE_BREAKER", "value": "([\\r\\n]+)" } + ] + } + }, + { + "id": "missing_linemerge_false", + "trigger": { + "setting_exists": "LINE_BREAKER", + "without": ["SHOULD_LINEMERGE"] + }, + "suggestion": { + "severity": "warning", + "title": "Add SHOULD_LINEMERGE = false", + "message": "When using a custom LINE_BREAKER, you should explicitly set SHOULD_LINEMERGE = false to prevent Splunk from attempting to reassemble lines after breaking.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Data/Configureeventlinebreaking" + }, + "fix": { + "type": "add", + "changes": [ + { "action": "add", "setting": "SHOULD_LINEMERGE", "value": "false" } + ] + } + }, + { + "id": "linemerge_without_break", + "trigger": { + "settings": { "SHOULD_LINEMERGE": "true" }, + "without": ["BREAK_ONLY_BEFORE", "MUST_BREAK_AFTER", "BREAK_ONLY_BEFORE_DATE", "LINE_BREAKER"] + }, + "suggestion": { + "severity": "information", + "title": "Consider adding event boundary settings", + "message": "SHOULD_LINEMERGE is enabled but no event boundary pattern is defined. Consider adding BREAK_ONLY_BEFORE, MUST_BREAK_AFTER, or BREAK_ONLY_BEFORE_DATE to define how events should be merged.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Data/Configureeventlinebreaking" + } + } + ] + }, + "timestamp": { + "description": "Rules for timestamp extraction configuration", + "patterns": [ + { + "id": "datetime_none_warning", + "trigger": { + "settings": { "DATETIME_CONFIG": "NONE" }, + "without": ["LINE_BREAKER", "SHOULD_LINEMERGE", "BREAK_ONLY_BEFORE"] + }, + "suggestion": { + "severity": "warning", + "title": "DATETIME_CONFIG = NONE without event boundaries", + "message": "When DATETIME_CONFIG is set to NONE, Splunk cannot use timestamps to determine event boundaries. Ensure you have configured LINE_BREAKER or other event boundary settings.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Data/Configuretimestamprecognition" + } + }, + { + "id": "time_format_without_prefix", + "trigger": { + "setting_exists": "TIME_FORMAT", + "without": ["TIME_PREFIX"] + }, + "suggestion": { + "severity": "hint", + "title": "Consider adding TIME_PREFIX", + "message": "TIME_FORMAT is set but TIME_PREFIX is not defined. If your timestamp doesn't appear at the start of the event, adding TIME_PREFIX can improve timestamp extraction accuracy and performance.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Data/Configuretimestamprecognition" + } + }, + { + "id": "max_timestamp_lookahead_low", + "trigger": { + "value_less_than": { "MAX_TIMESTAMP_LOOKAHEAD": 10 } + }, + "suggestion": { + "severity": "warning", + "title": "MAX_TIMESTAMP_LOOKAHEAD may be too low", + "message": "MAX_TIMESTAMP_LOOKAHEAD is set to a very low value. This limits how far into each event Splunk looks for a timestamp. If timestamps are not being extracted correctly, consider increasing this value.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Data/Configuretimestamprecognition" + } + } + ] + }, + "truncation": { + "description": "Rules for event truncation settings", + "patterns": [ + { + "id": "truncate_warning", + "trigger": { + "value_less_than": { "TRUNCATE": 10000 } + }, + "suggestion": { + "severity": "warning", + "title": "Low TRUNCATE value may cause data loss", + "message": "TRUNCATE is set below 10000 bytes. Events longer than this will be truncated, potentially losing important data. The default is 10000. Only lower this if you're certain your events are smaller.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Data/Configureeventlinebreaking" + } + }, + { + "id": "truncate_very_high", + "trigger": { + "value_greater_than": { "TRUNCATE": 100000 } + }, + "suggestion": { + "severity": "information", + "title": "High TRUNCATE value may impact performance", + "message": "TRUNCATE is set above 100KB. Very large events can impact indexing performance and search speed. Ensure this is necessary for your data.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Data/Configureeventlinebreaking" + } + } + ] + }, + "field_extraction": { + "description": "Rules for field extraction configuration", + "patterns": [ + { + "id": "kv_mode_json_with_indexed", + "trigger": { + "settings": { "KV_MODE": "json" }, + "setting_exists": "INDEXED_EXTRACTIONS" + }, + "suggestion": { + "severity": "warning", + "title": "Duplicate JSON extraction", + "message": "Both KV_MODE = json and INDEXED_EXTRACTIONS are set. This causes JSON fields to be extracted twice: once at index time and again at search time, impacting performance.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Data/Configureindex-timefieldextraction" + }, + "fix": { + "type": "remove", + "changes": [ + { "action": "remove", "setting": "KV_MODE" } + ] + } + }, + { + "id": "extract_without_report", + "trigger": { + "setting_exists": "EXTRACT", + "without": ["REPORT"] + }, + "suggestion": { + "severity": "hint", + "title": "Consider using REPORT for reusable extractions", + "message": "Inline EXTRACT is defined. For complex or reusable extractions, consider using REPORT- with a transforms.conf stanza instead.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Knowledge/Configureadvancedextractionswithfieldtransforms" + } + } + ] + } + }, + "inputs.conf": { + "file_monitoring": { + "description": "Rules for file and directory monitoring", + "patterns": [ + { + "id": "recursive_no_filter", + "trigger": { + "stanza_pattern": "^\\[monitor://", + "without": ["whitelist", "blacklist", "allowlist", "denylist"] + }, + "suggestion": { + "severity": "information", + "title": "Consider adding file filters", + "message": "This monitor input has no file filters (allowlist/denylist). For directories, consider adding filters to avoid indexing unwanted files like .gz, .tmp, or backup files.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Data/Specifyinputpathswithwildcards" + } + }, + { + "id": "missing_sourcetype", + "trigger": { + "stanza_pattern": "^\\[monitor://", + "without": ["sourcetype"] + }, + "suggestion": { + "severity": "information", + "title": "Consider setting explicit sourcetype", + "message": "No sourcetype is defined for this input. Splunk will attempt to auto-detect the sourcetype, which may not always be accurate. Consider setting an explicit sourcetype.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Data/Listofpretrainedsourcetypes" + } + }, + { + "id": "missing_index", + "trigger": { + "stanza_pattern": "^\\[(monitor|tcp|udp|script)://", + "without": ["index"] + }, + "suggestion": { + "severity": "hint", + "title": "Consider setting explicit index", + "message": "No index is specified. Data will go to the default index. For better data organization and access control, consider specifying an index.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Indexer/Setupmultipleindexes" + } + }, + { + "id": "crcsalt_with_initcrclen", + "trigger": { + "with_all": ["crcSalt", "initCrcLength"] + }, + "suggestion": { + "severity": "warning", + "title": "crcSalt and initCrcLength both set", + "message": "Both crcSalt and initCrcLength are configured. crcSalt= is typically sufficient for handling files with identical content. Using both may be unnecessary.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Data/Handleduplicatedata" + } + }, + { + "id": "monitor_wildcard_no_crcsalt", + "trigger": { + "stanza_pattern": "^\\[monitor://.*\\*\\]$", + "without": ["crcSalt"] + }, + "suggestion": { + "severity": "hint", + "title": "Consider adding crcSalt for wildcard monitor", + "message": "This monitor path ends with a wildcard and may match multiple files with similar content (e.g., access_log, access_log.1). Adding crcSalt = ensures each file has a unique CRC, preventing Splunk from treating files with identical headers as duplicates.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Admin/Inputsconf" + }, + "fix": { + "type": "add", + "changes": [ + { "action": "add", "setting": "crcSalt", "value": "" } + ] + } + } + ] + }, + "network_inputs": { + "description": "Rules for TCP/UDP network inputs", + "patterns": [ + { + "id": "tcp_no_connection_host", + "trigger": { + "stanza_pattern": "^\\[tcp://", + "without": ["connection_host"] + }, + "suggestion": { + "severity": "hint", + "title": "Consider setting connection_host", + "message": "No connection_host is set for this TCP input. By default, the host field will be set to the IP address. Consider setting connection_host = dns to use DNS names instead.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Data/Monitornetworkports" + } + }, + { + "id": "udp_no_no_appending_timestamp", + "trigger": { + "stanza_pattern": "^\\[udp://", + "without": ["no_appending_timestamp"] + }, + "suggestion": { + "severity": "hint", + "title": "Consider no_appending_timestamp setting", + "message": "For UDP inputs, Splunk appends a timestamp to each packet by default. If your data already has timestamps, consider setting no_appending_timestamp = true.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Data/Monitornetworkports" + } + } + ] + } + }, + "transforms.conf": { + "field_extraction": { + "description": "Rules for transform-based field extraction", + "patterns": [ + { + "id": "regex_no_format", + "trigger": { + "setting_exists": "REGEX", + "without": ["FORMAT"] + }, + "suggestion": { + "severity": "hint", + "title": "Consider adding FORMAT for explicit field naming", + "message": "REGEX is defined without FORMAT. Field names will be derived from named capture groups in the regex. Adding FORMAT explicitly documents the field names and can make the configuration clearer.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Knowledge/Configureadvancedextractionswithfieldtransforms" + } + }, + { + "id": "lookup_no_default", + "trigger": { + "setting_exists": "filename", + "stanza_pattern": "^\\[(?!default)", + "without": ["default_match"] + }, + "suggestion": { + "severity": "hint", + "title": "Consider adding default_match", + "message": "This lookup has no default_match configured. If a lookup key is not found, no fields will be added. Consider setting default_match to provide fallback values.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Knowledge/Configurealookup" + } + }, + { + "id": "lookup_case_sensitive", + "trigger": { + "setting_exists": "filename", + "without": ["case_sensitive_match"] + }, + "suggestion": { + "severity": "hint", + "title": "Consider case_sensitive_match setting", + "message": "Lookup case sensitivity is not explicitly set. By default, lookups are case-sensitive. If your data has inconsistent casing, consider setting case_sensitive_match = false.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Knowledge/Configurealookup" + } + } + ] + }, + "routing": { + "description": "Rules for event routing configuration", + "patterns": [ + { + "id": "dest_key_queue", + "trigger": { + "settings": { "DEST_KEY": "queue" }, + "without": ["FORMAT"] + }, + "suggestion": { + "severity": "warning", + "title": "DEST_KEY = queue requires FORMAT", + "message": "When routing events with DEST_KEY = queue, you must specify FORMAT to indicate the destination queue (indexQueue or nullQueue).", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Data/Routeandfilterdatawithprops" + }, + "fix": { + "type": "add", + "changes": [ + { "action": "add", "setting": "FORMAT", "value": "nullQueue" } + ] + } + } + ] + } + }, + "outputs.conf": { + "forwarding": { + "description": "Rules for data forwarding configuration", + "patterns": [ + { + "id": "tcpout_no_server", + "trigger": { + "stanza_pattern": "^\\[tcpout:", + "without": ["server"] + }, + "suggestion": { + "severity": "error", + "title": "Missing server setting", + "message": "This tcpout group has no server defined. At least one server must be specified for forwarding to work.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Forwarding/Configureforwardingwithoutputs.conf" + } + }, + { + "id": "ssl_without_cert", + "trigger": { + "settings": { "useSSL": "true" }, + "without": ["sslCertPath"] + }, + "suggestion": { + "severity": "warning", + "title": "SSL enabled without certificate path", + "message": "SSL is enabled but no certificate path is specified. Ensure sslCertPath and related SSL settings are properly configured.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Security/ConfigureSplunkforwardingtousesignedcertificates" + } + } + ] + } + }, + "server.conf": { + "clustering": { + "description": "Rules for clustering configuration", + "patterns": [ + { + "id": "replication_factor_one", + "trigger": { + "settings": { "replication_factor": "1" } + }, + "suggestion": { + "severity": "warning", + "title": "Replication factor of 1 provides no redundancy", + "message": "With replication_factor = 1, there is no data redundancy. If a peer fails, data may be lost. Consider increasing this for production environments.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Indexer/Thereplicationfactor" + } + } + ] + } + }, + "indexes.conf": { + "storage": { + "description": "Rules for index storage configuration", + "patterns": [ + { + "id": "homepath_same_as_coldpath", + "trigger": { + "settings_regex": { + "homePath": ".*", + "coldPath": ".*" + } + }, + "suggestion": { + "severity": "information", + "title": "Consider separating hot/warm and cold storage", + "message": "For optimal performance, consider placing homePath (hot/warm) on faster storage (SSD) and coldPath on slower, cheaper storage.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Indexer/HowSplunkstoresindexes" + } + }, + { + "id": "no_maxdatasize", + "trigger": { + "stanza_pattern": "^\\[(?!default|volume:)", + "without": ["maxDataSize"] + }, + "suggestion": { + "severity": "hint", + "title": "Consider setting maxDataSize", + "message": "maxDataSize is not explicitly set. The default is 'auto' which sets bucket size to 750MB. For high-volume indexes, consider 'auto_high_volume' for 10GB buckets.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Indexer/Configureindexstorage" + } + } + ] + }, + "retention": { + "description": "Rules for data retention configuration", + "patterns": [ + { + "id": "no_retention_policy", + "trigger": { + "stanza_pattern": "^\\[(?!default|volume:)", + "without": ["frozenTimePeriodInSecs", "maxTotalDataSizeMB"] + }, + "suggestion": { + "severity": "information", + "title": "Consider setting retention policy", + "message": "No explicit retention policy is set. Data will be retained based on default settings. Consider setting frozenTimePeriodInSecs or maxTotalDataSizeMB to manage storage.", + "documentation": "https://docs.splunk.com/Documentation/Splunk/latest/Indexer/Setaretirementandarchivingpolicy" + } + } + ] + } + } + } +}