diff --git a/clients/extjs/css/stigman.css b/clients/extjs/css/stigman.css index 37bb8e07e..260c6252e 100644 --- a/clients/extjs/css/stigman.css +++ b/clients/extjs/css/stigman.css @@ -1,3 +1,58 @@ +.x-grid3-row-selected { + background-color: hsl(212deg 14% 85%) !important; + background-image: none; + border-color: #ACACAC; +} + +.x-grid3-row-checker,.x-grid3-hd-checker { + background-image: url(../img/checkboxes.svg) +} +.x-grid3-body .x-grid3-td-checker { + background: unset; +} +.x-grid3-body .x-grid3-row-selected .x-grid3-td-checker { + background-image: unset; +} +.x-grid3-row-checker,.x-grid3-hd-checker { + width: 100%; + height: 18px; + background-position: 1px 2px; + background-repeat: no-repeat; + background-color: transparent; +} + +.x-grid3-row .x-grid3-row-checker { + background-position: 1px 2px +} + +.x-grid3-row-selected .x-grid3-row-checker,.x-grid3-hd-checker-on .x-grid3-hd-checker,.x-grid3-row-checked .x-grid3-row-checker { + background-position: -24px 2px +} + +.x-grid3-hd-checker { + background-position: 1px 1px +} + +.ext-border-box .x-grid3-hd-checker { + background-position: 1px 3px +} + +.x-grid3-hd-checker-on .x-grid3-hd-checker { + background-position: -24px 1px +} + +.x-grid3-hd-checker-ind .x-grid3-hd-checker { + background-position: -50px 1px +} + +/* Adjust position of the grid checkbox */ +/* Required by SM.SelectingGridToolbar */ +.x-grid3-row-selected-checkonly .x-grid3-row-checker { + background-position: -24px 2px; +} + + + input.x-tree-node-cb { margin-right: 2px; } @@ -49,6 +104,7 @@ a.x-grid3-hd-btn:hover { background-color: transparent; background-image:none; display: block; + cursor: default; } .sm-cb .x-form-cb-label { @@ -401,7 +457,7 @@ body { .x-fieldset { border-radius: 3px; } -.x-body-masked .ext-el-mask { +.x-body-masked .ext-el-mask, .x-masked .ext-el-mask { background-color: rgb(0 0 0 / 35%); backdrop-filter: blur(1px) saturate(50%); opacity: unset; @@ -560,13 +616,13 @@ td.x-grid3-hd-over, td.sort-desc, td.sort-asc, td.x-grid3-hd-menu-open { border-left-color: #eee; border-right-color: #d0d0d0; } -.x-grid3-row { +/* .x-grid3-row { cursor: default; border-width: 1px 1px 1px 0px; border-style: solid; width: 100%; } - + */ td.x-grid3-hd-over .x-grid3-hd-inner, td.sort-desc .x-grid3-hd-inner, td.sort-asc .x-grid3-hd-inner, td.x-grid3-hd-menu-open .x-grid3-hd-inner { background-color:#f0f0f0; background-image:none; @@ -690,11 +746,6 @@ td.x-grid3-hd-over .x-grid3-hd-inner, td.sort-desc .x-grid3-hd-inner, td.sort-as } -/* Adjust position of the grid checkbox */ -/* Required by SM.SelectingGridToolbar */ -.x-grid3-row-selected-checkonly .x-grid3-row-checker { - background-position: -22px 3px; -} .sm-tree-node-create { font-style: italic diff --git a/clients/extjs/img/checkboxes.svg b/clients/extjs/img/checkboxes.svg new file mode 100644 index 000000000..c0961d695 --- /dev/null +++ b/clients/extjs/img/checkboxes.svg @@ -0,0 +1,124 @@ + + + Checkbox + + + + + + image/svg+xml + + Checkbox + + + carl.a.smigielski@saic.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clients/extjs/js/SM/ColumnFilters.js b/clients/extjs/js/SM/ColumnFilters.js index 8a4bd5f39..4bb30a7d3 100644 --- a/clients/extjs/js/SM/ColumnFilters.js +++ b/clients/extjs/js/SM/ColumnFilters.js @@ -67,6 +67,10 @@ SM.ColumnFilters.GridView = Ext.extend(Ext.grid.GridView, { }, handleHdDown: function (e, target) { // Modifies superclass method to support lastHide + + if (target.className == 'x-grid3-hd-checker') { + return + } e.stopEvent() if (!this.lastHide || this.lastHide.getElapsed() > 100) { var colModel = this.cm, @@ -110,7 +114,8 @@ SM.ColumnFilters.GridView = Ext.extend(Ext.grid.GridView, { const colCount = this.cm.getColumnCount() for (let i = 0; i < colCount; i++) { const td = this.getHeaderCell(i) - td.getElementsByTagName("a")[0].style.height = (td.firstChild.offsetHeight - 1) + 'px' + // td.getElementsByTagName("a")[0].style.height = (td.firstChild.offsetHeight - 1) + 'px' + td.getElementsByTagName("a")[0].style.height = 0 if (this.cm.config[i].filtered) { td.classList.add('sm-grid3-col-filtered') } diff --git a/clients/extjs/js/SM/StigRevision.js b/clients/extjs/js/SM/StigRevision.js new file mode 100644 index 000000000..46092e9a6 --- /dev/null +++ b/clients/extjs/js/SM/StigRevision.js @@ -0,0 +1,566 @@ +Ext.ns('SM.StigRevision') + +SM.StigRevision.RevisionMenuBtn = Ext.extend(Ext.Button, { + initComponent: function () { + const _this = this + menu = new SM.StigRevision.RevisionMenu( { iconCls: 'icon-del' }) + const config = { + menu: menu + } + Ext.apply(this, Ext.apply(this.initialConfig, config)) + SM.StigRevision.RevisionMenuBtn.superclass.initComponent.call(this) + } +}) + +SM.StigRevision.RevisionMenu = Ext.extend(Ext.menu.Menu, { + load: async function (record) { + this.removeAll() + const re = /^V([\d,\.]{1,5})R([\d,\.]{1,5})$/ + for (const revisionStr of record.data.revisionStrs) { + const matches = re.exec(revisionStr) + if (matches && matches.length === 3) { + const text = `Version ${SM.he(matches[1])} Release ${SM.he(matches[2])}${revisionStr === record.data.lastRevisionStr ? 'latest' : ''}` + this.addItem({ + iconCls: 'icon-del', + text, + revisionStr, + benchmarkId: record.data.benchmarkId, + record + }) + } + } + this.addItem('-') + this.addItem({ + iconCls: 'icon-del', + text: 'Remove all revisions', + benchmarkId: record.data.benchmarkId, + record + }) + } +}) + +SM.StigRevision.removeStig = async function (benchmarkId) { + const result = await Ext.Ajax.requestPromise({ + url: `${STIGMAN.Env.apiBase}/stigs/${benchmarkId}`, + method: 'DELETE' + }) + return JSON.parse(result.response.responseText) +} + +SM.StigRevision.removeStigRevision = async function (benchmarkId, revisionStr) { + const result = await Ext.Ajax.requestPromise({ + url: `${STIGMAN.Env.apiBase}/stigs/${benchmarkId}/revisions/${revisionStr}`, + method: 'DELETE' + }) + return JSON.parse(result.response.responseText) +} + +SM.StigRevision.getStig = async function (benchmarkId) { + const result = await Ext.Ajax.requestPromise({ + url: `${STIGMAN.Env.apiBase}/stigs/${benchmarkId}`, + method: 'GET' + }) + return JSON.parse(result.response.responseText) +} + +SM.StigRevision.StigGrid = Ext.extend(Ext.grid.GridPanel, { + initComponent: function () { + const _this = this + + const fields = Ext.data.Record.create([ + 'benchmarkId', + 'title', + 'status', + 'lastRevisionStr', + 'lastRevisionDate', + // { + // name: 'lastRevisionDate', + // type: 'date', + // dateFormat: 'Y-m-d' + // }, + 'ruleCount', + 'autoCount', + 'revisionStrs' + ]) + + const sm = new Ext.grid.CheckboxSelectionModel({ + singleSelect: false, + checkOnly: false, + listeners: { + selectionchange: function (sm) { + const count = sm.getCount() + if (count === 1) { + const r = sm.getSelected() + if (r.data.revisionStrs.length >= 2) { + revisionMenu.load(r) + removeRevisionBtn.setVisible(true) + removeStigsBtn.setVisible(false) + } + else { + removeRevisionBtn.setVisible(false) + removeStigsBtn.setVisible(true) + removeStigsBtn.setDisabled(false) + removeStigsBtn.setText(`Remove STIG`) + } + libraryBtn.setDisabled(false) + } + else { + libraryBtn.setDisabled(true) + removeRevisionBtn.setVisible(false) + if (count > 0) { + removeStigsBtn.setText(`Remove STIG (${count})`) + removeStigsBtn.setDisabled(false) + } + else { + removeStigsBtn.setText(`Remove`) + removeStigsBtn.setDisabled(true) + } + removeStigsBtn.setVisible(true) + } + + const hd = this.grid.view.innerHd.querySelector('.x-grid3-hd-row .x-grid3-td-checker .x-grid3-hd-checker') + if (hd) { + const hdState = this.selections.length === 0 ? null : this.grid.store.getCount() === this.selections.length ? 'on' : 'ind' + hd.classList.remove('x-grid3-hd-checker-on') + hd.classList.remove('x-grid3-hd-checker-ind') + if (hdState) { + hd.classList.add(`x-grid3-hd-checker-${hdState}`) + } + } + + } + } + }) + + const columns = [ + sm, + { + header: "Benchmark ID", + width: 300, + dataIndex: 'benchmarkId', + sortable: true, + filter: {type: 'string'} + }, + { + header: "Title", + id: 'stigGrid-title-column', + width: 350, + dataIndex: 'title', + sortable: true, + filter: {type: 'string'} + }, + { + header: "Status", + width: 150, + align: "center", + dataIndex: 'status', + sortable: true, + filter: {type: 'values'} + }, + { + header: "Latest revision", + width: 150, + align: "center", + dataIndex: 'lastRevisionStr', + sortable: true + }, + { + header: "Revision date", + width: 150, + align: "center", + dataIndex: 'lastRevisionDate', + xtype: 'datecolumn', + format: 'Y-m-d', + sortable: true + }, + { + header: "Earlier revisions", + width: 150, + align: "center", + dataIndex: 'revisionStrs', + sortable: true, + renderer: (v, md, r) => v.filter( rev => rev !== r.data.lastRevisionStr ).join(', ') || '--' + }, + { + header: "Rules", + width: 150, + align: "center", + dataIndex: 'ruleCount', + sortable: true + }, + { + header: "SCAP Rules", + width: 150, + align: "center", + dataIndex: 'autoCount', + sortable: true, + renderer: (v) => v ? v : '--' + } + ] + + const store = new Ext.data.JsonStore({ + proxy: new Ext.data.HttpProxy({ + url: `${STIGMAN.Env.apiBase}/stigs`, + method: 'GET' + }), + root: '', + fields, + idProperty: 'benchmarkId', + sortInfo: { + field: 'benchmarkId', + direction: 'ASC' // or 'DESC' (case sensitive for local sorting) + }, + listeners: { + // load: function (store,records) { + // store.isLoaded = true + // _this.getSelectionModel().selectFirstRow(); + // } + } + }) + + const totalTextCmp = new SM.RowCountTextItem({ store }) + + const view = new SM.ColumnFilters.GridView({ + forceFit:true, + // These listeners keep the grid in the same scroll position after the store is reloaded + listeners: { + beforerefresh: function(v) { + v.scrollTop = v.scroller.dom.scrollTop; + v.scrollHeight = v.scroller.dom.scrollHeight; + }, + refresh: function(v) { + setTimeout(function() { + v.scroller.dom.scrollTop = v.scrollTop + (v.scrollTop == 0 ? 0 : v.scroller.dom.scrollHeight - v.scrollHeight); + }, 100); + }, + filterschanged: function (view) { + store.filter(view.getFilterFns()) + } + }, + deferEmptyText:false + }) + + const importStigsBtn = new Ext.Button({ + iconCls: 'sm-import-icon', + text: 'Import STIGs', + disabled: false, + handler: function() { + SM.StigRevision.ImportStigs(_this) + } + }) + + const libraryBtn = new Ext.Button({ + iconCls: 'sm-library-icon', + text: 'Open in Library', + disabled: true, + handler: function () { + const record = _this.getSelectionModel().getSelected() + addLibraryStig({ + benchmarkId: record.data.benchmarkId, + revisionStr: record.data.lastRevisionStr, + stigTitle: record.data.title + }) + } + }) + + const removeStigsBtn = new Ext.Button({ + iconCls: 'icon-del', + text: 'Remove', + disabled: true, + handler: async function () { + try { + const records = _this.getSelectionModel().getSelections() + const heBenchmarkIds = records.map( r => SM.he(r.data.benchmarkId)) + let benchmarkList + if (heBenchmarkIds.length > 25) { + benchmarkList = `The ${heBenchmarkIds.length} selected STIGs` + } + else { + benchmarkList = heBenchmarkIds.join('
') + } + const savedMinWidth = Ext.MessageBox.minWidth + Ext.MessageBox.minWidth = 400 + Ext.MessageBox.buttonText.yes = 'Remove' + Ext.MessageBox.buttonText.no = 'Cancel' + const id = await SM.confirmPromise('Confirm', `Do you wish to remove:

${benchmarkList}
`) + if (id === 'yes') { + Ext.getBody().mask('Removing') + for (const record of records) { + await SM.StigRevision.removeStig(record.data.benchmarkId) + _this.store.remove(record) + } + } + } + catch (e) { + alert('Error removing STIGs') + } + finally { + Ext.getBody().unmask() + } + } + }) + + const revisionMenu = new SM.StigRevision.RevisionMenu({ + listeners: { + itemclick: async function (item, e) { + try { + Ext.MessageBox.minWidth = 400 + Ext.MessageBox.buttonText.yes = 'Remove' + Ext.MessageBox.buttonText.no = 'Cancel' + const id = await SM.confirmPromise('Confirm', `Do you wish to remove:

${item.benchmarkId} ${item.revisionStr ? item.revisionStr : ''}
`) + if (id === 'yes') { + Ext.getBody().mask('Removing') + const record = item.record + if (!item.revisionStr) { // remove STIG + await SM.StigRevision.removeStig(item.benchmarkId) + _this.store.remove(record) + } + else { + await SM.StigRevision.removeStigRevision(item.benchmarkId, item.revisionStr) + const apiStig = await SM.StigRevision.getStig(item.benchmarkId) + record.data = apiStig + record.commit() + + // hack to reselect the record + const sm = _this.getSelectionModel() + sm.onRefresh() + sm.fireEvent('selectionchange', sm) + } + } + } + catch (e) { + alert('Error removing revision(s)') + } + finally { + Ext.getBody().unmask() + } + + } + } + }) + + const removeRevisionBtn = new Ext.Button({ + iconCls: 'icon-del', + text: 'Remove Revision', + hidden: true, + menu: revisionMenu + }) + + const tbar = [ + importStigsBtn, + '-', + removeStigsBtn, + removeRevisionBtn, + '-', + libraryBtn + ] + + const bbar = new Ext.Toolbar({ + items: [ + { + xtype: 'exportbutton', + hasMenu: false, + gridBasename: 'Installed-STIGs', + exportType: 'grid', + iconCls: 'sm-export-icon', + text: 'CSV' + }, + { + xtype: 'tbfill' + },{ + xtype: 'tbseparator' + }, + totalTextCmp] + }) + + const config = { + store, + columns, + view, + tbar, + bbar, + sm, + listeners: { + rowdblclick: function (grid, rowIndex) { + const record = grid.store.getAt(rowIndex) + addLibraryStig({ + benchmarkId: record.data.benchmarkId, + revisionStr: record.data.lastRevisionStr, + stigTitle: record.data.title + }) + } + } + } + + Ext.apply(this, Ext.apply(this.initialConfig, config)) + SM.StigRevision.StigGrid.superclass.initComponent.call(this) + } +}) + +SM.StigRevision.ImportStigs = function ( grid ) { + const fp = new Ext.FormPanel({ + padding: 10, + standardSubmit: false, + fileUpload: true, + baseCls: 'x-plain', + monitorValid: true, + autoHeight: true, + labelWidth: 1, + hideLabel: true, + defaults: { + anchor: '100%', + allowBlank: false + }, + items: [ + { + xtype:'fieldset', + title: 'Instructions', + autoHeight:true, + items: [ + { + xtype: 'displayfield', + id: 'infoText1', + name: 'infoText', + html: "Please browse for STIG", + }] + }, + { + xtype: 'fileuploadfield', + id: 'form-file', + emptyText: 'Browse for a file...', + name: 'importFile', + accept: '.zip,.xml', + buttonText: 'Browse...', + buttonCfg: { + icon: "img/disc_drive.png" + } + }, + { + xtype: 'displayfield', + id: 'infoText2', + name: 'infoText', + html: "IMPORTANT: Results from the imported file will overwrite any existing results!", + } + ], + buttonAlign: 'center', + buttons: [{ + text: 'Import', + icon: 'img/page_white_get.png', + tooltip: 'Import the archive', + formBind: true, + handler: async function(){ + try { + let input = document.getElementById("form-file-file") + let file = input.files[0] + let extension = file.name.substring(file.name.lastIndexOf(".")+1) + if (extension.toLowerCase() === 'xml') { + let formEl = fp.getForm().getEl().dom + let formData = new FormData(formEl) + formData.set('replace', 'true') + appwindow.close(); + initProgress("Importing file", "Initializing..."); + updateStatusText (file.name) + + await window.oidcProvider.updateToken(10) + let response = await fetch(`${STIGMAN.Env.apiBase}/stigs`, { + method: 'POST', + headers: new Headers({ + 'Authorization': `Bearer ${window.oidcProvider.token}` + }), + body: formData + }) + let json = await response.json() + updateStatusText (JSON.stringify(json, null, 2)) + updateStatusText ('------------------------------------') + updateStatusText ('Done') + updateProgress(0, 'Done') + } + else if (extension === 'zip') { + appwindow.close() + initProgress("Importing file", "Initializing..."); + await processZip(input.files[0]) + updateStatusText ('Done') + updateProgress(0, 'Done') + } else { + alert(`No handler for ${extension}`) + } + } + catch (e) { + alert(e) + } + finally { + grid?.getStore()?.reload() + } + + async function processZip (f) { + try { + let parentZip = new JSZip() + + let contents = await parentZip.loadAsync(f) + let fns = Object.keys(contents.files) + let xmlMembers = fns.filter( fn => fn.toLowerCase().endsWith('.xml')) + let zipMembers = fns.filter( fn => fn.toLowerCase().endsWith('.zip') ) + for (let x=0,l=xmlMembers.length; x v ? v : '--' - } - ], - autoExpandColumn: 'stigGrid-title-column', - view: new SM.ColumnFilters.GridView({ - forceFit:false, - // These listeners keep the grid in the same scroll position after the store is reloaded - listeners: { - beforerefresh: function(v) { - v.scrollTop = v.scroller.dom.scrollTop; - v.scrollHeight = v.scroller.dom.scrollHeight; - }, - refresh: function(v) { - setTimeout(function() { - v.scroller.dom.scrollTop = v.scrollTop + (v.scrollTop == 0 ? 0 : v.scroller.dom.scrollHeight - v.scrollHeight); - }, 100); - }, - filterschanged: function (view, item, value) { - stigStore.filter(view.getFilterFns()) - } - }, - deferEmptyText:false - }), - listeners: { - // rowdblclick: { - // fn: function(grid,rowIndex,e) { - // var r = grid.getStore().getAt(rowIndex); - // Ext.getBody().mask('Getting assignments for ' + r.get('benchmarkId') + '...'); - // showStigAssignments(r.get('benchmarkId')); - // } - // } - }, - tbar: [ - { - iconCls: 'sm-import-icon', - text: 'Import STIGs', - disabled: false, - handler: function() { - uploadStigs(); - } - } - ], - bbar: new Ext.Toolbar({ - items: [ - { - xtype: 'exportbutton', - hasMenu: false, - gridBasename: 'Installed-STIGs', - exportType: 'grid', - iconCls: 'sm-export-icon', - text: 'CSV' - }, - { - xtype: 'tbfill' - },{ - xtype: 'tbseparator' - }, - totalTextCmp] - }), loadMask: true - }); + }) var thisTab = Ext.getCmp('main-tab-panel').add({ id: 'stig-admin-tab',