diff --git a/.github/actions/log_to_splunk/LICENSE b/.github/actions/log_to_splunk/LICENSE new file mode 100644 index 0000000..4fc208e --- /dev/null +++ b/.github/actions/log_to_splunk/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Splunk GitHub + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.github/workflows/appinspect_cli.yml b/.github/workflows/appinspect_cli.yml index 06517ad..d80e9ed 100644 --- a/.github/workflows/appinspect_cli.yml +++ b/.github/workflows/appinspect_cli.yml @@ -35,10 +35,6 @@ jobs: with: args: build - - name: Update Permissions - run: | - chmod +x ./.github/actions/appinspect_cli/entrypoint.sh - - name: Update Version Number run: | old_str="X.Y.Z" @@ -46,7 +42,11 @@ jobs: sed -i "s/$old_str/$new_str/g" package.json sed -i "s/$old_str/$new_str/g" ./github_app_for_splunk/default/app.conf + - name: Build Package + run: COPYFILE_DISABLE=1 tar -cvzf package.tar.gz ./github_app_for_splunk + - name: Run App Inspect CLI - uses: ./.github/actions/appinspect_cli + uses: splunk/appinspect-cli-action@v1 with: - app-path: github_app_for_splunk/ + app_path: package.tar.gz + included_tags: cloud, splunk_appinspect diff --git a/.github/workflows/log_to_splunk.yml b/.github/workflows/log_to_splunk.yml index 3cf957e..1a708f1 100644 --- a/.github/workflows/log_to_splunk.yml +++ b/.github/workflows/log_to_splunk.yml @@ -26,7 +26,7 @@ jobs: if: ${{ always() }} uses: ./.github/actions/log_to_splunk with: - splunk-url: http://54.189.34.108:8088/ + splunk-url: ${{ secrets.HEC_URL }} hec-token: ${{ secrets.HEC_TOKEN }} github-token: ${{ secrets.API_TOKEN }} workflowID: ${{ env.triggerID }} diff --git a/docs/ghes_syslog_setup.MD b/docs/ghes_syslog_setup.MD index f084386..7e32826 100644 --- a/docs/ghes_syslog_setup.MD +++ b/docs/ghes_syslog_setup.MD @@ -1,25 +1,3 @@ # Sending GitHub Enterprise Server Logs to Splunk -GitHub Enterprise Server comes with syslog-ng built in to send data to platforms like Splunk: https://docs.github.com/en/enterprise-server@3.3/admin/user-management/monitoring-activity-in-your-enterprise/log-forwarding. Following those directions will allow you to easily onboard logs to Splunk. However, The GitHub App for Splunk comes with enhancements for those logs that will allow you to search more efficently. - -## Sources and Transformations - - The syslog feed from GitHub Enterprise Server contains ALL application logs including audit logs, web server logs, database logs, etc. Being able to differentiate the logs is critical. This app includes the ability to overwrite the source of events with the log type out of the box. However, for this to happen, you must use the sourcetype of `GithubEnterpriseServerLog` or duplicate that stanza from the default `props.conf` file into a custom stanza in your local copy. When setting up a TCP input you have the ability to force that specific sourcetype. This will enable easy filtering of log files to their specific process. - -## Default `props.conf` - -``` -[GithubEnterpriseServerLog] -DATETIME_CONFIG = -LINE_BREAKER = ([\r\n]+) -NO_BINARY_CHECK = true -category = Application -pulldown_type = true -TIME_FORMAT = -TZ = -EXTRACT-audit_event = github_audit\[\d+\]\:\s(?.*) -EXTRACT-audit_fields = \"(?<_KEY_1>.*?)\"\:\"*(?<_VAL_1>.*?)\"*, -EXTRACT-github_log_type = \d+\:\d+\:\d+\s\d+\-\d+\-\d+\-\d+\s(?.*?)\: -EXTRACT-github_document_id = \"_document_id\"\:\"(?.*?)\" -FIELDALIAS-source = github_log_type AS source -``` +GitHub Enterprise Server comes with syslog-ng built in to send data to platforms like Splunk: https://docs.github.com/en/enterprise-server@3.3/admin/user-management/monitoring-activity-in-your-enterprise/log-forwarding. Following those directions will allow you to easily onboard logs to Splunk. To take advantage of GitHub Enterprise Server's built in syslog, you can direct GHES to a Splunk Connect for Syslog endpoint which has built in capability to parse GitHub Enterprise Server logs. Pairing that with the [Splunk Add-On for GitHub](https://splunkbase.splunk.com/app/6254/) will enable proper field extractions and field aliases. diff --git a/github_app_for_splunk/appserver/static/custom.css b/github_app_for_splunk/appserver/static/custom.css new file mode 100644 index 0000000..697e6d4 --- /dev/null +++ b/github_app_for_splunk/appserver/static/custom.css @@ -0,0 +1,24 @@ +/* custom.css */ + +/* Define icon styles */ + +td.icon { + text-align: center; +} + +td.icon i { + font-size: 30px; + text-shadow: 1px 1px #aaa; +} + +td.icon .failure { + color: red; +} + +td.icon .in_progress { + color: yellow; +} + +td.icon .success { + color: green; +} diff --git a/github_app_for_splunk/appserver/static/example_customtables.js b/github_app_for_splunk/appserver/static/example_customtables.js new file mode 100644 index 0000000..ca9f045 --- /dev/null +++ b/github_app_for_splunk/appserver/static/example_customtables.js @@ -0,0 +1,130 @@ +require([ + "underscore", + "splunkjs/mvc", + "splunkjs/mvc/searchmanager", + "splunkjs/mvc/tableview", + "splunkjs/mvc/simplexml/ready!" +], function( + _, + mvc, + SearchManager, + TableView +) { + + // Set up search managers + var search2 = new SearchManager({ + id: "search2", + preview: true, + cache: true, + search: "index=github_webhook \"workflow_run.name\"=\"*\" | spath \"repository.full_name\" | search repository.full_name=* | eval started=if(action=\"requested\",_time,NULL), completed=if(action=\"completed\",_time, NULL), created=round(strptime('workflow_run.created_at',\"%Y-%m-%dT%H:%M:%SZ\")) | stats latest(created) as created, latest(started) as started, latest(completed) as completed, latest(duration) as duration, latest(workflow_run.conclusion) as workflow_run.conclusion by repository.full_name,workflow_run.name,workflow_run.id | eval started=if(isnull(started), created, started) | eval duration=if(isnotnull(completed),tostring(completed-started,\"Duration\"),\"In Progress\") | rename workflow_run.conclusion as status, repository.full_name as \"Repository Name\", workflow_run.name as \"Workflow Name\", workflow_run.id as \"Run ID\" | table status, \"Repository Name\", \"Workflow Name\", \"Run ID\", duration,completed|sort completed|fields - completed", + earliest_time: mvc.tokenSafe("$field1.earliest$"), + latest_time: mvc.tokenSafe("$field1.latest$") + }); + + // Create a table for a custom row expander + var mycustomrowtable = new TableView({ + id: "table-customrow", + managerid: "search2", + drilldown: "none", + drilldownRedirect: false, + el: $("#table-customrow") + }); + + // Define icons for the custom table cell + var ICONS = { + failure: "error", + in_progress: "question-circle", + success: "check-circle" + }; + + // Use the BaseCellRenderer class to create a custom table cell renderer + var CustomCellRenderer = TableView.BaseCellRenderer.extend({ + canRender: function(cellData) { + // This method returns "true" for the "range" field + return cellData.field === "status"; + }, + + // This render function only works when canRender returns "true" + render: function($td, cellData) { + console.log("cellData: ", cellData); + + var icon = "question"; + if(ICONS.hasOwnProperty(cellData.value)) { + icon = ICONS[cellData.value]; + } + $td.addClass("icon").html(_.template('', { + icon: icon, + status: cellData.value + })); + } + }); + + // Use the BasicRowRenderer class to create a custom table row renderer + var CustomRowRenderer = TableView.BaseRowExpansionRenderer.extend({ + canRender: function(rowData) { + console.log("RowData: ", rowData); + return true; + }, + + initialize: function(args){ + this._searchManager = new SearchManager({ + id: 'details-search-manager', + preview: false + }); + this._TableView = new TableView({ + id: 'ResultsTable', + managerid: 'details-search-manager', + drilldown: "all", + drilldownRedirect: true + }); + }, + + render: function($container, rowData) { + // Print the rowData object to the console + console.log("RowData: ", rowData); + + var repoNameCell = _(rowData.cells).find(function (cell) { + return cell.field === 'Repository Name'; + }); + + + var workflowName = _(rowData.cells).find(function (cell) { + return cell.field === 'Workflow Name'; + }); + + var workflowIDCell = _(rowData.cells).find(function (cell) { + return cell.field === 'Run ID'; + }); + + this._TableView.on("click", function(e) { + e.preventDefault(); + console.log(e); + window.open("/app/github_app_for_splunk/workflow_details?form.workflow_id="+workflowIDCell.value+"&form.repoName="+repoNameCell.value+"&form.workflowName="+workflowName.value+"&form.field1.earliest=-24h%40h&form.field1.latest=now&form.timeRange.earliest=-30d%40d&form.timeRange.latest=now&form.workflowCount=25",'_self'); + }); + + this._searchManager.set({ search: 'index=github_webhook (workflow_run.id='+workflowIDCell.value+' OR workflow_job.run_id='+workflowIDCell.value+') | eval started=if(action=="requested", _time, null), completed=if(action=="completed", _time,null) | stats latest(workflow_run.conclusion) as Status, earliest(started) as Started, latest(completed) as Completed, latest(workflow_run.head_branch) as Branch, latest(workflow_run.event) as Trigger | eval Duration=tostring(Completed-Started, "Duration") | eval Started=strftime(Started,"%Y-%m-%dT%H:%M:%S"), Completed=strftime(Completed,"%Y-%m-%dT%H:%M:%S")| fields Status, Started, Completed, Duration, Branch, Trigger | eval Details="Click here for Workflow Details" | transpose|rename column AS Details| rename "row 1" AS values'}); + // $container is the jquery object where we can put out content. + // In this case we will render our chart and add it to the $container + $container.append(this._TableView.render().el); + } + }); + + // Create an instance of the custom cell renderer, + // add it to the table, and render the table + var myCellRenderer = new CustomCellRenderer(); + mycustomrowtable.addCellRenderer(myCellRenderer); + mycustomrowtable.render(); + + // Create an instance of the custom row renderer, + // add it to the table, and render the table + var myRowRenderer = new CustomRowRenderer(); + mycustomrowtable.addRowExpansionRenderer(myRowRenderer); + mycustomrowtable.render(); + + mycustomrowtable.on("click", function(e) { + e.preventDefault(); + console.log(e.data); + window.open("/app/github_app_for_splunk/workflow_details?form.repoName="+e.data["row.repository.full_name"]+"&form.workflowName="+e.data["row.workflow_job.name"]+"&form.field1.earliest=-24h%40h&form.field1.latest=now&form.timeRange.earliest=-30d%40d&form.timeRange.latest=now&form.workflowCount=25",'_blank'); + }); + +}); diff --git a/github_app_for_splunk/appserver/static/tabs.css b/github_app_for_splunk/appserver/static/tabs.css index 3e61043..4d7705b 100644 --- a/github_app_for_splunk/appserver/static/tabs.css +++ b/github_app_for_splunk/appserver/static/tabs.css @@ -17,3 +17,26 @@ border-top: 0px; } +/** + * This fixes the issue where the tab focus looks weird. + */ + +.nav-tabs { + background: #212527; +} + +.nav-tabs > li > a { + color: #FFF; +} + +.nav-tabs > li > a:focus { + box-shadow: none !important; +} + +.nav-tabs > li:focus-within:after { + box-shadow: inset -2px 2px 3px rgba(82, 168, 236, .5); +} + +.nav-tabs > li:focus-within:before { + box-shadow: inset 3px 2px 3px rgba(82, 168, 236, .5); +} diff --git a/github_app_for_splunk/appserver/static/tabs.js b/github_app_for_splunk/appserver/static/tabs.js index 32bea79..1858339 100644 --- a/github_app_for_splunk/appserver/static/tabs.js +++ b/github_app_for_splunk/appserver/static/tabs.js @@ -1,178 +1,241 @@ require(['jquery','underscore','splunkjs/mvc', 'bootstrap.tab', 'splunkjs/mvc/simplexml/ready!'], - function($, _, mvc){ - - var tabsInitialzed = []; - - /** - * The below defines the tab handling logic. - */ - - // The normal, auto-magical Bootstrap tab processing doesn't work for us since it requires a particular - // layout of HTML that we cannot use without converting the view entirely to simpleXML. So, we are - // going to handle it ourselves. - var hideTabTargets = function(){ - - var tabs = $('a[data-elements]'); - - // Go through each toggle tab - for(var c = 0; c < tabs.length; c++){ - - // Hide the targets associated with the tab - var targets = $(tabs[c]).data("elements").split(","); - - for(var d = 0; d < targets.length; d++){ - $('#' + targets[d], this.$el).hide(); - } - } - }; - - var rerenderPanels = function(row_id, force){ - - // Set a default argument for dont_rerender_until_needed - if( typeof force === 'undefined'){ - force = true; - } - - // Don't do both if the panel was already rendered - if( !force && _.contains(tabsInitialzed, row_id) ){ - return; - } - - // Get the elements so that we can find the components to re-render - var elements = $('#' + row_id + ' .dashboard-element'); - - // Iterate the list and re-render the components so that they fill the screen - for(var d = 0; d < elements.length; d++){ - - // Determine if this is re-sizable - if( $('#' + row_id + ' .ui-resizable').length > 0){ - - var component = mvc.Components.get(elements[d].id); - - if(component){ - component.render(); - } - } - } - - // Remember that we initialized this tab - tabsInitialzed.push(row_id); - }; - - var selectTab = function (e) { - - // Stop if the tabs have no elements - if( $(e.target).data("elements") === undefined ){ - console.warn("Yikes, the clicked tab has no elements to hide!"); - return; - } - - // Get the IDs that we should enable for this tab - var toToggle = $(e.target).data("elements").split(","); - - // Hide the tab content by default - hideTabTargets(); - - // Now show this tabs toggle elements - for(var c = 0; c < toToggle.length; c++){ - - // Show the items - $('#' + toToggle[c], this.$el).show(); - - // Re-render the panels under the item if necessary - rerenderPanels(toToggle[c]); - } - - }; - - // Wire up the function to show the appropriate tab - $('a[data-toggle="tab"]').on('shown', selectTab); - - // Show the first tab - $('.toggle-tab').first().trigger('shown'); - - // Make the tabs into tabs - $('#tabs', this.$el).tab(); - + function($, _, mvc){ + + var tabsInitialzed = []; + + /** + * The below defines the tab handling logic. + */ + + /** + * This hides the content associated with the tabs. + * + * The normal, auto-magical Bootstrap tab processing doesn't work for us since it requires a particular + * layout of HTML that we cannot use without converting the view entirely to simpleXML. So, we are + * going to handle it ourselves. + * @param {string} tabSetClass the + */ + var hideTabTargets = function(tabSetClass) { + + var tabs = $('a[data-elements]'); + + // If we are only applying this to a particular set of tabs, then limit the selector accordingly + if (typeof tabSetClass !== 'undefined' && tabSetClass) { + tabs = $('a.' + tabSetClass + '[data-elements]'); + } + + // Go through each toggle tab + for (var c = 0; c < tabs.length; c++) { + + // Hide the targets associated with the tab + var targets = $(tabs[c]).data("elements").split(","); + + for (var d = 0; d < targets.length; d++) { + $('#' + targets[d], this.$el).hide(); + } + } + }; + + /** + * Force a re-render of the panels with the given row ID. + * + * @param {string} row_id The ID of the row to force a rerender on + * @param {bool} force Force the tab to re-render even if it was already rendered once (defaults to true) + */ + var rerenderPanels = function(row_id, force){ + + // Set a default argument for dont_rerender_until_needed + if( typeof force === 'undefined'){ + force = true; + } + + // Don't do both if the panel was already rendered + if( !force && _.contains(tabsInitialzed, row_id) ){ + return; + } + + // Get the elements so that we can find the components to re-render + var elements = $('#' + row_id + ' .dashboard-element'); + + // Iterate the list and re-render the components so that they fill the screen + for(var d = 0; d < elements.length; d++){ + + // Determine if this is re-sizable + if( $('#' + row_id + ' .ui-resizable').length > 0){ + + var component = mvc.Components.get(elements[d].id); + + if(component){ + component.render(); + } + } + } + + // Remember that we initialized this tab + tabsInitialzed.push(row_id); + }; + + /** + * Handles the selection of a partiular tab. + * + * @param {*} e + */ + var selectTab = function (e) { + console.log("selectTab"); + // Update which tab is considered active + $('#tabs > li.active').removeClass("active"); + $(e.target).closest("li").addClass("active"); + + // clearTabControlTokens(); + setActiveTabToken(); + + // Stop if the tabs have no elements + if( $(e.target).data("elements") === undefined ){ + console.warn("Yikes, the clicked tab has no elements to hide!"); + return; + } + + // Determine if the set of tabs has a restriction on the classes to manipulate + var tabSet = null; + + if ($(e.target).data("tab-set") !== undefined) { + tabSet = $(e.target).data("tab-set"); + } + + // Get the IDs that we should enable for this tab + var toToggle = $(e.target).data("elements").split(","); + + // Hide the tab content by default + hideTabTargets(tabSet); + + // Now show this tabs toggle elements + for(var c = 0; c < toToggle.length; c++){ + + // Show the items + $('#' + toToggle[c], this.$el).show(); + + // Re-render the panels under the item if necessary + rerenderPanels(toToggle[c]); + } + + }; + /** * The code below handles the tokens that trigger when searches are kicked off for a tab. */ - - // Get the tab token for a given tab name + + /** + * Get the tab token for a given tab name + * @param {string} tab_name The name of the tab + */ var getTabTokenForTabName = function(tab_name){ - return tab_name; //"tab_" + - } - + return tab_name; + }; + // Get all of the possible tab control tokens var getTabTokens = function(){ - var tabTokens = []; - - var tabLinks = $('#tabs > li > a'); - - for(var c = 0; c < tabLinks.length; c++){ - tabTokens.push( getTabTokenForTabName( $(tabLinks[c]).data('token') ) ); - } - - return tabTokens; - } - - // Clear all but the active tab control tokens + var tabTokens = []; + + var tabLinks = $('#tabs > li > a'); + + for(var c = 0; c < tabLinks.length; c++){ + tabTokens.push( getTabTokenForTabName( $(tabLinks[c]).data('token') ) ); + } + + return tabTokens; + }; + + /** + * Clear all but the active tab control tokens + */ var clearTabControlTokens = function(){ - console.info("Clearing tab control tokens"); - - //tabsInitialzed = []; - var tabTokens = getTabTokens(); - var activeTabToken = getActiveTabToken(); - var tokens = mvc.Components.getInstance("submitted"); - - // Clear the tokens for all tabs except for the active one - for(var c = 0; c < tabTokens.length; c++){ - - if( activeTabToken !== tabTokens[c] ){ - tokens.set(tabTokens[c], undefined); - } - } - } - - // Get the tab control token for the active tab + console.info("Clearing tab control tokens"); + + //tabsInitialzed = []; + var tabTokens = getTabTokens(); + var activeTabToken = getActiveTabToken(); + var tokens = mvc.Components.getInstance("submitted"); + + // Clear the tokens for all tabs except for the active one + for(var c = 0; c < tabTokens.length; c++){ + + if( activeTabToken !== tabTokens[c] ){ + tokens.set(tabTokens[c], undefined); + } + } + }; + + /** + * Get the tab control token for the active tab + */ var getActiveTabToken = function(){ - return $('#tabs > li.active > a').data('token'); - } - - // Set the token for the active tab + return $('#tabs > li.active > a').data('token'); + }; + + /** + * Set the token for the active tab + */ var setActiveTabToken = function(){ - var activeTabToken = getActiveTabToken(); - - var tokens = mvc.Components.getInstance("submitted"); - - tokens.set(activeTabToken, ''); - } - + var activeTabToken = getActiveTabToken(); + var tokens = mvc.Components.getInstance("submitted"); + + if(activeTabToken){ + // Set each token if necessary + activeTabToken.split(",").forEach(function(token){ + + // If the token wasn't set, set it so that the searches can run + if(!tokens.toJSON()[token] || tokens.toJSON()[token] == undefined){ + tokens.set(token, ""); + } + }); + } + }; + + /** + * Handle the setting of the token for the clicked tab. + * @param {*} e + */ var setTokenForTab = function(e){ - - // Get the token for the tab - var tabToken = getTabTokenForTabName($(e.target).data('token')); - - // Set the token - var tokens = mvc.Components.getInstance("submitted"); - tokens.set(tabToken, ''); - - console.info("Set the token for the active tab (" + tabToken + ")"); - } - - $('a[data-toggle="tab"]').on('shown', setTokenForTab); - - // Wire up the tab control tokenization - var submit = mvc.Components.get("submit"); - - if( submit ){ - submit.on("submit", function() { - clearTabControlTokens(); - }); - } - - // Set the token for the selected tab - setActiveTabToken(); - -}); + // Get the token for the tab + var tabToken = getTabTokenForTabName($(e.target).data('token')); + + // Set the token + var tokens = mvc.Components.getInstance("submitted"); + tokens.set(tabToken, ''); + + console.info("Set the token for the active tab (" + tabToken + ")"); + }; + + /** + * Perform the initial setup for making the tabs work. + */ + var firstTimeTabSetup = function() { + $('a.toggle-tab').on('shown', setTokenForTab); + + // Wire up the function to show the appropriate tab + $('a.toggle-tab').on('click shown', selectTab); + + // Show the first tab in each tab set + $.each($('.nav-tabs'), function(index, value) { + $('.toggle-tab', value).first().trigger('shown'); + }); + + // Make the tabs into tabs + $('#tabs', this.$el).tab(); + + // Wire up the tab control tokenization + var submit = mvc.Components.get("submit"); + + if(submit){ + submit.on("submit", function() { + clearTabControlTokens(); + }); + } + + // Set the token for the selected tab + setActiveTabToken(); + }; + + firstTimeTabSetup(); +}); diff --git a/github_app_for_splunk/appserver/static/workflowdetails.js b/github_app_for_splunk/appserver/static/workflowdetails.js new file mode 100644 index 0000000..dbf8f8f --- /dev/null +++ b/github_app_for_splunk/appserver/static/workflowdetails.js @@ -0,0 +1,118 @@ +require([ + "underscore", + "splunkjs/mvc", + "splunkjs/mvc/searchmanager", + "splunkjs/mvc/tableview", + "splunkjs/mvc/simplexml/ready!" +], function( + _, + mvc, + SearchManager, + TableView +) { + + // Set up search managers + var search2 = new SearchManager({ + id: "workflow_details", + preview: true, + cache: true, + search: mvc.tokenSafe("index=github_webhook eventtype=\"GitHub::Workflow\" \"workflow_job.run_id\"=$workflow_id$| fields * | eval queued=if(action==\"queued\",_time,null), started=if(action==\"in_progress\",_time,null), completed=if(action==\"completed\",_time,null) | stats latest(workflow_job.conclusion) as status, latest(workflow_job.name) as Name, latest(queued) as queued, latest(started) as started, latest(completed) as completed by workflow_job.id | eval queueTime=toString(round(started-queued),\"Duration\"), runTime=toString(round(completed-started),\"Duration\"), totalTime=toString(round(completed-queued),\"Duration\"), status=if(status==\"null\",\"in_progress\",status) | rename workflow_job.id AS JobID | fields status, Name, JobID, queueTime, runTime, totalTime"), + earliest_time: mvc.tokenSafe("$field1.earliest$"), + latest_time: mvc.tokenSafe("$field1.latest$") + }); + + // Create a table for a custom row expander + var mycustomrowtable = new TableView({ + id: "table-customrow", + managerid: "workflow_details", + drilldown: "none", + drilldownRedirect: false, + el: $("#table-customrow") + }); + + // Define icons for the custom table cell + var ICONS = { + failure: "error", + in_progress: "question-circle", + success: "check-circle" + }; + + // Use the BaseCellRenderer class to create a custom table cell renderer + var CustomCellRenderer = TableView.BaseCellRenderer.extend({ + canRender: function(cellData) { + // This method returns "true" for the "range" field + return cellData.field === "status"; + }, + + // This render function only works when canRender returns "true" + render: function($td, cellData) { + console.log("cellData: ", cellData); + + var icon = "question"; + if(ICONS.hasOwnProperty(cellData.value)) { + icon = ICONS[cellData.value]; + } + $td.addClass("icon").html(_.template('', { + icon: icon, + status: cellData.value + })); + } + }); + + // Use the BasicRowRenderer class to create a custom table row renderer + var CustomRowRenderer = TableView.BaseRowExpansionRenderer.extend({ + canRender: function(rowData) { + console.log("RowData: ", rowData); + return true; + }, + + initialize: function(args){ + this._searchManager = new SearchManager({ + id: 'details-search-manager', + preview: false + }); + this._TableView = new TableView({ + id: 'ResultsTable', + managerid: 'details-search-manager', + drilldown: "all", + drilldownRedirect: true + }); + }, + + render: function($container, rowData) { + // Print the rowData object to the console + // console.log("RowData: ", rowData); + + var repoNameCell = _(rowData.cells).find(function (cell) { + return cell.field === 'Repository Name'; + }); + + + var workflowName = _(rowData.cells).find(function (cell) { + return cell.field === 'Workflow Name'; + }); + + var workflowIDCell = _(rowData.cells).find(function (cell) { + return cell.field === 'Run ID'; + }); + + this._searchManager.set({ search: 'index=github_webhook (workflow_run.id='+workflowIDCell.value+' OR workflow_job.run_id='+workflowIDCell.value+') | eval started=if(action=="requested", _time, null), completed=if(action=="completed", _time,null) | stats latest(workflow_run.conclusion) as Status, earliest(started) as Started, latest(completed) as Completed, latest(workflow_run.head_branch) as Branch, latest(workflow_run.event) as Trigger | eval Duration=tostring(Completed-Started, "Duration") | fields Status, Duration, Branch, Trigger | eval Details="Click here for Workflow Details" | transpose|rename column AS Details| rename "row 1" AS values'}); + // $container is the jquery object where we can put out content. + // In this case we will render our chart and add it to the $container + $container.append(this._TableView.render().el); + } + }); + + // Create an instance of the custom cell renderer, + // add it to the table, and render the table + var myCellRenderer = new CustomCellRenderer(); + mycustomrowtable.addCellRenderer(myCellRenderer); + mycustomrowtable.render(); + + // Create an instance of the custom row renderer, + // add it to the table, and render the table + var myRowRenderer = new CustomRowRenderer(); + mycustomrowtable.render(); + + +}); diff --git a/github_app_for_splunk/default/data/ui/nav/default.xml b/github_app_for_splunk/default/data/ui/nav/default.xml index c537b06..d80e7a3 100644 --- a/github_app_for_splunk/default/data/ui/nav/default.xml +++ b/github_app_for_splunk/default/data/ui/nav/default.xml @@ -16,11 +16,14 @@ + - - - + + + + + diff --git a/github_app_for_splunk/default/data/ui/views/audit_log_activity.xml b/github_app_for_splunk/default/data/ui/views/audit_log_activity.xml index 3a1dc99..b3d06d2 100644 --- a/github_app_for_splunk/default/data/ui/views/audit_log_activity.xml +++ b/github_app_for_splunk/default/data/ui/views/audit_log_activity.xml @@ -14,7 +14,7 @@ Events over time - `github_source` action | dedup document_id | timechart count by action + `github_source` action=* | timechart count by action $timeRng.earliest$ $timeRng.latest$ @@ -29,7 +29,7 @@ Total events - `github_source` action | dedup document_id | stats count + `github_source` action=* | stats count $timeRng.earliest$ $timeRng.latest$ @@ -48,10 +48,10 @@ @@ -61,7 +61,7 @@ - `github_source` | rex field=actor_location "\{\'country_code\'\: \'(?<iso2>[A-Z]{2})\'" | stats count by iso2 | lookup geo_attr_countries iso2 OUTPUT country | append [ | inputlookup geo_attr_countries] | dedup country | fillnull value=0 | fields+ count, country, geom | geom geo_countries featureIdField="country" + `github_source` | rename actor_location.country_code AS iso2 | stats count by iso2 | lookup geo_attr_countries iso2 OUTPUT country | append [ | inputlookup geo_attr_countries] | dedup country | fillnull value=0 | fields+ count, country, geom | geom geo_countries featureIdField="country" $timeRng.earliest$ $timeRng.latest$ 1 @@ -99,7 +99,7 @@ - `github_source` action | rex field=actor_location "\{\'country_code\'\: \'(?<iso2>[A-Z]{2})\'" | stats count by iso2 | lookup geo_attr_countries iso2 OUTPUT country | fields country, count + `github_source` action=* | rename actor_location.country_code AS iso2 | stats count by iso2 | lookup geo_attr_countries iso2 OUTPUT country | fields country, count $timeRng.earliest$ $timeRng.latest$ 1 @@ -120,7 +120,7 @@ Top 5 event types - `github_source` action | dedup document_id | stats count by action | sort 5 - count + `github_source` action=* | stats count by action | sort 5 - count $timeRng.earliest$ $timeRng.latest$ @@ -138,7 +138,7 @@ Top 5 active users - `github_source` action | dedup document_id | stats count by actor | sort 5 - count + `github_source` action=* | stats count by actor | sort 5 - count $timeRng.earliest$ $timeRng.latest$ @@ -154,7 +154,7 @@ Events per org - `github_source` action | dedup document_id | stats count by org + `github_source` action=* | stats count by org $timeRng.earliest$ $timeRng.latest$ @@ -168,7 +168,7 @@ Workflow runs - `github_source` | dedup document_id | stats count by conclusion + `github_source` | stats count by conclusion $timeRng.earliest$ $timeRng.latest$ @@ -183,7 +183,7 @@ Top 10 active repositories - `github_source` | dedup document_id | rename repo as repository | stats count by repository | sort 10 - count + `github_source` | rename repo as repository | stats count by repository | sort 10 - count $timeRng.earliest$ $timeRng.latest$ diff --git a/github_app_for_splunk/default/data/ui/views/repository_audit.xml b/github_app_for_splunk/default/data/ui/views/repository_audit.xml index df556cd..ca6ff2e 100644 --- a/github_app_for_splunk/default/data/ui/views/repository_audit.xml +++ b/github_app_for_splunk/default/data/ui/views/repository_audit.xml @@ -53,13 +53,13 @@ diff --git a/github_app_for_splunk/default/data/ui/views/security_alert_overview.xml b/github_app_for_splunk/default/data/ui/views/security_alert_overview.xml index 7433349..8cefd7c 100644 --- a/github_app_for_splunk/default/data/ui/views/security_alert_overview.xml +++ b/github_app_for_splunk/default/data/ui/views/security_alert_overview.xml @@ -2,7 +2,7 @@ - `github_webhooks` alert.created_at=* | eval reason=if(isnotnull('alert.affected_package_name'),'alert.affected_package_name','alert.rule.name'), id=if(isnotnull('alert.external_identifier'),'alert.external_identifier','alert.rule.id'), severity=if(isnotnull('alert.severity'),'alert.severity','alert.rule.security_severity_level'), type=if(isnotnull('alert.external_identifier'),"Dependabot Alert","Code Scanning Alert") | stats latest(action) as status, earliest(alert.created_at) as created_at by repository.name, reason, id, type, severity | eval age = toString(round(now() - strptime(created_at, "%Y-%m-%dT%H:%M:%S")),"Duration") + index=gh_vuln OR (`github_webhooks` alert.created_at=*) | eval reason=if(isnotnull('alert.affected_package_name'),'alert.affected_package_name','alert.rule.name'), id=if(isnotnull('alert.external_identifier'),'alert.external_identifier','alert.rule.id'), severity=if(isnotnull('alert.severity'),'alert.severity','alert.rule.security_severity_level'), type=if(isnotnull('alert.external_identifier'),"Dependabot Alert","Code Scanning Alert") | stats latest(action) as status, earliest(alert.created_at) as created_at, latest(alert.number) as number by repository.full_name, reason, id, type, severity | eval source=if(type=="Dependabot Alert","dependabot","code-scanning") | eval age = toString(round(now() - strptime(created_at, "%Y-%m-%dT%H:%M:%S")),"Duration") $timeTkn.earliest$ $timeTkn.latest$ @@ -78,7 +78,7 @@ Open Alerts By Repository - | search status IN("create","created") | stats count by repository.name + | search status IN("create","created") | stats count by repository.full_name @@ -229,14 +229,23 @@ |search severity IN($severityTkn$) status IN($statusTkn$) type IN($typeTkn$) | sort -age + repository.full_name, reason, id, type,severity,status, created_at, age + + + https://github.com/$row.repository.full_name|n$/security/$row.source$/$row.number$ + + - + + + {"critical":#DC4E41,"high":#F1813F,"moderate":#F8BE34} +
diff --git a/github_app_for_splunk/default/data/ui/views/workflow_analysis.xml b/github_app_for_splunk/default/data/ui/views/workflow_analysis.xml new file mode 100644 index 0000000..2bb9aed --- /dev/null +++ b/github_app_for_splunk/default/data/ui/views/workflow_analysis.xml @@ -0,0 +1,77 @@ +
+ +
+ + + + -24h@h + now + + + + + All + * + * + +
+ + + + Workflow Conclusions Over Time + + index=github_webhook "workflow_run.name"="*" | spath "repository.full_name" | search repository.full_name="$repos$" | stats latest(_time) as _time, latest(workflow_run.conclusion) as workflow_run.conclusion by repository.full_name,workflow_run.name,workflow_run.id | timechart count by workflow_run.conclusion span=1h | rename null as "in-progress" + $field1.earliest$ + $field1.latest$ + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Workflow History

