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
+
+
+
+
+
+
+
+ Name
+ Queries
+ Status
+ Author
+ Number of Hosts
+ Last Updated
+
+
+
+ {packs.map((pack) => {
+ return renderPack(pack);
+ })}
+
+
+
+
+ );
+ }
+}
+
+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(