mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
Client side query validation (#335)
* validate query text * Update structure of submitted SaveQueryForm data * form calls correct prop function when invalid query text * Lowercase directory names
This commit is contained in:
parent
10c32bc47f
commit
aa275751b2
22 changed files with 214 additions and 89 deletions
|
|
@ -1,53 +0,0 @@
|
|||
import React from 'react';
|
||||
import expect, { restoreSpies } from 'expect';
|
||||
import { mount } from 'enzyme';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import { createAceSpy } from '../../../test/helpers';
|
||||
import NewQuery from './index';
|
||||
|
||||
describe('NewQuery - component', () => {
|
||||
beforeEach(() => {
|
||||
createAceSpy();
|
||||
});
|
||||
afterEach(restoreSpies);
|
||||
|
||||
it('renders the SaveQuerySection', () => {
|
||||
const component = mount(
|
||||
<NewQuery
|
||||
onOsqueryTableSelect={noop}
|
||||
onTextEditorInputChange={noop}
|
||||
textEditorText="Hello world"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find('SaveQuerySection').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('renders the ThemeDropdown', () => {
|
||||
const component = mount(
|
||||
<NewQuery
|
||||
onOsqueryTableSelect={noop}
|
||||
onTextEditorInputChange={noop}
|
||||
textEditorText="Hello world"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find('ThemeDropdown').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('renders the SaveQueryForm', () => {
|
||||
const component = mount(
|
||||
<NewQuery
|
||||
onOsqueryTableSelect={noop}
|
||||
onTextEditorInputChange={noop}
|
||||
textEditorText="Hello world"
|
||||
/>
|
||||
);
|
||||
|
||||
component.find('Slider').simulate('click');
|
||||
|
||||
expect(component.find('SaveQueryForm').length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -81,7 +81,7 @@ class SaveQueryForm extends Component {
|
|||
const { validate } = this;
|
||||
|
||||
if (validate(runType)) {
|
||||
return onSubmit({ formData, runType });
|
||||
return onSubmit({ ...formData, runType });
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -55,16 +55,14 @@ describe('SaveQueryForm - component', () => {
|
|||
form.simulate('submit');
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
description: null,
|
||||
duration: 'short',
|
||||
hosts: 'all',
|
||||
hostsPercentage: null,
|
||||
name: queryName,
|
||||
platforms: 'all',
|
||||
runType: 'RUN_AND_SAVE',
|
||||
formData: {
|
||||
description: null,
|
||||
duration: 'short',
|
||||
hosts: 'all',
|
||||
hostsPercentage: null,
|
||||
name: queryName,
|
||||
platforms: 'all',
|
||||
scanInterval: 0,
|
||||
},
|
||||
scanInterval: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -77,16 +75,14 @@ describe('SaveQueryForm - component', () => {
|
|||
form.simulate('submit');
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
description: null,
|
||||
duration: 'short',
|
||||
hosts: 'all',
|
||||
hostsPercentage: null,
|
||||
name: null,
|
||||
platforms: 'all',
|
||||
runType: 'RUN',
|
||||
formData: {
|
||||
description: null,
|
||||
duration: 'short',
|
||||
hosts: 'all',
|
||||
hostsPercentage: null,
|
||||
name: null,
|
||||
platforms: 'all',
|
||||
scanInterval: 0,
|
||||
},
|
||||
scanInterval: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,12 +8,15 @@ import 'react-select/dist/react-select.css';
|
|||
import './mode';
|
||||
import './theme';
|
||||
import componentStyles from './styles';
|
||||
import debounce from '../../../utilities/debounce';
|
||||
import SaveQueryForm from '../../forms/queries/SaveQueryForm';
|
||||
import SaveQuerySection from './SaveQuerySection';
|
||||
import ThemeDropdown from './ThemeDropdown';
|
||||
import { validateQuery } from './helpers';
|
||||
|
||||
class NewQuery extends Component {
|
||||
static propTypes = {
|
||||
onInvalidQuerySubmit: PropTypes.func,
|
||||
onNewQueryFormSubmit: PropTypes.func,
|
||||
onOsqueryTableSelect: PropTypes.func,
|
||||
onTextEditorInputChange: PropTypes.func,
|
||||
|
|
@ -67,17 +70,26 @@ class NewQuery extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
onSaveQueryFormSubmit = (formData) => {
|
||||
const { onNewQueryFormSubmit } = this.props;
|
||||
onSaveQueryFormSubmit = debounce((formData) => {
|
||||
const {
|
||||
onInvalidQuerySubmit,
|
||||
onNewQueryFormSubmit,
|
||||
textEditorText,
|
||||
} = this.props;
|
||||
const { selectedTargets } = this.state;
|
||||
|
||||
onNewQueryFormSubmit({
|
||||
const { error } = validateQuery(textEditorText);
|
||||
|
||||
if (error) {
|
||||
return onInvalidQuerySubmit(error);
|
||||
}
|
||||
|
||||
return onNewQueryFormSubmit({
|
||||
...formData,
|
||||
query: textEditorText,
|
||||
selectedTargets,
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
})
|
||||
|
||||
onTargetSelect = (selectedTargets) => {
|
||||
this.setState({ selectedTargets });
|
||||
92
frontend/components/queries/NewQuery/NewQuery.tests.jsx
Normal file
92
frontend/components/queries/NewQuery/NewQuery.tests.jsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import React from 'react';
|
||||
import expect, { createSpy, restoreSpies } from 'expect';
|
||||
import { mount } from 'enzyme';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import { createAceSpy } from '../../../test/helpers';
|
||||
import NewQuery from './index';
|
||||
|
||||
describe('NewQuery - component', () => {
|
||||
beforeEach(() => {
|
||||
createAceSpy();
|
||||
});
|
||||
afterEach(restoreSpies);
|
||||
|
||||
it('renders the SaveQuerySection', () => {
|
||||
const component = mount(
|
||||
<NewQuery
|
||||
onOsqueryTableSelect={noop}
|
||||
onTextEditorInputChange={noop}
|
||||
textEditorText="Hello world"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find('SaveQuerySection').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('renders the ThemeDropdown', () => {
|
||||
const component = mount(
|
||||
<NewQuery
|
||||
onOsqueryTableSelect={noop}
|
||||
onTextEditorInputChange={noop}
|
||||
textEditorText="Hello world"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find('ThemeDropdown').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('renders the SaveQueryForm', () => {
|
||||
const component = mount(
|
||||
<NewQuery
|
||||
onOsqueryTableSelect={noop}
|
||||
onTextEditorInputChange={noop}
|
||||
textEditorText="Hello world"
|
||||
/>
|
||||
);
|
||||
|
||||
component.find('Slider').simulate('click');
|
||||
|
||||
expect(component.find('SaveQueryForm').length).toEqual(1);
|
||||
});
|
||||
|
||||
describe('Query string validations', () => {
|
||||
const invalidQuery = 'CREATE TABLE users (LastName varchar(255))';
|
||||
const validQuery = 'SELECT * FROM users';
|
||||
|
||||
it('calls onInvalidQuerySubmit when invalid', () => {
|
||||
const invalidQuerySubmitSpy = createSpy();
|
||||
const component = mount(
|
||||
<NewQuery
|
||||
onInvalidQuerySubmit={invalidQuerySubmitSpy}
|
||||
onOsqueryTableSelect={noop}
|
||||
onTextEditorInputChange={noop}
|
||||
textEditorText={invalidQuery}
|
||||
/>
|
||||
);
|
||||
const form = component.find('SaveQueryForm');
|
||||
|
||||
form.simulate('submit');
|
||||
|
||||
expect(invalidQuerySubmitSpy).toHaveBeenCalledWith('Cannot INSERT or CREATE in osquery queries');
|
||||
});
|
||||
|
||||
it('calls onNewQueryFormSubmit when valid', () => {
|
||||
const onNewQueryFormSubmitSpy = createSpy();
|
||||
const component = mount(
|
||||
<NewQuery
|
||||
onNewQueryFormSubmit={onNewQueryFormSubmitSpy}
|
||||
onOsqueryTableSelect={noop}
|
||||
onTextEditorInputChange={noop}
|
||||
textEditorText={validQuery}
|
||||
/>
|
||||
);
|
||||
const form = component.find('SaveQueryForm');
|
||||
|
||||
form.simulate('submit');
|
||||
|
||||
expect(onNewQueryFormSubmitSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
29
frontend/components/queries/NewQuery/helpers.js
Normal file
29
frontend/components/queries/NewQuery/helpers.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import sqliteParser from 'sqlite-parser';
|
||||
import { includes, some } from 'lodash';
|
||||
|
||||
const BLACKLISTED_ACTIONS = ['insert', 'create'];
|
||||
const invalidQueryErrorMessage = 'Cannot INSERT or CREATE in osquery queries';
|
||||
const invalidQueryResponse = (message) => {
|
||||
return { valid: false, error: message };
|
||||
};
|
||||
const validQueryResponse = { valid: true, error: null };
|
||||
|
||||
export const validateQuery = (queryText) => {
|
||||
try {
|
||||
const ast = sqliteParser(queryText);
|
||||
const { statement } = ast;
|
||||
const invalidQuery = some(statement, (obj) => {
|
||||
return includes(BLACKLISTED_ACTIONS, obj.variant.toLowerCase());
|
||||
});
|
||||
|
||||
if (invalidQuery) {
|
||||
return invalidQueryResponse(invalidQueryErrorMessage);
|
||||
}
|
||||
|
||||
return validQueryResponse;
|
||||
} catch (error) {
|
||||
return invalidQueryResponse(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
export default { validateQuery };
|
||||
39
frontend/components/queries/NewQuery/helpers.tests.js
Normal file
39
frontend/components/queries/NewQuery/helpers.tests.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import expect from 'expect';
|
||||
|
||||
import helpers from './helpers';
|
||||
|
||||
const malformedQuery = 'this is not a thing';
|
||||
const validQuery = 'SELECT * FROM users';
|
||||
const createQuery = 'CREATE TABLE users (LastName varchar(255))';
|
||||
const insertQuery = 'INSERT INTO users (name) values ("Mike")';
|
||||
|
||||
describe('NewQuery - helpers', () => {
|
||||
describe('#validateQuery', () => {
|
||||
const { validateQuery } = helpers;
|
||||
|
||||
it('rejects malformed queries', () => {
|
||||
const { error, valid } = validateQuery(malformedQuery);
|
||||
|
||||
expect(valid).toEqual(false);
|
||||
expect(error).toEqual('Syntax error found near WITH Clause (Statement)');
|
||||
});
|
||||
|
||||
it('rejects create queries', () => {
|
||||
const { error, valid } = validateQuery(createQuery);
|
||||
expect(valid).toEqual(false);
|
||||
expect(error).toEqual('Cannot INSERT or CREATE in osquery queries');
|
||||
});
|
||||
|
||||
it('rejects insert queries', () => {
|
||||
const { error, valid } = validateQuery(insertQuery);
|
||||
expect(valid).toEqual(false);
|
||||
expect(error).toEqual('Cannot INSERT or CREATE in osquery queries');
|
||||
});
|
||||
|
||||
it('accepts valid queries', () => {
|
||||
const { error, valid } = validateQuery(validQuery);
|
||||
expect(valid).toEqual(true);
|
||||
expect(error).toNotExist();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -2,10 +2,11 @@ import React, { Component, PropTypes } from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { find } from 'lodash';
|
||||
|
||||
import NewQuery from '../../../components/Queries/NewQuery';
|
||||
import NewQuery from '../../../components/queries/NewQuery';
|
||||
import { osqueryTables } from '../../../utilities/osquery_tables';
|
||||
import QuerySidePanel from '../../../components/side_panels/QuerySidePanel';
|
||||
import { showRightSidePanel, removeRightSidePanel } from '../../../redux/nodes/app/actions';
|
||||
import { osqueryTables } from '../../../utilities/osquery_tables';
|
||||
import { renderFlash } from '../../../redux/nodes/notifications/actions';
|
||||
|
||||
class NewQueryPage extends Component {
|
||||
static propTypes = {
|
||||
|
|
@ -35,13 +36,15 @@ class NewQueryPage extends Component {
|
|||
}
|
||||
|
||||
onNewQueryFormSubmit = (formData) => {
|
||||
const { textEditorText } = this.state;
|
||||
const data = {
|
||||
queryText: textEditorText,
|
||||
...formData,
|
||||
};
|
||||
console.log('New Query Form submitted', formData);
|
||||
}
|
||||
|
||||
console.log('New Query Form submitted', data);
|
||||
onInvalidQuerySubmit = (errorMessage) => {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(renderFlash('error', errorMessage));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onOsqueryTableSelect = (tableName) => {
|
||||
|
|
@ -59,12 +62,18 @@ class NewQueryPage extends Component {
|
|||
|
||||
render () {
|
||||
const { selectedOsqueryTable, textEditorText } = this.state;
|
||||
const { onNewQueryFormSubmit, onOsqueryTableSelect, onTextEditorInputChange } = this;
|
||||
const {
|
||||
onNewQueryFormSubmit,
|
||||
onInvalidQuerySubmit,
|
||||
onOsqueryTableSelect,
|
||||
onTextEditorInputChange,
|
||||
} = this;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NewQuery
|
||||
onNewQueryFormSubmit={onNewQueryFormSubmit}
|
||||
onInvalidQuerySubmit={onInvalidQuerySubmit}
|
||||
onOsqueryTableSelect={onOsqueryTableSelect}
|
||||
onTextEditorInputChange={onTextEditorInputChange}
|
||||
selectedOsqueryTable={selectedOsqueryTable}
|
||||
|
|
@ -15,8 +15,8 @@ import LoginRoutes from '../components/LoginRoutes';
|
|||
import LogoutPage from '../pages/LogoutPage';
|
||||
import ManageHostsPage from '../pages/hosts/ManageHostsPage';
|
||||
import NewHostPage from '../pages/hosts/NewHostPage';
|
||||
import NewQueryPage from '../pages/Queries/NewQueryPage';
|
||||
import QueryPageWrapper from '../components/Queries/QueryPageWrapper';
|
||||
import NewQueryPage from '../pages/queries/NewQueryPage';
|
||||
import QueryPageWrapper from '../components/queries/QueryPageWrapper';
|
||||
import ResetPasswordPage from '../pages/ResetPasswordPage';
|
||||
import store from '../redux/store';
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@
|
|||
"require-hacker": "^2.1.4",
|
||||
"sass-loader": "^4.0.2",
|
||||
"select": "^1.0.6",
|
||||
"sqlite-parser": "^0.14.5",
|
||||
"style-loader": "^0.13.0",
|
||||
"stylus-loader": "1.5.1",
|
||||
"url-loader": "^0.5.7",
|
||||
|
|
|
|||
Loading…
Reference in a new issue