+
+
+ +
+
+
diff --git a/github_app_for_splunk/default/data/ui/views/workflow_analytics.xml b/github_app_for_splunk/default/data/ui/views/workflow_analytics.xml index b070ee7..59d5c8d 100644 --- a/github_app_for_splunk/default/data/ui/views/workflow_analytics.xml +++ b/github_app_for_splunk/default/data/ui/views/workflow_analytics.xml @@ -1,5 +1,5 @@
- +
@@ -30,7 +30,7 @@ Average Workflow Overview - `github_webhooks` eventtype="GitHub::Workflow" repository.name IN("$repoTkn$") | eval queued=if(action="queued",_time,NULL), started=if(action="started",_time,NULL),completed=if(action="completed",_time,NULL) | stats min(queued) as queued, min(started) as started, min(completed) as completed by repository.name,workflow_job.name,workflow_job.id | eval queTime=started-queued, runTime=completed-started, totalTime=completed-queued | fields repository.name,workflow_job.name, workflow_job.id, queTime, runTime, totalTime | stats avg(queTime) as queTime, avg(runTime) as runTime, avg(totalTime) as totalTime | eval queTime=toString(round(queTime),"Duration"), runTime=toString(round(runTime),"Duration"),totalTime=toString(round(totalTime),"Duration") + `github_webhooks` eventtype="GitHub::Workflow" repository.name IN("$repoTkn$") | eval queued=if(action="queued",_time,NULL), started=if(action="in_progress",_time,NULL),completed=if(action="completed",_time,NULL) | stats min(queued) as queued, min(started) as started, min(completed) as completed by repository.name,workflow_job.name,workflow_job.id | eval queueTime=started-queued, runTime=completed-started, totalTime=completed-queued | fields repository.name,workflow_job.name, workflow_job.id, queueTime, runTime, totalTime | stats avg(queueTime) as queueTime, avg(runTime) as runTime, avg(totalTime) as totalTime | eval queueTime=toString(round(queueTime),"Duration"), runTime=toString(round(runTime),"Duration"),totalTime=toString(round(totalTime),"Duration") $timeTkn.earliest$ $timeTkn.latest$ 1 @@ -55,38 +55,12 @@ - - - Workflow History - - - `github_webhooks` eventtype="GitHub::Workflow" repository.name IN("$repoTkn$") | eval queued=if(action="queued",_time,NULL), started=if(action="started",_time,NULL),completed=if(action="completed",_time,NULL) | stats min(queued) as queued, min(started) as started, min(completed) as completed by repository.full_name,workflow_job.name,workflow_job.id | eval queTime=toString(round(started-queued),"Duration"), runTime=toString(round(completed-started),"Duration"), totalTime=toString(round(completed-queued),"Duration") | table repository.full_name,workflow_job.name, workflow_job.id, queTime, runTime, totalTime - $timeTkn.earliest$ - $timeTkn.latest$ - 1 - - - - - - - - - - - - - - -
-
-
Workflow Analytics by Job Name - `github_webhooks` eventtype="GitHub::Workflow" repository.name IN("$repoTkn$") | eval queued=if(action="queued",_time,NULL), started=if(action="started",_time,NULL),completed=if(action="completed",_time,NULL) | stats min(queued) as queued, min(started) as started, min(completed) as completed by repository.full_name,workflow_job.name,workflow_job.id | eval queTime=started-queued, runTime=completed-started, totalTime=completed-queued | fields repository.full_name,workflow_job.name, workflow_job.id, queTime, runTime, totalTime | stats avg(queTime) as queTime, avg(runTime) as runTime, avg(totalTime) as totalTime by repository.full_name,workflow_job.name | eval queTime=toString(round(queTime),"Duration"), runTime=toString(round(runTime),"Duration"),totalTime=toString(round(totalTime),"Duration") + `github_webhooks` eventtype="GitHub::Workflow" repository.name IN("$repoTkn$") | eval queued=if(action="queued",_time,NULL), started=if(action="in_progress",_time,NULL),completed=if(action="completed",_time,NULL) | stats min(queued) as queued, min(started) as started, min(completed) as completed by repository.full_name,workflow_job.name,workflow_job.id | eval queueTime=started-queued, runTime=completed-started, totalTime=completed-queued | fields repository.full_name,workflow_job.name, workflow_job.id, queueTime, runTime, totalTime | stats avg(queueTime) as queueTime, avg(runTime) as runTime, avg(totalTime) as totalTime by repository.full_name,workflow_job.name | eval queueTime=toString(round(queueTime),"Duration"), runTime=toString(round(runTime),"Duration"),totalTime=toString(round(totalTime),"Duration") $timeTkn.earliest$ $timeTkn.latest$ 1 diff --git a/github_app_for_splunk/default/data/ui/views/workflow_details.xml b/github_app_for_splunk/default/data/ui/views/workflow_details.xml new file mode 100644 index 0000000..cc562c7 --- /dev/null +++ b/github_app_for_splunk/default/data/ui/views/workflow_details.xml @@ -0,0 +1,214 @@ + + + + + index=github_webhook "workflow_run.name"="$workflowName$" | fields * | spath "repository.full_name" + + $timeRange.earliest$ + $timeRange.latest$ + +
+ + + * + + + + * + + + + + + + + -24h@h + now + + + + + 25 + 10 + 25 + 50 + 100 + 250 + 25 + +
+ + + + + + + + + + Build Duration History + + + | eval started=if(action=="requested",_time, NULL), ended=if(action=="completed", _time, NULL) | stats latest(_time) as _time, latest(workflow_run.conclusion) as conclusion, earliest(started) as started, latest(ended) as ended by workflow_run.name, workflow_run.id, repository.full_name | eval duration=ended-started, queued=started-queued | table workflow_run.id, _time, duration | sort -_time | head 25 | sort _time | fields - _time + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Build Status History + + + | eval failed=if('workflow_run.conclusion'=="failure",1,0), successful=if('workflow_run.conclusion'=="success",1,0) | stats latest(_time) as _time, max(successful) as successful, max(failed) as failed, latest(workflow_run.conclusion) as conclusion by repository.full_name,workflow_run.name,workflow_run.id | table _time, workflow_run.id, successful,failed | sort -_time | head $workflowCount$ | sort _time | fields - _time + + + + + + + + + + + Build Status Overview + + + | stats latest(_time) as _time, latest(workflow_run.conclusion) as conclusion by repository.full_name,workflow_run.name,workflow_run.id | sort -_time | head $workflowCount$ | sort _time | fields - _time | stats count by conclusion + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Workflow Information + + index=github_webhook (workflow_run.id=$workflow_id$ OR workflow_job.run_id=$workflow_id$) | eval started=if(action=="requested", _time, null), completed=if(action=="completed", _time,null) | stats latest(workflow_run.name) as WorkflowName, latest(workflow_run.id) as WorkflowID, latest(workflow_run.conclusion) as Status, latest(repository.full_name) as RepositoryName,earliest(started) as Started, latest(completed) as Completed, latest(workflow_run.head_branch) as Branch, latest(workflow_run.event) as Trigger, latest(workflow_run.run_number) as RunNumber, latest(workflow_run.run_attempt) as Attempt, latest(workflow_run.html_url) as URL | eval Duration=tostring(Completed-Started, "Duration"), Completed=strftime(Completed,"%Y-%m-%dT%H:%M:%S"), Started=strftime(Started,"%Y-%m-%dT%H:%M:%S") | fields WorkflowName, RepositoryName, Status, WorkflowID, RunNumber, Attempt, Started, Completed, Duration, Branch, Trigger, URL|transpose|rename column AS Details| rename "row 1" AS values + 0 + + 1 + + + + + + + + + + https://github.com/$repoName|n$/actions/runs/$workflow_id$ + +
+
+
+ + + + + + + +
+

