2016-11-07 16:42:39 +00:00
|
|
|
import React, { Component, PropTypes } from 'react';
|
|
|
|
|
import { connect } from 'react-redux';
|
|
|
|
|
import { push } from 'react-router-redux';
|
2016-12-21 17:07:13 +00:00
|
|
|
import { first, isEqual, values } from 'lodash';
|
2017-01-12 18:28:46 +00:00
|
|
|
import classnames from 'classnames';
|
2016-11-07 16:42:39 +00:00
|
|
|
|
2016-12-21 17:07:13 +00:00
|
|
|
import Kolide from 'kolide';
|
|
|
|
|
import campaignActions from 'redux/nodes/entities/campaigns/actions';
|
|
|
|
|
import campaignInterface from 'interfaces/campaign';
|
2016-11-07 16:42:39 +00:00
|
|
|
import debounce from 'utilities/debounce';
|
|
|
|
|
import entityGetter from 'redux/utilities/entityGetter';
|
2016-12-21 17:07:13 +00:00
|
|
|
import { formatSelectedTargetsForApi } from 'kolide/helpers';
|
2017-01-06 00:01:17 +00:00
|
|
|
import QueryForm from 'components/forms/queries/QueryForm';
|
2016-11-07 16:42:39 +00:00
|
|
|
import osqueryTableInterface from 'interfaces/osquery_table';
|
|
|
|
|
import queryActions from 'redux/nodes/entities/queries/actions';
|
|
|
|
|
import queryInterface from 'interfaces/query';
|
2016-12-21 17:07:13 +00:00
|
|
|
import QueryResultsTable from 'components/queries/QueryResultsTable';
|
2016-11-07 16:42:39 +00:00
|
|
|
import QuerySidePanel from 'components/side_panels/QuerySidePanel';
|
|
|
|
|
import { renderFlash } from 'redux/nodes/notifications/actions';
|
2017-01-06 00:01:17 +00:00
|
|
|
import { selectOsqueryTable, setSelectedTargets, setSelectedTargetsQuery } from 'redux/nodes/components/QueryPages/actions';
|
2016-11-07 16:42:39 +00:00
|
|
|
import targetInterface from 'interfaces/target';
|
2016-11-17 17:12:41 +00:00
|
|
|
import validateQuery from 'components/forms/validators/validate_query';
|
2017-01-12 18:28:46 +00:00
|
|
|
import Spinner from 'components/loaders/Spinner';
|
2016-11-07 16:42:39 +00:00
|
|
|
|
2017-01-09 23:13:52 +00:00
|
|
|
const baseClass = 'query-page';
|
|
|
|
|
|
2016-11-07 16:42:39 +00:00
|
|
|
class QueryPage extends Component {
|
|
|
|
|
static propTypes = {
|
2016-12-21 17:07:13 +00:00
|
|
|
campaign: campaignInterface,
|
2016-11-07 16:42:39 +00:00
|
|
|
dispatch: PropTypes.func,
|
2017-01-06 00:01:17 +00:00
|
|
|
errors: PropTypes.shape({
|
|
|
|
|
base: PropTypes.string,
|
|
|
|
|
}),
|
2016-11-07 16:42:39 +00:00
|
|
|
query: queryInterface,
|
|
|
|
|
selectedOsqueryTable: osqueryTableInterface,
|
|
|
|
|
selectedTargets: PropTypes.arrayOf(targetInterface),
|
|
|
|
|
};
|
|
|
|
|
|
2016-11-21 15:38:23 +00:00
|
|
|
constructor (props) {
|
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
|
|
this.state = {
|
2016-12-21 17:07:13 +00:00
|
|
|
queryIsRunning: false,
|
2016-12-13 23:59:59 +00:00
|
|
|
targetsCount: 0,
|
2016-11-21 15:38:23 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2016-12-21 17:07:13 +00:00
|
|
|
componentWillUnmount () {
|
|
|
|
|
const { destroyCampaign, removeSocket } = this;
|
|
|
|
|
|
|
|
|
|
removeSocket();
|
|
|
|
|
destroyCampaign();
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-21 15:38:23 +00:00
|
|
|
onFetchTargets = (query, targetResponse) => {
|
|
|
|
|
const { dispatch } = this.props;
|
|
|
|
|
const {
|
2016-12-13 23:59:59 +00:00
|
|
|
targets_count: targetsCount,
|
2016-11-21 15:38:23 +00:00
|
|
|
} = targetResponse;
|
|
|
|
|
|
|
|
|
|
dispatch(setSelectedTargetsQuery(query));
|
2016-12-13 23:59:59 +00:00
|
|
|
this.setState({ targetsCount });
|
2016-11-14 17:32:13 +00:00
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-07 16:42:39 +00:00
|
|
|
onOsqueryTableSelect = (tableName) => {
|
|
|
|
|
const { dispatch } = this.props;
|
|
|
|
|
|
|
|
|
|
dispatch(selectOsqueryTable(tableName));
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-09 20:32:23 +00:00
|
|
|
onRunQuery = debounce((queryText) => {
|
|
|
|
|
const { dispatch, selectedTargets } = this.props;
|
|
|
|
|
const { error } = validateQuery(queryText);
|
2016-11-07 16:42:39 +00:00
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
dispatch(renderFlash('error', error));
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2016-12-21 17:07:13 +00:00
|
|
|
const { create, update } = campaignActions;
|
|
|
|
|
const { destroyCampaign, removeSocket } = this;
|
|
|
|
|
const selected = formatSelectedTargetsForApi(selectedTargets);
|
|
|
|
|
|
|
|
|
|
removeSocket();
|
|
|
|
|
destroyCampaign();
|
|
|
|
|
|
2017-01-09 20:32:23 +00:00
|
|
|
dispatch(create({ query: queryText, selected }))
|
2016-12-21 17:07:13 +00:00
|
|
|
.then((campaignResponse) => {
|
|
|
|
|
return Kolide.runQueryWebsocket(campaignResponse.id)
|
|
|
|
|
.then((socket) => {
|
|
|
|
|
this.campaign = campaignResponse;
|
|
|
|
|
this.socket = socket;
|
|
|
|
|
this.setState({ queryIsRunning: true });
|
|
|
|
|
|
|
|
|
|
this.socket.onmessage = ({ data }) => {
|
|
|
|
|
const socketData = JSON.parse(data);
|
|
|
|
|
const { previousSocketData } = this;
|
|
|
|
|
|
|
|
|
|
if (previousSocketData && isEqual(socketData, previousSocketData)) {
|
|
|
|
|
this.previousSocketData = socketData;
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return dispatch(update(this.campaign, socketData))
|
|
|
|
|
.then((updatedCampaign) => {
|
|
|
|
|
this.previousSocketData = socketData;
|
|
|
|
|
this.campaign = updatedCampaign;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.catch((campaignError) => {
|
|
|
|
|
if (campaignError === 'resource already created') {
|
|
|
|
|
dispatch(renderFlash('error', 'A campaign with the provided query text has already been created'));
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dispatch(renderFlash('error', campaignError));
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
});
|
2016-11-07 16:42:39 +00:00
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onSaveQueryFormSubmit = debounce((formData) => {
|
2017-01-06 00:01:17 +00:00
|
|
|
const { dispatch } = this.props;
|
|
|
|
|
const { error } = validateQuery(formData.query);
|
2016-11-07 16:42:39 +00:00
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
dispatch(renderFlash('error', error));
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-06 00:01:17 +00:00
|
|
|
return dispatch(queryActions.create(formData))
|
2016-11-07 16:42:39 +00:00
|
|
|
.then((query) => {
|
2016-11-08 14:23:25 +00:00
|
|
|
dispatch(push(`/queries/${query.id}`));
|
|
|
|
|
dispatch(renderFlash('success', 'Query created'));
|
2016-11-07 16:42:39 +00:00
|
|
|
})
|
2017-01-06 00:01:17 +00:00
|
|
|
.catch(() => false);
|
2016-11-07 16:42:39 +00:00
|
|
|
})
|
|
|
|
|
|
2016-12-21 17:07:13 +00:00
|
|
|
onStopQuery = (evt) => {
|
|
|
|
|
evt.preventDefault();
|
|
|
|
|
|
|
|
|
|
const { removeSocket } = this;
|
|
|
|
|
|
|
|
|
|
this.setState({ queryIsRunning: false });
|
|
|
|
|
|
|
|
|
|
return removeSocket();
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-07 16:42:39 +00:00
|
|
|
onTargetSelect = (selectedTargets) => {
|
|
|
|
|
const { dispatch } = this.props;
|
|
|
|
|
|
|
|
|
|
dispatch(setSelectedTargets(selectedTargets));
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onUpdateQuery = (formData) => {
|
2016-11-08 14:23:25 +00:00
|
|
|
const { dispatch, query } = this.props;
|
2016-11-07 16:42:39 +00:00
|
|
|
|
2016-11-08 14:23:25 +00:00
|
|
|
dispatch(queryActions.update(query, formData))
|
|
|
|
|
.then(() => {
|
|
|
|
|
dispatch(renderFlash('success', 'Query updated!'));
|
|
|
|
|
});
|
2016-11-07 16:42:39 +00:00
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
2016-12-21 17:07:13 +00:00
|
|
|
destroyCampaign = () => {
|
|
|
|
|
const { campaign, dispatch } = this.props;
|
|
|
|
|
const { destroy } = campaignActions;
|
|
|
|
|
|
|
|
|
|
if (campaign) {
|
|
|
|
|
this.campaign = null;
|
|
|
|
|
dispatch(destroy(campaign));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
removeSocket = () => {
|
|
|
|
|
if (this.socket) {
|
|
|
|
|
this.socket.close();
|
|
|
|
|
this.socket = null;
|
|
|
|
|
this.previousSocketData = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-12 18:28:46 +00:00
|
|
|
renderResultsTable = () => {
|
|
|
|
|
const { campaign } = this.props;
|
|
|
|
|
const resultsClasses = classnames(`${baseClass}__results`, 'body-wrap', {
|
|
|
|
|
[`${baseClass}__results--loading`]: this.socket && !campaign.query_results,
|
|
|
|
|
});
|
|
|
|
|
let resultBody = '';
|
|
|
|
|
|
|
|
|
|
if (!campaign || (!this.socket && !campaign.query_results)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ((!campaign.query_results || campaign.query_results.length < 1) && this.socket) {
|
|
|
|
|
resultBody = <Spinner />;
|
|
|
|
|
} else {
|
|
|
|
|
resultBody = <QueryResultsTable campaign={campaign} />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className={resultsClasses}>
|
|
|
|
|
{resultBody}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-07 16:42:39 +00:00
|
|
|
render () {
|
|
|
|
|
const {
|
2016-11-21 15:38:23 +00:00
|
|
|
onFetchTargets,
|
2016-11-07 16:42:39 +00:00
|
|
|
onOsqueryTableSelect,
|
|
|
|
|
onRunQuery,
|
|
|
|
|
onSaveQueryFormSubmit,
|
2016-12-21 17:07:13 +00:00
|
|
|
onStopQuery,
|
2016-11-07 16:42:39 +00:00
|
|
|
onTargetSelect,
|
|
|
|
|
onTextEditorInputChange,
|
|
|
|
|
onUpdateQuery,
|
2017-01-12 18:28:46 +00:00
|
|
|
renderResultsTable,
|
2016-11-07 16:42:39 +00:00
|
|
|
} = this;
|
2016-12-21 17:07:13 +00:00
|
|
|
const { queryIsRunning, targetsCount } = this.state;
|
2016-11-07 16:42:39 +00:00
|
|
|
const {
|
2017-01-06 00:01:17 +00:00
|
|
|
errors,
|
2016-11-07 16:42:39 +00:00
|
|
|
query,
|
|
|
|
|
selectedOsqueryTable,
|
|
|
|
|
selectedTargets,
|
|
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
|
|
return (
|
2017-01-09 23:13:52 +00:00
|
|
|
<div className={`${baseClass} has-sidebar`}>
|
|
|
|
|
<div className={`${baseClass}__content`}>
|
|
|
|
|
<div className={`${baseClass}__form body-wrap`}>
|
|
|
|
|
<QueryForm
|
|
|
|
|
formData={query}
|
|
|
|
|
handleSubmit={onSaveQueryFormSubmit}
|
|
|
|
|
onFetchTargets={onFetchTargets}
|
|
|
|
|
onOsqueryTableSelect={onOsqueryTableSelect}
|
|
|
|
|
onRunQuery={onRunQuery}
|
|
|
|
|
onStopQuery={onStopQuery}
|
|
|
|
|
onTargetSelect={onTargetSelect}
|
|
|
|
|
onUpdate={onUpdateQuery}
|
|
|
|
|
queryIsRunning={queryIsRunning}
|
|
|
|
|
selectedTargets={selectedTargets}
|
|
|
|
|
serverErrors={errors}
|
|
|
|
|
targetsCount={targetsCount}
|
|
|
|
|
selectedOsqueryTable={selectedOsqueryTable}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2017-01-12 18:28:46 +00:00
|
|
|
{renderResultsTable()}
|
2017-01-09 23:13:52 +00:00
|
|
|
</div>
|
2016-11-07 16:42:39 +00:00
|
|
|
<QuerySidePanel
|
|
|
|
|
onOsqueryTableSelect={onOsqueryTableSelect}
|
|
|
|
|
onTextEditorInputChange={onTextEditorInputChange}
|
|
|
|
|
selectedOsqueryTable={selectedOsqueryTable}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const mapStateToProps = (state, { params }) => {
|
|
|
|
|
const { id: queryID } = params;
|
2016-12-21 17:07:13 +00:00
|
|
|
const { entities: campaigns } = entityGetter(state).get('campaigns');
|
2017-01-06 00:01:17 +00:00
|
|
|
const reduxQuery = entityGetter(state).get('queries').findBy({ id: queryID });
|
2016-11-21 15:38:23 +00:00
|
|
|
const { queryText, selectedOsqueryTable, selectedTargets } = state.components.QueryPages;
|
2016-12-21 17:07:13 +00:00
|
|
|
const campaign = first(values(campaigns));
|
2017-01-06 00:01:17 +00:00
|
|
|
const { errors } = state.entities.queries;
|
|
|
|
|
const queryStub = { description: '', name: '', query: queryText };
|
|
|
|
|
const query = reduxQuery || queryStub;
|
2016-11-07 16:42:39 +00:00
|
|
|
|
2017-01-06 00:01:17 +00:00
|
|
|
return { campaign, errors, query, selectedOsqueryTable, selectedTargets };
|
2016-11-07 16:42:39 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default connect(mapStateToProps)(QueryPage);
|