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:
Mike Stone 2016-10-21 17:58:13 -04:00 committed by GitHub
parent 10c32bc47f
commit aa275751b2
22 changed files with 214 additions and 89 deletions

View file

@ -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);
});
});

View file

@ -81,7 +81,7 @@ class SaveQueryForm extends Component {
const { validate } = this;
if (validate(runType)) {
return onSubmit({ formData, runType });
return onSubmit({ ...formData, runType });
}
return false;

View file

@ -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,
});
});
});

View file

@ -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 });

View 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();
});
});
});

View 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 };

View 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();
});
});
});

View file

@ -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}

View file

@ -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';

View file

@ -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",