Workflow Jobs

+
+
+ +
+
+ + + Workflow Run Logs + + + index="github_workflow_logs" workflowID::$workflow_id$ | sort _time + 0 + 1 + + + + + + + + + + + + + + + diff --git a/github_app_for_splunk/default/eventtypes.conf b/github_app_for_splunk/default/eventtypes.conf index 591e352..5ef01a1 100644 --- a/github_app_for_splunk/default/eventtypes.conf +++ b/github_app_for_splunk/default/eventtypes.conf @@ -26,7 +26,7 @@ search = `github_webhooks` action IN ("created","edited","moved","converted","de search = `github_webhooks` action IN ("created","edited","moved","deleted") "project_column.id"=* [GitHub::Workflow] -search = `github_webhooks` action IN ("queued","created","started","completed") workflow_job.id=* +search = `github_webhooks` action IN ("queued","created","in_progress","completed") workflow_job.id=* [GitHub::CodeScanning] search = `github_webhooks` action IN ("appeared_in_branch", "closed_by_user", "created", "fixed", "reopened", "reopened_by_user") "alert.created_at"=* diff --git a/github_app_for_splunk/default/props.conf b/github_app_for_splunk/default/props.conf index b6fcbb1..97314cc 100644 --- a/github_app_for_splunk/default/props.conf +++ b/github_app_for_splunk/default/props.conf @@ -42,7 +42,7 @@ FIELDALIAS-issueNumber = "issue.number" ASNEW issueNumber [github_audit] DATETIME_CONFIG = -INDEXED_EXTRACTIONS = json +KV_MODE = json LINE_BREAKER = ([\r\n]+) NO_BINARY_CHECK = true TIMESTAMP_FIELDS = @timestamp @@ -52,3 +52,4 @@ TZ = GMT category = Application disabled = false pulldown_type = 1 +FIELDALIAS-user = actor AS user pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy