Packs pages (#426)

* initial scaffolding

* pack info sidebar

* fixing the merge of the routes

* Remove radium from pack info sidepanel

* lint

* cards!

* redux entity config

* pack interface

* wiring up redux with fake dev data

* Add description attribute to packs

* move redux to top level page component to isolate data fetching

* initial scaffolding of all packs table

* adding redux entities back

* minimal

* alpha order in packs.js

* no newlines in HTML

* onclick handler to function on component class

* alpha order in router

* alpha order in paths.js

* no newline in side panel

* removing input field

* lint fixes
This commit is contained in:
Mike Arpaia 2016-11-21 11:49:36 -08:00 committed by GitHub
parent 57d959b5ea
commit a8a7be7f20
24 changed files with 361 additions and 11 deletions

View file

@ -93,6 +93,7 @@ the way that the kolide server works.
createDevQueries(ds, config)
createDevLabels(ds, config)
createDevOrgInfo(ds, config)
createDevPacksAndQueries(ds, config)
}
fieldKeys := []string{"method", "error"}
@ -423,3 +424,64 @@ func createDevLabels(ds kolide.Datastore, config config.KolideConfig) {
}
}
}
func createDevPacksAndQueries(ds kolide.Datastore, config config.KolideConfig) {
query1 := &kolide.Query{
Name: "Osquery Info",
Query: "select * from osquery_info",
}
query1, err := ds.NewQuery(query1)
if err != nil {
initFatal(err, "creating dev queries")
}
query2 := &kolide.Query{
Name: "Launchd",
Query: "select * from launchd",
Platform: "darwin",
}
query2, err = ds.NewQuery(query2)
if err != nil {
initFatal(err, "creating dev queries")
}
query3 := &kolide.Query{
Name: "registry",
Query: "select * from osquery_registry",
}
query3, err = ds.NewQuery(query3)
if err != nil {
initFatal(err, "creating dev queries")
}
pack1 := &kolide.Pack{
Name: "Osquery Internal Info",
}
pack1, err = ds.NewPack(pack1)
if err != nil {
initFatal(err, "creating dev packs")
}
pack2 := &kolide.Pack{
Name: "macOS Attacks",
}
pack2, err = ds.NewPack(pack2)
if err != nil {
initFatal(err, "creating dev packs")
}
err = ds.AddQueryToPack(query1.ID, pack1.ID)
if err != nil {
initFatal(err, "creating dev packs")
}
err = ds.AddQueryToPack(query3.ID, pack1.ID)
if err != nil {
initFatal(err, "creating dev packs")
}
err = ds.AddQueryToPack(query2.ID, pack2.ID)
if err != nil {
initFatal(err, "creating dev packs")
}
}

View file

@ -0,0 +1,19 @@
import React, { Component, PropTypes } from 'react';
class PackPageWrapper extends Component {
static propTypes = {
children: PropTypes.node,
};
render () {
const { children } = this.props;
return (
<div>
{children}
</div>
);
}
}
export default PackPageWrapper;

View file

@ -0,0 +1 @@
export default from './PackPageWrapper';

View file

@ -0,0 +1,60 @@
import React from 'react';
import SecondarySidePanelContainer from '../SecondarySidePanelContainer';
const classBlock = 'pack-info-side-panel';
class PackInfoSidePanel extends React.Component {
render () {
return (
<SecondarySidePanelContainer className={classBlock}>
<div>
<h3>
<i className="kolidecon-packs" />
&nbsp;
What&apos;s a Query Pack?
</h3>
</div>
<hr />
<p>
Osquery supports grouping of queries (called <b>query packs</b>)
which run on a scheduled basis and log the results to a configurable
destination.
</p>
<p>
Query Packs are useful for monitoring specific attributes of hosts
over time and can be used for alerting and incident response
investigations. By default, queries added to packs run every hour
(<b>interval = 3600s</b>).
</p>
<p>
Queries can be run in two modes:
</p>
<p>
<b>-Differential:</b>
</p>
<p>
Only record data that has changed.
</p>
<p>
<b>-Snapshot:</b>
</p>
<p>
Record full query result each time.
</p>
<p>
Packs are distributed to specified <b>targets</b>. Targets may be
<b>individual hosts</b> or groups of hosts called <b>labels.</b>
</p>
<p>
Learn more about Query Packs in the
<a href="https://kolide.co">
documentation
</a>.
</p>
</SecondarySidePanelContainer>
);
}
}
export default PackInfoSidePanel;

View file

@ -0,0 +1,13 @@
.pack-info-side-panel {
background-color: $white;
border-left: 1px solid $border-medium;
bottom: 0;
box-shadow: 2px 0 8px 0 rgba($black, 0.1);
box-sizing: border-box;
overflow: scroll;
padding: px-to-rem(13) px-to-rem(13) 70px;
position: fixed;
right: 0;
top: 0;
width: 300px;
}

View file

@ -0,0 +1 @@
export default from './PackInfoSidePanel';

View file

@ -70,12 +70,29 @@ export default (admin) => {
],
},
{
defaultPathname: '/packs/all',
icon: 'kolidecon-packs',
name: 'Packs',
path: {
regex: /^\/packs/,
location: '/packs/all',
},
subItems: [],
subItems: [
{
name: 'All Packs',
path: {
regex: /\/all/,
location: '/packs/all',
},
},
{
name: 'Pack Composer',
path: {
regex: /\/new/,
location: '/packs/new',
},
},
],
},
{
icon: 'kolidecon-help',

View file

@ -0,0 +1,10 @@
import { PropTypes } from 'react';
export default PropTypes.shape({
description: PropTypes.string,
detail_updated_at: PropTypes.string,
id: PropTypes.number,
name: PropTypes.string,
platform: PropTypes.string,
updated_at: PropTypes.string,
});

View file

@ -10,6 +10,7 @@ export default {
LOGIN: '/v1/kolide/login',
LOGOUT: '/v1/kolide/logout',
ME: '/v1/kolide/me',
PACKS: '/v1/kolide/packs',
QUERIES: '/v1/kolide/queries',
RESET_PASSWORD: '/v1/kolide/reset_password',
TARGETS: '/v1/kolide/targets',

View file

@ -140,6 +140,13 @@ class Kolide extends Base {
});
}
getPacks = () => {
const { PACKS } = endpoints;
return this.authenticatedGet(this.endpoint(PACKS))
.then((response) => { return response.packs; });
}
getUsers = () => {
const { USERS } = endpoints;

View file

@ -0,0 +1,97 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { push } from 'react-router-redux';
import Button from 'components/buttons/Button';
import entityGetter from 'redux/utilities/entityGetter';
import packActions from 'redux/nodes/entities/packs/actions';
import packInterface from 'interfaces/pack';
import paths from 'router/paths';
const baseClass = 'all-packs-page';
class AllPacksPage extends Component {
static propTypes = {
dispatch: PropTypes.func,
packs: PropTypes.arrayOf(packInterface),
}
componentWillMount() {
const { dispatch, packs } = this.props;
if (!packs.length) {
dispatch(packActions.loadAll());
}
return false;
}
goToNewPackPage = () => {
const { dispatch } = this.props;
const { NEW_PACK } = paths;
dispatch(push(NEW_PACK));
return false;
}
renderPack = (pack) => {
return (
<tr key={`pack-${pack.id}-table`}>
<td>{pack.name}</td>
<td>0?</td>
<td>Enabled?</td>
<td>Jason Meller?</td>
<td>0?</td>
<td>Yesterday?</td>
</tr>
);
}
render () {
const { goToNewPackPage, renderPack } = this;
const { packs } = this.props;
return (
<div>
<div className={`${baseClass}__wrapper`}>
<p className={`${baseClass}__title`}>
Query Packs
</p>
<div className={`${baseClass}__new_pack`}>
<Button
text="CREATE NEW PACK"
variant="brand"
onClick={goToNewPackPage}
/>
</div>
<table className={`${baseClass}__table`}>
<thead>
<tr>
<th>Name</th>
<th>Queries</th>
<th>Status</th>
<th>Author</th>
<th>Number of Hosts</th>
<th>Last Updated</th>
</tr>
</thead>
<tbody>
{packs.map((pack) => {
return renderPack(pack);
})}
</tbody>
</table>
</div>
</div>
);
}
}
const mapStateToProps = (state) => {
const { entities: packs } = entityGetter(state).get('packs');
return { packs };
};
export default connect(mapStateToProps)(AllPacksPage);

View file

@ -0,0 +1,19 @@
.all-packs-page {
&__title {
color: $text-medium;
display: inline-block;
font-size: $large;
}
&__wrapper {
background-color: $white;
padding: $base;
}
}
&__table {
border-collapse: collapse;
color: $text-medium;
font-size: $small;
width: 100%;
}

View file

@ -0,0 +1 @@
export default from './AllPacksPage';

View file

@ -3,6 +3,7 @@ import { Schema } from 'normalizr';
const hostsSchema = new Schema('hosts');
const invitesSchema = new Schema('invites');
const labelsSchema = new Schema('labels');
const packsSchema = new Schema('packs');
const queriesSchema = new Schema('queries');
const targetsSchema = new Schema('targets');
const usersSchema = new Schema('users');
@ -11,6 +12,7 @@ export default {
HOSTS: hostsSchema,
INVITES: invitesSchema,
LABELS: labelsSchema,
PACKS: packsSchema,
QUERIES: queriesSchema,
TARGETS: targetsSchema,
USERS: usersSchema,

View file

@ -0,0 +1,3 @@
import config from './config';
export default config.actions;

View file

@ -0,0 +1,11 @@
import Kolide from '../../../../kolide';
import reduxConfig from '../base/reduxConfig';
import schemas from '../base/schemas';
const { PACKS: schema } = schemas;
export default reduxConfig({
entityName: 'packs',
loadAllFunc: Kolide.getPacks,
schema,
});

View file

@ -0,0 +1,3 @@
import config from './config';
export default config.reducer;

View file

@ -3,6 +3,7 @@ import { combineReducers } from 'redux';
import hosts from './hosts/reducer';
import invites from './invites/reducer';
import labels from './labels/reducer';
import packs from './packs/reducer';
import queries from './queries/reducer';
import users from './users/reducer';
@ -10,6 +11,7 @@ export default combineReducers({
hosts,
invites,
labels,
packs,
queries,
users,
});

View file

@ -4,6 +4,7 @@ import { Provider } from 'react-redux';
import { syncHistoryWithStore } from 'react-router-redux';
import AdminUserManagementPage from 'pages/Admin/UserManagementPage';
import AllPacksPage from 'pages/packs/AllPacksPage';
import App from 'components/App';
import AuthenticatedAdminRoutes from 'components/AuthenticatedAdminRoutes';
import AuthenticatedRoutes from 'components/AuthenticatedRoutes';
@ -18,6 +19,7 @@ import QueryPage from 'pages/queries/QueryPage';
import QueryPageWrapper from 'components/queries/QueryPageWrapper';
import RegistrationPage from 'pages/RegistrationPage';
import ResetPasswordPage from 'pages/ResetPasswordPage';
import PackPageWrapper from 'components/packs/PackPageWrapper';
import store from 'redux/store';
const history = syncHistoryWithStore(browserHistory, store);
@ -42,6 +44,9 @@ const routes = (
<Route path="new" component={QueryPage} />
<Route path=":id" component={QueryPage} />
</Route>
<Route path="packs" component={PackPageWrapper}>
<Route path="all" component={AllPacksPage} />
</Route>
<Route path="hosts">
<Route path="new" component={NewHostPage} />
<Route path="manage" component={ManageHostsPage} />

View file

@ -1,8 +1,10 @@
export default {
ADMIN_DASHBOARD: '/admin',
ALL_PACKS: '/packs/all',
FORGOT_PASSWORD: '/login/forgot',
HOME: '/',
LOGIN: '/login',
LOGOUT: '/logout',
NEW_PACK: '/packs/new',
RESET_PASSWORD: '/login/reset',
};

View file

@ -46,9 +46,16 @@ type PackService interface {
type Pack struct {
UpdateCreateTimestamps
DeleteFields
ID uint `json:"id"`
Name string `json:"name"`
Platform string `json:"platform"`
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Platform string `json:"platform"`
}
type PackPayload struct {
Name *string
Description *string
Platform *string
}
type PackQuery struct {

View file

@ -47,11 +47,6 @@ type QueryPayload struct {
Version *string
}
type PackPayload struct {
Name *string
Platform *string
}
type Query struct {
UpdateCreateTimestamps
DeleteFields

View file

@ -20,6 +20,10 @@ func (svc service) NewPack(ctx context.Context, p kolide.PackPayload) (*kolide.P
pack.Name = *p.Name
}
if p.Description != nil {
pack.Description = *p.Description
}
if p.Platform != nil {
pack.Platform = *p.Platform
}
@ -41,6 +45,10 @@ func (svc service) ModifyPack(ctx context.Context, id uint, p kolide.PackPayload
pack.Name = *p.Name
}
if p.Description != nil {
pack.Description = *p.Description
}
if p.Platform != nil {
pack.Platform = *p.Platform
}

View file

@ -19,11 +19,13 @@ func TestDecodeCreatePackRequest(t *testing.T) {
params := r.(createPackRequest)
assert.Equal(t, "foo", *params.payload.Name)
assert.Equal(t, "bar", *params.payload.Description)
}).Methods("POST")
var body bytes.Buffer
body.Write([]byte(`{
"name": "foo"
"name": "foo",
"description": "bar"
}`))
router.ServeHTTP(
@ -40,12 +42,14 @@ func TestDecodeModifyPackRequest(t *testing.T) {
params := r.(modifyPackRequest)
assert.Equal(t, "foo", *params.payload.Name)
assert.Equal(t, "bar", *params.payload.Description)
assert.Equal(t, uint(1), params.ID)
}).Methods("PATCH")
var body bytes.Buffer
body.Write([]byte(`{
"name": "foo"
"name": "foo",
"description": "bar"
}`))
router.ServeHTTP(