diff --git a/cli/serve.go b/cli/serve.go index fc6a3d9a0c..eee26f92a0 100644 --- a/cli/serve.go +++ b/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") + } +} diff --git a/frontend/components/packs/PackPageWrapper/PackPageWrapper.jsx b/frontend/components/packs/PackPageWrapper/PackPageWrapper.jsx new file mode 100644 index 0000000000..644c4f6e60 --- /dev/null +++ b/frontend/components/packs/PackPageWrapper/PackPageWrapper.jsx @@ -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 ( +
+ {children} +
+ ); + } +} + +export default PackPageWrapper; diff --git a/frontend/components/packs/PackPageWrapper/index.js b/frontend/components/packs/PackPageWrapper/index.js new file mode 100644 index 0000000000..12048233e3 --- /dev/null +++ b/frontend/components/packs/PackPageWrapper/index.js @@ -0,0 +1 @@ +export default from './PackPageWrapper'; diff --git a/frontend/components/side_panels/PackInfoSidePanel/PackInfoSidePanel.jsx b/frontend/components/side_panels/PackInfoSidePanel/PackInfoSidePanel.jsx new file mode 100644 index 0000000000..c6f6fc78e2 --- /dev/null +++ b/frontend/components/side_panels/PackInfoSidePanel/PackInfoSidePanel.jsx @@ -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 ( + +
+

+ +   + What's a Query Pack? +

+
+
+

+ Osquery supports grouping of queries (called query packs) + which run on a scheduled basis and log the results to a configurable + destination. +

+

+ 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 + (interval = 3600s). +

+

+ Queries can be run in two modes: +

+

+ -Differential: +

+

+ Only record data that has changed. +

+

+ -Snapshot: +

+

+ Record full query result each time. +

+

+ Packs are distributed to specified targets. Targets may be + individual hosts or groups of hosts called labels. +

+

+ Learn more about Query Packs in the + + documentation + . +

+
+ ); + } +} + +export default PackInfoSidePanel; diff --git a/frontend/components/side_panels/PackInfoSidePanel/_styles.scss b/frontend/components/side_panels/PackInfoSidePanel/_styles.scss new file mode 100644 index 0000000000..acbb21331a --- /dev/null +++ b/frontend/components/side_panels/PackInfoSidePanel/_styles.scss @@ -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; +} diff --git a/frontend/components/side_panels/PackInfoSidePanel/index.js b/frontend/components/side_panels/PackInfoSidePanel/index.js new file mode 100644 index 0000000000..ad2e4ab02c --- /dev/null +++ b/frontend/components/side_panels/PackInfoSidePanel/index.js @@ -0,0 +1 @@ +export default from './PackInfoSidePanel'; diff --git a/frontend/components/side_panels/SiteNavSidePanel/navItems.js b/frontend/components/side_panels/SiteNavSidePanel/navItems.js index 717fa711a7..f180958a81 100644 --- a/frontend/components/side_panels/SiteNavSidePanel/navItems.js +++ b/frontend/components/side_panels/SiteNavSidePanel/navItems.js @@ -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', diff --git a/frontend/interfaces/pack.js b/frontend/interfaces/pack.js new file mode 100644 index 0000000000..4df4491130 --- /dev/null +++ b/frontend/interfaces/pack.js @@ -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, +}); diff --git a/frontend/kolide/endpoints.js b/frontend/kolide/endpoints.js index 300e379cc6..6966469dbd 100644 --- a/frontend/kolide/endpoints.js +++ b/frontend/kolide/endpoints.js @@ -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', diff --git a/frontend/kolide/index.js b/frontend/kolide/index.js index 32978518e2..173c01e059 100644 --- a/frontend/kolide/index.js +++ b/frontend/kolide/index.js @@ -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; diff --git a/frontend/pages/packs/AllPacksPage/AllPacksPage.jsx b/frontend/pages/packs/AllPacksPage/AllPacksPage.jsx new file mode 100644 index 0000000000..1c70b88ef0 --- /dev/null +++ b/frontend/pages/packs/AllPacksPage/AllPacksPage.jsx @@ -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 ( + + {pack.name} + 0? + Enabled? + Jason Meller? + 0? + Yesterday? + + ); + } + + render () { + const { goToNewPackPage, renderPack } = this; + const { packs } = this.props; + + return ( +
+
+

+ Query Packs +

+
+
+ + + + + + + + + + + + + {packs.map((pack) => { + return renderPack(pack); + })} + +
NameQueriesStatusAuthorNumber of HostsLast Updated
+
+
+ ); + } +} + +const mapStateToProps = (state) => { + const { entities: packs } = entityGetter(state).get('packs'); + + return { packs }; +}; + +export default connect(mapStateToProps)(AllPacksPage); diff --git a/frontend/pages/packs/AllPacksPage/_styles.scss b/frontend/pages/packs/AllPacksPage/_styles.scss new file mode 100644 index 0000000000..63145f800d --- /dev/null +++ b/frontend/pages/packs/AllPacksPage/_styles.scss @@ -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%; + } \ No newline at end of file diff --git a/frontend/pages/packs/AllPacksPage/index.js b/frontend/pages/packs/AllPacksPage/index.js new file mode 100644 index 0000000000..5a6546df7a --- /dev/null +++ b/frontend/pages/packs/AllPacksPage/index.js @@ -0,0 +1 @@ +export default from './AllPacksPage'; diff --git a/frontend/redux/nodes/entities/base/schemas.js b/frontend/redux/nodes/entities/base/schemas.js index 5165a7c82d..95847ba737 100644 --- a/frontend/redux/nodes/entities/base/schemas.js +++ b/frontend/redux/nodes/entities/base/schemas.js @@ -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, diff --git a/frontend/redux/nodes/entities/packs/actions.js b/frontend/redux/nodes/entities/packs/actions.js new file mode 100644 index 0000000000..b95c41eb89 --- /dev/null +++ b/frontend/redux/nodes/entities/packs/actions.js @@ -0,0 +1,3 @@ +import config from './config'; + +export default config.actions; diff --git a/frontend/redux/nodes/entities/packs/config.js b/frontend/redux/nodes/entities/packs/config.js new file mode 100644 index 0000000000..c69ad5ad4c --- /dev/null +++ b/frontend/redux/nodes/entities/packs/config.js @@ -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, +}); diff --git a/frontend/redux/nodes/entities/packs/reducer.js b/frontend/redux/nodes/entities/packs/reducer.js new file mode 100644 index 0000000000..93a1bd364b --- /dev/null +++ b/frontend/redux/nodes/entities/packs/reducer.js @@ -0,0 +1,3 @@ +import config from './config'; + +export default config.reducer; diff --git a/frontend/redux/nodes/entities/reducer.js b/frontend/redux/nodes/entities/reducer.js index 9da0ab2959..efa5cc9f3b 100644 --- a/frontend/redux/nodes/entities/reducer.js +++ b/frontend/redux/nodes/entities/reducer.js @@ -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, }); diff --git a/frontend/router/index.jsx b/frontend/router/index.jsx index 68f13a9c69..09d9e20272 100644 --- a/frontend/router/index.jsx +++ b/frontend/router/index.jsx @@ -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 = ( + + + diff --git a/frontend/router/paths.js b/frontend/router/paths.js index 93aecf47f3..aa772b079a 100644 --- a/frontend/router/paths.js +++ b/frontend/router/paths.js @@ -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', }; diff --git a/server/kolide/packs.go b/server/kolide/packs.go index cf66487536..29a753287f 100644 --- a/server/kolide/packs.go +++ b/server/kolide/packs.go @@ -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 { diff --git a/server/kolide/queries.go b/server/kolide/queries.go index 93878b3fcd..a64b25ae2a 100644 --- a/server/kolide/queries.go +++ b/server/kolide/queries.go @@ -47,11 +47,6 @@ type QueryPayload struct { Version *string } -type PackPayload struct { - Name *string - Platform *string -} - type Query struct { UpdateCreateTimestamps DeleteFields diff --git a/server/service/service_packs.go b/server/service/service_packs.go index 4fcdde4e4f..fba0567ab5 100644 --- a/server/service/service_packs.go +++ b/server/service/service_packs.go @@ -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 } diff --git a/server/service/transport_packs_test.go b/server/service/transport_packs_test.go index 9a198b6f2c..6dcbc9bee6 100644 --- a/server/service/transport_packs_test.go +++ b/server/service/transport_packs_test.go @@ -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(