mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
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:
parent
57d959b5ea
commit
a8a7be7f20
24 changed files with 361 additions and 11 deletions
62
cli/serve.go
62
cli/serve.go
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
1
frontend/components/packs/PackPageWrapper/index.js
Normal file
1
frontend/components/packs/PackPageWrapper/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export default from './PackPageWrapper';
|
||||
|
|
@ -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" />
|
||||
|
||||
What'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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export default from './PackInfoSidePanel';
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
10
frontend/interfaces/pack.js
Normal file
10
frontend/interfaces/pack.js
Normal 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,
|
||||
});
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
97
frontend/pages/packs/AllPacksPage/AllPacksPage.jsx
Normal file
97
frontend/pages/packs/AllPacksPage/AllPacksPage.jsx
Normal 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);
|
||||
19
frontend/pages/packs/AllPacksPage/_styles.scss
Normal file
19
frontend/pages/packs/AllPacksPage/_styles.scss
Normal 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%;
|
||||
}
|
||||
1
frontend/pages/packs/AllPacksPage/index.js
Normal file
1
frontend/pages/packs/AllPacksPage/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export default from './AllPacksPage';
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
3
frontend/redux/nodes/entities/packs/actions.js
Normal file
3
frontend/redux/nodes/entities/packs/actions.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import config from './config';
|
||||
|
||||
export default config.actions;
|
||||
11
frontend/redux/nodes/entities/packs/config.js
Normal file
11
frontend/redux/nodes/entities/packs/config.js
Normal 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,
|
||||
});
|
||||
3
frontend/redux/nodes/entities/packs/reducer.js
Normal file
3
frontend/redux/nodes/entities/packs/reducer.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import config from './config';
|
||||
|
||||
export default config.reducer;
|
||||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -47,11 +47,6 @@ type QueryPayload struct {
|
|||
Version *string
|
||||
}
|
||||
|
||||
type PackPayload struct {
|
||||
Name *string
|
||||
Platform *string
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
UpdateCreateTimestamps
|
||||
DeleteFields
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue